From 08ee37b383d29f1b97949527ed328a3127ffaef6 Mon Sep 17 00:00:00 2001 From: Calboot Date: Fri, 30 Jan 2026 23:15:26 +0800 Subject: [PATCH 01/53] =?UTF-8?q?=E5=BB=B6=E8=BF=9F=E5=8A=A0=E8=BD=BD?= =?UTF-8?q?=E6=B7=B1=E5=B1=82=E7=9B=AE=E5=BD=95=E4=B8=AD=E7=9A=84=E5=8E=9F?= =?UTF-8?q?=E7=90=86=E5=9B=BE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../hmcl/ui/versions/SchematicsPage.java | 86 ++++++++++++------- .../hmcl/schematic/LitematicFile.java | 5 ++ 2 files changed, 62 insertions(+), 29 deletions(-) diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/SchematicsPage.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/SchematicsPage.java index c50e4f686e..e456730cac 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/SchematicsPage.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/SchematicsPage.java @@ -20,6 +20,8 @@ import com.jfoenix.controls.JFXButton; import com.jfoenix.controls.JFXDialogLayout; import com.jfoenix.controls.JFXListView; +import javafx.beans.property.BooleanProperty; +import javafx.beans.property.SimpleBooleanProperty; import javafx.geometry.Insets; import javafx.geometry.Pos; import javafx.scene.Group; @@ -98,7 +100,7 @@ public void refresh() { if (schematicsDirectory == null) return; setLoading(true); - Task.supplyAsync(() -> loadAll(schematicsDirectory, null)) + Task.supplyAsync(() -> loadRoot(schematicsDirectory)) .whenComplete(Schedulers.javafx(), (result, exception) -> { setLoading(false); if (exception == null) { @@ -191,32 +193,15 @@ public void onCreateDirectory() { })); } - private DirItem loadAll(Path dir, @Nullable DirItem parent) { - DirItem item = new DirItem(dir, parent); - - try (Stream stream = Files.list(dir)) { - for (Path path : Lang.toIterable(stream)) { - if (Files.isDirectory(path)) { - item.children.add(loadAll(path, item)); - } else if (path.getFileName().toString().endsWith(".litematic") && Files.isRegularFile(path)) { - try { - item.children.add(new LitematicFileItem(LitematicFile.load(path))); - } catch (IOException e) { - LOG.warning("Failed to load litematic file: " + path, e); - } - } - } - } catch (NoSuchFileException ignored) { - } catch (IOException e) { - LOG.warning("Failed to load schematics in " + dir, e); - } - - item.children.sort(Comparator.naturalOrder()); + private DirItem loadRoot(Path dir) { + var item = new DirItem(dir, null); + item.load(); return item; } private void navigateTo(DirItem item) { currentDirectory = item; + item.load(); getItems().clear(); if (item.parent != null) { getItems().add(new BackItem(item.parent)); @@ -317,6 +302,9 @@ private final class DirItem extends Item { final @Nullable DirItem parent; final List children = new ArrayList<>(); final List relativePath; + int size = 0; + boolean preLoaded = false; + boolean loaded = false; DirItem(Path path, @Nullable DirItem parent) { this.path = path; @@ -324,7 +312,7 @@ private final class DirItem extends Item { if (parent != null) { this.relativePath = new ArrayList<>(parent.relativePath); - relativePath.add(path.getFileName().toString()); + relativePath.add(FileUtils.getName(path)); } else { this.relativePath = Collections.emptyList(); } @@ -342,12 +330,12 @@ Path getPath() { @Override public String getName() { - return path.getFileName().toString(); + return FileUtils.getName(path); } @Override String getDescription() { - return i18n("schematics.sub_items", children.size()); + return i18n("schematics.sub_items", size); } @Override @@ -355,6 +343,40 @@ SVG getIcon() { return SVG.FOLDER; } + void preLoad() throws IOException { + if (this.preLoaded) return; + try (Stream stream = Files.list(path)) { + this.size = (int) stream.filter(p -> Files.isDirectory(p) || LitematicFile.isFileLitematic(p)).count(); + } + } + + void load() { + if (this.loaded) return; + + try (Stream stream = Files.list(path)) { + if (!this.preLoaded) preLoad(); + for (Path p : Lang.toIterable(stream)) { + if (Files.isDirectory(p)) { + var child = new DirItem(p, this); + child.preLoad(); + this.children.add(child); + } else if (LitematicFile.isFileLitematic(p)) { + try { + this.children.add(new LitematicFileItem(LitematicFile.load(p))); + } catch (IOException e) { + LOG.warning("Failed to load litematic file: " + path, e); + } + } + } + } catch (NoSuchFileException ignored) { + } catch (IOException e) { + LOG.warning("Failed to load schematics in " + path, e); + } + + this.children.sort(Comparator.naturalOrder()); + this.loaded = true; + } + @Override void onClick() { navigateTo(this); @@ -368,8 +390,7 @@ void onReveal() { @Override void onDelete() { try { - FileUtils.cleanDirectory(path); - Files.deleteIfExists(path); + FileUtils.deleteDirectory(path); refresh(); } catch (IOException e) { LOG.warning("Failed to delete directory: " + path, e); @@ -389,7 +410,7 @@ private LitematicFileItem(LitematicFile file) { if (name != null && !"Unnamed".equals(name)) { this.name = name; } else { - this.name = StringUtils.removeSuffix(file.getFile().getFileName().toString(), ".litematic"); + this.name = FileUtils.getNameWithoutExtension(name); } WritableImage image = null; @@ -554,6 +575,8 @@ private static final class Cell extends ListCell { private final SVGPath iconSVG; private final StackPane iconSVGWrapper; + private final BooleanProperty directoryProperty = new SimpleBooleanProperty(this, "isDirectory", false); + private final Tooltip tooltip = new Tooltip(); public Cell() { @@ -594,7 +617,11 @@ public Cell() { JFXButton btnReveal = new JFXButton(); FXUtils.installFastTooltip(btnReveal, i18n("reveal.in_file_manager")); btnReveal.getStyleClass().add("toggle-icon4"); - btnReveal.setGraphic(SVG.FOLDER_OPEN.createIcon()); + { + var fo = SVG.FOLDER_OPEN.createIcon(); + var f = SVG.FOLDER.createIcon(); + btnReveal.graphicProperty().bind(directoryProperty.map(b -> b ? fo : f)); + } btnReveal.setOnAction(event -> { Item item = getItem(); if (item != null && !(item instanceof BackItem)) @@ -634,6 +661,7 @@ protected void updateItem(Item item, boolean empty) { center.setTitle(""); center.setSubtitle(""); } else { + directoryProperty.set(item.isDirectory()); if (item instanceof LitematicFileItem fileItem && fileItem.getImage() != null) { iconImageView.setImage(fileItem.getImage()); left.getChildren().setAll(iconImageView); diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/schematic/LitematicFile.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/schematic/LitematicFile.java index 3ad27a3692..1de4f0318d 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/schematic/LitematicFile.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/schematic/LitematicFile.java @@ -20,6 +20,7 @@ import com.github.steveice10.opennbt.NBTIO; import com.github.steveice10.opennbt.tag.builtin.*; import javafx.geometry.Point3D; +import org.jackhuang.hmcl.util.io.FileUtils; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; @@ -51,6 +52,10 @@ private static int tryGetInt(Tag tag) { return tag instanceof StringTag ? ((StringTag) tag).getValue() : null; } + public static boolean isFileLitematic(Path path) { + return "litematic".equals(FileUtils.getExtension(path)) && Files.isRegularFile(path); + } + public static LitematicFile load(Path file) throws IOException { CompoundTag root; From b87a6d0257ac8e39d01a2a6e3c3d9495edddbe3f Mon Sep 17 00:00:00 2001 From: Calboot Date: Sat, 31 Jan 2026 20:50:22 +0800 Subject: [PATCH 02/53] =?UTF-8?q?=E5=BC=82=E6=AD=A5=E5=8A=A0=E8=BD=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../hmcl/ui/versions/SchematicsPage.java | 169 +++++++++++------- 1 file changed, 101 insertions(+), 68 deletions(-) diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/SchematicsPage.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/SchematicsPage.java index e456730cac..5904faa3a6 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/SchematicsPage.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/SchematicsPage.java @@ -20,6 +20,7 @@ import com.jfoenix.controls.JFXButton; import com.jfoenix.controls.JFXDialogLayout; import com.jfoenix.controls.JFXListView; +import javafx.beans.binding.Bindings; import javafx.beans.property.BooleanProperty; import javafx.beans.property.SimpleBooleanProperty; import javafx.geometry.Insets; @@ -31,8 +32,11 @@ import javafx.scene.image.ImageView; import javafx.scene.image.PixelWriter; import javafx.scene.image.WritableImage; +import javafx.scene.input.KeyCode; +import javafx.scene.input.KeyEvent; import javafx.scene.layout.BorderPane; import javafx.scene.layout.HBox; +import javafx.scene.layout.Priority; import javafx.scene.layout.StackPane; import javafx.scene.shape.SVGPath; import javafx.stage.FileChooser; @@ -57,7 +61,9 @@ import java.util.*; import java.util.stream.Stream; +import static org.jackhuang.hmcl.ui.FXUtils.ignoreEvent; import static org.jackhuang.hmcl.ui.FXUtils.onEscPressed; +import static org.jackhuang.hmcl.ui.ToolbarListPageSkin.createToolbarButton2; import static org.jackhuang.hmcl.util.i18n.I18n.i18n; import static org.jackhuang.hmcl.util.logging.Logger.LOG; @@ -85,7 +91,7 @@ public SchematicsPage() { @Override protected Skin createDefaultSkin() { - return new SchematicsPageSkin(); + return new SchematicsPageSkin(this); } @Override @@ -193,6 +199,10 @@ public void onCreateDirectory() { })); } + public void onRevealSchematicsFolder() { + FXUtils.openFolder(schematicsDirectory); + } + private DirItem loadRoot(Path dir) { var item = new DirItem(dir, null); item.load(); @@ -201,12 +211,15 @@ private DirItem loadRoot(Path dir) { private void navigateTo(DirItem item) { currentDirectory = item; - item.load(); - getItems().clear(); - if (item.parent != null) { - getItems().add(new BackItem(item.parent)); - } - getItems().addAll(item.children); + setLoading(true); + Task.runAsync(item::load).whenComplete(Schedulers.javafx(), exception -> { + getItems().clear(); + if (item.parent != null) { + getItems().add(new BackItem(item.parent)); + } + getItems().addAll(item.children); + setLoading(false); + }).start(); } abstract sealed class Item implements Comparable { @@ -563,10 +576,8 @@ private void updateContent(LitematicFile file) { } } - private static final class Cell extends ListCell { + private static final class Cell extends MDListCell { - private final RipplerContainer graphics; - private final BorderPane root; private final StackPane left; private final TwoLineListItem center; private final HBox right; @@ -579,10 +590,11 @@ private static final class Cell extends ListCell { private final Tooltip tooltip = new Tooltip(); - public Cell() { - this.root = new BorderPane(); - root.getStyleClass().add("md-list-cell"); - root.setPadding(new Insets(8)); + public Cell(JFXListView listView) { + super(listView); + + var box = new HBox(8); + box.setAlignment(Pos.CENTER_LEFT); { this.left = new StackPane(); @@ -600,16 +612,11 @@ public Cell() { iconSVGWrapper.setAlignment(Pos.CENTER); FXUtils.setLimitWidth(iconSVGWrapper, 32); FXUtils.setLimitHeight(iconSVGWrapper, 32); - - BorderPane.setAlignment(left, Pos.CENTER); - root.setLeft(left); } - { this.center = new TwoLineListItem(); - root.setCenter(center); + HBox.setHgrow(center, Priority.ALWAYS); } - { this.right = new HBox(8); right.setAlignment(Pos.CENTER_RIGHT); @@ -642,70 +649,96 @@ public Cell() { right.getChildren().setAll(btnReveal, btnDelete); } - this.graphics = new RipplerContainer(root); - FXUtils.onClicked(graphics, () -> { - Item item = getItem(); - if (item != null) - item.onClick(); + box.getChildren().setAll(left, center, right); + StackPane.setMargin(box, new Insets(8)); + + FXUtils.onClicked(box, () -> { + var item = getItem(); + if (item != null) item.onClick(); }); + getContainer().getChildren().setAll(box); } @Override - protected void updateItem(Item item, boolean empty) { - super.updateItem(item, empty); + protected void updateControl(Item item, boolean empty) { + if (empty || item == null) return; iconImageView.setImage(null); - if (empty || item == null) { - setGraphic(null); - center.setTitle(""); - center.setSubtitle(""); - } else { - directoryProperty.set(item.isDirectory()); - if (item instanceof LitematicFileItem fileItem && fileItem.getImage() != null) { - iconImageView.setImage(fileItem.getImage()); - left.getChildren().setAll(iconImageView); - } else { - iconSVG.setContent(item.getIcon().getPath()); - left.getChildren().setAll(iconSVGWrapper); - } - - center.setTitle(item.getName()); - center.setSubtitle(item.getDescription()); + directoryProperty.set(item.isDirectory()); - Path path = item.getPath(); - if (path != null) { - tooltip.setText(FileUtils.getAbsolutePath(path)); - FXUtils.installSlowTooltip(left, tooltip); - } else { - tooltip.setText(""); - Tooltip.uninstall(left, tooltip); - } + if (item instanceof LitematicFileItem fileItem && fileItem.getImage() != null) { + iconImageView.setImage(fileItem.getImage()); + left.getChildren().setAll(iconImageView); + } else { + iconSVG.setContent(item.getIcon().getPath()); + left.getChildren().setAll(iconSVGWrapper); + } - root.setRight(item instanceof BackItem ? null : right); + center.setTitle(item.getName()); + center.setSubtitle(item.getDescription()); - setGraphic(graphics); + Path path = item.getPath(); + if (path != null) { + tooltip.setText(FileUtils.getAbsolutePath(path)); + FXUtils.installSlowTooltip(left, tooltip); + } else { + Tooltip.uninstall(left, tooltip); } + + right.setVisible(!(item instanceof BackItem)); } } - private final class SchematicsPageSkin extends ToolbarListPageSkin { - SchematicsPageSkin() { - super(SchematicsPage.this); - } + private static final class SchematicsPageSkin extends SkinBase { - @Override - protected List initializeToolbar(SchematicsPage skinnable) { - return Arrays.asList( - createToolbarButton2(i18n("button.refresh"), SVG.REFRESH, skinnable::refresh), - createToolbarButton2(i18n("schematics.add"), SVG.ADD, skinnable::onAddFiles), - createToolbarButton2(i18n("schematics.create_directory"), SVG.CREATE_NEW_FOLDER, skinnable::onCreateDirectory) - ); - } + private final HBox toolbar; - @Override - protected ListCell createListCell(JFXListView listView) { - return new Cell(); + private final JFXListView listView; + + SchematicsPageSkin(SchematicsPage skinnable) { + super(skinnable); + + StackPane pane = new StackPane(); + pane.setPadding(new Insets(10)); + pane.getStyleClass().addAll("notice-pane"); + + ComponentList root = new ComponentList(); + root.getStyleClass().add("no-padding"); + listView = new JFXListView<>(); + listView.setSelectionModel(new NoneMultipleSelectionModel<>()); + + { + toolbar = new HBox(); + toolbar.getChildren().setAll( + createToolbarButton2(i18n("button.refresh"), SVG.REFRESH, skinnable::refresh), + createToolbarButton2(i18n("schematics.add"), SVG.ADD, skinnable::onAddFiles), + createToolbarButton2(i18n("schematics.create_directory"), SVG.CREATE_NEW_FOLDER, skinnable::onCreateDirectory), + createToolbarButton2(i18n("button.reveal_dir"), SVG.FOLDER_OPEN, skinnable::onRevealSchematicsFolder) + ); + root.getContent().add(toolbar); + } + + { + SpinnerPane center = new SpinnerPane(); + ComponentList.setVgrow(center, Priority.ALWAYS); + center.getStyleClass().add("large-spinner-pane"); + center.loadingProperty().bind(skinnable.loadingProperty()); + + listView.setCellFactory(x -> new Cell(listView)); + listView.getSelectionModel().setSelectionMode(SelectionMode.MULTIPLE); + Bindings.bindContent(listView.getItems(), skinnable.getItems()); + + // ListViewBehavior would consume ESC pressed event, preventing us from handling it + // So we ignore it here + ignoreEvent(listView, KeyEvent.KEY_PRESSED, e -> e.getCode() == KeyCode.ESCAPE); + + center.setContent(listView); + root.getContent().add(center); + } + + pane.getChildren().setAll(root); + getChildren().setAll(pane); } } } From 3c2e3ba970374a8ba12066752bf43bcb9d31bb88 Mon Sep 17 00:00:00 2001 From: Calboot Date: Sat, 31 Jan 2026 21:16:56 +0800 Subject: [PATCH 03/53] update --- .../java/org/jackhuang/hmcl/ui/versions/SchematicsPage.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/SchematicsPage.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/SchematicsPage.java index 5904faa3a6..55dfee5f56 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/SchematicsPage.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/SchematicsPage.java @@ -367,7 +367,7 @@ void load() { if (this.loaded) return; try (Stream stream = Files.list(path)) { - if (!this.preLoaded) preLoad(); + preLoad(); for (Path p : Lang.toIterable(stream)) { if (Files.isDirectory(p)) { var child = new DirItem(p, this); @@ -423,7 +423,7 @@ private LitematicFileItem(LitematicFile file) { if (name != null && !"Unnamed".equals(name)) { this.name = name; } else { - this.name = FileUtils.getNameWithoutExtension(name); + this.name = FileUtils.getNameWithoutExtension(file.getFile()); } WritableImage image = null; From 27e137159daf83fe7a30ed28139ad5402c5245ff Mon Sep 17 00:00:00 2001 From: Calboot Date: Sat, 31 Jan 2026 22:39:36 +0800 Subject: [PATCH 04/53] Fix rippler --- .../org/jackhuang/hmcl/ui/construct/MDListCell.java | 7 ++++++- .../jackhuang/hmcl/ui/versions/SchematicsPage.java | 12 +++++------- 2 files changed, 11 insertions(+), 8 deletions(-) diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/construct/MDListCell.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/construct/MDListCell.java index 62fbf277a3..721d8946fd 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/construct/MDListCell.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/construct/MDListCell.java @@ -29,6 +29,7 @@ public abstract class MDListCell extends ListCell { private final PseudoClass SELECTED = PseudoClass.getPseudoClass("selected"); private final StackPane container = new StackPane(); + private final RipplerContainer ripplerContainer; private final StackPane root = new StackPane(); public MDListCell(JFXListView listView) { @@ -37,7 +38,7 @@ public MDListCell(JFXListView listView) { setGraphic(null); root.getStyleClass().add("md-list-cell"); - RipplerContainer ripplerContainer = new RipplerContainer(container); + this.ripplerContainer = new RipplerContainer(container); root.getChildren().setAll(ripplerContainer); Region clippedContainer = (Region) listView.lookup(".clipped-container"); @@ -73,5 +74,9 @@ protected void setSelectable() { }); } + protected void onClicked(Runnable action) { + FXUtils.onClicked(ripplerContainer, action); + } + protected abstract void updateControl(T item, boolean empty); } diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/SchematicsPage.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/SchematicsPage.java index 55dfee5f56..c8b47f8c26 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/SchematicsPage.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/SchematicsPage.java @@ -595,6 +595,7 @@ public Cell(JFXListView listView) { var box = new HBox(8); box.setAlignment(Pos.CENTER_LEFT); + box.setPickOnBounds(false); { this.left = new StackPane(); @@ -651,12 +652,11 @@ public Cell(JFXListView listView) { box.getChildren().setAll(left, center, right); StackPane.setMargin(box, new Insets(8)); - - FXUtils.onClicked(box, () -> { + getContainer().getChildren().setAll(box); + onClicked(() -> { var item = getItem(); if (item != null) item.onClick(); }); - getContainer().getChildren().setAll(box); } @Override @@ -692,8 +692,6 @@ protected void updateControl(Item item, boolean empty) { private static final class SchematicsPageSkin extends SkinBase { - private final HBox toolbar; - private final JFXListView listView; SchematicsPageSkin(SchematicsPage skinnable) { @@ -709,7 +707,7 @@ private static final class SchematicsPageSkin extends SkinBase { listView.setSelectionModel(new NoneMultipleSelectionModel<>()); { - toolbar = new HBox(); + var toolbar = new HBox(); toolbar.getChildren().setAll( createToolbarButton2(i18n("button.refresh"), SVG.REFRESH, skinnable::refresh), createToolbarButton2(i18n("schematics.add"), SVG.ADD, skinnable::onAddFiles), @@ -726,7 +724,7 @@ private static final class SchematicsPageSkin extends SkinBase { center.loadingProperty().bind(skinnable.loadingProperty()); listView.setCellFactory(x -> new Cell(listView)); - listView.getSelectionModel().setSelectionMode(SelectionMode.MULTIPLE); + listView.setSelectionModel(new NoneMultipleSelectionModel<>()); Bindings.bindContent(listView.getItems(), skinnable.getItems()); // ListViewBehavior would consume ESC pressed event, preventing us from handling it From cc4336625a5644008f40d381a9a389074a614168 Mon Sep 17 00:00:00 2001 From: Calboot Date: Sat, 31 Jan 2026 22:55:55 +0800 Subject: [PATCH 05/53] =?UTF-8?q?=E6=8E=A5=E5=85=A5=20NBTEditorPage?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../jackhuang/hmcl/ui/nbt/NBTFileType.java | 7 +++++++ .../hmcl/ui/versions/SchematicsPage.java | 19 ++++++++++++++++++- .../hmcl/schematic/LitematicFile.java | 9 +++++++-- 3 files changed, 32 insertions(+), 3 deletions(-) diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/nbt/NBTFileType.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/nbt/NBTFileType.java index ceab275040..c0975b99a5 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/nbt/NBTFileType.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/nbt/NBTFileType.java @@ -23,6 +23,7 @@ import com.github.steveice10.opennbt.tag.builtin.ListTag; import com.github.steveice10.opennbt.tag.builtin.Tag; import kala.compress.utils.BoundedInputStream; +import org.jackhuang.hmcl.schematic.LitematicFile; import org.jackhuang.hmcl.util.io.FileUtils; import java.io.BufferedInputStream; @@ -160,6 +161,12 @@ public NBTTreeView.Item readAsTree(Path file) throws IOException { return item; } + }, + SCHEMATIC("litematic") { + @Override + public Tag read(Path file) throws IOException { + return LitematicFile.readRoot(file); + } }; static final NBTFileType[] types = values(); diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/SchematicsPage.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/SchematicsPage.java index c8b47f8c26..8fd56daf2d 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/SchematicsPage.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/SchematicsPage.java @@ -46,6 +46,7 @@ import org.jackhuang.hmcl.task.Task; import org.jackhuang.hmcl.ui.*; import org.jackhuang.hmcl.ui.construct.*; +import org.jackhuang.hmcl.ui.nbt.NBTEditorPage; import org.jackhuang.hmcl.util.Lang; import org.jackhuang.hmcl.util.StringUtils; import org.jackhuang.hmcl.util.i18n.I18n; @@ -586,6 +587,7 @@ private static final class Cell extends MDListCell { private final SVGPath iconSVG; private final StackPane iconSVGWrapper; + private final BooleanProperty fileProperty = new SimpleBooleanProperty(this, "isFile", false); private final BooleanProperty directoryProperty = new SimpleBooleanProperty(this, "isDirectory", false); private final Tooltip tooltip = new Tooltip(); @@ -636,6 +638,20 @@ public Cell(JFXListView listView) { item.onReveal(); }); + JFXButton btnEdit = new JFXButton(); + btnEdit.getStyleClass().add("toggle-icon4"); + btnEdit.setGraphic(SVG.EDIT.createIcon()); + btnEdit.setOnAction(event -> { + Item item = getItem(); + if (item instanceof LitematicFileItem) { + try { + Controllers.navigate(new NBTEditorPage(item.getPath())); + } catch (IOException ignored) { + } + } + }); + btnEdit.visibleProperty().bind(fileProperty); + JFXButton btnDelete = new JFXButton(); btnDelete.getStyleClass().add("toggle-icon4"); btnDelete.setGraphic(SVG.DELETE_FOREVER.createIcon()); @@ -647,7 +663,7 @@ public Cell(JFXListView listView) { } }); - right.getChildren().setAll(btnReveal, btnDelete); + right.getChildren().setAll(btnEdit, btnReveal, btnDelete); } box.getChildren().setAll(left, center, right); @@ -665,6 +681,7 @@ protected void updateControl(Item item, boolean empty) { iconImageView.setImage(null); + fileProperty.set(item instanceof LitematicFileItem); directoryProperty.set(item.isDirectory()); if (item instanceof LitematicFileItem fileItem && fileItem.getImage() != null) { diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/schematic/LitematicFile.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/schematic/LitematicFile.java index 1de4f0318d..ba112436c2 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/schematic/LitematicFile.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/schematic/LitematicFile.java @@ -56,12 +56,17 @@ public static boolean isFileLitematic(Path path) { return "litematic".equals(FileUtils.getExtension(path)) && Files.isRegularFile(path); } - public static LitematicFile load(Path file) throws IOException { - + public static CompoundTag readRoot(Path file) throws IOException { CompoundTag root; try (InputStream in = new GZIPInputStream(Files.newInputStream(file))) { root = (CompoundTag) NBTIO.readTag(in); } + return root; + } + + public static LitematicFile load(Path file) throws IOException { + + CompoundTag root = readRoot(file); Tag versionTag = root.get("Version"); if (versionTag == null) From e8af74f1a74b3762c7fba679f8dca2c17a196761 Mon Sep 17 00:00:00 2001 From: Calboot Date: Sun, 1 Feb 2026 12:42:56 +0800 Subject: [PATCH 06/53] =?UTF-8?q?=E6=B7=BB=E5=8A=A0=E8=BF=94=E5=9B=9E?= =?UTF-8?q?=E6=8C=89=E9=92=AE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../hmcl/ui/versions/SchematicsPage.java | 127 ++++++++---------- .../resources/assets/lang/I18N.properties | 1 - .../resources/assets/lang/I18N_ar.properties | 1 - .../resources/assets/lang/I18N_es.properties | 1 - .../resources/assets/lang/I18N_lzh.properties | 1 - .../resources/assets/lang/I18N_ru.properties | 1 - .../resources/assets/lang/I18N_uk.properties | 1 - .../resources/assets/lang/I18N_zh.properties | 1 - .../assets/lang/I18N_zh_CN.properties | 1 - 9 files changed, 54 insertions(+), 81 deletions(-) diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/SchematicsPage.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/SchematicsPage.java index 8fd56daf2d..21f6d27156 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/SchematicsPage.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/SchematicsPage.java @@ -23,6 +23,8 @@ import javafx.beans.binding.Bindings; import javafx.beans.property.BooleanProperty; import javafx.beans.property.SimpleBooleanProperty; +import javafx.beans.property.SimpleStringProperty; +import javafx.beans.property.StringProperty; import javafx.geometry.Insets; import javafx.geometry.Pos; import javafx.scene.Group; @@ -60,6 +62,7 @@ import java.nio.file.NoSuchFileException; import java.nio.file.Path; import java.util.*; +import java.util.stream.Collectors; import java.util.stream.Stream; import static org.jackhuang.hmcl.ui.FXUtils.ignoreEvent; @@ -83,6 +86,9 @@ private static String translateAuthorName(String author) { private Path schematicsDirectory; private DirItem currentDirectory; + private final StringProperty relativePathProperty = new SimpleStringProperty(this, "relativePath", "schematics"); + private final BooleanProperty isRootProperty = new SimpleBooleanProperty(this, "isRoot", true); + public SchematicsPage() { FXUtils.applyDragListener(this, file -> currentDirectory != null && Files.isRegularFile(file) && FileUtils.getName(file).endsWith(".litematic"), @@ -102,6 +108,18 @@ public void loadVersion(Profile profile, String version) { refresh(); } + public BooleanProperty isRootProperty() { + return isRootProperty; + } + + public StringProperty relativePathProperty() { + return relativePathProperty; + } + + public void navigateBack() { + if (currentDirectory.parent != null) navigateTo(currentDirectory.parent); + } + public void refresh() { Path schematicsDirectory = this.schematicsDirectory; if (schematicsDirectory == null) return; @@ -114,9 +132,7 @@ public void refresh() { DirItem target = result; if (currentDirectory != null) { loop: - for (int i = 0; i < currentDirectory.relativePath.size(); i++) { - String dirName = currentDirectory.relativePath.get(i); - + for (String dirName : currentDirectory.relativePath) { for (Item child : target.children) { if (child instanceof DirItem && child.getName().equals(dirName)) { target = (DirItem) child; @@ -201,7 +217,7 @@ public void onCreateDirectory() { } public void onRevealSchematicsFolder() { - FXUtils.openFolder(schematicsDirectory); + FXUtils.openFolder(Objects.requireNonNullElse(currentDirectory.path, schematicsDirectory)); } private DirItem loadRoot(Path dir) { @@ -212,14 +228,18 @@ private DirItem loadRoot(Path dir) { private void navigateTo(DirItem item) { currentDirectory = item; + if (item.relativePath.isEmpty()) { + relativePathProperty().set("schematics"); + } else { + relativePathProperty().set(item.relativePath.stream().collect(Collectors.joining("/", "schematics/", ""))); + } + isRootProperty().set(currentDirectory.parent == null); setLoading(true); Task.runAsync(item::load).whenComplete(Schedulers.javafx(), exception -> { - getItems().clear(); - if (item.parent != null) { - getItems().add(new BackItem(item.parent)); + if (currentDirectory == item) { + getItems().setAll(item.children); + setLoading(false); } - getItems().addAll(item.children); - setLoading(false); }).start(); } @@ -262,55 +282,6 @@ public int compareTo(@NotNull SchematicsPage.Item o) { } } - private final class BackItem extends Item { - - private final DirItem parent; - - BackItem(DirItem parent) { - this.parent = parent; - } - - @Override - int order() { - return 0; - } - - @Override - Path getPath() { - return null; - } - - @Override - String getName() { - return ".."; - } - - @Override - String getDescription() { - return i18n("schematics.back_to", parent.getName()); - } - - @Override - SVG getIcon() { - return SVG.FOLDER; - } - - @Override - void onClick() { - navigateTo(parent); - } - - @Override - void onReveal() { - throw new UnsupportedOperationException("Unreachable"); - } - - @Override - void onDelete() { - throw new UnsupportedOperationException("Unreachable"); - } - } - private final class DirItem extends Item { final Path path; final @Nullable DirItem parent; @@ -334,7 +305,7 @@ private final class DirItem extends Item { @Override int order() { - return 1; + return 0; } @Override @@ -421,7 +392,7 @@ private LitematicFileItem(LitematicFile file) { this.file = file; String name = file.getName(); - if (name != null && !"Unnamed".equals(name)) { + if (StringUtils.isNotBlank(name) && !"Unnamed".equals(name)) { this.name = name; } else { this.name = FileUtils.getNameWithoutExtension(file.getFile()); @@ -448,7 +419,7 @@ private LitematicFileItem(LitematicFile file) { @Override int order() { - return 2; + return 1; } @Override @@ -587,8 +558,8 @@ private static final class Cell extends MDListCell { private final SVGPath iconSVG; private final StackPane iconSVGWrapper; - private final BooleanProperty fileProperty = new SimpleBooleanProperty(this, "isFile", false); - private final BooleanProperty directoryProperty = new SimpleBooleanProperty(this, "isDirectory", false); + private final BooleanProperty isFileProperty = new SimpleBooleanProperty(this, "isFile", false); + private final BooleanProperty isDirectoryProperty = new SimpleBooleanProperty(this, "isDirectory", false); private final Tooltip tooltip = new Tooltip(); @@ -630,12 +601,11 @@ public Cell(JFXListView listView) { { var fo = SVG.FOLDER_OPEN.createIcon(); var f = SVG.FOLDER.createIcon(); - btnReveal.graphicProperty().bind(directoryProperty.map(b -> b ? fo : f)); + btnReveal.graphicProperty().bind(isDirectoryProperty.map(b -> b ? fo : f)); } btnReveal.setOnAction(event -> { Item item = getItem(); - if (item != null && !(item instanceof BackItem)) - item.onReveal(); + if (item != null) item.onReveal(); }); JFXButton btnEdit = new JFXButton(); @@ -646,18 +616,18 @@ public Cell(JFXListView listView) { if (item instanceof LitematicFileItem) { try { Controllers.navigate(new NBTEditorPage(item.getPath())); - } catch (IOException ignored) { + } catch (IOException ignored) { // Should be impossible } } }); - btnEdit.visibleProperty().bind(fileProperty); + btnEdit.visibleProperty().bind(isFileProperty); JFXButton btnDelete = new JFXButton(); btnDelete.getStyleClass().add("toggle-icon4"); btnDelete.setGraphic(SVG.DELETE_FOREVER.createIcon()); btnDelete.setOnAction(event -> { Item item = getItem(); - if (item != null && !(item instanceof BackItem)) { + if (item != null) { Controllers.confirm(i18n("button.remove.confirm"), i18n("button.remove"), item::onDelete, null); } @@ -681,8 +651,8 @@ protected void updateControl(Item item, boolean empty) { iconImageView.setImage(null); - fileProperty.set(item instanceof LitematicFileItem); - directoryProperty.set(item.isDirectory()); + isFileProperty.set(item instanceof LitematicFileItem); + isDirectoryProperty.set(item.isDirectory()); if (item instanceof LitematicFileItem fileItem && fileItem.getImage() != null) { iconImageView.setImage(fileItem.getImage()); @@ -702,8 +672,6 @@ protected void updateControl(Item item, boolean empty) { } else { Tooltip.uninstall(left, tooltip); } - - right.setVisible(!(item instanceof BackItem)); } } @@ -725,7 +693,10 @@ private static final class SchematicsPageSkin extends SkinBase { { var toolbar = new HBox(); + JFXButton btnGoBack = createToolbarButton2("", SVG.ARROW_BACK, skinnable::navigateBack); + btnGoBack.disableProperty().bind(skinnable.isRootProperty()); toolbar.getChildren().setAll( + btnGoBack, createToolbarButton2(i18n("button.refresh"), SVG.REFRESH, skinnable::refresh), createToolbarButton2(i18n("schematics.add"), SVG.ADD, skinnable::onAddFiles), createToolbarButton2(i18n("schematics.create_directory"), SVG.CREATE_NEW_FOLDER, skinnable::onCreateDirectory), @@ -734,6 +705,16 @@ private static final class SchematicsPageSkin extends SkinBase { root.getContent().add(toolbar); } + { + var relPath = new Label(); + relPath.setStyle("-fx-font-size: 13"); + HBox.setMargin(relPath, new Insets(4, 0, 4, 5)); + relPath.textProperty().bind(skinnable.relativePathProperty()); + var relPathPane = new HBox(relPath); + relPathPane.setAlignment(Pos.CENTER_LEFT); + root.getContent().add(relPathPane); + } + { SpinnerPane center = new SpinnerPane(); ComponentList.setVgrow(center, Priority.ALWAYS); diff --git a/HMCL/src/main/resources/assets/lang/I18N.properties b/HMCL/src/main/resources/assets/lang/I18N.properties index 579fdfe5ea..4072491adf 100644 --- a/HMCL/src/main/resources/assets/lang/I18N.properties +++ b/HMCL/src/main/resources/assets/lang/I18N.properties @@ -1261,7 +1261,6 @@ reveal.in_file_manager=Reveal in File Manager schematics=Schematics schematics.add=Add Schematic Files schematics.add.failed=Failed to add schematic files -schematics.back_to=Back to "%s" schematics.create_directory=Create Directory schematics.create_directory.prompt=Please enter the new directory name schematics.create_directory.failed=Failed to create directory diff --git a/HMCL/src/main/resources/assets/lang/I18N_ar.properties b/HMCL/src/main/resources/assets/lang/I18N_ar.properties index cac28f1758..a67cf979a0 100644 --- a/HMCL/src/main/resources/assets/lang/I18N_ar.properties +++ b/HMCL/src/main/resources/assets/lang/I18N_ar.properties @@ -1193,7 +1193,6 @@ reveal.in_file_manager=إظهار في مدير الملفات schematics=المخططات schematics.add=إضافة ملفات مخططات schematics.add.failed=فشل إضافة ملفات المخططات -schematics.back_to=العودة إلى "%s" schematics.create_directory=إنشاء دليل schematics.create_directory.prompt=يرجى إدخال اسم الدليل الجديد schematics.create_directory.failed=فشل إنشاء الدليل diff --git a/HMCL/src/main/resources/assets/lang/I18N_es.properties b/HMCL/src/main/resources/assets/lang/I18N_es.properties index 3caa0fca52..73e30296ca 100644 --- a/HMCL/src/main/resources/assets/lang/I18N_es.properties +++ b/HMCL/src/main/resources/assets/lang/I18N_es.properties @@ -1202,7 +1202,6 @@ reveal.in_file_manager=Mostrar en el administrador de archivos schematics=Esquemas schematics.add=Añadir archivos de esquema schematics.add.failed=No se han podido añadir archivos de esquema -schematics.back_to=Atrás a «%s» schematics.create_directory=Crear directorio schematics.create_directory.prompt=Introduzca el nuevo nombre del directorio schematics.create_directory.failed=No se ha podido crear el directorio diff --git a/HMCL/src/main/resources/assets/lang/I18N_lzh.properties b/HMCL/src/main/resources/assets/lang/I18N_lzh.properties index 726347fdde..ba88a10386 100644 --- a/HMCL/src/main/resources/assets/lang/I18N_lzh.properties +++ b/HMCL/src/main/resources/assets/lang/I18N_lzh.properties @@ -986,7 +986,6 @@ reveal.in_file_manager=覽於理案臺 schematics=蜃圖 schematics.add=增蜃圖 schematics.add.failed=增蜃圖未成 -schematics.back_to=退至「%s」 schematics.create_directory=增案夾 schematics.create_directory.prompt=書新案夾之名 schematics.create_directory.failed=增案夾未成 diff --git a/HMCL/src/main/resources/assets/lang/I18N_ru.properties b/HMCL/src/main/resources/assets/lang/I18N_ru.properties index bfac684d4d..14faafe175 100644 --- a/HMCL/src/main/resources/assets/lang/I18N_ru.properties +++ b/HMCL/src/main/resources/assets/lang/I18N_ru.properties @@ -1194,7 +1194,6 @@ reveal.in_file_manager=Открыть в файловый менеджер schematics=Схемы schematics.add=Добавить файлы схем schematics.add.failed=Не удалось добавить файлы схем -schematics.back_to=Назад на «%s» schematics.create_directory=Создать папку schematics.create_directory.prompt=Введите новое имя папки schematics.create_directory.failed=Не удалось создать папку diff --git a/HMCL/src/main/resources/assets/lang/I18N_uk.properties b/HMCL/src/main/resources/assets/lang/I18N_uk.properties index 9e9a9aeb74..b129005276 100644 --- a/HMCL/src/main/resources/assets/lang/I18N_uk.properties +++ b/HMCL/src/main/resources/assets/lang/I18N_uk.properties @@ -1140,7 +1140,6 @@ reveal.in_file_manager=Показати в менеджері файлів schematics=Схематики schematics.add=Додати файли схематик schematics.add.failed=Не вдалося додати файли схематик -schematics.back_to=Назад до "%s" schematics.create_directory=Створити каталог schematics.create_directory.prompt=Введіть нову назву каталогу schematics.create_directory.failed=Не вдалося створити каталог diff --git a/HMCL/src/main/resources/assets/lang/I18N_zh.properties b/HMCL/src/main/resources/assets/lang/I18N_zh.properties index c1166f1b4d..715836268d 100644 --- a/HMCL/src/main/resources/assets/lang/I18N_zh.properties +++ b/HMCL/src/main/resources/assets/lang/I18N_zh.properties @@ -1046,7 +1046,6 @@ reveal.in_file_manager=在檔案管理員中查看 schematics=原理圖 schematics.add=添加原理圖 schematics.add.failed=添加原理圖失敗 -schematics.back_to=返回到「%s」 schematics.create_directory=建立目錄 schematics.create_directory.prompt=請輸入新目錄名稱 schematics.create_directory.failed=建立目錄失敗 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 d47299447f..68df6265e4 100644 --- a/HMCL/src/main/resources/assets/lang/I18N_zh_CN.properties +++ b/HMCL/src/main/resources/assets/lang/I18N_zh_CN.properties @@ -1051,7 +1051,6 @@ reveal.in_file_manager=在文件管理器中查看 schematics=原理图 schematics.add=添加原理图 schematics.add.failed=添加原理图失败 -schematics.back_to=返回到“%s” schematics.create_directory=创建文件夹 schematics.create_directory.prompt=请输入新文件夹名称 schematics.create_directory.failed=创建文件夹失败 From 95f20c359980adc2aa890bdb4e337ad41491d841 Mon Sep 17 00:00:00 2001 From: Calboot Date: Sun, 1 Feb 2026 12:56:26 +0800 Subject: [PATCH 07/53] update --- .../hmcl/ui/versions/SchematicsPage.java | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/SchematicsPage.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/SchematicsPage.java index 21f6d27156..ed3b12d7f8 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/SchematicsPage.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/SchematicsPage.java @@ -347,7 +347,7 @@ void load() { this.children.add(child); } else if (LitematicFile.isFileLitematic(p)) { try { - this.children.add(new LitematicFileItem(LitematicFile.load(p))); + this.children.add(new LitematicFileItem(p)); } catch (IOException e) { LOG.warning("Failed to load litematic file: " + path, e); } @@ -384,18 +384,20 @@ void onDelete() { } private final class LitematicFileItem extends Item { + final Path path; final LitematicFile file; final String name; final Image image; - private LitematicFileItem(LitematicFile file) { - this.file = file; + private LitematicFileItem(Path path) throws IOException { + this.path = path; + this.file = LitematicFile.load(path); String name = file.getName(); if (StringUtils.isNotBlank(name) && !"Unnamed".equals(name)) { this.name = name; } else { - this.name = FileUtils.getNameWithoutExtension(file.getFile()); + this.name = FileUtils.getNameWithoutExtension(path); } WritableImage image = null; @@ -424,7 +426,7 @@ int order() { @Override Path getPath() { - return file.getFile(); + return path; } @Override @@ -434,7 +436,7 @@ public String getName() { @Override String getDescription() { - return file.getFile().getFileName().toString(); + return path.getFileName().toString(); } @Override @@ -519,7 +521,7 @@ private void updateContent(LitematicFile file) { TwoLineListItem title = new TwoLineListItem(); title.setTitle(getName()); - title.setSubtitle(file.getFile().getFileName().toString()); + title.setSubtitle(path.getFileName().toString()); titleBox.getChildren().setAll(icon, title); setHeading(titleBox); From d7d7f014a5be4046c56291e82d36262e6e346530 Mon Sep 17 00:00:00 2001 From: Calboot Date: Sun, 1 Feb 2026 17:27:32 +0800 Subject: [PATCH 08/53] update --- .../hmcl/ui/versions/SchematicsPage.java | 79 ++++++++++--------- 1 file changed, 43 insertions(+), 36 deletions(-) diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/SchematicsPage.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/SchematicsPage.java index ed3b12d7f8..be7f750494 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/SchematicsPage.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/SchematicsPage.java @@ -22,9 +22,9 @@ import com.jfoenix.controls.JFXListView; import javafx.beans.binding.Bindings; import javafx.beans.property.BooleanProperty; +import javafx.beans.property.ObjectProperty; import javafx.beans.property.SimpleBooleanProperty; -import javafx.beans.property.SimpleStringProperty; -import javafx.beans.property.StringProperty; +import javafx.beans.property.SimpleObjectProperty; import javafx.geometry.Insets; import javafx.geometry.Pos; import javafx.scene.Group; @@ -41,6 +41,8 @@ import javafx.scene.layout.Priority; import javafx.scene.layout.StackPane; import javafx.scene.shape.SVGPath; +import javafx.scene.text.Text; +import javafx.scene.text.TextFlow; import javafx.stage.FileChooser; import org.jackhuang.hmcl.schematic.LitematicFile; import org.jackhuang.hmcl.setting.Profile; @@ -62,7 +64,6 @@ import java.nio.file.NoSuchFileException; import java.nio.file.Path; import java.util.*; -import java.util.stream.Collectors; import java.util.stream.Stream; import static org.jackhuang.hmcl.ui.FXUtils.ignoreEvent; @@ -84,14 +85,13 @@ private static String translateAuthorName(String author) { } private Path schematicsDirectory; - private DirItem currentDirectory; + private final ObjectProperty currentDirectory = new SimpleObjectProperty<>(this, "currentDirectory", null); - private final StringProperty relativePathProperty = new SimpleStringProperty(this, "relativePath", "schematics"); private final BooleanProperty isRootProperty = new SimpleBooleanProperty(this, "isRoot", true); public SchematicsPage() { FXUtils.applyDragListener(this, - file -> currentDirectory != null && Files.isRegularFile(file) && FileUtils.getName(file).endsWith(".litematic"), + file -> currentDirectoryProperty().get() != null && Files.isRegularFile(file) && FileUtils.getName(file).endsWith(".litematic"), this::addFiles ); } @@ -108,16 +108,17 @@ public void loadVersion(Profile profile, String version) { refresh(); } - public BooleanProperty isRootProperty() { - return isRootProperty; + private ObjectProperty currentDirectoryProperty() { + return currentDirectory; } - public StringProperty relativePathProperty() { - return relativePathProperty; + public BooleanProperty isRootProperty() { + return isRootProperty; } public void navigateBack() { - if (currentDirectory.parent != null) navigateTo(currentDirectory.parent); + var p = currentDirectoryProperty().get().parent; + if (p != null) navigateTo(p); } public void refresh() { @@ -130,9 +131,9 @@ public void refresh() { setLoading(false); if (exception == null) { DirItem target = result; - if (currentDirectory != null) { + if (currentDirectoryProperty().get() != null) { loop: - for (String dirName : currentDirectory.relativePath) { + for (String dirName : currentDirectoryProperty().get().relativePath) { for (Item child : target.children) { if (child instanceof DirItem && child.getName().equals(dirName)) { target = (DirItem) child; @@ -151,10 +152,10 @@ public void refresh() { } public void addFiles(List files) { - if (currentDirectory == null) + if (currentDirectoryProperty().get() == null) return; - Path dir = currentDirectory.path; + Path dir = currentDirectoryProperty().get().path; try { // Can be executed in the background, but be careful that users can call loadVersion during this time Files.createDirectories(dir); @@ -181,10 +182,10 @@ public void onAddFiles() { } public void onCreateDirectory() { - if (currentDirectory == null) + if (currentDirectoryProperty().get() == null) return; - Path parent = currentDirectory.path; + Path parent = currentDirectoryProperty().get().path; Controllers.dialog(new InputDialogPane( i18n("schematics.create_directory.prompt"), "", @@ -217,7 +218,7 @@ public void onCreateDirectory() { } public void onRevealSchematicsFolder() { - FXUtils.openFolder(Objects.requireNonNullElse(currentDirectory.path, schematicsDirectory)); + FXUtils.openFolder(Objects.requireNonNullElse(currentDirectoryProperty().get().path, schematicsDirectory)); } private DirItem loadRoot(Path dir) { @@ -227,16 +228,12 @@ private DirItem loadRoot(Path dir) { } private void navigateTo(DirItem item) { - currentDirectory = item; - if (item.relativePath.isEmpty()) { - relativePathProperty().set("schematics"); - } else { - relativePathProperty().set(item.relativePath.stream().collect(Collectors.joining("/", "schematics/", ""))); - } - isRootProperty().set(currentDirectory.parent == null); + if (currentDirectoryProperty().get() == item) return; + currentDirectoryProperty().set(item); + isRootProperty().set(item.parent == null); setLoading(true); Task.runAsync(item::load).whenComplete(Schedulers.javafx(), exception -> { - if (currentDirectory == item) { + if (currentDirectoryProperty().get() == item) { getItems().setAll(item.children); setLoading(false); } @@ -707,16 +704,6 @@ private static final class SchematicsPageSkin extends SkinBase { root.getContent().add(toolbar); } - { - var relPath = new Label(); - relPath.setStyle("-fx-font-size: 13"); - HBox.setMargin(relPath, new Insets(4, 0, 4, 5)); - relPath.textProperty().bind(skinnable.relativePathProperty()); - var relPathPane = new HBox(relPath); - relPathPane.setAlignment(Pos.CENTER_LEFT); - root.getContent().add(relPathPane); - } - { SpinnerPane center = new SpinnerPane(); ComponentList.setVgrow(center, Priority.ALWAYS); @@ -735,6 +722,26 @@ private static final class SchematicsPageSkin extends SkinBase { root.getContent().add(center); } + { + var relPath = new TextFlow(); + relPath.setStyle("-fx-font-size: 13"); + HBox.setMargin(relPath, new Insets(5)); + skinnable.currentDirectoryProperty().addListener((__, ___, newValue) -> { + relPath.getChildren().clear(); + var d = newValue; + while (d != null) { + var txt = new Text(d.getName() + "/"); + var finalD = d; + FXUtils.onClicked(txt, () -> skinnable.navigateTo(finalD)); + relPath.getChildren().add(0, txt); + d = d.parent; + } + }); + var relPathPane = new HBox(relPath); + relPathPane.setAlignment(Pos.CENTER_LEFT); + root.getContent().add(relPathPane); + } + pane.getChildren().setAll(root); getChildren().setAll(pane); } From cdcb8ac8026f36a2a073fc8eaf031d2b2107ae4b Mon Sep 17 00:00:00 2001 From: Calboot Date: Sun, 1 Feb 2026 17:38:03 +0800 Subject: [PATCH 09/53] update --- .../java/org/jackhuang/hmcl/ui/versions/SchematicsPage.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/SchematicsPage.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/SchematicsPage.java index be7f750494..9922b6013e 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/SchematicsPage.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/SchematicsPage.java @@ -730,7 +730,8 @@ private static final class SchematicsPageSkin extends SkinBase { relPath.getChildren().clear(); var d = newValue; while (d != null) { - var txt = new Text(d.getName() + "/"); + relPath.getChildren().add(0, new Text("/")); + var txt = new Text(d.getName()); var finalD = d; FXUtils.onClicked(txt, () -> skinnable.navigateTo(finalD)); relPath.getChildren().add(0, txt); From 485d6ece8446842e2f9a2594bd5b54ddfea939b2 Mon Sep 17 00:00:00 2001 From: Calboot Date: Sun, 1 Feb 2026 18:26:48 +0800 Subject: [PATCH 10/53] fix refresh --- .../hmcl/ui/versions/SchematicsPage.java | 57 ++++++++++++------- 1 file changed, 36 insertions(+), 21 deletions(-) diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/SchematicsPage.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/SchematicsPage.java index 9922b6013e..1f0a29def3 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/SchematicsPage.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/SchematicsPage.java @@ -29,7 +29,10 @@ import javafx.geometry.Pos; import javafx.scene.Group; import javafx.scene.Node; -import javafx.scene.control.*; +import javafx.scene.control.Label; +import javafx.scene.control.Skin; +import javafx.scene.control.SkinBase; +import javafx.scene.control.Tooltip; import javafx.scene.image.Image; import javafx.scene.image.ImageView; import javafx.scene.image.PixelWriter; @@ -48,7 +51,10 @@ import org.jackhuang.hmcl.setting.Profile; import org.jackhuang.hmcl.task.Schedulers; import org.jackhuang.hmcl.task.Task; -import org.jackhuang.hmcl.ui.*; +import org.jackhuang.hmcl.ui.Controllers; +import org.jackhuang.hmcl.ui.FXUtils; +import org.jackhuang.hmcl.ui.ListPageBase; +import org.jackhuang.hmcl.ui.SVG; import org.jackhuang.hmcl.ui.construct.*; import org.jackhuang.hmcl.ui.nbt.NBTEditorPage; import org.jackhuang.hmcl.util.Lang; @@ -134,9 +140,10 @@ public void refresh() { if (currentDirectoryProperty().get() != null) { loop: for (String dirName : currentDirectoryProperty().get().relativePath) { - for (Item child : target.children) { - if (child instanceof DirItem && child.getName().equals(dirName)) { - target = (DirItem) child; + target.preLoad(); + for (var dirChild : target.dirChildren) { + if (dirChild.getName().equals(dirName)) { + target = dirChild; continue loop; } } @@ -182,10 +189,9 @@ public void onAddFiles() { } public void onCreateDirectory() { - if (currentDirectoryProperty().get() == null) - return; + if (currentDirectoryProperty().get() == null) return; - Path parent = currentDirectoryProperty().get().path; + Path parent = currentDirectoryProperty().get().getPath(); Controllers.dialog(new InputDialogPane( i18n("schematics.create_directory.prompt"), "", @@ -283,6 +289,7 @@ private final class DirItem extends Item { final Path path; final @Nullable DirItem parent; final List children = new ArrayList<>(); + final List dirChildren = new ArrayList<>(); final List relativePath; int size = 0; boolean preLoaded = false; @@ -328,8 +335,17 @@ SVG getIcon() { void preLoad() throws IOException { if (this.preLoaded) return; try (Stream stream = Files.list(path)) { - this.size = (int) stream.filter(p -> Files.isDirectory(p) || LitematicFile.isFileLitematic(p)).count(); + stream.forEach(p -> { + boolean b1 = Files.isDirectory(p); + boolean b2 = LitematicFile.isFileLitematic(p); + if (b1 || b2) this.size++; + if (b1) { + var child = new DirItem(p, this); + this.dirChildren.add(child); + } + }); } + this.preLoaded = true; } void load() { @@ -337,19 +353,18 @@ void load() { try (Stream stream = Files.list(path)) { preLoad(); - for (Path p : Lang.toIterable(stream)) { - if (Files.isDirectory(p)) { - var child = new DirItem(p, this); - child.preLoad(); - this.children.add(child); - } else if (LitematicFile.isFileLitematic(p)) { - try { - this.children.add(new LitematicFileItem(p)); - } catch (IOException e) { - LOG.warning("Failed to load litematic file: " + path, e); - } - } + for (var dir : dirChildren) { + dir.preLoad(); + this.children.add(dir); } + stream.filter(LitematicFile::isFileLitematic) + .forEach(p -> { + try { + this.children.add(new LitematicFileItem(p)); + } catch (IOException e) { + LOG.warning("Failed to load litematic file: " + path, e); + } + }); } catch (NoSuchFileException ignored) { } catch (IOException e) { LOG.warning("Failed to load schematics in " + path, e); From aecd81dab9926270c151b8341db0eb1c98632665 Mon Sep 17 00:00:00 2001 From: Calboot Date: Sun, 1 Feb 2026 18:31:31 +0800 Subject: [PATCH 11/53] update --- .../main/java/org/jackhuang/hmcl/ui/versions/SchematicsPage.java | 1 - 1 file changed, 1 deletion(-) diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/SchematicsPage.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/SchematicsPage.java index 1f0a29def3..d957c52678 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/SchematicsPage.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/SchematicsPage.java @@ -57,7 +57,6 @@ import org.jackhuang.hmcl.ui.SVG; import org.jackhuang.hmcl.ui.construct.*; import org.jackhuang.hmcl.ui.nbt.NBTEditorPage; -import org.jackhuang.hmcl.util.Lang; import org.jackhuang.hmcl.util.StringUtils; import org.jackhuang.hmcl.util.i18n.I18n; import org.jackhuang.hmcl.util.io.FileUtils; From 512641a455baeb7fcc6132341326829c763b5fca Mon Sep 17 00:00:00 2001 From: Calboot Date: Sun, 1 Feb 2026 19:46:45 +0800 Subject: [PATCH 12/53] Resolves #4052 --- .../hmcl/ui/versions/SchematicsPage.java | 4 ++-- .../hmcl/game/DefaultGameRepository.java | 21 ++++++++++++++++++- .../hmcl/schematic/LitematicaConfig.java | 8 +++++++ 3 files changed, 30 insertions(+), 3 deletions(-) create mode 100644 HMCLCore/src/main/java/org/jackhuang/hmcl/schematic/LitematicaConfig.java diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/SchematicsPage.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/SchematicsPage.java index d957c52678..f515a7a567 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/SchematicsPage.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/SchematicsPage.java @@ -161,7 +161,7 @@ public void addFiles(List files) { if (currentDirectoryProperty().get() == null) return; - Path dir = currentDirectoryProperty().get().path; + Path dir = currentDirectoryProperty().get().getPath(); try { // Can be executed in the background, but be careful that users can call loadVersion during this time Files.createDirectories(dir); @@ -223,7 +223,7 @@ public void onCreateDirectory() { } public void onRevealSchematicsFolder() { - FXUtils.openFolder(Objects.requireNonNullElse(currentDirectoryProperty().get().path, schematicsDirectory)); + FXUtils.openFolder(Objects.requireNonNullElse(currentDirectoryProperty().get().getPath(), schematicsDirectory)); } private DirItem loadRoot(Path dir) { diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/game/DefaultGameRepository.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/game/DefaultGameRepository.java index 42d5fcafec..7d4ec4657d 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/game/DefaultGameRepository.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/game/DefaultGameRepository.java @@ -24,8 +24,10 @@ import org.jackhuang.hmcl.game.tlauncher.TLauncherVersion; import org.jackhuang.hmcl.mod.ModManager; import org.jackhuang.hmcl.mod.ModpackConfiguration; +import org.jackhuang.hmcl.schematic.LitematicaConfig; import org.jackhuang.hmcl.task.Task; import org.jackhuang.hmcl.util.Lang; +import org.jackhuang.hmcl.util.StringUtils; import org.jackhuang.hmcl.util.ToStringBuilder; import org.jackhuang.hmcl.util.gson.JsonUtils; import org.jackhuang.hmcl.util.io.FileUtils; @@ -554,7 +556,24 @@ public Path getBackupsDirectory(String id) { } public Path getSchematicsDirectory(String id) { - return getRunDirectory(id).resolve("schematics"); + var runDir = getRunDirectory(id); + var config = runDir.resolve("config").resolve("litematica.json"); + Path dir = runDir.resolve("schematics"); + if (Files.isRegularFile(config)) { + try { + var conf = JsonUtils.fromJsonFile(config, LitematicaConfig.class); + if (conf != null + && conf.generic() != null + && conf.generic().customSchematicBaseDirectoryEnabled() + && StringUtils.isNotBlank(conf.generic().customSchematicBaseDirectory()) + ) { + return Path.of(conf.generic().customSchematicBaseDirectory()); + } + } catch (Exception e) { + LOG.warning("Failed to read litematica config at '%s'".formatted(config), e); + } + } + return dir; } @Override diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/schematic/LitematicaConfig.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/schematic/LitematicaConfig.java new file mode 100644 index 0000000000..cdc0b294b4 --- /dev/null +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/schematic/LitematicaConfig.java @@ -0,0 +1,8 @@ +package org.jackhuang.hmcl.schematic; + +import com.google.gson.annotations.SerializedName; + +public record LitematicaConfig(@SerializedName("Generic") Generic generic) { + public record Generic(boolean customSchematicBaseDirectoryEnabled, String customSchematicBaseDirectory) { + } +} From 145d4394ac1e82996e28ad2d4fad3412fdb72d1d Mon Sep 17 00:00:00 2001 From: Calboot Date: Sun, 1 Feb 2026 20:05:00 +0800 Subject: [PATCH 13/53] update --- .../java/org/jackhuang/hmcl/game/DefaultGameRepository.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/game/DefaultGameRepository.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/game/DefaultGameRepository.java index 7d4ec4657d..ff7a5f0060 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/game/DefaultGameRepository.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/game/DefaultGameRepository.java @@ -567,7 +567,8 @@ public Path getSchematicsDirectory(String id) { && conf.generic().customSchematicBaseDirectoryEnabled() && StringUtils.isNotBlank(conf.generic().customSchematicBaseDirectory()) ) { - return Path.of(conf.generic().customSchematicBaseDirectory()); + // The given path is used if it's absolute. Otherwise, resolves it against the game directory + dir = runDir.resolve(conf.generic().customSchematicBaseDirectory()); } } catch (Exception e) { LOG.warning("Failed to read litematica config at '%s'".formatted(config), e); From 959c94dd382adffd6130a96032864453c9dba73d Mon Sep 17 00:00:00 2001 From: Calboot Date: Sun, 1 Feb 2026 22:04:15 +0800 Subject: [PATCH 14/53] =?UTF-8?q?Resolves=20#3957=20(=E9=9C=80=E8=A6=81?= =?UTF-8?q?=E8=BF=9B=E4=B8=80=E6=AD=A5=E6=B5=8B=E8=AF=95)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../jackhuang/hmcl/ui/nbt/NBTFileType.java | 9 +- .../hmcl/ui/versions/SchematicsPage.java | 80 ++++++++------ .../resources/assets/lang/I18N.properties | 1 + .../resources/assets/lang/I18N_zh.properties | 1 + .../assets/lang/I18N_zh_CN.properties | 1 + .../hmcl/schematic/LitematicFile.java | 56 ++++------ .../hmcl/schematic/NBTStructureFile.java | 83 +++++++++++++++ .../jackhuang/hmcl/schematic/Schematic.java | 100 ++++++++++++++++++ .../hmcl/schematic/SchematicFile.java | 78 ++++++++++++++ .../java/org/jackhuang/hmcl/util/Vec3i.java | 7 ++ 10 files changed, 340 insertions(+), 76 deletions(-) create mode 100644 HMCLCore/src/main/java/org/jackhuang/hmcl/schematic/NBTStructureFile.java create mode 100644 HMCLCore/src/main/java/org/jackhuang/hmcl/schematic/Schematic.java create mode 100644 HMCLCore/src/main/java/org/jackhuang/hmcl/schematic/SchematicFile.java create mode 100644 HMCLCore/src/main/java/org/jackhuang/hmcl/util/Vec3i.java diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/nbt/NBTFileType.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/nbt/NBTFileType.java index c0975b99a5..6d9eb02ceb 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/nbt/NBTFileType.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/nbt/NBTFileType.java @@ -23,7 +23,6 @@ import com.github.steveice10.opennbt.tag.builtin.ListTag; import com.github.steveice10.opennbt.tag.builtin.Tag; import kala.compress.utils.BoundedInputStream; -import org.jackhuang.hmcl.schematic.LitematicFile; import org.jackhuang.hmcl.util.io.FileUtils; import java.io.BufferedInputStream; @@ -42,7 +41,7 @@ * @author Glavo */ public enum NBTFileType { - COMPRESSED("dat", "dat_old") { + COMPRESSED("dat", "dat_old", "litematic", "nbt", "schematic", "schem") { @Override public Tag read(Path file) throws IOException { try (BufferedInputStream fileInputStream = new BufferedInputStream(Files.newInputStream(file))) { @@ -161,12 +160,6 @@ public NBTTreeView.Item readAsTree(Path file) throws IOException { return item; } - }, - SCHEMATIC("litematic") { - @Override - public Tag read(Path file) throws IOException { - return LitematicFile.readRoot(file); - } }; static final NBTFileType[] types = values(); diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/SchematicsPage.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/SchematicsPage.java index f515a7a567..469e8cf585 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/SchematicsPage.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/SchematicsPage.java @@ -48,6 +48,7 @@ import javafx.scene.text.TextFlow; import javafx.stage.FileChooser; import org.jackhuang.hmcl.schematic.LitematicFile; +import org.jackhuang.hmcl.schematic.Schematic; import org.jackhuang.hmcl.setting.Profile; import org.jackhuang.hmcl.task.Schedulers; import org.jackhuang.hmcl.task.Task; @@ -96,7 +97,7 @@ private static String translateAuthorName(String author) { public SchematicsPage() { FXUtils.applyDragListener(this, - file -> currentDirectoryProperty().get() != null && Files.isRegularFile(file) && FileUtils.getName(file).endsWith(".litematic"), + file -> currentDirectoryProperty().get() != null && Schematic.isFileSchematicAlike(file), this::addFiles ); } @@ -336,7 +337,7 @@ void preLoad() throws IOException { try (Stream stream = Files.list(path)) { stream.forEach(p -> { boolean b1 = Files.isDirectory(p); - boolean b2 = LitematicFile.isFileLitematic(p); + boolean b2 = Schematic.isFileSchematicAlike(p); if (b1 || b2) this.size++; if (b1) { var child = new DirItem(p, this); @@ -356,10 +357,10 @@ void load() { dir.preLoad(); this.children.add(dir); } - stream.filter(LitematicFile::isFileLitematic) + stream.filter(Schematic::isFileSchematicAlike) .forEach(p -> { try { - this.children.add(new LitematicFileItem(p)); + this.children.add(new SchematicItem(p)); } catch (IOException e) { LOG.warning("Failed to load litematic file: " + path, e); } @@ -394,37 +395,43 @@ void onDelete() { } } - private final class LitematicFileItem extends Item { + private final class SchematicItem extends Item { final Path path; - final LitematicFile file; + final Schematic file; final String name; final Image image; - private LitematicFileItem(Path path) throws IOException { + private SchematicItem(Path path) throws IOException { this.path = path; - this.file = LitematicFile.load(path); - - String name = file.getName(); - if (StringUtils.isNotBlank(name) && !"Unnamed".equals(name)) { - this.name = name; + this.file = Schematic.load(path); + + if (this.file instanceof LitematicFile lFile) { + String name = lFile.getName(); + if (StringUtils.isNotBlank(name) && !"Unnamed".equals(name)) { + this.name = name; + } else { + this.name = FileUtils.getNameWithoutExtension(path); + } } else { this.name = FileUtils.getNameWithoutExtension(path); } WritableImage image = null; - int[] previewImageData = file.getPreviewImageData(); - if (previewImageData != null && previewImageData.length > 0) { - int size = (int) Math.sqrt(previewImageData.length); - if ((size * size) == previewImageData.length) { - image = new WritableImage(size, size); - PixelWriter pixelWriter = image.getPixelWriter(); - - for (int y = 0, i = 0; y < size; ++y) { - for (int x = 0; x < size; ++x) { - pixelWriter.setArgb(x, y, previewImageData[i++]); + if (this.file instanceof LitematicFile lFile) { + int[] previewImageData = lFile.getPreviewImageData(); + if (previewImageData != null && previewImageData.length > 0) { + int size = (int) Math.sqrt(previewImageData.length); + if ((size * size) == previewImageData.length) { + image = new WritableImage(size, size); + PixelWriter pixelWriter = image.getPixelWriter(); + + for (int y = 0, i = 0; y < size; ++y) { + for (int x = 0; x < size; ++x) { + pixelWriter.setArgb(x, y, previewImageData[i++]); + } } - } + } } } this.image = image; @@ -473,7 +480,7 @@ Node getIcon(int size) { @Override void onClick() { - Controllers.dialog(new LitematicInfoDialog()); + Controllers.dialog(new SchematicInfoDialog()); } @Override @@ -491,7 +498,7 @@ void onDelete() { } } - private final class LitematicInfoDialog extends JFXDialogLayout { + private final class SchematicInfoDialog extends JFXDialogLayout { private final ComponentList details; private void addDetailItem(String key, Object detail) { @@ -501,7 +508,7 @@ private void addDetailItem(String key, Object detail) { details.getContent().add(borderPane); } - private void updateContent(LitematicFile file) { + private void updateContent(Schematic file) { details.getContent().clear(); addDetailItem(i18n("schematics.info.name"), file.getName()); if (StringUtils.isNotBlank(file.getAuthor())) @@ -518,14 +525,17 @@ private void updateContent(LitematicFile file) { addDetailItem(i18n("schematics.info.total_blocks"), file.getTotalBlocks()); if (file.getEnclosingSize() != null) addDetailItem(i18n("schematics.info.enclosing_size"), - String.format("%d x %d x %d", (int) file.getEnclosingSize().getX(), - (int) file.getEnclosingSize().getY(), - (int) file.getEnclosingSize().getZ())); - - addDetailItem(i18n("schematics.info.version"), file.getVersion()); + String.format("%d x %d x %d", file.getEnclosingSize().x(), + file.getEnclosingSize().y(), + file.getEnclosingSize().z())); + if (StringUtils.isNotBlank(file.getMinecraftVersion())) + addDetailItem(i18n("schematics.info.mc_version"), file.getMinecraftVersion()); + + if (file instanceof LitematicFile lFile) + addDetailItem(i18n("schematics.info.version"), lFile.getVersion()); } - LitematicInfoDialog() { + SchematicInfoDialog() { HBox titleBox = new HBox(8); { Node icon = getIcon(40); @@ -626,7 +636,7 @@ public Cell(JFXListView listView) { btnEdit.setGraphic(SVG.EDIT.createIcon()); btnEdit.setOnAction(event -> { Item item = getItem(); - if (item instanceof LitematicFileItem) { + if (item instanceof SchematicItem) { try { Controllers.navigate(new NBTEditorPage(item.getPath())); } catch (IOException ignored) { // Should be impossible @@ -664,10 +674,10 @@ protected void updateControl(Item item, boolean empty) { iconImageView.setImage(null); - isFileProperty.set(item instanceof LitematicFileItem); + isFileProperty.set(item instanceof SchematicItem); isDirectoryProperty.set(item.isDirectory()); - if (item instanceof LitematicFileItem fileItem && fileItem.getImage() != null) { + if (item instanceof SchematicItem fileItem && fileItem.getImage() != null) { iconImageView.setImage(fileItem.getImage()); left.getChildren().setAll(iconImageView); } else { diff --git a/HMCL/src/main/resources/assets/lang/I18N.properties b/HMCL/src/main/resources/assets/lang/I18N.properties index 4072491adf..6583f14331 100644 --- a/HMCL/src/main/resources/assets/lang/I18N.properties +++ b/HMCL/src/main/resources/assets/lang/I18N.properties @@ -1277,6 +1277,7 @@ schematics.info.time_modified=Modified Time schematics.info.total_blocks=Total Blocks schematics.info.total_volume=Total Volume schematics.info.version=Schematic Version +schematics.info.mc_version=Minecraft Version schematics.manage=Schematics schematics.sub_items=%d sub-item(s) diff --git a/HMCL/src/main/resources/assets/lang/I18N_zh.properties b/HMCL/src/main/resources/assets/lang/I18N_zh.properties index 715836268d..2e3287d5a3 100644 --- a/HMCL/src/main/resources/assets/lang/I18N_zh.properties +++ b/HMCL/src/main/resources/assets/lang/I18N_zh.properties @@ -1062,6 +1062,7 @@ schematics.info.time_modified=修改時間 schematics.info.total_blocks=總方塊數 schematics.info.total_volume=總體積 schematics.info.version=原理圖版本 +schematics.info.mc_version=MC版本 schematics.manage=原理圖管理 schematics.sub_items=%d 個子項 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 68df6265e4..a1d441c041 100644 --- a/HMCL/src/main/resources/assets/lang/I18N_zh_CN.properties +++ b/HMCL/src/main/resources/assets/lang/I18N_zh_CN.properties @@ -1067,6 +1067,7 @@ schematics.info.time_modified=修改时间 schematics.info.total_blocks=总方块数 schematics.info.total_volume=总体积 schematics.info.version=原理图版本 +schematics.info.mc_version=MC版本 schematics.manage=原理图管理 schematics.sub_items=%d 个子项 diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/schematic/LitematicFile.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/schematic/LitematicFile.java index ba112436c2..2e06f32c9d 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/schematic/LitematicFile.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/schematic/LitematicFile.java @@ -17,54 +17,30 @@ */ package org.jackhuang.hmcl.schematic; -import com.github.steveice10.opennbt.NBTIO; import com.github.steveice10.opennbt.tag.builtin.*; -import javafx.geometry.Point3D; +import org.jackhuang.hmcl.util.Vec3i; import org.jackhuang.hmcl.util.io.FileUtils; import org.jetbrains.annotations.NotNull; -import org.jetbrains.annotations.Nullable; import java.io.IOException; -import java.io.InputStream; import java.nio.file.Files; import java.nio.file.Path; import java.time.Instant; -import java.util.zip.GZIPInputStream; + +import static org.jackhuang.hmcl.schematic.Schematic.*; /** * @author Glavo * @see The Litematic file format */ -public final class LitematicFile { - - private static int tryGetInt(Tag tag) { - return tag instanceof IntTag ? ((IntTag) tag).getValue() : 0; - } - - private static @Nullable Instant tryGetLongTimestamp(Tag tag) { - if (tag instanceof LongTag) { - return Instant.ofEpochMilli(((LongTag) tag).getValue()); - } - return null; - } - - private static @Nullable String tryGetString(Tag tag) { - return tag instanceof StringTag ? ((StringTag) tag).getValue() : null; - } +public final class LitematicFile implements Schematic { public static boolean isFileLitematic(Path path) { return "litematic".equals(FileUtils.getExtension(path)) && Files.isRegularFile(path); } - public static CompoundTag readRoot(Path file) throws IOException { - CompoundTag root; - try (InputStream in = new GZIPInputStream(Files.newInputStream(file))) { - root = (CompoundTag) NBTIO.readTag(in); - } - return root; - } - public static LitematicFile load(Path file) throws IOException { + if (!isFileLitematic(file)) return null; CompoundTag root = readRoot(file); @@ -107,7 +83,7 @@ else if (!(metadataTag instanceof CompoundTag)) private final Instant timeModified; private final int totalBlocks; private final int totalVolume; - private final Point3D enclosingSize; + private final Vec3i enclosingSize; private LitematicFile(@NotNull Path file, @NotNull CompoundTag metadata, int version, int subVersion, int minecraftDataVersion, int regionCount) { @@ -131,7 +107,7 @@ private LitematicFile(@NotNull Path file, @NotNull CompoundTag metadata, this.totalVolume = tryGetInt(metadata.get("TotalVolume")); - Point3D enclosingSize = null; + Vec3i enclosingSize = null; Tag enclosingSizeTag = metadata.get("EnclosingSize"); if (enclosingSizeTag instanceof CompoundTag) { CompoundTag list = (CompoundTag) enclosingSizeTag; @@ -140,12 +116,13 @@ private LitematicFile(@NotNull Path file, @NotNull CompoundTag metadata, int z = tryGetInt(list.get("z")); if (x >= 0 && y >= 0 && z >= 0) - enclosingSize = new Point3D(x, y, z); + enclosingSize = new Vec3i(x, y, z); } this.enclosingSize = enclosingSize; } + @Override public @NotNull Path getFile() { return file; } @@ -162,14 +139,21 @@ public int getMinecraftDataVersion() { return minecraftDataVersion; } + @Override + public String getMinecraftVersion() { + return Integer.toString(minecraftDataVersion); + } + public int[] getPreviewImageData() { return previewImageData != null ? previewImageData.clone() : null; } + @Override public String getName() { return name; } + @Override public String getAuthor() { return author; } @@ -178,26 +162,32 @@ public String getDescription() { return description; } + @Override public Instant getTimeCreated() { return timeCreated; } + @Override public Instant getTimeModified() { return timeModified; } + @Override public int getTotalBlocks() { return totalBlocks; } + @Override public int getTotalVolume() { return totalVolume; } - public Point3D getEnclosingSize() { + @Override + public Vec3i getEnclosingSize() { return enclosingSize; } + @Override public int getRegionCount() { return regionCount; } diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/schematic/NBTStructureFile.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/schematic/NBTStructureFile.java new file mode 100644 index 0000000000..37f3c337d9 --- /dev/null +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/schematic/NBTStructureFile.java @@ -0,0 +1,83 @@ +package org.jackhuang.hmcl.schematic; + +import com.github.steveice10.opennbt.tag.builtin.*; +import org.jackhuang.hmcl.util.Vec3i; +import org.jackhuang.hmcl.util.io.FileUtils; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.List; + +import static org.jackhuang.hmcl.schematic.Schematic.tryGetInt; + +/// @author Calboot +/// @see Structure File +public final class NBTStructureFile implements Schematic { + + public static boolean isFileNBTStructure(Path path) { + return "nbt".equals(FileUtils.getExtension(path)) && Files.isRegularFile(path); + } + + public static NBTStructureFile load(Path file) throws IOException { + if (!isFileNBTStructure(file)) return null; + + CompoundTag root = Schematic.readRoot(file); + + Tag dataVersionTag = root.get("DataVersion"); + if (dataVersionTag == null) + throw new IOException("Materials tag not found"); + else if (!(dataVersionTag instanceof IntTag)) + throw new IOException("Materials tag is not an integer"); + + Tag sizeTag = root.get("size"); + if (sizeTag == null) + throw new IOException("size tag not found"); + else if (!(sizeTag instanceof ListTag)) + throw new IOException("size tag is not a list"); + List size = ((ListTag) sizeTag).getValue(); + if (size.size() != 3) + throw new IOException("size tag does not have 3 elements"); + Tag xTag = size.get(0); + Tag yTag = size.get(1); + Tag zTag = size.get(2); + Vec3i enclosingSize = null; + if (xTag != null && yTag != null && zTag != null) { + int width = tryGetInt(xTag); + int height = tryGetInt(yTag); + int length = tryGetInt(zTag); + if (width >= 0 && height >= 0 && length >= 0) { + enclosingSize = new Vec3i(width, height, length); + } + } + + return new NBTStructureFile(file, ((IntTag) dataVersionTag).getValue(), enclosingSize); + } + + private final Path file; + private final int dataVersion; + private final Vec3i enclosingSize; + + private NBTStructureFile(Path file, int dataVersion, Vec3i enclosingSize) { + this.file = file; + this.dataVersion = dataVersion; + this.enclosingSize = enclosingSize; + } + + @Override + public @NotNull Path getFile() { + return file; + } + + @Override + public String getMinecraftVersion() { + return Integer.toString(dataVersion); + } + + @Override + public @Nullable Vec3i getEnclosingSize() { + return enclosingSize; + } +} diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/schematic/Schematic.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/schematic/Schematic.java new file mode 100644 index 0000000000..bb7047b078 --- /dev/null +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/schematic/Schematic.java @@ -0,0 +1,100 @@ +package org.jackhuang.hmcl.schematic; + +import com.github.steveice10.opennbt.NBTIO; +import com.github.steveice10.opennbt.tag.builtin.*; +import org.jackhuang.hmcl.util.Vec3i; +import org.jackhuang.hmcl.util.io.FileUtils; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.io.IOException; +import java.io.InputStream; +import java.nio.file.Files; +import java.nio.file.Path; +import java.time.Instant; +import java.util.zip.GZIPInputStream; + +public sealed interface Schematic permits LitematicFile, SchematicFile, NBTStructureFile { + + static boolean isFileSchematicAlike(Path file) { + if (file == null) return false; + return LitematicFile.isFileLitematic(file) || SchematicFile.isFileSchematic(file) || NBTStructureFile.isFileNBTStructure(file); + } + + @Nullable + static Schematic load(Path file) throws IOException { + if (file == null) return null; + if (LitematicFile.isFileLitematic(file)) { + return LitematicFile.load(file); + } else if (SchematicFile.isFileSchematic(file)) { + return SchematicFile.load(file); + } else if (NBTStructureFile.isFileNBTStructure(file)) { + return NBTStructureFile.load(file); + } + return null; + } + + static CompoundTag readRoot(Path file) throws IOException { + CompoundTag root; + try (InputStream in = new GZIPInputStream(Files.newInputStream(file))) { + root = (CompoundTag) NBTIO.readTag(in); + } + return root; + } + + static int tryGetInt(Tag tag) { + return tag instanceof IntTag ? ((IntTag) tag).getValue() : 0; + } + + static short tryGetShort(Tag tag) { + return tag instanceof ShortTag ? ((ShortTag) tag).getValue() : 0; + } + + static @Nullable Instant tryGetLongTimestamp(Tag tag) { + if (tag instanceof LongTag) { + return Instant.ofEpochMilli(((LongTag) tag).getValue()); + } + return null; + } + + static @Nullable String tryGetString(Tag tag) { + return tag instanceof StringTag ? ((StringTag) tag).getValue() : null; + } + + @NotNull Path getFile(); + + String getMinecraftVersion(); + + default String getName() { + return FileUtils.getNameWithoutExtension(getFile()); + } + + default String getAuthor(){ + return null; + } + + default Instant getTimeCreated() { + return null; + } + + default Instant getTimeModified() { + return null; + } + + default int getRegionCount() { + return 0; + } + + default int getTotalBlocks() { + return 0; + } + + @Nullable Vec3i getEnclosingSize(); + + default int getTotalVolume() { + var enclosingSize = getEnclosingSize(); + if (enclosingSize != null) return enclosingSize.x() * enclosingSize.y() * enclosingSize.z(); + return 0; + } + +} diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/schematic/SchematicFile.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/schematic/SchematicFile.java new file mode 100644 index 0000000000..8fd23f9c0a --- /dev/null +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/schematic/SchematicFile.java @@ -0,0 +1,78 @@ +package org.jackhuang.hmcl.schematic; + +import com.github.steveice10.opennbt.tag.builtin.CompoundTag; +import com.github.steveice10.opennbt.tag.builtin.StringTag; +import com.github.steveice10.opennbt.tag.builtin.Tag; +import org.jackhuang.hmcl.util.Vec3i; +import org.jackhuang.hmcl.util.io.FileUtils; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; + +import static org.jackhuang.hmcl.schematic.Schematic.tryGetShort; + +/// @author Calboot +/// @see Schematic File Format Wiki +/// @see Schematica +public final class SchematicFile implements Schematic { + + public static boolean isFileSchematic(Path path) { + return "schematic".equals(FileUtils.getExtension(path)) && Files.isRegularFile(path); + } + + public static SchematicFile load(Path file) throws IOException { + if (!isFileSchematic(file)) return null; + + CompoundTag root = Schematic.readRoot(file); + + Tag materialsTag = root.get("Materials"); + if (materialsTag == null) + throw new IOException("Materials tag not found"); + else if (!(materialsTag instanceof StringTag)) + throw new IOException("Materials tag is not an integer"); + + Tag widthTag = root.get("Width"); + Tag heightTag = root.get("Height"); + Tag lengthTag = root.get("Length"); + Vec3i enclosingSize = null; + if (widthTag != null && heightTag != null && lengthTag != null) { + short width = tryGetShort(widthTag); + short height = tryGetShort(heightTag); + short length = tryGetShort(lengthTag); + if (width >= 0 && height >= 0 && length >= 0) { + enclosingSize = new Vec3i(width, height, length); + } + } + + return new SchematicFile(file, ((StringTag) materialsTag).getValue(), enclosingSize); + } + + private final Path file; + private final String materials; + private final Vec3i enclosingSize; + + private SchematicFile(Path file, String materials, Vec3i enclosingSize) { + this.file = file; + this.materials = materials; + this.enclosingSize = enclosingSize; + } + + @Override + public @NotNull Path getFile() { + return file; + } + + @Override + public String getMinecraftVersion() { + return materials; + } + + @Override + public @Nullable Vec3i getEnclosingSize() { + return enclosingSize; + } + +} diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/util/Vec3i.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/util/Vec3i.java new file mode 100644 index 0000000000..9e5948c5b8 --- /dev/null +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/util/Vec3i.java @@ -0,0 +1,7 @@ +package org.jackhuang.hmcl.util; + +public record Vec3i(int x, int y, int z) { + + public static Vec3i ZERO = new Vec3i(0, 0, 0); + +} From 5ae71b77cc06328499512ba4ed838ea6da1cc305 Mon Sep 17 00:00:00 2001 From: Calboot Date: Sun, 1 Feb 2026 22:06:01 +0800 Subject: [PATCH 15/53] Fix text --- .../java/org/jackhuang/hmcl/schematic/LitematicFileTest.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/HMCLCore/src/test/java/org/jackhuang/hmcl/schematic/LitematicFileTest.java b/HMCLCore/src/test/java/org/jackhuang/hmcl/schematic/LitematicFileTest.java index 1a42bfba96..69a31ac1fb 100644 --- a/HMCLCore/src/test/java/org/jackhuang/hmcl/schematic/LitematicFileTest.java +++ b/HMCLCore/src/test/java/org/jackhuang/hmcl/schematic/LitematicFileTest.java @@ -17,8 +17,8 @@ */ package org.jackhuang.hmcl.schematic; -import javafx.geometry.Point3D; import org.jackhuang.hmcl.game.CrashReportAnalyzerTest; +import org.jackhuang.hmcl.util.Vec3i; import org.junit.jupiter.api.Test; import java.io.IOException; @@ -47,7 +47,7 @@ public void test() throws Exception { assertEquals(Instant.ofEpochMilli(1746443586433L), file.getTimeModified()); assertEquals(1334, file.getTotalBlocks()); assertEquals(5746, file.getTotalVolume()); - assertEquals(new Point3D(17, 26, 13), file.getEnclosingSize()); + assertEquals(new Vec3i(17, 26, 13), file.getEnclosingSize()); assertEquals(1, file.getRegionCount()); } } From c9c902bde47c2a520a0322f84c1ad3f0aad4e06f Mon Sep 17 00:00:00 2001 From: Calboot Date: Sun, 1 Feb 2026 22:08:27 +0800 Subject: [PATCH 16/53] update --- .../src/main/java/org/jackhuang/hmcl/schematic/Schematic.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/schematic/Schematic.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/schematic/Schematic.java index bb7047b078..c42d648841 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/schematic/Schematic.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/schematic/Schematic.java @@ -69,7 +69,7 @@ default String getName() { return FileUtils.getNameWithoutExtension(getFile()); } - default String getAuthor(){ + default String getAuthor() { return null; } From 8e589e52d311ea8463f43c0dd85ef4ef61f8bdbc Mon Sep 17 00:00:00 2001 From: Calboot Date: Sun, 1 Feb 2026 22:24:46 +0800 Subject: [PATCH 17/53] =?UTF-8?q?=E6=9B=B4=E6=96=B0=E6=B5=8B=E8=AF=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../hmcl/schematic/LitematicFile.java | 6 +- .../hmcl/schematic/NBTStructureFile.java | 4 +- .../jackhuang/hmcl/schematic/Schematic.java | 8 +- .../hmcl/schematic/LitematicFileTest.java | 53 -------------- .../hmcl/schematic/SchematicTest.java | 69 ++++++++++++++++++ .../src/test/resources/schematics/test.nbt | Bin 0 -> 4115 bytes .../test/resources/schematics/test.schematic | Bin 0 -> 1149 bytes 7 files changed, 79 insertions(+), 61 deletions(-) delete mode 100644 HMCLCore/src/test/java/org/jackhuang/hmcl/schematic/LitematicFileTest.java create mode 100644 HMCLCore/src/test/java/org/jackhuang/hmcl/schematic/SchematicTest.java create mode 100644 HMCLCore/src/test/resources/schematics/test.nbt create mode 100644 HMCLCore/src/test/resources/schematics/test.schematic diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/schematic/LitematicFile.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/schematic/LitematicFile.java index 2e06f32c9d..cb4a3eeb07 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/schematic/LitematicFile.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/schematic/LitematicFile.java @@ -135,15 +135,11 @@ public int getSubVersion() { return subVersion; } + @Override public int getMinecraftDataVersion() { return minecraftDataVersion; } - @Override - public String getMinecraftVersion() { - return Integer.toString(minecraftDataVersion); - } - public int[] getPreviewImageData() { return previewImageData != null ? previewImageData.clone() : null; } diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/schematic/NBTStructureFile.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/schematic/NBTStructureFile.java index 37f3c337d9..dd612e985c 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/schematic/NBTStructureFile.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/schematic/NBTStructureFile.java @@ -72,8 +72,8 @@ private NBTStructureFile(Path file, int dataVersion, Vec3i enclosingSize) { } @Override - public String getMinecraftVersion() { - return Integer.toString(dataVersion); + public int getMinecraftDataVersion() { + return dataVersion; } @Override diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/schematic/Schematic.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/schematic/Schematic.java index c42d648841..ea1e0f0912 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/schematic/Schematic.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/schematic/Schematic.java @@ -63,7 +63,13 @@ static short tryGetShort(Tag tag) { @NotNull Path getFile(); - String getMinecraftVersion(); + default int getMinecraftDataVersion() { + return 0; + } + + default String getMinecraftVersion() { + return Integer.toString(getMinecraftDataVersion()); + } default String getName() { return FileUtils.getNameWithoutExtension(getFile()); diff --git a/HMCLCore/src/test/java/org/jackhuang/hmcl/schematic/LitematicFileTest.java b/HMCLCore/src/test/java/org/jackhuang/hmcl/schematic/LitematicFileTest.java deleted file mode 100644 index 69a31ac1fb..0000000000 --- a/HMCLCore/src/test/java/org/jackhuang/hmcl/schematic/LitematicFileTest.java +++ /dev/null @@ -1,53 +0,0 @@ -/* - * Hello Minecraft! Launcher - * Copyright (C) 2025 huangyuhui and contributors - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ -package org.jackhuang.hmcl.schematic; - -import org.jackhuang.hmcl.game.CrashReportAnalyzerTest; -import org.jackhuang.hmcl.util.Vec3i; -import org.junit.jupiter.api.Test; - -import java.io.IOException; -import java.net.URISyntaxException; -import java.net.URL; -import java.nio.file.Paths; -import java.time.Instant; - -import static org.junit.jupiter.api.Assertions.assertEquals; - -public final class LitematicFileTest { - private static LitematicFile load(String name) throws IOException, URISyntaxException { - URL resource = CrashReportAnalyzerTest.class.getResource(name); - if (resource == null) - throw new IOException("Resource not found: " + name); - return LitematicFile.load(Paths.get(resource.toURI())); - } - - @Test - public void test() throws Exception { - LitematicFile file = load("/schematics/test.litematic"); - assertEquals("刷石机一桶岩浆下推爆破8.3万每小时", file.getName()); - assertEquals("hsds", file.getAuthor()); - assertEquals("", file.getDescription()); - assertEquals(Instant.ofEpochMilli(1746443586433L), file.getTimeCreated()); - assertEquals(Instant.ofEpochMilli(1746443586433L), file.getTimeModified()); - assertEquals(1334, file.getTotalBlocks()); - assertEquals(5746, file.getTotalVolume()); - assertEquals(new Vec3i(17, 26, 13), file.getEnclosingSize()); - assertEquals(1, file.getRegionCount()); - } -} diff --git a/HMCLCore/src/test/java/org/jackhuang/hmcl/schematic/SchematicTest.java b/HMCLCore/src/test/java/org/jackhuang/hmcl/schematic/SchematicTest.java new file mode 100644 index 0000000000..d09a35323b --- /dev/null +++ b/HMCLCore/src/test/java/org/jackhuang/hmcl/schematic/SchematicTest.java @@ -0,0 +1,69 @@ +/* + * Hello Minecraft! Launcher + * Copyright (C) 2025 huangyuhui and contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package org.jackhuang.hmcl.schematic; + +import org.jackhuang.hmcl.game.CrashReportAnalyzerTest; +import org.jackhuang.hmcl.util.Vec3i; +import org.junit.jupiter.api.Test; + +import java.io.IOException; +import java.net.URISyntaxException; +import java.net.URL; +import java.nio.file.Paths; +import java.time.Instant; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +public final class SchematicTest { + private static Schematic load(String name) throws IOException, URISyntaxException { + URL resource = CrashReportAnalyzerTest.class.getResource(name); + if (resource == null) + throw new IOException("Resource not found: " + name); + return Schematic.load(Paths.get(resource.toURI())); + } + + @Test + public void test() throws Exception { + LitematicFile lFile = (LitematicFile) load("/schematics/test.litematic"); + assertEquals("刷石机一桶岩浆下推爆破8.3万每小时", lFile.getName()); + assertEquals("hsds", lFile.getAuthor()); + assertEquals("", lFile.getDescription()); + assertEquals(Instant.ofEpochMilli(1746443586433L), lFile.getTimeCreated()); + assertEquals(Instant.ofEpochMilli(1746443586433L), lFile.getTimeModified()); + assertEquals(1334, lFile.getTotalBlocks()); + assertEquals(5746, lFile.getTotalVolume()); + assertEquals(new Vec3i(17, 26, 13), lFile.getEnclosingSize()); + assertEquals(1, lFile.getRegionCount()); + assertEquals(4325, lFile.getMinecraftDataVersion()); + assertEquals("4325", lFile.getMinecraftVersion()); + assertEquals(7, lFile.getVersion()); + + SchematicFile sFile = (SchematicFile) load("/schematics/test.schematic"); + assertEquals("test", sFile.getName()); + assertEquals(new Vec3i(28, 35, 18), sFile.getEnclosingSize()); + assertEquals(17640, sFile.getTotalVolume()); + assertEquals("Alpha", sFile.getMinecraftVersion()); + + NBTStructureFile nFile = (NBTStructureFile) load("/schematics/test.nbt"); + assertEquals("test", nFile.getName()); + assertEquals(new Vec3i(9, 11, 13), nFile.getEnclosingSize()); + assertEquals(1287, nFile.getTotalVolume()); + assertEquals(3465, nFile.getMinecraftDataVersion()); + assertEquals("3465", nFile.getMinecraftVersion()); + } +} diff --git a/HMCLCore/src/test/resources/schematics/test.nbt b/HMCLCore/src/test/resources/schematics/test.nbt new file mode 100644 index 0000000000000000000000000000000000000000..389866b030b07a2e728c4bd4ca1740e18fd61410 GIT binary patch literal 4115 zcmah}2{@E{-_N)*f_^uI#jDU=52;CRqyU0%+h(+NQ#9?<*5=$ zxLC!c;71&m-wuY*G}rBWUk)uAhF>3Zxaps9v3@Fdyk#fAa@B+CLU>O_e}@M{5VNMH zXyrsKLPmV82qJzqKe(N>atisV_R0I*f@ke8i`_WN56{Zk?#?p5+;7S^FsD7$=ik9R z-+pwS4(rsj)4dEXo*Gh~c)5(;T%DjqHrq!AjTlh+2?;ceyOd!6O02bT-0Ji5*TB;m zQLX1?Z#t&XLR|0?R{isV(s>vwe}qQDfyqVK{EcjzABHUf&6~(tM*Sa=Q{wQ0>OVFp za-M?R%aakL_6#>BBf;%$N=T0$A>5$MJlE6MkTtc<+)|()o11L#L!C#@Hh*vFC`WHE zWTY5PL@tc3=bg+u#W(eUR+dqc@KSl0Rug$Oc7Jj3%cE`a2?2xW4!HIieMB1$-3}|F z+uFrSM>g+$fJSc24o!uxVojdh;z2*l`3Z!c}1-%@Po=DL($XRW9&2 zi!){=>@V}*5xRdU{228Al<3b`Dkr*Y#8jj~GeQs?F=RhU5wG5o z=KRAQ_gc3hFk2`Hyo4YE;rtK+ws!igv;M!t;M%{$YtDU3KUxM0bSU20$N!8Ah8HIS zH#lP4{{&hp(#Cn-{OcB|pHC;d8_3*2Mm)(YRFZWK3|@ykZ1d?|+eojsy{l2ZU)CP= z*{-ee;*9WOkCZ^EI&9>v+a-kQ3X}LDbPG&6_A5WOv&7eBiFT9@eV61c$j#Lah~ZRJ z<%T16G_3pvbVA=+(^}Hz>U$z_q&M5E867Q790@JA-~+tgQrMx@O?nO-`K?D4sMzA- z*J|Oka8v;2I1vHn$w7MYX66-yDJ2q(kpU`dIUs~SP|U_%z^gp+1$=2Uq7FYE`{$dD z4`e0R={CkTv9m4!xFO@H@-joVKX>m9ULF)zEFNUsvrlgBchPDeX)yn)3UP!KM;3t9 zQA|LWz7j_q2Wr}vMV!IlrP5als-iI)j(|RA%!V7l$^PC2j%&7UX30M8WnaWrryOMa ze>%Fh#tYxsuU8Q_I(Pm;N%b$X>V_*XsuL^o+V3ZcE0BW3B~FB9LB{QJ%i?4o$H}$| zNN*NuZ7n7Df*8V^QX`-^6Gnu-@l1#1t zQ|P_TbQGg7Ds{eHTLal)wXA;|pz9iW0n(f^A`mR``+kMrU~*BZp5GH(1zu`iTOr0% z)uBVlsM=nvB;<83_VE80bYAm#;)nV8#Wyp;H5?|xG+r{blFSEGp!V@=soraye5Y z*7fLWAFt$nF)47M0uG*^9@*vmYZUn)UJ>v*Ru7X4Bu|-d1ha*`kDUjDxTT+lR|lKC zVq*RlgYdJm#Q9?&c0~(_u{o)EGy(N2aLrQj+WLoR2}Zda_AFEs4^-`d|7 zvjJx>7X2~>?bp4FvV{g*Gq&5<&`lp_&R9Pl21m?>E>Pj9{VAU0T=d0vmzwB_PbhWzK0|AEM}(^NR~MX~|Kp z1ZO&9I^^4CglCkX3^~>t?SsjfK_SSpG8d%ab18x`G%-^e!4dbKs!u*}z1Y$4C@t&A zL{}Ip)%6qD=qBAQTQ!WUuP)9^tUxIaP6@eU8Qnq&DIps^&z4cB4>{7jS3EV2AVhPF!GmNSJ1oMQiI_q9e%B*O%M@ZCl zK@<4;v?VmKowiU+15F(1oBZne8^HN81_>)2&GX zkDJ*37-co<)xmxxUp8*@t`+(B)IfX?}M)~`u{t`3@>8$vP9H2qVa#gwZldw#sHtFy&Xs6mR^+KvY zjGOlJF+_TYF!c{p`5ou<9<_F(P1QoORxf~6nKGT9U7F=3o4%M-bu3Q_BxE7M2ydjlIZMRu4u@0{Svo(jp&`Dd9-@E06%X$_Olx4#H}(myNXXIJ+y@cN|+-RdScH@!oq^+)2j(r_zn zWkDgDD>jxNSFa1U2iq>qJKJpD|Gvw|*z%<@RomxV_|m?59R2!YB7+sN>0`&Wcet8P z3NW)UL#GyvPGpWt3{vSn4WT;zJCCw+!oK_OBxUCWvkz`f?VsOfH!VGUTBo~LoCc}y zl>B3UqHilI##o^o&~DdF8l|4r4Sl4q4^C!8XiQ2;>UVd~U=@9Z7_Uq_$=lwt@3yTY zFdag@H(OLF);~kjy}!|@%=(r)a9>UG_Rxc(v4C_VvCPbihb0DzKAWA92K7dE5g#~p z_Z#*h;UTXCceIW{)q+izuS;Y#n@9%k$Y5xRqip8OoD!88Ylh=*%xFFg@_Efam- ziA;X;<*ZLU@W}>QeQNvRxSZ|!5S4&2qzCc*`!#ik_0})u65+5n-_Jw!p=IZxRsAa4 zHS+g6(&4f=RJMTl=e~XPBaT@U`*`uV>a9c|+DeT1jQU|*9U5A@it;t zoo&a&D_#BGRLb?2%L{{(ux;O1+zSo6k-V*+<@LbKG7S~2!!0Q3GK<2iB1M?RhI9wz zs+?U~Yo60?sD1v!4HLi96+`!8tlp5-m24vo7IAMq)ha#0SSur!DE|2qv@DOVSu4kc z+fQ}0#GYi;9_$afE1HUH2+hISP^M}+l6-u2oNX+6=_`d6_VD zkx2o!`4Q?OAo(6Xc!9PSAA$iL(mN!Iz^_=Ig}?ipPsS9}j_O z`r!_C>6VP%pKhOxCf!h7(7kXdV<8FmyqT8qqC6yU@uXwUb`pgRQ`j&K&4qe|H+9>~ z(SutDAMDi|fx8;mRGvqdh3*uPJ!<1ts|CrYUxNr21Hs(3=|ryP6Nzm_LxtO~E<-=4 z{Y+FIf~K#$B8g@OcdF{>xSZ7u4B!?bBzg5V@-Xk_opI|A6=)+*m}E?ys%CX*+t4OY zUYhF%Tt9xv4Y4JDfqQMqj#nxK&DXZ}U$f*-|1p=}He9M6wQ@p(nJIfYf<>^HeG_$Ag~1mvhtkg)yQ61ohq|3Vw_v#$%jYg`+&{$Z^<6Ob9)zAcqCbbnV zRV!5;fMI@EEQ~Qn$A5Gd7^Vu`t$G@0ou94N(wDYqcOXtSbZoOJfp<%6T3D#YLMoyH zQgy0D<>i3t%Yqf3+Pe>+$3Xdle3PXGYlYjIM|W!0_~Tut9if=xLVt*m#1O{CFU=vKzOTKaW~-0;e6xMPsN7%F4#=R>ZVN(Bqq| zA1d7L8woyw$l>h}Y_{?C#ycHaP@gm1yZG5mP)W(6Bg*>R>n8clXK!`YEV&XFxG zTrEr0J^FS`Sk{CYp|aMlX44jLSe6rZ!jxx6_x6bIex{-0$EI%=Q-@(y7Ud-`CyQ{$ z$Y<-!?ywRxAC$RSoV|{6=--Ua^iDeM<$mnM6T$W>n7JtF!Cw9zl#aN40{(!5E literal 0 HcmV?d00001 diff --git a/HMCLCore/src/test/resources/schematics/test.schematic b/HMCLCore/src/test/resources/schematics/test.schematic new file mode 100644 index 0000000000000000000000000000000000000000..eda71a36b9ed7124062713eb5f990736c6229031 GIT binary patch literal 1149 zcmV-@1cLh?iwFP!000000PUR3Zqq;%g>MrYQvN|=$IfrT4k!{xbX6plXcSChktS7Q z#4}ZWF&=|9BzA1iJ!4O5JB`n`Xzd(d&z(6lo*Ad=BRM%AO~MNkN27-_ct4$wel0~# z|4Mm+8#HC5DvGM5KXMnW&(^oiXuFwzte^+!uiHYei_$7SlTaYmE+B<=D!8M?;qn}L!uo9@l?DavDaeOfz@yzZ zRAlq9Ub&=Qib3l#^Y!00DPw!azwBlCevnH8bPBz^e4a&7ub-fH!R^_~XxF6VM=?lr zE&YYsQ&iitaj1JJMLXhfd5%0`{b0M`KGC%8Zr}B%JoH@(BRV(XDovd~Q$4BXdvO*h zB^L68^(*B&c*@(Adf3HxYXw{My<8kF&yi;--xcH6)4Npz^L{m+P@V_lJ6LImWp>*L`U5T5T4_5V`w1{YAMknOE1Z>o8ss%pKgWY!Yflu^20m;3 zq|aCKz4Y+Et25tRVEs_LY+^L+eqgWlTD$#mWxD|Hw^glq-$9_}+7(Gre&y^?f0r+m;3S}maD!$`5 zwLE8@!~^GeFKz`=&FZ_V1waGkIrAhQxORcc(>mEYTcT9oVVqi?Gf(1y*V|#oTr9fi zjN6xQOAa3Vz7oDA#GOJ}pW!&J+4lgrbplST7I6CHfxZx-x$T3V1b8=q9 zjco88m?#dG=T#cfpzqy;z8^Ah#jb1n<2z@5AMpCjL#oMkzwSvCJjX41q2uYhV-o=4 zd`crKuHBff`4;D=d6hv8njKQCtk`3fCFl&~kg;6Lf2D71SweNryCqKBlp=P0azM9_V{Fq3<09_&z|3 zH~N04a7YtE2q7)dZ(j7&?p*3JeZ5_3Y_U6C>*4-K-z$PzOp|V65~=8IU)TCT1LbA! zN?O8UfiBa!dyT_BG^Nn_z2b4DtNVDs1K)SGy;`1x5JCtcgmjvwzgy*Zx%&Zq?(7UpOCcDyjl z<&hkJnE#%|M?D#wnhP@yR}nAIN0acv#L>u{naj&)HtxxbG<0MZmtic|J$apexD3tr zshP#$VkTETd73IFTHOANmS^cFf9CV41UH`@e~W&^6M1tYPrgRe@Z&6w;wW6M5(R$TMv=ZXLTb}2%~ literal 0 HcmV?d00001 From abd6862343fc6907d1e75db30d18744e909c576d Mon Sep 17 00:00:00 2001 From: Calboot Date: Mon, 2 Feb 2026 13:11:59 +0800 Subject: [PATCH 18/53] =?UTF-8?q?=E6=94=AF=E6=8C=81=20*.schem?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../hmcl/ui/versions/SchematicsPage.java | 17 ++- .../resources/assets/lang/I18N.properties | 4 + .../resources/assets/lang/I18N_zh.properties | 4 + .../assets/lang/I18N_zh_CN.properties | 4 + .../hmcl/schematic/LitematicFile.java | 13 +- .../hmcl/schematic/NBTStructureFile.java | 12 +- .../jackhuang/hmcl/schematic/SchemFile.java | 121 ++++++++++++++++++ .../jackhuang/hmcl/schematic/Schematic.java | 29 +++-- .../hmcl/schematic/SchematicFile.java | 78 ----------- .../hmcl/schematic/SchematicType.java | 28 ++++ .../hmcl/schematic/SchematicTest.java | 66 ++++++---- .../src/test/resources/schematics/test.schem | Bin 0 -> 300 bytes 12 files changed, 241 insertions(+), 135 deletions(-) create mode 100644 HMCLCore/src/main/java/org/jackhuang/hmcl/schematic/SchemFile.java delete mode 100644 HMCLCore/src/main/java/org/jackhuang/hmcl/schematic/SchematicFile.java create mode 100644 HMCLCore/src/main/java/org/jackhuang/hmcl/schematic/SchematicType.java create mode 100644 HMCLCore/src/test/resources/schematics/test.schem diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/SchematicsPage.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/SchematicsPage.java index 469e8cf585..4c7ea09a6c 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/SchematicsPage.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/SchematicsPage.java @@ -49,6 +49,7 @@ import javafx.stage.FileChooser; import org.jackhuang.hmcl.schematic.LitematicFile; import org.jackhuang.hmcl.schematic.Schematic; +import org.jackhuang.hmcl.schematic.SchematicType; import org.jackhuang.hmcl.setting.Profile; import org.jackhuang.hmcl.task.Schedulers; import org.jackhuang.hmcl.task.Task; @@ -90,6 +91,10 @@ private static String translateAuthorName(String author) { return author; } + private static String translateType(SchematicType type) { + return i18n("schematics.info.type." + type.name().toLowerCase(Locale.ROOT)); + } + private Path schematicsDirectory; private final ObjectProperty currentDirectory = new SimpleObjectProperty<>(this, "currentDirectory", null); @@ -97,7 +102,7 @@ private static String translateAuthorName(String author) { public SchematicsPage() { FXUtils.applyDragListener(this, - file -> currentDirectoryProperty().get() != null && Schematic.isFileSchematicAlike(file), + file -> currentDirectoryProperty().get() != null && Schematic.isFileSchematic(file), this::addFiles ); } @@ -337,7 +342,7 @@ void preLoad() throws IOException { try (Stream stream = Files.list(path)) { stream.forEach(p -> { boolean b1 = Files.isDirectory(p); - boolean b2 = Schematic.isFileSchematicAlike(p); + boolean b2 = Schematic.isFileSchematic(p); if (b1 || b2) this.size++; if (b1) { var child = new DirItem(p, this); @@ -357,7 +362,7 @@ void load() { dir.preLoad(); this.children.add(dir); } - stream.filter(Schematic::isFileSchematicAlike) + stream.filter(Schematic::isFileSchematic) .forEach(p -> { try { this.children.add(new SchematicItem(p)); @@ -511,6 +516,7 @@ private void addDetailItem(String key, Object detail) { private void updateContent(Schematic file) { details.getContent().clear(); addDetailItem(i18n("schematics.info.name"), file.getName()); + addDetailItem(i18n("schematics.info.type"), translateType(file.getType())); if (StringUtils.isNotBlank(file.getAuthor())) addDetailItem(i18n("schematics.info.schematic_author"), translateAuthorName(file.getAuthor())); if (file.getTimeCreated() != null) @@ -530,9 +536,8 @@ private void updateContent(Schematic file) { file.getEnclosingSize().z())); if (StringUtils.isNotBlank(file.getMinecraftVersion())) addDetailItem(i18n("schematics.info.mc_version"), file.getMinecraftVersion()); - - if (file instanceof LitematicFile lFile) - addDetailItem(i18n("schematics.info.version"), lFile.getVersion()); + if (file.getVersion() > 0) + addDetailItem(i18n("schematics.info.version"), file.getVersion()); } SchematicInfoDialog() { diff --git a/HMCL/src/main/resources/assets/lang/I18N.properties b/HMCL/src/main/resources/assets/lang/I18N.properties index 6583f14331..96f870d7f4 100644 --- a/HMCL/src/main/resources/assets/lang/I18N.properties +++ b/HMCL/src/main/resources/assets/lang/I18N.properties @@ -1276,6 +1276,10 @@ schematics.info.time_created=Created Time schematics.info.time_modified=Modified Time schematics.info.total_blocks=Total Blocks schematics.info.total_volume=Total Volume +schematics.info.type=Schematic Type +schematics.info.type.litematic=Litematic (Litematica) +schematics.info.type.nbt_structure=NBT Structure (Vanilla) +schematics.info.type.schem=Schem (Schematica, WE, MCEdit) schematics.info.version=Schematic Version schematics.info.mc_version=Minecraft Version schematics.manage=Schematics diff --git a/HMCL/src/main/resources/assets/lang/I18N_zh.properties b/HMCL/src/main/resources/assets/lang/I18N_zh.properties index 2e3287d5a3..963d16f0f3 100644 --- a/HMCL/src/main/resources/assets/lang/I18N_zh.properties +++ b/HMCL/src/main/resources/assets/lang/I18N_zh.properties @@ -1061,6 +1061,10 @@ schematics.info.time_created=建立時間 schematics.info.time_modified=修改時間 schematics.info.total_blocks=總方塊數 schematics.info.total_volume=總體積 +schematics.info.type=原理圖型別 +schematics.info.type.litematic=Litematic (Litematica) +schematics.info.type.nbt_structure=NBT 結構 (原版) +schematics.info.type.schem=Schem (Schematica, WE, MCEdit) schematics.info.version=原理圖版本 schematics.info.mc_version=MC版本 schematics.manage=原理圖管理 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 a1d441c041..48269a03a9 100644 --- a/HMCL/src/main/resources/assets/lang/I18N_zh_CN.properties +++ b/HMCL/src/main/resources/assets/lang/I18N_zh_CN.properties @@ -1066,6 +1066,10 @@ schematics.info.time_created=创建时间 schematics.info.time_modified=修改时间 schematics.info.total_blocks=总方块数 schematics.info.total_volume=总体积 +schematics.info.type=原理图类型 +schematics.info.type.litematic=Litematic (Litematica) +schematics.info.type.nbt_structure=NBT 结构 (原版) +schematics.info.type.schem=Schem (Schematica, WE, MCEdit) schematics.info.version=原理图版本 schematics.info.mc_version=MC版本 schematics.manage=原理图管理 diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/schematic/LitematicFile.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/schematic/LitematicFile.java index cb4a3eeb07..bddffcd2fd 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/schematic/LitematicFile.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/schematic/LitematicFile.java @@ -19,11 +19,9 @@ import com.github.steveice10.opennbt.tag.builtin.*; import org.jackhuang.hmcl.util.Vec3i; -import org.jackhuang.hmcl.util.io.FileUtils; import org.jetbrains.annotations.NotNull; import java.io.IOException; -import java.nio.file.Files; import java.nio.file.Path; import java.time.Instant; @@ -35,12 +33,7 @@ */ public final class LitematicFile implements Schematic { - public static boolean isFileLitematic(Path path) { - return "litematic".equals(FileUtils.getExtension(path)) && Files.isRegularFile(path); - } - public static LitematicFile load(Path file) throws IOException { - if (!isFileLitematic(file)) return null; CompoundTag root = readRoot(file); @@ -122,11 +115,17 @@ private LitematicFile(@NotNull Path file, @NotNull CompoundTag metadata, } + @Override + public SchematicType getType() { + return SchematicType.LITEMATIC; + } + @Override public @NotNull Path getFile() { return file; } + @Override public int getVersion() { return version; } diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/schematic/NBTStructureFile.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/schematic/NBTStructureFile.java index dd612e985c..e26e02e09a 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/schematic/NBTStructureFile.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/schematic/NBTStructureFile.java @@ -2,12 +2,10 @@ import com.github.steveice10.opennbt.tag.builtin.*; import org.jackhuang.hmcl.util.Vec3i; -import org.jackhuang.hmcl.util.io.FileUtils; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import java.io.IOException; -import java.nio.file.Files; import java.nio.file.Path; import java.util.List; @@ -17,12 +15,7 @@ /// @see Structure File public final class NBTStructureFile implements Schematic { - public static boolean isFileNBTStructure(Path path) { - return "nbt".equals(FileUtils.getExtension(path)) && Files.isRegularFile(path); - } - public static NBTStructureFile load(Path file) throws IOException { - if (!isFileNBTStructure(file)) return null; CompoundTag root = Schematic.readRoot(file); @@ -66,6 +59,11 @@ private NBTStructureFile(Path file, int dataVersion, Vec3i enclosingSize) { this.enclosingSize = enclosingSize; } + @Override + public SchematicType getType() { + return SchematicType.NBT_STRUCTURE; + } + @Override public @NotNull Path getFile() { return file; diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/schematic/SchemFile.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/schematic/SchemFile.java new file mode 100644 index 0000000000..895447e302 --- /dev/null +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/schematic/SchemFile.java @@ -0,0 +1,121 @@ +package org.jackhuang.hmcl.schematic; + +import com.github.steveice10.opennbt.tag.builtin.CompoundTag; +import com.github.steveice10.opennbt.tag.builtin.IntTag; +import com.github.steveice10.opennbt.tag.builtin.StringTag; +import com.github.steveice10.opennbt.tag.builtin.Tag; +import org.jackhuang.hmcl.util.Vec3i; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.io.IOException; +import java.nio.file.Path; + +import static org.jackhuang.hmcl.schematic.Schematic.*; +import static org.jackhuang.hmcl.schematic.Schematic.tryGetShort; + +/// @author Calboot +/// @see Schematic File Format Wiki +/// @see Schematica +/// @see Schem +public final class SchemFile implements Schematic { + + private static final int DATA_VERSION_MC_1_13_2 = 1631; + + public static SchemFile load(Path file) throws IOException { + + CompoundTag root = readRoot(file); + + if (root.contains("Materials")) return loadLegacy(file, root); + else if (root.contains("Version")) return loadSponge(file, root); + throw new IOException("No Materials tag or Version tag found"); + } + + private static SchemFile loadLegacy(Path file, CompoundTag root) throws IOException { + Tag materialsTag = root.get("Materials"); + if (!(materialsTag instanceof StringTag)) + throw new IOException("Materials tag is not a string"); + + Tag widthTag = root.get("Width"); + Tag heightTag = root.get("Height"); + Tag lengthTag = root.get("Length"); + Vec3i enclosingSize = null; + if (widthTag != null && heightTag != null && lengthTag != null) { + short width = tryGetShort(widthTag); + short height = tryGetShort(heightTag); + short length = tryGetShort(lengthTag); + if (width >= 0 && height >= 0 && length >= 0) { + enclosingSize = new Vec3i(width, height, length); + } + } + + return new SchemFile(file, ((StringTag) materialsTag).getValue(), 0, 0, enclosingSize); + } + + private static SchemFile loadSponge(Path file, CompoundTag root) throws IOException { + Tag versionTag = root.get("Version"); + if (!(versionTag instanceof IntTag)) + throw new IOException("Version tag is not an integer"); + + Tag dataVersionTag = root.get("DataVersion"); + int dataVersion = dataVersionTag == null ? DATA_VERSION_MC_1_13_2 : tryGetInt(dataVersionTag); + + Tag widthTag = root.get("Width"); + Tag heightTag = root.get("Height"); + Tag lengthTag = root.get("Length"); + Vec3i enclosingSize = null; + if (widthTag != null && heightTag != null && lengthTag != null) { + int width = tryGetShort(widthTag) & 0xFFFF; + int height = tryGetShort(heightTag) & 0xFFFF; + int length = tryGetShort(lengthTag) & 0xFFFF; + enclosingSize = new Vec3i(width, height, length); + } + + return new SchemFile(file, null, dataVersion, ((IntTag) versionTag).getValue(), enclosingSize); + } + + private final Path file; + private final String materials; + private final int dataVersion; + private final int version; + private final Vec3i enclosingSize; + + private SchemFile(Path file, @Nullable String materials, int dataVersion, int version, Vec3i enclosingSize) { + this.file = file; + this.materials = materials; + this.dataVersion = dataVersion; + this.version = version; + this.enclosingSize = enclosingSize; + } + + @Override + public SchematicType getType() { + return SchematicType.SCHEM; + } + + @Override + public @NotNull Path getFile() { + return file; + } + + @Override + public int getVersion() { + return version; + } + + @Override + public int getMinecraftDataVersion() { + return dataVersion; + } + + @Override + public String getMinecraftVersion() { + return materials == null ? Integer.toString(dataVersion) : materials; + } + + @Override + public @Nullable Vec3i getEnclosingSize() { + return enclosingSize; + } + +} diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/schematic/Schematic.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/schematic/Schematic.java index ea1e0f0912..63f2cd30d8 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/schematic/Schematic.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/schematic/Schematic.java @@ -14,24 +14,21 @@ import java.time.Instant; import java.util.zip.GZIPInputStream; -public sealed interface Schematic permits LitematicFile, SchematicFile, NBTStructureFile { +public sealed interface Schematic permits LitematicFile, SchemFile, NBTStructureFile { - static boolean isFileSchematicAlike(Path file) { - if (file == null) return false; - return LitematicFile.isFileLitematic(file) || SchematicFile.isFileSchematic(file) || NBTStructureFile.isFileNBTStructure(file); + static boolean isFileSchematic(Path file) { + return SchematicType.getType(file) != null; } @Nullable static Schematic load(Path file) throws IOException { - if (file == null) return null; - if (LitematicFile.isFileLitematic(file)) { - return LitematicFile.load(file); - } else if (SchematicFile.isFileSchematic(file)) { - return SchematicFile.load(file); - } else if (NBTStructureFile.isFileNBTStructure(file)) { - return NBTStructureFile.load(file); - } - return null; + var type = SchematicType.getType(file); + if (type == null) return null; + return switch (SchematicType.getType(file)) { + case LITEMATIC -> LitematicFile.load(file); + case SCHEM -> SchemFile.load(file); + case NBT_STRUCTURE -> NBTStructureFile.load(file); + }; } static CompoundTag readRoot(Path file) throws IOException { @@ -61,8 +58,14 @@ static short tryGetShort(Tag tag) { return tag instanceof StringTag ? ((StringTag) tag).getValue() : null; } + SchematicType getType(); + @NotNull Path getFile(); + default int getVersion() { + return 0; + } + default int getMinecraftDataVersion() { return 0; } diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/schematic/SchematicFile.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/schematic/SchematicFile.java deleted file mode 100644 index 8fd23f9c0a..0000000000 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/schematic/SchematicFile.java +++ /dev/null @@ -1,78 +0,0 @@ -package org.jackhuang.hmcl.schematic; - -import com.github.steveice10.opennbt.tag.builtin.CompoundTag; -import com.github.steveice10.opennbt.tag.builtin.StringTag; -import com.github.steveice10.opennbt.tag.builtin.Tag; -import org.jackhuang.hmcl.util.Vec3i; -import org.jackhuang.hmcl.util.io.FileUtils; -import org.jetbrains.annotations.NotNull; -import org.jetbrains.annotations.Nullable; - -import java.io.IOException; -import java.nio.file.Files; -import java.nio.file.Path; - -import static org.jackhuang.hmcl.schematic.Schematic.tryGetShort; - -/// @author Calboot -/// @see Schematic File Format Wiki -/// @see Schematica -public final class SchematicFile implements Schematic { - - public static boolean isFileSchematic(Path path) { - return "schematic".equals(FileUtils.getExtension(path)) && Files.isRegularFile(path); - } - - public static SchematicFile load(Path file) throws IOException { - if (!isFileSchematic(file)) return null; - - CompoundTag root = Schematic.readRoot(file); - - Tag materialsTag = root.get("Materials"); - if (materialsTag == null) - throw new IOException("Materials tag not found"); - else if (!(materialsTag instanceof StringTag)) - throw new IOException("Materials tag is not an integer"); - - Tag widthTag = root.get("Width"); - Tag heightTag = root.get("Height"); - Tag lengthTag = root.get("Length"); - Vec3i enclosingSize = null; - if (widthTag != null && heightTag != null && lengthTag != null) { - short width = tryGetShort(widthTag); - short height = tryGetShort(heightTag); - short length = tryGetShort(lengthTag); - if (width >= 0 && height >= 0 && length >= 0) { - enclosingSize = new Vec3i(width, height, length); - } - } - - return new SchematicFile(file, ((StringTag) materialsTag).getValue(), enclosingSize); - } - - private final Path file; - private final String materials; - private final Vec3i enclosingSize; - - private SchematicFile(Path file, String materials, Vec3i enclosingSize) { - this.file = file; - this.materials = materials; - this.enclosingSize = enclosingSize; - } - - @Override - public @NotNull Path getFile() { - return file; - } - - @Override - public String getMinecraftVersion() { - return materials; - } - - @Override - public @Nullable Vec3i getEnclosingSize() { - return enclosingSize; - } - -} diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/schematic/SchematicType.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/schematic/SchematicType.java new file mode 100644 index 0000000000..71cea9764a --- /dev/null +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/schematic/SchematicType.java @@ -0,0 +1,28 @@ +package org.jackhuang.hmcl.schematic; + +import org.jackhuang.hmcl.util.io.FileUtils; + +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.List; + +public enum SchematicType { + LITEMATIC("litematic"), + NBT_STRUCTURE("nbt"), + SCHEM("schematic", "schem"); + + public static SchematicType getType(Path file) { + if (file == null || !Files.isRegularFile(file)) return null; + String ext = FileUtils.getExtension(file); + for (SchematicType type : values()) { + if (type.extensions.contains(ext)) return type; + } + return null; + } + + public final List extensions; + + SchematicType(String... ext) { + this.extensions = List.of(ext); + } +} diff --git a/HMCLCore/src/test/java/org/jackhuang/hmcl/schematic/SchematicTest.java b/HMCLCore/src/test/java/org/jackhuang/hmcl/schematic/SchematicTest.java index d09a35323b..775d8079e7 100644 --- a/HMCLCore/src/test/java/org/jackhuang/hmcl/schematic/SchematicTest.java +++ b/HMCLCore/src/test/java/org/jackhuang/hmcl/schematic/SchematicTest.java @@ -39,31 +39,49 @@ private static Schematic load(String name) throws IOException, URISyntaxExceptio @Test public void test() throws Exception { - LitematicFile lFile = (LitematicFile) load("/schematics/test.litematic"); - assertEquals("刷石机一桶岩浆下推爆破8.3万每小时", lFile.getName()); - assertEquals("hsds", lFile.getAuthor()); - assertEquals("", lFile.getDescription()); - assertEquals(Instant.ofEpochMilli(1746443586433L), lFile.getTimeCreated()); - assertEquals(Instant.ofEpochMilli(1746443586433L), lFile.getTimeModified()); - assertEquals(1334, lFile.getTotalBlocks()); - assertEquals(5746, lFile.getTotalVolume()); - assertEquals(new Vec3i(17, 26, 13), lFile.getEnclosingSize()); - assertEquals(1, lFile.getRegionCount()); - assertEquals(4325, lFile.getMinecraftDataVersion()); - assertEquals("4325", lFile.getMinecraftVersion()); - assertEquals(7, lFile.getVersion()); + { + LitematicFile lFile = (LitematicFile) load("/schematics/test.litematic"); + assertEquals(SchematicType.LITEMATIC, lFile.getType()); + assertEquals("刷石机一桶岩浆下推爆破8.3万每小时", lFile.getName()); + assertEquals("hsds", lFile.getAuthor()); + assertEquals("", lFile.getDescription()); + assertEquals(Instant.ofEpochMilli(1746443586433L), lFile.getTimeCreated()); + assertEquals(Instant.ofEpochMilli(1746443586433L), lFile.getTimeModified()); + assertEquals(1334, lFile.getTotalBlocks()); + assertEquals(5746, lFile.getTotalVolume()); + assertEquals(new Vec3i(17, 26, 13), lFile.getEnclosingSize()); + assertEquals(1, lFile.getRegionCount()); + assertEquals(4325, lFile.getMinecraftDataVersion()); + assertEquals("4325", lFile.getMinecraftVersion()); + assertEquals(7, lFile.getVersion()); + } - SchematicFile sFile = (SchematicFile) load("/schematics/test.schematic"); - assertEquals("test", sFile.getName()); - assertEquals(new Vec3i(28, 35, 18), sFile.getEnclosingSize()); - assertEquals(17640, sFile.getTotalVolume()); - assertEquals("Alpha", sFile.getMinecraftVersion()); + { + SchemFile sFile = (SchemFile) load("/schematics/test.schematic"); + assertEquals(SchematicType.SCHEM, sFile.getType()); + assertEquals("test", sFile.getName()); + assertEquals(new Vec3i(28, 35, 18), sFile.getEnclosingSize()); + assertEquals(17640, sFile.getTotalVolume()); + assertEquals("Alpha", sFile.getMinecraftVersion()); + } - NBTStructureFile nFile = (NBTStructureFile) load("/schematics/test.nbt"); - assertEquals("test", nFile.getName()); - assertEquals(new Vec3i(9, 11, 13), nFile.getEnclosingSize()); - assertEquals(1287, nFile.getTotalVolume()); - assertEquals(3465, nFile.getMinecraftDataVersion()); - assertEquals("3465", nFile.getMinecraftVersion()); + { + SchemFile sFileSponge = (SchemFile) load("/schematics/test.schem"); + assertEquals(SchematicType.SCHEM, sFileSponge.getType()); + assertEquals("test", sFileSponge.getName()); + assertEquals(3465, sFileSponge.getMinecraftDataVersion()); + assertEquals("3465", sFileSponge.getMinecraftVersion()); + assertEquals(new Vec3i(9, 5, 9), sFileSponge.getEnclosingSize()); + } + + { + NBTStructureFile nFile = (NBTStructureFile) load("/schematics/test.nbt"); + assertEquals(SchematicType.NBT_STRUCTURE, nFile.getType()); + assertEquals("test", nFile.getName()); + assertEquals(new Vec3i(9, 11, 13), nFile.getEnclosingSize()); + assertEquals(1287, nFile.getTotalVolume()); + assertEquals(3465, nFile.getMinecraftDataVersion()); + assertEquals("3465", nFile.getMinecraftVersion()); + } } } diff --git a/HMCLCore/src/test/resources/schematics/test.schem b/HMCLCore/src/test/resources/schematics/test.schem new file mode 100644 index 0000000000000000000000000000000000000000..ae4c82196285a99e2cc074cfa38c6a328fb84e1c GIT binary patch literal 300 zcmV+{0n`2;iwFP!00000|E*HnO2a@DoMvs)Bm{i$YkU+73KjzetG<+Na?&j30-HtZ zC-?#Wf}iK34r7MVd`AUNkIxRQq-WVbk$QdClaAmp&m*RwDHiFTEMU yxlDG07eGXnH3)0^KAgHptD?1XUiwp&*wh6Wtb^1#mVEuj=M`VmxKxrC0{{Ra5Q@40 literal 0 HcmV?d00001 From 3a3064708ee28feb28b174e0d422f6307a58f75d Mon Sep 17 00:00:00 2001 From: Calboot Date: Mon, 2 Feb 2026 14:17:03 +0800 Subject: [PATCH 19/53] update --- .../hmcl/ui/versions/SchematicsPage.java | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/SchematicsPage.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/SchematicsPage.java index 4c7ea09a6c..ca797cbeeb 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/SchematicsPage.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/SchematicsPage.java @@ -29,10 +29,7 @@ import javafx.geometry.Pos; import javafx.scene.Group; import javafx.scene.Node; -import javafx.scene.control.Label; -import javafx.scene.control.Skin; -import javafx.scene.control.SkinBase; -import javafx.scene.control.Tooltip; +import javafx.scene.control.*; import javafx.scene.image.Image; import javafx.scene.image.ImageView; import javafx.scene.image.PixelWriter; @@ -555,10 +552,14 @@ private void updateContent(Schematic file) { { this.details = new ComponentList(); + details.setStyle("-fx-effect: null;"); StackPane detailsContainer = new StackPane(); - detailsContainer.setPadding(new Insets(10, 0, 0, 0)); detailsContainer.getChildren().add(details); - setBody(detailsContainer); + ScrollPane scrollPane = new ScrollPane(detailsContainer); + scrollPane.setStyle("-fx-effect: dropshadow(gaussian, rgba(0, 0, 0, 0.26), 5, 0.06, -0.5, 1);"); + scrollPane.setFitToWidth(true); + StackPane.setMargin(scrollPane, new Insets(10, 0, 0, 0)); + setBody(scrollPane); } { @@ -571,6 +572,9 @@ private void updateContent(Schematic file) { onEscPressed(this, okButton::fire); } + this.prefWidthProperty().bind(Controllers.getStage().widthProperty().multiply(0.6)); + this.maxHeightProperty().bind(Controllers.getStage().heightProperty().multiply(0.8)); + updateContent(file); } } From 6733a58bc146179ac7c76bb36376ee20e8795363 Mon Sep 17 00:00:00 2001 From: Calboot Date: Mon, 2 Feb 2026 16:09:20 +0800 Subject: [PATCH 20/53] =?UTF-8?q?=E6=B7=BB=E5=8A=A0=E6=8A=95=E5=BD=B1?= =?UTF-8?q?=E6=8F=90=E7=A4=BA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../hmcl/ui/versions/DownloadListPage.java | 4 + .../hmcl/ui/versions/SchematicsPage.java | 91 +++++++++++++++---- .../resources/assets/lang/I18N.properties | 2 + .../resources/assets/lang/I18N_zh.properties | 2 + .../assets/lang/I18N_zh_CN.properties | 2 + 5 files changed, 81 insertions(+), 20 deletions(-) diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/DownloadListPage.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/DownloadListPage.java index 6769981388..4001fcd0d6 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/DownloadListPage.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/DownloadListPage.java @@ -154,6 +154,10 @@ public void selectVersion(String versionID) { FXUtils.runInFX(() -> selectedVersion.set(versionID)); } + public DownloadPage.DownloadCallback getCallback() { + return callback; + } + private void search(String userGameVersion, RemoteModRepository.Category category, int pageOffset, String searchFilter, RemoteModRepository.SortType sort) { retrySearch = null; setLoading(true); diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/SchematicsPage.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/SchematicsPage.java index ca797cbeeb..0e631a50b4 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/SchematicsPage.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/SchematicsPage.java @@ -44,6 +44,9 @@ import javafx.scene.text.Text; import javafx.scene.text.TextFlow; import javafx.stage.FileChooser; +import org.jackhuang.hmcl.mod.LocalModFile; +import org.jackhuang.hmcl.mod.RemoteMod; +import org.jackhuang.hmcl.mod.modrinth.ModrinthRemoteModRepository; import org.jackhuang.hmcl.schematic.LitematicFile; import org.jackhuang.hmcl.schematic.Schematic; import org.jackhuang.hmcl.schematic.SchematicType; @@ -81,6 +84,8 @@ */ public final class SchematicsPage extends ListPageBase implements VersionPage.VersionLoadable { + private static RemoteMod litematica; + private static String translateAuthorName(String author) { if (I18n.isUseChinese() && "hsds".equals(author)) { return "黑山大叔"; @@ -92,6 +97,8 @@ private static String translateType(SchematicType type) { return i18n("schematics.info.type." + type.name().toLowerCase(Locale.ROOT)); } + private Profile profile; + private String instanceId; private Path schematicsDirectory; private final ObjectProperty currentDirectory = new SimpleObjectProperty<>(this, "currentDirectory", null); @@ -111,6 +118,8 @@ protected Skin createDefaultSkin() { @Override public void loadVersion(Profile profile, String version) { + this.profile = profile; + this.instanceId = version; this.schematicsDirectory = profile.getRepository().getSchematicsDirectory(version); refresh(); @@ -134,30 +143,70 @@ public void refresh() { if (schematicsDirectory == null) return; setLoading(true); - Task.supplyAsync(() -> loadRoot(schematicsDirectory)) - .whenComplete(Schedulers.javafx(), (result, exception) -> { - setLoading(false); - if (exception == null) { - DirItem target = result; - if (currentDirectoryProperty().get() != null) { - loop: - for (String dirName : currentDirectoryProperty().get().relativePath) { - target.preLoad(); - for (var dirChild : target.dirChildren) { - if (dirChild.getName().equals(dirName)) { - target = dirChild; - continue loop; - } + Task.supplyAsync(() -> { + boolean hasLitematica = false; + try { + if (profile.getRepository() + .getModManager(instanceId) + .getMods() + .stream() + .map(LocalModFile::getId) + .toList() + .contains("litematica") + ) { + hasLitematica = true; + } + } catch (IOException e) { + LOG.warning("Failed to load mods, unable to check litematica", e); + } + if (!hasLitematica) { + try { + if (litematica == null) litematica = ModrinthRemoteModRepository.MODS.getModById("litematica"); + } catch (IOException ignored) { + } + return null; + } + return loadRoot(schematicsDirectory); + }).whenComplete(Schedulers.javafx(), (result, exception) -> { + if (exception == null) { + if (result == null) { + if (litematica != null) { + setFailedReason(i18n("schematics.no_litematica_install")); + setOnFailedAction(__ -> FXUtils.runInFX(() -> { + if (litematica != null) { + DownloadListPage modDownloads = Controllers.getDownloadPage().showModDownloads(); + modDownloads.selectVersion(instanceId); + Controllers.navigate(new DownloadPage(modDownloads, litematica, modDownloads.getProfileVersion(), modDownloads.getCallback())); + } + })); + } else { + setFailedReason(i18n("schematics.no_litematica")); + setOnFailedAction(null); + } + } else { + DirItem target = result; + if (currentDirectoryProperty().get() != null) { + loop: + for (String dirName : currentDirectoryProperty().get().relativePath) { + target.preLoad(); + for (var dirChild : target.dirChildren) { + if (dirChild.getName().equals(dirName)) { + target = dirChild; + continue loop; } - break; } + break; } - - navigateTo(target); - } else { - LOG.warning("Failed to load schematics", exception); } - }).start(); + + setFailedReason(null); + navigateTo(target); + } + setLoading(false); + } else { + LOG.warning("Failed to load schematics", exception); + } + }).start(); } public void addFiles(List files) { @@ -742,6 +791,8 @@ private static final class SchematicsPageSkin extends SkinBase { ComponentList.setVgrow(center, Priority.ALWAYS); center.getStyleClass().add("large-spinner-pane"); center.loadingProperty().bind(skinnable.loadingProperty()); + center.failedReasonProperty().bind(skinnable.failedReasonProperty()); + center.onFailedActionProperty().bind(skinnable.onFailedActionProperty()); listView.setCellFactory(x -> new Cell(listView)); listView.setSelectionModel(new NoneMultipleSelectionModel<>()); diff --git a/HMCL/src/main/resources/assets/lang/I18N.properties b/HMCL/src/main/resources/assets/lang/I18N.properties index 96f870d7f4..e651cbae2d 100644 --- a/HMCL/src/main/resources/assets/lang/I18N.properties +++ b/HMCL/src/main/resources/assets/lang/I18N.properties @@ -1283,6 +1283,8 @@ schematics.info.type.schem=Schem (Schematica, WE, MCEdit) schematics.info.version=Schematic Version schematics.info.mc_version=Minecraft Version schematics.manage=Schematics +schematics.no_litematica=Litematica not installed +schematics.no_litematica_install=Litematica not installed\nClick to install schematics.sub_items=%d sub-item(s) search=Search diff --git a/HMCL/src/main/resources/assets/lang/I18N_zh.properties b/HMCL/src/main/resources/assets/lang/I18N_zh.properties index 963d16f0f3..3626768490 100644 --- a/HMCL/src/main/resources/assets/lang/I18N_zh.properties +++ b/HMCL/src/main/resources/assets/lang/I18N_zh.properties @@ -1068,6 +1068,8 @@ schematics.info.type.schem=Schem (Schematica, WE, MCEdit) schematics.info.version=原理圖版本 schematics.info.mc_version=MC版本 schematics.manage=原理圖管理 +schematics.no_litematica=未安裝 Litematica +schematics.no_litematica_install=未安裝 Litematica\n點選以安裝 schematics.sub_items=%d 個子項 search=搜尋 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 48269a03a9..693f1aefe2 100644 --- a/HMCL/src/main/resources/assets/lang/I18N_zh_CN.properties +++ b/HMCL/src/main/resources/assets/lang/I18N_zh_CN.properties @@ -1073,6 +1073,8 @@ schematics.info.type.schem=Schem (Schematica, WE, MCEdit) schematics.info.version=原理图版本 schematics.info.mc_version=MC版本 schematics.manage=原理图管理 +schematics.no_litematica=未安装 Litematica +schematics.no_litematica_install=未安装 Litematica\n点击以安装 schematics.sub_items=%d 个子项 search=搜索 From c1b8336f17aeba864eb3c22f079772484f5d13e0 Mon Sep 17 00:00:00 2001 From: Calboot Date: Mon, 2 Feb 2026 16:57:06 +0800 Subject: [PATCH 21/53] forgematica --- .../hmcl/ui/versions/ModListPage.java | 51 +--------------- .../hmcl/ui/versions/SchematicsPage.java | 41 ++++++++----- .../resources/assets/lang/I18N.properties | 4 +- .../resources/assets/lang/I18N_zh.properties | 4 +- .../assets/lang/I18N_zh_CN.properties | 4 +- .../org/jackhuang/hmcl/mod/ModManager.java | 60 ++++++++++++++++++- 6 files changed, 95 insertions(+), 69 deletions(-) diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/ModListPage.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/ModListPage.java index 6411333012..f9471fc606 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/ModListPage.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/ModListPage.java @@ -57,7 +57,6 @@ public final class ModListPage extends ListPageBase supportedLoaders = EnumSet.noneOf(ModLoaderType.class); @@ -90,7 +89,7 @@ public void loadVersion(Profile profile, String id) { HMCLGameRepository repository = profile.getRepository(); Version resolved = repository.getResolvedPreservingPatchesVersion(id); - this.gameVersion = repository.getGameVersion(resolved).orElse(null); + String gameVersion = repository.getGameVersion(resolved).orElse(null); LibraryAnalyzer analyzer = LibraryAnalyzer.analyze(resolved, gameVersion); modded.set(analyzer.hasModLoader()); loadMods(profile.getRepository().getModManager(id)); @@ -111,7 +110,8 @@ private void loadMods(ModManager modManager) { lock.unlock(); } }, Schedulers.io()).whenCompleteAsync((list, exception) -> { - updateSupportedLoaders(modManager); + supportedLoaders.clear(); + supportedLoaders.addAll(modManager.getSupportedLoaders()); if (exception == null) { getItems().setAll(list); @@ -123,51 +123,6 @@ private void loadMods(ModManager modManager) { }, Schedulers.javafx()); } - private void updateSupportedLoaders(ModManager modManager) { - supportedLoaders.clear(); - - LibraryAnalyzer analyzer = modManager.getLibraryAnalyzer(); - if (analyzer == null) { - Collections.addAll(supportedLoaders, ModLoaderType.values()); - return; - } - - for (LibraryAnalyzer.LibraryType type : LibraryAnalyzer.LibraryType.values()) { - if (type.isModLoader() && analyzer.has(type)) { - ModLoaderType modLoaderType = type.getModLoaderType(); - if (modLoaderType != null) { - supportedLoaders.add(modLoaderType); - - if (modLoaderType == ModLoaderType.CLEANROOM) - supportedLoaders.add(ModLoaderType.FORGE); - } - } - } - - if (analyzer.has(LibraryAnalyzer.LibraryType.NEO_FORGE) && "1.20.1".equals(gameVersion)) { - supportedLoaders.add(ModLoaderType.FORGE); - } - - if (analyzer.has(LibraryAnalyzer.LibraryType.QUILT)) { - supportedLoaders.add(ModLoaderType.FABRIC); - } - - if (analyzer.has(LibraryAnalyzer.LibraryType.LEGACY_FABRIC)) { - supportedLoaders.add(ModLoaderType.FABRIC); - } - - if (analyzer.has(LibraryAnalyzer.LibraryType.FABRIC) && modManager.hasMod("kilt", ModLoaderType.FABRIC)) { - supportedLoaders.add(ModLoaderType.FORGE); - supportedLoaders.add(ModLoaderType.NEO_FORGED); - } - - // Sinytra Connector - if (analyzer.has(LibraryAnalyzer.LibraryType.NEO_FORGE) && modManager.hasMod("connectormod", ModLoaderType.NEO_FORGED) - || "1.20.1".equals(gameVersion) && analyzer.has(LibraryAnalyzer.LibraryType.FORGE) && modManager.hasMod("connectormod", ModLoaderType.FORGE)) { - supportedLoaders.add(ModLoaderType.FABRIC); - } - } - public void add() { FileChooser chooser = new FileChooser(); chooser.setTitle(i18n("mods.choose_mod")); diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/SchematicsPage.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/SchematicsPage.java index 0e631a50b4..192ea3c96d 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/SchematicsPage.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/SchematicsPage.java @@ -45,6 +45,7 @@ import javafx.scene.text.TextFlow; import javafx.stage.FileChooser; import org.jackhuang.hmcl.mod.LocalModFile; +import org.jackhuang.hmcl.mod.ModLoaderType; import org.jackhuang.hmcl.mod.RemoteMod; import org.jackhuang.hmcl.mod.modrinth.ModrinthRemoteModRepository; import org.jackhuang.hmcl.schematic.LitematicFile; @@ -62,6 +63,7 @@ import org.jackhuang.hmcl.util.StringUtils; import org.jackhuang.hmcl.util.i18n.I18n; import org.jackhuang.hmcl.util.io.FileUtils; +import org.jackhuang.hmcl.util.versioning.GameVersionNumber; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; @@ -71,6 +73,7 @@ import java.nio.file.NoSuchFileException; import java.nio.file.Path; import java.util.*; +import java.util.stream.Collectors; import java.util.stream.Stream; import static org.jackhuang.hmcl.ui.FXUtils.ignoreEvent; @@ -85,6 +88,7 @@ public final class SchematicsPage extends ListPageBase implements VersionPage.VersionLoadable { private static RemoteMod litematica; + private static RemoteMod forgematica; private static String translateAuthorName(String author) { if (I18n.isUseChinese() && "hsds".equals(author)) { @@ -143,17 +147,16 @@ public void refresh() { if (schematicsDirectory == null) return; setLoading(true); + var modManager = profile.getRepository().getModManager(instanceId); Task.supplyAsync(() -> { boolean hasLitematica = false; try { - if (profile.getRepository() - .getModManager(instanceId) - .getMods() + modManager.refreshMods(); + var set = modManager.getMods() .stream() .map(LocalModFile::getId) - .toList() - .contains("litematica") - ) { + .collect(Collectors.toSet()); + if (set.contains("litematica") || set.contains("forgematica")) { hasLitematica = true; } } catch (IOException e) { @@ -164,21 +167,31 @@ public void refresh() { if (litematica == null) litematica = ModrinthRemoteModRepository.MODS.getModById("litematica"); } catch (IOException ignored) { } + try { + if (forgematica == null) forgematica = ModrinthRemoteModRepository.MODS.getModById("forgematica"); + } catch (IOException ignored) { + } return null; } return loadRoot(schematicsDirectory); }).whenComplete(Schedulers.javafx(), (result, exception) -> { if (exception == null) { if (result == null) { - if (litematica != null) { + boolean useForgematica = forgematica != null + && (modManager.getSupportedLoaders().contains(ModLoaderType.FORGE) || modManager.getSupportedLoaders().contains(ModLoaderType.NEO_FORGED)) + && GameVersionNumber.asGameVersion(Optional.ofNullable(modManager.getGameVersion())).isAtLeast("1.16.4", "20w45a"); + if (useForgematica || litematica != null) { setFailedReason(i18n("schematics.no_litematica_install")); - setOnFailedAction(__ -> FXUtils.runInFX(() -> { - if (litematica != null) { - DownloadListPage modDownloads = Controllers.getDownloadPage().showModDownloads(); - modDownloads.selectVersion(instanceId); - Controllers.navigate(new DownloadPage(modDownloads, litematica, modDownloads.getProfileVersion(), modDownloads.getCallback())); - } - })); + setOnFailedAction(__ -> { + var modDownloads = Controllers.getDownloadPage().showModDownloads(); + modDownloads.selectVersion(instanceId); + Controllers.navigate(new DownloadPage( + modDownloads, + useForgematica ? forgematica : litematica, + modDownloads.getProfileVersion(), + modDownloads.getCallback()) + ); + }); } else { setFailedReason(i18n("schematics.no_litematica")); setOnFailedAction(null); diff --git a/HMCL/src/main/resources/assets/lang/I18N.properties b/HMCL/src/main/resources/assets/lang/I18N.properties index e651cbae2d..b242568706 100644 --- a/HMCL/src/main/resources/assets/lang/I18N.properties +++ b/HMCL/src/main/resources/assets/lang/I18N.properties @@ -1283,8 +1283,8 @@ schematics.info.type.schem=Schem (Schematica, WE, MCEdit) schematics.info.version=Schematic Version schematics.info.mc_version=Minecraft Version schematics.manage=Schematics -schematics.no_litematica=Litematica not installed -schematics.no_litematica_install=Litematica not installed\nClick to install +schematics.no_litematica=Litematica or Forgematica not installed +schematics.no_litematica_install=Litematica or Forgematica not installed\nClick to install schematics.sub_items=%d sub-item(s) search=Search diff --git a/HMCL/src/main/resources/assets/lang/I18N_zh.properties b/HMCL/src/main/resources/assets/lang/I18N_zh.properties index 3626768490..58decf57b3 100644 --- a/HMCL/src/main/resources/assets/lang/I18N_zh.properties +++ b/HMCL/src/main/resources/assets/lang/I18N_zh.properties @@ -1068,8 +1068,8 @@ schematics.info.type.schem=Schem (Schematica, WE, MCEdit) schematics.info.version=原理圖版本 schematics.info.mc_version=MC版本 schematics.manage=原理圖管理 -schematics.no_litematica=未安裝 Litematica -schematics.no_litematica_install=未安裝 Litematica\n點選以安裝 +schematics.no_litematica=未安裝 Litematica 或 Forgematica +schematics.no_litematica_install=未安裝 Litematica 或 Forgematica\n點選以安裝 schematics.sub_items=%d 個子項 search=搜尋 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 693f1aefe2..064c843602 100644 --- a/HMCL/src/main/resources/assets/lang/I18N_zh_CN.properties +++ b/HMCL/src/main/resources/assets/lang/I18N_zh_CN.properties @@ -1073,8 +1073,8 @@ schematics.info.type.schem=Schem (Schematica, WE, MCEdit) schematics.info.version=原理图版本 schematics.info.mc_version=MC版本 schematics.manage=原理图管理 -schematics.no_litematica=未安装 Litematica -schematics.no_litematica_install=未安装 Litematica\n点击以安装 +schematics.no_litematica=未安装 Litematica 或 Forgematica +schematics.no_litematica_install=未安装 Litematica 或 Forgematica\n点击以安装 schematics.sub_items=%d 个子项 search=搜索 diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/ModManager.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/ModManager.java index 6297e49e94..47ee4ef676 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/ModManager.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/ModManager.java @@ -63,8 +63,10 @@ private interface ModMetadataReader { private final GameRepository repository; private final String id; + private String gameVersion; private final TreeSet localModFiles = new TreeSet<>(); private final HashMap, LocalMod> localMods = new HashMap<>(); + private final EnumSet supportedLoaders = EnumSet.noneOf(ModLoaderType.class); private LibraryAnalyzer analyzer; private boolean loaded = false; @@ -99,6 +101,14 @@ public boolean hasMod(String modId, ModLoaderType modLoaderType) { return localMods.containsKey(pair(modId, modLoaderType)); } + public String getGameVersion() { + return gameVersion; + } + + public EnumSet getSupportedLoaders() { + return EnumSet.copyOf(supportedLoaders); + } + private void addModInfo(Path file) { String fileName = StringUtils.removeSuffix(FileUtils.getName(file), DISABLED_EXTENSION, OLD_EXTENSION); String extension = fileName.substring(fileName.lastIndexOf(".") + 1); @@ -174,7 +184,11 @@ public void refreshMods() throws IOException { localModFiles.clear(); localMods.clear(); - analyzer = LibraryAnalyzer.analyze(getRepository().getResolvedPreservingPatchesVersion(id), null); + var resolved = getRepository().getResolvedPreservingPatchesVersion(id); + gameVersion = repository.getGameVersion(resolved).orElse(null); + analyzer = LibraryAnalyzer.analyze(resolved, gameVersion); + + updateSupportedLoaders(); boolean supportSubfolders = analyzer.has(LibraryAnalyzer.LibraryType.FORGE) || analyzer.has(LibraryAnalyzer.LibraryType.QUILT); @@ -225,6 +239,50 @@ public void removeMods(LocalModFile... localModFiles) throws IOException { } } + private void updateSupportedLoaders() { + supportedLoaders.clear(); + + if (this.analyzer == null) { + Collections.addAll(supportedLoaders, ModLoaderType.values()); + return; + } + + for (LibraryAnalyzer.LibraryType type : LibraryAnalyzer.LibraryType.values()) { + if (type.isModLoader() && this.analyzer.has(type)) { + ModLoaderType modLoaderType = type.getModLoaderType(); + if (modLoaderType != null) { + supportedLoaders.add(modLoaderType); + + if (modLoaderType == ModLoaderType.CLEANROOM) + supportedLoaders.add(ModLoaderType.FORGE); + } + } + } + + if (this.analyzer.has(LibraryAnalyzer.LibraryType.NEO_FORGE) && "1.20.1".equals(gameVersion)) { + supportedLoaders.add(ModLoaderType.FORGE); + } + + if (this.analyzer.has(LibraryAnalyzer.LibraryType.QUILT)) { + supportedLoaders.add(ModLoaderType.FABRIC); + } + + if (this.analyzer.has(LibraryAnalyzer.LibraryType.LEGACY_FABRIC)) { + supportedLoaders.add(ModLoaderType.FABRIC); + } + + if (this.analyzer.has(LibraryAnalyzer.LibraryType.FABRIC) && hasMod("kilt", ModLoaderType.FABRIC)) { + supportedLoaders.add(ModLoaderType.FORGE); + supportedLoaders.add(ModLoaderType.NEO_FORGED); + } + + // Sinytra Connector + if (this.analyzer.has(LibraryAnalyzer.LibraryType.NEO_FORGE) && hasMod("connectormod", ModLoaderType.NEO_FORGED) + || "1.20.1".equals(gameVersion) && this.analyzer.has(LibraryAnalyzer.LibraryType.FORGE) && hasMod("connectormod", ModLoaderType.FORGE)) { + supportedLoaders.add(ModLoaderType.FABRIC); + } + } + public void rollback(LocalModFile from, LocalModFile to) throws IOException { if (!loaded) { throw new IllegalStateException("ModManager Not loaded"); From e080a05b0620e9acfa319884239eb7947c9cd754 Mon Sep 17 00:00:00 2001 From: Calboot Date: Mon, 2 Feb 2026 16:59:09 +0800 Subject: [PATCH 22/53] Apply i18n suggestions Co-authored-by: 3gf8jv4dv <3gf8jv4dv@gmail.com> --- HMCL/src/main/resources/assets/lang/I18N_zh.properties | 2 +- HMCL/src/main/resources/assets/lang/I18N_zh_CN.properties | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/HMCL/src/main/resources/assets/lang/I18N_zh.properties b/HMCL/src/main/resources/assets/lang/I18N_zh.properties index 58decf57b3..6a4af3b341 100644 --- a/HMCL/src/main/resources/assets/lang/I18N_zh.properties +++ b/HMCL/src/main/resources/assets/lang/I18N_zh.properties @@ -1066,7 +1066,7 @@ schematics.info.type.litematic=Litematic (Litematica) schematics.info.type.nbt_structure=NBT 結構 (原版) schematics.info.type.schem=Schem (Schematica, WE, MCEdit) schematics.info.version=原理圖版本 -schematics.info.mc_version=MC版本 +schematics.info.mc_version=Minecraft 版本 schematics.manage=原理圖管理 schematics.no_litematica=未安裝 Litematica 或 Forgematica schematics.no_litematica_install=未安裝 Litematica 或 Forgematica\n點選以安裝 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 064c843602..354e8402f3 100644 --- a/HMCL/src/main/resources/assets/lang/I18N_zh_CN.properties +++ b/HMCL/src/main/resources/assets/lang/I18N_zh_CN.properties @@ -1071,7 +1071,7 @@ schematics.info.type.litematic=Litematic (Litematica) schematics.info.type.nbt_structure=NBT 结构 (原版) schematics.info.type.schem=Schem (Schematica, WE, MCEdit) schematics.info.version=原理图版本 -schematics.info.mc_version=MC版本 +schematics.info.mc_version=Minecraft 版本 schematics.manage=原理图管理 schematics.no_litematica=未安装 Litematica 或 Forgematica schematics.no_litematica_install=未安装 Litematica 或 Forgematica\n点击以安装 From ad468af72ffe5924fbec2180d1c410464b3e9acc Mon Sep 17 00:00:00 2001 From: Calboot Date: Mon, 2 Feb 2026 17:08:28 +0800 Subject: [PATCH 23/53] =?UTF-8?q?=E5=89=AF=E7=89=88=E6=9C=AC=E5=8F=B7?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../org/jackhuang/hmcl/ui/versions/SchematicsPage.java | 4 +++- .../java/org/jackhuang/hmcl/schematic/LitematicFile.java | 9 ++++++--- .../java/org/jackhuang/hmcl/schematic/Schematic.java | 5 +++++ .../java/org/jackhuang/hmcl/schematic/SchematicTest.java | 3 +++ 4 files changed, 17 insertions(+), 4 deletions(-) diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/SchematicsPage.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/SchematicsPage.java index 192ea3c96d..b9318957f9 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/SchematicsPage.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/SchematicsPage.java @@ -596,7 +596,9 @@ private void updateContent(Schematic file) { if (StringUtils.isNotBlank(file.getMinecraftVersion())) addDetailItem(i18n("schematics.info.mc_version"), file.getMinecraftVersion()); if (file.getVersion() > 0) - addDetailItem(i18n("schematics.info.version"), file.getVersion()); + addDetailItem(i18n("schematics.info.version"), + file.getSubVersion().isPresent() ? "%d.%d".formatted(file.getVersion(), file.getSubVersion().getAsInt()) : file.getVersion() + ); } SchematicInfoDialog() { diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/schematic/LitematicFile.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/schematic/LitematicFile.java index bddffcd2fd..7ff014d57e 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/schematic/LitematicFile.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/schematic/LitematicFile.java @@ -24,6 +24,7 @@ import java.io.IOException; import java.nio.file.Path; import java.time.Instant; +import java.util.OptionalInt; import static org.jackhuang.hmcl.schematic.Schematic.*; @@ -54,9 +55,10 @@ else if (!(metadataTag instanceof CompoundTag)) if (regionsTag instanceof CompoundTag) regions = ((CompoundTag) regionsTag).size(); + Tag tag = root.get("SubVersion"); return new LitematicFile(file, (CompoundTag) metadataTag, ((IntTag) versionTag).getValue(), - tryGetInt(root.get("SubVersion")), + tag instanceof IntTag ? ((IntTag) tag).getValue() : -1, tryGetInt(root.get("MinecraftDataVersion")), regions ); @@ -130,8 +132,9 @@ public int getVersion() { return version; } - public int getSubVersion() { - return subVersion; + @Override + public OptionalInt getSubVersion() { + return subVersion >= 0 ? OptionalInt.of(subVersion) : OptionalInt.empty(); } @Override diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/schematic/Schematic.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/schematic/Schematic.java index 63f2cd30d8..c596148c2d 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/schematic/Schematic.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/schematic/Schematic.java @@ -12,6 +12,7 @@ import java.nio.file.Files; import java.nio.file.Path; import java.time.Instant; +import java.util.OptionalInt; import java.util.zip.GZIPInputStream; public sealed interface Schematic permits LitematicFile, SchemFile, NBTStructureFile { @@ -66,6 +67,10 @@ default int getVersion() { return 0; } + default OptionalInt getSubVersion() { + return OptionalInt.empty(); + } + default int getMinecraftDataVersion() { return 0; } diff --git a/HMCLCore/src/test/java/org/jackhuang/hmcl/schematic/SchematicTest.java b/HMCLCore/src/test/java/org/jackhuang/hmcl/schematic/SchematicTest.java index 775d8079e7..35e2b519f8 100644 --- a/HMCLCore/src/test/java/org/jackhuang/hmcl/schematic/SchematicTest.java +++ b/HMCLCore/src/test/java/org/jackhuang/hmcl/schematic/SchematicTest.java @@ -28,6 +28,7 @@ import java.time.Instant; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; public final class SchematicTest { private static Schematic load(String name) throws IOException, URISyntaxException { @@ -54,6 +55,8 @@ public void test() throws Exception { assertEquals(4325, lFile.getMinecraftDataVersion()); assertEquals("4325", lFile.getMinecraftVersion()); assertEquals(7, lFile.getVersion()); + assertTrue(lFile.getSubVersion().isPresent()); + assertEquals(1, lFile.getSubVersion().getAsInt()); } { From 20ba1b8278f603b8e06ad3082a8a5ed43a5a87bd Mon Sep 17 00:00:00 2001 From: Calboot Date: Mon, 2 Feb 2026 17:13:13 +0800 Subject: [PATCH 24/53] update --- HMCLCore/src/main/java/org/jackhuang/hmcl/mod/ModAdviser.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/ModAdviser.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/ModAdviser.java index 837bdbcb02..57530cf532 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/ModAdviser.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/ModAdviser.java @@ -62,7 +62,6 @@ enum ModSuggestion { "irisUpdateInfo.json", // Iris "modernfix", // ModernFix "modtranslations", // Mod translations - "schematics", // Schematics mod "journeymap/data", // JourneyMap "mods/.connector" // Sinytra Connector ); @@ -73,6 +72,7 @@ enum ModSuggestion { "blueprints" /* BuildCraft */, "optionsof.txt" /* OptiFine */, "journeymap" /* JourneyMap */, + "schematics" /* Schematics mod */, "optionsshaders.txt", "mods" + File.separator + "VoxelMods"); From 5519f96cde22a86b6d5fa7a5222dd6250c4f6a77 Mon Sep 17 00:00:00 2001 From: Calboot Date: Mon, 2 Feb 2026 17:17:16 +0800 Subject: [PATCH 25/53] update --- .../java/org/jackhuang/hmcl/ui/versions/VersionPage.java | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) 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 7ea91bacb8..136da2d2f0 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 @@ -157,6 +157,10 @@ private void onBrowse(String sub) { FXUtils.openFolder(getProfile().getRepository().getRunDirectory(getVersion()).resolve(sub)); } + private void onBrowseSchematics() { + FXUtils.openFolder(getProfile().getRepository().getSchematicsDirectory(getVersion())); + } + private void redownloadAssetIndex() { Versions.updateGameAssets(getProfile(), getVersion()); } @@ -251,7 +255,7 @@ protected Skin(VersionPage control) { new IconedMenuItem(SVG.EXTENSION, i18n("folder.mod"), () -> control.onBrowse("mods"), browsePopup), new IconedMenuItem(SVG.TEXTURE, i18n("folder.resourcepacks"), () -> control.onBrowse("resourcepacks"), browsePopup), new IconedMenuItem(SVG.PUBLIC, i18n("folder.saves"), () -> control.onBrowse("saves"), browsePopup), - new IconedMenuItem(SVG.SCHEMA, i18n("folder.schematics"), () -> control.onBrowse("schematics"), browsePopup), + new IconedMenuItem(SVG.SCHEMA, i18n("folder.schematics"), control::onBrowseSchematics, browsePopup), new IconedMenuItem(SVG.WB_SUNNY, i18n("folder.shaderpacks"), () -> control.onBrowse("shaderpacks"), browsePopup), new IconedMenuItem(SVG.SCREENSHOT_MONITOR, i18n("folder.screenshots"), () -> control.onBrowse("screenshots"), browsePopup), new IconedMenuItem(SVG.SETTINGS, i18n("folder.config"), () -> control.onBrowse("config"), browsePopup), From 2515bb21921a70d138a2a5d2da5d6b2997aa626a Mon Sep 17 00:00:00 2001 From: Calboot Date: Mon, 2 Feb 2026 17:21:58 +0800 Subject: [PATCH 26/53] update --- .../jackhuang/hmcl/ui/versions/SchematicsPage.java | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/SchematicsPage.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/SchematicsPage.java index b9318957f9..f94eafb211 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/SchematicsPage.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/SchematicsPage.java @@ -704,10 +704,10 @@ public Cell(JFXListView listView) { if (item != null) item.onReveal(); }); - JFXButton btnEdit = new JFXButton(); - btnEdit.getStyleClass().add("toggle-icon4"); - btnEdit.setGraphic(SVG.EDIT.createIcon()); - btnEdit.setOnAction(event -> { + JFXButton btnExplore = new JFXButton(); + btnExplore.getStyleClass().add("toggle-icon4"); + btnExplore.setGraphic(SVG.EXPLORE.createIcon()); // Change the icon if allows editing + btnExplore.setOnAction(event -> { Item item = getItem(); if (item instanceof SchematicItem) { try { @@ -716,7 +716,7 @@ public Cell(JFXListView listView) { } } }); - btnEdit.visibleProperty().bind(isFileProperty); + btnExplore.visibleProperty().bind(isFileProperty); JFXButton btnDelete = new JFXButton(); btnDelete.getStyleClass().add("toggle-icon4"); @@ -729,7 +729,7 @@ public Cell(JFXListView listView) { } }); - right.getChildren().setAll(btnEdit, btnReveal, btnDelete); + right.getChildren().setAll(btnExplore, btnReveal, btnDelete); } box.getChildren().setAll(left, center, right); From 5af02ba872f1ddcaa65320a29df0843a66aaaf1d Mon Sep 17 00:00:00 2001 From: Calboot Date: Mon, 2 Feb 2026 17:39:31 +0800 Subject: [PATCH 27/53] update --- .../hmcl/schematic/LitematicFile.java | 58 ++++++----------- .../hmcl/schematic/NBTStructureFile.java | 28 +------- .../jackhuang/hmcl/schematic/SchemFile.java | 30 +-------- .../jackhuang/hmcl/schematic/Schematic.java | 64 ++++++++++++------- .../hmcl/schematic/SchematicTest.java | 9 ++- 5 files changed, 69 insertions(+), 120 deletions(-) diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/schematic/LitematicFile.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/schematic/LitematicFile.java index 7ff014d57e..7c28681fbf 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/schematic/LitematicFile.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/schematic/LitematicFile.java @@ -26,13 +26,11 @@ import java.time.Instant; import java.util.OptionalInt; -import static org.jackhuang.hmcl.schematic.Schematic.*; - /** * @author Glavo * @see The Litematic file format */ -public final class LitematicFile implements Schematic { +public final class LitematicFile extends Schematic { public static LitematicFile load(Path file) throws IOException { @@ -55,20 +53,30 @@ else if (!(metadataTag instanceof CompoundTag)) if (regionsTag instanceof CompoundTag) regions = ((CompoundTag) regionsTag).size(); + Vec3i enclosingSize = null; + Tag enclosingSizeTag = ((CompoundTag) metadataTag).get("EnclosingSize"); + if (enclosingSizeTag instanceof CompoundTag) { + CompoundTag list = (CompoundTag) enclosingSizeTag; + int x = tryGetInt(list.get("x")); + int y = tryGetInt(list.get("y")); + int z = tryGetInt(list.get("z")); + + if (x >= 0 && y >= 0 && z >= 0) + enclosingSize = new Vec3i(x, y, z); + } + Tag tag = root.get("SubVersion"); return new LitematicFile(file, (CompoundTag) metadataTag, ((IntTag) versionTag).getValue(), tag instanceof IntTag ? ((IntTag) tag).getValue() : -1, tryGetInt(root.get("MinecraftDataVersion")), - regions + regions, + enclosingSize ); } - private final @NotNull Path file; - private final int version; private final int subVersion; - private final int minecraftDataVersion; private final int regionCount; private final int[] previewImageData; private final String name; @@ -78,14 +86,12 @@ else if (!(metadataTag instanceof CompoundTag)) private final Instant timeModified; private final int totalBlocks; private final int totalVolume; - private final Vec3i enclosingSize; private LitematicFile(@NotNull Path file, @NotNull CompoundTag metadata, - int version, int subVersion, int minecraftDataVersion, int regionCount) { - this.file = file; + int version, int subVersion, int minecraftDataVersion, int regionCount, Vec3i enclosingSize) { + super(file, minecraftDataVersion, enclosingSize); this.version = version; this.subVersion = subVersion; - this.minecraftDataVersion = minecraftDataVersion; this.regionCount = regionCount; Tag previewImageData = metadata.get("PreviewImageData"); @@ -101,20 +107,6 @@ private LitematicFile(@NotNull Path file, @NotNull CompoundTag metadata, this.totalBlocks = tryGetInt(metadata.get("TotalBlocks")); this.totalVolume = tryGetInt(metadata.get("TotalVolume")); - - Vec3i enclosingSize = null; - Tag enclosingSizeTag = metadata.get("EnclosingSize"); - if (enclosingSizeTag instanceof CompoundTag) { - CompoundTag list = (CompoundTag) enclosingSizeTag; - int x = tryGetInt(list.get("x")); - int y = tryGetInt(list.get("y")); - int z = tryGetInt(list.get("z")); - - if (x >= 0 && y >= 0 && z >= 0) - enclosingSize = new Vec3i(x, y, z); - } - this.enclosingSize = enclosingSize; - } @Override @@ -122,11 +114,6 @@ public SchematicType getType() { return SchematicType.LITEMATIC; } - @Override - public @NotNull Path getFile() { - return file; - } - @Override public int getVersion() { return version; @@ -137,11 +124,6 @@ public OptionalInt getSubVersion() { return subVersion >= 0 ? OptionalInt.of(subVersion) : OptionalInt.empty(); } - @Override - public int getMinecraftDataVersion() { - return minecraftDataVersion; - } - public int[] getPreviewImageData() { return previewImageData != null ? previewImageData.clone() : null; } @@ -180,13 +162,9 @@ public int getTotalVolume() { return totalVolume; } - @Override - public Vec3i getEnclosingSize() { - return enclosingSize; - } - @Override public int getRegionCount() { return regionCount; } + } diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/schematic/NBTStructureFile.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/schematic/NBTStructureFile.java index e26e02e09a..bd8a6d715d 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/schematic/NBTStructureFile.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/schematic/NBTStructureFile.java @@ -2,18 +2,14 @@ import com.github.steveice10.opennbt.tag.builtin.*; import org.jackhuang.hmcl.util.Vec3i; -import org.jetbrains.annotations.NotNull; -import org.jetbrains.annotations.Nullable; import java.io.IOException; import java.nio.file.Path; import java.util.List; -import static org.jackhuang.hmcl.schematic.Schematic.tryGetInt; - /// @author Calboot /// @see Structure File -public final class NBTStructureFile implements Schematic { +public final class NBTStructureFile extends Schematic { public static NBTStructureFile load(Path file) throws IOException { @@ -49,14 +45,8 @@ else if (!(sizeTag instanceof ListTag)) return new NBTStructureFile(file, ((IntTag) dataVersionTag).getValue(), enclosingSize); } - private final Path file; - private final int dataVersion; - private final Vec3i enclosingSize; - private NBTStructureFile(Path file, int dataVersion, Vec3i enclosingSize) { - this.file = file; - this.dataVersion = dataVersion; - this.enclosingSize = enclosingSize; + super(file, dataVersion, enclosingSize); } @Override @@ -64,18 +54,4 @@ public SchematicType getType() { return SchematicType.NBT_STRUCTURE; } - @Override - public @NotNull Path getFile() { - return file; - } - - @Override - public int getMinecraftDataVersion() { - return dataVersion; - } - - @Override - public @Nullable Vec3i getEnclosingSize() { - return enclosingSize; - } } diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/schematic/SchemFile.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/schematic/SchemFile.java index 895447e302..bf7c703657 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/schematic/SchemFile.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/schematic/SchemFile.java @@ -5,20 +5,16 @@ import com.github.steveice10.opennbt.tag.builtin.StringTag; import com.github.steveice10.opennbt.tag.builtin.Tag; import org.jackhuang.hmcl.util.Vec3i; -import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import java.io.IOException; import java.nio.file.Path; -import static org.jackhuang.hmcl.schematic.Schematic.*; -import static org.jackhuang.hmcl.schematic.Schematic.tryGetShort; - /// @author Calboot /// @see Schematic File Format Wiki /// @see Schematica /// @see Schem -public final class SchemFile implements Schematic { +public final class SchemFile extends Schematic { private static final int DATA_VERSION_MC_1_13_2 = 1631; @@ -74,18 +70,13 @@ private static SchemFile loadSponge(Path file, CompoundTag root) throws IOExcept return new SchemFile(file, null, dataVersion, ((IntTag) versionTag).getValue(), enclosingSize); } - private final Path file; private final String materials; - private final int dataVersion; private final int version; - private final Vec3i enclosingSize; private SchemFile(Path file, @Nullable String materials, int dataVersion, int version, Vec3i enclosingSize) { - this.file = file; + super(file, dataVersion, enclosingSize); this.materials = materials; - this.dataVersion = dataVersion; this.version = version; - this.enclosingSize = enclosingSize; } @Override @@ -93,29 +84,14 @@ public SchematicType getType() { return SchematicType.SCHEM; } - @Override - public @NotNull Path getFile() { - return file; - } - @Override public int getVersion() { return version; } - @Override - public int getMinecraftDataVersion() { - return dataVersion; - } - @Override public String getMinecraftVersion() { - return materials == null ? Integer.toString(dataVersion) : materials; - } - - @Override - public @Nullable Vec3i getEnclosingSize() { - return enclosingSize; + return materials == null ? super.getMinecraftVersion() : materials; } } diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/schematic/Schematic.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/schematic/Schematic.java index c596148c2d..3a04a6ad75 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/schematic/Schematic.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/schematic/Schematic.java @@ -15,14 +15,14 @@ import java.util.OptionalInt; import java.util.zip.GZIPInputStream; -public sealed interface Schematic permits LitematicFile, SchemFile, NBTStructureFile { +public sealed abstract class Schematic permits LitematicFile, SchemFile, NBTStructureFile { - static boolean isFileSchematic(Path file) { + public static boolean isFileSchematic(Path file) { return SchematicType.getType(file) != null; } @Nullable - static Schematic load(Path file) throws IOException { + public static Schematic load(Path file) throws IOException { var type = SchematicType.getType(file); if (type == null) return null; return switch (SchematicType.getType(file)) { @@ -32,7 +32,7 @@ static Schematic load(Path file) throws IOException { }; } - static CompoundTag readRoot(Path file) throws IOException { + public static CompoundTag readRoot(Path file) throws IOException { CompoundTag root; try (InputStream in = new GZIPInputStream(Files.newInputStream(file))) { root = (CompoundTag) NBTIO.readTag(in); @@ -40,72 +40,88 @@ static CompoundTag readRoot(Path file) throws IOException { return root; } - static int tryGetInt(Tag tag) { + public static int tryGetInt(Tag tag) { return tag instanceof IntTag ? ((IntTag) tag).getValue() : 0; } - static short tryGetShort(Tag tag) { + public static short tryGetShort(Tag tag) { return tag instanceof ShortTag ? ((ShortTag) tag).getValue() : 0; } - static @Nullable Instant tryGetLongTimestamp(Tag tag) { + public static @Nullable Instant tryGetLongTimestamp(Tag tag) { if (tag instanceof LongTag) { return Instant.ofEpochMilli(((LongTag) tag).getValue()); } return null; } - static @Nullable String tryGetString(Tag tag) { + public static @Nullable String tryGetString(Tag tag) { return tag instanceof StringTag ? ((StringTag) tag).getValue() : null; } - SchematicType getType(); + private final Path file; + private final int dataVersion; + private final Vec3i enclosingSize; - @NotNull Path getFile(); + protected Schematic(Path file, int dataVersion, @Nullable Vec3i enclosingSize) { + this.file = file; + this.dataVersion = dataVersion; + this.enclosingSize = enclosingSize; + } + + public abstract SchematicType getType(); + + @NotNull + public Path getFile() { + return file; + } - default int getVersion() { + public int getVersion() { return 0; } - default OptionalInt getSubVersion() { + public OptionalInt getSubVersion() { return OptionalInt.empty(); } - default int getMinecraftDataVersion() { - return 0; + public OptionalInt getMinecraftDataVersion() { + return dataVersion >= 100 /* 15w32a */ ? OptionalInt.of(dataVersion) : OptionalInt.empty(); } - default String getMinecraftVersion() { - return Integer.toString(getMinecraftDataVersion()); + public String getMinecraftVersion() { + return getMinecraftDataVersion().isPresent() ? Integer.toString(getMinecraftDataVersion().getAsInt()) : null; } - default String getName() { + public String getName() { return FileUtils.getNameWithoutExtension(getFile()); } - default String getAuthor() { + public String getAuthor() { return null; } - default Instant getTimeCreated() { + public Instant getTimeCreated() { return null; } - default Instant getTimeModified() { + public Instant getTimeModified() { return null; } - default int getRegionCount() { + public int getRegionCount() { return 0; } - default int getTotalBlocks() { + public int getTotalBlocks() { return 0; } - @Nullable Vec3i getEnclosingSize(); + @Nullable + public Vec3i getEnclosingSize() { + return enclosingSize; + } - default int getTotalVolume() { + public int getTotalVolume() { var enclosingSize = getEnclosingSize(); if (enclosingSize != null) return enclosingSize.x() * enclosingSize.y() * enclosingSize.z(); return 0; diff --git a/HMCLCore/src/test/java/org/jackhuang/hmcl/schematic/SchematicTest.java b/HMCLCore/src/test/java/org/jackhuang/hmcl/schematic/SchematicTest.java index 35e2b519f8..02df4faec8 100644 --- a/HMCLCore/src/test/java/org/jackhuang/hmcl/schematic/SchematicTest.java +++ b/HMCLCore/src/test/java/org/jackhuang/hmcl/schematic/SchematicTest.java @@ -52,7 +52,8 @@ public void test() throws Exception { assertEquals(5746, lFile.getTotalVolume()); assertEquals(new Vec3i(17, 26, 13), lFile.getEnclosingSize()); assertEquals(1, lFile.getRegionCount()); - assertEquals(4325, lFile.getMinecraftDataVersion()); + assertTrue(lFile.getMinecraftDataVersion().isPresent()); + assertEquals(4325, lFile.getMinecraftDataVersion().getAsInt()); assertEquals("4325", lFile.getMinecraftVersion()); assertEquals(7, lFile.getVersion()); assertTrue(lFile.getSubVersion().isPresent()); @@ -72,7 +73,8 @@ public void test() throws Exception { SchemFile sFileSponge = (SchemFile) load("/schematics/test.schem"); assertEquals(SchematicType.SCHEM, sFileSponge.getType()); assertEquals("test", sFileSponge.getName()); - assertEquals(3465, sFileSponge.getMinecraftDataVersion()); + assertTrue(sFileSponge.getMinecraftDataVersion().isPresent()); + assertEquals(3465, sFileSponge.getMinecraftDataVersion().getAsInt()); assertEquals("3465", sFileSponge.getMinecraftVersion()); assertEquals(new Vec3i(9, 5, 9), sFileSponge.getEnclosingSize()); } @@ -83,7 +85,8 @@ public void test() throws Exception { assertEquals("test", nFile.getName()); assertEquals(new Vec3i(9, 11, 13), nFile.getEnclosingSize()); assertEquals(1287, nFile.getTotalVolume()); - assertEquals(3465, nFile.getMinecraftDataVersion()); + assertTrue(nFile.getMinecraftDataVersion().isPresent()); + assertEquals(3465, nFile.getMinecraftDataVersion().getAsInt()); assertEquals("3465", nFile.getMinecraftVersion()); } } From 25cf9f4bad75d1ad8ddbf770ddcb2aeba6024423 Mon Sep 17 00:00:00 2001 From: Calboot Date: Mon, 2 Feb 2026 18:41:15 +0800 Subject: [PATCH 28/53] update --- .../java/org/jackhuang/hmcl/ui/versions/SchematicsPage.java | 2 +- HMCL/src/main/resources/assets/css/root.css | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/SchematicsPage.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/SchematicsPage.java index f94eafb211..a6c6e2a742 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/SchematicsPage.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/SchematicsPage.java @@ -823,7 +823,7 @@ private static final class SchematicsPageSkin extends SkinBase { { var relPath = new TextFlow(); - relPath.setStyle("-fx-font-size: 13"); + relPath.setStyle("-fx-font-size: 13;"); HBox.setMargin(relPath, new Insets(5)); skinnable.currentDirectoryProperty().addListener((__, ___, newValue) -> { relPath.getChildren().clear(); diff --git a/HMCL/src/main/resources/assets/css/root.css b/HMCL/src/main/resources/assets/css/root.css index f5bbc4bea6..9179aef5dc 100644 --- a/HMCL/src/main/resources/assets/css/root.css +++ b/HMCL/src/main/resources/assets/css/root.css @@ -19,7 +19,8 @@ .root { } -.svg { +.svg, +Text { -fx-fill: -monet-on-surface; } From 32bf679f149156cd7a14a9e44186d92eb24a2b94 Mon Sep 17 00:00:00 2001 From: Calboot Date: Mon, 2 Feb 2026 18:48:00 +0800 Subject: [PATCH 29/53] Apply i18n suggestions --- .../java/org/jackhuang/hmcl/ui/versions/SchematicsPage.java | 2 +- HMCL/src/main/resources/assets/lang/I18N.properties | 2 +- HMCL/src/main/resources/assets/lang/I18N_zh.properties | 2 +- HMCL/src/main/resources/assets/lang/I18N_zh_CN.properties | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/SchematicsPage.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/SchematicsPage.java index a6c6e2a742..29015f65a3 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/SchematicsPage.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/SchematicsPage.java @@ -594,7 +594,7 @@ private void updateContent(Schematic file) { file.getEnclosingSize().y(), file.getEnclosingSize().z())); if (StringUtils.isNotBlank(file.getMinecraftVersion())) - addDetailItem(i18n("schematics.info.mc_version"), file.getMinecraftVersion()); + addDetailItem(i18n("schematics.info.mc_data_version"), file.getMinecraftVersion()); if (file.getVersion() > 0) addDetailItem(i18n("schematics.info.version"), file.getSubVersion().isPresent() ? "%d.%d".formatted(file.getVersion(), file.getSubVersion().getAsInt()) : file.getVersion() diff --git a/HMCL/src/main/resources/assets/lang/I18N.properties b/HMCL/src/main/resources/assets/lang/I18N.properties index b242568706..76fd83ff55 100644 --- a/HMCL/src/main/resources/assets/lang/I18N.properties +++ b/HMCL/src/main/resources/assets/lang/I18N.properties @@ -1281,7 +1281,7 @@ schematics.info.type.litematic=Litematic (Litematica) schematics.info.type.nbt_structure=NBT Structure (Vanilla) schematics.info.type.schem=Schem (Schematica, WE, MCEdit) schematics.info.version=Schematic Version -schematics.info.mc_version=Minecraft Version +schematics.info.mc_data_version=Minecraft Data Version schematics.manage=Schematics schematics.no_litematica=Litematica or Forgematica not installed schematics.no_litematica_install=Litematica or Forgematica not installed\nClick to install diff --git a/HMCL/src/main/resources/assets/lang/I18N_zh.properties b/HMCL/src/main/resources/assets/lang/I18N_zh.properties index 6a4af3b341..0a4013ac3e 100644 --- a/HMCL/src/main/resources/assets/lang/I18N_zh.properties +++ b/HMCL/src/main/resources/assets/lang/I18N_zh.properties @@ -1066,7 +1066,7 @@ schematics.info.type.litematic=Litematic (Litematica) schematics.info.type.nbt_structure=NBT 結構 (原版) schematics.info.type.schem=Schem (Schematica, WE, MCEdit) schematics.info.version=原理圖版本 -schematics.info.mc_version=Minecraft 版本 +schematics.info.mc_data_version=Minecraft 資料版本 schematics.manage=原理圖管理 schematics.no_litematica=未安裝 Litematica 或 Forgematica schematics.no_litematica_install=未安裝 Litematica 或 Forgematica\n點選以安裝 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 354e8402f3..db23475721 100644 --- a/HMCL/src/main/resources/assets/lang/I18N_zh_CN.properties +++ b/HMCL/src/main/resources/assets/lang/I18N_zh_CN.properties @@ -1071,7 +1071,7 @@ schematics.info.type.litematic=Litematic (Litematica) schematics.info.type.nbt_structure=NBT 结构 (原版) schematics.info.type.schem=Schem (Schematica, WE, MCEdit) schematics.info.version=原理图版本 -schematics.info.mc_version=Minecraft 版本 +schematics.info.mc_data_version=Minecraft 数据版本 schematics.manage=原理图管理 schematics.no_litematica=未安装 Litematica 或 Forgematica schematics.no_litematica_install=未安装 Litematica 或 Forgematica\n点击以安装 From 6f1618bbb27e90fdad0edaa5ac69a778a8522972 Mon Sep 17 00:00:00 2001 From: Calboot Date: Mon, 2 Feb 2026 20:29:32 +0800 Subject: [PATCH 30/53] Update HMCL/src/main/resources/assets/lang/I18N_zh.properties Co-authored-by: 3gf8jv4dv <3gf8jv4dv@gmail.com> --- HMCL/src/main/resources/assets/lang/I18N_zh.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/HMCL/src/main/resources/assets/lang/I18N_zh.properties b/HMCL/src/main/resources/assets/lang/I18N_zh.properties index 0a4013ac3e..054fed2a67 100644 --- a/HMCL/src/main/resources/assets/lang/I18N_zh.properties +++ b/HMCL/src/main/resources/assets/lang/I18N_zh.properties @@ -1061,7 +1061,7 @@ schematics.info.time_created=建立時間 schematics.info.time_modified=修改時間 schematics.info.total_blocks=總方塊數 schematics.info.total_volume=總體積 -schematics.info.type=原理圖型別 +schematics.info.type=原理圖類型 schematics.info.type.litematic=Litematic (Litematica) schematics.info.type.nbt_structure=NBT 結構 (原版) schematics.info.type.schem=Schem (Schematica, WE, MCEdit) From 3c8eaabb376cba70f56e3a95b65275805af2e56a Mon Sep 17 00:00:00 2001 From: Calboot Date: Mon, 2 Feb 2026 20:57:05 +0800 Subject: [PATCH 31/53] smooth scrolling --- .../main/java/org/jackhuang/hmcl/ui/versions/SchematicsPage.java | 1 + 1 file changed, 1 insertion(+) diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/SchematicsPage.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/SchematicsPage.java index 29015f65a3..4a6274f022 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/SchematicsPage.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/SchematicsPage.java @@ -622,6 +622,7 @@ private void updateContent(Schematic file) { ScrollPane scrollPane = new ScrollPane(detailsContainer); scrollPane.setStyle("-fx-effect: dropshadow(gaussian, rgba(0, 0, 0, 0.26), 5, 0.06, -0.5, 1);"); scrollPane.setFitToWidth(true); + FXUtils.smoothScrolling(scrollPane); StackPane.setMargin(scrollPane, new Insets(10, 0, 0, 0)); setBody(scrollPane); } From e96e43aeb138c2014358d06c6b7a03c142d57916 Mon Sep 17 00:00:00 2001 From: Calboot Date: Mon, 2 Feb 2026 22:05:09 +0800 Subject: [PATCH 32/53] tip --- .../hmcl/ui/versions/SchematicsPage.java | 121 +++++++++++------- .../resources/assets/lang/I18N.properties | 5 +- .../resources/assets/lang/I18N_zh.properties | 5 +- .../assets/lang/I18N_zh_CN.properties | 5 +- 4 files changed, 82 insertions(+), 54 deletions(-) diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/SchematicsPage.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/SchematicsPage.java index 4a6274f022..46df87ed77 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/SchematicsPage.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/SchematicsPage.java @@ -44,7 +44,6 @@ import javafx.scene.text.Text; import javafx.scene.text.TextFlow; import javafx.stage.FileChooser; -import org.jackhuang.hmcl.mod.LocalModFile; import org.jackhuang.hmcl.mod.ModLoaderType; import org.jackhuang.hmcl.mod.RemoteMod; import org.jackhuang.hmcl.mod.modrinth.ModrinthRemoteModRepository; @@ -60,6 +59,7 @@ import org.jackhuang.hmcl.ui.SVG; import org.jackhuang.hmcl.ui.construct.*; import org.jackhuang.hmcl.ui.nbt.NBTEditorPage; +import org.jackhuang.hmcl.util.Pair; import org.jackhuang.hmcl.util.StringUtils; import org.jackhuang.hmcl.util.i18n.I18n; import org.jackhuang.hmcl.util.io.FileUtils; @@ -73,12 +73,12 @@ import java.nio.file.NoSuchFileException; import java.nio.file.Path; import java.util.*; -import java.util.stream.Collectors; import java.util.stream.Stream; import static org.jackhuang.hmcl.ui.FXUtils.ignoreEvent; import static org.jackhuang.hmcl.ui.FXUtils.onEscPressed; import static org.jackhuang.hmcl.ui.ToolbarListPageSkin.createToolbarButton2; +import static org.jackhuang.hmcl.util.Pair.pair; import static org.jackhuang.hmcl.util.i18n.I18n.i18n; import static org.jackhuang.hmcl.util.logging.Logger.LOG; @@ -105,6 +105,7 @@ private static String translateType(SchematicType type) { private String instanceId; private Path schematicsDirectory; private final ObjectProperty currentDirectory = new SimpleObjectProperty<>(this, "currentDirectory", null); + private final ObjectProperty> warningTip = new SimpleObjectProperty<>(this, "tip", pair(null, null)); private final BooleanProperty isRootProperty = new SimpleBooleanProperty(this, "isRoot", true); @@ -149,20 +150,24 @@ public void refresh() { setLoading(true); var modManager = profile.getRepository().getModManager(instanceId); Task.supplyAsync(() -> { - boolean hasLitematica = false; + LitematicaState litematicaState = LitematicaState.NOT_INSTALLED; try { modManager.refreshMods(); - var set = modManager.getMods() - .stream() - .map(LocalModFile::getId) - .collect(Collectors.toSet()); - if (set.contains("litematica") || set.contains("forgematica")) { - hasLitematica = true; + var mods = modManager.getMods(); + for (var localModFile : mods) { + if ("litematica".equals(localModFile.getId()) || "forgematica".equals(localModFile.getId())) { + if (localModFile.isActive()) { + litematicaState = LitematicaState.OK; + break; + } else { + litematicaState = LitematicaState.DISABLED; + } + } } } catch (IOException e) { LOG.warning("Failed to load mods, unable to check litematica", e); } - if (!hasLitematica) { + if (litematicaState == LitematicaState.NOT_INSTALLED) { try { if (litematica == null) litematica = ModrinthRemoteModRepository.MODS.getModById("litematica"); } catch (IOException ignored) { @@ -171,50 +176,48 @@ public void refresh() { if (forgematica == null) forgematica = ModrinthRemoteModRepository.MODS.getModById("forgematica"); } catch (IOException ignored) { } - return null; } - return loadRoot(schematicsDirectory); + return pair(litematicaState, loadRoot(schematicsDirectory)); }).whenComplete(Schedulers.javafx(), (result, exception) -> { if (exception == null) { - if (result == null) { - boolean useForgematica = forgematica != null - && (modManager.getSupportedLoaders().contains(ModLoaderType.FORGE) || modManager.getSupportedLoaders().contains(ModLoaderType.NEO_FORGED)) - && GameVersionNumber.asGameVersion(Optional.ofNullable(modManager.getGameVersion())).isAtLeast("1.16.4", "20w45a"); - if (useForgematica || litematica != null) { - setFailedReason(i18n("schematics.no_litematica_install")); - setOnFailedAction(__ -> { - var modDownloads = Controllers.getDownloadPage().showModDownloads(); - modDownloads.selectVersion(instanceId); - Controllers.navigate(new DownloadPage( - modDownloads, - useForgematica ? forgematica : litematica, - modDownloads.getProfileVersion(), - modDownloads.getCallback()) - ); - }); - } else { - setFailedReason(i18n("schematics.no_litematica")); - setOnFailedAction(null); + switch (result.key()) { + case NOT_INSTALLED -> { + boolean useForgematica = forgematica != null + && (modManager.getSupportedLoaders().contains(ModLoaderType.FORGE) || modManager.getSupportedLoaders().contains(ModLoaderType.NEO_FORGED)) + && GameVersionNumber.asGameVersion(Optional.ofNullable(modManager.getGameVersion())).isAtLeast("1.16.4", "20w45a"); + if (useForgematica || litematica != null) { + warningTip.set(pair(i18n("schematics.warning.no_litematica_install"), () -> { + var modDownloads = Controllers.getDownloadPage().showModDownloads(); + modDownloads.selectVersion(instanceId); + Controllers.navigate(new DownloadPage( + modDownloads, + useForgematica ? forgematica : litematica, + modDownloads.getProfileVersion(), + modDownloads.getCallback()) + ); + })); + } else { + warningTip.set(pair(i18n("schematics.warning.no_litematica"), null)); + } } - } else { - DirItem target = result; - if (currentDirectoryProperty().get() != null) { - loop: - for (String dirName : currentDirectoryProperty().get().relativePath) { - target.preLoad(); - for (var dirChild : target.dirChildren) { - if (dirChild.getName().equals(dirName)) { - target = dirChild; - continue loop; - } + case DISABLED -> warningTip.set(pair(i18n("schematics.warning.litematica_disabled"), null)); + default -> warningTip.set(pair(null, null)); + } + DirItem target = result.value(); + if (currentDirectoryProperty().get() != null) { + loop: + for (String dirName : currentDirectoryProperty().get().relativePath) { + target.preLoad(); + for (var dirChild : target.dirChildren) { + if (dirChild.getName().equals(dirName)) { + target = dirChild; + continue loop; } - break; } + break; } - - setFailedReason(null); - navigateTo(target); } + navigateTo(target); setLoading(false); } else { LOG.warning("Failed to load schematics", exception); @@ -802,13 +805,28 @@ private static final class SchematicsPageSkin extends SkinBase { root.getContent().add(toolbar); } + { + var tip = new TextFlow(); + tip.setStyle("-fx-font-size: 13;"); + HBox.setMargin(tip, new Insets(5)); + var tipPane = new HBox(tip); + tipPane.setAlignment(Pos.CENTER_LEFT); + skinnable.warningTip.addListener((observable, oldValue, newValue) -> { + root.getContent().remove(tipPane); + if (newValue != null && !StringUtils.isBlank(newValue.key())) { + var txt = new Text(newValue.key()); + if (newValue.value() != null) FXUtils.onClicked(txt, newValue.value()); + tip.getChildren().setAll(txt); + root.getContent().add(1, tipPane); + } + }); + } + { SpinnerPane center = new SpinnerPane(); ComponentList.setVgrow(center, Priority.ALWAYS); center.getStyleClass().add("large-spinner-pane"); center.loadingProperty().bind(skinnable.loadingProperty()); - center.failedReasonProperty().bind(skinnable.failedReasonProperty()); - center.onFailedActionProperty().bind(skinnable.onFailedActionProperty()); listView.setCellFactory(x -> new Cell(listView)); listView.setSelectionModel(new NoneMultipleSelectionModel<>()); @@ -847,4 +865,11 @@ private static final class SchematicsPageSkin extends SkinBase { getChildren().setAll(pane); } } + + private enum LitematicaState { + DISABLED, + NOT_INSTALLED, + OK + } + } diff --git a/HMCL/src/main/resources/assets/lang/I18N.properties b/HMCL/src/main/resources/assets/lang/I18N.properties index 76fd83ff55..d12a50fdef 100644 --- a/HMCL/src/main/resources/assets/lang/I18N.properties +++ b/HMCL/src/main/resources/assets/lang/I18N.properties @@ -1283,8 +1283,9 @@ schematics.info.type.schem=Schem (Schematica, WE, MCEdit) schematics.info.version=Schematic Version schematics.info.mc_data_version=Minecraft Data Version schematics.manage=Schematics -schematics.no_litematica=Litematica or Forgematica not installed -schematics.no_litematica_install=Litematica or Forgematica not installed\nClick to install +schematics.warning.litematica_disabled=Litematica or Forgematica disabled +schematics.warning.no_litematica=Litematica or Forgematica not installed +schematics.warning.no_litematica_install=Litematica or Forgematica not installed. Click to install schematics.sub_items=%d sub-item(s) search=Search diff --git a/HMCL/src/main/resources/assets/lang/I18N_zh.properties b/HMCL/src/main/resources/assets/lang/I18N_zh.properties index 054fed2a67..a7c8b07727 100644 --- a/HMCL/src/main/resources/assets/lang/I18N_zh.properties +++ b/HMCL/src/main/resources/assets/lang/I18N_zh.properties @@ -1068,8 +1068,9 @@ schematics.info.type.schem=Schem (Schematica, WE, MCEdit) schematics.info.version=原理圖版本 schematics.info.mc_data_version=Minecraft 資料版本 schematics.manage=原理圖管理 -schematics.no_litematica=未安裝 Litematica 或 Forgematica -schematics.no_litematica_install=未安裝 Litematica 或 Forgematica\n點選以安裝 +schematics.warning.litematica_disabled=Litematica 或 Forgematica 被禁用 +schematics.warning.no_litematica=未安裝 Litematica 或 Forgematica +schematics.warning.no_litematica_install=未安裝 Litematica 或 Forgematica,點選以安裝 schematics.sub_items=%d 個子項 search=搜尋 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 db23475721..883f58e0e6 100644 --- a/HMCL/src/main/resources/assets/lang/I18N_zh_CN.properties +++ b/HMCL/src/main/resources/assets/lang/I18N_zh_CN.properties @@ -1073,8 +1073,9 @@ schematics.info.type.schem=Schem (Schematica, WE, MCEdit) schematics.info.version=原理图版本 schematics.info.mc_data_version=Minecraft 数据版本 schematics.manage=原理图管理 -schematics.no_litematica=未安装 Litematica 或 Forgematica -schematics.no_litematica_install=未安装 Litematica 或 Forgematica\n点击以安装 +schematics.warning.litematica_disabled=Litematica 或 Forgematica 被禁用 +schematics.warning.no_litematica=未安装 Litematica 或 Forgematica +schematics.warning.no_litematica_install=未安装 Litematica 或 Forgematica,点击以安装 schematics.sub_items=%d 个子项 search=搜索 From 64b86b7aa45b4e4477416e971c4b4c6c85b8d275 Mon Sep 17 00:00:00 2001 From: Calboot Date: Mon, 2 Feb 2026 22:38:04 +0800 Subject: [PATCH 33/53] Apply suggestions from code review Co-authored-by: 3gf8jv4dv <3gf8jv4dv@gmail.com> --- HMCL/src/main/resources/assets/lang/I18N.properties | 6 +++--- HMCL/src/main/resources/assets/lang/I18N_zh.properties | 2 +- HMCL/src/main/resources/assets/lang/I18N_zh_CN.properties | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/HMCL/src/main/resources/assets/lang/I18N.properties b/HMCL/src/main/resources/assets/lang/I18N.properties index d12a50fdef..870c26ff5f 100644 --- a/HMCL/src/main/resources/assets/lang/I18N.properties +++ b/HMCL/src/main/resources/assets/lang/I18N.properties @@ -1283,9 +1283,9 @@ schematics.info.type.schem=Schem (Schematica, WE, MCEdit) schematics.info.version=Schematic Version schematics.info.mc_data_version=Minecraft Data Version schematics.manage=Schematics -schematics.warning.litematica_disabled=Litematica or Forgematica disabled -schematics.warning.no_litematica=Litematica or Forgematica not installed -schematics.warning.no_litematica_install=Litematica or Forgematica not installed. Click to install +schematics.warning.litematica_disabled=Litematica or Forgematica has been disabled. +schematics.warning.no_litematica=Litematica or Forgematica not installed. +schematics.warning.no_litematica_install=Litematica or Forgematica not installed. Click to install. schematics.sub_items=%d sub-item(s) search=Search diff --git a/HMCL/src/main/resources/assets/lang/I18N_zh.properties b/HMCL/src/main/resources/assets/lang/I18N_zh.properties index a7c8b07727..2ab2f280df 100644 --- a/HMCL/src/main/resources/assets/lang/I18N_zh.properties +++ b/HMCL/src/main/resources/assets/lang/I18N_zh.properties @@ -1068,7 +1068,7 @@ schematics.info.type.schem=Schem (Schematica, WE, MCEdit) schematics.info.version=原理圖版本 schematics.info.mc_data_version=Minecraft 資料版本 schematics.manage=原理圖管理 -schematics.warning.litematica_disabled=Litematica 或 Forgematica 被禁用 +schematics.warning.litematica_disabled=Litematica 或 Forgematica 已被停用 schematics.warning.no_litematica=未安裝 Litematica 或 Forgematica schematics.warning.no_litematica_install=未安裝 Litematica 或 Forgematica,點選以安裝 schematics.sub_items=%d 個子項 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 883f58e0e6..8f887489e2 100644 --- a/HMCL/src/main/resources/assets/lang/I18N_zh_CN.properties +++ b/HMCL/src/main/resources/assets/lang/I18N_zh_CN.properties @@ -1073,7 +1073,7 @@ schematics.info.type.schem=Schem (Schematica, WE, MCEdit) schematics.info.version=原理图版本 schematics.info.mc_data_version=Minecraft 数据版本 schematics.manage=原理图管理 -schematics.warning.litematica_disabled=Litematica 或 Forgematica 被禁用 +schematics.warning.litematica_disabled=Litematica 或 Forgematica 已被禁用 schematics.warning.no_litematica=未安装 Litematica 或 Forgematica schematics.warning.no_litematica_install=未安装 Litematica 或 Forgematica,点击以安装 schematics.sub_items=%d 个子项 From 0a460e74d983b0b3c214aec94d1bbd36795fda69 Mon Sep 17 00:00:00 2001 From: Calboot Date: Tue, 3 Feb 2026 11:47:46 +0800 Subject: [PATCH 34/53] update --- .../jackhuang/hmcl/ui/versions/SchematicsPage.java | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/SchematicsPage.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/SchematicsPage.java index 46df87ed77..7cb6114cc4 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/SchematicsPage.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/SchematicsPage.java @@ -811,11 +811,11 @@ private static final class SchematicsPageSkin extends SkinBase { HBox.setMargin(tip, new Insets(5)); var tipPane = new HBox(tip); tipPane.setAlignment(Pos.CENTER_LEFT); - skinnable.warningTip.addListener((observable, oldValue, newValue) -> { + FXUtils.onChangeAndOperate(skinnable.warningTip, pair -> { root.getContent().remove(tipPane); - if (newValue != null && !StringUtils.isBlank(newValue.key())) { - var txt = new Text(newValue.key()); - if (newValue.value() != null) FXUtils.onClicked(txt, newValue.value()); + if (pair != null && !StringUtils.isBlank(pair.key())) { + var txt = new Text(pair.key()); + if (pair.value() != null) FXUtils.onClicked(txt, pair.value()); tip.getChildren().setAll(txt); root.getContent().add(1, tipPane); } @@ -844,9 +844,9 @@ private static final class SchematicsPageSkin extends SkinBase { var relPath = new TextFlow(); relPath.setStyle("-fx-font-size: 13;"); HBox.setMargin(relPath, new Insets(5)); - skinnable.currentDirectoryProperty().addListener((__, ___, newValue) -> { + FXUtils.onChangeAndOperate(skinnable.currentDirectoryProperty(), currentDir -> { relPath.getChildren().clear(); - var d = newValue; + var d = currentDir; while (d != null) { relPath.getChildren().add(0, new Text("/")); var txt = new Text(d.getName()); From 7d2ace7cdd1db73d0679c8ae321c3bcf176d3232 Mon Sep 17 00:00:00 2001 From: Calboot Date: Tue, 3 Feb 2026 16:07:17 +0800 Subject: [PATCH 35/53] OptionalInt --- .../hmcl/ui/versions/SchematicsPage.java | 50 ++++++++++--------- .../hmcl/schematic/LitematicFile.java | 44 ++++++++-------- .../hmcl/schematic/NBTStructureFile.java | 20 +++++--- .../jackhuang/hmcl/schematic/SchemFile.java | 7 +-- .../jackhuang/hmcl/schematic/Schematic.java | 36 ++++++++----- .../java/org/jackhuang/hmcl/util/Lang.java | 4 ++ .../hmcl/schematic/SchematicTest.java | 25 ++++------ 7 files changed, 105 insertions(+), 81 deletions(-) diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/SchematicsPage.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/SchematicsPage.java index 7cb6114cc4..58fd0f53d5 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/SchematicsPage.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/SchematicsPage.java @@ -472,6 +472,8 @@ private SchematicItem(Path path) throws IOException { this.path = path; this.file = Schematic.load(path); + if (file == null) throw new AssertionError(); // Should be impossible + if (this.file instanceof LitematicFile lFile) { String name = lFile.getName(); if (StringUtils.isNotBlank(name) && !"Unnamed".equals(name)) { @@ -484,21 +486,19 @@ private SchematicItem(Path path) throws IOException { } WritableImage image = null; - if (this.file instanceof LitematicFile lFile) { - int[] previewImageData = lFile.getPreviewImageData(); - if (previewImageData != null && previewImageData.length > 0) { - int size = (int) Math.sqrt(previewImageData.length); - if ((size * size) == previewImageData.length) { - image = new WritableImage(size, size); - PixelWriter pixelWriter = image.getPixelWriter(); - - for (int y = 0, i = 0; y < size; ++y) { - for (int x = 0; x < size; ++x) { - pixelWriter.setArgb(x, y, previewImageData[i++]); - } + int[] previewImageData = file.getPreviewImageData(); + if (previewImageData != null && previewImageData.length > 0) { + int size = (int) Math.sqrt(previewImageData.length); + if ((size * size) == previewImageData.length) { + image = new WritableImage(size, size); + PixelWriter pixelWriter = image.getPixelWriter(); + + for (int y = 0, i = 0; y < size; ++y) { + for (int x = 0; x < size; ++x) { + pixelWriter.setArgb(x, y, previewImageData[i++]); } - } + } } this.image = image; @@ -552,16 +552,16 @@ void onClick() { @Override void onReveal() { - FXUtils.showFileInExplorer(file.getFile()); + FXUtils.showFileInExplorer(path); } @Override void onDelete() { try { - Files.deleteIfExists(file.getFile()); + Files.deleteIfExists(path); refresh(); } catch (IOException e) { - LOG.warning("Failed to delete litematic file: " + file.getFile(), e); + LOG.warning("Failed to delete schematic file: " + path, e); } } @@ -585,12 +585,12 @@ private void updateContent(Schematic file) { addDetailItem(i18n("schematics.info.time_created"), I18n.formatDateTime(file.getTimeCreated())); if (file.getTimeModified() != null && !file.getTimeModified().equals(file.getTimeCreated())) addDetailItem(i18n("schematics.info.time_modified"), I18n.formatDateTime(file.getTimeModified())); - if (file.getRegionCount() > 0) - addDetailItem(i18n("schematics.info.region_count"), String.valueOf(file.getRegionCount())); - if (file.getTotalVolume() > 0) - addDetailItem(i18n("schematics.info.total_volume"), file.getTotalVolume()); - if (file.getTotalBlocks() > 0) - addDetailItem(i18n("schematics.info.total_blocks"), file.getTotalBlocks()); + if (file.getRegionCount().isPresent()) + addDetailItem(i18n("schematics.info.region_count"), String.valueOf(file.getRegionCount().getAsInt())); + if (file.getTotalVolume().isPresent()) + addDetailItem(i18n("schematics.info.total_volume"), file.getTotalVolume().getAsInt()); + if (file.getTotalBlocks().isPresent()) + addDetailItem(i18n("schematics.info.total_blocks"), file.getTotalBlocks().getAsInt()); if (file.getEnclosingSize() != null) addDetailItem(i18n("schematics.info.enclosing_size"), String.format("%d x %d x %d", file.getEnclosingSize().x(), @@ -598,9 +598,11 @@ private void updateContent(Schematic file) { file.getEnclosingSize().z())); if (StringUtils.isNotBlank(file.getMinecraftVersion())) addDetailItem(i18n("schematics.info.mc_data_version"), file.getMinecraftVersion()); - if (file.getVersion() > 0) + if (file.getVersion().isPresent()) addDetailItem(i18n("schematics.info.version"), - file.getSubVersion().isPresent() ? "%d.%d".formatted(file.getVersion(), file.getSubVersion().getAsInt()) : file.getVersion() + file.getSubVersion().isPresent() + ? "%d.%d".formatted(file.getVersion().getAsInt(), file.getSubVersion().getAsInt()) + : file.getVersion().getAsInt() ); } diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/schematic/LitematicFile.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/schematic/LitematicFile.java index 7c28681fbf..1c1c584213 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/schematic/LitematicFile.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/schematic/LitematicFile.java @@ -18,6 +18,8 @@ package org.jackhuang.hmcl.schematic; import com.github.steveice10.opennbt.tag.builtin.*; +import org.jackhuang.hmcl.util.Lang; +import org.jackhuang.hmcl.util.StringUtils; import org.jackhuang.hmcl.util.Vec3i; import org.jetbrains.annotations.NotNull; @@ -57,19 +59,18 @@ else if (!(metadataTag instanceof CompoundTag)) Tag enclosingSizeTag = ((CompoundTag) metadataTag).get("EnclosingSize"); if (enclosingSizeTag instanceof CompoundTag) { CompoundTag list = (CompoundTag) enclosingSizeTag; - int x = tryGetInt(list.get("x")); - int y = tryGetInt(list.get("y")); - int z = tryGetInt(list.get("z")); + int x = tryGetInt(list.get("x")).orElse(0); + int y = tryGetInt(list.get("y")).orElse(0); + int z = tryGetInt(list.get("z")).orElse(0); - if (x >= 0 && y >= 0 && z >= 0) - enclosingSize = new Vec3i(x, y, z); + if (x > 0 && y > 0 && z > 0) enclosingSize = new Vec3i(x, y, z); } - Tag tag = root.get("SubVersion"); + Tag subVersionTag = root.get("SubVersion"); return new LitematicFile(file, (CompoundTag) metadataTag, ((IntTag) versionTag).getValue(), - tag instanceof IntTag ? ((IntTag) tag).getValue() : -1, - tryGetInt(root.get("MinecraftDataVersion")), + tryGetInt(subVersionTag).orElse(-1), + tryGetInt(root.get("MinecraftDataVersion")).orElse(0), regions, enclosingSize ); @@ -104,8 +105,8 @@ private LitematicFile(@NotNull Path file, @NotNull CompoundTag metadata, this.description = tryGetString(metadata.get("Description")); this.timeCreated = tryGetLongTimestamp(metadata.get("TimeCreated")); this.timeModified = tryGetLongTimestamp(metadata.get("TimeModified")); - this.totalBlocks = tryGetInt(metadata.get("TotalBlocks")); - this.totalVolume = tryGetInt(metadata.get("TotalVolume")); + this.totalBlocks = tryGetInt(metadata.get("TotalBlocks")).orElse(-1); + this.totalVolume = tryGetInt(metadata.get("TotalVolume")).orElse(-1); } @@ -115,22 +116,23 @@ public SchematicType getType() { } @Override - public int getVersion() { - return version; + public OptionalInt getVersion() { + return OptionalInt.of(version); } @Override public OptionalInt getSubVersion() { - return subVersion >= 0 ? OptionalInt.of(subVersion) : OptionalInt.empty(); + return Lang.wrapWithMinValue(subVersion, 0); } + @Override public int[] getPreviewImageData() { return previewImageData != null ? previewImageData.clone() : null; } @Override - public String getName() { - return name; + public @NotNull String getName() { + return StringUtils.isNotBlank(name) ? name : super.getName(); } @Override @@ -153,18 +155,18 @@ public Instant getTimeModified() { } @Override - public int getTotalBlocks() { - return totalBlocks; + public OptionalInt getTotalBlocks() { + return Lang.wrapWithMinValue(totalBlocks, 1); } @Override - public int getTotalVolume() { - return totalVolume; + public OptionalInt getTotalVolume() { + return Lang.wrapWithMinValue(totalVolume, 1); } @Override - public int getRegionCount() { - return regionCount; + public OptionalInt getRegionCount() { + return Lang.wrapWithMinValue(regionCount, 1); } } diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/schematic/NBTStructureFile.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/schematic/NBTStructureFile.java index bd8a6d715d..3fcc583079 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/schematic/NBTStructureFile.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/schematic/NBTStructureFile.java @@ -34,19 +34,22 @@ else if (!(sizeTag instanceof ListTag)) Tag zTag = size.get(2); Vec3i enclosingSize = null; if (xTag != null && yTag != null && zTag != null) { - int width = tryGetInt(xTag); - int height = tryGetInt(yTag); - int length = tryGetInt(zTag); - if (width >= 0 && height >= 0 && length >= 0) { + int width = tryGetInt(xTag).orElse(0); + int height = tryGetInt(yTag).orElse(0); + int length = tryGetInt(zTag).orElse(0); + if (width > 0 && height > 0 && length > 0) { enclosingSize = new Vec3i(width, height, length); } } - return new NBTStructureFile(file, ((IntTag) dataVersionTag).getValue(), enclosingSize); + return new NBTStructureFile(file, ((IntTag) dataVersionTag).getValue(), tryGetString(root.get("author")), enclosingSize); } - private NBTStructureFile(Path file, int dataVersion, Vec3i enclosingSize) { + private final String author; + + private NBTStructureFile(Path file, int dataVersion, String author, Vec3i enclosingSize) { super(file, dataVersion, enclosingSize); + this.author = author; } @Override @@ -54,4 +57,9 @@ public SchematicType getType() { return SchematicType.NBT_STRUCTURE; } + @Override + public String getAuthor() { + return author; + } + } diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/schematic/SchemFile.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/schematic/SchemFile.java index bf7c703657..aa1f8de487 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/schematic/SchemFile.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/schematic/SchemFile.java @@ -9,6 +9,7 @@ import java.io.IOException; import java.nio.file.Path; +import java.util.OptionalInt; /// @author Calboot /// @see Schematic File Format Wiki @@ -54,7 +55,7 @@ private static SchemFile loadSponge(Path file, CompoundTag root) throws IOExcept throw new IOException("Version tag is not an integer"); Tag dataVersionTag = root.get("DataVersion"); - int dataVersion = dataVersionTag == null ? DATA_VERSION_MC_1_13_2 : tryGetInt(dataVersionTag); + int dataVersion = dataVersionTag == null ? DATA_VERSION_MC_1_13_2 : tryGetInt(dataVersionTag).orElse(0); Tag widthTag = root.get("Width"); Tag heightTag = root.get("Height"); @@ -85,8 +86,8 @@ public SchematicType getType() { } @Override - public int getVersion() { - return version; + public OptionalInt getVersion() { + return version > 0 ? OptionalInt.of(version) : OptionalInt.empty(); } @Override diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/schematic/Schematic.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/schematic/Schematic.java index 3a04a6ad75..585813f37a 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/schematic/Schematic.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/schematic/Schematic.java @@ -2,6 +2,7 @@ import com.github.steveice10.opennbt.NBTIO; import com.github.steveice10.opennbt.tag.builtin.*; +import org.jackhuang.hmcl.util.Lang; import org.jackhuang.hmcl.util.Vec3i; import org.jackhuang.hmcl.util.io.FileUtils; import org.jetbrains.annotations.NotNull; @@ -40,8 +41,8 @@ public static CompoundTag readRoot(Path file) throws IOException { return root; } - public static int tryGetInt(Tag tag) { - return tag instanceof IntTag ? ((IntTag) tag).getValue() : 0; + public static OptionalInt tryGetInt(Tag tag) { + return tag instanceof IntTag ? OptionalInt.of(((IntTag) tag).getValue()) : OptionalInt.empty(); } public static short tryGetShort(Tag tag) { @@ -76,44 +77,51 @@ public Path getFile() { return file; } - public int getVersion() { - return 0; + public OptionalInt getVersion() { + return OptionalInt.empty(); } + /// None-negative, otherwise empty public OptionalInt getSubVersion() { return OptionalInt.empty(); } + /// At least 100, otherwise empty public OptionalInt getMinecraftDataVersion() { - return dataVersion >= 100 /* 15w32a */ ? OptionalInt.of(dataVersion) : OptionalInt.empty(); + return Lang.wrapWithMinValue(dataVersion, /* 15w32a */ 100); } + @Nullable public String getMinecraftVersion() { return getMinecraftDataVersion().isPresent() ? Integer.toString(getMinecraftDataVersion().getAsInt()) : null; } + @NotNull public String getName() { return FileUtils.getNameWithoutExtension(getFile()); } + @Nullable public String getAuthor() { return null; } + @Nullable public Instant getTimeCreated() { return null; } + @Nullable public Instant getTimeModified() { return null; } - public int getRegionCount() { - return 0; + public OptionalInt getRegionCount() { + return OptionalInt.empty(); } - public int getTotalBlocks() { - return 0; + public OptionalInt getTotalBlocks() { + return OptionalInt.empty(); } @Nullable @@ -121,10 +129,14 @@ public Vec3i getEnclosingSize() { return enclosingSize; } - public int getTotalVolume() { + public OptionalInt getTotalVolume() { var enclosingSize = getEnclosingSize(); - if (enclosingSize != null) return enclosingSize.x() * enclosingSize.y() * enclosingSize.z(); - return 0; + if (enclosingSize != null) return OptionalInt.of(enclosingSize.x() * enclosingSize.y() * enclosingSize.z()); + return OptionalInt.empty(); + } + + public int @Nullable [] getPreviewImageData() { + return null; } } diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/util/Lang.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/util/Lang.java index 5322e5b75b..2eea57c4b0 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/util/Lang.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/util/Lang.java @@ -138,6 +138,10 @@ public static Optional tryCast(Object obj, Class clazz) { } } + public static OptionalInt wrapWithMinValue(int value, int min) { + return value >= min ? OptionalInt.of(value) : OptionalInt.empty(); + } + public static T getOrDefault(List a, int index, T defaultValue) { return index < 0 || index >= a.size() ? defaultValue : a.get(index); } diff --git a/HMCLCore/src/test/java/org/jackhuang/hmcl/schematic/SchematicTest.java b/HMCLCore/src/test/java/org/jackhuang/hmcl/schematic/SchematicTest.java index 02df4faec8..f8ce9938a1 100644 --- a/HMCLCore/src/test/java/org/jackhuang/hmcl/schematic/SchematicTest.java +++ b/HMCLCore/src/test/java/org/jackhuang/hmcl/schematic/SchematicTest.java @@ -28,7 +28,6 @@ import java.time.Instant; import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertTrue; public final class SchematicTest { private static Schematic load(String name) throws IOException, URISyntaxException { @@ -48,16 +47,14 @@ public void test() throws Exception { assertEquals("", lFile.getDescription()); assertEquals(Instant.ofEpochMilli(1746443586433L), lFile.getTimeCreated()); assertEquals(Instant.ofEpochMilli(1746443586433L), lFile.getTimeModified()); - assertEquals(1334, lFile.getTotalBlocks()); - assertEquals(5746, lFile.getTotalVolume()); + assertEquals(1334, lFile.getTotalBlocks().orElse(0)); + assertEquals(5746, lFile.getTotalVolume().orElse(0)); assertEquals(new Vec3i(17, 26, 13), lFile.getEnclosingSize()); - assertEquals(1, lFile.getRegionCount()); - assertTrue(lFile.getMinecraftDataVersion().isPresent()); - assertEquals(4325, lFile.getMinecraftDataVersion().getAsInt()); + assertEquals(1, lFile.getRegionCount().orElse(0)); + assertEquals(4325, lFile.getMinecraftDataVersion().orElse(0)); assertEquals("4325", lFile.getMinecraftVersion()); - assertEquals(7, lFile.getVersion()); - assertTrue(lFile.getSubVersion().isPresent()); - assertEquals(1, lFile.getSubVersion().getAsInt()); + assertEquals(7, lFile.getVersion().orElse(0)); + assertEquals(1, lFile.getSubVersion().orElse(-1)); } { @@ -65,7 +62,7 @@ public void test() throws Exception { assertEquals(SchematicType.SCHEM, sFile.getType()); assertEquals("test", sFile.getName()); assertEquals(new Vec3i(28, 35, 18), sFile.getEnclosingSize()); - assertEquals(17640, sFile.getTotalVolume()); + assertEquals(17640, sFile.getTotalVolume().orElse(0)); assertEquals("Alpha", sFile.getMinecraftVersion()); } @@ -73,8 +70,7 @@ public void test() throws Exception { SchemFile sFileSponge = (SchemFile) load("/schematics/test.schem"); assertEquals(SchematicType.SCHEM, sFileSponge.getType()); assertEquals("test", sFileSponge.getName()); - assertTrue(sFileSponge.getMinecraftDataVersion().isPresent()); - assertEquals(3465, sFileSponge.getMinecraftDataVersion().getAsInt()); + assertEquals(3465, sFileSponge.getMinecraftDataVersion().orElse(0)); assertEquals("3465", sFileSponge.getMinecraftVersion()); assertEquals(new Vec3i(9, 5, 9), sFileSponge.getEnclosingSize()); } @@ -84,9 +80,8 @@ public void test() throws Exception { assertEquals(SchematicType.NBT_STRUCTURE, nFile.getType()); assertEquals("test", nFile.getName()); assertEquals(new Vec3i(9, 11, 13), nFile.getEnclosingSize()); - assertEquals(1287, nFile.getTotalVolume()); - assertTrue(nFile.getMinecraftDataVersion().isPresent()); - assertEquals(3465, nFile.getMinecraftDataVersion().getAsInt()); + assertEquals(1287, nFile.getTotalVolume().orElse(0)); + assertEquals(3465, nFile.getMinecraftDataVersion().orElse(0)); assertEquals("3465", nFile.getMinecraftVersion()); } } From ef2911f2e18106ca44f7011c55c950a555c22577 Mon Sep 17 00:00:00 2001 From: Calboot Date: Fri, 6 Feb 2026 11:17:12 +0800 Subject: [PATCH 36/53] License header --- .../hmcl/schematic/LitematicaConfig.java | 17 +++++++++++++++++ .../hmcl/schematic/NBTStructureFile.java | 17 +++++++++++++++++ .../org/jackhuang/hmcl/schematic/SchemFile.java | 17 +++++++++++++++++ .../org/jackhuang/hmcl/schematic/Schematic.java | 17 +++++++++++++++++ .../jackhuang/hmcl/schematic/SchematicType.java | 17 +++++++++++++++++ .../java/org/jackhuang/hmcl/util/Vec3i.java | 17 +++++++++++++++++ 6 files changed, 102 insertions(+) diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/schematic/LitematicaConfig.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/schematic/LitematicaConfig.java index cdc0b294b4..bb4b5a11b4 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/schematic/LitematicaConfig.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/schematic/LitematicaConfig.java @@ -1,3 +1,20 @@ +/* + * Hello Minecraft! Launcher + * Copyright (C) 2026 huangyuhui and contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ package org.jackhuang.hmcl.schematic; import com.google.gson.annotations.SerializedName; diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/schematic/NBTStructureFile.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/schematic/NBTStructureFile.java index 3fcc583079..66867bf521 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/schematic/NBTStructureFile.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/schematic/NBTStructureFile.java @@ -1,3 +1,20 @@ +/* + * Hello Minecraft! Launcher + * Copyright (C) 2026 huangyuhui and contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ package org.jackhuang.hmcl.schematic; import com.github.steveice10.opennbt.tag.builtin.*; diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/schematic/SchemFile.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/schematic/SchemFile.java index aa1f8de487..c39ee09084 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/schematic/SchemFile.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/schematic/SchemFile.java @@ -1,3 +1,20 @@ +/* + * Hello Minecraft! Launcher + * Copyright (C) 2026 huangyuhui and contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ package org.jackhuang.hmcl.schematic; import com.github.steveice10.opennbt.tag.builtin.CompoundTag; diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/schematic/Schematic.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/schematic/Schematic.java index 585813f37a..fb3500c631 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/schematic/Schematic.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/schematic/Schematic.java @@ -1,3 +1,20 @@ +/* + * Hello Minecraft! Launcher + * Copyright (C) 2026 huangyuhui and contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ package org.jackhuang.hmcl.schematic; import com.github.steveice10.opennbt.NBTIO; diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/schematic/SchematicType.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/schematic/SchematicType.java index 71cea9764a..50bcc7ef58 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/schematic/SchematicType.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/schematic/SchematicType.java @@ -1,3 +1,20 @@ +/* + * Hello Minecraft! Launcher + * Copyright (C) 2026 huangyuhui and contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ package org.jackhuang.hmcl.schematic; import org.jackhuang.hmcl.util.io.FileUtils; diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/util/Vec3i.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/util/Vec3i.java index 9e5948c5b8..5048313ea9 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/util/Vec3i.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/util/Vec3i.java @@ -1,3 +1,20 @@ +/* + * Hello Minecraft! Launcher + * Copyright (C) 2026 huangyuhui and contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ package org.jackhuang.hmcl.util; public record Vec3i(int x, int y, int z) { From 0df85d209b478f8db0e847e86925a63efeddaa40 Mon Sep 17 00:00:00 2001 From: Calboot Date: Fri, 6 Feb 2026 11:22:06 +0800 Subject: [PATCH 37/53] update --- .../java/org/jackhuang/hmcl/ui/versions/SchematicsPage.java | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/SchematicsPage.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/SchematicsPage.java index 58fd0f53d5..b177050a8b 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/SchematicsPage.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/SchematicsPage.java @@ -139,8 +139,9 @@ public BooleanProperty isRootProperty() { } public void navigateBack() { - var p = currentDirectoryProperty().get().parent; - if (p != null) navigateTo(p); + var d = currentDirectoryProperty().get(); + if (d == null) return; + if (d.parent != null) navigateTo(d.parent); } public void refresh() { From dcfb5d04de15caa7b5e5c8562a165a39e497dcd1 Mon Sep 17 00:00:00 2001 From: Calboot Date: Fri, 6 Feb 2026 11:27:22 +0800 Subject: [PATCH 38/53] update --- .../hmcl/ui/versions/SchematicsPage.java | 26 ++++++++++--------- .../hmcl/schematic/NBTStructureFile.java | 4 +-- .../jackhuang/hmcl/schematic/Schematic.java | 4 +-- .../java/org/jackhuang/hmcl/util/Vec3i.java | 2 +- 4 files changed, 19 insertions(+), 17 deletions(-) diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/SchematicsPage.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/SchematicsPage.java index b177050a8b..0388d9223c 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/SchematicsPage.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/SchematicsPage.java @@ -419,20 +419,22 @@ void preLoad() throws IOException { void load() { if (this.loaded) return; - try (Stream stream = Files.list(path)) { + try { preLoad(); - for (var dir : dirChildren) { - dir.preLoad(); - this.children.add(dir); + try (Stream stream = Files.list(path)) { + for (var dir : dirChildren) { + dir.preLoad(); + this.children.add(dir); + } + stream.filter(Schematic::isFileSchematic) + .forEach(p -> { + try { + this.children.add(new SchematicItem(p)); + } catch (IOException e) { + LOG.warning("Failed to load schematic file: " + path, e); + } + }); } - stream.filter(Schematic::isFileSchematic) - .forEach(p -> { - try { - this.children.add(new SchematicItem(p)); - } catch (IOException e) { - LOG.warning("Failed to load litematic file: " + path, e); - } - }); } catch (NoSuchFileException ignored) { } catch (IOException e) { LOG.warning("Failed to load schematics in " + path, e); diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/schematic/NBTStructureFile.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/schematic/NBTStructureFile.java index 66867bf521..79e245ff78 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/schematic/NBTStructureFile.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/schematic/NBTStructureFile.java @@ -34,9 +34,9 @@ public static NBTStructureFile load(Path file) throws IOException { Tag dataVersionTag = root.get("DataVersion"); if (dataVersionTag == null) - throw new IOException("Materials tag not found"); + throw new IOException("DataVersion tag not found"); else if (!(dataVersionTag instanceof IntTag)) - throw new IOException("Materials tag is not an integer"); + throw new IOException("DataVersion tag is not an integer"); Tag sizeTag = root.get("size"); if (sizeTag == null) diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/schematic/Schematic.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/schematic/Schematic.java index fb3500c631..bc8c838d53 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/schematic/Schematic.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/schematic/Schematic.java @@ -43,7 +43,7 @@ public static boolean isFileSchematic(Path file) { public static Schematic load(Path file) throws IOException { var type = SchematicType.getType(file); if (type == null) return null; - return switch (SchematicType.getType(file)) { + return switch (type) { case LITEMATIC -> LitematicFile.load(file); case SCHEM -> SchemFile.load(file); case NBT_STRUCTURE -> NBTStructureFile.load(file); @@ -98,7 +98,7 @@ public OptionalInt getVersion() { return OptionalInt.empty(); } - /// None-negative, otherwise empty + /// Non-negative, otherwise empty public OptionalInt getSubVersion() { return OptionalInt.empty(); } diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/util/Vec3i.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/util/Vec3i.java index 5048313ea9..c4d3357b39 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/util/Vec3i.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/util/Vec3i.java @@ -19,6 +19,6 @@ public record Vec3i(int x, int y, int z) { - public static Vec3i ZERO = new Vec3i(0, 0, 0); + public static final Vec3i ZERO = new Vec3i(0, 0, 0); } From 287b7627e5e42e5985b86ac5a45df57de3819de1 Mon Sep 17 00:00:00 2001 From: Calboot Date: Fri, 6 Feb 2026 11:44:59 +0800 Subject: [PATCH 39/53] update --- .../java/org/jackhuang/hmcl/ui/versions/SchematicsPage.java | 6 ++++-- .../java/org/jackhuang/hmcl/game/DefaultGameRepository.java | 6 +++--- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/SchematicsPage.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/SchematicsPage.java index 0388d9223c..5414094922 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/SchematicsPage.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/SchematicsPage.java @@ -219,10 +219,10 @@ public void refresh() { } } navigateTo(target); - setLoading(false); } else { LOG.warning("Failed to load schematics", exception); } + setLoading(false); }).start(); } @@ -292,7 +292,9 @@ public void onCreateDirectory() { } public void onRevealSchematicsFolder() { - FXUtils.openFolder(Objects.requireNonNullElse(currentDirectoryProperty().get().getPath(), schematicsDirectory)); + var d = currentDirectoryProperty().get(); + var p = d != null ? d.getPath() : schematicsDirectory; + if (p != null) FXUtils.openFolder(p); } private DirItem loadRoot(Path dir) { diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/game/DefaultGameRepository.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/game/DefaultGameRepository.java index ff7a5f0060..f0e26f8f92 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/game/DefaultGameRepository.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/game/DefaultGameRepository.java @@ -567,11 +567,11 @@ public Path getSchematicsDirectory(String id) { && conf.generic().customSchematicBaseDirectoryEnabled() && StringUtils.isNotBlank(conf.generic().customSchematicBaseDirectory()) ) { - // The given path is used if it's absolute. Otherwise, resolves it against the game directory - dir = runDir.resolve(conf.generic().customSchematicBaseDirectory()); + var p = Path.of(conf.generic().customSchematicBaseDirectory()); + dir = p.isAbsolute() ? p : runDir.resolve(p); } } catch (Exception e) { - LOG.warning("Failed to read litematica config at '%s'".formatted(config), e); + LOG.warning("Failed to load custom schematics directory from litematica config at '%s'".formatted(config), e); } } return dir; From 9a1a8bdf167e4873502000aa348c57d3dec63ebe Mon Sep 17 00:00:00 2001 From: Calboot Date: Fri, 6 Feb 2026 12:06:29 +0800 Subject: [PATCH 40/53] update --- .../main/java/org/jackhuang/hmcl/ui/versions/SchematicsPage.java | 1 - 1 file changed, 1 deletion(-) diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/SchematicsPage.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/SchematicsPage.java index c3426e7882..b38b47a525 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/SchematicsPage.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/SchematicsPage.java @@ -832,7 +832,6 @@ private static final class SchematicsPageSkin extends SkinBase { { SpinnerPane center = new SpinnerPane(); ComponentList.setVgrow(center, Priority.ALWAYS); - center.getStyleClass().add("large-spinner-pane"); center.loadingProperty().bind(skinnable.loadingProperty()); listView.setCellFactory(x -> new Cell(listView)); From 477e3724af5d0f89a00b95a1ebd17797f634f4ca Mon Sep 17 00:00:00 2001 From: Calboot Date: Fri, 6 Feb 2026 12:40:06 +0800 Subject: [PATCH 41/53] update --- .../org/jackhuang/hmcl/ui/ToolbarListPageSkin.java | 7 +++++++ .../org/jackhuang/hmcl/ui/versions/SchematicsPage.java | 8 +++----- HMCL/src/main/resources/assets/css/root.css | 10 +++++++--- 3 files changed, 17 insertions(+), 8 deletions(-) diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/ToolbarListPageSkin.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/ToolbarListPageSkin.java index 317f45f753..d7ece41179 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/ToolbarListPageSkin.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/ToolbarListPageSkin.java @@ -30,6 +30,7 @@ import javafx.scene.layout.HBox; import javafx.scene.layout.Priority; import javafx.scene.layout.StackPane; +import javafx.scene.text.TextFlow; import org.jackhuang.hmcl.ui.construct.ComponentList; import org.jackhuang.hmcl.ui.construct.SpinnerPane; @@ -100,6 +101,12 @@ public static JFXButton createDecoratorButton(String tooltip, SVG svg, Runnable return ret; } + public static TextFlow createTip() { + TextFlow textFlow = new TextFlow(); + textFlow.getStyleClass().add("list-tip"); + return textFlow; + } + protected abstract List initializeToolbar(P skinnable); protected ListCell createListCell(JFXListView listView) { diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/SchematicsPage.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/SchematicsPage.java index b38b47a525..0b8ca4b0f7 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/SchematicsPage.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/SchematicsPage.java @@ -42,7 +42,6 @@ import javafx.scene.layout.StackPane; import javafx.scene.shape.SVGPath; import javafx.scene.text.Text; -import javafx.scene.text.TextFlow; import javafx.stage.FileChooser; import org.jackhuang.hmcl.mod.ModLoaderType; import org.jackhuang.hmcl.mod.RemoteMod; @@ -77,6 +76,7 @@ import static org.jackhuang.hmcl.ui.FXUtils.ignoreEvent; import static org.jackhuang.hmcl.ui.FXUtils.onEscPressed; +import static org.jackhuang.hmcl.ui.ToolbarListPageSkin.createTip; import static org.jackhuang.hmcl.ui.ToolbarListPageSkin.createToolbarButton2; import static org.jackhuang.hmcl.util.Pair.pair; import static org.jackhuang.hmcl.util.i18n.I18n.i18n; @@ -813,8 +813,7 @@ private static final class SchematicsPageSkin extends SkinBase { } { - var tip = new TextFlow(); - tip.setStyle("-fx-font-size: 13;"); + var tip = createTip(); HBox.setMargin(tip, new Insets(5)); var tipPane = new HBox(tip); tipPane.setAlignment(Pos.CENTER_LEFT); @@ -847,8 +846,7 @@ private static final class SchematicsPageSkin extends SkinBase { } { - var relPath = new TextFlow(); - relPath.setStyle("-fx-font-size: 13;"); + var relPath = createTip(); HBox.setMargin(relPath, new Insets(5)); FXUtils.onChangeAndOperate(skinnable.currentDirectoryProperty(), currentDir -> { relPath.getChildren().clear(); diff --git a/HMCL/src/main/resources/assets/css/root.css b/HMCL/src/main/resources/assets/css/root.css index 5403bfe8bb..e9168b69e1 100644 --- a/HMCL/src/main/resources/assets/css/root.css +++ b/HMCL/src/main/resources/assets/css/root.css @@ -19,8 +19,7 @@ .root { } -.svg, -Text { +.svg { -fx-fill: -monet-on-surface; } @@ -1906,4 +1905,9 @@ Text { .line-toggle-button .jfx-toggle-button { -fx-padding: 0; -} \ No newline at end of file +} + +.list-tip Text { + -fx-font-size: 13; + -fx-fill: -monet-on-surface; +} From b1adbf2a65c4f692a1170d2396dec346d7a37d83 Mon Sep 17 00:00:00 2001 From: Calboot Date: Fri, 6 Feb 2026 12:54:36 +0800 Subject: [PATCH 42/53] update --- .../hmcl/ui/versions/SchematicsPage.java | 44 ++++++++++++------- 1 file changed, 28 insertions(+), 16 deletions(-) diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/SchematicsPage.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/SchematicsPage.java index 0b8ca4b0f7..cff7c76166 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/SchematicsPage.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/SchematicsPage.java @@ -74,8 +74,7 @@ import java.util.*; import java.util.stream.Stream; -import static org.jackhuang.hmcl.ui.FXUtils.ignoreEvent; -import static org.jackhuang.hmcl.ui.FXUtils.onEscPressed; +import static org.jackhuang.hmcl.ui.FXUtils.*; import static org.jackhuang.hmcl.ui.ToolbarListPageSkin.createTip; import static org.jackhuang.hmcl.ui.ToolbarListPageSkin.createToolbarButton2; import static org.jackhuang.hmcl.util.Pair.pair; @@ -90,6 +89,20 @@ public final class SchematicsPage extends ListPageBase impl private static RemoteMod litematica; private static RemoteMod forgematica; + private static synchronized void tryGetLitematica() { + try { + if (litematica == null) litematica = ModrinthRemoteModRepository.MODS.getModById("litematica"); + } catch (IOException ignored) { + } + } + + private static synchronized void tryGetForgematica() { + try { + if (forgematica == null) forgematica = ModrinthRemoteModRepository.MODS.getModById("forgematica"); + } catch (IOException ignored) { + } + } + private static String translateAuthorName(String author) { if (I18n.isUseChinese() && "hsds".equals(author)) { return "黑山大叔"; @@ -149,9 +162,9 @@ public void refresh() { if (schematicsDirectory == null) return; setLoading(true); - var modManager = profile.getRepository().getModManager(instanceId); Task.supplyAsync(() -> { - LitematicaState litematicaState = LitematicaState.NOT_INSTALLED; + var litematicaState = LitematicaState.NOT_INSTALLED; + var modManager = profile.getRepository().getModManager(instanceId); try { modManager.refreshMods(); var mods = modManager.getMods(); @@ -168,24 +181,23 @@ public void refresh() { } catch (IOException e) { LOG.warning("Failed to load mods, unable to check litematica", e); } + boolean shouldUseForgematica = false; if (litematicaState == LitematicaState.NOT_INSTALLED) { - try { - if (litematica == null) litematica = ModrinthRemoteModRepository.MODS.getModById("litematica"); - } catch (IOException ignored) { - } - try { - if (forgematica == null) forgematica = ModrinthRemoteModRepository.MODS.getModById("forgematica"); - } catch (IOException ignored) { + tryGetLitematica(); + shouldUseForgematica = + (modManager.getSupportedLoaders().contains(ModLoaderType.FORGE) + || modManager.getSupportedLoaders().contains(ModLoaderType.NEO_FORGED)) + && GameVersionNumber.asGameVersion(Optional.ofNullable(modManager.getGameVersion())).isAtLeast("1.16.4", "20w45a"); + if (shouldUseForgematica) { + tryGetForgematica(); } } - return pair(litematicaState, loadRoot(schematicsDirectory)); + return pair(pair(litematicaState, shouldUseForgematica), loadRoot(schematicsDirectory)); }).whenComplete(Schedulers.javafx(), (result, exception) -> { if (exception == null) { - switch (result.key()) { + switch (result.key().key()) { case NOT_INSTALLED -> { - boolean useForgematica = forgematica != null - && (modManager.getSupportedLoaders().contains(ModLoaderType.FORGE) || modManager.getSupportedLoaders().contains(ModLoaderType.NEO_FORGED)) - && GameVersionNumber.asGameVersion(Optional.ofNullable(modManager.getGameVersion())).isAtLeast("1.16.4", "20w45a"); + boolean useForgematica = forgematica != null && result.key().value(); if (useForgematica || litematica != null) { warningTip.set(pair(i18n("schematics.warning.no_litematica_install"), () -> { var modDownloads = Controllers.getDownloadPage().showModDownloads(); From de07a284b82d963515427c2b0ae40f697d14de9a Mon Sep 17 00:00:00 2001 From: Calboot Date: Fri, 6 Feb 2026 13:17:18 +0800 Subject: [PATCH 43/53] update --- .../hmcl/ui/versions/SchematicsPage.java | 33 ++++++++----------- 1 file changed, 13 insertions(+), 20 deletions(-) diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/SchematicsPage.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/SchematicsPage.java index cff7c76166..d77a9257ed 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/SchematicsPage.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/SchematicsPage.java @@ -86,22 +86,8 @@ */ public final class SchematicsPage extends ListPageBase implements VersionPage.VersionLoadable { - private static RemoteMod litematica; - private static RemoteMod forgematica; - - private static synchronized void tryGetLitematica() { - try { - if (litematica == null) litematica = ModrinthRemoteModRepository.MODS.getModById("litematica"); - } catch (IOException ignored) { - } - } - - private static synchronized void tryGetForgematica() { - try { - if (forgematica == null) forgematica = ModrinthRemoteModRepository.MODS.getModById("forgematica"); - } catch (IOException ignored) { - } - } + private static volatile RemoteMod litematica; + private static volatile RemoteMod forgematica; private static String translateAuthorName(String author) { if (I18n.isUseChinese() && "hsds".equals(author)) { @@ -158,7 +144,6 @@ public void navigateBack() { } public void refresh() { - Path schematicsDirectory = this.schematicsDirectory; if (schematicsDirectory == null) return; setLoading(true); @@ -183,13 +168,21 @@ public void refresh() { } boolean shouldUseForgematica = false; if (litematicaState == LitematicaState.NOT_INSTALLED) { - tryGetLitematica(); + if (litematica == null) { + try { + litematica = ModrinthRemoteModRepository.MODS.getModById("litematica"); + } catch (IOException ignored) { + } + } shouldUseForgematica = (modManager.getSupportedLoaders().contains(ModLoaderType.FORGE) || modManager.getSupportedLoaders().contains(ModLoaderType.NEO_FORGED)) && GameVersionNumber.asGameVersion(Optional.ofNullable(modManager.getGameVersion())).isAtLeast("1.16.4", "20w45a"); - if (shouldUseForgematica) { - tryGetForgematica(); + if (forgematica == null && shouldUseForgematica) { + try { + forgematica = ModrinthRemoteModRepository.MODS.getModById("forgematica"); + } catch (IOException ignored) { + } } } return pair(pair(litematicaState, shouldUseForgematica), loadRoot(schematicsDirectory)); From fadde6ebb20bb8a9f53dcb3e9ebc7fe91be99395 Mon Sep 17 00:00:00 2001 From: Calboot Date: Fri, 6 Feb 2026 13:26:38 +0800 Subject: [PATCH 44/53] update --- .../hmcl/ui/versions/SchematicsPage.java | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/SchematicsPage.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/SchematicsPage.java index d77a9257ed..0cf20940fe 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/SchematicsPage.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/SchematicsPage.java @@ -168,17 +168,16 @@ public void refresh() { } boolean shouldUseForgematica = false; if (litematicaState == LitematicaState.NOT_INSTALLED) { - if (litematica == null) { + shouldUseForgematica = + (modManager.getSupportedLoaders().contains(ModLoaderType.FORGE) + || modManager.getSupportedLoaders().contains(ModLoaderType.NEO_FORGED)) + && GameVersionNumber.asGameVersion(Optional.ofNullable(modManager.getGameVersion())).isAtLeast("1.16.4", "20w45a"); + if (litematica == null && !shouldUseForgematica) { try { litematica = ModrinthRemoteModRepository.MODS.getModById("litematica"); } catch (IOException ignored) { } - } - shouldUseForgematica = - (modManager.getSupportedLoaders().contains(ModLoaderType.FORGE) - || modManager.getSupportedLoaders().contains(ModLoaderType.NEO_FORGED)) - && GameVersionNumber.asGameVersion(Optional.ofNullable(modManager.getGameVersion())).isAtLeast("1.16.4", "20w45a"); - if (forgematica == null && shouldUseForgematica) { + } else if (forgematica == null && shouldUseForgematica) { try { forgematica = ModrinthRemoteModRepository.MODS.getModById("forgematica"); } catch (IOException ignored) { @@ -191,7 +190,8 @@ public void refresh() { switch (result.key().key()) { case NOT_INSTALLED -> { boolean useForgematica = forgematica != null && result.key().value(); - if (useForgematica || litematica != null) { + boolean useLitematica = litematica != null && !result.key().value(); + if (useForgematica || useLitematica) { warningTip.set(pair(i18n("schematics.warning.no_litematica_install"), () -> { var modDownloads = Controllers.getDownloadPage().showModDownloads(); modDownloads.selectVersion(instanceId); From a942a20bac811afe1ece23f035f3bcd96416c361 Mon Sep 17 00:00:00 2001 From: Calboot Date: Fri, 6 Feb 2026 16:32:50 +0800 Subject: [PATCH 45/53] update --- .../org/jackhuang/hmcl/ui/versions/SchematicsPage.java | 7 +++---- .../src/main/java/org/jackhuang/hmcl/mod/ModManager.java | 5 ----- 2 files changed, 3 insertions(+), 9 deletions(-) diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/SchematicsPage.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/SchematicsPage.java index 0cf20940fe..7884ffb7db 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/SchematicsPage.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/SchematicsPage.java @@ -168,10 +168,9 @@ public void refresh() { } boolean shouldUseForgematica = false; if (litematicaState == LitematicaState.NOT_INSTALLED) { - shouldUseForgematica = - (modManager.getSupportedLoaders().contains(ModLoaderType.FORGE) - || modManager.getSupportedLoaders().contains(ModLoaderType.NEO_FORGED)) - && GameVersionNumber.asGameVersion(Optional.ofNullable(modManager.getGameVersion())).isAtLeast("1.16.4", "20w45a"); + var modLoaders = modManager.getLibraryAnalyzer().getModLoaders(); + shouldUseForgematica = (modLoaders.contains(ModLoaderType.FORGE) || modLoaders.contains(ModLoaderType.NEO_FORGED)) + && GameVersionNumber.asGameVersion(Optional.ofNullable(modManager.getGameVersion())).isAtLeast("1.16.4", "20w45a"); if (litematica == null && !shouldUseForgematica) { try { litematica = ModrinthRemoteModRepository.MODS.getModById("litematica"); diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/ModManager.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/ModManager.java index 47ee4ef676..1a58da653d 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/ModManager.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/mod/ModManager.java @@ -242,11 +242,6 @@ public void removeMods(LocalModFile... localModFiles) throws IOException { private void updateSupportedLoaders() { supportedLoaders.clear(); - if (this.analyzer == null) { - Collections.addAll(supportedLoaders, ModLoaderType.values()); - return; - } - for (LibraryAnalyzer.LibraryType type : LibraryAnalyzer.LibraryType.values()) { if (type.isModLoader() && this.analyzer.has(type)) { ModLoaderType modLoaderType = type.getModLoaderType(); From f8a96ee3850226250933f6b14f46b224d07a6620 Mon Sep 17 00:00:00 2001 From: Calboot Date: Sun, 8 Feb 2026 12:35:41 +0800 Subject: [PATCH 46/53] update --- .../java/org/jackhuang/hmcl/ui/versions/SchematicsPage.java | 2 ++ 1 file changed, 2 insertions(+) diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/SchematicsPage.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/SchematicsPage.java index 7884ffb7db..e3a6b8134f 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/SchematicsPage.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/SchematicsPage.java @@ -844,6 +844,8 @@ private static final class SchematicsPageSkin extends SkinBase { // ListViewBehavior would consume ESC pressed event, preventing us from handling it // So we ignore it here ignoreEvent(listView, KeyEvent.KEY_PRESSED, e -> e.getCode() == KeyCode.ESCAPE); + listView.getStyleClass().add("no-horizontal-scrollbar"); + center.setContent(listView); root.getContent().add(center); From 212c91260d6cea82e5a8430b0ec10538cfe1eca9 Mon Sep 17 00:00:00 2001 From: Calboot Date: Sun, 8 Feb 2026 21:51:45 +0800 Subject: [PATCH 47/53] rename --- .../jackhuang/hmcl/schematic/LitematicFile.java | 10 +++++----- .../hmcl/schematic/NBTStructureFile.java | 10 +++++----- .../org/jackhuang/hmcl/schematic/SchemFile.java | 16 ++++++++-------- .../org/jackhuang/hmcl/schematic/Schematic.java | 10 +++++----- .../hmcl/util/{Vec3i.java => Point3I.java} | 4 ++-- .../jackhuang/hmcl/schematic/SchematicTest.java | 10 +++++----- 6 files changed, 30 insertions(+), 30 deletions(-) rename HMCLCore/src/main/java/org/jackhuang/hmcl/util/{Vec3i.java => Point3I.java} (88%) diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/schematic/LitematicFile.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/schematic/LitematicFile.java index 1c1c584213..3f43768b57 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/schematic/LitematicFile.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/schematic/LitematicFile.java @@ -20,7 +20,7 @@ import com.github.steveice10.opennbt.tag.builtin.*; import org.jackhuang.hmcl.util.Lang; import org.jackhuang.hmcl.util.StringUtils; -import org.jackhuang.hmcl.util.Vec3i; +import org.jackhuang.hmcl.util.Point3I; import org.jetbrains.annotations.NotNull; import java.io.IOException; @@ -55,15 +55,15 @@ else if (!(metadataTag instanceof CompoundTag)) if (regionsTag instanceof CompoundTag) regions = ((CompoundTag) regionsTag).size(); - Vec3i enclosingSize = null; - Tag enclosingSizeTag = ((CompoundTag) metadataTag).get("EnclosingSize"); + Point3I enclosingSize = null; + Tag enclosingSizeTag = ((CompoundTag) metadataTag).get("EnclosingSize"); if (enclosingSizeTag instanceof CompoundTag) { CompoundTag list = (CompoundTag) enclosingSizeTag; int x = tryGetInt(list.get("x")).orElse(0); int y = tryGetInt(list.get("y")).orElse(0); int z = tryGetInt(list.get("z")).orElse(0); - if (x > 0 && y > 0 && z > 0) enclosingSize = new Vec3i(x, y, z); + if (x > 0 && y > 0 && z > 0) enclosingSize = new Point3I(x, y, z); } Tag subVersionTag = root.get("SubVersion"); @@ -89,7 +89,7 @@ else if (!(metadataTag instanceof CompoundTag)) private final int totalVolume; private LitematicFile(@NotNull Path file, @NotNull CompoundTag metadata, - int version, int subVersion, int minecraftDataVersion, int regionCount, Vec3i enclosingSize) { + int version, int subVersion, int minecraftDataVersion, int regionCount, Point3I enclosingSize) { super(file, minecraftDataVersion, enclosingSize); this.version = version; this.subVersion = subVersion; diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/schematic/NBTStructureFile.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/schematic/NBTStructureFile.java index 79e245ff78..431055f22a 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/schematic/NBTStructureFile.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/schematic/NBTStructureFile.java @@ -18,7 +18,7 @@ package org.jackhuang.hmcl.schematic; import com.github.steveice10.opennbt.tag.builtin.*; -import org.jackhuang.hmcl.util.Vec3i; +import org.jackhuang.hmcl.util.Point3I; import java.io.IOException; import java.nio.file.Path; @@ -48,14 +48,14 @@ else if (!(sizeTag instanceof ListTag)) throw new IOException("size tag does not have 3 elements"); Tag xTag = size.get(0); Tag yTag = size.get(1); - Tag zTag = size.get(2); - Vec3i enclosingSize = null; + Tag zTag = size.get(2); + Point3I enclosingSize = null; if (xTag != null && yTag != null && zTag != null) { int width = tryGetInt(xTag).orElse(0); int height = tryGetInt(yTag).orElse(0); int length = tryGetInt(zTag).orElse(0); if (width > 0 && height > 0 && length > 0) { - enclosingSize = new Vec3i(width, height, length); + enclosingSize = new Point3I(width, height, length); } } @@ -64,7 +64,7 @@ else if (!(sizeTag instanceof ListTag)) private final String author; - private NBTStructureFile(Path file, int dataVersion, String author, Vec3i enclosingSize) { + private NBTStructureFile(Path file, int dataVersion, String author, Point3I enclosingSize) { super(file, dataVersion, enclosingSize); this.author = author; } diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/schematic/SchemFile.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/schematic/SchemFile.java index c39ee09084..c6df302283 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/schematic/SchemFile.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/schematic/SchemFile.java @@ -21,7 +21,7 @@ import com.github.steveice10.opennbt.tag.builtin.IntTag; import com.github.steveice10.opennbt.tag.builtin.StringTag; import com.github.steveice10.opennbt.tag.builtin.Tag; -import org.jackhuang.hmcl.util.Vec3i; +import org.jackhuang.hmcl.util.Point3I; import org.jetbrains.annotations.Nullable; import java.io.IOException; @@ -52,14 +52,14 @@ private static SchemFile loadLegacy(Path file, CompoundTag root) throws IOExcept Tag widthTag = root.get("Width"); Tag heightTag = root.get("Height"); - Tag lengthTag = root.get("Length"); - Vec3i enclosingSize = null; + Tag lengthTag = root.get("Length"); + Point3I enclosingSize = null; if (widthTag != null && heightTag != null && lengthTag != null) { short width = tryGetShort(widthTag); short height = tryGetShort(heightTag); short length = tryGetShort(lengthTag); if (width >= 0 && height >= 0 && length >= 0) { - enclosingSize = new Vec3i(width, height, length); + enclosingSize = new Point3I(width, height, length); } } @@ -76,13 +76,13 @@ private static SchemFile loadSponge(Path file, CompoundTag root) throws IOExcept Tag widthTag = root.get("Width"); Tag heightTag = root.get("Height"); - Tag lengthTag = root.get("Length"); - Vec3i enclosingSize = null; + Tag lengthTag = root.get("Length"); + Point3I enclosingSize = null; if (widthTag != null && heightTag != null && lengthTag != null) { int width = tryGetShort(widthTag) & 0xFFFF; int height = tryGetShort(heightTag) & 0xFFFF; int length = tryGetShort(lengthTag) & 0xFFFF; - enclosingSize = new Vec3i(width, height, length); + enclosingSize = new Point3I(width, height, length); } return new SchemFile(file, null, dataVersion, ((IntTag) versionTag).getValue(), enclosingSize); @@ -91,7 +91,7 @@ private static SchemFile loadSponge(Path file, CompoundTag root) throws IOExcept private final String materials; private final int version; - private SchemFile(Path file, @Nullable String materials, int dataVersion, int version, Vec3i enclosingSize) { + private SchemFile(Path file, @Nullable String materials, int dataVersion, int version, Point3I enclosingSize) { super(file, dataVersion, enclosingSize); this.materials = materials; this.version = version; diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/schematic/Schematic.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/schematic/Schematic.java index bc8c838d53..a31fe6a087 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/schematic/Schematic.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/schematic/Schematic.java @@ -20,7 +20,7 @@ import com.github.steveice10.opennbt.NBTIO; import com.github.steveice10.opennbt.tag.builtin.*; import org.jackhuang.hmcl.util.Lang; -import org.jackhuang.hmcl.util.Vec3i; +import org.jackhuang.hmcl.util.Point3I; import org.jackhuang.hmcl.util.io.FileUtils; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; @@ -78,10 +78,10 @@ public static short tryGetShort(Tag tag) { } private final Path file; - private final int dataVersion; - private final Vec3i enclosingSize; + private final int dataVersion; + private final Point3I enclosingSize; - protected Schematic(Path file, int dataVersion, @Nullable Vec3i enclosingSize) { + protected Schematic(Path file, int dataVersion, @Nullable Point3I enclosingSize) { this.file = file; this.dataVersion = dataVersion; this.enclosingSize = enclosingSize; @@ -142,7 +142,7 @@ public OptionalInt getTotalBlocks() { } @Nullable - public Vec3i getEnclosingSize() { + public Point3I getEnclosingSize() { return enclosingSize; } diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/util/Vec3i.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/util/Point3I.java similarity index 88% rename from HMCLCore/src/main/java/org/jackhuang/hmcl/util/Vec3i.java rename to HMCLCore/src/main/java/org/jackhuang/hmcl/util/Point3I.java index c4d3357b39..d478dca470 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/util/Vec3i.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/util/Point3I.java @@ -17,8 +17,8 @@ */ package org.jackhuang.hmcl.util; -public record Vec3i(int x, int y, int z) { +public record Point3I(int x, int y, int z) { - public static final Vec3i ZERO = new Vec3i(0, 0, 0); + public static final Point3I ZERO = new Point3I(0, 0, 0); } diff --git a/HMCLCore/src/test/java/org/jackhuang/hmcl/schematic/SchematicTest.java b/HMCLCore/src/test/java/org/jackhuang/hmcl/schematic/SchematicTest.java index f8ce9938a1..529c83b50e 100644 --- a/HMCLCore/src/test/java/org/jackhuang/hmcl/schematic/SchematicTest.java +++ b/HMCLCore/src/test/java/org/jackhuang/hmcl/schematic/SchematicTest.java @@ -18,7 +18,7 @@ package org.jackhuang.hmcl.schematic; import org.jackhuang.hmcl.game.CrashReportAnalyzerTest; -import org.jackhuang.hmcl.util.Vec3i; +import org.jackhuang.hmcl.util.Point3I; import org.junit.jupiter.api.Test; import java.io.IOException; @@ -49,7 +49,7 @@ public void test() throws Exception { assertEquals(Instant.ofEpochMilli(1746443586433L), lFile.getTimeModified()); assertEquals(1334, lFile.getTotalBlocks().orElse(0)); assertEquals(5746, lFile.getTotalVolume().orElse(0)); - assertEquals(new Vec3i(17, 26, 13), lFile.getEnclosingSize()); + assertEquals(new Point3I(17, 26, 13), lFile.getEnclosingSize()); assertEquals(1, lFile.getRegionCount().orElse(0)); assertEquals(4325, lFile.getMinecraftDataVersion().orElse(0)); assertEquals("4325", lFile.getMinecraftVersion()); @@ -61,7 +61,7 @@ public void test() throws Exception { SchemFile sFile = (SchemFile) load("/schematics/test.schematic"); assertEquals(SchematicType.SCHEM, sFile.getType()); assertEquals("test", sFile.getName()); - assertEquals(new Vec3i(28, 35, 18), sFile.getEnclosingSize()); + assertEquals(new Point3I(28, 35, 18), sFile.getEnclosingSize()); assertEquals(17640, sFile.getTotalVolume().orElse(0)); assertEquals("Alpha", sFile.getMinecraftVersion()); } @@ -72,14 +72,14 @@ public void test() throws Exception { assertEquals("test", sFileSponge.getName()); assertEquals(3465, sFileSponge.getMinecraftDataVersion().orElse(0)); assertEquals("3465", sFileSponge.getMinecraftVersion()); - assertEquals(new Vec3i(9, 5, 9), sFileSponge.getEnclosingSize()); + assertEquals(new Point3I(9, 5, 9), sFileSponge.getEnclosingSize()); } { NBTStructureFile nFile = (NBTStructureFile) load("/schematics/test.nbt"); assertEquals(SchematicType.NBT_STRUCTURE, nFile.getType()); assertEquals("test", nFile.getName()); - assertEquals(new Vec3i(9, 11, 13), nFile.getEnclosingSize()); + assertEquals(new Point3I(9, 11, 13), nFile.getEnclosingSize()); assertEquals(1287, nFile.getTotalVolume().orElse(0)); assertEquals(3465, nFile.getMinecraftDataVersion().orElse(0)); assertEquals("3465", nFile.getMinecraftVersion()); From 6c59dafa26bddb3d4e1c399c01b37cecacbcd1b1 Mon Sep 17 00:00:00 2001 From: Calboot Date: Sun, 8 Feb 2026 21:55:58 +0800 Subject: [PATCH 48/53] update --- .../main/java/org/jackhuang/hmcl/schematic/LitematicFile.java | 4 ++-- .../java/org/jackhuang/hmcl/schematic/NBTStructureFile.java | 2 +- .../src/main/java/org/jackhuang/hmcl/schematic/SchemFile.java | 4 ++-- .../src/main/java/org/jackhuang/hmcl/schematic/Schematic.java | 2 +- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/schematic/LitematicFile.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/schematic/LitematicFile.java index 3f43768b57..948dbf2e47 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/schematic/LitematicFile.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/schematic/LitematicFile.java @@ -55,8 +55,8 @@ else if (!(metadataTag instanceof CompoundTag)) if (regionsTag instanceof CompoundTag) regions = ((CompoundTag) regionsTag).size(); - Point3I enclosingSize = null; - Tag enclosingSizeTag = ((CompoundTag) metadataTag).get("EnclosingSize"); + Point3I enclosingSize = null; + Tag enclosingSizeTag = ((CompoundTag) metadataTag).get("EnclosingSize"); if (enclosingSizeTag instanceof CompoundTag) { CompoundTag list = (CompoundTag) enclosingSizeTag; int x = tryGetInt(list.get("x")).orElse(0); diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/schematic/NBTStructureFile.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/schematic/NBTStructureFile.java index 431055f22a..ae6c3d7a24 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/schematic/NBTStructureFile.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/schematic/NBTStructureFile.java @@ -48,7 +48,7 @@ else if (!(sizeTag instanceof ListTag)) throw new IOException("size tag does not have 3 elements"); Tag xTag = size.get(0); Tag yTag = size.get(1); - Tag zTag = size.get(2); + Tag zTag = size.get(2); Point3I enclosingSize = null; if (xTag != null && yTag != null && zTag != null) { int width = tryGetInt(xTag).orElse(0); diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/schematic/SchemFile.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/schematic/SchemFile.java index c6df302283..c3cf8acc6e 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/schematic/SchemFile.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/schematic/SchemFile.java @@ -52,7 +52,7 @@ private static SchemFile loadLegacy(Path file, CompoundTag root) throws IOExcept Tag widthTag = root.get("Width"); Tag heightTag = root.get("Height"); - Tag lengthTag = root.get("Length"); + Tag lengthTag = root.get("Length"); Point3I enclosingSize = null; if (widthTag != null && heightTag != null && lengthTag != null) { short width = tryGetShort(widthTag); @@ -76,7 +76,7 @@ private static SchemFile loadSponge(Path file, CompoundTag root) throws IOExcept Tag widthTag = root.get("Width"); Tag heightTag = root.get("Height"); - Tag lengthTag = root.get("Length"); + Tag lengthTag = root.get("Length"); Point3I enclosingSize = null; if (widthTag != null && heightTag != null && lengthTag != null) { int width = tryGetShort(widthTag) & 0xFFFF; diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/schematic/Schematic.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/schematic/Schematic.java index a31fe6a087..03f99edd87 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/schematic/Schematic.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/schematic/Schematic.java @@ -78,7 +78,7 @@ public static short tryGetShort(Tag tag) { } private final Path file; - private final int dataVersion; + private final int dataVersion; private final Point3I enclosingSize; protected Schematic(Path file, int dataVersion, @Nullable Point3I enclosingSize) { From 39d54ea88891d462c88c53ba373ab14a77526595 Mon Sep 17 00:00:00 2001 From: Calboot Date: Sun, 8 Feb 2026 22:07:19 +0800 Subject: [PATCH 49/53] update --- .../hmcl/ui/versions/SchematicsPage.java | 36 ++++++++++--------- 1 file changed, 20 insertions(+), 16 deletions(-) diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/SchematicsPage.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/SchematicsPage.java index e3a6b8134f..26d7b41f95 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/SchematicsPage.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/SchematicsPage.java @@ -58,7 +58,6 @@ import org.jackhuang.hmcl.ui.SVG; import org.jackhuang.hmcl.ui.construct.*; import org.jackhuang.hmcl.ui.nbt.NBTEditorPage; -import org.jackhuang.hmcl.util.Pair; import org.jackhuang.hmcl.util.StringUtils; import org.jackhuang.hmcl.util.i18n.I18n; import org.jackhuang.hmcl.util.io.FileUtils; @@ -77,7 +76,6 @@ import static org.jackhuang.hmcl.ui.FXUtils.*; import static org.jackhuang.hmcl.ui.ToolbarListPageSkin.createTip; import static org.jackhuang.hmcl.ui.ToolbarListPageSkin.createToolbarButton2; -import static org.jackhuang.hmcl.util.Pair.pair; import static org.jackhuang.hmcl.util.i18n.I18n.i18n; import static org.jackhuang.hmcl.util.logging.Logger.LOG; @@ -104,7 +102,7 @@ private static String translateType(SchematicType type) { private String instanceId; private Path schematicsDirectory; private final ObjectProperty currentDirectory = new SimpleObjectProperty<>(this, "currentDirectory", null); - private final ObjectProperty> warningTip = new SimpleObjectProperty<>(this, "tip", pair(null, null)); + private final ObjectProperty warningTip = new SimpleObjectProperty<>(this, "tip", null); private final BooleanProperty isRootProperty = new SimpleBooleanProperty(this, "isRoot", true); @@ -147,7 +145,7 @@ public void refresh() { if (schematicsDirectory == null) return; setLoading(true); - Task.supplyAsync(() -> { + Task.supplyAsync(Schedulers.io(), () -> { var litematicaState = LitematicaState.NOT_INSTALLED; var modManager = profile.getRepository().getModManager(instanceId); try { @@ -183,15 +181,15 @@ public void refresh() { } } } - return pair(pair(litematicaState, shouldUseForgematica), loadRoot(schematicsDirectory)); + return new LoadResult(litematicaState, shouldUseForgematica, loadRoot(schematicsDirectory)); }).whenComplete(Schedulers.javafx(), (result, exception) -> { if (exception == null) { - switch (result.key().key()) { + switch (result.state()) { case NOT_INSTALLED -> { - boolean useForgematica = forgematica != null && result.key().value(); - boolean useLitematica = litematica != null && !result.key().value(); + boolean useForgematica = forgematica != null && result.shouldUseForgematica(); + boolean useLitematica = litematica != null && !result.shouldUseForgematica(); if (useForgematica || useLitematica) { - warningTip.set(pair(i18n("schematics.warning.no_litematica_install"), () -> { + warningTip.set(new WarningTip(i18n("schematics.warning.no_litematica_install"), () -> { var modDownloads = Controllers.getDownloadPage().showModDownloads(); modDownloads.selectVersion(instanceId); Controllers.navigate(new DownloadPage( @@ -202,13 +200,13 @@ public void refresh() { ); })); } else { - warningTip.set(pair(i18n("schematics.warning.no_litematica"), null)); + warningTip.set(new WarningTip(i18n("schematics.warning.no_litematica"), null)); } } - case DISABLED -> warningTip.set(pair(i18n("schematics.warning.litematica_disabled"), null)); - default -> warningTip.set(pair(null, null)); + case DISABLED -> warningTip.set(new WarningTip(i18n("schematics.warning.litematica_disabled"), null)); + default -> warningTip.set(new WarningTip(null, null)); } - DirItem target = result.value(); + DirItem target = result.rootDirItem(); if (currentDirectoryProperty().get() != null) { loop: for (String dirName : currentDirectoryProperty().get().relativePath) { @@ -823,9 +821,9 @@ private static final class SchematicsPageSkin extends SkinBase { tipPane.setAlignment(Pos.CENTER_LEFT); FXUtils.onChangeAndOperate(skinnable.warningTip, pair -> { root.getContent().remove(tipPane); - if (pair != null && !StringUtils.isBlank(pair.key())) { - var txt = new Text(pair.key()); - if (pair.value() != null) FXUtils.onClicked(txt, pair.value()); + if (pair != null && !StringUtils.isBlank(pair.message())) { + var txt = new Text(pair.message()); + if (pair.action() != null) FXUtils.onClicked(txt, pair.action()); tip.getChildren().setAll(txt); root.getContent().add(1, tipPane); } @@ -882,4 +880,10 @@ private enum LitematicaState { OK } + private record LoadResult(LitematicaState state, boolean shouldUseForgematica, DirItem rootDirItem) { + } + + private record WarningTip(String message, Runnable action) { + } + } From a3b9a87c4613755b1f2c23195e7e4b9c9887c0ea Mon Sep 17 00:00:00 2001 From: Calboot Date: Sun, 8 Feb 2026 22:20:50 +0800 Subject: [PATCH 50/53] Apply AI suggestions --- .../hmcl/ui/versions/SchematicsPage.java | 66 +++++++++---------- 1 file changed, 33 insertions(+), 33 deletions(-) diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/SchematicsPage.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/SchematicsPage.java index 26d7b41f95..6f129d97bf 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/SchematicsPage.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/SchematicsPage.java @@ -148,6 +148,7 @@ public void refresh() { Task.supplyAsync(Schedulers.io(), () -> { var litematicaState = LitematicaState.NOT_INSTALLED; var modManager = profile.getRepository().getModManager(instanceId); + boolean shouldUseForgematica = false; try { modManager.refreshMods(); var mods = modManager.getMods(); @@ -161,27 +162,40 @@ public void refresh() { } } } + if (litematicaState == LitematicaState.NOT_INSTALLED && modManager.getLibraryAnalyzer() != null) { + var modLoaders = modManager.getLibraryAnalyzer().getModLoaders(); + shouldUseForgematica = (modLoaders.contains(ModLoaderType.FORGE) || modLoaders.contains(ModLoaderType.NEO_FORGED)) + && GameVersionNumber.asGameVersion(Optional.ofNullable(modManager.getGameVersion())).isAtLeast("1.16.4", "20w45a"); + if (litematica == null && !shouldUseForgematica) { + try { + litematica = ModrinthRemoteModRepository.MODS.getModById("litematica"); + } catch (IOException ignored) { + } + } else if (forgematica == null && shouldUseForgematica) { + try { + forgematica = ModrinthRemoteModRepository.MODS.getModById("forgematica"); + } catch (IOException ignored) { + } + } + } } catch (IOException e) { LOG.warning("Failed to load mods, unable to check litematica", e); } - boolean shouldUseForgematica = false; - if (litematicaState == LitematicaState.NOT_INSTALLED) { - var modLoaders = modManager.getLibraryAnalyzer().getModLoaders(); - shouldUseForgematica = (modLoaders.contains(ModLoaderType.FORGE) || modLoaders.contains(ModLoaderType.NEO_FORGED)) - && GameVersionNumber.asGameVersion(Optional.ofNullable(modManager.getGameVersion())).isAtLeast("1.16.4", "20w45a"); - if (litematica == null && !shouldUseForgematica) { - try { - litematica = ModrinthRemoteModRepository.MODS.getModById("litematica"); - } catch (IOException ignored) { - } - } else if (forgematica == null && shouldUseForgematica) { - try { - forgematica = ModrinthRemoteModRepository.MODS.getModById("forgematica"); - } catch (IOException ignored) { + DirItem target = loadRoot(schematicsDirectory); + if (currentDirectoryProperty().get() != null) { + loop: + for (String dirName : currentDirectoryProperty().get().relativePath) { + target.preLoad(); + for (var dirChild : target.dirChildren) { + if (dirChild.getName().equals(dirName)) { + target = dirChild; + continue loop; + } } + break; } } - return new LoadResult(litematicaState, shouldUseForgematica, loadRoot(schematicsDirectory)); + return new LoadResult(litematicaState, shouldUseForgematica, target); }).whenComplete(Schedulers.javafx(), (result, exception) -> { if (exception == null) { switch (result.state()) { @@ -206,21 +220,7 @@ public void refresh() { case DISABLED -> warningTip.set(new WarningTip(i18n("schematics.warning.litematica_disabled"), null)); default -> warningTip.set(new WarningTip(null, null)); } - DirItem target = result.rootDirItem(); - if (currentDirectoryProperty().get() != null) { - loop: - for (String dirName : currentDirectoryProperty().get().relativePath) { - target.preLoad(); - for (var dirChild : target.dirChildren) { - if (dirChild.getName().equals(dirName)) { - target = dirChild; - continue loop; - } - } - break; - } - } - navigateTo(target); + navigateTo(result.targetDir()); } else { LOG.warning("Failed to load schematics", exception); } @@ -435,7 +435,7 @@ void load() { try { this.children.add(new SchematicItem(p)); } catch (IOException e) { - LOG.warning("Failed to load schematic file: " + path, e); + LOG.warning("Failed to load schematic file: " + p, e); } }); } @@ -479,7 +479,7 @@ private SchematicItem(Path path) throws IOException { this.path = path; this.file = Schematic.load(path); - if (file == null) throw new AssertionError(); // Should be impossible + if (file == null) throw new IOException("Unsupported or deleted file: " + path); if (this.file instanceof LitematicFile lFile) { String name = lFile.getName(); @@ -880,7 +880,7 @@ private enum LitematicaState { OK } - private record LoadResult(LitematicaState state, boolean shouldUseForgematica, DirItem rootDirItem) { + private record LoadResult(LitematicaState state, boolean shouldUseForgematica, DirItem targetDir) { } private record WarningTip(String message, Runnable action) { From b178014e5b3ed8fdef957c336a8c18d8dae0be5e Mon Sep 17 00:00:00 2001 From: Calboot Date: Thu, 19 Feb 2026 20:48:48 +0800 Subject: [PATCH 51/53] update --- .../java/org/jackhuang/hmcl/ui/versions/SchematicsPage.java | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/SchematicsPage.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/SchematicsPage.java index c2e288cdf7..96735ae0c6 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/SchematicsPage.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/SchematicsPage.java @@ -50,10 +50,7 @@ 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.FXUtils; -import org.jackhuang.hmcl.ui.ListPageBase; -import org.jackhuang.hmcl.ui.SVG; +import org.jackhuang.hmcl.ui.*; import org.jackhuang.hmcl.ui.construct.*; import org.jackhuang.hmcl.ui.nbt.NBTEditorPage; import org.jackhuang.hmcl.util.StringUtils; From 967e23d3527fa13f510fcdd3cab408da1fc10980 Mon Sep 17 00:00:00 2001 From: Calboot Date: Wed, 25 Feb 2026 10:54:32 +0800 Subject: [PATCH 52/53] =?UTF-8?q?=E4=BC=98=E5=8C=96UI?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/org/jackhuang/hmcl/ui/versions/SchematicsPage.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/SchematicsPage.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/SchematicsPage.java index 253f0c2c8c..2672b49f76 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/SchematicsPage.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/SchematicsPage.java @@ -624,8 +624,8 @@ private void updateContent(Schematic file) { StackPane detailsContainer = new StackPane(); detailsContainer.getChildren().add(details); ScrollPane scrollPane = new ScrollPane(detailsContainer); - scrollPane.setStyle("-fx-effect: dropshadow(gaussian, rgba(0, 0, 0, 0.26), 5, 0.06, -0.5, 1);"); scrollPane.setFitToWidth(true); + FXUtils.setOverflowHidden(scrollPane, 8); FXUtils.smoothScrolling(scrollPane); StackPane.setMargin(scrollPane, new Insets(10, 0, 0, 0)); setBody(scrollPane); From 5774b508c3d95554755986da7ad1f28eb75f5884 Mon Sep 17 00:00:00 2001 From: Calboot Date: Thu, 26 Feb 2026 22:02:43 +0800 Subject: [PATCH 53/53] update --- .../java/org/jackhuang/hmcl/ui/versions/SchematicsPage.java | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/SchematicsPage.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/SchematicsPage.java index a8c8715cc5..3e849577b7 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/SchematicsPage.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/SchematicsPage.java @@ -687,7 +687,6 @@ public Cell(JFXListView listView) { JFXButton btnReveal = FXUtils.newToggleButton4(SVG.FOLDER_OPEN); FXUtils.installFastTooltip(btnReveal, i18n("reveal.in_file_manager")); - btnReveal.getStyleClass().add("toggle-icon4"); { var fo = SVG.FOLDER_OPEN.createIcon(); var f = SVG.FOLDER.createIcon(); @@ -698,9 +697,7 @@ public Cell(JFXListView listView) { if (item != null) item.onReveal(); }); - JFXButton btnExplore = new JFXButton(); - btnExplore.getStyleClass().add("toggle-icon4"); - btnExplore.setGraphic(SVG.EXPLORE.createIcon()); // Change the icon if allows editing + JFXButton btnExplore = FXUtils.newToggleButton4(SVG.EXPLORE); // Change the icon if allows editing btnExplore.setOnAction(event -> { Item item = getItem(); if (item instanceof SchematicItem) {