import 'package:rimworld_modman/mod.dart'; import 'package:rimworld_modman/mod_list.dart'; import 'package:test/test.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'; Map generateDummyMods() { final dummyMod = Mod( name: 'Dummy Mod', id: 'dummy', path: '', versions: ["1.5"], description: '', dependencies: [], loadAfter: [], loadBefore: [], incompatibilities: [], size: 0, isBaseGame: false, isExpansion: false, enabled: false, ); final dummyMods = { 'harmony': dummyMod.copyWith( name: 'Harmony', id: 'harmony', loadBefore: ["ludeon.rimworld"], size: 47, ), 'prepatcher': dummyMod.copyWith( name: 'Prepatcher', id: 'prepatcher', loadAfter: ["ludeon.rimworld"], dependencies: ["harmony"], size: 47, ), 'ludeon.rimworld': dummyMod.copyWith( name: 'RimWorld', id: 'ludeon.rimworld', isBaseGame: true, ), 'ludeon.rimworld.anomaly': dummyMod.copyWith( name: 'RimWorld Anomaly', id: 'ludeon.rimworld.anomaly', isExpansion: true, ), 'disabledDummy': dummyMod.copyWith( name: 'Disabled Dummy', id: 'disabledDummy', ), 'yuuuge': dummyMod.copyWith(name: 'Yuuuge', id: 'yuuuge', size: 1000000), 'smol': dummyMod.copyWith(name: 'Smol', id: 'smol', size: 1), 'incompatible': dummyMod.copyWith( name: 'Incompatible', id: 'incompatible', size: 1, incompatibilities: ['harmony'], ), }; return dummyMods; } void main() { final dummyMods = generateDummyMods(); final dummyList = ModList(); dummyList.mods = dummyMods; dummyList.enableAll(); dummyList.setEnabled('disabledDummy', false); dummyList.setEnabled('incompatible', false); final sortedMods = dummyList.generateLoadOrder(); group('Test sorting', () { test('Harmony should load before RimWorld', () { final harmonyIndex = sortedMods.indexOf('harmony'); final rimworldIndex = sortedMods.indexOf('ludeon.rimworld'); expect(harmonyIndex, lessThan(rimworldIndex)); }); test('Prepatcher should load after Harmony and RimWorld', () { final prepatcherIndex = sortedMods.indexOf('prepatcher'); final harmonyIndex = sortedMods.indexOf('harmony'); final rimworldIndex = sortedMods.indexOf('ludeon.rimworld'); expect(prepatcherIndex, greaterThan(harmonyIndex)); expect(prepatcherIndex, greaterThan(rimworldIndex)); }); test('RimWorld should load before Anomaly', () { final rimworldIndex = sortedMods.indexOf('ludeon.rimworld'); final anomalyIndex = sortedMods.indexOf('ludeon.rimworld.anomaly'); expect(rimworldIndex, lessThan(anomalyIndex)); }); test('Disabled dummy mod should not be loaded', () { final disabledIndex = sortedMods.indexOf('disabledDummy'); expect(disabledIndex, isNegative); }); test('Larger mods should load before smaller ones', () { final smolIndex = sortedMods.indexOf('smol'); final yuuugeIndex = sortedMods.indexOf('yuuuge'); expect(yuuugeIndex, lessThan(smolIndex)); }); test('Incompatible mods should throw exception', () { dummyList.setEnabled('incompatible', true); expect(() => dummyList.generateLoadOrder(), throwsException); }); }); final dummyMods2 = generateDummyMods(); final dummyList2 = ModList(); dummyList2.mods = dummyMods2; dummyList2.disableAll(); dummyList2.setEnabled('prepatcher', true); final sortedMods2 = dummyList2.loadRequired(); group('Test loadRequired', () { test('Dependencies should be automatically enabled', () { final harmonyIndex = sortedMods2.indexOf('harmony'); expect(harmonyIndex, isNot(-1)); }); test('Only required mods should be enabled', () { for (final mod in dummyMods2.keys) { if (mod != 'prepatcher' && mod != 'harmony') { expect(sortedMods2.indexOf(mod), isNegative); } } }); test('Incompatible mods should throw exception', () { dummyList2.setEnabled('incompatible', true); expect(() => dummyList2.loadRequired(), throwsException); }); }); group('Test cyclic dependencies', () { test('Cyclic dependencies should throw exception', () { final cyclicMods = generateDummyMods(); // Create a cyclic dependency: A -> B -> C -> A cyclicMods['modA'] = cyclicMods['smol']!.copyWith( name: 'Mod A', id: 'modA', dependencies: ['modB'], ); cyclicMods['modB'] = cyclicMods['smol']!.copyWith( name: 'Mod B', id: 'modB', dependencies: ['modC'], ); cyclicMods['modC'] = cyclicMods['smol']!.copyWith( name: 'Mod C', id: 'modC', dependencies: ['modA'], ); final list = ModList(); list.mods = cyclicMods; list.enableAll(); expect(() => list.generateLoadOrder(), throwsException); }); }); group('Test soft constraints', () { test('Load preferences should be respected when possible', () { final softConstraintMods = generateDummyMods(); softConstraintMods['modA'] = softConstraintMods['smol']!.copyWith( name: 'Mod A', id: 'modA', loadAfter: ['modB'], loadBefore: ['modC'], ); softConstraintMods['modB'] = softConstraintMods['smol']!.copyWith( name: 'Mod B', id: 'modB', ); softConstraintMods['modC'] = softConstraintMods['smol']!.copyWith( name: 'Mod C', id: 'modC', ); final list = ModList(); list.mods = softConstraintMods; list.enableAll(); final order = list.generateLoadOrder(); final aIndex = order.indexOf('modA'); final bIndex = order.indexOf('modB'); final cIndex = order.indexOf('modC'); expect(aIndex, greaterThan(bIndex)); expect(aIndex, lessThan(cIndex)); }); }); group('Test conflict detection', () { test('All conflicts should be correctly identified', () { final incompatibleMods = generateDummyMods(); incompatibleMods['modA'] = incompatibleMods['smol']!.copyWith( name: 'Mod A', id: 'modA', incompatibilities: ['modB', 'modC'], ); incompatibleMods['modB'] = incompatibleMods['smol']!.copyWith( name: 'Mod B', id: 'modB', ); incompatibleMods['modC'] = incompatibleMods['smol']!.copyWith( name: 'Mod C', id: 'modC', ); final list = ModList(); list.mods = incompatibleMods; list.enableAll(); final conflicts = list.checkIncompatibilities(); 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, ); }); }); group('Test enable/disable functionality', () { test('Enable and disable methods should work correctly', () { final testMods = generateDummyMods(); final list = ModList(); list.mods = testMods; list.enableAll(); for (final mod in list.mods.values) { expect(mod.enabled, isTrue); } list.disableAll(); for (final mod in list.mods.values) { expect(mod.enabled, isFalse); } }); }); group('Test base game and expansion handling', () { test('Base game and expansions should be correctly ordered', () { final gameMods = generateDummyMods(); final list = ModList(); list.mods = gameMods; list.enableAll(); list.setEnabled('incompatible', false); // Avoid exception final order = list.generateLoadOrder(); // Base game should load before any expansions final baseGameIndex = order.indexOf('ludeon.rimworld'); final expansionIndex = order.indexOf('ludeon.rimworld.anomaly'); expect(baseGameIndex, lessThan(expansionIndex)); // Normal mods should come after base game (except harmony) for (final modId in order) { if (modId != 'ludeon.rimworld' && modId != 'harmony' && !modId.startsWith('ludeon.rimworld.')) { expect(order.indexOf(modId), greaterThan(baseGameIndex)); } } }); }); group('Test complex dependency chains', () { test('Complex dependency chains should resolve correctly', () { final complexMods = generateDummyMods(); // Create a chain: A -> B -> C -> D complexMods['modA'] = complexMods['smol']!.copyWith( name: 'Mod A', id: 'modA', dependencies: ['modB'], ); complexMods['modB'] = complexMods['smol']!.copyWith( name: 'Mod B', id: 'modB', dependencies: ['modC'], ); complexMods['modC'] = complexMods['smol']!.copyWith( name: 'Mod C', id: 'modC', dependencies: ['modD'], ); complexMods['modD'] = complexMods['smol']!.copyWith( name: 'Mod D', id: 'modD', ); final list = ModList(); list.mods = complexMods; list.disableAll(); list.setEnabled('modA', true); final result = list.loadRequired(); // All mods in the chain should be enabled expect(result.contains('modA'), isTrue); expect(result.contains('modB'), isTrue); expect(result.contains('modC'), isTrue); expect(result.contains('modD'), isTrue); // The order should be D -> C -> B -> A expect(result.indexOf('modD'), lessThan(result.indexOf('modC'))); expect(result.indexOf('modC'), lessThan(result.indexOf('modB'))); expect(result.indexOf('modB'), lessThan(result.indexOf('modA'))); }); }); group('Test constraint prioritization', () { test('Hard dependencies should override soft constraints', () { final constraintMods = generateDummyMods(); // A depends on B but wants to load before it (impossible) constraintMods['modA'] = constraintMods['smol']!.copyWith( name: 'Mod A', id: 'modA', dependencies: ['modB'], loadBefore: ['modB'], ); constraintMods['modB'] = constraintMods['smol']!.copyWith( name: 'Mod B', id: 'modB', ); final list = ModList(); list.mods = constraintMods; list.enableAll(); final order = list.generateLoadOrder(); // Hard dependency must win expect(order.indexOf('modB'), lessThan(order.indexOf('modA'))); }); }); }