From 2cd9d585e645275838907af1df7ae7a9f2955f1b Mon Sep 17 00:00:00 2001 From: PhatPhuckDave Date: Sun, 16 Mar 2025 12:52:55 +0100 Subject: [PATCH] Implement everything... --- lib/mod_list.dart | 201 ++++++++++++++++++++++++++++++++++++++++ test/mod_list_test.dart | 6 +- 2 files changed, 204 insertions(+), 3 deletions(-) diff --git a/lib/mod_list.dart b/lib/mod_list.dart index 3e52502..1c7809b 100644 --- a/lib/mod_list.dart +++ b/lib/mod_list.dart @@ -194,6 +194,207 @@ class ModList { }); return sortedMods; } + + List> checkIncompatibilities() { + List> conflicts = []; + List activeModIds = activeMods.keys.toList(); + + // Only check each pair once + for (int i = 0; i < activeModIds.length; i++) { + String modId = activeModIds[i]; + Mod mod = mods[modId]!; + + for (String incompId in mod.incompatibilities) { + // Only process if other mod is active and we haven't checked this pair yet + if (activeMods.containsKey(incompId) && modId.compareTo(incompId) < 0) { + conflicts.add([modId, incompId]); + } + } + } + return conflicts; + } + + /// Generate a load order for active mods + List generateLoadOrder() { + // Check for incompatibilities first + List> conflicts = checkIncompatibilities(); + if (conflicts.isNotEmpty) { + throw Exception( + "Incompatible mods selected: ${conflicts.map((c) => "${c[0]} and ${c[1]}").join(', ')}", + ); + } + + // Reset all marks for topological sort + for (Mod mod in mods.values) { + mod.visited = false; + mod.mark = false; + mod.position = -1; + } + + List result = []; + int position = 0; + + // Topological sort + void visit(Mod mod) { + if (mod.mark) { + List cyclePath = + mods.values.where((m) => m.mark).map((m) => m.name).toList(); + throw Exception( + "Cyclic dependency detected: ${cyclePath.join(' -> ')}", + ); + } + + if (!mod.visited) { + mod.mark = true; + + // Visit all dependencies + for (String depId in mod.dependencies) { + if (activeMods.containsKey(depId)) { + visit(mods[depId]!); + } + } + + mod.mark = false; + mod.visited = true; + mod.position = position++; + result.add(mod.id); + } + } + + // Visit all nodes + for (Mod mod in mods.values) { + if (!mod.visited) { + visit(mod); + } + } + + // Optimize for soft constraints + return _optimizeSoftConstraints(result); + } + + /// Calculate how many soft constraints are satisfied + Map _calculateSoftConstraintsScore(List order) { + Map positions = {}; + for (int i = 0; i < order.length; i++) { + positions[order[i]] = i; + } + + int satisfied = 0; + int total = 0; + + for (String modId in order) { + Mod mod = mods[modId]!; + + // Check "load before" preferences + for (String beforeId in mod.loadBefore) { + if (positions.containsKey(beforeId)) { + total++; + if (positions[modId]! < positions[beforeId]!) { + satisfied++; + } + } + } + + // Check "load after" preferences + for (String afterId in mod.loadAfter) { + if (positions.containsKey(afterId)) { + total++; + if (positions[modId]! > positions[afterId]!) { + satisfied++; + } + } + } + } + + return {'satisfied': satisfied, 'total': total}; + } + + /// Optimize for soft constraints using a greedy approach + List _optimizeSoftConstraints( + List initialOrder, { + int maxIterations = 5, + }) { + List bestOrder = List.from(initialOrder); + Map scoreInfo = _calculateSoftConstraintsScore(bestOrder); + int bestScore = scoreInfo['satisfied']!; + int total = scoreInfo['total']!; + + if (total == 0 || bestScore == total) { + return bestOrder; // All constraints satisfied or no constraints + } + + // Use a limited number of improvement passes + for (int iteration = 0; iteration < maxIterations; iteration++) { + bool improved = false; + + // Try moving each mod to improve score + for (int i = 0; i < bestOrder.length; i++) { + String modId = bestOrder[i]; + Mod mod = mods[modId]!; + + // Calculate current local score for this mod + Map currentPositions = {}; + for (int idx = 0; idx < bestOrder.length; idx++) { + currentPositions[bestOrder[idx]] = idx; + } + + // Try moving this mod to different positions + for (int newPos = 0; newPos < bestOrder.length; newPos++) { + if (newPos == i) continue; + + // Skip if move would break hard dependencies + bool skip = false; + if (newPos < i) { + // Moving earlier + // Check if any mod between newPos and i depends on this mod + for (int j = newPos; j < i; j++) { + String depModId = bestOrder[j]; + if (mods[depModId]!.dependencies.contains(modId)) { + skip = true; + break; + } + } + } else { + // Moving later + // Check if this mod depends on any mod between i and newPos + for (int j = i + 1; j <= newPos; j++) { + String depModId = bestOrder[j]; + if (mod.dependencies.contains(depModId)) { + skip = true; + break; + } + } + } + + if (skip) continue; + + // Create a new order with the mod moved + List newOrder = List.from(bestOrder); + newOrder.removeAt(i); + newOrder.insert(newPos, modId); + + // Calculate new score + Map newScoreInfo = _calculateSoftConstraintsScore( + newOrder, + ); + int newScore = newScoreInfo['satisfied']!; + + if (newScore > bestScore) { + bestScore = newScore; + bestOrder = newOrder; + improved = true; + break; // Break inner loop, move to next mod + } + } + + if (improved) break; // If improved, start a new iteration + } + + if (!improved) break; // If no improvements in this pass, stop + } + + return bestOrder; + } } String _expansionNameFromId(String id) { diff --git a/test/mod_list_test.dart b/test/mod_list_test.dart index 7a2cfc5..169f9b3 100644 --- a/test/mod_list_test.dart +++ b/test/mod_list_test.dart @@ -15,10 +15,10 @@ void main() { path: '', versions: ["1.5"], description: '', - hardDependencies: [], + dependencies: [], loadAfter: [], loadBefore: [], - incompatabilities: [], + incompatibilities: [], size: 0, isBaseGame: false, isExpansion: false, @@ -57,7 +57,7 @@ void main() { for (final mod in dummyMods.keys) { dummyList.activeMods[mod] = true; } - final sortedMods = dummyList.sort(); + final sortedMods = dummyList.generateLoadOrder(); group('Test sorting', () { test('Harmony should load before RimWorld', () {