Skip to content

Commit 21bb25c

Browse files
committed
Simplify PortablePath handling
1 parent 0dad55f commit 21bb25c

9 files changed

Lines changed: 112 additions & 59 deletions

File tree

HMCL/src/main/java/org/jackhuang/hmcl/game/LauncherHelper.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -224,7 +224,7 @@ private void launch0() {
224224
.thenComposeAsync(() -> logIn(account).withStage("launch.state.logging_in"))
225225
.thenComposeAsync(authInfo -> Task.supplyAsync(() -> {
226226
LaunchOptions.Builder launchOptionsBuilder = repository.getLaunchOptions(
227-
selectedVersion, javaVersionRef.get(), profile.getGameDir(), javaAgents, javaArguments, scriptFile != null);
227+
selectedVersion, javaVersionRef.get(), profile.getPath().toPath(), javaAgents, javaArguments, scriptFile != null);
228228
if (disableOfflineSkin) {
229229
launchOptionsBuilder.setDaemon(false);
230230
}

HMCL/src/main/java/org/jackhuang/hmcl/game/ModpackHelper.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@
3939
import org.jackhuang.hmcl.task.Schedulers;
4040
import org.jackhuang.hmcl.task.Task;
4141
import org.jackhuang.hmcl.util.Lang;
42+
import org.jackhuang.hmcl.util.PortablePath;
4243
import org.jackhuang.hmcl.util.function.ExceptionalConsumer;
4344
import org.jackhuang.hmcl.util.function.ExceptionalRunnable;
4445
import org.jackhuang.hmcl.util.gson.JsonUtils;
@@ -190,8 +191,7 @@ public static Task<?> getInstallManuallyCreatedModpackTask(Profile profile, Path
190191

191192
return new ManuallyCreatedModpackInstallTask(profile, zipFile, charset, name)
192193
.thenAcceptAsync(Schedulers.javafx(), location -> {
193-
Profile newProfile = new Profile(name, location);
194-
newProfile.setUseRelativePath(true);
194+
Profile newProfile = new Profile(name, PortablePath.fromPath(location));
195195
Profiles.getProfiles().add(newProfile);
196196
Profiles.setSelectedProfile(newProfile);
197197
});

HMCL/src/main/java/org/jackhuang/hmcl/setting/Profile.java

Lines changed: 37 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -22,9 +22,7 @@
2222
import javafx.application.Platform;
2323
import javafx.beans.InvalidationListener;
2424
import javafx.beans.Observable;
25-
import javafx.beans.property.BooleanProperty;
2625
import javafx.beans.property.ObjectProperty;
27-
import javafx.beans.property.SimpleBooleanProperty;
2826
import javafx.beans.property.SimpleObjectProperty;
2927
import javafx.beans.property.SimpleStringProperty;
3028
import javafx.beans.property.StringProperty;
@@ -38,8 +36,10 @@
3836
import org.jackhuang.hmcl.game.Version;
3937
import org.jackhuang.hmcl.ui.WeakListenerHolder;
4038
import org.jackhuang.hmcl.util.GUID;
39+
import org.jackhuang.hmcl.util.PortablePath;
4140
import org.jackhuang.hmcl.util.ToStringBuilder;
4241
import org.jackhuang.hmcl.util.javafx.ObservableHelper;
42+
import org.jetbrains.annotations.NotNullByDefault;
4343
import org.jetbrains.annotations.Nullable;
4444

4545
import java.lang.reflect.Type;
@@ -55,6 +55,7 @@
5555
* @author huangyuhui
5656
*/
5757
@JsonAdapter(Profile.Serializer.class)
58+
@NotNullByDefault
5859
public final class Profile implements Observable {
5960
private final WeakListenerHolder listenerHolder = new WeakListenerHolder();
6061
private final HMCLGameRepository repository;
@@ -83,26 +84,30 @@ public StringProperty selectedVersionProperty() {
8384
return selectedVersion;
8485
}
8586

86-
public String getSelectedVersion() {
87+
public @Nullable String getSelectedVersion() {
8788
return selectedVersion.get();
8889
}
8990

90-
public void setSelectedVersion(String selectedVersion) {
91+
public void setSelectedVersion(@Nullable String selectedVersion) {
9192
this.selectedVersion.set(selectedVersion);
9293
}
9394

94-
private final ObjectProperty<Path> gameDir;
95+
/// The game directory path.
96+
private final ObjectProperty<PortablePath> path;
9597

96-
public ObjectProperty<Path> gameDirProperty() {
97-
return gameDir;
98+
/// Returns the game directory path property.
99+
public ObjectProperty<PortablePath> pathProperty() {
100+
return path;
98101
}
99102

100-
public Path getGameDir() {
101-
return gameDir.get();
103+
/// Returns the game directory path.
104+
public PortablePath getPath() {
105+
return path.get();
102106
}
103107

104-
public void setGameDir(Path gameDir) {
105-
this.gameDir.set(gameDir);
108+
/// Sets the game directory path.
109+
public void setPath(PortablePath path) {
110+
this.path.set(Objects.requireNonNull(path));
106111
}
107112

108113
private final SimpleStringProperty name;
@@ -119,38 +124,24 @@ public void setName(String name) {
119124
this.name.set(name);
120125
}
121126

122-
private final BooleanProperty useRelativePath = new SimpleBooleanProperty(this, "useRelativePath", false);
123-
124-
public BooleanProperty useRelativePathProperty() {
125-
return useRelativePath;
126-
}
127-
128-
public boolean isUseRelativePath() {
129-
return useRelativePath.get();
130-
}
131-
132-
public void setUseRelativePath(boolean useRelativePath) {
133-
this.useRelativePath.set(useRelativePath);
134-
}
135-
136127
public Profile(String name, Path initialGameDir) {
137-
this(name, initialGameDir, null, false);
128+
this(name, PortablePath.fromPath(initialGameDir));
138129
}
139130

140-
public Profile(String name, Path initialGameDir, @Nullable String selectedVersion, boolean useRelativePath) {
141-
this(GUID.random(), name, initialGameDir, selectedVersion, useRelativePath);
131+
/// Creates a profile.
132+
public Profile(String name, PortablePath path) {
133+
this(GUID.random(), name, path, null);
142134
}
143135

144136
/// Creates a profile with an explicit stable ID.
145-
Profile(GUID id, String name, Path initialGameDir, @Nullable String selectedVersion, boolean useRelativePath) {
137+
Profile(GUID id, String name, PortablePath path, @Nullable String selectedVersion) {
146138
this.id.set(Objects.requireNonNull(id));
147139
this.name = new SimpleStringProperty(this, "name", name);
148-
gameDir = new SimpleObjectProperty<>(this, "gameDir", initialGameDir);
149-
repository = new HMCLGameRepository(this, initialGameDir);
140+
this.path = new SimpleObjectProperty<>(this, "path", Objects.requireNonNull(path));
141+
repository = new HMCLGameRepository(this, path.toPath());
150142
this.selectedVersion.set(selectedVersion);
151-
this.useRelativePath.set(useRelativePath);
152143

153-
gameDir.addListener((a, b, newValue) -> repository.changeDirectory(newValue));
144+
this.path.addListener((a, b, newValue) -> repository.changeDirectory(newValue.toPath()));
154145
this.selectedVersion.addListener(o -> checkSelectedVersion());
155146
listenerHolder.add(EventBus.EVENT_BUS.channel(RefreshedVersionsEvent.class).registerWeak(event -> checkSelectedVersion(), EventPriority.HIGHEST));
156147

@@ -186,17 +177,15 @@ public DefaultDependencyManager getDependency(DownloadProvider downloadProvider)
186177
@Override
187178
public String toString() {
188179
return new ToStringBuilder(this)
189-
.append("gameDir", getGameDir())
180+
.append("path", getPath())
190181
.append("name", getName())
191-
.append("useRelativePath", isUseRelativePath())
192182
.toString();
193183
}
194184

195185
private void addPropertyChangedListener(InvalidationListener listener) {
196186
id.addListener(listener);
197187
name.addListener(listener);
198-
gameDir.addListener(listener);
199-
useRelativePath.addListener(listener);
188+
path.addListener(listener);
200189
selectedVersion.addListener(listener);
201190
}
202191

@@ -216,39 +205,41 @@ private void invalidate() {
216205
Platform.runLater(observableHelper::invalidate);
217206
}
218207

219-
public record ProfileVersion(Profile profile, String version) {
208+
public record ProfileVersion(Profile profile, @Nullable String version) {
220209
}
221210

222211
public static final class Serializer implements JsonSerializer<Profile>, JsonDeserializer<Profile> {
223212
@Override
224-
public JsonElement serialize(Profile src, Type typeOfSrc, JsonSerializationContext context) {
213+
public JsonElement serialize(@Nullable Profile src, Type typeOfSrc, JsonSerializationContext context) {
225214
if (src == null)
226215
return JsonNull.INSTANCE;
227216

228217
JsonObject jsonObject = new JsonObject();
229218
jsonObject.add("id", context.serialize(src.getId(), GUID.class));
230219
jsonObject.addProperty("name", src.getName());
231-
jsonObject.addProperty("gameDir", src.getGameDir().toString());
232-
jsonObject.addProperty("useRelativePath", src.isUseRelativePath());
220+
jsonObject.add("path", context.serialize(src.getPath(), PortablePath.class));
233221
jsonObject.addProperty("selectedMinecraftVersion", src.getSelectedVersion());
234222

235223
return jsonObject;
236224
}
237225

238226
@Override
239-
public Profile deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws JsonParseException {
227+
public @Nullable Profile deserialize(@Nullable JsonElement json, Type typeOfT, JsonDeserializationContext context) throws JsonParseException {
240228
if (!(json instanceof JsonObject obj)) return null;
241229
GUID id = context.deserialize(obj.get("id"), GUID.class);
242230
if (id == null) {
243231
throw new JsonParseException("Profile ID cannot be null");
244232
}
245-
String gameDir = Optional.ofNullable(obj.get("gameDir")).map(JsonElement::getAsString).orElse("");
233+
PortablePath path = context.deserialize(obj.get("path"), PortablePath.class);
234+
if (path == null) {
235+
String gameDir = Optional.ofNullable(obj.get("gameDir")).map(JsonElement::getAsString).orElse("");
236+
path = PortablePath.of(gameDir);
237+
}
246238

247239
return new Profile(id,
248240
Optional.ofNullable(obj.get("name")).map(JsonElement::getAsString).orElse("Default"),
249-
Path.of(gameDir),
250-
Optional.ofNullable(obj.get("selectedMinecraftVersion")).map(JsonElement::getAsString).orElse(""),
251-
Optional.ofNullable(obj.get("useRelativePath")).map(JsonElement::getAsBoolean).orElse(false));
241+
path,
242+
Optional.ofNullable(obj.get("selectedMinecraftVersion")).map(JsonElement::getAsString).orElse(""));
252243
}
253244

254245
}

HMCL/src/main/java/org/jackhuang/hmcl/setting/Profiles.java

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -25,9 +25,9 @@
2525
import org.jackhuang.hmcl.event.EventBus;
2626
import org.jackhuang.hmcl.event.RefreshedVersionsEvent;
2727
import org.jackhuang.hmcl.util.GUID;
28+
import org.jackhuang.hmcl.util.PortablePath;
2829
import org.jetbrains.annotations.Nullable;
2930

30-
import java.nio.file.Path;
3131
import java.util.ArrayList;
3232
import java.util.HashSet;
3333
import java.util.List;
@@ -108,9 +108,9 @@ private static void checkProfiles() {
108108
ObservableList<Profile> profiles = ConfigHolder.getGameDirectories();
109109
if (profiles.isEmpty()) {
110110
Profile current = new Profile(
111-
Profiles.DEFAULT_PROFILE_ID, Profiles.DEFAULT_PROFILE, Path.of(".minecraft"), null, true);
111+
Profiles.DEFAULT_PROFILE_ID, Profiles.DEFAULT_PROFILE, PortablePath.of(".minecraft"), null);
112112
Profile home = new Profile(
113-
Profiles.HOME_PROFILE_ID, Profiles.HOME_PROFILE, Metadata.MINECRAFT_DIRECTORY, null, false);
113+
Profiles.HOME_PROFILE_ID, Profiles.HOME_PROFILE, PortablePath.fromPath(Metadata.MINECRAFT_DIRECTORY), null);
114114
Platform.runLater(() -> profiles.addAll(current, home));
115115
}
116116
}

HMCL/src/main/java/org/jackhuang/hmcl/ui/profile/ProfileListItem.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ public ProfileListItem(Profile profile) {
3636
setUserData(profile);
3737

3838
title.set(Profiles.getProfileDisplayName(profile));
39-
subtitle.set(profile.getGameDir().toString());
39+
subtitle.set(profile.getPath().toString());
4040
}
4141

4242
@Override

HMCL/src/main/java/org/jackhuang/hmcl/ui/profile/ProfilePage.java

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@
3737
import org.jackhuang.hmcl.ui.FXUtils;
3838
import org.jackhuang.hmcl.ui.construct.*;
3939
import org.jackhuang.hmcl.ui.decorator.DecoratorPage;
40+
import org.jackhuang.hmcl.util.PortablePath;
4041
import org.jackhuang.hmcl.util.StringUtils;
4142
import org.jackhuang.hmcl.util.io.FileUtils;
4243

@@ -65,7 +66,7 @@ public ProfilePage(Profile profile) {
6566

6667
state.set(State.fromTitle(profile == null ? i18n("profile.new") : i18n("profile") + " - " + profileDisplayName));
6768
location = new SimpleStringProperty(this, "location",
68-
Optional.ofNullable(profile).map(Profile::getGameDir).map(FileUtils::getAbsolutePath).orElse(".minecraft"));
69+
Optional.ofNullable(profile).map(Profile::getPath).map(PortablePath::toPath).map(FileUtils::getAbsolutePath).orElse(".minecraft"));
6970

7071
ScrollPane scroll = new ScrollPane();
7172
this.setCenter(scroll);
@@ -115,7 +116,7 @@ protected void eval() {
115116

116117
gameDir.convertToRelativePathProperty().bind(toggleUseRelativePath.selectedProperty());
117118
if (profile != null) {
118-
toggleUseRelativePath.setSelected(profile.isUseRelativePath());
119+
toggleUseRelativePath.setSelected(!profile.getPath().isAbsolute());
119120
}
120121

121122
componentList.getContent().setAll(profileNamePane, gameDir, toggleUseRelativePath);
@@ -180,16 +181,14 @@ public void changed(ObservableValue<? extends String> observable, String oldValu
180181
private void onSave() {
181182
if (profile != null) {
182183
profile.setName(txtProfileName.getText());
183-
profile.setUseRelativePath(toggleUseRelativePath.isSelected());
184184
if (StringUtils.isNotBlank(getLocation())) {
185-
profile.setGameDir(Path.of(getLocation()));
185+
profile.setPath(PortablePath.of(getLocation()));
186186
}
187187
} else {
188188
if (StringUtils.isBlank(getLocation())) {
189189
gameDir.fire();
190190
}
191-
Profile newProfile = new Profile(txtProfileName.getText(), Path.of(getLocation()));
192-
newProfile.setUseRelativePath(toggleUseRelativePath.isSelected());
191+
Profile newProfile = new Profile(txtProfileName.getText(), PortablePath.of(getLocation()));
193192
Profiles.getProfiles().add(newProfile);
194193
}
195194

HMCL/src/test/java/org/jackhuang/hmcl/setting/GameDirectoriesTest.java

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
import com.google.gson.JsonObject;
2121
import com.google.gson.JsonParser;
2222
import org.jackhuang.hmcl.util.GUID;
23+
import org.jackhuang.hmcl.util.PortablePath;
2324
import org.jackhuang.hmcl.util.gson.JsonFileFormat;
2425
import org.jackhuang.hmcl.util.gson.JsonUtils;
2526
import org.jetbrains.annotations.NotNullByDefault;
@@ -65,6 +66,23 @@ public void extractsProfilesFromConfigJson() {
6566
assertEquals(1, gameDirectories.getGameDirectories().size());
6667
assertEquals(id, gameDirectories.getGameDirectories().get(0).getId());
6768
assertEquals("Dev", gameDirectories.getGameDirectories().get(0).getName());
69+
assertEquals(".minecraft", gameDirectories.getGameDirectories().get(0).getPath().getPath());
70+
}
71+
72+
/// Tests that profiles store their directory as a portable path.
73+
@Test
74+
public void storesProfilePath() {
75+
GUID id = GUID.fromString("123e4567-e89b-12d3-a456-426614174000");
76+
Profile profile = new Profile(id, "Dev", PortablePath.of("versions\\Dev"), "1.20.1");
77+
78+
JsonObject serialized = JsonUtils.GSON.toJsonTree(profile, Profile.class).getAsJsonObject();
79+
Profile deserialized = Objects.requireNonNull(JsonUtils.GSON.fromJson(serialized, Profile.class));
80+
81+
assertEquals("versions/Dev", serialized.get("path").getAsString());
82+
assertFalse(serialized.has("gameDir"));
83+
assertFalse(serialized.has("useRelativePath"));
84+
assertEquals("versions/Dev", deserialized.getPath().getPath());
85+
assertFalse(deserialized.getPath().isAbsolute());
6886
}
6987

7088
/// Tests that game directory files do not preserve the workspace-level selected directory.

HMCLCore/src/main/java/org/jackhuang/hmcl/util/PortablePath.java

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,17 @@
1717
*/
1818
package org.jackhuang.hmcl.util;
1919

20+
import com.google.gson.JsonParseException;
21+
import com.google.gson.TypeAdapter;
22+
import com.google.gson.annotations.JsonAdapter;
23+
import com.google.gson.stream.JsonReader;
24+
import com.google.gson.stream.JsonToken;
25+
import com.google.gson.stream.JsonWriter;
26+
import org.jackhuang.hmcl.util.gson.JsonSerializable;
2027
import org.jetbrains.annotations.NotNullByDefault;
28+
import org.jetbrains.annotations.Nullable;
2129

30+
import java.io.IOException;
2231
import java.nio.file.Path;
2332
import java.util.Objects;
2433

@@ -28,6 +37,8 @@
2837
/// provided so their platform-specific separators are preserved.
2938
///
3039
/// @author Glavo
40+
@JsonAdapter(PortablePath.Adapter.class)
41+
@JsonSerializable
3142
@NotNullByDefault
3243
public final class PortablePath {
3344
/// The separator used by relative portable paths.
@@ -106,4 +117,28 @@ public Path toPath() {
106117
public String toString() {
107118
return path;
108119
}
120+
121+
/// Gson adapter that serializes portable paths as strings.
122+
@NotNullByDefault
123+
public static final class Adapter extends TypeAdapter<@Nullable PortablePath> {
124+
/// Writes a portable path as its stored path string, or JSON null when the value is null.
125+
@Override
126+
public void write(JsonWriter out, @Nullable PortablePath value) throws IOException {
127+
out.value(value == null ? null : value.getPath());
128+
}
129+
130+
/// Reads a portable path from a string or JSON null.
131+
@Override
132+
public @Nullable PortablePath read(JsonReader in) throws IOException {
133+
if (in.peek() == JsonToken.NULL) {
134+
in.nextNull();
135+
return null;
136+
}
137+
if (in.peek() != JsonToken.STRING) {
138+
throw new JsonParseException("PortablePath must be a string: " + in.peek());
139+
}
140+
141+
return PortablePath.of(in.nextString());
142+
}
143+
}
109144
}

0 commit comments

Comments
 (0)