支持查看 NBT 文件 (#2402)

* init

* fix checkstyle

* update RootPage

* fix build

* update

* update
This commit is contained in:
Glavo
2024-01-26 01:24:31 +08:00
committed by GitHub
parent 43fee4878f
commit 644866a24b
33 changed files with 500 additions and 6 deletions

View File

@@ -39,9 +39,12 @@ import org.jackhuang.hmcl.ui.construct.MessageDialogPane;
import org.jackhuang.hmcl.ui.decorator.DecoratorAnimatedPage;
import org.jackhuang.hmcl.ui.decorator.DecoratorPage;
import org.jackhuang.hmcl.ui.download.ModpackInstallWizardProvider;
import org.jackhuang.hmcl.ui.nbt.NBTEditorPage;
import org.jackhuang.hmcl.ui.nbt.NBTHelper;
import org.jackhuang.hmcl.ui.versions.GameAdvancedListItem;
import org.jackhuang.hmcl.ui.versions.Versions;
import org.jackhuang.hmcl.upgrade.hmcl.UpdateChecker;
import org.jackhuang.hmcl.util.StringUtils;
import org.jackhuang.hmcl.util.TaskCancellationAction;
import org.jackhuang.hmcl.util.io.CompressingUtils;
import org.jackhuang.hmcl.util.versioning.VersionNumber;
@@ -51,10 +54,12 @@ import java.util.Comparator;
import java.util.Date;
import java.util.List;
import java.util.Locale;
import java.util.logging.Level;
import java.util.stream.Collectors;
import static org.jackhuang.hmcl.ui.FXUtils.runInFX;
import static org.jackhuang.hmcl.ui.versions.VersionPage.wrap;
import static org.jackhuang.hmcl.util.Logging.LOG;
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
public class RootPage extends DecoratorAnimatedPage implements DecoratorPage {
@@ -85,12 +90,24 @@ public class RootPage extends DecoratorAnimatedPage implements DecoratorPage {
public MainPage getMainPage() {
if (mainPage == null) {
MainPage mainPage = new MainPage();
FXUtils.applyDragListener(mainPage, ModpackHelper::isFileModpackByExtension, modpacks -> {
File modpack = modpacks.get(0);
Controllers.getDecorator().startWizard(
new ModpackInstallWizardProvider(Profiles.getSelectedProfile(), modpack),
i18n("install.modpack"));
});
FXUtils.applyDragListener(mainPage,
file -> ModpackHelper.isFileModpackByExtension(file) || NBTHelper.isNBTFileByExtension(file),
modpacks -> {
File file = modpacks.get(0);
if (ModpackHelper.isFileModpackByExtension(file)) {
Controllers.getDecorator().startWizard(
new ModpackInstallWizardProvider(Profiles.getSelectedProfile(), file),
i18n("install.modpack"));
} else if (NBTHelper.isNBTFileByExtension(file)) {
try {
Controllers.navigate(new NBTEditorPage(file));
} catch (Throwable e) {
LOG.log(Level.WARNING, "Fail to open nbt file", e);
Controllers.dialog(i18n("nbt.open.failed") + "\n\n" + StringUtils.getStackTrace(e),
i18n("message.error"), MessageDialogPane.MessageType.ERROR);
}
}
});
FXUtils.onChangeAndOperate(Profiles.selectedVersionProperty(), mainPage::setCurrentGame);
mainPage.showUpdateProperty().bind(UpdateChecker.outdatedProperty());

View File

@@ -0,0 +1,91 @@
package org.jackhuang.hmcl.ui.nbt;
import com.jfoenix.controls.JFXButton;
import javafx.beans.property.ReadOnlyObjectProperty;
import javafx.beans.property.ReadOnlyObjectWrapper;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.control.ProgressIndicator;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.HBox;
import org.jackhuang.hmcl.task.Schedulers;
import org.jackhuang.hmcl.ui.Controllers;
import org.jackhuang.hmcl.ui.construct.MessageDialogPane;
import org.jackhuang.hmcl.ui.construct.PageCloseEvent;
import org.jackhuang.hmcl.ui.decorator.DecoratorPage;
import org.jackhuang.hmcl.util.Lang;
import org.jackhuang.hmcl.util.StringUtils;
import java.io.*;
import java.util.concurrent.CompletableFuture;
import java.util.logging.Level;
import static org.jackhuang.hmcl.ui.FXUtils.onEscPressed;
import static org.jackhuang.hmcl.util.Logging.LOG;
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
public class NBTEditorPage extends BorderPane implements DecoratorPage {
private final ReadOnlyObjectWrapper<State> state;
private final File file;
private final NBTFileType type;
public NBTEditorPage(File file) throws IOException {
getStyleClass().add("gray-background");
this.state = new ReadOnlyObjectWrapper<>(DecoratorPage.State.fromTitle(i18n("nbt.title", file.getAbsolutePath())));
this.file = file;
this.type = NBTFileType.ofFile(file);
if (type == null) {
throw new IOException("Unknown type of file " + file);
}
setCenter(new ProgressIndicator());
HBox actions = new HBox(8);
actions.setPadding(new Insets(8));
actions.setAlignment(Pos.CENTER_RIGHT);
JFXButton saveButton = new JFXButton(i18n("button.save"));
saveButton.getStyleClass().add("jfx-button-raised");
saveButton.setButtonType(JFXButton.ButtonType.RAISED);
saveButton.setOnAction(e -> {
try {
save();
} catch (IOException ex) {
LOG.log(Level.WARNING, "Failed to save NBT file", ex);
Controllers.dialog(i18n("nbt.save.failed") + "\n\n" + StringUtils.getStackTrace(ex));
}
});
JFXButton cancelButton = new JFXButton(i18n("button.cancel"));
cancelButton.getStyleClass().add("jfx-button-raised");
cancelButton.setButtonType(JFXButton.ButtonType.RAISED);
cancelButton.setOnAction(e -> fireEvent(new PageCloseEvent()));
onEscPressed(this, cancelButton::fire);
actions.getChildren().setAll(saveButton, cancelButton);
CompletableFuture.supplyAsync(Lang.wrap(() -> type.readAsTree(file)))
.thenAcceptAsync(tree -> {
setCenter(new NBTTreeView(tree));
// setBottom(actions);
}, Schedulers.javafx())
.handleAsync((result, e) -> {
if (e != null) {
LOG.log(Level.WARNING, "Fail to open nbt file", e);
Controllers.dialog(i18n("nbt.open.failed") + "\n\n" + StringUtils.getStackTrace(e), null, MessageDialogPane.MessageType.WARNING, cancelButton::fire);
}
return null;
}, Schedulers.javafx());
}
public void save() throws IOException {
// TODO
}
@Override
public ReadOnlyObjectProperty<State> stateProperty() {
return state;
}
}

View File

@@ -0,0 +1,148 @@
package org.jackhuang.hmcl.ui.nbt;
import com.github.steveice10.opennbt.NBTIO;
import com.github.steveice10.opennbt.tag.builtin.CompoundTag;
import com.github.steveice10.opennbt.tag.builtin.IntTag;
import com.github.steveice10.opennbt.tag.builtin.ListTag;
import com.github.steveice10.opennbt.tag.builtin.Tag;
import org.apache.commons.compress.utils.BoundedInputStream;
import org.jackhuang.hmcl.util.io.FileUtils;
import java.io.*;
import java.util.zip.GZIPInputStream;
import java.util.zip.Inflater;
import java.util.zip.InflaterInputStream;
public enum NBTFileType {
COMPRESSED("dat", "dat_old") {
@Override
public Tag read(File file) throws IOException {
try (InputStream in = new GZIPInputStream(new FileInputStream(file))) {
Tag tag = NBTIO.readTag(in);
if (!(tag instanceof CompoundTag))
throw new IOException("Unexpected tag: " + tag);
return tag;
}
}
},
ANVIL("mca") {
@Override
public Tag read(File file) throws IOException {
return REGION.read(file);
}
@Override
public NBTTreeView.Item readAsTree(File file) throws IOException {
return REGION.readAsTree(file);
}
},
REGION("mcr") {
@Override
public Tag read(File file) throws IOException {
try (RandomAccessFile r = new RandomAccessFile(file, "r")) {
byte[] header = new byte[4096];
byte[] buffer = new byte[1 * 1024 * 1024]; // The maximum size of each chunk is 1MiB
Inflater inflater = new Inflater();
ListTag tag = new ListTag(file.getName(), CompoundTag.class);
r.readFully(header);
for (int i = 0; i < 4096; i += 4) {
int offset = ((header[i] & 0xff) << 16) + ((header[i + 1] & 0xff) << 8) + (header[i + 2] & 0xff);
int length = header[i + 3] & 0xff;
if (offset == 0 || length == 0) {
continue;
}
r.seek(offset * 4096L);
r.readFully(buffer, 0, length * 4096);
int chunkLength = ((buffer[0] & 0xff) << 24) + ((buffer[1] & 0xff) << 16) + ((buffer[2] & 0xff) << 8) + (buffer[3] & 0xff);
InputStream input = new ByteArrayInputStream(buffer);
input.skip(5);
input = new BoundedInputStream(input, chunkLength - 1);
switch (buffer[4]) {
case 0x01:
// GZip
input = new GZIPInputStream(input);
break;
case 0x02:
// Zlib
inflater.reset();
input = new InflaterInputStream(input, inflater);
break;
case 0x03:
// Uncompressed
break;
default:
throw new IOException("Unsupported compression method: " + Integer.toHexString(buffer[4] & 0xff));
}
try (InputStream in = input) {
Tag chunk = NBTIO.readTag(in);
if (!(chunk instanceof CompoundTag))
throw new IOException("Unexpected tag: " + chunk);
tag.add(chunk);
}
}
return tag;
}
}
@Override
public NBTTreeView.Item readAsTree(File file) throws IOException {
NBTTreeView.Item item = new NBTTreeView.Item(read(file));
for (Tag tag : ((ListTag) item.getValue())) {
CompoundTag chunk = (CompoundTag) tag;
NBTTreeView.Item tree = NBTTreeView.buildTree(chunk);
Tag xPos = chunk.get("xPos");
Tag zPos = chunk.get("zPos");
if (xPos instanceof IntTag && zPos instanceof IntTag) {
tree.setText(String.format("Chunk: %d %d", xPos.getValue(), zPos.getValue()));
} else {
tree.setText("Chunk: Unknown");
}
item.getChildren().add(tree);
}
return item;
}
};
static final NBTFileType[] types = values();
public static NBTFileType ofFile(File file) {
String ext = FileUtils.getExtension(file);
for (NBTFileType type : types) {
for (String extension : type.extensions) {
if (extension.equals(ext))
return type;
}
}
return null;
}
private final String[] extensions;
NBTFileType(String... extensions) {
this.extensions = extensions;
}
public abstract Tag read(File file) throws IOException;
public NBTTreeView.Item readAsTree(File file) throws IOException {
NBTTreeView.Item root = NBTTreeView.buildTree(read(file));
root.setName(file.getName());
return root;
}
}

View File

@@ -0,0 +1,12 @@
package org.jackhuang.hmcl.ui.nbt;
import java.io.File;
public final class NBTHelper {
private NBTHelper() {
}
public static boolean isNBTFileByExtension(File file) {
return NBTFileType.ofFile(file) != null;
}
}

View File

@@ -0,0 +1,59 @@
package org.jackhuang.hmcl.ui.nbt;
import com.github.steveice10.opennbt.tag.builtin.Tag;
import java.util.HashMap;
import java.util.Locale;
import java.util.Map;
public enum NBTTagType {
BYTE, SHORT, INT, LONG, FLOAT, DOUBLE,
BYTE_ARRAY, INT_ARRAY, LONG_ARRAY,
STRING,
LIST, COMPOUND;
private static final Map<String, NBTTagType> lookupTable = new HashMap<>();
static {
for (NBTTagType type : values()) {
lookupTable.put(type.getTagClassName(), type);
}
}
public static NBTTagType typeOf(Tag tag) {
NBTTagType type = lookupTable.get(tag.getClass().getSimpleName());
if (type == null) {
throw new IllegalArgumentException("Unknown tag: " + type);
}
return type;
}
private final String iconUrl;
private final String tagClassName;
NBTTagType() {
String tagName;
String className;
int idx = name().indexOf('_');
if (idx < 0) {
tagName = name().charAt(0) + name().substring(1).toLowerCase(Locale.ROOT);
className = tagName + "Tag";
} else {
tagName = name().charAt(0) + name().substring(1, idx + 1).toLowerCase(Locale.ROOT)
+ name().charAt(idx + 1) + name().substring(idx + 2).toLowerCase(Locale.ROOT);
className = tagName.substring(0, idx) + tagName.substring(idx + 1) + "Tag";
}
this.iconUrl = "/assets/img/nbt/TAG_" + tagName + ".png";
this.tagClassName = className;
}
public String getIconUrl() {
return iconUrl;
}
public String getTagClassName() {
return tagClassName;
}
}

View File

@@ -0,0 +1,152 @@
package org.jackhuang.hmcl.ui.nbt;
import com.github.steveice10.opennbt.tag.builtin.CompoundTag;
import com.github.steveice10.opennbt.tag.builtin.ListTag;
import com.github.steveice10.opennbt.tag.builtin.Tag;
import javafx.scene.control.*;
import javafx.scene.image.Image;
import javafx.scene.image.ImageView;
import javafx.util.Callback;
import org.jackhuang.hmcl.util.Holder;
import java.lang.reflect.Array;
import java.util.EnumMap;
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
public class NBTTreeView extends TreeView<Tag> {
public NBTTreeView(NBTTreeView.Item tree) {
this.setRoot(tree);
this.setCellFactory(cellFactory());
}
private static Callback<TreeView<Tag>, TreeCell<Tag>> cellFactory() {
Holder<Object> lastCell = new Holder<>();
EnumMap<NBTTagType, Image> icons = new EnumMap<>(NBTTagType.class);
return view -> new TreeCell<Tag>() {
private void setTagText(String text) {
String name = ((Item) getTreeItem()).getName();
if (name == null) {
setText(text);
} else if (text == null) {
setText(name);
} else {
setText(name + ": " + text);
}
}
private void setTagText(int nEntries) {
setTagText(i18n("nbt.entries", nEntries));
}
@Override
public void updateItem(Tag item, boolean empty) {
super.updateItem(item, empty);
// https://mail.openjdk.org/pipermail/openjfx-dev/2022-July/034764.html
if (this == lastCell.value && !isVisible())
return;
lastCell.value = this;
ImageView imageView = (ImageView) this.getGraphic();
if (imageView == null) {
imageView = new ImageView();
this.setGraphic(imageView);
}
if (item == null) {
imageView.setImage(null);
setText(null);
return;
}
NBTTagType tagType = NBTTagType.typeOf(item);
imageView.setImage(icons.computeIfAbsent(tagType, type -> new Image(type.getIconUrl())));
if (((Item) getTreeItem()).getText() != null) {
setText(((Item) getTreeItem()).getText());
} else {
switch (tagType) {
case BYTE:
case SHORT:
case INT:
case LONG:
case FLOAT:
case DOUBLE:
case STRING:
setTagText(item.getValue().toString());
break;
case BYTE_ARRAY:
case INT_ARRAY:
case LONG_ARRAY:
setTagText(Array.getLength(item.getValue()));
break;
case LIST:
setTagText(((ListTag) item).size());
break;
case COMPOUND:
setTagText(((CompoundTag) item).size());
break;
default:
setTagText(null);
}
}
}
};
}
public static Item buildTree(Tag tag) {
Item item = new Item(tag);
if (tag instanceof CompoundTag) {
for (Tag subTag : ((CompoundTag) tag)) {
item.getChildren().add(buildTree(subTag));
}
} else if (tag instanceof ListTag) {
int idx = 0;
for (Tag subTag : ((ListTag) tag)) {
Item subTree = buildTree(subTag);
subTree.setName(String.valueOf(idx++));
item.getChildren().add(subTree);
}
}
return item;
}
public CompoundTag getRootTag() {
return ((CompoundTag) getRoot().getValue());
}
public static class Item extends TreeItem<Tag> {
private String text;
private String name;
public Item() {
}
public Item(Tag value) {
super(value);
}
public String getText() {
return text;
}
public void setText(String text) {
this.text = text;
}
public void setName(String name) {
this.name = name;
}
public String getName() {
return name == null ? getValue().getName() : name;
}
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 980 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

View File

@@ -906,6 +906,11 @@ multiplayer=Multiplayer online
multiplayer.hint=Multiplayer online function is under maintenance.
multiplayer.hint.details=View details
nbt.entries=%s entries
nbt.open.failed=Fail to open file
nbt.save.failed=Fail to save file
nbt.title=View File - %s
datapack=Datapacks
datapack.add=Install datapack
datapack.choose_datapack=Select a datapack to import

View File

@@ -780,6 +780,11 @@ multiplayer=多人聯機
multiplayer.hint=多人聯機服務正在維護中。
multiplayer.hint.details=查看詳情
nbt.entries=%s 個條目
nbt.open.failed=打開檔案失敗
nbt.save.failed=保存檔案失敗
nbt.title=查看檔案 - %s
datapack=資料包
datapack.add=加入資料包
datapack.choose_datapack=選擇要匯入的資料包壓縮檔

View File

@@ -779,6 +779,11 @@ multiplayer=多人联机
multiplayer.hint=多人联机服务正在维护中。
multiplayer.hint.details=查看详情
nbt.entries=%s 个条目
nbt.open.failed=打开文件失败
nbt.save.failed=保存文件失败
nbt.title=查看文件 - %s
datapack=数据包
datapack.add=添加数据包
datapack.choose_datapack=选择要导入的数据包压缩包