diff --git a/arclight-api/build.gradle b/arclight-api/build.gradle new file mode 100644 index 00000000..9b9b398d --- /dev/null +++ b/arclight-api/build.gradle @@ -0,0 +1,14 @@ +plugins { + id 'java' +} + +repositories { + mavenCentral() +} + +dependencies { +} + +compileJava { + options.compilerArgs << '-XDignore.symbol.file' << '-XDenableSunApiLintControl' +} diff --git a/arclight-coremod/src/main/java/io/izzel/arclight/util/Unsafe.java b/arclight-api/src/main/java/io/izzel/arclight/api/Unsafe.java similarity index 99% rename from arclight-coremod/src/main/java/io/izzel/arclight/util/Unsafe.java rename to arclight-api/src/main/java/io/izzel/arclight/api/Unsafe.java index 29de91db..b2c0c90b 100644 --- a/arclight-coremod/src/main/java/io/izzel/arclight/util/Unsafe.java +++ b/arclight-api/src/main/java/io/izzel/arclight/api/Unsafe.java @@ -1,4 +1,4 @@ -package io.izzel.arclight.util; +package io.izzel.arclight.api; import sun.reflect.CallerSensitive; diff --git a/arclight-common/build.gradle b/arclight-common/build.gradle new file mode 100644 index 00000000..017ef278 --- /dev/null +++ b/arclight-common/build.gradle @@ -0,0 +1,11 @@ +plugins { + id 'java' +} + +repositories { + mavenCentral() +} + +dependencies { + compile project(':arclight-api') +} diff --git a/arclight-coremod/build.gradle b/arclight-coremod/build.gradle index b7961816..ecae450c 100644 --- a/arclight-coremod/build.gradle +++ b/arclight-coremod/build.gradle @@ -16,6 +16,13 @@ apply plugin: 'org.spongepowered.mixin' apply plugin: 'java' apply plugin: 'idea' +ext { + minecraftVersion = '1.14.4' + forgeVersion = '28.2.0' + installerInfoDir = file("$buildDir/installer-info") + installerInfo = file("$installerInfoDir/META-INF/installer.json") +} + sourceCompatibility = targetCompatibility = compileJava.sourceCompatibility = compileJava.targetCompatibility = '1.8' configurations { @@ -25,7 +32,7 @@ configurations { } minecraft { - mappings channel: 'stable', version: '58-1.14.4' + mappings channel: 'stable', version: "58-$minecraftVersion" accessTransformer = project.file('src/main/resources/META-INF/accesstransformer.cfg') runs { server { @@ -55,19 +62,21 @@ repositories { def embedLibs = ['org.spongepowered:mixin:0.8', 'org.ow2.asm:asm-util:6.2', 'org.ow2.asm:asm-analysis:6.2', 'org.yaml:snakeyaml:1.23', - 'net.md-5:bungeecord-chat:1.13-SNAPSHOT', 'org.xerial:sqlite-jdbc:3.28.0', - 'mysql:mysql-connector-java:5.1.47', 'commons-lang:commons-lang:2.6', - 'jline:jline:2.12.1', 'com.googlecode.json-simple:json-simple:1.1.1', - 'org.apache.logging.log4j:log4j-jul:2.11.2', 'net.md-5:SpecialSource:1.8.6', - 'net.minecraftforge:eventbus:2.0.0-milestone.1:service'] + 'org.xerial:sqlite-jdbc:3.28.0', 'mysql:mysql-connector-java:5.1.47', + 'commons-lang:commons-lang:2.6', 'jline:jline:2.12.1', + 'com.googlecode.json-simple:json-simple:1.1.1', 'org.apache.logging.log4j:log4j-jul:2.11.2', + 'net.md-5:SpecialSource:1.8.6', 'net.minecraftforge:eventbus:2.0.0-milestone.1:service'] dependencies { - minecraft 'net.minecraftforge:forge:1.14.4-28.2.0' + minecraft "net.minecraftforge:forge:$minecraftVersion-$forgeVersion" compile group: 'org.jetbrains', name: 'annotations', version: '19.0.0' + embed project(':arclight-common') + embed project(':forge-installer') for (def lib : embedLibs) { embedJar "$lib@jar" } - embed 'org.spigotmc:spigot-api:1.14.4-R0.1-SNAPSHOT@jar' + embed 'net.md-5:bungeecord-chat:1.13-SNAPSHOT@jar' + embed "org.spigotmc:spigot-api:$minecraftVersion-R0.1-SNAPSHOT@jar" embed files("$projectDir/libs/spigot-1.14.4-mapped-deobf.jar") } @@ -82,26 +91,13 @@ def getGitHash = { -> processResources { filesNotMatching("**/accesstransformer.cfg") { - expand 'version': "1.14.4-${project.version}-${getGitHash()}" + expand 'version': "$minecraftVersion-${project.version}-${getGitHash()}" } } -def classpath = { - "libraries/org/ow2/asm/asm/6.2/asm-6.2.jar libraries/org/ow2/asm/asm-commons/6.2/asm-commons-6.2.jar libraries/org/ow2/asm/asm-tree/6.2/asm-tree-6.2.jar libraries/cpw/mods/modlauncher/4.1.0/modlauncher-4.1.0.jar libraries/cpw/mods/grossjava9hacks/1.1.0/grossjava9hacks-1.1.0.jar libraries/net/minecraftforge/accesstransformers/1.0.1-milestone.0.1+94458e7-shadowed/accesstransformers-1.0.1-milestone.0.1+94458e7-shadowed.jar libraries/net/minecraftforge/forgespi/1.5.0/forgespi-1.5.0.jar libraries/net/minecraftforge/coremods/1.0.0/coremods-1.0.0.jar libraries/net/minecraftforge/unsafe/0.2.0/unsafe-0.2.0.jar libraries/com/electronwill/night-config/core/3.6.0/core-3.6.0.jar libraries/com/electronwill/night-config/toml/3.6.0/toml-3.6.0.jar libraries/org/jline/jline/3.12.1/jline-3.12.1.jar libraries/org/apache/maven/maven-artifact/3.6.0/maven-artifact-3.6.0.jar libraries/net/jodah/typetools/0.6.0/typetools-0.6.0.jar libraries/java3d/vecmath/1.5.2/vecmath-1.5.2.jar libraries/org/apache/logging/log4j/log4j-api/2.11.2/log4j-api-2.11.2.jar libraries/org/apache/logging/log4j/log4j-core/2.11.2/log4j-core-2.11.2.jar libraries/net/minecrell/terminalconsoleappender/1.2.0/terminalconsoleappender-1.2.0.jar libraries/net/sf/jopt-simple/jopt-simple/5.0.4/jopt-simple-5.0.4.jar libraries/net/minecraft/server/1.14.4/server-1.14.4-extra-stable.jar " + - embedLibs.collect { - def arr = it.split(':') - if (arr.length == 3) { - return "libraries/${arr[0].replace('.', '/')}/${arr[1]}/${arr[2]}/${arr[1]}-${arr[2]}.jar" - } else if (arr.length == 4) { - return "libraries/${arr[0].replace('.', '/')}/${arr[1]}/${arr[2]}/${arr[1]}-${arr[2]}-${arr[3]}.jar" - } else return "" - }.join(' ') + " forge-1.14.4-28.2.0.jar" -} - jar { manifest.attributes 'MixinConnector': 'io.izzel.arclight.mod.ArclightConnector' manifest.attributes 'Main-Class': 'io.izzel.arclight.server.Main' - manifest.attributes 'Class-Path': classpath() manifest.attributes 'Implementation-Title': 'Arclight' manifest.attributes 'Implementation-Version': "arclight-${project.version}-${getGitHash()}" manifest.attributes 'Implementation-Vendor': 'Arclight Team' @@ -112,15 +108,22 @@ jar { exclude "META-INF/*.RSA" exclude "LICENSE.txt" } - into('libs') { - from(configurations.embedJar.collect()) - } into('META-INF') { from(files("${project(':scripts').projectDir}/bukkit_srg.srg")) from(files("${project(':scripts').projectDir}/resources/inheritanceMap.txt")) } } +task generateInstallerInfo { + def output = [installer: [minecraft: minecraftVersion, forge: forgeVersion], libraries: embedLibs] + outputs.file(installerInfo) + doLast { + installerInfo.text = groovy.json.JsonOutput.toJson(output) + } +} + +sourceSets.main.output.dir installerInfoDir, builtBy: generateInstallerInfo + mixin { add sourceSets.main, 'mixins.arclight.refmap.json' } diff --git a/arclight-coremod/src/main/java/io/izzel/arclight/mixin/core/state/EnumPropertyMixin.java b/arclight-coremod/src/main/java/io/izzel/arclight/mixin/core/state/EnumPropertyMixin.java new file mode 100644 index 00000000..82122b2c --- /dev/null +++ b/arclight-coremod/src/main/java/io/izzel/arclight/mixin/core/state/EnumPropertyMixin.java @@ -0,0 +1,45 @@ +package io.izzel.arclight.mixin.core.state; + +import net.minecraft.state.EnumProperty; +import net.minecraft.util.IStringSerializable; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Overwrite; +import org.spongepowered.asm.mixin.Shadow; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; +import java.util.function.Predicate; + +@Mixin(EnumProperty.class) +public abstract class EnumPropertyMixin { + + @Shadow + public static & IStringSerializable> EnumProperty create(String name, Class clazz, Collection values) { + return null; + } + + /** + * @author IzzelAliz + * @reason + */ + @Overwrite + public static & IStringSerializable> EnumProperty create(String name, Class clazz, Predicate filter) { + try { + List list = new ArrayList<>(); + for (T enumConstant : clazz.getEnumConstants()) { + if (filter.test(enumConstant)) list.add(enumConstant); + } + return create(name, clazz, list); + } catch (Throwable t) { + System.out.println(name); + System.out.println(clazz); + System.out.println(filter); + for (T constant : clazz.getEnumConstants()) { + System.out.println(constant); + } + t.printStackTrace(); + throw t; + } + } +} diff --git a/arclight-coremod/src/main/java/io/izzel/arclight/mixin/core/util/BootstrapMixin.java b/arclight-coremod/src/main/java/io/izzel/arclight/mixin/core/util/BootstrapMixin.java index 64011f0b..c51b9dce 100644 --- a/arclight-coremod/src/main/java/io/izzel/arclight/mixin/core/util/BootstrapMixin.java +++ b/arclight-coremod/src/main/java/io/izzel/arclight/mixin/core/util/BootstrapMixin.java @@ -6,7 +6,7 @@ import org.spongepowered.asm.mixin.Mixin; import org.spongepowered.asm.mixin.injection.At; import org.spongepowered.asm.mixin.injection.Inject; import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; -import io.izzel.arclight.util.Unsafe; +import io.izzel.arclight.api.Unsafe; import java.lang.reflect.Field; import java.util.HashSet; diff --git a/arclight-coremod/src/main/java/io/izzel/arclight/mod/server/BukkitRegistry.java b/arclight-coremod/src/main/java/io/izzel/arclight/mod/server/BukkitRegistry.java index c49ff169..3b5672a9 100644 --- a/arclight-coremod/src/main/java/io/izzel/arclight/mod/server/BukkitRegistry.java +++ b/arclight-coremod/src/main/java/io/izzel/arclight/mod/server/BukkitRegistry.java @@ -19,7 +19,7 @@ import io.izzel.arclight.bridge.bukkit.MaterialBridge; import io.izzel.arclight.mod.ArclightMod; import io.izzel.arclight.mod.util.potion.ArclightPotionEffect; import io.izzel.arclight.util.EnumHelper; -import io.izzel.arclight.util.Unsafe; +import io.izzel.arclight.api.Unsafe; import java.lang.reflect.Field; import java.util.ArrayList; diff --git a/arclight-coremod/src/main/java/io/izzel/arclight/mod/util/remapper/ArclightRemapper.java b/arclight-coremod/src/main/java/io/izzel/arclight/mod/util/remapper/ArclightRemapper.java index f97f0922..d64932da 100644 --- a/arclight-coremod/src/main/java/io/izzel/arclight/mod/util/remapper/ArclightRemapper.java +++ b/arclight-coremod/src/main/java/io/izzel/arclight/mod/util/remapper/ArclightRemapper.java @@ -2,7 +2,7 @@ package io.izzel.arclight.mod.util.remapper; import com.google.common.collect.BiMap; import com.google.common.collect.HashBiMap; -import io.izzel.arclight.util.Unsafe; +import io.izzel.arclight.api.Unsafe; import net.md_5.specialsource.InheritanceMap; import net.md_5.specialsource.JarMapping; import net.md_5.specialsource.provider.ClassLoaderProvider; diff --git a/arclight-coremod/src/main/java/io/izzel/arclight/mod/util/remapper/PluginRemapper.java b/arclight-coremod/src/main/java/io/izzel/arclight/mod/util/remapper/PluginRemapper.java index 204ee26c..594c5499 100644 --- a/arclight-coremod/src/main/java/io/izzel/arclight/mod/util/remapper/PluginRemapper.java +++ b/arclight-coremod/src/main/java/io/izzel/arclight/mod/util/remapper/PluginRemapper.java @@ -5,7 +5,7 @@ import com.google.common.collect.BiMap; import com.google.common.collect.HashBiMap; import com.google.common.collect.Maps; import io.izzel.arclight.mod.util.remapper.generated.ArclightReflectionHandler; -import io.izzel.arclight.util.Unsafe; +import io.izzel.arclight.api.Unsafe; import net.md_5.specialsource.JarMapping; import net.md_5.specialsource.JarRemapper; import net.md_5.specialsource.RemappingClassAdapter; diff --git a/arclight-coremod/src/main/java/io/izzel/arclight/server/Main.java b/arclight-coremod/src/main/java/io/izzel/arclight/server/Main.java index 8aa116c1..8754d466 100644 --- a/arclight-coremod/src/main/java/io/izzel/arclight/server/Main.java +++ b/arclight-coremod/src/main/java/io/izzel/arclight/server/Main.java @@ -1,32 +1,25 @@ package io.izzel.arclight.server; -import com.google.common.io.ByteStreams; +import io.izzel.arclight.api.Unsafe; +import io.izzel.arclight.forgeinstaller.ForgeInstaller; import io.izzel.arclight.mod.util.BukkitOptionParser; import io.izzel.arclight.mod.util.remapper.ArclightRemapper; import io.izzel.arclight.util.EnumHelper; -import io.izzel.arclight.util.Unsafe; import joptsimple.OptionSet; import net.minecraftforge.server.ServerMain; import org.apache.logging.log4j.LogManager; import org.fusesource.jansi.AnsiConsole; -import java.io.IOException; -import java.io.InputStream; -import java.io.OutputStream; -import java.nio.file.FileVisitResult; -import java.nio.file.Files; -import java.nio.file.Path; -import java.nio.file.Paths; -import java.nio.file.SimpleFileVisitor; -import java.nio.file.attribute.BasicFileAttributes; +import java.net.URL; +import java.net.URLClassLoader; import java.util.Objects; public class Main { public static void main(String[] args) throws Throwable { - if (Files.notExists(Paths.get("forge-1.14.4-28.2.0.jar"))) { - System.err.println("Install forge 1.14.4-28.2.0 before launching Arclight."); - return; + ForgeInstaller.install(); + for (URL url : ((URLClassLoader) Main.class.getClassLoader()).getURLs()) { + System.out.println(url); } try { // Java 9 & Java 兼容性 int javaVersion = (int) Float.parseFloat(System.getProperty("java.class.version")); @@ -40,114 +33,34 @@ public class Main { return; } try { - if (Files.notExists(Paths.get("./libraries/net/minecraftforge/eventbus/2.0.0-milestone.1/eventbus-2.0.0-milestone.1-service.jar"))) { - Path folder = Paths.get("./libraries/net/minecraftforge/eventbus"); - Files.walkFileTree(folder, new SimpleFileVisitor() { - @Override - public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException { - Files.delete(file); - return FileVisitResult.CONTINUE; - } + OptionSet options = new BukkitOptionParser().parse(args); + String jline_UnsupportedTerminal = new String(new char[]{'j', 'l', 'i', 'n', 'e', '.', 'U', 'n', 's', 'u', 'p', 'p', 'o', 'r', 't', 'e', 'd', 'T', 'e', 'r', 'm', 'i', 'n', 'a', 'l'}); + String jline_terminal = new String(new char[]{'j', 'l', 'i', 'n', 'e', '.', 't', 'e', 'r', 'm', 'i', 'n', 'a', 'l'}); - @Override - public FileVisitResult postVisitDirectory(Path dir, IOException exc) throws IOException { - Files.delete(dir); - return FileVisitResult.CONTINUE; - } - }); - throw new Exception(); + boolean useJline = !(jline_UnsupportedTerminal).equals(System.getProperty(jline_terminal)); + + if (options.has("nojline")) { + System.setProperty("user.language", "en"); + useJline = false; } - Class.forName("org.spongepowered.asm.mixin.Mixins", false, ClassLoader.getSystemClassLoader()); - Class.forName("org.objectweb.asm.util.CheckClassAdapter", false, ClassLoader.getSystemClassLoader()); - Class.forName("org.objectweb.asm.tree.analysis.AnalyzerException", false, ClassLoader.getSystemClassLoader()); - Class.forName("net.md_5.bungee.api.ChatColor", false, ClassLoader.getSystemClassLoader()); - Class.forName("org.yaml.snakeyaml.Yaml", false, ClassLoader.getSystemClassLoader()); - Class.forName("org.sqlite.JDBC", false, ClassLoader.getSystemClassLoader()); - Class.forName("com.mysql.jdbc.Driver", false, ClassLoader.getSystemClassLoader()); - Class.forName("org.apache.commons.lang.Validate", false, ClassLoader.getSystemClassLoader()); - Class.forName("jline.Terminal", false, ClassLoader.getSystemClassLoader()); - Class.forName("org.json.simple.JSONObject", false, ClassLoader.getSystemClassLoader()); - Class.forName("org.apache.logging.log4j.jul.LogManager", false, ClassLoader.getSystemClassLoader()); - Class.forName("net.minecraftforge.eventbus.EventBus", false, ClassLoader.getSystemClassLoader()); - Class.forName("net.md_5.specialsource.JarRemapper", false, ClassLoader.getSystemClassLoader()); - try { - OptionSet options = new BukkitOptionParser().parse(args); - String jline_UnsupportedTerminal = new String(new char[]{'j', 'l', 'i', 'n', 'e', '.', 'U', 'n', 's', 'u', 'p', 'p', 'o', 'r', 't', 'e', 'd', 'T', 'e', 'r', 'm', 'i', 'n', 'a', 'l'}); - String jline_terminal = new String(new char[]{'j', 'l', 'i', 'n', 'e', '.', 't', 'e', 'r', 'm', 'i', 'n', 'a', 'l'}); - boolean useJline = !(jline_UnsupportedTerminal).equals(System.getProperty(jline_terminal)); - - if (options.has("nojline")) { - System.setProperty("user.language", "en"); - useJline = false; - } - - if (useJline) { - AnsiConsole.systemInstall(); - } else { - System.setProperty(jline.TerminalFactory.JLINE_TERMINAL, jline.UnsupportedTerminal.class.getName()); - } - } catch (Exception e) { - e.printStackTrace(); - } - try { - System.setProperty("java.util.logging.manager", "org.apache.logging.log4j.jul.LogManager"); - System.setProperty("log4j.jul.LoggerAdapter", "io.izzel.arclight.mod.util.ArclightLoggerAdapter"); - LogManager.getLogger("Arclight").info("Loading mappings ..."); - Objects.requireNonNull(ArclightRemapper.INSTANCE); - ServerMain.main(args); - } catch (Exception e) { - e.printStackTrace(); - System.err.println("Fail to launch Arclight."); + if (useJline) { + AnsiConsole.systemInstall(); + } else { + System.setProperty(jline.TerminalFactory.JLINE_TERMINAL, jline.UnsupportedTerminal.class.getName()); } } catch (Exception e) { - System.err.println("FATAL ERROR: The libraries required to launch Arclight are missing, extracting..."); - extract("org.spongepowered:mixin:0.8"); - extract("org.ow2.asm:asm-util:6.2"); - extract("org.ow2.asm:asm-analysis:6.2"); - extract("org.yaml:snakeyaml:1.23"); - extract("net.md-5:bungeecord-chat:1.13-SNAPSHOT"); - extract("org.xerial:sqlite-jdbc:3.28.0"); - extract("mysql:mysql-connector-java:5.1.47"); - extract("commons-lang:commons-lang:2.6"); - extract("jline:jline:2.12.1"); - extract("com.googlecode.json-simple:json-simple:1.1.1"); - extract("org.apache.logging.log4j:log4j-jul:2.11.2"); - extract("net.md-5:SpecialSource:1.8.6"); - extract("net.minecraftforge:eventbus:2.0.0-milestone.1:service"); - System.out.println("Please RESTART the server."); + e.printStackTrace(); } - } - - private static void extract(String artifact) throws Throwable { - String[] split = artifact.split(":"); - if (split.length == 3) { - String jar = String.format("/%s-%s.jar", split[1], split[2]); - String path = split[0].replace('.', '/') + "/" + - split[1] + "/" + split[2] + jar; - extract("libs" + jar, "./libraries/" + path); - System.out.println("Extracted " + artifact); - } else if (split.length == 4) { - String jar = String.format("/%s-%s-%s.jar", split[1], split[2], split[3]); - String path = split[0].replace('.', '/') + "/" + - split[1] + "/" + split[2] + jar; - extract("libs" + jar, "./libraries/" + path); - System.out.println("Extracted " + artifact); - } - } - - private static void extract(String name, String target) throws Throwable { - Path path = Paths.get(target); - if (Files.notExists(path)) { - Files.createDirectories(path.getParent()); - Files.createFile(path); - InputStream stream = Main.class.getResourceAsStream("/" + name); - if (stream != null) { - OutputStream outputStream = Files.newOutputStream(Paths.get(target)); - ByteStreams.copy(stream, outputStream); - stream.close(); - outputStream.close(); - } + try { + System.setProperty("java.util.logging.manager", "org.apache.logging.log4j.jul.LogManager"); + System.setProperty("log4j.jul.LoggerAdapter", "io.izzel.arclight.mod.util.ArclightLoggerAdapter"); + LogManager.getLogger("Arclight").info("Loading mappings ..."); + Objects.requireNonNull(ArclightRemapper.INSTANCE); + ServerMain.main(args); + } catch (Exception e) { + e.printStackTrace(); + System.err.println("Fail to launch Arclight."); } } } \ No newline at end of file diff --git a/arclight-coremod/src/main/java/io/izzel/arclight/util/EnumHelper.java b/arclight-coremod/src/main/java/io/izzel/arclight/util/EnumHelper.java index eda72118..53205ef0 100644 --- a/arclight-coremod/src/main/java/io/izzel/arclight/util/EnumHelper.java +++ b/arclight-coremod/src/main/java/io/izzel/arclight/util/EnumHelper.java @@ -1,6 +1,7 @@ package io.izzel.arclight.util; import com.google.common.collect.ImmutableList; +import io.izzel.arclight.api.Unsafe; import org.bukkit.Material; import java.lang.invoke.MethodHandle; diff --git a/arclight-coremod/src/main/resources/mixins.arclight.core.json b/arclight-coremod/src/main/resources/mixins.arclight.core.json index eae84b96..46860063 100644 --- a/arclight-coremod/src/main/resources/mixins.arclight.core.json +++ b/arclight-coremod/src/main/resources/mixins.arclight.core.json @@ -321,6 +321,7 @@ "server.management.PlayerInteractionManagerMixin", "server.management.PlayerListMixin", "server.management.UserListMixin", + "state.EnumPropertyMixin", "state.IntegerPropertyMixin", "stats.StatisticsManagerMixin", "tags.NetworkTagCollectionMixin", diff --git a/forge-installer/build.gradle b/forge-installer/build.gradle new file mode 100644 index 00000000..38df83fb --- /dev/null +++ b/forge-installer/build.gradle @@ -0,0 +1,12 @@ +plugins { + id 'java' +} + +repositories { + mavenCentral() +} + +dependencies { + compile 'com.google.code.gson:gson:2.8.0' + compile project(':arclight-api') +} diff --git a/forge-installer/src/main/java/io/izzel/arclight/forgeinstaller/ForgeInstaller.java b/forge-installer/src/main/java/io/izzel/arclight/forgeinstaller/ForgeInstaller.java new file mode 100644 index 00000000..d989589d --- /dev/null +++ b/forge-installer/src/main/java/io/izzel/arclight/forgeinstaller/ForgeInstaller.java @@ -0,0 +1,138 @@ +package io.izzel.arclight.forgeinstaller; + +import com.google.gson.Gson; +import com.google.gson.JsonArray; +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; +import com.google.gson.JsonParser; +import io.izzel.arclight.api.Unsafe; + +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.lang.reflect.Field; +import java.lang.reflect.Method; +import java.net.URL; +import java.nio.file.FileSystem; +import java.nio.file.FileSystems; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.Collections; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; +import java.util.jar.JarFile; +import java.util.jar.Manifest; + +public class ForgeInstaller { + + private static final String INFO = "Download mirror service by BMCLAPI: https://bmclapidoc.bangbang93.com\n" + + "Support MinecraftForge project at https://www.patreon.com/LexManos/"; + private static final String INSTALLER_URL = "https://bmclapi2.bangbang93.com/maven/net/minecraftforge/forge/%s-%s/forge-%s-%s-installer.jar"; + private static final String BMCL_API = "https://bmclapi2.bangbang93.com/maven/"; + private static final String SERVER_URL = "https://bmclapi2.bangbang93.com/version/%s/server"; + + public static void install() throws Throwable { + InputStream stream = ForgeInstaller.class.getResourceAsStream("/META-INF/installer.json"); + InstallInfo installInfo = new Gson().fromJson(new InputStreamReader(stream), InstallInfo.class); + Path path = Paths.get(String.format("forge-%s-%s.jar", installInfo.installer.minecraft, installInfo.installer.forge)); + if (!Files.exists(path)) { + System.out.println(INFO); + Thread.sleep(5000); + download(installInfo); + ProcessBuilder builder = new ProcessBuilder(); + builder.command("java", "-jar", String.format("forge-%s-%s-installer.jar", installInfo.installer.minecraft, installInfo.installer.forge), "--installServer", "."); + builder.inheritIO(); + Process process = builder.start(); + process.waitFor(); + } + classpath(path, installInfo); + } + + private static void classpath(Path path, InstallInfo installInfo) throws Throwable { + JarFile jarFile = new JarFile(path.toFile()); + Manifest manifest = jarFile.getManifest(); + String[] split = manifest.getMainAttributes().getValue("Class-Path").split(" "); + for (String s : split) { + if (s.contains("eventbus-1.0.0-service")) continue; + addToPath(Paths.get(s)); + } + for (String library : installInfo.libraries) { + addToPath(Paths.get("libraries", mavenToPath(library))); + } + addToPath(path); + } + + private static void addToPath(Path path) throws Throwable { + ClassLoader loader = ForgeInstaller.class.getClassLoader(); + Field ucpField = loader.getClass().getDeclaredField("ucp"); + long offset = Unsafe.objectFieldOffset(ucpField); + Object ucp = Unsafe.getObject(loader, offset); + Method method = ucp.getClass().getDeclaredMethod("addURL", URL.class); + Unsafe.lookup().unreflect(method).invoke(ucp, path.toUri().toURL()); + } + + private static void download(InstallInfo info) throws Exception { + SimpleDownloader downloader = new SimpleDownloader(); + String format = String.format(INSTALLER_URL, info.installer.minecraft, info.installer.forge, info.installer.minecraft, info.installer.forge); + String dist = String.format("forge-%s-%s-installer.jar", info.installer.minecraft, info.installer.forge); + downloader.download(format, dist, null, + path -> processInstaller(path, downloader)); + for (String library : info.libraries) { + String path = mavenToPath(library); + downloader.downloadMaven(path); + } + downloader.download(String.format(SERVER_URL, info.installer.minecraft), String.format("minecraft_server.%s.jar", info.installer.minecraft), null); + if (!downloader.awaitTermination()) { + Files.deleteIfExists(Paths.get(dist)); + throw new Exception(); + } + } + + private static void processInstaller(Path path, SimpleDownloader downloader) { + try { + FileSystem system = FileSystems.newFileSystem(path, null); + Set set = Collections.newSetFromMap(new ConcurrentHashMap<>()); + Path profile = system.getPath("install_profile.json"); + downloadLibraries(downloader, profile, set); + Path version = system.getPath("version.json"); + downloadLibraries(downloader, version, set); + system.close(); + } catch (IOException e) { + e.printStackTrace(); + } + } + + private static void downloadLibraries(SimpleDownloader downloader, Path path, Set set) throws IOException { + JsonArray array = new JsonParser().parse(Files.newBufferedReader(path)).getAsJsonObject().getAsJsonArray("libraries"); + for (JsonElement element : array) { + String name = element.getAsJsonObject().get("name").getAsString(); + if (!set.add(name)) continue; + String libPath = mavenToPath(name); + JsonObject artifact = element.getAsJsonObject().getAsJsonObject("downloads").getAsJsonObject("artifact"); + String url = artifact.get("url").getAsString(); + if (url == null || url.trim().isEmpty()) continue; + String hash = artifact.get("sha1").getAsString(); + downloader.download(BMCL_API + libPath, "libraries/" + libPath, hash); + } + } + + private static String mavenToPath(String maven) { + String type; + if (maven.matches(".*@\\w+$")) { + int i = maven.lastIndexOf('@'); + type = maven.substring(i + 1); + maven = maven.substring(0, i); + } else { + type = "jar"; + } + String[] arr = maven.split(":"); + if (arr.length == 3) { + String pkg = arr[0].replace('.', '/'); + return String.format("%s/%s/%s/%s-%s.%s", pkg, arr[1], arr[2], arr[1], arr[2], type); + } else if (arr.length == 4) { + String pkg = arr[0].replace('.', '/'); + return String.format("%s/%s/%s/%s-%s-%s.%s", pkg, arr[1], arr[2], arr[1], arr[2], arr[3], type); + } else throw new RuntimeException("Wrong maven coordinate " + maven); + } +} diff --git a/forge-installer/src/main/java/io/izzel/arclight/forgeinstaller/InstallInfo.java b/forge-installer/src/main/java/io/izzel/arclight/forgeinstaller/InstallInfo.java new file mode 100644 index 00000000..e7061b12 --- /dev/null +++ b/forge-installer/src/main/java/io/izzel/arclight/forgeinstaller/InstallInfo.java @@ -0,0 +1,13 @@ +package io.izzel.arclight.forgeinstaller; + +public class InstallInfo { + + public Installer installer; + public String[] libraries; + + public static class Installer { + + public String minecraft; + public String forge; + } +} diff --git a/forge-installer/src/main/java/io/izzel/arclight/forgeinstaller/SimpleDownloader.java b/forge-installer/src/main/java/io/izzel/arclight/forgeinstaller/SimpleDownloader.java new file mode 100644 index 00000000..989f9398 --- /dev/null +++ b/forge-installer/src/main/java/io/izzel/arclight/forgeinstaller/SimpleDownloader.java @@ -0,0 +1,152 @@ +package io.izzel.arclight.forgeinstaller; + +import java.io.FileNotFoundException; +import java.io.InputStream; +import java.math.BigInteger; +import java.net.HttpURLConnection; +import java.net.SocketTimeoutException; +import java.net.URL; +import java.net.URLDecoder; +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.util.concurrent.Callable; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.function.Consumer; + +public class SimpleDownloader { + + private static final String[] MAVEN_REPO = { + "https://repo.spongepowered.org/maven", + "https://oss.sonatype.org/content/repositories/snapshots/", + "https://hub.spigotmc.org/nexus/content/repositories/snapshots/", + "https://bmclapi2.bangbang93.com/maven/", + "https://maven.aliyun.com/repository/public/" + }; + + private final CompletableFuture future = new CompletableFuture<>(); + private final AtomicInteger integer = new AtomicInteger(0); + private final ExecutorService service = Executors.newFixedThreadPool(8); + private final AtomicBoolean error = new AtomicBoolean(false); + + public void run(Callable callable) { + integer.incrementAndGet(); + CompletableFuture.supplyAsync(() -> { + try { + return callable.call(); + } catch (Throwable e) { + System.err.println(e.toString()); + error.compareAndSet(false, true); + return false; + } + }, service).thenAccept(b -> { + int remain = integer.decrementAndGet(); + if (remain == 0) { + if (error.get()) + future.completeExceptionally(new Exception()); + else + future.complete(null); + } + }); + } + + public void download(String url, String dist, String hash) { + download(url, dist, hash, path -> {}); + } + + public void download(String url, String dist, String hash, Consumer onComplete) { + run(() -> downloadFile(url, dist, hash, 5, onComplete)); + } + + public void downloadMaven(String path) { + run(() -> { + for (String s : MAVEN_REPO) { + if (downloadFile(s + path, "libraries/" + path, null, 3, p -> {})) { + return true; + } + } + return false; + }); + } + + public boolean awaitTermination() { + try { + future.join(); + return true; + } catch (Exception e) { + return false; + } finally { + service.shutdownNow(); + } + } + + private boolean downloadFile(String url, String dist, String hash, int retry, Consumer onComplete) throws Exception { + if (retry <= 0) return false; + try { + Path path = Paths.get(dist); + if (Files.exists(path) && hash != null) { + String hash1 = hash(path); + if (hash.equals(hash1)) { + onComplete.accept(path); + return true; + } else { + Files.delete(path); + throw new Exception("Checksum failed: expect " + hash + " but found " + hash1); + } + } + if (!Files.exists(path) && path.getParent() != null) { + Files.createDirectories(path.getParent()); + } + System.out.println("Downloading " + url); + InputStream stream = redirect(new URL(url)); + Files.copy(stream, path, StandardCopyOption.REPLACE_EXISTING); + stream.close(); + if (hash != null) { + String hash1 = hash(path); + if (!hash.equals(hash1)) { + Files.delete(path); + throw new Exception("Checksum failed: expect " + hash + " but found " + hash1); + } + } + onComplete.accept(path); + return true; + } catch (FileNotFoundException | SocketTimeoutException e) { + return false; + } catch (Exception e) { + run(() -> downloadFile(url, dist, hash, retry - 1, onComplete)); + System.err.println("Failed to download file " + dist); + throw e; + } + } + + private static InputStream redirect(URL url) throws Exception { + HttpURLConnection connection = (HttpURLConnection) url.openConnection(); + connection.setInstanceFollowRedirects(false); + connection.setReadTimeout(15000); + connection.setConnectTimeout(15000); + switch (connection.getResponseCode()) { + case HttpURLConnection.HTTP_MOVED_PERM: + case HttpURLConnection.HTTP_MOVED_TEMP: + String location = URLDecoder.decode(connection.getHeaderField("Location"), "UTF-8"); + return redirect(new URL(url, location)); + case HttpURLConnection.HTTP_FORBIDDEN: + case HttpURLConnection.HTTP_NOT_FOUND: + throw new FileNotFoundException(); + } + return connection.getInputStream(); + } + + private static String SHA_PAD = String.format("%040d", 0); + + private static String hash(Path path) throws Exception { + MessageDigest digest = MessageDigest.getInstance("SHA-1"); + String hash = new BigInteger(1, digest.digest(Files.readAllBytes(path))).toString(16); + return (SHA_PAD + hash).substring(hash.length()); + } +} diff --git a/settings.gradle b/settings.gradle index 19ae35c2..e805311c 100644 --- a/settings.gradle +++ b/settings.gradle @@ -2,4 +2,7 @@ rootProject.name = 'arclight' include 'arclight-coremod' include 'scripts' include 'arclight-testplugin' +include 'arclight-common' +include 'forge-installer' +include 'arclight-api'