Fix hard dependency loading

This commit is contained in:
2025-03-16 00:09:55 +01:00
parent ec4b78acc4
commit ee85131da6

View File

@@ -1,7 +1,6 @@
import 'dart:io'; import 'dart:io';
import 'dart:async'; import 'dart:async';
import 'package:xml/xml.dart'; import 'package:xml/xml.dart';
import 'package:path/path.dart' as path;
const root = r'C:/Users/Administrator/Seafile/Games-Rimworld'; const root = r'C:/Users/Administrator/Seafile/Games-Rimworld';
const modsRoot = '$root/294100'; const modsRoot = '$root/294100';
@@ -13,37 +12,36 @@ const logsPath = '$root/ModManager';
class Logger { class Logger {
static final Logger _instance = Logger._internal(); static final Logger _instance = Logger._internal();
static Logger get instance => _instance; static Logger get instance => _instance;
File? _logFile; File? _logFile;
IOSink? _logSink; IOSink? _logSink;
Logger._internal() { Logger._internal() {
_initLogFile(); _initLogFile();
} }
void _initLogFile() { void _initLogFile() {
try { try {
// Use system temp directory // Use system temp directory
final tempDir = Directory.systemTemp; final tempDir = Directory.systemTemp;
final timestamp = DateTime.now().toIso8601String().replaceAll(':', '-').substring(0, 19); final logFileName = 'rimworld_modman.log';
final logFileName = 'rimworld_modman_$timestamp.log';
_logFile = File('${tempDir.path}${Platform.pathSeparator}$logFileName'); _logFile = File('${tempDir.path}${Platform.pathSeparator}$logFileName');
_logSink = _logFile!.openWrite(mode: FileMode.writeOnly); _logSink = _logFile!.openWrite(mode: FileMode.writeOnly);
info('Logging initialized. Log file: ${_logFile!.path}'); info('Logging initialized. Log file: ${_logFile!.path}');
} catch (e) { } catch (e) {
print('Failed to initialize log file: $e'); print('Failed to initialize log file: $e');
} }
} }
void _log(String message, String level) { void _log(String message, String level) {
final timestamp = DateTime.now().toIso8601String(); final timestamp = DateTime.now().toIso8601String();
final formattedMessage = '[$timestamp] [$level] $message'; final formattedMessage = '[$timestamp] [$level] $message';
// Always print to console // Always print to console
print(formattedMessage); print(formattedMessage);
// Write to file if initialized // Write to file if initialized
if (_logSink != null) { if (_logSink != null) {
try { try {
@@ -53,19 +51,19 @@ class Logger {
} }
} }
} }
void info(String message) { void info(String message) {
_log(message, 'INFO'); _log(message, 'INFO');
} }
void warning(String message) { void warning(String message) {
_log(message, 'WARN'); _log(message, 'WARN');
} }
void error(String message) { void error(String message) {
_log(message, 'ERROR'); _log(message, 'ERROR');
} }
void close() { void close() {
if (_logSink != null) { if (_logSink != null) {
try { try {
@@ -102,7 +100,8 @@ class Mod {
final List<String> loadAfter; // ModMetaData.loadAfter final List<String> loadAfter; // ModMetaData.loadAfter
final List<String> loadBefore; // ModMetaData.loadBefore final List<String> loadBefore; // ModMetaData.loadBefore
final List<String> incompatabilities; // ModMetaData.incompatibleWith final List<String> incompatabilities; // ModMetaData.incompatibleWith
final bool enabled; // ConfigFile.mods.firstWhere((mod) => mod.id == id).enabled final bool
enabled; // ConfigFile.mods.firstWhere((mod) => mod.id == id).enabled
final int size; // Count of files in the mod directory final int size; // Count of files in the mod directory
final bool isBaseGame; // Is this the base RimWorld game final bool isBaseGame; // Is this the base RimWorld game
final bool isExpansion; // Is this a RimWorld expansion final bool isExpansion; // Is this a RimWorld expansion
@@ -127,18 +126,25 @@ class Mod {
final logger = Logger.instance; final logger = Logger.instance;
final stopwatch = Stopwatch()..start(); final stopwatch = Stopwatch()..start();
logger.info('Attempting to load mod from directory: $path');
final aboutFile = File('$path/About/About.xml'); final aboutFile = File('$path/About/About.xml');
if (!aboutFile.existsSync()) { if (!aboutFile.existsSync()) {
logger.error('About.xml file does not exist in $aboutFile');
throw Exception('About.xml file does not exist in $aboutFile'); throw Exception('About.xml file does not exist in $aboutFile');
} }
logger.info('Parsing About.xml file...');
final aboutXml = XmlDocument.parse(aboutFile.readAsStringSync()); final aboutXml = XmlDocument.parse(aboutFile.readAsStringSync());
final xmlTime = stopwatch.elapsedMilliseconds; final xmlTime = stopwatch.elapsedMilliseconds;
late final XmlElement metadata; late final XmlElement metadata;
try { try {
metadata = findCaseInsensitiveDoc(aboutXml, 'ModMetaData'); metadata = findCaseInsensitiveDoc(aboutXml, 'ModMetaData');
logger.info('Successfully found ModMetaData in About.xml');
} catch (e) { } catch (e) {
logger.error(
'Error: ModMetaData element is missing in About.xml ($aboutFile). Original error: $e',
);
throw Exception( throw Exception(
'Error: ModMetaData element is missing in About.xml ($aboutFile). Original error: $e', 'Error: ModMetaData element is missing in About.xml ($aboutFile). Original error: $e',
); );
@@ -147,7 +153,11 @@ class Mod {
late final String name; late final String name;
try { try {
name = metadata.findElements('name').first.innerText; name = metadata.findElements('name').first.innerText;
logger.info('Mod name found: $name');
} catch (e) { } catch (e) {
logger.error(
'Error: name element is missing in ModMetaData ($aboutFile). Original error: $e',
);
throw Exception( throw Exception(
'Error: name element is missing in ModMetaData ($aboutFile). Original error: $e', 'Error: name element is missing in ModMetaData ($aboutFile). Original error: $e',
); );
@@ -156,7 +166,11 @@ class Mod {
late final String id; late final String id;
try { try {
id = metadata.findElements('packageId').first.innerText.toLowerCase(); id = metadata.findElements('packageId').first.innerText.toLowerCase();
logger.info('Mod ID found: $id');
} catch (e) { } catch (e) {
logger.error(
'Error: packageId element is missing in ModMetaData ($aboutFile). Original error: $e',
);
throw Exception( throw Exception(
'Error: packageId element is missing in ModMetaData ($aboutFile). Original error: $e', 'Error: packageId element is missing in ModMetaData ($aboutFile). Original error: $e',
); );
@@ -171,7 +185,11 @@ class Mod {
.findElements('li') .findElements('li')
.map((e) => e.innerText) .map((e) => e.innerText)
.toList(); .toList();
logger.info('Supported versions found: ${versions.join(", ")}');
} catch (e) { } catch (e) {
logger.error(
'Error: supportedVersions or li elements are missing in ModMetaData ($aboutFile). Original error: $e',
);
throw Exception( throw Exception(
'Error: supportedVersions or li elements are missing in ModMetaData ($aboutFile). Original error: $e', 'Error: supportedVersions or li elements are missing in ModMetaData ($aboutFile). Original error: $e',
); );
@@ -180,8 +198,11 @@ class Mod {
String description = ''; String description = '';
try { try {
description = metadata.findElements('description').first.innerText; description = metadata.findElements('description').first.innerText;
logger.info('Mod description found: $description');
} catch (e) { } catch (e) {
// Silent error for optional element logger.warning(
'Description element is missing in ModMetaData ($aboutFile).',
);
} }
List<String> hardDependencies = []; List<String> hardDependencies = [];
@@ -189,15 +210,21 @@ class Mod {
hardDependencies = hardDependencies =
metadata metadata
.findElements('modDependenciesByVersion') .findElements('modDependenciesByVersion')
.first
.children
.whereType<XmlElement>()
.last .last
.findElements('li') .findElements('li')
.map( .map(
(e) => (e) =>
e.findElements('packageId').first.innerText.toLowerCase(), e.findElements("packageId").first.innerText.toLowerCase(),
) )
.toList(); .toList();
logger.info('Hard dependencies found: ${hardDependencies.join(", ")}');
} catch (e) { } catch (e) {
// Silent error for optional element logger.warning(
'Hard dependencies element is missing in ModMetaData ($aboutFile).',
);
} }
List<String> loadAfter = []; List<String> loadAfter = [];
@@ -209,8 +236,11 @@ class Mod {
.findElements('li') .findElements('li')
.map((e) => e.innerText.toLowerCase()) .map((e) => e.innerText.toLowerCase())
.toList(); .toList();
logger.info('Load after dependencies found: ${loadAfter.join(", ")}');
} catch (e) { } catch (e) {
// Silent error for optional element logger.warning(
'Load after element is missing in ModMetaData ($aboutFile).',
);
} }
List<String> loadBefore = []; List<String> loadBefore = [];
@@ -222,8 +252,11 @@ class Mod {
.findElements('li') .findElements('li')
.map((e) => e.innerText.toLowerCase()) .map((e) => e.innerText.toLowerCase())
.toList(); .toList();
logger.info('Load before dependencies found: ${loadBefore.join(", ")}');
} catch (e) { } catch (e) {
// Silent error for optional element logger.warning(
'Load before element is missing in ModMetaData ($aboutFile).',
);
} }
List<String> incompatabilities = []; List<String> incompatabilities = [];
@@ -235,8 +268,11 @@ class Mod {
.findElements('li') .findElements('li')
.map((e) => e.innerText.toLowerCase()) .map((e) => e.innerText.toLowerCase())
.toList(); .toList();
logger.info('Incompatibilities found: ${incompatabilities.join(", ")}');
} catch (e) { } catch (e) {
// Silent error for optional element logger.warning(
'Incompatibilities element is missing in ModMetaData ($aboutFile).',
);
} }
final metadataTime = stopwatch.elapsedMilliseconds - xmlTime; final metadataTime = stopwatch.elapsedMilliseconds - xmlTime;
@@ -254,6 +290,7 @@ class Mod {
.startsWith('.'), .startsWith('.'),
) )
.length; .length;
logger.info('File count in mod directory: $size');
} }
// Check if this is RimWorld base game or expansion // Check if this is RimWorld base game or expansion
@@ -263,6 +300,9 @@ class Mod {
// If this is an expansion, ensure it depends on the base game // If this is an expansion, ensure it depends on the base game
if (isExpansion && !loadAfter.contains('ludeon.rimworld')) { if (isExpansion && !loadAfter.contains('ludeon.rimworld')) {
loadAfter.add('ludeon.rimworld'); loadAfter.add('ludeon.rimworld');
logger.info(
'Added base game dependency for expansion mod: ludeon.rimworld',
);
} }
final fileCountTime = final fileCountTime =
@@ -305,16 +345,17 @@ class ModList {
// Simplified loading with config file first // Simplified loading with config file first
Future<void> loadWithConfig({bool skipFileCount = false}) async { Future<void> loadWithConfig({bool skipFileCount = false}) async {
final logger = Logger.instance; final logger = Logger.instance;
// Clear existing state if reloading // Clear existing state if reloading
if (modsLoaded) { if (modsLoaded) {
logger.info('Clearing existing mods state for reload.');
mods.clear(); mods.clear();
} }
modsLoaded = false; modsLoaded = false;
loadedModsCount = 0; loadedModsCount = 0;
loadingStatus = 'Loading active mods from config...'; loadingStatus = 'Loading active mods from config...';
final stopwatch = Stopwatch()..start(); final stopwatch = Stopwatch()..start();
logger.info('Loading configuration from config file: $configPath'); logger.info('Loading configuration from config file: $configPath');
@@ -322,25 +363,36 @@ class ModList {
// First, load the config file to get the list of active mods // First, load the config file to get the list of active mods
final configFile = ConfigFile(path: configPath); final configFile = ConfigFile(path: configPath);
await configFile.load(); await configFile.load();
logger.info('Config file loaded successfully.');
// Create a Set of active mod IDs for quick lookups // Create a Set of active mod IDs for quick lookups
final activeModIds = configFile.mods.map((m) => m.id).toSet(); final activeModIds = configFile.mods.map((m) => m.id).toSet();
logger.info('Active mod IDs created: ${activeModIds.join(', ')}');
// Special handling for Ludeon mods that might not exist as directories // Special handling for Ludeon mods that might not exist as directories
for (final configMod in configFile.mods) { for (final configMod in configFile.mods) {
if (configMod.id.startsWith('ludeon.')) { if (configMod.id.startsWith('ludeon.')) {
final isBaseGame = configMod.id == 'ludeon.rimworld'; final isBaseGame = configMod.id == 'ludeon.rimworld';
final isExpansion = configMod.id.startsWith('ludeon.rimworld.') && !isBaseGame; final isExpansion =
configMod.id.startsWith('ludeon.rimworld.') && !isBaseGame;
// Create a placeholder mod for the Ludeon mods that might not have directories // Create a placeholder mod for the Ludeon mods that might not have directories
final mod = Mod( final mod = Mod(
name: isBaseGame ? "RimWorld" : name:
isExpansion ? "RimWorld ${_expansionNameFromId(configMod.id)}" : configMod.id, isBaseGame
? "RimWorld"
: isExpansion
? "RimWorld ${_expansionNameFromId(configMod.id)}"
: configMod.id,
id: configMod.id, id: configMod.id,
path: '', path: '',
versions: [], versions: [],
description: isBaseGame ? "RimWorld base game" : description:
isExpansion ? "RimWorld expansion" : "", isBaseGame
? "RimWorld base game"
: isExpansion
? "RimWorld expansion"
: "",
hardDependencies: [], hardDependencies: [],
loadAfter: isExpansion ? ['ludeon.rimworld'] : [], loadAfter: isExpansion ? ['ludeon.rimworld'] : [],
loadBefore: [], loadBefore: [],
@@ -350,40 +402,47 @@ class ModList {
isBaseGame: isBaseGame, isBaseGame: isBaseGame,
isExpansion: isExpansion, isExpansion: isExpansion,
); );
mods[configMod.id] = mod; mods[configMod.id] = mod;
loadedModsCount++; loadedModsCount++;
logger.info('Added mod from config: ${mod.name} (ID: ${mod.id})');
} }
} }
// Now scan the directory for mod metadata // Now scan the directory for mod metadata
loadingStatus = 'Scanning mod directories...'; loadingStatus = 'Scanning mod directories...';
final directory = Directory(path); final directory = Directory(path);
if (!directory.existsSync()) { if (!directory.existsSync()) {
loadingStatus = 'Error: Mods root directory does not exist: $path'; loadingStatus = 'Error: Mods root directory does not exist: $path';
logger.error(loadingStatus); logger.error(loadingStatus);
return; return;
} }
final List<FileSystemEntity> entities = directory.listSync(); final List<FileSystemEntity> entities = directory.listSync();
final List<String> modDirectories = final List<String> modDirectories =
entities.whereType<Directory>().map((dir) => dir.path).toList(); entities.whereType<Directory>().map((dir) => dir.path).toList();
totalModsFound = modDirectories.length; totalModsFound = modDirectories.length;
loadingStatus = 'Found $totalModsFound mod directories. Loading...'; loadingStatus = 'Found $totalModsFound mod directories. Loading...';
logger.info('Found ${modDirectories.length} mod directories (${stopwatch.elapsedMilliseconds}ms)'); logger.info(
'Found ${modDirectories.length} mod directories (${stopwatch.elapsedMilliseconds}ms)',
);
for (final modDir in modDirectories) { for (final modDir in modDirectories) {
try { try {
final modStart = stopwatch.elapsedMilliseconds; final modStart = stopwatch.elapsedMilliseconds;
// Check if this directory contains a valid mod // Check if this directory contains a valid mod
final aboutFile = File('$modDir/About/About.xml'); final aboutFile = File('$modDir/About/About.xml');
if (!aboutFile.existsSync()) continue; if (!aboutFile.existsSync()) {
logger.warning('No About.xml found in directory: $modDir');
continue;
}
final mod = Mod.fromDirectory(modDir, skipFileCount: skipFileCount); final mod = Mod.fromDirectory(modDir, skipFileCount: skipFileCount);
logger.info('Loaded mod from directory: ${mod.name} (ID: ${mod.id})');
// If we already have this mod from the config (like Ludeon mods), update its data // If we already have this mod from the config (like Ludeon mods), update its data
if (mods.containsKey(mod.id)) { if (mods.containsKey(mod.id)) {
final existingMod = mods[mod.id]!; final existingMod = mods[mod.id]!;
@@ -397,11 +456,14 @@ class ModList {
loadAfter: mod.loadAfter, loadAfter: mod.loadAfter,
loadBefore: mod.loadBefore, loadBefore: mod.loadBefore,
incompatabilities: mod.incompatabilities, incompatabilities: mod.incompatabilities,
enabled: activeModIds.contains(mod.id), // Set enabled based on config enabled: activeModIds.contains(
mod.id,
), // Set enabled based on config
size: mod.size, size: mod.size,
isBaseGame: existingMod.isBaseGame, isBaseGame: existingMod.isBaseGame,
isExpansion: existingMod.isExpansion, isExpansion: existingMod.isExpansion,
); );
logger.info('Updated existing mod: ${mod.name} (ID: ${mod.id})');
} else { } else {
// Otherwise add as a new mod // Otherwise add as a new mod
mods[mod.id] = Mod( mods[mod.id] = Mod(
@@ -414,43 +476,52 @@ class ModList {
loadAfter: mod.loadAfter, loadAfter: mod.loadAfter,
loadBefore: mod.loadBefore, loadBefore: mod.loadBefore,
incompatabilities: mod.incompatabilities, incompatabilities: mod.incompatabilities,
enabled: activeModIds.contains(mod.id), // Set enabled based on config enabled: activeModIds.contains(
mod.id,
), // Set enabled based on config
size: mod.size, size: mod.size,
isBaseGame: mod.isBaseGame, isBaseGame: mod.isBaseGame,
isExpansion: mod.isExpansion, isExpansion: mod.isExpansion,
); );
loadedModsCount++; loadedModsCount++;
logger.info('Added new mod: ${mod.name} (ID: ${mod.id})');
} }
final modTime = stopwatch.elapsedMilliseconds - modStart; final modTime = stopwatch.elapsedMilliseconds - modStart;
loadingStatus = 'Loaded $loadedModsCount/$totalModsFound mods...'; loadingStatus = 'Loaded $loadedModsCount/$totalModsFound mods...';
if (loadedModsCount % 50 == 0 || loadedModsCount == totalModsFound) { if (loadedModsCount % 50 == 0 || loadedModsCount == totalModsFound) {
logger.info('Progress: Loaded $loadedModsCount mods (${stopwatch.elapsedMilliseconds}ms)'); logger.info(
'Progress: Loaded $loadedModsCount mods (${stopwatch.elapsedMilliseconds}ms)',
);
} }
} catch (e) { } catch (e) {
logger.error('Error loading mod from directory: $modDir'); logger.error('Error loading mod from directory: $modDir');
logger.error('Error: $e'); logger.error('Error: $e');
} }
} }
modsLoaded = true; modsLoaded = true;
final totalTime = stopwatch.elapsedMilliseconds; final totalTime = stopwatch.elapsedMilliseconds;
loadingStatus = 'Completed! Loaded $loadedModsCount mods in ${totalTime}ms.'; loadingStatus =
logger.info('Loading complete! Loaded ${mods.length} mods in ${totalTime}ms'); 'Completed! Loaded $loadedModsCount mods in ${totalTime}ms.';
logger.info(
'Loading complete! Loaded ${mods.length} mods in ${totalTime}ms',
);
} catch (e) { } catch (e) {
loadingStatus = 'Error loading mods: $e'; loadingStatus = 'Error loading mods: $e';
logger.error(loadingStatus); logger.error(loadingStatus);
} }
} }
// Helper function to get a nice expansion name from ID // Helper function to get a nice expansion name from ID
String _expansionNameFromId(String id) { String _expansionNameFromId(String id) {
final parts = id.split('.'); final parts = id.split('.');
if (parts.length < 3) return id; if (parts.length < 3) return id;
final expansionPart = parts[2]; final expansionPart = parts[2];
return expansionPart.substring(0, 1).toUpperCase() + expansionPart.substring(1); return expansionPart.substring(0, 1).toUpperCase() +
expansionPart.substring(1);
} }
// Build a directed graph of mod dependencies // Build a directed graph of mod dependencies
@@ -478,7 +549,8 @@ class ModList {
// 2. Add expansions as dependencies of mods that load after them // 2. Add expansions as dependencies of mods that load after them
// First identify the base game and expansions // First identify the base game and expansions
final baseGameId = mods.values.where((m) => m.isBaseGame).map((m) => m.id).firstOrNull; final baseGameId =
mods.values.where((m) => m.isBaseGame).map((m) => m.id).firstOrNull;
if (baseGameId != null) { if (baseGameId != null) {
for (final mod in mods.values) { for (final mod in mods.values) {
// Skip the base game itself and mods that explicitly load before it // Skip the base game itself and mods that explicitly load before it
@@ -590,7 +662,7 @@ class ModList {
// Separate nodes by "layers" (nodes that can be processed at the same time) // Separate nodes by "layers" (nodes that can be processed at the same time)
List<List<String>> layers = []; List<List<String>> layers = [];
// Process until all nodes are assigned to layers // Process until all nodes are assigned to layers
while (inDegree.isNotEmpty) { while (inDegree.isNotEmpty) {
// Find all nodes with in-degree 0 in this iteration // Find all nodes with in-degree 0 in this iteration
@@ -600,13 +672,15 @@ class ModList {
currentLayer.add(node); currentLayer.add(node);
} }
}); });
if (currentLayer.isEmpty && inDegree.isNotEmpty) { if (currentLayer.isEmpty && inDegree.isNotEmpty) {
// We have a cycle - add all remaining nodes to a final layer // We have a cycle - add all remaining nodes to a final layer
currentLayer = inDegree.keys.toList(); currentLayer = inDegree.keys.toList();
print("Warning: Cycle detected in dependency graph. Adding all remaining nodes to final layer."); print(
"Warning: Cycle detected in dependency graph. Adding all remaining nodes to final layer.",
);
} }
// Sort this layer by mod size (descending) // Sort this layer by mod size (descending)
currentLayer.sort((a, b) { currentLayer.sort((a, b) {
final modA = mods[a]; final modA = mods[a];
@@ -614,14 +688,14 @@ class ModList {
if (modA == null || modB == null) return 0; if (modA == null || modB == null) return 0;
return modB.size.compareTo(modA.size); // Larger mods first return modB.size.compareTo(modA.size); // Larger mods first
}); });
// Add the layer to our layers list // Add the layer to our layers list
layers.add(currentLayer); layers.add(currentLayer);
// Remove processed nodes from inDegree // Remove processed nodes from inDegree
for (final node in currentLayer) { for (final node in currentLayer) {
inDegree.remove(node); inDegree.remove(node);
// Update in-degrees for remaining nodes // Update in-degrees for remaining nodes
for (final entry in graphCopy.entries) { for (final entry in graphCopy.entries) {
if (entry.value.contains(node)) { if (entry.value.contains(node)) {
@@ -632,13 +706,13 @@ class ModList {
} }
} }
} }
// Flatten the layers to get the final order (first layer first) // Flatten the layers to get the final order (first layer first)
List<String> result = []; List<String> result = [];
for (final layer in layers) { for (final layer in layers) {
result.addAll(layer); result.addAll(layer);
} }
// Final sanity check to make sure all nodes are included // Final sanity check to make sure all nodes are included
if (result.length != graph.keys.length) { if (result.length != graph.keys.length) {
// Add any missing nodes // Add any missing nodes
@@ -648,7 +722,7 @@ class ModList {
} }
} }
} }
return result; return result;
} }
@@ -728,7 +802,9 @@ class ModList {
print("Will attempt to break cycle to produce a valid load order"); print("Will attempt to break cycle to produce a valid load order");
} }
print("Performing topological sort for hard dependencies (prioritizing larger mods)..."); print(
"Performing topological sort for hard dependencies (prioritizing larger mods)...",
);
final hardOrder = topologicalSort(hardGraph); final hardOrder = topologicalSort(hardGraph);
print("Adjusting for soft dependencies..."); print("Adjusting for soft dependencies...");
@@ -786,33 +862,48 @@ class ConfigFile {
logger.info('Found ${modElements.length} active mods.'); logger.info('Found ${modElements.length} active mods.');
// Get the list of known expansions // Get the list of known expansions
final knownExpansionsElement = modConfigData.findElements("knownExpansions").firstOrNull; final knownExpansionsElement =
final knownExpansionIds = knownExpansionsElement != null modConfigData.findElements("knownExpansions").firstOrNull;
? knownExpansionsElement.findElements("li").map((e) => e.innerText.toLowerCase()).toList() final knownExpansionIds =
: <String>[]; knownExpansionsElement != null
? knownExpansionsElement
.findElements("li")
.map((e) => e.innerText.toLowerCase())
.toList()
: <String>[];
logger.info('Found ${knownExpansionIds.length} known expansions.'); logger.info('Found ${knownExpansionIds.length} known expansions.');
// Clear and recreate the mods list // Clear and recreate the mods list
mods = []; mods = [];
for (final modElement in modElements) { for (final modElement in modElements) {
final modId = modElement.innerText.toLowerCase(); final modId = modElement.innerText.toLowerCase();
// Check if this is a special Ludeon mod // Check if this is a special Ludeon mod
final isBaseGame = modId == 'ludeon.rimworld'; final isBaseGame = modId == 'ludeon.rimworld';
final isExpansion = !isBaseGame && modId.startsWith('ludeon.rimworld.') && final isExpansion =
knownExpansionIds.contains(modId); !isBaseGame &&
modId.startsWith('ludeon.rimworld.') &&
knownExpansionIds.contains(modId);
// We'll populate with dummy mods for now, they'll be replaced later // We'll populate with dummy mods for now, they'll be replaced later
mods.add( mods.add(
Mod( Mod(
name: isBaseGame ? "RimWorld" : name:
isExpansion ? "RimWorld ${_expansionNameFromId(modId)}" : modId, isBaseGame
? "RimWorld"
: isExpansion
? "RimWorld ${_expansionNameFromId(modId)}"
: modId,
id: modId, id: modId,
path: '', path: '',
versions: [], versions: [],
description: isBaseGame ? "RimWorld base game" : description:
isExpansion ? "RimWorld expansion" : "", isBaseGame
? "RimWorld base game"
: isExpansion
? "RimWorld expansion"
: "",
hardDependencies: [], hardDependencies: [],
loadAfter: isExpansion ? ['ludeon.rimworld'] : [], loadAfter: isExpansion ? ['ludeon.rimworld'] : [],
loadBefore: [], loadBefore: [],
@@ -831,14 +922,15 @@ class ConfigFile {
throw Exception('Failed to load config file: $e'); throw Exception('Failed to load config file: $e');
} }
} }
// Helper function to get a nice expansion name from ID // Helper function to get a nice expansion name from ID
String _expansionNameFromId(String id) { String _expansionNameFromId(String id) {
final parts = id.split('.'); final parts = id.split('.');
if (parts.length < 3) return id; if (parts.length < 3) return id;
final expansionPart = parts[2]; final expansionPart = parts[2];
return expansionPart.substring(0, 1).toUpperCase() + expansionPart.substring(1); return expansionPart.substring(0, 1).toUpperCase() +
expansionPart.substring(1);
} }
// Save the current mod order back to the config file // Save the current mod order back to the config file