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)); }, } } } }