Merge branch 'javafx' of https://github.com/huanghongxun/HMCL into javafx

This commit is contained in:
huanghongxun
2019-02-13 17:46:18 +08:00
103 changed files with 2354 additions and 1118 deletions

View File

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

View File

@@ -80,13 +80,6 @@ public class HMCLGameRepository extends DefaultGameRepository {
} }
} }
@Override
public void refreshVersions() {
EventBus.EVENT_BUS.fireEvent(new RefreshingVersionsEvent(this));
refreshVersionsImpl();
EventBus.EVENT_BUS.fireEvent(new RefreshedVersionsEvent(this));
}
public void changeDirectory(File newDirectory) { public void changeDirectory(File newDirectory) {
setBaseDirectory(newDirectory); setBaseDirectory(newDirectory);
refreshVersionsAsync().start(); refreshVersionsAsync().start();
@@ -168,6 +161,8 @@ public class HMCLGameRepository extends DefaultGameRepository {
return new Image("file:" + iconFile.getAbsolutePath()); return new Image("file:" + iconFile.getAbsolutePath());
else if ("net.minecraft.launchwrapper.Launch".equals(version.getMainClass())) else if ("net.minecraft.launchwrapper.Launch".equals(version.getMainClass()))
return new Image("/assets/img/furnace.png"); return new Image("/assets/img/furnace.png");
else if ("cpw.mods.modlauncher.Launcher".equals(version.getMainClass()))
return new Image("/assets/img/furnace.png");
else else
return new Image("/assets/img/grass.png"); return new Image("/assets/img/grass.png");
} }

View File

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

View File

@@ -35,7 +35,6 @@ import org.jackhuang.hmcl.auth.authlibinjector.AuthlibInjectorServer;
import org.jackhuang.hmcl.auth.authlibinjector.SimpleAuthlibInjectorArtifactProvider; import org.jackhuang.hmcl.auth.authlibinjector.SimpleAuthlibInjectorArtifactProvider;
import org.jackhuang.hmcl.auth.offline.OfflineAccount; import org.jackhuang.hmcl.auth.offline.OfflineAccount;
import org.jackhuang.hmcl.auth.offline.OfflineAccountFactory; 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.YggdrasilAccount;
import org.jackhuang.hmcl.auth.yggdrasil.YggdrasilAccountFactory; import org.jackhuang.hmcl.auth.yggdrasil.YggdrasilAccountFactory;
import org.jackhuang.hmcl.task.Schedulers; import org.jackhuang.hmcl.task.Schedulers;
@@ -63,7 +62,7 @@ public final class Accounts {
private Accounts() {} private Accounts() {}
public static final OfflineAccountFactory FACTORY_OFFLINE = OfflineAccountFactory.INSTANCE; 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); public static final AuthlibInjectorAccountFactory FACTORY_AUTHLIB_INJECTOR = new AuthlibInjectorAccountFactory(createAuthlibInjectorArtifactProvider(), Accounts::getOrCreateAuthlibInjectorServer);
// ==== login type / account factory mapping ==== // ==== login type / account factory mapping ====
@@ -71,7 +70,7 @@ public final class Accounts {
private static final Map<AccountFactory<?>, String> factory2type = new HashMap<>(); private static final Map<AccountFactory<?>, String> factory2type = new HashMap<>();
static { static {
type2factory.put("offline", FACTORY_OFFLINE); type2factory.put("offline", FACTORY_OFFLINE);
type2factory.put("yggdrasil", FACTORY_YGGDRASIL); type2factory.put("yggdrasil", FACTORY_MOJANG);
type2factory.put("authlibInjector", FACTORY_AUTHLIB_INJECTOR); type2factory.put("authlibInjector", FACTORY_AUTHLIB_INJECTOR);
type2factory.forEach((type, factory) -> factory2type.put(factory, type)); type2factory.forEach((type, factory) -> factory2type.put(factory, type));
@@ -94,7 +93,7 @@ public final class Accounts {
else if (account instanceof AuthlibInjectorAccount) else if (account instanceof AuthlibInjectorAccount)
return FACTORY_AUTHLIB_INJECTOR; return FACTORY_AUTHLIB_INJECTOR;
else if (account instanceof YggdrasilAccount) else if (account instanceof YggdrasilAccount)
return FACTORY_YGGDRASIL; return FACTORY_MOJANG;
else else
throw new IllegalArgumentException("Failed to determine account type: " + account); throw new IllegalArgumentException("Failed to determine account type: " + account);
} }
@@ -279,7 +278,7 @@ public final class Accounts {
// ==== Login type name i18n === // ==== Login type name i18n ===
private static Map<AccountFactory<?>, String> unlocalizedLoginTypeNames = mapOf( private static Map<AccountFactory<?>, String> unlocalizedLoginTypeNames = mapOf(
pair(Accounts.FACTORY_OFFLINE, "account.methods.offline"), 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")); pair(Accounts.FACTORY_AUTHLIB_INJECTOR, "account.methods.authlib_injector"));
public static String getLocalizedLoginTypeName(AccountFactory<?> factory) { public static String getLocalizedLoginTypeName(AccountFactory<?> factory) {

View File

@@ -234,8 +234,20 @@ public final class FXUtils {
} }
} }
public static void installTooltip(Node node, String tooltip) { public static void installFastTooltip(Node node, Tooltip tooltip) {
installTooltip(node, 0, 5000, 0, new 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) { public static void installTooltip(Node node, double openDelay, double visibleDelay, double closeDelay, Tooltip tooltip) {

View File

@@ -19,12 +19,13 @@ package org.jackhuang.hmcl.ui;
import com.jfoenix.controls.JFXButton; import com.jfoenix.controls.JFXButton;
import com.jfoenix.controls.JFXScrollPane; import com.jfoenix.controls.JFXScrollPane;
import javafx.beans.binding.Bindings; import javafx.beans.binding.Bindings;
import javafx.geometry.Insets; import javafx.geometry.Insets;
import javafx.geometry.Pos; import javafx.geometry.Pos;
import javafx.scene.control.ScrollPane; import javafx.scene.control.ScrollPane;
import javafx.scene.control.SkinBase; import javafx.scene.control.SkinBase;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.Pane;
import javafx.scene.layout.StackPane; import javafx.scene.layout.StackPane;
import javafx.scene.layout.VBox; import javafx.scene.layout.VBox;
import org.jackhuang.hmcl.setting.Theme; import org.jackhuang.hmcl.setting.Theme;
@@ -36,6 +37,7 @@ public class ListPageSkin extends SkinBase<ListPage<?>> {
super(skinnable); super(skinnable);
SpinnerPane spinnerPane = new SpinnerPane(); SpinnerPane spinnerPane = new SpinnerPane();
Pane placeholder = new Pane();
StackPane contentPane = new StackPane(); StackPane contentPane = new StackPane();
{ {
@@ -48,9 +50,12 @@ public class ListPageSkin extends SkinBase<ListPage<?>> {
list.setSpacing(10); list.setSpacing(10);
list.setPadding(new Insets(10)); list.setPadding(new Insets(10));
VBox content = new VBox();
content.getChildren().setAll(list, placeholder);
Bindings.bindContent(list.getChildren(), skinnable.itemsProperty()); Bindings.bindContent(list.getChildren(), skinnable.itemsProperty());
scrollPane.setContent(list); scrollPane.setContent(content);
JFXScrollPane.smoothScrolling(scrollPane); JFXScrollPane.smoothScrolling(scrollPane);
} }
@@ -86,7 +91,13 @@ public class ListPageSkin extends SkinBase<ListPage<?>> {
}); });
} }
contentPane.getChildren().setAll(scrollPane, vBox); // Keep a blank space to prevent buttons from blocking up mod items.
BorderPane group = new BorderPane();
group.setPickOnBounds(false);
group.setBottom(vBox);
placeholder.minHeightProperty().bind(vBox.heightProperty());
contentPane.getChildren().setAll(scrollPane, group);
} }
spinnerPane.loadingProperty().bind(skinnable.loadingProperty()); spinnerPane.loadingProperty().bind(skinnable.loadingProperty());

View File

@@ -87,11 +87,8 @@ public final class MainPage extends StackPane implements DecoratorPage {
lblIcon.setGraphic(SVG.update(Theme.whiteFillBinding(), 20, 20)); lblIcon.setGraphic(SVG.update(Theme.whiteFillBinding(), 20, 20));
TwoLineListItem prompt = new TwoLineListItem(); TwoLineListItem prompt = new TwoLineListItem();
prompt.setTitleFill(Color.WHITE);
prompt.setSubtitleFill(Color.WHITE);
prompt.setSubtitle(i18n("update.bubble.subtitle")); prompt.setSubtitle(i18n("update.bubble.subtitle"));
prompt.setPickOnBounds(false); prompt.setPickOnBounds(false);
prompt.setStyle("-jfx-title-font-weight: BOLD;");
prompt.titleProperty().bind(latestVersionProperty()); prompt.titleProperty().bind(latestVersionProperty());
hBox.getChildren().setAll(lblIcon, prompt); hBox.getChildren().setAll(lblIcon, prompt);

View File

@@ -129,7 +129,7 @@ public final class SettingsPage extends SettingsView implements DecoratorPage {
config().commonDirectoryProperty(), config().commonDirTypeProperty())); config().commonDirectoryProperty(), config().commonDirTypeProperty()));
// ==== Update ==== // ==== Update ====
FXUtils.installTooltip(btnUpdate, i18n("update.tooltip")); FXUtils.installFastTooltip(btnUpdate, i18n("update.tooltip"));
updateListener = any -> { updateListener = any -> {
btnUpdate.setVisible(UpdateChecker.isOutdated()); btnUpdate.setVisible(UpdateChecker.isOutdated());

View File

@@ -359,7 +359,7 @@ public abstract class SettingsView extends StackPane {
HBox hBox = new HBox(); HBox hBox = new HBox();
hBox.setSpacing(3); hBox.setSpacing(3);
cboFont = new FontComboBox(12, false); cboFont = new FontComboBox(12);
txtFontSize = new JFXTextField(); txtFontSize = new JFXTextField();
FXUtils.setLimitWidth(txtFontSize, 50); FXUtils.setLimitWidth(txtFontSize, 50);
hBox.getChildren().setAll(cboFont, txtFontSize); hBox.getChildren().setAll(cboFont, txtFontSize);

View File

@@ -17,15 +17,15 @@
*/ */
package org.jackhuang.hmcl.ui.account; package org.jackhuang.hmcl.ui.account;
import javafx.beans.binding.Bindings;
import javafx.beans.property.ObjectProperty; import javafx.beans.property.ObjectProperty;
import javafx.beans.property.SimpleObjectProperty; import javafx.beans.property.SimpleObjectProperty;
import javafx.scene.image.Image; import javafx.scene.image.Image;
import org.jackhuang.hmcl.auth.Account; import org.jackhuang.hmcl.auth.Account;
import org.jackhuang.hmcl.auth.offline.OfflineAccount; import org.jackhuang.hmcl.auth.offline.OfflineAccount;
import org.jackhuang.hmcl.auth.yggdrasil.YggdrasilAccount; 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.setting.Theme;
import org.jackhuang.hmcl.task.Schedulers;
import org.jackhuang.hmcl.ui.SVG; import org.jackhuang.hmcl.ui.SVG;
import org.jackhuang.hmcl.ui.construct.AdvancedListItem; import org.jackhuang.hmcl.ui.construct.AdvancedListItem;
@@ -38,23 +38,15 @@ public class AccountAdvancedListItem extends AdvancedListItem {
protected void invalidated() { protected void invalidated() {
Account account = get(); Account account = get();
if (account == null) { if (account == null) {
titleProperty().unbind();
setTitle(i18n("account.missing")); setTitle(i18n("account.missing"));
setSubtitle(i18n("account.missing.add")); setSubtitle(i18n("account.missing.add"));
imageProperty().unbind();
setImage(new Image("/assets/img/craft_table.png")); setImage(new Image("/assets/img/craft_table.png"));
} else { } else {
setTitle(account.getCharacter()); titleProperty().bind(Bindings.createStringBinding(account::getCharacter, account));
setSubtitle(accountSubtitle(account)); setSubtitle(accountSubtitle(account));
imageProperty().bind(TexturesLoader.fxAvatarBinding(account, 32));
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));
});
}
} }
} }
}; };

View File

