import 'dart:io'; import 'package:xml/xml.dart'; const root = r'C:/Users/Administrator/Seafile/Games-Rimworld'; const modsRoot = '$root/294100'; const configRoot = '$root/AppData/RimWorld by Ludeon Studios/Config'; const configPath = '$configRoot/ModsConfig.xml'; XmlElement findCaseInsensitive(XmlElement element, String name) { return element.childElements.firstWhere( (e) => e.name.local.toLowerCase() == name, ); } XmlElement findCaseInsensitiveDoc(XmlDocument document, String name) { name = name.toLowerCase(); return document.childElements.firstWhere( (e) => e.name.local.toLowerCase() == name, ); } class Mod { final String name; // ModMetaData.name final String id; // ModMetaData.packageId final String path; // figure it out final List versions; // ModMetaData.supportedVersions final String description; // ModMetaData.description final List hardDependencies; // ModMetaData.modDependencies - this is a li with packageId, displayName, steamWorkshopUrl and downloadUrl final List softDependencies; // ModMetaData.loadAfter final List incompatabilities; // ModMetaData.incompatibleWith final bool enabled; // ConfigFile.mods.firstWhere((mod) => mod.id == id).enabled final int size; // Count of files in the mod directory Mod({ required this.name, required this.id, required this.path, required this.versions, required this.description, required this.hardDependencies, required this.softDependencies, required this.incompatabilities, required this.enabled, required this.size, }); static Mod fromDirectory(String path, {bool skipFileCount = false}) { final stopwatch = Stopwatch()..start(); final aboutFile = File('$path/About/About.xml'); if (!aboutFile.existsSync()) { throw Exception('About.xml file does not exist in $aboutFile'); } final aboutXml = XmlDocument.parse(aboutFile.readAsStringSync()); final xmlTime = stopwatch.elapsedMilliseconds; late final XmlElement metadata; try { metadata = findCaseInsensitiveDoc(aboutXml, 'ModMetaData'); } catch (e) { throw Exception( 'Error: ModMetaData element is missing in About.xml ($aboutFile). Original error: $e', ); } late final String name; try { name = metadata.findElements('name').first.innerText; } catch (e) { throw Exception( 'Error: name element is missing in ModMetaData ($aboutFile). Original error: $e', ); } late final String id; try { id = metadata.findElements('packageId').first.innerText.toLowerCase(); } catch (e) { throw Exception( 'Error: packageId element is missing in ModMetaData ($aboutFile). Original error: $e', ); } late final List versions; try { versions = metadata .findElements('supportedVersions') .first .findElements('li') .map((e) => e.innerText) .toList(); } catch (e) { throw Exception( 'Error: supportedVersions or li elements are missing in ModMetaData ($aboutFile). Original error: $e', ); } String description = ''; try { description = metadata.findElements('description').first.innerText; } catch (e) { // Silent error for optional element } List hardDependencies = []; try { hardDependencies = metadata .findElements('modDependenciesByVersion') .last .findElements('li') .map( (e) => e.findElements('packageId').first.innerText.toLowerCase(), ) .toList(); } catch (e) { // Silent error for optional element } List softDependencies = []; try { softDependencies = metadata .findElements('loadAfter') .first .findElements('li') .map((e) => e.innerText.toLowerCase()) .toList(); } catch (e) { // Silent error for optional element } List incompatabilities = []; try { incompatabilities = metadata .findElements('incompatibleWith') .first .findElements('li') .map((e) => e.innerText.toLowerCase()) .toList(); } catch (e) { // Silent error for optional element } final metadataTime = stopwatch.elapsedMilliseconds - xmlTime; int size = 0; if (!skipFileCount) { size = Directory(path) .listSync(recursive: true) .where( (entity) => !entity.path .split(Platform.pathSeparator) .last .startsWith('.'), ) .length; } final fileCountTime = stopwatch.elapsedMilliseconds - metadataTime - xmlTime; final totalTime = stopwatch.elapsedMilliseconds; // Uncomment for detailed timing print( 'Mod $name timing: XML=${xmlTime}ms, Metadata=${metadataTime}ms, FileCount=${fileCountTime}ms, Total=${totalTime}ms', ); return Mod( name: name, id: id, path: path, versions: versions, description: description, hardDependencies: hardDependencies, softDependencies: softDependencies, incompatabilities: incompatabilities, enabled: false, size: size, ); } } class ModList { final String path; Map mods = {}; ModList({required this.path}); Stream load({bool skipFileCount = false}) async* { final stopwatch = Stopwatch()..start(); final directory = Directory(path); print('Loading configuration from: $path'); if (directory.existsSync()) { final List entities = directory.listSync(); final List modDirectories = entities.whereType().map((dir) => dir.path).toList(); print( 'Found ${modDirectories.length} mod directories (${stopwatch.elapsedMilliseconds}ms)', ); int processedCount = 0; int totalMods = modDirectories.length; for (final modDir in modDirectories) { try { final modStart = stopwatch.elapsedMilliseconds; final mod = Mod.fromDirectory(modDir, skipFileCount: skipFileCount); mods[mod.id] = mod; processedCount++; final modTime = stopwatch.elapsedMilliseconds - modStart; if (processedCount % 50 == 0 || processedCount == totalMods) { print( 'Progress: Loaded $processedCount/$totalMods mods (${stopwatch.elapsedMilliseconds}ms, avg ${stopwatch.elapsedMilliseconds / processedCount}ms per mod)', ); } yield mod; } catch (e) { print('Error loading mod from directory: $modDir'); print('Error: $e'); } } final totalTime = stopwatch.elapsedMilliseconds; print( 'Loading complete! Loaded ${mods.length} mods in ${totalTime}ms (${totalTime / mods.length}ms per mod)', ); } else { print('Mods root directory does not exist: $path'); } } // Build a directed graph of mod dependencies Map> buildDependencyGraph() { // Graph where graph[A] contains B if A depends on B (B must load before A) final Map> graph = {}; // Initialize the graph with empty dependency sets for all mods for (final mod in mods.values) { graph[mod.id] = Set(); } // Add hard dependencies to the graph for (final mod in mods.values) { for (final dependency in mod.hardDependencies) { // Only add if the dependency exists in our loaded mods if (mods.containsKey(dependency)) { graph[mod.id]!.add(dependency); } } } return graph; } // Build a graph for soft dependencies Map> buildSoftDependencyGraph() { final Map> graph = {}; // Initialize the graph with empty sets for (final mod in mods.values) { graph[mod.id] = Set(); } // Add soft dependencies for (final mod in mods.values) { for (final dependency in mod.softDependencies) { // Only add if the dependency exists in our loaded mods if (mods.containsKey(dependency)) { graph[mod.id]!.add(dependency); } } } return graph; } // Detect cycles in the dependency graph (which would make a valid loading order impossible) List? detectCycle(Map> graph) { // Track visited nodes and the current path Set visited = {}; Set currentPath = {}; List cycleNodes = []; bool dfs(String node, List path) { if (currentPath.contains(node)) { // Found a cycle int cycleStart = path.indexOf(node); cycleNodes = path.sublist(cycleStart); cycleNodes.add(node); // Close the cycle return true; } if (visited.contains(node)) { return false; } visited.add(node); currentPath.add(node); path.add(node); for (final dependency in graph[node] ?? {}) { if (dfs(dependency, path)) { return true; } } currentPath.remove(node); return false; } for (final node in graph.keys) { if (!visited.contains(node)) { if (dfs(node, [])) { return cycleNodes; } } } return null; // No cycle found } // Perform a topological sort using Kahn's algorithm List topologicalSort(Map> graph) { // Create a copy of the graph to work with final Map> graphCopy = {}; for (final entry in graph.entries) { graphCopy[entry.key] = Set.from(entry.value); } // Calculate in-degree of each node (number of edges coming in) Map inDegree = {}; for (final node in graphCopy.keys) { inDegree[node] = 0; } for (final dependencies in graphCopy.values) { for (final dep in dependencies) { inDegree[dep] = (inDegree[dep] ?? 0) + 1; } } // Start with nodes that have no dependencies (in-degree = 0) List nodesWithNoDependencies = []; for (final node in inDegree.keys) { if (inDegree[node] == 0) { nodesWithNoDependencies.add(node); } } // Result will store the topological order List result = []; // Process nodes with no dependencies while (nodesWithNoDependencies.isNotEmpty) { final node = nodesWithNoDependencies.removeLast(); result.add(node); // For each node that depends on this one, decrement its in-degree final dependents = []; for (final entry in graphCopy.entries) { if (entry.value.contains(node)) { dependents.add(entry.key); } } for (final dependent in dependents) { graphCopy[dependent]!.remove(node); inDegree[dependent] = inDegree[dependent]! - 1; if (inDegree[dependent] == 0) { nodesWithNoDependencies.add(dependent); } } } // Check if we have a valid topological sort if (result.length != graph.keys.length) { print( "Warning: Cyclic dependency detected, topological sort may be incomplete", ); // Add any remaining nodes to keep all mods for (final node in graph.keys) { if (!result.contains(node)) { result.add(node); } } } return result.reversed.toList(); // Reverse to get correct load order } // Adjust the order to respect soft dependencies where possible List adjustForSoftDependencies( List hardOrder, Map> softGraph, ) { // Create a map of positions in the hard dependency order Map positions = {}; for (int i = 0; i < hardOrder.length; i++) { positions[hardOrder[i]] = i; } // For each mod, try to move its soft dependencies earlier in the order bool changed = true; while (changed) { changed = false; for (final modId in hardOrder) { final softDeps = softGraph[modId] ?? {}; for (final softDep in softDeps) { // If the soft dependency is loaded after the mod, try to move it earlier if (positions.containsKey(softDep) && positions[softDep]! > positions[modId]!) { // Find where we can move the soft dependency to int targetPos = positions[modId]!; // Move the soft dependency just before the mod hardOrder.removeAt(positions[softDep]!); hardOrder.insert(targetPos, softDep); // Update positions for (int i = 0; i < hardOrder.length; i++) { positions[hardOrder[i]] = i; } changed = true; break; } } if (changed) break; } } return hardOrder; } // Check for incompatibilities in the current mod list List> findIncompatibilities() { List> incompatiblePairs = []; for (final mod in mods.values) { for (final incompatibility in mod.incompatabilities) { if (mods.containsKey(incompatibility)) { incompatiblePairs.add([mod.id, incompatibility]); } } } return incompatiblePairs; } // Sort mods based on dependencies and return the sorted list List sortMods() { print("Building dependency graph..."); final hardGraph = buildDependencyGraph(); // Check for cycles in hard dependencies final cycle = detectCycle(hardGraph); if (cycle != null) { print( "Warning: Cycle in hard dependencies detected: ${cycle.join(" -> ")}", ); print("Will attempt to break cycle to produce a valid load order"); } print("Performing topological sort for hard dependencies..."); final hardOrder = topologicalSort(hardGraph); print("Adjusting for soft dependencies..."); final softGraph = buildSoftDependencyGraph(); final finalOrder = adjustForSoftDependencies(hardOrder, softGraph); // Check for incompatibilities final incompatibilities = findIncompatibilities(); if (incompatibilities.isNotEmpty) { print("Warning: Incompatible mods detected:"); for (final pair in incompatibilities) { print(" - ${mods[pair[0]]?.name} and ${mods[pair[1]]?.name}"); } } print( "Sorting complete. Final mod order contains ${finalOrder.length} mods.", ); return finalOrder; } // Get a list of mods in the proper load order List getModsInLoadOrder() { final orderedIds = sortMods(); return orderedIds.map((id) => mods[id]!).toList(); } } // Add a method to ConfigFile to fix the mod order class ConfigFile { final String path; List mods; ConfigFile({required this.path, this.mods = const []}); void load() { final file = File(path); print('Loading configuration from: $path'); final xmlString = file.readAsStringSync(); print('XML content read successfully.'); final xmlDocument = XmlDocument.parse(xmlString); print('XML document parsed successfully.'); final modConfigData = xmlDocument.findElements("ModsConfigData").first; print('Found ModsConfigData element.'); final modsElement = modConfigData.findElements("activeMods").first; print('Found activeMods element.'); final modElements = modsElement.findElements("li"); print('Found ${modElements.length} active mods.'); mods = []; for (final modElement in modElements) { final modId = modElement.innerText.toLowerCase(); // We'll populate with dummy mods for now, they'll be replaced later mods.add( Mod( name: modId, id: modId, path: '', versions: [], description: '', hardDependencies: [], softDependencies: [], incompatabilities: [], enabled: true, size: 0, ), ); } print('Loaded ${mods.length} mods from config file.'); } // Save the current mod order back to the config file void save() { final file = File(path); print('Saving configuration to: $path'); // Create a backup just in case final backupPath = '$path.bak'; file.copySync(backupPath); print('Created backup at: $backupPath'); try { // Load the existing XML final xmlString = file.readAsStringSync(); final xmlDocument = XmlDocument.parse(xmlString); // Get the ModsConfigData element final modConfigData = xmlDocument.findElements("ModsConfigData").first; // Get the activeMods element final modsElement = modConfigData.findElements("activeMods").first; // Clear existing mod entries modsElement.children.clear(); // Add mods in the new order for (final mod in mods) { final liElement = XmlElement(XmlName('li')); liElement.innerText = mod.id; modsElement.children.add(liElement); } // Write the updated XML back to the file file.writeAsStringSync(xmlDocument.toXmlString(pretty: true)); print('Configuration saved successfully with ${mods.length} mods.'); } catch (e) { print('Error saving configuration: $e'); print('Original configuration preserved at: $backupPath'); } } // Fix the load order of mods according to dependencies void fixLoadOrder(ModList modList) { print("Fixing mod load order..."); // Get the ordered mod IDs from the mod list final orderedIds = modList.sortMods(); // Reorder the current mods list according to the dependency-sorted order // We only modify mods that exist in both the configFile and the modList List orderedMods = []; Set addedIds = {}; // First add mods in the sorted order for (final id in orderedIds) { final modIndex = mods.indexWhere((m) => m.id == id); if (modIndex >= 0) { orderedMods.add(mods[modIndex]); addedIds.add(id); } } // Then add any mods that weren't in the sorted list for (final mod in mods) { if (!addedIds.contains(mod.id)) { orderedMods.add(mod); } } // Replace the current mods list with the ordered one mods = orderedMods; print( "Load order fixed. ${mods.length} mods are now in dependency-sorted order.", ); } }