mirror of
https://github.com/HMCL-dev/HMCL.git
synced 2025-03-01 17:25:53 +08:00
World and datapacks management
This commit is contained in:
parent
1753b4d27e
commit
07febc18d4
@ -17,6 +17,7 @@
|
||||
*/
|
||||
package org.jackhuang.hmcl.ui;
|
||||
|
||||
import com.jfoenix.concurrency.JFXUtilities;
|
||||
import com.jfoenix.controls.*;
|
||||
import javafx.animation.Animation;
|
||||
import javafx.animation.Interpolator;
|
||||
@ -210,27 +211,29 @@ public final class FXUtils {
|
||||
}
|
||||
|
||||
public static void installTooltip(Node node, double openDelay, double visibleDelay, double closeDelay, Tooltip tooltip) {
|
||||
try {
|
||||
// Java 8
|
||||
Class<?> behaviorClass = Class.forName("javafx.scene.control.Tooltip$TooltipBehavior");
|
||||
Constructor<?> behaviorConstructor = behaviorClass.getDeclaredConstructor(Duration.class, Duration.class, Duration.class, boolean.class);
|
||||
behaviorConstructor.setAccessible(true);
|
||||
Object behavior = behaviorConstructor.newInstance(new Duration(openDelay), new Duration(visibleDelay), new Duration(closeDelay), false);
|
||||
Method installMethod = behaviorClass.getDeclaredMethod("install", Node.class, Tooltip.class);
|
||||
installMethod.setAccessible(true);
|
||||
installMethod.invoke(behavior, node, tooltip);
|
||||
} catch (ReflectiveOperationException e) {
|
||||
JFXUtilities.runInFX(() -> {
|
||||
try {
|
||||
// Java 9
|
||||
Tooltip.class.getMethod("setShowDelay", Duration.class).invoke(tooltip, new Duration(openDelay));
|
||||
Tooltip.class.getMethod("setShowDuration", Duration.class).invoke(tooltip, new Duration(visibleDelay));
|
||||
Tooltip.class.getMethod("setHideDelay", Duration.class).invoke(tooltip, new Duration(closeDelay));
|
||||
} catch (ReflectiveOperationException e2) {
|
||||
e.addSuppressed(e2);
|
||||
Logging.LOG.log(Level.SEVERE, "Cannot install tooltip", e);
|
||||
// Java 8
|
||||
Class<?> behaviorClass = Class.forName("javafx.scene.control.Tooltip$TooltipBehavior");
|
||||
Constructor<?> behaviorConstructor = behaviorClass.getDeclaredConstructor(Duration.class, Duration.class, Duration.class, boolean.class);
|
||||
behaviorConstructor.setAccessible(true);
|
||||
Object behavior = behaviorConstructor.newInstance(new Duration(openDelay), new Duration(visibleDelay), new Duration(closeDelay), false);
|
||||
Method installMethod = behaviorClass.getDeclaredMethod("install", Node.class, Tooltip.class);
|
||||
installMethod.setAccessible(true);
|
||||
installMethod.invoke(behavior, node, tooltip);
|
||||
} catch (ReflectiveOperationException e) {
|
||||
try {
|
||||
// Java 9
|
||||
Tooltip.class.getMethod("setShowDelay", Duration.class).invoke(tooltip, new Duration(openDelay));
|
||||
Tooltip.class.getMethod("setShowDuration", Duration.class).invoke(tooltip, new Duration(visibleDelay));
|
||||
Tooltip.class.getMethod("setHideDelay", Duration.class).invoke(tooltip, new Duration(closeDelay));
|
||||
} catch (ReflectiveOperationException e2) {
|
||||
e.addSuppressed(e2);
|
||||
Logging.LOG.log(Level.SEVERE, "Cannot install tooltip", e);
|
||||
}
|
||||
Tooltip.install(node, tooltip);
|
||||
}
|
||||
Tooltip.install(node, tooltip);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public static void openFolder(File file) {
|
||||
|
@ -100,13 +100,15 @@ public class ComponentList extends Control {
|
||||
}
|
||||
|
||||
protected static class Skin extends SkinBase<ComponentList> {
|
||||
private final ObservableList<Node> list;
|
||||
|
||||
protected Skin(ComponentList control) {
|
||||
super(control);
|
||||
|
||||
list = MappedObservableList.create(control.getContent(), this::mapper);
|
||||
|
||||
VBox vbox = new VBox();
|
||||
Bindings.bindContent(vbox.getChildren(),
|
||||
MappedObservableList.create(control.getContent(), this::mapper));
|
||||
Bindings.bindContent(vbox.getChildren(), list);
|
||||
getChildren().setAll(vbox);
|
||||
}
|
||||
|
||||
|
@ -1,10 +0,0 @@
|
||||
package org.jackhuang.hmcl.ui.versions;
|
||||
|
||||
import org.jackhuang.hmcl.ui.ListPage;
|
||||
|
||||
public class DatapackList extends ListPage<DatapackListItem> {
|
||||
@Override
|
||||
public void add() {
|
||||
|
||||
}
|
||||
}
|
@ -1,6 +1,5 @@
|
||||
package org.jackhuang.hmcl.ui.versions;
|
||||
|
||||
import com.jfoenix.concurrency.JFXUtilities;
|
||||
import com.jfoenix.controls.JFXButton;
|
||||
import com.jfoenix.controls.JFXCheckBox;
|
||||
import com.jfoenix.effects.JFXDepthManager;
|
||||
@ -18,7 +17,7 @@ import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
|
||||
|
||||
public class DatapackListItem extends BorderPane {
|
||||
|
||||
public DatapackListItem(Datapack root, Datapack.Pack info, Consumer<DatapackListItem> deleteCallback) {
|
||||
public DatapackListItem(Datapack.Pack info, Consumer<DatapackListItem> deleteCallback) {
|
||||
JFXCheckBox chkEnabled = new JFXCheckBox();
|
||||
BorderPane.setAlignment(chkEnabled, Pos.CENTER);
|
||||
setLeft(chkEnabled);
|
||||
@ -28,9 +27,7 @@ public class DatapackListItem extends BorderPane {
|
||||
setCenter(modItem);
|
||||
|
||||
JFXButton btnRemove = new JFXButton();
|
||||
JFXUtilities.runInFX(() -> {
|
||||
FXUtils.installTooltip(btnRemove, i18n("mods.remove"));
|
||||
});
|
||||
FXUtils.installTooltip(btnRemove, i18n("datapack.remove"));
|
||||
btnRemove.setOnMouseClicked(e -> deleteCallback.accept(this));
|
||||
btnRemove.getStyleClass().add("toggle-icon4");
|
||||
BorderPane.setAlignment(btnRemove, Pos.CENTER);
|
||||
|
@ -0,0 +1,103 @@
|
||||
package org.jackhuang.hmcl.ui.versions;
|
||||
|
||||
import javafx.beans.property.SimpleStringProperty;
|
||||
import javafx.beans.property.StringProperty;
|
||||
import javafx.collections.ObservableList;
|
||||
import javafx.scene.input.TransferMode;
|
||||
import javafx.stage.FileChooser;
|
||||
import org.jackhuang.hmcl.mod.Datapack;
|
||||
import org.jackhuang.hmcl.ui.Controllers;
|
||||
import org.jackhuang.hmcl.ui.ListPage;
|
||||
import org.jackhuang.hmcl.ui.decorator.DecoratorPage;
|
||||
import org.jackhuang.hmcl.util.FileUtils;
|
||||
import org.jackhuang.hmcl.util.Logging;
|
||||
import org.jackhuang.hmcl.util.MappedObservableList;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Path;
|
||||
import java.util.*;
|
||||
import java.util.logging.Level;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
|
||||
|
||||
public class DatapackListPage extends ListPage<DatapackListItem> implements DecoratorPage {
|
||||
private final StringProperty title = new SimpleStringProperty();
|
||||
private final ObservableList<DatapackListItem> list; // Hold weak references
|
||||
private final Path worldDir;
|
||||
private final Datapack datapack;
|
||||
|
||||
public DatapackListPage(String worldName, Path worldDir) {
|
||||
this.worldDir = worldDir;
|
||||
|
||||
title.set(i18n("datapack.title", worldName));
|
||||
|
||||
datapack = new Datapack(worldDir.resolve("datapacks"));
|
||||
datapack.loadFromDir();
|
||||
|
||||
list = MappedObservableList.create(datapack.getInfo(), pack -> new DatapackListItem(pack, item -> {
|
||||
try {
|
||||
datapack.deletePack(pack.getId());
|
||||
} catch (IOException e) {
|
||||
Logging.LOG.warning("Failed to delete datapack");
|
||||
}
|
||||
}));
|
||||
itemsProperty().bindContent(list);
|
||||
|
||||
setOnDragOver(event -> {
|
||||
if (event.getGestureSource() != this && event.getDragboard().hasFiles())
|
||||
event.acceptTransferModes(TransferMode.COPY_OR_MOVE);
|
||||
event.consume();
|
||||
});
|
||||
|
||||
setOnDragDropped(event -> {
|
||||
List<File> files = event.getDragboard().getFiles();
|
||||
if (files != null) {
|
||||
Collection<File> mods = files.stream()
|
||||
.filter(it -> Objects.equals("zip", FileUtils.getExtension(it)))
|
||||
.collect(Collectors.toList());
|
||||
if (!mods.isEmpty()) {
|
||||
mods.forEach(it -> {
|
||||
try {
|
||||
Datapack zip = new Datapack(it.toPath());
|
||||
zip.loadFromZip();
|
||||
zip.installTo(worldDir);
|
||||
} catch (IOException | IllegalArgumentException e) {
|
||||
Logging.LOG.log(Level.WARNING, "Unable to parse datapack file " + it, e);
|
||||
}
|
||||
});
|
||||
event.setDropCompleted(true);
|
||||
}
|
||||
}
|
||||
datapack.loadFromDir();
|
||||
event.consume();
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public StringProperty titleProperty() {
|
||||
return title;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void add() {
|
||||
FileChooser chooser = new FileChooser();
|
||||
chooser.setTitle(i18n("datapack.choose_datapack"));
|
||||
chooser.getExtensionFilters().setAll(new FileChooser.ExtensionFilter(i18n("extension.datapack"), "*.zip"));
|
||||
List<File> res = chooser.showOpenMultipleDialog(Controllers.getStage());
|
||||
|
||||
if (res != null)
|
||||
res.forEach(it -> {
|
||||
try {
|
||||
Datapack zip = new Datapack(it.toPath());
|
||||
zip.loadFromZip();
|
||||
zip.installTo(worldDir);
|
||||
} catch (IOException | IllegalArgumentException e) {
|
||||
Logging.LOG.log(Level.WARNING, "Unable to parse datapack file " + it, e);
|
||||
}
|
||||
});
|
||||
|
||||
datapack.loadFromDir();
|
||||
}
|
||||
}
|
@ -92,7 +92,7 @@ public class GameListItemSkin extends SkinBase<GameListItem> {
|
||||
};
|
||||
|
||||
menu.getChildren().setAll(
|
||||
new IconedMenuItem(limitWidth.apply(SVG.gear(Theme.blackFillBinding(), 14, 14)), i18n("settings"), wrap.apply(skinnable::modifyGameSettings)),
|
||||
new IconedMenuItem(limitWidth.apply(SVG.gear(Theme.blackFillBinding(), 14, 14)), i18n("version.manage.manage"), wrap.apply(skinnable::modifyGameSettings)),
|
||||
new IconedMenuItem(limitWidth.apply(SVG.pencil(Theme.blackFillBinding(), 14, 14)), i18n("version.manage.rename"), wrap.apply(skinnable::rename)),
|
||||
new IconedMenuItem(limitWidth.apply(SVG.delete(Theme.blackFillBinding(), 14, 14)), i18n("version.manage.remove"), wrap.apply(skinnable::remove)),
|
||||
new IconedMenuItem(limitWidth.apply(SVG.export(Theme.blackFillBinding(), 14, 14)), i18n("modpack.export"), wrap.apply(skinnable::export)),
|
||||
|
@ -48,6 +48,8 @@ public final class VersionPage extends StackPane implements DecoratorPage {
|
||||
@FXML
|
||||
private InstallerListPage installer;
|
||||
@FXML
|
||||
private WorldListPage world;
|
||||
@FXML
|
||||
private JFXListView<?> browseList;
|
||||
@FXML
|
||||
private JFXListView<?> managementList;
|
||||
@ -90,13 +92,14 @@ public final class VersionPage extends StackPane implements DecoratorPage {
|
||||
this.version = id;
|
||||
this.profile = profile;
|
||||
|
||||
title.set(i18n("settings.game") + " - " + id);
|
||||
title.set(i18n("version.manage.manage") + " - " + id);
|
||||
|
||||
versionSettings.loadVersionSetting(profile, id);
|
||||
mod.setParentTab(tabPane);
|
||||
modTab.setUserData(mod);
|
||||
mod.loadMods(profile.getModManager(), id);
|
||||
installer.loadVersion(profile, id);
|
||||
world.loadVersion(profile, id);
|
||||
}
|
||||
|
||||
@FXML
|
||||
|
@ -0,0 +1,62 @@
|
||||
package org.jackhuang.hmcl.ui.versions;
|
||||
|
||||
import javafx.beans.property.SimpleStringProperty;
|
||||
import javafx.beans.property.StringProperty;
|
||||
import javafx.scene.control.Skin;
|
||||
import org.jackhuang.hmcl.game.World;
|
||||
import org.jackhuang.hmcl.task.Task;
|
||||
import org.jackhuang.hmcl.ui.wizard.WizardSinglePage;
|
||||
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
|
||||
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
|
||||
|
||||
public class WorldExportPage extends WizardSinglePage {
|
||||
private final StringProperty path = new SimpleStringProperty();
|
||||
private final StringProperty gameVersion = new SimpleStringProperty();
|
||||
private final StringProperty worldName = new SimpleStringProperty();
|
||||
private final World world;
|
||||
|
||||
public WorldExportPage(World world, Path export, Runnable onFinish) {
|
||||
super(onFinish);
|
||||
|
||||
this.world = world;
|
||||
|
||||
path.set(export.toString());
|
||||
gameVersion.set(world.getGameVersion());
|
||||
worldName.set(world.getWorldName());
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Skin<?> createDefaultSkin() {
|
||||
return new WorldExportPageSkin(this);
|
||||
}
|
||||
|
||||
|
||||
public StringProperty pathProperty() {
|
||||
return path;
|
||||
}
|
||||
|
||||
public StringProperty gameVersionProperty() {
|
||||
return gameVersion;
|
||||
}
|
||||
|
||||
public StringProperty worldNameProperty() {
|
||||
return worldName;
|
||||
}
|
||||
|
||||
public void export() {
|
||||
onFinish.run();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getTitle() {
|
||||
return i18n("world.export.wizard", world.getFileName());
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Object finish() {
|
||||
return Task.of(i18n("world.export.wizard", worldName.get()), () -> world.export(Paths.get(path.get()), worldName.get()));
|
||||
}
|
||||
}
|
@ -0,0 +1,81 @@
|
||||
package org.jackhuang.hmcl.ui.versions;
|
||||
|
||||
import com.jfoenix.controls.JFXButton;
|
||||
import com.jfoenix.controls.JFXTextField;
|
||||
import javafx.beans.binding.Bindings;
|
||||
import javafx.geometry.Insets;
|
||||
import javafx.geometry.Pos;
|
||||
import javafx.scene.control.Label;
|
||||
import javafx.scene.control.SkinBase;
|
||||
import javafx.scene.layout.BorderPane;
|
||||
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.ui.construct.ComponentList;
|
||||
import org.jackhuang.hmcl.ui.construct.FileItem;
|
||||
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Paths;
|
||||
|
||||
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
|
||||
|
||||
public class WorldExportPageSkin extends SkinBase<WorldExportPage> {
|
||||
|
||||
public WorldExportPageSkin(WorldExportPage skinnable) {
|
||||
super(skinnable);
|
||||
|
||||
Insets insets = new Insets(0, 0, 12, 0);
|
||||
VBox container = new VBox();
|
||||
container.setSpacing(16);
|
||||
container.setAlignment(Pos.CENTER);
|
||||
FXUtils.setLimitWidth(container, 500);
|
||||
{
|
||||
HBox labelContainer = new HBox();
|
||||
labelContainer.setPadding(new Insets(0, 0, 0, 5));
|
||||
Label label = new Label(i18n("world.export"));
|
||||
labelContainer.getChildren().setAll(label);
|
||||
container.getChildren().add(labelContainer);
|
||||
}
|
||||
|
||||
ComponentList list = new ComponentList();
|
||||
|
||||
FileItem fileItem = new FileItem();
|
||||
fileItem.setName(i18n("world.export.location"));
|
||||
fileItem.pathProperty().bindBidirectional(skinnable.pathProperty());
|
||||
list.getContent().add(fileItem);
|
||||
|
||||
JFXTextField txtWorldName = new JFXTextField();
|
||||
txtWorldName.textProperty().bindBidirectional(skinnable.worldNameProperty());
|
||||
txtWorldName.setLabelFloat(true);
|
||||
txtWorldName.setPromptText(i18n("world.name"));
|
||||
StackPane.setMargin(txtWorldName, insets);
|
||||
list.getContent().add(txtWorldName);
|
||||
|
||||
Label lblGameVersionTitle = new Label(i18n("world.game_version"));
|
||||
Label lblGameVersion = new Label();
|
||||
lblGameVersion.textProperty().bind(skinnable.gameVersionProperty());
|
||||
BorderPane gameVersionPane = new BorderPane();
|
||||
gameVersionPane.setPadding(new Insets(4, 0, 4, 0));
|
||||
gameVersionPane.setLeft(lblGameVersionTitle);
|
||||
gameVersionPane.setRight(lblGameVersion);
|
||||
list.getContent().add(gameVersionPane);
|
||||
|
||||
container.getChildren().add(list);
|
||||
|
||||
JFXButton btnExport = new JFXButton(i18n("button.export"));
|
||||
btnExport.disableProperty().bind(Bindings.createBooleanBinding(() -> txtWorldName.getText().isEmpty() || Files.exists(Paths.get(fileItem.getPath())),
|
||||
txtWorldName.textProperty().isEmpty(), fileItem.pathProperty()));
|
||||
btnExport.setButtonType(JFXButton.ButtonType.RAISED);
|
||||
btnExport.getStyleClass().add("jfx-button-raised");
|
||||
btnExport.setOnMouseClicked(e -> skinnable.export());
|
||||
HBox bottom = new HBox();
|
||||
bottom.setAlignment(Pos.CENTER_RIGHT);
|
||||
bottom.getChildren().setAll(btnExport);
|
||||
container.getChildren().add(bottom);
|
||||
|
||||
getChildren().setAll(container);
|
||||
}
|
||||
|
||||
|
||||
}
|
@ -1,12 +0,0 @@
|
||||
package org.jackhuang.hmcl.ui.versions;
|
||||
|
||||
import org.jackhuang.hmcl.ui.ListPage;
|
||||
|
||||
public class WorldList extends ListPage<WorldListItem> {
|
||||
|
||||
|
||||
@Override
|
||||
public void add() {
|
||||
// Not adding world here.
|
||||
}
|
||||
}
|
@ -7,8 +7,10 @@ import javafx.scene.image.Image;
|
||||
import javafx.stage.FileChooser;
|
||||
import org.jackhuang.hmcl.game.World;
|
||||
import org.jackhuang.hmcl.ui.Controllers;
|
||||
import org.jackhuang.hmcl.ui.wizard.SinglePageWizardProvider;
|
||||
|
||||
import java.io.File;
|
||||
import java.util.Date;
|
||||
|
||||
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
|
||||
|
||||
@ -20,6 +22,9 @@ public class WorldListItem extends Control {
|
||||
|
||||
public WorldListItem(World world) {
|
||||
this.world = world;
|
||||
|
||||
title.set(world.getWorldName());
|
||||
subtitle.set(i18n("world.description", world.getFileName(), new Date(world.getLastPlayed()).toString(), world.getGameVersion()));
|
||||
}
|
||||
|
||||
@Override
|
||||
@ -43,14 +48,16 @@ public class WorldListItem extends Control {
|
||||
FileChooser fileChooser = new FileChooser();
|
||||
fileChooser.setTitle(i18n("world.export.title"));
|
||||
fileChooser.getExtensionFilters().add(new FileChooser.ExtensionFilter(i18n("world"), "*.zip"));
|
||||
fileChooser.setInitialFileName(world.getWorldName());
|
||||
File file = fileChooser.showSaveDialog(Controllers.getStage());
|
||||
if (file == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
Controllers.getDecorator().startWizard(new SinglePageWizardProvider(controller -> new WorldExportPage(world, file.toPath(), controller::onFinish)));
|
||||
}
|
||||
|
||||
public void manageDatapacks() {
|
||||
Controllers.navigate(new DatapackListPage(world.getWorldName(), world.getFile()));
|
||||
}
|
||||
}
|
||||
|
@ -28,7 +28,6 @@ public class WorldListItemSkin extends SkinBase<WorldListItem> {
|
||||
|
||||
BorderPane root = new BorderPane();
|
||||
|
||||
|
||||
HBox center = new HBox();
|
||||
center.setSpacing(8);
|
||||
center.setAlignment(Pos.CENTER_LEFT);
|
||||
|
@ -0,0 +1,57 @@
|
||||
package org.jackhuang.hmcl.ui.versions;
|
||||
|
||||
import javafx.stage.FileChooser;
|
||||
import org.jackhuang.hmcl.game.World;
|
||||
import org.jackhuang.hmcl.setting.Profile;
|
||||
import org.jackhuang.hmcl.ui.Controllers;
|
||||
import org.jackhuang.hmcl.ui.ListPage;
|
||||
import org.jackhuang.hmcl.util.Logging;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.List;
|
||||
import java.util.logging.Level;
|
||||
|
||||
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
|
||||
|
||||
public class WorldListPage extends ListPage<WorldListItem> {
|
||||
private Path savesDir;
|
||||
|
||||
public WorldListPage() {
|
||||
}
|
||||
|
||||
public void loadVersion(Profile profile, String id) {
|
||||
this.savesDir = profile.getRepository().getRunDirectory(id).toPath().resolve("saves");
|
||||
|
||||
itemsProperty().clear();
|
||||
try {
|
||||
if (Files.exists(savesDir))
|
||||
for (Path worldDir : Files.newDirectoryStream(savesDir)) {
|
||||
itemsProperty().add(new WorldListItem(new World(worldDir)));
|
||||
}
|
||||
} catch (IOException e) {
|
||||
Logging.LOG.log(Level.WARNING, "Failed to read saves", e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void add() {
|
||||
FileChooser chooser = new FileChooser();
|
||||
chooser.setTitle(i18n("world.choose_world"));
|
||||
chooser.getExtensionFilters().setAll(new FileChooser.ExtensionFilter(i18n("world.extension"), "*.zip"));
|
||||
List<File> res = chooser.showOpenMultipleDialog(Controllers.getStage());
|
||||
|
||||
if (res == null) return;
|
||||
res.forEach(it -> {
|
||||
try {
|
||||
World world = new World(it.toPath());
|
||||
world.install(savesDir, world.getWorldName());
|
||||
itemsProperty().add(new WorldListItem(new World(savesDir.resolve(world.getWorldName()))));
|
||||
} catch (IOException | IllegalArgumentException e) {
|
||||
Logging.LOG.log(Level.WARNING, "Unable to parse datapack file " + it, e);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
@ -0,0 +1,37 @@
|
||||
package org.jackhuang.hmcl.ui.wizard;
|
||||
|
||||
import javafx.scene.Node;
|
||||
|
||||
import java.util.Map;
|
||||
import java.util.function.Function;
|
||||
|
||||
public class SinglePageWizardProvider implements WizardProvider {
|
||||
|
||||
private final Function<WizardController, WizardSinglePage> provider;
|
||||
private WizardSinglePage page;
|
||||
|
||||
public SinglePageWizardProvider(Function<WizardController, WizardSinglePage> provider) {
|
||||
this.provider = provider;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void start(Map<String, Object> settings) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object finish(Map<String, Object> settings) {
|
||||
return page.finish();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Node createPage(WizardController controller, int step, Map<String, Object> settings) {
|
||||
if (step != 0) throw new IllegalStateException("Step must be 0");
|
||||
|
||||
return page = provider.apply(controller);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean cancel() {
|
||||
return true;
|
||||
}
|
||||
}
|
@ -0,0 +1,19 @@
|
||||
package org.jackhuang.hmcl.ui.wizard;
|
||||
|
||||
import javafx.scene.control.Control;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
public abstract class WizardSinglePage extends Control implements WizardPage {
|
||||
protected final Runnable onFinish;
|
||||
|
||||
protected WizardSinglePage(Runnable onFinish) {
|
||||
this.onFinish = onFinish;
|
||||
}
|
||||
|
||||
protected abstract Object finish();
|
||||
|
||||
@Override
|
||||
public void cleanup(Map<String, Object> settings) {
|
||||
}
|
||||
}
|
@ -7,6 +7,7 @@
|
||||
<?import org.jackhuang.hmcl.ui.versions.VersionSettingsPage?>
|
||||
<?import org.jackhuang.hmcl.ui.versions.ModListPage?>
|
||||
<?import org.jackhuang.hmcl.ui.versions.InstallerListPage?>
|
||||
<?import org.jackhuang.hmcl.ui.versions.WorldListPage?>
|
||||
<fx:root xmlns="http://javafx.com/javafx"
|
||||
xmlns:fx="http://javafx.com/fxml"
|
||||
fx:id="rootPane"
|
||||
@ -23,6 +24,9 @@
|
||||
<Tab text="%settings.tabs.installers">
|
||||
<InstallerListPage fx:id="installer" />
|
||||
</Tab>
|
||||
<Tab text="%world">
|
||||
<WorldListPage fx:id="world" />
|
||||
</Tab>
|
||||
</JFXTabPane>
|
||||
|
||||
<HBox alignment="TOP_RIGHT" style="-fx-padding: 2 2 2 2;" spacing="3" pickOnBounds="false">
|
||||
|
@ -70,6 +70,7 @@ button.clear=Clear
|
||||
button.delete=Delete
|
||||
button.edit=Edit
|
||||
button.install=Install
|
||||
button.export=Export
|
||||
button.no=No
|
||||
button.ok=OK
|
||||
button.refresh=Refresh
|
||||
@ -242,12 +243,21 @@ mods.remove=Remove
|
||||
|
||||
datapack=Data packs
|
||||
datapack.add=Add data pack
|
||||
datapack.choose_datapack=Choose the datapack zip to be imported
|
||||
datapack.title=World %s - Datapacks
|
||||
datapack.remove=Remove
|
||||
|
||||
world=Worlds
|
||||
world=Worlds/Datapacks
|
||||
world.choose_world=Choose the save zip to be imported
|
||||
world.datapack=Manage data packs
|
||||
world.description=%s. Last played time: %s. Game version: %s.
|
||||
world.export=Export this world
|
||||
world.export.title=Choose a file location to hold your world
|
||||
world.export.location=Export to
|
||||
world.export.wizard=Export world %s
|
||||
world.extension=World zip
|
||||
world.game_version=Game Version
|
||||
world.name=World Name
|
||||
|
||||
profile=Game Directories
|
||||
profile.default=Current directory
|
||||
@ -352,6 +362,7 @@ 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=Game List
|
||||
version.manage.manage=Game Management
|
||||
version.manage.redownload_assets_index=Redownload Assets Index
|
||||
version.manage.remove=Delete this game
|
||||
version.manage.remove.confirm=Sure to remove game %s? You cannot restore this game again!
|
||||
|
@ -70,6 +70,7 @@ button.clear=清除
|
||||
button.delete=刪除
|
||||
button.edit=編輯
|
||||
button.install=安裝
|
||||
button.export=導出
|
||||
button.no=否
|
||||
button.ok=確定
|
||||
button.refresh=重新整理
|
||||
@ -240,6 +241,24 @@ mods.add.success=成功新增模組 %s。
|
||||
mods.choose_mod=選擇模組
|
||||
mods.remove=刪除
|
||||
|
||||
datapack=數據檔
|
||||
datapack.add=添加數據檔
|
||||
datapack.choose_datapack=選擇要導入的數據包壓縮檔
|
||||
datapack.title=世界 %s - 數據檔
|
||||
datapack.remove=刪除
|
||||
|
||||
world=世界/數據檔
|
||||
world.choose_world=選擇要導入的存檔壓縮檔
|
||||
world.datapack=管理數據檔
|
||||
world.description=%s. 上一次遊戲時間: %s. 遊戲版本: %s
|
||||
world.export=導出此世界
|
||||
world.export.title=選擇該世界的存儲位置
|
||||
world.export.location=保存到
|
||||
world.export.wizard=導出世界 %s
|
||||
world.extension=存檔壓縮檔
|
||||
world.game_version=遊戲版本
|
||||
world.name=世界名稱
|
||||
|
||||
profile=遊戲目錄
|
||||
profile.default=目前目錄
|
||||
profile.home=主資料夾
|
||||
@ -343,6 +362,7 @@ version.launch_script.failed=生成啟動腳本失敗
|
||||
version.launch_script.save=儲存啟動腳本
|
||||
version.launch_script.success=啟動腳本已生成完畢:%s
|
||||
version.manage=遊戲列表
|
||||
version.manage.manage=游戏管理
|
||||
version.manage.redownload_assets_index=重新下載資源設定(assets_index.json)
|
||||
version.manage.remove=刪除該版本
|
||||
version.manage.remove.confirm=真的要刪除版本 %s 嗎?你將無法找回被刪除的檔案!
|
||||
|
@ -70,6 +70,7 @@ button.clear=清除
|
||||
button.delete=删除
|
||||
button.edit=修改
|
||||
button.install=安装
|
||||
button.export=导出
|
||||
button.no=否
|
||||
button.ok=确定
|
||||
button.refresh=刷新
|
||||
@ -240,6 +241,24 @@ mods.add.success=成功添加模组 %s。
|
||||
mods.choose_mod=选择模组
|
||||
mods.remove=删除
|
||||
|
||||
datapack=数据包
|
||||
datapack.add=添加数据包
|
||||
datapack.choose_datapack=选择要导入的数据包压缩包
|
||||
datapack.title=世界 %s - 数据包
|
||||
datapack.remove=删除
|
||||
|
||||
world=世界/数据包
|
||||
world.choose_world=选择要导入的存档压缩包
|
||||
world.datapack=管理数据包
|
||||
world.description=%s. 上一次游戏时间: %s. 游戏版本: %s
|
||||
world.export=导出此世界
|
||||
world.export.title=选择该世界的存储位置
|
||||
world.export.location=保存到
|
||||
world.export.wizard=导出世界 %s
|
||||
world.extension=世界压缩包
|
||||
world.game_version=游戏版本
|
||||
world.name=世界名称
|
||||
|
||||
profile=游戏目录
|
||||
profile.default=当前目录
|
||||
profile.home=主文件夹
|
||||
@ -343,6 +362,7 @@ version.launch_script.failed=生成启动脚本失败
|
||||
version.launch_script.save=保存启动脚本
|
||||
version.launch_script.success=启动脚本已生成完毕:%s
|
||||
version.manage=游戏列表
|
||||
version.manage.manage=游戏管理
|
||||
version.manage.redownload_assets_index=重新下载资源配置(assets_index.json)
|
||||
version.manage.remove=删除该版本
|
||||
version.manage.remove.confirm=真的要删除版本 %s 吗?你将无法找回被删除的文件!
|
||||
|
@ -55,7 +55,10 @@ public class DefaultCacheRepository extends CacheRepository {
|
||||
|
||||
lock.writeLock().lock();
|
||||
try {
|
||||
index = Constants.GSON.fromJson(FileUtils.readText(indexFile.toFile()), Index.class);
|
||||
if (Files.isRegularFile(indexFile))
|
||||
index = Constants.GSON.fromJson(FileUtils.readText(indexFile.toFile()), Index.class);
|
||||
else
|
||||
index = new Index();
|
||||
} catch (IOException e) {
|
||||
Logging.LOG.log(Level.WARNING, "Unable to read index file", e);
|
||||
index = new Index();
|
||||
|
@ -1,4 +1,162 @@
|
||||
package org.jackhuang.hmcl.game;
|
||||
|
||||
import com.github.steveice10.opennbt.NBTIO;
|
||||
import com.github.steveice10.opennbt.tag.builtin.CompoundTag;
|
||||
import com.github.steveice10.opennbt.tag.builtin.LongTag;
|
||||
import com.github.steveice10.opennbt.tag.builtin.StringTag;
|
||||
import com.github.steveice10.opennbt.tag.builtin.Tag;
|
||||
import org.jackhuang.hmcl.util.CompressingUtils;
|
||||
import org.jackhuang.hmcl.util.FileUtils;
|
||||
import org.jackhuang.hmcl.util.Unzipper;
|
||||
import org.jackhuang.hmcl.util.Zipper;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.file.FileAlreadyExistsException;
|
||||
import java.nio.file.FileSystem;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.stream.Collectors;
|
||||
import java.util.zip.GZIPInputStream;
|
||||
import java.util.zip.GZIPOutputStream;
|
||||
|
||||
public class World {
|
||||
|
||||
private final Path file;
|
||||
private String fileName;
|
||||
private String worldName;
|
||||
private String gameVersion;
|
||||
private long lastPlayed, seed;
|
||||
|
||||
public World(Path file) throws IOException {
|
||||
this.file = file;
|
||||
|
||||
if (Files.isDirectory(file))
|
||||
loadFromDirectory();
|
||||
else if (Files.isRegularFile(file))
|
||||
loadFromZip();
|
||||
else
|
||||
throw new IOException("Path " + file + " cannot be recognized as a Minecraft world");
|
||||
}
|
||||
|
||||
private void loadFromDirectory() throws IOException {
|
||||
fileName = FileUtils.getName(file);
|
||||
Path levelDat = file.resolve("level.dat");
|
||||
getWorldName(levelDat);
|
||||
}
|
||||
|
||||
public Path getFile() {
|
||||
return file;
|
||||
}
|
||||
|
||||
public String getFileName() {
|
||||
return fileName;
|
||||
}
|
||||
|
||||
public String getWorldName() {
|
||||
return worldName;
|
||||
}
|
||||
|
||||
public long getLastPlayed() {
|
||||
return lastPlayed;
|
||||
}
|
||||
|
||||
public long getSeed() {
|
||||
return seed;
|
||||
}
|
||||
|
||||
public String getGameVersion() {
|
||||
return gameVersion;
|
||||
}
|
||||
|
||||
private void loadFromZip() throws IOException {
|
||||
try (FileSystem fs = CompressingUtils.createReadOnlyZipFileSystem(file)) {
|
||||
Path root = Files.list(fs.getPath("/")).filter(Files::isDirectory).findAny()
|
||||
.orElseThrow(() -> new IOException("Not a valid world zip file"));
|
||||
|
||||
Path levelDat = root.resolve("level.dat");
|
||||
if (!Files.exists(levelDat))
|
||||
throw new IllegalArgumentException("Not a valid world zip file since level.dat cannot be found.");
|
||||
|
||||
fileName = FileUtils.getName(root);
|
||||
getWorldName(levelDat);
|
||||
}
|
||||
}
|
||||
|
||||
private void getWorldName(Path levelDat) throws IOException {
|
||||
CompoundTag nbt = parseLevelDat(levelDat);
|
||||
|
||||
CompoundTag data = nbt.get("Data");
|
||||
String name = data.<StringTag>get("LevelName").getValue();
|
||||
lastPlayed = data.<LongTag>get("LastPlayed").getValue();
|
||||
seed = data.<LongTag>get("RandomSeed").getValue();
|
||||
CompoundTag version = data.get("Version");
|
||||
gameVersion = version.<StringTag>get("Name").getValue();
|
||||
worldName = name;
|
||||
}
|
||||
|
||||
public void rename(String newName) throws IOException {
|
||||
if (!Files.isDirectory(file))
|
||||
throw new IOException("Not a valid world directory");
|
||||
|
||||
// Change the name recorded in level.dat
|
||||
Path levelDat = file.resolve("level.dat");
|
||||
CompoundTag nbt = parseLevelDat(levelDat);
|
||||
CompoundTag data = nbt.get("Data");
|
||||
data.put(new StringTag("LevelName", newName));
|
||||
|
||||
NBTIO.writeTag(new GZIPOutputStream(Files.newOutputStream(levelDat)), nbt);
|
||||
|
||||
// then change the folder's name
|
||||
Files.move(file, file.resolveSibling(newName));
|
||||
}
|
||||
|
||||
public void install(Path savesDir, String name) throws IOException {
|
||||
Path worldDir = savesDir.resolve(name);
|
||||
if (Files.isDirectory(worldDir)) {
|
||||
throw new FileAlreadyExistsException("World already exists");
|
||||
}
|
||||
|
||||
if (Files.isRegularFile(file)) {
|
||||
String subDirectoryName;
|
||||
try (FileSystem fs = CompressingUtils.createReadOnlyZipFileSystem(file)) {
|
||||
List<Path> subDirs = Files.list(fs.getPath("/")).collect(Collectors.toList());
|
||||
if (subDirs.size() != 1) {
|
||||
throw new IOException("World zip malformed");
|
||||
}
|
||||
subDirectoryName = FileUtils.getName(subDirs.get(0));
|
||||
}
|
||||
new Unzipper(file, savesDir)
|
||||
.setSubDirectory("/" + subDirectoryName + "/")
|
||||
.unzip();
|
||||
} else if (Files.isDirectory(file)) {
|
||||
FileUtils.copyDirectory(file, worldDir);
|
||||
}
|
||||
}
|
||||
|
||||
public void export(Path zip, String worldName) throws IOException {
|
||||
if (!Files.isDirectory(file))
|
||||
throw new IOException();
|
||||
|
||||
try (Zipper zipper = new Zipper(zip)) {
|
||||
zipper.putDirectory(file, "/" + worldName + "/");
|
||||
}
|
||||
}
|
||||
|
||||
private static CompoundTag parseLevelDat(Path path) throws IOException {
|
||||
Tag nbt = NBTIO.readTag(new GZIPInputStream(Files.newInputStream(path)));
|
||||
if (nbt instanceof CompoundTag)
|
||||
return (CompoundTag) nbt;
|
||||
else
|
||||
throw new IOException("level.dat malformed");
|
||||
}
|
||||
|
||||
public static List<World> getWorlds(Path worldDir) throws IOException {
|
||||
List<World> worlds = new ArrayList<>();
|
||||
for (Path world : Files.newDirectoryStream(worldDir)) {
|
||||
worlds.add(new World(world));
|
||||
}
|
||||
return worlds;
|
||||
}
|
||||
}
|
||||
|
@ -2,8 +2,11 @@ package org.jackhuang.hmcl.mod;
|
||||
|
||||
import com.google.gson.JsonParseException;
|
||||
import com.google.gson.annotations.SerializedName;
|
||||
import javafx.application.Platform;
|
||||
import javafx.beans.property.BooleanProperty;
|
||||
import javafx.beans.property.SimpleBooleanProperty;
|
||||
import javafx.collections.FXCollections;
|
||||
import javafx.collections.ObservableList;
|
||||
import org.jackhuang.hmcl.util.*;
|
||||
|
||||
import java.io.IOException;
|
||||
@ -11,25 +14,21 @@ import java.nio.file.FileSystem;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.*;
|
||||
import java.util.logging.Level;
|
||||
|
||||
public class Datapack {
|
||||
private final Path path;
|
||||
private final List<Pack> info;
|
||||
private final ObservableList<Pack> info = FXCollections.observableArrayList();
|
||||
|
||||
private Datapack(Path path, List<Pack> info) {
|
||||
public Datapack(Path path) {
|
||||
this.path = path;
|
||||
this.info = Collections.unmodifiableList(info);
|
||||
|
||||
for (Pack pack : info) {
|
||||
pack.datapack = this;
|
||||
}
|
||||
}
|
||||
|
||||
public Path getPath() {
|
||||
return path;
|
||||
}
|
||||
|
||||
public List<Pack> getInfo() {
|
||||
public ObservableList<Pack> getInfo() {
|
||||
return info;
|
||||
}
|
||||
|
||||
@ -39,40 +38,53 @@ public class Datapack {
|
||||
Set<String> packs = new HashSet<>();
|
||||
for (Pack pack : info) packs.add(pack.getId());
|
||||
|
||||
for (Path datapack : Files.newDirectoryStream(datapacks)) {
|
||||
if (packs.contains(FileUtils.getName(datapack)))
|
||||
FileUtils.deleteDirectory(datapack.toFile());
|
||||
}
|
||||
if (Files.isDirectory(datapacks))
|
||||
for (Path datapack : Files.newDirectoryStream(datapacks)) {
|
||||
if (packs.contains(FileUtils.getName(datapack)))
|
||||
FileUtils.deleteDirectory(datapack.toFile());
|
||||
}
|
||||
|
||||
new Unzipper(path, worldPath).setReplaceExistentFile(true).unzip();
|
||||
}
|
||||
|
||||
public static Datapack fromZip(Path path) throws IOException {
|
||||
public void deletePack(String pack) throws IOException {
|
||||
FileUtils.deleteDirectory(path.resolve(pack).toFile());
|
||||
Platform.runLater(() -> info.removeIf(p -> p.getId().equals(pack)));
|
||||
}
|
||||
|
||||
public void loadFromZip() throws IOException {
|
||||
try (FileSystem fs = CompressingUtils.createReadOnlyZipFileSystem(path)) {
|
||||
Datapack datapack = fromDir(fs.getPath("/datapacks/"));
|
||||
return new Datapack(path, datapack.info);
|
||||
loadFromDir(fs.getPath("/datapacks/"));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param dir
|
||||
* @return
|
||||
* @throws IOException
|
||||
*/
|
||||
public static Datapack fromDir(Path dir) throws IOException {
|
||||
List<Pack> info = new LinkedList<>();
|
||||
|
||||
for (Path subDir : Files.newDirectoryStream(dir)) {
|
||||
Path mcmeta = subDir.resolve("pack.mcmeta");
|
||||
|
||||
if (!Files.exists(mcmeta))
|
||||
continue;
|
||||
|
||||
PackMcMeta pack = JsonUtils.fromNonNullJson(FileUtils.readText(mcmeta), PackMcMeta.class);
|
||||
info.add(new Pack(mcmeta, FileUtils.getName(subDir), pack.getPackInfo().getDescription()));
|
||||
public void loadFromDir() {
|
||||
try {
|
||||
loadFromDir(path);
|
||||
} catch (IOException e) {
|
||||
Logging.LOG.log(Level.WARNING, "Failed to read datapacks " + path, e);
|
||||
}
|
||||
return new Datapack(dir, info);
|
||||
}
|
||||
|
||||
private void loadFromDir(Path dir) throws IOException {
|
||||
List<Pack> info = new ArrayList<>();
|
||||
|
||||
if (Files.isDirectory(dir))
|
||||
for (Path subDir : Files.newDirectoryStream(dir)) {
|
||||
Path mcmeta = subDir.resolve("pack.mcmeta");
|
||||
|
||||
if (!Files.exists(mcmeta))
|
||||
continue;
|
||||
|
||||
try {
|
||||
PackMcMeta pack = JsonUtils.fromNonNullJson(FileUtils.readText(mcmeta), PackMcMeta.class);
|
||||
info.add(new Pack(mcmeta, FileUtils.getName(subDir), pack.getPackInfo().getDescription(), this));
|
||||
} catch (IOException e) {
|
||||
Logging.LOG.log(Level.WARNING, "Failed to read datapack " + subDir, e);
|
||||
}
|
||||
}
|
||||
|
||||
this.info.setAll(info);
|
||||
}
|
||||
|
||||
public static class Pack {
|
||||
@ -80,12 +92,13 @@ public class Datapack {
|
||||
private final BooleanProperty active;
|
||||
private final String id;
|
||||
private final String description;
|
||||
private Datapack datapack;
|
||||
private final Datapack datapack;
|
||||
|
||||
public Pack(Path packMcMeta, String id, String description) {
|
||||
public Pack(Path packMcMeta, String id, String description, Datapack datapack) {
|
||||
this.packMcMeta = packMcMeta;
|
||||
this.id = id;
|
||||
this.description = description;
|
||||
this.datapack = datapack;
|
||||
|
||||
active = new SimpleBooleanProperty(this, "active", !DISABLED_EXT.equals(FileUtils.getExtension(packMcMeta))) {
|
||||
@Override
|
||||
|
@ -36,6 +36,8 @@ subprojects {
|
||||
repositories {
|
||||
mavenCentral()
|
||||
jcenter()
|
||||
|
||||
maven { url 'https://jitpack.io' }
|
||||
}
|
||||
|
||||
sourceCompatibility = 1.8
|
||||
@ -45,7 +47,8 @@ subprojects {
|
||||
compile group: 'org.tukaani', name: 'xz', version: '1.8'
|
||||
compile group: 'org.hildan.fxgson', name: 'fx-gson', version: '3.1.0'
|
||||
compile group: 'org.jenkins-ci', name: 'constant-pool-scanner', version: '1.2'
|
||||
compile group: 'org.spacehq', name: 'opennbt', version: '1.0'
|
||||
compile group: 'com.github.steveice10', name: 'opennbt', version: '1.1'
|
||||
|
||||
testCompile group: 'junit', name: 'junit', version: '4.12'
|
||||
}
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user