Compare commits

...
22 Commits
Author SHA1 Message Date
Glavo 1c0e506d34 Enhance renderer selection based on game version (#6316) 2026-07-05 20:42:24 +08:00
庐庐绫 6374ec0de7 fix: 修复联机核心第三方下载渠道按钮高度不一致 (#6247)
close #6245
2026-07-05 20:06:23 +08:00
快乐凤爪王 a6b3872af4 Fix: Fixed truncated authlib injector warning text (#6301) 2026-07-05 20:06:02 +08:00
辞庐 ad98a5a6d4 HMCL ProcessListener 游戏退出日志行开头添加时间 (#6314) 2026-07-05 20:04:51 +08:00
快乐凤爪王 05393af537 Fix: Adjust account item spacing (#6302) 2026-07-05 19:57:14 +08:00
Glavo 3438da2ab9 Add game version handling for graphics backend visibility (#6300) 2026-07-05 19:56:02 +08:00
辞庐 3163b465ec fix: 拷贝 uuid 按钮冗余 SpinnerPane (#6315) 2026-07-05 19:54:19 +08:00
辞庐 573dac471c 禁用密码框右键菜单的 剪切 & 复制 按钮 (#6310) 2026-07-05 19:53:43 +08:00
ToobLacandGlavo 12f54f353a [Bugfix] 修复 CF 文件下载未带有 api key 的问题 (#6297)
Fixes #6293

Co-authored-by: Glavo <zjx001202@gmail.com>
2026-07-03 21:26:58 +08:00
Glavo 246699d571 refactor: update preset management terminology for clarity (#6298) 2026-07-03 21:26:14 +08:00
Haoran Xu 3480e6974b Docs: synchronize localized README content (#6235) 2026-07-03 21:12:23 +08:00
Zkitefly 6d14351833 feat(mod): 支持读取Forge新模组元数据中的logo文件 (#6253) 2026-07-03 20:22:24 +08:00
Glavo fd0d83257d 调整旧版实例设置迁移与默认父预设解析逻辑 (#6287) 2026-07-03 20:19:20 +08:00
ToobLac 0eb1d3cf4b [Enhancement] 无论何时都显示关闭愚人节模式的按钮 (#6105)
Closes #5982
2026-07-02 21:54:39 +08:00
辞庐 f9b8dad3d2 修复安装手动打包整合包时任务名未本地化的问题 (#6248) 2026-07-02 21:50:49 +08:00
Glavo 57fbd0910e 不再将从旧版本游戏文件夹设置中迁移出的游戏预设名称设置为文件夹名称 (#6292) 2026-07-02 21:47:51 +08:00
快乐凤爪王 7767a57f50 Fix: Restrict JVM memory text fields to numeric input only (#6260) 2026-07-02 21:46:11 +08:00
快乐凤爪王 f024dba940 Fix: Dynamically lock fixedCellSize of VersionsPageSkin (#6288) 2026-07-02 21:15:03 +08:00
Wulian233 52736d3c86 导出主题时支持设置主题版本 (#6284) 2026-07-02 21:06:36 +08:00
ToobLac f75b472068 [Chores] 其他类向记录的转换 (#6277) 2026-07-02 21:03:36 +08:00
ToobLac d8e0c9e9c7 [Chore] 优化default_addon_source的中文翻译 (#6278) 2026-07-01 20:38:04 +08:00
快乐凤爪王 9601c1c1ea Fix: Aligned the height of the jfx-color-picker with the jfx-text-field (#6285) 2026-07-01 20:37:30 +08:00
83 changed files with 1029 additions and 1059 deletions

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",
@@ -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));
};
}
}
@@ -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
@@ -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;
@@ -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.
+1 -1
View File
@@ -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.
}
@@ -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) {
}
}
@@ -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) {
}
@@ -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
@@ -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",
@@ -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;
}
}
@@ -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;
}
@@ -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;
}
}
@@ -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
View File
@@ -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.
[![Contributors](https://contrib.rocks/image?repo=HMCL-dev/HMCL)](https://github.com/HMCL-dev/HMCL/graphs/contributors)
## Licencia
+23 -3
View File
@@ -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 に参加しています。ご協力ありがとうございます。
[![Contributors](https://contrib.rocks/image?repo=HMCL-dev/HMCL)](https://github.com/HMCL-dev/HMCL/graphs/contributors)
## ライセンス
+23 -3
View File
@@ -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 человек. Спасибо за ваш труд!
[![Contributors](https://contrib.rocks/image?repo=HMCL-dev/HMCL)](https://github.com/HMCL-dev/HMCL/graphs/contributors)
## Лицензия
+23 -10
View File
@@ -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 людей. Дякуємо за вашу працю!
[![Contributors](https://contrib.rocks/image?repo=HMCL-dev/HMCL)](https://github.com/HMCL-dev/HMCL/graphs/contributors)
## Ліцензія
Дивіться [README.md](README.md#license).
## Внесок
Якщо ви хочете надіслати pull request, ознайомтеся з наступними вимогами:
* IDE: IntelliJ IDEA
* Компілятор: Java 17+
+1 -1
View File
@@ -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))
+1 -1
View File
@@ -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))