add: copy instance. Closes #687

This commit is contained in:
huanghongxun 2020-03-19 13:12:51 +08:00
parent 3e2bb9678d
commit b2f6ef72c3
19 changed files with 370 additions and 59 deletions

View File

@ -20,6 +20,7 @@ package org.jackhuang.hmcl.game;
import com.google.gson.Gson; import com.google.gson.Gson;
import com.google.gson.GsonBuilder; import com.google.gson.GsonBuilder;
import javafx.scene.image.Image; import javafx.scene.image.Image;
import org.jackhuang.hmcl.mod.Modpack;
import org.jackhuang.hmcl.setting.EnumGameDirectory; import org.jackhuang.hmcl.setting.EnumGameDirectory;
import org.jackhuang.hmcl.setting.Profile; import org.jackhuang.hmcl.setting.Profile;
import org.jackhuang.hmcl.setting.VersionSetting; import org.jackhuang.hmcl.setting.VersionSetting;
@ -36,7 +37,9 @@ import static org.jackhuang.hmcl.ui.FXUtils.newImage;
public class HMCLGameRepository extends DefaultGameRepository { public class HMCLGameRepository extends DefaultGameRepository {
private final Profile profile; private final Profile profile;
private final Map<String, VersionSetting> versionSettings = new HashMap<>();
// local version settings
private final Map<String, VersionSetting> localVersionSettings = new HashMap<>();
private final Set<String> beingModpackVersions = new HashSet<>(); private final Set<String> beingModpackVersions = new HashSet<>();
public boolean checkedModpack = false, checkingModpack = false; public boolean checkedModpack = false, checkingModpack = false;
@ -55,7 +58,7 @@ public class HMCLGameRepository extends DefaultGameRepository {
if (beingModpackVersions.contains(id) || isModpack(id)) if (beingModpackVersions.contains(id) || isModpack(id))
return getVersionRoot(id); return getVersionRoot(id);
else { else {
VersionSetting vs = profile.getVersionSetting(id); VersionSetting vs = getVersionSetting(id);
switch (vs.getGameDirType()) { switch (vs.getGameDirType()) {
case VERSION_FOLDER: return getVersionRoot(id); case VERSION_FOLDER: return getVersionRoot(id);
case ROOT_FOLDER: return super.getRunDirectory(id); case ROOT_FOLDER: return super.getRunDirectory(id);
@ -67,9 +70,9 @@ public class HMCLGameRepository extends DefaultGameRepository {
@Override @Override
protected void refreshVersionsImpl() { protected void refreshVersionsImpl() {
versionSettings.clear(); localVersionSettings.clear();
super.refreshVersionsImpl(); super.refreshVersionsImpl();
versions.keySet().forEach(this::loadVersionSetting); versions.keySet().forEach(this::loadLocalVersionSetting);
try { try {
File file = new File(getBaseDirectory(), "launcher_profiles.json"); File file = new File(getBaseDirectory(), "launcher_profiles.json");
@ -95,19 +98,55 @@ public class HMCLGameRepository extends DefaultGameRepository {
clean(getRunDirectory(id)); clean(getRunDirectory(id));
} }
private File getVersionSettingFile(String id) { public void duplicateVersion(String srcId, String dstId, boolean copySaves) throws IOException {
File srcDir = getVersionRoot(srcId);
File dstDir = getVersionRoot(dstId);
if (dstDir.exists()) throw new IOException("Version exists");
FileUtils.copyDirectory(srcDir.toPath(), dstDir.toPath());
VersionSetting oldVersionSetting = getVersionSetting(srcId).clone();
EnumGameDirectory originalGameDirType = oldVersionSetting.getGameDirType();
oldVersionSetting.setUsesGlobal(false);
oldVersionSetting.setGameDirType(EnumGameDirectory.VERSION_FOLDER);
VersionSetting newVersionSetting = initLocalVersionSetting(dstId, oldVersionSetting);
saveVersionSetting(dstId);
File srcGameDir = getRunDirectory(srcId);
File dstGameDir = getRunDirectory(dstId);
List<String> blackList = new ArrayList<>(Arrays.asList(
"regex:(.*?)\\.log",
"usernamecache.json", "usercache.json", // Minecraft
"launcher_profiles.json", "launcher.pack.lzma", // Minecraft Launcher
"backup", "pack.json", "launcher.jar", "cache", // HMCL
".curseclient", // Curse
".fabric", ".mixin.out", // Fabric
"jars", "logs", "versions", "assets", "libraries", "crash-reports", "NVIDIA", "AMD", "screenshots", "natives", "native", "$native", "server-resource-packs", // Minecraft
"downloads", // Curse
"asm", "backups", "TCNodeTracker", "CustomDISkins", "data", "CustomSkinLoader/caches" // Mods
));
blackList.add(srcId + ".jar");
blackList.add(srcId + ".json");
if (!copySaves)
blackList.add("saves");
if (originalGameDirType != EnumGameDirectory.VERSION_FOLDER)
FileUtils.copyDirectory(srcGameDir.toPath(), dstGameDir.toPath(), path -> Modpack.acceptFile(path, blackList, null));
}
private File getLocalVersionSettingFile(String id) {
return new File(getVersionRoot(id), "hmclversion.cfg"); return new File(getVersionRoot(id), "hmclversion.cfg");
} }
private void loadVersionSetting(String id) { private void loadLocalVersionSetting(String id) {
File file = getVersionSettingFile(id); File file = getLocalVersionSettingFile(id);
if (file.exists()) if (file.exists())
try { try {
VersionSetting versionSetting = GSON.fromJson(FileUtils.readText(file), VersionSetting.class); VersionSetting versionSetting = GSON.fromJson(FileUtils.readText(file), VersionSetting.class);
initVersionSetting(id, versionSetting); initLocalVersionSetting(id, versionSetting);
} catch (Exception ex) { } catch (Exception ex) {
// If [JsonParseException], [IOException] or [NullPointerException] happens, the json file is malformed and needed to be recreated. // If [JsonParseException], [IOException] or [NullPointerException] happens, the json file is malformed and needed to be recreated.
initVersionSetting(id, new VersionSetting()); initLocalVersionSetting(id, new VersionSetting());
} }
} }
@ -116,18 +155,18 @@ public class HMCLGameRepository extends DefaultGameRepository {
* @param id the version id. * @param id the version id.
* @return new version setting, null if given version does not exist. * @return new version setting, null if given version does not exist.
*/ */
public VersionSetting createVersionSetting(String id) { public VersionSetting createLocalVersionSetting(String id) {
if (!hasVersion(id)) if (!hasVersion(id))
return null; return null;
if (versionSettings.containsKey(id)) if (localVersionSettings.containsKey(id))
return getVersionSetting(id); return getLocalVersionSetting(id);
else else
return initVersionSetting(id, new VersionSetting()); return initLocalVersionSetting(id, new VersionSetting());
} }
private VersionSetting initVersionSetting(String id, VersionSetting vs) { private VersionSetting initLocalVersionSetting(String id, VersionSetting vs) {
localVersionSettings.put(id, vs);
vs.addPropertyChangedListener(a -> saveVersionSetting(id)); vs.addPropertyChangedListener(a -> saveVersionSetting(id));
versionSettings.put(id, vs);
return vs; return vs;
} }
@ -136,17 +175,27 @@ public class HMCLGameRepository extends DefaultGameRepository {
* *
* @param id version id * @param id version id
* *
* @return may return null if the id not exists * @return corresponding version setting, null if the version has no its own version setting.
*/ */
public VersionSetting getVersionSetting(String id) { public VersionSetting getLocalVersionSetting(String id) {
if (!versionSettings.containsKey(id)) if (!localVersionSettings.containsKey(id))
loadVersionSetting(id); loadLocalVersionSetting(id);
VersionSetting setting = versionSettings.get(id); VersionSetting setting = localVersionSettings.get(id);
if (setting != null && isModpack(id)) if (setting != null && isModpack(id))
setting.setGameDirType(EnumGameDirectory.VERSION_FOLDER); setting.setGameDirType(EnumGameDirectory.VERSION_FOLDER);
return setting; return setting;
} }
public VersionSetting getVersionSetting(String id) {
VersionSetting vs = getLocalVersionSetting(id);
if (vs == null || vs.isUsesGlobal()) {
profile.getGlobal().setGlobal(true); // always keep global.isGlobal = true
profile.getGlobal().setUsesGlobal(true);
return profile.getGlobal();
} else
return vs;
}
public File getVersionIconFile(String id) { public File getVersionIconFile(String id) {
return new File(getVersionRoot(id), "icon.png"); return new File(getVersionRoot(id), "icon.png");
} }
@ -169,14 +218,14 @@ public class HMCLGameRepository extends DefaultGameRepository {
} }
public boolean saveVersionSetting(String id) { public boolean saveVersionSetting(String id) {
if (!versionSettings.containsKey(id)) if (!localVersionSettings.containsKey(id))
return false; return false;
File file = getVersionSettingFile(id); File file = getLocalVersionSettingFile(id);
if (!FileUtils.makeDirectory(file.getAbsoluteFile().getParentFile())) if (!FileUtils.makeDirectory(file.getAbsoluteFile().getParentFile()))
return false; return false;
try { try {
FileUtils.writeText(file, GSON.toJson(versionSettings.get(id))); FileUtils.writeText(file, GSON.toJson(localVersionSettings.get(id)));
return true; return true;
} catch (IOException e) { } catch (IOException e) {
Logging.LOG.log(Level.SEVERE, "Unable to save version setting of " + id, e); Logging.LOG.log(Level.SEVERE, "Unable to save version setting of " + id, e);
@ -190,9 +239,9 @@ public class HMCLGameRepository extends DefaultGameRepository {
* @return specialized version setting, null if given version does not exist. * @return specialized version setting, null if given version does not exist.
*/ */
public VersionSetting specializeVersionSetting(String id) { public VersionSetting specializeVersionSetting(String id) {
VersionSetting vs = getVersionSetting(id); VersionSetting vs = getLocalVersionSetting(id);
if (vs == null) if (vs == null)
vs = createVersionSetting(id); vs = createLocalVersionSetting(id);
if (vs == null) if (vs == null)
return null; return null;
vs.setUsesGlobal(false); vs.setUsesGlobal(false);
@ -200,7 +249,7 @@ public class HMCLGameRepository extends DefaultGameRepository {
} }
public void globalizeVersionSetting(String id) { public void globalizeVersionSetting(String id) {
VersionSetting vs = getVersionSetting(id); VersionSetting vs = getLocalVersionSetting(id);
if (vs != null) if (vs != null)
vs.setUsesGlobal(true); vs.setUsesGlobal(true);
} }

View File

@ -166,13 +166,7 @@ public final class Profile implements Observable {
} }
public VersionSetting getVersionSetting(String id) { public VersionSetting getVersionSetting(String id) {
VersionSetting vs = repository.getVersionSetting(id); return repository.getVersionSetting(id);
if (vs == null || vs.isUsesGlobal()) {
getGlobal().setGlobal(true); // always keep global.isGlobal = true
getGlobal().setUsesGlobal(true);
return getGlobal();
} else
return vs;
} }
@Override @Override

View File

@ -45,7 +45,7 @@ import static org.jackhuang.hmcl.setting.ConfigHolder.config;
* @author huangyuhui * @author huangyuhui
*/ */
@JsonAdapter(VersionSetting.Serializer.class) @JsonAdapter(VersionSetting.Serializer.class)
public final class VersionSetting { public final class VersionSetting implements Cloneable {
public transient String id; public transient String id;
@ -563,6 +563,34 @@ public final class VersionSetting {
return builder.create(); return builder.create();
} }
@Override
public VersionSetting clone() {
VersionSetting versionSetting = new VersionSetting();
versionSetting.setUsesGlobal(isUsesGlobal());
versionSetting.setJava(getJava());
versionSetting.setDefaultJavaPath(getDefaultJavaPath());
versionSetting.setJavaDir(getJavaDir());
versionSetting.setWrapper(getWrapper());
versionSetting.setPermSize(getPermSize());
versionSetting.setMaxMemory(getMaxMemory());
versionSetting.setMinMemory(getMinMemory());
versionSetting.setPreLaunchCommand(getPreLaunchCommand());
versionSetting.setJavaArgs(getJavaArgs());
versionSetting.setMinecraftArgs(getMinecraftArgs());
versionSetting.setNoJVMArgs(isNoJVMArgs());
versionSetting.setNotCheckGame(isNotCheckGame());
versionSetting.setNotCheckJVM(isNotCheckJVM());
versionSetting.setShowLogs(isShowLogs());
versionSetting.setServerIp(getServerIp());
versionSetting.setFullscreen(isFullscreen());
versionSetting.setWidth(getWidth());
versionSetting.setHeight(getHeight());
versionSetting.setGameDirType(getGameDirType());
versionSetting.setGameDir(getGameDir());
versionSetting.setLauncherVisibility(getLauncherVisibility());
return versionSetting;
}
public static class Serializer implements JsonSerializer<VersionSetting>, JsonDeserializer<VersionSetting> { public static class Serializer implements JsonSerializer<VersionSetting>, JsonDeserializer<VersionSetting> {
@Override @Override
public JsonElement serialize(VersionSetting src, Type typeOfSrc, JsonSerializationContext context) { public JsonElement serialize(VersionSetting src, Type typeOfSrc, JsonSerializationContext context) {

View File

@ -31,6 +31,7 @@ import org.jackhuang.hmcl.ui.animation.ContainerAnimations;
import org.jackhuang.hmcl.ui.construct.InputDialogPane; import org.jackhuang.hmcl.ui.construct.InputDialogPane;
import org.jackhuang.hmcl.ui.construct.MessageDialogPane; import org.jackhuang.hmcl.ui.construct.MessageDialogPane;
import org.jackhuang.hmcl.ui.construct.MessageDialogPane.MessageType; import org.jackhuang.hmcl.ui.construct.MessageDialogPane.MessageType;
import org.jackhuang.hmcl.ui.construct.PromptDialogPane;
import org.jackhuang.hmcl.ui.construct.TaskExecutorDialogPane; import org.jackhuang.hmcl.ui.construct.TaskExecutorDialogPane;
import org.jackhuang.hmcl.ui.decorator.DecoratorController; import org.jackhuang.hmcl.ui.decorator.DecoratorController;
import org.jackhuang.hmcl.ui.main.RootPage; import org.jackhuang.hmcl.ui.main.RootPage;
@ -40,6 +41,7 @@ import org.jackhuang.hmcl.util.Logging;
import org.jackhuang.hmcl.util.io.FileUtils; import org.jackhuang.hmcl.util.io.FileUtils;
import org.jackhuang.hmcl.util.platform.JavaVersion; import org.jackhuang.hmcl.util.platform.JavaVersion;
import java.util.List;
import java.util.concurrent.CompletableFuture; import java.util.concurrent.CompletableFuture;
import java.util.function.Consumer; import java.util.function.Consumer;
@ -143,14 +145,19 @@ public final class Controllers {
dialog(new MessageDialogPane(text, title, onAccept, onCancel)); dialog(new MessageDialogPane(text, title, onAccept, onCancel));
} }
public static CompletableFuture<String> prompt(String text, FutureCallback<String> onResult) { public static CompletableFuture<String> prompt(String title, FutureCallback<String> onResult) {
return prompt(text, onResult, ""); return prompt(title, onResult, "");
} }
public static CompletableFuture<String> prompt(String text, FutureCallback<String> onResult, String initialValue) { public static CompletableFuture<String> prompt(String title, FutureCallback<String> onResult, String initialValue) {
InputDialogPane pane = new InputDialogPane(text, onResult); InputDialogPane pane = new InputDialogPane(title, initialValue, onResult);
dialog(pane);
return pane.getCompletableFuture();
}
public static CompletableFuture<List<PromptDialogPane.Builder.Question<?>>> prompt(PromptDialogPane.Builder builder) {
PromptDialogPane pane = new PromptDialogPane(builder);
dialog(pane); dialog(pane);
pane.setInitialValue(initialValue);
return pane.getCompletableFuture(); return pane.getCompletableFuture();
} }

View File

@ -67,6 +67,10 @@ public final class SVG {
return createSVGPath("M19,6.41L17.59,5L12,10.59L6.41,5L5,6.41L10.59,12L5,17.59L6.41,19L12,13.41L17.59,19L19,17.59L13.41,12L19,6.41Z", fill, width, height); return createSVGPath("M19,6.41L17.59,5L12,10.59L6.41,5L5,6.41L10.59,12L5,17.59L6.41,19L12,13.41L17.59,19L19,17.59L13.41,12L19,6.41Z", fill, width, height);
} }
public static Node copy(ObjectBinding<? extends Paint> fill, double width, double height) {
return createSVGPath("M19,21H8V7H19M19,5H8A2,2 0 0,0 6,7V21A2,2 0 0,0 8,23H19A2,2 0 0,0 21,21V7A2,2 0 0,0 19,5M16,1H4A2,2 0 0,0 2,3V17H4V3H16V1Z", fill, width, height);
}
public static Node dotsVertical(ObjectBinding<? extends Paint> fill, double width, double height) { public static Node dotsVertical(ObjectBinding<? extends Paint> fill, double width, double height) {
return createSVGPath("M12,16A2,2 0 0,1 14,18A2,2 0 0,1 12,20A2,2 0 0,1 10,18A2,2 0 0,1 12,16M12,10A2,2 0 0,1 14,12A2,2 0 0,1 12,14A2,2 0 0,1 10,12A2,2 0 0,1 12,10M12,4A2,2 0 0,1 14,6A2,2 0 0,1 12,8A2,2 0 0,1 10,6A2,2 0 0,1 12,4Z", fill, width, height); return createSVGPath("M12,16A2,2 0 0,1 14,18A2,2 0 0,1 12,20A2,2 0 0,1 10,18A2,2 0 0,1 12,16M12,10A2,2 0 0,1 14,12A2,2 0 0,1 12,14A2,2 0 0,1 10,12A2,2 0 0,1 12,10M12,4A2,2 0 0,1 14,6A2,2 0 0,1 12,8A2,2 0 0,1 10,6A2,2 0 0,1 12,4Z", fill, width, height);
} }

View File

@ -19,10 +19,10 @@ package org.jackhuang.hmcl.ui.construct;
import com.jfoenix.controls.JFXButton; import com.jfoenix.controls.JFXButton;
import com.jfoenix.controls.JFXTextField; import com.jfoenix.controls.JFXTextField;
import javafx.beans.binding.Bindings;
import javafx.fxml.FXML; import javafx.fxml.FXML;
import javafx.scene.control.Label; import javafx.scene.control.Label;
import javafx.scene.layout.StackPane; import javafx.scene.layout.StackPane;
import javafx.scene.layout.VBox;
import org.jackhuang.hmcl.ui.FXUtils; import org.jackhuang.hmcl.ui.FXUtils;
import org.jackhuang.hmcl.util.FutureCallback; import org.jackhuang.hmcl.util.FutureCallback;
@ -36,17 +36,20 @@ public class InputDialogPane extends StackPane {
@FXML @FXML
private JFXButton cancelButton; private JFXButton cancelButton;
@FXML @FXML
private JFXTextField textField; private Label title;
@FXML @FXML
private Label content; private VBox vbox;
@FXML @FXML
private Label lblCreationWarning; private Label lblCreationWarning;
@FXML @FXML
private SpinnerPane acceptPane; private SpinnerPane acceptPane;
public InputDialogPane(String text, FutureCallback<String> onResult) { public InputDialogPane(String text, String initialValue, FutureCallback<String> onResult) {
FXUtils.loadFXML(this, "/assets/fxml/input-dialog.fxml"); FXUtils.loadFXML(this, "/assets/fxml/input-dialog.fxml");
content.setText(text); title.setText(text);
JFXTextField textField = new JFXTextField();
textField.setText(initialValue);
vbox.getChildren().setAll(textField);
cancelButton.setOnMouseClicked(e -> fireEvent(new DialogCloseEvent())); cancelButton.setOnMouseClicked(e -> fireEvent(new DialogCloseEvent()));
acceptButton.setOnMouseClicked(e -> { acceptButton.setOnMouseClicked(e -> {
acceptPane.showSpinner(); acceptPane.showSpinner();
@ -60,15 +63,6 @@ public class InputDialogPane extends StackPane {
lblCreationWarning.setText(msg); lblCreationWarning.setText(msg);
}); });
}); });
acceptButton.disableProperty().bind(Bindings.createBooleanBinding(
() -> !textField.validate(),
textField.textProperty()
));
}
public void setInitialValue(String value) {
textField.setText(value);
} }
public CompletableFuture<String> getCompletableFuture() { public CompletableFuture<String> getCompletableFuture() {

View File

@ -0,0 +1,157 @@
/*
* Hello Minecraft! Launcher
* Copyright (C) 2020 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.ui.construct;
import com.jfoenix.controls.JFXButton;
import com.jfoenix.controls.JFXCheckBox;
import com.jfoenix.controls.JFXTextField;
import com.jfoenix.validation.base.ValidatorBase;
import javafx.beans.binding.Bindings;
import javafx.beans.binding.BooleanBinding;
import javafx.fxml.FXML;
import javafx.geometry.Insets;
import javafx.scene.control.Label;
import javafx.scene.layout.HBox;
import javafx.scene.layout.StackPane;
import javafx.scene.layout.VBox;
import org.jackhuang.hmcl.ui.FXUtils;
import org.jackhuang.hmcl.util.FutureCallback;
import org.jackhuang.hmcl.util.StringUtils;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.concurrent.CompletableFuture;
public class PromptDialogPane extends StackPane {
private final CompletableFuture<List<Builder.Question<?>>> future = new CompletableFuture<>();
@FXML
private JFXButton acceptButton;
@FXML
private JFXButton cancelButton;
@FXML
private VBox vbox;
@FXML
private Label title;
@FXML
private Label lblCreationWarning;
@FXML
private SpinnerPane acceptPane;
public PromptDialogPane(Builder builder) {
FXUtils.loadFXML(this, "/assets/fxml/input-dialog.fxml");
this.title.setText(builder.title);
List<BooleanBinding> bindings = new ArrayList<>();
for (Builder.Question<?> question : builder.questions) {
if (question instanceof Builder.StringQuestion) {
JFXTextField textField = new JFXTextField();
textField.textProperty().addListener((a, b, newValue) -> ((Builder.StringQuestion) question).value = textField.getText());
textField.setText(((Builder.StringQuestion) question).value);
textField.setValidators(((Builder.StringQuestion) question).validators.toArray(new ValidatorBase[0]));
bindings.add(Bindings.createBooleanBinding(textField::validate, textField.textProperty()));
if (StringUtils.isNotBlank(question.question)) {
vbox.getChildren().add(new Label(question.question));
}
VBox.setMargin(textField, new Insets(0, 0, 20, 0));
vbox.getChildren().add(textField);
} else if (question instanceof Builder.BooleanQuestion) {
HBox hBox = new HBox();
JFXCheckBox checkBox = new JFXCheckBox();
hBox.getChildren().setAll(checkBox);
HBox.setMargin(checkBox, new Insets(0, 0, 0, -10));
checkBox.setSelected(((Builder.BooleanQuestion) question).value);
checkBox.selectedProperty().addListener((a, b, newValue) -> ((Builder.BooleanQuestion) question).value = newValue);
checkBox.setText(question.question);
vbox.getChildren().add(hBox);
}
}
cancelButton.setOnMouseClicked(e -> fireEvent(new DialogCloseEvent()));
acceptButton.disableProperty().bind(Bindings.createBooleanBinding(
() -> bindings.stream().map(BooleanBinding::get).anyMatch(x -> !x),
bindings.toArray(new BooleanBinding[0])
));
acceptButton.setOnMouseClicked(e -> {
acceptPane.showSpinner();
builder.callback.call(builder.questions, () -> {
acceptPane.hideSpinner();
future.complete(builder.questions);
fireEvent(new DialogCloseEvent());
}, msg -> {
acceptPane.hideSpinner();
lblCreationWarning.setText(msg);
});
});
}
public CompletableFuture<List<Builder.Question<?>>> getCompletableFuture() {
return future;
}
public static class Builder {
private final List<Question<?>> questions = new ArrayList<>();
private final String title;
private final FutureCallback<List<Question<?>>> callback;
public Builder(String title, FutureCallback<List<Question<?>>> callback) {
this.title = title;
this.callback = callback;
}
public <T> Builder addQuestion(Question<T> question) {
questions.add(question);
return this;
}
public static class Question<T> {
public final String question;
protected T value;
public Question(String question) {
this.question = question;
}
public T getValue() {
return value;
}
}
public static class StringQuestion extends Question<String> {
protected final List<ValidatorBase> validators;
public StringQuestion(String question, String defaultValue, ValidatorBase... validators) {
super(question);
this.value = defaultValue;
this.validators = Arrays.asList(validators);
}
}
public static class BooleanQuestion extends Question<Boolean> {
public BooleanQuestion(String question, boolean defaultValue) {
super(question);
this.value = defaultValue;
}
}
}
}

View File

@ -69,6 +69,10 @@ public class GameListItem extends Control {
Versions.renameVersion(profile, version); Versions.renameVersion(profile, version);
} }
public void duplicate() {
Versions.duplicateVersion(profile, version);
}
public void remove() { public void remove() {
Versions.deleteVersion(profile, version); Versions.deleteVersion(profile, version);
} }

View File

@ -69,6 +69,7 @@ public class GameListItemSkin extends SkinBase<GameListItem> {
new IconedMenuItem(FXUtils.limitingSize(SVG.gear(Theme.blackFillBinding(), 14, 14), 14, 14), i18n("version.manage.manage"), FXUtils.withJFXPopupClosing(() -> currentSkinnable.modifyGameSettings(), popup)), new IconedMenuItem(FXUtils.limitingSize(SVG.gear(Theme.blackFillBinding(), 14, 14), 14, 14), i18n("version.manage.manage"), FXUtils.withJFXPopupClosing(() -> currentSkinnable.modifyGameSettings(), popup)),
new MenuSeparator(), new MenuSeparator(),
new IconedMenuItem(FXUtils.limitingSize(SVG.pencil(Theme.blackFillBinding(), 14, 14), 14, 14), i18n("version.manage.rename"), FXUtils.withJFXPopupClosing(() -> currentSkinnable.rename(), popup)), new IconedMenuItem(FXUtils.limitingSize(SVG.pencil(Theme.blackFillBinding(), 14, 14), 14, 14), i18n("version.manage.rename"), FXUtils.withJFXPopupClosing(() -> currentSkinnable.rename(), popup)),
new IconedMenuItem(FXUtils.limitingSize(SVG.copy(Theme.blackFillBinding(), 14, 14), 14, 14), i18n("version.manage.duplicate"), FXUtils.withJFXPopupClosing(() -> currentSkinnable.duplicate(), popup)),
new IconedMenuItem(FXUtils.limitingSize(SVG.delete(Theme.blackFillBinding(), 14, 14), 14, 14), i18n("version.manage.remove"), FXUtils.withJFXPopupClosing(() -> currentSkinnable.remove(), popup)), new IconedMenuItem(FXUtils.limitingSize(SVG.delete(Theme.blackFillBinding(), 14, 14), 14, 14), i18n("version.manage.remove"), FXUtils.withJFXPopupClosing(() -> currentSkinnable.remove(), popup)),
new IconedMenuItem(FXUtils.limitingSize(SVG.export(Theme.blackFillBinding(), 14, 14), 14, 14), i18n("modpack.export"), FXUtils.withJFXPopupClosing(() -> currentSkinnable.export(), popup)), new IconedMenuItem(FXUtils.limitingSize(SVG.export(Theme.blackFillBinding(), 14, 14), 14, 14), i18n("modpack.export"), FXUtils.withJFXPopupClosing(() -> currentSkinnable.export(), popup)),
new MenuSeparator(), new MenuSeparator(),

View File

@ -192,6 +192,10 @@ public class VersionPage extends Control implements DecoratorPage {
Versions.deleteVersion(profile, version); Versions.deleteVersion(profile, version);
} }
private void duplicate() {
Versions.duplicateVersion(profile, version);
}
@Override @Override
protected Skin createDefaultSkin() { protected Skin createDefaultSkin() {
return new Skin(this); return new Skin(this);
@ -228,6 +232,9 @@ public class VersionPage extends Control implements DecoratorPage {
new IconedMenuItem(FXUtils.limitingSize(SVG.pencil(Theme.blackFillBinding(), 14, 14), 14, 14), i18n("version.manage.rename"), FXUtils.withJFXPopupClosing(() -> { new IconedMenuItem(FXUtils.limitingSize(SVG.pencil(Theme.blackFillBinding(), 14, 14), 14, 14), i18n("version.manage.rename"), FXUtils.withJFXPopupClosing(() -> {
Versions.renameVersion(getSkinnable().profile, currentVersion).thenApply(name -> getSkinnable().preferredVersionName = name); Versions.renameVersion(getSkinnable().profile, currentVersion).thenApply(name -> getSkinnable().preferredVersionName = name);
}, listViewItemPopup)), }, listViewItemPopup)),
new IconedMenuItem(FXUtils.limitingSize(SVG.copy(Theme.blackFillBinding(), 14, 14), 14, 14), i18n("version.manage.duplicate"), FXUtils.withJFXPopupClosing(() -> {
Versions.duplicateVersion(getSkinnable().profile, currentVersion);
}, listViewItemPopup)),
new IconedMenuItem(FXUtils.limitingSize(SVG.delete(Theme.blackFillBinding(), 14, 14), 14, 14), i18n("version.manage.remove"), FXUtils.withJFXPopupClosing(() -> { new IconedMenuItem(FXUtils.limitingSize(SVG.delete(Theme.blackFillBinding(), 14, 14), 14, 14), i18n("version.manage.remove"), FXUtils.withJFXPopupClosing(() -> {
Versions.deleteVersion(getSkinnable().profile, currentVersion); Versions.deleteVersion(getSkinnable().profile, currentVersion);
}, listViewItemPopup)), }, listViewItemPopup)),
@ -313,6 +320,7 @@ public class VersionPage extends Control implements DecoratorPage {
new IconedMenuItem(FXUtils.limitingSize(SVG.script(Theme.blackFillBinding(), 14, 14), 14, 14), i18n("version.launch_script"), FXUtils.withJFXPopupClosing(control::generateLaunchScript, managementPopup)), new IconedMenuItem(FXUtils.limitingSize(SVG.script(Theme.blackFillBinding(), 14, 14), 14, 14), i18n("version.launch_script"), FXUtils.withJFXPopupClosing(control::generateLaunchScript, managementPopup)),
new MenuSeparator(), new MenuSeparator(),
new IconedMenuItem(FXUtils.limitingSize(SVG.pencil(Theme.blackFillBinding(), 14, 14), 14, 14), i18n("version.manage.rename"), FXUtils.withJFXPopupClosing(control::rename, managementPopup)), new IconedMenuItem(FXUtils.limitingSize(SVG.pencil(Theme.blackFillBinding(), 14, 14), 14, 14), i18n("version.manage.rename"), FXUtils.withJFXPopupClosing(control::rename, managementPopup)),
new IconedMenuItem(FXUtils.limitingSize(SVG.copy(Theme.blackFillBinding(), 14, 14), 14, 14), i18n("version.manage.duplicate"), FXUtils.withJFXPopupClosing(control::duplicate, managementPopup)),
new IconedMenuItem(FXUtils.limitingSize(SVG.delete(Theme.blackFillBinding(), 14, 14), 14, 14), i18n("version.manage.remove"), FXUtils.withJFXPopupClosing(control::remove, managementPopup)), new IconedMenuItem(FXUtils.limitingSize(SVG.delete(Theme.blackFillBinding(), 14, 14), 14, 14), i18n("version.manage.remove"), FXUtils.withJFXPopupClosing(control::remove, managementPopup)),
new IconedMenuItem(FXUtils.limitingSize(SVG.export(Theme.blackFillBinding(), 14, 14), 14, 14), i18n("modpack.export"), FXUtils.withJFXPopupClosing(control::export, managementPopup)), new IconedMenuItem(FXUtils.limitingSize(SVG.export(Theme.blackFillBinding(), 14, 14), 14, 14), i18n("modpack.export"), FXUtils.withJFXPopupClosing(control::export, managementPopup)),
new MenuSeparator(), new MenuSeparator(),

View File

@ -24,12 +24,17 @@ import org.jackhuang.hmcl.game.LauncherHelper;
import org.jackhuang.hmcl.setting.Accounts; import org.jackhuang.hmcl.setting.Accounts;
import org.jackhuang.hmcl.setting.EnumGameDirectory; import org.jackhuang.hmcl.setting.EnumGameDirectory;
import org.jackhuang.hmcl.setting.Profile; import org.jackhuang.hmcl.setting.Profile;
import org.jackhuang.hmcl.task.Schedulers;
import org.jackhuang.hmcl.task.Task;
import org.jackhuang.hmcl.task.TaskExecutor; import org.jackhuang.hmcl.task.TaskExecutor;
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.PromptDialogPane;
import org.jackhuang.hmcl.ui.construct.Validator;
import org.jackhuang.hmcl.ui.download.ModpackInstallWizardProvider; import org.jackhuang.hmcl.ui.download.ModpackInstallWizardProvider;
import org.jackhuang.hmcl.ui.export.ExportWizardProvider; import org.jackhuang.hmcl.ui.export.ExportWizardProvider;
import org.jackhuang.hmcl.util.Logging; import org.jackhuang.hmcl.util.Logging;
import org.jackhuang.hmcl.util.StringUtils;
import org.jackhuang.hmcl.util.io.FileUtils; import org.jackhuang.hmcl.util.io.FileUtils;
import org.jackhuang.hmcl.util.platform.OperatingSystem; import org.jackhuang.hmcl.util.platform.OperatingSystem;
@ -72,6 +77,27 @@ public class Versions {
FXUtils.openFolder(profile.getRepository().getRunDirectory(version)); FXUtils.openFolder(profile.getRepository().getRunDirectory(version));
} }
public static void duplicateVersion(Profile profile, String version) {
Controllers.prompt(
new PromptDialogPane.Builder(i18n("version.manage.duplicate.prompt"), (res, resolve, reject) -> {
String newVersionName = ((PromptDialogPane.Builder.StringQuestion) res.get(0)).getValue();
boolean copySaves = ((PromptDialogPane.Builder.BooleanQuestion) res.get(1)).getValue();
Task.runAsync(() -> profile.getRepository().duplicateVersion(version, newVersionName, copySaves))
.thenComposeAsync(profile.getRepository().refreshVersionsAsync())
.whenComplete(Schedulers.javafx(), (result, exception) -> {
if (exception == null) {
resolve.run();
} else {
reject.accept(StringUtils.getStackTrace(exception));
profile.getRepository().removeVersionFromDisk(newVersionName);
}
}).start();
})
.addQuestion(new PromptDialogPane.Builder.StringQuestion(i18n("version.manage.duplicate.confirm"), version,
new Validator(i18n("install.new_game.already_exists"), newVersionName -> !profile.getRepository().hasVersion(newVersionName))))
.addQuestion(new PromptDialogPane.Builder.BooleanQuestion(i18n("version.manage.duplicate.duplicate_save"), false)));
}
public static void updateVersion(Profile profile, String version) { public static void updateVersion(Profile profile, String version) {
Controllers.getDecorator().startWizard(new ModpackInstallWizardProvider(profile, version)); Controllers.getDecorator().startWizard(new ModpackInstallWizardProvider(profile, version));
} }

View File

@ -6,15 +6,20 @@
<?import javafx.scene.control.Label?> <?import javafx.scene.control.Label?>
<?import javafx.scene.layout.StackPane?> <?import javafx.scene.layout.StackPane?>
<?import org.jackhuang.hmcl.ui.construct.SpinnerPane?> <?import org.jackhuang.hmcl.ui.construct.SpinnerPane?>
<?import javafx.scene.layout.HBox?>
<?import javafx.scene.layout.VBox?>
<fx:root xmlns="http://javafx.com/javafx" <fx:root xmlns="http://javafx.com/javafx"
xmlns:fx="http://javafx.com/fxml" xmlns:fx="http://javafx.com/fxml"
type="StackPane"> type="StackPane">
<JFXDialogLayout> <JFXDialogLayout>
<heading> <heading>
<Label fx:id="content" /> <HBox>
<Label fx:id="title" />
</HBox>
</heading> </heading>
<body> <body>
<JFXTextField fx:id="textField" /> <VBox fx:id="vbox">
</VBox>
</body> </body>
<actions> <actions>
<Label fx:id="lblCreationWarning"/> <Label fx:id="lblCreationWarning"/>

View File

@ -445,6 +445,10 @@ version.launch_script.success=Created script %s.
version.manage=All Versions version.manage=All Versions
version.manage.clean=Clear game directory version.manage.clean=Clear game directory
version.manage.clean.tooltip=Clear logs, crash-reports version.manage.clean.tooltip=Clear logs, crash-reports
version.manage.duplicate=Copy game instance
version.manage.duplicate.duplicate_save=Copy saves
version.manage.duplicate.prompt=Type new version name
version.manage.duplicate.confirm=
version.manage.manage=Manage Version version.manage.manage=Manage Version
version.manage.redownload_assets_index=Update Game Asset Files version.manage.redownload_assets_index=Update Game Asset Files
version.manage.remove=Delete this version version.manage.remove=Delete this version

View File

@ -444,6 +444,10 @@ version.launch_script.success=啟動指令碼已生成完畢: %s
version.manage=遊戲列表 version.manage=遊戲列表
version.manage.clean=清理遊戲目錄 version.manage.clean=清理遊戲目錄
version.manage.clean.tooltip=清理 logs, crash-reports version.manage.clean.tooltip=清理 logs, crash-reports
version.manage.duplicate=複製遊戲實例
version.manage.duplicate.duplicate_save=複製存檔
version.manage.duplicate.prompt=請輸入新遊戲實例名稱
version.manage.duplicate.confirm=將鎖定複製產生的新遊戲實例:強制版本隔離、遊戲設置獨立。
version.manage.manage=遊戲管理 version.manage.manage=遊戲管理
version.manage.redownload_assets_index=更新遊戲資源檔案 version.manage.redownload_assets_index=更新遊戲資源檔案
version.manage.remove=刪除該版本 version.manage.remove=刪除該版本

View File

@ -444,6 +444,10 @@ version.launch_script.success=启动脚本已生成完毕:%s
version.manage=游戏列表 version.manage=游戏列表
version.manage.clean=清理游戏目录 version.manage.clean=清理游戏目录
version.manage.clean.tooltip=清理 logs, crash-reports version.manage.clean.tooltip=清理 logs, crash-reports
version.manage.duplicate=复制游戏实例
version.manage.duplicate.duplicate_save=复制存档
version.manage.duplicate.prompt=请输入新游戏实例名称
version.manage.duplicate.confirm=将锁定复制产生的新游戏实例:强制版本隔离、游戏设置独立。
version.manage.manage=游戏管理 version.manage.manage=游戏管理
version.manage.redownload_assets_index=更新游戏资源文件 version.manage.redownload_assets_index=更新游戏资源文件
version.manage.remove=删除该版本 version.manage.remove=删除该版本

View File

@ -45,7 +45,7 @@ public interface ModAdviser {
"regex:(.*?)\\.log", "regex:(.*?)\\.log",
"usernamecache.json", "usercache.json", // Minecraft "usernamecache.json", "usercache.json", // Minecraft
"launcher_profiles.json", "launcher.pack.lzma", // Minecraft Launcher "launcher_profiles.json", "launcher.pack.lzma", // Minecraft Launcher
"pack.json", "launcher.jar", "cache", "modpack.cfg", // HMCL "backup", "pack.json", "launcher.jar", "cache", "modpack.cfg", // HMCL
"manifest.json", "minecraftinstance.json", ".curseclient", // Curse "manifest.json", "minecraftinstance.json", ".curseclient", // Curse
".fabric", ".mixin.out", // Fabric ".fabric", ".mixin.out", // Fabric
"jars", "logs", "versions", "assets", "libraries", "crash-reports", "NVIDIA", "AMD", "screenshots", "natives", "native", "$native", "server-resource-packs", // Minecraft "jars", "logs", "versions", "assets", "libraries", "crash-reports", "NVIDIA", "AMD", "screenshots", "natives", "native", "$native", "server-resource-packs", // Minecraft

View File

@ -93,6 +93,8 @@ public final class Modpack {
for (String s : blackList) for (String s : blackList)
if (path.equals(s)) if (path.equals(s))
return false; return false;
if (whiteList == null || whiteList.isEmpty())
return true;
for (String s : whiteList) for (String s : whiteList)
if (path.equals(s)) if (path.equals(s))
return true; return true;

View File

@ -21,5 +21,14 @@ import java.util.function.Consumer;
@FunctionalInterface @FunctionalInterface
public interface FutureCallback<T> { public interface FutureCallback<T> {
void call(T obj, Runnable resolve, Consumer<String> reject);
/**
* Callback of future, called after future finishes.
* This callback gives the feedback whether the result of future is acceptable or not,
* if not, giving the reason, and future will be relaunched when necessary.
* @param result result of the future
* @param resolve accept the result
* @param reject reject the result with failure reason
*/
void call(T result, Runnable resolve, Consumer<String> reject);
} }

View File

@ -30,6 +30,7 @@ import java.nio.file.attribute.BasicFileAttributes;
import java.util.LinkedList; import java.util.LinkedList;
import java.util.List; import java.util.List;
import java.util.Objects; import java.util.Objects;
import java.util.function.Predicate;
import static java.nio.charset.StandardCharsets.UTF_8; import static java.nio.charset.StandardCharsets.UTF_8;
@ -196,20 +197,30 @@ public final class FileUtils {
* @throws IOException if an I/O error occurs. * @throws IOException if an I/O error occurs.
*/ */
public static void copyDirectory(Path src, Path dest) throws IOException { public static void copyDirectory(Path src, Path dest) throws IOException {
copyDirectory(src, dest, path -> true);
}
public static void copyDirectory(Path src, Path dest, Predicate<String> filePredicate) throws IOException {
Files.walkFileTree(src, new SimpleFileVisitor<Path>(){ Files.walkFileTree(src, new SimpleFileVisitor<Path>(){
@Override @Override
public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException { public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException {
if (!filePredicate.test(src.relativize(file).toString())) {
return FileVisitResult.SKIP_SUBTREE;
}
Path destFile = dest.resolve(src.relativize(file).toString()); Path destFile = dest.resolve(src.relativize(file).toString());
Files.copy(file, destFile, StandardCopyOption.REPLACE_EXISTING); Files.copy(file, destFile, StandardCopyOption.REPLACE_EXISTING);
return FileVisitResult.CONTINUE; return FileVisitResult.CONTINUE;
} }
@Override @Override
public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs) throws IOException { public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs) throws IOException {
if (!filePredicate.test(src.relativize(dir).toString())) {
return FileVisitResult.SKIP_SUBTREE;
}
Path destDir = dest.resolve(src.relativize(dir).toString()); Path destDir = dest.resolve(src.relativize(dir).toString());
Files.createDirectories(destDir); Files.createDirectories(destDir);
return FileVisitResult.CONTINUE; return FileVisitResult.CONTINUE;
} }
}); });