Compare commits
33
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
00f1173bac | ||
|
|
4721fcea66 | ||
|
|
5ea63273be | ||
|
|
7cee25aab2 | ||
|
|
9298f5e030 | ||
|
|
93f943d40c | ||
|
|
f20c11a4e2 | ||
|
|
0c25f31493 | ||
|
|
8d774e267f | ||
|
|
6c1de5d485 | ||
|
|
77c86ed3bc | ||
|
|
d75c3bbf96 | ||
|
|
8bb6cb714c | ||
|
|
28d002f6d3 | ||
|
|
78fcc69d29 | ||
|
|
cdfae54ab9 | ||
|
|
0813211c20 | ||
|
|
056a4eead2 | ||
|
|
2635d31425 | ||
|
|
76d35af5b9 | ||
|
|
bcff1695d3 | ||
|
|
3d04447c2f | ||
|
|
2736b88a10 | ||
|
|
0804a2fbf8 | ||
|
|
7224ed6a7d | ||
|
|
c41737364c | ||
|
|
320e579844 | ||
|
|
fc3f2e045b | ||
|
|
481b47bf10 | ||
|
|
0fb87f0b1d | ||
|
|
1252d7700f | ||
|
|
30c2d88f62 | ||
|
|
718a98d7ec |
No files matched your search
@@ -1,194 +0,0 @@
|
||||
/*
|
||||
* Hello Minecraft! Launcher
|
||||
* Copyright (C) 2019 huangyuhui <huanghongxun2008@126.com> and contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.jackhuang.hmcl.game;
|
||||
|
||||
import javafx.embed.swing.SwingFXUtils;
|
||||
import javafx.scene.image.Image;
|
||||
import org.jackhuang.hmcl.Metadata;
|
||||
import org.jackhuang.hmcl.auth.Account;
|
||||
import org.jackhuang.hmcl.auth.yggdrasil.GameProfile;
|
||||
import org.jackhuang.hmcl.auth.yggdrasil.Texture;
|
||||
import org.jackhuang.hmcl.auth.yggdrasil.YggdrasilAccount;
|
||||
import org.jackhuang.hmcl.setting.Accounts;
|
||||
import org.jackhuang.hmcl.task.FileDownloadTask;
|
||||
import org.jackhuang.hmcl.task.Scheduler;
|
||||
import org.jackhuang.hmcl.task.Schedulers;
|
||||
import org.jackhuang.hmcl.task.Task;
|
||||
import org.jackhuang.hmcl.ui.DialogController;
|
||||
import org.jackhuang.hmcl.util.io.NetworkUtils;
|
||||
|
||||
import java.awt.Graphics2D;
|
||||
import java.awt.image.BufferedImage;
|
||||
import java.io.File;
|
||||
import java.util.*;
|
||||
|
||||
public final class AccountHelper {
|
||||
|
||||
private AccountHelper() {}
|
||||
|
||||
public static final File SKIN_DIR = Metadata.HMCL_DIRECTORY.resolve("skins").toFile();
|
||||
|
||||
public static void loadSkins() {
|
||||
for (Account account : Accounts.getAccounts()) {
|
||||
if (account instanceof YggdrasilAccount) {
|
||||
new SkinLoadTask((YggdrasilAccount) account, false).start();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static Task loadSkinAsync(YggdrasilAccount account) {
|
||||
return new SkinLoadTask(account, false);
|
||||
}
|
||||
|
||||
public static Task refreshSkinAsync(YggdrasilAccount account) {
|
||||
return new SkinLoadTask(account, true);
|
||||
}
|
||||
|
||||
private static File getSkinFile(UUID uuid) {
|
||||
return new File(SKIN_DIR, uuid + ".png");
|
||||
}
|
||||
|
||||
public static Image getSkin(YggdrasilAccount account) {
|
||||
return getSkin(account, 1);
|
||||
}
|
||||
|
||||
public static Image getSkin(YggdrasilAccount account, double scaleRatio) {
|
||||
UUID uuid = account.getUUID();
|
||||
if (uuid == null)
|
||||
return getSteveSkin(scaleRatio);
|
||||
|
||||
File file = getSkinFile(uuid);
|
||||
if (file.exists()) {
|
||||
Image original = new Image("file:" + file.getAbsolutePath());
|
||||
if (original.isError())
|
||||
return getDefaultSkin(uuid, scaleRatio);
|
||||
|
||||
return new Image("file:" + file.getAbsolutePath(),
|
||||
original.getWidth() * scaleRatio,
|
||||
original.getHeight() * scaleRatio,
|
||||
false, false);
|
||||
}
|
||||
return getDefaultSkin(uuid, scaleRatio);
|
||||
}
|
||||
|
||||
public static Image getSkinImmediately(YggdrasilAccount account, GameProfile profile, double scaleRatio) throws Exception {
|
||||
File file = getSkinFile(profile.getId());
|
||||
downloadSkin(account, profile, true);
|
||||
if (!file.exists())
|
||||
return getDefaultSkin(profile.getId(), scaleRatio);
|
||||
|
||||
String url = "file:" + file.getAbsolutePath();
|
||||
return scale(url, scaleRatio);
|
||||
}
|
||||
|
||||
public static Image getHead(Image skin, int scaleRatio) {
|
||||
final int size = 8 * scaleRatio;
|
||||
final int faceOffset = (int) Math.round(scaleRatio * 4d / 9d);
|
||||
BufferedImage image = SwingFXUtils.fromFXImage(skin, null);
|
||||
BufferedImage head = new BufferedImage(size, size, BufferedImage.TYPE_INT_ARGB);
|
||||
Graphics2D g2d = head.createGraphics();
|
||||
g2d.drawImage(image, faceOffset, faceOffset, size - faceOffset, size - faceOffset,
|
||||
size, size, size + size, size + size, null);
|
||||
if (image.getHeight() > 32) {
|
||||
g2d.drawImage(image, 0, 0, size, size,
|
||||
40 * scaleRatio, 8 * scaleRatio, 48 * scaleRatio, 16 * scaleRatio, null);
|
||||
}
|
||||
return SwingFXUtils.toFXImage(head, null);
|
||||
}
|
||||
|
||||
private static class SkinLoadTask extends Task {
|
||||
private final YggdrasilAccount account;
|
||||
private final boolean refresh;
|
||||
private final List<Task> dependencies = new LinkedList<>();
|
||||
|
||||
public SkinLoadTask(YggdrasilAccount account, boolean refresh) {
|
||||
this.account = account;
|
||||
this.refresh = refresh;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Scheduler getScheduler() {
|
||||
return Schedulers.io();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Collection<Task> getDependencies() {
|
||||
return dependencies;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void execute() throws Exception {
|
||||
if (!account.isLoggedIn() && (account.getCharacter() == null || refresh))
|
||||
DialogController.logIn(account);
|
||||
|
||||
downloadSkin(account, refresh);
|
||||
}
|
||||
}
|
||||
|
||||
private static void downloadSkin(YggdrasilAccount account, GameProfile profile, boolean refresh) throws Exception {
|
||||
account.clearCache();
|
||||
|
||||
File file = getSkinFile(profile.getId());
|
||||
if (!refresh && file.exists())
|
||||
return;
|
||||
Optional<Texture> texture = account.getSkin(profile);
|
||||
if (!texture.isPresent()) return;
|
||||
String url = texture.get().getUrl();
|
||||
new FileDownloadTask(NetworkUtils.toURL(url), file).run();
|
||||
}
|
||||
|
||||
private static void downloadSkin(YggdrasilAccount account, boolean refresh) throws Exception {
|
||||
account.clearCache();
|
||||
|
||||
if (account.getCharacter() == null) return;
|
||||
File file = getSkinFile(account.getUUID());
|
||||
if (!refresh && file.exists()) {
|
||||
Image original = new Image("file:" + file.getAbsolutePath());
|
||||
if (!original.isError())
|
||||
return;
|
||||
}
|
||||
Optional<Texture> texture = account.getSkin();
|
||||
if (!texture.isPresent()) return;
|
||||
String url = texture.get().getUrl();
|
||||
new FileDownloadTask(NetworkUtils.toURL(url), file).run();
|
||||
}
|
||||
|
||||
public static Image scale(String url, double scaleRatio) {
|
||||
Image origin = new Image(url);
|
||||
return new Image(url,
|
||||
origin.getWidth() * scaleRatio,
|
||||
origin.getHeight() * scaleRatio,
|
||||
false, false);
|
||||
}
|
||||
|
||||
public static Image getSteveSkin(double scaleRatio) {
|
||||
return scale("/assets/img/steve.png", scaleRatio);
|
||||
}
|
||||
|
||||
public static Image getAlexSkin(double scaleRatio) {
|
||||
return scale("/assets/img/alex.png", scaleRatio);
|
||||
}
|
||||
|
||||
public static Image getDefaultSkin(UUID uuid, double scaleRatio) {
|
||||
int type = uuid.hashCode() & 1;
|
||||
if (type == 1)
|
||||
return getAlexSkin(scaleRatio);
|
||||
else
|
||||
return getSteveSkin(scaleRatio);
|
||||
}
|
||||
}
|
||||
@@ -375,7 +375,7 @@ public final class LauncherHelper {
|
||||
if (!flag) {
|
||||
boolean hasForge2760 = version.getLibraries().stream().filter(it -> it.is("net.minecraftforge", "forge"))
|
||||
.anyMatch(it ->
|
||||
0 <= VersionNumber.VERSION_COMPARATOR.compare("1.12.2-14.23.5.2760", it.getVersion()) &&
|
||||
VersionNumber.VERSION_COMPARATOR.compare("1.12.2-14.23.5.2760", it.getVersion()) <= 0 &&
|
||||
VersionNumber.VERSION_COMPARATOR.compare(it.getVersion(), "1.12.2-14.23.5.2773") < 0);
|
||||
boolean hasLiteLoader = version.getLibraries().stream().anyMatch(it -> it.is("com.mumfrey", "liteloader"));
|
||||
if (hasForge2760 && hasLiteLoader && gameVersion.compareTo(VersionNumber.asVersion("1.12.2")) == 0) {
|
||||
|
||||
@@ -0,0 +1,209 @@
|
||||
/*
|
||||
* Hello Minecraft! Launcher
|
||||
* Copyright (C) 2019 huangyuhui <huanghongxun2008@126.com> and contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.jackhuang.hmcl.game;
|
||||
|
||||
import static java.util.Collections.singletonMap;
|
||||
import static org.jackhuang.hmcl.util.Lang.threadPool;
|
||||
import static org.jackhuang.hmcl.util.Logging.LOG;
|
||||
|
||||
import java.awt.Graphics2D;
|
||||
import java.awt.image.BufferedImage;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.UncheckedIOException;
|
||||
import java.net.URL;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.EnumMap;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.ThreadPoolExecutor;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.logging.Level;
|
||||
|
||||
import javax.imageio.ImageIO;
|
||||
|
||||
import org.jackhuang.hmcl.Metadata;
|
||||
import org.jackhuang.hmcl.auth.Account;
|
||||
import org.jackhuang.hmcl.auth.ServerResponseMalformedException;
|
||||
import org.jackhuang.hmcl.auth.yggdrasil.Texture;
|
||||
import org.jackhuang.hmcl.auth.yggdrasil.TextureModel;
|
||||
import org.jackhuang.hmcl.auth.yggdrasil.TextureType;
|
||||
import org.jackhuang.hmcl.auth.yggdrasil.YggdrasilAccount;
|
||||
import org.jackhuang.hmcl.auth.yggdrasil.YggdrasilService;
|
||||
import org.jackhuang.hmcl.task.FileDownloadTask;
|
||||
import org.jackhuang.hmcl.util.javafx.MultiStepBinding;
|
||||
|
||||
import javafx.beans.binding.Bindings;
|
||||
import javafx.beans.binding.ObjectBinding;
|
||||
import javafx.embed.swing.SwingFXUtils;
|
||||
import javafx.scene.image.Image;
|
||||
|
||||
/**
|
||||
* @author yushijinhun
|
||||
*/
|
||||
public final class TexturesLoader {
|
||||
|
||||
private TexturesLoader() {
|
||||
}
|
||||
|
||||
// ==== Texture Loading ====
|
||||
public static class LoadedTexture {
|
||||
private final BufferedImage image;
|
||||
private final Map<String, String> metadata;
|
||||
|
||||
public LoadedTexture(BufferedImage image, Map<String, String> metadata) {
|
||||
this.image = image;
|
||||
this.metadata = metadata;
|
||||
}
|
||||
|
||||
public BufferedImage getImage() {
|
||||
return image;
|
||||
}
|
||||
|
||||
public Map<String, String> getMetadata() {
|
||||
return metadata;
|
||||
}
|
||||
}
|
||||
|
||||
private static final ThreadPoolExecutor POOL = threadPool("TexturesDownload", true, 2, 10, TimeUnit.SECONDS);
|
||||
private static final Path TEXTURES_DIR = Metadata.MINECRAFT_DIRECTORY.resolve("assets").resolve("skins");
|
||||
|
||||
private static Path getTexturePath(Texture texture) {
|
||||
String url = texture.getUrl();
|
||||
int slash = url.lastIndexOf('/');
|
||||
int dot = url.lastIndexOf('.');
|
||||
if (dot < slash) {
|
||||
dot = url.length();
|
||||
}
|
||||
String hash = url.substring(slash + 1, dot);
|
||||
String prefix = hash.length() > 2 ? hash.substring(0, 2) : "xx";
|
||||
return TEXTURES_DIR.resolve(prefix).resolve(hash);
|
||||
}
|
||||
|
||||
public static LoadedTexture loadTexture(Texture texture) throws IOException {
|
||||
Path file = getTexturePath(texture);
|
||||
if (!Files.isRegularFile(file)) {
|
||||
// download it
|
||||
try {
|
||||
new FileDownloadTask(new URL(texture.getUrl()), file.toFile()).run();
|
||||
LOG.info("Texture downloaded: " + texture.getUrl());
|
||||
} catch (Exception e) {
|
||||
if (Files.isRegularFile(file)) {
|
||||
// concurrency conflict?
|
||||
LOG.log(Level.WARNING, "Failed to download texture " + texture.getUrl() + ", but the file is available", e);
|
||||
} else {
|
||||
throw new IOException("Failed to download texture " + texture.getUrl());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
BufferedImage img;
|
||||
try (InputStream in = Files.newInputStream(file)) {
|
||||
img = ImageIO.read(in);
|
||||
}
|
||||
return new LoadedTexture(img, texture.getMetadata());
|
||||
}
|
||||
// ====
|
||||
|
||||
// ==== Skins ====
|
||||
private final static Map<TextureModel, LoadedTexture> DEFAULT_SKINS = new EnumMap<>(TextureModel.class);
|
||||
static {
|
||||
loadDefaultSkin("/assets/img/steve.png", TextureModel.STEVE);
|
||||
loadDefaultSkin("/assets/img/alex.png", TextureModel.ALEX);
|
||||
}
|
||||
private static void loadDefaultSkin(String path, TextureModel model) {
|
||||
try (InputStream in = TexturesLoader.class.getResourceAsStream(path)) {
|
||||
DEFAULT_SKINS.put(model, new LoadedTexture(ImageIO.read(in), singletonMap("model", model.modelName)));
|
||||
} catch (IOException e) {
|
||||
throw new UncheckedIOException(e);
|
||||
}
|
||||
}
|
||||
|
||||
public static LoadedTexture getDefaultSkin(TextureModel model) {
|
||||
return DEFAULT_SKINS.get(model);
|
||||
}
|
||||
|
||||
public static ObjectBinding<LoadedTexture> skinBinding(YggdrasilService service, UUID uuid) {
|
||||
LoadedTexture uuidFallback = getDefaultSkin(TextureModel.detectUUID(uuid));
|
||||
return MultiStepBinding.of(service.getProfileRepository().binding(uuid))
|
||||
.map(profile -> profile
|
||||
.flatMap(it -> {
|
||||
try {
|
||||
return YggdrasilService.getTextures(it);
|
||||
} catch (ServerResponseMalformedException e) {
|
||||
LOG.log(Level.WARNING, "Failed to parse texture payload", e);
|
||||
return Optional.empty();
|
||||
}
|
||||
})
|
||||
.flatMap(it -> Optional.ofNullable(it.get(TextureType.SKIN))))
|
||||
.asyncMap(it -> {
|
||||
if (it.isPresent()) {
|
||||
Texture texture = it.get();
|
||||
try {
|
||||
return loadTexture(texture);
|
||||
} catch (IOException e) {
|
||||
LOG.log(Level.WARNING, "Failed to load texture " + texture.getUrl() + ", using fallback texture", e);
|
||||
return getDefaultSkin(TextureModel.detectModelName(texture.getMetadata()));
|
||||
}
|
||||
} else {
|
||||
return uuidFallback;
|
||||
}
|
||||
}, uuidFallback, POOL);
|
||||
}
|
||||
// ====
|
||||
|
||||
// ==== Avatar ====
|
||||
public static BufferedImage toAvatar(BufferedImage skin, int size) {
|
||||
BufferedImage avatar = new BufferedImage(size, size, BufferedImage.TYPE_INT_ARGB);
|
||||
Graphics2D g = avatar.createGraphics();
|
||||
|
||||
int scale = skin.getWidth() / 64;
|
||||
int faceOffset = (int) Math.round(size / 18.0);
|
||||
g.drawImage(skin,
|
||||
faceOffset, faceOffset, size - faceOffset, size - faceOffset,
|
||||
8 * scale, 8 * scale, 16 * scale, 16 * scale,
|
||||
null);
|
||||
|
||||
if (skin.getWidth() == skin.getHeight()) {
|
||||
g.drawImage(skin,
|
||||
0, 0, size, size,
|
||||
40 * scale, 8 * scale, 48 * scale, 16 * scale, null);
|
||||
}
|
||||
|
||||
g.dispose();
|
||||
return avatar;
|
||||
}
|
||||
|
||||
public static ObjectBinding<Image> fxAvatarBinding(YggdrasilService service, UUID uuid, int size) {
|
||||
return MultiStepBinding.of(skinBinding(service, uuid))
|
||||
.map(it -> toAvatar(it.image, size))
|
||||
.map(img -> SwingFXUtils.toFXImage(img, null));
|
||||
}
|
||||
|
||||
public static ObjectBinding<Image> fxAvatarBinding(Account account, int size) {
|
||||
if (account instanceof YggdrasilAccount) {
|
||||
return fxAvatarBinding(((YggdrasilAccount) account).getYggdrasilService(), account.getUUID(), size);
|
||||
} else {
|
||||
return Bindings.createObjectBinding(
|
||||
() -> SwingFXUtils.toFXImage(toAvatar(getDefaultSkin(TextureModel.detectUUID(account.getUUID())).image, size), null));
|
||||
}
|
||||
}
|
||||
// ====
|
||||
}
|
||||
@@ -35,7 +35,6 @@ import org.jackhuang.hmcl.auth.authlibinjector.AuthlibInjectorServer;
|
||||
import org.jackhuang.hmcl.auth.authlibinjector.SimpleAuthlibInjectorArtifactProvider;
|
||||
import org.jackhuang.hmcl.auth.offline.OfflineAccount;
|
||||
import org.jackhuang.hmcl.auth.offline.OfflineAccountFactory;
|
||||
import org.jackhuang.hmcl.auth.yggdrasil.MojangYggdrasilProvider;
|
||||
import org.jackhuang.hmcl.auth.yggdrasil.YggdrasilAccount;
|
||||
import org.jackhuang.hmcl.auth.yggdrasil.YggdrasilAccountFactory;
|
||||
import org.jackhuang.hmcl.task.Schedulers;
|
||||
@@ -63,7 +62,7 @@ public final class Accounts {
|
||||
private Accounts() {}
|
||||
|
||||
public static final OfflineAccountFactory FACTORY_OFFLINE = OfflineAccountFactory.INSTANCE;
|
||||
public static final YggdrasilAccountFactory FACTORY_YGGDRASIL = new YggdrasilAccountFactory(MojangYggdrasilProvider.INSTANCE);
|
||||
public static final YggdrasilAccountFactory FACTORY_MOJANG = YggdrasilAccountFactory.MOJANG;
|
||||
public static final AuthlibInjectorAccountFactory FACTORY_AUTHLIB_INJECTOR = new AuthlibInjectorAccountFactory(createAuthlibInjectorArtifactProvider(), Accounts::getOrCreateAuthlibInjectorServer);
|
||||
|
||||
// ==== login type / account factory mapping ====
|
||||
@@ -71,7 +70,7 @@ public final class Accounts {
|
||||
private static final Map<AccountFactory<?>, String> factory2type = new HashMap<>();
|
||||
static {
|
||||
type2factory.put("offline", FACTORY_OFFLINE);
|
||||
type2factory.put("yggdrasil", FACTORY_YGGDRASIL);
|
||||
type2factory.put("yggdrasil", FACTORY_MOJANG);
|
||||
type2factory.put("authlibInjector", FACTORY_AUTHLIB_INJECTOR);
|
||||
|
||||
type2factory.forEach((type, factory) -> factory2type.put(factory, type));
|
||||
@@ -94,7 +93,7 @@ public final class Accounts {
|
||||
else if (account instanceof AuthlibInjectorAccount)
|
||||
return FACTORY_AUTHLIB_INJECTOR;
|
||||
else if (account instanceof YggdrasilAccount)
|
||||
return FACTORY_YGGDRASIL;
|
||||
return FACTORY_MOJANG;
|
||||
else
|
||||
throw new IllegalArgumentException("Failed to determine account type: " + account);
|
||||
}
|
||||
@@ -279,7 +278,7 @@ public final class Accounts {
|
||||
// ==== Login type name i18n ===
|
||||
private static Map<AccountFactory<?>, String> unlocalizedLoginTypeNames = mapOf(
|
||||
pair(Accounts.FACTORY_OFFLINE, "account.methods.offline"),
|
||||
pair(Accounts.FACTORY_YGGDRASIL, "account.methods.yggdrasil"),
|
||||
pair(Accounts.FACTORY_MOJANG, "account.methods.yggdrasil"),
|
||||
pair(Accounts.FACTORY_AUTHLIB_INJECTOR, "account.methods.authlib_injector"));
|
||||
|
||||
public static String getLocalizedLoginTypeName(AccountFactory<?> factory) {
|
||||
|
||||
@@ -90,8 +90,13 @@ public final class Controllers {
|
||||
|
||||
// FXThread
|
||||
public static GameList getGameListPage() {
|
||||
if (gameListPage == null)
|
||||
if (gameListPage == null) {
|
||||
gameListPage = new GameList();
|
||||
FXUtils.applyDragListener(gameListPage, it -> "zip".equals(FileUtils.getExtension(it)), modpacks -> {
|
||||
File modpack = modpacks.get(0);
|
||||
Controllers.getDecorator().startWizard(new ModpackInstallWizardProvider(Profiles.getSelectedProfile(), modpack), i18n("install.modpack"));
|
||||
});
|
||||
}
|
||||
return gameListPage;
|
||||
}
|
||||
|
||||
|
||||
@@ -234,8 +234,20 @@ public final class FXUtils {
|
||||
}
|
||||
}
|
||||
|
||||
public static void installTooltip(Node node, String tooltip) {
|
||||
installTooltip(node, 0, 5000, 0, new Tooltip(tooltip));
|
||||
public static void installFastTooltip(Node node, Tooltip tooltip) {
|
||||
installTooltip(node, 50, 5000, 0, tooltip);
|
||||
}
|
||||
|
||||
public static void installFastTooltip(Node node, String tooltip) {
|
||||
installFastTooltip(node, new Tooltip(tooltip));
|
||||
}
|
||||
|
||||
public static void installSlowTooltip(Node node, Tooltip tooltip) {
|
||||
installTooltip(node, 500, 5000, 0, tooltip);
|
||||
}
|
||||
|
||||
public static void installSlowTooltip(Node node, String tooltip) {
|
||||
installSlowTooltip(node, new Tooltip(tooltip));
|
||||
}
|
||||
|
||||
public static void installTooltip(Node node, double openDelay, double visibleDelay, double closeDelay, Tooltip tooltip) {
|
||||
|
||||
@@ -119,16 +119,16 @@ public final class LeftPaneController extends AdvancedListBox {
|
||||
if (repository.getVersionCount() == 0) {
|
||||
File modpackFile = new File("modpack.zip").getAbsoluteFile();
|
||||
if (modpackFile.exists()) {
|
||||
Task.ofResult("encoding", () -> CompressingUtils.findSuitableEncoding(modpackFile.toPath()))
|
||||
.then(Task.ofResult("modpack", var -> ModpackHelper.readModpackManifest(modpackFile.toPath(), var.get("encoding"))))
|
||||
.then(Task.of(var -> {
|
||||
Task.ofResult(() -> CompressingUtils.findSuitableEncoding(modpackFile.toPath()))
|
||||
.thenResult(encoding -> ModpackHelper.readModpackManifest(modpackFile.toPath(), encoding))
|
||||
.thenResult(modpack -> {
|
||||
AtomicReference<Region> region = new AtomicReference<>();
|
||||
Modpack modpack = var.get("modpack");
|
||||
TaskExecutor executor = ModpackHelper.getInstallTask(repository.getProfile(), modpackFile, modpack.getName(), modpack)
|
||||
.with(Task.of(Schedulers.javafx(), this::checkAccount)).executor();
|
||||
region.set(Controllers.taskDialog(executor, i18n("modpack.installing")));
|
||||
executor.start();
|
||||
})).start();
|
||||
return null;
|
||||
}).start();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,7 +19,7 @@ package org.jackhuang.hmcl.ui;
|
||||
|
||||
import com.jfoenix.controls.JFXButton;
|
||||
import com.jfoenix.controls.JFXScrollPane;
|
||||
import com.jfoenix.controls.JFXSpinner;
|
||||
|
||||
import javafx.beans.binding.Bindings;
|
||||
import javafx.geometry.Insets;
|
||||
import javafx.geometry.Pos;
|
||||
@@ -28,17 +28,14 @@ import javafx.scene.control.SkinBase;
|
||||
import javafx.scene.layout.StackPane;
|
||||
import javafx.scene.layout.VBox;
|
||||
import org.jackhuang.hmcl.setting.Theme;
|
||||
import org.jackhuang.hmcl.ui.construct.SpinnerPane;
|
||||
|
||||
public class ListPageSkin extends SkinBase<ListPage> {
|
||||
public class ListPageSkin extends SkinBase<ListPage<?>> {
|
||||
|
||||
public ListPageSkin(ListPage<?> skinnable) {
|
||||
super(skinnable);
|
||||
|
||||
StackPane rootPane = new StackPane();
|
||||
|
||||
JFXSpinner spinner = new JFXSpinner();
|
||||
spinner.setRadius(16);
|
||||
spinner.getStyleClass().setAll("materialDesign-purple", "first-spinner");
|
||||
SpinnerPane spinnerPane = new SpinnerPane();
|
||||
|
||||
StackPane contentPane = new StackPane();
|
||||
{
|
||||
@@ -56,7 +53,7 @@ public class ListPageSkin extends SkinBase<ListPage> {
|
||||
scrollPane.setContent(list);
|
||||
JFXScrollPane.smoothScrolling(scrollPane);
|
||||
}
|
||||
|
||||
|
||||
VBox vBox = new VBox();
|
||||
{
|
||||
vBox.setAlignment(Pos.BOTTOM_RIGHT);
|
||||
@@ -92,13 +89,9 @@ public class ListPageSkin extends SkinBase<ListPage> {
|
||||
contentPane.getChildren().setAll(scrollPane, vBox);
|
||||
}
|
||||
|
||||
rootPane.getChildren().setAll(contentPane);
|
||||
spinnerPane.loadingProperty().bind(skinnable.loadingProperty());
|
||||
spinnerPane.setContent(contentPane);
|
||||
|
||||
skinnable.loadingProperty().addListener((a, b, newValue) -> {
|
||||
if (newValue) rootPane.getChildren().setAll(spinner);
|
||||
else rootPane.getChildren().setAll(contentPane);
|
||||
});
|
||||
|
||||
getChildren().setAll(rootPane);
|
||||
getChildren().setAll(spinnerPane);
|
||||
}
|
||||
}
|
||||
@@ -87,11 +87,8 @@ public final class MainPage extends StackPane implements DecoratorPage {
|
||||
lblIcon.setGraphic(SVG.update(Theme.whiteFillBinding(), 20, 20));
|
||||
|
||||
TwoLineListItem prompt = new TwoLineListItem();
|
||||
prompt.setTitleFill(Color.WHITE);
|
||||
prompt.setSubtitleFill(Color.WHITE);
|
||||
prompt.setSubtitle(i18n("update.bubble.subtitle"));
|
||||
prompt.setPickOnBounds(false);
|
||||
prompt.setStyle("-jfx-title-font-weight: BOLD;");
|
||||
prompt.titleProperty().bind(latestVersionProperty());
|
||||
|
||||
hBox.getChildren().setAll(lblIcon, prompt);
|
||||
|
||||
@@ -129,7 +129,7 @@ public final class SettingsPage extends SettingsView implements DecoratorPage {
|
||||
config().commonDirectoryProperty(), config().commonDirTypeProperty()));
|
||||
|
||||
// ==== Update ====
|
||||
FXUtils.installTooltip(btnUpdate, i18n("update.tooltip"));
|
||||
FXUtils.installFastTooltip(btnUpdate, i18n("update.tooltip"));
|
||||
updateListener = any -> {
|
||||
btnUpdate.setVisible(UpdateChecker.isOutdated());
|
||||
|
||||
|
||||
@@ -359,7 +359,7 @@ public abstract class SettingsView extends StackPane {
|
||||
HBox hBox = new HBox();
|
||||
hBox.setSpacing(3);
|
||||
|
||||
cboFont = new FontComboBox(12, false);
|
||||
cboFont = new FontComboBox(12);
|
||||
txtFontSize = new JFXTextField();
|
||||
FXUtils.setLimitWidth(txtFontSize, 50);
|
||||
hBox.getChildren().setAll(cboFont, txtFontSize);
|
||||
|
||||
@@ -17,15 +17,15 @@
|
||||
*/
|
||||
package org.jackhuang.hmcl.ui.account;
|
||||
|
||||
import javafx.beans.binding.Bindings;
|
||||
import javafx.beans.property.ObjectProperty;
|
||||
import javafx.beans.property.SimpleObjectProperty;
|
||||
import javafx.scene.image.Image;
|
||||
import org.jackhuang.hmcl.auth.Account;
|
||||
import org.jackhuang.hmcl.auth.offline.OfflineAccount;
|
||||
import org.jackhuang.hmcl.auth.yggdrasil.YggdrasilAccount;
|
||||
import org.jackhuang.hmcl.game.AccountHelper;
|
||||
import org.jackhuang.hmcl.game.TexturesLoader;
|
||||
import org.jackhuang.hmcl.setting.Theme;
|
||||
import org.jackhuang.hmcl.task.Schedulers;
|
||||
import org.jackhuang.hmcl.ui.SVG;
|
||||
import org.jackhuang.hmcl.ui.construct.AdvancedListItem;
|
||||
|
||||
@@ -38,23 +38,15 @@ public class AccountAdvancedListItem extends AdvancedListItem {
|
||||
protected void invalidated() {
|
||||
Account account = get();
|
||||
if (account == null) {
|
||||
titleProperty().unbind();
|
||||
setTitle(i18n("account.missing"));
|
||||
setSubtitle(i18n("account.missing.add"));
|
||||
imageProperty().unbind();
|
||||
setImage(new Image("/assets/img/craft_table.png"));
|
||||
} else {
|
||||
setTitle(account.getCharacter());
|
||||
titleProperty().bind(Bindings.createStringBinding(account::getCharacter, account));
|
||||
setSubtitle(accountSubtitle(account));
|
||||
|
||||
final int scaleRatio = 4;
|
||||
Image defaultSkin = AccountHelper.getDefaultSkin(account.getUUID(), scaleRatio);
|
||||
setImage(AccountHelper.getHead(defaultSkin, scaleRatio));
|
||||
|
||||
if (account instanceof YggdrasilAccount) {
|
||||
AccountHelper.loadSkinAsync((YggdrasilAccount) account).subscribe(Schedulers.javafx(), () -> {
|
||||
Image image = AccountHelper.getSkin((YggdrasilAccount) account, scaleRatio);
|
||||
setImage(AccountHelper.getHead(image, scaleRatio));
|
||||
});
|
||||
}
|
||||
imageProperty().bind(TexturesLoader.fxAvatarBinding(account, 32));
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -17,21 +17,28 @@
|
||||
*/
|
||||
package org.jackhuang.hmcl.ui.account;
|
||||
|
||||
import javafx.beans.binding.Bindings;
|
||||
import javafx.beans.binding.StringBinding;
|
||||
import javafx.beans.property.*;
|
||||
import javafx.scene.control.RadioButton;
|
||||
import javafx.scene.control.Skin;
|
||||
import javafx.scene.image.Image;
|
||||
import org.jackhuang.hmcl.auth.Account;
|
||||
import org.jackhuang.hmcl.auth.AuthenticationException;
|
||||
import org.jackhuang.hmcl.auth.CredentialExpiredException;
|
||||
import org.jackhuang.hmcl.auth.authlibinjector.AuthlibInjectorAccount;
|
||||
import org.jackhuang.hmcl.auth.authlibinjector.AuthlibInjectorServer;
|
||||
import org.jackhuang.hmcl.auth.offline.OfflineAccount;
|
||||
import org.jackhuang.hmcl.auth.yggdrasil.YggdrasilAccount;
|
||||
import org.jackhuang.hmcl.game.AccountHelper;
|
||||
import org.jackhuang.hmcl.game.TexturesLoader;
|
||||
import org.jackhuang.hmcl.setting.Accounts;
|
||||
import org.jackhuang.hmcl.task.Schedulers;
|
||||
import org.jackhuang.hmcl.ui.DialogController;
|
||||
|
||||
import static org.jackhuang.hmcl.util.Lang.thread;
|
||||
import static org.jackhuang.hmcl.util.Logging.LOG;
|
||||
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
|
||||
|
||||
import java.util.logging.Level;
|
||||
|
||||
public class AccountListItem extends RadioButton {
|
||||
|
||||
private final Account account;
|
||||
@@ -44,23 +51,24 @@ public class AccountListItem extends RadioButton {
|
||||
getStyleClass().clear();
|
||||
setUserData(account);
|
||||
|
||||
StringBuilder subtitleString = new StringBuilder(Accounts.getLocalizedLoginTypeName(Accounts.getAccountFactory(account)));
|
||||
String loginTypeName = Accounts.getLocalizedLoginTypeName(Accounts.getAccountFactory(account));
|
||||
if (account instanceof AuthlibInjectorAccount) {
|
||||
AuthlibInjectorServer server = ((AuthlibInjectorAccount) account).getServer();
|
||||
subtitleString.append(", ").append(i18n("account.injector.server")).append(": ").append(server.getName());
|
||||
subtitle.bind(Bindings.concat(
|
||||
loginTypeName, ", ", i18n("account.injector.server"), ": ",
|
||||
Bindings.createStringBinding(server::getName, server)));
|
||||
} else {
|
||||
subtitle.set(loginTypeName);
|
||||
}
|
||||
|
||||
if (account instanceof OfflineAccount)
|
||||
title.set(account.getCharacter());
|
||||
else
|
||||
title.set(account.getUsername() + " - " + account.getCharacter());
|
||||
subtitle.set(subtitleString.toString());
|
||||
StringBinding characterName = Bindings.createStringBinding(account::getCharacter, account);
|
||||
if (account instanceof OfflineAccount) {
|
||||
title.bind(characterName);
|
||||
} else {
|
||||
title.bind(Bindings.concat(account.getUsername(), " - ", characterName));
|
||||
}
|
||||
|
||||
final int scaleRatio = 4;
|
||||
Image image = account instanceof YggdrasilAccount ?
|
||||
AccountHelper.getSkin((YggdrasilAccount) account, scaleRatio) :
|
||||
AccountHelper.getDefaultSkin(account.getUUID(), scaleRatio);
|
||||
this.image.set(AccountHelper.getHead(image, scaleRatio));
|
||||
image.bind(TexturesLoader.fxAvatarBinding(account, 32));
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -69,19 +77,20 @@ public class AccountListItem extends RadioButton {
|
||||
}
|
||||
|
||||
public void refresh() {
|
||||
if (account instanceof YggdrasilAccount) {
|
||||
// progressBar.setVisible(true);
|
||||
AccountHelper.refreshSkinAsync((YggdrasilAccount) account)
|
||||
.finalized(Schedulers.javafx(), (variables, isDependentsSucceeded) -> {
|
||||
// progressBar.setVisible(false);
|
||||
|
||||
if (isDependentsSucceeded) {
|
||||
final int scaleRatio = 4;
|
||||
Image image = AccountHelper.getSkin((YggdrasilAccount) account, scaleRatio);
|
||||
this.image.set(AccountHelper.getHead(image, scaleRatio));
|
||||
}
|
||||
}).start();
|
||||
}
|
||||
account.clearCache();
|
||||
thread(() -> {
|
||||
try {
|
||||
account.logIn();
|
||||
} catch (CredentialExpiredException e) {
|
||||
try {
|
||||
DialogController.logIn(account);
|
||||
} catch (Exception e1) {
|
||||
LOG.log(Level.WARNING, "Failed to refresh " + account + " with password", e1);
|
||||
}
|
||||
} catch (AuthenticationException e) {
|
||||
LOG.log(Level.WARNING, "Failed to refresh " + account + " with token", e);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public void remove() {
|
||||
|
||||
@@ -21,18 +21,26 @@ import com.jfoenix.concurrency.JFXUtilities;
|
||||
import com.jfoenix.controls.JFXButton;
|
||||
import com.jfoenix.controls.JFXRadioButton;
|
||||
import com.jfoenix.effects.JFXDepthManager;
|
||||
|
||||
import javafx.beans.binding.Bindings;
|
||||
import javafx.geometry.Pos;
|
||||
import javafx.scene.control.Label;
|
||||
import javafx.scene.control.SkinBase;
|
||||
import javafx.scene.control.Tooltip;
|
||||
import javafx.scene.image.ImageView;
|
||||
import javafx.scene.layout.BorderPane;
|
||||
import javafx.scene.layout.HBox;
|
||||
import javafx.scene.layout.VBox;
|
||||
|
||||
import org.jackhuang.hmcl.setting.Theme;
|
||||
import org.jackhuang.hmcl.ui.FXUtils;
|
||||
import org.jackhuang.hmcl.ui.SVG;
|
||||
import org.jackhuang.hmcl.ui.construct.TwoLineListItem;
|
||||
|
||||
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
|
||||
|
||||
import org.jackhuang.hmcl.auth.authlibinjector.AuthlibInjectorAccount;
|
||||
import org.jackhuang.hmcl.auth.authlibinjector.AuthlibInjectorServer;
|
||||
|
||||
public class AccountListItemSkin extends SkinBase<AccountListItem> {
|
||||
|
||||
public AccountListItemSkin(AccountListItem skinnable) {
|
||||
@@ -58,8 +66,22 @@ public class AccountListItemSkin extends SkinBase<AccountListItem> {
|
||||
FXUtils.limitSize(imageView, 32, 32);
|
||||
imageView.imageProperty().bind(skinnable.imageProperty());
|
||||
|
||||
TwoLineListItem item = new TwoLineListItem();
|
||||
Label title = new Label();
|
||||
title.getStyleClass().add("title");
|
||||
title.textProperty().bind(skinnable.titleProperty());
|
||||
Label subtitle = new Label();
|
||||
subtitle.getStyleClass().add("subtitle");
|
||||
subtitle.textProperty().bind(skinnable.subtitleProperty());
|
||||
if (skinnable.getAccount() instanceof AuthlibInjectorAccount) {
|
||||
Tooltip tooltip = new Tooltip();
|
||||
AuthlibInjectorServer server = ((AuthlibInjectorAccount) skinnable.getAccount()).getServer();
|
||||
tooltip.textProperty().bind(Bindings.createStringBinding(server::toString, server));
|
||||
FXUtils.installSlowTooltip(subtitle, tooltip);
|
||||
}
|
||||
VBox item = new VBox(title, subtitle);
|
||||
item.getStyleClass().add("two-line-list-item");
|
||||
BorderPane.setAlignment(item, Pos.CENTER);
|
||||
|
||||
center.getChildren().setAll(imageView, item);
|
||||
root.setCenter(center);
|
||||
|
||||
@@ -69,7 +91,7 @@ public class AccountListItemSkin extends SkinBase<AccountListItem> {
|
||||
btnRefresh.setOnMouseClicked(e -> skinnable.refresh());
|
||||
btnRefresh.getStyleClass().add("toggle-icon4");
|
||||
btnRefresh.setGraphic(SVG.refresh(Theme.blackFillBinding(), -1, -1));
|
||||
JFXUtilities.runInFX(() -> FXUtils.installTooltip(btnRefresh, i18n("button.refresh")));
|
||||
JFXUtilities.runInFX(() -> FXUtils.installFastTooltip(btnRefresh, i18n("button.refresh")));
|
||||
right.getChildren().add(btnRefresh);
|
||||
|
||||
JFXButton btnRemove = new JFXButton();
|
||||
@@ -77,14 +99,12 @@ public class AccountListItemSkin extends SkinBase<AccountListItem> {
|
||||
btnRemove.getStyleClass().add("toggle-icon4");
|
||||
BorderPane.setAlignment(btnRemove, Pos.CENTER);
|
||||
btnRemove.setGraphic(SVG.delete(Theme.blackFillBinding(), -1, -1));
|
||||
JFXUtilities.runInFX(() -> FXUtils.installTooltip(btnRemove, i18n("button.delete")));
|
||||
JFXUtilities.runInFX(() -> FXUtils.installFastTooltip(btnRemove, i18n("button.delete")));
|
||||
right.getChildren().add(btnRemove);
|
||||
root.setRight(right);
|
||||
|
||||
root.setStyle("-fx-background-color: white; -fx-padding: 8 8 8 0;");
|
||||
JFXDepthManager.setDepth(root, 1);
|
||||
item.titleProperty().bind(skinnable.titleProperty());
|
||||
item.subtitleProperty().bind(skinnable.subtitleProperty());
|
||||
|
||||
getChildren().setAll(root);
|
||||
}
|
||||
|
||||
@@ -37,8 +37,7 @@ public class AccountLoginPane extends StackPane {
|
||||
private final Consumer<AuthInfo> success;
|
||||
private final Runnable failed;
|
||||
|
||||
@FXML
|
||||
private Label lblUsername;
|
||||
@FXML private Label lblUsername;
|
||||
@FXML private JFXPasswordField txtPassword;
|
||||
@FXML private Label lblCreationWarning;
|
||||
@FXML private JFXProgressBar progressBar;
|
||||
@@ -59,25 +58,19 @@ public class AccountLoginPane extends StackPane {
|
||||
String password = txtPassword.getText();
|
||||
progressBar.setVisible(true);
|
||||
lblCreationWarning.setText("");
|
||||
Task.ofResult("login", () -> {
|
||||
try {
|
||||
return oldAccount.logInWithPassword(password);
|
||||
} catch (Exception e) {
|
||||
return e;
|
||||
}
|
||||
}).subscribe(Schedulers.javafx(), variable -> {
|
||||
Object account = variable.get("login");
|
||||
if (account instanceof AuthInfo) {
|
||||
success.accept(((AuthInfo) account));
|
||||
fireEvent(new DialogCloseEvent());
|
||||
} else if (account instanceof NoSelectedCharacterException) {
|
||||
fireEvent(new DialogCloseEvent());
|
||||
} else if (account instanceof Exception) {
|
||||
lblCreationWarning.setText(AddAccountPane.accountException((Exception) account));
|
||||
}
|
||||
|
||||
progressBar.setVisible(false);
|
||||
});
|
||||
Task.ofResult(() -> oldAccount.logInWithPassword(password))
|
||||
.finalizedResult(Schedulers.javafx(), authInfo -> {
|
||||
success.accept(authInfo);
|
||||
fireEvent(new DialogCloseEvent());
|
||||
progressBar.setVisible(false);
|
||||
}, e -> {
|
||||
if (e instanceof NoSelectedCharacterException) {
|
||||
fireEvent(new DialogCloseEvent());
|
||||
} else {
|
||||
lblCreationWarning.setText(AddAccountPane.accountException(e));
|
||||
}
|
||||
progressBar.setVisible(false);
|
||||
}).start();
|
||||
}
|
||||
|
||||
@FXML
|
||||
|
||||
@@ -19,13 +19,17 @@ package org.jackhuang.hmcl.ui.account;
|
||||
|
||||
import com.jfoenix.concurrency.JFXUtilities;
|
||||
import com.jfoenix.controls.*;
|
||||
|
||||
import javafx.application.Platform;
|
||||
import javafx.beans.binding.Bindings;
|
||||
import javafx.beans.property.ListProperty;
|
||||
import javafx.beans.property.ReadOnlyObjectProperty;
|
||||
import javafx.beans.property.SimpleListProperty;
|
||||
import javafx.collections.FXCollections;
|
||||
import javafx.fxml.FXML;
|
||||
import javafx.geometry.Pos;
|
||||
import javafx.scene.control.Hyperlink;
|
||||
import javafx.scene.control.Label;
|
||||
import javafx.scene.image.Image;
|
||||
import javafx.scene.image.ImageView;
|
||||
import javafx.scene.layout.BorderPane;
|
||||
import javafx.scene.layout.HBox;
|
||||
@@ -35,20 +39,22 @@ import org.jackhuang.hmcl.auth.authlibinjector.AuthlibInjectorDownloadException;
|
||||
import org.jackhuang.hmcl.auth.authlibinjector.AuthlibInjectorServer;
|
||||
import org.jackhuang.hmcl.auth.yggdrasil.GameProfile;
|
||||
import org.jackhuang.hmcl.auth.yggdrasil.RemoteAuthenticationException;
|
||||
import org.jackhuang.hmcl.auth.yggdrasil.YggdrasilAccount;
|
||||
import org.jackhuang.hmcl.game.AccountHelper;
|
||||
import org.jackhuang.hmcl.auth.yggdrasil.YggdrasilService;
|
||||
import org.jackhuang.hmcl.game.TexturesLoader;
|
||||
import org.jackhuang.hmcl.setting.Accounts;
|
||||
import org.jackhuang.hmcl.task.Schedulers;
|
||||
import org.jackhuang.hmcl.task.Task;
|
||||
import org.jackhuang.hmcl.ui.Controllers;
|
||||
import org.jackhuang.hmcl.ui.FXUtils;
|
||||
import org.jackhuang.hmcl.ui.construct.*;
|
||||
import org.jackhuang.hmcl.util.Logging;
|
||||
import org.jackhuang.hmcl.util.javafx.MultiStepBinding;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.CountDownLatch;
|
||||
import java.util.logging.Level;
|
||||
|
||||
import static java.util.Collections.emptyList;
|
||||
import static java.util.Collections.unmodifiableList;
|
||||
import static java.util.Objects.requireNonNull;
|
||||
import static org.jackhuang.hmcl.setting.ConfigHolder.config;
|
||||
import static org.jackhuang.hmcl.ui.FXUtils.*;
|
||||
@@ -63,12 +69,13 @@ public class AddAccountPane extends StackPane {
|
||||
@FXML private JFXComboBox<AccountFactory<?>> cboType;
|
||||
@FXML private JFXComboBox<AuthlibInjectorServer> cboServers;
|
||||
@FXML private Label lblInjectorServer;
|
||||
@FXML private Hyperlink linkManageInjectorServers;
|
||||
@FXML private JFXDialogLayout layout;
|
||||
@FXML private JFXButton btnAccept;
|
||||
@FXML private JFXButton btnAddServer;
|
||||
@FXML private JFXButton btnManageServer;
|
||||
@FXML private SpinnerPane acceptPane;
|
||||
@FXML private HBox linksContainer;
|
||||
|
||||
private ListProperty<Hyperlink> links = new SimpleListProperty<>();;
|
||||
|
||||
public AddAccountPane() {
|
||||
FXUtils.loadFXML(this, "/assets/fxml/account-add.fxml");
|
||||
@@ -79,7 +86,7 @@ public class AddAccountPane extends StackPane {
|
||||
cboServers.getItems().addListener(onInvalidating(this::selectDefaultServer));
|
||||
selectDefaultServer();
|
||||
|
||||
cboType.getItems().setAll(Accounts.FACTORY_OFFLINE, Accounts.FACTORY_YGGDRASIL, Accounts.FACTORY_AUTHLIB_INJECTOR);
|
||||
cboType.getItems().setAll(Accounts.FACTORY_OFFLINE, Accounts.FACTORY_MOJANG, Accounts.FACTORY_AUTHLIB_INJECTOR);
|
||||
cboType.setConverter(stringConverter(Accounts::getLocalizedLoginTypeName));
|
||||
// try selecting the preferred login type
|
||||
cboType.getSelectionModel().select(
|
||||
@@ -117,6 +124,34 @@ public class AddAccountPane extends StackPane {
|
||||
txtUsername.textProperty(),
|
||||
txtPassword.textProperty(), txtPassword.visibleProperty(),
|
||||
cboServers.getSelectionModel().selectedItemProperty(), cboServers.visibleProperty()));
|
||||
|
||||
// authlib-injector links
|
||||
links.bind(MultiStepBinding.of(cboServers.getSelectionModel().selectedItemProperty())
|
||||
.map(AddAccountPane::createHyperlinks)
|
||||
.map(FXCollections::observableList));
|
||||
Bindings.bindContent(linksContainer.getChildren(), links);
|
||||
linksContainer.visibleProperty().bind(cboServers.visibleProperty());
|
||||
}
|
||||
|
||||
private static final String[] ALLOWED_LINKS = { "register" };
|
||||
|
||||
public static List<Hyperlink> createHyperlinks(AuthlibInjectorServer server) {
|
||||
if (server == null) {
|
||||
return emptyList();
|
||||
}
|
||||
|
||||
Map<String, String> links = server.getLinks();
|
||||
List<Hyperlink> result = new ArrayList<>();
|
||||
for (String key : ALLOWED_LINKS) {
|
||||
String value = links.get(key);
|
||||
if (value != null) {
|
||||
Hyperlink link = new Hyperlink(i18n("account.injector.link." + key));
|
||||
FXUtils.installSlowTooltip(link, value);
|
||||
link.setOnAction(e -> FXUtils.openLink(value));
|
||||
result.add(link);
|
||||
}
|
||||
}
|
||||
return unmodifiableList(result);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -160,10 +195,8 @@ public class AddAccountPane extends StackPane {
|
||||
AccountFactory<?> factory = cboType.getSelectionModel().getSelectedItem();
|
||||
Object additionalData = getAuthAdditionalData();
|
||||
|
||||
Task.ofResult("create_account", () -> factory.create(new Selector(), username, password, additionalData))
|
||||
.finalized(Schedulers.javafx(), variables -> {
|
||||
|
||||
Account account = variables.get("create_account");
|
||||
Task.ofResult(() -> factory.create(new Selector(), username, password, additionalData))
|
||||
.finalizedResult(Schedulers.javafx(), account -> {
|
||||
int oldIndex = Accounts.getAccounts().indexOf(account);
|
||||
if (oldIndex == -1) {
|
||||
Accounts.getAccounts().add(account);
|
||||
@@ -214,7 +247,7 @@ public class AddAccountPane extends StackPane {
|
||||
private final CountDownLatch latch = new CountDownLatch(1);
|
||||
private GameProfile selectedProfile = null;
|
||||
|
||||
{
|
||||
public Selector() {
|
||||
setStyle("-fx-padding: 8px;");
|
||||
|
||||
cancel.setText(i18n("button.cancel"));
|
||||
@@ -232,45 +265,33 @@ public class AddAccountPane extends StackPane {
|
||||
}
|
||||
|
||||
@Override
|
||||
public GameProfile select(Account account, List<GameProfile> names) throws NoSelectedCharacterException {
|
||||
if (!(account instanceof YggdrasilAccount))
|
||||
return CharacterSelector.DEFAULT.select(account, names);
|
||||
YggdrasilAccount yggdrasilAccount = (YggdrasilAccount) account;
|
||||
public GameProfile select(YggdrasilService service, List<GameProfile> profiles) throws NoSelectedCharacterException {
|
||||
Platform.runLater(() -> {
|
||||
for (GameProfile profile : profiles) {
|
||||
ImageView portraitView = new ImageView();
|
||||
portraitView.setSmooth(false);
|
||||
portraitView.imageProperty().bind(TexturesLoader.fxAvatarBinding(service, profile.getId(), 32));
|
||||
FXUtils.limitSize(portraitView, 32, 32);
|
||||
|
||||
for (GameProfile profile : names) {
|
||||
Image image;
|
||||
final int scaleRatio = 4;
|
||||
try {
|
||||
image = AccountHelper.getSkinImmediately(yggdrasilAccount, profile, scaleRatio);
|
||||
} catch (Exception e) {
|
||||
Logging.LOG.log(Level.WARNING, "Failed to get skin for " + profile.getName(), e);
|
||||
image = AccountHelper.getDefaultSkin(profile.getId(), scaleRatio);
|
||||
IconedItem accountItem = new IconedItem(portraitView, profile.getName());
|
||||
accountItem.setOnMouseClicked(e -> {
|
||||
selectedProfile = profile;
|
||||
latch.countDown();
|
||||
});
|
||||
listBox.add(accountItem);
|
||||
}
|
||||
|
||||
ImageView portraitView = new ImageView();
|
||||
portraitView.setSmooth(false);
|
||||
portraitView.setImage(AccountHelper.getHead(image, scaleRatio));
|
||||
FXUtils.limitSize(portraitView, 32, 32);
|
||||
|
||||
IconedItem accountItem = new IconedItem(portraitView, profile.getName());
|
||||
accountItem.setOnMouseClicked(e -> {
|
||||
selectedProfile = profile;
|
||||
latch.countDown();
|
||||
});
|
||||
listBox.add(accountItem);
|
||||
}
|
||||
|
||||
JFXUtilities.runInFX(() -> Controllers.dialog(this));
|
||||
Controllers.dialog(this);
|
||||
});
|
||||
|
||||
try {
|
||||
latch.await();
|
||||
|
||||
if (selectedProfile == null)
|
||||
throw new NoSelectedCharacterException(account);
|
||||
throw new NoSelectedCharacterException();
|
||||
|
||||
return selectedProfile;
|
||||
} catch (InterruptedException ignore) {
|
||||
throw new NoSelectedCharacterException(account);
|
||||
throw new NoSelectedCharacterException();
|
||||
} finally {
|
||||
JFXUtilities.runInFX(() -> Selector.this.fireEvent(new DialogCloseEvent()));
|
||||
}
|
||||
@@ -298,6 +319,8 @@ public class AddAccountPane extends StackPane {
|
||||
return exception.getMessage();
|
||||
} else if (exception instanceof AuthlibInjectorDownloadException) {
|
||||
return i18n("account.failed.injector_download_failure");
|
||||
} else if (exception instanceof CharacterDeletedException) {
|
||||
return i18n("account.failed.character_deleted");
|
||||
} else if (exception.getClass() == AuthenticationException.class) {
|
||||
return exception.getLocalizedMessage();
|
||||
} else {
|
||||
|
||||
@@ -19,6 +19,8 @@ package org.jackhuang.hmcl.ui.account;
|
||||
|
||||
import com.jfoenix.controls.JFXButton;
|
||||
import com.jfoenix.effects.JFXDepthManager;
|
||||
|
||||
import javafx.beans.binding.Bindings;
|
||||
import javafx.geometry.Pos;
|
||||
import javafx.scene.control.Label;
|
||||
import javafx.scene.layout.BorderPane;
|
||||
@@ -33,17 +35,17 @@ public final class AuthlibInjectorServerItem extends BorderPane {
|
||||
private final AuthlibInjectorServer server;
|
||||
|
||||
private final Label lblServerName = new Label();
|
||||
private final Label lblServerIp = new Label();
|
||||
private final Label lblServerUrl = new Label();
|
||||
|
||||
public AuthlibInjectorServerItem(AuthlibInjectorServer server, Consumer<AuthlibInjectorServerItem> deleteCallback) {
|
||||
this.server = server;
|
||||
|
||||
lblServerName.setStyle("-fx-font-size: 15;");
|
||||
lblServerIp.setStyle("-fx-font-size: 10;");
|
||||
lblServerUrl.setStyle("-fx-font-size: 10;");
|
||||
|
||||
VBox center = new VBox();
|
||||
BorderPane.setAlignment(center, Pos.CENTER);
|
||||
center.getChildren().addAll(lblServerName, lblServerIp);
|
||||
center.getChildren().addAll(lblServerName, lblServerUrl);
|
||||
setCenter(center);
|
||||
|
||||
JFXButton right = new JFXButton();
|
||||
@@ -55,8 +57,8 @@ public final class AuthlibInjectorServerItem extends BorderPane {
|
||||
|
||||
setStyle("-fx-background-radius: 2; -fx-background-color: white; -fx-padding: 8;");
|
||||
JFXDepthManager.setDepth(this, 1);
|
||||
lblServerName.setText(server.getName());
|
||||
lblServerIp.setText(server.getUrl());
|
||||
lblServerName.textProperty().bind(Bindings.createStringBinding(server::getName, server));
|
||||
lblServerUrl.setText(server.getUrl());
|
||||
}
|
||||
|
||||
public AuthlibInjectorServer getServer() {
|
||||
|
||||
@@ -61,7 +61,7 @@ public class FileItem extends BorderPane {
|
||||
right.setGraphic(SVG.pencil(Theme.blackFillBinding(), 15, 15));
|
||||
right.getStyleClass().add("toggle-icon4");
|
||||
right.setOnMouseClicked(e -> onExplore());
|
||||
FXUtils.installTooltip(right, i18n("button.edit"));
|
||||
FXUtils.installFastTooltip(right, i18n("button.edit"));
|
||||
setRight(right);
|
||||
|
||||
Tooltip tip = new Tooltip();
|
||||
|
||||
@@ -24,27 +24,27 @@ import static javafx.collections.FXCollections.singletonObservableList;
|
||||
import org.jackhuang.hmcl.util.javafx.MultiStepBinding;
|
||||
|
||||
import com.jfoenix.controls.JFXComboBox;
|
||||
import com.jfoenix.controls.JFXListCell;
|
||||
|
||||
import javafx.beans.NamedArg;
|
||||
import javafx.beans.binding.Bindings;
|
||||
import javafx.scene.control.ListCell;
|
||||
import javafx.scene.text.Font;
|
||||
|
||||
public class FontComboBox extends JFXComboBox<String> {
|
||||
|
||||
private boolean loaded = false;
|
||||
|
||||
public FontComboBox(@NamedArg(value = "fontSize", defaultValue = "12.0") double fontSize,
|
||||
@NamedArg(value = "enableStyle", defaultValue = "false") boolean enableStyle) {
|
||||
public FontComboBox(@NamedArg(value = "fontSize", defaultValue = "12.0") double fontSize) {
|
||||
styleProperty().bind(Bindings.concat("-fx-font-family: \"", valueProperty(), "\""));
|
||||
|
||||
setCellFactory(listView -> new ListCell<String>() {
|
||||
setCellFactory(listView -> new JFXListCell<String>() {
|
||||
@Override
|
||||
protected void updateItem(String item, boolean empty) {
|
||||
public void updateItem(String item, boolean empty) {
|
||||
super.updateItem(item, empty);
|
||||
if (item != null) {
|
||||
if (!empty) {
|
||||
setText(item);
|
||||
setFont(new Font(item, fontSize));
|
||||
setGraphic(null);
|
||||
setStyle("-fx-font-family: \"" + item + "\"");
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
@@ -30,7 +30,7 @@ public class IconedMenuItem extends IconedItem {
|
||||
}
|
||||
|
||||
public IconedMenuItem addTooltip(String tooltip) {
|
||||
FXUtils.installTooltip(this, tooltip);
|
||||
FXUtils.installFastTooltip(this, tooltip);
|
||||
return this;
|
||||
}
|
||||
}
|
||||
@@ -63,7 +63,7 @@ public final class ImagePickerItem extends BorderPane {
|
||||
deleteButton.onMouseClickedProperty().bind(onDeleteButtonClicked);
|
||||
deleteButton.getStyleClass().add("toggle-icon4");
|
||||
|
||||
FXUtils.installTooltip(selectButton, i18n("button.edit"));
|
||||
FXUtils.installFastTooltip(selectButton, i18n("button.edit"));
|
||||
|
||||
HBox hBox = new HBox();
|
||||
hBox.getChildren().setAll(imageView, selectButton, deleteButton);
|
||||
|
||||
@@ -19,7 +19,9 @@ package org.jackhuang.hmcl.ui.construct;
|
||||
|
||||
import com.jfoenix.controls.JFXSpinner;
|
||||
import javafx.beans.DefaultProperty;
|
||||
import javafx.beans.property.BooleanProperty;
|
||||
import javafx.beans.property.ObjectProperty;
|
||||
import javafx.beans.property.SimpleBooleanProperty;
|
||||
import javafx.beans.property.SimpleObjectProperty;
|
||||
import javafx.scene.Node;
|
||||
import javafx.scene.layout.StackPane;
|
||||
@@ -32,19 +34,29 @@ public class SpinnerPane extends StackPane {
|
||||
private final JFXSpinner spinner = new JFXSpinner();
|
||||
private final StackPane contentPane = new StackPane();
|
||||
private final ObjectProperty<Node> content = new SimpleObjectProperty<>(this, "content");
|
||||
private final BooleanProperty loading = new SimpleBooleanProperty(this, "loading") {
|
||||
protected void invalidated() {
|
||||
if (get())
|
||||
transitionHandler.setContent(spinner, ContainerAnimations.FADE.getAnimationProducer());
|
||||
else
|
||||
transitionHandler.setContent(contentPane, ContainerAnimations.FADE.getAnimationProducer());
|
||||
}
|
||||
};
|
||||
|
||||
public SpinnerPane() {
|
||||
getStyleClass().add("spinner-pane");
|
||||
|
||||
getChildren().setAll(contentPane);
|
||||
|
||||
content.addListener((a, b, newValue) -> contentPane.getChildren().setAll(newValue));
|
||||
}
|
||||
|
||||
public void showSpinner() {
|
||||
transitionHandler.setContent(spinner, ContainerAnimations.FADE.getAnimationProducer());
|
||||
setLoading(true);
|
||||
}
|
||||
|
||||
public void hideSpinner() {
|
||||
transitionHandler.setContent(contentPane, ContainerAnimations.FADE.getAnimationProducer());
|
||||
setLoading(false);
|
||||
}
|
||||
|
||||
public Node getContent() {
|
||||
@@ -58,4 +70,16 @@ public class SpinnerPane extends StackPane {
|
||||
public void setContent(Node content) {
|
||||
this.content.set(content);
|
||||
}
|
||||
|
||||
public boolean isLoading() {
|
||||
return loading.get();
|
||||
}
|
||||
|
||||
public BooleanProperty loadingProperty() {
|
||||
return loading;
|
||||
}
|
||||
|
||||
public void setLoading(boolean loading) {
|
||||
this.loading.set(loading);
|
||||
}
|
||||
}
|
||||
@@ -17,14 +17,17 @@
|
||||
*/
|
||||
package org.jackhuang.hmcl.ui.construct;
|
||||
|
||||
import com.jfoenix.controls.JFXListView;
|
||||
import com.jfoenix.controls.JFXProgressBar;
|
||||
import javafx.application.Platform;
|
||||
import javafx.beans.binding.Bindings;
|
||||
import javafx.beans.property.ReadOnlyIntegerProperty;
|
||||
import javafx.beans.property.ReadOnlyIntegerWrapper;
|
||||
import javafx.geometry.Insets;
|
||||
import javafx.scene.control.Label;
|
||||
import javafx.scene.control.ListCell;
|
||||
import javafx.scene.layout.BorderPane;
|
||||
import javafx.scene.layout.StackPane;
|
||||
import javafx.scene.layout.VBox;
|
||||
import org.jackhuang.hmcl.download.forge.ForgeInstallTask;
|
||||
import org.jackhuang.hmcl.download.game.GameAssetDownloadTask;
|
||||
import org.jackhuang.hmcl.download.liteloader.LiteLoaderInstallTask;
|
||||
@@ -42,15 +45,16 @@ import java.util.Map;
|
||||
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
|
||||
|
||||
public final class TaskListPane extends StackPane {
|
||||
private final AdvancedListBox listBox = new AdvancedListBox();
|
||||
private final JFXListView<Task> listBox = new JFXListView<>();
|
||||
private final Map<Task, ProgressListNode> nodes = new HashMap<>();
|
||||
private final ReadOnlyIntegerWrapper finishedTasks = new ReadOnlyIntegerWrapper();
|
||||
private final ReadOnlyIntegerWrapper totTasks = new ReadOnlyIntegerWrapper();
|
||||
|
||||
public TaskListPane() {
|
||||
listBox.setSpacing(0);
|
||||
|
||||
getChildren().setAll(listBox);
|
||||
|
||||
listBox.setPadding(Insets.EMPTY);
|
||||
listBox.setCellFactory(listView -> new ProgressListNode());
|
||||
}
|
||||
|
||||
public ReadOnlyIntegerProperty finishedTasksProperty() {
|
||||
@@ -66,7 +70,7 @@ public final class TaskListPane extends StackPane {
|
||||
@Override
|
||||
public void onStart() {
|
||||
Platform.runLater(() -> {
|
||||
listBox.clear();
|
||||
listBox.getItems().clear();
|
||||
finishedTasks.set(0);
|
||||
totTasks.set(0);
|
||||
});
|
||||
@@ -108,66 +112,66 @@ public final class TaskListPane extends StackPane {
|
||||
task.setName(i18n("modpack.scan"));
|
||||
}
|
||||
|
||||
ProgressListNode node = new ProgressListNode(task);
|
||||
nodes.put(task, node);
|
||||
Platform.runLater(() -> listBox.add(node));
|
||||
|
||||
Platform.runLater(() -> listBox.getItems().add(task));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onFinished(Task task) {
|
||||
ProgressListNode node = nodes.remove(task);
|
||||
if (node == null)
|
||||
return;
|
||||
node.unbind();
|
||||
Platform.runLater(() -> {
|
||||
listBox.remove(node);
|
||||
finishedTasks.set(finishedTasks.getValue() + 1);
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onFailed(Task task, Throwable throwable) {
|
||||
ProgressListNode node = nodes.remove(task);
|
||||
if (node == null)
|
||||
return;
|
||||
Platform.runLater(() -> {
|
||||
node.setThrowable(throwable);
|
||||
finishedTasks.set(finishedTasks.getValue() + 1);
|
||||
if (listBox.getItems().remove(task))
|
||||
finishedTasks.set(finishedTasks.getValue() + 1);
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private static class ProgressListNode extends VBox {
|
||||
private static class ProgressListNode extends ListCell<Task> {
|
||||
private final BorderPane borderPane = new BorderPane();
|
||||
private final JFXProgressBar bar = new JFXProgressBar();
|
||||
private final Label title = new Label();
|
||||
private final Label state = new Label();
|
||||
|
||||
public ProgressListNode(Task task) {
|
||||
bar.progressProperty().bind(task.progressProperty());
|
||||
title.setText(task.getName());
|
||||
state.textProperty().bind(task.messageProperty());
|
||||
|
||||
BorderPane borderPane = new BorderPane();
|
||||
{
|
||||
borderPane.setLeft(title);
|
||||
borderPane.setRight(state);
|
||||
getChildren().addAll(borderPane, bar);
|
||||
borderPane.setBottom(bar);
|
||||
borderPane.setMinWidth(0);
|
||||
borderPane.setPrefWidth(1);
|
||||
|
||||
setPadding(Insets.EMPTY);
|
||||
|
||||
bar.minWidthProperty().bind(widthProperty());
|
||||
bar.prefWidthProperty().bind(widthProperty());
|
||||
bar.maxWidthProperty().bind(widthProperty());
|
||||
}
|
||||
|
||||
public void unbind() {
|
||||
bar.progressProperty().unbind();
|
||||
state.textProperty().unbind();
|
||||
}
|
||||
@Override
|
||||
protected void updateItem(Task item, boolean empty) {
|
||||
boolean wasEmpty = isEmpty();
|
||||
Task oldTask = getItem();
|
||||
|
||||
public void setThrowable(Throwable throwable) {
|
||||
unbind();
|
||||
state.setText(throwable.getLocalizedMessage());
|
||||
bar.setProgress(0);
|
||||
if (!wasEmpty && oldTask != null) {
|
||||
bar.progressProperty().unbind();
|
||||
state.textProperty().unbind();
|
||||
}
|
||||
|
||||
super.updateItem(item, empty);
|
||||
|
||||
if (empty || item == null) {
|
||||
setGraphic(null);
|
||||
} else {
|
||||
setGraphic(borderPane);
|
||||
bar.visibleProperty().bind(Bindings.createBooleanBinding(() -> item.progressProperty().get() != -1, item.progressProperty()));
|
||||
bar.progressProperty().bind(item.progressProperty());
|
||||
state.textProperty().bind(Bindings.createObjectBinding(() -> {
|
||||
if (item.getState() == Task.TaskState.FAILED) {
|
||||
return item.getLastException().getLocalizedMessage();
|
||||
} else {
|
||||
return item.messageProperty().get();
|
||||
}
|
||||
}, item.messageProperty(), item.stateProperty()));
|
||||
title.setText(item.getName());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -19,32 +19,15 @@ package org.jackhuang.hmcl.ui.construct;
|
||||
|
||||
import javafx.beans.property.SimpleStringProperty;
|
||||
import javafx.beans.property.StringProperty;
|
||||
import javafx.css.CssMetaData;
|
||||
import javafx.css.SimpleStyleableObjectProperty;
|
||||
import javafx.css.Styleable;
|
||||
import javafx.css.StyleableObjectProperty;
|
||||
import javafx.css.StyleablePropertyFactory;
|
||||
import javafx.scene.control.Label;
|
||||
import javafx.scene.layout.StackPane;
|
||||
import javafx.scene.layout.VBox;
|
||||
import javafx.scene.paint.Color;
|
||||
import javafx.scene.paint.Paint;
|
||||
import javafx.scene.text.Font;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public class TwoLineListItem extends StackPane {
|
||||
public class TwoLineListItem extends VBox {
|
||||
private static final String DEFAULT_STYLE_CLASS = "two-line-list-item";
|
||||
|
||||
private final StringProperty title = new SimpleStringProperty(this, "title");
|
||||
private final StringProperty subtitle = new SimpleStringProperty(this, "subtitle");
|
||||
|
||||
private final StyleableObjectProperty<Font> titleFont = new SimpleStyleableObjectProperty<>(StyleableProperties.TITLE_FONT, this, "title-font", Font.font(15));
|
||||
private final StyleableObjectProperty<Font> subtitleFont = new SimpleStyleableObjectProperty<>(StyleableProperties.SUBTITLE_FONT, this, "subtitle-font", Font.getDefault());
|
||||
|
||||
private final StyleableObjectProperty<Paint> titleFill = new SimpleStyleableObjectProperty<>(StyleableProperties.TITLE_FILL, this, "title-fill", Color.BLACK);
|
||||
private final StyleableObjectProperty<Paint> subtitleFill = new SimpleStyleableObjectProperty<>(StyleableProperties.SUBTITLE_FILL, this, "subtitle-fill", Color.GRAY);
|
||||
|
||||
public TwoLineListItem(String titleString, String subtitleString) {
|
||||
this();
|
||||
|
||||
@@ -55,19 +38,14 @@ public class TwoLineListItem extends StackPane {
|
||||
public TwoLineListItem() {
|
||||
setMouseTransparent(true);
|
||||
Label lblTitle = new Label();
|
||||
lblTitle.textFillProperty().bind(titleFill);
|
||||
lblTitle.fontProperty().bind(titleFont);
|
||||
lblTitle.getStyleClass().add("title");
|
||||
lblTitle.textProperty().bind(title);
|
||||
|
||||
Label lblSubtitle = new Label();
|
||||
lblSubtitle.textFillProperty().bind(subtitleFill);
|
||||
lblSubtitle.fontProperty().bind(subtitleFont);
|
||||
lblSubtitle.getStyleClass().add("subtitle");
|
||||
lblSubtitle.textProperty().bind(subtitle);
|
||||
|
||||
VBox vbox = new VBox();
|
||||
vbox.getChildren().setAll(lblTitle, lblSubtitle);
|
||||
getChildren().setAll(vbox);
|
||||
|
||||
getChildren().setAll(lblTitle, lblSubtitle);
|
||||
getStyleClass().add(DEFAULT_STYLE_CLASS);
|
||||
}
|
||||
|
||||
@@ -95,74 +73,8 @@ public class TwoLineListItem extends StackPane {
|
||||
this.subtitle.set(subtitle);
|
||||
}
|
||||
|
||||
public Font getTitleFont() {
|
||||
return titleFont.get();
|
||||
}
|
||||
|
||||
public StyleableObjectProperty<Font> titleFontProperty() {
|
||||
return titleFont;
|
||||
}
|
||||
|
||||
public void setTitleFont(Font titleFont) {
|
||||
this.titleFont.set(titleFont);
|
||||
}
|
||||
|
||||
public Font getSubtitleFont() {
|
||||
return subtitleFont.get();
|
||||
}
|
||||
|
||||
public StyleableObjectProperty<Font> subtitleFontProperty() {
|
||||
return subtitleFont;
|
||||
}
|
||||
|
||||
public void setSubtitleFont(Font subtitleFont) {
|
||||
this.subtitleFont.set(subtitleFont);
|
||||
}
|
||||
|
||||
public Paint getTitleFill() {
|
||||
return titleFill.get();
|
||||
}
|
||||
|
||||
public StyleableObjectProperty<Paint> titleFillProperty() {
|
||||
return titleFill;
|
||||
}
|
||||
|
||||
public void setTitleFill(Paint titleFill) {
|
||||
this.titleFill.set(titleFill);
|
||||
}
|
||||
|
||||
public Paint getSubtitleFill() {
|
||||
return subtitleFill.get();
|
||||
}
|
||||
|
||||
public StyleableObjectProperty<Paint> subtitleFillProperty() {
|
||||
return subtitleFill;
|
||||
}
|
||||
|
||||
public void setSubtitleFill(Paint subtitleFill) {
|
||||
this.subtitleFill.set(subtitleFill);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return getTitle();
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<CssMetaData<? extends Styleable, ?>> getCssMetaData() {
|
||||
return getClassCssMetaData();
|
||||
}
|
||||
|
||||
public static List<CssMetaData<? extends Styleable, ?>> getClassCssMetaData() {
|
||||
return StyleableProperties.FACTORY.getCssMetaData();
|
||||
}
|
||||
|
||||
private static class StyleableProperties {
|
||||
private static final StyleablePropertyFactory<TwoLineListItem> FACTORY = new StyleablePropertyFactory<>(StackPane.getClassCssMetaData());
|
||||
|
||||
private static final CssMetaData<TwoLineListItem, Font> TITLE_FONT = FACTORY.createFontCssMetaData("-jfx-title-font", s -> s.titleFont, Font.font(15));
|
||||
private static final CssMetaData<TwoLineListItem, Font> SUBTITLE_FONT = FACTORY.createFontCssMetaData("-jfx-subtitle-font", s -> s.subtitleFont);
|
||||
private static final CssMetaData<TwoLineListItem, Paint> TITLE_FILL = FACTORY.createPaintCssMetaData("-jfx-title-fill", s -> s.titleFill);
|
||||
private static final CssMetaData<TwoLineListItem, Paint> SUBTITLE_FILL = FACTORY.createPaintCssMetaData("-jfx-subtitle-fill", s -> s.subtitleFill, Color.GREY);
|
||||
}
|
||||
}
|
||||
@@ -207,7 +207,7 @@ public class DecoratorSkin extends SkinBase<Decorator> {
|
||||
else navLeft.getChildren().setAll(backNavButton);
|
||||
});
|
||||
|
||||
skinnable.showCloseAsHomeProperty().addListener((a, b, newValue) -> {
|
||||
FXUtils.onChangeAndOperate(skinnable.showCloseAsHomeProperty(), (newValue) -> {
|
||||
if (newValue)
|
||||
closeNavButton.setGraphic(SVG.home(Theme.foregroundFillBinding(), -1, -1));
|
||||
else
|
||||
|
||||
@@ -109,10 +109,9 @@ public final class ModpackPage extends StackPane implements WizardPage {
|
||||
}
|
||||
|
||||
spinnerPane.showSpinner();
|
||||
Task.ofResult("encoding", () -> CompressingUtils.findSuitableEncoding(selectedFile.toPath()))
|
||||
.then(Task.ofResult("manifest", var ->
|
||||
manifest = ModpackHelper.readModpackManifest(selectedFile.toPath(), var.get("encoding"))))
|
||||
.finalized(Schedulers.javafx(), var -> {
|
||||
Task.ofResult(() -> CompressingUtils.findSuitableEncoding(selectedFile.toPath()))
|
||||
.thenResult(encoding -> manifest = ModpackHelper.readModpackManifest(selectedFile.toPath(), encoding))
|
||||
.finalizedResult(Schedulers.javafx(), manifest -> {
|
||||
spinnerPane.hideSpinner();
|
||||
controller.getSettings().put(MODPACK_MANIFEST, manifest);
|
||||
lblName.setText(manifest.getName());
|
||||
|
||||
@@ -44,7 +44,7 @@ public class DatapackListItem extends BorderPane {
|
||||
setCenter(modItem);
|
||||
|
||||
JFXButton btnRemove = new JFXButton();
|
||||
FXUtils.installTooltip(btnRemove, i18n("datapack.remove"));
|
||||
FXUtils.installFastTooltip(btnRemove, i18n("datapack.remove"));
|
||||
btnRemove.setOnMouseClicked(e -> deleteCallback.accept(this));
|
||||
btnRemove.getStyleClass().add("toggle-icon4");
|
||||
BorderPane.setAlignment(btnRemove, Pos.CENTER);
|
||||
|
||||
@@ -73,7 +73,7 @@ public class GameListItemSkin extends SkinBase<GameListItem> {
|
||||
btnUpgrade.setOnMouseClicked(e -> skinnable.update());
|
||||
btnUpgrade.getStyleClass().add("toggle-icon4");
|
||||
btnUpgrade.setGraphic(SVG.update(Theme.blackFillBinding(), -1, -1));
|
||||
JFXUtilities.runInFX(() -> FXUtils.installTooltip(btnUpgrade, i18n("version.update")));
|
||||
JFXUtilities.runInFX(() -> FXUtils.installFastTooltip(btnUpgrade, i18n("version.update")));
|
||||
right.getChildren().add(btnUpgrade);
|
||||
}
|
||||
|
||||
|
||||
@@ -47,7 +47,7 @@ public final class ModItem extends BorderPane {
|
||||
|
||||
JFXButton btnRemove = new JFXButton();
|
||||
JFXUtilities.runInFX(() -> {
|
||||
FXUtils.installTooltip(btnRemove, i18n("mods.remove"));
|
||||
FXUtils.installFastTooltip(btnRemove, i18n("mods.remove"));
|
||||
});
|
||||
btnRemove.setOnMouseClicked(e -> deleteCallback.accept(this));
|
||||
btnRemove.getStyleClass().add("toggle-icon4");
|
||||
|
||||
@@ -94,20 +94,18 @@ public final class VersionPage extends StackPane implements DecoratorPage {
|
||||
PopupMenu managementList = new PopupMenu();
|
||||
managementPopup = new JFXPopup(managementList);
|
||||
managementList.getContent().setAll(
|
||||
new IconedMenuItem(null, i18n("version.manage.rename"), FXUtils.withJFXPopupClosing(() -> Versions.renameVersion(profile, version), managementPopup)),
|
||||
new IconedMenuItem(null, i18n("version.manage.remove"), FXUtils.withJFXPopupClosing(() -> Versions.deleteVersion(profile, version), managementPopup)),
|
||||
new IconedMenuItem(null, i18n("version.manage.redownload_assets_index"), FXUtils.withJFXPopupClosing(() -> Versions.updateGameAssets(profile, version), managementPopup)),
|
||||
new IconedMenuItem(null, i18n("version.manage.remove_libraries"), FXUtils.withJFXPopupClosing(() -> FileUtils.deleteDirectoryQuietly(new File(profile.getRepository().getBaseDirectory(), "libraries")), managementPopup)),
|
||||
new IconedMenuItem(null, i18n("version.manage.clean"), FXUtils.withJFXPopupClosing(() -> Versions.cleanVersion(profile, version), managementPopup)).addTooltip(i18n("version.manage.clean.tooltip"))
|
||||
);
|
||||
|
||||
FXUtils.installTooltip(btnDelete, i18n("version.manage.remove"));
|
||||
FXUtils.installTooltip(btnBrowseMenu, i18n("settings.game.exploration"));
|
||||
FXUtils.installTooltip(btnManagementMenu, i18n("settings.game.management"));
|
||||
FXUtils.installTooltip(btnExport, i18n("modpack.export"));
|
||||
FXUtils.installFastTooltip(btnDelete, i18n("version.manage.remove"));
|
||||
FXUtils.installFastTooltip(btnBrowseMenu, i18n("settings.game.exploration"));
|
||||
FXUtils.installFastTooltip(btnManagementMenu, i18n("settings.game.management"));
|
||||
FXUtils.installFastTooltip(btnExport, i18n("modpack.export"));
|
||||
|
||||
btnTestGame.setGraphic(SVG.launch(Theme.whiteFillBinding(), 20, 20));
|
||||
FXUtils.installTooltip(btnTestGame, i18n("version.launch.test"));
|
||||
FXUtils.installFastTooltip(btnTestGame, i18n("version.launch.test"));
|
||||
|
||||
setEventHandler(Navigator.NavigationEvent.NAVIGATED, this::onNavigated);
|
||||
}
|
||||
|
||||
@@ -35,6 +35,7 @@ import javafx.scene.layout.StackPane;
|
||||
import javafx.scene.layout.VBox;
|
||||
import javafx.stage.FileChooser;
|
||||
import org.jackhuang.hmcl.setting.EnumGameDirectory;
|
||||
import org.jackhuang.hmcl.setting.LauncherVisibility;
|
||||
import org.jackhuang.hmcl.setting.Profile;
|
||||
import org.jackhuang.hmcl.setting.Profiles;
|
||||
import org.jackhuang.hmcl.setting.VersionSetting;
|
||||
@@ -60,6 +61,7 @@ import java.util.List;
|
||||
import java.util.logging.Level;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import static org.jackhuang.hmcl.ui.FXUtils.stringConverter;
|
||||
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
|
||||
|
||||
public final class VersionSettingsPage extends StackPane implements DecoratorPage {
|
||||
@@ -85,7 +87,7 @@ public final class VersionSettingsPage extends StackPane implements DecoratorPag
|
||||
@FXML private ComponentList advancedSettingsPane;
|
||||
@FXML private ComponentList componentList;
|
||||
@FXML private ComponentList iconPickerItemWrapper;
|
||||
@FXML private JFXComboBox<?> cboLauncherVisibility;
|
||||
@FXML private JFXComboBox<LauncherVisibility> cboLauncherVisibility;
|
||||
@FXML private JFXCheckBox chkFullscreen;
|
||||
@FXML private Label lblPhysicalMemory;
|
||||
@FXML private JFXToggleButton chkNoJVMArgs;
|
||||
@@ -100,6 +102,9 @@ public final class VersionSettingsPage extends StackPane implements DecoratorPag
|
||||
|
||||
public VersionSettingsPage() {
|
||||
FXUtils.loadFXML(this, "/assets/fxml/version/version-settings.fxml");
|
||||
|
||||
cboLauncherVisibility.getItems().setAll(LauncherVisibility.values());
|
||||
cboLauncherVisibility.setConverter(stringConverter(e -> i18n("settings.advanced.launcher_visibility." + e.name().toLowerCase())));
|
||||
}
|
||||
|
||||
@FXML
|
||||
|
||||
@@ -50,8 +50,13 @@ public class WorldListPage extends ListPage<WorldListItem> {
|
||||
public void loadVersion(Profile profile, String id) {
|
||||
this.savesDir = profile.getRepository().getRunDirectory(id).toPath().resolve("saves");
|
||||
|
||||
itemsProperty().setAll(World.getWorlds(savesDir).stream()
|
||||
.map(WorldListItem::new).collect(Collectors.toList()));
|
||||
setLoading(true);
|
||||
Task.ofResult(() -> World.getWorlds(savesDir).parallel().collect(Collectors.toList()))
|
||||
.finalizedResult(Schedulers.javafx(), (result, isDependentsSucceeded) -> {
|
||||
setLoading(false);
|
||||
if (isDependentsSucceeded)
|
||||
itemsProperty().setAll(result.stream().map(WorldListItem::new).collect(Collectors.toList()));
|
||||
}).start();
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -65,28 +70,26 @@ public class WorldListPage extends ListPage<WorldListItem> {
|
||||
installWorld(res.get(0));
|
||||
}
|
||||
|
||||
public void installWorld(File zipFile) {
|
||||
try {
|
||||
// Only accept one world file because user is required to confirm the new world name
|
||||
// Or too many input dialogs are popped.
|
||||
World world = new World(zipFile.toPath());
|
||||
|
||||
Controllers.inputDialog(i18n("world.name.enter"), (name, resolve, reject) -> {
|
||||
Task.of(() -> world.install(savesDir, name))
|
||||
.finalized(Schedulers.javafx(), var -> {
|
||||
itemsProperty().add(new WorldListItem(new World(savesDir.resolve(name))));
|
||||
resolve.run();
|
||||
}, e -> {
|
||||
if (e instanceof FileAlreadyExistsException)
|
||||
reject.accept(i18n("world.import.failed", i18n("world.import.already_exists")));
|
||||
else
|
||||
reject.accept(i18n("world.import.failed", e.getClass().getName() + ": " + e.getLocalizedMessage()));
|
||||
}).start();
|
||||
}).setInitialText(world.getWorldName());
|
||||
|
||||
} catch (IOException | IllegalArgumentException e) {
|
||||
Logging.LOG.log(Level.WARNING, "Unable to parse world file " + zipFile, e);
|
||||
Controllers.dialog(i18n("world.import.invalid"));
|
||||
}
|
||||
private void installWorld(File zipFile) {
|
||||
// Only accept one world file because user is required to confirm the new world name
|
||||
// Or too many input dialogs are popped.
|
||||
Task.ofResult(() -> new World(zipFile.toPath()))
|
||||
.finalizedResult(Schedulers.javafx(), world -> {
|
||||
Controllers.inputDialog(i18n("world.name.enter"), (name, resolve, reject) -> {
|
||||
Task.of(() -> world.install(savesDir, name))
|
||||
.finalized(Schedulers.javafx(), var -> {
|
||||
itemsProperty().add(new WorldListItem(new World(savesDir.resolve(name))));
|
||||
resolve.run();
|
||||
}, e -> {
|
||||
if (e instanceof FileAlreadyExistsException)
|
||||
reject.accept(i18n("world.import.failed", i18n("world.import.already_exists")));
|
||||
else
|
||||
reject.accept(i18n("world.import.failed", e.getClass().getName() + ": " + e.getLocalizedMessage()));
|
||||
}).start();
|
||||
}).setInitialText(world.getWorldName());
|
||||
}, e -> {
|
||||
Logging.LOG.log(Level.WARNING, "Unable to parse world file " + zipFile, e);
|
||||
Controllers.dialog(i18n("world.import.invalid"));
|
||||
}).start();
|
||||
}
|
||||
}
|
||||
@@ -104,20 +104,24 @@
|
||||
-fx-padding: 4 0 4 0;
|
||||
}
|
||||
|
||||
.two-line-list-item > .title {
|
||||
-fx-text-fill: black;
|
||||
-fx-font-size: 15px;
|
||||
}
|
||||
|
||||
.two-line-list-item > .subtitle {
|
||||
-fx-text-fill: gray;
|
||||
}
|
||||
|
||||
.bubble {
|
||||
-fx-background-color: gray;
|
||||
-fx-background-radius: 2px;
|
||||
-fx-text-fill: white;
|
||||
}
|
||||
|
||||
.bubble .two-line-list-item {
|
||||
-jfx-title-fill: white;
|
||||
-jfx-subtitle-fill: white;
|
||||
}
|
||||
|
||||
.two-line-list-item {
|
||||
-jfx-title-font-size: 15px;
|
||||
-jfx-title-fill: black;
|
||||
-jfx-subtitle-fill: gray;
|
||||
.bubble .two-line-list-item > .title,
|
||||
.bubble .two-line-list-item > .subtitle {
|
||||
-fx-text-fill: white;
|
||||
}
|
||||
|
||||
.window-title-bar .separator {
|
||||
@@ -678,20 +682,12 @@
|
||||
* *
|
||||
*******************************************************************************/
|
||||
|
||||
.jfx-list-cell:odd,
|
||||
.jfx-list-cell:even,
|
||||
.list-cell:odd,
|
||||
.list-cell:even {
|
||||
.jfx-list-cell, .list-cell {
|
||||
-fx-background-color: WHITE;
|
||||
}
|
||||
|
||||
.list-cell:selected, .jfx-list-cell:selected {
|
||||
-fx-background-insets: 0.0;
|
||||
-fx-text-fill: BLACK;
|
||||
}
|
||||
|
||||
.jfx-list-cell:filled:hover,
|
||||
.jfx-list-cell:selected .label {
|
||||
.list-cell:selected, .jfx-list-cell:selected,
|
||||
.list-cell:hover, .jfx-list-cell:hover {
|
||||
-fx-text-fill: black;
|
||||
}
|
||||
|
||||
@@ -1109,31 +1105,10 @@
|
||||
-fx-fill: #D34336;
|
||||
}
|
||||
|
||||
.combo-box-popup .list-view .jfx-list-cell .label,
|
||||
.combo-box-popup .list-view .jfx-list-cell:filled:hover .label {
|
||||
-fx-text-fill: BLACK;
|
||||
}
|
||||
|
||||
.combo-box-popup .list-view .jfx-list-cell .custom-jfx-list-view-icon,
|
||||
.combo-box-popup .list-view .jfx-list-cell:filled:hover .custom-jfx-list-view-icon,
|
||||
.combo-box-popup .list-view .jfx-list-cell:selected .custom-jfx-list-view-icon {
|
||||
-fx-fill: -fx-base-color;
|
||||
}
|
||||
|
||||
.combo-box-popup .list-view .jfx-list-cell:odd:selected > .jfx-rippler > StackPane,
|
||||
.combo-box-popup .list-view .jfx-list-cell:even:selected > .jfx-rippler > StackPane {
|
||||
-fx-background-color: rgba(0.0, 0.0, 255.0, 0.2);
|
||||
}
|
||||
|
||||
.combo-box-popup .list-view .jfx-list-cell {
|
||||
-fx-background-insets: 0.0;
|
||||
}
|
||||
|
||||
.combo-box-popup .list-view .jfx-list-cell:odd,
|
||||
.combo-box-popup .list-view .jfx-list-cell:even {
|
||||
-fx-background-color: WHITE;
|
||||
}
|
||||
|
||||
.combo-box-popup .list-view .jfx-list-cell .jfx-rippler {
|
||||
-jfx-rippler-fill: -fx-base-color;
|
||||
}
|
||||
|
||||
@@ -32,6 +32,7 @@
|
||||
<JFXComboBox fx:id="cboServers" promptText="%account.injector.empty" maxHeight="25" GridPane.columnIndex="1" GridPane.rowIndex="1"/>
|
||||
|
||||
<HBox GridPane.columnIndex="2" GridPane.rowIndex="1" spacing="8">
|
||||
<HBox fx:id="linksContainer" alignment="CENTER_LEFT"/>
|
||||
<JFXButton fx:id="btnAddServer" styleClass="toggle-icon4" onMouseClicked="#onAddInjecterServer">
|
||||
<graphic>
|
||||
<javafx.scene.shape.SVGPath content="M19,13H13V19H11V13H5V11H11V5H13V11H19V13Z" />
|
||||
|
||||
@@ -60,16 +60,7 @@
|
||||
<Label text="%settings.advanced.launcher_visible" BorderPane.alignment="CENTER_LEFT"/>
|
||||
</left>
|
||||
<right>
|
||||
<JFXComboBox fx:id="cboLauncherVisibility" BorderPane.alignment="CENTER_RIGHT" FXUtils.limitWidth="300">
|
||||
<items>
|
||||
<FXCollections fx:factory="observableArrayList">
|
||||
<Label text="%settings.advanced.launcher_visibility.close"/>
|
||||
<Label text="%settings.advanced.launcher_visibility.hide"/>
|
||||
<Label text="%settings.advanced.launcher_visibility.keep"/>
|
||||
<Label text="%settings.advanced.launcher_visibility.hide_reopen"/>
|
||||
</FXCollections>
|
||||
</items>
|
||||
</JFXComboBox>
|
||||
<JFXComboBox fx:id="cboLauncherVisibility" BorderPane.alignment="CENTER_RIGHT" FXUtils.limitWidth="300" />
|
||||
</right>
|
||||
</BorderPane>
|
||||
|
||||
|
||||
@@ -36,6 +36,7 @@ account.character=character
|
||||
account.choose=Choose a character
|
||||
account.create=Create a new account
|
||||
account.email=Email
|
||||
account.failed.character_deleted=The character has been deleted.
|
||||
account.failed.connect_authentication_server=Cannot connect to the authentication server. Check your network.
|
||||
account.failed.connect_injector_server=Cannot connect to the authentication server. Check your network and ensure the URL is correct.
|
||||
account.failed.injector_download_failure=Failed to download authlib-injector. Check your network and try switching to another download source.
|
||||
@@ -49,6 +50,7 @@ account.injector.empty=Empty (Click the plus button right to add)
|
||||
account.injector.manage=Manage authentication servers
|
||||
account.injector.manage.title=Authentication servers
|
||||
account.injector.http=Warning: This server is using HTTP, which will cause your password be transmitted in clear text.
|
||||
account.injector.link.register=Register
|
||||
account.injector.server=Auth Server
|
||||
account.injector.server_url=Server URL
|
||||
account.injector.server_name=Server Name
|
||||
@@ -144,7 +146,7 @@ lang.default=Use system language
|
||||
launch.advice.corrected=We have already corrected JVM selection and game can be launched now. If you do want to keep your JVM choice, you can turn off the Java VM checker in game settings.
|
||||
launch.advice.uncorrected=If you are sure that game can be launched normally, you can turn off the Java VM checker in game settings.
|
||||
launch.advice.different_platform=Your OS is 64-Bit but your Java is 32-Bit. The 64-Bit Java is recommended.
|
||||
launch.advice.forge2760_liteloader=Forge 2760 and above is not compatible with LiteLoader, please consider downgrade Forge to 2759 or earlier.
|
||||
launch.advice.forge2760_liteloader=Forge 2760 and above is not compatible with LiteLoader, please consider upgrade Forge to 2773 or later.
|
||||
launch.advice.java8_1_13=Minecraft 1.13 and later versions can only run on Java 8 or later.
|
||||
launch.advice.java8_51_1_13=Minecraft 1.13 may crash on Java 8 earlier than 1.8.0_51. You are supposed to install the latest version of Java 8.
|
||||
launch.advice.java9=You cannot launch Minecraft until game version is higher than 1.13 with Java 9 or later Java versions.
|
||||
@@ -307,7 +309,7 @@ settings.advanced.java_permanent_generation_space=PermGen Space/MB
|
||||
settings.advanced.jvm_args=Java VM Arguments
|
||||
settings.advanced.launcher_visibility.close=Close the launcher when the game launched.
|
||||
settings.advanced.launcher_visibility.hide=Hide the launcher when the game launched.
|
||||
settings.advanced.launcher_visibility.hide_reopen=Hide the launcher and re-open when game closes.
|
||||
settings.advanced.launcher_visibility.hide_and_reopen=Hide the launcher and re-open when game closes.
|
||||
settings.advanced.launcher_visibility.keep=Keep the launcher visible.
|
||||
settings.advanced.launcher_visible=Launcher Visibility
|
||||
settings.advanced.minecraft_arguments=Minecraft Arguments
|
||||
|
||||
@@ -35,6 +35,7 @@ account.character=角色
|
||||
account.choose=選擇一個角色
|
||||
account.create=建立帳戶
|
||||
account.email=電子信箱
|
||||
account.failed.character_deleted=此角色已被刪除
|
||||
account.failed.connect_authentication_server=無法連接認證伺服器,可能是網路問題
|
||||
account.failed.connect_injector_server=無法連接認證伺服器,可能是網路故障或 URL 輸入錯誤
|
||||
account.failed.injector_download_failure=無法下載 authlib-injector,請檢查網路或嘗試切換下載源
|
||||
@@ -48,6 +49,7 @@ account.injector.empty=無(點擊右側加號添加)
|
||||
account.injector.manage=管理認證伺服器
|
||||
account.injector.manage.title=認證伺服器
|
||||
account.injector.http=警告:此伺服器使用不安全的 HTTP 協議,您的密碼在登入時會被明文傳輸。
|
||||
account.injector.link.register=註冊
|
||||
account.injector.server=認證伺服器
|
||||
account.injector.server_url=伺服器位址
|
||||
account.injector.server_name=伺服器名稱
|
||||
@@ -143,7 +145,7 @@ lang.default=跟隨系統語言
|
||||
launch.advice.corrected=我们已经修正了问题。如果您確實希望使用您自訂的 Java 虛擬機,您可以在遊戲設定中關閉 Java 虛擬機相容性檢查。
|
||||
launch.advice.uncorrected=如果您確實希望使用您自訂的 Java 虛擬機,您可以在遊戲設定中關閉 Java 虛擬機相容性檢查。
|
||||
launch.advice.different_platform=您的系統為 64 位元,但是 Java 是 32 位元,建議您安裝 64 位 Java。
|
||||
launch.advice.forge2760_liteloader=Forge 2760 與 LiteLoader 不相容。請刪除 LiteLoader 或者更換 Forge 至 2759 或更舊的版本。
|
||||
launch.advice.forge2760_liteloader=Forge 2760 與 LiteLoader 不相容。請刪除 LiteLoader 或者更換 Forge 至 2773 或更新的版本。
|
||||
launch.advice.java8_1_13=Minecraft 1.13 只支援 Java 8 或更高版本,請使用 Java 8 或最新版本。
|
||||
launch.advice.java8_51_1_13=低於 1.8.0_51 的 Java 版本可能會導致 Minecraft 1.13 崩潰。建議您到 https://java.com 安裝最新版的 Java 8。
|
||||
launch.advice.java9=低於 (包含) 1.13 的有安裝 Mod Minecraft 版本 不支援 Java 9 或更高版本,請使用 Java 8。
|
||||
@@ -306,7 +308,7 @@ settings.advanced.java_permanent_generation_space=記憶體永久儲存區域(
|
||||
settings.advanced.jvm_args=Java 虛擬機參數(不必填寫)
|
||||
settings.advanced.launcher_visibility.close=遊戲啟動後結束啟動器
|
||||
settings.advanced.launcher_visibility.hide=遊戲啟動後隱藏啟動器
|
||||
settings.advanced.launcher_visibility.hide_reopen=隱藏啟動器並在遊戲結束後重新開啟
|
||||
settings.advanced.launcher_visibility.hide_and_reopen=隱藏啟動器並在遊戲結束後重新開啟
|
||||
settings.advanced.launcher_visibility.keep=不隱藏啟動器
|
||||
settings.advanced.launcher_visible=啟動器可見性
|
||||
settings.advanced.minecraft_arguments=Minecraft 額外參數(不必填寫)
|
||||
|
||||
@@ -35,6 +35,7 @@ account.character=角色
|
||||
account.choose=选择一个角色
|
||||
account.create=新建账户
|
||||
account.email=邮箱
|
||||
account.failed.character_deleted=此角色已被删除
|
||||
account.failed.connect_authentication_server=无法连接认证服务器,可能是网络问题
|
||||
account.failed.connect_injector_server=无法连接认证服务器,可能是网络故障或 URL 输入错误
|
||||
account.failed.injector_download_failure=无法下载 authlib-injector,请检查网络或尝试切换下载源
|
||||
@@ -48,6 +49,7 @@ account.injector.empty=无(点击右侧加号添加)
|
||||
account.injector.manage=管理认证服务器
|
||||
account.injector.manage.title=认证服务器
|
||||
account.injector.http=警告:此服务器使用不安全的 HTTP 协议,您的密码在登录时会被明文传输。
|
||||
account.injector.link.register=注册
|
||||
account.injector.server=认证服务器
|
||||
account.injector.server_url=服务器地址
|
||||
account.injector.server_name=服务器名称
|
||||
@@ -143,7 +145,7 @@ lang.default=跟随系统语言
|
||||
launch.advice.corrected=我们已经修复了问题。如果您确实希望使用您自定义的 Java 虚拟机,您可以在游戏设置中关闭 Java 虚拟机兼容性检查。
|
||||
launch.advice.uncorrected=如果您确实希望使用您自定义的 Java 虚拟机,您可以在游戏设置中关闭 Java 虚拟机兼容性检查。
|
||||
launch.advice.different_platform=您的系统是 64 位,但是 Java 是 32 位的,建议您安装 64 位 Java。
|
||||
launch.advice.forge2760_liteloader=Forge 2760 与 LiteLoader 不兼容。请删除 LiteLoader 或者更换 Forge 至 2759 或更旧的版本。
|
||||
launch.advice.forge2760_liteloader=Forge 2760 与 LiteLoader 不兼容。请删除 LiteLoader 或者更换 Forge 至 2773 或更新的版本。
|
||||
launch.advice.java8_1_13=Minecraft 1.13 只支持 Java 8 或更高版本,请使用 Java 8 或最新版本。
|
||||
launch.advice.java8_51_1_13=低于 1.8.0_51 的 Java 版本可能会导致 Minecraft 1.13 崩溃。建议您到 https://java.com 安装最新版的 Java 8。
|
||||
launch.advice.java9=低于 1.13 的有安装 Mod 的 Minecraft 版本不支持 Java 9 或更高版本,请使用 Java 8。
|
||||
@@ -306,7 +308,7 @@ settings.advanced.java_permanent_generation_space=内存永久保存区域(不
|
||||
settings.advanced.jvm_args=Java 虚拟机参数(不必填写)
|
||||
settings.advanced.launcher_visibility.close=游戏启动后结束启动器
|
||||
settings.advanced.launcher_visibility.hide=游戏启动后隐藏启动器
|
||||
settings.advanced.launcher_visibility.hide_reopen=隐藏启动器并在游戏结束后重新打开
|
||||
settings.advanced.launcher_visibility.hide_and_reopen=隐藏启动器并在游戏结束后重新打开
|
||||
settings.advanced.launcher_visibility.keep=保持启动器可见
|
||||
settings.advanced.launcher_visible=启动器可见性
|
||||
settings.advanced.minecraft_arguments=Minecraft 额外参数(不必填写)
|
||||
|
||||
@@ -69,7 +69,8 @@ public abstract class Account implements Observable {
|
||||
|
||||
public abstract Map<Object, Object> toStorage();
|
||||
|
||||
public abstract void clearCache();
|
||||
public void clearCache() {
|
||||
}
|
||||
|
||||
private ObservableHelper helper = new ObservableHelper(this);
|
||||
|
||||
|
||||
+3
-20
@@ -17,27 +17,10 @@
|
||||
*/
|
||||
package org.jackhuang.hmcl.auth;
|
||||
|
||||
import org.jackhuang.hmcl.auth.yggdrasil.GameProfile;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
|
||||
/**
|
||||
* Select character by name.
|
||||
* Thrown when a previously existing character cannot be found.
|
||||
*/
|
||||
public class SpecificCharacterSelector implements CharacterSelector {
|
||||
private UUID uuid;
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
* @param uuid character's uuid.
|
||||
*/
|
||||
public SpecificCharacterSelector(UUID uuid) {
|
||||
this.uuid = uuid;
|
||||
}
|
||||
|
||||
@Override
|
||||
public GameProfile select(Account account, List<GameProfile> names) throws NoSelectedCharacterException {
|
||||
return names.stream().filter(profile -> profile.getId().equals(uuid)).findAny().orElseThrow(() -> new NoSelectedCharacterException(account));
|
||||
public final class CharacterDeletedException extends AuthenticationException {
|
||||
public CharacterDeletedException() {
|
||||
}
|
||||
}
|
||||
@@ -18,6 +18,7 @@
|
||||
package org.jackhuang.hmcl.auth;
|
||||
|
||||
import org.jackhuang.hmcl.auth.yggdrasil.GameProfile;
|
||||
import org.jackhuang.hmcl.auth.yggdrasil.YggdrasilService;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@@ -33,7 +34,6 @@ public interface CharacterSelector {
|
||||
* @throws NoSelectedCharacterException if cannot select any character may because user close the selection window or cancel the selection.
|
||||
* @return your choice of game profile.
|
||||
*/
|
||||
GameProfile select(Account account, List<GameProfile> names) throws NoSelectedCharacterException;
|
||||
GameProfile select(YggdrasilService yggdrasilService, List<GameProfile> names) throws NoSelectedCharacterException;
|
||||
|
||||
CharacterSelector DEFAULT = (account, names) -> names.stream().findFirst().orElseThrow(() -> new NoSelectedCharacterException(account));
|
||||
}
|
||||
@@ -22,13 +22,6 @@ package org.jackhuang.hmcl.auth;
|
||||
* (A account may hold more than one characters.)
|
||||
*/
|
||||
public final class NoCharacterException extends AuthenticationException {
|
||||
private final Account account;
|
||||
|
||||
public NoCharacterException(Account account) {
|
||||
this.account = account;
|
||||
}
|
||||
|
||||
public Account getAccount() {
|
||||
return account;
|
||||
public NoCharacterException() {
|
||||
}
|
||||
}
|
||||
@@ -25,17 +25,6 @@ package org.jackhuang.hmcl.auth;
|
||||
* @author huangyuhui
|
||||
*/
|
||||
public final class NoSelectedCharacterException extends AuthenticationException {
|
||||
private final Account account;
|
||||
|
||||
/**
|
||||
*
|
||||
* @param account the error yggdrasil account.
|
||||
*/
|
||||
public NoSelectedCharacterException(Account account) {
|
||||
this.account = account;
|
||||
}
|
||||
|
||||
public Account getAccount() {
|
||||
return account;
|
||||
public NoSelectedCharacterException() {
|
||||
}
|
||||
}
|
||||
+18
-8
@@ -22,7 +22,6 @@ import org.jackhuang.hmcl.auth.AuthenticationException;
|
||||
import org.jackhuang.hmcl.auth.CharacterSelector;
|
||||
import org.jackhuang.hmcl.auth.ServerDisconnectException;
|
||||
import org.jackhuang.hmcl.auth.yggdrasil.YggdrasilAccount;
|
||||
import org.jackhuang.hmcl.auth.yggdrasil.YggdrasilService;
|
||||
import org.jackhuang.hmcl.auth.yggdrasil.YggdrasilSession;
|
||||
import org.jackhuang.hmcl.game.Arguments;
|
||||
import org.jackhuang.hmcl.util.ToStringBuilder;
|
||||
@@ -36,14 +35,19 @@ import java.util.concurrent.ExecutionException;
|
||||
import static java.nio.charset.StandardCharsets.UTF_8;
|
||||
|
||||
public class AuthlibInjectorAccount extends YggdrasilAccount {
|
||||
private AuthlibInjectorServer server;
|
||||
private final AuthlibInjectorServer server;
|
||||
private AuthlibInjectorArtifactProvider downloader;
|
||||
|
||||
protected AuthlibInjectorAccount(YggdrasilService service, AuthlibInjectorServer server, AuthlibInjectorArtifactProvider downloader, String username, UUID characterUUID, YggdrasilSession session) {
|
||||
super(service, username, characterUUID, session);
|
||||
|
||||
this.downloader = downloader;
|
||||
public AuthlibInjectorAccount(AuthlibInjectorServer server, AuthlibInjectorArtifactProvider downloader, String username, String password, CharacterSelector selector) throws AuthenticationException {
|
||||
super(server.getYggdrasilService(), username, password, selector);
|
||||
this.server = server;
|
||||
this.downloader = downloader;
|
||||
}
|
||||
|
||||
public AuthlibInjectorAccount(AuthlibInjectorServer server, AuthlibInjectorArtifactProvider downloader, String username, YggdrasilSession session) {
|
||||
super(server.getYggdrasilService(), username, session);
|
||||
this.server = server;
|
||||
this.downloader = downloader;
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -52,8 +56,8 @@ public class AuthlibInjectorAccount extends YggdrasilAccount {
|
||||
}
|
||||
|
||||
@Override
|
||||
protected AuthInfo logInWithPassword(String password, CharacterSelector selector) throws AuthenticationException {
|
||||
return inject(() -> super.logInWithPassword(password, selector));
|
||||
public synchronized AuthInfo logInWithPassword(String password) throws AuthenticationException {
|
||||
return inject(() -> super.logInWithPassword(password));
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -121,6 +125,12 @@ public class AuthlibInjectorAccount extends YggdrasilAccount {
|
||||
return map;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void clearCache() {
|
||||
super.clearCache();
|
||||
server.invalidateMetadataCache();
|
||||
}
|
||||
|
||||
public AuthlibInjectorServer getServer() {
|
||||
return server;
|
||||
}
|
||||
|
||||
+12
-7
@@ -20,7 +20,8 @@ package org.jackhuang.hmcl.auth.authlibinjector;
|
||||
import org.jackhuang.hmcl.auth.AccountFactory;
|
||||
import org.jackhuang.hmcl.auth.AuthenticationException;
|
||||
import org.jackhuang.hmcl.auth.CharacterSelector;
|
||||
import org.jackhuang.hmcl.auth.yggdrasil.YggdrasilService;
|
||||
import org.jackhuang.hmcl.auth.yggdrasil.CompleteGameProfile;
|
||||
import org.jackhuang.hmcl.auth.yggdrasil.GameProfile;
|
||||
import org.jackhuang.hmcl.auth.yggdrasil.YggdrasilSession;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
@@ -48,10 +49,7 @@ public class AuthlibInjectorAccountFactory extends AccountFactory<AuthlibInjecto
|
||||
|
||||
AuthlibInjectorServer server = (AuthlibInjectorServer) additionalData;
|
||||
|
||||
AuthlibInjectorAccount account = new AuthlibInjectorAccount(new YggdrasilService(new AuthlibInjectorProvider(server.getUrl())),
|
||||
server, downloader, username, null, null);
|
||||
account.logInWithPassword(password, selector);
|
||||
return account;
|
||||
return new AuthlibInjectorAccount(server, downloader, username, password, selector);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -67,7 +65,14 @@ public class AuthlibInjectorAccountFactory extends AccountFactory<AuthlibInjecto
|
||||
|
||||
AuthlibInjectorServer server = serverLookup.apply(apiRoot);
|
||||
|
||||
return new AuthlibInjectorAccount(new YggdrasilService(new AuthlibInjectorProvider(server.getUrl())),
|
||||
server, downloader, username, session.getSelectedProfile().getId(), session);
|
||||
tryCast(storage.get("profileProperties"), Map.class).ifPresent(
|
||||
it -> {
|
||||
@SuppressWarnings("unchecked")
|
||||
Map<String, String> properties = it;
|
||||
GameProfile selected = session.getSelectedProfile();
|
||||
server.getYggdrasilService().getProfileRepository().put(selected.getId(), new CompleteGameProfile(selected, properties));
|
||||
});
|
||||
|
||||
return new AuthlibInjectorAccount(server, downloader, username, session);
|
||||
}
|
||||
}
|
||||
+5
@@ -56,4 +56,9 @@ public class AuthlibInjectorProvider implements YggdrasilProvider {
|
||||
public URL getProfilePropertiesURL(UUID uuid) {
|
||||
return NetworkUtils.toURL(apiRoot + "sessionserver/session/minecraft/profile/" + UUIDTypeAdapter.fromUUID(uuid));
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return apiRoot;
|
||||
}
|
||||
}
|
||||
+30
-1
@@ -18,6 +18,7 @@
|
||||
package org.jackhuang.hmcl.auth.authlibinjector;
|
||||
|
||||
import static java.nio.charset.StandardCharsets.UTF_8;
|
||||
import static java.util.Collections.emptyMap;
|
||||
import static org.jackhuang.hmcl.util.Lang.tryCast;
|
||||
import static org.jackhuang.hmcl.util.Logging.LOG;
|
||||
import static org.jackhuang.hmcl.util.io.IOUtils.readFullyAsByteArray;
|
||||
@@ -29,9 +30,12 @@ import java.net.HttpURLConnection;
|
||||
import java.net.MalformedURLException;
|
||||
import java.net.URL;
|
||||
import java.net.URLConnection;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.logging.Level;
|
||||
|
||||
import org.jackhuang.hmcl.auth.yggdrasil.YggdrasilService;
|
||||
import org.jackhuang.hmcl.util.javafx.ObservableHelper;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
@@ -137,18 +141,25 @@ public class AuthlibInjectorServer implements Observable {
|
||||
|
||||
@Nullable
|
||||
private transient String name;
|
||||
private transient Map<String, String> links = emptyMap();
|
||||
|
||||
private transient boolean metadataRefreshed;
|
||||
private transient ObservableHelper helper = new ObservableHelper(this);
|
||||
private final transient ObservableHelper helper = new ObservableHelper(this);
|
||||
private final transient YggdrasilService yggdrasilService;
|
||||
|
||||
public AuthlibInjectorServer(String url) {
|
||||
this.url = url;
|
||||
this.yggdrasilService = new YggdrasilService(new AuthlibInjectorProvider(url));
|
||||
}
|
||||
|
||||
public String getUrl() {
|
||||
return url;
|
||||
}
|
||||
|
||||
public YggdrasilService getYggdrasilService() {
|
||||
return yggdrasilService;
|
||||
}
|
||||
|
||||
public Optional<String> getMetadataResponse() {
|
||||
return Optional.ofNullable(metadataResponse);
|
||||
}
|
||||
@@ -162,6 +173,10 @@ public class AuthlibInjectorServer implements Observable {
|
||||
.orElse(url);
|
||||
}
|
||||
|
||||
public Map<String, String> getLinks() {
|
||||
return links;
|
||||
}
|
||||
|
||||
public String fetchMetadataResponse() throws IOException {
|
||||
if (metadataResponse == null || !metadataRefreshed) {
|
||||
refreshMetadata();
|
||||
@@ -201,9 +216,23 @@ public class AuthlibInjectorServer implements Observable {
|
||||
|
||||
this.name = metaObject.flatMap(meta -> tryCast(meta.get("serverName"), JsonPrimitive.class).map(JsonPrimitive::getAsString))
|
||||
.orElse(null);
|
||||
this.links = metaObject.flatMap(meta -> tryCast(meta.get("links"), JsonObject.class))
|
||||
.map(linksObject -> {
|
||||
Map<String, String> converted = new LinkedHashMap<>();
|
||||
linksObject.entrySet().forEach(
|
||||
entry -> tryCast(entry.getValue(), JsonPrimitive.class).ifPresent(element -> {
|
||||
converted.put(entry.getKey(), element.getAsString());
|
||||
}));
|
||||
return converted;
|
||||
})
|
||||
.orElse(emptyMap());
|
||||
}
|
||||
}
|
||||
|
||||
public void invalidateMetadataCache() {
|
||||
metadataRefreshed = false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return url.hashCode();
|
||||
|
||||
@@ -25,10 +25,10 @@ import org.jackhuang.hmcl.util.ToStringBuilder;
|
||||
import org.jackhuang.hmcl.util.gson.UUIDTypeAdapter;
|
||||
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.Optional;
|
||||
import java.util.UUID;
|
||||
|
||||
import static java.util.Objects.requireNonNull;
|
||||
import static org.jackhuang.hmcl.util.Lang.mapOf;
|
||||
import static org.jackhuang.hmcl.util.Pair.pair;
|
||||
|
||||
@@ -41,15 +41,13 @@ public class OfflineAccount extends Account {
|
||||
private final String username;
|
||||
private final UUID uuid;
|
||||
|
||||
OfflineAccount(String username, UUID uuid) {
|
||||
Objects.requireNonNull(username);
|
||||
Objects.requireNonNull(uuid);
|
||||
protected OfflineAccount(String username, UUID uuid) {
|
||||
this.username = requireNonNull(username);
|
||||
this.uuid = requireNonNull(uuid);
|
||||
|
||||
this.username = username;
|
||||
this.uuid = uuid;
|
||||
|
||||
if (StringUtils.isBlank(username))
|
||||
if (StringUtils.isBlank(username)) {
|
||||
throw new IllegalArgumentException("Username cannot be blank");
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -68,10 +66,7 @@ public class OfflineAccount extends Account {
|
||||
}
|
||||
|
||||
@Override
|
||||
public AuthInfo logIn() throws AuthenticationException {
|
||||
if (StringUtils.isBlank(username))
|
||||
throw new AuthenticationException("Username cannot be empty");
|
||||
|
||||
public AuthInfo logIn() {
|
||||
return new AuthInfo(username, uuid, UUIDTypeAdapter.fromUUID(UUID.randomUUID()), "{}");
|
||||
}
|
||||
|
||||
@@ -82,7 +77,7 @@ public class OfflineAccount extends Account {
|
||||
|
||||
@Override
|
||||
public Optional<AuthInfo> playOffline() {
|
||||
return Optional.empty();
|
||||
return Optional.of(logIn());
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -93,11 +88,6 @@ public class OfflineAccount extends Account {
|
||||
);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void clearCache() {
|
||||
// Nothing to clear.
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return new ToStringBuilder(this)
|
||||
|
||||
@@ -0,0 +1,59 @@
|
||||
/*
|
||||
* Hello Minecraft! Launcher
|
||||
* Copyright (C) 2019 huangyuhui <huanghongxun2008@126.com> and contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.jackhuang.hmcl.auth.yggdrasil;
|
||||
|
||||
import static java.util.Objects.requireNonNull;
|
||||
|
||||
import java.util.Map;
|
||||
import java.util.UUID;
|
||||
|
||||
import org.jackhuang.hmcl.util.Immutable;
|
||||
|
||||
import com.google.gson.JsonParseException;
|
||||
import com.google.gson.annotations.JsonAdapter;
|
||||
|
||||
/**
|
||||
* @author yushijinhun
|
||||
*/
|
||||
@Immutable
|
||||
public class CompleteGameProfile extends GameProfile {
|
||||
|
||||
@JsonAdapter(PropertyMapSerializer.class)
|
||||
private final Map<String, String> properties;
|
||||
|
||||
public CompleteGameProfile(UUID id, String name, Map<String, String> properties) {
|
||||
super(id, name);
|
||||
this.properties = requireNonNull(properties);
|
||||
}
|
||||
|
||||
public CompleteGameProfile(GameProfile profile, Map<String, String> properties) {
|
||||
this(profile.getId(), profile.getName(), properties);
|
||||
}
|
||||
|
||||
public Map<String, String> getProperties() {
|
||||
return properties;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void validate() throws JsonParseException {
|
||||
super.validate();
|
||||
|
||||
if (properties == null)
|
||||
throw new JsonParseException("Game profile properties cannot be null");
|
||||
}
|
||||
}
|
||||
@@ -17,36 +17,31 @@
|
||||
*/
|
||||
package org.jackhuang.hmcl.auth.yggdrasil;
|
||||
|
||||
import com.google.gson.JsonParseException;
|
||||
import org.jackhuang.hmcl.util.Immutable;
|
||||
import org.jackhuang.hmcl.util.StringUtils;
|
||||
import org.jackhuang.hmcl.util.gson.Validation;
|
||||
import static java.util.Objects.requireNonNull;
|
||||
|
||||
import java.util.UUID;
|
||||
|
||||
import org.jackhuang.hmcl.util.Immutable;
|
||||
import org.jackhuang.hmcl.util.gson.UUIDTypeAdapter;
|
||||
import org.jackhuang.hmcl.util.gson.Validation;
|
||||
|
||||
import com.google.gson.JsonParseException;
|
||||
import com.google.gson.annotations.JsonAdapter;
|
||||
|
||||
/**
|
||||
*
|
||||
* @author huangyuhui
|
||||
*/
|
||||
@Immutable
|
||||
public final class GameProfile implements Validation {
|
||||
public class GameProfile implements Validation {
|
||||
|
||||
@JsonAdapter(UUIDTypeAdapter.class)
|
||||
private final UUID id;
|
||||
private final String name;
|
||||
private final PropertyMap properties;
|
||||
|
||||
public GameProfile() {
|
||||
this(null, null);
|
||||
}
|
||||
private final String name;
|
||||
|
||||
public GameProfile(UUID id, String name) {
|
||||
this(id, name, new PropertyMap());
|
||||
}
|
||||
|
||||
public GameProfile(UUID id, String name, PropertyMap properties) {
|
||||
this.id = id;
|
||||
this.name = name;
|
||||
this.properties = properties;
|
||||
this.id = requireNonNull(id);
|
||||
this.name = requireNonNull(name);
|
||||
}
|
||||
|
||||
public UUID getId() {
|
||||
@@ -57,18 +52,11 @@ public final class GameProfile implements Validation {
|
||||
return name;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return nullable
|
||||
*/
|
||||
public PropertyMap getProperties() {
|
||||
return properties;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void validate() throws JsonParseException {
|
||||
if (id == null)
|
||||
throw new JsonParseException("Game profile id cannot be null or malformed");
|
||||
if (StringUtils.isBlank(name))
|
||||
throw new JsonParseException("Game profile name cannot be null or blank");
|
||||
throw new JsonParseException("Game profile id cannot be null");
|
||||
if (name == null)
|
||||
throw new JsonParseException("Game profile name cannot be null");
|
||||
}
|
||||
}
|
||||
+5
-1
@@ -24,7 +24,6 @@ import java.net.URL;
|
||||
import java.util.UUID;
|
||||
|
||||
public class MojangYggdrasilProvider implements YggdrasilProvider {
|
||||
public static final MojangYggdrasilProvider INSTANCE = new MojangYggdrasilProvider();
|
||||
|
||||
@Override
|
||||
public URL getAuthenticationURL() {
|
||||
@@ -50,4 +49,9 @@ public class MojangYggdrasilProvider implements YggdrasilProvider {
|
||||
public URL getProfilePropertiesURL(UUID uuid) {
|
||||
return NetworkUtils.toURL("https://sessionserver.mojang.com/session/minecraft/profile/" + UUIDTypeAdapter.fromUUID(uuid));
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "mojang";
|
||||
}
|
||||
}
|
||||
@@ -1,85 +0,0 @@
|
||||
/*
|
||||
* Hello Minecraft! Launcher
|
||||
* Copyright (C) 2019 huangyuhui <huanghongxun2008@126.com> and contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.jackhuang.hmcl.auth.yggdrasil;
|
||||
|
||||
import com.google.gson.*;
|
||||
|
||||
import java.lang.reflect.Type;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
public final class PropertyMap extends HashMap<String, String> {
|
||||
|
||||
public static PropertyMap fromMap(Map<?, ?> map) {
|
||||
PropertyMap propertyMap = new PropertyMap();
|
||||
for (Map.Entry<?, ?> entry : map.entrySet()) {
|
||||
if (entry.getKey() instanceof String && entry.getValue() instanceof String)
|
||||
propertyMap.put((String) entry.getKey(), (String) entry.getValue());
|
||||
}
|
||||
return propertyMap;
|
||||
}
|
||||
|
||||
public static class Serializer implements JsonSerializer<PropertyMap>, JsonDeserializer<PropertyMap> {
|
||||
|
||||
public static final Serializer INSTANCE = new Serializer();
|
||||
|
||||
private Serializer() {
|
||||
}
|
||||
|
||||
@Override
|
||||
public PropertyMap deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws JsonParseException {
|
||||
PropertyMap result = new PropertyMap();
|
||||
for (JsonElement element : json.getAsJsonArray())
|
||||
if (element instanceof JsonObject) {
|
||||
JsonObject object = (JsonObject) element;
|
||||
result.put(object.get("name").getAsString(), object.get("value").getAsString());
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
@Override
|
||||
public JsonElement serialize(PropertyMap src, Type typeOfSrc, JsonSerializationContext context) {
|
||||
JsonArray result = new JsonArray();
|
||||
for (Map.Entry<String, String> entry : src.entrySet()) {
|
||||
JsonObject object = new JsonObject();
|
||||
object.addProperty("name", entry.getKey());
|
||||
object.addProperty("value", entry.getValue());
|
||||
result.add(object);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
public static class LegacySerializer
|
||||
implements JsonSerializer<PropertyMap> {
|
||||
public static final LegacySerializer INSTANCE = new LegacySerializer();
|
||||
|
||||
@Override
|
||||
public JsonElement serialize(PropertyMap src, Type typeOfSrc, JsonSerializationContext context) {
|
||||
JsonObject result = new JsonObject();
|
||||
for (PropertyMap.Entry<String, String> entry : src.entrySet()) {
|
||||
JsonArray values = new JsonArray();
|
||||
values.add(new JsonPrimitive(entry.getValue()));
|
||||
result.add(entry.getKey(), values);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
/*
|
||||
* Hello Minecraft! Launcher
|
||||
* Copyright (C) 2019 huangyuhui <huanghongxun2008@126.com> and contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.jackhuang.hmcl.auth.yggdrasil;
|
||||
|
||||
import static java.util.Collections.unmodifiableMap;
|
||||
|
||||
import java.lang.reflect.Type;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.Map;
|
||||
|
||||
import com.google.gson.JsonArray;
|
||||
import com.google.gson.JsonDeserializationContext;
|
||||
import com.google.gson.JsonDeserializer;
|
||||
import com.google.gson.JsonElement;
|
||||
import com.google.gson.JsonObject;
|
||||
import com.google.gson.JsonParseException;
|
||||
import com.google.gson.JsonSerializationContext;
|
||||
import com.google.gson.JsonSerializer;
|
||||
|
||||
public class PropertyMapSerializer implements JsonSerializer<Map<String, String>>, JsonDeserializer<Map<String, String>> {
|
||||
|
||||
@Override
|
||||
public Map<String, String> deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws JsonParseException {
|
||||
Map<String, String> result = new LinkedHashMap<>();
|
||||
for (JsonElement element : json.getAsJsonArray())
|
||||
if (element instanceof JsonObject) {
|
||||
JsonObject object = (JsonObject) element;
|
||||
result.put(object.get("name").getAsString(), object.get("value").getAsString());
|
||||
}
|
||||
|
||||
return unmodifiableMap(result);
|
||||
}
|
||||
|
||||
@Override
|
||||
public JsonElement serialize(Map<String, String> src, Type typeOfSrc, JsonSerializationContext context) {
|
||||
JsonArray result = new JsonArray();
|
||||
src.forEach((k, v) -> {
|
||||
JsonObject object = new JsonObject();
|
||||
object.addProperty("name", k);
|
||||
object.addProperty("value", v);
|
||||
result.add(object);
|
||||
});
|
||||
return result;
|
||||
}
|
||||
}
|
||||
@@ -40,10 +40,7 @@ public final class Texture {
|
||||
return url;
|
||||
}
|
||||
|
||||
public String getMetadata(String key) {
|
||||
if (metadata == null)
|
||||
return null;
|
||||
else
|
||||
return metadata.get(key);
|
||||
public Map<String, String> getMetadata() {
|
||||
return metadata;
|
||||
}
|
||||
}
|
||||
+16
-19
@@ -15,32 +15,29 @@
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.jackhuang.hmcl.task;
|
||||
package org.jackhuang.hmcl.auth.yggdrasil;
|
||||
|
||||
import org.jackhuang.hmcl.util.AutoTypingMap;
|
||||
import org.jackhuang.hmcl.util.function.ExceptionalFunction;
|
||||
import java.util.Map;
|
||||
import java.util.UUID;
|
||||
|
||||
/**
|
||||
*
|
||||
* @author huangyuhui
|
||||
*/
|
||||
class TaskCallable2<V> extends TaskResult<V> {
|
||||
public enum TextureModel {
|
||||
STEVE("default"), ALEX("slim");
|
||||
|
||||
private final String id;
|
||||
private final ExceptionalFunction<AutoTypingMap<String>, V, ?> callable;
|
||||
public final String modelName;
|
||||
|
||||
public TaskCallable2(String id, ExceptionalFunction<AutoTypingMap<String>, V, ?> callable) {
|
||||
this.id = id;
|
||||
this.callable = callable;
|
||||
private TextureModel(String modelName) {
|
||||
this.modelName = modelName;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getId() {
|
||||
return id;
|
||||
public static TextureModel detectModelName(Map<String, String> metadata) {
|
||||
if (metadata != null && "slim".equals(metadata.get("model"))) {
|
||||
return ALEX;
|
||||
} else {
|
||||
return STEVE;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void execute() throws Exception {
|
||||
setResult(callable.apply(getVariables()));
|
||||
public static TextureModel detectUUID(UUID uuid) {
|
||||
return (uuid.hashCode() & 1) == 1 ? ALEX : STEVE;
|
||||
}
|
||||
}
|
||||
@@ -18,23 +18,31 @@
|
||||
package org.jackhuang.hmcl.auth.yggdrasil;
|
||||
|
||||
import com.google.gson.JsonParseException;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
import org.jackhuang.hmcl.util.Immutable;
|
||||
import org.jackhuang.hmcl.util.StringUtils;
|
||||
import org.jackhuang.hmcl.util.gson.Validation;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
/**
|
||||
*
|
||||
* @author huang
|
||||
*/
|
||||
@Immutable
|
||||
public final class User implements Validation {
|
||||
|
||||
private final String id;
|
||||
private final PropertyMap properties;
|
||||
|
||||
@Nullable
|
||||
private final Map<String, String> properties;
|
||||
|
||||
public User(String id) {
|
||||
this(id, null);
|
||||
}
|
||||
|
||||
public User(String id, PropertyMap properties) {
|
||||
public User(String id, Map<String, String> properties) {
|
||||
this.id = id;
|
||||
this.properties = properties;
|
||||
}
|
||||
@@ -43,7 +51,7 @@ public final class User implements Validation {
|
||||
return id;
|
||||
}
|
||||
|
||||
public PropertyMap getProperties() {
|
||||
public Map<String, String> getProperties() {
|
||||
return properties;
|
||||
}
|
||||
|
||||
@@ -52,5 +60,4 @@ public final class User implements Validation {
|
||||
if (StringUtils.isBlank(id))
|
||||
throw new JsonParseException("User id cannot be empty.");
|
||||
}
|
||||
|
||||
}
|
||||
@@ -17,32 +17,62 @@
|
||||
*/
|
||||
package org.jackhuang.hmcl.auth.yggdrasil;
|
||||
|
||||
import org.jackhuang.hmcl.auth.*;
|
||||
import org.jackhuang.hmcl.util.StringUtils;
|
||||
import static java.util.Objects.requireNonNull;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.UUID;
|
||||
|
||||
import org.jackhuang.hmcl.auth.Account;
|
||||
import org.jackhuang.hmcl.auth.AuthInfo;
|
||||
import org.jackhuang.hmcl.auth.AuthenticationException;
|
||||
import org.jackhuang.hmcl.auth.CharacterDeletedException;
|
||||
import org.jackhuang.hmcl.auth.CharacterSelector;
|
||||
import org.jackhuang.hmcl.auth.CredentialExpiredException;
|
||||
import org.jackhuang.hmcl.auth.NoCharacterException;
|
||||
import org.jackhuang.hmcl.auth.ServerResponseMalformedException;
|
||||
import org.jackhuang.hmcl.util.gson.UUIDTypeAdapter;
|
||||
|
||||
import java.util.*;
|
||||
|
||||
/**
|
||||
*
|
||||
* @author huangyuhui
|
||||
*/
|
||||
public class YggdrasilAccount extends Account {
|
||||
|
||||
private final String username;
|
||||
private final YggdrasilService service;
|
||||
private boolean isOnline = false;
|
||||
private final UUID characterUUID;
|
||||
private final String username;
|
||||
|
||||
private boolean authenticated = false;
|
||||
private YggdrasilSession session;
|
||||
private UUID characterUUID;
|
||||
|
||||
protected YggdrasilAccount(YggdrasilService service, String username, UUID characterUUID, YggdrasilSession session) {
|
||||
this.service = service;
|
||||
this.username = username;
|
||||
this.session = session;
|
||||
this.characterUUID = characterUUID;
|
||||
protected YggdrasilAccount(YggdrasilService service, String username, YggdrasilSession session) {
|
||||
this.service = requireNonNull(service);
|
||||
this.username = requireNonNull(username);
|
||||
this.characterUUID = requireNonNull(session.getSelectedProfile().getId());
|
||||
this.session = requireNonNull(session);
|
||||
}
|
||||
|
||||
if (session == null || session.getSelectedProfile() == null || StringUtils.isBlank(session.getAccessToken()))
|
||||
this.session = null;
|
||||
protected YggdrasilAccount(YggdrasilService service, String username, String password, CharacterSelector selector) throws AuthenticationException {
|
||||
this.service = requireNonNull(service);
|
||||
this.username = requireNonNull(username);
|
||||
|
||||
YggdrasilSession acquiredSession = service.authenticate(username, password, randomClientToken());
|
||||
if (acquiredSession.getSelectedProfile() == null) {
|
||||
if (acquiredSession.getAvailableProfiles() == null || acquiredSession.getAvailableProfiles().isEmpty()) {
|
||||
throw new NoCharacterException();
|
||||
}
|
||||
|
||||
GameProfile characterToSelect = selector.select(service, acquiredSession.getAvailableProfiles());
|
||||
|
||||
session = service.refresh(
|
||||
acquiredSession.getAccessToken(),
|
||||
acquiredSession.getClientToken(),
|
||||
characterToSelect);
|
||||
// response validity has been checked in refresh()
|
||||
} else {
|
||||
session = acquiredSession;
|
||||
}
|
||||
|
||||
characterUUID = session.getSelectedProfile().getId();
|
||||
authenticated = true;
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -55,22 +85,20 @@ public class YggdrasilAccount extends Account {
|
||||
return session.getSelectedProfile().getName();
|
||||
}
|
||||
|
||||
public boolean isLoggedIn() {
|
||||
return session != null && StringUtils.isNotBlank(session.getAccessToken());
|
||||
}
|
||||
|
||||
public boolean canPlayOnline() {
|
||||
return isLoggedIn() && session.getSelectedProfile() != null && isOnline;
|
||||
@Override
|
||||
public UUID getUUID() {
|
||||
return session.getSelectedProfile().getId();
|
||||
}
|
||||
|
||||
@Override
|
||||
public synchronized AuthInfo logIn() throws AuthenticationException {
|
||||
if (!canPlayOnline()) {
|
||||
if (!authenticated) {
|
||||
if (service.validate(session.getAccessToken(), session.getClientToken())) {
|
||||
isOnline = true;
|
||||
authenticated = true;
|
||||
} else {
|
||||
YggdrasilSession acquiredSession;
|
||||
try {
|
||||
updateSession(service.refresh(session.getAccessToken(), session.getClientToken(), null), new SpecificCharacterSelector(characterUUID));
|
||||
acquiredSession = service.refresh(session.getAccessToken(), session.getClientToken(), null);
|
||||
} catch (RemoteAuthenticationException e) {
|
||||
if ("ForbiddenOperationException".equals(e.getRemoteName())) {
|
||||
throw new CredentialExpiredException(e);
|
||||
@@ -78,95 +106,85 @@ public class YggdrasilAccount extends Account {
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
if (acquiredSession.getSelectedProfile() == null ||
|
||||
!acquiredSession.getSelectedProfile().getId().equals(characterUUID)) {
|
||||
throw new ServerResponseMalformedException("Selected profile changed");
|
||||
}
|
||||
|
||||
session = acquiredSession;
|
||||
|
||||
authenticated = true;
|
||||
invalidate();
|
||||
}
|
||||
}
|
||||
|
||||
return session.toAuthInfo();
|
||||
}
|
||||
|
||||
@Override
|
||||
public synchronized AuthInfo logInWithPassword(String password) throws AuthenticationException {
|
||||
return logInWithPassword(password, new SpecificCharacterSelector(characterUUID));
|
||||
}
|
||||
YggdrasilSession acquiredSession = service.authenticate(username, password, randomClientToken());
|
||||
|
||||
protected AuthInfo logInWithPassword(String password, CharacterSelector selector) throws AuthenticationException {
|
||||
updateSession(service.authenticate(username, password, UUIDTypeAdapter.fromUUID(UUID.randomUUID())), selector);
|
||||
return session.toAuthInfo();
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the current session. This method shall be invoked after authenticate/refresh operation.
|
||||
* {@link #session} field shall be set only using this method. This method ensures {@link #session}
|
||||
* has a profile selected.
|
||||
*
|
||||
* @param acquiredSession the session acquired by making an authenticate/refresh request
|
||||
*/
|
||||
private void updateSession(YggdrasilSession acquiredSession, CharacterSelector selector) throws AuthenticationException {
|
||||
if (acquiredSession.getSelectedProfile() == null) {
|
||||
if (acquiredSession.getAvailableProfiles() == null || acquiredSession.getAvailableProfiles().length == 0)
|
||||
throw new NoCharacterException(this);
|
||||
if (acquiredSession.getAvailableProfiles() == null || acquiredSession.getAvailableProfiles().isEmpty()) {
|
||||
throw new CharacterDeletedException();
|
||||
}
|
||||
|
||||
this.session = service.refresh(
|
||||
GameProfile characterToSelect = acquiredSession.getAvailableProfiles().stream()
|
||||
.filter(charatcer -> charatcer.getId().equals(characterUUID))
|
||||
.findFirst()
|
||||
.orElseThrow(CharacterDeletedException::new);
|
||||
|
||||
session = service.refresh(
|
||||
acquiredSession.getAccessToken(),
|
||||
acquiredSession.getClientToken(),
|
||||
selector.select(this, Arrays.asList(acquiredSession.getAvailableProfiles())));
|
||||
characterToSelect);
|
||||
|
||||
} else {
|
||||
this.session = acquiredSession;
|
||||
if (!acquiredSession.getSelectedProfile().getId().equals(characterUUID)) {
|
||||
throw new CharacterDeletedException();
|
||||
}
|
||||
session = acquiredSession;
|
||||
}
|
||||
|
||||
this.characterUUID = this.session.getSelectedProfile().getId();
|
||||
authenticated = true;
|
||||
invalidate();
|
||||
return session.toAuthInfo();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Optional<AuthInfo> playOffline() {
|
||||
if (isLoggedIn() && session.getSelectedProfile() != null && !canPlayOnline())
|
||||
return Optional.of(session.toAuthInfo());
|
||||
|
||||
return Optional.empty();
|
||||
return Optional.of(session.toAuthInfo());
|
||||
}
|
||||
|
||||
@Override
|
||||
public Map<Object, Object> toStorage() {
|
||||
if (session == null)
|
||||
throw new IllegalStateException("No session is specified");
|
||||
|
||||
HashMap<Object, Object> storage = new HashMap<>();
|
||||
storage.put("username", getUsername());
|
||||
Map<Object, Object> storage = new HashMap<>();
|
||||
storage.put("username", username);
|
||||
storage.putAll(session.toStorage());
|
||||
service.getProfileRepository().getImmediately(characterUUID).ifPresent(profile -> {
|
||||
storage.put("profileProperties", profile.getProperties());
|
||||
});
|
||||
return storage;
|
||||
}
|
||||
|
||||
@Override
|
||||
public UUID getUUID() {
|
||||
if (session == null || session.getSelectedProfile() == null)
|
||||
return null;
|
||||
else
|
||||
return session.getSelectedProfile().getId();
|
||||
}
|
||||
|
||||
public Optional<Texture> getSkin() throws AuthenticationException {
|
||||
return getSkin(session.getSelectedProfile());
|
||||
}
|
||||
|
||||
public Optional<Texture> getSkin(GameProfile profile) throws AuthenticationException {
|
||||
if (!service.getTextures(profile).isPresent()) {
|
||||
profile = service.getCompleteGameProfile(profile.getId()).orElse(profile);
|
||||
}
|
||||
|
||||
return service.getTextures(profile).map(map -> map.get(TextureType.SKIN));
|
||||
public YggdrasilService getYggdrasilService() {
|
||||
return service;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void clearCache() {
|
||||
Optional.ofNullable(session)
|
||||
.map(YggdrasilSession::getSelectedProfile)
|
||||
.map(GameProfile::getProperties)
|
||||
.ifPresent(it -> it.remove("textures"));
|
||||
authenticated = false;
|
||||
service.getProfileRepository().invalidate(characterUUID);
|
||||
}
|
||||
|
||||
private static String randomClientToken() {
|
||||
return UUIDTypeAdapter.fromUUID(UUID.randomUUID());
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "YggdrasilAccount[username=" + getUsername() + "]";
|
||||
return "YggdrasilAccount[uuid=" + characterUUID + ", username=" + username + "]";
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
+14
-12
@@ -20,11 +20,9 @@ package org.jackhuang.hmcl.auth.yggdrasil;
|
||||
import org.jackhuang.hmcl.auth.AccountFactory;
|
||||
import org.jackhuang.hmcl.auth.AuthenticationException;
|
||||
import org.jackhuang.hmcl.auth.CharacterSelector;
|
||||
import org.jackhuang.hmcl.util.gson.UUIDTypeAdapter;
|
||||
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.UUID;
|
||||
|
||||
import static org.jackhuang.hmcl.util.Lang.tryCast;
|
||||
|
||||
@@ -34,10 +32,12 @@ import static org.jackhuang.hmcl.util.Lang.tryCast;
|
||||
*/
|
||||
public class YggdrasilAccountFactory extends AccountFactory<YggdrasilAccount> {
|
||||
|
||||
private final YggdrasilProvider provider;
|
||||
public static final YggdrasilAccountFactory MOJANG = new YggdrasilAccountFactory(YggdrasilService.MOJANG);
|
||||
|
||||
public YggdrasilAccountFactory(YggdrasilProvider provider) {
|
||||
this.provider = provider;
|
||||
private YggdrasilService service;
|
||||
|
||||
public YggdrasilAccountFactory(YggdrasilService service) {
|
||||
this.service = service;
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -46,9 +46,7 @@ public class YggdrasilAccountFactory extends AccountFactory<YggdrasilAccount> {
|
||||
Objects.requireNonNull(username);
|
||||
Objects.requireNonNull(password);
|
||||
|
||||
YggdrasilAccount account = new YggdrasilAccount(new YggdrasilService(provider), username, null, null);
|
||||
account.logInWithPassword(password, selector);
|
||||
return account;
|
||||
return new YggdrasilAccount(service, username, password, selector);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -60,10 +58,14 @@ public class YggdrasilAccountFactory extends AccountFactory<YggdrasilAccount> {
|
||||
String username = tryCast(storage.get("username"), String.class)
|
||||
.orElseThrow(() -> new IllegalArgumentException("storage does not have username"));
|
||||
|
||||
return new YggdrasilAccount(new YggdrasilService(provider), username, session.getSelectedProfile().getId(), session);
|
||||
}
|
||||
tryCast(storage.get("profileProperties"), Map.class).ifPresent(
|
||||
it -> {
|
||||
@SuppressWarnings("unchecked")
|
||||
Map<String, String> properties = it;
|
||||
GameProfile selected = session.getSelectedProfile();
|
||||
service.getProfileRepository().put(selected.getId(), new CompleteGameProfile(selected, properties));
|
||||
});
|
||||
|
||||
public static String randomToken() {
|
||||
return UUIDTypeAdapter.fromUUID(UUID.randomUUID());
|
||||
return new YggdrasilAccount(service, username, session);
|
||||
}
|
||||
}
|
||||
@@ -27,21 +27,44 @@ import org.jackhuang.hmcl.util.StringUtils;
|
||||
import org.jackhuang.hmcl.util.gson.UUIDTypeAdapter;
|
||||
import org.jackhuang.hmcl.util.gson.ValidationTypeAdapterFactory;
|
||||
import org.jackhuang.hmcl.util.io.NetworkUtils;
|
||||
import org.jackhuang.hmcl.util.javafx.ObservableOptionalCache;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.net.URL;
|
||||
import java.util.*;
|
||||
import java.util.concurrent.ThreadPoolExecutor;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.logging.Level;
|
||||
|
||||
import static java.nio.charset.StandardCharsets.UTF_8;
|
||||
import static java.util.Collections.unmodifiableList;
|
||||
import static org.jackhuang.hmcl.util.Lang.mapOf;
|
||||
import static org.jackhuang.hmcl.util.Lang.threadPool;
|
||||
import static org.jackhuang.hmcl.util.Logging.LOG;
|
||||
import static org.jackhuang.hmcl.util.Pair.pair;
|
||||
|
||||
public class YggdrasilService {
|
||||
|
||||
private static final ThreadPoolExecutor POOL = threadPool("ProfileProperties", true, 2, 10, TimeUnit.SECONDS);
|
||||
|
||||
public static final YggdrasilService MOJANG = new YggdrasilService(new MojangYggdrasilProvider());
|
||||
|
||||
private final YggdrasilProvider provider;
|
||||
private final ObservableOptionalCache<UUID, CompleteGameProfile, AuthenticationException> profileRepository;
|
||||
|
||||
public YggdrasilService(YggdrasilProvider provider) {
|
||||
this.provider = provider;
|
||||
this.profileRepository = new ObservableOptionalCache<>(
|
||||
uuid -> {
|
||||
LOG.info("Fetching properties of " + uuid + " from " + provider);
|
||||
return getCompleteGameProfile(uuid);
|
||||
},
|
||||
(uuid, e) -> LOG.log(Level.WARNING, "Failed to fetch properties of " + uuid + " from " + provider, e),
|
||||
POOL);
|
||||
}
|
||||
|
||||
public ObservableOptionalCache<UUID, CompleteGameProfile, AuthenticationException> getProfileRepository() {
|
||||
return profileRepository;
|
||||
}
|
||||
|
||||
public YggdrasilSession authenticate(String username, String password, String clientToken) throws AuthenticationException {
|
||||
@@ -62,7 +85,7 @@ public class YggdrasilService {
|
||||
return handleAuthenticationResponse(request(provider.getAuthenticationURL(), request), clientToken);
|
||||
}
|
||||
|
||||
private Map<String, Object> createRequestWithCredentials(String accessToken, String clientToken) {
|
||||
private static Map<String, Object> createRequestWithCredentials(String accessToken, String clientToken) {
|
||||
Map<String, Object> request = new HashMap<>();
|
||||
request.put("accessToken", accessToken);
|
||||
request.put("clientToken", clientToken);
|
||||
@@ -82,7 +105,16 @@ public class YggdrasilService {
|
||||
pair("name", characterToSelect.getName())));
|
||||
}
|
||||
|
||||
return handleAuthenticationResponse(request(provider.getRefreshmentURL(), request), clientToken);
|
||||
YggdrasilSession response = handleAuthenticationResponse(request(provider.getRefreshmentURL(), request), clientToken);
|
||||
|
||||
if (characterToSelect != null) {
|
||||
if (response.getSelectedProfile() == null ||
|
||||
!response.getSelectedProfile().getId().equals(characterToSelect.getId())) {
|
||||
throw new ServerResponseMalformedException("Failed to select character");
|
||||
}
|
||||
}
|
||||
|
||||
return response;
|
||||
}
|
||||
|
||||
public boolean validate(String accessToken) throws AuthenticationException {
|
||||
@@ -121,20 +153,19 @@ public class YggdrasilService {
|
||||
* @param uuid the uuid that the character corresponding to.
|
||||
* @return the complete game profile(filled with more properties)
|
||||
*/
|
||||
public Optional<GameProfile> getCompleteGameProfile(UUID uuid) throws AuthenticationException {
|
||||
public Optional<CompleteGameProfile> getCompleteGameProfile(UUID uuid) throws AuthenticationException {
|
||||
Objects.requireNonNull(uuid);
|
||||
|
||||
return Optional.ofNullable(fromJson(request(provider.getProfilePropertiesURL(uuid), null), GameProfile.class));
|
||||
return Optional.ofNullable(fromJson(request(provider.getProfilePropertiesURL(uuid), null), CompleteGameProfile.class));
|
||||
}
|
||||
|
||||
public Optional<Map<TextureType, Texture>> getTextures(GameProfile profile) throws AuthenticationException {
|
||||
public static Optional<Map<TextureType, Texture>> getTextures(CompleteGameProfile profile) throws ServerResponseMalformedException {
|
||||
Objects.requireNonNull(profile);
|
||||
|
||||
Optional<String> encodedTextures = Optional.ofNullable(profile.getProperties())
|
||||
.flatMap(properties -> Optional.ofNullable(properties.get("textures")));
|
||||
String encodedTextures = profile.getProperties().get("textures");
|
||||
|
||||
if (encodedTextures.isPresent()) {
|
||||
TextureResponse texturePayload = fromJson(new String(Base64.getDecoder().decode(encodedTextures.get()), UTF_8), TextureResponse.class);
|
||||
if (encodedTextures != null) {
|
||||
TextureResponse texturePayload = fromJson(new String(Base64.getDecoder().decode(encodedTextures), UTF_8), TextureResponse.class);
|
||||
return Optional.ofNullable(texturePayload.textures);
|
||||
} else {
|
||||
return Optional.empty();
|
||||
@@ -148,7 +179,12 @@ public class YggdrasilService {
|
||||
if (!clientToken.equals(response.clientToken))
|
||||
throw new AuthenticationException("Client token changed from " + clientToken + " to " + response.clientToken);
|
||||
|
||||
return new YggdrasilSession(response.clientToken, response.accessToken, response.selectedProfile, response.availableProfiles, response.user);
|
||||
return new YggdrasilSession(
|
||||
response.clientToken,
|
||||
response.accessToken,
|
||||
response.selectedProfile,
|
||||
response.availableProfiles == null ? null : unmodifiableList(response.availableProfiles),
|
||||
response.user);
|
||||
}
|
||||
|
||||
private static void requireEmpty(String response) throws AuthenticationException {
|
||||
@@ -168,7 +204,7 @@ public class YggdrasilService {
|
||||
}
|
||||
}
|
||||
|
||||
private String request(URL url, Object payload) throws AuthenticationException {
|
||||
private static String request(URL url, Object payload) throws AuthenticationException {
|
||||
try {
|
||||
if (payload == null)
|
||||
return NetworkUtils.doGet(url);
|
||||
@@ -187,26 +223,25 @@ public class YggdrasilService {
|
||||
}
|
||||
}
|
||||
|
||||
private class TextureResponse {
|
||||
private static class TextureResponse {
|
||||
public Map<TextureType, Texture> textures;
|
||||
}
|
||||
|
||||
private class AuthenticationResponse extends ErrorResponse {
|
||||
private static class AuthenticationResponse extends ErrorResponse {
|
||||
public String accessToken;
|
||||
public String clientToken;
|
||||
public GameProfile selectedProfile;
|
||||
public GameProfile[] availableProfiles;
|
||||
public List<GameProfile> availableProfiles;
|
||||
public User user;
|
||||
}
|
||||
|
||||
private class ErrorResponse {
|
||||
private static class ErrorResponse {
|
||||
public String error;
|
||||
public String errorMessage;
|
||||
public String cause;
|
||||
}
|
||||
|
||||
private static final Gson GSON = new GsonBuilder()
|
||||
.registerTypeAdapter(PropertyMap.class, PropertyMap.Serializer.INSTANCE)
|
||||
.registerTypeAdapter(UUID.class, UUIDTypeAdapter.INSTANCE)
|
||||
.registerTypeAdapterFactory(ValidationTypeAdapterFactory.INSTANCE)
|
||||
.create();
|
||||
|
||||
@@ -18,10 +18,11 @@
|
||||
package org.jackhuang.hmcl.auth.yggdrasil;
|
||||
|
||||
import com.google.gson.Gson;
|
||||
import com.google.gson.GsonBuilder;
|
||||
import org.jackhuang.hmcl.auth.AuthInfo;
|
||||
import org.jackhuang.hmcl.util.Immutable;
|
||||
import org.jackhuang.hmcl.util.gson.UUIDTypeAdapter;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.UUID;
|
||||
@@ -30,15 +31,16 @@ import static org.jackhuang.hmcl.util.Lang.mapOf;
|
||||
import static org.jackhuang.hmcl.util.Lang.tryCast;
|
||||
import static org.jackhuang.hmcl.util.Pair.pair;
|
||||
|
||||
@Immutable
|
||||
public class YggdrasilSession {
|
||||
|
||||
private String clientToken;
|
||||
private String accessToken;
|
||||
private GameProfile selectedProfile;
|
||||
private GameProfile[] availableProfiles;
|
||||
private User user;
|
||||
private final String clientToken;
|
||||
private final String accessToken;
|
||||
private final GameProfile selectedProfile;
|
||||
private final List<GameProfile> availableProfiles;
|
||||
private final User user;
|
||||
|
||||
public YggdrasilSession(String clientToken, String accessToken, GameProfile selectedProfile, GameProfile[] availableProfiles, User user) {
|
||||
public YggdrasilSession(String clientToken, String accessToken, GameProfile selectedProfile, List<GameProfile> availableProfiles, User user) {
|
||||
this.clientToken = clientToken;
|
||||
this.accessToken = accessToken;
|
||||
this.selectedProfile = selectedProfile;
|
||||
@@ -64,7 +66,7 @@ public class YggdrasilSession {
|
||||
/**
|
||||
* @return nullable (null if the YggdrasilSession is loaded from storage)
|
||||
*/
|
||||
public GameProfile[] getAvailableProfiles() {
|
||||
public List<GameProfile> getAvailableProfiles() {
|
||||
return availableProfiles;
|
||||
}
|
||||
|
||||
@@ -78,7 +80,7 @@ public class YggdrasilSession {
|
||||
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 userId = tryCast(storage.get("userid"), String.class).orElseThrow(() -> new IllegalArgumentException("userid is missing"));
|
||||
PropertyMap userProperties = tryCast(storage.get("userProperties"), Map.class).map(PropertyMap::fromMap).orElse(null);
|
||||
Map<String, String> userProperties = tryCast(storage.get("userProperties"), Map.class).orElse(null);
|
||||
return new YggdrasilSession(clientToken, accessToken, new GameProfile(uuid, name), null, new User(userId, userProperties));
|
||||
}
|
||||
|
||||
@@ -107,5 +109,5 @@ public class YggdrasilSession {
|
||||
Optional.ofNullable(user.getProperties()).map(GSON_PROPERTIES::toJson).orElse("{}"));
|
||||
}
|
||||
|
||||
private static final Gson GSON_PROPERTIES = new GsonBuilder().registerTypeAdapter(PropertyMap.class, PropertyMap.LegacySerializer.INSTANCE).create();
|
||||
private static final Gson GSON_PROPERTIES = new Gson();
|
||||
}
|
||||
@@ -17,18 +17,8 @@
|
||||
*/
|
||||
package org.jackhuang.hmcl.download;
|
||||
|
||||
import org.jackhuang.hmcl.game.Argument;
|
||||
import org.jackhuang.hmcl.game.Arguments;
|
||||
import org.jackhuang.hmcl.game.StringArgument;
|
||||
import org.jackhuang.hmcl.game.Version;
|
||||
import org.jackhuang.hmcl.game.*;
|
||||
import org.jackhuang.hmcl.task.TaskResult;
|
||||
import org.jackhuang.hmcl.util.StringUtils;
|
||||
import org.jackhuang.hmcl.util.platform.CommandBuilder;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
public class MaintainTask extends TaskResult<Version> {
|
||||
|
||||
@@ -51,40 +41,30 @@ public class MaintainTask extends TaskResult<Version> {
|
||||
|
||||
public static Version maintain(Version version) {
|
||||
LibraryAnalyzer libraryAnalyzer = LibraryAnalyzer.analyze(version);
|
||||
List<String> mcArgs = version.getMinecraftArguments().map(StringUtils::tokenize).map(ArrayList::new).orElse(null);
|
||||
List<Argument> game = version.getArguments().map(Arguments::getGame).map(ArrayList::new).orElseGet(ArrayList::new);
|
||||
boolean useMcArgs = mcArgs != null;
|
||||
VersionLibraryBuilder builder = new VersionLibraryBuilder(version);
|
||||
|
||||
if (!libraryAnalyzer.hasForge()) {
|
||||
removeTweakClass(useMcArgs, mcArgs, game, "forge");
|
||||
builder.removeTweakClass("forge");
|
||||
}
|
||||
|
||||
// Installing Forge will override the Minecraft arguments in json, so LiteLoader and OptiFine Tweaker are being re-added.
|
||||
|
||||
removeTweakClass(useMcArgs, mcArgs, game, "liteloader");
|
||||
builder.removeTweakClass("liteloader");
|
||||
if (libraryAnalyzer.hasLiteLoader()) {
|
||||
addArgument(useMcArgs, mcArgs, game, "--tweakClass", "com.mumfrey.liteloader.launch.LiteLoaderTweaker");
|
||||
builder.addArgument("--tweakClass", "com.mumfrey.liteloader.launch.LiteLoaderTweaker");
|
||||
}
|
||||
|
||||
removeTweakClass(useMcArgs, mcArgs, game, "optifine");
|
||||
builder.removeTweakClass("optifine");
|
||||
if (libraryAnalyzer.hasOptiFine()) {
|
||||
if (!libraryAnalyzer.hasLiteLoader() && !libraryAnalyzer.hasForge()) {
|
||||
addArgument(useMcArgs, mcArgs, game, "--tweakClass", "optifine.OptiFineTweaker");
|
||||
builder.addArgument("--tweakClass", "optifine.OptiFineTweaker");
|
||||
} else {
|
||||
// If forge or LiteLoader installed, OptiFine Forge Tweaker is needed.
|
||||
addArgument(useMcArgs, mcArgs, game, "--tweakClass", "optifine.OptiFineForgeTweaker");
|
||||
builder.addArgument("--tweakClass", "optifine.OptiFineForgeTweaker");
|
||||
}
|
||||
}
|
||||
|
||||
Version result;
|
||||
if (useMcArgs) {
|
||||
// Since $ will be escaped in linux, and our maintain of minecraftArgument will not cause escaping,
|
||||
// so we regenerate the minecraftArgument without escaping.
|
||||
result = version.setMinecraftArguments(new CommandBuilder().addAllWithoutParsing(mcArgs).toString());
|
||||
} else {
|
||||
result = version.setArguments(version.getArguments().map(args -> args.withGame(game)).orElse(new Arguments(game, Collections.emptyList())));
|
||||
}
|
||||
return result;
|
||||
return builder.build();
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -92,43 +72,5 @@ public class MaintainTask extends TaskResult<Version> {
|
||||
return id;
|
||||
}
|
||||
|
||||
private static void removeTweakClass(boolean useMcArgs, List<String> mcArgs, List<Argument> game, String target) {
|
||||
if (useMcArgs) {
|
||||
for (int i = 0; i + 1 < mcArgs.size(); ++i) {
|
||||
String arg0Str = mcArgs.get(i);
|
||||
String arg1Str = mcArgs.get(i + 1);
|
||||
if (arg0Str.equals("--tweakClass") && arg1Str.toLowerCase().contains(target)) {
|
||||
mcArgs.remove(i);
|
||||
mcArgs.remove(i);
|
||||
--i;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
for (int i = 0; i + 1 < game.size(); ++i) {
|
||||
Argument arg0 = game.get(i);
|
||||
Argument arg1 = game.get(i + 1);
|
||||
if (arg0 instanceof StringArgument && arg1 instanceof StringArgument) {
|
||||
// We need to preserve the tokens
|
||||
String arg0Str = arg0.toString();
|
||||
String arg1Str = arg1.toString();
|
||||
if (arg0Str.equals("--tweakClass") && arg1Str.toLowerCase().contains(target)) {
|
||||
game.remove(i);
|
||||
game.remove(i);
|
||||
--i;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void addArgument(boolean useMcArgs, List<String> mcArgs, List<Argument> game, String... args) {
|
||||
if (useMcArgs) {
|
||||
mcArgs.addAll(Arrays.asList(args));
|
||||
} else {
|
||||
for (String arg : args)
|
||||
game.add(new StringArgument(arg));
|
||||
}
|
||||
}
|
||||
|
||||
public static final String ID = "version";
|
||||
}
|
||||
@@ -104,6 +104,11 @@ public class LibraryDownloadTask extends Task {
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean doPreExecute() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void preExecute() throws Exception {
|
||||
Optional<Path> libPath = cacheRepository.getLibrary(originalLibrary);
|
||||
@@ -135,6 +140,11 @@ public class LibraryDownloadTask extends Task {
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean doPostExecute() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void postExecute() throws Exception {
|
||||
if (!cached)
|
||||
|
||||
+2
-1
@@ -76,10 +76,11 @@ public final class LiteLoaderInstallTask extends TaskResult<Version> {
|
||||
|
||||
Version tempVersion = version.setLibraries(Lang.merge(remote.getLibraries(), Collections.singleton(library)));
|
||||
|
||||
// --tweakClass will be added in MaintainTask
|
||||
setResult(version
|
||||
.setMainClass("net.minecraft.launchwrapper.Launch")
|
||||
.setLibraries(Lang.merge(tempVersion.getLibraries(), version.getLibraries()))
|
||||
.setLogging(Collections.emptyMap())
|
||||
.setLogging(Collections.emptyMap()) // Mods may log in malformed format, causing XML parser to crash. So we suppress using official log4j configuration
|
||||
);
|
||||
|
||||
dependencies.add(dependencyManager.checkLibraryCompletionAsync(tempVersion));
|
||||
|
||||
+1
-1
@@ -66,7 +66,7 @@ public final class OptiFineBMCLVersionList extends VersionList<OptiFineRemoteVer
|
||||
if (!duplicates.add(mirror))
|
||||
continue;
|
||||
|
||||
boolean isPre = element.getPatch() != null && (element.getPatch().equals("pre") || element.getPatch().startsWith("alpha"));
|
||||
boolean isPre = element.getPatch() != null && (element.getPatch().startsWith("pre") || element.getPatch().startsWith("alpha"));
|
||||
|
||||
if (StringUtils.isBlank(element.getGameVersion()))
|
||||
continue;
|
||||
|
||||
@@ -86,6 +86,7 @@ public final class OptiFineInstallTask extends TaskResult<Version> {
|
||||
if (version.getMainClass() == null || !version.getMainClass().startsWith("net.minecraft.launchwrapper."))
|
||||
libraries.add(0, new Library("net.minecraft", "launchwrapper", "1.12"));
|
||||
|
||||
// --tweakClass will be added in MaintainTask
|
||||
setResult(version
|
||||
.setLibraries(Lang.merge(version.getLibraries(), libraries))
|
||||
.setMainClass("net.minecraft.launchwrapper.Launch")
|
||||
|
||||
@@ -0,0 +1,92 @@
|
||||
/*
|
||||
* Hello Minecraft! Launcher
|
||||
* Copyright (C) 2019 huangyuhui <huanghongxun2008@126.com> and contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.jackhuang.hmcl.game;
|
||||
|
||||
import org.jackhuang.hmcl.util.StringUtils;
|
||||
import org.jackhuang.hmcl.util.platform.CommandBuilder;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
*
|
||||
* @author huangyuhui
|
||||
*/
|
||||
public final class VersionLibraryBuilder {
|
||||
private final Version version;
|
||||
private final List<String> mcArgs;
|
||||
private final List<Argument> game;
|
||||
private final boolean useMcArgs;
|
||||
|
||||
public VersionLibraryBuilder(Version version) {
|
||||
this.version = version;
|
||||
this.mcArgs = version.getMinecraftArguments().map(StringUtils::tokenize).map(ArrayList::new).orElse(null);
|
||||
this.game = version.getArguments().map(Arguments::getGame).map(ArrayList::new).orElseGet(ArrayList::new);
|
||||
this.useMcArgs = mcArgs != null;
|
||||
}
|
||||
|
||||
public Version build() {
|
||||
if (useMcArgs) {
|
||||
// Since $ will be escaped in linux, and our maintain of minecraftArgument will not cause escaping,
|
||||
// so we regenerate the minecraftArgument without escaping.
|
||||
return version.setMinecraftArguments(new CommandBuilder().addAllWithoutParsing(mcArgs).toString());
|
||||
} else {
|
||||
return version.setArguments(version.getArguments().map(args -> args.withGame(game)).orElse(new Arguments(game, Collections.emptyList())));
|
||||
}
|
||||
}
|
||||
|
||||
public void removeTweakClass(String target) {
|
||||
if (useMcArgs) {
|
||||
for (int i = 0; i + 1 < mcArgs.size(); ++i) {
|
||||
String arg0Str = mcArgs.get(i);
|
||||
String arg1Str = mcArgs.get(i + 1);
|
||||
if (arg0Str.equals("--tweakClass") && arg1Str.toLowerCase().contains(target)) {
|
||||
mcArgs.remove(i);
|
||||
mcArgs.remove(i);
|
||||
--i;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
for (int i = 0; i + 1 < game.size(); ++i) {
|
||||
Argument arg0 = game.get(i);
|
||||
Argument arg1 = game.get(i + 1);
|
||||
if (arg0 instanceof StringArgument && arg1 instanceof StringArgument) {
|
||||
// We need to preserve the tokens
|
||||
String arg0Str = arg0.toString();
|
||||
String arg1Str = arg1.toString();
|
||||
if (arg0Str.equals("--tweakClass") && arg1Str.toLowerCase().contains(target)) {
|
||||
game.remove(i);
|
||||
game.remove(i);
|
||||
--i;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void addArgument(String... args) {
|
||||
if (useMcArgs) {
|
||||
mcArgs.addAll(Arrays.asList(args));
|
||||
} else {
|
||||
for (String arg : args)
|
||||
game.add(new StringArgument(arg));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -28,6 +28,7 @@ import org.jackhuang.hmcl.util.io.FileUtils;
|
||||
import org.jackhuang.hmcl.util.io.Unzipper;
|
||||
import org.jackhuang.hmcl.util.io.Zipper;
|
||||
|
||||
import java.io.BufferedInputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.OutputStream;
|
||||
@@ -39,6 +40,7 @@ import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.logging.Level;
|
||||
import java.util.stream.Collectors;
|
||||
import java.util.stream.Stream;
|
||||
import java.util.zip.GZIPInputStream;
|
||||
import java.util.zip.GZIPOutputStream;
|
||||
|
||||
@@ -193,7 +195,7 @@ public class World {
|
||||
}
|
||||
|
||||
private static CompoundTag parseLevelDat(Path path) throws IOException {
|
||||
try (InputStream is = new GZIPInputStream(Files.newInputStream(path))) {
|
||||
try (InputStream is = new BufferedInputStream(new GZIPInputStream(Files.newInputStream(path)))) {
|
||||
Tag nbt = NBTIO.readTag(is);
|
||||
if (nbt instanceof CompoundTag)
|
||||
return (CompoundTag) nbt;
|
||||
@@ -202,20 +204,20 @@ public class World {
|
||||
}
|
||||
}
|
||||
|
||||
public static List<World> getWorlds(Path savesDir) {
|
||||
List<World> worlds = new ArrayList<>();
|
||||
public static Stream<World> getWorlds(Path savesDir) {
|
||||
try {
|
||||
if (Files.exists(savesDir))
|
||||
for (Path world : Files.newDirectoryStream(savesDir)) {
|
||||
return Files.list(savesDir).flatMap(world -> {
|
||||
try {
|
||||
worlds.add(new World(world));
|
||||
return Stream.of(new World(world));
|
||||
} catch (IOException | IllegalArgumentException e) {
|
||||
Logging.LOG.log(Level.WARNING, "Failed to read world " + world, e);
|
||||
return Stream.empty();
|
||||
}
|
||||
}
|
||||
});
|
||||
} catch (IOException e) {
|
||||
Logging.LOG.log(Level.WARNING, "Failed to read saves", e);
|
||||
}
|
||||
return worlds;
|
||||
return Stream.empty();
|
||||
}
|
||||
}
|
||||
@@ -64,6 +64,11 @@ public class ModpackUpdateTask extends Task {
|
||||
FileUtils.copyDirectory(repository.getVersionRoot(id).toPath(), backupFolder);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean doPostExecute() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void postExecute() throws Exception {
|
||||
if (isDependenciesSucceeded()) {
|
||||
|
||||
@@ -20,6 +20,7 @@ package org.jackhuang.hmcl.mod;
|
||||
import com.google.gson.annotations.SerializedName;
|
||||
import org.jackhuang.hmcl.game.Library;
|
||||
import org.jackhuang.hmcl.util.Immutable;
|
||||
import org.jackhuang.hmcl.util.Lang;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
@@ -44,19 +45,23 @@ public final class MultiMCInstancePatch {
|
||||
private final List<String> tweakers;
|
||||
|
||||
@SerializedName("+libraries")
|
||||
private final List<Library> _libraries;
|
||||
|
||||
@SerializedName("libraries")
|
||||
private final List<Library> libraries;
|
||||
|
||||
public MultiMCInstancePatch() {
|
||||
this("", "", "", "", "", Collections.emptyList(), Collections.emptyList());
|
||||
this("", "", "", "", "", Collections.emptyList(), Collections.emptyList(), Collections.emptyList());
|
||||
}
|
||||
|
||||
public MultiMCInstancePatch(String name, String version, String gameVersion, String mainClass, String fileId, List<String> tweakers, List<Library> libraries) {
|
||||
public MultiMCInstancePatch(String name, String version, String gameVersion, String mainClass, String fileId, List<String> tweakers, List<Library> _libraries, List<Library> libraries) {
|
||||
this.name = name;
|
||||
this.version = version;
|
||||
this.gameVersion = gameVersion;
|
||||
this.mainClass = mainClass;
|
||||
this.fileId = fileId;
|
||||
this.tweakers = new ArrayList<>(tweakers);
|
||||
this._libraries = new ArrayList<>(_libraries);
|
||||
this.libraries = new ArrayList<>(libraries);
|
||||
}
|
||||
|
||||
@@ -85,7 +90,7 @@ public final class MultiMCInstancePatch {
|
||||
}
|
||||
|
||||
public List<Library> getLibraries() {
|
||||
return Collections.unmodifiableList(libraries);
|
||||
return Lang.merge(_libraries, libraries);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -21,10 +21,11 @@ import com.google.gson.JsonParseException;
|
||||
import com.google.gson.reflect.TypeToken;
|
||||
import org.jackhuang.hmcl.download.DefaultDependencyManager;
|
||||
import org.jackhuang.hmcl.download.GameBuilder;
|
||||
import org.jackhuang.hmcl.download.MaintainTask;
|
||||
import org.jackhuang.hmcl.download.game.VersionJsonSaveTask;
|
||||
import org.jackhuang.hmcl.game.Arguments;
|
||||
import org.jackhuang.hmcl.game.DefaultGameRepository;
|
||||
import org.jackhuang.hmcl.game.Version;
|
||||
import org.jackhuang.hmcl.game.VersionLibraryBuilder;
|
||||
import org.jackhuang.hmcl.task.Task;
|
||||
import org.jackhuang.hmcl.util.Lang;
|
||||
import org.jackhuang.hmcl.util.gson.JsonUtils;
|
||||
@@ -63,7 +64,6 @@ public final class MultiMCModpackInstallTask extends Task {
|
||||
this.name = name;
|
||||
this.repository = dependencyManager.getGameRepository();
|
||||
|
||||
File run = repository.getRunDirectory(name);
|
||||
File json = repository.getModpackConfiguration(name);
|
||||
if (repository.hasVersion(name) && !json.exists())
|
||||
throw new IllegalArgumentException("Version " + name + " already exists.");
|
||||
@@ -89,6 +89,22 @@ public final class MultiMCModpackInstallTask extends Task {
|
||||
if (event.isFailed())
|
||||
repository.removeVersionFromDisk(name);
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<Task> getDependencies() {
|
||||
return dependencies;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean doPreExecute() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void preExecute() throws Exception {
|
||||
File run = repository.getRunDirectory(name);
|
||||
File json = repository.getModpackConfiguration(name);
|
||||
|
||||
ModpackConfiguration<MultiMCInstanceConfiguration> config = null;
|
||||
try {
|
||||
@@ -102,14 +118,14 @@ public final class MultiMCModpackInstallTask extends Task {
|
||||
} catch (JsonParseException | IOException ignore) {
|
||||
}
|
||||
|
||||
dependents.add(new ModpackInstallTask<>(zipFile, run, modpack.getEncoding(), "/" + manifest.getName() + "/minecraft", any -> true, config));
|
||||
try (FileSystem fs = CompressingUtils.readonly(zipFile.toPath()).setEncoding(modpack.getEncoding()).build()) {
|
||||
if (Files.exists(fs.getPath("/" + manifest.getName() + "/.minecraft")))
|
||||
dependents.add(new ModpackInstallTask<>(zipFile, run, modpack.getEncoding(), "/" + manifest.getName() + "/.minecraft", any -> true, config));
|
||||
else if (Files.exists(fs.getPath("/" + manifest.getName() + "/minecraft")))
|
||||
dependents.add(new ModpackInstallTask<>(zipFile, run, modpack.getEncoding(), "/" + manifest.getName() + "/minecraft", any -> true, config));
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<Task> getDependencies() {
|
||||
return dependencies;
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public List<Task> getDependents() {
|
||||
return dependents;
|
||||
@@ -129,20 +145,19 @@ public final class MultiMCModpackInstallTask extends Task {
|
||||
if (patchJson.toString().endsWith(".json")) {
|
||||
// If json is malformed, we should stop installing this modpack instead of skipping it.
|
||||
MultiMCInstancePatch patch = JsonUtils.GSON.fromJson(IOUtils.readFullyAsString(Files.newInputStream(patchJson)), MultiMCInstancePatch.class);
|
||||
List<String> newArguments = new LinkedList<>();
|
||||
for (String arg : patch.getTweakers()) {
|
||||
newArguments.add("--tweakClass");
|
||||
newArguments.add(arg);
|
||||
}
|
||||
version = version
|
||||
|
||||
VersionLibraryBuilder builder = new VersionLibraryBuilder(version);
|
||||
for (String arg : patch.getTweakers())
|
||||
builder.addArgument("--tweakClass", arg);
|
||||
|
||||
version = builder.build()
|
||||
.setLibraries(Lang.merge(version.getLibraries(), patch.getLibraries()))
|
||||
.setMainClass(patch.getMainClass())
|
||||
.setArguments(version.getArguments().orElseGet(Arguments::new).addGameArguments(newArguments));
|
||||
.setMainClass(patch.getMainClass());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
dependencies.add(new VersionJsonSaveTask(repository, version));
|
||||
dependencies.add(new MaintainTask(version).then(var -> new VersionJsonSaveTask(repository, var.get(MaintainTask.ID))));
|
||||
dependencies.add(new MinecraftInstanceTask<>(zipFile, modpack.getEncoding(), "/" + manifest.getName() + "/minecraft", manifest, MODPACK_TYPE, repository.getModpackConfiguration(name)));
|
||||
}
|
||||
|
||||
|
||||
@@ -126,7 +126,7 @@ public final class GetTask extends TaskResult<String> {
|
||||
}
|
||||
|
||||
if (size > 0 && size != read)
|
||||
throw new IllegalStateException("Not completed! Readed: " + read + ", total size: " + size);
|
||||
throw new IOException("Not completed! Readed: " + read + ", total size: " + size);
|
||||
|
||||
String result = baos.toString(charset.name());
|
||||
setResult(result);
|
||||
|
||||
@@ -18,10 +18,7 @@
|
||||
package org.jackhuang.hmcl.task;
|
||||
|
||||
import javafx.application.Platform;
|
||||
import javafx.beans.property.ReadOnlyDoubleProperty;
|
||||
import javafx.beans.property.ReadOnlyDoubleWrapper;
|
||||
import javafx.beans.property.ReadOnlyStringProperty;
|
||||
import javafx.beans.property.ReadOnlyStringWrapper;
|
||||
import javafx.beans.property.*;
|
||||
import org.jackhuang.hmcl.event.EventManager;
|
||||
import org.jackhuang.hmcl.util.AutoTypingMap;
|
||||
import org.jackhuang.hmcl.util.InvocationDispatcher;
|
||||
@@ -58,14 +55,18 @@ public abstract class Task {
|
||||
this.significance = significance;
|
||||
}
|
||||
|
||||
private TaskState state = TaskState.READY;
|
||||
private ReadOnlyObjectWrapper<TaskState> state = new ReadOnlyObjectWrapper<>(this, "state", TaskState.READY);
|
||||
|
||||
public TaskState getState() {
|
||||
return state;
|
||||
return state.get();
|
||||
}
|
||||
|
||||
void setState(TaskState state) {
|
||||
this.state = state;
|
||||
this.state.setValue(state);
|
||||
}
|
||||
|
||||
public ReadOnlyObjectProperty<TaskState> stateProperty() {
|
||||
return state.getReadOnlyProperty();
|
||||
}
|
||||
|
||||
private Throwable lastException = null;
|
||||
@@ -144,6 +145,10 @@ public abstract class Task {
|
||||
this.variables = variables;
|
||||
}
|
||||
|
||||
public boolean doPreExecute() {
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws InterruptedException if current thread is interrupted
|
||||
* @see Thread#isInterrupted
|
||||
@@ -156,6 +161,10 @@ public abstract class Task {
|
||||
*/
|
||||
public abstract void execute() throws Exception;
|
||||
|
||||
public boolean doPostExecute() {
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws InterruptedException if current thread is interrupted
|
||||
* @see Thread#isInterrupted
|
||||
@@ -371,12 +380,16 @@ public abstract class Task {
|
||||
return new CoupleTask<>(null, b, true);
|
||||
}
|
||||
|
||||
public static <V> TaskResult<V> ofResult(Callable<V> callable) {
|
||||
return ofResult("", callable);
|
||||
}
|
||||
|
||||
public static <V> TaskResult<V> ofResult(String id, Callable<V> callable) {
|
||||
return new TaskCallable<>(id, callable);
|
||||
}
|
||||
|
||||
public static <V> TaskResult<V> ofResult(String id, ExceptionalFunction<AutoTypingMap<String>, V, ?> closure) {
|
||||
return new TaskCallable2<>(id, closure);
|
||||
return new TaskCallable<>(id, closure);
|
||||
}
|
||||
|
||||
private static ExceptionalFunction<AutoTypingMap<String>, Task, ?> convert(Task t) {
|
||||
|
||||
@@ -17,6 +17,9 @@
|
||||
*/
|
||||
package org.jackhuang.hmcl.task;
|
||||
|
||||
import org.jackhuang.hmcl.util.AutoTypingMap;
|
||||
import org.jackhuang.hmcl.util.function.ExceptionalFunction;
|
||||
|
||||
import java.util.concurrent.Callable;
|
||||
|
||||
/**
|
||||
@@ -26,9 +29,13 @@ import java.util.concurrent.Callable;
|
||||
class TaskCallable<V> extends TaskResult<V> {
|
||||
|
||||
private final String id;
|
||||
private final Callable<V> callable;
|
||||
private final ExceptionalFunction<AutoTypingMap<String>, V, ?> callable;
|
||||
|
||||
public TaskCallable(String id, Callable<V> callable) {
|
||||
this(id, variables -> callable.call());
|
||||
}
|
||||
|
||||
public TaskCallable(String id, ExceptionalFunction<AutoTypingMap<String>, V, ?> callable) {
|
||||
this.id = id;
|
||||
this.callable = callable;
|
||||
}
|
||||
@@ -40,6 +47,6 @@ class TaskCallable<V> extends TaskResult<V> {
|
||||
|
||||
@Override
|
||||
public void execute() throws Exception {
|
||||
setResult(callable.call());
|
||||
setResult(callable.apply(getVariables()));
|
||||
}
|
||||
}
|
||||
@@ -149,13 +149,15 @@ public final class TaskExecutor {
|
||||
try {
|
||||
task.setVariables(variables);
|
||||
|
||||
try {
|
||||
task.getScheduler().schedule(task::preExecute).get();
|
||||
} catch (ExecutionException e) {
|
||||
if (e.getCause() instanceof Exception)
|
||||
throw (Exception) e.getCause();
|
||||
else
|
||||
throw e;
|
||||
if (task.doPreExecute()) {
|
||||
try {
|
||||
task.getScheduler().schedule(task::preExecute).get();
|
||||
} catch (ExecutionException e) {
|
||||
if (e.getCause() instanceof Exception)
|
||||
throw (Exception) e.getCause();
|
||||
else
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
boolean doDependentsSucceeded = executeTasks(task.getDependents());
|
||||
@@ -194,13 +196,15 @@ public final class TaskExecutor {
|
||||
if (doDependenciesSucceeded)
|
||||
task.setDependenciesSucceeded();
|
||||
|
||||
try {
|
||||
task.getScheduler().schedule(task::postExecute).get();
|
||||
} catch (ExecutionException e) {
|
||||
if (e.getCause() instanceof Exception)
|
||||
throw (Exception) e.getCause();
|
||||
else
|
||||
throw e;
|
||||
if (task.doPostExecute()) {
|
||||
try {
|
||||
task.getScheduler().schedule(task::postExecute).get();
|
||||
} catch (ExecutionException e) {
|
||||
if (e.getCause() instanceof Exception)
|
||||
throw (Exception) e.getCause();
|
||||
else
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
flag = true;
|
||||
@@ -211,6 +215,7 @@ public final class TaskExecutor {
|
||||
task.onDone().fireEvent(new TaskEvent(this, task, false));
|
||||
taskListeners.forEach(it -> it.onFinished(task));
|
||||
} catch (InterruptedException e) {
|
||||
task.setLastException(e);
|
||||
if (task.getSignificance().shouldLog()) {
|
||||
Logging.LOG.log(Level.FINE, "Task aborted: " + task.getName());
|
||||
}
|
||||
|
||||
@@ -38,6 +38,7 @@ public abstract class TaskListener implements EventListener {
|
||||
}
|
||||
|
||||
public void onFailed(Task task, Throwable throwable) {
|
||||
onFinished(task);
|
||||
}
|
||||
|
||||
public void onStop(boolean success, TaskExecutor executor) {
|
||||
|
||||
@@ -17,6 +17,13 @@
|
||||
*/
|
||||
package org.jackhuang.hmcl.task;
|
||||
|
||||
import org.jackhuang.hmcl.util.ReflectionHelper;
|
||||
import org.jackhuang.hmcl.util.function.ExceptionalConsumer;
|
||||
import org.jackhuang.hmcl.util.function.ExceptionalFunction;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
|
||||
/**
|
||||
* A task that has a result.
|
||||
*
|
||||
@@ -35,4 +42,62 @@ public abstract class TaskResult<V> extends Task {
|
||||
}
|
||||
|
||||
public abstract String getId();
|
||||
|
||||
public <R, E extends Exception> TaskResult<R> thenResult(ExceptionalFunction<V, R, E> task) {
|
||||
return thenResult(Schedulers.defaultScheduler(), task);
|
||||
}
|
||||
|
||||
public <R, E extends Exception> TaskResult<R> thenResult(Scheduler scheduler, ExceptionalFunction<V, R, E> task) {
|
||||
return thenResult(ReflectionHelper.getCaller().toString(), scheduler, task);
|
||||
}
|
||||
|
||||
public <R, E extends Exception> TaskResult<R> thenResult(String id, Scheduler scheduler, ExceptionalFunction<V, R, E> task) {
|
||||
return new Subtask<>(id, scheduler, task);
|
||||
}
|
||||
|
||||
public <T extends Exception, K extends Exception> Task finalizedResult(Scheduler scheduler, ExceptionalConsumer<V, T> success, ExceptionalConsumer<Exception, K> failure) {
|
||||
return finalized(scheduler, variables -> success.accept(getResult()), failure);
|
||||
}
|
||||
|
||||
public Task finalizedResult(Scheduler scheduler, FinalizedCallback<V> callback) {
|
||||
return new FinalizedTask(this, scheduler,
|
||||
(variables, isDependentsSucceeded) -> callback.execute(getResult(), isDependentsSucceeded),
|
||||
ReflectionHelper.getCaller().toString());
|
||||
}
|
||||
|
||||
private class Subtask<R> extends TaskResult<R> {
|
||||
private final String id;
|
||||
private final Scheduler scheduler;
|
||||
private final ExceptionalFunction<V, R, ?> callable;
|
||||
|
||||
public Subtask(String id, Scheduler scheduler, ExceptionalFunction<V, R, ?> callable) {
|
||||
this.id = id;
|
||||
this.scheduler = scheduler;
|
||||
this.callable = callable;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Collection<? extends Task> getDependents() {
|
||||
return Collections.singleton(TaskResult.this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getId() {
|
||||
return id;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Scheduler getScheduler() {
|
||||
return scheduler;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void execute() throws Exception {
|
||||
setResult(callable.apply(TaskResult.this.getResult()));
|
||||
}
|
||||
}
|
||||
|
||||
public interface FinalizedCallback<V> {
|
||||
void execute(V result, boolean isDependentsSucceeded) throws Exception;
|
||||
}
|
||||
}
|
||||
@@ -18,6 +18,7 @@
|
||||
package org.jackhuang.hmcl.util;
|
||||
|
||||
import java.util.Optional;
|
||||
import java.util.concurrent.Executor;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
import java.util.function.Consumer;
|
||||
import java.util.function.Supplier;
|
||||
@@ -31,9 +32,11 @@ import java.util.function.Supplier;
|
||||
*/
|
||||
public class InvocationDispatcher<ARG> implements Consumer<ARG> {
|
||||
|
||||
public static <ARG> InvocationDispatcher<ARG> runOn(Consumer<Runnable> executor, Consumer<ARG> action) {
|
||||
return new InvocationDispatcher<>(arg -> executor.accept(() -> {
|
||||
action.accept(arg.get());
|
||||
public static <ARG> InvocationDispatcher<ARG> runOn(Executor executor, Consumer<ARG> action) {
|
||||
return new InvocationDispatcher<>(arg -> executor.execute(() -> {
|
||||
synchronized (action) {
|
||||
action.accept(arg.get());
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
|
||||
@@ -18,7 +18,10 @@
|
||||
package org.jackhuang.hmcl.util;
|
||||
|
||||
import java.util.*;
|
||||
import java.util.concurrent.LinkedBlockingQueue;
|
||||
import java.util.concurrent.ThreadPoolExecutor;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
import java.util.function.Consumer;
|
||||
import org.jackhuang.hmcl.util.function.ExceptionalRunnable;
|
||||
import org.jackhuang.hmcl.util.function.ExceptionalSupplier;
|
||||
@@ -172,6 +175,17 @@ public final class Lang {
|
||||
return thread;
|
||||
}
|
||||
|
||||
public static ThreadPoolExecutor threadPool(String name, boolean daemon, int threads, long timeout, TimeUnit timeunit) {
|
||||
AtomicInteger counter = new AtomicInteger(1);
|
||||
ThreadPoolExecutor pool = new ThreadPoolExecutor(0, threads, timeout, timeunit, new LinkedBlockingQueue<>(), r -> {
|
||||
Thread t = new Thread(r, name + "-" + counter.getAndIncrement());
|
||||
t.setDaemon(daemon);
|
||||
return t;
|
||||
});
|
||||
pool.allowsCoreThreadTimeOut();
|
||||
return pool;
|
||||
}
|
||||
|
||||
public static int parseInt(Object string, int defaultValue) {
|
||||
try {
|
||||
return Integer.parseInt(string.toString());
|
||||
|
||||
@@ -33,9 +33,6 @@ public final class UUIDTypeAdapter extends TypeAdapter<UUID> {
|
||||
|
||||
public static final UUIDTypeAdapter INSTANCE = new UUIDTypeAdapter();
|
||||
|
||||
private UUIDTypeAdapter() {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void write(JsonWriter writer, UUID value) throws IOException {
|
||||
writer.value(value == null ? null : fromUUID(value));
|
||||
|
||||
@@ -19,9 +19,14 @@ package org.jackhuang.hmcl.util.javafx;
|
||||
|
||||
import static java.util.Objects.requireNonNull;
|
||||
|
||||
import java.util.Objects;
|
||||
import java.util.concurrent.Executor;
|
||||
import java.util.function.Function;
|
||||
import java.util.function.Supplier;
|
||||
|
||||
import org.jackhuang.hmcl.util.InvocationDispatcher;
|
||||
|
||||
import javafx.application.Platform;
|
||||
import javafx.beans.binding.ObjectBinding;
|
||||
import javafx.beans.value.ObservableValue;
|
||||
|
||||
@@ -53,6 +58,10 @@ public abstract class MultiStepBinding<T, U> extends ObjectBinding<U> {
|
||||
return new FlatMappedBinding<>(map(mapper), nullAlternative);
|
||||
}
|
||||
|
||||
public <V> MultiStepBinding<?, V> asyncMap(Function<U, V> mapper, V initial, Executor executor) {
|
||||
return new AsyncMappedBinding<>(this, mapper, executor, initial);
|
||||
}
|
||||
|
||||
private static class SimpleBinding<T> extends MultiStepBinding<T, T> {
|
||||
|
||||
public SimpleBinding(ObservableValue<T> predecessor) {
|
||||
@@ -68,6 +77,11 @@ public abstract class MultiStepBinding<T, U> extends ObjectBinding<U> {
|
||||
public <V> MultiStepBinding<?, V> map(Function<T, V> mapper) {
|
||||
return new MappedBinding<>(predecessor, mapper);
|
||||
}
|
||||
|
||||
@Override
|
||||
public <V> MultiStepBinding<?, V> asyncMap(Function<T, V> mapper, V initial, Executor executor) {
|
||||
return new AsyncMappedBinding<>(predecessor, mapper, executor, initial);
|
||||
}
|
||||
}
|
||||
|
||||
private static class MappedBinding<T, U> extends MultiStepBinding<T, U> {
|
||||
@@ -119,4 +133,52 @@ public abstract class MultiStepBinding<T, U> extends ObjectBinding<U> {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static class AsyncMappedBinding<T, U> extends MultiStepBinding<T, U> {
|
||||
|
||||
private final InvocationDispatcher<T> dispatcher;
|
||||
|
||||
private boolean initialized = false;
|
||||
private T prev;
|
||||
private U value;
|
||||
|
||||
public AsyncMappedBinding(ObservableValue<T> predecessor, Function<T, U> mapper, Executor executor, U initial) {
|
||||
super(predecessor);
|
||||
this.value = initial;
|
||||
|
||||
dispatcher = InvocationDispatcher.runOn(executor, arg -> {
|
||||
synchronized (this) {
|
||||
if (initialized && Objects.equals(arg, prev)) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
U newValue = mapper.apply(arg);
|
||||
synchronized (this) {
|
||||
prev = arg;
|
||||
value = newValue;
|
||||
initialized = true;
|
||||
}
|
||||
Platform.runLater(this::invalidate);
|
||||
});
|
||||
}
|
||||
|
||||
// called on FX thread, this method is serial
|
||||
@Override
|
||||
protected U computeValue() {
|
||||
T currentPrev = predecessor.getValue();
|
||||
U value;
|
||||
boolean updateNeeded = false;
|
||||
synchronized (this) {
|
||||
value = this.value;
|
||||
if (!initialized || !Objects.equals(currentPrev, prev)) {
|
||||
updateNeeded = true;
|
||||
}
|
||||
}
|
||||
if (updateNeeded) {
|
||||
dispatcher.accept(currentPrev);
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,162 @@
|
||||
/*
|
||||
* Hello Minecraft! Launcher
|
||||
* Copyright (C) 2019 huangyuhui <huanghongxun2008@126.com> and contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.jackhuang.hmcl.util.javafx;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.concurrent.CancellationException;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.concurrent.CompletionException;
|
||||
import java.util.concurrent.Executor;
|
||||
import java.util.function.BiConsumer;
|
||||
|
||||
import org.jackhuang.hmcl.util.function.ExceptionalFunction;
|
||||
|
||||
import javafx.application.Platform;
|
||||
import javafx.beans.binding.Bindings;
|
||||
import javafx.beans.binding.ObjectBinding;
|
||||
|
||||
/**
|
||||
* @author yushijinhun
|
||||
*/
|
||||
public class ObservableCache<K, V, E extends Exception> {
|
||||
|
||||
private final ExceptionalFunction<K, V, E> source;
|
||||
private final BiConsumer<K, Throwable> exceptionHandler;
|
||||
private final V fallbackValue;
|
||||
private final Executor executor;
|
||||
private final ObservableHelper observable = new ObservableHelper();
|
||||
private final Map<K, V> cache = new HashMap<>();
|
||||
private final Map<K, CompletableFuture<V>> pendings = new HashMap<>();
|
||||
private final Map<K, Boolean> invalidated = new HashMap<>();
|
||||
|
||||
public ObservableCache(ExceptionalFunction<K, V, E> source, BiConsumer<K, Throwable> exceptionHandler, V fallbackValue, Executor executor) {
|
||||
this.source = source;
|
||||
this.exceptionHandler = exceptionHandler;
|
||||
this.fallbackValue = fallbackValue;
|
||||
this.executor = executor;
|
||||
}
|
||||
|
||||
public Optional<V> getImmediately(K key) {
|
||||
synchronized (this) {
|
||||
return Optional.ofNullable(cache.get(key));
|
||||
}
|
||||
}
|
||||
|
||||
public void put(K key, V value) {
|
||||
synchronized (this) {
|
||||
cache.put(key, value);
|
||||
invalidated.remove(key);
|
||||
}
|
||||
Platform.runLater(observable::invalidate);
|
||||
}
|
||||
|
||||
private CompletableFuture<V> query(K key, Executor executor) {
|
||||
CompletableFuture<V> future;
|
||||
synchronized (this) {
|
||||
CompletableFuture<V> prev = pendings.get(key);
|
||||
if (prev != null) {
|
||||
return prev;
|
||||
} else {
|
||||
future = new CompletableFuture<>();
|
||||
pendings.put(key, future);
|
||||
}
|
||||
}
|
||||
|
||||
executor.execute(() -> {
|
||||
V result;
|
||||
try {
|
||||
result = source.apply(key);
|
||||
} catch (Throwable ex) {
|
||||
synchronized (this) {
|
||||
pendings.remove(key);
|
||||
}
|
||||
exceptionHandler.accept(key, ex);
|
||||
future.completeExceptionally(ex);
|
||||
return;
|
||||
}
|
||||
|
||||
synchronized (this) {
|
||||
cache.put(key, result);
|
||||
invalidated.remove(key);
|
||||
pendings.remove(key, future);
|
||||
}
|
||||
future.complete(result);
|
||||
Platform.runLater(observable::invalidate);
|
||||
});
|
||||
|
||||
return future;
|
||||
}
|
||||
|
||||
public V get(K key) {
|
||||
V cached;
|
||||
synchronized (this) {
|
||||
cached = cache.get(key);
|
||||
if (cached != null && !invalidated.containsKey(key)) {
|
||||
return cached;
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
return query(key, Runnable::run).join();
|
||||
} catch (CompletionException | CancellationException ignored) {
|
||||
}
|
||||
|
||||
if (cached == null) {
|
||||
return fallbackValue;
|
||||
} else {
|
||||
return cached;
|
||||
}
|
||||
}
|
||||
|
||||
public V getDirectly(K key) throws E {
|
||||
V result = source.apply(key);
|
||||
put(key, result);
|
||||
return result;
|
||||
}
|
||||
|
||||
public ObjectBinding<V> binding(K key) {
|
||||
return Bindings.createObjectBinding(() -> {
|
||||
V result;
|
||||
boolean refresh;
|
||||
synchronized (this) {
|
||||
result = cache.get(key);
|
||||
if (result == null) {
|
||||
result = fallbackValue;
|
||||
refresh = true;
|
||||
} else {
|
||||
refresh = invalidated.containsKey(key);
|
||||
}
|
||||
}
|
||||
if (refresh) {
|
||||
query(key, executor);
|
||||
}
|
||||
return result;
|
||||
}, observable);
|
||||
}
|
||||
|
||||
public void invalidate(K key) {
|
||||
synchronized (this) {
|
||||
if (cache.containsKey(key)) {
|
||||
invalidated.put(key, Boolean.TRUE);
|
||||
}
|
||||
}
|
||||
Platform.runLater(observable::invalidate);
|
||||
}
|
||||
}
|
||||
@@ -33,6 +33,10 @@ public class ObservableHelper implements Observable, InvalidationListener {
|
||||
private List<InvalidationListener> listeners = new CopyOnWriteArrayList<>();
|
||||
private Observable source;
|
||||
|
||||
public ObservableHelper() {
|
||||
this.source = this;
|
||||
}
|
||||
|
||||
public ObservableHelper(Observable source) {
|
||||
this.source = source;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,62 @@
|
||||
/*
|
||||
* Hello Minecraft! Launcher
|
||||
* Copyright (C) 2019 huangyuhui <huanghongxun2008@126.com> and contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.jackhuang.hmcl.util.javafx;
|
||||
|
||||
import java.util.Optional;
|
||||
import java.util.concurrent.Executor;
|
||||
import java.util.function.BiConsumer;
|
||||
|
||||
import org.jackhuang.hmcl.util.function.ExceptionalFunction;
|
||||
|
||||
import javafx.beans.binding.ObjectBinding;
|
||||
|
||||
/**
|
||||
* @author yushijinhun
|
||||
*/
|
||||
public class ObservableOptionalCache<K, V, E extends Exception> {
|
||||
|
||||
private final ObservableCache<K, Optional<V>, E> backed;
|
||||
|
||||
public ObservableOptionalCache(ExceptionalFunction<K, Optional<V>, E> source, BiConsumer<K, Throwable> exceptionHandler, Executor executor) {
|
||||
backed = new ObservableCache<>(source, exceptionHandler, Optional.empty(), executor);
|
||||
}
|
||||
|
||||
public Optional<V> getImmediately(K key) {
|
||||
return backed.getImmediately(key).flatMap(it -> it);
|
||||
}
|
||||
|
||||
public void put(K key, V value) {
|
||||
backed.put(key, Optional.of(value));
|
||||
}
|
||||
|
||||
public Optional<V> get(K key) {
|
||||
return backed.get(key);
|
||||
}
|
||||
|
||||
public Optional<V> getDirectly(K key) throws E {
|
||||
return backed.getDirectly(key);
|
||||
}
|
||||
|
||||
public ObjectBinding<Optional<V>> binding(K key) {
|
||||
return backed.binding(key);
|
||||
}
|
||||
|
||||
public void invalidate(K key) {
|
||||
backed.invalidate(key);
|
||||
}
|
||||
}
|
||||
@@ -66,6 +66,10 @@ public class VersionNumberTest {
|
||||
u = VersionNumber.asVersion("1.6.0_22");
|
||||
v = VersionNumber.asVersion("1.8.0_11");
|
||||
Assert.assertTrue(u.compareTo(v) < 0);
|
||||
|
||||
u = VersionNumber.asVersion("1.12.2-14.23.5.2760");
|
||||
v = VersionNumber.asVersion("1.12.2-14.23.4.2739");
|
||||
Assert.assertTrue(u.compareTo(v) > 0);
|
||||
}
|
||||
|
||||
@Test
|
||||
|
||||
Reference in new issue
Block a user