Synchronize HMCL updates

This commit is contained in:
Tungstend 2023-07-04 02:36:03 +08:00
parent 5cc743df5e
commit d8170d0077
102 changed files with 2074 additions and 692 deletions

View File

@ -23,6 +23,7 @@ import com.tungsten.fclcore.game.GameDirectoryType;
import com.tungsten.fclcore.game.JavaVersion;
import com.tungsten.fclcore.game.LaunchOptions;
import com.tungsten.fclcore.game.Version;
import com.tungsten.fclcore.mod.ModAdviser;
import com.tungsten.fclcore.mod.Modpack;
import com.tungsten.fclcore.mod.ModpackConfiguration;
import com.tungsten.fclcore.mod.ModpackProvider;
@ -158,17 +159,7 @@ public class FCLGameRepository extends DefaultGameRepository {
File srcGameDir = getRunDirectory(srcId);
File dstGameDir = getRunDirectory(dstId);
List<String> blackList = new ArrayList<>(Arrays.asList(
"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
));
List<String> blackList = new ArrayList<>(ModAdviser.MODPACK_BLACK_LIST);
blackList.add(srcId + ".jar");
blackList.add(srcId + ".json");
if (!copySaves)
@ -324,8 +315,8 @@ public class FCLGameRepository extends DefaultGameRepository {
.setVersionName(version)
.setProfileName(FCLPath.CONTEXT.getString(R.string.app_name))
.setGameArguments(StringUtils.tokenize(vs.getMinecraftArgs()))
.setJavaArguments(StringUtils.tokenize(vs.getJavaArgs()))
.setMaxMemory((int)(getAllocatedMemory(
.setOverrideJavaArguments(StringUtils.tokenize(vs.getJavaArgs()))
.setMaxMemory(vs.isAutoMemory() ? null : (int)(getAllocatedMemory(
vs.getMaxMemory() * 1024L * 1024L,
MemoryUtils.getFreeDeviceMemory(FCLPath.CONTEXT),
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();
}
@ -409,7 +403,7 @@ public class FCLGameRepository extends DefaultGameRepository {
public static long getAllocatedMemory(long minimum, long available, boolean auto) {
if (auto) {
available -= 256 * 1024 * 1024;
available -= 384 * 1024 * 1024; // Reserve 384MiB memory for off-heap memory and HMCL itself
if (available <= 0) {
return minimum;
}
@ -418,7 +412,7 @@ public class FCLGameRepository extends DefaultGameRepository {
final long suggested = Math.min(available <= threshold
? (long) (available * 0.8)
: (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);
} else {
return minimum;

View File

@ -20,7 +20,7 @@ public abstract class LocalizedRemoteModRepository implements RemoteModRepositor
@Override
public Stream<RemoteMod> search(String gameVersion, Category category, int pageOffset, int pageSize, String searchFilter, SortType sort, SortOrder sortOrder) throws IOException {
String newSearchFilter;
if (StringUtils.CHINESE_PATTERN.matcher(searchFilter).find()) {
if (StringUtils.containsChinese(searchFilter)) {
ModTranslations modTranslations = ModTranslations.getTranslationsByRepositoryType(getType());
List<ModTranslations.Mod> mods = modTranslations.searchMod(searchFilter);
List<String> searchFilters = new ArrayList<>();

View File

@ -41,12 +41,17 @@ public final class LogExporter {
return CompletableFuture.runAsync(() -> {
try (Zipper zipper = new Zipper(zipFile)) {
if (Files.exists(runDirectory.resolve("logs").resolve("debug.log"))) {
zipper.putFile(runDirectory.resolve("logs").resolve("debug.log"), "debug.log");
Path logsDir = runDirectory.resolve("logs");
if (Files.exists(logsDir.resolve("debug.log"))) {
zipper.putFile(logsDir.resolve("debug.log"), "debug.log");
}
if (Files.exists(runDirectory.resolve("logs").resolve("latest.log"))) {
zipper.putFile(runDirectory.resolve("logs").resolve("latest.log"), "latest.log");
if (Files.exists(logsDir.resolve("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(logs, "minecraft.log");

View File

@ -18,7 +18,6 @@ import com.tungsten.fclcore.util.io.NetworkUtils;
import fi.iki.elonen.NanoHTTPD;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.CompletableFuture;
@ -88,7 +87,7 @@ public final class OAuthServer extends NanoHTTPD implements OAuth.Session {
String html;
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));
} catch (IOException e) {
Logging.LOG.log(Level.SEVERE, "Failed to load html");

View File

@ -10,7 +10,11 @@ import static com.tungsten.fclcore.util.Pair.pair;
import java.io.File;
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.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
@ -21,6 +25,7 @@ import static java.util.stream.Collectors.toList;
import android.content.Context;
import com.google.gson.reflect.TypeToken;
import com.tungsten.fcl.R;
import com.tungsten.fcl.game.OAuthServer;
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.YggdrasilAccount;
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.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.collections.FXCollections;
import com.tungsten.fclcore.fakefx.collections.ObservableList;
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;
public final class Accounts {
@ -80,7 +88,6 @@ public final class Accounts {
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 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);
// ==== login type / account factory mapping ====
@ -129,59 +136,20 @@ public final class Accounts {
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 ReadOnlyListWrapper<Account> accountsWrapper = new ReadOnlyListWrapper<>(Accounts.class, "accounts", accounts);
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();
}
};
private static final ObjectProperty<Account> selectedAccount = new SimpleObjectProperty<>(Accounts.class, "selectedAccount");
/**
* True if {@link #init()} hasn't been called.
*/
private static boolean initialized = false;
static {
accounts.addListener(onInvalidating(Accounts::updateAccountStorages));
}
private static Map<Object, Object> getAccountStorage(Account account) {
Map<Object, Object> storage = account.toStorage();
storage.put("type", getLoginType(getAccountFactory(account)));
if (account == selectedAccount.get()) {
storage.put("selected", true);
}
return storage;
}
@ -191,7 +159,67 @@ public final class Accounts {
if (!initialized)
return;
// 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)
throw new IllegalStateException("Already initialized");
// load accounts
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);
loadGlobalAccountStorages();
// load accounts
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"))) {
selectedAccount.set(account);
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;
config().getAuthlibInjectorServers().addListener(onInvalidating(Accounts::removeDanglingAuthlibInjectorAccounts));
Account selected = selectedAccount.get();
if (selected != null) {
Account finalSelected = selected;
Schedulers.io().execute(() -> {
try {
selected.logIn();
} catch (AuthenticationException e) {
LOG.log(Level.WARNING, "Failed to log " + selected + " in", e);
finalSelected.logIn();
} catch (Throwable e) {
LOG.log(Level.WARNING, "Failed to log " + finalSelected + " in", e);
}
});
}
if (!config().getAuthlibInjectorServers().isEmpty()) {
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()) {
if (selected instanceof AuthlibInjectorAccount && ((AuthlibInjectorAccount) selected).getServer() == server)
@ -266,10 +343,6 @@ public final class Accounts {
return accounts;
}
public static ReadOnlyListProperty<Account> accountsProperty() {
return accountsWrapper.getReadOnlyProperty();
}
public static Account getSelectedAccount() {
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(
pair(Accounts.FACTORY_OFFLINE, R.string.account_methods_offline),
pair(Accounts.FACTORY_MOJANG, R.string.account_methods_yggdrasil),

View File

@ -9,11 +9,11 @@ import com.tungsten.fclcore.auth.authlibinjector.AuthlibInjectorServer;
import com.tungsten.fclcore.fakefx.beans.InvalidationListener;
import com.tungsten.fclcore.fakefx.beans.Observable;
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.MapProperty;
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.SimpleMapProperty;
import com.tungsten.fclcore.fakefx.beans.property.SimpleStringProperty;
import com.tungsten.fclcore.fakefx.beans.property.StringProperty;
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;
private static final Gson CONFIG_GSON = new GsonBuilder()
public static final Gson CONFIG_GSON = new GsonBuilder()
.registerTypeAdapter(File.class, FileTypeAdapter.INSTANCE)
.registerTypeAdapter(ObservableList.class, new ObservableListCreator())
.registerTypeAdapter(ObservableSet.class, new ObservableSetCreator())
@ -80,7 +80,10 @@ public final class Config implements Cloneable, Observable {
private StringProperty versionListSource = new SimpleStringProperty("balanced");
@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")
private ObservableList<Map<Object, Object>> accountStorages = FXCollections.observableArrayList();
@ -220,10 +223,22 @@ public final class Config implements Cloneable, Observable {
return versionListSource;
}
public ObservableMap<String, Profile> getConfigurations() {
public MapProperty<String, Profile> getConfigurations() {
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() {
return accountStorages;
}

View File

@ -33,7 +33,7 @@ public class Controllers {
public static void checkControllers() {
if (controllers.isEmpty()) {
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()
.registerTypeAdapterFactory(new JavaFxPropertyTypeAdapterFactory(true, true))
.setPrettyPrinting()

View File

@ -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.SimpleStringProperty;
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.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 java.io.File;
import java.lang.reflect.Type;
import java.util.*;
@JsonAdapter(GlobalConfig.Serializer.class)
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
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) {
return null;
}
@ -72,7 +54,7 @@ public class GlobalConfig implements Cloneable, Observable {
}
public String toJson() {
return CONFIG_GSON.toJson(this);
return Config.CONFIG_GSON.toJson(this);
}
@Override

View File

@ -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.ReadOnlyStringWrapper;
import com.tungsten.fclcore.fakefx.beans.property.SimpleObjectProperty;
import com.tungsten.fclcore.fakefx.collections.FXCollections;
import com.tungsten.fclcore.fakefx.collections.ObservableList;
import java.io.File;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.TreeMap;
import java.util.function.Consumer;
import java.util.stream.Collectors;
public final class Profiles {
@ -103,8 +104,11 @@ public final class Profiles {
if (!initialized)
return;
// update storage
config().getConfigurations().clear();
config().getConfigurations().putAll(profiles.stream().collect(Collectors.toMap(Profile::getName, it -> it)));
TreeMap<String, Profile> newConfigurations = new TreeMap<>();
for (Profile profile : profiles) {
newConfigurations.put(profile.getName(), profile);
}
config().getConfigurations().setValue(FXCollections.observableMap(newConfigurations));
}
/**

View File

@ -107,7 +107,7 @@ public enum ModTranslations {
return true;
}
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());
return true;
} catch (Exception e) {

View File

@ -26,7 +26,7 @@ public class RuntimeUtils {
public static boolean isLatest(String targetDir, String srcDir) throws IOException {
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;
}
@ -43,7 +43,7 @@ public class RuntimeUtils {
new File(targetDir).mkdirs();
String universalPath = srcDir + "/universal.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(archPath), new File(targetDir));
FileUtils.writeText(new File(targetDir + "/version"), version);

View File

@ -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.WeakListChangeListener;
import java.util.LinkedList;
import java.util.ArrayList;
import java.util.List;
public class WeakListenerHolder {
private List<Object> refs = new LinkedList<>();
private final List<Object> refs = new ArrayList<>(0);
public WeakListenerHolder() {
}

View File

@ -6,10 +6,13 @@ import com.tungsten.fclcore.fakefx.beans.InvalidationListener;
import com.tungsten.fclcore.fakefx.beans.Observable;
import com.tungsten.fclcore.fakefx.beans.binding.Bindings;
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.fakefx.ObservableHelper;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.UUID;
@ -48,7 +51,23 @@ public abstract class Account implements Observable {
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
public void addListener(InvalidationListener listener) {
@ -72,12 +91,29 @@ public abstract class Account implements Observable {
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
public String toString() {
return new ToStringBuilder(this)
.append("username", getUsername())
.append("character", getCharacter())
.append("uuid", getUUID())
.append("portable", isPortable())
.toString();
}
}

View File

@ -7,16 +7,22 @@ import java.io.IOException;
import java.util.UUID;
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 UUID uuid;
private final String accessToken;
private final String userType;
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.uuid = uuid;
this.accessToken = accessToken;
this.userType = userType;
this.userProperties = userProperties;
}
@ -32,6 +38,10 @@ public class AuthInfo implements AutoCloseable {
return accessToken;
}
public String getUserType() {
return userType;
}
/**
* Properties of this user.
* Don't know the difference between user properties and user property map.

View File

@ -222,7 +222,7 @@ public class OAuth {
*
* @param url OAuth url.
*/
void openBrowser(String url);
void openBrowser(String url) throws IOException;
String getClientId();
@ -236,7 +236,7 @@ public class OAuth {
DEVICE,
}
public class Result {
public static final class Result {
private final String accessToken;
private final String refreshToken;

View File

@ -108,7 +108,7 @@ public class AuthlibInjectorAccount extends YggdrasilAccount {
private final 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.server = server;
@ -141,6 +141,11 @@ public class AuthlibInjectorAccount extends YggdrasilAccount {
return server;
}
@Override
public String getIdentifier() {
return server.getUrl() + ":" + super.getIdentifier();
}
@Override
public int hashCode() {
return Objects.hash(super.hashCode(), server.hashCode());
@ -151,7 +156,8 @@ public class AuthlibInjectorAccount extends YggdrasilAccount {
if (obj == null || obj.getClass() != AuthlibInjectorAccount.class)
return false;
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
@ -169,7 +175,7 @@ public class AuthlibInjectorAccount extends YggdrasilAccount {
return emptySet();
Set<TextureType> result = EnumSet.noneOf(TextureType.class);
for (String val : prop.split(",")) {
val = val.toUpperCase();
val = val.toUpperCase(Locale.ROOT);
TextureType parsed;
try {
parsed = TextureType.valueOf(val);

View File

@ -65,6 +65,11 @@ public class MicrosoftAccount extends OAuthAccount {
return session.getProfile().getId();
}
@Override
public String getIdentifier() {
return "microsoft:" + getUUID();
}
@Override
public AuthInfo logIn() throws AuthenticationException {
if (!authenticated) {
@ -151,6 +156,6 @@ public class MicrosoftAccount extends OAuthAccount {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
MicrosoftAccount that = (MicrosoftAccount) o;
return characterUUID.equals(that.characterUUID);
return this.isPortable() == that.isPortable() && characterUUID.equals(that.characterUUID);
}
}

View File

@ -94,7 +94,7 @@ public class MicrosoftSession {
public AuthInfo toAuthInfo() {
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 {

View File

@ -51,6 +51,10 @@ public class OfflineAccount extends Account {
}
}
public AuthlibInjectorArtifactProvider getDownloader() {
return downloader;
}
@Override
public UUID getUUID() {
return uuid;
@ -66,6 +70,11 @@ public class OfflineAccount extends Account {
return username;
}
@Override
public String getIdentifier() {
return username + ":" + username;
}
public Skin getSkin() {
return skin;
}
@ -75,7 +84,7 @@ public class OfflineAccount extends Account {
invalidate();
}
private boolean loadAuthlibInjector(Skin skin) {
protected boolean loadAuthlibInjector(Skin skin) {
if (skin == null) return false;
if (skin.getType() == Skin.Type.DEFAULT) return false;
TextureModel defaultModel = TextureModel.detectUUID(getUUID());
@ -88,7 +97,8 @@ public class OfflineAccount extends Account {
@Override
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)) {
CompletableFuture<AuthlibInjectorArtifactInfo> artifactTask = CompletableFuture.supplyAsync(() -> {
@ -128,7 +138,7 @@ public class OfflineAccount extends Account {
private YggdrasilServer server;
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;
}
@ -140,7 +150,8 @@ public class OfflineAccount extends Account {
server.start();
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) {
// ignore
} catch (Exception e) {
@ -182,9 +193,9 @@ public class OfflineAccount extends Account {
Skin.LoadedSkin loadedSkin = skin.load(username).run();
Map<TextureType, Texture> map = new HashMap<>();
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) {
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 {
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))
return false;
OfflineAccount another = (OfflineAccount) obj;
return username.equals(another.username);
return isPortable() == another.isPortable() && username.equals(another.username);
}
}

View File

@ -27,13 +27,21 @@ import java.util.Collections;
import java.util.Locale;
import java.util.Map;
import java.util.Optional;
import java.util.function.Function;
public class Skin {
public enum Type {
DEFAULT,
STEVE,
ALEX,
ARI,
EFE,
KAI,
MAKENA,
NOOR,
STEVE,
SUNNY,
ZURI,
LOCAL_FILE,
LITTLE_SKIN,
CUSTOM_SKIN_LOADER_API,
@ -43,10 +51,24 @@ public class Skin {
switch (type) {
case "default":
return DEFAULT;
case "steve":
return STEVE;
case "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":
return LOCAL_FILE;
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 String cslApi;
private final TextureModel textureModel;
@ -99,10 +127,19 @@ public class Skin {
switch (type) {
case DEFAULT:
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:
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:
return Task.supplyAsync(() -> {
Texture skin = null, cape = null;

View File

@ -1,11 +1,7 @@
package com.tungsten.fclcore.auth.offline;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.math.BigInteger;
import java.net.URL;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.HashMap;
@ -16,29 +12,23 @@ import static java.util.Objects.requireNonNull;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
public class Texture {
import com.tungsten.fclcore.util.Hex;
public final class Texture {
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.data = requireNonNull(data);
}
public byte[] getData() {
return data;
this.image = requireNonNull(image);
}
public String getHash() {
return hash;
}
public InputStream getInputStream() {
return new ByteArrayInputStream(data);
}
public int getLength() {
return data.length;
public Bitmap getImage() {
return image;
}
private static final Map<String, Texture> textures = new HashMap<>();
@ -58,8 +48,9 @@ public class Texture {
} catch (NoSuchAlgorithmException 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];
putInt(buf, 0, width);
@ -82,8 +73,7 @@ public class Texture {
digest.update(buf, 0, pos);
}
byte[] sha256 = digest.digest();
return String.format("%0" + (sha256.length << 1) + "x", new BigInteger(1, sha256));
return Hex.encodeHex(digest.digest());
}
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 {
if (in == null) return null;
Bitmap img = BitmapFactory.decodeStream(in);
if (img == null) {
throw new IOException("No image found");
Bitmap img;
try (InputStream is = in) {
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);
if (existent != null) {
return existent;
}
ByteArrayOutputStream buf = new ByteArrayOutputStream();
img.compress(Bitmap.CompressFormat.PNG, 100, buf);
Texture texture = new Texture(hash, buf.toByteArray());
Texture texture = new Texture(hash, image);
existent = textures.putIfAbsent(hash, texture);
if (existent != null) {
@ -119,9 +112,4 @@ public class Texture {
return texture;
}
public static Texture loadTexture(String url) throws IOException {
if (url == null) return null;
return loadTexture(new URL(url).openStream());
}
}

View File

@ -11,8 +11,9 @@ import com.tungsten.fclcore.util.Lang;
import com.tungsten.fclcore.util.gson.JsonUtils;
import com.tungsten.fclcore.util.gson.UUIDTypeAdapter;
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.security.*;
import java.util.*;
@ -22,8 +23,6 @@ import java.util.stream.Stream;
import static java.nio.charset.StandardCharsets.UTF_8;
import fi.iki.elonen.NanoHTTPD;
public class YggdrasilServer extends HttpServer {
private final Map<UUID, Character> charactersByUuid = new HashMap<>();
@ -32,7 +31,7 @@ public class YggdrasilServer extends HttpServer {
public YggdrasilServer(int 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.POST, Pattern.compile("/api/profiles/minecraft"), this::profiles);
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 {
String body = IOUtils.readFullyAsString(request.getSession().getInputStream(), UTF_8);
List<String> names = JsonUtils.fromNonNullJson(body, new TypeToken<List<String>>() {
List<String> names = JsonUtils.fromNonNullJsonFully(request.getSession().getInputStream(), new TypeToken<List<String>>() {
}.getType());
return ok(names.stream().distinct()
.map(this::findCharacterByName)
@ -115,7 +113,8 @@ public class YggdrasilServer extends HttpServer {
if (Texture.hasTexture(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("Cache-Control", "max-age=2592000, public");
return response;

View File

@ -89,6 +89,11 @@ public class YggdrasilAccount extends ClassicAccount {
return session.getSelectedProfile().getId();
}
@Override
public String getIdentifier() {
return getUsername() + ":" + getUUID();
}
@Override
public synchronized AuthInfo logIn() throws AuthenticationException {
if (!authenticated) {
@ -214,6 +219,6 @@ public class YggdrasilAccount extends ClassicAccount {
if (obj == null || obj.getClass() != YggdrasilAccount.class)
return false;
YggdrasilAccount another = (YggdrasilAccount) obj;
return characterUUID.equals(another.characterUUID);
return isPortable() == another.isPortable() && characterUUID.equals(another.characterUUID);
}
}

View File

@ -257,6 +257,6 @@ public class YggdrasilService {
.create();
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 PURCHASE_URL = "https://www.minecraft.net/store/minecraft-java-bedrock-edition-pc";
public static final String MIGRATION_FAQ_URL = "https://help.minecraft.net/articles/360050865492";
public static final String PURCHASE_URL = "https://www.microsoft.com/store/productId/9NXP44L49SHJ";
}

View File

@ -66,6 +66,7 @@ public class YggdrasilSession {
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 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);
return new YggdrasilSession(clientToken, accessToken, new GameProfile(uuid, name), null, userProperties);
}
@ -86,7 +87,7 @@ public class YggdrasilSession {
if (selectedProfile == null)
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)
.map(properties -> properties.entrySet().stream()
.collect(Collectors.toMap(Map.Entry::getKey,

View File

@ -7,7 +7,6 @@ import com.tungsten.fclcore.game.Library;
import com.tungsten.fclcore.game.LibraryDownloadInfo;
import com.tungsten.fclcore.util.CacheRepository;
import com.tungsten.fclcore.util.DigestUtils;
import com.tungsten.fclcore.util.Hex;
import com.tungsten.fclcore.util.Logging;
import com.tungsten.fclcore.util.gson.JsonUtils;
import com.tungsten.fclcore.util.gson.TolerableValidationException;
@ -83,7 +82,7 @@ public class DefaultCacheRepository extends CacheRepository {
LibraryDownloadInfo info = library.getDownload();
String hash = info.getSha1();
if (hash != null) {
String checksum = Hex.encodeHex(DigestUtils.digest("SHA-1", jar));
String checksum = DigestUtils.digestToString("SHA-1", jar);
if (hash.equalsIgnoreCase(checksum))
cacheLibrary(library, jar, false);
} else if (library.getChecksums() != null && !library.getChecksums().isEmpty()) {
@ -136,7 +135,7 @@ public class DefaultCacheRepository extends CacheRepository {
if (Files.exists(jar)) {
try {
if (hash != null) {
String checksum = Hex.encodeHex(DigestUtils.digest("SHA-1", jar));
String checksum = DigestUtils.digestToString("SHA-1", jar);
if (hash.equalsIgnoreCase(checksum))
return Optional.of(restore(jar, () -> cacheLibrary(library, jar, false)));
} 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 {
String hash = library.getDownload().getSha1();
if (hash == null)
hash = Hex.encodeHex(DigestUtils.digest(SHA1, path));
hash = DigestUtils.digestToString(SHA1, path);
Path cache = getFile(SHA1, hash);
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 hash;
private final String type;

View File

@ -96,7 +96,7 @@ public final class LibraryAnalyzer implements Iterable<LibraryAnalyzer.LibraryMa
/**
* 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
*/
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) {
Version resolvedVersion = version.resolve(provider);
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 {

View File

@ -1,7 +1,5 @@
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.gson.JsonUtils.fromNonNullJson;
@ -25,6 +23,7 @@ import com.tungsten.fclcore.game.Library;
import com.tungsten.fclcore.game.Version;
import com.tungsten.fclcore.task.FileDownloadTask;
import com.tungsten.fclcore.task.Task;
import com.tungsten.fclcore.util.DigestUtils;
import com.tungsten.fclcore.util.SocketServer;
import com.tungsten.fclcore.util.StringUtils;
import com.tungsten.fclcore.util.function.ExceptionalFunction;
@ -86,7 +85,7 @@ public class ForgeNewInstallTask extends Task<Version> {
if (Files.exists(artifact)) {
String code;
try (InputStream stream = Files.newInputStream(artifact)) {
code = encodeHex(digest("SHA-1", stream));
code = (DigestUtils.digestToString("SHA-1", stream));
}
if (!Objects.equals(code, value)) {
@ -173,7 +172,7 @@ public class ForgeNewInstallTask extends Task<Version> {
String code;
try (InputStream stream = Files.newInputStream(artifact)) {
code = encodeHex(digest("SHA-1", stream));
code = DigestUtils.digestToString("SHA-1", stream);
}
if (!Objects.equals(code, entry.getValue())) {

View File

@ -51,8 +51,7 @@ public class ForgeOldInstallTask extends Task<Version> {
InputStream stream = zipFile.getInputStream(zipFile.getEntry("install_profile.json"));
if (stream == null)
throw new ArtifactMalformedException("Malformed forge installer file, install_profile.json does not exist.");
String json = IOUtils.readFullyAsString(stream);
ForgeInstallProfile installProfile = JsonUtils.fromNonNullJson(json, ForgeInstallProfile.class);
ForgeInstallProfile installProfile = JsonUtils.fromNonNullJsonFully(stream, ForgeInstallProfile.class);
// unpack the universal jar in the installer file.
Library forgeLibrary = new Library(installProfile.getInstall().getPath());

View File

@ -10,7 +10,6 @@ import com.tungsten.fclcore.game.Version;
import com.tungsten.fclcore.task.FileDownloadTask;
import com.tungsten.fclcore.task.Task;
import com.tungsten.fclcore.util.DigestUtils;
import com.tungsten.fclcore.util.Hex;
import com.tungsten.fclcore.util.StringUtils;
import com.tungsten.fclcore.util.gson.JsonUtils;
import com.tungsten.fclcore.util.io.FileUtils;
@ -60,7 +59,7 @@ public final class GameAssetIndexDownloadTask extends Task<Void> {
// verify correctness of file content
if (verifyHashCode) {
try {
String actualSum = Hex.encodeHex(DigestUtils.digest("SHA-1", assetIndexFile));
String actualSum = DigestUtils.digestToString("SHA-1", assetIndexFile);
if (actualSum.equalsIgnoreCase(assetIndexInfo.getSha1()))
return;
} catch (IOException e) {

View File

@ -1,7 +1,5 @@
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 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.FileDownloadTask;
import com.tungsten.fclcore.task.Task;
import com.tungsten.fclcore.util.DigestUtils;
import com.tungsten.fclcore.util.Pack200Utils;
import com.tungsten.fclcore.util.io.FileUtils;
import com.tungsten.fclcore.util.io.IOUtils;
@ -165,7 +164,7 @@ public class LibraryDownloadTask extends Task<Void> {
return true;
}
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")) {
valid = validateJar(fileData, checksums);
}
@ -187,7 +186,7 @@ public class LibraryDownloadTask extends Task<Void> {
hashes = new String(eData, StandardCharsets.UTF_8).split("\n");
}
if (!entry.isDirectory()) {
files.put(entry.getName(), encodeHex(digest("SHA-1", eData)));
files.put(entry.getName(), DigestUtils.digestToString("SHA-1", eData));
}
entry = jar.getNextJarEntry();
}
@ -255,8 +254,6 @@ public class LibraryDownloadTask extends Task<Void> {
jos.closeEntry();
}
if (temp.toFile().exists()) {
Files.delete(temp);
}
}
}

View File

@ -10,8 +10,7 @@ public final class EventBus {
@SuppressWarnings("unchecked")
public <T extends Event> EventManager<T> channel(Class<T> clazz) {
events.putIfAbsent(clazz, new EventManager<>());
return (EventManager<T>) events.get(clazz);
return (EventManager<T>) events.computeIfAbsent(clazz, ignored -> new EventManager<>());
}
@SuppressWarnings("unchecked")

View File

@ -44,7 +44,7 @@ public final class Artifact {
String fileName = this.name + "-" + this.version;
if (classifier != null) fileName += "-" + this.classifier;
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
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");
}
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() {

View File

@ -2,7 +2,6 @@ package com.tungsten.fclcore.game;
import com.google.gson.JsonParseException;
import com.tungsten.fclcore.util.DigestUtils;
import com.tungsten.fclcore.util.Hex;
import com.tungsten.fclcore.util.StringUtils;
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 {
if (hash == null) return defaultValue;
return Hex.encodeHex(DigestUtils.digest("SHA-1", file)).equalsIgnoreCase(hash);
return DigestUtils.digestToString("SHA-1", file).equalsIgnoreCase(hash);
}
}

View File

@ -19,26 +19,30 @@ public final class CrashReportAnalyzer {
public enum Rule {
// 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"),
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.
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.
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))")),
// 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(Pattern.compile("There is insufficient memory for the Java Runtime Environment to continue")),
// Too high resolution
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.
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
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.
NO_SUCH_METHOD_ERROR(Pattern.compile("java\\.lang\\.NoSuchMethodError: (?<class>.*?)"), "class"),
// 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"),
// Forge found some mod crashed in game loading
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
LOADING_CRASHED_FABRIC(Pattern.compile("Could not execute entrypoint stage '(.*?)' due to errors, provided by '(?<id>.*)'!"), "id"),
// 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)")),
// Manually triggerd 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_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
ENTITY(Pattern.compile("Entity Type: (?<type>.*)[\\w\\W\\n\\r]*?Entity's Exact location: (?<location>.*)"), "type", "location"),
// Game crashed when tesselating block model
@ -78,10 +82,30 @@ public final class CrashReportAnalyzer {
// Cannot find native libraries
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
// 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 (.*)"));
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_MODULE_PATTERN = Pattern.compile("\\{(?<tokens>.*)}");
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",
"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",
"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
"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",
"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
));

View File

@ -48,7 +48,7 @@ public class DefaultGameRepository implements GameRepository {
private File baseDirectory;
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) {
this.baseDirectory = baseDirectory;
@ -126,19 +126,13 @@ public class DefaultGameRepository implements GameRepository {
// This implementation may cause multiple flows against the same version entering
// this function, which is accepted because GameVersion::minecraftVersion should
// be consistent.
File versionJar = getVersionJar(version);
if (gameVersions.containsKey(versionJar)) {
return gameVersions.get(versionJar);
} else {
return gameVersions.computeIfAbsent(getVersionJar(version), versionJar -> {
Optional<String> gameVersion = GameVersion.minecraftVersion(versionJar);
if (!gameVersion.isPresent()) {
LOG.warning("Cannot find out game version of " + version.getId() + ", primary jar: " + versionJar.toString() + ", jar exists: " + versionJar.exists());
}
gameVersions.put(versionJar, gameVersion);
return gameVersion;
}
});
}
@Override
@ -168,7 +162,7 @@ public class DefaultGameRepository implements GameRepository {
} 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");
}

View File

@ -3,7 +3,6 @@ package com.tungsten.fclcore.game;
import com.google.gson.JsonParseException;
import com.google.gson.annotations.SerializedName;
import com.tungsten.fclcore.util.DigestUtils;
import com.tungsten.fclcore.util.Hex;
import com.tungsten.fclcore.util.StringUtils;
import com.tungsten.fclcore.util.ToStringBuilder;
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 {
if (getSha1() == null) return defaultValue;
return Hex.encodeHex(DigestUtils.digest("SHA-1", file)).equalsIgnoreCase(getSha1());
return DigestUtils.digestToString("SHA-1", file).equalsIgnoreCase(getSha1());
}
}

View File

@ -175,7 +175,7 @@ public interface GameRepository extends VersionProvider {
* @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 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
*/
Optional<Path> getAssetObject(String version, String assetId, String name) throws IOException;

View File

@ -5,8 +5,6 @@ import static com.tungsten.fclcore.util.Logging.LOG;
import com.google.gson.JsonParseException;
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.ConstantPoolScanner;
@ -15,31 +13,31 @@ import org.jenkinsci.constant_pool_scanner.StringConstant;
import java.io.File;
import java.io.IOException;
import java.nio.file.FileSystem;
import java.nio.file.Files;
import java.nio.file.Path;
import java.io.InputStream;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.logging.Level;
import java.util.stream.Collectors;
import java.util.stream.StreamSupport;
import java.util.zip.ZipEntry;
import java.util.zip.ZipFile;
public final class GameVersion {
private GameVersion() {
}
private static Optional<String> getVersionFromJson(Path versionJson) {
private static Optional<String> getVersionFromJson(InputStream versionJson) {
try {
Map<?, ?> version = JsonUtils.fromNonNullJson(FileUtils.readText(versionJson), Map.class);
return tryCast(version.get("name"), String.class);
Map<?, ?> version = JsonUtils.fromNonNullJsonFully(versionJson, Map.class);
return tryCast(version.get("id"), String.class);
} catch (IOException | JsonParseException e) {
LOG.log(Level.WARNING, "Failed to parse version.json", e);
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);
return StreamSupport.stream(pool.list(StringConstant.class).spliterator(), false)
@ -49,7 +47,7 @@ public final class GameVersion {
.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);
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())
return Optional.empty();
try (FileSystem gameJar = CompressingUtils.createReadOnlyZipFileSystem(file.toPath())) {
Path versionJson = gameJar.getPath("version.json");
if (Files.exists(versionJson)) {
Optional<String> result = getVersionFromJson(versionJson);
try (ZipFile gameJar = new ZipFile(file)) {
ZipEntry versionJson = gameJar.getEntry("version.json");
if (versionJson != null) {
Optional<String> result = getVersionFromJson(gameJar.getInputStream(versionJson));
if (result.isPresent())
return result;
}
Path minecraft = gameJar.getPath("net/minecraft/client/Minecraft.class");
if (Files.exists(minecraft)) {
Optional<String> result = getVersionOfClassMinecraft(Files.readAllBytes(minecraft));
ZipEntry minecraft = gameJar.getEntry("net/minecraft/client/Minecraft.class");
if (minecraft != null) {
try (InputStream is = gameJar.getInputStream(minecraft)) {
Optional<String> result = getVersionOfClassMinecraft(is);
if (result.isPresent())
return result;
}
Path minecraftServer = gameJar.getPath("net/minecraft/server/MinecraftServer.class");
if (Files.exists(minecraftServer))
return getVersionFromClassMinecraftServer(Files.readAllBytes(minecraftServer));
}
ZipEntry minecraftServer = gameJar.getEntry("net/minecraft/server/MinecraftServer.class");
if (minecraftServer != null) {
try (InputStream is = gameJar.getInputStream(minecraftServer)) {
return getVersionFromClassMinecraftServer(is);
}
}
return Optional.empty();
} catch (IOException e) {
return Optional.empty();

View File

@ -18,9 +18,10 @@ public class LaunchOptions implements Serializable {
private String versionName;
private String versionType;
private String profileName;
private List<String> gameArguments = new ArrayList<>();
private List<String> javaArguments = new ArrayList<>();
private List<String> javaAgents = new ArrayList<>(0);
private final List<String> gameArguments = new ArrayList<>();
private final List<String> overrideJavaArguments = new ArrayList<>();
private final List<String> javaArguments = new ArrayList<>();
private final List<String> javaAgents = new ArrayList<>(0);
private Integer minMemory;
private Integer maxMemory;
private Integer metaspace;
@ -76,6 +77,14 @@ public class LaunchOptions implements Serializable {
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.
*/
@ -206,6 +215,13 @@ public class LaunchOptions implements Serializable {
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.
*/
@ -313,6 +329,12 @@ public class LaunchOptions implements Serializable {
return this;
}
public Builder setOverrideJavaArguments(List<String> overrideJavaArguments) {
options.overrideJavaArguments.clear();
options.overrideJavaArguments.addAll(overrideJavaArguments);
return this;
}
public Builder setJavaArguments(List<String> javaArguments) {
options.javaArguments.clear();
options.javaArguments.addAll(javaArguments);

View File

@ -43,7 +43,7 @@ public final class StringArgument implements Argument {
return argument;
}
public class Serializer implements JsonSerializer<StringArgument> {
public static final class Serializer implements JsonSerializer<StringArgument> {
@Override
public JsonElement serialize(StringArgument src, Type typeOfSrc, JsonSerializationContext context) {
return new JsonPrimitive(src.getArgument());

View File

@ -181,7 +181,7 @@ public class World {
throw new IOException();
try (Zipper zipper = new Zipper(zip)) {
zipper.putDirectory(file, "/" + worldName + "/");
zipper.putDirectory(file, worldName);
}
}

View File

@ -250,7 +250,7 @@ public class DefaultLauncher extends Launcher {
public String getInjectorArg() {
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()
.setPrettyPrinting()
.create()

View File

@ -3,7 +3,6 @@ package com.tungsten.fclcore.mod;
import com.google.gson.*;
import com.google.gson.annotations.JsonAdapter;
import com.tungsten.fclcore.util.gson.JsonUtils;
import com.tungsten.fclcore.util.io.CompressingUtils;
import com.tungsten.fclcore.util.io.FileUtils;
import java.io.IOException;
@ -39,8 +38,7 @@ public final class FabricModMetadata {
this.contact = contact;
}
public static LocalModFile fromFile(ModManager modManager, Path modFile) throws IOException, JsonParseException {
try (FileSystem fs = CompressingUtils.createReadOnlyZipFileSystem(modFile)) {
public static LocalModFile fromFile(ModManager modManager, Path modFile, FileSystem fs) throws IOException, JsonParseException {
Path mcmod = fs.getPath("fabric.mod.json");
if (Files.notExists(mcmod))
throw new IOException("File " + modFile + " is not a Fabric mod.");
@ -49,7 +47,6 @@ public final class FabricModMetadata {
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);
}
}
@JsonAdapter(FabricModAuthorSerializer.class)
public static final class FabricModAuthor {

View File

@ -4,10 +4,10 @@ import static com.tungsten.fclcore.util.Logging.LOG;
import com.google.gson.JsonParseException;
import com.moandjiezana.toml.Toml;
import com.tungsten.fclcore.util.io.CompressingUtils;
import com.tungsten.fclcore.util.io.FileUtils;
import java.io.IOException;
import java.io.InputStream;
import java.nio.file.FileSystem;
import java.nio.file.Files;
import java.nio.file.Path;
@ -113,8 +113,7 @@ public final class ForgeNewModMetadata {
}
}
public static LocalModFile fromFile(ModManager modManager, Path modFile) throws IOException, JsonParseException {
try (FileSystem fs = CompressingUtils.createReadOnlyZipFileSystem(modFile)) {
public static LocalModFile fromFile(ModManager modManager, Path modFile, FileSystem fs) throws IOException, JsonParseException {
Path modstoml = fs.getPath("META-INF/mods.toml");
if (Files.notExists(modstoml))
throw new IOException("File " + modFile + " is not a Forge 1.13+ mod.");
@ -125,8 +124,8 @@ public final class ForgeNewModMetadata {
Path manifestMF = fs.getPath("META-INF/MANIFEST.MF");
String jarVersion = "";
if (Files.exists(manifestMF)) {
try {
Manifest manifest = new Manifest(Files.newInputStream(manifestMF));
try (InputStream is = Files.newInputStream(manifestMF)) {
Manifest manifest = new Manifest(is);
jarVersion = manifest.getMainAttributes().getValue(Attributes.Name.IMPLEMENTATION_VERSION);
} catch (IOException e) {
LOG.log(Level.WARNING, "Failed to parse MANIFEST.MF in file " + modFile);
@ -138,4 +137,3 @@ public final class ForgeNewModMetadata {
metadata.getLogoFile());
}
}
}

View File

@ -5,7 +5,6 @@ import com.google.gson.annotations.SerializedName;
import com.google.gson.reflect.TypeToken;
import com.tungsten.fclcore.util.StringUtils;
import com.tungsten.fclcore.util.gson.JsonUtils;
import com.tungsten.fclcore.util.io.CompressingUtils;
import com.tungsten.fclcore.util.io.FileUtils;
import java.io.IOException;
@ -97,8 +96,7 @@ public final class ForgeOldModMetadata {
return authors;
}
public static LocalModFile fromFile(ModManager modManager, Path modFile) throws IOException, JsonParseException {
try (FileSystem fs = CompressingUtils.createReadOnlyZipFileSystem(modFile)) {
public static LocalModFile fromFile(ModManager modManager, Path modFile, FileSystem fs) throws IOException, JsonParseException {
Path mcmod = fs.getPath("mcmod.info");
if (Files.notExists(mcmod))
throw new IOException("File " + modFile + " is not a Forge mod.");
@ -121,4 +119,3 @@ public final class ForgeOldModMetadata {
metadata.getLogoFile());
}
}
}

View File

@ -2,7 +2,6 @@ package com.tungsten.fclcore.mod;
import com.google.gson.JsonParseException;
import com.tungsten.fclcore.util.gson.JsonUtils;
import com.tungsten.fclcore.util.io.IOUtils;
import java.io.IOException;
import java.nio.file.Path;
@ -89,7 +88,7 @@ public final class LiteModMetadata {
ZipEntry entry = zipFile.getEntry("litemod.json");
if (entry == null)
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)
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(),

View File

@ -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 {
Optional<RemoteMod.Version> currentVersion = repository.getRemoteVersionByLocalFile(this, file);
if (!currentVersion.isPresent()) return null;

View File

@ -1,9 +1,7 @@
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.util.DigestUtils;
import com.tungsten.fclcore.util.gson.JsonUtils;
import com.tungsten.fclcore.util.io.CompressingUtils;
import com.tungsten.fclcore.util.io.FileUtils;
@ -52,7 +50,7 @@ public final class MinecraftInstanceTask<T> extends Task<ModpackConfiguration<T>
@Override
public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException {
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;
}
});

View File

@ -2,6 +2,7 @@ package com.tungsten.fclcore.mod;
import com.tungsten.fclcore.util.Lang;
import java.io.File;
import java.util.List;
public interface ModAdviser {
@ -41,21 +42,21 @@ public interface ModAdviser {
"optionsof.txt" /* OptiFine */,
"journeymap" /* JourneyMap */,
"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))
return ModSuggestion.HIDDEN;
return ModAdviser.ModSuggestion.HIDDEN;
if (match(MODPACK_SUGGESTED_BLACK_LIST, fileName, isDirectory))
return ModSuggestion.NORMAL;
return ModAdviser.ModSuggestion.NORMAL;
else
return ModSuggestion.SUGGESTED;
return ModAdviser.ModSuggestion.SUGGESTED;
}
static boolean match(List<String> l, String fileName, boolean isDirectory) {
for (String s : l)
if (isDirectory) {
if (fileName.startsWith(s + "/"))
if (fileName.startsWith(s + File.separator))
return true;
} else {
if (s.startsWith("regex:")) {

View File

@ -4,6 +4,7 @@ public enum ModLoaderType {
UNKNOWN,
FORGE,
FABRIC,
QUILT,
LITE_LOADER,
PACK
}

View File

@ -56,25 +56,28 @@ public final class ModManager {
String fileName = StringUtils.removeSuffix(FileUtils.getName(modFile), DISABLED_EXTENSION, OLD_EXTENSION);
String description;
if (fileName.endsWith(".zip") || fileName.endsWith(".jar")) {
try (FileSystem fs = CompressingUtils.createReadOnlyZipFileSystem(modFile)) {
try {
return ForgeOldModMetadata.fromFile(this, modFile);
return ForgeOldModMetadata.fromFile(this, modFile, fs);
} catch (Exception ignore) {
}
try {
return ForgeNewModMetadata.fromFile(this, modFile);
return ForgeNewModMetadata.fromFile(this, modFile, fs);
} catch (Exception ignore) {
}
try {
return FabricModMetadata.fromFile(this, modFile);
return FabricModMetadata.fromFile(this, modFile, fs);
} catch (Exception ignore) {
}
try {
return PackMcMeta.fromFile(this, modFile);
return PackMcMeta.fromFile(this, modFile, fs);
} catch (Exception ignore) {
}
} catch (Exception ignored) {
}
description = "";
} else if (fileName.endsWith(".litemod")) {
@ -214,7 +217,11 @@ public final class ModManager {
public Path disableMod(Path file) throws IOException {
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))
Files.move(file, disabled, StandardCopyOption.REPLACE_EXISTING);
return disabled;

View File

@ -98,8 +98,7 @@ public abstract class Modpack {
public static boolean acceptFile(String path, List<String> blackList, List<String> whiteList) {
if (path.isEmpty())
return true;
for (String s : blackList)
if (path.equals(s))
if (ModAdviser.match(blackList, path, false))
return false;
if (whiteList == null || whiteList.isEmpty())
return true;

View File

@ -1,9 +1,7 @@
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.util.DigestUtils;
import com.tungsten.fclcore.util.io.FileUtils;
import com.tungsten.fclcore.util.io.Unzipper;
@ -76,7 +74,7 @@ public class ModpackInstallTask<T> extends Task<Void> {
} else {
// 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.
String fileHash = encodeHex(digest("SHA-1", destPath));
String fileHash = DigestUtils.digestToString("SHA-1", destPath);
String oldHash = files.get(entryPath).getHash();
return Objects.equals(oldHash, fileHash);
}

View File

@ -5,7 +5,6 @@ import com.google.gson.annotations.JsonAdapter;
import com.google.gson.annotations.SerializedName;
import com.tungsten.fclcore.util.gson.JsonUtils;
import com.tungsten.fclcore.util.gson.Validation;
import com.tungsten.fclcore.util.io.CompressingUtils;
import com.tungsten.fclcore.util.io.FileUtils;
import java.io.IOException;
@ -124,8 +123,7 @@ public class PackMcMeta implements Validation {
}
}
public static LocalModFile fromFile(ModManager modManager, Path modFile) throws IOException, JsonParseException {
try (FileSystem fs = CompressingUtils.createReadOnlyZipFileSystem(modFile)) {
public static LocalModFile fromFile(ModManager modManager, Path modFile, FileSystem fs) throws IOException, JsonParseException {
Path mcmod = fs.getPath("pack.mcmeta");
if (Files.notExists(mcmod))
throw new IOException("File " + modFile + " is not a resource pack.");
@ -139,4 +137,3 @@ public class PackMcMeta implements Validation {
"", "", "", "", "");
}
}
}

View File

@ -2,6 +2,8 @@ package com.tungsten.fclcore.mod;
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 java.io.IOException;
@ -70,8 +72,18 @@ public class RemoteMod {
}
public enum Type {
CURSEFORGE,
MODRINTH
CURSEFORGE(CurseForgeRemoteModRepository.MODS),
MODRINTH(ModrinthRemoteModRepository.MODS);
private final RemoteModRepository remoteModRepository;
public RemoteModRepository getRemoteModRepository() {
return this.remoteModRepository;
}
Type(RemoteModRepository remoteModRepository) {
this.remoteModRepository = remoteModRepository;
}
}
public interface IMod {

View File

@ -21,10 +21,10 @@ public interface RemoteModRepository {
Type getType();
enum SortType {
DATE_CREATED,
POPULARITY,
LAST_UPDATED,
NAME,
DATE_CREATED,
LAST_UPDATED,
AUTHOR,
TOTAL_DOWNLOADS
}
@ -72,6 +72,8 @@ public interface RemoteModRepository {
}
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.17.1", "1.17",
"1.16.5", "1.16.4", "1.16.3", "1.16.2", "1.16.1", "1.16",

View File

@ -532,15 +532,6 @@ public class CurseAddon implements RemoteMod.IMod {
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(
this,
Integer.toString(modId),
@ -552,7 +543,12 @@ public class CurseAddon implements RemoteMod.IMod {
new RemoteMod.File(Collections.emptyMap(), getDownloadUrl(), getFileName()),
Collections.emptyList(),
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())
);
}
}

View File

@ -24,9 +24,12 @@ import java.util.stream.Stream;
public final class CurseForgeRemoteModRepository implements RemoteModRepository {
private static final String PREFIX = "https://api.curseforge.com";
private static final String apiKey = "$2a$10$qqJ3zZFG5CDsVHk8eV5ft.2ywg2edBtHwS3gzFnw7CDe3X2cKpWZG";
public static boolean isAvailable() {
return true;
}
private final Type type;
private final int section;
@ -72,7 +75,7 @@ public final class CurseForgeRemoteModRepository implements RemoteModRepository
}
@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;
if (category != null) categoryId = ((CurseAddon.Category) category.getSelf()).getId();
Response<List<CurseAddon>> response = HttpRequest.GET(PREFIX + "/v1/mods/search",
@ -159,7 +162,7 @@ public final class CurseForgeRemoteModRepository implements RemoteModRepository
}
@Override
public Stream<Category> getCategories() throws IOException {
public Stream<RemoteModRepository.Category> getCategories() throws IOException {
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_UNKNOWN3 = 4984;
public static final CurseForgeRemoteModRepository MODS = new CurseForgeRemoteModRepository(Type.MOD, SECTION_MOD);
public static final CurseForgeRemoteModRepository MODPACKS = new CurseForgeRemoteModRepository(Type.MODPACK, SECTION_MODPACK);
public static final CurseForgeRemoteModRepository RESOURCE_PACKS = new CurseForgeRemoteModRepository(Type.RESOURCE_PACK, SECTION_RESOURCE_PACK);
public static final CurseForgeRemoteModRepository WORLDS = new CurseForgeRemoteModRepository(Type.WORLD, SECTION_WORLD);
public static final CurseForgeRemoteModRepository CUSTOMIZATIONS = new CurseForgeRemoteModRepository(Type.CUSTOMIZATION, SECTION_CUSTOMIZATION);
public static final CurseForgeRemoteModRepository MODS = new CurseForgeRemoteModRepository(RemoteModRepository.Type.MOD, SECTION_MOD);
public static final CurseForgeRemoteModRepository MODPACKS = new CurseForgeRemoteModRepository(RemoteModRepository.Type.MODPACK, SECTION_MODPACK);
public static final CurseForgeRemoteModRepository RESOURCE_PACKS = new CurseForgeRemoteModRepository(RemoteModRepository.Type.RESOURCE_PACK, SECTION_RESOURCE_PACK);
public static final CurseForgeRemoteModRepository WORLDS = new CurseForgeRemoteModRepository(RemoteModRepository.Type.WORLD, SECTION_WORLD);
public static final CurseForgeRemoteModRepository CUSTOMIZATIONS = new CurseForgeRemoteModRepository(RemoteModRepository.Type.CUSTOMIZATION, SECTION_CUSTOMIZATION);
public static class Pagination {
private final int index;

View File

@ -1,7 +1,5 @@
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.wrapConsumer;
@ -18,6 +16,7 @@ import com.tungsten.fclcore.task.FileDownloadTask;
import com.tungsten.fclcore.task.GetTask;
import com.tungsten.fclcore.task.Task;
import com.tungsten.fclcore.task.TaskCompletableFuture;
import com.tungsten.fclcore.util.DigestUtils;
import com.tungsten.fclcore.util.Logging;
import com.tungsten.fclcore.util.StringUtils;
import com.tungsten.fclcore.util.gson.JsonUtils;
@ -126,7 +125,7 @@ public class McbbsModpackCompletionTask extends CompletableFutureTask<Void> {
} 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.
// 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 newHash = getFileHash(file);
if (oldHash == null) {

View File

@ -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.MINECRAFT;
import static com.tungsten.fclcore.download.LibraryAnalyzer.LibraryType.OPTIFINE;
import static com.tungsten.fclcore.util.DigestUtils.digest;
import static com.tungsten.fclcore.util.Hex.encodeHex;
import static com.tungsten.fclcore.download.LibraryAnalyzer.LibraryType.QUILT;
import com.tungsten.fclcore.download.LibraryAnalyzer;
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.CurseManifestModLoader;
import com.tungsten.fclcore.task.Task;
import com.tungsten.fclcore.util.DigestUtils;
import com.tungsten.fclcore.util.Logging;
import com.tungsten.fclcore.util.StringUtils;
import com.tungsten.fclcore.util.gson.JsonUtils;
@ -62,7 +62,7 @@ public class McbbsModpackExportTask extends Task<Void> {
Path file = runDirectory.resolve(path);
if (Files.isRegularFile(file)) {
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;
} else {
@ -85,6 +85,8 @@ public class McbbsModpackExportTask extends Task<Void> {
addons.add(new McbbsModpackManifest.Addon(OPTIFINE.getPatchId(), optifineVersion)));
analyzer.getVersion(FABRIC).ifPresent(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<>();
// TODO libraries

View File

@ -11,13 +11,13 @@ import com.tungsten.fclcore.mod.ModpackProvider;
import com.tungsten.fclcore.mod.ModpackUpdateTask;
import com.tungsten.fclcore.task.Task;
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.ZipFile;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.nio.charset.Charset;
import java.nio.file.Path;
@ -54,8 +54,8 @@ public final class McbbsModpackProvider implements ModpackProvider {
config.getManifest().injectLaunchOptions(builder);
}
private static Modpack fromManifestFile(String json, Charset encoding) throws IOException, JsonParseException {
McbbsModpackManifest manifest = JsonUtils.fromNonNullJson(json, McbbsModpackManifest.class);
private static Modpack fromManifestFile(InputStream json, Charset encoding) throws IOException, JsonParseException {
McbbsModpackManifest manifest = JsonUtils.fromNonNullJsonFully(json, McbbsModpackManifest.class);
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 {
ZipArchiveEntry mcbbsPackMeta = zip.getEntry("mcbbs.packmeta");
if (mcbbsPackMeta != null) {
return fromManifestFile(IOUtils.readFullyAsString(zip.getInputStream(mcbbsPackMeta)), encoding);
return fromManifestFile(zip.getInputStream(mcbbsPackMeta), encoding);
}
ZipArchiveEntry manifestJson = zip.getEntry("manifest.json");
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");
}

View File

@ -10,7 +10,6 @@ import com.tungsten.fclcore.mod.ModLoaderType;
import com.tungsten.fclcore.mod.RemoteMod;
import com.tungsten.fclcore.mod.RemoteModRepository;
import com.tungsten.fclcore.util.DigestUtils;
import com.tungsten.fclcore.util.Hex;
import com.tungsten.fclcore.util.Lang;
import com.tungsten.fclcore.util.StringUtils;
import com.tungsten.fclcore.util.gson.JsonUtils;
@ -85,7 +84,7 @@ public final class ModrinthRemoteModRepository implements RemoteModRepository {
@Override
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 {
ProjectVersion mod = HttpRequest.GET(PREFIX + "/v2/version_file/" + sha1,
@ -483,6 +482,7 @@ public final class ModrinthRemoteModRepository implements RemoteModRepository {
loaders.stream().flatMap(loader -> {
if ("fabric".equalsIgnoreCase(loader)) return Stream.of(ModLoaderType.FABRIC);
else if ("forge".equalsIgnoreCase(loader)) return Stream.of(ModLoaderType.FORGE);
else if ("quilt".equalsIgnoreCase(loader)) return Stream.of(ModLoaderType.QUILT);
else return Stream.empty();
}).collect(Collectors.toList())
));

View File

@ -2,7 +2,6 @@ package com.tungsten.fclcore.mod.multimc;
import com.google.gson.annotations.SerializedName;
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.ZipFile;
@ -42,8 +41,7 @@ public final class MultiMCManifest {
ZipArchiveEntry mmcPack = zipFile.getEntry(rootEntryName + "mmc-pack.json");
if (mmcPack == null)
return null;
String json = IOUtils.readFullyAsString(zipFile.getInputStream(mmcPack));
MultiMCManifest manifest = JsonUtils.fromNonNullJson(json, MultiMCManifest.class);
MultiMCManifest manifest = JsonUtils.fromNonNullJsonFully(zipFile.getInputStream(mmcPack), MultiMCManifest.class);
if (manifest.getComponents() == null)
throw new IOException("mmc-pack.json malformed.");

View File

@ -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.FORGE;
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.game.DefaultGameRepository;
@ -66,6 +67,8 @@ public class MultiMCModpackExportTask extends Task<Void> {
components.add(new MultiMCManifest.MultiMCManifestComponent(false, false, "com.mumfrey.liteloader", liteLoaderVersion)));
analyzer.getVersion(FABRIC).ifPresent(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);
zip.putTextFile(JsonUtils.GSON.toJson(mmcPack), "mmc-pack.json");

View File

@ -69,7 +69,7 @@ public final class MultiMCModpackInstallTask extends Task<Void> {
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 -> {
if (c.getVersion() != null)
builder.version("quilt", c.getVersion());

View File

@ -1,8 +1,5 @@
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.reflect.TypeToken;
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.GetTask;
import com.tungsten.fclcore.task.Task;
import com.tungsten.fclcore.util.DigestUtils;
import com.tungsten.fclcore.util.Logging;
import com.tungsten.fclcore.util.StringUtils;
import com.tungsten.fclcore.util.gson.JsonUtils;
@ -132,7 +130,7 @@ public class ServerModpackCompletionTask extends Task<Void> {
download = true;
} else {
// 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();
download = !Objects.equals(oldHash, file.getHash()) && Objects.equals(oldHash, fileHash);
}

View File

@ -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.MINECRAFT;
import static com.tungsten.fclcore.download.LibraryAnalyzer.LibraryType.OPTIFINE;
import static com.tungsten.fclcore.util.DigestUtils.digest;
import static com.tungsten.fclcore.util.Hex.encodeHex;
import static com.tungsten.fclcore.download.LibraryAnalyzer.LibraryType.QUILT;
import com.tungsten.fclcore.download.LibraryAnalyzer;
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.ModpackExportInfo;
import com.tungsten.fclcore.task.Task;
import com.tungsten.fclcore.util.DigestUtils;
import com.tungsten.fclcore.util.Logging;
import com.tungsten.fclcore.util.StringUtils;
import com.tungsten.fclcore.util.gson.JsonUtils;
@ -58,7 +58,7 @@ public class ServerModpackExportTask extends Task<Void> {
Path file = runDirectory.resolve(path);
if (Files.isRegularFile(file)) {
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;
} else {
@ -79,6 +79,8 @@ public class ServerModpackExportTask extends Task<Void> {
addons.add(new ServerModpackManifest.Addon(OPTIFINE.getPatchId(), optifineVersion)));
analyzer.getVersion(FABRIC).ifPresent(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);
zip.putTextFile(JsonUtils.GSON.toJson(manifest), "server-manifest.json");
}

View File

@ -222,7 +222,7 @@ public abstract class FetchTask<T> extends Task<T> {
CACHED
}
protected class DownloadState {
protected static final class DownloadState {
private final int startPosition;
private final int endPosition;
private final int currentPosition;
@ -255,9 +255,7 @@ public abstract class FetchTask<T> extends Task<T> {
}
}
protected class DownloadMission {
protected static final class DownloadMission {
}

View File

@ -5,7 +5,6 @@ import static com.tungsten.fclcore.util.DigestUtils.getDigest;
import java.io.File;
import java.io.IOException;
import java.io.RandomAccessFile;
import java.math.BigInteger;
import java.net.URL;
import java.net.URLConnection;
import java.nio.file.FileSystem;
@ -17,6 +16,7 @@ import java.util.logging.Level;
import static java.util.Objects.requireNonNull;
import com.tungsten.fclcore.util.Hex;
import com.tungsten.fclcore.util.Logging;
import com.tungsten.fclcore.util.io.ChecksumMismatchException;
import com.tungsten.fclcore.util.io.CompressingUtils;
@ -54,7 +54,7 @@ public class FileDownloadTask extends FetchTask<Void> {
}
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)) {
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) -> {
String ext = FileUtils.getExtension(destinationPath).toLowerCase();
String ext = FileUtils.getExtension(destinationPath).toLowerCase(Locale.ROOT);
if (ext.equals("zip") || ext.equals("jar")) {
try (FileSystem ignored = CompressingUtils.createReadOnlyZipFileSystem(filePath)) {
// test for zip format

View File

@ -2,7 +2,6 @@ package com.tungsten.fclcore.task;
import static com.tungsten.fclcore.util.Lang.threadPool;
import android.app.Activity;
import android.os.Handler;
import android.os.Looper;

View File

@ -690,7 +690,7 @@ public abstract class Task<T> {
@Override
public void execute() throws Exception {
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());

View File

@ -12,9 +12,9 @@ import com.tungsten.fclcore.util.io.IOUtils;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.OutputStream;
import java.io.RandomAccessFile;
import java.net.URLConnection;
import java.nio.ByteBuffer;
import java.nio.channels.Channels;
import java.nio.channels.FileChannel;
import java.nio.channels.FileLock;
@ -38,7 +38,7 @@ public class CacheRepository {
private Path cacheDirectory;
private Path indexFile;
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();
public void changeDirectory(Path commonDir) {
@ -95,7 +95,7 @@ public class CacheRepository {
Path file = getFile(algorithm, hash);
if (Files.exists(file)) {
try {
return Hex.encodeHex(DigestUtils.digest(algorithm, file)).equalsIgnoreCase(hash);
return DigestUtils.digestToString(algorithm, file).equalsIgnoreCase(hash);
} catch (IOException e) {
return false;
}
@ -123,7 +123,7 @@ public class CacheRepository {
if (original != null && Files.exists(original)) {
if (hash != null) {
try {
String checksum = Hex.encodeHex(DigestUtils.digest(algorithm, original));
String checksum = DigestUtils.digestToString(algorithm, original);
if (checksum.equalsIgnoreCase(hash))
return Optional.of(restore(original, () -> cacheFile(original, algorithm, hash)));
} catch (IOException e) {
@ -157,7 +157,7 @@ public class CacheRepository {
if (StringUtils.isBlank(eTagItem.hash) || !fileExists(SHA1, eTagItem.hash)) throw new FileNotFoundException();
Path file = getFile(SHA1, eTagItem.hash);
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))
throw new IOException("This file is modified");
}
@ -192,7 +192,7 @@ public class CacheRepository {
public void cacheRemoteFile(Path downloaded, URLConnection conn) throws IOException {
cacheData(() -> {
String hash = Hex.encodeHex(DigestUtils.digest(SHA1, downloaded));
String hash = DigestUtils.digestToString(SHA1, downloaded);
Path cached = cacheFile(downloaded, SHA1, hash);
return new CacheResult(hash, cached);
}, conn);
@ -204,7 +204,7 @@ public class CacheRepository {
public void cacheBytes(byte[] bytes, URLConnection conn) throws IOException {
cacheData(() -> {
String hash = Hex.encodeHex(DigestUtils.digest(SHA1, bytes));
String hash = DigestUtils.digestToString(SHA1, bytes);
Path cached = getFile(SHA1, hash);
FileUtils.writeBytes(cached.toFile(), bytes);
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);
Map<String, ETagItem> newIndex = joinETagIndexes(indexOnDisk == null ? null : indexOnDisk.eTag, index.values());
channel.truncate(0);
OutputStream os = Channels.newOutputStream(channel);
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;
} finally {
lock.release();
@ -287,7 +286,7 @@ public class CacheRepository {
}
}
private class ETagIndex {
private static final class ETagIndex {
private final Collection<ETagItem> eTag;
public ETagIndex() {
@ -299,7 +298,7 @@ public class CacheRepository {
}
}
private class ETagItem {
private static final class ETagItem {
private final String url;
private final String eTag;
private final String hash;
@ -413,8 +412,7 @@ public class CacheRepository {
if (indexOnDisk == null) indexOnDisk = new HashMap<>();
indexOnDisk.putAll(storage);
channel.truncate(0);
OutputStream os = Channels.newOutputStream(channel);
IOUtils.write(JsonUtils.GSON.toJson(storage).getBytes(UTF_8), os);
channel.write(ByteBuffer.wrap(JsonUtils.GSON.toJson(storage).getBytes(UTF_8)));
this.storage = indexOnDisk;
} finally {
lock.release();

View File

@ -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) {
return getDigest(algorithm).digest(data);
}
@ -46,8 +42,22 @@ public final class DigestUtils {
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 {
byte[] buffer = new byte[STREAM_BUFFER_LENGTH];
byte[] buffer = threadLocalBuffer.get();
int read = data.read(buffer, 0, STREAM_BUFFER_LENGTH);
while (read > -1) {

View File

@ -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 + "]";
}
}

View File

@ -17,6 +17,18 @@ public final class 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.
* @param pairs entries in the new map
@ -349,6 +361,13 @@ public final class Lang {
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;
public static synchronized Timer getTimer() {

View File

@ -7,7 +7,10 @@ import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.text.MessageFormat;
import java.util.Arrays;
import java.util.Date;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
@ -21,24 +24,24 @@ public final class Logging {
public static final Logger LOG = Logger.getLogger("FCL");
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) {
forbiddenTokens.put(token, replacement);
}
public static synchronized void registerAccessToken(String token) {
final String[] oldAccessTokens = accessTokens;
final String[] newAccessTokens = Arrays.copyOf(oldAccessTokens, oldAccessTokens.length + 1);
public static void registerAccessToken(String accessToken) {
registerForbiddenToken(accessToken, "<access token>");
newAccessTokens[oldAccessTokens.length] = token;
accessTokens = newAccessTokens;
}
public static String filterForbiddenToken(String message) {
for (Map.Entry<String, String> entry : forbiddenTokens.entrySet()) {
message = message.replace(entry.getKey(), entry.getValue());
}
for (String token : accessTokens)
message = message.replace(token, "<access token>");
return message;
}
public static void start(File logFolder) {
public static void start(Path logFolder) {
LOG.setLevel(Level.ALL);
LOG.setUseParentHandlers(false);
LOG.setFilter(record -> {
@ -47,14 +50,17 @@ public final class Logging {
});
try {
FileUtils.makeDirectory(logFolder);
FileHandler fileHandler = new FileHandler(logFolder + "/fcl.log");
if (Files.isRegularFile(logFolder))
Files.delete(logFolder);
Files.createDirectories(logFolder);
FileHandler fileHandler = new FileHandler(logFolder.resolve("fcl.log").toAbsolutePath().toString());
fileHandler.setLevel(Level.FINEST);
fileHandler.setFormatter(DefaultFormatter.INSTANCE);
fileHandler.setEncoding("UTF-8");
LOG.addHandler(fileHandler);
} 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();
@ -96,7 +102,7 @@ public final class Logging {
try {
return storedLogs.toString("UTF-8");
} catch (UnsupportedEncodingException e) {
return e.getMessage();
throw new InternalError(e);
}
}

View File

@ -1,28 +1,11 @@
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;
public final class 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.
*

View File

@ -2,10 +2,7 @@ package com.tungsten.fclcore.util;
import java.io.PrintWriter;
import java.io.StringWriter;
import java.nio.charset.CharsetEncoder;
import java.nio.charset.StandardCharsets;
import java.util.*;
import java.util.regex.Pattern;
public final class StringUtils {
@ -129,6 +126,10 @@ public final class StringUtils {
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) {
for (String prefix : prefixes)
if (str.startsWith(prefix))
@ -136,6 +137,10 @@ public final class StringUtils {
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.
*/
@ -147,24 +152,29 @@ public final class StringUtils {
}
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)
if (pattern.toLowerCase().contains(target.toLowerCase()))
if (lowerPattern.contains(target.toLowerCase(Locale.ROOT)))
return true;
}
return false;
}
public static boolean containsOne(String pattern, String... targets) {
String lowerPattern = pattern.toLowerCase(Locale.ROOT);
for (String target : targets)
if (pattern.toLowerCase().contains(target.toLowerCase()))
if (lowerPattern.contains(target.toLowerCase(Locale.ROOT)))
return true;
return false;
}
public static boolean containsOne(String pattern, char... targets) {
for (char target : targets)
if (pattern.toLowerCase().indexOf(Character.toLowerCase(target)) >= 0)
public static boolean containsChinese(String str) {
for (int i = 0; i < str.length(); i++) {
char ch = str.charAt(i);
if (ch >= '\u4e00' && ch <= '\u9fa5')
return true;
}
return false;
}
@ -187,7 +197,10 @@ public final class StringUtils {
}
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) {
@ -216,8 +229,32 @@ public final class StringUtils {
return result.toString();
}
public static boolean isASCII(CharSequence cs) {
return US_ASCII_ENCODER.canEncode(cs);
public static int MAX_SHORT_STRING_LENGTH = 77;
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()];
}
}
public static final Pattern CHINESE_PATTERN = Pattern.compile("[\\u4e00-\\u9fa5]");
public static final CharsetEncoder US_ASCII_ENCODER = StandardCharsets.US_ASCII.newEncoder();
}

View File

@ -6,7 +6,11 @@ import com.google.gson.JsonParseException;
import com.google.gson.JsonSyntaxException;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.lang.reflect.Type;
import java.nio.charset.StandardCharsets;
import java.time.Instant;
import java.util.Date;
import java.util.UUID;
@ -24,6 +28,18 @@ public final class 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 {
T parsed = GSON.fromJson(json, classOfT);
if (parsed == null)
@ -38,6 +54,24 @@ public final class JsonUtils {
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 {
try {
return GSON.fromJson(json, classOfT);

View File

@ -42,12 +42,12 @@ public final class LowerCaseEnumTypeAdapterFactory implements TypeAdapterFactory
reader.nextNull();
return null;
}
return lowercaseToConstant.get(reader.nextString().toLowerCase());
return lowercaseToConstant.get(reader.nextString().toLowerCase(Locale.ROOT));
}
};
}
private static String toLowercase(Object o) {
return o.toString().toLowerCase(Locale.US);
return o.toString().toLowerCase(Locale.ROOT);
}
}

View File

@ -7,6 +7,7 @@ import com.google.gson.stream.JsonWriter;
import java.io.IOException;
import java.util.UUID;
import java.util.regex.Pattern;
public final class UUIDTypeAdapter extends TypeAdapter<UUID> {
@ -30,8 +31,10 @@ public final class UUIDTypeAdapter extends TypeAdapter<UUID> {
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) {
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"));
}
}

View File

@ -2,7 +2,6 @@ package com.tungsten.fclcore.util.io;
import com.tungsten.fclcore.download.ArtifactMalformedException;
import com.tungsten.fclcore.util.DigestUtils;
import com.tungsten.fclcore.util.Hex;
import java.io.IOException;
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 {
String checksum = Hex.encodeHex(DigestUtils.digest(algorithm, file));
String checksum = DigestUtils.digestToString(algorithm, file);
if (!checksum.equalsIgnoreCase(expectedChecksum)) {
throw new ChecksumMismatchException(algorithm, expectedChecksum, checksum);
}

View File

@ -10,10 +10,11 @@ import java.net.HttpURLConnection;
import static java.nio.charset.StandardCharsets.UTF_8;
public class HttpMultipartRequest implements Closeable {
private static final String endl = "\r\n";
private final String boundary = "*****" + System.currentTimeMillis() + "*****";
private final HttpURLConnection urlConnection;
private final ByteArrayOutputStream stream;
private final String endl = "\r\n";
public HttpMultipartRequest(HttpURLConnection urlConnection) throws IOException {
this.urlConnection = urlConnection;
@ -52,7 +53,7 @@ public class HttpMultipartRequest implements Closeable {
addLine("--" + boundary + "--");
urlConnection.setRequestProperty("Content-Length", "" + stream.size());
try (OutputStream os = urlConnection.getOutputStream()) {
IOUtils.write(stream.toByteArray(), os);
stream.writeTo(os);
}
}
}

View File

@ -20,7 +20,6 @@ import java.net.HttpURLConnection;
import java.net.MalformedURLException;
import java.net.SocketTimeoutException;
import java.net.URL;
import java.nio.charset.StandardCharsets;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
@ -127,7 +126,7 @@ public abstract class HttpRequest {
return getStringWithRetry(() -> {
HttpURLConnection con = createConnection();
con = resolveConnection(con);
return IOUtils.readFullyAsString(con.getInputStream(), StandardCharsets.UTF_8);
return IOUtils.readFullyAsString(con.getInputStream());
}, retryTimes);
}
}

View File

@ -1,7 +1,6 @@
package com.tungsten.fclcore.util.io;
import java.io.*;
import java.nio.charset.Charset;
/**
* 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.
*/
public static byte[] readFullyWithoutClosing(InputStream stream) throws IOException {
ByteArrayOutputStream result = new ByteArrayOutputStream();
ByteArrayOutputStream result = new ByteArrayOutputStream(Math.max(stream.available(), 32));
copyTo(stream, result);
return result.toByteArray();
}
@ -35,7 +34,7 @@ public final class IOUtils {
*/
public static ByteArrayOutputStream readFully(InputStream stream) throws IOException {
try (InputStream is = stream) {
ByteArrayOutputStream result = new ByteArrayOutputStream();
ByteArrayOutputStream result = new ByteArrayOutputStream(Math.max(is.available(), 32));
copyTo(is, result);
return result;
}
@ -49,18 +48,6 @@ public final class IOUtils {
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 {
copyTo(src, dest, new byte[DEFAULT_BUFFER_SIZE]);
}

View File

@ -6,9 +6,7 @@ import java.nio.file.FileSystemNotFoundException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.security.CodeSource;
import java.util.Optional;
import java.util.jar.Attributes;
import java.util.jar.JarFile;
import java.util.jar.Manifest;
@ -17,22 +15,33 @@ public final class JarUtils {
}
@SuppressWarnings("OptionalUsedAsFieldOrParameterType")
private static final Optional<Path> THIS_JAR =
Optional.ofNullable(JarUtils.class.getProtectionDomain().getCodeSource())
.map(CodeSource::getLocation)
.map(url -> {
private static final Optional<Path> THIS_JAR;
private static final Manifest manifest;
static {
THIS_JAR = Optional.ofNullable(JarUtils.class.getProtectionDomain().getCodeSource())
.map(codeSource -> {
try {
return Paths.get(url.toURI());
return Paths.get(codeSource.getLocation().toURI());
} catch (FileSystemNotFoundException | IllegalArgumentException | URISyntaxException e) {
return null;
}
})
.filter(Files::isRegularFile);
manifest = THIS_JAR.flatMap(JarUtils::getManifest).orElseGet(Manifest::new);
}
public static Optional<Path> thisJar() {
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) {
try (JarFile file = new JarFile(jar.toFile())) {
return Optional.ofNullable(file.getManifest());
@ -40,9 +49,4 @@ public final class JarUtils {
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)));
}
}

View File

@ -7,7 +7,6 @@ import static com.tungsten.fclcore.util.StringUtils.substringAfterLast;
import java.io.*;
import java.net.*;
import java.nio.charset.StandardCharsets;
import java.util.*;
import java.util.Map.Entry;
@ -158,13 +157,13 @@ public final class NetworkUtils {
public static String doGet(URL url) throws IOException {
HttpURLConnection con = createHttpConnection(url);
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 {
StringBuilder sb = new StringBuilder();
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.deleteCharAt(sb.length() - 1);
}
@ -192,13 +191,13 @@ public final class NetworkUtils {
public static String readData(HttpURLConnection con) throws IOException {
try {
try (InputStream stdout = con.getInputStream()) {
return IOUtils.readFullyAsString(stdout, UTF_8);
return IOUtils.readFullyAsString(stdout);
}
} catch (IOException e) {
try (InputStream stderr = con.getErrorStream()) {
if (stderr == null)
throw e;
return IOUtils.readFullyAsString(stderr, UTF_8);
return IOUtils.readFullyAsString(stderr);
}
}
}

View File

@ -16,8 +16,6 @@ import java.util.zip.ZipOutputStream;
/**
* Non thread-safe
*
* @author huangyuhui
*/
public final class Zipper implements Closeable {

View File

@ -2,8 +2,6 @@ package com.tungsten.fclcore.util.platform;
import static com.tungsten.fclcore.util.Logging.LOG;
import com.tungsten.fclcore.util.StringUtils;
import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.util.*;
@ -20,7 +18,6 @@ public final class CommandBuilder {
private final List<Item> raw = new ArrayList<>();
public CommandBuilder() {
}
private String parse(String s) {
@ -57,28 +54,97 @@ public final class CommandBuilder {
return this;
}
public String addDefault(String opt) {
public void addAllDefault(Collection<String> args) {
addAllDefault(args, true);
}
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.equals(opt)) {
return item.arg;
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(opt, true));
return null;
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));
}
}
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) {
if (item.arg.startsWith(opt)) {
LOG.info("Default option '" + opt + value + "' is suppressed by '" + item.arg + "'");
return item.arg;
}
}
raw.add(new Item(opt + value, true));
raw.add(new Item(opt + value, parse));
return null;
}
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) {
final Matcher matcher = UNSTABLE_BOOLEAN_OPTION_PATTERN.matcher(item.arg);
if (matcher.matches()) {
@ -89,14 +155,18 @@ public final class CommandBuilder {
}
if (value) {
raw.add(new Item("-XX:+" + opt, true));
raw.add(new Item("-XX:+" + opt, parse));
} else {
raw.add(new Item("-XX:-" + opt, true));
raw.add(new Item("-XX:-" + opt, parse));
}
return null;
}
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) {
final Matcher matcher = UNSTABLE_OPTION_PATTERN.matcher(item.arg);
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;
}
@ -114,6 +184,10 @@ public final class CommandBuilder {
return raw.removeIf(i -> pred.test(i.arg));
}
public boolean noneMatch(Predicate<String> predicate) {
return raw.stream().noneMatch(it -> predicate.test(it.arg));
}
@Override
public String toString() {
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 {
String arg;
boolean parse;
final String arg;
final boolean parse;
Item(String arg, boolean parse) {
this.arg = arg;
@ -147,22 +221,53 @@ public final class CommandBuilder {
}
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() {
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;
}
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) {
String escaping = " \t\"!#$&'()*,;<=>?[\\]^`{|}~";
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;
return containsEscape(s, " \t\"!#$&'()*,;<=>?[\\]^`{|}~") ? '"' + escape(s, '"', '$', '&', '`') + '"' : s;
}
}

View File

@ -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;
}
}

View File

@ -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;
}
}

View File

@ -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;
}
}

View File

@ -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;
}
}
}

View File

@ -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));
}
}

View File

@ -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());
}
}

View File

@ -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);
}
}

View File

@ -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);
}
}

View File

@ -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);
}
}

View File

@ -26,8 +26,8 @@ public class NormalizedSkin {
this.texture = texture;
// check format
int w = texture.getWidth();
int h = texture.getHeight();
int w = (int) texture.getWidth();
int h = (int) texture.getHeight();
if (w % 64 != 0) {
throw new InvalidSkinException("Invalid size " + w + "x" + h);
}
@ -84,10 +84,6 @@ public class NormalizedSkin {
return oldFormat;
}
/**
* Tests whether the skin is slim.
* Note that this method doesn't guarantee the result is correct.
*/
public boolean isSlim() {
return (hasTransparencyRelative(50, 16, 2, 4) ||
hasTransparencyRelative(54, 20, 2, 12) ||

Some files were not shown because too many files have changed in this diff Show More