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