Compare commits
22
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1c0e506d34 | ||
|
|
6374ec0de7 | ||
|
|
a6b3872af4 | ||
|
|
ad98a5a6d4 | ||
|
|
05393af537 | ||
|
|
3438da2ab9 | ||
|
|
3163b465ec | ||
|
|
573dac471c | ||
|
|
12f54f353a | ||
|
|
246699d571 | ||
|
|
3480e6974b | ||
|
|
6d14351833 | ||
|
|
fd0d83257d | ||
|
|
0eb1d3cf4b | ||
|
|
f9b8dad3d2 | ||
|
|
57fbd0910e | ||
|
|
7767a57f50 | ||
|
|
f024dba940 | ||
|
|
52736d3c86 | ||
|
|
f75b472068 | ||
|
|
d8e0c9e9c7 | ||
|
|
9601c1c1ea |
No files matched your search
@@ -337,10 +337,15 @@ public final class HMCLGameRepository extends DefaultGameRepository {
|
||||
return;
|
||||
}
|
||||
|
||||
@Nullable GameSettingsPresetID legacyParent = gameDirectory.getLegacyGameSettings();
|
||||
if (SettingsManager.getGameSettings(legacyParent) == null) {
|
||||
legacyParent = null;
|
||||
}
|
||||
|
||||
LegacyGameSettingsMigrator.InstanceMigrationResult migrationResult =
|
||||
LegacyGameSettingsMigrator.migrateInstanceGameSettings(
|
||||
this, id,
|
||||
getParentGameSettings(null).idProperty().getValue());
|
||||
legacyParent);
|
||||
if (migrationResult != null) {
|
||||
initInstanceGameSettings(id, migrationResult.setting());
|
||||
try {
|
||||
@@ -351,14 +356,6 @@ public final class HMCLGameRepository extends DefaultGameRepository {
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
GameSettings.Preset gameDirectoryPreset = SettingsManager.getGameSettings(gameDirectory.getLegacyGameSettings());
|
||||
if (gameDirectoryPreset != null && gameDirectoryPreset.defaultIsolationTypeProperty().getValue() == DefaultIsolationType.ALWAYS) {
|
||||
GameSettings.Instance setting = new GameSettings.Instance();
|
||||
setting.getOverrideProperties().add(GameSettings.PROPERTY_RUNNING_DIRECTORY);
|
||||
initInstanceGameSettings(id, setting);
|
||||
saveGameSettings(id);
|
||||
}
|
||||
}
|
||||
|
||||
/// Loads a new-format instance game settings file.
|
||||
@@ -516,10 +513,9 @@ public final class HMCLGameRepository extends DefaultGameRepository {
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the explicit parent preset of the instance, falling back to the default preset.
|
||||
public GameSettings.Preset getParentGameSettings(@Nullable GameSettings.Instance instance) {
|
||||
@Nullable GameSettingsPresetID parent = instance != null && instance.parentProperty().getValue() != null
|
||||
? instance.parentProperty().getValue()
|
||||
: gameDirectory.getLegacyGameSettings();
|
||||
@Nullable GameSettingsPresetID parent = instance != null ? instance.parentProperty().getValue() : null;
|
||||
GameSettings.Preset parentSetting = SettingsManager.getGameSettings(parent);
|
||||
return parentSetting != null ? parentSetting : SettingsManager.getDefaultGameSettingsPresetOrCreate();
|
||||
}
|
||||
@@ -534,7 +530,8 @@ public final class HMCLGameRepository extends DefaultGameRepository {
|
||||
return;
|
||||
}
|
||||
|
||||
GameSettings.Preset preset = getParentGameSettings(null);
|
||||
GameSettings.Instance instanceSetting = getInstanceGameSettings(id);
|
||||
GameSettings.Preset preset = getParentGameSettings(instanceSetting);
|
||||
DefaultIsolationType type = Lang.requireNonNullElse(preset.defaultIsolationTypeProperty().getValue(), DefaultIsolationType.MODDED);
|
||||
boolean isolated = switch (type) {
|
||||
case NEVER -> false;
|
||||
@@ -543,7 +540,7 @@ public final class HMCLGameRepository extends DefaultGameRepository {
|
||||
};
|
||||
|
||||
if (isolated) {
|
||||
GameSettings.Instance setting = getInstanceGameSettingsOrCreate(id);
|
||||
GameSettings.Instance setting = instanceSetting != null ? instanceSetting : getInstanceGameSettingsOrCreate(id);
|
||||
if (setting != null) {
|
||||
setting.getOverrideProperties().add(GameSettings.PROPERTY_RUNNING_DIRECTORY);
|
||||
}
|
||||
@@ -698,6 +695,7 @@ public final class HMCLGameRepository extends DefaultGameRepository {
|
||||
GameSettings.Effective vs = getEffectiveGameSettings(version);
|
||||
boolean noJVMOptions = vs.getInheritable(GameSettings::noJVMOptionsProperty);
|
||||
boolean autoMemory = vs.get(GameSettings::autoMemoryProperty);
|
||||
GameVersionNumber gameVersionNumber = GameVersionNumber.asGameVersion(getGameVersion(version));
|
||||
|
||||
LaunchOptions.Builder builder = new LaunchOptions.Builder()
|
||||
.setGameDir(gameDir)
|
||||
@@ -709,7 +707,7 @@ public final class HMCLGameRepository extends DefaultGameRepository {
|
||||
.setOverrideJavaArguments(StringUtils.tokenize(vs.get(GameSettings::jvmOptionsProperty)))
|
||||
.setMaxMemory(noJVMOptions && autoMemory ? null : (int) (getAllocatedMemory(
|
||||
vs.getMaxMemory() * 1024L * 1024L,
|
||||
SystemInfo.getPhysicalMemoryStatus().getAvailable(),
|
||||
SystemInfo.getPhysicalMemoryStatus().available(),
|
||||
autoMemory
|
||||
) / 1024 / 1024))
|
||||
.setMinMemory(vs.get(GameSettings::minMemoryProperty))
|
||||
@@ -737,7 +735,7 @@ public final class HMCLGameRepository extends DefaultGameRepository {
|
||||
.setNativesDir(vs.get(GameSettings::nativesDirectoryProperty))
|
||||
.setProcessPriority(vs.getInheritable(GameSettings::processPriorityProperty))
|
||||
.setGraphicsBackend(vs.getInheritable(GameSettings::graphicsBackendProperty))
|
||||
.setRenderer(vs.getRenderer())
|
||||
.setRenderer(vs.getRenderer(gameVersionNumber))
|
||||
.setEnableDebugLogOutput(vs.getInheritable(GameSettings::enableDebugLogOutputProperty))
|
||||
.setAllowAutoAgent(vs.getInheritable(GameSettings::allowAutoAgentProperty))
|
||||
.setDisableAutoGameOptions(vs.getInheritable(GameSettings::disableAutoGameOptionsProperty))
|
||||
|
||||
@@ -34,7 +34,10 @@ import org.jackhuang.hmcl.launch.*;
|
||||
import org.jackhuang.hmcl.modpack.ModpackCompletionException;
|
||||
import org.jackhuang.hmcl.modpack.ModpackConfiguration;
|
||||
import org.jackhuang.hmcl.modpack.ModpackProvider;
|
||||
import org.jackhuang.hmcl.setting.*;
|
||||
import org.jackhuang.hmcl.setting.DownloadProviders;
|
||||
import org.jackhuang.hmcl.setting.GameSettings;
|
||||
import org.jackhuang.hmcl.setting.JavaVersionType;
|
||||
import org.jackhuang.hmcl.setting.LauncherVisibility;
|
||||
import org.jackhuang.hmcl.task.*;
|
||||
import org.jackhuang.hmcl.ui.*;
|
||||
import org.jackhuang.hmcl.ui.construct.DialogCloseEvent;
|
||||
@@ -57,6 +60,7 @@ import java.net.SocketTimeoutException;
|
||||
import java.net.URI;
|
||||
import java.nio.file.AccessDeniedException;
|
||||
import java.nio.file.Path;
|
||||
import java.time.Instant;
|
||||
import java.util.*;
|
||||
import java.util.concurrent.*;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
@@ -71,6 +75,7 @@ import static org.jackhuang.hmcl.util.DataSizeUnit.MEGABYTES;
|
||||
import static org.jackhuang.hmcl.util.Lang.resolveException;
|
||||
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
|
||||
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
|
||||
import static org.jackhuang.hmcl.util.logging.Logger.TIME_FORMATTER;
|
||||
import static org.jackhuang.hmcl.util.platform.Platform.SYSTEM_PLATFORM;
|
||||
|
||||
public final class LauncherHelper {
|
||||
@@ -172,7 +177,7 @@ public final class LauncherHelper {
|
||||
}),
|
||||
Task.composeAsync(() -> {
|
||||
if (OperatingSystem.CURRENT_OS != OperatingSystem.WINDOWS
|
||||
|| !(setting.getRenderer() instanceof Renderer.Driver renderer)
|
||||
|| !(setting.getRenderer(GameVersionNumber.asGameVersion(gameVersion)) instanceof Renderer.Driver renderer)
|
||||
|| renderer.mesaDriverName() == null)
|
||||
return null;
|
||||
|
||||
@@ -964,7 +969,7 @@ public final class LauncherHelper {
|
||||
@Override
|
||||
public void onExit(int exitCode, ExitType exitType) {
|
||||
if (showLogs) {
|
||||
logBuffer.add(new Log(String.format("[HMCL ProcessListener] Minecraft exit with code %d(0x%x), type is %s.", exitCode, exitCode, exitType), Log4jLevel.INFO));
|
||||
logBuffer.add(new Log(String.format("[%s] [HMCL ProcessListener] Minecraft exit with code %d(0x%x), type is %s.", TIME_FORMATTER.format(Instant.now()), exitCode, exitCode, exitType), Log4jLevel.INFO));
|
||||
submitLogThread.interrupt();
|
||||
try {
|
||||
submitLogThread.join();
|
||||
|
||||
@@ -26,6 +26,8 @@ import java.nio.file.FileSystem;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
|
||||
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
|
||||
|
||||
public class ManuallyCreatedModpackInstallTask extends Task<Path> {
|
||||
|
||||
private final Path zipFile;
|
||||
@@ -36,6 +38,8 @@ public class ManuallyCreatedModpackInstallTask extends Task<Path> {
|
||||
this.zipFile = zipFile;
|
||||
this.charset = charset;
|
||||
this.name = name;
|
||||
|
||||
setName(i18n("modpack.installing"));
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -45,7 +45,10 @@ import java.io.InputStream;
|
||||
import java.lang.ref.WeakReference;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.*;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.UUID;
|
||||
import java.util.WeakHashMap;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.concurrent.ThreadPoolExecutor;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
@@ -65,21 +68,10 @@ public final class TexturesLoader {
|
||||
}
|
||||
|
||||
// ==== Texture Loading ====
|
||||
public static class LoadedTexture {
|
||||
private final Image image;
|
||||
private final Map<String, String> metadata;
|
||||
|
||||
public LoadedTexture(Image image, Map<String, String> metadata) {
|
||||
this.image = requireNonNull(image);
|
||||
this.metadata = requireNonNull(metadata);
|
||||
}
|
||||
|
||||
public Image getImage() {
|
||||
return image;
|
||||
}
|
||||
|
||||
public Map<String, String> getMetadata() {
|
||||
return metadata;
|
||||
public record LoadedTexture(Image image, Map<String, String> metadata) {
|
||||
public LoadedTexture {
|
||||
requireNonNull(image);
|
||||
requireNonNull(metadata);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -158,7 +150,7 @@ public final class TexturesLoader {
|
||||
}
|
||||
|
||||
public static TextureModel getDefaultModel(UUID uuid) {
|
||||
return TextureModel.WIDE.modelName.equals(getDefaultSkin(uuid).getMetadata().get("model"))
|
||||
return TextureModel.WIDE.modelName.equals(getDefaultSkin(uuid).metadata().get("model"))
|
||||
? TextureModel.WIDE
|
||||
: TextureModel.SLIM;
|
||||
}
|
||||
|
||||
@@ -168,7 +168,7 @@ public final class HMCLJavaRepository implements JavaRepository {
|
||||
update.put("component", gameJavaVersion.component());
|
||||
|
||||
Map<String, JavaLocalFiles.Local> files = new LinkedHashMap<>();
|
||||
result.remoteFiles().getFiles().forEach((path, file) -> {
|
||||
result.remoteFiles().files().forEach((path, file) -> {
|
||||
if (file instanceof MojangJavaRemoteFiles.RemoteFile) {
|
||||
DownloadInfo downloadInfo = ((MojangJavaRemoteFiles.RemoteFile) file).getDownloads().get("raw");
|
||||
if (downloadInfo != null) {
|
||||
@@ -190,12 +190,12 @@ public final class HMCLJavaRepository implements JavaRepository {
|
||||
public Task<JavaRuntime> getInstallJavaTask(Platform platform, String name, Map<String, Object> update, Path archiveFile) {
|
||||
Path javaDir = getJavaDir(platform, name);
|
||||
return new JavaInstallTask(javaDir, update, archiveFile).thenApplyAsync(result -> {
|
||||
if (!result.getInfo().getPlatform().equals(platform))
|
||||
throw new IOException("Platform is mismatch: expected " + platform + " but got " + result.getInfo().getPlatform());
|
||||
if (!result.info().getPlatform().equals(platform))
|
||||
throw new IOException("Platform is mismatch: expected " + platform + " but got " + result.info().getPlatform());
|
||||
|
||||
Path executable = javaDir.resolve("bin").resolve(platform.getOperatingSystem().getJavaExecutable()).toRealPath();
|
||||
JsonUtils.writeToJsonFile(getManifestFile(platform, name), result);
|
||||
return JavaRuntime.of(executable, result.getInfo(), true);
|
||||
return JavaRuntime.of(executable, result.info(), true);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -34,33 +34,7 @@ import static org.jackhuang.hmcl.util.gson.JsonUtils.mapTypeOf;
|
||||
* @author Glavo
|
||||
*/
|
||||
@JsonAdapter(JavaManifest.Serializer.class)
|
||||
public final class JavaManifest {
|
||||
|
||||
private final JavaInfo info;
|
||||
|
||||
@Nullable
|
||||
private final Map<String, Object> update;
|
||||
|
||||
@Nullable
|
||||
private final Map<String, JavaLocalFiles.Local> files;
|
||||
|
||||
public JavaManifest(JavaInfo info, @Nullable Map<String, Object> update, @Nullable Map<String, JavaLocalFiles.Local> files) {
|
||||
this.info = info;
|
||||
this.update = update;
|
||||
this.files = files;
|
||||
}
|
||||
|
||||
public JavaInfo getInfo() {
|
||||
return info;
|
||||
}
|
||||
|
||||
public Map<String, Object> getUpdate() {
|
||||
return update;
|
||||
}
|
||||
|
||||
public Map<String, JavaLocalFiles.Local> getFiles() {
|
||||
return files;
|
||||
}
|
||||
public record JavaManifest(JavaInfo info, @Nullable Map<String, Object> update, @Nullable Map<String, JavaLocalFiles.Local> files) {
|
||||
|
||||
public static final class Serializer implements JsonSerializer<JavaManifest>, JsonDeserializer<JavaManifest> {
|
||||
|
||||
@@ -69,16 +43,16 @@ public final class JavaManifest {
|
||||
@Override
|
||||
public JsonElement serialize(JavaManifest src, Type typeOfSrc, JsonSerializationContext context) {
|
||||
JsonObject res = new JsonObject();
|
||||
res.addProperty("os.name", src.getInfo().getPlatform().getOperatingSystem().getCheckedName());
|
||||
res.addProperty("os.arch", src.getInfo().getPlatform().getArchitecture().getCheckedName());
|
||||
res.addProperty("java.version", src.getInfo().getVersion());
|
||||
res.addProperty("java.vendor", src.getInfo().getVendor());
|
||||
res.addProperty("os.name", src.info().getPlatform().getOperatingSystem().getCheckedName());
|
||||
res.addProperty("os.arch", src.info().getPlatform().getArchitecture().getCheckedName());
|
||||
res.addProperty("java.version", src.info().getVersion());
|
||||
res.addProperty("java.vendor", src.info().getVendor());
|
||||
|
||||
if (src.getUpdate() != null)
|
||||
res.add("update", context.serialize(src.getUpdate()));
|
||||
if (src.update() != null)
|
||||
res.add("update", context.serialize(src.update()));
|
||||
|
||||
if (src.getFiles() != null)
|
||||
res.add("files", context.serialize(src.getFiles(), LOCAL_FILES_TYPE));
|
||||
if (src.files() != null)
|
||||
res.add("files", context.serialize(src.files(), LOCAL_FILES_TYPE));
|
||||
|
||||
return res;
|
||||
}
|
||||
|
||||
@@ -996,11 +996,16 @@ public sealed abstract class GameSettings extends ObservableSetting {
|
||||
}
|
||||
|
||||
/// Returns the effective renderer.
|
||||
public Renderer getRenderer() {
|
||||
return switch (getInheritable(GameSettings::graphicsBackendProperty)) {
|
||||
public Renderer getRenderer(GameVersionNumber version) {
|
||||
GraphicsAPI graphicsAPI = getInheritable(GameSettings::graphicsBackendProperty);
|
||||
if (graphicsAPI == GraphicsAPI.DEFAULT) {
|
||||
graphicsAPI = GraphicsAPI.getDefaultGraphicsAPI(version);
|
||||
}
|
||||
|
||||
return switch (graphicsAPI) {
|
||||
case DEFAULT -> Renderer.DEFAULT;
|
||||
case OPENGL -> selectRenderer(GraphicsAPI.OPENGL, getInheritable(GameSettings::openGLRendererProperty));
|
||||
case VULKAN -> selectRenderer(GraphicsAPI.VULKAN, getInheritable(GameSettings::vulkanRendererProperty));
|
||||
case DEFAULT -> Renderer.DEFAULT;
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -129,6 +129,21 @@ public final class GameSettingsPresets extends ObservableSetting implements Json
|
||||
return id;
|
||||
}
|
||||
|
||||
/// Returns the first automatic preset number that is not used by existing presets.
|
||||
public int newPresetAutoNameNumber() {
|
||||
mainLoop:
|
||||
for (int index = 1; ; index++) {
|
||||
for (GameSettings.Preset setting : getPresets()) {
|
||||
Integer autoNameNumber = setting.autoNameNumberProperty().getValue();
|
||||
if ((autoNameNumber != null && autoNameNumber == index)) {
|
||||
continue mainLoop;
|
||||
}
|
||||
}
|
||||
|
||||
return index;
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the preset with the given ID.
|
||||
public GameSettings.@Nullable Preset getPreset(@Nullable GameSettingsPresetID id) {
|
||||
if (id == null) {
|
||||
|
||||
@@ -28,6 +28,7 @@ import org.jackhuang.hmcl.theme.NetworkBackgroundImageCachePolicy;
|
||||
import org.jackhuang.hmcl.util.StringUtils;
|
||||
import org.jackhuang.hmcl.util.gson.JsonSchema;
|
||||
import org.jackhuang.hmcl.util.gson.JsonUtils;
|
||||
import org.jackhuang.hmcl.util.io.FileUtils;
|
||||
import org.jackhuang.hmcl.util.io.JarUtils;
|
||||
import org.jetbrains.annotations.NotNullByDefault;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
@@ -276,6 +277,7 @@ public final class LegacyConfigMigrator {
|
||||
|
||||
GameSettingsPresets gameSettingsPresets = new GameSettingsPresets();
|
||||
migrateLegacyPresetSettings(gameDirectories, gameSettingsPresets, legacyConfigurations);
|
||||
migrateLegacyInstanceGameSettings(gameDirectories, gameSettingsPresets);
|
||||
migrateLegacyAllowAutoAgent(deserialized, gameSettingsPresets, legacyAllowAutoAgent);
|
||||
migrateLegacyDisableAutoGameOptions(deserialized, gameSettingsPresets, legacyDisableAutoGameOptions);
|
||||
DetachedSettings detachedSettings = new DetachedSettings(gameDirectories, gameSettingsPresets,
|
||||
@@ -1272,12 +1274,89 @@ public final class LegacyConfigMigrator {
|
||||
continue;
|
||||
}
|
||||
|
||||
legacyParent = LegacyGameSettingsMigrator.toPreset(legacyGameSettings, profileName, legacySettingObject);
|
||||
legacyParent = LegacyGameSettingsMigrator.toPreset(legacyGameSettings, gameSettingsPresets.newPresetAutoNameNumber(), legacySettingObject);
|
||||
gameSettingsPresets.getPresets().add(legacyParent);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Migrates existing instances under migrated legacy game directories to explicit instance settings.
|
||||
static void migrateLegacyInstanceGameSettings(
|
||||
GameDirectories gameDirectories,
|
||||
GameSettingsPresets gameSettingsPresets) {
|
||||
Objects.requireNonNull(gameDirectories);
|
||||
Objects.requireNonNull(gameSettingsPresets);
|
||||
|
||||
for (GameDirectory gameDirectory : gameDirectories.getGameDirectories()) {
|
||||
@Nullable GameSettingsPresetID legacyGameSettings = gameDirectory.getLegacyGameSettings();
|
||||
if (legacyGameSettings == null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
GameSettings.Preset legacyParent = gameSettingsPresets.getPreset(legacyGameSettings);
|
||||
if (legacyParent == null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
migrateLegacyInstanceGameSettings(gameDirectory.getPath().toPath(), legacyGameSettings, legacyParent);
|
||||
}
|
||||
}
|
||||
|
||||
/// Migrates existing instances under one game directory to explicit instance settings.
|
||||
private static void migrateLegacyInstanceGameSettings(
|
||||
Path gameDirectory,
|
||||
GameSettingsPresetID legacyGameSettings,
|
||||
GameSettings.Preset legacyParent) {
|
||||
Path versionsDirectory = gameDirectory.resolve("versions");
|
||||
if (!Files.isDirectory(versionsDirectory)) {
|
||||
return;
|
||||
}
|
||||
|
||||
try (var stream = Files.list(versionsDirectory)) {
|
||||
stream.filter(Files::isDirectory)
|
||||
.forEach(instanceRoot -> migrateLegacyInstanceGameSettingsInVersionRoot(
|
||||
instanceRoot,
|
||||
legacyGameSettings,
|
||||
legacyParent));
|
||||
} catch (IOException e) {
|
||||
LOG.warning("Failed to migrate legacy instance game settings under " + versionsDirectory, e);
|
||||
}
|
||||
}
|
||||
|
||||
/// Migrates one existing instance to explicit instance settings.
|
||||
private static void migrateLegacyInstanceGameSettingsInVersionRoot(
|
||||
Path instanceRoot,
|
||||
GameSettingsPresetID legacyGameSettings,
|
||||
GameSettings.Preset legacyParent) {
|
||||
@Nullable Path instanceId = instanceRoot.getFileName();
|
||||
if (instanceId == null || !Files.isRegularFile(instanceRoot.resolve(instanceId.toString() + ".json"))) {
|
||||
return;
|
||||
}
|
||||
|
||||
Path currentSettings = instanceRoot.resolve(LegacyGameSettingsMigrator.INSTANCE_METADATA_DIRECTORY)
|
||||
.resolve(LegacyGameSettingsMigrator.INSTANCE_CONFIG_DIRECTORY)
|
||||
.resolve(LegacyGameSettingsMigrator.INSTANCE_GAME_SETTINGS_FILENAME);
|
||||
if (Files.exists(currentSettings)) {
|
||||
return;
|
||||
}
|
||||
if (Files.exists(instanceRoot.resolve(LegacyGameSettingsMigrator.LEGACY_INSTANCE_SETTINGS_FILENAME))) {
|
||||
return;
|
||||
}
|
||||
|
||||
GameSettings.Instance setting = new GameSettings.Instance();
|
||||
setting.parentProperty().setValue(legacyGameSettings);
|
||||
if (legacyParent.defaultIsolationTypeProperty().getValue() == DefaultIsolationType.ALWAYS
|
||||
&& StringUtils.isBlank(legacyParent.runningDirectoryProperty().getValue())) {
|
||||
setting.getOverrideProperties().add(GameSettings.PROPERTY_RUNNING_DIRECTORY);
|
||||
}
|
||||
|
||||
try {
|
||||
FileUtils.saveSafely(currentSettings, LauncherSettings.SETTINGS_GSON.toJson(setting));
|
||||
} catch (IOException e) {
|
||||
LOG.warning("Failed to save migrated instance game settings " + currentSettings, e);
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the legacy profile name used in `configurations`.
|
||||
private static @Nullable String getLegacyProfileName(GameDirectory gameDirectory) {
|
||||
if (LEGACY_DEFAULT_PROFILE_ID.equals(gameDirectory.getId())) {
|
||||
|
||||
@@ -24,7 +24,6 @@ import org.jackhuang.hmcl.game.*;
|
||||
import org.jackhuang.hmcl.setting.property.InheritableProperty;
|
||||
import org.jackhuang.hmcl.util.StringUtils;
|
||||
import org.jackhuang.hmcl.util.gson.JsonUtils;
|
||||
import org.jackhuang.hmcl.util.i18n.LocalizedText;
|
||||
import org.jetbrains.annotations.NotNullByDefault;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
import org.jetbrains.annotations.Unmodifiable;
|
||||
@@ -43,12 +42,24 @@ import static org.jackhuang.hmcl.util.logging.Logger.LOG;
|
||||
@NotNullByDefault
|
||||
public final class LegacyGameSettingsMigrator {
|
||||
/// Legacy file name used by old per-version `VersionSetting` data.
|
||||
private static final String LEGACY_INSTANCE_SETTINGS_FILENAME = "hmclversion.cfg";
|
||||
static final String LEGACY_INSTANCE_SETTINGS_FILENAME = "hmclversion.cfg";
|
||||
|
||||
/// Receipt file name used to record successful legacy per-version settings migration.
|
||||
private static final String LEGACY_INSTANCE_SETTINGS_MIGRATION_RECEIPT_FILENAME =
|
||||
"instance-game-settings.migration-receipt.json";
|
||||
|
||||
/// Directory under the version root that stores HMCL-managed instance metadata.
|
||||
static final String INSTANCE_METADATA_DIRECTORY = ".hmcl";
|
||||
|
||||
/// Directory under the instance metadata directory that stores instance configuration files.
|
||||
static final String INSTANCE_CONFIG_DIRECTORY = "config";
|
||||
|
||||
/// Directory under the instance metadata directory that stores instance state files.
|
||||
private static final String INSTANCE_STATE_DIRECTORY = "state";
|
||||
|
||||
/// Current file name for instance-specific game settings.
|
||||
static final String INSTANCE_GAME_SETTINGS_FILENAME = "instance-game-settings.json";
|
||||
|
||||
/// Legacy game directory modes stored by old configuration files.
|
||||
private enum GameDirectoryType {
|
||||
/// Use the root `.minecraft` folder.
|
||||
@@ -101,9 +112,9 @@ public final class LegacyGameSettingsMigrator {
|
||||
}
|
||||
|
||||
/// Converts a legacy profile-level setting JSON object into a preset with the given ID.
|
||||
public static GameSettings.Preset toPreset(GameSettingsPresetID id, String name, @Nullable JsonObject source) {
|
||||
public static GameSettings.Preset toPreset(GameSettingsPresetID id, int autoNameNumber, @Nullable JsonObject source) {
|
||||
GameSettings.Preset target = new GameSettings.Preset(id);
|
||||
target.nameProperty().setValue(LocalizedText.plain(name));
|
||||
target.autoNameNumberProperty().setValue(autoNameNumber);
|
||||
if (getLegacyGameDirType(source, GameDirectoryType.ROOT_FOLDER) == GameDirectoryType.VERSION_FOLDER) {
|
||||
target.defaultIsolationTypeProperty().setValue(DefaultIsolationType.ALWAYS);
|
||||
}
|
||||
|
||||
@@ -77,7 +77,7 @@ public final class ThemePackManager {
|
||||
private static final Path USER_THEME_PACKS_DIRECTORY = Metadata.HMCL_USER_HOME.resolve("themes");
|
||||
|
||||
/// Default version used when exporting the current launcher appearance.
|
||||
private static final String CURRENT_THEME_PACK_VERSION = "1.0.0";
|
||||
public static final String CURRENT_THEME_PACK_VERSION = "1.0.0";
|
||||
|
||||
/// IDs of launcher-bundled theme packs in display order.
|
||||
private static final @Unmodifiable List<String> BUILTIN_THEME_PACK_IDS = List.of(
|
||||
@@ -615,13 +615,14 @@ public final class ThemePackManager {
|
||||
///
|
||||
/// @param outputFile the target theme-pack file
|
||||
/// @param packId the exported package identifier
|
||||
/// @param version the exported package version
|
||||
/// @param packName the exported package display name
|
||||
/// @param authorName the exported package author name
|
||||
/// @throws IOException if the current appearance cannot be exported
|
||||
public static void exportCurrent(Path outputFile, String packId, String packName, String authorName) throws IOException {
|
||||
public static void exportCurrent(Path outputFile, String packId, String version, String packName, String authorName) throws IOException {
|
||||
Objects.requireNonNull(outputFile);
|
||||
|
||||
ExportedThemePack themePack = createCurrent(packId, packName, authorName);
|
||||
ExportedThemePack themePack = createCurrent(packId, version, packName, authorName);
|
||||
try {
|
||||
ThemePackExporter.export(themePack.manifest(), themePack.assets(), outputFile);
|
||||
} finally {
|
||||
@@ -638,12 +639,14 @@ public final class ThemePackManager {
|
||||
/// Creates an exportable theme pack from the current launcher appearance.
|
||||
///
|
||||
/// @param packId the exported package identifier
|
||||
/// @param version the exported package version
|
||||
/// @param packName the exported package display name
|
||||
/// @param authorName the exported package author name
|
||||
/// @return the exportable theme-pack descriptor
|
||||
/// @throws IOException if the current appearance cannot be represented as a theme pack
|
||||
public static ExportedThemePack createCurrent(String packId, String packName, String authorName) throws IOException {
|
||||
public static ExportedThemePack createCurrent(String packId, String version, String packName, String authorName) throws IOException {
|
||||
packId = ThemePackManifest.requirePackageId(packId);
|
||||
version = requireNonBlank(version, "version");
|
||||
packName = requireNonBlank(packName, "packName");
|
||||
authorName = requireNonBlank(authorName, "authorName");
|
||||
|
||||
@@ -668,7 +671,7 @@ public final class ThemePackManager {
|
||||
List.of());
|
||||
ThemePackManifest manifest = new ThemePackManifest(
|
||||
packId,
|
||||
CURRENT_THEME_PACK_VERSION,
|
||||
version,
|
||||
LocalizedText.plain(packName),
|
||||
List.of(new ThemePackAuthor(LocalizedText.plain(authorName))),
|
||||
background.source() instanceof ThemeBackground.Image image ? image.path() : null,
|
||||
|
||||
@@ -92,11 +92,7 @@ import org.xml.sax.SAXException;
|
||||
import javax.xml.parsers.DocumentBuilder;
|
||||
import javax.xml.parsers.DocumentBuilderFactory;
|
||||
import javax.xml.parsers.ParserConfigurationException;
|
||||
import java.io.BufferedInputStream;
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.StringReader;
|
||||
import java.io.*;
|
||||
import java.lang.invoke.MethodHandle;
|
||||
import java.lang.invoke.MethodHandles;
|
||||
import java.lang.invoke.MethodType;
|
||||
@@ -1691,6 +1687,7 @@ public final class FXUtils {
|
||||
|
||||
control.setOnContextMenuRequested(e -> {
|
||||
boolean hasNoSelection = control.getSelectedText().isEmpty();
|
||||
boolean isPassword = control instanceof PasswordField;
|
||||
|
||||
IconedMenuItem undo = new IconedMenuItem(SVG.UNDO, i18n("menu.undo"), control::undo, popup);
|
||||
IconedMenuItem redo = new IconedMenuItem(SVG.REDO, i18n("menu.redo"), control::redo, popup);
|
||||
@@ -1704,9 +1701,9 @@ public final class FXUtils {
|
||||
|
||||
undo.setDisable(!control.isUndoable());
|
||||
redo.setDisable(!control.isRedoable());
|
||||
cut.setDisable(hasNoSelection);
|
||||
cut.setDisable(hasNoSelection || isPassword);
|
||||
delete.setDisable(hasNoSelection);
|
||||
copy.setDisable(hasNoSelection);
|
||||
copy.setDisable(hasNoSelection || isPassword);
|
||||
paste.setDisable(!Clipboard.getSystemClipboard().hasString());
|
||||
selectall.setDisable(control.getText() == null || control.getText().isEmpty());
|
||||
|
||||
|
||||
@@ -167,7 +167,7 @@ public class GameCrashWindow extends Stage {
|
||||
Set<String> keywords = new HashSet<>();
|
||||
for (Pair<Set<CrashReportAnalyzer.Result>, Set<String>> pair : (List<Pair<Set<CrashReportAnalyzer.Result>, Set<String>>>) (List<?>) taskResult) {
|
||||
for (CrashReportAnalyzer.Result result : pair.getKey()) {
|
||||
results.put(result.getRule(), result);
|
||||
results.put(result.rule(), result);
|
||||
}
|
||||
keywords.addAll(pair.getValue());
|
||||
}
|
||||
@@ -184,22 +184,22 @@ public class GameCrashWindow extends Stage {
|
||||
|
||||
for (CrashReportAnalyzer.Result result : results.values()) {
|
||||
String message;
|
||||
switch (result.getRule()) {
|
||||
switch (result.rule()) {
|
||||
case TOO_OLD_JAVA:
|
||||
message = i18n("game.crash.reason.too_old_java", CrashReportAnalyzer.getJavaVersionFromMajorVersion(Integer.parseInt(result.getMatcher().group("expected"))));
|
||||
message = i18n("game.crash.reason.too_old_java", CrashReportAnalyzer.getJavaVersionFromMajorVersion(Integer.parseInt(result.matcher().group("expected"))));
|
||||
break;
|
||||
case MOD_RESOLUTION_CONFLICT:
|
||||
case MOD_RESOLUTION_MISSING:
|
||||
case MOD_RESOLUTION_COLLECTION:
|
||||
message = i18n("game.crash.reason." + result.getRule().name().toLowerCase(Locale.ROOT),
|
||||
translateFabricModId(result.getMatcher().group("sourcemod")),
|
||||
parseFabricModId(result.getMatcher().group("destmod")),
|
||||
parseFabricModId(result.getMatcher().group("destmod")));
|
||||
message = i18n("game.crash.reason." + result.rule().name().toLowerCase(Locale.ROOT),
|
||||
translateFabricModId(result.matcher().group("sourcemod")),
|
||||
parseFabricModId(result.matcher().group("destmod")),
|
||||
parseFabricModId(result.matcher().group("destmod")));
|
||||
break;
|
||||
case MOD_RESOLUTION_MISSING_MINECRAFT:
|
||||
message = i18n("game.crash.reason." + result.getRule().name().toLowerCase(Locale.ROOT),
|
||||
translateFabricModId(result.getMatcher().group("mod")),
|
||||
result.getMatcher().group("version"));
|
||||
message = i18n("game.crash.reason." + result.rule().name().toLowerCase(Locale.ROOT),
|
||||
translateFabricModId(result.matcher().group("mod")),
|
||||
result.matcher().group("version"));
|
||||
break;
|
||||
case MOD_FOREST_OPTIFINE:
|
||||
case TWILIGHT_FOREST_OPTIFINE:
|
||||
@@ -207,15 +207,15 @@ public class GameCrashWindow extends Stage {
|
||||
case JADE_FOREST_OPTIFINE:
|
||||
case NEOFORGE_FOREST_OPTIFINE:
|
||||
message = i18n("game.crash.reason.mod", "OptiFine");
|
||||
LOG.info("Crash cause: " + result.getRule() + ": " + i18n("game.crash.reason.mod", "OptiFine"));
|
||||
LOG.info("Crash cause: " + result.rule() + ": " + i18n("game.crash.reason.mod", "OptiFine"));
|
||||
break;
|
||||
default:
|
||||
message = i18n("game.crash.reason." + result.getRule().name().toLowerCase(Locale.ROOT),
|
||||
Arrays.stream(result.getRule().getGroupNames()).map(groupName -> result.getMatcher().group(groupName))
|
||||
message = i18n("game.crash.reason." + result.rule().name().toLowerCase(Locale.ROOT),
|
||||
Arrays.stream(result.rule().getGroupNames()).map(groupName -> result.matcher().group(groupName))
|
||||
.toArray());
|
||||
break;
|
||||
}
|
||||
LOG.info("Crash cause: " + result.getRule() + ": " + message);
|
||||
LOG.info("Crash cause: " + result.rule() + ": " + message);
|
||||
segments.addAll(FXUtils.parseSegment(message, Controllers::onHyperlinkAction));
|
||||
segments.add(new Text("\n\n"));
|
||||
}
|
||||
|
||||
@@ -118,7 +118,7 @@ public final class MemoryStatusBar extends Control {
|
||||
}
|
||||
|
||||
PhysicalMemoryStatus status = getSkinnable().getMemoryStatus();
|
||||
double total = status.getTotal();
|
||||
double total = status.total();
|
||||
double used = status.getUsed();
|
||||
double allocated = getSkinnable().getMemoryAllocated();
|
||||
|
||||
|
||||
@@ -21,6 +21,7 @@ import com.jfoenix.controls.JFXButton;
|
||||
import com.jfoenix.controls.JFXRadioButton;
|
||||
import com.jfoenix.effects.JFXDepthManager;
|
||||
import javafx.beans.binding.Bindings;
|
||||
import javafx.geometry.Insets;
|
||||
import javafx.geometry.Pos;
|
||||
import javafx.scene.Cursor;
|
||||
import javafx.scene.canvas.Canvas;
|
||||
@@ -86,6 +87,7 @@ public final class AccountListItemSkin extends SkinBase<AccountListItem> {
|
||||
|
||||
center.getChildren().setAll(canvas, item);
|
||||
root.setCenter(center);
|
||||
BorderPane.setMargin(center, new Insets(0, 0, 0, 8));
|
||||
|
||||
HBox right = new HBox();
|
||||
right.setAlignment(Pos.CENTER_RIGHT);
|
||||
@@ -157,12 +159,9 @@ public final class AccountListItemSkin extends SkinBase<AccountListItem> {
|
||||
right.getChildren().add(spinnerUpload);
|
||||
|
||||
JFXButton btnCopyUUID = FXUtils.newToggleButton4(SVG.CONTENT_COPY);
|
||||
SpinnerPane spinnerCopyUUID = new SpinnerPane();
|
||||
spinnerCopyUUID.getStyleClass().add("small-spinner-pane");
|
||||
btnCopyUUID.setOnAction(e -> FXUtils.copyText(skinnable.getAccount().getProfileID().toString()));
|
||||
FXUtils.installFastTooltip(btnCopyUUID, i18n("account.copy_uuid"));
|
||||
spinnerCopyUUID.setContent(btnCopyUUID);
|
||||
right.getChildren().add(spinnerCopyUUID);
|
||||
right.getChildren().add(btnCopyUUID);
|
||||
|
||||
JFXButton btnRemove = FXUtils.newToggleButton4(SVG.DELETE_FOREVER);
|
||||
btnRemove.setOnAction(e -> Controllers.confirm(i18n("button.remove.confirm"), i18n("button.remove"), skinnable::remove, null));
|
||||
|
||||
@@ -87,8 +87,22 @@ public final class AddAuthlibInjectorServerPane extends TransitionPane implement
|
||||
actions.getChildren().setAll(cancel, nextPane);
|
||||
}
|
||||
|
||||
addServerPane.setBody(txtServerUrl);
|
||||
addServerPane.setActions(lblCreationWarning, actions);
|
||||
VBox body = new VBox();
|
||||
body.setSpacing(8);
|
||||
|
||||
lblCreationWarning.setMinWidth(0);
|
||||
lblCreationWarning.setPrefWidth(0);
|
||||
lblCreationWarning.maxWidthProperty().bind(body.widthProperty());
|
||||
lblCreationWarning.visibleProperty().bind(lblCreationWarning.textProperty().isNotEmpty());
|
||||
lblCreationWarning.managedProperty().bind(lblCreationWarning.visibleProperty());
|
||||
|
||||
body.getChildren().addAll(
|
||||
txtServerUrl,
|
||||
lblCreationWarning
|
||||
);
|
||||
|
||||
addServerPane.setBody(body);
|
||||
addServerPane.setActions(actions);
|
||||
|
||||
txtServerUrl.getValidators().addAll(new RequiredValidator(), new URLValidator());
|
||||
FXUtils.setValidateWhileTextChanged(txtServerUrl, true);
|
||||
@@ -154,7 +168,6 @@ public final class AddAuthlibInjectorServerPane extends TransitionPane implement
|
||||
|
||||
this.setContent(addServerPane, ContainerAnimations.NONE);
|
||||
|
||||
lblCreationWarning.maxWidthProperty().bind(((FlowPane) lblCreationWarning.getParent()).widthProperty());
|
||||
nextPane.hideSpinner();
|
||||
|
||||
onEscPressed(this, this::onAddCancel);
|
||||
|
||||
@@ -148,14 +148,14 @@ public class OfflineAccountSkinPane extends StackPane {
|
||||
UUID uuid = this.account.getProfileID();
|
||||
if (result == null || result.skin() == null && result.cape() == null) {
|
||||
canvas.updateSkin(
|
||||
TexturesLoader.getDefaultSkin(uuid).getImage(),
|
||||
TexturesLoader.getDefaultSkin(uuid).image(),
|
||||
TexturesLoader.getDefaultModel(uuid) == TextureModel.SLIM,
|
||||
null
|
||||
);
|
||||
return;
|
||||
}
|
||||
canvas.updateSkin(
|
||||
result.skin() != null ? result.skin().image() : TexturesLoader.getDefaultSkin(uuid).getImage(),
|
||||
result.skin() != null ? result.skin().image() : TexturesLoader.getDefaultSkin(uuid).image(),
|
||||
result.model() == TextureModel.SLIM,
|
||||
result.cape() != null ? result.cape().image() : null);
|
||||
}
|
||||
|
||||
@@ -19,6 +19,7 @@ package org.jackhuang.hmcl.ui.construct;
|
||||
|
||||
import javafx.beans.property.*;
|
||||
import javafx.collections.ObservableList;
|
||||
import javafx.css.PseudoClass;
|
||||
import javafx.geometry.Orientation;
|
||||
import javafx.scene.Node;
|
||||
import javafx.scene.control.Control;
|
||||
@@ -228,4 +229,30 @@ public class ComponentSublist extends Control implements NoPaddingComponent {
|
||||
public void setTip(String tip) {
|
||||
this.tip.set(tip);
|
||||
}
|
||||
|
||||
/// Whether the sublist header should use a larger title font.
|
||||
private static final PseudoClass PSEUDO_LARGE_TITLE = PseudoClass.getPseudoClass("large-title");
|
||||
|
||||
/// The property controlling whether the sublist header uses a larger title font.
|
||||
private final BooleanProperty largeTitle = new SimpleBooleanProperty(this, "largeTitle", false) {
|
||||
@Override
|
||||
protected void invalidated() {
|
||||
pseudoClassStateChanged(PSEUDO_LARGE_TITLE, get());
|
||||
}
|
||||
};
|
||||
|
||||
/// Returns the property controlling whether the sublist header uses a larger title font.
|
||||
public BooleanProperty largeTitleProperty() {
|
||||
return largeTitle;
|
||||
}
|
||||
|
||||
/// Returns whether the sublist header uses a larger title font.
|
||||
public final boolean isLargeTitle() {
|
||||
return largeTitle.get();
|
||||
}
|
||||
|
||||
/// Sets whether the sublist header uses a larger title font.
|
||||
public final void setLargeTitle(boolean value) {
|
||||
largeTitle.set(value);
|
||||
}
|
||||
}
|
||||
@@ -145,10 +145,7 @@ final class ComponentSublistWrapper extends VBox implements NoPaddingComponent {
|
||||
});
|
||||
});
|
||||
|
||||
Node headerLeft = sublist.getHeaderLeft();
|
||||
if (headerLeft != null) {
|
||||
header.setTitleContent(headerLeft);
|
||||
}
|
||||
FXUtils.onChangeAndOperate(sublist.largeTitleProperty(), header::setLargeTitle);
|
||||
|
||||
InvalidationListener updateTitleTrailing = observable -> {
|
||||
Node titleTrailing = sublist.getTitleTrailing();
|
||||
|
||||
@@ -44,27 +44,11 @@ public interface DecoratorPage extends Refreshable {
|
||||
((Node) this).getStyleClass().add("content-background");
|
||||
}
|
||||
|
||||
class State {
|
||||
private final String title;
|
||||
private final Node titleNode;
|
||||
private final boolean backable;
|
||||
private final boolean refreshable;
|
||||
private final boolean animate;
|
||||
private final double leftPaneWidth;
|
||||
|
||||
record State(String title, Node titleNode, boolean backable, boolean refreshable, boolean animate, double leftPaneWidth) {
|
||||
public State(String title, Node titleNode, boolean backable, boolean refreshable, boolean animate) {
|
||||
this(title, titleNode, backable, refreshable, animate, 0);
|
||||
}
|
||||
|
||||
public State(String title, Node titleNode, boolean backable, boolean refreshable, boolean animate, double leftPaneWidth) {
|
||||
this.title = title;
|
||||
this.titleNode = titleNode;
|
||||
this.backable = backable;
|
||||
this.refreshable = refreshable;
|
||||
this.animate = animate;
|
||||
this.leftPaneWidth = leftPaneWidth;
|
||||
}
|
||||
|
||||
public static State fromTitle(String title) {
|
||||
return new State(title, null, true, false, true);
|
||||
}
|
||||
@@ -76,29 +60,5 @@ public interface DecoratorPage extends Refreshable {
|
||||
public static State fromTitleNode(Node titleNode) {
|
||||
return new State(null, titleNode, true, false, true);
|
||||
}
|
||||
|
||||
public String getTitle() {
|
||||
return title;
|
||||
}
|
||||
|
||||
public Node getTitleNode() {
|
||||
return titleNode;
|
||||
}
|
||||
|
||||
public boolean isBackable() {
|
||||
return backable;
|
||||
}
|
||||
|
||||
public boolean isRefreshable() {
|
||||
return refreshable;
|
||||
}
|
||||
|
||||
public boolean isAnimate() {
|
||||
return animate;
|
||||
}
|
||||
|
||||
public double getLeftPaneWidth() {
|
||||
return leftPaneWidth;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -208,8 +208,8 @@ public class DecoratorSkin extends SkinBase<Decorator> {
|
||||
navBarPane.setId("decoratorTitleTransitionPane");
|
||||
FXUtils.onChangeAndOperate(skinnable.stateProperty(), s -> {
|
||||
if (s == null) return;
|
||||
Node node = createNavBar(skinnable, s.getLeftPaneWidth(), s.isBackable(), skinnable.canCloseProperty().get(), skinnable.showCloseAsHomeProperty().get(), s.isRefreshable(), s.getTitle(), s.getTitleNode());
|
||||
if (s.isAnimate()) {
|
||||
Node node = createNavBar(skinnable, s.leftPaneWidth(), s.backable(), skinnable.canCloseProperty().get(), skinnable.showCloseAsHomeProperty().get(), s.refreshable(), s.title(), s.titleNode());
|
||||
if (s.animate()) {
|
||||
TransitionPane.AnimationProducer animation = switch (skinnable.getNavigationDirection()) {
|
||||
case NEXT -> NavBarAnimations.NEXT;
|
||||
case PREVIOUS -> NavBarAnimations.PREVIOUS;
|
||||
|
||||
@@ -57,7 +57,7 @@ public abstract class DecoratorTransitionPage extends Control implements Decorat
|
||||
if (to instanceof DecoratorPage) {
|
||||
state.bind(Bindings.createObjectBinding(() -> {
|
||||
State state = ((DecoratorPage) to).stateProperty().get();
|
||||
return new State(state.getTitle(), state.getTitleNode(), backable.get(), state.isRefreshable(), true, leftPaneWidth.get());
|
||||
return new State(state.title(), state.titleNode(), backable.get(), state.refreshable(), true, leftPaneWidth.get());
|
||||
}, ((DecoratorPage) to).stateProperty()));
|
||||
} else {
|
||||
state.unbind();
|
||||
|
||||
@@ -390,7 +390,12 @@ public final class VersionsPage extends Control implements WizardPage, Refreshab
|
||||
list = new JFXListView<>();
|
||||
list.getStyleClass().add("jfx-list-view-float");
|
||||
|
||||
list.setFixedCellSize(65);
|
||||
RemoteVersionListCell dummyCell = new RemoteVersionListCell(control);
|
||||
dummyCell.twoLineListItem.setTitle("Dummy");
|
||||
dummyCell.twoLineListItem.setSubtitle("Dummy");
|
||||
dummyCell.imageView.setImage(VersionIconType.GRASS.getIcon());
|
||||
prepareNode(dummyCell.pane);
|
||||
list.setFixedCellSize(dummyCell.pane.prefHeight(-1));
|
||||
|
||||
VBox.setVgrow(list, Priority.ALWAYS);
|
||||
|
||||
|
||||
@@ -35,6 +35,7 @@ import javafx.scene.Scene;
|
||||
import javafx.scene.control.ContentDisplay;
|
||||
import javafx.scene.control.Label;
|
||||
import javafx.scene.control.ScrollPane;
|
||||
import javafx.scene.control.TextFormatter;
|
||||
import javafx.scene.control.Tooltip;
|
||||
import javafx.scene.input.MouseEvent;
|
||||
import javafx.scene.layout.*;
|
||||
@@ -101,6 +102,8 @@ public final class GameSettingsPage<S extends GameSettings> extends StackPane
|
||||
/// The current setting.
|
||||
private final ObjectProperty<@Nullable S> currentSetting = new SimpleObjectProperty<>(this, "setting");
|
||||
|
||||
private final ObjectProperty<GameVersionNumber> currentGameVersionNumber = new SimpleObjectProperty<>(this, "currentGameVersionNumber", GameVersionNumber.unknown());
|
||||
|
||||
private boolean updatingJavaSetting = false;
|
||||
private boolean updatingSelectedJava = false;
|
||||
private boolean updatingParentSetting = false;
|
||||
@@ -190,7 +193,7 @@ public final class GameSettingsPage<S extends GameSettings> extends StackPane
|
||||
parentGameSettingsPane.setTitle(i18n("settings.type.global.preset"));
|
||||
parentGameSettingsPane.setConverter(setting -> setting != null
|
||||
? PresetManagementPane.getPresetDisplayName(setting)
|
||||
: getImplicitParentGameSettingsDisplayName());
|
||||
: i18n("settings.type.global.preset.default"));
|
||||
bindInstanceParentSetting(parentGameSettingsPane);
|
||||
}
|
||||
|
||||
@@ -339,15 +342,15 @@ public final class GameSettingsPage<S extends GameSettings> extends StackPane
|
||||
defaultIsolationTypePane.setSubtitle(i18n("settings.game.default_isolation.subtitle"));
|
||||
defaultIsolationTypePane.setItems(DefaultIsolationType.values());
|
||||
defaultIsolationTypePane.setNullSafeConverter(type -> switch (type) {
|
||||
case NEVER -> i18n("settings.game.default_isolation.never");
|
||||
case ALWAYS -> i18n("settings.game.default_isolation.always");
|
||||
case MODDED -> i18n("settings.game.default_isolation.modded");
|
||||
});
|
||||
case NEVER -> i18n("settings.game.default_isolation.never");
|
||||
case ALWAYS -> i18n("settings.game.default_isolation.always");
|
||||
case MODDED -> i18n("settings.game.default_isolation.modded");
|
||||
});
|
||||
defaultIsolationTypePane.setDescriptionConverter(type -> switch (type) {
|
||||
case NEVER -> i18n("settings.game.default_isolation.never.desc");
|
||||
case ALWAYS -> i18n("settings.game.default_isolation.always.desc");
|
||||
case MODDED -> i18n("settings.game.default_isolation.modded.desc");
|
||||
});
|
||||
case NEVER -> i18n("settings.game.default_isolation.never.desc");
|
||||
case ALWAYS -> i18n("settings.game.default_isolation.always.desc");
|
||||
case MODDED -> i18n("settings.game.default_isolation.modded.desc");
|
||||
});
|
||||
bindPresetBidirectional(defaultIsolationTypePane.valueProperty(), GameSettings.Preset::defaultIsolationTypeProperty);
|
||||
} else {
|
||||
var isolationButton = new LineToggleButton();
|
||||
@@ -653,6 +656,10 @@ public final class GameSettingsPage<S extends GameSettings> extends StackPane
|
||||
{
|
||||
var txtMinMemory = new JFXTextField();
|
||||
txtMinMemory.setPrefWidth(160);
|
||||
txtMinMemory.setTextFormatter(new TextFormatter<>(change -> {
|
||||
change.setText(change.getText().replaceAll("[^0-9]", ""));
|
||||
return change;
|
||||
}));
|
||||
|
||||
var rightBox = new HBox(8, txtMinMemory, new Label(i18n("settings.memory.unit.mib")));
|
||||
rightBox.setAlignment(Pos.CENTER_RIGHT);
|
||||
@@ -666,6 +673,10 @@ public final class GameSettingsPage<S extends GameSettings> extends StackPane
|
||||
{
|
||||
var txtMetaspace = new JFXTextField();
|
||||
txtMetaspace.setPrefWidth(160);
|
||||
txtMetaspace.setTextFormatter(new TextFormatter<>(change -> {
|
||||
change.setText(change.getText().replaceAll("[^0-9]", ""));
|
||||
return change;
|
||||
}));
|
||||
|
||||
var rightBox = new HBox(8, txtMetaspace, new Label(i18n("settings.memory.unit.mib")));
|
||||
rightBox.setAlignment(Pos.CENTER_RIGHT);
|
||||
@@ -740,6 +751,9 @@ public final class GameSettingsPage<S extends GameSettings> extends StackPane
|
||||
graphicsSettings.getContent().add(graphicsBackendPane);
|
||||
graphicsBackendPane.setTitle(i18n("settings.advanced.graphics_backend"));
|
||||
|
||||
if (isPresetSetting)
|
||||
graphicsBackendPane.setSubtitle(i18n("settings.advanced.graphics_backend.desc"));
|
||||
|
||||
var openGLRendererPane = createInheritableButton(
|
||||
GameSettings::openGLRendererProperty,
|
||||
e -> i18n("settings.advanced.renderer." + e.name().toLowerCase(Locale.ROOT)),
|
||||
@@ -762,6 +776,19 @@ public final class GameSettingsPage<S extends GameSettings> extends StackPane
|
||||
graphicsSettings.getContent().add(vulkanRendererPane);
|
||||
vulkanRendererPane.setTitle(i18n("settings.advanced.renderer.vulkan"));
|
||||
|
||||
this.currentGameVersionNumber.addListener((o, oldValue, newValue) -> {
|
||||
boolean showBackendChoose = isPresetSetting || newValue.compareTo("26.2-snapshot-2") >= 0;
|
||||
graphicsBackendPane.setVisible(showBackendChoose);
|
||||
graphicsBackendPane.setManaged(showBackendChoose);
|
||||
|
||||
boolean showOpenGL = GraphicsAPI.OPENGL.isSupported(newValue);
|
||||
openGLRendererPane.setVisible(showOpenGL);
|
||||
openGLRendererPane.setManaged(showOpenGL);
|
||||
|
||||
boolean showVulkan = GraphicsAPI.VULKAN.isSupported(newValue);
|
||||
vulkanRendererPane.setVisible(showVulkan);
|
||||
vulkanRendererPane.setManaged(showVulkan);
|
||||
});
|
||||
}
|
||||
|
||||
var nativeLibrarySettings = new ComponentList();
|
||||
@@ -846,28 +873,11 @@ public final class GameSettingsPage<S extends GameSettings> extends StackPane
|
||||
});
|
||||
}
|
||||
|
||||
/// Refreshes parent preset display text because the implicit parent depends on the current game directory.
|
||||
/// Refreshes parent preset display text after the preset list changes.
|
||||
private void refreshInstanceParentSettingConverter(LineSelectButton<GameSettings.@Nullable Preset> button) {
|
||||
button.setConverter(setting -> setting != null
|
||||
? PresetManagementPane.getPresetDisplayName(setting)
|
||||
: getImplicitParentGameSettingsDisplayName());
|
||||
}
|
||||
|
||||
/// Returns the label for the implicit parent preset selected by a null instance parent.
|
||||
private String getImplicitParentGameSettingsDisplayName() {
|
||||
GameSettings.Preset legacyParent = getGameDirectoryLegacyGameSettings();
|
||||
return legacyParent != null
|
||||
? PresetManagementPane.getPresetDisplayName(legacyParent)
|
||||
: i18n("settings.type.global.preset.default");
|
||||
}
|
||||
|
||||
/// Returns the migrated game-directory-level parent preset, or `null` when this game directory uses the default preset.
|
||||
private @Nullable GameSettings.Preset getGameDirectoryLegacyGameSettings() {
|
||||
if (gameDirectory == null || gameDirectory.getLegacyGameSettings() == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return SettingsManager.getGameSettings(gameDirectory.getLegacyGameSettings());
|
||||
: i18n("settings.type.global.preset.default"));
|
||||
}
|
||||
|
||||
/// Adds the title-line inheritance button for the Java selection sublist.
|
||||
@@ -2438,6 +2448,8 @@ public final class GameSettingsPage<S extends GameSettings> extends StackPane
|
||||
assert isPresetSetting == (instanceId == null);
|
||||
|
||||
if (instanceId != null) {
|
||||
this.currentGameVersionNumber.set(GameVersionNumber.asGameVersion(repository.getGameVersion(instanceId)));
|
||||
|
||||
@Nullable GameSettings.Instance setting = repository.getInstanceGameSettingsOrCreate(instanceId);
|
||||
this.currentSetting.set((S) setting);
|
||||
setSettingsReadOnly(
|
||||
@@ -2446,6 +2458,7 @@ public final class GameSettingsPage<S extends GameSettings> extends StackPane
|
||||
setting != null ? this::forceOverwriteInstanceGameSettings : null);
|
||||
loadIcon();
|
||||
} else {
|
||||
this.currentGameVersionNumber.set(GameVersionNumber.unknown());
|
||||
this.currentSetting.set((S) SettingsManager.getDefaultGameSettingsPresetOrCreate());
|
||||
setSettingsReadOnly(
|
||||
SettingsManager.isGameSettingsReadOnly(),
|
||||
@@ -2457,15 +2470,15 @@ public final class GameSettingsPage<S extends GameSettings> extends StackPane
|
||||
/// Updates the page read-only state used when settings cannot be saved safely.
|
||||
///
|
||||
/// @param readOnly whether the current settings should be displayed read-only
|
||||
/// @param message the warning message shown when the page is read-only
|
||||
/// @param message the warning message shown when the page is read-only
|
||||
private void setSettingsReadOnly(boolean readOnly, String message) {
|
||||
setSettingsReadOnly(readOnly, message, null);
|
||||
}
|
||||
|
||||
/// Updates the page read-only state used when settings cannot be saved safely.
|
||||
///
|
||||
/// @param readOnly whether the current settings should be displayed read-only
|
||||
/// @param message the warning message shown when the page is read-only
|
||||
/// @param readOnly whether the current settings should be displayed read-only
|
||||
/// @param message the warning message shown when the page is read-only
|
||||
/// @param forceOverwrite the action used to back up and overwrite the read-only file
|
||||
private void setSettingsReadOnly(boolean readOnly, String message, @Nullable Runnable forceOverwrite) {
|
||||
if (readOnly && forceOverwrite != null) {
|
||||
@@ -2600,13 +2613,11 @@ public final class GameSettingsPage<S extends GameSettings> extends StackPane
|
||||
}
|
||||
|
||||
if (JavaManager.isInitialized()) {
|
||||
GameVersionNumber gameVersionNumber;
|
||||
GameVersionNumber gameVersionNumber = this.currentGameVersionNumber.get();
|
||||
Version version;
|
||||
if (this.instanceId == null) {
|
||||
gameVersionNumber = GameVersionNumber.unknown();
|
||||
version = null;
|
||||
} else {
|
||||
gameVersionNumber = repository != null ? GameVersionNumber.asGameVersion(repository.getGameVersion(this.instanceId)) : GameVersionNumber.unknown();
|
||||
version = repository != null ? repository.getResolvedVersion(this.instanceId) : null;
|
||||
}
|
||||
|
||||
|
||||
@@ -580,7 +580,7 @@ final class IndependentSettingBinder {
|
||||
long maxMemoryBytes = Math.max(0, maxMemory != null ? maxMemory : 0) * 1024L * 1024L;
|
||||
return HMCLGameRepository.getAllocatedMemory(
|
||||
maxMemoryBytes,
|
||||
memoryStatusBar.getMemoryStatus().getAvailable(),
|
||||
memoryStatusBar.getMemoryStatus().available(),
|
||||
Boolean.TRUE.equals(autoMemory));
|
||||
}
|
||||
|
||||
@@ -595,9 +595,9 @@ final class IndependentSettingBinder {
|
||||
boolean autoMemoryEnabled = Boolean.TRUE.equals(autoMemory);
|
||||
physicalMemoryLabel.setText(i18n("settings.memory.used_per_total",
|
||||
GIGABYTES.convertFromBytes(memoryStatus.getUsed()),
|
||||
GIGABYTES.convertFromBytes(memoryStatus.getTotal())));
|
||||
GIGABYTES.convertFromBytes(memoryStatus.total())));
|
||||
allocatedMemoryLabel.setText(i18n(
|
||||
memoryStatus.hasAvailable() && maxMemoryBytes > memoryStatus.getAvailable()
|
||||
memoryStatus.hasAvailable() && maxMemoryBytes > memoryStatus.available()
|
||||
? (autoMemoryEnabled
|
||||
? "settings.memory.allocate.auto.exceeded"
|
||||
: "settings.memory.allocate.manual.exceeded")
|
||||
@@ -607,9 +607,9 @@ final class IndependentSettingBinder {
|
||||
GIGABYTES.convertFromBytes(maxMemoryBytes),
|
||||
GIGABYTES.convertFromBytes(HMCLGameRepository.getAllocatedMemory(
|
||||
maxMemoryBytes,
|
||||
memoryStatus.getAvailable(),
|
||||
memoryStatus.available(),
|
||||
autoMemoryEnabled)),
|
||||
GIGABYTES.convertFromBytes(memoryStatus.getAvailable())));
|
||||
GIGABYTES.convertFromBytes(memoryStatus.available())));
|
||||
}
|
||||
|
||||
private static <T> void toggleOverride(
|
||||
|
||||
@@ -76,7 +76,6 @@ final class PresetManagementPane extends ComponentSublist {
|
||||
this.holder = Objects.requireNonNull(holder);
|
||||
|
||||
setTitle(i18n("settings.type.global.preset.manage_all"));
|
||||
setHasSubtitle(true);
|
||||
|
||||
LineButton createButton = new LineButton();
|
||||
createButton.setTitle(i18n("settings.type.global.preset.create"));
|
||||
@@ -189,7 +188,7 @@ final class PresetManagementPane extends ComponentSublist {
|
||||
|
||||
/// Creates a new preset and selects it for editing.
|
||||
private void createPreset() {
|
||||
int number = createDefaultPresetNumber();
|
||||
int number = SettingsManager.gameSettingsPresets().newPresetAutoNameNumber();
|
||||
PromptDialogPane.Builder.StringQuestion nameQuestion =
|
||||
new PromptDialogPane.Builder.StringQuestion("", "")
|
||||
.setPromptText(i18n("settings.type.global.preset.auto_name", number));
|
||||
@@ -216,26 +215,6 @@ final class PresetManagementPane extends ComponentSublist {
|
||||
}).addQuestion(nameQuestion));
|
||||
}
|
||||
|
||||
/// Returns the first automatic preset number that is not used by existing presets.
|
||||
private int createDefaultPresetNumber() {
|
||||
for (int index = 1; ; index++) {
|
||||
String name = i18n("settings.type.global.preset.auto_name", index);
|
||||
boolean used = false;
|
||||
for (GameSettings.Preset setting : SettingsManager.getGameSettings()) {
|
||||
Integer autoNameNumber = setting.autoNameNumberProperty().getValue();
|
||||
if ((autoNameNumber != null && autoNameNumber == index)
|
||||
|| Objects.equals(name, getPresetDisplayName(setting))) {
|
||||
used = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!used) {
|
||||
return index;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Asks the user for a new preset name.
|
||||
private void renamePreset(GameSettings.Preset setting) {
|
||||
@Nullable String currentName = getPresetCustomName(setting);
|
||||
|
||||
@@ -45,7 +45,10 @@ import org.jetbrains.annotations.Nullable;
|
||||
import java.io.IOException;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.*;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
@@ -154,13 +157,13 @@ public final class ImageUtils {
|
||||
if (doScale) {
|
||||
targetWidth = requestedWidth;
|
||||
targetHeight = requestedHeight;
|
||||
pixels = scale(defaultImage.array,
|
||||
defaultImage.width, defaultImage.height,
|
||||
pixels = scale(defaultImage.array(),
|
||||
defaultImage.width(), defaultImage.height(),
|
||||
targetWidth, targetHeight);
|
||||
} else {
|
||||
targetWidth = defaultImage.width;
|
||||
targetHeight = defaultImage.height;
|
||||
pixels = defaultImage.array;
|
||||
targetWidth = defaultImage.width();
|
||||
targetHeight = defaultImage.height();
|
||||
pixels = defaultImage.array();
|
||||
}
|
||||
|
||||
WritableImage image = new WritableImage(targetWidth, targetHeight);
|
||||
@@ -219,18 +222,10 @@ public final class ImageUtils {
|
||||
(byte) 0x0d, (byte) 0x0a, (byte) 0x1a, (byte) 0x0a,
|
||||
};
|
||||
|
||||
private static final class PngChunkHeader {
|
||||
private record PngChunkHeader(int length, int chunkType) {
|
||||
private static final int IDAT_HEADER = 0x49444154;
|
||||
private static final int acTL_HEADER = 0x6163544c;
|
||||
|
||||
private final int length;
|
||||
private final int chunkType;
|
||||
|
||||
private PngChunkHeader(int length, int chunkType) {
|
||||
this.length = length;
|
||||
this.chunkType = chunkType;
|
||||
}
|
||||
|
||||
private static @Nullable PngChunkHeader readHeader(ByteBuffer headerBuffer) {
|
||||
if (headerBuffer.remaining() < 8)
|
||||
return null;
|
||||
@@ -327,7 +322,7 @@ public final class ImageUtils {
|
||||
int[] buffer = new int[Math.multiplyExact(width, height)];
|
||||
for (int frameIndex = 0; frameIndex < frames.size(); frameIndex++) {
|
||||
var frame = frames.get(frameIndex);
|
||||
PngFrameControl control = frame.control;
|
||||
PngFrameControl control = frame.control();
|
||||
|
||||
if (frameIndex == 0 && (
|
||||
control.xOffset != 0 || control.yOffset != 0
|
||||
@@ -347,7 +342,7 @@ public final class ImageUtils {
|
||||
int[] currentFrameBuffer = buffer.clone();
|
||||
if (control.blendOp == 0) {
|
||||
for (int row = 0; row < control.height; row++) {
|
||||
System.arraycopy(frame.bitmap.array,
|
||||
System.arraycopy(frame.bitmap().array(),
|
||||
row * control.width,
|
||||
currentFrameBuffer,
|
||||
(control.yOffset + row) * width + control.xOffset,
|
||||
@@ -360,7 +355,7 @@ public final class ImageUtils {
|
||||
int srcIndex = row * control.width + col;
|
||||
int dstIndex = (control.yOffset + row) * width + control.xOffset + col;
|
||||
|
||||
int srcPixel = frame.bitmap.array[srcIndex];
|
||||
int srcPixel = frame.bitmap().array()[srcIndex];
|
||||
int dstPixel = currentFrameBuffer[dstIndex];
|
||||
|
||||
int srcAlpha = (srcPixel >>> 24) & 0xFF;
|
||||
@@ -444,7 +439,7 @@ public final class ImageUtils {
|
||||
PngAnimationControl animationControl = sequence.getAnimationControl();
|
||||
int cycleCount;
|
||||
if (animationControl != null) {
|
||||
cycleCount = animationControl.numPlays;
|
||||
cycleCount = animationControl.numPlays();
|
||||
if (cycleCount == 0)
|
||||
cycleCount = Timeline.INDEFINITE;
|
||||
} else {
|
||||
|
||||
@@ -9,22 +9,12 @@ package org.jackhuang.hmcl.ui.image.apng.argb8888;
|
||||
* to be compatible with the input array used to build Android Bitmap objects, though of course
|
||||
* its use is not limited to that.
|
||||
*/
|
||||
public final class Argb8888Bitmap {
|
||||
public final int[] array;
|
||||
public final int width;
|
||||
public final int height;
|
||||
public record Argb8888Bitmap(int[] array, int width, int height) {
|
||||
|
||||
public Argb8888Bitmap(int width, int height) {
|
||||
this(new int[width * height], width, height);
|
||||
}
|
||||
|
||||
public Argb8888Bitmap(int[] array, int width, int height) {//}, int x, int y) {
|
||||
this.array = array;
|
||||
this.width = width;
|
||||
this.height = height;
|
||||
// this.x = y;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new Bitmap that shares the byte array of this bitmap.
|
||||
* This is useful when rendering a number of animation frames from an APNG file
|
||||
@@ -37,7 +27,7 @@ public final class Argb8888Bitmap {
|
||||
* from the original bitmap).
|
||||
* @return new bitmap object sharing the same data array.
|
||||
*/
|
||||
public Argb8888Bitmap makeView(int width, int height) {//}, int x, int y) {
|
||||
public Argb8888Bitmap makeView(int width, int height) {
|
||||
if ((width * height) > (this.width * this.height)) {
|
||||
throw new IllegalArgumentException(String.format(
|
||||
"Requested width and height (%d x %d) exceeds maximum pixels allowed by host bitmap (%d x %d",
|
||||
|
||||
+3
-10
@@ -37,7 +37,7 @@ public final class Argb8888BitmapSequence {
|
||||
|
||||
public void receiveAnimationControl(PngAnimationControl animationControl) {
|
||||
this.animationControl = animationControl;
|
||||
this.animationFrames = new ArrayList<>(animationControl.numFrames);
|
||||
this.animationFrames = new ArrayList<>(animationControl.numFrames());
|
||||
}
|
||||
|
||||
public void receiveDefaultImage(Argb8888Bitmap bitmap) {
|
||||
@@ -49,7 +49,7 @@ public final class Argb8888BitmapSequence {
|
||||
}
|
||||
|
||||
public boolean isAnimated() {
|
||||
return null != animationControl && animationControl.numFrames > 0;
|
||||
return null != animationControl && animationControl.numFrames() > 0;
|
||||
}
|
||||
|
||||
public PngAnimationControl getAnimationControl() {
|
||||
@@ -60,13 +60,6 @@ public final class Argb8888BitmapSequence {
|
||||
return animationFrames;
|
||||
}
|
||||
|
||||
public static final class Frame {
|
||||
public final PngFrameControl control;
|
||||
public final Argb8888Bitmap bitmap;
|
||||
|
||||
public Frame(PngFrameControl control, Argb8888Bitmap bitmap) {
|
||||
this.control = control;
|
||||
this.bitmap = bitmap;
|
||||
}
|
||||
public record Frame(PngFrameControl control, Argb8888Bitmap bitmap) {
|
||||
}
|
||||
}
|
||||
@@ -12,28 +12,21 @@ import org.jackhuang.hmcl.ui.image.apng.error.PngIntegrityException;
|
||||
* Note that the transparency of individual entries in the palette is modified in-place
|
||||
* when the trNS chunk is processed.
|
||||
*
|
||||
* @param argbArray The colour array is public and mutable by design.
|
||||
* @see BasicArgb8888Director
|
||||
*/
|
||||
public class Argb8888Palette {
|
||||
public record Argb8888Palette(int[] argbArray) {
|
||||
protected static Argb8888Palette monochromePalette = null;
|
||||
protected static Argb8888Palette greyPalette2 = null;
|
||||
protected static Argb8888Palette greyPalette4 = null;
|
||||
protected static Argb8888Palette greyPalette8 = null;
|
||||
|
||||
/**
|
||||
* The colour array is public and mutable by design.
|
||||
*
|
||||
* @see this.get()
|
||||
*/
|
||||
public final int[] argbArray;
|
||||
|
||||
/**
|
||||
* Construct a new palette using the specific array passed as the argument as-is (not copied).
|
||||
*
|
||||
* @param argbArray array of colours to use as-is (it is not copied).
|
||||
*/
|
||||
public Argb8888Palette(int[] argbArray) {
|
||||
this.argbArray = argbArray;
|
||||
public Argb8888Palette {
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -89,29 +82,33 @@ public class Argb8888Palette {
|
||||
}
|
||||
|
||||
public static Argb8888Palette forGreyscale(int bitDepth) throws PngException {
|
||||
switch (bitDepth) {
|
||||
case 1:
|
||||
return switch (bitDepth) {
|
||||
case 1 -> {
|
||||
if (null == monochromePalette) {
|
||||
monochromePalette = forGreyscale(2, 0xff); // Worth it or not really?
|
||||
}
|
||||
return monochromePalette;
|
||||
case 2:
|
||||
yield monochromePalette;
|
||||
}
|
||||
case 2 -> {
|
||||
if (null == greyPalette2) {
|
||||
greyPalette2 = forGreyscale(4, 0x55);
|
||||
}
|
||||
return greyPalette2;
|
||||
case 4:
|
||||
yield greyPalette2;
|
||||
}
|
||||
case 4 -> {
|
||||
if (null == greyPalette4) {
|
||||
greyPalette4 = forGreyscale(16, 0x11);
|
||||
}
|
||||
return greyPalette4;
|
||||
case 8: // TODO: need??
|
||||
yield greyPalette4;
|
||||
}
|
||||
case 8 -> {
|
||||
if (null == greyPalette8) {
|
||||
greyPalette8 = forGreyscale(256, 0x01);
|
||||
}
|
||||
return greyPalette8;
|
||||
default:
|
||||
throw new PngIntegrityException(String.format("Valid greyscale bit depths are 1, 2, 4, 8, not %d", bitDepth));
|
||||
}
|
||||
yield greyPalette8;
|
||||
}
|
||||
default ->
|
||||
throw new PngIntegrityException(String.format("Valid greyscale bit depths are 1, 2, 4, 8, not %d", bitDepth));
|
||||
};
|
||||
}
|
||||
}
|
||||
+22
-22
@@ -122,8 +122,8 @@ public class Argb8888Processors {
|
||||
|
||||
@Override
|
||||
public void processScanline(byte[] srcBytes, int srcPosition) {
|
||||
final int[] destArray = this.bitmap.array;
|
||||
final int width = this.bitmap.width;
|
||||
final int[] destArray = this.bitmap.array();
|
||||
final int width = this.bitmap.width();
|
||||
int writePosition = this.y * width;
|
||||
int lastWritePosition = writePosition + width;
|
||||
int bit = highBit;
|
||||
@@ -136,7 +136,7 @@ public class Argb8888Processors {
|
||||
|
||||
int dest;
|
||||
if (palette != null) {
|
||||
dest = palette.argbArray[index];
|
||||
dest = palette.argbArray()[index];
|
||||
} else {
|
||||
dest = 0;
|
||||
}
|
||||
@@ -168,8 +168,8 @@ public class Argb8888Processors {
|
||||
|
||||
@Override
|
||||
public void processScanline(byte[] srcBytes, int srcPosition) {
|
||||
final int[] destArray = this.bitmap.array;
|
||||
final int width = this.bitmap.width;
|
||||
final int[] destArray = this.bitmap.array();
|
||||
final int width = this.bitmap.width();
|
||||
int writePosition = this.y * width;
|
||||
for (int x = 0; x < width; x++) {
|
||||
|
||||
@@ -177,7 +177,7 @@ public class Argb8888Processors {
|
||||
|
||||
int dest;
|
||||
if (palette != null) {
|
||||
dest = palette.argbArray[index];
|
||||
dest = palette.argbArray()[index];
|
||||
} else {
|
||||
dest = 0;
|
||||
}
|
||||
@@ -203,8 +203,8 @@ public class Argb8888Processors {
|
||||
|
||||
@Override
|
||||
public void processScanline(byte[] srcBytes, int srcPosition) {
|
||||
final int[] destArray = this.bitmap.array;
|
||||
final int width = this.bitmap.width;
|
||||
final int[] destArray = this.bitmap.array();
|
||||
final int width = this.bitmap.width();
|
||||
int writePosition = this.y * width;
|
||||
for (int x = 0; x < width; x++) {
|
||||
final int v = srcBytes[srcPosition++];
|
||||
@@ -236,8 +236,8 @@ public class Argb8888Processors {
|
||||
|
||||
@Override
|
||||
public void processScanline(byte[] srcBytes, int srcPosition) {
|
||||
final int[] destArray = this.bitmap.array;
|
||||
final int width = this.bitmap.width;
|
||||
final int[] destArray = this.bitmap.array();
|
||||
final int width = this.bitmap.width();
|
||||
final int alpha = 0xff000000; // No alpha in the image means every pixel must be fully opaque
|
||||
int writePosition = this.y * width;
|
||||
for (int x = 0; x < width; x++) {
|
||||
@@ -274,8 +274,8 @@ public class Argb8888Processors {
|
||||
|
||||
@Override
|
||||
public void processScanline(byte[] srcBytes, int srcPosition) {
|
||||
final int[] destArray = this.bitmap.array;
|
||||
final int width = this.bitmap.width;
|
||||
final int[] destArray = this.bitmap.array();
|
||||
final int width = this.bitmap.width();
|
||||
int writePosition = this.y * width;
|
||||
for (int x = 0; x < width; x++) {
|
||||
final int k = 0xff & srcBytes[srcPosition++];
|
||||
@@ -303,8 +303,8 @@ public class Argb8888Processors {
|
||||
|
||||
@Override
|
||||
public void processScanline(byte[] srcBytes, int srcPosition) {
|
||||
final int[] destArray = this.bitmap.array;
|
||||
final int width = this.bitmap.width;
|
||||
final int[] destArray = this.bitmap.array();
|
||||
final int width = this.bitmap.width();
|
||||
int writePosition = this.y * width;
|
||||
for (int x = 0; x < width; x++) {
|
||||
final int k = 0xff & srcBytes[srcPosition++];
|
||||
@@ -333,8 +333,8 @@ public class Argb8888Processors {
|
||||
|
||||
@Override
|
||||
public void processScanline(byte[] srcBytes, int srcPosition) {
|
||||
final int[] destArray = this.bitmap.array;
|
||||
final int width = this.bitmap.width;
|
||||
final int[] destArray = this.bitmap.array();
|
||||
final int width = this.bitmap.width();
|
||||
final int alpha = 0xff000000; // No alpha in the image means every pixel must be fully opaque
|
||||
int writePosition = this.y * width;
|
||||
for (int x = 0; x < width; x++) {
|
||||
@@ -364,8 +364,8 @@ public class Argb8888Processors {
|
||||
|
||||
@Override
|
||||
public void processScanline(byte[] srcBytes, int srcPosition) {
|
||||
final int[] destArray = this.bitmap.array;
|
||||
final int width = this.bitmap.width;
|
||||
final int[] destArray = this.bitmap.array();
|
||||
final int width = this.bitmap.width();
|
||||
int writePosition = this.y * width;
|
||||
//srcPosition++; // skip filter byte
|
||||
for (int x = 0; x < width; x++) {
|
||||
@@ -395,8 +395,8 @@ public class Argb8888Processors {
|
||||
|
||||
@Override
|
||||
public void processScanline(byte[] srcBytes, int srcPosition) {
|
||||
final int[] destArray = this.bitmap.array;
|
||||
final int width = this.bitmap.width;
|
||||
final int[] destArray = this.bitmap.array();
|
||||
final int width = this.bitmap.width();
|
||||
final int alpha = 0xff000000; // No alpha in the image means every pixel must be fully opaque
|
||||
int writePosition = this.y * width;
|
||||
//srcPosition++; // skip filter byte
|
||||
@@ -431,8 +431,8 @@ public class Argb8888Processors {
|
||||
|
||||
@Override
|
||||
public void processScanline(byte[] srcBytes, int srcPosition) {
|
||||
final int[] destArray = this.bitmap.array;
|
||||
final int width = this.bitmap.width;
|
||||
final int[] destArray = this.bitmap.array();
|
||||
final int width = this.bitmap.width();
|
||||
//final int alpha = 0xff000000; // No alpha in the image means every pixel must be fully opaque
|
||||
int writePosition = this.y * width;
|
||||
//srcPosition++; // skip filter byte
|
||||
|
||||
+2
-2
@@ -55,7 +55,7 @@ public abstract class Argb8888ScanlineProcessor extends BasicScanlineProcessor {
|
||||
public Argb8888ScanlineProcessor cloneWithNewBitmap(PngHeader header) {
|
||||
Argb8888ScanlineProcessor cloned = clone(header.bytesPerRow, new Argb8888Bitmap(header.width, header.height));
|
||||
if (this.palette != null) {
|
||||
cloned.setPalette(new Argb8888Palette(palette.argbArray));
|
||||
cloned.setPalette(new Argb8888Palette(palette.argbArray()));
|
||||
}
|
||||
return cloned;
|
||||
|
||||
@@ -71,7 +71,7 @@ public abstract class Argb8888ScanlineProcessor extends BasicScanlineProcessor {
|
||||
public Argb8888ScanlineProcessor cloneWithSharedBitmap(PngHeader header) {
|
||||
Argb8888ScanlineProcessor cloned = clone(header.bytesPerRow, bitmap.makeView(header.width, header.height));
|
||||
if (this.palette != null) {
|
||||
cloned.setPalette(new Argb8888Palette(palette.argbArray));
|
||||
cloned.setPalette(new Argb8888Palette(palette.argbArray()));
|
||||
}
|
||||
return cloned;
|
||||
|
||||
|
||||
+1
-1
@@ -28,7 +28,7 @@ public abstract class BasicArgb8888Director<ResultT> implements Argb8888Director
|
||||
}
|
||||
for (int i = 0; i < length; i++) {
|
||||
final int alpha = 0xff & bytes[position + i];
|
||||
palette.argbArray[i] = alpha << 24 | palette.argbArray[i] & 0x00FFFFFF;
|
||||
palette.argbArray()[i] = alpha << 24 | palette.argbArray()[i] & 0x00FFFFFF;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -3,25 +3,20 @@
|
||||
|
||||
package org.jackhuang.hmcl.ui.image.apng.chunks;
|
||||
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
/**
|
||||
* A PngAnimationControl object contains data parsed from the ``acTL``
|
||||
* animation control chunk of an animated PNG file.
|
||||
*/
|
||||
public class PngAnimationControl {
|
||||
public final int numFrames;
|
||||
public final int numPlays;
|
||||
|
||||
public PngAnimationControl(int numFrames, int numPlays) {
|
||||
this.numFrames = numFrames;
|
||||
this.numPlays = numPlays;
|
||||
}
|
||||
public record PngAnimationControl(int numFrames, int numPlays) {
|
||||
|
||||
public boolean loopForever() {
|
||||
return 0 == numPlays;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
public @NotNull String toString() {
|
||||
return "PngAnimationControl{" +
|
||||
"numFrames=" + numFrames +
|
||||
", numPlays=" + numPlays +
|
||||
|
||||
@@ -9,12 +9,7 @@ import java.io.IOException;
|
||||
/**
|
||||
* A PngGamma object represents data parsed from a ``gAMA`` chunk.
|
||||
*/
|
||||
public class PngGamma {
|
||||
public final int imageGamma;
|
||||
|
||||
public PngGamma(int imageGamma) {
|
||||
this.imageGamma = imageGamma;
|
||||
}
|
||||
public record PngGamma(int imageGamma) {
|
||||
|
||||
public static PngGamma from(DataInputStream dis) throws IOException {
|
||||
return new PngGamma(dis.readInt());
|
||||
|
||||
@@ -49,7 +49,7 @@ public abstract class PngContainerProcessor<ResultT> implements PngChunkProcesso
|
||||
@Override
|
||||
public void processAnimationControl(PngAnimationControl animationControl) {
|
||||
container.animationControl = animationControl;
|
||||
container.animationFrames = new ArrayList<PngFrameControl>(container.animationControl.numFrames);
|
||||
container.animationFrames = new ArrayList<PngFrameControl>(container.animationControl.numFrames());
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -371,7 +371,7 @@ public final class JavaDownloadDialog extends StackPane {
|
||||
if (version == null)
|
||||
return;
|
||||
|
||||
Controllers.taskDialog(new GetTask(downloadProvider.injectURLWithCandidates(version.getLinks().getPkgInfoUri()))
|
||||
Controllers.taskDialog(new GetTask(downloadProvider.injectURLWithCandidates(version.getLinks().pkgInfoUri()))
|
||||
.setExecutor(Schedulers.io())
|
||||
.thenComposeAsync(json -> {
|
||||
DiscoResult<DiscoRemoteFileInfo> result = JsonUtils.fromNonNullJson(json, DiscoResult.typeOf(DiscoRemoteFileInfo.class));
|
||||
@@ -379,20 +379,20 @@ public final class JavaDownloadDialog extends StackPane {
|
||||
throw new IOException("Illegal result: " + json);
|
||||
|
||||
DiscoRemoteFileInfo fileInfo = result.getResult().get(0);
|
||||
if (StringUtils.isNotBlank(fileInfo.getChecksumType())
|
||||
&& !fileInfo.getChecksumType().equals("sha1") && !fileInfo.getChecksumType().equals("sha256") && !fileInfo.getChecksumType().equals("md5"))
|
||||
throw new IOException("Unsupported checksum type: " + fileInfo.getChecksumType());
|
||||
if (StringUtils.isBlank(fileInfo.getDirectDownloadUri()))
|
||||
if (StringUtils.isNotBlank(fileInfo.checksumType())
|
||||
&& !fileInfo.checksumType().equals("sha1") && !fileInfo.checksumType().equals("sha256") && !fileInfo.checksumType().equals("md5"))
|
||||
throw new IOException("Unsupported checksum type: " + fileInfo.checksumType());
|
||||
if (StringUtils.isBlank(fileInfo.directDownloadUri()))
|
||||
throw new IOException("Missing download URI: " + json);
|
||||
|
||||
Path targetFile = Files.createTempFile("hmcl-java-", "." + version.getArchiveType());
|
||||
targetFile.toFile().deleteOnExit();
|
||||
|
||||
Task<FileDownloadTask.IntegrityCheck> getIntegrityCheck;
|
||||
if (StringUtils.isNotBlank(fileInfo.getChecksum()))
|
||||
getIntegrityCheck = Task.completed(new FileDownloadTask.IntegrityCheck(fileInfo.getChecksumType(), fileInfo.getChecksum()));
|
||||
else if (StringUtils.isNotBlank(fileInfo.getChecksumUri()))
|
||||
getIntegrityCheck = new GetTask(downloadProvider.injectURLWithCandidates(fileInfo.getChecksumUri()))
|
||||
if (StringUtils.isNotBlank(fileInfo.checksum()))
|
||||
getIntegrityCheck = Task.completed(new FileDownloadTask.IntegrityCheck(fileInfo.checksumType(), fileInfo.checksum()));
|
||||
else if (StringUtils.isNotBlank(fileInfo.checksumUri()))
|
||||
getIntegrityCheck = new GetTask(downloadProvider.injectURLWithCandidates(fileInfo.checksumUri()))
|
||||
.thenApplyAsync(checksum -> {
|
||||
checksum = checksum.trim();
|
||||
|
||||
@@ -400,15 +400,15 @@ public final class JavaDownloadDialog extends StackPane {
|
||||
if (idx > 0)
|
||||
checksum = checksum.substring(0, idx);
|
||||
|
||||
return new FileDownloadTask.IntegrityCheck(fileInfo.getChecksumType(), checksum);
|
||||
return new FileDownloadTask.IntegrityCheck(fileInfo.checksumType(), checksum);
|
||||
});
|
||||
else
|
||||
getIntegrityCheck = Task.completed(null);
|
||||
|
||||
return getIntegrityCheck
|
||||
.thenComposeAsync(integrityCheck ->
|
||||
new FileDownloadTask(downloadProvider.injectURLWithCandidates(fileInfo.getDirectDownloadUri()),
|
||||
targetFile, integrityCheck).setName(fileInfo.getFileName()))
|
||||
new FileDownloadTask(downloadProvider.injectURLWithCandidates(fileInfo.directDownloadUri()),
|
||||
targetFile, integrityCheck).setName(fileInfo.fileName()))
|
||||
.thenSupplyAsync(() -> targetFile);
|
||||
})
|
||||
.whenComplete(Schedulers.javafx(), ((result, exception) -> {
|
||||
|
||||
@@ -294,30 +294,33 @@ public class PersonalizationPage extends StackPane {
|
||||
String userName = System.getProperty("user.name").trim();
|
||||
String defaultAuthorName = StringUtils.isBlank(userName) ? "Unknown" : userName;
|
||||
|
||||
PromptDialogPane.Builder.StringQuestion packIdQuestion = new PromptDialogPane.Builder.StringQuestion(
|
||||
i18n("theme_pack.export.id"),
|
||||
"")
|
||||
.setPromptText(defaultPackId);
|
||||
PromptDialogPane.Builder.StringQuestion packNameQuestion = new PromptDialogPane.Builder.StringQuestion(
|
||||
i18n("theme_pack.export.name"),
|
||||
"")
|
||||
.setPromptText(defaultPackName);
|
||||
PromptDialogPane.Builder.StringQuestion versionQuestion = new PromptDialogPane.Builder.StringQuestion(
|
||||
i18n("theme_pack.export.version"),
|
||||
"")
|
||||
.setPromptText(ThemePackManager.CURRENT_THEME_PACK_VERSION);
|
||||
PromptDialogPane.Builder.StringQuestion authorNameQuestion = new PromptDialogPane.Builder.StringQuestion(
|
||||
i18n("theme_pack.export.author"),
|
||||
"")
|
||||
.setPromptText(defaultAuthorName);
|
||||
|
||||
Controllers.prompt(new PromptDialogPane.Builder(i18n("theme_pack.export.title"), (questions, handler) -> handler.resolve())
|
||||
.addQuestion(packIdQuestion)
|
||||
.addQuestion(packNameQuestion)
|
||||
.addQuestion(versionQuestion)
|
||||
.addQuestion(authorNameQuestion)).thenAccept(questions -> exportCurrentThemePack(
|
||||
StringUtils.isBlank(packIdQuestion.getValue()) ? defaultPackId : packIdQuestion.getValue().trim(),
|
||||
defaultPackId,
|
||||
StringUtils.isBlank(versionQuestion.getValue())
|
||||
? ThemePackManager.CURRENT_THEME_PACK_VERSION
|
||||
: versionQuestion.getValue().trim(),
|
||||
StringUtils.isBlank(packNameQuestion.getValue()) ? defaultPackName : packNameQuestion.getValue().trim(),
|
||||
StringUtils.isBlank(authorNameQuestion.getValue()) ? defaultAuthorName : authorNameQuestion.getValue().trim()));
|
||||
}
|
||||
|
||||
/// Saves current launcher appearance as a theme-pack file with the given package metadata.
|
||||
private void exportCurrentThemePack(String packId, String packName, String authorName) {
|
||||
private void exportCurrentThemePack(String packId, String version, String packName, String authorName) {
|
||||
FileChooser chooser = new FileChooser();
|
||||
chooser.setTitle(i18n("theme_pack.export.title"));
|
||||
String initialFileName = sanitizeThemePackFileName(packName) + ThemePackExporter.FILE_EXTENSION;
|
||||
@@ -340,6 +343,7 @@ public class PersonalizationPage extends StackPane {
|
||||
ThemePackManager.exportCurrent(
|
||||
output,
|
||||
packId,
|
||||
version,
|
||||
packName,
|
||||
authorName);
|
||||
Controllers.dialog(
|
||||
|
||||
@@ -41,7 +41,6 @@ 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.util.AprilFools;
|
||||
import org.jackhuang.hmcl.util.Lang;
|
||||
import org.jackhuang.hmcl.util.StringUtils;
|
||||
import org.jackhuang.hmcl.util.i18n.I18n;
|
||||
@@ -195,7 +194,7 @@ public final class SettingsPage extends ScrollPane {
|
||||
{
|
||||
ComponentList miscPaneList = new ComponentList();
|
||||
|
||||
if (AprilFools.isShowAprilFoolsSettings()) {
|
||||
{
|
||||
LineToggleButton disableAprilFools = new LineToggleButton();
|
||||
disableAprilFools.setTitle(i18n("settings.launcher.disable_april_fools"));
|
||||
disableAprilFools.setSubtitle(i18n("settings.take_effect_after_restart"));
|
||||
|
||||
@@ -32,8 +32,7 @@ import javafx.scene.Cursor;
|
||||
import javafx.scene.Node;
|
||||
import javafx.scene.control.Label;
|
||||
import javafx.scene.control.ScrollPane;
|
||||
import javafx.scene.layout.HBox;
|
||||
import javafx.scene.layout.Priority;
|
||||
import javafx.scene.image.ImageView;
|
||||
import javafx.scene.layout.StackPane;
|
||||
import javafx.scene.layout.VBox;
|
||||
import javafx.scene.text.TextFlow;
|
||||
@@ -550,17 +549,10 @@ public class TerracottaControllerPage extends StackPane {
|
||||
|
||||
private ComponentSublist getThirdPartyDownloadNodes() {
|
||||
ComponentSublist locals = new ComponentSublist();
|
||||
|
||||
var header = new LinePane();
|
||||
header.getStyleClass().add("no-padding");
|
||||
header.setLargeTitle(true);
|
||||
header.setMinHeight(LinePane.USE_COMPUTED_SIZE);
|
||||
HBox.setHgrow(header, Priority.ALWAYS);
|
||||
header.setMouseTransparent(true);
|
||||
header.setLeading(FXUtils.newBuiltinImage("/assets/img/terracotta.png"));
|
||||
header.setTitle(i18n("terracotta.from_local.title"));
|
||||
header.setSubtitle(i18n("terracotta.from_local.desc"));
|
||||
locals.setHeaderLeft(header);
|
||||
locals.setLargeTitle(true);
|
||||
locals.setLeading(new ImageView(FXUtils.newBuiltinImage("/assets/img/terracotta.png")));
|
||||
locals.setTitle(i18n("terracotta.from_local.title"));
|
||||
locals.setSubtitle(i18n("terracotta.from_local.desc"));
|
||||
|
||||
for (TerracottaMetadata.Link link : TerracottaMetadata.PACKAGE_LINKS) {
|
||||
LineButton item = new LineButton();
|
||||
|
||||
@@ -328,46 +328,6 @@ final class ModListPageSkin extends SkinBase<ModListPage> {
|
||||
iconPaths.add(this.localModFile.getLogoPath());
|
||||
}
|
||||
|
||||
iconPaths.addAll(List.of(
|
||||
"icon.png",
|
||||
"logo.png",
|
||||
"mod_logo.png",
|
||||
"pack.png",
|
||||
"logoFile.png",
|
||||
"assets/icon.png",
|
||||
"assets/logo.png",
|
||||
"assets/mod_icon.png",
|
||||
"assets/mod_logo.png",
|
||||
"META-INF/icon.png",
|
||||
"META-INF/logo.png",
|
||||
"META-INF/mod_icon.png",
|
||||
"textures/icon.png",
|
||||
"textures/logo.png",
|
||||
"textures/mod_icon.png",
|
||||
"resources/icon.png",
|
||||
"resources/logo.png",
|
||||
"resources/mod_icon.png"
|
||||
));
|
||||
|
||||
String modId = this.localModFile.getId();
|
||||
if (StringUtils.isNotBlank(modId)) {
|
||||
iconPaths.addAll(List.of(
|
||||
"assets/" + modId + "/icon.png",
|
||||
"assets/" + modId + "/logo.png",
|
||||
"assets/" + modId.replace("-", "") + "/icon.png",
|
||||
"assets/" + modId.replace("-", "") + "/logo.png",
|
||||
modId + ".png",
|
||||
modId + "-logo.png",
|
||||
modId + "-icon.png",
|
||||
modId + "_logo.png",
|
||||
modId + "_icon.png",
|
||||
"textures/" + modId + "/icon.png",
|
||||
"textures/" + modId + "/logo.png",
|
||||
"resources/" + modId + "/icon.png",
|
||||
"resources/" + modId + "/logo.png"
|
||||
));
|
||||
}
|
||||
|
||||
try (FileSystem fs = CompressingUtils.createReadOnlyZipFileSystem(this.localModFile.getFile())) {
|
||||
for (String path : iconPaths) {
|
||||
Path iconPath = fs.getPath(path);
|
||||
|
||||
@@ -34,7 +34,6 @@ import static org.jackhuang.hmcl.setting.SettingsManager.settings;
|
||||
public final class AprilFools {
|
||||
|
||||
private static final boolean ENABLED;
|
||||
private static final boolean SHOW_APRIL_FOOLS_SETTINGS;
|
||||
|
||||
static {
|
||||
var date = LocalDate.now();
|
||||
@@ -58,14 +57,6 @@ public final class AprilFools {
|
||||
aprilFoolsMode = date.getMonth() == Month.APRIL && date.getDayOfMonth() == 1;
|
||||
|
||||
ENABLED = aprilFoolsMode && !settings().disableAprilFoolsProperty().get();
|
||||
SHOW_APRIL_FOOLS_SETTINGS = aprilFoolsMode || supportedRegion && date.getMonth() == Month.MARCH && date.getDayOfMonth() > 30;
|
||||
}
|
||||
|
||||
/// Whether April Fools settings should be shown.
|
||||
///
|
||||
/// This method returns true if April Fools settings should be shown.
|
||||
public static boolean isShowAprilFoolsSettings() {
|
||||
return SHOW_APRIL_FOOLS_SETTINGS;
|
||||
}
|
||||
|
||||
/// Whether April Fools is enabled.
|
||||
|
||||
@@ -1581,7 +1581,7 @@
|
||||
-fx-background-color: TRANSPARENT, TRANSPARENT, TRANSPARENT, TRANSPARENT;
|
||||
-fx-background-radius: 3px;
|
||||
-fx-background-insets: 0px;
|
||||
-fx-min-height: 25px;
|
||||
-fx-min-height: 36px;
|
||||
}
|
||||
|
||||
.color-palette-region {
|
||||
|
||||
@@ -1374,6 +1374,7 @@ settings.advanced.game_dir.default=Default (".minecraft/")
|
||||
settings.advanced.game_dir.independent=Isolated (".minecraft/versions/<instance name>/", except for assets and libraries)
|
||||
settings.advanced.graphics=Graphics Settings
|
||||
settings.advanced.graphics_backend=Graphics API
|
||||
settings.advanced.graphics_backend.desc=Only effective for Minecraft 26.2+
|
||||
settings.advanced.graphics_backend.default=Default
|
||||
settings.advanced.graphics_backend.default.desc=Follow the game's settings
|
||||
settings.advanced.graphics_backend.opengl=OpenGL
|
||||
@@ -1631,7 +1632,7 @@ settings.type.global.preset=Preset
|
||||
settings.type.global.preset.create=Create Preset
|
||||
settings.type.global.preset.default=Use Default Preset
|
||||
settings.type.global.preset.duplicate_name=Preset name already exists
|
||||
settings.type.global.preset.manage_all=Default Preset
|
||||
settings.type.global.preset.manage_all=Current Preset
|
||||
settings.type.global.preset.name=Preset Name
|
||||
settings.type.global.preset.rename=Rename Preset
|
||||
settings.type.global.preset.remove=Delete Preset
|
||||
@@ -1749,6 +1750,7 @@ theme_pack.export.id=Theme Pack ID
|
||||
theme_pack.export.name=Theme Name
|
||||
theme_pack.export.subtitle=Save the current launcher appearance as a theme-pack file.
|
||||
theme_pack.export.success=Theme pack has been saved to "%s".
|
||||
theme_pack.export.version=Theme Version
|
||||
theme_pack.export.title=Save theme pack
|
||||
theme_pack.file=HMCL Theme Pack
|
||||
theme_pack.import=Import Theme Pack
|
||||
|
||||
@@ -1167,6 +1167,7 @@ settings.advanced.game_dir.default=預設 (".minecraft/")
|
||||
settings.advanced.game_dir.independent=各實例獨立 (".minecraft/versions/<實例名>/",除 assets、libraries 外)
|
||||
settings.advanced.graphics=圖形設定
|
||||
settings.advanced.graphics_backend=圖形 API
|
||||
settings.advanced.graphics_backend.desc=僅對 Minecraft 26.2+ 生效
|
||||
settings.advanced.graphics_backend.default=預設
|
||||
settings.advanced.graphics_backend.default.desc=遵循遊戲設定
|
||||
settings.advanced.graphics_backend.opengl=OpenGL
|
||||
@@ -1342,7 +1343,7 @@ settings.launcher.download.threads.auto=自動選取執行緒數
|
||||
settings.launcher.download.threads.hint=執行緒數過高可能導致系統卡頓。你的下載速度會受到網際網路運營商、下載來源伺服器等方面的影響。調高下載執行緒數不一定能大幅提升總下載速度。
|
||||
settings.launcher.download_source=下載來源
|
||||
settings.launcher.download_source.auto=自動選取下載來源
|
||||
settings.launcher.default_addon_source=遊戲內容預設下載來源
|
||||
settings.launcher.default_addon_source=附加內容預設下載來源
|
||||
settings.launcher.enable_game_list=在首頁內顯示遊戲清單
|
||||
settings.launcher.misc=雜項
|
||||
settings.launcher.font=字體
|
||||
@@ -1422,7 +1423,7 @@ settings.type.global.preset=預設
|
||||
settings.type.global.preset.create=新增預設
|
||||
settings.type.global.preset.default=使用預設項目
|
||||
settings.type.global.preset.duplicate_name=預設名稱已存在
|
||||
settings.type.global.preset.manage_all=預設項目
|
||||
settings.type.global.preset.manage_all=當前預設
|
||||
settings.type.global.preset.name=預設名稱
|
||||
settings.type.global.preset.rename=重新命名預設
|
||||
settings.type.global.preset.remove=刪除預設
|
||||
@@ -1540,6 +1541,7 @@ theme_pack.export.id=主題包 ID
|
||||
theme_pack.export.name=主題名稱
|
||||
theme_pack.export.subtitle=將目前啟動器外觀儲存為主題包檔案
|
||||
theme_pack.export.success=主題包已儲存到「%s」
|
||||
theme_pack.export.version=主題版本
|
||||
theme_pack.export.title=儲存主題包
|
||||
theme_pack.file=HMCL 主題包
|
||||
theme_pack.import=匯入主題包
|
||||
|
||||
@@ -1172,6 +1172,7 @@ settings.advanced.game_dir.default=默认 (".minecraft/")
|
||||
settings.advanced.game_dir.independent=各实例独立 (存放在 ".minecraft/versions/<实例名>/",除 assets、libraries 外)
|
||||
settings.advanced.graphics=图形设置
|
||||
settings.advanced.graphics_backend=图形 API
|
||||
settings.advanced.graphics_backend.desc=仅对 Minecraft 26.2+ 生效
|
||||
settings.advanced.graphics_backend.default=默认
|
||||
settings.advanced.graphics_backend.default.desc=遵循游戏设置
|
||||
settings.advanced.graphics_backend.opengl=OpenGL
|
||||
@@ -1347,7 +1348,7 @@ settings.launcher.download.threads.auto=自动选择线程数
|
||||
settings.launcher.download.threads.hint=线程数过高可能导致系统卡顿。你的下载速度会受到互联网运营商、下载源服务器等方面的影响。调高下载线程数不一定能大幅提升总下载速度。
|
||||
settings.launcher.download_source=下载源
|
||||
settings.launcher.download_source.auto=自动选择下载源
|
||||
settings.launcher.default_addon_source=游戏内容默认下载源
|
||||
settings.launcher.default_addon_source=附加内容默认下载源
|
||||
settings.launcher.enable_game_list=在主页内显示版本列表
|
||||
settings.launcher.misc=杂项
|
||||
settings.launcher.font=启动器字体
|
||||
@@ -1427,7 +1428,7 @@ settings.type.global.preset=预设
|
||||
settings.type.global.preset.create=新建预设
|
||||
settings.type.global.preset.default=使用默认预设
|
||||
settings.type.global.preset.duplicate_name=预设名称已存在
|
||||
settings.type.global.preset.manage_all=默认预设
|
||||
settings.type.global.preset.manage_all=当前预设
|
||||
settings.type.global.preset.name=预设名称
|
||||
settings.type.global.preset.rename=重命名预设
|
||||
settings.type.global.preset.remove=删除预设
|
||||
@@ -1545,6 +1546,7 @@ theme_pack.export.id=主题包 ID
|
||||
theme_pack.export.name=主题名称
|
||||
theme_pack.export.subtitle=将当前启动器外观保存为主题包文件
|
||||
theme_pack.export.success=主题包已保存到“%s”
|
||||
theme_pack.export.version=主题版本
|
||||
theme_pack.export.title=保存主题包
|
||||
theme_pack.file=HMCL 主题包
|
||||
theme_pack.import=导入主题包
|
||||
|
||||
@@ -33,6 +33,7 @@ import org.jackhuang.hmcl.util.gson.JsonUtils;
|
||||
import org.jackhuang.hmcl.util.i18n.LocalizedText;
|
||||
import org.jetbrains.annotations.NotNullByDefault;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.io.TempDir;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.lang.reflect.Field;
|
||||
@@ -407,6 +408,193 @@ public final class GameDirectoriesTest {
|
||||
}
|
||||
}
|
||||
|
||||
/// Tests that instance settings without an explicit parent use the default preset.
|
||||
@Test
|
||||
public void nullInstanceParentUsesDefaultPresetInsteadOfLegacyGameDirectoryPreset()
|
||||
throws ReflectiveOperationException {
|
||||
GameSettingsPresetID defaultPresetId =
|
||||
GameSettingsPresetID.parse("game-settings-preset:123e4567-e89b-12d3-a456-426614174000");
|
||||
GameSettingsPresetID legacyPresetId =
|
||||
GameSettingsPresetID.parse("game-settings-preset:123e4567-e89b-12d3-a456-426614174001");
|
||||
GameSettings.Preset defaultPreset = new GameSettings.Preset(defaultPresetId);
|
||||
GameSettings.Preset legacyPreset = new GameSettings.Preset(legacyPresetId);
|
||||
GameSettingsPresets presets = new GameSettingsPresets();
|
||||
presets.getPresets().setAll(defaultPreset, legacyPreset);
|
||||
|
||||
GameDirectory gameDirectory = new GameDirectory(
|
||||
GameDirectoryID.generate(),
|
||||
LocalizedText.plain("Dev"),
|
||||
PortablePath.of("local/Dev"),
|
||||
legacyPresetId);
|
||||
GameDirectories localDirectories = new GameDirectories();
|
||||
localDirectories.getGameDirectories().add(gameDirectory);
|
||||
GameDirectories userDirectories = new GameDirectories();
|
||||
|
||||
try (GameDirectoryEnvironment ignored =
|
||||
new GameDirectoryEnvironment(localDirectories, userDirectories, presets)) {
|
||||
settings().defaultGameSettingsPresetProperty().set(defaultPresetId);
|
||||
HMCLGameRepository repository = new HMCLGameRepository(gameDirectory);
|
||||
|
||||
assertSame(defaultPreset, repository.getParentGameSettings(new GameSettings.Instance()));
|
||||
}
|
||||
}
|
||||
|
||||
/// Tests that legacy per-version settings migration stores the game directory preset as an explicit parent.
|
||||
@Test
|
||||
public void legacyInstanceSettingsMigrationStoresLegacyGameDirectoryPresetAsParent(@TempDir Path tempDirectory)
|
||||
throws IOException, ReflectiveOperationException {
|
||||
GameSettingsPresetID defaultPresetId =
|
||||
GameSettingsPresetID.parse("game-settings-preset:123e4567-e89b-12d3-a456-426614174000");
|
||||
GameSettingsPresetID legacyPresetId =
|
||||
GameSettingsPresetID.parse("game-settings-preset:123e4567-e89b-12d3-a456-426614174001");
|
||||
GameSettingsPresets presets = new GameSettingsPresets();
|
||||
presets.getPresets().setAll(
|
||||
new GameSettings.Preset(defaultPresetId),
|
||||
new GameSettings.Preset(legacyPresetId));
|
||||
|
||||
GameDirectory gameDirectory = new GameDirectory(
|
||||
GameDirectoryID.generate(),
|
||||
LocalizedText.plain("Dev"),
|
||||
PortablePath.of(tempDirectory.toString()),
|
||||
legacyPresetId);
|
||||
GameDirectories localDirectories = new GameDirectories();
|
||||
localDirectories.getGameDirectories().add(gameDirectory);
|
||||
GameDirectories userDirectories = new GameDirectories();
|
||||
|
||||
try (GameDirectoryEnvironment ignored =
|
||||
new GameDirectoryEnvironment(localDirectories, userDirectories, presets)) {
|
||||
settings().defaultGameSettingsPresetProperty().set(defaultPresetId);
|
||||
HMCLGameRepository repository = new HMCLGameRepository(gameDirectory);
|
||||
Path versionRoot = repository.getVersionRoot("1.20.1");
|
||||
Files.createDirectories(versionRoot);
|
||||
Files.writeString(versionRoot.resolve("hmclversion.cfg"), """
|
||||
{
|
||||
"usesGlobal": true
|
||||
}
|
||||
""");
|
||||
|
||||
GameSettings.Instance setting = Objects.requireNonNull(repository.getInstanceGameSettings("1.20.1"));
|
||||
|
||||
assertEquals(legacyPresetId, setting.parentProperty().getValue());
|
||||
}
|
||||
}
|
||||
|
||||
/// Tests that startup migration leaves legacy per-version settings for lazy migration.
|
||||
@Test
|
||||
public void startupMigrationSkipsLegacyInstanceSettingsFile(@TempDir Path tempDirectory)
|
||||
throws IOException, ReflectiveOperationException {
|
||||
GameSettingsPresetID defaultPresetId =
|
||||
GameSettingsPresetID.parse("game-settings-preset:123e4567-e89b-12d3-a456-426614174000");
|
||||
GameSettingsPresetID legacyPresetId =
|
||||
GameSettingsPresetID.parse("game-settings-preset:123e4567-e89b-12d3-a456-426614174001");
|
||||
GameSettingsPresets presets = new GameSettingsPresets();
|
||||
presets.getPresets().setAll(
|
||||
new GameSettings.Preset(defaultPresetId),
|
||||
new GameSettings.Preset(legacyPresetId));
|
||||
|
||||
GameDirectory gameDirectory = new GameDirectory(
|
||||
GameDirectoryID.generate(),
|
||||
LocalizedText.plain("Dev"),
|
||||
PortablePath.of(tempDirectory.toString()),
|
||||
legacyPresetId);
|
||||
GameDirectories localDirectories = new GameDirectories();
|
||||
localDirectories.getGameDirectories().add(gameDirectory);
|
||||
GameDirectories userDirectories = new GameDirectories();
|
||||
|
||||
try (GameDirectoryEnvironment ignored =
|
||||
new GameDirectoryEnvironment(localDirectories, userDirectories, presets)) {
|
||||
settings().defaultGameSettingsPresetProperty().set(defaultPresetId);
|
||||
HMCLGameRepository repository = new HMCLGameRepository(gameDirectory);
|
||||
writeVersionJson(repository, "1.20.1");
|
||||
Path versionRoot = repository.getVersionRoot("1.20.1");
|
||||
Files.writeString(versionRoot.resolve(LegacyGameSettingsMigrator.LEGACY_INSTANCE_SETTINGS_FILENAME), """
|
||||
{
|
||||
"usesGlobal": true
|
||||
}
|
||||
""");
|
||||
|
||||
LegacyConfigMigrator.migrateLegacyInstanceGameSettings(localDirectories, presets);
|
||||
|
||||
assertFalse(Files.exists(repository.getInstanceConfigDirectory("1.20.1")
|
||||
.resolve(LegacyGameSettingsMigrator.INSTANCE_GAME_SETTINGS_FILENAME)));
|
||||
GameSettings.Instance setting = Objects.requireNonNull(repository.getInstanceGameSettings("1.20.1"));
|
||||
assertEquals(legacyPresetId, setting.parentProperty().getValue());
|
||||
}
|
||||
}
|
||||
|
||||
/// Tests that existing versions without instance settings store the legacy game directory parent.
|
||||
@Test
|
||||
public void absentInstanceSettingsStoreLegacyGameDirectoryPresetAsParent(@TempDir Path tempDirectory)
|
||||
throws IOException, ReflectiveOperationException {
|
||||
GameSettingsPresetID defaultPresetId =
|
||||
GameSettingsPresetID.parse("game-settings-preset:123e4567-e89b-12d3-a456-426614174000");
|
||||
GameSettingsPresetID legacyPresetId =
|
||||
GameSettingsPresetID.parse("game-settings-preset:123e4567-e89b-12d3-a456-426614174001");
|
||||
GameSettings.Preset legacyPreset = new GameSettings.Preset(legacyPresetId);
|
||||
legacyPreset.defaultIsolationTypeProperty().setValue(DefaultIsolationType.ALWAYS);
|
||||
GameSettingsPresets presets = new GameSettingsPresets();
|
||||
presets.getPresets().setAll(
|
||||
new GameSettings.Preset(defaultPresetId),
|
||||
legacyPreset);
|
||||
|
||||
GameDirectory gameDirectory = new GameDirectory(
|
||||
GameDirectoryID.generate(),
|
||||
LocalizedText.plain("Dev"),
|
||||
PortablePath.of(tempDirectory.toString()),
|
||||
legacyPresetId);
|
||||
GameDirectories localDirectories = new GameDirectories();
|
||||
localDirectories.getGameDirectories().add(gameDirectory);
|
||||
GameDirectories userDirectories = new GameDirectories();
|
||||
|
||||
try (GameDirectoryEnvironment ignored =
|
||||
new GameDirectoryEnvironment(localDirectories, userDirectories, presets)) {
|
||||
settings().defaultGameSettingsPresetProperty().set(defaultPresetId);
|
||||
HMCLGameRepository repository = new HMCLGameRepository(gameDirectory);
|
||||
writeVersionJson(repository, "1.20.1");
|
||||
LegacyConfigMigrator.migrateLegacyInstanceGameSettings(localDirectories, presets);
|
||||
|
||||
GameSettings.Instance setting = Objects.requireNonNull(repository.getInstanceGameSettings("1.20.1"));
|
||||
|
||||
assertEquals(legacyPresetId, setting.parentProperty().getValue());
|
||||
assertTrue(setting.getOverrideProperties().contains(GameSettings.PROPERTY_RUNNING_DIRECTORY));
|
||||
}
|
||||
}
|
||||
|
||||
/// Tests that versions created after migration do not inherit the legacy game directory parent.
|
||||
@Test
|
||||
public void newInstanceAfterMigrationDoesNotUseLegacyGameDirectoryParent(@TempDir Path tempDirectory)
|
||||
throws IOException, ReflectiveOperationException {
|
||||
GameSettingsPresetID defaultPresetId =
|
||||
GameSettingsPresetID.parse("game-settings-preset:123e4567-e89b-12d3-a456-426614174000");
|
||||
GameSettingsPresetID legacyPresetId =
|
||||
GameSettingsPresetID.parse("game-settings-preset:123e4567-e89b-12d3-a456-426614174001");
|
||||
GameSettings.Preset defaultPreset = new GameSettings.Preset(defaultPresetId);
|
||||
defaultPreset.defaultIsolationTypeProperty().setValue(DefaultIsolationType.NEVER);
|
||||
GameSettings.Preset legacyPreset = new GameSettings.Preset(legacyPresetId);
|
||||
legacyPreset.defaultIsolationTypeProperty().setValue(DefaultIsolationType.ALWAYS);
|
||||
GameSettingsPresets presets = new GameSettingsPresets();
|
||||
presets.getPresets().setAll(defaultPreset, legacyPreset);
|
||||
|
||||
GameDirectory gameDirectory = new GameDirectory(
|
||||
GameDirectoryID.generate(),
|
||||
LocalizedText.plain("Dev"),
|
||||
PortablePath.of(tempDirectory.toString()),
|
||||
legacyPresetId);
|
||||
GameDirectories localDirectories = new GameDirectories();
|
||||
localDirectories.getGameDirectories().add(gameDirectory);
|
||||
GameDirectories userDirectories = new GameDirectories();
|
||||
|
||||
try (GameDirectoryEnvironment ignored =
|
||||
new GameDirectoryEnvironment(localDirectories, userDirectories, presets)) {
|
||||
settings().defaultGameSettingsPresetProperty().set(defaultPresetId);
|
||||
HMCLGameRepository repository = new HMCLGameRepository(gameDirectory);
|
||||
LegacyConfigMigrator.migrateLegacyInstanceGameSettings(localDirectories, presets);
|
||||
writeVersionJson(repository, "1.20.1");
|
||||
|
||||
assertNull(repository.getInstanceGameSettings("1.20.1"));
|
||||
}
|
||||
}
|
||||
|
||||
/// Temporary static state override for game directory tests.
|
||||
private static final class GameDirectoryEnvironment implements AutoCloseable {
|
||||
/// The reflected SettingsManager local game directories field.
|
||||
@@ -418,6 +606,9 @@ public final class GameDirectoriesTest {
|
||||
/// The reflected SettingsManager launcher settings field.
|
||||
private final Field launcherSettingsField;
|
||||
|
||||
/// The reflected SettingsManager game settings presets field.
|
||||
private final Field gameSettingsPresetsField;
|
||||
|
||||
/// The reflected SettingsManager local game directories access field.
|
||||
private final Field localGameDirectoriesAccessField;
|
||||
|
||||
@@ -448,6 +639,9 @@ public final class GameDirectoriesTest {
|
||||
/// The previous launcher settings instance.
|
||||
private final Object previousLauncherSettings;
|
||||
|
||||
/// The previous game settings presets instance.
|
||||
private final Object previousGameSettingsPresets;
|
||||
|
||||
/// The previous local game directories access.
|
||||
private final SettingFileAccess previousLocalGameDirectoriesAccess;
|
||||
|
||||
@@ -469,12 +663,22 @@ public final class GameDirectoriesTest {
|
||||
/// The previous repository map entries.
|
||||
private final Map<GameDirectory, HMCLGameRepository> previousRepositories;
|
||||
|
||||
/// Replaces game-directory-related static state with the given stores.
|
||||
/// Replaces game-directory-related static state with the given stores and an empty preset store.
|
||||
private GameDirectoryEnvironment(GameDirectories localDirectories, GameDirectories userDirectories)
|
||||
throws ReflectiveOperationException {
|
||||
this(localDirectories, userDirectories, new GameSettingsPresets());
|
||||
}
|
||||
|
||||
/// Replaces game-directory-related static state with the given stores.
|
||||
private GameDirectoryEnvironment(
|
||||
GameDirectories localDirectories,
|
||||
GameDirectories userDirectories,
|
||||
GameSettingsPresets gameSettingsPresets)
|
||||
throws ReflectiveOperationException {
|
||||
localGameDirectoriesField = SettingsManager.class.getDeclaredField("localGameDirectories");
|
||||
userGameDirectoriesField = SettingsManager.class.getDeclaredField("userGameDirectories");
|
||||
launcherSettingsField = SettingsManager.class.getDeclaredField("launcherSettings");
|
||||
gameSettingsPresetsField = SettingsManager.class.getDeclaredField("gameSettingsPresets");
|
||||
localGameDirectoriesAccessField = SettingsManager.class.getDeclaredField("localGameDirectoriesAccess");
|
||||
userGameDirectoriesAccessField = SettingsManager.class.getDeclaredField("userGameDirectoriesAccess");
|
||||
initializedField = GameDirectoryManager.class.getDeclaredField("initialized");
|
||||
@@ -485,6 +689,7 @@ public final class GameDirectoriesTest {
|
||||
localGameDirectoriesField.setAccessible(true);
|
||||
userGameDirectoriesField.setAccessible(true);
|
||||
launcherSettingsField.setAccessible(true);
|
||||
gameSettingsPresetsField.setAccessible(true);
|
||||
localGameDirectoriesAccessField.setAccessible(true);
|
||||
userGameDirectoriesAccessField.setAccessible(true);
|
||||
initializedField.setAccessible(true);
|
||||
@@ -496,6 +701,7 @@ public final class GameDirectoriesTest {
|
||||
previousLocalGameDirectories = localGameDirectoriesField.get(null);
|
||||
previousUserGameDirectories = userGameDirectoriesField.get(null);
|
||||
previousLauncherSettings = launcherSettingsField.get(null);
|
||||
previousGameSettingsPresets = gameSettingsPresetsField.get(null);
|
||||
previousLocalGameDirectoriesAccess = (SettingFileAccess) localGameDirectoriesAccessField.get(null);
|
||||
previousUserGameDirectoriesAccess = (SettingFileAccess) userGameDirectoriesAccessField.get(null);
|
||||
previousInitialized = initializedField.getBoolean(null);
|
||||
@@ -527,6 +733,7 @@ public final class GameDirectoriesTest {
|
||||
localGameDirectoriesField.set(null, localDirectories);
|
||||
userGameDirectoriesField.set(null, userDirectories);
|
||||
launcherSettingsField.set(null, new LauncherSettings());
|
||||
gameSettingsPresetsField.set(null, gameSettingsPresets);
|
||||
localGameDirectoriesAccessField.set(null, SettingFileAccess.READ_WRITE);
|
||||
userGameDirectoriesAccessField.set(null, SettingFileAccess.READ_WRITE);
|
||||
initializedField.setBoolean(null, false);
|
||||
@@ -558,12 +765,24 @@ public final class GameDirectoriesTest {
|
||||
localGameDirectoriesField.set(null, previousLocalGameDirectories);
|
||||
userGameDirectoriesField.set(null, previousUserGameDirectories);
|
||||
launcherSettingsField.set(null, previousLauncherSettings);
|
||||
gameSettingsPresetsField.set(null, previousGameSettingsPresets);
|
||||
localGameDirectoriesAccessField.set(null, previousLocalGameDirectoriesAccess);
|
||||
userGameDirectoriesAccessField.set(null, previousUserGameDirectoriesAccess);
|
||||
initializedField.setBoolean(null, previousInitialized);
|
||||
}
|
||||
}
|
||||
|
||||
/// Writes a minimal valid version json for repository refresh tests.
|
||||
private static void writeVersionJson(HMCLGameRepository repository, String id) throws IOException {
|
||||
Path versionRoot = repository.getVersionRoot(id);
|
||||
Files.createDirectories(versionRoot);
|
||||
Files.writeString(versionRoot.resolve(id + ".json"), """
|
||||
{
|
||||
"id": "%s"
|
||||
}
|
||||
""".formatted(id));
|
||||
}
|
||||
|
||||
/// Returns the only default game directory in the given store, asserting its path.
|
||||
private static GameDirectory assertSingleDefaultGameDirectory(GameDirectories gameDirectories, PortablePath path) {
|
||||
assertEquals(1, gameDirectories.getGameDirectories().size());
|
||||
|
||||
@@ -87,7 +87,7 @@ public final class ThemePackManagerTest {
|
||||
launcherSettingsField.set(null, launcherSettings);
|
||||
|
||||
ThemePackManager.ExportedThemePack exported =
|
||||
ThemePackManager.createCurrent("example.export", "Example", "Tester");
|
||||
ThemePackManager.createCurrent("example.export", "1.0", "Example", "Tester");
|
||||
Theme theme = exported.manifest().themes().get(0);
|
||||
ThemeBackgroundSettings background = Objects.requireNonNull(theme.appearance().background());
|
||||
ThemeBackground.Image image = assertInstanceOf(ThemeBackground.Image.class, background.source());
|
||||
@@ -146,7 +146,7 @@ public final class ThemePackManagerTest {
|
||||
launcherSettingsField.set(null, launcherSettings);
|
||||
|
||||
ThemePackManager.ExportedThemePack exported =
|
||||
ThemePackManager.createCurrent("example.export", "Example", "Tester");
|
||||
ThemePackManager.createCurrent("example.export", "1.0", "Example", "Tester");
|
||||
try {
|
||||
Theme theme = exported.manifest().themes().get(0);
|
||||
ThemeBackgroundSettings background = Objects.requireNonNull(theme.appearance().background());
|
||||
|
||||
@@ -30,6 +30,7 @@ import org.jackhuang.hmcl.addon.mod.LocalModFile;
|
||||
import org.jackhuang.hmcl.addon.mod.ModLoaderType;
|
||||
import org.jackhuang.hmcl.addon.mod.ModManager;
|
||||
import org.jackhuang.hmcl.util.Immutable;
|
||||
import org.jackhuang.hmcl.util.StringUtils;
|
||||
import org.jackhuang.hmcl.util.gson.JsonSerializable;
|
||||
import org.jackhuang.hmcl.util.gson.JsonUtils;
|
||||
import org.jackhuang.hmcl.util.gson.Validation;
|
||||
@@ -103,12 +104,13 @@ public final class ForgeNewModMetadata {
|
||||
@JsonAdapter(AuthorDeserializer.class)
|
||||
private final String authors;
|
||||
private final String description;
|
||||
private final String logoFile;
|
||||
|
||||
public Mod() {
|
||||
this("", "", "", "", "", "", "");
|
||||
this("", "", "", "", "", "", "", "");
|
||||
}
|
||||
|
||||
public Mod(String modId, String version, String displayName, String side, String displayURL, String authors, String description) {
|
||||
public Mod(String modId, String version, String displayName, String side, String displayURL, String authors, String description, String logoFile) {
|
||||
this.modId = modId;
|
||||
this.version = version;
|
||||
this.displayName = displayName;
|
||||
@@ -116,6 +118,7 @@ public final class ForgeNewModMetadata {
|
||||
this.displayURL = displayURL;
|
||||
this.authors = authors;
|
||||
this.description = description;
|
||||
this.logoFile = logoFile;
|
||||
}
|
||||
|
||||
public String getModId() {
|
||||
@@ -146,6 +149,10 @@ public final class ForgeNewModMetadata {
|
||||
return description;
|
||||
}
|
||||
|
||||
public String getLogoFile() {
|
||||
return logoFile;
|
||||
}
|
||||
|
||||
static final class AuthorDeserializer implements JsonDeserializer<String> {
|
||||
@Override
|
||||
public String deserialize(JsonElement authors, Type type, JsonDeserializationContext context) throws JsonParseException {
|
||||
@@ -235,10 +242,12 @@ public final class ForgeNewModMetadata {
|
||||
|
||||
ModLoaderType type = analyzeLoader(tomlParseResult, mod.getModId(), modLoaderType);
|
||||
|
||||
String logoPath = StringUtils.isNotBlank(mod.getLogoFile()) ? mod.getLogoFile() : metadata.getLogoFile();
|
||||
|
||||
return new LocalModFile(modManager, modManager.getLocalMod(mod.getModId(), type), modFile, mod.getDisplayName(), new LocalAddonFile.Description(mod.getDescription()),
|
||||
mod.getAuthors(), jarVersion == null ? mod.getVersion() : mod.getVersion().replace("${file.jarVersion}", jarVersion), "",
|
||||
mod.getDisplayURL(),
|
||||
metadata.getLogoFile());
|
||||
logoPath);
|
||||
}
|
||||
|
||||
private static LocalModFile fromEmbeddedMod(ModManager modManager, Path modFile, ZipFileTree tree, ModLoaderType modLoaderType) throws IOException {
|
||||
|
||||
@@ -21,7 +21,8 @@ import com.google.gson.JsonParseException;
|
||||
import org.jackhuang.hmcl.download.game.LibraryDownloadTask;
|
||||
import org.jackhuang.hmcl.game.Library;
|
||||
import org.jackhuang.hmcl.game.LibraryDownloadInfo;
|
||||
import org.jackhuang.hmcl.util.*;
|
||||
import org.jackhuang.hmcl.util.CacheRepository;
|
||||
import org.jackhuang.hmcl.util.DigestUtils;
|
||||
import org.jackhuang.hmcl.util.gson.JsonUtils;
|
||||
import org.jackhuang.hmcl.util.gson.TolerableValidationException;
|
||||
import org.jackhuang.hmcl.util.gson.Validation;
|
||||
@@ -37,7 +38,6 @@ import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.*;
|
||||
import java.util.concurrent.locks.Lock;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
|
||||
|
||||
@@ -90,7 +90,7 @@ public class DefaultCacheRepository extends CacheRepository {
|
||||
public void tryCacheLibrary(Library library, Path jar) {
|
||||
lock.readLock().lock();
|
||||
try {
|
||||
if (index.getLibraries().stream().anyMatch(it -> library.getName().equals(it.getName())))
|
||||
if (index.libraries().stream().anyMatch(it -> library.getName().equals(it.name())))
|
||||
return;
|
||||
} finally {
|
||||
lock.readLock().unlock();
|
||||
@@ -132,13 +132,13 @@ public class DefaultCacheRepository extends CacheRepository {
|
||||
|
||||
try {
|
||||
// check if this library is from Forge
|
||||
List<LibraryIndex> libraries = index.getLibraries().stream()
|
||||
.filter(it -> it.getName().equals(library.getName()))
|
||||
.collect(Collectors.toList());
|
||||
List<LibraryIndex> libraries = index.libraries().stream()
|
||||
.filter(it -> it.name().equals(library.getName()))
|
||||
.toList();
|
||||
for (LibraryIndex libIndex : libraries) {
|
||||
if (fileExists(SHA1, libIndex.getHash())) {
|
||||
Path file = getFile(SHA1, libIndex.getHash());
|
||||
if (libIndex.getType().equalsIgnoreCase(LibraryIndex.TYPE_FORGE)) {
|
||||
if (fileExists(SHA1, libIndex.hash())) {
|
||||
Path file = getFile(SHA1, libIndex.hash());
|
||||
if (libIndex.type().equalsIgnoreCase(LibraryIndex.TYPE_FORGE)) {
|
||||
if (LibraryDownloadTask.checksumValid(file, library.getChecksums()))
|
||||
return Optional.of(file);
|
||||
}
|
||||
@@ -191,7 +191,7 @@ public class DefaultCacheRepository extends CacheRepository {
|
||||
writeLock.lock();
|
||||
try {
|
||||
LibraryIndex libIndex = new LibraryIndex(library.getName(), hash, forge ? LibraryIndex.TYPE_FORGE : LibraryIndex.TYPE_JAR);
|
||||
index.getLibraries().add(libIndex);
|
||||
index.libraries().add(libIndex);
|
||||
saveIndex();
|
||||
} finally {
|
||||
writeLock.unlock();
|
||||
@@ -225,24 +225,17 @@ public class DefaultCacheRepository extends CacheRepository {
|
||||
/// ]
|
||||
/// }
|
||||
/// }
|
||||
///```
|
||||
/// ```
|
||||
/// assets and versions will not be included in index.
|
||||
private static final class Index implements Validation {
|
||||
private final Set<LibraryIndex> libraries;
|
||||
|
||||
private record Index(@NotNull Set<LibraryIndex> libraries) implements Validation {
|
||||
public Index() {
|
||||
this(new HashSet<>());
|
||||
}
|
||||
|
||||
public Index(Set<LibraryIndex> libraries) {
|
||||
private Index(Set<LibraryIndex> libraries) {
|
||||
this.libraries = Objects.requireNonNull(libraries);
|
||||
}
|
||||
|
||||
@NotNull
|
||||
public Set<LibraryIndex> getLibraries() {
|
||||
return libraries;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void validate() throws JsonParseException, TolerableValidationException {
|
||||
if (libraries == null)
|
||||
@@ -250,57 +243,17 @@ public class DefaultCacheRepository extends CacheRepository {
|
||||
}
|
||||
}
|
||||
|
||||
private static final class LibraryIndex implements Validation {
|
||||
private final String name;
|
||||
private final String hash;
|
||||
private final String type;
|
||||
|
||||
private record LibraryIndex(@NotNull String name, @NotNull String hash, @NotNull String type) implements Validation {
|
||||
public LibraryIndex() {
|
||||
this("", "", "");
|
||||
}
|
||||
|
||||
public LibraryIndex(String name, String hash, String type) {
|
||||
this.name = name;
|
||||
this.hash = hash;
|
||||
this.type = type;
|
||||
}
|
||||
|
||||
@NotNull
|
||||
public String getName() {
|
||||
return name;
|
||||
}
|
||||
|
||||
@NotNull
|
||||
public String getHash() {
|
||||
return hash;
|
||||
}
|
||||
|
||||
@NotNull
|
||||
public String getType() {
|
||||
return type;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void validate() throws JsonParseException, TolerableValidationException {
|
||||
if (name == null || hash == null || type == null)
|
||||
throw new JsonParseException("Index.LibraryIndex.* cannot be null");
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if (this == o) return true;
|
||||
if (o == null || getClass() != o.getClass()) return false;
|
||||
LibraryIndex that = (LibraryIndex) o;
|
||||
return Objects.equals(name, that.name) &&
|
||||
Objects.equals(hash, that.hash) &&
|
||||
Objects.equals(type, that.type);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return Objects.hash(name, hash, type);
|
||||
}
|
||||
|
||||
public static final String TYPE_FORGE = "forge";
|
||||
public static final String TYPE_JAR = "jar";
|
||||
}
|
||||
|
||||
@@ -272,7 +272,7 @@ public final class LibraryAnalyzer implements Iterable<LibraryAnalyzer.LibraryMa
|
||||
if (!optArgument.isPresent()) {
|
||||
return null;
|
||||
}
|
||||
List<Argument> gameArguments = optArgument.get().getGame();
|
||||
List<Argument> gameArguments = optArgument.get().game();
|
||||
if (gameArguments == null) {
|
||||
return null;
|
||||
}
|
||||
@@ -280,11 +280,11 @@ public final class LibraryAnalyzer implements Iterable<LibraryAnalyzer.LibraryMa
|
||||
for (int i = 0; i < gameArguments.size() - 1; i++) {
|
||||
Argument argument = gameArguments.get(i);
|
||||
if (argument instanceof StringArgument) {
|
||||
String argumentValue = ((StringArgument) argument).getArgument();
|
||||
String argumentValue = ((StringArgument) argument).argument();
|
||||
if ("--fml.neoForgeVersion".equals(argumentValue) || "--fml.forgeVersion".equals(argumentValue)) {
|
||||
Argument next = gameArguments.get(i + 1);
|
||||
if (next instanceof StringArgument) {
|
||||
return ((StringArgument) next).getArgument();
|
||||
return ((StringArgument) next).argument();
|
||||
}
|
||||
return null; // Normally, there should not be two --fml.neoForgeVersion argument.
|
||||
}
|
||||
|
||||
+2
-19
@@ -240,24 +240,7 @@ public final class DiscoJavaRemoteVersion implements JavaRemoteVersion {
|
||||
return "DiscoJavaRemoteVersion " + JsonUtils.GSON.toJson(this);
|
||||
}
|
||||
|
||||
public static final class Links {
|
||||
@SerializedName("pkg_info_uri")
|
||||
private final String pkgInfoUri;
|
||||
|
||||
@SerializedName("pkg_download_redirect")
|
||||
private final String pkgDownloadRedirect;
|
||||
|
||||
public Links(String pkgInfoUri, String pkgDownloadRedirect) {
|
||||
this.pkgInfoUri = pkgInfoUri;
|
||||
this.pkgDownloadRedirect = pkgDownloadRedirect;
|
||||
}
|
||||
|
||||
public String getPkgInfoUri() {
|
||||
return pkgInfoUri;
|
||||
}
|
||||
|
||||
public String getPkgDownloadRedirect() {
|
||||
return pkgDownloadRedirect;
|
||||
}
|
||||
public record Links(@SerializedName("pkg_info_uri") String pkgInfoUri,
|
||||
@SerializedName("pkg_download_redirect") String pkgDownloadRedirect) {
|
||||
}
|
||||
}
|
||||
+5
-43
@@ -22,47 +22,9 @@ import com.google.gson.annotations.SerializedName;
|
||||
/**
|
||||
* @author Glavo
|
||||
*/
|
||||
public final class DiscoRemoteFileInfo {
|
||||
@SerializedName("filename")
|
||||
private final String fileName;
|
||||
|
||||
@SerializedName("direct_download_uri")
|
||||
private final String directDownloadUri;
|
||||
|
||||
@SerializedName("checksum_type")
|
||||
private final String checksumType;
|
||||
|
||||
@SerializedName("checksum")
|
||||
private final String checksum;
|
||||
|
||||
@SerializedName("checksum_uri")
|
||||
private final String checksumUri;
|
||||
|
||||
public DiscoRemoteFileInfo(String fileName, String directDownloadUri, String checksumType, String checksum, String checksumUri) {
|
||||
this.fileName = fileName;
|
||||
this.directDownloadUri = directDownloadUri;
|
||||
this.checksumType = checksumType;
|
||||
this.checksum = checksum;
|
||||
this.checksumUri = checksumUri;
|
||||
}
|
||||
|
||||
public String getFileName() {
|
||||
return fileName;
|
||||
}
|
||||
|
||||
public String getDirectDownloadUri() {
|
||||
return directDownloadUri;
|
||||
}
|
||||
|
||||
public String getChecksumType() {
|
||||
return checksumType;
|
||||
}
|
||||
|
||||
public String getChecksum() {
|
||||
return checksum;
|
||||
}
|
||||
|
||||
public String getChecksumUri() {
|
||||
return checksumUri;
|
||||
}
|
||||
public record DiscoRemoteFileInfo(@SerializedName("filename") String fileName,
|
||||
@SerializedName("direct_download_uri") String directDownloadUri,
|
||||
@SerializedName("checksum_type") String checksumType,
|
||||
@SerializedName("checksum") String checksum,
|
||||
@SerializedName("checksum_uri") String checksumUri) {
|
||||
}
|
||||
+1
-1
@@ -85,7 +85,7 @@ public final class MojangJavaDownloadTask extends Task<MojangJavaDownloadTask.Re
|
||||
|
||||
@Override
|
||||
public void execute() throws Exception {
|
||||
for (Map.Entry<String, MojangJavaRemoteFiles.Remote> entry : javaDownloadsTask.getResult().getFiles().entrySet()) {
|
||||
for (Map.Entry<String, MojangJavaRemoteFiles.Remote> entry : javaDownloadsTask.getResult().files().entrySet()) {
|
||||
Path dest = tempDir.resolve(entry.getKey());
|
||||
if (entry.getValue() instanceof MojangJavaRemoteFiles.RemoteFile file) {
|
||||
// Use local file if it already exists
|
||||
|
||||
+1
-10
@@ -24,16 +24,7 @@ import org.jackhuang.hmcl.util.gson.JsonType;
|
||||
import java.util.Collections;
|
||||
import java.util.Map;
|
||||
|
||||
public final class MojangJavaRemoteFiles {
|
||||
private final Map<String, Remote> files;
|
||||
|
||||
public MojangJavaRemoteFiles(Map<String, Remote> files) {
|
||||
this.files = files;
|
||||
}
|
||||
|
||||
public Map<String, Remote> getFiles() {
|
||||
return files;
|
||||
}
|
||||
public record MojangJavaRemoteFiles(Map<String, Remote> files) {
|
||||
|
||||
@JsonType(
|
||||
property = "type",
|
||||
|
||||
+3
-11
@@ -19,20 +19,12 @@ package org.jackhuang.hmcl.download.java.mojang;
|
||||
|
||||
import org.jackhuang.hmcl.download.java.JavaRemoteVersion;
|
||||
import org.jackhuang.hmcl.game.GameJavaVersion;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
/**
|
||||
* @author Glavo
|
||||
*/
|
||||
public final class MojangJavaRemoteVersion implements JavaRemoteVersion {
|
||||
private final GameJavaVersion gameJavaVersion;
|
||||
|
||||
public MojangJavaRemoteVersion(GameJavaVersion gameJavaVersion) {
|
||||
this.gameJavaVersion = gameJavaVersion;
|
||||
}
|
||||
|
||||
public GameJavaVersion getGameJavaVersion() {
|
||||
return gameJavaVersion;
|
||||
}
|
||||
public record MojangJavaRemoteVersion(GameJavaVersion gameJavaVersion) implements JavaRemoteVersion {
|
||||
|
||||
@Override
|
||||
public int getJdkVersion() {
|
||||
@@ -50,7 +42,7 @@ public final class MojangJavaRemoteVersion implements JavaRemoteVersion {
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
public @NotNull String toString() {
|
||||
return "MojangJavaRemoteVersion[gameJavaVersion=" + gameJavaVersion + "]";
|
||||
}
|
||||
}
|
||||
@@ -31,24 +31,15 @@ import java.util.stream.Collectors;
|
||||
* @author huangyuhui
|
||||
*/
|
||||
@Immutable
|
||||
public final class Arguments {
|
||||
|
||||
@SerializedName("game")
|
||||
private final List<Argument> game;
|
||||
@SerializedName("jvm")
|
||||
private final List<Argument> jvm;
|
||||
public record Arguments(@SerializedName("game") @Nullable List<Argument> game, @SerializedName("jvm") @Nullable List<Argument> jvm) {
|
||||
|
||||
public Arguments() {
|
||||
this(null, null);
|
||||
}
|
||||
|
||||
public Arguments(List<Argument> game, List<Argument> jvm) {
|
||||
this.game = game;
|
||||
this.jvm = jvm;
|
||||
}
|
||||
|
||||
@Override
|
||||
@Nullable
|
||||
public List<Argument> getGame() {
|
||||
public List<Argument> game() {
|
||||
return game == null ? null : Collections.unmodifiableList(game);
|
||||
}
|
||||
|
||||
@@ -56,8 +47,9 @@ public final class Arguments {
|
||||
return new Arguments(game, jvm);
|
||||
}
|
||||
|
||||
@Override
|
||||
@Nullable
|
||||
public List<Argument> getJvm() {
|
||||
public List<Argument> jvm() {
|
||||
return jvm == null ? null : Collections.unmodifiableList(jvm);
|
||||
}
|
||||
|
||||
@@ -70,8 +62,8 @@ public final class Arguments {
|
||||
}
|
||||
|
||||
public Arguments addGameArguments(List<String> gameArguments) {
|
||||
List<Argument> list = gameArguments.stream().map(StringArgument::new).collect(Collectors.toList());
|
||||
return new Arguments(Lang.merge(getGame(), list), getJvm());
|
||||
List<StringArgument> list = gameArguments.stream().map(StringArgument::new).toList();
|
||||
return new Arguments(Lang.merge(game(), list), jvm());
|
||||
}
|
||||
|
||||
public Arguments addJVMArguments(String... jvmArguments) {
|
||||
@@ -79,12 +71,12 @@ public final class Arguments {
|
||||
}
|
||||
|
||||
public Arguments addJVMArguments(List<String> jvmArguments) {
|
||||
return addJVMArgumentsDirect(jvmArguments.stream().map(StringArgument::new).collect(Collectors.toList()));
|
||||
return addJVMArgumentsDirect(jvmArguments.stream().map(StringArgument::new).toList());
|
||||
}
|
||||
|
||||
// TODO: How to distinguish addJVMArgumentsDirect from addJVMArguments? Naming is hard :)
|
||||
public Arguments addJVMArgumentsDirect(List<Argument> jvmArguments) {
|
||||
return new Arguments(getGame(), Lang.merge(getJvm(), jvmArguments));
|
||||
public Arguments addJVMArgumentsDirect(List<? extends Argument> jvmArguments) {
|
||||
return new Arguments(game(), Lang.merge(jvm(), jvmArguments));
|
||||
}
|
||||
|
||||
public static Arguments merge(Arguments a, Arguments b) {
|
||||
|
||||
@@ -165,48 +165,7 @@ public final class CrashReportAnalyzer {
|
||||
}
|
||||
}
|
||||
|
||||
public static class Result {
|
||||
private final Rule rule;
|
||||
private final String log;
|
||||
private final Matcher matcher;
|
||||
|
||||
public Result(Rule rule, String log, Matcher matcher) {
|
||||
this.rule = rule;
|
||||
this.log = log;
|
||||
this.matcher = matcher;
|
||||
}
|
||||
|
||||
public Rule getRule() {
|
||||
return rule;
|
||||
}
|
||||
|
||||
public String getLog() {
|
||||
return log;
|
||||
}
|
||||
|
||||
public Matcher getMatcher() {
|
||||
return matcher;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if (this == o) return true;
|
||||
if (o == null || getClass() != o.getClass()) return false;
|
||||
|
||||
Result result = (Result) o;
|
||||
|
||||
if (rule != result.rule) return false;
|
||||
if (!log.equals(result.log)) return false;
|
||||
return matcher.equals(result.matcher);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
int result = rule.hashCode();
|
||||
result = 31 * result + log.hashCode();
|
||||
result = 31 * result + matcher.hashCode();
|
||||
return result;
|
||||
}
|
||||
public record Result(Rule rule, String log, Matcher matcher) {
|
||||
}
|
||||
|
||||
public static Set<Result> analyze(String log) {
|
||||
|
||||
@@ -454,7 +454,7 @@ public class DefaultGameRepository implements GameRepository {
|
||||
|
||||
@Override
|
||||
public Path getLoggingObject(String version, String assetId, LoggingInfo loggingInfo) {
|
||||
return getAssetDirectory(version, assetId).resolve("log_configs").resolve(loggingInfo.getFile().getId());
|
||||
return getAssetDirectory(version, assetId).resolve("log_configs").resolve(loggingInfo.file().getId());
|
||||
}
|
||||
|
||||
protected Path reconstructAssets(String version, String assetId) throws IOException, JsonParseException {
|
||||
|
||||
@@ -17,17 +17,46 @@
|
||||
*/
|
||||
package org.jackhuang.hmcl.game;
|
||||
|
||||
import org.jackhuang.hmcl.util.versioning.GameVersionNumber;
|
||||
import org.jetbrains.annotations.NotNullByDefault;
|
||||
|
||||
import java.util.Locale;
|
||||
|
||||
/// The Graphics API.
|
||||
@NotNullByDefault
|
||||
public enum GraphicsAPI {
|
||||
DEFAULT,
|
||||
OPENGL,
|
||||
VULKAN;
|
||||
|
||||
private static final GameVersionNumber VERSION_26_2_SNAP1 = GameVersionNumber.asGameVersion("26.2-snapshot-1");
|
||||
private static final GameVersionNumber VERSION_26_2 = GameVersionNumber.asGameVersion("26.2");
|
||||
|
||||
public static GraphicsAPI getDefaultGraphicsAPI(GameVersionNumber gameVersionNumber) {
|
||||
// Before Minecraft 26.2-snapshot-1, all versions only supported OpenGL
|
||||
if (gameVersionNumber.compareTo(VERSION_26_2_SNAP1) < 0) {
|
||||
return OPENGL;
|
||||
}
|
||||
|
||||
// From Minecraft 26.2-snapshot-1 to 26.2-rc-2, defaults to Vulkan
|
||||
if (gameVersionNumber.compareTo(VERSION_26_2) < 0) {
|
||||
return VULKAN;
|
||||
}
|
||||
|
||||
// After the official release of Minecraft 26.2, the default graphics API switched back to OpenGL
|
||||
return OPENGL;
|
||||
}
|
||||
|
||||
private final String minecraftArg = name().toLowerCase(Locale.ROOT);
|
||||
|
||||
public String getMinecraftArg() {
|
||||
return minecraftArg;
|
||||
}
|
||||
|
||||
public boolean isSupported(GameVersionNumber version) {
|
||||
return switch (this) {
|
||||
case DEFAULT, OPENGL -> true;
|
||||
case VULKAN -> version.compareTo(VERSION_26_2_SNAP1) >= 0;
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -27,14 +27,8 @@ import org.jackhuang.hmcl.util.gson.Validation;
|
||||
*
|
||||
* @author huangyuhui
|
||||
*/
|
||||
public final class LoggingInfo implements Validation {
|
||||
|
||||
@SerializedName("file")
|
||||
private final IdDownloadInfo file;
|
||||
@SerializedName("argument")
|
||||
private final String argument;
|
||||
@SerializedName("type")
|
||||
private final String type;
|
||||
public record LoggingInfo(@SerializedName("file") IdDownloadInfo file, @SerializedName("argument") String argument,
|
||||
@SerializedName("type") String type) implements Validation {
|
||||
|
||||
public LoggingInfo() {
|
||||
this(new IdDownloadInfo());
|
||||
@@ -48,24 +42,6 @@ public final class LoggingInfo implements Validation {
|
||||
this(file, argument, "");
|
||||
}
|
||||
|
||||
public LoggingInfo(IdDownloadInfo file, String argument, String type) {
|
||||
this.file = file;
|
||||
this.argument = argument;
|
||||
this.type = type;
|
||||
}
|
||||
|
||||
public IdDownloadInfo getFile() {
|
||||
return file;
|
||||
}
|
||||
|
||||
public String getArgument() {
|
||||
return argument;
|
||||
}
|
||||
|
||||
public String getType() {
|
||||
return type;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void validate() throws JsonParseException, TolerableValidationException {
|
||||
file.validate();
|
||||
|
||||
@@ -33,25 +33,19 @@ import static org.jackhuang.hmcl.util.gson.JsonUtils.listTypeOf;
|
||||
*/
|
||||
@JsonAdapter(RuledArgument.Serializer.class)
|
||||
@Immutable
|
||||
public class RuledArgument implements Argument {
|
||||
|
||||
private final List<CompatibilityRule> rules;
|
||||
private final List<String> value;
|
||||
public record RuledArgument(List<CompatibilityRule> rules, List<String> value) implements Argument {
|
||||
|
||||
public RuledArgument() {
|
||||
this(null, null);
|
||||
}
|
||||
|
||||
public RuledArgument(List<CompatibilityRule> rules, List<String> args) {
|
||||
this.rules = rules;
|
||||
this.value = args;
|
||||
}
|
||||
|
||||
public List<CompatibilityRule> getRules() {
|
||||
@Override
|
||||
public List<CompatibilityRule> rules() {
|
||||
return Collections.unmodifiableList(rules);
|
||||
}
|
||||
|
||||
public List<String> getValue() {
|
||||
@Override
|
||||
public List<String> value() {
|
||||
return Collections.unmodifiableList(value);
|
||||
}
|
||||
|
||||
|
||||
@@ -24,6 +24,7 @@ import com.google.gson.JsonPrimitive;
|
||||
import com.google.gson.JsonSerializationContext;
|
||||
import com.google.gson.JsonSerializer;
|
||||
import com.google.gson.annotations.JsonAdapter;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.lang.reflect.Type;
|
||||
import java.util.Collections;
|
||||
@@ -38,17 +39,7 @@ import java.util.regex.Pattern;
|
||||
*/
|
||||
@JsonAdapter(StringArgument.Serializer.class)
|
||||
@Immutable
|
||||
public final class StringArgument implements Argument {
|
||||
|
||||
private final String argument;
|
||||
|
||||
public StringArgument(String argument) {
|
||||
this.argument = argument;
|
||||
}
|
||||
|
||||
public String getArgument() {
|
||||
return argument;
|
||||
}
|
||||
public record StringArgument(String argument) implements Argument {
|
||||
|
||||
@Override
|
||||
public List<String> toString(Map<String, String> keys, Map<String, Boolean> features) {
|
||||
@@ -63,14 +54,14 @@ public final class StringArgument implements Argument {
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
public @NotNull String toString() {
|
||||
return argument;
|
||||
}
|
||||
|
||||
public static final class Serializer implements JsonSerializer<StringArgument> {
|
||||
@Override
|
||||
public JsonElement serialize(StringArgument src, Type typeOfSrc, JsonSerializationContext context) {
|
||||
return new JsonPrimitive(src.getArgument());
|
||||
return new JsonPrimitive(src.argument());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -43,8 +43,8 @@ public final class VersionLibraryBuilder {
|
||||
this.version = version;
|
||||
this.libraries = new ArrayList<>(version.getLibraries());
|
||||
this.mcArgs = version.getMinecraftArguments().map(StringUtils::tokenize).map(ArrayList::new).orElse(null);
|
||||
this.game = version.getArguments().map(Arguments::getGame).map(ArrayList::new).orElseGet(ArrayList::new);
|
||||
this.jvm = new ArrayList<>(version.getArguments().map(Arguments::getJvm).orElse(Arguments.DEFAULT_JVM_ARGUMENTS));
|
||||
this.game = version.getArguments().map(Arguments::game).map(ArrayList::new).orElseGet(ArrayList::new);
|
||||
this.jvm = new ArrayList<>(version.getArguments().map(Arguments::jvm).orElse(Arguments.DEFAULT_JVM_ARGUMENTS));
|
||||
this.useMcArgs = mcArgs != null;
|
||||
}
|
||||
|
||||
|
||||
@@ -314,20 +314,20 @@ public class DefaultLauncher extends Launcher {
|
||||
configuration.put("${natives_directory}", nativeFolderPath);
|
||||
|
||||
Path javaNativeFolder = FileUtils.toAbsolute(nativeFolder);
|
||||
@Nullable List<Argument> jvmArguments = version.getArguments().map(Arguments::getJvm).orElse(null);
|
||||
@Nullable List<Argument> jvmArguments = version.getArguments().map(Arguments::jvm).orElse(null);
|
||||
|
||||
if (jvmArguments != null) {
|
||||
for (Argument jvmArgument : jvmArguments) {
|
||||
if (jvmArgument instanceof StringArgument stringArgument
|
||||
&& stringArgument.getArgument().startsWith("-Djava.library.path=")) {
|
||||
&& stringArgument.argument().startsWith("-Djava.library.path=")) {
|
||||
|
||||
// We conservatively handle parameters like "-Djava.library.path=${natives_directory}/java"
|
||||
// to avoid extracting native libraries to unexpected locations.
|
||||
|
||||
String prefix = "-Djava.library.path=${natives_directory}/";
|
||||
if (stringArgument.getArgument().startsWith(prefix)) {
|
||||
if (stringArgument.argument().startsWith(prefix)) {
|
||||
try {
|
||||
String subDir = stringArgument.getArgument().substring(prefix.length());
|
||||
String subDir = stringArgument.argument().substring(prefix.length());
|
||||
Path actualNativeFolder = FileUtils.toAbsolute(javaNativeFolder.resolve(subDir));
|
||||
|
||||
if (actualNativeFolder.startsWith(javaNativeFolder)) {
|
||||
@@ -344,8 +344,8 @@ public class DefaultLauncher extends Launcher {
|
||||
|
||||
res.addAll(Arguments.parseArguments(Objects.requireNonNullElseGet(jvmArguments, this::getDefaultJVMArguments), configuration));
|
||||
Arguments argumentsFromAuthInfo = authInfo.getLaunchArguments(options);
|
||||
if (argumentsFromAuthInfo != null && argumentsFromAuthInfo.getJvm() != null && !argumentsFromAuthInfo.getJvm().isEmpty())
|
||||
res.addAll(Arguments.parseArguments(argumentsFromAuthInfo.getJvm(), configuration));
|
||||
if (argumentsFromAuthInfo != null && argumentsFromAuthInfo.jvm() != null && !argumentsFromAuthInfo.jvm().isEmpty())
|
||||
res.addAll(Arguments.parseArguments(argumentsFromAuthInfo.jvm(), configuration));
|
||||
|
||||
for (String javaAgent : options.getJavaAgents()) {
|
||||
res.add("-javaagent:" + javaAgent);
|
||||
@@ -360,12 +360,12 @@ public class DefaultLauncher extends Launcher {
|
||||
res.addAll(Arguments.parseStringArguments(version.getMinecraftArguments().map(StringUtils::tokenize).orElseGet(ArrayList::new), configuration));
|
||||
|
||||
Map<String, Boolean> features = getFeatures();
|
||||
version.getArguments().map(Arguments::getGame).ifPresent(arguments -> res.addAll(Arguments.parseArguments(arguments, configuration, features)));
|
||||
version.getArguments().map(Arguments::game).ifPresent(arguments -> res.addAll(Arguments.parseArguments(arguments, configuration, features)));
|
||||
if (version.getMinecraftArguments().isPresent()) {
|
||||
res.addAll(Arguments.parseArguments(this.getDefaultGameArguments(), configuration, features));
|
||||
}
|
||||
if (argumentsFromAuthInfo != null && argumentsFromAuthInfo.getGame() != null && !argumentsFromAuthInfo.getGame().isEmpty())
|
||||
res.addAll(Arguments.parseArguments(argumentsFromAuthInfo.getGame(), configuration, features));
|
||||
if (argumentsFromAuthInfo != null && argumentsFromAuthInfo.game() != null && !argumentsFromAuthInfo.game().isEmpty())
|
||||
res.addAll(Arguments.parseArguments(argumentsFromAuthInfo.game(), configuration, features));
|
||||
|
||||
if (options.getQuickPlayOption() instanceof QuickPlayOption.MultiPlayer multiPlayer) {
|
||||
String address = multiPlayer.serverIP();
|
||||
@@ -374,12 +374,12 @@ public class DefaultLauncher extends Launcher {
|
||||
ServerAddress parsed = ServerAddress.parse(address);
|
||||
if (World.supportQuickPlay(GameVersionNumber.asGameVersion(gameVersion))) {
|
||||
res.add("--quickPlayMultiplayer");
|
||||
res.add(parsed.getPort() >= 0 ? address : parsed.getHost() + ":25565");
|
||||
res.add(parsed.port() >= 0 ? address : parsed.host() + ":25565");
|
||||
} else {
|
||||
res.add("--server");
|
||||
res.add(parsed.getHost());
|
||||
res.add(parsed.host());
|
||||
res.add("--port");
|
||||
res.add(parsed.getPort() >= 0 ? String.valueOf(parsed.getPort()) : "25565");
|
||||
res.add(parsed.port() >= 0 ? String.valueOf(parsed.port()) : "25565");
|
||||
}
|
||||
} catch (IllegalArgumentException e) {
|
||||
LOG.warning("Invalid server address: " + address, e);
|
||||
|
||||
@@ -27,7 +27,10 @@ import org.jackhuang.hmcl.util.io.UrlResponseInfo;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import java.io.*;
|
||||
import java.io.BufferedReader;
|
||||
import java.io.BufferedWriter;
|
||||
import java.io.FileNotFoundException;
|
||||
import java.io.IOException;
|
||||
import java.net.URI;
|
||||
import java.net.http.HttpRequest;
|
||||
import java.nio.channels.Channels;
|
||||
@@ -48,7 +51,7 @@ import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
import static java.nio.charset.StandardCharsets.UTF_8;
|
||||
import static org.jackhuang.hmcl.util.gson.JsonUtils.*;
|
||||
import static org.jackhuang.hmcl.util.gson.JsonUtils.GSON;
|
||||
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
|
||||
|
||||
public class CacheRepository {
|
||||
@@ -383,27 +386,18 @@ public class CacheRepository {
|
||||
}
|
||||
}
|
||||
|
||||
private static final class ETagIndex {
|
||||
private final Collection<ETagItem> eTag;
|
||||
|
||||
private record ETagIndex(Collection<ETagItem> eTag) {
|
||||
public ETagIndex() {
|
||||
this.eTag = new HashSet<>();
|
||||
this(new HashSet<>());
|
||||
}
|
||||
|
||||
public ETagIndex(Collection<ETagItem> eTags) {
|
||||
this.eTag = new HashSet<>(eTags);
|
||||
private ETagIndex(Collection<ETagItem> eTag) {
|
||||
this.eTag = new HashSet<>(eTag);
|
||||
}
|
||||
}
|
||||
|
||||
private static final class ETagItem {
|
||||
private final String url;
|
||||
private final String eTag;
|
||||
private final String hash;
|
||||
@SerializedName("local")
|
||||
private final long localLastModified;
|
||||
@SerializedName("remote")
|
||||
private final String remoteLastModified;
|
||||
private final long expires;
|
||||
private record ETagItem(String url, String eTag, String hash, @SerializedName("local") long localLastModified,
|
||||
@SerializedName("remote") String remoteLastModified, long expires) {
|
||||
|
||||
/**
|
||||
* For Gson.
|
||||
@@ -412,19 +406,6 @@ public class CacheRepository {
|
||||
this(null, null, null, 0, null, 0L);
|
||||
}
|
||||
|
||||
public ETagItem(String url, String eTag, String hash, long localLastModified, String remoteLastModified, long expires) {
|
||||
this.url = url;
|
||||
this.eTag = eTag;
|
||||
this.hash = hash;
|
||||
this.localLastModified = localLastModified;
|
||||
this.remoteLastModified = remoteLastModified;
|
||||
this.expires = expires;
|
||||
}
|
||||
|
||||
public long getExpires() {
|
||||
return expires;
|
||||
}
|
||||
|
||||
public int compareTo(ETagItem other) {
|
||||
if (!url.equals(other.url) && !NetworkUtils.toURI(url).equals(NetworkUtils.toURI(other.url)))
|
||||
throw new IllegalArgumentException();
|
||||
@@ -438,24 +419,7 @@ public class CacheRepository {
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if (this == o) return true;
|
||||
return o instanceof ETagItem that
|
||||
&& localLastModified == that.localLastModified
|
||||
&& Objects.equals(url, that.url)
|
||||
&& Objects.equals(eTag, that.eTag)
|
||||
&& Objects.equals(hash, that.hash)
|
||||
&& Objects.equals(remoteLastModified, that.remoteLastModified)
|
||||
&& this.expires == that.expires;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return Objects.hash(url, eTag, hash, localLastModified, remoteLastModified, expires);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
public @NotNull String toString() {
|
||||
return "ETagItem[" +
|
||||
"url='" + url + '\'' +
|
||||
", eTag='" + eTag + '\'' +
|
||||
|
||||
@@ -24,7 +24,7 @@ import java.util.Objects;
|
||||
/**
|
||||
* @author Glavo
|
||||
*/
|
||||
public final class ServerAddress {
|
||||
public record ServerAddress(String host, int port) {
|
||||
|
||||
private static final int UNKNOWN_PORT = -1;
|
||||
|
||||
@@ -86,9 +86,6 @@ public final class ServerAddress {
|
||||
}
|
||||
}
|
||||
|
||||
private final String host;
|
||||
private final int port;
|
||||
|
||||
public ServerAddress(@NotNull String host) {
|
||||
this(host, UNKNOWN_PORT);
|
||||
}
|
||||
@@ -98,28 +95,8 @@ public final class ServerAddress {
|
||||
this.port = port;
|
||||
}
|
||||
|
||||
public String getHost() {
|
||||
return host;
|
||||
}
|
||||
|
||||
public int getPort() {
|
||||
return port;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if (!(o instanceof ServerAddress)) return false;
|
||||
ServerAddress that = (ServerAddress) o;
|
||||
return port == that.port && Objects.equals(host, that.host);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return Objects.hash(host, port);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
public @NotNull String toString() {
|
||||
return String.format("ServerAddress[host='%s', port=%d]", host, port);
|
||||
}
|
||||
}
|
||||
@@ -260,6 +260,14 @@ public final class StringUtils {
|
||||
return false;
|
||||
}
|
||||
|
||||
public static boolean startsWithAny(String str, Collection<String> prefixes) {
|
||||
return prefixes.stream().anyMatch(str::startsWith);
|
||||
}
|
||||
|
||||
public static boolean endsWithAny(String str, Collection<String> suffixes) {
|
||||
return suffixes.stream().anyMatch(str::endsWith);
|
||||
}
|
||||
|
||||
public static Predicate<@Nullable String> compileQuery(String queryString) throws PatternSyntaxException {
|
||||
Predicate<@Nullable String> predicate;
|
||||
if (queryString.startsWith("regex:")) {
|
||||
|
||||
@@ -164,19 +164,43 @@ public final class NetworkUtils {
|
||||
}
|
||||
}
|
||||
|
||||
private static final Map<String, String> API_KEYS = Map.of(
|
||||
"api.curseforge.com", CurseForgeRemoteAddonRepository.API_KEY,
|
||||
"edge.forgecdn.net", CurseForgeRemoteAddonRepository.API_KEY
|
||||
);
|
||||
private static final List<Pair<String, String>> API_KEYS;
|
||||
|
||||
public static java.net.http.HttpRequest.Builder newRequestBuilder(URI uri) {
|
||||
var builder = java.net.http.HttpRequest.newBuilder(uri);
|
||||
var host = uri.getHost();
|
||||
if (host == null) return builder;
|
||||
var apiKey = API_KEYS.get(host.toLowerCase(Locale.ROOT));
|
||||
if (StringUtils.isNotBlank(apiKey))
|
||||
builder.header("x-api-key", apiKey);
|
||||
return builder;
|
||||
static {
|
||||
if (CurseForgeRemoteAddonRepository.API_KEY.isEmpty()) {
|
||||
API_KEYS = List.of();
|
||||
} else {
|
||||
API_KEYS = List.of(
|
||||
pair("api.curseforge.com", CurseForgeRemoteAddonRepository.API_KEY),
|
||||
pair("forgecdn.net", CurseForgeRemoteAddonRepository.API_KEY)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
private static boolean matchDomainSuffix(String domain, String suffix) {
|
||||
return domain.endsWith(suffix)
|
||||
&& (domain.length() == suffix.length() || domain.charAt(domain.length() - suffix.length() - 1) == '.');
|
||||
}
|
||||
|
||||
public static void injectApiKey(WebURL url, URLConnection connection) {
|
||||
if (!(connection instanceof HttpURLConnection))
|
||||
return;
|
||||
|
||||
if (connection.getRequestProperty("x-api-key") != null) {
|
||||
return;
|
||||
}
|
||||
|
||||
String host = url.getHost();
|
||||
if (host == null || host.isEmpty())
|
||||
return;
|
||||
|
||||
for (Pair<String, String> pair : API_KEYS) {
|
||||
String hostSuffix = pair.getKey();
|
||||
if (matchDomainSuffix(host, hostSuffix)) {
|
||||
connection.addRequestProperty("x-api-key", pair.getValue());
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static URLConnection createConnection(WebURL url) throws IOException {
|
||||
@@ -192,6 +216,7 @@ public final class NetworkUtils {
|
||||
httpConnection.setRequestProperty("Accept-Language", Locale.getDefault().toLanguageTag());
|
||||
httpConnection.setRequestProperty("User-Agent", USER_AGENT);
|
||||
httpConnection.setInstanceFollowRedirects(false);
|
||||
injectApiKey(url, connection);
|
||||
}
|
||||
return connection;
|
||||
}
|
||||
@@ -309,10 +334,10 @@ public final class NetworkUtils {
|
||||
throw new IOException("Too much redirects");
|
||||
}
|
||||
|
||||
HttpURLConnection redirected = (HttpURLConnection) new URL(conn.getURL(), encodeLocation(newURL))
|
||||
.openConnection();
|
||||
properties
|
||||
.forEach((key, value) -> value.forEach(element -> redirected.addRequestProperty(key, element)));
|
||||
WebURL redirectedUrl = WebURL.of(conn.getURL()).resolve(newURL);
|
||||
HttpURLConnection redirected = (HttpURLConnection) redirectedUrl.toURL().openConnection();
|
||||
properties.forEach((key, value) -> value.forEach(element -> redirected.addRequestProperty(key, element)));
|
||||
injectApiKey(redirectedUrl, redirected);
|
||||
redirected.setRequestMethod(method);
|
||||
conn = redirected;
|
||||
++redirect;
|
||||
|
||||
@@ -37,14 +37,7 @@ public final class PropertyUtils {
|
||||
private PropertyUtils() {
|
||||
}
|
||||
|
||||
public static final class PropertyHandle {
|
||||
public final WritableValue<Object> accessor;
|
||||
public final Observable observable;
|
||||
|
||||
public PropertyHandle(WritableValue<Object> accessor, Observable observable) {
|
||||
this.accessor = accessor;
|
||||
this.observable = observable;
|
||||
}
|
||||
public record PropertyHandle(WritableValue<Object> accessor, Observable observable) {
|
||||
}
|
||||
|
||||
@SuppressWarnings({ "rawtypes", "unchecked" })
|
||||
|
||||
@@ -24,7 +24,10 @@ import org.tukaani.xz.XZOutputStream;
|
||||
|
||||
import java.io.*;
|
||||
import java.lang.System.Logger.Level;
|
||||
import java.nio.file.*;
|
||||
import java.nio.file.DirectoryStream;
|
||||
import java.nio.file.FileAlreadyExistsException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.time.Instant;
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.ZoneId;
|
||||
@@ -67,7 +70,7 @@ public final class Logger {
|
||||
static final String PACKAGE_PREFIX = "org.jackhuang.hmcl.";
|
||||
static final String CLASS_NAME = Logger.class.getName();
|
||||
|
||||
private static final DateTimeFormatter TIME_FORMATTER = DateTimeFormatter.ofPattern("HH:mm:ss").withZone(ZoneId.systemDefault());
|
||||
public static final DateTimeFormatter TIME_FORMATTER = DateTimeFormatter.ofPattern("HH:mm:ss").withZone(ZoneId.systemDefault());
|
||||
|
||||
private final BlockingQueue<LogEvent> queue = new LinkedBlockingQueue<>();
|
||||
private final StringBuilder builder = new StringBuilder(512);
|
||||
|
||||
@@ -17,6 +17,8 @@
|
||||
*/
|
||||
package org.jackhuang.hmcl.util.platform;
|
||||
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.*;
|
||||
@@ -285,17 +287,10 @@ public final class CommandBuilder {
|
||||
return raw.stream().map(i -> i.arg).collect(Collectors.toCollection(ArrayList::new));
|
||||
}
|
||||
|
||||
private static class Item {
|
||||
final String arg;
|
||||
final boolean parse;
|
||||
|
||||
Item(String arg, boolean parse) {
|
||||
this.arg = arg;
|
||||
this.parse = parse;
|
||||
}
|
||||
private record Item(String arg, boolean parse) {
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
public @NotNull String toString() {
|
||||
return parse ? (OperatingSystem.WINDOWS == OperatingSystem.CURRENT_OS ? toBatchStringLiteral(arg) : toShellStringLiteral(arg)) : arg;
|
||||
}
|
||||
}
|
||||
|
||||
+5
-14
@@ -18,6 +18,7 @@
|
||||
package org.jackhuang.hmcl.util.platform.hardware;
|
||||
|
||||
import org.jackhuang.hmcl.util.StringUtils;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import java.util.regex.Matcher;
|
||||
@@ -111,23 +112,13 @@ public final class CentralProcessor {
|
||||
return builder.toString();
|
||||
}
|
||||
|
||||
public static final class Cores {
|
||||
public final int physical;
|
||||
public final int logical;
|
||||
public final int packages;
|
||||
|
||||
public record Cores(int physical, int logical, int packages) {
|
||||
public Cores(int logical) {
|
||||
this(logical, logical, 1);
|
||||
}
|
||||
|
||||
public Cores(int physical, int logical, int packages) {
|
||||
this.physical = physical;
|
||||
this.logical = logical;
|
||||
this.packages = packages;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
public @NotNull String toString() {
|
||||
return String.format("Cores[physical=%d, logical=%d, packages=%d]", physical, logical, packages);
|
||||
}
|
||||
}
|
||||
@@ -158,7 +149,7 @@ public final class CentralProcessor {
|
||||
return this;
|
||||
}
|
||||
|
||||
public HardwareVendor getVendor() {
|
||||
public @Nullable HardwareVendor getVendor() {
|
||||
return vendor;
|
||||
}
|
||||
|
||||
@@ -167,7 +158,7 @@ public final class CentralProcessor {
|
||||
return this;
|
||||
}
|
||||
|
||||
public Cores getCores() {
|
||||
public @Nullable Cores getCores() {
|
||||
return cores;
|
||||
}
|
||||
|
||||
|
||||
+37
-93
@@ -18,11 +18,12 @@
|
||||
package org.jackhuang.hmcl.util.platform.hardware;
|
||||
|
||||
import org.jetbrains.annotations.Contract;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import java.util.Locale;
|
||||
|
||||
public final class HardwareVendor {
|
||||
public record HardwareVendor(String name) {
|
||||
public static final HardwareVendor INTEL = new HardwareVendor("Intel");
|
||||
public static final HardwareVendor NVIDIA = new HardwareVendor("NVIDIA");
|
||||
public static final HardwareVendor AMD = new HardwareVendor("AMD");
|
||||
@@ -59,8 +60,8 @@ public final class HardwareVendor {
|
||||
if (lower.startsWith("intel") || lower.startsWith("genuineintel")) return INTEL;
|
||||
if (lower.startsWith("nvidia")) return NVIDIA;
|
||||
if (lower.startsWith("advanced micro devices")
|
||||
|| lower.startsWith("authenticamd")
|
||||
|| (lower.startsWith("amd") && !(lower.length() > 3 && Character.isAlphabetic(lower.charAt(3)))))
|
||||
|| lower.startsWith("authenticamd")
|
||||
|| (lower.startsWith("amd") && !(lower.length() > 3 && Character.isAlphabetic(lower.charAt(3)))))
|
||||
return AMD;
|
||||
if (lower.equals("brcm") || lower.startsWith("broadcom")) return BROADCOM;
|
||||
if (lower.startsWith("mediatek")) return MTK;
|
||||
@@ -96,103 +97,46 @@ public final class HardwareVendor {
|
||||
|
||||
public static @Nullable HardwareVendor ofPciVendorId(int vendorId) {
|
||||
// https://devicehunt.com/all-pci-vendors
|
||||
switch (vendorId) {
|
||||
case 0x106b:
|
||||
return APPLE;
|
||||
case 0x1002:
|
||||
case 0x1022:
|
||||
case 0x1dd8: // AMD Pensando Systems
|
||||
case 0x1924: // AMD Solarflare
|
||||
return AMD;
|
||||
case 0x8086:
|
||||
case 0x8087:
|
||||
case 0x03e7:
|
||||
return INTEL;
|
||||
case 0x0955:
|
||||
case 0x10de:
|
||||
case 0x12d2:
|
||||
return NVIDIA;
|
||||
case 0x1ed5:
|
||||
return MOORE_THREADS;
|
||||
case 0x168c:
|
||||
case 0x5143:
|
||||
return QUALCOMM;
|
||||
case 0x14c3:
|
||||
return MTK;
|
||||
case 0x15ad:
|
||||
return VMWARE;
|
||||
case 0x1ab8:
|
||||
return PARALLEL;
|
||||
case 0x1414:
|
||||
return MICROSOFT;
|
||||
case 0x182f:
|
||||
case 0x14e4:
|
||||
return BROADCOM;
|
||||
case 0x0014:
|
||||
return LOONGSON;
|
||||
case 0x0731:
|
||||
return JINGJIA_MICRO;
|
||||
case 0x19e5:
|
||||
return HUAWEI;
|
||||
case 0x1d17:
|
||||
return ZHAOXIN;
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
return switch (vendorId) {
|
||||
case 0x106b -> APPLE; // AMD Pensando Systems
|
||||
case 0x1002, 0x1022, 0x1dd8, 0x1924 -> AMD; // AMD Solarflare
|
||||
case 0x8086, 0x8087, 0x03e7 -> INTEL;
|
||||
case 0x0955, 0x10de, 0x12d2 -> NVIDIA;
|
||||
case 0x1ed5 -> MOORE_THREADS;
|
||||
case 0x168c, 0x5143 -> QUALCOMM;
|
||||
case 0x14c3 -> MTK;
|
||||
case 0x15ad -> VMWARE;
|
||||
case 0x1ab8 -> PARALLEL;
|
||||
case 0x1414 -> MICROSOFT;
|
||||
case 0x182f, 0x14e4 -> BROADCOM;
|
||||
case 0x0014 -> LOONGSON;
|
||||
case 0x0731 -> JINGJIA_MICRO;
|
||||
case 0x19e5 -> HUAWEI;
|
||||
case 0x1d17 -> ZHAOXIN;
|
||||
default -> null;
|
||||
};
|
||||
}
|
||||
|
||||
public static @Nullable HardwareVendor ofArmImplementerId(int implementerId) {
|
||||
// https://github.com/util-linux/util-linux/blob/0a21358af3e50fcb13a9bf3702779f11a4739667/sys-utils/lscpu-arm.c#L301
|
||||
switch (implementerId) {
|
||||
case 0x41:
|
||||
return ARM;
|
||||
case 0x42:
|
||||
return BROADCOM;
|
||||
case 0x48:
|
||||
return HUAWEI;
|
||||
case 0x4e:
|
||||
return NVIDIA;
|
||||
case 0x51:
|
||||
return QUALCOMM;
|
||||
case 0x53:
|
||||
return SAMSUNG;
|
||||
case 0x56:
|
||||
return MARVELL;
|
||||
case 0x61:
|
||||
return APPLE;
|
||||
case 0x69:
|
||||
return INTEL;
|
||||
case 0x6D:
|
||||
return MICROSOFT;
|
||||
case 0xc0:
|
||||
return AMPERE;
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private final String name;
|
||||
|
||||
public HardwareVendor(String name) {
|
||||
this.name = name;
|
||||
}
|
||||
|
||||
public String getName() {
|
||||
return name;
|
||||
return switch (implementerId) {
|
||||
case 0x41 -> ARM;
|
||||
case 0x42 -> BROADCOM;
|
||||
case 0x48 -> HUAWEI;
|
||||
case 0x4e -> NVIDIA;
|
||||
case 0x51 -> QUALCOMM;
|
||||
case 0x53 -> SAMSUNG;
|
||||
case 0x56 -> MARVELL;
|
||||
case 0x61 -> APPLE;
|
||||
case 0x69 -> INTEL;
|
||||
case 0x6D -> MICROSOFT;
|
||||
case 0xc0 -> AMPERE;
|
||||
default -> null;
|
||||
};
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
return o instanceof HardwareVendor && name.equals(((HardwareVendor) o).name);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return name.hashCode();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
public @NotNull String toString() {
|
||||
return name;
|
||||
}
|
||||
}
|
||||
+1
-16
@@ -17,27 +17,12 @@
|
||||
*/
|
||||
package org.jackhuang.hmcl.util.platform.hardware;
|
||||
|
||||
public final class PhysicalMemoryStatus {
|
||||
private final long total;
|
||||
private final long available;
|
||||
|
||||
public PhysicalMemoryStatus(long total, long available) {
|
||||
this.total = total;
|
||||
this.available = available;
|
||||
}
|
||||
|
||||
public long getTotal() {
|
||||
return total;
|
||||
}
|
||||
public record PhysicalMemoryStatus(long total, long available) {
|
||||
|
||||
public long getUsed() {
|
||||
return hasAvailable() ? total - available : 0;
|
||||
}
|
||||
|
||||
public long getAvailable() {
|
||||
return available;
|
||||
}
|
||||
|
||||
public boolean hasAvailable() {
|
||||
return available >= 0;
|
||||
}
|
||||
|
||||
@@ -17,6 +17,9 @@
|
||||
*/
|
||||
package org.jackhuang.hmcl.util.versioning;
|
||||
|
||||
import org.jackhuang.hmcl.util.StringUtils;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.math.BigInteger;
|
||||
import java.util.*;
|
||||
|
||||
@@ -109,15 +112,9 @@ public final class VersionNumber implements Comparable<VersionNumber> {
|
||||
void appendTo(StringBuilder buffer);
|
||||
}
|
||||
|
||||
private static final class LongItem implements Item {
|
||||
private final long value;
|
||||
|
||||
private record LongItem(long value) implements Item {
|
||||
public static final LongItem ZERO = new LongItem(0L);
|
||||
|
||||
LongItem(long value) {
|
||||
this.value = value;
|
||||
}
|
||||
|
||||
public int getType() {
|
||||
return LONG_ITEM;
|
||||
}
|
||||
@@ -131,22 +128,18 @@ public final class VersionNumber implements Comparable<VersionNumber> {
|
||||
return value == 0L ? 0 : 1; // 1.0 == 1, 1.1 > 1
|
||||
}
|
||||
|
||||
switch (item.getType()) {
|
||||
case LONG_ITEM:
|
||||
return switch (item.getType()) {
|
||||
case LONG_ITEM -> {
|
||||
long itemValue = ((LongItem) item).value;
|
||||
return Long.compare(value, itemValue);
|
||||
case BIGINTEGER_ITEM:
|
||||
return -1;
|
||||
yield Long.compare(value, itemValue);
|
||||
}
|
||||
case BIGINTEGER_ITEM -> -1;
|
||||
case STRING_ITEM -> 1; // 1.1 > 1-sp
|
||||
|
||||
case STRING_ITEM:
|
||||
return 1; // 1.1 > 1-sp
|
||||
case LIST_ITEM -> 1; // 1.1 > 1-1
|
||||
|
||||
case LIST_ITEM:
|
||||
return 1; // 1.1 > 1-1
|
||||
|
||||
default:
|
||||
throw new AssertionError("invalid item: " + item.getClass());
|
||||
}
|
||||
default -> throw new AssertionError("invalid item: " + item.getClass());
|
||||
};
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -154,7 +147,7 @@ public final class VersionNumber implements Comparable<VersionNumber> {
|
||||
buffer.append(value);
|
||||
}
|
||||
|
||||
public String toString() {
|
||||
public @NotNull String toString() {
|
||||
return Long.toString(value);
|
||||
}
|
||||
}
|
||||
@@ -162,11 +155,10 @@ public final class VersionNumber implements Comparable<VersionNumber> {
|
||||
/**
|
||||
* Represents a numeric item in the version item list.
|
||||
*/
|
||||
private static final class BigIntegerItem implements Item {
|
||||
private final BigInteger value;
|
||||
private record BigIntegerItem(BigInteger value) implements Item {
|
||||
|
||||
BigIntegerItem(String str) {
|
||||
this.value = new BigInteger(str);
|
||||
private BigIntegerItem(String value) {
|
||||
this(new BigInteger(value));
|
||||
}
|
||||
|
||||
public int getType() {
|
||||
@@ -185,21 +177,15 @@ public final class VersionNumber implements Comparable<VersionNumber> {
|
||||
return 1;
|
||||
}
|
||||
|
||||
switch (item.getType()) {
|
||||
case LONG_ITEM:
|
||||
return 1;
|
||||
case BIGINTEGER_ITEM:
|
||||
return value.compareTo(((BigIntegerItem) item).value);
|
||||
return switch (item.getType()) {
|
||||
case LONG_ITEM -> 1;
|
||||
case BIGINTEGER_ITEM -> value.compareTo(((BigIntegerItem) item).value);
|
||||
case STRING_ITEM -> 1; // 1.1 > 1-sp
|
||||
|
||||
case STRING_ITEM:
|
||||
return 1; // 1.1 > 1-sp
|
||||
case LIST_ITEM -> 1; // 1.1 > 1-1
|
||||
|
||||
case LIST_ITEM:
|
||||
return 1; // 1.1 > 1-1
|
||||
|
||||
default:
|
||||
throw new AssertionError("invalid item: " + item.getClass());
|
||||
}
|
||||
default -> throw new AssertionError("invalid item: " + item.getClass());
|
||||
};
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -207,7 +193,7 @@ public final class VersionNumber implements Comparable<VersionNumber> {
|
||||
buffer.append(value);
|
||||
}
|
||||
|
||||
public String toString() {
|
||||
public @NotNull String toString() {
|
||||
return value.toString();
|
||||
}
|
||||
}
|
||||
@@ -215,19 +201,9 @@ public final class VersionNumber implements Comparable<VersionNumber> {
|
||||
/**
|
||||
* Represents a string in the version item list, usually a qualifier.
|
||||
*/
|
||||
private static final class StringItem implements Item {
|
||||
private final String value;
|
||||
private final boolean pre;
|
||||
|
||||
StringItem(String value) {
|
||||
this.value = value;
|
||||
|
||||
String lower = value.trim().toLowerCase(Locale.ROOT);
|
||||
this.pre = lower.startsWith("alpha")
|
||||
|| lower.startsWith("beta")
|
||||
|| lower.startsWith("pre")
|
||||
|| lower.startsWith("rc")
|
||||
|| lower.startsWith("experimental");
|
||||
private record StringItem(String value, boolean pre) implements Item {
|
||||
private StringItem(String value) {
|
||||
this(value, StringUtils.startsWithAny(value.trim().toLowerCase(Locale.ROOT), Set.of("alpha", "beta", "pre", "rc", "experimental")));
|
||||
}
|
||||
|
||||
public int getType() {
|
||||
@@ -243,20 +219,14 @@ public final class VersionNumber implements Comparable<VersionNumber> {
|
||||
// 1-beta < 1 < 1-string
|
||||
return pre ? -1 : 1;
|
||||
}
|
||||
switch (item.getType()) {
|
||||
case LONG_ITEM:
|
||||
case BIGINTEGER_ITEM:
|
||||
return -1; // 1.any < 1.1 ?
|
||||
return switch (item.getType()) {
|
||||
case LONG_ITEM, BIGINTEGER_ITEM -> -1; // 1.any < 1.1 ?
|
||||
|
||||
case STRING_ITEM:
|
||||
return value.compareTo(((StringItem) item).value);
|
||||
case STRING_ITEM -> value.compareTo(((StringItem) item).value);
|
||||
case LIST_ITEM -> -1; // 1.any < 1-1
|
||||
|
||||
case LIST_ITEM:
|
||||
return -1; // 1.any < 1-1
|
||||
|
||||
default:
|
||||
throw new AssertionError("invalid item: " + item.getClass());
|
||||
}
|
||||
default -> throw new AssertionError("invalid item: " + item.getClass());
|
||||
};
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -264,7 +234,8 @@ public final class VersionNumber implements Comparable<VersionNumber> {
|
||||
buffer.append(value);
|
||||
}
|
||||
|
||||
public String toString() {
|
||||
@Override
|
||||
public @NotNull String toString() {
|
||||
return value;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -39,7 +39,7 @@ public class CrashReportAnalyzerTest {
|
||||
}
|
||||
|
||||
private CrashReportAnalyzer.Result findResultByRule(Set<CrashReportAnalyzer.Result> results, CrashReportAnalyzer.Rule rule) {
|
||||
CrashReportAnalyzer.Result r = results.stream().filter(result -> result.getRule() == rule).findFirst().orElse(null);
|
||||
CrashReportAnalyzer.Result r = results.stream().filter(result -> result.rule() == rule).findFirst().orElse(null);
|
||||
assertNotNull(r);
|
||||
return r;
|
||||
}
|
||||
@@ -88,7 +88,7 @@ public class CrashReportAnalyzerTest {
|
||||
" - Mod test depends on mod {fabricloader @ [>=0.11.3]}, which is missing!\n" +
|
||||
" - Mod test depends on mod {fabric @ [*]}, which is missing!\n" +
|
||||
" - Mod test depends on mod {java @ [>=16]}, which is missing!\n").replaceAll("\\s+", ""),
|
||||
result.getMatcher().group("reason").replaceAll("\\s+", ""));
|
||||
result.matcher().group("reason").replaceAll("\\s+", ""));
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -97,7 +97,7 @@ public class CrashReportAnalyzerTest {
|
||||
CrashReportAnalyzer.analyze(loadLog("/logs/forgemod_resolution.txt")),
|
||||
CrashReportAnalyzer.Rule.FORGEMOD_RESOLUTION);
|
||||
assertEquals(("\tMod ID: 'vampirism', Requested by: 'werewolves', Expected range: '[1.9.0-beta.1,)', Actual version: '[MISSING]'\n").replaceAll("\\s+", ""),
|
||||
result.getMatcher().group("reason").replaceAll("\\s+", ""));
|
||||
result.matcher().group("reason").replaceAll("\\s+", ""));
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -106,7 +106,7 @@ public class CrashReportAnalyzerTest {
|
||||
CrashReportAnalyzer.analyze(loadLog("/logs/forge_found_duplicate_mods.txt")),
|
||||
CrashReportAnalyzer.Rule.FORGE_FOUND_DUPLICATE_MODS);
|
||||
assertEquals(("\tMod ID: 'jei' from mod files: REIPluginCompatibilities-forge-12.0.93.jar, jei-1.20.1-forge-15.2.0.27.jar\n").replaceAll("\\s+", ""),
|
||||
result.getMatcher().group("reason").replaceAll("\\s+", ""));
|
||||
result.matcher().group("reason").replaceAll("\\s+", ""));
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -114,8 +114,8 @@ public class CrashReportAnalyzerTest {
|
||||
CrashReportAnalyzer.Result result = findResultByRule(
|
||||
CrashReportAnalyzer.analyze(loadLog("/logs/mod_resolution_collection.txt")),
|
||||
CrashReportAnalyzer.Rule.MOD_RESOLUTION_COLLECTION);
|
||||
assertEquals("tabtps-fabric", result.getMatcher().group("sourcemod"));
|
||||
assertEquals("{fabricloader @ [>=0.11.1]}", result.getMatcher().group("destmod"));
|
||||
assertEquals("tabtps-fabric", result.matcher().group("sourcemod"));
|
||||
assertEquals("{fabricloader @ [>=0.11.1]}", result.matcher().group("destmod"));
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -142,7 +142,7 @@ public class CrashReportAnalyzerTest {
|
||||
"\tat java.lang.reflect.Method.invoke(Method.java:498) ~[?:1.8.0_131]\n" +
|
||||
"\tat oolloo.jlw.Wrapper.invokeMain(Wrapper.java:58) [JavaWrapper.jar:?]\n" +
|
||||
"\tat oolloo.jlw.Wrapper.main(Wrapper.java:51) [JavaWrapper.jar:?]").replaceAll("\\s+", ""),
|
||||
result.getMatcher().group("reason").replaceAll("\\s+", ""));
|
||||
result.matcher().group("reason").replaceAll("\\s+", ""));
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -157,7 +157,7 @@ public class CrashReportAnalyzerTest {
|
||||
CrashReportAnalyzer.Result result = findResultByRule(
|
||||
CrashReportAnalyzer.analyze(loadLog("/logs/too_old_java.txt")),
|
||||
CrashReportAnalyzer.Rule.TOO_OLD_JAVA);
|
||||
assertEquals("60", result.getMatcher().group("expected"));
|
||||
assertEquals("60", result.matcher().group("expected"));
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -165,7 +165,7 @@ public class CrashReportAnalyzerTest {
|
||||
CrashReportAnalyzer.Result result = findResultByRule(
|
||||
CrashReportAnalyzer.analyze(loadLog("/crash-report/too_old_java.txt")),
|
||||
CrashReportAnalyzer.Rule.TOO_OLD_JAVA);
|
||||
assertEquals("52", result.getMatcher().group("expected"));
|
||||
assertEquals("52", result.matcher().group("expected"));
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -173,7 +173,7 @@ public class CrashReportAnalyzerTest {
|
||||
CrashReportAnalyzer.Result result = findResultByRule(
|
||||
CrashReportAnalyzer.analyze(loadLog("/crash-report/too_old_java2.txt")),
|
||||
CrashReportAnalyzer.Rule.TOO_OLD_JAVA);
|
||||
assertEquals("52", result.getMatcher().group("expected"));
|
||||
assertEquals("52", result.matcher().group("expected"));
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -188,7 +188,7 @@ public class CrashReportAnalyzerTest {
|
||||
CrashReportAnalyzer.Result result = findResultByRule(
|
||||
CrashReportAnalyzer.analyze(loadLog("/crash-report/security.txt")),
|
||||
CrashReportAnalyzer.Rule.FILE_CHANGED);
|
||||
assertEquals("assets/minecraft/texts/splashes.txt", result.getMatcher().group("file"));
|
||||
assertEquals("assets/minecraft/texts/splashes.txt", result.matcher().group("file"));
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -196,7 +196,7 @@ public class CrashReportAnalyzerTest {
|
||||
CrashReportAnalyzer.Result result = findResultByRule(
|
||||
CrashReportAnalyzer.analyze(loadLog("/crash-report/no_class_def_found_error.txt")),
|
||||
CrashReportAnalyzer.Rule.NO_CLASS_DEF_FOUND_ERROR);
|
||||
assertEquals("blk", result.getMatcher().group("class"));
|
||||
assertEquals("blk", result.matcher().group("class"));
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -204,7 +204,7 @@ public class CrashReportAnalyzerTest {
|
||||
CrashReportAnalyzer.Result result = findResultByRule(
|
||||
CrashReportAnalyzer.analyze(loadLog("/crash-report/no_class_def_found_error2.txt")),
|
||||
CrashReportAnalyzer.Rule.NO_CLASS_DEF_FOUND_ERROR);
|
||||
assertEquals("cer", result.getMatcher().group("class"));
|
||||
assertEquals("cer", result.matcher().group("class"));
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -214,7 +214,7 @@ public class CrashReportAnalyzerTest {
|
||||
CrashReportAnalyzer.Rule.FILE_ALREADY_EXISTS);
|
||||
assertEquals(
|
||||
"D:\\Games\\Minecraft\\Minecraft Longtimeusing\\.minecraft\\versions\\1.12.2-forge1.12.2-14.23.5.2775\\config\\pvpsettings.txt",
|
||||
result.getMatcher().group("file"));
|
||||
result.matcher().group("file"));
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -222,8 +222,8 @@ public class CrashReportAnalyzerTest {
|
||||
CrashReportAnalyzer.Result result = findResultByRule(
|
||||
CrashReportAnalyzer.analyze(loadLog("/crash-report/loader_exception_mod_crash.txt")),
|
||||
CrashReportAnalyzer.Rule.LOADING_CRASHED_FORGE);
|
||||
assertEquals("Better PvP", result.getMatcher().group("name"));
|
||||
assertEquals("xaerobetterpvp", result.getMatcher().group("id"));
|
||||
assertEquals("Better PvP", result.matcher().group("name"));
|
||||
assertEquals("xaerobetterpvp", result.matcher().group("id"));
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -231,8 +231,8 @@ public class CrashReportAnalyzerTest {
|
||||
CrashReportAnalyzer.Result result = findResultByRule(
|
||||
CrashReportAnalyzer.analyze(loadLog("/crash-report/loader_exception_mod_crash2.txt")),
|
||||
CrashReportAnalyzer.Rule.LOADING_CRASHED_FORGE);
|
||||
assertEquals("Inventory Sort", result.getMatcher().group("name"));
|
||||
assertEquals("invsort", result.getMatcher().group("id"));
|
||||
assertEquals("Inventory Sort", result.matcher().group("name"));
|
||||
assertEquals("invsort", result.matcher().group("id"));
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -240,8 +240,8 @@ public class CrashReportAnalyzerTest {
|
||||
CrashReportAnalyzer.Result result = findResultByRule(
|
||||
CrashReportAnalyzer.analyze(loadLog("/crash-report/loader_exception_mod_crash3.txt")),
|
||||
CrashReportAnalyzer.Rule.LOADING_CRASHED_FORGE);
|
||||
assertEquals("SuperOres", result.getMatcher().group("name"));
|
||||
assertEquals("superores", result.getMatcher().group("id"));
|
||||
assertEquals("SuperOres", result.matcher().group("name"));
|
||||
assertEquals("superores", result.matcher().group("id"));
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -249,8 +249,8 @@ public class CrashReportAnalyzerTest {
|
||||
CrashReportAnalyzer.Result result = findResultByRule(
|
||||
CrashReportAnalyzer.analyze(loadLog("/crash-report/loader_exception_mod_crash4.txt")),
|
||||
CrashReportAnalyzer.Rule.LOADING_CRASHED_FORGE);
|
||||
assertEquals("Kathairis", result.getMatcher().group("name"));
|
||||
assertEquals("kathairis", result.getMatcher().group("id"));
|
||||
assertEquals("Kathairis", result.matcher().group("name"));
|
||||
assertEquals("kathairis", result.matcher().group("id"));
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -258,7 +258,7 @@ public class CrashReportAnalyzerTest {
|
||||
CrashReportAnalyzer.Result result = findResultByRule(
|
||||
CrashReportAnalyzer.analyze(loadLog("/crash-report/loading_error_fabric.txt")),
|
||||
CrashReportAnalyzer.Rule.LOADING_CRASHED_FABRIC);
|
||||
assertEquals("test", result.getMatcher().group("id"));
|
||||
assertEquals("test", result.matcher().group("id"));
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -315,7 +315,7 @@ public class CrashReportAnalyzerTest {
|
||||
CrashReportAnalyzer.Result result = findResultByRule(
|
||||
CrashReportAnalyzer.analyze(loadLog("/logs/bootstrap.txt")),
|
||||
CrashReportAnalyzer.Rule.BOOTSTRAP_FAILED);
|
||||
assertEquals("prefab", result.getMatcher().group("id"));
|
||||
assertEquals("prefab", result.matcher().group("id"));
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -323,7 +323,7 @@ public class CrashReportAnalyzerTest {
|
||||
CrashReportAnalyzer.Result result = findResultByRule(
|
||||
CrashReportAnalyzer.analyze(loadLog("/logs/mixin_apply_mod_failed.txt")),
|
||||
CrashReportAnalyzer.Rule.MIXIN_APPLY_MOD_FAILED);
|
||||
assertEquals("enhancedblockentities", result.getMatcher().group("id"));
|
||||
assertEquals("enhancedblockentities", result.matcher().group("id"));
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -331,7 +331,7 @@ public class CrashReportAnalyzerTest {
|
||||
CrashReportAnalyzer.Result result = findResultByRule(
|
||||
CrashReportAnalyzer.analyze(loadLog("/logs/unsatisfied_link_error.txt")),
|
||||
CrashReportAnalyzer.Rule.UNSATISFIED_LINK_ERROR);
|
||||
assertEquals("lwjgl.dll", result.getMatcher().group("name"));
|
||||
assertEquals("lwjgl.dll", result.matcher().group("name"));
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -367,8 +367,8 @@ public class CrashReportAnalyzerTest {
|
||||
CrashReportAnalyzer.Result result = findResultByRule(
|
||||
CrashReportAnalyzer.analyze(loadLog("/crash-report/config.txt")),
|
||||
CrashReportAnalyzer.Rule.CONFIG);
|
||||
assertEquals("jumbofurnace", result.getMatcher().group("id"));
|
||||
assertEquals("jumbofurnace-server.toml", result.getMatcher().group("file"));
|
||||
assertEquals("jumbofurnace", result.matcher().group("id"));
|
||||
assertEquals("jumbofurnace-server.toml", result.matcher().group("file"));
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -384,7 +384,7 @@ public class CrashReportAnalyzerTest {
|
||||
"\t - You must install any version of reeses-sodium-options.\n" +
|
||||
" - Conflicting versions found for fabric-screen-api-v1: used 1.0.4+155f865c18, also found 1.0.4+198a96213d\n" +
|
||||
" - Conflicting versions found for fabric-key-binding-api-v1: used 1.0.4+a02b446318, also found 1.0.4+a02b44633d\n").replaceAll("\\s+", ""),
|
||||
result.getMatcher().group("reason").replaceAll("\\s+", ""));
|
||||
result.matcher().group("reason").replaceAll("\\s+", ""));
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -398,7 +398,7 @@ public class CrashReportAnalyzerTest {
|
||||
"Unmet dependency listing:\n" +
|
||||
"\t - Mod 'Roughly Searchable' (roughlysearchable) 2.2.1+1.17.1 requires version 6.0.2 or later of roughlyenoughitems, which is missing!\n" +
|
||||
"\tat net.fabricmc.loader.impl.FabricLoaderImpl.load(FabricLoaderImpl.java:190) ~").replaceAll("\\s+", ""),
|
||||
result.getMatcher().group("reason").replaceAll("\\s+", ""));
|
||||
result.matcher().group("reason").replaceAll("\\s+", ""));
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -414,7 +414,7 @@ public class CrashReportAnalyzerTest {
|
||||
"\t - 模组 'Sodium Extra' (sodium-extra) 0.5.4+mc1.20.4-build.116 需要 fabric-api 的 任意版本,但没有安装它!\n" +
|
||||
"\t - 模组 'Sodium Extra' (sodium-extra) 0.5.4+mc1.20.4-build.116 需要 sodium 的 0.5.6 及以上版本,但没有安装它!\n" +
|
||||
"\tat net.fabricmc.loader.impl.FormattedException.ofLocalized(FormattedException.java:51) ~").replaceAll("\\s+", ""),
|
||||
result.getMatcher().group("reason").replaceAll("\\s+", ""));
|
||||
result.matcher().group("reason").replaceAll("\\s+", ""));
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -422,8 +422,8 @@ public class CrashReportAnalyzerTest {
|
||||
CrashReportAnalyzer.Result result = findResultByRule(
|
||||
CrashReportAnalyzer.analyze(loadLog("/logs/fabric-mod-conflict.txt")),
|
||||
CrashReportAnalyzer.Rule.MOD_RESOLUTION_CONFLICT);
|
||||
assertEquals("phosphor", result.getMatcher().group("sourcemod"));
|
||||
assertEquals("{starlight @ [*]}", result.getMatcher().group("destmod"));
|
||||
assertEquals("phosphor", result.matcher().group("sourcemod"));
|
||||
assertEquals("{starlight @ [*]}", result.matcher().group("destmod"));
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -431,8 +431,8 @@ public class CrashReportAnalyzerTest {
|
||||
CrashReportAnalyzer.Result result = findResultByRule(
|
||||
CrashReportAnalyzer.analyze(loadLog("/logs/fabric-mod-missing.txt")),
|
||||
CrashReportAnalyzer.Rule.MOD_RESOLUTION_MISSING);
|
||||
assertEquals("pca", result.getMatcher().group("sourcemod"));
|
||||
assertEquals("{fabric @ [>=0.39.2]}", result.getMatcher().group("destmod"));
|
||||
assertEquals("pca", result.matcher().group("sourcemod"));
|
||||
assertEquals("{fabric @ [>=0.39.2]}", result.matcher().group("destmod"));
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -468,8 +468,8 @@ public class CrashReportAnalyzerTest {
|
||||
CrashReportAnalyzer.Result result = findResultByRule(
|
||||
CrashReportAnalyzer.analyze(loadLog("/logs/fabric-minecraft.txt")),
|
||||
CrashReportAnalyzer.Rule.MOD_RESOLUTION_MISSING_MINECRAFT);
|
||||
assertEquals("fabric", result.getMatcher().group("mod"));
|
||||
assertEquals("[~1.16.2-alpha.20.28.a]", result.getMatcher().group("version"));
|
||||
assertEquals("fabric", result.matcher().group("mod"));
|
||||
assertEquals("[~1.16.2-alpha.20.28.a]", result.matcher().group("version"));
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -632,9 +632,9 @@ public class CrashReportAnalyzerTest {
|
||||
CrashReportAnalyzer.analyze(loadLog("/crash-report/mod/customnpc.txt")),
|
||||
CrashReportAnalyzer.Rule.ENTITY);
|
||||
assertEquals("customnpcs.CustomNpc (noppes.npcs.entity.EntityCustomNpc)",
|
||||
result.getMatcher().group("type"));
|
||||
result.matcher().group("type"));
|
||||
assertEquals("99942.59, 4.00, 100000.98",
|
||||
result.getMatcher().group("location"));
|
||||
result.matcher().group("location"));
|
||||
|
||||
assertEquals(
|
||||
new HashSet<>(Arrays.asList("npcs", "noppes")),
|
||||
@@ -647,9 +647,9 @@ public class CrashReportAnalyzerTest {
|
||||
CrashReportAnalyzer.analyze(loadLog("/crash-report/mod/tconstruct.txt")),
|
||||
CrashReportAnalyzer.Rule.BLOCK);
|
||||
assertEquals("Block{tconstruct:seared_drain}[active=true,facing=north]",
|
||||
result.getMatcher().group("type"));
|
||||
result.matcher().group("type"));
|
||||
assertEquals("World: (1370,92,-738), Chunk: (at 10,5,14 in 85,-47; contains blocks 1360,0,-752 to 1375,255,-737), Region: (2,-2; contains chunks 64,-64 to 95,-33, blocks 1024,0,-1024 to 1535,255,-513)",
|
||||
result.getMatcher().group("location"));
|
||||
result.matcher().group("location"));
|
||||
|
||||
assertEquals(
|
||||
new HashSet<>(Arrays.asList("tconstruct", "slimeknights", "smeltery")),
|
||||
|
||||
+23
-3
@@ -38,11 +38,31 @@ Para los sistemas y arquitecturas de CPU compatibles con HMCL, consulta [esta ta
|
||||
|
||||
## Descarga
|
||||
|
||||
Descarga la última versión desde el [sitio web oficial](https://hmcl.huangyuhui.net/download).
|
||||
Puedes descargar HMCL desde las siguientes fuentes:
|
||||
|
||||
También puedes encontrar la última versión de HMCL en [GitHub Releases](https://github.com/HMCL-dev/HMCL/releases).
|
||||
- [Sitio web oficial de HMCL](https://hmcl.huangyuhui.net/download)
|
||||
- [GitHub Release](https://github.com/HMCL-dev/HMCL/releases)
|
||||
- [CNB Release](https://cnb.cool/HMCL-dev/HMCL/-/releases)
|
||||
|
||||
Aunque no es necesario, se recomienda descargar las versiones solo de los sitios web oficiales mencionados anteriormente.
|
||||
## Contribuir
|
||||
|
||||
HMCL es un proyecto de código abierto impulsado por la comunidad, y todos son bienvenidos a contribuir código o enviar sugerencias.
|
||||
|
||||
Puedes contribuir al desarrollo de HMCL de las siguientes maneras:
|
||||
|
||||
- Informa errores o solicita funciones [creando un issue](https://github.com/HMCL-dev/HMCL/issues/new/choose) en GitHub.
|
||||
- Contribuye código haciendo fork del repositorio en GitHub y [enviando un pull request](https://github.com/HMCL-dev/HMCL/compare).
|
||||
|
||||
Antes de contribuir, lee la [guía de contribución](./Contributing.md), que incluye lo siguiente:
|
||||
|
||||
- [Cómo compilar y ejecutar HMCL desde el código fuente](./Contributing.md#build-hmcl)
|
||||
- [Cómo ajustar el comportamiento de HMCL con opciones de depuración](./Contributing.md#debug-options)
|
||||
|
||||
## Colaboradores
|
||||
|
||||
Desde 2015, más de 120 colaboradores han participado en HMCL. Gracias por su trabajo.
|
||||
|
||||
[](https://github.com/HMCL-dev/HMCL/graphs/contributors)
|
||||
|
||||
## Licencia
|
||||
|
||||
|
||||
+23
-3
@@ -38,11 +38,31 @@ HMCLが対応しているシステムやCPUアーキテクチャについては
|
||||
|
||||
## ダウンロード
|
||||
|
||||
最新版は[公式サイト](https://hmcl.huangyuhui.net/download)からダウンロードできます。
|
||||
HMCL は以下の場所からダウンロードできます。
|
||||
|
||||
また、[GitHub Releases](https://github.com/HMCL-dev/HMCL/releases)でも最新版を入手できます。
|
||||
- [HMCL 公式サイト](https://hmcl.huangyuhui.net/download)
|
||||
- [GitHub Release](https://github.com/HMCL-dev/HMCL/releases)
|
||||
- [CNB Release](https://cnb.cool/HMCL-dev/HMCL/-/releases)
|
||||
|
||||
必要ではありませんが、上記の公式サイトからのみリリース版をダウンロードすることを推奨します。
|
||||
## 貢献
|
||||
|
||||
HMCL はコミュニティ主導のオープンソースプロジェクトであり、コードの貢献や提案を歓迎しています。
|
||||
|
||||
HMCL の開発には、次の方法で参加できます。
|
||||
|
||||
- GitHub で [Issue を作成](https://github.com/HMCL-dev/HMCL/issues/new/choose)して、バグ報告や機能要望を行う。
|
||||
- GitHub でリポジトリを fork し、[Pull Request を送信](https://github.com/HMCL-dev/HMCL/compare)してコードを貢献する。
|
||||
|
||||
貢献する前に、以下の内容を含む[貢献ガイド](./Contributing.md)をお読みください。
|
||||
|
||||
- [ソースコードから HMCL をビルドして実行する方法](./Contributing.md#build-hmcl)
|
||||
- [デバッグオプションで HMCL の動作を調整する方法](./Contributing.md#debug-options)
|
||||
|
||||
## 貢献者
|
||||
|
||||
2015 年以来、120 人を超える貢献者が HMCL に参加しています。ご協力ありがとうございます。
|
||||
|
||||
[](https://github.com/HMCL-dev/HMCL/graphs/contributors)
|
||||
|
||||
## ライセンス
|
||||
|
||||
|
||||
+23
-3
@@ -38,11 +38,31 @@ HMCL обладает отличной кроссплатформенность
|
||||
|
||||
## Загрузка
|
||||
|
||||
Скачайте последнюю версию с [официального сайта](https://hmcl.huangyuhui.net/download).
|
||||
Вы можете скачать HMCL из следующих источников:
|
||||
|
||||
Также вы можете найти последнюю версию HMCL в [релизах на GitHub](https://github.com/HMCL-dev/HMCL/releases).
|
||||
- [Официальный сайт HMCL](https://hmcl.huangyuhui.net/download)
|
||||
- [GitHub Release](https://github.com/HMCL-dev/HMCL/releases)
|
||||
- [CNB Release](https://cnb.cool/HMCL-dev/HMCL/-/releases)
|
||||
|
||||
Хотя это не обязательно, рекомендуется скачивать релизы только с указанных выше официальных сайтов.
|
||||
## Участие
|
||||
|
||||
HMCL — это открытый проект, развиваемый сообществом. Мы рады любому вкладу в код и предложениям.
|
||||
|
||||
Вы можете помочь разработке HMCL следующими способами:
|
||||
|
||||
- Сообщайте об ошибках или предлагайте новые функции, [создав issue](https://github.com/HMCL-dev/HMCL/issues/new/choose) на GitHub.
|
||||
- Вносите код, сделав fork репозитория на GitHub и [отправив pull request](https://github.com/HMCL-dev/HMCL/compare).
|
||||
|
||||
Перед участием прочитайте [руководство для участников](./Contributing.md), в котором описано следующее:
|
||||
|
||||
- [Как собрать и запустить HMCL из исходного кода](./Contributing.md#build-hmcl)
|
||||
- [Как изменять поведение HMCL с помощью параметров отладки](./Contributing.md#debug-options)
|
||||
|
||||
## Участники
|
||||
|
||||
С 2015 года в разработке HMCL участвовали более 120 человек. Спасибо за ваш труд!
|
||||
|
||||
[](https://github.com/HMCL-dev/HMCL/graphs/contributors)
|
||||
|
||||
## Лицензия
|
||||
|
||||
|
||||
+23
-10
@@ -38,19 +38,32 @@ HMCL має чудові кросплатформні можливості. Ві
|
||||
|
||||
## Завантаження
|
||||
|
||||
Завантажте останню версію з [офіційного сайту](https://hmcl.huangyuhui.net/download).
|
||||
Ви можете завантажити HMCL з таких джерел:
|
||||
|
||||
Також ви можете знайти останню версію HMCL у [релізах GitHub](https://github.com/HMCL-dev/HMCL/releases).
|
||||
- [Офіційний сайт HMCL](https://hmcl.huangyuhui.net/download)
|
||||
- [GitHub Release](https://github.com/HMCL-dev/HMCL/releases)
|
||||
- [CNB Release](https://cnb.cool/HMCL-dev/HMCL/-/releases)
|
||||
|
||||
Хоча це не обовʼязково, рекомендується завантажувати релізи лише з офіційних сайтів, зазначених вище.
|
||||
## Внесок
|
||||
|
||||
HMCL — це відкритий проект, який розвиває спільнота. Ми раді внескам у код і пропозиціям.
|
||||
|
||||
Ви можете допомогти розробці HMCL такими способами:
|
||||
|
||||
- Повідомляйте про помилки або пропонуйте нові функції, [створивши issue](https://github.com/HMCL-dev/HMCL/issues/new/choose) на GitHub.
|
||||
- Додавайте код, зробивши fork репозиторію на GitHub і [надіславши pull request](https://github.com/HMCL-dev/HMCL/compare).
|
||||
|
||||
Перед участю прочитайте [посібник для учасників](./Contributing.md), який містить таке:
|
||||
|
||||
- [Як зібрати і запустити HMCL з вихідного коду](./Contributing.md#build-hmcl)
|
||||
- [Як змінювати поведінку HMCL за допомогою параметрів налагодження](./Contributing.md#debug-options)
|
||||
|
||||
## Учасники
|
||||
|
||||
З 2015 року в розробці HMCL взяли участь понад 120 людей. Дякуємо за вашу працю!
|
||||
|
||||
[](https://github.com/HMCL-dev/HMCL/graphs/contributors)
|
||||
|
||||
## Ліцензія
|
||||
|
||||
Дивіться [README.md](README.md#license).
|
||||
|
||||
## Внесок
|
||||
|
||||
Якщо ви хочете надіслати pull request, ознайомтеся з наступними вимогами:
|
||||
|
||||
* IDE: IntelliJ IDEA
|
||||
* Компілятор: Java 17+
|
||||
+1
-1
@@ -73,6 +73,6 @@ HMCL 是一个社区驱动的开源项目,欢迎任何人参与贡献代码或
|
||||
1. 当你分发该程序的修改版本时,你必须以一种合理的方式修改该程序的名称或版本号,以示其与原始版本不同。(
|
||||
依据 [GPLv3, 7(c)](https://github.com/HMCL-dev/HMCL/blob/11820e31a85d8989e41d97476712b07e7094b190/LICENSE#L372-L374))
|
||||
|
||||
该程序的名称及版本号可在 [此处](https://github.com/HMCL-dev/HMCL/blob/javafx/HMCL/src/main/java/org/jackhuang/hmcl/Metadata.java#L33-L35) 修改。
|
||||
该程序的名称及版本号可在 [此处](https://github.com/HMCL-dev/HMCL/blob/86529c6f15b3be54ed8d89ece47566cf57f43eb7/HMCL/src/main/java/org/jackhuang/hmcl/Metadata.java#L36-L38) 修改。
|
||||
|
||||
2. 你不得移除该程序所显示的版权声明。(依据 [GPLv3, 7(b)](https://github.com/HMCL-dev/HMCL/blob/11820e31a85d8989e41d97476712b07e7094b190/LICENSE#L368-L370))
|
||||
@@ -72,6 +72,6 @@ HMCL 是一個由社群驅動的開源專案,歡迎任何人參與貢獻程式
|
||||
|
||||
1. 當你分發該程式的修改版本時,你必須以一種合理的方式修改該程式的名稱或版本號,以示其與原始版本不同。(依據 [GPLv3, 7(c)](https://github.com/HMCL-dev/HMCL/blob/11820e31a85d8989e41d97476712b07e7094b190/LICENSE#L372-L374))
|
||||
|
||||
該程式的名稱及版本號可在 [此處](https://github.com/HMCL-dev/HMCL/blob/javafx/HMCL/src/main/java/org/jackhuang/hmcl/Metadata.java#L33-L35) 修改。
|
||||
該程式的名稱及版本號可在 [此處](https://github.com/HMCL-dev/HMCL/blob/86529c6f15b3be54ed8d89ece47566cf57f43eb7/HMCL/src/main/java/org/jackhuang/hmcl/Metadata.java#L36-L38) 修改。
|
||||
|
||||
2. 你不得移除該程式所顯示的版權宣告。(依據 [GPLv3, 7(b)](https://github.com/HMCL-dev/HMCL/blob/11820e31a85d8989e41d97476712b07e7094b190/LICENSE#L368-L370))
|
||||
Reference in new issue
Block a user