Skip to content

Commit b8421a7

Browse files
Fix mod manager update edge cases
1 parent 0c06adf commit b8421a7

6 files changed

Lines changed: 184 additions & 49 deletions

File tree

Natives/LauncherNavigationController.m

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -430,6 +430,7 @@ - (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(N
430430
BOOL hitEnter = task.postInstallHitEnter;
431431
void (^finishInstall)(void) = ^{
432432
[task finalizeModInstall];
433+
[task finalizeModpackMetadata];
433434
[self reloadProfileList];
434435
if (installerPath) {
435436
[self enterModInstallerWithPath:installerPath hitEnterAfterWindowShown:hitEnter];

Natives/MinecraftResourceDownloadTask.h

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
@property NSString *postInstallInstallerPath;
1111
@property BOOL postInstallHitEnter;
1212
@property NSArray<NSDictionary *> *postInstallManualDownloads;
13+
@property NSDictionary *postInstallModpackMetadataPlan;
1314
@property(nonatomic, copy) void(^handleError)(void);
1415

1516
- (NSURLSessionDownloadTask *)createDownloadTask:(NSString *)url size:(NSUInteger)size sha:(NSString *)sha altName:(NSString *)altName toPath:(NSString *)path;
@@ -22,5 +23,6 @@
2223
- (void)downloadModpackFromAPI:(ModpackAPI *)api detail:(NSDictionary *)modDetail atIndex:(NSUInteger)selectedVersion;
2324
- (void)downloadModsWithPlan:(NSDictionary *)plan store:(ModManagerStore *)store;
2425
- (void)finalizeModInstall;
26+
- (void)finalizeModpackMetadata;
2527

2628
@end

Natives/MinecraftResourceDownloadTask.m

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -568,6 +568,47 @@ - (void)finalizeModInstall {
568568
[NSNotificationCenter.defaultCenter postNotificationName:@"ModManagerModsChanged" object:self.postInstallModStore];
569569
}
570570

571+
- (void)finalizeModpackMetadata {
572+
NSDictionary *plan = self.postInstallModpackMetadataPlan;
573+
if (![plan isKindOfClass:NSDictionary.class]) {
574+
return;
575+
}
576+
577+
NSString *destPath = plan[@"destPath"];
578+
NSArray *records = [plan[@"records"] isKindOfClass:NSArray.class] ? plan[@"records"] : @[];
579+
if (![destPath isKindOfClass:NSString.class] || destPath.length == 0 || records.count == 0) {
580+
return;
581+
}
582+
583+
NSString *modsDir = [destPath stringByAppendingPathComponent:@"mods"];
584+
NSMutableDictionary *mods = [NSMutableDictionary new];
585+
for (NSDictionary *record in records) {
586+
if (![record isKindOfClass:NSDictionary.class]) {
587+
continue;
588+
}
589+
NSString *fileName = record[@"fileName"];
590+
if (![fileName isKindOfClass:NSString.class] || fileName.length == 0 ||
591+
[fileName containsString:@"/"] || [fileName containsString:@"\\"] || [fileName containsString:@":"]) {
592+
continue;
593+
}
594+
595+
NSString *path = [modsDir stringByAppendingPathComponent:fileName];
596+
if (![NSFileManager.defaultManager fileExistsAtPath:path]) {
597+
NSLog(@"[ModManager] Skipping modpack metadata for missing mod %@", fileName);
598+
continue;
599+
}
600+
mods[fileName] = record;
601+
}
602+
603+
NSString *metadataPath = [destPath stringByAppendingPathComponent:@"amethyst_mods.json"];
604+
NSError *error = saveJSONToFile(@{@"version": @1, @"mods": mods}, metadataPath);
605+
if (error) {
606+
dispatch_async(dispatch_get_main_queue(), ^{
607+
showDialog(localize(@"Error", nil), [NSString stringWithFormat:@"Modpack installed, but mod metadata could not be saved: %@", error.localizedDescription]);
608+
});
609+
}
610+
}
611+
571612
#pragma mark - Utilities
572613

573614
- (void)prepareForDownload {

Natives/installer/modpack/CurseForgeAPI.m

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -635,6 +635,10 @@ - (void)downloader:(MinecraftResourceDownloadTask *)downloader submitDownloadTas
635635
}
636636
}
637637
downloader.postInstallManualDownloads = manualDownloads.copy;
638+
downloader.postInstallModpackMetadataPlan = @{
639+
@"destPath": destPath,
640+
@"records": modManagerRecords.copy
641+
};
638642

639643
NSMutableOrderedSet<NSString *> *overrideDirs = [NSMutableOrderedSet orderedSet];
640644
NSString *manifestOverrides = manifest[@"overrides"];
@@ -655,7 +659,6 @@ - (void)downloader:(MinecraftResourceDownloadTask *)downloader submitDownloadTas
655659
return;
656660
}
657661
}
658-
[self saveModManagerMetadataRecords:modManagerRecords toPath:destPath];
659662

660663
[NSFileManager.defaultManager removeItemAtPath:packagePath error:nil];
661664

Natives/installer/mods/ModManagerViewController.m

Lines changed: 122 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -89,10 +89,18 @@ - (void)updateBusyPrompt {
8989
}
9090
}
9191

