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.JavaVersion;
import com.tungsten.fclcore.game.LaunchOptions; import com.tungsten.fclcore.game.LaunchOptions;
import com.tungsten.fclcore.game.Version; import com.tungsten.fclcore.game.Version;
import com.tungsten.fclcore.mod.ModAdviser;
import com.tungsten.fclcore.mod.Modpack; import com.tungsten.fclcore.mod.Modpack;
import com.tungsten.fclcore.mod.ModpackConfiguration; import com.tungsten.fclcore.mod.ModpackConfiguration;
import com.tungsten.fclcore.mod.ModpackProvider; import com.tungsten.fclcore.mod.ModpackProvider;
@ -158,17 +159,7 @@ public class FCLGameRepository extends DefaultGameRepository {
File srcGameDir = getRunDirectory(srcId); File srcGameDir = getRunDirectory(srcId);
File dstGameDir = getRunDirectory(dstId); File dstGameDir = getRunDirectory(dstId);
List<String> blackList = new ArrayList<>(Arrays.asList( List<String> blackList = new ArrayList<>(ModAdviser.MODPACK_BLACK_LIST);
"regex:(.*?)\\.log",
"usernamecache.json", "usercache.json", // Minecraft
"launcher_profiles.json", "launcher.pack.lzma", // Minecraft Launcher
"backup", "pack.json", "launcher.jar", "cache", // FCL
".curseclient", // Curse
".fabric", ".mixin.out", // Fabric
"jars", "logs", "versions", "assets", "libraries", "crash-reports", "NVIDIA", "AMD", "screenshots", "natives", "native", "$native", "server-resource-packs", // Minecraft
"downloads", // Curse
"asm", "backups", "TCNodeTracker", "CustomDISkins", "data", "CustomSkinLoader/caches" // Mods
));
blackList.add(srcId + ".jar"); blackList.add(srcId + ".jar");
blackList.add(srcId + ".json"); blackList.add(srcId + ".json");
if (!copySaves) if (!copySaves)
@ -324,8 +315,8 @@ public class FCLGameRepository extends DefaultGameRepository {
.setVersionName(version) .setVersionName(version)
.setProfileName(FCLPath.CONTEXT.getString(R.string.app_name)) .setProfileName(FCLPath.CONTEXT.getString(R.string.app_name))
.setGameArguments(StringUtils.tokenize(vs.getMinecraftArgs())) .setGameArguments(StringUtils.tokenize(vs.getMinecraftArgs()))
.setJavaArguments(StringUtils.tokenize(vs.getJavaArgs())) .setOverrideJavaArguments(StringUtils.tokenize(vs.getJavaArgs()))
.setMaxMemory((int)(getAllocatedMemory( .setMaxMemory(vs.isAutoMemory() ? null : (int)(getAllocatedMemory(
vs.getMaxMemory() * 1024L * 1024L, vs.getMaxMemory() * 1024L * 1024L,
MemoryUtils.getFreeDeviceMemory(FCLPath.CONTEXT), MemoryUtils.getFreeDeviceMemory(FCLPath.CONTEXT),
vs.isAutoMemory() vs.isAutoMemory()
@ -352,6 +343,9 @@ public class FCLGameRepository extends DefaultGameRepository {
} }
} }
if (vs.isAutoMemory() && builder.getJavaArguments().stream().anyMatch(it -> it.startsWith("-Xmx")))
builder.setMaxMemory(null);
return builder.create(); return builder.create();
} }
@ -409,7 +403,7 @@ public class FCLGameRepository extends DefaultGameRepository {
public static long getAllocatedMemory(long minimum, long available, boolean auto) { public static long getAllocatedMemory(long minimum, long available, boolean auto) {
if (auto) { if (auto) {
available -= 256 * 1024 * 1024; available -= 384 * 1024 * 1024; // Reserve 384MiB memory for off-heap memory and HMCL itself
if (available <= 0) { if (available <= 0) {
return minimum; return minimum;
} }
@ -418,7 +412,7 @@ public class FCLGameRepository extends DefaultGameRepository {
final long suggested = Math.min(available <= threshold final long suggested = Math.min(available <= threshold
? (long) (available * 0.8) ? (long) (available * 0.8)
: (long) (threshold * 0.8 + (available - threshold) * 0.2), : (long) (threshold * 0.8 + (available - threshold) * 0.2),
32736L * 1024 * 1024); // Limit the maximum suggested memory to ensure that compressed oops are available 16384L * 1024 * 1024);
return Math.max(minimum, suggested); return Math.max(minimum, suggested);
} else { } else {
return minimum; return minimum;

View File

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

View File

@ -41,12 +41,17 @@ public final class LogExporter {
return CompletableFuture.runAsync(() -> { return CompletableFuture.runAsync(() -> {
try (Zipper zipper = new Zipper(zipFile)) { try (Zipper zipper = new Zipper(zipFile)) {
if (Files.exists(runDirectory.resolve("logs").resolve("debug.log"))) { Path logsDir = runDirectory.resolve("logs");
zipper.putFile(runDirectory.resolve("logs").resolve("debug.log"), "debug.log"); if (Files.exists(logsDir.resolve("debug.log"))) {
zipper.putFile(logsDir.resolve("debug.log"), "debug.log");
} }
if (Files.exists(runDirectory.resolve("logs").resolve("latest.log"))) { if (Files.exists(logsDir.resolve("latest.log"))) {
zipper.putFile(runDirectory.resolve("logs").resolve("latest.log"), "latest.log"); zipper.putFile(logsDir.resolve("latest.log"), "latest.log");
} }
if (Files.exists(logsDir.resolve("fml-client-latest.log"))) {
zipper.putFile(logsDir.resolve("fml-client-latest.log"), "fml-client-latest.log");
}
zipper.putTextFile(Logging.getLogs(), "fcl.log"); zipper.putTextFile(Logging.getLogs(), "fcl.log");
zipper.putTextFile(logs, "minecraft.log"); zipper.putTextFile(logs, "minecraft.log");

View File

@ -18,7 +18,6 @@ import com.tungsten.fclcore.util.io.NetworkUtils;
import fi.iki.elonen.NanoHTTPD; import fi.iki.elonen.NanoHTTPD;
import java.io.IOException; import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.HashMap; import java.util.HashMap;
import java.util.Map; import java.util.Map;
import java.util.concurrent.CompletableFuture; import java.util.concurrent.CompletableFuture;
@ -88,7 +87,7 @@ public final class OAuthServer extends NanoHTTPD implements OAuth.Session {
String html; String html;
try { try {
html = IOUtils.readFullyAsString(OAuthServer.class.getResourceAsStream("/assets/microsoft_auth.html"), StandardCharsets.UTF_8) html = IOUtils.readFullyAsString(OAuthServer.class.getResourceAsStream("/assets/microsoft_auth.html"))
.replace("%close-page%", FCLPath.CONTEXT.getString(R.string.account_methods_microsoft_close_page)); .replace("%close-page%", FCLPath.CONTEXT.getString(R.string.account_methods_microsoft_close_page));
} catch (IOException e) { } catch (IOException e) {
Logging.LOG.log(Level.SEVERE, "Failed to load html"); Logging.LOG.log(Level.SEVERE, "Failed to load html");

View File

@ -10,7 +10,11 @@ import static com.tungsten.fclcore.util.Pair.pair;
import java.io.File; import java.io.File;
import java.io.IOException; import java.io.IOException;
import java.io.Reader;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths; import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.HashMap; import java.util.HashMap;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
@ -21,6 +25,7 @@ import static java.util.stream.Collectors.toList;
import android.content.Context; import android.content.Context;
import com.google.gson.reflect.TypeToken;
import com.tungsten.fcl.R; import com.tungsten.fcl.R;
import com.tungsten.fcl.game.OAuthServer; import com.tungsten.fcl.game.OAuthServer;
import com.tungsten.fclauncher.FCLPath; import com.tungsten.fclauncher.FCLPath;
@ -49,13 +54,16 @@ import com.tungsten.fclcore.auth.offline.OfflineAccountFactory;
import com.tungsten.fclcore.auth.yggdrasil.RemoteAuthenticationException; import com.tungsten.fclcore.auth.yggdrasil.RemoteAuthenticationException;
import com.tungsten.fclcore.auth.yggdrasil.YggdrasilAccount; import com.tungsten.fclcore.auth.yggdrasil.YggdrasilAccount;
import com.tungsten.fclcore.auth.yggdrasil.YggdrasilAccountFactory; import com.tungsten.fclcore.auth.yggdrasil.YggdrasilAccountFactory;
import com.tungsten.fclcore.fakefx.beans.InvalidationListener;
import com.tungsten.fclcore.fakefx.beans.Observable; import com.tungsten.fclcore.fakefx.beans.Observable;
import com.tungsten.fclcore.fakefx.beans.property.ObjectProperty; import com.tungsten.fclcore.fakefx.beans.property.ObjectProperty;
import com.tungsten.fclcore.fakefx.beans.property.ReadOnlyListProperty;
import com.tungsten.fclcore.fakefx.beans.property.ReadOnlyListWrapper;
import com.tungsten.fclcore.fakefx.beans.property.SimpleObjectProperty; import com.tungsten.fclcore.fakefx.beans.property.SimpleObjectProperty;
import com.tungsten.fclcore.fakefx.collections.FXCollections;
import com.tungsten.fclcore.fakefx.collections.ObservableList; import com.tungsten.fclcore.fakefx.collections.ObservableList;
import com.tungsten.fclcore.task.Schedulers; import com.tungsten.fclcore.task.Schedulers;
import com.tungsten.fclcore.util.InvocationDispatcher;
import com.tungsten.fclcore.util.Lang;
import com.tungsten.fclcore.util.io.FileUtils;
import com.tungsten.fclcore.util.skin.InvalidSkinException; import com.tungsten.fclcore.util.skin.InvalidSkinException;
public final class Accounts { public final class Accounts {
@ -80,7 +88,6 @@ public final class Accounts {
public static final YggdrasilAccountFactory FACTORY_MOJANG = YggdrasilAccountFactory.MOJANG; public static final YggdrasilAccountFactory FACTORY_MOJANG = YggdrasilAccountFactory.MOJANG;
public static final AuthlibInjectorAccountFactory FACTORY_AUTHLIB_INJECTOR = new AuthlibInjectorAccountFactory(AUTHLIB_INJECTOR_DOWNLOADER, Accounts::getOrCreateAuthlibInjectorServer); public static final AuthlibInjectorAccountFactory FACTORY_AUTHLIB_INJECTOR = new AuthlibInjectorAccountFactory(AUTHLIB_INJECTOR_DOWNLOADER, Accounts::getOrCreateAuthlibInjectorServer);
public static final MicrosoftAccountFactory FACTORY_MICROSOFT = new MicrosoftAccountFactory(new MicrosoftService(OAUTH_CALLBACK)); public static final MicrosoftAccountFactory FACTORY_MICROSOFT = new MicrosoftAccountFactory(new MicrosoftService(OAUTH_CALLBACK));
public static final BoundAuthlibInjectorAccountFactory FACTORY_LITTLE_SKIN = getAccountFactoryByAuthlibInjectorServer(new AuthlibInjectorServer("https://littleskin.cn/api/yggdrasil/"));
public static final List<AccountFactory<?>> FACTORIES = immutableListOf(FACTORY_OFFLINE, FACTORY_MOJANG, FACTORY_MICROSOFT, FACTORY_AUTHLIB_INJECTOR); public static final List<AccountFactory<?>> FACTORIES = immutableListOf(FACTORY_OFFLINE, FACTORY_MOJANG, FACTORY_MICROSOFT, FACTORY_AUTHLIB_INJECTOR);
// ==== login type / account factory mapping ==== // ==== login type / account factory mapping ====
@ -129,59 +136,20 @@ public final class Accounts {
throw new IllegalArgumentException("Failed to determine account type: " + account); throw new IllegalArgumentException("Failed to determine account type: " + account);
} }
private static final String GLOBAL_PREFIX = "$GLOBAL:";
private static final ObservableList<Map<Object, Object>> globalAccountStorages = FXCollections.observableArrayList();
private static final ObservableList<Account> accounts = observableArrayList(account -> new Observable[] { account }); private static final ObservableList<Account> accounts = observableArrayList(account -> new Observable[] { account });
private static final ReadOnlyListWrapper<Account> accountsWrapper = new ReadOnlyListWrapper<>(Accounts.class, "accounts", accounts); private static final ObjectProperty<Account> selectedAccount = new SimpleObjectProperty<>(Accounts.class, "selectedAccount");
private static final ObjectProperty<Account> selectedAccount = new SimpleObjectProperty<Account>(Accounts.class, "selectedAccount") {
{
accounts.addListener(onInvalidating(this::invalidated));
}
@Override
protected void invalidated() {
// this methods first checks whether the current selection is valid
// if it's valid, the underlying storage will be updated
// otherwise, the first account will be selected as an alternative(or null if accounts is empty)
Account selected = get();
if (accounts.isEmpty()) {
if (selected == null) {
// valid
} else {
// the previously selected account is gone, we can only set it to null here
set(null);
return;
}
} else {
if (accounts.contains(selected)) {
// valid
} else {
// the previously selected account is gone
set(accounts.get(0));
return;
}
}
// selection is valid, store it
if (!initialized)
return;
updateAccountStorages();
}
};
/** /**
* True if {@link #init()} hasn't been called. * True if {@link #init()} hasn't been called.
*/ */
private static boolean initialized = false; private static boolean initialized = false;
static {
accounts.addListener(onInvalidating(Accounts::updateAccountStorages));
}
private static Map<Object, Object> getAccountStorage(Account account) { private static Map<Object, Object> getAccountStorage(Account account) {
Map<Object, Object> storage = account.toStorage(); Map<Object, Object> storage = account.toStorage();
storage.put("type", getLoginType(getAccountFactory(account))); storage.put("type", getLoginType(getAccountFactory(account)));
if (account == selectedAccount.get()) {
storage.put("selected", true);
}
return storage; return storage;
} }
@ -191,7 +159,67 @@ public final class Accounts {
if (!initialized) if (!initialized)
return; return;
// update storage // update storage
config().getAccountStorages().setAll(accounts.stream().map(Accounts::getAccountStorage).collect(toList()));
ArrayList<Map<Object, Object>> global = new ArrayList<>();
ArrayList<Map<Object, Object>> portable = new ArrayList<>();
for (Account account : accounts) {
Map<Object, Object> storage = getAccountStorage(account);
if (account.isPortable())
portable.add(storage);
else
global.add(storage);
}
if (!global.equals(globalAccountStorages))
globalAccountStorages.setAll(global);
if (!portable.equals(config().getAccountStorages()))
config().getAccountStorages().setAll(portable);
}
@SuppressWarnings("unchecked")
private static void loadGlobalAccountStorages() {
Path globalAccountsFile = new File(FCLPath.FILES_DIR, "accounts.json").toPath();
if (Files.exists(globalAccountsFile)) {
try (Reader reader = Files.newBufferedReader(globalAccountsFile)) {
globalAccountStorages.setAll((List<Map<Object, Object>>)
Config.CONFIG_GSON.fromJson(reader, new TypeToken<List<Map<Object, Object>>>() {
}.getType()));
} catch (Throwable e) {
LOG.log(Level.WARNING, "Failed to load global accounts", e);
}
}
InvocationDispatcher<String> dispatcher = InvocationDispatcher.runOn(Lang::thread, json -> {
LOG.info("Saving global accounts");
synchronized (globalAccountsFile) {
try {
synchronized (globalAccountsFile) {
FileUtils.saveSafely(globalAccountsFile, json);
}
} catch (IOException e) {
LOG.log(Level.SEVERE, "Failed to save global accounts", e);
}
}
});
globalAccountStorages.addListener(onInvalidating(() ->
dispatcher.accept(Config.CONFIG_GSON.toJson(globalAccountStorages))));
}
private static Account parseAccount(Map<Object, Object> storage) {
AccountFactory<?> factory = type2factory.get(storage.get("type"));
if (factory == null) {
LOG.warning("Unrecognized account type: " + storage);
return null;
}
try {
return factory.fromStorage(storage);
} catch (Exception e) {
LOG.log(Level.WARNING, "Failed to load account: " + storage, e);
return null;
}
} }
/** /**
@ -201,53 +229,102 @@ public final class Accounts {
if (initialized) if (initialized)
throw new IllegalStateException("Already initialized"); throw new IllegalStateException("Already initialized");
// load accounts loadGlobalAccountStorages();
config().getAccountStorages().forEach(storage -> {
AccountFactory<?> factory = type2factory.get(storage.get("type"));
if (factory == null) {
LOG.warning("Unrecognized account type: " + storage);
return;
}
Account account;
try {
account = factory.fromStorage(storage);
} catch (Exception e) {
LOG.log(Level.WARNING, "Failed to load account: " + storage, e);
return;
}
accounts.add(account);
if (Boolean.TRUE.equals(storage.get("selected"))) { // load accounts
selectedAccount.set(account); Account selected = null;
for (Map<Object, Object> storage : config().getAccountStorages()) {
Account account = parseAccount(storage);
if (account != null) {
account.setPortable(true);
accounts.add(account);
if (Boolean.TRUE.equals(storage.get("selected"))) {
selected = account;
}
} }
}); }
for (Map<Object, Object> storage : globalAccountStorages) {
Account account = parseAccount(storage);
if (account != null) {
accounts.add(account);
}
}
String selectedAccountIdentifier = config().getSelectedAccount();
if (selected == null && selectedAccountIdentifier != null) {
boolean portable = true;
if (selectedAccountIdentifier.startsWith(GLOBAL_PREFIX)) {
portable = false;
selectedAccountIdentifier = selectedAccountIdentifier.substring(GLOBAL_PREFIX.length());
}
for (Account account : accounts) {
if (selectedAccountIdentifier.equals(account.getIdentifier())) {
if (portable == account.isPortable()) {
selected = account;
break;
} else if (selected == null) {
selected = account;
}
}
}
}
if (selected == null && !accounts.isEmpty()) {
selected = accounts.get(0);
}
selectedAccount.set(selected);
InvalidationListener listener = o -> {
// this method first checks whether the current selection is valid
// if it's valid, the underlying storage will be updated
// otherwise, the first account will be selected as an alternative(or null if accounts is empty)
Account account = selectedAccount.get();
if (accounts.isEmpty()) {
if (account == null) {
// valid
} else {
// the previously selected account is gone, we can only set it to null here
selectedAccount.set(null);
}
} else {
if (accounts.contains(account)) {
// valid
} else {
// the previously selected account is gone
selectedAccount.set(accounts.get(0));
}
}
};
selectedAccount.addListener(listener);
selectedAccount.addListener(onInvalidating(() -> {
Account account = selectedAccount.get();
if (account != null)
config().setSelectedAccount(account.isPortable() ? account.getIdentifier() : GLOBAL_PREFIX + account.getIdentifier());
else
config().setSelectedAccount(null);
}));
accounts.addListener(listener);
accounts.addListener(onInvalidating(Accounts::updateAccountStorages));
initialized = true; initialized = true;
config().getAuthlibInjectorServers().addListener(onInvalidating(Accounts::removeDanglingAuthlibInjectorAccounts)); config().getAuthlibInjectorServers().addListener(onInvalidating(Accounts::removeDanglingAuthlibInjectorAccounts));
Account selected = selectedAccount.get();
if (selected != null) { if (selected != null) {
Account finalSelected = selected;
Schedulers.io().execute(() -> { Schedulers.io().execute(() -> {
try { try {
selected.logIn(); finalSelected.logIn();
} catch (AuthenticationException e) { } catch (Throwable e) {
LOG.log(Level.WARNING, "Failed to log " + selected + " in", e); LOG.log(Level.WARNING, "Failed to log " + finalSelected + " in", e);
} }
}); });
} }
if (!config().getAuthlibInjectorServers().isEmpty()) { triggerAuthlibInjectorUpdateCheck();
triggerAuthlibInjectorUpdateCheck();
}
Schedulers.io().execute(() -> {
try {
FACTORY_LITTLE_SKIN.getServer().fetchMetadataResponse();
} catch (IOException e) {
LOG.log(Level.WARNING, "Failed to fetch authlib-injector server metdata: " + FACTORY_LITTLE_SKIN.getServer(), e);
}
});
for (AuthlibInjectorServer server : config().getAuthlibInjectorServers()) { for (AuthlibInjectorServer server : config().getAuthlibInjectorServers()) {
if (selected instanceof AuthlibInjectorAccount && ((AuthlibInjectorAccount) selected).getServer() == server) if (selected instanceof AuthlibInjectorAccount && ((AuthlibInjectorAccount) selected).getServer() == server)
@ -266,10 +343,6 @@ public final class Accounts {
return accounts; return accounts;
} }
public static ReadOnlyListProperty<Account> accountsProperty() {
return accountsWrapper.getReadOnlyProperty();
}
public static Account getSelectedAccount() { public static Account getSelectedAccount() {
return selectedAccount.get(); return selectedAccount.get();
} }
@ -330,7 +403,7 @@ public final class Accounts {
} }
// ==== // ====
// ==== Login type name i18n === // ==== Login type name ===
private static final Map<AccountFactory<?>, Integer> unlocalizedLoginTypeNames = mapOf( private static final Map<AccountFactory<?>, Integer> unlocalizedLoginTypeNames = mapOf(
pair(Accounts.FACTORY_OFFLINE, R.string.account_methods_offline), pair(Accounts.FACTORY_OFFLINE, R.string.account_methods_offline),
pair(Accounts.FACTORY_MOJANG, R.string.account_methods_yggdrasil), pair(Accounts.FACTORY_MOJANG, R.string.account_methods_yggdrasil),

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.InvalidationListener;
import com.tungsten.fclcore.fakefx.beans.Observable; import com.tungsten.fclcore.fakefx.beans.Observable;
import com.tungsten.fclcore.fakefx.beans.property.BooleanProperty; import com.tungsten.fclcore.fakefx.beans.property.BooleanProperty;
import com.tungsten.fclcore.fakefx.beans.property.DoubleProperty;
import com.tungsten.fclcore.fakefx.beans.property.IntegerProperty; import com.tungsten.fclcore.fakefx.beans.property.IntegerProperty;
import com.tungsten.fclcore.fakefx.beans.property.MapProperty;
import com.tungsten.fclcore.fakefx.beans.property.SimpleBooleanProperty; import com.tungsten.fclcore.fakefx.beans.property.SimpleBooleanProperty;
import com.tungsten.fclcore.fakefx.beans.property.SimpleDoubleProperty;
import com.tungsten.fclcore.fakefx.beans.property.SimpleIntegerProperty; import com.tungsten.fclcore.fakefx.beans.property.SimpleIntegerProperty;
import com.tungsten.fclcore.fakefx.beans.property.SimpleMapProperty;
import com.tungsten.fclcore.fakefx.beans.property.SimpleStringProperty; import com.tungsten.fclcore.fakefx.beans.property.SimpleStringProperty;
import com.tungsten.fclcore.fakefx.beans.property.StringProperty; import com.tungsten.fclcore.fakefx.beans.property.StringProperty;
import com.tungsten.fclcore.fakefx.collections.FXCollections; import com.tungsten.fclcore.fakefx.collections.FXCollections;
@ -38,7 +38,7 @@ public final class Config implements Cloneable, Observable {
public static final int CURRENT_UI_VERSION = 0; public static final int CURRENT_UI_VERSION = 0;
private static final Gson CONFIG_GSON = new GsonBuilder() public static final Gson CONFIG_GSON = new GsonBuilder()
.registerTypeAdapter(File.class, FileTypeAdapter.INSTANCE) .registerTypeAdapter(File.class, FileTypeAdapter.INSTANCE)
.registerTypeAdapter(ObservableList.class, new ObservableListCreator()) .registerTypeAdapter(ObservableList.class, new ObservableListCreator())
.registerTypeAdapter(ObservableSet.class, new ObservableSetCreator()) .registerTypeAdapter(ObservableSet.class, new ObservableSetCreator())
@ -80,7 +80,10 @@ public final class Config implements Cloneable, Observable {
private StringProperty versionListSource = new SimpleStringProperty("balanced"); private StringProperty versionListSource = new SimpleStringProperty("balanced");
@SerializedName("configurations") @SerializedName("configurations")
private ObservableMap<String, Profile> configurations = FXCollections.observableMap(new TreeMap<>()); private SimpleMapProperty<String, Profile> configurations = new SimpleMapProperty<>(FXCollections.observableMap(new TreeMap<>()));
@SerializedName("selectedAccount")
private StringProperty selectedAccount = new SimpleStringProperty();
@SerializedName("accounts") @SerializedName("accounts")
private ObservableList<Map<Object, Object>> accountStorages = FXCollections.observableArrayList(); private ObservableList<Map<Object, Object>> accountStorages = FXCollections.observableArrayList();
@ -220,10 +223,22 @@ public final class Config implements Cloneable, Observable {
return versionListSource; return versionListSource;
} }
public ObservableMap<String, Profile> getConfigurations() { public MapProperty<String, Profile> getConfigurations() {
return configurations; return configurations;
} }
public String getSelectedAccount() {
return selectedAccount.get();
}
public void setSelectedAccount(String selectedAccount) {
this.selectedAccount.set(selectedAccount);
}
public StringProperty selectedAccountProperty() {
return selectedAccount;
}
public ObservableList<Map<Object, Object>> getAccountStorages() { public ObservableList<Map<Object, Object>> getAccountStorages() {
return accountStorages; return accountStorages;
} }

View File

@ -33,7 +33,7 @@ public class Controllers {
public static void checkControllers() { public static void checkControllers() {
if (controllers.isEmpty()) { if (controllers.isEmpty()) {
try { try {
String str = IOUtils.readFullyAsString(Controllers.class.getResourceAsStream("/assets/controllers/Default.json"), StandardCharsets.UTF_8); String str = IOUtils.readFullyAsString(Controllers.class.getResourceAsStream("/assets/controllers/Default.json"));
Controller controller = new GsonBuilder() Controller controller = new GsonBuilder()
.registerTypeAdapterFactory(new JavaFxPropertyTypeAdapterFactory(true, true)) .registerTypeAdapterFactory(new JavaFxPropertyTypeAdapterFactory(true, true))
.setPrettyPrinting() .setPrettyPrinting()

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.SimpleIntegerProperty;
import com.tungsten.fclcore.fakefx.beans.property.SimpleStringProperty; import com.tungsten.fclcore.fakefx.beans.property.SimpleStringProperty;
import com.tungsten.fclcore.fakefx.beans.property.StringProperty; import com.tungsten.fclcore.fakefx.beans.property.StringProperty;
import com.tungsten.fclcore.fakefx.collections.ObservableList;
import com.tungsten.fclcore.fakefx.collections.ObservableMap;
import com.tungsten.fclcore.fakefx.collections.ObservableSet;
import com.tungsten.fclcore.util.fakefx.ObservableHelper; import com.tungsten.fclcore.util.fakefx.ObservableHelper;
import com.tungsten.fclcore.util.fakefx.PropertyUtils; import com.tungsten.fclcore.util.fakefx.PropertyUtils;
import com.tungsten.fclcore.util.gson.FileTypeAdapter;
import com.tungsten.fclcore.util.gson.fakefx.creators.ObservableListCreator;
import com.tungsten.fclcore.util.gson.fakefx.creators.ObservableMapCreator;
import com.tungsten.fclcore.util.gson.fakefx.creators.ObservableSetCreator;
import com.tungsten.fclcore.util.gson.fakefx.factories.JavaFxPropertyTypeAdapterFactory;
import org.jetbrains.annotations.Nullable; import org.jetbrains.annotations.Nullable;
import java.io.File;
import java.lang.reflect.Type; import java.lang.reflect.Type;
import java.util.*; import java.util.*;
@JsonAdapter(GlobalConfig.Serializer.class) @JsonAdapter(GlobalConfig.Serializer.class)
public class GlobalConfig implements Cloneable, Observable { public class GlobalConfig implements Cloneable, Observable {
private static final Gson CONFIG_GSON = new GsonBuilder()
.registerTypeAdapter(File.class, FileTypeAdapter.INSTANCE)
.registerTypeAdapter(ObservableList.class, new ObservableListCreator())
.registerTypeAdapter(ObservableSet.class, new ObservableSetCreator())
.registerTypeAdapter(ObservableMap.class, new ObservableMapCreator())
.registerTypeAdapterFactory(new JavaFxPropertyTypeAdapterFactory(true, true))
.setPrettyPrinting()
.create();
@Nullable @Nullable
public static GlobalConfig fromJson(String json) throws JsonParseException { public static GlobalConfig fromJson(String json) throws JsonParseException {
GlobalConfig loaded = CONFIG_GSON.fromJson(json, GlobalConfig.class); GlobalConfig loaded = Config.CONFIG_GSON.fromJson(json, GlobalConfig.class);
if (loaded == null) { if (loaded == null) {
return null; return null;
} }
@ -72,7 +54,7 @@ public class GlobalConfig implements Cloneable, Observable {
} }
public String toJson() { public String toJson() {
return CONFIG_GSON.toJson(this); return Config.CONFIG_GSON.toJson(this);
} }
@Override @Override

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

View File

@ -107,7 +107,7 @@ public enum ModTranslations {
return true; return true;
} }
try { try {
String modData = IOUtils.readFullyAsString(ModTranslations.class.getResourceAsStream(resourceName), StandardCharsets.UTF_8); String modData = IOUtils.readFullyAsString(ModTranslations.class.getResourceAsStream(resourceName));
mods = Arrays.stream(modData.split("\n")).filter(line -> !line.startsWith("#")).map(Mod::new).collect(Collectors.toList()); mods = Arrays.stream(modData.split("\n")).filter(line -> !line.startsWith("#")).map(Mod::new).collect(Collectors.toList());
return true; return true;
} catch (Exception e) { } catch (Exception e) {

View File

@ -26,7 +26,7 @@ public class RuntimeUtils {
public static boolean isLatest(String targetDir, String srcDir) throws IOException { public static boolean isLatest(String targetDir, String srcDir) throws IOException {
File targetFile = new File(targetDir + "/version"); File targetFile = new File(targetDir + "/version");
long version = Long.parseLong(IOUtils.readFullyAsString(RuntimeUtils.class.getResourceAsStream(srcDir + "/version"), StandardCharsets.UTF_8)); long version = Long.parseLong(IOUtils.readFullyAsString(RuntimeUtils.class.getResourceAsStream(srcDir + "/version")));
return targetFile.exists() && Long.parseLong(FileUtils.readText(targetFile)) == version; return targetFile.exists() && Long.parseLong(FileUtils.readText(targetFile)) == version;
} }
@ -43,7 +43,7 @@ public class RuntimeUtils {
new File(targetDir).mkdirs(); new File(targetDir).mkdirs();
String universalPath = srcDir + "/universal.tar.xz"; String universalPath = srcDir + "/universal.tar.xz";
String archPath = srcDir + "/bin-" + Architecture.archAsString(Architecture.getDeviceArchitecture()) + ".tar.xz"; String archPath = srcDir + "/bin-" + Architecture.archAsString(Architecture.getDeviceArchitecture()) + ".tar.xz";
String version = IOUtils.readFullyAsString(RuntimeUtils.class.getResourceAsStream("/assets/" + srcDir + "/version"), StandardCharsets.UTF_8); String version = IOUtils.readFullyAsString(RuntimeUtils.class.getResourceAsStream("/assets/" + srcDir + "/version"));
uncompressTarXZ(context.getAssets().open(universalPath), new File(targetDir)); uncompressTarXZ(context.getAssets().open(universalPath), new File(targetDir));
uncompressTarXZ(context.getAssets().open(archPath), new File(targetDir)); uncompressTarXZ(context.getAssets().open(archPath), new File(targetDir));
FileUtils.writeText(new File(targetDir + "/version"), version); FileUtils.writeText(new File(targetDir + "/version"), version);

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.ListChangeListener;
import com.tungsten.fclcore.fakefx.collections.WeakListChangeListener; import com.tungsten.fclcore.fakefx.collections.WeakListChangeListener;
import java.util.LinkedList; import java.util.ArrayList;
import java.util.List; import java.util.List;
public class WeakListenerHolder { public class WeakListenerHolder {
private List<Object> refs = new LinkedList<>(); private final List<Object> refs = new ArrayList<>(0);
public WeakListenerHolder() { public WeakListenerHolder() {
} }
@ -38,4 +38,4 @@ public class WeakListenerHolder {
public boolean remove(Object obj) { public boolean remove(Object obj) {
return refs.remove(obj); return refs.remove(obj);
} }
} }

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.Observable;
import com.tungsten.fclcore.fakefx.beans.binding.Bindings; import com.tungsten.fclcore.fakefx.beans.binding.Bindings;
import com.tungsten.fclcore.fakefx.beans.binding.ObjectBinding; import com.tungsten.fclcore.fakefx.beans.binding.ObjectBinding;
import com.tungsten.fclcore.fakefx.beans.property.BooleanProperty;
import com.tungsten.fclcore.fakefx.beans.property.SimpleBooleanProperty;
import com.tungsten.fclcore.util.ToStringBuilder; import com.tungsten.fclcore.util.ToStringBuilder;
import com.tungsten.fclcore.util.fakefx.ObservableHelper; import com.tungsten.fclcore.util.fakefx.ObservableHelper;
import java.util.Map; import java.util.Map;
import java.util.Objects;
import java.util.Optional; import java.util.Optional;
import java.util.UUID; import java.util.UUID;
@ -48,7 +51,23 @@ public abstract class Account implements Observable {
public void clearCache() { public void clearCache() {
} }
private ObservableHelper helper = new ObservableHelper(this); private final BooleanProperty portable = new SimpleBooleanProperty(false);
public BooleanProperty portableProperty() {
return portable;
}
public boolean isPortable() {
return portable.get();
}
public void setPortable(boolean value) {
this.portable.set(value);
}
public abstract String getIdentifier();
private final ObservableHelper helper = new ObservableHelper(this);
@Override @Override
public void addListener(InvalidationListener listener) { public void addListener(InvalidationListener listener) {
@ -72,12 +91,29 @@ public abstract class Account implements Observable {
return Bindings.createObjectBinding(Optional::empty); return Bindings.createObjectBinding(Optional::empty);
} }
@Override
public int hashCode() {
return Objects.hash(portable);
}
@Override
public boolean equals(Object obj) {
if (this == obj)
return true;
if (!(obj instanceof Account))
return false;
Account another = (Account) obj;
return isPortable() == another.isPortable();
}
@Override @Override
public String toString() { public String toString() {
return new ToStringBuilder(this) return new ToStringBuilder(this)
.append("username", getUsername()) .append("username", getUsername())
.append("character", getCharacter()) .append("character", getCharacter())
.append("uuid", getUUID()) .append("uuid", getUUID())
.append("portable", isPortable())
.toString(); .toString();
} }
} }

View File

@ -7,16 +7,22 @@ import java.io.IOException;
import java.util.UUID; import java.util.UUID;
public class AuthInfo implements AutoCloseable { public class AuthInfo implements AutoCloseable {
public static final String USER_TYPE_MSA = "msa";
public static final String USER_TYPE_MOJANG = "mojang";
public static final String USER_TYPE_LEGACY = "legacy";
private final String username; private final String username;
private final UUID uuid; private final UUID uuid;
private final String accessToken; private final String accessToken;
private final String userType;
private final String userProperties; private final String userProperties;
public AuthInfo(String username, UUID uuid, String accessToken, String userProperties) { public AuthInfo(String username, UUID uuid, String accessToken, String userType, String userProperties) {
this.username = username; this.username = username;
this.uuid = uuid; this.uuid = uuid;
this.accessToken = accessToken; this.accessToken = accessToken;
this.userType = userType;
this.userProperties = userProperties; this.userProperties = userProperties;
} }
@ -32,6 +38,10 @@ public class AuthInfo implements AutoCloseable {
return accessToken; return accessToken;
} }
public String getUserType() {
return userType;
}
/** /**
* Properties of this user. * Properties of this user.
* Don't know the difference between user properties and user property map. * Don't know the difference between user properties and user property map.

View File

@ -222,7 +222,7 @@ public class OAuth {
* *
* @param url OAuth url. * @param url OAuth url.
*/ */
void openBrowser(String url); void openBrowser(String url) throws IOException;
String getClientId(); String getClientId();
@ -236,7 +236,7 @@ public class OAuth {
DEVICE, DEVICE,
} }
public class Result { public static final class Result {
private final String accessToken; private final String accessToken;
private final String refreshToken; private final String refreshToken;
@ -265,7 +265,7 @@ public class OAuth {
@SerializedName("verification_uri") @SerializedName("verification_uri")
public String verificationURI; public String verificationURI;
// Life time in seconds for device_code and user_code // Lifetime in seconds for device_code and user_code
@SerializedName("expires_in") @SerializedName("expires_in")
public int expiresIn; public int expiresIn;

View File

@ -108,7 +108,7 @@ public class AuthlibInjectorAccount extends YggdrasilAccount {
private final String prefetchedMeta; private final String prefetchedMeta;
public AuthlibInjectorAuthInfo(AuthInfo authInfo, AuthlibInjectorArtifactInfo artifact, AuthlibInjectorServer server, String prefetchedMeta) { public AuthlibInjectorAuthInfo(AuthInfo authInfo, AuthlibInjectorArtifactInfo artifact, AuthlibInjectorServer server, String prefetchedMeta) {
super(authInfo.getUsername(), authInfo.getUUID(), authInfo.getAccessToken(), authInfo.getUserProperties()); super(authInfo.getUsername(), authInfo.getUUID(), authInfo.getAccessToken(), authInfo.getUserType(), authInfo.getUserProperties());
this.artifact = artifact; this.artifact = artifact;
this.server = server; this.server = server;
@ -141,6 +141,11 @@ public class AuthlibInjectorAccount extends YggdrasilAccount {
return server; return server;
} }
@Override
public String getIdentifier() {
return server.getUrl() + ":" + super.getIdentifier();
}
@Override @Override
public int hashCode() { public int hashCode() {
return Objects.hash(super.hashCode(), server.hashCode()); return Objects.hash(super.hashCode(), server.hashCode());
@ -151,7 +156,8 @@ public class AuthlibInjectorAccount extends YggdrasilAccount {
if (obj == null || obj.getClass() != AuthlibInjectorAccount.class) if (obj == null || obj.getClass() != AuthlibInjectorAccount.class)
return false; return false;
AuthlibInjectorAccount another = (AuthlibInjectorAccount) obj; AuthlibInjectorAccount another = (AuthlibInjectorAccount) obj;
return characterUUID.equals(another.characterUUID) && server.equals(another.server); return isPortable() == another.isPortable()
&& characterUUID.equals(another.characterUUID) && server.equals(another.server);
} }
@Override @Override
@ -169,7 +175,7 @@ public class AuthlibInjectorAccount extends YggdrasilAccount {
return emptySet(); return emptySet();
Set<TextureType> result = EnumSet.noneOf(TextureType.class); Set<TextureType> result = EnumSet.noneOf(TextureType.class);
for (String val : prop.split(",")) { for (String val : prop.split(",")) {
val = val.toUpperCase(); val = val.toUpperCase(Locale.ROOT);
TextureType parsed; TextureType parsed;
try { try {
parsed = TextureType.valueOf(val); parsed = TextureType.valueOf(val);

View File

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

View File

@ -94,7 +94,7 @@ public class MicrosoftSession {
public AuthInfo toAuthInfo() { public AuthInfo toAuthInfo() {
requireNonNull(profile); requireNonNull(profile);
return new AuthInfo(profile.getName(), profile.getId(), accessToken, "{}"); return new AuthInfo(profile.getName(), profile.getId(), accessToken, AuthInfo.USER_TYPE_MSA, "{}");
} }
public static class User { public static class User {

View File

@ -51,6 +51,10 @@ public class OfflineAccount extends Account {
} }
} }
public AuthlibInjectorArtifactProvider getDownloader() {
return downloader;
}
@Override @Override
public UUID getUUID() { public UUID getUUID() {
return uuid; return uuid;
@ -66,6 +70,11 @@ public class OfflineAccount extends Account {
return username; return username;
} }
@Override
public String getIdentifier() {
return username + ":" + username;
}
public Skin getSkin() { public Skin getSkin() {
return skin; return skin;
} }
@ -75,7 +84,7 @@ public class OfflineAccount extends Account {
invalidate(); invalidate();
} }
private boolean loadAuthlibInjector(Skin skin) { protected boolean loadAuthlibInjector(Skin skin) {
if (skin == null) return false; if (skin == null) return false;
if (skin.getType() == Skin.Type.DEFAULT) return false; if (skin.getType() == Skin.Type.DEFAULT) return false;
TextureModel defaultModel = TextureModel.detectUUID(getUUID()); TextureModel defaultModel = TextureModel.detectUUID(getUUID());
@ -88,7 +97,8 @@ public class OfflineAccount extends Account {
@Override @Override
public AuthInfo logIn() throws AuthenticationException { public AuthInfo logIn() throws AuthenticationException {
AuthInfo authInfo = new AuthInfo(username, uuid, UUIDTypeAdapter.fromUUID(UUID.randomUUID()), "{}"); // Using "legacy" user type here because "mojang" user type may cause "invalid session token" or "disconnected" when connecting to a game server.
AuthInfo authInfo = new AuthInfo(username, uuid, UUIDTypeAdapter.fromUUID(UUID.randomUUID()), AuthInfo.USER_TYPE_MSA, "{}");
if (loadAuthlibInjector(skin)) { if (loadAuthlibInjector(skin)) {
CompletableFuture<AuthlibInjectorArtifactInfo> artifactTask = CompletableFuture.supplyAsync(() -> { CompletableFuture<AuthlibInjectorArtifactInfo> artifactTask = CompletableFuture.supplyAsync(() -> {
@ -128,7 +138,7 @@ public class OfflineAccount extends Account {
private YggdrasilServer server; private YggdrasilServer server;
public OfflineAuthInfo(AuthInfo authInfo, AuthlibInjectorArtifactInfo artifact) { public OfflineAuthInfo(AuthInfo authInfo, AuthlibInjectorArtifactInfo artifact) {
super(authInfo.getUsername(), authInfo.getUUID(), authInfo.getAccessToken(), authInfo.getUserProperties()); super(authInfo.getUsername(), authInfo.getUUID(), authInfo.getAccessToken(), USER_TYPE_MSA, authInfo.getUserProperties());
this.artifact = artifact; this.artifact = artifact;
} }
@ -140,7 +150,8 @@ public class OfflineAccount extends Account {
server.start(); server.start();
try { try {
server.addCharacter(new YggdrasilServer.Character(uuid, username, skin.load(username).run())); server.addCharacter(new YggdrasilServer.Character(uuid, username,
skin != null ? skin.load(username).run() : null));
} catch (IOException e) { } catch (IOException e) {
// ignore // ignore
} catch (Exception e) { } catch (Exception e) {
@ -182,9 +193,9 @@ public class OfflineAccount extends Account {
Skin.LoadedSkin loadedSkin = skin.load(username).run(); Skin.LoadedSkin loadedSkin = skin.load(username).run();
Map<TextureType, Texture> map = new HashMap<>(); Map<TextureType, Texture> map = new HashMap<>();
if (loadedSkin != null) { if (loadedSkin != null) {
map.put(TextureType.SKIN, new Texture(null, null, BitmapFactory.decodeStream(loadedSkin.getSkin().getInputStream()))); map.put(TextureType.SKIN, new Texture(null, null, loadedSkin.getSkin().getImage()));
if (loadedSkin.getCape() != null) { if (loadedSkin.getCape() != null) {
map.put(TextureType.CAPE, new Texture(null, null, BitmapFactory.decodeStream(loadedSkin.getCape().getInputStream()))); map.put(TextureType.CAPE, new Texture(null, null, loadedSkin.getCape().getImage()));
} }
} else { } else {
map.put(TextureType.SKIN, new Texture(null, null, BitmapFactory.decodeStream(OfflineAccount.class.getResourceAsStream(TextureModel.detectUUID(uuid) == TextureModel.ALEX ? "/assets/img/alex.img" : "/assets/img/steve.png")))); map.put(TextureType.SKIN, new Texture(null, null, BitmapFactory.decodeStream(OfflineAccount.class.getResourceAsStream(TextureModel.detectUUID(uuid) == TextureModel.ALEX ? "/assets/img/alex.img" : "/assets/img/steve.png"))));
@ -213,6 +224,6 @@ public class OfflineAccount extends Account {
if (!(obj instanceof OfflineAccount)) if (!(obj instanceof OfflineAccount))
return false; return false;
OfflineAccount another = (OfflineAccount) obj; OfflineAccount another = (OfflineAccount) obj;
return username.equals(another.username); return isPortable() == another.isPortable() && username.equals(another.username);
} }
} }

View File

@ -27,13 +27,21 @@ import java.util.Collections;
import java.util.Locale; import java.util.Locale;
import java.util.Map; import java.util.Map;
import java.util.Optional; import java.util.Optional;
import java.util.function.Function;
public class Skin { public class Skin {
public enum Type { public enum Type {
DEFAULT, DEFAULT,
STEVE,
ALEX, ALEX,
ARI,
EFE,
KAI,
MAKENA,
NOOR,
STEVE,
SUNNY,
ZURI,
LOCAL_FILE, LOCAL_FILE,
LITTLE_SKIN, LITTLE_SKIN,
CUSTOM_SKIN_LOADER_API, CUSTOM_SKIN_LOADER_API,
@ -43,10 +51,24 @@ public class Skin {
switch (type) { switch (type) {
case "default": case "default":
return DEFAULT; return DEFAULT;
case "steve":
return STEVE;
case "alex": case "alex":
return ALEX; return ALEX;
case "ari":
return ARI;
case "efe":
return EFE;
case "kai":
return KAI;
case "makena":
return MAKENA;
case "noor":
return NOOR;
case "steve":
return STEVE;
case "sunny":
return SUNNY;
case "zuri":
return ZURI;
case "local_file": case "local_file":
return LOCAL_FILE; return LOCAL_FILE;
case "little_skin": case "little_skin":
@ -61,6 +83,12 @@ public class Skin {
} }
} }
private static Function<Type, InputStream> defaultSkinLoader = null;
public static void registerDefaultSkinLoader(Function<Type, InputStream> defaultSkinLoader0) {
defaultSkinLoader = defaultSkinLoader0;
}
private final Type type; private final Type type;
private final String cslApi; private final String cslApi;
private final TextureModel textureModel; private final TextureModel textureModel;
@ -99,10 +127,19 @@ public class Skin {
switch (type) { switch (type) {
case DEFAULT: case DEFAULT:
return Task.supplyAsync(() -> null); return Task.supplyAsync(() -> null);
case STEVE:
return Task.supplyAsync(() -> new LoadedSkin(TextureModel.STEVE, Texture.loadTexture(Skin.class.getResourceAsStream("/assets/img/steve.png")), null));
case ALEX: case ALEX:
return Task.supplyAsync(() -> new LoadedSkin(TextureModel.ALEX, Texture.loadTexture(Skin.class.getResourceAsStream("/assets/img/alex.png")), null)); case ARI:
case EFE:
case KAI:
case MAKENA:
case NOOR:
case STEVE:
case SUNNY:
case ZURI:
if (defaultSkinLoader == null) {
return Task.supplyAsync(() -> null);
}
return Task.supplyAsync(() -> new LoadedSkin(TextureModel.STEVE, Texture.loadTexture(defaultSkinLoader.apply(type)), null));
case LOCAL_FILE: case LOCAL_FILE:
return Task.supplyAsync(() -> { return Task.supplyAsync(() -> {
Texture skin = null, cape = null; Texture skin = null, cape = null;

View File

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

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.JsonUtils;
import com.tungsten.fclcore.util.gson.UUIDTypeAdapter; import com.tungsten.fclcore.util.gson.UUIDTypeAdapter;
import com.tungsten.fclcore.util.io.HttpServer; import com.tungsten.fclcore.util.io.HttpServer;
import com.tungsten.fclcore.util.io.IOUtils; import com.tungsten.fclcore.util.png.fakefx.PNGFakeFXUtils;
import java.io.ByteArrayInputStream;
import java.io.IOException; import java.io.IOException;
import java.security.*; import java.security.*;
import java.util.*; import java.util.*;
@ -22,8 +23,6 @@ import java.util.stream.Stream;
import static java.nio.charset.StandardCharsets.UTF_8; import static java.nio.charset.StandardCharsets.UTF_8;
import fi.iki.elonen.NanoHTTPD;
public class YggdrasilServer extends HttpServer { public class YggdrasilServer extends HttpServer {
private final Map<UUID, Character> charactersByUuid = new HashMap<>(); private final Map<UUID, Character> charactersByUuid = new HashMap<>();
@ -32,7 +31,7 @@ public class YggdrasilServer extends HttpServer {
public YggdrasilServer(int port) { public YggdrasilServer(int port) {
super(port); super(port);
addRoute(NanoHTTPD.Method.GET, Pattern.compile("^/$"), this::root); addRoute(Method.GET, Pattern.compile("^/$"), this::root);
addRoute(Method.GET, Pattern.compile("/status"), this::status); addRoute(Method.GET, Pattern.compile("/status"), this::status);
addRoute(Method.POST, Pattern.compile("/api/profiles/minecraft"), this::profiles); addRoute(Method.POST, Pattern.compile("/api/profiles/minecraft"), this::profiles);
addRoute(Method.GET, Pattern.compile("/sessionserver/session/minecraft/hasJoined"), this::hasJoined); addRoute(Method.GET, Pattern.compile("/sessionserver/session/minecraft/hasJoined"), this::hasJoined);
@ -66,8 +65,7 @@ public class YggdrasilServer extends HttpServer {
} }
private Response profiles(Request request) throws IOException { private Response profiles(Request request) throws IOException {
String body = IOUtils.readFullyAsString(request.getSession().getInputStream(), UTF_8); List<String> names = JsonUtils.fromNonNullJsonFully(request.getSession().getInputStream(), new TypeToken<List<String>>() {
List<String> names = JsonUtils.fromNonNullJson(body, new TypeToken<List<String>>() {
}.getType()); }.getType());
return ok(names.stream().distinct() return ok(names.stream().distinct()
.map(this::findCharacterByName) .map(this::findCharacterByName)
@ -115,7 +113,8 @@ public class YggdrasilServer extends HttpServer {
if (Texture.hasTexture(hash)) { if (Texture.hasTexture(hash)) {
Texture texture = Texture.getTexture(hash); Texture texture = Texture.getTexture(hash);
Response response = newFixedLengthResponse(Response.Status.OK, "image/png", texture.getInputStream(), texture.getLength()); byte[] data = PNGFakeFXUtils.writeImageToArray(texture.getImage());
Response response = newFixedLengthResponse(Response.Status.OK, "image/png", new ByteArrayInputStream(data), data.length);
response.addHeader("Etag", String.format("\"%s\"", hash)); response.addHeader("Etag", String.format("\"%s\"", hash));
response.addHeader("Cache-Control", "max-age=2592000, public"); response.addHeader("Cache-Control", "max-age=2592000, public");
return response; return response;

View File

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

View File

@ -257,6 +257,6 @@ public class YggdrasilService {
.create(); .create();
public static final String PROFILE_URL = "https://aka.ms/MinecraftMigration"; public static final String PROFILE_URL = "https://aka.ms/MinecraftMigration";
public static final String MIGRATION_FAQ_URL = "https://help.minecraft.net/hc/en-us/articles/360050865492-JAVA-Account-Migration-FAQ"; public static final String MIGRATION_FAQ_URL = "https://help.minecraft.net/articles/360050865492";
public static final String PURCHASE_URL = "https://www.minecraft.net/store/minecraft-java-bedrock-edition-pc"; public static final String PURCHASE_URL = "https://www.microsoft.com/store/productId/9NXP44L49SHJ";
} }

View File

@ -66,6 +66,7 @@ public class YggdrasilSession {
String name = tryCast(storage.get("displayName"), String.class).orElseThrow(() -> new IllegalArgumentException("displayName is missing")); String name = tryCast(storage.get("displayName"), String.class).orElseThrow(() -> new IllegalArgumentException("displayName is missing"));
String clientToken = tryCast(storage.get("clientToken"), String.class).orElseThrow(() -> new IllegalArgumentException("clientToken is missing")); String clientToken = tryCast(storage.get("clientToken"), String.class).orElseThrow(() -> new IllegalArgumentException("clientToken is missing"));
String accessToken = tryCast(storage.get("accessToken"), String.class).orElseThrow(() -> new IllegalArgumentException("accessToken is missing")); String accessToken = tryCast(storage.get("accessToken"), String.class).orElseThrow(() -> new IllegalArgumentException("accessToken is missing"));
@SuppressWarnings("unchecked")
Map<String, String> userProperties = tryCast(storage.get("userProperties"), Map.class).orElse(null); Map<String, String> userProperties = tryCast(storage.get("userProperties"), Map.class).orElse(null);
return new YggdrasilSession(clientToken, accessToken, new GameProfile(uuid, name), null, userProperties); return new YggdrasilSession(clientToken, accessToken, new GameProfile(uuid, name), null, userProperties);
} }
@ -86,7 +87,7 @@ public class YggdrasilSession {
if (selectedProfile == null) if (selectedProfile == null)
throw new IllegalStateException("No character is selected"); throw new IllegalStateException("No character is selected");
return new AuthInfo(selectedProfile.getName(), selectedProfile.getId(), accessToken, return new AuthInfo(selectedProfile.getName(), selectedProfile.getId(), accessToken, AuthInfo.USER_TYPE_MSA,
Optional.ofNullable(userProperties) Optional.ofNullable(userProperties)
.map(properties -> properties.entrySet().stream() .map(properties -> properties.entrySet().stream()
.collect(Collectors.toMap(Map.Entry::getKey, .collect(Collectors.toMap(Map.Entry::getKey,

View File

@ -7,7 +7,6 @@ import com.tungsten.fclcore.game.Library;
import com.tungsten.fclcore.game.LibraryDownloadInfo; import com.tungsten.fclcore.game.LibraryDownloadInfo;
import com.tungsten.fclcore.util.CacheRepository; import com.tungsten.fclcore.util.CacheRepository;
import com.tungsten.fclcore.util.DigestUtils; import com.tungsten.fclcore.util.DigestUtils;
import com.tungsten.fclcore.util.Hex;
import com.tungsten.fclcore.util.Logging; import com.tungsten.fclcore.util.Logging;
import com.tungsten.fclcore.util.gson.JsonUtils; import com.tungsten.fclcore.util.gson.JsonUtils;
import com.tungsten.fclcore.util.gson.TolerableValidationException; import com.tungsten.fclcore.util.gson.TolerableValidationException;
@ -83,7 +82,7 @@ public class DefaultCacheRepository extends CacheRepository {
LibraryDownloadInfo info = library.getDownload(); LibraryDownloadInfo info = library.getDownload();
String hash = info.getSha1(); String hash = info.getSha1();
if (hash != null) { if (hash != null) {
String checksum = Hex.encodeHex(DigestUtils.digest("SHA-1", jar)); String checksum = DigestUtils.digestToString("SHA-1", jar);
if (hash.equalsIgnoreCase(checksum)) if (hash.equalsIgnoreCase(checksum))
cacheLibrary(library, jar, false); cacheLibrary(library, jar, false);
} else if (library.getChecksums() != null && !library.getChecksums().isEmpty()) { } else if (library.getChecksums() != null && !library.getChecksums().isEmpty()) {
@ -136,7 +135,7 @@ public class DefaultCacheRepository extends CacheRepository {
if (Files.exists(jar)) { if (Files.exists(jar)) {
try { try {
if (hash != null) { if (hash != null) {
String checksum = Hex.encodeHex(DigestUtils.digest("SHA-1", jar)); String checksum = DigestUtils.digestToString("SHA-1", jar);
if (hash.equalsIgnoreCase(checksum)) if (hash.equalsIgnoreCase(checksum))
return Optional.of(restore(jar, () -> cacheLibrary(library, jar, false))); return Optional.of(restore(jar, () -> cacheLibrary(library, jar, false)));
} else if (library.getChecksums() != null && !library.getChecksums().isEmpty()) { } else if (library.getChecksums() != null && !library.getChecksums().isEmpty()) {
@ -165,7 +164,7 @@ public class DefaultCacheRepository extends CacheRepository {
public Path cacheLibrary(Library library, Path path, boolean forge) throws IOException { public Path cacheLibrary(Library library, Path path, boolean forge) throws IOException {
String hash = library.getDownload().getSha1(); String hash = library.getDownload().getSha1();
if (hash == null) if (hash == null)
hash = Hex.encodeHex(DigestUtils.digest(SHA1, path)); hash = DigestUtils.digestToString(SHA1, path);
Path cache = getFile(SHA1, hash); Path cache = getFile(SHA1, hash);
FileUtils.copyFile(path.toFile(), cache.toFile()); FileUtils.copyFile(path.toFile(), cache.toFile());
@ -228,7 +227,7 @@ public class DefaultCacheRepository extends CacheRepository {
} }
} }
private class LibraryIndex implements Validation { private static final class LibraryIndex implements Validation {
private final String name; private final String name;
private final String hash; private final String hash;
private final String type; private final String type;

View File

@ -96,7 +96,7 @@ public final class LibraryAnalyzer implements Iterable<LibraryAnalyzer.LibraryMa
/** /**
* Remove library by library id * Remove library by library id
* @param libraryId patch id or "forge"/"optifine"/"liteloader"/"fabric" * @param libraryId patch id or "forge"/"optifine"/"liteloader"/"fabric"/"quilt"
* @return this * @return this
*/ */
public LibraryAnalyzer removeLibrary(String libraryId) { public LibraryAnalyzer removeLibrary(String libraryId) {
@ -139,7 +139,10 @@ public final class LibraryAnalyzer implements Iterable<LibraryAnalyzer.LibraryMa
public static boolean isModded(VersionProvider provider, Version version) { public static boolean isModded(VersionProvider provider, Version version) {
Version resolvedVersion = version.resolve(provider); Version resolvedVersion = version.resolve(provider);
String mainClass = resolvedVersion.getMainClass(); String mainClass = resolvedVersion.getMainClass();
return mainClass != null && (LAUNCH_WRAPPER_MAIN.equals(mainClass) || mainClass.startsWith("net.fabricmc") || mainClass.startsWith("cpw.mods")); return mainClass != null && (LAUNCH_WRAPPER_MAIN.equals(mainClass)
|| mainClass.startsWith("net.fabricmc")
|| mainClass.startsWith("org.quiltmc")
|| mainClass.startsWith("cpw.mods"));
} }
public enum LibraryType { public enum LibraryType {
@ -225,13 +228,13 @@ public final class LibraryAnalyzer implements Iterable<LibraryAnalyzer.LibraryMa
public static final String BOOTSTRAP_LAUNCHER_MAIN = "cpw.mods.bootstraplauncher.BootstrapLauncher"; public static final String BOOTSTRAP_LAUNCHER_MAIN = "cpw.mods.bootstraplauncher.BootstrapLauncher";
public static final String[] FORGE_TWEAKERS = new String[] { public static final String[] FORGE_TWEAKERS = new String[] {
"net.minecraftforge.legacy._1_5_2.LibraryFixerTweaker", // 1.5.2 "net.minecraftforge.legacy._1_5_2.LibraryFixerTweaker", // 1.5.2
"cpw.mods.fml.common.launcher.FMLTweaker", // 1.6.1 ~ 1.7.10 "cpw.mods.fml.common.launcher.FMLTweaker", // 1.6.1 ~ 1.7.10
"net.minecraftforge.fml.common.launcher.FMLTweaker" // 1.8 ~ 1.12.2 "net.minecraftforge.fml.common.launcher.FMLTweaker" // 1.8 ~ 1.12.2
}; };
public static final String[] OPTIFINE_TWEAKERS = new String[] { public static final String[] OPTIFINE_TWEAKERS = new String[] {
"optifine.OptiFineTweaker", "optifine.OptiFineTweaker",
"optifine.OptiFineForgeTweaker" "optifine.OptiFineForgeTweaker"
}; };
public static final String LITELOADER_TWEAKER = "com.mumfrey.liteloader.launch.LiteLoaderTweaker"; public static final String LITELOADER_TWEAKER = "com.mumfrey.liteloader.launch.LiteLoaderTweaker";
} }

View File

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

View File

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

View File

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

View File

@ -1,7 +1,5 @@
package com.tungsten.fclcore.download.game; package com.tungsten.fclcore.download.game;
import static com.tungsten.fclcore.util.DigestUtils.digest;
import static com.tungsten.fclcore.util.Hex.encodeHex;
import static com.tungsten.fclcore.util.Logging.LOG; import static com.tungsten.fclcore.util.Logging.LOG;
import com.tungsten.fclauncher.FCLPath; import com.tungsten.fclauncher.FCLPath;
@ -12,6 +10,7 @@ import com.tungsten.fclcore.game.Library;
import com.tungsten.fclcore.task.DownloadException; import com.tungsten.fclcore.task.DownloadException;
import com.tungsten.fclcore.task.FileDownloadTask; import com.tungsten.fclcore.task.FileDownloadTask;
import com.tungsten.fclcore.task.Task; import com.tungsten.fclcore.task.Task;
import com.tungsten.fclcore.util.DigestUtils;
import com.tungsten.fclcore.util.Pack200Utils; import com.tungsten.fclcore.util.Pack200Utils;
import com.tungsten.fclcore.util.io.FileUtils; import com.tungsten.fclcore.util.io.FileUtils;
import com.tungsten.fclcore.util.io.IOUtils; import com.tungsten.fclcore.util.io.IOUtils;
@ -165,7 +164,7 @@ public class LibraryDownloadTask extends Task<Void> {
return true; return true;
} }
byte[] fileData = Files.readAllBytes(libPath.toPath()); byte[] fileData = Files.readAllBytes(libPath.toPath());
boolean valid = checksums.contains(encodeHex(digest("SHA-1", fileData))); boolean valid = checksums.contains(DigestUtils.digestToString("SHA-1", fileData));
if (!valid && libPath.getName().endsWith(".jar")) { if (!valid && libPath.getName().endsWith(".jar")) {
valid = validateJar(fileData, checksums); valid = validateJar(fileData, checksums);
} }
@ -187,7 +186,7 @@ public class LibraryDownloadTask extends Task<Void> {
hashes = new String(eData, StandardCharsets.UTF_8).split("\n"); hashes = new String(eData, StandardCharsets.UTF_8).split("\n");
} }
if (!entry.isDirectory()) { if (!entry.isDirectory()) {
files.put(entry.getName(), encodeHex(digest("SHA-1", eData))); files.put(entry.getName(), DigestUtils.digestToString("SHA-1", eData));
} }
entry = jar.getNextJarEntry(); entry = jar.getNextJarEntry();
} }
@ -255,8 +254,6 @@ public class LibraryDownloadTask extends Task<Void> {
jos.closeEntry(); jos.closeEntry();
} }
if (temp.toFile().exists()) { Files.delete(temp);
Files.delete(temp);
}
} }
} }

View File

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

View File

@ -44,7 +44,7 @@ public final class Artifact {
String fileName = this.name + "-" + this.version; String fileName = this.name + "-" + this.version;
if (classifier != null) fileName += "-" + this.classifier; if (classifier != null) fileName += "-" + this.classifier;
this.fileName = fileName + "." + this.extension; this.fileName = fileName + "." + this.extension;
this.path = String.format("%s/%s/%s/%s", this.group.replace(".", "/"), this.name, this.version, this.fileName); this.path = String.format("%s/%s/%s/%s", this.group.replace('.', '/'), this.name, this.version, this.fileName);
// group:name:version:classifier@extension // group:name:version:classifier@extension
String descriptor = String.format("%s:%s:%s", group, name, version); String descriptor = String.format("%s:%s:%s", group, name, version);
@ -68,7 +68,7 @@ public final class Artifact {
throw new IllegalArgumentException("Artifact name is malformed"); throw new IllegalArgumentException("Artifact name is malformed");
} }
return new Artifact(arr[0].replace("\\", "/"), arr[1], arr[2], arr.length >= 4 ? arr[3] : null, ext); return new Artifact(arr[0].replace('\\', '/'), arr[1], arr[2], arr.length >= 4 ? arr[3] : null, ext);
} }
public String getGroup() { public String getGroup() {

View File

@ -2,7 +2,6 @@ package com.tungsten.fclcore.game;
import com.google.gson.JsonParseException; import com.google.gson.JsonParseException;
import com.tungsten.fclcore.util.DigestUtils; import com.tungsten.fclcore.util.DigestUtils;
import com.tungsten.fclcore.util.Hex;
import com.tungsten.fclcore.util.StringUtils; import com.tungsten.fclcore.util.StringUtils;
import com.tungsten.fclcore.util.gson.Validation; import com.tungsten.fclcore.util.gson.Validation;
@ -43,6 +42,6 @@ public final class AssetObject implements Validation {
public boolean validateChecksum(Path file, boolean defaultValue) throws IOException { public boolean validateChecksum(Path file, boolean defaultValue) throws IOException {
if (hash == null) return defaultValue; if (hash == null) return defaultValue;
return Hex.encodeHex(DigestUtils.digest("SHA-1", file)).equalsIgnoreCase(hash); return DigestUtils.digestToString("SHA-1", file).equalsIgnoreCase(hash);
} }
} }

View File

@ -19,26 +19,30 @@ public final class CrashReportAnalyzer {
public enum Rule { public enum Rule {
// We manually write "Pattern.compile" here for IDEA syntax highlighting. // We manually write "Pattern.compile" here for IDEA syntax highlighting.
OPENJ9(Pattern.compile("(Open J9 is not supported|OpenJ9 is incompatible)")), OPENJ9(Pattern.compile("(Open J9 is not supported|OpenJ9 is incompatible|\\.J9VMInternals\\.)")),
TOO_OLD_JAVA(Pattern.compile("java\\.lang\\.UnsupportedClassVersionError: (.*?) version (?<expected>\\d+)\\.0"), "expected"), TOO_OLD_JAVA(Pattern.compile("java\\.lang\\.UnsupportedClassVersionError: (.*?) version (?<expected>\\d+)\\.0"), "expected"),
JVM_32BIT(Pattern.compile("(Could not reserve enough space for (.*?) object heap|The specified size exceeds the maximum representable size)")), JVM_32BIT(Pattern.compile("(Could not reserve enough space for (.*?)KB object heap|The specified size exceeds the maximum representable size)")),
// Some mods/shader packs do incorrect GL operations. // Some mods/shader packs do incorrect GL operations.
GL_OPERATION_FAILURE(Pattern.compile("1282: Invalid operation")), GL_OPERATION_FAILURE(Pattern.compile("(1282: Invalid operation|Maybe try a lower resolution resourcepack\\?)")),
// Maybe software rendering? Suggest user for using a graphics card. // Maybe software rendering? Suggest user for using a graphics card.
OPENGL_NOT_SUPPORTED(Pattern.compile("The driver does not appear to support OpenGL")), OPENGL_NOT_SUPPORTED(Pattern.compile("The driver does not appear to support OpenGL")),
GRAPHICS_DRIVER(Pattern.compile("(Pixel format not accelerated|GLX: Failed to create context: GLXBadFBConfig|Couldn't set pixel format|net\\.minecraftforge\\.fml.client\\.SplashProgress|org\\.lwjgl\\.LWJGLException|EXCEPTION_ACCESS_VIOLATION(.|\\n|\\r)+# C {2}\\[(ig|atio|nvoglv))")), GRAPHICS_DRIVER(Pattern.compile("(Pixel format not accelerated|GLX: Failed to create context: GLXBadFBConfig|Couldn't set pixel format|net\\.minecraftforge\\.fml.client\\.SplashProgress|org\\.lwjgl\\.LWJGLException|EXCEPTION_ACCESS_VIOLATION(.|\\n|\\r)+# C {2}\\[(ig|atio|nvoglv))")),
// Out of memory // Out of memory
OUT_OF_MEMORY(Pattern.compile("(java\\.lang\\.OutOfMemoryError|The system is out of physical RAM or swap space)")), OUT_OF_MEMORY(Pattern.compile("(java\\.lang\\.OutOfMemoryError|The system is out of physical RAM or swap space|Out of Memory Error)")),
// Memory exceeded // Memory exceeded
MEMORY_EXCEEDED(Pattern.compile("There is insufficient memory for the Java Runtime Environment to continue")), MEMORY_EXCEEDED(Pattern.compile("There is insufficient memory for the Java Runtime Environment to continue")),
// Too high resolution // Too high resolution
RESOLUTION_TOO_HIGH(Pattern.compile("Maybe try a (lower resolution|lowerresolution) (resourcepack|texturepack)\\?")), RESOLUTION_TOO_HIGH(Pattern.compile("Maybe try a (lower resolution|lowerresolution) (resourcepack|texturepack)\\?")),
// game can only run on Java 8. Version of uesr's JVM is too high. // game can only run on Java 8. Version of uesr's JVM is too high.
JDK_9(Pattern.compile("java\\.lang\\.ClassCastException: (java\\.base/jdk|class jdk)")), JDK_9(Pattern.compile("java\\.lang\\.ClassCastException: (java\\.base/jdk|class jdk)")),
// Forge and OptiFine with crash because the JVM compiled with a new version of Xcode
// https://github.com/sp614x/optifine/issues/4824
// https://github.com/MinecraftForge/MinecraftForge/issues/7546
MAC_JDK_8U261(Pattern.compile("Terminating app due to uncaught exception 'NSInternalInconsistencyException', reason: 'NSWindow drag regions should only be invalidated on the Main Thread!'")),
// user modifies minecraft primary jar without changing hash file // user modifies minecraft primary jar without changing hash file
FILE_CHANGED(Pattern.compile("java\\.lang\\.SecurityException: SHA1 digest error for (?<file>.*)"), "file"), FILE_CHANGED(Pattern.compile("java\\.lang\\.SecurityException: SHA1 digest error for (?<file>.*)|signer information does not match signer information of other classes in the same package"), "file"),
// mod loader/coremod injection fault, prompt user to reinstall game. // mod loader/coremod injection fault, prompt user to reinstall game.
NO_SUCH_METHOD_ERROR(Pattern.compile("java\\.lang\\.NoSuchMethodError: (?<class>.*?)"), "class"), NO_SUCH_METHOD_ERROR(Pattern.compile("java\\.lang\\.NoSuchMethodError: (?<class>.*?)"), "class"),
// mod loader/coremod injection fault, prompt user to reinstall game. // mod loader/coremod injection fault, prompt user to reinstall game.
@ -57,7 +61,7 @@ public final class CrashReportAnalyzer {
FILE_ALREADY_EXISTS(Pattern.compile("java\\.nio\\.file\\.FileAlreadyExistsException: (?<file>.*)"), "file"), FILE_ALREADY_EXISTS(Pattern.compile("java\\.nio\\.file\\.FileAlreadyExistsException: (?<file>.*)"), "file"),
// Forge found some mod crashed in game loading // Forge found some mod crashed in game loading
LOADING_CRASHED_FORGE(Pattern.compile("LoaderExceptionModCrash: Caught exception from (?<name>.*?) \\((?<id>.*)\\)"), "name", "id"), LOADING_CRASHED_FORGE(Pattern.compile("LoaderExceptionModCrash: Caught exception from (?<name>.*?) \\((?<id>.*)\\)"), "name", "id"),
BOOTSTRAP_FAILED(Pattern.compile("Failed to create mod instance. ModID: (?<id>.*?),"), "id"), BOOTSTRAP_FAILED(Pattern.compile("Failed to create mod instance\\. ModID: (?<id>.*?),"), "id"),
// Fabric found some mod crashed in game loading // Fabric found some mod crashed in game loading
LOADING_CRASHED_FABRIC(Pattern.compile("Could not execute entrypoint stage '(.*?)' due to errors, provided by '(?<id>.*)'!"), "id"), LOADING_CRASHED_FABRIC(Pattern.compile("Could not execute entrypoint stage '(.*?)' due to errors, provided by '(?<id>.*)'!"), "id"),
// Fabric may have breaking changes. // Fabric may have breaking changes.
@ -68,9 +72,9 @@ public final class CrashReportAnalyzer {
MODLAUNCHER_8(Pattern.compile("java\\.lang\\.NoSuchMethodError: ('void sun\\.security\\.util\\.ManifestEntryVerifier\\.<init>\\(java\\.util\\.jar\\.Manifest\\)'|sun\\.security\\.util\\.ManifestEntryVerifier\\.<init>\\(Ljava/util/jar/Manifest;\\)V)")), MODLAUNCHER_8(Pattern.compile("java\\.lang\\.NoSuchMethodError: ('void sun\\.security\\.util\\.ManifestEntryVerifier\\.<init>\\(java\\.util\\.jar\\.Manifest\\)'|sun\\.security\\.util\\.ManifestEntryVerifier\\.<init>\\(Ljava/util/jar/Manifest;\\)V)")),
// Manually triggerd debug crash // Manually triggerd debug crash
DEBUG_CRASH(Pattern.compile("Manually triggered debug crash")), DEBUG_CRASH(Pattern.compile("Manually triggered debug crash")),
CONFIG(Pattern.compile("Failed loading config file (?<file>.*?) of type SERVER for modid (?<id>.*)"), "id", "file"), CONFIG(Pattern.compile("Failed loading config file (?<file>.*?) of type (.*?) for modid (?<id>.*)"), "id", "file"),
// Fabric gives some warnings // Fabric gives some warnings
FABRIC_WARNINGS(Pattern.compile("Warnings were found!(.*?)[\\n\\r]+(?<reason>[^\\[]+)\\["), "reason"), FABRIC_WARNINGS(Pattern.compile("(Warnings were found!|Incompatible mod set!)(.*?)[\\n\\r]+(?<reason>[^\\[]+)\\["), "reason"),
// Game crashed when ticking entity // Game crashed when ticking entity
ENTITY(Pattern.compile("Entity Type: (?<type>.*)[\\w\\W\\n\\r]*?Entity's Exact location: (?<location>.*)"), "type", "location"), ENTITY(Pattern.compile("Entity Type: (?<type>.*)[\\w\\W\\n\\r]*?Entity's Exact location: (?<location>.*)"), "type", "location"),
// Game crashed when tesselating block model // Game crashed when tesselating block model
@ -78,10 +82,30 @@ public final class CrashReportAnalyzer {
// Cannot find native libraries // Cannot find native libraries
UNSATISFIED_LINK_ERROR(Pattern.compile("java.lang.UnsatisfiedLinkError: Failed to locate library: (?<name>.*)"), "name"), UNSATISFIED_LINK_ERROR(Pattern.compile("java.lang.UnsatisfiedLinkError: Failed to locate library: (?<name>.*)"), "name"),
OPTIFINE_IS_NOT_COMPATIBLE_WITH_FORGE(Pattern.compile("(java\\.lang\\.NoSuchMethodError: 'void net\\.minecraft\\.server\\.level\\.DistanceManager\\.addRegionTicket\\(net\\.minecraft\\.server\\.level\\.TicketType, net\\.minecraft\\.world\\.level\\.ChunkPos, int, java\\.lang\\.Object, boolean\\)'|java\\.lang\\.NoSuchMethodError: 'void net\\.minecraft\\.client\\.renderer\\.block\\.model\\.BakedQuad\\.<init>\\(int\\[\\], int, net\\.minecraft\\.core\\.Direction, net\\.minecraft\\.client\\.renderer\\.texture\\.TextureAtlasSprite, boolean, boolean\\)'|TRANSFORMER/net\\.optifine/net\\.optifine\\.reflect\\.Reflector\\.<clinit>\\(Reflector\\.java)")),
MOD_FILES_ARE_DECOMPRESSED(Pattern.compile("(The directories below appear to be extracted jar files\\. Fix this before you continue|Extracted mod jars found, loading will NOT continue)")),//Mod文件被解压
OPTIFINE_CAUSES_THE_WORLD_TO_FAIL_TO_LOAD(Pattern.compile("java\\.lang\\.NoSuchMethodError: net\\.minecraft\\.world\\.server\\.ChunkManager$ProxyTicketManager\\.shouldForceTicks\\(J\\)Z")),//OptiFine导致无法加载世界 https://www.minecraftforum.net/forums/support/java-edition-support/3051132-exception-ticking-world
TOO_MANY_MODS_LEAD_TO_EXCEEDING_THE_ID_LIMIT(Pattern.compile("maximum id range exceeded")),//Mod过多导致超出ID限制
// Mod issues // Mod issues
// TwilightForest is not compatible with OptiFine on Minecraft 1.16. MODMIXIN_FAILURE(Pattern.compile("(Mixin prepare failed |Mixin apply failed |mixin\\.injection\\.throwables\\.|\\.mixins\\.json\\] FAILED during \\))")),//ModMixin失败
MOD_REPEAT_INSTALLATION(Pattern.compile("(DuplicateModsFoundException|ModResolutionException: Duplicate)")),//Mod重复安装
FORGE_ERROR(Pattern.compile("An exception was thrown, the game will display an error screen and halt.")),//Forge报错,Forge可能已经提供了错误信息
MOD_RESOLUTION0(Pattern.compile("(Multiple entries with same key: |Failure message: MISSING)")),//可能是Mod问题
FORGE_REPEAT_INSTALLATION(Pattern.compile("--launchTarget, fmlclient, --fml.forgeVersion,[\\w\\W]*?--launchTarget, fmlclient, --fml.forgeVersion,[\\w\\W\\n\\r]*?MultipleArgumentsForOptionException: Found multiple arguments for option gameDir, but you asked for only one")),
OPTIFINE_REPEAT_INSTALLATION(Pattern.compile("ResolutionException: Module optifine reads another module named optifine")),//Optifine 重复安装及Mod文件夹有自动安装也有
JAVA_VERSION_IS_TOO_HIGH(Pattern.compile("(Unable to make protected final java\\.lang\\.Class java\\.lang\\.ClassLoader\\.defineClass|java\\.lang\\.NoSuchFieldException: ucp|Unsupported class file major version|because module java\\.base does not export|java\\.lang\\.ClassNotFoundException: jdk\\.nashorn\\.api\\.scripting\\.NashornScriptEngineFactory|java\\.lang\\.ClassNotFoundException: java\\.lang\\.invoke\\.LambdaMetafactory)")),//Java版本过高
//Forge 默认会把每一个 mod jar 都当做一个 JPMS 的模块Module加载在这个 jar 没有给出 module-info 声明的情况下JPMS 会采用这样的顺序决定 module 名字
//1. META-INF/MANIFEST.MF 里的 Automatic-Module-Name
//2. 根据文件名生成文件名里的 .jar 后缀名先去掉然后检查是否有 -(\\d+(\\.|$)) 的部分有的话只取 - 前面的部分- 后面的部分成为 module 的版本号即尝试判断文件名里是否有版本号有的话去掉然后把不是拉丁字母和数字的字符正则表达式 [^A-Za-z0-9]都换成点然后把连续的多个点换成一个点最后去掉开头和结尾的点那么
//按照 2.如果你的文件名是拔刀剑.jar那么这么一通流程下来你得到的 module 名就是空字符串而这是不允许的(来自 @Föhn 说明)
MOD_NAME(Pattern.compile("Invalid module name: '' is not a Java identifier")),
//Forge 安装不完整
INCOMPLETE_FORGE_INSTALLATION(Pattern.compile("(java\\.io\\.UncheckedIOException: java\\.io\\.IOException: Invalid paths argument, contained no existing paths: \\[(.*?)\\\\libraries\\\\net\\\\minecraftforge\\\\forge\\\\(.*?)\\\\forge-(.*?)-client\\.jar\\]|Failed to find Minecraft resource version (.*?) at (.*?)\\\\libraries\\\\net\\\\minecraftforge\\\\forge\\\\(.*?)\\\\forge-(.*?)-client\\.jar|Cannot find launch target fmlclient, unable to launch)")),
// TwilightForest is not compatible with OptiFine on Minecraft 1.16
TWILIGHT_FOREST_OPTIFINE(Pattern.compile("java.lang.IllegalArgumentException: (.*) outside of image bounds (.*)")); TWILIGHT_FOREST_OPTIFINE(Pattern.compile("java.lang.IllegalArgumentException: (.*) outside of image bounds (.*)"));
private final Pattern pattern; private final Pattern pattern;
@ -159,16 +183,16 @@ public final class CrashReportAnalyzer {
private static final Pattern STACK_TRACE_LINE_PATTERN = Pattern.compile("at (?<method>.*?)\\((?<sourcefile>.*?)\\)"); private static final Pattern STACK_TRACE_LINE_PATTERN = Pattern.compile("at (?<method>.*?)\\((?<sourcefile>.*?)\\)");
private static final Pattern STACK_TRACE_LINE_MODULE_PATTERN = Pattern.compile("\\{(?<tokens>.*)}"); private static final Pattern STACK_TRACE_LINE_MODULE_PATTERN = Pattern.compile("\\{(?<tokens>.*)}");
private static final Set<String> PACKAGE_KEYWORD_BLACK_LIST = new HashSet<>(Arrays.asList( private static final Set<String> PACKAGE_KEYWORD_BLACK_LIST = new HashSet<>(Arrays.asList(
"net", "minecraft", "item", "block", "player", "tileentity", "events", "common", "client", "entity", "mojang", "main", "gui", "world", "server", "dedicated", // minecraft "net", "minecraft", "item", "setup", "block", "assist", "optifine", "player", "unimi", "fastutil", "tileentity", "events", "common", "blockentity", "client", "entity", "mojang", "main", "gui", "world", "server", "dedicated", // minecraft
"renderer", "chunk", "model", "loading", "color", "pipeline", "inventory", "launcher", "physics", "particle", "gen", "registry", "worldgen", "texture", "biomes", "biome", "renderer", "chunk", "model", "loading", "color", "pipeline", "inventory", "launcher", "physics", "particle", "gen", "registry", "worldgen", "texture", "biomes", "biome",
"monster", "passive", "ai", "integrated", "tile", "state", "play", "structure", "nbt", "pathfinding", "chunk", "audio", "entities", "items", "renderers", "monster", "passive", "ai", "integrated", "tile", "state", "play", "override", "transformers", "structure", "nbt", "pathfinding", "chunk", "audio", "entities", "items", "renderers",
"storage", "storage",
"java", "lang", "util", "nio", "io", "sun", "reflect", "zip", "jdk", "nashorn", "scripts", "runtime", "internal", // java "java", "lang", "util", "nio", "io", "sun", "reflect", "zip", "jar", "jdk", "nashorn", "scripts", "runtime", "internal", // java
"mods", "mod", "impl", "org", "com", "cn", "cc", "jp", // title "mods", "mod", "impl", "org", "com", "cn", "cc", "jp", // title
"core", "config", "registries", "lib", "ruby", "mc", "codec", "channel", "embedded", "netty", "network", "handler", "feature", // misc "core", "config", "registries", "lib", "ruby", "mc", "codec", "recipe", "channel", "embedded", "done", "net", "netty", "network", "load", "github", "handler", "content", "feature", // misc
"file", "machine", "shader", "general", "helper", "init", "library", "api", "integration", "engine", "preload", "preinit", "file", "machine", "shader", "general", "helper", "init", "library", "api", "integration", "engine", "preload", "preinit",
"fcl", "tungsten", // fcl "fcl", "tungsten", // fcl
"fml", "minecraftforge", "forge", "cpw", "modlauncher", "launchwrapper", "objectweb", "asm", "event", "eventhandler", "handshake", "kcauldron", // forge "fml", "minecraftforge", "forge", "cpw", "modlauncher", "launchwrapper", "objectweb", "asm", "event", "eventhandler", "handshake", "modapi", "kcauldron", // forge
"fabricmc", "loader", "game", "knot", "launch", "mixin" // fabric "fabricmc", "loader", "game", "knot", "launch", "mixin" // fabric
)); ));

View File

@ -48,7 +48,7 @@ public class DefaultGameRepository implements GameRepository {
private File baseDirectory; private File baseDirectory;
protected Map<String, Version> versions; protected Map<String, Version> versions;
private ConcurrentHashMap<File, Optional<String>> gameVersions = new ConcurrentHashMap<>(); private final ConcurrentHashMap<File, Optional<String>> gameVersions = new ConcurrentHashMap<>();
public DefaultGameRepository(File baseDirectory) { public DefaultGameRepository(File baseDirectory) {
this.baseDirectory = baseDirectory; this.baseDirectory = baseDirectory;
@ -126,19 +126,13 @@ public class DefaultGameRepository implements GameRepository {
// This implementation may cause multiple flows against the same version entering // This implementation may cause multiple flows against the same version entering
// this function, which is accepted because GameVersion::minecraftVersion should // this function, which is accepted because GameVersion::minecraftVersion should
// be consistent. // be consistent.
File versionJar = getVersionJar(version); return gameVersions.computeIfAbsent(getVersionJar(version), versionJar -> {
if (gameVersions.containsKey(versionJar)) {
return gameVersions.get(versionJar);
} else {
Optional<String> gameVersion = GameVersion.minecraftVersion(versionJar); Optional<String> gameVersion = GameVersion.minecraftVersion(versionJar);
if (!gameVersion.isPresent()) { if (!gameVersion.isPresent()) {
LOG.warning("Cannot find out game version of " + version.getId() + ", primary jar: " + versionJar.toString() + ", jar exists: " + versionJar.exists()); LOG.warning("Cannot find out game version of " + version.getId() + ", primary jar: " + versionJar.toString() + ", jar exists: " + versionJar.exists());
} }
gameVersions.put(versionJar, gameVersion);
return gameVersion; return gameVersion;
} });
} }
@Override @Override
@ -168,7 +162,7 @@ public class DefaultGameRepository implements GameRepository {
} catch (JsonParseException ignored) { } catch (JsonParseException ignored) {
} }
LOG.warning("Cannot parse version json + " + file.toString() + "\n" + jsonText); LOG.warning("Cannot parse version json: " + file.toString() + "\n" + jsonText);
throw new JsonParseException("Version json incorrect"); throw new JsonParseException("Version json incorrect");
} }

View File

@ -3,7 +3,6 @@ package com.tungsten.fclcore.game;
import com.google.gson.JsonParseException; import com.google.gson.JsonParseException;
import com.google.gson.annotations.SerializedName; import com.google.gson.annotations.SerializedName;
import com.tungsten.fclcore.util.DigestUtils; import com.tungsten.fclcore.util.DigestUtils;
import com.tungsten.fclcore.util.Hex;
import com.tungsten.fclcore.util.StringUtils; import com.tungsten.fclcore.util.StringUtils;
import com.tungsten.fclcore.util.ToStringBuilder; import com.tungsten.fclcore.util.ToStringBuilder;
import com.tungsten.fclcore.util.gson.TolerableValidationException; import com.tungsten.fclcore.util.gson.TolerableValidationException;
@ -64,6 +63,6 @@ public class DownloadInfo implements Validation {
public boolean validateChecksum(Path file, boolean defaultValue) throws IOException { public boolean validateChecksum(Path file, boolean defaultValue) throws IOException {
if (getSha1() == null) return defaultValue; if (getSha1() == null) return defaultValue;
return Hex.encodeHex(DigestUtils.digest("SHA-1", file)).equalsIgnoreCase(getSha1()); return DigestUtils.digestToString("SHA-1", file).equalsIgnoreCase(getSha1());
} }
} }

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 version the id of specific version that is relevant to {@code assetId}
* @param assetId the asset id, you can find it in {@link AssetIndexInfo#getId()} {@link Version#getAssetIndex()} * @param assetId the asset id, you can find it in {@link AssetIndexInfo#getId()} {@link Version#getAssetIndex()}
* @param name the asset object name, you can find it in keys of {@link AssetIndex#getObjects()} * @param name the asset object name, you can find it in keys of {@link AssetIndex#getObjects()}
* @throws IOException if I/O operation fails. * @throws java.io.IOException if I/O operation fails.
* @return the file that given asset object refers to * @return the file that given asset object refers to
*/ */
Optional<Path> getAssetObject(String version, String assetId, String name) throws IOException; Optional<Path> getAssetObject(String version, String assetId, String name) throws IOException;

View File

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

View File

@ -18,9 +18,10 @@ public class LaunchOptions implements Serializable {
private String versionName; private String versionName;
private String versionType; private String versionType;
private String profileName; private String profileName;
private List<String> gameArguments = new ArrayList<>(); private final List<String> gameArguments = new ArrayList<>();
private List<String> javaArguments = new ArrayList<>(); private final List<String> overrideJavaArguments = new ArrayList<>();
private List<String> javaAgents = new ArrayList<>(0); private final List<String> javaArguments = new ArrayList<>();
private final List<String> javaAgents = new ArrayList<>(0);
private Integer minMemory; private Integer minMemory;
private Integer maxMemory; private Integer maxMemory;
private Integer metaspace; private Integer metaspace;
@ -76,6 +77,14 @@ public class LaunchOptions implements Serializable {
return Collections.unmodifiableList(gameArguments); return Collections.unmodifiableList(gameArguments);
} }
/**
* The highest priority JVM arguments (overrides the version setting)
*/
@NotNull
public List<String> getOverrideJavaArguments() {
return Collections.unmodifiableList(overrideJavaArguments);
}
/** /**
* User custom additional java virtual machine command line arguments. * User custom additional java virtual machine command line arguments.
*/ */
@ -206,6 +215,13 @@ public class LaunchOptions implements Serializable {
return options.gameArguments; return options.gameArguments;
} }
/**
* The highest priority JVM arguments (overrides the version setting)
*/
public List<String> getOverrideJavaArguments() {
return options.overrideJavaArguments;
}
/** /**
* User custom additional java virtual machine command line arguments. * User custom additional java virtual machine command line arguments.
*/ */
@ -313,6 +329,12 @@ public class LaunchOptions implements Serializable {
return this; return this;
} }
public Builder setOverrideJavaArguments(List<String> overrideJavaArguments) {
options.overrideJavaArguments.clear();
options.overrideJavaArguments.addAll(overrideJavaArguments);
return this;
}
public Builder setJavaArguments(List<String> javaArguments) { public Builder setJavaArguments(List<String> javaArguments) {
options.javaArguments.clear(); options.javaArguments.clear();
options.javaArguments.addAll(javaArguments); options.javaArguments.addAll(javaArguments);

View File

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

View File

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

View File

@ -250,7 +250,7 @@ public class DefaultLauncher extends Launcher {
public String getInjectorArg() { public String getInjectorArg() {
try { try {
String map = IOUtils.readFullyAsString(DefaultLauncher.class.getResourceAsStream("/assets/map.json"), StandardCharsets.UTF_8); String map = IOUtils.readFullyAsString(DefaultLauncher.class.getResourceAsStream("/assets/map.json"));
InjectorMap injectorMap = new GsonBuilder() InjectorMap injectorMap = new GsonBuilder()
.setPrettyPrinting() .setPrettyPrinting()
.create() .create()

View File

@ -3,7 +3,6 @@ package com.tungsten.fclcore.mod;
import com.google.gson.*; import com.google.gson.*;
import com.google.gson.annotations.JsonAdapter; import com.google.gson.annotations.JsonAdapter;
import com.tungsten.fclcore.util.gson.JsonUtils; import com.tungsten.fclcore.util.gson.JsonUtils;
import com.tungsten.fclcore.util.io.CompressingUtils;
import com.tungsten.fclcore.util.io.FileUtils; import com.tungsten.fclcore.util.io.FileUtils;
import java.io.IOException; import java.io.IOException;
@ -39,16 +38,14 @@ public final class FabricModMetadata {
this.contact = contact; this.contact = contact;
} }
public static LocalModFile fromFile(ModManager modManager, Path modFile) throws IOException, JsonParseException { public static LocalModFile fromFile(ModManager modManager, Path modFile, FileSystem fs) throws IOException, JsonParseException {
try (FileSystem fs = CompressingUtils.createReadOnlyZipFileSystem(modFile)) { Path mcmod = fs.getPath("fabric.mod.json");
Path mcmod = fs.getPath("fabric.mod.json"); if (Files.notExists(mcmod))
if (Files.notExists(mcmod)) throw new IOException("File " + modFile + " is not a Fabric mod.");
throw new IOException("File " + modFile + " is not a Fabric mod."); FabricModMetadata metadata = JsonUtils.fromNonNullJson(FileUtils.readText(mcmod), FabricModMetadata.class);
FabricModMetadata metadata = JsonUtils.fromNonNullJson(FileUtils.readText(mcmod), FabricModMetadata.class); String authors = metadata.authors == null ? "" : metadata.authors.stream().map(author -> author.name).collect(Collectors.joining(", "));
String authors = metadata.authors == null ? "" : metadata.authors.stream().map(author -> author.name).collect(Collectors.joining(", ")); return new LocalModFile(modManager, modManager.getLocalMod(metadata.id, ModLoaderType.FABRIC), modFile, metadata.name, new LocalModFile.Description(metadata.description),
return new LocalModFile(modManager, modManager.getLocalMod(metadata.id, ModLoaderType.FABRIC), modFile, metadata.name, new LocalModFile.Description(metadata.description), authors, metadata.version, "", metadata.contact != null ? metadata.contact.getOrDefault("homepage", "") : "", metadata.icon);
authors, metadata.version, "", metadata.contact != null ? metadata.contact.getOrDefault("homepage", "") : "", metadata.icon);
}
} }
@JsonAdapter(FabricModAuthorSerializer.class) @JsonAdapter(FabricModAuthorSerializer.class)

View File

@ -4,10 +4,10 @@ import static com.tungsten.fclcore.util.Logging.LOG;
import com.google.gson.JsonParseException; import com.google.gson.JsonParseException;
import com.moandjiezana.toml.Toml; import com.moandjiezana.toml.Toml;
import com.tungsten.fclcore.util.io.CompressingUtils;
import com.tungsten.fclcore.util.io.FileUtils; import com.tungsten.fclcore.util.io.FileUtils;
import java.io.IOException; import java.io.IOException;
import java.io.InputStream;
import java.nio.file.FileSystem; import java.nio.file.FileSystem;
import java.nio.file.Files; import java.nio.file.Files;
import java.nio.file.Path; import java.nio.file.Path;
@ -113,29 +113,27 @@ public final class ForgeNewModMetadata {
} }
} }
public static LocalModFile fromFile(ModManager modManager, Path modFile) throws IOException, JsonParseException { public static LocalModFile fromFile(ModManager modManager, Path modFile, FileSystem fs) throws IOException, JsonParseException {
try (FileSystem fs = CompressingUtils.createReadOnlyZipFileSystem(modFile)) { Path modstoml = fs.getPath("META-INF/mods.toml");
Path modstoml = fs.getPath("META-INF/mods.toml"); if (Files.notExists(modstoml))
if (Files.notExists(modstoml)) throw new IOException("File " + modFile + " is not a Forge 1.13+ mod.");
throw new IOException("File " + modFile + " is not a Forge 1.13+ mod."); ForgeNewModMetadata metadata = new Toml().read(FileUtils.readText(modstoml)).to(ForgeNewModMetadata.class);
ForgeNewModMetadata metadata = new Toml().read(FileUtils.readText(modstoml)).to(ForgeNewModMetadata.class); if (metadata == null || metadata.getMods().isEmpty())
if (metadata == null || metadata.getMods().isEmpty()) throw new IOException("Mod " + modFile + " `mods.toml` is malformed..");
throw new IOException("Mod " + modFile + " `mods.toml` is malformed.."); Mod mod = metadata.getMods().get(0);
Mod mod = metadata.getMods().get(0); Path manifestMF = fs.getPath("META-INF/MANIFEST.MF");
Path manifestMF = fs.getPath("META-INF/MANIFEST.MF"); String jarVersion = "";
String jarVersion = ""; if (Files.exists(manifestMF)) {
if (Files.exists(manifestMF)) { try (InputStream is = Files.newInputStream(manifestMF)) {
try { Manifest manifest = new Manifest(is);
Manifest manifest = new Manifest(Files.newInputStream(manifestMF)); jarVersion = manifest.getMainAttributes().getValue(Attributes.Name.IMPLEMENTATION_VERSION);
jarVersion = manifest.getMainAttributes().getValue(Attributes.Name.IMPLEMENTATION_VERSION); } catch (IOException e) {
} catch (IOException e) { LOG.log(Level.WARNING, "Failed to parse MANIFEST.MF in file " + modFile);
LOG.log(Level.WARNING, "Failed to parse MANIFEST.MF in file " + modFile);
}
} }
return new LocalModFile(modManager, modManager.getLocalMod(mod.getModId(), ModLoaderType.FORGE), modFile, mod.getDisplayName(), new LocalModFile.Description(mod.getDescription()),
mod.getAuthors(), mod.getVersion().replace("${file.jarVersion}", jarVersion), "",
mod.getDisplayURL(),
metadata.getLogoFile());
} }
return new LocalModFile(modManager, modManager.getLocalMod(mod.getModId(), ModLoaderType.FORGE), modFile, mod.getDisplayName(), new LocalModFile.Description(mod.getDescription()),
mod.getAuthors(), mod.getVersion().replace("${file.jarVersion}", jarVersion), "",
mod.getDisplayURL(),
metadata.getLogoFile());
} }
} }

View File

@ -5,7 +5,6 @@ import com.google.gson.annotations.SerializedName;
import com.google.gson.reflect.TypeToken; import com.google.gson.reflect.TypeToken;
import com.tungsten.fclcore.util.StringUtils; import com.tungsten.fclcore.util.StringUtils;
import com.tungsten.fclcore.util.gson.JsonUtils; import com.tungsten.fclcore.util.gson.JsonUtils;
import com.tungsten.fclcore.util.io.CompressingUtils;
import com.tungsten.fclcore.util.io.FileUtils; import com.tungsten.fclcore.util.io.FileUtils;
import java.io.IOException; import java.io.IOException;
@ -97,28 +96,26 @@ public final class ForgeOldModMetadata {
return authors; return authors;
} }
public static LocalModFile fromFile(ModManager modManager, Path modFile) throws IOException, JsonParseException { public static LocalModFile fromFile(ModManager modManager, Path modFile, FileSystem fs) throws IOException, JsonParseException {
try (FileSystem fs = CompressingUtils.createReadOnlyZipFileSystem(modFile)) { Path mcmod = fs.getPath("mcmod.info");
Path mcmod = fs.getPath("mcmod.info"); if (Files.notExists(mcmod))
if (Files.notExists(mcmod)) throw new IOException("File " + modFile + " is not a Forge mod.");
throw new IOException("File " + modFile + " is not a Forge mod."); List<ForgeOldModMetadata> modList = JsonUtils.GSON.fromJson(FileUtils.readText(mcmod),
List<ForgeOldModMetadata> modList = JsonUtils.GSON.fromJson(FileUtils.readText(mcmod), new TypeToken<List<ForgeOldModMetadata>>() {
new TypeToken<List<ForgeOldModMetadata>>() { }.getType());
}.getType()); if (modList == null || modList.isEmpty())
if (modList == null || modList.isEmpty()) throw new IOException("Mod " + modFile + " `mcmod.info` is malformed..");
throw new IOException("Mod " + modFile + " `mcmod.info` is malformed.."); ForgeOldModMetadata metadata = modList.get(0);
ForgeOldModMetadata metadata = modList.get(0); String authors = metadata.getAuthor();
String authors = metadata.getAuthor(); if (StringUtils.isBlank(authors) && metadata.getAuthors().length > 0)
if (StringUtils.isBlank(authors) && metadata.getAuthors().length > 0) authors = String.join(", ", metadata.getAuthors());
authors = String.join(", ", metadata.getAuthors()); if (StringUtils.isBlank(authors) && metadata.getAuthorList().length > 0)
if (StringUtils.isBlank(authors) && metadata.getAuthorList().length > 0) authors = String.join(", ", metadata.getAuthorList());
authors = String.join(", ", metadata.getAuthorList()); if (StringUtils.isBlank(authors))
if (StringUtils.isBlank(authors)) authors = metadata.getCredits();
authors = metadata.getCredits(); return new LocalModFile(modManager, modManager.getLocalMod(metadata.getModId(), ModLoaderType.FORGE), modFile, metadata.getName(), new LocalModFile.Description(metadata.getDescription()),
return new LocalModFile(modManager, modManager.getLocalMod(metadata.getModId(), ModLoaderType.FORGE), modFile, metadata.getName(), new LocalModFile.Description(metadata.getDescription()), authors, metadata.getVersion(), metadata.getGameVersion(),
authors, metadata.getVersion(), metadata.getGameVersion(), StringUtils.isBlank(metadata.getUrl()) ? metadata.getUpdateUrl() : metadata.url,
StringUtils.isBlank(metadata.getUrl()) ? metadata.getUpdateUrl() : metadata.url, metadata.getLogoFile());
metadata.getLogoFile());
}
} }
} }

View File

@ -2,7 +2,6 @@ package com.tungsten.fclcore.mod;
import com.google.gson.JsonParseException; import com.google.gson.JsonParseException;
import com.tungsten.fclcore.util.gson.JsonUtils; import com.tungsten.fclcore.util.gson.JsonUtils;
import com.tungsten.fclcore.util.io.IOUtils;
import java.io.IOException; import java.io.IOException;
import java.nio.file.Path; import java.nio.file.Path;
@ -83,18 +82,18 @@ public final class LiteModMetadata {
public String getUpdateURI() { public String getUpdateURI() {
return updateURI; return updateURI;
} }
public static LocalModFile fromFile(ModManager modManager, Path modFile) throws IOException, JsonParseException { public static LocalModFile fromFile(ModManager modManager, Path modFile) throws IOException, JsonParseException {
try (ZipFile zipFile = new ZipFile(modFile.toFile())) { try (ZipFile zipFile = new ZipFile(modFile.toFile())) {
ZipEntry entry = zipFile.getEntry("litemod.json"); ZipEntry entry = zipFile.getEntry("litemod.json");
if (entry == null) if (entry == null)
throw new IOException("File " + modFile + "is not a LiteLoader mod."); throw new IOException("File " + modFile + "is not a LiteLoader mod.");
LiteModMetadata metadata = JsonUtils.GSON.fromJson(IOUtils.readFullyAsString(zipFile.getInputStream(entry)), LiteModMetadata.class); LiteModMetadata metadata = JsonUtils.fromJsonFully(zipFile.getInputStream(entry), LiteModMetadata.class);
if (metadata == null) if (metadata == null)
throw new IOException("Mod " + modFile + " `litemod.json` is malformed."); throw new IOException("Mod " + modFile + " `litemod.json` is malformed.");
return new LocalModFile(modManager, modManager.getLocalMod(metadata.getName(), ModLoaderType.LITE_LOADER), modFile, metadata.getName(), new LocalModFile.Description(metadata.getDescription()), metadata.getAuthor(), return new LocalModFile(modManager, modManager.getLocalMod(metadata.getName(), ModLoaderType.LITE_LOADER), modFile, metadata.getName(), new LocalModFile.Description(metadata.getDescription()), metadata.getAuthor(),
metadata.getVersion(), metadata.getGameVersion(), metadata.getUpdateURI(), ""); metadata.getVersion(), metadata.getGameVersion(), metadata.getUpdateURI(), "");
} }
} }
} }

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 { public ModUpdate checkUpdates(String gameVersion, RemoteModRepository repository) throws IOException {
Optional<RemoteMod.Version> currentVersion = repository.getRemoteVersionByLocalFile(this, file); Optional<RemoteMod.Version> currentVersion = repository.getRemoteVersionByLocalFile(this, file);
if (!currentVersion.isPresent()) return null; if (!currentVersion.isPresent()) return null;

View File

@ -1,9 +1,7 @@
package com.tungsten.fclcore.mod; package com.tungsten.fclcore.mod;
import static com.tungsten.fclcore.util.DigestUtils.digest;
import static com.tungsten.fclcore.util.Hex.encodeHex;
import com.tungsten.fclcore.task.Task; import com.tungsten.fclcore.task.Task;
import com.tungsten.fclcore.util.DigestUtils;
import com.tungsten.fclcore.util.gson.JsonUtils; import com.tungsten.fclcore.util.gson.JsonUtils;
import com.tungsten.fclcore.util.io.CompressingUtils; import com.tungsten.fclcore.util.io.CompressingUtils;
import com.tungsten.fclcore.util.io.FileUtils; import com.tungsten.fclcore.util.io.FileUtils;
@ -52,7 +50,7 @@ public final class MinecraftInstanceTask<T> extends Task<ModpackConfiguration<T>
@Override @Override
public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException { public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException {
String relativePath = root.relativize(file).normalize().toString().replace(File.separatorChar, '/'); String relativePath = root.relativize(file).normalize().toString().replace(File.separatorChar, '/');
overrides.add(new ModpackConfiguration.FileInformation(relativePath, encodeHex(digest("SHA-1", file)))); overrides.add(new ModpackConfiguration.FileInformation(relativePath, DigestUtils.digestToString("SHA-1", file)));
return FileVisitResult.CONTINUE; return FileVisitResult.CONTINUE;
} }
}); });

View File

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

View File

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

View File

@ -56,24 +56,27 @@ public final class ModManager {
String fileName = StringUtils.removeSuffix(FileUtils.getName(modFile), DISABLED_EXTENSION, OLD_EXTENSION); String fileName = StringUtils.removeSuffix(FileUtils.getName(modFile), DISABLED_EXTENSION, OLD_EXTENSION);
String description; String description;
if (fileName.endsWith(".zip") || fileName.endsWith(".jar")) { if (fileName.endsWith(".zip") || fileName.endsWith(".jar")) {
try { try (FileSystem fs = CompressingUtils.createReadOnlyZipFileSystem(modFile)) {
return ForgeOldModMetadata.fromFile(this, modFile); try {
} catch (Exception ignore) { return ForgeOldModMetadata.fromFile(this, modFile, fs);
} } catch (Exception ignore) {
}
try { try {
return ForgeNewModMetadata.fromFile(this, modFile); return ForgeNewModMetadata.fromFile(this, modFile, fs);
} catch (Exception ignore) { } catch (Exception ignore) {
} }
try { try {
return FabricModMetadata.fromFile(this, modFile); return FabricModMetadata.fromFile(this, modFile, fs);
} catch (Exception ignore) { } catch (Exception ignore) {
} }
try { try {
return PackMcMeta.fromFile(this, modFile); return PackMcMeta.fromFile(this, modFile, fs);
} catch (Exception ignore) { } catch (Exception ignore) {
}
} catch (Exception ignored) {
} }
description = ""; description = "";
@ -214,7 +217,11 @@ public final class ModManager {
public Path disableMod(Path file) throws IOException { public Path disableMod(Path file) throws IOException {
if (isOld(file)) return file; // no need to disable an old mod. if (isOld(file)) return file; // no need to disable an old mod.
Path disabled = file.resolveSibling(StringUtils.addSuffix(FileUtils.getName(file), DISABLED_EXTENSION));
String fileName = FileUtils.getName(file);
if (fileName.endsWith(DISABLED_EXTENSION)) return file;
Path disabled = file.resolveSibling(fileName + DISABLED_EXTENSION);
if (Files.exists(file)) if (Files.exists(file))
Files.move(file, disabled, StandardCopyOption.REPLACE_EXISTING); Files.move(file, disabled, StandardCopyOption.REPLACE_EXISTING);
return disabled; return disabled;

View File

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

View File

@ -1,9 +1,7 @@
package com.tungsten.fclcore.mod; package com.tungsten.fclcore.mod;
import static com.tungsten.fclcore.util.DigestUtils.digest;
import static com.tungsten.fclcore.util.Hex.encodeHex;
import com.tungsten.fclcore.task.Task; import com.tungsten.fclcore.task.Task;
import com.tungsten.fclcore.util.DigestUtils;
import com.tungsten.fclcore.util.io.FileUtils; import com.tungsten.fclcore.util.io.FileUtils;
import com.tungsten.fclcore.util.io.Unzipper; import com.tungsten.fclcore.util.io.Unzipper;
@ -76,7 +74,7 @@ public class ModpackInstallTask<T> extends Task<Void> {
} else { } else {
// If both old and new modpacks have this entry, and user has modified this file, // If both old and new modpacks have this entry, and user has modified this file,
// we will not replace it since this modified file is what user expects. // we will not replace it since this modified file is what user expects.
String fileHash = encodeHex(digest("SHA-1", destPath)); String fileHash = DigestUtils.digestToString("SHA-1", destPath);
String oldHash = files.get(entryPath).getHash(); String oldHash = files.get(entryPath).getHash();
return Objects.equals(oldHash, fileHash); return Objects.equals(oldHash, fileHash);
} }

View File

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

View File

@ -2,6 +2,8 @@ package com.tungsten.fclcore.mod;
import static com.tungsten.fclcore.util.io.NetworkUtils.encodeLocation; import static com.tungsten.fclcore.util.io.NetworkUtils.encodeLocation;
import com.tungsten.fclcore.mod.curse.CurseForgeRemoteModRepository;
import com.tungsten.fclcore.mod.modrinth.ModrinthRemoteModRepository;
import com.tungsten.fclcore.task.FileDownloadTask; import com.tungsten.fclcore.task.FileDownloadTask;
import java.io.IOException; import java.io.IOException;
@ -70,8 +72,18 @@ public class RemoteMod {
} }
public enum Type { public enum Type {
CURSEFORGE, CURSEFORGE(CurseForgeRemoteModRepository.MODS),
MODRINTH MODRINTH(ModrinthRemoteModRepository.MODS);
private final RemoteModRepository remoteModRepository;
public RemoteModRepository getRemoteModRepository() {
return this.remoteModRepository;
}
Type(RemoteModRepository remoteModRepository) {
this.remoteModRepository = remoteModRepository;
}
} }
public interface IMod { public interface IMod {
@ -184,7 +196,7 @@ public class RemoteMod {
} }
public String getUrl() { public String getUrl() {
return encodeLocation (url); return encodeLocation(url);
} }
public String getFilename() { public String getFilename() {

View File

@ -21,10 +21,10 @@ public interface RemoteModRepository {
Type getType(); Type getType();
enum SortType { enum SortType {
DATE_CREATED,
POPULARITY, POPULARITY,
LAST_UPDATED,
NAME, NAME,
DATE_CREATED,
LAST_UPDATED,
AUTHOR, AUTHOR,
TOTAL_DOWNLOADS TOTAL_DOWNLOADS
} }
@ -72,6 +72,8 @@ public interface RemoteModRepository {
} }
String[] DEFAULT_GAME_VERSIONS = new String[]{ String[] DEFAULT_GAME_VERSIONS = new String[]{
"1.20.1", "1.20",
"1.19.4", "1.19.3", "1.19.2", "1.19.1", "1.19",
"1.18.2", "1.18.1", "1.18", "1.18.2", "1.18.1", "1.18",
"1.17.1", "1.17", "1.17.1", "1.17",
"1.16.5", "1.16.4", "1.16.3", "1.16.2", "1.16.1", "1.16", "1.16.5", "1.16.4", "1.16.3", "1.16.2", "1.16.1", "1.16",

View File

@ -532,15 +532,6 @@ public class CurseAddon implements RemoteMod.IMod {
break; break;
} }
ModLoaderType modLoaderType;
if (gameVersions.contains("Forge")) {
modLoaderType = ModLoaderType.FORGE;
} else if (gameVersions.contains("Fabric")) {
modLoaderType = ModLoaderType.FABRIC;
} else {
modLoaderType = ModLoaderType.UNKNOWN;
}
return new RemoteMod.Version( return new RemoteMod.Version(
this, this,
Integer.toString(modId), Integer.toString(modId),
@ -552,7 +543,12 @@ public class CurseAddon implements RemoteMod.IMod {
new RemoteMod.File(Collections.emptyMap(), getDownloadUrl(), getFileName()), new RemoteMod.File(Collections.emptyMap(), getDownloadUrl(), getFileName()),
Collections.emptyList(), Collections.emptyList(),
gameVersions.stream().filter(ver -> ver.startsWith("1.") || ver.contains("w")).collect(Collectors.toList()), gameVersions.stream().filter(ver -> ver.startsWith("1.") || ver.contains("w")).collect(Collectors.toList()),
Collections.singletonList(modLoaderType) gameVersions.stream().flatMap(version -> {
if ("fabric".equalsIgnoreCase(version)) return Stream.of(ModLoaderType.FABRIC);
else if ("forge".equalsIgnoreCase(version)) return Stream.of(ModLoaderType.FORGE);
else if ("quilt".equalsIgnoreCase(version)) return Stream.of(ModLoaderType.QUILT);
else return Stream.empty();
}).collect(Collectors.toList())
); );
} }
} }

View File

@ -24,9 +24,12 @@ import java.util.stream.Stream;
public final class CurseForgeRemoteModRepository implements RemoteModRepository { public final class CurseForgeRemoteModRepository implements RemoteModRepository {
private static final String PREFIX = "https://api.curseforge.com"; private static final String PREFIX = "https://api.curseforge.com";
private static final String apiKey = "$2a$10$qqJ3zZFG5CDsVHk8eV5ft.2ywg2edBtHwS3gzFnw7CDe3X2cKpWZG"; private static final String apiKey = "$2a$10$qqJ3zZFG5CDsVHk8eV5ft.2ywg2edBtHwS3gzFnw7CDe3X2cKpWZG";
public static boolean isAvailable() {
return true;
}
private final Type type; private final Type type;
private final int section; private final int section;
@ -72,7 +75,7 @@ public final class CurseForgeRemoteModRepository implements RemoteModRepository
} }
@Override @Override
public Stream<RemoteMod> search(String gameVersion, @Nullable Category category, int pageOffset, int pageSize, String searchFilter, SortType sortType, SortOrder sortOrder) throws IOException { public Stream<RemoteMod> search(String gameVersion, @Nullable RemoteModRepository.Category category, int pageOffset, int pageSize, String searchFilter, SortType sortType, SortOrder sortOrder) throws IOException {
int categoryId = 0; int categoryId = 0;
if (category != null) categoryId = ((CurseAddon.Category) category.getSelf()).getId(); if (category != null) categoryId = ((CurseAddon.Category) category.getSelf()).getId();
Response<List<CurseAddon>> response = HttpRequest.GET(PREFIX + "/v1/mods/search", Response<List<CurseAddon>> response = HttpRequest.GET(PREFIX + "/v1/mods/search",
@ -159,7 +162,7 @@ public final class CurseForgeRemoteModRepository implements RemoteModRepository
} }
@Override @Override
public Stream<Category> getCategories() throws IOException { public Stream<RemoteModRepository.Category> getCategories() throws IOException {
return getCategoriesImpl().stream().map(CurseAddon.Category::toCategory); return getCategoriesImpl().stream().map(CurseAddon.Category::toCategory);
} }
@ -196,11 +199,11 @@ public final class CurseForgeRemoteModRepository implements RemoteModRepository
public static final int SECTION_UNKNOWN2 = 4979; public static final int SECTION_UNKNOWN2 = 4979;
public static final int SECTION_UNKNOWN3 = 4984; public static final int SECTION_UNKNOWN3 = 4984;
public static final CurseForgeRemoteModRepository MODS = new CurseForgeRemoteModRepository(Type.MOD, SECTION_MOD); public static final CurseForgeRemoteModRepository MODS = new CurseForgeRemoteModRepository(RemoteModRepository.Type.MOD, SECTION_MOD);
public static final CurseForgeRemoteModRepository MODPACKS = new CurseForgeRemoteModRepository(Type.MODPACK, SECTION_MODPACK); public static final CurseForgeRemoteModRepository MODPACKS = new CurseForgeRemoteModRepository(RemoteModRepository.Type.MODPACK, SECTION_MODPACK);
public static final CurseForgeRemoteModRepository RESOURCE_PACKS = new CurseForgeRemoteModRepository(Type.RESOURCE_PACK, SECTION_RESOURCE_PACK); public static final CurseForgeRemoteModRepository RESOURCE_PACKS = new CurseForgeRemoteModRepository(RemoteModRepository.Type.RESOURCE_PACK, SECTION_RESOURCE_PACK);
public static final CurseForgeRemoteModRepository WORLDS = new CurseForgeRemoteModRepository(Type.WORLD, SECTION_WORLD); public static final CurseForgeRemoteModRepository WORLDS = new CurseForgeRemoteModRepository(RemoteModRepository.Type.WORLD, SECTION_WORLD);
public static final CurseForgeRemoteModRepository CUSTOMIZATIONS = new CurseForgeRemoteModRepository(Type.CUSTOMIZATION, SECTION_CUSTOMIZATION); public static final CurseForgeRemoteModRepository CUSTOMIZATIONS = new CurseForgeRemoteModRepository(RemoteModRepository.Type.CUSTOMIZATION, SECTION_CUSTOMIZATION);
public static class Pagination { public static class Pagination {
private final int index; private final int index;

View File

@ -1,7 +1,5 @@
package com.tungsten.fclcore.mod.mcbbs; package com.tungsten.fclcore.mod.mcbbs;
import static com.tungsten.fclcore.util.DigestUtils.digest;
import static com.tungsten.fclcore.util.Hex.encodeHex;
import static com.tungsten.fclcore.util.Lang.wrap; import static com.tungsten.fclcore.util.Lang.wrap;
import static com.tungsten.fclcore.util.Lang.wrapConsumer; import static com.tungsten.fclcore.util.Lang.wrapConsumer;
@ -18,6 +16,7 @@ import com.tungsten.fclcore.task.FileDownloadTask;
import com.tungsten.fclcore.task.GetTask; import com.tungsten.fclcore.task.GetTask;
import com.tungsten.fclcore.task.Task; import com.tungsten.fclcore.task.Task;
import com.tungsten.fclcore.task.TaskCompletableFuture; import com.tungsten.fclcore.task.TaskCompletableFuture;
import com.tungsten.fclcore.util.DigestUtils;
import com.tungsten.fclcore.util.Logging; import com.tungsten.fclcore.util.Logging;
import com.tungsten.fclcore.util.StringUtils; import com.tungsten.fclcore.util.StringUtils;
import com.tungsten.fclcore.util.gson.JsonUtils; import com.tungsten.fclcore.util.gson.JsonUtils;
@ -126,7 +125,7 @@ public class McbbsModpackCompletionTask extends CompletableFutureTask<Void> {
} else if (getFileHash(file) != null) { } else if (getFileHash(file) != null) {
// If user modified this entry file, we will not replace this file since this modified file is what user expects. // If user modified this entry file, we will not replace this file since this modified file is what user expects.
// Or we have downloaded latest file in previous completion task, this time we have no need to download it again. // Or we have downloaded latest file in previous completion task, this time we have no need to download it again.
String fileHash = encodeHex(digest("SHA-1", actualPath)); String fileHash = DigestUtils.digestToString("SHA-1", actualPath);
String oldHash = getFileHash(oldFile); String oldHash = getFileHash(oldFile);
String newHash = getFileHash(file); String newHash = getFileHash(file);
if (oldHash == null) { if (oldHash == null) {

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.LITELOADER;
import static com.tungsten.fclcore.download.LibraryAnalyzer.LibraryType.MINECRAFT; import static com.tungsten.fclcore.download.LibraryAnalyzer.LibraryType.MINECRAFT;
import static com.tungsten.fclcore.download.LibraryAnalyzer.LibraryType.OPTIFINE; import static com.tungsten.fclcore.download.LibraryAnalyzer.LibraryType.OPTIFINE;
import static com.tungsten.fclcore.util.DigestUtils.digest; import static com.tungsten.fclcore.download.LibraryAnalyzer.LibraryType.QUILT;
import static com.tungsten.fclcore.util.Hex.encodeHex;
import com.tungsten.fclcore.download.LibraryAnalyzer; import com.tungsten.fclcore.download.LibraryAnalyzer;
import com.tungsten.fclcore.game.DefaultGameRepository; import com.tungsten.fclcore.game.DefaultGameRepository;
@ -18,6 +17,7 @@ import com.tungsten.fclcore.mod.curse.CurseManifest;
import com.tungsten.fclcore.mod.curse.CurseManifestMinecraft; import com.tungsten.fclcore.mod.curse.CurseManifestMinecraft;
import com.tungsten.fclcore.mod.curse.CurseManifestModLoader; import com.tungsten.fclcore.mod.curse.CurseManifestModLoader;
import com.tungsten.fclcore.task.Task; import com.tungsten.fclcore.task.Task;
import com.tungsten.fclcore.util.DigestUtils;
import com.tungsten.fclcore.util.Logging; import com.tungsten.fclcore.util.Logging;
import com.tungsten.fclcore.util.StringUtils; import com.tungsten.fclcore.util.StringUtils;
import com.tungsten.fclcore.util.gson.JsonUtils; import com.tungsten.fclcore.util.gson.JsonUtils;
@ -62,7 +62,7 @@ public class McbbsModpackExportTask extends Task<Void> {
Path file = runDirectory.resolve(path); Path file = runDirectory.resolve(path);
if (Files.isRegularFile(file)) { if (Files.isRegularFile(file)) {
String relativePath = runDirectory.relativize(file).normalize().toString().replace(File.separatorChar, '/'); String relativePath = runDirectory.relativize(file).normalize().toString().replace(File.separatorChar, '/');
files.add(new McbbsModpackManifest.AddonFile(true, relativePath, encodeHex(digest("SHA-1", file)))); files.add(new McbbsModpackManifest.AddonFile(true, relativePath, DigestUtils.digestToString("SHA-1", file)));
} }
return true; return true;
} else { } else {
@ -85,6 +85,8 @@ public class McbbsModpackExportTask extends Task<Void> {
addons.add(new McbbsModpackManifest.Addon(OPTIFINE.getPatchId(), optifineVersion))); addons.add(new McbbsModpackManifest.Addon(OPTIFINE.getPatchId(), optifineVersion)));
analyzer.getVersion(FABRIC).ifPresent(fabricVersion -> analyzer.getVersion(FABRIC).ifPresent(fabricVersion ->
addons.add(new McbbsModpackManifest.Addon(FABRIC.getPatchId(), fabricVersion))); addons.add(new McbbsModpackManifest.Addon(FABRIC.getPatchId(), fabricVersion)));
analyzer.getVersion(QUILT).ifPresent(quiltVersion ->
addons.add(new McbbsModpackManifest.Addon(QUILT.getPatchId(), quiltVersion)));
List<Library> libraries = new ArrayList<>(); List<Library> libraries = new ArrayList<>();
// TODO libraries // TODO libraries

View File

@ -11,13 +11,13 @@ import com.tungsten.fclcore.mod.ModpackProvider;
import com.tungsten.fclcore.mod.ModpackUpdateTask; import com.tungsten.fclcore.mod.ModpackUpdateTask;
import com.tungsten.fclcore.task.Task; import com.tungsten.fclcore.task.Task;
import com.tungsten.fclcore.util.gson.JsonUtils; import com.tungsten.fclcore.util.gson.JsonUtils;
import com.tungsten.fclcore.util.io.IOUtils;
import org.apache.commons.compress.archivers.zip.ZipArchiveEntry; import org.apache.commons.compress.archivers.zip.ZipArchiveEntry;
import org.apache.commons.compress.archivers.zip.ZipFile; import org.apache.commons.compress.archivers.zip.ZipFile;
import java.io.File; import java.io.File;
import java.io.IOException; import java.io.IOException;
import java.io.InputStream;
import java.nio.charset.Charset; import java.nio.charset.Charset;
import java.nio.file.Path; import java.nio.file.Path;
@ -54,8 +54,8 @@ public final class McbbsModpackProvider implements ModpackProvider {
config.getManifest().injectLaunchOptions(builder); config.getManifest().injectLaunchOptions(builder);
} }
private static Modpack fromManifestFile(String json, Charset encoding) throws IOException, JsonParseException { private static Modpack fromManifestFile(InputStream json, Charset encoding) throws IOException, JsonParseException {
McbbsModpackManifest manifest = JsonUtils.fromNonNullJson(json, McbbsModpackManifest.class); McbbsModpackManifest manifest = JsonUtils.fromNonNullJsonFully(json, McbbsModpackManifest.class);
return manifest.toModpack(encoding); return manifest.toModpack(encoding);
} }
@ -63,11 +63,11 @@ public final class McbbsModpackProvider implements ModpackProvider {
public Modpack readManifest(ZipFile zip, Path file, Charset encoding) throws IOException, JsonParseException { public Modpack readManifest(ZipFile zip, Path file, Charset encoding) throws IOException, JsonParseException {
ZipArchiveEntry mcbbsPackMeta = zip.getEntry("mcbbs.packmeta"); ZipArchiveEntry mcbbsPackMeta = zip.getEntry("mcbbs.packmeta");
if (mcbbsPackMeta != null) { if (mcbbsPackMeta != null) {
return fromManifestFile(IOUtils.readFullyAsString(zip.getInputStream(mcbbsPackMeta)), encoding); return fromManifestFile(zip.getInputStream(mcbbsPackMeta), encoding);
} }
ZipArchiveEntry manifestJson = zip.getEntry("manifest.json"); ZipArchiveEntry manifestJson = zip.getEntry("manifest.json");
if (manifestJson != null) { if (manifestJson != null) {
return fromManifestFile(IOUtils.readFullyAsString(zip.getInputStream(manifestJson)), encoding); return fromManifestFile(zip.getInputStream(manifestJson), encoding);
} }
throw new IOException("`mcbbs.packmeta` or `manifest.json` cannot be found"); throw new IOException("`mcbbs.packmeta` or `manifest.json` cannot be found");
} }

View File

@ -10,7 +10,6 @@ import com.tungsten.fclcore.mod.ModLoaderType;
import com.tungsten.fclcore.mod.RemoteMod; import com.tungsten.fclcore.mod.RemoteMod;
import com.tungsten.fclcore.mod.RemoteModRepository; import com.tungsten.fclcore.mod.RemoteModRepository;
import com.tungsten.fclcore.util.DigestUtils; import com.tungsten.fclcore.util.DigestUtils;
import com.tungsten.fclcore.util.Hex;
import com.tungsten.fclcore.util.Lang; import com.tungsten.fclcore.util.Lang;
import com.tungsten.fclcore.util.StringUtils; import com.tungsten.fclcore.util.StringUtils;
import com.tungsten.fclcore.util.gson.JsonUtils; import com.tungsten.fclcore.util.gson.JsonUtils;
@ -85,7 +84,7 @@ public final class ModrinthRemoteModRepository implements RemoteModRepository {
@Override @Override
public Optional<RemoteMod.Version> getRemoteVersionByLocalFile(LocalModFile localModFile, Path file) throws IOException { public Optional<RemoteMod.Version> getRemoteVersionByLocalFile(LocalModFile localModFile, Path file) throws IOException {
String sha1 = Hex.encodeHex(DigestUtils.digest("SHA-1", file)); String sha1 = DigestUtils.digestToString("SHA-1", file);
try { try {
ProjectVersion mod = HttpRequest.GET(PREFIX + "/v2/version_file/" + sha1, ProjectVersion mod = HttpRequest.GET(PREFIX + "/v2/version_file/" + sha1,
@ -483,6 +482,7 @@ public final class ModrinthRemoteModRepository implements RemoteModRepository {
loaders.stream().flatMap(loader -> { loaders.stream().flatMap(loader -> {
if ("fabric".equalsIgnoreCase(loader)) return Stream.of(ModLoaderType.FABRIC); if ("fabric".equalsIgnoreCase(loader)) return Stream.of(ModLoaderType.FABRIC);
else if ("forge".equalsIgnoreCase(loader)) return Stream.of(ModLoaderType.FORGE); else if ("forge".equalsIgnoreCase(loader)) return Stream.of(ModLoaderType.FORGE);
else if ("quilt".equalsIgnoreCase(loader)) return Stream.of(ModLoaderType.QUILT);
else return Stream.empty(); else return Stream.empty();
}).collect(Collectors.toList()) }).collect(Collectors.toList())
)); ));

View File

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

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.FABRIC;
import static com.tungsten.fclcore.download.LibraryAnalyzer.LibraryType.FORGE; import static com.tungsten.fclcore.download.LibraryAnalyzer.LibraryType.FORGE;
import static com.tungsten.fclcore.download.LibraryAnalyzer.LibraryType.LITELOADER; import static com.tungsten.fclcore.download.LibraryAnalyzer.LibraryType.LITELOADER;
import static com.tungsten.fclcore.download.LibraryAnalyzer.LibraryType.QUILT;
import com.tungsten.fclcore.download.LibraryAnalyzer; import com.tungsten.fclcore.download.LibraryAnalyzer;
import com.tungsten.fclcore.game.DefaultGameRepository; import com.tungsten.fclcore.game.DefaultGameRepository;
@ -66,6 +67,8 @@ public class MultiMCModpackExportTask extends Task<Void> {
components.add(new MultiMCManifest.MultiMCManifestComponent(false, false, "com.mumfrey.liteloader", liteLoaderVersion))); components.add(new MultiMCManifest.MultiMCManifestComponent(false, false, "com.mumfrey.liteloader", liteLoaderVersion)));
analyzer.getVersion(FABRIC).ifPresent(fabricVersion -> analyzer.getVersion(FABRIC).ifPresent(fabricVersion ->
components.add(new MultiMCManifest.MultiMCManifestComponent(false, false, "net.fabricmc.fabric-loader", fabricVersion))); components.add(new MultiMCManifest.MultiMCManifestComponent(false, false, "net.fabricmc.fabric-loader", fabricVersion)));
analyzer.getVersion(QUILT).ifPresent(quiltVersion ->
components.add(new MultiMCManifest.MultiMCManifestComponent(false, false, "org.quiltmc.quilt-loader", quiltVersion)));
MultiMCManifest mmcPack = new MultiMCManifest(1, components); MultiMCManifest mmcPack = new MultiMCManifest(1, components);
zip.putTextFile(JsonUtils.GSON.toJson(mmcPack), "mmc-pack.json"); zip.putTextFile(JsonUtils.GSON.toJson(mmcPack), "mmc-pack.json");

View File

@ -69,7 +69,7 @@ public final class MultiMCModpackInstallTask extends Task<Void> {
builder.version("fabric", c.getVersion()); builder.version("fabric", c.getVersion());
}); });
Optional<MultiMCManifest.MultiMCManifestComponent> quilt = manifest.getMmcPack().getComponents().stream().filter(e -> e.getUid().equals("net.quiltmc.quilt-loader")).findAny(); Optional<MultiMCManifest.MultiMCManifestComponent> quilt = manifest.getMmcPack().getComponents().stream().filter(e -> e.getUid().equals("org.quiltmc.quilt-loader")).findAny();
quilt.ifPresent(c -> { quilt.ifPresent(c -> {
if (c.getVersion() != null) if (c.getVersion() != null)
builder.version("quilt", c.getVersion()); builder.version("quilt", c.getVersion());

View File

@ -1,8 +1,5 @@
package com.tungsten.fclcore.mod.server; package com.tungsten.fclcore.mod.server;
import static com.tungsten.fclcore.util.DigestUtils.digest;
import static com.tungsten.fclcore.util.Hex.encodeHex;
import com.google.gson.JsonParseException; import com.google.gson.JsonParseException;
import com.google.gson.reflect.TypeToken; import com.google.gson.reflect.TypeToken;
import com.tungsten.fclcore.download.DefaultDependencyManager; import com.tungsten.fclcore.download.DefaultDependencyManager;
@ -12,6 +9,7 @@ import com.tungsten.fclcore.mod.ModpackConfiguration;
import com.tungsten.fclcore.task.FileDownloadTask; import com.tungsten.fclcore.task.FileDownloadTask;
import com.tungsten.fclcore.task.GetTask; import com.tungsten.fclcore.task.GetTask;
import com.tungsten.fclcore.task.Task; import com.tungsten.fclcore.task.Task;
import com.tungsten.fclcore.util.DigestUtils;
import com.tungsten.fclcore.util.Logging; import com.tungsten.fclcore.util.Logging;
import com.tungsten.fclcore.util.StringUtils; import com.tungsten.fclcore.util.StringUtils;
import com.tungsten.fclcore.util.gson.JsonUtils; import com.tungsten.fclcore.util.gson.JsonUtils;
@ -132,7 +130,7 @@ public class ServerModpackCompletionTask extends Task<Void> {
download = true; download = true;
} else { } else {
// If user modified this entry file, we will not replace this file since this modified file is that user expects. // If user modified this entry file, we will not replace this file since this modified file is that user expects.
String fileHash = encodeHex(digest("SHA-1", actualPath)); String fileHash = DigestUtils.digestToString("SHA-1", actualPath);
String oldHash = files.get(file.getPath()).getHash(); String oldHash = files.get(file.getPath()).getHash();
download = !Objects.equals(oldHash, file.getHash()) && Objects.equals(oldHash, fileHash); download = !Objects.equals(oldHash, file.getHash()) && Objects.equals(oldHash, fileHash);
} }

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.LITELOADER;
import static com.tungsten.fclcore.download.LibraryAnalyzer.LibraryType.MINECRAFT; import static com.tungsten.fclcore.download.LibraryAnalyzer.LibraryType.MINECRAFT;
import static com.tungsten.fclcore.download.LibraryAnalyzer.LibraryType.OPTIFINE; import static com.tungsten.fclcore.download.LibraryAnalyzer.LibraryType.OPTIFINE;
import static com.tungsten.fclcore.util.DigestUtils.digest; import static com.tungsten.fclcore.download.LibraryAnalyzer.LibraryType.QUILT;
import static com.tungsten.fclcore.util.Hex.encodeHex;
import com.tungsten.fclcore.download.LibraryAnalyzer; import com.tungsten.fclcore.download.LibraryAnalyzer;
import com.tungsten.fclcore.game.DefaultGameRepository; import com.tungsten.fclcore.game.DefaultGameRepository;
@ -15,6 +14,7 @@ import com.tungsten.fclcore.mod.Modpack;
import com.tungsten.fclcore.mod.ModpackConfiguration; import com.tungsten.fclcore.mod.ModpackConfiguration;
import com.tungsten.fclcore.mod.ModpackExportInfo; import com.tungsten.fclcore.mod.ModpackExportInfo;
import com.tungsten.fclcore.task.Task; import com.tungsten.fclcore.task.Task;
import com.tungsten.fclcore.util.DigestUtils;
import com.tungsten.fclcore.util.Logging; import com.tungsten.fclcore.util.Logging;
import com.tungsten.fclcore.util.StringUtils; import com.tungsten.fclcore.util.StringUtils;
import com.tungsten.fclcore.util.gson.JsonUtils; import com.tungsten.fclcore.util.gson.JsonUtils;
@ -58,7 +58,7 @@ public class ServerModpackExportTask extends Task<Void> {
Path file = runDirectory.resolve(path); Path file = runDirectory.resolve(path);
if (Files.isRegularFile(file)) { if (Files.isRegularFile(file)) {
String relativePath = runDirectory.relativize(file).normalize().toString().replace(File.separatorChar, '/'); String relativePath = runDirectory.relativize(file).normalize().toString().replace(File.separatorChar, '/');
files.add(new ModpackConfiguration.FileInformation(relativePath, encodeHex(digest("SHA-1", file)))); files.add(new ModpackConfiguration.FileInformation(relativePath, DigestUtils.digestToString("SHA-1", file)));
} }
return true; return true;
} else { } else {
@ -79,6 +79,8 @@ public class ServerModpackExportTask extends Task<Void> {
addons.add(new ServerModpackManifest.Addon(OPTIFINE.getPatchId(), optifineVersion))); addons.add(new ServerModpackManifest.Addon(OPTIFINE.getPatchId(), optifineVersion)));
analyzer.getVersion(FABRIC).ifPresent(fabricVersion -> analyzer.getVersion(FABRIC).ifPresent(fabricVersion ->
addons.add(new ServerModpackManifest.Addon(FABRIC.getPatchId(), fabricVersion))); addons.add(new ServerModpackManifest.Addon(FABRIC.getPatchId(), fabricVersion)));
analyzer.getVersion(QUILT).ifPresent(quiltVersion ->
addons.add(new ServerModpackManifest.Addon(QUILT.getPatchId(), quiltVersion)));
ServerModpackManifest manifest = new ServerModpackManifest(exportInfo.getName(), exportInfo.getAuthor(), exportInfo.getVersion(), exportInfo.getDescription(), StringUtils.removeSuffix(exportInfo.getFileApi(), "/"), files, addons); ServerModpackManifest manifest = new ServerModpackManifest(exportInfo.getName(), exportInfo.getAuthor(), exportInfo.getVersion(), exportInfo.getDescription(), StringUtils.removeSuffix(exportInfo.getFileApi(), "/"), files, addons);
zip.putTextFile(JsonUtils.GSON.toJson(manifest), "server-manifest.json"); zip.putTextFile(JsonUtils.GSON.toJson(manifest), "server-manifest.json");
} }

View File

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

View File

@ -5,7 +5,6 @@ import static com.tungsten.fclcore.util.DigestUtils.getDigest;
import java.io.File; import java.io.File;
import java.io.IOException; import java.io.IOException;
import java.io.RandomAccessFile; import java.io.RandomAccessFile;
import java.math.BigInteger;
import java.net.URL; import java.net.URL;
import java.net.URLConnection; import java.net.URLConnection;
import java.nio.file.FileSystem; import java.nio.file.FileSystem;
@ -17,6 +16,7 @@ import java.util.logging.Level;
import static java.util.Objects.requireNonNull; import static java.util.Objects.requireNonNull;
import com.tungsten.fclcore.util.Hex;
import com.tungsten.fclcore.util.Logging; import com.tungsten.fclcore.util.Logging;
import com.tungsten.fclcore.util.io.ChecksumMismatchException; import com.tungsten.fclcore.util.io.ChecksumMismatchException;
import com.tungsten.fclcore.util.io.CompressingUtils; import com.tungsten.fclcore.util.io.CompressingUtils;
@ -54,7 +54,7 @@ public class FileDownloadTask extends FetchTask<Void> {
} }
public void performCheck(MessageDigest digest) throws ChecksumMismatchException { public void performCheck(MessageDigest digest) throws ChecksumMismatchException {
String actualChecksum = String.format("%1$040x", new BigInteger(1, digest.digest())); String actualChecksum = Hex.encodeHex(digest.digest());
if (!checksum.equalsIgnoreCase(actualChecksum)) { if (!checksum.equalsIgnoreCase(actualChecksum)) {
throw new ChecksumMismatchException(algorithm, checksum, actualChecksum); throw new ChecksumMismatchException(algorithm, checksum, actualChecksum);
} }
@ -248,7 +248,7 @@ public class FileDownloadTask extends FetchTask<Void> {
} }
public static final IntegrityCheckHandler ZIP_INTEGRITY_CHECK_HANDLER = (filePath, destinationPath) -> { public static final IntegrityCheckHandler ZIP_INTEGRITY_CHECK_HANDLER = (filePath, destinationPath) -> {
String ext = FileUtils.getExtension(destinationPath).toLowerCase(); String ext = FileUtils.getExtension(destinationPath).toLowerCase(Locale.ROOT);
if (ext.equals("zip") || ext.equals("jar")) { if (ext.equals("zip") || ext.equals("jar")) {
try (FileSystem ignored = CompressingUtils.createReadOnlyZipFileSystem(filePath)) { try (FileSystem ignored = CompressingUtils.createReadOnlyZipFileSystem(filePath)) {
// test for zip format // test for zip format

View File

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

View File

@ -690,7 +690,7 @@ public abstract class Task<T> {
@Override @Override
public void execute() throws Exception { public void execute() throws Exception {
if (isDependentsSucceeded() != (Task.this.getException() == null)) if (isDependentsSucceeded() != (Task.this.getException() == null))
throw new AssertionError("When whenComplete succeeded, Task.exception must be null."); throw new AssertionError("When whenComplete succeeded, Task.exception must be null.", Task.this.getException());
action.execute(Task.this.getException()); action.execute(Task.this.getException());

View File

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

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) { public static byte[] digest(String algorithm, byte[] data) {
return getDigest(algorithm).digest(data); return getDigest(algorithm).digest(data);
} }
@ -46,8 +42,22 @@ public final class DigestUtils {
return updateDigest(digest, data).digest(); return updateDigest(digest, data).digest();
} }
public static String digestToString(String algorithm, byte[] data) throws IOException {
return Hex.encodeHex(digest(algorithm, data));
}
public static String digestToString(String algorithm, Path path) throws IOException {
return Hex.encodeHex(digest(algorithm, path));
}
public static String digestToString(String algorithm, InputStream data) throws IOException {
return Hex.encodeHex(digest(algorithm, data));
}
private static final ThreadLocal<byte[]> threadLocalBuffer = ThreadLocal.withInitial(() -> new byte[STREAM_BUFFER_LENGTH]);
public static MessageDigest updateDigest(MessageDigest digest, InputStream data) throws IOException { public static MessageDigest updateDigest(MessageDigest digest, InputStream data) throws IOException {
byte[] buffer = new byte[STREAM_BUFFER_LENGTH]; byte[] buffer = threadLocalBuffer.get();
int read = data.read(buffer, 0, STREAM_BUFFER_LENGTH); int read = data.read(buffer, 0, STREAM_BUFFER_LENGTH);
while (read > -1) { while (read > -1) {

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() { private Lang() {
} }
public static <T> T requireNonNullElse(T value, T defaultValue) {
return value != null ? value : defaultValue;
}
public static <T> T requireNonNullElseGet(T value, Supplier<? extends T> defaultValue) {
return value != null ? value : defaultValue.get();
}
public static <T, U> U requireNonNullElseGet(T value, Function<? super T, ? extends U> mapper, Supplier<? extends U> defaultValue) {
return value != null ? mapper.apply(value) : defaultValue.get();
}
/** /**
* Construct a mutable map by given key-value pairs. * Construct a mutable map by given key-value pairs.
* @param pairs entries in the new map * @param pairs entries in the new map
@ -349,6 +361,13 @@ public final class Lang {
return () -> iterator; return () -> iterator;
} }
public static <T, U> void forEachZipped(Iterable<T> i1, Iterable<U> i2, BiConsumer<T, U> action) {
Iterator<T> it1 = i1.iterator();
Iterator<U> it2 = i2.iterator();
while (it1.hasNext() && it2.hasNext())
action.accept(it1.next(), it2.next());
}
private static Timer timer; private static Timer timer;
public static synchronized Timer getTimer() { public static synchronized Timer getTimer() {

View File

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

View File

@ -1,28 +1,11 @@
package com.tungsten.fclcore.util; package com.tungsten.fclcore.util;
import java.lang.reflect.AccessibleObject;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.function.Predicate; import java.util.function.Predicate;
public final class ReflectionHelper { public final class ReflectionHelper {
private ReflectionHelper() { private ReflectionHelper() {
} }
private static Method accessible0;
static {
try {
accessible0 = AccessibleObject.class.getDeclaredMethod("setAccessible0", boolean.class);
accessible0.setAccessible(true);
} catch (Throwable ignored) {
}
}
public static void setAccessible(AccessibleObject obj) throws InvocationTargetException, IllegalAccessException {
accessible0.invoke(obj, true);
}
/** /**
* Get caller, this method is caller sensitive. * Get caller, this method is caller sensitive.
* *

View File

@ -2,10 +2,7 @@ package com.tungsten.fclcore.util;
import java.io.PrintWriter; import java.io.PrintWriter;
import java.io.StringWriter; import java.io.StringWriter;
import java.nio.charset.CharsetEncoder;
import java.nio.charset.StandardCharsets;
import java.util.*; import java.util.*;
import java.util.regex.Pattern;
public final class StringUtils { public final class StringUtils {
@ -129,6 +126,10 @@ public final class StringUtils {
return str + suffix; return str + suffix;
} }
public static String removePrefix(String str, String prefix) {
return str.startsWith(prefix) ? str.substring(prefix.length()) : str;
}
public static String removePrefix(String str, String... prefixes) { public static String removePrefix(String str, String... prefixes) {
for (String prefix : prefixes) for (String prefix : prefixes)
if (str.startsWith(prefix)) if (str.startsWith(prefix))
@ -136,6 +137,10 @@ public final class StringUtils {
return str; return str;
} }
public static String removeSuffix(String str, String suffix) {
return str.endsWith(suffix) ? str.substring(0, str.length() - suffix.length()) : str;
}
/** /**
* Remove one suffix of the suffixes of the string. * Remove one suffix of the suffixes of the string.
*/ */
@ -147,24 +152,29 @@ public final class StringUtils {
} }
public static boolean containsOne(Collection<String> patterns, String... targets) { public static boolean containsOne(Collection<String> patterns, String... targets) {
for (String pattern : patterns) for (String pattern : patterns) {
String lowerPattern = pattern.toLowerCase(Locale.ROOT);
for (String target : targets) for (String target : targets)
if (pattern.toLowerCase().contains(target.toLowerCase())) if (lowerPattern.contains(target.toLowerCase(Locale.ROOT)))
return true; return true;
}
return false; return false;
} }
public static boolean containsOne(String pattern, String... targets) { public static boolean containsOne(String pattern, String... targets) {
String lowerPattern = pattern.toLowerCase(Locale.ROOT);
for (String target : targets) for (String target : targets)
if (pattern.toLowerCase().contains(target.toLowerCase())) if (lowerPattern.contains(target.toLowerCase(Locale.ROOT)))
return true; return true;
return false; return false;
} }
public static boolean containsOne(String pattern, char... targets) { public static boolean containsChinese(String str) {
for (char target : targets) for (int i = 0; i < str.length(); i++) {
if (pattern.toLowerCase().indexOf(Character.toLowerCase(target)) >= 0) char ch = str.charAt(i);
if (ch >= '\u4e00' && ch <= '\u9fa5')
return true; return true;
}
return false; return false;
} }
@ -187,7 +197,10 @@ public final class StringUtils {
} }
public static String parseColorEscapes(String original) { public static String parseColorEscapes(String original) {
return original.replaceAll("\u00A7\\d", ""); if (original.indexOf('\u00A7') < 0)
return original;
return original.replaceAll("\u00A7[0-9a-gklmnor]", "");
} }
public static String parseEscapeSequence(String str) { public static String parseEscapeSequence(String str) {
@ -216,8 +229,32 @@ public final class StringUtils {
return result.toString(); return result.toString();
} }
public static boolean isASCII(CharSequence cs) { public static int MAX_SHORT_STRING_LENGTH = 77;
return US_ASCII_ENCODER.canEncode(cs);
public static Optional<String> truncate(String str) {
if (str.length() <= MAX_SHORT_STRING_LENGTH) {
return Optional.empty();
}
final int halfLength = (MAX_SHORT_STRING_LENGTH - 5) / 2;
return Optional.of(str.substring(0, halfLength) + " ... " + str.substring(str.length() - halfLength));
}
public static boolean isASCII(String cs) {
for (int i = 0; i < cs.length(); i++)
if (cs.charAt(i) >= 128)
return false;
return true;
}
public static boolean isAlphabeticOrNumber(String str) {
int length = str.length();
for (int i = 0; i < length; i++) {
char ch = str.charAt(i);
if (!(ch >= '0' && ch <= '9') && !(ch >= 'a' && ch <= 'z') && !(ch >= 'A' && ch <= 'Z'))
return false;
}
return true;
} }
/** /**
@ -254,8 +291,4 @@ public final class StringUtils {
return f[a.length()][b.length()]; return f[a.length()][b.length()];
} }
} }
public static final Pattern CHINESE_PATTERN = Pattern.compile("[\\u4e00-\\u9fa5]");
public static final CharsetEncoder US_ASCII_ENCODER = StandardCharsets.US_ASCII.newEncoder();
} }

View File

@ -6,7 +6,11 @@ import com.google.gson.JsonParseException;
import com.google.gson.JsonSyntaxException; import com.google.gson.JsonSyntaxException;
import java.io.File; import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.lang.reflect.Type; import java.lang.reflect.Type;
import java.nio.charset.StandardCharsets;
import java.time.Instant; import java.time.Instant;
import java.util.Date; import java.util.Date;
import java.util.UUID; import java.util.UUID;
@ -24,6 +28,18 @@ public final class JsonUtils {
private JsonUtils() { private JsonUtils() {
} }
public static <T> T fromJsonFully(InputStream json, Class<T> classOfT) throws IOException, JsonParseException {
try (InputStreamReader reader = new InputStreamReader(json, StandardCharsets.UTF_8)) {
return GSON.fromJson(reader, classOfT);
}
}
public static <T> T fromJsonFully(InputStream json, Type type) throws IOException, JsonParseException {
try (InputStreamReader reader = new InputStreamReader(json, StandardCharsets.UTF_8)) {
return GSON.fromJson(reader, type);
}
}
public static <T> T fromNonNullJson(String json, Class<T> classOfT) throws JsonParseException { public static <T> T fromNonNullJson(String json, Class<T> classOfT) throws JsonParseException {
T parsed = GSON.fromJson(json, classOfT); T parsed = GSON.fromJson(json, classOfT);
if (parsed == null) if (parsed == null)
@ -38,6 +54,24 @@ public final class JsonUtils {
return parsed; return parsed;
} }
public static <T> T fromNonNullJsonFully(InputStream json, Class<T> classOfT) throws IOException, JsonParseException {
try (InputStreamReader reader = new InputStreamReader(json, StandardCharsets.UTF_8)) {
T parsed = GSON.fromJson(reader, classOfT);
if (parsed == null)
throw new JsonParseException("Json object cannot be null.");
return parsed;
}
}
public static <T> T fromNonNullJsonFully(InputStream json, Type type) throws IOException, JsonParseException {
try (InputStreamReader reader = new InputStreamReader(json, StandardCharsets.UTF_8)) {
T parsed = GSON.fromJson(reader, type);
if (parsed == null)
throw new JsonParseException("Json object cannot be null.");
return parsed;
}
}
public static <T> T fromMaybeMalformedJson(String json, Class<T> classOfT) throws JsonParseException { public static <T> T fromMaybeMalformedJson(String json, Class<T> classOfT) throws JsonParseException {
try { try {
return GSON.fromJson(json, classOfT); return GSON.fromJson(json, classOfT);

View File

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

View File

@ -7,6 +7,7 @@ import com.google.gson.stream.JsonWriter;
import java.io.IOException; import java.io.IOException;
import java.util.UUID; import java.util.UUID;
import java.util.regex.Pattern;
public final class UUIDTypeAdapter extends TypeAdapter<UUID> { public final class UUIDTypeAdapter extends TypeAdapter<UUID> {
@ -30,8 +31,10 @@ public final class UUIDTypeAdapter extends TypeAdapter<UUID> {
return value.toString().replace("-", ""); return value.toString().replace("-", "");
} }
private static final Pattern regex = Pattern.compile("(\\w{8})(\\w{4})(\\w{4})(\\w{4})(\\w{12})");
public static UUID fromString(String input) { public static UUID fromString(String input) {
return UUID.fromString(input.replaceFirst("(\\w{8})(\\w{4})(\\w{4})(\\w{4})(\\w{12})", "$1-$2-$3-$4-$5")); return UUID.fromString(regex.matcher(input).replaceFirst("$1-$2-$3-$4-$5"));
} }
} }

View File

@ -2,7 +2,6 @@ package com.tungsten.fclcore.util.io;
import com.tungsten.fclcore.download.ArtifactMalformedException; import com.tungsten.fclcore.download.ArtifactMalformedException;
import com.tungsten.fclcore.util.DigestUtils; import com.tungsten.fclcore.util.DigestUtils;
import com.tungsten.fclcore.util.Hex;
import java.io.IOException; import java.io.IOException;
import java.nio.file.Path; import java.nio.file.Path;
@ -33,7 +32,7 @@ public class ChecksumMismatchException extends ArtifactMalformedException {
} }
public static void verifyChecksum(Path file, String algorithm, String expectedChecksum) throws IOException { public static void verifyChecksum(Path file, String algorithm, String expectedChecksum) throws IOException {
String checksum = Hex.encodeHex(DigestUtils.digest(algorithm, file)); String checksum = DigestUtils.digestToString(algorithm, file);
if (!checksum.equalsIgnoreCase(expectedChecksum)) { if (!checksum.equalsIgnoreCase(expectedChecksum)) {
throw new ChecksumMismatchException(algorithm, expectedChecksum, checksum); throw new ChecksumMismatchException(algorithm, expectedChecksum, checksum);
} }

View File

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

View File

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

View File

@ -1,7 +1,6 @@
package com.tungsten.fclcore.util.io; package com.tungsten.fclcore.util.io;
import java.io.*; import java.io.*;
import java.nio.charset.Charset;
/** /**
* This utility class consists of some util methods operating on InputStream/OutputStream. * This utility class consists of some util methods operating on InputStream/OutputStream.
@ -21,7 +20,7 @@ public final class IOUtils {
* @throws IOException if an I/O error occurs. * @throws IOException if an I/O error occurs.
*/ */
public static byte[] readFullyWithoutClosing(InputStream stream) throws IOException { public static byte[] readFullyWithoutClosing(InputStream stream) throws IOException {
ByteArrayOutputStream result = new ByteArrayOutputStream(); ByteArrayOutputStream result = new ByteArrayOutputStream(Math.max(stream.available(), 32));
copyTo(stream, result); copyTo(stream, result);
return result.toByteArray(); return result.toByteArray();
} }
@ -35,7 +34,7 @@ public final class IOUtils {
*/ */
public static ByteArrayOutputStream readFully(InputStream stream) throws IOException { public static ByteArrayOutputStream readFully(InputStream stream) throws IOException {
try (InputStream is = stream) { try (InputStream is = stream) {
ByteArrayOutputStream result = new ByteArrayOutputStream(); ByteArrayOutputStream result = new ByteArrayOutputStream(Math.max(is.available(), 32));
copyTo(is, result); copyTo(is, result);
return result; return result;
} }
@ -49,18 +48,6 @@ public final class IOUtils {
return readFully(stream).toString("UTF-8"); return readFully(stream).toString("UTF-8");
} }
public static String readFullyAsString(InputStream stream, Charset charset) throws IOException {
return readFully(stream).toString(charset.name());
}
public static void write(String text, OutputStream outputStream) throws IOException {
write(text.getBytes(), outputStream);
}
public static void write(byte[] bytes, OutputStream outputStream) throws IOException {
copyTo(new ByteArrayInputStream(bytes), outputStream);
}
public static void copyTo(InputStream src, OutputStream dest) throws IOException { public static void copyTo(InputStream src, OutputStream dest) throws IOException {
copyTo(src, dest, new byte[DEFAULT_BUFFER_SIZE]); copyTo(src, dest, new byte[DEFAULT_BUFFER_SIZE]);
} }

View File

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

View File

@ -7,7 +7,6 @@ import static com.tungsten.fclcore.util.StringUtils.substringAfterLast;
import java.io.*; import java.io.*;
import java.net.*; import java.net.*;
import java.nio.charset.StandardCharsets;
import java.util.*; import java.util.*;
import java.util.Map.Entry; import java.util.Map.Entry;
@ -116,7 +115,7 @@ public final class NetworkUtils {
/** /**
* This method is a work-around that aims to solve problem when "Location" in * This method is a work-around that aims to solve problem when "Location" in
* stupid server's response is not encoded. * stupid server's response is not encoded.
* *
* @see <a href="https://github.com/curl/curl/issues/473">Issue with libcurl</a> * @see <a href="https://github.com/curl/curl/issues/473">Issue with libcurl</a>
* @param conn the stupid http connection. * @param conn the stupid http connection.
* @return manually redirected http connection. * @return manually redirected http connection.
@ -158,13 +157,13 @@ public final class NetworkUtils {
public static String doGet(URL url) throws IOException { public static String doGet(URL url) throws IOException {
HttpURLConnection con = createHttpConnection(url); HttpURLConnection con = createHttpConnection(url);
con = resolveConnection(con); con = resolveConnection(con);
return IOUtils.readFullyAsString(con.getInputStream(), StandardCharsets.UTF_8); return IOUtils.readFullyAsString(con.getInputStream());
} }
public static String doPost(URL u, Map<String, String> params) throws IOException { public static String doPost(URL u, Map<String, String> params) throws IOException {
StringBuilder sb = new StringBuilder(); StringBuilder sb = new StringBuilder();
if (params != null) { if (params != null) {
for (Entry<String, String> e : params.entrySet()) for (Map.Entry<String, String> e : params.entrySet())
sb.append(e.getKey()).append("=").append(e.getValue()).append("&"); sb.append(e.getKey()).append("=").append(e.getValue()).append("&");
sb.deleteCharAt(sb.length() - 1); sb.deleteCharAt(sb.length() - 1);
} }
@ -192,13 +191,13 @@ public final class NetworkUtils {
public static String readData(HttpURLConnection con) throws IOException { public static String readData(HttpURLConnection con) throws IOException {
try { try {
try (InputStream stdout = con.getInputStream()) { try (InputStream stdout = con.getInputStream()) {
return IOUtils.readFullyAsString(stdout, UTF_8); return IOUtils.readFullyAsString(stdout);
} }
} catch (IOException e) { } catch (IOException e) {
try (InputStream stderr = con.getErrorStream()) { try (InputStream stderr = con.getErrorStream()) {
if (stderr == null) if (stderr == null)
throw e; throw e;
return IOUtils.readFullyAsString(stderr, UTF_8); return IOUtils.readFullyAsString(stderr);
} }
} }
} }

View File

@ -16,8 +16,6 @@ import java.util.zip.ZipOutputStream;
/** /**
* Non thread-safe * Non thread-safe
*
* @author huangyuhui
*/ */
public final class Zipper implements Closeable { public final class Zipper implements Closeable {
@ -147,4 +145,4 @@ public final class Zipper implements Closeable {
zos.write(text.getBytes(encoding)); zos.write(text.getBytes(encoding));
zos.closeEntry(); zos.closeEntry();
} }
} }

View File

@ -2,8 +2,6 @@ package com.tungsten.fclcore.util.platform;
import static com.tungsten.fclcore.util.Logging.LOG; import static com.tungsten.fclcore.util.Logging.LOG;
import com.tungsten.fclcore.util.StringUtils;
import java.io.BufferedReader; import java.io.BufferedReader;
import java.io.InputStreamReader; import java.io.InputStreamReader;
import java.util.*; import java.util.*;
@ -20,7 +18,6 @@ public final class CommandBuilder {
private final List<Item> raw = new ArrayList<>(); private final List<Item> raw = new ArrayList<>();
public CommandBuilder() { public CommandBuilder() {
} }
private String parse(String s) { private String parse(String s) {
@ -57,28 +54,97 @@ public final class CommandBuilder {
return this; return this;
} }
public String addDefault(String opt) { public void addAllDefault(Collection<String> args) {
for (Item item : raw) { addAllDefault(args, true);
if (item.arg.equals(opt)) { }
return item.arg;
public void addAllDefaultWithoutParsing(Collection<String> args) {
addAllDefault(args, false);
}
private void addAllDefault(Collection<String> args, boolean parse) {
loop:
for (String arg : args) {
if (arg.startsWith("-D")) {
int idx = arg.indexOf('=');
if (idx >= 0) {
addDefault(arg.substring(0, idx + 1), arg.substring(idx + 1), parse);
} else {
String opt = arg + "=";
for (Item item : raw) {
if (item.arg.startsWith(opt)) {
LOG.info("Default option '" + arg + "' is suppressed by '" + item.arg + "'");
continue loop;
} else if (item.arg.equals(arg)) {
continue loop;
}
}
raw.add(new Item(arg, parse));
}
continue;
} }
if (arg.startsWith("-XX:")) {
Matcher matcher = UNSTABLE_OPTION_PATTERN.matcher(arg);
if (matcher.matches()) {
addUnstableDefault(matcher.group("key"), matcher.group("value"), parse);
continue;
}
matcher = UNSTABLE_BOOLEAN_OPTION_PATTERN.matcher(arg);
if (matcher.matches()) {
addUnstableDefault(matcher.group("key"), "+".equals(matcher.group("value")), parse);
continue;
}
}
if (arg.startsWith("-X")) {
String opt = null;
String value = null;
for (String prefix : new String[]{"-Xmx", "-Xms", "-Xmn", "-Xss"}) {
if (arg.startsWith(prefix)) {
opt = prefix;
value = arg.substring(prefix.length());
break;
}
}
if (opt != null) {
addDefault(opt, value, parse);
continue;
}
}
for (Item item : raw) {
if (item.arg.equals(arg)) {
continue loop;
}
}
raw.add(new Item(arg, parse));
} }
raw.add(new Item(opt, true));
return null;
} }
public String addDefault(String opt, String value) { public String addDefault(String opt, String value) {
return addDefault(opt, value, true);
}
private String addDefault(String opt, String value, boolean parse) {
for (Item item : raw) { for (Item item : raw) {
if (item.arg.startsWith(opt)) { if (item.arg.startsWith(opt)) {
LOG.info("Default option '" + opt + value + "' is suppressed by '" + item.arg + "'"); LOG.info("Default option '" + opt + value + "' is suppressed by '" + item.arg + "'");
return item.arg; return item.arg;
} }
} }
raw.add(new Item(opt + value, true)); raw.add(new Item(opt + value, parse));
return null; return null;
} }
public String addUnstableDefault(String opt, boolean value) { public String addUnstableDefault(String opt, boolean value) {
return addUnstableDefault(opt, value, true);
}
private String addUnstableDefault(String opt, boolean value, boolean parse) {
for (Item item : raw) { for (Item item : raw) {
final Matcher matcher = UNSTABLE_BOOLEAN_OPTION_PATTERN.matcher(item.arg); final Matcher matcher = UNSTABLE_BOOLEAN_OPTION_PATTERN.matcher(item.arg);
if (matcher.matches()) { if (matcher.matches()) {
@ -89,14 +155,18 @@ public final class CommandBuilder {
} }
if (value) { if (value) {
raw.add(new Item("-XX:+" + opt, true)); raw.add(new Item("-XX:+" + opt, parse));
} else { } else {
raw.add(new Item("-XX:-" + opt, true)); raw.add(new Item("-XX:-" + opt, parse));
} }
return null; return null;
} }
public String addUnstableDefault(String opt, String value) { public String addUnstableDefault(String opt, String value) {
return addUnstableDefault(opt, value, true);
}
private String addUnstableDefault(String opt, String value, boolean parse) {
for (Item item : raw) { for (Item item : raw) {
final Matcher matcher = UNSTABLE_OPTION_PATTERN.matcher(item.arg); final Matcher matcher = UNSTABLE_OPTION_PATTERN.matcher(item.arg);
if (matcher.matches()) { if (matcher.matches()) {
@ -106,7 +176,7 @@ public final class CommandBuilder {
} }
} }
raw.add(new Item("-XX:" + opt + "=" + value, true)); raw.add(new Item("-XX:" + opt + "=" + value, parse));
return null; return null;
} }
@ -114,6 +184,10 @@ public final class CommandBuilder {
return raw.removeIf(i -> pred.test(i.arg)); return raw.removeIf(i -> pred.test(i.arg));
} }
public boolean noneMatch(Predicate<String> predicate) {
return raw.stream().noneMatch(it -> predicate.test(it.arg));
}
@Override @Override
public String toString() { public String toString() {
return raw.stream().map(i -> i.parse ? parse(i.arg) : i.arg).collect(Collectors.joining(" ")); return raw.stream().map(i -> i.parse ? parse(i.arg) : i.arg).collect(Collectors.joining(" "));
@ -128,8 +202,8 @@ public final class CommandBuilder {
} }
private static class Item { private static class Item {
String arg; final String arg;
boolean parse; final boolean parse;
Item(String arg, boolean parse) { Item(String arg, boolean parse) {
this.arg = arg; this.arg = arg;
@ -147,22 +221,53 @@ public final class CommandBuilder {
} }
public static boolean hasExecutionPolicy() { public static boolean hasExecutionPolicy() {
return true; try {
final Process process = Runtime.getRuntime().exec(new String[]{"powershell", "-Command", "Get-ExecutionPolicy"});
if (!process.waitFor(5, TimeUnit.SECONDS)) {
process.destroy();
return false;
}
try (BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream(), OperatingSystem.NATIVE_CHARSET))) {
String policy = reader.readLine();
return "Unrestricted".equalsIgnoreCase(policy) || "RemoteSigned".equalsIgnoreCase(policy);
}
} catch (Throwable ignored) {
}
return false;
} }
public static boolean setExecutionPolicy() { public static boolean setExecutionPolicy() {
try {
final Process process = Runtime.getRuntime().exec(new String[]{"powershell", "-Command", "Set-ExecutionPolicy -ExecutionPolicy RemoteSigned -Scope CurrentUser"});
if (!process.waitFor(5, TimeUnit.SECONDS)) {
process.destroy();
return false;
}
} catch (Throwable ignored) {
}
return true; return true;
} }
private static boolean containsEscape(String str, String escapeChars) {
for (int i = 0; i < escapeChars.length(); i++) {
if (str.indexOf(escapeChars.charAt(i)) >= 0)
return true;
}
return false;
}
private static String escape(String str, char... escapeChars) {
for (char ch : escapeChars) {
str = str.replace("" + ch, "\\" + ch);
}
return str;
}
public static String toBatchStringLiteral(String s) {
return containsEscape(s, " \t\"^&<>|") ? '"' + escape(s, '\\', '"') + '"' : s;
}
public static String toShellStringLiteral(String s) { public static String toShellStringLiteral(String s) {
String escaping = " \t\"!#$&'()*,;<=>?[\\]^`{|}~"; return containsEscape(s, " \t\"!#$&'()*,;<=>?[\\]^`{|}~") ? '"' + escape(s, '"', '$', '&', '`') + '"' : s;
String escaped = "\"$&`";
if (s.indexOf(' ') >= 0 || s.indexOf('\t') >= 0 || StringUtils.containsOne(s, escaping.toCharArray())) {
// The argument has not been quoted, add quotes.
for (char ch : escaped.toCharArray())
s = s.replace("" + ch, "\\" + ch);
return '"' + s + '"';
} else
return s;
} }
} }

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

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