Skip to content

Commit 880aab0

Browse files
ToobLacCalboot3gf8jv4dvGlavoCalbootOnceMore
authored
[Feature] 资源包管理 #4980 reforged reforged reforged (#6091)
Co-authored-by: Calboot <calboot39@outlook.com> Co-authored-by: 3gf8jv4dv <3gf8jv4dv@gmail.com> Co-authored-by: Glavo <zjx001202@gmail.com> Co-authored-by: CalbootOnceMore <calboot43@outlook.com> Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com>
1 parent 0b64e89 commit 880aab0

54 files changed

Lines changed: 2451 additions & 919 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

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

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,6 @@
1818
package org.jackhuang.hmcl.game;
1919

2020
import org.jackhuang.hmcl.download.DownloadProvider;
21-
import org.jackhuang.hmcl.mod.LocalModFile;
2221
import org.jackhuang.hmcl.mod.RemoteMod;
2322
import org.jackhuang.hmcl.mod.RemoteModRepository;
2423
import org.jackhuang.hmcl.ui.versions.ModTranslations;
@@ -110,8 +109,8 @@ public Stream<Category> getCategories() throws IOException {
110109
}
111110

112111
@Override
113-
public Optional<RemoteMod.Version> getRemoteVersionByLocalFile(LocalModFile localModFile, Path file) throws IOException {
114-
return getBackedRemoteModRepository().getRemoteVersionByLocalFile(localModFile, file);
112+
public Optional<RemoteMod.Version> getRemoteVersionByLocalFile(Path file) throws IOException {
113+
return getBackedRemoteModRepository().getRemoteVersionByLocalFile(file);
115114
}
116115

117116
@Override

HMCL/src/main/java/org/jackhuang/hmcl/ui/download/DownloadPage.java

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,13 @@
6868
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
6969

7070
public class DownloadPage extends DecoratorAnimatedPage implements DecoratorPage {
71+
public static final org.jackhuang.hmcl.ui.versions.DownloadPage.DownloadCallback FOR_MOD =
72+
(downloadProvider, profile, version, mod, file) -> download(downloadProvider, profile, version, file, "mods");
73+
public static final org.jackhuang.hmcl.ui.versions.DownloadPage.DownloadCallback FOR_RESOURCE_PACK =
74+
(downloadProvider, profile, version, mod, file) -> download(downloadProvider, profile, version, file, "resourcepacks");
75+
public static final org.jackhuang.hmcl.ui.versions.DownloadPage.DownloadCallback FOR_SHADER =
76+
(downloadProvider, profile, version, mod, file) -> download(downloadProvider, profile, version, file, "shaderpacks");
77+
7178
private final ReadOnlyObjectWrapper<DecoratorPage.State> state = new ReadOnlyObjectWrapper<>(DecoratorPage.State.fromTitle(i18n("download"), -1));
7279
private final TabHeader tab;
7380
private final TabHeader.Tab<VersionsPage> newGameTab = new TabHeader.Tab<>("newGameTab");
@@ -99,9 +106,9 @@ public DownloadPage(String uploadVersion) {
99106
page.getActions().add(installLocalModpackButton);
100107
return page;
101108
}));
102-
modTab.setNodeSupplier(loadVersionFor(() -> HMCLLocalizedDownloadListPage.ofMod((downloadProvider, profile, version, mod, file) -> download(downloadProvider, profile, version, file, "mods"), true)));
103-
resourcePackTab.setNodeSupplier(loadVersionFor(() -> HMCLLocalizedDownloadListPage.ofResourcePack((downloadProvider, profile, version, mod, file) -> download(downloadProvider, profile, version, file, "resourcepacks"), true)));
104-
shaderTab.setNodeSupplier(loadVersionFor(() -> HMCLLocalizedDownloadListPage.ofShaderPack((downloadProvider, profile, version, mod, file) -> download(downloadProvider, profile, version, file, "shaderpacks"), true)));
109+
modTab.setNodeSupplier(loadVersionFor(() -> HMCLLocalizedDownloadListPage.ofMod(FOR_MOD, true)));
110+
resourcePackTab.setNodeSupplier(loadVersionFor(() -> HMCLLocalizedDownloadListPage.ofResourcePack(FOR_RESOURCE_PACK, true)));
111+
shaderTab.setNodeSupplier(loadVersionFor(() -> HMCLLocalizedDownloadListPage.ofShaderPack(FOR_SHADER, true)));
105112
worldTab.setNodeSupplier(loadVersionFor(() -> new DownloadListPage(CurseForgeRemoteModRepository.WORLDS)));
106113
tab = new TabHeader(transitionPane, newGameTab, modpackTab, modTab, resourcePackTab, shaderTab, worldTab);
107114

@@ -213,8 +220,9 @@ public void showModpackDownloads() {
213220
tab.select(modpackTab, false);
214221
}
215222

216-
public void showResourcepackDownloads() {
223+
public DownloadListPage showResourcePackDownloads() {
217224
tab.select(resourcePackTab, false);
225+
return resourcePackTab.getNode();
218226
}
219227

220228
public DownloadListPage showModDownloads() {

HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/ModCheckUpdatesTask.java renamed to HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/AddonCheckUpdatesTask.java

Lines changed: 11 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@
1818
package org.jackhuang.hmcl.ui.versions;
1919

2020
import org.jackhuang.hmcl.download.DownloadProvider;
21-
import org.jackhuang.hmcl.mod.LocalModFile;
21+
import org.jackhuang.hmcl.mod.LocalAddonFile;
2222
import org.jackhuang.hmcl.mod.RemoteMod;
2323
import org.jackhuang.hmcl.task.Schedulers;
2424
import org.jackhuang.hmcl.task.Task;
@@ -30,33 +30,33 @@
3030

3131
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
3232

33-
public class ModCheckUpdatesTask extends Task<List<LocalModFile.ModUpdate>> {
33+
public class AddonCheckUpdatesTask<T extends LocalAddonFile> extends Task<List<LocalAddonFile.AddonUpdate>> {
3434
private final DownloadProvider downloadProvider;
35-
private final List<Task<LocalModFile.ModUpdate>> dependents;
35+
private final List<Task<LocalAddonFile.AddonUpdate>> dependents;
3636

37-
public ModCheckUpdatesTask(DownloadProvider downloadProvider, String gameVersion, Collection<LocalModFile> mods) {
37+
public AddonCheckUpdatesTask(DownloadProvider downloadProvider, String gameVersion, Collection<T> addons) {
3838
this.downloadProvider = downloadProvider;
39-
dependents = mods.stream().map(mod ->
39+
dependents = addons.stream().map(addon ->
4040
Task.supplyAsync(Schedulers.io(), () -> {
41-
LocalModFile.ModUpdate candidate = null;
41+
LocalAddonFile.AddonUpdate candidate = null;
4242
for (RemoteMod.Type type : RemoteMod.Type.values()) {
43-
LocalModFile.ModUpdate update = null;
43+
LocalAddonFile.AddonUpdate update = null;
4444
try {
45-
update = mod.checkUpdates(downloadProvider, gameVersion, type.getRemoteModRepository());
45+
update = addon.checkUpdates(downloadProvider, gameVersion, type);
4646
} catch (IOException e) {
47-
LOG.warning(String.format("Cannot check update for mod %s.", mod.getFileName()), e);
47+
LOG.warning(String.format("Cannot check update for addon %s.", addon.getFileName()), e);
4848
}
4949
if (update == null) {
5050
continue;
5151
}
5252

53-
if (candidate == null || candidate.getCandidate().getDatePublished().isBefore(update.getCandidate().getDatePublished())) {
53+
if (candidate == null || candidate.targetVersion().getDatePublished().isBefore(update.targetVersion().getDatePublished())) {
5454
candidate = update;
5555
}
5656
}
5757

5858
return candidate;
59-
}).setName(mod.getFileName()).setSignificance(TaskSignificance.MAJOR).withCounter("update.checking")
59+
}).setName(addon.getFileName()).setSignificance(TaskSignificance.MAJOR).withCounter("update.checking")
6060
).toList();
6161

6262
setStage("update.checking");

HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/ModUpdatesPage.java renamed to HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/AddonUpdatesPage.java

Lines changed: 68 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -29,8 +29,8 @@
2929
import javafx.scene.control.TableView;
3030
import javafx.scene.layout.BorderPane;
3131
import javafx.scene.layout.HBox;
32-
import org.jackhuang.hmcl.mod.LocalModFile;
33-
import org.jackhuang.hmcl.mod.ModManager;
32+
import org.jackhuang.hmcl.mod.LocalAddonFile;
33+
import org.jackhuang.hmcl.mod.LocalAddonManager;
3434
import org.jackhuang.hmcl.mod.RemoteMod;
3535
import org.jackhuang.hmcl.task.FileDownloadTask;
3636
import org.jackhuang.hmcl.task.Schedulers;
@@ -41,10 +41,11 @@
4141
import org.jackhuang.hmcl.ui.construct.MessageDialogPane;
4242
import org.jackhuang.hmcl.ui.construct.PageCloseEvent;
4343
import org.jackhuang.hmcl.ui.decorator.DecoratorPage;
44-
import org.jackhuang.hmcl.util.Pair;
44+
import org.jackhuang.hmcl.util.StringUtils;
4545
import org.jackhuang.hmcl.util.TaskCancellationAction;
4646
import org.jackhuang.hmcl.util.io.CSVTable;
4747

48+
import java.io.IOException;
4849
import java.nio.file.Path;
4950
import java.nio.file.Paths;
5051
import java.time.LocalDateTime;
@@ -56,50 +57,50 @@
5657
import java.util.stream.Collectors;
5758

5859
import static org.jackhuang.hmcl.ui.FXUtils.onEscPressed;
59-
import static org.jackhuang.hmcl.util.Pair.pair;
6060
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
61+
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
6162

62-
public class ModUpdatesPage extends BorderPane implements DecoratorPage {
63+
public class AddonUpdatesPage<F extends LocalAddonFile> extends BorderPane implements DecoratorPage {
6364
private final ReadOnlyObjectWrapper<State> state = new ReadOnlyObjectWrapper<>(DecoratorPage.State.fromTitle(i18n("mods.check_updates")));
6465

65-
private final ModManager modManager;
66-
private final ObservableList<ModUpdateObject> objects;
66+
private final LocalAddonManager<F> localAddonManager;
67+
private final ObservableList<AddonUpdateObject> objects;
6768

6869
@SuppressWarnings("unchecked")
69-
public ModUpdatesPage(ModManager modManager, List<LocalModFile.ModUpdate> updates) {
70-
this.modManager = modManager;
70+
public AddonUpdatesPage(LocalAddonManager<F> localAddonManager, List<LocalAddonFile.AddonUpdate> updates) {
71+
this.localAddonManager = localAddonManager;
7172

7273
getStyleClass().add("gray-background");
7374

74-
TableColumn<ModUpdateObject, Boolean> enabledColumn = new TableColumn<>();
75+
TableColumn<AddonUpdateObject, Boolean> enabledColumn = new TableColumn<>();
7576
var allEnabledBox = new JFXCheckBox();
7677
enabledColumn.setStyle("-fx-alignment: CENTER;");
7778
enabledColumn.setGraphic(allEnabledBox);
7879
enabledColumn.setCellFactory(JFXCheckBoxTableCell.forTableColumn(enabledColumn));
79-
setupCellValueFactory(enabledColumn, ModUpdateObject::enabledProperty);
80+
setupCellValueFactory(enabledColumn, AddonUpdateObject::enabledProperty);
8081
enabledColumn.setEditable(true);
8182
enabledColumn.setMaxWidth(40);
8283
enabledColumn.setMinWidth(40);
8384

84-
TableColumn<ModUpdateObject, String> fileNameColumn = new TableColumn<>(i18n("mods.check_updates.file"));
85+
TableColumn<AddonUpdateObject, String> fileNameColumn = new TableColumn<>(i18n("mods.check_updates.file"));
8586
fileNameColumn.setPrefWidth(200);
86-
setupCellValueFactory(fileNameColumn, ModUpdateObject::fileNameProperty);
87+
setupCellValueFactory(fileNameColumn, AddonUpdateObject::fileNameProperty);
8788

88-
TableColumn<ModUpdateObject, String> currentVersionColumn = new TableColumn<>(i18n("mods.check_updates.current_version"));
89+
TableColumn<AddonUpdateObject, String> currentVersionColumn = new TableColumn<>(i18n("mods.check_updates.current_version"));
8990
currentVersionColumn.setPrefWidth(200);
90-
setupCellValueFactory(currentVersionColumn, ModUpdateObject::currentVersionProperty);
91+
setupCellValueFactory(currentVersionColumn, AddonUpdateObject::currentVersionProperty);
9192

92-
TableColumn<ModUpdateObject, String> targetVersionColumn = new TableColumn<>(i18n("mods.check_updates.target_version"));
93+
TableColumn<AddonUpdateObject, String> targetVersionColumn = new TableColumn<>(i18n("mods.check_updates.target_version"));
9394
targetVersionColumn.setPrefWidth(200);
94-
setupCellValueFactory(targetVersionColumn, ModUpdateObject::targetVersionProperty);
95+
setupCellValueFactory(targetVersionColumn, AddonUpdateObject::targetVersionProperty);
9596

96-
TableColumn<ModUpdateObject, String> sourceColumn = new TableColumn<>(i18n("mods.check_updates.source"));
97-
setupCellValueFactory(sourceColumn, ModUpdateObject::sourceProperty);
97+
TableColumn<AddonUpdateObject, String> sourceColumn = new TableColumn<>(i18n("mods.check_updates.source"));
98+
setupCellValueFactory(sourceColumn, AddonUpdateObject::sourceProperty);
9899

99-
objects = FXCollections.observableList(updates.stream().map(ModUpdateObject::new).collect(Collectors.toList()));
100+
objects = FXCollections.observableList(updates.stream().map(AddonUpdateObject::new).collect(Collectors.toList()));
100101
FXUtils.bindAllEnabled(allEnabledBox.selectedProperty(), objects.stream().map(o -> o.enabled).toArray(BooleanProperty[]::new));
101102

102-
TableView<ModUpdateObject> table = new TableView<>(objects);
103+
TableView<AddonUpdateObject> table = new TableView<>(objects);
103104
table.setEditable(true);
104105
table.getColumns().setAll(enabledColumn, fileNameColumn, currentVersionColumn, targetVersionColumn, sourceColumn);
105106
setMargin(table, new Insets(10, 10, 5, 10));
@@ -114,7 +115,7 @@ public ModUpdatesPage(ModManager modManager, List<LocalModFile.ModUpdate> update
114115
exportListButton.setOnAction(e -> exportList());
115116

116117
JFXButton nextButton = FXUtils.newRaisedButton(i18n("mods.check_updates.confirm"));
117-
nextButton.setOnAction(e -> updateMods());
118+
nextButton.setOnAction(e -> updateFiles());
118119

119120
JFXButton cancelButton = FXUtils.newRaisedButton(i18n("button.cancel"));
120121
cancelButton.setOnAction(e -> fireEvent(new PageCloseEvent()));
@@ -125,23 +126,24 @@ public ModUpdatesPage(ModManager modManager, List<LocalModFile.ModUpdate> update
125126
setBottom(actions);
126127
}
127128

128-
private <T> void setupCellValueFactory(TableColumn<ModUpdateObject, T> column, Function<ModUpdateObject, ObservableValue<T>> mapper) {
129+
private <T> void setupCellValueFactory(TableColumn<AddonUpdateObject, T> column, Function<AddonUpdateObject, ObservableValue<T>> mapper) {
129130
column.setCellValueFactory(param -> mapper.apply(param.getValue()));
130131
}
131132

132-
private void updateMods() {
133-
ModUpdateTask task = new ModUpdateTask(
134-
modManager,
133+
private void updateFiles() {
134+
AddonUpdateTask task = new AddonUpdateTask(
135+
localAddonManager.getDirectory(),
135136
objects.stream()
136-
.filter(o -> o.enabled.get())
137-
.map(object -> pair(object.data.getLocalMod(), object.data.getCandidate()))
138-
.collect(Collectors.toList()));
137+
.filter(AddonUpdateObject::isEnabled)
138+
.map(AddonUpdateObject::getData)
139+
.toList()
140+
);
139141
Controllers.taskDialog(
140142
task.whenComplete(Schedulers.javafx(), exception -> {
141143
fireEvent(new PageCloseEvent());
142-
if (!task.getFailedMods().isEmpty()) {
144+
if (!task.getFailedAddons().isEmpty()) {
143145
Controllers.dialog(i18n("mods.check_updates.failed_download") + "\n" +
144-
task.getFailedMods().stream().map(LocalModFile::getFileName).collect(Collectors.joining("\n")),
146+
task.getFailedAddons().stream().map(LocalAddonFile::getFileName).collect(Collectors.joining("\n")),
145147
i18n("install.failed"),
146148
MessageDialogPane.MessageType.ERROR);
147149
}
@@ -189,22 +191,22 @@ public ReadOnlyObjectWrapper<State> stateProperty() {
189191
return state;
190192
}
191193

192-
private static final class ModUpdateObject {
193-
final LocalModFile.ModUpdate data;
194+
private static final class AddonUpdateObject {
195+
final LocalAddonFile.AddonUpdate data;
194196
final BooleanProperty enabled = new SimpleBooleanProperty();
195197
final StringProperty fileName = new SimpleStringProperty();
196198
final StringProperty currentVersion = new SimpleStringProperty();
197199
final StringProperty targetVersion = new SimpleStringProperty();
198200
final StringProperty source = new SimpleStringProperty();
199201

200-
public ModUpdateObject(LocalModFile.ModUpdate data) {
202+
public AddonUpdateObject(LocalAddonFile.AddonUpdate data) {
201203
this.data = data;
202204

203-
enabled.set(!data.getLocalMod().getModManager().isDisabled(data.getLocalMod().getFile()));
204-
fileName.set(data.getLocalMod().getFileName());
205-
currentVersion.set(data.getCurrentVersion().getVersion());
206-
targetVersion.set(data.getCandidate().getVersion());
207-
switch (data.getCurrentVersion().getSelf().getType()) {
205+
enabled.set(!data.localAddonFile().isDisabled());
206+
fileName.set(data.localAddonFile().getFileName());
207+
currentVersion.set(data.currentVersion().getVersion());
208+
targetVersion.set(data.targetVersion().getVersion());
209+
switch (data.currentVersion().getSelf().getType()) {
208210
case CURSEFORGE:
209211
source.set(i18n("mods.curseforge"));
210212
break;
@@ -213,6 +215,10 @@ public ModUpdateObject(LocalModFile.ModUpdate data) {
213215
}
214216
}
215217

218+
public LocalAddonFile.AddonUpdate getData() {
219+
return data;
220+
}
221+
216222
public boolean isEnabled() {
217223
return enabled.get();
218224
}
@@ -274,30 +280,32 @@ public void setSource(String source) {
274280
}
275281
}
276282

277-
public static class ModUpdateTask extends Task<Void> {
283+
public static class AddonUpdateTask extends Task<Void> {
278284
private final Collection<Task<?>> dependents;
279-
private final List<LocalModFile> failedMods = new ArrayList<>();
285+
private final List<LocalAddonFile> failedAddons = new ArrayList<>();
280286

281-
ModUpdateTask(ModManager modManager, List<Pair<LocalModFile, RemoteMod.Version>> mods) {
287+
AddonUpdateTask(Path addonDirectory, List<LocalAddonFile.AddonUpdate> addons) {
282288
setStage("mods.check_updates.confirm");
283-
getProperties().put("total", mods.size());
289+
getProperties().put("total", addons.size());
284290

285291
this.dependents = new ArrayList<>();
286-
for (Pair<LocalModFile, RemoteMod.Version> mod : mods) {
287-
LocalModFile local = mod.getKey();
288-
RemoteMod.Version remote = mod.getValue();
289-
boolean isDisabled = local.getModManager().isDisabled(local.getFile());
292+
for (LocalAddonFile.AddonUpdate addon : addons) {
293+
LocalAddonFile local = addon.localAddonFile();
294+
RemoteMod.Version remote = addon.targetVersion();
295+
boolean isDisabled = local.isDisabled();
296+
String originalFileName = local.getFile().getFileName().toString();
290297

291298
dependents.add(Task
292299
.runAsync(Schedulers.javafx(), () -> local.setOld(true))
293300
.thenComposeAsync(() -> {
294-
String fileName = remote.getFile().getFilename();
301+
String fileName = addon.useRemoteFileName() ? remote.getFile().getFilename() : originalFileName;
295302
if (isDisabled)
296-
fileName += ModManager.DISABLED_EXTENSION;
303+
fileName = StringUtils.addSuffix(fileName, LocalAddonManager.DISABLED_EXTENSION);
297304

298305
var task = new FileDownloadTask(
299306
remote.getFile().getUrl(),
300-
modManager.getModsDirectory().resolve(fileName));
307+
addonDirectory.resolve(fileName)
308+
);
301309

302310
task.setName(remote.getName());
303311
return task;
@@ -307,16 +315,22 @@ public static class ModUpdateTask extends Task<Void> {
307315
// restore state if failed
308316
local.setOld(false);
309317
if (isDisabled)
310-
local.disable();
311-
failedMods.add(local);
318+
local.markDisabled();
319+
failedAddons.add(local);
320+
} else if (!local.keepOldFiles()) {
321+
try {
322+
local.delete();
323+
} catch (IOException e) {
324+
LOG.warning("Failed to delete outdated addon: " + local.getFile(), e);
325+
}
312326
}
313327
})
314328
.withCounter("mods.check_updates.confirm"));
315329
}
316330
}
317331

318-
public List<LocalModFile> getFailedMods() {
319-
return failedMods;
332+
public List<LocalAddonFile> getFailedAddons() {
333+
return failedAddons;
320334
}
321335

322336
@Override

0 commit comments

Comments
 (0)