665 lines
		
	
	
		
			20 KiB
		
	
	
	
		
			Dart
		
	
	
	
	
	
			
		
		
	
	
			665 lines
		
	
	
		
			20 KiB
		
	
	
	
		
			Dart
		
	
	
	
	
	
import 'dart:io';
 | 
						|
 | 
						|
import 'package:collection/collection.dart';
 | 
						|
import 'package:rimworld_modman/logger.dart';
 | 
						|
import 'package:rimworld_modman/mod.dart';
 | 
						|
import 'package:xml/xml.dart';
 | 
						|
 | 
						|
class LoadOrder {
 | 
						|
  List<Mod> order = [];
 | 
						|
  final List<String> errors = [];
 | 
						|
 | 
						|
  List<String> get loadOrder {
 | 
						|
    return order.map((mod) => mod.id).toList();
 | 
						|
  }
 | 
						|
 | 
						|
  LoadOrder([List<Mod>? order]) {
 | 
						|
    this.order = order ?? [];
 | 
						|
  }
 | 
						|
 | 
						|
  bool get hasErrors => errors.isNotEmpty;
 | 
						|
}
 | 
						|
 | 
						|
class ModList {
 | 
						|
  String configPath = '';
 | 
						|
  String modsPath = '';
 | 
						|
  // O(1) lookup
 | 
						|
  Map<String, Mod> activeMods = {};
 | 
						|
  Map<String, Mod> mods = {};
 | 
						|
 | 
						|
  ModList({this.configPath = '', this.modsPath = ''});
 | 
						|
 | 
						|
  ModList copyWith({
 | 
						|
    String? configPath,
 | 
						|
    String? modsPath,
 | 
						|
    Map<String, Mod>? mods,
 | 
						|
    Map<String, bool>? activeMods,
 | 
						|
  }) {
 | 
						|
    final newModlist = ModList(
 | 
						|
      configPath: configPath ?? this.configPath,
 | 
						|
      modsPath: modsPath ?? this.modsPath,
 | 
						|
    );
 | 
						|
    newModlist.mods = Map.from(mods ?? this.mods);
 | 
						|
    newModlist.activeMods = Map.from(activeMods ?? this.activeMods);
 | 
						|
    return newModlist;
 | 
						|
  }
 | 
						|
 | 
						|
  Stream<Mod> loadAvailable() async* {
 | 
						|
    final logger = Logger.instance;
 | 
						|
    final stopwatch = Stopwatch()..start();
 | 
						|
 | 
						|
    final directory = Directory(modsPath);
 | 
						|
 | 
						|
    if (!directory.existsSync()) {
 | 
						|
      logger.error('Error: Mods root directory does not exist: $modsPath');
 | 
						|
      return;
 | 
						|
    }
 | 
						|
 | 
						|
    final List<FileSystemEntity> entities = directory.listSync();
 | 
						|
    // TODO: Count only the latest version of each mod and not all versions
 | 
						|
    final List<String> modDirectories =
 | 
						|
        entities.whereType<Directory>().map((dir) => dir.path).toList();
 | 
						|
 | 
						|
    logger.info(
 | 
						|
      'Found ${modDirectories.length} mod directories (${stopwatch.elapsedMilliseconds}ms)',
 | 
						|
    );
 | 
						|
 | 
						|
    for (final modDir in modDirectories) {
 | 
						|
      try {
 | 
						|
        final modStart = stopwatch.elapsedMilliseconds;
 | 
						|
 | 
						|
        // Check if this directory contains a valid mod
 | 
						|
        final aboutFile = File('$modDir/About/About.xml');
 | 
						|
        if (!aboutFile.existsSync()) {
 | 
						|
          logger.warning('No About.xml found in directory: $modDir');
 | 
						|
          continue;
 | 
						|
        }
 | 
						|
 | 
						|
        final mod = Mod.fromDirectory(modDir);
 | 
						|
        logger.info('Loaded mod from directory: ${mod.name} (ID: ${mod.id})');
 | 
						|
 | 
						|
        if (mods.containsKey(mod.id)) {
 | 
						|
          logger.warning(
 | 
						|
            'Mod $mod.id already exists in mods list, overwriting',
 | 
						|
          );
 | 
						|
          final existingMod = mods[mod.id]!;
 | 
						|
          mods[mod.id] = Mod(
 | 
						|
            name: mod.name,
 | 
						|
            id: mod.id,
 | 
						|
            path: mod.path,
 | 
						|
            versions: mod.versions,
 | 
						|
            description: mod.description,
 | 
						|
            dependencies: mod.dependencies,
 | 
						|
            loadAfter: mod.loadAfter,
 | 
						|
            loadBefore: mod.loadBefore,
 | 
						|
            incompatibilities: mod.incompatibilities,
 | 
						|
            size: mod.size,
 | 
						|
            enabled: existingMod.enabled,
 | 
						|
            isBaseGame: existingMod.isBaseGame,
 | 
						|
            isExpansion: existingMod.isExpansion,
 | 
						|
          );
 | 
						|
          logger.info('Updated existing mod: ${mod.name} (ID: ${mod.id})');
 | 
						|
        } else {
 | 
						|
          mods[mod.id] = mod;
 | 
						|
          logger.info('Added new mod: ${mod.name} (ID: ${mod.id})');
 | 
						|
        }
 | 
						|
 | 
						|
        final modTime = stopwatch.elapsedMilliseconds - modStart;
 | 
						|
        logger.info(
 | 
						|
          'Loaded mod from directory: ${mod.name} (ID: ${mod.id}) in $modTime ms',
 | 
						|
        );
 | 
						|
        yield mod;
 | 
						|
      } catch (e) {
 | 
						|
        logger.error('Error loading mod from directory: $modDir');
 | 
						|
        logger.error('Error: $e');
 | 
						|
      }
 | 
						|
    }
 | 
						|
  }
 | 
						|
 | 
						|
  Stream<Mod> loadActive() async* {
 | 
						|
    final logger = Logger.instance;
 | 
						|
    final file = File(configPath);
 | 
						|
    logger.info('Loading configuration from: $configPath');
 | 
						|
 | 
						|
    try {
 | 
						|
      final xmlString = file.readAsStringSync();
 | 
						|
      logger.info('XML content read successfully.');
 | 
						|
 | 
						|
      final xmlDocument = XmlDocument.parse(xmlString);
 | 
						|
      logger.info('XML document parsed successfully.');
 | 
						|
 | 
						|
      final modConfigData = xmlDocument.findElements("ModsConfigData").first;
 | 
						|
      logger.info('Found ModsConfigData element.');
 | 
						|
 | 
						|
      final modsElement = modConfigData.findElements("activeMods").first;
 | 
						|
      logger.info('Found activeMods element.');
 | 
						|
 | 
						|
      final modElements = modsElement.findElements("li");
 | 
						|
      logger.info('Found ${modElements.length} active mods.');
 | 
						|
 | 
						|
      // Get the list of known expansions
 | 
						|
      final knownExpansionsElement =
 | 
						|
          modConfigData.findElements("knownExpansions").firstOrNull;
 | 
						|
      final knownExpansionIds =
 | 
						|
          knownExpansionsElement != null
 | 
						|
              ? knownExpansionsElement
 | 
						|
                  .findElements("li")
 | 
						|
                  .map((e) => e.innerText.toLowerCase())
 | 
						|
                  .toList()
 | 
						|
              : <String>[];
 | 
						|
 | 
						|
      logger.info('Found ${knownExpansionIds.length} known expansions.');
 | 
						|
 | 
						|
      // Clear and recreate the mods list
 | 
						|
      for (final modElement in modElements) {
 | 
						|
        final modId = modElement.innerText.toLowerCase();
 | 
						|
 | 
						|
        // Check if this is a special Ludeon mod
 | 
						|
        final isBaseGame = modId == 'ludeon.rimworld';
 | 
						|
        final isExpansion =
 | 
						|
            !isBaseGame &&
 | 
						|
            modId.startsWith('ludeon.rimworld.') &&
 | 
						|
            knownExpansionIds.contains(modId);
 | 
						|
 | 
						|
        final existingMod = mods[modId];
 | 
						|
        final mod = Mod(
 | 
						|
          name:
 | 
						|
              existingMod?.name ??
 | 
						|
              (isBaseGame
 | 
						|
                  ? "RimWorld"
 | 
						|
                  : isExpansion
 | 
						|
                  ? "RimWorld ${_expansionNameFromId(modId)}"
 | 
						|
                  : modId),
 | 
						|
          id: existingMod?.id ?? modId,
 | 
						|
          path: existingMod?.path ?? '',
 | 
						|
          versions: existingMod?.versions ?? [],
 | 
						|
          description:
 | 
						|
              existingMod?.description ??
 | 
						|
              (isBaseGame
 | 
						|
                  ? "RimWorld base game"
 | 
						|
                  : isExpansion
 | 
						|
                  ? "RimWorld expansion"
 | 
						|
                  : ""),
 | 
						|
          dependencies: existingMod?.dependencies ?? [],
 | 
						|
          loadAfter:
 | 
						|
              existingMod?.loadAfter ??
 | 
						|
              (isExpansion ? ['ludeon.rimworld'] : []),
 | 
						|
          loadBefore: existingMod?.loadBefore ?? [],
 | 
						|
          incompatibilities: existingMod?.incompatibilities ?? [],
 | 
						|
          enabled: existingMod?.enabled ?? false,
 | 
						|
          size: existingMod?.size ?? 0,
 | 
						|
          isBaseGame: isBaseGame,
 | 
						|
          isExpansion: isExpansion,
 | 
						|
        );
 | 
						|
        if (mods.containsKey(modId)) {
 | 
						|
          logger.warning('Mod $modId already exists in mods list, overwriting');
 | 
						|
        }
 | 
						|
        mods[modId] = mod;
 | 
						|
        setEnabled(modId, true);
 | 
						|
        yield mod;
 | 
						|
      }
 | 
						|
 | 
						|
      logger.info('Loaded ${modElements.length} mods from config file.');
 | 
						|
    } catch (e) {
 | 
						|
      logger.error('Error loading configuration file: $e');
 | 
						|
      throw Exception('Failed to load config file: $e');
 | 
						|
    }
 | 
						|
  }
 | 
						|
 | 
						|
  void saveToConfig(LoadOrder loadOrder) {
 | 
						|
    final file = File(configPath);
 | 
						|
    final logger = Logger.instance;
 | 
						|
 | 
						|
    try {
 | 
						|
      // Create XML builder
 | 
						|
      final builder = XmlBuilder();
 | 
						|
 | 
						|
      // Add XML declaration
 | 
						|
      builder.declaration(encoding: 'utf-8');
 | 
						|
 | 
						|
      // Add root element
 | 
						|
      builder.element(
 | 
						|
        'ModsConfigData',
 | 
						|
        nest: () {
 | 
						|
          // Add version element
 | 
						|
          builder.element('version', nest: '1.5.4297 rev994');
 | 
						|
 | 
						|
          // Add active mods element
 | 
						|
          builder.element(
 | 
						|
            'activeMods',
 | 
						|
            nest: () {
 | 
						|
              // Add each mod as a list item
 | 
						|
              for (final mod in loadOrder.order) {
 | 
						|
                builder.element('li', nest: mod.id);
 | 
						|
                logger.info('Adding mod to config: ${mod.name} (${mod.id})');
 | 
						|
              }
 | 
						|
            },
 | 
						|
          );
 | 
						|
 | 
						|
          // Add known expansions element
 | 
						|
          final expansions = mods.values.where((m) => m.isExpansion).toList();
 | 
						|
          if (expansions.isNotEmpty) {
 | 
						|
            builder.element(
 | 
						|
              'knownExpansions',
 | 
						|
              nest: () {
 | 
						|
                for (final mod in expansions) {
 | 
						|
                  builder.element('li', nest: mod.id);
 | 
						|
                  logger.info(
 | 
						|
                    'Adding expansion to config: ${mod.name} (${mod.id})',
 | 
						|
                  );
 | 
						|
                }
 | 
						|
              },
 | 
						|
            );
 | 
						|
          }
 | 
						|
        },
 | 
						|
      );
 | 
						|
 | 
						|
      // Build the XML document
 | 
						|
      final xmlDocument = builder.buildDocument();
 | 
						|
 | 
						|
      // Convert to string with 2-space indentation
 | 
						|
      final prettyXml = xmlDocument.toXmlString(
 | 
						|
        pretty: true, 
 | 
						|
        indent: '  ', // 2 spaces
 | 
						|
        newLine: '\n',
 | 
						|
      );
 | 
						|
 | 
						|
      // Write the formatted XML document to file
 | 
						|
      file.writeAsStringSync(prettyXml);
 | 
						|
      logger.info('Successfully saved mod configuration to: $configPath');
 | 
						|
    } catch (e) {
 | 
						|
      logger.error('Error saving configuration file: $e');
 | 
						|
      throw Exception('Failed to save config file: $e');
 | 
						|
    }
 | 
						|
  }
 | 
						|
 | 
						|
  void setEnabled(String modId, bool enabled) {
 | 
						|
    if (mods.containsKey(modId)) {
 | 
						|
      final mod = mods[modId]!;
 | 
						|
      mod.enabled = enabled;
 | 
						|
      if (enabled) {
 | 
						|
        activeMods[modId] = mod;
 | 
						|
      } else {
 | 
						|
        activeMods.remove(modId);
 | 
						|
      }
 | 
						|
    }
 | 
						|
  }
 | 
						|
 | 
						|
  void enableAll() {
 | 
						|
    for (final mod in mods.values) {
 | 
						|
      setEnabled(mod.id, true);
 | 
						|
    }
 | 
						|
  }
 | 
						|
 | 
						|
  void disableAll() {
 | 
						|
    for (final mod in mods.values) {
 | 
						|
      setEnabled(mod.id, false);
 | 
						|
    }
 | 
						|
  }
 | 
						|
 | 
						|
  void enableMods(List<String> modIds) {
 | 
						|
    for (final modId in modIds) {
 | 
						|
      setEnabled(modId, true);
 | 
						|
    }
 | 
						|
  }
 | 
						|
 | 
						|
  void disableMods(List<String> modIds) {
 | 
						|
    for (final modId in modIds) {
 | 
						|
      setEnabled(modId, false);
 | 
						|
    }
 | 
						|
  }
 | 
						|
 | 
						|
  LoadOrder generateLoadOrder([LoadOrder? loadOrder]) {
 | 
						|
    loadOrder ??= LoadOrder();
 | 
						|
    final logger = Logger.instance;
 | 
						|
    logger.info('Generating load order...');
 | 
						|
 | 
						|
    for (final mod in activeMods.values) {
 | 
						|
      logger.info('Checking mod: ${mod.id}');
 | 
						|
      logger.info('Mod details: ${mod.toString()}');
 | 
						|
      for (final incomp in mod.incompatibilities) {
 | 
						|
        if (activeMods.containsKey(incomp)) {
 | 
						|
          loadOrder.errors.add(
 | 
						|
            'Incompatibility detected: ${mod.id} is incompatible with $incomp',
 | 
						|
          );
 | 
						|
          logger.warning(
 | 
						|
            'Incompatibility detected: ${mod.id} is incompatible with $incomp',
 | 
						|
          );
 | 
						|
        } else {
 | 
						|
          logger.info('No incompatibility found for: $incomp');
 | 
						|
        }
 | 
						|
      }
 | 
						|
      for (final dep in mod.dependencies) {
 | 
						|
        if (!activeMods.containsKey(dep)) {
 | 
						|
          loadOrder.errors.add('Missing dependency: ${mod.id} requires $dep');
 | 
						|
          logger.warning('Missing dependency: ${mod.id} requires $dep');
 | 
						|
        } else {
 | 
						|
          logger.info('Dependency found: ${mod.id} requires $dep');
 | 
						|
        }
 | 
						|
      }
 | 
						|
    }
 | 
						|
 | 
						|
    logger.info('Adding active mods to load order...');
 | 
						|
    loadOrder.order.addAll(activeMods.values.toList());
 | 
						|
    logger.info(
 | 
						|
      'Active mods added: ${loadOrder.order.map((mod) => mod.id).join(', ')}',
 | 
						|
    );
 | 
						|
 | 
						|
    final modMap = {for (final mod in loadOrder.order) mod.id: mod};
 | 
						|
    final graph = <String, Set<String>>{};
 | 
						|
    final inDegree = <String, int>{};
 | 
						|
 | 
						|
    // Step 1: Initialize graph and inDegree
 | 
						|
    for (final mod in loadOrder.order) {
 | 
						|
      graph[mod.id] = <String>{};
 | 
						|
      inDegree[mod.id] = 0;
 | 
						|
    }
 | 
						|
 | 
						|
    // Step 2: Build dependency graph
 | 
						|
    void addEdge(String from, String to) {
 | 
						|
      final fromMod = modMap[from];
 | 
						|
      if (fromMod == null) {
 | 
						|
        logger.warning('Missing dependency: $from');
 | 
						|
        return;
 | 
						|
      }
 | 
						|
      final toMod = modMap[to];
 | 
						|
      if (toMod == null) {
 | 
						|
        logger.warning('Missing dependency: $to');
 | 
						|
        return;
 | 
						|
      }
 | 
						|
      if (graph[from]!.add(to)) {
 | 
						|
        inDegree[to] = inDegree[to]! + 1;
 | 
						|
      }
 | 
						|
    }
 | 
						|
 | 
						|
    for (final mod in loadOrder.order) {
 | 
						|
      for (final target in mod.loadBefore) {
 | 
						|
        addEdge(mod.id, target);
 | 
						|
      }
 | 
						|
      for (final target in mod.loadAfter) {
 | 
						|
        addEdge(target, mod.id);
 | 
						|
      }
 | 
						|
      for (final dep in mod.dependencies) {
 | 
						|
        addEdge(dep, mod.id);
 | 
						|
      }
 | 
						|
    }
 | 
						|
 | 
						|
    // Step 3: Calculate tiers dynamically with cross-tier dependencies
 | 
						|
    final tiers = <Mod, int>{};
 | 
						|
    for (final mod in loadOrder.order) {
 | 
						|
      int tier = 2; // Default to Tier 2
 | 
						|
 | 
						|
      // Check if mod loads before any base game mod (Tier 0)
 | 
						|
      final loadsBeforeBase = mod.loadBefore.any(
 | 
						|
        (id) => modMap[id]?.isBaseGame ?? false,
 | 
						|
      );
 | 
						|
      if (mod.isBaseGame || loadsBeforeBase) {
 | 
						|
        tier = 0;
 | 
						|
      } else {
 | 
						|
        // Check if mod loads before any expansion (Tier 1)
 | 
						|
        final loadsBeforeExpansion = mod.loadBefore.any(
 | 
						|
          (id) => modMap[id]?.isExpansion ?? false,
 | 
						|
        );
 | 
						|
        if (mod.isExpansion || loadsBeforeExpansion) {
 | 
						|
          tier = 1;
 | 
						|
        }
 | 
						|
      }
 | 
						|
 | 
						|
      tiers[mod] = tier;
 | 
						|
    }
 | 
						|
 | 
						|
    // Step 4: Global priority queue (tier ascending, size descending)
 | 
						|
    final pq = PriorityQueue<Mod>((a, b) {
 | 
						|
      final tierA = tiers[a]!;
 | 
						|
      final tierB = tiers[b]!;
 | 
						|
      if (tierA != tierB) return tierA.compareTo(tierB);
 | 
						|
      return b.size.compareTo(a.size);
 | 
						|
    });
 | 
						|
 | 
						|
    // Initialize queue with mods having inDegree 0
 | 
						|
    for (final mod in loadOrder.order) {
 | 
						|
      if (inDegree[mod.id] == 0) {
 | 
						|
        pq.add(mod);
 | 
						|
      }
 | 
						|
    }
 | 
						|
 | 
						|
    final orderedMods = <Mod>[];
 | 
						|
    while (pq.isNotEmpty) {
 | 
						|
      final current = pq.removeFirst();
 | 
						|
      orderedMods.add(current);
 | 
						|
 | 
						|
      for (final neighborId in graph[current.id]!) {
 | 
						|
        inDegree[neighborId] = inDegree[neighborId]! - 1;
 | 
						|
        if (inDegree[neighborId] == 0) {
 | 
						|
          final neighbor = modMap[neighborId]!;
 | 
						|
          pq.add(neighbor);
 | 
						|
        }
 | 
						|
      }
 | 
						|
    }
 | 
						|
    if (orderedMods.length != loadOrder.order.length) {
 | 
						|
      loadOrder.errors.add('Cycle detected in dependencies');
 | 
						|
      logger.warning(
 | 
						|
        'Cycle detected in dependencies: expected ${loadOrder.order.length}, got ${orderedMods.length}.',
 | 
						|
      );
 | 
						|
    }
 | 
						|
 | 
						|
    loadOrder.order = orderedMods;
 | 
						|
    logger.info(
 | 
						|
      'Load order generated successfully with ${loadOrder.order.length} mods.',
 | 
						|
    );
 | 
						|
    for (final mod in loadOrder.order) {
 | 
						|
      logger.info('Mod: ${mod.toString()}');
 | 
						|
    }
 | 
						|
    return loadOrder;
 | 
						|
  }
 | 
						|
 | 
						|
  // The point of relations and the recursive call is to handle the case where
 | 
						|
  // A mod depends on a mod that depends on another mod
 | 
						|
  // So we move our first mod A to after B
 | 
						|
  // But then we move B after C and A is no longer guranteed to be after B
 | 
						|
  // So we update it too just in case
 | 
						|
  // To make sure we have A B C
 | 
						|
  // Now it opens us to a stack overflow...
 | 
						|
  LoadOrder shuffleMod(
 | 
						|
    Mod mod,
 | 
						|
    LoadOrder loadOrder,
 | 
						|
    Map<String, List<Mod>> relations, [
 | 
						|
    Map<String, bool>? seen,
 | 
						|
  ]) {
 | 
						|
    final logger = Logger.instance;
 | 
						|
    logger.info('Starting shuffleMod for mod: ${mod.id}');
 | 
						|
 | 
						|
    // Prevent infinite loops
 | 
						|
    seen ??= <String, bool>{};
 | 
						|
    if (seen[mod.id] == true) {
 | 
						|
      logger.info('Mod ${mod.id} has already been seen, skipping.');
 | 
						|
      return loadOrder;
 | 
						|
    }
 | 
						|
    seen[mod.id] = true;
 | 
						|
    logger.info('Marking mod ${mod.id} as seen.');
 | 
						|
 | 
						|
    for (final dependency in mod.dependencies) {
 | 
						|
      logger.info('Checking dependency: $dependency for mod ${mod.id}');
 | 
						|
      final depMod = mods[dependency];
 | 
						|
      if (depMod == null) {
 | 
						|
        loadOrder.errors.add(
 | 
						|
          'Missing dependency: ${mod.id} requires mod with ID $dependency',
 | 
						|
        );
 | 
						|
        logger.warning(
 | 
						|
          'Missing dependency: ${mod.id} requires mod with ID $dependency',
 | 
						|
        );
 | 
						|
        continue;
 | 
						|
      }
 | 
						|
 | 
						|
      if (loadOrder.order.indexOf(mod) < loadOrder.order.indexOf(depMod)) {
 | 
						|
        logger.info('Reordering: ${mod.id} should come after ${depMod.id}');
 | 
						|
        loadOrder.order.removeAt(loadOrder.order.indexOf(mod));
 | 
						|
        loadOrder.order.insert(loadOrder.order.indexOf(depMod) + 1, mod);
 | 
						|
        relations[mod.id] = [...relations[mod.id] ?? [], depMod];
 | 
						|
      }
 | 
						|
    }
 | 
						|
 | 
						|
    for (final loadAfter in mod.loadAfter) {
 | 
						|
      logger.info('Checking loadAfter: $loadAfter for mod ${mod.id}');
 | 
						|
      final loadAfterMod = mods[loadAfter];
 | 
						|
      if (loadAfterMod != null &&
 | 
						|
          loadOrder.order.indexOf(mod) <
 | 
						|
              loadOrder.order.indexOf(loadAfterMod)) {
 | 
						|
        final loadAfterIndex = loadOrder.order.indexOf(loadAfterMod);
 | 
						|
        // Mod is not loaded, we don't care about it
 | 
						|
        if (loadAfterIndex == -1) {
 | 
						|
          logger.warning(
 | 
						|
            'Missing loadAfter: ${mod.id} requires mod with ID $loadAfter',
 | 
						|
          );
 | 
						|
          continue;
 | 
						|
        }
 | 
						|
 | 
						|
        logger.info(
 | 
						|
          'Reordering: ${mod.id} should come after ${loadAfterMod.id}',
 | 
						|
        );
 | 
						|
        loadOrder.order.removeAt(loadOrder.order.indexOf(mod));
 | 
						|
        loadOrder.order.insert(loadOrder.order.indexOf(loadAfterMod) + 1, mod);
 | 
						|
        relations[mod.id] = [...relations[mod.id] ?? [], loadAfterMod];
 | 
						|
      }
 | 
						|
    }
 | 
						|
 | 
						|
    for (final loadBefore in mod.loadBefore) {
 | 
						|
      logger.info('Checking loadBefore: $loadBefore for mod ${mod.id}');
 | 
						|
      final loadBeforeMod = mods[loadBefore];
 | 
						|
      if (loadBeforeMod != null &&
 | 
						|
          loadOrder.order.indexOf(mod) >
 | 
						|
              loadOrder.order.indexOf(loadBeforeMod)) {
 | 
						|
        final loadBeforeIndex = loadOrder.order.indexOf(loadBeforeMod);
 | 
						|
        // Mod is not loaded, we don't care about it
 | 
						|
        if (loadBeforeIndex == -1) {
 | 
						|
          logger.warning(
 | 
						|
            'Missing loadBefore: ${mod.id} requires mod with ID $loadBefore',
 | 
						|
          );
 | 
						|
          continue;
 | 
						|
        }
 | 
						|
 | 
						|
        logger.info(
 | 
						|
          'Reordering: ${mod.id} should come before ${loadBeforeMod.id}',
 | 
						|
        );
 | 
						|
        loadOrder.order.removeAt(loadOrder.order.indexOf(mod));
 | 
						|
        loadOrder.order.insert(loadOrder.order.indexOf(loadBeforeMod), mod);
 | 
						|
        relations[mod.id] = [...relations[mod.id] ?? [], loadBeforeMod];
 | 
						|
      }
 | 
						|
    }
 | 
						|
 | 
						|
    for (final relatedMod in relations[mod.id] ?? []) {
 | 
						|
      logger.info('Recursively shuffling related mod: ${relatedMod.id}');
 | 
						|
      loadOrder = shuffleMod(relatedMod, loadOrder, relations, seen);
 | 
						|
    }
 | 
						|
    logger.info('Completed shuffleMod for mod: ${mod.id}');
 | 
						|
    return loadOrder;
 | 
						|
  }
 | 
						|
 | 
						|
  List<List<String>> checkIncompatibilities(List<String> modIds) {
 | 
						|
    final incompatibilities = <List<String>>[];
 | 
						|
    for (final modId in modIds) {
 | 
						|
      final mod = mods[modId]!;
 | 
						|
      for (final incomp in mod.incompatibilities) {
 | 
						|
        if (modIds.contains(incomp)) {
 | 
						|
          incompatibilities.add([mod.id, incomp]);
 | 
						|
        }
 | 
						|
      }
 | 
						|
    }
 | 
						|
    return incompatibilities;
 | 
						|
  }
 | 
						|
 | 
						|
  LoadOrder loadDependencies(
 | 
						|
    String modId, [
 | 
						|
    LoadOrder? loadOrder,
 | 
						|
    List<String>? toEnable,
 | 
						|
    Map<String, bool>? seen,
 | 
						|
    List<String>? cyclePath,
 | 
						|
  ]) {
 | 
						|
    final mod = mods[modId]!;
 | 
						|
    loadOrder ??= LoadOrder();
 | 
						|
    toEnable ??= <String>[];
 | 
						|
    seen ??= <String, bool>{};
 | 
						|
    cyclePath ??= <String>[];
 | 
						|
 | 
						|
    // Add current mod to cycle path
 | 
						|
    cyclePath.add(modId);
 | 
						|
 | 
						|
    for (final dep in mod.dependencies) {
 | 
						|
      if (!mods.containsKey(dep)) {
 | 
						|
        loadOrder.errors.add(
 | 
						|
          'Missing dependency: ${mod.name} requires mod with ID $dep',
 | 
						|
        );
 | 
						|
        continue;
 | 
						|
      }
 | 
						|
      final depMod = mods[dep]!;
 | 
						|
      if (seen[dep] == true) {
 | 
						|
        // Find the start of the cycle
 | 
						|
        int cycleStart = cyclePath.indexOf(dep);
 | 
						|
        if (cycleStart >= 0) {
 | 
						|
          // Extract the cycle part
 | 
						|
          List<String> cycleIds = [...cyclePath.sublist(cycleStart), modId];
 | 
						|
          loadOrder.errors.add(
 | 
						|
            'Cyclic dependency detected: ${cycleIds.join(' -> ')}',
 | 
						|
          );
 | 
						|
        } else {
 | 
						|
          loadOrder.errors.add('Cyclic dependency detected: $modId -> $dep');
 | 
						|
        }
 | 
						|
        continue;
 | 
						|
      }
 | 
						|
      seen[dep] = true;
 | 
						|
      toEnable.add(depMod.id);
 | 
						|
      loadDependencies(
 | 
						|
        depMod.id,
 | 
						|
        loadOrder,
 | 
						|
        toEnable,
 | 
						|
        seen,
 | 
						|
        List.from(cyclePath),
 | 
						|
      );
 | 
						|
    }
 | 
						|
 | 
						|
    return loadOrder;
 | 
						|
  }
 | 
						|
 | 
						|
  LoadOrder loadRequired([LoadOrder? loadOrder]) {
 | 
						|
    loadOrder ??= LoadOrder();
 | 
						|
    final toEnable = <String>[];
 | 
						|
    for (final modid in activeMods.keys) {
 | 
						|
      loadDependencies(modid, loadOrder, toEnable);
 | 
						|
    }
 | 
						|
    for (final modid in toEnable) {
 | 
						|
      setEnabled(modid, true);
 | 
						|
    }
 | 
						|
    return generateLoadOrder(loadOrder);
 | 
						|
  }
 | 
						|
 | 
						|
  LoadOrder loadRequiredBaseGame([LoadOrder? loadOrder]) {
 | 
						|
    loadOrder ??= LoadOrder();
 | 
						|
    final baseGameMods =
 | 
						|
        mods.values.where((mod) => mod.isBaseGame || mod.isExpansion).toList();
 | 
						|
    // You would probably want to load these too if you had them
 | 
						|
    final specialMods =
 | 
						|
        mods.values
 | 
						|
            .where(
 | 
						|
              (mod) =>
 | 
						|
                  mod.id.contains("harmony") ||
 | 
						|
                  mod.id.contains("prepatcher") ||
 | 
						|
                  mod.id.contains("betterlog"),
 | 
						|
            )
 | 
						|
            .toList();
 | 
						|
 | 
						|
    enableMods(baseGameMods.map((mod) => mod.id).toList());
 | 
						|
    enableMods(specialMods.map((mod) => mod.id).toList());
 | 
						|
 | 
						|
    return loadRequired(loadOrder);
 | 
						|
  }
 | 
						|
}
 | 
						|
 | 
						|
String _expansionNameFromId(String id) {
 | 
						|
  final parts = id.split('.');
 | 
						|
  if (parts.length < 3) return id;
 | 
						|
 | 
						|
  final expansionPart = parts[2];
 | 
						|
  return expansionPart.substring(0, 1).toUpperCase() +
 | 
						|
      expansionPart.substring(1);
 | 
						|
}
 |