#![warn(clippy::pedantic)]
#![allow(clippy::must_use_candidate)]
use parser::{
creature::Creature, creature_variation::CreatureVariation, unprocessed_raw::UnprocessedRaw,
};
use std::path::{Path, PathBuf};
use tracing::{debug, error, info, warn};
use util::validate_options;
use walkdir::{DirEntry, WalkDir};
mod errors;
mod legends_export;
mod options;
pub mod parser;
mod traits;
pub use errors::ParserError;
#[derive(serde::Serialize, serde::Deserialize)]
pub struct ParseResult {
pub raws: Vec<Box<dyn RawObject>>,
pub info_files: Vec<ModuleInfoFile>,
}
#[cfg(feature = "tauri")]
mod tauri_lib;
pub mod util;
pub use options::ParserOptions;
pub use parser::*;
#[cfg(feature = "tauri")]
pub use tauri_lib::ProgressDetails;
#[cfg(feature = "tauri")]
pub use tauri_lib::ProgressPayload;
#[cfg(feature = "tauri")]
pub use tauri_lib::ProgressTask;
use crate::{
helpers::clone_raw_object_box,
util::{log_summary, summarize_raws},
};
#[allow(clippy::too_many_lines)]
#[allow(clippy::cognitive_complexity)]
pub fn parse(options: &ParserOptions) -> Result<ParseResult, ParserError> {
let options = validate_options(options)?;
let mut results = ParseResult {
raws: Vec::new(),
info_files: Vec::new(),
};
let mut unprocessed_raws: Vec<UnprocessedRaw> = Vec::new();
if !options.locations_to_parse.is_empty() {
let target_path = Path::new(&options.dwarf_fortress_directory);
let data_path = target_path.join("data");
let vanilla_path = data_path.join("vanilla");
let installed_mods_path = data_path.join("installed_mods");
let workshop_mods_path = target_path.join("mods");
if options
.locations_to_parse
.contains(&RawModuleLocation::Vanilla)
{
info!("Dispatching parse for vanilla raws");
let parsed_raws = parse_location(&vanilla_path, &options)?;
results.raws.extend(parsed_raws.parsed_raws);
unprocessed_raws.extend(parsed_raws.unprocessed_raws);
}
if options
.locations_to_parse
.contains(&RawModuleLocation::InstalledMods)
{
info!("Dispatching parse for installed mods");
let parsed_raws = parse_location(&installed_mods_path, &options)?;
results.raws.extend(parsed_raws.parsed_raws);
unprocessed_raws.extend(parsed_raws.unprocessed_raws);
}
if options
.locations_to_parse
.contains(&RawModuleLocation::Mods)
{
info!("Dispatching parse for workshop/downloaded mods");
let parsed_raws = parse_location(&workshop_mods_path, &options)?;
results.raws.extend(parsed_raws.parsed_raws);
unprocessed_raws.extend(parsed_raws.unprocessed_raws);
}
}
if !options.raw_modules_to_parse.is_empty() {
for raw_module in &options.raw_modules_to_parse {
let target_path = Path::new(&raw_module);
let info_txt_path = target_path.join("info.txt");
if info_txt_path.exists() {
info!(
"Dispatching parse for module {:?}",
target_path.file_name().unwrap_or_default()
);
let parsed_raws = parse_module(&target_path, &options)?;
results.raws.extend(parsed_raws.parsed_raws);
unprocessed_raws.extend(parsed_raws.unprocessed_raws);
}
}
}
if !options.raw_files_to_parse.is_empty() {
for raw_file in &options.raw_files_to_parse {
let target_path = Path::new(&raw_file);
info!(
"Dispatching parse for raw file {:?}",
target_path.file_name().unwrap_or_default()
);
let parsed_raws = parser::parse_raw_file(&target_path, &options)?;
results.raws.extend(parsed_raws.parsed_raws);
unprocessed_raws.extend(parsed_raws.unprocessed_raws);
}
}
if !options.legends_exports_to_parse.is_empty() {
for legends_export in &options.legends_exports_to_parse {
let target_path = Path::new(&legends_export);
results
.raws
.extend(legends_export::parse(&target_path, &options)?);
}
}
let creature_variations: Vec<CreatureVariation> = results
.raws
.iter()
.filter_map(|raw| {
if raw.get_type() == &ObjectType::CreatureVariation {
if let Some(cv) = raw
.as_ref()
.as_any()
.downcast_ref::<CreatureVariation>()
.cloned()
{
return Some(cv);
}
error!(
"Matched CreatureVariation but failed to downcast for {}",
raw.get_identifier()
);
}
None
})
.collect();
info!(
"Resolving {} unprocessed creatures using {} creature variation definitions",
unprocessed_raws.len(),
creature_variations.len()
);
let mut simple_unprocessed: Vec<UnprocessedRaw> = Vec::new();
let mut complex_unprocessed: Vec<UnprocessedRaw> = Vec::new();
for unprocessed_raw in unprocessed_raws {
if unprocessed_raw.is_simple() {
simple_unprocessed.push(unprocessed_raw);
} else {
complex_unprocessed.push(unprocessed_raw);
}
}
let resolved_simple_creatures: Vec<Creature> = simple_unprocessed
.iter_mut()
.filter(|raw| raw.raw_type() == ObjectType::Creature)
.filter_map(|raw| {
match raw.resolve(creature_variations.as_slice(), results.raws.as_slice()) {
Ok(c) => Some(c),
Err(e) => {
error!(
"Unable to resolve simple creature {}: {:?}",
raw.get_identifier(),
e
);
None
}
}
})
.map(|c| clone_raw_object_box(&c))
.filter_map(|c| {
c.as_ref().as_any().downcast_ref::<Creature>().map_or_else(
|| {
error!("Downcast failed for simple creature {}", c.get_identifier());
None
},
|creature| Some(creature.clone()),
)
})
.collect();
info!(
"Resolved {} simple creatures",
resolved_simple_creatures.len()
);
results.raws.extend(
resolved_simple_creatures
.iter()
.map(|c| Box::new(c.clone()) as Box<dyn RawObject>),
);
let mut resolved_complex_creatures = 0_usize;
for unprocessed_raw in &mut complex_unprocessed {
if unprocessed_raw.raw_type() == ObjectType::Creature {
match unprocessed_raw.resolve(creature_variations.as_slice(), results.raws.as_slice()) {
Ok(c) => {
resolved_complex_creatures += 1;
results.raws.push(clone_raw_object_box(&c));
}
Err(e) => {
error!(
"Unable to resolve complex creature {}: {:?}",
unprocessed_raw.get_identifier(),
e
);
}
}
}
}
info!("Resolved {resolved_complex_creatures} complex creatures");
results.info_files = parse_module_info_files(&options)?;
if options.log_summary {
let summary = summarize_raws(results.raws.as_slice());
log_summary(&summary);
}
Ok(results)
}
fn parse_module_info_files(options: &ParserOptions) -> Result<Vec<ModuleInfoFile>, ParserError> {
let mut results = Vec::new();
if !options.locations_to_parse.is_empty() {
let target_path = Path::new(&options.dwarf_fortress_directory);
let data_path = target_path.join("data");
let vanilla_path = data_path.join("vanilla");
let installed_mods_path = data_path.join("installed_mods");
let workshop_mods_path = target_path.join("mods");
if options
.locations_to_parse
.contains(&RawModuleLocation::Vanilla)
{
results.extend(parse_module_info_files_at_location(&vanilla_path)?);
}
if options
.locations_to_parse
.contains(&RawModuleLocation::InstalledMods)
{
results.extend(parse_module_info_files_at_location(&installed_mods_path)?);
}
if options
.locations_to_parse
.contains(&RawModuleLocation::Mods)
{
results.extend(parse_module_info_files_at_location(&workshop_mods_path)?);
}
}
if !options.raw_modules_to_parse.is_empty() {
for raw_module_path in options.raw_modules_to_parse.as_slice() {
results.push(parse_module_info_file_in_module(raw_module_path)?);
}
}
if !options.module_info_files_to_parse.is_empty() {
for module_info_file_path in options.module_info_files_to_parse.as_slice() {
results.push(parse_module_info_file_direct(module_info_file_path)?);
}
}
Ok(results)
}
fn parse_module_info_file_in_module<P: AsRef<Path>>(
module_path: &P,
) -> Result<ModuleInfoFile, ParserError> {
let module_path: PathBuf = module_path.as_ref().to_path_buf();
let module_info_file_path = module_path.join("info.txt");
parse_module_info_file_direct(&module_info_file_path)
}
#[cfg(feature = "tauri")]
#[allow(clippy::too_many_lines)]
pub fn parse_with_tauri_emit(
options: &ParserOptions,
window: tauri::Window,
) -> Result<ParseResult, ParserError> {
let mut progress_helper = tauri_lib::ProgressHelper::with_tauri_window(window);
tauri_lib::parse(options, &mut progress_helper)
}
fn parse_location<P: AsRef<Path>>(
location_path: &P,
options: &ParserOptions,
) -> Result<FileParseResults, ParserError> {
let mut results: Vec<Box<dyn RawObject>> = Vec::new();
let mut unprocessed_raws: Vec<UnprocessedRaw> = Vec::new();
let location_path: PathBuf = location_path.as_ref().to_path_buf();
let raw_modules_in_location: Vec<DirEntry> = util::subdirectories(location_path)?;
info!(
"Found {} raw modules in {:?}",
raw_modules_in_location.len(),
options
.locations_to_parse
.first()
.unwrap_or(&RawModuleLocation::Unknown)
);
for raw_module in raw_modules_in_location {
match parse_module(&raw_module.path(), options) {
Ok(module_results) => {
results.extend(module_results.parsed_raws);
unprocessed_raws.extend(module_results.unprocessed_raws);
}
Err(e) => {
debug!("Skipping parsing module: {:?}", e);
}
}
}
Ok(FileParseResults {
parsed_raws: results,
unprocessed_raws,
})
}
fn parse_module_info_files_at_location<P: AsRef<Path>>(
location_path: &P,
) -> Result<Vec<ModuleInfoFile>, ParserError> {
let location_path: PathBuf = location_path.as_ref().to_path_buf();
let raw_modules_in_location: Vec<DirEntry> = util::subdirectories(location_path.clone())?;
info!(
"Found {} raw modules in {:?}",
raw_modules_in_location.len(),
location_path.file_name().unwrap_or_default(),
);
Ok(raw_modules_in_location
.iter()
.filter_map(
|raw_module| match parse_module_info_file_in_module(&raw_module.path()) {
Ok(info_file) => Some(info_file),
Err(e) => {
debug!("Skipping parsing module info file: {:?}", e);
None
}
},
)
.collect::<Vec<ModuleInfoFile>>())
}
fn parse_module_info_file_direct<P: AsRef<Path>>(
module_info_file_path: &P,
) -> Result<ModuleInfoFile, ParserError> {
parser::ModuleInfoFile::parse(module_info_file_path)
}
#[allow(clippy::too_many_lines)]
fn parse_module<P: AsRef<Path>>(
module_path: &P,
options: &ParserOptions,
) -> Result<FileParseResults, ParserError> {
let module_info_file_path = module_path.as_ref().join("info.txt");
let module_info_file: ModuleInfoFile =
match parse_module_info_file_direct(&module_info_file_path) {
Ok(info_file) => info_file,
Err(e) => {
return Err(e);
}
};
let objects_path = module_path.as_ref().join("objects");
let graphics_path = module_path.as_ref().join("graphics");
let mut parse_objects = true;
let mut parse_graphics = options
.object_types_to_parse
.contains(&ObjectType::Graphics);
if !objects_path.exists() {
debug!(
"Ignoring objects directory in {:?} because it does not exist",
module_path.as_ref().file_name().unwrap_or_default(),
);
parse_objects = false;
}
if parse_objects && !objects_path.is_dir() {
debug!(
"Ignoring objects directory in {:?} because it is not a directory",
module_path.as_ref().file_name().unwrap_or_default(),
);
parse_objects = false;
}
if !graphics_path.exists() {
debug!(
"Ignoring graphics directory in {:?} because it does not exist",
module_path.as_ref().file_name().unwrap_or_default(),
);
parse_graphics = false;
}
if parse_graphics && !graphics_path.is_dir() {
debug!(
"Ignoring graphics directory in {:?} because it is not a directory",
module_path.as_ref().file_name().unwrap_or_default(),
);
parse_graphics = false;
}
if !parse_graphics && !parse_objects {
return Ok(FileParseResults {
parsed_raws: Vec::new(),
unprocessed_raws: Vec::new(),
});
}
let mut results: Vec<Box<dyn RawObject>> = Vec::new();
let mut unprocessed_raws: Vec<UnprocessedRaw> = Vec::new();
if parse_objects {
info!(
"Parsing objects for {} v{}",
module_info_file.get_identifier(),
module_info_file.get_version(),
);
for entry in WalkDir::new(objects_path)
.into_iter()
.filter_map(std::result::Result::ok)
{
if entry.file_type().is_file() {
let file_path = entry.path();
let file_name = file_path.file_name().unwrap_or_default();
let file_name_str = file_name.to_str().unwrap_or_default();
if Path::new(file_name_str)
.extension()
.map_or(false, |ext| ext.eq_ignore_ascii_case("txt"))
{
match parser::parse_raw_file(&file_path, options) {
Ok(mut file_parse_results) => {
results.append(&mut file_parse_results.parsed_raws);
unprocessed_raws.append(&mut file_parse_results.unprocessed_raws);
}
Err(e) => {
debug!("Skipping parsing objects: {:?}", e);
}
}
}
}
}
}
if parse_graphics {
info!(
"Parsing graphics for {} v{}",
module_info_file.get_identifier(),
module_info_file.get_version(),
);
for entry in WalkDir::new(graphics_path)
.into_iter()
.filter_map(std::result::Result::ok)
{
if entry.file_type().is_file() {
let file_path = entry.path();
let file_name = file_path.file_name().unwrap_or_default();
let file_name_str = file_name.to_str().unwrap_or_default();
if Path::new(file_name_str)
.extension()
.map_or(false, |ext| ext.eq_ignore_ascii_case("txt"))
{
match parser::parse_raw_file(&file_path, options) {
Ok(mut graphics) => {
results.append(&mut graphics.parsed_raws);
unprocessed_raws.append(&mut graphics.unprocessed_raws);
}
Err(e) => {
debug!("Skipping parsing graphics: {:?}", e);
}
}
}
}
}
}
Ok(FileParseResults {
parsed_raws: results,
unprocessed_raws,
})
}
pub fn build_search_string(raw_object: &dyn Searchable) -> String {
crate::parser::get_search_string(raw_object)
}