Synchronize HMCL updates
This commit is contained in:
parent
5cc743df5e
commit
d8170d0077
|
@ -23,6 +23,7 @@ import com.tungsten.fclcore.game.GameDirectoryType;
|
||||||
import com.tungsten.fclcore.game.JavaVersion;
|
import com.tungsten.fclcore.game.JavaVersion;
|
||||||
import com.tungsten.fclcore.game.LaunchOptions;
|
import com.tungsten.fclcore.game.LaunchOptions;
|
||||||
import com.tungsten.fclcore.game.Version;
|
import com.tungsten.fclcore.game.Version;
|
||||||
|
import com.tungsten.fclcore.mod.ModAdviser;
|
||||||
import com.tungsten.fclcore.mod.Modpack;
|
import com.tungsten.fclcore.mod.Modpack;
|
||||||
import com.tungsten.fclcore.mod.ModpackConfiguration;
|
import com.tungsten.fclcore.mod.ModpackConfiguration;
|
||||||
import com.tungsten.fclcore.mod.ModpackProvider;
|
import com.tungsten.fclcore.mod.ModpackProvider;
|
||||||
|
@ -158,17 +159,7 @@ public class FCLGameRepository extends DefaultGameRepository {
|
||||||
File srcGameDir = getRunDirectory(srcId);
|
File srcGameDir = getRunDirectory(srcId);
|
||||||
File dstGameDir = getRunDirectory(dstId);
|
File dstGameDir = getRunDirectory(dstId);
|
||||||
|
|
||||||
List<String> blackList = new ArrayList<>(Arrays.asList(
|
List<String> blackList = new ArrayList<>(ModAdviser.MODPACK_BLACK_LIST);
|
||||||
"regex:(.*?)\\.log",
|
|
||||||
"usernamecache.json", "usercache.json", // Minecraft
|
|
||||||
"launcher_profiles.json", "launcher.pack.lzma", // Minecraft Launcher
|
|
||||||
"backup", "pack.json", "launcher.jar", "cache", // FCL
|
|
||||||
".curseclient", // Curse
|
|
||||||
".fabric", ".mixin.out", // Fabric
|
|
||||||
"jars", "logs", "versions", "assets", "libraries", "crash-reports", "NVIDIA", "AMD", "screenshots", "natives", "native", "$native", "server-resource-packs", // Minecraft
|
|
||||||
"downloads", // Curse
|
|
||||||
"asm", "backups", "TCNodeTracker", "CustomDISkins", "data", "CustomSkinLoader/caches" // Mods
|
|
||||||
));
|
|
||||||
blackList.add(srcId + ".jar");
|
blackList.add(srcId + ".jar");
|
||||||
blackList.add(srcId + ".json");
|
blackList.add(srcId + ".json");
|
||||||
if (!copySaves)
|
if (!copySaves)
|
||||||
|
@ -324,8 +315,8 @@ public class FCLGameRepository extends DefaultGameRepository {
|
||||||
.setVersionName(version)
|
.setVersionName(version)
|
||||||
.setProfileName(FCLPath.CONTEXT.getString(R.string.app_name))
|
.setProfileName(FCLPath.CONTEXT.getString(R.string.app_name))
|
||||||
.setGameArguments(StringUtils.tokenize(vs.getMinecraftArgs()))
|
.setGameArguments(StringUtils.tokenize(vs.getMinecraftArgs()))
|
||||||
.setJavaArguments(StringUtils.tokenize(vs.getJavaArgs()))
|
.setOverrideJavaArguments(StringUtils.tokenize(vs.getJavaArgs()))
|
||||||
.setMaxMemory((int)(getAllocatedMemory(
|
.setMaxMemory(vs.isAutoMemory() ? null : (int)(getAllocatedMemory(
|
||||||
vs.getMaxMemory() * 1024L * 1024L,
|
vs.getMaxMemory() * 1024L * 1024L,
|
||||||
MemoryUtils.getFreeDeviceMemory(FCLPath.CONTEXT),
|
MemoryUtils.getFreeDeviceMemory(FCLPath.CONTEXT),
|
||||||
vs.isAutoMemory()
|
vs.isAutoMemory()
|
||||||
|
@ -352,6 +343,9 @@ public class FCLGameRepository extends DefaultGameRepository {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (vs.isAutoMemory() && builder.getJavaArguments().stream().anyMatch(it -> it.startsWith("-Xmx")))
|
||||||
|
builder.setMaxMemory(null);
|
||||||
|
|
||||||
return builder.create();
|
return builder.create();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -409,7 +403,7 @@ public class FCLGameRepository extends DefaultGameRepository {
|
||||||
|
|
||||||
public static long getAllocatedMemory(long minimum, long available, boolean auto) {
|
public static long getAllocatedMemory(long minimum, long available, boolean auto) {
|
||||||
if (auto) {
|
if (auto) {
|
||||||
available -= 256 * 1024 * 1024;
|
available -= 384 * 1024 * 1024; // Reserve 384MiB memory for off-heap memory and HMCL itself
|
||||||
if (available <= 0) {
|
if (available <= 0) {
|
||||||
return minimum;
|
return minimum;
|
||||||
}
|
}
|
||||||
|
@ -418,7 +412,7 @@ public class FCLGameRepository extends DefaultGameRepository {
|
||||||
final long suggested = Math.min(available <= threshold
|
final long suggested = Math.min(available <= threshold
|
||||||
? (long) (available * 0.8)
|
? (long) (available * 0.8)
|
||||||
: (long) (threshold * 0.8 + (available - threshold) * 0.2),
|
: (long) (threshold * 0.8 + (available - threshold) * 0.2),
|
||||||
32736L * 1024 * 1024); // Limit the maximum suggested memory to ensure that compressed oops are available
|
16384L * 1024 * 1024);
|
||||||
return Math.max(minimum, suggested);
|
return Math.max(minimum, suggested);
|
||||||
} else {
|
} else {
|
||||||
return minimum;
|
return minimum;
|
||||||
|
|
|
@ -20,7 +20,7 @@ public abstract class LocalizedRemoteModRepository implements RemoteModRepositor
|
||||||
@Override
|
@Override
|
||||||
public Stream<RemoteMod> search(String gameVersion, Category category, int pageOffset, int pageSize, String searchFilter, SortType sort, SortOrder sortOrder) throws IOException {
|
public Stream<RemoteMod> search(String gameVersion, Category category, int pageOffset, int pageSize, String searchFilter, SortType sort, SortOrder sortOrder) throws IOException {
|
||||||
String newSearchFilter;
|
String newSearchFilter;
|
||||||
if (StringUtils.CHINESE_PATTERN.matcher(searchFilter).find()) {
|
if (StringUtils.containsChinese(searchFilter)) {
|
||||||
ModTranslations modTranslations = ModTranslations.getTranslationsByRepositoryType(getType());
|
ModTranslations modTranslations = ModTranslations.getTranslationsByRepositoryType(getType());
|
||||||
List<ModTranslations.Mod> mods = modTranslations.searchMod(searchFilter);
|
List<ModTranslations.Mod> mods = modTranslations.searchMod(searchFilter);
|
||||||
List<String> searchFilters = new ArrayList<>();
|
List<String> searchFilters = new ArrayList<>();
|
||||||
|
|
|
@ -41,12 +41,17 @@ public final class LogExporter {
|
||||||
|
|
||||||
return CompletableFuture.runAsync(() -> {
|
return CompletableFuture.runAsync(() -> {
|
||||||
try (Zipper zipper = new Zipper(zipFile)) {
|
try (Zipper zipper = new Zipper(zipFile)) {
|
||||||
if (Files.exists(runDirectory.resolve("logs").resolve("debug.log"))) {
|
Path logsDir = runDirectory.resolve("logs");
|
||||||
zipper.putFile(runDirectory.resolve("logs").resolve("debug.log"), "debug.log");
|
if (Files.exists(logsDir.resolve("debug.log"))) {
|
||||||
|
zipper.putFile(logsDir.resolve("debug.log"), "debug.log");
|
||||||
}
|
}
|
||||||
if (Files.exists(runDirectory.resolve("logs").resolve("latest.log"))) {
|
if (Files.exists(logsDir.resolve("latest.log"))) {
|
||||||
zipper.putFile(runDirectory.resolve("logs").resolve("latest.log"), "latest.log");
|
zipper.putFile(logsDir.resolve("latest.log"), "latest.log");
|
||||||
}
|
}
|
||||||
|
if (Files.exists(logsDir.resolve("fml-client-latest.log"))) {
|
||||||
|
zipper.putFile(logsDir.resolve("fml-client-latest.log"), "fml-client-latest.log");
|
||||||
|
}
|
||||||
|
|
||||||
zipper.putTextFile(Logging.getLogs(), "fcl.log");
|
zipper.putTextFile(Logging.getLogs(), "fcl.log");
|
||||||
zipper.putTextFile(logs, "minecraft.log");
|
zipper.putTextFile(logs, "minecraft.log");
|
||||||
|
|
||||||
|
|
|
@ -18,7 +18,6 @@ import com.tungsten.fclcore.util.io.NetworkUtils;
|
||||||
import fi.iki.elonen.NanoHTTPD;
|
import fi.iki.elonen.NanoHTTPD;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.nio.charset.StandardCharsets;
|
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.concurrent.CompletableFuture;
|
import java.util.concurrent.CompletableFuture;
|
||||||
|
@ -88,7 +87,7 @@ public final class OAuthServer extends NanoHTTPD implements OAuth.Session {
|
||||||
|
|
||||||
String html;
|
String html;
|
||||||
try {
|
try {
|
||||||
html = IOUtils.readFullyAsString(OAuthServer.class.getResourceAsStream("/assets/microsoft_auth.html"), StandardCharsets.UTF_8)
|
html = IOUtils.readFullyAsString(OAuthServer.class.getResourceAsStream("/assets/microsoft_auth.html"))
|
||||||
.replace("%close-page%", FCLPath.CONTEXT.getString(R.string.account_methods_microsoft_close_page));
|
.replace("%close-page%", FCLPath.CONTEXT.getString(R.string.account_methods_microsoft_close_page));
|
||||||
} catch (IOException e) {
|
} catch (IOException e) {
|
||||||
Logging.LOG.log(Level.SEVERE, "Failed to load html");
|
Logging.LOG.log(Level.SEVERE, "Failed to load html");
|
||||||
|
|
|
@ -10,7 +10,11 @@ import static com.tungsten.fclcore.util.Pair.pair;
|
||||||
|
|
||||||
import java.io.File;
|
import java.io.File;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
|
import java.io.Reader;
|
||||||
|
import java.nio.file.Files;
|
||||||
|
import java.nio.file.Path;
|
||||||
import java.nio.file.Paths;
|
import java.nio.file.Paths;
|
||||||
|
import java.util.ArrayList;
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
@ -21,6 +25,7 @@ import static java.util.stream.Collectors.toList;
|
||||||
|
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
|
|
||||||
|
import com.google.gson.reflect.TypeToken;
|
||||||
import com.tungsten.fcl.R;
|
import com.tungsten.fcl.R;
|
||||||
import com.tungsten.fcl.game.OAuthServer;
|
import com.tungsten.fcl.game.OAuthServer;
|
||||||
import com.tungsten.fclauncher.FCLPath;
|
import com.tungsten.fclauncher.FCLPath;
|
||||||
|
@ -49,13 +54,16 @@ import com.tungsten.fclcore.auth.offline.OfflineAccountFactory;
|
||||||
import com.tungsten.fclcore.auth.yggdrasil.RemoteAuthenticationException;
|
import com.tungsten.fclcore.auth.yggdrasil.RemoteAuthenticationException;
|
||||||
import com.tungsten.fclcore.auth.yggdrasil.YggdrasilAccount;
|
import com.tungsten.fclcore.auth.yggdrasil.YggdrasilAccount;
|
||||||
import com.tungsten.fclcore.auth.yggdrasil.YggdrasilAccountFactory;
|
import com.tungsten.fclcore.auth.yggdrasil.YggdrasilAccountFactory;
|
||||||
|
import com.tungsten.fclcore.fakefx.beans.InvalidationListener;
|
||||||
import com.tungsten.fclcore.fakefx.beans.Observable;
|
import com.tungsten.fclcore.fakefx.beans.Observable;
|
||||||
import com.tungsten.fclcore.fakefx.beans.property.ObjectProperty;
|
import com.tungsten.fclcore.fakefx.beans.property.ObjectProperty;
|
||||||
import com.tungsten.fclcore.fakefx.beans.property.ReadOnlyListProperty;
|
|
||||||
import com.tungsten.fclcore.fakefx.beans.property.ReadOnlyListWrapper;
|
|
||||||
import com.tungsten.fclcore.fakefx.beans.property.SimpleObjectProperty;
|
import com.tungsten.fclcore.fakefx.beans.property.SimpleObjectProperty;
|
||||||
|
import com.tungsten.fclcore.fakefx.collections.FXCollections;
|
||||||
import com.tungsten.fclcore.fakefx.collections.ObservableList;
|
import com.tungsten.fclcore.fakefx.collections.ObservableList;
|
||||||
import com.tungsten.fclcore.task.Schedulers;
|
import com.tungsten.fclcore.task.Schedulers;
|
||||||
|
import com.tungsten.fclcore.util.InvocationDispatcher;
|
||||||
|
import com.tungsten.fclcore.util.Lang;
|
||||||
|
import com.tungsten.fclcore.util.io.FileUtils;
|
||||||
import com.tungsten.fclcore.util.skin.InvalidSkinException;
|
import com.tungsten.fclcore.util.skin.InvalidSkinException;
|
||||||
|
|
||||||
public final class Accounts {
|
public final class Accounts {
|
||||||
|
@ -80,7 +88,6 @@ public final class Accounts {
|
||||||
public static final YggdrasilAccountFactory FACTORY_MOJANG = YggdrasilAccountFactory.MOJANG;
|
public static final YggdrasilAccountFactory FACTORY_MOJANG = YggdrasilAccountFactory.MOJANG;
|
||||||
public static final AuthlibInjectorAccountFactory FACTORY_AUTHLIB_INJECTOR = new AuthlibInjectorAccountFactory(AUTHLIB_INJECTOR_DOWNLOADER, Accounts::getOrCreateAuthlibInjectorServer);
|
public static final AuthlibInjectorAccountFactory FACTORY_AUTHLIB_INJECTOR = new AuthlibInjectorAccountFactory(AUTHLIB_INJECTOR_DOWNLOADER, Accounts::getOrCreateAuthlibInjectorServer);
|
||||||
public static final MicrosoftAccountFactory FACTORY_MICROSOFT = new MicrosoftAccountFactory(new MicrosoftService(OAUTH_CALLBACK));
|
public static final MicrosoftAccountFactory FACTORY_MICROSOFT = new MicrosoftAccountFactory(new MicrosoftService(OAUTH_CALLBACK));
|
||||||
public static final BoundAuthlibInjectorAccountFactory FACTORY_LITTLE_SKIN = getAccountFactoryByAuthlibInjectorServer(new AuthlibInjectorServer("https://littleskin.cn/api/yggdrasil/"));
|
|
||||||
public static final List<AccountFactory<?>> FACTORIES = immutableListOf(FACTORY_OFFLINE, FACTORY_MOJANG, FACTORY_MICROSOFT, FACTORY_AUTHLIB_INJECTOR);
|
public static final List<AccountFactory<?>> FACTORIES = immutableListOf(FACTORY_OFFLINE, FACTORY_MOJANG, FACTORY_MICROSOFT, FACTORY_AUTHLIB_INJECTOR);
|
||||||
|
|
||||||
// ==== login type / account factory mapping ====
|
// ==== login type / account factory mapping ====
|
||||||
|
@ -129,59 +136,20 @@ public final class Accounts {
|
||||||
throw new IllegalArgumentException("Failed to determine account type: " + account);
|
throw new IllegalArgumentException("Failed to determine account type: " + account);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static final String GLOBAL_PREFIX = "$GLOBAL:";
|
||||||
|
private static final ObservableList<Map<Object, Object>> globalAccountStorages = FXCollections.observableArrayList();
|
||||||
|
|
||||||
private static final ObservableList<Account> accounts = observableArrayList(account -> new Observable[] { account });
|
private static final ObservableList<Account> accounts = observableArrayList(account -> new Observable[] { account });
|
||||||
private static final ReadOnlyListWrapper<Account> accountsWrapper = new ReadOnlyListWrapper<>(Accounts.class, "accounts", accounts);
|
private static final ObjectProperty<Account> selectedAccount = new SimpleObjectProperty<>(Accounts.class, "selectedAccount");
|
||||||
|
|
||||||
private static final ObjectProperty<Account> selectedAccount = new SimpleObjectProperty<Account>(Accounts.class, "selectedAccount") {
|
|
||||||
{
|
|
||||||
accounts.addListener(onInvalidating(this::invalidated));
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected void invalidated() {
|
|
||||||
// this methods first checks whether the current selection is valid
|
|
||||||
// if it's valid, the underlying storage will be updated
|
|
||||||
// otherwise, the first account will be selected as an alternative(or null if accounts is empty)
|
|
||||||
Account selected = get();
|
|
||||||
if (accounts.isEmpty()) {
|
|
||||||
if (selected == null) {
|
|
||||||
// valid
|
|
||||||
} else {
|
|
||||||
// the previously selected account is gone, we can only set it to null here
|
|
||||||
set(null);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if (accounts.contains(selected)) {
|
|
||||||
// valid
|
|
||||||
} else {
|
|
||||||
// the previously selected account is gone
|
|
||||||
set(accounts.get(0));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// selection is valid, store it
|
|
||||||
if (!initialized)
|
|
||||||
return;
|
|
||||||
updateAccountStorages();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* True if {@link #init()} hasn't been called.
|
* True if {@link #init()} hasn't been called.
|
||||||
*/
|
*/
|
||||||
private static boolean initialized = false;
|
private static boolean initialized = false;
|
||||||
|
|
||||||
static {
|
|
||||||
accounts.addListener(onInvalidating(Accounts::updateAccountStorages));
|
|
||||||
}
|
|
||||||
|
|
||||||
private static Map<Object, Object> getAccountStorage(Account account) {
|
private static Map<Object, Object> getAccountStorage(Account account) {
|
||||||
Map<Object, Object> storage = account.toStorage();
|
Map<Object, Object> storage = account.toStorage();
|
||||||
storage.put("type", getLoginType(getAccountFactory(account)));
|
storage.put("type", getLoginType(getAccountFactory(account)));
|
||||||
if (account == selectedAccount.get()) {
|
|
||||||
storage.put("selected", true);
|
|
||||||
}
|
|
||||||
return storage;
|
return storage;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -191,7 +159,67 @@ public final class Accounts {
|
||||||
if (!initialized)
|
if (!initialized)
|
||||||
return;
|
return;
|
||||||
// update storage
|
// update storage
|
||||||
config().getAccountStorages().setAll(accounts.stream().map(Accounts::getAccountStorage).collect(toList()));
|
|
||||||
|
ArrayList<Map<Object, Object>> global = new ArrayList<>();
|
||||||
|
ArrayList<Map<Object, Object>> portable = new ArrayList<>();
|
||||||
|
|
||||||
|
for (Account account : accounts) {
|
||||||
|
Map<Object, Object> storage = getAccountStorage(account);
|
||||||
|
if (account.isPortable())
|
||||||
|
portable.add(storage);
|
||||||
|
else
|
||||||
|
global.add(storage);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!global.equals(globalAccountStorages))
|
||||||
|
globalAccountStorages.setAll(global);
|
||||||
|
if (!portable.equals(config().getAccountStorages()))
|
||||||
|
config().getAccountStorages().setAll(portable);
|
||||||
|
}
|
||||||
|
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
private static void loadGlobalAccountStorages() {
|
||||||
|
Path globalAccountsFile = new File(FCLPath.FILES_DIR, "accounts.json").toPath();
|
||||||
|
if (Files.exists(globalAccountsFile)) {
|
||||||
|
try (Reader reader = Files.newBufferedReader(globalAccountsFile)) {
|
||||||
|
globalAccountStorages.setAll((List<Map<Object, Object>>)
|
||||||
|
Config.CONFIG_GSON.fromJson(reader, new TypeToken<List<Map<Object, Object>>>() {
|
||||||
|
}.getType()));
|
||||||
|
} catch (Throwable e) {
|
||||||
|
LOG.log(Level.WARNING, "Failed to load global accounts", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
InvocationDispatcher<String> dispatcher = InvocationDispatcher.runOn(Lang::thread, json -> {
|
||||||
|
LOG.info("Saving global accounts");
|
||||||
|
synchronized (globalAccountsFile) {
|
||||||
|
try {
|
||||||
|
synchronized (globalAccountsFile) {
|
||||||
|
FileUtils.saveSafely(globalAccountsFile, json);
|
||||||
|
}
|
||||||
|
} catch (IOException e) {
|
||||||
|
LOG.log(Level.SEVERE, "Failed to save global accounts", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
globalAccountStorages.addListener(onInvalidating(() ->
|
||||||
|
dispatcher.accept(Config.CONFIG_GSON.toJson(globalAccountStorages))));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Account parseAccount(Map<Object, Object> storage) {
|
||||||
|
AccountFactory<?> factory = type2factory.get(storage.get("type"));
|
||||||
|
if (factory == null) {
|
||||||
|
LOG.warning("Unrecognized account type: " + storage);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
return factory.fromStorage(storage);
|
||||||
|
} catch (Exception e) {
|
||||||
|
LOG.log(Level.WARNING, "Failed to load account: " + storage, e);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -201,53 +229,102 @@ public final class Accounts {
|
||||||
if (initialized)
|
if (initialized)
|
||||||
throw new IllegalStateException("Already initialized");
|
throw new IllegalStateException("Already initialized");
|
||||||
|
|
||||||
// load accounts
|
loadGlobalAccountStorages();
|
||||||
config().getAccountStorages().forEach(storage -> {
|
|
||||||
AccountFactory<?> factory = type2factory.get(storage.get("type"));
|
|
||||||
if (factory == null) {
|
|
||||||
LOG.warning("Unrecognized account type: " + storage);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
Account account;
|
|
||||||
try {
|
|
||||||
account = factory.fromStorage(storage);
|
|
||||||
} catch (Exception e) {
|
|
||||||
LOG.log(Level.WARNING, "Failed to load account: " + storage, e);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
accounts.add(account);
|
|
||||||
|
|
||||||
if (Boolean.TRUE.equals(storage.get("selected"))) {
|
// load accounts
|
||||||
selectedAccount.set(account);
|
Account selected = null;
|
||||||
|
for (Map<Object, Object> storage : config().getAccountStorages()) {
|
||||||
|
Account account = parseAccount(storage);
|
||||||
|
if (account != null) {
|
||||||
|
account.setPortable(true);
|
||||||
|
accounts.add(account);
|
||||||
|
if (Boolean.TRUE.equals(storage.get("selected"))) {
|
||||||
|
selected = account;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
}
|
||||||
|
|
||||||
|
for (Map<Object, Object> storage : globalAccountStorages) {
|
||||||
|
Account account = parseAccount(storage);
|
||||||
|
if (account != null) {
|
||||||
|
accounts.add(account);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
String selectedAccountIdentifier = config().getSelectedAccount();
|
||||||
|
if (selected == null && selectedAccountIdentifier != null) {
|
||||||
|
boolean portable = true;
|
||||||
|
if (selectedAccountIdentifier.startsWith(GLOBAL_PREFIX)) {
|
||||||
|
portable = false;
|
||||||
|
selectedAccountIdentifier = selectedAccountIdentifier.substring(GLOBAL_PREFIX.length());
|
||||||
|
}
|
||||||
|
|
||||||
|
for (Account account : accounts) {
|
||||||
|
if (selectedAccountIdentifier.equals(account.getIdentifier())) {
|
||||||
|
if (portable == account.isPortable()) {
|
||||||
|
selected = account;
|
||||||
|
break;
|
||||||
|
} else if (selected == null) {
|
||||||
|
selected = account;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (selected == null && !accounts.isEmpty()) {
|
||||||
|
selected = accounts.get(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
selectedAccount.set(selected);
|
||||||
|
|
||||||
|
InvalidationListener listener = o -> {
|
||||||
|
// this method first checks whether the current selection is valid
|
||||||
|
// if it's valid, the underlying storage will be updated
|
||||||
|
// otherwise, the first account will be selected as an alternative(or null if accounts is empty)
|
||||||
|
Account account = selectedAccount.get();
|
||||||
|
if (accounts.isEmpty()) {
|
||||||
|
if (account == null) {
|
||||||
|
// valid
|
||||||
|
} else {
|
||||||
|
// the previously selected account is gone, we can only set it to null here
|
||||||
|
selectedAccount.set(null);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (accounts.contains(account)) {
|
||||||
|
// valid
|
||||||
|
} else {
|
||||||
|
// the previously selected account is gone
|
||||||
|
selectedAccount.set(accounts.get(0));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
selectedAccount.addListener(listener);
|
||||||
|
selectedAccount.addListener(onInvalidating(() -> {
|
||||||
|
Account account = selectedAccount.get();
|
||||||
|
if (account != null)
|
||||||
|
config().setSelectedAccount(account.isPortable() ? account.getIdentifier() : GLOBAL_PREFIX + account.getIdentifier());
|
||||||
|
else
|
||||||
|
config().setSelectedAccount(null);
|
||||||
|
}));
|
||||||
|
accounts.addListener(listener);
|
||||||
|
accounts.addListener(onInvalidating(Accounts::updateAccountStorages));
|
||||||
|
|
||||||
initialized = true;
|
initialized = true;
|
||||||
|
|
||||||
config().getAuthlibInjectorServers().addListener(onInvalidating(Accounts::removeDanglingAuthlibInjectorAccounts));
|
config().getAuthlibInjectorServers().addListener(onInvalidating(Accounts::removeDanglingAuthlibInjectorAccounts));
|
||||||
|
|
||||||
Account selected = selectedAccount.get();
|
|
||||||
if (selected != null) {
|
if (selected != null) {
|
||||||
|
Account finalSelected = selected;
|
||||||
Schedulers.io().execute(() -> {
|
Schedulers.io().execute(() -> {
|
||||||
try {
|
try {
|
||||||
selected.logIn();
|
finalSelected.logIn();
|
||||||
} catch (AuthenticationException e) {
|
} catch (Throwable e) {
|
||||||
LOG.log(Level.WARNING, "Failed to log " + selected + " in", e);
|
LOG.log(Level.WARNING, "Failed to log " + finalSelected + " in", e);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!config().getAuthlibInjectorServers().isEmpty()) {
|
triggerAuthlibInjectorUpdateCheck();
|
||||||
triggerAuthlibInjectorUpdateCheck();
|
|
||||||
}
|
|
||||||
|
|
||||||
Schedulers.io().execute(() -> {
|
|
||||||
try {
|
|
||||||
FACTORY_LITTLE_SKIN.getServer().fetchMetadataResponse();
|
|
||||||
} catch (IOException e) {
|
|
||||||
LOG.log(Level.WARNING, "Failed to fetch authlib-injector server metdata: " + FACTORY_LITTLE_SKIN.getServer(), e);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
for (AuthlibInjectorServer server : config().getAuthlibInjectorServers()) {
|
for (AuthlibInjectorServer server : config().getAuthlibInjectorServers()) {
|
||||||
if (selected instanceof AuthlibInjectorAccount && ((AuthlibInjectorAccount) selected).getServer() == server)
|
if (selected instanceof AuthlibInjectorAccount && ((AuthlibInjectorAccount) selected).getServer() == server)
|
||||||
|
@ -266,10 +343,6 @@ public final class Accounts {
|
||||||
return accounts;
|
return accounts;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static ReadOnlyListProperty<Account> accountsProperty() {
|
|
||||||
return accountsWrapper.getReadOnlyProperty();
|
|
||||||
}
|
|
||||||
|
|
||||||
public static Account getSelectedAccount() {
|
public static Account getSelectedAccount() {
|
||||||
return selectedAccount.get();
|
return selectedAccount.get();
|
||||||
}
|
}
|
||||||
|
@ -330,7 +403,7 @@ public final class Accounts {
|
||||||
}
|
}
|
||||||
// ====
|
// ====
|
||||||
|
|
||||||
// ==== Login type name i18n ===
|
// ==== Login type name ===
|
||||||
private static final Map<AccountFactory<?>, Integer> unlocalizedLoginTypeNames = mapOf(
|
private static final Map<AccountFactory<?>, Integer> unlocalizedLoginTypeNames = mapOf(
|
||||||
pair(Accounts.FACTORY_OFFLINE, R.string.account_methods_offline),
|
pair(Accounts.FACTORY_OFFLINE, R.string.account_methods_offline),
|
||||||
pair(Accounts.FACTORY_MOJANG, R.string.account_methods_yggdrasil),
|
pair(Accounts.FACTORY_MOJANG, R.string.account_methods_yggdrasil),
|
||||||
|
|
|
@ -9,11 +9,11 @@ import com.tungsten.fclcore.auth.authlibinjector.AuthlibInjectorServer;
|
||||||
import com.tungsten.fclcore.fakefx.beans.InvalidationListener;
|
import com.tungsten.fclcore.fakefx.beans.InvalidationListener;
|
||||||
import com.tungsten.fclcore.fakefx.beans.Observable;
|
import com.tungsten.fclcore.fakefx.beans.Observable;
|
||||||
import com.tungsten.fclcore.fakefx.beans.property.BooleanProperty;
|
import com.tungsten.fclcore.fakefx.beans.property.BooleanProperty;
|
||||||
import com.tungsten.fclcore.fakefx.beans.property.DoubleProperty;
|
|
||||||
import com.tungsten.fclcore.fakefx.beans.property.IntegerProperty;
|
import com.tungsten.fclcore.fakefx.beans.property.IntegerProperty;
|
||||||
|
import com.tungsten.fclcore.fakefx.beans.property.MapProperty;
|
||||||
import com.tungsten.fclcore.fakefx.beans.property.SimpleBooleanProperty;
|
import com.tungsten.fclcore.fakefx.beans.property.SimpleBooleanProperty;
|
||||||
import com.tungsten.fclcore.fakefx.beans.property.SimpleDoubleProperty;
|
|
||||||
import com.tungsten.fclcore.fakefx.beans.property.SimpleIntegerProperty;
|
import com.tungsten.fclcore.fakefx.beans.property.SimpleIntegerProperty;
|
||||||
|
import com.tungsten.fclcore.fakefx.beans.property.SimpleMapProperty;
|
||||||
import com.tungsten.fclcore.fakefx.beans.property.SimpleStringProperty;
|
import com.tungsten.fclcore.fakefx.beans.property.SimpleStringProperty;
|
||||||
import com.tungsten.fclcore.fakefx.beans.property.StringProperty;
|
import com.tungsten.fclcore.fakefx.beans.property.StringProperty;
|
||||||
import com.tungsten.fclcore.fakefx.collections.FXCollections;
|
import com.tungsten.fclcore.fakefx.collections.FXCollections;
|
||||||
|
@ -38,7 +38,7 @@ public final class Config implements Cloneable, Observable {
|
||||||
|
|
||||||
public static final int CURRENT_UI_VERSION = 0;
|
public static final int CURRENT_UI_VERSION = 0;
|
||||||
|
|
||||||
private static final Gson CONFIG_GSON = new GsonBuilder()
|
public static final Gson CONFIG_GSON = new GsonBuilder()
|
||||||
.registerTypeAdapter(File.class, FileTypeAdapter.INSTANCE)
|
.registerTypeAdapter(File.class, FileTypeAdapter.INSTANCE)
|
||||||
.registerTypeAdapter(ObservableList.class, new ObservableListCreator())
|
.registerTypeAdapter(ObservableList.class, new ObservableListCreator())
|
||||||
.registerTypeAdapter(ObservableSet.class, new ObservableSetCreator())
|
.registerTypeAdapter(ObservableSet.class, new ObservableSetCreator())
|
||||||
|
@ -80,7 +80,10 @@ public final class Config implements Cloneable, Observable {
|
||||||
private StringProperty versionListSource = new SimpleStringProperty("balanced");
|
private StringProperty versionListSource = new SimpleStringProperty("balanced");
|
||||||
|
|
||||||
@SerializedName("configurations")
|
@SerializedName("configurations")
|
||||||
private ObservableMap<String, Profile> configurations = FXCollections.observableMap(new TreeMap<>());
|
private SimpleMapProperty<String, Profile> configurations = new SimpleMapProperty<>(FXCollections.observableMap(new TreeMap<>()));
|
||||||
|
|
||||||
|
@SerializedName("selectedAccount")
|
||||||
|
private StringProperty selectedAccount = new SimpleStringProperty();
|
||||||
|
|
||||||
@SerializedName("accounts")
|
@SerializedName("accounts")
|
||||||
private ObservableList<Map<Object, Object>> accountStorages = FXCollections.observableArrayList();
|
private ObservableList<Map<Object, Object>> accountStorages = FXCollections.observableArrayList();
|
||||||
|
@ -220,10 +223,22 @@ public final class Config implements Cloneable, Observable {
|
||||||
return versionListSource;
|
return versionListSource;
|
||||||
}
|
}
|
||||||
|
|
||||||
public ObservableMap<String, Profile> getConfigurations() {
|
public MapProperty<String, Profile> getConfigurations() {
|
||||||
return configurations;
|
return configurations;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public String getSelectedAccount() {
|
||||||
|
return selectedAccount.get();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setSelectedAccount(String selectedAccount) {
|
||||||
|
this.selectedAccount.set(selectedAccount);
|
||||||
|
}
|
||||||
|
|
||||||
|
public StringProperty selectedAccountProperty() {
|
||||||
|
return selectedAccount;
|
||||||
|
}
|
||||||
|
|
||||||
public ObservableList<Map<Object, Object>> getAccountStorages() {
|
public ObservableList<Map<Object, Object>> getAccountStorages() {
|
||||||
return accountStorages;
|
return accountStorages;
|
||||||
}
|
}
|
||||||
|
|
|
@ -33,7 +33,7 @@ public class Controllers {
|
||||||
public static void checkControllers() {
|
public static void checkControllers() {
|
||||||
if (controllers.isEmpty()) {
|
if (controllers.isEmpty()) {
|
||||||
try {
|
try {
|
||||||
String str = IOUtils.readFullyAsString(Controllers.class.getResourceAsStream("/assets/controllers/Default.json"), StandardCharsets.UTF_8);
|
String str = IOUtils.readFullyAsString(Controllers.class.getResourceAsStream("/assets/controllers/Default.json"));
|
||||||
Controller controller = new GsonBuilder()
|
Controller controller = new GsonBuilder()
|
||||||
.registerTypeAdapterFactory(new JavaFxPropertyTypeAdapterFactory(true, true))
|
.registerTypeAdapterFactory(new JavaFxPropertyTypeAdapterFactory(true, true))
|
||||||
.setPrettyPrinting()
|
.setPrettyPrinting()
|
||||||
|
|
|
@ -8,38 +8,20 @@ import com.tungsten.fclcore.fakefx.beans.property.IntegerProperty;
|
||||||
import com.tungsten.fclcore.fakefx.beans.property.SimpleIntegerProperty;
|
import com.tungsten.fclcore.fakefx.beans.property.SimpleIntegerProperty;
|
||||||
import com.tungsten.fclcore.fakefx.beans.property.SimpleStringProperty;
|
import com.tungsten.fclcore.fakefx.beans.property.SimpleStringProperty;
|
||||||
import com.tungsten.fclcore.fakefx.beans.property.StringProperty;
|
import com.tungsten.fclcore.fakefx.beans.property.StringProperty;
|
||||||
import com.tungsten.fclcore.fakefx.collections.ObservableList;
|
|
||||||
import com.tungsten.fclcore.fakefx.collections.ObservableMap;
|
|
||||||
import com.tungsten.fclcore.fakefx.collections.ObservableSet;
|
|
||||||
import com.tungsten.fclcore.util.fakefx.ObservableHelper;
|
import com.tungsten.fclcore.util.fakefx.ObservableHelper;
|
||||||
import com.tungsten.fclcore.util.fakefx.PropertyUtils;
|
import com.tungsten.fclcore.util.fakefx.PropertyUtils;
|
||||||
import com.tungsten.fclcore.util.gson.FileTypeAdapter;
|
|
||||||
import com.tungsten.fclcore.util.gson.fakefx.creators.ObservableListCreator;
|
|
||||||
import com.tungsten.fclcore.util.gson.fakefx.creators.ObservableMapCreator;
|
|
||||||
import com.tungsten.fclcore.util.gson.fakefx.creators.ObservableSetCreator;
|
|
||||||
import com.tungsten.fclcore.util.gson.fakefx.factories.JavaFxPropertyTypeAdapterFactory;
|
|
||||||
|
|
||||||
import org.jetbrains.annotations.Nullable;
|
import org.jetbrains.annotations.Nullable;
|
||||||
|
|
||||||
import java.io.File;
|
|
||||||
import java.lang.reflect.Type;
|
import java.lang.reflect.Type;
|
||||||
import java.util.*;
|
import java.util.*;
|
||||||
|
|
||||||
@JsonAdapter(GlobalConfig.Serializer.class)
|
@JsonAdapter(GlobalConfig.Serializer.class)
|
||||||
public class GlobalConfig implements Cloneable, Observable {
|
public class GlobalConfig implements Cloneable, Observable {
|
||||||
|
|
||||||
private static final Gson CONFIG_GSON = new GsonBuilder()
|
|
||||||
.registerTypeAdapter(File.class, FileTypeAdapter.INSTANCE)
|
|
||||||
.registerTypeAdapter(ObservableList.class, new ObservableListCreator())
|
|
||||||
.registerTypeAdapter(ObservableSet.class, new ObservableSetCreator())
|
|
||||||
.registerTypeAdapter(ObservableMap.class, new ObservableMapCreator())
|
|
||||||
.registerTypeAdapterFactory(new JavaFxPropertyTypeAdapterFactory(true, true))
|
|
||||||
.setPrettyPrinting()
|
|
||||||
.create();
|
|
||||||
|
|
||||||
@Nullable
|
@Nullable
|
||||||
public static GlobalConfig fromJson(String json) throws JsonParseException {
|
public static GlobalConfig fromJson(String json) throws JsonParseException {
|
||||||
GlobalConfig loaded = CONFIG_GSON.fromJson(json, GlobalConfig.class);
|
GlobalConfig loaded = Config.CONFIG_GSON.fromJson(json, GlobalConfig.class);
|
||||||
if (loaded == null) {
|
if (loaded == null) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
@ -72,7 +54,7 @@ public class GlobalConfig implements Cloneable, Observable {
|
||||||
}
|
}
|
||||||
|
|
||||||
public String toJson() {
|
public String toJson() {
|
||||||
return CONFIG_GSON.toJson(this);
|
return Config.CONFIG_GSON.toJson(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|
|
@ -16,14 +16,15 @@ import com.tungsten.fclcore.fakefx.beans.property.ReadOnlyListWrapper;
|
||||||
import com.tungsten.fclcore.fakefx.beans.property.ReadOnlyStringProperty;
|
import com.tungsten.fclcore.fakefx.beans.property.ReadOnlyStringProperty;
|
||||||
import com.tungsten.fclcore.fakefx.beans.property.ReadOnlyStringWrapper;
|
import com.tungsten.fclcore.fakefx.beans.property.ReadOnlyStringWrapper;
|
||||||
import com.tungsten.fclcore.fakefx.beans.property.SimpleObjectProperty;
|
import com.tungsten.fclcore.fakefx.beans.property.SimpleObjectProperty;
|
||||||
|
import com.tungsten.fclcore.fakefx.collections.FXCollections;
|
||||||
import com.tungsten.fclcore.fakefx.collections.ObservableList;
|
import com.tungsten.fclcore.fakefx.collections.ObservableList;
|
||||||
|
|
||||||
import java.io.File;
|
import java.io.File;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.HashSet;
|
import java.util.HashSet;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.TreeMap;
|
||||||
import java.util.function.Consumer;
|
import java.util.function.Consumer;
|
||||||
import java.util.stream.Collectors;
|
|
||||||
|
|
||||||
public final class Profiles {
|
public final class Profiles {
|
||||||
|
|
||||||
|
@ -103,8 +104,11 @@ public final class Profiles {
|
||||||
if (!initialized)
|
if (!initialized)
|
||||||
return;
|
return;
|
||||||
// update storage
|
// update storage
|
||||||
config().getConfigurations().clear();
|
TreeMap<String, Profile> newConfigurations = new TreeMap<>();
|
||||||
config().getConfigurations().putAll(profiles.stream().collect(Collectors.toMap(Profile::getName, it -> it)));
|
for (Profile profile : profiles) {
|
||||||
|
newConfigurations.put(profile.getName(), profile);
|
||||||
|
}
|
||||||
|
config().getConfigurations().setValue(FXCollections.observableMap(newConfigurations));
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -107,7 +107,7 @@ public enum ModTranslations {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
String modData = IOUtils.readFullyAsString(ModTranslations.class.getResourceAsStream(resourceName), StandardCharsets.UTF_8);
|
String modData = IOUtils.readFullyAsString(ModTranslations.class.getResourceAsStream(resourceName));
|
||||||
mods = Arrays.stream(modData.split("\n")).filter(line -> !line.startsWith("#")).map(Mod::new).collect(Collectors.toList());
|
mods = Arrays.stream(modData.split("\n")).filter(line -> !line.startsWith("#")).map(Mod::new).collect(Collectors.toList());
|
||||||
return true;
|
return true;
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
|
|
|
@ -26,7 +26,7 @@ public class RuntimeUtils {
|
||||||
|
|
||||||
public static boolean isLatest(String targetDir, String srcDir) throws IOException {
|
public static boolean isLatest(String targetDir, String srcDir) throws IOException {
|
||||||
File targetFile = new File(targetDir + "/version");
|
File targetFile = new File(targetDir + "/version");
|
||||||
long version = Long.parseLong(IOUtils.readFullyAsString(RuntimeUtils.class.getResourceAsStream(srcDir + "/version"), StandardCharsets.UTF_8));
|
long version = Long.parseLong(IOUtils.readFullyAsString(RuntimeUtils.class.getResourceAsStream(srcDir + "/version")));
|
||||||
return targetFile.exists() && Long.parseLong(FileUtils.readText(targetFile)) == version;
|
return targetFile.exists() && Long.parseLong(FileUtils.readText(targetFile)) == version;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -43,7 +43,7 @@ public class RuntimeUtils {
|
||||||
new File(targetDir).mkdirs();
|
new File(targetDir).mkdirs();
|
||||||
String universalPath = srcDir + "/universal.tar.xz";
|
String universalPath = srcDir + "/universal.tar.xz";
|
||||||
String archPath = srcDir + "/bin-" + Architecture.archAsString(Architecture.getDeviceArchitecture()) + ".tar.xz";
|
String archPath = srcDir + "/bin-" + Architecture.archAsString(Architecture.getDeviceArchitecture()) + ".tar.xz";
|
||||||
String version = IOUtils.readFullyAsString(RuntimeUtils.class.getResourceAsStream("/assets/" + srcDir + "/version"), StandardCharsets.UTF_8);
|
String version = IOUtils.readFullyAsString(RuntimeUtils.class.getResourceAsStream("/assets/" + srcDir + "/version"));
|
||||||
uncompressTarXZ(context.getAssets().open(universalPath), new File(targetDir));
|
uncompressTarXZ(context.getAssets().open(universalPath), new File(targetDir));
|
||||||
uncompressTarXZ(context.getAssets().open(archPath), new File(targetDir));
|
uncompressTarXZ(context.getAssets().open(archPath), new File(targetDir));
|
||||||
FileUtils.writeText(new File(targetDir + "/version"), version);
|
FileUtils.writeText(new File(targetDir + "/version"), version);
|
||||||
|
|
|
@ -7,11 +7,11 @@ import com.tungsten.fclcore.fakefx.beans.value.WeakChangeListener;
|
||||||
import com.tungsten.fclcore.fakefx.collections.ListChangeListener;
|
import com.tungsten.fclcore.fakefx.collections.ListChangeListener;
|
||||||
import com.tungsten.fclcore.fakefx.collections.WeakListChangeListener;
|
import com.tungsten.fclcore.fakefx.collections.WeakListChangeListener;
|
||||||
|
|
||||||
import java.util.LinkedList;
|
import java.util.ArrayList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
public class WeakListenerHolder {
|
public class WeakListenerHolder {
|
||||||
private List<Object> refs = new LinkedList<>();
|
private final List<Object> refs = new ArrayList<>(0);
|
||||||
|
|
||||||
public WeakListenerHolder() {
|
public WeakListenerHolder() {
|
||||||
}
|
}
|
||||||
|
@ -38,4 +38,4 @@ public class WeakListenerHolder {
|
||||||
public boolean remove(Object obj) {
|
public boolean remove(Object obj) {
|
||||||
return refs.remove(obj);
|
return refs.remove(obj);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -6,10 +6,13 @@ import com.tungsten.fclcore.fakefx.beans.InvalidationListener;
|
||||||
import com.tungsten.fclcore.fakefx.beans.Observable;
|
import com.tungsten.fclcore.fakefx.beans.Observable;
|
||||||
import com.tungsten.fclcore.fakefx.beans.binding.Bindings;
|
import com.tungsten.fclcore.fakefx.beans.binding.Bindings;
|
||||||
import com.tungsten.fclcore.fakefx.beans.binding.ObjectBinding;
|
import com.tungsten.fclcore.fakefx.beans.binding.ObjectBinding;
|
||||||
|
import com.tungsten.fclcore.fakefx.beans.property.BooleanProperty;
|
||||||
|
import com.tungsten.fclcore.fakefx.beans.property.SimpleBooleanProperty;
|
||||||
import com.tungsten.fclcore.util.ToStringBuilder;
|
import com.tungsten.fclcore.util.ToStringBuilder;
|
||||||
import com.tungsten.fclcore.util.fakefx.ObservableHelper;
|
import com.tungsten.fclcore.util.fakefx.ObservableHelper;
|
||||||
|
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
import java.util.Objects;
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
import java.util.UUID;
|
import java.util.UUID;
|
||||||
|
|
||||||
|
@ -48,7 +51,23 @@ public abstract class Account implements Observable {
|
||||||
public void clearCache() {
|
public void clearCache() {
|
||||||
}
|
}
|
||||||
|
|
||||||
private ObservableHelper helper = new ObservableHelper(this);
|
private final BooleanProperty portable = new SimpleBooleanProperty(false);
|
||||||
|
|
||||||
|
public BooleanProperty portableProperty() {
|
||||||
|
return portable;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isPortable() {
|
||||||
|
return portable.get();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setPortable(boolean value) {
|
||||||
|
this.portable.set(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
public abstract String getIdentifier();
|
||||||
|
|
||||||
|
private final ObservableHelper helper = new ObservableHelper(this);
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void addListener(InvalidationListener listener) {
|
public void addListener(InvalidationListener listener) {
|
||||||
|
@ -72,12 +91,29 @@ public abstract class Account implements Observable {
|
||||||
return Bindings.createObjectBinding(Optional::empty);
|
return Bindings.createObjectBinding(Optional::empty);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int hashCode() {
|
||||||
|
return Objects.hash(portable);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean equals(Object obj) {
|
||||||
|
if (this == obj)
|
||||||
|
return true;
|
||||||
|
if (!(obj instanceof Account))
|
||||||
|
return false;
|
||||||
|
|
||||||
|
Account another = (Account) obj;
|
||||||
|
return isPortable() == another.isPortable();
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public String toString() {
|
public String toString() {
|
||||||
return new ToStringBuilder(this)
|
return new ToStringBuilder(this)
|
||||||
.append("username", getUsername())
|
.append("username", getUsername())
|
||||||
.append("character", getCharacter())
|
.append("character", getCharacter())
|
||||||
.append("uuid", getUUID())
|
.append("uuid", getUUID())
|
||||||
|
.append("portable", isPortable())
|
||||||
.toString();
|
.toString();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -7,16 +7,22 @@ import java.io.IOException;
|
||||||
import java.util.UUID;
|
import java.util.UUID;
|
||||||
|
|
||||||
public class AuthInfo implements AutoCloseable {
|
public class AuthInfo implements AutoCloseable {
|
||||||
|
public static final String USER_TYPE_MSA = "msa";
|
||||||
|
public static final String USER_TYPE_MOJANG = "mojang";
|
||||||
|
public static final String USER_TYPE_LEGACY = "legacy";
|
||||||
|
|
||||||
|
|
||||||
private final String username;
|
private final String username;
|
||||||
private final UUID uuid;
|
private final UUID uuid;
|
||||||
private final String accessToken;
|
private final String accessToken;
|
||||||
|
private final String userType;
|
||||||
private final String userProperties;
|
private final String userProperties;
|
||||||
|
|
||||||
public AuthInfo(String username, UUID uuid, String accessToken, String userProperties) {
|
public AuthInfo(String username, UUID uuid, String accessToken, String userType, String userProperties) {
|
||||||
this.username = username;
|
this.username = username;
|
||||||
this.uuid = uuid;
|
this.uuid = uuid;
|
||||||
this.accessToken = accessToken;
|
this.accessToken = accessToken;
|
||||||
|
this.userType = userType;
|
||||||
this.userProperties = userProperties;
|
this.userProperties = userProperties;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -32,6 +38,10 @@ public class AuthInfo implements AutoCloseable {
|
||||||
return accessToken;
|
return accessToken;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public String getUserType() {
|
||||||
|
return userType;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Properties of this user.
|
* Properties of this user.
|
||||||
* Don't know the difference between user properties and user property map.
|
* Don't know the difference between user properties and user property map.
|
||||||
|
|
|
@ -222,7 +222,7 @@ public class OAuth {
|
||||||
*
|
*
|
||||||
* @param url OAuth url.
|
* @param url OAuth url.
|
||||||
*/
|
*/
|
||||||
void openBrowser(String url);
|
void openBrowser(String url) throws IOException;
|
||||||
|
|
||||||
String getClientId();
|
String getClientId();
|
||||||
|
|
||||||
|
@ -236,7 +236,7 @@ public class OAuth {
|
||||||
DEVICE,
|
DEVICE,
|
||||||
}
|
}
|
||||||
|
|
||||||
public class Result {
|
public static final class Result {
|
||||||
private final String accessToken;
|
private final String accessToken;
|
||||||
private final String refreshToken;
|
private final String refreshToken;
|
||||||
|
|
||||||
|
@ -265,7 +265,7 @@ public class OAuth {
|
||||||
@SerializedName("verification_uri")
|
@SerializedName("verification_uri")
|
||||||
public String verificationURI;
|
public String verificationURI;
|
||||||
|
|
||||||
// Life time in seconds for device_code and user_code
|
// Lifetime in seconds for device_code and user_code
|
||||||
@SerializedName("expires_in")
|
@SerializedName("expires_in")
|
||||||
public int expiresIn;
|
public int expiresIn;
|
||||||
|
|
||||||
|
|
|
@ -108,7 +108,7 @@ public class AuthlibInjectorAccount extends YggdrasilAccount {
|
||||||
private final String prefetchedMeta;
|
private final String prefetchedMeta;
|
||||||
|
|
||||||
public AuthlibInjectorAuthInfo(AuthInfo authInfo, AuthlibInjectorArtifactInfo artifact, AuthlibInjectorServer server, String prefetchedMeta) {
|
public AuthlibInjectorAuthInfo(AuthInfo authInfo, AuthlibInjectorArtifactInfo artifact, AuthlibInjectorServer server, String prefetchedMeta) {
|
||||||
super(authInfo.getUsername(), authInfo.getUUID(), authInfo.getAccessToken(), authInfo.getUserProperties());
|
super(authInfo.getUsername(), authInfo.getUUID(), authInfo.getAccessToken(), authInfo.getUserType(), authInfo.getUserProperties());
|
||||||
|
|
||||||
this.artifact = artifact;
|
this.artifact = artifact;
|
||||||
this.server = server;
|
this.server = server;
|
||||||
|
@ -141,6 +141,11 @@ public class AuthlibInjectorAccount extends YggdrasilAccount {
|
||||||
return server;
|
return server;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getIdentifier() {
|
||||||
|
return server.getUrl() + ":" + super.getIdentifier();
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public int hashCode() {
|
public int hashCode() {
|
||||||
return Objects.hash(super.hashCode(), server.hashCode());
|
return Objects.hash(super.hashCode(), server.hashCode());
|
||||||
|
@ -151,7 +156,8 @@ public class AuthlibInjectorAccount extends YggdrasilAccount {
|
||||||
if (obj == null || obj.getClass() != AuthlibInjectorAccount.class)
|
if (obj == null || obj.getClass() != AuthlibInjectorAccount.class)
|
||||||
return false;
|
return false;
|
||||||
AuthlibInjectorAccount another = (AuthlibInjectorAccount) obj;
|
AuthlibInjectorAccount another = (AuthlibInjectorAccount) obj;
|
||||||
return characterUUID.equals(another.characterUUID) && server.equals(another.server);
|
return isPortable() == another.isPortable()
|
||||||
|
&& characterUUID.equals(another.characterUUID) && server.equals(another.server);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@ -169,7 +175,7 @@ public class AuthlibInjectorAccount extends YggdrasilAccount {
|
||||||
return emptySet();
|
return emptySet();
|
||||||
Set<TextureType> result = EnumSet.noneOf(TextureType.class);
|
Set<TextureType> result = EnumSet.noneOf(TextureType.class);
|
||||||
for (String val : prop.split(",")) {
|
for (String val : prop.split(",")) {
|
||||||
val = val.toUpperCase();
|
val = val.toUpperCase(Locale.ROOT);
|
||||||
TextureType parsed;
|
TextureType parsed;
|
||||||
try {
|
try {
|
||||||
parsed = TextureType.valueOf(val);
|
parsed = TextureType.valueOf(val);
|
||||||
|
|
|
@ -65,6 +65,11 @@ public class MicrosoftAccount extends OAuthAccount {
|
||||||
return session.getProfile().getId();
|
return session.getProfile().getId();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getIdentifier() {
|
||||||
|
return "microsoft:" + getUUID();
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public AuthInfo logIn() throws AuthenticationException {
|
public AuthInfo logIn() throws AuthenticationException {
|
||||||
if (!authenticated) {
|
if (!authenticated) {
|
||||||
|
@ -151,6 +156,6 @@ public class MicrosoftAccount extends OAuthAccount {
|
||||||
if (this == o) return true;
|
if (this == o) return true;
|
||||||
if (o == null || getClass() != o.getClass()) return false;
|
if (o == null || getClass() != o.getClass()) return false;
|
||||||
MicrosoftAccount that = (MicrosoftAccount) o;
|
MicrosoftAccount that = (MicrosoftAccount) o;
|
||||||
return characterUUID.equals(that.characterUUID);
|
return this.isPortable() == that.isPortable() && characterUUID.equals(that.characterUUID);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -94,7 +94,7 @@ public class MicrosoftSession {
|
||||||
public AuthInfo toAuthInfo() {
|
public AuthInfo toAuthInfo() {
|
||||||
requireNonNull(profile);
|
requireNonNull(profile);
|
||||||
|
|
||||||
return new AuthInfo(profile.getName(), profile.getId(), accessToken, "{}");
|
return new AuthInfo(profile.getName(), profile.getId(), accessToken, AuthInfo.USER_TYPE_MSA, "{}");
|
||||||
}
|
}
|
||||||
|
|
||||||
public static class User {
|
public static class User {
|
||||||
|
|
|
@ -51,6 +51,10 @@ public class OfflineAccount extends Account {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public AuthlibInjectorArtifactProvider getDownloader() {
|
||||||
|
return downloader;
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public UUID getUUID() {
|
public UUID getUUID() {
|
||||||
return uuid;
|
return uuid;
|
||||||
|
@ -66,6 +70,11 @@ public class OfflineAccount extends Account {
|
||||||
return username;
|
return username;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getIdentifier() {
|
||||||
|
return username + ":" + username;
|
||||||
|
}
|
||||||
|
|
||||||
public Skin getSkin() {
|
public Skin getSkin() {
|
||||||
return skin;
|
return skin;
|
||||||
}
|
}
|
||||||
|
@ -75,7 +84,7 @@ public class OfflineAccount extends Account {
|
||||||
invalidate();
|
invalidate();
|
||||||
}
|
}
|
||||||
|
|
||||||
private boolean loadAuthlibInjector(Skin skin) {
|
protected boolean loadAuthlibInjector(Skin skin) {
|
||||||
if (skin == null) return false;
|
if (skin == null) return false;
|
||||||
if (skin.getType() == Skin.Type.DEFAULT) return false;
|
if (skin.getType() == Skin.Type.DEFAULT) return false;
|
||||||
TextureModel defaultModel = TextureModel.detectUUID(getUUID());
|
TextureModel defaultModel = TextureModel.detectUUID(getUUID());
|
||||||
|
@ -88,7 +97,8 @@ public class OfflineAccount extends Account {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public AuthInfo logIn() throws AuthenticationException {
|
public AuthInfo logIn() throws AuthenticationException {
|
||||||
AuthInfo authInfo = new AuthInfo(username, uuid, UUIDTypeAdapter.fromUUID(UUID.randomUUID()), "{}");
|
// Using "legacy" user type here because "mojang" user type may cause "invalid session token" or "disconnected" when connecting to a game server.
|
||||||
|
AuthInfo authInfo = new AuthInfo(username, uuid, UUIDTypeAdapter.fromUUID(UUID.randomUUID()), AuthInfo.USER_TYPE_MSA, "{}");
|
||||||
|
|
||||||
if (loadAuthlibInjector(skin)) {
|
if (loadAuthlibInjector(skin)) {
|
||||||
CompletableFuture<AuthlibInjectorArtifactInfo> artifactTask = CompletableFuture.supplyAsync(() -> {
|
CompletableFuture<AuthlibInjectorArtifactInfo> artifactTask = CompletableFuture.supplyAsync(() -> {
|
||||||
|
@ -128,7 +138,7 @@ public class OfflineAccount extends Account {
|
||||||
private YggdrasilServer server;
|
private YggdrasilServer server;
|
||||||
|
|
||||||
public OfflineAuthInfo(AuthInfo authInfo, AuthlibInjectorArtifactInfo artifact) {
|
public OfflineAuthInfo(AuthInfo authInfo, AuthlibInjectorArtifactInfo artifact) {
|
||||||
super(authInfo.getUsername(), authInfo.getUUID(), authInfo.getAccessToken(), authInfo.getUserProperties());
|
super(authInfo.getUsername(), authInfo.getUUID(), authInfo.getAccessToken(), USER_TYPE_MSA, authInfo.getUserProperties());
|
||||||
|
|
||||||
this.artifact = artifact;
|
this.artifact = artifact;
|
||||||
}
|
}
|
||||||
|
@ -140,7 +150,8 @@ public class OfflineAccount extends Account {
|
||||||
server.start();
|
server.start();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
server.addCharacter(new YggdrasilServer.Character(uuid, username, skin.load(username).run()));
|
server.addCharacter(new YggdrasilServer.Character(uuid, username,
|
||||||
|
skin != null ? skin.load(username).run() : null));
|
||||||
} catch (IOException e) {
|
} catch (IOException e) {
|
||||||
// ignore
|
// ignore
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
|
@ -182,9 +193,9 @@ public class OfflineAccount extends Account {
|
||||||
Skin.LoadedSkin loadedSkin = skin.load(username).run();
|
Skin.LoadedSkin loadedSkin = skin.load(username).run();
|
||||||
Map<TextureType, Texture> map = new HashMap<>();
|
Map<TextureType, Texture> map = new HashMap<>();
|
||||||
if (loadedSkin != null) {
|
if (loadedSkin != null) {
|
||||||
map.put(TextureType.SKIN, new Texture(null, null, BitmapFactory.decodeStream(loadedSkin.getSkin().getInputStream())));
|
map.put(TextureType.SKIN, new Texture(null, null, loadedSkin.getSkin().getImage()));
|
||||||
if (loadedSkin.getCape() != null) {
|
if (loadedSkin.getCape() != null) {
|
||||||
map.put(TextureType.CAPE, new Texture(null, null, BitmapFactory.decodeStream(loadedSkin.getCape().getInputStream())));
|
map.put(TextureType.CAPE, new Texture(null, null, loadedSkin.getCape().getImage()));
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
map.put(TextureType.SKIN, new Texture(null, null, BitmapFactory.decodeStream(OfflineAccount.class.getResourceAsStream(TextureModel.detectUUID(uuid) == TextureModel.ALEX ? "/assets/img/alex.img" : "/assets/img/steve.png"))));
|
map.put(TextureType.SKIN, new Texture(null, null, BitmapFactory.decodeStream(OfflineAccount.class.getResourceAsStream(TextureModel.detectUUID(uuid) == TextureModel.ALEX ? "/assets/img/alex.img" : "/assets/img/steve.png"))));
|
||||||
|
@ -213,6 +224,6 @@ public class OfflineAccount extends Account {
|
||||||
if (!(obj instanceof OfflineAccount))
|
if (!(obj instanceof OfflineAccount))
|
||||||
return false;
|
return false;
|
||||||
OfflineAccount another = (OfflineAccount) obj;
|
OfflineAccount another = (OfflineAccount) obj;
|
||||||
return username.equals(another.username);
|
return isPortable() == another.isPortable() && username.equals(another.username);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -27,13 +27,21 @@ import java.util.Collections;
|
||||||
import java.util.Locale;
|
import java.util.Locale;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
|
import java.util.function.Function;
|
||||||
|
|
||||||
public class Skin {
|
public class Skin {
|
||||||
|
|
||||||
public enum Type {
|
public enum Type {
|
||||||
DEFAULT,
|
DEFAULT,
|
||||||
STEVE,
|
|
||||||
ALEX,
|
ALEX,
|
||||||
|
ARI,
|
||||||
|
EFE,
|
||||||
|
KAI,
|
||||||
|
MAKENA,
|
||||||
|
NOOR,
|
||||||
|
STEVE,
|
||||||
|
SUNNY,
|
||||||
|
ZURI,
|
||||||
LOCAL_FILE,
|
LOCAL_FILE,
|
||||||
LITTLE_SKIN,
|
LITTLE_SKIN,
|
||||||
CUSTOM_SKIN_LOADER_API,
|
CUSTOM_SKIN_LOADER_API,
|
||||||
|
@ -43,10 +51,24 @@ public class Skin {
|
||||||
switch (type) {
|
switch (type) {
|
||||||
case "default":
|
case "default":
|
||||||
return DEFAULT;
|
return DEFAULT;
|
||||||
case "steve":
|
|
||||||
return STEVE;
|
|
||||||
case "alex":
|
case "alex":
|
||||||
return ALEX;
|
return ALEX;
|
||||||
|
case "ari":
|
||||||
|
return ARI;
|
||||||
|
case "efe":
|
||||||
|
return EFE;
|
||||||
|
case "kai":
|
||||||
|
return KAI;
|
||||||
|
case "makena":
|
||||||
|
return MAKENA;
|
||||||
|
case "noor":
|
||||||
|
return NOOR;
|
||||||
|
case "steve":
|
||||||
|
return STEVE;
|
||||||
|
case "sunny":
|
||||||
|
return SUNNY;
|
||||||
|
case "zuri":
|
||||||
|
return ZURI;
|
||||||
case "local_file":
|
case "local_file":
|
||||||
return LOCAL_FILE;
|
return LOCAL_FILE;
|
||||||
case "little_skin":
|
case "little_skin":
|
||||||
|
@ -61,6 +83,12 @@ public class Skin {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static Function<Type, InputStream> defaultSkinLoader = null;
|
||||||
|
|
||||||
|
public static void registerDefaultSkinLoader(Function<Type, InputStream> defaultSkinLoader0) {
|
||||||
|
defaultSkinLoader = defaultSkinLoader0;
|
||||||
|
}
|
||||||
|
|
||||||
private final Type type;
|
private final Type type;
|
||||||
private final String cslApi;
|
private final String cslApi;
|
||||||
private final TextureModel textureModel;
|
private final TextureModel textureModel;
|
||||||
|
@ -99,10 +127,19 @@ public class Skin {
|
||||||
switch (type) {
|
switch (type) {
|
||||||
case DEFAULT:
|
case DEFAULT:
|
||||||
return Task.supplyAsync(() -> null);
|
return Task.supplyAsync(() -> null);
|
||||||
case STEVE:
|
|
||||||
return Task.supplyAsync(() -> new LoadedSkin(TextureModel.STEVE, Texture.loadTexture(Skin.class.getResourceAsStream("/assets/img/steve.png")), null));
|
|
||||||
case ALEX:
|
case ALEX:
|
||||||
return Task.supplyAsync(() -> new LoadedSkin(TextureModel.ALEX, Texture.loadTexture(Skin.class.getResourceAsStream("/assets/img/alex.png")), null));
|
case ARI:
|
||||||
|
case EFE:
|
||||||
|
case KAI:
|
||||||
|
case MAKENA:
|
||||||
|
case NOOR:
|
||||||
|
case STEVE:
|
||||||
|
case SUNNY:
|
||||||
|
case ZURI:
|
||||||
|
if (defaultSkinLoader == null) {
|
||||||
|
return Task.supplyAsync(() -> null);
|
||||||
|
}
|
||||||
|
return Task.supplyAsync(() -> new LoadedSkin(TextureModel.STEVE, Texture.loadTexture(defaultSkinLoader.apply(type)), null));
|
||||||
case LOCAL_FILE:
|
case LOCAL_FILE:
|
||||||
return Task.supplyAsync(() -> {
|
return Task.supplyAsync(() -> {
|
||||||
Texture skin = null, cape = null;
|
Texture skin = null, cape = null;
|
||||||
|
|
|
@ -1,11 +1,7 @@
|
||||||
package com.tungsten.fclcore.auth.offline;
|
package com.tungsten.fclcore.auth.offline;
|
||||||
|
|
||||||
import java.io.ByteArrayInputStream;
|
|
||||||
import java.io.ByteArrayOutputStream;
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.io.InputStream;
|
import java.io.InputStream;
|
||||||
import java.math.BigInteger;
|
|
||||||
import java.net.URL;
|
|
||||||
import java.security.MessageDigest;
|
import java.security.MessageDigest;
|
||||||
import java.security.NoSuchAlgorithmException;
|
import java.security.NoSuchAlgorithmException;
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
|
@ -16,29 +12,23 @@ import static java.util.Objects.requireNonNull;
|
||||||
import android.graphics.Bitmap;
|
import android.graphics.Bitmap;
|
||||||
import android.graphics.BitmapFactory;
|
import android.graphics.BitmapFactory;
|
||||||
|
|
||||||
public class Texture {
|
import com.tungsten.fclcore.util.Hex;
|
||||||
|
|
||||||
|
public final class Texture {
|
||||||
private final String hash;
|
private final String hash;
|
||||||
private final byte[] data;
|
private final Bitmap image;
|
||||||
|
|
||||||
public Texture(String hash, byte[] data) {
|
public Texture(String hash, Bitmap image) {
|
||||||
this.hash = requireNonNull(hash);
|
this.hash = requireNonNull(hash);
|
||||||
this.data = requireNonNull(data);
|
this.image = requireNonNull(image);
|
||||||
}
|
|
||||||
|
|
||||||
public byte[] getData() {
|
|
||||||
return data;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public String getHash() {
|
public String getHash() {
|
||||||
return hash;
|
return hash;
|
||||||
}
|
}
|
||||||
|
|
||||||
public InputStream getInputStream() {
|
public Bitmap getImage() {
|
||||||
return new ByteArrayInputStream(data);
|
return image;
|
||||||
}
|
|
||||||
|
|
||||||
public int getLength() {
|
|
||||||
return data.length;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private static final Map<String, Texture> textures = new HashMap<>();
|
private static final Map<String, Texture> textures = new HashMap<>();
|
||||||
|
@ -58,8 +48,9 @@ public class Texture {
|
||||||
} catch (NoSuchAlgorithmException e) {
|
} catch (NoSuchAlgorithmException e) {
|
||||||
throw new RuntimeException(e);
|
throw new RuntimeException(e);
|
||||||
}
|
}
|
||||||
int width = img.getWidth();
|
|
||||||
int height = img.getHeight();
|
int width = (int) img.getWidth();
|
||||||
|
int height = (int) img.getHeight();
|
||||||
byte[] buf = new byte[4096];
|
byte[] buf = new byte[4096];
|
||||||
|
|
||||||
putInt(buf, 0, width);
|
putInt(buf, 0, width);
|
||||||
|
@ -82,8 +73,7 @@ public class Texture {
|
||||||
digest.update(buf, 0, pos);
|
digest.update(buf, 0, pos);
|
||||||
}
|
}
|
||||||
|
|
||||||
byte[] sha256 = digest.digest();
|
return Hex.encodeHex(digest.digest());
|
||||||
return String.format("%0" + (sha256.length << 1) + "x", new BigInteger(1, sha256));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void putInt(byte[] array, int offset, int x) {
|
private static void putInt(byte[] array, int offset, int x) {
|
||||||
|
@ -95,22 +85,25 @@ public class Texture {
|
||||||
|
|
||||||
public static Texture loadTexture(InputStream in) throws IOException {
|
public static Texture loadTexture(InputStream in) throws IOException {
|
||||||
if (in == null) return null;
|
if (in == null) return null;
|
||||||
Bitmap img = BitmapFactory.decodeStream(in);
|
Bitmap img;
|
||||||
if (img == null) {
|
try (InputStream is = in) {
|
||||||
throw new IOException("No image found");
|
img = BitmapFactory.decodeStream(is);
|
||||||
}
|
}
|
||||||
|
|
||||||
String hash = computeTextureHash(img);
|
return loadTexture(img);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Texture loadTexture(Bitmap image) {
|
||||||
|
if (image == null) return null;
|
||||||
|
|
||||||
|
String hash = computeTextureHash(image);
|
||||||
|
|
||||||
Texture existent = textures.get(hash);
|
Texture existent = textures.get(hash);
|
||||||
if (existent != null) {
|
if (existent != null) {
|
||||||
return existent;
|
return existent;
|
||||||
}
|
}
|
||||||
|
|
||||||
ByteArrayOutputStream buf = new ByteArrayOutputStream();
|
Texture texture = new Texture(hash, image);
|
||||||
img.compress(Bitmap.CompressFormat.PNG, 100, buf);
|
|
||||||
Texture texture = new Texture(hash, buf.toByteArray());
|
|
||||||
|
|
||||||
existent = textures.putIfAbsent(hash, texture);
|
existent = textures.putIfAbsent(hash, texture);
|
||||||
|
|
||||||
if (existent != null) {
|
if (existent != null) {
|
||||||
|
@ -119,9 +112,4 @@ public class Texture {
|
||||||
return texture;
|
return texture;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static Texture loadTexture(String url) throws IOException {
|
|
||||||
if (url == null) return null;
|
|
||||||
return loadTexture(new URL(url).openStream());
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -11,8 +11,9 @@ import com.tungsten.fclcore.util.Lang;
|
||||||
import com.tungsten.fclcore.util.gson.JsonUtils;
|
import com.tungsten.fclcore.util.gson.JsonUtils;
|
||||||
import com.tungsten.fclcore.util.gson.UUIDTypeAdapter;
|
import com.tungsten.fclcore.util.gson.UUIDTypeAdapter;
|
||||||
import com.tungsten.fclcore.util.io.HttpServer;
|
import com.tungsten.fclcore.util.io.HttpServer;
|
||||||
import com.tungsten.fclcore.util.io.IOUtils;
|
import com.tungsten.fclcore.util.png.fakefx.PNGFakeFXUtils;
|
||||||
|
|
||||||
|
import java.io.ByteArrayInputStream;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.security.*;
|
import java.security.*;
|
||||||
import java.util.*;
|
import java.util.*;
|
||||||
|
@ -22,8 +23,6 @@ import java.util.stream.Stream;
|
||||||
|
|
||||||
import static java.nio.charset.StandardCharsets.UTF_8;
|
import static java.nio.charset.StandardCharsets.UTF_8;
|
||||||
|
|
||||||
import fi.iki.elonen.NanoHTTPD;
|
|
||||||
|
|
||||||
public class YggdrasilServer extends HttpServer {
|
public class YggdrasilServer extends HttpServer {
|
||||||
|
|
||||||
private final Map<UUID, Character> charactersByUuid = new HashMap<>();
|
private final Map<UUID, Character> charactersByUuid = new HashMap<>();
|
||||||
|
@ -32,7 +31,7 @@ public class YggdrasilServer extends HttpServer {
|
||||||
public YggdrasilServer(int port) {
|
public YggdrasilServer(int port) {
|
||||||
super(port);
|
super(port);
|
||||||
|
|
||||||
addRoute(NanoHTTPD.Method.GET, Pattern.compile("^/$"), this::root);
|
addRoute(Method.GET, Pattern.compile("^/$"), this::root);
|
||||||
addRoute(Method.GET, Pattern.compile("/status"), this::status);
|
addRoute(Method.GET, Pattern.compile("/status"), this::status);
|
||||||
addRoute(Method.POST, Pattern.compile("/api/profiles/minecraft"), this::profiles);
|
addRoute(Method.POST, Pattern.compile("/api/profiles/minecraft"), this::profiles);
|
||||||
addRoute(Method.GET, Pattern.compile("/sessionserver/session/minecraft/hasJoined"), this::hasJoined);
|
addRoute(Method.GET, Pattern.compile("/sessionserver/session/minecraft/hasJoined"), this::hasJoined);
|
||||||
|
@ -66,8 +65,7 @@ public class YggdrasilServer extends HttpServer {
|
||||||
}
|
}
|
||||||
|
|
||||||
private Response profiles(Request request) throws IOException {
|
private Response profiles(Request request) throws IOException {
|
||||||
String body = IOUtils.readFullyAsString(request.getSession().getInputStream(), UTF_8);
|
List<String> names = JsonUtils.fromNonNullJsonFully(request.getSession().getInputStream(), new TypeToken<List<String>>() {
|
||||||
List<String> names = JsonUtils.fromNonNullJson(body, new TypeToken<List<String>>() {
|
|
||||||
}.getType());
|
}.getType());
|
||||||
return ok(names.stream().distinct()
|
return ok(names.stream().distinct()
|
||||||
.map(this::findCharacterByName)
|
.map(this::findCharacterByName)
|
||||||
|
@ -115,7 +113,8 @@ public class YggdrasilServer extends HttpServer {
|
||||||
|
|
||||||
if (Texture.hasTexture(hash)) {
|
if (Texture.hasTexture(hash)) {
|
||||||
Texture texture = Texture.getTexture(hash);
|
Texture texture = Texture.getTexture(hash);
|
||||||
Response response = newFixedLengthResponse(Response.Status.OK, "image/png", texture.getInputStream(), texture.getLength());
|
byte[] data = PNGFakeFXUtils.writeImageToArray(texture.getImage());
|
||||||
|
Response response = newFixedLengthResponse(Response.Status.OK, "image/png", new ByteArrayInputStream(data), data.length);
|
||||||
response.addHeader("Etag", String.format("\"%s\"", hash));
|
response.addHeader("Etag", String.format("\"%s\"", hash));
|
||||||
response.addHeader("Cache-Control", "max-age=2592000, public");
|
response.addHeader("Cache-Control", "max-age=2592000, public");
|
||||||
return response;
|
return response;
|
||||||
|
|
|
@ -89,6 +89,11 @@ public class YggdrasilAccount extends ClassicAccount {
|
||||||
return session.getSelectedProfile().getId();
|
return session.getSelectedProfile().getId();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getIdentifier() {
|
||||||
|
return getUsername() + ":" + getUUID();
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public synchronized AuthInfo logIn() throws AuthenticationException {
|
public synchronized AuthInfo logIn() throws AuthenticationException {
|
||||||
if (!authenticated) {
|
if (!authenticated) {
|
||||||
|
@ -214,6 +219,6 @@ public class YggdrasilAccount extends ClassicAccount {
|
||||||
if (obj == null || obj.getClass() != YggdrasilAccount.class)
|
if (obj == null || obj.getClass() != YggdrasilAccount.class)
|
||||||
return false;
|
return false;
|
||||||
YggdrasilAccount another = (YggdrasilAccount) obj;
|
YggdrasilAccount another = (YggdrasilAccount) obj;
|
||||||
return characterUUID.equals(another.characterUUID);
|
return isPortable() == another.isPortable() && characterUUID.equals(another.characterUUID);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -257,6 +257,6 @@ public class YggdrasilService {
|
||||||
.create();
|
.create();
|
||||||
|
|
||||||
public static final String PROFILE_URL = "https://aka.ms/MinecraftMigration";
|
public static final String PROFILE_URL = "https://aka.ms/MinecraftMigration";
|
||||||
public static final String MIGRATION_FAQ_URL = "https://help.minecraft.net/hc/en-us/articles/360050865492-JAVA-Account-Migration-FAQ";
|
public static final String MIGRATION_FAQ_URL = "https://help.minecraft.net/articles/360050865492";
|
||||||
public static final String PURCHASE_URL = "https://www.minecraft.net/store/minecraft-java-bedrock-edition-pc";
|
public static final String PURCHASE_URL = "https://www.microsoft.com/store/productId/9NXP44L49SHJ";
|
||||||
}
|
}
|
||||||
|
|
|
@ -66,6 +66,7 @@ public class YggdrasilSession {
|
||||||
String name = tryCast(storage.get("displayName"), String.class).orElseThrow(() -> new IllegalArgumentException("displayName is missing"));
|
String name = tryCast(storage.get("displayName"), String.class).orElseThrow(() -> new IllegalArgumentException("displayName is missing"));
|
||||||
String clientToken = tryCast(storage.get("clientToken"), String.class).orElseThrow(() -> new IllegalArgumentException("clientToken is missing"));
|
String clientToken = tryCast(storage.get("clientToken"), String.class).orElseThrow(() -> new IllegalArgumentException("clientToken is missing"));
|
||||||
String accessToken = tryCast(storage.get("accessToken"), String.class).orElseThrow(() -> new IllegalArgumentException("accessToken is missing"));
|
String accessToken = tryCast(storage.get("accessToken"), String.class).orElseThrow(() -> new IllegalArgumentException("accessToken is missing"));
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
Map<String, String> userProperties = tryCast(storage.get("userProperties"), Map.class).orElse(null);
|
Map<String, String> userProperties = tryCast(storage.get("userProperties"), Map.class).orElse(null);
|
||||||
return new YggdrasilSession(clientToken, accessToken, new GameProfile(uuid, name), null, userProperties);
|
return new YggdrasilSession(clientToken, accessToken, new GameProfile(uuid, name), null, userProperties);
|
||||||
}
|
}
|
||||||
|
@ -86,7 +87,7 @@ public class YggdrasilSession {
|
||||||
if (selectedProfile == null)
|
if (selectedProfile == null)
|
||||||
throw new IllegalStateException("No character is selected");
|
throw new IllegalStateException("No character is selected");
|
||||||
|
|
||||||
return new AuthInfo(selectedProfile.getName(), selectedProfile.getId(), accessToken,
|
return new AuthInfo(selectedProfile.getName(), selectedProfile.getId(), accessToken, AuthInfo.USER_TYPE_MSA,
|
||||||
Optional.ofNullable(userProperties)
|
Optional.ofNullable(userProperties)
|
||||||
.map(properties -> properties.entrySet().stream()
|
.map(properties -> properties.entrySet().stream()
|
||||||
.collect(Collectors.toMap(Map.Entry::getKey,
|
.collect(Collectors.toMap(Map.Entry::getKey,
|
||||||
|
|
|
@ -7,7 +7,6 @@ import com.tungsten.fclcore.game.Library;
|
||||||
import com.tungsten.fclcore.game.LibraryDownloadInfo;
|
import com.tungsten.fclcore.game.LibraryDownloadInfo;
|
||||||
import com.tungsten.fclcore.util.CacheRepository;
|
import com.tungsten.fclcore.util.CacheRepository;
|
||||||
import com.tungsten.fclcore.util.DigestUtils;
|
import com.tungsten.fclcore.util.DigestUtils;
|
||||||
import com.tungsten.fclcore.util.Hex;
|
|
||||||
import com.tungsten.fclcore.util.Logging;
|
import com.tungsten.fclcore.util.Logging;
|
||||||
import com.tungsten.fclcore.util.gson.JsonUtils;
|
import com.tungsten.fclcore.util.gson.JsonUtils;
|
||||||
import com.tungsten.fclcore.util.gson.TolerableValidationException;
|
import com.tungsten.fclcore.util.gson.TolerableValidationException;
|
||||||
|
@ -83,7 +82,7 @@ public class DefaultCacheRepository extends CacheRepository {
|
||||||
LibraryDownloadInfo info = library.getDownload();
|
LibraryDownloadInfo info = library.getDownload();
|
||||||
String hash = info.getSha1();
|
String hash = info.getSha1();
|
||||||
if (hash != null) {
|
if (hash != null) {
|
||||||
String checksum = Hex.encodeHex(DigestUtils.digest("SHA-1", jar));
|
String checksum = DigestUtils.digestToString("SHA-1", jar);
|
||||||
if (hash.equalsIgnoreCase(checksum))
|
if (hash.equalsIgnoreCase(checksum))
|
||||||
cacheLibrary(library, jar, false);
|
cacheLibrary(library, jar, false);
|
||||||
} else if (library.getChecksums() != null && !library.getChecksums().isEmpty()) {
|
} else if (library.getChecksums() != null && !library.getChecksums().isEmpty()) {
|
||||||
|
@ -136,7 +135,7 @@ public class DefaultCacheRepository extends CacheRepository {
|
||||||
if (Files.exists(jar)) {
|
if (Files.exists(jar)) {
|
||||||
try {
|
try {
|
||||||
if (hash != null) {
|
if (hash != null) {
|
||||||
String checksum = Hex.encodeHex(DigestUtils.digest("SHA-1", jar));
|
String checksum = DigestUtils.digestToString("SHA-1", jar);
|
||||||
if (hash.equalsIgnoreCase(checksum))
|
if (hash.equalsIgnoreCase(checksum))
|
||||||
return Optional.of(restore(jar, () -> cacheLibrary(library, jar, false)));
|
return Optional.of(restore(jar, () -> cacheLibrary(library, jar, false)));
|
||||||
} else if (library.getChecksums() != null && !library.getChecksums().isEmpty()) {
|
} else if (library.getChecksums() != null && !library.getChecksums().isEmpty()) {
|
||||||
|
@ -165,7 +164,7 @@ public class DefaultCacheRepository extends CacheRepository {
|
||||||
public Path cacheLibrary(Library library, Path path, boolean forge) throws IOException {
|
public Path cacheLibrary(Library library, Path path, boolean forge) throws IOException {
|
||||||
String hash = library.getDownload().getSha1();
|
String hash = library.getDownload().getSha1();
|
||||||
if (hash == null)
|
if (hash == null)
|
||||||
hash = Hex.encodeHex(DigestUtils.digest(SHA1, path));
|
hash = DigestUtils.digestToString(SHA1, path);
|
||||||
|
|
||||||
Path cache = getFile(SHA1, hash);
|
Path cache = getFile(SHA1, hash);
|
||||||
FileUtils.copyFile(path.toFile(), cache.toFile());
|
FileUtils.copyFile(path.toFile(), cache.toFile());
|
||||||
|
@ -228,7 +227,7 @@ public class DefaultCacheRepository extends CacheRepository {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private class LibraryIndex implements Validation {
|
private static final class LibraryIndex implements Validation {
|
||||||
private final String name;
|
private final String name;
|
||||||
private final String hash;
|
private final String hash;
|
||||||
private final String type;
|
private final String type;
|
||||||
|
|
|
@ -96,7 +96,7 @@ public final class LibraryAnalyzer implements Iterable<LibraryAnalyzer.LibraryMa
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Remove library by library id
|
* Remove library by library id
|
||||||
* @param libraryId patch id or "forge"/"optifine"/"liteloader"/"fabric"
|
* @param libraryId patch id or "forge"/"optifine"/"liteloader"/"fabric"/"quilt"
|
||||||
* @return this
|
* @return this
|
||||||
*/
|
*/
|
||||||
public LibraryAnalyzer removeLibrary(String libraryId) {
|
public LibraryAnalyzer removeLibrary(String libraryId) {
|
||||||
|
@ -139,7 +139,10 @@ public final class LibraryAnalyzer implements Iterable<LibraryAnalyzer.LibraryMa
|
||||||
public static boolean isModded(VersionProvider provider, Version version) {
|
public static boolean isModded(VersionProvider provider, Version version) {
|
||||||
Version resolvedVersion = version.resolve(provider);
|
Version resolvedVersion = version.resolve(provider);
|
||||||
String mainClass = resolvedVersion.getMainClass();
|
String mainClass = resolvedVersion.getMainClass();
|
||||||
return mainClass != null && (LAUNCH_WRAPPER_MAIN.equals(mainClass) || mainClass.startsWith("net.fabricmc") || mainClass.startsWith("cpw.mods"));
|
return mainClass != null && (LAUNCH_WRAPPER_MAIN.equals(mainClass)
|
||||||
|
|| mainClass.startsWith("net.fabricmc")
|
||||||
|
|| mainClass.startsWith("org.quiltmc")
|
||||||
|
|| mainClass.startsWith("cpw.mods"));
|
||||||
}
|
}
|
||||||
|
|
||||||
public enum LibraryType {
|
public enum LibraryType {
|
||||||
|
@ -225,13 +228,13 @@ public final class LibraryAnalyzer implements Iterable<LibraryAnalyzer.LibraryMa
|
||||||
public static final String BOOTSTRAP_LAUNCHER_MAIN = "cpw.mods.bootstraplauncher.BootstrapLauncher";
|
public static final String BOOTSTRAP_LAUNCHER_MAIN = "cpw.mods.bootstraplauncher.BootstrapLauncher";
|
||||||
|
|
||||||
public static final String[] FORGE_TWEAKERS = new String[] {
|
public static final String[] FORGE_TWEAKERS = new String[] {
|
||||||
"net.minecraftforge.legacy._1_5_2.LibraryFixerTweaker", // 1.5.2
|
"net.minecraftforge.legacy._1_5_2.LibraryFixerTweaker", // 1.5.2
|
||||||
"cpw.mods.fml.common.launcher.FMLTweaker", // 1.6.1 ~ 1.7.10
|
"cpw.mods.fml.common.launcher.FMLTweaker", // 1.6.1 ~ 1.7.10
|
||||||
"net.minecraftforge.fml.common.launcher.FMLTweaker" // 1.8 ~ 1.12.2
|
"net.minecraftforge.fml.common.launcher.FMLTweaker" // 1.8 ~ 1.12.2
|
||||||
};
|
};
|
||||||
public static final String[] OPTIFINE_TWEAKERS = new String[] {
|
public static final String[] OPTIFINE_TWEAKERS = new String[] {
|
||||||
"optifine.OptiFineTweaker",
|
"optifine.OptiFineTweaker",
|
||||||
"optifine.OptiFineForgeTweaker"
|
"optifine.OptiFineForgeTweaker"
|
||||||
};
|
};
|
||||||
public static final String LITELOADER_TWEAKER = "com.mumfrey.liteloader.launch.LiteLoaderTweaker";
|
public static final String LITELOADER_TWEAKER = "com.mumfrey.liteloader.launch.LiteLoaderTweaker";
|
||||||
}
|
}
|
|
@ -1,7 +1,5 @@
|
||||||
package com.tungsten.fclcore.download.forge;
|
package com.tungsten.fclcore.download.forge;
|
||||||
|
|
||||||
import static com.tungsten.fclcore.util.DigestUtils.digest;
|
|
||||||
import static com.tungsten.fclcore.util.Hex.encodeHex;
|
|
||||||
import static com.tungsten.fclcore.util.Logging.LOG;
|
import static com.tungsten.fclcore.util.Logging.LOG;
|
||||||
import static com.tungsten.fclcore.util.gson.JsonUtils.fromNonNullJson;
|
import static com.tungsten.fclcore.util.gson.JsonUtils.fromNonNullJson;
|
||||||
|
|
||||||
|
@ -25,6 +23,7 @@ import com.tungsten.fclcore.game.Library;
|
||||||
import com.tungsten.fclcore.game.Version;
|
import com.tungsten.fclcore.game.Version;
|
||||||
import com.tungsten.fclcore.task.FileDownloadTask;
|
import com.tungsten.fclcore.task.FileDownloadTask;
|
||||||
import com.tungsten.fclcore.task.Task;
|
import com.tungsten.fclcore.task.Task;
|
||||||
|
import com.tungsten.fclcore.util.DigestUtils;
|
||||||
import com.tungsten.fclcore.util.SocketServer;
|
import com.tungsten.fclcore.util.SocketServer;
|
||||||
import com.tungsten.fclcore.util.StringUtils;
|
import com.tungsten.fclcore.util.StringUtils;
|
||||||
import com.tungsten.fclcore.util.function.ExceptionalFunction;
|
import com.tungsten.fclcore.util.function.ExceptionalFunction;
|
||||||
|
@ -86,7 +85,7 @@ public class ForgeNewInstallTask extends Task<Version> {
|
||||||
if (Files.exists(artifact)) {
|
if (Files.exists(artifact)) {
|
||||||
String code;
|
String code;
|
||||||
try (InputStream stream = Files.newInputStream(artifact)) {
|
try (InputStream stream = Files.newInputStream(artifact)) {
|
||||||
code = encodeHex(digest("SHA-1", stream));
|
code = (DigestUtils.digestToString("SHA-1", stream));
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!Objects.equals(code, value)) {
|
if (!Objects.equals(code, value)) {
|
||||||
|
@ -173,7 +172,7 @@ public class ForgeNewInstallTask extends Task<Version> {
|
||||||
|
|
||||||
String code;
|
String code;
|
||||||
try (InputStream stream = Files.newInputStream(artifact)) {
|
try (InputStream stream = Files.newInputStream(artifact)) {
|
||||||
code = encodeHex(digest("SHA-1", stream));
|
code = DigestUtils.digestToString("SHA-1", stream);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!Objects.equals(code, entry.getValue())) {
|
if (!Objects.equals(code, entry.getValue())) {
|
||||||
|
|
|
@ -51,8 +51,7 @@ public class ForgeOldInstallTask extends Task<Version> {
|
||||||
InputStream stream = zipFile.getInputStream(zipFile.getEntry("install_profile.json"));
|
InputStream stream = zipFile.getInputStream(zipFile.getEntry("install_profile.json"));
|
||||||
if (stream == null)
|
if (stream == null)
|
||||||
throw new ArtifactMalformedException("Malformed forge installer file, install_profile.json does not exist.");
|
throw new ArtifactMalformedException("Malformed forge installer file, install_profile.json does not exist.");
|
||||||
String json = IOUtils.readFullyAsString(stream);
|
ForgeInstallProfile installProfile = JsonUtils.fromNonNullJsonFully(stream, ForgeInstallProfile.class);
|
||||||
ForgeInstallProfile installProfile = JsonUtils.fromNonNullJson(json, ForgeInstallProfile.class);
|
|
||||||
|
|
||||||
// unpack the universal jar in the installer file.
|
// unpack the universal jar in the installer file.
|
||||||
Library forgeLibrary = new Library(installProfile.getInstall().getPath());
|
Library forgeLibrary = new Library(installProfile.getInstall().getPath());
|
||||||
|
|
|
@ -10,7 +10,6 @@ import com.tungsten.fclcore.game.Version;
|
||||||
import com.tungsten.fclcore.task.FileDownloadTask;
|
import com.tungsten.fclcore.task.FileDownloadTask;
|
||||||
import com.tungsten.fclcore.task.Task;
|
import com.tungsten.fclcore.task.Task;
|
||||||
import com.tungsten.fclcore.util.DigestUtils;
|
import com.tungsten.fclcore.util.DigestUtils;
|
||||||
import com.tungsten.fclcore.util.Hex;
|
|
||||||
import com.tungsten.fclcore.util.StringUtils;
|
import com.tungsten.fclcore.util.StringUtils;
|
||||||
import com.tungsten.fclcore.util.gson.JsonUtils;
|
import com.tungsten.fclcore.util.gson.JsonUtils;
|
||||||
import com.tungsten.fclcore.util.io.FileUtils;
|
import com.tungsten.fclcore.util.io.FileUtils;
|
||||||
|
@ -60,7 +59,7 @@ public final class GameAssetIndexDownloadTask extends Task<Void> {
|
||||||
// verify correctness of file content
|
// verify correctness of file content
|
||||||
if (verifyHashCode) {
|
if (verifyHashCode) {
|
||||||
try {
|
try {
|
||||||
String actualSum = Hex.encodeHex(DigestUtils.digest("SHA-1", assetIndexFile));
|
String actualSum = DigestUtils.digestToString("SHA-1", assetIndexFile);
|
||||||
if (actualSum.equalsIgnoreCase(assetIndexInfo.getSha1()))
|
if (actualSum.equalsIgnoreCase(assetIndexInfo.getSha1()))
|
||||||
return;
|
return;
|
||||||
} catch (IOException e) {
|
} catch (IOException e) {
|
||||||
|
|
|
@ -1,7 +1,5 @@
|
||||||
package com.tungsten.fclcore.download.game;
|
package com.tungsten.fclcore.download.game;
|
||||||
|
|
||||||
import static com.tungsten.fclcore.util.DigestUtils.digest;
|
|
||||||
import static com.tungsten.fclcore.util.Hex.encodeHex;
|
|
||||||
import static com.tungsten.fclcore.util.Logging.LOG;
|
import static com.tungsten.fclcore.util.Logging.LOG;
|
||||||
|
|
||||||
import com.tungsten.fclauncher.FCLPath;
|
import com.tungsten.fclauncher.FCLPath;
|
||||||
|
@ -12,6 +10,7 @@ import com.tungsten.fclcore.game.Library;
|
||||||
import com.tungsten.fclcore.task.DownloadException;
|
import com.tungsten.fclcore.task.DownloadException;
|
||||||
import com.tungsten.fclcore.task.FileDownloadTask;
|
import com.tungsten.fclcore.task.FileDownloadTask;
|
||||||
import com.tungsten.fclcore.task.Task;
|
import com.tungsten.fclcore.task.Task;
|
||||||
|
import com.tungsten.fclcore.util.DigestUtils;
|
||||||
import com.tungsten.fclcore.util.Pack200Utils;
|
import com.tungsten.fclcore.util.Pack200Utils;
|
||||||
import com.tungsten.fclcore.util.io.FileUtils;
|
import com.tungsten.fclcore.util.io.FileUtils;
|
||||||
import com.tungsten.fclcore.util.io.IOUtils;
|
import com.tungsten.fclcore.util.io.IOUtils;
|
||||||
|
@ -165,7 +164,7 @@ public class LibraryDownloadTask extends Task<Void> {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
byte[] fileData = Files.readAllBytes(libPath.toPath());
|
byte[] fileData = Files.readAllBytes(libPath.toPath());
|
||||||
boolean valid = checksums.contains(encodeHex(digest("SHA-1", fileData)));
|
boolean valid = checksums.contains(DigestUtils.digestToString("SHA-1", fileData));
|
||||||
if (!valid && libPath.getName().endsWith(".jar")) {
|
if (!valid && libPath.getName().endsWith(".jar")) {
|
||||||
valid = validateJar(fileData, checksums);
|
valid = validateJar(fileData, checksums);
|
||||||
}
|
}
|
||||||
|
@ -187,7 +186,7 @@ public class LibraryDownloadTask extends Task<Void> {
|
||||||
hashes = new String(eData, StandardCharsets.UTF_8).split("\n");
|
hashes = new String(eData, StandardCharsets.UTF_8).split("\n");
|
||||||
}
|
}
|
||||||
if (!entry.isDirectory()) {
|
if (!entry.isDirectory()) {
|
||||||
files.put(entry.getName(), encodeHex(digest("SHA-1", eData)));
|
files.put(entry.getName(), DigestUtils.digestToString("SHA-1", eData));
|
||||||
}
|
}
|
||||||
entry = jar.getNextJarEntry();
|
entry = jar.getNextJarEntry();
|
||||||
}
|
}
|
||||||
|
@ -255,8 +254,6 @@ public class LibraryDownloadTask extends Task<Void> {
|
||||||
jos.closeEntry();
|
jos.closeEntry();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (temp.toFile().exists()) {
|
Files.delete(temp);
|
||||||
Files.delete(temp);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -10,8 +10,7 @@ public final class EventBus {
|
||||||
|
|
||||||
@SuppressWarnings("unchecked")
|
@SuppressWarnings("unchecked")
|
||||||
public <T extends Event> EventManager<T> channel(Class<T> clazz) {
|
public <T extends Event> EventManager<T> channel(Class<T> clazz) {
|
||||||
events.putIfAbsent(clazz, new EventManager<>());
|
return (EventManager<T>) events.computeIfAbsent(clazz, ignored -> new EventManager<>());
|
||||||
return (EventManager<T>) events.get(clazz);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@SuppressWarnings("unchecked")
|
@SuppressWarnings("unchecked")
|
||||||
|
|
|
@ -44,7 +44,7 @@ public final class Artifact {
|
||||||
String fileName = this.name + "-" + this.version;
|
String fileName = this.name + "-" + this.version;
|
||||||
if (classifier != null) fileName += "-" + this.classifier;
|
if (classifier != null) fileName += "-" + this.classifier;
|
||||||
this.fileName = fileName + "." + this.extension;
|
this.fileName = fileName + "." + this.extension;
|
||||||
this.path = String.format("%s/%s/%s/%s", this.group.replace(".", "/"), this.name, this.version, this.fileName);
|
this.path = String.format("%s/%s/%s/%s", this.group.replace('.', '/'), this.name, this.version, this.fileName);
|
||||||
|
|
||||||
// group:name:version:classifier@extension
|
// group:name:version:classifier@extension
|
||||||
String descriptor = String.format("%s:%s:%s", group, name, version);
|
String descriptor = String.format("%s:%s:%s", group, name, version);
|
||||||
|
@ -68,7 +68,7 @@ public final class Artifact {
|
||||||
throw new IllegalArgumentException("Artifact name is malformed");
|
throw new IllegalArgumentException("Artifact name is malformed");
|
||||||
}
|
}
|
||||||
|
|
||||||
return new Artifact(arr[0].replace("\\", "/"), arr[1], arr[2], arr.length >= 4 ? arr[3] : null, ext);
|
return new Artifact(arr[0].replace('\\', '/'), arr[1], arr[2], arr.length >= 4 ? arr[3] : null, ext);
|
||||||
}
|
}
|
||||||
|
|
||||||
public String getGroup() {
|
public String getGroup() {
|
||||||
|
|
|
@ -2,7 +2,6 @@ package com.tungsten.fclcore.game;
|
||||||
|
|
||||||
import com.google.gson.JsonParseException;
|
import com.google.gson.JsonParseException;
|
||||||
import com.tungsten.fclcore.util.DigestUtils;
|
import com.tungsten.fclcore.util.DigestUtils;
|
||||||
import com.tungsten.fclcore.util.Hex;
|
|
||||||
import com.tungsten.fclcore.util.StringUtils;
|
import com.tungsten.fclcore.util.StringUtils;
|
||||||
import com.tungsten.fclcore.util.gson.Validation;
|
import com.tungsten.fclcore.util.gson.Validation;
|
||||||
|
|
||||||
|
@ -43,6 +42,6 @@ public final class AssetObject implements Validation {
|
||||||
|
|
||||||
public boolean validateChecksum(Path file, boolean defaultValue) throws IOException {
|
public boolean validateChecksum(Path file, boolean defaultValue) throws IOException {
|
||||||
if (hash == null) return defaultValue;
|
if (hash == null) return defaultValue;
|
||||||
return Hex.encodeHex(DigestUtils.digest("SHA-1", file)).equalsIgnoreCase(hash);
|
return DigestUtils.digestToString("SHA-1", file).equalsIgnoreCase(hash);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -19,26 +19,30 @@ public final class CrashReportAnalyzer {
|
||||||
public enum Rule {
|
public enum Rule {
|
||||||
// We manually write "Pattern.compile" here for IDEA syntax highlighting.
|
// We manually write "Pattern.compile" here for IDEA syntax highlighting.
|
||||||
|
|
||||||
OPENJ9(Pattern.compile("(Open J9 is not supported|OpenJ9 is incompatible)")),
|
OPENJ9(Pattern.compile("(Open J9 is not supported|OpenJ9 is incompatible|\\.J9VMInternals\\.)")),
|
||||||
TOO_OLD_JAVA(Pattern.compile("java\\.lang\\.UnsupportedClassVersionError: (.*?) version (?<expected>\\d+)\\.0"), "expected"),
|
TOO_OLD_JAVA(Pattern.compile("java\\.lang\\.UnsupportedClassVersionError: (.*?) version (?<expected>\\d+)\\.0"), "expected"),
|
||||||
JVM_32BIT(Pattern.compile("(Could not reserve enough space for (.*?) object heap|The specified size exceeds the maximum representable size)")),
|
JVM_32BIT(Pattern.compile("(Could not reserve enough space for (.*?)KB object heap|The specified size exceeds the maximum representable size)")),
|
||||||
|
|
||||||
// Some mods/shader packs do incorrect GL operations.
|
// Some mods/shader packs do incorrect GL operations.
|
||||||
GL_OPERATION_FAILURE(Pattern.compile("1282: Invalid operation")),
|
GL_OPERATION_FAILURE(Pattern.compile("(1282: Invalid operation|Maybe try a lower resolution resourcepack\\?)")),
|
||||||
|
|
||||||
// Maybe software rendering? Suggest user for using a graphics card.
|
// Maybe software rendering? Suggest user for using a graphics card.
|
||||||
OPENGL_NOT_SUPPORTED(Pattern.compile("The driver does not appear to support OpenGL")),
|
OPENGL_NOT_SUPPORTED(Pattern.compile("The driver does not appear to support OpenGL")),
|
||||||
GRAPHICS_DRIVER(Pattern.compile("(Pixel format not accelerated|GLX: Failed to create context: GLXBadFBConfig|Couldn't set pixel format|net\\.minecraftforge\\.fml.client\\.SplashProgress|org\\.lwjgl\\.LWJGLException|EXCEPTION_ACCESS_VIOLATION(.|\\n|\\r)+# C {2}\\[(ig|atio|nvoglv))")),
|
GRAPHICS_DRIVER(Pattern.compile("(Pixel format not accelerated|GLX: Failed to create context: GLXBadFBConfig|Couldn't set pixel format|net\\.minecraftforge\\.fml.client\\.SplashProgress|org\\.lwjgl\\.LWJGLException|EXCEPTION_ACCESS_VIOLATION(.|\\n|\\r)+# C {2}\\[(ig|atio|nvoglv))")),
|
||||||
// Out of memory
|
// Out of memory
|
||||||
OUT_OF_MEMORY(Pattern.compile("(java\\.lang\\.OutOfMemoryError|The system is out of physical RAM or swap space)")),
|
OUT_OF_MEMORY(Pattern.compile("(java\\.lang\\.OutOfMemoryError|The system is out of physical RAM or swap space|Out of Memory Error)")),
|
||||||
// Memory exceeded
|
// Memory exceeded
|
||||||
MEMORY_EXCEEDED(Pattern.compile("There is insufficient memory for the Java Runtime Environment to continue")),
|
MEMORY_EXCEEDED(Pattern.compile("There is insufficient memory for the Java Runtime Environment to continue")),
|
||||||
// Too high resolution
|
// Too high resolution
|
||||||
RESOLUTION_TOO_HIGH(Pattern.compile("Maybe try a (lower resolution|lowerresolution) (resourcepack|texturepack)\\?")),
|
RESOLUTION_TOO_HIGH(Pattern.compile("Maybe try a (lower resolution|lowerresolution) (resourcepack|texturepack)\\?")),
|
||||||
// game can only run on Java 8. Version of uesr's JVM is too high.
|
// game can only run on Java 8. Version of uesr's JVM is too high.
|
||||||
JDK_9(Pattern.compile("java\\.lang\\.ClassCastException: (java\\.base/jdk|class jdk)")),
|
JDK_9(Pattern.compile("java\\.lang\\.ClassCastException: (java\\.base/jdk|class jdk)")),
|
||||||
|
// Forge and OptiFine with crash because the JVM compiled with a new version of Xcode
|
||||||
|
// https://github.com/sp614x/optifine/issues/4824
|
||||||
|
// https://github.com/MinecraftForge/MinecraftForge/issues/7546
|
||||||
|
MAC_JDK_8U261(Pattern.compile("Terminating app due to uncaught exception 'NSInternalInconsistencyException', reason: 'NSWindow drag regions should only be invalidated on the Main Thread!'")),
|
||||||
// user modifies minecraft primary jar without changing hash file
|
// user modifies minecraft primary jar without changing hash file
|
||||||
FILE_CHANGED(Pattern.compile("java\\.lang\\.SecurityException: SHA1 digest error for (?<file>.*)"), "file"),
|
FILE_CHANGED(Pattern.compile("java\\.lang\\.SecurityException: SHA1 digest error for (?<file>.*)|signer information does not match signer information of other classes in the same package"), "file"),
|
||||||
// mod loader/coremod injection fault, prompt user to reinstall game.
|
// mod loader/coremod injection fault, prompt user to reinstall game.
|
||||||
NO_SUCH_METHOD_ERROR(Pattern.compile("java\\.lang\\.NoSuchMethodError: (?<class>.*?)"), "class"),
|
NO_SUCH_METHOD_ERROR(Pattern.compile("java\\.lang\\.NoSuchMethodError: (?<class>.*?)"), "class"),
|
||||||
// mod loader/coremod injection fault, prompt user to reinstall game.
|
// mod loader/coremod injection fault, prompt user to reinstall game.
|
||||||
|
@ -57,7 +61,7 @@ public final class CrashReportAnalyzer {
|
||||||
FILE_ALREADY_EXISTS(Pattern.compile("java\\.nio\\.file\\.FileAlreadyExistsException: (?<file>.*)"), "file"),
|
FILE_ALREADY_EXISTS(Pattern.compile("java\\.nio\\.file\\.FileAlreadyExistsException: (?<file>.*)"), "file"),
|
||||||
// Forge found some mod crashed in game loading
|
// Forge found some mod crashed in game loading
|
||||||
LOADING_CRASHED_FORGE(Pattern.compile("LoaderExceptionModCrash: Caught exception from (?<name>.*?) \\((?<id>.*)\\)"), "name", "id"),
|
LOADING_CRASHED_FORGE(Pattern.compile("LoaderExceptionModCrash: Caught exception from (?<name>.*?) \\((?<id>.*)\\)"), "name", "id"),
|
||||||
BOOTSTRAP_FAILED(Pattern.compile("Failed to create mod instance. ModID: (?<id>.*?),"), "id"),
|
BOOTSTRAP_FAILED(Pattern.compile("Failed to create mod instance\\. ModID: (?<id>.*?),"), "id"),
|
||||||
// Fabric found some mod crashed in game loading
|
// Fabric found some mod crashed in game loading
|
||||||
LOADING_CRASHED_FABRIC(Pattern.compile("Could not execute entrypoint stage '(.*?)' due to errors, provided by '(?<id>.*)'!"), "id"),
|
LOADING_CRASHED_FABRIC(Pattern.compile("Could not execute entrypoint stage '(.*?)' due to errors, provided by '(?<id>.*)'!"), "id"),
|
||||||
// Fabric may have breaking changes.
|
// Fabric may have breaking changes.
|
||||||
|
@ -68,9 +72,9 @@ public final class CrashReportAnalyzer {
|
||||||
MODLAUNCHER_8(Pattern.compile("java\\.lang\\.NoSuchMethodError: ('void sun\\.security\\.util\\.ManifestEntryVerifier\\.<init>\\(java\\.util\\.jar\\.Manifest\\)'|sun\\.security\\.util\\.ManifestEntryVerifier\\.<init>\\(Ljava/util/jar/Manifest;\\)V)")),
|
MODLAUNCHER_8(Pattern.compile("java\\.lang\\.NoSuchMethodError: ('void sun\\.security\\.util\\.ManifestEntryVerifier\\.<init>\\(java\\.util\\.jar\\.Manifest\\)'|sun\\.security\\.util\\.ManifestEntryVerifier\\.<init>\\(Ljava/util/jar/Manifest;\\)V)")),
|
||||||
// Manually triggerd debug crash
|
// Manually triggerd debug crash
|
||||||
DEBUG_CRASH(Pattern.compile("Manually triggered debug crash")),
|
DEBUG_CRASH(Pattern.compile("Manually triggered debug crash")),
|
||||||
CONFIG(Pattern.compile("Failed loading config file (?<file>.*?) of type SERVER for modid (?<id>.*)"), "id", "file"),
|
CONFIG(Pattern.compile("Failed loading config file (?<file>.*?) of type (.*?) for modid (?<id>.*)"), "id", "file"),
|
||||||
// Fabric gives some warnings
|
// Fabric gives some warnings
|
||||||
FABRIC_WARNINGS(Pattern.compile("Warnings were found!(.*?)[\\n\\r]+(?<reason>[^\\[]+)\\["), "reason"),
|
FABRIC_WARNINGS(Pattern.compile("(Warnings were found!|Incompatible mod set!)(.*?)[\\n\\r]+(?<reason>[^\\[]+)\\["), "reason"),
|
||||||
// Game crashed when ticking entity
|
// Game crashed when ticking entity
|
||||||
ENTITY(Pattern.compile("Entity Type: (?<type>.*)[\\w\\W\\n\\r]*?Entity's Exact location: (?<location>.*)"), "type", "location"),
|
ENTITY(Pattern.compile("Entity Type: (?<type>.*)[\\w\\W\\n\\r]*?Entity's Exact location: (?<location>.*)"), "type", "location"),
|
||||||
// Game crashed when tesselating block model
|
// Game crashed when tesselating block model
|
||||||
|
@ -78,10 +82,30 @@ public final class CrashReportAnalyzer {
|
||||||
// Cannot find native libraries
|
// Cannot find native libraries
|
||||||
UNSATISFIED_LINK_ERROR(Pattern.compile("java.lang.UnsatisfiedLinkError: Failed to locate library: (?<name>.*)"), "name"),
|
UNSATISFIED_LINK_ERROR(Pattern.compile("java.lang.UnsatisfiedLinkError: Failed to locate library: (?<name>.*)"), "name"),
|
||||||
|
|
||||||
|
OPTIFINE_IS_NOT_COMPATIBLE_WITH_FORGE(Pattern.compile("(java\\.lang\\.NoSuchMethodError: 'void net\\.minecraft\\.server\\.level\\.DistanceManager\\.addRegionTicket\\(net\\.minecraft\\.server\\.level\\.TicketType, net\\.minecraft\\.world\\.level\\.ChunkPos, int, java\\.lang\\.Object, boolean\\)'|java\\.lang\\.NoSuchMethodError: 'void net\\.minecraft\\.client\\.renderer\\.block\\.model\\.BakedQuad\\.<init>\\(int\\[\\], int, net\\.minecraft\\.core\\.Direction, net\\.minecraft\\.client\\.renderer\\.texture\\.TextureAtlasSprite, boolean, boolean\\)'|TRANSFORMER/net\\.optifine/net\\.optifine\\.reflect\\.Reflector\\.<clinit>\\(Reflector\\.java)")),
|
||||||
|
MOD_FILES_ARE_DECOMPRESSED(Pattern.compile("(The directories below appear to be extracted jar files\\. Fix this before you continue|Extracted mod jars found, loading will NOT continue)")),//Mod文件被解压
|
||||||
|
OPTIFINE_CAUSES_THE_WORLD_TO_FAIL_TO_LOAD(Pattern.compile("java\\.lang\\.NoSuchMethodError: net\\.minecraft\\.world\\.server\\.ChunkManager$ProxyTicketManager\\.shouldForceTicks\\(J\\)Z")),//OptiFine导致无法加载世界 https://www.minecraftforum.net/forums/support/java-edition-support/3051132-exception-ticking-world
|
||||||
|
TOO_MANY_MODS_LEAD_TO_EXCEEDING_THE_ID_LIMIT(Pattern.compile("maximum id range exceeded")),//Mod过多导致超出ID限制
|
||||||
|
|
||||||
// Mod issues
|
// Mod issues
|
||||||
// TwilightForest is not compatible with OptiFine on Minecraft 1.16.
|
MODMIXIN_FAILURE(Pattern.compile("(Mixin prepare failed |Mixin apply failed |mixin\\.injection\\.throwables\\.|\\.mixins\\.json\\] FAILED during \\))")),//ModMixin失败
|
||||||
|
MOD_REPEAT_INSTALLATION(Pattern.compile("(DuplicateModsFoundException|ModResolutionException: Duplicate)")),//Mod重复安装
|
||||||
|
FORGE_ERROR(Pattern.compile("An exception was thrown, the game will display an error screen and halt.")),//Forge报错,Forge可能已经提供了错误信息
|
||||||
|
MOD_RESOLUTION0(Pattern.compile("(Multiple entries with same key: |Failure message: MISSING)")),//可能是Mod问题
|
||||||
|
FORGE_REPEAT_INSTALLATION(Pattern.compile("--launchTarget, fmlclient, --fml.forgeVersion,[\\w\\W]*?--launchTarget, fmlclient, --fml.forgeVersion,[\\w\\W\\n\\r]*?MultipleArgumentsForOptionException: Found multiple arguments for option gameDir, but you asked for only one")),
|
||||||
|
OPTIFINE_REPEAT_INSTALLATION(Pattern.compile("ResolutionException: Module optifine reads another module named optifine")),//Optifine 重复安装(及Mod文件夹有,自动安装也有)
|
||||||
|
JAVA_VERSION_IS_TOO_HIGH(Pattern.compile("(Unable to make protected final java\\.lang\\.Class java\\.lang\\.ClassLoader\\.defineClass|java\\.lang\\.NoSuchFieldException: ucp|Unsupported class file major version|because module java\\.base does not export|java\\.lang\\.ClassNotFoundException: jdk\\.nashorn\\.api\\.scripting\\.NashornScriptEngineFactory|java\\.lang\\.ClassNotFoundException: java\\.lang\\.invoke\\.LambdaMetafactory)")),//Java版本过高
|
||||||
|
|
||||||
|
//Forge 默认会把每一个 mod jar 都当做一个 JPMS 的模块(Module)加载。在这个 jar 没有给出 module-info 声明的情况下,JPMS 会采用这样的顺序决定 module 名字:
|
||||||
|
//1. META-INF/MANIFEST.MF 里的 Automatic-Module-Name
|
||||||
|
//2. 根据文件名生成。文件名里的 .jar 后缀名先去掉,然后检查是否有 -(\\d+(\\.|$)) 的部分,有的话只取 - 前面的部分,- 后面的部分成为 module 的版本号(即尝试判断文件名里是否有版本号,有的话去掉),然后把不是拉丁字母和数字的字符(正则表达式 [^A-Za-z0-9])都换成点,然后把连续的多个点换成一个点,最后去掉开头和结尾的点。那么
|
||||||
|
//按照 2.,如果你的文件名是拔刀剑.jar,那么这么一通流程下来,你得到的 module 名就是空字符串,而这是不允许的。(来自 @Föhn 说明)
|
||||||
|
MOD_NAME(Pattern.compile("Invalid module name: '' is not a Java identifier")),
|
||||||
|
|
||||||
|
//Forge 安装不完整
|
||||||
|
INCOMPLETE_FORGE_INSTALLATION(Pattern.compile("(java\\.io\\.UncheckedIOException: java\\.io\\.IOException: Invalid paths argument, contained no existing paths: \\[(.*?)\\\\libraries\\\\net\\\\minecraftforge\\\\forge\\\\(.*?)\\\\forge-(.*?)-client\\.jar\\]|Failed to find Minecraft resource version (.*?) at (.*?)\\\\libraries\\\\net\\\\minecraftforge\\\\forge\\\\(.*?)\\\\forge-(.*?)-client\\.jar|Cannot find launch target fmlclient, unable to launch)")),
|
||||||
|
|
||||||
|
// TwilightForest is not compatible with OptiFine on Minecraft 1.16
|
||||||
TWILIGHT_FOREST_OPTIFINE(Pattern.compile("java.lang.IllegalArgumentException: (.*) outside of image bounds (.*)"));
|
TWILIGHT_FOREST_OPTIFINE(Pattern.compile("java.lang.IllegalArgumentException: (.*) outside of image bounds (.*)"));
|
||||||
|
|
||||||
private final Pattern pattern;
|
private final Pattern pattern;
|
||||||
|
@ -159,16 +183,16 @@ public final class CrashReportAnalyzer {
|
||||||
private static final Pattern STACK_TRACE_LINE_PATTERN = Pattern.compile("at (?<method>.*?)\\((?<sourcefile>.*?)\\)");
|
private static final Pattern STACK_TRACE_LINE_PATTERN = Pattern.compile("at (?<method>.*?)\\((?<sourcefile>.*?)\\)");
|
||||||
private static final Pattern STACK_TRACE_LINE_MODULE_PATTERN = Pattern.compile("\\{(?<tokens>.*)}");
|
private static final Pattern STACK_TRACE_LINE_MODULE_PATTERN = Pattern.compile("\\{(?<tokens>.*)}");
|
||||||
private static final Set<String> PACKAGE_KEYWORD_BLACK_LIST = new HashSet<>(Arrays.asList(
|
private static final Set<String> PACKAGE_KEYWORD_BLACK_LIST = new HashSet<>(Arrays.asList(
|
||||||
"net", "minecraft", "item", "block", "player", "tileentity", "events", "common", "client", "entity", "mojang", "main", "gui", "world", "server", "dedicated", // minecraft
|
"net", "minecraft", "item", "setup", "block", "assist", "optifine", "player", "unimi", "fastutil", "tileentity", "events", "common", "blockentity", "client", "entity", "mojang", "main", "gui", "world", "server", "dedicated", // minecraft
|
||||||
"renderer", "chunk", "model", "loading", "color", "pipeline", "inventory", "launcher", "physics", "particle", "gen", "registry", "worldgen", "texture", "biomes", "biome",
|
"renderer", "chunk", "model", "loading", "color", "pipeline", "inventory", "launcher", "physics", "particle", "gen", "registry", "worldgen", "texture", "biomes", "biome",
|
||||||
"monster", "passive", "ai", "integrated", "tile", "state", "play", "structure", "nbt", "pathfinding", "chunk", "audio", "entities", "items", "renderers",
|
"monster", "passive", "ai", "integrated", "tile", "state", "play", "override", "transformers", "structure", "nbt", "pathfinding", "chunk", "audio", "entities", "items", "renderers",
|
||||||
"storage",
|
"storage",
|
||||||
"java", "lang", "util", "nio", "io", "sun", "reflect", "zip", "jdk", "nashorn", "scripts", "runtime", "internal", // java
|
"java", "lang", "util", "nio", "io", "sun", "reflect", "zip", "jar", "jdk", "nashorn", "scripts", "runtime", "internal", // java
|
||||||
"mods", "mod", "impl", "org", "com", "cn", "cc", "jp", // title
|
"mods", "mod", "impl", "org", "com", "cn", "cc", "jp", // title
|
||||||
"core", "config", "registries", "lib", "ruby", "mc", "codec", "channel", "embedded", "netty", "network", "handler", "feature", // misc
|
"core", "config", "registries", "lib", "ruby", "mc", "codec", "recipe", "channel", "embedded", "done", "net", "netty", "network", "load", "github", "handler", "content", "feature", // misc
|
||||||
"file", "machine", "shader", "general", "helper", "init", "library", "api", "integration", "engine", "preload", "preinit",
|
"file", "machine", "shader", "general", "helper", "init", "library", "api", "integration", "engine", "preload", "preinit",
|
||||||
"fcl", "tungsten", // fcl
|
"fcl", "tungsten", // fcl
|
||||||
"fml", "minecraftforge", "forge", "cpw", "modlauncher", "launchwrapper", "objectweb", "asm", "event", "eventhandler", "handshake", "kcauldron", // forge
|
"fml", "minecraftforge", "forge", "cpw", "modlauncher", "launchwrapper", "objectweb", "asm", "event", "eventhandler", "handshake", "modapi", "kcauldron", // forge
|
||||||
"fabricmc", "loader", "game", "knot", "launch", "mixin" // fabric
|
"fabricmc", "loader", "game", "knot", "launch", "mixin" // fabric
|
||||||
));
|
));
|
||||||
|
|
||||||
|
|
|
@ -48,7 +48,7 @@ public class DefaultGameRepository implements GameRepository {
|
||||||
|
|
||||||
private File baseDirectory;
|
private File baseDirectory;
|
||||||
protected Map<String, Version> versions;
|
protected Map<String, Version> versions;
|
||||||
private ConcurrentHashMap<File, Optional<String>> gameVersions = new ConcurrentHashMap<>();
|
private final ConcurrentHashMap<File, Optional<String>> gameVersions = new ConcurrentHashMap<>();
|
||||||
|
|
||||||
public DefaultGameRepository(File baseDirectory) {
|
public DefaultGameRepository(File baseDirectory) {
|
||||||
this.baseDirectory = baseDirectory;
|
this.baseDirectory = baseDirectory;
|
||||||
|
@ -126,19 +126,13 @@ public class DefaultGameRepository implements GameRepository {
|
||||||
// This implementation may cause multiple flows against the same version entering
|
// This implementation may cause multiple flows against the same version entering
|
||||||
// this function, which is accepted because GameVersion::minecraftVersion should
|
// this function, which is accepted because GameVersion::minecraftVersion should
|
||||||
// be consistent.
|
// be consistent.
|
||||||
File versionJar = getVersionJar(version);
|
return gameVersions.computeIfAbsent(getVersionJar(version), versionJar -> {
|
||||||
if (gameVersions.containsKey(versionJar)) {
|
|
||||||
return gameVersions.get(versionJar);
|
|
||||||
} else {
|
|
||||||
Optional<String> gameVersion = GameVersion.minecraftVersion(versionJar);
|
Optional<String> gameVersion = GameVersion.minecraftVersion(versionJar);
|
||||||
|
|
||||||
if (!gameVersion.isPresent()) {
|
if (!gameVersion.isPresent()) {
|
||||||
LOG.warning("Cannot find out game version of " + version.getId() + ", primary jar: " + versionJar.toString() + ", jar exists: " + versionJar.exists());
|
LOG.warning("Cannot find out game version of " + version.getId() + ", primary jar: " + versionJar.toString() + ", jar exists: " + versionJar.exists());
|
||||||
}
|
}
|
||||||
|
|
||||||
gameVersions.put(versionJar, gameVersion);
|
|
||||||
return gameVersion;
|
return gameVersion;
|
||||||
}
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@ -168,7 +162,7 @@ public class DefaultGameRepository implements GameRepository {
|
||||||
} catch (JsonParseException ignored) {
|
} catch (JsonParseException ignored) {
|
||||||
}
|
}
|
||||||
|
|
||||||
LOG.warning("Cannot parse version json + " + file.toString() + "\n" + jsonText);
|
LOG.warning("Cannot parse version json: " + file.toString() + "\n" + jsonText);
|
||||||
throw new JsonParseException("Version json incorrect");
|
throw new JsonParseException("Version json incorrect");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -3,7 +3,6 @@ package com.tungsten.fclcore.game;
|
||||||
import com.google.gson.JsonParseException;
|
import com.google.gson.JsonParseException;
|
||||||
import com.google.gson.annotations.SerializedName;
|
import com.google.gson.annotations.SerializedName;
|
||||||
import com.tungsten.fclcore.util.DigestUtils;
|
import com.tungsten.fclcore.util.DigestUtils;
|
||||||
import com.tungsten.fclcore.util.Hex;
|
|
||||||
import com.tungsten.fclcore.util.StringUtils;
|
import com.tungsten.fclcore.util.StringUtils;
|
||||||
import com.tungsten.fclcore.util.ToStringBuilder;
|
import com.tungsten.fclcore.util.ToStringBuilder;
|
||||||
import com.tungsten.fclcore.util.gson.TolerableValidationException;
|
import com.tungsten.fclcore.util.gson.TolerableValidationException;
|
||||||
|
@ -64,6 +63,6 @@ public class DownloadInfo implements Validation {
|
||||||
|
|
||||||
public boolean validateChecksum(Path file, boolean defaultValue) throws IOException {
|
public boolean validateChecksum(Path file, boolean defaultValue) throws IOException {
|
||||||
if (getSha1() == null) return defaultValue;
|
if (getSha1() == null) return defaultValue;
|
||||||
return Hex.encodeHex(DigestUtils.digest("SHA-1", file)).equalsIgnoreCase(getSha1());
|
return DigestUtils.digestToString("SHA-1", file).equalsIgnoreCase(getSha1());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -175,7 +175,7 @@ public interface GameRepository extends VersionProvider {
|
||||||
* @param version the id of specific version that is relevant to {@code assetId}
|
* @param version the id of specific version that is relevant to {@code assetId}
|
||||||
* @param assetId the asset id, you can find it in {@link AssetIndexInfo#getId()} {@link Version#getAssetIndex()}
|
* @param assetId the asset id, you can find it in {@link AssetIndexInfo#getId()} {@link Version#getAssetIndex()}
|
||||||
* @param name the asset object name, you can find it in keys of {@link AssetIndex#getObjects()}
|
* @param name the asset object name, you can find it in keys of {@link AssetIndex#getObjects()}
|
||||||
* @throws IOException if I/O operation fails.
|
* @throws java.io.IOException if I/O operation fails.
|
||||||
* @return the file that given asset object refers to
|
* @return the file that given asset object refers to
|
||||||
*/
|
*/
|
||||||
Optional<Path> getAssetObject(String version, String assetId, String name) throws IOException;
|
Optional<Path> getAssetObject(String version, String assetId, String name) throws IOException;
|
||||||
|
|
|
@ -5,8 +5,6 @@ import static com.tungsten.fclcore.util.Logging.LOG;
|
||||||
|
|
||||||
import com.google.gson.JsonParseException;
|
import com.google.gson.JsonParseException;
|
||||||
import com.tungsten.fclcore.util.gson.JsonUtils;
|
import com.tungsten.fclcore.util.gson.JsonUtils;
|
||||||
import com.tungsten.fclcore.util.io.CompressingUtils;
|
|
||||||
import com.tungsten.fclcore.util.io.FileUtils;
|
|
||||||
|
|
||||||
import org.jenkinsci.constant_pool_scanner.ConstantPool;
|
import org.jenkinsci.constant_pool_scanner.ConstantPool;
|
||||||
import org.jenkinsci.constant_pool_scanner.ConstantPoolScanner;
|
import org.jenkinsci.constant_pool_scanner.ConstantPoolScanner;
|
||||||
|
@ -15,31 +13,31 @@ import org.jenkinsci.constant_pool_scanner.StringConstant;
|
||||||
|
|
||||||
import java.io.File;
|
import java.io.File;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.nio.file.FileSystem;
|
import java.io.InputStream;
|
||||||
import java.nio.file.Files;
|
|
||||||
import java.nio.file.Path;
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
import java.util.logging.Level;
|
import java.util.logging.Level;
|
||||||
import java.util.stream.Collectors;
|
import java.util.stream.Collectors;
|
||||||
import java.util.stream.StreamSupport;
|
import java.util.stream.StreamSupport;
|
||||||
|
import java.util.zip.ZipEntry;
|
||||||
|
import java.util.zip.ZipFile;
|
||||||
|
|
||||||
public final class GameVersion {
|
public final class GameVersion {
|
||||||
private GameVersion() {
|
private GameVersion() {
|
||||||
}
|
}
|
||||||
|
|
||||||
private static Optional<String> getVersionFromJson(Path versionJson) {
|
private static Optional<String> getVersionFromJson(InputStream versionJson) {
|
||||||
try {
|
try {
|
||||||
Map<?, ?> version = JsonUtils.fromNonNullJson(FileUtils.readText(versionJson), Map.class);
|
Map<?, ?> version = JsonUtils.fromNonNullJsonFully(versionJson, Map.class);
|
||||||
return tryCast(version.get("name"), String.class);
|
return tryCast(version.get("id"), String.class);
|
||||||
} catch (IOException | JsonParseException e) {
|
} catch (IOException | JsonParseException e) {
|
||||||
LOG.log(Level.WARNING, "Failed to parse version.json", e);
|
LOG.log(Level.WARNING, "Failed to parse version.json", e);
|
||||||
return Optional.empty();
|
return Optional.empty();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static Optional<String> getVersionOfClassMinecraft(byte[] bytecode) throws IOException {
|
private static Optional<String> getVersionOfClassMinecraft(InputStream bytecode) throws IOException {
|
||||||
ConstantPool pool = ConstantPoolScanner.parse(bytecode, ConstantType.STRING);
|
ConstantPool pool = ConstantPoolScanner.parse(bytecode, ConstantType.STRING);
|
||||||
|
|
||||||
return StreamSupport.stream(pool.list(StringConstant.class).spliterator(), false)
|
return StreamSupport.stream(pool.list(StringConstant.class).spliterator(), false)
|
||||||
|
@ -49,7 +47,7 @@ public final class GameVersion {
|
||||||
.findFirst();
|
.findFirst();
|
||||||
}
|
}
|
||||||
|
|
||||||
private static Optional<String> getVersionFromClassMinecraftServer(byte[] bytecode) throws IOException {
|
private static Optional<String> getVersionFromClassMinecraftServer(InputStream bytecode) throws IOException {
|
||||||
ConstantPool pool = ConstantPoolScanner.parse(bytecode, ConstantType.STRING);
|
ConstantPool pool = ConstantPoolScanner.parse(bytecode, ConstantType.STRING);
|
||||||
|
|
||||||
List<String> list = StreamSupport.stream(pool.list(StringConstant.class).spliterator(), false)
|
List<String> list = StreamSupport.stream(pool.list(StringConstant.class).spliterator(), false)
|
||||||
|
@ -75,23 +73,29 @@ public final class GameVersion {
|
||||||
if (file == null || !file.exists() || !file.isFile() || !file.canRead())
|
if (file == null || !file.exists() || !file.isFile() || !file.canRead())
|
||||||
return Optional.empty();
|
return Optional.empty();
|
||||||
|
|
||||||
try (FileSystem gameJar = CompressingUtils.createReadOnlyZipFileSystem(file.toPath())) {
|
try (ZipFile gameJar = new ZipFile(file)) {
|
||||||
Path versionJson = gameJar.getPath("version.json");
|
ZipEntry versionJson = gameJar.getEntry("version.json");
|
||||||
if (Files.exists(versionJson)) {
|
if (versionJson != null) {
|
||||||
Optional<String> result = getVersionFromJson(versionJson);
|
Optional<String> result = getVersionFromJson(gameJar.getInputStream(versionJson));
|
||||||
if (result.isPresent())
|
if (result.isPresent())
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
Path minecraft = gameJar.getPath("net/minecraft/client/Minecraft.class");
|
ZipEntry minecraft = gameJar.getEntry("net/minecraft/client/Minecraft.class");
|
||||||
if (Files.exists(minecraft)) {
|
if (minecraft != null) {
|
||||||
Optional<String> result = getVersionOfClassMinecraft(Files.readAllBytes(minecraft));
|
try (InputStream is = gameJar.getInputStream(minecraft)) {
|
||||||
if (result.isPresent())
|
Optional<String> result = getVersionOfClassMinecraft(is);
|
||||||
return result;
|
if (result.isPresent())
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ZipEntry minecraftServer = gameJar.getEntry("net/minecraft/server/MinecraftServer.class");
|
||||||
|
if (minecraftServer != null) {
|
||||||
|
try (InputStream is = gameJar.getInputStream(minecraftServer)) {
|
||||||
|
return getVersionFromClassMinecraftServer(is);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
Path minecraftServer = gameJar.getPath("net/minecraft/server/MinecraftServer.class");
|
|
||||||
if (Files.exists(minecraftServer))
|
|
||||||
return getVersionFromClassMinecraftServer(Files.readAllBytes(minecraftServer));
|
|
||||||
return Optional.empty();
|
return Optional.empty();
|
||||||
} catch (IOException e) {
|
} catch (IOException e) {
|
||||||
return Optional.empty();
|
return Optional.empty();
|
||||||
|
|
|
@ -18,9 +18,10 @@ public class LaunchOptions implements Serializable {
|
||||||
private String versionName;
|
private String versionName;
|
||||||
private String versionType;
|
private String versionType;
|
||||||
private String profileName;
|
private String profileName;
|
||||||
private List<String> gameArguments = new ArrayList<>();
|
private final List<String> gameArguments = new ArrayList<>();
|
||||||
private List<String> javaArguments = new ArrayList<>();
|
private final List<String> overrideJavaArguments = new ArrayList<>();
|
||||||
private List<String> javaAgents = new ArrayList<>(0);
|
private final List<String> javaArguments = new ArrayList<>();
|
||||||
|
private final List<String> javaAgents = new ArrayList<>(0);
|
||||||
private Integer minMemory;
|
private Integer minMemory;
|
||||||
private Integer maxMemory;
|
private Integer maxMemory;
|
||||||
private Integer metaspace;
|
private Integer metaspace;
|
||||||
|
@ -76,6 +77,14 @@ public class LaunchOptions implements Serializable {
|
||||||
return Collections.unmodifiableList(gameArguments);
|
return Collections.unmodifiableList(gameArguments);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The highest priority JVM arguments (overrides the version setting)
|
||||||
|
*/
|
||||||
|
@NotNull
|
||||||
|
public List<String> getOverrideJavaArguments() {
|
||||||
|
return Collections.unmodifiableList(overrideJavaArguments);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* User custom additional java virtual machine command line arguments.
|
* User custom additional java virtual machine command line arguments.
|
||||||
*/
|
*/
|
||||||
|
@ -206,6 +215,13 @@ public class LaunchOptions implements Serializable {
|
||||||
return options.gameArguments;
|
return options.gameArguments;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The highest priority JVM arguments (overrides the version setting)
|
||||||
|
*/
|
||||||
|
public List<String> getOverrideJavaArguments() {
|
||||||
|
return options.overrideJavaArguments;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* User custom additional java virtual machine command line arguments.
|
* User custom additional java virtual machine command line arguments.
|
||||||
*/
|
*/
|
||||||
|
@ -313,6 +329,12 @@ public class LaunchOptions implements Serializable {
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public Builder setOverrideJavaArguments(List<String> overrideJavaArguments) {
|
||||||
|
options.overrideJavaArguments.clear();
|
||||||
|
options.overrideJavaArguments.addAll(overrideJavaArguments);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
public Builder setJavaArguments(List<String> javaArguments) {
|
public Builder setJavaArguments(List<String> javaArguments) {
|
||||||
options.javaArguments.clear();
|
options.javaArguments.clear();
|
||||||
options.javaArguments.addAll(javaArguments);
|
options.javaArguments.addAll(javaArguments);
|
||||||
|
|
|
@ -43,7 +43,7 @@ public final class StringArgument implements Argument {
|
||||||
return argument;
|
return argument;
|
||||||
}
|
}
|
||||||
|
|
||||||
public class Serializer implements JsonSerializer<StringArgument> {
|
public static final class Serializer implements JsonSerializer<StringArgument> {
|
||||||
@Override
|
@Override
|
||||||
public JsonElement serialize(StringArgument src, Type typeOfSrc, JsonSerializationContext context) {
|
public JsonElement serialize(StringArgument src, Type typeOfSrc, JsonSerializationContext context) {
|
||||||
return new JsonPrimitive(src.getArgument());
|
return new JsonPrimitive(src.getArgument());
|
||||||
|
|
|
@ -181,7 +181,7 @@ public class World {
|
||||||
throw new IOException();
|
throw new IOException();
|
||||||
|
|
||||||
try (Zipper zipper = new Zipper(zip)) {
|
try (Zipper zipper = new Zipper(zip)) {
|
||||||
zipper.putDirectory(file, "/" + worldName + "/");
|
zipper.putDirectory(file, worldName);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -250,7 +250,7 @@ public class DefaultLauncher extends Launcher {
|
||||||
|
|
||||||
public String getInjectorArg() {
|
public String getInjectorArg() {
|
||||||
try {
|
try {
|
||||||
String map = IOUtils.readFullyAsString(DefaultLauncher.class.getResourceAsStream("/assets/map.json"), StandardCharsets.UTF_8);
|
String map = IOUtils.readFullyAsString(DefaultLauncher.class.getResourceAsStream("/assets/map.json"));
|
||||||
InjectorMap injectorMap = new GsonBuilder()
|
InjectorMap injectorMap = new GsonBuilder()
|
||||||
.setPrettyPrinting()
|
.setPrettyPrinting()
|
||||||
.create()
|
.create()
|
||||||
|
|
|
@ -3,7 +3,6 @@ package com.tungsten.fclcore.mod;
|
||||||
import com.google.gson.*;
|
import com.google.gson.*;
|
||||||
import com.google.gson.annotations.JsonAdapter;
|
import com.google.gson.annotations.JsonAdapter;
|
||||||
import com.tungsten.fclcore.util.gson.JsonUtils;
|
import com.tungsten.fclcore.util.gson.JsonUtils;
|
||||||
import com.tungsten.fclcore.util.io.CompressingUtils;
|
|
||||||
import com.tungsten.fclcore.util.io.FileUtils;
|
import com.tungsten.fclcore.util.io.FileUtils;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
|
@ -39,16 +38,14 @@ public final class FabricModMetadata {
|
||||||
this.contact = contact;
|
this.contact = contact;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static LocalModFile fromFile(ModManager modManager, Path modFile) throws IOException, JsonParseException {
|
public static LocalModFile fromFile(ModManager modManager, Path modFile, FileSystem fs) throws IOException, JsonParseException {
|
||||||
try (FileSystem fs = CompressingUtils.createReadOnlyZipFileSystem(modFile)) {
|
Path mcmod = fs.getPath("fabric.mod.json");
|
||||||
Path mcmod = fs.getPath("fabric.mod.json");
|
if (Files.notExists(mcmod))
|
||||||
if (Files.notExists(mcmod))
|
throw new IOException("File " + modFile + " is not a Fabric mod.");
|
||||||
throw new IOException("File " + modFile + " is not a Fabric mod.");
|
FabricModMetadata metadata = JsonUtils.fromNonNullJson(FileUtils.readText(mcmod), FabricModMetadata.class);
|
||||||
FabricModMetadata metadata = JsonUtils.fromNonNullJson(FileUtils.readText(mcmod), FabricModMetadata.class);
|
String authors = metadata.authors == null ? "" : metadata.authors.stream().map(author -> author.name).collect(Collectors.joining(", "));
|
||||||
String authors = metadata.authors == null ? "" : metadata.authors.stream().map(author -> author.name).collect(Collectors.joining(", "));
|
return new LocalModFile(modManager, modManager.getLocalMod(metadata.id, ModLoaderType.FABRIC), modFile, metadata.name, new LocalModFile.Description(metadata.description),
|
||||||
return new LocalModFile(modManager, modManager.getLocalMod(metadata.id, ModLoaderType.FABRIC), modFile, metadata.name, new LocalModFile.Description(metadata.description),
|
authors, metadata.version, "", metadata.contact != null ? metadata.contact.getOrDefault("homepage", "") : "", metadata.icon);
|
||||||
authors, metadata.version, "", metadata.contact != null ? metadata.contact.getOrDefault("homepage", "") : "", metadata.icon);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@JsonAdapter(FabricModAuthorSerializer.class)
|
@JsonAdapter(FabricModAuthorSerializer.class)
|
||||||
|
|
|
@ -4,10 +4,10 @@ import static com.tungsten.fclcore.util.Logging.LOG;
|
||||||
|
|
||||||
import com.google.gson.JsonParseException;
|
import com.google.gson.JsonParseException;
|
||||||
import com.moandjiezana.toml.Toml;
|
import com.moandjiezana.toml.Toml;
|
||||||
import com.tungsten.fclcore.util.io.CompressingUtils;
|
|
||||||
import com.tungsten.fclcore.util.io.FileUtils;
|
import com.tungsten.fclcore.util.io.FileUtils;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
|
import java.io.InputStream;
|
||||||
import java.nio.file.FileSystem;
|
import java.nio.file.FileSystem;
|
||||||
import java.nio.file.Files;
|
import java.nio.file.Files;
|
||||||
import java.nio.file.Path;
|
import java.nio.file.Path;
|
||||||
|
@ -113,29 +113,27 @@ public final class ForgeNewModMetadata {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public static LocalModFile fromFile(ModManager modManager, Path modFile) throws IOException, JsonParseException {
|
public static LocalModFile fromFile(ModManager modManager, Path modFile, FileSystem fs) throws IOException, JsonParseException {
|
||||||
try (FileSystem fs = CompressingUtils.createReadOnlyZipFileSystem(modFile)) {
|
Path modstoml = fs.getPath("META-INF/mods.toml");
|
||||||
Path modstoml = fs.getPath("META-INF/mods.toml");
|
if (Files.notExists(modstoml))
|
||||||
if (Files.notExists(modstoml))
|
throw new IOException("File " + modFile + " is not a Forge 1.13+ mod.");
|
||||||
throw new IOException("File " + modFile + " is not a Forge 1.13+ mod.");
|
ForgeNewModMetadata metadata = new Toml().read(FileUtils.readText(modstoml)).to(ForgeNewModMetadata.class);
|
||||||
ForgeNewModMetadata metadata = new Toml().read(FileUtils.readText(modstoml)).to(ForgeNewModMetadata.class);
|
if (metadata == null || metadata.getMods().isEmpty())
|
||||||
if (metadata == null || metadata.getMods().isEmpty())
|
throw new IOException("Mod " + modFile + " `mods.toml` is malformed..");
|
||||||
throw new IOException("Mod " + modFile + " `mods.toml` is malformed..");
|
Mod mod = metadata.getMods().get(0);
|
||||||
Mod mod = metadata.getMods().get(0);
|
Path manifestMF = fs.getPath("META-INF/MANIFEST.MF");
|
||||||
Path manifestMF = fs.getPath("META-INF/MANIFEST.MF");
|
String jarVersion = "";
|
||||||
String jarVersion = "";
|
if (Files.exists(manifestMF)) {
|
||||||
if (Files.exists(manifestMF)) {
|
try (InputStream is = Files.newInputStream(manifestMF)) {
|
||||||
try {
|
Manifest manifest = new Manifest(is);
|
||||||
Manifest manifest = new Manifest(Files.newInputStream(manifestMF));
|
jarVersion = manifest.getMainAttributes().getValue(Attributes.Name.IMPLEMENTATION_VERSION);
|
||||||
jarVersion = manifest.getMainAttributes().getValue(Attributes.Name.IMPLEMENTATION_VERSION);
|
} catch (IOException e) {
|
||||||
} catch (IOException e) {
|
LOG.log(Level.WARNING, "Failed to parse MANIFEST.MF in file " + modFile);
|
||||||
LOG.log(Level.WARNING, "Failed to parse MANIFEST.MF in file " + modFile);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
return new LocalModFile(modManager, modManager.getLocalMod(mod.getModId(), ModLoaderType.FORGE), modFile, mod.getDisplayName(), new LocalModFile.Description(mod.getDescription()),
|
|
||||||
mod.getAuthors(), mod.getVersion().replace("${file.jarVersion}", jarVersion), "",
|
|
||||||
mod.getDisplayURL(),
|
|
||||||
metadata.getLogoFile());
|
|
||||||
}
|
}
|
||||||
|
return new LocalModFile(modManager, modManager.getLocalMod(mod.getModId(), ModLoaderType.FORGE), modFile, mod.getDisplayName(), new LocalModFile.Description(mod.getDescription()),
|
||||||
|
mod.getAuthors(), mod.getVersion().replace("${file.jarVersion}", jarVersion), "",
|
||||||
|
mod.getDisplayURL(),
|
||||||
|
metadata.getLogoFile());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,7 +5,6 @@ import com.google.gson.annotations.SerializedName;
|
||||||
import com.google.gson.reflect.TypeToken;
|
import com.google.gson.reflect.TypeToken;
|
||||||
import com.tungsten.fclcore.util.StringUtils;
|
import com.tungsten.fclcore.util.StringUtils;
|
||||||
import com.tungsten.fclcore.util.gson.JsonUtils;
|
import com.tungsten.fclcore.util.gson.JsonUtils;
|
||||||
import com.tungsten.fclcore.util.io.CompressingUtils;
|
|
||||||
import com.tungsten.fclcore.util.io.FileUtils;
|
import com.tungsten.fclcore.util.io.FileUtils;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
|
@ -97,28 +96,26 @@ public final class ForgeOldModMetadata {
|
||||||
return authors;
|
return authors;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static LocalModFile fromFile(ModManager modManager, Path modFile) throws IOException, JsonParseException {
|
public static LocalModFile fromFile(ModManager modManager, Path modFile, FileSystem fs) throws IOException, JsonParseException {
|
||||||
try (FileSystem fs = CompressingUtils.createReadOnlyZipFileSystem(modFile)) {
|
Path mcmod = fs.getPath("mcmod.info");
|
||||||
Path mcmod = fs.getPath("mcmod.info");
|
if (Files.notExists(mcmod))
|
||||||
if (Files.notExists(mcmod))
|
throw new IOException("File " + modFile + " is not a Forge mod.");
|
||||||
throw new IOException("File " + modFile + " is not a Forge mod.");
|
List<ForgeOldModMetadata> modList = JsonUtils.GSON.fromJson(FileUtils.readText(mcmod),
|
||||||
List<ForgeOldModMetadata> modList = JsonUtils.GSON.fromJson(FileUtils.readText(mcmod),
|
new TypeToken<List<ForgeOldModMetadata>>() {
|
||||||
new TypeToken<List<ForgeOldModMetadata>>() {
|
}.getType());
|
||||||
}.getType());
|
if (modList == null || modList.isEmpty())
|
||||||
if (modList == null || modList.isEmpty())
|
throw new IOException("Mod " + modFile + " `mcmod.info` is malformed..");
|
||||||
throw new IOException("Mod " + modFile + " `mcmod.info` is malformed..");
|
ForgeOldModMetadata metadata = modList.get(0);
|
||||||
ForgeOldModMetadata metadata = modList.get(0);
|
String authors = metadata.getAuthor();
|
||||||
String authors = metadata.getAuthor();
|
if (StringUtils.isBlank(authors) && metadata.getAuthors().length > 0)
|
||||||
if (StringUtils.isBlank(authors) && metadata.getAuthors().length > 0)
|
authors = String.join(", ", metadata.getAuthors());
|
||||||
authors = String.join(", ", metadata.getAuthors());
|
if (StringUtils.isBlank(authors) && metadata.getAuthorList().length > 0)
|
||||||
if (StringUtils.isBlank(authors) && metadata.getAuthorList().length > 0)
|
authors = String.join(", ", metadata.getAuthorList());
|
||||||
authors = String.join(", ", metadata.getAuthorList());
|
if (StringUtils.isBlank(authors))
|
||||||
if (StringUtils.isBlank(authors))
|
authors = metadata.getCredits();
|
||||||
authors = metadata.getCredits();
|
return new LocalModFile(modManager, modManager.getLocalMod(metadata.getModId(), ModLoaderType.FORGE), modFile, metadata.getName(), new LocalModFile.Description(metadata.getDescription()),
|
||||||
return new LocalModFile(modManager, modManager.getLocalMod(metadata.getModId(), ModLoaderType.FORGE), modFile, metadata.getName(), new LocalModFile.Description(metadata.getDescription()),
|
authors, metadata.getVersion(), metadata.getGameVersion(),
|
||||||
authors, metadata.getVersion(), metadata.getGameVersion(),
|
StringUtils.isBlank(metadata.getUrl()) ? metadata.getUpdateUrl() : metadata.url,
|
||||||
StringUtils.isBlank(metadata.getUrl()) ? metadata.getUpdateUrl() : metadata.url,
|
metadata.getLogoFile());
|
||||||
metadata.getLogoFile());
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,7 +2,6 @@ package com.tungsten.fclcore.mod;
|
||||||
|
|
||||||
import com.google.gson.JsonParseException;
|
import com.google.gson.JsonParseException;
|
||||||
import com.tungsten.fclcore.util.gson.JsonUtils;
|
import com.tungsten.fclcore.util.gson.JsonUtils;
|
||||||
import com.tungsten.fclcore.util.io.IOUtils;
|
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.nio.file.Path;
|
import java.nio.file.Path;
|
||||||
|
@ -83,18 +82,18 @@ public final class LiteModMetadata {
|
||||||
public String getUpdateURI() {
|
public String getUpdateURI() {
|
||||||
return updateURI;
|
return updateURI;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static LocalModFile fromFile(ModManager modManager, Path modFile) throws IOException, JsonParseException {
|
public static LocalModFile fromFile(ModManager modManager, Path modFile) throws IOException, JsonParseException {
|
||||||
try (ZipFile zipFile = new ZipFile(modFile.toFile())) {
|
try (ZipFile zipFile = new ZipFile(modFile.toFile())) {
|
||||||
ZipEntry entry = zipFile.getEntry("litemod.json");
|
ZipEntry entry = zipFile.getEntry("litemod.json");
|
||||||
if (entry == null)
|
if (entry == null)
|
||||||
throw new IOException("File " + modFile + "is not a LiteLoader mod.");
|
throw new IOException("File " + modFile + "is not a LiteLoader mod.");
|
||||||
LiteModMetadata metadata = JsonUtils.GSON.fromJson(IOUtils.readFullyAsString(zipFile.getInputStream(entry)), LiteModMetadata.class);
|
LiteModMetadata metadata = JsonUtils.fromJsonFully(zipFile.getInputStream(entry), LiteModMetadata.class);
|
||||||
if (metadata == null)
|
if (metadata == null)
|
||||||
throw new IOException("Mod " + modFile + " `litemod.json` is malformed.");
|
throw new IOException("Mod " + modFile + " `litemod.json` is malformed.");
|
||||||
return new LocalModFile(modManager, modManager.getLocalMod(metadata.getName(), ModLoaderType.LITE_LOADER), modFile, metadata.getName(), new LocalModFile.Description(metadata.getDescription()), metadata.getAuthor(),
|
return new LocalModFile(modManager, modManager.getLocalMod(metadata.getName(), ModLoaderType.LITE_LOADER), modFile, metadata.getName(), new LocalModFile.Description(metadata.getDescription()), metadata.getAuthor(),
|
||||||
metadata.getVersion(), metadata.getGameVersion(), metadata.getUpdateURI(), "");
|
metadata.getVersion(), metadata.getGameVersion(), metadata.getUpdateURI(), "");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -149,6 +149,10 @@ public final class LocalModFile implements Comparable<LocalModFile> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void disable() throws IOException {
|
||||||
|
file = modManager.disableMod(file);
|
||||||
|
}
|
||||||
|
|
||||||
public ModUpdate checkUpdates(String gameVersion, RemoteModRepository repository) throws IOException {
|
public ModUpdate checkUpdates(String gameVersion, RemoteModRepository repository) throws IOException {
|
||||||
Optional<RemoteMod.Version> currentVersion = repository.getRemoteVersionByLocalFile(this, file);
|
Optional<RemoteMod.Version> currentVersion = repository.getRemoteVersionByLocalFile(this, file);
|
||||||
if (!currentVersion.isPresent()) return null;
|
if (!currentVersion.isPresent()) return null;
|
||||||
|
|
|
@ -1,9 +1,7 @@
|
||||||
package com.tungsten.fclcore.mod;
|
package com.tungsten.fclcore.mod;
|
||||||
|
|
||||||
import static com.tungsten.fclcore.util.DigestUtils.digest;
|
|
||||||
import static com.tungsten.fclcore.util.Hex.encodeHex;
|
|
||||||
|
|
||||||
import com.tungsten.fclcore.task.Task;
|
import com.tungsten.fclcore.task.Task;
|
||||||
|
import com.tungsten.fclcore.util.DigestUtils;
|
||||||
import com.tungsten.fclcore.util.gson.JsonUtils;
|
import com.tungsten.fclcore.util.gson.JsonUtils;
|
||||||
import com.tungsten.fclcore.util.io.CompressingUtils;
|
import com.tungsten.fclcore.util.io.CompressingUtils;
|
||||||
import com.tungsten.fclcore.util.io.FileUtils;
|
import com.tungsten.fclcore.util.io.FileUtils;
|
||||||
|
@ -52,7 +50,7 @@ public final class MinecraftInstanceTask<T> extends Task<ModpackConfiguration<T>
|
||||||
@Override
|
@Override
|
||||||
public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException {
|
public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException {
|
||||||
String relativePath = root.relativize(file).normalize().toString().replace(File.separatorChar, '/');
|
String relativePath = root.relativize(file).normalize().toString().replace(File.separatorChar, '/');
|
||||||
overrides.add(new ModpackConfiguration.FileInformation(relativePath, encodeHex(digest("SHA-1", file))));
|
overrides.add(new ModpackConfiguration.FileInformation(relativePath, DigestUtils.digestToString("SHA-1", file)));
|
||||||
return FileVisitResult.CONTINUE;
|
return FileVisitResult.CONTINUE;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
|
@ -2,6 +2,7 @@ package com.tungsten.fclcore.mod;
|
||||||
|
|
||||||
import com.tungsten.fclcore.util.Lang;
|
import com.tungsten.fclcore.util.Lang;
|
||||||
|
|
||||||
|
import java.io.File;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
public interface ModAdviser {
|
public interface ModAdviser {
|
||||||
|
@ -41,21 +42,21 @@ public interface ModAdviser {
|
||||||
"optionsof.txt" /* OptiFine */,
|
"optionsof.txt" /* OptiFine */,
|
||||||
"journeymap" /* JourneyMap */,
|
"journeymap" /* JourneyMap */,
|
||||||
"optionsshaders.txt",
|
"optionsshaders.txt",
|
||||||
"mods/VoxelMods");
|
"mods" + File.separator + "VoxelMods");
|
||||||
|
|
||||||
static ModSuggestion suggestMod(String fileName, boolean isDirectory) {
|
static ModAdviser.ModSuggestion suggestMod(String fileName, boolean isDirectory) {
|
||||||
if (match(MODPACK_BLACK_LIST, fileName, isDirectory))
|
if (match(MODPACK_BLACK_LIST, fileName, isDirectory))
|
||||||
return ModSuggestion.HIDDEN;
|
return ModAdviser.ModSuggestion.HIDDEN;
|
||||||
if (match(MODPACK_SUGGESTED_BLACK_LIST, fileName, isDirectory))
|
if (match(MODPACK_SUGGESTED_BLACK_LIST, fileName, isDirectory))
|
||||||
return ModSuggestion.NORMAL;
|
return ModAdviser.ModSuggestion.NORMAL;
|
||||||
else
|
else
|
||||||
return ModSuggestion.SUGGESTED;
|
return ModAdviser.ModSuggestion.SUGGESTED;
|
||||||
}
|
}
|
||||||
|
|
||||||
static boolean match(List<String> l, String fileName, boolean isDirectory) {
|
static boolean match(List<String> l, String fileName, boolean isDirectory) {
|
||||||
for (String s : l)
|
for (String s : l)
|
||||||
if (isDirectory) {
|
if (isDirectory) {
|
||||||
if (fileName.startsWith(s + "/"))
|
if (fileName.startsWith(s + File.separator))
|
||||||
return true;
|
return true;
|
||||||
} else {
|
} else {
|
||||||
if (s.startsWith("regex:")) {
|
if (s.startsWith("regex:")) {
|
||||||
|
|
|
@ -4,6 +4,7 @@ public enum ModLoaderType {
|
||||||
UNKNOWN,
|
UNKNOWN,
|
||||||
FORGE,
|
FORGE,
|
||||||
FABRIC,
|
FABRIC,
|
||||||
|
QUILT,
|
||||||
LITE_LOADER,
|
LITE_LOADER,
|
||||||
PACK
|
PACK
|
||||||
}
|
}
|
||||||
|
|
|
@ -56,24 +56,27 @@ public final class ModManager {
|
||||||
String fileName = StringUtils.removeSuffix(FileUtils.getName(modFile), DISABLED_EXTENSION, OLD_EXTENSION);
|
String fileName = StringUtils.removeSuffix(FileUtils.getName(modFile), DISABLED_EXTENSION, OLD_EXTENSION);
|
||||||
String description;
|
String description;
|
||||||
if (fileName.endsWith(".zip") || fileName.endsWith(".jar")) {
|
if (fileName.endsWith(".zip") || fileName.endsWith(".jar")) {
|
||||||
try {
|
try (FileSystem fs = CompressingUtils.createReadOnlyZipFileSystem(modFile)) {
|
||||||
return ForgeOldModMetadata.fromFile(this, modFile);
|
try {
|
||||||
} catch (Exception ignore) {
|
return ForgeOldModMetadata.fromFile(this, modFile, fs);
|
||||||
}
|
} catch (Exception ignore) {
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
return ForgeNewModMetadata.fromFile(this, modFile);
|
return ForgeNewModMetadata.fromFile(this, modFile, fs);
|
||||||
} catch (Exception ignore) {
|
} catch (Exception ignore) {
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
return FabricModMetadata.fromFile(this, modFile);
|
return FabricModMetadata.fromFile(this, modFile, fs);
|
||||||
} catch (Exception ignore) {
|
} catch (Exception ignore) {
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
return PackMcMeta.fromFile(this, modFile);
|
return PackMcMeta.fromFile(this, modFile, fs);
|
||||||
} catch (Exception ignore) {
|
} catch (Exception ignore) {
|
||||||
|
}
|
||||||
|
} catch (Exception ignored) {
|
||||||
}
|
}
|
||||||
|
|
||||||
description = "";
|
description = "";
|
||||||
|
@ -214,7 +217,11 @@ public final class ModManager {
|
||||||
|
|
||||||
public Path disableMod(Path file) throws IOException {
|
public Path disableMod(Path file) throws IOException {
|
||||||
if (isOld(file)) return file; // no need to disable an old mod.
|
if (isOld(file)) return file; // no need to disable an old mod.
|
||||||
Path disabled = file.resolveSibling(StringUtils.addSuffix(FileUtils.getName(file), DISABLED_EXTENSION));
|
|
||||||
|
String fileName = FileUtils.getName(file);
|
||||||
|
if (fileName.endsWith(DISABLED_EXTENSION)) return file;
|
||||||
|
|
||||||
|
Path disabled = file.resolveSibling(fileName + DISABLED_EXTENSION);
|
||||||
if (Files.exists(file))
|
if (Files.exists(file))
|
||||||
Files.move(file, disabled, StandardCopyOption.REPLACE_EXISTING);
|
Files.move(file, disabled, StandardCopyOption.REPLACE_EXISTING);
|
||||||
return disabled;
|
return disabled;
|
||||||
|
|
|
@ -98,9 +98,8 @@ public abstract class Modpack {
|
||||||
public static boolean acceptFile(String path, List<String> blackList, List<String> whiteList) {
|
public static boolean acceptFile(String path, List<String> blackList, List<String> whiteList) {
|
||||||
if (path.isEmpty())
|
if (path.isEmpty())
|
||||||
return true;
|
return true;
|
||||||
for (String s : blackList)
|
if (ModAdviser.match(blackList, path, false))
|
||||||
if (path.equals(s))
|
return false;
|
||||||
return false;
|
|
||||||
if (whiteList == null || whiteList.isEmpty())
|
if (whiteList == null || whiteList.isEmpty())
|
||||||
return true;
|
return true;
|
||||||
for (String s : whiteList)
|
for (String s : whiteList)
|
||||||
|
|
|
@ -1,9 +1,7 @@
|
||||||
package com.tungsten.fclcore.mod;
|
package com.tungsten.fclcore.mod;
|
||||||
|
|
||||||
import static com.tungsten.fclcore.util.DigestUtils.digest;
|
|
||||||
import static com.tungsten.fclcore.util.Hex.encodeHex;
|
|
||||||
|
|
||||||
import com.tungsten.fclcore.task.Task;
|
import com.tungsten.fclcore.task.Task;
|
||||||
|
import com.tungsten.fclcore.util.DigestUtils;
|
||||||
import com.tungsten.fclcore.util.io.FileUtils;
|
import com.tungsten.fclcore.util.io.FileUtils;
|
||||||
import com.tungsten.fclcore.util.io.Unzipper;
|
import com.tungsten.fclcore.util.io.Unzipper;
|
||||||
|
|
||||||
|
@ -76,7 +74,7 @@ public class ModpackInstallTask<T> extends Task<Void> {
|
||||||
} else {
|
} else {
|
||||||
// If both old and new modpacks have this entry, and user has modified this file,
|
// If both old and new modpacks have this entry, and user has modified this file,
|
||||||
// we will not replace it since this modified file is what user expects.
|
// we will not replace it since this modified file is what user expects.
|
||||||
String fileHash = encodeHex(digest("SHA-1", destPath));
|
String fileHash = DigestUtils.digestToString("SHA-1", destPath);
|
||||||
String oldHash = files.get(entryPath).getHash();
|
String oldHash = files.get(entryPath).getHash();
|
||||||
return Objects.equals(oldHash, fileHash);
|
return Objects.equals(oldHash, fileHash);
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,7 +5,6 @@ import com.google.gson.annotations.JsonAdapter;
|
||||||
import com.google.gson.annotations.SerializedName;
|
import com.google.gson.annotations.SerializedName;
|
||||||
import com.tungsten.fclcore.util.gson.JsonUtils;
|
import com.tungsten.fclcore.util.gson.JsonUtils;
|
||||||
import com.tungsten.fclcore.util.gson.Validation;
|
import com.tungsten.fclcore.util.gson.Validation;
|
||||||
import com.tungsten.fclcore.util.io.CompressingUtils;
|
|
||||||
import com.tungsten.fclcore.util.io.FileUtils;
|
import com.tungsten.fclcore.util.io.FileUtils;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
|
@ -124,19 +123,17 @@ public class PackMcMeta implements Validation {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public static LocalModFile fromFile(ModManager modManager, Path modFile) throws IOException, JsonParseException {
|
public static LocalModFile fromFile(ModManager modManager, Path modFile, FileSystem fs) throws IOException, JsonParseException {
|
||||||
try (FileSystem fs = CompressingUtils.createReadOnlyZipFileSystem(modFile)) {
|
Path mcmod = fs.getPath("pack.mcmeta");
|
||||||
Path mcmod = fs.getPath("pack.mcmeta");
|
if (Files.notExists(mcmod))
|
||||||
if (Files.notExists(mcmod))
|
throw new IOException("File " + modFile + " is not a resource pack.");
|
||||||
throw new IOException("File " + modFile + " is not a resource pack.");
|
PackMcMeta metadata = JsonUtils.fromNonNullJson(FileUtils.readText(mcmod), PackMcMeta.class);
|
||||||
PackMcMeta metadata = JsonUtils.fromNonNullJson(FileUtils.readText(mcmod), PackMcMeta.class);
|
return new LocalModFile(
|
||||||
return new LocalModFile(
|
modManager,
|
||||||
modManager,
|
modManager.getLocalMod(FileUtils.getNameWithoutExtension(modFile), ModLoaderType.PACK),
|
||||||
modManager.getLocalMod(FileUtils.getNameWithoutExtension(modFile), ModLoaderType.PACK),
|
modFile,
|
||||||
modFile,
|
FileUtils.getNameWithoutExtension(modFile),
|
||||||
FileUtils.getNameWithoutExtension(modFile),
|
metadata.pack.description,
|
||||||
metadata.pack.description,
|
"", "", "", "", "");
|
||||||
"", "", "", "", "");
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,6 +2,8 @@ package com.tungsten.fclcore.mod;
|
||||||
|
|
||||||
import static com.tungsten.fclcore.util.io.NetworkUtils.encodeLocation;
|
import static com.tungsten.fclcore.util.io.NetworkUtils.encodeLocation;
|
||||||
|
|
||||||
|
import com.tungsten.fclcore.mod.curse.CurseForgeRemoteModRepository;
|
||||||
|
import com.tungsten.fclcore.mod.modrinth.ModrinthRemoteModRepository;
|
||||||
import com.tungsten.fclcore.task.FileDownloadTask;
|
import com.tungsten.fclcore.task.FileDownloadTask;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
|
@ -70,8 +72,18 @@ public class RemoteMod {
|
||||||
}
|
}
|
||||||
|
|
||||||
public enum Type {
|
public enum Type {
|
||||||
CURSEFORGE,
|
CURSEFORGE(CurseForgeRemoteModRepository.MODS),
|
||||||
MODRINTH
|
MODRINTH(ModrinthRemoteModRepository.MODS);
|
||||||
|
|
||||||
|
private final RemoteModRepository remoteModRepository;
|
||||||
|
|
||||||
|
public RemoteModRepository getRemoteModRepository() {
|
||||||
|
return this.remoteModRepository;
|
||||||
|
}
|
||||||
|
|
||||||
|
Type(RemoteModRepository remoteModRepository) {
|
||||||
|
this.remoteModRepository = remoteModRepository;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public interface IMod {
|
public interface IMod {
|
||||||
|
@ -184,7 +196,7 @@ public class RemoteMod {
|
||||||
}
|
}
|
||||||
|
|
||||||
public String getUrl() {
|
public String getUrl() {
|
||||||
return encodeLocation (url);
|
return encodeLocation(url);
|
||||||
}
|
}
|
||||||
|
|
||||||
public String getFilename() {
|
public String getFilename() {
|
||||||
|
|
|
@ -21,10 +21,10 @@ public interface RemoteModRepository {
|
||||||
Type getType();
|
Type getType();
|
||||||
|
|
||||||
enum SortType {
|
enum SortType {
|
||||||
DATE_CREATED,
|
|
||||||
POPULARITY,
|
POPULARITY,
|
||||||
LAST_UPDATED,
|
|
||||||
NAME,
|
NAME,
|
||||||
|
DATE_CREATED,
|
||||||
|
LAST_UPDATED,
|
||||||
AUTHOR,
|
AUTHOR,
|
||||||
TOTAL_DOWNLOADS
|
TOTAL_DOWNLOADS
|
||||||
}
|
}
|
||||||
|
@ -72,6 +72,8 @@ public interface RemoteModRepository {
|
||||||
}
|
}
|
||||||
|
|
||||||
String[] DEFAULT_GAME_VERSIONS = new String[]{
|
String[] DEFAULT_GAME_VERSIONS = new String[]{
|
||||||
|
"1.20.1", "1.20",
|
||||||
|
"1.19.4", "1.19.3", "1.19.2", "1.19.1", "1.19",
|
||||||
"1.18.2", "1.18.1", "1.18",
|
"1.18.2", "1.18.1", "1.18",
|
||||||
"1.17.1", "1.17",
|
"1.17.1", "1.17",
|
||||||
"1.16.5", "1.16.4", "1.16.3", "1.16.2", "1.16.1", "1.16",
|
"1.16.5", "1.16.4", "1.16.3", "1.16.2", "1.16.1", "1.16",
|
||||||
|
|
|
@ -532,15 +532,6 @@ public class CurseAddon implements RemoteMod.IMod {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
ModLoaderType modLoaderType;
|
|
||||||
if (gameVersions.contains("Forge")) {
|
|
||||||
modLoaderType = ModLoaderType.FORGE;
|
|
||||||
} else if (gameVersions.contains("Fabric")) {
|
|
||||||
modLoaderType = ModLoaderType.FABRIC;
|
|
||||||
} else {
|
|
||||||
modLoaderType = ModLoaderType.UNKNOWN;
|
|
||||||
}
|
|
||||||
|
|
||||||
return new RemoteMod.Version(
|
return new RemoteMod.Version(
|
||||||
this,
|
this,
|
||||||
Integer.toString(modId),
|
Integer.toString(modId),
|
||||||
|
@ -552,7 +543,12 @@ public class CurseAddon implements RemoteMod.IMod {
|
||||||
new RemoteMod.File(Collections.emptyMap(), getDownloadUrl(), getFileName()),
|
new RemoteMod.File(Collections.emptyMap(), getDownloadUrl(), getFileName()),
|
||||||
Collections.emptyList(),
|
Collections.emptyList(),
|
||||||
gameVersions.stream().filter(ver -> ver.startsWith("1.") || ver.contains("w")).collect(Collectors.toList()),
|
gameVersions.stream().filter(ver -> ver.startsWith("1.") || ver.contains("w")).collect(Collectors.toList()),
|
||||||
Collections.singletonList(modLoaderType)
|
gameVersions.stream().flatMap(version -> {
|
||||||
|
if ("fabric".equalsIgnoreCase(version)) return Stream.of(ModLoaderType.FABRIC);
|
||||||
|
else if ("forge".equalsIgnoreCase(version)) return Stream.of(ModLoaderType.FORGE);
|
||||||
|
else if ("quilt".equalsIgnoreCase(version)) return Stream.of(ModLoaderType.QUILT);
|
||||||
|
else return Stream.empty();
|
||||||
|
}).collect(Collectors.toList())
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -24,9 +24,12 @@ import java.util.stream.Stream;
|
||||||
public final class CurseForgeRemoteModRepository implements RemoteModRepository {
|
public final class CurseForgeRemoteModRepository implements RemoteModRepository {
|
||||||
|
|
||||||
private static final String PREFIX = "https://api.curseforge.com";
|
private static final String PREFIX = "https://api.curseforge.com";
|
||||||
|
|
||||||
private static final String apiKey = "$2a$10$qqJ3zZFG5CDsVHk8eV5ft.2ywg2edBtHwS3gzFnw7CDe3X2cKpWZG";
|
private static final String apiKey = "$2a$10$qqJ3zZFG5CDsVHk8eV5ft.2ywg2edBtHwS3gzFnw7CDe3X2cKpWZG";
|
||||||
|
|
||||||
|
public static boolean isAvailable() {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
private final Type type;
|
private final Type type;
|
||||||
private final int section;
|
private final int section;
|
||||||
|
|
||||||
|
@ -72,7 +75,7 @@ public final class CurseForgeRemoteModRepository implements RemoteModRepository
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Stream<RemoteMod> search(String gameVersion, @Nullable Category category, int pageOffset, int pageSize, String searchFilter, SortType sortType, SortOrder sortOrder) throws IOException {
|
public Stream<RemoteMod> search(String gameVersion, @Nullable RemoteModRepository.Category category, int pageOffset, int pageSize, String searchFilter, SortType sortType, SortOrder sortOrder) throws IOException {
|
||||||
int categoryId = 0;
|
int categoryId = 0;
|
||||||
if (category != null) categoryId = ((CurseAddon.Category) category.getSelf()).getId();
|
if (category != null) categoryId = ((CurseAddon.Category) category.getSelf()).getId();
|
||||||
Response<List<CurseAddon>> response = HttpRequest.GET(PREFIX + "/v1/mods/search",
|
Response<List<CurseAddon>> response = HttpRequest.GET(PREFIX + "/v1/mods/search",
|
||||||
|
@ -159,7 +162,7 @@ public final class CurseForgeRemoteModRepository implements RemoteModRepository
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Stream<Category> getCategories() throws IOException {
|
public Stream<RemoteModRepository.Category> getCategories() throws IOException {
|
||||||
return getCategoriesImpl().stream().map(CurseAddon.Category::toCategory);
|
return getCategoriesImpl().stream().map(CurseAddon.Category::toCategory);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -196,11 +199,11 @@ public final class CurseForgeRemoteModRepository implements RemoteModRepository
|
||||||
public static final int SECTION_UNKNOWN2 = 4979;
|
public static final int SECTION_UNKNOWN2 = 4979;
|
||||||
public static final int SECTION_UNKNOWN3 = 4984;
|
public static final int SECTION_UNKNOWN3 = 4984;
|
||||||
|
|
||||||
public static final CurseForgeRemoteModRepository MODS = new CurseForgeRemoteModRepository(Type.MOD, SECTION_MOD);
|
public static final CurseForgeRemoteModRepository MODS = new CurseForgeRemoteModRepository(RemoteModRepository.Type.MOD, SECTION_MOD);
|
||||||
public static final CurseForgeRemoteModRepository MODPACKS = new CurseForgeRemoteModRepository(Type.MODPACK, SECTION_MODPACK);
|
public static final CurseForgeRemoteModRepository MODPACKS = new CurseForgeRemoteModRepository(RemoteModRepository.Type.MODPACK, SECTION_MODPACK);
|
||||||
public static final CurseForgeRemoteModRepository RESOURCE_PACKS = new CurseForgeRemoteModRepository(Type.RESOURCE_PACK, SECTION_RESOURCE_PACK);
|
public static final CurseForgeRemoteModRepository RESOURCE_PACKS = new CurseForgeRemoteModRepository(RemoteModRepository.Type.RESOURCE_PACK, SECTION_RESOURCE_PACK);
|
||||||
public static final CurseForgeRemoteModRepository WORLDS = new CurseForgeRemoteModRepository(Type.WORLD, SECTION_WORLD);
|
public static final CurseForgeRemoteModRepository WORLDS = new CurseForgeRemoteModRepository(RemoteModRepository.Type.WORLD, SECTION_WORLD);
|
||||||
public static final CurseForgeRemoteModRepository CUSTOMIZATIONS = new CurseForgeRemoteModRepository(Type.CUSTOMIZATION, SECTION_CUSTOMIZATION);
|
public static final CurseForgeRemoteModRepository CUSTOMIZATIONS = new CurseForgeRemoteModRepository(RemoteModRepository.Type.CUSTOMIZATION, SECTION_CUSTOMIZATION);
|
||||||
|
|
||||||
public static class Pagination {
|
public static class Pagination {
|
||||||
private final int index;
|
private final int index;
|
||||||
|
|
|
@ -1,7 +1,5 @@
|
||||||
package com.tungsten.fclcore.mod.mcbbs;
|
package com.tungsten.fclcore.mod.mcbbs;
|
||||||
|
|
||||||
import static com.tungsten.fclcore.util.DigestUtils.digest;
|
|
||||||
import static com.tungsten.fclcore.util.Hex.encodeHex;
|
|
||||||
import static com.tungsten.fclcore.util.Lang.wrap;
|
import static com.tungsten.fclcore.util.Lang.wrap;
|
||||||
import static com.tungsten.fclcore.util.Lang.wrapConsumer;
|
import static com.tungsten.fclcore.util.Lang.wrapConsumer;
|
||||||
|
|
||||||
|
@ -18,6 +16,7 @@ import com.tungsten.fclcore.task.FileDownloadTask;
|
||||||
import com.tungsten.fclcore.task.GetTask;
|
import com.tungsten.fclcore.task.GetTask;
|
||||||
import com.tungsten.fclcore.task.Task;
|
import com.tungsten.fclcore.task.Task;
|
||||||
import com.tungsten.fclcore.task.TaskCompletableFuture;
|
import com.tungsten.fclcore.task.TaskCompletableFuture;
|
||||||
|
import com.tungsten.fclcore.util.DigestUtils;
|
||||||
import com.tungsten.fclcore.util.Logging;
|
import com.tungsten.fclcore.util.Logging;
|
||||||
import com.tungsten.fclcore.util.StringUtils;
|
import com.tungsten.fclcore.util.StringUtils;
|
||||||
import com.tungsten.fclcore.util.gson.JsonUtils;
|
import com.tungsten.fclcore.util.gson.JsonUtils;
|
||||||
|
@ -126,7 +125,7 @@ public class McbbsModpackCompletionTask extends CompletableFutureTask<Void> {
|
||||||
} else if (getFileHash(file) != null) {
|
} else if (getFileHash(file) != null) {
|
||||||
// If user modified this entry file, we will not replace this file since this modified file is what user expects.
|
// If user modified this entry file, we will not replace this file since this modified file is what user expects.
|
||||||
// Or we have downloaded latest file in previous completion task, this time we have no need to download it again.
|
// Or we have downloaded latest file in previous completion task, this time we have no need to download it again.
|
||||||
String fileHash = encodeHex(digest("SHA-1", actualPath));
|
String fileHash = DigestUtils.digestToString("SHA-1", actualPath);
|
||||||
String oldHash = getFileHash(oldFile);
|
String oldHash = getFileHash(oldFile);
|
||||||
String newHash = getFileHash(file);
|
String newHash = getFileHash(file);
|
||||||
if (oldHash == null) {
|
if (oldHash == null) {
|
||||||
|
|
|
@ -5,8 +5,7 @@ import static com.tungsten.fclcore.download.LibraryAnalyzer.LibraryType.FORGE;
|
||||||
import static com.tungsten.fclcore.download.LibraryAnalyzer.LibraryType.LITELOADER;
|
import static com.tungsten.fclcore.download.LibraryAnalyzer.LibraryType.LITELOADER;
|
||||||
import static com.tungsten.fclcore.download.LibraryAnalyzer.LibraryType.MINECRAFT;
|
import static com.tungsten.fclcore.download.LibraryAnalyzer.LibraryType.MINECRAFT;
|
||||||
import static com.tungsten.fclcore.download.LibraryAnalyzer.LibraryType.OPTIFINE;
|
import static com.tungsten.fclcore.download.LibraryAnalyzer.LibraryType.OPTIFINE;
|
||||||
import static com.tungsten.fclcore.util.DigestUtils.digest;
|
import static com.tungsten.fclcore.download.LibraryAnalyzer.LibraryType.QUILT;
|
||||||
import static com.tungsten.fclcore.util.Hex.encodeHex;
|
|
||||||
|
|
||||||
import com.tungsten.fclcore.download.LibraryAnalyzer;
|
import com.tungsten.fclcore.download.LibraryAnalyzer;
|
||||||
import com.tungsten.fclcore.game.DefaultGameRepository;
|
import com.tungsten.fclcore.game.DefaultGameRepository;
|
||||||
|
@ -18,6 +17,7 @@ import com.tungsten.fclcore.mod.curse.CurseManifest;
|
||||||
import com.tungsten.fclcore.mod.curse.CurseManifestMinecraft;
|
import com.tungsten.fclcore.mod.curse.CurseManifestMinecraft;
|
||||||
import com.tungsten.fclcore.mod.curse.CurseManifestModLoader;
|
import com.tungsten.fclcore.mod.curse.CurseManifestModLoader;
|
||||||
import com.tungsten.fclcore.task.Task;
|
import com.tungsten.fclcore.task.Task;
|
||||||
|
import com.tungsten.fclcore.util.DigestUtils;
|
||||||
import com.tungsten.fclcore.util.Logging;
|
import com.tungsten.fclcore.util.Logging;
|
||||||
import com.tungsten.fclcore.util.StringUtils;
|
import com.tungsten.fclcore.util.StringUtils;
|
||||||
import com.tungsten.fclcore.util.gson.JsonUtils;
|
import com.tungsten.fclcore.util.gson.JsonUtils;
|
||||||
|
@ -62,7 +62,7 @@ public class McbbsModpackExportTask extends Task<Void> {
|
||||||
Path file = runDirectory.resolve(path);
|
Path file = runDirectory.resolve(path);
|
||||||
if (Files.isRegularFile(file)) {
|
if (Files.isRegularFile(file)) {
|
||||||
String relativePath = runDirectory.relativize(file).normalize().toString().replace(File.separatorChar, '/');
|
String relativePath = runDirectory.relativize(file).normalize().toString().replace(File.separatorChar, '/');
|
||||||
files.add(new McbbsModpackManifest.AddonFile(true, relativePath, encodeHex(digest("SHA-1", file))));
|
files.add(new McbbsModpackManifest.AddonFile(true, relativePath, DigestUtils.digestToString("SHA-1", file)));
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
} else {
|
} else {
|
||||||
|
@ -85,6 +85,8 @@ public class McbbsModpackExportTask extends Task<Void> {
|
||||||
addons.add(new McbbsModpackManifest.Addon(OPTIFINE.getPatchId(), optifineVersion)));
|
addons.add(new McbbsModpackManifest.Addon(OPTIFINE.getPatchId(), optifineVersion)));
|
||||||
analyzer.getVersion(FABRIC).ifPresent(fabricVersion ->
|
analyzer.getVersion(FABRIC).ifPresent(fabricVersion ->
|
||||||
addons.add(new McbbsModpackManifest.Addon(FABRIC.getPatchId(), fabricVersion)));
|
addons.add(new McbbsModpackManifest.Addon(FABRIC.getPatchId(), fabricVersion)));
|
||||||
|
analyzer.getVersion(QUILT).ifPresent(quiltVersion ->
|
||||||
|
addons.add(new McbbsModpackManifest.Addon(QUILT.getPatchId(), quiltVersion)));
|
||||||
|
|
||||||
List<Library> libraries = new ArrayList<>();
|
List<Library> libraries = new ArrayList<>();
|
||||||
// TODO libraries
|
// TODO libraries
|
||||||
|
|
|
@ -11,13 +11,13 @@ import com.tungsten.fclcore.mod.ModpackProvider;
|
||||||
import com.tungsten.fclcore.mod.ModpackUpdateTask;
|
import com.tungsten.fclcore.mod.ModpackUpdateTask;
|
||||||
import com.tungsten.fclcore.task.Task;
|
import com.tungsten.fclcore.task.Task;
|
||||||
import com.tungsten.fclcore.util.gson.JsonUtils;
|
import com.tungsten.fclcore.util.gson.JsonUtils;
|
||||||
import com.tungsten.fclcore.util.io.IOUtils;
|
|
||||||
|
|
||||||
import org.apache.commons.compress.archivers.zip.ZipArchiveEntry;
|
import org.apache.commons.compress.archivers.zip.ZipArchiveEntry;
|
||||||
import org.apache.commons.compress.archivers.zip.ZipFile;
|
import org.apache.commons.compress.archivers.zip.ZipFile;
|
||||||
|
|
||||||
import java.io.File;
|
import java.io.File;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
|
import java.io.InputStream;
|
||||||
import java.nio.charset.Charset;
|
import java.nio.charset.Charset;
|
||||||
import java.nio.file.Path;
|
import java.nio.file.Path;
|
||||||
|
|
||||||
|
@ -54,8 +54,8 @@ public final class McbbsModpackProvider implements ModpackProvider {
|
||||||
config.getManifest().injectLaunchOptions(builder);
|
config.getManifest().injectLaunchOptions(builder);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static Modpack fromManifestFile(String json, Charset encoding) throws IOException, JsonParseException {
|
private static Modpack fromManifestFile(InputStream json, Charset encoding) throws IOException, JsonParseException {
|
||||||
McbbsModpackManifest manifest = JsonUtils.fromNonNullJson(json, McbbsModpackManifest.class);
|
McbbsModpackManifest manifest = JsonUtils.fromNonNullJsonFully(json, McbbsModpackManifest.class);
|
||||||
return manifest.toModpack(encoding);
|
return manifest.toModpack(encoding);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -63,11 +63,11 @@ public final class McbbsModpackProvider implements ModpackProvider {
|
||||||
public Modpack readManifest(ZipFile zip, Path file, Charset encoding) throws IOException, JsonParseException {
|
public Modpack readManifest(ZipFile zip, Path file, Charset encoding) throws IOException, JsonParseException {
|
||||||
ZipArchiveEntry mcbbsPackMeta = zip.getEntry("mcbbs.packmeta");
|
ZipArchiveEntry mcbbsPackMeta = zip.getEntry("mcbbs.packmeta");
|
||||||
if (mcbbsPackMeta != null) {
|
if (mcbbsPackMeta != null) {
|
||||||
return fromManifestFile(IOUtils.readFullyAsString(zip.getInputStream(mcbbsPackMeta)), encoding);
|
return fromManifestFile(zip.getInputStream(mcbbsPackMeta), encoding);
|
||||||
}
|
}
|
||||||
ZipArchiveEntry manifestJson = zip.getEntry("manifest.json");
|
ZipArchiveEntry manifestJson = zip.getEntry("manifest.json");
|
||||||
if (manifestJson != null) {
|
if (manifestJson != null) {
|
||||||
return fromManifestFile(IOUtils.readFullyAsString(zip.getInputStream(manifestJson)), encoding);
|
return fromManifestFile(zip.getInputStream(manifestJson), encoding);
|
||||||
}
|
}
|
||||||
throw new IOException("`mcbbs.packmeta` or `manifest.json` cannot be found");
|
throw new IOException("`mcbbs.packmeta` or `manifest.json` cannot be found");
|
||||||
}
|
}
|
||||||
|
|
|
@ -10,7 +10,6 @@ import com.tungsten.fclcore.mod.ModLoaderType;
|
||||||
import com.tungsten.fclcore.mod.RemoteMod;
|
import com.tungsten.fclcore.mod.RemoteMod;
|
||||||
import com.tungsten.fclcore.mod.RemoteModRepository;
|
import com.tungsten.fclcore.mod.RemoteModRepository;
|
||||||
import com.tungsten.fclcore.util.DigestUtils;
|
import com.tungsten.fclcore.util.DigestUtils;
|
||||||
import com.tungsten.fclcore.util.Hex;
|
|
||||||
import com.tungsten.fclcore.util.Lang;
|
import com.tungsten.fclcore.util.Lang;
|
||||||
import com.tungsten.fclcore.util.StringUtils;
|
import com.tungsten.fclcore.util.StringUtils;
|
||||||
import com.tungsten.fclcore.util.gson.JsonUtils;
|
import com.tungsten.fclcore.util.gson.JsonUtils;
|
||||||
|
@ -85,7 +84,7 @@ public final class ModrinthRemoteModRepository implements RemoteModRepository {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Optional<RemoteMod.Version> getRemoteVersionByLocalFile(LocalModFile localModFile, Path file) throws IOException {
|
public Optional<RemoteMod.Version> getRemoteVersionByLocalFile(LocalModFile localModFile, Path file) throws IOException {
|
||||||
String sha1 = Hex.encodeHex(DigestUtils.digest("SHA-1", file));
|
String sha1 = DigestUtils.digestToString("SHA-1", file);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
ProjectVersion mod = HttpRequest.GET(PREFIX + "/v2/version_file/" + sha1,
|
ProjectVersion mod = HttpRequest.GET(PREFIX + "/v2/version_file/" + sha1,
|
||||||
|
@ -483,6 +482,7 @@ public final class ModrinthRemoteModRepository implements RemoteModRepository {
|
||||||
loaders.stream().flatMap(loader -> {
|
loaders.stream().flatMap(loader -> {
|
||||||
if ("fabric".equalsIgnoreCase(loader)) return Stream.of(ModLoaderType.FABRIC);
|
if ("fabric".equalsIgnoreCase(loader)) return Stream.of(ModLoaderType.FABRIC);
|
||||||
else if ("forge".equalsIgnoreCase(loader)) return Stream.of(ModLoaderType.FORGE);
|
else if ("forge".equalsIgnoreCase(loader)) return Stream.of(ModLoaderType.FORGE);
|
||||||
|
else if ("quilt".equalsIgnoreCase(loader)) return Stream.of(ModLoaderType.QUILT);
|
||||||
else return Stream.empty();
|
else return Stream.empty();
|
||||||
}).collect(Collectors.toList())
|
}).collect(Collectors.toList())
|
||||||
));
|
));
|
||||||
|
|
|
@ -2,7 +2,6 @@ package com.tungsten.fclcore.mod.multimc;
|
||||||
|
|
||||||
import com.google.gson.annotations.SerializedName;
|
import com.google.gson.annotations.SerializedName;
|
||||||
import com.tungsten.fclcore.util.gson.JsonUtils;
|
import com.tungsten.fclcore.util.gson.JsonUtils;
|
||||||
import com.tungsten.fclcore.util.io.IOUtils;
|
|
||||||
|
|
||||||
import org.apache.commons.compress.archivers.zip.ZipArchiveEntry;
|
import org.apache.commons.compress.archivers.zip.ZipArchiveEntry;
|
||||||
import org.apache.commons.compress.archivers.zip.ZipFile;
|
import org.apache.commons.compress.archivers.zip.ZipFile;
|
||||||
|
@ -42,8 +41,7 @@ public final class MultiMCManifest {
|
||||||
ZipArchiveEntry mmcPack = zipFile.getEntry(rootEntryName + "mmc-pack.json");
|
ZipArchiveEntry mmcPack = zipFile.getEntry(rootEntryName + "mmc-pack.json");
|
||||||
if (mmcPack == null)
|
if (mmcPack == null)
|
||||||
return null;
|
return null;
|
||||||
String json = IOUtils.readFullyAsString(zipFile.getInputStream(mmcPack));
|
MultiMCManifest manifest = JsonUtils.fromNonNullJsonFully(zipFile.getInputStream(mmcPack), MultiMCManifest.class);
|
||||||
MultiMCManifest manifest = JsonUtils.fromNonNullJson(json, MultiMCManifest.class);
|
|
||||||
if (manifest.getComponents() == null)
|
if (manifest.getComponents() == null)
|
||||||
throw new IOException("mmc-pack.json malformed.");
|
throw new IOException("mmc-pack.json malformed.");
|
||||||
|
|
||||||
|
|
|
@ -3,6 +3,7 @@ package com.tungsten.fclcore.mod.multimc;
|
||||||
import static com.tungsten.fclcore.download.LibraryAnalyzer.LibraryType.FABRIC;
|
import static com.tungsten.fclcore.download.LibraryAnalyzer.LibraryType.FABRIC;
|
||||||
import static com.tungsten.fclcore.download.LibraryAnalyzer.LibraryType.FORGE;
|
import static com.tungsten.fclcore.download.LibraryAnalyzer.LibraryType.FORGE;
|
||||||
import static com.tungsten.fclcore.download.LibraryAnalyzer.LibraryType.LITELOADER;
|
import static com.tungsten.fclcore.download.LibraryAnalyzer.LibraryType.LITELOADER;
|
||||||
|
import static com.tungsten.fclcore.download.LibraryAnalyzer.LibraryType.QUILT;
|
||||||
|
|
||||||
import com.tungsten.fclcore.download.LibraryAnalyzer;
|
import com.tungsten.fclcore.download.LibraryAnalyzer;
|
||||||
import com.tungsten.fclcore.game.DefaultGameRepository;
|
import com.tungsten.fclcore.game.DefaultGameRepository;
|
||||||
|
@ -66,6 +67,8 @@ public class MultiMCModpackExportTask extends Task<Void> {
|
||||||
components.add(new MultiMCManifest.MultiMCManifestComponent(false, false, "com.mumfrey.liteloader", liteLoaderVersion)));
|
components.add(new MultiMCManifest.MultiMCManifestComponent(false, false, "com.mumfrey.liteloader", liteLoaderVersion)));
|
||||||
analyzer.getVersion(FABRIC).ifPresent(fabricVersion ->
|
analyzer.getVersion(FABRIC).ifPresent(fabricVersion ->
|
||||||
components.add(new MultiMCManifest.MultiMCManifestComponent(false, false, "net.fabricmc.fabric-loader", fabricVersion)));
|
components.add(new MultiMCManifest.MultiMCManifestComponent(false, false, "net.fabricmc.fabric-loader", fabricVersion)));
|
||||||
|
analyzer.getVersion(QUILT).ifPresent(quiltVersion ->
|
||||||
|
components.add(new MultiMCManifest.MultiMCManifestComponent(false, false, "org.quiltmc.quilt-loader", quiltVersion)));
|
||||||
MultiMCManifest mmcPack = new MultiMCManifest(1, components);
|
MultiMCManifest mmcPack = new MultiMCManifest(1, components);
|
||||||
zip.putTextFile(JsonUtils.GSON.toJson(mmcPack), "mmc-pack.json");
|
zip.putTextFile(JsonUtils.GSON.toJson(mmcPack), "mmc-pack.json");
|
||||||
|
|
||||||
|
|
|
@ -69,7 +69,7 @@ public final class MultiMCModpackInstallTask extends Task<Void> {
|
||||||
builder.version("fabric", c.getVersion());
|
builder.version("fabric", c.getVersion());
|
||||||
});
|
});
|
||||||
|
|
||||||
Optional<MultiMCManifest.MultiMCManifestComponent> quilt = manifest.getMmcPack().getComponents().stream().filter(e -> e.getUid().equals("net.quiltmc.quilt-loader")).findAny();
|
Optional<MultiMCManifest.MultiMCManifestComponent> quilt = manifest.getMmcPack().getComponents().stream().filter(e -> e.getUid().equals("org.quiltmc.quilt-loader")).findAny();
|
||||||
quilt.ifPresent(c -> {
|
quilt.ifPresent(c -> {
|
||||||
if (c.getVersion() != null)
|
if (c.getVersion() != null)
|
||||||
builder.version("quilt", c.getVersion());
|
builder.version("quilt", c.getVersion());
|
||||||
|
|
|
@ -1,8 +1,5 @@
|
||||||
package com.tungsten.fclcore.mod.server;
|
package com.tungsten.fclcore.mod.server;
|
||||||
|
|
||||||
import static com.tungsten.fclcore.util.DigestUtils.digest;
|
|
||||||
import static com.tungsten.fclcore.util.Hex.encodeHex;
|
|
||||||
|
|
||||||
import com.google.gson.JsonParseException;
|
import com.google.gson.JsonParseException;
|
||||||
import com.google.gson.reflect.TypeToken;
|
import com.google.gson.reflect.TypeToken;
|
||||||
import com.tungsten.fclcore.download.DefaultDependencyManager;
|
import com.tungsten.fclcore.download.DefaultDependencyManager;
|
||||||
|
@ -12,6 +9,7 @@ import com.tungsten.fclcore.mod.ModpackConfiguration;
|
||||||
import com.tungsten.fclcore.task.FileDownloadTask;
|
import com.tungsten.fclcore.task.FileDownloadTask;
|
||||||
import com.tungsten.fclcore.task.GetTask;
|
import com.tungsten.fclcore.task.GetTask;
|
||||||
import com.tungsten.fclcore.task.Task;
|
import com.tungsten.fclcore.task.Task;
|
||||||
|
import com.tungsten.fclcore.util.DigestUtils;
|
||||||
import com.tungsten.fclcore.util.Logging;
|
import com.tungsten.fclcore.util.Logging;
|
||||||
import com.tungsten.fclcore.util.StringUtils;
|
import com.tungsten.fclcore.util.StringUtils;
|
||||||
import com.tungsten.fclcore.util.gson.JsonUtils;
|
import com.tungsten.fclcore.util.gson.JsonUtils;
|
||||||
|
@ -132,7 +130,7 @@ public class ServerModpackCompletionTask extends Task<Void> {
|
||||||
download = true;
|
download = true;
|
||||||
} else {
|
} else {
|
||||||
// If user modified this entry file, we will not replace this file since this modified file is that user expects.
|
// If user modified this entry file, we will not replace this file since this modified file is that user expects.
|
||||||
String fileHash = encodeHex(digest("SHA-1", actualPath));
|
String fileHash = DigestUtils.digestToString("SHA-1", actualPath);
|
||||||
String oldHash = files.get(file.getPath()).getHash();
|
String oldHash = files.get(file.getPath()).getHash();
|
||||||
download = !Objects.equals(oldHash, file.getHash()) && Objects.equals(oldHash, fileHash);
|
download = !Objects.equals(oldHash, file.getHash()) && Objects.equals(oldHash, fileHash);
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,8 +5,7 @@ import static com.tungsten.fclcore.download.LibraryAnalyzer.LibraryType.FORGE;
|
||||||
import static com.tungsten.fclcore.download.LibraryAnalyzer.LibraryType.LITELOADER;
|
import static com.tungsten.fclcore.download.LibraryAnalyzer.LibraryType.LITELOADER;
|
||||||
import static com.tungsten.fclcore.download.LibraryAnalyzer.LibraryType.MINECRAFT;
|
import static com.tungsten.fclcore.download.LibraryAnalyzer.LibraryType.MINECRAFT;
|
||||||
import static com.tungsten.fclcore.download.LibraryAnalyzer.LibraryType.OPTIFINE;
|
import static com.tungsten.fclcore.download.LibraryAnalyzer.LibraryType.OPTIFINE;
|
||||||
import static com.tungsten.fclcore.util.DigestUtils.digest;
|
import static com.tungsten.fclcore.download.LibraryAnalyzer.LibraryType.QUILT;
|
||||||
import static com.tungsten.fclcore.util.Hex.encodeHex;
|
|
||||||
|
|
||||||
import com.tungsten.fclcore.download.LibraryAnalyzer;
|
import com.tungsten.fclcore.download.LibraryAnalyzer;
|
||||||
import com.tungsten.fclcore.game.DefaultGameRepository;
|
import com.tungsten.fclcore.game.DefaultGameRepository;
|
||||||
|
@ -15,6 +14,7 @@ import com.tungsten.fclcore.mod.Modpack;
|
||||||
import com.tungsten.fclcore.mod.ModpackConfiguration;
|
import com.tungsten.fclcore.mod.ModpackConfiguration;
|
||||||
import com.tungsten.fclcore.mod.ModpackExportInfo;
|
import com.tungsten.fclcore.mod.ModpackExportInfo;
|
||||||
import com.tungsten.fclcore.task.Task;
|
import com.tungsten.fclcore.task.Task;
|
||||||
|
import com.tungsten.fclcore.util.DigestUtils;
|
||||||
import com.tungsten.fclcore.util.Logging;
|
import com.tungsten.fclcore.util.Logging;
|
||||||
import com.tungsten.fclcore.util.StringUtils;
|
import com.tungsten.fclcore.util.StringUtils;
|
||||||
import com.tungsten.fclcore.util.gson.JsonUtils;
|
import com.tungsten.fclcore.util.gson.JsonUtils;
|
||||||
|
@ -58,7 +58,7 @@ public class ServerModpackExportTask extends Task<Void> {
|
||||||
Path file = runDirectory.resolve(path);
|
Path file = runDirectory.resolve(path);
|
||||||
if (Files.isRegularFile(file)) {
|
if (Files.isRegularFile(file)) {
|
||||||
String relativePath = runDirectory.relativize(file).normalize().toString().replace(File.separatorChar, '/');
|
String relativePath = runDirectory.relativize(file).normalize().toString().replace(File.separatorChar, '/');
|
||||||
files.add(new ModpackConfiguration.FileInformation(relativePath, encodeHex(digest("SHA-1", file))));
|
files.add(new ModpackConfiguration.FileInformation(relativePath, DigestUtils.digestToString("SHA-1", file)));
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
} else {
|
} else {
|
||||||
|
@ -79,6 +79,8 @@ public class ServerModpackExportTask extends Task<Void> {
|
||||||
addons.add(new ServerModpackManifest.Addon(OPTIFINE.getPatchId(), optifineVersion)));
|
addons.add(new ServerModpackManifest.Addon(OPTIFINE.getPatchId(), optifineVersion)));
|
||||||
analyzer.getVersion(FABRIC).ifPresent(fabricVersion ->
|
analyzer.getVersion(FABRIC).ifPresent(fabricVersion ->
|
||||||
addons.add(new ServerModpackManifest.Addon(FABRIC.getPatchId(), fabricVersion)));
|
addons.add(new ServerModpackManifest.Addon(FABRIC.getPatchId(), fabricVersion)));
|
||||||
|
analyzer.getVersion(QUILT).ifPresent(quiltVersion ->
|
||||||
|
addons.add(new ServerModpackManifest.Addon(QUILT.getPatchId(), quiltVersion)));
|
||||||
ServerModpackManifest manifest = new ServerModpackManifest(exportInfo.getName(), exportInfo.getAuthor(), exportInfo.getVersion(), exportInfo.getDescription(), StringUtils.removeSuffix(exportInfo.getFileApi(), "/"), files, addons);
|
ServerModpackManifest manifest = new ServerModpackManifest(exportInfo.getName(), exportInfo.getAuthor(), exportInfo.getVersion(), exportInfo.getDescription(), StringUtils.removeSuffix(exportInfo.getFileApi(), "/"), files, addons);
|
||||||
zip.putTextFile(JsonUtils.GSON.toJson(manifest), "server-manifest.json");
|
zip.putTextFile(JsonUtils.GSON.toJson(manifest), "server-manifest.json");
|
||||||
}
|
}
|
||||||
|
|
|
@ -221,8 +221,8 @@ public abstract class FetchTask<T> extends Task<T> {
|
||||||
NOT_CHECK_E_TAG,
|
NOT_CHECK_E_TAG,
|
||||||
CACHED
|
CACHED
|
||||||
}
|
}
|
||||||
|
|
||||||
protected class DownloadState {
|
protected static final class DownloadState {
|
||||||
private final int startPosition;
|
private final int startPosition;
|
||||||
private final int endPosition;
|
private final int endPosition;
|
||||||
private final int currentPosition;
|
private final int currentPosition;
|
||||||
|
@ -255,9 +255,7 @@ public abstract class FetchTask<T> extends Task<T> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
protected class DownloadMission {
|
protected static final class DownloadMission {
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -5,7 +5,6 @@ import static com.tungsten.fclcore.util.DigestUtils.getDigest;
|
||||||
import java.io.File;
|
import java.io.File;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.io.RandomAccessFile;
|
import java.io.RandomAccessFile;
|
||||||
import java.math.BigInteger;
|
|
||||||
import java.net.URL;
|
import java.net.URL;
|
||||||
import java.net.URLConnection;
|
import java.net.URLConnection;
|
||||||
import java.nio.file.FileSystem;
|
import java.nio.file.FileSystem;
|
||||||
|
@ -17,6 +16,7 @@ import java.util.logging.Level;
|
||||||
|
|
||||||
import static java.util.Objects.requireNonNull;
|
import static java.util.Objects.requireNonNull;
|
||||||
|
|
||||||
|
import com.tungsten.fclcore.util.Hex;
|
||||||
import com.tungsten.fclcore.util.Logging;
|
import com.tungsten.fclcore.util.Logging;
|
||||||
import com.tungsten.fclcore.util.io.ChecksumMismatchException;
|
import com.tungsten.fclcore.util.io.ChecksumMismatchException;
|
||||||
import com.tungsten.fclcore.util.io.CompressingUtils;
|
import com.tungsten.fclcore.util.io.CompressingUtils;
|
||||||
|
@ -54,7 +54,7 @@ public class FileDownloadTask extends FetchTask<Void> {
|
||||||
}
|
}
|
||||||
|
|
||||||
public void performCheck(MessageDigest digest) throws ChecksumMismatchException {
|
public void performCheck(MessageDigest digest) throws ChecksumMismatchException {
|
||||||
String actualChecksum = String.format("%1$040x", new BigInteger(1, digest.digest()));
|
String actualChecksum = Hex.encodeHex(digest.digest());
|
||||||
if (!checksum.equalsIgnoreCase(actualChecksum)) {
|
if (!checksum.equalsIgnoreCase(actualChecksum)) {
|
||||||
throw new ChecksumMismatchException(algorithm, checksum, actualChecksum);
|
throw new ChecksumMismatchException(algorithm, checksum, actualChecksum);
|
||||||
}
|
}
|
||||||
|
@ -248,7 +248,7 @@ public class FileDownloadTask extends FetchTask<Void> {
|
||||||
}
|
}
|
||||||
|
|
||||||
public static final IntegrityCheckHandler ZIP_INTEGRITY_CHECK_HANDLER = (filePath, destinationPath) -> {
|
public static final IntegrityCheckHandler ZIP_INTEGRITY_CHECK_HANDLER = (filePath, destinationPath) -> {
|
||||||
String ext = FileUtils.getExtension(destinationPath).toLowerCase();
|
String ext = FileUtils.getExtension(destinationPath).toLowerCase(Locale.ROOT);
|
||||||
if (ext.equals("zip") || ext.equals("jar")) {
|
if (ext.equals("zip") || ext.equals("jar")) {
|
||||||
try (FileSystem ignored = CompressingUtils.createReadOnlyZipFileSystem(filePath)) {
|
try (FileSystem ignored = CompressingUtils.createReadOnlyZipFileSystem(filePath)) {
|
||||||
// test for zip format
|
// test for zip format
|
||||||
|
|
|
@ -2,7 +2,6 @@ package com.tungsten.fclcore.task;
|
||||||
|
|
||||||
import static com.tungsten.fclcore.util.Lang.threadPool;
|
import static com.tungsten.fclcore.util.Lang.threadPool;
|
||||||
|
|
||||||
import android.app.Activity;
|
|
||||||
import android.os.Handler;
|
import android.os.Handler;
|
||||||
import android.os.Looper;
|
import android.os.Looper;
|
||||||
|
|
||||||
|
|
|
@ -690,7 +690,7 @@ public abstract class Task<T> {
|
||||||
@Override
|
@Override
|
||||||
public void execute() throws Exception {
|
public void execute() throws Exception {
|
||||||
if (isDependentsSucceeded() != (Task.this.getException() == null))
|
if (isDependentsSucceeded() != (Task.this.getException() == null))
|
||||||
throw new AssertionError("When whenComplete succeeded, Task.exception must be null.");
|
throw new AssertionError("When whenComplete succeeded, Task.exception must be null.", Task.this.getException());
|
||||||
|
|
||||||
action.execute(Task.this.getException());
|
action.execute(Task.this.getException());
|
||||||
|
|
||||||
|
|
|
@ -12,9 +12,9 @@ import com.tungsten.fclcore.util.io.IOUtils;
|
||||||
|
|
||||||
import java.io.FileNotFoundException;
|
import java.io.FileNotFoundException;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.io.OutputStream;
|
|
||||||
import java.io.RandomAccessFile;
|
import java.io.RandomAccessFile;
|
||||||
import java.net.URLConnection;
|
import java.net.URLConnection;
|
||||||
|
import java.nio.ByteBuffer;
|
||||||
import java.nio.channels.Channels;
|
import java.nio.channels.Channels;
|
||||||
import java.nio.channels.FileChannel;
|
import java.nio.channels.FileChannel;
|
||||||
import java.nio.channels.FileLock;
|
import java.nio.channels.FileLock;
|
||||||
|
@ -38,7 +38,7 @@ public class CacheRepository {
|
||||||
private Path cacheDirectory;
|
private Path cacheDirectory;
|
||||||
private Path indexFile;
|
private Path indexFile;
|
||||||
private Map<String, ETagItem> index;
|
private Map<String, ETagItem> index;
|
||||||
private Map<String, Storage> storages = new HashMap<>();
|
private final Map<String, Storage> storages = new HashMap<>();
|
||||||
private final ReadWriteLock lock = new ReentrantReadWriteLock();
|
private final ReadWriteLock lock = new ReentrantReadWriteLock();
|
||||||
|
|
||||||
public void changeDirectory(Path commonDir) {
|
public void changeDirectory(Path commonDir) {
|
||||||
|
@ -95,7 +95,7 @@ public class CacheRepository {
|
||||||
Path file = getFile(algorithm, hash);
|
Path file = getFile(algorithm, hash);
|
||||||
if (Files.exists(file)) {
|
if (Files.exists(file)) {
|
||||||
try {
|
try {
|
||||||
return Hex.encodeHex(DigestUtils.digest(algorithm, file)).equalsIgnoreCase(hash);
|
return DigestUtils.digestToString(algorithm, file).equalsIgnoreCase(hash);
|
||||||
} catch (IOException e) {
|
} catch (IOException e) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
@ -123,7 +123,7 @@ public class CacheRepository {
|
||||||
if (original != null && Files.exists(original)) {
|
if (original != null && Files.exists(original)) {
|
||||||
if (hash != null) {
|
if (hash != null) {
|
||||||
try {
|
try {
|
||||||
String checksum = Hex.encodeHex(DigestUtils.digest(algorithm, original));
|
String checksum = DigestUtils.digestToString(algorithm, original);
|
||||||
if (checksum.equalsIgnoreCase(hash))
|
if (checksum.equalsIgnoreCase(hash))
|
||||||
return Optional.of(restore(original, () -> cacheFile(original, algorithm, hash)));
|
return Optional.of(restore(original, () -> cacheFile(original, algorithm, hash)));
|
||||||
} catch (IOException e) {
|
} catch (IOException e) {
|
||||||
|
@ -157,7 +157,7 @@ public class CacheRepository {
|
||||||
if (StringUtils.isBlank(eTagItem.hash) || !fileExists(SHA1, eTagItem.hash)) throw new FileNotFoundException();
|
if (StringUtils.isBlank(eTagItem.hash) || !fileExists(SHA1, eTagItem.hash)) throw new FileNotFoundException();
|
||||||
Path file = getFile(SHA1, eTagItem.hash);
|
Path file = getFile(SHA1, eTagItem.hash);
|
||||||
if (Files.getLastModifiedTime(file).toMillis() != eTagItem.localLastModified) {
|
if (Files.getLastModifiedTime(file).toMillis() != eTagItem.localLastModified) {
|
||||||
String hash = Hex.encodeHex(DigestUtils.digest(SHA1, file));
|
String hash = DigestUtils.digestToString(SHA1, file);
|
||||||
if (!Objects.equals(hash, eTagItem.hash))
|
if (!Objects.equals(hash, eTagItem.hash))
|
||||||
throw new IOException("This file is modified");
|
throw new IOException("This file is modified");
|
||||||
}
|
}
|
||||||
|
@ -192,7 +192,7 @@ public class CacheRepository {
|
||||||
|
|
||||||
public void cacheRemoteFile(Path downloaded, URLConnection conn) throws IOException {
|
public void cacheRemoteFile(Path downloaded, URLConnection conn) throws IOException {
|
||||||
cacheData(() -> {
|
cacheData(() -> {
|
||||||
String hash = Hex.encodeHex(DigestUtils.digest(SHA1, downloaded));
|
String hash = DigestUtils.digestToString(SHA1, downloaded);
|
||||||
Path cached = cacheFile(downloaded, SHA1, hash);
|
Path cached = cacheFile(downloaded, SHA1, hash);
|
||||||
return new CacheResult(hash, cached);
|
return new CacheResult(hash, cached);
|
||||||
}, conn);
|
}, conn);
|
||||||
|
@ -204,7 +204,7 @@ public class CacheRepository {
|
||||||
|
|
||||||
public void cacheBytes(byte[] bytes, URLConnection conn) throws IOException {
|
public void cacheBytes(byte[] bytes, URLConnection conn) throws IOException {
|
||||||
cacheData(() -> {
|
cacheData(() -> {
|
||||||
String hash = Hex.encodeHex(DigestUtils.digest(SHA1, bytes));
|
String hash = DigestUtils.digestToString(SHA1, bytes);
|
||||||
Path cached = getFile(SHA1, hash);
|
Path cached = getFile(SHA1, hash);
|
||||||
FileUtils.writeBytes(cached.toFile(), bytes);
|
FileUtils.writeBytes(cached.toFile(), bytes);
|
||||||
return new CacheResult(hash, cached);
|
return new CacheResult(hash, cached);
|
||||||
|
@ -277,9 +277,8 @@ public class CacheRepository {
|
||||||
ETagIndex indexOnDisk = JsonUtils.fromMaybeMalformedJson(new String(IOUtils.readFullyWithoutClosing(Channels.newInputStream(channel)), UTF_8), ETagIndex.class);
|
ETagIndex indexOnDisk = JsonUtils.fromMaybeMalformedJson(new String(IOUtils.readFullyWithoutClosing(Channels.newInputStream(channel)), UTF_8), ETagIndex.class);
|
||||||
Map<String, ETagItem> newIndex = joinETagIndexes(indexOnDisk == null ? null : indexOnDisk.eTag, index.values());
|
Map<String, ETagItem> newIndex = joinETagIndexes(indexOnDisk == null ? null : indexOnDisk.eTag, index.values());
|
||||||
channel.truncate(0);
|
channel.truncate(0);
|
||||||
OutputStream os = Channels.newOutputStream(channel);
|
|
||||||
ETagIndex writeTo = new ETagIndex(newIndex.values());
|
ETagIndex writeTo = new ETagIndex(newIndex.values());
|
||||||
IOUtils.write(JsonUtils.GSON.toJson(writeTo).getBytes(UTF_8), os);
|
channel.write(ByteBuffer.wrap(JsonUtils.GSON.toJson(writeTo).getBytes(UTF_8)));
|
||||||
this.index = newIndex;
|
this.index = newIndex;
|
||||||
} finally {
|
} finally {
|
||||||
lock.release();
|
lock.release();
|
||||||
|
@ -287,7 +286,7 @@ public class CacheRepository {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private class ETagIndex {
|
private static final class ETagIndex {
|
||||||
private final Collection<ETagItem> eTag;
|
private final Collection<ETagItem> eTag;
|
||||||
|
|
||||||
public ETagIndex() {
|
public ETagIndex() {
|
||||||
|
@ -299,7 +298,7 @@ public class CacheRepository {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private class ETagItem {
|
private static final class ETagItem {
|
||||||
private final String url;
|
private final String url;
|
||||||
private final String eTag;
|
private final String eTag;
|
||||||
private final String hash;
|
private final String hash;
|
||||||
|
@ -413,8 +412,7 @@ public class CacheRepository {
|
||||||
if (indexOnDisk == null) indexOnDisk = new HashMap<>();
|
if (indexOnDisk == null) indexOnDisk = new HashMap<>();
|
||||||
indexOnDisk.putAll(storage);
|
indexOnDisk.putAll(storage);
|
||||||
channel.truncate(0);
|
channel.truncate(0);
|
||||||
OutputStream os = Channels.newOutputStream(channel);
|
channel.write(ByteBuffer.wrap(JsonUtils.GSON.toJson(storage).getBytes(UTF_8)));
|
||||||
IOUtils.write(JsonUtils.GSON.toJson(storage).getBytes(UTF_8), os);
|
|
||||||
this.storage = indexOnDisk;
|
this.storage = indexOnDisk;
|
||||||
} finally {
|
} finally {
|
||||||
lock.release();
|
lock.release();
|
||||||
|
|
|
@ -24,10 +24,6 @@ public final class DigestUtils {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public static byte[] digest(String algorithm, String data) {
|
|
||||||
return digest(algorithm, data.getBytes(UTF_8));
|
|
||||||
}
|
|
||||||
|
|
||||||
public static byte[] digest(String algorithm, byte[] data) {
|
public static byte[] digest(String algorithm, byte[] data) {
|
||||||
return getDigest(algorithm).digest(data);
|
return getDigest(algorithm).digest(data);
|
||||||
}
|
}
|
||||||
|
@ -46,8 +42,22 @@ public final class DigestUtils {
|
||||||
return updateDigest(digest, data).digest();
|
return updateDigest(digest, data).digest();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static String digestToString(String algorithm, byte[] data) throws IOException {
|
||||||
|
return Hex.encodeHex(digest(algorithm, data));
|
||||||
|
}
|
||||||
|
|
||||||
|
public static String digestToString(String algorithm, Path path) throws IOException {
|
||||||
|
return Hex.encodeHex(digest(algorithm, path));
|
||||||
|
}
|
||||||
|
|
||||||
|
public static String digestToString(String algorithm, InputStream data) throws IOException {
|
||||||
|
return Hex.encodeHex(digest(algorithm, data));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static final ThreadLocal<byte[]> threadLocalBuffer = ThreadLocal.withInitial(() -> new byte[STREAM_BUFFER_LENGTH]);
|
||||||
|
|
||||||
public static MessageDigest updateDigest(MessageDigest digest, InputStream data) throws IOException {
|
public static MessageDigest updateDigest(MessageDigest digest, InputStream data) throws IOException {
|
||||||
byte[] buffer = new byte[STREAM_BUFFER_LENGTH];
|
byte[] buffer = threadLocalBuffer.get();
|
||||||
int read = data.read(buffer, 0, STREAM_BUFFER_LENGTH);
|
int read = data.read(buffer, 0, STREAM_BUFFER_LENGTH);
|
||||||
|
|
||||||
while (read > -1) {
|
while (read > -1) {
|
||||||
|
|
|
@ -0,0 +1,28 @@
|
||||||
|
package com.tungsten.fclcore.util;
|
||||||
|
|
||||||
|
import java.util.Objects;
|
||||||
|
|
||||||
|
public final class Holder<T> {
|
||||||
|
public T value;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int hashCode() {
|
||||||
|
return Objects.hashCode(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean equals(Object obj) {
|
||||||
|
if (this == obj)
|
||||||
|
return true;
|
||||||
|
|
||||||
|
if (!(obj instanceof Holder))
|
||||||
|
return false;
|
||||||
|
|
||||||
|
return Objects.equals(this.value, ((Holder<?>) obj).value);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String toString() {
|
||||||
|
return "Holder[" + value + "]";
|
||||||
|
}
|
||||||
|
}
|
|
@ -17,6 +17,18 @@ public final class Lang {
|
||||||
private Lang() {
|
private Lang() {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static <T> T requireNonNullElse(T value, T defaultValue) {
|
||||||
|
return value != null ? value : defaultValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static <T> T requireNonNullElseGet(T value, Supplier<? extends T> defaultValue) {
|
||||||
|
return value != null ? value : defaultValue.get();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static <T, U> U requireNonNullElseGet(T value, Function<? super T, ? extends U> mapper, Supplier<? extends U> defaultValue) {
|
||||||
|
return value != null ? mapper.apply(value) : defaultValue.get();
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Construct a mutable map by given key-value pairs.
|
* Construct a mutable map by given key-value pairs.
|
||||||
* @param pairs entries in the new map
|
* @param pairs entries in the new map
|
||||||
|
@ -349,6 +361,13 @@ public final class Lang {
|
||||||
return () -> iterator;
|
return () -> iterator;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static <T, U> void forEachZipped(Iterable<T> i1, Iterable<U> i2, BiConsumer<T, U> action) {
|
||||||
|
Iterator<T> it1 = i1.iterator();
|
||||||
|
Iterator<U> it2 = i2.iterator();
|
||||||
|
while (it1.hasNext() && it2.hasNext())
|
||||||
|
action.accept(it1.next(), it2.next());
|
||||||
|
}
|
||||||
|
|
||||||
private static Timer timer;
|
private static Timer timer;
|
||||||
|
|
||||||
public static synchronized Timer getTimer() {
|
public static synchronized Timer getTimer() {
|
||||||
|
|
|
@ -7,7 +7,10 @@ import java.io.ByteArrayOutputStream;
|
||||||
import java.io.File;
|
import java.io.File;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.io.UnsupportedEncodingException;
|
import java.io.UnsupportedEncodingException;
|
||||||
|
import java.nio.file.Files;
|
||||||
|
import java.nio.file.Path;
|
||||||
import java.text.MessageFormat;
|
import java.text.MessageFormat;
|
||||||
|
import java.util.Arrays;
|
||||||
import java.util.Date;
|
import java.util.Date;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.concurrent.ConcurrentHashMap;
|
import java.util.concurrent.ConcurrentHashMap;
|
||||||
|
@ -21,24 +24,24 @@ public final class Logging {
|
||||||
public static final Logger LOG = Logger.getLogger("FCL");
|
public static final Logger LOG = Logger.getLogger("FCL");
|
||||||
private static final ByteArrayOutputStream storedLogs = new ByteArrayOutputStream(IOUtils.DEFAULT_BUFFER_SIZE);
|
private static final ByteArrayOutputStream storedLogs = new ByteArrayOutputStream(IOUtils.DEFAULT_BUFFER_SIZE);
|
||||||
|
|
||||||
private static final ConcurrentMap<String, String> forbiddenTokens = new ConcurrentHashMap<>();
|
private static volatile String[] accessTokens = new String[0];
|
||||||
|
|
||||||
public static void registerForbiddenToken(String token, String replacement) {
|
public static synchronized void registerAccessToken(String token) {
|
||||||
forbiddenTokens.put(token, replacement);
|
final String[] oldAccessTokens = accessTokens;
|
||||||
}
|
final String[] newAccessTokens = Arrays.copyOf(oldAccessTokens, oldAccessTokens.length + 1);
|
||||||
|
|
||||||
public static void registerAccessToken(String accessToken) {
|
newAccessTokens[oldAccessTokens.length] = token;
|
||||||
registerForbiddenToken(accessToken, "<access token>");
|
|
||||||
|
accessTokens = newAccessTokens;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static String filterForbiddenToken(String message) {
|
public static String filterForbiddenToken(String message) {
|
||||||
for (Map.Entry<String, String> entry : forbiddenTokens.entrySet()) {
|
for (String token : accessTokens)
|
||||||
message = message.replace(entry.getKey(), entry.getValue());
|
message = message.replace(token, "<access token>");
|
||||||
}
|
|
||||||
return message;
|
return message;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static void start(File logFolder) {
|
public static void start(Path logFolder) {
|
||||||
LOG.setLevel(Level.ALL);
|
LOG.setLevel(Level.ALL);
|
||||||
LOG.setUseParentHandlers(false);
|
LOG.setUseParentHandlers(false);
|
||||||
LOG.setFilter(record -> {
|
LOG.setFilter(record -> {
|
||||||
|
@ -47,14 +50,17 @@ public final class Logging {
|
||||||
});
|
});
|
||||||
|
|
||||||
try {
|
try {
|
||||||
FileUtils.makeDirectory(logFolder);
|
if (Files.isRegularFile(logFolder))
|
||||||
FileHandler fileHandler = new FileHandler(logFolder + "/fcl.log");
|
Files.delete(logFolder);
|
||||||
|
|
||||||
|
Files.createDirectories(logFolder);
|
||||||
|
FileHandler fileHandler = new FileHandler(logFolder.resolve("fcl.log").toAbsolutePath().toString());
|
||||||
fileHandler.setLevel(Level.FINEST);
|
fileHandler.setLevel(Level.FINEST);
|
||||||
fileHandler.setFormatter(DefaultFormatter.INSTANCE);
|
fileHandler.setFormatter(DefaultFormatter.INSTANCE);
|
||||||
fileHandler.setEncoding("UTF-8");
|
fileHandler.setEncoding("UTF-8");
|
||||||
LOG.addHandler(fileHandler);
|
LOG.addHandler(fileHandler);
|
||||||
} catch (IOException e) {
|
} catch (IOException e) {
|
||||||
System.err.println("Unable to create fcl.log, " + e.getMessage());
|
System.err.println("Unable to create fcl.log\n" + StringUtils.getStackTrace(e));
|
||||||
}
|
}
|
||||||
|
|
||||||
ConsoleHandler consoleHandler = new ConsoleHandler();
|
ConsoleHandler consoleHandler = new ConsoleHandler();
|
||||||
|
@ -96,7 +102,7 @@ public final class Logging {
|
||||||
try {
|
try {
|
||||||
return storedLogs.toString("UTF-8");
|
return storedLogs.toString("UTF-8");
|
||||||
} catch (UnsupportedEncodingException e) {
|
} catch (UnsupportedEncodingException e) {
|
||||||
return e.getMessage();
|
throw new InternalError(e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -119,4 +125,4 @@ public final class Logging {
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,28 +1,11 @@
|
||||||
package com.tungsten.fclcore.util;
|
package com.tungsten.fclcore.util;
|
||||||
|
|
||||||
import java.lang.reflect.AccessibleObject;
|
|
||||||
import java.lang.reflect.InvocationTargetException;
|
|
||||||
import java.lang.reflect.Method;
|
|
||||||
import java.util.function.Predicate;
|
import java.util.function.Predicate;
|
||||||
|
|
||||||
public final class ReflectionHelper {
|
public final class ReflectionHelper {
|
||||||
private ReflectionHelper() {
|
private ReflectionHelper() {
|
||||||
}
|
}
|
||||||
|
|
||||||
private static Method accessible0;
|
|
||||||
|
|
||||||
static {
|
|
||||||
try {
|
|
||||||
accessible0 = AccessibleObject.class.getDeclaredMethod("setAccessible0", boolean.class);
|
|
||||||
accessible0.setAccessible(true);
|
|
||||||
} catch (Throwable ignored) {
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public static void setAccessible(AccessibleObject obj) throws InvocationTargetException, IllegalAccessException {
|
|
||||||
accessible0.invoke(obj, true);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get caller, this method is caller sensitive.
|
* Get caller, this method is caller sensitive.
|
||||||
*
|
*
|
||||||
|
|
|
@ -2,10 +2,7 @@ package com.tungsten.fclcore.util;
|
||||||
|
|
||||||
import java.io.PrintWriter;
|
import java.io.PrintWriter;
|
||||||
import java.io.StringWriter;
|
import java.io.StringWriter;
|
||||||
import java.nio.charset.CharsetEncoder;
|
|
||||||
import java.nio.charset.StandardCharsets;
|
|
||||||
import java.util.*;
|
import java.util.*;
|
||||||
import java.util.regex.Pattern;
|
|
||||||
|
|
||||||
public final class StringUtils {
|
public final class StringUtils {
|
||||||
|
|
||||||
|
@ -129,6 +126,10 @@ public final class StringUtils {
|
||||||
return str + suffix;
|
return str + suffix;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static String removePrefix(String str, String prefix) {
|
||||||
|
return str.startsWith(prefix) ? str.substring(prefix.length()) : str;
|
||||||
|
}
|
||||||
|
|
||||||
public static String removePrefix(String str, String... prefixes) {
|
public static String removePrefix(String str, String... prefixes) {
|
||||||
for (String prefix : prefixes)
|
for (String prefix : prefixes)
|
||||||
if (str.startsWith(prefix))
|
if (str.startsWith(prefix))
|
||||||
|
@ -136,6 +137,10 @@ public final class StringUtils {
|
||||||
return str;
|
return str;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static String removeSuffix(String str, String suffix) {
|
||||||
|
return str.endsWith(suffix) ? str.substring(0, str.length() - suffix.length()) : str;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Remove one suffix of the suffixes of the string.
|
* Remove one suffix of the suffixes of the string.
|
||||||
*/
|
*/
|
||||||
|
@ -147,24 +152,29 @@ public final class StringUtils {
|
||||||
}
|
}
|
||||||
|
|
||||||
public static boolean containsOne(Collection<String> patterns, String... targets) {
|
public static boolean containsOne(Collection<String> patterns, String... targets) {
|
||||||
for (String pattern : patterns)
|
for (String pattern : patterns) {
|
||||||
|
String lowerPattern = pattern.toLowerCase(Locale.ROOT);
|
||||||
for (String target : targets)
|
for (String target : targets)
|
||||||
if (pattern.toLowerCase().contains(target.toLowerCase()))
|
if (lowerPattern.contains(target.toLowerCase(Locale.ROOT)))
|
||||||
return true;
|
return true;
|
||||||
|
}
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static boolean containsOne(String pattern, String... targets) {
|
public static boolean containsOne(String pattern, String... targets) {
|
||||||
|
String lowerPattern = pattern.toLowerCase(Locale.ROOT);
|
||||||
for (String target : targets)
|
for (String target : targets)
|
||||||
if (pattern.toLowerCase().contains(target.toLowerCase()))
|
if (lowerPattern.contains(target.toLowerCase(Locale.ROOT)))
|
||||||
return true;
|
return true;
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static boolean containsOne(String pattern, char... targets) {
|
public static boolean containsChinese(String str) {
|
||||||
for (char target : targets)
|
for (int i = 0; i < str.length(); i++) {
|
||||||
if (pattern.toLowerCase().indexOf(Character.toLowerCase(target)) >= 0)
|
char ch = str.charAt(i);
|
||||||
|
if (ch >= '\u4e00' && ch <= '\u9fa5')
|
||||||
return true;
|
return true;
|
||||||
|
}
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -187,7 +197,10 @@ public final class StringUtils {
|
||||||
}
|
}
|
||||||
|
|
||||||
public static String parseColorEscapes(String original) {
|
public static String parseColorEscapes(String original) {
|
||||||
return original.replaceAll("\u00A7\\d", "");
|
if (original.indexOf('\u00A7') < 0)
|
||||||
|
return original;
|
||||||
|
|
||||||
|
return original.replaceAll("\u00A7[0-9a-gklmnor]", "");
|
||||||
}
|
}
|
||||||
|
|
||||||
public static String parseEscapeSequence(String str) {
|
public static String parseEscapeSequence(String str) {
|
||||||
|
@ -216,8 +229,32 @@ public final class StringUtils {
|
||||||
return result.toString();
|
return result.toString();
|
||||||
}
|
}
|
||||||
|
|
||||||
public static boolean isASCII(CharSequence cs) {
|
public static int MAX_SHORT_STRING_LENGTH = 77;
|
||||||
return US_ASCII_ENCODER.canEncode(cs);
|
|
||||||
|
public static Optional<String> truncate(String str) {
|
||||||
|
if (str.length() <= MAX_SHORT_STRING_LENGTH) {
|
||||||
|
return Optional.empty();
|
||||||
|
}
|
||||||
|
|
||||||
|
final int halfLength = (MAX_SHORT_STRING_LENGTH - 5) / 2;
|
||||||
|
return Optional.of(str.substring(0, halfLength) + " ... " + str.substring(str.length() - halfLength));
|
||||||
|
}
|
||||||
|
|
||||||
|
public static boolean isASCII(String cs) {
|
||||||
|
for (int i = 0; i < cs.length(); i++)
|
||||||
|
if (cs.charAt(i) >= 128)
|
||||||
|
return false;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static boolean isAlphabeticOrNumber(String str) {
|
||||||
|
int length = str.length();
|
||||||
|
for (int i = 0; i < length; i++) {
|
||||||
|
char ch = str.charAt(i);
|
||||||
|
if (!(ch >= '0' && ch <= '9') && !(ch >= 'a' && ch <= 'z') && !(ch >= 'A' && ch <= 'Z'))
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -254,8 +291,4 @@ public final class StringUtils {
|
||||||
return f[a.length()][b.length()];
|
return f[a.length()][b.length()];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public static final Pattern CHINESE_PATTERN = Pattern.compile("[\\u4e00-\\u9fa5]");
|
|
||||||
|
|
||||||
public static final CharsetEncoder US_ASCII_ENCODER = StandardCharsets.US_ASCII.newEncoder();
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -6,7 +6,11 @@ import com.google.gson.JsonParseException;
|
||||||
import com.google.gson.JsonSyntaxException;
|
import com.google.gson.JsonSyntaxException;
|
||||||
|
|
||||||
import java.io.File;
|
import java.io.File;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.InputStream;
|
||||||
|
import java.io.InputStreamReader;
|
||||||
import java.lang.reflect.Type;
|
import java.lang.reflect.Type;
|
||||||
|
import java.nio.charset.StandardCharsets;
|
||||||
import java.time.Instant;
|
import java.time.Instant;
|
||||||
import java.util.Date;
|
import java.util.Date;
|
||||||
import java.util.UUID;
|
import java.util.UUID;
|
||||||
|
@ -24,6 +28,18 @@ public final class JsonUtils {
|
||||||
private JsonUtils() {
|
private JsonUtils() {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static <T> T fromJsonFully(InputStream json, Class<T> classOfT) throws IOException, JsonParseException {
|
||||||
|
try (InputStreamReader reader = new InputStreamReader(json, StandardCharsets.UTF_8)) {
|
||||||
|
return GSON.fromJson(reader, classOfT);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static <T> T fromJsonFully(InputStream json, Type type) throws IOException, JsonParseException {
|
||||||
|
try (InputStreamReader reader = new InputStreamReader(json, StandardCharsets.UTF_8)) {
|
||||||
|
return GSON.fromJson(reader, type);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public static <T> T fromNonNullJson(String json, Class<T> classOfT) throws JsonParseException {
|
public static <T> T fromNonNullJson(String json, Class<T> classOfT) throws JsonParseException {
|
||||||
T parsed = GSON.fromJson(json, classOfT);
|
T parsed = GSON.fromJson(json, classOfT);
|
||||||
if (parsed == null)
|
if (parsed == null)
|
||||||
|
@ -38,6 +54,24 @@ public final class JsonUtils {
|
||||||
return parsed;
|
return parsed;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static <T> T fromNonNullJsonFully(InputStream json, Class<T> classOfT) throws IOException, JsonParseException {
|
||||||
|
try (InputStreamReader reader = new InputStreamReader(json, StandardCharsets.UTF_8)) {
|
||||||
|
T parsed = GSON.fromJson(reader, classOfT);
|
||||||
|
if (parsed == null)
|
||||||
|
throw new JsonParseException("Json object cannot be null.");
|
||||||
|
return parsed;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static <T> T fromNonNullJsonFully(InputStream json, Type type) throws IOException, JsonParseException {
|
||||||
|
try (InputStreamReader reader = new InputStreamReader(json, StandardCharsets.UTF_8)) {
|
||||||
|
T parsed = GSON.fromJson(reader, type);
|
||||||
|
if (parsed == null)
|
||||||
|
throw new JsonParseException("Json object cannot be null.");
|
||||||
|
return parsed;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public static <T> T fromMaybeMalformedJson(String json, Class<T> classOfT) throws JsonParseException {
|
public static <T> T fromMaybeMalformedJson(String json, Class<T> classOfT) throws JsonParseException {
|
||||||
try {
|
try {
|
||||||
return GSON.fromJson(json, classOfT);
|
return GSON.fromJson(json, classOfT);
|
||||||
|
|
|
@ -42,12 +42,12 @@ public final class LowerCaseEnumTypeAdapterFactory implements TypeAdapterFactory
|
||||||
reader.nextNull();
|
reader.nextNull();
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
return lowercaseToConstant.get(reader.nextString().toLowerCase());
|
return lowercaseToConstant.get(reader.nextString().toLowerCase(Locale.ROOT));
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
private static String toLowercase(Object o) {
|
private static String toLowercase(Object o) {
|
||||||
return o.toString().toLowerCase(Locale.US);
|
return o.toString().toLowerCase(Locale.ROOT);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -7,6 +7,7 @@ import com.google.gson.stream.JsonWriter;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.util.UUID;
|
import java.util.UUID;
|
||||||
|
import java.util.regex.Pattern;
|
||||||
|
|
||||||
public final class UUIDTypeAdapter extends TypeAdapter<UUID> {
|
public final class UUIDTypeAdapter extends TypeAdapter<UUID> {
|
||||||
|
|
||||||
|
@ -30,8 +31,10 @@ public final class UUIDTypeAdapter extends TypeAdapter<UUID> {
|
||||||
return value.toString().replace("-", "");
|
return value.toString().replace("-", "");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static final Pattern regex = Pattern.compile("(\\w{8})(\\w{4})(\\w{4})(\\w{4})(\\w{12})");
|
||||||
|
|
||||||
public static UUID fromString(String input) {
|
public static UUID fromString(String input) {
|
||||||
return UUID.fromString(input.replaceFirst("(\\w{8})(\\w{4})(\\w{4})(\\w{4})(\\w{12})", "$1-$2-$3-$4-$5"));
|
return UUID.fromString(regex.matcher(input).replaceFirst("$1-$2-$3-$4-$5"));
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,7 +2,6 @@ package com.tungsten.fclcore.util.io;
|
||||||
|
|
||||||
import com.tungsten.fclcore.download.ArtifactMalformedException;
|
import com.tungsten.fclcore.download.ArtifactMalformedException;
|
||||||
import com.tungsten.fclcore.util.DigestUtils;
|
import com.tungsten.fclcore.util.DigestUtils;
|
||||||
import com.tungsten.fclcore.util.Hex;
|
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.nio.file.Path;
|
import java.nio.file.Path;
|
||||||
|
@ -33,7 +32,7 @@ public class ChecksumMismatchException extends ArtifactMalformedException {
|
||||||
}
|
}
|
||||||
|
|
||||||
public static void verifyChecksum(Path file, String algorithm, String expectedChecksum) throws IOException {
|
public static void verifyChecksum(Path file, String algorithm, String expectedChecksum) throws IOException {
|
||||||
String checksum = Hex.encodeHex(DigestUtils.digest(algorithm, file));
|
String checksum = DigestUtils.digestToString(algorithm, file);
|
||||||
if (!checksum.equalsIgnoreCase(expectedChecksum)) {
|
if (!checksum.equalsIgnoreCase(expectedChecksum)) {
|
||||||
throw new ChecksumMismatchException(algorithm, expectedChecksum, checksum);
|
throw new ChecksumMismatchException(algorithm, expectedChecksum, checksum);
|
||||||
}
|
}
|
||||||
|
|
|
@ -10,10 +10,11 @@ import java.net.HttpURLConnection;
|
||||||
import static java.nio.charset.StandardCharsets.UTF_8;
|
import static java.nio.charset.StandardCharsets.UTF_8;
|
||||||
|
|
||||||
public class HttpMultipartRequest implements Closeable {
|
public class HttpMultipartRequest implements Closeable {
|
||||||
|
private static final String endl = "\r\n";
|
||||||
|
|
||||||
private final String boundary = "*****" + System.currentTimeMillis() + "*****";
|
private final String boundary = "*****" + System.currentTimeMillis() + "*****";
|
||||||
private final HttpURLConnection urlConnection;
|
private final HttpURLConnection urlConnection;
|
||||||
private final ByteArrayOutputStream stream;
|
private final ByteArrayOutputStream stream;
|
||||||
private final String endl = "\r\n";
|
|
||||||
|
|
||||||
public HttpMultipartRequest(HttpURLConnection urlConnection) throws IOException {
|
public HttpMultipartRequest(HttpURLConnection urlConnection) throws IOException {
|
||||||
this.urlConnection = urlConnection;
|
this.urlConnection = urlConnection;
|
||||||
|
@ -52,7 +53,7 @@ public class HttpMultipartRequest implements Closeable {
|
||||||
addLine("--" + boundary + "--");
|
addLine("--" + boundary + "--");
|
||||||
urlConnection.setRequestProperty("Content-Length", "" + stream.size());
|
urlConnection.setRequestProperty("Content-Length", "" + stream.size());
|
||||||
try (OutputStream os = urlConnection.getOutputStream()) {
|
try (OutputStream os = urlConnection.getOutputStream()) {
|
||||||
IOUtils.write(stream.toByteArray(), os);
|
stream.writeTo(os);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -20,7 +20,6 @@ import java.net.HttpURLConnection;
|
||||||
import java.net.MalformedURLException;
|
import java.net.MalformedURLException;
|
||||||
import java.net.SocketTimeoutException;
|
import java.net.SocketTimeoutException;
|
||||||
import java.net.URL;
|
import java.net.URL;
|
||||||
import java.nio.charset.StandardCharsets;
|
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
import java.util.HashSet;
|
import java.util.HashSet;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
@ -127,7 +126,7 @@ public abstract class HttpRequest {
|
||||||
return getStringWithRetry(() -> {
|
return getStringWithRetry(() -> {
|
||||||
HttpURLConnection con = createConnection();
|
HttpURLConnection con = createConnection();
|
||||||
con = resolveConnection(con);
|
con = resolveConnection(con);
|
||||||
return IOUtils.readFullyAsString(con.getInputStream(), StandardCharsets.UTF_8);
|
return IOUtils.readFullyAsString(con.getInputStream());
|
||||||
}, retryTimes);
|
}, retryTimes);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,7 +1,6 @@
|
||||||
package com.tungsten.fclcore.util.io;
|
package com.tungsten.fclcore.util.io;
|
||||||
|
|
||||||
import java.io.*;
|
import java.io.*;
|
||||||
import java.nio.charset.Charset;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This utility class consists of some util methods operating on InputStream/OutputStream.
|
* This utility class consists of some util methods operating on InputStream/OutputStream.
|
||||||
|
@ -21,7 +20,7 @@ public final class IOUtils {
|
||||||
* @throws IOException if an I/O error occurs.
|
* @throws IOException if an I/O error occurs.
|
||||||
*/
|
*/
|
||||||
public static byte[] readFullyWithoutClosing(InputStream stream) throws IOException {
|
public static byte[] readFullyWithoutClosing(InputStream stream) throws IOException {
|
||||||
ByteArrayOutputStream result = new ByteArrayOutputStream();
|
ByteArrayOutputStream result = new ByteArrayOutputStream(Math.max(stream.available(), 32));
|
||||||
copyTo(stream, result);
|
copyTo(stream, result);
|
||||||
return result.toByteArray();
|
return result.toByteArray();
|
||||||
}
|
}
|
||||||
|
@ -35,7 +34,7 @@ public final class IOUtils {
|
||||||
*/
|
*/
|
||||||
public static ByteArrayOutputStream readFully(InputStream stream) throws IOException {
|
public static ByteArrayOutputStream readFully(InputStream stream) throws IOException {
|
||||||
try (InputStream is = stream) {
|
try (InputStream is = stream) {
|
||||||
ByteArrayOutputStream result = new ByteArrayOutputStream();
|
ByteArrayOutputStream result = new ByteArrayOutputStream(Math.max(is.available(), 32));
|
||||||
copyTo(is, result);
|
copyTo(is, result);
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
@ -49,18 +48,6 @@ public final class IOUtils {
|
||||||
return readFully(stream).toString("UTF-8");
|
return readFully(stream).toString("UTF-8");
|
||||||
}
|
}
|
||||||
|
|
||||||
public static String readFullyAsString(InputStream stream, Charset charset) throws IOException {
|
|
||||||
return readFully(stream).toString(charset.name());
|
|
||||||
}
|
|
||||||
|
|
||||||
public static void write(String text, OutputStream outputStream) throws IOException {
|
|
||||||
write(text.getBytes(), outputStream);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static void write(byte[] bytes, OutputStream outputStream) throws IOException {
|
|
||||||
copyTo(new ByteArrayInputStream(bytes), outputStream);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static void copyTo(InputStream src, OutputStream dest) throws IOException {
|
public static void copyTo(InputStream src, OutputStream dest) throws IOException {
|
||||||
copyTo(src, dest, new byte[DEFAULT_BUFFER_SIZE]);
|
copyTo(src, dest, new byte[DEFAULT_BUFFER_SIZE]);
|
||||||
}
|
}
|
||||||
|
|
|
@ -6,9 +6,7 @@ import java.nio.file.FileSystemNotFoundException;
|
||||||
import java.nio.file.Files;
|
import java.nio.file.Files;
|
||||||
import java.nio.file.Path;
|
import java.nio.file.Path;
|
||||||
import java.nio.file.Paths;
|
import java.nio.file.Paths;
|
||||||
import java.security.CodeSource;
|
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
import java.util.jar.Attributes;
|
|
||||||
import java.util.jar.JarFile;
|
import java.util.jar.JarFile;
|
||||||
import java.util.jar.Manifest;
|
import java.util.jar.Manifest;
|
||||||
|
|
||||||
|
@ -17,22 +15,33 @@ public final class JarUtils {
|
||||||
}
|
}
|
||||||
|
|
||||||
@SuppressWarnings("OptionalUsedAsFieldOrParameterType")
|
@SuppressWarnings("OptionalUsedAsFieldOrParameterType")
|
||||||
private static final Optional<Path> THIS_JAR =
|
private static final Optional<Path> THIS_JAR;
|
||||||
Optional.ofNullable(JarUtils.class.getProtectionDomain().getCodeSource())
|
|
||||||
.map(CodeSource::getLocation)
|
private static final Manifest manifest;
|
||||||
.map(url -> {
|
|
||||||
try {
|
static {
|
||||||
return Paths.get(url.toURI());
|
THIS_JAR = Optional.ofNullable(JarUtils.class.getProtectionDomain().getCodeSource())
|
||||||
} catch (FileSystemNotFoundException | IllegalArgumentException | URISyntaxException e) {
|
.map(codeSource -> {
|
||||||
return null;
|
try {
|
||||||
}
|
return Paths.get(codeSource.getLocation().toURI());
|
||||||
})
|
} catch (FileSystemNotFoundException | IllegalArgumentException | URISyntaxException e) {
|
||||||
.filter(Files::isRegularFile);
|
return null;
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.filter(Files::isRegularFile);
|
||||||
|
|
||||||
|
manifest = THIS_JAR.flatMap(JarUtils::getManifest).orElseGet(Manifest::new);
|
||||||
|
}
|
||||||
|
|
||||||
public static Optional<Path> thisJar() {
|
public static Optional<Path> thisJar() {
|
||||||
return THIS_JAR;
|
return THIS_JAR;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static String getManifestAttribute(String name, String defaultValue) {
|
||||||
|
String value = manifest.getMainAttributes().getValue(name);
|
||||||
|
return value != null ? value : defaultValue;
|
||||||
|
}
|
||||||
|
|
||||||
public static Optional<Manifest> getManifest(Path jar) {
|
public static Optional<Manifest> getManifest(Path jar) {
|
||||||
try (JarFile file = new JarFile(jar.toFile())) {
|
try (JarFile file = new JarFile(jar.toFile())) {
|
||||||
return Optional.ofNullable(file.getManifest());
|
return Optional.ofNullable(file.getManifest());
|
||||||
|
@ -40,9 +49,4 @@ public final class JarUtils {
|
||||||
return Optional.empty();
|
return Optional.empty();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public static Optional<String> getImplementationVersion(Path jar) {
|
|
||||||
return Optional.of(jar).flatMap(JarUtils::getManifest)
|
|
||||||
.flatMap(manifest -> Optional.ofNullable(manifest.getMainAttributes().getValue(Attributes.Name.IMPLEMENTATION_VERSION)));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -7,7 +7,6 @@ import static com.tungsten.fclcore.util.StringUtils.substringAfterLast;
|
||||||
|
|
||||||
import java.io.*;
|
import java.io.*;
|
||||||
import java.net.*;
|
import java.net.*;
|
||||||
import java.nio.charset.StandardCharsets;
|
|
||||||
import java.util.*;
|
import java.util.*;
|
||||||
import java.util.Map.Entry;
|
import java.util.Map.Entry;
|
||||||
|
|
||||||
|
@ -116,7 +115,7 @@ public final class NetworkUtils {
|
||||||
/**
|
/**
|
||||||
* This method is a work-around that aims to solve problem when "Location" in
|
* This method is a work-around that aims to solve problem when "Location" in
|
||||||
* stupid server's response is not encoded.
|
* stupid server's response is not encoded.
|
||||||
*
|
*
|
||||||
* @see <a href="https://github.com/curl/curl/issues/473">Issue with libcurl</a>
|
* @see <a href="https://github.com/curl/curl/issues/473">Issue with libcurl</a>
|
||||||
* @param conn the stupid http connection.
|
* @param conn the stupid http connection.
|
||||||
* @return manually redirected http connection.
|
* @return manually redirected http connection.
|
||||||
|
@ -158,13 +157,13 @@ public final class NetworkUtils {
|
||||||
public static String doGet(URL url) throws IOException {
|
public static String doGet(URL url) throws IOException {
|
||||||
HttpURLConnection con = createHttpConnection(url);
|
HttpURLConnection con = createHttpConnection(url);
|
||||||
con = resolveConnection(con);
|
con = resolveConnection(con);
|
||||||
return IOUtils.readFullyAsString(con.getInputStream(), StandardCharsets.UTF_8);
|
return IOUtils.readFullyAsString(con.getInputStream());
|
||||||
}
|
}
|
||||||
|
|
||||||
public static String doPost(URL u, Map<String, String> params) throws IOException {
|
public static String doPost(URL u, Map<String, String> params) throws IOException {
|
||||||
StringBuilder sb = new StringBuilder();
|
StringBuilder sb = new StringBuilder();
|
||||||
if (params != null) {
|
if (params != null) {
|
||||||
for (Entry<String, String> e : params.entrySet())
|
for (Map.Entry<String, String> e : params.entrySet())
|
||||||
sb.append(e.getKey()).append("=").append(e.getValue()).append("&");
|
sb.append(e.getKey()).append("=").append(e.getValue()).append("&");
|
||||||
sb.deleteCharAt(sb.length() - 1);
|
sb.deleteCharAt(sb.length() - 1);
|
||||||
}
|
}
|
||||||
|
@ -192,13 +191,13 @@ public final class NetworkUtils {
|
||||||
public static String readData(HttpURLConnection con) throws IOException {
|
public static String readData(HttpURLConnection con) throws IOException {
|
||||||
try {
|
try {
|
||||||
try (InputStream stdout = con.getInputStream()) {
|
try (InputStream stdout = con.getInputStream()) {
|
||||||
return IOUtils.readFullyAsString(stdout, UTF_8);
|
return IOUtils.readFullyAsString(stdout);
|
||||||
}
|
}
|
||||||
} catch (IOException e) {
|
} catch (IOException e) {
|
||||||
try (InputStream stderr = con.getErrorStream()) {
|
try (InputStream stderr = con.getErrorStream()) {
|
||||||
if (stderr == null)
|
if (stderr == null)
|
||||||
throw e;
|
throw e;
|
||||||
return IOUtils.readFullyAsString(stderr, UTF_8);
|
return IOUtils.readFullyAsString(stderr);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -16,8 +16,6 @@ import java.util.zip.ZipOutputStream;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Non thread-safe
|
* Non thread-safe
|
||||||
*
|
|
||||||
* @author huangyuhui
|
|
||||||
*/
|
*/
|
||||||
public final class Zipper implements Closeable {
|
public final class Zipper implements Closeable {
|
||||||
|
|
||||||
|
@ -147,4 +145,4 @@ public final class Zipper implements Closeable {
|
||||||
zos.write(text.getBytes(encoding));
|
zos.write(text.getBytes(encoding));
|
||||||
zos.closeEntry();
|
zos.closeEntry();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,8 +2,6 @@ package com.tungsten.fclcore.util.platform;
|
||||||
|
|
||||||
import static com.tungsten.fclcore.util.Logging.LOG;
|
import static com.tungsten.fclcore.util.Logging.LOG;
|
||||||
|
|
||||||
import com.tungsten.fclcore.util.StringUtils;
|
|
||||||
|
|
||||||
import java.io.BufferedReader;
|
import java.io.BufferedReader;
|
||||||
import java.io.InputStreamReader;
|
import java.io.InputStreamReader;
|
||||||
import java.util.*;
|
import java.util.*;
|
||||||
|
@ -20,7 +18,6 @@ public final class CommandBuilder {
|
||||||
private final List<Item> raw = new ArrayList<>();
|
private final List<Item> raw = new ArrayList<>();
|
||||||
|
|
||||||
public CommandBuilder() {
|
public CommandBuilder() {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private String parse(String s) {
|
private String parse(String s) {
|
||||||
|
@ -57,28 +54,97 @@ public final class CommandBuilder {
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
public String addDefault(String opt) {
|
public void addAllDefault(Collection<String> args) {
|
||||||
for (Item item : raw) {
|
addAllDefault(args, true);
|
||||||
if (item.arg.equals(opt)) {
|
}
|
||||||
return item.arg;
|
|
||||||
|
public void addAllDefaultWithoutParsing(Collection<String> args) {
|
||||||
|
addAllDefault(args, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void addAllDefault(Collection<String> args, boolean parse) {
|
||||||
|
loop:
|
||||||
|
for (String arg : args) {
|
||||||
|
if (arg.startsWith("-D")) {
|
||||||
|
int idx = arg.indexOf('=');
|
||||||
|
if (idx >= 0) {
|
||||||
|
addDefault(arg.substring(0, idx + 1), arg.substring(idx + 1), parse);
|
||||||
|
} else {
|
||||||
|
String opt = arg + "=";
|
||||||
|
for (Item item : raw) {
|
||||||
|
if (item.arg.startsWith(opt)) {
|
||||||
|
LOG.info("Default option '" + arg + "' is suppressed by '" + item.arg + "'");
|
||||||
|
continue loop;
|
||||||
|
} else if (item.arg.equals(arg)) {
|
||||||
|
continue loop;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
raw.add(new Item(arg, parse));
|
||||||
|
}
|
||||||
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (arg.startsWith("-XX:")) {
|
||||||
|
Matcher matcher = UNSTABLE_OPTION_PATTERN.matcher(arg);
|
||||||
|
if (matcher.matches()) {
|
||||||
|
addUnstableDefault(matcher.group("key"), matcher.group("value"), parse);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
matcher = UNSTABLE_BOOLEAN_OPTION_PATTERN.matcher(arg);
|
||||||
|
if (matcher.matches()) {
|
||||||
|
addUnstableDefault(matcher.group("key"), "+".equals(matcher.group("value")), parse);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (arg.startsWith("-X")) {
|
||||||
|
String opt = null;
|
||||||
|
String value = null;
|
||||||
|
|
||||||
|
for (String prefix : new String[]{"-Xmx", "-Xms", "-Xmn", "-Xss"}) {
|
||||||
|
if (arg.startsWith(prefix)) {
|
||||||
|
opt = prefix;
|
||||||
|
value = arg.substring(prefix.length());
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (opt != null) {
|
||||||
|
addDefault(opt, value, parse);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (Item item : raw) {
|
||||||
|
if (item.arg.equals(arg)) {
|
||||||
|
continue loop;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
raw.add(new Item(arg, parse));
|
||||||
}
|
}
|
||||||
raw.add(new Item(opt, true));
|
|
||||||
return null;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public String addDefault(String opt, String value) {
|
public String addDefault(String opt, String value) {
|
||||||
|
return addDefault(opt, value, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
private String addDefault(String opt, String value, boolean parse) {
|
||||||
for (Item item : raw) {
|
for (Item item : raw) {
|
||||||
if (item.arg.startsWith(opt)) {
|
if (item.arg.startsWith(opt)) {
|
||||||
LOG.info("Default option '" + opt + value + "' is suppressed by '" + item.arg + "'");
|
LOG.info("Default option '" + opt + value + "' is suppressed by '" + item.arg + "'");
|
||||||
return item.arg;
|
return item.arg;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
raw.add(new Item(opt + value, true));
|
raw.add(new Item(opt + value, parse));
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
public String addUnstableDefault(String opt, boolean value) {
|
public String addUnstableDefault(String opt, boolean value) {
|
||||||
|
return addUnstableDefault(opt, value, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
private String addUnstableDefault(String opt, boolean value, boolean parse) {
|
||||||
for (Item item : raw) {
|
for (Item item : raw) {
|
||||||
final Matcher matcher = UNSTABLE_BOOLEAN_OPTION_PATTERN.matcher(item.arg);
|
final Matcher matcher = UNSTABLE_BOOLEAN_OPTION_PATTERN.matcher(item.arg);
|
||||||
if (matcher.matches()) {
|
if (matcher.matches()) {
|
||||||
|
@ -89,14 +155,18 @@ public final class CommandBuilder {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (value) {
|
if (value) {
|
||||||
raw.add(new Item("-XX:+" + opt, true));
|
raw.add(new Item("-XX:+" + opt, parse));
|
||||||
} else {
|
} else {
|
||||||
raw.add(new Item("-XX:-" + opt, true));
|
raw.add(new Item("-XX:-" + opt, parse));
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
public String addUnstableDefault(String opt, String value) {
|
public String addUnstableDefault(String opt, String value) {
|
||||||
|
return addUnstableDefault(opt, value, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
private String addUnstableDefault(String opt, String value, boolean parse) {
|
||||||
for (Item item : raw) {
|
for (Item item : raw) {
|
||||||
final Matcher matcher = UNSTABLE_OPTION_PATTERN.matcher(item.arg);
|
final Matcher matcher = UNSTABLE_OPTION_PATTERN.matcher(item.arg);
|
||||||
if (matcher.matches()) {
|
if (matcher.matches()) {
|
||||||
|
@ -106,7 +176,7 @@ public final class CommandBuilder {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
raw.add(new Item("-XX:" + opt + "=" + value, true));
|
raw.add(new Item("-XX:" + opt + "=" + value, parse));
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -114,6 +184,10 @@ public final class CommandBuilder {
|
||||||
return raw.removeIf(i -> pred.test(i.arg));
|
return raw.removeIf(i -> pred.test(i.arg));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public boolean noneMatch(Predicate<String> predicate) {
|
||||||
|
return raw.stream().noneMatch(it -> predicate.test(it.arg));
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public String toString() {
|
public String toString() {
|
||||||
return raw.stream().map(i -> i.parse ? parse(i.arg) : i.arg).collect(Collectors.joining(" "));
|
return raw.stream().map(i -> i.parse ? parse(i.arg) : i.arg).collect(Collectors.joining(" "));
|
||||||
|
@ -128,8 +202,8 @@ public final class CommandBuilder {
|
||||||
}
|
}
|
||||||
|
|
||||||
private static class Item {
|
private static class Item {
|
||||||
String arg;
|
final String arg;
|
||||||
boolean parse;
|
final boolean parse;
|
||||||
|
|
||||||
Item(String arg, boolean parse) {
|
Item(String arg, boolean parse) {
|
||||||
this.arg = arg;
|
this.arg = arg;
|
||||||
|
@ -147,22 +221,53 @@ public final class CommandBuilder {
|
||||||
}
|
}
|
||||||
|
|
||||||
public static boolean hasExecutionPolicy() {
|
public static boolean hasExecutionPolicy() {
|
||||||
return true;
|
try {
|
||||||
|
final Process process = Runtime.getRuntime().exec(new String[]{"powershell", "-Command", "Get-ExecutionPolicy"});
|
||||||
|
if (!process.waitFor(5, TimeUnit.SECONDS)) {
|
||||||
|
process.destroy();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
try (BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream(), OperatingSystem.NATIVE_CHARSET))) {
|
||||||
|
String policy = reader.readLine();
|
||||||
|
return "Unrestricted".equalsIgnoreCase(policy) || "RemoteSigned".equalsIgnoreCase(policy);
|
||||||
|
}
|
||||||
|
} catch (Throwable ignored) {
|
||||||
|
}
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static boolean setExecutionPolicy() {
|
public static boolean setExecutionPolicy() {
|
||||||
|
try {
|
||||||
|
final Process process = Runtime.getRuntime().exec(new String[]{"powershell", "-Command", "Set-ExecutionPolicy -ExecutionPolicy RemoteSigned -Scope CurrentUser"});
|
||||||
|
if (!process.waitFor(5, TimeUnit.SECONDS)) {
|
||||||
|
process.destroy();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
} catch (Throwable ignored) {
|
||||||
|
}
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static boolean containsEscape(String str, String escapeChars) {
|
||||||
|
for (int i = 0; i < escapeChars.length(); i++) {
|
||||||
|
if (str.indexOf(escapeChars.charAt(i)) >= 0)
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String escape(String str, char... escapeChars) {
|
||||||
|
for (char ch : escapeChars) {
|
||||||
|
str = str.replace("" + ch, "\\" + ch);
|
||||||
|
}
|
||||||
|
return str;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static String toBatchStringLiteral(String s) {
|
||||||
|
return containsEscape(s, " \t\"^&<>|") ? '"' + escape(s, '\\', '"') + '"' : s;
|
||||||
|
}
|
||||||
|
|
||||||
public static String toShellStringLiteral(String s) {
|
public static String toShellStringLiteral(String s) {
|
||||||
String escaping = " \t\"!#$&'()*,;<=>?[\\]^`{|}~";
|
return containsEscape(s, " \t\"!#$&'()*,;<=>?[\\]^`{|}~") ? '"' + escape(s, '"', '$', '&', '`') + '"' : s;
|
||||||
String escaped = "\"$&`";
|
|
||||||
if (s.indexOf(' ') >= 0 || s.indexOf('\t') >= 0 || StringUtils.containsOne(s, escaping.toCharArray())) {
|
|
||||||
// The argument has not been quoted, add quotes.
|
|
||||||
for (char ch : escaped.toCharArray())
|
|
||||||
s = s.replace("" + ch, "\\" + ch);
|
|
||||||
return '"' + s + '"';
|
|
||||||
} else
|
|
||||||
return s;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,16 @@
|
||||||
|
package com.tungsten.fclcore.util.png;
|
||||||
|
|
||||||
|
public enum PNGFilterType {
|
||||||
|
NONE(0),
|
||||||
|
SUB(1),
|
||||||
|
UP(2),
|
||||||
|
AVERAGE(3),
|
||||||
|
PAETH(4)
|
||||||
|
;
|
||||||
|
|
||||||
|
final int id;
|
||||||
|
|
||||||
|
PNGFilterType(int id) {
|
||||||
|
this.id = id;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,116 @@
|
||||||
|
package com.tungsten.fclcore.util.png;
|
||||||
|
|
||||||
|
import java.io.Serializable;
|
||||||
|
import java.nio.file.attribute.FileTime;
|
||||||
|
import java.time.*;
|
||||||
|
import java.time.format.DateTimeFormatter;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.LinkedHashMap;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
public final class PNGMetadata implements Serializable {
|
||||||
|
public final static String KEY_TITLE = "Title"; // Short (one line) title or caption for image
|
||||||
|
public final static String KEY_AUTHOR = "Author"; // Name of image's creator
|
||||||
|
public final static String KEY_DESCRIPTION = "Description"; // Description of image (possibly long)
|
||||||
|
public final static String KEY_COPYRIGHT = "Copyright"; // Copyright notice
|
||||||
|
public final static String KEY_CREATION_TIME = "Creation Time"; // Time of original image creation
|
||||||
|
public final static String KEY_SOFTWARE = "Software"; // Software used to create the image
|
||||||
|
public final static String KEY_DISCLAIMER = "Disclaimer"; // Legal disclaimer
|
||||||
|
public final static String KEY_WARNING = "Warning"; // Warning of nature of content
|
||||||
|
public final static String KEY_SOURCE = "Source"; // Device used to create the image
|
||||||
|
public final static String KEY_COMMENT = "Comment"; // Miscellaneous comment
|
||||||
|
|
||||||
|
private static final long serialVersionUID = 0L;
|
||||||
|
|
||||||
|
Map<String, String> texts = Collections.emptyMap();
|
||||||
|
|
||||||
|
public PNGMetadata() {
|
||||||
|
}
|
||||||
|
|
||||||
|
public PNGMetadata setText(String key, String value) {
|
||||||
|
if (texts.isEmpty())
|
||||||
|
texts = new LinkedHashMap<>();
|
||||||
|
|
||||||
|
texts.put(key, value);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getText(String key) {
|
||||||
|
return texts.get(key);
|
||||||
|
}
|
||||||
|
|
||||||
|
public PNGMetadata setTitle(String title) {
|
||||||
|
setText(KEY_TITLE, title);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public PNGMetadata setAuthor(String author) {
|
||||||
|
setText(KEY_AUTHOR, author);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public PNGMetadata setAuthor() {
|
||||||
|
setText(KEY_AUTHOR, System.getProperty("user.name"));
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public PNGMetadata setDescription(String description) {
|
||||||
|
setText(KEY_DESCRIPTION, description);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public PNGMetadata setCopyright(String copyright) {
|
||||||
|
setText(KEY_COPYRIGHT, copyright);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public PNGMetadata setCreationTime(String creationTime) {
|
||||||
|
setText(KEY_CREATION_TIME, creationTime);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public PNGMetadata setCreationTime(LocalDateTime time) {
|
||||||
|
setCreationTime(ZonedDateTime.of(time, ZoneOffset.UTC).toOffsetDateTime());
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public PNGMetadata setCreationTime(OffsetDateTime time) {
|
||||||
|
setCreationTime(time.format(DateTimeFormatter.RFC_1123_DATE_TIME));
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public PNGMetadata setCreationTime(FileTime time) {
|
||||||
|
setCreationTime(ZonedDateTime.ofInstant(time.toInstant(), ZoneOffset.UTC).toOffsetDateTime());
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public PNGMetadata setCreationTime() {
|
||||||
|
setCreationTime(LocalDateTime.now());
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public PNGMetadata setSoftware(String software) {
|
||||||
|
setText(KEY_SOFTWARE, software);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public PNGMetadata setDisclaimer(String disclaimer) {
|
||||||
|
setText(KEY_DISCLAIMER, disclaimer);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public PNGMetadata setWarning(String warning) {
|
||||||
|
setText(KEY_WARNING, warning);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public PNGMetadata setSource(String source) {
|
||||||
|
setText(KEY_SOURCE, source);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public PNGMetadata setComment(String comment) {
|
||||||
|
setText(KEY_COMMENT, comment);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,17 @@
|
||||||
|
package com.tungsten.fclcore.util.png;
|
||||||
|
|
||||||
|
public enum PNGType {
|
||||||
|
GRAYSCALE(0, 1),
|
||||||
|
RGB(2, 3),
|
||||||
|
PALETTE(3, 1),
|
||||||
|
GRAYSCALE_ALPHA(4, 2),
|
||||||
|
RGBA(6, 4);
|
||||||
|
|
||||||
|
final int id;
|
||||||
|
final int cpp;
|
||||||
|
|
||||||
|
PNGType(int id, int cpp) {
|
||||||
|
this.id = id;
|
||||||
|
this.cpp = cpp;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,276 @@
|
||||||
|
package com.tungsten.fclcore.util.png;
|
||||||
|
|
||||||
|
import com.tungsten.fclcore.util.png.image.ArgbImage;
|
||||||
|
|
||||||
|
import java.io.ByteArrayOutputStream;
|
||||||
|
import java.io.Closeable;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.OutputStream;
|
||||||
|
import java.nio.charset.StandardCharsets;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Objects;
|
||||||
|
import java.util.zip.CRC32;
|
||||||
|
import java.util.zip.Deflater;
|
||||||
|
import java.util.zip.DeflaterOutputStream;
|
||||||
|
|
||||||
|
public final class PNGWriter implements Closeable {
|
||||||
|
public static final int DEFAULT_COMPRESS_LEVEL = Deflater.DEFAULT_COMPRESSION;
|
||||||
|
|
||||||
|
private static final int COMPRESS_THRESHOLD = 20;
|
||||||
|
private static final byte[] PNG_FILE_HEADER = {
|
||||||
|
(byte) 0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A
|
||||||
|
};
|
||||||
|
|
||||||
|
private final OutputStream out;
|
||||||
|
private final PNGType type;
|
||||||
|
private final int compressLevel;
|
||||||
|
|
||||||
|
private final Deflater deflater = new Deflater();
|
||||||
|
private final CRC32 crc32 = new CRC32();
|
||||||
|
private final byte[] writeBuffer = new byte[8];
|
||||||
|
|
||||||
|
public PNGWriter(OutputStream out) {
|
||||||
|
this(out, PNGType.RGBA, DEFAULT_COMPRESS_LEVEL);
|
||||||
|
}
|
||||||
|
|
||||||
|
public PNGWriter(OutputStream out, PNGType type) {
|
||||||
|
this(out, type, DEFAULT_COMPRESS_LEVEL);
|
||||||
|
}
|
||||||
|
|
||||||
|
public PNGWriter(OutputStream out, int compressLevel) {
|
||||||
|
this(out, PNGType.RGBA, compressLevel);
|
||||||
|
}
|
||||||
|
|
||||||
|
public PNGWriter(OutputStream out, PNGType type, int compressLevel) {
|
||||||
|
Objects.requireNonNull(type);
|
||||||
|
Objects.requireNonNull(out);
|
||||||
|
|
||||||
|
if (compressLevel != Deflater.DEFAULT_COMPRESSION && (compressLevel < 0 || compressLevel > 9)) {
|
||||||
|
throw new IllegalArgumentException();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (type != PNGType.RGB && type != PNGType.RGBA) {
|
||||||
|
throw new UnsupportedOperationException("SimplePNG currently only supports RGB or RGBA images");
|
||||||
|
}
|
||||||
|
|
||||||
|
this.type = type;
|
||||||
|
this.compressLevel = compressLevel;
|
||||||
|
this.out = out;
|
||||||
|
|
||||||
|
this.deflater.setLevel(compressLevel);
|
||||||
|
}
|
||||||
|
|
||||||
|
public PNGType getType() {
|
||||||
|
return type;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getCompressLevel() {
|
||||||
|
return compressLevel;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void writeByte(int b) throws IOException {
|
||||||
|
out.write(b);
|
||||||
|
crc32.update(b);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void writeShort(int s) throws IOException {
|
||||||
|
writeBuffer[0] = (byte) (s >>> 8);
|
||||||
|
writeBuffer[1] = (byte) (s >>> 0);
|
||||||
|
writeBytes(writeBuffer, 0, 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void writeInt(int value) throws IOException {
|
||||||
|
writeBuffer[0] = (byte) (value >>> 24);
|
||||||
|
writeBuffer[1] = (byte) (value >>> 16);
|
||||||
|
writeBuffer[2] = (byte) (value >>> 8);
|
||||||
|
writeBuffer[3] = (byte) (value >>> 0);
|
||||||
|
writeBytes(writeBuffer, 0, 4);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void writeLong(long value) throws IOException {
|
||||||
|
writeBuffer[0] = (byte) (value >>> 56);
|
||||||
|
writeBuffer[1] = (byte) (value >>> 48);
|
||||||
|
writeBuffer[2] = (byte) (value >>> 40);
|
||||||
|
writeBuffer[3] = (byte) (value >>> 32);
|
||||||
|
writeBuffer[4] = (byte) (value >>> 24);
|
||||||
|
writeBuffer[5] = (byte) (value >>> 16);
|
||||||
|
writeBuffer[6] = (byte) (value >>> 8);
|
||||||
|
writeBuffer[7] = (byte) (value >>> 0);
|
||||||
|
writeBytes(writeBuffer, 0, 8);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void writeBytes(byte[] bytes) throws IOException {
|
||||||
|
writeBytes(bytes, 0, bytes.length);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void writeBytes(byte[] bytes, int off, int len) throws IOException {
|
||||||
|
out.write(bytes, off, len);
|
||||||
|
crc32.update(bytes, off, len);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void beginChunk(String header, int length) throws IOException {
|
||||||
|
writeBuffer[0] = (byte) (length >>> 24);
|
||||||
|
writeBuffer[1] = (byte) (length >>> 16);
|
||||||
|
writeBuffer[2] = (byte) (length >>> 8);
|
||||||
|
writeBuffer[3] = (byte) (length >>> 0);
|
||||||
|
writeBuffer[4] = (byte) header.charAt(0);
|
||||||
|
writeBuffer[5] = (byte) header.charAt(1);
|
||||||
|
writeBuffer[6] = (byte) header.charAt(2);
|
||||||
|
writeBuffer[7] = (byte) header.charAt(3);
|
||||||
|
out.write(writeBuffer, 0, 8);
|
||||||
|
|
||||||
|
crc32.reset();
|
||||||
|
crc32.update(writeBuffer, 4, 4);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void endChunk() throws IOException {
|
||||||
|
int crc = (int) crc32.getValue();
|
||||||
|
writeBuffer[0] = (byte) (crc >>> 24);
|
||||||
|
writeBuffer[1] = (byte) (crc >>> 16);
|
||||||
|
writeBuffer[2] = (byte) (crc >>> 8);
|
||||||
|
writeBuffer[3] = (byte) (crc >>> 0);
|
||||||
|
out.write(writeBuffer, 0, 4);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static final byte[] textSeparator = {0};
|
||||||
|
|
||||||
|
private void textChunk(String keyword, String text) throws IOException {
|
||||||
|
byte[] keywordBytes = keyword.getBytes(StandardCharsets.US_ASCII);
|
||||||
|
byte[] textBytes = text.getBytes(StandardCharsets.UTF_8);
|
||||||
|
int textBytesLength = textBytes.length;
|
||||||
|
|
||||||
|
boolean isAscii = text.length() == textBytesLength;
|
||||||
|
|
||||||
|
boolean compress = compressLevel != 0 && textBytesLength >= COMPRESS_THRESHOLD;
|
||||||
|
|
||||||
|
if (compress) {
|
||||||
|
byte[] compressed = new byte[textBytesLength];
|
||||||
|
|
||||||
|
deflater.reset();
|
||||||
|
deflater.setInput(textBytes);
|
||||||
|
deflater.finish();
|
||||||
|
|
||||||
|
int len = deflater.deflate(compressed, 0, textBytesLength, Deflater.SYNC_FLUSH);
|
||||||
|
if (len < textBytesLength) {
|
||||||
|
textBytes = compressed;
|
||||||
|
textBytesLength = len;
|
||||||
|
} else {
|
||||||
|
compress = false;
|
||||||
|
deflater.reset();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
String chunkType;
|
||||||
|
|
||||||
|
byte[] separator;
|
||||||
|
|
||||||
|
if (isAscii) {
|
||||||
|
if (compress) {
|
||||||
|
chunkType = "zTXt";
|
||||||
|
separator = new byte[]{
|
||||||
|
0, // null separator
|
||||||
|
0 // compression method
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
chunkType = "tEXt";
|
||||||
|
separator = new byte[]{
|
||||||
|
0, // null separator
|
||||||
|
};
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
chunkType = "iTXt";
|
||||||
|
separator = new byte[] {
|
||||||
|
0, // null separator
|
||||||
|
(byte) (compress ? 1 : 0), // compression flag
|
||||||
|
0, // compression method
|
||||||
|
0, // null separator
|
||||||
|
0 // null separator
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
beginChunk(chunkType, keywordBytes.length + separator.length + textBytesLength);
|
||||||
|
writeBytes(keywordBytes);
|
||||||
|
writeBytes(separator);
|
||||||
|
writeBytes(textBytes, 0, textBytesLength);
|
||||||
|
endChunk();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void write(ArgbImage image) throws IOException {
|
||||||
|
PNGMetadata metadata = image.getMetadata();
|
||||||
|
final int width = image.getWidth();
|
||||||
|
final int height = image.getHeight();
|
||||||
|
|
||||||
|
out.write(PNG_FILE_HEADER);
|
||||||
|
|
||||||
|
// IHDR Chunk
|
||||||
|
beginChunk("IHDR", 13);
|
||||||
|
writeInt(width);
|
||||||
|
writeInt(height);
|
||||||
|
writeByte(8); // bit depth
|
||||||
|
writeByte(type.id);
|
||||||
|
writeByte(0); // compression
|
||||||
|
writeByte(0); // filter
|
||||||
|
writeByte(0); // interlace method
|
||||||
|
endChunk();
|
||||||
|
|
||||||
|
if (metadata != null) {
|
||||||
|
for (Map.Entry<String, String> entry : metadata.texts.entrySet()) {
|
||||||
|
textChunk(entry.getKey(), entry.getValue());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// IDAT Chunk
|
||||||
|
int colorPerPixel = type.cpp;
|
||||||
|
int bytesPerLine = 1 + colorPerPixel * width;
|
||||||
|
int rawOutputSize = bytesPerLine * height;
|
||||||
|
byte[] lineBuffer = new byte[bytesPerLine];
|
||||||
|
|
||||||
|
deflater.reset();
|
||||||
|
OutputBuffer buffer = new OutputBuffer(compressLevel == 0 ? rawOutputSize + 12 : rawOutputSize / 2);
|
||||||
|
try (DeflaterOutputStream dos = new DeflaterOutputStream(buffer, deflater)) {
|
||||||
|
for (int y = 0; y < height; y++) {
|
||||||
|
for (int x = 0; x < width; x++) {
|
||||||
|
int color = image.getArgb(x, y);
|
||||||
|
int off = 1 + colorPerPixel * x;
|
||||||
|
|
||||||
|
lineBuffer[off + 0] = (byte) (color >>> 16);
|
||||||
|
lineBuffer[off + 1] = (byte) (color >>> 8);
|
||||||
|
lineBuffer[off + 2] = (byte) (color >>> 0);
|
||||||
|
if (colorPerPixel == 4)
|
||||||
|
lineBuffer[off + 3] = (byte) (color >>> 24);
|
||||||
|
}
|
||||||
|
|
||||||
|
dos.write(lineBuffer);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
int len = buffer.size();
|
||||||
|
beginChunk("IDAT", len);
|
||||||
|
writeBytes(buffer.getBuffer(), 0, len);
|
||||||
|
endChunk();
|
||||||
|
|
||||||
|
// IEND Chunk
|
||||||
|
beginChunk("IEND", 0);
|
||||||
|
endChunk();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void close() throws IOException {
|
||||||
|
deflater.end();
|
||||||
|
out.close();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static final class OutputBuffer extends ByteArrayOutputStream {
|
||||||
|
public OutputBuffer() {
|
||||||
|
}
|
||||||
|
|
||||||
|
public OutputBuffer(int size) {
|
||||||
|
super(size);
|
||||||
|
}
|
||||||
|
|
||||||
|
byte[] getBuffer() {
|
||||||
|
return super.buf;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,79 @@
|
||||||
|
package com.tungsten.fclcore.util.png.fakefx;
|
||||||
|
|
||||||
|
import android.graphics.Bitmap;
|
||||||
|
|
||||||
|
import com.tungsten.fclcore.util.png.PNGType;
|
||||||
|
import com.tungsten.fclcore.util.png.PNGWriter;
|
||||||
|
import com.tungsten.fclcore.util.png.image.ArgbImageWrapper;
|
||||||
|
|
||||||
|
import java.io.ByteArrayOutputStream;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.OutputStream;
|
||||||
|
import java.io.UncheckedIOException;
|
||||||
|
import java.nio.file.Files;
|
||||||
|
import java.nio.file.Path;
|
||||||
|
|
||||||
|
public final class PNGFakeFXUtils {
|
||||||
|
private PNGFakeFXUtils() {
|
||||||
|
}
|
||||||
|
|
||||||
|
public static ArgbImageWrapper<Bitmap> asArgbImage(Bitmap image) {
|
||||||
|
return new ArgbImageWrapper<Bitmap>(image, (int) image.getWidth(), (int) image.getHeight()) {
|
||||||
|
@Override
|
||||||
|
public int getArgb(int x, int y) {
|
||||||
|
return image.getPixel(x, y);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void writeImage(Bitmap image, Path file) throws IOException {
|
||||||
|
writeImage(image, Files.newOutputStream(file));
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void writeImage(Bitmap image, Path file, PNGType type) throws IOException {
|
||||||
|
writeImage(image, Files.newOutputStream(file), type);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void writeImage(Bitmap image, Path file, PNGType type, int compressLevel) throws IOException {
|
||||||
|
writeImage(image, Files.newOutputStream(file), type, compressLevel);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static byte[] writeImageToArray(Bitmap image) {
|
||||||
|
return writeImageToArray(image, PNGType.RGBA, PNGWriter.DEFAULT_COMPRESS_LEVEL);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static byte[] writeImageToArray(Bitmap image, PNGType type) {
|
||||||
|
return writeImageToArray(image, type, PNGWriter.DEFAULT_COMPRESS_LEVEL);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static byte[] writeImageToArray(Bitmap image, PNGType type, int compressLevel) {
|
||||||
|
int estimatedSize = (int) (
|
||||||
|
image.getWidth() * image.getHeight()
|
||||||
|
* (type == PNGType.RGB ? 3 : 4)
|
||||||
|
+ 32);
|
||||||
|
|
||||||
|
if (compressLevel != 1) {
|
||||||
|
estimatedSize /= 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
ByteArrayOutputStream temp = new ByteArrayOutputStream(Integer.max(estimatedSize, 32));
|
||||||
|
writeImage(image, temp, type, compressLevel);
|
||||||
|
return temp.toByteArray();
|
||||||
|
} catch (IOException e) {
|
||||||
|
throw new UncheckedIOException(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void writeImage(Bitmap image, OutputStream out) throws IOException {
|
||||||
|
writeImage(image, out, PNGType.RGBA, PNGWriter.DEFAULT_COMPRESS_LEVEL);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void writeImage(Bitmap image, OutputStream out, PNGType type) throws IOException {
|
||||||
|
writeImage(image, out, type, PNGWriter.DEFAULT_COMPRESS_LEVEL);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void writeImage(Bitmap image, OutputStream out, PNGType type, int compressLevel) throws IOException {
|
||||||
|
new PNGWriter(out, type, compressLevel).write(asArgbImage(image));
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,26 @@
|
||||||
|
package com.tungsten.fclcore.util.png.image;
|
||||||
|
|
||||||
|
import com.tungsten.fclcore.util.png.PNGMetadata;
|
||||||
|
|
||||||
|
public interface ArgbImage {
|
||||||
|
|
||||||
|
int getWidth();
|
||||||
|
|
||||||
|
int getHeight();
|
||||||
|
|
||||||
|
int getArgb(int x, int y);
|
||||||
|
|
||||||
|
default PNGMetadata getMetadata() {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
default ArgbImage withMetadata(PNGMetadata metadata) {
|
||||||
|
return new ArgbImageWithMetadata(this, metadata);
|
||||||
|
}
|
||||||
|
|
||||||
|
default ArgbImage withDefaultMetadata() {
|
||||||
|
return withMetadata(new PNGMetadata()
|
||||||
|
.setAuthor()
|
||||||
|
.setCreationTime());
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,52 @@
|
||||||
|
package com.tungsten.fclcore.util.png.image;
|
||||||
|
|
||||||
|
public final class ArgbImageBuffer implements ArgbImage {
|
||||||
|
private final int width;
|
||||||
|
private final int height;
|
||||||
|
private final int[] colors;
|
||||||
|
|
||||||
|
public ArgbImageBuffer(int width, int height) {
|
||||||
|
if (width <= 0 || height <= 0) {
|
||||||
|
throw new IllegalArgumentException();
|
||||||
|
}
|
||||||
|
|
||||||
|
this.width = width;
|
||||||
|
this.height = height;
|
||||||
|
this.colors = new int[width * height];
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int getWidth() {
|
||||||
|
return width;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int getHeight() {
|
||||||
|
return height;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int getArgb(int x, int y) {
|
||||||
|
if (x < 0 || x >= width || y < 0 || y >= height) {
|
||||||
|
throw new IllegalArgumentException();
|
||||||
|
}
|
||||||
|
|
||||||
|
return colors[x + y * width];
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setArgb(int x, int y, int color) {
|
||||||
|
if (x < 0 || x >= width || y < 0 || y >= height) {
|
||||||
|
throw new IllegalArgumentException();
|
||||||
|
}
|
||||||
|
|
||||||
|
colors[x + y * width] = color;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setArgb(int x, int y, int a, int r, int g, int b) {
|
||||||
|
setArgb(x, y, (a << 24) | (r << 16) | (g << 8) | b);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setRgb(int x, int y, int r, int g, int b) {
|
||||||
|
setArgb(x, y, 0xff, r, g, b);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,38 @@
|
||||||
|
package com.tungsten.fclcore.util.png.image;
|
||||||
|
|
||||||
|
import com.tungsten.fclcore.util.png.PNGMetadata;
|
||||||
|
|
||||||
|
final class ArgbImageWithMetadata implements ArgbImage {
|
||||||
|
private final ArgbImage source;
|
||||||
|
private final PNGMetadata metadata;
|
||||||
|
|
||||||
|
ArgbImageWithMetadata(ArgbImage source, PNGMetadata metadata) {
|
||||||
|
this.source = source;
|
||||||
|
this.metadata = metadata;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int getWidth() {
|
||||||
|
return source.getWidth();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int getHeight() {
|
||||||
|
return source.getHeight();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int getArgb(int x, int y) {
|
||||||
|
return source.getArgb(x, y);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public PNGMetadata getMetadata() {
|
||||||
|
return metadata;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public ArgbImage withMetadata(PNGMetadata metadata) {
|
||||||
|
return new ArgbImageWithMetadata(source, metadata);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,48 @@
|
||||||
|
package com.tungsten.fclcore.util.png.image;
|
||||||
|
|
||||||
|
import java.util.Objects;
|
||||||
|
|
||||||
|
public abstract class ArgbImageWrapper<T> implements ArgbImage {
|
||||||
|
protected final T image;
|
||||||
|
protected final int width;
|
||||||
|
protected final int height;
|
||||||
|
|
||||||
|
protected ArgbImageWrapper(T image, int width, int height) {
|
||||||
|
this.image = image;
|
||||||
|
this.width = width;
|
||||||
|
this.height = height;
|
||||||
|
|
||||||
|
if (width <= 0 || height <= 0) {
|
||||||
|
throw new IllegalArgumentException("Invalid picture size");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public T getImage() {
|
||||||
|
return image;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int getWidth() {
|
||||||
|
return width;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int getHeight() {
|
||||||
|
return height;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static <T> ArgbImageWrapper<T> of(T image, int width, int height, ColorExtractor<T> extractor) {
|
||||||
|
Objects.requireNonNull(extractor);
|
||||||
|
return new ArgbImageWrapper<T>(image, width, height) {
|
||||||
|
@Override
|
||||||
|
public int getArgb(int x, int y) {
|
||||||
|
return extractor.getArgb(super.image, x, y);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
@FunctionalInterface
|
||||||
|
public interface ColorExtractor<T> {
|
||||||
|
int getArgb(T image, int x, int y);
|
||||||
|
}
|
||||||
|
}
|
|
@ -26,8 +26,8 @@ public class NormalizedSkin {
|
||||||
this.texture = texture;
|
this.texture = texture;
|
||||||
|
|
||||||
// check format
|
// check format
|
||||||
int w = texture.getWidth();
|
int w = (int) texture.getWidth();
|
||||||
int h = texture.getHeight();
|
int h = (int) texture.getHeight();
|
||||||
if (w % 64 != 0) {
|
if (w % 64 != 0) {
|
||||||
throw new InvalidSkinException("Invalid size " + w + "x" + h);
|
throw new InvalidSkinException("Invalid size " + w + "x" + h);
|
||||||
}
|
}
|
||||||
|
@ -84,10 +84,6 @@ public class NormalizedSkin {
|
||||||
return oldFormat;
|
return oldFormat;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Tests whether the skin is slim.
|
|
||||||
* Note that this method doesn't guarantee the result is correct.
|
|
||||||
*/
|
|
||||||
public boolean isSlim() {
|
public boolean isSlim() {
|
||||||
return (hasTransparencyRelative(50, 16, 2, 4) ||
|
return (hasTransparencyRelative(50, 16, 2, 4) ||
|
||||||
hasTransparencyRelative(54, 20, 2, 12) ||
|
hasTransparencyRelative(54, 20, 2, 12) ||
|
||||||
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue