[Enhancement] 原理图管理优化 (#6094)
Co-authored-by: Calboot <calboot39@outlook.com> Co-authored-by: 3gf8jv4dv <3gf8jv4dv@gmail.com> Co-authored-by: Glavo <zjx001202@gmail.com> Co-authored-by: CalbootOnceMore <calboot43@outlook.com>
This commit is contained in:
35 files changed
+1426
-425
No files matched your search
@@ -98,5 +98,9 @@ public abstract class MDListCell<T> extends ListCell<T> {
|
||||
});
|
||||
}
|
||||
|
||||
protected void onClicked(Runnable action) {
|
||||
FXUtils.onClicked(ripplerContainer, action);
|
||||
}
|
||||
|
||||
protected abstract void updateControl(T item, boolean empty);
|
||||
}
|
||||
@@ -158,6 +158,10 @@ public class DownloadListPage extends Control implements DecoratorPage {
|
||||
FXUtils.runInFX(() -> selectedInstance.set(instanceId));
|
||||
}
|
||||
|
||||
public DownloadPage.DownloadCallback getCallback() {
|
||||
return callback;
|
||||
}
|
||||
|
||||
private void search(String userGameVersion, RemoteAddonRepository.Category category, int pageOffset, String searchFilter, RemoteAddonRepository.SortType sort) {
|
||||
retrySearch = null;
|
||||
setLoading(true);
|
||||
|
||||
@@ -207,6 +207,14 @@ public class GameInstancePage extends DecoratorAnimatedPage implements Decorator
|
||||
FXUtils.openFolder(gameInstance.getRunDirectory().resolve(sub));
|
||||
}
|
||||
|
||||
private void onBrowseSchematics() {
|
||||
HMCLGameInstance gameInstance = requireGameInstance();
|
||||
if (gameInstance == null) {
|
||||
return;
|
||||
}
|
||||
FXUtils.openFolder(gameInstance.getSchematicsDirectory());
|
||||
}
|
||||
|
||||
private void redownloadAssetIndex() {
|
||||
HMCLGameInstance gameInstance = requireGameInstance();
|
||||
if (gameInstance != null) {
|
||||
@@ -358,7 +366,7 @@ public class GameInstancePage extends DecoratorAnimatedPage implements Decorator
|
||||
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),
|
||||
|
||||
@@ -35,6 +35,10 @@ public final class HMCLLocalizedDownloadListPage extends DownloadListPage {
|
||||
return new HMCLLocalizedDownloadListPage(callback, instanceSelection, RemoteAddon.Type.MOD, CurseForgeRemoteAddonRepository.MODS, ModrinthRemoteAddonRepository.MODS);
|
||||
}
|
||||
|
||||
public static DownloadListPage ofModrinthMod(DownloadPage.DownloadCallback callback, boolean instanceSelection) {
|
||||
return new HMCLLocalizedDownloadListPage(callback, instanceSelection, RemoteAddon.Type.MOD, null, ModrinthRemoteAddonRepository.MODS);
|
||||
}
|
||||
|
||||
public static DownloadListPage ofModPack(DownloadPage.DownloadCallback callback, boolean instanceSelection) {
|
||||
return new HMCLLocalizedDownloadListPage(callback, instanceSelection, RemoteAddon.Type.MODPACK, CurseForgeRemoteAddonRepository.MODPACKS, ModrinthRemoteAddonRepository.MODPACKS);
|
||||
}
|
||||
|
||||
@@ -95,7 +95,6 @@ public final class ModListPage extends ListPageBase<ModListPage.ModInfoObject> i
|
||||
|
||||
private ModManager modManager;
|
||||
private @Nullable HMCLGameInstance gameInstance;
|
||||
private String gameVersion;
|
||||
|
||||
final EnumSet<ModLoaderType> supportedLoaders = EnumSet.noneOf(ModLoaderType.class);
|
||||
|
||||
@@ -137,8 +136,6 @@ public final class ModListPage extends ListPageBase<ModListPage.ModInfoObject> i
|
||||
return;
|
||||
}
|
||||
|
||||
this.gameVersion = gameInstance.getVersion().toString();
|
||||
|
||||
loadMods(gameInstance.getModManager());
|
||||
}
|
||||
|
||||
@@ -164,7 +161,8 @@ public final class ModListPage extends ListPageBase<ModListPage.ModInfoObject> i
|
||||
return;
|
||||
}
|
||||
|
||||
updateSupportedLoaders(modManager);
|
||||
supportedLoaders.clear();
|
||||
supportedLoaders.addAll(modManager.getSupportedLoaders());
|
||||
|
||||
if (exception == null) {
|
||||
getItems().setAll(list);
|
||||
@@ -176,51 +174,6 @@ public final class ModListPage extends ListPageBase<ModListPage.ModInfoObject> i
|
||||
}, Schedulers.javafx());
|
||||
}
|
||||
|
||||
private void updateSupportedLoaders(ModManager modManager) {
|
||||
supportedLoaders.clear();
|
||||
|
||||
GameComponentAnalyzer analyzer = modManager.getComponentAnalyzer();
|
||||
if (analyzer == null) {
|
||||
Collections.addAll(supportedLoaders, ModLoaderType.values());
|
||||
return;
|
||||
}
|
||||
|
||||
for (GameComponentType type : GameComponentType.MOD_LOADERS) {
|
||||
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(GameComponentType.NEO_FORGE) && "1.20.1".equals(gameVersion)) {
|
||||
supportedLoaders.add(ModLoaderType.FORGE);
|
||||
}
|
||||
|
||||
if (analyzer.has(GameComponentType.QUILT)) {
|
||||
supportedLoaders.add(ModLoaderType.FABRIC);
|
||||
}
|
||||
|
||||
if (analyzer.has(GameComponentType.LEGACY_FABRIC)) {
|
||||
supportedLoaders.add(ModLoaderType.FABRIC);
|
||||
}
|
||||
|
||||
if (analyzer.has(GameComponentType.FABRIC) && modManager.hasMod("kilt", ModLoaderType.FABRIC)) {
|
||||
supportedLoaders.add(ModLoaderType.FORGE);
|
||||
supportedLoaders.add(ModLoaderType.NEO_FORGE);
|
||||
}
|
||||
|
||||
// Sinytra Connector
|
||||
if (analyzer.has(GameComponentType.NEO_FORGE) && (modManager.hasMod("connector", ModLoaderType.NEO_FORGE) || modManager.hasMod("connectormod", ModLoaderType.NEO_FORGE))
|
||||
|| "1.20.1".equals(gameVersion) && analyzer.has(GameComponentType.FORGE) && modManager.hasMod("connectormod", ModLoaderType.FORGE)) {
|
||||
supportedLoaders.add(ModLoaderType.FABRIC);
|
||||
}
|
||||
}
|
||||
|
||||
public void add() {
|
||||
FileChooser chooser = new FileChooser();
|
||||
chooser.setTitle(i18n("mods.add.title"));
|
||||
|
||||
@@ -20,32 +20,48 @@ package org.jackhuang.hmcl.ui.instances;
|
||||
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.ObjectProperty;
|
||||
import javafx.beans.property.SimpleBooleanProperty;
|
||||
import javafx.beans.property.SimpleObjectProperty;
|
||||
import javafx.beans.value.ObservableValue;
|
||||
import javafx.geometry.Insets;
|
||||
import javafx.geometry.Pos;
|
||||
import javafx.scene.Node;
|
||||
import javafx.scene.control.Label;
|
||||
import javafx.scene.control.ListCell;
|
||||
import javafx.scene.control.Skin;
|
||||
import javafx.scene.control.Tooltip;
|
||||
import javafx.scene.control.*;
|
||||
import javafx.scene.image.Image;
|
||||
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.stage.FileChooser;
|
||||
import javafx.scene.text.Text;
|
||||
import org.jackhuang.hmcl.addon.RemoteAddon;
|
||||
import org.jackhuang.hmcl.addon.mod.LocalMod;
|
||||
import org.jackhuang.hmcl.addon.mod.LocalModFile;
|
||||
import org.jackhuang.hmcl.addon.repository.ModrinthRemoteAddonRepository;
|
||||
import org.jackhuang.hmcl.game.GameComponentType;
|
||||
import org.jackhuang.hmcl.game.HMCLGameInstance;
|
||||
import org.jackhuang.hmcl.schematic.LitematicFile;
|
||||
import org.jackhuang.hmcl.schematic.Schematic;
|
||||
import org.jackhuang.hmcl.schematic.SchematicType;
|
||||
import org.jackhuang.hmcl.setting.DownloadProviders;
|
||||
import org.jackhuang.hmcl.task.Schedulers;
|
||||
import org.jackhuang.hmcl.task.Task;
|
||||
import org.jackhuang.hmcl.ui.*;
|
||||
import org.jackhuang.hmcl.ui.construct.*;
|
||||
import org.jackhuang.hmcl.util.FileNameSet;
|
||||
import org.jackhuang.hmcl.util.Lang;
|
||||
import org.jackhuang.hmcl.ui.nbt.NBTEditorPage;
|
||||
import org.jackhuang.hmcl.util.StringUtils;
|
||||
import org.jackhuang.hmcl.util.SynchronizedExceptionalLazy;
|
||||
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;
|
||||
|
||||
@@ -55,10 +71,12 @@ import java.nio.file.Files;
|
||||
import java.nio.file.NoSuchFileException;
|
||||
import java.nio.file.Path;
|
||||
import java.util.*;
|
||||
import java.util.Objects;
|
||||
import java.util.concurrent.locks.ReentrantLock;
|
||||
import java.util.stream.Collectors;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
import static org.jackhuang.hmcl.ui.FXUtils.onEscPressed;
|
||||
import static org.jackhuang.hmcl.ui.FXUtils.*;
|
||||
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;
|
||||
|
||||
@@ -67,6 +85,23 @@ import static org.jackhuang.hmcl.util.logging.Logger.LOG;
|
||||
*/
|
||||
public final class SchematicsPage extends ListPageBase<SchematicsPage.Item> {
|
||||
|
||||
private static final SynchronizedExceptionalLazy<RemoteAddon> litematicaLazy = new SynchronizedExceptionalLazy<>(() -> {
|
||||
try {
|
||||
return ModrinthRemoteAddonRepository.MODS.getAddonById(DownloadProviders.getDownloadProvider(), "litematica");
|
||||
} catch (IOException e) {
|
||||
LOG.warning("Failed to fetch litematica", e);
|
||||
throw e;
|
||||
}
|
||||
});
|
||||
private static final SynchronizedExceptionalLazy<RemoteAddon> forgematicaLazy = new SynchronizedExceptionalLazy<>(() -> {
|
||||
try {
|
||||
return ModrinthRemoteAddonRepository.MODS.getAddonById(DownloadProviders.getDownloadProvider(), "forgematica");
|
||||
} catch (IOException e) {
|
||||
LOG.warning("Failed to fetch forgematica", e);
|
||||
throw e;
|
||||
}
|
||||
});
|
||||
|
||||
private static String translateAuthorName(String author) {
|
||||
if (I18n.isUseChinese() && "hsds".equals(author)) {
|
||||
return "黑山大叔";
|
||||
@@ -74,9 +109,22 @@ public final class SchematicsPage extends ListPageBase<SchematicsPage.Item> {
|
||||
return author;
|
||||
}
|
||||
|
||||
private static String translateType(SchematicType type) {
|
||||
return switch (type) {
|
||||
case SCHEM -> i18n("schematics.info.type.schem");
|
||||
case NBT_STRUCTURE -> i18n("schematics.info.type.nbt_structure");
|
||||
case LITEMATIC -> i18n("schematics.info.type.litematic");
|
||||
};
|
||||
}
|
||||
|
||||
private final WeakListenerHolder listenerHolder = new WeakListenerHolder();
|
||||
|
||||
private @Nullable HMCLGameInstance gameInstance;
|
||||
private Path schematicsDirectory;
|
||||
private DirItem currentDirectory;
|
||||
|
||||
private final ObjectProperty<DirItem> currentDirectory = new SimpleObjectProperty<>(this, "currentDirectory", null);
|
||||
private final BooleanProperty isRootProperty = new SimpleBooleanProperty(this, "isRoot", true);
|
||||
private final ObjectProperty<RemoteAddon> downloadTarget = new SimpleObjectProperty<>(this, "downloadTarget");
|
||||
|
||||
/// Creates a schematics list that reloads when `instanceContext` changes.
|
||||
///
|
||||
@@ -84,7 +132,7 @@ public final class SchematicsPage extends ListPageBase<SchematicsPage.Item> {
|
||||
public SchematicsPage(ObservableValue<? extends HMCLGameInstance.Optional> instanceContext) {
|
||||
Objects.requireNonNull(instanceContext, "instanceContext");
|
||||
FXUtils.applyDragListener(this,
|
||||
file -> currentDirectory != null && Files.isRegularFile(file) && FileUtils.getName(file).endsWith(".litematic"),
|
||||
file -> currentDirectoryProperty().get() != null && Schematic.isFileSchematic(file),
|
||||
this::addFiles
|
||||
);
|
||||
|
||||
@@ -97,53 +145,120 @@ public final class SchematicsPage extends ListPageBase<SchematicsPage.Item> {
|
||||
|
||||
@Override
|
||||
protected Skin<?> createDefaultSkin() {
|
||||
return new SchematicsPageSkin();
|
||||
return new SchematicsPageSkin(this);
|
||||
}
|
||||
|
||||
public void loadInstance(HMCLGameInstance.Optional instance) {
|
||||
HMCLGameInstance gameInstance = instance.instance();
|
||||
this.schematicsDirectory = gameInstance != null ? gameInstance.getSchematicsDirectory() : null;
|
||||
this.gameInstance = instance.instance();
|
||||
|
||||
if (gameInstance == null) {
|
||||
this.schematicsDirectory = null;
|
||||
return;
|
||||
}
|
||||
|
||||
this.schematicsDirectory = gameInstance.getSchematicsDirectory();
|
||||
|
||||
refresh();
|
||||
}
|
||||
|
||||
private ObjectProperty<DirItem> currentDirectoryProperty() {
|
||||
return currentDirectory;
|
||||
}
|
||||
|
||||
public BooleanProperty isRootProperty() {
|
||||
return isRootProperty;
|
||||
}
|
||||
|
||||
public void navigateBack() {
|
||||
var d = currentDirectoryProperty().get();
|
||||
if (d == null) return;
|
||||
if (d.parent != null) navigateTo(d.parent);
|
||||
}
|
||||
|
||||
public void downloadLitematica() {
|
||||
if (downloadTarget.get() != null && gameInstance != null) {
|
||||
var modDownloads = HMCLLocalizedDownloadListPage.ofModrinthMod(org.jackhuang.hmcl.ui.download.DownloadPage.FOR_MOD, false);
|
||||
modDownloads.loadInstance(HMCLGameInstance.Optional.of(gameInstance));
|
||||
Controllers.navigate(new DownloadPage(
|
||||
modDownloads,
|
||||
downloadTarget.get(),
|
||||
modDownloads.getInstanceOptional(),
|
||||
modDownloads.getCallback())
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
public void refresh() {
|
||||
Path schematicsDirectory = this.schematicsDirectory;
|
||||
if (schematicsDirectory == null) return;
|
||||
HMCLGameInstance currentGameInstance = this.gameInstance;
|
||||
if (currentGameInstance == null) return;
|
||||
|
||||
setLoading(true);
|
||||
Task.supplyAsync(() -> loadAll(schematicsDirectory, null))
|
||||
.whenComplete(Schedulers.javafx(), (result, exception) -> {
|
||||
setLoading(false);
|
||||
if (exception == null) {
|
||||
DirItem target = result;
|
||||
if (currentDirectory != null) {
|
||||
loop:
|
||||
for (int i = 0; i < currentDirectory.relativePath.size(); i++) {
|
||||
String dirName = currentDirectory.relativePath.get(i);
|
||||
|
||||
for (Item child : target.children) {
|
||||
if (child instanceof DirItem && child.getName().equals(dirName)) {
|
||||
target = (DirItem) child;
|
||||
continue loop;
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
navigateTo(target);
|
||||
} else {
|
||||
LOG.warning("Failed to load schematics", exception);
|
||||
DirItem currentDir = currentDirectoryProperty().get();
|
||||
Task.supplyAsync(Schedulers.io(), () -> {
|
||||
DirItem target = loadRoot(schematicsDirectory);
|
||||
if (currentDir != null) {
|
||||
loop:
|
||||
for (String dirName : currentDir.relativePath) {
|
||||
try {
|
||||
target.preLoad();
|
||||
} catch (IOException e) {
|
||||
LOG.warning("Failed to preload sub-directories in " + target, e);
|
||||
break;
|
||||
}
|
||||
}).start();
|
||||
for (var dirChild : target.dirChildren) {
|
||||
if (dirChild.getName().equals(dirName)) {
|
||||
target = dirChild;
|
||||
continue loop;
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
return target;
|
||||
}).whenComplete(Schedulers.javafx(), (result, exception) -> {
|
||||
if (!Objects.equals(currentGameInstance, this.gameInstance)) return;
|
||||
if (exception == null) {
|
||||
navigateTo(result);
|
||||
} else {
|
||||
LOG.warning("Failed to load schematics", exception);
|
||||
}
|
||||
setLoading(false);
|
||||
}).start();
|
||||
|
||||
downloadTarget.set(null);
|
||||
Task.supplyAsync(Schedulers.io(), () -> {
|
||||
var modManager = currentGameInstance.getModManager();
|
||||
modManager.analyze();
|
||||
var analyzer = modManager.getComponentAnalyzer();
|
||||
if (analyzer == null) return null;
|
||||
var modIds = modManager.getLocalFiles().stream()
|
||||
.map(LocalModFile::getMod)
|
||||
.map(LocalMod::getId)
|
||||
.collect(Collectors.toSet());
|
||||
if (modIds.contains("litematica") || modIds.contains("forgematica")) return null;
|
||||
var gameVersionNumber = GameVersionNumber.asGameVersion(modManager.getGameVersion());
|
||||
if (!gameVersionNumber.isAtLeast("1.12", "17w31a"))
|
||||
return null;
|
||||
if ((analyzer.has(GameComponentType.FORGE) || analyzer.has(GameComponentType.NEO_FORGE))
|
||||
&& gameVersionNumber.isAtLeast("1.16.4", "20w45a"))
|
||||
return forgematicaLazy.get();
|
||||
return litematicaLazy.get();
|
||||
}).whenComplete(Schedulers.javafx(), (result, exception) -> {
|
||||
if (!Objects.equals(currentGameInstance, gameInstance)) return;
|
||||
if (exception == null) {
|
||||
downloadTarget.set(result);
|
||||
} else {
|
||||
LOG.warning("Failed to fetch litematica", exception);
|
||||
}
|
||||
}).start();
|
||||
}
|
||||
|
||||
public void addFiles(List<Path> files) {
|
||||
if (currentDirectory == null)
|
||||
if (currentDirectoryProperty().get() == null)
|
||||
return;
|
||||
|
||||
Path dir = currentDirectory.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);
|
||||
@@ -162,7 +277,7 @@ public final class SchematicsPage extends ListPageBase<SchematicsPage.Item> {
|
||||
FileChooser fileChooser = new FileChooser();
|
||||
fileChooser.setTitle(i18n("schematics.add.title"));
|
||||
fileChooser.getExtensionFilters().add(new FileChooser.ExtensionFilter(
|
||||
i18n("extension.schematic"), "*.litematic"));
|
||||
i18n("extension.schematic"), "*.litematic", "*.nbt", "*.schematic", "*.schem"));
|
||||
List<Path> files = Controllers.showOpenMultipleDialog(fileChooser);
|
||||
if (files != null && !files.isEmpty()) {
|
||||
addFiles(files);
|
||||
@@ -170,10 +285,9 @@ public final class SchematicsPage extends ListPageBase<SchematicsPage.Item> {
|
||||
}
|
||||
|
||||
public void onCreateDirectory() {
|
||||
if (currentDirectory == null)
|
||||
return;
|
||||
if (currentDirectoryProperty().get() == null) return;
|
||||
|
||||
Path parent = currentDirectory.path;
|
||||
Path parent = currentDirectoryProperty().get().getPath();
|
||||
|
||||
FileNameSet existingPaths;
|
||||
try {
|
||||
@@ -208,37 +322,29 @@ public final class SchematicsPage extends ListPageBase<SchematicsPage.Item> {
|
||||
new Validator(i18n("schematics.create_directory.failed.already_exists"), existingPaths::notContains));
|
||||
}
|
||||
|
||||
private DirItem loadAll(Path dir, @Nullable DirItem parent) {
|
||||
DirItem item = new DirItem(dir, parent);
|
||||
public void onRevealSchematicsFolder() {
|
||||
var d = currentDirectoryProperty().get();
|
||||
var p = d != null ? d.getPath() : schematicsDirectory;
|
||||
if (p != null) FXUtils.openFolder(p);
|
||||
}
|
||||
|
||||
try (Stream<Path> 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;
|
||||
getItems().clear();
|
||||
if (item.parent != null) {
|
||||
getItems().add(new BackItem(item.parent));
|
||||
}
|
||||
getItems().addAll(item.children);
|
||||
if (currentDirectoryProperty().get() == item) return;
|
||||
currentDirectoryProperty().set(item);
|
||||
isRootProperty().set(item.parent == null);
|
||||
setLoading(true);
|
||||
Task.runAsync(item::load).whenComplete(Schedulers.javafx(), exception -> {
|
||||
if (currentDirectoryProperty().get() == item) {
|
||||
getItems().setAll(item.children);
|
||||
setLoading(false);
|
||||
}
|
||||
}).start();
|
||||
}
|
||||
|
||||
abstract sealed class Item implements Comparable<Item> {
|
||||
@@ -280,12 +386,28 @@ public final class SchematicsPage extends ListPageBase<SchematicsPage.Item> {
|
||||
}
|
||||
}
|
||||
|
||||
private final class BackItem extends Item {
|
||||
private final class DirItem extends Item {
|
||||
final ReentrantLock lock = new ReentrantLock();
|
||||
|
||||
private final DirItem parent;
|
||||
final Path path;
|
||||
final @Nullable DirItem parent;
|
||||
final List<Item> children = new ArrayList<>();
|
||||
final List<DirItem> dirChildren = new ArrayList<>();
|
||||
final List<String> relativePath;
|
||||
int size = 0;
|
||||
boolean preLoaded = false;
|
||||
boolean loaded = false;
|
||||
|
||||
BackItem(DirItem parent) {
|
||||
DirItem(Path path, @Nullable DirItem parent) {
|
||||
this.path = path;
|
||||
this.parent = parent;
|
||||
|
||||
if (parent != null) {
|
||||
this.relativePath = new ArrayList<>(parent.relativePath);
|
||||
relativePath.add(FileUtils.getName(path));
|
||||
} else {
|
||||
this.relativePath = Collections.emptyList();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -293,65 +415,6 @@ public final class SchematicsPage extends ListPageBase<SchematicsPage.Item> {
|
||||
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;
|
||||
final List<Item> children = new ArrayList<>();
|
||||
final List<String> relativePath;
|
||||
|
||||
DirItem(Path path, @Nullable DirItem parent) {
|
||||
this.path = path;
|
||||
this.parent = parent;
|
||||
|
||||
if (parent != null) {
|
||||
this.relativePath = new ArrayList<>(parent.relativePath);
|
||||
relativePath.add(path.getFileName().toString());
|
||||
} else {
|
||||
this.relativePath = Collections.emptyList();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
int order() {
|
||||
return 1;
|
||||
}
|
||||
|
||||
@Override
|
||||
Path getPath() {
|
||||
return path;
|
||||
@@ -359,12 +422,12 @@ public final class SchematicsPage extends ListPageBase<SchematicsPage.Item> {
|
||||
|
||||
@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
|
||||
@@ -372,6 +435,66 @@ public final class SchematicsPage extends ListPageBase<SchematicsPage.Item> {
|
||||
return SVG.FOLDER;
|
||||
}
|
||||
|
||||
void preLoad() throws IOException {
|
||||
lock.lock();
|
||||
try {
|
||||
if (this.preLoaded) return;
|
||||
this.size = 0;
|
||||
this.dirChildren.clear();
|
||||
try (Stream<Path> stream = Files.list(path)) {
|
||||
stream.forEach(p -> {
|
||||
boolean b1 = Files.isDirectory(p);
|
||||
boolean b2 = Schematic.isFileSchematic(p);
|
||||
if (b1 || b2) this.size++;
|
||||
if (b1) {
|
||||
var child = new DirItem(p, this);
|
||||
this.dirChildren.add(child);
|
||||
}
|
||||
});
|
||||
}
|
||||
this.preLoaded = true;
|
||||
} finally {
|
||||
lock.unlock();
|
||||
}
|
||||
}
|
||||
|
||||
void load() {
|
||||
lock.lock();
|
||||
try {
|
||||
if (this.loaded) return;
|
||||
|
||||
this.children.clear();
|
||||
|
||||
preLoad();
|
||||
for (var dir : dirChildren) {
|
||||
try {
|
||||
dir.preLoad();
|
||||
} catch (IOException e) {
|
||||
LOG.warning("Failed to pre-load schematics in " + dir.path, e);
|
||||
}
|
||||
this.children.add(dir);
|
||||
}
|
||||
try (Stream<Path> stream = Files.list(path)) {
|
||||
stream.filter(Schematic::isFileSchematic)
|
||||
.forEach(p -> {
|
||||
try {
|
||||
this.children.add(new SchematicItem(p));
|
||||
} catch (IOException e) {
|
||||
LOG.warning("Failed to load schematic file: " + p, e);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
this.children.sort(Comparator.naturalOrder());
|
||||
this.loaded = true;
|
||||
} catch (NoSuchFileException ignored) {
|
||||
} catch (IOException e) {
|
||||
LOG.warning("Failed to load schematics in " + path, e);
|
||||
} finally {
|
||||
lock.unlock();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
void onClick() {
|
||||
navigateTo(this);
|
||||
@@ -385,8 +508,7 @@ public final class SchematicsPage extends ListPageBase<SchematicsPage.Item> {
|
||||
@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);
|
||||
@@ -394,19 +516,27 @@ public final class SchematicsPage extends ListPageBase<SchematicsPage.Item> {
|
||||
}
|
||||
}
|
||||
|
||||
private final class LitematicFileItem extends Item {
|
||||
final LitematicFile file;
|
||||
private final class SchematicItem extends Item {
|
||||
final Path path;
|
||||
final Schematic file;
|
||||
final String name;
|
||||
final Image image;
|
||||
|
||||
private LitematicFileItem(LitematicFile file) {
|
||||
this.file = file;
|
||||
private SchematicItem(Path path) throws IOException {
|
||||
this.path = path;
|
||||
this.file = Schematic.load(path);
|
||||
|
||||
String name = file.getName();
|
||||
if (name != null && !"Unnamed".equals(name)) {
|
||||
this.name = name;
|
||||
if (file == null) throw new IOException("Unsupported or deleted file: " + 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 = StringUtils.removeSuffix(file.getFile().getFileName().toString(), ".litematic");
|
||||
this.name = FileUtils.getNameWithoutExtension(path);
|
||||
}
|
||||
|
||||
WritableImage image = null;
|
||||
@@ -430,12 +560,12 @@ public final class SchematicsPage extends ListPageBase<SchematicsPage.Item> {
|
||||
|
||||
@Override
|
||||
int order() {
|
||||
return 2;
|
||||
return 1;
|
||||
}
|
||||
|
||||
@Override
|
||||
Path getPath() {
|
||||
return file.getFile();
|
||||
return path;
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -445,7 +575,7 @@ public final class SchematicsPage extends ListPageBase<SchematicsPage.Item> {
|
||||
|
||||
@Override
|
||||
String getDescription() {
|
||||
return file.getFile().getFileName().toString();
|
||||
return path.getFileName().toString();
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -469,25 +599,25 @@ public final class SchematicsPage extends ListPageBase<SchematicsPage.Item> {
|
||||
|
||||
@Override
|
||||
void onClick() {
|
||||
Controllers.dialog(new LitematicInfoDialog());
|
||||
Controllers.dialog(new SchematicInfoDialog());
|
||||
}
|
||||
|
||||
@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);
|
||||
}
|
||||
}
|
||||
|
||||
private final class LitematicInfoDialog extends JFXDialogLayout {
|
||||
private final class SchematicInfoDialog extends JFXDialogLayout {
|
||||
private final ComponentList details;
|
||||
|
||||
private void addDetailItem(String key, Object detail) {
|
||||
@@ -497,38 +627,45 @@ public final class SchematicsPage extends ListPageBase<SchematicsPage.Item> {
|
||||
details.getContent().add(borderPane);
|
||||
}
|
||||
|
||||
private void updateContent(LitematicFile file) {
|
||||
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)
|
||||
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().getAsLong());
|
||||
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", (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_data_version"), file.getMinecraftVersion());
|
||||
if (file.getVersion().isPresent())
|
||||
addDetailItem(i18n("schematics.info.version"),
|
||||
file.getSubVersion().isPresent()
|
||||
? "%d.%d".formatted(file.getVersion().getAsInt(), file.getSubVersion().getAsInt())
|
||||
: file.getVersion().getAsInt()
|
||||
);
|
||||
}
|
||||
|
||||
LitematicInfoDialog() {
|
||||
SchematicInfoDialog() {
|
||||
HBox titleBox = new HBox(8);
|
||||
{
|
||||
Node icon = getIcon(40);
|
||||
|
||||
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);
|
||||
@@ -536,10 +673,15 @@ public final class SchematicsPage extends ListPageBase<SchematicsPage.Item> {
|
||||
|
||||
{
|
||||
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.setFitToWidth(true);
|
||||
FXUtils.setOverflowHidden(scrollPane, 8);
|
||||
FXUtils.smoothScrolling(scrollPane);
|
||||
StackPane.setMargin(scrollPane, new Insets(10, 0, 0, 0));
|
||||
setBody(scrollPane);
|
||||
}
|
||||
|
||||
{
|
||||
@@ -552,15 +694,16 @@ public final class SchematicsPage extends ListPageBase<SchematicsPage.Item> {
|
||||
onEscPressed(this, okButton::fire);
|
||||
}
|
||||
|
||||
this.prefWidthProperty().bind(Controllers.getDecorator().contentWidthProperty().multiply(0.6));
|
||||
this.maxHeightProperty().bind(Controllers.getDecorator().contentHeightProperty().multiply(0.8));
|
||||
|
||||
updateContent(file);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static final class Cell extends ListCell<Item> {
|
||||
private static final class Cell extends MDListCell<Item> {
|
||||
|
||||
private final RipplerContainer graphics;
|
||||
private final BorderPane root;
|
||||
private final StackPane left;
|
||||
private final TwoLineListItem center;
|
||||
private final HBox right;
|
||||
@@ -568,12 +711,17 @@ public final class SchematicsPage extends ListPageBase<SchematicsPage.Item> {
|
||||
private final ImageContainer iconImageView;
|
||||
private final SVGContainer iconSVGView;
|
||||
|
||||
private final BooleanProperty isFileProperty = new SimpleBooleanProperty(this, "isFile", false);
|
||||
private final BooleanProperty isDirectoryProperty = new SimpleBooleanProperty(this, "isDirectory", false);
|
||||
|
||||
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<Item> listView) {
|
||||
super(listView);
|
||||
|
||||
var box = new HBox(8);
|
||||
box.setAlignment(Pos.CENTER_LEFT);
|
||||
box.setPickOnBounds(false);
|
||||
|
||||
{
|
||||
this.left = new StackPane();
|
||||
@@ -581,115 +729,186 @@ public final class SchematicsPage extends ListPageBase<SchematicsPage.Item> {
|
||||
|
||||
this.iconImageView = new ImageContainer(32);
|
||||
this.iconSVGView = new SVGContainer(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);
|
||||
|
||||
JFXButton btnReveal = FXUtils.newToggleButton4(SVG.FOLDER_OPEN);
|
||||
FXUtils.installFastTooltip(btnReveal, i18n("reveal.in_file_manager"));
|
||||
{
|
||||
var fo = SVG.FOLDER_OPEN.createIcon();
|
||||
var f = SVG.FOLDER.createIcon();
|
||||
btnReveal.graphicProperty().bind(isDirectoryProperty.map(b -> b ? fo : f));
|
||||
|
||||
var tooltip = new Tooltip();
|
||||
tooltip.textProperty().bind(isDirectoryProperty.map(b -> b ? i18n("button.reveal_dir") : i18n("reveal.in_file_manager")));
|
||||
FXUtils.installFastTooltip(btnReveal, tooltip);
|
||||
}
|
||||
btnReveal.setOnAction(event -> {
|
||||
Item item = getItem();
|
||||
if (item != null && !(item instanceof BackItem))
|
||||
item.onReveal();
|
||||
if (item != null) item.onReveal();
|
||||
});
|
||||
|
||||
JFXButton btnExplore = FXUtils.newToggleButton4(SVG.EXPLORE); // Change the icon if allows editing
|
||||
btnExplore.setOnAction(event -> {
|
||||
Item item = getItem();
|
||||
if (item instanceof SchematicItem) {
|
||||
try {
|
||||
Controllers.navigate(new NBTEditorPage(item.getPath()));
|
||||
} catch (IOException ignored) { // Should be impossible
|
||||
}
|
||||
}
|
||||
});
|
||||
btnExplore.visibleProperty().bind(isFileProperty);
|
||||
|
||||
JFXButton btnDelete = FXUtils.newToggleButton4(SVG.DELETE_FOREVER);
|
||||
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);
|
||||
}
|
||||
});
|
||||
|
||||
right.getChildren().setAll(btnReveal, btnDelete);
|
||||
right.getChildren().setAll(btnExplore, 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));
|
||||
getContainer().getChildren().setAll(box);
|
||||
onClicked(() -> {
|
||||
var item = getItem();
|
||||
if (item != null) item.onClick();
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void updateItem(Item item, boolean empty) {
|
||||
Item oldItem = getItem();
|
||||
boolean oldEmpty = isEmpty();
|
||||
protected void updateControl(Item item, boolean empty) {
|
||||
if (empty || item == null) return;
|
||||
|
||||
super.updateItem(item, empty);
|
||||
|
||||
if (oldItem == item && oldEmpty == empty) return;
|
||||
|
||||
graphics.releaseRippleImmediately();
|
||||
iconImageView.setImage(null);
|
||||
|
||||
if (empty || item == null) {
|
||||
setGraphic(null);
|
||||
center.setTitle("");
|
||||
center.setSubtitle("");
|
||||
isFileProperty.set(item instanceof SchematicItem);
|
||||
isDirectoryProperty.set(item.isDirectory());
|
||||
|
||||
if (item instanceof SchematicItem fileItem && fileItem.getImage() != null) {
|
||||
iconImageView.setImage(fileItem.getImage());
|
||||
left.getChildren().setAll(iconImageView);
|
||||
} else {
|
||||
if (item instanceof LitematicFileItem fileItem && fileItem.getImage() != null) {
|
||||
iconImageView.setImage(fileItem.getImage());
|
||||
left.getChildren().setAll(iconImageView);
|
||||
} else {
|
||||
iconSVGView.setIcon(item.getIcon());
|
||||
left.getChildren().setAll(iconSVGView);
|
||||
}
|
||||
iconSVGView.setIcon(item.getIcon());
|
||||
left.getChildren().setAll(iconSVGView);
|
||||
}
|
||||
|
||||
center.setTitle(item.getName());
|
||||
center.setSubtitle(item.getDescription());
|
||||
center.setTitle(item.getName());
|
||||
center.setSubtitle(item.getDescription());
|
||||
|
||||
Path path = item.getPath();
|
||||
if (path != null) {
|
||||
tooltip.setText(FileUtils.getAbsolutePath(path));
|
||||
FXUtils.installSlowTooltip(left, tooltip);
|
||||
} else {
|
||||
tooltip.setText("");
|
||||
Tooltip.uninstall(left, tooltip);
|
||||
}
|
||||
|
||||
root.setRight(item instanceof BackItem ? null : right);
|
||||
|
||||
setGraphic(graphics);
|
||||
Path path = item.getPath();
|
||||
if (path != null) {
|
||||
tooltip.setText(FileUtils.getAbsolutePath(path));
|
||||
FXUtils.installSlowTooltip(left, tooltip);
|
||||
} else {
|
||||
Tooltip.uninstall(left, tooltip);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private final class SchematicsPageSkin extends ToolbarListPageSkin<Item, SchematicsPage> {
|
||||
SchematicsPageSkin() {
|
||||
super(SchematicsPage.this);
|
||||
private static final class SchematicsPageSkin extends SkinBase<SchematicsPage> {
|
||||
|
||||
StackPane placeholderContainer = new StackPane();
|
||||
placeholderContainer.getStyleClass().add("notice-pane");
|
||||
Label placeholderLabel = new Label(i18n("schematics.empty"));
|
||||
placeholderContainer.getChildren().add(placeholderLabel);
|
||||
listView.setPlaceholder(placeholderContainer);
|
||||
}
|
||||
private final JFXListView<Item> listView;
|
||||
|
||||
@Override
|
||||
protected List<Node> 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)
|
||||
);
|
||||
}
|
||||
SchematicsPageSkin(SchematicsPage skinnable) {
|
||||
super(skinnable);
|
||||
|
||||
@Override
|
||||
protected ListCell<Item> createListCell(JFXListView<Item> listView) {
|
||||
return new Cell();
|
||||
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<>());
|
||||
|
||||
{
|
||||
StackPane placeholderContainer = new StackPane();
|
||||
placeholderContainer.getStyleClass().add("notice-pane");
|
||||
Label placeholderLabel = new Label(i18n("schematics.empty"));
|
||||
placeholderContainer.getChildren().add(placeholderLabel);
|
||||
listView.setPlaceholder(placeholderContainer);
|
||||
}
|
||||
|
||||
{
|
||||
var toolbar = new HBox();
|
||||
JFXButton btnGoBack = createToolbarButton2("", SVG.ARROW_BACK, skinnable::navigateBack);
|
||||
btnGoBack.disableProperty().bind(skinnable.isRootProperty());
|
||||
JFXButton btnDownload = createToolbarButton2(i18n("schematics.install_mod"), SVG.DOWNLOAD, skinnable::downloadLitematica);
|
||||
FXUtils.onChangeAndOperate(skinnable.downloadTarget, (t) -> btnDownload.setDisable(t == null));
|
||||
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),
|
||||
createToolbarButton2(i18n("button.reveal_dir"), SVG.FOLDER_OPEN, skinnable::onRevealSchematicsFolder),
|
||||
btnDownload
|
||||
);
|
||||
FXUtils.setOverflowHidden(toolbar, 8);
|
||||
root.getContent().add(toolbar);
|
||||
}
|
||||
|
||||
{
|
||||
SpinnerPane center = new SpinnerPane();
|
||||
ComponentList.setVgrow(center, Priority.ALWAYS);
|
||||
center.loadingProperty().bind(skinnable.loadingProperty());
|
||||
|
||||
listView.setCellFactory(x -> new Cell(listView));
|
||||
listView.setSelectionModel(new NoneMultipleSelectionModel<>());
|
||||
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);
|
||||
listView.getStyleClass().add("no-horizontal-scrollbar");
|
||||
|
||||
|
||||
center.setContent(listView);
|
||||
root.getContent().add(center);
|
||||
}
|
||||
|
||||
{
|
||||
var relPath = new HBox();
|
||||
relPath.getStyleClass().add("jfx-tool-bar-tip");
|
||||
relPath.setAlignment(Pos.CENTER_LEFT);
|
||||
FXUtils.onChangeAndOperate(skinnable.currentDirectoryProperty(), currentDir -> {
|
||||
relPath.getChildren().clear();
|
||||
var d = currentDir;
|
||||
while (d != null) {
|
||||
if (d != currentDir) {
|
||||
var box = new HBox(new Text(">"));
|
||||
box.setPadding(new Insets(3));
|
||||
relPath.getChildren().add(0, box);
|
||||
}
|
||||
var txt = new Text(d.getName());
|
||||
var box = new HBox(txt);
|
||||
box.setPadding(new Insets(3));
|
||||
box.setMouseTransparent(true);
|
||||
var rippler = new RipplerContainer(box);
|
||||
var finalD = d;
|
||||
FXUtils.onClicked(rippler, () -> skinnable.navigateTo(finalD));
|
||||
relPath.getChildren().add(0, rippler);
|
||||
d = d.parent;
|
||||
}
|
||||
});
|
||||
var relPathPane = new HBox(relPath);
|
||||
relPathPane.setPadding(new Insets(2));
|
||||
root.getContent().add(relPathPane);
|
||||
}
|
||||
|
||||
pane.getChildren().setAll(root);
|
||||
getChildren().setAll(pane);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -29,7 +29,7 @@ import java.nio.file.Path;
|
||||
|
||||
/// @author Glavo
|
||||
public enum NBTFileType {
|
||||
COMPRESSED("nbt", "dat", "dat_old") {
|
||||
COMPRESSED("dat", "dat_old", "litematic", "nbt", "schematic", "schem") {
|
||||
@Override
|
||||
public Tag read(Path file) throws IOException {
|
||||
return NBTCodec.of().readTag(file);
|
||||
|
||||
@@ -0,0 +1,71 @@
|
||||
/*
|
||||
* Hello Minecraft! Launcher
|
||||
* Copyright (C) 2026 huangyuhui <huanghongxun2008@126.com> 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 <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.jackhuang.hmcl.util;
|
||||
|
||||
import org.jackhuang.hmcl.util.function.ExceptionalSupplier;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import java.util.Objects;
|
||||
import java.util.concurrent.locks.ReentrantLock;
|
||||
|
||||
/**
|
||||
* Thread-safe exceptional lazy initialization wrapper.
|
||||
* <p>
|
||||
* On failure, the result is set to null in order to allow retrying.
|
||||
*
|
||||
* @param <T> value type
|
||||
*/
|
||||
public final class SynchronizedExceptionalLazy<T> {
|
||||
private final ExceptionalSupplier<@Nullable T, Exception> supplier;
|
||||
private volatile Result<T> result = null;
|
||||
|
||||
private final ReentrantLock lock = new ReentrantLock();
|
||||
|
||||
public SynchronizedExceptionalLazy(ExceptionalSupplier<@Nullable T, Exception> supplier) {
|
||||
this.supplier = Objects.requireNonNull(supplier);
|
||||
}
|
||||
|
||||
/// Gets and returns the result.
|
||||
///
|
||||
/// If the previous attempt succeeded, the cached value is returned directly.
|
||||
///
|
||||
/// Otherwise, the supplier is invoked to compute a new value.If this attempt fails,
|
||||
/// {@code null} is returned; if succeeded, the computed value is cached and returned.
|
||||
///
|
||||
/// @return the result, or null on failure
|
||||
public T get() {
|
||||
if (result != null) return result.value();
|
||||
lock.lock();
|
||||
try {
|
||||
if (result == null) {
|
||||
try {
|
||||
result = new Result<>(supplier.get());
|
||||
} catch (Exception e) {
|
||||
result = null;
|
||||
return null;
|
||||
}
|
||||
}
|
||||
return result.value();
|
||||
} finally {
|
||||
lock.unlock();
|
||||
}
|
||||
}
|
||||
|
||||
private record Result<T>(T value) {
|
||||
}
|
||||
}
|
||||
@@ -713,6 +713,10 @@
|
||||
-fx-text-fill: BLACK;
|
||||
}
|
||||
|
||||
.jfx-tool-bar Text {
|
||||
|
||||
}
|
||||
|
||||
.jfx-tool-bar .jfx-options-burger {
|
||||
-fx-padding: 22px;
|
||||
}
|
||||
@@ -761,6 +765,11 @@
|
||||
-fx-padding: 10.0;
|
||||
}
|
||||
|
||||
.jfx-tool-bar-tip Text {
|
||||
-fx-font-size: 13;
|
||||
-fx-fill: -monet-on-surface;
|
||||
}
|
||||
|
||||
.jfx-decorator-button {
|
||||
-fx-max-width: 40px;
|
||||
-fx-background-radius: 5px;
|
||||
|
||||
@@ -1333,7 +1333,6 @@ schematics=Schematics
|
||||
schematics.add=Add
|
||||
schematics.add.failed=Failed to add schematic files
|
||||
schematics.add.title=Choose schematic file you want to import
|
||||
schematics.back_to=Back to "%s"
|
||||
schematics.create_directory=New Directory
|
||||
schematics.create_directory.prompt=Please enter the new directory name
|
||||
schematics.create_directory.failed=Failed to create directory
|
||||
@@ -1349,7 +1348,13 @@ 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, Create)
|
||||
schematics.info.type.schem=Schem (Schematica, WE, MCEdit)
|
||||
schematics.info.version=Schematic Version
|
||||
schematics.info.mc_data_version=Minecraft Data Version
|
||||
schematics.install_mod=Install Litematica Mod
|
||||
schematics.manage=Schematics
|
||||
schematics.sub_items=%d sub-item(s)
|
||||
|
||||
|
||||
@@ -1333,7 +1333,6 @@ schematics=المخططات
|
||||
schematics.add=إضافة
|
||||
schematics.add.failed=فشل إضافة ملفات المخططات
|
||||
schematics.add.title=اختر ملف المخطط الذي تريد استيراده
|
||||
schematics.back_to=العودة إلى "%s"
|
||||
schematics.create_directory=مجلد جديد
|
||||
schematics.create_directory.prompt=يُرجى إدخال اسم المجلد الجديد
|
||||
schematics.create_directory.failed=فشل إنشاء المجلد
|
||||
@@ -1349,7 +1348,13 @@ schematics.info.time_created=وقت الإنشاء
|
||||
schematics.info.time_modified=وقت التعديل
|
||||
schematics.info.total_blocks=إجمالي البلوكات
|
||||
schematics.info.total_volume=الحجم الكلي
|
||||
# schematics.info.type=
|
||||
# schematics.info.type.litematic=
|
||||
# schematics.info.type.nbt_structure=
|
||||
# schematics.info.type.schem=
|
||||
schematics.info.version=إصدار المخطط
|
||||
# schematics.info.mc_data_version=
|
||||
# schematics.install_mod=
|
||||
schematics.manage=المخططات
|
||||
schematics.sub_items=%d عنصر فرعي
|
||||
|
||||
|
||||
@@ -1332,7 +1332,6 @@ schematics=Schematiken
|
||||
schematics.add=Hinzufügen
|
||||
schematics.add.failed=Fehler beim Hinzufügen der Schematik-Dateien
|
||||
schematics.add.title=Wählen Sie die Schematik-Datei, die Sie importieren möchten
|
||||
schematics.back_to=Zurück zu "%s"
|
||||
schematics.create_directory=Neues Verzeichnis
|
||||
schematics.create_directory.prompt=Bitte geben Sie den neuen Verzeichnisnamen ein
|
||||
schematics.create_directory.failed=Fehler beim Erstellen des Verzeichnisses
|
||||
@@ -1348,7 +1347,13 @@ schematics.info.time_created=Erstellungszeit
|
||||
schematics.info.time_modified=Änderungszeit
|
||||
schematics.info.total_blocks=Gesamte Blöcke
|
||||
schematics.info.total_volume=Gesamtes Volumen
|
||||
# schematics.info.type=
|
||||
# schematics.info.type.litematic=
|
||||
# schematics.info.type.nbt_structure=
|
||||
# schematics.info.type.schem=
|
||||
schematics.info.version=Schematik-Version
|
||||
# schematics.info.mc_data_version=
|
||||
# schematics.install_mod=
|
||||
schematics.manage=Schematiken
|
||||
schematics.sub_items=%d Unterelement(e)
|
||||
|
||||
|
||||
@@ -1325,7 +1325,6 @@ schematics=Esquemas
|
||||
# schematics.add=
|
||||
schematics.add.failed=No se han podido añadir archivos de esquema
|
||||
# schematics.add.title=
|
||||
schematics.back_to=Atrás a «%s»
|
||||
# schematics.create_directory=
|
||||
schematics.create_directory.prompt=Introduzca el nuevo nombre del directorio
|
||||
schematics.create_directory.failed=No se ha podido crear el directorio
|
||||
@@ -1341,7 +1340,13 @@ schematics.info.time_created=Creado
|
||||
schematics.info.time_modified=Modificado
|
||||
schematics.info.total_blocks=Bloques totales
|
||||
schematics.info.total_volume=Volumen total
|
||||
# schematics.info.type=
|
||||
# schematics.info.type.litematic=
|
||||
# schematics.info.type.nbt_structure=
|
||||
# schematics.info.type.schem=
|
||||
schematics.info.version=Versión de esquema
|
||||
# schematics.info.mc_data_version=
|
||||
# schematics.install_mod=
|
||||
schematics.manage=Esquemas
|
||||
schematics.sub_items=%d elemento(s)
|
||||
|
||||
|
||||
@@ -1129,7 +1129,6 @@ reveal.in_file_manager=ファイルマネージャーで表示する
|
||||
# schematics.add=
|
||||
# schematics.add.failed=
|
||||
# schematics.add.title=
|
||||
# schematics.back_to=
|
||||
# schematics.create_directory=
|
||||
# schematics.create_directory.prompt=
|
||||
# schematics.create_directory.failed=
|
||||
@@ -1145,7 +1144,13 @@ reveal.in_file_manager=ファイルマネージャーで表示する
|
||||
# schematics.info.time_modified=
|
||||
# schematics.info.total_blocks=
|
||||
# schematics.info.total_volume=
|
||||
# schematics.info.type=
|
||||
# schematics.info.type.litematic=
|
||||
# schematics.info.type.nbt_structure=
|
||||
# schematics.info.type.schem=
|
||||
# schematics.info.version=
|
||||
# schematics.info.mc_data_version=
|
||||
# schematics.install_mod=
|
||||
# schematics.manage=
|
||||
# schematics.sub_items=
|
||||
|
||||
|
||||
@@ -1141,7 +1141,6 @@ schematics=蜃圖
|
||||
schematics.add=增蜃圖
|
||||
schematics.add.failed=增蜃圖未成
|
||||
# schematics.add.title=
|
||||
schematics.back_to=退至「%s」
|
||||
schematics.create_directory=增案夾
|
||||
schematics.create_directory.prompt=書新案夾之名
|
||||
schematics.create_directory.failed=增案夾未成
|
||||
@@ -1157,7 +1156,13 @@ schematics.info.time_created=創之時辰
|
||||
schematics.info.time_modified=改之時辰
|
||||
schematics.info.total_blocks=總塊數
|
||||
schematics.info.total_volume=總體積
|
||||
# schematics.info.type=
|
||||
# schematics.info.type.litematic=
|
||||
# schematics.info.type.nbt_structure=
|
||||
# schematics.info.type.schem=
|
||||
schematics.info.version=蜃圖之版
|
||||
# schematics.info.mc_data_version=
|
||||
# schematics.install_mod=
|
||||
schematics.manage=司蜃圖
|
||||
schematics.sub_items=%d 子项
|
||||
|
||||
|
||||
@@ -1265,7 +1265,6 @@ schematics=Схемы
|
||||
schematics.add=Добавить
|
||||
schematics.add.failed=Не удалось добавить файлы схем
|
||||
schematics.add.title=Выберите файл схемы для импорта
|
||||
schematics.back_to=Назад на «%s»
|
||||
schematics.create_directory=Новая папка
|
||||
schematics.create_directory.prompt=Введите новое имя папки
|
||||
schematics.create_directory.failed=Не удалось создать папку
|
||||
@@ -1281,7 +1280,13 @@ schematics.info.time_created=Создан
|
||||
schematics.info.time_modified=Изменен
|
||||
schematics.info.total_blocks=Всего блоков
|
||||
schematics.info.total_volume=Общий объём
|
||||
# schematics.info.type=
|
||||
# schematics.info.type.litematic=
|
||||
# schematics.info.type.nbt_structure=
|
||||
# schematics.info.type.schem=
|
||||
schematics.info.version=Версия схемы
|
||||
# schematics.info.mc_data_version=
|
||||
# schematics.install_mod=
|
||||
schematics.manage=Схемы
|
||||
schematics.sub_items=%d элемент(ов)
|
||||
|
||||
|
||||
@@ -1288,7 +1288,6 @@ schematics=Схематики
|
||||
schematics.add=Додати
|
||||
schematics.add.failed=Не вдалося додати файли схематик
|
||||
schematics.add.title=Виберіть файл схеми, який ви хочете імпортувати
|
||||
schematics.back_to=Назад до "%s"
|
||||
schematics.create_directory=Новий каталог
|
||||
schematics.create_directory.prompt=Введіть нову назву каталогу
|
||||
schematics.create_directory.failed=Не вдалося створити каталог
|
||||
@@ -1304,7 +1303,13 @@ schematics.info.time_created=Час створення
|
||||
schematics.info.time_modified=Час зміни
|
||||
schematics.info.total_blocks=Всього блоків
|
||||
schematics.info.total_volume=Загальний об'єм
|
||||
# schematics.info.type=
|
||||
# schematics.info.type.litematic=
|
||||
# schematics.info.type.nbt_structure=
|
||||
# schematics.info.type.schem=
|
||||
schematics.info.version=Версія схематики
|
||||
# schematics.info.mc_data_version=
|
||||
# schematics.install_mod=
|
||||
schematics.manage=Схематики
|
||||
schematics.sub_items=%d підпунктів
|
||||
|
||||
|
||||
@@ -1145,7 +1145,6 @@ schematics=原理图
|
||||
schematics.add=添加原理图
|
||||
schematics.add.failed=添加原理图失败
|
||||
schematics.add.title=选择要添加的原理图文件
|
||||
schematics.back_to=返回到“%s”
|
||||
schematics.create_directory=创建文件夹
|
||||
schematics.create_directory.prompt=请输入新文件夹名称
|
||||
schematics.create_directory.failed=创建文件夹失败
|
||||
@@ -1161,7 +1160,13 @@ 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_data_version=Minecraft 数据版本
|
||||
schematics.install_mod=安装投影模组
|
||||
schematics.manage=原理图管理
|
||||
schematics.sub_items=%d 个子项
|
||||
|
||||
|
||||
@@ -1139,7 +1139,6 @@ schematics=原理圖
|
||||
schematics.add=新增原理圖
|
||||
schematics.add.failed=新增原理圖失敗
|
||||
schematics.add.title=選取要新增的原理圖檔案
|
||||
schematics.back_to=返回到「%s」
|
||||
schematics.create_directory=建立目錄
|
||||
schematics.create_directory.prompt=請輸入新目錄名稱
|
||||
schematics.create_directory.failed=建立目錄失敗
|
||||
@@ -1155,7 +1154,13 @@ 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_data_version=Minecraft 資料版本
|
||||
schematics.install_mod=安裝投影模組
|
||||
schematics.manage=原理圖管理
|
||||
schematics.sub_items=%d 個子項
|
||||
|
||||
|
||||
@@ -24,6 +24,7 @@ import org.jackhuang.hmcl.addon.meta.*;
|
||||
import org.jackhuang.hmcl.game.DefaultGameInstance;
|
||||
import org.jackhuang.hmcl.game.GameComponentAnalyzer;
|
||||
import org.jackhuang.hmcl.game.GameComponentType;
|
||||
import org.jackhuang.hmcl.game.NoSuchGameInstanceException;
|
||||
import org.jackhuang.hmcl.util.Pair;
|
||||
import org.jackhuang.hmcl.util.StringUtils;
|
||||
import org.jackhuang.hmcl.util.io.CompressingUtils;
|
||||
@@ -65,7 +66,9 @@ public final class ModManager extends LocalAddonManager<LocalModFile> {
|
||||
READERS = map;
|
||||
}
|
||||
|
||||
private String gameVersion;
|
||||
private final HashMap<Pair<String, ModLoaderType>, LocalMod> localMods = new HashMap<>();
|
||||
private final EnumSet<ModLoaderType> supportedLoaders = EnumSet.noneOf(ModLoaderType.class);
|
||||
private GameComponentAnalyzer analyzer;
|
||||
|
||||
private boolean loaded = false;
|
||||
@@ -83,7 +86,12 @@ public final class ModManager extends LocalAddonManager<LocalModFile> {
|
||||
}
|
||||
|
||||
public GameComponentAnalyzer getComponentAnalyzer() {
|
||||
return analyzer;
|
||||
lock.lock();
|
||||
try {
|
||||
return analyzer;
|
||||
} finally {
|
||||
lock.unlock();
|
||||
}
|
||||
}
|
||||
|
||||
public LocalMod getLocalMod(String modId, ModLoaderType modLoaderType) {
|
||||
@@ -105,6 +113,24 @@ public final class ModManager extends LocalAddonManager<LocalModFile> {
|
||||
}
|
||||
}
|
||||
|
||||
public String getGameVersion() {
|
||||
lock.lock();
|
||||
try {
|
||||
return gameVersion;
|
||||
} finally {
|
||||
lock.unlock();
|
||||
}
|
||||
}
|
||||
|
||||
public EnumSet<ModLoaderType> getSupportedLoaders() {
|
||||
lock.lock();
|
||||
try {
|
||||
return EnumSet.copyOf(supportedLoaders);
|
||||
} finally {
|
||||
lock.unlock();
|
||||
}
|
||||
}
|
||||
|
||||
private void addModInfo(Path file) {
|
||||
String fileName = StringUtils.removeSuffix(FileUtils.getName(file), DISABLED_EXTENSION, OLD_EXTENSION);
|
||||
String extension = fileName.substring(fileName.lastIndexOf(".") + 1);
|
||||
@@ -176,6 +202,18 @@ public final class ModManager extends LocalAddonManager<LocalModFile> {
|
||||
}
|
||||
}
|
||||
|
||||
public void analyze() throws IOException {
|
||||
lock.lock();
|
||||
try {
|
||||
gameVersion = instance.getVersion().toString();
|
||||
analyzer = instance.getAnalyzer();
|
||||
} catch (NoSuchGameInstanceException e) {
|
||||
throw new IOException(e);
|
||||
} finally {
|
||||
lock.unlock();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void refresh() throws IOException {
|
||||
lock.lock();
|
||||
@@ -183,7 +221,7 @@ public final class ModManager extends LocalAddonManager<LocalModFile> {
|
||||
localFiles.clear();
|
||||
localMods.clear();
|
||||
|
||||
analyzer = instance.getAnalyzer();
|
||||
analyze();
|
||||
|
||||
boolean supportSubfolders = analyzer.has(GameComponentType.FORGE)
|
||||
|| analyzer.has(GameComponentType.QUILT)
|
||||
@@ -205,6 +243,9 @@ public final class ModManager extends LocalAddonManager<LocalModFile> {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
updateSupportedLoaders();
|
||||
|
||||
loaded = true;
|
||||
} finally {
|
||||
lock.unlock();
|
||||
@@ -254,6 +295,50 @@ public final class ModManager extends LocalAddonManager<LocalModFile> {
|
||||
}
|
||||
}
|
||||
|
||||
private void updateSupportedLoaders() {
|
||||
supportedLoaders.clear();
|
||||
|
||||
if (this.analyzer == null) {
|
||||
Collections.addAll(supportedLoaders, ModLoaderType.values());
|
||||
return;
|
||||
}
|
||||
|
||||
for (GameComponentType type : GameComponentType.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(GameComponentType.NEO_FORGE) && "1.20.1".equals(gameVersion)) {
|
||||
supportedLoaders.add(ModLoaderType.FORGE);
|
||||
}
|
||||
|
||||
if (this.analyzer.has(GameComponentType.QUILT)) {
|
||||
supportedLoaders.add(ModLoaderType.FABRIC);
|
||||
}
|
||||
|
||||
if (this.analyzer.has(GameComponentType.LEGACY_FABRIC)) {
|
||||
supportedLoaders.add(ModLoaderType.FABRIC);
|
||||
}
|
||||
|
||||
if (this.analyzer.has(GameComponentType.FABRIC) && hasMod("kilt", ModLoaderType.FABRIC)) {
|
||||
supportedLoaders.add(ModLoaderType.FORGE);
|
||||
supportedLoaders.add(ModLoaderType.NEO_FORGE);
|
||||
}
|
||||
|
||||
// Sinytra Connector
|
||||
if (this.analyzer.has(GameComponentType.NEO_FORGE) && (hasMod("connector", ModLoaderType.NEO_FORGE) || hasMod("connectormod", ModLoaderType.NEO_FORGE))
|
||||
|| "1.20.1".equals(gameVersion) && this.analyzer.has(GameComponentType.FORGE) && hasMod("connectormod", ModLoaderType.FORGE)) {
|
||||
supportedLoaders.add(ModLoaderType.FABRIC);
|
||||
}
|
||||
}
|
||||
|
||||
public void rollback(LocalModFile from, LocalModFile to) throws IOException {
|
||||
lock.lock();
|
||||
try {
|
||||
|
||||
@@ -18,17 +18,23 @@
|
||||
package org.jackhuang.hmcl.game;
|
||||
|
||||
import org.jackhuang.hmcl.addon.mod.ModLoaderType;
|
||||
import org.jackhuang.hmcl.schematic.LitematicaConfig;
|
||||
import org.jackhuang.hmcl.util.StringUtils;
|
||||
import org.jackhuang.hmcl.util.gson.JsonUtils;
|
||||
import org.jackhuang.hmcl.util.platform.Platform;
|
||||
import org.jackhuang.hmcl.util.versioning.GameVersionNumber;
|
||||
import org.jetbrains.annotations.NotNullByDefault;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.EnumSet;
|
||||
import java.util.Optional;
|
||||
import java.util.Set;
|
||||
|
||||
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
|
||||
|
||||
/// Provides a view of a game instance and its instance-specific paths within a
|
||||
/// [GameRepositorySnapshot].
|
||||
///
|
||||
@@ -160,7 +166,25 @@ public interface GameInstance {
|
||||
///
|
||||
/// @return the schematics directory below the run directory
|
||||
default Path getSchematicsDirectory() {
|
||||
return getRunDirectory().resolve("schematics");
|
||||
var runDir = getRunDirectory();
|
||||
var litematicaConfig = runDir.resolve("config").resolve("litematica.json");
|
||||
Path dir = runDir.resolve("schematics");
|
||||
if (Files.isRegularFile(litematicaConfig)) {
|
||||
try {
|
||||
var conf = JsonUtils.fromJsonFile(litematicaConfig, LitematicaConfig.class);
|
||||
if (conf != null
|
||||
&& conf.generic() != null
|
||||
&& conf.generic().customSchematicBaseDirectoryEnabled()
|
||||
&& StringUtils.isNotBlank(conf.generic().customSchematicBaseDirectory())
|
||||
) {
|
||||
var p = Path.of(conf.generic().customSchematicBaseDirectory());
|
||||
dir = p.isAbsolute() ? p.normalize() : runDir.resolve(p).normalize();
|
||||
}
|
||||
} catch (Exception e) {
|
||||
LOG.warning("Failed to load custom schematics directory from litematica config at '%s'".formatted(litematicaConfig), e);
|
||||
}
|
||||
}
|
||||
return dir;
|
||||
}
|
||||
|
||||
/// Returns the directory used for extracted native libraries for a platform.
|
||||
|
||||
@@ -59,7 +59,6 @@ public interface ModAdviser {
|
||||
"irisUpdateInfo.json", // Iris
|
||||
"modernfix", // ModernFix
|
||||
"modtranslations", // Mod translations
|
||||
"schematics", // Schematics mod
|
||||
"journeymap/data", // JourneyMap
|
||||
"mods/.connector" // Sinytra Connector
|
||||
);
|
||||
@@ -70,6 +69,7 @@ public interface ModAdviser {
|
||||
"blueprints" /* BuildCraft */,
|
||||
"optionsof.txt" /* OptiFine */,
|
||||
"journeymap" /* JourneyMap */,
|
||||
"schematics" /* Schematics mod */,
|
||||
"optionsshaders.txt",
|
||||
"mods/VoxelMods");
|
||||
|
||||
|
||||
@@ -17,33 +17,31 @@
|
||||
*/
|
||||
package org.jackhuang.hmcl.schematic;
|
||||
|
||||
import javafx.geometry.Point3D;
|
||||
import org.glavo.nbt.io.NBTCodec;
|
||||
import org.glavo.nbt.tag.*;
|
||||
import org.glavo.nbt.tag.CompoundTag;
|
||||
import org.glavo.nbt.tag.IntArrayTag;
|
||||
import org.glavo.nbt.tag.IntTag;
|
||||
import org.glavo.nbt.tag.Tag;
|
||||
import org.jackhuang.hmcl.util.Point3I;
|
||||
import org.jackhuang.hmcl.util.StringUtils;
|
||||
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 java.util.OptionalInt;
|
||||
import java.util.OptionalLong;
|
||||
|
||||
/// @author Glavo
|
||||
/// @see <a href="https://litemapy.readthedocs.io/en/v0.9.0b0/litematics.html">The Litematic file format</a>
|
||||
public final class LitematicFile {
|
||||
import static org.jackhuang.hmcl.util.NBTUtils.*;
|
||||
|
||||
private static @Nullable String tryGetString(Tag tag) {
|
||||
return tag instanceof StringTag stringTag ? stringTag.get() : null;
|
||||
}
|
||||
/**
|
||||
* @author Glavo
|
||||
* @see <a href="https://litemapy.readthedocs.io/en/v0.9.0b0/litematics.html">The Litematic file format</a>
|
||||
*/
|
||||
public final class LitematicFile extends Schematic {
|
||||
|
||||
public static LitematicFile load(Path file) throws IOException {
|
||||
|
||||
CompoundTag root;
|
||||
try (InputStream in = new GZIPInputStream(Files.newInputStream(file))) {
|
||||
root = NBTCodec.of().readTag(in, TagType.COMPOUND);
|
||||
}
|
||||
CompoundTag root = readCompressed(file);
|
||||
|
||||
Tag versionTag = root.get("Version");
|
||||
if (versionTag == null)
|
||||
@@ -61,19 +59,29 @@ public final class LitematicFile {
|
||||
if (root.get("Regions") instanceof CompoundTag regionsTag)
|
||||
regions = regionsTag.size();
|
||||
|
||||
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 Point3I(x, y, z);
|
||||
}
|
||||
|
||||
Tag subVersionTag = root.get("SubVersion");
|
||||
return new LitematicFile(file, (CompoundTag) metadataTag,
|
||||
((IntTag) versionTag).getValue(),
|
||||
root.getIntOrZero("SubVersion"),
|
||||
root.getIntOrZero("MinecraftDataVersion"),
|
||||
regions
|
||||
((IntTag) versionTag).get(),
|
||||
tryGetInt(subVersionTag).orElse(-1),
|
||||
tryGetInt(root.get("MinecraftDataVersion")).orElse(0),
|
||||
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;
|
||||
@@ -83,14 +91,12 @@ public final class LitematicFile {
|
||||
private final Instant timeModified;
|
||||
private final int totalBlocks;
|
||||
private final int totalVolume;
|
||||
private final Point3D 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, Point3I enclosingSize) {
|
||||
super(file, minecraftDataVersion, enclosingSize);
|
||||
this.version = version;
|
||||
this.subVersion = subVersion;
|
||||
this.minecraftDataVersion = minecraftDataVersion;
|
||||
this.regionCount = regionCount;
|
||||
|
||||
Tag previewImageData = metadata.get("PreviewImageData");
|
||||
@@ -101,48 +107,39 @@ public final class LitematicFile {
|
||||
this.name = tryGetString(metadata.get("Name"));
|
||||
this.author = tryGetString(metadata.get("Author"));
|
||||
this.description = tryGetString(metadata.get("Description"));
|
||||
this.timeCreated = metadata.get("TimeCreated") instanceof LongTag time ? Instant.ofEpochMilli(time.getValue()) : null;
|
||||
this.timeModified = metadata.get("TimeModified") instanceof LongTag time ? Instant.ofEpochMilli(time.getValue()) : null;
|
||||
this.totalBlocks = metadata.getIntOrZero("TotalBlocks");
|
||||
this.totalVolume = metadata.getIntOrZero("TotalVolume");
|
||||
|
||||
Point3D enclosingSize = null;
|
||||
if (metadata.get("EnclosingSize") instanceof CompoundTag list) {
|
||||
int x = list.getIntOrZero("x");
|
||||
int y = list.getIntOrZero("y");
|
||||
int z = list.getIntOrZero("z");
|
||||
|
||||
if (x >= 0 && y >= 0 && z >= 0)
|
||||
enclosingSize = new Point3D(x, y, z);
|
||||
}
|
||||
this.enclosingSize = enclosingSize;
|
||||
this.timeCreated = tryGetLongTimestamp(metadata.get("TimeCreated"));
|
||||
this.timeModified = tryGetLongTimestamp(metadata.get("TimeModified"));
|
||||
this.totalBlocks = tryGetInt(metadata.get("TotalBlocks")).orElse(-1);
|
||||
this.totalVolume = tryGetInt(metadata.get("TotalVolume")).orElse(-1);
|
||||
|
||||
}
|
||||
|
||||
public @NotNull Path getFile() {
|
||||
return file;
|
||||
@Override
|
||||
public SchematicType getType() {
|
||||
return SchematicType.LITEMATIC;
|
||||
}
|
||||
|
||||
public int getVersion() {
|
||||
return version;
|
||||
@Override
|
||||
public OptionalInt getVersion() {
|
||||
return OptionalInt.of(version);
|
||||
}
|
||||
|
||||
public int getSubVersion() {
|
||||
return subVersion;
|
||||
}
|
||||
|
||||
public int getMinecraftDataVersion() {
|
||||
return minecraftDataVersion;
|
||||
@Override
|
||||
public OptionalInt getSubVersion() {
|
||||
return subVersion >= 0 ? OptionalInt.of(subVersion) : OptionalInt.empty();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int[] getPreviewImageData() {
|
||||
return previewImageData != null ? previewImageData.clone() : null;
|
||||
}
|
||||
|
||||
public String getName() {
|
||||
return name;
|
||||
@Override
|
||||
public @NotNull String getName() {
|
||||
return StringUtils.isNotBlank(name) ? name : super.getName();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getAuthor() {
|
||||
return author;
|
||||
}
|
||||
@@ -151,27 +148,29 @@ public final class LitematicFile {
|
||||
return description;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Instant getTimeCreated() {
|
||||
return timeCreated;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Instant getTimeModified() {
|
||||
return timeModified;
|
||||
}
|
||||
|
||||
public int getTotalBlocks() {
|
||||
return totalBlocks;
|
||||
@Override
|
||||
public OptionalInt getTotalBlocks() {
|
||||
return totalBlocks >= 1 ? OptionalInt.of(totalBlocks) : OptionalInt.empty();
|
||||
}
|
||||
|
||||
public int getTotalVolume() {
|
||||
return totalVolume;
|
||||
@Override
|
||||
public OptionalLong getTotalVolume() {
|
||||
return totalVolume >= 1 ? OptionalLong.of(totalVolume) : OptionalLong.empty();
|
||||
}
|
||||
|
||||
public Point3D getEnclosingSize() {
|
||||
return enclosingSize;
|
||||
@Override
|
||||
public OptionalInt getRegionCount() {
|
||||
return regionCount >= 1 ? OptionalInt.of(regionCount) : OptionalInt.empty();
|
||||
}
|
||||
|
||||
public int getRegionCount() {
|
||||
return regionCount;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
/*
|
||||
* Hello Minecraft! Launcher
|
||||
* Copyright (C) 2026 huangyuhui <huanghongxun2008@126.com> 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 <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
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) {
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
/*
|
||||
* Hello Minecraft! Launcher
|
||||
* Copyright (C) 2026 huangyuhui <huanghongxun2008@126.com> 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 <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.jackhuang.hmcl.schematic;
|
||||
|
||||
import org.glavo.nbt.tag.CompoundTag;
|
||||
import org.glavo.nbt.tag.ListTag;
|
||||
import org.glavo.nbt.tag.Tag;
|
||||
import org.jackhuang.hmcl.util.NBTUtils;
|
||||
import org.jackhuang.hmcl.util.Point3I;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Path;
|
||||
|
||||
import static org.jackhuang.hmcl.util.NBTUtils.tryGetInt;
|
||||
|
||||
/// @author Calboot
|
||||
/// @see <a href="https://minecraft.wiki/w/Structure_file">Structure File</a>
|
||||
public final class NBTStructureFile extends Schematic {
|
||||
|
||||
public static NBTStructureFile load(Path file) throws IOException {
|
||||
|
||||
CompoundTag root = NBTUtils.readCompressed(file);
|
||||
|
||||
if (!(root.get("blocks") instanceof ListTag<?>))
|
||||
throw new IOException("blocks tag not found");
|
||||
if (!(root.get("entities") instanceof ListTag<?>))
|
||||
throw new IOException("entities tag not found");
|
||||
|
||||
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");
|
||||
ListTag<?> size = (ListTag<?>) sizeTag;
|
||||
if (size.size() != 3)
|
||||
throw new IOException("size tag does not have 3 elements");
|
||||
Tag xTag = size.getTag(0);
|
||||
Tag yTag = size.getTag(1);
|
||||
Tag zTag = size.getTag(2);
|
||||
Point3I enclosingSize = 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 Point3I(width, height, length);
|
||||
}
|
||||
|
||||
return new NBTStructureFile(file, tryGetInt(root.get("DataVersion")).orElse(500), NBTUtils.tryGetString(root.get("author")), enclosingSize);
|
||||
}
|
||||
|
||||
private final String author;
|
||||
|
||||
private NBTStructureFile(Path file, int dataVersion, String author, Point3I enclosingSize) {
|
||||
super(file, dataVersion, enclosingSize);
|
||||
this.author = author;
|
||||
}
|
||||
|
||||
@Override
|
||||
public SchematicType getType() {
|
||||
return SchematicType.NBT_STRUCTURE;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getAuthor() {
|
||||
return author;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,152 @@
|
||||
/*
|
||||
* Hello Minecraft! Launcher
|
||||
* Copyright (C) 2026 huangyuhui <huanghongxun2008@126.com> 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 <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.jackhuang.hmcl.schematic;
|
||||
|
||||
import org.glavo.nbt.tag.CompoundTag;
|
||||
import org.glavo.nbt.tag.IntTag;
|
||||
import org.glavo.nbt.tag.StringTag;
|
||||
import org.glavo.nbt.tag.Tag;
|
||||
import org.jackhuang.hmcl.util.NBTUtils;
|
||||
import org.jackhuang.hmcl.util.Point3I;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Path;
|
||||
import java.util.OptionalInt;
|
||||
|
||||
import static org.jackhuang.hmcl.util.NBTUtils.tryGetShort;
|
||||
import static org.jackhuang.hmcl.util.NBTUtils.tryGetString;
|
||||
|
||||
/// @author Calboot
|
||||
/// @see <a href="https://minecraft.wiki/w/Schematic_file_format">Schematic File Format Wiki</a>
|
||||
/// @see <a href="https://github.com/Lunatrius/Schematica/blob/master/src/main/java/com/github/lunatrius/schematica/world/schematic/SchematicAlpha.java">Schematica</a>
|
||||
/// @see <a href="https://github.com/SpongePowered/Schematic-Specification/tree/master/versions">Sponge Spec</a>
|
||||
public final class SchemFile extends Schematic {
|
||||
|
||||
private static final int DATA_VERSION_MC_1_13_2 = 1631;
|
||||
|
||||
public static SchemFile load(Path file) throws IOException {
|
||||
|
||||
CompoundTag root = NBTUtils.readCompressed(file);
|
||||
|
||||
if (root.get("Materials") != null) return loadLegacy(file, root);
|
||||
else if (root.get("Version") != null) return loadSponge(file, root);
|
||||
else {
|
||||
var schem = root.get("Schematic");
|
||||
if (schem instanceof CompoundTag sponge3 && sponge3.get("Version") != null) {
|
||||
return loadSponge(file, sponge3);
|
||||
}
|
||||
}
|
||||
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");
|
||||
Point3I enclosingSize = null;
|
||||
if (widthTag != null && heightTag != null && lengthTag != null) {
|
||||
short width = tryGetShort(widthTag).orElse((short) 0);
|
||||
short height = tryGetShort(heightTag).orElse((short) 0);
|
||||
short length = tryGetShort(lengthTag).orElse((short) 0);
|
||||
if (width > 0 && height > 0 && length > 0) {
|
||||
enclosingSize = new Point3I(width, height, length);
|
||||
}
|
||||
}
|
||||
|
||||
return new SchemFile(file, null, null, ((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;
|
||||
dataVersion = dataVersionTag == null ? DATA_VERSION_MC_1_13_2 : NBTUtils.tryGetInt(dataVersionTag).orElse(0);
|
||||
|
||||
Tag widthTag = root.get("Width");
|
||||
Tag heightTag = root.get("Height");
|
||||
Tag lengthTag = root.get("Length");
|
||||
Point3I enclosingSize = null;
|
||||
if (widthTag != null && heightTag != null && lengthTag != null) {
|
||||
int width = tryGetShort(widthTag).orElse((short) 0) & 0xFFFF;
|
||||
int height = tryGetShort(heightTag).orElse((short) 0) & 0xFFFF;
|
||||
int length = tryGetShort(lengthTag).orElse((short) 0) & 0xFFFF;
|
||||
if (width > 0 && height > 0 && length > 0) {
|
||||
enclosingSize = new Point3I(width, height, length);
|
||||
}
|
||||
}
|
||||
|
||||
Tag metaTag = root.get("Metadata");
|
||||
String name = null, author = null;
|
||||
if (metaTag instanceof CompoundTag meta) {
|
||||
name = tryGetString(meta.get("Name"));
|
||||
author = tryGetString(meta.get("Author"));
|
||||
}
|
||||
|
||||
return new SchemFile(file, name, author, null, dataVersion, ((IntTag) versionTag).getValue(), enclosingSize);
|
||||
}
|
||||
|
||||
private final @Nullable String name;
|
||||
private final @Nullable String author;
|
||||
private final @Nullable String materials;
|
||||
private final int version;
|
||||
|
||||
private SchemFile(Path file, @Nullable String name, @Nullable String author, @Nullable String materials, int dataVersion, int version, Point3I enclosingSize) {
|
||||
super(file, dataVersion, enclosingSize);
|
||||
this.name = name;
|
||||
this.author = author;
|
||||
this.materials = materials;
|
||||
this.version = version;
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NotNull String getName() {
|
||||
if (name == null) return super.getName();
|
||||
return name;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getAuthor() {
|
||||
if (author == null) return super.getAuthor();
|
||||
return author;
|
||||
}
|
||||
|
||||
@Override
|
||||
public SchematicType getType() {
|
||||
return SchematicType.SCHEM;
|
||||
}
|
||||
|
||||
@Override
|
||||
public OptionalInt getVersion() {
|
||||
return version > 0 ? OptionalInt.of(version) : OptionalInt.empty();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getMinecraftVersion() {
|
||||
return materials == null ? super.getMinecraftVersion() : materials;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,128 @@
|
||||
/*
|
||||
* Hello Minecraft! Launcher
|
||||
* Copyright (C) 2026 huangyuhui <huanghongxun2008@126.com> 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 <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.jackhuang.hmcl.schematic;
|
||||
|
||||
import org.jackhuang.hmcl.util.Point3I;
|
||||
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.Path;
|
||||
import java.time.Instant;
|
||||
import java.util.OptionalInt;
|
||||
import java.util.OptionalLong;
|
||||
|
||||
public sealed abstract class Schematic permits LitematicFile, SchemFile, NBTStructureFile {
|
||||
|
||||
public static boolean isFileSchematic(Path file) {
|
||||
return SchematicType.getType(file) != null;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public static Schematic load(Path file) throws IOException {
|
||||
var type = SchematicType.getType(file);
|
||||
if (type == null) return null;
|
||||
return switch (type) {
|
||||
case LITEMATIC -> LitematicFile.load(file);
|
||||
case SCHEM -> SchemFile.load(file);
|
||||
case NBT_STRUCTURE -> NBTStructureFile.load(file);
|
||||
};
|
||||
}
|
||||
|
||||
private final Path file;
|
||||
private final int dataVersion;
|
||||
private final Point3I enclosingSize;
|
||||
|
||||
protected Schematic(Path file, int dataVersion, @Nullable Point3I enclosingSize) {
|
||||
this.file = file;
|
||||
this.dataVersion = dataVersion;
|
||||
this.enclosingSize = enclosingSize;
|
||||
}
|
||||
|
||||
public abstract SchematicType getType();
|
||||
|
||||
@NotNull
|
||||
public Path getFile() {
|
||||
return file;
|
||||
}
|
||||
|
||||
public OptionalInt getVersion() {
|
||||
return OptionalInt.empty();
|
||||
}
|
||||
|
||||
/// Non-negative, otherwise empty
|
||||
public OptionalInt getSubVersion() {
|
||||
return OptionalInt.empty();
|
||||
}
|
||||
|
||||
/// At least 100, otherwise empty
|
||||
public OptionalInt getMinecraftDataVersion() {
|
||||
/* 15w32a */
|
||||
return dataVersion >= 100 ? OptionalInt.of(dataVersion) : OptionalInt.empty();
|
||||
}
|
||||
|
||||
@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 OptionalInt getRegionCount() {
|
||||
return OptionalInt.empty();
|
||||
}
|
||||
|
||||
public OptionalInt getTotalBlocks() {
|
||||
return OptionalInt.empty();
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public Point3I getEnclosingSize() {
|
||||
return enclosingSize;
|
||||
}
|
||||
|
||||
public OptionalLong getTotalVolume() {
|
||||
var enclosingSize = getEnclosingSize();
|
||||
if (enclosingSize != null) return OptionalLong.of((long) enclosingSize.x() * enclosingSize.y() * enclosingSize.z());
|
||||
return OptionalLong.empty();
|
||||
}
|
||||
|
||||
public int @Nullable [] getPreviewImageData() {
|
||||
return null;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
/*
|
||||
* Hello Minecraft! Launcher
|
||||
* Copyright (C) 2026 huangyuhui <huanghongxun2008@126.com> 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 <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
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<String> extensions;
|
||||
|
||||
SchematicType(String... ext) {
|
||||
this.extensions = List.of(ext);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
/*
|
||||
* Hello Minecraft! Launcher
|
||||
* Copyright (C) 2026 huangyuhui <huanghongxun2008@126.com> 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 <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.jackhuang.hmcl.util;
|
||||
|
||||
import org.glavo.nbt.io.NBTCodec;
|
||||
import org.glavo.nbt.tag.*;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Path;
|
||||
import java.time.Instant;
|
||||
import java.util.Optional;
|
||||
import java.util.OptionalInt;
|
||||
import java.util.OptionalLong;
|
||||
|
||||
public final class NBTUtils {
|
||||
|
||||
public static CompoundTag readCompressed(Path file) throws IOException {
|
||||
return NBTCodec.of().readTag(file, TagType.COMPOUND);
|
||||
}
|
||||
|
||||
public static OptionalLong tryGetLong(Tag tag) {
|
||||
if (tag instanceof ValueTag<?> valueTag) {
|
||||
Object val = valueTag.getValue();
|
||||
if (val instanceof Long || val instanceof Integer || val instanceof Short || val instanceof Byte) {
|
||||
return OptionalLong.of(((Number) val).longValue());
|
||||
}
|
||||
}
|
||||
return OptionalLong.empty();
|
||||
}
|
||||
|
||||
public static OptionalInt tryGetInt(Tag tag) {
|
||||
var ol = tryGetLong(tag);
|
||||
if (ol.isEmpty()) return OptionalInt.empty();
|
||||
long l = ol.getAsLong();
|
||||
int i = (int) l;
|
||||
if (l == i) return OptionalInt.of(i);
|
||||
return OptionalInt.empty();
|
||||
}
|
||||
|
||||
public static Optional<Short> tryGetShort(Tag tag) {
|
||||
var ol = tryGetLong(tag);
|
||||
if (ol.isEmpty()) return Optional.empty();
|
||||
long l = ol.getAsLong();
|
||||
short i = (short) l;
|
||||
if (l == i) return Optional.of(i);
|
||||
return Optional.empty();
|
||||
}
|
||||
|
||||
public static @Nullable Instant tryGetLongTimestamp(Tag tag) {
|
||||
if (tag instanceof LongTag longTag) {
|
||||
return Instant.ofEpochMilli(longTag.get());
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public static @Nullable String tryGetString(Tag tag) {
|
||||
return tag instanceof StringTag stringTag ? stringTag.get() : null;
|
||||
}
|
||||
|
||||
private NBTUtils() {
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
/*
|
||||
* Hello Minecraft! Launcher
|
||||
* Copyright (C) 2026 huangyuhui <huanghongxun2008@126.com> 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 <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.jackhuang.hmcl.util;
|
||||
|
||||
public record Point3I(int x, int y, int z) {
|
||||
|
||||
public static final Point3I ZERO = new Point3I(0, 0, 0);
|
||||
|
||||
}
|
||||
@@ -1,53 +0,0 @@
|
||||
/*
|
||||
* Hello Minecraft! Launcher
|
||||
* Copyright (C) 2025 huangyuhui <huanghongxun2008@126.com> 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 <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.jackhuang.hmcl.schematic;
|
||||
|
||||
import javafx.geometry.Point3D;
|
||||
import org.jackhuang.hmcl.game.CrashReportAnalyzerTest;
|
||||
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 Point3D(17, 26, 13), file.getEnclosingSize());
|
||||
assertEquals(1, file.getRegionCount());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,88 @@
|
||||
/*
|
||||
* Hello Minecraft! Launcher
|
||||
* Copyright (C) 2025 huangyuhui <huanghongxun2008@126.com> 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 <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.jackhuang.hmcl.schematic;
|
||||
|
||||
import org.jackhuang.hmcl.game.CrashReportAnalyzerTest;
|
||||
import org.jackhuang.hmcl.util.Point3I;
|
||||
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(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().orElse(0));
|
||||
assertEquals(5746, lFile.getTotalVolume().orElse(0));
|
||||
assertEquals(new Point3I(17, 26, 13), lFile.getEnclosingSize());
|
||||
assertEquals(1, lFile.getRegionCount().orElse(0));
|
||||
assertEquals(4325, lFile.getMinecraftDataVersion().orElse(0));
|
||||
assertEquals("4325", lFile.getMinecraftVersion());
|
||||
assertEquals(7, lFile.getVersion().orElse(0));
|
||||
assertEquals(1, lFile.getSubVersion().orElse(-1));
|
||||
}
|
||||
|
||||
{
|
||||
SchemFile sFile = (SchemFile) load("/schematics/test.schematic");
|
||||
assertEquals(SchematicType.SCHEM, sFile.getType());
|
||||
assertEquals("test", sFile.getName());
|
||||
assertEquals(new Point3I(28, 35, 18), sFile.getEnclosingSize());
|
||||
assertEquals(17640, sFile.getTotalVolume().orElse(0));
|
||||
assertEquals("Alpha", sFile.getMinecraftVersion());
|
||||
}
|
||||
|
||||
{
|
||||
SchemFile sFileSponge = (SchemFile) load("/schematics/test.schem");
|
||||
assertEquals(SchematicType.SCHEM, sFileSponge.getType());
|
||||
assertEquals("test", sFileSponge.getName());
|
||||
assertEquals(3465, sFileSponge.getMinecraftDataVersion().orElse(0));
|
||||
assertEquals("3465", sFileSponge.getMinecraftVersion());
|
||||
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 Point3I(9, 11, 13), nFile.getEnclosingSize());
|
||||
assertEquals(1287, nFile.getTotalVolume().orElse(0));
|
||||
assertEquals(3465, nFile.getMinecraftDataVersion().orElse(0));
|
||||
assertEquals("3465", nFile.getMinecraftVersion());
|
||||
}
|
||||
}
|
||||
}
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Reference in new issue
Block a user