Compare commits

..
10 Commits
24 changed files with 432 additions and 150 deletions

No files matched your search

+4
View File
@@ -25,8 +25,12 @@ NVIDIA
/HMCLCore/out/
# eclipse
/bin/
/HMCL/bin/
/HMCLCore/bin/
.classpath
.project
.settings
# netbeans
.nb-gradle
@@ -17,14 +17,16 @@
*/
package org.jackhuang.hmcl.setting;
import com.jfoenix.concurrency.JFXUtilities;
import javafx.application.Platform;
import javafx.beans.InvalidationListener;
import javafx.beans.Observable;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.ReadOnlyListProperty;
import javafx.beans.property.ReadOnlyListWrapper;
import javafx.beans.property.SimpleObjectProperty;
import javafx.beans.property.*;
import javafx.beans.value.ObservableValue;
import javafx.collections.ObservableList;
import org.jackhuang.hmcl.Launcher;
import org.jackhuang.hmcl.event.EventBus;
import org.jackhuang.hmcl.event.RefreshedVersionsEvent;
import java.io.File;
import java.util.HashSet;
@@ -41,6 +43,8 @@ public final class Profiles {
public static final String DEFAULT_PROFILE = "Default";
public static final String HOME_PROFILE = "Home";
private static InvalidationListener listener = o -> loadVersion();
private Profiles() {
}
@@ -61,11 +65,17 @@ public final class Profiles {
private static ObjectProperty<Profile> selectedProfile = new SimpleObjectProperty<Profile>() {
{
profiles.addListener(onInvalidating(this::invalidated));
this.addListener(this::change);
}
@Override
protected void invalidated() {
Profile profile = get();
if (get() != null)
get().removeListener(listener);
if (profiles.isEmpty()) {
if (profile != null) {
set(null);
@@ -80,7 +90,16 @@ public final class Profiles {
if (!initialized)
return;
config().setSelectedProfile(profile == null ? "" : profile.getName());
loadVersion();
}
private void change(ObservableValue<? extends Profile> observableValue, Profile oldProfile, Profile newProfile) {
if (oldProfile != null)
oldProfile.selectedVersionProperty().removeListener(listener);
if (newProfile != null)
newProfile.selectedVersionProperty().addListener(listener);
}
};
@@ -127,18 +146,29 @@ public final class Profiles {
HashSet<String> names = new HashSet<>();
config().getConfigurations().forEach((name, profile) -> {
if (!names.add(name)) return;
profile.setName(name);
profiles.add(profile);
profile.setName(name);
});
checkProfiles();
initialized = true;
// Platform.runLater is necessary or profiles will be empty
// since checkProfiles adds 2 base profile later.
Platform.runLater(() -> {
selectedProfile.set(
profiles.stream()
.filter(it -> it.getName().equals(config().getSelectedProfile()))
.findFirst()
.orElse(profiles.get(0)));
selectedProfile.set(
profiles.stream()
.filter(it -> it.getName().equals(config().getSelectedProfile()))
.findFirst()
.orElse(profiles.get(0)));
initialized = true;
});
EventBus.EVENT_BUS.channel(RefreshedVersionsEvent.class).registerWeak(event -> {
JFXUtilities.runInFX(() -> {
if (selectedProfile.get() != null && selectedProfile.get().getRepository() == event.getSource())
loadVersion();
});
});
}
public static ObservableList<Profile> getProfiles() {
@@ -160,4 +190,21 @@ public final class Profiles {
public static ObjectProperty<Profile> selectedProfileProperty() {
return selectedProfile;
}
private static final ReadOnlyStringWrapper selectedVersion = new ReadOnlyStringWrapper();
public static ReadOnlyStringProperty selectedVersionProperty() {
return selectedVersion.getReadOnlyProperty();
}
public static String getSelectedVersion() {
return selectedVersion.get();
}
private static void loadVersion() {
Profile profile = selectedProfile.get();
if (profile == null || !profile.getRepository().isLoaded()) return;
JFXUtilities.runInFX(() ->
selectedVersion.set(profile.getSelectedVersion()));
}
}
@@ -51,6 +51,7 @@ import java.lang.reflect.Method;
import java.net.URI;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.atomic.AtomicReference;
import java.util.function.BooleanSupplier;
import java.util.function.Consumer;
import java.util.function.Function;
import java.util.function.Supplier;
@@ -92,6 +93,11 @@ public final class FXUtils {
consumer.accept(value.getValue());
}
public static void runLaterIf(BooleanSupplier condition, Runnable runnable) {
if (condition.getAsBoolean()) Platform.runLater(() -> runLaterIf(condition, runnable));
else runnable.run();
}
public static void limitSize(ImageView imageView, double maxWidth, double maxHeight) {
imageView.setPreserveRatio(true);
onChangeAndOperate(imageView.imageProperty(), image -> {
@@ -29,6 +29,7 @@ import org.jackhuang.hmcl.game.ModpackHelper;
import org.jackhuang.hmcl.mod.Modpack;
import org.jackhuang.hmcl.mod.UnsupportedModpackException;
import org.jackhuang.hmcl.setting.Accounts;
import org.jackhuang.hmcl.setting.Profile;
import org.jackhuang.hmcl.setting.Profiles;
import org.jackhuang.hmcl.setting.Theme;
import org.jackhuang.hmcl.task.Schedulers;
@@ -86,7 +87,9 @@ public final class LeftPaneController extends AdvancedListBox {
.add(launcherSettingsItem);
EventBus.EVENT_BUS.channel(RefreshedVersionsEvent.class).register(event -> onRefreshedVersions((HMCLGameRepository) event.getSource()));
if (Profiles.selectedProfileProperty().get().getRepository().isLoaded())
Profile profile = Profiles.getSelectedProfile();
if (profile != null && profile.getRepository().isLoaded())
onRefreshedVersions(Profiles.selectedProfileProperty().get().getRepository());
}
@@ -17,25 +17,55 @@
*/
package org.jackhuang.hmcl.ui;
import com.jfoenix.concurrency.JFXUtilities;
import com.jfoenix.controls.JFXButton;
import com.jfoenix.controls.JFXPopup;
import javafx.beans.property.ReadOnlyStringProperty;
import javafx.beans.property.ReadOnlyStringWrapper;
import javafx.fxml.FXML;
import javafx.geometry.Pos;
import javafx.scene.Node;
import javafx.scene.control.Label;
import javafx.scene.layout.StackPane;
import javafx.scene.layout.VBox;
import javafx.scene.paint.Color;
import javafx.scene.shape.Rectangle;
import org.jackhuang.hmcl.event.EventBus;
import org.jackhuang.hmcl.event.RefreshedVersionsEvent;
import org.jackhuang.hmcl.event.RefreshingVersionsEvent;
import org.jackhuang.hmcl.game.HMCLGameRepository;
import org.jackhuang.hmcl.setting.ConfigHolder;
import org.jackhuang.hmcl.setting.Profile;
import org.jackhuang.hmcl.setting.Profiles;
import org.jackhuang.hmcl.setting.Theme;
import org.jackhuang.hmcl.ui.construct.IconedMenuItem;
import org.jackhuang.hmcl.ui.decorator.DecoratorPage;
import org.jackhuang.hmcl.ui.versions.Versions;
import org.jackhuang.hmcl.util.VersionNumber;
import java.util.List;
import java.util.stream.Collectors;
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
public final class MainPage extends StackPane implements DecoratorPage {
private final ReadOnlyStringWrapper title = new ReadOnlyStringWrapper(this, "title", i18n("main_page"));
private final VBox menu = new VBox();
private final JFXPopup popup = new JFXPopup(menu);
@FXML
private StackPane main;
@FXML
private JFXButton btnLaunch;
@FXML
private JFXButton btnMenu;
@FXML
private Label lblCurrentGame;
@FXML
private Rectangle separator;
private Profile profile;
{
FXUtils.loadFXML(this, "/assets/fxml/main.fxml");
@@ -45,6 +75,54 @@ public final class MainPage extends StackPane implements DecoratorPage {
else
getChildren().setAll(main);
});
btnLaunch.setClip(new Rectangle(-100, -100, 280, 200));
btnMenu.setClip(new Rectangle(181, -100, 100, 200));
menu.setMinWidth(200);
StackPane graphic = new StackPane();
Node svg = SVG.triangle(Theme.whiteFillBinding(), 10, 10);
StackPane.setAlignment(svg, Pos.CENTER_RIGHT);
graphic.getChildren().setAll(svg);
graphic.setTranslateX(12);
btnMenu.setGraphic(graphic);
FXUtils.onChangeAndOperate(Profiles.selectedVersionProperty(), version -> {
if (version != null) {
lblCurrentGame.setText(version);
} else {
lblCurrentGame.setText(i18n("version.empty"));
}
});
EventBus.EVENT_BUS.channel(RefreshedVersionsEvent.class).register(event -> {
if (event.getSource() == profile.getRepository())
loadVersions((HMCLGameRepository) event.getSource());
});
Profiles.selectedProfileProperty().addListener((a, b, newValue) -> profile = newValue);
profile = Profiles.getSelectedProfile();
if (profile != null) {
if (profile.getRepository().isLoaded())
loadVersions(profile.getRepository());
else
profile.getRepository().refreshVersionsAsync().start();
}
}
private void loadVersions(HMCLGameRepository repository) {
List<IconedMenuItem> children = repository.getVersions().parallelStream()
.filter(version -> !version.isHidden())
.sorted((a, b) -> VersionNumber.COMPARATOR.compare(VersionNumber.asVersion(a.getId()), VersionNumber.asVersion(b.getId())))
.map(version -> new IconedMenuItem(null, version.getId(), () -> {
repository.getProfile().setSelectedVersion(version.getId());
popup.hide();
}))
.collect(Collectors.toList());
JFXUtilities.runInFX(() -> {
if (profile == repository.getProfile())
menu.getChildren().setAll(children);
});
}
@FXML
@@ -53,6 +131,11 @@ public final class MainPage extends StackPane implements DecoratorPage {
Versions.launch(profile, profile.getSelectedVersion());
}
@FXML
private void onMenu() {
popup.show(btnMenu, JFXPopup.PopupVPosition.BOTTOM, JFXPopup.PopupHPosition.RIGHT, 0, -btnMenu.getHeight());
}
public String getTitle() {
return title.get();
}
@@ -150,4 +150,8 @@ public final class SVG {
public static Node openInNew(ObjectBinding<? extends Paint> fill, double width, double height) {
return createSVGPath("M14,3V5H17.59L7.76,14.83L9.17,16.24L19,6.41V10H21V3M19,19H5V5H12V3H5C3.89,3 3,3.9 3,5V19A2,2 0 0,0 5,21H19A2,2 0 0,0 21,19V12H19V19Z", fill, width, height);
}
public static Node triangle(ObjectBinding<? extends Paint> fill, double width, double height) {
return createSVGPath("M1,21H23L12,2", fill, width, height);
}
}
@@ -39,12 +39,17 @@ public class IconedItem extends RipplerContainer {
private static HBox createHBox(Node icon) {
HBox hBox = new HBox();
if (icon != null) {
icon.setMouseTransparent(true);
hBox.getChildren().add(icon);
}
hBox.getStyleClass().setAll("iconed-item-container");
icon.setMouseTransparent(true);
Label textLabel = new Label();
textLabel.setId("label");
textLabel.setMouseTransparent(true);
hBox.getChildren().addAll(icon, textLabel);
hBox.getChildren().addAll(textLabel);
return hBox;
}
@@ -22,6 +22,7 @@ import javafx.animation.Interpolator;
import javafx.animation.KeyFrame;
import javafx.animation.KeyValue;
import javafx.animation.Timeline;
import javafx.application.Platform;
import javafx.beans.binding.Bindings;
import javafx.event.EventHandler;
import javafx.geometry.Insets;
@@ -93,6 +94,7 @@ public class DecoratorController {
welcomeView = new ImageView();
welcomeView.setImage(new Image("/assets/img/welcome.png"));
welcomeView.setCursor(Cursor.HAND);
FXUtils.limitSize(welcomeView, 796, 517);
welcomeView.setOnMouseClicked(e -> {
Timeline nowAnimation = new Timeline();
nowAnimation.getKeyFrames().addAll(
@@ -277,12 +279,17 @@ public class DecoratorController {
FXUtils.checkFxUserThread();
if (dialog == null) {
if (decorator.getDrawerWrapper() == null) {
// Sometimes showDialog will be invoked before decorator was initialized.
// Keep trying again.
Platform.runLater(() -> showDialog(node));
return;
}
dialog = new JFXDialog();
dialogPane = new StackContainerPane();
dialog.setContent(dialogPane);
if (decorator.getDrawerWrapper() == null)
throw new IllegalStateException("Decorator has not been initialized");
dialog.setDialogContainer(decorator.getDrawerWrapper());
dialog.setOverlayClose(false);
dialog.show();
@@ -233,6 +233,7 @@ public class DecoratorSkin extends SkinBase<DecoratorControl> {
Rectangle separator = new Rectangle();
separator.visibleProperty().bind(refreshNavButton.visibleProperty());
separator.heightProperty().bind(navBar.heightProperty());
separator.setFill(Color.GRAY);
JFXButton btnMin = new JFXButton();
StackPane pane = new StackPane(minus);
@@ -38,7 +38,7 @@ public class DatapackListPage extends ListPage<DatapackListItem> implements Deco
list = MappedObservableList.create(datapack.getInfo(), pack -> new DatapackListItem(pack, item -> {
try {
datapack.deletePack(pack.getId());
datapack.deletePack(pack);
} catch (IOException e) {
Logging.LOG.warning("Failed to delete datapack");
}
@@ -84,7 +84,7 @@ public class DatapackListPage extends ListPage<DatapackListItem> implements Deco
public void add() {
FileChooser chooser = new FileChooser();
chooser.setTitle(i18n("datapack.choose_datapack"));
chooser.getExtensionFilters().setAll(new FileChooser.ExtensionFilter(i18n("extension.datapack"), "*.zip"));
chooser.getExtensionFilters().setAll(new FileChooser.ExtensionFilter(i18n("datapack.extension"), "*.zip"));
List<File> res = chooser.showOpenMultipleDialog(Controllers.getStage());
if (res != null)
@@ -24,60 +24,34 @@ import org.jackhuang.hmcl.event.EventBus;
import org.jackhuang.hmcl.event.RefreshedVersionsEvent;
import org.jackhuang.hmcl.setting.Profile;
import org.jackhuang.hmcl.setting.Profiles;
import org.jackhuang.hmcl.ui.FXUtils;
import org.jackhuang.hmcl.ui.WeakListenerHolder;
import org.jackhuang.hmcl.ui.construct.AdvancedListItem;
import java.io.File;
import java.util.Objects;
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
public class GameAdvancedListItem extends AdvancedListItem {
private final WeakListenerHolder listenerHolder = new WeakListenerHolder();
private Profile profile;
private InvalidationListener listener = o -> loadVersion();
public GameAdvancedListItem() {
Profiles.selectedProfileProperty().addListener(listenerHolder.weak((a, b, newValue) -> {
JFXUtilities.runInFX(() -> loadProfile(newValue));
}));
listenerHolder.add(EventBus.EVENT_BUS.channel(RefreshedVersionsEvent.class).registerWeak(event -> {
JFXUtilities.runInFX(() -> {
if (profile != null && profile.getRepository() == event.getSource())
loadVersion();
FXUtils.onChangeAndOperate(Profiles.selectedVersionProperty(), version -> {
FXUtils.runLaterIf(() -> !Objects.nonNull(Profiles.getSelectedProfile()), () -> {
File iconFile = Profiles.getSelectedProfile().getRepository().getVersionIcon(version);
if (iconFile.exists())
imageProperty().set(new Image("file:" + iconFile.getAbsolutePath()));
else
imageProperty().set(new Image("/assets/img/grass.png"));
if (version != null) {
titleProperty().set(version);
subtitleProperty().set(null);
} else {
titleProperty().set(i18n("version.empty"));
subtitleProperty().set(i18n("version.empty.add"));
}
});
}));
loadProfile(Profiles.getSelectedProfile());
}
private void loadProfile(Profile newProfile) {
if (profile != null)
profile.selectedVersionProperty().removeListener(listener);
profile = newProfile;
if (profile != null)
profile.selectedVersionProperty().addListener(listener);
loadVersion();
}
private void loadVersion() {
Profile profile = this.profile;
if (profile == null || !profile.getRepository().isLoaded()) return;
String version = profile.getSelectedVersion();
File iconFile = profile.getRepository().getVersionIcon(version);
JFXUtilities.runInFX(() -> {
if (iconFile.exists())
imageProperty().set(new Image("file:" + iconFile.getAbsolutePath()));
else
imageProperty().set(new Image("/assets/img/grass.png"));
if (version != null) {
titleProperty().set(version);
subtitleProperty().set(null);
} else {
titleProperty().set(i18n("version.empty"));
subtitleProperty().set(i18n("version.empty.add"));
}
});
}
}
@@ -8,6 +8,8 @@ import javafx.stage.FileChooser;
import org.jackhuang.hmcl.game.World;
import org.jackhuang.hmcl.ui.Controllers;
import org.jackhuang.hmcl.ui.wizard.SinglePageWizardProvider;
import org.jackhuang.hmcl.util.IntVersionNumber;
import org.jackhuang.hmcl.util.VersionNumber;
import java.io.File;
import java.util.Date;
@@ -24,7 +26,7 @@ public class WorldListItem extends Control {
this.world = world;
title.set(world.getWorldName());
subtitle.set(i18n("world.description", world.getFileName(), new Date(world.getLastPlayed()).toString(), world.getGameVersion()));
subtitle.set(i18n("world.description", world.getFileName(), new Date(world.getLastPlayed()).toString(), world.getGameVersion() == null ? i18n("message.unknown") : world.getGameVersion()));
}
@Override
@@ -58,6 +60,12 @@ public class WorldListItem extends Control {
}
public void manageDatapacks() {
if (world.getGameVersion() == null || // old game will not write game version to level.dat
(IntVersionNumber.isIntVersionNumber(world.getGameVersion()) // we don't parse snapshot version
&& VersionNumber.asVersion(world.getGameVersion()).compareTo(VersionNumber.asVersion("1.13")) < 0)) {
Controllers.dialog(i18n("world.datapack.1_13"));
return;
}
Controllers.navigate(new DatapackListPage(world.getWorldName(), world.getFile()));
}
}
@@ -3,16 +3,20 @@ 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.task.Schedulers;
import org.jackhuang.hmcl.task.Task;
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.FileAlreadyExistsException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.List;
import java.util.logging.Level;
import java.util.stream.Collectors;
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
@@ -25,33 +29,38 @@ public class WorldListPage extends ListPage<WorldListItem> {
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);
}
itemsProperty().setAll(World.getWorlds(savesDir).stream()
.map(WorldListItem::new).collect(Collectors.toList()));
}
@Override
public void add() {
FileChooser chooser = new FileChooser();
chooser.setTitle(i18n("world.choose_world"));
chooser.setTitle(i18n("world.import.choose"));
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);
}
});
if (res == null || res.isEmpty()) return;
try {
// Only accept one world file because user is required to confirm the new world name
// Or too many input dialogs are popped.
World world = new World(res.get(0).toPath());
Controllers.inputDialog(i18n("world.name.enter"), (name, resolve, reject) -> {
Task.of(() -> world.install(savesDir, name))
.finalized(Schedulers.javafx(), var -> {
itemsProperty().add(new WorldListItem(new World(savesDir.resolve(name))));
resolve.run();
}, e -> {
if (e instanceof FileAlreadyExistsException)
reject.accept(i18n("world.import.failed", i18n("world.import.already_exists")));
else
reject.accept(i18n("world.import.failed", e.getClass().getName() + ": " + e.getLocalizedMessage()));
}).start();
}).setInitialText(world.getWorldName());
} catch (IOException | IllegalArgumentException e) {
Logging.LOG.log(Level.WARNING, "Unable to parse datapack file " + res.get(0), e);
}
}
}
@@ -510,6 +510,15 @@
-fx-font-size:14px;
}
.jfx-button-raised .jfx-rippler {
-jfx-rippler-fill: white;
}
.jfx-button-raised .label {
-fx-text-fill: white;
-fx-font-size: 14px;
}
.jfx-button-raised-round {
-fx-background-color: -fx-base-color;
-fx-background-radius: 50px;
+17 -3
View File
@@ -2,11 +2,25 @@
<?import com.jfoenix.controls.JFXButton?>
<?import javafx.scene.layout.StackPane?>
<?import javafx.scene.control.Label?>
<?import javafx.scene.layout.VBox?>
<?import javafx.scene.shape.Rectangle?>
<fx:root type="StackPane" pickOnBounds="false"
xmlns="http://javafx.com/javafx/8.0.112" xmlns:fx="http://javafx.com/fxml/1">
<StackPane fx:id="main" style="-fx-padding: 25;">
<JFXButton prefWidth="150" prefHeight="50" buttonType="RAISED" styleClass="jfx-button-raised"
style="-fx-font-size: 15;" onMouseClicked="#launch"
text="%version.launch" StackPane.alignment="BOTTOM_RIGHT"/>
<StackPane maxWidth="200" maxHeight="50" StackPane.alignment="BOTTOM_RIGHT">
<JFXButton prefWidth="200" prefHeight="50" buttonType="RAISED" styleClass="jfx-button-raised"
onMouseClicked="#launch" fx:id="btnLaunch">
<graphic>
<VBox alignment="CENTER" translateX="-7" maxWidth="160">
<Label style="-fx-font-size: 15;" text="%version.launch"/>
<Label style="-fx-font-size: 10px;" fx:id="lblCurrentGame"/>
</VBox>
</graphic>
</JFXButton>
<Rectangle fx:id="separator" translateX="80" height="52" fill="gray" width="1" mouseTransparent="true"/>
<JFXButton prefWidth="200" prefHeight="50" buttonType="RAISED" styleClass="jfx-button-raised"
style="-fx-font-size: 15;" onMouseClicked="#onMenu" fx:id="btnMenu" />
</StackPane>
</StackPane>
</fx:root>
@@ -247,12 +247,13 @@ mods.remove=Remove
datapack=Data packs
datapack.add=Add data pack
datapack.choose_datapack=Choose the datapack zip to be imported
datapack.extension=Datapack
datapack.title=World %s - Datapacks
datapack.remove=Remove
world=Worlds/Datapacks
world.choose_world=Choose the save zip to be imported
world.datapack=Manage data packs
world.datapack.1_13=Only Minecraft 1.13 and later versions support 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
@@ -260,7 +261,11 @@ world.export.location=Export to
world.export.wizard=Export world %s
world.extension=World zip
world.game_version=Game Version
world.import.already_exists=This world already exists.
world.import.choose=Choose the save zip to be imported
world.import.failed=Unable to import this world: %s
world.name=World Name
world.name.enter=Enter the world name
profile=Game Directories
profile.default=Current directory
@@ -244,23 +244,28 @@ mods.add.success=成功新增模組 %s。
mods.choose_mod=選擇模組
mods.remove=刪除
datapack=數據檔
datapack.add=添加數據檔
datapack.choose_datapack=選擇要導入的數據包壓縮檔
datapack.title=世界 %s - 數據檔
datapack=資料包
datapack.add=添加資料包
datapack.choose_datapack=選擇要導入的資料包壓縮檔
datapack.extension=資料包
datapack.title=世界 %s - 資料包
datapack.remove=刪除
world=世界/數據檔
world.choose_world=選擇要導入的存檔壓縮檔
world.datapack=管理數據檔
world=世界/資料包
world.datapack=管理資料包
world.datapack.1_13=僅 Minecraft 1.13 及之後的版本支持資料包
world.description=%s. 上一次遊戲時間: %s. 遊戲版本: %s
world.export=導出此世界
world.export.title=選擇該世界的存儲位置
world.export.location=保存到
world.export.wizard=導出世界 %s
world.extension=存檔壓縮檔
world.import.already_exists=此世界已經存在
world.import.choose=選擇要導入的存檔壓縮檔
world.import.failed=無法導入此世界: %s
world.game_version=遊戲版本
world.name=世界名稱
world.name.enter=輸入世界名稱
profile=遊戲目錄
profile.default=目前目錄
@@ -247,12 +247,13 @@ mods.remove=删除
datapack=数据包
datapack.add=添加数据包
datapack.choose_datapack=选择要导入的数据包压缩包
datapack.extension=数据包
datapack.title=世界 %s - 数据包
datapack.remove=删除
world=世界/数据包
world.choose_world=选择要导入的存档压缩包
world.datapack=管理数据包
world.datapack.1_13=仅 Minecraft 1.13 及之后的版本支持数据包
world.description=%s. 上一次游戏时间: %s. 游戏版本: %s
world.export=导出此世界
world.export.title=选择该世界的存储位置
@@ -260,7 +261,11 @@ world.export.location=保存到
world.export.wizard=导出世界 %s
world.extension=世界压缩包
world.game_version=游戏版本
world.import.already_exists=此世界已经存在
world.import.choose=选择要导入的存档压缩包
world.import.failed=无法导入此世界:%s
world.name=世界名称
world.name.enter=输入世界名称
profile=游戏目录
profile.default=当前目录
@@ -140,6 +140,8 @@ public class DefaultGameRepository implements GameRepository {
return false;
}
if (fromVersion.getId().equals(fromVersion.getJar()))
fromVersion = fromVersion.setJar(to);
FileUtils.writeText(toJson, Constants.GSON.toJson(fromVersion.setId(to)));
return true;
} catch (IOException | JsonParseException | VersionNotFoundException e) {
@@ -5,18 +5,18 @@ 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 org.jackhuang.hmcl.util.*;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
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.logging.Level;
import java.util.stream.Collectors;
import java.util.zip.GZIPInputStream;
import java.util.zip.GZIPOutputStream;
@@ -27,7 +27,7 @@ public class World {
private String fileName;
private String worldName;
private String gameVersion;
private long lastPlayed, seed;
private long lastPlayed;
public World(Path file) throws IOException {
this.file = file;
@@ -62,10 +62,6 @@ public class World {
return lastPlayed;
}
public long getSeed() {
return seed;
}
public String getGameVersion() {
return gameVersion;
}
@@ -88,12 +84,23 @@ public class World {
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;
if (data.get("LevelName") instanceof StringTag)
worldName = data.<StringTag>get("LevelName").getValue();
else
throw new IOException("level.dat missing LevelName");
if (data.get("LastPlayed") instanceof LongTag)
lastPlayed = data.<LongTag>get("LastPlayed").getValue();
else
throw new IOException("level.dat missing LastPlayed");
gameVersion = null;
if (data.get("Version") instanceof CompoundTag) {
CompoundTag version = data.get("Version");
if (version.get("Name") instanceof StringTag)
gameVersion = version.<StringTag>get("Name").getValue();
}
}
public void rename(String newName) throws IOException {
@@ -106,7 +113,9 @@ public class World {
CompoundTag data = nbt.get("Data");
data.put(new StringTag("LevelName", newName));
NBTIO.writeTag(new GZIPOutputStream(Files.newOutputStream(levelDat)), nbt);
try (OutputStream os = new GZIPOutputStream(Files.newOutputStream(levelDat))) {
NBTIO.writeTag(os, nbt);
}
// then change the folder's name
Files.move(file, file.resolveSibling(newName));
@@ -127,9 +136,10 @@ public class World {
}
subDirectoryName = FileUtils.getName(subDirs.get(0));
}
new Unzipper(file, savesDir)
new Unzipper(file, worldDir)
.setSubDirectory("/" + subDirectoryName + "/")
.unzip();
new World(worldDir).rename(name);
} else if (Files.isDirectory(file)) {
FileUtils.copyDirectory(file, worldDir);
}
@@ -145,17 +155,28 @@ public class World {
}
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");
try (InputStream is = new GZIPInputStream(Files.newInputStream(path))) {
Tag nbt = NBTIO.readTag(is);
if (nbt instanceof CompoundTag)
return (CompoundTag) nbt;
else
throw new IOException("level.dat malformed");
}
}
public static List<World> getWorlds(Path worldDir) throws IOException {
public static List<World> getWorlds(Path savesDir) {
List<World> worlds = new ArrayList<>();
for (Path world : Files.newDirectoryStream(worldDir)) {
worlds.add(new World(world));
try {
if (Files.exists(savesDir))
for (Path world : Files.newDirectoryStream(savesDir)) {
try {
worlds.add(new World(world));
} catch (IOException | IllegalArgumentException e) {
Logging.LOG.log(Level.WARNING, "Failed to read world " + world, e);
}
}
} catch (IOException e) {
Logging.LOG.log(Level.WARNING, "Failed to read saves", e);
}
return worlds;
}
@@ -10,9 +10,7 @@ import javafx.collections.ObservableList;
import org.jackhuang.hmcl.util.*;
import java.io.IOException;
import java.nio.file.FileSystem;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.*;
import java.util.*;
import java.util.logging.Level;
@@ -41,20 +39,60 @@ public class Datapack {
if (Files.isDirectory(datapacks))
for (Path datapack : Files.newDirectoryStream(datapacks)) {
if (packs.contains(FileUtils.getName(datapack)))
if (Files.isDirectory(datapack) && packs.contains(FileUtils.getName(datapack)))
FileUtils.deleteDirectory(datapack.toFile());
else if (Files.isRegularFile(datapack) && packs.contains(FileUtils.getNameWithoutExtension(datapack)))
Files.delete(datapack);
}
if (isMultiple) {
new Unzipper(path, worldPath).setReplaceExistentFile(true).unzip();
new Unzipper(path, worldPath)
.setReplaceExistentFile(true)
.setFilter(new Unzipper.FileFilter() {
@Override
public boolean accept(Path destPath, boolean isDirectory, Path zipEntry, String entryPath) {
// We will merge resources.zip instead of replacement.
return !entryPath.equals("resources.zip");
}
})
.unzip();
try (FileSystem dest = CompressingUtils.createWritableZipFileSystem(worldPath.resolve("resources.zip"));
FileSystem zip = CompressingUtils.createReadOnlyZipFileSystem(path)) {
Path resourcesZip = zip.getPath("resources.zip");
if (Files.isRegularFile(resourcesZip)) {
Path temp = Files.createTempFile("hmcl", ".zip");
Files.copy(resourcesZip, temp, StandardCopyOption.REPLACE_EXISTING);
try (FileSystem resources = CompressingUtils.createReadOnlyZipFileSystem(temp)) {
FileUtils.copyDirectory(resources.getPath("/"), dest.getPath("/"));
}
}
Path packMcMeta = dest.getPath("pack.mcmeta");
Files.write(packMcMeta, Arrays.asList("{",
"\t\"pack\": {",
"\t\t\"pack_format\": 4,",
"\t\t\"description\": \"Modified by HMCL.\"",
"\t}",
"}"), StandardOpenOption.CREATE);
Path packPng = dest.getPath("pack.png");
if (Files.isRegularFile(packPng))
Files.delete(packPng);
}
} else {
new Unzipper(path, worldPath.resolve("datapacks").resolve(FileUtils.getNameWithoutExtension(path))).unzip();
FileUtils.copyFile(path.toFile(), datapacks.resolve(FileUtils.getName(path)).toFile());
}
}
public void deletePack(String pack) throws IOException {
FileUtils.deleteDirectory(path.resolve(pack).toFile());
Platform.runLater(() -> info.removeIf(p -> p.getId().equals(pack)));
public void deletePack(Pack pack) throws IOException {
Path subPath = pack.file;
if (Files.isDirectory(subPath))
FileUtils.deleteDirectory(subPath.toFile());
else if (Files.isRegularFile(subPath))
Files.delete(subPath);
Platform.runLater(() -> info.removeIf(p -> p.getId().equals(pack.getId())));
}
public void loadFromZip() throws IOException {
@@ -68,7 +106,7 @@ public class Datapack {
isMultiple = false;
try {
PackMcMeta pack = JsonUtils.fromNonNullJson(FileUtils.readText(mcmeta), PackMcMeta.class);
info.add(new Pack(mcmeta, FileUtils.getNameWithoutExtension(path), pack.getPackInfo().getDescription(), this));
info.add(new Pack(path, FileUtils.getNameWithoutExtension(path), pack.getPackInfo().getDescription(), this));
} catch (IOException e) {
Logging.LOG.log(Level.WARNING, "Failed to read datapack " + path, e);
}
@@ -91,20 +129,44 @@ public class Datapack {
if (Files.isDirectory(dir))
for (Path subDir : Files.newDirectoryStream(dir)) {
Path mcmeta = subDir.resolve("pack.mcmeta");
Path mcmetaDisabled = subDir.resolve("pack.mcmeta.disabled");
if (Files.isDirectory(subDir)) {
Path mcmeta = subDir.resolve("pack.mcmeta");
Path mcmetaDisabled = subDir.resolve("pack.mcmeta.disabled");
if (!Files.exists(mcmeta) && !Files.exists(mcmetaDisabled))
continue;
if (!Files.exists(mcmeta) && !Files.exists(mcmetaDisabled))
continue;
boolean enabled = Files.exists(mcmeta);
boolean enabled = Files.exists(mcmeta);
try {
PackMcMeta pack = enabled ? JsonUtils.fromNonNullJson(FileUtils.readText(mcmeta), PackMcMeta.class)
: JsonUtils.fromNonNullJson(FileUtils.readText(mcmetaDisabled), PackMcMeta.class);
info.add(new Pack(enabled ? mcmeta : mcmetaDisabled, FileUtils.getName(subDir), pack.getPackInfo().getDescription(), this));
} catch (IOException e) {
Logging.LOG.log(Level.WARNING, "Failed to read datapack " + subDir, e);
try {
PackMcMeta pack = enabled ? JsonUtils.fromNonNullJson(FileUtils.readText(mcmeta), PackMcMeta.class)
: JsonUtils.fromNonNullJson(FileUtils.readText(mcmetaDisabled), PackMcMeta.class);
info.add(new Pack(enabled ? mcmeta : mcmetaDisabled, FileUtils.getName(subDir), pack.getPackInfo().getDescription(), this));
} catch (IOException e) {
Logging.LOG.log(Level.WARNING, "Failed to read datapack " + subDir, e);
}
} else if (Files.isRegularFile(subDir)) {
try (FileSystem fs = CompressingUtils.createReadOnlyZipFileSystem(subDir)) {
Path mcmeta = fs.getPath("pack.mcmeta");
if (!Files.exists(mcmeta))
continue;
String name = FileUtils.getName(subDir);
boolean enabled = true;
if (name.endsWith(".disabled")) {
name = name.substring(0, name.length() - ".disabled".length());
enabled = false;
}
if (!name.endsWith(".zip"))
continue;
name = StringUtils.substringBeforeLast(name, ".zip");
PackMcMeta pack = JsonUtils.fromNonNullJson(FileUtils.readText(mcmeta), PackMcMeta.class);
info.add(new Pack(subDir, name, pack.getPackInfo().getDescription(), this));
} catch (IOException e) {
Logging.LOG.log(Level.WARNING, "Failed to read datapack " + subDir, e);
}
}
}
@@ -112,22 +174,22 @@ public class Datapack {
}
public static class Pack {
private Path packMcMeta;
private Path file;
private final BooleanProperty active;
private final String id;
private final String description;
private final Datapack datapack;
public Pack(Path packMcMeta, String id, String description, Datapack datapack) {
this.packMcMeta = packMcMeta;
public Pack(Path file, String id, String description, Datapack datapack) {
this.file = file;
this.id = id;
this.description = description;
this.datapack = datapack;
active = new SimpleBooleanProperty(this, "active", !DISABLED_EXT.equals(FileUtils.getExtension(packMcMeta))) {
active = new SimpleBooleanProperty(this, "active", !DISABLED_EXT.equals(FileUtils.getExtension(file))) {
@Override
protected void invalidated() {
Path f = Pack.this.packMcMeta.toAbsolutePath(), newF;
Path f = Pack.this.file.toAbsolutePath(), newF;
if (DISABLED_EXT.equals(FileUtils.getExtension(f)))
newF = f.getParent().resolve(FileUtils.getNameWithoutExtension(f));
else
@@ -135,7 +197,7 @@ public class Datapack {
try {
Files.move(f, newF);
Pack.this.packMcMeta = newF;
Pack.this.file = newF;
} catch (IOException e) {
// Mod file is occupied.
Logging.LOG.warning("Unable to rename file " + f + " to " + newF);
@@ -301,7 +301,13 @@ public abstract class Task {
return finalized(scheduler, (variables, isDependentsSucceeded) -> {
if (isDependentsSucceeded) {
if (success != null)
success.accept(variables);
try {
success.accept(variables);
} catch (Exception e) {
Logging.LOG.log(Level.WARNING, "Failed to execute " + success, e);
if (failure != null)
failure.accept(e);
}
}
else {
if (failure != null)
@@ -73,6 +73,8 @@ public final class CompressingUtils {
} catch (ZipError error) {
// Since Java 8 throws ZipError stupidly
throw new ZipException(error.getMessage());
} catch (UnsupportedOperationException ex) {
throw new IOException("Not a zip file", ex);
}
}
@@ -118,7 +118,7 @@ public final class FileUtils {
@Override
public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException {
Path destFile = dest.resolve(src.relativize(file));
Files.copy(file, destFile);
Files.copy(file, destFile, StandardCopyOption.REPLACE_EXISTING);
return FileVisitResult.CONTINUE;
}
@@ -126,7 +126,7 @@ public final class FileUtils {
@Override
public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs) throws IOException {
Path destDir = dest.resolve(src.relativize(dir));
Files.createDirectory(destDir);
Files.createDirectories(destDir);
return FileVisitResult.CONTINUE;
}