diff --git a/dcb-tools/Cargo.toml b/dcb-tools/Cargo.toml new file mode 100644 index 0000000..93c3753 --- /dev/null +++ b/dcb-tools/Cargo.toml @@ -0,0 +1,53 @@ +[package] +name = "dcb-tools" +version = "0.1.0" +authors = ["Filipe Rodrigues "] +edition = "2018" + +[[bin]] +name = "extractor" +path = "src/extractor/main.rs" + +[[bin]] +name = "patcher" +path = "src/patcher/main.rs" + +[dependencies] +# Dcb +dcb = { path = "../dcb" } + +# Text +ascii = "1.0" + +# Helpers +float-ord = "0.2" +itertools = "0.9" +rand = "0.7" + +# Cmd +clap = "2.33" + +# Logging +log = "0.4" +simplelog = "0.7" + +# Error handling +string-err = "0.1" +err-backtrace = { path = "../err-backtrace" } +err-panic = { path = "../err-panic", features = ["string-err-ext"] } +err-ext = { path = "../err-ext" } +err-impl = { git = "https://github.com/Zenithsiz/err-impl" } + +# Derives +derive_more = "0.99" +smart-default = "0.6" + +# Serde +serde = "1.0" +serde_yaml = "0.8" + +# Build dependencies in release mode +[profile.dev.package."*"] +opt-level = 2 +[profile.dev.package.dcb] # Except dcb for debugging +opt-level = 0 diff --git a/dcb-tools/rustfmt.toml b/dcb-tools/rustfmt.toml new file mode 100644 index 0000000..f0c05cf --- /dev/null +++ b/dcb-tools/rustfmt.toml @@ -0,0 +1,57 @@ +# We're fine with unstable features +unstable_features = true + + + +binop_separator = "Back" +hard_tabs = true +condense_wildcard_suffixes = true +match_block_trailing_comma = true +newline_style = "Unix" +reorder_impl_items = true +overflow_delimited_expr = true +use_field_init_shorthand = true +enum_discrim_align_threshold = 100 +fn_args_layout = "Compressed" +merge_derives = false +merge_imports = true +max_width = 150 +use_small_heuristics = "Default" +indent_style = "Block" +wrap_comments = false +format_code_in_doc_comments = false +comment_width = 150 +normalize_comments = false +normalize_doc_attributes = false +license_template_path = "" +format_strings = true +format_macro_matchers = true +format_macro_bodies = true +empty_item_single_line = true +struct_lit_single_line = true +fn_single_line = false +where_single_line = false +imports_indent = "Block" +imports_layout = "Mixed" +reorder_imports = true +reorder_modules = true +type_punctuation_density = "Wide" +space_before_colon = false +space_after_colon = true +spaces_around_ranges = false +remove_nested_parens = true +combine_control_expr = true +struct_field_align_threshold = 20 +match_arm_blocks = true +force_multiline_blocks = false +brace_style = "SameLineWhere" +control_brace_style = "AlwaysSameLine" +trailing_semicolon = true +trailing_comma = "Vertical" +blank_lines_lower_bound = 0 +blank_lines_upper_bound = 2 +inline_attribute_width = 0 +use_try_shorthand = true +force_explicit_abi = true +error_on_line_overflow = true +error_on_unformatted = true diff --git a/dcb-tools/src/extractor/cli.rs b/dcb-tools/src/extractor/cli.rs new file mode 100644 index 0000000..9f3fe38 --- /dev/null +++ b/dcb-tools/src/extractor/cli.rs @@ -0,0 +1,63 @@ +//! Cli manager for the extractor + +// Filesystem +use std::path::{Path, PathBuf}; + +// Clap +use clap::{App as ClapApp, Arg as ClapArg}; + +// Errors +use err_panic::ErrorExtPanic; + +/// Data from the command line +#[derive(PartialEq, Clone, Debug)] +pub struct CliData { + /// The game file + pub game_file_path: PathBuf, + + /// The ouput directory + pub output_dir: PathBuf, +} + +impl CliData { + /// Constructs all of the cli data given and returns it + pub fn new() -> Self { + // Get all matches from cli + let matches = ClapApp::new("Dcb Extractor") + .version("0.0") + .author("Filipe [...] <[...]@gmail.com>") + .about("Extracts all data from a Digimon Digital Card Battle `.bin` game file") + .arg( + ClapArg::with_name("GAME_FILE") + .help("Sets the input game file to use") + .required(true) + .index(1), + ) + .arg( + ClapArg::with_name("OUTPUT") + .help("Sets the output directory to use") + .short("o") + .long("output") + .takes_value(true) + .required(false), + ) + .get_matches(); + + // Get the input filename + // Note: required + let game_file_path = matches + .value_of("GAME_FILE") + .map(Path::new) + .map(Path::to_path_buf) + .panic_msg("Unable to get required argument `GAME_FILE`"); + + // Try to get the output + let output_dir = match matches.value_of("OUTPUT") { + Some(output) => PathBuf::from(output), + None => game_file_path.parent().unwrap_or_else(|| Path::new(".")).to_path_buf(), + }; + + // Return the cli data + Self { game_file_path, output_dir } + } +} diff --git a/dcb-tools/src/extractor/main.rs b/dcb-tools/src/extractor/main.rs new file mode 100644 index 0000000..95c5da1 --- /dev/null +++ b/dcb-tools/src/extractor/main.rs @@ -0,0 +1,73 @@ +//! Data extractor +//! +//! # Details +//! Extracts data from the game file to several other files, that can be +//! edited and then used by `Patcher` to modify the game file. +//! +//! # Syntax +//! The executable may be called as `./extractor {-o }` +//! +//! Use the command `./extractor --help` for more information. +//! +//! # Data extracted +//! Currently only the following is extracted: +//! - Card table + +// Features +#![feature(box_syntax, backtrace, panic_info_message)] +// Lints +#![warn(clippy::restriction, clippy::pedantic, clippy::nursery)] +#![allow( + clippy::implicit_return, // We prefer implicit returns where possible + clippy::module_name_repetitions, // This happens often due to separating things into modules finely + clippy::wildcard_enum_match_arm, // We only use wildcards when we truly only care about some variants + clippy::result_expect_used, + clippy::option_expect_used, // We use expect when there is no alternative. + clippy::used_underscore_binding, // Useful for macros and such +)] + +// Modules +// TODO: `cargo fmt` cannot use this syntax, possibly change it once possible +mod cli; +#[path = "../logger.rs"] +mod logger; +#[path = "../panic.rs"] +mod panic; + +// Exports +use cli::CliData; + +// Dcb +use dcb::{ + game::{card::Table as CardTable, deck::Table as DeckTable}, + GameFile, +}; + +// Errors +use err_panic::ErrorExtPanic; + +fn main() { + // Initialize the logger and set the panic handler + logger::init(); + std::panic::set_hook(box panic::log_handler); + + // Get all data from cli + let CliData { game_file_path, output_dir } = CliData::new(); + + // Open the game file + let input_file = std::fs::File::open(&game_file_path).panic_err_msg("Unable to open input file"); + let mut game_file = GameFile::from_reader(input_file).panic_err_msg("Unable to parse input file as dcb"); + + // Get the cards table + let cards_table = CardTable::deserialize(&mut game_file).panic_err_msg("Unable to deserialize cards table from game file"); + let cards_table_yaml = serde_yaml::to_string(&cards_table).panic_err_msg("Unable to serialize cards table to yaml"); + log::info!("Extracted {} cards", cards_table.card_count()); + + // Get the decks table + let decks_table = DeckTable::deserialize(&mut game_file).panic_err_msg("Unable to deserialize decks table from game file"); + let decks_table_yaml = serde_yaml::to_string(&decks_table).panic_err_msg("Unable to serialize decks table to yaml"); + + // And output everything to the files + std::fs::write(&output_dir.join("cards.yaml"), cards_table_yaml).panic_err_msg("Unable to write cards table to file"); + std::fs::write(&output_dir.join("decks.yaml"), decks_table_yaml).panic_err_msg("Unable to write decks table to file"); +} diff --git a/dcb-tools/src/logger.rs b/dcb-tools/src/logger.rs new file mode 100644 index 0000000..be7bff5 --- /dev/null +++ b/dcb-tools/src/logger.rs @@ -0,0 +1,27 @@ +//! Logger initialization + +// Log +use log::LevelFilter; +use simplelog::{CombinedLogger, Config, SharedLogger, TermLogger, TerminalMode, WriteLogger}; + +// Error +use err_ext::ResultExt; + +/// The type of logger required to pass to `CombinedLogger::init` +type BoxedLogger = Box; + +/// Initializes the global logger +pub fn init() { + // All loggers to try and initialize + let loggers: Vec> = vec![ + TermLogger::new(LevelFilter::Warn, Config::default(), TerminalMode::Mixed).map(|logger| BoxedLogger::from(logger)), + std::fs::File::create("latest.log") + .ok() + .map(|file| WriteLogger::new(LevelFilter::Trace, Config::default(), file)) + .map(|logger| BoxedLogger::from(logger)), + ]; + + // Filter all logger that actually work and initialize them + CombinedLogger::init(loggers.into_iter().filter_map(std::convert::identity).collect()) + .ignore_with_err(|_| log::warn!("Logger was already initialized at the start of the program")); +} diff --git a/dcb-tools/src/panic.rs b/dcb-tools/src/panic.rs new file mode 100644 index 0000000..10657ef --- /dev/null +++ b/dcb-tools/src/panic.rs @@ -0,0 +1,31 @@ +//! Panic handlers for this application + +// Std +use std::{ + backtrace::{Backtrace, BacktraceStatus}, + error::Error, +}; +// Error backtrace +use err_backtrace::ErrBacktraceExt; + +/// Panic handler based on logging to the current initialization +pub fn log_handler(info: &std::panic::PanicInfo) { + // Log that this thread has panicked + log::error!("Thread \"{}\" panicked", std::thread::current().name().unwrap_or("[Unknown]")); + + // Log any message that came with the panic + log::info!("Panic message: {}", info.message().unwrap_or(&format_args!("None"))); + + // Print an error backtrace if we found any + if let Some(err) = info.payload().downcast_ref::>() { + log::info!("Error backtrace:\n{}", err.err_backtrace()); + } + + // And print a backtrace of where this panic occured. + let backtrace = Backtrace::force_capture(); + if backtrace.status() == BacktraceStatus::Captured { + log::info!("Backtrace:\n{}", backtrace); + } else { + log::info!("Unable to get backtrace: {}", backtrace); + } +} diff --git a/dcb-tools/src/patcher/cli.rs b/dcb-tools/src/patcher/cli.rs new file mode 100644 index 0000000..30bf7fb --- /dev/null +++ b/dcb-tools/src/patcher/cli.rs @@ -0,0 +1,58 @@ +//! Cli manager for the extractor + +// Filesystem +use std::path::{Path, PathBuf}; + +// Clap +use clap::{App as ClapApp, Arg as ClapArg}; + +// Errors +use err_panic::ErrorExtPanic; + +/// Data from the command line +#[derive(PartialEq, Clone, Debug)] +pub struct CliData { + /// The game file + pub game_file_path: PathBuf, + + /// The input directory + pub input_dir: PathBuf, +} + +impl CliData { + /// Constructs all of the cli data given and returns it + pub fn new() -> Self { + // Get all matches from cli + let matches = ClapApp::new("Dcb Patcher") + .version("0.0") + .author("Filipe [...] <[...]@gmail.com>") + .about("Patches data to a Digimon Digital Card Battle `.bin` game file") + .arg(ClapArg::with_name("GAME_FILE").help("Sets the game file to use").required(true).index(1)) + .arg( + ClapArg::with_name("INPUT") + .help("Sets the input directory to use") + .short("i") + .long("input") + .takes_value(true) + .required(false), + ) + .get_matches(); + + // Get the ouput filename + // Note: required + let game_file_path = matches + .value_of("GAME_FILE") + .map(Path::new) + .map(Path::to_path_buf) + .panic_msg("Unable to get required argument `GAME_FILE`"); + + // Get the input dir as either an input, the game file directory or the current directory + let input_dir = matches + .value_of("INPUT") + .map_or_else(|| game_file_path.parent().unwrap_or_else(|| Path::new(".")), Path::new) + .to_path_buf(); + + // Return the cli data + Self { game_file_path, input_dir } + } +} diff --git a/dcb-tools/src/patcher/main.rs b/dcb-tools/src/patcher/main.rs new file mode 100644 index 0000000..09db450 --- /dev/null +++ b/dcb-tools/src/patcher/main.rs @@ -0,0 +1,74 @@ +//! Data patches +//! +//! # Details +//! Patches data to the game file from several other files. +//! +//! # Syntax +//! The executable may be called as `./patcher ` +//! +//! Use the command `./patcher --help` for more information. +//! +//! # Data patched +//! Currently only the following is patched: +//! - Card table + +// Features +#![feature(box_syntax, backtrace, panic_info_message)] +// Lints +#![warn(clippy::restriction, clippy::pedantic, clippy::nursery)] +#![allow( + clippy::implicit_return, // We prefer implicit returns where possible + clippy::module_name_repetitions, // This happens often due to separating things into modules finely + clippy::wildcard_enum_match_arm, // We only use wildcards when we truly only care about some variants + clippy::result_expect_used, + clippy::option_expect_used, // We use expect when there is no alternative. + clippy::used_underscore_binding, // Useful for macros and such +)] + +// Modules +mod cli; +#[path = "../logger.rs"] +mod logger; +#[path = "../panic.rs"] +mod panic; + +// Exports +use cli::CliData; + +// Dcb +use dcb::{ + game::{card::Table as CardTable, deck::Table as DeckTable}, + GameFile, +}; + +// Errors +use err_panic::ErrorExtPanic; + +fn main() { + // Initialize the logger and set the panic handler + logger::init(); + std::panic::set_hook(box panic::log_handler); + + // Get all data from cli + let CliData { game_file_path, input_dir } = CliData::new(); + + // Load the card table + let cards_table_file = std::fs::File::open(input_dir.join("cards.yaml")).panic_err_msg("Unable to open `cards.yaml`"); + let cards_table: CardTable = serde_yaml::from_reader(cards_table_file).panic_err_msg("Unable to parse `cards.yaml`"); + + // Load the decks table + let decks_table_file = std::fs::File::open(input_dir.join("decks.yaml")).panic_err_msg("Unable to open `decks.yaml`"); + let decks_table: DeckTable = serde_yaml::from_reader(decks_table_file).panic_err_msg("Unable to parse `decks.yaml`"); + + // Open the game file + let game_file = std::fs::OpenOptions::new() + .write(true) + .truncate(false) + .open(game_file_path) + .panic_err_msg("Unable to open game file"); + let mut game_file = GameFile::from_reader(game_file).panic_err_msg("Unable to initialize game file"); + + // And write everything + cards_table.serialize(&mut game_file).panic_err_msg("Unable to serialize cards table"); + decks_table.serialize(&mut game_file).panic_err_msg("Unable to serialize decks table"); +}