Compare commits

...
22 Commits
Author SHA1 Message Date
huangyuhui b5f72af513 Friendly hint of failing to rename game version 2018-07-16 20:39:21 +08:00
huangyuhui 1532893c5f Change current directory to run directory for launching scripts 2018-07-16 19:51:12 +08:00
huanghongxun a3fbc9595a Merge pull request #385 from yushijinhun/relative-path
添加设置项:若可能,游戏目录使用相对路径
2018-07-15 18:51:34 +08:00
yushijinhun f789343b6e Add game dir option: use relative path 2018-07-14 14:40:59 +08:00
yushijinhun ff74417352 [FileItem]Replace setProperty with bindBidirectional 2018-07-14 14:40:34 +08:00
huanghongxun a0ff843d0c Merge pull request #383 from yushijinhun/fix-382
Fix #382
2018-07-13 18:39:02 +08:00
yushijinhun da93cb0dbb Fix #382 2018-07-13 18:19:18 +08:00
huangyuhui 0de446c608 Update JFoenix 2018-07-11 22:33:29 +08:00
huangyuhui 03bccf16d4 Remove replacement of logs 2018-07-11 20:56:36 +08:00
huangyuhui 635ae8c11e Remove unused preset themes 2018-07-11 20:55:56 +08:00
huangyuhui dfd5a82112 Fix #365 by blocking all hacked Minecraft logs text as XML 2018-07-11 20:11:09 +08:00
huangyuhui 7dad8d452f Filter preview version of OptiFine 2018-07-11 13:08:08 +08:00
huangyuhui 07f8294876 Fixed mis-unbound data. GH-377 2018-07-11 12:46:02 +08:00
huanghongxun 7185cc410b Merge pull request #376 from yushijinhun/fix
Fix selected account is not loaded
2018-07-09 21:41:44 +08:00
yushijinhun 2d985af60d Fix selected account is not loaded 2018-07-09 21:34:43 +08:00
huanghongxun bee2e64482 Merge pull request #373 from yushijinhun/account-page
完善账户信息页的显示
2018-07-09 19:14:26 +08:00
huanghongxun d5dd43c0c1 Merge pull request #372 from yushijinhun/bmclapi
支持从 BMCLAPI 下载 authlib-injector
2018-07-09 19:14:00 +08:00
yushijinhun 09470eeb8a Use auto layout instead of manual sizing 2018-07-09 14:35:30 +08:00
yushijinhun 094249a6fa Set margin of fields in AccountPage 2018-07-09 14:31:41 +08:00
yushijinhun 6b5302907a [AccountPage]Display auth server url in tooltip of auth server name 2018-07-09 14:09:45 +08:00
yushijinhun adc5b08765 Fix account page overflow 2018-07-09 14:04:30 +08:00
yushijinhun 0150e2a8d7 Support downloading authlib-injector from BMCLAPI 2018-07-09 13:17:19 +08:00
33 changed files with 420 additions and 180 deletions

No files matched your search

