Files
flutter-rimworld-modman/lib/modloader.dart

266 lines
7.9 KiB
Dart

import 'dart:io';
import 'package:xml/xml.dart';
const root = r'C:/Users/Administrator/Seafile/Games-Rimworld';
const modsRoot = '$root/294100';
const configRoot = '$root/AppData/RimWorld by Ludeon Studios/Config';
const configPath = '$configRoot/ModsConfig.xml';
XmlElement findCaseInsensitive(XmlElement element, String name) {
return element.childElements.firstWhere(
(e) => e.name.local.toLowerCase() == name,
);
}
XmlElement findCaseInsensitiveDoc(XmlDocument document, String name) {
name = name.toLowerCase();
return document.childElements.firstWhere(
(e) => e.name.local.toLowerCase() == name,
);
}
class Mod {
final String name; // ModMetaData.name
final String id; // ModMetaData.packageId
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> softDependencies; // ModMetaData.loadAfter
final List<String> incompatabilities; // ModMetaData.incompatibleWith
final bool
enabled; // ConfigFile.mods.firstWhere((mod) => mod.id == id).enabled
final int size; // Count of files in the mod directory
Mod({
required this.name,
required this.id,
required this.path,
required this.versions,
required this.description,
required this.hardDependencies,
required this.softDependencies,
required this.incompatabilities,
required this.enabled,
required this.size,
});
static Mod fromDirectory(String path, {bool skipFileCount = false}) {
final stopwatch = Stopwatch()..start();
final aboutFile = File('$path/About/About.xml');
if (!aboutFile.existsSync()) {
throw Exception('About.xml file does not exist in $aboutFile');
}
final aboutXml = XmlDocument.parse(aboutFile.readAsStringSync());
final xmlTime = stopwatch.elapsedMilliseconds;
late final XmlElement metadata;
try {
metadata = findCaseInsensitiveDoc(aboutXml, 'ModMetaData');
} catch (e) {
throw Exception(
'Error: ModMetaData element is missing in About.xml ($aboutFile). Original error: $e',
);
}
late final String name;
try {
name = metadata.findElements('name').first.innerText;
} catch (e) {
throw Exception(
'Error: name element is missing in ModMetaData ($aboutFile). Original error: $e',
);
}
late final String id;
try {
id = metadata.findElements('packageId').first.innerText.toLowerCase();
} catch (e) {
throw Exception(
'Error: packageId element is missing in ModMetaData ($aboutFile). Original error: $e',
);
}
late final List<String> versions;
try {
versions =
metadata
.findElements('supportedVersions')
.first
.findElements('li')
.map((e) => e.innerText)
.toList();
} catch (e) {
throw Exception(
'Error: supportedVersions or li elements are missing in ModMetaData ($aboutFile). Original error: $e',
);
}
String description = '';
try {
description = metadata.findElements('description').first.innerText;
} catch (e) {
// Silent error for optional element
}
List<String> hardDependencies = [];
try {
hardDependencies =
metadata
.findElements('modDependenciesByVersion')
.last
.findElements('li')
.map(
(e) =>
e.findElements('packageId').first.innerText.toLowerCase(),
)
.toList();
} catch (e) {
// Silent error for optional element
}
List<String> softDependencies = [];
try {
softDependencies =
metadata
.findElements('loadAfter')
.first
.findElements('li')
.map((e) => e.innerText.toLowerCase())
.toList();
} catch (e) {
// Silent error for optional element
}
List<String> incompatabilities = [];
try {
incompatabilities =
metadata
.findElements('incompatibleWith')
.first
.findElements('li')
.map((e) => e.innerText.toLowerCase())
.toList();
} catch (e) {
// Silent error for optional element
}
final metadataTime = stopwatch.elapsedMilliseconds - xmlTime;
int size = 0;
if (!skipFileCount) {
size = Directory(path)
.listSync(recursive: true)
.where(
(entity) =>
!entity.path
.split(Platform.pathSeparator)
.last
.startsWith('.'),
)
.length;
}
final fileCountTime = stopwatch.elapsedMilliseconds - metadataTime - xmlTime;
final totalTime = stopwatch.elapsedMilliseconds;
// Uncomment for detailed timing
print('Mod $name timing: XML=${xmlTime}ms, Metadata=${metadataTime}ms, FileCount=${fileCountTime}ms, Total=${totalTime}ms');
return Mod(
name: name,
id: id,
path: path,
versions: versions,
description: description,
hardDependencies: hardDependencies,
softDependencies: softDependencies,
incompatabilities: incompatabilities,
enabled: false,
size: size,
);
}
}
class ModList {
final String path;
Map<String, Mod> mods = {};
ModList({required this.path});
Stream<Mod> load({bool skipFileCount = false}) async* {
final stopwatch = Stopwatch()..start();
final directory = Directory(path);
print('Loading configuration from: $path');
if (directory.existsSync()) {
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;
for (final modDir in modDirectories) {
try {
final modStart = stopwatch.elapsedMilliseconds;
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)');
}
yield mod;
} catch (e) {
print('Error loading mod from directory: $modDir');
print('Error: $e');
}
}
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');
}
}
}
class ConfigFile {
final String path;
List<Mod> mods;
ConfigFile({required this.path, this.mods = const []});
void load() {
final file = File(path);
print('Loading configuration from: $path');
final xmlString = file.readAsStringSync();
print('XML content read successfully.');
final xmlDocument = XmlDocument.parse(xmlString);
print('XML document parsed successfully.');
final modConfigData = xmlDocument.findElements("ModsConfigData").first;
print('Found ModsConfigData element.');
final modsElement = modConfigData.findElements("activeMods").first;
print('Found activeMods element.');
final mods = modsElement.findElements("li");
print('Found ${mods.length} active mods.');
for (final mod in mods) {
// print('Mod found: ${mod.innerText}');
}
}
}