From b9f75e426c7c37140622541f32b00bb9f43cac3e Mon Sep 17 00:00:00 2001 From: 2ndbeam <2ndbeam@disroot.org> Date: Wed, 3 Dec 2025 17:01:40 +0300 Subject: [PATCH] feat: Added Map - Implemented Map - Partially implemented CLI interaction with map - Added load_map test --- src/bin/cli.rs | 185 +++++++++++++++++++++++++++++++++++++++----- src/config/mod.rs | 27 ++++++- src/map/mod.rs | 108 +++++++++++++++++++++++++- tests/io/map.toml | 1 + tests/main.rs | 49 +++++++++++- tests/main/map.toml | 26 +++++++ 6 files changed, 371 insertions(+), 25 deletions(-) create mode 100644 tests/io/map.toml create mode 100644 tests/main/map.toml diff --git a/src/bin/cli.rs b/src/bin/cli.rs index 05e2c24..19bbf4e 100644 --- a/src/bin/cli.rs +++ b/src/bin/cli.rs @@ -1,8 +1,8 @@ -use std::path::PathBuf; +use std::path::{Path, PathBuf}; use clap::{Parser,Subcommand,Args,ValueEnum}; use serde::Deserialize; -use squad_quest::{SquadObject, account::Account, config::Config, quest::{Quest,QuestDifficulty as LibQuestDifficulty}}; +use squad_quest::{SquadObject, account::Account, config::Config, error::Error, map::{Map, Room}, quest::{Quest,QuestDifficulty as LibQuestDifficulty}}; use toml::value::Date; use chrono::{Datelike, NaiveDate, Utc}; @@ -37,6 +37,9 @@ enum Objects { /// Operations on the accounts #[command(subcommand)] Account(AccountCommands), + /// Operations on the map rooms + #[command(subcommand)] + Map(MapCommands), } #[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, ValueEnum)] @@ -208,6 +211,62 @@ struct AccountDeleteArgs { id: String, } +#[derive(Subcommand)] +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)] +struct MapAddArgs { + /// Name of the room + name: String, + /// Price of the room + value: u32, + /// Optional description for the room + #[arg(long,short)] + description: Option, +} + +#[derive(Args)] +struct MapConnectArgs { + /// First room ID + first: u16, + /// Second room ID + second: u16, +} + +#[derive(Args)] +struct MapDeleteArgs { + /// ID of the room to delete + id: u16, +} + +#[derive(Args)] +struct MapUpdateArgs { + /// ID of the room to update + id: u16, + /// Room name + #[arg(short,long)] + name: Option, + /// Room description + #[arg(short,long)] + description: Option, + /// Room price + #[arg(short,long)] + value: Option, +} + fn print_quest_short(quest: &Quest) { println!("Quest #{}: {}", quest.id, quest.name); } @@ -219,7 +278,7 @@ fn print_quest_long(quest: &Quest) { println!("Answer:\n{}", quest.answer); } -fn main() { +fn main() -> Result<(), Error> { let cli = Cli::parse(); let config = Config::load(cli.config.clone()); @@ -241,9 +300,6 @@ fn main() { 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 }; @@ -342,8 +398,7 @@ fn main() { let not_str = if args.reverse {" not "} else {" "}; if quest.public != args.reverse { - println!("Quest #{} is already{}public", quest.id, not_str); - return; + panic!("Quest #{} is already{}public", quest.id, not_str); } quest.public = !args.reverse; @@ -358,8 +413,8 @@ fn main() { }, } }, - Objects::Account(args) => { - match args { + Objects::Account(commands) => { + match commands { AccountCommands::List => { let accounts = config.load_accounts(); @@ -376,8 +431,7 @@ fn main() { let accounts = config.load_accounts(); if let Some(_) = accounts.iter().find(|a| a.id == account.id) { - eprintln!("Error: account {} exists.", account.id); - return; + panic!("Error: account {} exists.", account.id); } let accounts_path = config.full_accounts_path(); @@ -397,8 +451,7 @@ fn main() { let account = match accounts.iter_mut().find(|a| a.id == args.id) { Some(acc) => acc, None => { - eprintln!("Could not find account \"{}\"", args.id); - return; + panic!("Could not find account \"{}\"", args.id); } }; @@ -414,8 +467,7 @@ fn main() { if args.negative_ok { account.balance = 0u32; } else { - eprintln!("Error: balance ({}) is less than {}.", account.balance, args.value); - return; + panic!("Error: balance ({}) is less than {}.", account.balance, args.value); } } else { account.balance -= args.value; @@ -440,16 +492,14 @@ fn main() { let account = match accounts.iter_mut().find(|a| a.id == args.account) { Some(acc) => acc, None => { - eprintln!("Could not find account \"{}\"", args.account); - return; + panic!("Could not find account \"{}\"", args.account); } }; let quests = config.load_quests(); if let None = quests.iter().find(|q| q.id == args.quest) { - eprintln!("Could not find quest #{}", args.quest); - return; + panic!("Could not find quest #{}", args.quest); } match account.quests_completed.iter().find(|qid| **qid == args.quest) { @@ -483,6 +533,101 @@ fn main() { } }, } + }, + Objects::Map(commands) => { + let map_path = config.full_map_path(); + let mut map = Map::load(map_path.clone())?; + map.room.sort_by(|a,b| a.id.cmp(&b.id)); + match commands { + MapCommands::List => { + for room in map.room { + println!("Room #{}: {}; Connections: {:?}", room.id, room.name, room.children); + } + }, + MapCommands::Add(args) => { + let last_id = match map.room.last() { + Some(r) => r.id + 1u16, + None => 0u16 + }; + let room = Room { + id: last_id, + name: args.name.clone(), + description: args.description.clone(), + ..Default::default() + }; + let r_id = room.id; + map.room.push(room); + match map.save(map_path.parent().unwrap_or(Path::new("")).to_owned()) { + Ok(_) => { + println!("Created room #{}.", r_id); + println!("Successfully saved map."); + }, + Err(error) => { + eprintln!("Error while saving map: {error}"); + } + } + }, + MapCommands::Delete(args) => { + let Some(room) = map.room.iter().find(|r| r.id == args.id) else { + panic!("Error: Room #{} not found", args.id); + }; + + 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_path.parent().unwrap_or(Path::new("")).to_owned()) { + Ok(_) => { + println!("Removed room #{}.", r_id); + println!("Successfully saved map."); + }, + Err(error) => { + eprintln!("Error while saving map: {error}"); + } + } + }, + MapCommands::Update(args) => { + let Some(room) = map.room.iter_mut().find(|r| r.id == args.id) else { + panic!("Error: Room #{} not found", args.id); + }; + + 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; + } + + match map.save(map_path.parent().unwrap_or(Path::new("")).to_owned()) { + Ok(_) => { + println!("Updated room #{}.", args.id); + println!("Successfully saved map."); + }, + Err(error) => { + eprintln!("Error while saving map: {error}"); + } + } + }, + MapCommands::Connect(_) => { + todo!(); + }, + MapCommands::Disconnect(_) => { + todo!(); + } + } } } + Ok(()) } diff --git a/src/config/mod.rs b/src/config/mod.rs index d5a86da..b47b565 100644 --- a/src/config/mod.rs +++ b/src/config/mod.rs @@ -171,7 +171,7 @@ impl Config { out_vec } - /// Returns full path to quests folder + /// Returns full path to accounts folder /// This path will be relative to $PWD, not to config. /// /// # Examples @@ -237,4 +237,29 @@ impl Config { 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/map/mod.rs b/src/map/mod.rs index d4eb205..17e42ce 100644 --- a/src/map/mod.rs +++ b/src/map/mod.rs @@ -1,5 +1,107 @@ //! Map, a.k.a. a graph of rooms -#![allow(dead_code)] -/// Graph for room nodes -pub struct Map; +use std::{fs, io::Write, path::PathBuf}; + +use serde::{Deserialize, Serialize}; + +use crate::{SquadObject, error::Error}; + +/// 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(()) + } + +} + +/// 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, + } + } +} diff --git a/tests/io/map.toml b/tests/io/map.toml new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/tests/io/map.toml @@ -0,0 +1 @@ + diff --git a/tests/main.rs b/tests/main.rs index b582e5c..b2d054d 100644 --- a/tests/main.rs +++ b/tests/main.rs @@ -1,4 +1,4 @@ -use squad_quest::{account::Account, config::Config, quest::Quest}; +use squad_quest::{SquadObject, account::Account, config::Config, map::{Map, Room}, quest::Quest}; static CONFIG_PATH: &str = "./tests/main/config.toml"; @@ -81,3 +81,50 @@ fn account_test() { 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/map.toml b/tests/main/map.toml new file mode 100644 index 0000000..018fb2e --- /dev/null +++ b/tests/main/map.toml @@ -0,0 +1,26 @@ +[[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"