New downloader

This commit is contained in:
IzzelAliz 2020-06-16 16:18:26 +08:00
parent 1d3f4169c0
commit 36aa38a179
14 changed files with 417 additions and 237 deletions

View File

@ -9,7 +9,7 @@ buildscript {
dependencies {
classpath group: 'net.minecraftforge.gradle', name: 'ForgeGradle', version: '3.+', changing: true
classpath 'org.spongepowered:mixingradle:0.7-SNAPSHOT'
classpath 'com.github.IzzelAliz:arclight-gradle-plugin:1.3'
classpath "com.github.IzzelAliz:arclight-gradle-plugin:$agpVersion"
}
}

View File

@ -9,7 +9,7 @@ buildscript {
dependencies {
classpath group: 'net.minecraftforge.gradle', name: 'ForgeGradle', version: '3.+', changing: true
classpath 'org.spongepowered:mixingradle:0.7-SNAPSHOT'
classpath 'com.github.IzzelAliz:arclight-gradle-plugin:1.3'
classpath "com.github.IzzelAliz:arclight-gradle-plugin:$agpVersion"
}
}

View File

@ -9,7 +9,7 @@ buildscript {
dependencies {
classpath group: 'net.minecraftforge.gradle', name: 'ForgeGradle', version: '3.+', changing: true
classpath 'org.spongepowered:mixingradle:0.7-SNAPSHOT'
classpath 'com.github.IzzelAliz:arclight-gradle-plugin:1.3'
classpath "com.github.IzzelAliz:arclight-gradle-plugin:$agpVersion"
}
}

View File

