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

@@ -1,4 +1,5 @@
import 'dart:io';
import 'dart:async';
import 'package:xml/xml.dart';
const root = r'C:/Users/Administrator/Seafile/Games-Rimworld';
@@ -25,13 +26,14 @@ class Mod {
final String path; // figure it out
final List<String> versions; // ModMetaData.supportedVersions
final String description; // ModMetaData.description
final List<String>
hardDependencies; // ModMetaData.modDependencies - this is a li with packageId, displayName, steamWorkshopUrl and downloadUrl
final List<String> hardDependencies; // ModMetaData.modDependencies
final List<String> softDependencies; // ModMetaData.loadAfter
final List<String> loadBefore; // ModMetaData.loadBefore
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 bool isBaseGame; // Is this the base RimWorld game
final bool isExpansion; // Is this a RimWorld expansion
Mod({
required this.name,
@@ -41,9 +43,12 @@ class Mod {
required this.description,
required this.hardDependencies,
required this.softDependencies,
required this.loadBefore,
required this.incompatabilities,
required this.enabled,
required this.size,
this.isBaseGame = false,
this.isExpansion = false,
});
static Mod fromDirectory(String path, {bool skipFileCount = false}) {
@@ -135,6 +140,19 @@ class Mod {
// 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 = [];
try {
incompatabilities =
@@ -165,6 +183,15 @@ class Mod {
.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 =
stopwatch.elapsedMilliseconds - metadataTime - xmlTime;
final totalTime = stopwatch.elapsedMilliseconds;
@@ -182,9 +209,12 @@ class Mod {
description: description,
hardDependencies: hardDependencies,
softDependencies: softDependencies,
loadBefore: loadBefore,
incompatabilities: incompatabilities,
enabled: false,
size: size,
isBaseGame: isBaseGame,
isExpansion: isExpansion,
);
}
}
@@ -192,55 +222,161 @@ class Mod {
class ModList {
final String path;
Map<String, Mod> mods = {};
bool modsLoaded = false;
String loadingStatus = '';
int totalModsFound = 0;
int loadedModsCount = 0;
ModList({required this.path});
Stream<Mod> load({bool skipFileCount = false}) async* {
// Simplified loading with config file first
Future<void> loadWithConfig({bool skipFileCount = false}) async {
// Clear existing state if reloading
if (modsLoaded) {
mods.clear();
}
modsLoaded = false;
loadedModsCount = 0;
loadingStatus = 'Loading active mods from config...';
final stopwatch = Stopwatch()..start();
final directory = Directory(path);
print('Loading configuration from: $path');
print('Loading configuration from config file: $configPath');
if (directory.existsSync()) {
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;
}
final List<FileSystemEntity> entities = directory.listSync();
final List<String> modDirectories =
entities.whereType<Directory>().map((dir) => dir.path).toList();
print(
'Found ${modDirectories.length} mod directories (${stopwatch.elapsedMilliseconds}ms)',
);
int processedCount = 0;
int totalMods = modDirectories.length;
totalModsFound = modDirectories.length;
loadingStatus = 'Found $totalModsFound mod directories. Loading...';
print('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()) continue;
final mod = Mod.fromDirectory(modDir, skipFileCount: skipFileCount);
mods[mod.id] = mod;
processedCount++;
final modTime = stopwatch.elapsedMilliseconds - modStart;
if (processedCount % 50 == 0 || processedCount == totalMods) {
print(
'Progress: Loaded $processedCount/$totalMods mods (${stopwatch.elapsedMilliseconds}ms, avg ${stopwatch.elapsedMilliseconds / processedCount}ms per mod)',
// If we already have this mod from the config (like Ludeon mods), update its data
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++;
}
final modTime = stopwatch.elapsedMilliseconds - modStart;
loadingStatus = 'Loaded $loadedModsCount/$totalModsFound mods...';
if (loadedModsCount % 50 == 0 || loadedModsCount == totalModsFound) {
print('Progress: Loaded $loadedModsCount mods (${stopwatch.elapsedMilliseconds}ms)');
}
yield mod;
} catch (e) {
print('Error loading mod from directory: $modDir');
print('Error: $e');
}
}
modsLoaded = true;
final totalTime = stopwatch.elapsedMilliseconds;
print(
'Loading complete! Loaded ${mods.length} mods in ${totalTime}ms (${totalTime / mods.length}ms per mod)',
);
} else {
print('Mods root directory does not exist: $path');
loadingStatus = 'Completed! Loaded $loadedModsCount mods in ${totalTime}ms.';
print('Loading complete! Loaded ${mods.length} mods in ${totalTime}ms');
} catch (e) {
loadingStatus = 'Error loading mods: $e';
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
Map<String, Set<String>> buildDependencyGraph() {
@@ -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;
}
@@ -274,7 +425,7 @@ class ModList {
graph[mod.id] = Set<String>();
}
// Add soft dependencies
// Add soft dependencies (loadAfter)
for (final mod in mods.values) {
for (final dependency in mod.softDependencies) {
// 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;
}
@@ -528,46 +689,80 @@ class ConfigFile {
ConfigFile({required this.path, this.mods = const []});
void load() {
Future<void> load() async {
final file = File(path);
print('Loading configuration from: $path');
final xmlString = file.readAsStringSync();
print('XML content read successfully.');
try {
final xmlString = file.readAsStringSync();
print('XML content read successfully.');
final xmlDocument = XmlDocument.parse(xmlString);
print('XML document parsed successfully.');
final xmlDocument = XmlDocument.parse(xmlString);
print('XML document parsed successfully.');
final modConfigData = xmlDocument.findElements("ModsConfigData").first;
print('Found ModsConfigData element.');
final modConfigData = xmlDocument.findElements("ModsConfigData").first;
print('Found ModsConfigData element.');
final modsElement = modConfigData.findElements("activeMods").first;
print('Found activeMods element.');
final modsElement = modConfigData.findElements("activeMods").first;
print('Found activeMods element.');
final modElements = modsElement.findElements("li");
print('Found ${modElements.length} active mods.');
final modElements = modsElement.findElements("li");
print('Found ${modElements.length} active mods.');
mods = [];
for (final modElement in modElements) {
final modId = modElement.innerText.toLowerCase();
// We'll populate with dummy mods for now, they'll be replaced later
mods.add(
Mod(
name: modId,
id: modId,
path: '',
versions: [],
description: '',
hardDependencies: [],
softDependencies: [],
incompatabilities: [],
enabled: true,
size: 0,
),
);
// 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 = [];
for (final modElement in modElements) {
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
mods.add(
Mod(
name: isBaseGame ? "RimWorld" :
isExpansion ? "RimWorld ${_expansionNameFromId(modId)}" : modId,
id: modId,
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,
),
);
}
print('Loaded ${mods.length} mods from config file.');
} catch (e) {
print('Error loading configuration file: $e');
throw Exception('Failed to load config file: $e');
}
print('Loaded ${mods.length} mods from config file.');
}
// 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