Implement dependency graphing resolving
This commit is contained in:
252
lib/main.dart
252
lib/main.dart
@@ -19,7 +19,7 @@ class RimWorldModManager extends StatelessWidget {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return MaterialApp(
|
||||
title: 'RimWorld Mod Manager',
|
||||
title: 'Rimworld Mod Manager',
|
||||
theme: ThemeData.dark().copyWith(
|
||||
primaryColor: const Color(0xFF3D4A59),
|
||||
colorScheme: ColorScheme.fromSeed(
|
||||
@@ -57,7 +57,7 @@ class _ModManagerHomePageState extends State<ModManagerHomePage> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(title: const Text('RimWorld Mod Manager')),
|
||||
appBar: AppBar(title: const Text('Rimworld Mod Manager')),
|
||||
body: _pages[_selectedIndex],
|
||||
bottomNavigationBar: BottomNavigationBar(
|
||||
currentIndex: _selectedIndex,
|
||||
@@ -95,7 +95,7 @@ class _ModListPageState extends State<ModListPage> {
|
||||
bool _isLoading = false;
|
||||
String _loadingStatus = '';
|
||||
int _totalModsFound = 0;
|
||||
bool _skipFileCount = true; // Skip file counting by default for faster loading
|
||||
bool _skipFileCount = false; // Skip file counting by default for faster loading
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
@@ -116,7 +116,7 @@ class _ModListPageState extends State<ModListPage> {
|
||||
Text('Mod List', style: Theme.of(context).textTheme.headlineMedium),
|
||||
const SizedBox(height: 16),
|
||||
Text(
|
||||
'Ready to scan for RimWorld mods.',
|
||||
'Ready to scan for Rimworld mods.',
|
||||
style: Theme.of(context).textTheme.bodyLarge,
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
@@ -263,35 +263,235 @@ class _ModListPageState extends State<ModListPage> {
|
||||
}
|
||||
|
||||
// Page to manage mod load order with dependency visualization
|
||||
class LoadOrderPage extends StatelessWidget {
|
||||
class LoadOrderPage extends StatefulWidget {
|
||||
const LoadOrderPage({super.key});
|
||||
|
||||
@override
|
||||
State<LoadOrderPage> createState() => _LoadOrderPageState();
|
||||
}
|
||||
|
||||
class _LoadOrderPageState extends State<LoadOrderPage> {
|
||||
bool _isLoading = false;
|
||||
String _statusMessage = '';
|
||||
List<Mod> _sortedMods = [];
|
||||
bool _hasCycles = false;
|
||||
List<String>? _cycleInfo;
|
||||
List<List<String>> _incompatibleMods = [];
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Center(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
const Icon(Icons.reorder, size: 64),
|
||||
const SizedBox(height: 16),
|
||||
Text('Load Order', style: Theme.of(context).textTheme.headlineMedium),
|
||||
const SizedBox(height: 16),
|
||||
Text(
|
||||
'Manage your mod loading order with dependency resolution.',
|
||||
style: Theme.of(context).textTheme.bodyLarge,
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
ElevatedButton(
|
||||
onPressed: () {
|
||||
// TODO: Implement automatic load order sorting
|
||||
},
|
||||
child: const Text('Auto-sort Mods'),
|
||||
),
|
||||
],
|
||||
return Scaffold(
|
||||
body: Padding(
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
'Mod Load Order',
|
||||
style: Theme.of(context).textTheme.headlineMedium,
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Text(
|
||||
'Automatically sort mods based on dependencies.',
|
||||
style: Theme.of(context).textTheme.bodyLarge,
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
Row(
|
||||
children: [
|
||||
ElevatedButton(
|
||||
onPressed: _isLoading ? null : _sortMods,
|
||||
child: const Text('Auto-sort Mods'),
|
||||
),
|
||||
const SizedBox(width: 16),
|
||||
ElevatedButton(
|
||||
onPressed: _isLoading || _sortedMods.isEmpty ? null : _saveModOrder,
|
||||
child: const Text('Save Load Order'),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
if (_isLoading)
|
||||
const LinearProgressIndicator(),
|
||||
if (_statusMessage.isNotEmpty)
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 8.0),
|
||||
child: Text(
|
||||
_statusMessage,
|
||||
style: TextStyle(
|
||||
color: _hasCycles || _incompatibleMods.isNotEmpty
|
||||
? Colors.orange
|
||||
: Colors.green,
|
||||
),
|
||||
),
|
||||
),
|
||||
if (_hasCycles && _cycleInfo != null)
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(top: 8.0),
|
||||
child: Text(
|
||||
'Dependency cycle detected: ${_cycleInfo!.join(" -> ")}',
|
||||
style: TextStyle(color: Colors.orange),
|
||||
),
|
||||
),
|
||||
if (_incompatibleMods.isNotEmpty)
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(top: 8.0),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
'Incompatible mods detected:',
|
||||
style: TextStyle(color: Colors.orange),
|
||||
),
|
||||
...List.generate(_incompatibleMods.length > 5 ? 5 : _incompatibleMods.length, (index) {
|
||||
final pair = _incompatibleMods[index];
|
||||
return Text(
|
||||
'- ${modManager.mods[pair[0]]?.name} and ${modManager.mods[pair[1]]?.name}',
|
||||
style: TextStyle(color: Colors.orange),
|
||||
);
|
||||
}),
|
||||
if (_incompatibleMods.length > 5)
|
||||
Text('...and ${_incompatibleMods.length - 5} more'),
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Expanded(
|
||||
child: _sortedMods.isEmpty
|
||||
? Center(
|
||||
child: Text(
|
||||
'Click "Auto-sort Mods" to generate a load order based on dependencies.',
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
)
|
||||
: ListView.builder(
|
||||
itemCount: _sortedMods.length,
|
||||
itemBuilder: (context, index) {
|
||||
final mod = _sortedMods[index];
|
||||
return Card(
|
||||
margin: const EdgeInsets.symmetric(vertical: 4),
|
||||
child: ListTile(
|
||||
leading: Text('${index + 1}'),
|
||||
title: Text(mod.name),
|
||||
subtitle: Text(mod.id),
|
||||
trailing: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
if (mod.hardDependencies.isNotEmpty)
|
||||
Tooltip(
|
||||
message: 'Hard dependencies: ${mod.hardDependencies.length}',
|
||||
child: Icon(Icons.link, color: Colors.orange, size: 16),
|
||||
),
|
||||
const SizedBox(width: 4),
|
||||
if (mod.softDependencies.isNotEmpty)
|
||||
Tooltip(
|
||||
message: 'Soft dependencies: ${mod.softDependencies.length}',
|
||||
child: Icon(Icons.link_off, color: Colors.blue, size: 16),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _sortMods() async {
|
||||
if (modManager.mods.isEmpty) {
|
||||
setState(() {
|
||||
_statusMessage = 'No mods have been loaded yet. Please load mods first.';
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
setState(() {
|
||||
_isLoading = true;
|
||||
_statusMessage = 'Sorting mods based on dependencies...';
|
||||
_hasCycles = false;
|
||||
_cycleInfo = null;
|
||||
_incompatibleMods = [];
|
||||
});
|
||||
|
||||
// This could be slow so run in a separate isolate or compute
|
||||
await Future.delayed(Duration.zero); // Allow UI to update
|
||||
|
||||
try {
|
||||
// Check for cycles first
|
||||
final hardGraph = modManager.buildDependencyGraph();
|
||||
final cycle = modManager.detectCycle(hardGraph);
|
||||
|
||||
if (cycle != null) {
|
||||
setState(() {
|
||||
_hasCycles = true;
|
||||
_cycleInfo = cycle;
|
||||
});
|
||||
}
|
||||
|
||||
// Get incompatibilities
|
||||
_incompatibleMods = modManager.findIncompatibilities();
|
||||
|
||||
// Get the sorted mods
|
||||
final sortedMods = modManager.getModsInLoadOrder();
|
||||
|
||||
setState(() {
|
||||
_sortedMods = sortedMods;
|
||||
_isLoading = false;
|
||||
_statusMessage = 'Sorting complete! ${sortedMods.length} mods sorted.';
|
||||
if (_hasCycles) {
|
||||
_statusMessage += ' Warning: Dependency cycles were found and fixed.';
|
||||
}
|
||||
if (_incompatibleMods.isNotEmpty) {
|
||||
_statusMessage += ' Warning: ${_incompatibleMods.length} incompatible mod pairs found.';
|
||||
}
|
||||
});
|
||||
} catch (e) {
|
||||
setState(() {
|
||||
_isLoading = false;
|
||||
_statusMessage = 'Error sorting mods: $e';
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
void _saveModOrder() async {
|
||||
if (_sortedMods.isEmpty) return;
|
||||
|
||||
setState(() {
|
||||
_isLoading = true;
|
||||
_statusMessage = 'Saving mod load order...';
|
||||
});
|
||||
|
||||
try {
|
||||
// Create a ConfigFile instance
|
||||
final configFile = ConfigFile(path: configPath);
|
||||
|
||||
// Load the current config
|
||||
configFile.load();
|
||||
|
||||
// Replace the mods with our sorted list
|
||||
// We need to convert our Mods to the format expected by the config file
|
||||
configFile.mods.clear();
|
||||
for (final mod in _sortedMods) {
|
||||
configFile.mods.add(mod);
|
||||
}
|
||||
|
||||
// Save the updated config
|
||||
configFile.save();
|
||||
|
||||
setState(() {
|
||||
_isLoading = false;
|
||||
_statusMessage = 'Mod load order saved successfully!';
|
||||
});
|
||||
} catch (e) {
|
||||
setState(() {
|
||||
_isLoading = false;
|
||||
_statusMessage = 'Error saving mod load order: $e';
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Page for troubleshooting problematic mods
|
||||
|
@@ -13,7 +13,7 @@ XmlElement findCaseInsensitive(XmlElement element, String name) {
|
||||
}
|
||||
|
||||
XmlElement findCaseInsensitiveDoc(XmlDocument document, String name) {
|
||||
name = name.toLowerCase();
|
||||
name = name.toLowerCase();
|
||||
return document.childElements.firstWhere(
|
||||
(e) => e.name.local.toLowerCase() == name,
|
||||
);
|
||||
@@ -152,23 +152,27 @@ class Mod {
|
||||
|
||||
int size = 0;
|
||||
if (!skipFileCount) {
|
||||
size = Directory(path)
|
||||
.listSync(recursive: true)
|
||||
.where(
|
||||
(entity) =>
|
||||
!entity.path
|
||||
.split(Platform.pathSeparator)
|
||||
.last
|
||||
.startsWith('.'),
|
||||
)
|
||||
.length;
|
||||
size =
|
||||
Directory(path)
|
||||
.listSync(recursive: true)
|
||||
.where(
|
||||
(entity) =>
|
||||
!entity.path
|
||||
.split(Platform.pathSeparator)
|
||||
.last
|
||||
.startsWith('.'),
|
||||
)
|
||||
.length;
|
||||
}
|
||||
|
||||
final fileCountTime = stopwatch.elapsedMilliseconds - metadataTime - xmlTime;
|
||||
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');
|
||||
print(
|
||||
'Mod $name timing: XML=${xmlTime}ms, Metadata=${metadataTime}ms, FileCount=${fileCountTime}ms, Total=${totalTime}ms',
|
||||
);
|
||||
|
||||
return Mod(
|
||||
name: name,
|
||||
@@ -201,7 +205,9 @@ class ModList {
|
||||
final List<String> modDirectories =
|
||||
entities.whereType<Directory>().map((dir) => dir.path).toList();
|
||||
|
||||
print('Found ${modDirectories.length} mod directories (${stopwatch.elapsedMilliseconds}ms)');
|
||||
print(
|
||||
'Found ${modDirectories.length} mod directories (${stopwatch.elapsedMilliseconds}ms)',
|
||||
);
|
||||
int processedCount = 0;
|
||||
int totalMods = modDirectories.length;
|
||||
|
||||
@@ -215,7 +221,9 @@ class ModList {
|
||||
|
||||
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)');
|
||||
print(
|
||||
'Progress: Loaded $processedCount/$totalMods mods (${stopwatch.elapsedMilliseconds}ms, avg ${stopwatch.elapsedMilliseconds / processedCount}ms per mod)',
|
||||
);
|
||||
}
|
||||
|
||||
yield mod;
|
||||
@@ -226,13 +234,280 @@ class ModList {
|
||||
}
|
||||
|
||||
final totalTime = stopwatch.elapsedMilliseconds;
|
||||
print('Loading complete! Loaded ${mods.length} mods in ${totalTime}ms (${totalTime/mods.length}ms per mod)');
|
||||
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<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] = Set<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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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] = Set<String>();
|
||||
}
|
||||
|
||||
// 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<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
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
// Start with nodes that have no dependencies (in-degree = 0)
|
||||
List<String> nodesWithNoDependencies = [];
|
||||
for (final node in inDegree.keys) {
|
||||
if (inDegree[node] == 0) {
|
||||
nodesWithNoDependencies.add(node);
|
||||
}
|
||||
}
|
||||
|
||||
// Result will store the topological order
|
||||
List<String> 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<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() {
|
||||
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<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;
|
||||
@@ -255,11 +530,105 @@ class ConfigFile {
|
||||
final modsElement = modConfigData.findElements("activeMods").first;
|
||||
print('Found activeMods element.');
|
||||
|
||||
final mods = modsElement.findElements("li");
|
||||
print('Found ${mods.length} active mods.');
|
||||
final modElements = modsElement.findElements("li");
|
||||
print('Found ${modElements.length} active mods.');
|
||||
|
||||
for (final mod in mods) {
|
||||
// print('Mod found: ${mod.innerText}');
|
||||
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<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;
|
||||
|
||||
print(
|
||||
"Load order fixed. ${mods.length} mods are now in dependency-sorted order.",
|
||||
);
|
||||
}
|
||||
}
|
||||
|
Reference in New Issue
Block a user