- Bump version to 0.11.0 - Added data table to quests, accounts and rooms - Discord bot now adds "avatar" and "name" data to accounts on init - Added CLI "map data" command
462 lines
20 KiB
Rust
462 lines
20 KiB
Rust
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(),
|
|
impl_path: args.implpath.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(),
|
|
..Default::default()
|
|
};
|
|
|
|
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()),
|
|
..Default::default()
|
|
};
|
|
|
|
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));
|
|
},
|
|
MapCommands::Data(args) => {
|
|
if let Some(room) = map.room.iter().find(|r| r.id == args.id) {
|
|
if let Some(data) = &room.data {
|
|
for (key, value) in data {
|
|
println!("{key} = {value}");
|
|
}
|
|
}
|
|
}
|
|
},
|
|
}
|
|
}
|
|
}
|
|
}
|