From 07febc18d4e8ac09a5f0ee9bebe9a5005a6c58ba Mon Sep 17 00:00:00 2001 From: huanghongxun Date: Sun, 16 Sep 2018 12:38:15 +0800 Subject: [PATCH] World and datapacks management --- .../java/org/jackhuang/hmcl/ui/FXUtils.java | 41 ++--- .../hmcl/ui/construct/ComponentList.java | 6 +- .../hmcl/ui/versions/DatapackList.java | 10 -- .../hmcl/ui/versions/DatapackListItem.java | 7 +- .../hmcl/ui/versions/DatapackListPage.java | 103 ++++++++++++ .../hmcl/ui/versions/GameListItemSkin.java | 2 +- .../hmcl/ui/versions/VersionPage.java | 5 +- .../hmcl/ui/versions/WorldExportPage.java | 62 +++++++ .../hmcl/ui/versions/WorldExportPageSkin.java | 81 +++++++++ .../jackhuang/hmcl/ui/versions/WorldList.java | 12 -- .../hmcl/ui/versions/WorldListItem.java | 9 +- .../hmcl/ui/versions/WorldListItemSkin.java | 1 - .../hmcl/ui/versions/WorldListPage.java | 57 +++++++ .../ui/wizard/SinglePageWizardProvider.java | 37 ++++ .../hmcl/ui/wizard/WizardSinglePage.java | 19 +++ .../assets/fxml/version/version.fxml | 4 + .../resources/assets/lang/I18N.properties | 13 +- .../resources/assets/lang/I18N_zh.properties | 20 +++ .../assets/lang/I18N_zh_CN.properties | 20 +++ .../hmcl/download/DefaultCacheRepository.java | 5 +- .../java/org/jackhuang/hmcl/game/World.java | 158 ++++++++++++++++++ .../java/org/jackhuang/hmcl/mod/Datapack.java | 83 +++++---- build.gradle | 5 +- 23 files changed, 670 insertions(+), 90 deletions(-) delete mode 100644 HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/DatapackList.java create mode 100644 HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/DatapackListPage.java create mode 100644 HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/WorldExportPage.java create mode 100644 HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/WorldExportPageSkin.java delete mode 100644 HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/WorldList.java create mode 100644 HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/WorldListPage.java create mode 100644 HMCL/src/main/java/org/jackhuang/hmcl/ui/wizard/SinglePageWizardProvider.java create mode 100644 HMCL/src/main/java/org/jackhuang/hmcl/ui/wizard/WizardSinglePage.java diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/FXUtils.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/FXUtils.java index 19b96d32e..8f9b09806 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/FXUtils.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/FXUtils.java @@ -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) { diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/construct/ComponentList.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/construct/ComponentList.java index 382a58116..1bff3babf 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/construct/ComponentList.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/construct/ComponentList.java @@ -100,13 +100,15 @@ public class ComponentList extends Control { } protected static class Skin extends SkinBase { + private final ObservableList 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); } diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/DatapackList.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/DatapackList.java deleted file mode 100644 index e4e144868..000000000 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/DatapackList.java +++ /dev/null @@ -1,10 +0,0 @@ -package org.jackhuang.hmcl.ui.versions; - -import org.jackhuang.hmcl.ui.ListPage; - -public class DatapackList extends ListPage { - @Override - public void add() { - - } -} diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/DatapackListItem.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/DatapackListItem.java index d8415b3ef..e2e24c73d 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/DatapackListItem.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/DatapackListItem.java @@ -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 deleteCallback) { + public DatapackListItem(Datapack.Pack info, Consumer 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); diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/DatapackListPage.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/DatapackListPage.java new file mode 100644 index 000000000..95e54ddbe --- /dev/null +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/DatapackListPage.java @@ -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 implements DecoratorPage { + private final StringProperty title = new SimpleStringProperty(); + private final ObservableList 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 files = event.getDragboard().getFiles(); + if (files != null) { + Collection 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 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(); + } +} diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/GameListItemSkin.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/GameListItemSkin.java index 64565be9b..916581e41 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/GameListItemSkin.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/GameListItemSkin.java @@ -92,7 +92,7 @@ public class GameListItemSkin extends SkinBase { }; 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)), diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/VersionPage.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/VersionPage.java index 51aa9182a..612d01bef 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/VersionPage.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/VersionPage.java @@ -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 diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/WorldExportPage.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/WorldExportPage.java new file mode 100644 index 000000000..7a8e4a18f --- /dev/null +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/WorldExportPage.java @@ -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())); + } +} diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/WorldExportPageSkin.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/WorldExportPageSkin.java new file mode 100644 index 000000000..4d5ef7ba0 --- /dev/null +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/WorldExportPageSkin.java @@ -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 { + + 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); + } + + +} diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/WorldList.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/WorldList.java deleted file mode 100644 index 1ee9f1886..000000000 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/WorldList.java +++ /dev/null @@ -1,12 +0,0 @@ -package org.jackhuang.hmcl.ui.versions; - -import org.jackhuang.hmcl.ui.ListPage; - -public class WorldList extends ListPage { - - - @Override - public void add() { - // Not adding world here. - } -} diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/WorldListItem.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/WorldListItem.java index 7cd809b78..50d6b2866 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/WorldListItem.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/WorldListItem.java @@ -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())); } } diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/WorldListItemSkin.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/WorldListItemSkin.java index 56032ebd4..ee37c025d 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/WorldListItemSkin.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/WorldListItemSkin.java @@ -28,7 +28,6 @@ public class WorldListItemSkin extends SkinBase { BorderPane root = new BorderPane(); - HBox center = new HBox(); center.setSpacing(8); center.setAlignment(Pos.CENTER_LEFT); diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/WorldListPage.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/WorldListPage.java new file mode 100644 index 000000000..da1ce1806 --- /dev/null +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/WorldListPage.java @@ -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 { + 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 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); + } + }); + } +} diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/wizard/SinglePageWizardProvider.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/wizard/SinglePageWizardProvider.java new file mode 100644 index 000000000..cef4fc592 --- /dev/null +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/wizard/SinglePageWizardProvider.java @@ -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 provider; + private WizardSinglePage page; + + public SinglePageWizardProvider(Function provider) { + this.provider = provider; + } + + @Override + public void start(Map settings) { + } + + @Override + public Object finish(Map settings) { + return page.finish(); + } + + @Override + public Node createPage(WizardController controller, int step, Map settings) { + if (step != 0) throw new IllegalStateException("Step must be 0"); + + return page = provider.apply(controller); + } + + @Override + public boolean cancel() { + return true; + } +} diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/wizard/WizardSinglePage.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/wizard/WizardSinglePage.java new file mode 100644 index 000000000..9b596320c --- /dev/null +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/wizard/WizardSinglePage.java @@ -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 settings) { + } +} diff --git a/HMCL/src/main/resources/assets/fxml/version/version.fxml b/HMCL/src/main/resources/assets/fxml/version/version.fxml index 13ab04457..45f7d7d12 100644 --- a/HMCL/src/main/resources/assets/fxml/version/version.fxml +++ b/HMCL/src/main/resources/assets/fxml/version/version.fxml @@ -7,6 +7,7 @@ + + + + diff --git a/HMCL/src/main/resources/assets/lang/I18N.properties b/HMCL/src/main/resources/assets/lang/I18N.properties index c977c639a..2ac92d75a 100644 --- a/HMCL/src/main/resources/assets/lang/I18N.properties +++ b/HMCL/src/main/resources/assets/lang/I18N.properties @@ -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! diff --git a/HMCL/src/main/resources/assets/lang/I18N_zh.properties b/HMCL/src/main/resources/assets/lang/I18N_zh.properties index c9ea96367..505b8404b 100644 --- a/HMCL/src/main/resources/assets/lang/I18N_zh.properties +++ b/HMCL/src/main/resources/assets/lang/I18N_zh.properties @@ -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 嗎?你將無法找回被刪除的檔案! diff --git a/HMCL/src/main/resources/assets/lang/I18N_zh_CN.properties b/HMCL/src/main/resources/assets/lang/I18N_zh_CN.properties index 00436eba7..e1322b225 100644 --- a/HMCL/src/main/resources/assets/lang/I18N_zh_CN.properties +++ b/HMCL/src/main/resources/assets/lang/I18N_zh_CN.properties @@ -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 吗?你将无法找回被删除的文件! diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/download/DefaultCacheRepository.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/download/DefaultCacheRepository.java index 2f7bc6a52..f58bde8e9 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/download/DefaultCacheRepository.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/download/DefaultCacheRepository.java @@ -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(); diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/game/World.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/game/World.java index 44c0c9ff2..464e9f2b6 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/game/World.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/game/World.java @@ -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.get("LevelName").getValue(); + lastPlayed = data.get("LastPlayed").getValue(); + seed = data.get("RandomSeed").getValue(); + CompoundTag version = data.get("Version"); + gameVersion = version.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 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 getWorlds(Path worldDir) throws IOException { + List worlds = new ArrayList<>(); + for (Path world : Files.newDirectoryStream(worldDir)) { + worlds.add(new World(world)); + } + return worlds; + } } diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/Datapack.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/Datapack.java index 01fc19e94..9d02bd406 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/Datapack.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/Datapack.java @@ -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 info; + private final ObservableList info = FXCollections.observableArrayList(); - private Datapack(Path path, List 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 getInfo() { + public ObservableList getInfo() { return info; } @@ -39,40 +38,53 @@ public class Datapack { Set 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 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 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 diff --git a/build.gradle b/build.gradle index a962e84bf..b83739842 100644 --- a/build.gradle +++ b/build.gradle @@ -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' }