Compare commits
44
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2f5594d8c1 | ||
|
|
13ad50208f | ||
|
|
13910c24b7 | ||
|
|
cc9bdb62ca | ||
|
|
baa7c816b7 | ||
|
|
090f0b9822 | ||
|
|
6d9ae24912 | ||
|
|
d03851b196 | ||
|
|
97cf9dcf16 | ||
|
|
7bacc2bb5a | ||
|
|
c7637079d7 | ||
|
|
8f3282e9a2 | ||
|
|
8e491862b9 | ||
|
|
e0c276075c | ||
|
|
19ea64e14b | ||
|
|
5b26b8f4eb | ||
|
|
ed7ef92ad9 | ||
|
|
cd95891108 | ||
|
|
df52bc6e81 | ||
|
|
9d9fc78384 | ||
|
|
e287f672fd | ||
|
|
24702dc5a0 | ||
|
|
886a25a935 | ||
|
|
450504ceda | ||
|
|
8767cc0e98 | ||
|
|
7331f619cf | ||
|
|
7a8f931641 | ||
|
|
ce51dc4dd7 | ||
|
|
b97e7fbd55 | ||
|
|
7f578be08e | ||
|
|
513f53bc06 | ||
|
|
b2d4d36856 | ||
|
|
9c031c5ac1 | ||
|
|
e2d3ac847d | ||
|
|
0f4152406c | ||
|
|
1a258a2554 | ||
|
|
3742be0aa8 | ||
|
|
0e34554344 | ||
|
|
1acfc55004 | ||
|
|
1428183e36 | ||
|
|
cbc6daf3d5 | ||
|
|
0ad180c082 | ||
|
|
1da8c8d6ac | ||
|
|
ea232b0976 |
No files matched your search
@@ -4,6 +4,8 @@
|
||||
hs_err_pid*
|
||||
*.hprof
|
||||
|
||||
*.jfr
|
||||
|
||||
.gradle
|
||||
gradle.properties
|
||||
|
||||
|
||||
+12
-2
@@ -4,6 +4,7 @@ import org.jackhuang.hmcl.gradle.ci.JenkinsUtils
|
||||
import org.jackhuang.hmcl.gradle.l10n.CheckTranslations
|
||||
import org.jackhuang.hmcl.gradle.l10n.CreateLanguageList
|
||||
import org.jackhuang.hmcl.gradle.l10n.CreateLocaleNamesResourceBundle
|
||||
import org.jackhuang.hmcl.gradle.l10n.SyncTranslations
|
||||
import org.jackhuang.hmcl.gradle.l10n.UpsideDownTranslate
|
||||
import org.jackhuang.hmcl.gradle.mod.ParseModDataTask
|
||||
import org.jackhuang.hmcl.gradle.pack.CreateDeb
|
||||
@@ -430,13 +431,22 @@ tasks.register<CheckTranslations>("checkTranslations") {
|
||||
val dir = layout.projectDirectory.dir("src/main/resources/assets/lang")
|
||||
|
||||
englishFile.set(dir.file("I18N.properties"))
|
||||
simplifiedChineseFile.set(dir.file("I18N_zh_CN.properties"))
|
||||
traditionalChineseFile.set(dir.file("I18N_zh.properties"))
|
||||
simplifiedChineseFile.set(dir.file("I18N_zh_Hans.properties"))
|
||||
traditionalChineseFile.set(dir.file("I18N_zh_Hant.properties"))
|
||||
classicalChineseFile.set(dir.file("I18N_lzh.properties"))
|
||||
}
|
||||
|
||||
// l10n
|
||||
|
||||
tasks.register<SyncTranslations>("syncTranslations") {
|
||||
group = "localization"
|
||||
description = "Synchronizes translation keys and blank lines with I18N.properties."
|
||||
|
||||
val dir = layout.projectDirectory.dir("src/main/resources/assets/lang")
|
||||
sourceFile.set(dir.file("I18N.properties"))
|
||||
translationFiles.from(fileTree(dir) { include("I18N_*.properties") })
|
||||
}
|
||||
|
||||
val generatedDir = layout.buildDirectory.dir("generated")
|
||||
|
||||
val upsideDownTranslate = tasks.register<UpsideDownTranslate>("upsideDownTranslate") {
|
||||
|
||||
@@ -0,0 +1,101 @@
|
||||
/*
|
||||
* 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 com.jfoenix.controls;
|
||||
|
||||
import com.jfoenix.skins.JFXCheckBoxSkin;
|
||||
import javafx.geometry.Pos;
|
||||
import javafx.scene.Node;
|
||||
import javafx.scene.control.CheckBoxTreeItem;
|
||||
import javafx.scene.layout.HBox;
|
||||
import org.jetbrains.annotations.NotNullByDefault;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
/// Displays a [CheckBoxTreeItem] with a material-design checkbox.
|
||||
///
|
||||
/// The checkbox's selected and indeterminate states are bidirectionally bound to
|
||||
/// the current tree item. Reusing or clearing the cell removes the old bindings.
|
||||
/// Installing a checkbox skin or switching tree items applies the selection state
|
||||
/// immediately, without playing a selection animation.
|
||||
/// Ordinary tree items retain the display provided by [JFXTreeCell].
|
||||
/// Empty cells and `null` values display neither content nor a checkbox.
|
||||
///
|
||||
/// @param <T> the type of value stored in each tree item
|
||||
@NotNullByDefault
|
||||
public class JFXCheckTreeCell<T> extends JFXTreeCell<T> {
|
||||
|
||||
/// The checkbox bound to the currently displayed checkable tree item.
|
||||
private final JFXCheckBox checkBox = new JFXCheckBox();
|
||||
|
||||
/// Places the checkbox before the graphic supplied by the superclass.
|
||||
private final HBox graphic = new HBox();
|
||||
|
||||
/// The tree item whose selection properties are currently bound, if any.
|
||||
private @Nullable CheckBoxTreeItem<?> boundTreeItem;
|
||||
|
||||
/// Creates an empty cell whose checkbox toggles the current tree item's selection.
|
||||
public JFXCheckTreeCell() {
|
||||
checkBox.setAllowIndeterminate(false);
|
||||
checkBox.setFocusTraversable(false);
|
||||
graphic.setAlignment(Pos.CENTER_LEFT);
|
||||
graphic.setPickOnBounds(false);
|
||||
checkBox.skinProperty().addListener(observable -> finishSelectionAnimation());
|
||||
treeItemProperty().addListener(observable -> updateDisplay(getItem(), isEmpty()));
|
||||
}
|
||||
|
||||
/// Finishes animations from the previous item and immediately displays the current state.
|
||||
private void finishSelectionAnimation() {
|
||||
if (checkBox.getSkin() instanceof JFXCheckBoxSkin skin) {
|
||||
skin.finishSelectionAnimation();
|
||||
}
|
||||
}
|
||||
|
||||
/// Adds the checkbox to the inherited display and updates its selection bindings.
|
||||
@Override
|
||||
protected void updateDisplay(@Nullable T item, boolean empty) {
|
||||
// Release graphics before the superclass reuses or reparents them.
|
||||
graphic.getChildren().clear();
|
||||
super.updateDisplay(item, empty);
|
||||
|
||||
@Nullable CheckBoxTreeItem<?> treeItem = !empty && item != null
|
||||
&& getTreeItem() instanceof CheckBoxTreeItem<?> checkTreeItem ? checkTreeItem : null;
|
||||
if (boundTreeItem != treeItem) {
|
||||
if (boundTreeItem != null) {
|
||||
checkBox.selectedProperty().unbindBidirectional(boundTreeItem.selectedProperty());
|
||||
checkBox.indeterminateProperty().unbindBidirectional(boundTreeItem.indeterminateProperty());
|
||||
}
|
||||
boundTreeItem = treeItem;
|
||||
if (treeItem != null) {
|
||||
checkBox.selectedProperty().bindBidirectional(treeItem.selectedProperty());
|
||||
checkBox.indeterminateProperty().bindBidirectional(treeItem.indeterminateProperty());
|
||||
} else {
|
||||
checkBox.setSelected(false);
|
||||
checkBox.setIndeterminate(false);
|
||||
}
|
||||
finishSelectionAnimation();
|
||||
}
|
||||
|
||||
if (treeItem != null) {
|
||||
@Nullable Node itemGraphic = getGraphic();
|
||||
graphic.getChildren().add(checkBox);
|
||||
if (itemGraphic != null) {
|
||||
graphic.getChildren().add(itemGraphic);
|
||||
}
|
||||
setGraphic(graphic);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -28,6 +28,7 @@ import javafx.scene.control.TreeCell;
|
||||
import javafx.scene.control.TreeItem;
|
||||
import javafx.scene.layout.*;
|
||||
import javafx.scene.paint.Color;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import java.lang.ref.WeakReference;
|
||||
|
||||
@@ -83,8 +84,7 @@ public class JFXTreeCell<T> extends TreeCell<T> {
|
||||
treeItemRef = new WeakReference<>(newTreeItem);
|
||||
}
|
||||
};
|
||||
final WeakInvalidationListener weakTreeItemListener = new WeakInvalidationListener(treeItemInvalidationListener);
|
||||
treeItemProperty().addListener(weakTreeItemListener);
|
||||
treeItemProperty().addListener(treeItemInvalidationListener);
|
||||
if (getTreeItem() != null) {
|
||||
getTreeItem().graphicProperty().addListener(weakTreeItemGraphicListener);
|
||||
}
|
||||
@@ -104,13 +104,20 @@ public class JFXTreeCell<T> extends TreeCell<T> {
|
||||
selectedPane.setVisible(isSelected());
|
||||
}
|
||||
|
||||
private void updateDisplay(T item, boolean empty) {
|
||||
/// Updates the text and graphic for the current value and tree item.
|
||||
///
|
||||
/// @implSpec Called when the cell value or the current tree item's graphic changes.
|
||||
/// Subclasses may override this method to decorate the default display.
|
||||
///
|
||||
/// @param item the cell value, or `null` to clear the display
|
||||
/// @param empty whether the cell has no item
|
||||
protected void updateDisplay(@Nullable T item, boolean empty) {
|
||||
if (item == null || empty) {
|
||||
hbox = null;
|
||||
setText(null);
|
||||
setGraphic(null);
|
||||
} else {
|
||||
TreeItem<T> treeItem = getTreeItem();
|
||||
@Nullable TreeItem<T> treeItem = getTreeItem();
|
||||
if (treeItem != null && treeItem.getGraphic() != null) {
|
||||
if (item instanceof Node) {
|
||||
setText(null);
|
||||
|
||||
@@ -169,11 +169,8 @@ public class JFXCheckBoxSkin extends CheckBoxSkin {
|
||||
};
|
||||
}
|
||||
|
||||
private void playSelectAnimation(Boolean selection) {
|
||||
if (selection == null) {
|
||||
selection = false;
|
||||
}
|
||||
|
||||
/// Animates a selection change, or applies it immediately when animations are disabled.
|
||||
private void playSelectAnimation(boolean selection) {
|
||||
JFXCheckBox control = (JFXCheckBox) this.getSkinnable();
|
||||
|
||||
this.box.setBorder(new Border(new BorderStroke(
|
||||
@@ -183,17 +180,7 @@ public class JFXCheckBoxSkin extends CheckBoxSkin {
|
||||
new BorderWidths(this.lineThick))));
|
||||
|
||||
if (!AnimationUtils.isAnimationEnabled()) {
|
||||
if (selection) {
|
||||
this.mark.setVisible(true);
|
||||
this.mark.setScaleX(1.0);
|
||||
this.mark.setScaleY(1.0);
|
||||
JFXNodeUtils.updateBackground(box.getBackground(), box, control.getCheckedColor());
|
||||
} else {
|
||||
this.mark.setVisible(false);
|
||||
this.mark.setScaleX(0.0);
|
||||
this.mark.setScaleY(0.0);
|
||||
JFXNodeUtils.updateBackground(box.getBackground(), box, Color.TRANSPARENT);
|
||||
}
|
||||
finishSelectionAnimation();
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -203,6 +190,22 @@ public class JFXCheckBoxSkin extends CheckBoxSkin {
|
||||
this.select.play();
|
||||
}
|
||||
|
||||
/// Stops selection animations and immediately displays the checkbox's current selected state.
|
||||
///
|
||||
/// The check mark, fill, and border are updated even if no animation is running.
|
||||
/// This also suppresses the initial selection animation on the first layout.
|
||||
/// The skin must not have been disposed.
|
||||
public void finishSelectionAnimation() {
|
||||
transition.stop();
|
||||
select.stop();
|
||||
invalid = false;
|
||||
boolean selected = getSkinnable().isSelected();
|
||||
mark.setVisible(selected);
|
||||
mark.setScaleX(selected ? 1.0 : 0.0);
|
||||
mark.setScaleY(selected ? 1.0 : 0.0);
|
||||
updateColors();
|
||||
}
|
||||
|
||||
private void createFillTransition() {
|
||||
this.select = new JFXFillTransition(Duration.millis(120.0), this.box, Color.TRANSPARENT,
|
||||
(Color) ((JFXCheckBox) this.getSkinnable()).getCheckedColor());
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -39,8 +39,6 @@ public abstract class LocalizedRemoteAddonRepository implements RemoteAddonRepos
|
||||
|
||||
protected abstract RemoteAddonRepository getBackedRemoteModRepository();
|
||||
|
||||
protected abstract SortType getBackedRemoteModRepositorySortOrder();
|
||||
|
||||
@Override
|
||||
public String getApiBaseUrl() {
|
||||
return getBackedRemoteModRepository().getApiBaseUrl();
|
||||
@@ -80,8 +78,8 @@ public abstract class LocalizedRemoteAddonRepository implements RemoteAddonRepos
|
||||
SearchResult searchResult = null;
|
||||
List<RemoteAddon> remoteAddons = List.of();
|
||||
for (String englishSearchFilter : englishSearchFiltersSet) {
|
||||
searchResult = getBackedRemoteModRepository().search(downloadProvider, gameVersion, category, pageOffset, pageSize, englishSearchFilter, getBackedRemoteModRepositorySortOrder(), sortOrder);
|
||||
remoteAddons = searchResult.getUnsortedResults().toList();
|
||||
searchResult = getBackedRemoteModRepository().search(downloadProvider, gameVersion, category, pageOffset, pageSize, englishSearchFilter, SortType.RELEVANCY, sortOrder);
|
||||
remoteAddons = searchResult.results().toList();
|
||||
if (!remoteAddons.isEmpty()) {
|
||||
break;
|
||||
}
|
||||
@@ -100,7 +98,7 @@ public abstract class LocalizedRemoteAddonRepository implements RemoteAddonRepos
|
||||
searchResultArray[englishIndex--] = remoteAddon;
|
||||
}
|
||||
}
|
||||
totalPages = searchResult.getTotalPages();
|
||||
totalPages = searchResult.totalPages();
|
||||
}
|
||||
|
||||
StringUtils.LevCalculator levCalculator = new StringUtils.LevCalculator();
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
|
||||
|
||||
@@ -24,8 +24,7 @@ import javafx.beans.binding.Bindings;
|
||||
import javafx.beans.binding.BooleanBinding;
|
||||
import javafx.beans.binding.ObjectBinding;
|
||||
import javafx.beans.binding.ObjectExpression;
|
||||
import javafx.beans.property.ReadOnlyObjectProperty;
|
||||
import javafx.beans.property.ReadOnlyObjectWrapper;
|
||||
import javafx.beans.property.*;
|
||||
import javafx.beans.value.ChangeListener;
|
||||
import javafx.beans.value.ObservableValue;
|
||||
import javafx.collections.SetChangeListener;
|
||||
@@ -80,6 +79,7 @@ import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.time.Duration;
|
||||
import java.util.*;
|
||||
import java.util.function.Supplier;
|
||||
import java.util.stream.Collectors;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
@@ -283,6 +283,9 @@ public final class Themes {
|
||||
colorScheme
|
||||
);
|
||||
|
||||
/// Whether the current color scheme brightness is inherited from system.
|
||||
private static final ReadOnlyBooleanWrapper autoBrightness = new ReadOnlyBooleanWrapper();
|
||||
|
||||
/// The current JavaFX launcher background and its node opacity.
|
||||
private static final ReadOnlyObjectWrapper<LauncherBackground> background = new ReadOnlyObjectWrapper<>(
|
||||
new LauncherBackground(
|
||||
@@ -380,6 +383,35 @@ public final class Themes {
|
||||
}
|
||||
|
||||
static {
|
||||
Supplier<@Nullable Brightness> effectiveBrightnessSupplier = () -> {
|
||||
try {
|
||||
return ThemePackManager.resolveCurrentThemeBrightness(ThemePackManager.currentResolveContext());
|
||||
} catch (IOException | RuntimeException e) {
|
||||
return null;
|
||||
}
|
||||
};
|
||||
InvalidationListener autoBrightnessListener = o -> {
|
||||
boolean auto;
|
||||
if (settings().getThemeAppearanceOverrides().contains(LauncherSettings.THEME_APPEARANCE_BRIGHTNESS_MODE)) {
|
||||
String val = settings().themeBrightnessModeProperty().get();
|
||||
auto = val == null || switch (val.trim().toLowerCase(Locale.ROOT)) {
|
||||
case "light", "dark" -> false;
|
||||
default -> true;
|
||||
};
|
||||
} else {
|
||||
auto = effectiveBrightnessSupplier.get() == null;
|
||||
}
|
||||
autoBrightness.set(auto);
|
||||
};
|
||||
settings().selectedThemeProperty().addListener(autoBrightnessListener);
|
||||
settings().getThemeAppearanceOverrides().addListener(autoBrightnessListener);
|
||||
settings().themeBrightnessModeProperty().addListener(autoBrightnessListener);
|
||||
settings().backgroundTypeProperty().addListener(autoBrightnessListener);
|
||||
if (FXUtils.DARK_MODE != null) {
|
||||
FXUtils.DARK_MODE.addListener(autoBrightnessListener);
|
||||
}
|
||||
autoBrightnessListener.invalidated(null);
|
||||
|
||||
ChangeListener<ResolvedTheme> listener = (observable, oldValue, newValue) -> {
|
||||
if (!Objects.equals(oldValue, newValue)) {
|
||||
colorScheme.set(newValue.toColorScheme());
|
||||
@@ -433,7 +465,7 @@ public final class Themes {
|
||||
}
|
||||
|
||||
String themeBrightnessMode = settings().themeBrightnessModeProperty().get();
|
||||
return switch (Objects.toString(themeBrightnessMode, "").toLowerCase(Locale.ROOT).trim()) {
|
||||
return switch (Objects.toString(themeBrightnessMode, "").trim().toLowerCase(Locale.ROOT)) {
|
||||
case "light" -> Brightness.LIGHT;
|
||||
case "dark" -> Brightness.DARK;
|
||||
default -> getAutomaticBrightness();
|
||||
@@ -444,24 +476,16 @@ public final class Themes {
|
||||
///
|
||||
/// @return the effective launcher brightness
|
||||
public static Brightness getCurrentBrightness() {
|
||||
Brightness contextBrightness = getThemeConditionBrightness();
|
||||
if (!settings().getThemeAppearanceOverrides().contains(LauncherSettings.THEME_APPEARANCE_BRIGHTNESS_MODE)) {
|
||||
Brightness contextBrightness = getThemeConditionBrightness();
|
||||
try {
|
||||
return Objects.requireNonNullElse(
|
||||
ThemePackManager.resolveCurrentThemeBrightness(ThemeResolveContext.current(contextBrightness)),
|
||||
contextBrightness);
|
||||
} catch (IOException | RuntimeException e) {
|
||||
return contextBrightness;
|
||||
} catch (IOException | RuntimeException ignored) {
|
||||
}
|
||||
}
|
||||
|
||||
String themeBrightnessMode = settings().themeBrightnessModeProperty().get();
|
||||
return switch (Objects.toString(themeBrightnessMode, "").toLowerCase(Locale.ROOT).trim()) {
|
||||
case "auto" -> getAutomaticBrightness();
|
||||
case "dark" -> Brightness.DARK;
|
||||
case "light" -> Brightness.LIGHT;
|
||||
default -> getAutomaticBrightness();
|
||||
};
|
||||
return contextBrightness;
|
||||
}
|
||||
|
||||
/// Returns the brightness requested by the current system or platform settings.
|
||||
@@ -1119,6 +1143,11 @@ public final class Themes {
|
||||
return darkMode;
|
||||
}
|
||||
|
||||
/// Returns whether the current color scheme brightness is inherited from system.
|
||||
public static ReadOnlyBooleanProperty autoBrightnessProperty() {
|
||||
return autoBrightness.getReadOnlyProperty();
|
||||
}
|
||||
|
||||
/// Applies native dark-mode integration to a JavaFX stage where the platform supports it.
|
||||
public static void applyNativeDarkMode(Stage stage) {
|
||||
if (OperatingSystem.SYSTEM_VERSION.isAtLeast(OSVersion.WINDOWS_11) && NativeUtils.USE_JNA && Dwmapi.INSTANCE != null) {
|
||||
|
||||
@@ -72,6 +72,8 @@ public class CrashWindow extends Stage {
|
||||
FXUtils.setIcon(this);
|
||||
setTitle(i18n("message.error"));
|
||||
|
||||
FXUtils.addMacOSCloseWindowHandler(this, null);
|
||||
|
||||
setOnCloseRequest(e -> javafx.application.Platform.exit());
|
||||
}
|
||||
|
||||
|
||||
@@ -58,9 +58,7 @@ import javafx.scene.paint.Paint;
|
||||
import javafx.scene.shape.Rectangle;
|
||||
import javafx.scene.text.Text;
|
||||
import javafx.scene.text.TextFlow;
|
||||
import javafx.stage.FileChooser;
|
||||
import javafx.stage.Screen;
|
||||
import javafx.stage.Stage;
|
||||
import javafx.stage.*;
|
||||
import javafx.util.Duration;
|
||||
import javafx.util.StringConverter;
|
||||
import org.glavo.url.WebURL;
|
||||
@@ -109,10 +107,7 @@ import java.nio.file.Path;
|
||||
import java.nio.file.PathMatcher;
|
||||
import java.util.*;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.function.Consumer;
|
||||
import java.util.function.Function;
|
||||
import java.util.function.Predicate;
|
||||
import java.util.function.ToIntFunction;
|
||||
import java.util.function.*;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
@@ -1200,6 +1195,18 @@ public final class FXUtils {
|
||||
});
|
||||
}
|
||||
|
||||
public static void addMacOSCloseWindowHandler(Window window, @Nullable Supplier<Boolean> restriction) {
|
||||
window.addEventHandler(KeyEvent.KEY_PRESSED, event -> {
|
||||
if (OperatingSystem.CURRENT_OS == OperatingSystem.MACOS
|
||||
&& event.isMetaDown()
|
||||
&& event.getCode() == KeyCode.W
|
||||
&& (restriction == null || restriction.get())) {
|
||||
window.fireEvent(new WindowEvent(window, WindowEvent.WINDOW_CLOSE_REQUEST));
|
||||
event.consume();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public static <N extends Parent> N prepareNode(N node) {
|
||||
Scene dummyScene = new Scene(node);
|
||||
StyleSheets.init(dummyScene);
|
||||
|
||||
@@ -114,6 +114,8 @@ public class GameCrashWindow extends Stage {
|
||||
setTitle(i18n("game.crash.title"));
|
||||
FXUtils.setIcon(this);
|
||||
|
||||
FXUtils.addMacOSCloseWindowHandler(this, null);
|
||||
|
||||
analyzeCrashReport();
|
||||
}
|
||||
|
||||
|
||||
@@ -100,6 +100,8 @@ public final class LogWindow extends Stage {
|
||||
}
|
||||
|
||||
this.gameProcess = gameProcess;
|
||||
|
||||
FXUtils.addMacOSCloseWindowHandler(this, () -> !gameProcess.isRunning());
|
||||
}
|
||||
|
||||
public void logLine(Log log) {
|
||||
|
||||
@@ -18,6 +18,8 @@
|
||||
package org.jackhuang.hmcl.ui;
|
||||
|
||||
import com.sun.jna.Pointer;
|
||||
import javafx.beans.property.ReadOnlyBooleanProperty;
|
||||
import org.jackhuang.hmcl.theme.Themes;
|
||||
import org.jackhuang.hmcl.util.platform.macos.ObjectiveCRuntime;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
@@ -56,8 +58,30 @@ public final class MacOSNativeUtils {
|
||||
return pointer == null || Pointer.nativeValue(pointer) == 0;
|
||||
}
|
||||
|
||||
private static final ReadOnlyBooleanProperty autoBrightness = Themes.autoBrightnessProperty();
|
||||
|
||||
static {
|
||||
FXUtils.onChange(autoBrightness, b -> setAppearance(Themes.darkModeProperty().get()));
|
||||
}
|
||||
|
||||
public static void setAppearance(boolean dark) {
|
||||
setAppearance(dark, false);
|
||||
if (autoBrightness.get()) {
|
||||
clearAppearance();
|
||||
} else {
|
||||
setAppearance(dark, false);
|
||||
}
|
||||
}
|
||||
|
||||
public static void clearAppearance() {
|
||||
if (nsApp == null) return;
|
||||
|
||||
try {
|
||||
var objc = ObjectiveCRuntime.INSTANCE;
|
||||
Pointer setSel = objc.sel_registerName("setAppearance:");
|
||||
objc.objc_msgSend(nsApp, setSel, Pointer.NULL);
|
||||
} catch (Throwable t) {
|
||||
LOG.warning("Failed to set macOS appearance", t);
|
||||
}
|
||||
}
|
||||
|
||||
public static void setAppearance(boolean dark, boolean highContrast) {
|
||||
|
||||
@@ -19,15 +19,11 @@ package org.jackhuang.hmcl.ui.construct;
|
||||
|
||||
import com.jfoenix.controls.JFXButton;
|
||||
import com.jfoenix.controls.JFXDialogLayout;
|
||||
import com.jfoenix.controls.JFXProgressBar;
|
||||
import javafx.beans.property.BooleanProperty;
|
||||
import javafx.beans.property.SimpleBooleanProperty;
|
||||
import javafx.beans.property.SimpleStringProperty;
|
||||
import javafx.beans.property.StringProperty;
|
||||
import javafx.geometry.Insets;
|
||||
import javafx.geometry.Pos;
|
||||
import javafx.scene.control.Label;
|
||||
import javafx.scene.layout.StackPane;
|
||||
|
||||
import static org.jackhuang.hmcl.ui.FXUtils.onEscPressed;
|
||||
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
|
||||
@@ -38,18 +34,11 @@ public class DialogPane extends JFXDialogLayout {
|
||||
protected final SpinnerPane acceptPane = new SpinnerPane();
|
||||
protected final JFXButton cancelButton = new JFXButton();
|
||||
protected final Label warningLabel = new Label();
|
||||
private final JFXProgressBar progressBar = new JFXProgressBar();
|
||||
|
||||
public DialogPane() {
|
||||
Label titleLabel = new Label();
|
||||
titleLabel.textProperty().bind(title);
|
||||
setHeading(titleLabel);
|
||||
getChildren().add(progressBar);
|
||||
|
||||
progressBar.setVisible(false);
|
||||
StackPane.setMargin(progressBar, new Insets(-24.0D, -24.0D, -16.0D, -24.0D));
|
||||
StackPane.setAlignment(progressBar, Pos.TOP_CENTER);
|
||||
progressBar.setMaxWidth(Double.MAX_VALUE);
|
||||
|
||||
JFXButton acceptButton = new JFXButton(i18n("button.ok"));
|
||||
acceptButton.setOnAction(e -> onAccept());
|
||||
@@ -66,10 +55,6 @@ public class DialogPane extends JFXDialogLayout {
|
||||
setActions(warningLabel, acceptPane, cancelButton);
|
||||
}
|
||||
|
||||
protected JFXProgressBar getProgressBar() {
|
||||
return progressBar;
|
||||
}
|
||||
|
||||
public String getTitle() {
|
||||
return title.get();
|
||||
}
|
||||
|
||||
@@ -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,45 +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.MultiMCModpackExportTask;
|
||||
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;
|
||||
@@ -162,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 MultiMCModpackExportTask || 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);
|
||||
|
||||
@@ -24,12 +24,21 @@ import javafx.collections.FXCollections;
|
||||
import javafx.collections.ObservableList;
|
||||
import javafx.css.PseudoClass;
|
||||
import javafx.geometry.Pos;
|
||||
import javafx.scene.Node;
|
||||
import javafx.scene.control.Label;
|
||||
import javafx.scene.control.OverrunStyle;
|
||||
import javafx.scene.layout.HBox;
|
||||
import javafx.scene.layout.Priority;
|
||||
import javafx.scene.layout.VBox;
|
||||
import org.jackhuang.hmcl.ui.FXUtils;
|
||||
import org.jetbrains.annotations.NotNullByDefault;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/// Displays a title with optional tags and a subtitle.
|
||||
public class TwoLineListItem extends VBox {
|
||||
private static final String DEFAULT_STYLE_CLASS = "two-line-list-item";
|
||||
|
||||
@@ -39,14 +48,28 @@ public class TwoLineListItem extends VBox {
|
||||
private final Label lblTitle;
|
||||
private Label lblSubtitle;
|
||||
|
||||
/// Creates an empty item whose children do not receive mouse events.
|
||||
public TwoLineListItem() {
|
||||
getStyleClass().add(DEFAULT_STYLE_CLASS);
|
||||
setMouseTransparent(true);
|
||||
|
||||
lblTitle = new Label();
|
||||
lblTitle.getStyleClass().add("title");
|
||||
lblTitle.setTextOverrun(OverrunStyle.ELLIPSIS);
|
||||
|
||||
this.firstLine = new HBox(lblTitle);
|
||||
this.firstLine = new HBox(lblTitle) {
|
||||
/// Includes the tags' natural width when requesting space from the parent.
|
||||
@Override
|
||||
protected double computePrefWidth(double height) {
|
||||
double width = super.computePrefWidth(height);
|
||||
for (Node child : getManagedChildren()) {
|
||||
if (child instanceof TagsBox tagsBox) {
|
||||
width += snapSizeX(tagsBox.computeNaturalPrefWidth(height));
|
||||
}
|
||||
}
|
||||
return width;
|
||||
}
|
||||
};
|
||||
firstLine.getStyleClass().add("first-line");
|
||||
firstLine.setAlignment(Pos.CENTER_LEFT);
|
||||
|
||||
@@ -158,17 +181,29 @@ public class TwoLineListItem extends VBox {
|
||||
return lblSubtitle;
|
||||
}
|
||||
|
||||
private ObservableList<Label> tags;
|
||||
private static Label createTag(String tag, PseudoClass pseudoClass) {
|
||||
var tagLabel = new Label(tag);
|
||||
tagLabel.getStyleClass().add("tag");
|
||||
tagLabel.setMinWidth(Label.USE_PREF_SIZE);
|
||||
if (pseudoClass != null)
|
||||
tagLabel.pseudoClassStateChanged(pseudoClass, true);
|
||||
return tagLabel;
|
||||
}
|
||||
|
||||
/// Stores the mutable tag list once tag support has been initialized.
|
||||
private @Nullable ObservableList<Label> tags;
|
||||
|
||||
/// Returns the mutable list of tags displayed after the title.
|
||||
public ObservableList<Label> getTags() {
|
||||
if (tags == null) {
|
||||
tags = FXCollections.observableArrayList();
|
||||
|
||||
var tagsBox = new HBox(8);
|
||||
var tagsBox = new TagsBox();
|
||||
tagsBox.getStyleClass().add("tags");
|
||||
tagsBox.setAlignment(Pos.CENTER_LEFT);
|
||||
tagsBox.setMinWidth(0);
|
||||
HBox.setHgrow(tagsBox, Priority.ALWAYS);
|
||||
|
||||
HBox.setHgrow(tagsBox, Priority.SOMETIMES);
|
||||
|
||||
Bindings.bindContent(tagsBox.getChildren(), tags);
|
||||
var isNotEmpty = Bindings.isNotEmpty(tags);
|
||||
tagsBox.managedProperty().bind(isNotEmpty);
|
||||
@@ -176,25 +211,53 @@ public class TwoLineListItem extends VBox {
|
||||
|
||||
FXUtils.setOverflowHidden(tagsBox);
|
||||
|
||||
lblTitle.setMinWidth(Label.USE_PREF_SIZE);
|
||||
HBox.setHgrow(lblTitle, Priority.ALWAYS);
|
||||
lblTitle.setMinWidth(0);
|
||||
firstLine.getChildren().setAll(lblTitle, tagsBox);
|
||||
}
|
||||
return tags;
|
||||
}
|
||||
|
||||
/// Reports its natural width separately from the width used to allocate space within the title row.
|
||||
@NotNullByDefault
|
||||
private static final class TagsBox extends HBox {
|
||||
|
||||
/// Creates a tag container with zero minimum and preferred allocation widths.
|
||||
private TagsBox() {
|
||||
super(8);
|
||||
// Start at zero during HBox allocation so tags do not compete with the title for space.
|
||||
setPrefWidth(0);
|
||||
setMinWidth(0);
|
||||
}
|
||||
|
||||
/// Returns the computed preferred width without applying the zero preferred-width override.
|
||||
///
|
||||
/// @param height the available height, or -1 if unspecified
|
||||
/// @return the natural preferred width of the tags and their container
|
||||
private double computeNaturalPrefWidth(double height) {
|
||||
return super.computePrefWidth(height);
|
||||
}
|
||||
}
|
||||
|
||||
public void addTag(String tag, PseudoClass pseudoClass) {
|
||||
var tagLabel = new Label(tag);
|
||||
tagLabel.getStyleClass().add("tag");
|
||||
tagLabel.setMinWidth(Label.USE_PREF_SIZE);
|
||||
if (pseudoClass != null)
|
||||
tagLabel.pseudoClassStateChanged(pseudoClass, true);
|
||||
getTags().add(tagLabel);
|
||||
getTags().add(createTag(tag, pseudoClass));
|
||||
}
|
||||
|
||||
public void addTag(String tag) {
|
||||
addTag(tag, null);
|
||||
}
|
||||
|
||||
public void addTags(Collection<String> tags) {
|
||||
getTags().addAll(tags.stream().map(tag -> createTag(tag, null)).toList());
|
||||
}
|
||||
|
||||
public void addTagsIfNotExist(Collection<String> tags) {
|
||||
var current = getTags().stream().map(Label::getText).collect(Collectors.toSet());
|
||||
var target = new LinkedHashSet<>(tags);
|
||||
target.removeAll(current);
|
||||
addTags(target);
|
||||
}
|
||||
|
||||
private static final PseudoClass WARNING_PSEUDO_CLASS = PseudoClass.getPseudoClass("warning");
|
||||
|
||||
public void addTagWarning(String tag) {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -59,7 +59,7 @@ class AdditionalInstallersPage extends AbstractInstallersPage {
|
||||
});
|
||||
}
|
||||
|
||||
installable.bind(Bindings.createBooleanBinding(() -> compatible.get() && txtName.validate(), txtName.textProperty(), compatible));
|
||||
installable.bind(Bindings.createBooleanBinding(() -> txtName.validate(), txtName.textProperty()));
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -80,27 +80,14 @@ class AdditionalInstallersPage extends AbstractInstallersPage {
|
||||
|
||||
@Override
|
||||
protected void reload() {
|
||||
boolean gameVersionChanged = !instance.getVersion().toString().equals(getVersion(GameComponentType.GAME));
|
||||
boolean compatible = true;
|
||||
|
||||
for (InstallerItem component : group.getComponents()) {
|
||||
GameComponentType componentType = component.getComponentType();
|
||||
String version = instance.getComponentVersion(component.getComponentType());
|
||||
String libraryVersion = Lang.requireNonNullElse(getVersion(componentType), version);
|
||||
boolean alreadyInstalled = version != null && !(controller.getSettings().get(componentType.getPatchId()) instanceof UpdateInstallerWizardProvider.RemoveComponentAction);
|
||||
if (component.getComponentType() != GameComponentType.GAME && gameVersionChanged && getVersion(componentType) == null && alreadyInstalled) {
|
||||
// For third-party libraries, if game version is being changed, and the library is not being reinstalled,
|
||||
// warns the user that we should update the library.
|
||||
component.versionProperty().set(new InstallerItem.InstalledState(libraryVersion, false, true));
|
||||
compatible = false;
|
||||
} else if (alreadyInstalled || getVersion(componentType) != null) {
|
||||
component.versionProperty().set(new InstallerItem.InstalledState(libraryVersion, false, false));
|
||||
if (controller.getSettings().containsKey(componentType.getPatchId())) {
|
||||
component.versionProperty().set(new InstallerItem.InstalledState(getVersion(componentType), false, false));
|
||||
} else {
|
||||
component.versionProperty().set(null);
|
||||
}
|
||||
}
|
||||
|
||||
this.compatible.set(compatible);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -307,7 +307,7 @@ public class DownloadPage extends DecoratorAnimatedPage implements DecoratorPage
|
||||
/// @param settings the installer selections and target instance id
|
||||
/// @return the builder task with its stage hints preserved as the outermost task wrapper
|
||||
private Task<?> finishVersionDownloadingAsync(SettingsMap settings) {
|
||||
GameInstanceID instanceId = settings.get(AbstractInstallersPage.INSTANCE_ID);
|
||||
@Nullable GameInstanceID instanceId = settings.get(AbstractInstallersPage.INSTANCE_ID);
|
||||
if (instanceId == null) {
|
||||
throw new IllegalStateException("Instance ID is not set");
|
||||
}
|
||||
@@ -329,6 +329,7 @@ public class DownloadPage extends DecoratorAnimatedPage implements DecoratorPage
|
||||
}
|
||||
|
||||
Task<?> buildTask = builder.buildAsync();
|
||||
settings.put(TaskCleanup.KEY, builder::abort);
|
||||
buildTask.onDone().register(event -> {
|
||||
if (!event.isFailed()) {
|
||||
runInFX(() -> repository.setSelectedInstance(repository.getInstance(instanceId)));
|
||||
|
||||
@@ -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) {
|
||||
}
|
||||
}
|
||||
+2
-1
@@ -65,6 +65,7 @@ public final class UpdateInstallerWizardProvider implements WizardProvider {
|
||||
public void start(SettingsMap settings) {
|
||||
}
|
||||
|
||||
/// Creates a task that updates components and synchronizes the client JAR with the resulting manifest.
|
||||
@Override
|
||||
public Object finish(SettingsMap settings) {
|
||||
settings.put("title", i18n("install.change_version.process"));
|
||||
@@ -83,7 +84,7 @@ public final class UpdateInstallerWizardProvider implements WizardProvider {
|
||||
}
|
||||
}
|
||||
|
||||
return gameInstance.getRepository().updateInstanceAsync(gameInstance.getId(), publishedInstance -> {
|
||||
return dependencyManager.updateInstanceAsync(gameInstance.getId(), publishedInstance -> {
|
||||
Task<GameInstanceManifest> update = Task.supplyAsync(publishedInstance::getManifest);
|
||||
for (Object value : settings.asStringMap().values()) {
|
||||
if (value instanceof ComponentRemoteVersion remoteVersion) {
|
||||
|
||||
@@ -24,14 +24,11 @@ import org.jackhuang.hmcl.modpack.ModAdviser;
|
||||
import org.jackhuang.hmcl.modpack.ModpackExportInfo;
|
||||
import org.jackhuang.hmcl.modpack.mcbbs.McbbsModpackExportTask;
|
||||
import org.jackhuang.hmcl.modpack.modrinth.ModrinthModpackExportTask;
|
||||
import org.jackhuang.hmcl.modpack.multimc.MultiMCInstanceConfiguration;
|
||||
import org.jackhuang.hmcl.modpack.multimc.MultiMCModpackExportTask;
|
||||
import org.jackhuang.hmcl.modpack.server.ServerModpackExportTask;
|
||||
import org.jackhuang.hmcl.setting.*;
|
||||
import org.jackhuang.hmcl.task.Task;
|
||||
import org.jackhuang.hmcl.ui.wizard.WizardController;
|
||||
import org.jackhuang.hmcl.ui.wizard.WizardProvider;
|
||||
import org.jackhuang.hmcl.util.Lang;
|
||||
import org.jackhuang.hmcl.util.SettingsMap;
|
||||
import org.jackhuang.hmcl.util.gson.JsonUtils;
|
||||
import org.jackhuang.hmcl.util.io.JarUtils;
|
||||
@@ -97,9 +94,6 @@ public final class ExportWizardProvider implements WizardProvider {
|
||||
case ModpackTypeSelectionPage.MODPACK_TYPE_MCBBS:
|
||||
exportTask = exportAsMcbbs(exportInfo, dest);
|
||||
break;
|
||||
case ModpackTypeSelectionPage.MODPACK_TYPE_MULTIMC:
|
||||
exportTask = exportAsMultiMC(exportInfo, dest);
|
||||
break;
|
||||
case ModpackTypeSelectionPage.MODPACK_TYPE_SERVER:
|
||||
exportTask = exportAsServer(exportInfo, dest);
|
||||
break;
|
||||
@@ -183,55 +177,6 @@ public final class ExportWizardProvider implements WizardProvider {
|
||||
};
|
||||
}
|
||||
|
||||
private Task<?> exportAsMultiMC(ModpackExportInfo exportInfo, Path modpackFile) {
|
||||
return new Task<Void>() {
|
||||
@Nullable Task<?> dependency;
|
||||
|
||||
{
|
||||
setSignificance(TaskSignificance.MODERATE);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void execute() {
|
||||
HMCLGameInstance instance = resolveCurrentGameInstance();
|
||||
GameSettings.Effective setting = instance.getEffectiveSettings();
|
||||
dependency = new MultiMCModpackExportTask(instance, exportInfo.getWhitelist(),
|
||||
new MultiMCInstanceConfiguration(
|
||||
"OneSix",
|
||||
exportInfo.getName() + "-" + exportInfo.getVersion(),
|
||||
null,
|
||||
Lang.toIntOrNull(setting.getInheritable(GameSettings::permSizeProperty)),
|
||||
setting.getInheritable(GameSettings::commandWrapperProperty),
|
||||
setting.getInheritable(GameSettings::preLaunchCommandProperty),
|
||||
null,
|
||||
exportInfo.getDescription(),
|
||||
null,
|
||||
exportInfo.getJavaArguments(),
|
||||
setting.getInheritable(GameSettings::windowTypeProperty) == GameWindowType.FULLSCREEN,
|
||||
setting.getWidth(),
|
||||
setting.getHeight(),
|
||||
null,
|
||||
exportInfo.getMinMemory(),
|
||||
setting.getInheritable(GameSettings::showLogsProperty),
|
||||
/* showConsoleOnError */ true,
|
||||
/* autoCloseConsole */ false,
|
||||
/* overrideMemory */ true,
|
||||
/* overrideJavaLocation */ false,
|
||||
/* overrideJavaArgs */ true,
|
||||
/* overrideConsole */ true,
|
||||
/* overrideCommands */ true,
|
||||
/* overrideWindow */ true,
|
||||
/* iconKey */ null // TODO
|
||||
), modpackFile);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Collection<Task<?>> getDependencies() {
|
||||
return Collections.singleton(dependency);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
private Task<?> exportAsServer(ModpackExportInfo exportInfo, Path modpackFile) {
|
||||
return new Task<Void>() {
|
||||
@Nullable Task<?> dependency;
|
||||
|
||||
@@ -18,17 +18,17 @@
|
||||
package org.jackhuang.hmcl.ui.export;
|
||||
|
||||
import com.jfoenix.controls.JFXButton;
|
||||
import com.jfoenix.controls.JFXCheckBox;
|
||||
import com.jfoenix.controls.JFXCheckTreeCell;
|
||||
import com.jfoenix.controls.JFXTreeView;
|
||||
import javafx.geometry.Insets;
|
||||
import javafx.geometry.Pos;
|
||||
import javafx.scene.Node;
|
||||
import javafx.scene.control.CheckBoxTreeItem;
|
||||
import javafx.scene.control.Label;
|
||||
import javafx.scene.control.TreeItem;
|
||||
import javafx.scene.layout.BorderPane;
|
||||
import javafx.scene.layout.HBox;
|
||||
import javafx.scene.layout.StackPane;
|
||||
import org.jackhuang.hmcl.game.GameInstanceID;
|
||||
import org.jackhuang.hmcl.game.HMCLGameInstance;
|
||||
import org.jackhuang.hmcl.modpack.ModAdviser;
|
||||
import org.jackhuang.hmcl.task.Schedulers;
|
||||
@@ -41,6 +41,7 @@ import org.jackhuang.hmcl.util.Pair;
|
||||
import org.jackhuang.hmcl.util.SettingsMap;
|
||||
import org.jackhuang.hmcl.util.StringUtils;
|
||||
import org.jackhuang.hmcl.util.io.FileUtils;
|
||||
import org.jetbrains.annotations.NotNullByDefault;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import java.io.IOException;
|
||||
@@ -66,13 +67,14 @@ public final class ModpackFileSelectionPage extends BorderPane implements Wizard
|
||||
private final ModAdviser adviser;
|
||||
private @Nullable ModpackFileTreeItem rootNode;
|
||||
|
||||
/// Creates a file-selection page and starts loading the instance's exportable files.
|
||||
public ModpackFileSelectionPage(WizardController controller, HMCLGameInstance gameInstance, ModAdviser adviser) {
|
||||
this.controller = controller;
|
||||
this.gameInstance = gameInstance;
|
||||
this.adviser = adviser;
|
||||
GameInstanceID instanceId = gameInstance.getId();
|
||||
|
||||
JFXTreeView<String> treeView = new JFXTreeView<>();
|
||||
treeView.setCellFactory(__ -> new ModpackFileTreeCell());
|
||||
treeView.setSelectionModel(new NoneMultipleSelectionModel<>());
|
||||
onEscPressed(treeView, () -> controller.onPrev(true));
|
||||
|
||||
@@ -196,14 +198,16 @@ public final class ModpackFileSelectionPage extends BorderPane implements Wizard
|
||||
return node;
|
||||
}
|
||||
|
||||
private void getFilesNeeded(ModpackFileTreeItem node, String basePath, List<String> list) {
|
||||
/// Appends selected paths relative to `minecraft/`, traversing partially selected directories.
|
||||
/// A `null` node contributes no paths.
|
||||
private void getFilesNeeded(@Nullable ModpackFileTreeItem node, String basePath, List<String> list) {
|
||||
if (node == null) return;
|
||||
if (node.isSelected() || node.isIndeterminate()) {
|
||||
if (basePath.length() > "minecraft/".length())
|
||||
list.add(StringUtils.substringAfter(basePath, "minecraft/"));
|
||||
for (TreeItem<String> child : node.getChildren()) {
|
||||
if (child instanceof ModpackFileTreeItem mChild) {
|
||||
getFilesNeeded(mChild, basePath + "/" + mChild.getFileName(), list);
|
||||
getFilesNeeded(mChild, basePath + "/" + mChild.getValue(), list);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -244,39 +248,69 @@ public final class ModpackFileSelectionPage extends BorderPane implements Wizard
|
||||
pair("minecraft/scripts", i18n("modpack.files.scripts"))
|
||||
);
|
||||
|
||||
/// Stores a file name, optional explanation, and hierarchical selection state.
|
||||
@NotNullByDefault
|
||||
private static final class ModpackFileTreeItem extends CheckBoxTreeItem<String> {
|
||||
|
||||
private final String fileName;
|
||||
/// The localized explanation for a recognized path, or `null` if none exists.
|
||||
private final @Nullable String comment;
|
||||
|
||||
/// Creates an item whose value is the file name; only the `minecraft` root starts expanded.
|
||||
public ModpackFileTreeItem(String fileName, String basePath) {
|
||||
this.fileName = fileName;
|
||||
|
||||
HBox graphic = new HBox();
|
||||
JFXCheckBox checkBox = new JFXCheckBox();
|
||||
checkBox.selectedProperty().bindBidirectional(this.selectedProperty());
|
||||
checkBox.indeterminateProperty().bindBidirectional(this.indeterminateProperty());
|
||||
graphic.getChildren().add(checkBox);
|
||||
|
||||
{
|
||||
Label text = new Label(fileName);
|
||||
text.setMouseTransparent(true);
|
||||
graphic.getChildren().add(text);
|
||||
}
|
||||
if (TRANSLATION.containsKey(basePath)) {
|
||||
Label comment = new Label(TRANSLATION.get(basePath));
|
||||
comment.setStyle("-fx-text-fill: -monet-on-surface-variant;");
|
||||
comment.setMouseTransparent(true);
|
||||
graphic.getChildren().add(comment);
|
||||
}
|
||||
graphic.setPickOnBounds(false);
|
||||
super(fileName);
|
||||
this.comment = TRANSLATION.get(basePath);
|
||||
this.setExpanded("minecraft".equals(basePath));
|
||||
this.setValue(""); // To disable the default display of text
|
||||
this.setGraphic(graphic);
|
||||
}
|
||||
}
|
||||
|
||||
/// Renders file names and localized explanations alongside the bound checkbox.
|
||||
@NotNullByDefault
|
||||
private static final class ModpackFileTreeCell extends JFXCheckTreeCell<String> {
|
||||
|
||||
/// Contains the inherited checkbox graphic followed by the file labels.
|
||||
private final HBox content = new HBox();
|
||||
|
||||
/// Displays the current file name.
|
||||
private final Label fileName = new Label();
|
||||
|
||||
/// Displays the current path's optional explanation.
|
||||
private final Label comment = new Label();
|
||||
|
||||
/// Creates reusable labels whose mouse events pass through to the cell.
|
||||
private ModpackFileTreeCell() {
|
||||
fileName.setMouseTransparent(true);
|
||||
comment.setStyle("-fx-text-fill: -monet-on-surface-variant;");
|
||||
comment.setMouseTransparent(true);
|
||||
content.setAlignment(Pos.CENTER_LEFT);
|
||||
content.setPickOnBounds(false);
|
||||
}
|
||||
|
||||
public String getFileName() {
|
||||
return fileName;
|
||||
}
|
||||
/// Refreshes the file labels and removes content when the cell is cleared.
|
||||
@Override
|
||||
protected void updateDisplay(@Nullable String item, boolean empty) {
|
||||
content.getChildren().clear();
|
||||
super.updateDisplay(item, empty);
|
||||
|
||||
fileName.setText(null);
|
||||
comment.setText(null);
|
||||
if (empty || item == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Updating the text makes the skin reattach the current graphic to the cell.
|
||||
// Clear it before moving the checkbox graphic into the content container.
|
||||
setText(null);
|
||||
@Nullable Node graphic = getGraphic();
|
||||
if (graphic != null) {
|
||||
content.getChildren().add(graphic);
|
||||
}
|
||||
fileName.setText(item);
|
||||
content.getChildren().add(fileName);
|
||||
if (getTreeItem() instanceof ModpackFileTreeItem treeItem && treeItem.comment != null) {
|
||||
comment.setText(treeItem.comment);
|
||||
content.getChildren().add(comment);
|
||||
}
|
||||
setGraphic(content);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -26,7 +26,6 @@ import javafx.scene.layout.BorderPane;
|
||||
import javafx.scene.layout.VBox;
|
||||
import org.jackhuang.hmcl.modpack.ModpackExportInfo;
|
||||
import org.jackhuang.hmcl.modpack.mcbbs.McbbsModpackExportTask;
|
||||
import org.jackhuang.hmcl.modpack.multimc.MultiMCModpackExportTask;
|
||||
import org.jackhuang.hmcl.modpack.server.ServerModpackExportTask;
|
||||
import org.jackhuang.hmcl.modpack.modrinth.ModrinthModpackExportTask;
|
||||
import org.jackhuang.hmcl.ui.SVG;
|
||||
@@ -55,7 +54,6 @@ public final class ModpackTypeSelectionPage extends VBox implements WizardPage {
|
||||
this.getChildren().setAll(
|
||||
title,
|
||||
createButton(MODPACK_TYPE_MCBBS, McbbsModpackExportTask.OPTION),
|
||||
createButton(MODPACK_TYPE_MULTIMC, MultiMCModpackExportTask.OPTION),
|
||||
createButton(MODPACK_TYPE_SERVER, ServerModpackExportTask.OPTION),
|
||||
createButton(MODPACK_TYPE_MODRINTH, ModrinthModpackExportTask.OPTION)
|
||||
);
|
||||
@@ -98,7 +96,6 @@ public final class ModpackTypeSelectionPage extends VBox implements WizardPage {
|
||||
public static final SettingsMap.Key<String> MODPACK_TYPE = new SettingsMap.Key<>("modpack.type");
|
||||
|
||||
public static final String MODPACK_TYPE_MCBBS = "mcbbs";
|
||||
public static final String MODPACK_TYPE_MULTIMC = "multimc";
|
||||
public static final String MODPACK_TYPE_SERVER = "server";
|
||||
public static final String MODPACK_TYPE_MODRINTH = "modrinth";
|
||||
}
|
||||
@@ -27,7 +27,6 @@ import javafx.application.Platform;
|
||||
import javafx.beans.InvalidationListener;
|
||||
import javafx.beans.binding.Bindings;
|
||||
import javafx.beans.property.BooleanProperty;
|
||||
import javafx.beans.property.ObjectProperty;
|
||||
import javafx.beans.property.SimpleBooleanProperty;
|
||||
import javafx.collections.ListChangeListener;
|
||||
import javafx.collections.transformation.FilteredList;
|
||||
@@ -46,7 +45,6 @@ import javafx.scene.layout.Priority;
|
||||
import javafx.scene.layout.StackPane;
|
||||
import javafx.util.Duration;
|
||||
import org.jackhuang.hmcl.addon.datapack.DataPack;
|
||||
import org.jackhuang.hmcl.task.Schedulers;
|
||||
import org.jackhuang.hmcl.ui.Controllers;
|
||||
import org.jackhuang.hmcl.ui.FXUtils;
|
||||
import org.jackhuang.hmcl.ui.SVG;
|
||||
@@ -55,15 +53,13 @@ import org.jackhuang.hmcl.ui.animation.TransitionPane;
|
||||
import org.jackhuang.hmcl.ui.construct.*;
|
||||
import org.jackhuang.hmcl.util.StringUtils;
|
||||
import org.jackhuang.hmcl.util.io.CompressingUtils;
|
||||
import org.jackhuang.hmcl.util.javafx.ItemPropertyAsyncCache;
|
||||
import org.jetbrains.annotations.NotNullByDefault;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import java.lang.ref.SoftReference;
|
||||
import java.lang.ref.WeakReference;
|
||||
import java.nio.file.FileSystem;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
import java.util.function.Consumer;
|
||||
import java.util.stream.IntStream;
|
||||
@@ -263,11 +259,13 @@ final class DataPackListPageSkin extends SkinBase<DataPackListPage> {
|
||||
private final BooleanProperty activeProperty;
|
||||
private final DataPack.Pack packInfo;
|
||||
|
||||
private SoftReference<CompletableFuture<Image>> iconCache;
|
||||
private final ItemPropertyAsyncCache<Image, DataPackInfoObject> iconCache;
|
||||
|
||||
DataPackInfoObject(DataPack.Pack packInfo) {
|
||||
this.packInfo = packInfo;
|
||||
this.activeProperty = packInfo.activeProperty();
|
||||
|
||||
this.iconCache = new ItemPropertyAsyncCache.Soft<>(this, this::loadIcon, this::getDefaultIcon);
|
||||
}
|
||||
|
||||
String getTitle() {
|
||||
@@ -282,7 +280,11 @@ final class DataPackListPageSkin extends SkinBase<DataPackListPage> {
|
||||
return packInfo;
|
||||
}
|
||||
|
||||
Image loadIcon() {
|
||||
private Image getDefaultIcon() {
|
||||
return FXUtils.newBuiltinImage("/assets/img/unknown_pack.png");
|
||||
}
|
||||
|
||||
private Image loadIcon() {
|
||||
Image image = null;
|
||||
Path imagePath;
|
||||
if (this.getPackInfo().isDirectory()) {
|
||||
@@ -291,7 +293,6 @@ final class DataPackListPageSkin extends SkinBase<DataPackListPage> {
|
||||
image = FXUtils.loadImage(imagePath, 64, 64, true, true);
|
||||
} catch (Exception e) {
|
||||
LOG.warning("fail to load image, datapack path: " + getPackInfo().getPath(), e);
|
||||
return FXUtils.newBuiltinImage("/assets/img/unknown_pack.png");
|
||||
}
|
||||
} else {
|
||||
try (FileSystem fs = CompressingUtils.createReadOnlyZipFileSystem(getPackInfo().getPath())) {
|
||||
@@ -301,7 +302,6 @@ final class DataPackListPageSkin extends SkinBase<DataPackListPage> {
|
||||
}
|
||||
} catch (Exception e) {
|
||||
LOG.warning("fail to load image, datapack path: " + getPackInfo().getPath(), e);
|
||||
return FXUtils.newBuiltinImage("/assets/img/unknown_pack.png");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -309,36 +309,9 @@ final class DataPackListPageSkin extends SkinBase<DataPackListPage> {
|
||||
Math.abs(image.getWidth() - image.getHeight()) < 1) {
|
||||
return image;
|
||||
} else {
|
||||
return FXUtils.newBuiltinImage("/assets/img/unknown_pack.png");
|
||||
return getDefaultIcon();
|
||||
}
|
||||
}
|
||||
|
||||
public void loadIcon(ImageContainer imageContainer, @Nullable WeakReference<ObjectProperty<DataPackInfoObject>> current) {
|
||||
SoftReference<CompletableFuture<Image>> iconCache = this.iconCache;
|
||||
CompletableFuture<Image> imageFuture;
|
||||
if (iconCache != null && (imageFuture = iconCache.get()) != null) {
|
||||
Image image = imageFuture.getNow(null);
|
||||
if (image != null) {
|
||||
imageContainer.setImage(image);
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
imageFuture = CompletableFuture.supplyAsync(this::loadIcon, Schedulers.io());
|
||||
this.iconCache = new SoftReference<>(imageFuture);
|
||||
}
|
||||
imageContainer.setImage(FXUtils.newBuiltinImage("/assets/img/unknown_pack.png"));
|
||||
imageFuture.thenAcceptAsync(image -> {
|
||||
if (current != null) {
|
||||
ObjectProperty<DataPackInfoObject> infoObjectProperty = current.get();
|
||||
if (infoObjectProperty == null || infoObjectProperty.get() != this) {
|
||||
// The current ListCell has already switched to another object
|
||||
return;
|
||||
}
|
||||
}
|
||||
imageContainer.setImage(image);
|
||||
}, Schedulers.javafx());
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
private final class DataPackInfoListCell extends MDListCell<DataPackInfoObject> {
|
||||
@@ -377,7 +350,7 @@ final class DataPackListPageSkin extends SkinBase<DataPackListPage> {
|
||||
checkBox.selectedProperty().unbindBidirectional(booleanProperty);
|
||||
}
|
||||
checkBox.selectedProperty().bindBidirectional(booleanProperty = dataItem.activeProperty);
|
||||
dataItem.loadIcon(imageContainer, new WeakReference<>(this.itemProperty()));
|
||||
dataItem.iconCache.attachValue(imageContainer.imageProperty(), new WeakReference<>(this.itemProperty()));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -117,7 +117,7 @@ public class DownloadListPage extends Control implements DecoratorPage {
|
||||
|
||||
if (!searchInitialized) {
|
||||
searchInitialized = true;
|
||||
search("", null, 0, "", RemoteAddonRepository.SortType.POPULARITY);
|
||||
search("", null, 0, "", RemoteAddonRepository.SortType.RELEVANCY);
|
||||
}
|
||||
|
||||
if (instanceSelection) {
|
||||
@@ -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);
|
||||
@@ -182,8 +186,8 @@ public class DownloadListPage extends Control implements DecoratorPage {
|
||||
|
||||
setLoading(false);
|
||||
if (exception == null) {
|
||||
items.setAll(result.getResults().collect(Collectors.toList()));
|
||||
pageCount.set(result.getTotalPages());
|
||||
items.setAll(result.results().collect(Collectors.toList()));
|
||||
pageCount.set(result.totalPages());
|
||||
failed.set(false);
|
||||
} else {
|
||||
failed.set(true);
|
||||
@@ -371,7 +375,7 @@ public class DownloadListPage extends Control implements DecoratorPage {
|
||||
sortStackPane.getChildren().setAll(sortComboBox);
|
||||
sortComboBox.prefWidthProperty().bind(sortStackPane.widthProperty());
|
||||
sortComboBox.getStyleClass().add("fit-width");
|
||||
sortComboBox.setConverter(stringConverter(sortType -> i18n("curse.sort." + sortType.name().toLowerCase(Locale.ROOT))));
|
||||
sortComboBox.setConverter(stringConverter(sortType -> i18n("addon.sort." + sortType.name().toLowerCase(Locale.ROOT))));
|
||||
sortComboBox.getItems().setAll(RemoteAddonRepository.SortType.values());
|
||||
sortComboBox.getSelectionModel().select(0);
|
||||
searchPane.addRow(rowIndex++, new Label(i18n("addon.category")), categoryStackPane, new Label(i18n("search.sort")), sortStackPane);
|
||||
|
||||
@@ -29,6 +29,7 @@ import javafx.geometry.Pos;
|
||||
import javafx.scene.control.*;
|
||||
import javafx.scene.layout.*;
|
||||
import javafx.stage.FileChooser;
|
||||
import org.jackhuang.hmcl.addon.AddonLoader;
|
||||
import org.jackhuang.hmcl.download.DownloadProvider;
|
||||
import org.jackhuang.hmcl.game.*;
|
||||
import org.jackhuang.hmcl.addon.mod.ModLoaderType;
|
||||
@@ -281,8 +282,8 @@ public class DownloadPage extends Control implements DecoratorPage {
|
||||
resolve:
|
||||
for (RemoteAddon.Version addonVersion : addonVersions) {
|
||||
if (getSkinnable().type == RemoteAddon.Type.MOD) {
|
||||
for (ModLoaderType loader : addonVersion.loaders()) {
|
||||
if (targetLoaders.contains(loader)) {
|
||||
for (AddonLoader loader : addonVersion.loaders()) {
|
||||
if (loader.type() instanceof ModLoaderType modLoaderType && targetLoaders.contains(modLoaderType)) {
|
||||
list.getContent().addAll(
|
||||
ComponentList.createComponentListTitle(i18n("addon.download.recommend", gameVersion)),
|
||||
new AddonItem(control.addon, addonVersion, control)
|
||||
@@ -460,31 +461,11 @@ public class DownloadPage extends Control implements DecoratorPage {
|
||||
graphicPane.getChildren().setAll(icon);
|
||||
}
|
||||
|
||||
for (ModLoaderType modLoaderType : dataItem.loaders()) {
|
||||
switch (modLoaderType) {
|
||||
case FORGE:
|
||||
content.addTag(i18n("install.installer.forge"));
|
||||
break;
|
||||
case CLEANROOM:
|
||||
content.addTag(i18n("install.installer.cleanroom"));
|
||||
break;
|
||||
case NEO_FORGE:
|
||||
content.addTag(i18n("install.installer.neoforge"));
|
||||
break;
|
||||
case FABRIC:
|
||||
content.addTag(i18n("install.installer.fabric"));
|
||||
break;
|
||||
case LITE_LOADER:
|
||||
content.addTag(i18n("install.installer.liteloader"));
|
||||
break;
|
||||
case QUILT:
|
||||
content.addTag(i18n("install.installer.quilt"));
|
||||
break;
|
||||
case LEGACY_FABRIC:
|
||||
content.addTag(i18n("install.installer.legacyfabric"));
|
||||
break;
|
||||
}
|
||||
Set<String> tags = new LinkedHashSet<>();
|
||||
for (AddonLoader loader : dataItem.loaders()) {
|
||||
tags.add(I18n.translateLoaderName(loader));
|
||||
}
|
||||
content.addTags(tags);
|
||||
|
||||
descPane.getChildren().setAll(graphicPane, content);
|
||||
}
|
||||
|
||||
@@ -17,7 +17,10 @@
|
||||
*/
|
||||
package org.jackhuang.hmcl.ui.instances;
|
||||
|
||||
import com.jfoenix.controls.JFXButton;
|
||||
import com.jfoenix.controls.JFXDialogLayout;
|
||||
import javafx.scene.Node;
|
||||
import javafx.scene.control.Label;
|
||||
import javafx.scene.image.ImageView;
|
||||
import javafx.scene.layout.FlowPane;
|
||||
import javafx.stage.FileChooser;
|
||||
@@ -27,17 +30,18 @@ import org.jackhuang.hmcl.setting.GameInstanceIconType;
|
||||
import org.jackhuang.hmcl.ui.Controllers;
|
||||
import org.jackhuang.hmcl.ui.FXUtils;
|
||||
import org.jackhuang.hmcl.ui.SVG;
|
||||
import org.jackhuang.hmcl.ui.construct.DialogPane;
|
||||
import org.jackhuang.hmcl.ui.construct.DialogCloseEvent;
|
||||
import org.jackhuang.hmcl.ui.construct.RipplerContainer;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Path;
|
||||
|
||||
import static org.jackhuang.hmcl.ui.FXUtils.onEscPressed;
|
||||
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
|
||||
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
|
||||
|
||||
public class GameInstanceIconDialog extends DialogPane {
|
||||
public class GameInstanceIconDialog extends JFXDialogLayout {
|
||||
private final HMCLGameInstance gameInstance;
|
||||
private final Runnable onFinish;
|
||||
private final GameSettings.@Nullable Instance setting;
|
||||
@@ -47,10 +51,9 @@ public class GameInstanceIconDialog extends DialogPane {
|
||||
this.onFinish = onFinish;
|
||||
this.setting = gameInstance.getSettingsOrCreate();
|
||||
|
||||
setTitle(i18n("settings.icon"));
|
||||
FlowPane pane = new FlowPane();
|
||||
setBody(pane);
|
||||
setHeading(new Label(i18n("settings.icon")));
|
||||
|
||||
FlowPane pane = new FlowPane();
|
||||
pane.getChildren().setAll(
|
||||
createCustomIcon(),
|
||||
createIcon(GameInstanceIconType.GRASS),
|
||||
@@ -68,6 +71,14 @@ public class GameInstanceIconDialog extends DialogPane {
|
||||
createIcon(GameInstanceIconType.FURNACE),
|
||||
createIcon(GameInstanceIconType.QUILT)
|
||||
);
|
||||
setBody(pane);
|
||||
|
||||
JFXButton cancelButton = new JFXButton(i18n("button.cancel"));
|
||||
cancelButton.getStyleClass().add("dialog-cancel");
|
||||
cancelButton.setOnAction(event -> fireEvent(new DialogCloseEvent()));
|
||||
onEscPressed(this, cancelButton::fire);
|
||||
|
||||
setActions(cancelButton);
|
||||
}
|
||||
|
||||
private void exploreIcon() {
|
||||
@@ -114,10 +125,9 @@ public class GameInstanceIconDialog extends DialogPane {
|
||||
return container;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onAccept() {
|
||||
// Icon file / settings.iconProperty updates already invalidate iconImageProperty.
|
||||
onFinish.run();
|
||||
super.onAccept();
|
||||
fireEvent(new DialogCloseEvent());
|
||||
}
|
||||
}
|
||||
@@ -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),
|
||||
|
||||
@@ -19,6 +19,7 @@ package org.jackhuang.hmcl.ui.instances;
|
||||
|
||||
import com.jfoenix.controls.JFXButton;
|
||||
import com.jfoenix.controls.JFXListView;
|
||||
import com.jfoenix.controls.JFXPopup;
|
||||
import com.jfoenix.controls.JFXTextField;
|
||||
import javafx.animation.PauseTransition;
|
||||
import javafx.beans.binding.Bindings;
|
||||
@@ -38,10 +39,7 @@ import javafx.scene.control.Skin;
|
||||
import javafx.scene.control.SkinBase;
|
||||
import javafx.scene.input.KeyCode;
|
||||
import javafx.scene.input.KeyEvent;
|
||||
import javafx.scene.layout.HBox;
|
||||
import javafx.scene.layout.Priority;
|
||||
import javafx.scene.layout.StackPane;
|
||||
import javafx.scene.layout.VBox;
|
||||
import javafx.scene.layout.*;
|
||||
import javafx.util.Duration;
|
||||
import org.jackhuang.hmcl.game.HMCLGameRepository;
|
||||
import org.jackhuang.hmcl.game.ModpackHelper;
|
||||
@@ -49,10 +47,7 @@ import org.jackhuang.hmcl.setting.GameDirectoryManager;
|
||||
import org.jackhuang.hmcl.ui.*;
|
||||
import org.jackhuang.hmcl.ui.animation.ContainerAnimations;
|
||||
import org.jackhuang.hmcl.ui.animation.TransitionPane;
|
||||
import org.jackhuang.hmcl.ui.construct.AdvancedListBox;
|
||||
import org.jackhuang.hmcl.ui.construct.AdvancedListItem;
|
||||
import org.jackhuang.hmcl.ui.construct.ComponentList;
|
||||
import org.jackhuang.hmcl.ui.construct.SpinnerPane;
|
||||
import org.jackhuang.hmcl.ui.construct.*;
|
||||
import org.jackhuang.hmcl.ui.decorator.DecoratorAnimatedPage;
|
||||
import org.jackhuang.hmcl.ui.decorator.DecoratorPage;
|
||||
import org.jackhuang.hmcl.ui.directory.GameDirectoryListItem;
|
||||
@@ -84,6 +79,16 @@ public class GameListPage extends DecoratorAnimatedPage implements DecoratorPage
|
||||
gameDirectoryListItems = MappedObservableList.create(GameDirectoryManager.getGameDirectories(), gameDirectory -> {
|
||||
GameDirectoryListItem item = new GameDirectoryListItem(gameDirectory);
|
||||
FXUtils.setLimitWidth(item, 200);
|
||||
FXUtils.onSecondaryButtonClicked(item, () -> {
|
||||
PopupMenu menu = new PopupMenu();
|
||||
JFXPopup popup = new JFXPopup(menu);
|
||||
menu.getContent().add(new IconedMenuItem(
|
||||
SVG.EDIT,
|
||||
i18n("button.edit"),
|
||||
() -> Controllers.navigate(new GameDirectoryPage(gameDirectory)),
|
||||
popup));
|
||||
popup.show(item, JFXPopup.PopupVPosition.TOP, JFXPopup.PopupHPosition.LEFT, item.getWidth(), 0);
|
||||
});
|
||||
return item;
|
||||
});
|
||||
|
||||
|
||||
@@ -73,7 +73,9 @@ public final class GameListPopupMenu extends StackPane {
|
||||
public static void show(Node owner, JFXPopup.PopupVPosition vAlign, JFXPopup.PopupHPosition hAlign,
|
||||
double initOffsetX, double initOffsetY,
|
||||
List<HMCLGameInstance> instances) {
|
||||
showAndGetPopup(owner, vAlign, hAlign, initOffsetX, initOffsetY, instances);
|
||||
if (!hideShowing(owner)) {
|
||||
showAndGetPopup(owner, vAlign, hAlign, initOffsetX, initOffsetY, instances);
|
||||
}
|
||||
}
|
||||
|
||||
/// Shows and returns an instance selection popup relative to its owner.
|
||||
@@ -117,9 +119,7 @@ public final class GameListPopupMenu extends StackPane {
|
||||
Label placeholder = new Label(i18n("instance.empty"));
|
||||
placeholder.setStyle("-fx-padding: 10px; -fx-text-fill: -monet-on-surface-variant; -fx-font-style: italic;");
|
||||
|
||||
FXUtils.onChangeAndOperate(isEmpty, empty -> {
|
||||
getChildren().setAll(empty ? placeholder : listView);
|
||||
});
|
||||
FXUtils.onChangeAndOperate(isEmpty, empty -> getChildren().setAll(empty ? placeholder : listView));
|
||||
}
|
||||
|
||||
public ObservableList<GameItem> getItems() {
|
||||
|
||||
+4
-9
@@ -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);
|
||||
}
|
||||
@@ -102,15 +106,6 @@ public final class HMCLLocalizedDownloadListPage extends DownloadListPage {
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected SortType getBackedRemoteModRepositorySortOrder() {
|
||||
if ("addon.modrinth".equals(downloadSource.get())) {
|
||||
return SortType.NAME;
|
||||
} else {
|
||||
return SortType.POPULARITY;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public RemoteAddon.Type getType() {
|
||||
return type;
|
||||
|
||||
@@ -66,8 +66,9 @@ public class InstallerListPage extends ListPageBase<InstallerItem> {
|
||||
return new InstallerListPageSkin();
|
||||
}
|
||||
|
||||
/// Rebuilds the component controls for the selected instance, or clears them if none is selected.
|
||||
public void loadInstance(HMCLGameInstance.Optional instance) {
|
||||
HMCLGameInstance gameInstance = instance.instance();
|
||||
@Nullable HMCLGameInstance gameInstance = instance.instance();
|
||||
this.gameInstance = gameInstance;
|
||||
if (gameInstance == null) {
|
||||
itemsProperty().clear();
|
||||
@@ -103,7 +104,7 @@ public class InstallerListPage extends ListPageBase<InstallerItem> {
|
||||
Controllers.getDecorator().startWizard(new UpdateInstallerWizardProvider(gameInstance, component.getComponentType(), libraryVersion));
|
||||
});
|
||||
|
||||
component.setOnRemove(() -> repository.updateInstanceAsync(
|
||||
component.setOnRemove(() -> repository.getDependency().updateInstanceAsync(
|
||||
gameInstance.getId(),
|
||||
publishedInstance -> repository.getDependency().removeComponentAsync(
|
||||
publishedInstance,
|
||||
@@ -123,7 +124,7 @@ public class InstallerListPage extends ListPageBase<InstallerItem> {
|
||||
|
||||
InstallerItem installerItem = new InstallerItem(mark.componentType(), InstallerItem.Style.LIST_ITEM);
|
||||
installerItem.versionProperty().set(new InstallerItem.InstalledState(mark.version(), false, false));
|
||||
installerItem.setOnRemove(() -> repository.updateInstanceAsync(
|
||||
installerItem.setOnRemove(() -> repository.getDependency().updateInstanceAsync(
|
||||
gameInstance.getId(),
|
||||
publishedInstance -> repository.getDependency().removeComponentAsync(
|
||||
publishedInstance,
|
||||
@@ -148,13 +149,14 @@ public class InstallerListPage extends ListPageBase<InstallerItem> {
|
||||
if (file != null) doInstallOffline(file);
|
||||
}
|
||||
|
||||
/// Runs a local component installer for the current modifiable instance and displays its result.
|
||||
private void doInstallOffline(Path file) {
|
||||
if (gameInstance == null || !gameInstance.getManifest().isModifiable()) {
|
||||
return;
|
||||
}
|
||||
|
||||
HMCLGameRepository repository = gameInstance.getRepository();
|
||||
Task<?> task = repository.updateInstanceAsync(
|
||||
Task<?> task = repository.getDependency().updateInstanceAsync(
|
||||
gameInstance.getId(),
|
||||
publishedInstance -> repository.getDependency().installComponentLocalAsync(publishedInstance, file));
|
||||
task.setName(i18n("install.installer.install_offline"));
|
||||
|
||||
@@ -23,7 +23,6 @@ import javafx.application.Platform;
|
||||
import javafx.beans.binding.Bindings;
|
||||
import javafx.beans.binding.DoubleBinding;
|
||||
import javafx.beans.property.BooleanProperty;
|
||||
import javafx.beans.property.ObjectProperty;
|
||||
import javafx.beans.property.SimpleBooleanProperty;
|
||||
import javafx.beans.value.ObservableValue;
|
||||
import javafx.collections.ListChangeListener;
|
||||
@@ -41,6 +40,7 @@ import javafx.scene.layout.Priority;
|
||||
import javafx.scene.layout.StackPane;
|
||||
import javafx.stage.FileChooser;
|
||||
import javafx.util.Duration;
|
||||
import org.jackhuang.hmcl.addon.AddonLoader;
|
||||
import org.jackhuang.hmcl.addon.RemoteAddon;
|
||||
import org.jackhuang.hmcl.addon.RemoteAddonRepository;
|
||||
import org.jackhuang.hmcl.addon.repository.CurseForgeRemoteAddonRepository;
|
||||
@@ -63,13 +63,13 @@ import org.jackhuang.hmcl.util.i18n.I18n;
|
||||
import org.jackhuang.hmcl.util.io.CompressingUtils;
|
||||
import org.jackhuang.hmcl.util.io.FileUtils;
|
||||
import org.jackhuang.hmcl.util.io.NetworkUtils;
|
||||
import org.jackhuang.hmcl.util.javafx.ItemPropertyAsyncCache;
|
||||
import org.jackhuang.hmcl.util.versioning.GameVersionNumber;
|
||||
import org.jetbrains.annotations.NotNullByDefault;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.UncheckedIOException;
|
||||
import java.lang.ref.SoftReference;
|
||||
import java.lang.ref.WeakReference;
|
||||
import java.nio.file.FileSystem;
|
||||
import java.nio.file.Files;
|
||||
@@ -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"));
|
||||
@@ -598,13 +551,15 @@ public final class ModListPage extends ListPageBase<ModListPage.ModInfoObject> i
|
||||
private final LocalModFile localModFile;
|
||||
private final @Nullable ModTranslations.Mod modTranslations;
|
||||
|
||||
private SoftReference<CompletableFuture<Image>> iconCache;
|
||||
private final ItemPropertyAsyncCache<Image, ModInfoObject> iconCache;
|
||||
|
||||
ModInfoObject(LocalModFile localModFile) {
|
||||
this.localModFile = localModFile;
|
||||
this.active = localModFile.activeProperty();
|
||||
|
||||
this.modTranslations = ModTranslations.MOD.getMod(localModFile.getId(), localModFile.getName());
|
||||
|
||||
this.iconCache = new ItemPropertyAsyncCache.Soft<>(this, this::loadIcon, this::getDefaultIcon);
|
||||
}
|
||||
|
||||
public LocalModFile getModInfo() {
|
||||
@@ -615,7 +570,10 @@ public final class ModListPage extends ListPageBase<ModListPage.ModInfoObject> i
|
||||
return modTranslations;
|
||||
}
|
||||
|
||||
@FXThread
|
||||
private Image getDefaultIcon() {
|
||||
return GameInstanceIconType.getIconType(this.localModFile.getModLoaderType()).getIcon();
|
||||
}
|
||||
|
||||
private Image loadIcon() {
|
||||
List<String> iconPaths = new ArrayList<>();
|
||||
|
||||
@@ -638,34 +596,7 @@ public final class ModListPage extends ListPageBase<ModListPage.ModInfoObject> i
|
||||
LOG.warning("Failed to load mod icons", e);
|
||||
}
|
||||
|
||||
return GameInstanceIconType.getIconType(this.localModFile.getModLoaderType()).getIcon();
|
||||
}
|
||||
|
||||
public void loadIcon(ImageContainer imageContainer, @Nullable WeakReference<ObjectProperty<ModInfoObject>> current) {
|
||||
SoftReference<CompletableFuture<Image>> iconCache = this.iconCache;
|
||||
CompletableFuture<Image> imageFuture;
|
||||
if (iconCache != null && (imageFuture = iconCache.get()) != null) {
|
||||
Image image = imageFuture.getNow(null);
|
||||
if (image != null) {
|
||||
imageContainer.setImage(image);
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
imageFuture = CompletableFuture.supplyAsync(this::loadIcon, Schedulers.io());
|
||||
this.iconCache = new SoftReference<>(imageFuture);
|
||||
}
|
||||
imageContainer.setImage(GameInstanceIconType.getIconType(localModFile.getModLoaderType()).getIcon());
|
||||
imageFuture.thenAcceptAsync(image -> {
|
||||
if (current != null) {
|
||||
ObjectProperty<ModInfoObject> infoObjectProperty = current.get();
|
||||
if (infoObjectProperty == null || infoObjectProperty.get() != this) {
|
||||
// The current ListCell has already switched to another object
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
imageContainer.setImage(image);
|
||||
}, Schedulers.javafx());
|
||||
return getDefaultIcon();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -682,9 +613,10 @@ public final class ModListPage extends ListPageBase<ModListPage.ModInfoObject> i
|
||||
|
||||
var imageContainer = new ImageContainer(40);
|
||||
titleContainer.setAlignment(Pos.CENTER_LEFT);
|
||||
modInfo.loadIcon(imageContainer, null);
|
||||
modInfo.iconCache.attachValue(imageContainer.imageProperty(), null);
|
||||
|
||||
TwoLineListItem title = new TwoLineListItem();
|
||||
title.getTitleLabel().setWrapText(true);
|
||||
if (modInfo.getModTranslations() != null && I18n.isUseChinese())
|
||||
title.setTitle(modInfo.getModTranslations().getDisplayName());
|
||||
else
|
||||
@@ -735,25 +667,12 @@ public final class ModListPage extends ListPageBase<ModListPage.ModInfoObject> i
|
||||
if (versionOptional.isPresent()) {
|
||||
RemoteAddon remoteAddon = repository.getAddonById(DownloadProviders.getDownloadProvider(), versionOptional.get().projectId());
|
||||
FXUtils.runInFX(() -> {
|
||||
for (ModLoaderType modLoaderType : versionOptional.get().loaders()) {
|
||||
String loaderName = switch (modLoaderType) {
|
||||
case FORGE -> i18n("install.installer.forge");
|
||||
case CLEANROOM -> i18n("install.installer.cleanroom");
|
||||
case LEGACY_FABRIC -> i18n("install.installer.legacyfabric");
|
||||
case NEO_FORGE -> i18n("install.installer.neoforge");
|
||||
case FABRIC -> i18n("install.installer.fabric");
|
||||
case LITE_LOADER -> i18n("install.installer.liteloader");
|
||||
case QUILT -> i18n("install.installer.quilt");
|
||||
default -> null;
|
||||
};
|
||||
if (loaderName == null)
|
||||
continue;
|
||||
if (title.getTags()
|
||||
.stream()
|
||||
.noneMatch(it -> it.getText().equals(loaderName))) {
|
||||
title.addTag(loaderName);
|
||||
}
|
||||
Set<String> tags = new LinkedHashSet<>();
|
||||
for (AddonLoader loader : versionOptional.get().loaders()) {
|
||||
String tag = I18n.translateLoaderName(loader);
|
||||
if (tag != null) tags.add(tag);
|
||||
}
|
||||
title.addTagsIfNotExist(tags);
|
||||
|
||||
button.setExternalLink(remoteAddon.pageUrl());
|
||||
button.setDisable(false);
|
||||
@@ -856,7 +775,7 @@ public final class ModListPage extends ListPageBase<ModListPage.ModInfoObject> i
|
||||
|
||||
ModLoaderType modLoaderType = modInfo.getModLoaderType();
|
||||
|
||||
dataItem.loadIcon(imageContainer, new WeakReference<>(this.itemProperty()));
|
||||
dataItem.iconCache.attachValue(imageContainer.imageProperty(), new WeakReference<>(this.itemProperty()));
|
||||
|
||||
String displayName = modInfo.getName();
|
||||
if (modTranslations != null && I18n.isUseChinese()) {
|
||||
@@ -892,15 +811,7 @@ public final class ModListPage extends ListPageBase<ModListPage.ModInfoObject> i
|
||||
content.addTagWarning(i18n("mods.unknown"));
|
||||
} else if (!page.supportedLoaders.contains(modLoaderType)) {
|
||||
warning.add(i18n("mods.warning.loader_mismatch"));
|
||||
switch (dataItem.getModInfo().getModLoaderType()) {
|
||||
case FORGE -> content.addTagWarning(i18n("install.installer.forge"));
|
||||
case LEGACY_FABRIC -> content.addTagWarning(i18n("install.installer.legacyfabric"));
|
||||
case CLEANROOM -> content.addTagWarning(i18n("install.installer.cleanroom"));
|
||||
case NEO_FORGE -> content.addTagWarning(i18n("install.installer.neoforge"));
|
||||
case FABRIC -> content.addTagWarning(i18n("install.installer.fabric"));
|
||||
case LITE_LOADER -> content.addTagWarning(i18n("install.installer.liteloader"));
|
||||
case QUILT -> content.addTagWarning(i18n("install.installer.quilt"));
|
||||
}
|
||||
content.addTagWarning(I18n.translateLoaderType(dataItem.getModInfo().getModLoaderType()));
|
||||
}
|
||||
|
||||
String modVersion = modInfo.getVersion();
|
||||
|
||||
@@ -57,14 +57,14 @@ import org.jackhuang.hmcl.ui.WeakListenerHolder;
|
||||
import org.jackhuang.hmcl.ui.animation.ContainerAnimations;
|
||||
import org.jackhuang.hmcl.ui.animation.TransitionPane;
|
||||
import org.jackhuang.hmcl.ui.construct.*;
|
||||
import org.jackhuang.hmcl.util.Pair;
|
||||
import org.jackhuang.hmcl.util.StringUtils;
|
||||
import org.jackhuang.hmcl.util.TaskCancellationAction;
|
||||
import org.jackhuang.hmcl.util.*;
|
||||
import org.jackhuang.hmcl.util.javafx.ItemPropertyAsyncCache;
|
||||
import org.jackhuang.hmcl.util.versioning.GameVersionNumber;
|
||||
import org.jetbrains.annotations.NotNullByDefault;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.lang.ref.WeakReference;
|
||||
import java.nio.file.Path;
|
||||
import java.util.*;
|
||||
import java.util.Objects;
|
||||
@@ -494,13 +494,18 @@ public final class ResourcePackListPage extends ListPageBase<ResourcePackListPag
|
||||
}
|
||||
}
|
||||
|
||||
public static class ResourcePackInfoObject {
|
||||
public static final class ResourcePackInfoObject {
|
||||
|
||||
private final ResourcePackFile file;
|
||||
private final BooleanProperty enabled;
|
||||
|
||||
private final ItemPropertyAsyncCache<Image, ResourcePackInfoObject> iconCache;
|
||||
|
||||
public ResourcePackInfoObject(Pair<ResourcePackFile, Boolean> pair) {
|
||||
this.file = pair.key();
|
||||
this.enabled = new SimpleBooleanProperty(this, "enabled", pair.value());
|
||||
|
||||
this.iconCache = new ItemPropertyAsyncCache.Soft<>(this, this::loadIcon, this::getDefaultIcon);
|
||||
}
|
||||
|
||||
public ResourcePackFile getFile() {
|
||||
@@ -511,13 +516,16 @@ public final class ResourcePackListPage extends ListPageBase<ResourcePackListPag
|
||||
return enabled;
|
||||
}
|
||||
|
||||
Image getIcon() {
|
||||
Image image = file.getIcon();
|
||||
if (image == null || image.isError() || image.getWidth() <= 0 || image.getHeight() <= 0 ||
|
||||
(Math.abs(image.getWidth() - image.getHeight()) >= 1)) {
|
||||
image = FXUtils.newBuiltinImage("/assets/img/unknown_pack.png");
|
||||
private Image getDefaultIcon() {
|
||||
return FXUtils.newBuiltinImage("/assets/img/unknown_pack.png");
|
||||
}
|
||||
|
||||
private Image loadIcon() {
|
||||
Image icon = file.loadIcon();
|
||||
if (icon != null && !icon.isError() && icon.getWidth() > 0 && icon.getHeight() > 0 && Math.abs(icon.getWidth() - icon.getHeight()) < 1) {
|
||||
return icon;
|
||||
}
|
||||
return image;
|
||||
return getDefaultIcon();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -583,7 +591,7 @@ public final class ResourcePackListPage extends ListPageBase<ResourcePackListPag
|
||||
if (empty || item == null) return;
|
||||
|
||||
ResourcePackFile file = item.getFile();
|
||||
imageContainer.setImage(item.getIcon());
|
||||
item.iconCache.attachValue(imageContainer.imageProperty(), new WeakReference<>(itemProperty()));
|
||||
|
||||
content.getTags().clear();
|
||||
content.setTitle(file.getFileName());
|
||||
@@ -623,7 +631,7 @@ public final class ResourcePackListPage extends ListPageBase<ResourcePackListPag
|
||||
maxWidthProperty().bind(Controllers.getDecorator().contentWidthProperty().multiply(0.7));
|
||||
|
||||
ImageContainer imageContainer = new ImageContainer(40);
|
||||
imageContainer.setImage(packInfoObject.getIcon());
|
||||
packInfoObject.iconCache.attachValue(imageContainer.imageProperty(), null);
|
||||
|
||||
TwoLineListItem title = new TwoLineListItem();
|
||||
title.setTitle(pack.getFileName());
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -435,7 +435,7 @@ public class PersonalizationPage extends StackPane {
|
||||
LineSelectButton<String> brightnessPane = new LineSelectButton<>();
|
||||
brightnessPane.setTitle(i18n("settings.launcher.brightness"));
|
||||
brightnessPane.setConverter(name -> i18n("settings.launcher.brightness."
|
||||
+ Objects.requireNonNullElse(name, "auto")));
|
||||
+ Objects.requireNonNullElse(name, "auto").trim().toLowerCase(Locale.ROOT)));
|
||||
brightnessPane.setItems(Arrays.asList("auto", "light", "dark"));
|
||||
bindThemeAppearanceLineSelectButton(
|
||||
brightnessPane,
|
||||
|
||||
@@ -37,10 +37,7 @@ import org.jackhuang.hmcl.ui.FXUtils;
|
||||
import org.jackhuang.hmcl.ui.SVG;
|
||||
import org.jackhuang.hmcl.ui.construct.*;
|
||||
import org.jackhuang.hmcl.ui.construct.MessageDialogPane.MessageType;
|
||||
import org.jackhuang.hmcl.upgrade.RemoteVersion;
|
||||
import org.jackhuang.hmcl.upgrade.UpdateChannel;
|
||||
import org.jackhuang.hmcl.upgrade.UpdateChecker;
|
||||
import org.jackhuang.hmcl.upgrade.UpdateHandler;
|
||||
import org.jackhuang.hmcl.upgrade.*;
|
||||
import org.jackhuang.hmcl.util.Lang;
|
||||
import org.jackhuang.hmcl.util.StringUtils;
|
||||
import org.jackhuang.hmcl.util.i18n.I18n;
|
||||
@@ -121,10 +118,18 @@ public final class SettingsPage extends ScrollPane {
|
||||
updateButton.setManaged(outdated);
|
||||
updatePane.pseudoClassStateChanged(PseudoClass.getPseudoClass("active"), outdated);
|
||||
|
||||
if (UpdateChecker.isOutdated()) {
|
||||
if (outdated) {
|
||||
lblUpdateSubProperty.set(i18n("update.newest_version", UpdateChecker.getLatestVersion().version()));
|
||||
} else if (UpdateChecker.isCheckingUpdate()) {
|
||||
lblUpdateSubProperty.set(i18n("update.checking"));
|
||||
} else if (UpdateChecker.errorProperty().get() != null) {
|
||||
Throwable t = UpdateChecker.errorProperty().get();
|
||||
if (t instanceof SelfVerificationException) {
|
||||
lblUpdateSubProperty.set(i18n("update.unverified"));
|
||||
} else {
|
||||
String msg = t.getClass().getSimpleName() + ": " + t.getLocalizedMessage();
|
||||
lblUpdateSubProperty.set(i18n("update.check_failed", msg));
|
||||
}
|
||||
} else {
|
||||
lblUpdateSubProperty.set(i18n("update.latest"));
|
||||
}
|
||||
@@ -132,6 +137,7 @@ public final class SettingsPage extends ScrollPane {
|
||||
UpdateChecker.latestVersionProperty().addListener(new WeakInvalidationListener(updateListener));
|
||||
UpdateChecker.outdatedProperty().addListener(new WeakInvalidationListener(updateListener));
|
||||
UpdateChecker.checkingUpdateProperty().addListener(new WeakInvalidationListener(updateListener));
|
||||
UpdateChecker.errorProperty().addListener(new WeakInvalidationListener(updateListener));
|
||||
updateListener.invalidated(null);
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
+27
-6
@@ -28,6 +28,7 @@ import org.jackhuang.hmcl.ui.construct.TaskExecutorDialogPane;
|
||||
import org.jackhuang.hmcl.util.SettingsMap;
|
||||
import org.jackhuang.hmcl.util.StringUtils;
|
||||
import org.jackhuang.hmcl.util.TaskCancellationAction;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import java.util.Queue;
|
||||
import java.util.concurrent.CancellationException;
|
||||
@@ -41,9 +42,16 @@ public abstract class TaskExecutorDialogWizardDisplayer extends AbstractWizardDi
|
||||
super(cancelQueue);
|
||||
}
|
||||
|
||||
/// Displays a task and releases its optional resources after execution stops.
|
||||
@Override
|
||||
public void handleTask(SettingsMap settings, Task<?> task) {
|
||||
@Nullable WizardProvider.TaskCleanup cleanup = settings.remove(WizardProvider.TaskCleanup.KEY);
|
||||
TaskExecutorDialogPane pane = new TaskExecutorDialogPane(new TaskCancellationAction(it -> {
|
||||
if (cleanup != null) {
|
||||
// Keep the installation modal until onStop has released its repository draft.
|
||||
it.setCancel(null);
|
||||
return;
|
||||
}
|
||||
it.fireEvent(new DialogCloseEvent());
|
||||
onEnd();
|
||||
}));
|
||||
@@ -61,24 +69,37 @@ public abstract class TaskExecutorDialogWizardDisplayer extends AbstractWizardDi
|
||||
TaskExecutor executor = task.executor(new TaskListener() {
|
||||
@Override
|
||||
public void onStop(boolean success, TaskExecutor executor) {
|
||||
@Nullable Exception failure = success ? null : executor.getException();
|
||||
if (cleanup != null) {
|
||||
try {
|
||||
cleanup.cleanup();
|
||||
} catch (Exception cleanupFailure) {
|
||||
if (failure == null || failure instanceof CancellationException) {
|
||||
failure = cleanupFailure;
|
||||
} else if (failure != cleanupFailure) {
|
||||
failure.addSuppressed(cleanupFailure);
|
||||
}
|
||||
}
|
||||
}
|
||||
@Nullable Exception completedFailure = failure;
|
||||
runInFX(() -> {
|
||||
if (success) {
|
||||
if (success && completedFailure == null) {
|
||||
if (settings.get("success_message") instanceof String successMessage)
|
||||
Controllers.dialog(successMessage, null, MessageType.SUCCESS, () -> onEnd());
|
||||
else if (!settings.containsKey("forbid_success_message"))
|
||||
Controllers.dialog(i18n("message.success"), null, MessageType.SUCCESS, () -> onEnd());
|
||||
} else {
|
||||
if (executor.getException() == null) {
|
||||
if (completedFailure == null) {
|
||||
onEnd();
|
||||
return;
|
||||
}
|
||||
|
||||
if (executor.getException() instanceof CancellationException) {
|
||||
if (completedFailure instanceof CancellationException) {
|
||||
onEnd();
|
||||
return;
|
||||
}
|
||||
|
||||
if (executor.getException().getCause() instanceof OutOfMemoryError outOfMemoryError) {
|
||||
if (completedFailure.getCause() instanceof OutOfMemoryError outOfMemoryError) {
|
||||
try {
|
||||
Controllers.dialog(StringUtils.getStackTrace(outOfMemoryError), null, MessageType.ERROR, () -> onEnd());
|
||||
} catch (OutOfMemoryError ignored) {
|
||||
@@ -87,9 +108,9 @@ public abstract class TaskExecutorDialogWizardDisplayer extends AbstractWizardDi
|
||||
return;
|
||||
}
|
||||
|
||||
String appendix = StringUtils.getStackTrace(executor.getException());
|
||||
String appendix = StringUtils.getStackTrace(completedFailure);
|
||||
if (settings.get(WizardProvider.FailureCallback.KEY) != null)
|
||||
settings.get(WizardProvider.FailureCallback.KEY).onFail(settings, executor.getException(), () -> onEnd());
|
||||
settings.get(WizardProvider.FailureCallback.KEY).onFail(settings, completedFailure, () -> onEnd());
|
||||
else if (settings.get("failure_message") instanceof String failureMessage)
|
||||
Controllers.dialog(appendix, failureMessage, MessageType.ERROR, () -> onEnd());
|
||||
else if (!settings.containsKey("forbid_failure_message"))
|
||||
|
||||
@@ -19,6 +19,7 @@ package org.jackhuang.hmcl.ui.wizard;
|
||||
|
||||
import javafx.scene.Node;
|
||||
import org.jackhuang.hmcl.util.SettingsMap;
|
||||
import org.jetbrains.annotations.NotNullByDefault;
|
||||
|
||||
public interface WizardProvider {
|
||||
|
||||
@@ -39,4 +40,20 @@ public interface WizardProvider {
|
||||
|
||||
void onFail(SettingsMap settings, Exception exception, Runnable next);
|
||||
}
|
||||
|
||||
/// Releases resources owned by a task displayed by [TaskExecutorDialogWizardDisplayer].
|
||||
///
|
||||
/// Cleanup runs on the executor's completion thread after task execution stops, including
|
||||
/// cancellation before the first task starts, and before the dialog reports its outcome.
|
||||
@NotNullByDefault
|
||||
@FunctionalInterface
|
||||
interface TaskCleanup {
|
||||
/// Optional cleanup consumed by the task dialog when it accepts the task.
|
||||
SettingsMap.Key<TaskCleanup> KEY = new SettingsMap.Key<>("task_cleanup");
|
||||
|
||||
/// Releases the completed task's resources.
|
||||
///
|
||||
/// @throws Exception if cleanup fails; the task dialog reports the failure
|
||||
void cleanup() throws Exception;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
/*
|
||||
* 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.upgrade;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
public class SelfVerificationException extends IOException {
|
||||
|
||||
public SelfVerificationException() {
|
||||
super("Self verification failed");
|
||||
}
|
||||
|
||||
}
|
||||
@@ -25,6 +25,7 @@ import javafx.beans.value.ObservableBooleanValue;
|
||||
import org.jackhuang.hmcl.Metadata;
|
||||
import org.jackhuang.hmcl.util.io.NetworkUtils;
|
||||
import org.jackhuang.hmcl.util.versioning.VersionNumber;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.LinkedHashMap;
|
||||
@@ -54,6 +55,7 @@ public final class UpdateChecker {
|
||||
},
|
||||
latestVersion);
|
||||
private static final ReadOnlyBooleanWrapper checkingUpdate = new ReadOnlyBooleanWrapper(false);
|
||||
private static final ReadOnlyObjectWrapper<@Nullable Throwable> error = new ReadOnlyObjectWrapper<>();
|
||||
|
||||
public static void init() {
|
||||
requestCheckUpdate(UpdateChannel.getChannel(), settings().acceptPreviewUpdateProperty().get());
|
||||
@@ -83,9 +85,13 @@ public final class UpdateChecker {
|
||||
return checkingUpdate.getReadOnlyProperty();
|
||||
}
|
||||
|
||||
public static ReadOnlyObjectProperty<@Nullable Throwable> errorProperty() {
|
||||
return error.getReadOnlyProperty();
|
||||
}
|
||||
|
||||
private static RemoteVersion checkUpdate(UpdateChannel channel, boolean preview) throws IOException {
|
||||
if (!IntegrityChecker.DISABLE_SELF_INTEGRITY_CHECK && !IntegrityChecker.isSelfVerified()) {
|
||||
throw new IOException("Self verification failed");
|
||||
throw new SelfVerificationException();
|
||||
}
|
||||
|
||||
var query = new LinkedHashMap<String, String>();
|
||||
@@ -109,16 +115,23 @@ public final class UpdateChecker {
|
||||
|
||||
thread(() -> {
|
||||
RemoteVersion result = null;
|
||||
Throwable t = null;
|
||||
try {
|
||||
result = checkUpdate(channel, preview);
|
||||
LOG.info("Latest version (" + channel + ", preview=" + preview + ") is " + result);
|
||||
} catch (Throwable e) {
|
||||
t = e;
|
||||
LOG.warning("Failed to check for update", e);
|
||||
}
|
||||
Throwable throwable = t;
|
||||
|
||||
RemoteVersion finalResult = result;
|
||||
Platform.runLater(() -> {
|
||||
if (finalResult != null) {
|
||||
if (throwable != null) {
|
||||
latestVersion.set(null);
|
||||
error.set(throwable);
|
||||
} else {
|
||||
error.set(null);
|
||||
latestVersion.set(finalResult);
|
||||
}
|
||||
checkingUpdate.set(false);
|
||||
|
||||
@@ -172,7 +172,7 @@ public final class UpdateHandler {
|
||||
|
||||
Path self = getCurrentLocation();
|
||||
if (!IntegrityChecker.DISABLE_SELF_INTEGRITY_CHECK && !IntegrityChecker.isSelfVerified()) {
|
||||
throw new IOException("Self verification failed");
|
||||
throw new SelfVerificationException();
|
||||
}
|
||||
ExecutableHeaderHelper.copyWithHeader(self, target);
|
||||
|
||||
|
||||
@@ -107,7 +107,7 @@ public final class NativePatcher {
|
||||
.filter(library -> {
|
||||
if (library.classifier() != null && library.classifier().startsWith("natives")
|
||||
&& "org.lwjgl".equals(library.groupId())) {
|
||||
if ((useNativeGLFWorSDL && ("lwjgl-glfw".equals(library.artifactId()) || library.artifactId().contains("sdl")))
|
||||
if ((useNativeGLFWorSDL && ("lwjgl-glfw".equals(library.artifactId()) || "lwjgl-sdl".equals(library.artifactId())))
|
||||
|| (useNativeOpenAL && "lwjgl-openal".equals(library.artifactId()))) {
|
||||
LOG.info("Filter out " + library.name());
|
||||
return false;
|
||||
|
||||
@@ -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) {
|
||||
}
|
||||
}
|
||||
@@ -17,8 +17,44 @@
|
||||
*/
|
||||
package org.jackhuang.hmcl.util.i18n;
|
||||
|
||||
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;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
@@ -124,6 +160,84 @@ public final class I18n {
|
||||
return MinecraftWiki.getWikiLink(locale, remoteVersion);
|
||||
}
|
||||
|
||||
public static String translateLoaderType(AddonLoaderType loaderType) {
|
||||
if (loaderType instanceof ModLoaderType modLoaderType) {
|
||||
return switch (modLoaderType) {
|
||||
case FORGE -> i18n("install.installer.forge");
|
||||
case CLEANROOM -> i18n("install.installer.cleanroom");
|
||||
case NEO_FORGE -> i18n("install.installer.neoforge");
|
||||
case FABRIC -> i18n("install.installer.fabric");
|
||||
case LITE_LOADER -> i18n("install.installer.liteloader");
|
||||
case QUILT -> i18n("install.installer.quilt");
|
||||
case LEGACY_FABRIC -> i18n("install.installer.legacyfabric");
|
||||
default -> modLoaderType.displayName();
|
||||
};
|
||||
}
|
||||
return loaderType.displayName();
|
||||
}
|
||||
|
||||
public static String translateLoaderName(AddonLoader loader) {
|
||||
if (loader.type() != null)
|
||||
return translateLoaderType(loader.type());
|
||||
else return "bungeecord".equalsIgnoreCase(loader.name())
|
||||
? "BungeeCord"
|
||||
: 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;
|
||||
}
|
||||
@@ -745,6 +749,7 @@
|
||||
|
||||
.jfx-tool-bar-button {
|
||||
-fx-text-fill: -monet-on-surface;
|
||||
-fx-font-size: 14px;
|
||||
-fx-toggle-icon4-size: 37px;
|
||||
-fx-pref-height: -fx-toggle-icon4-size;
|
||||
-fx-max-height: -fx-toggle-icon4-size;
|
||||
@@ -760,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;
|
||||
@@ -1092,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;
|
||||
}
|
||||
|
||||
@@ -190,6 +190,11 @@ addon.download.recommend=Recommended Version for Minecraft %1s
|
||||
addon.download.title.release=Minecraft %s
|
||||
addon.download.title.snapshot=Minecraft %s (Snapshots)
|
||||
addon.modrinth=Modrinth
|
||||
addon.sort.date_created=Date Created
|
||||
addon.sort.last_updated=Last Updated
|
||||
addon.sort.popularity=Popularity
|
||||
addon.sort.relevancy=Relevancy
|
||||
addon.sort.total_downloads=Total Downloads
|
||||
|
||||
archive.author=Author(s)
|
||||
archive.date=Publish Date
|
||||
@@ -371,13 +376,6 @@ curse.category.6553=Realistic
|
||||
curse.category.6554=Fantasy
|
||||
curse.category.6555=Vanilla
|
||||
|
||||
curse.sort.author=Author
|
||||
curse.sort.date_created=Date Created
|
||||
curse.sort.last_updated=Last Updated
|
||||
curse.sort.name=Name
|
||||
curse.sort.popularity=Popularity
|
||||
curse.sort.total_downloads=Total Downloads
|
||||
|
||||
datetime.format=MMM d, yyyy, h\:mm\:ss a
|
||||
|
||||
download=Download
|
||||
@@ -433,32 +431,32 @@ extension.resourcepack=Resource Pack Archive
|
||||
extension.schematic=Schematic File
|
||||
extension.world=World Archive
|
||||
|
||||
fatal.create_hmcl_current_directory_failure=Hello Minecraft! Launcher cannot create the HMCL directory (%s). Please move HMCL to another location and reopen it.
|
||||
fatal.create_hmcl_current_directory_failure=HMCL cannot create the HMCL directory (%s). Please move HMCL to another location and reopen it.
|
||||
fatal.javafx.incomplete=The JavaFX environment is incomplete.\n\
|
||||
Please try replacing your Java or reinstalling OpenJFX.
|
||||
fatal.javafx.missing=Missing JavaFX environment. Please open Hello Minecraft! Launcher with Java, which includes OpenJFX.
|
||||
fatal.config_change_owner_root=You are using the root account to open Hello Minecraft! Launcher. This may prevent you from opening HMCL with another account in the future.\n\
|
||||
fatal.javafx.missing=Missing JavaFX environment. Please open HMCL with Java, which includes OpenJFX.
|
||||
fatal.config_change_owner_root=You are using the root account to open HMCL. This may prevent you from opening HMCL with another account in the future.\n\
|
||||
Do you still want to continue?
|
||||
fatal.config_in_temp_dir=You are opening Hello Minecraft! Launcher in a temporary directory. Your settings and game data may be lost.\n\
|
||||
fatal.config_in_temp_dir=You are opening HMCL in a temporary directory. Your settings and game data may be lost.\n\
|
||||
It is recommended to move HMCL to another location and reopen it.\n\
|
||||
Do you still want to continue?
|
||||
fatal.config_loading_failure=Cannot load configuration files.\n\
|
||||
Please make sure that Hello Minecraft! Launcher has read and write access to "%s" and the files in it.\n\
|
||||
Please make sure that HMCL has read and write access to "%s" and the files in it.\n\
|
||||
For macOS, try putting HMCL somewhere with permissions other than "Desktop", "Downloads", and "Documents" and try again.
|
||||
fatal.config_loading_failure.unix=Hello Minecraft! Launcher could not load the configuration file because it was created by user "%1$s".\n\
|
||||
fatal.config_loading_failure.unix=HMCL could not load the configuration file because it was created by user "%1$s".\n\
|
||||
Please open HMCL as root user (not recommended), or execute the following command in the terminal to change the ownership of the configuration file to the current user:\n%2$s
|
||||
fatal.config_unsupported_version=The current configuration file was created by a newer version of Hello Minecraft! Launcher, and this version of HMCL cannot load it properly.\n\
|
||||
fatal.config_unsupported_version=The current configuration file was created by a newer version of HMCL, and this version of HMCL cannot load it properly.\n\
|
||||
Please update and restart HMCL.\n\
|
||||
Before updating the launcher, any settings you modify will not be saved.
|
||||
fatal.mac_app_translocation=Hello Minecraft! Launcher is isolated to a temporary directory by the OS due to macOS security mechanisms.\n\
|
||||
fatal.mac_app_translocation=HMCL is isolated to a temporary directory by the OS due to macOS security mechanisms.\n\
|
||||
Please move HMCL to a different directory before attempting to open. Otherwise, your settings and game data may be lost after restarting.\n\
|
||||
Do you still want to continue?
|
||||
fatal.migration_requires_manual_reboot=Hello Minecraft! Launcher has been upgraded. Please restart the launcher.
|
||||
fatal.apply_update_failure=We are sorry, but Hello Minecraft! Launcher is unable to update automatically.\n\
|
||||
fatal.migration_requires_manual_reboot=HMCL has been upgraded. Please restart the launcher.
|
||||
fatal.apply_update_failure=We are sorry, but HMCL is unable to update automatically.\n\
|
||||
\n\
|
||||
You can update manually by downloading a newer launcher version from %s.\n\
|
||||
If the problem persists, please consider reporting this to us.
|
||||
fatal.apply_update_need_win7=Hello Minecraft! Launcher cannot automatically update on Windows XP/Vista.\n\
|
||||
fatal.apply_update_need_win7=HMCL cannot automatically update on Windows XP/Vista.\n\
|
||||
\n\
|
||||
You can update manually by downloading a newer launcher version from %s.
|
||||
fatal.deprecated_java_version=HMCL will require Java 17 or later to run in the future, but will still support launching games with Java 6~16.\n\
|
||||
@@ -467,14 +465,14 @@ It is recommended to install the latest version of Java to ensure that HMCL work
|
||||
\n\
|
||||
You can continue to keep the old version of Java. HMCL can recognize and manage multiple Java versions and will automatically select the appropriate Java for you based on the game version.
|
||||
fatal.deprecated_java_version.download_link=Download Java %d
|
||||
fatal.samba=If you opened Hello Minecraft! Launcher from a Samba network drive, some features might not be working. Please try updating your Java or moving the launcher to another directory.
|
||||
fatal.samba=If you opened HMCL from a Samba network drive, some features might not be working. Please try updating your Java or moving the launcher to another directory.
|
||||
fatal.illegal_char=Your working directory contains an illegal character "=". You will not be able to use authlib-injector or change the skin of your offline account.
|
||||
fatal.unsupported_platform=Minecraft is not fully supported on your platform yet, so you may experience missing features or even be unable to launch the game.\n\
|
||||
\n\
|
||||
If you cannot launch Minecraft 1.17 and later, you can try switching the "Renderer" to "Mesa LLVMpipe" or "Mesa Lavapipe" in "Global/Instance-specific Settings → Graphics Settings" to use CPU rendering for better compatibility.
|
||||
fatal.unsupported_platform.loongarch=Hello Minecraft! Launcher has provided support for the Loongson platform.\n\
|
||||
fatal.unsupported_platform.loongarch=HMCL has provided support for the Loongson platform.\n\
|
||||
If you encounter problems when playing a game, you can visit https://docs.hmcl.net/groups.html for help.
|
||||
fatal.unsupported_platform.windows_arm64=Hello Minecraft! Launcher has provided native support for the Windows on Arm platform. If you encounter problems when playing a game, please try launching the game with Java based on x86 architecture.\n\
|
||||
fatal.unsupported_platform.windows_arm64=HMCL has provided native support for the Windows on Arm platform. If you encounter problems when playing a game, please try launching the game with Java based on x86 architecture.\n\
|
||||
\n\
|
||||
If you are using the <b>Qualcomm</b> platform, you may need to install the <a href="ms-windows-store://pdp/?productid=9NQPSL29BFFF">OpenGL Compatibility Pack</a> before playing games.\n\
|
||||
Click the link to navigate to the Microsoft Store and install the compatibility pack.
|
||||
@@ -735,7 +733,7 @@ game_directory.use_relative_path=Use a relative path for the game directory if p
|
||||
game_directory.root=Using the disk root directory as the game folder may cause the game to fail to launch and will create multiple files and folders in the disk root directory.\n\nAre you sure you want to continue?
|
||||
|
||||
help=Help
|
||||
help.doc=Hello Minecraft! Launcher Documentation
|
||||
help.doc=HMCL Documentation
|
||||
help.detail=For data pack and modpack makers.
|
||||
|
||||
input.email=The username must be an email address.
|
||||
@@ -954,9 +952,9 @@ launcher.cache_directory.default=Default ("%APPDATA%/.minecraft" or "~/.minecraf
|
||||
launcher.cache_directory.disabled=Disabled
|
||||
launcher.cache_directory.invalid=Failed to create a cache directory, falling back to default.
|
||||
launcher.contact=Contact Us
|
||||
launcher.crash=Hello Minecraft! Launcher has encountered a fatal error! Please copy the following log and ask for help on our Discord, QQ group, GitHub, or other Minecraft forum.
|
||||
launcher.crash.java_internal_error=Hello Minecraft! Launcher has encountered a fatal error because your Java is corrupted. Please uninstall your Java and download a suitable Java <a href="https://bell-sw.com/pages/downloads/#downloads">here</a>.
|
||||
launcher.crash.hmcl_out_dated=Hello Minecraft! Launcher has encountered a fatal error! Your launcher is outdated. Please update your launcher!
|
||||
launcher.crash=HMCL has encountered a fatal error! Please copy the following log and ask for help on our Discord, QQ group, GitHub, or other Minecraft forum.
|
||||
launcher.crash.java_internal_error=HMCL has encountered a fatal error because your Java is corrupted. Please uninstall your Java and download a suitable Java <a href="https://bell-sw.com/pages/downloads/#downloads">here</a>.
|
||||
launcher.crash.hmcl_out_dated=HMCL has encountered a fatal error! Your launcher is outdated. Please update your launcher!
|
||||
launcher.update_java=Please update your Java version.
|
||||
|
||||
libraries.download=Downloading Libraries
|
||||
@@ -1025,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
|
||||
@@ -1038,11 +1039,11 @@ modpack.type.curse.error=Failed to download dependencies. Please try again or us
|
||||
modpack.type.curse.not_found=Some dependencies are no longer available. Please try installing a newer modpack version.
|
||||
modpack.type.manual.warning=The modpack is manually packaged by the publisher, which may already contain a launcher. It is recommended to try extracting the modpack and running the game with its own launcher. HMCL can still import it, with no guarantee of its usability. Still continue?
|
||||
modpack.type.mcbbs=MCBBS
|
||||
modpack.type.mcbbs.export=Can be imported by Hello Minecraft! Launcher
|
||||
modpack.type.mcbbs.export=Can be imported by HMCL
|
||||
modpack.type.modrinth=Modrinth
|
||||
modpack.type.modrinth.export=Can be imported by popular third-party launchers
|
||||
modpack.type.multimc=MultiMC
|
||||
modpack.type.multimc.export=Can be imported by Hello Minecraft! Launcher and MultiMC
|
||||
modpack.type.multimc.export=Can be imported by HMCL and MultiMC
|
||||
modpack.type.server=Auto-Update Modpack from Server
|
||||
modpack.type.server.export=Allows server owner to remotely update the game instance
|
||||
modpack.type.server.malformed=Invalid modpack manifest. Please contact the modpack maker to resolve this problem.
|
||||
@@ -1332,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
|
||||
@@ -1348,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)
|
||||
|
||||
@@ -1785,6 +1791,7 @@ update.channel.nightly.hint=You are currently using a Nightly channel build of t
|
||||
update.channel.nightly.title=Nightly Channel Notice
|
||||
update.channel.stable=Stable
|
||||
update.checking=Checking for updates
|
||||
update.check_failed=Failed to check for update. %s
|
||||
update.disable_auto_show_update_dialog=Do not show update dialog automatically
|
||||
update.disable_auto_show_update_dialog.subtitle=Enable this option to prevent HMCL from automatically showing the update dialog.
|
||||
update.failed=Failed to update
|
||||
@@ -1798,6 +1805,7 @@ update.latest=This is the latest version
|
||||
update.tooltip=Update
|
||||
update.preview=Preview HMCL releases early
|
||||
update.preview.subtitle=Enable this option to receive new versions of HMCL early for testing before their official release.
|
||||
update.unverified=Update unavailable due to this launcher instance not verified
|
||||
|
||||
warning.java_interpreted_mode=HMCL is running in an interpreted Java environment, which will greatly affect performance.\n\
|
||||
\n\
|
||||
|
||||
@@ -47,6 +47,8 @@ about.thanks_to.shulkersakura.statement=لتوفير الشعار الخاص ب
|
||||
about.thanks_to.users=أعضاء مجموعة مستخدمي HMCL
|
||||
about.thanks_to.users.statement=شكراً على التبرعات وتقارير الأخطاء وغير ذلك.
|
||||
about.thanks_to.yushijinhun.statement=لتوفير الدعم المتعلق بـ authlib-injector.
|
||||
# about.thanks_to.8mi-tech=
|
||||
# about.thanks_to.8mi-tech.statement=
|
||||
about.open_source=المصدر المفتوح
|
||||
about.open_source.statement=GPL v3 (https://github.com/HMCL-dev/HMCL)
|
||||
|
||||
@@ -76,12 +78,14 @@ account.failed.server_response_malformed=استجابة الخادم غير صا
|
||||
account.failed.ssl=حدث خطأ SSL أثناء الاتصال بالخادم. يُرجى محاولة تحديث Java.
|
||||
account.failed.dns=حدث خطأ SSL أثناء الاتصال بالخادم. قد يكون تحليل DNS غير صحيح. يُرجى تغيير خادم DNS الخاص بك أو استخدام خدمة وكيل.
|
||||
account.failed.wrong_account=لقد سجّلت الدخول إلى حساب خاطئ.
|
||||
# account.storage.read_only=
|
||||
account.injector.add=خادم مصادقة جديد
|
||||
account.injector.empty=لا يوجد (يمكنك النقر على "+" لإضافة واحد)
|
||||
account.injector.http=تحذير: يستخدم هذا الخادم بروتوكول HTTP غير الآمن. أي شخص يعترض اتصالك سيتمكن من رؤية بياناتك بنص واضح.
|
||||
account.injector.link.homepage=الصفحة الرئيسية
|
||||
account.injector.link.register=التسجيل
|
||||
account.injector.server=خادم المصادقة
|
||||
# account.injector.server.storage.read_only=
|
||||
account.injector.server_url=رابط الخادم
|
||||
account.injector.server_name=اسم الخادم
|
||||
account.login=تسجيل الدخول
|
||||
@@ -103,6 +107,7 @@ account.methods.microsoft.close_page=اكتمل تفويض حساب Microsoft.\n
|
||||
account.methods.microsoft.error.add_family=يُرجى النقر <a href="https://support.microsoft.com/account-billing/837badbc-999e-54d2-2617-d19206b9540a">هنا</a> لتغيير تاريخ ميلاد حسابك ليكون فوق 18 عاماً، أو إضافة حسابك إلى عائلة.
|
||||
account.methods.microsoft.error.country_unavailable=XBOX Live غير متاح في بلدك/منطقتك الحالية.
|
||||
account.methods.microsoft.error.missing_xbox_account=لا يوجد حساب XBOX مرتبط بحساب Microsoft الخاص بك بعد. يُرجى النقر <a href="https://www.minecraft.net/msaprofile/mygames/editprofile">هنا</a> لربط واحد.
|
||||
# account.methods.microsoft.error.no_license=
|
||||
account.methods.microsoft.error.no_character=يُرجى التأكد من أنك اشتريت Minecraft: Java Edition.\n\
|
||||
إذا اشتريته بالفعل، فقد لا يكون ملف اللعبة قد تم إنشاؤه. يُرجى النقر <a href="https://www.minecraft.net/msaprofile/mygames/editprofile">هنا</a> لإنشاء ملف اللعبة.
|
||||
account.methods.microsoft.error.banned=قد يكون حسابك محظوراً من XBOX Live.\n\
|
||||
@@ -112,6 +117,7 @@ account.methods.microsoft.error.wrong_verify_method=فشل تسجيل الدخو
|
||||
account.methods.microsoft.hint=انقر على زر "تسجيل الدخول" لبدء إضافة حساب Microsoft الخاص بك.
|
||||
account.methods.microsoft.methods.device=تسجيل الدخول برمز QR
|
||||
account.methods.microsoft.methods.device.hint=امسح رمز QR أو زر <a href="%s">%s</a> لإتمام تسجيل الدخول، وأدخل <b>%s</b> في الصفحة المفتوحة.
|
||||
# account.methods.microsoft.methods.device.hint.completed=
|
||||
account.methods.microsoft.methods.browser=تسجيل الدخول عبر المتصفح
|
||||
account.methods.microsoft.methods.browser.hint=انقر على زر "تسجيل الدخول" أو <a href="%s">انسخ الرابط</a> والصقه في المتصفح لتسجيل الدخول.
|
||||
account.methods.microsoft.manual=<b>إذا كان اتصالك بالإنترنت ضعيفاً، قد يتسبب ذلك في بطء تحميل صفحات الويب أو فشلها كلياً.\nيمكنك المحاولة لاحقاً أو التبديل إلى اتصال إنترنت مختلف.</b>
|
||||
@@ -157,9 +163,12 @@ account.skin.invalid_skin=ملف المظهر غير صالح.
|
||||
account.username=اسم المستخدم
|
||||
|
||||
addon.category=الفئة
|
||||
# addon.changelog=
|
||||
# addon.changelog.empty=
|
||||
addon.channel.alpha=ألفا
|
||||
addon.channel.beta=بيتا
|
||||
addon.channel.release=إصدار رسمي
|
||||
# addon.check_update=
|
||||
addon.check_update.button=تحديث
|
||||
addon.check_update.confirm=تحديث
|
||||
addon.check_update.current_version=الإصدار الحالي
|
||||
@@ -170,7 +179,23 @@ addon.check_update.file=الملف
|
||||
addon.check_update.source=المصدر
|
||||
addon.check_update.target_version=الإصدار المستهدف
|
||||
addon.curseforge=CurseForge
|
||||
# addon.dependency.embedded=
|
||||
# addon.dependency.optional=
|
||||
# addon.dependency.required=
|
||||
# addon.dependency.tool=
|
||||
# addon.dependency.include=
|
||||
# addon.dependency.incompatible=
|
||||
# addon.dependency.broken=
|
||||
# addon.dependencies.has_broken=
|
||||
# addon.download.recommend=
|
||||
# addon.download.title.release=
|
||||
# addon.download.title.snapshot=
|
||||
addon.modrinth=Modrinth
|
||||
addon.sort.date_created=تاريخ الإنشاء
|
||||
addon.sort.last_updated=آخر تحديث
|
||||
addon.sort.popularity=الشعبية
|
||||
# addon.sort.relevancy=
|
||||
addon.sort.total_downloads=إجمالي التنزيلات
|
||||
|
||||
archive.author=المؤلف/المؤلفون
|
||||
archive.date=تاريخ النشر
|
||||
@@ -184,6 +209,7 @@ assets.index.malformed=ملفات فهرس الأصول المنزّلة تال
|
||||
button.cancel=إلغاء
|
||||
button.change_source=تغيير مصدر التنزيل
|
||||
button.clear=مسح
|
||||
# button.copy=
|
||||
button.copy_and_exit=نسخ والخروج
|
||||
button.delete=حذف
|
||||
button.disable=تعطيل
|
||||
@@ -242,6 +268,8 @@ curse.category.4472=تقنية
|
||||
curse.category.4473=سحر
|
||||
curse.category.5128=Vanilla+
|
||||
curse.category.7418=رعب
|
||||
# curse.category.9243=
|
||||
# curse.category.10683=
|
||||
|
||||
# https://addons-ecs.forgesvc.net/api/v2/category/section/6
|
||||
curse.category.5299=تعليم
|
||||
@@ -267,6 +295,9 @@ curse.category.419=سحر
|
||||
curse.category.412=تكنولوجيا
|
||||
curse.category.4557=ريدستون
|
||||
curse.category.428=Tinker's Construct
|
||||
# curse.category.8937=
|
||||
# curse.category.9026=
|
||||
# curse.category.10775=
|
||||
# '
|
||||
curse.category.414=نقل اللاعب
|
||||
curse.category.4486=Lucky Blocks
|
||||
@@ -299,6 +330,8 @@ curse.category.4558=ريدستون
|
||||
curse.category.4843=أتمتة
|
||||
curse.category.4906=MCreator
|
||||
curse.category.7669=Twilight Forest
|
||||
# curse.category.9049=
|
||||
# curse.category.10754=
|
||||
|
||||
# https://addons-ecs.forgesvc.net/api/v2/category/section/12
|
||||
curse.category.5244=حزم خطوط
|
||||
@@ -317,6 +350,7 @@ curse.category.404=متحرك
|
||||
curse.category.4465=دعم المودات
|
||||
curse.category.402=قرون وسطى
|
||||
curse.category.401=عصري
|
||||
# curse.category.8939=
|
||||
|
||||
# https://addons-ecs.forgesvc.net/api/v2/category/section/17
|
||||
curse.category.4464=عالم المودات
|
||||
@@ -340,12 +374,9 @@ curse.category.4550=مهام
|
||||
curse.category.4555=توليد العالم
|
||||
curse.category.4552=سكريبتات
|
||||
|
||||
curse.sort.author=المؤلف
|
||||
curse.sort.date_created=تاريخ الإنشاء
|
||||
curse.sort.last_updated=آخر تحديث
|
||||
curse.sort.name=الاسم
|
||||
curse.sort.popularity=الشعبية
|
||||
curse.sort.total_downloads=إجمالي التنزيلات
|
||||
# curse.category.6553=
|
||||
# curse.category.6554=
|
||||
# curse.category.6555=
|
||||
|
||||
datetime.format=MMM d, yyyy, h\:mm\:ss a
|
||||
|
||||
@@ -358,9 +389,16 @@ download.failed.empty=لا توجد إصدارات متاحة. انقر هنا
|
||||
download.failed.no_code=فشل التنزيل
|
||||
download.failed.refresh=فشل في جلب قائمة الإصدارات. يرجى النقر هنا لإعادة المحاولة.
|
||||
download.game=New Game
|
||||
# download.provider.bmclapi=
|
||||
# download.provider.bmclapi.desc=
|
||||
# download.provider.mojang=
|
||||
# download.provider.mojang.desc=
|
||||
download.provider.official=من المصادر الرسمية
|
||||
# download.provider.official.desc=
|
||||
download.provider.balanced=من أسرع المصادر المتاحة
|
||||
# download.provider.balanced.desc=
|
||||
download.provider.mirror=من مرآة
|
||||
# download.provider.mirror.desc=
|
||||
download.java=جارٍ تنزيل Java
|
||||
download.java.process=عملية تنزيل Java
|
||||
download.javafx=جارٍ تنزيل مكتبات المشغّل...
|
||||
@@ -395,32 +433,32 @@ extension.resourcepack=أرشيف حزمة الموارد
|
||||
extension.schematic=ملف مخطط
|
||||
extension.world=أرشيف العالم
|
||||
|
||||
fatal.create_hmcl_current_directory_failure=لا يستطيع Hello Minecraft! Launcher إنشاء مجلد HMCL (%s). يُرجى نقل HMCL إلى موقع آخر وإعادة تشغيله.
|
||||
fatal.create_hmcl_current_directory_failure=لا يستطيع HMCL إنشاء مجلد HMCL (%s). يُرجى نقل HMCL إلى موقع آخر وإعادة تشغيله.
|
||||
fatal.javafx.incomplete=بيئة JavaFX غير مكتملة.\n\
|
||||
يُرجى محاولة استبدال Java أو إعادة تثبيت OpenJFX.
|
||||
fatal.javafx.missing=بيئة JavaFX مفقودة. يُرجى فتح Hello Minecraft! Launcher باستخدام Java الذي يتضمن OpenJFX.
|
||||
fatal.config_change_owner_root=أنت تستخدم حساب الجذر لفتح Hello Minecraft! Launcher. قد يمنعك ذلك من فتح HMCL بحساب آخر في المستقبل.\n\
|
||||
fatal.javafx.missing=بيئة JavaFX مفقودة. يُرجى فتح HMCL باستخدام Java الذي يتضمن OpenJFX.
|
||||
fatal.config_change_owner_root=أنت تستخدم حساب الجذر لفتح HMCL. قد يمنعك ذلك من فتح HMCL بحساب آخر في المستقبل.\n\
|
||||
هل تريد المتابعة؟
|
||||
fatal.config_in_temp_dir=أنت تفتح Hello Minecraft! Launcher في مجلد مؤقت. قد تُفقد إعداداتك وبيانات اللعبة.\n\
|
||||
fatal.config_in_temp_dir=أنت تفتح HMCL في مجلد مؤقت. قد تُفقد إعداداتك وبيانات اللعبة.\n\
|
||||
يُنصح بنقل HMCL إلى موقع آخر وإعادة تشغيله.\n\
|
||||
هل تريد المتابعة؟
|
||||
fatal.config_loading_failure=لا يمكن تحميل ملفات الإعدادات.\n\
|
||||
يُرجى التأكد من أن Hello Minecraft! Launcher يملك صلاحيات القراءة والكتابة على "%s" والملفات الموجودة فيه.\n\
|
||||
يُرجى التأكد من أن HMCL يملك صلاحيات القراءة والكتابة على "%s" والملفات الموجودة فيه.\n\
|
||||
لمستخدمي macOS، جرّب وضع HMCL في موقع بصلاحيات غير "سطح المكتب" و"التنزيلات" و"المستندات" وأعد المحاولة.
|
||||
fatal.config_loading_failure.unix=لا يستطيع Hello Minecraft! Launcher تحميل ملف الإعدادات لأنه أُنشئ بواسطة المستخدم "%1$s".\n\
|
||||
fatal.config_loading_failure.unix=لا يستطيع HMCL تحميل ملف الإعدادات لأنه أُنشئ بواسطة المستخدم "%1$s".\n\
|
||||
يُرجى فتح HMCL كمستخدم جذر (غير موصى به)، أو تنفيذ الأمر التالي في الطرفية لتغيير ملكية ملف الإعدادات إلى المستخدم الحالي:\n%2$s
|
||||
fatal.config_unsupported_version=ملف الإعدادات الحالي أُنشئ بواسطة إصدار أحدث من Hello Minecraft! Launcher، ولا يستطيع هذا الإصدار من HMCL تحميله بشكل صحيح.\n\
|
||||
fatal.config_unsupported_version=ملف الإعدادات الحالي أُنشئ بواسطة إصدار أحدث من HMCL، ولا يستطيع هذا الإصدار من HMCL تحميله بشكل صحيح.\n\
|
||||
يُرجى تحديث HMCL وإعادة تشغيله.\n\
|
||||
قبل تحديث المشغّل، لن يتم حفظ أي إعدادات تقوم بتعديلها.
|
||||
fatal.mac_app_translocation=تم عزل Hello Minecraft! Launcher في مجلد مؤقت بواسطة نظام التشغيل بسبب آليات أمان macOS.\n\
|
||||
fatal.mac_app_translocation=تم عزل HMCL في مجلد مؤقت بواسطة نظام التشغيل بسبب آليات أمان macOS.\n\
|
||||
يُرجى نقل HMCL إلى مجلد آخر قبل محاولة الفتح. وإلا، قد تُفقد إعداداتك وبيانات اللعبة بعد إعادة التشغيل.\n\
|
||||
هل تريد المتابعة؟
|
||||
fatal.migration_requires_manual_reboot=تم ترقية Hello Minecraft! Launcher. يُرجى إعادة تشغيل المشغّل.
|
||||
fatal.apply_update_failure=نأسف، لكن Hello Minecraft! Launcher غير قادر على التحديث التلقائي.\n\
|
||||
fatal.migration_requires_manual_reboot=تم ترقية HMCL. يُرجى إعادة تشغيل المشغّل.
|
||||
fatal.apply_update_failure=نأسف، لكن HMCL غير قادر على التحديث التلقائي.\n\
|
||||
\n\
|
||||
يمكنك التحديث يدوياً بتنزيل إصدار أحدث من المشغّل من %s.\n\
|
||||
إذا استمرت المشكلة، يُرجى إرسال تقرير عنها إلينا.
|
||||
fatal.apply_update_need_win7=لا يستطيع Hello Minecraft! Launcher التحديث التلقائي على Windows XP/Vista.\n\
|
||||
fatal.apply_update_need_win7=لا يستطيع HMCL التحديث التلقائي على Windows XP/Vista.\n\
|
||||
\n\
|
||||
يمكنك التحديث يدوياً بتنزيل إصدار أحدث من المشغّل من %s.
|
||||
fatal.deprecated_java_version=سيتطلب HMCL Java 17 أو أحدث للتشغيل في المستقبل، لكنه سيستمر في دعم تشغيل الألعاب باستخدام Java 6~16.\n\
|
||||
@@ -429,23 +467,25 @@ fatal.deprecated_java_version=سيتطلب HMCL Java 17 أو أحدث للتشغ
|
||||
\n\
|
||||
يمكنك الاحتفاظ بإصدار Java القديم. يستطيع HMCL التعرف على إصدارات Java المتعددة وإدارتها وسيختار تلقائياً Java المناسب بناءً على إصدار اللعبة.
|
||||
fatal.deprecated_java_version.download_link=تنزيل Java %d
|
||||
fatal.samba=إذا فتحت Hello Minecraft! Launcher من محرك أقراص شبكة Samba، قد لا تعمل بعض الميزات. يُرجى محاولة تحديث Java أو نقل المشغّل إلى مجلد آخر.
|
||||
fatal.samba=إذا فتحت HMCL من محرك أقراص شبكة Samba، قد لا تعمل بعض الميزات. يُرجى محاولة تحديث Java أو نقل المشغّل إلى مجلد آخر.
|
||||
fatal.illegal_char=مسار المستخدم الخاص بك يحتوي على حرف غير مسموح به "=". لن تتمكن من استخدام authlib-injector أو تغيير مظهر حسابك غير المتصل.
|
||||
fatal.unsupported_platform=لا يدعم Minecraft منصتك الحالية بشكل كامل بعد، لذا قد تواجه ميزات مفقودة أو عدم القدرة على تشغيل اللعبة.\n\
|
||||
\n\
|
||||
إذا لم تتمكن من تشغيل Minecraft 1.17 أو أحدث، يمكنك محاولة تغيير "المُصيِّر" إلى "Mesa LLVMpipe" في "الإعدادات العامة/الخاصة بالنسخة ← الإعدادات المتقدمة" لاستخدام معالجة المعالج المركزي لتوافق أفضل.
|
||||
fatal.unsupported_platform.loongarch=وفّر Hello Minecraft! Launcher دعماً لمنصة Loongson.\n\
|
||||
fatal.unsupported_platform.loongarch=وفّر HMCL دعماً لمنصة Loongson.\n\
|
||||
إذا واجهت مشاكل أثناء اللعب، يمكنك زيارة https://docs.hmcl.net/groups.html للحصول على مساعدة.
|
||||
fatal.unsupported_platform.windows_arm64=وفّر Hello Minecraft! Launcher دعماً أصلياً لمنصة Windows on Arm. إذا واجهت مشاكل أثناء اللعب، يُرجى محاولة تشغيل اللعبة بـ Java المبني على معمارية x86.\n\
|
||||
fatal.unsupported_platform.windows_arm64=وفّر HMCL دعماً أصلياً لمنصة Windows on Arm. إذا واجهت مشاكل أثناء اللعب، يُرجى محاولة تشغيل اللعبة بـ Java المبني على معمارية x86.\n\
|
||||
\n\
|
||||
إذا كنت تستخدم منصة <b>Qualcomm</b>، قد تحتاج إلى تثبيت <a href="ms-windows-store://pdp/?productid=9NQPSL29BFFF">حزمة توافق OpenGL</a> قبل اللعب.\n\
|
||||
انقر على الرابط للانتقال إلى Microsoft Store وتثبيت حزمة التوافق.
|
||||
# fatal.wine_warning=
|
||||
|
||||
file=ملف
|
||||
|
||||
folder.config=الإعدادات
|
||||
folder.game=مجلد العمل
|
||||
folder.logs=السجلات
|
||||
# folder.crash-reports=
|
||||
folder.mod=المودات
|
||||
folder.resourcepacks=حزم الموارد
|
||||
folder.shaderpacks=حزم الظلال
|
||||
@@ -691,9 +731,10 @@ game_directory.instance_directory.choose=اختر مجلد اللعبة
|
||||
game_directory.name=الاسم
|
||||
game_directory.new=مجلد جديد
|
||||
game_directory.use_relative_path=استخدام مسار نسبي لمجلد اللعبة إن أمكن
|
||||
# game_directory.root=
|
||||
|
||||
help=مساعدة
|
||||
help.doc=توثيق Hello Minecraft! Launcher
|
||||
help.doc=توثيق HMCL
|
||||
help.detail=لصانعي حزم البيانات وحزم المودات.
|
||||
|
||||
input.email=يجب أن يكون اسم المستخدم بريداً إلكترونياً.
|
||||
@@ -703,6 +744,7 @@ input.url=يجب أن يكون الإدخال رابطاً صالحاً.
|
||||
|
||||
install=نسخة جديدة
|
||||
install.change_version=تغيير الإصدار
|
||||
# install.change_version.title=
|
||||
install.change_version.confirm=هل أنت متأكد من تغيير %1$s من الإصدار %2$s إلى %3$s؟
|
||||
install.change_version.process=عملية تغيير الإصدار
|
||||
install.failed=فشل التثبيت
|
||||
@@ -711,6 +753,7 @@ install.failed.downloading.detail=فشل تنزيل الملف: %s
|
||||
install.failed.downloading.timeout=انتهت مهلة التنزيل عند جلب: %s
|
||||
install.failed.install_online=فشل التعرف على الملف المقدم. إذا كنت تثبّت مود، انتقل إلى صفحة "المودات".
|
||||
install.failed.malformed=الملفات المنزّلة تالفة. يمكنك محاولة حل هذه المشكلة بالتبديل إلى مصدر تنزيل آخر في "الإعدادات ← التنزيل ← مصدر التنزيل".
|
||||
# install.failed.cleanroom_not_compatible_with_forge=
|
||||
install.failed.optifine_conflict=لا يمكن تثبيت كل من OptiFine وFabric معاً على Minecraft 1.13 أو أحدث.
|
||||
install.failed.optifine_forge_1.17=في Minecraft 1.17.1، يتوافق Forge فقط مع OptiFine H1 pre2 أو أحدث. يمكنك تثبيتها بتفعيل "الإصدارات التجريبية" عند اختيار إصدار OptiFine في HMCL.
|
||||
install.failed.version_mismatch=يتطلب هذا المحمّل إصدار اللعبة %1$s، لكن الإصدار المثبت هو %2$s.
|
||||
@@ -744,6 +787,7 @@ install.new_game=تثبيت نسخة
|
||||
install.new_game.already_exists=اسم هذه النسخة موجود مسبقاً. يُرجى اختيار اسم آخر.
|
||||
install.new_game.installation=تثبيت النسخة
|
||||
install.new_game.malformed=اسم غير صالح.
|
||||
# install.new_game.malformed_json=
|
||||
install.success=تم التثبيت بنجاح.
|
||||
|
||||
instance=الألعاب
|
||||
@@ -844,6 +888,8 @@ launch.advice.forge28_2_2_optifine=إصدار Forge 28.2.2 أو أحدث غير
|
||||
launch.advice.forge37_0_60=إصدارات Forge قبل 37.0.60 غير متوافقة مع Java 17. يُرجى تحديث Forge إلى 37.0.60 أو أحدث، أو تشغيل اللعبة بـ Java 16.
|
||||
launch.advice.java8_51_1_13=قد يتعطل Minecraft 1.13 على إصدارات Java 8 قبل 1.8.0_51. يُرجى تثبيت أحدث إصدار من Java 8.
|
||||
launch.advice.java9=لا يمكن تشغيل Minecraft 1.12 أو أقدم على Java 9 أو أحدث. يُرجى استخدام Java 8.
|
||||
# launch.advice.lwjgl_3_4_1=
|
||||
# launch.advice.lwjgl_3_4_1.title=
|
||||
launch.advice.modded_java=قد لا تكون بعض المودات متوافقة مع إصدارات Java الأحدث. يُنصح باستخدام Java %1$s لتشغيل Minecraft %2$s.
|
||||
launch.advice.modlauncher8=إصدار Forge الذي تستخدمه غير متوافق مع إصدار Java الحالي. يُرجى محاولة تحديث Forge.
|
||||
launch.advice.newer_java=أنت تستخدم إصداراً قديماً من Java لتشغيل اللعبة. يُنصح بالتحديث إلى Java 8، وإلا قد تتسبب بعض المودات في تعطل اللعبة.
|
||||
@@ -880,13 +926,26 @@ launcher.agreement=الشروط واتفاقية الترخيص
|
||||
launcher.agreement.accept=قبول
|
||||
launcher.agreement.decline=رفض
|
||||
launcher.agreement.hint=يجب الموافقة على اتفاقية الترخيص لاستخدام هذا البرنامج.
|
||||
# launcher.april_fools.switch_lzh=
|
||||
# launcher.april_fools.switch_lzh.confirm=
|
||||
launcher.background=صورة الخلفية
|
||||
launcher.background.choose=اختر صورة الخلفية
|
||||
# launcher.background.builtin=
|
||||
launcher.background.default=افتراضي
|
||||
launcher.background.default.tooltip=أو "background.png/.jpg/.gif/.webp" والصور في مجلد "bg"
|
||||
# launcher.background.fallback=
|
||||
# launcher.background.fallback.builtin=
|
||||
# launcher.background.fallback.paint=
|
||||
# launcher.background.fallback.theme_color=
|
||||
# launcher.background.load_policy=
|
||||
# launcher.background.load_policy.show_fallback_while_loading=
|
||||
# launcher.background.load_policy.wait_for_background=
|
||||
# launcher.background.loading=
|
||||
launcher.background.network=من رابط
|
||||
# launcher.background.network.cache=
|
||||
launcher.background.paint=لون خالص
|
||||
launcher.background.theme=اتبع السمة
|
||||
# launcher.background.theme_color=
|
||||
launcher.cache_directory=مجلد الذاكرة المؤقتة
|
||||
launcher.cache_directory.clean=مسح الذاكرة المؤقتة
|
||||
launcher.cache_directory.choose=اختر مجلد الذاكرة المؤقتة
|
||||
@@ -894,20 +953,22 @@ launcher.cache_directory.default=افتراضي ("%APPDATA%/.minecraft" أو "~/
|
||||
launcher.cache_directory.disabled=معطّل
|
||||
launcher.cache_directory.invalid=فشل إنشاء مجلد الذاكرة المؤقتة، جارٍ الرجوع إلى الافتراضي.
|
||||
launcher.contact=تواصل معنا
|
||||
launcher.crash=واجه Hello Minecraft! Launcher خطأً فادحاً! يُرجى نسخ السجل التالي وطلب المساعدة على Discord أو مجموعة QQ أو GitHub أو أي منتدى Minecraft آخر.
|
||||
launcher.crash.java_internal_error=واجه Hello Minecraft! Launcher خطأً فادحاً بسبب تلف Java. يُرجى إلغاء تثبيت Java وتنزيل إصدار مناسب من <a href="https://bell-sw.com/pages/downloads/#downloads">هنا</a>.
|
||||
launcher.crash.hmcl_out_dated=واجه Hello Minecraft! Launcher خطأً فادحاً! المشغّل قديم. يُرجى تحديثه!
|
||||
launcher.crash=واجه HMCL خطأً فادحاً! يُرجى نسخ السجل التالي وطلب المساعدة على Discord أو مجموعة QQ أو GitHub أو أي منتدى Minecraft آخر.
|
||||
launcher.crash.java_internal_error=واجه HMCL خطأً فادحاً بسبب تلف Java. يُرجى إلغاء تثبيت Java وتنزيل إصدار مناسب من <a href="https://bell-sw.com/pages/downloads/#downloads">هنا</a>.
|
||||
launcher.crash.hmcl_out_dated=واجه HMCL خطأً فادحاً! المشغّل قديم. يُرجى تحديثه!
|
||||
launcher.update_java=يُرجى تحديث إصدار Java الخاص بك.
|
||||
|
||||
libraries.download=جارٍ تنزيل المكتبات
|
||||
|
||||
login.enter_password=يُرجى إدخال كلمة المرور.
|
||||
|
||||
# logwindow.always_on_top=
|
||||
logwindow.show_lines=إظهار أرقام الأسطر
|
||||
logwindow.terminate_game=إنهاء عملية اللعبة
|
||||
logwindow.title=السجل
|
||||
logwindow.help=يمكنك الانتقال إلى مجتمع HMCL وإيجاد مساعدة من الآخرين.
|
||||
logwindow.autoscroll=التمرير التلقائي
|
||||
# logwindow.wrap_text=
|
||||
logwindow.export_game_crash_logs=تصدير سجلات التعطل
|
||||
logwindow.export_dump=تصدير تفريغ مكدس اللعبة
|
||||
logwindow.export_dump.no_dependency=لا يحتوي Java الخاص بك على المكتبات اللازمة لإنشاء تفريغ المكدس. يُرجى الانضمام إلى Discord أو مجموعة QQ للحصول على مساعدة.
|
||||
@@ -947,8 +1008,10 @@ modpack.file_api=بادئة رابط حزمة المودات
|
||||
modpack.files.blueprints=مخططات BuildCraft
|
||||
modpack.files.config=ملفات إعداد المودات
|
||||
modpack.files.dumps=ملفات تصحيح NEI
|
||||
# modpack.files.empty=
|
||||
modpack.files.hmclversion_cfg=ملف إعداد المشغّل
|
||||
modpack.files.liteconfig=ملفات LiteLoader
|
||||
# modpack.files.load_failed=
|
||||
modpack.files.mods=المودات
|
||||
modpack.files.mods.voxelmods=خيارات VoxelMods
|
||||
modpack.files.options_txt=ملف خيارات Minecraft
|
||||
@@ -961,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=الموقع الرسمي
|
||||
@@ -974,11 +1040,11 @@ modpack.type.curse.error=فشل تنزيل التبعيات. يُرجى المح
|
||||
modpack.type.curse.not_found=بعض التبعيات لم تعد متاحة. يُرجى محاولة تثبيت إصدار أحدث من الحزمة.
|
||||
modpack.type.manual.warning=هذه الحزمة معبّأة يدوياً من قِبل الناشر، وقد تحتوي بالفعل على مشغّل. يُنصح بمحاولة فك ضغط الحزمة وتشغيل اللعبة بمشغّلها الخاص. يستطيع HMCL استيرادها مع عدم ضمان قابليتها للاستخدام. هل تريد المتابعة؟
|
||||
modpack.type.mcbbs=MCBBS
|
||||
modpack.type.mcbbs.export=يمكن استيرادها بواسطة Hello Minecraft! Launcher
|
||||
modpack.type.mcbbs.export=يمكن استيرادها بواسطة HMCL
|
||||
modpack.type.modrinth=Modrinth
|
||||
modpack.type.modrinth.export=يمكن استيرادها بواسطة مشغّلات شائعة من أطراف ثالثة
|
||||
modpack.type.multimc=MultiMC
|
||||
modpack.type.multimc.export=يمكن استيرادها بواسطة Hello Minecraft! Launcher وMultiMC
|
||||
modpack.type.multimc.export=يمكن استيرادها بواسطة HMCL وMultiMC
|
||||
modpack.type.server=تحديث تلقائي لحزمة المودات من الخادم
|
||||
modpack.type.server.export=يتيح لمالك الخادم تحديث نسخة اللعبة عن بُعد
|
||||
modpack.type.server.malformed=ملف بيانات الحزمة غير صالح. يُرجى التواصل مع صانع الحزمة لحل هذه المشكلة.
|
||||
@@ -1105,6 +1171,7 @@ mods.add.title=اختر ملف المود الذي تريد إضافته
|
||||
mods.disable=تعطيل
|
||||
mods.download=تنزيل مود
|
||||
mods.download.title=تنزيل مود - %1s
|
||||
# mods.empty=
|
||||
mods.enable=تفعيل
|
||||
mods.game.version=إصدار اللعبة
|
||||
mods.manage=المودات
|
||||
@@ -1137,6 +1204,7 @@ nbt.title=عرض الملف - %s
|
||||
datapack=حزم البيانات
|
||||
datapack.add=إضافة
|
||||
datapack.add.title=اختر أرشيف حزمة البيانات التي تريد إضافتها
|
||||
# datapack.empty=
|
||||
datapack.reload.toast=اللعبة تعمل حالياً، يُرجى استخدام الأمر /reload لإعادة تحميل حزمة البيانات
|
||||
datapack.title=العالم [%s] - حزم البيانات
|
||||
|
||||
@@ -1155,6 +1223,7 @@ world.backup.create.new_one=نسخة احتياطية جديدة
|
||||
world.backup.create.failed=فشل إنشاء النسخة الاحتياطية.\n%s
|
||||
world.backup.create.success=تم إنشاء نسخة احتياطية جديدة بنجاح: %s
|
||||
world.backup.delete=حذف هذه النسخة الاحتياطية
|
||||
# world.backup.empty=
|
||||
world.backup.processing=جارٍ النسخ الاحتياطي...
|
||||
world.chunkbase=Chunk Base
|
||||
world.chunkbase.end_city=مدينة النهاية
|
||||
@@ -1173,6 +1242,7 @@ world.delete=حذف العالم
|
||||
world.delete.failed=فشل حذف العالم.\n%s
|
||||
world.download=تنزيل
|
||||
world.download.title=تنزيل العالم - %1s
|
||||
# world.empty=
|
||||
world.export=تصدير العالم
|
||||
world.export.title=اختر المجلد لهذا العالم المُصدَّر
|
||||
world.export.location=حفظ باسم
|
||||
@@ -1224,6 +1294,7 @@ world.load.fail=فشل تحميل العالم
|
||||
world.locked=قيد الاستخدام
|
||||
world.locked.failed=العالم قيد الاستخدام حالياً. يُرجى إغلاق اللعبة والمحاولة مجدداً.
|
||||
world.manage=العوالم
|
||||
# world.manage.button=
|
||||
world.manage.title=العالم - %s
|
||||
world.name=اسم العالم
|
||||
world.name.enter=أدخل اسم العالم
|
||||
@@ -1241,12 +1312,20 @@ repositories.chooser.title=اختر مصدر تنزيل JavaFX
|
||||
|
||||
resourcepack=حزم الموارد
|
||||
resourcepack.add=إضافة
|
||||
resourcepack.add.title=اختر أرشيف حزمة الموارد التي تريد إضافتها
|
||||
resourcepack.manage=حزم الموارد
|
||||
resourcepack.download=تنزيل
|
||||
resourcepack.add.failed=فشل إضافة حزمة الموارد
|
||||
resourcepack.add.title=اختر أرشيف حزمة الموارد التي تريد إضافتها
|
||||
resourcepack.delete.failed=فشل حذف حزمة الموارد
|
||||
resourcepack.download=تنزيل
|
||||
resourcepack.download.title=تنزيل حزمة موارد - %1s
|
||||
# resourcepack.empty=
|
||||
resourcepack.manage=حزم الموارد
|
||||
# resourcepack.update_in_modpack.warning=
|
||||
# resourcepack.warning.invalid=
|
||||
# resourcepack.warning.manipulate=
|
||||
# resourcepack.warning.missing_game_meta=
|
||||
# resourcepack.warning.missing_pack_meta=
|
||||
# resourcepack.warning.too_new=
|
||||
# resourcepack.warning.too_old=
|
||||
|
||||
reveal.in_file_manager=فتح في مدير الملفات
|
||||
|
||||
@@ -1254,12 +1333,12 @@ schematics=المخططات
|
||||
schematics.add=إضافة
|
||||
schematics.add.failed=فشل إضافة ملفات المخططات
|
||||
schematics.add.title=اختر ملف المخطط الذي تريد استيراده
|
||||
schematics.back_to=العودة إلى "%s"
|
||||
schematics.create_directory=مجلد جديد
|
||||
schematics.create_directory.prompt=يُرجى إدخال اسم المجلد الجديد
|
||||
schematics.create_directory.failed=فشل إنشاء المجلد
|
||||
schematics.create_directory.failed.already_exists=المجلد موجود مسبقاً
|
||||
schematics.create_directory.failed.invalid_name=الاسم يحتوي على أحرف غير صالحة
|
||||
# schematics.empty=
|
||||
schematics.info.description=الوصف
|
||||
schematics.info.enclosing_size=الحجم المحيط
|
||||
schematics.info.name=الاسم
|
||||
@@ -1269,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 عنصر فرعي
|
||||
|
||||
@@ -1281,11 +1366,13 @@ search.sort=ترتيب حسب
|
||||
search.first_page=الأولى
|
||||
search.previous_page=السابقة
|
||||
search.next_page=التالية
|
||||
# search.no_results_found=
|
||||
search.last_page=الأخيرة
|
||||
search.page_n=%1$d / %2$s
|
||||
|
||||
selector.choose=اختر
|
||||
selector.choose_file=اختر ملفاً
|
||||
# selector.choose_directory=
|
||||
selector.custom=مخصص
|
||||
|
||||
settings=الإعدادات
|
||||
@@ -1298,14 +1385,30 @@ settings.advanced.dont_check_game_completeness=عدم التحقق من سلام
|
||||
settings.advanced.dont_check_jvm_validity=عدم التحقق من توافق JVM
|
||||
settings.advanced.dont_patch_natives=عدم محاولة استبدال المكتبات الأصلية تلقائياً
|
||||
settings.advanced.environment_variables=متغيرات البيئة
|
||||
# settings.advanced.environment_variables.subtitle=
|
||||
settings.advanced.game_dir.default=افتراضي (".minecraft/")
|
||||
settings.advanced.game_dir.independent=معزول (".minecraft/versions/<اسم النسخة>/", باستثناء الأصول والمكتبات)
|
||||
# settings.advanced.graphics=
|
||||
# settings.advanced.graphics_backend=
|
||||
# settings.advanced.graphics_backend.desc=
|
||||
# settings.advanced.graphics_backend.default=
|
||||
# settings.advanced.graphics_backend.default.desc=
|
||||
# settings.advanced.graphics_backend.opengl=
|
||||
# settings.advanced.graphics_backend.opengl.desc=
|
||||
# settings.advanced.graphics_backend.vulkan=
|
||||
# settings.advanced.graphics_backend.vulkan.desc=
|
||||
# settings.advanced.graphics_backend.vulkan.desc.global=
|
||||
# settings.advanced.graphics_backend.vulkan.desc.unsupported=
|
||||
settings.advanced.java_permanent_generation_space=مساحة PermGen
|
||||
settings.advanced.jvm=خيارات JVM
|
||||
settings.advanced.jvm_args=وسيطات JVM
|
||||
settings.advanced.jvm_args.prompt=\ · إذا كانت الوسيطات المُدخلة في "وسيطات JVM" مطابقة للوسيطات الافتراضية، فلن تُضاف.\n\
|
||||
\ · أدخل أي وسيطات GC في "وسيطات JVM"، وسيتم تعطيل وسيطة G1 الافتراضية.\n\
|
||||
\ · فعّل "عدم إضافة وسيطات JVM الافتراضية" لتشغيل اللعبة بدون إضافة الوسيطات الافتراضية.
|
||||
# settings.advanced.jvm_memory.deprecated=
|
||||
# settings.advanced.jvm_memory.deprecated.subtitle=
|
||||
# settings.advanced.launch_options=
|
||||
# settings.advanced.launch_options.subtitle=
|
||||
settings.advanced.launcher_visibility.close=إغلاق المشغّل بعد تشغيل اللعبة
|
||||
settings.advanced.launcher_visibility.hide=إخفاء المشغّل بعد تشغيل اللعبة
|
||||
settings.advanced.launcher_visibility.hide_and_reopen=إخفاء المشغّل وإظهاره عند إغلاق اللعبة
|
||||
@@ -1313,7 +1416,9 @@ settings.advanced.launcher_visibility.keep=إبقاء المشغّل ظاهرا
|
||||
settings.advanced.launcher_visible=ظهور المشغّل
|
||||
settings.advanced.minecraft_arguments=وسيطات التشغيل
|
||||
settings.advanced.minecraft_arguments.prompt=افتراضي
|
||||
# settings.advanced.natives_settings=
|
||||
settings.advanced.natives_directory=مسار المكتبات الأصلية
|
||||
# settings.advanced.natives_directory.custom.enabled=
|
||||
settings.advanced.no_jvm_args=عدم إضافة وسيطات JVM الافتراضية
|
||||
settings.advanced.no_optimizing_jvm_args=عدم إضافة وسيطات تحسين JVM الافتراضية
|
||||
settings.advanced.precall_command=أمر ما قبل التشغيل
|
||||
@@ -1333,15 +1438,56 @@ settings.advanced.post_exit_command=أمر ما بعد الإغلاق
|
||||
settings.advanced.post_exit_command.prompt=أوامر تُنفَّذ بعد إغلاق اللعبة
|
||||
settings.advanced.renderer=المُصيِّر
|
||||
settings.advanced.renderer.default=افتراضي
|
||||
settings.advanced.renderer.d3d12=Mesa D3D12
|
||||
# settings.advanced.renderer.default.desc=
|
||||
# settings.advanced.renderer.opengl=
|
||||
# settings.advanced.renderer.vulkan=
|
||||
# settings.advanced.renderer.gpu_preferences=
|
||||
# settings.advanced.renderer.lavapipe=
|
||||
# settings.advanced.renderer.lavapipe.desc=
|
||||
# settings.advanced.renderer.dozen=
|
||||
# settings.advanced.renderer.dozen.desc=
|
||||
# settings.advanced.renderer.nvidia_vulkan=
|
||||
# settings.advanced.renderer.nvidia_vulkan.desc=
|
||||
# settings.advanced.renderer.nvidia_nvk=
|
||||
# settings.advanced.renderer.nvidia_nvk.desc=
|
||||
# settings.advanced.renderer.amdvlk=
|
||||
# settings.advanced.renderer.amdvlk.desc=
|
||||
# settings.advanced.renderer.amd_radv=
|
||||
# settings.advanced.renderer.amd_radv.desc=
|
||||
# settings.advanced.renderer.intel_vulkan=
|
||||
# settings.advanced.renderer.intel_vulkan.desc=
|
||||
# settings.advanced.renderer.intel_anv=
|
||||
# settings.advanced.renderer.intel_anv.desc=
|
||||
# settings.advanced.renderer.intel_hasvk=
|
||||
# settings.advanced.renderer.intel_hasvk.desc=
|
||||
# settings.advanced.renderer.qualcomm=
|
||||
# settings.advanced.renderer.qualcomm.desc=
|
||||
# settings.advanced.renderer.turnip=
|
||||
# settings.advanced.renderer.turnip.desc=
|
||||
# settings.advanced.renderer.moltenvk=
|
||||
# settings.advanced.renderer.moltenvk.desc=
|
||||
# settings.advanced.renderer.kosmickrisp=
|
||||
# settings.advanced.renderer.kosmickrisp.desc=
|
||||
# settings.advanced.renderer.powervr=
|
||||
# settings.advanced.renderer.powervr.desc=
|
||||
# settings.advanced.renderer.panvk=
|
||||
# settings.advanced.renderer.panvk.desc=
|
||||
# settings.advanced.renderer.v3dv=
|
||||
# settings.advanced.renderer.v3dv.desc=
|
||||
settings.advanced.renderer.llvmpipe=Mesa LLVMpipe
|
||||
# settings.advanced.renderer.llvmpipe.desc=
|
||||
settings.advanced.renderer.d3d12=Mesa D3D12
|
||||
# settings.advanced.renderer.d3d12.desc=
|
||||
settings.advanced.renderer.zink=Mesa Zink
|
||||
# settings.advanced.renderer.zink.desc=
|
||||
settings.advanced.server_ip=عنوان الخادم
|
||||
settings.advanced.server_ip.prompt=الانضمام تلقائياً بعد تشغيل اللعبة
|
||||
settings.advanced.unsupported_system_options=إعدادات غير مطبّقة على النظام الحالي
|
||||
settings.advanced.use_native_glfw_or_sdl=استخدام GLFW / SDL النظام
|
||||
settings.advanced.use_native_openal=استخدام OpenAL النظام
|
||||
# settings.advanced.use_native_sdl=
|
||||
settings.advanced.linux_freebsd_only=Linux/FreeBSD فقط
|
||||
# settings.advanced.windows_only=
|
||||
settings.advanced.wrapper_launcher=أمر التغليف
|
||||
settings.advanced.wrapper_launcher.prompt=يتيح التشغيل باستخدام برنامج تغليف إضافي مثل "optirun" على Linux
|
||||
|
||||
@@ -1349,9 +1495,22 @@ settings.custom=مخصص
|
||||
|
||||
settings.game=الإعدادات
|
||||
settings.game.current=اللعبة
|
||||
# settings.game.default_isolation=
|
||||
# settings.game.default_isolation.subtitle=
|
||||
# settings.game.default_isolation.always=
|
||||
# settings.game.default_isolation.always.desc=
|
||||
# settings.game.default_isolation.modded=
|
||||
# settings.game.default_isolation.modded.desc=
|
||||
# settings.game.default_isolation.never=
|
||||
# settings.game.default_isolation.never.desc=
|
||||
settings.game.dimension=الدقة
|
||||
settings.game.exploration=استكشاف
|
||||
settings.game.fullscreen=ملء الشاشة
|
||||
# settings.game.inherit_global=
|
||||
# settings.game.instance_settings.unsupported=
|
||||
# settings.game.presets.unsupported=
|
||||
# settings.game.isolation=
|
||||
# settings.game.isolation.subtitle=
|
||||
settings.game.java_directory=Java
|
||||
settings.game.java_directory.auto=اختيار تلقائي
|
||||
settings.game.java_directory.auto.not_found=لم يتم تثبيت أي إصدار Java مناسب.
|
||||
@@ -1361,28 +1520,51 @@ settings.game.java_directory.invalid=مسار Java غير صحيح
|
||||
settings.game.java_directory.version=تحديد إصدار Java
|
||||
settings.game.java_directory.template=%1$s (%2$s)
|
||||
settings.game.management=إدارة
|
||||
# settings.game.override_global=
|
||||
# settings.game.quick_play=
|
||||
# settings.game.quick_play.multiplayer=
|
||||
# settings.game.quick_play.none=
|
||||
# settings.game.quick_play.realms=
|
||||
# settings.game.quick_play.singleplayer=
|
||||
# settings.game.quick_play.subtitle=
|
||||
# settings.game.running_directory=
|
||||
# settings.game.section.basic=
|
||||
# settings.game.section.game=
|
||||
# settings.game.window_type=
|
||||
# settings.game.window_type.fullscreen=
|
||||
# settings.game.window_type.maximized=
|
||||
# settings.game.window_type.windowed=
|
||||
settings.game.working_directory.choose=اختر مجلد العمل
|
||||
|
||||
settings.icon=الأيقونة
|
||||
# settings.game_directories.read_only=
|
||||
|
||||
settings.launcher=إعدادات المشغّل
|
||||
# settings.launcher.allow_auto_agent=
|
||||
# settings.launcher.allow_auto_agent.subtitle=
|
||||
# settings.launcher.animation=
|
||||
settings.launcher.appearance=المظهر
|
||||
settings.launcher.brightness=وضع السمة
|
||||
settings.launcher.brightness.auto=متابعة إعدادات النظام
|
||||
settings.launcher.brightness.dark=الوضع الداكن
|
||||
settings.launcher.brightness.light=الوضع الفاتح
|
||||
settings.launcher.debug=تصحيح
|
||||
# settings.launcher.disable_april_fools=
|
||||
settings.launcher.disable_auto_game_options=عدم تغيير لغة اللعبة تلقائياً
|
||||
settings.launcher.download=التنزيل
|
||||
settings.launcher.download.threads=الخيوط
|
||||
settings.launcher.download.threads.auto=تحديد تلقائي
|
||||
# settings.launcher.download.threads.custom=
|
||||
settings.launcher.download_source=مصدر التنزيل
|
||||
settings.launcher.download_source.auto=اختيار مصادر التنزيل تلقائياً
|
||||
# settings.launcher.default_addon_source=
|
||||
# settings.launcher.misc=
|
||||
settings.launcher.font=الخط
|
||||
settings.launcher.font.anti_aliasing=مضاد التعرج
|
||||
settings.launcher.font.anti_aliasing.auto=تلقائي
|
||||
settings.launcher.font.anti_aliasing.gray=تدرج رمادي
|
||||
settings.launcher.font.anti_aliasing.lcd=بكسل فرعي
|
||||
# settings.launcher.fonts=
|
||||
settings.launcher.general=عام
|
||||
settings.launcher.language=اللغة
|
||||
settings.launcher.launcher_log.export=تصدير سجلات المشغّل
|
||||
@@ -1402,9 +1584,35 @@ settings.launcher.proxy.port=المنفذ
|
||||
settings.launcher.proxy.socks=SOCKS
|
||||
settings.launcher.proxy.username=اسم المستخدم
|
||||
settings.launcher.theme=لون السمة
|
||||
# settings.launcher.theme_color=
|
||||
# settings.launcher.theme_color_style=
|
||||
# settings.launcher.theme_color_style.content=
|
||||
# settings.launcher.theme_color_style.content.desc=
|
||||
# settings.launcher.theme_color_style.expressive=
|
||||
# settings.launcher.theme_color_style.expressive.desc=
|
||||
# settings.launcher.theme_color_style.fidelity=
|
||||
# settings.launcher.theme_color_style.fidelity.desc=
|
||||
# settings.launcher.theme_color_style.fruit_salad=
|
||||
# settings.launcher.theme_color_style.fruit_salad.desc=
|
||||
# settings.launcher.theme_color_style.monochrome=
|
||||
# settings.launcher.theme_color_style.monochrome.desc=
|
||||
# settings.launcher.theme_color_style.neutral=
|
||||
# settings.launcher.theme_color_style.neutral.desc=
|
||||
# settings.launcher.theme_color_style.rainbow=
|
||||
# settings.launcher.theme_color_style.rainbow.desc=
|
||||
# settings.launcher.theme_color_style.tonal_spot=
|
||||
# settings.launcher.theme_color_style.tonal_spot.desc=
|
||||
# settings.launcher.theme_color_style.vibrant=
|
||||
# settings.launcher.theme_color_style.vibrant.desc=
|
||||
# settings.launcher.theme_color_type.background=
|
||||
# settings.launcher.theme_color_type.custom=
|
||||
# settings.launcher.theme_color_type.custom.description=
|
||||
# settings.launcher.theme_color_type.default=
|
||||
# settings.launcher.theme_color_type.system=
|
||||
settings.launcher.title_transparent=شريط عنوان شفاف
|
||||
settings.launcher.turn_off_animations=تعطيل الحركات
|
||||
settings.launcher.version_list_source=قائمة الإصدارات
|
||||
# settings.launcher.window_transparent=
|
||||
settings.launcher.background.settings.opacity=الشفافية
|
||||
|
||||
settings.memory=الذاكرة
|
||||
@@ -1412,15 +1620,30 @@ settings.memory.allocate=%1$.1f جيجابايت مخصص
|
||||
settings.memory.allocate.exceeded=%1$.1f جيجابايت مخصص (%2$.1f جيجابايت متاح)
|
||||
settings.memory.auto_allocate=تخصيص تلقائي
|
||||
settings.memory.lower_bound=الحد الأدنى للذاكرة
|
||||
# settings.memory.manual_allocate=
|
||||
settings.memory.unit.mib=ميجابايت
|
||||
settings.memory.used_per_total=%1$.1f جيجابايت مستخدم / %2$.1f جيجابايت إجمالي
|
||||
settings.physical_memory=حجم الذاكرة الفعلية
|
||||
settings.show_log=عرض السجلات
|
||||
settings.enable_debug_log_output=إخراج سجل التصحيح
|
||||
# settings.file.force_write=
|
||||
# settings.file.force_write.confirm=
|
||||
settings.tabs.installers=محمّلات المودات
|
||||
settings.take_effect_after_restart=يُطبَّق بعد إعادة التشغيل
|
||||
settings.type=نوع إعدادات النسخة
|
||||
settings.type.global.manage=الإعدادات العامة
|
||||
# settings.type.global.preset=
|
||||
# settings.type.global.preset.create=
|
||||
# settings.type.global.preset.default=
|
||||
# settings.type.global.preset.duplicate_name=
|
||||
# settings.type.global.preset.help=
|
||||
# settings.type.global.preset.manage_all=
|
||||
# settings.type.global.preset.name=
|
||||
# settings.type.global.preset.rename=
|
||||
# settings.type.global.preset.remove=
|
||||
# settings.type.global.preset.remove.confirm=
|
||||
# settings.type.global.preset.subtitle=
|
||||
# settings.type.global.preset.auto_name=
|
||||
|
||||
shaderpack.download.title=تنزيل ظلال - %1s
|
||||
|
||||
@@ -1433,6 +1656,7 @@ terracotta.terracotta=Terracotta | متعدد اللاعبين
|
||||
terracotta.status=الردهة
|
||||
terracotta.back=خروج
|
||||
terracotta.feedback.title=ملء استمارة التقييم
|
||||
# terracotta.feedback.desc=
|
||||
terracotta.feedback.desc.update=مع تحديث HMCL لنواة متعدد اللاعبين، نأمل أن تخصص 10 ثوانٍ لملء استمارة التقييم.
|
||||
terracotta.sudo_installing=يجب على HMCL التحقق من كلمة مرورك قبل تثبيت نواة متعدد اللاعبين
|
||||
terracotta.difficulty.easiest=شبكة ممتازة: الاتصال شبه مضمون!
|
||||
@@ -1509,6 +1733,40 @@ terracotta.unsupported.arch.32bit=متعدد اللاعبين غير مدعوم
|
||||
terracotta.unsupported.arch.loongarch64_ow=متعدد اللاعبين غير مدعوم على توزيعات Linux LoongArch64 القديمة. يُرجى التحديث إلى توزيعة العالم الجديد (مثل AOSC OS).
|
||||
terracotta.unsupported.region=ميزة متعدد اللاعبين متاحة حالياً فقط لمستخدمي البر الرئيسي الصيني وقد لا تكون متاحة في منطقتك.
|
||||
|
||||
# theme_pack.appearance.follow_theme.tooltip=
|
||||
# theme_pack.appearance.custom.tooltip=
|
||||
# theme_pack.apply=
|
||||
# theme_pack.apply.confirm=
|
||||
# theme_pack.apply.failed=
|
||||
# theme_pack.builtin=
|
||||
# theme_pack.current.missing=
|
||||
# theme_pack.delete=
|
||||
# theme_pack.delete.confirm=
|
||||
# theme_pack.delete.failed=
|
||||
# theme_pack.delete.success=
|
||||
# theme_pack.directory=
|
||||
# theme_pack.empty=
|
||||
# theme_pack.export=
|
||||
# theme_pack.export.author=
|
||||
# theme_pack.export.failed=
|
||||
# theme_pack.export.id=
|
||||
# theme_pack.export.name=
|
||||
# theme_pack.export.subtitle=
|
||||
# theme_pack.export.success=
|
||||
# theme_pack.export.version=
|
||||
# theme_pack.export.title=
|
||||
# theme_pack.file=
|
||||
# theme_pack.import=
|
||||
# theme_pack.import.failed=
|
||||
# theme_pack.import.success=
|
||||
# theme_pack.import.title=
|
||||
# theme_pack.load.failed=
|
||||
# theme_pack.manage=
|
||||
# theme_pack.theme=
|
||||
# theme_pack.theme.id=
|
||||
# theme_pack.themes=
|
||||
# theme_pack.version=
|
||||
|
||||
unofficial.hint=أنت تستخدم إصداراً غير رسمي من HMCL. لا نستطيع ضمان أمانه.
|
||||
|
||||
update=تحديث
|
||||
@@ -1530,15 +1788,21 @@ update.channel.nightly.hint=أنت تستخدم حالياً إصدار القن
|
||||
update.channel.nightly.title=إشعار القناة الليلية
|
||||
update.channel.stable=رسمي
|
||||
update.checking=جارٍ التحقق من التحديثات
|
||||
# update.check_failed=
|
||||
# update.disable_auto_show_update_dialog=
|
||||
# update.disable_auto_show_update_dialog.subtitle=
|
||||
update.failed=فشل التحديث
|
||||
update.found=تحديث متاح!
|
||||
update.newest_version=أحدث إصدار: %s
|
||||
update.bubble.title=تحديث متاح: %s
|
||||
update.bubble.subtitle=انقر هنا للتحديث
|
||||
# update.note.stable=
|
||||
# update.note.dev=
|
||||
update.latest=هذا هو أحدث إصدار
|
||||
update.tooltip=تحديث
|
||||
update.preview=الاطلاع المبكر على إصدارات HMCL
|
||||
update.preview.subtitle=فعّل هذا الخيار لاستقبال إصدارات HMCL الجديدة مبكراً للاختبار قبل إصدارها الرسمي.
|
||||
# update.unverified=
|
||||
|
||||
warning.java_interpreted_mode=يعمل HMCL في بيئة Java مُترجمة تفسيرياً، مما سيؤثر بشكل كبير على الأداء.\n\
|
||||
\n\
|
||||
|
||||
@@ -160,8 +160,6 @@ account.skin.upload.failed=Fehler beim Hochladen des Skins.
|
||||
account.skin.invalid_skin=Ungültige Skin-Datei.
|
||||
account.username=Benutzername
|
||||
|
||||
addon.broken_dependency.title=Fehlerhafte Abhängigkeit
|
||||
addon.broken_dependency.desc=Diese Abhängigkeit existierte vorher, ist aber nicht mehr vorhanden. Versuchen Sie, eine andere Download-Quelle zu verwenden.
|
||||
addon.category=Kategorie
|
||||
addon.changelog=Änderungsprotokoll
|
||||
addon.changelog.empty=Derzeit kein Änderungsprotokoll
|
||||
@@ -186,10 +184,16 @@ addon.dependency.tool=Erforderliche Abhängigkeiten (Müssen separat heruntergel
|
||||
addon.dependency.include=Integrierte Abhängigkeiten (Bereits in der Addon-Datei vom Autor gepackt. Muss nicht separat heruntergeladen werden)
|
||||
addon.dependency.incompatible=Inkompatible Addons (Wenn diese Addons gleichzeitig installiert werden, kann das Spiel nicht gestartet werden)
|
||||
addon.dependency.broken=Fehlerhafte Abhängigkeiten (Dieses Addon existierte vorher, ist aber nicht mehr vorhanden. Versuchen Sie, eine andere Download-Quelle zu verwenden.)
|
||||
# addon.dependencies.has_broken=
|
||||
addon.download.recommend=Empfohlene Version für Minecraft %1s
|
||||
addon.download.title.release=Minecraft %s
|
||||
addon.download.title.snapshot=Minecraft %s (Snapshots)
|
||||
addon.modrinth=Modrinth
|
||||
addon.sort.date_created=Erstellungsdatum
|
||||
addon.sort.last_updated=Zuletzt aktualisiert
|
||||
addon.sort.popularity=Beliebtheit
|
||||
# addon.sort.relevancy=
|
||||
addon.sort.total_downloads=Downloads insgesamt
|
||||
|
||||
archive.author=Autor(en)
|
||||
archive.date=Veröffentlichungsdatum
|
||||
@@ -371,13 +375,6 @@ curse.category.6553=Realistisch
|
||||
curse.category.6554=Fantasie
|
||||
curse.category.6555=Vanilla
|
||||
|
||||
curse.sort.author=Autor
|
||||
curse.sort.date_created=Erstellungsdatum
|
||||
curse.sort.last_updated=Zuletzt aktualisiert
|
||||
curse.sort.name=Name
|
||||
curse.sort.popularity=Beliebtheit
|
||||
curse.sort.total_downloads=Downloads insgesamt
|
||||
|
||||
datetime.format=dd.MM.yyyy, HH\:mm\:ss
|
||||
|
||||
download=Herunterladen
|
||||
@@ -433,32 +430,32 @@ extension.resourcepack=Ressourcenpaket-Archiv
|
||||
extension.schematic=Schematik-Datei
|
||||
extension.world=Welt-Archiv
|
||||
|
||||
fatal.create_hmcl_current_directory_failure=Hello Minecraft! Launcher kann das HMCL-Verzeichnis (%s) nicht erstellen. Bitte verschieben Sie HMCL an einen anderen Speicherort und öffnen Sie es erneut.
|
||||
fatal.create_hmcl_current_directory_failure=HMCL kann das HMCL-Verzeichnis (%s) nicht erstellen. Bitte verschieben Sie HMCL an einen anderen Speicherort und öffnen Sie es erneut.
|
||||
fatal.javafx.incomplete=Die JavaFX-Umgebung ist unvollständig.\n\
|
||||
Bitte versuchen Sie, Ihr Java zu ersetzen oder OpenJFX neu zu installieren.
|
||||
fatal.javafx.missing=Fehlende JavaFX-Umgebung. Bitte öffnen Sie den Hello Minecraft! Launcher mit Java, welches OpenJFX enthält.
|
||||
fatal.config_change_owner_root=Sie verwenden das Root-Konto, um den Hello Minecraft! Launcher zu öffnen. Dies könnte Sie daran hindern, HMCL in Zukunft mit einem anderen Konto zu öffnen.\n\
|
||||
fatal.javafx.missing=Fehlende JavaFX-Umgebung. Bitte öffnen Sie den HMCL mit Java, welches OpenJFX enthält.
|
||||
fatal.config_change_owner_root=Sie verwenden das Root-Konto, um den HMCL zu öffnen. Dies könnte Sie daran hindern, HMCL in Zukunft mit einem anderen Konto zu öffnen.\n\
|
||||
Möchten Sie trotzdem fortfahren?
|
||||
fatal.config_in_temp_dir=Sie öffnen den Hello Minecraft! Launcher in einem temporären Verzeichnis. Ihre Einstellungen und Spieldaten könnten verloren gehen.\n\
|
||||
fatal.config_in_temp_dir=Sie öffnen den HMCL in einem temporären Verzeichnis. Ihre Einstellungen und Spieldaten könnten verloren gehen.\n\
|
||||
Es wird empfohlen, HMCL an einen anderen Ort zu verschieben und erneut zu öffnen.\n\
|
||||
Möchten Sie trotzdem fortfahren?
|
||||
fatal.config_loading_failure=Konfigurationsdateien können nicht geladen werden.\n\
|
||||
Bitte stellen Sie sicher, dass der Hello Minecraft! Launcher Lese- und Schreibzugriff auf "%s" und die darin enthaltenen Dateien hat.\n\
|
||||
Bitte stellen Sie sicher, dass der HMCL Lese- und Schreibzugriff auf "%s" und die darin enthaltenen Dateien hat.\n\
|
||||
Versuchen Sie unter macOS, HMCL an einen Ort mit anderen Berechtigungen als "Schreibtisch", "Downloads" und "Dokumente" zu legen und versuchen Sie es erneut.
|
||||
fatal.config_loading_failure.unix=Hello Minecraft! Launcher konnte die Konfigurationsdatei nicht laden, da sie vom Benutzer "%1$s" erstellt wurde.\n\
|
||||
fatal.config_loading_failure.unix=HMCL konnte die Konfigurationsdatei nicht laden, da sie vom Benutzer "%1$s" erstellt wurde.\n\
|
||||
Bitte öffnen Sie HMCL als Root-Benutzer (nicht empfohlen) oder führen Sie den folgenden Befehl im Terminal aus, um den Besitz der Konfigurationsdatei auf den aktuellen Benutzer zu übertragen:\n%2$s
|
||||
fatal.config_unsupported_version=Die aktuelle Konfigurationsdatei wurde von einer neueren Version des Hello Minecraft! Launchers erstellt, und diese Version von HMCL kann sie nicht richtig laden.\n\
|
||||
fatal.config_unsupported_version=Die aktuelle Konfigurationsdatei wurde von einer neueren Version des HMCLs erstellt, und diese Version von HMCL kann sie nicht richtig laden.\n\
|
||||
Bitte aktualisieren Sie HMCL und starten Sie es neu.\n\
|
||||
Vor dem Aktualisieren des Launchers werden keine von Ihnen geänderten Einstellungen gespeichert.
|
||||
fatal.mac_app_translocation=Hello Minecraft! Launcher wurde aufgrund von macOS-Sicherheitsmechanismen vom Betriebssystem in ein temporäres Verzeichnis isoliert.\n\
|
||||
fatal.mac_app_translocation=HMCL wurde aufgrund von macOS-Sicherheitsmechanismen vom Betriebssystem in ein temporäres Verzeichnis isoliert.\n\
|
||||
Bitte verschieben Sie HMCL in ein anderes Verzeichnis, bevor Sie versuchen, es zu öffnen. Andernfalls könnten Ihre Einstellungen und Spieldaten nach einem Neustart verloren gehen.\n\
|
||||
Möchten Sie trotzdem fortfahren?
|
||||
fatal.migration_requires_manual_reboot=Hello Minecraft! Launcher wurde aktualisiert. Bitte starten Sie den Launcher neu.
|
||||
fatal.apply_update_failure=Es tut uns leid, aber der Hello Minecraft! Launcher kann nicht automatisch aktualisiert werden.\n\
|
||||
fatal.migration_requires_manual_reboot=HMCL wurde aktualisiert. Bitte starten Sie den Launcher neu.
|
||||
fatal.apply_update_failure=Es tut uns leid, aber der HMCL kann nicht automatisch aktualisiert werden.\n\
|
||||
\n\
|
||||
Sie können manuell aktualisieren, indem Sie eine neuere Launcher-Version von %s herunterladen.\n\
|
||||
Wenn das Problem weiterhin besteht, melden Sie es uns bitte.
|
||||
fatal.apply_update_need_win7=Der Hello Minecraft! Launcher kann unter Windows XP/Vista nicht automatisch aktualisiert werden.\n\
|
||||
fatal.apply_update_need_win7=Der HMCL kann unter Windows XP/Vista nicht automatisch aktualisiert werden.\n\
|
||||
\n\
|
||||
Sie können manuell aktualisieren, indem Sie eine neuere Launcher-Version von %s herunterladen.
|
||||
fatal.deprecated_java_version=HMCL wird in Zukunft Java 17 oder neuer benötigen, um zu laufen, wird aber weiterhin das Starten von Spielen mit Java 6~16 unterstützen.\n\
|
||||
@@ -467,14 +464,14 @@ Es wird empfohlen, die neueste Java-Version zu installieren, um sicherzustellen,
|
||||
\n\
|
||||
Sie können die alte Java-Version weiterhin behalten. HMCL kann mehrere Java-Versionen erkennen und verwalten und wählt basierend auf der Spielversion automatisch das entsprechende Java für Sie aus.
|
||||
fatal.deprecated_java_version.download_link=Java %d herunterladen
|
||||
fatal.samba=Wenn Sie den Hello Minecraft! Launcher von einem Samba-Netzlaufwerk geöffnet haben, funktionieren einige Funktionen möglicherweise nicht. Bitte versuchen Sie, Ihr Java zu aktualisieren oder den Launcher in ein anderes Verzeichnis zu verschieben.
|
||||
fatal.samba=Wenn Sie den HMCL von einem Samba-Netzlaufwerk geöffnet haben, funktionieren einige Funktionen möglicherweise nicht. Bitte versuchen Sie, Ihr Java zu aktualisieren oder den Launcher in ein anderes Verzeichnis zu verschieben.
|
||||
fatal.illegal_char=Ihr Arbeitsverzeichnis enthält ein ungültiges Zeichen "=". Sie können authlib-injector nicht verwenden oder den Skin Ihres Offline-Kontos nicht ändern.
|
||||
fatal.unsupported_platform=Minecraft wird auf Ihrer Plattform noch nicht vollständig unterstützt, daher fehlen möglicherweise Funktionen oder Sie können das Spiel überhaupt nicht starten.\n\
|
||||
\n\
|
||||
Wenn Sie Minecraft 1.17 und neuer nicht starten können, können Sie versuchen, den "Renderer" unter "Allgemeine/Instanzspezifische Einstellungen → Grafikeinstellungen" auf "Mesa LLVMpipe" oder "Mesa Lavapipe" umzustellen, um CPU-Rendering für bessere Kompatibilität zu verwenden.
|
||||
fatal.unsupported_platform.loongarch=Der Hello Minecraft! Launcher bietet nun Unterstützung für die Loongson-Plattform.\n\
|
||||
fatal.unsupported_platform.loongarch=Der HMCL bietet nun Unterstützung für die Loongson-Plattform.\n\
|
||||
Wenn beim Spielen Probleme auftreten, können Sie https://docs.hmcl.net/groups.html für Hilfe besuchen.
|
||||
fatal.unsupported_platform.windows_arm64=Der Hello Minecraft! Launcher bietet native Unterstützung für die Windows-on-Arm-Plattform. Wenn beim Spielen Probleme auftreten, versuchen Sie bitte, das Spiel mit Java basierend auf der x86-Architektur zu starten.\n\
|
||||
fatal.unsupported_platform.windows_arm64=Der HMCL bietet native Unterstützung für die Windows-on-Arm-Plattform. Wenn beim Spielen Probleme auftreten, versuchen Sie bitte, das Spiel mit Java basierend auf der x86-Architektur zu starten.\n\
|
||||
\n\
|
||||
Wenn Sie die <b>Qualcomm</b>-Plattform verwenden, müssen Sie möglicherweise das <a href="ms-windows-store://pdp/?productid=9NQPSL29BFFF">OpenGL-Kompatibilitätspaket</a> installieren, bevor Sie Spiele spielen.\n\
|
||||
Klicken Sie auf den Link, um zum Microsoft Store zu navigieren und das Kompatibilitätspaket zu installieren.
|
||||
@@ -732,9 +729,10 @@ game_directory.instance_directory.choose=Spielverzeichnis auswählen
|
||||
game_directory.name=Name
|
||||
game_directory.new=Neues Verzeichnis
|
||||
game_directory.use_relative_path=Verwenden Sie, wenn möglich, einen relativen Pfad für das Spielverzeichnis
|
||||
# game_directory.root=
|
||||
|
||||
help=Hilfe
|
||||
help.doc=Hello Minecraft! Launcher Dokumentation
|
||||
help.doc=HMCL Dokumentation
|
||||
help.detail=Für Hersteller von Datenpaketen und Modpacks.
|
||||
|
||||
input.email=Der Benutzername muss eine E-Mail-Adresse sein.
|
||||
@@ -744,6 +742,7 @@ input.url=Die Eingabe muss eine gültige URL sein.
|
||||
|
||||
install=Neue Instanz
|
||||
install.change_version=Version ändern
|
||||
# install.change_version.title=
|
||||
install.change_version.confirm=Sind Sie sicher, dass Sie %1$s von Version %2$s zu %3$s wechseln möchten?
|
||||
install.change_version.process=Versionsänderungsprozess
|
||||
install.failed=Installation fehlgeschlagen
|
||||
@@ -752,6 +751,7 @@ install.failed.downloading.detail=Datei konnte nicht heruntergeladen werden: %s
|
||||
install.failed.downloading.timeout=Download-Timeout beim Abrufen von: %s
|
||||
install.failed.install_online=Die bereitgestellte Datei konnte nicht identifiziert werden. Wenn Sie einen Mod installieren, navigieren Sie zur Seite "Mods".
|
||||
install.failed.malformed=Die heruntergeladenen Dateien sind beschädigt. Sie können versuchen, dieses Problem zu beheben, indem Sie unter "Einstellungen → Herunterladen → Downloadquelle" zu einer anderen Downloadquelle wechseln.
|
||||
# install.failed.cleanroom_not_compatible_with_forge=
|
||||
install.failed.optifine_conflict=OptiFine und Fabric können auf Minecraft 1.13 oder neuer nicht gleichzeitig installiert werden.
|
||||
install.failed.optifine_forge_1.17=Für Minecraft 1.17.1 ist Forge nur mit OptiFine H1 pre2 oder neuer kompatibel. Sie können sie installieren, indem Sie "Snapshots" aktivieren, wenn Sie eine OptiFine-Version in HMCL auswählen.
|
||||
install.failed.version_mismatch=Dieser Loader benötigt die Spielversion %1$s, aber die installierte ist %2$s.
|
||||
@@ -886,6 +886,7 @@ launch.advice.forge28_2_2_optifine=Forge Version 28.2.2 oder neuer ist nicht mit
|
||||
launch.advice.forge37_0_60=Forge-Versionen vor 37.0.60 sind nicht mit Java 17 kompatibel. Bitte aktualisieren Sie Forge auf 37.0.60 oder neuer, oder starten Sie das Spiel mit Java 16.
|
||||
launch.advice.java8_51_1_13=Minecraft 1.13 kann auf Java 8-Versionen vor 1.8.0_51 abstürzen. Bitte installieren Sie die neueste Java 8-Version.
|
||||
launch.advice.java9=Sie können Minecraft 1.12 oder früher nicht mit Java 9 oder neuer starten. Bitte verwenden Sie stattdessen Java 8.
|
||||
# launch.advice.lwjgl_3_4_1=
|
||||
# launch.advice.lwjgl_3_4_1=Die aktuelle Spielversion hat ein bekanntes Problem, das zu Ruckeln im Spiel führen kann.\nHMCL kann die Spielleistung automatisch optimieren (mittels eines Java-Agenten).\nErlauben Sie HMCL, das Spiel automatisch zu optimieren?\nWenn Probleme auftreten, können Sie die Option "HMCL erlauben, das Spiel zu modifizieren" unter "Einstellungen → Allgemein" deaktivieren.
|
||||
launch.advice.lwjgl_3_4_1.title=HMCL erlauben, das Spiel automatisch zu optimieren?
|
||||
launch.advice.modded_java=Einige Mods sind möglicherweise nicht mit neueren Java-Versionen kompatibel. Es wird empfohlen, Java %1$s zu verwenden, um Minecraft %2$s zu starten.
|
||||
@@ -951,9 +952,9 @@ launcher.cache_directory.default=Standard ("%APPDATA%/.minecraft" oder "~/.minec
|
||||
launcher.cache_directory.disabled=Deaktiviert
|
||||
launcher.cache_directory.invalid=Fehler beim Erstellen des Cache-Verzeichnisses, Rückgriff auf Standard.
|
||||
launcher.contact=Kontaktiere uns
|
||||
launcher.crash=Hello Minecraft! Launcher ist auf einen fatalen Fehler gestoßen! Bitte kopieren Sie das folgende Protokoll und bitten Sie in unserem Discord, der QQ-Gruppe, auf GitHub oder in einem anderen Minecraft-Forum um Hilfe.
|
||||
launcher.crash.java_internal_error=Hello Minecraft! Launcher ist auf einen fatalen Fehler gestoßen, da Ihre Java-Installation beschädigt ist. Bitte deinstallieren Sie Java und laden Sie eine geeignete Java-Version <a href="https://bell-sw.com/pages/downloads/#downloads">hier</a> herunter.
|
||||
launcher.crash.hmcl_out_dated=Hello Minecraft! Launcher ist auf einen fatalen Fehler gestoßen! Ihr Launcher ist veraltet. Bitte aktualisieren Sie Ihren Launcher!
|
||||
launcher.crash=HMCL ist auf einen fatalen Fehler gestoßen! Bitte kopieren Sie das folgende Protokoll und bitten Sie in unserem Discord, der QQ-Gruppe, auf GitHub oder in einem anderen Minecraft-Forum um Hilfe.
|
||||
launcher.crash.java_internal_error=HMCL ist auf einen fatalen Fehler gestoßen, da Ihre Java-Installation beschädigt ist. Bitte deinstallieren Sie Java und laden Sie eine geeignete Java-Version <a href="https://bell-sw.com/pages/downloads/#downloads">hier</a> herunter.
|
||||
launcher.crash.hmcl_out_dated=HMCL ist auf einen fatalen Fehler gestoßen! Ihr Launcher ist veraltet. Bitte aktualisieren Sie Ihren Launcher!
|
||||
launcher.update_java=Bitte aktualisieren Sie Ihre Java-Version.
|
||||
|
||||
libraries.download=Bibliotheken werden heruntergeladen
|
||||
@@ -1022,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
|
||||
@@ -1035,11 +1039,11 @@ modpack.type.curse.error=Fehler beim Herunterladen von Abhängigkeiten. Bitte ve
|
||||
modpack.type.curse.not_found=Einige Abhängigkeiten sind nicht mehr verfügbar. Bitte versuchen Sie, eine neuere Modpack-Version zu installieren.
|
||||
modpack.type.manual.warning=Das Modpack wurde vom Herausgeber manuell verpackt, was möglicherweise bereits einen Launcher enthält. Es wird empfohlen, das Modpack zu entpacken und das Spiel mit dem eigenen Launcher zu starten. HMCL kann es dennoch importieren, jedoch ohne Garantie auf Nutzbarkeit. Trotzdem fortfahren?
|
||||
modpack.type.mcbbs=MCBBS
|
||||
modpack.type.mcbbs.export=Kann durch Hello Minecraft! Launcher importiert werden
|
||||
modpack.type.mcbbs.export=Kann durch HMCL importiert werden
|
||||
modpack.type.modrinth=Modrinth
|
||||
modpack.type.modrinth.export=Kann von gängigen Drittanbieter-Launchern importiert werden
|
||||
modpack.type.multimc=MultiMC
|
||||
modpack.type.multimc.export=Kann durch Hello Minecraft! Launcher und MultiMC importiert werden
|
||||
modpack.type.multimc.export=Kann durch HMCL und MultiMC importiert werden
|
||||
modpack.type.server=Modpack vom Server automatisch aktualisieren
|
||||
modpack.type.server.export=Ermöglicht dem Serverbesitzer, die Spielinstanz remote zu aktualisieren
|
||||
modpack.type.server.malformed=Ungültiges Modpack-Manifest. Bitte kontaktieren Sie den Modpack-Ersteller, um dieses Problem zu beheben.
|
||||
@@ -1328,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
|
||||
@@ -1344,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)
|
||||
|
||||
@@ -1477,6 +1486,7 @@ settings.advanced.server_ip.prompt=Nach Spielstart automatisch beitreten
|
||||
settings.advanced.unsupported_system_options=Für das aktuelle System nicht zutreffende Einstellungen
|
||||
settings.advanced.use_native_glfw_or_sdl=System-GLFW / SDL verwenden
|
||||
settings.advanced.use_native_openal=System-OpenAL verwenden
|
||||
# settings.advanced.use_native_sdl=
|
||||
settings.advanced.linux_freebsd_only=Nur Linux/FreeBSD
|
||||
settings.advanced.windows_only=Nur Windows
|
||||
settings.advanced.wrapper_launcher=Wrapper-Befehl
|
||||
@@ -1780,6 +1790,7 @@ update.channel.nightly.hint=Sie verwenden derzeit einen Nightly-Kanal-Build des
|
||||
update.channel.nightly.title=Hinweis zum Nightly-Kanal
|
||||
update.channel.stable=Stabil
|
||||
update.checking=Suche nach Updates
|
||||
# update.check_failed=
|
||||
update.disable_auto_show_update_dialog=Update-Dialog nicht automatisch anzeigen
|
||||
update.disable_auto_show_update_dialog.subtitle=Aktivieren Sie diese Option, um zu verhindern, dass HMCL den Update-Dialog automatisch anzeigt.
|
||||
update.failed=Fehler beim Aktualisieren
|
||||
@@ -1793,6 +1804,7 @@ update.latest=Dies ist die neueste Version
|
||||
update.tooltip=Aktualisieren
|
||||
update.preview=Vorschau auf HMCL-Veröffentlichungen frühzeitig erhalten
|
||||
update.preview.subtitle=Aktivieren Sie diese Option, um neue Versionen von HMCL frühzeitig zum Testen vor ihrer offiziellen Veröffentlichung zu erhalten.
|
||||
# update.unverified=
|
||||
|
||||
warning.java_interpreted_mode=HMCL wird in einer interpretierten Java-Umgebung ausgeführt, was die Leistung erheblich beeinträchtigt.\n\
|
||||
\n\
|
||||
|
||||
File diff suppressed because it is too large.
Load diff
File diff suppressed because it is too large.
Load diff
File diff suppressed because it is too large.
Load diff
File diff suppressed because it is too large.
Load diff
@@ -100,12 +100,10 @@ account.copy_uuid=Копіювати UUID облікового запису
|
||||
account.methods=Тип входу
|
||||
account.methods.authlib_injector=authlib-injector
|
||||
account.methods.microsoft=Microsoft
|
||||
account.methods.microsoft.birth=Як змінити дату народження облікового запису
|
||||
account.methods.microsoft.code=Код (Скопійовано в буфер обміну)
|
||||
account.methods.microsoft.close_page=Авторизація облікового запису Microsoft тепер завершена.\n\
|
||||
\n\
|
||||
Нам залишилося виконати ще трохи роботи, але ви можете безпечно закрити цю вкладку зараз.
|
||||
account.methods.microsoft.deauthorize=Скасувати авторизацію
|
||||
account.methods.microsoft.error.add_family=Будь ласка, натисніть <a href="https://support.microsoft.com/account-billing/837badbc-999e-54d2-2617-d19206b9540a">тут</a>, щоб змінити дату народження вашого акаунта на старше 18 років, або додайте свій акаунт до сім'ї.
|
||||
account.methods.microsoft.error.country_unavailable=XBOX Live недоступний у вашій поточній країні/регіоні.
|
||||
account.methods.microsoft.error.missing_xbox_account=Будь ласка, підтвердіть, що ви придбали Minecraft: Java Edition. Якщо так, натисніть <a href="https://www.minecraft.net/msaprofile/mygames/editprofile">тут</a>, щоб створити свій ігровий профіль.
|
||||
@@ -115,8 +113,6 @@ account.methods.microsoft.error.banned=Ваш акаунт міг бути за
|
||||
Ви можете натиснути <a href="https://enforcement.xbox.com/enforcement/showenforcementhistory">тут</a>, щоб перевірити статус блокування вашого акаунта.
|
||||
account.methods.microsoft.error.unknown=Не вдалося увійти, код помилки: %d.
|
||||
account.methods.microsoft.error.wrong_verify_method=Не вдалося увійти. Будь ласка, спробуйте увійти у свій акаунт, використовуючи ПАРОЛЬ, замість інших методів входу.
|
||||
account.methods.microsoft.logging_in=Вхід...
|
||||
account.methods.microsoft.makegameidsettings=Створити профіль / Редагувати ім'я профілю
|
||||
account.methods.microsoft.hint=Натисніть кнопку "Увійти", щоб почати додавання вашого акаунта Microsoft.
|
||||
account.methods.microsoft.methods.device=Увійти за допомогою QR-коду
|
||||
account.methods.microsoft.methods.device.hint=Відскануйте QR-код або перейдіть за посиланням <a href="%s">%s</a>, щоб завершити вхід, введіть <b>%s</b> на відкритій сторінці.
|
||||
@@ -128,7 +124,6 @@ account.methods.microsoft.profile=Профіль облікового запис
|
||||
account.methods.microsoft.purchase=Купити Minecraft
|
||||
account.methods.microsoft.snapshot=Ви використовуєте неофіційну збірку HMCL. Будь ласка, завантажте <a href="https://hmcl.huangyuhui.net/download">офіційну збірку</a> для входу.
|
||||
account.methods.microsoft.snapshot.tooltip=Ви використовуєте неофіційну збірку HMCL. Будь ласка, завантажте офіційну збірку для оновлення акаунта.
|
||||
account.methods.microsoft.snapshot.website=Офіційний сайт
|
||||
account.methods.offline=Автономно
|
||||
account.methods.offline.name.special_characters=Використовуйте лише літери, цифри та підкреслення (макс. 16 символів)
|
||||
account.methods.offline.name.invalid=Рекомендується використовувати лише англійські літери, цифри та підкреслення для імені користувача, довжина не повинна перевищувати 16 символів.\n\
|
||||
@@ -191,10 +186,16 @@ addon.dependency.tool=Обов'язкові залежності (Необхід
|
||||
addon.dependency.include=Вбудовані залежності (Вже упаковані у файл доповнення автором. Не потрібно завантажувати окремо)
|
||||
addon.dependency.incompatible=Несумісні доповнення (Одночасне встановлення цих доповнень перешкодить запуску гри)
|
||||
addon.dependency.broken=Зламані залежності (Це доповнення існувало раніше, але його більше немає. Спробуйте використати інше джерело завантаження.)
|
||||
# addon.dependencies.has_broken=
|
||||
addon.download.recommend=Рекомендована версія для Minecraft %1s
|
||||
addon.download.title.release=Minecraft %s
|
||||
addon.download.title.snapshot=Minecraft %s (Снапшоти)
|
||||
addon.modrinth=Modrinth
|
||||
addon.sort.date_created=Дата створення
|
||||
addon.sort.last_updated=Останнє оновлення
|
||||
addon.sort.popularity=Популярність
|
||||
# addon.sort.relevancy=
|
||||
addon.sort.total_downloads=Всього завантажень
|
||||
|
||||
archive.author=Автор(и)
|
||||
archive.date=Дата публікації
|
||||
@@ -371,13 +372,6 @@ curse.category.6553=Реалістично
|
||||
curse.category.6554=Фентезі
|
||||
curse.category.6555=Ваніла
|
||||
|
||||
curse.sort.author=Автор
|
||||
curse.sort.date_created=Дата створення
|
||||
curse.sort.last_updated=Останнє оновлення
|
||||
curse.sort.name=Назва
|
||||
curse.sort.popularity=Популярність
|
||||
curse.sort.total_downloads=Всього завантажень
|
||||
|
||||
datetime.format=dd.MM.yyyy, HH:mm:ss
|
||||
|
||||
download=Завантажити
|
||||
@@ -446,7 +440,7 @@ fatal.config_loading_failure=Не вдається завантажити фай
|
||||
Для macOS спробуйте помістити HMCL кудись із дозволами, відмінними від "Робочий стіл", "Завантаження" та "Документи", і спробуйте знову.
|
||||
fatal.config_loading_failure.unix=Hello Minecraft! Лаунчер не зміг завантажити файл конфігурації, оскільки він був створений користувачем "%1$s".\n\
|
||||
Відкрийте HMCL як користувач root (не рекомендується) або виконайте наступну команду в терміналі, щоб змінити власника файлу конфігурації на поточного користувача:\n%2$s
|
||||
fatal.config_unsupported_version=Поточний файл конфігурації був створений новішою версією Hello Minecraft! Launcher, і ця версія HMCL не може завантажити його належним чином.\n\
|
||||
fatal.config_unsupported_version=Поточний файл конфігурації був створений новішою версією HMCL, і ця версія HMCL не може завантажити його належним чином.\n\
|
||||
Будь ласка, оновіть та перезапустіть HMCL.\n\
|
||||
До оновлення лаунчера будь-які змінені вами налаштування не будуть збережені.
|
||||
fatal.mac_app_translocation=Hello Minecraft! Лаунчер ізольовано до тимчасового каталогу ОС через механізми безпеки macOS.\n\
|
||||
@@ -459,7 +453,6 @@ fatal.apply_update_failure=Вибачте, але Hello Minecraft! Лаунче
|
||||
fatal.apply_update_need_win7=Hello Minecraft! Лаунчер не може автоматично оновитися на Windows XP/Vista. \n\
|
||||
Ви можете оновити вручну, завантаживши новішу версію лаунчера з %s.
|
||||
fatal.deprecated_java_version=У майбутньому HMCL потребуватиме Java 17 або новішої версії для роботи, але все ще підтримуватиме запуск ігор на Java 6~16.\n\nРекомендується встановити останню версію Java, щоб гарантувати належну роботу HMCL.\n\nВи можете продовжувати зберігати стару версію Java. HMCL може розпізнавати та керувати декількома версіями Java і автоматично вибере відповідну Java для вас на основі версії гри.
|
||||
fatal.deprecated_java_version.update=У майбутньому HMCL потребуватиме Java 17 або новішої версії для роботи. Будь ласка, встановіть останню версію Java, щоб HMCL міг завершити оновлення.\n\nВи можете продовжувати зберігати стару версію Java. HMCL може розпізнавати та керувати декількома версіями Java і автоматично вибере відповідну Java для вас на основі версії гри.
|
||||
fatal.deprecated_java_version.download_link=Завантажити Java %d
|
||||
fatal.samba=Якщо ви відкрили Hello Minecraft! Лаунчер з мережевого диска Samba, деякі функції можуть не працювати. Спробуйте оновити вашу Java або перемістити лаунчер в інший каталог.
|
||||
fatal.illegal_char=Ваш шлях користувача містить недопустимий символ "=". Ви не зможете використовувати authlib-injector або змінювати скін вашого автономного облікового запису.
|
||||
@@ -691,6 +684,7 @@ game_directory.instance_directory.choose=Вибрати каталог гри
|
||||
game_directory.name=Назва
|
||||
game_directory.new=Новий каталог
|
||||
game_directory.use_relative_path=Використовувати відносний шлях для каталогу гри, якщо можливо
|
||||
# game_directory.root=
|
||||
|
||||
help=Допомога
|
||||
help.doc=Документація Hello Minecraft! Лаунчера
|
||||
@@ -703,6 +697,7 @@ input.url=Введення має бути дійсною URL-адресою.
|
||||
|
||||
install=Новий екземпляр
|
||||
install.change_version=Змінити версію
|
||||
# install.change_version.title=
|
||||
install.change_version.confirm=Ви впевнені, що хочете переключити %s з версії %s на %s?
|
||||
install.change_version.process=Процес зміни версії
|
||||
install.failed=Не вдалося встановити
|
||||
@@ -711,18 +706,19 @@ install.failed.downloading.detail=Не вдалося завантажити ф
|
||||
install.failed.downloading.timeout=Час очікування завантаження при отриманні: %s
|
||||
install.failed.install_online=Не вдалося ідентифікувати наданий файл. Якщо ви встановлюєте мод, перейдіть на сторінку "Моди".
|
||||
install.failed.malformed=Завантажені файли пошкоджені. Ви можете спробувати вирішити цю проблему, переключившись на інше джерело завантаження у "Налаштування → Завантаження → Джерело завантаження".
|
||||
# install.failed.cleanroom_not_compatible_with_forge=
|
||||
install.failed.optifine_conflict=Не можна встановити OptiFine та Fabric одночасно на Minecraft 1.13 або новішому.
|
||||
install.failed.optifine_forge_1.17=Для Minecraft 1.17.1 Forge сумісний лише з OptiFine H1 pre2 або новішою. Ви можете встановити їх, позначивши "Снапшоти" при виборі версії OptiFine в HMCL.
|
||||
install.failed.version_mismatch=Цей завантажувач вимагає версію гри %s, але встановлена %s.
|
||||
install.installer.change_version=%s Несумісний
|
||||
install.installer.choose=Виберіть вашу версію %s
|
||||
install.installer.do_not_install=Не встановлювати
|
||||
install.installer.cleanroom=Cleanroom
|
||||
install.installer.do_not_install=Не встановлювати
|
||||
install.installer.fabric=Fabric
|
||||
install.installer.fabric-api=Fabric API
|
||||
install.installer.fabric-quilt-api.warning=%1$s - це мод, і він буде встановлений у каталог модів екземпляра гри. Не змінюйте робочий каталог гри, інакше %1$s не працюватиме. Якщо ви все ж хочете змінити каталог, вам слід перевстановити його.
|
||||
install.installer.legacyfabric=Legacy Fabric
|
||||
install.installer.legacyfabric-api=Legacy Fabric API
|
||||
install.installer.fabric-quilt-api.warning=%1$s - це мод, і він буде встановлений у каталог модів екземпляра гри. Не змінюйте робочий каталог гри, інакше %1$s не працюватиме. Якщо ви все ж хочете змінити каталог, вам слід перевстановити його.
|
||||
install.installer.forge=Forge
|
||||
install.installer.neoforge=NeoForge
|
||||
install.installer.game=Minecraft
|
||||
@@ -779,8 +775,8 @@ instance.manage=Усі екземпляри
|
||||
instance.manage.clean=Видалити файли журналів
|
||||
instance.manage.clean.tooltip=Видалити файли в каталогах "logs" та "crash-reports".
|
||||
instance.manage.duplicate=Дублювати екземпляр
|
||||
instance.manage.duplicate.prompt=Введіть нову назву екземпляра
|
||||
instance.manage.duplicate.duplicate_save=Дублювати світи
|
||||
instance.manage.duplicate.prompt=Введіть нову назву екземпляра
|
||||
instance.manage.duplicate.confirm=Дубльований екземпляр матиме копію всіх файлів у каталозі екземпляра (".minecraft/versions/<назва екземпляра>"), з ізольованим робочим каталогом та налаштуваннями.
|
||||
instance.manage.manage=Редагувати екземпляр
|
||||
instance.manage.manage.title=Редагувати екземпляр - %1s
|
||||
@@ -845,6 +841,9 @@ launch.advice.forge28_2_2_optifine=Forge версії 28.2.2 або новішо
|
||||
launch.advice.forge37_0_60=Forge версій до 37.0.60 несумісний з Java 17. Оновіть Forge до 37.0.60 або новішої, або запустіть гру з Java 16.
|
||||
launch.advice.java8_51_1_13=Minecraft 1.13 може зазнати збою на версіях Java 8 до 1.8.0_51. Встановіть останню версію Java 8.
|
||||
launch.advice.java9=Ви не можете запустити Minecraft 1.12 або раніші з Java 9 або новішою. Використовуйте Java 8.
|
||||
# launch.advice.lwjgl_3_4_1=
|
||||
# launch.advice.lwjgl_3_4_1=Поточна версія гри має відому проблему, яка може спричинити підвисання у грі.\nHMCL може автоматично оптимізувати продуктивність гри (використовуючи Java Agent).\nЧи дозволяєте ви HMCL автоматично оптимізувати гру?\nЯкщо у вас виникнуть проблеми, ви можете вимкнути опцію "Дозволити HMCL змінювати гру" у "Налаштування → Загальні".
|
||||
launch.advice.lwjgl_3_4_1.title=Дозволити HMCL автоматично оптимізувати гру?
|
||||
launch.advice.modded_java=Деякі моди можуть бути несумісні з новішими версіями Java. Рекомендується використовувати Java %s для запуску Minecraft %s.
|
||||
launch.advice.modlauncher8=Версія Forge, яку ви використовуєте, несумісна з поточною версією Java. Спробуйте оновити Forge.
|
||||
launch.advice.newer_java=Ви використовуєте старішу версію Java для запуску гри. Рекомендується оновити до Java 8, інакше деякі моди можуть призвести до збою гри.
|
||||
@@ -853,8 +852,6 @@ launch.advice.require_newer_java_version=Поточна версія гри ви
|
||||
launch.advice.too_large_memory_for_32bit=Ви виділили розмір пам'яті, більший за обмеження пам'яті 32-бітної інсталяції Java. Ви можете не змогти запустити гру.
|
||||
launch.advice.vanilla_linux_java_8=Minecraft 1.12.2 або раніші підтримують лише Java 8 для платформи Linux x86-64, оскільки новіші версії не можуть завантажити 32-бітні нативні бібліотеки, такі як liblwjgl.so. Завантажте її з java.com або встановіть OpenJDK 8.
|
||||
launch.advice.vanilla_x86.translation=Minecraft не повністю підтримується на вашій платформі, тому ви можете зіткнутися з відсутністю функцій або навіть не зможете запустити гру. Ви можете завантажити Java для архітектури <b>x86-64</b> <a href="https://learn.microsoft.com/java/openjdk/download">тут</a> для повного ігрового досвіду.
|
||||
# launch.advice.lwjgl_3_4_1=Поточна версія гри має відому проблему, яка може спричинити підвисання у грі.\nHMCL може автоматично оптимізувати продуктивність гри (використовуючи Java Agent).\nЧи дозволяєте ви HMCL автоматично оптимізувати гру?\nЯкщо у вас виникнуть проблеми, ви можете вимкнути опцію "Дозволити HMCL змінювати гру" у "Налаштування → Загальні".
|
||||
launch.advice.lwjgl_3_4_1.title=Дозволити HMCL автоматично оптимізувати гру?
|
||||
launch.advice.unknown=Гра не може бути запущена з наступних причин:
|
||||
launch.failed=Не вдалося запустити
|
||||
launch.failed.cannot_create_jvm=Ми не можемо створити JVM. Це може бути через неправильні аргументи JVM. Ви можете спробувати вирішити це, видаливши всі аргументи, які ви додали у "Глобальні/Специфічні налаштування екземпляра → Розширені налаштування → Параметри JVM".
|
||||
@@ -900,8 +897,6 @@ launcher.background.load_policy.wait_for_background=Очікувати на тл
|
||||
launcher.background.loading=Завантаження тла
|
||||
launcher.background.network=З URL
|
||||
launcher.background.network.cache=Кешувати зображення з URL-адрес
|
||||
launcher.background.network.cache.disabled=Вимкнено
|
||||
launcher.background.network.cache.enabled=Увімкнено
|
||||
launcher.background.paint=Суцільний колір
|
||||
launcher.background.theme=Слідувати темі
|
||||
launcher.background.theme_color=За кольором теми
|
||||
@@ -983,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=Офіційний сайт
|
||||
@@ -1113,7 +1111,6 @@ modrinth.category.velocity=Velocity
|
||||
modrinth.category.waterfall=Waterfall
|
||||
modrinth.category.worldgen=Генерація світу
|
||||
modrinth.category.8x-=×8 і менше
|
||||
modrinth.category.16x=×16
|
||||
modrinth.category.32x=×32
|
||||
modrinth.category.48x=×48
|
||||
modrinth.category.64x=×64
|
||||
@@ -1291,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=Не вдалося створити каталог
|
||||
@@ -1307,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 підпунктів
|
||||
|
||||
@@ -1369,14 +1371,9 @@ settings.advanced.launcher_visibility.keep=Залишити лаунчер ви
|
||||
settings.advanced.launcher_visible=Видимість лаунчера
|
||||
settings.advanced.minecraft_arguments=Аргументи запуску
|
||||
settings.advanced.minecraft_arguments.prompt=Типово
|
||||
settings.advanced.natives=Нативні бібліотеки
|
||||
settings.advanced.natives_settings=Налаштування нативних бібліотек
|
||||
settings.advanced.natives_directory=Шлях до нативних бібліотек
|
||||
settings.advanced.natives_directory.choose=Виберіть місце розташування бажаних нативних бібліотек
|
||||
settings.advanced.natives_directory.custom=Власний
|
||||
settings.advanced.natives_directory.custom.enabled=Використовувати власні нативні бібліотеки
|
||||
settings.advanced.natives_directory.default=Типово
|
||||
settings.advanced.natives_directory.default.version_id=<Назва екземпляра>
|
||||
settings.advanced.no_jvm_args=Не додавати типові аргументи JVM
|
||||
settings.advanced.no_optimizing_jvm_args=Не додавати типові аргументи оптимізації JVM
|
||||
settings.advanced.precall_command=Команда перед запуском
|
||||
@@ -1443,6 +1440,7 @@ settings.advanced.server_ip.prompt=Автоматично приєднатися
|
||||
settings.advanced.unsupported_system_options=Налаштування, що не застосовуються до поточної системи
|
||||
settings.advanced.use_native_glfw_or_sdl=Використовувати системний GLFW / SDL
|
||||
settings.advanced.use_native_openal=Використовувати системний OpenAL
|
||||
# settings.advanced.use_native_sdl=
|
||||
settings.advanced.linux_freebsd_only=Лише Linux/FreeBSD
|
||||
settings.advanced.windows_only=Тільки для Windows
|
||||
settings.advanced.wrapper_launcher=Команда обгортки
|
||||
@@ -1491,9 +1489,7 @@ settings.game.window_type=Тип вікна гри
|
||||
settings.game.window_type.fullscreen=На весь екран
|
||||
settings.game.window_type.maximized=Розгорнуте
|
||||
settings.game.window_type.windowed=Віконне
|
||||
settings.game.working_directory=Робочий каталог
|
||||
settings.game.working_directory.choose=Вибрати робочий каталог
|
||||
settings.game.working_directory.hint=Увімкніть опцію "Ізольовано" в "Робоча директорія", щоб дозволити поточному екземпляру зберігати свої налаштування, світи та моди в окремій директорії.\n\ \n\ Рекомендується увімкнути цю опцію, щоб уникнути конфліктів модів, але вам доведеться перенести ваші світи вручну.
|
||||
|
||||
settings.icon=Іконка
|
||||
settings.game_directories.read_only=Файл директорії гри був збережений новішою версією HMCL. Поточна версія HMCL не може змінювати директорії гри.
|
||||
@@ -1507,7 +1503,6 @@ settings.launcher.brightness=Режим теми
|
||||
settings.launcher.brightness.auto=Слідувати системним налаштуванням
|
||||
settings.launcher.brightness.dark=Темний режим
|
||||
settings.launcher.brightness.light=Світлий режим
|
||||
settings.launcher.common_path.tooltip=HMCL розмістить усі ігрові ресурси та залежності тут. Якщо в каталозі гри вже є існуючі бібліотеки, тоді HMCL спочатку надасть перевагу їх використанню.
|
||||
settings.launcher.debug=Налагодження
|
||||
settings.launcher.disable_april_fools=Не вмикати першоквітневі функції
|
||||
settings.launcher.disable_auto_game_options=Не змінювати мову гри
|
||||
@@ -1746,6 +1741,7 @@ update.channel.nightly.hint=Ви зараз використовуєте збі
|
||||
update.channel.nightly.title=Повідомлення каналу Нічний
|
||||
update.channel.stable=Реліз
|
||||
update.checking=Перевірка оновлень
|
||||
# update.check_failed=
|
||||
update.disable_auto_show_update_dialog=Не показувати діалогове вікно оновлення автоматично
|
||||
update.disable_auto_show_update_dialog.subtitle=Увімкніть цю опцію, щоб HMCL автоматично не показував діалогове вікно оновлення.
|
||||
update.failed=Не вдалося оновити
|
||||
@@ -1759,6 +1755,7 @@ update.latest=Це остання версія
|
||||
update.tooltip=Оновити
|
||||
update.preview=Ранній попередній перегляд випусків HMCL
|
||||
update.preview.subtitle=Увімкніть цю опцію, щоб отримувати нові версії HMCL раніше для тестування перед їх офіційним випуском.
|
||||
# update.unverified=
|
||||
|
||||
warning.java_interpreted_mode=HMCL працює в інтерпретованому середовищі Java, що значно вплине на продуктивність.\n\ \n\ Для найкращого досвіду ми рекомендуємо використовувати Java з підтримкою JIT для запуску HMCL.
|
||||
warning.software_rendering=Наразі HMCL використовує програмний рендеринг, що значно вплине на продуктивність.
|
||||
|
||||
+76
-66
@@ -190,6 +190,11 @@ addon.download.recommend=推荐版本 - Minecraft %1s
|
||||
addon.download.title.release=Minecraft %s
|
||||
addon.download.title.snapshot=Minecraft %s (快照)
|
||||
addon.modrinth=Modrinth
|
||||
addon.sort.date_created=创建日期
|
||||
addon.sort.last_updated=最近更新
|
||||
addon.sort.popularity=热度
|
||||
addon.sort.relevancy=相关度
|
||||
addon.sort.total_downloads=下载量
|
||||
|
||||
archive.author=作者
|
||||
archive.date=发布日期
|
||||
@@ -209,8 +214,8 @@ button.delete=删除
|
||||
button.disable=禁用
|
||||
button.do_not_show_again=不再显示
|
||||
button.edit=修改
|
||||
button.install=安装
|
||||
button.enable=启用
|
||||
button.install=安装
|
||||
button.export=导出
|
||||
button.no=否
|
||||
button.ok=确定
|
||||
@@ -226,20 +231,20 @@ button.select_all=全选
|
||||
button.view=查看
|
||||
button.yes=是
|
||||
|
||||
color.recent=推荐
|
||||
color.custom=自定义颜色
|
||||
|
||||
contact=反馈
|
||||
contact.chat=官方群组
|
||||
contact.chat.discord=Discord
|
||||
contact.chat.discord.statement=欢迎加入 Discord 服务器,加入后请遵守讨论区规定
|
||||
contact.chat.qq_group=用户 QQ 群
|
||||
contact.chat.qq_group.statement=欢迎加入 HMCL 官方 QQ 群,加入后请遵守群规
|
||||
contact.feedback=提交反馈
|
||||
contact.feedback.github=GitHub Issue
|
||||
contact.feedback.github.statement=提交一个 GitHub Issue
|
||||
contact.chat=官方群组
|
||||
contact.chat.qq_group=用户 QQ 群
|
||||
contact.chat.qq_group.statement=欢迎加入 HMCL 官方 QQ 群,加入后请遵守群规
|
||||
contact.chat.discord=Discord
|
||||
contact.chat.discord.statement=欢迎加入 Discord 服务器,加入后请遵守讨论区规定
|
||||
|
||||
crash.NoClassDefFound=请确认 Hello Minecraft! Launcher 本体是否完整,或更新你的 Java。\n你可以访问 https://docs.hmcl.net/help.html 页面寻求帮助。
|
||||
color.recent=推荐
|
||||
color.custom=自定义颜色
|
||||
|
||||
crash.NoClassDefFound=请确认 HMCL 本体是否完整,或更新你的 Java。\n你可以访问 https://docs.hmcl.net/help.html 页面寻求帮助。
|
||||
crash.user_fault=你的系统或 Java 环境可能安装不当导致本软件崩溃,请检查你的 Java 环境或你的电脑。\n你可以访问 https://docs.hmcl.net/help.html 页面寻求帮助。
|
||||
|
||||
curse.category.0=全部
|
||||
@@ -270,14 +275,14 @@ curse.category.5299=教育
|
||||
curse.category.5232=额外行星
|
||||
curse.category.5129=原版增强
|
||||
curse.category.5189=实用与 QOL
|
||||
curse.category.5190=QOL
|
||||
curse.category.5191=实用与 QOL
|
||||
curse.category.5192=梦幻菜单
|
||||
curse.category.6145=空岛
|
||||
curse.category.6814=性能
|
||||
curse.category.6954=动态联合/集成动力 (Integrated Dynamics)
|
||||
curse.category.6484=机械动力 (Create)
|
||||
curse.category.6821=错误修复
|
||||
curse.category.6145=空岛
|
||||
curse.category.5190=QOL
|
||||
curse.category.5191=实用与 QOL
|
||||
curse.category.5192=梦幻菜单
|
||||
curse.category.423=信息展示
|
||||
curse.category.426=模组扩展
|
||||
curse.category.434=装备武器
|
||||
@@ -288,10 +293,10 @@ curse.category.429=工业 (Industrialcraft)
|
||||
curse.category.419=魔法
|
||||
curse.category.412=科技
|
||||
curse.category.4557=红石
|
||||
curse.category.428=匠魂
|
||||
curse.category.8937=ModJam 2025
|
||||
curse.category.9026=创造模式
|
||||
curse.category.10775=恐怖
|
||||
curse.category.428=匠魂
|
||||
curse.category.414=交通运输
|
||||
curse.category.4486=幸运方块 (Lucky Blocks)
|
||||
curse.category.432=建筑 (Buildcraft)
|
||||
@@ -371,13 +376,6 @@ curse.category.6553=写实
|
||||
curse.category.6554=幻想
|
||||
curse.category.6555=原生
|
||||
|
||||
curse.sort.author=作者
|
||||
curse.sort.date_created=创建日期
|
||||
curse.sort.last_updated=最近更新
|
||||
curse.sort.name=名称
|
||||
curse.sort.popularity=热度
|
||||
curse.sort.total_downloads=下载量
|
||||
|
||||
datetime.format=yyyy 年 MM 月 dd 日 HH:mm:ss
|
||||
|
||||
download=下载
|
||||
@@ -430,27 +428,27 @@ extension.resourcepack=资源包压缩包
|
||||
extension.schematic=原理图文件
|
||||
extension.world=世界压缩包
|
||||
|
||||
fatal.create_hmcl_current_directory_failure=Hello Minecraft! Launcher 无法创建 HMCL 文件夹 (%s),请将 HMCL 移动至其他位置再启动。\n如遇到问题,你可以访问 https://docs.hmcl.net/help.html 页面寻求帮助。
|
||||
fatal.create_hmcl_current_directory_failure=HMCL 无法创建 HMCL 文件夹 (%s),请将 HMCL 移动至其他位置再启动。\n如遇到问题,你可以访问 https://docs.hmcl.net/help.html 页面寻求帮助。
|
||||
fatal.javafx.incomplete=JavaFX 运行环境不完整。请尝试更换你的 Java 或者重新安装 OpenJFX。
|
||||
fatal.javafx.missing=缺少 JavaFX 运行环境。请使用包含 OpenJFX 的 Java 运行环境启动 Hello Minecraft! Launcher。\n你可以访问 https://docs.hmcl.net/help.html 页面寻求帮助。
|
||||
fatal.config_change_owner_root=你正在使用 root 账户启动 Hello Minecraft! Launcher,这可能导致你未来无法正常使用其他账户正常启动 HMCL。\n你可以访问 https://docs.hmcl.net/help.html 页面寻求帮助。\n是否继续启动?
|
||||
fatal.config_in_temp_dir=你正在临时文件夹中启动 Hello Minecraft! Launcher,你的设置和游戏数据可能会丢失。建议将 HMCL 移动至其他位置再启动。\n你可以访问 https://docs.hmcl.net/help.html 页面寻求帮助。\n是否继续启动?
|
||||
fatal.config_loading_failure=Hello Minecraft! Launcher 无法加载配置文件。\n请确保 HMCL 对“%s”文件夹及该文件夹下的文件拥有读写权限。\n对于 macOS,尝试将 HMCL 放在除“桌面”“下载”“文稿”之外的有权限的地方再试。\n你可以访问 https://docs.hmcl.net/help.html 页面寻求帮助。
|
||||
fatal.config_loading_failure.unix=Hello Minecraft! Launcher 无法加载配置文件,因为配置文件是由用户“%1$s”创建的。\n请使用 root 账户启动 HMCL (不推荐),或在终端中执行以下命令将配置文件的所有权变更为当前用户:\n%2$s\n你可以访问 https://docs.hmcl.net/help.html 页面寻求帮助。
|
||||
fatal.config_unsupported_version=当前配置文件是由更高版本的 Hello Minecraft! Launcher 创建的,当前版本的 HMCL 无法正常加载。请更新并重启 HMCL。\n在更新启动器之前,你修改的所有设置都不会被保存。\n如果遇到问题,你可以点击右上角帮助按钮进行求助。
|
||||
fatal.mac_app_translocation=由于 macOS 的安全机制,Hello Minecraft! Launcher 被系统隔离至临时文件夹中。\n请将 HMCL 移动到其他文件夹后再尝试启动,否则你的设置和游戏数据可能会在重启后丢失。\n你可以访问 https://docs.hmcl.net/help.html 页面寻求帮助。\n是否继续启动?
|
||||
fatal.migration_requires_manual_reboot=Hello Minecraft! Launcher 即将完成升级,请重新打开 HMCL。\n如遇到问题,你可以访问 https://docs.hmcl.net/help.html 页面寻求帮助。
|
||||
fatal.apply_update_failure=我们很抱歉 Hello Minecraft! Launcher 无法自动完成升级,因为出现了一些问题。\n但你依可以从 %s 手动下载 HMCL 来完成升级。\n你可以访问 https://docs.hmcl.net/help.html 页面寻求帮助。
|
||||
fatal.apply_update_need_win7=Hello Minecraft! Launcher 无法在 Windows XP/Vista 上进行自动更新。请从 %s 手动下载 HMCL 来完成升级。
|
||||
fatal.javafx.missing=缺少 JavaFX 运行环境。请使用包含 OpenJFX 的 Java 运行环境启动 HMCL。\n你可以访问 https://docs.hmcl.net/help.html 页面寻求帮助。
|
||||
fatal.config_change_owner_root=你正在使用 root 账户启动 HMCL,这可能导致你未来无法正常使用其他账户正常启动 HMCL。\n你可以访问 https://docs.hmcl.net/help.html 页面寻求帮助。\n是否继续启动?
|
||||
fatal.config_in_temp_dir=你正在临时文件夹中启动 HMCL,你的设置和游戏数据可能会丢失。建议将 HMCL 移动至其他位置再启动。\n你可以访问 https://docs.hmcl.net/help.html 页面寻求帮助。\n是否继续启动?
|
||||
fatal.config_loading_failure=HMCL 无法加载配置文件。\n请确保 HMCL 对“%s”文件夹及该文件夹下的文件拥有读写权限。\n对于 macOS,尝试将 HMCL 放在除“桌面”“下载”“文稿”之外的有权限的地方再试。\n你可以访问 https://docs.hmcl.net/help.html 页面寻求帮助。
|
||||
fatal.config_loading_failure.unix=HMCL 无法加载配置文件,因为配置文件是由用户“%1$s”创建的。\n请使用 root 账户启动 HMCL (不推荐),或在终端中执行以下命令将配置文件的所有权变更为当前用户:\n%2$s\n你可以访问 https://docs.hmcl.net/help.html 页面寻求帮助。
|
||||
fatal.config_unsupported_version=当前配置文件是由更高版本的 HMCL 创建的,当前版本的 HMCL 无法正常加载。请更新并重启 HMCL。\n在更新启动器之前,你修改的所有设置都不会被保存。\n如果遇到问题,你可以点击右上角帮助按钮进行求助。
|
||||
fatal.mac_app_translocation=由于 macOS 的安全机制,HMCL 被系统隔离至临时文件夹中。\n请将 HMCL 移动到其他文件夹后再尝试启动,否则你的设置和游戏数据可能会在重启后丢失。\n你可以访问 https://docs.hmcl.net/help.html 页面寻求帮助。\n是否继续启动?
|
||||
fatal.migration_requires_manual_reboot=HMCL 即将完成升级,请重新打开 HMCL。\n如遇到问题,你可以访问 https://docs.hmcl.net/help.html 页面寻求帮助。
|
||||
fatal.apply_update_failure=我们很抱歉 HMCL 无法自动完成升级,因为出现了一些问题。\n但你依可以从 %s 手动下载 HMCL 来完成升级。\n你可以访问 https://docs.hmcl.net/help.html 页面寻求帮助。
|
||||
fatal.apply_update_need_win7=HMCL 无法在 Windows XP/Vista 上进行自动更新。请从 %s 手动下载 HMCL 来完成升级。
|
||||
fatal.deprecated_java_version=HMCL 未来需要 Java 17 或更高版本才能运行,但依然支持使用 Java 6~16 启动游戏。建议安装最新版本的 Java 以确保 HMCL 能正常工作。\n你可以继续保留旧版本 Java。HMCL 能够识别与管理多个 Java,并会自动根据游戏版本为你选择合适的 Java。\n你可以访问 https://docs.hmcl.net/help.html 页面寻求帮助。
|
||||
fatal.deprecated_java_version.download_link=下载 Java %d
|
||||
fatal.samba=如果你正在通过 Samba 共享的文件夹中运行 Hello Minecraft! Launcher,启动器可能无法正常工作。请尝试更新你的 Java 或在本地文件夹内运行 HMCL。\n你可以访问 https://docs.hmcl.net/help.html 页面寻求帮助。
|
||||
fatal.samba=如果你正在通过 Samba 共享的文件夹中运行 HMCL,启动器可能无法正常工作。请尝试更新你的 Java 或在本地文件夹内运行 HMCL。\n你可以访问 https://docs.hmcl.net/help.html 页面寻求帮助。
|
||||
fatal.illegal_char=由于你的运行路径中存在非法字符“=”,你将无法使用外置登录账户以及离线登录更换皮肤功能。\n你可以访问 https://docs.hmcl.net/help.html 页面寻求帮助。
|
||||
fatal.unsupported_platform=Minecraft 尚未对你的平台提供完善支持,所以可能影响游戏体验或无法启动游戏。\n\
|
||||
若无法启动 Minecraft 1.17 及更高版本,可以尝试在“(全局/实例特定) 游戏设置 → 图形设置”中将“渲染器”切换为“Mesa LLVMpipe” / “Mesa Lavapipe”,以获得更好的兼容性。\n\
|
||||
如遇到问题,你可以点击右上角帮助按钮进行求助。
|
||||
fatal.unsupported_platform.loongarch=Hello Minecraft! Launcher 已为龙芯提供支持。\n如果遇到问题,你可以点击右上角帮助按钮进行求助。
|
||||
fatal.unsupported_platform.windows_arm64=Hello Minecraft! Launcher 已为 Windows on Arm 平台提供原生支持。如果你在游戏中遇到问题,请尝试使用 x86 架构的 Java 启动游戏。\n如果你正在使用 <b>高通</b> 平台,你可能需要安装 <a href="ms-windows-store://pdp/?productid=9NQPSL29BFFF">OpenGL 兼容包</a> 后才能进行游戏。点击链接前往 Microsoft Store 安装兼容包。\n如遇到问题,你可以点击右上角帮助按钮进行求助。
|
||||
fatal.unsupported_platform.loongarch=HMCL 已为龙芯提供支持。\n如果遇到问题,你可以点击右上角帮助按钮进行求助。
|
||||
fatal.unsupported_platform.windows_arm64=HMCL 已为 Windows on Arm 平台提供原生支持。如果你在游戏中遇到问题,请尝试使用 x86 架构的 Java 启动游戏。\n如果你正在使用 <b>高通</b> 平台,你可能需要安装 <a href="ms-windows-store://pdp/?productid=9NQPSL29BFFF">OpenGL 兼容包</a> 后才能进行游戏。点击链接前往 Microsoft Store 安装兼容包。\n如遇到问题,你可以点击右上角帮助按钮进行求助。
|
||||
# When translating the following string into non-Latin languages, please retain the English translation
|
||||
fatal.wine_warning=HMCL is running under Wine or one of its distributions.\nAs a cross-platform application, HMCL is best experienced natively on your host OS.\nPress "OK" to continue, but please note that some issues may arise.\n\n您目前正在使用 Wine 或其发行版运行 HMCL。\nHMCL 是跨平台的,我们建议您直接在宿主系统上运行。\n按下“确定”以继续,但请注意,此操作可能会引发一些问题。
|
||||
|
||||
@@ -473,14 +471,21 @@ game.crash.feedback=<b>请不要将本界面截图或拍照给他人!</b>如
|
||||
game.crash.info=游戏信息
|
||||
game.crash.reason=崩溃原因
|
||||
game.crash.reason.analyzing=分析中……
|
||||
game.crash.reason.multiple=检测到多个原因:\n\n
|
||||
game.crash.reason.block=当前游戏由于某个方块不能正常工作,无法继续运行。\n你可以尝试使用地图编辑工具编辑世界删除该方块,或者直接删除对应的模组。\n方块类型:%1$s\n方块坐标:%2$s\n\n相关工具:<a href="https://minecraft.wiki/w/Tutorial:Programs_and_editors/Mapping#Map_editors">英文 Wiki</a> | <a href="https://zh.minecraft.wiki/w/辅助程序与编辑器/地图工具?variant=zh-cn#地图编辑工具">中文 Wiki</a>
|
||||
game.crash.reason.bootstrap_failed=当前游戏由于模组“%1$s”出现问题,无法继续运行。\n你可以尝试删除或更新该模组以解决问题。
|
||||
game.crash.reason.mixin_apply_mod_failed=当前游戏由于 Mixin 无法应用于“%1$s”模组,无法继续运行。\n你可以尝试删除或更新该模组以解决问题。
|
||||
game.crash.reason.config=当前游戏由于无法解析模组配置文件,无法继续运行。\n无法解析模组“%1$s”的配置文件“%2$s”。
|
||||
game.crash.reason.multiple=检测到多个原因:\n\n
|
||||
game.crash.reason.debug_crash=当前游戏由于手动触发崩溃,无法继续运行。\n事实上游戏并没有问题,问题都是你造成的!
|
||||
game.crash.reason.duplicated_mod=当前游戏由于模组“%1$s”重复安装,无法继续运行。\n%2$s\n每种模组只能安装一个,请你删除多余的模组再试。
|
||||
game.crash.reason.entity=当前游戏由于某个实体不能正常工作,无法继续运行。\n你可以尝试使用地图编辑工具编辑世界删除该实体,或者直接删除对应的模组。\n实体类型:%1$s\n实体坐标:%2$s\n\n相关工具:<a href="https://minecraft.wiki/w/Tutorial:Programs_and_editors/Mapping#Map_editors">英文 Wiki</a> | <a href="https://zh.minecraft.wiki/w/辅助程序与编辑器/地图工具?variant=zh-cn#地图编辑工具">中文 Wiki</a>
|
||||
game.crash.reason.modmixin_failure=当前游戏由于某些模组注入失败,无法继续运行。\n这一般代表着该模组存在问题,或与当前环境不兼容。\n你可以查看日志寻找出错模组。
|
||||
game.crash.reason.forge_error=Forge/NeoForge 可能已经提供了错误信息。\n你可以查看日志,并根据错误报告中的日志信息进行对应处理。\n如果没有看到报错信息,可以查看错误报告了解错误具体是如何发生的。\n%1$s
|
||||
game.crash.reason.mod_resolution0=当前游戏由于一些模组出现问题,无法继续运行。\n你可以查看日志寻找出错模组。
|
||||
game.crash.reason.mixin_apply_mod_failed=当前游戏由于 Mixin 无法应用于“%1$s”模组,无法继续运行。\n你可以尝试删除或更新该模组以解决问题。
|
||||
game.crash.reason.java_version_is_too_high=当前游戏由于 Java 版本过高,无法继续运行。\n请在“(全局/实例特定) 游戏设置 → 游戏 Java”中改用较低版本的 Java,然后再启动游戏。\n如果没有,可以从 <a href="https://www.java.com/download/">java.com (Java 8)</a> 或 <a href="https://bell-sw.com/pages/downloads/#downloads">BellSoft Liberica Full JRE (Java 17)</a> 等平台下载、安装一个 (安装完后需重启启动器)。
|
||||
game.crash.reason.need_jdk11=当前游戏由于 Java 版本不合适,无法继续运行。\n你需要下载安装 <a href="https://bell-sw.com/pages/downloads/#downloads">Java 11</a>,并在“(全局/实例特定) 游戏设置 → 游戏 Java”中将 Java 设置为 11.x 的版本。
|
||||
game.crash.reason.mod_name=当前游戏由于模组文件名称问题,无法继续运行。\n模组文件名称应只使用半角的大小写字母 (Aa~Zz)、数字 (0~9)、横线 (-)、下划线 (_)和点 (.)。\n请到模组文件夹中将所有不合规的模组文件名修改为上述合规字符。
|
||||
game.crash.reason.incomplete_forge_installation=当前游戏由于 Forge/NeoForge 安装不完整,无法继续运行。\n请在“实例管理 → 自动安装”中卸载 Forge 并重新安装。
|
||||
game.crash.reason.fabric_version_0_12=Fabric Loader 0.12 及更高版本与当前已经安装的模组可能不兼容,你需要将 Fabric Loader 降级至 0.11.7。
|
||||
game.crash.reason.fabric_warnings=Fabric 提供了一些警告信息:\n%1$s
|
||||
game.crash.reason.file_already_exists=当前游戏由于文件“%1$s”已经存在,无法继续运行。\n如果你认为这个文件可以删除,你可以在备份这个文件后尝试删除它,并重新启动游戏。
|
||||
@@ -494,38 +499,31 @@ game.crash.reason.graphics_driver=当前游戏由于显卡驱动问题而崩溃
|
||||
game.crash.reason.macos_failed_to_find_service_port_for_display=当前游戏由于 Apple Silicon 平台下初始化 OpenGL 窗口失败,无法继续运行。\n对于该问题,HMCL 暂无直接性的解决方案。请你尝试任意打开一个浏览器并全屏,然后再回到 HMCL 启动游戏。在弹出游戏窗口前<b>迅速切回浏览器页面</b>,等待游戏窗口出现后再切回游戏窗口。
|
||||
game.crash.reason.illegal_access_error=当前游戏由于某些模组的问题,无法继续运行。\n如果你认识“%1$s”,你可以更新或删除对应模组再试。
|
||||
game.crash.reason.install_mixinbootstrap=当前游戏由于缺失 MixinBootstrap,无法继续运行。\n你可以尝试安装 <a href="https://www.curseforge.com/minecraft/mc-mods/mixinbootstrap">MixinBootstrap</a> 解决该问题。若安装后崩溃,可以尝试在该模组的文件名开头添加半角感叹号 (!) 解决。
|
||||
game.crash.reason.need_jdk11=当前游戏由于 Java 版本不合适,无法继续运行。\n你需要下载安装 <a href="https://bell-sw.com/pages/downloads/#downloads">Java 11</a>,并在“(全局/实例特定) 游戏设置 → 游戏 Java”中将 Java 设置为 11.x 的版本。
|
||||
game.crash.reason.optifine_is_not_compatible_with_forge=当前游戏由于 OptiFine 与当前版本的 Forge 不兼容,导致游戏崩溃。\n点击 <a href="https://zkitefly.github.io/optifine-forge-support-list">此处</a> 查看 OptiFine 所兼容的 Forge 版本,并严格按照对应版本重新安装游戏或在“实例管理 → 自动安装”中更换版本。\n经测试,Forge 版本过高或过低都可能导致崩溃。
|
||||
game.crash.reason.mod_files_are_decompressed=当前游戏由于模组文件被解压了,无法继续运行。\n请直接把整个模组文件放进模组文件夹中即可。\n解压模组文件会导致游戏出错。请删除模组文件夹中已被解压的模组,然后再启动游戏。
|
||||
game.crash.reason.shaders_mod=当前游戏由于同时安装了 OptiFine 和 Shaders 模组,无法继续运行。\n由于 OptiFine 已集成 Shaders 模组的功能,所以只需删除 Shaders 模组即可。
|
||||
game.crash.reason.rtss_forest_sodium=当前游戏由于 RivaTuner Statistics Server (RTSS) 与 Sodium 不兼容,导致游戏崩溃。\n点击 <a href="https://github.com/CaffeineMC/sodium-fabric/wiki/Known-Issues#rtss-incompatible">此处</a> 查看详情。
|
||||
game.crash.reason.too_many_mods_lead_to_exceeding_the_id_limit=当前游戏由于你所安装的模组过多,超出了游戏的 ID 限制,无法继续运行。\n请尝试安装 <a href="https://www.curseforge.com/minecraft/mc-mods/jeid">JEID</a> 等修复模组,或删除部分大型模组。
|
||||
game.crash.reason.night_config_fixes=当前游戏由于 Night Config 库的一些问题,无法继续运行。\n你可以尝试安装 <a href="https://www.curseforge.com/minecraft/mc-mods/night-config-fixes">Night Config Fixes</a> 模组,这或许能帮助你解决这个问题。\n更多详情,可访问该模组的 <a href="https://github.com/Fuzss/nightconfigfixes">GitHub 仓库</a>。
|
||||
game.crash.reason.optifine_causes_the_world_to_fail_to_load=当前游戏可能由于 OptiFine 而无法继续运行。\n该问题只在特定 OptiFine 版本中出现,你可以尝试在“实例管理 → 自动安装”中更换 OptiFine 的版本。
|
||||
game.crash.reason.jdk_9=当前游戏由于 Java 版本过高,无法继续运行。\n你需要下载安装 <a href="https://bell-sw.com/pages/downloads/#downloads">Java 8</a>,并在“(全局/实例特定) 游戏设置 → 游戏 Java”中将 Java 设置为 1.8.x 的版本。
|
||||
game.crash.reason.jvm_32bit=当前游戏由于内存分配过大,超过了 32 位 Java 虚拟机内存限制,无法继续运行。\n如果你的电脑是 64 位系统,请下载安装并更换 64 位 Java。<a href="https://bell-sw.com/pages/downloads/#downloads">下载 Java</a>\n如果你的电脑是 32 位系统,你或许可以重新安装 64 位系统,或换一台新电脑。\n或者,你可以在“(全局/实例特定) 游戏设置 → 游戏内存”中关闭“自动分配内存”,并把内存分配值调整为 1024 MiB 或以下。
|
||||
game.crash.reason.loading_crashed_forge=当前游戏由于模组“%1$s (%2$s)”错误,无法继续运行。\n你可以尝试删除或更新该模组以解决问题。
|
||||
game.crash.reason.loading_crashed_fabric=当前游戏由于模组“%1$s”错误,无法继续运行。\n你可以尝试删除或更新该模组以解决问题。
|
||||
game.crash.reason.memory_exceeded=当前游戏由于内存分配过大,无法继续运行。\n该问题是由于系统页面文件太小导致的。\n你需要在“(全局/实例特定) 游戏设置”中关闭“自动分配内存”,并将内存分配值调低至游戏能正常启动为止。\n你还可以尝试将虚拟内存设置调整为“自动管理所有驱动器分页文件大小”,<a href="https://docs.hmcl.net/assets/img/hmcl/自动管理所有驱动器分页文件大小.webp">详情</a>。
|
||||
game.crash.reason.mac_jdk_8u261=当前游戏由于你所使用的 Forge/OptiFine 与 Java 冲突而崩溃。\n请尝试更新 Forge/OptiFine,或使用 Java 8u251 及更早版本启动。
|
||||
game.crash.reason.forge_repeat_installation=当前游戏由于 Forge 重复安装,无法继续运行。<a href="https://github.com/HMCL-dev/HMCL/issues/1880">此为已知问题</a>\n建议将日志上传并反馈至 GitHub,以便我们找到更多线索并修复此问题。\n目前你可以在“实例管理 → 自动安装”中卸载 Forge 并重新安装。
|
||||
game.crash.reason.optifine_repeat_installation=当前游戏由于 OptiFine 重复安装,无法继续运行。\n请删除模组文件夹下的 OptiFine 或前往“实例管理 → 自动安装”卸载安装的 OptiFine。
|
||||
game.crash.reason.memory_exceeded=当前游戏由于内存分配过大,无法继续运行。\n该问题是由于系统页面文件太小导致的。\n你需要在“(全局/实例特定) 游戏设置”中关闭“自动分配内存”,并将内存分配值调低至游戏能正常启动为止。\n你还可以尝试将虚拟内存设置调整为“自动管理所有驱动器分页文件大小”,<a href="https://docs.hmcl.net/assets/img/hmcl/自动管理所有驱动器分页文件大小.webp">详情</a>。
|
||||
game.crash.reason.mod=当前游戏由于“%1$s”的问题,无法继续运行。\n你可以更新或删除已经安装的“%1$s”再试。
|
||||
game.crash.reason.mod_resolution=当前游戏由于模组依赖问题,无法继续运行。Fabric 提供了如下信息:\n%1$s
|
||||
game.crash.reason.forgemod_resolution=当前游戏由于模组依赖问题,无法继续运行。Forge/NeoForge 提供了如下信息:\n%1$s
|
||||
game.crash.reason.forge_found_duplicate_mods=当前游戏由于模组重复安装,无法继续运行。Forge/NeoForge 提供了如下信息:\n%1$s
|
||||
game.crash.reason.mod_resolution_collection=当前游戏由于前置模组版本不匹配,无法继续运行。\n“%1$s”需要前置模组“%2$s”才能继续运行。\n这表示你需要更新或降级前置模组。你可以前往“下载 → 模组”页面或网上下载“%3$s”。
|
||||
game.crash.reason.mod_resolution_conflict=当前游戏由于模组冲突,无法继续运行。\n“%1$s”与“%2$s”不兼容。
|
||||
game.crash.reason.mod_resolution_missing=当前游戏由于缺少前置模组,无法继续运行。\n“%1$s”需要前置模组“%2$s”才能继续运行。\n这表示有一些必需的模组没有安装,或该模组版本不匹配。你可以前往“下载 → 模组”页面或网上下载“%3$s”。
|
||||
game.crash.reason.mod_resolution_missing_minecraft=当前游戏由于模组和 Minecraft 游戏版本不匹配,无法继续运行。\n“%1$s”需要 Minecraft %2$s 才能运行。\n如果你要继续使用你已经安装的模组,你可以选择安装对应的 Minecraft 版本。如果你要继续使用当前 Minecraft 版本,你需要安装对应版本的模组。
|
||||
game.crash.reason.mod_resolution_mod_version=%1$s (版本号 %2$s)
|
||||
game.crash.reason.mod_resolution_mod_version.any=%1$s (任意版本)
|
||||
game.crash.reason.forge_repeat_installation=当前游戏由于 Forge 重复安装,无法继续运行。<a href="https://github.com/HMCL-dev/HMCL/issues/1880">此为已知问题</a>\n建议将日志上传并反馈至 GitHub,以便我们找到更多线索并修复此问题。\n目前你可以在“实例管理 → 自动安装”中卸载 Forge 并重新安装。
|
||||
game.crash.reason.optifine_repeat_installation=当前游戏由于 OptiFine 重复安装,无法继续运行。\n请删除模组文件夹下的 OptiFine 或前往“实例管理 → 自动安装”卸载安装的 OptiFine。
|
||||
game.crash.reason.forgemod_resolution=当前游戏由于模组依赖问题,无法继续运行。Forge/NeoForge 提供了如下信息:\n%1$s
|
||||
game.crash.reason.forge_found_duplicate_mods=当前游戏由于模组重复安装,无法继续运行。Forge/NeoForge 提供了如下信息:\n%1$s
|
||||
game.crash.reason.modmixin_failure=当前游戏由于某些模组注入失败,无法继续运行。\n这一般代表着该模组存在问题,或与当前环境不兼容。\n你可以查看日志寻找出错模组。
|
||||
game.crash.reason.night_config_fixes=当前游戏由于 Night Config 库的一些问题,无法继续运行。\n你可以尝试安装 <a href="https://www.curseforge.com/minecraft/mc-mods/night-config-fixes">Night Config Fixes</a> 模组,这或许能帮助你解决这个问题。\n更多详情,可访问该模组的 <a href="https://github.com/Fuzss/nightconfigfixes">GitHub 仓库</a>。
|
||||
game.crash.reason.forge_error=Forge/NeoForge 可能已经提供了错误信息。\n你可以查看日志,并根据错误报告中的日志信息进行对应处理。\n如果没有看到报错信息,可以查看错误报告了解错误具体是如何发生的。\n%1$s
|
||||
game.crash.reason.mod_resolution0=当前游戏由于一些模组出现问题,无法继续运行。\n你可以查看日志寻找出错模组。
|
||||
game.crash.reason.java_version_is_too_high=当前游戏由于 Java 版本过高,无法继续运行。\n请在“(全局/实例特定) 游戏设置 → 游戏 Java”中改用较低版本的 Java,然后再启动游戏。\n如果没有,可以从 <a href="https://www.java.com/download/">java.com (Java 8)</a> 或 <a href="https://bell-sw.com/pages/downloads/#downloads">BellSoft Liberica Full JRE (Java 17)</a> 等平台下载、安装一个 (安装完后需重启启动器)。
|
||||
game.crash.reason.mod_name=当前游戏由于模组文件名称问题,无法继续运行。\n模组文件名称应只使用半角的大小写字母 (Aa~Zz)、数字 (0~9)、横线 (-)、下划线 (_)和点 (.)。\n请到模组文件夹中将所有不合规的模组文件名修改为上述合规字符。
|
||||
game.crash.reason.incomplete_forge_installation=当前游戏由于 Forge/NeoForge 安装不完整,无法继续运行。\n请在“实例管理 → 自动安装”中卸载 Forge 并重新安装。
|
||||
game.crash.reason.optifine_is_not_compatible_with_forge=当前游戏由于 OptiFine 与当前版本的 Forge 不兼容,导致游戏崩溃。\n点击 <a href="https://zkitefly.github.io/optifine-forge-support-list">此处</a> 查看 OptiFine 所兼容的 Forge 版本,并严格按照对应版本重新安装游戏或在“实例管理 → 自动安装”中更换版本。\n经测试,Forge 版本过高或过低都可能导致崩溃。
|
||||
game.crash.reason.mod_files_are_decompressed=当前游戏由于模组文件被解压了,无法继续运行。\n请直接把整个模组文件放进模组文件夹中即可。\n解压模组文件会导致游戏出错。请删除模组文件夹中已被解压的模组,然后再启动游戏。
|
||||
game.crash.reason.shaders_mod=当前游戏由于同时安装了 OptiFine 和 Shaders 模组,无法继续运行。\n由于 OptiFine 已集成 Shaders 模组的功能,所以只需删除 Shaders 模组即可。
|
||||
game.crash.reason.rtss_forest_sodium=当前游戏由于 RivaTuner Statistics Server (RTSS) 与 Sodium 不兼容,导致游戏崩溃。\n点击 <a href="https://github.com/CaffeineMC/sodium-fabric/wiki/Known-Issues#rtss-incompatible">此处</a> 查看详情。
|
||||
game.crash.reason.too_many_mods_lead_to_exceeding_the_id_limit=当前游戏由于你所安装的模组过多,超出了游戏的 ID 限制,无法继续运行。\n请尝试安装 <a href="https://www.curseforge.com/minecraft/mc-mods/jeid">JEID</a> 等修复模组,或删除部分大型模组。
|
||||
game.crash.reason.optifine_causes_the_world_to_fail_to_load=当前游戏可能由于 OptiFine 而无法继续运行。\n该问题只在特定 OptiFine 版本中出现,你可以尝试在“实例管理 → 自动安装”中更换 OptiFine 的版本。
|
||||
game.crash.reason.modlauncher_8=当前游戏由于你所使用的 Forge 版本与当前使用的 Java 冲突而崩溃。请尝试更新 Forge 到 36.2.26 或更高版本或换用版本低于 1.8.0.320 的 Java。<a href="https://bell-sw.com/pages/downloads/?version=java-8&package=jre-full">Liberica JDK 8u312+7</a>
|
||||
game.crash.reason.no_class_def_found_error=当前游戏由于代码不完整,无法继续运行。\n你的游戏可能缺失了某个模组,或者某些模组文件不完整,或者模组与游戏的版本不匹配。\n你可能需要重新安装游戏和模组,或寻求他人帮助。\n缺失:\n%1$s
|
||||
game.crash.reason.no_such_method_error=当前游戏由于代码不完整,无法继续运行。\n你的游戏可能缺失了某个模组,或者某些模组文件不完整,或者模组与游戏的版本不匹配。\n你可能需要重新安装游戏和模组,或寻求他人帮助。
|
||||
@@ -553,7 +551,7 @@ game_directory.use_relative_path=若可能,游戏文件夹使用相对路径
|
||||
game_directory.root=使用磁盘根目录作为游戏文件夹可能会导致无法启动游戏,并会在磁盘根目录下创建多个文件(夹)。\n\n确定要继续吗?
|
||||
|
||||
help=帮助
|
||||
help.doc=Hello Minecraft! Launcher 帮助文档
|
||||
help.doc=HMCL 帮助文档
|
||||
help.detail=可查阅数据包、整合包制作指南等内容
|
||||
|
||||
input.email=用户名必须是邮箱
|
||||
@@ -581,9 +579,9 @@ install.installer.choose=选择 %s 版本
|
||||
install.installer.cleanroom=Cleanroom
|
||||
install.installer.do_not_install=不安装
|
||||
install.installer.fabric=Fabric
|
||||
install.installer.fabric-api=Fabric API
|
||||
install.installer.legacyfabric=Legacy Fabric
|
||||
install.installer.legacyfabric-api=Legacy Fabric API
|
||||
install.installer.fabric-api=Fabric API
|
||||
install.installer.fabric-quilt-api.warning=%1$s 是一个模组,将会被安装到新游戏的模组文件夹。请你在安装游戏后不要修改当前游戏的“版本隔离”设置。如果你在之后修改了相关设置,则需要重新安装 %1$s。
|
||||
install.installer.forge=Forge
|
||||
install.installer.neoforge=NeoForge
|
||||
@@ -772,9 +770,9 @@ launcher.cache_directory.default=默认 ("%APPDATA%/.minecraft" 或 "~/.minecraf
|
||||
launcher.cache_directory.disabled=禁用 (总是使用游戏文件夹路径)
|
||||
launcher.cache_directory.invalid=无法创建自定义的缓存文件夹。已经恢复到默认设置。
|
||||
launcher.contact=联系我们
|
||||
launcher.crash=Hello Minecraft! Launcher 遇到了无法处理的错误。请复制下列内容并点击右下角的按钮反馈问题。
|
||||
launcher.crash.java_internal_error=Hello Minecraft! Launcher 由于当前 Java 损坏而无法继续运行。请卸载当前 Java,点击 <a href="https://bell-sw.com/pages/downloads/#downloads">此处</a> 安装合适的 Java 版本。
|
||||
launcher.crash.hmcl_out_dated=Hello Minecraft! Launcher 遇到了无法处理的错误。已检测到你的启动器不是最新版本,请更新后再试。
|
||||
launcher.crash=HMCL 遇到了无法处理的错误。请复制下列内容并点击右下角的按钮反馈问题。
|
||||
launcher.crash.java_internal_error=HMCL 由于当前 Java 损坏而无法继续运行。请卸载当前 Java,点击 <a href="https://bell-sw.com/pages/downloads/#downloads">此处</a> 安装合适的 Java 版本。
|
||||
launcher.crash.hmcl_out_dated=HMCL 遇到了无法处理的错误。已检测到你的启动器不是最新版本,请更新后再试。
|
||||
launcher.update_java=请更新你的 Java。\n你可以访问 https://docs.hmcl.net/help.html 页面寻求帮助。
|
||||
|
||||
libraries.download=下载依赖库
|
||||
@@ -792,6 +790,7 @@ logwindow.export_game_crash_logs=导出游戏崩溃信息
|
||||
logwindow.export_dump=导出游戏运行栈
|
||||
logwindow.export_dump.no_dependency=你的 Java 不包含用于创建游戏运行栈的依赖。请前往 HMCL QQ 群或 Discord 寻求帮助。
|
||||
|
||||
|
||||
message.cancelled=操作被取消
|
||||
message.confirm=提示
|
||||
message.copied=已复制到剪贴板
|
||||
@@ -842,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=官方网站
|
||||
@@ -855,15 +857,15 @@ modpack.type.curse.error=未能完成该整合包所需的依赖下载,请再
|
||||
modpack.type.curse.not_found=部分必需文件已经在网络中被删除并且再也无法下载。请尝试该整合包的最新版本或者安装其他整合包。\n如遇到问题,你可以点击右上角帮助按钮进行求助。
|
||||
modpack.type.manual.warning=该整合包由发布者手动打包,其中可能已经包含启动器。建议尝试解压后使用其自带的启动器运行游戏。\n如遇到问题,你可以点击右上角帮助按钮进行求助。\nHMCL 可以尝试导入该整合包,但不保证可用性。是否继续?
|
||||
modpack.type.mcbbs=MCBBS 整合包 (推荐)
|
||||
modpack.type.mcbbs.export=可以被 Hello Minecraft! Launcher 导入
|
||||
modpack.type.mcbbs.export=可以被 HMCL 导入
|
||||
modpack.type.modrinth=Modrinth
|
||||
modpack.type.modrinth.export=可以被主流第三方启动器导入
|
||||
modpack.type.multimc=MultiMC
|
||||
modpack.type.multimc.export=可以被 Hello Minecraft! Launcher 和 MultiMC 导入
|
||||
modpack.type.multimc.export=可以被 HMCL 和 MultiMC 导入
|
||||
modpack.type.server=服务器自动更新整合包
|
||||
modpack.type.server.export=允许服务器管理员远程更新游戏客户端
|
||||
modpack.type.server.malformed=服务器整合包配置格式错误,请联系服务器管理员解决此问题。\n如遇到问题,你可以点击右上角帮助按钮进行求助。
|
||||
modpack.unsupported=Hello Minecraft! Launcher 不支持该整合包格式
|
||||
modpack.unsupported=HMCL 不支持该整合包格式
|
||||
modpack.update=正在升级整合包
|
||||
modpack.wizard=导出整合包向导
|
||||
modpack.wizard.step.1=基本设置
|
||||
@@ -1143,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=创建文件夹失败
|
||||
@@ -1159,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 个子项
|
||||
|
||||
@@ -1455,6 +1462,7 @@ settings.type.global.preset.auto_name=预设 %d
|
||||
|
||||
shaderpack.download.title=光影下载 - %1s
|
||||
|
||||
|
||||
system.architecture=架构
|
||||
system.operating_system=操作系统
|
||||
|
||||
@@ -1591,6 +1599,7 @@ update.channel.nightly.hint=你正在使用 HMCL 预览版。预览版更新较
|
||||
update.channel.nightly.title=预览版提示
|
||||
update.channel.stable=稳定版
|
||||
update.checking=正在检查更新
|
||||
update.check_failed=检查更新失败 %s
|
||||
update.disable_auto_show_update_dialog=不自动显示更新弹窗
|
||||
update.disable_auto_show_update_dialog.subtitle=启用此选项,HMCL 将不会自动弹出更新弹窗。
|
||||
update.failed=更新失败
|
||||
@@ -1604,6 +1613,7 @@ update.latest=当前版本为最新版本
|
||||
update.tooltip=更新
|
||||
update.preview=提前预览 HMCL 版本
|
||||
update.preview.subtitle=启用此选项,你将可以提前获取 HMCL 的新版本,以便在正式发布前进行测试。
|
||||
update.unverified=更新不可用,因为无法验证此启动器实例完整性
|
||||
|
||||
warning.java_interpreted_mode=HMCL 正在运行在解释器模式的 Java 环境中,性能将会受到很大影响。\n我们建议你使用支持 JIT 的 Java 启动 HMCL 以获取最佳体验。
|
||||
warning.software_rendering=HMCL 正在使用软件渲染,性能将会受到很大影响。\n你可以访问 https://docs.hmcl.net/help.html 页面寻求帮助。
|
||||
+55
-47
@@ -188,6 +188,11 @@ addon.download.recommend=推薦版本 - Minecraft %1s
|
||||
addon.download.title.release=Minecraft %s
|
||||
addon.download.title.snapshot=Minecraft %s (快照)
|
||||
addon.modrinth=Modrinth
|
||||
addon.sort.date_created=建立日期
|
||||
addon.sort.last_updated=最近更新
|
||||
addon.sort.popularity=熱度
|
||||
addon.sort.relevancy=相關度
|
||||
addon.sort.total_downloads=下載量
|
||||
|
||||
archive.author=作者
|
||||
archive.date=發布日期
|
||||
@@ -207,8 +212,8 @@ button.delete=刪除
|
||||
button.disable=停用
|
||||
button.do_not_show_again=不再顯示
|
||||
button.edit=編輯
|
||||
button.install=安裝
|
||||
button.enable=啟用
|
||||
button.install=安裝
|
||||
button.export=匯出
|
||||
button.no=否
|
||||
button.ok=確定
|
||||
@@ -237,7 +242,7 @@ contact.feedback.github.statement=提交一個 GitHub Issue
|
||||
color.recent=建議
|
||||
color.custom=自訂顏色
|
||||
|
||||
crash.NoClassDefFound=請確認 Hello Minecraft! Launcher 本體是否完整,或更新你的 Java。
|
||||
crash.NoClassDefFound=請確認 HMCL 本體是否完整,或更新你的 Java。
|
||||
crash.user_fault=你的系統或 Java 環境可能安裝不當導致本軟體當機,請檢查你的 Java 環境或你的電腦!可以嘗試重新安裝 Java。
|
||||
|
||||
curse.category.0=全部
|
||||
@@ -286,10 +291,10 @@ curse.category.429=工業 (Industrialcraft)
|
||||
curse.category.419=魔法
|
||||
curse.category.412=科技
|
||||
curse.category.4557=紅石
|
||||
curse.category.428=匠魂
|
||||
curse.category.8937=ModJam 2025
|
||||
curse.category.9026=創造模式
|
||||
curse.category.10775=恐怖
|
||||
curse.category.428=匠魂
|
||||
curse.category.414=交通運輸
|
||||
curse.category.4486=幸運方塊 (Lucky Blocks)
|
||||
curse.category.432=建築 (Buildcraft)
|
||||
@@ -369,13 +374,6 @@ curse.category.6553=寫實
|
||||
curse.category.6554=幻想
|
||||
curse.category.6555=原生
|
||||
|
||||
curse.sort.author=作者
|
||||
curse.sort.date_created=建立日期
|
||||
curse.sort.last_updated=最近更新
|
||||
curse.sort.name=名稱
|
||||
curse.sort.popularity=熱度
|
||||
curse.sort.total_downloads=下載量
|
||||
|
||||
datetime.format=yyyy 年 MM 月 dd 日 HH:mm:ss
|
||||
|
||||
download=下載
|
||||
@@ -427,26 +425,26 @@ extension.resourcepack=資源包壓縮檔
|
||||
extension.schematic=原理圖檔案
|
||||
extension.world=世界壓縮檔
|
||||
|
||||
fatal.create_hmcl_current_directory_failure=Hello Minecraft! Launcher 無法建立 HMCL 資料夾 (%s),請將 HMCL 移動至其他位置再開啟。
|
||||
fatal.create_hmcl_current_directory_failure=HMCL 無法建立 HMCL 資料夾 (%s),請將 HMCL 移動至其他位置再開啟。
|
||||
fatal.javafx.incomplete=JavaFX 執行環境不完整。請嘗試更換你的 Java 或者重新安裝 OpenJFX。
|
||||
fatal.javafx.missing=缺少 JavaFX 執行環境。請使用包含 OpenJFX 的 Java 執行環境開啟 Hello Minecraft! Launcher。
|
||||
fatal.config_change_owner_root=你正在使用 root 帳戶開啟 Hello Minecraft! Launcher,這可能導致你未來無法使用其他帳戶正常開啟 HMCL。\n是否繼續開啟?
|
||||
fatal.config_in_temp_dir=你正在暫存目錄中開啟 Hello Minecraft! Launcher,你的設定和遊戲資料可能會遺失。建議將 HMCL 移動至其他位置再開啟。\n是否繼續開啟?
|
||||
fatal.config_loading_failure=Hello Minecraft! Launcher 無法載入設定檔案。\n請確保 HMCL 對「%s」目錄及該目錄下的檔案擁有讀寫權限。
|
||||
fatal.config_loading_failure.unix=Hello Minecraft! Launcher 無法載入設定檔案,因為設定檔案是由使用者「%1$s」建立的。\n請使用 root 帳戶開啟 HMCL (不推薦),或在終端中執行以下指令將設定檔案的所有權變更為目前使用者:\n%2$s
|
||||
fatal.config_unsupported_version=目前設定檔案是由更高版本的 Hello Minecraft! Launcher 建立的,目前版本的 HMCL 無法正常載入。請更新並重新啟動 HMCL。\n在更新啟動器之前,你所做的所有設定更改都不會被儲存。
|
||||
fatal.mac_app_translocation=由於 macOS 的安全機制,Hello Minecraft! Launcher 被系統隔離至暫存目錄中。\n請將 HMCL 移動到其他目錄後再嘗試開啟,否則你的設定和遊戲資料可能會在重啟後遺失。\n是否繼續開啟?
|
||||
fatal.migration_requires_manual_reboot=Hello Minecraft! Launcher 即將升級完成,請重新開啟 HMCL。
|
||||
fatal.apply_update_failure=我們很抱歉 Hello Minecraft! Launcher 無法自動完成升級程式,因為出現了一些問題。\n但你依然可以從 %s 處手動下載 HMCL 來完成升級。
|
||||
fatal.apply_update_need_win7=Hello Minecraft! Launcher 無法在 Windows XP/Vista 上進行自動更新。請從 %s 處手動下載 HMCL 來完成升級。
|
||||
fatal.javafx.missing=缺少 JavaFX 執行環境。請使用包含 OpenJFX 的 Java 執行環境開啟 HMCL。
|
||||
fatal.config_change_owner_root=你正在使用 root 帳戶開啟 HMCL,這可能導致你未來無法使用其他帳戶正常開啟 HMCL。\n是否繼續開啟?
|
||||
fatal.config_in_temp_dir=你正在暫存目錄中開啟 HMCL,你的設定和遊戲資料可能會遺失。建議將 HMCL 移動至其他位置再開啟。\n是否繼續開啟?
|
||||
fatal.config_loading_failure=HMCL 無法載入設定檔案。\n請確保 HMCL 對「%s」目錄及該目錄下的檔案擁有讀寫權限。
|
||||
fatal.config_loading_failure.unix=HMCL 無法載入設定檔案,因為設定檔案是由使用者「%1$s」建立的。\n請使用 root 帳戶開啟 HMCL (不推薦),或在終端中執行以下指令將設定檔案的所有權變更為目前使用者:\n%2$s
|
||||
fatal.config_unsupported_version=目前設定檔案是由更高版本的 HMCL 建立的,目前版本的 HMCL 無法正常載入。請更新並重新啟動 HMCL。\n在更新啟動器之前,你所做的所有設定更改都不會被儲存。
|
||||
fatal.mac_app_translocation=由於 macOS 的安全機制,HMCL 被系統隔離至暫存目錄中。\n請將 HMCL 移動到其他目錄後再嘗試開啟,否則你的設定和遊戲資料可能會在重啟後遺失。\n是否繼續開啟?
|
||||
fatal.migration_requires_manual_reboot=HMCL 即將升級完成,請重新開啟 HMCL。
|
||||
fatal.apply_update_failure=我們很抱歉 HMCL 無法自動完成升級程式,因為出現了一些問題。\n但你依然可以從 %s 處手動下載 HMCL 來完成升級。
|
||||
fatal.apply_update_need_win7=HMCL 無法在 Windows XP/Vista 上進行自動更新。請從 %s 處手動下載 HMCL 來完成升級。
|
||||
fatal.deprecated_java_version=HMCL 未來需要 Java 17 或更高版本才能執行,但依然支援使用 Java 6~16 啟動遊戲。建議安裝最新版本的 Java 以確保 HMCL 正常執行。\n你可以繼續保留舊版本 Java。HMCL 能夠識別與管理多個 Java,並會自動根據遊戲版本為你選取合適的 Java。
|
||||
fatal.deprecated_java_version.download_link=下載 Java %d
|
||||
fatal.samba=如果您正在透過 Samba 共亯的目錄中開啟 Hello Minecraft! Launcher,啟動器可能無法正常工作,請嘗試更新您的 Java 或在本機目錄內開啟 HMCL。
|
||||
fatal.samba=如果您正在透過 Samba 共亯的目錄中開啟 HMCL,啟動器可能無法正常工作,請嘗試更新您的 Java 或在本機目錄內開啟 HMCL。
|
||||
fatal.illegal_char=由於您的執行路徑中存在無效字元『=』,您將無法使用外部登入帳戶以及離線登入更換外觀功能。
|
||||
fatal.unsupported_platform=Minecraft 尚未對您的平臺提供完善支援,所以可能影響遊戲體驗或無法啟動遊戲。\n\
|
||||
若無法啟動 Minecraft 1.17 及更高版本,可以嘗試在「(全域/實例特定) 遊戲設定 → 圖形設定」中將「繪製器」切換為「Mesa LLVMpipe」/「Mesa Lavapipe」,以獲得更好的相容性。
|
||||
fatal.unsupported_platform.loongarch=Hello Minecraft! Launcher 已為龍芯提供支援。\n如果遇到問題,你可以點擊右上角幫助按鈕進行求助。
|
||||
fatal.unsupported_platform.windows_arm64=Hello Minecraft! Launcher 已為 Windows on Arm 平臺提供原生支援。如果你在遊戲中遭遇問題,請嘗試使用 x86 架構的 Java 啟動遊戲。\n\n如果你正在使用<b>高通</b>平臺,你可能需要安裝 <a href="ms-windows-store://pdp/?productid=9NQPSL29BFFF">OpenGL 相容包</a>後才能進行遊戲。點擊連結前往 Microsoft Store 安裝相容包。
|
||||
fatal.unsupported_platform.loongarch=HMCL 已為龍芯提供支援。\n如果遇到問題,你可以點擊右上角幫助按鈕進行求助。
|
||||
fatal.unsupported_platform.windows_arm64=HMCL 已為 Windows on Arm 平臺提供原生支援。如果你在遊戲中遭遇問題,請嘗試使用 x86 架構的 Java 啟動遊戲。\n\n如果你正在使用<b>高通</b>平臺,你可能需要安裝 <a href="ms-windows-store://pdp/?productid=9NQPSL29BFFF">OpenGL 相容包</a>後才能進行遊戲。點擊連結前往 Microsoft Store 安裝相容包。
|
||||
# When translating the following string into non-Latin languages, please retain the English translation
|
||||
fatal.wine_warning=HMCL is running under Wine or one of its distributions.\nAs a cross-platform application, HMCL is best experienced natively on your host OS.\nPress "OK" to continue, but please note that some issues may arise.\n\n您目前正在使用 Wine 或其衍生版本執行 HMCL。\nHMCL 是跨平臺的,我們建議您直接在主機系統上執行。\n按下「確定」以繼續,但請注意,此操作可能會引發一些問題。
|
||||
|
||||
@@ -472,20 +470,20 @@ game.crash.reason.analyzing=分析中……
|
||||
game.crash.reason.multiple=檢測到多個原因:\n\n
|
||||
game.crash.reason.block=目前遊戲由於某個方塊不能正常工作,無法繼續執行。\n你可以嘗試使用地圖編輯工具編輯世界刪除該方塊,或者直接刪除對應的模組。\n方塊類型:%1$s\n方塊坐標:%2$s\n\n相關工具:<a href="https://minecraft.wiki/w/Tutorial:Programs_and_editors/Mapping#Map_editors">英文 Wiki</a> | <a href="https://zh.minecraft.wiki/w/辅助程序与编辑器/地图工具?variant=zh-tw#地图编辑工具">中文 Wiki</a>
|
||||
game.crash.reason.bootstrap_failed=目前遊戲由於模組「%1$s」出現問題,無法繼續執行。\n你可以嘗試刪除或更新該模組以解決問題。
|
||||
game.crash.reason.mixin_apply_mod_failed=目前遊戲由於 Mixin 無法應用於「%1$s」模組,無法繼續執行。\n你可以嘗試刪除或更新該模組以解決問題。
|
||||
game.crash.reason.config=目前遊戲由於無法解析模組配置檔案,無法繼續執行\n模組「%1$s」的配置檔案「%2$s」無法被解析。
|
||||
game.crash.reason.debug_crash=目前遊戲由於手動觸發崩潰,無法繼續執行。\n事實上遊戲並沒有問題,問題都是你造成的!
|
||||
game.crash.reason.duplicated_mod=目前遊戲由於模組重複安裝,無法繼續執行。\n%s\n每種模組只能安裝一個,請你刪除多餘的模組再試。
|
||||
game.crash.reason.entity=目前遊戲由於某個實體不能正常工作,無法繼續執行。\n你可以嘗試使用地圖編輯工具編輯世界刪除該實體,或者直接刪除對應的模組。\n實體類型:%1$s\n實體坐標:%2$s\n\n相關工具:<a href="https://minecraft.wiki/w/Tutorial:Programs_and_editors/Mapping#Map_editors">英文 Wiki</a> | <a href="https://zh.minecraft.wiki/w/辅助程序与编辑器/地图工具?variant=zh-tw#地图编辑工具">中文 Wiki</a>
|
||||
game.crash.reason.fabric_version_0_12=Fabric Loader 0.12 及更高版本與目前已經安裝的模組可能不相容,你需要將 Fabric Loader 降級至 0.11.7。
|
||||
game.crash.reason.fabric_warnings=Fabric 提供了一些警告訊息:\n%1$s
|
||||
game.crash.reason.modmixin_failure=目前遊戲由於某些模組注入失敗,無法繼續執行。\n這一般代表著該模組存在問題,或與目前環境不相容。\n你可以查看日誌尋找出錯模組。
|
||||
game.crash.reason.forge_error=Forge/NeoForge 可能已經提供了錯誤資訊。\n你可以查看日誌,並根據錯誤報告中的日誌資訊進行對應處。\n如果沒有看到報錯資訊,可以查看錯誤報告了解錯誤具體是如何發生的。\n%1$s
|
||||
game.crash.reason.mod_resolution0=目前遊戲由於一些模組出現問題,無法繼續執行。\n你可以查看日誌尋找出錯模組。
|
||||
game.crash.reason.need_jdk11=目前遊戲由於 Java 版本不合適,無法繼續執行。\n你需要下載安裝 Java 11,並在「(全域/實例特定) 遊戲設定 → 遊戲 Java」中將 Java 設定為 11.x 的版本。
|
||||
game.crash.reason.mixin_apply_mod_failed=目前遊戲由於 Mixin 無法應用於「%1$s」模組,無法繼續執行。\n你可以嘗試刪除或更新該模組以解決問題。
|
||||
game.crash.reason.java_version_is_too_high=目前遊戲由於使用的 Java 版本過高而崩潰了,無法繼續執行。\n請在「(全域/實例特定) 遊戲設定 → 遊戲 Java」中改用較低版本的 Java,然後再啟動遊戲。\n如果沒有,可以從 <a href="https://www.java.com/download/">java.com (Java 8)</a> 或 <a href="https://bell-sw.com/pages/downloads/#downloads">BellSoft Liberica Full JRE (Java 17)</a> 等平台下載、安裝一個 (安裝完後需重啟啟動器)。
|
||||
game.crash.reason.need_jdk11=目前遊戲由於 Java 版本不合適,無法繼續執行。\n你需要下載安裝 Java 11,並在「(全域/實例特定) 遊戲設定 → 遊戲 Java」中將 Java 設定為 11.x 的版本。
|
||||
game.crash.reason.mod_name=目前遊戲由於模組檔案名稱問題,無法繼續執行。\n模組檔案名稱應只使用半型的大小寫字母 (Aa~Zz)、數位 (0~9)、橫線 (-)、底線 (_)和點 (.)。\n請到模組目錄中將所有不合規的模組檔案名稱修正為上述合規字元。
|
||||
game.crash.reason.incomplete_forge_installation=目前遊戲由於 Forge 安裝不完整,無法繼續執行。\n請在「實例管理 - 自動安裝」中移除 Forge 並重新安裝。
|
||||
game.crash.reason.fabric_version_0_12=Fabric Loader 0.12 及更高版本與目前已經安裝的模組可能不相容,你需要將 Fabric Loader 降級至 0.11.7。
|
||||
game.crash.reason.fabric_warnings=Fabric 提供了一些警告訊息:\n%1$s
|
||||
game.crash.reason.file_already_exists=目前遊戲由於檔案「%1$s」已經存在,無法繼續執行。\n如果你認為這個檔案可以刪除,你可以在備份這個檔案後嘗試刪除它,並重新啟動遊戲。
|
||||
game.crash.reason.file_changed=目前遊戲由於檔案校驗失敗,無法繼續執行。\n如果你手動修改了 Minecraft.jar 檔案,你需要回退修改,或者重新下載遊戲。
|
||||
game.crash.reason.gl_operation_failure=目前遊戲由於你使用的某些模組/光影包/資源包/紋理包出現問題,無法繼續執行。\n請先嘗試停用你所使用的模組/光影包/資源包/紋理包再試。
|
||||
@@ -495,11 +493,20 @@ game.crash.reason.graphics_driver=目前遊戲由於你的顯示卡驅動存在
|
||||
game.crash.reason.macos_failed_to_find_service_port_for_display=目前遊戲由於 Apple Silicon 平台下初始化 OpenGL 視窗失敗,無法繼續執行。\n對於該問題,HMCL 暫無直接性的解決方案。請你嘗試任意開啟一個瀏覽器並切換為全螢幕,然後再回到 HMCL 啟動遊戲。在彈出遊戲視窗前<b>迅速切回瀏覽器頁面</b>,等待遊戲視窗出現後再切回遊戲視窗。
|
||||
game.crash.reason.illegal_access_error=目前遊戲由於某些模組的問題,無法繼續執行。\n如果你認識「%1$s」,你可以更新或刪除對應模組再試。
|
||||
game.crash.reason.install_mixinbootstrap=目前遊戲由於缺失 MixinBootstrap,無法繼續執行。\n你可以嘗試安裝 <a href="https://www.curseforge.com/minecraft/mc-mods/mixinbootstrap">MixinBootstrap</a> 解決該問題。若安裝後崩潰,可以嘗試在該模組的檔案名前新增半形驚嘆號 (!) 解決。
|
||||
game.crash.reason.optifine_is_not_compatible_with_forge=目前遊戲由於 OptiFine 與目前版本的 Forge 不相容,導致了遊戲崩潰。\n請前往 <a href="https://optifine.net/downloads">OptiFine 官網</a>查看 OptiFine 所相容的 Forge 版本,並嚴格按照對應版本重新安裝遊戲,或在「實例管理 → 自動安裝」中更換版本。\n經測試,Forge 版本過高或過低都可能導致崩潰。
|
||||
game.crash.reason.mod_files_are_decompressed=目前遊戲由於模組檔案被解壓了,無法繼續執行。\n請直接把整個模組檔案放進模組目錄中即可。\n解壓模組檔案會導致遊戲出錯。請刪除模組目錄中已被解壓的模組,然後再啟動遊戲。
|
||||
game.crash.reason.shaders_mod=目前遊戲由於同時安裝了 OptiFine 和 Shaders 模組,無法繼續執行。 \n由於 OptiFine 已整合 Shaders 模組的功能,只需刪除 Shaders 模組即可。
|
||||
game.crash.reason.rtss_forest_sodium=目前遊戲由於 RivaTuner Statistics Server (RTSS) 與 Sodium 不相容,導致遊戲崩潰。\n點擊 <a href="https://github.com/CaffeineMC/sodium-fabric/wiki/Known-Issues#rtss-incompatible">此處</a> 查看詳情。
|
||||
game.crash.reason.too_many_mods_lead_to_exceeding_the_id_limit=目前遊戲由於你所安裝的模組過多,超出了遊戲的 ID 限制,無法繼續執行。\n請嘗試安裝<a href="https://www.curseforge.com/minecraft/mc-mods/jeid">JEID</a>等修正模組,或刪除部分大型模組。
|
||||
game.crash.reason.night_config_fixes=目前遊戲由於 Night Config 庫的一些問題,無法繼續執行。\n你可以嘗試安裝 <a href="https://www.curseforge.com/minecraft/mc-mods/night-config-fixes">Night Config Fixes</a> 模組,這或許能幫助你解決這個問題。\n了解更多,可訪問該模組的 <a href="https://github.com/Fuzss/nightconfigfixes">GitHub 儲存庫</a>。
|
||||
game.crash.reason.optifine_causes_the_world_to_fail_to_load=目前遊戲可能由於 OptiFine 而無法繼續執行。\n該問題只在特定 OptiFine 版本中出現,你可以嘗試在「實例管理 → 自動安裝」中更換 OptiFine 的版本。
|
||||
game.crash.reason.jdk_9=目前遊戲由於 Java 版本過高,無法繼續執行。\n你需要下載安裝 Java 8,並在「(全域/實例特定) 遊戲設定 → 遊戲 Java」中將 Java 設定為 1.8.x 的版本。
|
||||
game.crash.reason.jvm_32bit=目前遊戲由於記憶體分配過大,超過了 32 位 Java 虛擬機記憶體限制,無法繼續執行。\n如果你的電腦是 64 位系統,請下載安裝並更換 64 位 Java。\n如果你的電腦是 32 位系統,你或許可以重新安裝 64 位系統,或換一台新電腦。\n或者,你可以關閉「(全域/實例特定) 遊戲設定 → 遊戲記憶體」中的「自動分配」,並且把記憶體分配值調節為 1024 MiB 或以下。
|
||||
game.crash.reason.loading_crashed_forge=目前遊戲由於模組「%1$s (%2$s)」錯誤,無法繼續執行。\n你可以嘗試刪除或更新該模組以解決問題。
|
||||
game.crash.reason.loading_crashed_fabric=目前遊戲由於模組「%1$s」錯誤,無法繼續執行。\n你可以嘗試刪除或更新該模組以解決問題。
|
||||
game.crash.reason.mac_jdk_8u261=目前遊戲由於你所使用的 Forge 或 OptiFine 與 Java 衝突而崩潰。\n請嘗試更新 Forge 和 OptiFine,或使用 Java 8u251 及更早版本啟動。
|
||||
game.crash.reason.forge_repeat_installation=目前遊戲由於 Forge 重複安裝,無法繼續執行。<a href="https://github.com/HMCL-dev/HMCL/issues/1880">此為已知問題</a>\n建議將日誌上傳並回報至 GitHub,以便我們找到更多線索並修復此問題。\n目前你可以在「實例管理 → 自動安裝」中移除 Forge 並重新安裝。
|
||||
game.crash.reason.optifine_repeat_installation=目前遊戲由於重複安裝 OptiFine,無法繼續執行。\n請刪除模組目錄下的 OptiFine 或前往「實例管理 → 自動安裝」移除安裝的 OptiFine。
|
||||
game.crash.reason.memory_exceeded=目前遊戲由於記憶體分配過大,無法繼續執行。\n該問題是由於系統分頁檔太小導致的。\n你需要在「(全域/實例特定) 遊戲設定 → 遊戲記憶體」中關閉「自動分配」,並將記憶體分配值調低至遊戲能正常啟動為止。\n你還可以嘗試調大系統的分頁檔大小。
|
||||
game.crash.reason.mod=目前遊戲由於 %1$s 的問題,無法繼續執行。\n你可以更新或刪除已經安裝的「%1$s」再試。
|
||||
game.crash.reason.mod_resolution=目前遊戲由於相依模組問題,無法繼續執行。Fabric 提供了如下訊息:\n%1$s
|
||||
@@ -511,16 +518,7 @@ game.crash.reason.mod_resolution_missing=目前遊戲由於缺少相依模組,
|
||||
game.crash.reason.mod_resolution_missing_minecraft=目前遊戲由於模組和 Minecraft 遊戲版本不匹配,無法繼續執行。\n「%1$s」需要 Minecraft %2$s 才能執行。\n如果你要繼續使用你已經安裝的模組,你可以選取安裝對應的 Minecraft 版本。如果你要繼續使用目前的 Minecraft 版本,你需要安裝對應版本的模組。
|
||||
game.crash.reason.mod_resolution_mod_version=%1$s (版本號 %2$s)
|
||||
game.crash.reason.mod_resolution_mod_version.any=%1$s (任意版本)
|
||||
game.crash.reason.forge_repeat_installation=目前遊戲由於 Forge 重複安裝,無法繼續執行。<a href="https://github.com/HMCL-dev/HMCL/issues/1880">此為已知問題</a>\n建議將日誌上傳並回報至 GitHub,以便我們找到更多線索並修復此問題。\n目前你可以在「實例管理 → 自動安裝」中移除 Forge 並重新安裝。
|
||||
game.crash.reason.optifine_repeat_installation=目前遊戲由於重複安裝 OptiFine,無法繼續執行。\n請刪除模組目錄下的 OptiFine 或前往「實例管理 → 自動安裝」移除安裝的 OptiFine。
|
||||
game.crash.reason.optifine_is_not_compatible_with_forge=目前遊戲由於 OptiFine 與目前版本的 Forge 不相容,導致了遊戲崩潰。\n請前往 <a href="https://optifine.net/downloads">OptiFine 官網</a>查看 OptiFine 所相容的 Forge 版本,並嚴格按照對應版本重新安裝遊戲,或在「實例管理 → 自動安裝」中更換版本。\n經測試,Forge 版本過高或過低都可能導致崩潰。
|
||||
game.crash.reason.night_config_fixes=目前遊戲由於 Night Config 庫的一些問題,無法繼續執行。\n你可以嘗試安裝 <a href="https://www.curseforge.com/minecraft/mc-mods/night-config-fixes">Night Config Fixes</a> 模組,這或許能幫助你解決這個問題。\n了解更多,可訪問該模組的 <a href="https://github.com/Fuzss/nightconfigfixes">GitHub 儲存庫</a>。
|
||||
game.crash.reason.mod_files_are_decompressed=目前遊戲由於模組檔案被解壓了,無法繼續執行。\n請直接把整個模組檔案放進模組目錄中即可。\n解壓模組檔案會導致遊戲出錯。請刪除模組目錄中已被解壓的模組,然後再啟動遊戲。
|
||||
game.crash.reason.too_many_mods_lead_to_exceeding_the_id_limit=目前遊戲由於你所安裝的模組過多,超出了遊戲的 ID 限制,無法繼續執行。\n請嘗試安裝<a href="https://www.curseforge.com/minecraft/mc-mods/jeid">JEID</a>等修正模組,或刪除部分大型模組。
|
||||
game.crash.reason.optifine_causes_the_world_to_fail_to_load=目前遊戲可能由於 OptiFine 而無法繼續執行。\n該問題只在特定 OptiFine 版本中出現,你可以嘗試在「實例管理 → 自動安裝」中更換 OptiFine 的版本。
|
||||
game.crash.reason.modlauncher_8=目前遊戲由於你所使用的 Forge 版本與目前使用的 Java 衝突崩潰。請嘗試更新 Forge。
|
||||
game.crash.reason.shaders_mod=目前遊戲由於同時安裝了 OptiFine 和 Shaders 模組,無法繼續執行。 \n由於 OptiFine 已整合 Shaders 模組的功能,只需刪除 Shaders 模組即可。
|
||||
game.crash.reason.rtss_forest_sodium=目前遊戲由於 RivaTuner Statistics Server (RTSS) 與 Sodium 不相容,導致遊戲崩潰。\n點擊 <a href="https://github.com/CaffeineMC/sodium-fabric/wiki/Known-Issues#rtss-incompatible">此處</a> 查看詳情。
|
||||
game.crash.reason.no_class_def_found_error=目前遊戲由於程式碼不完整,無法繼續執行。\n你的遊戲可能缺失了某個模組,或者某些模組檔案不完整,或者模組與遊戲的版本不匹配。\n你可能需要重新安裝遊戲和模組,或請求他人幫助。\n缺失:%1$s
|
||||
game.crash.reason.no_such_method_error=目前遊戲由於程式碼不完整,無法繼續執行。\n你的遊戲可能缺失了某個模組,或者某些模組檔案不完整,或者模組與遊戲的版本不匹配。\n你可能需要重新安裝遊戲和模組,或請求他人幫助。
|
||||
game.crash.reason.opengl_not_supported=目前遊戲由於你的顯示卡驅動存在問題,無法繼續執行。\n原因是 OpenGL 不受支援,你現在是否在遠端桌面或者串流模式下?如果是,請直接使用原電腦啟動遊戲。\n或者嘗試升級你的顯示卡驅動到最新版本後再嘗試啟動遊戲。如果你的電腦同時存在獨立顯示卡與整合式顯示卡,你需要檢查遊戲是否使用整合式顯示卡啟動。如果是,請嘗試使用獨立顯示卡開啟 HMCL 與遊戲。如果仍有問題,你可能需要考慮換一個新顯示卡或新電腦。
|
||||
@@ -547,7 +545,7 @@ game_directory.use_relative_path=如可行,則對於遊戲目錄使用相對
|
||||
game_directory.root=使用磁碟根目錄作為遊戲資料夾可能會導致無法啟動遊戲,並會在磁碟根目錄下建立多個檔案和資料夾。\n\n確定要繼續嗎?
|
||||
|
||||
help=說明
|
||||
help.doc=Hello Minecraft! Launcher 說明文件
|
||||
help.doc=HMCL 說明文件
|
||||
help.detail=可查閱資料包、模組包製作教學等內容
|
||||
|
||||
input.email=[使用者名稱] 必須是電子信箱格式
|
||||
@@ -766,9 +764,9 @@ launcher.cache_directory.default=預設 ("%APPDATA%/.minecraft" 或 "~/.minecraf
|
||||
launcher.cache_directory.disabled=停用
|
||||
launcher.cache_directory.invalid=無法建立自訂的快取目錄。已還原至預設設定。
|
||||
launcher.contact=聯絡我們
|
||||
launcher.crash=Hello Minecraft! Launcher 遇到了無法處理的錯誤。請複製下列內容並透過 GitHub、Discord 或 HMCL QQ 群回報問題。
|
||||
launcher.crash.java_internal_error=Hello Minecraft! Launcher 由於目前 Java 損壞而無法繼續執行。請移除目前 Java,點擊 <a href="https://bell-sw.com/pages/downloads/#downloads">此處</a> 安裝合適的 Java 版本。
|
||||
launcher.crash.hmcl_out_dated=Hello Minecraft! Launcher 遇到了無法處理的錯誤。已偵測到你的啟動器不是最新版本,請更新後重試!
|
||||
launcher.crash=HMCL 遇到了無法處理的錯誤。請複製下列內容並透過 GitHub、Discord 或 HMCL QQ 群回報問題。
|
||||
launcher.crash.java_internal_error=HMCL 由於目前 Java 損壞而無法繼續執行。請移除目前 Java,點擊 <a href="https://bell-sw.com/pages/downloads/#downloads">此處</a> 安裝合適的 Java 版本。
|
||||
launcher.crash.hmcl_out_dated=HMCL 遇到了無法處理的錯誤。已偵測到你的啟動器不是最新版本,請更新後重試!
|
||||
launcher.update_java=請更新你的 Java
|
||||
|
||||
libraries.download=下載依賴庫
|
||||
@@ -837,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=官方網站
|
||||
@@ -850,15 +851,15 @@ modpack.type.curse.error=無法完成該模組包所需的相依元件下載,
|
||||
modpack.type.curse.not_found=部分必需檔案已經從網路中被刪除並且再也無法下載。請嘗試該模組包的最新版本或者安裝其他模組包。
|
||||
modpack.type.manual.warning=該模組包由發佈者手動打包,其中可能已經包含啟動器。建議嘗試解壓後使用其內建的啟動器執行遊戲。\nHMCL 可以嘗試匯入該模組包,但不保證可用性。是否繼續?
|
||||
modpack.type.mcbbs=MCBBS 模組包
|
||||
modpack.type.mcbbs.export=可以被 Hello Minecraft! Launcher 匯入
|
||||
modpack.type.mcbbs.export=可以被 HMCL 匯入
|
||||
modpack.type.modrinth=Modrinth
|
||||
modpack.type.modrinth.export=可以被主流第三方啟動器匯入
|
||||
modpack.type.multimc=MultiMC
|
||||
modpack.type.multimc.export=可以被 Hello Minecraft! Launcher 和 MultiMC 匯入
|
||||
modpack.type.multimc.export=可以被 HMCL 和 MultiMC 匯入
|
||||
modpack.type.server=伺服器自動更新模組包
|
||||
modpack.type.server.export=允許伺服器管理員遠端更新遊戲用戶端
|
||||
modpack.type.server.malformed=伺服器模組包配置格式錯誤,請聯絡伺服器管理員解決此問題
|
||||
modpack.unsupported=Hello Minecraft! Launcher 不支援該模組包格式
|
||||
modpack.unsupported=HMCL 不支援該模組包格式
|
||||
modpack.update=正在升級模組包
|
||||
modpack.wizard=匯出模組包引導
|
||||
modpack.wizard.step.1=基本設定
|
||||
@@ -1056,6 +1057,7 @@ world.export=匯出此世界
|
||||
world.export.title=選取該世界的儲存位置
|
||||
world.export.location=儲存到
|
||||
world.export.wizard=匯出世界「%s」
|
||||
world.game_version=遊戲版本
|
||||
world.icon=世界圖示
|
||||
world.icon.change=修改世界圖示
|
||||
world.icon.change.fail.load.title=圖片解析失敗
|
||||
@@ -1101,7 +1103,6 @@ world.info.time.format=%d 天 %d 小時 %d 分鐘
|
||||
world.load.fail=世界載入失敗
|
||||
world.locked=使用中
|
||||
world.locked.failed=該世界正在使用中,請關閉遊戲後重試。
|
||||
world.game_version=遊戲版本
|
||||
world.manage=世界管理
|
||||
world.manage.button=管理此世界
|
||||
world.manage.title=世界 - %s
|
||||
@@ -1138,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=建立目錄失敗
|
||||
@@ -1154,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 個子項
|
||||
|
||||
@@ -1536,8 +1542,8 @@ terracotta.unsupported.arch.32bit=多人聯機功能不支援 32 位元系統。
|
||||
terracotta.unsupported.arch.loongarch64_ow=多人聯機功能不支援 Linux LoongArch64 舊世界發行版,請更新至新世界發行版 (如 AOSC OC)。
|
||||
terracotta.unsupported.region=多人聯機功能目前僅為中國內地使用者提供服務,在您所在地區可能不可用。
|
||||
|
||||
theme_pack.appearance.custom.tooltip=目前使用自訂啟動器設定。點擊後跟隨所選主題。
|
||||
theme_pack.appearance.follow_theme.tooltip=目前跟隨所選主題。點擊後使用自訂啟動器設定。
|
||||
theme_pack.appearance.custom.tooltip=目前使用自訂啟動器設定。點擊後跟隨所選主題。
|
||||
theme_pack.apply=套用主題
|
||||
theme_pack.apply.confirm=確定要套用主題「%s」嗎?
|
||||
theme_pack.apply.failed=套用主題失敗
|
||||
@@ -1587,6 +1593,7 @@ update.channel.nightly.hint=你正在使用 HMCL 預覽版。預覽版更新較
|
||||
update.channel.nightly.title=預覽版提示
|
||||
update.channel.stable=穩定版
|
||||
update.checking=正在檢查更新
|
||||
update.check_failed=檢查更新失敗 %s
|
||||
update.disable_auto_show_update_dialog=不自動顯示更新彈窗
|
||||
update.disable_auto_show_update_dialog.subtitle=啟用此選項,HMCL 將不會自動彈出更新彈窗。
|
||||
update.failed=更新失敗
|
||||
@@ -1600,6 +1607,7 @@ update.latest=目前版本為最新版本
|
||||
update.tooltip=更新
|
||||
update.preview=提前測試 HMCL 預覽版本
|
||||
update.preview.subtitle=啟用此選項,你將可以提前取得 HMCL 的新版本,以便在正式發布前進行測試。
|
||||
update.unverified=更新不可用,因為無法驗證此啟動器例項完整性
|
||||
|
||||
warning.java_interpreted_mode=HMCL 正在執行於直譯器模式的 Java 環境中,效能將會受到很大影響。\n我們建議你使用支援 JIT 的 Java 開啟 HMCL 以獲得最佳體驗。
|
||||
warning.software_rendering=HMCL 正在使用軟體繪製,效能將會受到很大影響。
|
||||
@@ -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,
|
||||
|
||||
@@ -0,0 +1,147 @@
|
||||
/*
|
||||
* 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.construct;
|
||||
|
||||
import javafx.application.Platform;
|
||||
import javafx.scene.Scene;
|
||||
import javafx.scene.control.Label;
|
||||
import javafx.scene.layout.HBox;
|
||||
import javafx.scene.layout.Region;
|
||||
import org.jetbrains.annotations.NotNullByDefault;
|
||||
import org.junit.jupiter.api.condition.EnabledIf;
|
||||
import org.junit.jupiter.params.ParameterizedTest;
|
||||
import org.junit.jupiter.params.provider.ValueSource;
|
||||
|
||||
import java.util.concurrent.ExecutionException;
|
||||
import java.util.concurrent.FutureTask;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.TimeoutException;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
/// Verifies tag sizing in parents that allocate only the item's preferred width.
|
||||
@NotNullByDefault
|
||||
@EnabledIf("org.jackhuang.hmcl.JavaFXLauncher#isStarted")
|
||||
public final class TwoLineListItemTest {
|
||||
|
||||
/// Verifies tags remain visible as they are added, resized, removed, and added again.
|
||||
@ParameterizedTest
|
||||
@ValueSource(booleans = {false, true})
|
||||
public void testPreferredWidthIncludesTags(boolean wrapText) {
|
||||
onFxThread(() -> {
|
||||
TwoLineListItem item = new TwoLineListItem("Example Mod", "a.jar");
|
||||
item.getTitleLabel().setWrapText(wrapText);
|
||||
HBox parent = createParent(item);
|
||||
layout(parent, 800);
|
||||
double originalWidth = item.getWidth();
|
||||
|
||||
item.addTag("Fabric");
|
||||
layout(parent, 800);
|
||||
assertTagsVisible(item);
|
||||
assertTrue(item.getWidth() > originalWidth);
|
||||
|
||||
item.addTag("NeoForge");
|
||||
item.addTag("Quilt");
|
||||
layout(parent, 800);
|
||||
assertTagsVisible(item);
|
||||
double taggedWidth = item.getWidth();
|
||||
|
||||
item.getTags().get(0).setText("A longer loader tag");
|
||||
layout(parent, 800);
|
||||
assertTagsVisible(item);
|
||||
assertTrue(item.getWidth() > taggedWidth);
|
||||
|
||||
item.getTags().clear();
|
||||
layout(parent, 800);
|
||||
assertEquals(originalWidth, item.getWidth(), 0.001);
|
||||
|
||||
item.addTag("Fabric");
|
||||
item.setSubtitle("A subtitle that is wider than the title and its loader tag");
|
||||
layout(parent, 800);
|
||||
assertTagsVisible(item);
|
||||
assertTrue(item.getSubtitleLabel().getWidth() >= item.getSubtitleLabel().prefWidth(-1));
|
||||
});
|
||||
}
|
||||
|
||||
/// Verifies constrained rows give the title priority while allowing truncation or wrapping.
|
||||
@ParameterizedTest
|
||||
@ValueSource(booleans = {false, true})
|
||||
public void testTitleRetainsPriority(boolean wrapText) {
|
||||
onFxThread(() -> {
|
||||
TwoLineListItem item = new TwoLineListItem("A very long mod title ".repeat(20), "a.jar");
|
||||
item.getTitleLabel().setWrapText(wrapText);
|
||||
item.addTag("Fabric");
|
||||
item.addTag("NeoForge");
|
||||
HBox parent = createParent(item);
|
||||
layout(parent, 300);
|
||||
|
||||
assertEquals(252, item.getWidth(), 0.001);
|
||||
assertEquals(item.getWidth(), item.getTitleLabel().getWidth(), 0.001);
|
||||
assertTrue(item.getBoundsInParent().getMaxX() <= parent.getWidth());
|
||||
if (wrapText) {
|
||||
assertTrue(item.getTitleLabel().getHeight() > item.getTitleLabel().prefHeight(-1));
|
||||
}
|
||||
|
||||
item.setTitle("Mod");
|
||||
layout(parent, 300);
|
||||
assertTagsVisible(item);
|
||||
});
|
||||
}
|
||||
|
||||
/// Creates a scene with an icon and an item without a horizontal growth constraint.
|
||||
private static HBox createParent(TwoLineListItem item) {
|
||||
Region icon = new Region();
|
||||
icon.setMinSize(40, 40);
|
||||
icon.setPrefSize(40, 40);
|
||||
icon.setMaxSize(40, 40);
|
||||
HBox parent = new HBox(8, icon, item);
|
||||
new Scene(parent);
|
||||
return parent;
|
||||
}
|
||||
|
||||
/// Applies CSS and lays out the parent at the given width and its preferred height.
|
||||
private static void layout(HBox parent, double width) {
|
||||
parent.applyCss();
|
||||
parent.resize(width, parent.prefHeight(width));
|
||||
parent.layout();
|
||||
}
|
||||
|
||||
/// Verifies every tag fits within the region that clips its contents.
|
||||
private static void assertTagsVisible(TwoLineListItem item) {
|
||||
for (Label tag : item.getTags()) {
|
||||
Region container = (Region) tag.getParent();
|
||||
assertTrue(tag.getBoundsInParent().getMaxX() <= container.getWidth(), "Tag is clipped");
|
||||
assertTrue(tag.getWidth() >= tag.prefWidth(-1), "Tag text is truncated");
|
||||
}
|
||||
}
|
||||
|
||||
/// Runs assertions on the JavaFX thread and waits up to 30 seconds for completion.
|
||||
private static void onFxThread(Runnable action) {
|
||||
FutureTask<Boolean> task = new FutureTask<>(action, true);
|
||||
Platform.runLater(task);
|
||||
try {
|
||||
task.get(30, TimeUnit.SECONDS);
|
||||
} catch (InterruptedException e) {
|
||||
Thread.currentThread().interrupt();
|
||||
throw new AssertionError(e);
|
||||
} catch (ExecutionException | TimeoutException e) {
|
||||
throw new AssertionError(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
/*
|
||||
* 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.addon;
|
||||
|
||||
import org.jackhuang.hmcl.addon.mod.ModLoaderType;
|
||||
import org.jackhuang.hmcl.util.StringUtils;
|
||||
import org.jetbrains.annotations.NotNullByDefault;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
@NotNullByDefault
|
||||
public record AddonLoader(String name, @Nullable AddonLoaderType type) {
|
||||
public static boolean mightBeLoader(String str) {
|
||||
if (StringUtils.isBlank(str)
|
||||
|| !StringUtils.isASCII(str)
|
||||
|| "client".equalsIgnoreCase(str) || "server".equalsIgnoreCase(str))
|
||||
return false;
|
||||
int l = str.length();
|
||||
for (int i = 0; i < l; i++) {
|
||||
char c = str.charAt(i);
|
||||
if (c != '-' && c != ' ' && c != '_' && !StringUtils.isAlphabetic(c)) return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
public static AddonLoader of(String name) {
|
||||
for (var type : ModLoaderType.values()) {
|
||||
for (String loaderName : type.names()) {
|
||||
if (name.equalsIgnoreCase(loaderName)) {
|
||||
return new AddonLoader(name, type);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
return new AddonLoader(name, null);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
/*
|
||||
* 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.addon;
|
||||
|
||||
import org.jetbrains.annotations.NotNullByDefault;
|
||||
import org.jetbrains.annotations.Unmodifiable;
|
||||
|
||||
import java.util.Set;
|
||||
|
||||
/// For mods and shaders
|
||||
@NotNullByDefault
|
||||
public interface AddonLoaderType {
|
||||
|
||||
String displayName();
|
||||
|
||||
@Unmodifiable Set<String> names();
|
||||
|
||||
}
|
||||
@@ -17,7 +17,6 @@
|
||||
*/
|
||||
package org.jackhuang.hmcl.addon;
|
||||
|
||||
import org.jackhuang.hmcl.addon.mod.ModLoaderType;
|
||||
import org.jackhuang.hmcl.addon.repository.CurseForgeRemoteAddonRepository;
|
||||
import org.jackhuang.hmcl.addon.repository.ModrinthRemoteAddonRepository;
|
||||
import org.jackhuang.hmcl.download.DownloadProvider;
|
||||
@@ -224,7 +223,7 @@ public record RemoteAddon(String slug, String author, String title, String descr
|
||||
|
||||
public record Version(IVersion self, String versionId, String projectId, String name, String version,
|
||||
Instant datePublished, VersionType versionType, File file, List<Dependency> dependencies,
|
||||
List<String> gameVersions, List<ModLoaderType> loaders) {
|
||||
List<String> gameVersions, List<AddonLoader> loaders) {
|
||||
}
|
||||
|
||||
public record File(Map<String, String> hashes, String url, String filename) {
|
||||
|
||||
@@ -35,52 +35,6 @@ public interface RemoteAddonRepository {
|
||||
|
||||
String getBaseUrl();
|
||||
|
||||
enum SortType {
|
||||
POPULARITY,
|
||||
NAME,
|
||||
DATE_CREATED,
|
||||
LAST_UPDATED,
|
||||
AUTHOR,
|
||||
TOTAL_DOWNLOADS
|
||||
}
|
||||
|
||||
enum SortOrder {
|
||||
ASC,
|
||||
DESC
|
||||
}
|
||||
|
||||
class SearchResult {
|
||||
private final Stream<RemoteAddon> sortedResults;
|
||||
|
||||
private final Stream<RemoteAddon> unsortedResults;
|
||||
|
||||
private final int totalPages;
|
||||
|
||||
public SearchResult(Stream<RemoteAddon> sortedResults, Stream<RemoteAddon> unsortedResults, int totalPages) {
|
||||
this.sortedResults = sortedResults;
|
||||
this.unsortedResults = unsortedResults;
|
||||
this.totalPages = totalPages;
|
||||
}
|
||||
|
||||
public SearchResult(Stream<RemoteAddon> sortedResults, int pages) {
|
||||
this.sortedResults = sortedResults;
|
||||
this.unsortedResults = sortedResults;
|
||||
this.totalPages = pages;
|
||||
}
|
||||
|
||||
public Stream<RemoteAddon> getResults() {
|
||||
return this.sortedResults;
|
||||
}
|
||||
|
||||
public Stream<RemoteAddon> getUnsortedResults() {
|
||||
return this.unsortedResults;
|
||||
}
|
||||
|
||||
public int getTotalPages() {
|
||||
return this.totalPages;
|
||||
}
|
||||
}
|
||||
|
||||
SearchResult search(DownloadProvider downloadProvider, String gameVersion, @Nullable Category category, int pageOffset, int pageSize, String searchFilter, SortType sortType, SortOrder sortOrder)
|
||||
throws IOException;
|
||||
|
||||
@@ -106,4 +60,20 @@ public interface RemoteAddonRepository {
|
||||
|
||||
record Category(Object self, String id, List<Category> subcategories) {
|
||||
}
|
||||
|
||||
enum SortType {
|
||||
RELEVANCY,
|
||||
POPULARITY,
|
||||
DATE_CREATED,
|
||||
LAST_UPDATED,
|
||||
TOTAL_DOWNLOADS
|
||||
}
|
||||
|
||||
enum SortOrder {
|
||||
ASC,
|
||||
DESC
|
||||
}
|
||||
|
||||
record SearchResult(Stream<RemoteAddon> results, int totalPages) {
|
||||
}
|
||||
}
|
||||
@@ -202,7 +202,7 @@ public final class LocalModFile extends LocalAddonFile implements Comparable<Loc
|
||||
if (currentVersion.isEmpty()) return null;
|
||||
List<RemoteAddon.Version> remoteVersions = repository.getRemoteVersionsById(downloadProvider, currentVersion.get().projectId())
|
||||
.filter(version -> version.gameVersions().contains(gameVersion))
|
||||
.filter(version -> version.loaders().contains(getModLoaderType()))
|
||||
.filter(version -> version.loaders().stream().anyMatch(it -> it.type() == getModLoaderType()))
|
||||
.filter(version -> version.datePublished().compareTo(currentVersion.get().datePublished()) > 0)
|
||||
.sorted(Comparator.comparing(RemoteAddon.Version::datePublished).reversed())
|
||||
.toList();
|
||||
|
||||
@@ -17,27 +17,45 @@
|
||||
*/
|
||||
package org.jackhuang.hmcl.addon.mod;
|
||||
|
||||
import org.jackhuang.hmcl.addon.AddonLoaderType;
|
||||
import org.jetbrains.annotations.NotNullByDefault;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
import org.jetbrains.annotations.Unmodifiable;
|
||||
|
||||
import java.util.Set;
|
||||
|
||||
@NotNullByDefault
|
||||
public enum ModLoaderType {
|
||||
UNKNOWN(null),
|
||||
FORGE("INST_FORGE"),
|
||||
CLEANROOM("INST_CLEANROOM"),
|
||||
NEO_FORGE("INST_NEOFORGE"),
|
||||
FABRIC("INST_FABRIC"),
|
||||
QUILT("INST_QUILT"),
|
||||
LITE_LOADER("INST_LITELOADER"),
|
||||
LEGACY_FABRIC("INST_LEGACYFABRIC");
|
||||
public enum ModLoaderType implements AddonLoaderType {
|
||||
UNKNOWN("Unknown", null),
|
||||
FORGE("Forge", "INST_FORGE", "forge"),
|
||||
CLEANROOM("Cleanroom", "INST_CLEANROOM", "cleanroom"),
|
||||
NEO_FORGE("NeoForge", "INST_NEOFORGE", "neoforge"),
|
||||
FABRIC("Fabric", "INST_FABRIC", "fabric"),
|
||||
QUILT("Quilt", "INST_QUILT", "quilt"),
|
||||
LITE_LOADER("LiteLoader", "INST_LITELOADER", "liteloader"),
|
||||
LEGACY_FABRIC("Legacy Fabric", "INST_LEGACYFABRIC", "legacy-fabric");
|
||||
|
||||
private final String displayName;
|
||||
private final @Nullable String envVarName;
|
||||
private final Set<String> names;
|
||||
|
||||
ModLoaderType(@Nullable String envVarName) {
|
||||
ModLoaderType(String displayName, @Nullable String envVarName, String... names) {
|
||||
this.displayName = displayName;
|
||||
this.envVarName = envVarName;
|
||||
this.names = Set.of(names);
|
||||
}
|
||||
|
||||
public @Nullable String getEnvVarName() {
|
||||
return envVarName;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String displayName() {
|
||||
return displayName;
|
||||
}
|
||||
|
||||
@Override
|
||||
public @Unmodifiable Set<String> names() {
|
||||
return names;
|
||||
}
|
||||
}
|
||||
@@ -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 {
|
||||
|
||||
+43
-45
@@ -18,13 +18,12 @@
|
||||
package org.jackhuang.hmcl.addon.repository;
|
||||
|
||||
import com.google.gson.reflect.TypeToken;
|
||||
import org.jackhuang.hmcl.addon.AddonLoader;
|
||||
import org.jackhuang.hmcl.addon.RemoteAddon;
|
||||
import org.jackhuang.hmcl.addon.RemoteAddonRepository;
|
||||
import org.jackhuang.hmcl.addon.mod.ModLoaderType;
|
||||
import org.jackhuang.hmcl.download.DownloadProvider;
|
||||
import org.jackhuang.hmcl.util.Immutable;
|
||||
import org.jackhuang.hmcl.util.MurmurHash2;
|
||||
import org.jackhuang.hmcl.util.Pair;
|
||||
import org.jackhuang.hmcl.util.StringUtils;
|
||||
import org.jackhuang.hmcl.util.io.*;
|
||||
import org.jackhuang.hmcl.util.versioning.GameVersionNumber;
|
||||
@@ -57,11 +56,10 @@ public final class CurseForgeRemoteAddonRepository implements RemoteAddonReposit
|
||||
private static final String PREFIX = "https://api.curseforge.com";
|
||||
private static final String BASE = "https://www.curseforge.com";
|
||||
private static final Semaphore SEMAPHORE = new Semaphore(16);
|
||||
private static final int DEFAULT_RETRY_COUNT = 3;
|
||||
|
||||
public static final String API_KEY = System.getProperty("hmcl.curseforge.apikey", JarUtils.getAttribute("hmcl.curseforge.apikey", ""));
|
||||
|
||||
private static final int WORD_PERFECT_MATCH_WEIGHT = 5;
|
||||
|
||||
private static <R extends HttpRequest> R withApiKey(R request) {
|
||||
if (request.getUrl().startsWith(PREFIX) && !API_KEY.isEmpty()) {
|
||||
request.header("X-API-KEY", API_KEY);
|
||||
@@ -106,13 +104,11 @@ public final class CurseForgeRemoteAddonRepository implements RemoteAddonReposit
|
||||
private static int toModsSearchSortField(SortType sort) {
|
||||
// https://docs.curseforge.com/rest-api/#tocS_ModsSearchSortField
|
||||
return switch (sort) {
|
||||
case DATE_CREATED -> 1;
|
||||
case RELEVANCY -> 1; // This represents Featured, however test shows that they are quite similar
|
||||
case POPULARITY -> 2;
|
||||
case DATE_CREATED -> 11;
|
||||
case LAST_UPDATED -> 3;
|
||||
case NAME -> 4;
|
||||
case AUTHOR -> 5;
|
||||
case TOTAL_DOWNLOADS -> 6;
|
||||
default -> 8;
|
||||
};
|
||||
}
|
||||
|
||||
@@ -128,6 +124,11 @@ public final class CurseForgeRemoteAddonRepository implements RemoteAddonReposit
|
||||
return (int) Math.ceil((double) Math.min(response.pagination.totalCount, 10000) / pageSize);
|
||||
}
|
||||
|
||||
/// Searches the configured section, preserving the order returned by the server.
|
||||
///
|
||||
/// @throws UnsupportedOperationException if this repository has no configured section
|
||||
/// @throws NoCandidatesException if no response is obtained and no I/O failure was recorded
|
||||
/// @throws IOException if all candidate requests fail with I/O errors
|
||||
@Override
|
||||
public SearchResult search(DownloadProvider downloadProvider, String gameVersion, @Nullable RemoteAddonRepository.Category category, int pageOffset, int pageSize, String searchFilter, SortType sortType, SortOrder sortOrder) throws IOException {
|
||||
if (type == null) throw new UnsupportedOperationException();
|
||||
@@ -150,18 +151,16 @@ public final class CurseForgeRemoteAddonRepository implements RemoteAddonReposit
|
||||
query.put("index", Integer.toString(pageOffset * pageSize));
|
||||
query.put("pageSize", Integer.toString(pageSize));
|
||||
|
||||
Response<List<CurseAddon>> response = null;
|
||||
@Nullable Response<List<CurseAddon>> response = null;
|
||||
|
||||
IOException exception = null;
|
||||
@Nullable IOException exception = null;
|
||||
List<URI> candidates = downloadProvider.injectURLWithCandidates(NetworkUtils.withQuery(PREFIX + "/v1/mods/search", query));
|
||||
for (URI candidate : candidates) {
|
||||
LOG.info("Fetching " + candidate);
|
||||
try {
|
||||
response = withApiKey(HttpRequest.GET(candidate.toString()))
|
||||
.retry(DEFAULT_RETRY_COUNT)
|
||||
.getJson(Response.typeOf(listTypeOf(CurseAddon.class)));
|
||||
if (searchFilter.isEmpty()) {
|
||||
return new SearchResult(response.data().stream().map(CurseAddon::toAddon), calculateTotalPages(response, pageSize));
|
||||
}
|
||||
break;
|
||||
} catch (IOException e) {
|
||||
LOG.warning("Failed to search addons: " + candidate, e);
|
||||
@@ -176,31 +175,10 @@ public final class CurseForgeRemoteAddonRepository implements RemoteAddonReposit
|
||||
}
|
||||
}
|
||||
|
||||
if (response == null) {
|
||||
throw exception != null ? exception : new NoCandidatesException();
|
||||
}
|
||||
if (response != null)
|
||||
return new SearchResult(response.data().stream().map(CurseAddon::toAddon), calculateTotalPages(response, pageSize));
|
||||
|
||||
// https://github.com/HMCL-dev/HMCL/issues/1549
|
||||
String lowerCaseSearchFilter = searchFilter.toLowerCase(Locale.ROOT);
|
||||
Map<String, Integer> searchFilterWords = new HashMap<>();
|
||||
for (String s : StringUtils.tokenize(lowerCaseSearchFilter)) {
|
||||
searchFilterWords.put(s, searchFilterWords.getOrDefault(s, 0) + 1);
|
||||
}
|
||||
|
||||
StringUtils.LevCalculator levCalculator = new StringUtils.LevCalculator();
|
||||
|
||||
return new SearchResult(response.data().stream().map(CurseAddon::toAddon).map(remoteAddon -> {
|
||||
String lowerCaseResult = remoteAddon.title().toLowerCase(Locale.ROOT);
|
||||
int diff = levCalculator.calc(lowerCaseSearchFilter, lowerCaseResult);
|
||||
|
||||
for (String s : StringUtils.tokenize(lowerCaseResult)) {
|
||||
if (searchFilterWords.containsKey(s)) {
|
||||
diff -= WORD_PERFECT_MATCH_WEIGHT * searchFilterWords.get(s) * s.length();
|
||||
}
|
||||
}
|
||||
|
||||
return pair(remoteAddon, diff);
|
||||
}).sorted(Comparator.comparingInt(Pair::getValue)).map(Pair::getKey), response.data().stream().map(CurseAddon::toAddon), calculateTotalPages(response, pageSize));
|
||||
throw exception != null ? exception : new NoCandidatesException();
|
||||
} finally {
|
||||
SEMAPHORE.release();
|
||||
}
|
||||
@@ -273,6 +251,7 @@ public final class CurseForgeRemoteAddonRepository implements RemoteAddonReposit
|
||||
try {
|
||||
Response<FingerprintMatchesResult> response = withApiKey(HttpRequest.POST(PREFIX + "/v1/fingerprints/432"))
|
||||
.json(mapOf(pair("fingerprints", Collections.singletonList(hash))))
|
||||
.retry(DEFAULT_RETRY_COUNT)
|
||||
.getJson(Response.typeOf(FingerprintMatchesResult.class));
|
||||
|
||||
if (response.data().exactMatches() == null || response.data().exactMatches().isEmpty()) {
|
||||
@@ -290,6 +269,7 @@ public final class CurseForgeRemoteAddonRepository implements RemoteAddonReposit
|
||||
SEMAPHORE.acquireUninterruptibly();
|
||||
try {
|
||||
Response<CurseAddon> response = withApiKey(HttpRequest.GET(PREFIX + "/v1/mods/" + id))
|
||||
.retry(DEFAULT_RETRY_COUNT)
|
||||
.getJson(Response.typeOf(CurseAddon.class));
|
||||
return response.data.toAddon();
|
||||
} finally {
|
||||
@@ -315,6 +295,7 @@ public final class CurseForgeRemoteAddonRepository implements RemoteAddonReposit
|
||||
SEMAPHORE.acquireUninterruptibly();
|
||||
try {
|
||||
Response<CurseAddon.LatestFile> response = withApiKey(HttpRequest.GET(String.format("%s/v1/mods/%s/files/%s", PREFIX, projectId, fileId)))
|
||||
.retry(DEFAULT_RETRY_COUNT)
|
||||
.getJson(Response.typeOf(CurseAddon.LatestFile.class));
|
||||
return response.data().toVersion().file();
|
||||
} finally {
|
||||
@@ -328,6 +309,7 @@ public final class CurseForgeRemoteAddonRepository implements RemoteAddonReposit
|
||||
try {
|
||||
Response<List<CurseAddon.LatestFile>> response = withApiKey(HttpRequest.GET(PREFIX + "/v1/mods/" + id + "/files",
|
||||
pair("pageSize", "10000")))
|
||||
.retry(DEFAULT_RETRY_COUNT)
|
||||
.getJson(Response.typeOf(listTypeOf(CurseAddon.LatestFile.class)));
|
||||
return response.data().stream().map(CurseAddon.LatestFile::toVersion);
|
||||
} finally {
|
||||
@@ -340,6 +322,7 @@ public final class CurseForgeRemoteAddonRepository implements RemoteAddonReposit
|
||||
SEMAPHORE.acquireUninterruptibly();
|
||||
try {
|
||||
Response<String> response = withApiKey(HttpRequest.GET(String.format("%s/v1/mods/%s/files/%s/changelog", PREFIX, addonId, versionId)))
|
||||
.retry(DEFAULT_RETRY_COUNT)
|
||||
.getJson(Response.typeOf(String.class));
|
||||
return response.data();
|
||||
} finally {
|
||||
@@ -352,6 +335,7 @@ public final class CurseForgeRemoteAddonRepository implements RemoteAddonReposit
|
||||
SEMAPHORE.acquireUninterruptibly();
|
||||
try {
|
||||
Response<CurseAddon> response = withApiKey(HttpRequest.GET(PREFIX + "/v1/mods/" + version.projectId()))
|
||||
.retry(DEFAULT_RETRY_COUNT)
|
||||
.getJson(Response.typeOf(CurseAddon.class));
|
||||
var addon = response.data();
|
||||
var classId = addon.classId();
|
||||
@@ -380,6 +364,7 @@ public final class CurseForgeRemoteAddonRepository implements RemoteAddonReposit
|
||||
SEMAPHORE.acquireUninterruptibly();
|
||||
try {
|
||||
Response<List<Category>> categories = withApiKey(HttpRequest.GET(PREFIX + "/v1/categories", pair("gameId", "432")))
|
||||
.retry(DEFAULT_RETRY_COUNT)
|
||||
.getJson(Response.typeOf(listTypeOf(Category.class)));
|
||||
return reorganizeCategories(categories.data(), section).stream().map(Category::toCategory);
|
||||
} finally {
|
||||
@@ -598,6 +583,22 @@ public final class CurseForgeRemoteAddonRepository implements RemoteAddonReposit
|
||||
default -> RemoteAddon.VersionType.Release;
|
||||
};
|
||||
|
||||
Map<String, String> knownHashes;
|
||||
if (hashes != null) {
|
||||
knownHashes = new HashMap<>();
|
||||
for (LatestFileHash hash : hashes) {
|
||||
if (hash.value != null) {
|
||||
switch (hash.algo) {
|
||||
case 1 -> knownHashes.put("sha1", hash.value);
|
||||
case 2 -> knownHashes.put("md5", hash.value);
|
||||
}
|
||||
}
|
||||
}
|
||||
knownHashes = Map.copyOf(knownHashes);
|
||||
} else {
|
||||
knownHashes = Map.of();
|
||||
}
|
||||
|
||||
return new RemoteAddon.Version(
|
||||
this,
|
||||
Integer.toString(id()),
|
||||
@@ -606,7 +607,7 @@ public final class CurseForgeRemoteAddonRepository implements RemoteAddonReposit
|
||||
fileName(),
|
||||
fileDate(),
|
||||
versionType,
|
||||
new RemoteAddon.File(Collections.emptyMap(), downloadUrl(), fileName()),
|
||||
new RemoteAddon.File(knownHashes, downloadUrl(), fileName()),
|
||||
dependencies.stream().map(dependency -> {
|
||||
if (!RELATION_TYPE.containsKey(dependency.relationType())) {
|
||||
throw new IllegalStateException("Broken datas.");
|
||||
@@ -614,13 +615,10 @@ public final class CurseForgeRemoteAddonRepository implements RemoteAddonReposit
|
||||
return RemoteAddon.Dependency.ofGeneral(RELATION_TYPE.get(dependency.relationType()), RemoteAddon.Source.CURSEFORGE, Integer.toString(dependency.modId()));
|
||||
}).distinct().filter(Objects::nonNull).collect(Collectors.toList()),
|
||||
gameVersions.stream().filter(GameVersionNumber::isKnown).toList(),
|
||||
gameVersions.stream().flatMap(version -> {
|
||||
if ("fabric".equalsIgnoreCase(version)) return Stream.of(ModLoaderType.FABRIC);
|
||||
else if ("forge".equalsIgnoreCase(version)) return Stream.of(ModLoaderType.FORGE);
|
||||
else if ("quilt".equalsIgnoreCase(version)) return Stream.of(ModLoaderType.QUILT);
|
||||
else if ("neoforge".equalsIgnoreCase(version)) return Stream.of(ModLoaderType.NEO_FORGE);
|
||||
else return Stream.empty();
|
||||
}).collect(Collectors.toList())
|
||||
gameVersions.stream()
|
||||
.filter(AddonLoader::mightBeLoader)
|
||||
.map(AddonLoader::of)
|
||||
.toList()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
+12
-13
@@ -19,9 +19,9 @@ package org.jackhuang.hmcl.addon.repository;
|
||||
|
||||
import com.google.gson.annotations.SerializedName;
|
||||
import com.google.gson.reflect.TypeToken;
|
||||
import org.jackhuang.hmcl.addon.AddonLoader;
|
||||
import org.jackhuang.hmcl.addon.RemoteAddon;
|
||||
import org.jackhuang.hmcl.addon.RemoteAddonRepository;
|
||||
import org.jackhuang.hmcl.addon.mod.ModLoaderType;
|
||||
import org.jackhuang.hmcl.download.DownloadProvider;
|
||||
import org.jackhuang.hmcl.util.DigestUtils;
|
||||
import org.jackhuang.hmcl.util.Immutable;
|
||||
@@ -146,11 +146,11 @@ public final class ModrinthRemoteAddonRepository implements RemoteAddonRepositor
|
||||
|
||||
private static String convertSortType(SortType sortType) {
|
||||
return switch (sortType) {
|
||||
case RELEVANCY -> "relevance";
|
||||
case POPULARITY -> "follows";
|
||||
case DATE_CREATED -> "newest";
|
||||
case POPULARITY, NAME, AUTHOR -> "relevance";
|
||||
case LAST_UPDATED -> "updated";
|
||||
case TOTAL_DOWNLOADS -> "downloads";
|
||||
default -> throw new IllegalArgumentException("Unsupported sort type " + sortType);
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
@@ -490,15 +497,7 @@ public final class ModrinthRemoteAddonRepository implements RemoteAddonRepositor
|
||||
return RemoteAddon.Dependency.ofGeneral(DEPENDENCY_TYPE.get(dependency.dependencyType), RemoteAddon.Source.MODRINTH, dependency.projectId);
|
||||
}).filter(Objects::nonNull).collect(Collectors.toList()),
|
||||
gameVersions,
|
||||
loaders.stream().flatMap(loader -> {
|
||||
if ("fabric".equalsIgnoreCase(loader)) return Stream.of(ModLoaderType.FABRIC);
|
||||
else if ("forge".equalsIgnoreCase(loader)) return Stream.of(ModLoaderType.FORGE);
|
||||
else if ("legacy-fabric".equalsIgnoreCase(loader)) return Stream.of(ModLoaderType.LEGACY_FABRIC);
|
||||
else if ("neoforge".equalsIgnoreCase(loader)) return Stream.of(ModLoaderType.NEO_FORGE);
|
||||
else if ("quilt".equalsIgnoreCase(loader)) return Stream.of(ModLoaderType.QUILT);
|
||||
else if ("liteloader".equalsIgnoreCase(loader)) return Stream.of(ModLoaderType.LITE_LOADER);
|
||||
else return Stream.empty();
|
||||
}).collect(Collectors.toList())
|
||||
loaders.stream().map(AddonLoader::of).toList()
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -114,16 +114,16 @@ public sealed abstract class ResourcePackFile extends LocalAddonFile implements
|
||||
|
||||
@Nullable
|
||||
@Contract(pure = true)
|
||||
public abstract PackMcMeta getMeta();
|
||||
public abstract PackMcMeta.PackInfo getPackInfo();
|
||||
|
||||
@Nullable
|
||||
public LocalAddonFile.Description getDescription() {
|
||||
if (getMeta() == null || getMeta().pack() == null) return null;
|
||||
return getMeta().pack().description();
|
||||
if (getPackInfo() == null) return null;
|
||||
return getPackInfo().description();
|
||||
}
|
||||
|
||||
// 64*64
|
||||
public abstract @Nullable Image getIcon();
|
||||
public abstract @Nullable Image loadIcon();
|
||||
|
||||
@Override
|
||||
public int compareTo(@NotNull ResourcePackFile other) {
|
||||
|
||||
+16
-19
@@ -30,8 +30,7 @@ import java.nio.file.Path;
|
||||
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
|
||||
|
||||
final class ResourcePackFolder extends ResourcePackFile {
|
||||
private final PackMcMeta meta;
|
||||
private final @Nullable Image icon;
|
||||
private final PackMcMeta.PackInfo info;
|
||||
|
||||
public ResourcePackFolder(ResourcePackManager manager, Path path) {
|
||||
super(manager, path);
|
||||
@@ -42,33 +41,31 @@ final class ResourcePackFolder extends ResourcePackFile {
|
||||
} catch (Exception e) {
|
||||
LOG.warning("Failed to parse resource pack meta", e);
|
||||
}
|
||||
this.meta = meta;
|
||||
this.info = meta != null ? meta.pack() : null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public PackMcMeta.PackInfo getPackInfo() {
|
||||
return info;
|
||||
}
|
||||
|
||||
@Override
|
||||
public @Nullable Image loadIcon() {
|
||||
byte[] iconData = null;
|
||||
Image iconTemp = null;
|
||||
try {
|
||||
iconData = Files.readAllBytes(path.resolve("pack.png"));
|
||||
} catch (IOException e) {
|
||||
LOG.warning("Failed to read resource pack icon", e);
|
||||
iconData = Files.readAllBytes(getFile().resolve("pack.png"));
|
||||
} catch (Exception e) {
|
||||
LOG.warning("Failed to load resource pack icon", e);
|
||||
}
|
||||
|
||||
if (iconData != null) {
|
||||
try (ByteArrayInputStream inputStream = new ByteArrayInputStream(iconData)) {
|
||||
iconTemp = new Image(inputStream, 64, 64, true, true);
|
||||
return new Image(inputStream, 64, 64, true, true);
|
||||
} catch (Exception e) {
|
||||
LOG.warning("Failed to load resource pack icon", e);
|
||||
}
|
||||
}
|
||||
this.icon = iconTemp;
|
||||
}
|
||||
|
||||
@Override
|
||||
public PackMcMeta getMeta() {
|
||||
return meta;
|
||||
}
|
||||
|
||||
@Override
|
||||
public @Nullable Image getIcon() {
|
||||
return icon;
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
+4
-4
@@ -522,18 +522,18 @@ public final class ResourcePackManager extends LocalAddonManager<ResourcePackFil
|
||||
}
|
||||
|
||||
public ResourcePackFile.Compatibility getCompatibility(@NotNull ResourcePackFile resourcePack) {
|
||||
if (resourcePack.getMeta() == null || resourcePack.getMeta().pack() == null)
|
||||
if (resourcePack.getPackInfo() == null)
|
||||
return ResourcePackFile.Compatibility.MISSING_PACK_META;
|
||||
if (this.getRequiredVersion().isUnspecified())
|
||||
return ResourcePackFile.Compatibility.MISSING_GAME_META;
|
||||
int requiredMajor = getRequiredVersion().majorVersion();
|
||||
VersionRange<PackMcMeta.PackVersion> versionRange;
|
||||
if (requiredMajor > 64)
|
||||
versionRange = getResourcePackVersionRangeNew(resourcePack.getMeta().pack());
|
||||
versionRange = getResourcePackVersionRangeNew(resourcePack.getPackInfo());
|
||||
else if (requiredMajor > 15)
|
||||
versionRange = getResourcePackVersionRangeOld(resourcePack.getMeta().pack());
|
||||
versionRange = getResourcePackVersionRangeOld(resourcePack.getPackInfo());
|
||||
else
|
||||
versionRange = getResourcePackVersionRangeOldest(resourcePack.getMeta().pack());
|
||||
versionRange = getResourcePackVersionRangeOldest(resourcePack.getPackInfo());
|
||||
if (versionRange.isEmpty())
|
||||
return ResourcePackFile.Compatibility.INVALID;
|
||||
if (versionRange.getMaximum().compareTo(this.getRequiredVersion()) < 0)
|
||||
|
||||
+20
-25
@@ -36,52 +36,47 @@ import java.util.Optional;
|
||||
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
|
||||
|
||||
final class ResourcePackZipFile extends ResourcePackFile {
|
||||
private final PackMcMeta meta;
|
||||
private final @Nullable Image icon;
|
||||
private final PackMcMeta.PackInfo info;
|
||||
|
||||
public ResourcePackZipFile(ResourcePackManager manager, Path path) throws IOException {
|
||||
super(manager, path);
|
||||
|
||||
PackMcMeta metaTemp = null;
|
||||
byte[] iconData = null;
|
||||
|
||||
PackMcMeta meta = null;
|
||||
try (var zipFileTree = CompressingUtils.openZipTree(path)) {
|
||||
try {
|
||||
metaTemp = PackMcMeta.fromNonNullJson(zipFileTree.readTextEntry("/pack.mcmeta"));
|
||||
meta = PackMcMeta.fromNonNullJson(zipFileTree.readTextEntry("/pack.mcmeta"));
|
||||
} catch (Exception e) {
|
||||
LOG.warning("Failed to parse resource pack meta", e);
|
||||
}
|
||||
}
|
||||
this.info = meta != null ? meta.pack() : null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public PackMcMeta.PackInfo getPackInfo() {
|
||||
return info;
|
||||
}
|
||||
|
||||
@Override
|
||||
public @Nullable Image loadIcon() {
|
||||
byte[] iconData = null;
|
||||
try (var zipFileTree = CompressingUtils.openZipTree(getFile())) {
|
||||
var iconEntry = zipFileTree.getEntry("/pack.png");
|
||||
if (iconEntry != null) {
|
||||
try {
|
||||
iconData = zipFileTree.readBinaryEntry(iconEntry);
|
||||
} catch (Exception e) {
|
||||
LOG.warning("Failed to load resource pack icon", e);
|
||||
}
|
||||
iconData = zipFileTree.readBinaryEntry(iconEntry);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
LOG.warning("Failed to load resource pack icon", e);
|
||||
}
|
||||
this.meta = metaTemp;
|
||||
|
||||
Image iconTemp = null;
|
||||
if (iconData != null) {
|
||||
try (ByteArrayInputStream inputStream = new ByteArrayInputStream(iconData)) {
|
||||
iconTemp = new Image(inputStream, 64, 64, true, true);
|
||||
return new Image(inputStream, 64, 64, true, true);
|
||||
} catch (Exception e) {
|
||||
LOG.warning("Failed to load resource pack icon", e);
|
||||
}
|
||||
}
|
||||
this.icon = iconTemp;
|
||||
}
|
||||
|
||||
@Override
|
||||
public PackMcMeta getMeta() {
|
||||
return meta;
|
||||
}
|
||||
|
||||
@Override
|
||||
public @Nullable Image getIcon() {
|
||||
return icon;
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -36,7 +36,6 @@ public abstract class YggdrasilAccount extends ClassicAccount {
|
||||
protected final UUID profileID;
|
||||
protected final String loginName;
|
||||
|
||||
private boolean authenticated = false;
|
||||
private YggdrasilSession session;
|
||||
|
||||
protected YggdrasilAccount(AccountID accountID, YggdrasilService service, String loginName, YggdrasilSession session) {
|
||||
@@ -72,7 +71,6 @@ public abstract class YggdrasilAccount extends ClassicAccount {
|
||||
}
|
||||
|
||||
profileID = session.getSelectedProfile().getId();
|
||||
authenticated = true;
|
||||
|
||||
addProfilePropertiesListener();
|
||||
}
|
||||
@@ -101,37 +99,33 @@ public abstract class YggdrasilAccount extends ClassicAccount {
|
||||
return session.getSelectedProfile().getId();
|
||||
}
|
||||
|
||||
/// Returns authentication information after validating the current token.
|
||||
/// Refreshes the session if the profile name is missing or the token is invalid.
|
||||
@Override
|
||||
public synchronized AuthInfo logIn() throws AuthenticationException {
|
||||
if (!authenticated || !session.hasProfileName()) {
|
||||
if (session.hasProfileName() && service.validate(session.getAccessToken(), session.getClientToken())) {
|
||||
authenticated = true;
|
||||
} else {
|
||||
YggdrasilSession acquiredSession;
|
||||
try {
|
||||
acquiredSession = service.refresh(session.getAccessToken(), session.getClientToken(), null);
|
||||
} catch (RemoteAuthenticationException e) {
|
||||
if ("ForbiddenOperationException".equals(e.getRemoteName())) {
|
||||
throw new CredentialExpiredException(e);
|
||||
} else {
|
||||
throw e;
|
||||
}
|
||||
if (!session.hasProfileName() || !service.validate(session.getAccessToken(), session.getClientToken())) {
|
||||
YggdrasilSession acquiredSession;
|
||||
try {
|
||||
acquiredSession = service.refresh(session.getAccessToken(), session.getClientToken(), null);
|
||||
} catch (RemoteAuthenticationException e) {
|
||||
if ("ForbiddenOperationException".equals(e.getRemoteName())) {
|
||||
throw new CredentialExpiredException(e);
|
||||
} else {
|
||||
throw e;
|
||||
}
|
||||
if (acquiredSession.getSelectedProfile() == null ||
|
||||
!acquiredSession.getSelectedProfile().getId().equals(profileID)) {
|
||||
throw new ServerResponseMalformedException("Selected profile changed");
|
||||
}
|
||||
if (!acquiredSession.hasProfileName()) {
|
||||
throw new ServerResponseMalformedException("Profile name is missing");
|
||||
}
|
||||
|
||||
session = acquiredSession;
|
||||
|
||||
authenticated = true;
|
||||
invalidate();
|
||||
}
|
||||
}
|
||||
if (acquiredSession.getSelectedProfile() == null ||
|
||||
!acquiredSession.getSelectedProfile().getId().equals(profileID)) {
|
||||
throw new ServerResponseMalformedException("Selected profile changed");
|
||||
}
|
||||
if (!acquiredSession.hasProfileName()) {
|
||||
throw new ServerResponseMalformedException("Profile name is missing");
|
||||
}
|
||||
|
||||
session = acquiredSession;
|
||||
|
||||
invalidate();
|
||||
}
|
||||
return session.toAuthInfo();
|
||||
}
|
||||
|
||||
@@ -161,7 +155,6 @@ public abstract class YggdrasilAccount extends ClassicAccount {
|
||||
session = acquiredSession;
|
||||
}
|
||||
|
||||
authenticated = true;
|
||||
invalidate();
|
||||
return session.toAuthInfo();
|
||||
}
|
||||
@@ -196,7 +189,6 @@ public abstract class YggdrasilAccount extends ClassicAccount {
|
||||
|
||||
@Override
|
||||
public void clearCache() {
|
||||
authenticated = false;
|
||||
service.getProfileRepository().invalidate(profileID);
|
||||
}
|
||||
|
||||
|
||||
@@ -26,6 +26,7 @@ import org.jackhuang.hmcl.download.neoforge.NeoForgeInstallTask;
|
||||
import org.jackhuang.hmcl.download.optifine.OptiFineInstallTask;
|
||||
import org.jackhuang.hmcl.game.*;
|
||||
import org.jackhuang.hmcl.task.Task;
|
||||
import org.jackhuang.hmcl.util.function.ExceptionalFunction;
|
||||
import org.jackhuang.hmcl.util.io.FileUtils;
|
||||
import org.jackhuang.hmcl.util.versioning.GameVersionNumber;
|
||||
import org.jetbrains.annotations.NotNullByDefault;
|
||||
@@ -36,6 +37,7 @@ import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
import java.util.regex.Matcher;
|
||||
|
||||
/// Provides downloads and game-component installation for one game repository.
|
||||
@@ -163,6 +165,69 @@ public class DefaultDependencyManager extends AbstractDependencyManager {
|
||||
}
|
||||
}
|
||||
|
||||
/// Creates a task that updates an instance's manifest and its own client JAR in one draft.
|
||||
///
|
||||
/// The updater receives the published instance and must return a manifest with the same id.
|
||||
/// If the updated manifest uses this instance's own JAR, that JAR is replaced when the resolved
|
||||
/// client download changes or the previous manifest used another instance's JAR. Downloads are
|
||||
/// compared by SHA-1 when either declares a checksum, otherwise by URL and size. Matching
|
||||
/// checksums identify the same client even when the URLs differ. Referenced JARs are not modified.
|
||||
///
|
||||
/// A replacement is obtained through this manager's download provider and cache before the
|
||||
/// manifest and JAR are committed together. A task failure aborts the draft and retains the
|
||||
/// previously published instance files. Shared cache files written by the tasks are retained.
|
||||
///
|
||||
/// @param <E> the checked exception type thrown while creating the update task
|
||||
/// @param instanceId the instance to update
|
||||
/// @param updater the asynchronous manifest update
|
||||
/// @return the task that commits the updated instance
|
||||
public <E extends Exception> Task<Void> updateInstanceAsync(
|
||||
GameInstanceID instanceId,
|
||||
ExceptionalFunction<GameInstance, Task<GameInstanceManifest>, E> updater) {
|
||||
return Task.composeAsync(() -> {
|
||||
DefaultGameRepositoryDraft draft = repository.openDraft();
|
||||
try {
|
||||
GameInstance instance = draft.getBaseSnapshot().getInstance(instanceId);
|
||||
return Objects.requireNonNull(updater.apply(instance), "Instance updater returned null")
|
||||
.thenComposeAsync(manifest -> {
|
||||
if (!instanceId.equals(manifest.id())) {
|
||||
throw new IllegalArgumentException(
|
||||
"Instance updater changed id from " + instanceId + " to " + manifest.id());
|
||||
}
|
||||
GameInstanceManifest previous = instance.getResolvedManifest();
|
||||
GameInstanceManifest resolved = draft.getBaseSnapshot().resolve(manifest);
|
||||
draft.put(manifest);
|
||||
if (instanceId.equals(resolved.jar())
|
||||
&& (!instanceId.equals(previous.jar())
|
||||
|| !isSameClientDownload(previous.getDownloadInfo(), resolved.getDownloadInfo()))) {
|
||||
return new GameDownloadTask(this, resolved).thenAcceptAsync(
|
||||
jar -> draft.putPrimaryJar(instanceId, jar));
|
||||
}
|
||||
return null;
|
||||
}).thenRunAsync(() -> {
|
||||
draft.commit();
|
||||
}).whenComplete(exception -> {
|
||||
if (draft.isOpen()) {
|
||||
draft.abort();
|
||||
}
|
||||
});
|
||||
} catch (Throwable exception) {
|
||||
abortDraftAfterFailure(draft, exception);
|
||||
throw exception;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/// Compares client identity by checksum, falling back to URL and size when neither has one.
|
||||
private static boolean isSameClientDownload(DownloadInfo previous, DownloadInfo updated) {
|
||||
@Nullable String previousSha1 = previous.getSha1();
|
||||
@Nullable String updatedSha1 = updated.getSha1();
|
||||
if (previousSha1 != null || updatedSha1 != null) {
|
||||
return previousSha1 != null && previousSha1.equalsIgnoreCase(updatedSha1);
|
||||
}
|
||||
return previous.getUrl().equals(updated.getUrl()) && previous.getSize() == updated.getSize();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Task<?> checkGameCompletionAsync(
|
||||
GameInstance instance,
|
||||
|
||||
@@ -207,6 +207,22 @@ public class DefaultGameBuilder extends GameBuilder {
|
||||
}
|
||||
}
|
||||
|
||||
/// Aborts this builder's open draft, including after ownership has passed to its build task.
|
||||
///
|
||||
/// If [#buildAsync()] has returned a task, the caller must ensure its executor has stopped
|
||||
/// before invoking this method. This operation does not cancel or wait for task execution.
|
||||
/// It has no effect if the draft has already committed or aborted, and does not undo a
|
||||
/// committed installation. Subsequent calls have no effect after cleanup has completed or
|
||||
/// failed. Shared download caches are retained.
|
||||
///
|
||||
/// @throws IOException if reserved instance files cannot be removed; draft exclusivity is
|
||||
/// released even in this case
|
||||
public void abort() throws IOException {
|
||||
if (draft.isOpen()) {
|
||||
draft.abort();
|
||||
}
|
||||
}
|
||||
|
||||
/// Performs repository-specific initialization after an instance is committed.
|
||||
///
|
||||
/// This method is invoked after the repository has published the committed snapshot and before
|
||||
|
||||
+11
-69
@@ -22,6 +22,7 @@ import org.jackhuang.hmcl.download.ComponentVersionList;
|
||||
import org.jackhuang.hmcl.task.GetTask;
|
||||
import org.jackhuang.hmcl.task.Task;
|
||||
import org.jackhuang.hmcl.util.StringUtils;
|
||||
import org.jackhuang.hmcl.util.gson.JsonSerializable;
|
||||
import org.jackhuang.hmcl.util.versioning.VersionNumber;
|
||||
|
||||
import java.util.Collections;
|
||||
@@ -79,17 +80,17 @@ public final class OptiFineBMCLVersionList extends ComponentVersionList<OptiFine
|
||||
versions.clear();
|
||||
Set<String> duplicates = new HashSet<>();
|
||||
for (OptiFineVersion element : root) {
|
||||
String version = element.getType() + "_" + element.getPatch();
|
||||
String mirror = apiRoot + "/optifine/" + toLookupVersion(element.getGameVersion()) + "/" + element.getType() + "/" + element.getPatch();
|
||||
String version = element.type() + "_" + element.patch();
|
||||
String mirror = apiRoot + "/optifine/" + toLookupVersion(element.gameVersion()) + "/" + element.type() + "/" + element.patch();
|
||||
if (!duplicates.add(mirror))
|
||||
continue;
|
||||
|
||||
boolean isPre = element.getPatch() != null && (element.getPatch().startsWith("pre") || element.getPatch().startsWith("alpha"));
|
||||
boolean isPre = element.patch() != null && (element.patch().startsWith("pre") || element.patch().startsWith("alpha"));
|
||||
|
||||
if (StringUtils.isBlank(element.getGameVersion()))
|
||||
if (StringUtils.isBlank(element.gameVersion()))
|
||||
continue;
|
||||
|
||||
String gameVersion = VersionNumber.normalize(fromLookupVersion(element.getGameVersion()));
|
||||
String gameVersion = VersionNumber.normalize(fromLookupVersion(element.gameVersion()));
|
||||
versions.put(gameVersion, new OptiFineRemoteVersion(gameVersion, version, Collections.singletonList(mirror), isPre));
|
||||
}
|
||||
} finally {
|
||||
@@ -97,73 +98,14 @@ public final class OptiFineBMCLVersionList extends ComponentVersionList<OptiFine
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @author huangyuhui
|
||||
*/
|
||||
private static final class OptiFineVersion {
|
||||
|
||||
@SerializedName("dl")
|
||||
private final String downloadLink;
|
||||
|
||||
@SerializedName("ver")
|
||||
private final String version;
|
||||
|
||||
@SerializedName("date")
|
||||
private final String date;
|
||||
|
||||
@SerializedName("type")
|
||||
private final String type;
|
||||
|
||||
@SerializedName("patch")
|
||||
private final String patch;
|
||||
|
||||
@SerializedName("mirror")
|
||||
private final String mirror;
|
||||
|
||||
@SerializedName("mcversion")
|
||||
private final String gameVersion;
|
||||
|
||||
public OptiFineVersion() {
|
||||
this(null, null, null, null, null, null, null);
|
||||
}
|
||||
|
||||
public OptiFineVersion(String downloadLink, String version, String date, String type, String patch, String mirror, String gameVersion) {
|
||||
this.downloadLink = downloadLink;
|
||||
this.version = version;
|
||||
this.date = date;
|
||||
this.type = type;
|
||||
this.patch = patch;
|
||||
this.mirror = mirror;
|
||||
this.gameVersion = gameVersion;
|
||||
}
|
||||
|
||||
public String getDownloadLink() {
|
||||
return downloadLink;
|
||||
}
|
||||
|
||||
public String getVersion() {
|
||||
return version;
|
||||
}
|
||||
|
||||
public String getDate() {
|
||||
return date;
|
||||
}
|
||||
|
||||
public String getType() {
|
||||
return type;
|
||||
}
|
||||
|
||||
public String getPatch() {
|
||||
return patch;
|
||||
}
|
||||
|
||||
public String getMirror() {
|
||||
return mirror;
|
||||
}
|
||||
|
||||
public String getGameVersion() {
|
||||
return gameVersion;
|
||||
}
|
||||
@JsonSerializable
|
||||
private record OptiFineVersion(@SerializedName("dl") String downloadLink, @SerializedName("ver") String version,
|
||||
@SerializedName("date") String date, @SerializedName("type") String type,
|
||||
@SerializedName("patch") String patch, @SerializedName("mirror") String mirror,
|
||||
@SerializedName("mcversion") String gameVersion) {
|
||||
}
|
||||
}
|
||||
@@ -24,7 +24,6 @@ import javafx.beans.property.ReadOnlyObjectProperty;
|
||||
import javafx.beans.property.SimpleObjectProperty;
|
||||
import org.jackhuang.hmcl.task.Task;
|
||||
import org.jackhuang.hmcl.util.Lang;
|
||||
import org.jackhuang.hmcl.util.function.ExceptionalFunction;
|
||||
import org.jackhuang.hmcl.util.gson.JsonUtils;
|
||||
import org.jackhuang.hmcl.util.io.FileUtils;
|
||||
import org.jetbrains.annotations.NotNullByDefault;
|
||||
@@ -293,6 +292,8 @@ public abstract class DefaultGameRepository implements GameRepository {
|
||||
LOG.warning("Failed to load instance", e);
|
||||
}
|
||||
}
|
||||
|
||||
newSnapshot.removeInvalidInstances();
|
||||
} catch (IOException e) {
|
||||
LOG.warning("Failed to load instance from " + instancesDir, e);
|
||||
}
|
||||
@@ -523,57 +524,6 @@ public abstract class DefaultGameRepository implements GameRepository {
|
||||
return Task.supplyAsync(() -> save(instanceManifest));
|
||||
}
|
||||
|
||||
/// Creates a task that updates one registered instance inside an exclusive draft.
|
||||
///
|
||||
/// The updater receives the instance from the immutable published snapshot and must return a
|
||||
/// working manifest with the same id. Its result is staged and committed exactly once. Failure
|
||||
/// or cancellation aborts the draft; shared cache files written by the updater are retained.
|
||||
///
|
||||
/// @param <E> the checked exception type thrown while creating the update task
|
||||
/// @param instanceId the instance to update
|
||||
/// @param updater the asynchronous manifest update
|
||||
/// @return the task that commits the updated manifest
|
||||
public <E extends Exception> Task<Void> updateInstanceAsync(
|
||||
GameInstanceID instanceId,
|
||||
ExceptionalFunction<GameInstance, Task<GameInstanceManifest>, E> updater) {
|
||||
return Task.composeAsync(() -> {
|
||||
DefaultGameRepositoryDraft draft = openDraft();
|
||||
try {
|
||||
return Objects.requireNonNull(
|
||||
updater.apply(draft.getBaseSnapshot().getInstance(instanceId)),
|
||||
"Instance updater returned null")
|
||||
.thenApplyAsync(manifest -> {
|
||||
if (!instanceId.equals(manifest.id())) {
|
||||
throw new IllegalArgumentException(
|
||||
"Instance updater changed id from " + instanceId + " to " + manifest.id());
|
||||
}
|
||||
draft.put(manifest);
|
||||
draft.commit();
|
||||
return manifest;
|
||||
}).whenComplete(exception -> {
|
||||
if (draft.isOpen()) {
|
||||
draft.abort();
|
||||
}
|
||||
});
|
||||
} catch (Throwable exception) {
|
||||
abortDraftAfterFailure(draft, exception);
|
||||
throw exception;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/// Aborts a draft after task construction fails and preserves any cleanup failure.
|
||||
///
|
||||
/// @param draft the draft to abort
|
||||
/// @param failure the failure that prevented task construction
|
||||
private static void abortDraftAfterFailure(DefaultGameRepositoryDraft draft, Throwable failure) {
|
||||
try {
|
||||
draft.abort();
|
||||
} catch (Exception cleanupFailure) {
|
||||
failure.addSuppressed(cleanupFailure);
|
||||
}
|
||||
}
|
||||
|
||||
/// Creates an empty unsealed snapshot for the given layout.
|
||||
///
|
||||
/// @param layout the layout for the new snapshot
|
||||
|
||||
@@ -361,6 +361,7 @@ public final class DefaultGameRepositoryDraft implements GameRepositoryDraft {
|
||||
: repository.createInstance(committedSnapshot, manifest.id(), manifest);
|
||||
committedSnapshot.put(updated);
|
||||
}
|
||||
committedSnapshot.removeInvalidInstances();
|
||||
committedSnapshot.seal();
|
||||
return committedSnapshot;
|
||||
}
|
||||
|
||||
@@ -160,6 +160,51 @@ public class DefaultGameRepositorySnapshot implements GameRepositorySnapshot {
|
||||
instances.remove(id);
|
||||
}
|
||||
|
||||
/// Remove invalid instances.
|
||||
/// TODO: In the future we could retain these instances and show their status in the UI.
|
||||
void removeInvalidInstances() {
|
||||
checkMutable();
|
||||
|
||||
HashSet<GameInstanceID> visited = null;
|
||||
loop:
|
||||
for (DefaultGameInstance instance : List.copyOf(instances.values())) {
|
||||
if (instance.getManifest().inheritsFrom() == null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (visited != null)
|
||||
visited.clear();
|
||||
else
|
||||
visited = new HashSet<>();
|
||||
|
||||
DefaultGameInstance current = instance;
|
||||
visited.add(current.getId());
|
||||
while (current.getManifest().inheritsFrom() != null) {
|
||||
GameInstanceID parentId = current.getManifest().inheritsFrom();
|
||||
if (!visited.add(parentId)) {
|
||||
// DefaultGameRepositorySnapshot.resolve can handle circular references
|
||||
continue loop;
|
||||
}
|
||||
|
||||
DefaultGameInstance parent = instances.get(parentId);
|
||||
if (parent == null) {
|
||||
LOG.warning("Instance " + current.getId() + " inherits from missing instance " + parentId);
|
||||
|
||||
for (DefaultGameInstance toRemoved = instance; toRemoved != null; ) {
|
||||
instances.remove(toRemoved.getId());
|
||||
toRemoved = toRemoved.getManifest().inheritsFrom() != null
|
||||
? instances.get(toRemoved.getManifest().inheritsFrom())
|
||||
: null;
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
current = parent;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/// Creates an unpublished mutable copy with instances rebound to the copy.
|
||||
///
|
||||
/// The caller must call [#seal()] before retaining or publishing the result as a snapshot.
|
||||
|
||||
@@ -83,6 +83,7 @@ public enum GameComponentType {
|
||||
},
|
||||
FORGE("forge", ModLoaderType.FORGE) {
|
||||
private final Pattern FORGE_VERSION_MATCHER = Pattern.compile("^([0-9.]+)-(?<forge>[0-9.]+)(-([0-9.]+))?$");
|
||||
private final Set<String> ARTIFACTS = Set.of("forge", "fmlloader", "minecraftforge");
|
||||
|
||||
@Override
|
||||
protected @Nullable String getComponentVersion(GameInstanceManifest manifest, String libraryVersion) {
|
||||
@@ -95,13 +96,17 @@ public enum GameComponentType {
|
||||
|
||||
@Override
|
||||
protected boolean matchLibrary(Library library, List<Library> libraries) {
|
||||
for (Library l : libraries) {
|
||||
if (NEO_FORGE.matchLibrary(l, libraries)) {
|
||||
return false;
|
||||
if ("net.minecraftforge".equals(library.groupId()) && ARTIFACTS.contains(library.artifactId())) {
|
||||
for (Library l : libraries) {
|
||||
if (NEO_FORGE.matchLibrary(l, libraries)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
return "net.minecraftforge".equals(library.groupId()) && ("forge".equals(library.artifactId()) || "fmlloader".equals(library.artifactId()));
|
||||
return false;
|
||||
}
|
||||
},
|
||||
CLEANROOM("cleanroom", ModLoaderType.CLEANROOM) {
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -203,12 +203,9 @@ public record Library(
|
||||
return true;
|
||||
}
|
||||
|
||||
if (downloads != null && downloads.classifiers() != null
|
||||
&& downloads.classifiers().keySet().stream().anyMatch(s -> s.startsWith("native"))) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return this.artifact().getClassifier() != null && this.artifact().getClassifier().startsWith("natives-");
|
||||
return downloads != null
|
||||
&& downloads.classifiers() != null
|
||||
&& downloads.classifiers().keySet().stream().anyMatch(s -> s.startsWith("native"));
|
||||
}
|
||||
|
||||
public @Nullable LibraryDownloadInfo getRawDownloadInfo() {
|
||||
|
||||
@@ -279,6 +279,8 @@ 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);
|
||||
}
|
||||
@@ -134,7 +150,10 @@ public final class CurseCompletionTask extends Task<Void> {
|
||||
if (StringUtils.isBlank(file.fileName()) || file.url() == null) {
|
||||
try {
|
||||
RemoteAddon.File remoteFile = CurseForgeRemoteAddonRepository.MODS.getAddonFile(Integer.toString(file.projectID()), Integer.toString(file.fileID()));
|
||||
return file.withFileName(remoteFile.filename()).withURL(remoteFile.url());
|
||||
return file
|
||||
.withFileName(remoteFile.filename())
|
||||
.withURL(remoteFile.url())
|
||||
.withHashes(remoteFile.hashes());
|
||||
} catch (FileNotFoundException fof) {
|
||||
LOG.warning("Could not query api.curseforge.com for deleted mods: " + file.projectID() + ", " + file.fileID(), fof);
|
||||
notFound.set(true);
|
||||
@@ -148,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");
|
||||
@@ -158,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);
|
||||
@@ -165,10 +190,10 @@ public final class CurseCompletionTask extends Task<Void> {
|
||||
return Stream.empty();
|
||||
}
|
||||
|
||||
var task = new FileDownloadTask(f.url(), path);
|
||||
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.
|
||||
@@ -176,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,29 +19,97 @@ 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;
|
||||
import java.util.Objects;
|
||||
|
||||
/// @author huangyuhui
|
||||
@JsonSerializable
|
||||
public record CurseManifestFile(@SerializedName("projectID") int projectID,
|
||||
@SerializedName("fileID") int fileID,
|
||||
@SerializedName("fileName") String fileName,
|
||||
@SerializedName("url") String url,
|
||||
@SerializedName("required") boolean required) implements Validation {
|
||||
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") @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 {
|
||||
|
||||
/// 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)
|
||||
@@ -51,21 +119,64 @@ public record CurseManifestFile(@SerializedName("projectID") int projectID,
|
||||
}
|
||||
}
|
||||
|
||||
/// 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) {
|
||||
@Nullable String hash = hashes.get(algorithm.key());
|
||||
if (hash != null) {
|
||||
return new FileDownloadTask.IntegrityCheck(algorithm.value(), hash);
|
||||
}
|
||||
}
|
||||
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);
|
||||
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);
|
||||
return new CurseManifestFile(projectID, fileID, fileName, url, required, hashes, remoteAddon, addonQueried);
|
||||
}
|
||||
|
||||
/// 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);
|
||||
|
||||
@@ -26,43 +26,28 @@ import org.jackhuang.hmcl.util.Immutable;
|
||||
* https://addons-ecs.forgesvc.net/api/v2/addon/<projectID>/file/<fileID>
|
||||
*/
|
||||
@Immutable
|
||||
public final class CurseMetaMod {
|
||||
@SerializedName(value = "Id", alternate = "id")
|
||||
private final int id;
|
||||
public record CurseMetaMod(@SerializedName(value = "Id", alternate = "id") int id,
|
||||
@SerializedName(value = "FileName", alternate = "fileName") String fileName,
|
||||
@SerializedName(value = "FileNameOnDisk") String fileNameOnDisk,
|
||||
@SerializedName(value = "DownloadURL", alternate = "downloadUrl") String downloadURL) {
|
||||
|
||||
@SerializedName(value = "FileName", alternate = "fileName")
|
||||
private final String fileName;
|
||||
|
||||
@SerializedName(value = "FileNameOnDisk")
|
||||
private final String fileNameOnDisk;
|
||||
|
||||
@SerializedName(value = "DownloadURL", alternate = "downloadUrl")
|
||||
private final String downloadURL;
|
||||
|
||||
public CurseMetaMod() {
|
||||
this(0, "", "", "");
|
||||
}
|
||||
|
||||
public CurseMetaMod(int id, String fileName, String fileNameOnDisk, String downloadURL) {
|
||||
this.id = id;
|
||||
this.fileName = fileName;
|
||||
this.fileNameOnDisk = fileNameOnDisk;
|
||||
this.downloadURL = downloadURL;
|
||||
}
|
||||
|
||||
public int getId() {
|
||||
@Override
|
||||
public int id() {
|
||||
return id;
|
||||
}
|
||||
|
||||
public String getFileName() {
|
||||
@Override
|
||||
public String fileName() {
|
||||
return fileName;
|
||||
}
|
||||
|
||||
public String getFileNameOnDisk() {
|
||||
@Override
|
||||
public String fileNameOnDisk() {
|
||||
return fileNameOnDisk;
|
||||
}
|
||||
|
||||
public String getDownloadURL() {
|
||||
@Override
|
||||
public String downloadURL() {
|
||||
return downloadURL;
|
||||
}
|
||||
}
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
Loaded 100 of 167 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user