import 'dart:io'; import 'package:flutter/material.dart'; import 'package:rimworld_modman/logger.dart'; import 'package:rimworld_modman/mod.dart'; import 'package:rimworld_modman/mod_list.dart'; // Constants for file paths final String root = Platform.isWindows ? r'C:/Users/Administrator/Seafile/Games-RimWorld' : '~/Library/Application Support/RimWorld'; final String modsRoot = Platform.isWindows ? '$root/294100' : '$root/Mods'; final String configRoot = Platform.isWindows ? '$root/AppData/RimWorld by Ludeon Studios/Config' : '$root/Config'; final String configPath = '$configRoot/ModsConfig.xml'; final String logsPath = '$root/ModManager'; late ModList modManager; void main() { WidgetsFlutterBinding.ensureInitialized(); // Get a reference to the logger (now auto-initializes) final logger = Logger.instance; logger.info('RimWorld Mod Manager starting...'); // Initialize the mod manager modManager = ModList(modsPath: modsRoot, configPath: configPath); // Start the app runApp(const RimWorldModManager()); } class RimWorldModManager extends StatelessWidget { const RimWorldModManager({super.key}); @override Widget build(BuildContext context) { return MaterialApp( title: 'RimWorld Mod Manager', theme: ThemeData.dark().copyWith( primaryColor: const Color(0xFF3D4A59), colorScheme: ColorScheme.fromSeed( seedColor: const Color(0xFF3D4A59), brightness: Brightness.dark, ), scaffoldBackgroundColor: const Color(0xFF1E262F), cardColor: const Color(0xFF2A3440), appBarTheme: const AppBarTheme( backgroundColor: Color(0xFF2A3440), foregroundColor: Colors.white, ), ), home: const ModManagerHomePage(), ); } } class ModManagerHomePage extends StatefulWidget { const ModManagerHomePage({super.key}); @override State createState() => _ModManagerHomePageState(); } class _ModManagerHomePageState extends State { int _selectedIndex = 0; final List _pages = [ const ModManagerPage(), const TroubleshootingPage(), ]; @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar(title: const Text('RimWorld Mod Manager')), body: _pages[_selectedIndex], bottomNavigationBar: BottomNavigationBar( currentIndex: _selectedIndex, onTap: (index) { setState(() { _selectedIndex = index; }); }, items: const [ BottomNavigationBarItem(icon: Icon(Icons.extension), label: 'Mods'), BottomNavigationBarItem( icon: Icon(Icons.build), label: 'Troubleshoot', ), ], ), ); } } // Combined page for mod management with two-panel layout class ModManagerPage extends StatefulWidget { const ModManagerPage({super.key}); @override State createState() => _ModManagerPageState(); } class _ModManagerPageState extends State { // For all available mods (left panel) List _availableMods = []; // For active mods (right panel) List _activeMods = []; bool _isLoading = false; String _statusMessage = ''; int _totalModsFound = 0; bool _skipFileCount = false; bool _hasCycles = false; List? _cycleInfo; List> _incompatibleMods = []; List? _loadOrderErrors; final TextEditingController _searchController = TextEditingController(); String _searchQuery = ''; @override void initState() { super.initState(); // Check if mods are already loaded if (modManager.mods.isNotEmpty) { _loadModsFromGlobalState(); } _searchController.addListener(() { setState(() { _searchQuery = _searchController.text.toLowerCase(); }); }); } @override void dispose() { _searchController.dispose(); super.dispose(); } void _loadModsFromGlobalState() { setState(() { // Get all mods for the left panel (sorted alphabetically) _availableMods = modManager.mods.values.toList(); _availableMods.sort((a, b) => a.name.compareTo(b.name)); // Get active mods for the right panel (in load order) _activeMods = modManager.mods.values.where((m) => m.enabled).toList(); _isLoading = false; _statusMessage = 'Loaded ${_availableMods.length} mods'; _totalModsFound = _availableMods.length; }); } @override Widget build(BuildContext context) { return Scaffold( body: _isLoading && _availableMods.isEmpty ? _buildLoadingView() : _availableMods.isEmpty ? _buildEmptyState() : _buildSplitView(), ); } Widget _buildLoadingView() { return Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ const CircularProgressIndicator(), const SizedBox(height: 16), Text( _statusMessage, style: Theme.of(context).textTheme.bodyMedium, textAlign: TextAlign.center, ), ], ), ); } Widget _buildEmptyState() { return Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ const Icon(Icons.extension, size: 64), const SizedBox(height: 16), Text( 'Mod Manager', style: Theme.of(context).textTheme.headlineMedium, ), const SizedBox(height: 16), Text( 'Ready to scan for RimWorld mods.', style: Theme.of(context).textTheme.bodyLarge, ), const SizedBox(height: 12), Row( mainAxisSize: MainAxisSize.min, children: [ Checkbox( value: _skipFileCount, onChanged: (value) { setState(() { _skipFileCount = value ?? true; }); }, ), const Text('Skip file counting (faster loading)'), ], ), const SizedBox(height: 24), ElevatedButton( onPressed: _startLoadingMods, child: const Text('Scan for Mods'), ), ], ), ); } Widget _buildSplitView() { // Filter both available and active mods based on search query final filteredAvailableMods = _searchQuery.isEmpty ? _availableMods : _availableMods .where( (mod) => mod.name.toLowerCase().contains(_searchQuery) || mod.id.toLowerCase().contains(_searchQuery), ) .toList(); final filteredActiveMods = _searchQuery.isEmpty ? _activeMods : _activeMods .where( (mod) => mod.name.toLowerCase().contains(_searchQuery) || mod.id.toLowerCase().contains(_searchQuery), ) .toList(); return Column( children: [ Padding( padding: const EdgeInsets.all(8.0), child: Row( children: [ // Search field Expanded( child: TextField( controller: _searchController, decoration: InputDecoration( hintText: 'Search mods by name or ID...', prefixIcon: const Icon(Icons.search), border: OutlineInputBorder( borderRadius: BorderRadius.circular(8.0), ), suffixIcon: _searchQuery.isNotEmpty ? IconButton( icon: const Icon(Icons.clear), onPressed: () { _searchController.clear(); }, ) : null, ), ), ), const SizedBox(width: 8), // Reload button IconButton( icon: const Icon(Icons.refresh), tooltip: 'Reload mods', onPressed: _startLoadingMods, ), const SizedBox(width: 8), // Load Dependencies button Tooltip( message: 'Automatically load missing dependencies for active mods', child: ElevatedButton.icon( icon: const Icon(Icons.download), label: const Text('Load Deps'), onPressed: _loadRequiredDependencies, ), ), const SizedBox(width: 8), // Auto-sort button ElevatedButton.icon( icon: const Icon(Icons.sort), label: const Text('Auto-Sort'), onPressed: _sortActiveMods, ), const SizedBox(width: 8), // Save button ElevatedButton.icon( icon: const Icon(Icons.save), label: const Text('Save'), onPressed: _saveModOrder, ), ], ), ), // Status message if (!_isLoading && _statusMessage.isNotEmpty) Padding( padding: const EdgeInsets.symmetric(horizontal: 8.0, vertical: 4.0), child: Text( _statusMessage, style: TextStyle( color: _hasCycles || _incompatibleMods.isNotEmpty ? Colors.orange : Colors.green, ), ), ), // Error display section if (_hasCycles || _incompatibleMods.isNotEmpty || (_loadOrderErrors?.isNotEmpty ?? false)) Container( margin: const EdgeInsets.symmetric(horizontal: 8.0, vertical: 4.0), padding: const EdgeInsets.all(8.0), decoration: BoxDecoration( color: Colors.red.shade900.withOpacity(0.3), borderRadius: BorderRadius.circular(4.0), border: Border.all(color: Colors.red.shade800), ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ // Cycle warnings if (_hasCycles && _cycleInfo != null) Padding( padding: const EdgeInsets.only(bottom: 4.0), child: Row( children: [ const Icon(Icons.loop, color: Colors.orange, size: 16), const SizedBox(width: 4), Expanded( child: Text( 'Dependency cycle detected: ${_cycleInfo!.join(" -> ")}', style: const TextStyle(color: Colors.orange), ), ), ], ), ), // Incompatible mod warnings if (_incompatibleMods.isNotEmpty) Padding( padding: const EdgeInsets.only(bottom: 4.0), child: Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ const Icon( Icons.warning, color: Colors.orange, size: 16, ), const SizedBox(width: 4), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( '${_incompatibleMods.length} incompatible mod pairs:', style: const TextStyle(color: Colors.orange), ), if (_incompatibleMods.length <= 3) ...List.generate(_incompatibleMods.length, ( index, ) { final pair = _incompatibleMods[index]; final mod1 = modManager.mods[pair[0]]?.name ?? pair[0]; final mod2 = modManager.mods[pair[1]]?.name ?? pair[1]; return Padding( padding: const EdgeInsets.only( left: 12.0, top: 2.0, ), child: Text( '• $mod1 ↔ $mod2', style: TextStyle( color: Colors.orange.shade300, fontSize: 12, ), ), ); }), ], ), ), ], ), ), // Other errors (missing dependencies, etc.) if (_loadOrderErrors?.isNotEmpty ?? false) Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ const Icon( Icons.error_outline, color: Colors.red, size: 16, ), const SizedBox(width: 4), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ const Text( 'Dependency errors:', style: TextStyle(color: Colors.red), ), ...List.generate( _loadOrderErrors!.length > 5 ? 5 : _loadOrderErrors!.length, (index) => Padding( padding: const EdgeInsets.only( left: 12.0, top: 2.0, ), child: Text( '• ${_loadOrderErrors![index]}', style: TextStyle( color: Colors.red.shade300, fontSize: 12, ), ), ), ), if (_loadOrderErrors!.length > 5) Padding( padding: const EdgeInsets.only( left: 12.0, top: 4.0, ), child: Text( '(${_loadOrderErrors!.length - 5} more errors...)', style: TextStyle( color: Colors.red.shade300, fontSize: 12, fontStyle: FontStyle.italic, ), ), ), ], ), ), ], ), ], ), ), // Main split view Expanded( child: Row( children: [ // LEFT PANEL - All available mods (alphabetical) Expanded( child: Card( margin: const EdgeInsets.all(8.0), child: Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ Container( color: Theme.of(context).primaryColor, padding: const EdgeInsets.all(8.0), child: Text( 'Available Mods (${filteredAvailableMods.length}${_searchQuery.isNotEmpty ? '/${_availableMods.length}' : ''})', style: const TextStyle(fontWeight: FontWeight.bold), textAlign: TextAlign.center, ), ), if (_searchQuery.isNotEmpty) Padding( padding: const EdgeInsets.all(4.0), child: Text( 'Searching: "$_searchQuery"', style: TextStyle( fontSize: 12, fontStyle: FontStyle.italic, color: Colors.grey.shade400, ), textAlign: TextAlign.center, ), ), Expanded( child: ListView.builder( itemCount: filteredAvailableMods.length, itemBuilder: (context, index) { final mod = filteredAvailableMods[index]; final isActive = mod.enabled; return GestureDetector( onDoubleTap: () => _toggleModActive(mod), child: ListTile( title: Text( mod.name, style: TextStyle( color: isActive ? Colors.green : Colors.white, ), ), subtitle: Text( 'ID: ${mod.id}\nSize: ${mod.size} files', style: Theme.of(context).textTheme.bodySmall, ), trailing: Row( mainAxisSize: MainAxisSize.min, children: [ if (mod.isBaseGame) const Tooltip( message: 'Base Game', child: Icon( Icons.home, color: Colors.blue, size: 24, ), ), if (mod.isExpansion) const Tooltip( message: 'Expansion', child: Icon( Icons.star, color: Colors.yellow, size: 24, ), ), const SizedBox(width: 4), if (mod.dependencies.isNotEmpty) Tooltip( message: 'Dependencies:\n${mod.dependencies.join('\n')}', child: const Icon( Icons.link, color: Colors.orange, size: 24, ), ), if (mod.loadAfter.isNotEmpty) Tooltip( message: 'Loads after:\n${mod.loadAfter.join('\n')}', child: const Icon( Icons.arrow_downward, color: Colors.blue, size: 24, ), ), if (mod.loadBefore.isNotEmpty) Tooltip( message: 'Loads before:\n${mod.loadBefore.join('\n')}', child: const Icon( Icons.arrow_upward, color: Colors.green, size: 24, ), ), ], ), onTap: () { // Show mod details in future }, ), ); }, ), ), ], ), ), ), // RIGHT PANEL - Active mods (load order) Expanded( child: Card( margin: const EdgeInsets.all(8.0), child: Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ Container( color: Theme.of(context).primaryColor, padding: const EdgeInsets.all(8.0), child: Text( 'Active Mods (${filteredActiveMods.length}${_searchQuery.isNotEmpty ? '/${_activeMods.length}' : ''})', style: const TextStyle(fontWeight: FontWeight.bold), textAlign: TextAlign.center, ), ), Padding( padding: const EdgeInsets.all(8.0), child: Text( _searchQuery.isNotEmpty ? 'Searching: "$_searchQuery"' : 'Larger mods are prioritized during auto-sorting.', style: TextStyle( fontSize: 12, color: Colors.grey.shade400, fontStyle: FontStyle.italic, ), textAlign: TextAlign.center, ), ), Expanded( child: ListView.builder( itemCount: filteredActiveMods.length, itemBuilder: (context, index) { final mod = filteredActiveMods[index]; // Find the actual position in the complete active mods list (for correct load order) final actualLoadOrderPosition = _activeMods.indexWhere((m) => m.id == mod.id) + 1; return GestureDetector( onDoubleTap: () { // Don't allow deactivating base game or expansions if (!mod.isBaseGame && !mod.isExpansion) { _toggleModActive(mod); } else { ScaffoldMessenger.of(context).showSnackBar( const SnackBar( content: Text( 'Core game and expansions cannot be deactivated', ), backgroundColor: Colors.orange, ), ); } }, child: Card( margin: const EdgeInsets.symmetric( horizontal: 8.0, vertical: 4.0, ), child: ListTile( leading: SizedBox( width: 50, child: Column( mainAxisSize: MainAxisSize.min, mainAxisAlignment: MainAxisAlignment.center, children: [ SizedBox( height: 24, child: Container( padding: const EdgeInsets.symmetric( horizontal: 4, ), decoration: BoxDecoration( color: _searchQuery.isNotEmpty ? Colors.blue.withOpacity( 0.2, ) : null, borderRadius: BorderRadius.circular(4), ), child: Tooltip( message: 'Load position: $actualLoadOrderPosition of ${_activeMods.length}${_searchQuery.isNotEmpty ? " (preserved when filtering)" : ""}', child: Text( '$actualLoadOrderPosition', style: TextStyle( fontWeight: FontWeight.bold, fontSize: 12, color: _searchQuery.isNotEmpty ? Colors.blue.shade300 : null, ), ), ), ), ), SizedBox( height: 24, child: Center( child: mod.size > 0 ? Tooltip( message: 'This mod contains ${mod.size} files.', child: Text( '${mod.size}', style: const TextStyle( fontSize: 10, color: Colors.grey, ), ), ) : const SizedBox(), ), ), ], ), ), title: Text(mod.name), subtitle: Text( mod.id, style: const TextStyle(fontSize: 12), ), trailing: Row( mainAxisSize: MainAxisSize.min, children: [ if (mod.isBaseGame) const Tooltip( message: 'Base Game', child: Icon( Icons.home, color: Colors.blue, size: 24, ), ), if (mod.isExpansion) const Tooltip( message: 'Expansion', child: Icon( Icons.star, color: Colors.yellow, size: 24, ), ), if (mod.dependencies.isNotEmpty) Tooltip( message: 'Dependencies:\n${mod.dependencies.join('\n')}', child: const Icon( Icons.link, color: Colors.orange, size: 24, ), ), const SizedBox(width: 4), if (mod.loadAfter.isNotEmpty) Tooltip( message: 'Loads after other mods:\n${mod.loadAfter.join('\n')}', child: const Icon( Icons.arrow_downward, color: Colors.blue, size: 24, ), ), const SizedBox(width: 4), if (mod.loadBefore.isNotEmpty) Tooltip( message: 'Loads before other mods:\n${mod.loadBefore.join('\n')}', child: const Icon( Icons.arrow_upward, color: Colors.green, size: 24, ), ), ], ), onTap: () { // Show mod details in future }, ), ), ); }, ), ), ], ), ), ), ], ), ), ], ); } void _startLoadingMods() { setState(() { _availableMods.clear(); _activeMods.clear(); _isLoading = true; _statusMessage = 'Scanning for mods...'; _hasCycles = false; _cycleInfo = null; _incompatibleMods = []; }); // Create an async function to load mods Future loadMods() async { try { // First load available mods await for (final mod in modManager.loadAvailable()) { // Update UI for each mod loaded if (mounted) { setState(() { _statusMessage = 'Loaded mod: ${mod.name}'; _totalModsFound = modManager.mods.length; }); } } // Then load active mods from config await for (final mod in modManager.loadActive()) { // Update UI as active mods are loaded if (mounted) { setState(() { _statusMessage = 'Loading active mod: ${mod.name}'; }); } } // Update the UI with all loaded mods if (mounted) { _loadModsFromGlobalState(); setState(() { _statusMessage = 'Loaded ${_availableMods.length} mods, ${_activeMods.length} active'; }); } } catch (error) { if (mounted) { setState(() { _isLoading = false; _statusMessage = 'Error loading mods: $error'; }); } } } // Start the loading process loadMods(); } void _toggleModActive(Mod mod) { // Cannot deactivate base game or expansions if ((mod.isBaseGame || mod.isExpansion) && mod.enabled) { ScaffoldMessenger.of(context).showSnackBar( const SnackBar( content: Text('Core game and expansions cannot be deactivated'), backgroundColor: Colors.orange, ), ); return; } // Get the current state before toggling final bool wasEnabled = mod.enabled; // Toggle the mod in the global mod manager modManager.setEnabled(mod.id, !wasEnabled); Logger.instance.info( 'Toggled mod ${mod.name} (${mod.id}) from ${wasEnabled ? 'enabled' : 'disabled'} to ${!wasEnabled ? 'enabled' : 'disabled'}', ); // Update the UI setState(() { // Update in the available mods list final index = _availableMods.indexWhere((m) => m.id == mod.id); if (index >= 0) { _availableMods[index] = modManager.mods[mod.id]!; } // Update the active mods list _activeMods = modManager.mods.values.where((m) => m.enabled).toList(); _statusMessage = 'Mod "${mod.name}" ${!wasEnabled ? 'activated' : 'deactivated'}'; }); } void _sortActiveMods() async { if (_activeMods.isEmpty) { setState(() { _statusMessage = 'No active mods to sort'; }); return; } setState(() { _isLoading = true; _statusMessage = 'Sorting mods based on dependencies...'; _hasCycles = false; _cycleInfo = null; _incompatibleMods = []; _loadOrderErrors = null; }); // Use a Future.delayed to allow the UI to update await Future.delayed(Duration.zero); try { final logger = Logger.instance; logger.info('Starting auto-sort of ${_activeMods.length} active mods'); // Generate a load order for active mods final loadOrder = modManager.generateLoadOrder(); // Store all errors if (loadOrder.hasErrors) { setState(() { _loadOrderErrors = List.from(loadOrder.errors); }); logger.warning( 'Found ${loadOrder.errors.length} errors during sorting', ); for (final error in loadOrder.errors) { logger.warning(' - $error'); } setState(() { _hasCycles = loadOrder.errors.any( (e) => e.contains('Cyclic dependency'), ); if (_hasCycles) { // Extract cycle info from error message final cycleError = loadOrder.errors.firstWhere( (e) => e.contains('Cyclic dependency'), orElse: () => '', ); logger.warning('Detected dependency cycle: $cycleError'); if (cycleError.isNotEmpty) { // Extract cycle path from error message final startIndex = cycleError.indexOf(':'); if (startIndex != -1) { final pathStr = cycleError.substring(startIndex + 1).trim(); _cycleInfo = pathStr.split(' -> '); logger.info( 'Extracted cycle path: ${_cycleInfo!.join(" -> ")}', ); } } } }); } else { _loadOrderErrors = null; } // Check for incompatibilities _incompatibleMods = modManager.checkIncompatibilities( modManager.activeMods.keys.toList(), ); if (_incompatibleMods.isNotEmpty) { logger.warning( 'Found ${_incompatibleMods.length} incompatible mod pairs:', ); for (final pair in _incompatibleMods) { final mod1 = modManager.mods[pair[0]]?.name ?? pair[0]; final mod2 = modManager.mods[pair[1]]?.name ?? pair[1]; logger.warning(' - $mod1 is incompatible with $mod2'); } } // Get sorted mods from the load order final List sortedMods = []; for (final modId in loadOrder.loadOrder) { if (modManager.mods.containsKey(modId)) { sortedMods.add(modManager.mods[modId]!); } } logger.info( 'Sorting complete. Arranged ${sortedMods.length} mods in load order', ); if (sortedMods.isNotEmpty) { logger.info( 'First 5 mods in order: ${sortedMods.take(5).map((m) => m.name).join(', ')}', ); if (sortedMods.length > 5) { logger.info('... (${sortedMods.length - 5} more) ...'); logger.info( 'Last 3 mods in order: ${sortedMods.reversed.take(3).map((m) => m.name).toList().reversed.join(', ')}', ); } } setState(() { _activeMods = sortedMods; _isLoading = false; _statusMessage = 'Sorting complete! ${sortedMods.length} mods sorted.'; if (_hasCycles) { _statusMessage += ' Warning: Dependency cycles were found and fixed.'; } if (_incompatibleMods.isNotEmpty) { _statusMessage += ' Warning: ${_incompatibleMods.length} incompatible mod pairs found.'; } }); } catch (e) { Logger.instance.error('Error during auto-sort: $e'); setState(() { _isLoading = false; _statusMessage = 'Error sorting mods: $e'; }); } } void _saveModOrder() async { if (_activeMods.isEmpty) { setState(() { _statusMessage = 'No active mods to save'; }); return; } setState(() { _isLoading = true; _statusMessage = 'Saving mod load order...'; }); try { final logger = Logger.instance; logger.info( 'Saving mod load order for ${_activeMods.length} active mods to $configPath', ); // Save the mod list to the XML config file final file = File(configPath); final buffer = StringBuffer(); buffer.writeln(''); buffer.writeln(''); buffer.writeln(' 1'); // Write active mods buffer.writeln(' '); for (final mod in _activeMods) { buffer.writeln('
  • ${mod.id}
  • '); logger.info(' - Adding mod to config: ${mod.name} (${mod.id})'); } buffer.writeln('
    '); // Count expansions final expansions = _availableMods.where((m) => m.isExpansion).toList(); logger.info('Found ${expansions.length} expansions to include in config'); // Add known expansions buffer.writeln(' '); for (final mod in expansions) { buffer.writeln('
  • ${mod.id}
  • '); logger.info(' - Adding expansion to config: ${mod.name} (${mod.id})'); } buffer.writeln('
    '); buffer.writeln('
    '); // Ensure directory exists final directory = Directory(configRoot); if (!directory.existsSync()) { logger.info('Creating config directory: $configRoot'); directory.createSync(recursive: true); } // Write to file logger.info('Writing config file to $configPath'); await file.writeAsString(buffer.toString()); logger.info('Successfully saved mod configuration'); setState(() { _isLoading = false; _statusMessage = 'Mod load order saved successfully!'; }); // Show success message ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Text( 'Mod order saved to config. ${_activeMods.length} mods enabled.', ), backgroundColor: Colors.green, ), ); } catch (e) { final logger = Logger.instance; logger.error('Error saving mod load order: $e'); setState(() { _isLoading = false; _statusMessage = 'Error saving mod load order: $e'; }); // Show error message ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Text('Error saving mod order: $e'), backgroundColor: Colors.red, ), ); } } // Load all required dependencies for active mods void _loadRequiredDependencies() async { if (_activeMods.isEmpty) { setState(() { _statusMessage = 'No active mods to load dependencies for'; }); return; } setState(() { _isLoading = true; _statusMessage = 'Loading required dependencies...'; _hasCycles = false; _cycleInfo = null; _incompatibleMods = []; _loadOrderErrors = null; }); // Use a Future.delayed to allow the UI to update await Future.delayed(Duration.zero); try { final logger = Logger.instance; logger.info( 'Starting dependency resolution for ${_activeMods.length} active mods', ); // Get current active mod count for comparison final initialActiveCount = modManager.activeMods.length; // Load required dependencies and get the load order final loadOrder = modManager.loadRequired(); // Store any errors if (loadOrder.hasErrors) { setState(() { _loadOrderErrors = List.from(loadOrder.errors); }); logger.warning( 'Found ${loadOrder.errors.length} errors during dependency loading', ); for (final error in loadOrder.errors) { logger.warning(' - $error'); } // Check for cycles setState(() { _hasCycles = loadOrder.errors.any( (e) => e.contains('Cyclic dependency'), ); if (_hasCycles) { // Extract cycle info from error message final cycleError = loadOrder.errors.firstWhere( (e) => e.contains('Cyclic dependency'), orElse: () => '', ); logger.warning('Detected dependency cycle: $cycleError'); if (cycleError.isNotEmpty) { // Extract cycle path from error message final startIndex = cycleError.indexOf(':'); if (startIndex != -1) { final pathStr = cycleError.substring(startIndex + 1).trim(); _cycleInfo = pathStr.split(' -> '); logger.info( 'Extracted cycle path: ${_cycleInfo!.join(" -> ")}', ); } } } }); } else { _loadOrderErrors = null; } // Check for incompatibilities _incompatibleMods = modManager.checkIncompatibilities( modManager.activeMods.keys.toList(), ); if (_incompatibleMods.isNotEmpty) { logger.warning( 'Found ${_incompatibleMods.length} incompatible mod pairs:', ); for (final pair in _incompatibleMods) { final mod1 = modManager.mods[pair[0]]?.name ?? pair[0]; final mod2 = modManager.mods[pair[1]]?.name ?? pair[1]; logger.warning(' - $mod1 is incompatible with $mod2'); } } // Get sorted mods from the load order final List sortedMods = []; for (final modId in loadOrder.loadOrder) { if (modManager.mods.containsKey(modId)) { sortedMods.add(modManager.mods[modId]!); } } // Calculate how many dependencies were added final newModsCount = modManager.activeMods.length - initialActiveCount; logger.info( 'Dependency loading complete. Enabled $newModsCount new dependencies.', ); if (newModsCount > 0) { logger.info('Newly enabled dependencies:'); final activeModIds = modManager.activeMods.keys.toList(); for (int i = 0; i < newModsCount; i++) { final modId = activeModIds[initialActiveCount + i]; logger.info(' - ${modManager.mods[modId]?.name ?? modId} ($modId)'); } } setState(() { _activeMods = sortedMods; _isLoading = false; if (newModsCount > 0) { _statusMessage = 'Added $newModsCount required dependencies!'; } else { _statusMessage = 'All dependencies are already loaded.'; } if (_hasCycles) { _statusMessage += ' Warning: Dependency cycles were found and fixed.'; } if (_incompatibleMods.isNotEmpty) { _statusMessage += ' Warning: ${_incompatibleMods.length} incompatible mod pairs found.'; } }); } catch (e) { Logger.instance.error('Error during dependency loading: $e'); setState(() { _isLoading = false; _statusMessage = 'Error loading dependencies: $e'; }); } } } // Page for troubleshooting problematic mods class TroubleshootingPage extends StatelessWidget { const TroubleshootingPage({super.key}); @override Widget build(BuildContext context) { return Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ const Icon(Icons.build, size: 64), const SizedBox(height: 16), Text( 'Troubleshooting', style: Theme.of(context).textTheme.headlineMedium, ), const SizedBox(height: 16), Padding( padding: const EdgeInsets.symmetric(horizontal: 32.0), child: Text( 'Find problematic mods with smart batch testing.', style: Theme.of(context).textTheme.bodyLarge, textAlign: TextAlign.center, ), ), const SizedBox(height: 24), ElevatedButton( onPressed: () { // TODO: Implement troubleshooting wizard }, child: const Text('Start Troubleshooting'), ), ], ), ); } }