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}