@@ -17,21 +17,28 @@
*/ */
package org.jackhuang.hmcl.ui.account; package org.jackhuang.hmcl.ui.account;
import javafx.beans.binding.Bindings;
import javafx.beans.binding.StringBinding;
import javafx.beans.property.*; import javafx.beans.property.*;
import javafx.scene.control.RadioButton; import javafx.scene.control.RadioButton;
import javafx.scene.control.Skin; import javafx.scene.control.Skin;
import javafx.scene.image.Image; import javafx.scene.image.Image;
import org.jackhuang.hmcl.auth.Account; 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.AuthlibInjectorAccount;
import org.jackhuang.hmcl.auth.authlibinjector.AuthlibInjectorServer; import org.jackhuang.hmcl.auth.authlibinjector.AuthlibInjectorServer;
import org.jackhuang.hmcl.auth.offline.OfflineAccount; import org.jackhuang.hmcl.auth.offline.OfflineAccount;
import org.jackhuang.hmcl.auth.yggdrasil.YggdrasilAccount; import org.jackhuang.hmcl.game.TexturesLoader;
import org.jackhuang.hmcl.game.AccountHelper;
import org.jackhuang.hmcl.setting.Accounts; 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 static org.jackhuang.hmcl.util.i18n.I18n.i18n;
import java.util.logging.Level;
public class AccountListItem extends RadioButton { public class AccountListItem extends RadioButton {
private final Account account; private final Account account;
@@ -44,23 +51,24 @@ public class AccountListItem extends RadioButton {
getStyleClass().clear(); getStyleClass().clear();
setUserData(account); setUserData(account);
StringBuilder subtitleString = new StringBuilder(Accounts.getLocalizedLoginTypeName(Accounts.getAccountFactory(account))); String loginTypeName = Accounts.getLocalizedLoginTypeName(Accounts.getAccountFactory(account));
if (account instanceof AuthlibInjectorAccount) { if (account instanceof AuthlibInjectorAccount) {
AuthlibInjectorServer server = ((AuthlibInjectorAccount) account).getServer(); 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) StringBinding characterName = Bindings.createStringBinding(account::getCharacter, account);
title.set(account.getCharacter()); if (account instanceof OfflineAccount) {
else title.bind(characterName);
title.set(account.getUsername() + " - " + account.getCharacter()); } else {
subtitle.set(subtitleString.toString()); title.bind(Bindings.concat(account.getUsername(), " - ", characterName));
}
final int scaleRatio = 4; image.bind(TexturesLoader.fxAvatarBinding(account, 32));
Image image = account instanceof YggdrasilAccount ?
AccountHelper.getSkin((YggdrasilAccount) account, scaleRatio) :
AccountHelper.getDefaultSkin(account.getUUID(), scaleRatio);
this.image.set(AccountHelper.getHead(image, scaleRatio));
} }
@Override @Override
@@ -69,19 +77,20 @@ public class AccountListItem extends RadioButton {
} }
public void refresh() { public void refresh() {
if (account instanceof YggdrasilAccount) { account.clearCache();
// progressBar.setVisible(true); thread(() -> {
AccountHelper.refreshSkinAsync((YggdrasilAccount) account) try {
.finalized(Schedulers.javafx(), (variables, isDependentsSucceeded) -> { account.logIn();
// progressBar.setVisible(false); } catch (CredentialExpiredException e) {
try {
if (isDependentsSucceeded) { DialogController.logIn(account);
final int scaleRatio = 4; } catch (Exception e1) {
Image image = AccountHelper.getSkin((YggdrasilAccount) account, scaleRatio); LOG.log(Level.WARNING, "Failed to refresh " + account + " with password", e1);
this.image.set(AccountHelper.getHead(image, scaleRatio)); }
} } catch (AuthenticationException e) {
}).start(); LOG.log(Level.WARNING, "Failed to refresh " + account + " with token", e);
} }
});
} }
public void remove() { public void remove() {

View File

@@ -21,18 +21,26 @@ import com.jfoenix.concurrency.JFXUtilities;
import com.jfoenix.controls.JFXButton; import com.jfoenix.controls.JFXButton;
import com.jfoenix.controls.JFXRadioButton; import com.jfoenix.controls.JFXRadioButton;
import com.jfoenix.effects.JFXDepthManager; import com.jfoenix.effects.JFXDepthManager;
import javafx.beans.binding.Bindings;
import javafx.geometry.Pos; import javafx.geometry.Pos;
import javafx.scene.control.Label;
import javafx.scene.control.SkinBase; import javafx.scene.control.SkinBase;
import javafx.scene.control.Tooltip;
import javafx.scene.image.ImageView; import javafx.scene.image.ImageView;
import javafx.scene.layout.BorderPane; import javafx.scene.layout.BorderPane;
import javafx.scene.layout.HBox; import javafx.scene.layout.HBox;
import javafx.scene.layout.VBox;
import org.jackhuang.hmcl.setting.Theme; import org.jackhuang.hmcl.setting.Theme;
import org.jackhuang.hmcl.ui.FXUtils; import org.jackhuang.hmcl.ui.FXUtils;
import org.jackhuang.hmcl.ui.SVG; import org.jackhuang.hmcl.ui.SVG;
import org.jackhuang.hmcl.ui.construct.TwoLineListItem;
import static org.jackhuang.hmcl.util.i18n.I18n.i18n; 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 class AccountListItemSkin extends SkinBase<AccountListItem> {
public AccountListItemSkin(AccountListItem skinnable) { public AccountListItemSkin(AccountListItem skinnable) {
@@ -58,8 +66,22 @@ public class AccountListItemSkin extends SkinBase<AccountListItem> {
FXUtils.limitSize(imageView, 32, 32); FXUtils.limitSize(imageView, 32, 32);
imageView.imageProperty().bind(skinnable.imageProperty()); 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); BorderPane.setAlignment(item, Pos.CENTER);
center.getChildren().setAll(imageView, item); center.getChildren().setAll(imageView, item);
root.setCenter(center); root.setCenter(center);
@@ -69,7 +91,7 @@ public class AccountListItemSkin extends SkinBase<AccountListItem> {
btnRefresh.setOnMouseClicked(e -> skinnable.refresh()); btnRefresh.setOnMouseClicked(e -> skinnable.refresh());
btnRefresh.getStyleClass().add("toggle-icon4"); btnRefresh.getStyleClass().add("toggle-icon4");
btnRefresh.setGraphic(SVG.refresh(Theme.blackFillBinding(), -1, -1)); 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); right.getChildren().add(btnRefresh);
JFXButton btnRemove = new JFXButton(); JFXButton btnRemove = new JFXButton();
@@ -77,14 +99,12 @@ public class AccountListItemSkin extends SkinBase<AccountListItem> {
btnRemove.getStyleClass().add("toggle-icon4"); btnRemove.getStyleClass().add("toggle-icon4");
BorderPane.setAlignment(btnRemove, Pos.CENTER); BorderPane.setAlignment(btnRemove, Pos.CENTER);
btnRemove.setGraphic(SVG.delete(Theme.blackFillBinding(), -1, -1)); 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); right.getChildren().add(btnRemove);
root.setRight(right); root.setRight(right);
root.setStyle("-fx-background-color: white; -fx-padding: 8 8 8 0;"); root.setStyle("-fx-background-color: white; -fx-padding: 8 8 8 0;");
JFXDepthManager.setDepth(root, 1); JFXDepthManager.setDepth(root, 1);
item.titleProperty().bind(skinnable.titleProperty());
item.subtitleProperty().bind(skinnable.subtitleProperty());
getChildren().setAll(root); getChildren().setAll(root);
} }

View File

@@ -37,8 +37,7 @@ public class AccountLoginPane extends StackPane {
private final Consumer<AuthInfo> success; private final Consumer<AuthInfo> success;
private final Runnable failed; private final Runnable failed;
@FXML @FXML private Label lblUsername;
private Label lblUsername;
@FXML private JFXPasswordField txtPassword; @FXML private JFXPasswordField txtPassword;
@FXML private Label lblCreationWarning; @FXML private Label lblCreationWarning;
@FXML private JFXProgressBar progressBar; @FXML private JFXProgressBar progressBar;

View File

@@ -19,13 +19,17 @@ package org.jackhuang.hmcl.ui.account;
import com.jfoenix.concurrency.JFXUtilities; import com.jfoenix.concurrency.JFXUtilities;
import com.jfoenix.controls.*; import com.jfoenix.controls.*;
import javafx.application.Platform;
import javafx.beans.binding.Bindings; import javafx.beans.binding.Bindings;
import javafx.beans.property.ListProperty;
import javafx.beans.property.ReadOnlyObjectProperty; import javafx.beans.property.ReadOnlyObjectProperty;
import javafx.beans.property.SimpleListProperty;
import javafx.collections.FXCollections;
import javafx.fxml.FXML; import javafx.fxml.FXML;
import javafx.geometry.Pos; import javafx.geometry.Pos;
import javafx.scene.control.Hyperlink; import javafx.scene.control.Hyperlink;
import javafx.scene.control.Label; import javafx.scene.control.Label;
import javafx.scene.image.Image;
import javafx.scene.image.ImageView; import javafx.scene.image.ImageView;
import javafx.scene.layout.BorderPane; import javafx.scene.layout.BorderPane;
import javafx.scene.layout.HBox; 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.authlibinjector.AuthlibInjectorServer;
import org.jackhuang.hmcl.auth.yggdrasil.GameProfile; import org.jackhuang.hmcl.auth.yggdrasil.GameProfile;
import org.jackhuang.hmcl.auth.yggdrasil.RemoteAuthenticationException; import org.jackhuang.hmcl.auth.yggdrasil.RemoteAuthenticationException;
import org.jackhuang.hmcl.auth.yggdrasil.YggdrasilAccount; import org.jackhuang.hmcl.auth.yggdrasil.YggdrasilService;
import org.jackhuang.hmcl.game.AccountHelper; import org.jackhuang.hmcl.game.TexturesLoader;
import org.jackhuang.hmcl.setting.Accounts; import org.jackhuang.hmcl.setting.Accounts;
import org.jackhuang.hmcl.task.Schedulers; import org.jackhuang.hmcl.task.Schedulers;
import org.jackhuang.hmcl.task.Task; import org.jackhuang.hmcl.task.Task;
import org.jackhuang.hmcl.ui.Controllers; import org.jackhuang.hmcl.ui.Controllers;
import org.jackhuang.hmcl.ui.FXUtils; import org.jackhuang.hmcl.ui.FXUtils;
import org.jackhuang.hmcl.ui.construct.*; 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.List;
import java.util.Map;
import java.util.concurrent.CountDownLatch; 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 java.util.Objects.requireNonNull;
import static org.jackhuang.hmcl.setting.ConfigHolder.config; import static org.jackhuang.hmcl.setting.ConfigHolder.config;
import static org.jackhuang.hmcl.ui.FXUtils.*; import static org.jackhuang.hmcl.ui.FXUtils.*;
@@ -63,12 +69,13 @@ public class AddAccountPane extends StackPane {
@FXML private JFXComboBox<AccountFactory<?>> cboType; @FXML private JFXComboBox<AccountFactory<?>> cboType;
@FXML private JFXComboBox<AuthlibInjectorServer> cboServers; @FXML private JFXComboBox<AuthlibInjectorServer> cboServers;
@FXML private Label lblInjectorServer; @FXML private Label lblInjectorServer;
@FXML private Hyperlink linkManageInjectorServers;
@FXML private JFXDialogLayout layout;
@FXML private JFXButton btnAccept; @FXML private JFXButton btnAccept;
@FXML private JFXButton btnAddServer; @FXML private JFXButton btnAddServer;
@FXML private JFXButton btnManageServer; @FXML private JFXButton btnManageServer;
@FXML private SpinnerPane acceptPane; @FXML private SpinnerPane acceptPane;
@FXML private HBox linksContainer;
private ListProperty<Hyperlink> links = new SimpleListProperty<>();;
public AddAccountPane() { public AddAccountPane() {
FXUtils.loadFXML(this, "/assets/fxml/account-add.fxml"); FXUtils.loadFXML(this, "/assets/fxml/account-add.fxml");
@@ -79,7 +86,7 @@ public class AddAccountPane extends StackPane {
cboServers.getItems().addListener(onInvalidating(this::selectDefaultServer)); cboServers.getItems().addListener(onInvalidating(this::selectDefaultServer));
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)); cboType.setConverter(stringConverter(Accounts::getLocalizedLoginTypeName));
// try selecting the preferred login type // try selecting the preferred login type
cboType.getSelectionModel().select( cboType.getSelectionModel().select(
@@ -117,6 +124,34 @@ public class AddAccountPane extends StackPane {
txtUsername.textProperty(), txtUsername.textProperty(),
txtPassword.textProperty(), txtPassword.visibleProperty(), txtPassword.textProperty(), txtPassword.visibleProperty(),
cboServers.getSelectionModel().selectedItemProperty(), cboServers.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);
} }
/** /**
@@ -212,7 +247,7 @@ public class AddAccountPane extends StackPane {
private final CountDownLatch latch = new CountDownLatch(1); private final CountDownLatch latch = new CountDownLatch(1);
private GameProfile selectedProfile = null; private GameProfile selectedProfile = null;
{ public Selector() {
setStyle("-fx-padding: 8px;"); setStyle("-fx-padding: 8px;");
cancel.setText(i18n("button.cancel")); cancel.setText(i18n("button.cancel"));
@@ -230,45 +265,33 @@ public class AddAccountPane extends StackPane {
} }
@Override @Override
public GameProfile select(Account account, List<GameProfile> names) throws NoSelectedCharacterException { public GameProfile select(YggdrasilService service, List<GameProfile> profiles) throws NoSelectedCharacterException {
if (!(account instanceof YggdrasilAccount)) Platform.runLater(() -> {
return CharacterSelector.DEFAULT.select(account, names); for (GameProfile profile : profiles) {
YggdrasilAccount yggdrasilAccount = (YggdrasilAccount) account; 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) { IconedItem accountItem = new IconedItem(portraitView, profile.getName());
Image image; accountItem.setOnMouseClicked(e -> {
final int scaleRatio = 4; selectedProfile = profile;
try { latch.countDown();
image = AccountHelper.getSkinImmediately(yggdrasilAccount, profile, scaleRatio); });
} catch (Exception e) { listBox.add(accountItem);
Logging.LOG.log(Level.WARNING, "Failed to get skin for " + profile.getName(), e);
image = AccountHelper.getDefaultSkin(profile.getId(), scaleRatio);
} }
Controllers.dialog(this);
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));
try { try {
latch.await(); latch.await();
if (selectedProfile == null) if (selectedProfile == null)
throw new NoSelectedCharacterException(account); throw new NoSelectedCharacterException();
return selectedProfile; return selectedProfile;
} catch (InterruptedException ignore) { } catch (InterruptedException ignore) {
throw new NoSelectedCharacterException(account); throw new NoSelectedCharacterException();
} finally { } finally {
JFXUtilities.runInFX(() -> Selector.this.fireEvent(new DialogCloseEvent())); JFXUtilities.runInFX(() -> Selector.this.fireEvent(new DialogCloseEvent()));
} }
@@ -296,6 +319,8 @@ public class AddAccountPane extends StackPane {
return exception.getMessage(); return exception.getMessage();
} else if (exception instanceof AuthlibInjectorDownloadException) { } else if (exception instanceof AuthlibInjectorDownloadException) {
return i18n("account.failed.injector_download_failure"); return i18n("account.failed.injector_download_failure");
} else if (exception instanceof CharacterDeletedException) {
return i18n("account.failed.character_deleted");
} else if (exception.getClass() == AuthenticationException.class) { } else if (exception.getClass() == AuthenticationException.class) {
return exception.getLocalizedMessage(); return exception.getLocalizedMessage();
} else { } else {

View File

@@ -19,6 +19,8 @@ package org.jackhuang.hmcl.ui.account;
import com.jfoenix.controls.JFXButton; import com.jfoenix.controls.JFXButton;
import com.jfoenix.effects.JFXDepthManager; import com.jfoenix.effects.JFXDepthManager;
import javafx.beans.binding.Bindings;
import javafx.geometry.Pos; import javafx.geometry.Pos;
import javafx.scene.control.Label; import javafx.scene.control.Label;
import javafx.scene.layout.BorderPane; import javafx.scene.layout.BorderPane;
@@ -33,17 +35,17 @@ public final class AuthlibInjectorServerItem extends BorderPane {
private final AuthlibInjectorServer server; private final AuthlibInjectorServer server;
private final Label lblServerName = new Label(); 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) { public AuthlibInjectorServerItem(AuthlibInjectorServer server, Consumer<AuthlibInjectorServerItem> deleteCallback) {
this.server = server; this.server = server;
lblServerName.setStyle("-fx-font-size: 15;"); lblServerName.setStyle("-fx-font-size: 15;");
lblServerIp.setStyle("-fx-font-size: 10;"); lblServerUrl.setStyle("-fx-font-size: 10;");
VBox center = new VBox(); VBox center = new VBox();
BorderPane.setAlignment(center, Pos.CENTER); BorderPane.setAlignment(center, Pos.CENTER);
center.getChildren().addAll(lblServerName, lblServerIp); center.getChildren().addAll(lblServerName, lblServerUrl);
setCenter(center); setCenter(center);
JFXButton right = new JFXButton(); 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;"); setStyle("-fx-background-radius: 2; -fx-background-color: white; -fx-padding: 8;");
JFXDepthManager.setDepth(this, 1); JFXDepthManager.setDepth(this, 1);
lblServerName.setText(server.getName()); lblServerName.textProperty().bind(Bindings.createStringBinding(server::getName, server));
lblServerIp.setText(server.getUrl()); lblServerUrl.setText(server.getUrl());
} }
public AuthlibInjectorServer getServer() { public AuthlibInjectorServer getServer() {

View File

@@ -61,7 +61,7 @@ public class FileItem extends BorderPane {
right.setGraphic(SVG.pencil(Theme.blackFillBinding(), 15, 15)); right.setGraphic(SVG.pencil(Theme.blackFillBinding(), 15, 15));
right.getStyleClass().add("toggle-icon4"); right.getStyleClass().add("toggle-icon4");
right.setOnMouseClicked(e -> onExplore()); right.setOnMouseClicked(e -> onExplore());
FXUtils.installTooltip(right, i18n("button.edit")); FXUtils.installFastTooltip(right, i18n("button.edit"));
setRight(right); setRight(right);
Tooltip tip = new Tooltip(); Tooltip tip = new Tooltip();

View File

@@ -24,27 +24,27 @@ import static javafx.collections.FXCollections.singletonObservableList;
import org.jackhuang.hmcl.util.javafx.MultiStepBinding; import org.jackhuang.hmcl.util.javafx.MultiStepBinding;
import com.jfoenix.controls.JFXComboBox; import com.jfoenix.controls.JFXComboBox;
import com.jfoenix.controls.JFXListCell;
import javafx.beans.NamedArg; import javafx.beans.NamedArg;
import javafx.beans.binding.Bindings; import javafx.beans.binding.Bindings;
import javafx.scene.control.ListCell;
import javafx.scene.text.Font; import javafx.scene.text.Font;
public class FontComboBox extends JFXComboBox<String> { public class FontComboBox extends JFXComboBox<String> {
private boolean loaded = false; private boolean loaded = false;
public FontComboBox(@NamedArg(value = "fontSize", defaultValue = "12.0") double fontSize, public FontComboBox(@NamedArg(value = "fontSize", defaultValue = "12.0") double fontSize) {
@NamedArg(value = "enableStyle", defaultValue = "false") boolean enableStyle) {
styleProperty().bind(Bindings.concat("-fx-font-family: \"", valueProperty(), "\"")); styleProperty().bind(Bindings.concat("-fx-font-family: \"", valueProperty(), "\""));
setCellFactory(listView -> new ListCell<String>() { setCellFactory(listView -> new JFXListCell<String>() {
@Override @Override
protected void updateItem(String item, boolean empty) { public void updateItem(String item, boolean empty) {
super.updateItem(item, empty); super.updateItem(item, empty);
if (item != null) { if (!empty) {
setText(item); setText(item);
setFont(new Font(item, fontSize)); setGraphic(null);
setStyle("-fx-font-family: \"" + item + "\"");
} }
} }
}); });

View File

@@ -30,7 +30,7 @@ public class IconedMenuItem extends IconedItem {
} }
public IconedMenuItem addTooltip(String tooltip) { public IconedMenuItem addTooltip(String tooltip) {
FXUtils.installTooltip(this, tooltip); FXUtils.installFastTooltip(this, tooltip);
return this; return this;
} }
} }

View File

@@ -63,7 +63,7 @@ public final class ImagePickerItem extends BorderPane {
deleteButton.onMouseClickedProperty().bind(onDeleteButtonClicked); deleteButton.onMouseClickedProperty().bind(onDeleteButtonClicked);
deleteButton.getStyleClass().add("toggle-icon4"); deleteButton.getStyleClass().add("toggle-icon4");
FXUtils.installTooltip(selectButton, i18n("button.edit")); FXUtils.installFastTooltip(selectButton, i18n("button.edit"));
HBox hBox = new HBox(); HBox hBox = new HBox();
hBox.getChildren().setAll(imageView, selectButton, deleteButton); hBox.getChildren().setAll(imageView, selectButton, deleteButton);

View File

@@ -17,15 +17,11 @@
*/ */
package org.jackhuang.hmcl.ui.construct; package org.jackhuang.hmcl.ui.construct;
import com.jfoenix.controls.JFXListView;
import com.jfoenix.controls.JFXProgressBar; import com.jfoenix.controls.JFXProgressBar;
import javafx.application.Platform; import javafx.application.Platform;
import javafx.beans.binding.Bindings;
import javafx.beans.property.ReadOnlyIntegerProperty; import javafx.beans.property.ReadOnlyIntegerProperty;
import javafx.beans.property.ReadOnlyIntegerWrapper; import javafx.beans.property.ReadOnlyIntegerWrapper;
import javafx.geometry.Insets;
import javafx.scene.control.Label; import javafx.scene.control.Label;
import javafx.scene.control.ListCell;
import javafx.scene.layout.BorderPane; import javafx.scene.layout.BorderPane;
import javafx.scene.layout.StackPane; import javafx.scene.layout.StackPane;
import org.jackhuang.hmcl.download.forge.ForgeInstallTask; import org.jackhuang.hmcl.download.forge.ForgeInstallTask;
@@ -45,16 +41,15 @@ import java.util.Map;
import static org.jackhuang.hmcl.util.i18n.I18n.i18n; import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
public final class TaskListPane extends StackPane { public final class TaskListPane extends StackPane {
private final JFXListView<Task> listBox = new JFXListView<>(); private final AdvancedListBox listBox = new AdvancedListBox();
private final Map<Task, ProgressListNode> nodes = new HashMap<>(); private final Map<Task, ProgressListNode> nodes = new HashMap<>();
private final ReadOnlyIntegerWrapper finishedTasks = new ReadOnlyIntegerWrapper(); private final ReadOnlyIntegerWrapper finishedTasks = new ReadOnlyIntegerWrapper();
private final ReadOnlyIntegerWrapper totTasks = new ReadOnlyIntegerWrapper(); private final ReadOnlyIntegerWrapper totTasks = new ReadOnlyIntegerWrapper();
public TaskListPane() { public TaskListPane() {
getChildren().setAll(listBox); listBox.setSpacing(0);
listBox.setPadding(Insets.EMPTY); getChildren().setAll(listBox);
listBox.setCellFactory(listView -> new ProgressListNode());
} }
public ReadOnlyIntegerProperty finishedTasksProperty() { public ReadOnlyIntegerProperty finishedTasksProperty() {
@@ -70,7 +65,7 @@ public final class TaskListPane extends StackPane {
@Override @Override
public void onStart() { public void onStart() {
Platform.runLater(() -> { Platform.runLater(() -> {
listBox.getItems().clear(); listBox.clear();
finishedTasks.set(0); finishedTasks.set(0);
totTasks.set(0); totTasks.set(0);
}); });
@@ -112,66 +107,64 @@ public final class TaskListPane extends StackPane {
task.setName(i18n("modpack.scan")); task.setName(i18n("modpack.scan"));
} }
Platform.runLater(() -> listBox.getItems().add(task)); ProgressListNode node = new ProgressListNode(task);
nodes.put(task, node);
Platform.runLater(() -> listBox.add(node));
} }
@Override @Override
public void onFinished(Task task) { public void onFinished(Task task) {
ProgressListNode node = nodes.remove(task);
if (node == null)
return;
node.unbind();
Platform.runLater(() -> { Platform.runLater(() -> {
if (listBox.getItems().remove(task)) listBox.remove(node);
finishedTasks.set(finishedTasks.getValue() + 1); 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);
}); });
} }
}); });
} }
private static class ProgressListNode extends ListCell<Task> { private static class ProgressListNode extends BorderPane {
private final BorderPane borderPane = new BorderPane();
private final JFXProgressBar bar = new JFXProgressBar(); private final JFXProgressBar bar = new JFXProgressBar();
private final Label title = new Label(); private final Label title = new Label();
private final Label state = new Label(); private final Label state = new Label();
{ public ProgressListNode(Task task) {
borderPane.setLeft(title); bar.progressProperty().bind(task.progressProperty());
borderPane.setRight(state); title.setText(task.getName());
borderPane.setBottom(bar); state.textProperty().bind(task.messageProperty());
borderPane.setMinWidth(0);
borderPane.setPrefWidth(1);
setPadding(Insets.EMPTY); setLeft(title);
setRight(state);
setBottom(bar);
bar.minWidthProperty().bind(widthProperty()); bar.minWidthProperty().bind(widthProperty());
bar.prefWidthProperty().bind(widthProperty()); bar.prefWidthProperty().bind(widthProperty());
bar.maxWidthProperty().bind(widthProperty()); bar.maxWidthProperty().bind(widthProperty());
} }
@Override public void unbind() {
protected void updateItem(Task item, boolean empty) { bar.progressProperty().unbind();
boolean wasEmpty = isEmpty(); state.textProperty().unbind();
Task oldTask = getItem(); }
if (!wasEmpty && oldTask != null) { public void setThrowable(Throwable throwable) {
bar.progressProperty().unbind(); unbind();
state.textProperty().unbind(); state.setText(throwable.getLocalizedMessage());
} bar.setProgress(0);
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());
}
} }
} }
} }

View File