92+
- (void)updateBusyControls {
93+
BOOL busy = self.checkingUpdates || self.checkingDependencies;
94+
self.tableView.userInteractionEnabled = !busy;
95+
self.searchController.searchBar.userInteractionEnabled = !busy;
96+
for (UIBarButtonItem *item in self.navigationItem.rightBarButtonItems) {
97+
item.enabled = !busy;
98+
}
99+
}
100+
92101
- (void)setCheckingUpdates:(BOOL)checkingUpdates {
93102
_checkingUpdates = checkingUpdates;
94103
UIBarButtonItem *updateItem = self.navigationItem.rightBarButtonItems.lastObject;
95-
updateItem.enabled = !checkingUpdates;
96104
if (checkingUpdates) {
97105
UIActivityIndicatorView *indicator = [[UIActivityIndicatorView alloc] initWithActivityIndicatorStyle:UIActivityIndicatorViewStyleMedium];
98106
[indicator startAnimating];
@@ -101,80 +109,148 @@ - (void)setCheckingUpdates:(BOOL)checkingUpdates {
101109
updateItem.customView = nil;
102110
updateItem.image = [UIImage systemImageNamed:@"arrow.triangle.2.circlepath"];
103111
}
112+
[self updateBusyControls];
104113
[self updateBusyPrompt];
105114
}
106115

107116
- (void)setCheckingDependencies:(BOOL)checkingDependencies {
108117
_checkingDependencies = checkingDependencies;
109-
self.tableView.userInteractionEnabled = !checkingDependencies;
118+
[self updateBusyControls];
110119
[self updateBusyPrompt];
111120
}
112121

113122
- (BOOL)modCanCheckForUpdate:(NSDictionary *)mod {
114123
NSString *source = mod[@"source"];
124+
id projectID = mod[@"projectId"];
125+
BOOL hasProjectID = projectID && projectID != NSNull.null && [[projectID description] length] > 0;
115126
return [source isKindOfClass:NSString.class] &&
116127
![source isEqualToString:@"manual"] &&
117-
mod[@"projectId"] &&
128+
hasProjectID &&
118129
![mod[@"missing"] boolValue];
119130
}
120131

132+
- (NSArray<NSDictionary *> *)modsNeedingUpdateMetadataRefreshFromMods:(NSArray<NSDictionary *> *)mods {
133+
NSMutableArray *result = [NSMutableArray new];
134+
for (NSDictionary *mod in mods) {
135+
NSString *source = mod[@"source"];
136+
id projectID = mod[@"projectId"];
137+
NSString *sha = [mod[@"sha1"] isKindOfClass:NSString.class] ? mod[@"sha1"] : nil;
138+
NSString *path = [mod[@"path"] isKindOfClass:NSString.class] ? mod[@"path"] : nil;
139+
if ([source isEqualToString:@"modrinth"] &&
140+
![mod[@"missing"] boolValue] &&
141+
(!projectID || projectID == NSNull.null || [[projectID description] length] == 0) &&
142+
(sha.length > 0 || path.length > 0)) {
143+
[result addObject:mod];
144+
}
145+
}
146+
return result;
147+
}
148+
149+
- (NSString *)updateIdentityForMod:(NSDictionary *)mod {
150+
NSString *source = mod[@"source"];
151+
id projectID = mod[@"projectId"];
152+
if ([source isKindOfClass:NSString.class] && projectID && projectID != NSNull.null && [[projectID description] length] > 0) {
153+
return [NSString stringWithFormat:@"%@:%@", source, [[projectID description] lowercaseString]];
154+
}
155+
156+
NSString *fileName = mod[@"fileName"];
157+
if ([fileName isKindOfClass:NSString.class] && fileName.length > 0) {
158+
return [@"file:" stringByAppendingString:fileName.lowercaseString];
159+
}
160+
return nil;
161+
}
162+
163+
- (NSArray<NSMutableDictionary *> *)modsByMergingUpdateResults:(NSDictionary<NSString *, NSDictionary *> *)updatesByIdentity {
164+
NSArray<NSMutableDictionary *> *freshMods = [self.store installedModsMatchingQuery:self.searchController.searchBar.text ?: @""];
165+
NSMutableArray<NSMutableDictionary *> *mergedMods = [NSMutableArray arrayWithCapacity:freshMods.count];
166+
for (NSDictionary *mod in freshMods) {
167+
NSMutableDictionary *copy = mod.mutableCopy;
168+
NSString *identity = [self updateIdentityForMod:mod];
169+
NSDictionary *update = identity ? updatesByIdentity[identity] : nil;
170+
if (update) {
171+
copy[@"availableUpdate"] = update;
172+
} else {
173+
[copy removeObjectForKey:@"availableUpdate"];
174+
}
175+
[mergedMods addObject:copy];
176+
}
177+
return mergedMods;
178+
}
179+
121180
- (void)actionCheckUpdates {
122181
if (self.checkingUpdates) {
123182
return;
124183
}
125-
NSArray *mods = [self.store installedMods];
126-
NSMutableArray *updatedMods = [NSMutableArray arrayWithCapacity:mods.count];
127-
NSMutableArray<NSNumber *> *updateIndexes = [NSMutableArray new];
128-
for (NSUInteger i = 0; i < mods.count; i++) {
129-
NSDictionary *mod = mods[i];
130-
[updatedMods addObject:mod.mutableCopy];
131-
if ([self modCanCheckForUpdate:mod]) {
132-
[updateIndexes addObject:@(i)];
133-
}
134-
}
135184
self.updateCheckCompleted = 0;
136-
self.updateCheckTotal = updateIndexes.count;
185+
self.updateCheckTotal = 0;
137186
self.checkingUpdates = YES;
138-
if (updateIndexes.count == 0) {
139-
self.checkingUpdates = NO;
140-
self.mods = updatedMods;
141-
[self.tableView reloadData];
142-
return;
143-
}
144187

145188
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
146-
dispatch_group_t group = dispatch_group_create();
147-
dispatch_semaphore_t limit = dispatch_semaphore_create(4);
148189
NSDictionary *profileInfo = self.store.profileInfo;
149-
for (NSNumber *indexNumber in updateIndexes) {
150-
dispatch_group_async(group, queue, ^{
151-
dispatch_semaphore_wait(limit, DISPATCH_TIME_FOREVER);
152-
NSUInteger index = indexNumber.unsignedIntegerValue;
153-
NSDictionary *mod = mods[index];
190+
dispatch_async(queue, ^{
191+
NSArray *mods = [self.store installedMods];
192+
NSArray *refreshCandidates = [self modsNeedingUpdateMetadataRefreshFromMods:mods];
193+
if (refreshCandidates.count > 0) {
154194
ModManagerAPI *api = [ModManagerAPI new];
155-
NSError *error = nil;
156-
NSDictionary *update = [api latestVersionForInstalledMod:mod profileInfo:profileInfo error:&error];
157-
NSMutableDictionary *copy = mod.mutableCopy;
158-
if (update) {
159-
copy[@"availableUpdate"] = update;
160-
} else {
161-
[copy removeObjectForKey:@"availableUpdate"];
195+
NSArray *records = [api refreshedMetadataForInstalledMods:refreshCandidates profileInfo:profileInfo];
196+
if (records.count > 0) {
197+
NSError *saveError = [self.store saveMetadataRecords:records replacingFileNames:@[] replacements:@[]];
198+
if (saveError) {
199+
NSLog(@"[ModManager] Failed to refresh update metadata: %@", saveError.localizedDescription);
200+
} else {
201+
mods = [self.store installedMods];
202+
}
162203
}
163-
@synchronized (updatedMods) {
164-
updatedMods[index] = copy;
204+
}
205+
206+
NSMutableArray<NSDictionary *> *checkMods = [NSMutableArray new];
207+
for (NSDictionary *mod in mods) {
208+
if ([self modCanCheckForUpdate:mod]) {
209+
[checkMods addObject:mod];
210+
}
211+
}
212+
213+
dispatch_async(dispatch_get_main_queue(), ^{
214+
self.updateCheckTotal = checkMods.count;
215+
[self updateBusyPrompt];
216+
if (checkMods.count == 0) {
217+
self.mods = [self modsByMergingUpdateResults:@{}];
218+
[self.tableView reloadData];
219+
self.checkingUpdates = NO;
165220
}
166-
dispatch_async(dispatch_get_main_queue(), ^{
167-
self.updateCheckCompleted += 1;
168-
[self updateBusyPrompt];
169-
});
170-
dispatch_semaphore_signal(limit);
171221
});
172-
}
222+
if (checkMods.count == 0) {
223+
return;
224+
}
225+
226+
NSMutableDictionary<NSString *, NSDictionary *> *updatesByIdentity = [NSMutableDictionary new];
227+
dispatch_group_t group = dispatch_group_create();
228+
dispatch_semaphore_t limit = dispatch_semaphore_create(4);
229+
for (NSDictionary *mod in checkMods) {
230+
dispatch_group_async(group, queue, ^{
231+
dispatch_semaphore_wait(limit, DISPATCH_TIME_FOREVER);
232+
ModManagerAPI *api = [ModManagerAPI new];
233+
NSError *error = nil;
234+
NSDictionary *update = [api latestVersionForInstalledMod:mod profileInfo:profileInfo error:&error];
235+
NSString *identity = [self updateIdentityForMod:mod];
236+
if (update && identity.length > 0) {
237+
@synchronized (updatesByIdentity) {
238+
updatesByIdentity[identity] = update;
239+
}
240+
}
241+
dispatch_async(dispatch_get_main_queue(), ^{
242+
self.updateCheckCompleted += 1;
243+
[self updateBusyPrompt];
244+
});
245+
dispatch_semaphore_signal(limit);
246+
});
247+
}
173248

174-
dispatch_group_notify(group, dispatch_get_main_queue(), ^{
175-
self.checkingUpdates = NO;
176-
self.mods = updatedMods;
177-
[self.tableView reloadData];
249+
dispatch_group_notify(group, dispatch_get_main_queue(), ^{
250+
self.mods = [self modsByMergingUpdateResults:updatesByIdentity];
251+
[self.tableView reloadData];
252+
self.checkingUpdates = NO;
253+
});
178254
});
179255
}
180256

Natives/installer/mods/ModSearchViewController.m

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -137,8 +137,20 @@ - (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(N
137137
- (void)showVersionMenuForProject:(NSDictionary *)project versions:(NSArray<NSDictionary *> *)versions atIndexPath:(NSIndexPath *)indexPath {
138138
UITableViewCell *cell = [self.tableView cellForRowAtIndexPath:indexPath];
139139
UIAlertController *sheet = [UIAlertController alertControllerWithTitle:project[@"title"] message:@"Select a compatible version to install." preferredStyle:UIAlertControllerStyleActionSheet];
140-
sheet.popoverPresentationController.sourceView = cell;
141-
sheet.popoverPresentationController.sourceRect = cell.bounds;
140+
UIPopoverPresentationController *popover = sheet.popoverPresentationController;
141+
if (cell) {
142+
popover.sourceView = cell;
143+
popover.sourceRect = cell.bounds;
144+
} else {
145+
CGRect sourceRect = CGRectMake(CGRectGetMidX(self.view.bounds), CGRectGetMidY(self.view.bounds), 1, 1);
146+
if (indexPath.section < self.tableView.numberOfSections &&
147+
indexPath.row < [self.tableView numberOfRowsInSection:indexPath.section]) {
148+
CGRect rowRect = [self.tableView rectForRowAtIndexPath:indexPath];
149+
sourceRect = [self.view convertRect:rowRect fromView:self.tableView];
150+
}
151+
popover.sourceView = self.view;
152+
popover.sourceRect = sourceRect;
153+
}
142154

143155
NSUInteger count = MIN(versions.count, 12);
144156
for (NSUInteger i = 0; i < count; i++) {

0 commit comments

Comments
 (0)