Remove original modlist
This commit is contained in:
@@ -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<String, Mod> mods = {};
|
|
||||||
bool modsLoaded = false;
|
|
||||||
String loadingStatus = '';
|
|
||||||
int totalModsFound = 0;
|
|
||||||
int loadedModsCount = 0;
|
|
||||||
|
|
||||||
ModList({required this.path});
|
|
||||||
|
|
||||||
Future<void> 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<FileSystemEntity> entities = directory.listSync();
|
|
||||||
final List<String> modDirectories =
|
|
||||||
entities.whereType<Directory>().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<String, Set<String>> buildDependencyGraph() {
|
|
||||||
// Graph where graph[A] contains B if A depends on B (B must load before A)
|
|
||||||
final Map<String, Set<String>> graph = {};
|
|
||||||
|
|
||||||
// Initialize the graph with empty dependency sets for all mods
|
|
||||||
for (final mod in mods.values) {
|
|
||||||
graph[mod.id] = <String>{};
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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<String, Set<String>> buildSoftDependencyGraph() {
|
|
||||||
final Map<String, Set<String>> graph = {};
|
|
||||||
|
|
||||||
// Initialize the graph with empty sets
|
|
||||||
for (final mod in mods.values) {
|
|
||||||
graph[mod.id] = <String>{};
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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<String>? detectCycle(Map<String, Set<String>> graph) {
|
|
||||||
// Track visited nodes and the current path
|
|
||||||
Set<String> visited = {};
|
|
||||||
Set<String> currentPath = {};
|
|
||||||
List<String> cycleNodes = [];
|
|
||||||
|
|
||||||
bool dfs(String node, List<String> 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<String> topologicalSort(Map<String, Set<String>> graph) {
|
|
||||||
// Create a copy of the graph to work with
|
|
||||||
final Map<String, Set<String>> graphCopy = {};
|
|
||||||
for (final entry in graph.entries) {
|
|
||||||
graphCopy[entry.key] = Set<String>.from(entry.value);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Calculate in-degree of each node (number of edges coming in)
|
|
||||||
Map<String, int> 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<List<String>> layers = [];
|
|
||||||
|
|
||||||
// Process until all nodes are assigned to layers
|
|
||||||
while (inDegree.isNotEmpty) {
|
|
||||||
// Find all nodes with in-degree 0 in this iteration
|
|
||||||
List<String> 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<String> 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<String> adjustForSoftDependencies(
|
|
||||||
List<String> hardOrder,
|
|
||||||
Map<String, Set<String>> softGraph,
|
|
||||||
) {
|
|
||||||
// Create a map of positions in the hard dependency order
|
|
||||||
Map<String, int> 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<List<String>> findIncompatibilities() {
|
|
||||||
List<List<String>> 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<String> 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<Mod> 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<Mod> mods;
|
|
||||||
|
|
||||||
ConfigFile({required this.path, this.mods = const []});
|
|
||||||
|
|
||||||
Future<void> 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()
|
|
||||||
: <String>[];
|
|
||||||
|
|
||||||
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<Mod> orderedMods = [];
|
|
||||||
Set<String> 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.",
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
Reference in New Issue
Block a user