294 lines
8.9 KiB
Dart
294 lines
8.9 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
|
|
|
|
bool visited = false;
|
|
bool mark = false;
|
|
int position = -1;
|
|
|
|
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,
|
|
);
|
|
}
|
|
}
|