library

Online co-op book organization video game
git clone https://git.bradleygannon.com/library.git
Log | Files | Refs

book.rs (8258B)


      1 use std::{
      2     collections::{HashMap, hash_map::Keys},
      3     fmt::{Display, Write},
      4     slice::Iter,
      5 };
      6 
      7 use anyhow::{Result, anyhow};
      8 use lazy_static::lazy_static;
      9 use macroquad::math::{Rect, Vec2};
     10 use serde::{Deserialize, Serialize};
     11 use uuid::Uuid;
     12 
     13 use crate::game::RectSerde;
     14 
     15 lazy_static! {
     16     pub static ref BOOKS: BookDatabase =
     17         BookDatabase::from_csv(include_str!("../../assets/books.csv"))
     18             .expect("Failed to parse book database");
     19 }
     20 
     21 pub type BookId = Uuid;
     22 pub type BookshelfId = Uuid;
     23 
     24 #[derive(
     25     Clone, Copy, Debug, Default, Deserialize, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize,
     26 )]
     27 pub struct SlotAddress {
     28     bookshelf_id: BookshelfId,
     29     shelf_index: usize,
     30     position: usize,
     31 }
     32 
     33 impl SlotAddress {
     34     pub fn new(bookshelf_id: BookshelfId, shelf_index: usize, position: usize) -> Self {
     35         SlotAddress {
     36             bookshelf_id,
     37             shelf_index,
     38             position,
     39         }
     40     }
     41     pub fn bookshelf_id(&self) -> BookshelfId {
     42         self.bookshelf_id
     43     }
     44     pub fn shelf_index(&self) -> usize {
     45         self.shelf_index
     46     }
     47     pub fn position(&self) -> usize {
     48         self.position
     49     }
     50 }
     51 
     52 #[derive(
     53     Clone, Copy, Debug, Default, Deserialize, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize,
     54 )]
     55 pub enum Slot {
     56     Book {
     57         book_id: BookId,
     58     },
     59     #[default]
     60     Empty,
     61 }
     62 
     63 #[derive(Clone, Debug, Default, Deserialize, PartialEq, PartialOrd, Serialize)]
     64 pub struct Bookshelf {
     65     bounds: RectSerde,
     66     shelves: Vec<Vec<Slot>>,
     67 }
     68 
     69 impl Bookshelf {
     70     pub const BOOK_WIDTH: f32 = 10.;
     71     pub const BOOK_HEIGHT: f32 = 30.;
     72     pub const FRAME_THICKNESS: f32 = 4.;
     73     pub const SHELF_THICKNESS: f32 = 4.;
     74     pub(crate) fn new(position: Vec2, shelf_count: usize, books_per_shelf: usize) -> Self {
     75         Self {
     76             bounds: Rect::new(
     77                 position.x,
     78                 position.y,
     79                 books_per_shelf as f32 * Self::BOOK_WIDTH + Self::FRAME_THICKNESS * 2.,
     80                 shelf_count as f32 * Self::BOOK_HEIGHT
     81                     + shelf_count.saturating_sub(1) as f32 * Self::SHELF_THICKNESS
     82                     + Self::FRAME_THICKNESS * 2.,
     83             )
     84             .into(),
     85             shelves: vec![vec![Slot::Empty; books_per_shelf]; shelf_count],
     86         }
     87     }
     88     pub fn rect(&self) -> Rect {
     89         self.bounds.into()
     90     }
     91     pub fn iter(&self) -> Iter<'_, Vec<Slot>> {
     92         self.shelves.iter()
     93     }
     94     pub fn shelf_count(&self) -> usize {
     95         self.shelves.len()
     96     }
     97     pub fn get_shelf_mut(&mut self, index: usize) -> Option<&mut Vec<Slot>> {
     98         self.shelves.get_mut(index)
     99     }
    100     pub fn get_slot(&self, slot_address: &SlotAddress) -> Option<&Slot> {
    101         if let Some(shelf) = self.shelves.get(slot_address.shelf_index) {
    102             shelf.get(slot_address.position)
    103         } else {
    104             None
    105         }
    106     }
    107     pub fn get_slot_mut(&mut self, slot_address: &SlotAddress) -> Option<&mut Slot> {
    108         if let Some(shelf) = self.shelves.get_mut(slot_address.shelf_index) {
    109             shelf.get_mut(slot_address.position)
    110         } else {
    111             None
    112         }
    113     }
    114 }
    115 
    116 #[derive(Clone, Copy, Debug, Default, Deserialize, PartialEq, Serialize)]
    117 pub struct FloorBook {
    118     id: BookId,
    119     position: Vec2,
    120     collider: RectSerde,
    121 }
    122 
    123 impl FloorBook {
    124     pub(crate) fn new(id: BookId, position: Vec2, collider: Rect) -> Self {
    125         Self {
    126             id,
    127             position,
    128             collider: collider.into(),
    129         }
    130     }
    131     pub fn id(&self) -> BookId {
    132         self.id
    133     }
    134     pub fn position(&self) -> Vec2 {
    135         self.position
    136     }
    137     pub fn collider(&self) -> RectSerde {
    138         self.collider
    139     }
    140 }
    141 
    142 pub struct BookDatabase {
    143     books: HashMap<BookId, BookMetadata>,
    144 }
    145 
    146 fn _csv_escape(input: &String) -> String {
    147     if input.contains(',') {
    148         format!("\"{input}\"")
    149     } else {
    150         input.clone()
    151     }
    152 }
    153 
    154 impl BookDatabase {
    155     const CSV_HEADER: &str = "id,title,author,publish_year";
    156 
    157     fn new() -> Self {
    158         BookDatabase {
    159             books: HashMap::new(),
    160         }
    161     }
    162     pub(crate) fn keys(&self) -> Keys<'_, BookId, BookMetadata> {
    163         self.books.keys()
    164     }
    165     fn _to_csv(&self, output: &mut impl Write) -> Result<()> {
    166         writeln!(output, "{}", BookDatabase::CSV_HEADER)?;
    167         for (id, metadata) in self.books.iter() {
    168             let BookMetadata {
    169                 title,
    170                 author,
    171                 publish_year,
    172             } = metadata;
    173             writeln!(
    174                 output,
    175                 "{},{},{},{}",
    176                 id,
    177                 _csv_escape(title),
    178                 _csv_escape(author),
    179                 _csv_escape(publish_year),
    180             )?;
    181         }
    182         Ok(())
    183     }
    184     fn from_csv(input: &str) -> Result<Self> {
    185         let mut output = BookDatabase::new();
    186         for (line_index, line) in input.lines().enumerate() {
    187             if line_index == 0 {
    188                 if line != BookDatabase::CSV_HEADER {
    189                     return Err(anyhow!(
    190                         "Invalid CSV header: expected '{}' but got '{}'",
    191                         BookDatabase::CSV_HEADER,
    192                         line
    193                     ));
    194                 }
    195                 continue;
    196             }
    197             let mut fields = vec![];
    198             #[derive(Clone, Copy, Debug)]
    199             enum State {
    200                 Start,
    201                 FieldData,
    202                 Quote,
    203                 FieldEnd,
    204             }
    205             let mut state = State::Start;
    206             let mut new_field = String::new();
    207             let mut quoted_field = false;
    208             for c in line.chars() {
    209                 match (c, state) {
    210                     ('"', State::Start | State::FieldEnd) => {
    211                         quoted_field = true;
    212                         state = State::FieldData;
    213                     }
    214                     ('"', State::FieldData) => state = State::Quote,
    215                     ('"', State::Quote) => {
    216                         if quoted_field {
    217                             new_field.push('"');
    218                         }
    219                     }
    220                     (',', State::Start | State::FieldEnd) => state = State::FieldEnd,
    221                     (',', State::FieldData) => {
    222                         if quoted_field {
    223                             new_field.push(',');
    224                         } else {
    225                             state = State::FieldEnd;
    226                         }
    227                     }
    228                     (',', State::Quote) => {
    229                         if quoted_field {
    230                             state = State::FieldEnd;
    231                         }
    232                     }
    233                     (d, _) => {
    234                         new_field.push(d);
    235                         state = State::FieldData;
    236                     }
    237                 }
    238                 if let State::FieldEnd = state {
    239                     fields.push(new_field.clone());
    240                     new_field.clear();
    241                     quoted_field = false;
    242                 }
    243             }
    244             fields.push(new_field);
    245             if fields.len() != 4 {
    246                 return Err(anyhow!(
    247                     "Error on line {}: expected 4 fields but got {}",
    248                     line_index + 1,
    249                     fields.len()
    250                 ));
    251             }
    252             let id = fields[0].parse()?;
    253             let title = fields[1].clone();
    254             let author = fields[2].clone();
    255             let publish_year = fields[3].clone();
    256             output.books.insert(
    257                 id,
    258                 BookMetadata {
    259                     title,
    260                     author,
    261                     publish_year,
    262                 },
    263             );
    264         }
    265         Ok(output)
    266     }
    267     pub fn get(&self, k: &BookId) -> Option<&BookMetadata> {
    268         self.books.get(k)
    269     }
    270 }
    271 
    272 #[derive(Clone, Debug, Default, Eq, Hash, Ord, PartialEq, PartialOrd)]
    273 pub struct BookMetadata {
    274     title: String,
    275     author: String,
    276     publish_year: String,
    277 }
    278 
    279 impl Display for BookMetadata {
    280     fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
    281         write!(
    282             f,
    283             "{} by {} ({})",
    284             self.title, self.author, self.publish_year
    285         )
    286     }
    287 }