@@ -21,8 +21,10 @@ import org.jackhuang.hmcl.Launcher;
import org.jackhuang.hmcl.auth.AuthInfo;
import org.jackhuang.hmcl.launch.DefaultLauncher;
import org.jackhuang.hmcl.launch.ProcessListener;
import org.jackhuang.hmcl.util.CommandBuilder;
import java.util.List;
import java.util.Map;
/**
* @author huangyuhui
@@ -42,10 +44,10 @@ public final class HMCLGameLauncher extends DefaultLauncher {
}
@Override
protected void appendJvmArgs(List<String> result) {
super.appendJvmArgs(result);
result.add("-Dminecraft.launcher.version=" + Launcher.VERSION);
result.add("-Dminecraft.launcher.brand=" + Launcher.NAME);
protected Map<String, String> getConfigurations() {
Map<String, String> res = super.getConfigurations();
res.put("${launcher_name}", Launcher.NAME);
res.put("${launcher_version}", Launcher.VERSION);
return res;
}
}
@@ -434,7 +434,7 @@ public final class LauncherHelper {
logWindow.show();
logWindow.onDone.register(() -> {
logWindow.logLine("Commands: " + StringUtils.makeCommand(process.getCommands()), Log4jLevel.INFO);
logWindow.logLine("Command: " + new CommandBuilder().addAll(process.getCommands()).toString(), Log4jLevel.INFO);
for (Map.Entry<String, Log4jLevel> entry : logs)
logWindow.logLine(entry.getKey(), entry.getValue());
});
@@ -19,6 +19,9 @@ package org.jackhuang.hmcl.setting;
import com.google.gson.*;
import javafx.beans.InvalidationListener;
import javafx.beans.property.BooleanProperty;
import javafx.beans.property.SimpleBooleanProperty;
import org.jackhuang.hmcl.game.HMCLDependencyManager;
import org.jackhuang.hmcl.game.HMCLGameRepository;
import org.jackhuang.hmcl.mod.ModManager;
@@ -83,8 +86,18 @@ public final class Profile {
nameProperty.set(name);
}
public Profile() {
this("Default");
private BooleanProperty useRelativePathProperty = new SimpleBooleanProperty(this, "useRelativePath", false);
public BooleanProperty useRelativePathProperty() {
return useRelativePathProperty();
}
public boolean isUseRelativePath() {
return useRelativePathProperty.get();
}
public void setUseRelativePath(boolean useRelativePath) {
useRelativePathProperty.set(useRelativePath);
}
public Profile(String name) {
@@ -153,6 +166,7 @@ public final class Profile {
return new ToStringBuilder(this)
.append("gameDir", getGameDir())
.append("name", getName())
.append("useRelativePath", isUseRelativePath())
.toString();
}
@@ -160,6 +174,7 @@ public final class Profile {
nameProperty.addListener(listener);
globalProperty.addListener(listener);
gameDirProperty.addListener(listener);
useRelativePathProperty.addListener(listener);
}
public static final class Serializer implements JsonSerializer<Profile>, JsonDeserializer<Profile> {
@@ -176,6 +191,7 @@ public final class Profile {
JsonObject jsonObject = new JsonObject();
jsonObject.add("global", context.serialize(src.getGlobal()));
jsonObject.addProperty("gameDir", src.getGameDir().getPath());
jsonObject.addProperty("useRelativePath", src.isUseRelativePath());
return jsonObject;
}
@@ -188,6 +204,8 @@ public final class Profile {
Profile profile = new Profile("Default", new File(gameDir));
profile.setGlobal(context.deserialize(obj.get("global"), VersionSetting.class));
profile.setUseRelativePath(Optional.ofNullable(obj.get("useRelativePath")).map(JsonElement::getAsBoolean).orElse(false));
return profile;
}
@@ -84,6 +84,8 @@ public class Settings {
ConfigHolder.CONFIG.authlibInjectorServers.addListener(onInvalidating(this::removeDanglingAuthlibInjectorAccounts));
this.selectedAccount.set(accounts.get(ConfigHolder.CONFIG.selectedAccount.get()));
checkProfileMap();
save();
@@ -300,7 +302,7 @@ public class Settings {
* ACCOUNTS *
****************************************/
private final ImmediateObjectProperty<Account> selectedAccount = new ImmediateObjectProperty<Account>(this, "selectedAccount", accounts.get(ConfigHolder.CONFIG.selectedAccount.get())) {
private final ImmediateObjectProperty<Account> selectedAccount = new ImmediateObjectProperty<Account>(this, "selectedAccount", null) {
@Override
public Account get() {
Account a = super.get();
@@ -510,8 +512,12 @@ public class Settings {
private void checkProfileMap() {
if (getProfileMap().isEmpty()) {
getProfileMap().put(Profiles.DEFAULT_PROFILE, new Profile(Profiles.DEFAULT_PROFILE));
getProfileMap().put(Profiles.HOME_PROFILE, new Profile(Profiles.HOME_PROFILE, Launcher.MINECRAFT_DIRECTORY));
Profile current = new Profile(Profiles.DEFAULT_PROFILE);
current.setUseRelativePath(true);
getProfileMap().put(Profiles.DEFAULT_PROFILE, current);
Profile home = new Profile(Profiles.HOME_PROFILE, Launcher.MINECRAFT_DIRECTORY);
getProfileMap().put(Profiles.HOME_PROFILE, home);
}
}
@@ -31,13 +31,15 @@ import java.util.logging.Level;
public class Theme {
public static final Theme BLUE = new Theme("blue", "#5C6BC0");
public static final Theme DARK_BLUE = new Theme("dark_blue", "#283593");
public static final Theme GREEN = new Theme("green", "#43A047");
public static final Theme ORANGE = new Theme("orange", "#E67E22");
public static final Theme PURPLE = new Theme("purple", "#9C27B0");
public static final Theme RED = new Theme("red", "#F44336");
public static final Theme[] VALUES = new Theme[]{BLUE, DARK_BLUE, GREEN, ORANGE, PURPLE, RED};
public static final Color[] SUGGESTED_COLORS = new Color[]{
Color.web("#5C6BC0"), // blue
Color.web("#283593"), // dark blue
Color.web("#43A047"), // green
Color.web("#E67E22"), // orange
Color.web("#9C27B0"), // purple
Color.web("#F44336") // red
};
private final String color;
private final String name;
@@ -68,8 +70,7 @@ public class Theme {
}
public String[] getStylesheets() {
String name = isCustom() ? BLUE.getName() : this.name;
String css = Theme.class.getResource("/assets/css/" + name + ".css").toExternalForm();
String css;
try {
File temp = File.createTempFile("hmcl", ".css");
FileUtils.writeText(temp, IOUtils.readFullyAsString(Theme.class.getResourceAsStream("/assets/css/custom.css"))
@@ -77,7 +78,8 @@ public class Theme {
.replace("%font-color%", getColorDisplayName(getForegroundColor())));
css = temp.toURI().toString();
} catch (IOException e) {
Logging.LOG.log(Level.SEVERE, "Unable to create theme stylesheet", e);
Logging.LOG.log(Level.SEVERE, "Unable to create theme stylesheet. Fallback to blue theme.", e);
css = Theme.class.getResource("/assets/css/blue.css").toExternalForm();
}
return new String[]{
@@ -98,10 +100,6 @@ public class Theme {
if (name == null)
return Optional.empty();
for (Theme theme : VALUES)
if (theme.name.equalsIgnoreCase(name))
return Optional.of(theme);
if (name.startsWith("#"))
try {
Color.web(name);
@@ -31,6 +31,7 @@ import javafx.scene.layout.StackPane;
import org.jackhuang.hmcl.auth.Account;
import org.jackhuang.hmcl.auth.authlibinjector.AuthlibInjectorAccount;
import org.jackhuang.hmcl.auth.authlibinjector.AuthlibInjectorServer;
import org.jackhuang.hmcl.auth.offline.OfflineAccount;
import org.jackhuang.hmcl.auth.yggdrasil.YggdrasilAccount;
import org.jackhuang.hmcl.game.AccountHelper;
@@ -39,6 +40,8 @@ import org.jackhuang.hmcl.setting.Theme;
import org.jackhuang.hmcl.task.Schedulers;
import org.jackhuang.hmcl.ui.construct.ComponentList;
import org.jackhuang.hmcl.ui.wizard.DecoratorPage;
import static org.jackhuang.hmcl.ui.FXUtils.installTooltip;
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
import java.util.Optional;
@@ -78,18 +81,16 @@ public class AccountPage extends StackPane implements DecoratorPage {
FXUtils.loadFXML(this, "/assets/fxml/account.fxml");
FXUtils.setLimitWidth(this, 300);
if (account instanceof AuthlibInjectorAccount) {
lblServer.setText(((AuthlibInjectorAccount) account).getServer().getName());
FXUtils.setLimitHeight(this, 182);
AuthlibInjectorServer server = ((AuthlibInjectorAccount) account).getServer();
lblServer.setText(server.getName());
installTooltip(lblServer, server.getUrl());
} else {
componentList.removeChildren(paneServer);
if (account instanceof OfflineAccount) {
componentList.removeChildren(paneEmail);
FXUtils.setLimitHeight(this, 110);
} else
FXUtils.setLimitHeight(this, 145);
}
}
btnDelete.setGraphic(SVG.delete(Theme.blackFillBinding(), 15, 15));
@@ -31,6 +31,7 @@ import org.jackhuang.hmcl.ui.construct.InputDialogPane;
import org.jackhuang.hmcl.ui.construct.MessageBox;
import org.jackhuang.hmcl.ui.construct.MessageDialogPane;
import org.jackhuang.hmcl.ui.construct.TaskExecutorDialogPane;
import org.jackhuang.hmcl.util.FutureCallback;
import org.jackhuang.hmcl.util.JavaVersion;
import java.util.function.Consumer;
@@ -140,8 +141,10 @@ public final class Controllers {
dialog(new MessageDialogPane(text, title, onAccept, onCancel));
}
public static void inputDialog(String text, Consumer<String> onResult) {
dialog(new InputDialogPane(text, onResult));
public static InputDialogPane inputDialog(String text, FutureCallback<String> onResult) {
InputDialogPane pane = new InputDialogPane(text, onResult);
dialog(pane);
return pane;
}
public static Region taskDialog(TaskExecutor executor, String title, String subtitle) {
@@ -18,6 +18,7 @@
package org.jackhuang.hmcl.ui;
import com.jfoenix.controls.JFXButton;
import com.jfoenix.controls.JFXCheckBox;
import com.jfoenix.controls.JFXTextField;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.StringProperty;
@@ -40,12 +41,11 @@ public final class ProfilePage extends StackPane implements DecoratorPage {
private final StringProperty location;
private final Profile profile;
@FXML
private JFXTextField txtProfileName;
@FXML
private FileItem gameDir;
@FXML private JFXTextField txtProfileName;
@FXML private FileItem gameDir;
@FXML private JFXButton btnSave;
@FXML private JFXButton btnDelete;
@FXML private JFXCheckBox toggleUseRelativePath;
/**
* @param profile null if creating a new profile.
@@ -65,13 +65,16 @@ public final class ProfilePage extends StackPane implements DecoratorPage {
FXUtils.onChangeAndOperate(txtProfileName.textProperty(), it -> {
btnSave.setDisable(!txtProfileName.validate() || StringUtils.isBlank(getLocation()));
});
gameDir.setProperty(location);
gameDir.pathProperty().bindBidirectional(location);
FXUtils.onChangeAndOperate(location, it -> {
btnSave.setDisable(!txtProfileName.validate() || StringUtils.isBlank(getLocation()));
});
if (profile == null)
gameDir.convertToRelativePathProperty().bind(toggleUseRelativePath.selectedProperty());
if (profile == null) {
btnDelete.setVisible(false);
} else {
toggleUseRelativePath.setSelected(profile.isUseRelativePath());
}
}
@FXML
@@ -86,13 +89,17 @@ public final class ProfilePage extends StackPane implements DecoratorPage {
private void onSave() {
if (profile != null) {
profile.setName(txtProfileName.getText());
if (StringUtils.isNotBlank(getLocation()))
profile.setUseRelativePath(toggleUseRelativePath.isSelected());
if (StringUtils.isNotBlank(getLocation())) {
profile.setGameDir(new File(getLocation()));
}
} else {
if (StringUtils.isBlank(getLocation())) {
gameDir.onExplore();
}
Settings.INSTANCE.putProfile(new Profile(txtProfileName.getText(), new File(getLocation())));
Profile newProfile = new Profile(txtProfileName.getText(), new File(getLocation()));
newProfile.setUseRelativePath(toggleUseRelativePath.isSelected());
Settings.INSTANCE.putProfile(newProfile);
}
Settings.INSTANCE.onProfileLoading();
@@ -20,6 +20,7 @@ package org.jackhuang.hmcl.ui;
import com.jfoenix.controls.*;
import com.jfoenix.effects.JFXDepthManager;
import javafx.application.Platform;
import javafx.beans.binding.Bindings;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.StringProperty;
import javafx.collections.FXCollections;
@@ -178,7 +179,7 @@ public final class SettingsPage extends StackPane implements DecoratorPage {
chkProxyAuthentication.selectedProperty().addListener((a, b, newValue) -> Settings.INSTANCE.setHasProxyAuth(newValue));
authPane.disableProperty().bind(chkProxyAuthentication.selectedProperty().not());
fileCommonLocation.setProperty(Settings.INSTANCE.commonPathProperty());
fileCommonLocation.pathProperty().bindBidirectional(Settings.INSTANCE.commonPathProperty());
FXUtils.installTooltip(btnUpdate, i18n("update.tooltip"));
checkUpdate();
@@ -204,7 +205,7 @@ public final class SettingsPage extends StackPane implements DecoratorPage {
JFXColorPicker picker = new JFXColorPicker(Color.web(Settings.INSTANCE.getTheme().getColor()), null);
picker.setCustomColorText(i18n("color.custom"));
picker.setRecentColorsText(i18n("color.recent"));
picker.getCustomColors().setAll(Arrays.stream(Theme.VALUES).map(Theme::getColor).map(Color::web).collect(Collectors.toList()));
picker.getCustomColors().setAll(Theme.SUGGESTED_COLORS);
picker.setOnAction(e -> {
Theme theme = Theme.custom(Theme.getColorDisplayName(picker.getValue()));
Settings.INSTANCE.setTheme(theme);
@@ -195,12 +195,15 @@ public final class VersionPage extends StackPane implements DecoratorPage {
}
public static void renameVersion(Profile profile, String version) {
Controllers.inputDialog(i18n("version.manage.rename.message"), res -> {
Controllers.inputDialog(i18n("version.manage.rename.message"), (res, resolve, reject) -> {
if (profile.getRepository().renameVersion(version, res)) {
profile.getRepository().refreshVersionsAsync().start();
Controllers.navigate(null);
resolve.run();
} else {
reject.accept(i18n("version.manage.rename.fail"));
}
});
}).setInitialText(version);
}
public static void exportVersion(Profile profile, String version) {
@@ -45,7 +45,6 @@ import java.io.File;
import java.io.IOException;
import java.util.Arrays;
import java.util.List;
import java.util.Optional;
import java.util.logging.Level;
import java.util.stream.Collectors;
@@ -53,6 +52,7 @@ public final class VersionSettingsController {
private VersionSetting lastVersionSetting = null;
private Profile profile;
private String versionId;
private boolean javaItemsLoaded;
@FXML private VBox rootPane;
@FXML private ScrollPane scroll;
@@ -90,6 +90,7 @@ public final class VersionSettingsController {
(variables.<List<JavaVersion>>get("list")).stream()
.map(javaVersion -> javaItem.createChildren(javaVersion.getVersion(), javaVersion.getBinary().getAbsolutePath(), javaVersion))
.collect(Collectors.toList()));
javaItemsLoaded = true;
initializeSelectedJava();
});
@@ -116,6 +117,7 @@ public final class VersionSettingsController {
gameDirItem.setDisable(profile.getRepository().isModpack(versionId));
globalItem.setDisable(profile.getRepository().isModpack(versionId));
// unbind data fields
if (lastVersionSetting != null) {
FXUtils.unbindInt(txtWidth, lastVersionSetting.widthProperty());
FXUtils.unbindInt(txtHeight, lastVersionSetting.heightProperty());
@@ -135,6 +137,12 @@ public final class VersionSettingsController {
FXUtils.unbindEnum(cboLauncherVisibility);
}
// unbind data fields
globalItem.setToggleSelectedListener(null);
javaItem.setToggleSelectedListener(null);
gameDirItem.setToggleSelectedListener(null);
// bind new data fields
FXUtils.bindInt(txtWidth, versionSetting.widthProperty());
FXUtils.bindInt(txtHeight, versionSetting.heightProperty());
FXUtils.bindInt(txtMaxMemory, versionSetting.maxMemoryProperty());
@@ -205,23 +213,34 @@ public final class VersionSettingsController {
private void initializeSelectedJava() {
if (lastVersionSetting == null
|| !JavaVersion.getJREsImmediately().isPresent() /* JREs are still being loaded */) {
|| !javaItemsLoaded /* JREs are still being loaded */) {
return;
}
boolean flag = false;
Toggle defaultToggle = null;
for (Toggle toggle : javaItem.getGroup().getToggles()) {
if (toggle.getUserData() == Lang.invoke(lastVersionSetting::getJavaVersion)) {
toggle.setSelected(true);
flag = true;
} else if (toggle.getUserData() == JavaVersion.fromCurrentEnvironment()) {
defaultToggle = toggle;
List<Toggle> toggles = javaItem.getGroup().getToggles();
if ("Custom".equals(lastVersionSetting.getJava())) {
toggles.stream()
.filter(javaItem::isCustomToggle)
.findFirst()
.get().setSelected(true);
} else {
JavaVersion selectedJava;
try {
selectedJava = lastVersionSetting.getJavaVersion();
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
return;
}
}
if (!flag) {
Optional.ofNullable(defaultToggle).ifPresent(t -> t.setSelected(true));
toggles.stream()
.filter(it -> it.getUserData() == selectedJava)
.findFirst()
.orElseGet( // fallback to select current java
() -> toggles.stream()
.filter(it -> it.getUserData() == JavaVersion.fromCurrentEnvironment())
.findFirst()
.get())
.setSelected(true);
;
}
}
@@ -18,7 +18,9 @@
package org.jackhuang.hmcl.ui.construct;
import com.jfoenix.controls.JFXButton;
import javafx.beans.property.Property;
import javafx.beans.property.BooleanProperty;
import javafx.beans.property.SimpleBooleanProperty;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.StringProperty;
import javafx.scene.control.Label;
@@ -31,24 +33,30 @@ import org.jackhuang.hmcl.setting.Theme;
import org.jackhuang.hmcl.ui.Controllers;
import org.jackhuang.hmcl.ui.FXUtils;
import org.jackhuang.hmcl.ui.SVG;
import static org.jackhuang.hmcl.ui.FXUtils.onInvalidating;
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
import java.io.File;
import java.nio.file.Path;
import java.nio.file.Paths;
public class FileItem extends BorderPane {
private Property<String> property;
private final Label x = new Label();
private final Label lblPath = new Label();
private final SimpleStringProperty name = new SimpleStringProperty(this, "name");
private final SimpleStringProperty title = new SimpleStringProperty(this, "title");
private final SimpleStringProperty tooltip = new SimpleStringProperty(this, "tooltip");
private final SimpleStringProperty path = new SimpleStringProperty(this, "path");
private final SimpleBooleanProperty convertToRelativePath = new SimpleBooleanProperty(this, "convertToRelativePath");
public FileItem() {
VBox left = new VBox();
Label name = new Label();
name.textProperty().bind(nameProperty());
x.getStyleClass().addAll("subtitle-label");
left.getChildren().addAll(name, x);
lblPath.getStyleClass().addAll("subtitle-label");
lblPath.textProperty().bind(path);
left.getChildren().addAll(name, lblPath);
setLeft(left);
JFXButton right = new JFXButton();
@@ -61,12 +69,29 @@ public class FileItem extends BorderPane {
Tooltip tip = new Tooltip();
tip.textProperty().bind(tooltipProperty());
Tooltip.install(this, tip);
convertToRelativePath.addListener(onInvalidating(() -> path.set(processPath(path.get()))));
}
/**
* Converts the given path to absolute/relative(if possible) path according to {@link #convertToRelativePathProperty()}.
*/
private String processPath(String path) {
Path given = Paths.get(path).toAbsolutePath();
if (isConvertToRelativePath()) {
try {
return Paths.get(".").toAbsolutePath().relativize(given).normalize().toString();
} catch (IllegalArgumentException e) {
// the given path can't be relativized against current path
}
}
return given.normalize().toString();
}
public void onExplore() {
DirectoryChooser chooser = new DirectoryChooser();
if (property.getValue() != null) {
File file = new File(property.getValue());
if (path.get() != null) {
File file = new File(path.get());
if (file.exists()) {
if (file.isFile())
file = file.getAbsoluteFile().getParentFile();
@@ -77,16 +102,12 @@ public class FileItem extends BorderPane {
}
chooser.titleProperty().bind(titleProperty());
File selectedDir = chooser.showDialog(Controllers.getStage());
if (selectedDir != null)
property.setValue(selectedDir.getAbsolutePath());
if (selectedDir != null) {
path.set(processPath(selectedDir.toString()));
}
chooser.titleProperty().unbind();
}
public void setProperty(Property<String> property) {
this.property = property;
x.textProperty().bind(property);
}
public String getName() {
return name.get();
}
@@ -122,4 +143,28 @@ public class FileItem extends BorderPane {
public void setTooltip(String tooltip) {
this.tooltip.set(tooltip);
}
public String getPath() {
return path.get();
}
public StringProperty pathProperty() {
return path;
}
public void setPath(String path) {
this.path.set(path);
}
public boolean isConvertToRelativePath() {
return convertToRelativePath.get();
}
public BooleanProperty convertToRelativePathProperty() {
return convertToRelativePath;
}
public void setConvertToRelativePath(boolean convertToRelativePath) {
this.convertToRelativePath.set(convertToRelativePath);
}
}
@@ -19,12 +19,12 @@ package org.jackhuang.hmcl.ui.construct;
import com.jfoenix.controls.JFXButton;
import com.jfoenix.controls.JFXTextField;
import javafx.beans.binding.Bindings;
import javafx.fxml.FXML;
import javafx.scene.control.Label;
import javafx.scene.layout.StackPane;
import org.jackhuang.hmcl.ui.FXUtils;
import java.util.function.Consumer;
import org.jackhuang.hmcl.util.FutureCallback;
public class InputDialogPane extends StackPane {
@@ -36,14 +36,28 @@ public class InputDialogPane extends StackPane {
private JFXTextField textField;
@FXML
private Label content;
@FXML
private Label lblCreationWarning;
public InputDialogPane(String text, Consumer<String> onResult) {
public InputDialogPane(String text, FutureCallback<String> onResult) {
FXUtils.loadFXML(this, "/assets/fxml/input-dialog.fxml");
content.setText(text);
cancelButton.setOnMouseClicked(e -> fireEvent(new DialogCloseEvent()));
acceptButton.setOnMouseClicked(e -> {
onResult.accept(textField.getText());
fireEvent(new DialogCloseEvent());
onResult.call(textField.getText(), () -> {
fireEvent(new DialogCloseEvent());
}, msg -> {
lblCreationWarning.setText(msg);
});
});
acceptButton.disableProperty().bind(Bindings.createBooleanBinding(
() -> !textField.validate(),
textField.textProperty()
));
}
public void setInitialText(String text) {
textField.setText(text);
}
}
@@ -1,5 +0,0 @@
.root {
-fx-base-color: #283593;
-fx-base-check-color: derive(-fx-base-color, 30%);
-fx-base-text-fill: white;
}
@@ -1,5 +0,0 @@
.root {
-fx-base-color: #43A047;
-fx-base-check-color: derive(-fx-base-color, 30%);
-fx-base-text-fill: black;
}
@@ -1,5 +0,0 @@
.root {
-fx-base-color: #E67E22;
-fx-base-check-color: derive(-fx-base-color, 30%);
-fx-base-text-fill: black;
}
@@ -1,5 +0,0 @@
.root {
-fx-base-color: #9C27B0;
-fx-base-check-color: derive(-fx-base-color, 30%);
-fx-base-text-fill: white;
}
@@ -1,5 +0,0 @@
.root {
-fx-base-color: #F44336;
-fx-base-check-color: derive(-fx-base-color, 30%);
-fx-base-text-fill: black;
}
@@ -2,6 +2,7 @@
<?import com.jfoenix.controls.JFXButton?>
<?import javafx.scene.control.*?>
<?import javafx.geometry.*?>
<?import javafx.scene.layout.*?>
<?import org.jackhuang.hmcl.ui.construct.ComponentList?>
<?import com.jfoenix.controls.JFXProgressBar?>
@@ -20,6 +21,9 @@
</left>
<right>
<VBox>
<BorderPane.margin>
<Insets left="15.0" />
</BorderPane.margin>
<Label fx:id="lblType" BorderPane.alignment="CENTER_LEFT"/>
</VBox>
</right>
@@ -33,6 +37,9 @@
</left>
<right>
<VBox>
<BorderPane.margin>
<Insets left="15.0" />
</BorderPane.margin>
<Label fx:id="lblCharacter" BorderPane.alignment="CENTER_LEFT"/>
</VBox>
</right>
@@ -46,6 +53,9 @@
</left>
<right>
<VBox>
<BorderPane.margin>
<Insets left="15.0" />
</BorderPane.margin>
<Label fx:id="lblEmail" BorderPane.alignment="CENTER_LEFT"/>
</VBox>
</right>
@@ -59,24 +69,26 @@
</left>
<right>
<VBox>
<BorderPane.margin>
<Insets left="15.0" />
</BorderPane.margin>
<Label fx:id="lblServer" BorderPane.alignment="CENTER_LEFT"/>
</VBox>
</right>
</BorderPane>
</ComponentList>
<BorderPane pickOnBounds="false" style="-fx-padding: 4;">
<left>
<JFXButton BorderPane.alignment="BOTTOM_LEFT" fx:id="btnDelete" onMouseClicked="#onDelete"
styleClass="toggle-icon4" maxWidth="30" maxHeight="30" minWidth="30" minHeight="30"
prefWidth="30" prefHeight="30"/>
</left>
<right>
<JFXButton BorderPane.alignment="BOTTOM_RIGHT" fx:id="btnRefresh" onMouseClicked="#onRefresh"
styleClass="toggle-icon4" maxWidth="30" maxHeight="30" minWidth="30" minHeight="30"
prefWidth="30" prefHeight="30"/>
</right>
</BorderPane>
</VBox>
<BorderPane pickOnBounds="false" style="-fx-padding: 4;">
<left>
<JFXButton BorderPane.alignment="BOTTOM_LEFT" fx:id="btnDelete" onMouseClicked="#onDelete"
styleClass="toggle-icon4" maxWidth="30" maxHeight="30" minWidth="30" minHeight="30"
prefWidth="30" prefHeight="30"/>
</left>
<right>
<JFXButton BorderPane.alignment="BOTTOM_RIGHT" fx:id="btnRefresh" onMouseClicked="#onRefresh"
styleClass="toggle-icon4" maxWidth="30" maxHeight="30" minWidth="30" minHeight="30"
prefWidth="30" prefHeight="30"/>
</right>
</BorderPane>
<JFXProgressBar fx:id="progressBar" StackPane.alignment="TOP_CENTER" visible="false" />
</fx:root>
@@ -5,6 +5,7 @@
<?import com.jfoenix.controls.JFXTextField?>
<?import javafx.scene.control.Label?>
<?import javafx.scene.layout.StackPane?>
<?import org.jackhuang.hmcl.ui.construct.SpinnerPane?>
<fx:root xmlns="http://javafx.com/javafx"
xmlns:fx="http://javafx.com/fxml"
type="StackPane">
@@ -16,8 +17,11 @@
<JFXTextField fx:id="textField" />
</body>
<actions>
<Label fx:id="lblCreationWarning"/>
<SpinnerPane fx:id="acceptPane" styleClass="small-spinner-pane">
<JFXButton fx:id="acceptButton" text="%button.ok" styleClass="dialog-accept"/>
</SpinnerPane>
<JFXButton fx:id="cancelButton" styleClass="dialog-cancel" text="%button.cancel" />
<JFXButton fx:id="acceptButton" styleClass="dialog-accept" text="%button.ok" />
</actions>
</JFXDialogLayout>
</fx:root>
@@ -1,11 +1,13 @@
<?xml version="1.0" encoding="UTF-8"?>
<?import com.jfoenix.controls.JFXButton?>
<?import com.jfoenix.controls.JFXCheckBox?>
<?import com.jfoenix.controls.JFXTextField?>
<?import com.jfoenix.validation.RequiredFieldValidator?>
<?import javafx.scene.control.Label?>
<?import javafx.scene.control.ScrollPane?>
<?import javafx.scene.layout.*?>
<?import javafx.geometry.*?>
<?import org.jackhuang.hmcl.ui.construct.ComponentList?>
<?import org.jackhuang.hmcl.ui.construct.FileItem?>
<fx:root xmlns="http://javafx.com/javafx"
@@ -32,6 +34,12 @@
</BorderPane>
<FileItem fx:id="gameDir" name="%profile.instance_directory" title="%profile.instance_directory.choose"/>
<JFXCheckBox fx:id="toggleUseRelativePath" text="%profile.use_relative_path" StackPane.alignment="CENTER_LEFT">
<StackPane.margin>
<Insets left="-10"/>
</StackPane.margin>
</JFXCheckBox>
</ComponentList>
</VBox>
@@ -240,6 +240,7 @@ profile.instance_directory.choose=Choose Game Directory
profile.new=New Config
profile.title=Game Directories
profile.selected=Selected
profile.use_relative_path=Use relative path for game directory if possible
selector.choose=Choose
selector.choose_file=Select a file
@@ -324,11 +325,12 @@ version.launch_script.failed=Unable to make launch script.
version.launch_script.save=Save the launch script
version.launch_script.success=Finished script creation, %s.
version.manage.redownload_assets_index=Redownload Assets Index
version.manage.remove=Delete this version
version.manage.remove=Delete this game
version.manage.remove.confirm=Sure to remove version %s?
version.manage.remove_libraries=Delete library files
version.manage.rename=Rename this version
version.manage.rename=Rename this game
version.manage.rename.message=Please enter the new name
version.manage.rename.fail=Failed to rename this game.
version.settings=Settings
version.update=Update
@@ -240,6 +240,7 @@ profile.instance_directory.choose=選擇遊戲路徑
profile.new=新建配置
profile.title=遊戲目錄
profile.selected=已選中
profile.use_relative_path=若可能,遊戲目錄使用相對路徑
selector.choose=選擇
selector.choose_file=選擇文件
@@ -329,6 +330,7 @@ version.manage.remove.confirm=真的要刪除版本 %s 嗎?
version.manage.remove_libraries=刪除所有庫文件
version.manage.rename=重命名該版本
version.manage.rename.message=請輸入要改成的名字
version.manage.rename.fail=重命名版本失敗,可能文件被佔用或者名字有特殊字符
version.settings=遊戲設置
version.update=更新
@@ -240,6 +240,7 @@ profile.instance_directory.choose=选择游戏路径
profile.new=新建配置
profile.title=游戏目录
profile.selected=已选中
profile.use_relative_path=若可能,游戏目录使用相对路径
selector.choose=选择
selector.choose_file=选择文件
@@ -260,7 +261,7 @@ settings.advanced.launcher_visibility.hide_reopen=隐藏启动器并在游戏结
settings.advanced.launcher_visibility.keep=保持启动器可见
settings.advanced.launcher_visible=启动器可见性
settings.advanced.minecraft_arguments=Minecraft 额外参数(不必填写)
settings.advanced.no_common=不使用公共路径
settings.advanced.no_common=不使用公共路径(游戏依赖文件存放在公共路径)
settings.advanced.no_jvm_args=不添加默认的 JVM 参数(使用 Java 9 时必勾)
settings.advanced.precall_command=启动前执行命令(不必填写,将在游戏启动前调用)
settings.advanced.server_ip=直入服务器 IP 地址(不必填写,启动游戏后直接进入对应服务器)
@@ -329,6 +330,7 @@ version.manage.remove.confirm=真的要删除版本 %s 吗?
version.manage.remove_libraries=删除所有库文件
version.manage.rename=重命名该版本
version.manage.rename.message=请输入要改成的名字
version.manage.rename.fail=重命名版本失败,可能文件被占用或者名字有特殊字符
version.settings=游戏设置
version.update=更新
@@ -67,7 +67,8 @@ public class BMCLAPIDownloadProvider implements DownloadProvider {
.replace("https://libraries.minecraft.net", "https://bmclapi2.bangbang93.com/libraries")
.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", "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");
}
}
@@ -20,6 +20,7 @@ package org.jackhuang.hmcl.download;
import org.jackhuang.hmcl.game.Library;
import org.jackhuang.hmcl.game.Version;
import org.jackhuang.hmcl.task.TaskResult;
import org.jackhuang.hmcl.util.CommandBuilder;
import org.jackhuang.hmcl.util.StringUtils;
import java.util.ArrayList;
@@ -93,7 +94,7 @@ public class MaintainTask extends TaskResult<Version> {
args.add("optifine.OptiFineForgeTweaker");
}
return version.setMinecraftArguments(StringUtils.makeCommand(args));
return version.setMinecraftArguments(new CommandBuilder().addAll(args).toString());
}
@Override
@@ -56,8 +56,8 @@ public final class OptiFineBMCLVersionList extends VersionList<Void> {
List<OptiFineVersion> root = Constants.GSON.fromJson(task.getResult(), new TypeToken<List<OptiFineVersion>>() {
}.getType());
for (OptiFineVersion element : root) {
String version = element.getType();
if (version == null)
String version = element.getType() + "_" + element.getPatch();
if (element.getType() == null || "pre".equals(element.getPatch()))
continue;
String mirror = "http://bmclapi2.bangbang93.com/optifine/" + element.getGameVersion() + "/" + element.getType() + "/" + element.getPatch();
if (!duplicates.add(mirror))
@@ -25,7 +25,6 @@ import java.io.*;
import java.nio.file.Files;
import java.util.*;
import java.util.function.Supplier;
import java.util.stream.Collectors;
import static org.jackhuang.hmcl.util.Lang.mapOf;
import static org.jackhuang.hmcl.util.Pair.pair;
@@ -48,8 +47,8 @@ public class DefaultLauncher extends Launcher {
super(repository, versionId, authInfo, options, listener, daemon);
}
private List<String> generateCommandLine(File nativeFolder) throws IOException {
List<String> res = new LinkedList<>();
private CommandBuilder generateCommandLine(File nativeFolder, boolean enableLoggingInfo) throws IOException {
CommandBuilder res = new CommandBuilder();
// Executable
if (StringUtils.isNotBlank(options.getWrapper()))
@@ -58,7 +57,7 @@ public class DefaultLauncher extends Launcher {
res.add(options.getJava().getBinary().toString());
if (StringUtils.isNotBlank(options.getJavaArgs()))
res.addAll(StringUtils.tokenize(options.getJavaArgs()));
res.addAllWithoutParsing(StringUtils.tokenize(options.getJavaArgs()));
// JVM Args
if (!options.isNoGeneratedJVMArgs()) {
@@ -72,7 +71,7 @@ public class DefaultLauncher extends Launcher {
}
Map<DownloadType, LoggingInfo> logging = version.getLogging();
if (logging != null) {
if (logging != null && enableLoggingInfo) {
LoggingInfo loggingInfo = logging.get(DownloadType.CLIENT);
if (loggingInfo != null) {
File loggingFile = repository.getLoggingObject(version.getId(), version.getAssetIndex().getId(), loggingInfo);
@@ -187,11 +186,10 @@ public class DefaultLauncher extends Launcher {
}
if (StringUtils.isNotBlank(options.getMinecraftArgs()))
res.addAll(StringUtils.tokenize(options.getMinecraftArgs()));
res.addAllWithoutParsing(StringUtils.tokenize(options.getMinecraftArgs()));
return res.stream()
.filter(it -> !getForbiddens().containsKey(it) || !getForbiddens().get(it).get())
.collect(Collectors.toList());
res.removeIf(it -> getForbiddens().containsKey(it) && getForbiddens().get(it).get());
return res;
}
public Map<String, Boolean> getFeatures() {
@@ -224,7 +222,7 @@ public class DefaultLauncher extends Launcher {
* -Dminecraft.launcher.brand=&lt;Your launcher version&gt;
* -Dlog4j.configurationFile=&lt;Your custom log4j configuration&gt;
*/
protected void appendJvmArgs(List<String> result) {
protected void appendJvmArgs(CommandBuilder result) {
}
public void decompressNatives(File destination) throws NotDecompressingNativesException {
@@ -262,18 +260,20 @@ public class DefaultLauncher extends Launcher {
File nativeFolder = Files.createTempDirectory("minecraft").toFile();
// To guarantee that when failed to generate launch command line, we will not call pre-launch command
List<String> rawCommandLine = generateCommandLine(nativeFolder);
List<String> rawCommandLine = generateCommandLine(nativeFolder, true).asList();
decompressNatives(nativeFolder);
File runDirectory = repository.getRunDirectory(version.getId());
if (StringUtils.isNotBlank(options.getPreLaunchCommand()))
Runtime.getRuntime().exec(options.getPreLaunchCommand()).waitFor();
new ProcessBuilder(options.getPreLaunchCommand())
.directory(runDirectory).start().waitFor();
Process process;
try {
ProcessBuilder builder = new ProcessBuilder(rawCommandLine);
builder.directory(repository.getRunDirectory(version.getId()))
.environment().put("APPDATA", options.getGameDir().getAbsoluteFile().getParent());
ProcessBuilder builder = new ProcessBuilder(rawCommandLine).directory(runDirectory);
builder.environment().put("APPDATA", options.getGameDir().getAbsoluteFile().getParent());
process = builder.start();
} catch (IOException e) {
throw new ProcessCreationException(e);
@@ -305,14 +305,14 @@ public class DefaultLauncher extends Launcher {
writer.newLine();
writer.write("set APPDATA=" + options.getGameDir().getAbsoluteFile().getParent());
writer.newLine();
writer.write("cd /D %APPDATA%");
writer.write(new CommandBuilder().add("cd", "/D", repository.getRunDirectory(version.getId()).getAbsolutePath()).toString());
writer.newLine();
}
if (StringUtils.isNotBlank(options.getPreLaunchCommand())) {
writer.write(options.getPreLaunchCommand());
writer.newLine();
}
writer.write(StringUtils.makeCommand(generateCommandLine(nativeFolder)));
writer.write(generateCommandLine(nativeFolder, false).toString());
}
if (!scriptFile.setExecutable(true))
throw new PermissionException();
@@ -323,7 +323,8 @@ public class DefaultLauncher extends Launcher {
}
private void startMonitors(ManagedProcess managedProcess, ProcessListener processListener, boolean isDaemon) {
boolean enablesLoggingInfo = version.getLogging() != null && version.getLogging().containsKey(DownloadType.CLIENT);
boolean enablesLoggingInfo = version.getLogging() != null && version.getLogging().containsKey(DownloadType.CLIENT)
&& !"net.minecraft.launchwrapper.Launch".equals(version.getMainClass());
if (enablesLoggingInfo)
startMonitorsWithLoggingInfo(managedProcess, processListener, isDaemon);
else
@@ -97,14 +97,8 @@ final class Log4jHandler extends Thread {
public Future<?> newLine(String log) {
return Schedulers.computation().schedule(() -> {
try {
String line = log
.replace("<![CDATA[", "")
.replace("]]>", "")
.replace("log4j:", "log4j_")
.replace("<log4j_Message>", "<log4j_Message><![CDATA[")
.replace("</log4j_Message>", "]]></log4j_Message>")
.replace("<log4j_Throwable>", "<log4j_Throwable><![CDATA[")
.replace("</log4j_Throwable>", "]]></log4j_Throwable>");
// Prevent from namespace parsing
String line = log.replace("log4j:", "log4j_");
logs.add(line);
if (broken)
System.out.println(line);
@@ -118,6 +112,9 @@ final class Log4jHandler extends Thread {
Logging.LOG.log(Level.WARNING, "An error occurred when writing console lines", e);
logs.forEach(System.out::println);
broken = true;
} else {
// Output plain XML to user
callback.accept(log, Log4jLevel.INFO);
}
}
});
@@ -0,0 +1,123 @@
/*
* Hello Minecraft! Launcher.
* Copyright (C) 2017 huangyuhui <[email protected]>
*
* 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 {http://www.gnu.org/licenses/}.
*/
package org.jackhuang.hmcl.util;
import java.util.*;
import java.util.function.Predicate;
import java.util.stream.Collectors;
public final class CommandBuilder {
private final OperatingSystem os;
private List<Item> raw = new LinkedList<>();
public CommandBuilder() {
this(OperatingSystem.CURRENT_OS);
}
public CommandBuilder(OperatingSystem os) {
this.os = os;
}
private String parse(String s) {
if (OperatingSystem.WINDOWS == os) {
return parseWindows(s);
} else {
return parseBash(s);
}
}
public CommandBuilder add(String... args) {
for (String s : args)
raw.add(new Item(s, true));
return this;
}
public CommandBuilder addAll(Collection<String> args) {
for (String s : args)
raw.add(new Item(s, true));
return this;
}
public CommandBuilder addWithoutParsing(String... args) {
for (String s : args)
raw.add(new Item(s, false));
return this;
}
public CommandBuilder addAllWithoutParsing(Collection<String> args) {
for (String s : args)
raw.add(new Item(s, false));
return this;
}
public boolean removeIf(Predicate<String> pred) {
return raw.removeIf(i -> pred.test(i.arg));
}
@Override
public String toString() {
return String.join(" ", raw.stream().map(i -> i.parse ? parse(i.arg) : i.arg).collect(Collectors.toList()));
}
public List<String> asList() {
return raw.stream().map(i -> i.arg).collect(Collectors.toList());
}
private static class Item {
String arg;
boolean parse;
Item(String arg, boolean parse) {
this.arg = arg;
this.parse = parse;
}
}
private static String parseWindows(String s) {
if (s.indexOf(' ') >= 0 || s.indexOf('\t') >= 0)
if (s.charAt(0) != '"') {
// The argument has not been quoted, add quotes.
return '"' + s.replace("\\", "\\\\").replace("\"", "\"\"") + '"';
} else if (s.endsWith("\"")) {
// The argument has already been quoted.
return s;
} else {
// Unmatched quote for the argument.
throw new IllegalArgumentException();
}
else {
return s;
}
}
private static String parseBash(String s) {
if (s.indexOf(' ') >= 0 || s.indexOf('\t') >= 0)
if (s.charAt(0) != '"') {
// The argument has not been quoted, add quotes.
return '"' + s.replace("\"", "\\\"") + '"';
} else if (s.endsWith("\"")) {
// The argument has already been quoted.
return s;
} else {
// Unmatched quote for the argument.
throw new IllegalArgumentException();
}
else
return s;
}
}
@@ -0,0 +1,25 @@
/*
* Hello Minecraft! Launcher.
* Copyright (C) 2017 huangyuhui <[email protected]>
*
* 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 {http://www.gnu.org/licenses/}.
*/
package org.jackhuang.hmcl.util;
import java.util.function.Consumer;
@FunctionalInterface
public interface FutureCallback<T> {
void call(T obj, Runnable resolve, Consumer<String> reject);
}
@@ -33,36 +33,6 @@ public final class StringUtils {
private StringUtils() {
}
public static String makeCommand(List<String> cmd) {
StringBuilder cmdbuf = new StringBuilder(120);
for (int i = 0; i < cmd.size(); i++) {
if (i > 0)
cmdbuf.append(' ');
String s = cmd.get(i);
if (s.indexOf(' ') >= 0 || s.indexOf('\t') >= 0)
if (s.charAt(0) != '"') {
cmdbuf.append('"');
cmdbuf.append(s);
if (s.endsWith("\\"))
cmdbuf.append("\\");
cmdbuf.append('"');
} else if (s.endsWith("\""))
/*
* The argument has already been quoted.
*/
cmdbuf.append(s);
else
/*
* Unmatched quote for the argument.
*/
throw new IllegalArgumentException();
else
cmdbuf.append(s);
}
return cmdbuf.toString();
}
public static String getStackTrace(Throwable throwable) {
ByteArrayOutputStream stream = new ByteArrayOutputStream();
throwable.printStackTrace(new PrintStream(stream));
BIN
View File
Binary file not shown.