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 dependencies; // ModMetaData.modDependencies final List loadAfter; // ModMetaData.loadAfter final List loadBefore; // ModMetaData.loadBefore final List incompatibilities; // ModMetaData.incompatibleWith bool 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 int loadBeforeNotPlaced = 0; int loadAfterPlaced = 0; Mod({ required this.name, required this.id, required this.path, required this.versions, required this.description, required this.dependencies, required this.loadAfter, required this.loadBefore, required this.incompatibilities, required this.size, this.isBaseGame = false, this.isExpansion = false, this.enabled = 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 dependencies = []; try { dependencies = metadata .findElements('modDependenciesByVersion') .first .children .whereType() .last .findElements('li') .map( (e) => e.findElements("packageId").first.innerText.toLowerCase(), ) .toList(); logger.info('Dependencies found: ${dependencies.join(", ")}'); } catch (e) { logger.warning( '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 incompatibilities = []; try { incompatibilities = metadata .findElements('incompatibleWith') .first .findElements('li') .map((e) => e.innerText.toLowerCase()) .toList(); logger.info('Incompatibilities found: ${incompatibilities.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, dependencies: dependencies, loadAfter: loadAfter, loadBefore: loadBefore, incompatibilities: incompatibilities, size: size, isBaseGame: isBaseGame, isExpansion: isExpansion, ); } Mod copyWith({ String? name, String? id, String? path, List? versions, String? description, List? dependencies, List? loadAfter, List? loadBefore, List? incompatibilities, int? size, bool? isBaseGame, bool? isExpansion, bool? enabled, }) { return Mod( name: name ?? this.name, id: id ?? this.id, path: path ?? this.path, versions: versions ?? this.versions, description: description ?? this.description, dependencies: dependencies ?? this.dependencies, loadAfter: loadAfter ?? this.loadAfter, loadBefore: loadBefore ?? this.loadBefore, incompatibilities: incompatibilities ?? this.incompatibilities, size: size ?? this.size, isBaseGame: isBaseGame ?? this.isBaseGame, isExpansion: isExpansion ?? this.isExpansion, enabled: enabled ?? this.enabled, ); } }