diff --git a/lib/mod.dart b/lib/mod.dart new file mode 100644 index 0000000..3489ec1 --- /dev/null +++ b/lib/mod.dart @@ -0,0 +1,259 @@ +import 'dart:io'; + +import 'package:rimworld_modman/logger.dart'; +import 'package:xml/xml.dart'; + +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 versions; // ModMetaData.supportedVersions + final String description; // ModMetaData.description + final List hardDependencies; // ModMetaData.modDependencies + final List loadAfter; // ModMetaData.loadAfter + final List loadBefore; // ModMetaData.loadBefore + final List incompatabilities; // ModMetaData.incompatibleWith + 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, + required this.id, + required this.path, + required this.versions, + required this.description, + required this.hardDependencies, + required this.loadAfter, + 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}) { + final logger = Logger.instance; + final stopwatch = Stopwatch()..start(); + + logger.info('Attempting to load mod from directory: $path'); + final aboutFile = File('$path/About/About.xml'); + if (!aboutFile.existsSync()) { + logger.error('About.xml file does not exist in $aboutFile'); + throw Exception('About.xml file does not exist in $aboutFile'); + } + + logger.info('Parsing About.xml file...'); + final aboutXml = XmlDocument.parse(aboutFile.readAsStringSync()); + final xmlTime = stopwatch.elapsedMilliseconds; + + late final XmlElement metadata; + try { + metadata = findCaseInsensitiveDoc(aboutXml, 'ModMetaData'); + logger.info('Successfully found ModMetaData in About.xml'); + } catch (e) { + logger.error( + 'Error: ModMetaData element is missing in About.xml ($aboutFile). Original error: $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; + logger.info('Mod name found: $name'); + } catch (e) { + logger.error( + 'Error: name element is missing in ModMetaData ($aboutFile). Original error: $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(); + logger.info('Mod ID found: $id'); + } catch (e) { + logger.error( + 'Error: packageId element is missing in ModMetaData ($aboutFile). Original error: $e', + ); + throw Exception( + 'Error: packageId element is missing in ModMetaData ($aboutFile). Original error: $e', + ); + } + + late final List versions; + try { + versions = + metadata + .findElements('supportedVersions') + .first + .findElements('li') + .map((e) => e.innerText) + .toList(); + logger.info('Supported versions found: ${versions.join(", ")}'); + } catch (e) { + logger.error( + 'Error: supportedVersions or li elements are missing in ModMetaData ($aboutFile). Original error: $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; + logger.info('Mod description found: $description'); + } catch (e) { + logger.warning( + 'Description element is missing in ModMetaData ($aboutFile).', + ); + } + + List hardDependencies = []; + try { + hardDependencies = + metadata + .findElements('modDependenciesByVersion') + .first + .children + .whereType() + .last + .findElements('li') + .map( + (e) => + e.findElements("packageId").first.innerText.toLowerCase(), + ) + .toList(); + logger.info('Hard dependencies found: ${hardDependencies.join(", ")}'); + } catch (e) { + logger.warning( + 'Hard dependencies element is missing in ModMetaData ($aboutFile).', + ); + } + + List loadAfter = []; + try { + loadAfter = + metadata + .findElements('loadAfter') + .first + .findElements('li') + .map((e) => e.innerText.toLowerCase()) + .toList(); + logger.info('Load after dependencies found: ${loadAfter.join(", ")}'); + } catch (e) { + logger.warning( + 'Load after element is missing in ModMetaData ($aboutFile).', + ); + } + + List loadBefore = []; + try { + loadBefore = + metadata + .findElements('loadBefore') + .first + .findElements('li') + .map((e) => e.innerText.toLowerCase()) + .toList(); + logger.info('Load before dependencies found: ${loadBefore.join(", ")}'); + } catch (e) { + logger.warning( + 'Load before element is missing in ModMetaData ($aboutFile).', + ); + } + + List incompatabilities = []; + try { + incompatabilities = + metadata + .findElements('incompatibleWith') + .first + .findElements('li') + .map((e) => e.innerText.toLowerCase()) + .toList(); + logger.info('Incompatibilities found: ${incompatabilities.join(", ")}'); + } catch (e) { + logger.warning( + 'Incompatibilities element is missing in ModMetaData ($aboutFile).', + ); + } + + 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; + logger.info('File count in mod directory: $size'); + } + + // 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 && !loadAfter.contains('ludeon.rimworld')) { + loadAfter.add('ludeon.rimworld'); + logger.info( + 'Added base game dependency for expansion mod: ludeon.rimworld', + ); + } + + final fileCountTime = + stopwatch.elapsedMilliseconds - metadataTime - xmlTime; + final totalTime = stopwatch.elapsedMilliseconds; + + // Log detailed timing information + logger.info( + '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, + loadAfter: loadAfter, + loadBefore: loadBefore, + incompatabilities: incompatabilities, + enabled: false, + size: size, + isBaseGame: isBaseGame, + isExpansion: isExpansion, + ); + } +} diff --git a/lib/modloader.dart b/lib/modloader.dart index 9711ed4..a52dd06 100644 --- a/lib/modloader.dart +++ b/lib/modloader.dart @@ -1,6 +1,7 @@ import 'dart:io'; import 'dart:async'; import 'package:rimworld_modman/logger.dart'; +import 'package:rimworld_modman/mod.dart'; import 'package:xml/xml.dart'; const root = r'C:/Users/Administrator/Seafile/Games-Rimworld'; @@ -9,261 +10,6 @@ const configRoot = '$root/AppData/RimWorld by Ludeon Studios/Config'; const configPath = '$configRoot/ModsConfig.xml'; const logsPath = '$root/ModManager'; -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 versions; // ModMetaData.supportedVersions - final String description; // ModMetaData.description - final List hardDependencies; // ModMetaData.modDependencies - final List loadAfter; // ModMetaData.loadAfter - final List loadBefore; // ModMetaData.loadBefore - final List incompatabilities; // ModMetaData.incompatibleWith - 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, - required this.id, - required this.path, - required this.versions, - required this.description, - required this.hardDependencies, - required this.loadAfter, - 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}) { - final logger = Logger.instance; - final stopwatch = Stopwatch()..start(); - - logger.info('Attempting to load mod from directory: $path'); - final aboutFile = File('$path/About/About.xml'); - if (!aboutFile.existsSync()) { - logger.error('About.xml file does not exist in $aboutFile'); - throw Exception('About.xml file does not exist in $aboutFile'); - } - - logger.info('Parsing About.xml file...'); - final aboutXml = XmlDocument.parse(aboutFile.readAsStringSync()); - final xmlTime = stopwatch.elapsedMilliseconds; - - late final XmlElement metadata; - try { - metadata = findCaseInsensitiveDoc(aboutXml, 'ModMetaData'); - logger.info('Successfully found ModMetaData in About.xml'); - } catch (e) { - logger.error( - 'Error: ModMetaData element is missing in About.xml ($aboutFile). Original error: $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; - logger.info('Mod name found: $name'); - } catch (e) { - logger.error( - 'Error: name element is missing in ModMetaData ($aboutFile). Original error: $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(); - logger.info('Mod ID found: $id'); - } catch (e) { - logger.error( - 'Error: packageId element is missing in ModMetaData ($aboutFile). Original error: $e', - ); - throw Exception( - 'Error: packageId element is missing in ModMetaData ($aboutFile). Original error: $e', - ); - } - - late final List versions; - try { - versions = - metadata - .findElements('supportedVersions') - .first - .findElements('li') - .map((e) => e.innerText) - .toList(); - logger.info('Supported versions found: ${versions.join(", ")}'); - } catch (e) { - logger.error( - 'Error: supportedVersions or li elements are missing in ModMetaData ($aboutFile). Original error: $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; - logger.info('Mod description found: $description'); - } catch (e) { - logger.warning( - 'Description element is missing in ModMetaData ($aboutFile).', - ); - } - - List hardDependencies = []; - try { - hardDependencies = - metadata - .findElements('modDependenciesByVersion') - .first - .children - .whereType() - .last - .findElements('li') - .map( - (e) => - e.findElements("packageId").first.innerText.toLowerCase(), - ) - .toList(); - logger.info('Hard dependencies found: ${hardDependencies.join(", ")}'); - } catch (e) { - logger.warning( - 'Hard dependencies element is missing in ModMetaData ($aboutFile).', - ); - } - - List loadAfter = []; - try { - loadAfter = - metadata - .findElements('loadAfter') - .first - .findElements('li') - .map((e) => e.innerText.toLowerCase()) - .toList(); - logger.info('Load after dependencies found: ${loadAfter.join(", ")}'); - } catch (e) { - logger.warning( - 'Load after element is missing in ModMetaData ($aboutFile).', - ); - } - - List loadBefore = []; - try { - loadBefore = - metadata - .findElements('loadBefore') - .first - .findElements('li') - .map((e) => e.innerText.toLowerCase()) - .toList(); - logger.info('Load before dependencies found: ${loadBefore.join(", ")}'); - } catch (e) { - logger.warning( - 'Load before element is missing in ModMetaData ($aboutFile).', - ); - } - - List incompatabilities = []; - try { - incompatabilities = - metadata - .findElements('incompatibleWith') - .first - .findElements('li') - .map((e) => e.innerText.toLowerCase()) - .toList(); - logger.info('Incompatibilities found: ${incompatabilities.join(", ")}'); - } catch (e) { - logger.warning( - 'Incompatibilities element is missing in ModMetaData ($aboutFile).', - ); - } - - 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; - logger.info('File count in mod directory: $size'); - } - - // 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 && !loadAfter.contains('ludeon.rimworld')) { - loadAfter.add('ludeon.rimworld'); - logger.info( - 'Added base game dependency for expansion mod: ludeon.rimworld', - ); - } - - final fileCountTime = - stopwatch.elapsedMilliseconds - metadataTime - xmlTime; - final totalTime = stopwatch.elapsedMilliseconds; - - // Log detailed timing information - logger.info( - '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, - loadAfter: loadAfter, - loadBefore: loadBefore, - incompatabilities: incompatabilities, - enabled: false, - size: size, - isBaseGame: isBaseGame, - isExpansion: isExpansion, - ); - } -} - class ModList { final String path; Map mods = {};