@ -5,6 +5,10 @@ allprojects {
group 'io.izzel.arclight'
version '1.0.0-SNAPSHOT'
ext {
agpVersion = '1.5'
}
task cleanBuild {
doFirst {
def f = project.file("build/libs")

View File

@ -4,9 +4,14 @@ plugins {
repositories {
mavenCentral()
maven {
name = 'sponge'
url = 'https://repo.spongepowered.org/maven'
}
}
dependencies {
compile 'com.google.code.gson:gson:2.8.0'
compile project(':arclight-api')
compile project(':i18n-config')
}

View File

@ -0,0 +1,102 @@
package io.izzel.arclight.forgeinstaller;
import io.izzel.arclight.api.Unsafe;
import io.izzel.arclight.i18n.LocalizedException;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
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.StandardCopyOption;
import java.util.HashSet;
import java.util.Set;
import java.util.StringJoiner;
import java.util.function.Supplier;
public class FileDownloader implements Supplier<Path> {
private final String url;
private final String target;
private final String hash;
public FileDownloader(String url, String target, String hash) {
this.url = url;
this.target = target;
this.hash = hash;
}
@Override
public Path get() {
try {
Path path = new File(target).toPath();
if (Files.exists(path) && Files.isDirectory(path)) {
Files.delete(path);
}
if (Files.exists(path)) {
if (Files.isDirectory(path)) {
throw LocalizedException.checked("downloader.dir", target);
} else {
if (Util.hash(path).equals(hash)) return path;
else Files.delete(path);
}
}
if (!Files.exists(path) && path.getParent() != null) {
Files.createDirectories(path.getParent());
}
URL url = new URL(this.url);
try (InputStream stream = redirect(url)) {
Files.copy(stream, path, StandardCopyOption.REPLACE_EXISTING);
} catch (SocketTimeoutException e) {
throw LocalizedException.checked("downloader.timeout", e, url);
}
if (Files.exists(path)) {
String hash = Util.hash(path);
if (hash.equals(this.hash)) return path;
else {
Files.delete(path);
throw LocalizedException.checked("downloader.hash-not-match", this.hash, hash, url);
}
} else {
throw LocalizedException.checked("downloader.not-found", url);
}
} catch (Exception e) {
Unsafe.throwException(e);
return null;
}
}
private InputStream redirect(URL url) throws IOException {
return redirect(url, new HashSet<>());
}
private InputStream redirect(URL url, Set<String> history) throws IOException {
if (history.contains(url.toString())) {
StringJoiner joiner = new StringJoiner("\n ");
joiner.add("");
history.forEach(joiner::add);
throw LocalizedException.unchecked("downloader.redirect-error", joiner.toString());
} else {
history.add(url.toString());
}
HttpURLConnection connection = (HttpURLConnection) url.openConnection();
connection.setInstanceFollowRedirects(false);
connection.setReadTimeout(15000);
connection.setConnectTimeout(15000);
int responseCode = connection.getResponseCode();
if (responseCode == HttpURLConnection.HTTP_OK) {
return connection.getInputStream();
} else if (responseCode == HttpURLConnection.HTTP_MOVED_PERM || responseCode == HttpURLConnection.HTTP_MOVED_TEMP) {
String location = URLDecoder.decode(connection.getHeaderField("Location"), "UTF-8");
return redirect(new URL(url, location));
} else if (responseCode == HttpURLConnection.HTTP_NOT_FOUND || responseCode == HttpURLConnection.HTTP_FORBIDDEN) {
throw LocalizedException.unchecked("downloader.not-found", url);
} else {
throw LocalizedException.unchecked("downloader.http-error", responseCode, url);
}
}
}

View File

@ -1,12 +1,16 @@
package io.izzel.arclight.forgeinstaller;
import com.google.common.collect.ImmutableMap;
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 io.izzel.arclight.i18n.ArclightLocale;
import io.izzel.arclight.i18n.LocalizedException;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
@ -18,37 +22,145 @@ 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.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.CompletionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.function.Function;
import java.util.function.Supplier;
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[] MAVEN_REPO = {
"https://bmclapi2.bangbang93.com/maven/",
"https://maven.aliyun.com/repository/public/",
"https://repo.spongepowered.org/maven/",
"https://oss.sonatype.org/content/repositories/snapshots/",
"https://hub.spigotmc.org/nexus/content/repositories/snapshots/",
"https://files.minecraftforge.net/maven/",
"https://repo1.maven.org/maven2/"
};
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";
private static final Map<String, String> VERSION_HASH = ImmutableMap.of(
"1.14.4", "3dc3d84a581f14691199cf6831b71ed1296a9fdf",
"1.15.2", "bb2b6b1aefcd70dfd1892149ac3a215f6c636b07"
);
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);
List<Supplier<Path>> suppliers = checkMaven(installInfo.libraries);
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();
if (!suppliers.isEmpty() || !Files.exists(path)) {
ArclightLocale.info("downloader.info");
ExecutorService pool = Executors.newFixedThreadPool(8);
CompletableFuture<?>[] array = suppliers.stream().map(reportSupply(pool)).toArray(CompletableFuture[]::new);
if (!Files.exists(path)) {
CompletableFuture<?>[] futures = installForge(installInfo, pool);
handleFutures(futures);
ArclightLocale.info("downloader.forge-install");
ProcessBuilder builder = new ProcessBuilder();
builder.command("java", "-jar", String.format("forge-%s-%s-installer.jar", installInfo.installer.minecraft, installInfo.installer.forge), "--installServer", ".");
Process process = builder.start();
process.waitFor();
}
handleFutures(array);
pool.shutdownNow();
}
classpath(path, installInfo);
}
private static Function<Supplier<Path>, CompletableFuture<Path>> reportSupply(ExecutorService service) {
return it -> CompletableFuture.supplyAsync(it, service).thenApply(path -> {
ArclightLocale.info("downloader.complete", path);
return path;
});
}
private static CompletableFuture<?>[] installForge(InstallInfo info, ExecutorService pool) throws Exception {
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);
FileDownloader fd = new FileDownloader(format, dist, info.installer.hash);
CompletableFuture<?> installerFuture = reportSupply(pool).apply(fd).thenAccept(path -> {
try {
FileSystem system = FileSystems.newFileSystem(path, null);
Map<String, String> map = new HashMap<>();
Path profile = system.getPath("install_profile.json");
map.putAll(profileLibraries(profile));
Path version = system.getPath("version.json");
map.putAll(profileLibraries(version));
List<Supplier<Path>> suppliers = checkMaven(map);
CompletableFuture<?>[] array = suppliers.stream().map(reportSupply(pool)).toArray(CompletableFuture[]::new);
handleFutures(array);
} catch (IOException e) {
e.printStackTrace();
}
});
CompletableFuture<?> serverFuture = reportSupply(pool).apply(
new FileDownloader(String.format(SERVER_URL, info.installer.minecraft),
String.format("minecraft_server.%s.jar", info.installer.minecraft), VERSION_HASH.get(info.installer.minecraft))
);
return new CompletableFuture<?>[]{installerFuture, serverFuture};
}
private static void handleFutures(CompletableFuture<?>... futures) {
for (CompletableFuture<?> future : futures) {
try {
future.join();
} catch (CompletionException e) {
Throwable cause = e.getCause();
if (cause instanceof LocalizedException) {
LocalizedException local = (LocalizedException) cause;
ArclightLocale.error(local.node(), local.args());
} else throw e;
} catch (Exception e) {
e.printStackTrace();
}
}
}
private static Map<String, String> profileLibraries(Path path) throws IOException {
Map<String, String> ret = new HashMap<>();
JsonArray array = new JsonParser().parse(Files.newBufferedReader(path)).getAsJsonObject().getAsJsonArray("libraries");
for (JsonElement element : array) {
String name = element.getAsJsonObject().get("name").getAsString();
JsonObject artifact = element.getAsJsonObject().getAsJsonObject("downloads").getAsJsonObject("artifact");
String hash = artifact.get("sha1").getAsString();
String url = artifact.get("url").getAsString();
if (url == null || url.trim().isEmpty()) continue;
ret.put(name, hash);
}
return ret;
}
private static List<Supplier<Path>> checkMaven(Map<String, String> map) {
List<Supplier<Path>> incomplete = new ArrayList<>();
for (Map.Entry<String, String> entry : map.entrySet()) {
String maven = entry.getKey();
String path = "libraries/" + Util.mavenToPath(maven);
if (new File(path).exists()) {
try {
String hash = Util.hash(path);
if (!hash.equals(entry.getValue())) {
incomplete.add(new MavenDownloader(MAVEN_REPO, maven, path, entry.getValue()));
}
} catch (Exception e) {
incomplete.add(new MavenDownloader(MAVEN_REPO, maven, path, entry.getValue()));
}
} else {
incomplete.add(new MavenDownloader(MAVEN_REPO, maven, path, entry.getValue()));
}
}
return incomplete;
}
private static void classpath(Path path, InstallInfo installInfo) throws Throwable {
JarFile jarFile = new JarFile(path.toFile());
Manifest manifest = jarFile.getManifest();
@ -57,8 +169,8 @@ public class ForgeInstaller {
if (s.contains("eventbus-1.0.0-service")) continue;
addToPath(Paths.get(s));
}
for (String library : installInfo.libraries) {
addToPath(Paths.get("libraries", mavenToPath(library)));
for (String library : installInfo.libraries.keySet()) {
addToPath(Paths.get("libraries", Util.mavenToPath(library)));
}
addToPath(path);
}
@ -71,68 +183,4 @@ public class ForgeInstaller {
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<String> 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<String> 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);
}
}

