725 lines
22 KiB
Dart
725 lines
22 KiB
Dart
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';
|
|
|
|
class LoadOrder {
|
|
List<Mod> order = [];
|
|
final List<String> errors = [];
|
|
|
|
List<String> get loadOrder {
|
|
return order.map((mod) => mod.id).toList();
|
|
}
|
|
|
|
LoadOrder([List<Mod>? order]) {
|
|
this.order = order ?? [];
|
|
}
|
|
|
|
bool get hasErrors => errors.isNotEmpty;
|
|
}
|
|
|
|
var specialMods = {
|
|
'ludeon.rimworld': Mod(
|
|
id: 'ludeon.rimworld',
|
|
name: 'RimWorld',
|
|
path: '',
|
|
versions: [],
|
|
description: 'RimWorld base game',
|
|
dependencies: [],
|
|
loadAfter: [],
|
|
loadBefore: [],
|
|
incompatibilities: [],
|
|
isBaseGame: true,
|
|
size: 0,
|
|
isExpansion: false,
|
|
enabled: true,
|
|
),
|
|
'ludeon.rimworld.royalty': Mod(
|
|
id: 'ludeon.rimworld.royalty',
|
|
name: 'Royalty',
|
|
path: '',
|
|
versions: [],
|
|
description: 'RimWorld expansion - Royalty',
|
|
dependencies: ['ludeon.rimworld'],
|
|
loadAfter: [],
|
|
loadBefore: [
|
|
'ludeon.rimworld.anomaly',
|
|
'ludeon.rimworld.biotech',
|
|
'ludeon.rimworld.ideology',
|
|
],
|
|
incompatibilities: [],
|
|
isBaseGame: false,
|
|
size: 0,
|
|
isExpansion: true,
|
|
enabled: true,
|
|
),
|
|
'ludeon.rimworld.ideology': Mod(
|
|
id: 'ludeon.rimworld.ideology',
|
|
name: 'Ideology',
|
|
path: '',
|
|
versions: [],
|
|
description: 'RimWorld expansion - Ideology',
|
|
dependencies: ['ludeon.rimworld'],
|
|
loadAfter: ['ludeon.rimworld.royalty'],
|
|
loadBefore: ['ludeon.rimworld.anomaly', 'ludeon.rimworld.biotech'],
|
|
incompatibilities: [],
|
|
isBaseGame: false,
|
|
size: 0,
|
|
isExpansion: true,
|
|
enabled: true,
|
|
),
|
|
'ludeon.rimworld.biotech': Mod(
|
|
id: 'ludeon.rimworld.biotech',
|
|
name: 'Biotech',
|
|
path: '',
|
|
versions: [],
|
|
description: 'RimWorld expansion - Biotech',
|
|
dependencies: ['ludeon.rimworld'],
|
|
loadAfter: ['ludeon.rimworld.ideology', 'ludeon.rimworld.royalty'],
|
|
loadBefore: ['ludeon.rimworld.anomaly'],
|
|
incompatibilities: [],
|
|
isBaseGame: false,
|
|
size: 0,
|
|
isExpansion: true,
|
|
enabled: true,
|
|
),
|
|
'ludeon.rimworld.anomaly': Mod(
|
|
id: 'ludeon.rimworld.anomaly',
|
|
name: 'Anomaly',
|
|
path: '',
|
|
versions: [],
|
|
description: 'RimWorld expansion - Anomaly',
|
|
dependencies: ['ludeon.rimworld'],
|
|
loadAfter: [
|
|
'ludeon.rimworld.biotech',
|
|
'ludeon.rimworld.ideology',
|
|
'ludeon.rimworld.royalty',
|
|
],
|
|
loadBefore: [],
|
|
incompatibilities: [],
|
|
isBaseGame: false,
|
|
size: 0,
|
|
isExpansion: true,
|
|
enabled: true,
|
|
),
|
|
};
|
|
|
|
class ModList {
|
|
String configPath = '';
|
|
String modsPath = '';
|
|
// O(1) lookup
|
|
Map<String, Mod> activeMods = {};
|
|
Map<String, Mod> mods = {};
|
|
|
|
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();
|
|
|
|
final directory = Directory(modsPath);
|
|
|
|
if (!directory.existsSync()) {
|
|
logger.error('Error: Mods root directory does not exist: $modsPath');
|
|
return;
|
|
}
|
|
|
|
final List<FileSystemEntity> entities = directory.listSync();
|
|
final List<String> modDirectories =
|
|
entities.whereType<Directory>().map((dir) => dir.path).toList();
|
|
|
|
logger.info(
|
|
'Found ${modDirectories.length} mod directories (${stopwatch.elapsedMilliseconds}ms)',
|
|
);
|
|
|
|
for (final modDir in modDirectories) {
|
|
try {
|
|
final modStart = stopwatch.elapsedMilliseconds;
|
|
|
|
// Check if this directory contains a valid mod
|
|
final aboutFile = File('$modDir/About/About.xml');
|
|
if (!aboutFile.existsSync()) {
|
|
logger.warning('No About.xml found in directory: $modDir');
|
|
continue;
|
|
}
|
|
|
|
final mod = Mod.fromDirectory(modDir);
|
|
logger.info('Loaded mod from directory: ${mod.name} (ID: ${mod.id})');
|
|
|
|
if (mods.containsKey(mod.id)) {
|
|
logger.warning(
|
|
'Mod $mod.id already exists in mods list, overwriting',
|
|
);
|
|
final existingMod = mods[mod.id]!;
|
|
mods[mod.id] = Mod(
|
|
name: mod.name,
|
|
id: mod.id,
|
|
path: mod.path,
|
|
versions: mod.versions,
|
|
description: mod.description,
|
|
dependencies: mod.dependencies,
|
|
loadAfter: mod.loadAfter,
|
|
loadBefore: mod.loadBefore,
|
|
incompatibilities: mod.incompatibilities,
|
|
size: mod.size,
|
|
enabled: existingMod.enabled,
|
|
isBaseGame: existingMod.isBaseGame,
|
|
isExpansion: existingMod.isExpansion,
|
|
);
|
|
logger.info('Updated existing mod: ${mod.name} (ID: ${mod.id})');
|
|
} else {
|
|
mods[mod.id] = mod;
|
|
logger.info('Added new mod: ${mod.name} (ID: ${mod.id})');
|
|
}
|
|
|
|
final modTime = stopwatch.elapsedMilliseconds - modStart;
|
|
logger.info(
|
|
'Loaded mod from directory: ${mod.name} (ID: ${mod.id}) in $modTime ms',
|
|
);
|
|
yield mod;
|
|
} catch (e) {
|
|
logger.error('Error loading mod from directory: $modDir');
|
|
logger.error('Error: $e');
|
|
}
|
|
}
|
|
}
|
|
|
|
Stream<Mod> loadActive() async* {
|
|
final logger = Logger.instance;
|
|
final file = File(configPath);
|
|
logger.info('Loading configuration from: $configPath');
|
|
|
|
try {
|
|
final xmlString = file.readAsStringSync();
|
|
logger.info('XML content read successfully.');
|
|
|
|
final xmlDocument = XmlDocument.parse(xmlString);
|
|
logger.info('XML document parsed successfully.');
|
|
|
|
final modConfigData = xmlDocument.findElements("ModsConfigData").first;
|
|
logger.info('Found ModsConfigData element.');
|
|
|
|
final modsElement = modConfigData.findElements("activeMods").first;
|
|
logger.info('Found activeMods element.');
|
|
|
|
final modElements = modsElement.findElements("li");
|
|
logger.info('Found ${modElements.length} active mods.');
|
|
|
|
// Get the list of known expansions
|
|
final knownExpansionsElement =
|
|
modConfigData.findElements("knownExpansions").firstOrNull;
|
|
final knownExpansionIds =
|
|
knownExpansionsElement != null
|
|
? knownExpansionsElement
|
|
.findElements("li")
|
|
.map((e) => e.innerText.toLowerCase())
|
|
.toList()
|
|
: <String>[];
|
|
|
|
logger.info('Found ${knownExpansionIds.length} known expansions.');
|
|
|
|
// Clear and recreate the mods list
|
|
for (final modElement in modElements) {
|
|
final modId = modElement.innerText.toLowerCase();
|
|
|
|
if (specialMods.containsKey(modId)) {
|
|
mods[modId] = specialMods[modId]!;
|
|
continue;
|
|
}
|
|
|
|
final existingMod = mods[modId];
|
|
final mod = Mod(
|
|
name: existingMod?.name ?? modId,
|
|
id: existingMod?.id ?? modId,
|
|
path: existingMod?.path ?? '',
|
|
versions: existingMod?.versions ?? [],
|
|
description: existingMod?.description ?? '',
|
|
dependencies: existingMod?.dependencies ?? [],
|
|
loadAfter: existingMod?.loadAfter ?? [],
|
|
loadBefore: existingMod?.loadBefore ?? [],
|
|
incompatibilities: existingMod?.incompatibilities ?? [],
|
|
enabled: existingMod?.enabled ?? false,
|
|
size: existingMod?.size ?? 0,
|
|
isBaseGame: false,
|
|
isExpansion: false,
|
|
);
|
|
if (mods.containsKey(modId)) {
|
|
logger.warning('Mod $modId already exists in mods list, overwriting');
|
|
}
|
|
mods[modId] = mod;
|
|
setEnabled(modId, true);
|
|
yield mod;
|
|
}
|
|
|
|
logger.info('Loaded ${modElements.length} mods from config file.');
|
|
} catch (e) {
|
|
logger.error('Error loading configuration file: $e');
|
|
throw Exception('Failed to load config file: $e');
|
|
}
|
|
}
|
|
|
|
void saveToConfig(LoadOrder loadOrder) {
|
|
final file = File(configPath);
|
|
final logger = Logger.instance;
|
|
|
|
try {
|
|
// Create XML builder
|
|
final builder = XmlBuilder();
|
|
|
|
// Add XML declaration
|
|
builder.declaration(encoding: 'utf-8');
|
|
|
|
// Add root element
|
|
builder.element(
|
|
'ModsConfigData',
|
|
nest: () {
|
|
// Add version element
|
|
builder.element('version', nest: '1.5.4297 rev994');
|
|
|
|
// Add active mods element
|
|
builder.element(
|
|
'activeMods',
|
|
nest: () {
|
|
// Add each mod as a list item
|
|
for (final mod in loadOrder.order) {
|
|
builder.element('li', nest: mod.id);
|
|
logger.info('Adding mod to config: ${mod.name} (${mod.id})');
|
|
}
|
|
},
|
|
);
|
|
|
|
// Add known expansions element
|
|
final expansions = mods.values.where((m) => m.isExpansion).toList();
|
|
if (expansions.isNotEmpty) {
|
|
builder.element(
|
|
'knownExpansions',
|
|
nest: () {
|
|
for (final mod in expansions) {
|
|
builder.element('li', nest: mod.id);
|
|
logger.info(
|
|
'Adding expansion to config: ${mod.name} (${mod.id})',
|
|
);
|
|
}
|
|
},
|
|
);
|
|
}
|
|
},
|
|
);
|
|
|
|
// Build the XML document
|
|
final xmlDocument = builder.buildDocument();
|
|
|
|
// Convert to string with 2-space indentation
|
|
final prettyXml = xmlDocument.toXmlString(
|
|
pretty: true,
|
|
indent: ' ', // 2 spaces
|
|
newLine: '\n',
|
|
);
|
|
|
|
// Write the formatted XML document to file
|
|
file.writeAsStringSync(prettyXml);
|
|
logger.info('Successfully saved mod configuration to: $configPath');
|
|
} catch (e) {
|
|
logger.error('Error saving configuration file: $e');
|
|
throw Exception('Failed to save config file: $e');
|
|
}
|
|
}
|
|
|
|
void setEnabled(String modId, bool enabled) {
|
|
if (mods.containsKey(modId)) {
|
|
final mod = mods[modId]!;
|
|
mod.enabled = enabled;
|
|
if (enabled) {
|
|
activeMods[modId] = mod;
|
|
} else {
|
|
activeMods.remove(modId);
|
|
}
|
|
}
|
|
}
|
|
|
|
void enableAll() {
|
|
for (final mod in mods.values) {
|
|
setEnabled(mod.id, true);
|
|
}
|
|
}
|
|
|
|
void disableAll() {
|
|
for (final mod in mods.values) {
|
|
setEnabled(mod.id, false);
|
|
}
|
|
}
|
|
|
|
void enableMods(List<String> modIds) {
|
|
for (final modId in modIds) {
|
|
setEnabled(modId, true);
|
|
}
|
|
}
|
|
|
|
void disableMods(List<String> modIds) {
|
|
for (final modId in modIds) {
|
|
setEnabled(modId, false);
|
|
}
|
|
}
|
|
|
|
LoadOrder generateLoadOrder([LoadOrder? loadOrder]) {
|
|
loadOrder ??= LoadOrder();
|
|
final logger = Logger.instance;
|
|
logger.info('Generating load order...');
|
|
|
|
for (final mod in activeMods.values) {
|
|
logger.info('Checking mod: ${mod.id}');
|
|
logger.info('Mod details: ${mod.toString()}');
|
|
for (final incomp in mod.incompatibilities) {
|
|
if (activeMods.containsKey(incomp)) {
|
|
loadOrder.errors.add(
|
|
'Incompatibility detected: ${mod.id} is incompatible with $incomp',
|
|
);
|
|
logger.warning(
|
|
'Incompatibility detected: ${mod.id} is incompatible with $incomp',
|
|
);
|
|
} else {
|
|
logger.info('No incompatibility found for: $incomp');
|
|
}
|
|
}
|
|
for (final dep in mod.dependencies) {
|
|
if (!activeMods.containsKey(dep)) {
|
|
loadOrder.errors.add('Missing dependency: ${mod.id} requires $dep');
|
|
logger.warning('Missing dependency: ${mod.id} requires $dep');
|
|
} else {
|
|
logger.info('Dependency found: ${mod.id} requires $dep');
|
|
}
|
|
}
|
|
}
|
|
|
|
logger.info('Adding active mods to load order...');
|
|
loadOrder.order.addAll(activeMods.values.toList());
|
|
logger.info(
|
|
'Active mods added: ${loadOrder.order.map((mod) => mod.id).join(', ')}',
|
|
);
|
|
|
|
final modMap = {for (final mod in loadOrder.order) mod.id: mod};
|
|
final graph = <String, Set<String>>{};
|
|
final inDegree = <String, int>{};
|
|
|
|
// Step 1: Initialize graph and inDegree
|
|
for (final mod in loadOrder.order) {
|
|
graph[mod.id] = <String>{};
|
|
inDegree[mod.id] = 0;
|
|
}
|
|
|
|
// Step 2: Build dependency graph
|
|
void addEdge(String from, String to) {
|
|
final fromMod = modMap[from];
|
|
if (fromMod == null) {
|
|
logger.warning('Missing dependency: $from');
|
|
return;
|
|
}
|
|
final toMod = modMap[to];
|
|
if (toMod == null) {
|
|
logger.warning('Missing dependency: $to');
|
|
return;
|
|
}
|
|
if (graph[from]!.add(to)) {
|
|
inDegree[to] = inDegree[to]! + 1;
|
|
}
|
|
}
|
|
|
|
for (final mod in loadOrder.order) {
|
|
for (final target in mod.loadBefore) {
|
|
addEdge(mod.id, target);
|
|
}
|
|
for (final target in mod.loadAfter) {
|
|
addEdge(target, mod.id);
|
|
}
|
|
for (final dep in mod.dependencies) {
|
|
addEdge(dep, mod.id);
|
|
}
|
|
}
|
|
|
|
// Step 3: Calculate tiers dynamically with cross-tier dependencies
|
|
final tiers = <Mod, int>{};
|
|
for (final mod in loadOrder.order) {
|
|
int tier = 2; // Default to Tier 2
|
|
|
|
// Check if mod loads before any base game mod (Tier 0)
|
|
final loadsBeforeBase = mod.loadBefore.any(
|
|
(id) => modMap[id]?.isBaseGame ?? false,
|
|
);
|
|
if (mod.isBaseGame || loadsBeforeBase) {
|
|
tier = 0;
|
|
} else {
|
|
// Check if mod loads before any expansion (Tier 1)
|
|
final loadsBeforeExpansion = mod.loadBefore.any(
|
|
(id) => modMap[id]?.isExpansion ?? false,
|
|
);
|
|
if (mod.isExpansion || loadsBeforeExpansion) {
|
|
tier = 1;
|
|
}
|
|
}
|
|
|
|
tiers[mod] = tier;
|
|
}
|
|
|
|
// Step 4: Global priority queue (tier ascending, size descending)
|
|
final pq = PriorityQueue<Mod>((a, b) {
|
|
final tierA = tiers[a]!;
|
|
final tierB = tiers[b]!;
|
|
if (tierA != tierB) return tierA.compareTo(tierB);
|
|
return b.size.compareTo(a.size);
|
|
});
|
|
|
|
// Initialize queue with mods having inDegree 0
|
|
for (final mod in loadOrder.order) {
|
|
if (inDegree[mod.id] == 0) {
|
|
pq.add(mod);
|
|
}
|
|
}
|
|
|
|
final orderedMods = <Mod>[];
|
|
while (pq.isNotEmpty) {
|
|
final current = pq.removeFirst();
|
|
orderedMods.add(current);
|
|
|
|
for (final neighborId in graph[current.id]!) {
|
|
inDegree[neighborId] = inDegree[neighborId]! - 1;
|
|
if (inDegree[neighborId] == 0) {
|
|
final neighbor = modMap[neighborId]!;
|
|
pq.add(neighbor);
|
|
}
|
|
}
|
|
}
|
|
if (orderedMods.length != loadOrder.order.length) {
|
|
loadOrder.errors.add('Cycle detected in dependencies');
|
|
logger.warning(
|
|
'Cycle detected in dependencies: expected ${loadOrder.order.length}, got ${orderedMods.length}.',
|
|
);
|
|
}
|
|
|
|
loadOrder.order = orderedMods;
|
|
logger.info(
|
|
'Load order generated successfully with ${loadOrder.order.length} mods.',
|
|
);
|
|
for (final mod in loadOrder.order) {
|
|
logger.info('Mod: ${mod.toString()}');
|
|
}
|
|
return loadOrder;
|
|
}
|
|
|
|
// The point of relations and the recursive call is to handle the case where
|
|
// A mod depends on a mod that depends on another mod
|
|
// So we move our first mod A to after B
|
|
// But then we move B after C and A is no longer guranteed to be after B
|
|
// So we update it too just in case
|
|
// To make sure we have A B C
|
|
// Now it opens us to a stack overflow...
|
|
LoadOrder shuffleMod(
|
|
Mod mod,
|
|
LoadOrder loadOrder,
|
|
Map<String, List<Mod>> relations, [
|
|
Map<String, bool>? seen,
|
|
]) {
|
|
final logger = Logger.instance;
|
|
logger.info('Starting shuffleMod for mod: ${mod.id}');
|
|
|
|
// Prevent infinite loops
|
|
seen ??= <String, bool>{};
|
|
if (seen[mod.id] == true) {
|
|
logger.info('Mod ${mod.id} has already been seen, skipping.');
|
|
return loadOrder;
|
|
}
|
|
seen[mod.id] = true;
|
|
logger.info('Marking mod ${mod.id} as seen.');
|
|
|
|
for (final dependency in mod.dependencies) {
|
|
logger.info('Checking dependency: $dependency for mod ${mod.id}');
|
|
final depMod = mods[dependency];
|
|
if (depMod == null) {
|
|
loadOrder.errors.add(
|
|
'Missing dependency: ${mod.id} requires mod with ID $dependency',
|
|
);
|
|
logger.warning(
|
|
'Missing dependency: ${mod.id} requires mod with ID $dependency',
|
|
);
|
|
continue;
|
|
}
|
|
|
|
if (loadOrder.order.indexOf(mod) < loadOrder.order.indexOf(depMod)) {
|
|
logger.info('Reordering: ${mod.id} should come after ${depMod.id}');
|
|
loadOrder.order.removeAt(loadOrder.order.indexOf(mod));
|
|
loadOrder.order.insert(loadOrder.order.indexOf(depMod) + 1, mod);
|
|
relations[mod.id] = [...relations[mod.id] ?? [], depMod];
|
|
}
|
|
}
|
|
|
|
for (final loadAfter in mod.loadAfter) {
|
|
logger.info('Checking loadAfter: $loadAfter for mod ${mod.id}');
|
|
final loadAfterMod = mods[loadAfter];
|
|
if (loadAfterMod != null &&
|
|
loadOrder.order.indexOf(mod) <
|
|
loadOrder.order.indexOf(loadAfterMod)) {
|
|
final loadAfterIndex = loadOrder.order.indexOf(loadAfterMod);
|
|
// Mod is not loaded, we don't care about it
|
|
if (loadAfterIndex == -1) {
|
|
logger.warning(
|
|
'Missing loadAfter: ${mod.id} requires mod with ID $loadAfter',
|
|
);
|
|
continue;
|
|
}
|
|
|
|
logger.info(
|
|
'Reordering: ${mod.id} should come after ${loadAfterMod.id}',
|
|
);
|
|
loadOrder.order.removeAt(loadOrder.order.indexOf(mod));
|
|
loadOrder.order.insert(loadOrder.order.indexOf(loadAfterMod) + 1, mod);
|
|
relations[mod.id] = [...relations[mod.id] ?? [], loadAfterMod];
|
|
}
|
|
}
|
|
|
|
for (final loadBefore in mod.loadBefore) {
|
|
logger.info('Checking loadBefore: $loadBefore for mod ${mod.id}');
|
|
final loadBeforeMod = mods[loadBefore];
|
|
if (loadBeforeMod != null &&
|
|
loadOrder.order.indexOf(mod) >
|
|
loadOrder.order.indexOf(loadBeforeMod)) {
|
|
final loadBeforeIndex = loadOrder.order.indexOf(loadBeforeMod);
|
|
// Mod is not loaded, we don't care about it
|
|
if (loadBeforeIndex == -1) {
|
|
logger.warning(
|
|
'Missing loadBefore: ${mod.id} requires mod with ID $loadBefore',
|
|
);
|
|
continue;
|
|
}
|
|
|
|
logger.info(
|
|
'Reordering: ${mod.id} should come before ${loadBeforeMod.id}',
|
|
);
|
|
loadOrder.order.removeAt(loadOrder.order.indexOf(mod));
|
|
loadOrder.order.insert(loadOrder.order.indexOf(loadBeforeMod), mod);
|
|
relations[mod.id] = [...relations[mod.id] ?? [], loadBeforeMod];
|
|
}
|
|
}
|
|
|
|
for (final relatedMod in relations[mod.id] ?? []) {
|
|
logger.info('Recursively shuffling related mod: ${relatedMod.id}');
|
|
loadOrder = shuffleMod(relatedMod, loadOrder, relations, seen);
|
|
}
|
|
logger.info('Completed shuffleMod for mod: ${mod.id}');
|
|
return loadOrder;
|
|
}
|
|
|
|
List<List<String>> checkIncompatibilities(List<String> modIds) {
|
|
final incompatibilities = <List<String>>[];
|
|
for (final modId in modIds) {
|
|
final mod = mods[modId]!;
|
|
for (final incomp in mod.incompatibilities) {
|
|
if (modIds.contains(incomp)) {
|
|
incompatibilities.add([mod.id, incomp]);
|
|
}
|
|
}
|
|
}
|
|
return incompatibilities;
|
|
}
|
|
|
|
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);
|
|
}
|
|
|
|
LoadOrder loadRequiredBaseGame([LoadOrder? loadOrder]) {
|
|
loadOrder ??= LoadOrder();
|
|
final baseGameMods =
|
|
mods.values.where((mod) => mod.isBaseGame || mod.isExpansion).toList();
|
|
// You would probably want to load these too if you had them
|
|
final specialMods =
|
|
mods.values
|
|
.where(
|
|
(mod) =>
|
|
mod.id.contains("harmony") ||
|
|
mod.id.contains("prepatcher") ||
|
|
mod.id.contains("betterlog"),
|
|
)
|
|
.toList();
|
|
|
|
enableMods(baseGameMods.map((mod) => mod.id).toList());
|
|
enableMods(specialMods.map((mod) => mod.id).toList());
|
|
|
|
return loadRequired(loadOrder);
|
|
}
|
|
}
|