diff --git a/lib/modloader.dart b/lib/modloader.dart deleted file mode 100644 index b3d3858..0000000 --- a/lib/modloader.dart +++ /dev/null @@ -1,691 +0,0 @@ -import 'dart:io'; -import 'dart:async'; -import 'package:rimworld_modman/logger.dart'; -import 'package:rimworld_modman/mod.dart'; -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'; -const logsPath = '$root/ModManager'; - -class ModList { - final String path; - Map mods = {}; - bool modsLoaded = false; - String loadingStatus = ''; - int totalModsFound = 0; - int loadedModsCount = 0; - - ModList({required this.path}); - - Future loadWithConfig({bool skipFileCount = false}) async { - final logger = Logger.instance; - - // Clear existing state if reloading - if (modsLoaded) { - logger.info('Clearing existing mods state for reload.'); - mods.clear(); - } - - modsLoaded = false; - loadedModsCount = 0; - loadingStatus = 'Loading active mods from config...'; - - final stopwatch = Stopwatch()..start(); - logger.info('Loading configuration from config file: $configPath'); - - try { - // First, load the config file to get the list of active mods - final configFile = ConfigFile(path: configPath); - await configFile.load(); - logger.info('Config file loaded successfully.'); - - // Create a Set of active mod IDs for quick lookups - final activeModIds = configFile.mods.map((m) => m.id).toSet(); - logger.info('Active mod IDs created: ${activeModIds.join(', ')}'); - - // Special handling for Ludeon mods that might not exist as directories - for (final configMod in configFile.mods) { - if (configMod.id.startsWith('ludeon.')) { - final isBaseGame = configMod.id == 'ludeon.rimworld'; - final isExpansion = - configMod.id.startsWith('ludeon.rimworld.') && !isBaseGame; - - // Create a placeholder mod for the Ludeon mods that might not have directories - final mod = Mod( - name: - isBaseGame - ? "RimWorld" - : isExpansion - ? "RimWorld ${_expansionNameFromId(configMod.id)}" - : configMod.id, - id: configMod.id, - path: '', - versions: [], - description: - isBaseGame - ? "RimWorld base game" - : isExpansion - ? "RimWorld expansion" - : "", - hardDependencies: [], - loadAfter: isExpansion ? ['ludeon.rimworld'] : [], - loadBefore: [], - incompatabilities: [], - enabled: true, - size: 0, - isBaseGame: isBaseGame, - isExpansion: isExpansion, - ); - - mods[configMod.id] = mod; - loadedModsCount++; - logger.info('Added mod from config: ${mod.name} (ID: ${mod.id})'); - } - } - - // Now scan the directory for mod metadata - loadingStatus = 'Scanning mod directories...'; - final directory = Directory(path); - - if (!directory.existsSync()) { - loadingStatus = 'Error: Mods root directory does not exist: $path'; - logger.error(loadingStatus); - return; - } - - final List entities = directory.listSync(); - final List modDirectories = - entities.whereType().map((dir) => dir.path).toList(); - - totalModsFound = modDirectories.length; - loadingStatus = 'Found $totalModsFound mod directories. Loading...'; - 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, skipFileCount: skipFileCount); - logger.info('Loaded mod from directory: ${mod.name} (ID: ${mod.id})'); - - // If we already have this mod from the config (like Ludeon mods), update its data - if (mods.containsKey(mod.id)) { - final existingMod = mods[mod.id]!; - mods[mod.id] = Mod( - name: mod.name, - id: mod.id, - path: mod.path, - versions: mod.versions, - description: mod.description, - hardDependencies: mod.hardDependencies, - loadAfter: mod.loadAfter, - loadBefore: mod.loadBefore, - incompatabilities: mod.incompatabilities, - enabled: activeModIds.contains( - mod.id, - ), // Set enabled based on config - size: mod.size, - isBaseGame: existingMod.isBaseGame, - isExpansion: existingMod.isExpansion, - ); - logger.info('Updated existing mod: ${mod.name} (ID: ${mod.id})'); - } else { - // Otherwise add as a new mod - mods[mod.id] = Mod( - name: mod.name, - id: mod.id, - path: mod.path, - versions: mod.versions, - description: mod.description, - hardDependencies: mod.hardDependencies, - loadAfter: mod.loadAfter, - loadBefore: mod.loadBefore, - incompatabilities: mod.incompatabilities, - enabled: activeModIds.contains( - mod.id, - ), // Set enabled based on config - size: mod.size, - isBaseGame: mod.isBaseGame, - isExpansion: mod.isExpansion, - ); - loadedModsCount++; - logger.info('Added new mod: ${mod.name} (ID: ${mod.id})'); - } - - final modTime = stopwatch.elapsedMilliseconds - modStart; - loadingStatus = 'Loaded $loadedModsCount/$totalModsFound mods...'; - - if (loadedModsCount % 50 == 0 || loadedModsCount == totalModsFound) { - logger.info( - 'Progress: Loaded $loadedModsCount mods (${modTime}ms)', - ); - } - } catch (e) { - logger.error('Error loading mod from directory: $modDir'); - logger.error('Error: $e'); - } - } - - modsLoaded = true; - final totalTime = stopwatch.elapsedMilliseconds; - loadingStatus = - 'Completed! Loaded $loadedModsCount mods in ${totalTime}ms.'; - logger.info( - 'Loading complete! Loaded ${mods.length} mods in ${totalTime}ms', - ); - } catch (e) { - loadingStatus = 'Error loading mods: $e'; - logger.error(loadingStatus); - } - } - - // Helper function to get a nice expansion name from ID - 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); - } - - // 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] = {}; - } - - // 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); - } - } - } - - // Handle base game and expansions: - // 1. Add the base game as a dependency of all mods except those who have loadBefore for it - // 2. Add expansions as dependencies of mods that load after them - - // First identify the base game and expansions - final baseGameId = - mods.values.where((m) => m.isBaseGame).map((m) => m.id).firstOrNull; - if (baseGameId != null) { - for (final mod in mods.values) { - // Skip the base game itself and mods that explicitly load before it - if (mod.id != baseGameId && !mod.loadBefore.contains(baseGameId)) { - graph[mod.id]!.add(baseGameId); - } - } - } - - 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] = {}; - } - - // Add soft dependencies (loadAfter) - for (final mod in mods.values) { - for (final dependency in mod.loadAfter) { - // Only add if the dependency exists in our loaded mods - if (mods.containsKey(dependency)) { - graph[mod.id]!.add(dependency); - } - } - } - - // Handle loadBefore - invert the relationship for the graph - // If A loadBefore B, then B softDepends on A - for (final mod in mods.values) { - for (final loadBeforeId in mod.loadBefore) { - if (mods.containsKey(loadBeforeId)) { - graph[loadBeforeId]!.add(mod.id); - } - } - } - - 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 with size prioritization - 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; - } - } - - // Separate nodes by "layers" (nodes that can be processed at the same time) - List> layers = []; - - // Process until all nodes are assigned to layers - while (inDegree.isNotEmpty) { - // Find all nodes with in-degree 0 in this iteration - List currentLayer = []; - inDegree.forEach((node, degree) { - if (degree == 0) { - currentLayer.add(node); - } - }); - - if (currentLayer.isEmpty && inDegree.isNotEmpty) { - // We have a cycle - add all remaining nodes to a final layer - currentLayer = inDegree.keys.toList(); - print( - "Warning: Cycle detected in dependency graph. Adding all remaining nodes to final layer.", - ); - } - - // Sort this layer by mod size (descending) - currentLayer.sort((a, b) { - final modA = mods[a]; - final modB = mods[b]; - if (modA == null || modB == null) return 0; - return modB.size.compareTo(modA.size); // Larger mods first - }); - - // Add the layer to our layers list - layers.add(currentLayer); - - // Remove processed nodes from inDegree - for (final node in currentLayer) { - inDegree.remove(node); - - // Update in-degrees for remaining nodes - for (final entry in graphCopy.entries) { - if (entry.value.contains(node)) { - if (inDegree.containsKey(entry.key)) { - inDegree[entry.key] = inDegree[entry.key]! - 1; - } - } - } - } - } - - // Flatten the layers to get the final order (first layer first) - List result = []; - for (final layer in layers) { - result.addAll(layer); - } - - // Final sanity check to make sure all nodes are included - if (result.length != graph.keys.length) { - // Add any missing nodes - for (final node in graph.keys) { - if (!result.contains(node)) { - result.add(node); - } - } - } - - return result; - } - - // 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() { - final logger = Logger.instance; - logger.info("Building dependency graph..."); - final hardGraph = buildDependencyGraph(); - - // Check for cycles in hard dependencies - final cycle = detectCycle(hardGraph); - if (cycle != null) { - logger.warning( - "Cycle in hard dependencies detected: ${cycle.join(" -> ")}", - ); - logger.info("Will attempt to break cycle to produce a valid load order"); - } - - logger.info( - "Performing topological sort for hard dependencies (prioritizing larger mods)...", - ); - final hardOrder = topologicalSort(hardGraph); - - logger.info("Adjusting for soft dependencies..."); - final softGraph = buildSoftDependencyGraph(); - final finalOrder = adjustForSoftDependencies(hardOrder, softGraph); - - // Check for incompatibilities - final incompatibilities = findIncompatibilities(); - if (incompatibilities.isNotEmpty) { - logger.warning("Incompatible mods detected:"); - for (final pair in incompatibilities) { - logger.warning(" - ${mods[pair[0]]?.name} and ${mods[pair[1]]?.name}"); - } - } - - logger.info( - "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 []}); - - Future load() async { - final logger = Logger.instance; - final file = File(path); - logger.info('Loading configuration from: $path'); - - 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() - : []; - - logger.info('Found ${knownExpansionIds.length} known expansions.'); - - // Clear and recreate the mods list - mods = []; - 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); - - // We'll populate with dummy mods for now, they'll be replaced later - mods.add( - Mod( - name: - isBaseGame - ? "RimWorld" - : isExpansion - ? "RimWorld ${_expansionNameFromId(modId)}" - : modId, - id: modId, - path: '', - versions: [], - description: - isBaseGame - ? "RimWorld base game" - : isExpansion - ? "RimWorld expansion" - : "", - hardDependencies: [], - loadAfter: isExpansion ? ['ludeon.rimworld'] : [], - loadBefore: [], - incompatabilities: [], - enabled: true, - size: 0, - isBaseGame: isBaseGame, - isExpansion: isExpansion, - ), - ); - } - - logger.info('Loaded ${mods.length} mods from config file.'); - } catch (e) { - logger.error('Error loading configuration file: $e'); - throw Exception('Failed to load config file: $e'); - } - } - - // Helper function to get a nice expansion name from ID - 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); - } - - // Save the current mod order back to the config file - void save() { - final logger = Logger.instance; - final file = File(path); - logger.info('Saving configuration to: $path'); - - // Create a backup just in case - final backupPath = '$path.bak'; - file.copySync(backupPath); - logger.info('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)); - logger.info('Configuration saved successfully with ${mods.length} mods.'); - } catch (e) { - logger.error('Error saving configuration: $e'); - logger.info('Original configuration preserved at: $backupPath'); - } - } - - // Fix the load order of mods according to dependencies - void fixLoadOrder(ModList modList) { - final logger = Logger.instance; - logger.info("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; - - logger.info( - "Load order fixed. ${mods.length} mods are now in dependency-sorted order.", - ); - } -}