World and datapacks management

This commit is contained in:
huanghongxun 2018-09-16 12:38:15 +08:00
parent 1753b4d27e
commit 07febc18d4
23 changed files with 670 additions and 90 deletions

View File

@ -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) {

View 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);
}

View File

@ -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() {
}
}

View File

@ -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);

View File

@ -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();
}
}

View File

@ -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)),

View File

@ -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

View File

@ -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()));
}
}

View File

@ -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);
}
}

View File

@ -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.
}
}

View File

@ -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()));
}
}

View File

@ -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);

View File

@ -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);
}
});
}
}

View File

@ -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;
}
}

View File

@ -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) {
}
}

View File

@ -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">

View File

@ -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!

View File

@ -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 嗎?你將無法找回被刪除的檔案!

View File

@ -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 吗?你将无法找回被删除的文件!

View File

@ -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();

View File

@ -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;
}
}

View File

@ -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

View File

@ -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'
}