View File

@ -1,13 +1,16 @@
package io.izzel.arclight.forgeinstaller;
import java.util.Map;
public class InstallInfo {
public Installer installer;
public String[] libraries;
public Map<String, String> libraries;
public static class Installer {
public String minecraft;
public String forge;
public String hash;
}
}

View File

@ -0,0 +1,51 @@
package io.izzel.arclight.forgeinstaller;
import io.izzel.arclight.i18n.ArclightLocale;
import io.izzel.arclight.i18n.LocalizedException;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.List;
import java.util.StringJoiner;
import java.util.function.Supplier;
public class MavenDownloader implements Supplier<Path> {
private final String[] repos;
private final String coord;
private final String target;
private final String hash;
public MavenDownloader(String[] repos, String coord, String target, String hash) {
this.repos = repos;
this.coord = coord;
this.target = target;
this.hash = hash;
}
@Override
public Path get() {
String path = Util.mavenToPath(coord);
List<Exception> exceptions = new ArrayList<>();
for (String repo : repos) {
try {
return new FileDownloader(repo + path, target, hash).get();
} catch (Exception e) {
exceptions.add(e);
}
}
StringJoiner joiner = new StringJoiner("\n ");
joiner.add("");
for (int i = 0; i < exceptions.size(); i++) {
Exception exception = exceptions.get(i);
if (exception instanceof LocalizedException) {
LocalizedException local = (LocalizedException) exception;
String format = ArclightLocale.getInstance().format(local.node(), local.args());
joiner.add("(" + (i + 1) + ") " + format);
} else {
joiner.add("(" + (i + 1) + ") " + exception);
}
}
throw LocalizedException.unchecked("downloader.maven-fail", coord, joiner.toString());
}
}

