diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/game/LauncherHelper.java b/HMCL/src/main/java/org/jackhuang/hmcl/game/LauncherHelper.java index 412119bba..645322531 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/game/LauncherHelper.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/game/LauncherHelper.java @@ -190,7 +190,7 @@ public final class LauncherHelper { .thenComposeAsync(() -> logIn(account).withStage("launch.state.logging_in")) .thenComposeAsync(authInfo -> Task.supplyAsync(() -> { LaunchOptions launchOptions = repository.getLaunchOptions( - selectedVersion, javaVersionRef.get(), profile.getGameDir().toPath(), javaAgents, javaArguments, scriptFile != null); + selectedVersion, javaVersionRef.get(), profile.getGameDir(), javaAgents, javaArguments, scriptFile != null); LOG.info("Here's the structure of game mod directory:\n" + FileUtils.printFileStructure(repository.getModManager(selectedVersion).getModsDirectory(), 10)); diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/game/ModpackHelper.java b/HMCL/src/main/java/org/jackhuang/hmcl/game/ModpackHelper.java index 19eec2725..2cb2c3c2b 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/game/ModpackHelper.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/game/ModpackHelper.java @@ -184,7 +184,7 @@ public final class ModpackHelper { return new ManuallyCreatedModpackInstallTask(profile, zipFile.toPath(), charset, name) .thenAcceptAsync(Schedulers.javafx(), location -> { - Profile newProfile = new Profile(name, location.toFile()); + Profile newProfile = new Profile(name, location); newProfile.setUseRelativePath(true); Profiles.getProfiles().add(newProfile); Profiles.setSelectedProfile(newProfile); diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/setting/AuthlibInjectorServers.java b/HMCL/src/main/java/org/jackhuang/hmcl/setting/AuthlibInjectorServers.java index a68b9b836..e5e7f6edc 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/setting/AuthlibInjectorServers.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/setting/AuthlibInjectorServers.java @@ -21,6 +21,7 @@ import com.google.gson.JsonParseException; import org.jackhuang.hmcl.auth.authlibinjector.AuthlibInjectorServer; import org.jackhuang.hmcl.task.Schedulers; import org.jackhuang.hmcl.task.Task; +import org.jackhuang.hmcl.util.gson.JsonSerializable; import org.jackhuang.hmcl.util.gson.JsonUtils; import org.jackhuang.hmcl.util.gson.TolerableValidationException; import org.jackhuang.hmcl.util.gson.Validation; @@ -37,6 +38,7 @@ import java.util.concurrent.CopyOnWriteArraySet; import static org.jackhuang.hmcl.setting.ConfigHolder.config; import static org.jackhuang.hmcl.util.logging.Logger.LOG; +@JsonSerializable public final class AuthlibInjectorServers implements Validation { public static final String CONFIG_FILENAME = "authlib-injectors.json"; diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/setting/Config.java b/HMCL/src/main/java/org/jackhuang/hmcl/setting/Config.java index 10aa58dfc..0d182f386 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/setting/Config.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/setting/Config.java @@ -45,6 +45,7 @@ import java.io.File; import java.lang.invoke.MethodHandles; import java.lang.reflect.*; import java.net.Proxy; +import java.nio.file.Path; import java.util.*; @JsonAdapter(value = Config.Adapter.class) @@ -55,6 +56,7 @@ public final class Config implements Observable { public static final Gson CONFIG_GSON = new GsonBuilder() .registerTypeAdapter(File.class, FileTypeAdapter.INSTANCE) + .registerTypeAdapter(Path.class, PathTypeAdapter.INSTANCE) .registerTypeAdapter(ObservableList.class, new ObservableListCreator()) .registerTypeAdapter(ObservableSet.class, new ObservableSetCreator()) .registerTypeAdapter(ObservableMap.class, new ObservableMapCreator()) diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/setting/Profile.java b/HMCL/src/main/java/org/jackhuang/hmcl/setting/Profile.java index 8472d7fd2..d37d1787b 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/setting/Profile.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/setting/Profile.java @@ -35,8 +35,8 @@ import org.jackhuang.hmcl.ui.WeakListenerHolder; import org.jackhuang.hmcl.util.ToStringBuilder; import org.jackhuang.hmcl.util.javafx.ObservableHelper; -import java.io.File; import java.lang.reflect.Type; +import java.nio.file.Path; import java.util.Optional; import static org.jackhuang.hmcl.ui.FXUtils.onInvalidating; @@ -65,17 +65,17 @@ public final class Profile implements Observable { this.selectedVersion.set(selectedVersion); } - private final ObjectProperty gameDir; + private final ObjectProperty gameDir; - public ObjectProperty gameDirProperty() { + public ObjectProperty gameDirProperty() { return gameDir; } - public File getGameDir() { + public Path getGameDir() { return gameDir.get(); } - public void setGameDir(File gameDir) { + public void setGameDir(Path gameDir) { this.gameDir.set(gameDir); } @@ -103,7 +103,7 @@ public final class Profile implements Observable { this.name.set(name); } - private BooleanProperty useRelativePath = new SimpleBooleanProperty(this, "useRelativePath", false); + private final BooleanProperty useRelativePath = new SimpleBooleanProperty(this, "useRelativePath", false); public BooleanProperty useRelativePathProperty() { return useRelativePath; @@ -118,26 +118,26 @@ public final class Profile implements Observable { } public Profile(String name) { - this(name, new File(".minecraft")); + this(name, Path.of(".minecraft")); } - public Profile(String name, File initialGameDir) { + public Profile(String name, Path initialGameDir) { this(name, initialGameDir, new VersionSetting()); } - public Profile(String name, File initialGameDir, VersionSetting global) { + public Profile(String name, Path initialGameDir, VersionSetting global) { this(name, initialGameDir, global, null, false); } - public Profile(String name, File initialGameDir, VersionSetting global, String selectedVersion, boolean useRelativePath) { + public Profile(String name, Path initialGameDir, VersionSetting global, String selectedVersion, boolean useRelativePath) { this.name = new SimpleStringProperty(this, "name", name); gameDir = new SimpleObjectProperty<>(this, "gameDir", initialGameDir); - repository = new HMCLGameRepository(this, initialGameDir.toPath()); + repository = new HMCLGameRepository(this, initialGameDir); this.global.set(global == null ? new VersionSetting() : global); this.selectedVersion.set(selectedVersion); this.useRelativePath.set(useRelativePath); - gameDir.addListener((a, b, newValue) -> repository.changeDirectory(newValue.toPath())); + gameDir.addListener((a, b, newValue) -> repository.changeDirectory(newValue)); this.selectedVersion.addListener(o -> checkSelectedVersion()); listenerHolder.add(EventBus.EVENT_BUS.channel(RefreshedVersionsEvent.class).registerWeak(event -> checkSelectedVersion(), EventPriority.HIGHEST)); @@ -234,7 +234,7 @@ public final class Profile implements Observable { JsonObject jsonObject = new JsonObject(); jsonObject.add("global", context.serialize(src.getGlobal())); - jsonObject.addProperty("gameDir", src.getGameDir().getPath()); + jsonObject.addProperty("gameDir", src.getGameDir().toString()); jsonObject.addProperty("useRelativePath", src.isUseRelativePath()); jsonObject.addProperty("selectedMinecraftVersion", src.getSelectedVersion()); @@ -243,12 +243,11 @@ public final class Profile implements Observable { @Override public Profile deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws JsonParseException { - if (json == JsonNull.INSTANCE || !(json instanceof JsonObject)) return null; - JsonObject obj = (JsonObject) json; + if (!(json instanceof JsonObject obj)) return null; String gameDir = Optional.ofNullable(obj.get("gameDir")).map(JsonElement::getAsString).orElse(""); return new Profile("Default", - new File(gameDir), + Path.of(gameDir), context.deserialize(obj.get("global"), VersionSetting.class), Optional.ofNullable(obj.get("selectedMinecraftVersion")).map(JsonElement::getAsString).orElse(""), Optional.ofNullable(obj.get("useRelativePath")).map(JsonElement::getAsBoolean).orElse(false)); diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/setting/Profiles.java b/HMCL/src/main/java/org/jackhuang/hmcl/setting/Profiles.java index fd4022ea4..b4af9d5c6 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/setting/Profiles.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/setting/Profiles.java @@ -26,7 +26,7 @@ import org.jackhuang.hmcl.Metadata; import org.jackhuang.hmcl.event.EventBus; import org.jackhuang.hmcl.event.RefreshedVersionsEvent; -import java.io.File; +import java.nio.file.Path; import java.util.ArrayList; import java.util.HashSet; import java.util.List; @@ -48,20 +48,17 @@ public final class Profiles { } public static String getProfileDisplayName(Profile profile) { - switch (profile.getName()) { - case Profiles.DEFAULT_PROFILE: - return i18n("profile.default"); - case Profiles.HOME_PROFILE: - return i18n("profile.home"); - default: - return profile.getName(); - } + return switch (profile.getName()) { + case Profiles.DEFAULT_PROFILE -> i18n("profile.default"); + case Profiles.HOME_PROFILE -> i18n("profile.home"); + default -> profile.getName(); + }; } private static final ObservableList profiles = observableArrayList(profile -> new Observable[] { profile }); private static final ReadOnlyListWrapper profilesWrapper = new ReadOnlyListWrapper<>(profiles); - private static ObjectProperty selectedProfile = new SimpleObjectProperty() { + private static final ObjectProperty selectedProfile = new SimpleObjectProperty() { { profiles.addListener(onInvalidating(this::invalidated)); } @@ -104,8 +101,8 @@ public final class Profiles { private static void checkProfiles() { if (profiles.isEmpty()) { - Profile current = new Profile(Profiles.DEFAULT_PROFILE, new File(".minecraft"), new VersionSetting(), null, true); - Profile home = new Profile(Profiles.HOME_PROFILE, Metadata.MINECRAFT_DIRECTORY.toFile()); + Profile current = new Profile(Profiles.DEFAULT_PROFILE, Path.of(".minecraft"), new VersionSetting(), null, true); + Profile home = new Profile(Profiles.HOME_PROFILE, Metadata.MINECRAFT_DIRECTORY); Platform.runLater(() -> profiles.addAll(current, home)); } } diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/profile/ProfilePage.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/profile/ProfilePage.java index 71f27f099..91ea0b5ef 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/profile/ProfilePage.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/profile/ProfilePage.java @@ -42,8 +42,9 @@ import org.jackhuang.hmcl.ui.construct.OptionToggleButton; import org.jackhuang.hmcl.ui.construct.PageCloseEvent; import org.jackhuang.hmcl.ui.decorator.DecoratorPage; import org.jackhuang.hmcl.util.StringUtils; +import org.jackhuang.hmcl.util.io.FileUtils; -import java.io.File; +import java.nio.file.Path; import java.util.Optional; import static org.jackhuang.hmcl.util.i18n.I18n.i18n; @@ -68,7 +69,7 @@ public final class ProfilePage extends BorderPane implements DecoratorPage { state.set(State.fromTitle(profile == null ? i18n("profile.new") : i18n("profile") + " - " + profileDisplayName)); location = new SimpleStringProperty(this, "location", - Optional.ofNullable(profile).map(Profile::getGameDir).map(File::getAbsolutePath).orElse(".minecraft")); + Optional.ofNullable(profile).map(Profile::getGameDir).map(FileUtils::getAbsolutePath).orElse(".minecraft")); ScrollPane scroll = new ScrollPane(); this.setCenter(scroll); @@ -153,13 +154,13 @@ public final class ProfilePage extends BorderPane implements DecoratorPage { profile.setName(txtProfileName.getText()); profile.setUseRelativePath(toggleUseRelativePath.isSelected()); if (StringUtils.isNotBlank(getLocation())) { - profile.setGameDir(new File(getLocation())); + profile.setGameDir(Path.of(getLocation())); } } else { if (StringUtils.isBlank(getLocation())) { gameDir.onExplore(); } - Profile newProfile = new Profile(txtProfileName.getText(), new File(getLocation())); + Profile newProfile = new Profile(txtProfileName.getText(), Path.of(getLocation())); newProfile.setUseRelativePath(toggleUseRelativePath.isSelected()); Profiles.getProfiles().add(newProfile); } diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/util/gson/JsonUtils.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/util/gson/JsonUtils.java index be0f51e96..12f76bccb 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/util/gson/JsonUtils.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/util/gson/JsonUtils.java @@ -151,6 +151,7 @@ public final class JsonUtils { .registerTypeAdapter(Instant.class, InstantTypeAdapter.INSTANCE) .registerTypeAdapter(UUID.class, UUIDTypeAdapter.INSTANCE) .registerTypeAdapter(File.class, FileTypeAdapter.INSTANCE) + .registerTypeAdapter(Path.class, PathTypeAdapter.INSTANCE) .registerTypeAdapterFactory(ValidationTypeAdapterFactory.INSTANCE) .registerTypeAdapterFactory(LowerCaseEnumTypeAdapterFactory.INSTANCE) .registerTypeAdapterFactory(JsonTypeAdapterFactory.INSTANCE); diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/util/gson/PathTypeAdapter.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/util/gson/PathTypeAdapter.java new file mode 100644 index 000000000..8722e5722 --- /dev/null +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/util/gson/PathTypeAdapter.java @@ -0,0 +1,62 @@ +/* + * Hello Minecraft! Launcher + * Copyright (C) 2025 huangyuhui and contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package org.jackhuang.hmcl.util.gson; + +import com.google.gson.TypeAdapter; +import com.google.gson.stream.JsonReader; +import com.google.gson.stream.JsonToken; +import com.google.gson.stream.JsonWriter; + +import java.io.File; +import java.io.IOException; +import java.nio.file.FileSystems; +import java.nio.file.Path; + +/// @author Glavo +public final class PathTypeAdapter extends TypeAdapter { + + public static final PathTypeAdapter INSTANCE = new PathTypeAdapter(); + + @Override + public Path read(JsonReader in) throws IOException { + if (in.peek() == JsonToken.NULL) { + in.nextNull(); + return null; + } + + String value = in.nextString(); + if (File.separatorChar == '\\') + value = value.replace('/', '\\'); + return Path.of(value); + } + + @Override + public void write(JsonWriter out, Path path) throws IOException { + if (path != null) { + if (path.getFileSystem() != FileSystems.getDefault()) + throw new IOException("Unsupported file system: " + path.getFileSystem()); + + String value = path.toString(); + if (!path.isAbsolute() && File.separatorChar == '\\') + value = value.replace('\\', '/'); + out.value(value); + } else { + out.nullValue(); + } + } +}