Rework everything to be less dogshit
This commit is contained in:
@@ -31,9 +31,8 @@ class Mod {
|
||||
final bool isBaseGame; // Is this the base RimWorld game
|
||||
final bool isExpansion; // Is this a RimWorld expansion
|
||||
|
||||
bool visited = false;
|
||||
bool mark = false;
|
||||
int position = -1;
|
||||
int loadBeforeNotPlaced = 0;
|
||||
int loadAfterPlaced = 0;
|
||||
|
||||
Mod({
|
||||
required this.name,
|
||||
|
@@ -1,5 +1,6 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:rimworld_modman/logger.dart';
|
||||
import 'package:rimworld_modman/mod.dart';
|
||||
import 'package:xml/xml.dart';
|
||||
@@ -22,6 +23,21 @@ class ModList {
|
||||
|
||||
ModList({this.configPath = '', this.modsPath = ''});
|
||||
|
||||
ModList copyWith({
|
||||
String? configPath,
|
||||
String? modsPath,
|
||||
Map<String, Mod>? mods,
|
||||
Map<String, bool>? activeMods,
|
||||
}) {
|
||||
final newModlist = ModList(
|
||||
configPath: configPath ?? this.configPath,
|
||||
modsPath: modsPath ?? this.modsPath,
|
||||
);
|
||||
newModlist.mods = Map.from(mods ?? this.mods);
|
||||
newModlist.activeMods = Map.from(activeMods ?? this.activeMods);
|
||||
return newModlist;
|
||||
}
|
||||
|
||||
Stream<Mod> loadAvailable() async* {
|
||||
final logger = Logger.instance;
|
||||
final stopwatch = Stopwatch()..start();
|
||||
@@ -219,439 +235,172 @@ class ModList {
|
||||
}
|
||||
}
|
||||
|
||||
List<List<String>> checkIncompatibilities(List<String> activeModIds) {
|
||||
List<List<String>> conflicts = [];
|
||||
//LoadOrder loadRequired([LoadOrder? loadOrder]) {
|
||||
// loadOrder ??= LoadOrder();
|
||||
// final toEnable = <String>[];
|
||||
// for (final modid in activeMods.keys) {
|
||||
// loadDependencies(modid, loadOrder, toEnable);
|
||||
// }
|
||||
// for (final modid in toEnable) {
|
||||
// setEnabled(modid, true);
|
||||
// }
|
||||
// return generateLoadOrder(loadOrder);
|
||||
//}
|
||||
|
||||
// Only check each pair once
|
||||
for (final modId in activeModIds) {
|
||||
final mod = mods[modId]!;
|
||||
LoadOrder generateLoadOrder() {
|
||||
final modMap = {for (final m in mods.values) m.id: m};
|
||||
_validateIncompatibilities(mods.values.toList());
|
||||
|
||||
for (final incompId in mod.incompatibilities) {
|
||||
// Only process if other mod is active and we haven't checked this pair yet
|
||||
if (activeMods.containsKey(incompId)) {
|
||||
conflicts.add([modId, incompId]);
|
||||
}
|
||||
}
|
||||
}
|
||||
return conflicts;
|
||||
// Hard dependency graph
|
||||
final inDegree = <String, int>{};
|
||||
final adjacency = <String, List<String>>{};
|
||||
|
||||
// Soft constraint reverse mappings
|
||||
final reverseLoadBefore = <String, List<Mod>>{};
|
||||
final reverseLoadAfter = <String, List<Mod>>{};
|
||||
|
||||
// Initialize data structures
|
||||
for (final mod in mods.values) {
|
||||
mod.loadBeforeNotPlaced = mod.loadBefore.length;
|
||||
mod.loadAfterPlaced = 0;
|
||||
|
||||
reverseLoadBefore[mod.id] = [];
|
||||
reverseLoadAfter[mod.id] = [];
|
||||
inDegree[mod.id] = 0;
|
||||
adjacency[mod.id] = [];
|
||||
}
|
||||
|
||||
/// Generate a load order for active mods
|
||||
LoadOrder generateLoadOrder([LoadOrder? loadOrder]) {
|
||||
loadOrder ??= LoadOrder();
|
||||
// Check for incompatibilities first
|
||||
final conflicts = checkIncompatibilities(activeMods.keys.toList());
|
||||
if (conflicts.isNotEmpty) {
|
||||
for (final conflict in conflicts) {
|
||||
loadOrder.errors.add(
|
||||
"Incompatible mods selected: ${conflict[0]} and ${conflict[1]}",
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Check for missing dependencies
|
||||
for (final modId in activeMods.keys) {
|
||||
final mod = mods[modId]!;
|
||||
// Build dependency graph and reverse soft constraints
|
||||
for (final mod in mods.values) {
|
||||
for (final depId in mod.dependencies) {
|
||||
if (!mods.containsKey(depId)) {
|
||||
loadOrder.errors.add(
|
||||
"Missing dependency: ${mod.name} requires mod with ID $depId",
|
||||
);
|
||||
adjacency[depId]!.add(mod.id);
|
||||
inDegree[mod.id] = (inDegree[mod.id] ?? 0) + 1;
|
||||
}
|
||||
|
||||
for (final targetId in mod.loadBefore) {
|
||||
final target = modMap[targetId];
|
||||
if (target != null) {
|
||||
reverseLoadBefore[targetId]!.add(mod);
|
||||
}
|
||||
}
|
||||
|
||||
for (final targetId in mod.loadAfter) {
|
||||
final target = modMap[targetId];
|
||||
if (target != null) {
|
||||
reverseLoadAfter[targetId]!.add(mod);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Reset all marks for topological sort
|
||||
for (final mod in mods.values) {
|
||||
mod.visited = false;
|
||||
mod.mark = false;
|
||||
mod.position = -1;
|
||||
final heap = PriorityQueue<Mod>((a, b) {
|
||||
// 1. Base game first
|
||||
if (a.isBaseGame != b.isBaseGame) {
|
||||
return a.isBaseGame ? -1 : 1;
|
||||
}
|
||||
|
||||
int position = 0;
|
||||
|
||||
// Topological sort
|
||||
void visit(Mod mod, LoadOrder loadOrder) {
|
||||
if (!mod.enabled) {
|
||||
mod.visited = true;
|
||||
return;
|
||||
// 2. Expansions next
|
||||
if (a.isExpansion != b.isExpansion) {
|
||||
return a.isExpansion ? -1 : 1;
|
||||
}
|
||||
if (mod.mark) {
|
||||
final cyclePath =
|
||||
mods.values.where((m) => m.mark).map((m) => m.name).toList();
|
||||
loadOrder.errors.add(
|
||||
"Cyclic dependency detected: ${cyclePath.join(' -> ')}",
|
||||
);
|
||||
return;
|
||||
// 3. Soft constraints: Prioritize mods that need to be placed earlier
|
||||
final aUnmetBefore = a.loadBeforeNotPlaced;
|
||||
final bUnmetBefore = b.loadBeforeNotPlaced;
|
||||
if (aUnmetBefore != bUnmetBefore) {
|
||||
return bUnmetBefore.compareTo(aUnmetBefore); // Higher unmetBefore first
|
||||
}
|
||||
|
||||
if (!mod.visited) {
|
||||
mod.mark = true;
|
||||
|
||||
// Visit all dependencies
|
||||
for (String depId in mod.dependencies) {
|
||||
if (activeMods.containsKey(depId)) {
|
||||
visit(mods[depId]!, loadOrder);
|
||||
// If tied, deprioritize mods with more unmet `loadAfter`
|
||||
final aUnmetAfter = a.loadAfter.length - a.loadAfterPlaced;
|
||||
final bUnmetAfter = b.loadAfter.length - b.loadAfterPlaced;
|
||||
if (aUnmetAfter != bUnmetAfter) {
|
||||
return aUnmetAfter.compareTo(bUnmetAfter); // Lower unmetAfter first
|
||||
}
|
||||
}
|
||||
|
||||
mod.mark = false;
|
||||
mod.visited = true;
|
||||
mod.position = position++;
|
||||
loadOrder.loadOrder.add(mod.id);
|
||||
}
|
||||
}
|
||||
|
||||
// Visit all nodes
|
||||
for (final mod in mods.values) {
|
||||
if (!mod.visited) {
|
||||
visit(mod, loadOrder);
|
||||
}
|
||||
}
|
||||
|
||||
// Optimize for soft constraints
|
||||
_optimizeSoftConstraints(loadOrder: loadOrder);
|
||||
for (final modId in loadOrder.loadOrder) {
|
||||
final mod = mods[modId]!;
|
||||
print(
|
||||
'Mod ID: ${mod.id}, Name: ${mod.name}, Enabled: ${mod.enabled}, Size: ${mod.size}, Dependencies: ${mod.dependencies}, Load After: ${mod.loadAfter}, Load Before: ${mod.loadBefore}, Incompatibilities: ${mod.incompatibilities}',
|
||||
);
|
||||
}
|
||||
return loadOrder;
|
||||
}
|
||||
|
||||
/// Calculate how many soft constraints are satisfied
|
||||
Map<String, int> _calculateSoftConstraintsScore(List<String> order) {
|
||||
Map<String, int> 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
|
||||
LoadOrder _optimizeSoftConstraints({
|
||||
int maxIterations = 5,
|
||||
LoadOrder? loadOrder,
|
||||
}) {
|
||||
loadOrder ??= LoadOrder();
|
||||
|
||||
// First, ensure base game and expansions are at the beginning in the correct order
|
||||
List<String> baseAndExpansions = [];
|
||||
List<String> harmony = [];
|
||||
List<String> otherMods = [];
|
||||
|
||||
// Separate mods into categories
|
||||
for (final modId in loadOrder.loadOrder) {
|
||||
final mod = mods[modId]!;
|
||||
if (modId == 'brrainz.harmony') {
|
||||
harmony.add(modId);
|
||||
} else if (mod.isBaseGame) {
|
||||
baseAndExpansions.add(modId);
|
||||
} else if (mod.isExpansion) {
|
||||
baseAndExpansions.add(modId);
|
||||
} else {
|
||||
otherMods.add(modId);
|
||||
}
|
||||
}
|
||||
|
||||
// Sort expansions to ensure correct order
|
||||
baseAndExpansions.sort((a, b) {
|
||||
final modA = mods[a]!;
|
||||
final modB = mods[b]!;
|
||||
|
||||
// Base game always first
|
||||
if (modA.isBaseGame) return -1;
|
||||
if (modB.isBaseGame) return 1;
|
||||
|
||||
// Sort expansions alphabetically by ID (which should work for Ludeon expansions)
|
||||
return a.compareTo(b);
|
||||
// 4. Smaller size last
|
||||
return b.size.compareTo(a.size);
|
||||
});
|
||||
|
||||
// Combine the lists with harmony first, then base game and expansions, then other mods
|
||||
loadOrder.loadOrder.clear();
|
||||
loadOrder.loadOrder.addAll(harmony);
|
||||
loadOrder.loadOrder.addAll(baseAndExpansions);
|
||||
// Initialize heap with available mods
|
||||
for (final modId in activeMods.keys) {
|
||||
final mod = modMap[modId];
|
||||
if (mod != null && inDegree[modId] == 0) {
|
||||
heap.add(mod);
|
||||
}
|
||||
}
|
||||
|
||||
// Now apply the normal optimization for the remaining mods
|
||||
List<String> remainingMods = otherMods;
|
||||
final sortedMods = <Mod>[];
|
||||
while (heap.isNotEmpty) {
|
||||
final current = heap.removeFirst();
|
||||
sortedMods.add(current);
|
||||
|
||||
Map<String, int> scoreInfo = _calculateSoftConstraintsScore(remainingMods);
|
||||
int bestScore = scoreInfo['satisfied']!;
|
||||
int total = scoreInfo['total']!;
|
||||
// Update dependents' in-degree
|
||||
for (final neighborId in adjacency[current.id]!) {
|
||||
inDegree[neighborId] = inDegree[neighborId]! - 1;
|
||||
if (inDegree[neighborId] == 0) {
|
||||
heap.add(modMap[neighborId]!);
|
||||
}
|
||||
}
|
||||
|
||||
if (total == 0 || bestScore == total) {
|
||||
// All constraints satisfied or no constraints for remaining mods, sort by size where possible
|
||||
_sortSizeWithinConstraints(loadOrder: loadOrder, modList: remainingMods);
|
||||
loadOrder.loadOrder.addAll(remainingMods);
|
||||
// Update soft constraints
|
||||
_updateReverseConstraints(
|
||||
current,
|
||||
reverseLoadBefore,
|
||||
sortedMods,
|
||||
heap,
|
||||
(mod) => mod.loadBeforeNotPlaced--,
|
||||
);
|
||||
_updateReverseConstraints(
|
||||
current,
|
||||
reverseLoadAfter,
|
||||
sortedMods,
|
||||
heap,
|
||||
(mod) => mod.loadAfterPlaced++,
|
||||
);
|
||||
}
|
||||
|
||||
if (sortedMods.length != mods.length) {
|
||||
throw Exception("Cyclic dependencies detected");
|
||||
}
|
||||
|
||||
final loadOrder = LoadOrder();
|
||||
loadOrder.loadOrder.addAll(sortedMods.map((e) => e.id));
|
||||
return loadOrder;
|
||||
}
|
||||
|
||||
// Use a limited number of improvement passes for the remaining mods
|
||||
for (int iteration = 0; iteration < maxIterations; iteration++) {
|
||||
bool improved = false;
|
||||
|
||||
// Try moving each mod to improve score
|
||||
for (int i = 0; i < remainingMods.length; i++) {
|
||||
String modId = remainingMods[i];
|
||||
Mod mod = mods[modId]!;
|
||||
|
||||
// Calculate current local score for this mod
|
||||
Map<String, int> currentPositions = {};
|
||||
for (int idx = 0; idx < remainingMods.length; idx++) {
|
||||
currentPositions[remainingMods[idx]] = idx;
|
||||
void _validateIncompatibilities(List<Mod> mods) {
|
||||
final enabledMods = mods.where((m) => m.enabled).toList();
|
||||
for (final mod in enabledMods) {
|
||||
for (final incompatibleId in mod.incompatibilities) {
|
||||
if (enabledMods.any((m) => m.id == incompatibleId)) {
|
||||
throw Exception("Conflict: ${mod.id} vs $incompatibleId");
|
||||
}
|
||||
|
||||
// Try moving this mod to different positions
|
||||
for (int newPos = 0; newPos < remainingMods.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 = remainingMods[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 = remainingMods[j];
|
||||
if (mod.dependencies.contains(depModId)) {
|
||||
skip = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (skip) continue;
|
||||
|
||||
// Create a new order with the mod moved
|
||||
List<String> newOrder = List.from(remainingMods);
|
||||
newOrder.removeAt(i);
|
||||
newOrder.insert(newPos, modId);
|
||||
|
||||
// Calculate new score
|
||||
Map<String, int> newScoreInfo = _calculateSoftConstraintsScore(
|
||||
newOrder,
|
||||
);
|
||||
int newScore = newScoreInfo['satisfied']!;
|
||||
|
||||
if (newScore > bestScore) {
|
||||
bestScore = newScore;
|
||||
remainingMods.clear();
|
||||
remainingMods.addAll(newOrder);
|
||||
improved = true;
|
||||
break; // Break inner loop, move to next mod
|
||||
void _updateReverseConstraints(
|
||||
Mod current,
|
||||
Map<String, List<Mod>> reverseMap,
|
||||
List<Mod> sortedMods,
|
||||
PriorityQueue<Mod> heap,
|
||||
void Function(Mod) update,
|
||||
) {
|
||||
reverseMap[current.id]?.forEach((affectedMod) {
|
||||
if (!sortedMods.contains(affectedMod)) {
|
||||
update(affectedMod);
|
||||
// If mod is already in heap, re-add to update position
|
||||
if (heap.contains(affectedMod)) {
|
||||
heap.remove(affectedMod);
|
||||
heap.add(affectedMod);
|
||||
}
|
||||
}
|
||||
|
||||
if (improved) break; // If improved, start a new iteration
|
||||
});
|
||||
}
|
||||
|
||||
if (!improved) break; // If no improvements in this pass, stop
|
||||
LoadOrder loadRequired() {
|
||||
final loadOrder = generateLoadOrder();
|
||||
for (final modId in loadOrder.loadOrder) {
|
||||
setEnabled(modId, true);
|
||||
}
|
||||
|
||||
// Sort by size where possible for the remaining mods
|
||||
_sortSizeWithinConstraints(loadOrder: loadOrder, modList: remainingMods);
|
||||
loadOrder.loadOrder.addAll(remainingMods);
|
||||
|
||||
return loadOrder;
|
||||
}
|
||||
|
||||
/// Sort mods by size within compatible groups
|
||||
LoadOrder _sortSizeWithinConstraints({
|
||||
LoadOrder? loadOrder,
|
||||
List<String>? modList,
|
||||
}) {
|
||||
loadOrder ??= LoadOrder();
|
||||
List<String> modsToSort = modList ?? loadOrder.loadOrder;
|
||||
|
||||
// Find groups of mods that can be reordered without breaking constraints
|
||||
List<List<String>> groups = [];
|
||||
List<String> currentGroup = [];
|
||||
|
||||
for (int i = 0; i < modsToSort.length; i++) {
|
||||
String modId = modsToSort[i];
|
||||
Mod mod = mods[modId]!;
|
||||
|
||||
if (currentGroup.isEmpty) {
|
||||
currentGroup.add(modId);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Check if this mod can join the current group
|
||||
bool canJoin = true;
|
||||
for (String groupModId in currentGroup) {
|
||||
Mod groupMod = mods[groupModId]!;
|
||||
|
||||
// Check hard dependencies
|
||||
if (mod.dependencies.contains(groupModId) ||
|
||||
groupMod.dependencies.contains(modId)) {
|
||||
canJoin = false;
|
||||
break;
|
||||
}
|
||||
|
||||
// Check soft constraints
|
||||
if (mod.loadAfter.contains(groupModId) ||
|
||||
groupMod.loadBefore.contains(modId) ||
|
||||
mod.loadBefore.contains(groupModId) ||
|
||||
groupMod.loadAfter.contains(modId)) {
|
||||
canJoin = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (canJoin) {
|
||||
currentGroup.add(modId);
|
||||
} else {
|
||||
// Start a new group
|
||||
if (currentGroup.isNotEmpty) {
|
||||
groups.add(List.from(currentGroup));
|
||||
currentGroup = [modId];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Add the last group if not empty
|
||||
if (currentGroup.isNotEmpty) {
|
||||
groups.add(currentGroup);
|
||||
}
|
||||
|
||||
// Sort each group by size
|
||||
for (List<String> group in groups) {
|
||||
if (group.length > 1) {
|
||||
group.sort((a, b) => mods[b]!.size.compareTo(mods[a]!.size));
|
||||
}
|
||||
}
|
||||
|
||||
// Reconstruct the order
|
||||
modsToSort.clear();
|
||||
for (List<String> group in groups) {
|
||||
modsToSort.addAll(group);
|
||||
}
|
||||
|
||||
// If we were given the loadOrder directly, update it
|
||||
if (modList == null) {
|
||||
loadOrder.loadOrder.clear();
|
||||
loadOrder.loadOrder.addAll(modsToSort);
|
||||
}
|
||||
|
||||
return loadOrder;
|
||||
}
|
||||
|
||||
LoadOrder loadDependencies(
|
||||
String modId, [
|
||||
LoadOrder? loadOrder,
|
||||
List<String>? toEnable,
|
||||
Map<String, bool>? seen,
|
||||
List<String>? cyclePath,
|
||||
]) {
|
||||
final mod = mods[modId]!;
|
||||
loadOrder ??= LoadOrder();
|
||||
toEnable ??= <String>[];
|
||||
seen ??= <String, bool>{};
|
||||
cyclePath ??= <String>[];
|
||||
|
||||
// Add current mod to cycle path
|
||||
cyclePath.add(modId);
|
||||
|
||||
for (final dep in mod.dependencies) {
|
||||
if (!mods.containsKey(dep)) {
|
||||
loadOrder.errors.add(
|
||||
'Missing dependency: ${mod.name} requires mod with ID $dep',
|
||||
);
|
||||
continue;
|
||||
}
|
||||
final depMod = mods[dep]!;
|
||||
if (seen[dep] == true) {
|
||||
// Find the start of the cycle
|
||||
int cycleStart = cyclePath.indexOf(dep);
|
||||
if (cycleStart >= 0) {
|
||||
// Extract the cycle part
|
||||
List<String> cycleIds = [...cyclePath.sublist(cycleStart), modId];
|
||||
loadOrder.errors.add(
|
||||
'Cyclic dependency detected: ${cycleIds.join(' -> ')}',
|
||||
);
|
||||
} else {
|
||||
loadOrder.errors.add('Cyclic dependency detected: $modId -> $dep');
|
||||
}
|
||||
continue;
|
||||
}
|
||||
seen[dep] = true;
|
||||
toEnable.add(depMod.id);
|
||||
loadDependencies(
|
||||
depMod.id,
|
||||
loadOrder,
|
||||
toEnable,
|
||||
seen,
|
||||
List.from(cyclePath),
|
||||
);
|
||||
}
|
||||
|
||||
return loadOrder;
|
||||
}
|
||||
|
||||
LoadOrder loadRequired([LoadOrder? loadOrder]) {
|
||||
loadOrder ??= LoadOrder();
|
||||
final toEnable = <String>[];
|
||||
for (final modid in activeMods.keys) {
|
||||
loadDependencies(modid, loadOrder, toEnable);
|
||||
}
|
||||
for (final modid in toEnable) {
|
||||
setEnabled(modid, true);
|
||||
}
|
||||
return generateLoadOrder(loadOrder);
|
||||
}
|
||||
|
||||
ModList copyWith({
|
||||
String? configPath,
|
||||
String? modsPath,
|
||||
Map<String, Mod>? mods,
|
||||
Map<String, bool>? activeMods,
|
||||
}) {
|
||||
final newModlist = ModList(
|
||||
configPath: configPath ?? this.configPath,
|
||||
modsPath: modsPath ?? this.modsPath,
|
||||
);
|
||||
newModlist.mods = Map.from(mods ?? this.mods);
|
||||
newModlist.activeMods = Map.from(activeMods ?? this.activeMods);
|
||||
return newModlist;
|
||||
}
|
||||
}
|
||||
|
||||
String _expansionNameFromId(String id) {
|
||||
|
@@ -32,7 +32,11 @@ void main() {
|
||||
test('Harmony should load before RimWorld', () {
|
||||
final list = ModList();
|
||||
list.mods = {
|
||||
'harmony': makeDummy().copyWith(name: 'Harmony', id: 'harmony'),
|
||||
'harmony': makeDummy().copyWith(
|
||||
name: 'Harmony',
|
||||
id: 'harmony',
|
||||
loadBefore: ['ludeon.rimworld'],
|
||||
),
|
||||
'ludeon.rimworld': makeDummy().copyWith(
|
||||
name: 'RimWorld',
|
||||
id: 'ludeon.rimworld',
|
||||
@@ -40,11 +44,9 @@ void main() {
|
||||
};
|
||||
list.enableAll();
|
||||
final order = list.generateLoadOrder();
|
||||
|
||||
final harmonyIndex = order.loadOrder.indexOf('harmony');
|
||||
final rimworldIndex = order.loadOrder.indexOf('ludeon.rimworld');
|
||||
final expected = ['harmony', 'ludeon.rimworld'];
|
||||
expect(order.errors, isEmpty);
|
||||
expect(harmonyIndex, lessThan(rimworldIndex));
|
||||
expect(order.loadOrder, equals(expected));
|
||||
});
|
||||
|
||||
test('Prepatcher should load after Harmony and RimWorld', () {
|
||||
@@ -68,13 +70,9 @@ void main() {
|
||||
};
|
||||
list.enableAll();
|
||||
final order = list.generateLoadOrder();
|
||||
|
||||
final prepatcherIndex = order.loadOrder.indexOf('prepatcher');
|
||||
final harmonyIndex = order.loadOrder.indexOf('harmony');
|
||||
final rimworldIndex = order.loadOrder.indexOf('ludeon.rimworld');
|
||||
final expected = ['harmony', 'ludeon.rimworld', 'prepatcher'];
|
||||
expect(order.errors, isEmpty);
|
||||
expect(prepatcherIndex, greaterThan(harmonyIndex));
|
||||
expect(prepatcherIndex, greaterThan(rimworldIndex));
|
||||
expect(order.loadOrder, equals(expected));
|
||||
});
|
||||
|
||||
test('RimWorld should load before Anomaly', () {
|
||||
@@ -87,15 +85,14 @@ void main() {
|
||||
'ludeon.rimworld.anomaly': makeDummy().copyWith(
|
||||
name: 'RimWorld Anomaly',
|
||||
id: 'ludeon.rimworld.anomaly',
|
||||
dependencies: ['ludeon.rimworld'],
|
||||
),
|
||||
};
|
||||
list.enableAll();
|
||||
final order = list.generateLoadOrder();
|
||||
|
||||
final rimworldIndex = order.loadOrder.indexOf('ludeon.rimworld');
|
||||
final anomalyIndex = order.loadOrder.indexOf('ludeon.rimworld.anomaly');
|
||||
final expected = ['ludeon.rimworld', 'ludeon.rimworld.anomaly'];
|
||||
expect(order.errors, isEmpty);
|
||||
expect(rimworldIndex, lessThan(anomalyIndex));
|
||||
expect(order.loadOrder, equals(expected));
|
||||
});
|
||||
|
||||
test('Disabled dummy mod should not be loaded', () {
|
||||
@@ -108,9 +105,9 @@ void main() {
|
||||
};
|
||||
list.disableAll();
|
||||
final order = list.generateLoadOrder();
|
||||
|
||||
final disabledIndex = order.loadOrder.indexOf('disabledDummy');
|
||||
expect(disabledIndex, isNegative);
|
||||
final expected = [];
|
||||
expect(order.errors, isEmpty);
|
||||
expect(order.loadOrder, equals(expected));
|
||||
});
|
||||
|
||||
test('Larger mods should load before smaller ones', () {
|
||||
@@ -125,11 +122,9 @@ void main() {
|
||||
};
|
||||
list.enableAll();
|
||||
final order = list.generateLoadOrder();
|
||||
|
||||
final smolIndex = order.loadOrder.indexOf('smol');
|
||||
final yuuugeIndex = order.loadOrder.indexOf('yuuuge');
|
||||
final expected = ['yuuuge', 'smol'];
|
||||
expect(order.errors, isEmpty);
|
||||
expect(yuuugeIndex, lessThan(smolIndex));
|
||||
expect(order.loadOrder, equals(expected));
|
||||
});
|
||||
|
||||
test('Incompatible mods should return errors', () {
|
||||
@@ -332,43 +327,43 @@ void main() {
|
||||
});
|
||||
});
|
||||
|
||||
group('Test conflict detection', () {
|
||||
test('All conflicts should be correctly identified', () {
|
||||
final list = ModList();
|
||||
list.mods = {
|
||||
'modA': makeDummy().copyWith(
|
||||
name: 'Mod A',
|
||||
id: 'modA',
|
||||
incompatibilities: ['modB', 'modC'],
|
||||
),
|
||||
'modB': makeDummy().copyWith(name: 'Mod B', id: 'modB'),
|
||||
'modC': makeDummy().copyWith(name: 'Mod C', id: 'modC'),
|
||||
};
|
||||
list.enableAll();
|
||||
final conflicts = list.checkIncompatibilities(
|
||||
list.activeMods.keys.toList(),
|
||||
);
|
||||
expect(conflicts.length, equals(2));
|
||||
//group('Test conflict detection', () {
|
||||
// test('All conflicts should be correctly identified', () {
|
||||
// final list = ModList();
|
||||
// list.mods = {
|
||||
// 'modA': makeDummy().copyWith(
|
||||
// name: 'Mod A',
|
||||
// id: 'modA',
|
||||
// incompatibilities: ['modB', 'modC'],
|
||||
// ),
|
||||
// 'modB': makeDummy().copyWith(name: 'Mod B', id: 'modB'),
|
||||
// 'modC': makeDummy().copyWith(name: 'Mod C', id: 'modC'),
|
||||
// };
|
||||
// list.enableAll();
|
||||
// final conflicts = list.checkIncompatibilities(
|
||||
// list.activeMods.keys.toList(),
|
||||
// );
|
||||
// expect(conflicts.length, equals(2));
|
||||
|
||||
// Check if conflicts contain these pairs (order doesn't matter)
|
||||
expect(
|
||||
conflicts.any(
|
||||
(c) =>
|
||||
(c[0] == 'modA' && c[1] == 'modB') ||
|
||||
(c[0] == 'modB' && c[1] == 'modA'),
|
||||
),
|
||||
isTrue,
|
||||
);
|
||||
expect(
|
||||
conflicts.any(
|
||||
(c) =>
|
||||
(c[0] == 'modA' && c[1] == 'modC') ||
|
||||
(c[0] == 'modC' && c[1] == 'modA'),
|
||||
),
|
||||
isTrue,
|
||||
);
|
||||
});
|
||||
});
|
||||
// // Check if conflicts contain these pairs (order doesn't matter)
|
||||
// expect(
|
||||
// conflicts.any(
|
||||
// (c) =>
|
||||
// (c[0] == 'modA' && c[1] == 'modB') ||
|
||||
// (c[0] == 'modB' && c[1] == 'modA'),
|
||||
// ),
|
||||
// isTrue,
|
||||
// );
|
||||
// expect(
|
||||
// conflicts.any(
|
||||
// (c) =>
|
||||
// (c[0] == 'modA' && c[1] == 'modC') ||
|
||||
// (c[0] == 'modC' && c[1] == 'modA'),
|
||||
// ),
|
||||
// isTrue,
|
||||
// );
|
||||
// });
|
||||
//});
|
||||
|
||||
group('Test enable/disable functionality', () {
|
||||
test('Enable and disable methods should work correctly', () {
|
||||
|
Reference in New Issue
Block a user