Files
flutter-rimworld-modman/lib/mod.dart
2025-03-18 17:18:03 +01:00

290 lines
8.8 KiB
Dart

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<String> versions; // ModMetaData.supportedVersions
final String description; // ModMetaData.description
final List<String> dependencies; // ModMetaData.modDependencies
final List<String> loadAfter; // ModMetaData.loadAfter
final List<String> loadBefore; // ModMetaData.loadBefore
final List<String> 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
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<String> 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<String> dependencies = [];
try {
dependencies =
metadata
.findElements('modDependenciesByVersion')
.first
.children
.whereType<XmlElement>()
.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<String> 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<String> 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<String> 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<String>? versions,
String? description,
List<String>? dependencies,
List<String>? loadAfter,
List<String>? loadBefore,
List<String>? 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,
);
}
}