@@ -19,32 +19,15 @@ package org.jackhuang.hmcl.ui.construct;
import javafx.beans.property.SimpleStringProperty; import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.StringProperty; 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.control.Label;
import javafx.scene.layout.StackPane;
import javafx.scene.layout.VBox; 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 VBox {
public class TwoLineListItem extends StackPane {
private static final String DEFAULT_STYLE_CLASS = "two-line-list-item"; private static final String DEFAULT_STYLE_CLASS = "two-line-list-item";
private final StringProperty title = new SimpleStringProperty(this, "title"); private final StringProperty title = new SimpleStringProperty(this, "title");
private final StringProperty subtitle = new SimpleStringProperty(this, "subtitle"); 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) { public TwoLineListItem(String titleString, String subtitleString) {
this(); this();
@@ -55,19 +38,14 @@ public class TwoLineListItem extends StackPane {
public TwoLineListItem() { public TwoLineListItem() {
setMouseTransparent(true); setMouseTransparent(true);
Label lblTitle = new Label(); Label lblTitle = new Label();
lblTitle.textFillProperty().bind(titleFill); lblTitle.getStyleClass().add("title");
lblTitle.fontProperty().bind(titleFont);
lblTitle.textProperty().bind(title); lblTitle.textProperty().bind(title);
Label lblSubtitle = new Label(); Label lblSubtitle = new Label();
lblSubtitle.textFillProperty().bind(subtitleFill); lblSubtitle.getStyleClass().add("subtitle");
lblSubtitle.fontProperty().bind(subtitleFont);
lblSubtitle.textProperty().bind(subtitle); lblSubtitle.textProperty().bind(subtitle);
VBox vbox = new VBox(); getChildren().setAll(lblTitle, lblSubtitle);
vbox.getChildren().setAll(lblTitle, lblSubtitle);
getChildren().setAll(vbox);
getStyleClass().add(DEFAULT_STYLE_CLASS); getStyleClass().add(DEFAULT_STYLE_CLASS);
} }
@@ -95,74 +73,8 @@ public class TwoLineListItem extends StackPane {
this.subtitle.set(subtitle); 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 @Override
public String toString() { public String toString() {
return getTitle(); 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);
}
} }

View File

@@ -18,6 +18,7 @@
package org.jackhuang.hmcl.ui.download; package org.jackhuang.hmcl.ui.download;
import javafx.scene.Node; import javafx.scene.Node;
import org.jackhuang.hmcl.download.game.LibraryDownloadException;
import org.jackhuang.hmcl.game.ModpackHelper; import org.jackhuang.hmcl.game.ModpackHelper;
import org.jackhuang.hmcl.mod.CurseCompletionException; import org.jackhuang.hmcl.mod.CurseCompletionException;
import org.jackhuang.hmcl.mod.MismatchedModpackTypeException; import org.jackhuang.hmcl.mod.MismatchedModpackTypeException;
@@ -111,6 +112,8 @@ public class ModpackInstallWizardProvider implements WizardProvider {
} else { } else {
Controllers.dialog(i18n("modpack.type.curse.tolerable_error"), i18n("install.success"), MessageBox.INFORMATION_MESSAGE, next); Controllers.dialog(i18n("modpack.type.curse.tolerable_error"), i18n("install.success"), MessageBox.INFORMATION_MESSAGE, next);
} }
} else if (exception instanceof LibraryDownloadException) {
Controllers.dialog(i18n("launch.failed.download_library", ((LibraryDownloadException) exception).getLibrary().getName()) + "\n" + StringUtils.getStackTrace(exception.getCause()), i18n("install.failed.downloading"), MessageBox.ERROR_MESSAGE, next);
} else if (exception instanceof DownloadException) { } else if (exception instanceof DownloadException) {
Controllers.dialog(StringUtils.getStackTrace(exception), i18n("install.failed.downloading"), MessageBox.ERROR_MESSAGE, next); Controllers.dialog(StringUtils.getStackTrace(exception), i18n("install.failed.downloading"), MessageBox.ERROR_MESSAGE, next);
} else { } else {

View File

@@ -21,6 +21,7 @@ import javafx.scene.Node;
import org.jackhuang.hmcl.download.DownloadProvider; import org.jackhuang.hmcl.download.DownloadProvider;
import org.jackhuang.hmcl.download.GameBuilder; import org.jackhuang.hmcl.download.GameBuilder;
import org.jackhuang.hmcl.download.RemoteVersion; import org.jackhuang.hmcl.download.RemoteVersion;
import org.jackhuang.hmcl.download.game.LibraryDownloadException;
import org.jackhuang.hmcl.setting.Profile; import org.jackhuang.hmcl.setting.Profile;
import org.jackhuang.hmcl.task.DownloadException; import org.jackhuang.hmcl.task.DownloadException;
import org.jackhuang.hmcl.task.Schedulers; import org.jackhuang.hmcl.task.Schedulers;
@@ -73,7 +74,9 @@ public final class VanillaInstallWizardProvider implements WizardProvider {
settings.put("failure_callback", new FailureCallback() { settings.put("failure_callback", new FailureCallback() {
@Override @Override
public void onFail(Map<String, Object> settings, Exception exception, Runnable next) { public void onFail(Map<String, Object> settings, Exception exception, Runnable next) {
if (exception instanceof DownloadException) { if (exception instanceof LibraryDownloadException) {
Controllers.dialog(i18n("launch.failed.download_library", ((LibraryDownloadException) exception).getLibrary().getName()) + "\n" + StringUtils.getStackTrace(exception.getCause()), i18n("install.failed.downloading"), MessageBox.ERROR_MESSAGE, next);
} else if (exception instanceof DownloadException) {
Controllers.dialog(StringUtils.getStackTrace(exception), i18n("install.failed.downloading"), MessageBox.ERROR_MESSAGE, next); Controllers.dialog(StringUtils.getStackTrace(exception), i18n("install.failed.downloading"), MessageBox.ERROR_MESSAGE, next);
} else { } else {
Controllers.dialog(StringUtils.getStackTrace(exception), i18n("install.failed"), MessageBox.ERROR_MESSAGE, next); Controllers.dialog(StringUtils.getStackTrace(exception), i18n("install.failed"), MessageBox.ERROR_MESSAGE, next);

View File

@@ -44,7 +44,7 @@ public class DatapackListItem extends BorderPane {
setCenter(modItem); setCenter(modItem);
JFXButton btnRemove = new JFXButton(); JFXButton btnRemove = new JFXButton();
FXUtils.installTooltip(btnRemove, i18n("datapack.remove")); FXUtils.installFastTooltip(btnRemove, i18n("datapack.remove"));
btnRemove.setOnMouseClicked(e -> deleteCallback.accept(this)); btnRemove.setOnMouseClicked(e -> deleteCallback.accept(this));
btnRemove.getStyleClass().add("toggle-icon4"); btnRemove.getStyleClass().add("toggle-icon4");
BorderPane.setAlignment(btnRemove, Pos.CENTER); BorderPane.setAlignment(btnRemove, Pos.CENTER);

View File

@@ -73,7 +73,7 @@ public class GameListItemSkin extends SkinBase<GameListItem> {
btnUpgrade.setOnMouseClicked(e -> skinnable.update()); btnUpgrade.setOnMouseClicked(e -> skinnable.update());
btnUpgrade.getStyleClass().add("toggle-icon4"); btnUpgrade.getStyleClass().add("toggle-icon4");
btnUpgrade.setGraphic(SVG.update(Theme.blackFillBinding(), -1, -1)); 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); right.getChildren().add(btnUpgrade);
} }

View File

@@ -47,7 +47,7 @@ public final class ModItem extends BorderPane {
JFXButton btnRemove = new JFXButton(); JFXButton btnRemove = new JFXButton();
JFXUtilities.runInFX(() -> { JFXUtilities.runInFX(() -> {
FXUtils.installTooltip(btnRemove, i18n("mods.remove")); FXUtils.installFastTooltip(btnRemove, i18n("mods.remove"));
}); });
btnRemove.setOnMouseClicked(e -> deleteCallback.accept(this)); btnRemove.setOnMouseClicked(e -> deleteCallback.accept(this));
btnRemove.getStyleClass().add("toggle-icon4"); btnRemove.getStyleClass().add("toggle-icon4");

View File

@@ -99,13 +99,13 @@ public final class VersionPage extends StackPane implements DecoratorPage {
new IconedMenuItem(null, i18n("version.manage.clean"), FXUtils.withJFXPopupClosing(() -> Versions.cleanVersion(profile, version), managementPopup)).addTooltip(i18n("version.manage.clean.tooltip")) 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.installFastTooltip(btnDelete, i18n("version.manage.remove"));
FXUtils.installTooltip(btnBrowseMenu, i18n("settings.game.exploration")); FXUtils.installFastTooltip(btnBrowseMenu, i18n("settings.game.exploration"));
FXUtils.installTooltip(btnManagementMenu, i18n("settings.game.management")); FXUtils.installFastTooltip(btnManagementMenu, i18n("settings.game.management"));
FXUtils.installTooltip(btnExport, i18n("modpack.export")); FXUtils.installFastTooltip(btnExport, i18n("modpack.export"));
btnTestGame.setGraphic(SVG.launch(Theme.whiteFillBinding(), 20, 20)); 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); setEventHandler(Navigator.NavigationEvent.NAVIGATED, this::onNavigated);
} }

View File

@@ -35,6 +35,7 @@ import javafx.scene.layout.StackPane;
import javafx.scene.layout.VBox; import javafx.scene.layout.VBox;
import javafx.stage.FileChooser; import javafx.stage.FileChooser;
import org.jackhuang.hmcl.setting.EnumGameDirectory; import org.jackhuang.hmcl.setting.EnumGameDirectory;
import org.jackhuang.hmcl.setting.LauncherVisibility;
import org.jackhuang.hmcl.setting.Profile; import org.jackhuang.hmcl.setting.Profile;
import org.jackhuang.hmcl.setting.Profiles; import org.jackhuang.hmcl.setting.Profiles;
import org.jackhuang.hmcl.setting.VersionSetting; import org.jackhuang.hmcl.setting.VersionSetting;
@@ -60,6 +61,7 @@ import java.util.List;
import java.util.logging.Level; import java.util.logging.Level;
import java.util.stream.Collectors; import java.util.stream.Collectors;
import static org.jackhuang.hmcl.ui.FXUtils.stringConverter;
import static org.jackhuang.hmcl.util.i18n.I18n.i18n; import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
public final class VersionSettingsPage extends StackPane implements DecoratorPage { 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 advancedSettingsPane;
@FXML private ComponentList componentList; @FXML private ComponentList componentList;
@FXML private ComponentList iconPickerItemWrapper; @FXML private ComponentList iconPickerItemWrapper;
@FXML private JFXComboBox<?> cboLauncherVisibility; @FXML private JFXComboBox<LauncherVisibility> cboLauncherVisibility;
@FXML private JFXCheckBox chkFullscreen; @FXML private JFXCheckBox chkFullscreen;
@FXML private Label lblPhysicalMemory; @FXML private Label lblPhysicalMemory;
@FXML private JFXToggleButton chkNoJVMArgs; @FXML private JFXToggleButton chkNoJVMArgs;
@@ -100,6 +102,9 @@ public final class VersionSettingsPage extends StackPane implements DecoratorPag
public VersionSettingsPage() { public VersionSettingsPage() {
FXUtils.loadFXML(this, "/assets/fxml/version/version-settings.fxml"); 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 @FXML

View File

@@ -81,8 +81,7 @@ public class Versions {
public static void updateGameAssets(Profile profile, String version) { public static void updateGameAssets(Profile profile, String version) {
Version resolvedVersion = profile.getRepository().getResolvedVersion(version); Version resolvedVersion = profile.getRepository().getResolvedVersion(version);
TaskExecutor executor = new GameAssetIndexDownloadTask(profile.getDependency(), resolvedVersion) TaskExecutor executor = new GameAssetDownloadTask(profile.getDependency(), resolvedVersion, GameAssetDownloadTask.DOWNLOAD_INDEX_FORCIBLY)
.then(new GameAssetDownloadTask(profile.getDependency(), resolvedVersion))
.executor(); .executor();
Controllers.taskDialog(executor, i18n("version.manage.redownload_assets_index")); Controllers.taskDialog(executor, i18n("version.manage.redownload_assets_index"));
executor.start(); executor.start();

View File

@@ -103,7 +103,10 @@ public class CrashReporter implements Thread.UncaughtExceptionHandler {
"-- System Details --\n" + "-- System Details --\n" +
" Operating System: " + System.getProperty("os.name") + ' ' + OperatingSystem.SYSTEM_VERSION + "\n" + " Operating System: " + System.getProperty("os.name") + ' ' + OperatingSystem.SYSTEM_VERSION + "\n" +
" Java Version: " + System.getProperty("java.version") + ", " + System.getProperty("java.vendor") + "\n" + " Java Version: " + System.getProperty("java.version") + ", " + System.getProperty("java.vendor") + "\n" +
" Java VM Version: " + System.getProperty("java.vm.name") + " (" + System.getProperty("java.vm.info") + "), " + System.getProperty("java.vm.vendor") + "\n"; " Java VM Version: " + System.getProperty("java.vm.name") + " (" + System.getProperty("java.vm.info") + "), " + System.getProperty("java.vm.vendor") + "\n" +
" JVM Max Memory: " + Runtime.getRuntime().maxMemory() + "\n" +
" JVM Total Memory: " + Runtime.getRuntime().totalMemory() + "\n" +
" JVM Free Memory: " + Runtime.getRuntime().freeMemory() + "\n";
LOG.log(Level.SEVERE, text); LOG.log(Level.SEVERE, text);

View File

@@ -104,20 +104,24 @@
-fx-padding: 4 0 4 0; -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 { .bubble {
-fx-background-color: gray; -fx-background-color: gray;
-fx-background-radius: 2px; -fx-background-radius: 2px;
-fx-text-fill: white;
} }
.bubble .two-line-list-item { .bubble .two-line-list-item > .title,
-jfx-title-fill: white; .bubble .two-line-list-item > .subtitle {
-jfx-subtitle-fill: white; -fx-text-fill: white;
}
.two-line-list-item {
-jfx-title-font-size: 15px;
-jfx-title-fill: black;
-jfx-subtitle-fill: gray;
} }
.window-title-bar .separator { .window-title-bar .separator {
@@ -338,7 +342,6 @@
.jfx-tool-bar HBox { .jfx-tool-bar HBox {
-fx-alignment: center; -fx-alignment: center;
/* -fx-spacing: 25.0;*/
-fx-padding: 0.0 5.0; -fx-padding: 0.0 5.0;
} }
@@ -443,33 +446,6 @@
-fx-fill: -fx-base-check-color; -fx-fill: -fx-base-check-color;
} }
.custom-jfx-radio-button {
-fx-font-size: 16.0px;
}
.custom-jfx-radio-button .radio {
-fx-stroke-width: 2.0px;
-fx-fill: transparent;
}
.custom-jfx-radio-button-blue {
-fx-text-fill: -fx-base-color;
-jfx-selected-color: -fx-base-color;
-jfx-unselected-color: #212121;
}
.custom-jfx-radio-button-red {
-fx-text-fill: #f44336;
-jfx-selected-color: #f44336;
-jfx-unselected-color: #b71c1c;
}
.custom-jfx-radio-button-green {
-fx-text-fill: #4caf50;
-jfx-selected-color: #4caf50;
-jfx-unselected-color: #1b5e20;
}
/******************************************************************************* /*******************************************************************************
* * * *
* JFX Slider * * JFX Slider *
@@ -510,18 +486,12 @@
-fx-font-size: 10.0; -fx-font-size: 10.0;
} }
/******************************************************/
/******************************************************************************* /*******************************************************************************
* * * *
* JFX Rippler * * JFX Rippler *
* * * *
*******************************************************************************/ *******************************************************************************/
/*.jfx-rippler {
-fx-rippler-fill: -fx-base-color;
-fx-mask-type: RECT;
}*/
.jfx-rippler:hover { .jfx-rippler:hover {
-fx-cursor: hand; -fx-cursor: hand;
} }
@@ -532,62 +502,24 @@
* * * *
*******************************************************************************/ *******************************************************************************/
.custom-jfx-button-raised .jfx-rippler {
-jfx-rippler-fill: YELLOW;
}
.custom-jfx-button-raised {
-fx-padding: 0.7em 0.57em;
-fx-font-size: 14.0px;
-jfx-button-type: RAISED;
-fx-background-color: rgb(102.0, 153.0, 102.0);
-fx-pref-width: 200.0;
-fx-text-fill: WHITE;
}
.circle-jfx-button-raised .jfx-rippler {
-jfx-rippler-fill: YELLOW;
}
.circle-jfx-button-raised {
-fx-padding: 0.7em 0.57em;
-fx-font-size: 14.0px;
-jfx-button-type: RAISED;
-fx-background-color: rgb(102.0, 153.0, 102.0);
-fx-pref-width: 200.0;
-fx-text-fill: WHITE;
-jfx-mask-type: CIRCLE;
}
.jfx-button-raised { .jfx-button-raised {
-fx-text-fill: white;
-fx-background-color: -fx-base-color; -fx-background-color: -fx-base-color;
-fx-font-size: 14px;
} }
.jfx-button-raised .jfx-rippler { .jfx-button-raised, .jfx-button-raised * {
-jfx-rippler-fill: white; -fx-text-fill: -fx-base-text-fill;
}
.jfx-button-raised .label {
-fx-text-fill: white;
-fx-font-size: 14px; -fx-font-size: 14px;
} }
.jfx-button-border { .jfx-button-border {
-fx-text-fill: -fx-base-color;
-fx-border-color: gray; -fx-border-color: gray;
-fx-border-radius: 5px; -fx-border-radius: 5px;
-fx-border-width: 0.2px; -fx-border-width: 0.2px;
-fx-padding: 8px; -fx-padding: 8px;
} }
.jfx-button-border .jfx-rippler { .jfx-button-border, .jfx-button-border * {
-jfx-rippler-fill: -fx-base-check-color; -fx-text-fill: -fx-base-darker-color;
}
.jfx-button-border .label {
-fx-text-fill: -fx-base-color;
} }
.jfx-button-raised-round { .jfx-button-raised-round {
@@ -606,20 +538,6 @@
-jfx-checked-color: -fx-base-check-color; -jfx-checked-color: -fx-base-check-color;
} }
.custom-jfx-check-box {
-jfx-checked-color: RED;
}
.custom-jfx-check-box-all-colored {
-jfx-checked-color: -fx-base-color;
-jfx-unchecked-color: -fx-base-color;
-fx-text-fill: -fx-base-color;
}
.custom-jfx-check-box-text-colored {
-fx-text-fill: rgb(153.0, 0.0, 0.0);
}
/******************************************************************************* /*******************************************************************************
* * * *
* JFX Progress Bar * * JFX Progress Bar *
@@ -643,15 +561,6 @@
-fx-background-color: -fx-base-check-color; -fx-background-color: -fx-base-check-color;
} }
.custom-jfx-progress-bar > .bar {
-fx-background-color: rgb(255.0, 128.0, 0.0);
}
.custom-jfx-progress-bar-stroke > .bar {
-fx-background-color: -fx-base-color;
-fx-padding: 6;
}
/******************************************************************************* /*******************************************************************************
* * * *
* JFX Textfield * * JFX Textfield *
@@ -678,20 +587,12 @@
* * * *
*******************************************************************************/ *******************************************************************************/
.jfx-list-cell:odd, .jfx-list-cell, .list-cell {
.jfx-list-cell:even,
.list-cell:odd,
.list-cell:even {
-fx-background-color: WHITE; -fx-background-color: WHITE;
} }
.list-cell:selected, .jfx-list-cell:selected { .list-cell:selected, .jfx-list-cell:selected,
-fx-background-insets: 0.0; .list-cell:hover, .jfx-list-cell:hover {
-fx-text-fill: BLACK;
}
.jfx-list-cell:filled:hover,
.jfx-list-cell:selected .label {
-fx-text-fill: black; -fx-text-fill: black;
} }
@@ -712,47 +613,6 @@
-jfx-expanded: false; -jfx-expanded: false;
} }
.custom-jfx-list-view .jfx-list-cell:odd:selected > .jfx-rippler > StackPane,
.custom-jfx-list-view .jfx-list-cell:even:selected > .jfx-rippler > StackPane {
-fx-background-color: rgba(255, 0, 0, 0.2);
}
.custom-jfx-list-view {
-fx-background-insets: 0.0;
-jfx-cell-horizontal-margin: 0.0;
-jfx-cell-vertical-margin: 5.0;
-jfx-expanded: false;
-fx-max-width: 200.0px;
/* important to hide the list change of height */
-fx-background-color: TRANSPARENT;
}
.custom-jfx-list-view .jfx-rippler {
-jfx-rippler-fill: RED;
}
.custom-jfx-list-view1 {
-jfx-vertical-gap: 10.0;
-fx-pref-width: 150px;
-fx-background-color: transparent;
}
.custom-jfx-list-view-icon,
.jfx-list-cell:selected .label .custom-jfx-list-view-icon {
-fx-fill: -fx-base-color;
-fx-padding: 0.0 10.0 0.0 5.0;
-fx-cursor: hand;
}
.custom-jfx-list-view-icon-container {
-fx-pref-width: 40px;
}
.custom-jfx-list-view .sublist-item {
-fx-border-color: #e0e0e0;
-fx-border-width: 1 0 1 0;
}
.options-list { .options-list {
-fx-background-color: transparent; -fx-background-color: transparent;
-fx-effect: dropshadow(gaussian, rgba(0, 0, 0, 0.26), 5, 0.06, -0.5, 1); -fx-effect: dropshadow(gaussian, rgba(0, 0, 0, 0.26), 5, 0.06, -0.5, 1);
@@ -802,20 +662,6 @@
-jfx-mask-type: CIRCLE; -jfx-mask-type: CIRCLE;
} }
.custom-jfx-list-view .jfx-list-cell .sublist-header > .drop-icon {
-fx-background-color: GRAY;
}
.custom-jfx-list-view .jfx-list-cell:filled:hover .sublist-header > .drop-icon {
-fx-background-color: BLACK;
}
/*******************************************************************************/
/*******************************************************************************/
/*******************************************************************************/
/*******************************************************************************/
/*******************************************************************************/
/*******************************************************************************/
/******************************************************************************* /*******************************************************************************
* * * *
* JFX SUBLIST IMPORTANT * * JFX SUBLIST IMPORTANT *
@@ -855,10 +701,6 @@
-fx-padding: 0 0 0 12; -fx-padding: 0 0 0 12;
} }
/*.custom-jfx-list-view .sublist-container {
-fx-padding : 0 0 5 0;
}*/
/******************************************************************************* /*******************************************************************************
* * * *
* JFX Toggle Button * * JFX Toggle Button *
@@ -869,14 +711,6 @@
-jfx-toggle-color: -fx-base-check-color; -jfx-toggle-color: -fx-base-check-color;
} }
.custom-jfx-toggle-button {
-jfx-toggle-color: #4285F4;
}
.custom-jfx-toggle-button-red {
-jfx-toggle-color: red;
}
.toggle-label { .toggle-label {
-fx-font-size: 14.0px; -fx-font-size: 14.0px;
} }
@@ -1109,31 +943,10 @@
-fx-fill: #D34336; -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 { .combo-box-popup .list-view .jfx-list-cell {
-fx-background-insets: 0.0; -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 { .combo-box-popup .list-view .jfx-list-cell .jfx-rippler {
-jfx-rippler-fill: -fx-base-color; -jfx-rippler-fill: -fx-base-color;
} }
@@ -1387,12 +1200,3 @@
.fit-width { .fit-width {
-fx-pref-width: 100%; -fx-pref-width: 100%;
} }
/*
.jfx-scroll-pane .main-header {
-fx-background-image: url("../bg1.jpg");
}
.jfx-scroll-pane .condensed-header {
-fx-background-image: url("../bg4.jpg");
}
*/

View File

@@ -32,6 +32,7 @@
<JFXComboBox fx:id="cboServers" promptText="%account.injector.empty" maxHeight="25" GridPane.columnIndex="1" GridPane.rowIndex="1"/> <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 GridPane.columnIndex="2" GridPane.rowIndex="1" spacing="8">
<HBox fx:id="linksContainer" alignment="CENTER_LEFT"/>
<JFXButton fx:id="btnAddServer" styleClass="toggle-icon4" onMouseClicked="#onAddInjecterServer"> <JFXButton fx:id="btnAddServer" styleClass="toggle-icon4" onMouseClicked="#onAddInjecterServer">
<graphic> <graphic>
<javafx.scene.shape.SVGPath content="M19,13H13V19H11V13H5V11H11V5H13V11H19V13Z" /> <javafx.scene.shape.SVGPath content="M19,13H13V19H11V13H5V11H11V5H13V11H19V13Z" />

View File

@@ -60,16 +60,7 @@
<Label text="%settings.advanced.launcher_visible" BorderPane.alignment="CENTER_LEFT"/> <Label text="%settings.advanced.launcher_visible" BorderPane.alignment="CENTER_LEFT"/>
</left> </left>
<right> <right>
<JFXComboBox fx:id="cboLauncherVisibility" BorderPane.alignment="CENTER_RIGHT" FXUtils.limitWidth="300"> <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>
</right> </right>
</BorderPane> </BorderPane>

View File

@@ -36,6 +36,7 @@ account.character=character
account.choose=Choose a character account.choose=Choose a character
account.create=Create a new account account.create=Create a new account
account.email=Email 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_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.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. 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=Manage authentication servers
account.injector.manage.title=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.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=Auth Server
account.injector.server_url=Server URL account.injector.server_url=Server URL
account.injector.server_name=Server Name account.injector.server_name=Server Name
@@ -307,7 +309,7 @@ settings.advanced.java_permanent_generation_space=PermGen Space/MB
settings.advanced.jvm_args=Java VM Arguments settings.advanced.jvm_args=Java VM Arguments
settings.advanced.launcher_visibility.close=Close the launcher when the game launched. 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=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_visibility.keep=Keep the launcher visible.
settings.advanced.launcher_visible=Launcher Visibility settings.advanced.launcher_visible=Launcher Visibility
settings.advanced.minecraft_arguments=Minecraft Arguments settings.advanced.minecraft_arguments=Minecraft Arguments

View File

@@ -35,6 +35,7 @@ account.character=角色
account.choose=選擇一個角色 account.choose=選擇一個角色
account.create=建立帳戶 account.create=建立帳戶
account.email=電子信箱 account.email=電子信箱
account.failed.character_deleted=此角色已被刪除
account.failed.connect_authentication_server=無法連接認證伺服器,可能是網路問題 account.failed.connect_authentication_server=無法連接認證伺服器,可能是網路問題
account.failed.connect_injector_server=無法連接認證伺服器,可能是網路故障或 URL 輸入錯誤 account.failed.connect_injector_server=無法連接認證伺服器,可能是網路故障或 URL 輸入錯誤
account.failed.injector_download_failure=無法下載 authlib-injector請檢查網路或嘗試切換下載源 account.failed.injector_download_failure=無法下載 authlib-injector請檢查網路或嘗試切換下載源
@@ -48,6 +49,7 @@ account.injector.empty=無(點擊右側加號添加)
account.injector.manage=管理認證伺服器 account.injector.manage=管理認證伺服器
account.injector.manage.title=認證伺服器 account.injector.manage.title=認證伺服器
account.injector.http=警告:此伺服器使用不安全的 HTTP 協議,您的密碼在登入時會被明文傳輸。 account.injector.http=警告:此伺服器使用不安全的 HTTP 協議,您的密碼在登入時會被明文傳輸。
account.injector.link.register=註冊
account.injector.server=認證伺服器 account.injector.server=認證伺服器
account.injector.server_url=伺服器位址 account.injector.server_url=伺服器位址
account.injector.server_name=伺服器名稱 account.injector.server_name=伺服器名稱
@@ -306,7 +308,7 @@ settings.advanced.java_permanent_generation_space=記憶體永久儲存區域(
settings.advanced.jvm_args=Java 虛擬機參數(不必填寫) settings.advanced.jvm_args=Java 虛擬機參數(不必填寫)
settings.advanced.launcher_visibility.close=遊戲啟動後結束啟動器 settings.advanced.launcher_visibility.close=遊戲啟動後結束啟動器
settings.advanced.launcher_visibility.hide=遊戲啟動後隱藏啟動器 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_visibility.keep=不隱藏啟動器
settings.advanced.launcher_visible=啟動器可見性 settings.advanced.launcher_visible=啟動器可見性
settings.advanced.minecraft_arguments=Minecraft 額外參數(不必填寫) settings.advanced.minecraft_arguments=Minecraft 額外參數(不必填寫)

View File

@@ -35,6 +35,7 @@ account.character=角色
account.choose=选择一个角色 account.choose=选择一个角色
account.create=新建账户 account.create=新建账户
account.email=邮箱 account.email=邮箱
account.failed.character_deleted=此角色已被删除
account.failed.connect_authentication_server=无法连接认证服务器,可能是网络问题 account.failed.connect_authentication_server=无法连接认证服务器,可能是网络问题
account.failed.connect_injector_server=无法连接认证服务器,可能是网络故障或 URL 输入错误 account.failed.connect_injector_server=无法连接认证服务器,可能是网络故障或 URL 输入错误
account.failed.injector_download_failure=无法下载 authlib-injector请检查网络或尝试切换下载源 account.failed.injector_download_failure=无法下载 authlib-injector请检查网络或尝试切换下载源
@@ -48,6 +49,7 @@ account.injector.empty=无(点击右侧加号添加)
account.injector.manage=管理认证服务器 account.injector.manage=管理认证服务器
account.injector.manage.title=认证服务器 account.injector.manage.title=认证服务器
account.injector.http=警告:此服务器使用不安全的 HTTP 协议,您的密码在登录时会被明文传输。 account.injector.http=警告:此服务器使用不安全的 HTTP 协议,您的密码在登录时会被明文传输。
account.injector.link.register=注册
account.injector.server=认证服务器 account.injector.server=认证服务器
account.injector.server_url=服务器地址 account.injector.server_url=服务器地址
account.injector.server_name=服务器名称 account.injector.server_name=服务器名称
@@ -306,7 +308,7 @@ settings.advanced.java_permanent_generation_space=内存永久保存区域(不
settings.advanced.jvm_args=Java 虚拟机参数(不必填写) settings.advanced.jvm_args=Java 虚拟机参数(不必填写)
settings.advanced.launcher_visibility.close=游戏启动后结束启动器 settings.advanced.launcher_visibility.close=游戏启动后结束启动器
settings.advanced.launcher_visibility.hide=游戏启动后隐藏启动器 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_visibility.keep=保持启动器可见
settings.advanced.launcher_visible=启动器可见性 settings.advanced.launcher_visible=启动器可见性
settings.advanced.minecraft_arguments=Minecraft 额外参数(不必填写) settings.advanced.minecraft_arguments=Minecraft 额外参数(不必填写)

View File

@@ -69,7 +69,8 @@ public abstract class Account implements Observable {
public abstract Map<Object, Object> toStorage(); public abstract Map<Object, Object> toStorage();
public abstract void clearCache(); public void clearCache() {
}
private ObservableHelper helper = new ObservableHelper(this); private ObservableHelper helper = new ObservableHelper(this);

View File

@@ -17,27 +17,10 @@
*/ */
package org.jackhuang.hmcl.auth; 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 { public final class CharacterDeletedException extends AuthenticationException {
private UUID uuid; public CharacterDeletedException() {
/**
* 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));
} }
} }

View File

@@ -18,6 +18,7 @@
package org.jackhuang.hmcl.auth; package org.jackhuang.hmcl.auth;
import org.jackhuang.hmcl.auth.yggdrasil.GameProfile; import org.jackhuang.hmcl.auth.yggdrasil.GameProfile;
import org.jackhuang.hmcl.auth.yggdrasil.YggdrasilService;
import java.util.List; 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. * @throws NoSelectedCharacterException if cannot select any character may because user close the selection window or cancel the selection.
* @return your choice of game profile. * @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));
} }

View File

@@ -22,13 +22,6 @@ package org.jackhuang.hmcl.auth;
* (A account may hold more than one characters.) * (A account may hold more than one characters.)
*/ */
public final class NoCharacterException extends AuthenticationException { public final class NoCharacterException extends AuthenticationException {
private final Account account; public NoCharacterException() {
public NoCharacterException(Account account) {
this.account = account;
}
public Account getAccount() {
return account;
} }
} }

View File

@@ -25,17 +25,6 @@ package org.jackhuang.hmcl.auth;
* @author huangyuhui * @author huangyuhui
*/ */
public final class NoSelectedCharacterException extends AuthenticationException { public final class NoSelectedCharacterException extends AuthenticationException {
private final Account account; public NoSelectedCharacterException() {
/**
*
* @param account the error yggdrasil account.
*/
public NoSelectedCharacterException(Account account) {
this.account = account;
}
public Account getAccount() {
return account;
} }
} }

View File

@@ -22,7 +22,6 @@ import org.jackhuang.hmcl.auth.AuthenticationException;
import org.jackhuang.hmcl.auth.CharacterSelector; import org.jackhuang.hmcl.auth.CharacterSelector;
import org.jackhuang.hmcl.auth.ServerDisconnectException; import org.jackhuang.hmcl.auth.ServerDisconnectException;
import org.jackhuang.hmcl.auth.yggdrasil.YggdrasilAccount; 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.auth.yggdrasil.YggdrasilSession;
import org.jackhuang.hmcl.game.Arguments; import org.jackhuang.hmcl.game.Arguments;
import org.jackhuang.hmcl.util.ToStringBuilder; import org.jackhuang.hmcl.util.ToStringBuilder;
@@ -36,14 +35,19 @@ import java.util.concurrent.ExecutionException;
import static java.nio.charset.StandardCharsets.UTF_8; import static java.nio.charset.StandardCharsets.UTF_8;
public class AuthlibInjectorAccount extends YggdrasilAccount { public class AuthlibInjectorAccount extends YggdrasilAccount {
private AuthlibInjectorServer server; private final AuthlibInjectorServer server;
private AuthlibInjectorArtifactProvider downloader; private AuthlibInjectorArtifactProvider downloader;
protected AuthlibInjectorAccount(YggdrasilService service, AuthlibInjectorServer server, AuthlibInjectorArtifactProvider downloader, String username, UUID characterUUID, YggdrasilSession session) { public AuthlibInjectorAccount(AuthlibInjectorServer server, AuthlibInjectorArtifactProvider downloader, String username, String password, CharacterSelector selector) throws AuthenticationException {
super(service, username, characterUUID, session); super(server.getYggdrasilService(), username, password, selector);
this.downloader = downloader;
this.server = server; 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 @Override
@@ -52,8 +56,8 @@ public class AuthlibInjectorAccount extends YggdrasilAccount {
} }
@Override @Override
protected AuthInfo logInWithPassword(String password, CharacterSelector selector) throws AuthenticationException { public synchronized AuthInfo logInWithPassword(String password) throws AuthenticationException {
return inject(() -> super.logInWithPassword(password, selector)); return inject(() -> super.logInWithPassword(password));
} }
@Override @Override
@@ -111,7 +115,7 @@ public class AuthlibInjectorAccount extends YggdrasilAccount {
return new Arguments().addJVMArguments( return new Arguments().addJVMArguments(
"-javaagent:" + artifact.getLocation().toString() + "=" + server.getUrl(), "-javaagent:" + artifact.getLocation().toString() + "=" + server.getUrl(),
"-Dauthlibinjector.side=client", "-Dauthlibinjector.side=client",
"-Dorg.to2mbn.authlibinjector.config.prefetched=" + Base64.getEncoder().encodeToString(prefetchedMeta.getBytes(UTF_8))); "-Dauthlibinjector.yggdrasil.prefetched=" + Base64.getEncoder().encodeToString(prefetchedMeta.getBytes(UTF_8)));
} }
@Override @Override
@@ -121,6 +125,12 @@ public class AuthlibInjectorAccount extends YggdrasilAccount {
return map; return map;
} }
@Override
public void clearCache() {
super.clearCache();
server.invalidateMetadataCache();
}
public AuthlibInjectorServer getServer() { public AuthlibInjectorServer getServer() {
return server; return server;
} }

View File

@@ -20,7 +20,8 @@ package org.jackhuang.hmcl.auth.authlibinjector;
import org.jackhuang.hmcl.auth.AccountFactory; import org.jackhuang.hmcl.auth.AccountFactory;
import org.jackhuang.hmcl.auth.AuthenticationException; import org.jackhuang.hmcl.auth.AuthenticationException;
import org.jackhuang.hmcl.auth.CharacterSelector; 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 org.jackhuang.hmcl.auth.yggdrasil.YggdrasilSession;
import java.util.Map; import java.util.Map;
import java.util.Objects; import java.util.Objects;
@@ -48,10 +49,7 @@ public class AuthlibInjectorAccountFactory extends AccountFactory<AuthlibInjecto
AuthlibInjectorServer server = (AuthlibInjectorServer) additionalData; AuthlibInjectorServer server = (AuthlibInjectorServer) additionalData;
AuthlibInjectorAccount account = new AuthlibInjectorAccount(new YggdrasilService(new AuthlibInjectorProvider(server.getUrl())), return new AuthlibInjectorAccount(server, downloader, username, password, selector);
server, downloader, username, null, null);
account.logInWithPassword(password, selector);
return account;
} }
@Override @Override
@@ -67,7 +65,14 @@ public class AuthlibInjectorAccountFactory extends AccountFactory<AuthlibInjecto
AuthlibInjectorServer server = serverLookup.apply(apiRoot); AuthlibInjectorServer server = serverLookup.apply(apiRoot);
return new AuthlibInjectorAccount(new YggdrasilService(new AuthlibInjectorProvider(server.getUrl())), tryCast(storage.get("profileProperties"), Map.class).ifPresent(
server, downloader, username, session.getSelectedProfile().getId(), session); 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);
} }
} }

View File

@@ -127,7 +127,7 @@ public class AuthlibInjectorDownloader implements AuthlibInjectorArtifactProvide
} }
} }
private class AuthlibInjectorVersionInfo { private static class AuthlibInjectorVersionInfo {
@SerializedName("build_number") @SerializedName("build_number")
public int buildNumber; public int buildNumber;

View File

@@ -56,4 +56,9 @@ public class AuthlibInjectorProvider implements YggdrasilProvider {
public URL getProfilePropertiesURL(UUID uuid) { public URL getProfilePropertiesURL(UUID uuid) {
return NetworkUtils.toURL(apiRoot + "sessionserver/session/minecraft/profile/" + UUIDTypeAdapter.fromUUID(uuid)); return NetworkUtils.toURL(apiRoot + "sessionserver/session/minecraft/profile/" + UUIDTypeAdapter.fromUUID(uuid));
} }
@Override
public String toString() {
return apiRoot;
}
} }

View File

@@ -18,6 +18,7 @@
package org.jackhuang.hmcl.auth.authlibinjector; package org.jackhuang.hmcl.auth.authlibinjector;
import static java.nio.charset.StandardCharsets.UTF_8; 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.Lang.tryCast;
import static org.jackhuang.hmcl.util.Logging.LOG; import static org.jackhuang.hmcl.util.Logging.LOG;
import static org.jackhuang.hmcl.util.io.IOUtils.readFullyAsByteArray; import static org.jackhuang.hmcl.util.io.IOUtils.readFullyAsByteArray;
@@ -29,9 +30,12 @@ import java.net.HttpURLConnection;
import java.net.MalformedURLException; import java.net.MalformedURLException;
import java.net.URL; import java.net.URL;
import java.net.URLConnection; import java.net.URLConnection;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Optional; import java.util.Optional;
import java.util.logging.Level; import java.util.logging.Level;
import org.jackhuang.hmcl.auth.yggdrasil.YggdrasilService;
import org.jackhuang.hmcl.util.javafx.ObservableHelper; import org.jackhuang.hmcl.util.javafx.ObservableHelper;
import org.jetbrains.annotations.Nullable; import org.jetbrains.annotations.Nullable;
@@ -137,18 +141,25 @@ public class AuthlibInjectorServer implements Observable {
@Nullable @Nullable
private transient String name; private transient String name;
private transient Map<String, String> links = emptyMap();
private transient boolean metadataRefreshed; 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) { public AuthlibInjectorServer(String url) {
this.url = url; this.url = url;
this.yggdrasilService = new YggdrasilService(new AuthlibInjectorProvider(url));
} }
public String getUrl() { public String getUrl() {
return url; return url;
} }
public YggdrasilService getYggdrasilService() {
return yggdrasilService;
}
public Optional<String> getMetadataResponse() { public Optional<String> getMetadataResponse() {
return Optional.ofNullable(metadataResponse); return Optional.ofNullable(metadataResponse);
} }
@@ -162,6 +173,10 @@ public class AuthlibInjectorServer implements Observable {
.orElse(url); .orElse(url);
} }
public Map<String, String> getLinks() {
return links;
}
public String fetchMetadataResponse() throws IOException { public String fetchMetadataResponse() throws IOException {
if (metadataResponse == null || !metadataRefreshed) { if (metadataResponse == null || !metadataRefreshed) {
refreshMetadata(); refreshMetadata();
@@ -201,9 +216,23 @@ public class AuthlibInjectorServer implements Observable {
this.name = metaObject.flatMap(meta -> tryCast(meta.get("serverName"), JsonPrimitive.class).map(JsonPrimitive::getAsString)) this.name = metaObject.flatMap(meta -> tryCast(meta.get("serverName"), JsonPrimitive.class).map(JsonPrimitive::getAsString))
.orElse(null); .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 @Override
public int hashCode() { public int hashCode() {
return url.hashCode(); return url.hashCode();

View File

@@ -25,10 +25,10 @@ import org.jackhuang.hmcl.util.ToStringBuilder;
import org.jackhuang.hmcl.util.gson.UUIDTypeAdapter; import org.jackhuang.hmcl.util.gson.UUIDTypeAdapter;
import java.util.Map; import java.util.Map;
import java.util.Objects;
import java.util.Optional; import java.util.Optional;
import java.util.UUID; import java.util.UUID;
import static java.util.Objects.requireNonNull;
import static org.jackhuang.hmcl.util.Lang.mapOf; import static org.jackhuang.hmcl.util.Lang.mapOf;
import static org.jackhuang.hmcl.util.Pair.pair; import static org.jackhuang.hmcl.util.Pair.pair;
@@ -41,15 +41,13 @@ public class OfflineAccount extends Account {
private final String username; private final String username;
private final UUID uuid; private final UUID uuid;
OfflineAccount(String username, UUID uuid) { protected OfflineAccount(String username, UUID uuid) {
Objects.requireNonNull(username); this.username = requireNonNull(username);
Objects.requireNonNull(uuid); this.uuid = requireNonNull(uuid);
this.username = username; if (StringUtils.isBlank(username)) {
this.uuid = uuid;
if (StringUtils.isBlank(username))
throw new IllegalArgumentException("Username cannot be blank"); throw new IllegalArgumentException("Username cannot be blank");
}
} }
@Override @Override
@@ -68,10 +66,7 @@ public class OfflineAccount extends Account {
} }
@Override @Override
public AuthInfo logIn() throws AuthenticationException { public AuthInfo logIn() {
if (StringUtils.isBlank(username))
throw new AuthenticationException("Username cannot be empty");
return new AuthInfo(username, uuid, UUIDTypeAdapter.fromUUID(UUID.randomUUID()), "{}"); return new AuthInfo(username, uuid, UUIDTypeAdapter.fromUUID(UUID.randomUUID()), "{}");
} }
@@ -82,7 +77,7 @@ public class OfflineAccount extends Account {
@Override @Override
public Optional<AuthInfo> playOffline() { public Optional<AuthInfo> playOffline() {
return Optional.empty(); return Optional.of(logIn());
} }
@Override @Override
@@ -93,11 +88,6 @@ public class OfflineAccount extends Account {
); );
} }
@Override
public void clearCache() {
// Nothing to clear.
}
@Override @Override
public String toString() { public String toString() {
return new ToStringBuilder(this) return new ToStringBuilder(this)

View File

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

View File

@@ -17,36 +17,31 @@
*/ */
package org.jackhuang.hmcl.auth.yggdrasil; package org.jackhuang.hmcl.auth.yggdrasil;
import com.google.gson.JsonParseException; import static java.util.Objects.requireNonNull;
import org.jackhuang.hmcl.util.Immutable;
import org.jackhuang.hmcl.util.StringUtils;
import org.jackhuang.hmcl.util.gson.Validation;
import java.util.UUID; 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 * @author huangyuhui
*/ */
@Immutable @Immutable
public final class GameProfile implements Validation { public class GameProfile implements Validation {
@JsonAdapter(UUIDTypeAdapter.class)
private final UUID id; private final UUID id;
private final String name;
private final PropertyMap properties;
public GameProfile() { private final String name;
this(null, null);
}
public GameProfile(UUID id, String name) { public GameProfile(UUID id, String name) {
this(id, name, new PropertyMap()); this.id = requireNonNull(id);
} this.name = requireNonNull(name);
public GameProfile(UUID id, String name, PropertyMap properties) {
this.id = id;
this.name = name;
this.properties = properties;
} }
public UUID getId() { public UUID getId() {
@@ -57,18 +52,11 @@ public final class GameProfile implements Validation {
return name; return name;
} }
/**
* @return nullable
*/
public PropertyMap getProperties() {
return properties;
}
@Override @Override
public void validate() throws JsonParseException { public void validate() throws JsonParseException {
if (id == null) if (id == null)
throw new JsonParseException("Game profile id cannot be null or malformed"); throw new JsonParseException("Game profile id cannot be null");
if (StringUtils.isBlank(name)) if (name == null)
throw new JsonParseException("Game profile name cannot be null or blank"); throw new JsonParseException("Game profile name cannot be null");
} }
} }

View File

@@ -24,7 +24,6 @@ import java.net.URL;
import java.util.UUID; import java.util.UUID;
public class MojangYggdrasilProvider implements YggdrasilProvider { public class MojangYggdrasilProvider implements YggdrasilProvider {
public static final MojangYggdrasilProvider INSTANCE = new MojangYggdrasilProvider();
@Override @Override
public URL getAuthenticationURL() { public URL getAuthenticationURL() {
@@ -50,4 +49,9 @@ public class MojangYggdrasilProvider implements YggdrasilProvider {
public URL getProfilePropertiesURL(UUID uuid) { public URL getProfilePropertiesURL(UUID uuid) {
return NetworkUtils.toURL("https://sessionserver.mojang.com/session/minecraft/profile/" + UUIDTypeAdapter.fromUUID(uuid)); return NetworkUtils.toURL("https://sessionserver.mojang.com/session/minecraft/profile/" + UUIDTypeAdapter.fromUUID(uuid));
} }
@Override
public String toString() {
return "mojang";
}
} }

View File

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

View File

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

View File

@@ -40,10 +40,7 @@ public final class Texture {
return url; return url;
} }
public String getMetadata(String key) { public Map<String, String> getMetadata() {
if (metadata == null) return metadata;
return null;
else
return metadata.get(key);
} }
} }

View File

@@ -0,0 +1,43 @@
/*
* 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 java.util.Map;
import java.util.UUID;
public enum TextureModel {
STEVE("default"), ALEX("slim");
public final String modelName;
private TextureModel(String modelName) {
this.modelName = modelName;
}
public static TextureModel detectModelName(Map<String, String> metadata) {
if (metadata != null && "slim".equals(metadata.get("model"))) {
return ALEX;
} else {
return STEVE;
}
}
public static TextureModel detectUUID(UUID uuid) {
return (uuid.hashCode() & 1) == 1 ? ALEX : STEVE;
}
}

View File

@@ -18,23 +18,31 @@
package org.jackhuang.hmcl.auth.yggdrasil; package org.jackhuang.hmcl.auth.yggdrasil;
import com.google.gson.JsonParseException; 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.StringUtils;
import org.jackhuang.hmcl.util.gson.Validation; import org.jackhuang.hmcl.util.gson.Validation;
import org.jetbrains.annotations.Nullable;
/** /**
* *
* @author huang * @author huang
*/ */
@Immutable
public final class User implements Validation { public final class User implements Validation {
private final String id; private final String id;
private final PropertyMap properties;
@Nullable
private final Map<String, String> properties;
public User(String id) { public User(String id) {
this(id, null); this(id, null);
} }
public User(String id, PropertyMap properties) { public User(String id, Map<String, String> properties) {
this.id = id; this.id = id;
this.properties = properties; this.properties = properties;
} }
@@ -43,7 +51,7 @@ public final class User implements Validation {
return id; return id;
} }
public PropertyMap getProperties() { public Map<String, String> getProperties() {
return properties; return properties;
} }
@@ -52,5 +60,4 @@ public final class User implements Validation {
if (StringUtils.isBlank(id)) if (StringUtils.isBlank(id))
throw new JsonParseException("User id cannot be empty."); throw new JsonParseException("User id cannot be empty.");
} }
} }

View File

@@ -17,32 +17,62 @@
*/ */
package org.jackhuang.hmcl.auth.yggdrasil; package org.jackhuang.hmcl.auth.yggdrasil;
import org.jackhuang.hmcl.auth.*; import static java.util.Objects.requireNonNull;
import org.jackhuang.hmcl.util.StringUtils;
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 org.jackhuang.hmcl.util.gson.UUIDTypeAdapter;
import java.util.*;
/**
*
* @author huangyuhui
*/
public class YggdrasilAccount extends Account { public class YggdrasilAccount extends Account {
private final String username;
private final YggdrasilService service; private final YggdrasilService service;
private boolean isOnline = false; private final UUID characterUUID;
private final String username;
private boolean authenticated = false;
private YggdrasilSession session; private YggdrasilSession session;
private UUID characterUUID;
protected YggdrasilAccount(YggdrasilService service, String username, UUID characterUUID, YggdrasilSession session) { protected YggdrasilAccount(YggdrasilService service, String username, YggdrasilSession session) {
this.service = service; this.service = requireNonNull(service);
this.username = username; this.username = requireNonNull(username);
this.session = session; this.characterUUID = requireNonNull(session.getSelectedProfile().getId());
this.characterUUID = characterUUID; this.session = requireNonNull(session);
}
if (session == null || session.getSelectedProfile() == null || StringUtils.isBlank(session.getAccessToken())) protected YggdrasilAccount(YggdrasilService service, String username, String password, CharacterSelector selector) throws AuthenticationException {
this.session = null; 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 @Override
@@ -55,22 +85,20 @@ public class YggdrasilAccount extends Account {
return session.getSelectedProfile().getName(); return session.getSelectedProfile().getName();
} }
public boolean isLoggedIn() { @Override
return session != null && StringUtils.isNotBlank(session.getAccessToken()); public UUID getUUID() {
} return session.getSelectedProfile().getId();
public boolean canPlayOnline() {
return isLoggedIn() && session.getSelectedProfile() != null && isOnline;
} }
@Override @Override
public synchronized AuthInfo logIn() throws AuthenticationException { public synchronized AuthInfo logIn() throws AuthenticationException {
if (!canPlayOnline()) { if (!authenticated) {
if (service.validate(session.getAccessToken(), session.getClientToken())) { if (service.validate(session.getAccessToken(), session.getClientToken())) {
isOnline = true; authenticated = true;
} else { } else {
YggdrasilSession acquiredSession;
try { try {
updateSession(service.refresh(session.getAccessToken(), session.getClientToken(), null), new SpecificCharacterSelector(characterUUID)); acquiredSession = service.refresh(session.getAccessToken(), session.getClientToken(), null);
} catch (RemoteAuthenticationException e) { } catch (RemoteAuthenticationException e) {
if ("ForbiddenOperationException".equals(e.getRemoteName())) { if ("ForbiddenOperationException".equals(e.getRemoteName())) {
throw new CredentialExpiredException(e); throw new CredentialExpiredException(e);
@@ -78,95 +106,85 @@ public class YggdrasilAccount extends Account {
throw e; 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(); return session.toAuthInfo();
} }
@Override @Override
public synchronized AuthInfo logInWithPassword(String password) throws AuthenticationException { 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.getSelectedProfile() == null) {
if (acquiredSession.getAvailableProfiles() == null || acquiredSession.getAvailableProfiles().length == 0) if (acquiredSession.getAvailableProfiles() == null || acquiredSession.getAvailableProfiles().isEmpty()) {
throw new NoCharacterException(this); 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.getAccessToken(),
acquiredSession.getClientToken(), acquiredSession.getClientToken(),
selector.select(this, Arrays.asList(acquiredSession.getAvailableProfiles()))); characterToSelect);
} else { } else {
this.session = acquiredSession; if (!acquiredSession.getSelectedProfile().getId().equals(characterUUID)) {
throw new CharacterDeletedException();
}
session = acquiredSession;
} }
this.characterUUID = this.session.getSelectedProfile().getId(); authenticated = true;
invalidate(); invalidate();
return session.toAuthInfo();
} }
@Override @Override
public Optional<AuthInfo> playOffline() { public Optional<AuthInfo> playOffline() {
if (isLoggedIn() && session.getSelectedProfile() != null && !canPlayOnline()) return Optional.of(session.toAuthInfo());
return Optional.of(session.toAuthInfo());
return Optional.empty();
} }
@Override @Override
public Map<Object, Object> toStorage() { public Map<Object, Object> toStorage() {
if (session == null) Map<Object, Object> storage = new HashMap<>();
throw new IllegalStateException("No session is specified"); storage.put("username", username);
HashMap<Object, Object> storage = new HashMap<>();
storage.put("username", getUsername());
storage.putAll(session.toStorage()); storage.putAll(session.toStorage());
service.getProfileRepository().getImmediately(characterUUID).ifPresent(profile -> {
storage.put("profileProperties", profile.getProperties());
});
return storage; return storage;
} }
@Override public YggdrasilService getYggdrasilService() {
public UUID getUUID() { return service;
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));
} }
@Override @Override
public void clearCache() { public void clearCache() {
Optional.ofNullable(session) authenticated = false;
.map(YggdrasilSession::getSelectedProfile) service.getProfileRepository().invalidate(characterUUID);
.map(GameProfile::getProperties) }
.ifPresent(it -> it.remove("textures"));
private static String randomClientToken() {
return UUIDTypeAdapter.fromUUID(UUID.randomUUID());
} }
@Override @Override
public String toString() { public String toString() {
return "YggdrasilAccount[username=" + getUsername() + "]"; return "YggdrasilAccount[uuid=" + characterUUID + ", username=" + username + "]";
} }
@Override @Override

View File

@@ -20,11 +20,9 @@ package org.jackhuang.hmcl.auth.yggdrasil;
import org.jackhuang.hmcl.auth.AccountFactory; import org.jackhuang.hmcl.auth.AccountFactory;
import org.jackhuang.hmcl.auth.AuthenticationException; import org.jackhuang.hmcl.auth.AuthenticationException;
import org.jackhuang.hmcl.auth.CharacterSelector; import org.jackhuang.hmcl.auth.CharacterSelector;
import org.jackhuang.hmcl.util.gson.UUIDTypeAdapter;
import java.util.Map; import java.util.Map;
import java.util.Objects; import java.util.Objects;
import java.util.UUID;
import static org.jackhuang.hmcl.util.Lang.tryCast; 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> { public class YggdrasilAccountFactory extends AccountFactory<YggdrasilAccount> {
private final YggdrasilProvider provider; public static final YggdrasilAccountFactory MOJANG = new YggdrasilAccountFactory(YggdrasilService.MOJANG);
public YggdrasilAccountFactory(YggdrasilProvider provider) { private YggdrasilService service;
this.provider = provider;
public YggdrasilAccountFactory(YggdrasilService service) {
this.service = service;
} }
@Override @Override
@@ -46,9 +46,7 @@ public class YggdrasilAccountFactory extends AccountFactory<YggdrasilAccount> {
Objects.requireNonNull(username); Objects.requireNonNull(username);
Objects.requireNonNull(password); Objects.requireNonNull(password);
YggdrasilAccount account = new YggdrasilAccount(new YggdrasilService(provider), username, null, null); return new YggdrasilAccount(service, username, password, selector);
account.logInWithPassword(password, selector);
return account;
} }
@Override @Override
@@ -60,10 +58,14 @@ public class YggdrasilAccountFactory extends AccountFactory<YggdrasilAccount> {
String username = tryCast(storage.get("username"), String.class) String username = tryCast(storage.get("username"), String.class)
.orElseThrow(() -> new IllegalArgumentException("storage does not have username")); .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 new YggdrasilAccount(service, username, session);
return UUIDTypeAdapter.fromUUID(UUID.randomUUID());
} }
} }

View File

@@ -27,21 +27,44 @@ import org.jackhuang.hmcl.util.StringUtils;
import org.jackhuang.hmcl.util.gson.UUIDTypeAdapter; import org.jackhuang.hmcl.util.gson.UUIDTypeAdapter;
import org.jackhuang.hmcl.util.gson.ValidationTypeAdapterFactory; import org.jackhuang.hmcl.util.gson.ValidationTypeAdapterFactory;
import org.jackhuang.hmcl.util.io.NetworkUtils; import org.jackhuang.hmcl.util.io.NetworkUtils;
import org.jackhuang.hmcl.util.javafx.ObservableOptionalCache;
import java.io.IOException; import java.io.IOException;
import java.net.URL; import java.net.URL;
import java.util.*; 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.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.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; import static org.jackhuang.hmcl.util.Pair.pair;
public class YggdrasilService { 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 YggdrasilProvider provider;
private final ObservableOptionalCache<UUID, CompleteGameProfile, AuthenticationException> profileRepository;
public YggdrasilService(YggdrasilProvider provider) { public YggdrasilService(YggdrasilProvider provider) {
this.provider = 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 { 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); 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<>(); Map<String, Object> request = new HashMap<>();
request.put("accessToken", accessToken); request.put("accessToken", accessToken);
request.put("clientToken", clientToken); request.put("clientToken", clientToken);
@@ -82,7 +105,16 @@ public class YggdrasilService {
pair("name", characterToSelect.getName()))); 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 { public boolean validate(String accessToken) throws AuthenticationException {
@@ -121,20 +153,19 @@ public class YggdrasilService {
* @param uuid the uuid that the character corresponding to. * @param uuid the uuid that the character corresponding to.
* @return the complete game profile(filled with more properties) * @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); 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); Objects.requireNonNull(profile);
Optional<String> encodedTextures = Optional.ofNullable(profile.getProperties()) String encodedTextures = profile.getProperties().get("textures");
.flatMap(properties -> Optional.ofNullable(properties.get("textures")));
if (encodedTextures.isPresent()) { if (encodedTextures != null) {
TextureResponse texturePayload = fromJson(new String(Base64.getDecoder().decode(encodedTextures.get()), UTF_8), TextureResponse.class); TextureResponse texturePayload = fromJson(new String(Base64.getDecoder().decode(encodedTextures), UTF_8), TextureResponse.class);
return Optional.ofNullable(texturePayload.textures); return Optional.ofNullable(texturePayload.textures);
} else { } else {
return Optional.empty(); return Optional.empty();
@@ -148,7 +179,12 @@ public class YggdrasilService {
if (!clientToken.equals(response.clientToken)) if (!clientToken.equals(response.clientToken))
throw new AuthenticationException("Client token changed from " + clientToken + " to " + 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 { 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 { try {
if (payload == null) if (payload == null)
return NetworkUtils.doGet(url); return NetworkUtils.doGet(url);
@@ -187,26 +223,25 @@ public class YggdrasilService {
} }
} }
private class TextureResponse { private static class TextureResponse {
public Map<TextureType, Texture> textures; public Map<TextureType, Texture> textures;
} }
private class AuthenticationResponse extends ErrorResponse { private static class AuthenticationResponse extends ErrorResponse {
public String accessToken; public String accessToken;
public String clientToken; public String clientToken;
public GameProfile selectedProfile; public GameProfile selectedProfile;
public GameProfile[] availableProfiles; public List<GameProfile> availableProfiles;
public User user; public User user;
} }
private class ErrorResponse { private static class ErrorResponse {
public String error; public String error;
public String errorMessage; public String errorMessage;
public String cause; public String cause;
} }
private static final Gson GSON = new GsonBuilder() private static final Gson GSON = new GsonBuilder()
.registerTypeAdapter(PropertyMap.class, PropertyMap.Serializer.INSTANCE)
.registerTypeAdapter(UUID.class, UUIDTypeAdapter.INSTANCE) .registerTypeAdapter(UUID.class, UUIDTypeAdapter.INSTANCE)
.registerTypeAdapterFactory(ValidationTypeAdapterFactory.INSTANCE) .registerTypeAdapterFactory(ValidationTypeAdapterFactory.INSTANCE)
.create(); .create();

View File

@@ -18,10 +18,11 @@
package org.jackhuang.hmcl.auth.yggdrasil; package org.jackhuang.hmcl.auth.yggdrasil;
import com.google.gson.Gson; import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import org.jackhuang.hmcl.auth.AuthInfo; import org.jackhuang.hmcl.auth.AuthInfo;
import org.jackhuang.hmcl.util.Immutable;
import org.jackhuang.hmcl.util.gson.UUIDTypeAdapter; import org.jackhuang.hmcl.util.gson.UUIDTypeAdapter;
import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Optional; import java.util.Optional;
import java.util.UUID; 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.Lang.tryCast;
import static org.jackhuang.hmcl.util.Pair.pair; import static org.jackhuang.hmcl.util.Pair.pair;
@Immutable
public class YggdrasilSession { public class YggdrasilSession {
private String clientToken; private final String clientToken;
private String accessToken; private final String accessToken;
private GameProfile selectedProfile; private final GameProfile selectedProfile;
private GameProfile[] availableProfiles; private final List<GameProfile> availableProfiles;
private User user; 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.clientToken = clientToken;
this.accessToken = accessToken; this.accessToken = accessToken;
this.selectedProfile = selectedProfile; this.selectedProfile = selectedProfile;
@@ -64,7 +66,7 @@ public class YggdrasilSession {
/** /**
* @return nullable (null if the YggdrasilSession is loaded from storage) * @return nullable (null if the YggdrasilSession is loaded from storage)
*/ */
public GameProfile[] getAvailableProfiles() { public List<GameProfile> getAvailableProfiles() {
return availableProfiles; return availableProfiles;
} }
@@ -78,7 +80,7 @@ public class YggdrasilSession {
String clientToken = tryCast(storage.get("clientToken"), String.class).orElseThrow(() -> new IllegalArgumentException("clientToken is missing")); String clientToken = tryCast(storage.get("clientToken"), String.class).orElseThrow(() -> new IllegalArgumentException("clientToken is missing"));
String accessToken = tryCast(storage.get("accessToken"), String.class).orElseThrow(() -> new IllegalArgumentException("accessToken is missing")); String accessToken = tryCast(storage.get("accessToken"), String.class).orElseThrow(() -> new IllegalArgumentException("accessToken is missing"));
String userId = tryCast(storage.get("userid"), String.class).orElseThrow(() -> new IllegalArgumentException("userid 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)); 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("{}")); 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();
} }

View File

@@ -59,8 +59,8 @@ public class BMCLAPIDownloadProvider implements DownloadProvider {
return baseURL return baseURL
.replace("https://launchermeta.mojang.com", "https://bmclapi2.bangbang93.com") .replace("https://launchermeta.mojang.com", "https://bmclapi2.bangbang93.com")
.replace("https://launcher.mojang.com", "https://bmclapi2.bangbang93.com") .replace("https://launcher.mojang.com", "https://bmclapi2.bangbang93.com")
.replace("https://libraries.minecraft.net", "http://bmclapi2.bangbang93.com/libraries") .replace("https://libraries.minecraft.net", "https://bmclapi2.bangbang93.com/libraries")
.replaceFirst("https?://files\\.minecraftforge\\.net/maven", "http://bmclapi2.bangbang93.com/maven") .replaceFirst("https?://files\\.minecraftforge\\.net/maven", "https://bmclapi2.bangbang93.com/maven")
.replace("http://dl.liteloader.com/versions/versions.json", "https://bmclapi2.bangbang93.com/maven/com/mumfrey/liteloader/versions.json") .replace("http://dl.liteloader.com/versions/versions.json", "https://bmclapi2.bangbang93.com/maven/com/mumfrey/liteloader/versions.json")
.replace("http://dl.liteloader.com/versions", "https://bmclapi2.bangbang93.com/maven") .replace("http://dl.liteloader.com/versions", "https://bmclapi2.bangbang93.com/maven")
.replace("https://authlib-injector.yushi.moe", "https://bmclapi2.bangbang93.com/mirrors/authlib-injector"); .replace("https://authlib-injector.yushi.moe", "https://bmclapi2.bangbang93.com/mirrors/authlib-injector");

View File

@@ -19,10 +19,7 @@ package org.jackhuang.hmcl.download;
import org.jackhuang.hmcl.download.forge.ForgeInstallTask; import org.jackhuang.hmcl.download.forge.ForgeInstallTask;
import org.jackhuang.hmcl.download.forge.ForgeRemoteVersion; import org.jackhuang.hmcl.download.forge.ForgeRemoteVersion;
import org.jackhuang.hmcl.download.game.GameAssetDownloadTask; import org.jackhuang.hmcl.download.game.*;
import org.jackhuang.hmcl.download.game.GameLibrariesTask;
import org.jackhuang.hmcl.download.game.LibrariesUniqueTask;
import org.jackhuang.hmcl.download.game.VersionJsonSaveTask;
import org.jackhuang.hmcl.download.liteloader.LiteLoaderInstallTask; import org.jackhuang.hmcl.download.liteloader.LiteLoaderInstallTask;
import org.jackhuang.hmcl.download.liteloader.LiteLoaderRemoteVersion; import org.jackhuang.hmcl.download.liteloader.LiteLoaderRemoteVersion;
import org.jackhuang.hmcl.download.optifine.OptiFineInstallTask; import org.jackhuang.hmcl.download.optifine.OptiFineInstallTask;
@@ -74,7 +71,13 @@ public class DefaultDependencyManager extends AbstractDependencyManager {
@Override @Override
public Task checkGameCompletionAsync(Version version) { public Task checkGameCompletionAsync(Version version) {
return new ParallelTask( return new ParallelTask(
new GameAssetDownloadTask(this, version), Task.ofThen(var -> {
if (!repository.getVersionJar(version).exists())
return new GameDownloadTask(this, null, version);
else
return null;
}),
new GameAssetDownloadTask(this, version, GameAssetDownloadTask.DOWNLOAD_INDEX_IF_NECESSARY),
new GameLibrariesTask(this, version) new GameLibrariesTask(this, version)
); );
} }

View File

@@ -54,7 +54,7 @@ public class DefaultGameBuilder extends GameBuilder {
version = version.setId(name).setJar(null); version = version.setId(name).setJar(null);
variables.set("version", version); variables.set("version", version);
Task result = downloadGameAsync(gameVersion, version).then(new ParallelTask( Task result = downloadGameAsync(gameVersion, version).then(new ParallelTask(
new GameAssetDownloadTask(dependencyManager, version), new GameAssetDownloadTask(dependencyManager, version, GameAssetDownloadTask.DOWNLOAD_INDEX_FORCIBLY),
new GameLibrariesTask(dependencyManager, version) // Game libraries will be downloaded for multiple times partly, this time is for vanilla libraries. new GameLibrariesTask(dependencyManager, version) // Game libraries will be downloaded for multiple times partly, this time is for vanilla libraries.
).with(new VersionJsonSaveTask(dependencyManager.getGameRepository(), version))); // using [with] because download failure here are tolerant. ).with(new VersionJsonSaveTask(dependencyManager.getGameRepository(), version))); // using [with] because download failure here are tolerant.

View File

@@ -40,6 +40,16 @@ public class MaintainTask extends TaskResult<Version> {
} }
public static Version maintain(Version version) { public static Version maintain(Version version) {
if (version.getMainClass().contains("launchwrapper")) {
return maintainGameWithLaunchWrapper(version);
} else {
// Vanilla Minecraft does not need maintain
// Forge 1.13 support not implemented, not compatible with OptiFine currently.
return version;
}
}
private static Version maintainGameWithLaunchWrapper(Version version) {
LibraryAnalyzer libraryAnalyzer = LibraryAnalyzer.analyze(version); LibraryAnalyzer libraryAnalyzer = LibraryAnalyzer.analyze(version);
VersionLibraryBuilder builder = new VersionLibraryBuilder(version); VersionLibraryBuilder builder = new VersionLibraryBuilder(version);

View File

@@ -0,0 +1,183 @@
/*
* 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.download.forge;
import org.jackhuang.hmcl.game.Artifact;
import org.jackhuang.hmcl.game.Library;
import org.jackhuang.hmcl.util.Immutable;
import org.jackhuang.hmcl.util.function.ExceptionalFunction;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
@Immutable
public class ForgeNewInstallProfile {
private final int spec;
private final String minecraft;
private final String json;
private final List<Library> libraries;
private final List<Processor> processors;
private final Map<String, Datum> data;
public ForgeNewInstallProfile(int spec, String minecraft, String json, List<Library> libraries, List<Processor> processors, Map<String, Datum> data) {
this.spec = spec;
this.minecraft = minecraft;
this.json = json;
this.libraries = libraries;
this.processors = processors;
this.data = data;
}
/**
* Specification for install_profile.json.
*/
public int getSpec() {
return spec;
}
/**
* Vanilla game version that this installer supports.
*/
public String getMinecraft() {
return minecraft;
}
/**
* Version json to be installed.
* @return path of the version json relative to the installer JAR file.
*/
public String getJson() {
return json;
}
/**
* Libraries that processors depend on.
* @return the required dependencies.
*/
public List<Library> getLibraries() {
return libraries == null ? Collections.emptyList() : libraries;
}
/**
* Tasks to be executed to setup modded environment.
*/
public List<Processor> getProcessors() {
if (processors == null) return Collections.emptyList();
return processors.stream().filter(p -> p.isSide("client")).collect(Collectors.toList());
}
/**
* Data for processors.
*/
public Map<String, String> getData() {
if (data == null)
return new HashMap<>();
return data.entrySet().stream().collect(Collectors.toMap(Map.Entry::getKey, e -> e.getValue().getClient()));
}
public static class Processor {
private final List<String> sides;
private final Artifact jar;
private final List<Artifact> classpath;
private final List<String> args;
private final Map<String, String> outputs;
public Processor(List<String> sides, Artifact jar, List<Artifact> classpath, List<String> args, Map<String, String> outputs) {
this.sides = sides;
this.jar = jar;
this.classpath = classpath;
this.args = args;
this.outputs = outputs;
}
/**
* Check which side this processor should be run on. We only support client install currently.
* @param side can be one of "client", "server", "extract".
* @return true if the processor can run on the side.
*/
public boolean isSide(String side) {
return sides == null || sides.contains(side);
}
/**
* The executable jar of this processor task. Will be executed in installation process.
* @return the artifact path of executable jar.
*/
public Artifact getJar() {
return jar;
}
/**
* The dependencies of this processor task.
* @return the artifact path of dependencies.
*/
public List<Artifact> getClasspath() {
return classpath == null ? Collections.emptyList() : classpath;
}
/**
* Arguments to pass to the processor jar.
* Each item can be in one of the following formats:
* [artifact]: An artifact path, used for locating files.
* {entry}: Get corresponding value of the entry in {@link ForgeNewInstallProfile#getData()}
* {MINECRAFT_JAR}: path of the Minecraft jar.
* {SIDE}: values other than "client" will be ignored.
* @return arguments to pass to the processor jar.
* @see ForgeNewInstallTask#parseLiteral(String, Map, ExceptionalFunction)
*/
public List<String> getArgs() {
return args == null ? Collections.emptyList() : args;
}
/**
* File-checksum pairs, used for verifying the output file is correct.
* Arguments to pass to the processor jar.
* Keys can be in one of [artifact] or {entry}. Should be file path.
* Values can be in one of {entry} or 'literal'. Should be SHA-1 checksum.
* @return files output from this processor.
* @see ForgeNewInstallTask#parseLiteral(String, Map, ExceptionalFunction)
*/
public Map<String, String> getOutputs() {
return outputs == null ? Collections.emptyMap() : outputs;
}
}
public static class Datum {
private final String client;
public Datum(String client) {
this.client = client;
}
/**
* Can be in the following formats:
* [value]: An artifact path.
* 'value': A string literal.
* value: A file in the installer package, to be extracted to a temp folder, and then have the absolute path in replacements.
* @return Value to use for the client install
*/
public String getClient() {
return client;
}
}
}

View File

@@ -0,0 +1,249 @@
package org.jackhuang.hmcl.download.forge;
import org.jackhuang.hmcl.download.DefaultDependencyManager;
import org.jackhuang.hmcl.download.game.GameLibrariesTask;
import org.jackhuang.hmcl.game.*;
import org.jackhuang.hmcl.task.Task;
import org.jackhuang.hmcl.task.TaskResult;
import org.jackhuang.hmcl.util.StringUtils;
import org.jackhuang.hmcl.util.function.ExceptionalFunction;
import org.jackhuang.hmcl.util.gson.JsonUtils;
import org.jackhuang.hmcl.util.io.ChecksumMismatchException;
import org.jackhuang.hmcl.util.io.CompressingUtils;
import org.jackhuang.hmcl.util.io.FileUtils;
import org.jackhuang.hmcl.util.platform.CommandBuilder;
import org.jackhuang.hmcl.util.platform.JavaVersion;
import org.jackhuang.hmcl.util.platform.OperatingSystem;
import java.io.BufferedReader;
import java.io.FileNotFoundException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.nio.file.FileSystem;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.*;
import java.util.jar.Attributes;
import java.util.jar.JarFile;
import java.util.stream.Collectors;
import static org.jackhuang.hmcl.util.DigestUtils.digest;
import static org.jackhuang.hmcl.util.Hex.encodeHex;
import static org.jackhuang.hmcl.util.Logging.LOG;
public class ForgeNewInstallTask extends TaskResult<Version> {
private final DefaultDependencyManager dependencyManager;
private final DefaultGameRepository gameRepository;
private final Version version;
private final Path installer;
private final List<Task> dependents = new LinkedList<>();
private final List<Task> dependencies = new LinkedList<>();
private ForgeNewInstallProfile profile;
private Version forgeVersion;
public ForgeNewInstallTask(DefaultDependencyManager dependencyManager, Version version, Path installer) {
this.dependencyManager = dependencyManager;
this.gameRepository = dependencyManager.getGameRepository();
this.version = version;
this.installer = installer;
}
private <E extends Exception> String parseLiteral(String literal, Map<String, String> var, ExceptionalFunction<String, String, E> plainConverter) throws E {
if (StringUtils.isSurrounded(literal, "{", "}"))
return var.get(StringUtils.removeSurrounding(literal, "{", "}"));
else if (StringUtils.isSurrounded(literal, "'", "'"))
return StringUtils.removeSurrounding(literal, "'");
else if (StringUtils.isSurrounded(literal, "[", "]"))
return gameRepository.getArtifactFile(version, new Artifact(StringUtils.removeSurrounding(literal, "[", "]"))).toString();
else
return plainConverter.apply(literal);
}
@Override
public Collection<Task> getDependents() {
return dependents;
}
@Override
public List<Task> getDependencies() {
return dependencies;
}
@Override
public String getId() {
return "version";
}
@Override
public boolean doPreExecute() {
return true;
}
@Override
public void preExecute() throws Exception {
try (FileSystem fs = CompressingUtils.createReadOnlyZipFileSystem(installer)) {
profile = JsonUtils.fromNonNullJson(FileUtils.readText(fs.getPath("install_profile.json")), ForgeNewInstallProfile.class);
forgeVersion = JsonUtils.fromNonNullJson(FileUtils.readText(fs.getPath(profile.getJson())), Version.class);
for (Library library : profile.getLibraries()) {
Path file = fs.getPath("maven").resolve(library.getPath());
if (Files.exists(file)) {
Path dest = gameRepository.getLibraryFile(version, library).toPath();
FileUtils.copyFile(file, dest);
}
}
}
dependents.add(new GameLibrariesTask(dependencyManager, version, profile.getLibraries()));
}
@Override
public void execute() throws Exception {
Path temp = Files.createTempDirectory("forge_installer");
int finished = 0;
try (FileSystem fs = CompressingUtils.createReadOnlyZipFileSystem(installer)) {
List<ForgeNewInstallProfile.Processor> processors = profile.getProcessors();
Map<String, String> data = profile.getData();
updateProgress(0, processors.size());
for (Map.Entry<String, String> entry : data.entrySet()) {
String key = entry.getKey();
String value = entry.getValue();
data.put(key, parseLiteral(value,
Collections.emptyMap(),
str -> {
Path dest = temp.resolve(str);
FileUtils.copyFile(fs.getPath(str), dest);
return dest.toString();
}));
}
data.put("SIDE", "client");
data.put("MINECRAFT_JAR", gameRepository.getVersionJar(version).getAbsolutePath());
for (ForgeNewInstallProfile.Processor processor : processors) {
Map<String, String> outputs = new HashMap<>();
boolean miss = false;
for (Map.Entry<String, String> entry : processor.getOutputs().entrySet()) {
String key = entry.getKey();
String value = entry.getValue();
key = parseLiteral(key, data, ExceptionalFunction.identity());
value = parseLiteral(value, data, ExceptionalFunction.identity());
if (key == null || value == null) {
throw new Exception("Invalid forge installation configuration");
}
outputs.put(key, value);
Path artifact = Paths.get(key);
if (Files.exists(artifact)) {
String code;
try (InputStream stream = Files.newInputStream(artifact)) {
code = encodeHex(digest("SHA-1", stream));
}
if (!Objects.equals(code, value)) {
Files.delete(artifact);
LOG.info("Found existing file is not valid: " + artifact);
miss = true;
}
} else {
miss = true;
}
}
if (!processor.getOutputs().isEmpty() && !miss) {
continue;
}
Path jar = gameRepository.getArtifactFile(version, processor.getJar());
if (!Files.isRegularFile(jar))
throw new FileNotFoundException("Game processor file not found, should be downloaded in preprocess");
String mainClass;
try (JarFile jarFile = new JarFile(jar.toFile())) {
mainClass = jarFile.getManifest().getMainAttributes().getValue(Attributes.Name.MAIN_CLASS);
}
if (StringUtils.isBlank(mainClass))
throw new Exception("Game processor jar does not have main class " + jar);
List<String> command = new ArrayList<>();
command.add(JavaVersion.fromCurrentEnvironment().getBinary().toString());
command.add("-cp");
List<String> classpath = new ArrayList<>(processor.getClasspath().size() + 1);
for (Artifact artifact : processor.getClasspath()) {
Path file = gameRepository.getArtifactFile(version, artifact);
if (!Files.isRegularFile(file))
throw new Exception("Game processor dependency missing");
classpath.add(file.toString());
}
classpath.add(jar.toString());
command.add(String.join(OperatingSystem.PATH_SEPARATOR, classpath));
command.add(mainClass);
List<String> args = processor.getArgs().stream().map(arg -> {
String parsed = parseLiteral(arg, data, ExceptionalFunction.identity());
if (parsed == null)
throw new IllegalStateException("Invalid forge installation configuration");
return parsed;
}).collect(Collectors.toList());
command.addAll(args);
LOG.info("Executing external processor " + processor.getJar().toString() + ", command line: " + new CommandBuilder().addAll(command).toString());
Process process = new ProcessBuilder(command).start();
try (BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream()))) {
for (String line; (line = reader.readLine()) != null;) {
System.out.println(line);
}
}
int exitCode = process.waitFor();
if (exitCode != 0)
throw new IllegalStateException("Game processor exited abnormally");
for (Map.Entry<String, String> entry : outputs.entrySet()) {
Path artifact = Paths.get(entry.getKey());
if (!Files.isRegularFile(artifact))
throw new FileNotFoundException("File missing: " + artifact);
String code;
try (InputStream stream = Files.newInputStream(artifact)) {
code = encodeHex(digest("SHA-1", stream));
}
if (!Objects.equals(code, entry.getValue())) {
Files.delete(artifact);
throw new ChecksumMismatchException("SHA-1", entry.getValue(), code);
}
}
updateProgress(++finished, processors.size());
}
}
// resolve the version
SimpleVersionProvider provider = new SimpleVersionProvider();
provider.addVersion(version);
setResult(forgeVersion
.setInheritsFrom(version.getId())
.resolve(provider).setJar(null)
.setId(version.getId()).setLogging(Collections.emptyMap()));
dependencies.add(dependencyManager.checkLibraryCompletionAsync(forgeVersion));
FileUtils.deleteDirectory(temp.toFile());
}
}

View File

@@ -53,13 +53,13 @@ public final class GameAssetDownloadTask extends Task {
* @param dependencyManager the dependency manager that can provides {@link org.jackhuang.hmcl.game.GameRepository} * @param dependencyManager the dependency manager that can provides {@link org.jackhuang.hmcl.game.GameRepository}
* @param version the <b>resolved</b> version * @param version the <b>resolved</b> version
*/ */
public GameAssetDownloadTask(AbstractDependencyManager dependencyManager, Version version) { public GameAssetDownloadTask(AbstractDependencyManager dependencyManager, Version version, boolean forceDownloadingIndex) {
this.dependencyManager = dependencyManager; this.dependencyManager = dependencyManager;
this.version = version; this.version = version;
this.assetIndexInfo = version.getAssetIndex(); this.assetIndexInfo = version.getAssetIndex();
this.assetIndexFile = dependencyManager.getGameRepository().getIndexFile(version.getId(), assetIndexInfo.getId()); this.assetIndexFile = dependencyManager.getGameRepository().getIndexFile(version.getId(), assetIndexInfo.getId());
if (!assetIndexFile.exists()) if (!assetIndexFile.exists() || forceDownloadingIndex)
dependents.add(new GameAssetIndexDownloadTask(dependencyManager, version)); dependents.add(new GameAssetIndexDownloadTask(dependencyManager, version));
} }
@@ -99,5 +99,7 @@ public final class GameAssetDownloadTask extends Task {
updateProgress(++progress, index.getObjects().size()); updateProgress(++progress, index.getObjects().size());
} }
} }
public static final boolean DOWNLOAD_INDEX_FORCIBLY = true;
public static final boolean DOWNLOAD_INDEX_IF_NECESSARY = false;
} }

View File

@@ -55,14 +55,18 @@ public final class GameDownloadTask extends Task {
@Override @Override
public void execute() { public void execute() {
File jar = dependencyManager.getGameRepository().getVersionJar(version); File jar = dependencyManager.getGameRepository().getVersionJar(version);
dependencies.add(new FileDownloadTask( FileDownloadTask task = new FileDownloadTask(
NetworkUtils.toURL(dependencyManager.getDownloadProvider().injectURL(version.getDownloadInfo().getUrl())), NetworkUtils.toURL(dependencyManager.getDownloadProvider().injectURL(version.getDownloadInfo().getUrl())),
jar, jar,
IntegrityCheck.of(CacheRepository.SHA1, version.getDownloadInfo().getSha1())) IntegrityCheck.of(CacheRepository.SHA1, version.getDownloadInfo().getSha1()))
.setCaching(true) .setCaching(true)
.setCacheRepository(dependencyManager.getCacheRepository()) .setCacheRepository(dependencyManager.getCacheRepository());
.setCandidate(dependencyManager.getCacheRepository().getCommonDirectory().resolve("jars").resolve(gameVersion + ".jar")));
if (gameVersion != null)
task.setCandidate(dependencyManager.getCacheRepository().getCommonDirectory().resolve("jars").resolve(gameVersion + ".jar"));
dependencies.add(task);
} }
} }

View File

@@ -36,6 +36,7 @@ public final class GameLibrariesTask extends Task {
private final AbstractDependencyManager dependencyManager; private final AbstractDependencyManager dependencyManager;
private final Version version; private final Version version;
private final List<Library> libraries;
private final List<Task> dependencies = new LinkedList<>(); private final List<Task> dependencies = new LinkedList<>();
/** /**
@@ -45,8 +46,20 @@ public final class GameLibrariesTask extends Task {
* @param version the <b>resolved</b> version * @param version the <b>resolved</b> version
*/ */
public GameLibrariesTask(AbstractDependencyManager dependencyManager, Version version) { public GameLibrariesTask(AbstractDependencyManager dependencyManager, Version version) {
this(dependencyManager, version, version.getLibraries());
}
/**
* Constructor.
*
* @param dependencyManager the dependency manager that can provides {@link org.jackhuang.hmcl.game.GameRepository}
* @param version the <b>resolved</b> version
*/
public GameLibrariesTask(AbstractDependencyManager dependencyManager, Version version, List<Library> libraries) {
this.dependencyManager = dependencyManager; this.dependencyManager = dependencyManager;
this.version = version; this.version = version;
this.libraries = libraries;
setSignificance(TaskSignificance.MODERATE); setSignificance(TaskSignificance.MODERATE);
} }
@@ -57,7 +70,7 @@ public final class GameLibrariesTask extends Task {
@Override @Override
public void execute() { public void execute() {
version.getLibraries().stream().filter(Library::appliesToCurrentEnvironment).forEach(library -> { libraries.stream().filter(Library::appliesToCurrentEnvironment).forEach(library -> {
File file = dependencyManager.getGameRepository().getLibraryFile(version, library); File file = dependencyManager.getGameRepository().getLibraryFile(version, library);
if (!file.exists()) if (!file.exists())
dependencies.add(new LibraryDownloadTask(dependencyManager, file, library)); dependencies.add(new LibraryDownloadTask(dependencyManager, file, library));

View File

@@ -0,0 +1,97 @@
package org.jackhuang.hmcl.game;
import com.google.gson.*;
import com.google.gson.annotations.JsonAdapter;
import org.jackhuang.hmcl.util.Immutable;
import java.lang.reflect.Type;
import java.nio.file.Path;
@Immutable
@JsonAdapter(Artifact.Serializer.class)
public final class Artifact {
private final String group;
private final String name;
private final String version;
private final String classifier;
private final String extension;
private final String descriptor;
private final String fileName;
private final String path;
public Artifact(String descriptor) {
this.descriptor = descriptor;
String[] arr = descriptor.split(":", 4);
if (arr.length != 3 && arr.length != 4)
throw new IllegalArgumentException("Artifact name is malformed");
String ext = null;
int last = arr.length - 1;
String[] splitted = arr[last].split("@");
if (splitted.length == 2) {
arr[last] = splitted[0];
ext = splitted[1];
} else if (splitted.length > 2) {
throw new IllegalArgumentException("Artifact name is malformed");
}
this.group = arr[0].replace("\\", "/");
this.name = arr[1];
this.version = arr[2];
this.classifier = arr.length >= 4 ? arr[3] : null;
this.extension = ext == null ? "jar" : ext;
String fileName = this.name + "-" + this.version;
if (classifier != null) fileName += "-" + this.classifier;
this.fileName = fileName + "." + this.extension;
this.path = String.format("%s/%s/%s/%s", this.group.replace(".", "/"), this.name, this.version, this.fileName);
}
public String getGroup() {
return group;
}
public String getName() {
return name;
}
public String getVersion() {
return version;
}
public String getClassifier() {
return classifier;
}
public String getExtension() {
return extension;
}
public String getFileName() {
return fileName;
}
public Path getPath(Path root) {
return root.resolve(path);
}
@Override
public String toString() {
return descriptor;
}
public static class Serializer implements JsonDeserializer<Artifact>, JsonSerializer<Artifact> {
@Override
public JsonElement serialize(Artifact src, Type typeOfSrc, JsonSerializationContext context) {
return src == null ? JsonNull.INSTANCE : new JsonPrimitive(src.toString());
}
@Override
public Artifact deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws JsonParseException {
return json.isJsonPrimitive() ? new Artifact(json.getAsJsonPrimitive().getAsString()) : null;
}
}
}

View File

@@ -20,18 +20,19 @@ package org.jackhuang.hmcl.game;
import com.google.gson.JsonParseException; import com.google.gson.JsonParseException;
import org.jackhuang.hmcl.event.*; import org.jackhuang.hmcl.event.*;
import org.jackhuang.hmcl.mod.ModManager; import org.jackhuang.hmcl.mod.ModManager;
import org.jackhuang.hmcl.task.Schedulers;
import org.jackhuang.hmcl.util.Logging;
import org.jackhuang.hmcl.util.ToStringBuilder; import org.jackhuang.hmcl.util.ToStringBuilder;
import org.jackhuang.hmcl.util.gson.JsonUtils; import org.jackhuang.hmcl.util.gson.JsonUtils;
import org.jackhuang.hmcl.util.io.FileUtils; import org.jackhuang.hmcl.util.io.FileUtils;
import java.io.File; import java.io.File;
import java.io.IOException; import java.io.IOException;
import java.nio.file.Path;
import java.util.*; import java.util.*;
import java.util.logging.Level; import java.util.logging.Level;
import java.util.stream.Stream; import java.util.stream.Stream;
import static org.jackhuang.hmcl.util.Logging.LOG;
/** /**
* An implementation of classic Minecraft game repository. * An implementation of classic Minecraft game repository.
* *
@@ -81,6 +82,10 @@ public class DefaultGameRepository implements GameRepository {
return new File(getBaseDirectory(), "libraries/" + lib.getPath()); return new File(getBaseDirectory(), "libraries/" + lib.getPath());
} }
public Path getArtifactFile(Version version, Artifact artifact) {
return artifact.getPath(getBaseDirectory().toPath().resolve("libraries"));
}
@Override @Override
public File getRunDirectory(String id) { public File getRunDirectory(String id) {
return getBaseDirectory(); return getBaseDirectory();
@@ -112,7 +117,7 @@ public class DefaultGameRepository implements GameRepository {
} }
public Version readVersionJson(File file) throws IOException, JsonParseException { public Version readVersionJson(File file) throws IOException, JsonParseException {
return JsonUtils.GSON.fromJson(FileUtils.readText(file), Version.class); return JsonUtils.fromNonNullJson(FileUtils.readText(file), Version.class);
} }
@Override @Override
@@ -171,13 +176,13 @@ public class DefaultGameRepository implements GameRepository {
List<File> jsons = FileUtils.listFilesByExtension(removedFile, "json"); List<File> jsons = FileUtils.listFilesByExtension(removedFile, "json");
jsons.forEach(f -> { jsons.forEach(f -> {
if (!f.delete()) if (!f.delete())
Logging.LOG.warning("Unable to delete file " + f); LOG.warning("Unable to delete file " + f);
}); });
// remove the version from version list regardless of whether the directory was removed successfully or not. // remove the version from version list regardless of whether the directory was removed successfully or not.
try { try {
FileUtils.deleteDirectory(removedFile); FileUtils.deleteDirectory(removedFile);
} catch (IOException e) { } catch (IOException e) {
Logging.LOG.log(Level.WARNING, "Unable to remove version folder: " + file, e); LOG.log(Level.WARNING, "Unable to remove version folder: " + file, e);
} }
return true; return true;
} }
@@ -202,25 +207,31 @@ public class DefaultGameRepository implements GameRepository {
// we will find the only json and rename it to correct name. // we will find the only json and rename it to correct name.
if (!json.exists()) { if (!json.exists()) {
List<File> jsons = FileUtils.listFilesByExtension(dir, "json"); List<File> jsons = FileUtils.listFilesByExtension(dir, "json");
if (jsons.size() == 1) if (jsons.size() == 1) {
LOG.info("Renaming json file " + jsons.get(0) + " to " + json);
if (!jsons.get(0).renameTo(json)) { if (!jsons.get(0).renameTo(json)) {
Logging.LOG.warning("Cannot rename json file " + jsons.get(0) + " to " + json + ", ignoring version " + id); LOG.warning("Cannot rename json file, ignoring version " + id);
return Stream.empty(); return Stream.empty();
} }
} else {
LOG.info("No available json file found, ignoring version " + id);
return Stream.empty();
}
} }
Version version; Version version;
try { try {
version = Objects.requireNonNull(readVersionJson(json)); version = readVersionJson(json);
} catch (Exception e) { } catch (Exception e) {
LOG.log(Level.WARNING, "Malformed version json " + id, e);
// JsonSyntaxException or IOException or NullPointerException(!!) // JsonSyntaxException or IOException or NullPointerException(!!)
if (EventBus.EVENT_BUS.fireEvent(new GameJsonParseFailedEvent(this, json, id)) != Event.Result.ALLOW) if (EventBus.EVENT_BUS.fireEvent(new GameJsonParseFailedEvent(this, json, id)) != Event.Result.ALLOW)
return Stream.empty(); return Stream.empty();
try { try {
version = Objects.requireNonNull(readVersionJson(json)); version = readVersionJson(json);
} catch (Exception e2) { } catch (Exception e2) {
Logging.LOG.log(Level.SEVERE, "User corrected version json is still malformed"); LOG.log(Level.SEVERE, "User corrected version json is still malformed", e2);
return Stream.empty(); return Stream.empty();
} }
} }
@@ -230,7 +241,7 @@ public class DefaultGameRepository implements GameRepository {
try { try {
FileUtils.writeText(json, JsonUtils.GSON.toJson(version)); FileUtils.writeText(json, JsonUtils.GSON.toJson(version));
} catch (Exception e) { } catch (Exception e) {
Logging.LOG.log(Level.WARNING, "Ignoring version " + id + " because wrong id " + version.getId() + " is set and cannot correct it."); LOG.log(Level.WARNING, "Ignoring version " + id + " because wrong id " + version.getId() + " is set and cannot correct it.", e);
return Stream.empty(); return Stream.empty();
} }
} }
@@ -246,7 +257,7 @@ public class DefaultGameRepository implements GameRepository {
EventBus.EVENT_BUS.fireEvent(new LoadedOneVersionEvent(this, resolved)) != Event.Result.DENY) EventBus.EVENT_BUS.fireEvent(new LoadedOneVersionEvent(this, resolved)) != Event.Result.DENY)
versions.put(version.getId(), version); versions.put(version.getId(), version);
} catch (VersionNotFoundException e) { } catch (VersionNotFoundException e) {
Logging.LOG.log(Level.WARNING, "Ignoring version " + version.getId() + " because it inherits from a nonexistent version."); LOG.log(Level.WARNING, "Ignoring version " + version.getId() + " because it inherits from a nonexistent version.");
} }
} }
@@ -258,10 +269,8 @@ public class DefaultGameRepository implements GameRepository {
if (EventBus.EVENT_BUS.fireEvent(new RefreshingVersionsEvent(this)) == Event.Result.DENY) if (EventBus.EVENT_BUS.fireEvent(new RefreshingVersionsEvent(this)) == Event.Result.DENY)
return; return;
Schedulers.newThread().schedule(() -> { refreshVersionsImpl();
refreshVersionsImpl(); EventBus.EVENT_BUS.fireEvent(new RefreshedVersionsEvent(this));
EventBus.EVENT_BUS.fireEvent(new RefreshedVersionsEvent(this));
});
} }
@Override @Override
@@ -278,7 +287,7 @@ public class DefaultGameRepository implements GameRepository {
try { try {
return reconstructAssets(version, assetId); return reconstructAssets(version, assetId);
} catch (IOException | JsonParseException e) { } catch (IOException | JsonParseException e) {
Logging.LOG.log(Level.SEVERE, "Unable to reconstruct asset directory", e); LOG.log(Level.SEVERE, "Unable to reconstruct asset directory", e);
return getAssetDirectory(version, assetId); return getAssetDirectory(version, assetId);
} }
} }

View File

@@ -22,6 +22,7 @@ import com.google.gson.annotations.SerializedName;
import org.jackhuang.hmcl.util.Immutable; import org.jackhuang.hmcl.util.Immutable;
import org.jackhuang.hmcl.util.StringUtils; import org.jackhuang.hmcl.util.StringUtils;
import org.jackhuang.hmcl.util.ToStringBuilder; import org.jackhuang.hmcl.util.ToStringBuilder;
import org.jackhuang.hmcl.util.gson.TolerableValidationException;
import org.jackhuang.hmcl.util.gson.Validation; import org.jackhuang.hmcl.util.gson.Validation;
/** /**
@@ -74,8 +75,8 @@ public class DownloadInfo implements Validation {
} }
@Override @Override
public void validate() throws JsonParseException { public void validate() throws JsonParseException, TolerableValidationException {
if (StringUtils.isBlank(url)) if (StringUtils.isBlank(url))
throw new JsonParseException("DownloadInfo url can not be null"); throw new TolerableValidationException();
} }
} }

View File

@@ -17,7 +17,13 @@
*/ */
package org.jackhuang.hmcl.game; package org.jackhuang.hmcl.game;
import static org.jackhuang.hmcl.util.Logging.LOG;
import com.google.gson.JsonParseException;
import com.google.gson.annotations.SerializedName;
import org.jackhuang.hmcl.util.gson.JsonUtils;
import org.jackhuang.hmcl.util.io.CompressingUtils; import org.jackhuang.hmcl.util.io.CompressingUtils;
import org.jackhuang.hmcl.util.io.FileUtils;
import org.jenkinsci.constant_pool_scanner.ConstantPool; import org.jenkinsci.constant_pool_scanner.ConstantPool;
import org.jenkinsci.constant_pool_scanner.ConstantPoolScanner; import org.jenkinsci.constant_pool_scanner.ConstantPoolScanner;
import org.jenkinsci.constant_pool_scanner.ConstantType; import org.jenkinsci.constant_pool_scanner.ConstantType;
@@ -30,6 +36,7 @@ import java.nio.file.Files;
import java.nio.file.Path; import java.nio.file.Path;
import java.util.List; import java.util.List;
import java.util.Optional; import java.util.Optional;
import java.util.logging.Level;
import java.util.stream.Collectors; import java.util.stream.Collectors;
import java.util.stream.StreamSupport; import java.util.stream.StreamSupport;
@@ -37,6 +44,16 @@ import java.util.stream.StreamSupport;
* @author huangyuhui * @author huangyuhui
*/ */
public final class GameVersion { public final class GameVersion {
private static Optional<String> getVersionFromJson(Path versionJson) {
try {
MinecraftVersion version = JsonUtils.fromNonNullJson(FileUtils.readText(versionJson), MinecraftVersion.class);
return Optional.ofNullable(version.name);
} catch (IOException | JsonParseException e) {
LOG.log(Level.WARNING, "Failed to parse version.json", e);
return Optional.empty();
}
}
private static Optional<String> getVersionOfClassMinecraft(byte[] bytecode) throws IOException { private static Optional<String> getVersionOfClassMinecraft(byte[] bytecode) throws IOException {
ConstantPool pool = ConstantPoolScanner.parse(bytecode, ConstantType.STRING); ConstantPool pool = ConstantPoolScanner.parse(bytecode, ConstantType.STRING);
@@ -74,6 +91,13 @@ public final class GameVersion {
return Optional.empty(); return Optional.empty();
try (FileSystem gameJar = CompressingUtils.createReadOnlyZipFileSystem(file.toPath())) { try (FileSystem gameJar = CompressingUtils.createReadOnlyZipFileSystem(file.toPath())) {
Path versionJson = gameJar.getPath("version.json");
if (Files.exists(versionJson)) {
Optional<String> result = getVersionFromJson(versionJson);
if (result.isPresent())
return result;
}
Path minecraft = gameJar.getPath("net/minecraft/client/Minecraft.class"); Path minecraft = gameJar.getPath("net/minecraft/client/Minecraft.class");
if (Files.exists(minecraft)) { if (Files.exists(minecraft)) {
Optional<String> result = getVersionOfClassMinecraft(Files.readAllBytes(minecraft)); Optional<String> result = getVersionOfClassMinecraft(Files.readAllBytes(minecraft));
@@ -88,4 +112,24 @@ public final class GameVersion {
return Optional.empty(); return Optional.empty();
} }
} }
private static final class MinecraftVersion {
public String name;
@SerializedName("release_target")
public String releaseTarget;
public String id;
public boolean stable;
@SerializedName("world_version")
public int worldVersion;
@SerializedName("protocol_version")
public int protocolVersion;
@SerializedName("pack_version")
public int packVersion;
}
} }

View File

@@ -21,6 +21,7 @@ import com.google.gson.JsonParseException;
import com.google.gson.annotations.SerializedName; import com.google.gson.annotations.SerializedName;
import org.jackhuang.hmcl.util.Immutable; import org.jackhuang.hmcl.util.Immutable;
import org.jackhuang.hmcl.util.StringUtils; import org.jackhuang.hmcl.util.StringUtils;
import org.jackhuang.hmcl.util.gson.TolerableValidationException;
/** /**
* *
@@ -54,7 +55,7 @@ public class IdDownloadInfo extends DownloadInfo {
} }
@Override @Override
public void validate() throws JsonParseException { public void validate() throws JsonParseException, TolerableValidationException {
super.validate(); super.validate();
if (StringUtils.isBlank(id)) if (StringUtils.isBlank(id))

View File

@@ -45,13 +45,13 @@ public class Library implements Comparable<Library> {
private final String classifier; private final String classifier;
private final String url; private final String url;
private final LibrariesDownloadInfo downloads; private final LibrariesDownloadInfo downloads;
private final LibraryDownloadInfo download; private transient final LibraryDownloadInfo download;
private final ExtractRules extract; private final ExtractRules extract;
private final Map<OperatingSystem, String> natives; private final Map<OperatingSystem, String> natives;
private final List<CompatibilityRule> rules; private final List<CompatibilityRule> rules;
private final List<String> checksums; private final List<String> checksums;
private final String path; private transient final String path;
public Library(String groupId, String artifactId, String version) { public Library(String groupId, String artifactId, String version) {
this(groupId, artifactId, version, null, null, null); this(groupId, artifactId, version, null, null, null);

View File

@@ -20,6 +20,7 @@ package org.jackhuang.hmcl.game;
import com.google.gson.JsonParseException; import com.google.gson.JsonParseException;
import com.google.gson.annotations.SerializedName; import com.google.gson.annotations.SerializedName;
import org.jackhuang.hmcl.util.StringUtils; import org.jackhuang.hmcl.util.StringUtils;
import org.jackhuang.hmcl.util.gson.TolerableValidationException;
import org.jackhuang.hmcl.util.gson.Validation; import org.jackhuang.hmcl.util.gson.Validation;
/** /**
@@ -66,7 +67,7 @@ public final class LoggingInfo implements Validation {
} }
@Override @Override
public void validate() throws JsonParseException { public void validate() throws JsonParseException, TolerableValidationException {
file.validate(); file.validate();
if (StringUtils.isBlank(argument)) if (StringUtils.isBlank(argument))
throw new JsonParseException("LoggingInfo.argument is empty."); throw new JsonParseException("LoggingInfo.argument is empty.");

View File

@@ -40,7 +40,6 @@ import java.nio.file.Files;
import java.nio.file.Path; import java.nio.file.Path;
import java.util.LinkedList; import java.util.LinkedList;
import java.util.List; import java.util.List;
import java.util.Objects;
import java.util.Optional; import java.util.Optional;
/** /**
@@ -133,7 +132,7 @@ public final class MultiMCModpackInstallTask extends Task {
@Override @Override
public void execute() throws Exception { public void execute() throws Exception {
Version version = Objects.requireNonNull(repository.readVersionJson(name)); Version version = repository.readVersionJson(name);
try (FileSystem fs = CompressingUtils.createReadOnlyZipFileSystem(zipFile.toPath())) { try (FileSystem fs = CompressingUtils.createReadOnlyZipFileSystem(zipFile.toPath())) {
Path root = Files.list(fs.getPath("/")).filter(Files::isDirectory).findAny() Path root = Files.list(fs.getPath("/")).filter(Files::isDirectory).findAny()

View File

@@ -18,7 +18,10 @@
package org.jackhuang.hmcl.task; package org.jackhuang.hmcl.task;
import javafx.application.Platform; import javafx.application.Platform;
import javafx.beans.property.*; import javafx.beans.property.ReadOnlyDoubleProperty;
import javafx.beans.property.ReadOnlyDoubleWrapper;
import javafx.beans.property.ReadOnlyStringProperty;
import javafx.beans.property.ReadOnlyStringWrapper;
import org.jackhuang.hmcl.event.EventManager; import org.jackhuang.hmcl.event.EventManager;
import org.jackhuang.hmcl.util.AutoTypingMap; import org.jackhuang.hmcl.util.AutoTypingMap;
import org.jackhuang.hmcl.util.InvocationDispatcher; import org.jackhuang.hmcl.util.InvocationDispatcher;
@@ -55,18 +58,14 @@ public abstract class Task {
this.significance = significance; this.significance = significance;
} }
private ReadOnlyObjectWrapper<TaskState> state = new ReadOnlyObjectWrapper<>(this, "state", TaskState.READY); private TaskState state = TaskState.READY;
public TaskState getState() { public TaskState getState() {
return state.get(); return state;
} }
void setState(TaskState state) { void setState(TaskState state) {
this.state.setValue(state); this.state = state;
}
public ReadOnlyObjectProperty<TaskState> stateProperty() {
return state.getReadOnlyProperty();
} }
private Throwable lastException = null; private Throwable lastException = null;
@@ -340,10 +339,6 @@ public abstract class Task {
}); });
} }
public static Task empty() {
return of(ExceptionalConsumer.empty());
}
public static Task of(String name, ExceptionalRunnable<?> runnable) { public static Task of(String name, ExceptionalRunnable<?> runnable) {
return of(name, ExceptionalConsumer.fromRunnable(runnable)); return of(name, ExceptionalConsumer.fromRunnable(runnable));
} }

View File

@@ -18,6 +18,7 @@
package org.jackhuang.hmcl.util; package org.jackhuang.hmcl.util;
import java.util.Optional; import java.util.Optional;
import java.util.concurrent.Executor;
import java.util.concurrent.atomic.AtomicReference; import java.util.concurrent.atomic.AtomicReference;
import java.util.function.Consumer; import java.util.function.Consumer;
import java.util.function.Supplier; import java.util.function.Supplier;
@@ -31,9 +32,11 @@ import java.util.function.Supplier;
*/ */
public class InvocationDispatcher<ARG> implements Consumer<ARG> { public class InvocationDispatcher<ARG> implements Consumer<ARG> {
public static <ARG> InvocationDispatcher<ARG> runOn(Consumer<Runnable> executor, Consumer<ARG> action) { public static <ARG> InvocationDispatcher<ARG> runOn(Executor executor, Consumer<ARG> action) {
return new InvocationDispatcher<>(arg -> executor.accept(() -> { return new InvocationDispatcher<>(arg -> executor.execute(() -> {
action.accept(arg.get()); synchronized (action) {
action.accept(arg.get());
}
})); }));
} }

View File

@@ -17,12 +17,16 @@
*/ */
package org.jackhuang.hmcl.util; package org.jackhuang.hmcl.util;
import java.util.*;
import java.util.concurrent.TimeUnit;
import java.util.function.Consumer;
import org.jackhuang.hmcl.util.function.ExceptionalRunnable; import org.jackhuang.hmcl.util.function.ExceptionalRunnable;
import org.jackhuang.hmcl.util.function.ExceptionalSupplier; import org.jackhuang.hmcl.util.function.ExceptionalSupplier;
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;
/** /**
* *
* @author huangyuhui * @author huangyuhui
@@ -172,6 +176,17 @@ public final class Lang {
return thread; 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) { public static int parseInt(Object string, int defaultValue) {
try { try {
return Integer.parseInt(string.toString()); return Integer.parseInt(string.toString());
@@ -189,14 +204,6 @@ public final class Lang {
} }
} }
public static Double toDoubleOrNull(Object string) {
try {
return Double.parseDouble(string.toString());
} catch (NumberFormatException e) {
return null;
}
}
/** /**
* Find the first non-null reference in given list. * Find the first non-null reference in given list.
* @param t nullable references list. * @param t nullable references list.

View File

@@ -17,6 +17,8 @@
*/ */
package org.jackhuang.hmcl.util; package org.jackhuang.hmcl.util;
import org.jackhuang.hmcl.util.platform.OperatingSystem;
import java.io.ByteArrayOutputStream; import java.io.ByteArrayOutputStream;
import java.io.PrintStream; import java.io.PrintStream;
import java.util.Collection; import java.util.Collection;
@@ -24,8 +26,6 @@ import java.util.LinkedList;
import java.util.List; import java.util.List;
import java.util.StringTokenizer; import java.util.StringTokenizer;
import org.jackhuang.hmcl.util.platform.OperatingSystem;
/** /**
* *
* @author huangyuhui * @author huangyuhui
@@ -128,6 +128,10 @@ public final class StringUtils {
return index == -1 ? missingDelimiterValue : str.substring(index + delimiter.length()); return index == -1 ? missingDelimiterValue : str.substring(index + delimiter.length());
} }
public static boolean isSurrounded(String str, String prefix, String suffix) {
return str.startsWith(prefix) && str.endsWith(suffix);
}
public static String removeSurrounding(String str, String delimiter) { public static String removeSurrounding(String str, String delimiter) {
return removeSurrounding(str, delimiter, delimiter); return removeSurrounding(str, delimiter, delimiter);
} }

View File

@@ -23,4 +23,8 @@ package org.jackhuang.hmcl.util.function;
*/ */
public interface ExceptionalFunction<T, R, E extends Exception> { public interface ExceptionalFunction<T, R, E extends Exception> {
R apply(T t) throws E; R apply(T t) throws E;
static <T, E extends RuntimeException> ExceptionalFunction<T, T, E> identity() {
return t -> t;
}
} }

View File

@@ -15,24 +15,15 @@
* You should have received a copy of the GNU General Public License * You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>. * along with this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
package org.jackhuang.hmcl.game; package org.jackhuang.hmcl.util.gson;
/** /**
* What's circle dependency? * This exception gets thrown by implementations of {@link Validation#validate()} if you want to replace
* When C inherits from B, and B inherits from something else, and finally inherits from C again. * the nullable JSON-parsed object which does not satisfy the constraint with null value.
* * @see Validation
* @author huangyuhui
*/ */
public final class CircleDependencyException extends GameException { public final class TolerableValidationException extends Exception {
public CircleDependencyException() { public TolerableValidationException() {
}
public CircleDependencyException(String message) {
super(message);
}
public CircleDependencyException(String message, Throwable cause) {
super(message, cause);
} }
} }

View File

@@ -33,9 +33,6 @@ public final class UUIDTypeAdapter extends TypeAdapter<UUID> {
public static final UUIDTypeAdapter INSTANCE = new UUIDTypeAdapter(); public static final UUIDTypeAdapter INSTANCE = new UUIDTypeAdapter();
private UUIDTypeAdapter() {
}
@Override @Override
public void write(JsonWriter writer, UUID value) throws IOException { public void write(JsonWriter writer, UUID value) throws IOException {
writer.value(value == null ? null : fromUUID(value)); writer.value(value == null ? null : fromUUID(value));

View File

@@ -36,5 +36,5 @@ public interface Validation {
* *
* @throws JsonParseException if fields are filled in wrong format or wrong type. * @throws JsonParseException if fields are filled in wrong format or wrong type.
*/ */
void validate() throws JsonParseException; void validate() throws JsonParseException, TolerableValidationException;
} }

View File

@@ -40,8 +40,14 @@ public final class ValidationTypeAdapterFactory implements TypeAdapterFactory {
return new TypeAdapter<T>() { return new TypeAdapter<T>() {
@Override @Override
public void write(JsonWriter writer, T t) throws IOException { public void write(JsonWriter writer, T t) throws IOException {
if (t instanceof Validation) if (t instanceof Validation) {
((Validation) t).validate(); try {
((Validation) t).validate();
} catch (TolerableValidationException e) {
delegate.write(writer, null);
return;
}
}
delegate.write(writer, t); delegate.write(writer, t);
} }
@@ -49,8 +55,13 @@ public final class ValidationTypeAdapterFactory implements TypeAdapterFactory {
@Override @Override
public T read(JsonReader reader) throws IOException { public T read(JsonReader reader) throws IOException {
T t = delegate.read(reader); T t = delegate.read(reader);
if (t instanceof Validation) if (t instanceof Validation) {
((Validation) t).validate(); try {
((Validation) t).validate();
} catch (TolerableValidationException e) {
return null;
}
}
return t; return t;
} }
}; };

View File

@@ -275,6 +275,22 @@ public final class FileUtils {
Files.copy(srcFile.toPath(), destFile.toPath(), StandardCopyOption.COPY_ATTRIBUTES, StandardCopyOption.REPLACE_EXISTING); Files.copy(srcFile.toPath(), destFile.toPath(), StandardCopyOption.COPY_ATTRIBUTES, StandardCopyOption.REPLACE_EXISTING);
} }
public static void copyFile(Path srcFile, Path destFile)
throws IOException {
Objects.requireNonNull(srcFile, "Source must not be null");
Objects.requireNonNull(destFile, "Destination must not be null");
if (!Files.exists(srcFile))
throw new FileNotFoundException("Source '" + srcFile + "' does not exist");
if (Files.isDirectory(srcFile))
throw new IOException("Source '" + srcFile + "' exists but is a directory");
Path parentFile = destFile.getParent();
Files.createDirectories(parentFile);
if (Files.exists(destFile) && !Files.isWritable(destFile))
throw new IOException("Destination '" + destFile + "' exists but is read-only");
Files.copy(srcFile, destFile, StandardCopyOption.COPY_ATTRIBUTES, StandardCopyOption.REPLACE_EXISTING);
}
public static void moveFile(File srcFile, File destFile) throws IOException { public static void moveFile(File srcFile, File destFile) throws IOException {
copyFile(srcFile, destFile); copyFile(srcFile, destFile);
srcFile.delete(); srcFile.delete();

View File

@@ -19,9 +19,14 @@ package org.jackhuang.hmcl.util.javafx;
import static java.util.Objects.requireNonNull; import static java.util.Objects.requireNonNull;
import java.util.Objects;
import java.util.concurrent.Executor;
import java.util.function.Function; import java.util.function.Function;
import java.util.function.Supplier; import java.util.function.Supplier;
import org.jackhuang.hmcl.util.InvocationDispatcher;
import javafx.application.Platform;
import javafx.beans.binding.ObjectBinding; import javafx.beans.binding.ObjectBinding;
import javafx.beans.value.ObservableValue; import javafx.beans.value.ObservableValue;
@@ -53,6 +58,10 @@ public abstract class MultiStepBinding<T, U> extends ObjectBinding<U> {
return new FlatMappedBinding<>(map(mapper), nullAlternative); 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> { private static class SimpleBinding<T> extends MultiStepBinding<T, T> {
public SimpleBinding(ObservableValue<T> predecessor) { 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) { public <V> MultiStepBinding<?, V> map(Function<T, V> mapper) {
return new MappedBinding<>(predecessor, 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> { 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;
}
}
} }

View File

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

View File

@@ -33,6 +33,10 @@ public class ObservableHelper implements Observable, InvalidationListener {
private List<InvalidationListener> listeners = new CopyOnWriteArrayList<>(); private List<InvalidationListener> listeners = new CopyOnWriteArrayList<>();
private Observable source; private Observable source;
public ObservableHelper() {
this.source = this;
}
public ObservableHelper(Observable source) { public ObservableHelper(Observable source) {
this.source = source; this.source = source;
} }

View File

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

View File

@@ -25,6 +25,7 @@ import java.util.*;
/** /**
* Copied from org.apache.maven.artifact.versioning.ComparableVersion * Copied from org.apache.maven.artifact.versioning.ComparableVersion
* Apache License 2.0 * Apache License 2.0
* @see <a href="http://maven.apache.org/pom.html#Version_Order_Specification">Specification</a>
*/ */
public class VersionNumber implements Comparable<VersionNumber> { public class VersionNumber implements Comparable<VersionNumber> {

Binary file not shown.

Binary file not shown.

View File

@@ -23,7 +23,7 @@
<ProjectGuid>{672B1019-E741-4C0D-A986-627E2ACE157B}</ProjectGuid> <ProjectGuid>{672B1019-E741-4C0D-A986-627E2ACE157B}</ProjectGuid>
<Keyword>Win32Proj</Keyword> <Keyword>Win32Proj</Keyword>
<RootNamespace>HMCL</RootNamespace> <RootNamespace>HMCL</RootNamespace>
<WindowsTargetPlatformVersion>8.1</WindowsTargetPlatformVersion> <WindowsTargetPlatformVersion>7.0</WindowsTargetPlatformVersion>
</PropertyGroup> </PropertyGroup>
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.Default.props" /> <Import Project="$(VCTargetsPath)\Microsoft.Cpp.Default.props" />
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|Win32'" Label="Configuration"> <PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|Win32'" Label="Configuration">
@@ -152,19 +152,25 @@
</Link> </Link>
</ItemDefinitionGroup> </ItemDefinitionGroup>
<ItemGroup> <ItemGroup>
<ClInclude Include="HMCL.h" /> <ClInclude Include="java.h" />
<ClInclude Include="main.h" />
<ClInclude Include="os.h" />
<ClInclude Include="Resource.h" /> <ClInclude Include="Resource.h" />
<ClInclude Include="stdafx.h" /> <ClInclude Include="stdafx.h" />
<ClInclude Include="targetver.h" /> <ClInclude Include="targetver.h" />
<ClInclude Include="Version.h" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<ClCompile Include="HMCL.cpp" /> <ClCompile Include="java.cpp" />
<ClCompile Include="main.cpp" />
<ClCompile Include="os.cpp" />
<ClCompile Include="stdafx.cpp"> <ClCompile Include="stdafx.cpp">
<PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Debug|Win32'">Create</PrecompiledHeader> <PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Debug|Win32'">Create</PrecompiledHeader>
<PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">Create</PrecompiledHeader> <PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">Create</PrecompiledHeader>
<PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Release|Win32'">Create</PrecompiledHeader> <PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Release|Win32'">Create</PrecompiledHeader>
<PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Release|x64'">Create</PrecompiledHeader> <PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Release|x64'">Create</PrecompiledHeader>
</ClCompile> </ClCompile>
<ClCompile Include="Version.cpp" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<ResourceCompile Include="HMCL.rc" /> <ResourceCompile Include="HMCL.rc" />

View File

@@ -24,7 +24,16 @@
<ClInclude Include="Resource.h"> <ClInclude Include="Resource.h">
<Filter>头文件</Filter> <Filter>头文件</Filter>
</ClInclude> </ClInclude>
<ClInclude Include="HMCL.h"> <ClInclude Include="main.h">
<Filter>头文件</Filter>
</ClInclude>
<ClInclude Include="Version.h">
<Filter>头文件</Filter>
</ClInclude>
<ClInclude Include="java.h">
<Filter>头文件</Filter>
</ClInclude>
<ClInclude Include="os.h">
<Filter>头文件</Filter> <Filter>头文件</Filter>
</ClInclude> </ClInclude>
</ItemGroup> </ItemGroup>
@@ -32,7 +41,16 @@
<ClCompile Include="stdafx.cpp"> <ClCompile Include="stdafx.cpp">
<Filter>源文件</Filter> <Filter>源文件</Filter>
</ClCompile> </ClCompile>
<ClCompile Include="HMCL.cpp"> <ClCompile Include="main.cpp">
<Filter>源文件</Filter>
</ClCompile>
<ClCompile Include="Version.cpp">
<Filter>源文件</Filter>
</ClCompile>
<ClCompile Include="java.cpp">
<Filter>源文件</Filter>
</ClCompile>
<ClCompile Include="os.cpp">
<Filter>源文件</Filter> <Filter>源文件</Filter>
</ClCompile> </ClCompile>
</ItemGroup> </ItemGroup>

View File

@@ -0,0 +1,15 @@
#include "stdafx.h"
#include "Version.h"
Version::Version(const std::wstring & rawString)
{
int idx = 0;
ver[0] = ver[1] = ver[2] = ver[3] = 0;
for (auto &i : rawString)
{
if (idx >= 4) break;
if (i == '.') ++idx;
else if (i == '_') ++idx;
else if (isdigit(i)) ver[idx] = ver[idx] * 10 + (i - L'0');
}
}

View File

@@ -0,0 +1,20 @@
#pragma once
#include <string>
class Version
{
public:
int ver[4];
Version(const std::wstring &rawString);
bool operator<(const Version &other) const
{
for (int i = 0; i < 4; ++i)
if (ver[i] != other.ver[i])
return ver[i] < other.ver[i];
return false;
}
};

88
HMCLauncher/HMCL/java.cpp Normal file
View File

@@ -0,0 +1,88 @@
#include "stdafx.h"
#include "java.h"
#include "os.h"
const Version JAVA_8(L"1.8"), JAVA_11(L"11");
const LPCWSTR JDK_OLD = L"SOFTWARE\\JavaSoft\\Java Development Kit";
const LPCWSTR JRE_OLD = L"SOFTWARE\\JavaSoft\\Java Runtime Environment";
const LPCWSTR JDK_NEW = L"SOFTWARE\\JavaSoft\\JDK";
const LPCWSTR JRE_NEW = L"SOFTWARE\\JavaSoft\\JRE";
bool oldJavaFound = false, newJavaFound = false;
bool FindJavaByRegistryKey(HKEY rootKey, LPCWSTR subKey, std::wstring & path)
{
WCHAR javaVer[MAX_KEY_LENGTH]; // buffer for subkey name, special for JavaVersion
DWORD cbName; // size of name string
DWORD cSubKeys = 0; // number of subkeys
DWORD cbMaxSubKey; // longest subkey size
DWORD cValues; // number of values for key
DWORD cchMaxValue; // longest value name
DWORD cbMaxValueData; // longest value data
LSTATUS result;
HKEY hKey;
if (ERROR_SUCCESS != (result = RegOpenKeyEx(rootKey, subKey, 0, KEY_WOW64_64KEY | KEY_READ, &hKey)))
return false;
RegQueryInfoKey(
hKey, // key handle
NULL, // buffer for class name
NULL, // size of class string
NULL, // reserved
&cSubKeys, // number of subkeys
&cbMaxSubKey, // longest subkey size
NULL, // longest class string
&cValues, // number of values for this key
&cchMaxValue, // longest value name
&cbMaxValueData, // longest value data
NULL, // security descriptor
NULL); // last write time
if (!cSubKeys)
return false;
bool flag = false;
for (DWORD i = 0; i < cSubKeys; ++i)
{
cbName = MAX_KEY_LENGTH;
if (ERROR_SUCCESS != (result = RegEnumKeyEx(hKey, i, javaVer, &cbName, NULL, NULL, NULL, NULL)))
continue;
HKEY javaKey;
if (ERROR_SUCCESS != RegOpenKeyEx(hKey, javaVer, 0, KEY_READ, &javaKey))
continue;
if (ERROR_SUCCESS == MyRegQueryValue(javaKey, L"JavaHome", REG_SZ, path))
{
if (Version(javaVer) < JAVA_8)
oldJavaFound = true;
else if (!(Version(javaVer) < JAVA_11))
newJavaFound = true;
else
flag = true;
}
if (flag)
break;
}
RegCloseKey(hKey);
return flag;
}
bool FindJavaInRegistry(std::wstring & path)
{
return FindJavaByRegistryKey(HKEY_LOCAL_MACHINE, JDK_OLD, path) ||
FindJavaByRegistryKey(HKEY_LOCAL_MACHINE, JRE_OLD, path) ||
FindJavaByRegistryKey(HKEY_LOCAL_MACHINE, JDK_NEW, path) ||
FindJavaByRegistryKey(HKEY_LOCAL_MACHINE, JRE_NEW, path);
}
bool FindJava(std::wstring & path)
{
return FindJavaInRegistry(path) ||
ERROR_SUCCESS == MyGetEnvironmentVariable(L"JAVA_HOME", path);
}

10
HMCLauncher/HMCL/java.h Normal file
View File

@@ -0,0 +1,10 @@
#pragma once
#include <windows.h>
#include <string>
#include "Version.h"
// Find Java installation in system registry
bool FindJavaInRegistry(std::wstring &path);
// Find Java Installation in registry and environment variable
bool FindJava(std::wstring &path);

79
HMCLauncher/HMCL/main.cpp Normal file
View File

@@ -0,0 +1,79 @@
#include "stdafx.h"
#include "main.h"
#include "os.h"
#include "java.h"
using namespace std;
void LaunchJVM(const wstring &javaPath, const wstring &jarPath)
{
if (MyCreateProcess(L"\"" + javaPath + L"\" -XX:MinHeapFreeRatio=5 -XX:MaxHeapFreeRatio=15 -jar \"" + jarPath + L"\""))
exit(EXIT_SUCCESS);
}
int APIENTRY wWinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPWSTR lpCmdLine, int nCmdShow)
{
wstring path, exeName;
// Since Jar file is appended to this executable, we should first get the location of JAR file.
if (ERROR_SUCCESS != MyGetModuleFileName(NULL, exeName))
return 1;
// TODO: check whether the bundled JRE is valid.
// First try the Java packaged together.
bool is64Bit = false;
GetArch(is64Bit); // if failed to determine architecture of operating system, consider 32-bit.
if (is64Bit)
{
LaunchJVM(L"jre-x64\\bin\\javaw.exe", exeName);
}
else
{
LaunchJVM(L"jre-x86\\bin\\javaw.exe", exeName);
}
if (FindJava(path))
LaunchJVM(path + L"\\bin\\javaw.exe", exeName);
// Try java in PATH
LaunchJVM(L"javaw", exeName);
// Or we try to search Java in C:\Program Files.
{
WIN32_FIND_DATA data;
HANDLE hFind = FindFirstFile(L"C:\\Program Files\\Java\\*", &data); // Search all subdirectory
if (hFind != INVALID_HANDLE_VALUE) {
do {
wstring javaw = wstring(L"C:\\Program Files\\Java\\") + data.cFileName + wstring(L"\\bin\\javaw.exe");
if (FindFirstFileExists(javaw.c_str(), 0)) {
LaunchJVM(javaw, exeName);
}
} while (FindNextFile(hFind, &data));
FindClose(hFind);
}
}
// Consider C:\Program Files (x86)\Java
{
WIN32_FIND_DATA data;
HANDLE hFind = FindFirstFile(L"C:\\Program Files (x86)\\Java\\*", &data); // Search all subdirectory
if (hFind != INVALID_HANDLE_VALUE) {
do {
wstring javaw = wstring(L"C:\\Program Files (x86)\\Java\\") + data.cFileName + L"\\bin\\javaw.exe";
if (FindFirstFileExists(javaw.c_str(), 0)) {
LaunchJVM(javaw, exeName);
}
} while (FindNextFile(hFind, &data));
FindClose(hFind);
}
}
MessageBox(NULL, L"Java installation cannot be found in this computer, please download it from https://java.com \n"
L"未能在这台电脑上找到Java 8~Java 10请从 https://java.com 下载安装Java", L"Error", MB_ICONERROR | MB_OK);
ShellExecute(0, 0, L"https://java.com/", 0, 0, SW_SHOW);
return 1;
}

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