fish_lib/
game.rs

1use crate::config::{Config, ConfigBuilderInterface, ConfigInterface};
2use crate::data::item_data::ItemData;
3use crate::data::location_data::LocationData;
4use crate::data::species_data::SpeciesData;
5use crate::database::{Database, DatabaseInterface};
6use crate::dto::inventory::Inventory;
7use crate::dto::user_location_unlock::UserLocationUnlock;
8use crate::game::errors::resource::GameResourceError;
9use crate::game::errors::GameResult;
10use crate::game::interface::GameInterface;
11use crate::game::repositories::fishing_history_entry_repository::FishingHistoryEntryRepositoryInterface;
12use crate::game::repositories::item_repository::ItemRepositoryInterface;
13use crate::game::repositories::pond_repository::PondRepositoryInterface;
14use crate::game::repositories::specimen_repository::SpecimenRepositoryInterface;
15use crate::game::repositories::user_repository::UserRepositoryInterface;
16use crate::game::service_provider::{ServiceProvider, ServiceProviderInterface};
17use crate::game::services::encounter_service::EncounterServiceInterface;
18use crate::game::services::fishing_history_service::FishingHistoryServiceInterface;
19use crate::game::services::item_service::ItemServiceInterface;
20use crate::game::services::location_service::LocationServiceInterface;
21use crate::game::services::pond_service::PondServiceInterface;
22use crate::game::services::species_service::SpeciesServiceInterface;
23use crate::game::services::specimen_service::SpecimenServiceInterface;
24use crate::game::services::user_service::UserServiceInterface;
25use crate::game::services::weather_service::WeatherServiceInterface;
26use crate::game::systems::weather_system::weather::Weather;
27use crate::models::fishing_history_entry::FishingHistoryEntry;
28use crate::models::item::properties_container::ItemPropertiesContainerInterface;
29use crate::models::item::Item;
30use crate::models::specimen::Specimen;
31use crate::models::user::User;
32use std::sync::{Arc, RwLock};
33
34pub mod errors;
35pub mod interface;
36pub mod prelude;
37pub mod repositories;
38pub mod service_provider;
39pub mod services;
40pub mod systems;
41
42/// # Game
43/// Primary interface for all game operations.
44///
45/// The Game struct implements [`GameInterface`] and serves as the main entry point
46/// for interacting with the game system. All game functionality is accessed
47/// through this struct's implementation.
48pub struct Game {
49    service_provider: Arc<dyn ServiceProviderInterface>,
50}
51
52impl Game {
53    pub fn new(db_url: &str, config: Option<Arc<dyn ConfigInterface>>) -> GameResult<Self> {
54        let config = config.unwrap_or(Config::builder().build().unwrap());
55        let db = Database::create();
56        db.write()
57            .expect("Failed to get database write lock")
58            .connect(db_url)?;
59
60        let service_provider = ServiceProvider::create(config, db);
61        let game = Game { service_provider };
62        Ok(game)
63    }
64}
65
66impl GameInterface for Game {
67    /// Get [ItemData] for the specified item ID.
68    ///
69    /// # Arguments
70    ///
71    /// * `item_id`: The ID of the item to get the data of. (See [Config])
72    ///
73    /// # Returns
74    ///
75    /// Result<Arc<[ItemData], Global>, [errors::GameError]>
76    /// - The [ItemData], if an item with the given ID exists
77    /// - An error, if no item with the given id exists
78    ///
79    /// # Examples
80    ///
81    /// ```
82    /// use std::collections::HashMap;
83    /// use std::env;
84    /// use fish_lib::config::{Config, ConfigBuilderInterface};
85    /// use fish_lib::data::item_data::ItemData;
86    /// use fish_lib::game::prelude::*;
87    /// use fish_lib::game::service_provider::ServiceProviderInterface;
88    ///
89    /// const ITEM_ID: i32 = 1;
90    /// const ITEM_NAME: &str = "Super Bad Rod";
91    ///
92    /// // Define some item data
93    /// let item_data = ItemData {
94    ///     name: ITEM_NAME.to_string(),
95    ///     ..Default::default()
96    /// };
97    /// let item_data_map = HashMap::from([(ITEM_ID, item_data)]);
98    ///
99    /// // Add the location data to the config
100    /// let config = Config::builder().items(item_data_map).build().unwrap();
101    ///
102    /// // Create game and clear database for a blank test state
103    /// let database_url = env::var("DATABASE_URL").expect("DATABASE_URL must be set");
104    /// let game = Game::new(&database_url, Some(config)).unwrap();
105    /// game.database().write().unwrap().clear().unwrap();
106    ///
107    /// // Finding the item data
108    /// let found_item_data = game.item_find(ITEM_ID).unwrap();
109    /// assert_eq!(&found_item_data.name, ITEM_NAME);
110    ///
111    /// // Searching for non-existent item data
112    /// let error = game.item_find(ITEM_ID + 1).unwrap_err();
113    /// assert!(error.is_not_found());
114    /// if let Some(resource_error) = error.as_resource_error() {
115    ///     assert!(resource_error.is_item_not_found());
116    ///     assert_eq!(resource_error.get_item_type_id(), Some(ITEM_ID + 1));
117    /// } else {
118    ///     panic!("{:?}", error);
119    /// }
120    /// ```
121    fn item_find(&self, item_id: i32) -> GameResult<Arc<ItemData>> {
122        match self.config().get_item_data(item_id) {
123            Some(item_data) => Ok(item_data.clone()),
124            None => Err(GameResourceError::item_not_found(item_id).into()),
125        }
126    }
127
128    /// Get [LocationData] for the specified location ID.
129    ///
130    /// # Arguments
131    ///
132    /// * `location_id`: The ID of the location to get the data of. (See [Config])
133    ///
134    /// # Returns
135    ///
136    /// Result<Arc<[LocationData], Global>, [errors::GameError]>
137    /// - The [LocationData], if the location with the given ID exists
138    /// - An error, if no location with the given ID exists
139    ///
140    /// # Examples
141    ///
142    /// ```
143    /// use std::collections::HashMap;
144    /// use std::env;
145    /// use fish_lib::config::{Config, ConfigBuilderInterface};
146    /// use fish_lib::game::prelude::*;
147    /// use fish_lib::data::location_data::LocationData;
148    /// use fish_lib::game::service_provider::ServiceProviderInterface;
149    ///
150    /// const LOCATION_ID: i32 = 1;
151    /// const LOCATION_NAME: &str = "Central Europe";
152    ///
153    /// // Define some location data
154    /// let location_data = LocationData {
155    ///     name: LOCATION_NAME.to_string(),
156    ///     ..Default::default()
157    /// };
158    /// let location_data_map = HashMap::from([(LOCATION_ID, location_data)]);
159    ///
160    /// // Add the location data to the config
161    /// let config = Config::builder().locations(location_data_map).build().unwrap();
162    ///
163    /// // Create game and clear database for a blank test state
164    /// let database_url = env::var("DATABASE_URL").expect("DATABASE_URL must be set");
165    /// let game = Game::new(&database_url, Some(config)).unwrap();
166    /// game.database().write().unwrap().clear().unwrap();
167    ///
168    /// // Finding the location data
169    /// let found_location_data = game.location_find(LOCATION_ID).unwrap();
170    /// assert_eq!(&found_location_data.name, LOCATION_NAME);
171    ///
172    /// // Searching for non-existent location data
173    /// let error = game.location_find(LOCATION_ID + 1).unwrap_err();
174    /// assert!(error.is_not_found());
175    /// if let Some(resource_error) = error.as_resource_error() {
176    ///     assert!(resource_error.is_location_not_found());
177    ///     assert_eq!(resource_error.get_location_id(), Some(LOCATION_ID + 1));
178    /// } else {
179    ///     panic!("{:?}", error);
180    /// }
181    /// ```
182    fn location_find(&self, location_id: i32) -> GameResult<Arc<LocationData>> {
183        match self.config().get_location_data(location_id) {
184            Some(data) => Ok(data.clone()),
185            None => Err(GameResourceError::location_not_found(location_id).into()),
186        }
187    }
188
189    /// Get the current [Weather] of a specified location.
190    /// You will be able to get the weather for all locations specified by you in your [Config].
191    ///
192    /// # Arguments
193    ///
194    /// * `location_id`: The ID of the location to get the current [Weather] from. (See [Config])
195    ///
196    /// # Returns
197    /// Result<[Weather], [errors::GameError]>
198    ///
199    /// # Examples
200    ///
201    /// ```
202    /// use std::collections::HashMap;
203    /// use std::env;
204    /// use fish_lib::config::{Config, ConfigBuilderInterface};
205    /// use fish_lib::data::location_data::LocationData;
206    /// use fish_lib::data::season_data::SeasonData;
207    /// use fish_lib::game::prelude::*;
208    /// use fish_lib::game::service_provider::ServiceProviderInterface;
209    ///
210    /// const LOCATION_ID: i32 = 1;
211    ///
212    /// // For simplicity in testing, create a location with constant weather
213    /// let every_season = SeasonData {
214    ///     min_temp_c: 10.0,
215    ///     max_temp_c: 10.0,
216    ///     ..Default::default()
217    /// };
218    ///
219    /// let location_data = LocationData {
220    ///     spring: every_season.clone(),
221    ///     summer: every_season.clone(),
222    ///     autumn: every_season.clone(),
223    ///     winter: every_season.clone(),
224    ///     ..Default::default()
225    /// };
226    ///
227    /// let location_data_map = HashMap::from([(LOCATION_ID, location_data)]);
228    /// let config = Config::builder().locations(location_data_map).build().unwrap();
229    ///
230    /// // Create game and clear database for a blank test state
231    /// let database_url = env::var("DATABASE_URL").expect("DATABASE_URL must be set");
232    /// let game = Game::new(&database_url, Some(config)).unwrap();
233    /// game.database().write().unwrap().clear().unwrap();
234    ///
235    /// // Get the current weather
236    /// let location_data = game.location_find(LOCATION_ID).unwrap();
237    /// let weather = game.location_weather_current(location_data).unwrap();
238    /// assert_eq!(weather.temperature_c, 10.0);
239    /// ```
240    fn location_weather_current(&self, location: Arc<LocationData>) -> GameResult<Weather> {
241        self.weather_service().get_current_weather(location)
242    }
243
244    /// Get [SpeciesData] for the specified species ID.
245    ///
246    /// # Arguments
247    ///
248    /// * `species_id`: The ID of the species to get the data of. (See [Config])
249    ///
250    /// # Returns
251    ///
252    /// Result<Arc<[SpeciesData], Global>, [errors::GameError]>
253    /// - The [SpeciesData], if the species with the given ID exists
254    /// - An error, if no species with the given ID exists
255    ///
256    /// # Examples
257    ///
258    /// ```
259    /// use std::collections::HashMap;
260    /// use std::env;
261    /// use fish_lib::config::{Config, ConfigBuilderInterface};
262    /// use fish_lib::data::species_data::SpeciesData;
263    /// use fish_lib::game::prelude::*;
264    /// use fish_lib::game::service_provider::ServiceProviderInterface;
265    ///
266    /// const SPECIES_ID: i32 = 1;
267    /// const SPECIES_NAME: &str = "Salmon";
268    ///
269    /// // Define some species data
270    /// let species_data = SpeciesData {
271    ///     name: SPECIES_NAME.to_string(),
272    ///     ..Default::default()
273    /// };
274    /// let species_data_map = HashMap::from([(SPECIES_ID, species_data)]);
275    ///
276    /// // Add the species data to the config
277    /// let config = Config::builder().species(species_data_map).build().unwrap();
278    ///
279    /// // Create game and clear database for a blank test state
280    /// let database_url = env::var("DATABASE_URL").expect("DATABASE_URL must be set");
281    /// let game = Game::new(&database_url, Some(config)).unwrap();
282    /// game.database().write().unwrap().clear().unwrap();
283    ///
284    /// // Finding the species data
285    /// let found_species_data = game.species_find(SPECIES_ID).unwrap();
286    /// assert_eq!(&found_species_data.name, SPECIES_NAME);
287    ///
288    /// // Searching for non-existent species data
289    /// let error = game.species_find(SPECIES_ID + 1).unwrap_err();
290    /// assert!(error.is_not_found());
291    /// if let Some(resource_error) = error.as_resource_error() {
292    ///     assert!(resource_error.is_species_not_found());
293    ///     assert_eq!(resource_error.get_species_id(), Some(SPECIES_ID + 1));
294    /// } else {
295    ///     panic!("{:?}", error);
296    /// }
297    /// ```
298    fn species_find(&self, species_id: i32) -> GameResult<Arc<SpeciesData>> {
299        match self.config().get_species_data(species_id) {
300            Some(data) => Ok(data.clone()),
301            None => Err(GameResourceError::species_not_found(species_id).into()),
302        }
303    }
304
305    /// Generate a random [Specimen] of the given species ID and assign it to the given [User].
306    ///
307    /// # Arguments
308    ///
309    /// * `user`: The [User] for which the catch is to be registered
310    /// * `species_id`: The species ID of the [Specimen] to be caught (See [Config])
311    ///
312    /// # Returns
313    /// Result<([Specimen], [FishingHistoryEntry]), [errors::GameError]>
314    ///
315    /// # Examples
316    ///
317    /// ```
318    /// use std::collections::HashMap;
319    /// use std::env;
320    /// use fish_lib::config::{Config, ConfigBuilderInterface};
321    /// use fish_lib::data::species_data::SpeciesData;
322    /// use fish_lib::game::prelude::*;
323    /// use fish_lib::game::repositories::user_repository::UserRepository;
324    /// use fish_lib::game::service_provider::ServiceProviderInterface;
325    /// use fish_lib::models::user::User;
326    ///
327    /// const USER_EXTERNAL_ID: i64 = 1337;
328    /// const SPECIES_ID: i32 = 1;
329    /// const SPECIES_NAME: &str = "Salmon";
330    ///
331    /// // Define some species data
332    /// let species_data = SpeciesData {
333    ///     name: SPECIES_NAME.to_string(),
334    ///     ..Default::default()
335    /// };
336    /// let species_data_map = HashMap::from([(SPECIES_ID, species_data)]);
337    ///
338    /// // Add the species data to the config
339    /// let config = Config::builder().species(species_data_map).build().unwrap();
340    ///
341    /// // Create game and clear database for a blank test state
342    /// let database_url = env::var("DATABASE_URL").expect("DATABASE_URL must be set");
343    /// let game = Game::new(&database_url, Some(config)).unwrap();
344    /// game.database().write().unwrap().clear().unwrap();
345    ///
346    /// // Fetch the species data
347    /// let species = game.species_find(1).unwrap();
348    ///
349    /// // Create a user
350    /// let user = game.user_register(USER_EXTERNAL_ID).unwrap();
351    ///
352    /// // Let the user catch a specimen of the specified species ID
353    /// let (specimen, history_entry) = game.user_catch_specific_specimen(&user, species.clone()).unwrap();
354    /// assert_eq!(specimen.species_id, SPECIES_ID);
355    /// assert_eq!(specimen.user_id, user.id);
356    /// assert_eq!(history_entry.species_id, SPECIES_ID);
357    /// assert_eq!(history_entry.caught_count, 1);
358    ///
359    /// // Catch a specimen for a user that doesn't exist
360    /// let dummy_user = User {
361    ///     id: -1,
362    ///     external_id: USER_EXTERNAL_ID + 1,
363    ///     ..Default::default()
364    /// };
365    /// let user_error = game.user_catch_specific_specimen(&dummy_user, species).unwrap_err();
366    /// if let Some(resource_error) = user_error.as_resource_error() {
367    ///     assert!(resource_error.is_user_not_found());
368    ///     assert_eq!(resource_error.get_external_id(), Some(USER_EXTERNAL_ID + 1));
369    /// } else {
370    ///     panic!("{:?}", user_error);
371    /// }
372    ///
373    /// ```
374    fn user_catch_specific_specimen(
375        &self,
376        user: &User,
377        species: Arc<SpeciesData>,
378    ) -> GameResult<(Specimen, FishingHistoryEntry)> {
379        let specimen = self.specimen_service().process_catch(user, species)?;
380        let entry = self.fishing_history_service().register_catch(&specimen)?;
381        Ok((specimen, entry))
382    }
383
384    /// Check the fishing history of a [User] with a specified species ID
385    ///
386    /// # Arguments
387    ///
388    /// * `user`: The [User] to check the fishing history of
389    /// * `species_id`: Fishing history species ID to check for the giving [User] (See [Config])
390    ///
391    /// # Returns
392    ///
393    /// Result<[FishingHistoryEntry], [errors::GameError]>
394    /// - The fishing history of the given [User] with a specified species, if it exists
395    /// - An error, if it does not exist, aka if the [User] did not catch that species yet or the [User] does not exist
396    ///
397    /// # Examples
398    ///
399    /// ```
400    /// use std::collections::HashMap;
401    /// use std::env;
402    /// use fish_lib::config::{Config, ConfigBuilderInterface};
403    /// use fish_lib::data::species_data::SpeciesData;
404    /// use fish_lib::game::prelude::*;
405    /// use fish_lib::game::repositories::user_repository::UserRepository;
406    /// use fish_lib::game::service_provider::ServiceProviderInterface;
407    /// use fish_lib::models::user::User;
408    ///
409    /// const USER_EXTERNAL_ID: i64 = 1337;
410    /// const SPECIES_ID: i32 = 1;
411    /// const SPECIES_NAME: &str = "Salmon";
412    ///
413    /// // Define some species data
414    /// let species_data = SpeciesData {
415    ///     name: SPECIES_NAME.to_string(),
416    ///     ..Default::default()
417    /// };
418    /// let species_data2 = SpeciesData {
419    ///     ..Default::default()
420    ///  };
421    /// let species_data_map = HashMap::from([(SPECIES_ID, species_data), (SPECIES_ID + 1, species_data2)]);
422    ///
423    /// // Add the species data to the config
424    /// let config = Config::builder().species(species_data_map).build().unwrap();
425    ///
426    /// // Create game and clear database for a blank test state
427    /// let database_url = env::var("DATABASE_URL").expect("DATABASE_URL must be set");
428    /// let game = Game::new(&database_url, Some(config)).unwrap();
429    /// game.database().write().unwrap().clear().unwrap();
430    ///
431    /// // Create a user
432    /// let user = game.user_register(USER_EXTERNAL_ID).unwrap();
433    ///
434    /// // Get species data
435    /// let species = game.species_find(SPECIES_ID).unwrap();
436    /// let species2 = game.species_find(SPECIES_ID + 1).unwrap();
437    ///
438    /// // Let the user catch a specimen
439    /// game.user_catch_specific_specimen(&user, species.clone()).unwrap();
440    ///
441    /// // Fetch the fishing history of the user with the given species ID
442    /// let history_entry = game.user_get_fishing_history(&user, species.clone()).unwrap();
443    /// assert_eq!(history_entry.species_id, SPECIES_ID);
444    /// assert_eq!(history_entry.user_id, user.id);
445    /// assert_eq!(history_entry.caught_count, 1);
446    /// assert_eq!(history_entry.sold_count, 0);
447    ///
448    /// // Trying to fetch the fishing history with a species the user didn't catch yet
449    /// let error = game.user_get_fishing_history(&user, species2).unwrap_err();
450    /// assert!(error.is_not_found());
451    /// if let Some(resource_error) = error.as_resource_error() {
452    ///     assert!(resource_error.is_no_fishing_history());
453    ///     assert_eq!(resource_error.get_external_id(), Some(USER_EXTERNAL_ID));
454    ///     assert_eq!(resource_error.get_species_id(), Some(SPECIES_ID + 1));
455    /// } else {
456    ///     panic!("{:?}", error);
457    /// }
458    /// ```
459    fn user_get_fishing_history(
460        &self,
461        user: &User,
462        species: Arc<SpeciesData>,
463    ) -> GameResult<FishingHistoryEntry> {
464        match self
465            .fishing_history_entry_repository()
466            .find_by_user_and_species_id(user.id, species.id)?
467        {
468            Some(entry) => Ok(entry),
469            None => Err(GameResourceError::no_fishing_history(user.external_id, species.id).into()),
470        }
471    }
472
473    /// Find a [User] by their external ID.
474    ///
475    /// # Arguments
476    ///
477    /// * `external_id`: A freely selectable ID that your system will use to identify this [User].
478    ///
479    /// # Returns
480    ///
481    /// Result<[User], [errors::GameError]>
482    /// - A [User] with the given external ID
483    /// - An error, if:
484    ///     - The [User] is not found
485    ///     - Database operations fail
486    ///
487    /// # Examples
488    ///
489    /// ```
490    /// use std::env;
491    /// use fish_lib::game::prelude::*;
492    /// use fish_lib::game::service_provider::ServiceProviderInterface;
493    ///
494    /// const EXTERNAL_ID: i64 = 1337;
495    ///
496    /// // Create game and clear database for a blank test state
497    /// let database_url = env::var("DATABASE_URL").expect("DATABASE_URL must be set");
498    /// let game = Game::new(&database_url, None).unwrap();
499    /// game.database().write().unwrap().clear().unwrap();
500    ///
501    /// // Finding an existing user
502    /// let new_user = game.user_register(EXTERNAL_ID).unwrap();
503    /// let found_user = game.user_find(EXTERNAL_ID).unwrap();
504    /// assert_eq!(new_user, found_user);
505    ///
506    /// // Searching for a non-existent user
507    /// let error = game.user_find(EXTERNAL_ID + 1).unwrap_err();
508    /// assert!(error.is_not_found());
509    /// if let Some(resource_error) = error.as_resource_error() {
510    ///     assert!(resource_error.is_user_not_found());
511    ///     assert_eq!(resource_error.get_external_id(), Some(EXTERNAL_ID + 1));
512    /// } else {
513    ///     panic!("{:?}", error);
514    /// }
515    /// ```
516    fn user_find(&self, external_id: i64) -> GameResult<User> {
517        match self.user_repository().find_by_external_id(external_id)? {
518            Some(user) => Ok(user),
519            None => Err(GameResourceError::user_not_found(external_id).into()),
520        }
521    }
522
523    /// Fetch the unlocked locations of a given user.
524    ///
525    /// # Arguments
526    ///
527    /// * `user`: The [User] to get the unlocked locations ([UserLocationUnlock]) from.
528    ///
529    /// # Returns
530    /// Result<Vec<[UserLocationUnlock], Global>, [errors::GameError]>
531    /// - A vector with information about all location unlocks
532    /// - An error, if database operations fail
533    ///
534    /// # Examples
535    ///
536    /// ```
537    /// use std::collections::HashMap;
538    /// use std::env;
539    /// use fish_lib::config::{Config, ConfigBuilderInterface};
540    /// use fish_lib::data::location_data::LocationData;
541    /// use fish_lib::game::prelude::*;
542    /// use fish_lib::game::service_provider::ServiceProviderInterface;
543    ///
544    /// const EXTERNAL_ID: i64 = 1337;
545    /// const LOCATION_ID: i32 = 13;
546    /// const LOCATION_NAME: &str = "Island";
547    ///
548    /// // Define some location data
549    /// let location_data = LocationData {
550    ///     name: LOCATION_NAME.to_string(),
551    ///     ..Default::default()
552    /// };
553    /// let location_data_map = HashMap::from([(LOCATION_ID, location_data)]);
554    ///
555    /// // Add the location data to the config
556    /// let config = Config::builder().locations(location_data_map).build().unwrap();
557    ///
558    /// // Create game and clear database for a blank test state
559    /// let database_url = env::var("DATABASE_URL").expect("DATABASE_URL must be set");
560    /// let game = Game::new(&database_url, Some(config)).unwrap();
561    /// game.database().write().unwrap().clear().unwrap();
562    ///
563    /// // Registering a new user
564    /// let user = game.user_register(EXTERNAL_ID).unwrap();
565    ///
566    /// // Let the user unlock a location
567    /// let island_location = game.location_find(LOCATION_ID).unwrap();
568    /// let unlocked_location = game.user_unlock_location(&user, island_location).unwrap();
569    ///
570    /// // Get unlocked locations
571    /// let unlocked_locations = game.user_get_unlocked_locations(&user).unwrap();
572    /// assert_eq!(unlocked_locations.len(), 1);
573    /// assert_eq!(unlocked_locations[0], unlocked_location);
574    /// ```
575    fn user_get_unlocked_locations(&self, user: &User) -> GameResult<Vec<UserLocationUnlock>> {
576        let user_locations = self.user_service().get_unlocked_locations(user)?;
577        let location_unlocks =
578            UserLocationUnlock::from_user_locations(user_locations, |location_id| {
579                self.location_find(location_id).ok()
580            });
581        Ok(location_unlocks)
582    }
583
584    /// Get the [Inventory] of a specified [User].
585    ///
586    /// # Arguments
587    ///
588    /// * `user`: The [User] to get the [Inventory] from.
589    ///
590    /// # Returns
591    ///
592    /// Result<[Inventory], [errors::GameError]>
593    /// - The user's inventory, if the user exists
594    /// - An error, if a database error occurred
595    ///
596    /// # Examples
597    ///
598    /// ```
599    /// use std::collections::HashMap;
600    /// use std::env;
601    /// use fish_lib::config::{Config, ConfigBuilderInterface};
602    /// use fish_lib::data::item_data::ItemData;
603    /// use fish_lib::game::prelude::*;
604    /// use fish_lib::game::service_provider::ServiceProviderInterface;
605    ///
606    /// const EXTERNAL_ID: i64 = 1337;
607    /// const ITEM_ID: i32 = 1;
608    /// const ITEM_NAME: &str = "Super Bad Rod";
609    ///
610    /// // Define some item data
611    /// let item_data = ItemData {
612    ///     name: ITEM_NAME.to_string(),
613    ///     ..Default::default()
614    /// };
615    /// let item_data_map = HashMap::from([(ITEM_ID, item_data)]);
616    ///
617    /// // Add the location data to the config
618    /// let config = Config::builder().items(item_data_map).build().unwrap();
619    ///
620    /// // Create game and clear database for a blank test state
621    /// let database_url = env::var("DATABASE_URL").expect("DATABASE_URL must be set");
622    /// let game = Game::new(&database_url, Some(config)).unwrap();
623    /// game.database().write().unwrap().clear().unwrap();
624    ///
625    /// // Registering a new user
626    /// let user = game.user_register(EXTERNAL_ID).unwrap();
627    /// assert_eq!(user.external_id, EXTERNAL_ID);
628    ///
629    /// /// Giving the user an item
630    /// let item_data = game.item_find(ITEM_ID).unwrap();
631    /// let item = game.user_item_give(&user, item_data, 1).unwrap();
632    ///
633    /// let inventory = game.user_inventory(&user).unwrap();
634    /// let items = inventory.get_items();
635    /// assert_eq!(items[0], item);
636    ///
637    /// ```
638    fn user_inventory(&self, user: &User) -> GameResult<Inventory> {
639        self.item_service().get_inventory(user)
640    }
641
642    /// Give a [User] a specified [ItemData].
643    ///
644    /// # Arguments
645    ///
646    /// * `user`: The [User] to give the item to.
647    /// * `item_data`: The [ItemData] to give the user. (See [Config])
648    /// * `count`: How much of the item to give the user.
649    ///     0 or 1 results in the default specified count if the item is stackable.
650    ///     If the item is not stackable (unique) it'll be added once (no matter the specified count).
651    ///
652    /// # Returns
653    ///
654    /// Result<[Item], [errors::GameError]>
655    /// - The newly created item when the operation was successful
656    /// - An error, if there was an error stacking the item or database operations failed
657    ///
658    /// # Examples
659    ///
660    /// ```
661    /// use std::collections::HashMap;
662    /// use std::env;
663    /// use fish_lib::config::{Config, ConfigBuilderInterface};
664    /// use fish_lib::data::item_data::ItemData;
665    /// use fish_lib::game::prelude::*;
666    /// use fish_lib::game::service_provider::ServiceProviderInterface;
667    ///
668    /// const EXTERNAL_ID: i64 = 1337;
669    /// const ITEM_ID: i32 = 1;
670    /// const ITEM_NAME: &str = "Super Bad Rod";
671    ///
672    /// // Define some item data
673    /// let item_data = ItemData {
674    ///     name: ITEM_NAME.to_string(),
675    ///     ..Default::default()
676    /// };
677    /// let item_data_map = HashMap::from([(ITEM_ID, item_data)]);
678    ///
679    /// // Add the location data to the config
680    /// let config = Config::builder().items(item_data_map).build().unwrap();
681    ///
682    /// // Create game and clear database for a blank test state
683    /// let database_url = env::var("DATABASE_URL").expect("DATABASE_URL must be set");
684    /// let game = Game::new(&database_url, Some(config)).unwrap();
685    /// game.database().write().unwrap().clear().unwrap();
686    ///
687    /// // Registering a new user
688    /// let user = game.user_register(EXTERNAL_ID).unwrap();
689    /// assert_eq!(user.external_id, EXTERNAL_ID);
690    ///
691    /// // Giving the user an item
692    /// let item_data = game.item_find(ITEM_ID).unwrap();
693    /// let item = game.user_item_give(&user, item_data, 1).unwrap();
694    ///
695    /// let inventory = game.user_inventory(&user).unwrap();
696    /// let items = inventory.get_items();
697    /// assert_eq!(items[0], item);
698    /// ```
699    fn user_item_give(
700        &self,
701        user: &User,
702        item_data: Arc<ItemData>,
703        count: u64,
704    ) -> GameResult<Item> {
705        let item = if count <= 1 || !item_data.is_stackable() {
706            self.item_service().create_and_save_item(item_data, user)?
707        } else {
708            self.item_service()
709                .create_and_save_item_with_count(item_data, user, count)?
710        };
711        Ok(item)
712    }
713
714    /// Register a new [User] by their external ID.
715    ///
716    /// # Arguments
717    ///
718    /// * `external_id`: A freely selectable ID that your system will use to identify this [User].
719    ///
720    /// # Returns
721    ///
722    /// Result<[User], [errors::GameError]>
723    /// - A newly created [User] with the given external id
724    /// - An error, if:
725    ///     - A [User] with the given external id already exists
726    ///     - Database operations fail
727    ///
728    /// # Examples
729    ///
730    /// ```
731    /// use std::env;
732    /// use fish_lib::game::prelude::*;
733    /// use fish_lib::game::service_provider::ServiceProviderInterface;
734    ///
735    /// const EXTERNAL_ID: i64 = 1337;
736    ///
737    /// // Create game and clear database for a blank test state
738    /// let database_url = env::var("DATABASE_URL").expect("DATABASE_URL must be set");
739    /// let game = Game::new(&database_url, None).unwrap();
740    /// game.database().write().unwrap().clear().unwrap();
741    ///
742    /// // Registering a new user
743    /// let user = game.user_register(EXTERNAL_ID).unwrap();
744    /// assert_eq!(user.external_id, EXTERNAL_ID);
745    ///
746    /// // Registering an already existing user
747    /// let error = game.user_register(EXTERNAL_ID).unwrap_err();
748    /// assert!(error.is_already_exists());
749    /// if let Some(resource_error) = error.as_resource_error() {
750    ///     assert!(resource_error.is_user_already_exists());
751    ///     assert_eq!(resource_error.get_external_id(), Some(EXTERNAL_ID));
752    /// } else {
753    ///     panic!("{:?}", error);
754    /// }
755    /// ```
756    fn user_register(&self, external_id: i64) -> GameResult<User> {
757        match self.user_repository().find_by_external_id(external_id)? {
758            Some(_) => Err(GameResourceError::user_already_exists(external_id).into()),
759            None => Ok(self.user_service().create_and_save_user(external_id)?),
760        }
761    }
762
763    /// Save a [User].
764    ///
765    /// # Arguments
766    ///
767    /// * `user`: The [User] to save
768    ///
769    /// # Returns
770    /// Result<[User], [errors::GameError]>
771    /// - The updated [User] entity, if saving succeeded
772    /// - An error, if saving failed (database errors, or the user doesn't exist)
773    ///
774    /// # Examples
775    ///
776    /// ```
777    /// use std::env;
778    /// use fish_lib::game::prelude::*;
779    /// use fish_lib::game::service_provider::ServiceProviderInterface;    ///
780    ///
781    /// use fish_lib::models::user::User;
782    ///
783    /// const DUMMY_USER_ID: i64 = 64;
784    /// const USER_EXTERNAL_ID: i64 = 1337;
785    /// const USER_CREDITS: i64 = 293;
786    ///
787    /// // Create game and clear database for a blank test state
788    /// let database_url = env::var("DATABASE_URL").expect("DATABASE_URL must be set");
789    /// let game = Game::new(&database_url, None).unwrap();
790    /// game.database().write().unwrap().clear().unwrap();
791    ///
792    /// // Create a new user and update their credits
793    /// let mut user = game.user_register(USER_EXTERNAL_ID).unwrap();
794    /// user.credits = USER_CREDITS;
795    ///
796    /// // Save the user and check if the credits were updated properly
797    /// let updated_user = game.user_save(user).unwrap();
798    /// assert_eq!(updated_user.credits, USER_CREDITS);
799    ///
800    /// // Find user again and check if credits are updated properly
801    /// let found_user = game.user_find(USER_EXTERNAL_ID).unwrap();
802    /// assert_eq!(found_user.credits, USER_CREDITS);
803    ///
804    /// // Try to save a non-existent user
805    /// let dummy_user = User {
806    ///     id: DUMMY_USER_ID,
807    ///     ..Default::default()
808    /// };
809    ///
810    /// let error_not_found = game.user_save(dummy_user).unwrap_err();
811    /// assert!(error_not_found.is_not_found())
812    /// ```
813    fn user_save(&self, user: User) -> GameResult<User> {
814        Ok(self.user_repository().save(user)?)
815    }
816
817    /// Unlocks a given location for a given user
818    ///
819    /// # Arguments
820    ///
821    /// * `user`: The [User] to unlock a location for
822    /// * `location`: The location to unlock for the given [User]
823    ///
824    /// # Returns
825    /// Result<[UserLocationUnlock], [errors::GameError]>
826    /// - Information about the location unlock, if it succeeded
827    /// - An error, if:
828    ///     - unlock conditions were not met
829    ///     - the location was already unlocked
830    ///     - the location does not exist
831    ///     - database operations fail
832    ///
833    /// # Examples
834    ///
835    /// ```
836    /// use fish_lib::game::prelude::*;
837    /// use std::collections::HashMap;
838    /// use std::env;
839    /// use fish_lib::config::{Config, ConfigBuilderInterface};
840    /// use fish_lib::data::location_data::LocationData;
841    /// use fish_lib::game::prelude::*;
842    /// use fish_lib::game::service_provider::ServiceProviderInterface;
843    ///
844    /// const EXTERNAL_ID: i64 = 1337;
845    /// const LOCATION_ID: i32 = 13;
846    /// const LOCATION_NAME: &str = "Island";
847    ///
848    /// // Define some location data
849    /// let location_data = LocationData {
850    ///     name: LOCATION_NAME.to_string(),
851    ///     ..Default::default()
852    /// };
853    /// let location_data2 = LocationData {
854    ///     required_locations_unlocked: vec![LOCATION_ID + 2],
855    ///     ..Default::default()
856    /// };
857    /// let location_data3 = LocationData::default();
858    ///
859    /// let location_data_map = HashMap::from([
860    ///     (LOCATION_ID, location_data),
861    ///     (LOCATION_ID + 1, location_data2),
862    ///     (LOCATION_ID + 2, location_data3)
863    /// ]);
864    ///
865    /// // Add the location data to the config
866    /// let config = Config::builder().locations(location_data_map).build().unwrap();
867    ///
868    /// // Create game and clear database for a blank test state
869    /// let database_url = env::var("DATABASE_URL").expect("DATABASE_URL must be set");
870    /// let game = Game::new(&database_url, Some(config)).unwrap();
871    /// game.database().write().unwrap().clear().unwrap();
872    ///
873    /// // Registering a new user
874    /// let user = game.user_register(EXTERNAL_ID).unwrap();
875    ///
876    /// // Unlock a location for the user
877    /// let island = game.location_find(LOCATION_ID).unwrap();
878    /// let location_unlock = game.user_unlock_location(&user, island).unwrap();
879    ///
880    /// // Find unlocked locations
881    /// let unlocked_locations = game.user_get_unlocked_locations(&user).unwrap();
882    /// assert_eq!(unlocked_locations.len(), 1);
883    /// assert_eq!(unlocked_locations[0], location_unlock);
884    ///
885    /// // Unmet requirements
886    /// let location2 = game.location_find(LOCATION_ID + 1).unwrap();
887    /// let unlock_error = game.user_unlock_location(&user, location2).unwrap_err();
888    ///
889    /// assert!(unlock_error.is_unmet_requirements());
890    /// if let Some(resource_error) = unlock_error.as_resource_error() {
891    ///     assert!(resource_error.is_unmet_location_unlock_requirements());
892    ///     assert_eq!(resource_error.get_location_id(), Some(LOCATION_ID + 1));
893    /// } else {
894    ///     panic!("{:?}", unlock_error);
895    /// }
896    /// ```
897    fn user_unlock_location(
898        &self,
899        user: &User,
900        location: Arc<LocationData>,
901    ) -> GameResult<UserLocationUnlock> {
902        let user_location = self
903            .user_service()
904            .unlock_location(user, location.clone())?;
905        let user_location_unlock =
906            UserLocationUnlock::from_user_location(user_location, &|location_id| {
907                self.location_find(location_id).ok()
908            });
909        match user_location_unlock {
910            Some(location_unlock) => Ok(location_unlock),
911            None => Err(GameResourceError::location_not_found(location.id).into()),
912        }
913    }
914}
915
916impl ServiceProviderInterface for Game {
917    fn config(&self) -> Arc<dyn ConfigInterface> {
918        self.service_provider.config()
919    }
920
921    fn database(&self) -> Arc<RwLock<dyn DatabaseInterface>> {
922        self.service_provider.database()
923    }
924
925    fn fishing_history_entry_repository(&self) -> Arc<dyn FishingHistoryEntryRepositoryInterface> {
926        self.service_provider.fishing_history_entry_repository()
927    }
928
929    fn item_repository(&self) -> Arc<dyn ItemRepositoryInterface> {
930        self.service_provider.item_repository()
931    }
932
933    fn pond_repository(&self) -> Arc<dyn PondRepositoryInterface> {
934        self.service_provider.pond_repository()
935    }
936
937    fn specimen_repository(&self) -> Arc<dyn SpecimenRepositoryInterface> {
938        self.service_provider.specimen_repository()
939    }
940
941    fn user_repository(&self) -> Arc<dyn UserRepositoryInterface> {
942        self.service_provider.user_repository()
943    }
944
945    fn encounter_service(&self) -> Arc<dyn EncounterServiceInterface> {
946        self.service_provider.encounter_service()
947    }
948
949    fn fishing_history_service(&self) -> Arc<dyn FishingHistoryServiceInterface> {
950        self.service_provider.fishing_history_service()
951    }
952
953    fn item_service(&self) -> Arc<dyn ItemServiceInterface> {
954        self.service_provider.item_service()
955    }
956
957    fn location_service(&self) -> Arc<dyn LocationServiceInterface> {
958        self.service_provider.location_service()
959    }
960
961    fn pond_service(&self) -> Arc<dyn PondServiceInterface> {
962        self.service_provider.pond_service()
963    }
964
965    fn species_service(&self) -> Arc<dyn SpeciesServiceInterface> {
966        self.service_provider.species_service()
967    }
968
969    fn specimen_service(&self) -> Arc<dyn SpecimenServiceInterface> {
970        self.service_provider.specimen_service()
971    }
972
973    fn user_service(&self) -> Arc<dyn UserServiceInterface> {
974        self.service_provider.user_service()
975    }
976
977    fn weather_service(&self) -> Arc<dyn WeatherServiceInterface> {
978        self.service_provider.weather_service()
979    }
980}