Fix loadBefore and Rimworld and expansions

This commit is contained in:
2025-03-15 23:34:55 +01:00
parent 74eec3f3cc
commit 22ec31c2a7
2 changed files with 424 additions and 142 deletions

View File

@@ -91,12 +91,31 @@ class ModListPage extends StatefulWidget {
} }
class _ModListPageState extends State<ModListPage> { class _ModListPageState extends State<ModListPage> {
final List<Mod> _loadedMods = []; List<Mod> _loadedMods = [];
bool _isLoading = false; bool _isLoading = false;
String _loadingStatus = ''; String _loadingStatus = '';
int _totalModsFound = 0; int _totalModsFound = 0;
bool _skipFileCount = false; // Skip file counting by default for faster loading bool _skipFileCount = false; // Skip file counting by default for faster loading
@override
void initState() {
super.initState();
// Check if mods are already loaded in the global modManager
if (modManager.modsLoaded) {
_loadModsFromGlobalState();
}
}
void _loadModsFromGlobalState() {
setState(() {
_loadedMods = modManager.mods.values.toList();
_loadedMods.sort((a, b) => a.name.compareTo(b.name));
_isLoading = false;
_loadingStatus = modManager.loadingStatus;
_totalModsFound = modManager.totalModsFound;
});
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Scaffold( return Scaffold(
@@ -152,20 +171,27 @@ class _ModListPageState extends State<ModListPage> {
padding: const EdgeInsets.all(16.0), padding: const EdgeInsets.all(16.0),
child: Column( child: Column(
children: [ children: [
LinearProgressIndicator( const CircularProgressIndicator(),
value: const SizedBox(height: 16),
_totalModsFound > 0
? _loadedMods.length / _totalModsFound
: null,
),
const SizedBox(height: 8),
Text( Text(
_loadingStatus, _loadingStatus,
style: Theme.of(context).textTheme.bodyMedium, style: Theme.of(context).textTheme.bodyMedium,
textAlign: TextAlign.center,
), ),
], ],
), ),
), ),
if (!_isLoading && _loadedMods.isEmpty)
Center(
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Text(
'No mods found. Try reloading.',
textAlign: TextAlign.center,
),
),
),
if (_loadedMods.isNotEmpty)
Expanded( Expanded(
child: ListView.builder( child: ListView.builder(
itemCount: _loadedMods.length, itemCount: _loadedMods.length,
@@ -179,7 +205,20 @@ class _ModListPageState extends State<ModListPage> {
'ID: ${mod.id}\nSize: ${mod.size} files', 'ID: ${mod.id}\nSize: ${mod.size} files',
style: Theme.of(context).textTheme.bodySmall, style: Theme.of(context).textTheme.bodySmall,
), ),
trailing: Icon( trailing: Row(
mainAxisSize: MainAxisSize.min,
children: [
if (mod.isBaseGame)
Tooltip(
message: 'Base Game',
child: Icon(Icons.home, color: Colors.blue, size: 16),
),
if (mod.isExpansion)
Tooltip(
message: 'Expansion',
child: Icon(Icons.star, color: Colors.yellow, size: 16),
),
Icon(
Icons.circle, Icons.circle,
color: color:
mod.hardDependencies.isNotEmpty mod.hardDependencies.isNotEmpty
@@ -187,6 +226,8 @@ class _ModListPageState extends State<ModListPage> {
: Colors.green, : Colors.green,
size: 12, size: 12,
), ),
],
),
onTap: () { onTap: () {
// TODO: Show mod details // TODO: Show mod details
}, },
@@ -220,45 +261,23 @@ class _ModListPageState extends State<ModListPage> {
_loadingStatus = 'Scanning for mods...'; _loadingStatus = 'Scanning for mods...';
}); });
// First get the mod directories to know the total count // Use the simplified loading approach
final directory = Directory(modsRoot); modManager.loadWithConfig(skipFileCount: _skipFileCount).then((_) {
if (directory.existsSync()) {
final List<FileSystemEntity> entities = directory.listSync();
final List<String> modDirectories =
entities.whereType<Directory>().map((dir) => dir.path).toList();
setState(() {
_totalModsFound = modDirectories.length;
_loadingStatus = 'Found $_totalModsFound mod directories. Loading...';
});
}
// Use the serial loading with our skipFileCount option
modManager
.load(skipFileCount: _skipFileCount)
.listen(
(mod) {
setState(() {
_loadedMods.add(mod);
_loadingStatus = 'Loaded ${_loadedMods.length}/$_totalModsFound mods...';
});
},
onError: (error) {
setState(() { setState(() {
_loadedMods = modManager.mods.values.toList();
_isLoading = false; _isLoading = false;
_loadingStatus = 'Error loading mods: $error'; _loadingStatus = modManager.loadingStatus;
}); _totalModsFound = modManager.totalModsFound;
},
onDone: () {
setState(() {
_isLoading = false;
_loadingStatus = 'Completed! ${_loadedMods.length} mods loaded.';
// Sort mods by name for better display // Sort mods by name for better display
_loadedMods.sort((a, b) => a.name.compareTo(b.name)); _loadedMods.sort((a, b) => a.name.compareTo(b.name));
}); });
}, }).catchError((error) {
); setState(() {
_isLoading = false;
_loadingStatus = 'Error loading mods: $error';
});
});
} }
} }
@@ -278,6 +297,17 @@ class _LoadOrderPageState extends State<LoadOrderPage> {
List<String>? _cycleInfo; List<String>? _cycleInfo;
List<List<String>> _incompatibleMods = []; List<List<String>> _incompatibleMods = [];
@override
void initState() {
super.initState();
// If we already have loaded mods, update the status message
if (modManager.modsLoaded && modManager.mods.isNotEmpty) {
_statusMessage = 'Ready to sort ${modManager.mods.length} loaded mods';
} else {
_statusMessage = 'No mods have been loaded yet. Please load mods first.';
}
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Scaffold( return Scaffold(
@@ -319,8 +349,27 @@ class _LoadOrderPageState extends State<LoadOrderPage> {
), ),
const SizedBox(height: 16), const SizedBox(height: 16),
if (_isLoading) if (_isLoading)
const LinearProgressIndicator(), Padding(
if (_statusMessage.isNotEmpty) padding: const EdgeInsets.symmetric(vertical: 16.0),
child: Center(
child: Column(
children: [
const CircularProgressIndicator(),
const SizedBox(height: 16),
Text(
_statusMessage,
textAlign: TextAlign.center,
style: TextStyle(
color: _hasCycles || _incompatibleMods.isNotEmpty
? Colors.orange
: Colors.green,
),
),
],
),
),
),
if (!_isLoading && _statusMessage.isNotEmpty)
Padding( Padding(
padding: const EdgeInsets.symmetric(vertical: 8.0), padding: const EdgeInsets.symmetric(vertical: 8.0),
child: Text( child: Text(
@@ -376,6 +425,18 @@ class _LoadOrderPageState extends State<LoadOrderPage> {
children: [ children: [
Text('Legend:', style: TextStyle(fontWeight: FontWeight.bold)), Text('Legend:', style: TextStyle(fontWeight: FontWeight.bold)),
const SizedBox(height: 4), const SizedBox(height: 4),
Row(
children: [
Icon(Icons.home, color: Colors.blue, size: 16),
const SizedBox(width: 4),
Text('Base Game', style: TextStyle(fontSize: 12)),
const SizedBox(width: 12),
Icon(Icons.star, color: Colors.yellow, size: 16),
const SizedBox(width: 4),
Text('Expansion', style: TextStyle(fontSize: 12)),
],
),
const SizedBox(height: 4),
Row( Row(
children: [ children: [
Icon(Icons.link, color: Colors.orange, size: 16), Icon(Icons.link, color: Colors.orange, size: 16),
@@ -388,6 +449,14 @@ class _LoadOrderPageState extends State<LoadOrderPage> {
], ],
), ),
const SizedBox(height: 4), const SizedBox(height: 4),
Row(
children: [
Icon(Icons.arrow_forward, color: Colors.green, size: 16),
const SizedBox(width: 4),
Text('Loads before other mods', style: TextStyle(fontSize: 12)),
],
),
const SizedBox(height: 4),
Row( Row(
children: [ children: [
Container(width: 12, height: 12, color: Colors.amber), Container(width: 12, height: 12, color: Colors.amber),
@@ -406,7 +475,9 @@ class _LoadOrderPageState extends State<LoadOrderPage> {
child: _sortedMods.isEmpty child: _sortedMods.isEmpty
? Center( ? Center(
child: Text( child: Text(
'Click "Auto-sort Mods" to generate a load order based on dependencies.', modManager.modsLoaded
? 'Click "Auto-sort Mods" to generate a load order for ${modManager.mods.length} loaded mods.'
: 'Please go to the Mods tab first to load mods.',
textAlign: TextAlign.center, textAlign: TextAlign.center,
), ),
) )
@@ -446,12 +517,28 @@ class _LoadOrderPageState extends State<LoadOrderPage> {
trailing: Row( trailing: Row(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
children: [ children: [
if (mod.isBaseGame)
Tooltip(
message: 'Base Game',
child: Icon(Icons.home, color: Colors.blue, size: 16),
),
if (mod.isExpansion)
Tooltip(
message: 'Expansion',
child: Icon(Icons.star, color: Colors.yellow, size: 16),
),
if (mod.hardDependencies.isNotEmpty) if (mod.hardDependencies.isNotEmpty)
Icon(Icons.link, color: Colors.orange, size: 16), Icon(Icons.link, color: Colors.orange, size: 16),
const SizedBox(width: 4), const SizedBox(width: 4),
if (mod.softDependencies.isNotEmpty) if (mod.softDependencies.isNotEmpty)
Icon(Icons.link_off, color: Colors.blue, size: 16), Icon(Icons.link_off, color: Colors.blue, size: 16),
const SizedBox(width: 4), const SizedBox(width: 4),
if (mod.loadBefore.isNotEmpty)
Tooltip(
message: 'Loads before other mods',
child: Icon(Icons.arrow_forward, color: Colors.green, size: 16),
),
const SizedBox(width: 4),
Text( Text(
'${mod.size} files', '${mod.size} files',
style: TextStyle( style: TextStyle(
@@ -480,7 +567,7 @@ class _LoadOrderPageState extends State<LoadOrderPage> {
void _sortMods() async { void _sortMods() async {
if (modManager.mods.isEmpty) { if (modManager.mods.isEmpty) {
setState(() { setState(() {
_statusMessage = 'No mods have been loaded yet. Please load mods first.'; _statusMessage = 'No mods have been loaded yet. Please go to the Mods tab and load mods first.';
}); });
return; return;
} }
@@ -493,8 +580,8 @@ class _LoadOrderPageState extends State<LoadOrderPage> {
_incompatibleMods = []; _incompatibleMods = [];
}); });
// This could be slow so run in a separate isolate or compute // Use a Future.delayed to allow the UI to update
await Future.delayed(Duration.zero); // Allow UI to update await Future.delayed(Duration.zero);
try { try {
// Check for cycles first // Check for cycles first

View File

@@ -1,4 +1,5 @@
import 'dart:io'; import 'dart:io';
import 'dart:async';
import 'package:xml/xml.dart'; import 'package:xml/xml.dart';
const root = r'C:/Users/Administrator/Seafile/Games-Rimworld'; const root = r'C:/Users/Administrator/Seafile/Games-Rimworld';
@@ -25,13 +26,14 @@ class Mod {
final String path; // figure it out final String path; // figure it out
final List<String> versions; // ModMetaData.supportedVersions final List<String> versions; // ModMetaData.supportedVersions
final String description; // ModMetaData.description final String description; // ModMetaData.description
final List<String> final List<String> hardDependencies; // ModMetaData.modDependencies
hardDependencies; // ModMetaData.modDependencies - this is a li with packageId, displayName, steamWorkshopUrl and downloadUrl
final List<String> softDependencies; // ModMetaData.loadAfter final List<String> softDependencies; // ModMetaData.loadAfter
final List<String> loadBefore; // ModMetaData.loadBefore
final List<String> incompatabilities; // ModMetaData.incompatibleWith final List<String> incompatabilities; // ModMetaData.incompatibleWith
final bool final bool enabled; // ConfigFile.mods.firstWhere((mod) => mod.id == id).enabled
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 isExpansion; // Is this a RimWorld expansion
Mod({ Mod({
required this.name, required this.name,
@@ -41,9 +43,12 @@ class Mod {
required this.description, required this.description,
required this.hardDependencies, required this.hardDependencies,
required this.softDependencies, required this.softDependencies,
required this.loadBefore,
required this.incompatabilities, required this.incompatabilities,
required this.enabled, required this.enabled,
required this.size, required this.size,
this.isBaseGame = false,
this.isExpansion = false,
}); });
static Mod fromDirectory(String path, {bool skipFileCount = false}) { static Mod fromDirectory(String path, {bool skipFileCount = false}) {
@@ -135,6 +140,19 @@ class Mod {
// Silent error for optional element // Silent error for optional element
} }
List<String> loadBefore = [];
try {
loadBefore =
metadata
.findElements('loadBefore')
.first
.findElements('li')
.map((e) => e.innerText.toLowerCase())
.toList();
} catch (e) {
// Silent error for optional element
}
List<String> incompatabilities = []; List<String> incompatabilities = [];
try { try {
incompatabilities = incompatabilities =
@@ -165,6 +183,15 @@ class Mod {
.length; .length;
} }
// Check if this is RimWorld base game or expansion
bool isBaseGame = id == 'ludeon.rimworld';
bool isExpansion = !isBaseGame && id.startsWith('ludeon.rimworld.');
// If this is an expansion, ensure it depends on the base game
if (isExpansion && !softDependencies.contains('ludeon.rimworld')) {
softDependencies.add('ludeon.rimworld');
}
final fileCountTime = final fileCountTime =
stopwatch.elapsedMilliseconds - metadataTime - xmlTime; stopwatch.elapsedMilliseconds - metadataTime - xmlTime;
final totalTime = stopwatch.elapsedMilliseconds; final totalTime = stopwatch.elapsedMilliseconds;
@@ -182,9 +209,12 @@ class Mod {
description: description, description: description,
hardDependencies: hardDependencies, hardDependencies: hardDependencies,
softDependencies: softDependencies, softDependencies: softDependencies,
loadBefore: loadBefore,
incompatabilities: incompatabilities, incompatabilities: incompatabilities,
enabled: false, enabled: false,
size: size, size: size,
isBaseGame: isBaseGame,
isExpansion: isExpansion,
); );
} }
} }
@@ -192,56 +222,162 @@ class Mod {
class ModList { class ModList {
final String path; final String path;
Map<String, Mod> mods = {}; Map<String, Mod> mods = {};
bool modsLoaded = false;
String loadingStatus = '';
int totalModsFound = 0;
int loadedModsCount = 0;
ModList({required this.path}); ModList({required this.path});
Stream<Mod> load({bool skipFileCount = false}) async* { // Simplified loading with config file first
final stopwatch = Stopwatch()..start(); Future<void> loadWithConfig({bool skipFileCount = false}) async {
final directory = Directory(path); // Clear existing state if reloading
print('Loading configuration from: $path'); if (modsLoaded) {
mods.clear();
}
modsLoaded = false;
loadedModsCount = 0;
loadingStatus = 'Loading active mods from config...';
final stopwatch = Stopwatch()..start();
print('Loading configuration from config file: $configPath');
try {
// First, load the config file to get the list of active mods
final configFile = ConfigFile(path: configPath);
await configFile.load();
// Create a Set of active mod IDs for quick lookups
final activeModIds = configFile.mods.map((m) => m.id).toSet();
// Special handling for Ludeon mods that might not exist as directories
for (final configMod in configFile.mods) {
if (configMod.id.startsWith('ludeon.')) {
final isBaseGame = configMod.id == 'ludeon.rimworld';
final isExpansion = configMod.id.startsWith('ludeon.rimworld.') && !isBaseGame;
// Create a placeholder mod for the Ludeon mods that might not have directories
final mod = Mod(
name: isBaseGame ? "RimWorld" :
isExpansion ? "RimWorld ${_expansionNameFromId(configMod.id)}" : configMod.id,
id: configMod.id,
path: '',
versions: [],
description: isBaseGame ? "RimWorld base game" :
isExpansion ? "RimWorld expansion" : "",
hardDependencies: [],
softDependencies: isExpansion ? ['ludeon.rimworld'] : [],
loadBefore: [],
incompatabilities: [],
enabled: true,
size: 0,
isBaseGame: isBaseGame,
isExpansion: isExpansion,
);
mods[configMod.id] = mod;
loadedModsCount++;
}
}
// Now scan the directory for mod metadata
loadingStatus = 'Scanning mod directories...';
final directory = Directory(path);
if (!directory.existsSync()) {
loadingStatus = 'Error: Mods root directory does not exist: $path';
print(loadingStatus);
return;
}
if (directory.existsSync()) {
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();
print( totalModsFound = modDirectories.length;
'Found ${modDirectories.length} mod directories (${stopwatch.elapsedMilliseconds}ms)', loadingStatus = 'Found $totalModsFound mod directories. Loading...';
); print('Found ${modDirectories.length} mod directories (${stopwatch.elapsedMilliseconds}ms)');
int processedCount = 0;
int totalMods = modDirectories.length;
for (final modDir in modDirectories) { for (final modDir in modDirectories) {
try { try {
final modStart = stopwatch.elapsedMilliseconds; final modStart = stopwatch.elapsedMilliseconds;
final mod = Mod.fromDirectory(modDir, skipFileCount: skipFileCount); // Check if this directory contains a valid mod
mods[mod.id] = mod; final aboutFile = File('$modDir/About/About.xml');
processedCount++; if (!aboutFile.existsSync()) continue;
final modTime = stopwatch.elapsedMilliseconds - modStart; final mod = Mod.fromDirectory(modDir, skipFileCount: skipFileCount);
if (processedCount % 50 == 0 || processedCount == totalMods) {
print( // If we already have this mod from the config (like Ludeon mods), update its data
'Progress: Loaded $processedCount/$totalMods mods (${stopwatch.elapsedMilliseconds}ms, avg ${stopwatch.elapsedMilliseconds / processedCount}ms per mod)', if (mods.containsKey(mod.id)) {
final existingMod = mods[mod.id]!;
mods[mod.id] = Mod(
name: mod.name,
id: mod.id,
path: mod.path,
versions: mod.versions,
description: mod.description,
hardDependencies: mod.hardDependencies,
softDependencies: mod.softDependencies,
loadBefore: mod.loadBefore,
incompatabilities: mod.incompatabilities,
enabled: activeModIds.contains(mod.id), // Set enabled based on config
size: mod.size,
isBaseGame: existingMod.isBaseGame,
isExpansion: existingMod.isExpansion,
); );
} else {
// Otherwise add as a new mod
mods[mod.id] = Mod(
name: mod.name,
id: mod.id,
path: mod.path,
versions: mod.versions,
description: mod.description,
hardDependencies: mod.hardDependencies,
softDependencies: mod.softDependencies,
loadBefore: mod.loadBefore,
incompatabilities: mod.incompatabilities,
enabled: activeModIds.contains(mod.id), // Set enabled based on config
size: mod.size,
isBaseGame: mod.isBaseGame,
isExpansion: mod.isExpansion,
);
loadedModsCount++;
} }
yield mod; final modTime = stopwatch.elapsedMilliseconds - modStart;
loadingStatus = 'Loaded $loadedModsCount/$totalModsFound mods...';
if (loadedModsCount % 50 == 0 || loadedModsCount == totalModsFound) {
print('Progress: Loaded $loadedModsCount mods (${stopwatch.elapsedMilliseconds}ms)');
}
} catch (e) { } catch (e) {
print('Error loading mod from directory: $modDir'); print('Error loading mod from directory: $modDir');
print('Error: $e'); print('Error: $e');
} }
} }
modsLoaded = true;
final totalTime = stopwatch.elapsedMilliseconds; final totalTime = stopwatch.elapsedMilliseconds;
print( loadingStatus = 'Completed! Loaded $loadedModsCount mods in ${totalTime}ms.';
'Loading complete! Loaded ${mods.length} mods in ${totalTime}ms (${totalTime / mods.length}ms per mod)', print('Loading complete! Loaded ${mods.length} mods in ${totalTime}ms');
); } catch (e) {
} else { loadingStatus = 'Error loading mods: $e';
print('Mods root directory does not exist: $path'); print(loadingStatus);
} }
} }
// Helper function to get a nice expansion name from ID
String _expansionNameFromId(String id) {
final parts = id.split('.');
if (parts.length < 3) return id;
final expansionPart = parts[2];
return expansionPart.substring(0, 1).toUpperCase() + expansionPart.substring(1);
}
// Build a directed graph of mod dependencies // Build a directed graph of mod dependencies
Map<String, Set<String>> buildDependencyGraph() { Map<String, Set<String>> buildDependencyGraph() {
// Graph where graph[A] contains B if A depends on B (B must load before A) // Graph where graph[A] contains B if A depends on B (B must load before A)
@@ -262,6 +398,21 @@ class ModList {
} }
} }
// Handle base game and expansions:
// 1. Add the base game as a dependency of all mods except those who have loadBefore for it
// 2. Add expansions as dependencies of mods that load after them
// First identify the base game and expansions
final baseGameId = mods.values.where((m) => m.isBaseGame).map((m) => m.id).firstOrNull;
if (baseGameId != null) {
for (final mod in mods.values) {
// Skip the base game itself and mods that explicitly load before it
if (mod.id != baseGameId && !mod.loadBefore.contains(baseGameId)) {
graph[mod.id]!.add(baseGameId);
}
}
}
return graph; return graph;
} }
@@ -274,7 +425,7 @@ class ModList {
graph[mod.id] = Set<String>(); graph[mod.id] = Set<String>();
} }
// Add soft dependencies // Add soft dependencies (loadAfter)
for (final mod in mods.values) { for (final mod in mods.values) {
for (final dependency in mod.softDependencies) { for (final dependency in mod.softDependencies) {
// Only add if the dependency exists in our loaded mods // Only add if the dependency exists in our loaded mods
@@ -284,6 +435,16 @@ class ModList {
} }
} }
// Handle loadBefore - invert the relationship for the graph
// If A loadBefore B, then B softDepends on A
for (final mod in mods.values) {
for (final loadBeforeId in mod.loadBefore) {
if (mods.containsKey(loadBeforeId)) {
graph[loadBeforeId]!.add(mod.id);
}
}
}
return graph; return graph;
} }
@@ -528,10 +689,11 @@ class ConfigFile {
ConfigFile({required this.path, this.mods = const []}); ConfigFile({required this.path, this.mods = const []});
void load() { Future<void> load() async {
final file = File(path); final file = File(path);
print('Loading configuration from: $path'); print('Loading configuration from: $path');
try {
final xmlString = file.readAsStringSync(); final xmlString = file.readAsStringSync();
print('XML content read successfully.'); print('XML content read successfully.');
@@ -547,27 +709,60 @@ class ConfigFile {
final modElements = modsElement.findElements("li"); final modElements = modsElement.findElements("li");
print('Found ${modElements.length} active mods.'); print('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>[];
print('Found ${knownExpansionIds.length} known expansions.');
// 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
final isBaseGame = modId == 'ludeon.rimworld';
final isExpansion = !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: modId, name: isBaseGame ? "RimWorld" :
isExpansion ? "RimWorld ${_expansionNameFromId(modId)}" : modId,
id: modId, id: modId,
path: '', path: '',
versions: [], versions: [],
description: '', description: isBaseGame ? "RimWorld base game" :
isExpansion ? "RimWorld expansion" : "",
hardDependencies: [], hardDependencies: [],
softDependencies: [], softDependencies: isExpansion ? ['ludeon.rimworld'] : [],
loadBefore: [],
incompatabilities: [], incompatabilities: [],
enabled: true, enabled: true,
size: 0, size: 0,
isBaseGame: isBaseGame,
isExpansion: isExpansion,
), ),
); );
} }
print('Loaded ${mods.length} mods from config file.'); print('Loaded ${mods.length} mods from config file.');
} catch (e) {
print('Error loading configuration file: $e');
throw Exception('Failed to load config file: $e');
}
}
// Helper function to get a nice expansion name from ID
String _expansionNameFromId(String id) {
final parts = id.split('.');
if (parts.length < 3) return id;
final expansionPart = parts[2];
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