diff --git a/.gitignore b/.gitignore index f20fcf9..ea8c4bf 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1 @@ /target -/cli/target diff --git a/Cargo.lock b/Cargo.lock index a87f7bb..a71bcc7 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,15 +2,6 @@ # It is not intended for manual editing. version = 4 -[[package]] -name = "android_system_properties" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" -dependencies = [ - "libc", -] - [[package]] name = "anstream" version = "0.6.21" @@ -61,47 +52,6 @@ dependencies = [ "windows-sys", ] -[[package]] -name = "autocfg" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" - -[[package]] -name = "bumpalo" -version = "3.19.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "46c5e41b57b8bba42a04676d81cb89e9ee8e859a1a66f80a5a72e1cb76b34d43" - -[[package]] -name = "cc" -version = "1.2.48" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c481bdbf0ed3b892f6f806287d72acd515b352a4ec27a208489b8c1bc839633a" -dependencies = [ - "find-msvc-tools", - "shlex", -] - -[[package]] -name = "cfg-if" -version = "1.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" - -[[package]] -name = "chrono" -version = "0.4.42" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "145052bdd345b87320e369255277e3fb5152762ad123a901ef5c262dd38fe8d2" -dependencies = [ - "iana-time-zone", - "js-sys", - "num-traits", - "wasm-bindgen", - "windows-link", -] - [[package]] name = "clap" version = "4.5.53" @@ -148,24 +98,12 @@ version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75" -[[package]] -name = "core-foundation-sys" -version = "0.8.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" - [[package]] name = "equivalent" version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" -[[package]] -name = "find-msvc-tools" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3a3076410a55c90011c298b04d0cfa770b00fa04e1e3c97d3f6c9de105a03844" - [[package]] name = "hashbrown" version = "0.16.1" @@ -178,30 +116,6 @@ version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" -[[package]] -name = "iana-time-zone" -version = "0.1.64" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "33e57f83510bb73707521ebaffa789ec8caf86f9657cad665b092b581d40e9fb" -dependencies = [ - "android_system_properties", - "core-foundation-sys", - "iana-time-zone-haiku", - "js-sys", - "log", - "wasm-bindgen", - "windows-core", -] - -[[package]] -name = "iana-time-zone-haiku" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" -dependencies = [ - "cc", -] - [[package]] name = "indexmap" version = "2.12.1" @@ -218,43 +132,6 @@ version = "1.70.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695" -[[package]] -name = "js-sys" -version = "0.3.83" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "464a3709c7f55f1f721e5389aa6ea4e3bc6aba669353300af094b29ffbdde1d8" -dependencies = [ - "once_cell", - "wasm-bindgen", -] - -[[package]] -name = "libc" -version = "0.2.177" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2874a2af47a2325c2001a6e6fad9b16a53b802102b528163885171cf92b15976" - -[[package]] -name = "log" -version = "0.4.28" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "34080505efa8e45a4b816c349525ebe327ceaa8559756f0356cba97ef3bf7432" - -[[package]] -name = "num-traits" -version = "0.2.19" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" -dependencies = [ - "autocfg", -] - -[[package]] -name = "once_cell" -version = "1.21.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" - [[package]] name = "once_cell_polyfill" version = "1.70.2" @@ -279,12 +156,6 @@ dependencies = [ "proc-macro2", ] -[[package]] -name = "rustversion" -version = "1.0.22" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" - [[package]] name = "serde" version = "1.0.228" @@ -324,28 +195,13 @@ dependencies = [ "serde_core", ] -[[package]] -name = "shlex" -version = "1.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" - [[package]] name = "squad-quest" -version = "0.5.0" +version = "0.1.0" dependencies = [ - "serde", - "toml", -] - -[[package]] -name = "squad-quest-cli" -version = "0.5.0" -dependencies = [ - "chrono", "clap", + "clap_derive", "serde", - "squad-quest", "toml", ] @@ -417,110 +273,12 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" -[[package]] -name = "wasm-bindgen" -version = "0.2.106" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0d759f433fa64a2d763d1340820e46e111a7a5ab75f993d1852d70b03dbb80fd" -dependencies = [ - "cfg-if", - "once_cell", - "rustversion", - "wasm-bindgen-macro", - "wasm-bindgen-shared", -] - -[[package]] -name = "wasm-bindgen-macro" -version = "0.2.106" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "48cb0d2638f8baedbc542ed444afc0644a29166f1595371af4fecf8ce1e7eeb3" -dependencies = [ - "quote", - "wasm-bindgen-macro-support", -] - -[[package]] -name = "wasm-bindgen-macro-support" -version = "0.2.106" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cefb59d5cd5f92d9dcf80e4683949f15ca4b511f4ac0a6e14d4e1ac60c6ecd40" -dependencies = [ - "bumpalo", - "proc-macro2", - "quote", - "syn", - "wasm-bindgen-shared", -] - -[[package]] -name = "wasm-bindgen-shared" -version = "0.2.106" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cbc538057e648b67f72a982e708d485b2efa771e1ac05fec311f9f63e5800db4" -dependencies = [ - "unicode-ident", -] - -[[package]] -name = "windows-core" -version = "0.62.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb" -dependencies = [ - "windows-implement", - "windows-interface", - "windows-link", - "windows-result", - "windows-strings", -] - -[[package]] -name = "windows-implement" -version = "0.60.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "windows-interface" -version = "0.59.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - [[package]] name = "windows-link" version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" -[[package]] -name = "windows-result" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5" -dependencies = [ - "windows-link", -] - -[[package]] -name = "windows-strings" -version = "0.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091" -dependencies = [ - "windows-link", -] - [[package]] name = "windows-sys" version = "0.61.2" diff --git a/Cargo.toml b/Cargo.toml index ec4ada1..bef570a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,19 +1,10 @@ -[workspace] -members = ["cli"] - -[workspace.package] -version = "0.5.1" -edition = "2024" -repository = "https://2ndbeam.ru/git/2ndbeam/squad-quest" -license = "MIT" - [package] name = "squad-quest" -edition.workspace = true -version.workspace = true -repository.workspace = true -license.workspace = true +version = "0.1.0" +edition = "2024" [dependencies] +clap = { version = "4.5.53", features = ["derive"] } +clap_derive = "4.5.49" serde = { version = "1.0.228", features = ["derive"] } toml = "0.9.8" diff --git a/cfg/config.toml b/cfg/config.toml index 0a9e166..1d81f41 100644 --- a/cfg/config.toml +++ b/cfg/config.toml @@ -1,4 +1,10 @@ -quests_path = "quests" -accounts_path = "accounts" -map = "map.toml" -verbose = true +# Default config + +# Path to quests folder relative to config +quests_path = "./quests" + +# Path to accounts folder relative to config +accounts_path = "./accounts" + +# Path to map .toml file relative to config +map = "./map.toml" diff --git a/cli/Cargo.toml b/cli/Cargo.toml deleted file mode 100644 index 56fed8f..0000000 --- a/cli/Cargo.toml +++ /dev/null @@ -1,13 +0,0 @@ -[package] -name = "squad-quest-cli" -version.workspace = true -edition.workspace = true -repository.workspace = true -license.workspace = true - -[dependencies] -chrono = "0.4.42" -clap = { version = "4.5.53", features = ["derive"] } -serde = { version = "1.0.228", features = ["derive"] } -squad-quest = { version = "0.5.0", path = ".." } -toml = "0.9.8" diff --git a/cli/src/cli/account.rs b/cli/src/cli/account.rs deleted file mode 100644 index bde7393..0000000 --- a/cli/src/cli/account.rs +++ /dev/null @@ -1,66 +0,0 @@ -use clap::{Args,Subcommand,ValueEnum}; - -#[derive(Subcommand)] -pub enum AccountCommands { - /// List accounts - List, - /// Create empty account - Create(AccountCreateArgs), - /// Update balance value - Balance(AccountBalanceArgs), - /// Approve account answer for quest - Complete(AccountCompleteArgs), - /// Delete account - Delete(AccountDeleteArgs), - /// Unlock room for account if it has enough balance - Unlock(AccountUnlockArgs), -} - -#[derive(Args)] -pub struct AccountCreateArgs { - /// Account will be created with this id - pub id: String, -} - -#[derive(Clone, Copy, PartialEq, Eq, ValueEnum)] -pub enum AccountBalanceActions { - Set, - Add, - Remove, -} - -#[derive(Args)] -pub struct AccountBalanceArgs { - /// Account id - pub id: String, - /// What to do with the balance - #[arg(value_enum)] - pub action: AccountBalanceActions, - /// Amount of doing - pub value: u32, - /// If action is remove, set balance to 0 if the result is negative instead of returning error - #[arg(short,long)] - pub negative_ok: bool, -} - -#[derive(Args)] -pub struct AccountCompleteArgs { - /// Id of the account - pub account: String, - /// Id of the quest - pub quest: u16, -} - -#[derive(Args)] -pub struct AccountDeleteArgs { - /// Id of the account to delete - pub id: String, -} - -#[derive(Args)] -pub struct AccountUnlockArgs { - /// Id of the account - pub account: String, - /// Id of the room to unlock - pub room: u16, -} diff --git a/cli/src/cli/map.rs b/cli/src/cli/map.rs deleted file mode 100644 index c98a158..0000000 --- a/cli/src/cli/map.rs +++ /dev/null @@ -1,57 +0,0 @@ -use clap::{Args,Subcommand}; - -#[derive(Subcommand)] -pub enum MapCommands { - /// List all rooms with connections - List, - /// Add new room to map - Add(MapAddArgs), - /// Connect two rooms - Connect(MapConnectArgs), - /// Disconnect two rooms if they're connected - Disconnect(MapConnectArgs), - /// Remove all connections with the room - Delete(MapDeleteArgs), - /// Update room data - Update(MapUpdateArgs), -} - -#[derive(Args)] -pub struct MapAddArgs { - /// Name of the room - pub name: String, - /// Price of the room - pub value: u32, - /// Optional description for the room - #[arg(long,short)] - pub description: Option, -} - -#[derive(Args)] -pub struct MapConnectArgs { - /// First room ID - pub first: u16, - /// Second room ID - pub second: u16, -} - -#[derive(Args)] -pub struct MapDeleteArgs { - /// ID of the room to delete - pub id: u16, -} - -#[derive(Args)] -pub struct MapUpdateArgs { - /// ID of the room to update - pub id: u16, - /// Room name - #[arg(short,long)] - pub name: Option, - /// Room description - #[arg(short,long)] - pub description: Option, - /// Room price - #[arg(short,long)] - pub value: Option, -} diff --git a/cli/src/cli/mod.rs b/cli/src/cli/mod.rs deleted file mode 100644 index db78d60..0000000 --- a/cli/src/cli/mod.rs +++ /dev/null @@ -1,43 +0,0 @@ -use std::path::PathBuf; - -use clap::{Args,Parser,Subcommand}; - -pub mod account; -pub mod map; -pub mod quest; - -#[derive(Parser)] -#[command(version, about, long_about = None)] -#[command(propagate_version = true)] -pub struct Cli { - /// Path to config - #[arg(short, long)] - pub config: PathBuf, - /// Object to make operation on - #[command(subcommand)] - pub command: Objects, - /// Suppress most output - #[arg(short, long)] - pub quiet: bool, -} - -#[derive(Subcommand)] -pub enum Objects { - /// Initialize new SquadQuest in current working directory - Init(InitArgs), - /// Operations on the quests - #[command(subcommand)] - Quest(quest::QuestCommands), - /// Operations on the accounts - #[command(subcommand)] - Account(account::AccountCommands), - /// Operations on the map rooms - #[command(subcommand)] - Map(map::MapCommands), -} - -#[derive(Args)] -pub struct InitArgs { - #[arg(long,short)] - pub path: Option, -} diff --git a/cli/src/cli/quest.rs b/cli/src/cli/quest.rs deleted file mode 100644 index 1ed541c..0000000 --- a/cli/src/cli/quest.rs +++ /dev/null @@ -1,131 +0,0 @@ -use squad_quest::quest::QuestDifficulty as LibQuestDifficulty; -use toml::value::Date; -use serde::Deserialize; -use clap::{Args,Subcommand,ValueEnum}; - -#[derive(Deserialize)] -struct DateWrapper { - date: Date, -} - -fn parse_date(arg: &str) -> Result { - let toml_str = format!("date = {arg}"); - let wrapper: DateWrapper = toml::from_str(&toml_str)?; - Ok(wrapper.date) -} - -#[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, ValueEnum)] -pub enum QuestDifficulty { - /// Easy quest - Easy, - /// Normal quest - Normal, - /// Hard quest - Hard, - /// Special case of hard quests. - Secret, -} - -impl From for LibQuestDifficulty { - fn from(value: QuestDifficulty) -> Self { - match value { - QuestDifficulty::Easy => LibQuestDifficulty::Easy, - QuestDifficulty::Normal => LibQuestDifficulty::Normal, - QuestDifficulty::Hard => LibQuestDifficulty::Hard, - QuestDifficulty::Secret => LibQuestDifficulty::Secret, - } - } -} - -#[derive(Subcommand)] -pub enum QuestCommands { - /// List available quests - List(QuestListArgs), - /// Create new quest and automatically assign it id - Create(QuestCreateArgs), - /// Update existing quest - Update(QuestUpdateArgs), - /// Delete quest - Delete(QuestDeleteArgs), - /// Make certain quests public - Daily, - /// Publish quest with specified id - Publish(QuestPublishArgs), -} - - -#[derive(Args)] -pub struct QuestListArgs { - /// Only list id and name of the quest - #[arg(short, long)] - pub short: bool, -} - -#[derive(Args)] -pub struct QuestCreateArgs { - /// Difficulty of the quest - #[arg(value_enum)] - pub difficulty: QuestDifficulty, - /// Reward for the quest - pub reward: u32, - /// Name of the quest - pub name: String, - /// Visible description of the quest - pub description: String, - /// Answer for the quest for admins - pub answer: String, - /// Create quest and make it public immediately - #[arg(short,long)] - pub public: bool, - /// Make quest available on date (format = YYYY-MM-DD, ex. 2025-12-24) - #[arg(short,long,value_parser = parse_date)] - pub available: Option, - /// Quest expiration date (format = YYYY-MM-DD, ex. 2025-12-24) - #[arg(short,long,value_parser = parse_date)] - pub deadline: Option, -} - -#[derive(Args)] -pub struct QuestUpdateArgs { - /// Id of the quest to update - pub id: u16, - /// Difficulty of the quest - #[arg(value_enum,long)] - pub difficulty: Option, - /// Reward for the quest - #[arg(long)] - pub reward: Option, - /// Name of the quest - #[arg(long)] - pub name: Option, - /// Visible description of the quest - #[arg(long)] - pub description: Option, - /// Answer for the quest for admins - #[arg(long)] - pub answer: Option, - /// Create quest and make it public immediately - #[arg(long)] - pub public: Option, - /// Make quest available on date (format = YYYY-MM-DD, ex. 2025-12-24) - #[arg(long,value_parser = parse_date)] - pub available: Option, - /// Quest expiration date (format = YYYY-MM-DD, ex. 2025-12-24) - #[arg(long,value_parser = parse_date)] - pub deadline: Option, -} - -#[derive(Args)] -pub struct QuestDeleteArgs { - /// Id of the quest to delete - pub id: u16, -} - -#[derive(Args)] -pub struct QuestPublishArgs { - /// Id of the quest to publish - pub id: u16, - /// Make it non-public instead - #[arg(long,short)] - pub reverse: bool, -} diff --git a/cli/src/lib.rs b/cli/src/lib.rs deleted file mode 100644 index 4f77372..0000000 --- a/cli/src/lib.rs +++ /dev/null @@ -1 +0,0 @@ -pub mod cli; diff --git a/cli/src/main.rs b/cli/src/main.rs deleted file mode 100644 index d120a9d..0000000 --- a/cli/src/main.rs +++ /dev/null @@ -1,450 +0,0 @@ -use std::{fs::DirBuilder, path::{Path, PathBuf}}; - -use clap::Parser; -use squad_quest_cli::cli::{Cli,Objects,account::*,map::*,quest::*}; -use squad_quest::{SquadObject, account::Account, config::Config, error::Error, map::{Map, Room}, quest::Quest}; -use toml::value::Date; -use chrono::{Datelike, NaiveDate, Utc}; - -fn print_quest_short(quest: &Quest) { - println!("Quest #{}: {}", quest.id, quest.name); -} - -fn print_quest_long(quest: &Quest) { - print_quest_short(quest); - println!("Difficulty: {:?}", quest.difficulty); - println!("Description:\n{}", quest.description); - println!("Answer:\n{}", quest.answer); -} - -fn do_and_log(result: Result<(),Error>, log: bool, ok_text: String) { - match result { - Ok(_) if log => println!("{ok_text}"), - Err(error) if log => eprintln!("Error: {error}"), - _ => {}, - } -} - -fn load_config_silent(quiet: bool, path: PathBuf) -> Config { - match quiet { - false => Config::load(path.clone()), - true => { - match Config::try_load(path.clone()) { - Ok(mut config) => { - config.verbose = false; - config - }, - Err(_) => { - let path = path.clone().parent().unwrap_or(&Path::new(".")).to_owned(); - Config { - verbose: false, - path, - ..Default::default() - } - } - } - }, - } -} - -fn main() { - let cli = Cli::parse(); - - let config = load_config_silent(cli.quiet, cli.config.clone()); - let map_save = |map: Map, map_path: PathBuf| { map.save(map_path.parent().unwrap_or(Path::new("")).to_owned()) }; - - match &cli.command { - Objects::Init(args) => { - let path = match args.path.clone() { - Some(path) => path, - None => PathBuf::new(), - }; - - match DirBuilder::new().recursive(true).create(path.clone()) { - Ok(_) if !cli.quiet => println!("Created directory {:?}", path), - Err(error) => { - if !cli.quiet { eprintln!("Error: {error}"); } - return; - }, - _ => {}, - } - - let config = Config { - path: path.clone(), - ..Default::default() - }; - - do_and_log(config.save(path.clone()), !cli.quiet, format!("Created file {:?}/config.toml", path)); - let mut config_path = path.clone(); - config_path.push("config.toml"); - let mut config = load_config_silent(true, config_path); - config.verbose = Config::default().verbose; - - let map = Map::default(); - let map_path = config.full_map_path(); - - do_and_log(map_save(map, map_path.clone()), !cli.quiet, format!("Created file {:?}/map.toml", map_path)); - - let quests_path = config.full_quests_path(); - let accounts_path = config.full_accounts_path(); - - for path in [quests_path, accounts_path] { - match DirBuilder::new().recursive(true).create(path.clone()) { - Ok(_) if !cli.quiet => println!("Created directory {:?}", path), - Err(error) => { - if !cli.quiet { eprintln!("Error: {error}"); } - return; - }, - _ => {}, - } - } - }, - Objects::Quest(commands) => { - let mut quests = config.load_quests(); - let mut path = config.full_quests_path(); - - match commands { - QuestCommands::List(args) => { - for quest in quests { - if args.short { - print_quest_short(&quest); - } else { - print_quest_long(&quest); - } - } - }, - QuestCommands::Create(args) => { - quests.sort_by(|a,b| a.id.cmp(&b.id)); - let next_id = match quests.last() { - Some(quest) => quest.id + 1u16, - None => 0u16 - }; - - let mut check_path = path.clone(); - check_path.push(format!("{next_id}.toml")); - match std::fs::exists(&check_path) { - Ok(exists) => { - if exists { - if !cli.quiet { eprintln!("Error: {:?} is not empty.", path); } - return; - } - }, - Err(error) => { - if !cli.quiet { - eprintln!("Error: {error}"); - } - return; - } - } - - let quest = Quest { - id: next_id, - difficulty: args.difficulty.into(), - reward: args.reward, - name: args.name.clone(), - description: args.description.clone(), - answer: args.answer.clone(), - public: args.public, - available_on: args.available.clone(), - deadline: args.deadline.clone() - }; - - do_and_log(quest.save(path), !cli.quiet, format!("Created quest #{}.", quest.id)); - }, - QuestCommands::Update(args) => { - let Some(quest) = quests.iter().find(|q| q.id == args.id) else { - if !cli.quiet { eprintln!("Error: Quest #{} not found.", args.id); } - return; - }; - let quest = Quest { - id: args.id, - difficulty: match args.difficulty { - Some(diff) => diff.into(), - None => quest.difficulty - }, - reward: args.reward.unwrap_or(quest.reward), - name: args.name.clone().unwrap_or(quest.name.clone()), - description: args.description.clone().unwrap_or(quest.description.clone()), - answer: args.answer.clone().unwrap_or(quest.answer.clone()), - public: args.public.unwrap_or(quest.public), - available_on: args.available.clone().or(quest.available_on.clone()), - deadline: args.deadline.clone().or(quest.deadline.clone()) - }; - - do_and_log(quest.save(path), !cli.quiet, format!("Updated quest #{}.", quest.id)); - }, - QuestCommands::Delete(args) => { - path.push(format!("{}.toml", args.id)); - match Quest::delete(path) { - Ok(_) => { - if !cli.quiet { println!("Deleted quest #{}.", args.id); } - - let mut accounts = config.load_accounts(); - let accounts_path = config.full_accounts_path(); - for account in accounts.iter_mut() { - if let Some(index) = account.quests_completed.iter().position(|qid| *qid == args.id) { - account.quests_completed.remove(index); - do_and_log(account.save(accounts_path.clone()), !cli.quiet, format!("Removed quest #{} from account \"{}\" completed quests", args.id, account.id)); - } - } - }, - Err(error) if !cli.quiet => { - eprintln!("Error: {error}"); - }, - _ => {}, - } - }, - QuestCommands::Daily => { - let today: NaiveDate = Utc::now().date_naive(); - let toml_today = Date { - year: today.year() as u16, - month: today.month() as u8, - day: today.day() as u8 - }; - - for quest in quests.iter_mut().filter(|q| !q.public && q.available_on.is_some_and(|date| date.le(&toml_today))) { - quest.public = true; - do_and_log(quest.save(path.clone()), !cli.quiet, format!("Published quest #{}.", quest.id)); - } - }, - QuestCommands::Publish(args) => { - let quest = quests.iter_mut().find(|q| q.id == args.id); - - match quest { - Some(quest) => { - let not_str = if args.reverse {" not "} else {" "}; - - if quest.public != args.reverse { - if !cli.quiet { eprintln!("Error: quest #{} is already{}public.", quest.id, not_str); } - return; - } - - quest.public = !args.reverse; - do_and_log(quest.save(path), !cli.quiet, format!("Published quest #{}.", quest.id)); - }, - None if !cli.quiet => eprintln!("Error: quest #{} not found.", args.id), - _ => {}, - } - }, - } - }, - Objects::Account(commands) => { - let mut accounts = config.load_accounts(); - let mut path = config.full_accounts_path(); - - match commands { - AccountCommands::List => { - for account in accounts { - println!("\"{}\": Balance {}", account.id, account.balance); - } - }, - AccountCommands::Create(args) => { - let account = Account { - id: args.id.clone(), - ..Default::default() - }; - - if let Some(_) = accounts.iter().find(|a| a.id == account.id) { - if !cli.quiet { eprintln!("Error: account \"{}\" exists.", account.id); } - return; - } - - do_and_log(account.save(path), !cli.quiet, format!("Created account \"{}\".", account.id)); - }, - AccountCommands::Balance(args) => { - let Some(account) = accounts.iter_mut().find(|a| a.id == args.id) else { - if !cli.quiet { eprintln!("Error: account \"{}\" not found.", args.id); } - return; - }; - - match args.action { - AccountBalanceActions::Set => { - account.balance = args.value; - }, - AccountBalanceActions::Add => { - account.balance += args.value; - }, - AccountBalanceActions::Remove => { - if args.value > account.balance { - if args.negative_ok { - account.balance = 0u32; - } else { - if !cli.quiet { eprintln!("Error: account \"{}\" balance is less than {}.", account.id, args.value); } - return; - } - } else { - account.balance -= args.value; - } - } - } - - do_and_log(account.save(path), !cli.quiet, format!("Updated balance of account \"{}\".", account.id)); - }, - AccountCommands::Complete(args) => { - let Some(account) = accounts.iter_mut().find(|a| a.id == args.account) else { - if !cli.quiet { eprintln!("Error: account \"{}\" not found.", args.account); } - return; - }; - - let quests = config.load_quests(); - - let quest = match quests.iter().find(|q| q.id == args.quest) { - Some(quest) => quest, - None => { - if !cli.quiet { eprintln!("Error: quest #{} not found.", args.quest); } - return; - }, - }; - - match quest.complete_for_account(account) { - Err(error) if !cli.quiet => println!("Error: {error}"), - Ok(_) => do_and_log(account.save(path), !cli.quiet, format!("Completed quest #{} on account \"{}\".", args.quest, account.id)), - _ => {}, - } - }, - AccountCommands::Delete(args) => { - path.push(format!("{}.toml", args.id)); - do_and_log(Account::delete(path), !cli.quiet, format!("Deleted account \"{}\".", args.id)) - }, - AccountCommands::Unlock(args) => { - let Some(account) = accounts.iter_mut().find(|a| a.id == args.account) else { - if !cli.quiet { eprintln!("Error: account \"{}\" not found.", args.account) }; - return; - }; - - let map = match Map::load(config.full_map_path()) { - Ok(map) => map, - Err(error) => { - if !cli.quiet { eprintln!("Error: {error}"); } - return; - } - }; - - if let Err(error) = map.unlock_room_for_account(args.room, account) { - eprintln!("Error: {error}"); - return; - } - - do_and_log(account.save(path), !cli.quiet, format!("Unlocked room #{} for account \"{}\"", args.room, args.account)); - }, - } - }, - Objects::Map(commands) => { - let map_path = config.full_map_path(); - let mut map = match Map::load(map_path.clone()) { - Ok(map) => map, - Err(error) => { - if !cli.quiet { eprintln!("Error: {error}"); } - return; - } - }; - - match commands { - MapCommands::List => { - for room in map.room { - println!("Room #{}: {}; Connections: {:?}", room.id, room.name, room.children); - } - }, - MapCommands::Add(args) => { - map.room.sort_by(|a,b| a.id.cmp(&b.id)); - let last_id = match map.room.last() { - Some(r) => r.id + 1u16, - None => 0u16 - }; - let room = Room { - id: last_id, - name: args.name.clone(), - value: args.value, - description: args.description.clone(), - ..Default::default() - }; - let r_id = room.id; - map.room.push(room); - do_and_log(map_save(map, map_path), !cli.quiet, format!("Created room #{r_id}.")) - }, - MapCommands::Delete(args) => { - let Some(room) = map.room.iter().find(|r| r.id == args.id) else { - if !cli.quiet { eprintln!("Error: room #{} not found.", args.id); } - return; - }; - - let r_id = room.id; - let index = map.room.iter().position(|r| r.eq(room)).unwrap(); - map.room.remove(index); - - for room in map.room.iter_mut().filter(|r| r.children.contains(&r_id)) { - let idx = room.children.iter() - .position(|id| *id == r_id) - .unwrap(); - room.children.remove(idx); - } - - match map_save(map, map_path) { - Ok(_) => { - if !cli.quiet { println!("Deleted room #{r_id}."); } - - let mut accounts = config.load_accounts(); - let accounts_path = config.full_accounts_path(); - - for account in accounts.iter_mut() { - if let Some(index) = account.rooms_unlocked.iter().position(|rid| *rid == r_id) { - account.rooms_unlocked.remove(index); - do_and_log(account.save(accounts_path.clone()), !cli.quiet, format!("Removed room #{r_id} from account \"{}\" unlocked rooms.", account.id)); - } - } - }, - Err(error) if !cli.quiet => { - eprintln!("Error: {error}"); - }, - _ => {}, - } - }, - MapCommands::Update(args) => { - let Some(room) = map.room.iter_mut().find(|r| r.id == args.id) else { - if !cli.quiet { eprintln!("Error: room #{} not found", args.id); } - return; - }; - - if let Some(name) = &args.name { - room.name = name.to_string(); - } - - if args.description.is_some() { - room.description = args.description.clone(); - } - - if let Some(value) = args.value { - room.value = value; - } - - do_and_log(map_save(map, map_path), !cli.quiet, format!("Updated room #{}.", args.id)) - }, - MapCommands::Connect(args) | MapCommands::Disconnect(args) => { - let connect = match commands { - MapCommands::Connect(_) => true, - _ => false, - }; - - // We iterate twice to make references first->second and second->first - for (first, second) in [(args.first, args.second),(args.second, args.first)] { - let Some(room) = map.room.iter_mut().find(|r| r.id == first) else { - if !cli.quiet { eprintln!("Error: room #{} not found.", first); } - return; - }; - - match room.children.iter().position(|id| *id == second) { - Some(_) if connect && !cli.quiet => println!("Room #{} already has reference to #{}.", first, second), - None if connect => room.children.push(second), - Some(id) if !connect => {room.children.remove(id as usize);}, - None if !connect && !cli.quiet => println!("Room #{} has no reference to #{}.", first, second), - _ => {}, - } - } - - let connected = if connect { "Connected" } else { "Disconnected" }; - do_and_log(map_save(map, map_path), !cli.quiet, format!("{connected} rooms #{} <-> #{}.", args.first, args.second)); - }, - } - } - } -} diff --git a/src/account/mod.rs b/src/account/mod.rs index 5ed0373..0960db8 100644 --- a/src/account/mod.rs +++ b/src/account/mod.rs @@ -1,97 +1,24 @@ //! User accounts -use std::{fs, io::Write, path::PathBuf}; - use serde::{ Serialize, Deserialize }; -use crate::{SquadObject, error::Error}; - fn default_id() -> String { "none".to_string() } /// User account struct, which can be (de-)serialized from/into TOML -#[derive(Serialize, Deserialize, PartialEq, Debug)] -#[serde(default)] +#[derive(Serialize, Deserialize)] pub struct Account { /// User identifier, specific to used service + #[serde(default = "default_id")] pub id: String, /// User balance + #[serde(default)] pub balance: u32, /// Id of room node where user is located - pub location: u16, - - /// Vec of quests completed by this user - pub quests_completed: Vec, - - /// Vec of rooms unlocked by this user - pub rooms_unlocked: Vec, -} - -impl Default for Account { - fn default() -> Self { - Account { - id: default_id(), - balance: u32::default(), - location: u16::default(), - quests_completed: Vec::new(), - rooms_unlocked: Vec::new(), - } - } -} - -impl SquadObject for Account { - fn load(path: PathBuf) -> Result { - match std::fs::read_to_string(path) { - Ok(string) => { - match toml::from_str::(&string) { - Ok(object) => Ok(object), - Err(error) => Err(Error::TomlDeserializeError(error)) - } - }, - Err(error) => Err(Error::IoError(error)) - } - } - - fn delete(path: PathBuf) -> Result<(), Error> { - match Self::load(path.clone()) { - Ok(_) => { - if let Err(error) = fs::remove_file(path) { - return Err(Error::IoError(error)); - } - - Ok(()) - }, - Err(error) => Err(error) - } - } - - fn save(&self, path: PathBuf) -> Result<(), Error> { - let filename = format!("{}.toml", self.id); - let mut full_path = path; - full_path.push(filename); - - let str = match toml::to_string_pretty(&self) { - Ok(string) => string, - Err(error) => { - return Err(Error::TomlSerializeError(error)); - } - }; - - let mut file = match fs::File::create(full_path) { - Ok(f) => f, - Err(error) => { - return Err(Error::IoError(error)); - } - }; - - if let Err(error) = file.write_all(str.as_bytes()) { - return Err(Error::IoError(error)); - } - - Ok(()) - } + #[serde(default)] + pub location: u16 } diff --git a/src/bin/cli.rs b/src/bin/cli.rs new file mode 100644 index 0000000..fef2fc1 --- /dev/null +++ b/src/bin/cli.rs @@ -0,0 +1,210 @@ +use std::path::PathBuf; + +use clap::{Parser,Subcommand,Args,ValueEnum}; +use squad_quest::{config::Config,quest::{Quest,QuestDifficulty as LibQuestDifficulty}}; + +#[derive(Parser)] +#[command(version, about, long_about = None)] +#[command(propagate_version = true)] +struct Cli { + /// Path to config + #[arg(short, long)] + config: PathBuf, + /// Object to make operation on + #[command(subcommand)] + command: Objects, +} + +#[derive(Subcommand)] +enum Objects { + /// Operations on the quests + #[command(subcommand)] + Quest(QuestCommands) +} + +#[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, ValueEnum)] +enum QuestDifficulty { + /// Easy quest + Easy, + /// Normal quest + Normal, + /// Hard quest + Hard, + /// Special case of hard quests. + Secret +} + +impl From for LibQuestDifficulty { + fn from(value: QuestDifficulty) -> Self { + match value { + QuestDifficulty::Easy => LibQuestDifficulty::Easy, + QuestDifficulty::Normal => LibQuestDifficulty::Normal, + QuestDifficulty::Hard => LibQuestDifficulty::Hard, + QuestDifficulty::Secret => LibQuestDifficulty::Secret, + } + } +} + +#[derive(Subcommand)] +enum QuestCommands { + /// List available quests + List(QuestListArgs), + /// Create new quest and automatically assign it id + Create(QuestCreateArgs), + /// Update existing quest + Update(QuestUpdateArgs), + /// Delete quest + Delete(QuestDeleteArgs), +} + +#[derive(Args)] +struct QuestListArgs { + /// Only list id and name of the quest + #[arg(short, long)] + short: bool +} + +#[derive(Args)] +struct QuestCreateArgs { + /// Difficulty of the quest + #[arg(value_enum)] + difficulty: QuestDifficulty, + /// Reward for the quest + reward: u32, + /// Name of the quest + name: String, + /// Visible description of the quest + description: String, + /// Answer for the quest for admins + answer: String +} + +#[derive(Args)] +struct QuestUpdateArgs { + /// Id of the quest to update + id: u16, + /// Difficulty of the quest + #[arg(value_enum,long)] + difficulty: Option, + /// Reward for the quest + #[arg(long)] + reward: Option, + /// Name of the quest + #[arg(long)] + name: Option, + /// Visible description of the quest + #[arg(long)] + description: Option, + /// Answer for the quest for admins + #[arg(long)] + answer: Option +} + +#[derive(Args)] +struct QuestDeleteArgs { + /// Id of the quest to delete + id: u16 +} + +fn print_quest_short(quest: &Quest) { + println!("Quest #{}: {}", quest.id, quest.name); +} + +fn print_quest_long(quest: &Quest) { + print_quest_short(quest); + println!("Difficulty: {:?}", quest.difficulty); + println!("Description:\n{}", quest.description); + println!("Answer:\n{}", quest.answer); +} + +fn main() { + let cli = Cli::parse(); + + let config = Config::load(cli.config.clone()); + + match &cli.command { + Objects::Quest(commands) => { + match commands { + QuestCommands::List(args) => { + let quests = config.load_quests(); + for quest in quests { + if args.short { + print_quest_short(&quest); + } else { + print_quest_long(&quest); + } + } + }, + QuestCommands::Create(args) => { + let mut quests = config.load_quests(); + quests.sort_by(|a,b| a.id.cmp(&b.id)); + let next_id = match quests.last() { + Some(quest) if quest.id == u16::MAX => { + panic!("Error: quest list contains quest with u16::MAX id."); + } + Some(quest) => quest.id + 1u16, + None => 0u16 + }; + + let path = config.full_quests_path(); + let mut quest_path = path.clone(); + quest_path.push(format!("{next_id}.toml")); + match std::fs::exists(&quest_path) { + Ok(exists) => { + if exists { + panic!("Error: {:?} is not empty.", quest_path); + } + }, + Err(error) => { + panic!("Error while retrieving {:?}: {}.", quest_path, error); + } + } + + let quest = Quest { + id: next_id, + difficulty: args.difficulty.into(), + reward: args.reward, + name: args.name.clone(), + description: args.description.clone(), + answer: args.answer.clone(), + }; + if let Err(error) = quest.save(path) { + eprintln!("Error while saving quest: {error}."); + } else { + println!("Successfully saved quest #{}.", quest.id); + } + }, + QuestCommands::Update(args) => { + let quests = config.load_quests(); + let Some(quest) = quests.iter().find(|q| q.id == args.id) else { + panic!("Error: Quest #{} not found.", args.id); + }; + let quest = Quest { + id: args.id, + difficulty: match args.difficulty { + Some(diff) => diff.into(), + None => quest.difficulty + }, + reward: args.reward.unwrap_or(quest.reward), + name: args.name.clone().unwrap_or(quest.name.clone()), + description: args.description.clone().unwrap_or(quest.description.clone()), + answer: args.answer.clone().unwrap_or(quest.answer.clone()) + }; + let path = config.full_quests_path(); + match quest.save(path) { + Ok(_) => println!("Updated quest #{}", quest.id), + Err(error) => eprintln!("Error while updating quest: {error}") + } + }, + QuestCommands::Delete(args) => { + let mut path = config.full_quests_path(); + path.push(format!("{}.toml", args.id)); + match Quest::delete(path) { + Ok(_) => println!("Successfully deleted quest #{}", args.id), + Err(error) => eprintln!("Error deleting quest #{}: {}", args.id, error), + } + }, + } + } + } +} diff --git a/src/config/mod.rs b/src/config/mod.rs index f0b2f94..407f8dc 100644 --- a/src/config/mod.rs +++ b/src/config/mod.rs @@ -1,17 +1,17 @@ //! Configuration file that handles (de-)serializing other components -use std::{fs::{self, DirEntry}, io::Write, path::{Path, PathBuf}}; -use serde::{Deserialize, Serialize}; +use std::{fs::{self, DirEntry},path::{Path, PathBuf}}; +use serde::Deserialize; -use crate::{SquadObject, account::Account, error::Error, quest::Quest}; +use crate::quest::{Quest,error::QuestError}; /// Struct for containing paths to other (de-)serializable things -#[derive(Serialize, Deserialize)] +#[derive(Deserialize)] #[serde(default)] pub struct Config { /// Path to config directory #[serde(skip)] - pub path: PathBuf, + path: PathBuf, /// Path to serialized [quests][`crate::quest::Quest`] folder pub quests_path: PathBuf, @@ -20,10 +20,7 @@ pub struct Config { pub accounts_path: PathBuf, /// Path to serialized [map][`crate::map::Map`] file - pub map: PathBuf, - - /// If true, print to std{out/err} - pub verbose: bool, + pub map: PathBuf } impl Default for Config { @@ -32,51 +29,30 @@ impl Default for Config { path: ".".into(), quests_path: "quests".into(), accounts_path: "accounts".into(), - map: "map.toml".into(), - verbose: true, + map: "map.toml".into() } } } -fn handle_quest_entry(quest_entry: DirEntry) -> Result{ +fn handle_quest_entry(quest_entry: DirEntry) -> Result{ let filetype = quest_entry.file_type(); if let Err(error) = filetype { - return Err(Error::IoError(error)); + return Err(QuestError::IoError(error)); } let path = quest_entry.path(); let filetype = filetype.unwrap(); if !filetype.is_file() { - return Err(Error::IsNotAFile(path)); + return Err(QuestError::IsNotAFile(path)); } Quest::load(path) } -fn handle_account_entry(account_entry: DirEntry) -> Result{ - let filetype = account_entry.file_type(); - if let Err(error) = filetype { - return Err(Error::IoError(error)); - } - - let path = account_entry.path(); - - let filetype = filetype.unwrap(); - if !filetype.is_file() { - return Err(Error::IsNotAFile(path)); - } - - Account::load(path) -} - impl Config { - /// Deserialize config from TOML. - /// - /// This function wraps [try_load][Config::try_load]. - /// - /// Logs all errors if `config.verbose == true`. - /// Returns default config on error. + /// Deserialize config from TOML + /// Logs all errors and returns default config if that happens /// /// # Examples /// ```rust @@ -89,98 +65,25 @@ impl Config { let dir = path.parent() .unwrap_or(Path::new(".")) .to_owned(); - - match Self::try_load(path) { - Ok(conf) => { - if conf.verbose { - println!("Successfully loaded config"); - } - conf - }, - Err(error) => { - let conf = Config { - path: dir, - ..Default::default() - }; - - if conf.verbose { - println!("Error while loading config: {error}"); - } - - conf - } - } - } - - /// Serialize config into TOML. - /// Config will be saved as `path/config.toml` - /// - /// # Examples - /// ```rust - /// use squad_quest::config::Config; - /// - /// let path = "cfg".into(); - /// - /// let config = Config::default(); - /// - /// if let Err(error) = config.save(path) { - /// // handle error - /// } - /// ``` - pub fn save(&self, path: PathBuf) -> Result<(), Error> { - let mut path = path; - path.push("config.toml"); - - let str = match toml::to_string_pretty(&self) { - Ok(string) => string, - Err(error) => return Err(Error::TomlSerializeError(error)), - }; - - let mut file = match fs::File::create(path) { - Ok(f) => f, - Err(error) => return Err(Error::IoError(error)), - }; - - if let Err(error) = file.write_all(str.as_bytes()) { - return Err(Error::IoError(error)); - } - - Ok(()) - } - - /// Deserialize config from TOML - /// - /// # Examples - /// ```rust - /// use squad_quest::{config::Config,error::Error}; - /// # fn main() { - /// # let _ = wrapper(); - /// # } - /// # fn wrapper() -> Result<(), Error> { - /// let path = "cfg/config.toml".into(); - /// let config = Config::try_load(path)?; - /// # Ok(()) - /// # } - /// ``` - pub fn try_load(path: PathBuf) -> Result { - let dir = path.parent() - .unwrap_or(Path::new(".")) - .to_owned(); - match fs::read_to_string(path) { Ok(string) => { match toml::from_str::(&string) { Ok(mut conf) => { + println!("Successfully loaded config"); conf.path = dir; - Ok(conf) + conf }, Err(error) => { - Err(Error::TomlDeserializeError(error)) + eprintln!("Error on parsing config: {error}"); + let mut cfg = Config::default(); + cfg.path = dir; + cfg } } }, Err(error) => { - Err(Error::IoError(error)) + eprintln!("Error on reading config path: {error}"); + Config::default() } } } @@ -231,126 +134,24 @@ impl Config { Ok(quest_entry) => { match handle_quest_entry(quest_entry) { Ok(quest) => out_vec.push(quest), - Err(error) if self.verbose => { + Err(error) => { eprintln!("Error on loading single quest: {error}"); - }, - _ => {}, + } } }, - Err(error) if self.verbose => { + Err(error) => { eprintln!("Error on loading single quest: {error}"); - }, - _ => {}, + } } } }, - Err(error) if self.verbose => { + Err(error) => { eprintln!("Error on loading quests: {error}"); - }, - _ => {}, - } - - if self.verbose { - println!("Loaded {} quests successfully", out_vec.len()); + } } + println!("Loaded {} quests successfully", out_vec.len()); + out_vec } - - /// Returns full path to accounts folder - /// This path will be relative to $PWD, not to config. - /// - /// # Examples - /// ```rust - /// use squad_quest::config::Config; - /// - /// let path = "cfg/config.toml".into(); - /// let config = Config::load(path); - /// - /// let accounts_path = config.full_accounts_path(); - /// ``` - pub fn full_accounts_path(&self) -> PathBuf { - let mut path = self.path.clone(); - path.push(self.accounts_path.clone()); - path - } - - /// Load [Vec]<[Account]> from accounts folder. - /// Also logs errors and counts successfully loaded quests. - /// - /// # Examples - /// ```rust - /// use squad_quest::{config::Config, account::Account}; - /// - /// - /// let path = "cfg/config.toml".into(); - /// let config = Config::load(path); - /// let accounts = config.load_accounts(); - /// - /// for account in accounts { - /// println!("Account {}", account.id); - /// } - /// ``` - pub fn load_accounts(&self) -> Vec { - let mut out_vec = Vec::new(); - - let path = self.full_accounts_path(); - - match fs::read_dir(path) { - Ok(iter) => { - for entry in iter { - match entry { - Ok(acc_entry) => { - match handle_account_entry(acc_entry) { - Ok(quest) => out_vec.push(quest), - Err(error) if self.verbose => { - eprintln!("Error on loading single account: {error}"); - }, - _ => {}, - } - }, - Err(error) if self.verbose => { - eprintln!("Error on loading single account: {error}"); - }, - _ => {}, - } - } - }, - Err(error) if self.verbose => { - eprintln!("Error on loading accounts: {error}"); - }, - _ => {}, - } - - if self.verbose { - println!("Loaded {} accounts successfully", out_vec.len()); - } - - out_vec - } - - /// Returns full path to map.toml - /// This path will be relative to $PWD, not to config. - /// - /// # Examples - /// ```rust - /// use squad_quest::{config::Config,error::Error,map::Map,SquadObject}; - /// # fn main() { - /// # let _ = wrapper(); - /// # } - /// # fn wrapper() -> Result<(),Error> { - /// - /// let path = "cfg/config.toml".into(); - /// let config = Config::load(path); - /// - /// let map_path = config.full_map_path(); - /// let map = Map::load(map_path)?; - /// # Ok(()) - /// # } - /// ``` - pub fn full_map_path(&self) -> PathBuf { - let mut path = self.path.clone(); - path.push(self.map.clone()); - path - } } diff --git a/src/error.rs b/src/error.rs deleted file mode 100644 index 0804b58..0000000 --- a/src/error.rs +++ /dev/null @@ -1,66 +0,0 @@ -//! Module for handling crate errors - -use std::{fmt, path::PathBuf}; - -/// Error struct -#[derive(Debug)] -#[non_exhaustive] -pub enum Error { - /// Given path is not a file - IsNotAFile(PathBuf), - /// std::io::Error happenned when loading - IoError(std::io::Error), - /// toml::ser::Error happened when loading - TomlSerializeError(toml::ser::Error), - /// toml::de::Error happened when loading - TomlDeserializeError(toml::de::Error), -} - -impl fmt::Display for Error { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - match self { - Error::IsNotAFile(path) => write!(f, "{:?} is not a file", path), - Error::IoError(error) => write!(f, "io error: {error}"), - Error::TomlSerializeError(error) => write!(f, "serialize error: {error}"), - Error::TomlDeserializeError(error) => write!(f, "parse error: {error}") - } - } -} - -/// Error related to quest logic -#[derive(Debug)] -#[non_exhaustive] -pub enum QuestError { - /// Quest (self.0) is already completed for given account (self.1) - AlreadyCompleted(u16, String), -} - -impl fmt::Display for QuestError { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - match self { - Self::AlreadyCompleted(quest_id, account_id) => write!(f, "quest #{quest_id} is already completed for account \"{account_id}\""), - } - } -} - -/// Error related to map logic -#[derive(Debug)] -#[non_exhaustive] -pub enum MapError { - /// Room not found in map file - RoomNotFound(u16), - /// Room (self.0) is already unlocked on account (self.1) - RoomAlreadyUnlocked(u16, String), - /// Account (self.1) does not have much money (self.0) - InsufficientFunds(u16, String), -} - -impl fmt::Display for MapError { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - match self { - Self::RoomNotFound(id) => write!(f, "could not find room #{id}"), - Self::RoomAlreadyUnlocked(room_id, account_id) => write!(f, "room #{room_id} is already unlocked on account \"{account_id}\""), - Self::InsufficientFunds(room_id, account_id) => write!(f, "account \"{account_id}\" does not have enough money to unlock room #{room_id}"), - } - } -} diff --git a/src/lib.rs b/src/lib.rs index db152a5..594f090 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -2,75 +2,7 @@ #![warn(missing_docs)] -use std::path::PathBuf; - -use crate::error::Error; - pub mod account; pub mod config; -pub mod error; pub mod map; pub mod quest; - -/// Trait for objects that are internally used in squad_quest. -/// Contains functions and methods to interact with file system. -/// Files are saved in TOML format. -pub trait SquadObject { - /// Parse SquadObject TOML or return error - /// - /// # Examples - /// ```rust - /// use squad_quest::{SquadObject,error::Error,quest::Quest}; - /// # fn main() { - /// # let _ = wrapper(); - /// # } - /// - /// # fn wrapper() -> Result<(), Error> { - /// let path = "quests/0.toml".into(); - /// - /// let quest = Quest::load(path)?; - /// # - /// # Ok(()) - /// # } - /// ``` - fn load(path: PathBuf) -> Result where Self: Sized; - - /// Check if given file is a SquadObject, then delete it or raise an error. - /// If file is not a quest, raises [Error::TomlDeserializeError] - /// - /// # Examples - /// ```rust - /// use squad_quest::{SquadObject,error::Error,quest::Quest}; - /// - /// let path = "quests/0.toml".into(); - /// - /// if let Err(error) = Quest::delete(path) { - /// // handle the error - /// } - /// ``` - fn delete(path: PathBuf) -> Result<(), Error>; - - /// Save SquadObject to given folder in TOML format. - /// File will be saved as `{id}.toml`. - /// If file exists, this method will override it. - /// - /// # Examples - /// ```rust - /// # fn main() { - /// use squad_quest::{SquadObject,error::Error,quest::Quest}; - /// use std::path::PathBuf; - /// - /// let quest = Quest::default(); - /// - /// let path: PathBuf = "quests".into(); - /// # let path2 = path.clone(); - /// - /// if let Err(error) = quest.save(path) { - /// // handle the error - /// } - /// # let filename = format!("{}.toml", quest.id); - /// # let _ = Quest::delete(path2.with_file_name(filename)); - /// # } - /// ``` - fn save(&self, path: PathBuf) -> Result<(), Error>; -} diff --git a/src/map/mod.rs b/src/map/mod.rs index 69ccb68..d4eb205 100644 --- a/src/map/mod.rs +++ b/src/map/mod.rs @@ -1,143 +1,5 @@ //! Map, a.k.a. a graph of rooms +#![allow(dead_code)] -use std::{fs, io::Write, path::PathBuf}; - -use serde::{Deserialize, Serialize}; - -use crate::{SquadObject, account::Account, error::{Error, MapError}}; - -/// THE Graph. Actually, this is a Vec. -#[derive(Serialize, Deserialize)] -#[serde(default)] -pub struct Map { - /// Rooms go here - pub room: Vec -} - -impl Default for Map { - fn default() -> Self { - Map { room: Vec::new() } - } -} - -impl SquadObject for Map { - fn load(path: PathBuf) -> Result { - match std::fs::read_to_string(path) { - Ok(string) => { - match toml::from_str::(&string) { - Ok(object) => Ok(object), - Err(error) => Err(Error::TomlDeserializeError(error)) - } - }, - Err(error) => Err(Error::IoError(error)) - } - } - - fn delete(path: PathBuf) -> Result<(), Error> { - match Self::load(path.clone()) { - Ok(_) => { - if let Err(error) = fs::remove_file(path) { - return Err(Error::IoError(error)); - } - - Ok(()) - }, - Err(error) => Err(error) - } - } - - fn save(&self, path: PathBuf) -> Result<(), Error> { - let filename = "map.toml".to_string(); - let mut full_path = path; - full_path.push(filename); - - let str = match toml::to_string_pretty(&self) { - Ok(string) => string, - Err(error) => { - return Err(Error::TomlSerializeError(error)); - } - }; - - let mut file = match fs::File::create(full_path) { - Ok(f) => f, - Err(error) => { - return Err(Error::IoError(error)); - } - }; - - if let Err(error) = file.write_all(str.as_bytes()) { - return Err(Error::IoError(error)); - } - - Ok(()) - } -} - -impl Map { - /// Try to unlock room for account, or return [MapError] - /// - /// # Examples - /// ```rust - /// use squad_quest::{account::Account,map::{Map,Room},error::MapError}; - /// - /// let map = Map { - /// room: vec![Room { id: 0, value: 100, ..Default::default() }], - /// }; - /// - /// let mut account = Account { balance: 100, ..Default::default() }; - /// - /// if let Err(error) = map.unlock_room_for_account(0, &mut account) { - /// // handle error - /// } - /// ``` - pub fn unlock_room_for_account(&self, room_id: u16, account: &mut Account) -> Result<(), MapError> { - let Some(room) = self.room.iter().find(|r| r.id == room_id) else { - return Err(MapError::RoomNotFound(room_id)); - }; - - if let Some(_) = account.rooms_unlocked.iter().find(|rid| **rid == room_id) { - return Err(MapError::RoomAlreadyUnlocked(room_id, account.id.clone())); - } - - if account.balance < room.value { - return Err(MapError::InsufficientFunds(room_id, account.id.clone())); - } - - account.balance -= room.value; - account.rooms_unlocked.push(room_id); - - Ok(()) - } -} - -/// Component of the map -#[derive(Serialize, Deserialize, PartialEq, Debug)] -#[serde(default)] -pub struct Room { - /// Room id - pub id: u16, - /// Rooms that are connected with this - pub children: Vec, - /// Price of the room - pub value: u32, - /// Room name - pub name: String, - /// Room description - pub description: Option, -} - -fn default_name() -> String { - "Hall".to_string() -} - -impl Default for Room { - fn default() -> Self { - Room { - id: u16::default(), - children: Vec::new(), - value: u32::default(), - name: default_name(), - description: None, - } - } -} +/// Graph for room nodes +pub struct Map; diff --git a/src/quest/error.rs b/src/quest/error.rs new file mode 100644 index 0000000..55ddbcf --- /dev/null +++ b/src/quest/error.rs @@ -0,0 +1,28 @@ +//! Module for handling quest loading errors + +use std::{fmt, path::PathBuf}; + +/// Error raised when trying to parse quest file +#[derive(Debug)] +#[non_exhaustive] +pub enum QuestError { + /// Given path is not a file + IsNotAFile(PathBuf), + /// std::io::Error happenned when loading + IoError(std::io::Error), + /// toml::ser::Error happened when loading + TomlSerializeError(toml::ser::Error), + /// toml::de::Error happened when loading + TomlDeserializeError(toml::de::Error), +} + +impl fmt::Display for QuestError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + QuestError::IsNotAFile(path) => write!(f, "{:?} is not a file", path), + QuestError::IoError(error) => write!(f, "io error: {error}"), + QuestError::TomlSerializeError(error) => write!(f, "serialize error: {error}"), + QuestError::TomlDeserializeError(error) => write!(f, "parse error: {error}") + } + } +} diff --git a/src/quest/mod.rs b/src/quest/mod.rs index 669d061..6c95f69 100644 --- a/src/quest/mod.rs +++ b/src/quest/mod.rs @@ -1,10 +1,11 @@ //! Text-based quests and user solutions for them +pub mod error; + use std::{fs, io::Write, path::PathBuf}; use serde::{ Serialize, Deserialize }; -use crate::{SquadObject, account::Account, error::{Error, QuestError}}; -use toml::value::Date; +use error::QuestError; /// Difficulty of the quest #[derive(Serialize, Deserialize, Debug, PartialEq, Clone, Copy)] @@ -58,15 +59,6 @@ pub struct Quest { /// Quest answer, available for admins pub answer: String, - - /// Is quest available for regular users - pub public: bool, - - /// When quest becomes public - pub available_on: Option, - - /// When quest expires - pub deadline: Option } impl Default for Quest { @@ -77,32 +69,59 @@ impl Default for Quest { reward: u32::default(), name: default_name(), description: default_description(), - answer: default_answer(), - public: false, - available_on: None, - deadline: None + answer: default_answer() } } } -impl SquadObject for Quest { - fn load(path: PathBuf) -> Result { +impl Quest { + /// Parse quest TOML or return error + /// + /// # Examples + /// ```rust + /// use squad_quest::quest::{Quest,error::QuestError}; + /// # fn main() { + /// # let _ = wrapper(); + /// # } + /// + /// # fn wrapper() -> Result<(), QuestError> { + /// let path = "quests/0.toml".into(); + /// + /// let quest = Quest::load(path)?; + /// # + /// # Ok(()) + /// # } + /// ``` + pub fn load(path: PathBuf) -> Result { match std::fs::read_to_string(path) { Ok(string) => { - match toml::from_str::(&string) { - Ok(object) => Ok(object), - Err(error) => Err(Error::TomlDeserializeError(error)) + match toml::from_str::(&string) { + Ok(quest) => Ok(quest), + Err(error) => Err(QuestError::TomlDeserializeError(error)) } }, - Err(error) => Err(Error::IoError(error)) + Err(error) => Err(QuestError::IoError(error)) } } - fn delete(path: PathBuf) -> Result<(), Error> { + /// Check if given file is a quest, then delete it or raise an error. + /// If file is not a quest, raises [QuestError::TomlDeserializeError] + /// + /// # Examples + /// ```rust + /// use squad_quest::quest::{Quest,error::QuestError}; + /// + /// let path = "quests/0.toml".into(); + /// + /// if let Err(error) = Quest::delete(path) { + /// // handle the error + /// } + /// ``` + pub fn delete(path: PathBuf) -> Result<(), QuestError> { match Quest::load(path.clone()) { Ok(_) => { if let Err(error) = fs::remove_file(path) { - return Err(Error::IoError(error)); + return Err(QuestError::IoError(error)); } Ok(()) @@ -111,7 +130,29 @@ impl SquadObject for Quest { } } - fn save(&self, path: PathBuf) -> Result<(), Error> { + /// Save quest to given folder in TOML format. + /// File will be saved as `{id}.toml`. + /// If file exists, this method will override it. + /// + /// # Examples + /// ```rust + /// # fn main() { + /// use squad_quest::quest::{Quest,error::QuestError}; + /// use std::path::PathBuf; + /// + /// let quest = Quest::default(); + /// + /// let path: PathBuf = "quests".into(); + /// # let path2 = path.clone(); + /// + /// if let Err(error) = quest.save(path) { + /// // handle the error + /// } + /// # let filename = format!("{}.toml", quest.id); + /// # let _ = Quest::delete(path2.with_file_name(filename)); + /// # } + /// ``` + pub fn save(&self, path: PathBuf) -> Result<(), QuestError> { let filename = format!("{}.toml", self.id); let mut full_path = path; full_path.push(filename); @@ -119,50 +160,21 @@ impl SquadObject for Quest { let str = match toml::to_string_pretty(&self) { Ok(string) => string, Err(error) => { - return Err(Error::TomlSerializeError(error)); + return Err(QuestError::TomlSerializeError(error)); } }; let mut file = match fs::File::create(full_path) { Ok(f) => f, Err(error) => { - return Err(Error::IoError(error)); + return Err(QuestError::IoError(error)); } }; if let Err(error) = file.write_all(str.as_bytes()) { - return Err(Error::IoError(error)); + return Err(QuestError::IoError(error)); } Ok(()) } } - -impl Quest { - /// Complete quest for account and add reward to it's balance. - /// Does nothing and returns [QuestError::AlreadyCompleted] - /// if it is already completed. - /// - /// # Examples - /// - /// ```rust - /// use squad_quest::{account::Account,quest::Quest}; - /// - /// let quest = Quest::default(); - /// let mut account = Account::default(); - /// - /// if let Err(error) = quest.complete_for_account(&mut account) { - /// // handle error - /// } - /// ``` - pub fn complete_for_account(&self, account: &mut Account) -> Result<(),QuestError> { - match account.quests_completed.iter().find(|qid| **qid == self.id) { - Some(_) => Err(QuestError::AlreadyCompleted(self.id, account.id.clone())), - None => { - account.quests_completed.push(self.id); - account.balance += self.reward; - Ok(()) - }, - } - } -} diff --git a/tests/io.rs b/tests/io.rs index 356f972..fc3def8 100644 --- a/tests/io.rs +++ b/tests/io.rs @@ -1,4 +1,5 @@ -use squad_quest::{SquadObject, account::Account, config::Config, quest::Quest}; +use squad_quest::{config::Config,quest::{error::{QuestError}, Quest}}; +use std::path::PathBuf; const CONFIG_PATH: &str = "tests/io/config.toml"; @@ -6,35 +7,23 @@ const CONFIG_PATH: &str = "tests/io/config.toml"; // and Quest::save can override files, // so this test covers full quest CRUD #[test] -fn quest_crud() { +fn quest_crud() -> Result<(), QuestError> { let config = Config::load(CONFIG_PATH.into()); - let mut quests_path = config.full_quests_path(); + let mut quests_path = PathBuf::from(CONFIG_PATH).parent().unwrap().to_owned(); + quests_path.push(config.quests_path); let quest = Quest::default(); - quest.save(quests_path.clone()).unwrap(); + println!("{:?}", quests_path.clone()); + + quest.save(quests_path.clone())?; let filename = format!("{}.toml", quest.id); quests_path.push(filename); - Quest::delete(quests_path).unwrap(); -} - -#[test] -fn account_crud() { - let config = Config::load(CONFIG_PATH.into()); - - let mut accounts_path = config.full_accounts_path(); - - let account = Account::default(); - - account.save(accounts_path.clone()).unwrap(); - - let filename = format!("{}.toml", account.id); - - accounts_path.push(filename); - - Account::delete(accounts_path).unwrap(); + Quest::delete(quests_path)?; + + Ok(()) } diff --git a/tests/io/accounts/.placeholder b/tests/io/accounts/.placeholder deleted file mode 100644 index 16680ce..0000000 --- a/tests/io/accounts/.placeholder +++ /dev/null @@ -1 +0,0 @@ -Placeholder file for git diff --git a/tests/io/map.toml b/tests/io/map.toml deleted file mode 100644 index 38dffa5..0000000 --- a/tests/io/map.toml +++ /dev/null @@ -1 +0,0 @@ -room = [] diff --git a/tests/main.rs b/tests/main.rs index b2d054d..321674a 100644 --- a/tests/main.rs +++ b/tests/main.rs @@ -1,4 +1,4 @@ -use squad_quest::{SquadObject, account::Account, config::Config, map::{Map, Room}, quest::Quest}; +use squad_quest::{config::Config, quest::Quest}; static CONFIG_PATH: &str = "./tests/main/config.toml"; @@ -35,96 +35,8 @@ fn quest_one() { reward: 100, name: "Example easy quest".to_owned(), description: "Answer this quest without any attachments or comments".to_owned(), - answer: "Accept the answer if it has no attachments and an empty comment".to_owned(), - public: false, - available_on: None, - deadline: None + answer: "Accept the answer if it has no attachments and an empty comment".to_owned() }; assert_eq!(*quest, expected); } - -#[test] -fn load_accounts() { - let config = Config::load(CONFIG_PATH.into()); - let accounts = config.load_accounts(); - - assert_eq!(accounts.len(), 2); -} - -#[test] -fn empty_account_is_default() { - let config = Config::load(CONFIG_PATH.into()); - let accounts = config.load_accounts(); - - let default = Account::default(); - - let account = accounts.iter().find(|a| a.id == default.id).unwrap(); - - assert_eq!(*account, default); -} - -#[test] -fn account_test() { - let config = Config::load(CONFIG_PATH.into()); - - let expected = Account { - id: "test".to_string(), - balance: 150, - location: 0, - quests_completed: vec![0], - rooms_unlocked: Vec::new() - }; - - let accounts = config.load_accounts(); - let account = accounts.iter().find(|a| a.id == expected.id).unwrap(); - - assert_eq!(*account, expected); -} - -#[test] -fn load_map() { - let config = Config::load(CONFIG_PATH.into()); - - let room0 = Room { - id: 0, - children: vec![1, 2], - value: 0, - name: "Entrance".to_string(), - description: Some("Enter the dungeon".to_string()), - }; - - let room1 = Room { - id: 1, - children: vec![0, 3], - value: 100, - name: "Kitchen hall".to_string(), - description: None, - }; - - let room2 = Room { - id: 2, - children: vec![0], - value: 250, - name: "Room".to_string(), - description: Some("Simple room with no furniture".to_string()), - }; - - let room3 = Room { - id: 3, - children: vec![1], - value: 175, - name: "Kitchen".to_string(), - description: Some("Knives are stored here".to_string()), - }; - - let expected = Map { - room: vec![room0, room1, room2, room3], - }; - - let map_path = config.full_map_path(); - - let map = Map::load(map_path).unwrap(); - - assert_eq!(map.room, expected.room); -} diff --git a/tests/main/accounts/none.toml b/tests/main/accounts/none.toml deleted file mode 100644 index ca0a7fb..0000000 --- a/tests/main/accounts/none.toml +++ /dev/null @@ -1 +0,0 @@ -# Empty account for testing diff --git a/tests/main/accounts/test.toml b/tests/main/accounts/test.toml deleted file mode 100644 index 439a946..0000000 --- a/tests/main/accounts/test.toml +++ /dev/null @@ -1,5 +0,0 @@ -id = "test" -balance = 150 -location = 0 -quests_completed = [ 0 ] -rooms_unlocked = [] diff --git a/tests/main/map.toml b/tests/main/map.toml deleted file mode 100644 index 018fb2e..0000000 --- a/tests/main/map.toml +++ /dev/null @@ -1,26 +0,0 @@ -[[room]] -id = 0 -children = [ 1, 2 ] -value = 0 -name = "Entrance" -description = "Enter the dungeon" - -[[room]] -id = 1 -children = [ 0, 3 ] -value = 100 -name = "Kitchen hall" - -[[room]] -id = 2 -children = [ 0 ] -value = 250 -name = "Room" -description = "Simple room with no furniture" - -[[room]] -id = 3 -children = [ 1 ] -value = 175 -name = "Kitchen" -description = "Knives are stored here" diff --git a/tests/main/quests/1.toml b/tests/main/quests/1.toml index 855b576..5b67270 100644 --- a/tests/main/quests/1.toml +++ b/tests/main/quests/1.toml @@ -6,4 +6,3 @@ reward = 100 name = "Example easy quest" description = "Answer this quest without any attachments or comments" answer = "Accept the answer if it has no attachments and an empty comment" -public = false