Compare commits
8
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2f5594d8c1 | ||
|
|
13ad50208f | ||
|
|
13910c24b7 | ||
|
|
cc9bdb62ca | ||
|
|
baa7c816b7 | ||
|
|
090f0b9822 | ||
|
|
6d9ae24912 | ||
|
|
d03851b196 |
No files matched your search
@@ -33,6 +33,7 @@ import org.jetbrains.annotations.Nullable;
|
||||
import java.io.IOException;
|
||||
import java.nio.charset.Charset;
|
||||
import java.nio.file.Path;
|
||||
import java.util.Set;
|
||||
|
||||
public final class HMCLModpackProvider implements ModpackProvider {
|
||||
public static final HMCLModpackProvider INSTANCE = new HMCLModpackProvider();
|
||||
@@ -48,7 +49,12 @@ public final class HMCLModpackProvider implements ModpackProvider {
|
||||
}
|
||||
|
||||
@Override
|
||||
public Task<?> createUpdateTask(DefaultDependencyManager dependencyManager, DefaultGameInstance instance, Path zipFile, Modpack modpack) throws MismatchedModpackTypeException {
|
||||
public Task<?> createUpdateTask(
|
||||
DefaultDependencyManager dependencyManager,
|
||||
DefaultGameInstance instance,
|
||||
Path zipFile,
|
||||
Modpack modpack,
|
||||
@Nullable Set<String> excludedFiles) throws MismatchedModpackTypeException {
|
||||
if (!(modpack.getManifest() instanceof HMCLModpackManifest))
|
||||
throw new MismatchedModpackTypeException(getName(), modpack.getManifest().getProvider().getName());
|
||||
|
||||
@@ -77,7 +83,12 @@ public final class HMCLModpackProvider implements ModpackProvider {
|
||||
|
||||
private final static class HMCLModpack extends Modpack {
|
||||
@Override
|
||||
public Task<?> getInstallTask(DefaultDependencyManager dependencyManager, Path zipFile, GameInstanceID instanceId, String iconUrl) {
|
||||
public Task<?> getInstallTask(
|
||||
DefaultDependencyManager dependencyManager,
|
||||
Path zipFile,
|
||||
GameInstanceID instanceId,
|
||||
String iconUrl,
|
||||
@Nullable Set<String> excludedFiles) {
|
||||
return new HMCLModpackInstallTask((HMCLGameRepository) dependencyManager.getGameRepository(), zipFile, this, instanceId);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -56,6 +56,7 @@ import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.Optional;
|
||||
import java.util.Set;
|
||||
|
||||
import static org.jackhuang.hmcl.util.Lang.mapOf;
|
||||
import static org.jackhuang.hmcl.util.Pair.pair;
|
||||
@@ -196,6 +197,26 @@ public final class ModpackHelper {
|
||||
}
|
||||
|
||||
public static Task<?> getInstallTask(HMCLGameRepository repository, Path zipFile, GameInstanceID instanceId, Modpack modpack, @Nullable String iconUrl) {
|
||||
return getInstallTask(repository, zipFile, instanceId, modpack, iconUrl, null);
|
||||
}
|
||||
|
||||
/// Creates an install task that respects optional-file selection.
|
||||
///
|
||||
/// @param repository the target repository
|
||||
/// @param zipFile the modpack archive
|
||||
/// @param instanceId the new instance id
|
||||
/// @param modpack the parsed modpack
|
||||
/// @param iconUrl the optional icon URL, or `null`
|
||||
/// @param excludedFiles keys of optional files the user chose not to install; `null` means install all.
|
||||
/// When non-null, must not contain `null` elements.
|
||||
/// @return the install task
|
||||
public static Task<?> getInstallTask(
|
||||
HMCLGameRepository repository,
|
||||
Path zipFile,
|
||||
GameInstanceID instanceId,
|
||||
Modpack modpack,
|
||||
@Nullable String iconUrl,
|
||||
@Nullable Set<String> excludedFiles) {
|
||||
ExceptionalRunnable<?> success = () -> {
|
||||
repository.refresh();
|
||||
repository.getInstance(instanceId).enableIsolation();
|
||||
@@ -209,17 +230,17 @@ public final class ModpackHelper {
|
||||
};
|
||||
|
||||
if (modpack.getManifest() instanceof MultiMCInstanceConfiguration)
|
||||
return modpack.getInstallTask(repository.getDependency(), zipFile, instanceId, iconUrl)
|
||||
return modpack.getInstallTask(repository.getDependency(), zipFile, instanceId, iconUrl, excludedFiles)
|
||||
.whenComplete(Schedulers.defaultScheduler(), success, failure)
|
||||
.thenComposeAsync(createMultiMCPostInstallTask(repository, (MultiMCInstanceConfiguration) modpack.getManifest(), instanceId))
|
||||
.withStagesHints(new Task.StagesHint("hmcl.modpack"), new Task.StagesHint("hmcl.modpack.download", List.of("hmcl.install.assets", "hmcl.install.libraries")));
|
||||
else if (modpack.getManifest() instanceof McbbsModpackManifest)
|
||||
return modpack.getInstallTask(repository.getDependency(), zipFile, instanceId, iconUrl)
|
||||
return modpack.getInstallTask(repository.getDependency(), zipFile, instanceId, iconUrl, excludedFiles)
|
||||
.whenComplete(Schedulers.defaultScheduler(), success, failure)
|
||||
.thenComposeAsync(createMcbbsPostInstallTask(repository, (McbbsModpackManifest) modpack.getManifest(), instanceId))
|
||||
.withStagesHints(new Task.StagesHint("hmcl.modpack"), new Task.StagesHint("hmcl.modpack.download", List.of("hmcl.install.assets", "hmcl.install.libraries")));
|
||||
else
|
||||
return modpack.getInstallTask(repository.getDependency(), zipFile, instanceId, iconUrl)
|
||||
return modpack.getInstallTask(repository.getDependency(), zipFile, instanceId, iconUrl, excludedFiles)
|
||||
.whenComplete(Schedulers.defaultScheduler(), success, failure)
|
||||
.withStagesHints(new Task.StagesHint("hmcl.modpack"), new Task.StagesHint("hmcl.modpack.download", List.of("hmcl.install.assets", "hmcl.install.libraries")));
|
||||
}
|
||||
@@ -240,17 +261,38 @@ public final class ModpackHelper {
|
||||
}
|
||||
|
||||
public static Task<?> getUpdateTask(HMCLGameRepository repository, Path zipFile, Charset charset, GameInstanceID instanceId, ModpackConfiguration<?> configuration) throws UnsupportedModpackException, ManuallyCreatedModpackException, MismatchedModpackTypeException {
|
||||
return getUpdateTask(repository, zipFile, charset, instanceId, configuration, null);
|
||||
}
|
||||
|
||||
/// Creates an update task that respects optional-file selection.
|
||||
///
|
||||
/// @param repository the target repository
|
||||
/// @param zipFile the modpack archive
|
||||
/// @param charset the archive encoding
|
||||
/// @param instanceId the instance to update
|
||||
/// @param configuration the existing modpack configuration
|
||||
/// @param excludedFiles keys of optional files the user chose not to install; `null` means install all.
|
||||
/// When non-null, must not contain `null` elements.
|
||||
/// @return the update task
|
||||
public static Task<?> getUpdateTask(
|
||||
HMCLGameRepository repository,
|
||||
Path zipFile,
|
||||
Charset charset,
|
||||
GameInstanceID instanceId,
|
||||
ModpackConfiguration<?> configuration,
|
||||
@Nullable Set<String> excludedFiles)
|
||||
throws UnsupportedModpackException, ManuallyCreatedModpackException, MismatchedModpackTypeException {
|
||||
Modpack modpack = ModpackHelper.readModpackManifest(zipFile, charset);
|
||||
ModpackProvider provider = getProviderByType(configuration.getType());
|
||||
if (provider == null) {
|
||||
throw new UnsupportedModpackException();
|
||||
}
|
||||
if (modpack.getManifest() instanceof MultiMCInstanceConfiguration)
|
||||
return provider.createUpdateTask(repository.getDependency(), repository.getInstance(instanceId), zipFile, modpack)
|
||||
return provider.createUpdateTask(repository.getDependency(), repository.getInstance(instanceId), zipFile, modpack, excludedFiles)
|
||||
.thenComposeAsync(() -> createMultiMCPostUpdateTask(repository, (MultiMCInstanceConfiguration) modpack.getManifest(), instanceId))
|
||||
.thenComposeAsync(repository.refreshAsync());
|
||||
else
|
||||
return provider.createUpdateTask(repository.getDependency(), repository.getInstance(instanceId), zipFile, modpack)
|
||||
return provider.createUpdateTask(repository.getDependency(), repository.getInstance(instanceId), zipFile, modpack, excludedFiles)
|
||||
.thenComposeAsync(repository.refreshAsync());
|
||||
}
|
||||
|
||||
|
||||
@@ -21,12 +21,14 @@ import com.jfoenix.controls.JFXListView;
|
||||
import javafx.beans.binding.DoubleBinding;
|
||||
import javafx.css.PseudoClass;
|
||||
import javafx.scene.control.ListCell;
|
||||
import javafx.scene.control.ListView;
|
||||
import javafx.scene.layout.Region;
|
||||
import javafx.scene.layout.StackPane;
|
||||
import org.jackhuang.hmcl.ui.FXUtils;
|
||||
|
||||
public abstract class MDListCell<T> extends ListCell<T> {
|
||||
private static final PseudoClass SELECTED = PseudoClass.getPseudoClass("selected");
|
||||
private static final PseudoClass LAST = PseudoClass.getPseudoClass("last");
|
||||
|
||||
private final StackPane container = new StackPane();
|
||||
private final StackPane root = new StackPane();
|
||||
@@ -58,7 +60,10 @@ public abstract class MDListCell<T> extends ListCell<T> {
|
||||
|
||||
super.updateItem(item, empty);
|
||||
|
||||
if (oldItem == item && oldEmpty == empty) return;
|
||||
if (oldItem == item && oldEmpty == empty) {
|
||||
updateLastPseudoClass(empty);
|
||||
return;
|
||||
}
|
||||
|
||||
ripplerContainer.releaseRippleImmediately();
|
||||
|
||||
@@ -68,6 +73,19 @@ public abstract class MDListCell<T> extends ListCell<T> {
|
||||
} else {
|
||||
setGraphic(root);
|
||||
}
|
||||
updateLastPseudoClass(empty || item == null);
|
||||
}
|
||||
|
||||
/// Updates the `:last` pseudo-class so the trailing divider can be suppressed in CSS.
|
||||
///
|
||||
/// @param empty whether this cell is empty
|
||||
private void updateLastPseudoClass(boolean empty) {
|
||||
ListView<T> listView = getListView();
|
||||
boolean last = !empty
|
||||
&& listView != null
|
||||
&& getIndex() >= 0
|
||||
&& getIndex() == listView.getItems().size() - 1;
|
||||
root.pseudoClassStateChanged(LAST, last);
|
||||
}
|
||||
|
||||
protected StackPane getContainer() {
|
||||
@@ -80,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);
|
||||
}
|
||||
@@ -33,44 +33,16 @@ import javafx.scene.control.ListCell;
|
||||
import javafx.scene.control.ProgressIndicator;
|
||||
import javafx.scene.layout.BorderPane;
|
||||
import javafx.scene.layout.StackPane;
|
||||
import org.jackhuang.hmcl.download.cleanroom.CleanroomInstallTask;
|
||||
import org.jackhuang.hmcl.download.fabric.FabricAPIInstallTask;
|
||||
import org.jackhuang.hmcl.download.fabric.FabricInstallTask;
|
||||
import org.jackhuang.hmcl.download.forge.ForgeNewInstallTask;
|
||||
import org.jackhuang.hmcl.download.forge.ForgeOldInstallTask;
|
||||
import org.jackhuang.hmcl.download.game.GameAssetDownloadTask;
|
||||
import org.jackhuang.hmcl.download.game.GameInstallTask;
|
||||
import org.jackhuang.hmcl.download.java.mojang.MojangJavaDownloadTask;
|
||||
import org.jackhuang.hmcl.download.legacyfabric.LegacyFabricInstallTask;
|
||||
import org.jackhuang.hmcl.download.liteloader.LiteLoaderInstallTask;
|
||||
import org.jackhuang.hmcl.download.neoforge.NeoForgeInstallTask;
|
||||
import org.jackhuang.hmcl.download.neoforge.NeoForgeOldInstallTask;
|
||||
import org.jackhuang.hmcl.download.optifine.OptiFineInstallTask;
|
||||
import org.jackhuang.hmcl.download.quilt.QuiltAPIInstallTask;
|
||||
import org.jackhuang.hmcl.download.quilt.QuiltInstallTask;
|
||||
import org.jackhuang.hmcl.game.HMCLModpackInstallTask;
|
||||
import org.jackhuang.hmcl.java.JavaInstallTask;
|
||||
import org.jackhuang.hmcl.modpack.MinecraftInstanceTask;
|
||||
import org.jackhuang.hmcl.modpack.ModpackInstallTask;
|
||||
import org.jackhuang.hmcl.modpack.ModpackUpdateTask;
|
||||
import org.jackhuang.hmcl.modpack.curse.CurseCompletionTask;
|
||||
import org.jackhuang.hmcl.modpack.curse.CurseInstallTask;
|
||||
import org.jackhuang.hmcl.modpack.mcbbs.McbbsModpackCompletionTask;
|
||||
import org.jackhuang.hmcl.modpack.mcbbs.McbbsModpackExportTask;
|
||||
import org.jackhuang.hmcl.modpack.modrinth.ModrinthCompletionTask;
|
||||
import org.jackhuang.hmcl.modpack.modrinth.ModrinthInstallTask;
|
||||
import org.jackhuang.hmcl.modpack.modrinth.ModrinthModpackExportTask;
|
||||
import org.jackhuang.hmcl.modpack.multimc.MultiMCModpackInstallTask;
|
||||
import org.jackhuang.hmcl.modpack.server.ServerModpackCompletionTask;
|
||||
import org.jackhuang.hmcl.modpack.server.ServerModpackExportTask;
|
||||
import org.jackhuang.hmcl.modpack.server.ServerModpackLocalInstallTask;
|
||||
import org.jackhuang.hmcl.task.Task;
|
||||
import org.jackhuang.hmcl.task.TaskExecutor;
|
||||
import org.jackhuang.hmcl.task.TaskListener;
|
||||
import org.jackhuang.hmcl.ui.FXUtils;
|
||||
import org.jackhuang.hmcl.ui.SVG;
|
||||
import org.jackhuang.hmcl.util.FXThread;
|
||||
import org.jackhuang.hmcl.util.i18n.I18n;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import java.lang.ref.WeakReference;
|
||||
import java.util.Collection;
|
||||
@@ -161,58 +133,16 @@ public final class TaskListPane extends StackPane {
|
||||
if (!task.getSignificance().shouldShow() || task.getName() == null)
|
||||
return;
|
||||
|
||||
if (task instanceof GameAssetDownloadTask) {
|
||||
task.setName(i18n("assets.download_all"));
|
||||
} else if (task instanceof GameInstallTask) {
|
||||
if (task.getInheritedStage() != null && task.getInheritedStage().startsWith("hmcl.install.game"))
|
||||
return;
|
||||
task.setName(i18n("install.installer.install", i18n("install.installer.game")));
|
||||
} else if (task instanceof CleanroomInstallTask) {
|
||||
task.setName(i18n("install.installer.install", i18n("install.installer.cleanroom")));
|
||||
} else if (task instanceof LegacyFabricInstallTask) {
|
||||
task.setName(i18n("install.installer.install", i18n("install.installer.legacyfabric")));
|
||||
} else if (task instanceof ForgeNewInstallTask || task instanceof ForgeOldInstallTask) {
|
||||
task.setName(i18n("install.installer.install", i18n("install.installer.forge")));
|
||||
} else if (task instanceof NeoForgeInstallTask || task instanceof NeoForgeOldInstallTask) {
|
||||
task.setName(i18n("install.installer.install", i18n("install.installer.neoforge")));
|
||||
} else if (task instanceof LiteLoaderInstallTask) {
|
||||
task.setName(i18n("install.installer.install", i18n("install.installer.liteloader")));
|
||||
} else if (task instanceof OptiFineInstallTask) {
|
||||
task.setName(i18n("install.installer.install", i18n("install.installer.optifine")));
|
||||
} else if (task instanceof FabricInstallTask) {
|
||||
task.setName(i18n("install.installer.install", i18n("install.installer.fabric")));
|
||||
} else if (task instanceof FabricAPIInstallTask) {
|
||||
task.setName(i18n("install.installer.install", i18n("install.installer.fabric-api")));
|
||||
} else if (task instanceof QuiltInstallTask) {
|
||||
task.setName(i18n("install.installer.install", i18n("install.installer.quilt")));
|
||||
} else if (task instanceof QuiltAPIInstallTask) {
|
||||
task.setName(i18n("install.installer.install", i18n("install.installer.quilt-api")));
|
||||
} else if (task instanceof CurseCompletionTask || task instanceof ModrinthCompletionTask || task instanceof ServerModpackCompletionTask || task instanceof McbbsModpackCompletionTask) {
|
||||
task.setName(i18n("modpack.completion"));
|
||||
} else if (task instanceof ModpackInstallTask) {
|
||||
task.setName(i18n("modpack.installing"));
|
||||
} else if (task instanceof ModpackUpdateTask) {
|
||||
task.setName(i18n("modpack.update"));
|
||||
} else if (task instanceof CurseInstallTask) {
|
||||
task.setName(i18n("modpack.installing.given", i18n("modpack.type.curse")));
|
||||
} else if (task instanceof MultiMCModpackInstallTask) {
|
||||
task.setName(i18n("modpack.installing.given", i18n("modpack.type.multimc")));
|
||||
} else if (task instanceof ModrinthInstallTask) {
|
||||
task.setName(i18n("modpack.installing.given", i18n("modpack.type.modrinth")));
|
||||
} else if (task instanceof ServerModpackLocalInstallTask) {
|
||||
task.setName(i18n("install.installing") + ": " + i18n("modpack.type.server"));
|
||||
} else if (task instanceof HMCLModpackInstallTask) {
|
||||
task.setName(i18n("modpack.installing.given", i18n("modpack.type.hmcl")));
|
||||
} else if (task instanceof McbbsModpackExportTask || task instanceof ServerModpackExportTask || task instanceof ModrinthModpackExportTask) {
|
||||
task.setName(i18n("modpack.export"));
|
||||
} else if (task instanceof MinecraftInstanceTask) {
|
||||
task.setName(i18n("modpack.scan"));
|
||||
} else if (task instanceof MojangJavaDownloadTask) {
|
||||
task.setName(i18n("download.java"));
|
||||
} else if (task instanceof JavaInstallTask) {
|
||||
task.setName(i18n("java.installing"));
|
||||
if (task instanceof GameInstallTask
|
||||
&& task.getInheritedStage() != null
|
||||
&& task.getInheritedStage().startsWith("hmcl.install.game")) {
|
||||
return;
|
||||
}
|
||||
|
||||
@Nullable String localizedName = I18n.translateTaskName(task);
|
||||
if (localizedName != null)
|
||||
task.setName(localizedName);
|
||||
|
||||
Platform.runLater(() -> {
|
||||
ProgressListNode node = new ProgressListNode(task);
|
||||
nodes.put(task, node);
|
||||
|
||||
@@ -125,10 +125,12 @@ public final class GameDirectoryPage extends BorderPane implements DecoratorPage
|
||||
setMessage(i18n("game_directory.already_exists"));
|
||||
}
|
||||
|
||||
/// Rejects names used by other game directories, excluding the entry being edited.
|
||||
@Override
|
||||
protected void eval() {
|
||||
JFXTextField control = (JFXTextField) this.getSrcControl();
|
||||
hasErrors.set(GameDirectoryManager.getGameDirectories().stream()
|
||||
.filter(existingGameDirectory -> existingGameDirectory != gameDirectory)
|
||||
.anyMatch(existingGameDirectory -> Objects.equals(
|
||||
GameDirectoryManager.getGameDirectoryCustomName(existingGameDirectory), control.getText())));
|
||||
}
|
||||
@@ -170,43 +172,52 @@ public final class GameDirectoryPage extends BorderPane implements DecoratorPage
|
||||
StackPane.setAlignment(saveButton, Pos.BOTTOM_RIGHT);
|
||||
saveButton.setPrefSize(100, 40);
|
||||
saveButton.setOnAction(e -> onSave());
|
||||
String initialLocation = getLocation();
|
||||
boolean initialUseRelativePath = toggleUseRelativePath.isSelected();
|
||||
saveButton.disableProperty().bind(Bindings.createBooleanBinding(
|
||||
() -> !txtGameDirectoryName.validate() || StringUtils.isBlank(getLocation()),
|
||||
txtGameDirectoryName.textProperty(), location));
|
||||
() -> !txtGameDirectoryName.validate() || StringUtils.isBlank(getLocation())
|
||||
|| (gameDirectory != null
|
||||
&& Objects.equals(txtGameDirectoryName.getText(), gameDirectoryDisplayName)
|
||||
&& Objects.equals(getLocation(), initialLocation)
|
||||
&& toggleUseRelativePath.isSelected() == initialUseRelativePath),
|
||||
txtGameDirectoryName.textProperty(), location, toggleUseRelativePath.selectedProperty()));
|
||||
}
|
||||
|
||||
ChangeListener<String> locationChangeListener = (observable, oldValue, newValue) -> {
|
||||
Path newPath;
|
||||
try {
|
||||
newPath = FileUtils.toAbsolute(Path.of(newValue));
|
||||
} catch (InvalidPathException ignored) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!".minecraft".equals(FileUtils.getName(newPath)))
|
||||
return;
|
||||
|
||||
Path parent = newPath.getParent();
|
||||
if (parent == null)
|
||||
return;
|
||||
|
||||
String suggestedName = FileUtils.getName(parent);
|
||||
if (!suggestedName.isBlank()) {
|
||||
txtGameDirectoryName.setText(suggestedName);
|
||||
}
|
||||
};
|
||||
locationChangeListener.changed(null, this.location.get(), this.location.get());
|
||||
locationProperty().addListener(locationChangeListener);
|
||||
|
||||
txtGameDirectoryName.textProperty().addListener(new ChangeListener<>() {
|
||||
@Override
|
||||
public void changed(ObservableValue<? extends String> observable, String oldValue, String newValue) {
|
||||
if (txtGameDirectoryName.isFocused()) {
|
||||
txtGameDirectoryName.textProperty().removeListener(this);
|
||||
locationProperty().removeListener(locationChangeListener);
|
||||
if (gameDirectory == null) {
|
||||
ChangeListener<String> locationChangeListener = (observable, oldValue, newValue) -> {
|
||||
Path newPath;
|
||||
try {
|
||||
newPath = FileUtils.toAbsolute(Path.of(newValue));
|
||||
} catch (InvalidPathException ignored) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
if (!".minecraft".equals(FileUtils.getName(newPath)))
|
||||
return;
|
||||
|
||||
@Nullable Path parent = newPath.getParent();
|
||||
if (parent == null)
|
||||
return;
|
||||
|
||||
String suggestedName = FileUtils.getName(parent);
|
||||
if (!suggestedName.isBlank()) {
|
||||
txtGameDirectoryName.setText(suggestedName);
|
||||
}
|
||||
};
|
||||
locationChangeListener.changed(locationProperty(), this.location.get(), this.location.get());
|
||||
locationProperty().addListener(locationChangeListener);
|
||||
|
||||
txtGameDirectoryName.textProperty().addListener(new ChangeListener<>() {
|
||||
/// Stops automatic name suggestions after the user edits the name.
|
||||
@Override
|
||||
public void changed(ObservableValue<? extends String> observable, String oldValue, String newValue) {
|
||||
if (txtGameDirectoryName.isFocused()) {
|
||||
txtGameDirectoryName.textProperty().removeListener(this);
|
||||
locationProperty().removeListener(locationChangeListener);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@FXThread
|
||||
|
||||
@@ -20,11 +20,18 @@ package org.jackhuang.hmcl.ui.download;
|
||||
import javafx.application.Platform;
|
||||
import javafx.beans.property.BooleanProperty;
|
||||
import javafx.beans.property.SimpleBooleanProperty;
|
||||
import javafx.collections.FXCollections;
|
||||
import javafx.collections.ObservableList;
|
||||
import javafx.collections.ObservableSet;
|
||||
import javafx.collections.transformation.FilteredList;
|
||||
import javafx.stage.FileChooser;
|
||||
import org.jackhuang.hmcl.game.HMCLGameRepository;
|
||||
import org.jackhuang.hmcl.game.ManuallyCreatedModpackException;
|
||||
import org.jackhuang.hmcl.game.ModpackHelper;
|
||||
import org.jackhuang.hmcl.modpack.Modpack;
|
||||
import org.jackhuang.hmcl.modpack.ModpackFile;
|
||||
import org.jackhuang.hmcl.modpack.ModpackManifest;
|
||||
import org.jackhuang.hmcl.setting.DownloadProviders;
|
||||
import org.jackhuang.hmcl.setting.GameDirectoryManager;
|
||||
import org.jackhuang.hmcl.task.Schedulers;
|
||||
import org.jackhuang.hmcl.task.Task;
|
||||
@@ -39,9 +46,15 @@ import org.jackhuang.hmcl.util.SettingsMap;
|
||||
import org.jackhuang.hmcl.util.StringUtils;
|
||||
import org.jackhuang.hmcl.util.io.CompressingUtils;
|
||||
import org.jackhuang.hmcl.util.io.FileUtils;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import java.nio.charset.Charset;
|
||||
import java.nio.file.Path;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
import java.util.Set;
|
||||
|
||||
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
|
||||
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
|
||||
@@ -51,10 +64,22 @@ public final class LocalModpackPage extends ModpackPage {
|
||||
private final BooleanProperty installAsVersion = new SimpleBooleanProperty(true);
|
||||
private Modpack manifest = null;
|
||||
private Charset charset;
|
||||
private final ObservableList<ModpackFile> allFiles = FXCollections.observableList(new ArrayList<>());
|
||||
private final ObservableSet<String> excludedFiles = FXCollections.observableSet(new HashSet<>());
|
||||
private final BooleanProperty loadingOptionalFiles = new SimpleBooleanProperty(false);
|
||||
private final BooleanProperty loadedOptionalFiles = new SimpleBooleanProperty(true);
|
||||
|
||||
public LocalModpackPage(WizardController controller) {
|
||||
super(controller);
|
||||
|
||||
btnOptionalFiles.setOnAction(ev -> controller.onNext(new OptionalFilesPage(
|
||||
this::onInstall,
|
||||
this::loadOptionalFiles,
|
||||
loadingOptionalFiles,
|
||||
loadedOptionalFiles,
|
||||
new FilteredList<>(allFiles, ModpackFile::optional),
|
||||
excludedFiles)));
|
||||
|
||||
HMCLGameRepository repository = controller.getSettings().get(ModpackPage.REPOSITORY);
|
||||
|
||||
String name = controller.getSettings().get(MODPACK_NAME);
|
||||
@@ -103,8 +128,7 @@ public final class LocalModpackPage extends ModpackPage {
|
||||
Task.supplyAsync(() -> CompressingUtils.findSuitableEncoding(selectedFile))
|
||||
.thenApplyAsync(encoding -> {
|
||||
charset = encoding;
|
||||
manifest = ModpackHelper.readModpackManifest(selectedFile, encoding);
|
||||
return manifest;
|
||||
return ModpackHelper.readModpackManifest(selectedFile, encoding);
|
||||
})
|
||||
.whenComplete(Schedulers.javafx(), (manifest, exception) -> {
|
||||
if (exception instanceof ManuallyCreatedModpackException) {
|
||||
@@ -126,7 +150,11 @@ public final class LocalModpackPage extends ModpackPage {
|
||||
LOG.warning("Failed to read modpack manifest", exception);
|
||||
Controllers.dialog(i18n("modpack.task.install.error"), i18n("message.error"), MessageDialogPane.MessageType.ERROR);
|
||||
Platform.runLater(controller::onEnd);
|
||||
} else if (manifest == null) {
|
||||
Controllers.dialog(i18n("modpack.task.install.error"), i18n("message.error"), MessageDialogPane.MessageType.ERROR);
|
||||
Platform.runLater(controller::onEnd);
|
||||
} else {
|
||||
this.manifest = manifest;
|
||||
hideSpinner();
|
||||
controller.getSettings().put(MODPACK_MANIFEST, manifest);
|
||||
nameProperty.set(manifest.getName());
|
||||
@@ -139,6 +167,33 @@ public final class LocalModpackPage extends ModpackPage {
|
||||
}
|
||||
|
||||
btnDescription.setVisible(StringUtils.isNotBlank(manifest.getDescription()));
|
||||
|
||||
if (manifest.getManifest() instanceof ModpackManifest.SupportOptional supportOptional) {
|
||||
allFiles.setAll(supportOptional.getFiles());
|
||||
if (allFiles.stream().anyMatch(ModpackFile::optional)) {
|
||||
loadOptionalFiles();
|
||||
btnOptionalFiles.setVisible(true);
|
||||
btnOptionalFiles.setManaged(true);
|
||||
}
|
||||
}
|
||||
}
|
||||
}).start();
|
||||
}
|
||||
|
||||
private void loadOptionalFiles() {
|
||||
Objects.requireNonNull(manifest);
|
||||
loadingOptionalFiles.set(true);
|
||||
loadedOptionalFiles.set(false);
|
||||
Task.supplyAsync(() -> manifest.getManifest().getProvider().loadFiles(DownloadProviders.getDownloadProvider(), manifest.getManifest()))
|
||||
.whenComplete(Schedulers.javafx(), (manifest1, exception) -> {
|
||||
loadingOptionalFiles.set(false);
|
||||
List<? extends ModpackFile> files = ((ModpackManifest.SupportOptional) manifest1).getFiles();
|
||||
manifest.setManifest(manifest1);
|
||||
allFiles.setAll(files);
|
||||
if (files.stream().anyMatch(s -> s.optional() && (!s.addonQueried() || s.fileName() == null))) {
|
||||
LOG.warning("Failed to load optional files");
|
||||
} else {
|
||||
loadedOptionalFiles.set(true);
|
||||
}
|
||||
}).start();
|
||||
}
|
||||
@@ -157,21 +212,28 @@ public final class LocalModpackPage extends ModpackPage {
|
||||
i18n("install.name.invalid"),
|
||||
i18n("message.warning"),
|
||||
MessageDialogPane.MessageType.QUESTION)
|
||||
.yesOrNo(() -> {
|
||||
controller.getSettings().put(MODPACK_NAME, name);
|
||||
controller.getSettings().put(MODPACK_CHARSET, charset);
|
||||
controller.onFinish();
|
||||
}, () -> {
|
||||
.yesOrNo(() -> finishInstall(name), () -> {
|
||||
// The user selects Cancel and does nothing.
|
||||
})
|
||||
.build());
|
||||
} else {
|
||||
controller.getSettings().put(MODPACK_NAME, name);
|
||||
controller.getSettings().put(MODPACK_CHARSET, charset);
|
||||
controller.onFinish();
|
||||
finishInstall(name);
|
||||
}
|
||||
}
|
||||
|
||||
private void finishInstall(String name) {
|
||||
controller.getSettings().put(MODPACK_NAME, name);
|
||||
controller.getSettings().put(MODPACK_CHARSET, charset);
|
||||
controller.getSettings().put(MODPACK_EXCLUDED_FILES, getExcludedFiles());
|
||||
controller.onFinish();
|
||||
}
|
||||
|
||||
private @Nullable Set<String> getExcludedFiles() {
|
||||
if (allFiles.isEmpty())
|
||||
return null;
|
||||
return Set.copyOf(excludedFiles);
|
||||
}
|
||||
|
||||
protected void onDescribe() {
|
||||
if (manifest != null)
|
||||
Controllers.navigate(new WebPage(i18n("modpack.description"), manifest.getDescription()));
|
||||
@@ -183,4 +245,5 @@ public final class LocalModpackPage extends ModpackPage {
|
||||
public static final SettingsMap.Key<Charset> MODPACK_CHARSET = new SettingsMap.Key<>("MODPACK_CHARSET");
|
||||
public static final SettingsMap.Key<Boolean> MODPACK_MANUALLY_CREATED = new SettingsMap.Key<>("MODPACK_MANUALLY_CREATED");
|
||||
public static final SettingsMap.Key<String> MODPACK_ICON_URL = new SettingsMap.Key<>("MODPACK_ICON_URL");
|
||||
public static final SettingsMap.Key<Set<String>> MODPACK_EXCLUDED_FILES = new SettingsMap.Key<>("MODPACK_EXCLUDED_FILES");
|
||||
}
|
||||
@@ -90,6 +90,7 @@ public final class ModpackInstallWizardProvider implements WizardProvider {
|
||||
String iconUrl = settings.get(LocalModpackPage.MODPACK_ICON_URL);
|
||||
Charset charset = settings.get(LocalModpackPage.MODPACK_CHARSET);
|
||||
boolean isManuallyCreated = settings.getOrDefault(LocalModpackPage.MODPACK_MANUALLY_CREATED, false);
|
||||
var excludedFiles = settings.get(LocalModpackPage.MODPACK_EXCLUDED_FILES);
|
||||
|
||||
if (isManuallyCreated) {
|
||||
return ModpackHelper.getInstallManuallyCreatedModpackTask(selected, name, charset);
|
||||
@@ -108,7 +109,7 @@ public final class ModpackInstallWizardProvider implements WizardProvider {
|
||||
if (serverModpackManifest != null) {
|
||||
return ModpackHelper.getUpdateTask(repository, serverModpackManifest, modpack.getEncoding(), instanceId, ModpackHelper.readModpackConfiguration(repository.getLayout().getModpackConfigurationFile(instanceId)));
|
||||
} else {
|
||||
return ModpackHelper.getUpdateTask(repository, selected, modpack.getEncoding(), instanceId, ModpackHelper.readModpackConfiguration(repository.getLayout().getModpackConfigurationFile(instanceId)));
|
||||
return ModpackHelper.getUpdateTask(repository, selected, modpack.getEncoding(), instanceId, ModpackHelper.readModpackConfiguration(repository.getLayout().getModpackConfigurationFile(instanceId)), excludedFiles);
|
||||
}
|
||||
} catch (UnsupportedModpackException | ManuallyCreatedModpackException e) {
|
||||
Controllers.dialog(i18n("modpack.unsupported"), i18n("message.error"), MessageType.ERROR);
|
||||
@@ -123,7 +124,7 @@ public final class ModpackInstallWizardProvider implements WizardProvider {
|
||||
return ModpackHelper.getInstallTask(repository, serverModpackManifest, instanceId, modpack)
|
||||
.thenRunAsync(Schedulers.javafx(), () -> repository.setSelectedInstance(repository.getInstance(instanceId)));
|
||||
} else {
|
||||
return ModpackHelper.getInstallTask(repository, selected, instanceId, modpack, iconUrl)
|
||||
return ModpackHelper.getInstallTask(repository, selected, instanceId, modpack, iconUrl, excludedFiles)
|
||||
.thenRunAsync(Schedulers.javafx(), () -> repository.setSelectedInstance(repository.getInstance(instanceId)));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,6 +22,7 @@ import com.jfoenix.controls.JFXTextField;
|
||||
import javafx.beans.property.StringProperty;
|
||||
import javafx.geometry.Pos;
|
||||
import javafx.scene.layout.BorderPane;
|
||||
import javafx.scene.layout.HBox;
|
||||
import javafx.scene.layout.VBox;
|
||||
import org.jackhuang.hmcl.game.HMCLGameRepository;
|
||||
import org.jackhuang.hmcl.setting.GameDirectory;
|
||||
@@ -49,6 +50,7 @@ public abstract class ModpackPage extends SpinnerPane implements WizardPage {
|
||||
protected final JFXTextField txtModpackName;
|
||||
protected final JFXButton btnInstall;
|
||||
protected final JFXButton btnDescription;
|
||||
protected final JFXButton btnOptionalFiles;
|
||||
|
||||
protected ModpackPage(WizardController controller) {
|
||||
this.controller = controller;
|
||||
@@ -66,7 +68,6 @@ public abstract class ModpackPage extends SpinnerPane implements WizardPage {
|
||||
|
||||
txtModpackName = new JFXTextField();
|
||||
txtModpackName.setPrefWidth(300);
|
||||
// BorderPane.setMargin(txtModpackName, new Insets(0, 0, 8, 32));
|
||||
BorderPane.setAlignment(txtModpackName, Pos.CENTER_RIGHT);
|
||||
archiveNamePane.setRight(txtModpackName);
|
||||
}
|
||||
@@ -95,10 +96,19 @@ public abstract class ModpackPage extends SpinnerPane implements WizardPage {
|
||||
btnDescription.setOnAction(e -> onDescribe());
|
||||
descriptionPane.setLeft(btnDescription);
|
||||
|
||||
var installHBox = new HBox(8);
|
||||
btnOptionalFiles = FXUtils.newRaisedButton(i18n("modpack.optional_files"));
|
||||
btnOptionalFiles.setVisible(false);
|
||||
btnOptionalFiles.setManaged(false);
|
||||
installHBox.getChildren().add(btnOptionalFiles);
|
||||
|
||||
btnInstall = FXUtils.newRaisedButton(i18n("button.install"));
|
||||
btnInstall.setOnAction(e -> onInstall());
|
||||
btnInstall.disableProperty().bind(createBooleanBinding(() -> !txtModpackName.validate(), txtModpackName.textProperty()));
|
||||
descriptionPane.setRight(btnInstall);
|
||||
var nameInvalid = createBooleanBinding(() -> !txtModpackName.validate(), txtModpackName.textProperty());
|
||||
btnInstall.disableProperty().bind(nameInvalid);
|
||||
btnOptionalFiles.disableProperty().bind(nameInvalid);
|
||||
installHBox.getChildren().add(btnInstall);
|
||||
descriptionPane.setRight(installHBox);
|
||||
}
|
||||
|
||||
componentList.getContent().setAll(
|
||||
|
||||
@@ -0,0 +1,322 @@
|
||||
/*
|
||||
* 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.ui.download;
|
||||
|
||||
import com.jfoenix.controls.JFXButton;
|
||||
import com.jfoenix.controls.JFXCheckBox;
|
||||
import com.jfoenix.controls.JFXDialogLayout;
|
||||
import com.jfoenix.controls.JFXListView;
|
||||
import javafx.beans.binding.Bindings;
|
||||
import javafx.beans.property.BooleanProperty;
|
||||
import javafx.beans.property.SimpleBooleanProperty;
|
||||
import javafx.beans.value.ObservableBooleanValue;
|
||||
import javafx.collections.ObservableList;
|
||||
import javafx.collections.ObservableSet;
|
||||
import javafx.collections.SetChangeListener;
|
||||
import javafx.geometry.Insets;
|
||||
import javafx.geometry.Pos;
|
||||
import javafx.scene.control.Label;
|
||||
import javafx.scene.image.ImageView;
|
||||
import javafx.scene.layout.BorderPane;
|
||||
import javafx.scene.layout.HBox;
|
||||
import javafx.scene.layout.Priority;
|
||||
import javafx.scene.layout.Region;
|
||||
import javafx.scene.layout.VBox;
|
||||
import org.jackhuang.hmcl.addon.RemoteAddon;
|
||||
import org.jackhuang.hmcl.modpack.ModpackFile;
|
||||
import org.jackhuang.hmcl.task.Schedulers;
|
||||
import org.jackhuang.hmcl.ui.Controllers;
|
||||
import org.jackhuang.hmcl.ui.FXUtils;
|
||||
import org.jackhuang.hmcl.ui.SVG;
|
||||
import org.jackhuang.hmcl.ui.construct.ComponentList;
|
||||
import org.jackhuang.hmcl.ui.construct.DialogCloseEvent;
|
||||
import org.jackhuang.hmcl.ui.construct.JFXHyperlink;
|
||||
import org.jackhuang.hmcl.ui.construct.MDListCell;
|
||||
import org.jackhuang.hmcl.ui.construct.SpinnerPane;
|
||||
import org.jackhuang.hmcl.ui.construct.TwoLineListItem;
|
||||
import org.jackhuang.hmcl.ui.wizard.WizardPage;
|
||||
import org.jackhuang.hmcl.util.SettingsMap;
|
||||
import org.jetbrains.annotations.NotNullByDefault;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
|
||||
import static org.jackhuang.hmcl.ui.FXUtils.onEscPressed;
|
||||
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
|
||||
|
||||
/// Wizard page that lets the user choose which optional modpack files to install.
|
||||
@NotNullByDefault
|
||||
public class OptionalFilesPage extends SpinnerPane implements WizardPage {
|
||||
|
||||
/// Fixed row height used to size [#body] to its items.
|
||||
private static final double CELL_HEIGHT = 48;
|
||||
|
||||
/// Maximum list viewport height before scrolling.
|
||||
private static final double MAX_LIST_HEIGHT = 320;
|
||||
|
||||
/// Keys of files the user unchecked; empty means all optional files will be installed.
|
||||
private final ObservableSet<String> excludedFiles;
|
||||
|
||||
/// Per-file selected state mirrored from [#excludedFiles] (`true` means install / not excluded).
|
||||
private final Map<String, BooleanProperty> selectedByKey = new HashMap<>();
|
||||
|
||||
/// Whether [#selectedByKey] is being updated from an [#excludedFiles] set change.
|
||||
private boolean updatingFromSet;
|
||||
|
||||
/// List view showing optional files.
|
||||
private final JFXListView<ModpackFile> body = new JFXListView<>();
|
||||
|
||||
/// Creates the optional-files selection page.
|
||||
///
|
||||
/// @param install callback that finishes the install wizard
|
||||
/// @param retry callback that reloads optional-file metadata
|
||||
/// @param loading whether remote metadata is still loading
|
||||
/// @param successful whether remote metadata loaded successfully
|
||||
/// @param optionalFiles optional files to display
|
||||
/// @param excludedFiles mutable set of unchecked optional file keys
|
||||
public OptionalFilesPage(
|
||||
Runnable install,
|
||||
Runnable retry,
|
||||
ObservableBooleanValue loading,
|
||||
ObservableBooleanValue successful,
|
||||
ObservableList<ModpackFile> optionalFiles,
|
||||
ObservableSet<String> excludedFiles) {
|
||||
this.excludedFiles = excludedFiles;
|
||||
excludedFiles.addListener((SetChangeListener<String>) change -> {
|
||||
updatingFromSet = true;
|
||||
try {
|
||||
if (change.wasRemoved()) {
|
||||
BooleanProperty selected = selectedByKey.get(change.getElementRemoved());
|
||||
if (selected != null)
|
||||
selected.set(true);
|
||||
}
|
||||
if (change.wasAdded()) {
|
||||
BooleanProperty selected = selectedByKey.get(change.getElementAdded());
|
||||
if (selected != null)
|
||||
selected.set(false);
|
||||
}
|
||||
} finally {
|
||||
updatingFromSet = false;
|
||||
}
|
||||
});
|
||||
|
||||
VBox borderPane = new VBox();
|
||||
borderPane.setAlignment(Pos.CENTER);
|
||||
borderPane.setMaxHeight(Region.USE_PREF_SIZE);
|
||||
FXUtils.setLimitWidth(borderPane, 500);
|
||||
|
||||
ComponentList componentList = new ComponentList();
|
||||
|
||||
Label lblRetry = new Label(i18n("modpack.retry_optional_files"));
|
||||
lblRetry.setOnMouseClicked(e -> retry.run());
|
||||
|
||||
VBox tail = new VBox();
|
||||
{
|
||||
var descPane = new BorderPane();
|
||||
HBox selectionButtons = new HBox(8);
|
||||
JFXButton btnSelectAll = FXUtils.newBorderButton(i18n("button.select_all"));
|
||||
btnSelectAll.setOnAction(e -> excludedFiles.clear());
|
||||
JFXButton btnClear = FXUtils.newBorderButton(i18n("button.clear"));
|
||||
btnClear.setOnAction(e -> {
|
||||
excludedFiles.clear();
|
||||
for (ModpackFile file : optionalFiles) {
|
||||
excludedFiles.add(file.key());
|
||||
}
|
||||
});
|
||||
selectionButtons.getChildren().setAll(btnSelectAll, btnClear);
|
||||
descPane.setLeft(selectionButtons);
|
||||
|
||||
var btnInstall = FXUtils.newRaisedButton(i18n("button.install"));
|
||||
descPane.setRight(btnInstall);
|
||||
btnInstall.setOnAction(e -> install.run());
|
||||
tail.getChildren().add(descPane);
|
||||
}
|
||||
|
||||
// ListView defaults to a large preferred height (~400) regardless of item count, which
|
||||
// leaves empty viewport space when few optional files exist. Size exactly to content —
|
||||
// do not add extra pixels, or a blank strip appears under the last row.
|
||||
body.setFixedCellSize(CELL_HEIGHT);
|
||||
body.prefHeightProperty().bind(Bindings.createDoubleBinding(
|
||||
() -> Math.min(MAX_LIST_HEIGHT, optionalFiles.size() * CELL_HEIGHT + 2),
|
||||
optionalFiles));
|
||||
body.setMaxHeight(Region.USE_PREF_SIZE);
|
||||
body.setCellFactory(it -> new OptionalFileEntry(body));
|
||||
body.setItems(optionalFiles);
|
||||
|
||||
Runnable refreshContent = () -> {
|
||||
if (successful.get()) {
|
||||
componentList.getContent().setAll(body, tail);
|
||||
} else {
|
||||
componentList.getContent().setAll(lblRetry, body, tail);
|
||||
}
|
||||
};
|
||||
refreshContent.run();
|
||||
successful.addListener((obs, oldVal, newVal) -> refreshContent.run());
|
||||
|
||||
borderPane.getChildren().setAll(componentList);
|
||||
setContent(borderPane);
|
||||
loadingProperty().bind(loading);
|
||||
}
|
||||
|
||||
/// Returns the selected property for `key`, creating and wiring it on first use.
|
||||
///
|
||||
/// @param key the file exclusion key
|
||||
/// @return a property that is `true` when the file is selected for install
|
||||
private BooleanProperty selectedProperty(String key) {
|
||||
return selectedByKey.computeIfAbsent(key, k -> {
|
||||
BooleanProperty selected = new SimpleBooleanProperty(!excludedFiles.contains(k));
|
||||
selected.addListener((obs, wasSelected, isSelected) -> {
|
||||
if (updatingFromSet)
|
||||
return;
|
||||
if (isSelected) {
|
||||
excludedFiles.remove(k);
|
||||
} else {
|
||||
excludedFiles.add(k);
|
||||
}
|
||||
});
|
||||
return selected;
|
||||
});
|
||||
}
|
||||
|
||||
/// List cell for one optional file with checkbox and detail button.
|
||||
private final class OptionalFileEntry extends MDListCell<ModpackFile> {
|
||||
private final JFXCheckBox checkBox = new JFXCheckBox();
|
||||
private final TwoLineListItem content = new TwoLineListItem();
|
||||
private final JFXButton infoButton = new JFXButton();
|
||||
private final HBox container = new HBox(8);
|
||||
|
||||
/// Currently bidirectionally bound selected property, or `null` when unbound.
|
||||
private @Nullable BooleanProperty boundSelected = null;
|
||||
|
||||
/// Creates a cell bound to `listView`.
|
||||
///
|
||||
/// @param listView the owning list view
|
||||
public OptionalFileEntry(JFXListView<ModpackFile> listView) {
|
||||
super(listView);
|
||||
container.setPickOnBounds(false);
|
||||
container.setAlignment(Pos.CENTER_LEFT);
|
||||
HBox.setHgrow(content, Priority.ALWAYS);
|
||||
content.setMouseTransparent(true);
|
||||
setSelectable();
|
||||
container.getChildren().setAll(checkBox, content);
|
||||
|
||||
infoButton.getStyleClass().add("toggle-icon4");
|
||||
infoButton.setGraphic(SVG.INFO.createIcon());
|
||||
container.getChildren().add(infoButton);
|
||||
getContainer().getChildren().setAll(container);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void updateControl(ModpackFile item, boolean empty) {
|
||||
if (empty) {
|
||||
if (boundSelected != null) {
|
||||
checkBox.selectedProperty().unbindBidirectional(boundSelected);
|
||||
boundSelected = null;
|
||||
}
|
||||
return;
|
||||
}
|
||||
String name = item.fileName();
|
||||
if (name != null) {
|
||||
content.setTitle(name);
|
||||
} else {
|
||||
content.setTitle(i18n("modpack.unknown_optional_file"));
|
||||
}
|
||||
RemoteAddon addon = item.remoteAddon();
|
||||
if (addon != null) {
|
||||
content.setSubtitle(addon.title());
|
||||
infoButton.setOnMouseClicked(e -> Controllers.dialog(new ModInfo(addon)));
|
||||
infoButton.setManaged(true);
|
||||
infoButton.setVisible(true);
|
||||
} else {
|
||||
content.setSubtitle("");
|
||||
infoButton.setOnMouseClicked(null);
|
||||
infoButton.setManaged(false);
|
||||
infoButton.setVisible(false);
|
||||
}
|
||||
BooleanProperty selected = OptionalFilesPage.this.selectedProperty(item.key());
|
||||
if (boundSelected != selected) {
|
||||
if (boundSelected != null)
|
||||
checkBox.selectedProperty().unbindBidirectional(boundSelected);
|
||||
checkBox.selectedProperty().bindBidirectional(selected);
|
||||
boundSelected = selected;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Dialog showing remote addon details for an optional file.
|
||||
private static final class ModInfo extends JFXDialogLayout {
|
||||
/// Creates a detail dialog for `addon`.
|
||||
///
|
||||
/// @param addon the remote addon
|
||||
public ModInfo(RemoteAddon addon) {
|
||||
HBox container = new HBox(8);
|
||||
SpinnerPane spinnerPane = new SpinnerPane();
|
||||
ImageView imageView = new ImageView();
|
||||
imageView.setFitHeight(32);
|
||||
imageView.setFitWidth(32);
|
||||
spinnerPane.setContent(imageView);
|
||||
spinnerPane.setPrefSize(32, 32);
|
||||
spinnerPane.setLoading(true);
|
||||
CompletableFuture.supplyAsync(() -> {
|
||||
try {
|
||||
return FXUtils.getRemoteImageTask(addon.iconUrl(), 32, 32, true, true).run();
|
||||
} catch (Exception e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}, Schedulers.io()).thenAcceptAsync((image) -> {
|
||||
imageView.setImage(image);
|
||||
spinnerPane.setLoading(false);
|
||||
}, Schedulers.javafx());
|
||||
container.getChildren().add(spinnerPane);
|
||||
|
||||
TwoLineListItem title = new TwoLineListItem();
|
||||
title.setTitle(addon.title());
|
||||
title.setSubtitle(addon.author());
|
||||
container.getChildren().add(title);
|
||||
setHeading(container);
|
||||
|
||||
Label description = new Label(addon.description());
|
||||
description.setWrapText(true);
|
||||
description.setPadding(new Insets(8, 0, 0, 0));
|
||||
setBody(description);
|
||||
|
||||
JFXHyperlink pageButton = new JFXHyperlink(i18n("mods.url"));
|
||||
pageButton.setOnAction(e -> FXUtils.openLink(addon.pageUrl()));
|
||||
getActions().add(pageButton);
|
||||
|
||||
JFXButton okButton = new JFXButton();
|
||||
okButton.getStyleClass().add("dialog-accept");
|
||||
okButton.setText(i18n("button.ok"));
|
||||
okButton.setOnAction(e -> fireEvent(new DialogCloseEvent()));
|
||||
getActions().add(okButton);
|
||||
|
||||
onEscPressed(this, okButton::fire);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getTitle() {
|
||||
return i18n("modpack.optional_files");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void cleanup(SettingsMap settings) {
|
||||
}
|
||||
}
|
||||
@@ -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) {
|
||||
}
|
||||
}
|
||||
@@ -21,7 +21,39 @@ import org.jackhuang.hmcl.addon.AddonLoader;
|
||||
import org.jackhuang.hmcl.addon.AddonLoaderType;
|
||||
import org.jackhuang.hmcl.addon.mod.ModLoaderType;
|
||||
import org.jackhuang.hmcl.download.ComponentRemoteVersion;
|
||||
import org.jackhuang.hmcl.download.cleanroom.CleanroomInstallTask;
|
||||
import org.jackhuang.hmcl.download.fabric.FabricAPIInstallTask;
|
||||
import org.jackhuang.hmcl.download.fabric.FabricInstallTask;
|
||||
import org.jackhuang.hmcl.download.forge.ForgeNewInstallTask;
|
||||
import org.jackhuang.hmcl.download.forge.ForgeOldInstallTask;
|
||||
import org.jackhuang.hmcl.download.game.GameAssetDownloadTask;
|
||||
import org.jackhuang.hmcl.download.game.GameInstallTask;
|
||||
import org.jackhuang.hmcl.download.game.GameRemoteVersion;
|
||||
import org.jackhuang.hmcl.download.java.mojang.MojangJavaDownloadTask;
|
||||
import org.jackhuang.hmcl.download.legacyfabric.LegacyFabricInstallTask;
|
||||
import org.jackhuang.hmcl.download.liteloader.LiteLoaderInstallTask;
|
||||
import org.jackhuang.hmcl.download.neoforge.NeoForgeInstallTask;
|
||||
import org.jackhuang.hmcl.download.neoforge.NeoForgeOldInstallTask;
|
||||
import org.jackhuang.hmcl.download.optifine.OptiFineInstallTask;
|
||||
import org.jackhuang.hmcl.download.quilt.QuiltAPIInstallTask;
|
||||
import org.jackhuang.hmcl.download.quilt.QuiltInstallTask;
|
||||
import org.jackhuang.hmcl.game.HMCLModpackInstallTask;
|
||||
import org.jackhuang.hmcl.java.JavaInstallTask;
|
||||
import org.jackhuang.hmcl.modpack.MinecraftInstanceTask;
|
||||
import org.jackhuang.hmcl.modpack.ModpackInstallTask;
|
||||
import org.jackhuang.hmcl.modpack.ModpackUpdateTask;
|
||||
import org.jackhuang.hmcl.modpack.curse.CurseCompletionTask;
|
||||
import org.jackhuang.hmcl.modpack.curse.CurseInstallTask;
|
||||
import org.jackhuang.hmcl.modpack.mcbbs.McbbsModpackCompletionTask;
|
||||
import org.jackhuang.hmcl.modpack.mcbbs.McbbsModpackExportTask;
|
||||
import org.jackhuang.hmcl.modpack.modrinth.ModrinthCompletionTask;
|
||||
import org.jackhuang.hmcl.modpack.modrinth.ModrinthInstallTask;
|
||||
import org.jackhuang.hmcl.modpack.modrinth.ModrinthModpackExportTask;
|
||||
import org.jackhuang.hmcl.modpack.multimc.MultiMCModpackInstallTask;
|
||||
import org.jackhuang.hmcl.modpack.server.ServerModpackCompletionTask;
|
||||
import org.jackhuang.hmcl.modpack.server.ServerModpackExportTask;
|
||||
import org.jackhuang.hmcl.modpack.server.ServerModpackLocalInstallTask;
|
||||
import org.jackhuang.hmcl.task.Task;
|
||||
import org.jackhuang.hmcl.util.StringUtils;
|
||||
import org.jackhuang.hmcl.util.i18n.translator.Translator;
|
||||
import org.jackhuang.hmcl.util.versioning.GameVersionNumber;
|
||||
@@ -152,6 +184,60 @@ public final class I18n {
|
||||
: StringUtils.capitalizeWords(loader.name());
|
||||
}
|
||||
|
||||
public static @Nullable String translateTaskName(Task<?> task) {
|
||||
if (task instanceof GameAssetDownloadTask) {
|
||||
return i18n("assets.download_all");
|
||||
} else if (task instanceof GameInstallTask) {
|
||||
return i18n("install.installer.install", i18n("install.installer.game"));
|
||||
} else if (task instanceof CleanroomInstallTask) {
|
||||
return i18n("install.installer.install", i18n("install.installer.cleanroom"));
|
||||
} else if (task instanceof LegacyFabricInstallTask) {
|
||||
return i18n("install.installer.install", i18n("install.installer.legacyfabric"));
|
||||
} else if (task instanceof ForgeNewInstallTask || task instanceof ForgeOldInstallTask) {
|
||||
return i18n("install.installer.install", i18n("install.installer.forge"));
|
||||
} else if (task instanceof NeoForgeInstallTask || task instanceof NeoForgeOldInstallTask) {
|
||||
return i18n("install.installer.install", i18n("install.installer.neoforge"));
|
||||
} else if (task instanceof LiteLoaderInstallTask) {
|
||||
return i18n("install.installer.install", i18n("install.installer.liteloader"));
|
||||
} else if (task instanceof OptiFineInstallTask) {
|
||||
return i18n("install.installer.install", i18n("install.installer.optifine"));
|
||||
} else if (task instanceof FabricInstallTask) {
|
||||
return i18n("install.installer.install", i18n("install.installer.fabric"));
|
||||
} else if (task instanceof FabricAPIInstallTask) {
|
||||
return i18n("install.installer.install", i18n("install.installer.fabric-api"));
|
||||
} else if (task instanceof QuiltInstallTask) {
|
||||
return i18n("install.installer.install", i18n("install.installer.quilt"));
|
||||
} else if (task instanceof QuiltAPIInstallTask) {
|
||||
return i18n("install.installer.install", i18n("install.installer.quilt-api"));
|
||||
} else if (task instanceof CurseCompletionTask || task instanceof ModrinthCompletionTask || task instanceof ServerModpackCompletionTask || task instanceof McbbsModpackCompletionTask) {
|
||||
return i18n("modpack.completion");
|
||||
} else if (task instanceof ModpackInstallTask) {
|
||||
return i18n("modpack.installing");
|
||||
} else if (task instanceof ModpackUpdateTask) {
|
||||
return i18n("modpack.update");
|
||||
} else if (task instanceof CurseInstallTask) {
|
||||
return i18n("modpack.installing.given", i18n("modpack.type.curse"));
|
||||
} else if (task instanceof MultiMCModpackInstallTask) {
|
||||
return i18n("modpack.installing.given", i18n("modpack.type.multimc"));
|
||||
} else if (task instanceof ModrinthInstallTask) {
|
||||
return i18n("modpack.installing.given", i18n("modpack.type.modrinth"));
|
||||
} else if (task instanceof ServerModpackLocalInstallTask) {
|
||||
return i18n("install.installing") + ": " + i18n("modpack.type.server");
|
||||
} else if (task instanceof HMCLModpackInstallTask) {
|
||||
return i18n("modpack.installing.given", "HMCL");
|
||||
} else if (task instanceof McbbsModpackExportTask || task instanceof ServerModpackExportTask || task instanceof ModrinthModpackExportTask) {
|
||||
return i18n("modpack.export");
|
||||
} else if (task instanceof MinecraftInstanceTask) {
|
||||
return i18n("modpack.scan");
|
||||
} else if (task instanceof MojangJavaDownloadTask) {
|
||||
return i18n("download.java");
|
||||
} else if (task instanceof JavaInstallTask) {
|
||||
return i18n("java.installing");
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public static boolean hasKey(String key) {
|
||||
return getResourceBundle().containsKey(key);
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
@@ -1093,6 +1102,10 @@
|
||||
-fx-border-width: 0 0 1 0;
|
||||
}
|
||||
|
||||
.md-list-cell:last {
|
||||
-fx-border-width: 0;
|
||||
}
|
||||
|
||||
.md-list-cell:selected {
|
||||
-fx-background-color: -monet-secondary-container;
|
||||
}
|
||||
|
||||
@@ -1023,6 +1023,9 @@ modpack.installing=Installing modpack
|
||||
modpack.installing.given=Installing %s modpack
|
||||
modpack.invalid=Invalid modpack, you can try downloading it again.
|
||||
modpack.mismatched_type=Modpack type mismatched, the current instance is a(n) %1$s type, but the provided one is %2$s type.
|
||||
modpack.optional_files=Optional Files
|
||||
modpack.retry_optional_files=Failed to load optional files. Click here to retry.
|
||||
modpack.unknown_optional_file=Unknown file
|
||||
modpack.name=Modpack Name
|
||||
modpack.origin=Source
|
||||
modpack.origin.url=Official Website
|
||||
@@ -1330,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
|
||||
@@ -1346,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)
|
||||
|
||||
|
||||
@@ -1024,6 +1024,9 @@ modpack.installing=جارٍ تثبيت حزمة المودات
|
||||
modpack.installing.given=جارٍ تثبيت حزمة المودات %s
|
||||
modpack.invalid=حزمة مودات غير صالحة، يمكنك محاولة تنزيلها مجدداً.
|
||||
modpack.mismatched_type=نوع حزمة المودات غير متطابق، النسخة الحالية من نوع %1$s، لكن الحزمة المقدمة من نوع %2$s.
|
||||
modpack.optional_files=ملفات اختيارية
|
||||
modpack.retry_optional_files=فشل تحميل الملفات الاختيارية. انقر لإعادة المحاولة.
|
||||
modpack.unknown_optional_file=ملف غير معروف
|
||||
modpack.name=اسم حزمة المودات
|
||||
modpack.origin=المصدر
|
||||
modpack.origin.url=الموقع الرسمي
|
||||
@@ -1330,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=فشل إنشاء المجلد
|
||||
@@ -1346,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 عنصر فرعي
|
||||
|
||||
|
||||
@@ -1023,6 +1023,9 @@ modpack.installing=Modpack wird installiert
|
||||
modpack.installing.given=Installiere %s Modpack
|
||||
modpack.invalid=Ungültiges Modpack, Sie können versuchen, es erneut herunterzuladen.
|
||||
modpack.mismatched_type=Modpack-Typ stimmt nicht überein, die aktuelle Instanz ist ein(e) %1$s Typ, aber der bereitgestellte Typ ist %2$s.
|
||||
modpack.optional_files=Optionale Dateien
|
||||
modpack.retry_optional_files=Optionale Dateien konnten nicht geladen werden. Hier klicken, um es erneut zu versuchen.
|
||||
modpack.unknown_optional_file=Unbekannte Datei
|
||||
modpack.name=Modpack-Name
|
||||
modpack.origin=Quelle
|
||||
modpack.origin.url=Offizielle Website
|
||||
@@ -1329,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
|
||||
@@ -1345,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)
|
||||
|
||||
|
||||
@@ -1016,6 +1016,9 @@ modpack.installing=Instalando modpack
|
||||
modpack.installing.given=Instalando %s modpack
|
||||
modpack.invalid=Modpack inválido, puede intentar volver a descargarlo.
|
||||
modpack.mismatched_type=Tipo de modpack erróneo, la instancia actual es del tipo %s, pero la proporcionada es del tipo %s.
|
||||
modpack.optional_files=Archivos opcionales
|
||||
modpack.retry_optional_files=No se pudieron cargar los archivos opcionales. Haz clic para reintentar.
|
||||
modpack.unknown_optional_file=Archivo desconocido
|
||||
modpack.name=Nombre del modpack
|
||||
modpack.origin=Fuente
|
||||
modpack.origin.url=Sitio web oficial
|
||||
@@ -1322,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
|
||||
@@ -1338,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)
|
||||
|
||||
|
||||
@@ -826,6 +826,9 @@ modpack.installing=modpackのインストール
|
||||
# modpack.installing.given=
|
||||
modpack.invalid=無効なmodpackファイル。
|
||||
modpack.mismatched_type=不適切なmodpackタイプ、現在のゲームは %s modpackですが、更新ファイルは %s modpackです。
|
||||
modpack.optional_files=オプションファイル
|
||||
modpack.retry_optional_files=オプションファイルの読み込みに失敗しました。クリックして再試行してください。
|
||||
modpack.unknown_optional_file=不明なファイル
|
||||
modpack.name=Modpack名
|
||||
modpack.origin=Origin
|
||||
modpack.origin.url=公式ウェブサイト
|
||||
@@ -1126,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=
|
||||
@@ -1142,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=
|
||||
|
||||
|
||||
@@ -838,6 +838,9 @@ modpack.installing=裝改囊集
|
||||
modpack.installing.given=裝 %s 改囊集
|
||||
modpack.invalid=無效之改囊集新之案。殆引之誤也。
|
||||
modpack.mismatched_type=不合改囊集之類。夫戲爲「%s」改囊集,顧所供改囊集之案爲「%s」改囊集。\n君可求助於右上之鈕。
|
||||
modpack.optional_files=可選之檔
|
||||
modpack.retry_optional_files=可選檔載入敗,點此再試
|
||||
modpack.unknown_optional_file=未知之檔
|
||||
modpack.name=改囊集名
|
||||
modpack.origin=源
|
||||
modpack.origin.url=官網
|
||||
@@ -1138,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=增案夾未成
|
||||
@@ -1154,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 子项
|
||||
|
||||
|
||||
File diff suppressed because it is too large.
Load diff
@@ -978,6 +978,9 @@ modpack.installing=Встановлення модпака
|
||||
modpack.installing.given=Встановлення модпака %s
|
||||
modpack.invalid=Недійсний модпак, ви можете спробувати завантажити його знову.
|
||||
modpack.mismatched_type=Тип модпака не відповідає, поточний екземпляр має тип %s, але наданий має тип %s.
|
||||
modpack.optional_files=Необов’язкові файли
|
||||
modpack.retry_optional_files=Не вдалося завантажити необов’язкові файли. Натисніть, щоб повторити.
|
||||
modpack.unknown_optional_file=Невідомий файл
|
||||
modpack.name=Назва модпака
|
||||
modpack.origin=Джерело
|
||||
modpack.origin.url=Офіційний сайт
|
||||
@@ -1285,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=Не вдалося створити каталог
|
||||
@@ -1301,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 підпунктів
|
||||
|
||||
|
||||
@@ -841,6 +841,9 @@ modpack.installing=安装整合包
|
||||
modpack.installing.given=安装 %s 整合包
|
||||
modpack.invalid=无效的整合包升级文件。可能是下载时出现问题。
|
||||
modpack.mismatched_type=整合包类型不匹配。当前游戏为“%s”整合包,但是提供的整合包更新文件为“%s”整合包。\n如遇到问题,你可以点击右上角帮助按钮进行求助。
|
||||
modpack.optional_files=可选文件
|
||||
modpack.retry_optional_files=可选文件加载失败,点击重试
|
||||
modpack.unknown_optional_file=未知文件
|
||||
modpack.name=整合包名称
|
||||
modpack.origin=来源
|
||||
modpack.origin.url=官方网站
|
||||
@@ -1142,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=创建文件夹失败
|
||||
@@ -1158,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 个子项
|
||||
|
||||
|
||||
@@ -835,6 +835,9 @@ modpack.installing=安裝模組包
|
||||
modpack.installing.given=安裝 %s 模組包
|
||||
modpack.invalid=無效的模組包升級檔案。可能是下載時出現問題。
|
||||
modpack.mismatched_type=模組包類型不符。目前遊戲是「%s」模組包,但是提供的模組包更新檔案是「%s」模組包。
|
||||
modpack.optional_files=可選檔案
|
||||
modpack.retry_optional_files=可選檔案載入失敗,點擊重試
|
||||
modpack.unknown_optional_file=未知檔案
|
||||
modpack.name=模組包名稱
|
||||
modpack.origin=來源
|
||||
modpack.origin.url=官方網站
|
||||
@@ -1136,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=建立目錄失敗
|
||||
@@ -1152,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 個子項
|
||||
|
||||
|
||||
@@ -1045,6 +1045,138 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"org.lwjgl:lwjgl:3.4.3:natives-linux": {
|
||||
"name": "org.lwjgl:lwjgl:3.4.3:natives-linux-arm64",
|
||||
"downloads": {
|
||||
"artifact": {
|
||||
"path": "org/lwjgl/lwjgl/3.4.3/lwjgl-3.4.3-natives-linux-arm64.jar",
|
||||
"url": "https://repo1.maven.org/maven2/org/lwjgl/lwjgl/3.4.3/lwjgl-3.4.3-natives-linux-arm64.jar",
|
||||
"sha1": "101c753b129dbdeed565798d259b2b9544ef3878",
|
||||
"size": 126343
|
||||
}
|
||||
}
|
||||
},
|
||||
"org.lwjgl:lwjgl-jemalloc:3.4.3:natives-linux": {
|
||||
"name": "org.lwjgl:lwjgl-jemalloc:3.4.3:natives-linux-arm64",
|
||||
"downloads": {
|
||||
"artifact": {
|
||||
"path": "org/lwjgl/lwjgl-jemalloc/3.4.3/lwjgl-jemalloc-3.4.3-natives-linux-arm64.jar",
|
||||
"url": "https://repo1.maven.org/maven2/org/lwjgl/lwjgl-jemalloc/3.4.3/lwjgl-jemalloc-3.4.3-natives-linux-arm64.jar",
|
||||
"sha1": "b558d38d8eaa32831bb66835aae163428fe69646",
|
||||
"size": 235928
|
||||
}
|
||||
}
|
||||
},
|
||||
"org.lwjgl:lwjgl-openal:3.4.3:natives-linux": {
|
||||
"name": "org.lwjgl:lwjgl-openal:3.4.3:natives-linux-arm64",
|
||||
"downloads": {
|
||||
"artifact": {
|
||||
"path": "org/lwjgl/lwjgl-openal/3.4.3/lwjgl-openal-3.4.3-natives-linux-arm64.jar",
|
||||
"url": "https://repo1.maven.org/maven2/org/lwjgl/lwjgl-openal/3.4.3/lwjgl-openal-3.4.3-natives-linux-arm64.jar",
|
||||
"sha1": "f2083556a7d8a02e4abeb2b933642250ef06c61d",
|
||||
"size": 866036
|
||||
}
|
||||
}
|
||||
},
|
||||
"org.lwjgl:lwjgl-opengl:3.4.3:natives-linux": {
|
||||
"name": "org.lwjgl:lwjgl-opengl:3.4.3:natives-linux-arm64",
|
||||
"downloads": {
|
||||
"artifact": {
|
||||
"path": "org/lwjgl/lwjgl-opengl/3.4.3/lwjgl-opengl-3.4.3-natives-linux-arm64.jar",
|
||||
"url": "https://repo1.maven.org/maven2/org/lwjgl/lwjgl-opengl/3.4.3/lwjgl-opengl-3.4.3-natives-linux-arm64.jar",
|
||||
"sha1": "84f229d793388be57ae5555b0cfee09b5990fa20",
|
||||
"size": 80850
|
||||
}
|
||||
}
|
||||
},
|
||||
"org.lwjgl:lwjgl-glfw:3.4.3:natives-linux": {
|
||||
"name": "org.lwjgl:lwjgl-glfw:3.4.3:natives-linux-arm64",
|
||||
"downloads": {
|
||||
"artifact": {
|
||||
"path": "org/lwjgl/lwjgl-glfw/3.4.3/lwjgl-glfw-3.4.3-natives-linux-arm64.jar",
|
||||
"url": "https://repo1.maven.org/maven2/org/lwjgl/lwjgl-glfw/3.4.3/lwjgl-glfw-3.4.3-natives-linux-arm64.jar",
|
||||
"sha1": "e2f84903e71f7f2c5fe11a0f6de9576da270c503",
|
||||
"size": 144552
|
||||
}
|
||||
}
|
||||
},
|
||||
"org.lwjgl:lwjgl-stb:3.4.3:natives-linux": {
|
||||
"name": "org.lwjgl:lwjgl-stb:3.4.3:natives-linux-arm64",
|
||||
"downloads": {
|
||||
"artifact": {
|
||||
"path": "org/lwjgl/lwjgl-stb/3.4.3/lwjgl-stb-3.4.3-natives-linux-arm64.jar",
|
||||
"url": "https://repo1.maven.org/maven2/org/lwjgl/lwjgl-stb/3.4.3/lwjgl-stb-3.4.3-natives-linux-arm64.jar",
|
||||
"sha1": "74f1cd22ccf4fd7432b982372b8a34c93cd9aacf",
|
||||
"size": 258996
|
||||
}
|
||||
}
|
||||
},
|
||||
"org.lwjgl:lwjgl-tinyfd:3.4.3:natives-linux": {
|
||||
"name": "org.lwjgl:lwjgl-tinyfd:3.4.3:natives-linux-arm64",
|
||||
"downloads": {
|
||||
"artifact": {
|
||||
"path": "org/lwjgl/lwjgl-tinyfd/3.4.3/lwjgl-tinyfd-3.4.3-natives-linux-arm64.jar",
|
||||
"url": "https://repo1.maven.org/maven2/org/lwjgl/lwjgl-tinyfd/3.4.3/lwjgl-tinyfd-3.4.3-natives-linux-arm64.jar",
|
||||
"sha1": "97296e394d3d7dfebcc68754ab8273d44d8e9602",
|
||||
"size": 45345
|
||||
}
|
||||
}
|
||||
},
|
||||
"org.lwjgl:lwjgl-freetype:3.4.3:natives-linux": {
|
||||
"name": "org.lwjgl:lwjgl-freetype:3.4.3:natives-linux-arm64",
|
||||
"downloads": {
|
||||
"artifact": {
|
||||
"path": "org/lwjgl/lwjgl-freetype/3.4.3/lwjgl-freetype-3.4.3-natives-linux-arm64.jar",
|
||||
"url": "https://repo1.maven.org/maven2/org/lwjgl/lwjgl-freetype/3.4.3/lwjgl-freetype-3.4.3-natives-linux-arm64.jar",
|
||||
"sha1": "6babd5ddf9b4850e91a952d494cfd22b895ce200",
|
||||
"size": 1300898
|
||||
}
|
||||
}
|
||||
},
|
||||
"org.lwjgl:lwjgl-shaderc:3.4.3:natives-linux": {
|
||||
"name": "org.lwjgl:lwjgl-shaderc:3.4.3:natives-linux-arm64",
|
||||
"downloads": {
|
||||
"artifact": {
|
||||
"path": "org/lwjgl/lwjgl-shaderc/3.4.3/lwjgl-shaderc-3.4.3-natives-linux-arm64.jar",
|
||||
"url": "https://repo1.maven.org/maven2/org/lwjgl/lwjgl-shaderc/3.4.3/lwjgl-shaderc-3.4.3-natives-linux-arm64.jar",
|
||||
"sha1": "f17eb9b12086809a31e61734dabc1dea80f6b7b2",
|
||||
"size": 3540777
|
||||
}
|
||||
}
|
||||
},
|
||||
"org.lwjgl:lwjgl-spvc:3.4.3:natives-linux": {
|
||||
"name": "org.lwjgl:lwjgl-spvc:3.4.3:natives-linux-arm64",
|
||||
"downloads": {
|
||||
"artifact": {
|
||||
"path": "org/lwjgl/lwjgl-spvc/3.4.3/lwjgl-spvc-3.4.3-natives-linux-arm64.jar",
|
||||
"url": "https://repo1.maven.org/maven2/org/lwjgl/lwjgl-spvc/3.4.3/lwjgl-spvc-3.4.3-natives-linux-arm64.jar",
|
||||
"sha1": "85d2636eb1368a3864fb10faec02a4269020b31e",
|
||||
"size": 1166740
|
||||
}
|
||||
}
|
||||
},
|
||||
"org.lwjgl:lwjgl-vma:3.4.3:natives-linux": {
|
||||
"name": "org.lwjgl:lwjgl-vma:3.4.3:natives-linux-arm64",
|
||||
"downloads": {
|
||||
"artifact": {
|
||||
"path": "org/lwjgl/lwjgl-vma/3.4.3/lwjgl-vma-3.4.3-natives-linux-arm64.jar",
|
||||
"url": "https://repo1.maven.org/maven2/org/lwjgl/lwjgl-vma/3.4.3/lwjgl-vma-3.4.3-natives-linux-arm64.jar",
|
||||
"sha1": "0a9e20e7d8ae4d0106cd23ae58a1b72ca151c49f",
|
||||
"size": 60863
|
||||
}
|
||||
}
|
||||
},
|
||||
"org.lwjgl:lwjgl-sdl:3.4.3:natives-linux": {
|
||||
"name": "org.lwjgl:lwjgl-sdl:3.4.3:natives-linux-arm64",
|
||||
"downloads": {
|
||||
"artifact": {
|
||||
"path": "org/lwjgl/lwjgl-sdl/3.4.3/lwjgl-sdl-3.4.3-natives-linux-arm64.jar",
|
||||
"url": "https://repo1.maven.org/maven2/org/lwjgl/lwjgl-sdl/3.4.3/lwjgl-sdl-3.4.3-natives-linux-arm64.jar",
|
||||
"sha1": "577e41b6d0006f3d3f841c14abe19736e695a760",
|
||||
"size": 1460461
|
||||
}
|
||||
}
|
||||
},
|
||||
"org.lwjgl.lwjgl:lwjgl-platform:2.9.0:natives": {
|
||||
"name": "org.glavo.hmcl:lwjgl2-natives:2.9.3",
|
||||
"downloads": {
|
||||
@@ -2956,6 +3088,181 @@
|
||||
}
|
||||
},
|
||||
"org.lwjgl:lwjgl-sdl:3.4.2:natives-linux": null,
|
||||
"org.lwjgl:lwjgl:3.4.3": {
|
||||
"name": "org.lwjgl:lwjgl:3.4.1",
|
||||
"downloads": {
|
||||
"artifact": {
|
||||
"path": "org/lwjgl/lwjgl/3.4.1/lwjgl-3.4.1.jar",
|
||||
"url": "https://repo1.maven.org/maven2/org/lwjgl/lwjgl/3.4.1/lwjgl-3.4.1.jar",
|
||||
"sha1": "6105690714874b995aa7812e4f7732a21109276c",
|
||||
"size": 1157957
|
||||
}
|
||||
}
|
||||
},
|
||||
"org.lwjgl:lwjgl:3.4.3:natives-linux": {
|
||||
"name": "org.glavo.hmcl:lwjgl3-natives:3.4.1-rc1",
|
||||
"downloads": {
|
||||
"classifiers": {
|
||||
"linux-loongarch64": {
|
||||
"path": "org/glavo/hmcl/lwjgl3-natives/3.4.1-rc1/lwjgl3-natives-3.4.1-rc1-linux-loongarch64.jar",
|
||||
"url": "https://repo1.maven.org/maven2/org/glavo/hmcl/lwjgl3-natives/3.4.1-rc1-linux-loongarch64/lwjgl3-natives-3.4.1-rc1-linux-loongarch64.jar",
|
||||
"sha1": "3fed872fc2f9929d2be25389dd312dfb48e9f89e",
|
||||
"size": 22854580
|
||||
}
|
||||
}
|
||||
},
|
||||
"extract": {
|
||||
"exclude": [
|
||||
"META-INF/"
|
||||
]
|
||||
},
|
||||
"natives": {
|
||||
"linux": "linux-loongarch64"
|
||||
}
|
||||
},
|
||||
"org.lwjgl:lwjgl-jemalloc:3.4.3": {
|
||||
"name": "org.lwjgl:lwjgl-jemalloc:3.4.1",
|
||||
"downloads": {
|
||||
"artifact": {
|
||||
"path": "org/lwjgl/lwjgl-jemalloc/3.4.1/lwjgl-jemalloc-3.4.1.jar",
|
||||
"url": "https://repo1.maven.org/maven2/org/lwjgl/lwjgl-jemalloc/3.4.1/lwjgl-jemalloc-3.4.1.jar",
|
||||
"sha1": "16b663092854fc6adfe0a941e61cae2bb2e437b6",
|
||||
"size": 47872
|
||||
}
|
||||
}
|
||||
},
|
||||
"org.lwjgl:lwjgl-jemalloc:3.4.3:natives-linux": null,
|
||||
"org.lwjgl:lwjgl-openal:3.4.3": {
|
||||
"name": "org.lwjgl:lwjgl-openal:3.4.1",
|
||||
"downloads": {
|
||||
"artifact": {
|
||||
"path": "org/lwjgl/lwjgl-openal/3.4.1/lwjgl-openal-3.4.1.jar",
|
||||
"url": "https://repo1.maven.org/maven2/org/lwjgl/lwjgl-openal/3.4.1/lwjgl-openal-3.4.1.jar",
|
||||
"sha1": "8508bd60589de0539aa465b78fcf838efd07ae9a",
|
||||
"size": 153919
|
||||
}
|
||||
}
|
||||
},
|
||||
"org.lwjgl:lwjgl-openal:3.4.3:natives-linux": null,
|
||||
"org.lwjgl:lwjgl-opengl:3.4.3": {
|
||||
"name": "org.lwjgl:lwjgl-opengl:3.4.1",
|
||||
"downloads": {
|
||||
"artifact": {
|
||||
"path": "org/lwjgl/lwjgl-opengl/3.4.1/lwjgl-opengl-3.4.1.jar",
|
||||
"url": "https://repo1.maven.org/maven2/org/lwjgl/lwjgl-opengl/3.4.1/lwjgl-opengl-3.4.1.jar",
|
||||
"sha1": "830412cab823c029cda6b9729f9d2ed36cf79959",
|
||||
"size": 937660
|
||||
}
|
||||
}
|
||||
},
|
||||
"org.lwjgl:lwjgl-opengl:3.4.3:natives-linux": null,
|
||||
"org.lwjgl:lwjgl-glfw:3.4.3": {
|
||||
"name": "org.lwjgl:lwjgl-glfw:3.4.1",
|
||||
"downloads": {
|
||||
"artifact": {
|
||||
"path": "org/lwjgl/lwjgl-glfw/3.4.1/lwjgl-glfw-3.4.1.jar",
|
||||
"url": "https://repo1.maven.org/maven2/org/lwjgl/lwjgl-glfw/3.4.1/lwjgl-glfw-3.4.1.jar",
|
||||
"sha1": "a782b1ddd175c9107bf300fd92920579ea65e2a4",
|
||||
"size": 151893
|
||||
}
|
||||
}
|
||||
},
|
||||
"org.lwjgl:lwjgl-glfw:3.4.3:natives-linux": null,
|
||||
"org.lwjgl:lwjgl-stb:3.4.3": {
|
||||
"name": "org.lwjgl:lwjgl-stb:3.4.1",
|
||||
"downloads": {
|
||||
"artifact": {
|
||||
"path": "org/lwjgl/lwjgl-stb/3.4.1/lwjgl-stb-3.4.1.jar",
|
||||
"url": "https://repo1.maven.org/maven2/org/lwjgl/lwjgl-stb/3.4.1/lwjgl-stb-3.4.1.jar",
|
||||
"sha1": "f077c3dafc31924fbe8acb0b913f08977ba27f8e",
|
||||
"size": 142927
|
||||
}
|
||||
}
|
||||
},
|
||||
"org.lwjgl:lwjgl-stb:3.4.3:natives-linux": null,
|
||||
"org.lwjgl:lwjgl-tinyfd:3.4.3": {
|
||||
"name": "org.lwjgl:lwjgl-tinyfd:3.4.1",
|
||||
"downloads": {
|
||||
"artifact": {
|
||||
"path": "org/lwjgl/lwjgl-tinyfd/3.4.1/lwjgl-tinyfd-3.4.1.jar",
|
||||
"url": "https://repo1.maven.org/maven2/org/lwjgl/lwjgl-tinyfd/3.4.1/lwjgl-tinyfd-3.4.1.jar",
|
||||
"sha1": "562697d40dbd2ed0424bf24dcf65d51170f81adf",
|
||||
"size": 15931
|
||||
}
|
||||
}
|
||||
},
|
||||
"org.lwjgl:lwjgl-tinyfd:3.4.3:natives-linux": null,
|
||||
"org.lwjgl:lwjgl-freetype:3.4.3": {
|
||||
"name": "org.lwjgl:lwjgl-freetype:3.4.1",
|
||||
"downloads": {
|
||||
"artifact": {
|
||||
"path": "org/lwjgl/lwjgl-freetype/3.4.1/lwjgl-freetype-3.4.1.jar",
|
||||
"url": "https://repo1.maven.org/maven2/org/lwjgl/lwjgl-freetype/3.4.1/lwjgl-freetype-3.4.1.jar",
|
||||
"sha1": "caa75a37e857efc9b0686e434d708d6420c5b6ea",
|
||||
"size": 464342
|
||||
}
|
||||
}
|
||||
},
|
||||
"org.lwjgl:lwjgl-freetype:3.4.3:natives-linux": null,
|
||||
"org.lwjgl:lwjgl-shaderc:3.4.3": {
|
||||
"name": "org.lwjgl:lwjgl-shaderc:3.4.1",
|
||||
"downloads": {
|
||||
"artifact": {
|
||||
"path": "org/lwjgl/lwjgl-shaderc/3.4.1/lwjgl-shaderc-3.4.1.jar",
|
||||
"url": "https://repo1.maven.org/maven2/org/lwjgl/lwjgl-shaderc/3.4.1/lwjgl-shaderc-3.4.1.jar",
|
||||
"sha1": "5c02c88f0d879529014c78463b76f43fdecfd272",
|
||||
"size": 153051
|
||||
}
|
||||
}
|
||||
},
|
||||
"org.lwjgl:lwjgl-shaderc:3.4.3:natives-linux": null,
|
||||
"org.lwjgl:lwjgl-spvc:3.4.3": {
|
||||
"name": "org.lwjgl:lwjgl-spvc:3.4.1",
|
||||
"downloads": {
|
||||
"artifact": {
|
||||
"path": "org/lwjgl/lwjgl-spvc/3.4.1/lwjgl-spvc-3.4.1.jar",
|
||||
"url": "https://repo1.maven.org/maven2/org/lwjgl/lwjgl-spvc/3.4.1/lwjgl-spvc-3.4.1.jar",
|
||||
"sha1": "188553592e97b5ab2dba54a1b403ee80b48dad4c",
|
||||
"size": 148285
|
||||
}
|
||||
}
|
||||
},
|
||||
"org.lwjgl:lwjgl-spvc:3.4.3:natives-linux": null,
|
||||
"org.lwjgl:lwjgl-vma:3.4.3": {
|
||||
"name": "org.lwjgl:lwjgl-vma:3.4.1",
|
||||
"downloads": {
|
||||
"artifact": {
|
||||
"path": "org/lwjgl/lwjgl-vma/3.4.1/lwjgl-vma-3.4.1.jar",
|
||||
"url": "https://repo1.maven.org/maven2/org/lwjgl/lwjgl-vma/3.4.1/lwjgl-vma-3.4.1.jar",
|
||||
"sha1": "2c54ce10154feb1ede4d23c5947438cdbfdcc003",
|
||||
"size": 109209
|
||||
}
|
||||
}
|
||||
},
|
||||
"org.lwjgl:lwjgl-vma:3.4.3:natives-linux": null,
|
||||
"org.lwjgl:lwjgl-vulkan:3.4.3": {
|
||||
"name": "org.lwjgl:lwjgl-vulkan:3.4.1",
|
||||
"downloads": {
|
||||
"artifact": {
|
||||
"path": "org/lwjgl/lwjgl-vulkan/3.4.1/lwjgl-vulkan-3.4.1.jar",
|
||||
"url": "https://repo1.maven.org/maven2/org/lwjgl/lwjgl-vulkan/3.4.1/lwjgl-vulkan-3.4.1.jar",
|
||||
"sha1": "d0601fdc38890755e5638048c14cc0f7b990964b",
|
||||
"size": 8356700
|
||||
}
|
||||
}
|
||||
},
|
||||
"org.lwjgl:lwjgl-sdl:3.4.3": {
|
||||
"name": "org.lwjgl:lwjgl-sdl:3.4.1",
|
||||
"downloads": {
|
||||
"artifact": {
|
||||
"path": "org/lwjgl/lwjgl-sdl/3.4.1/lwjgl-sdl-3.4.1.jar",
|
||||
"url": "https://repo1.maven.org/maven2/org/lwjgl/lwjgl-sdl/3.4.1/lwjgl-sdl-3.4.1.jar",
|
||||
"sha1": "ec28db6e949478044c649fea25eae43b1b161971",
|
||||
"size": 983498
|
||||
}
|
||||
}
|
||||
},
|
||||
"org.lwjgl:lwjgl-sdl:3.4.3:natives-linux": null,
|
||||
"net.java.dev.jna:jna:5.8.0": {
|
||||
"name": "net.java.dev.jna:jna:5.13.0",
|
||||
"downloads": {
|
||||
@@ -4857,6 +5164,138 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"org.lwjgl:lwjgl:3.4.3:natives-linux": {
|
||||
"name": "org.lwjgl:lwjgl:3.4.3:natives-linux-riscv64",
|
||||
"downloads": {
|
||||
"artifact": {
|
||||
"path": "org/lwjgl/lwjgl/3.4.3/lwjgl-3.4.3-natives-linux-riscv64.jar",
|
||||
"url": "https://repo1.maven.org/maven2/org/lwjgl/lwjgl/3.4.3/lwjgl-3.4.3-natives-linux-riscv64.jar",
|
||||
"sha1": "38374c04f498a9b214f7cb28cfd3fcc9737b8065",
|
||||
"size": 127129
|
||||
}
|
||||
}
|
||||
},
|
||||
"org.lwjgl:lwjgl-jemalloc:3.4.3:natives-linux": {
|
||||
"name": "org.lwjgl:lwjgl-jemalloc:3.4.3:natives-linux-riscv64",
|
||||
"downloads": {
|
||||
"artifact": {
|
||||
"path": "org/lwjgl/lwjgl-jemalloc/3.4.3/lwjgl-jemalloc-3.4.3-natives-linux-riscv64.jar",
|
||||
"url": "https://repo1.maven.org/maven2/org/lwjgl/lwjgl-jemalloc/3.4.3/lwjgl-jemalloc-3.4.3-natives-linux-riscv64.jar",
|
||||
"sha1": "79b62b3efdcdba28d2f89e6a7d99d88cf3cda48f",
|
||||
"size": 248363
|
||||
}
|
||||
}
|
||||
},
|
||||
"org.lwjgl:lwjgl-openal:3.4.3:natives-linux": {
|
||||
"name": "org.lwjgl:lwjgl-openal:3.4.3:natives-linux-riscv64",
|
||||
"downloads": {
|
||||
"artifact": {
|
||||
"path": "org/lwjgl/lwjgl-openal/3.4.3/lwjgl-openal-3.4.3-natives-linux-riscv64.jar",
|
||||
"url": "https://repo1.maven.org/maven2/org/lwjgl/lwjgl-openal/3.4.3/lwjgl-openal-3.4.3-natives-linux-riscv64.jar",
|
||||
"sha1": "f4d3d1050f244b3a7fe9ddc02e94fbe0c507fc4d",
|
||||
"size": 938685
|
||||
}
|
||||
}
|
||||
},
|
||||
"org.lwjgl:lwjgl-opengl:3.4.3:natives-linux": {
|
||||
"name": "org.lwjgl:lwjgl-opengl:3.4.3:natives-linux-riscv64",
|
||||
"downloads": {
|
||||
"artifact": {
|
||||
"path": "org/lwjgl/lwjgl-opengl/3.4.3/lwjgl-opengl-3.4.3-natives-linux-riscv64.jar",
|
||||
"url": "https://repo1.maven.org/maven2/org/lwjgl/lwjgl-opengl/3.4.3/lwjgl-opengl-3.4.3-natives-linux-riscv64.jar",
|
||||
"sha1": "56ecca3a33c7966f3d678737757f5feb3efd03c2",
|
||||
"size": 79430
|
||||
}
|
||||
}
|
||||
},
|
||||
"org.lwjgl:lwjgl-glfw:3.4.3:natives-linux": {
|
||||
"name": "org.lwjgl:lwjgl-glfw:3.4.3:natives-linux-riscv64",
|
||||
"downloads": {
|
||||
"artifact": {
|
||||
"path": "org/lwjgl/lwjgl-glfw/3.4.3/lwjgl-glfw-3.4.3-natives-linux-riscv64.jar",
|
||||
"url": "https://repo1.maven.org/maven2/org/lwjgl/lwjgl-glfw/3.4.3/lwjgl-glfw-3.4.3-natives-linux-riscv64.jar",
|
||||
"sha1": "7f88d98db5c2fdd33063b5810df8a9809fd0983b",
|
||||
"size": 141071
|
||||
}
|
||||
}
|
||||
},
|
||||
"org.lwjgl:lwjgl-stb:3.4.3:natives-linux": {
|
||||
"name": "org.lwjgl:lwjgl-stb:3.4.3:natives-linux-riscv64",
|
||||
"downloads": {
|
||||
"artifact": {
|
||||
"path": "org/lwjgl/lwjgl-stb/3.4.3/lwjgl-stb-3.4.3-natives-linux-riscv64.jar",
|
||||
"url": "https://repo1.maven.org/maven2/org/lwjgl/lwjgl-stb/3.4.3/lwjgl-stb-3.4.3-natives-linux-riscv64.jar",
|
||||
"sha1": "8f0aeb06e1d532fd9411d08dcff6c0c3572d71a6",
|
||||
"size": 268102
|
||||
}
|
||||
}
|
||||
},
|
||||
"org.lwjgl:lwjgl-tinyfd:3.4.3:natives-linux": {
|
||||
"name": "org.lwjgl:lwjgl-tinyfd:3.4.3:natives-linux-riscv64",
|
||||
"downloads": {
|
||||
"artifact": {
|
||||
"path": "org/lwjgl/lwjgl-tinyfd/3.4.3/lwjgl-tinyfd-3.4.3-natives-linux-riscv64.jar",
|
||||
"url": "https://repo1.maven.org/maven2/org/lwjgl/lwjgl-tinyfd/3.4.3/lwjgl-tinyfd-3.4.3-natives-linux-riscv64.jar",
|
||||
"sha1": "f9b6c895de7f43caf50c12b8d30fa3aef13e82ff",
|
||||
"size": 56940
|
||||
}
|
||||
}
|
||||
},
|
||||
"org.lwjgl:lwjgl-freetype:3.4.3:natives-linux": {
|
||||
"name": "org.lwjgl:lwjgl-freetype:3.4.3:natives-linux-riscv64",
|
||||
"downloads": {
|
||||
"artifact": {
|
||||
"path": "org/lwjgl/lwjgl-freetype/3.4.3/lwjgl-freetype-3.4.3-natives-linux-riscv64.jar",
|
||||
"url": "https://repo1.maven.org/maven2/org/lwjgl/lwjgl-freetype/3.4.3/lwjgl-freetype-3.4.3-natives-linux-riscv64.jar",
|
||||
"sha1": "809c6df123ae9ea36aa3117a368e7d300ed712eb",
|
||||
"size": 1409111
|
||||
}
|
||||
}
|
||||
},
|
||||
"org.lwjgl:lwjgl-shaderc:3.4.3:natives-linux": {
|
||||
"name": "org.lwjgl:lwjgl-shaderc:3.4.3:natives-linux-riscv64",
|
||||
"downloads": {
|
||||
"artifact": {
|
||||
"path": "org/lwjgl/lwjgl-shaderc/3.4.3/lwjgl-shaderc-3.4.3-natives-linux-riscv64.jar",
|
||||
"url": "https://repo1.maven.org/maven2/org/lwjgl/lwjgl-shaderc/3.4.3/lwjgl-shaderc-3.4.3-natives-linux-riscv64.jar",
|
||||
"sha1": "1cb0fc92286da27d5782140a8ba72af5458eb1cf",
|
||||
"size": 3702544
|
||||
}
|
||||
}
|
||||
},
|
||||
"org.lwjgl:lwjgl-spvc:3.4.3:natives-linux": {
|
||||
"name": "org.lwjgl:lwjgl-spvc:3.4.3:natives-linux-riscv64",
|
||||
"downloads": {
|
||||
"artifact": {
|
||||
"path": "org/lwjgl/lwjgl-spvc/3.4.3/lwjgl-spvc-3.4.3-natives-linux-riscv64.jar",
|
||||
"url": "https://repo1.maven.org/maven2/org/lwjgl/lwjgl-spvc/3.4.3/lwjgl-spvc-3.4.3-natives-linux-riscv64.jar",
|
||||
"sha1": "38610faedc2a7c44f02c0bc7246ef1f8357a2846",
|
||||
"size": 1221331
|
||||
}
|
||||
}
|
||||
},
|
||||
"org.lwjgl:lwjgl-vma:3.4.3:natives-linux": {
|
||||
"name": "org.lwjgl:lwjgl-vma:3.4.3:natives-linux-riscv64",
|
||||
"downloads": {
|
||||
"artifact": {
|
||||
"path": "org/lwjgl/lwjgl-vma/3.4.3/lwjgl-vma-3.4.3-natives-linux-riscv64.jar",
|
||||
"url": "https://repo1.maven.org/maven2/org/lwjgl/lwjgl-vma/3.4.3/lwjgl-vma-3.4.3-natives-linux-riscv64.jar",
|
||||
"sha1": "e4dc73c846d42eb63c39c2b30d7cb5983a59bdbe",
|
||||
"size": 66532
|
||||
}
|
||||
}
|
||||
},
|
||||
"org.lwjgl:lwjgl-sdl:3.4.3:natives-linux": {
|
||||
"name": "org.lwjgl:lwjgl-sdl:3.4.3:natives-linux-riscv64",
|
||||
"downloads": {
|
||||
"artifact": {
|
||||
"path": "org/lwjgl/lwjgl-sdl/3.4.3/lwjgl-sdl-3.4.3-natives-linux-riscv64.jar",
|
||||
"url": "https://repo1.maven.org/maven2/org/lwjgl/lwjgl-sdl/3.4.3/lwjgl-sdl-3.4.3-natives-linux-riscv64.jar",
|
||||
"sha1": "30cd5ef9665905be2c0c7325dc848296d4788b49",
|
||||
"size": 1496337
|
||||
}
|
||||
}
|
||||
},
|
||||
"com.github.oshi:oshi-core:6.6.5": {
|
||||
"name": "com.github.oshi:oshi-core:6.8.0",
|
||||
"downloads": {
|
||||
@@ -7054,6 +7493,138 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"org.lwjgl:lwjgl:3.4.3:natives-linux": {
|
||||
"name": "org.lwjgl:lwjgl:3.4.3:natives-freebsd",
|
||||
"downloads": {
|
||||
"artifact": {
|
||||
"path": "org/lwjgl/lwjgl/3.4.3/lwjgl-3.4.3-natives-freebsd.jar",
|
||||
"url": "https://repo1.maven.org/maven2/org/lwjgl/lwjgl/3.4.3/lwjgl-3.4.3-natives-freebsd.jar",
|
||||
"sha1": "fbab1de5df7db20be4c74bf6cf14750f599e6d79",
|
||||
"size": 110742
|
||||
}
|
||||
}
|
||||
},
|
||||
"org.lwjgl:lwjgl-jemalloc:3.4.3:natives-linux": {
|
||||
"name": "org.lwjgl:lwjgl-jemalloc:3.4.3:natives-freebsd",
|
||||
"downloads": {
|
||||
"artifact": {
|
||||
"path": "org/lwjgl/lwjgl-jemalloc/3.4.3/lwjgl-jemalloc-3.4.3-natives-freebsd.jar",
|
||||
"url": "https://repo1.maven.org/maven2/org/lwjgl/lwjgl-jemalloc/3.4.3/lwjgl-jemalloc-3.4.3-natives-freebsd.jar",
|
||||
"sha1": "30a905bf255a0eac294da13cdfddb2963bda4541",
|
||||
"size": 160297
|
||||
}
|
||||
}
|
||||
},
|
||||
"org.lwjgl:lwjgl-openal:3.4.3:natives-linux": {
|
||||
"name": "org.lwjgl:lwjgl-openal:3.4.3:natives-freebsd",
|
||||
"downloads": {
|
||||
"artifact": {
|
||||
"path": "org/lwjgl/lwjgl-openal/3.4.3/lwjgl-openal-3.4.3-natives-freebsd.jar",
|
||||
"url": "https://repo1.maven.org/maven2/org/lwjgl/lwjgl-openal/3.4.3/lwjgl-openal-3.4.3-natives-freebsd.jar",
|
||||
"sha1": "6f72cdd56726eacaaa1e4f5824f9fc88f0bc2272",
|
||||
"size": 851111
|
||||
}
|
||||
}
|
||||
},
|
||||
"org.lwjgl:lwjgl-opengl:3.4.3:natives-linux": {
|
||||
"name": "org.lwjgl:lwjgl-opengl:3.4.3:natives-freebsd",
|
||||
"downloads": {
|
||||
"artifact": {
|
||||
"path": "org/lwjgl/lwjgl-opengl/3.4.3/lwjgl-opengl-3.4.3-natives-freebsd.jar",
|
||||
"url": "https://repo1.maven.org/maven2/org/lwjgl/lwjgl-opengl/3.4.3/lwjgl-opengl-3.4.3-natives-freebsd.jar",
|
||||
"sha1": "97992b72536b99ad922ac1318f397e049de50223",
|
||||
"size": 83069
|
||||
}
|
||||
}
|
||||
},
|
||||
"org.lwjgl:lwjgl-glfw:3.4.3:natives-linux": {
|
||||
"name": "org.lwjgl:lwjgl-glfw:3.4.3:natives-freebsd",
|
||||
"downloads": {
|
||||
"artifact": {
|
||||
"path": "org/lwjgl/lwjgl-glfw/3.4.3/lwjgl-glfw-3.4.3-natives-freebsd.jar",
|
||||
"url": "https://repo1.maven.org/maven2/org/lwjgl/lwjgl-glfw/3.4.3/lwjgl-glfw-3.4.3-natives-freebsd.jar",
|
||||
"sha1": "783d56acc7b0bc5690f4d139d843d5cc083a7ef9",
|
||||
"size": 108309
|
||||
}
|
||||
}
|
||||
},
|
||||
"org.lwjgl:lwjgl-stb:3.4.3:natives-linux": {
|
||||
"name": "org.lwjgl:lwjgl-stb:3.4.3:natives-freebsd",
|
||||
"downloads": {
|
||||
"artifact": {
|
||||
"path": "org/lwjgl/lwjgl-stb/3.4.3/lwjgl-stb-3.4.3-natives-freebsd.jar",
|
||||
"url": "https://repo1.maven.org/maven2/org/lwjgl/lwjgl-stb/3.4.3/lwjgl-stb-3.4.3-natives-freebsd.jar",
|
||||
"sha1": "0422952f19f24b9a10a29fcc44ac77ed8b842b6a",
|
||||
"size": 225716
|
||||
}
|
||||
}
|
||||
},
|
||||
"org.lwjgl:lwjgl-tinyfd:3.4.3:natives-linux": {
|
||||
"name": "org.lwjgl:lwjgl-tinyfd:3.4.3:natives-freebsd",
|
||||
"downloads": {
|
||||
"artifact": {
|
||||
"path": "org/lwjgl/lwjgl-tinyfd/3.4.3/lwjgl-tinyfd-3.4.3-natives-freebsd.jar",
|
||||
"url": "https://repo1.maven.org/maven2/org/lwjgl/lwjgl-tinyfd/3.4.3/lwjgl-tinyfd-3.4.3-natives-freebsd.jar",
|
||||
"sha1": "5650a5b1cddce100f5c13761f7e4a5a6cf248320",
|
||||
"size": 40582
|
||||
}
|
||||
}
|
||||
},
|
||||
"org.lwjgl:lwjgl-freetype:3.4.3:natives-linux": {
|
||||
"name": "org.lwjgl:lwjgl-freetype:3.4.3:natives-freebsd",
|
||||
"downloads": {
|
||||
"artifact": {
|
||||
"path": "org/lwjgl/lwjgl-freetype/3.4.3/lwjgl-freetype-3.4.3-natives-freebsd.jar",
|
||||
"url": "https://repo1.maven.org/maven2/org/lwjgl/lwjgl-freetype/3.4.3/lwjgl-freetype-3.4.3-natives-freebsd.jar",
|
||||
"sha1": "3938689ef674b5894b41af23de67aa53cf5c6a58",
|
||||
"size": 1217752
|
||||
}
|
||||
}
|
||||
},
|
||||
"org.lwjgl:lwjgl-shaderc:3.4.3:natives-linux": {
|
||||
"name": "org.lwjgl:lwjgl-shaderc:3.4.3:natives-freebsd",
|
||||
"downloads": {
|
||||
"artifact": {
|
||||
"path": "org/lwjgl/lwjgl-shaderc/3.4.3/lwjgl-shaderc-3.4.3-natives-freebsd.jar",
|
||||
"url": "https://repo1.maven.org/maven2/org/lwjgl/lwjgl-shaderc/3.4.3/lwjgl-shaderc-3.4.3-natives-freebsd.jar",
|
||||
"sha1": "843c2c6b1588175e28eefe260d3833244119607b",
|
||||
"size": 3063282
|
||||
}
|
||||
}
|
||||
},
|
||||
"org.lwjgl:lwjgl-spvc:3.4.3:natives-linux": {
|
||||
"name": "org.lwjgl:lwjgl-spvc:3.4.3:natives-freebsd",
|
||||
"downloads": {
|
||||
"artifact": {
|
||||
"path": "org/lwjgl/lwjgl-spvc/3.4.3/lwjgl-spvc-3.4.3-natives-freebsd.jar",
|
||||
"url": "https://repo1.maven.org/maven2/org/lwjgl/lwjgl-spvc/3.4.3/lwjgl-spvc-3.4.3-natives-freebsd.jar",
|
||||
"sha1": "f66777d0f5e784c203fda965e28e6f41f6287eed",
|
||||
"size": 1122077
|
||||
}
|
||||
}
|
||||
},
|
||||
"org.lwjgl:lwjgl-vma:3.4.3:natives-linux": {
|
||||
"name": "org.lwjgl:lwjgl-vma:3.4.3:natives-freebsd",
|
||||
"downloads": {
|
||||
"artifact": {
|
||||
"path": "org/lwjgl/lwjgl-vma/3.4.3/lwjgl-vma-3.4.3-natives-freebsd.jar",
|
||||
"url": "https://repo1.maven.org/maven2/org/lwjgl/lwjgl-vma/3.4.3/lwjgl-vma-3.4.3-natives-freebsd.jar",
|
||||
"sha1": "b160d451c6224139bcb47d565dac76b1d242cf2b",
|
||||
"size": 63336
|
||||
}
|
||||
}
|
||||
},
|
||||
"org.lwjgl:lwjgl-sdl:3.4.3:natives-linux": {
|
||||
"name": "org.lwjgl:lwjgl-sdl:3.4.3:natives-freebsd",
|
||||
"downloads": {
|
||||
"artifact": {
|
||||
"path": "org/lwjgl/lwjgl-sdl/3.4.3/lwjgl-sdl-3.4.3-natives-freebsd.jar",
|
||||
"url": "https://repo1.maven.org/maven2/org/lwjgl/lwjgl-sdl/3.4.3/lwjgl-sdl-3.4.3-natives-freebsd.jar",
|
||||
"sha1": "aad4d48eb5b3b72d550589fa6d07bbc6113b442e",
|
||||
"size": 1242611
|
||||
}
|
||||
}
|
||||
},
|
||||
"net.java.jinput:jinput-platform:2.0.5:natives": null,
|
||||
"com.mojang:text2speech:1.10.3:natives": null,
|
||||
"com.mojang:text2speech:1.11.3:natives": null,
|
||||
|
||||
@@ -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 {
|
||||
|
||||
+8
-1
@@ -213,8 +213,15 @@ public final class ModrinthRemoteAddonRepository implements RemoteAddonRepositor
|
||||
|
||||
@Override
|
||||
public Optional<RemoteAddon.Version> getRemoteVersionByLocalFile(Path file) throws IOException {
|
||||
String sha1 = DigestUtils.digestToString("SHA-1", file);
|
||||
return getRemoteVersionBySHA1(DigestUtils.digestToString("SHA-1", file));
|
||||
}
|
||||
|
||||
/// Looks up a Modrinth version by file SHA-1 hash.
|
||||
///
|
||||
/// @param sha1 the SHA-1 digest of the file
|
||||
/// @return the matching remote version, or empty when not found
|
||||
/// @throws IOException if the Modrinth request fails for a reason other than 404 / missing file
|
||||
public Optional<RemoteAddon.Version> getRemoteVersionBySHA1(String sha1) throws IOException {
|
||||
SEMAPHORE.acquireUninterruptibly();
|
||||
try {
|
||||
ProjectVersion projectVersion = HttpRequest.GET(PREFIX + "/v2/version_file/" + sha1,
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -280,6 +280,7 @@ public class DefaultLauncher extends Launcher {
|
||||
if (instance.hasComponent(GameComponentType.CLEANROOM)) {
|
||||
libraryClasspath.removeIf(c -> c.contains("2.9.4-nightly-20150209"));
|
||||
libraryClasspath.removeIf(c -> c.contains("platform-3.4.0"));
|
||||
libraryClasspath.removeIf(c -> c.contains("icu4j-core-mojang"));
|
||||
}
|
||||
|
||||
Path jar = instance.getInstanceJarFile();
|
||||
|
||||
@@ -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");
|
||||
|
||||
|
||||
@@ -20,6 +20,7 @@ package org.jackhuang.hmcl.modpack;
|
||||
import org.jackhuang.hmcl.download.DefaultDependencyManager;
|
||||
import org.jackhuang.hmcl.game.GameInstanceID;
|
||||
import org.jackhuang.hmcl.task.Task;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import java.nio.charset.Charset;
|
||||
import java.nio.file.Path;
|
||||
@@ -120,7 +121,21 @@ public abstract class Modpack {
|
||||
return this;
|
||||
}
|
||||
|
||||
public abstract Task<?> getInstallTask(DefaultDependencyManager dependencyManager, Path zipFile, GameInstanceID instanceId, String iconUrl);
|
||||
/// Creates the install task for this modpack.
|
||||
///
|
||||
/// @param dependencyManager the dependency manager
|
||||
/// @param zipFile the modpack archive
|
||||
/// @param instanceId the target instance id
|
||||
/// @param iconUrl the optional icon URL, or `null`
|
||||
/// @param excludedFiles keys of optional files the user chose not to install; `null` or empty means
|
||||
/// install all files. When non-null, must not contain `null` elements.
|
||||
/// @return the install task
|
||||
public abstract Task<?> getInstallTask(
|
||||
DefaultDependencyManager dependencyManager,
|
||||
Path zipFile,
|
||||
GameInstanceID instanceId,
|
||||
String iconUrl,
|
||||
@Nullable Set<String> excludedFiles);
|
||||
|
||||
public static boolean acceptFile(String path, List<String> blackList, List<String> whiteList) {
|
||||
if (path.isEmpty())
|
||||
|
||||
@@ -0,0 +1,63 @@
|
||||
/*
|
||||
* 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.modpack;
|
||||
|
||||
import org.jackhuang.hmcl.addon.RemoteAddon;
|
||||
import org.jetbrains.annotations.NotNullByDefault;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
/// A modpack file entry that may be marked optional by the pack author.
|
||||
@NotNullByDefault
|
||||
public interface ModpackFile {
|
||||
|
||||
/// Returns a stable identity for this file used when persisting exclusion choices.
|
||||
///
|
||||
/// @return the exclusion key
|
||||
String key();
|
||||
|
||||
/// Returns the file name, or `null` when it has not been resolved yet.
|
||||
///
|
||||
/// @return the file name, or `null`
|
||||
@Nullable
|
||||
String fileName();
|
||||
|
||||
/// Returns whether this file is optional on the client side.
|
||||
///
|
||||
/// @return `true` when the client may skip this file
|
||||
boolean optional();
|
||||
|
||||
/// Returns the path of the file relative to the instance run directory, or `null` when unknown.
|
||||
///
|
||||
/// @return the relative path, or `null`
|
||||
@Nullable
|
||||
String path();
|
||||
|
||||
/// Returns the remote addon metadata for this file when [#addonQueried()] is `true`.
|
||||
///
|
||||
/// @return the remote addon, or `null` when not found or not yet queried
|
||||
@Nullable
|
||||
RemoteAddon remoteAddon();
|
||||
|
||||
/// Returns whether remote addon metadata has been queried for this file.
|
||||
///
|
||||
/// When `false`, [#remoteAddon()] is unset and a query has not been attempted yet.
|
||||
/// When `true`, [#remoteAddon()] is the queried result (`null` means not found).
|
||||
///
|
||||
/// @return `true` when remote addon metadata has been queried
|
||||
boolean addonQueried();
|
||||
}
|
||||
@@ -17,6 +17,28 @@
|
||||
*/
|
||||
package org.jackhuang.hmcl.modpack;
|
||||
|
||||
import org.jetbrains.annotations.NotNullByDefault;
|
||||
import org.jetbrains.annotations.Unmodifiable;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/// Format-specific modpack manifest metadata.
|
||||
@NotNullByDefault
|
||||
public interface ModpackManifest {
|
||||
|
||||
/// Returns the provider that understands this manifest.
|
||||
///
|
||||
/// @return the modpack provider
|
||||
ModpackProvider getProvider();
|
||||
|
||||
/// Marker for manifests that expose optional file entries.
|
||||
@NotNullByDefault
|
||||
interface SupportOptional {
|
||||
|
||||
/// Returns all files declared by this manifest, including required and optional ones.
|
||||
///
|
||||
/// @return the modpack files
|
||||
@Unmodifiable
|
||||
List<? extends ModpackFile> getFiles();
|
||||
}
|
||||
}
|
||||
@@ -20,6 +20,7 @@ package org.jackhuang.hmcl.modpack;
|
||||
import com.google.gson.JsonParseException;
|
||||
import kala.compress.archivers.zip.ZipArchiveReader;
|
||||
import org.jackhuang.hmcl.download.DefaultDependencyManager;
|
||||
import org.jackhuang.hmcl.download.DownloadProvider;
|
||||
import org.jackhuang.hmcl.game.DefaultGameInstance;
|
||||
import org.jackhuang.hmcl.game.LaunchOptions;
|
||||
import org.jackhuang.hmcl.task.Task;
|
||||
@@ -29,6 +30,7 @@ import org.jetbrains.annotations.Nullable;
|
||||
import java.io.IOException;
|
||||
import java.nio.charset.Charset;
|
||||
import java.nio.file.Path;
|
||||
import java.util.Set;
|
||||
|
||||
/// Provides format-specific operations for reading, installing, updating, and completing modpacks.
|
||||
@NotNullByDefault
|
||||
@@ -52,9 +54,16 @@ public interface ModpackProvider {
|
||||
/// @param instance the registered instance to update
|
||||
/// @param zipFile the modpack archive
|
||||
/// @param modpack the parsed modpack
|
||||
/// @param excludedFiles keys of optional files the user chose not to install; `null` or empty means
|
||||
/// install all files. When non-null, must not contain `null` elements.
|
||||
/// @return the update task
|
||||
/// @throws MismatchedModpackTypeException if the parsed manifest belongs to another provider
|
||||
Task<?> createUpdateTask(DefaultDependencyManager dependencyManager, DefaultGameInstance instance, Path zipFile, Modpack modpack) throws MismatchedModpackTypeException;
|
||||
Task<?> createUpdateTask(
|
||||
DefaultDependencyManager dependencyManager,
|
||||
DefaultGameInstance instance,
|
||||
Path zipFile,
|
||||
Modpack modpack,
|
||||
@Nullable Set<String> excludedFiles) throws MismatchedModpackTypeException;
|
||||
|
||||
/// Reads this provider's manifest from an opened modpack archive.
|
||||
///
|
||||
@@ -72,4 +81,15 @@ public interface ModpackProvider {
|
||||
/// @param builder the launch options builder to update
|
||||
default void injectLaunchOptions(String modpackConfigurationJson, LaunchOptions.Builder builder) {
|
||||
}
|
||||
|
||||
/// Enriches a manifest with remote metadata for optional files (file names, URLs, addons).
|
||||
/// The type of the result manifest must be same as the original one.
|
||||
/// When the remote metadata can not be fetched, the original files are returned.
|
||||
///
|
||||
/// @param downloadProvider the download provider used for remote queries
|
||||
/// @param manifest the parsed manifest
|
||||
/// @return the enriched manifest, or `manifest` when no enrichment is needed
|
||||
default ModpackManifest loadFiles(DownloadProvider downloadProvider, ModpackManifest manifest) {
|
||||
return manifest;
|
||||
}
|
||||
}
|
||||
@@ -18,13 +18,13 @@
|
||||
package org.jackhuang.hmcl.modpack.curse;
|
||||
|
||||
import com.google.gson.JsonParseException;
|
||||
import org.jackhuang.hmcl.addon.RemoteAddon;
|
||||
import org.jackhuang.hmcl.addon.mod.ModManager;
|
||||
import org.jackhuang.hmcl.addon.repository.CurseForgeRemoteAddonRepository;
|
||||
import org.jackhuang.hmcl.download.DefaultDependencyManager;
|
||||
import org.jackhuang.hmcl.download.DownloadProvider;
|
||||
import org.jackhuang.hmcl.game.DefaultGameInstance;
|
||||
import org.jackhuang.hmcl.addon.mod.ModManager;
|
||||
import org.jackhuang.hmcl.modpack.ModpackCompletionException;
|
||||
import org.jackhuang.hmcl.addon.RemoteAddon;
|
||||
import org.jackhuang.hmcl.task.FileDownloadTask;
|
||||
import org.jackhuang.hmcl.task.Task;
|
||||
import org.jackhuang.hmcl.util.StringUtils;
|
||||
@@ -38,9 +38,10 @@ import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
import java.util.stream.Collectors;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
|
||||
@@ -61,6 +62,9 @@ public final class CurseCompletionTask extends Task<Void> {
|
||||
/// The manifest supplied by the caller or loaded from disk, if available.
|
||||
private @Nullable CurseManifest manifest;
|
||||
|
||||
/// Keys of optional files the user chose not to install; `null` means install all.
|
||||
private @Nullable Set<String> excludedFiles;
|
||||
|
||||
/// Download tasks produced during [#execute()].
|
||||
private List<Task<?>> dependencies = List.of();
|
||||
|
||||
@@ -78,7 +82,7 @@ public final class CurseCompletionTask extends Task<Void> {
|
||||
/// @param dependencyManager the dependency manager
|
||||
/// @param instance the registered instance to complete
|
||||
public CurseCompletionTask(DefaultDependencyManager dependencyManager, DefaultGameInstance instance) {
|
||||
this(dependencyManager, instance, null);
|
||||
this(dependencyManager, instance, null, null);
|
||||
}
|
||||
|
||||
/// Creates a task that completes the installed CurseForge modpack using an optional manifest.
|
||||
@@ -86,21 +90,33 @@ public final class CurseCompletionTask extends Task<Void> {
|
||||
/// @param dependencyManager the dependency manager
|
||||
/// @param instance the registered instance to complete
|
||||
/// @param manifest the CurseForge manifest, or `null` to read it from disk
|
||||
/// @param excludedFiles keys of optional files the user chose not to install; `null` means install all.
|
||||
/// When non-null, must not contain `null` elements.
|
||||
public CurseCompletionTask(
|
||||
DefaultDependencyManager dependencyManager,
|
||||
DefaultGameInstance instance,
|
||||
@Nullable CurseManifest manifest) {
|
||||
@Nullable CurseManifest manifest,
|
||||
@Nullable Set<String> excludedFiles) {
|
||||
dependencyManager.validateGameInstance(instance);
|
||||
this.dependency = dependencyManager;
|
||||
this.instance = instance;
|
||||
this.modManager = instance.getModManager();
|
||||
this.manifest = manifest;
|
||||
this.excludedFiles = excludedFiles == null ? null : Set.copyOf(excludedFiles);
|
||||
|
||||
if (manifest == null)
|
||||
try {
|
||||
Path manifestFile = instance.getInstanceRoot().resolve("manifest.json");
|
||||
Path root = instance.getInstanceRoot();
|
||||
Path manifestFile = root.resolve("manifest.json");
|
||||
if (Files.exists(manifestFile))
|
||||
this.manifest = JsonUtils.fromJsonFile(manifestFile, CurseManifest.class);
|
||||
Path excludedFile = root.resolve("excluded.json");
|
||||
if (Files.exists(excludedFile)) {
|
||||
this.excludedFiles = Set.copyOf(Objects.requireNonNull(
|
||||
JsonUtils.fromJsonFile(excludedFile, JsonUtils.listTypeOf(String.class))));
|
||||
} else {
|
||||
this.excludedFiles = null;
|
||||
}
|
||||
} catch (Exception e) {
|
||||
LOG.warning("Unable to read CurseForge modpack manifest.json", e);
|
||||
}
|
||||
@@ -151,8 +167,13 @@ public final class CurseCompletionTask extends Task<Void> {
|
||||
return file;
|
||||
}
|
||||
})
|
||||
.collect(Collectors.toList()));
|
||||
.toList());
|
||||
JsonUtils.writeToJsonFile(root.resolve("manifest.json"), newManifest);
|
||||
if (excludedFiles != null) {
|
||||
JsonUtils.writeToJsonFile(
|
||||
root.resolve("excluded.json"),
|
||||
List.copyOf(excludedFiles));
|
||||
}
|
||||
|
||||
Path versionRoot = instance.getInstanceRoot();
|
||||
Path resourcePacksRoot = versionRoot.resolve("resourcepacks");
|
||||
@@ -161,6 +182,7 @@ public final class CurseCompletionTask extends Task<Void> {
|
||||
dependencies = newManifest.files()
|
||||
.stream().parallel()
|
||||
.filter(f -> f.fileName() != null)
|
||||
.filter(f -> excludedFiles == null || !excludedFiles.contains(f.key()))
|
||||
.flatMap(f -> {
|
||||
try {
|
||||
Path path = guessFilePath(f, dependency.getDownloadProvider(), resourcePacksRoot, shaderPacksRoot);
|
||||
@@ -171,7 +193,7 @@ public final class CurseCompletionTask extends Task<Void> {
|
||||
var task = new FileDownloadTask(f.url(), path, f.getIntegrityCheck());
|
||||
task.setCacheRepository(dependency.getCacheRepository());
|
||||
task.setCaching(true);
|
||||
return Stream.of(task.withCounter("hmcl.modpack.download"));
|
||||
return Stream.<Task<?>>of(task.withCounter("hmcl.modpack.download"));
|
||||
} catch (IOException e) {
|
||||
LOG.warning("Could not query api.curseforge.com for mod: " + f.projectID() + ", " + f.fileID(), e);
|
||||
return Stream.empty(); // Ignore this file.
|
||||
@@ -179,7 +201,7 @@ public final class CurseCompletionTask extends Task<Void> {
|
||||
updateProgress(finished.incrementAndGet(), newManifest.files().size());
|
||||
}
|
||||
})
|
||||
.collect(Collectors.toList());
|
||||
.toList();
|
||||
|
||||
if (!dependencies.isEmpty()) {
|
||||
getProperties().put("total", dependencies.size());
|
||||
|
||||
@@ -61,6 +61,9 @@ public final class CurseInstallTask extends Task<Void> {
|
||||
/// Previous modpack configuration when updating, or `null` for a new installation.
|
||||
private final @Nullable ModpackConfiguration<CurseManifest> config;
|
||||
|
||||
/// Keys of optional files the user chose not to install; `null` or empty means install all.
|
||||
private final @Nullable Set<String> excludedFiles;
|
||||
|
||||
/// Validated extension of the scheduled icon download, or `null` when no icon is scheduled.
|
||||
private @Nullable String iconExt;
|
||||
|
||||
@@ -77,6 +80,8 @@ public final class CurseInstallTask extends Task<Void> {
|
||||
/// @param manifest the CurseForge manifest
|
||||
/// @param instanceId the id of the new instance
|
||||
/// @param iconUrl the optional icon URL, or `null`
|
||||
/// @param excludedFiles keys of optional files the user chose not to install; `null` or empty means
|
||||
/// install all files. When non-null, must not contain `null` elements.
|
||||
/// @throws IllegalStateException if the target cannot be reserved or another repository draft
|
||||
/// is open
|
||||
public CurseInstallTask(
|
||||
@@ -85,8 +90,9 @@ public final class CurseInstallTask extends Task<Void> {
|
||||
Modpack modpack,
|
||||
CurseManifest manifest,
|
||||
GameInstanceID instanceId,
|
||||
@Nullable String iconUrl) {
|
||||
this(dependencyManager, zipFile, modpack, manifest, instanceId, null, iconUrl);
|
||||
@Nullable String iconUrl,
|
||||
@Nullable Set<String> excludedFiles) {
|
||||
this(dependencyManager, zipFile, modpack, manifest, instanceId, null, iconUrl, excludedFiles);
|
||||
}
|
||||
|
||||
/// Creates a task that updates an existing CurseForge modpack instance.
|
||||
@@ -97,6 +103,8 @@ public final class CurseInstallTask extends Task<Void> {
|
||||
/// @param manifest the CurseForge manifest
|
||||
/// @param instance the existing instance to update
|
||||
/// @param iconUrl the optional icon URL, or `null`
|
||||
/// @param excludedFiles keys of optional files the user chose not to install; `null` or empty means
|
||||
/// install all files. When non-null, must not contain `null` elements.
|
||||
/// @throws IllegalArgumentException if `instance` belongs to another repository, has no
|
||||
/// modpack configuration, or records another provider type
|
||||
/// @throws IllegalStateException if `instance` is not the exact currently published object
|
||||
@@ -107,8 +115,9 @@ public final class CurseInstallTask extends Task<Void> {
|
||||
Modpack modpack,
|
||||
CurseManifest manifest,
|
||||
DefaultGameInstance instance,
|
||||
@Nullable String iconUrl) {
|
||||
this(dependencyManager, zipFile, modpack, manifest, instance.getId(), instance, iconUrl);
|
||||
@Nullable String iconUrl,
|
||||
@Nullable Set<String> excludedFiles) {
|
||||
this(dependencyManager, zipFile, modpack, manifest, instance.getId(), instance, iconUrl, excludedFiles);
|
||||
}
|
||||
|
||||
/// Creates a CurseForge installation task in the mode selected by `updateTarget`.
|
||||
@@ -120,6 +129,8 @@ public final class CurseInstallTask extends Task<Void> {
|
||||
/// @param instanceId the target instance id
|
||||
/// @param updateTarget the existing instance selecting update mode, or `null` for install
|
||||
/// @param iconUrl the optional icon URL, or `null`
|
||||
/// @param excludedFiles keys of optional files the user chose not to install; `null` or empty means
|
||||
/// install all files. When non-null, must not contain `null` elements.
|
||||
/// @throws IllegalArgumentException if an update target has no compatible configuration
|
||||
/// @throws IllegalStateException if the target cannot be reserved, an update target is not
|
||||
/// the exact published object, or another draft is open
|
||||
@@ -130,7 +141,8 @@ public final class CurseInstallTask extends Task<Void> {
|
||||
CurseManifest manifest,
|
||||
GameInstanceID instanceId,
|
||||
@Nullable DefaultGameInstance updateTarget,
|
||||
@Nullable String iconUrl) {
|
||||
@Nullable String iconUrl,
|
||||
@Nullable Set<String> excludedFiles) {
|
||||
this.dependencyManager = dependencyManager;
|
||||
this.zipFile = zipFile;
|
||||
this.modpack = modpack;
|
||||
@@ -138,6 +150,7 @@ public final class CurseInstallTask extends Task<Void> {
|
||||
this.instanceId = instanceId;
|
||||
this.updateTarget = updateTarget;
|
||||
this.iconUrl = iconUrl;
|
||||
this.excludedFiles = excludedFiles == null ? null : Set.copyOf(excludedFiles);
|
||||
this.repository = dependencyManager.getGameRepository();
|
||||
|
||||
this.run = repository.getLayout().getInstanceRoot(instanceId);
|
||||
@@ -282,6 +295,6 @@ public final class CurseInstallTask extends Task<Void> {
|
||||
}
|
||||
|
||||
// The game builder runs as a dependent and registers the instance before this phase.
|
||||
dependencies.add(new CurseCompletionTask(dependencyManager, repository.getInstance(instanceId), manifest));
|
||||
dependencies.add(new CurseCompletionTask(dependencyManager, repository.getInstance(instanceId), manifest, excludedFiles));
|
||||
}
|
||||
}
|
||||
@@ -18,6 +18,7 @@
|
||||
package org.jackhuang.hmcl.modpack.curse;
|
||||
|
||||
import com.google.gson.annotations.SerializedName;
|
||||
import org.jackhuang.hmcl.modpack.ModpackFile;
|
||||
import org.jackhuang.hmcl.modpack.ModpackManifest;
|
||||
import org.jackhuang.hmcl.modpack.ModpackProvider;
|
||||
import org.jackhuang.hmcl.util.gson.JsonSerializable;
|
||||
@@ -34,12 +35,18 @@ public record CurseManifest(@SerializedName("manifestType") String manifestType,
|
||||
@SerializedName("author") String author,
|
||||
@SerializedName("overrides") String overrides,
|
||||
@SerializedName("minecraft") CurseManifestMinecraft minecraft,
|
||||
@SerializedName("files") @Unmodifiable List<CurseManifestFile> files) implements ModpackManifest {
|
||||
@SerializedName("files") @Unmodifiable List<CurseManifestFile> files)
|
||||
implements ModpackManifest, ModpackManifest.SupportOptional {
|
||||
|
||||
public CurseManifest setFiles(List<CurseManifestFile> files) {
|
||||
return new CurseManifest(manifestType, manifestVersion, name, version, author, overrides, minecraft, files);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<? extends ModpackFile> getFiles() {
|
||||
return files;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ModpackProvider getProvider() {
|
||||
return CurseModpackProvider.INSTANCE;
|
||||
|
||||
@@ -19,11 +19,15 @@ package org.jackhuang.hmcl.modpack.curse;
|
||||
|
||||
import com.google.gson.JsonParseException;
|
||||
import com.google.gson.annotations.SerializedName;
|
||||
import org.jackhuang.hmcl.addon.RemoteAddon;
|
||||
import org.jackhuang.hmcl.modpack.ModpackFile;
|
||||
import org.jackhuang.hmcl.task.FileDownloadTask;
|
||||
import org.jackhuang.hmcl.util.Pair;
|
||||
import org.jackhuang.hmcl.util.gson.JsonSerializable;
|
||||
import org.jackhuang.hmcl.util.gson.Validation;
|
||||
import org.jetbrains.annotations.NotNullByDefault;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
import org.jetbrains.annotations.Unmodifiable;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
@@ -31,31 +35,81 @@ import java.util.Objects;
|
||||
|
||||
import static org.jackhuang.hmcl.util.Pair.pair;
|
||||
|
||||
/// A CurseForge modpack file entry.
|
||||
///
|
||||
/// @param projectID the project id
|
||||
/// @param fileID the file id
|
||||
/// @param fileName the file name, or `null` when unresolved
|
||||
/// @param url the explicit download URL, or `null` to derive it from the file name
|
||||
/// @param required whether the file is required
|
||||
/// @param hashes the hash algorithm names and checksums, or `null` when unavailable;
|
||||
/// the map is retained without copying
|
||||
/// @param remoteAddon the remote addon metadata, or `null` when unavailable
|
||||
/// @param addonQueried whether remote addon metadata has been queried
|
||||
/// @author huangyuhui
|
||||
@NotNullByDefault
|
||||
@JsonSerializable
|
||||
public record CurseManifestFile(@SerializedName("projectID") int projectID,
|
||||
@SerializedName("fileID") int fileID,
|
||||
@SerializedName("fileName") String fileName,
|
||||
@SerializedName("url") String url,
|
||||
@SerializedName("required") boolean required,
|
||||
@SerializedName("hashes") Map<String, String> hashes) implements Validation {
|
||||
public record CurseManifestFile(
|
||||
@SerializedName("projectID") int projectID,
|
||||
@SerializedName("fileID") int fileID,
|
||||
@SerializedName("fileName") @Nullable String fileName,
|
||||
@SerializedName("url") @Nullable String url,
|
||||
@SerializedName("required") boolean required,
|
||||
@SerializedName("hashes") @Nullable Map<String, @Nullable String> hashes,
|
||||
@Nullable RemoteAddon remoteAddon,
|
||||
boolean addonQueried) implements Validation, ModpackFile {
|
||||
|
||||
private static final List<Pair<String, String>> HASH_ALGORITHMS = List.of(
|
||||
/// Supported manifest hash names and digest algorithms, in selection order.
|
||||
private static final @Unmodifiable List<Pair<String, String>> HASH_ALGORITHMS = List.of(
|
||||
pair("sha1", "SHA-1"),
|
||||
pair("sha256", "SHA-256"),
|
||||
pair("sha512", "SHA-512"),
|
||||
pair("md5", "MD5")
|
||||
);
|
||||
|
||||
/// Creates a file entry without checksums or remote addon metadata.
|
||||
///
|
||||
/// @param projectID the project id
|
||||
/// @param fileID the file id
|
||||
/// @param fileName the file name, or `null`
|
||||
/// @param url the download URL, or `null`
|
||||
/// @param required whether the file is required
|
||||
public CurseManifestFile(int projectID, int fileID, @Nullable String fileName, @Nullable String url, boolean required) {
|
||||
this(projectID, fileID, fileName, url, required, null, null, false);
|
||||
}
|
||||
|
||||
/// Returns `curseforge:<projectID>:<fileID>` as the exclusion key.
|
||||
@Override
|
||||
public String key() {
|
||||
return "curseforge:" + projectID + ":" + fileID;
|
||||
}
|
||||
|
||||
/// Returns whether this entry is not required by the manifest.
|
||||
@Override
|
||||
public boolean optional() {
|
||||
return !required();
|
||||
}
|
||||
|
||||
/// Returns the file name prefixed with `mods/`, or `null` when unresolved.
|
||||
@Override
|
||||
public @Nullable String path() {
|
||||
return fileName != null ? "mods/" + fileName : null;
|
||||
}
|
||||
|
||||
/// Checks that the project and file ids are nonzero.
|
||||
///
|
||||
/// @throws JsonParseException if either id is zero
|
||||
@Override
|
||||
public void validate() throws JsonParseException {
|
||||
if (projectID == 0 || fileID == 0)
|
||||
throw new JsonParseException("Missing Project ID or File ID.");
|
||||
}
|
||||
|
||||
/// Returns the download URL, deriving a ForgeCDN URL when the manifest omits it.
|
||||
///
|
||||
/// @return the download URL, or `null` when the file name is also missing
|
||||
@Override
|
||||
@Nullable
|
||||
public String url() {
|
||||
public @Nullable String url() {
|
||||
if (url == null) {
|
||||
return fileName != null
|
||||
? String.format("https://edge.forgecdn.net/files/%d/%d/%s", fileID / 1000, fileID % 1000, fileName)
|
||||
@@ -65,12 +119,15 @@ public record CurseManifestFile(@SerializedName("projectID") int projectID,
|
||||
}
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public FileDownloadTask.IntegrityCheck getIntegrityCheck() {
|
||||
/// Returns an integrity check using the first non-null checksum under the keys
|
||||
/// `sha1`, `sha256`, `sha512`, and `md5`, in that order.
|
||||
///
|
||||
/// @return the integrity check, or `null` when no supported checksum is available
|
||||
public @Nullable FileDownloadTask.IntegrityCheck getIntegrityCheck() {
|
||||
if (hashes == null || hashes.isEmpty()) return null;
|
||||
|
||||
for (Pair<String, String> algorithm : HASH_ALGORITHMS) {
|
||||
String hash = hashes.get(algorithm.key());
|
||||
@Nullable String hash = hashes.get(algorithm.key());
|
||||
if (hash != null) {
|
||||
return new FileDownloadTask.IntegrityCheck(algorithm.value(), hash);
|
||||
}
|
||||
@@ -78,25 +135,48 @@ public record CurseManifestFile(@SerializedName("projectID") int projectID,
|
||||
return null;
|
||||
}
|
||||
|
||||
/// Returns a copy with a resolved file name, retaining all other fields.
|
||||
///
|
||||
/// @param fileName the resolved file name
|
||||
/// @return the updated entry
|
||||
public CurseManifestFile withFileName(String fileName) {
|
||||
return new CurseManifestFile(projectID, fileID, fileName, url, required, hashes);
|
||||
return new CurseManifestFile(projectID, fileID, fileName, url, required, hashes, remoteAddon, addonQueried);
|
||||
}
|
||||
|
||||
/// Returns a copy with a resolved download URL, retaining all other fields.
|
||||
///
|
||||
/// @param url the download URL
|
||||
/// @return the updated entry
|
||||
public CurseManifestFile withURL(String url) {
|
||||
return new CurseManifestFile(projectID, fileID, fileName, url, required, hashes);
|
||||
return new CurseManifestFile(projectID, fileID, fileName, url, required, hashes, remoteAddon, addonQueried);
|
||||
}
|
||||
|
||||
public CurseManifestFile withHashes(Map<String, String> hashes) {
|
||||
return new CurseManifestFile(projectID, fileID, fileName, url, required, hashes);
|
||||
/// Returns a copy marked as queried, with the given remote addon metadata.
|
||||
///
|
||||
/// @param remoteAddon the remote addon, or `null` when not found
|
||||
/// @return the updated entry, retaining all other fields
|
||||
public CurseManifestFile withAddon(@Nullable RemoteAddon remoteAddon) {
|
||||
return new CurseManifestFile(projectID, fileID, fileName, url, required, hashes, remoteAddon, true);
|
||||
}
|
||||
|
||||
/// Returns a copy with the given checksums, retaining all other fields.
|
||||
///
|
||||
/// @param hashes the hash algorithm names and checksums, retained without copying;
|
||||
/// `null` removes the checksums
|
||||
/// @return the updated entry
|
||||
public CurseManifestFile withHashes(@Nullable Map<String, @Nullable String> hashes) {
|
||||
return new CurseManifestFile(projectID, fileID, fileName, url, required, hashes, remoteAddon, addonQueried);
|
||||
}
|
||||
|
||||
/// Returns whether the other object is a file entry with the same project and file ids.
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
public boolean equals(@Nullable Object o) {
|
||||
return this == o || o instanceof CurseManifestFile that
|
||||
&& this.projectID == that.projectID
|
||||
&& this.fileID == that.fileID;
|
||||
}
|
||||
|
||||
/// Returns a hash code based only on the project and file ids.
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return Objects.hash(projectID, fileID);
|
||||
|
||||
@@ -20,21 +20,27 @@ package org.jackhuang.hmcl.modpack.curse;
|
||||
import com.google.gson.JsonParseException;
|
||||
import kala.compress.archivers.zip.ZipArchiveEntry;
|
||||
import kala.compress.archivers.zip.ZipArchiveReader;
|
||||
import org.jackhuang.hmcl.addon.RemoteAddon;
|
||||
import org.jackhuang.hmcl.addon.repository.CurseForgeRemoteAddonRepository;
|
||||
import org.jackhuang.hmcl.download.DefaultDependencyManager;
|
||||
import org.jackhuang.hmcl.download.DownloadProvider;
|
||||
import org.jackhuang.hmcl.game.DefaultGameInstance;
|
||||
import org.jackhuang.hmcl.game.GameInstanceID;
|
||||
import org.jackhuang.hmcl.modpack.MismatchedModpackTypeException;
|
||||
import org.jackhuang.hmcl.modpack.Modpack;
|
||||
import org.jackhuang.hmcl.modpack.ModpackProvider;
|
||||
import org.jackhuang.hmcl.modpack.ModpackUpdateTask;
|
||||
import org.jackhuang.hmcl.modpack.*;
|
||||
import org.jackhuang.hmcl.task.Task;
|
||||
import org.jackhuang.hmcl.util.StringUtils;
|
||||
import org.jackhuang.hmcl.util.gson.JsonUtils;
|
||||
import org.jackhuang.hmcl.util.io.CompressingUtils;
|
||||
import org.jackhuang.hmcl.util.io.IOUtils;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import java.io.FileNotFoundException;
|
||||
import java.io.IOException;
|
||||
import java.nio.charset.Charset;
|
||||
import java.nio.file.Path;
|
||||
import java.util.Set;
|
||||
|
||||
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
|
||||
|
||||
public final class CurseModpackProvider implements ModpackProvider {
|
||||
public static final CurseModpackProvider INSTANCE = new CurseModpackProvider();
|
||||
@@ -50,11 +56,16 @@ public final class CurseModpackProvider implements ModpackProvider {
|
||||
}
|
||||
|
||||
@Override
|
||||
public Task<?> createUpdateTask(DefaultDependencyManager dependencyManager, DefaultGameInstance instance, Path zipFile, Modpack modpack) throws MismatchedModpackTypeException {
|
||||
public Task<?> createUpdateTask(
|
||||
DefaultDependencyManager dependencyManager,
|
||||
DefaultGameInstance instance,
|
||||
Path zipFile,
|
||||
Modpack modpack,
|
||||
@Nullable Set<String> excludedFiles) throws MismatchedModpackTypeException {
|
||||
if (!(modpack.getManifest() instanceof CurseManifest curseManifest))
|
||||
throw new MismatchedModpackTypeException(getName(), modpack.getManifest().getProvider().getName());
|
||||
|
||||
return new ModpackUpdateTask(instance, new CurseInstallTask(dependencyManager, zipFile, modpack, curseManifest, instance, null));
|
||||
return new ModpackUpdateTask(instance, new CurseInstallTask(dependencyManager, zipFile, modpack, curseManifest, instance, null, excludedFiles));
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -70,10 +81,48 @@ public final class CurseModpackProvider implements ModpackProvider {
|
||||
|
||||
return new Modpack(manifest.name(), manifest.author(), manifest.version(), manifest.minecraft().gameVersion(), description, encoding, manifest) {
|
||||
@Override
|
||||
public Task<?> getInstallTask(DefaultDependencyManager dependencyManager, Path zipFile, GameInstanceID instanceId, String iconUrl) {
|
||||
return new CurseInstallTask(dependencyManager, zipFile, this, manifest, instanceId, iconUrl);
|
||||
public Task<?> getInstallTask(
|
||||
DefaultDependencyManager dependencyManager,
|
||||
Path zipFile,
|
||||
GameInstanceID instanceId,
|
||||
String iconUrl,
|
||||
@Nullable Set<String> excludedFiles) {
|
||||
return new CurseInstallTask(dependencyManager, zipFile, this, manifest, instanceId, iconUrl, excludedFiles);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@Override
|
||||
public CurseManifest loadFiles(DownloadProvider downloadProvider, ModpackManifest manifest1) {
|
||||
if (!(manifest1 instanceof CurseManifest manifest))
|
||||
throw new IllegalArgumentException("manifest1 is not a CurseManifest");
|
||||
return manifest.setFiles(
|
||||
manifest.files().parallelStream()
|
||||
.map(file -> {
|
||||
if (!file.optional()) {
|
||||
return file;
|
||||
}
|
||||
try {
|
||||
CurseManifestFile result = file;
|
||||
if (StringUtils.isBlank(file.fileName()) || file.url() == null) {
|
||||
RemoteAddon.File remoteFile = CurseForgeRemoteAddonRepository.MODS.getAddonFile(
|
||||
Integer.toString(file.projectID()), Integer.toString(file.fileID()));
|
||||
result = result.withFileName(remoteFile.filename()).withURL(remoteFile.url());
|
||||
}
|
||||
if (!file.addonQueried()) {
|
||||
RemoteAddon addon = CurseForgeRemoteAddonRepository.MODS.getAddonById(
|
||||
downloadProvider, Integer.toString(file.projectID()));
|
||||
result = result.withAddon(addon);
|
||||
}
|
||||
return result;
|
||||
} catch (FileNotFoundException fof) {
|
||||
LOG.warning("Could not query api.curseforge.com for deleted mods: " + file.projectID() + ", " + file.fileID(), fof);
|
||||
return file;
|
||||
} catch (IOException | JsonParseException e) {
|
||||
LOG.warning("Unable to fetch the file name projectID=" + file.projectID() + ", fileID=" + file.fileID(), e);
|
||||
return file;
|
||||
}
|
||||
})
|
||||
.toList());
|
||||
}
|
||||
}
|
||||
@@ -38,6 +38,7 @@ import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
import java.util.Optional;
|
||||
import java.util.Set;
|
||||
|
||||
public class McbbsModpackManifest implements ModpackManifest, Validation {
|
||||
public static final String MANIFEST_TYPE = "minecraftModpack";
|
||||
@@ -424,7 +425,12 @@ public class McbbsModpackManifest implements ModpackManifest, Validation {
|
||||
.orElseThrow(() -> new IOException("Cannot find game version")).getVersion();
|
||||
return new Modpack(name, author, version, gameVersion, description, encoding, this) {
|
||||
@Override
|
||||
public Task<?> getInstallTask(DefaultDependencyManager dependencyManager, Path zipFile, GameInstanceID instanceId, String iconUrl) {
|
||||
public Task<?> getInstallTask(
|
||||
DefaultDependencyManager dependencyManager,
|
||||
Path zipFile,
|
||||
GameInstanceID instanceId,
|
||||
String iconUrl,
|
||||
@Nullable Set<String> excludedFiles) {
|
||||
return new McbbsModpackLocalInstallTask(dependencyManager, zipFile, this, McbbsModpackManifest.this, instanceId);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -31,6 +31,9 @@ import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.nio.charset.Charset;
|
||||
import java.nio.file.Path;
|
||||
import java.util.Set;
|
||||
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
public final class McbbsModpackProvider implements ModpackProvider {
|
||||
public static final McbbsModpackProvider INSTANCE = new McbbsModpackProvider();
|
||||
@@ -46,7 +49,12 @@ public final class McbbsModpackProvider implements ModpackProvider {
|
||||
}
|
||||
|
||||
@Override
|
||||
public Task<?> createUpdateTask(DefaultDependencyManager dependencyManager, DefaultGameInstance instance, Path zipFile, Modpack modpack) throws MismatchedModpackTypeException {
|
||||
public Task<?> createUpdateTask(
|
||||
DefaultDependencyManager dependencyManager,
|
||||
DefaultGameInstance instance,
|
||||
Path zipFile,
|
||||
Modpack modpack,
|
||||
@Nullable Set<String> excludedFiles) throws MismatchedModpackTypeException {
|
||||
if (!(modpack.getManifest() instanceof McbbsModpackManifest mcbbsModpackManifest))
|
||||
throw new MismatchedModpackTypeException(getName(), modpack.getManifest().getProvider().getName());
|
||||
|
||||
|
||||
+34
-9
@@ -17,9 +17,9 @@
|
||||
*/
|
||||
package org.jackhuang.hmcl.modpack.modrinth;
|
||||
|
||||
import org.jackhuang.hmcl.addon.mod.ModManager;
|
||||
import org.jackhuang.hmcl.download.DefaultDependencyManager;
|
||||
import org.jackhuang.hmcl.game.DefaultGameInstance;
|
||||
import org.jackhuang.hmcl.addon.mod.ModManager;
|
||||
import org.jackhuang.hmcl.modpack.ModpackCompletionException;
|
||||
import org.jackhuang.hmcl.task.FileDownloadTask;
|
||||
import org.jackhuang.hmcl.task.Task;
|
||||
@@ -35,6 +35,8 @@ import java.nio.file.Path;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
|
||||
@@ -56,6 +58,9 @@ public class ModrinthCompletionTask extends Task<Void> {
|
||||
/// The manifest supplied by the caller or loaded from disk, if available.
|
||||
private @Nullable ModrinthManifest manifest;
|
||||
|
||||
/// Keys of optional files the user chose not to install; `null` means install all.
|
||||
private @Nullable Set<String> excludedFiles;
|
||||
|
||||
/// Download tasks produced during [#execute()].
|
||||
private final List<Task<?>> dependencies = new ArrayList<>();
|
||||
|
||||
@@ -73,7 +78,7 @@ public class ModrinthCompletionTask extends Task<Void> {
|
||||
/// @param dependencyManager the dependency manager
|
||||
/// @param instance the registered instance to complete
|
||||
public ModrinthCompletionTask(DefaultDependencyManager dependencyManager, DefaultGameInstance instance) {
|
||||
this(dependencyManager, instance, null);
|
||||
this(dependencyManager, instance, null, null);
|
||||
}
|
||||
|
||||
/// Creates a task that completes the installed Modrinth modpack using an optional manifest.
|
||||
@@ -81,21 +86,33 @@ public class ModrinthCompletionTask extends Task<Void> {
|
||||
/// @param dependencyManager the dependency manager
|
||||
/// @param instance the registered instance to complete
|
||||
/// @param manifest the Modrinth manifest, or `null` to read it from disk
|
||||
/// @param excludedFiles keys of optional files the user chose not to install; `null` means install all.
|
||||
/// When non-null, must not contain `null` elements.
|
||||
public ModrinthCompletionTask(
|
||||
DefaultDependencyManager dependencyManager,
|
||||
DefaultGameInstance instance,
|
||||
@Nullable ModrinthManifest manifest) {
|
||||
@Nullable ModrinthManifest manifest,
|
||||
@Nullable Set<String> excludedFiles) {
|
||||
dependencyManager.validateGameInstance(instance);
|
||||
this.dependency = dependencyManager;
|
||||
this.instance = instance;
|
||||
this.modManager = instance.getModManager();
|
||||
this.manifest = manifest;
|
||||
this.excludedFiles = excludedFiles == null ? null : Set.copyOf(excludedFiles);
|
||||
|
||||
if (manifest == null)
|
||||
try {
|
||||
Path manifestFile = instance.getInstanceRoot().resolve("modrinth.index.json");
|
||||
Path root = instance.getInstanceRoot();
|
||||
Path manifestFile = root.resolve("modrinth.index.json");
|
||||
if (Files.exists(manifestFile))
|
||||
this.manifest = JsonUtils.fromJsonFile(manifestFile, ModrinthManifest.class);
|
||||
Path excludedFile = root.resolve("excluded.json");
|
||||
if (Files.exists(excludedFile)) {
|
||||
this.excludedFiles = Set.copyOf(Objects.requireNonNull(
|
||||
JsonUtils.fromJsonFile(excludedFile, JsonUtils.listTypeOf(String.class))));
|
||||
} else {
|
||||
this.excludedFiles = null;
|
||||
}
|
||||
} catch (Exception e) {
|
||||
LOG.warning("Unable to read Modrinth modpack manifest.json", e);
|
||||
}
|
||||
@@ -121,15 +138,23 @@ public class ModrinthCompletionTask extends Task<Void> {
|
||||
Path runDirectory = FileUtils.toAbsolute(instance.getRunDirectory());
|
||||
Path modsDirectory = runDirectory.resolve("mods");
|
||||
|
||||
if (excludedFiles != null) {
|
||||
JsonUtils.writeToJsonFile(
|
||||
instance.getInstanceRoot().resolve("excluded.json"),
|
||||
List.copyOf(excludedFiles));
|
||||
}
|
||||
|
||||
for (ModrinthManifest.File file : manifest.getFiles()) {
|
||||
if (file.getEnv() != null && file.getEnv().getOrDefault("client", "required").equals("unsupported"))
|
||||
if (file.env() != null && file.env().getOrDefault("client", "required").equals("unsupported"))
|
||||
continue;
|
||||
if (file.getDownloads().isEmpty())
|
||||
if (file.downloads().isEmpty())
|
||||
continue;
|
||||
if (excludedFiles != null && excludedFiles.contains(file.key()))
|
||||
continue;
|
||||
|
||||
Path filePath = runDirectory.resolve(file.getPath()).toAbsolutePath().normalize();
|
||||
Path filePath = runDirectory.resolve(file.path()).toAbsolutePath().normalize();
|
||||
if (!filePath.startsWith(runDirectory))
|
||||
throw new IOException("Unsecure path: " + file.getPath());
|
||||
throw new IOException("Unsecure path: " + file.path());
|
||||
|
||||
if (Files.exists(filePath))
|
||||
continue;
|
||||
@@ -137,7 +162,7 @@ public class ModrinthCompletionTask extends Task<Void> {
|
||||
continue;
|
||||
|
||||
var task = new FileDownloadTask(
|
||||
dependency.getDownloadProvider().injectURLsWithCandidates(file.getDownloads()),
|
||||
dependency.getDownloadProvider().injectURLsWithCandidates(file.downloads()),
|
||||
filePath);
|
||||
task.setCacheRepository(dependency.getCacheRepository());
|
||||
task.setCaching(true);
|
||||
|
||||
+20
-7
@@ -57,6 +57,9 @@ public class ModrinthInstallTask extends Task<Void> {
|
||||
/// Previous modpack configuration when updating, or `null` for a new installation.
|
||||
private final @Nullable ModpackConfiguration<ModrinthManifest> config;
|
||||
|
||||
/// Keys of optional files the user chose not to install; `null` or empty means install all.
|
||||
private final @Nullable Set<String> excludedFiles;
|
||||
|
||||
/// Validated extension of the scheduled icon download, or `null` when no icon is scheduled.
|
||||
private @Nullable String iconExt;
|
||||
|
||||
@@ -73,6 +76,8 @@ public class ModrinthInstallTask extends Task<Void> {
|
||||
/// @param manifest the Modrinth index
|
||||
/// @param instanceId the id of the new instance
|
||||
/// @param iconUrl the optional icon URL, or `null`
|
||||
/// @param excludedFiles keys of optional files the user chose not to install; `null` or empty means
|
||||
/// install all files. When non-null, must not contain `null` elements.
|
||||
/// @throws IllegalStateException if the manifest declares an unsupported mod loader, the target
|
||||
/// cannot be reserved, or another repository draft is open
|
||||
public ModrinthInstallTask(
|
||||
@@ -81,8 +86,9 @@ public class ModrinthInstallTask extends Task<Void> {
|
||||
Modpack modpack,
|
||||
ModrinthManifest manifest,
|
||||
GameInstanceID instanceId,
|
||||
@Nullable String iconUrl) {
|
||||
this(dependencyManager, zipFile, modpack, manifest, instanceId, null, iconUrl);
|
||||
@Nullable String iconUrl,
|
||||
@Nullable Set<String> excludedFiles) {
|
||||
this(dependencyManager, zipFile, modpack, manifest, instanceId, null, iconUrl, excludedFiles);
|
||||
}
|
||||
|
||||
/// Creates a task that updates an existing Modrinth modpack instance.
|
||||
@@ -93,6 +99,8 @@ public class ModrinthInstallTask extends Task<Void> {
|
||||
/// @param manifest the Modrinth index
|
||||
/// @param instance the existing instance to update
|
||||
/// @param iconUrl the optional icon URL, or `null`
|
||||
/// @param excludedFiles keys of optional files the user chose not to install; `null` or empty means
|
||||
/// install all files. When non-null, must not contain `null` elements.
|
||||
/// @throws IllegalArgumentException if `instance` belongs to another repository, has no
|
||||
/// modpack configuration, or records another provider type
|
||||
/// @throws IllegalStateException if the manifest declares an unsupported mod loader,
|
||||
@@ -104,8 +112,9 @@ public class ModrinthInstallTask extends Task<Void> {
|
||||
Modpack modpack,
|
||||
ModrinthManifest manifest,
|
||||
DefaultGameInstance instance,
|
||||
@Nullable String iconUrl) {
|
||||
this(dependencyManager, zipFile, modpack, manifest, instance.getId(), instance, iconUrl);
|
||||
@Nullable String iconUrl,
|
||||
@Nullable Set<String> excludedFiles) {
|
||||
this(dependencyManager, zipFile, modpack, manifest, instance.getId(), instance, iconUrl, excludedFiles);
|
||||
}
|
||||
|
||||
/// Creates a Modrinth installation task in the mode selected by `updateTarget`.
|
||||
@@ -117,6 +126,8 @@ public class ModrinthInstallTask extends Task<Void> {
|
||||
/// @param instanceId the target instance id
|
||||
/// @param updateTarget the existing instance selecting update mode, or `null` for install
|
||||
/// @param iconUrl the optional icon URL, or `null`
|
||||
/// @param excludedFiles keys of optional files the user chose not to install; `null` or empty means
|
||||
/// install all files. When non-null, must not contain `null` elements.
|
||||
/// @throws IllegalArgumentException if an update target has no compatible configuration
|
||||
/// @throws IllegalStateException if the manifest declares an unsupported mod loader, the
|
||||
/// target cannot be reserved, an update target is not the exact
|
||||
@@ -128,7 +139,8 @@ public class ModrinthInstallTask extends Task<Void> {
|
||||
ModrinthManifest manifest,
|
||||
GameInstanceID instanceId,
|
||||
@Nullable DefaultGameInstance updateTarget,
|
||||
@Nullable String iconUrl) {
|
||||
@Nullable String iconUrl,
|
||||
@Nullable Set<String> excludedFiles) {
|
||||
this.dependencyManager = dependencyManager;
|
||||
this.zipFile = zipFile;
|
||||
this.modpack = modpack;
|
||||
@@ -136,6 +148,7 @@ public class ModrinthInstallTask extends Task<Void> {
|
||||
this.instanceId = instanceId;
|
||||
this.updateTarget = updateTarget;
|
||||
this.iconUrl = iconUrl;
|
||||
this.excludedFiles = excludedFiles == null ? null : Set.copyOf(excludedFiles);
|
||||
this.repository = dependencyManager.getGameRepository();
|
||||
this.run = repository.getLayout().getInstanceRoot(instanceId);
|
||||
|
||||
@@ -224,7 +237,7 @@ public class ModrinthInstallTask extends Task<Void> {
|
||||
if (config != null) {
|
||||
// For update, remove mods not listed in new manifest
|
||||
for (ModrinthManifest.File oldManifestFile : config.getManifest().getFiles()) {
|
||||
Path oldFile = run.resolve(oldManifestFile.getPath());
|
||||
Path oldFile = run.resolve(oldManifestFile.path());
|
||||
if (!Files.exists(oldFile)) continue;
|
||||
if (manifest.getFiles().stream().noneMatch(oldManifestFile::equals)) {
|
||||
Files.deleteIfExists(oldFile);
|
||||
@@ -249,6 +262,6 @@ public class ModrinthInstallTask extends Task<Void> {
|
||||
}
|
||||
|
||||
// The game builder runs as a dependent and registers the instance before this phase.
|
||||
dependencies.add(new ModrinthCompletionTask(dependencyManager, repository.getInstance(instanceId), manifest));
|
||||
dependencies.add(new ModrinthCompletionTask(dependencyManager, repository.getInstance(instanceId), manifest, excludedFiles));
|
||||
}
|
||||
}
|
||||
@@ -18,26 +18,58 @@
|
||||
package org.jackhuang.hmcl.modpack.modrinth;
|
||||
|
||||
import com.google.gson.JsonParseException;
|
||||
import org.jackhuang.hmcl.addon.RemoteAddon;
|
||||
import org.jackhuang.hmcl.modpack.ModpackFile;
|
||||
import org.jackhuang.hmcl.modpack.ModpackManifest;
|
||||
import org.jackhuang.hmcl.modpack.ModpackProvider;
|
||||
import org.jackhuang.hmcl.util.DigestUtils;
|
||||
import org.jackhuang.hmcl.util.StringUtils;
|
||||
import org.jackhuang.hmcl.util.gson.JsonSerializable;
|
||||
import org.jackhuang.hmcl.util.gson.TolerableValidationException;
|
||||
import org.jackhuang.hmcl.util.gson.Validation;
|
||||
import org.jetbrains.annotations.NotNullByDefault;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
import org.jetbrains.annotations.Unmodifiable;
|
||||
|
||||
import java.nio.file.Path;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
|
||||
public class ModrinthManifest implements ModpackManifest, Validation {
|
||||
/// Modrinth modpack index (`modrinth.index.json`).
|
||||
@NotNullByDefault
|
||||
public class ModrinthManifest implements ModpackManifest, ModpackManifest.SupportOptional, Validation {
|
||||
|
||||
/// The game id, typically `minecraft`.
|
||||
private final String game;
|
||||
|
||||
/// The Modrinth index format version.
|
||||
private final int formatVersion;
|
||||
|
||||
/// The pack version id.
|
||||
private final String versionId;
|
||||
|
||||
/// The pack display name.
|
||||
private final String name;
|
||||
|
||||
/// The pack summary, or `null`.
|
||||
private final @Nullable String summary;
|
||||
|
||||
/// Declared files in the pack.
|
||||
private final List<File> files;
|
||||
|
||||
/// Dependency versions such as `minecraft` and loaders.
|
||||
private final Map<String, String> dependencies;
|
||||
|
||||
/// Creates a Modrinth index.
|
||||
///
|
||||
/// @param game the game id
|
||||
/// @param formatVersion the format version
|
||||
/// @param versionId the pack version id
|
||||
/// @param name the pack name
|
||||
/// @param summary the summary, or `null`
|
||||
/// @param files the files
|
||||
/// @param dependencies the dependency map
|
||||
public ModrinthManifest(String game, int formatVersion, String versionId, String name, @Nullable String summary, List<File> files, Map<String, String> dependencies) {
|
||||
this.game = game;
|
||||
this.formatVersion = formatVersion;
|
||||
@@ -48,34 +80,64 @@ public class ModrinthManifest implements ModpackManifest, Validation {
|
||||
this.dependencies = dependencies;
|
||||
}
|
||||
|
||||
/// Returns the game id.
|
||||
///
|
||||
/// @return the game id
|
||||
public String getGame() {
|
||||
return game;
|
||||
}
|
||||
|
||||
/// Returns the format version.
|
||||
///
|
||||
/// @return the format version
|
||||
public int getFormatVersion() {
|
||||
return formatVersion;
|
||||
}
|
||||
|
||||
/// Returns the pack version id.
|
||||
///
|
||||
/// @return the version id
|
||||
public String getVersionId() {
|
||||
return versionId;
|
||||
}
|
||||
|
||||
/// Returns the pack name.
|
||||
///
|
||||
/// @return the name
|
||||
public String getName() {
|
||||
return name;
|
||||
}
|
||||
|
||||
/// Returns the summary, or an empty string when absent.
|
||||
///
|
||||
/// @return the summary
|
||||
public String getSummary() {
|
||||
return summary == null ? "" : summary;
|
||||
}
|
||||
|
||||
public List<File> getFiles() {
|
||||
@Override
|
||||
public @Unmodifiable List<File> getFiles() {
|
||||
return files;
|
||||
}
|
||||
|
||||
/// Returns a copy with a different file list.
|
||||
///
|
||||
/// @param files the new files
|
||||
/// @return the updated manifest
|
||||
public ModrinthManifest withFiles(List<File> files) {
|
||||
return new ModrinthManifest(game, formatVersion, versionId, name, summary, files, dependencies);
|
||||
}
|
||||
|
||||
/// Returns dependency versions such as Minecraft and loaders.
|
||||
///
|
||||
/// @return the dependency map
|
||||
public Map<String, String> getDependencies() {
|
||||
return dependencies;
|
||||
}
|
||||
|
||||
/// Returns the Minecraft version from dependencies.
|
||||
///
|
||||
/// @return the Minecraft version
|
||||
public String getGameVersion() {
|
||||
return dependencies.get("minecraft");
|
||||
}
|
||||
@@ -92,49 +154,75 @@ public class ModrinthManifest implements ModpackManifest, Validation {
|
||||
}
|
||||
}
|
||||
|
||||
public static class File {
|
||||
private final String path;
|
||||
private final Map<String, String> hashes;
|
||||
@Nullable
|
||||
private final Map<String, String> env;
|
||||
private final List<String> downloads;
|
||||
private final int fileSize;
|
||||
/// A file entry in a Modrinth index.
|
||||
@JsonSerializable
|
||||
@NotNullByDefault
|
||||
public record File(
|
||||
String path,
|
||||
Map<String, String> hashes,
|
||||
@Nullable Map<String, String> env,
|
||||
List<String> downloads,
|
||||
int fileSize,
|
||||
@Nullable RemoteAddon remoteAddon,
|
||||
boolean addonQueried) implements Validation, ModpackFile {
|
||||
|
||||
/// Creates a file entry that has not been queried for remote addon metadata.
|
||||
///
|
||||
/// @param path the relative path
|
||||
/// @param hashes the hashes
|
||||
/// @param env the environment map, or `null`
|
||||
/// @param downloads the download URLs
|
||||
/// @param fileSize the file size
|
||||
public File(String path, Map<String, String> hashes, @Nullable Map<String, String> env, List<String> downloads, int fileSize) {
|
||||
this.path = path;
|
||||
this.hashes = hashes;
|
||||
this.env = env;
|
||||
this.downloads = downloads;
|
||||
this.fileSize = fileSize;
|
||||
this(path, hashes, env, downloads, fileSize, null, false);
|
||||
}
|
||||
|
||||
public String getPath() {
|
||||
return path;
|
||||
@Override
|
||||
public void validate() throws JsonParseException {
|
||||
if (StringUtils.isBlank(path))
|
||||
throw new JsonParseException("Modrinth file path is missing.");
|
||||
Path normalizedPath = Path.of(path).normalize();
|
||||
if (normalizedPath.isAbsolute() || normalizedPath.startsWith(".."))
|
||||
throw new JsonParseException("Modrinth file path escapes the instance directory: " + path);
|
||||
if (hashes == null || !DigestUtils.isSha512Digest(hashes.get("sha512")))
|
||||
throw new JsonParseException("Modrinth file sha512 is missing or invalid.");
|
||||
if (env != null && !env.containsKey("client"))
|
||||
throw new JsonParseException("Modrinth file env must contain a client key when present.");
|
||||
if (downloads == null || downloads.isEmpty())
|
||||
throw new JsonParseException("Modrinth file downloads are missing.");
|
||||
}
|
||||
|
||||
public Map<String, String> getHashes() {
|
||||
return hashes;
|
||||
@Override
|
||||
public String key() {
|
||||
return "modrinth:" + Objects.requireNonNull(hashes.get("sha512"), "sha512") + ":" + path;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public Map<String, String> getEnv() {
|
||||
return env;
|
||||
@Override
|
||||
public String fileName() {
|
||||
return Path.of(path).getFileName().toString();
|
||||
}
|
||||
|
||||
public List<String> getDownloads() {
|
||||
return downloads;
|
||||
/// Returns a copy marked as queried, with the given remote addon metadata.
|
||||
///
|
||||
/// @param remoteAddon the remote addon, or `null` when not found
|
||||
/// @return the updated file entry
|
||||
public File withAddon(@Nullable RemoteAddon remoteAddon) {
|
||||
return new File(path, hashes, env, downloads, fileSize, remoteAddon, true);
|
||||
}
|
||||
|
||||
public int getFileSize() {
|
||||
return fileSize;
|
||||
@Override
|
||||
public boolean optional() {
|
||||
return env != null && "optional".equals(env.get("client"));
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if (this == o) return true;
|
||||
if (o == null || getClass() != o.getClass()) return false;
|
||||
File file = (File) o;
|
||||
return fileSize == file.fileSize && path.equals(file.path) && hashes.equals(file.hashes) && Objects.equals(this.env, file.env) && downloads.equals(file.downloads);
|
||||
return this == o || o instanceof File file
|
||||
&& fileSize == file.fileSize
|
||||
&& path.equals(file.path)
|
||||
&& hashes.equals(file.hashes)
|
||||
&& Objects.equals(env, file.env)
|
||||
&& downloads.equals(file.downloads);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
+65
-12
@@ -19,20 +19,26 @@ package org.jackhuang.hmcl.modpack.modrinth;
|
||||
|
||||
import com.google.gson.JsonParseException;
|
||||
import kala.compress.archivers.zip.ZipArchiveReader;
|
||||
import org.jackhuang.hmcl.addon.RemoteAddon;
|
||||
import org.jackhuang.hmcl.addon.repository.ModrinthRemoteAddonRepository;
|
||||
import org.jackhuang.hmcl.download.DefaultDependencyManager;
|
||||
import org.jackhuang.hmcl.download.DownloadProvider;
|
||||
import org.jackhuang.hmcl.game.DefaultGameInstance;
|
||||
import org.jackhuang.hmcl.game.GameInstanceID;
|
||||
import org.jackhuang.hmcl.modpack.MismatchedModpackTypeException;
|
||||
import org.jackhuang.hmcl.modpack.Modpack;
|
||||
import org.jackhuang.hmcl.modpack.ModpackProvider;
|
||||
import org.jackhuang.hmcl.modpack.ModpackUpdateTask;
|
||||
import org.jackhuang.hmcl.modpack.*;
|
||||
import org.jackhuang.hmcl.task.Task;
|
||||
import org.jackhuang.hmcl.util.gson.JsonUtils;
|
||||
import org.jackhuang.hmcl.util.io.CompressingUtils;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import java.io.FileNotFoundException;
|
||||
import java.io.IOException;
|
||||
import java.io.PrintStream;
|
||||
import java.nio.charset.Charset;
|
||||
import java.nio.file.Path;
|
||||
import java.util.Set;
|
||||
|
||||
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
|
||||
|
||||
public final class ModrinthModpackProvider implements ModpackProvider {
|
||||
public static final ModrinthModpackProvider INSTANCE = new ModrinthModpackProvider();
|
||||
@@ -48,22 +54,69 @@ public final class ModrinthModpackProvider implements ModpackProvider {
|
||||
}
|
||||
|
||||
@Override
|
||||
public Task<?> createUpdateTask(DefaultDependencyManager dependencyManager, DefaultGameInstance instance, Path zipFile, Modpack modpack) throws MismatchedModpackTypeException {
|
||||
public Task<?> createUpdateTask(
|
||||
DefaultDependencyManager dependencyManager,
|
||||
DefaultGameInstance instance,
|
||||
Path zipFile,
|
||||
Modpack modpack,
|
||||
@Nullable Set<String> excludedFiles) throws MismatchedModpackTypeException {
|
||||
if (!(modpack.getManifest() instanceof ModrinthManifest modrinthManifest))
|
||||
throw new MismatchedModpackTypeException(getName(), modpack.getManifest().getProvider().getName());
|
||||
|
||||
return new ModpackUpdateTask(instance, new ModrinthInstallTask(dependencyManager, zipFile, modpack, modrinthManifest, instance, null));
|
||||
return new ModpackUpdateTask(instance, new ModrinthInstallTask(dependencyManager, zipFile, modpack, modrinthManifest, instance, null, excludedFiles));
|
||||
}
|
||||
|
||||
@Override
|
||||
public Modpack readManifest(ZipArchiveReader zip, Path file, Charset encoding) throws IOException, JsonParseException {
|
||||
ModrinthManifest manifest = JsonUtils.fromNonNullJson(CompressingUtils.readTextZipEntry(zip, "modrinth.index.json"), ModrinthManifest.class);
|
||||
return new Modpack(manifest.getName(), "", manifest.getVersionId(), manifest.getGameVersion(), manifest.getSummary(), encoding, manifest) {
|
||||
@Override
|
||||
public Task<?> getInstallTask(DefaultDependencyManager dependencyManager, Path zipFile, GameInstanceID instanceId, String iconUrl) {
|
||||
return new ModrinthInstallTask(dependencyManager, zipFile, this, manifest, instanceId, iconUrl);
|
||||
try {
|
||||
ModrinthManifest manifest = JsonUtils.fromNonNullJson(CompressingUtils.readTextZipEntry(zip, "modrinth.index.json"), ModrinthManifest.class);
|
||||
|
||||
return new Modpack(manifest.getName(), "", manifest.getVersionId(), manifest.getGameVersion(), manifest.getSummary(), encoding, manifest) {
|
||||
@Override
|
||||
public Task<?> getInstallTask(
|
||||
DefaultDependencyManager dependencyManager,
|
||||
Path zipFile,
|
||||
GameInstanceID instanceId,
|
||||
String iconUrl,
|
||||
@Nullable Set<String> excludedFiles) {
|
||||
return new ModrinthInstallTask(dependencyManager, zipFile, this, manifest, instanceId, iconUrl, excludedFiles);
|
||||
}
|
||||
};
|
||||
} catch (IOException | JsonParseException ex) {
|
||||
try (var os = new PrintStream("/dev/stdout")) {
|
||||
ex.printStackTrace(os);
|
||||
}
|
||||
};
|
||||
throw ex;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public ModpackManifest loadFiles(DownloadProvider downloadProvider, ModpackManifest manifest1) {
|
||||
if (!(manifest1 instanceof ModrinthManifest manifest))
|
||||
throw new IllegalArgumentException("Manifest is not a ModrinthManifest");
|
||||
return manifest.withFiles(manifest.getFiles().parallelStream().map(file -> {
|
||||
if (file.optional() && !file.addonQueried()) {
|
||||
try {
|
||||
String sha1 = file.hashes().get("sha1");
|
||||
if (sha1 == null) {
|
||||
return file.withAddon(null);
|
||||
}
|
||||
RemoteAddon.Version version = ModrinthRemoteAddonRepository.MODS.getRemoteVersionBySHA1(sha1).orElse(null);
|
||||
if (version == null) {
|
||||
return file.withAddon(null);
|
||||
}
|
||||
RemoteAddon addon = ModrinthRemoteAddonRepository.MODS.getAddonById(downloadProvider, version.projectId());
|
||||
return file.withAddon(addon);
|
||||
} catch (FileNotFoundException fof) {
|
||||
LOG.warning("Could not query modrinth for deleted mods: " + file.fileName(), fof);
|
||||
return file;
|
||||
} catch (IOException | JsonParseException e) {
|
||||
LOG.warning("Unable to fetch the modid for " + file.fileName(), e);
|
||||
return file;
|
||||
}
|
||||
} else {
|
||||
return file;
|
||||
}
|
||||
}).toList());
|
||||
}
|
||||
}
|
||||
+13
-2
@@ -34,6 +34,7 @@ import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.nio.charset.Charset;
|
||||
import java.nio.file.Path;
|
||||
import java.util.Set;
|
||||
|
||||
public final class MultiMCModpackProvider implements ModpackProvider {
|
||||
public static final MultiMCModpackProvider INSTANCE = new MultiMCModpackProvider();
|
||||
@@ -49,7 +50,12 @@ public final class MultiMCModpackProvider implements ModpackProvider {
|
||||
}
|
||||
|
||||
@Override
|
||||
public Task<?> createUpdateTask(DefaultDependencyManager dependencyManager, DefaultGameInstance instance, Path zipFile, Modpack modpack) throws MismatchedModpackTypeException {
|
||||
public Task<?> createUpdateTask(
|
||||
DefaultDependencyManager dependencyManager,
|
||||
DefaultGameInstance instance,
|
||||
Path zipFile,
|
||||
Modpack modpack,
|
||||
@Nullable Set<String> excludedFiles) throws MismatchedModpackTypeException {
|
||||
if (!(modpack.getManifest() instanceof MultiMCInstanceConfiguration multiMCInstanceConfiguration))
|
||||
throw new MismatchedModpackTypeException(getName(), modpack.getManifest().getProvider().getName());
|
||||
|
||||
@@ -88,7 +94,12 @@ public final class MultiMCModpackProvider implements ModpackProvider {
|
||||
MultiMCInstanceConfiguration cfg = new MultiMCInstanceConfiguration(name, instanceStream, manifest);
|
||||
return new Modpack(cfg.getName(), "", "", cfg.getGameVersion(), cfg.getNotes(), encoding, cfg) {
|
||||
@Override
|
||||
public Task<?> getInstallTask(DefaultDependencyManager dependencyManager, Path zipFile, GameInstanceID instanceId, String iconUrl) {
|
||||
public Task<?> getInstallTask(
|
||||
DefaultDependencyManager dependencyManager,
|
||||
Path zipFile,
|
||||
GameInstanceID instanceId,
|
||||
String iconUrl,
|
||||
@Nullable Set<String> excludedFiles) {
|
||||
return new MultiMCModpackInstallTask(dependencyManager, zipFile, this, cfg, instanceId);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -28,12 +28,14 @@ import org.jackhuang.hmcl.modpack.ModpackProvider;
|
||||
import org.jackhuang.hmcl.task.Task;
|
||||
import org.jackhuang.hmcl.util.gson.TolerableValidationException;
|
||||
import org.jackhuang.hmcl.util.gson.Validation;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.charset.Charset;
|
||||
import java.nio.file.Path;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
|
||||
public class ServerModpackManifest implements ModpackManifest, Validation {
|
||||
private final String name;
|
||||
@@ -126,7 +128,12 @@ public class ServerModpackManifest implements ModpackManifest, Validation {
|
||||
.orElseThrow(() -> new IOException("Cannot find game version")).getVersion();
|
||||
return new Modpack(name, author, version, gameVersion, description, encoding, this) {
|
||||
@Override
|
||||
public Task<?> getInstallTask(DefaultDependencyManager dependencyManager, Path zipFile, GameInstanceID instanceId, String iconUrl) {
|
||||
public Task<?> getInstallTask(
|
||||
DefaultDependencyManager dependencyManager,
|
||||
Path zipFile,
|
||||
GameInstanceID instanceId,
|
||||
String iconUrl,
|
||||
@Nullable Set<String> excludedFiles) {
|
||||
return new ServerModpackLocalInstallTask(dependencyManager, zipFile, this, ServerModpackManifest.this, instanceId);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -28,10 +28,12 @@ import org.jackhuang.hmcl.modpack.ModpackUpdateTask;
|
||||
import org.jackhuang.hmcl.task.Task;
|
||||
import org.jackhuang.hmcl.util.gson.JsonUtils;
|
||||
import org.jackhuang.hmcl.util.io.CompressingUtils;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.charset.Charset;
|
||||
import java.nio.file.Path;
|
||||
import java.util.Set;
|
||||
|
||||
public final class ServerModpackProvider implements ModpackProvider {
|
||||
public static final ServerModpackProvider INSTANCE = new ServerModpackProvider();
|
||||
@@ -47,7 +49,12 @@ public final class ServerModpackProvider implements ModpackProvider {
|
||||
}
|
||||
|
||||
@Override
|
||||
public Task<?> createUpdateTask(DefaultDependencyManager dependencyManager, DefaultGameInstance instance, Path zipFile, Modpack modpack) throws MismatchedModpackTypeException {
|
||||
public Task<?> createUpdateTask(
|
||||
DefaultDependencyManager dependencyManager,
|
||||
DefaultGameInstance instance,
|
||||
Path zipFile,
|
||||
Modpack modpack,
|
||||
@Nullable Set<String> excludedFiles) throws MismatchedModpackTypeException {
|
||||
if (!(modpack.getManifest() instanceof ServerModpackManifest serverModpackManifest))
|
||||
throw new MismatchedModpackTypeException(getName(), modpack.getManifest().getProvider().getName());
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -48,6 +48,23 @@ public final class DigestUtils {
|
||||
return true;
|
||||
}
|
||||
|
||||
/// Returns whether `digest` is a 512-bit hexadecimal SHA-512 digest (128 hex digits).
|
||||
///
|
||||
/// @param digest the digest string to check, or `null`
|
||||
/// @return `true` when `digest` is a valid SHA-512 hex digest
|
||||
public static boolean isSha512Digest(String digest) {
|
||||
if (digest == null || digest.length() != 128) return false;
|
||||
|
||||
for (int i = 0; i < digest.length(); i++) {
|
||||
char ch = digest.charAt(i);
|
||||
if ((ch < '0' || ch > '9') && (ch < 'a' || ch > 'f') && (ch < 'A' || ch > 'F')) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public static MessageDigest getDigest(String algorithm) {
|
||||
try {
|
||||
return MessageDigest.getInstance(algorithm);
|
||||
|
||||
@@ -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);
|
||||
|
||||
}
|
||||
@@ -949,4 +949,5 @@
|
||||
26.3-pre-3
|
||||
26.3-rc-1
|
||||
26.3-rc-2
|
||||
26.3-rc-3
|
||||
26.3
|
||||
@@ -48,6 +48,7 @@ import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.Optional;
|
||||
import java.util.Set;
|
||||
import java.util.zip.ZipEntry;
|
||||
import java.util.zip.ZipFile;
|
||||
import java.util.zip.ZipOutputStream;
|
||||
@@ -709,7 +710,8 @@ public final class DefaultGameInstanceTest {
|
||||
DefaultDependencyManager dependencyManager,
|
||||
Path zipFile,
|
||||
GameInstanceID instanceId,
|
||||
String iconUrl) {
|
||||
String iconUrl,
|
||||
@Nullable Set<String> excludedFiles) {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
};
|
||||
|
||||
@@ -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