Fix loadBefore and Rimworld and expansions
This commit is contained in:
183
lib/main.dart
183
lib/main.dart
@@ -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
|
||||||
|
@@ -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
|
||||||
|
Reference in New Issue
Block a user