@@ -4,6 +4,7 @@ import 'dart:io';
44import 'package:crypto/crypto.dart' ;
55import 'package:path/path.dart' as p;
66
7+ import '../../utils/async_work_pool.dart' ;
78import 'addon_catalog.dart' ;
89import 'addon_resolver.dart' ;
910
@@ -186,68 +187,103 @@ final class AddonInstaller {
186187 stage = Directory (
187188 instancePath,
188189 ).createTempSync ('.multiplexor-addons-stage-' );
189- final Map <String , InstalledAddon > next = < String , InstalledAddon > {};
190- final Map <String , File > downloads = < String , File > {};
191- for (final String id in selected) {
190+ final List <String > refreshIds = selected
191+ .where (refresh.contains)
192+ .toList (growable: false );
193+ for (final String id in refreshIds) {
192194 final AddonDefinition definition = catalog.entries[id]! ;
193- final InstalledAddon ? old = previous.entries[id];
194- if (! refresh.contains (id) &&
195- old != null &&
196- old.json['minecraft' ] == minecraft &&
197- old.json['serverType' ] == serverType &&
198- File (p.join (instancePath, old.file)).existsSync ()) {
199- next[id] = old;
200- continue ;
201- }
202195 report (
203196 '[INFO] Resolving ${definition .name } for $serverType Minecraft $minecraft ' ,
204197 );
205- final ResolvedAddon resolved = await resolver.resolve (
206- definition,
207- serverType,
208- minecraft,
209- );
210- for (final String project in resolved.requiredProjects) {
211- final bool included = next.values.any (
212- (InstalledAddon dependency) =>
213- dependency.json['projectId' ] == project,
198+ }
199+ final List <ResolvedAddon > resolutions =
200+ await boundedMap <String , ResolvedAddon >(
201+ refreshIds,
202+ (String id) =>
203+ resolver.resolve (catalog.entries[id]! , serverType, minecraft),
214204 );
215- if (! included) {
205+ final Map <String , ResolvedAddon > resolvedById = < String , ResolvedAddon > {
206+ for (int index = 0 ; index < refreshIds.length; index++ )
207+ refreshIds[index]: resolutions[index],
208+ };
209+
210+ // Resolve independently, then validate in dependency order before any
211+ // downloads. Completion order cannot change which dependencies qualify.
212+ final Map <String , Set <String ?>> projectVersions =
213+ < String , Set <String ?>> {};
214+ for (final String id in selected) {
215+ final ResolvedAddon ? resolved = resolvedById[id];
216+ for (final String project
217+ in resolved? .requiredProjects ?? const < String > []) {
218+ final Set <String ?>? versions = projectVersions[project];
219+ final String name = catalog.entries[id]! .name;
220+ if (versions == null ) {
216221 throw StateError (
217- '${ definition . name } requires Modrinth project $project . Add it to the catalog and dependencies.' ,
222+ '$name requires Modrinth project $project . Add it to the catalog and dependencies.' ,
218223 );
219224 }
220- final String ? requiredVersion = resolved.requiredVersions[project];
221- if (requiredVersion != null &&
222- ! next.values.any (
223- (InstalledAddon dependency) =>
224- dependency.json['projectId' ] == project &&
225- dependency.json['versionId' ] == requiredVersion,
226- )) {
225+ final String ? requiredVersion = resolved! .requiredVersions[project];
226+ if (requiredVersion != null && ! versions.contains (requiredVersion)) {
227227 throw StateError (
228- '${ definition . name } requires Modrinth project $project version $requiredVersion . Set that dependency source\' s versionId to $requiredVersion .' ,
228+ '$name requires Modrinth project $project version $requiredVersion . Set that dependency source\' s versionId to $requiredVersion .' ,
229229 );
230230 }
231231 }
232- final File download = File (p.join (stage.path, '$id .jar' ));
233- await resolver.download (resolved, download);
234- next[id] = InstalledAddon (< String , Object ? > {
235- 'id' : id,
236- 'file' : definition.file,
237- 'sha256' : _hash (download),
238- 'version' : resolved.version,
239- 'source' : resolved.location,
240- if (resolved.projectId != null ) 'projectId' : resolved.projectId,
241- if (resolved.versionId != null ) 'versionId' : resolved.versionId,
242- 'minecraft' : minecraft,
243- 'serverType' : serverType,
244- 'filePrefixes' : definition.filePrefixes,
245- });
246- downloads[id] = download;
247- report ('[INFO] Ready: ${definition .name } ${resolved .version }' );
232+ final String ? projectId = resolved == null
233+ ? previous.entries[id]! .json['projectId' ] as String ?
234+ : resolved.projectId;
235+ final String ? versionId = resolved == null
236+ ? previous.entries[id]! .json['versionId' ] as String ?
237+ : resolved.versionId;
238+ if (projectId != null ) {
239+ projectVersions
240+ .putIfAbsent (projectId, () => < String ? > {})
241+ .add (versionId);
242+ }
248243 }
249- await beforeCommit ();
244+
250245 final Directory transaction = stage;
246+ final List <_PreparedAddon > prepared =
247+ await boundedMap <String , _PreparedAddon >(refreshIds, (
248+ String id,
249+ ) async {
250+ final AddonDefinition definition = catalog.entries[id]! ;
251+ final ResolvedAddon resolved = resolvedById[id]! ;
252+ final File download = File (p.join (transaction.path, '$id .jar' ));
253+ await resolver.download (resolved, download);
254+ final InstalledAddon entry = InstalledAddon (< String , Object ? > {
255+ 'id' : id,
256+ 'file' : definition.file,
257+ 'sha256' : (await sha256.bind (download.openRead ()).first)
258+ .toString (),
259+ 'version' : resolved.version,
260+ 'source' : resolved.location,
261+ if (resolved.projectId != null ) 'projectId' : resolved.projectId,
262+ if (resolved.versionId != null ) 'versionId' : resolved.versionId,
263+ 'minecraft' : minecraft,
264+ 'serverType' : serverType,
265+ 'filePrefixes' : definition.filePrefixes,
266+ });
267+ return _PreparedAddon (entry, download);
268+ });
269+ final Map <String , InstalledAddon > preparedEntries =
270+ < String , InstalledAddon > {
271+ for (final _PreparedAddon item in prepared)
272+ item.entry.id: item.entry,
273+ };
274+ final Map <String , InstalledAddon > next = < String , InstalledAddon > {
275+ for (final String id in selected)
276+ id: preparedEntries[id] ?? previous.entries[id]! ,
277+ };
278+ final Map <String , File > downloads = < String , File > {
279+ for (final _PreparedAddon item in prepared) item.entry.id: item.file,
280+ };
281+ for (final _PreparedAddon item in prepared) {
282+ report (
283+ '[INFO] Ready: ${catalog .entries [item .entry .id ]!.name } ${item .entry .json ['version' ]}' ,
284+ );
285+ }
286+ await beforeCommit ();
251287 commit (() {
252288 _regularDirectory (instancePath);
253289 _checkExisting (previous);
@@ -383,6 +419,13 @@ final class AddonInstaller {
383419 }
384420}
385421
422+ final class _PreparedAddon {
423+ const _PreparedAddon (this .entry, this .file);
424+
425+ final InstalledAddon entry;
426+ final File file;
427+ }
428+
386429String _hash (File file) => sha256.convert (file.readAsBytesSync ()).toString ();
387430
388431void _regularDirectory (String path) {
0 commit comments