View File

@ -1,152 +0,0 @@
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<Void> 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<Boolean> 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<Path> 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<Path> 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());
}
}

View File

@ -0,0 +1,46 @@
package io.izzel.arclight.forgeinstaller;
import java.io.File;
import java.math.BigInteger;
import java.nio.file.Files;
import java.nio.file.Path;
import java.security.MessageDigest;
class Util {
public 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);
}
private static final String SHA_PAD = String.format("%040d", 0);
public static String hash(String path) throws Exception {
return hash(new File(path).toPath());
}
public static String hash(File path) throws Exception {
return hash(path.toPath());
}
public 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());
}
}

View File

@ -0,0 +1,41 @@
package io.izzel.arclight.i18n;
@SuppressWarnings("unchecked")
public interface LocalizedException {
String node();
Object[] args();
static <T extends Exception & LocalizedException> T checked(String node, Object... args) {
class Checked extends Exception implements LocalizedException {
@Override
public String node() {
return node;
}
@Override
public Object[] args() {
return args;
}
}
return (T) new Checked();
}
static <T extends RuntimeException & LocalizedException> T unchecked(String node, Object... args) {
class Unchecked extends RuntimeException implements LocalizedException {
@Override
public String node() {
return node;
}
@Override
public Object[] args() {
return args;
}
}
return (T) new Unchecked();
}
}

View File

@ -12,6 +12,22 @@ logo = [
" §aBuild Date {}"
""
]
downloader {
info = [
"Libraries are missing, downloading!"
"Downloading service by BMCLAPI https://bmclapidoc.bangbang93.com"
"Support MinecraftForge project https://www.patreon.com/LexManos/"
]
http-error = "HTTP Error {0} {1}"
not-found = "Not found in {0}"
redirect-error = "Redirect error {0}"
dir = "Existing folder in destination {0}"
timeout = "Timeout {0} {1}"
hash-not-match = "File sha1 mismatch, expect {0} found {1}: {2}"
maven-fail = "{0} failed to download: {1}"
complete = "{0} complete"
forge-install = "Forge installation is starting, please wait... "
}
i18n {
current-not-available = "Current locale {0} is not available"

View File

@ -12,6 +12,22 @@ logo = [
" §a构建日期 {}"
""
]
downloader {
info = [
"文件不完整,正在自动下载"
"BMCLAPI 提供下载服务 https://bmclapidoc.bangbang93.com"
"支持 MinecraftForge 项目 https://www.patreon.com/LexManos/"
]
http-error = "HTTP 错误 {0} {1}"
not-found = "文件不存在 {0}"
redirect-error = "重定向错误 {0}"
dir = "下载地址已经有存在的文件夹 {0}"
timeout = "下载超时 {0} {1}"
hash-not-match = "文件 sha1 不符,期望 {0} 实际 {1}: {2}"
maven-fail = "{0} 下载失败 {1}"
complete = "{0} 下载完成"
forge-install = "即将开始 Forge 安装,请等待一段时间"
}
i18n {
current-not-available = "选择的语言 {0} 不可用"