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'; Mod makeDummy() { return Mod( name: 'Dummy Mod', id: 'dummy', path: '', versions: ["1.5"], description: '', dependencies: [], loadAfter: [], loadBefore: [], incompatibilities: [], size: 0, isBaseGame: false, isExpansion: false, enabled: false, ); } void main() { group('Test sorting', () { test('Harmony should load before RimWorld', () { final list = ModList(); list.mods = { 'harmony': makeDummy().copyWith(name: 'Harmony', id: 'harmony'), 'ludeon.rimworld': makeDummy().copyWith( name: 'RimWorld', id: 'ludeon.rimworld', ), }; list.enableAll(); final order = list.generateLoadOrder(); final harmonyIndex = order.indexOf('harmony'); final rimworldIndex = order.indexOf('ludeon.rimworld'); expect(harmonyIndex, lessThan(rimworldIndex)); }); test('Prepatcher should load after Harmony and RimWorld', () { final list = ModList(); list.mods = { 'prepatcher': makeDummy().copyWith( name: 'Prepatcher', id: 'prepatcher', dependencies: ['harmony'], loadAfter: ['ludeon.rimworld'], ), 'harmony': makeDummy().copyWith( name: 'Harmony', id: 'harmony', loadBefore: ['ludeon.rimworld'], ), 'ludeon.rimworld': makeDummy().copyWith( name: 'RimWorld', id: 'ludeon.rimworld', ), }; list.enableAll(); final order = list.generateLoadOrder(); final prepatcherIndex = order.indexOf('prepatcher'); final harmonyIndex = order.indexOf('harmony'); final rimworldIndex = order.indexOf('ludeon.rimworld'); expect(prepatcherIndex, greaterThan(harmonyIndex)); expect(prepatcherIndex, greaterThan(rimworldIndex)); }); test('RimWorld should load before Anomaly', () { final list = ModList(); list.mods = { 'ludeon.rimworld': makeDummy().copyWith( name: 'RimWorld', id: 'ludeon.rimworld', ), 'ludeon.rimworld.anomaly': makeDummy().copyWith( name: 'RimWorld Anomaly', id: 'ludeon.rimworld.anomaly', ), }; list.enableAll(); final order = list.generateLoadOrder(); final rimworldIndex = order.indexOf('ludeon.rimworld'); final anomalyIndex = order.indexOf('ludeon.rimworld.anomaly'); expect(rimworldIndex, lessThan(anomalyIndex)); }); test('Disabled dummy mod should not be loaded', () { final list = ModList(); list.mods = { 'disabledDummy': makeDummy().copyWith( name: 'Disabled Dummy', id: 'disabledDummy', ), }; list.disableAll(); final order = list.generateLoadOrder(); final disabledIndex = order.indexOf('disabledDummy'); expect(disabledIndex, isNegative); }); test('Larger mods should load before smaller ones', () { final list = ModList(); list.mods = { 'smol': makeDummy().copyWith(name: 'Smol', id: 'smol', size: 100), 'yuuuge': makeDummy().copyWith( name: 'Yuuuge', id: 'yuuuge', size: 10000, ), }; list.enableAll(); final order = list.generateLoadOrder(); final smolIndex = order.indexOf('smol'); final yuuugeIndex = order.indexOf('yuuuge'); expect(yuuugeIndex, lessThan(smolIndex)); }); test('Incompatible mods should throw exception', () { final list = ModList(); list.mods = { 'incompatible': makeDummy().copyWith( name: 'Incompatible', id: 'incompatible', incompatibilities: ['harmony'], ), 'harmony': makeDummy().copyWith(name: 'Harmony', id: 'harmony'), }; list.enableAll(); expect(() => list.generateLoadOrder(), throwsException); }); }); group('Test loadRequired', () { test('Dependencies should be automatically enabled', () { final list = ModList(); list.mods = { 'prepatcher': makeDummy().copyWith( name: 'Prepatcher', id: 'prepatcher', dependencies: ['harmony'], ), 'harmony': makeDummy().copyWith(name: 'Harmony', id: 'harmony'), }; list.disableAll(); list.setEnabled('prepatcher', true); final order = list.loadRequired(); expect(order.indexOf('harmony'), isNot(-1)); }); test('Only required mods should be enabled', () { final list = ModList(); list.mods = { 'prepatcher': makeDummy().copyWith( name: 'Prepatcher', id: 'prepatcher', dependencies: ['harmony'], ), 'harmony': makeDummy().copyWith(name: 'Harmony', id: 'harmony'), 'dummy': makeDummy(), }; list.disableAll(); list.setEnabled('prepatcher', true); final order = list.loadRequired(); expect(order.indexOf('harmony'), isNot(-1)); expect(order.indexOf('dummy'), -1); }); test('Incompatible mods should throw exception', () { final list = ModList(); list.mods = { 'incompatible': makeDummy().copyWith( name: 'Incompatible', id: 'incompatible', incompatibilities: ['harmony'], ), 'prepatcher': makeDummy().copyWith( name: 'Prepatcher', id: 'prepatcher', dependencies: ['harmony'], ), 'harmony': makeDummy().copyWith(name: 'Harmony', id: 'harmony'), }; list.disableAll(); list.setEnabled('incompatible', true); list.setEnabled('prepatcher', true); expect(() => list.loadRequired(), throwsException); }); test('Dependencies of dependencies should be loaded', () { final list = ModList(); list.mods = { 'modA': makeDummy().copyWith( name: 'Mod A', id: 'modA', dependencies: ['modB'], ), 'modB': makeDummy().copyWith( name: 'Mod B', id: 'modB', dependencies: ['modC'], ), 'modC': makeDummy().copyWith(name: 'Mod C', id: 'modC'), }; list.disableAll(); list.setEnabled('modA', true); final order = list.loadRequired(); expect(order.indexOf('modA'), isNot(-1)); expect(order.indexOf('modB'), isNot(-1)); expect(order.indexOf('modC'), isNot(-1)); }); }); group('Test cyclic dependencies', () { test('Cyclic dependencies should throw exception', () { final list = ModList(); list.mods = { 'modA': makeDummy().copyWith( name: 'Mod A', id: 'modA', dependencies: ['modB'], ), 'modB': makeDummy().copyWith( name: 'Mod B', id: 'modB', dependencies: ['modC'], ), 'modC': makeDummy().copyWith( name: 'Mod C', id: 'modC', dependencies: ['modA'], ), }; list.disableAll(); list.setEnabled('modA', true); expect(() => list.loadRequired(), throwsException); }); }); group('Test soft constraints', () { test('Load preferences should be respected when possible', () { final dummy = makeDummy(); final list = ModList(); list.mods = { 'modA': dummy.copyWith( name: 'Mod A', id: 'modA', loadAfter: ['modB'], loadBefore: ['modC'], ), 'modB': dummy.copyWith(name: 'Mod B', id: 'modB'), 'modC': dummy.copyWith(name: 'Mod C', id: 'modC'), }; 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 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(); 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 list = ModList(); list.mods = { 'modA': makeDummy().copyWith(name: 'Mod A', id: 'modA'), 'modB': makeDummy().copyWith(name: 'Mod B', id: 'modB'), }; 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 list = ModList(); list.mods = { 'ludeon.rimworld': makeDummy().copyWith( name: 'RimWorld', id: 'ludeon.rimworld', isBaseGame: true, ), 'ludeon.rimworld.anomaly': makeDummy().copyWith( name: 'RimWorld Anomaly', id: 'ludeon.rimworld.anomaly', dependencies: ['ludeon.rimworld'], isExpansion: true, ), }; list.enableAll(); 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)); }); }); group('Test complex dependency chains', () { test('Complex dependency chains should resolve correctly', () { final list = ModList(); list.mods = { 'modA': makeDummy().copyWith( name: 'Mod A', id: 'modA', dependencies: ['modB'], ), 'modB': makeDummy().copyWith( name: 'Mod B', id: 'modB', dependencies: ['modC'], ), 'modC': makeDummy().copyWith( name: 'Mod C', id: 'modC', dependencies: ['modD'], ), 'modD': makeDummy().copyWith(name: 'Mod D', id: 'modD'), }; 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'))); }); }); }