diff --git a/assets/robocraft/config.json b/assets/robocraft/config.json index ee1da56..86d5484 100644 --- a/assets/robocraft/config.json +++ b/assets/robocraft/config.json @@ -14542,6 +14542,7 @@ "uri": "sqlite:../data/robocraft/rc_archive.db?mode=rw" } }, + "shop": {}, "settings": { "banners": [ { diff --git a/rc_services_room/src/data/item_shop_bundle.rs b/rc_core/src/data/item_shop_bundle.rs similarity index 85% rename from rc_services_room/src/data/item_shop_bundle.rs rename to rc_core/src/data/item_shop_bundle.rs index a3d5816..520a3fa 100644 --- a/rc_services_room/src/data/item_shop_bundle.rs +++ b/rc_core/src/data/item_shop_bundle.rs @@ -30,21 +30,21 @@ impl ItemShopBundle { fn dump(&self, writer: &mut dyn Write) -> std::io::Result { let sku_bytes = self.sku.as_bytes(); - let mut total_len = writer.write(&oj_rc_core::data::encode_7_bit_i32(sku_bytes.len() as i32))?; + let mut total_len = writer.write(&crate::data::encode_7_bit_i32(sku_bytes.len() as i32))?; total_len += writer.write(sku_bytes)?; let bundle_name_key_bytes = self.bundle_name_key.as_bytes(); - total_len += writer.write(&oj_rc_core::data::encode_7_bit_i32(bundle_name_key_bytes.len() as i32))?; + total_len += writer.write(&crate::data::encode_7_bit_i32(bundle_name_key_bytes.len() as i32))?; total_len += writer.write(bundle_name_key_bytes)?; let sprite_bytes = self.sprite.as_bytes(); - total_len += writer.write(&oj_rc_core::data::encode_7_bit_i32(sprite_bytes.len() as i32))?; + total_len += writer.write(&crate::data::encode_7_bit_i32(sprite_bytes.len() as i32))?; total_len += writer.write(sprite_bytes)?; total_len += writer.write(&[self.is_sprite_full_size as u8])?; let currency_bytes = self.currency.as_str().as_bytes(); - total_len += writer.write(&oj_rc_core::data::encode_7_bit_i32(currency_bytes.len() as i32))?; + total_len += writer.write(&crate::data::encode_7_bit_i32(currency_bytes.len() as i32))?; total_len += writer.write(currency_bytes)?; total_len += writer.write(&self.price.to_le_bytes())?; @@ -64,7 +64,7 @@ impl ItemShopBundle { Ok(total_len) } - pub fn as_transmissible_vec(items: Vec) -> Typed { + pub fn as_transmissible_vec(items: Vec) -> Typed { let mut buf = Vec::new(); let mut writer = std::io::Cursor::new(&mut buf); writer.write_all(&(items.len() as i32).to_le_bytes()).unwrap(); diff --git a/rc_core/src/data/mod.rs b/rc_core/src/data/mod.rs index 7b62dda..bfc28fa 100644 --- a/rc_core/src/data/mod.rs +++ b/rc_core/src/data/mod.rs @@ -17,6 +17,7 @@ pub mod robot_data; pub mod lobby; pub mod battle_arena_config; pub mod game_result; +pub mod item_shop_bundle; pub mod error_codes; diff --git a/rc_core/src/persist/combat.rs b/rc_core/src/persist/combat.rs index d409d0b..585d9c5 100644 --- a/rc_core/src/persist/combat.rs +++ b/rc_core/src/persist/combat.rs @@ -336,7 +336,7 @@ fn default_game_modes() -> GameModes { respawn_heal_duration: 10.0, respawn_full_heal_duration: 0.5, kill_limit: 0, - game_time_m: 20, + game_time_m: 2, }, elimination: GameMode { respawn_heal_duration: 10.0, @@ -461,7 +461,7 @@ fn default_rotation() -> GameEventSequence { GameEventSequence { strategy: GameRotationStrategy::Sequence, modes: vec![ - /*GameEvents { + GameEvents { singleplayer: GameEvent { map: GameMap::Earth1, visibility: GameVisibility::Good, @@ -490,8 +490,8 @@ fn default_rotation() -> GameEventSequence { auto_heal: true, }, duration_s: 30, // 30 seconds - },*/ - GameEvents { + }, + /*GameEvents { singleplayer: GameEvent { map: GameMap::Neptune1, visibility: GameVisibility::Good, @@ -610,7 +610,7 @@ fn default_rotation() -> GameEventSequence { auto_heal: true, }, duration_s: 5*60, - }, + },*/ ] } } diff --git a/rc_core/src/persist/config/cubes_json.rs b/rc_core/src/persist/config/cubes_json.rs index 4b2fc0f..702b335 100644 --- a/rc_core/src/persist/config/cubes_json.rs +++ b/rc_core/src/persist/config/cubes_json.rs @@ -8,7 +8,7 @@ use polariton::serdes::TypePrefix; use crate::persist::config::SelfValidator; -use super::super::{MovementCategoryData, MovementData, Cube, ItemCategory, ItemTier, BattleConfig, Settings, ChatConfig, FactoryConfig}; +use super::super::{MovementCategoryData, MovementData, Cube, ItemCategory, ItemTier, BattleConfig, Settings, ChatConfig, FactoryConfig, ItemShopConfig}; const CUBE_CONFIG_FILENAME: &str = "config.json"; @@ -20,6 +20,7 @@ pub struct CubeConfig { battle: BattleConfig, chat: ChatConfig, factory: FactoryConfig, + shop: ItemShopConfig, settings: Settings, } @@ -503,4 +504,8 @@ impl super::ConfigProvider for CubeConfig { fn tdm_settings(&self) -> super::TeamDeathMatchSettings { self.battle.multiplayer.team_death_match.clone().into() } + + fn shop_entries(&self) -> super::ShopEntriesResolver { + super::ShopEntriesResolver::new(self.shop.items.clone()) + } } diff --git a/rc_core/src/persist/config/mod.rs b/rc_core/src/persist/config/mod.rs index 1ea313c..6a7a2ff 100644 --- a/rc_core/src/persist/config/mod.rs +++ b/rc_core/src/persist/config/mod.rs @@ -2,7 +2,7 @@ mod cubes_json; pub use cubes_json::CubeConfig; mod traits; -pub use traits::{ConfigProvider, DevMessageProvider, ServerConfig, GarageUpgrades, GarageUpgradeIncrement, ChatSystemConfig, GameEventSequence, GameEvents, GameRotationStrategy, GameEvent, GameMap, GameVisibility, GameType, SingleplayerConfig, VehicleInfo, VehicleDescriptor, QueueChangeMode, Point, Sphere, MapConfig, LinksConfig, FakePlayer, ClientEmulator, EnergyConfig, BattleArenaResolver, PitSettings, PitWinCondition, TeamDeathMatchSettings}; +pub use traits::{ConfigProvider, DevMessageProvider, ServerConfig, GarageUpgrades, GarageUpgradeIncrement, ChatSystemConfig, GameEventSequence, GameEvents, GameRotationStrategy, GameEvent, GameMap, GameVisibility, GameType, SingleplayerConfig, VehicleInfo, VehicleDescriptor, QueueChangeMode, Point, Sphere, MapConfig, LinksConfig, FakePlayer, ClientEmulator, EnergyConfig, BattleArenaResolver, PitSettings, PitWinCondition, TeamDeathMatchSettings, ShopEntriesResolver, ShopAction, ShopGain}; mod validation; pub use validation::{SelfValidator, ValidationInfo, ValidationMessage}; diff --git a/rc_core/src/persist/config/traits.rs b/rc_core/src/persist/config/traits.rs index 724af13..4fb88e0 100644 --- a/rc_core/src/persist/config/traits.rs +++ b/rc_core/src/persist/config/traits.rs @@ -36,6 +36,7 @@ pub trait ConfigProvider { fn ba_settings(&self) -> BattleArenaResolver; fn pit_settings(&self) -> PitSettings; fn tdm_settings(&self) -> TeamDeathMatchSettings; + fn shop_entries(&self) -> ShopEntriesResolver; } pub struct DevMessageProvider { @@ -428,3 +429,60 @@ pub struct TeamDeathMatchSettings { pub respawn_time_seconds: u64, pub self_destruct_is_kill: bool, } + +pub struct ShopEntriesResolver { + items: Vec, +} + +pub(super) fn item_shop_sku(i: usize) -> String { + format!("item-shop-bundle-{}", i) +} + +impl ShopEntriesResolver { + pub async fn resolve_entries(&self, user: &dyn crate::persist::user::User) -> Typed { + let unlocked_cubes = user.unlocked_parts().await; + crate::data::item_shop_bundle::ItemShopBundle::as_transmissible_vec( + self.items.iter().enumerate() + .map(|(i, entry)| entry.as_data(item_shop_sku(i), &unlocked_cubes)) + .collect() + ) + } + + // sku -> purchase details + pub async fn resolve_transactions(&self) -> std::collections::HashMap { + let mut map = std::collections::HashMap::with_capacity(self.items.len()); + let now = chrono::Utc::now().timestamp(); + for (i, entry) in self.items.iter().enumerate() { + let sku = super::traits::item_shop_sku(i); + let actual_price = if now > entry.discount_until { entry.discount_price } else { entry.price }; + map.insert(sku, super::ShopAction { + cost_free: if matches!(entry.currency, crate::persist::item_shop::Currency::Robits) { actual_price } else { 0 }, + cost_paid: if matches!(entry.currency, crate::persist::item_shop::Currency::CosmeticCredits) { actual_price } else { 0 }, + gives: entry.gives.clone().into_iter().map(|x| x.into()).collect(), + }); + } + map + } + + pub(super) fn new(items: Vec) -> Self { + Self { + items, + } + } +} + +#[derive(Debug)] +pub struct ShopAction { + pub cost_free: i32, + pub cost_paid: i32, + pub gives: Vec, +} + +#[derive(Debug)] +pub enum ShopGain { + Cube(u32), + Experience(i64), + FreeCurrency(i64), + PaidCurrency(i64), + TechPoints(i32) +} diff --git a/rc_core/src/persist/item_shop.rs b/rc_core/src/persist/item_shop.rs new file mode 100644 index 0000000..9f791a5 --- /dev/null +++ b/rc_core/src/persist/item_shop.rs @@ -0,0 +1,328 @@ +use serde::{Serialize, Deserialize}; + +#[derive(Serialize, Deserialize, Clone, Debug)] +pub struct ItemShopConfig { + #[serde(default = "default_items")] + pub items: Vec, +} + +impl super::config::SelfValidator for ItemShopConfig { + type Context = crate::ConfigImpl; + fn validate(&self, info: &mut super::config::ValidationInfo, _ctx: &Self::Context) -> bool { + let daily_count = self.items.iter().filter(|x| matches!(x.recurrence, Recurrence::Daily)).count(); + if daily_count < 6 { + info.warn(crate::persist::config::ValidationMessage { + path: vec!["items".to_owned()], + message: "Less than 6 daily items in shop so there will be blank slots".to_owned(), + }); + } else if daily_count > 6 { + info.warn(crate::persist::config::ValidationMessage { + path: vec!["items".to_owned()], + message: "More than 6 daily items in shop so some will never be shown".to_owned(), + }); + } + let weekly_count = self.items.iter().filter(|x| matches!(x.recurrence, Recurrence::Weekly)).count(); + if weekly_count < 3 { + info.warn(crate::persist::config::ValidationMessage { + path: vec!["items".to_owned()], + message: "Less than 3 weekly items in shop so there will be blank slots".to_owned(), + }); + } else if weekly_count > 3 { + info.warn(crate::persist::config::ValidationMessage { + path: vec!["items".to_owned()], + message: "More than 3 weekly items in shop so some will never be shown".to_owned(), + }); + } + // TODO + true + } +} + +#[derive(Serialize, Deserialize, Clone, Debug)] +pub struct ItemBundle { + //pub sku: String, + #[serde(alias = "localized_key")] + pub localised_key: String, + pub sprite: String, + pub is_sprite_full_size: bool, + pub category: ShopCategory, + pub currency: Currency, + pub price: i32, + #[serde(default)] + pub discount_until: i64, // seconds since unix epoch + #[serde(default)] + pub discount_price: i32, + pub recurrence: Recurrence, + //pub owns_required_cube: bool, + pub is_limited_edition: bool, + #[serde(default)] + pub required_cubes: Vec, + pub gives: Vec, +} + +impl ItemBundle { + pub fn as_data(&self, sku: String, unlocked_cubes: &[u32]) -> crate::data::item_shop_bundle::ItemShopBundle { + let mut contains_all = true; + for req in self.required_cubes.iter() { + contains_all &= unlocked_cubes.contains(req); + } + crate::data::item_shop_bundle::ItemShopBundle { + sku, + bundle_name_key: self.localised_key.clone(), + sprite: self.sprite.clone(), + is_sprite_full_size: self.is_sprite_full_size, + category: self.category.into(), + currency: self.currency.into(), + price: self.price, + discount_time: self.discount_until, + discount_price: self.discount_price, + recurrence: self.recurrence.into(), + owns_required_cube: contains_all, + is_limited_edition: self.is_limited_edition, + } + } +} + +#[derive(Serialize, Deserialize, Clone, Copy, Debug)] +pub enum ShopCategory { + Cube, + GarageBaySkin, + Bundle, + DeathEffect, + SpawnEffect, + Emotigram, +} + +impl std::convert::From for crate::data::item_shop_bundle::ItemShopCategory { + fn from(value: ShopCategory) -> Self { + match value { + ShopCategory::Cube => Self::Cube, + ShopCategory::GarageBaySkin => Self::GarageBaySkin, + ShopCategory::Bundle => Self::Bundle, + ShopCategory::DeathEffect => Self::DeathEffect, + ShopCategory::SpawnEffect => Self::SpawnEffect, + ShopCategory::Emotigram => Self::Emotigram, + } + } +} + +#[derive(Serialize, Deserialize, Clone, Copy, Debug)] +pub enum Recurrence { + Daily, + Weekly, +} + +impl std::convert::From for crate::data::item_shop_bundle::ItemShopRecurrence { + fn from(value: Recurrence) -> Self { + match value { + Recurrence::Daily => Self::Daily, + Recurrence::Weekly => Self::Weekly, + } + } +} + +#[derive(Serialize, Deserialize, Clone, Copy, Debug)] +pub enum Currency { + Robits, + CosmeticCredits, +} + +impl std::convert::From for crate::data::item_shop_bundle::CurrencyType { + fn from(value: Currency) -> Self { + match value { + Currency::Robits => Self::Robits, + Currency::CosmeticCredits => Self::CosmeticCredits, + } + } +} + +#[derive(Serialize, Deserialize, Clone, Debug)] +#[serde(untagged)] +pub enum ItemPurchase { + Cube { + item_id: u32, + }, + Experience { + #[serde(alias = "experience", alias = "exp")] + xp: i64, + }, + FreeCurrency { + #[serde(alias = "robits")] + free_currency: i64, + }, + PaidCurrency { + #[serde(alias = "cc", alias = "cosmetic_credits")] + paid_currency: i64, + }, + TechPoints { + #[serde(alias = "tp")] + tech_points: i64, + } +} + +impl std::convert::From for crate::persist::config::ShopGain { + fn from(value: ItemPurchase) -> Self { + match value { + ItemPurchase::Cube { item_id } => crate::persist::config::ShopGain::Cube(item_id), + ItemPurchase::Experience { xp } => crate::persist::config::ShopGain::Experience(xp), + ItemPurchase::FreeCurrency { free_currency } => crate::persist::config::ShopGain::FreeCurrency(free_currency), + ItemPurchase::PaidCurrency { paid_currency } => crate::persist::config::ShopGain::PaidCurrency(paid_currency), + ItemPurchase::TechPoints { tech_points } => crate::persist::config::ShopGain::PaidCurrency(tech_points), + } + } +} + +pub fn default_items() -> Vec { + vec![ + // weekly (top row of 3) + ItemBundle { + //sku: "buy cc 100".to_owned(), + localised_key: "strRealMoneyStoreName_CosmeticCredits1".to_owned(), + sprite: "ItemShop_CosmeticCredits".to_owned(), + is_sprite_full_size: false, + category: ShopCategory::Bundle, + currency: Currency::Robits, + price: 100_000, + discount_until: 0, + discount_price: 100_000, + recurrence: Recurrence::Weekly, + //owns_required_cube: true, + is_limited_edition: false, + required_cubes: vec![], + gives: vec![ + ItemPurchase::PaidCurrency { paid_currency: 100 }, + ], + }, + ItemBundle { + //sku: "buy robopass 1 1".to_owned(), + localised_key: "strRealMoneyStoreName_RoboPass".to_owned(), + sprite: "Store_RoboPass_Season2".to_owned(), + is_sprite_full_size: true, + category: ShopCategory::Bundle, + currency: Currency::CosmeticCredits, + price: 10_000_000, + discount_until: 0, + discount_price: 10_000_000, + recurrence: Recurrence::Weekly, + //owns_required_cube: false, + is_limited_edition: true, + required_cubes: vec![], + gives: vec![], + }, + ItemBundle { + //sku: "buy robit 100000".to_owned(), + localised_key: "strRealMoneyStoreName_RobitsBundle2".to_owned(), + sprite: "ItemShop_Robits".to_owned(), + is_sprite_full_size: false, + category: ShopCategory::Bundle, + currency: Currency::CosmeticCredits, + price: 100, + discount_until: 0, + discount_price: 100, + recurrence: Recurrence::Weekly, + //owns_required_cube: true, + is_limited_edition: false, + required_cubes: vec![], + gives: vec![ + ItemPurchase::FreeCurrency { free_currency: 100_000 }, + ], + }, + // daily (lower row or 6) + ItemBundle { + //sku: "buy robopass 1 1".to_owned(), + localised_key: "strRoboPassSeason02".to_owned(), + sprite: "Store_RoboPass".to_owned(), + is_sprite_full_size: true, + category: ShopCategory::Bundle, + currency: Currency::Robits, + price: 10_000, + discount_until: (chrono::Utc::now() + std::time::Duration::from_secs(24*60*60)).timestamp(), + discount_price: 1_000, + recurrence: Recurrence::Daily, + //owns_required_cube: false, + is_limited_edition: false, + required_cubes: vec![], + gives: vec![], + }, + ItemBundle { + //sku: "buy robopass 1 2".to_owned(), + localised_key: "strRoboPassSeason02".to_owned(), + sprite: "Store_RoboPass".to_owned(), + is_sprite_full_size: true, + category: ShopCategory::Cube, + currency: Currency::Robits, + price: 10_000, + discount_until: (chrono::Utc::now() + std::time::Duration::from_secs(24*60*60)).timestamp(), + discount_price: 2_000, + recurrence: Recurrence::Daily, + //owns_required_cube: false, + is_limited_edition: false, + required_cubes: vec![], + gives: vec![], + }, + ItemBundle { + //sku: "buy robopass 1 3".to_owned(), + localised_key: "strRoboPassSeason02".to_owned(), + sprite: "Store_RoboPass".to_owned(), + is_sprite_full_size: true, + category: ShopCategory::Cube, + currency: Currency::Robits, + price: 10_000, + discount_until: (chrono::Utc::now() + std::time::Duration::from_secs(24*60*60)).timestamp(), + discount_price: 3_000, + recurrence: Recurrence::Daily, + //owns_required_cube: false, + is_limited_edition: false, + required_cubes: vec![], + gives: vec![], + }, + ItemBundle { + //sku: "buy robopass 1 4".to_owned(), + localised_key: "strRoboPassSeason02".to_owned(), + sprite: "Store_RoboPass".to_owned(), + is_sprite_full_size: true, + category: ShopCategory::Cube, + currency: Currency::Robits, + price: 10_000, + discount_until: (chrono::Utc::now() + std::time::Duration::from_secs(24*60*60)).timestamp(), + discount_price: 4_000, + recurrence: Recurrence::Daily, + //owns_required_cube: false, + is_limited_edition: false, + required_cubes: vec![], + gives: vec![], + }, + ItemBundle { + //sku: "buy robopass 1 5".to_owned(), + localised_key: "strRoboPassSeason02".to_owned(), + sprite: "Store_RoboPass".to_owned(), + is_sprite_full_size: true, + category: ShopCategory::Cube, + currency: Currency::Robits, + price: 10_000, + discount_until: (chrono::Utc::now() + std::time::Duration::from_secs(24*60*60)).timestamp(), + discount_price: 5_000, + recurrence: Recurrence::Daily, + //owns_required_cube: false, + is_limited_edition: false, + required_cubes: vec![], + gives: vec![], + }, + ItemBundle { + //sku: "buy robopass 1 6".to_owned(), + localised_key: "strRoboPassSeason02".to_owned(), + sprite: "Store_RoboPass".to_owned(), + is_sprite_full_size: true, + category: ShopCategory::Cube, + currency: Currency::Robits, + price: 10_000, + discount_until: (chrono::Utc::now() + std::time::Duration::from_secs(24*60*60)).timestamp(), + discount_price: 6_000, + recurrence: Recurrence::Daily, + //owns_required_cube: false, + is_limited_edition: false, + required_cubes: vec![], + gives: vec![], + }, + ] +} diff --git a/rc_core/src/persist/mod.rs b/rc_core/src/persist/mod.rs index a031648..a7e9b0c 100644 --- a/rc_core/src/persist/mod.rs +++ b/rc_core/src/persist/mod.rs @@ -41,6 +41,9 @@ pub use multiplayer::{MultiplayerConfig, NetworkConf}; mod maps; pub use maps::{MapsConfig, MapConfig}; +mod item_shop; +pub use item_shop::{ItemShopConfig, ItemBundle}; + const VALID_ROBOT: &[u8] = &[64, 0, 0, diff --git a/rc_core/src/persist/user/account_json.rs b/rc_core/src/persist/user/account_json.rs index 560fdb6..f990a95 100644 --- a/rc_core/src/persist/user/account_json.rs +++ b/rc_core/src/persist/user/account_json.rs @@ -1,4 +1,5 @@ use argon2::PasswordVerifier; +use oj_rc_database::sea_orm::IntoActiveModel; use sha2::Digest; use crate::persist::config::ConfigProvider; @@ -666,6 +667,19 @@ impl UserData { } ).await? }, + super::CurrencyOp::AddSub(to_addsub) => { + self.db.update_user_aux_by_user_id_and_descriptor_custom( + self.account.id, + desc.clone(), + move |model| { + use oj_rc_database::sea_orm::IntoActiveModel; + let new_currency = (model.data.parse::().unwrap_or_default() as i64) + to_addsub; + let mut am = model.to_owned().into_active_model(); + am.data = oj_rc_database::sea_orm::ActiveValue::Set(new_currency.clamp(0, i64::MAX).to_string()); + Some(am) + } + ).await? + }, }; let num: u64 = if let Some(model) = model_opt { model.data.parse().unwrap_or_default() @@ -708,6 +722,58 @@ impl super::User for UserData { } } + async fn unlock_parts(&self, parts: &[u32]) -> Result<(), polariton_server::operations::SimpleOpError> { + let parts_row = self.db.user_aux_by_user_id_and_descriptor(self.account.id, oj_rc_database::schema::user_aux::Descriptor::UnlockedParts).await + .map_err(|e| { + log::error!("Failed to retrieve UnlockedParts to unlock parts for user {}: {}", self.account.id, e); + polariton_server::operations::SimpleOpError::with_message( + crate::data::error_codes::WebServicesError::DatabaseError as i16, + format!("Failed to retrieve UnlockedParts to unlock parts: {}", e), + ) + })? + .ok_or_else(|| { + log::error!("Failed to find UnlockedParts to unlock parts for user {}", self.account.id); + polariton_server::operations::SimpleOpError::with_message( + crate::data::error_codes::WebServicesError::DatabaseError as i16, + "Failed to find UnlockedParts to unlock parts".to_owned(), + ) + })?; + let mut unlocked_parts = serde_json::from_str::(&parts_row.data) + .map_err(|e| { + log::error!("Failed to deserialize UnlockedParts to unlock parts for user {}: {}", self.account.id, e); + polariton_server::operations::SimpleOpError::with_message( + crate::data::error_codes::WebServicesError::DatabaseError as i16, + format!("Failed to deserialize UnlockedParts to unlock parts: {}", e), + ) + })?; + for new_part in parts { + unlocked_parts.unlocked.push(*new_part); + } + let mut parts_row = parts_row.into_active_model(); + let parts_json = serde_json::to_string(&unlocked_parts) + .map_err(|e| { + log::error!("Failed to serialize UnlockedParts to unlock parts for user {}: {}", self.account.id, e); + polariton_server::operations::SimpleOpError::with_message( + crate::data::error_codes::WebServicesError::DatabaseError as i16, + format!("Failed to serialize UnlockedParts to unlock parts: {}", e), + ) + })?; + parts_row.data = oj_rc_database::sea_orm::ActiveValue::Set(parts_json); + self.db.update_user_aux_by_user_id_and_descriptor( + parts_row, + self.account.id, + oj_rc_database::schema::user_aux::Descriptor::UnlockedParts + ).await + .map_err(|e| { + log::error!("Failed to update UnlockedParts to unlock parts for user {}: {}", self.account.id, e); + polariton_server::operations::SimpleOpError::with_message( + crate::data::error_codes::WebServicesError::DatabaseError as i16, + format!("Failed to update UnlockedParts to unlock parts: {}", e), + ) + })?; + Ok(()) + } + async fn selected_garage(&self) -> (String, u32) { match self.db.garage_selected(self.account.id).await { Ok(Some(selected)) => (super::i64_as_uuid_str(selected.uuid), selected.slot as u32), diff --git a/rc_core/src/persist/user/mod.rs b/rc_core/src/persist/user/mod.rs index f444415..2755349 100644 --- a/rc_core/src/persist/user/mod.rs +++ b/rc_core/src/persist/user/mod.rs @@ -8,7 +8,7 @@ mod initial_data; pub use initial_data::{setup_new_user, register_new_user}; mod inventory; -pub use inventory::UnlockedParts; +pub use inventory::{UnlockedParts, UnlockOverride}; mod traits; pub use traits::{UserProvider, User, UserToken, UserSlots, UserSlotData, VehicleData, UserInfo, UserLoginInfo, ExtraUserInfo, UserAuthenticator, NewSlotData, UserId, RegistrationInfo, VehicleUploadData, ChatUser, AvatarInfo, GetAvatarInfo, ControlData, ControlType, CustomisationData, GetCustomisationData, SetSanction, SanctionType, LobbyUser, GameDescriptor, PlayerLobbyDescriptor, MultiplayerUser, PlayerScore, MultiplayerError, MultiplayerErrorCode, PlayerDescriptor, GameEventSetter, CurrentGameEvent, AuthError, IntercomUser, FakePlayers, ResolvedVehicle, CommonUser, IntercomListener, UserRole, SocialUser, CurrencyType, CurrencyOp, MatchRewards, SingleplayerUser}; diff --git a/rc_core/src/persist/user/traits.rs b/rc_core/src/persist/user/traits.rs index 4914b8c..18aa45e 100644 --- a/rc_core/src/persist/user/traits.rs +++ b/rc_core/src/persist/user/traits.rs @@ -63,6 +63,7 @@ pub trait UserAuthenticator { #[async_trait::async_trait] pub trait User: ChatUser + SocialUser + LobbyUser + MultiplayerUser + SingleplayerUser + IntercomUser + CommonUser { async fn unlocked_parts(&self) -> Vec; + async fn unlock_parts(&self, parts: &[u32]) -> Result<(), polariton_server::operations::SimpleOpError>; async fn selected_garage(&self) -> (String, u32); async fn select_garage(&self, slot: i32) -> Result<(), i16>; async fn all_slots(&self) -> UserSlots; @@ -417,6 +418,7 @@ pub enum CurrencyOp { Get, Add(u64), Sub(u64), + AddSub(i64), } #[async_trait::async_trait] diff --git a/rc_services_room/src/data/mod.rs b/rc_services_room/src/data/mod.rs index 96c4a3d..b8dc1ad 100644 --- a/rc_services_room/src/data/mod.rs +++ b/rc_services_room/src/data/mod.rs @@ -15,7 +15,7 @@ pub mod customisation_info; pub use oj_rc_core::data::garage_bay; pub mod custom_games; //pub use oj_rc_core::data::tech_tree; -pub mod item_shop_bundle; +//pub use oj_rc_core::data::item_shop_bundle; pub mod player_robopass_season; //pub use oj_rc_core::data::weapon_upgrade; pub mod player_rank; diff --git a/rc_services_room/src/operations/item_shop_bundles.rs b/rc_services_room/src/operations/item_shop_bundles.rs index 808e5b8..a1fc48a 100644 --- a/rc_services_room/src/operations/item_shop_bundles.rs +++ b/rc_services_room/src/operations/item_shop_bundles.rs @@ -1,144 +1,31 @@ -use polariton_server::operations::SimpleFunc; +use polariton_server::operations::{SimpleOpError, SimpleOperation, SimpleOpImpl}; use polariton::operation::ParameterTable; -use crate::data::item_shop_bundle::*; +const CODE: u8 = 188; const PARAM_KEY: u8 = 65; -pub(super) fn item_bundle_provider() -> SimpleFunc<188, crate::UserTy, impl (Fn(ParameterTable, &crate::UserTy) -> Result) + Sync + Sync> { - SimpleFunc::new(|params, _| { - let mut params = params.to_dict(); - params.insert(PARAM_KEY, ItemShopBundle::as_transmissible_vec(vec![ - // weekly (top row of 3) - ItemShopBundle { - sku: "buy cc 100".to_owned(), - bundle_name_key: "strRealMoneyStoreName_CosmeticCredits1".to_owned(), - sprite: "ItemShop_CosmeticCredits".to_owned(), - is_sprite_full_size: false, - category: ItemShopCategory::Bundle, - currency: CurrencyType::Robits, - price: 100_000, - discount_time: 0, - discount_price: 100_000, - recurrence: ItemShopRecurrence::Weekly, - owns_required_cube: true, - is_limited_edition: false, - }, - ItemShopBundle { - sku: "buy robopass 1 1".to_owned(), - bundle_name_key: "strRealMoneyStoreName_RoboPass".to_owned(), - sprite: "Store_RoboPass_Season2".to_owned(), - is_sprite_full_size: true, - category: ItemShopCategory::Bundle, - currency: CurrencyType::CosmeticCredits, - price: 10_000_000, - discount_time: 0, - discount_price: 10_000_000, - recurrence: ItemShopRecurrence::Weekly, - owns_required_cube: false, - is_limited_edition: true, - }, - ItemShopBundle { - sku: "buy robit 100000".to_owned(), - bundle_name_key: "strRealMoneyStoreName_RobitsBundle2".to_owned(), - sprite: "ItemShop_Robits".to_owned(), - is_sprite_full_size: false, - category: ItemShopCategory::Bundle, - currency: CurrencyType::CosmeticCredits, - price: 100, - discount_time: 0, - discount_price: 100, - recurrence: ItemShopRecurrence::Weekly, - owns_required_cube: true, - is_limited_edition: false, - }, - // daily (lower row or 6) - ItemShopBundle { - sku: "buy robopass 1 1".to_owned(), - bundle_name_key: "strRoboPassSeason02".to_owned(), - sprite: "Store_RoboPass".to_owned(), - is_sprite_full_size: true, - category: ItemShopCategory::Bundle, - currency: CurrencyType::Robits, - price: 10_000, - discount_time: (chrono::Utc::now() + std::time::Duration::from_secs(24*60*60)).timestamp(), - discount_price: 1_000, - recurrence: ItemShopRecurrence::Daily, - owns_required_cube: false, - is_limited_edition: false, - }, - ItemShopBundle { - sku: "buy robopass 1 2".to_owned(), - bundle_name_key: "strRoboPassSeason02".to_owned(), - sprite: "Store_RoboPass".to_owned(), - is_sprite_full_size: true, - category: ItemShopCategory::Cube, - currency: CurrencyType::Robits, - price: 10_000, - discount_time: (chrono::Utc::now() + std::time::Duration::from_secs(24*60*60)).timestamp(), - discount_price: 2_000, - recurrence: ItemShopRecurrence::Daily, - owns_required_cube: false, - is_limited_edition: false, - }, - ItemShopBundle { - sku: "buy robopass 1 3".to_owned(), - bundle_name_key: "strRoboPassSeason02".to_owned(), - sprite: "Store_RoboPass".to_owned(), - is_sprite_full_size: true, - category: ItemShopCategory::Cube, - currency: CurrencyType::Robits, - price: 10_000, - discount_time: (chrono::Utc::now() + std::time::Duration::from_secs(24*60*60)).timestamp(), - discount_price: 3_000, - recurrence: ItemShopRecurrence::Daily, - owns_required_cube: false, - is_limited_edition: false, - }, - ItemShopBundle { - sku: "buy robopass 1 4".to_owned(), - bundle_name_key: "strRoboPassSeason02".to_owned(), - sprite: "Store_RoboPass".to_owned(), - is_sprite_full_size: true, - category: ItemShopCategory::Cube, - currency: CurrencyType::Robits, - price: 10_000, - discount_time: (chrono::Utc::now() + std::time::Duration::from_secs(24*60*60)).timestamp(), - discount_price: 4_000, - recurrence: ItemShopRecurrence::Daily, - owns_required_cube: false, - is_limited_edition: false, - }, - ItemShopBundle { - sku: "buy robopass 1 5".to_owned(), - bundle_name_key: "strRoboPassSeason02".to_owned(), - sprite: "Store_RoboPass".to_owned(), - is_sprite_full_size: true, - category: ItemShopCategory::Cube, - currency: CurrencyType::Robits, - price: 10_000, - discount_time: (chrono::Utc::now() + std::time::Duration::from_secs(24*60*60)).timestamp(), - discount_price: 5_000, - recurrence: ItemShopRecurrence::Daily, - owns_required_cube: false, - is_limited_edition: false, - }, - ItemShopBundle { - sku: "buy robopass 1 6".to_owned(), - bundle_name_key: "strRoboPassSeason02".to_owned(), - sprite: "Store_RoboPass".to_owned(), - is_sprite_full_size: true, - category: ItemShopCategory::Cube, - currency: CurrencyType::Robits, - price: 10_000, - discount_time: (chrono::Utc::now() + std::time::Duration::from_secs(24*60*60)).timestamp(), - discount_price: 6_000, - recurrence: ItemShopRecurrence::Daily, - owns_required_cube: false, - is_limited_edition: false, - }, - ])); - Ok(params.into()) +pub(super) struct ItemBundleRetriever { + resolver: oj_rc_core::persist::config::ShopEntriesResolver, +} + +#[async_trait::async_trait] +impl SimpleOperation<()> for ItemBundleRetriever { + type User = crate::UserTy; + const CODE: u8 = CODE; + + async fn handle(&self, mut params: ParameterTable, user: &Self::User) -> Result { + //let mut params = ParameterTable::::with_capacity(2); + let user_info = user.user()?; + let entries = self.resolver.resolve_entries(user_info.as_ref().as_ref()).await; + params.insert(PARAM_KEY, entries); + Ok(params) + } +} + +pub(super) fn item_bundle_provider(conf: &oj_rc_core::ConfigImpl) -> SimpleOpImpl<(), crate::UserTy, ItemBundleRetriever> { + SimpleOpImpl::new(ItemBundleRetriever { + resolver: >::shop_entries(conf), }) } diff --git a/rc_services_room/src/operations/item_shop_purchase.rs b/rc_services_room/src/operations/item_shop_purchase.rs new file mode 100644 index 0000000..1c1b5c6 --- /dev/null +++ b/rc_services_room/src/operations/item_shop_purchase.rs @@ -0,0 +1,98 @@ +use polariton_server::operations::{SimpleOpError, SimpleOperation, SimpleOpImpl}; +use polariton::operation::{ParameterTable, Typed, Arr}; + +const CODE: u8 = 189; + +const SKU_PARAM_KEY: u8 = 32; // str; in +const CURRENCY_PARAM_KEY: u8 = 53; // str; in +const PRICE_PARAM_KEY: u8 = 65; // int; in +const NEW_CUBES_PARAM_KEY: u8 = 72; // arr of str; out + +pub(super) struct ItemBundleBuyer { + resolver: oj_rc_core::persist::config::ShopEntriesResolver, +} + +#[async_trait::async_trait] +impl SimpleOperation<()> for ItemBundleBuyer { + type User = crate::UserTy; + const CODE: u8 = CODE; + + async fn handle(&self, mut params: ParameterTable, user: &Self::User) -> Result { + if let Some(Typed::Str(sku)) = params.remove(&SKU_PARAM_KEY) { + if let Some(Typed::Str(currency)) = params.remove(&CURRENCY_PARAM_KEY) { + if let Some(Typed::Int(price)) = params.remove(&PRICE_PARAM_KEY) { + let user_info = user.user()?; + let transactions = self.resolver.resolve_transactions().await; + // TODO refactor this into oj_rc_core in one of the User traits + if let Some(transaction) = transactions.get(&sku.string) { + let mut is_ok = false; + if currency.string == "Robits" && transaction.cost_free == price { + let ty = oj_rc_core::persist::user::CurrencyType::Free; + let op = oj_rc_core::persist::user::CurrencyOp::AddSub(-price as _); + user_info.currency(ty, op).await?; + is_ok = true; + } else if currency.string == "CosmeticCredits" && transaction.cost_paid == price { + let ty = oj_rc_core::persist::user::CurrencyType::Paid; + let op = oj_rc_core::persist::user::CurrencyOp::AddSub(-price as _); + user_info.currency(ty, op).await?; + is_ok = true; + } + if is_ok { + let mut new_cubes = Vec::new(); + for award in transaction.gives.iter() { + match award { + oj_rc_core::persist::config::ShopGain::Cube(x) => { + new_cubes.push(*x); + }, + oj_rc_core::persist::config::ShopGain::Experience(xp) => { + user_info.currency( + oj_rc_core::persist::user::CurrencyType::Experience, + oj_rc_core::persist::user::CurrencyOp::AddSub(*xp as _), + ).await?; + }, + oj_rc_core::persist::config::ShopGain::FreeCurrency(c) => { + user_info.currency( + oj_rc_core::persist::user::CurrencyType::Free, + oj_rc_core::persist::user::CurrencyOp::AddSub(*c as _), + ).await?; + }, + oj_rc_core::persist::config::ShopGain::PaidCurrency(c) => { + user_info.currency( + oj_rc_core::persist::user::CurrencyType::Paid, + oj_rc_core::persist::user::CurrencyOp::AddSub(*c as _), + ).await?; + }, + oj_rc_core::persist::config::ShopGain::TechPoints(tp) => { + user_info.currency( + oj_rc_core::persist::user::CurrencyType::TechPoints, + oj_rc_core::persist::user::CurrencyOp::AddSub(*tp as _), + ).await?; + }, + } + } + let new_cubes_len = new_cubes.len(); + if !new_cubes.is_empty() { + user_info.unlock_parts(&new_cubes).await?; + } + + let new_cubes_typed: Vec<_> = new_cubes.into_iter().map(|x| Typed::Str(hex::encode((x as i32).to_be_bytes()).into())).collect(); + //params.insert(NEW_CUBES_PARAM_KEY, Typed::Arr(new_cubes_i32.into())); + params.insert(NEW_CUBES_PARAM_KEY, Typed::Arr(Arr { + ty: polariton::serdes::TypePrefix::Str, + items: new_cubes_typed, + })); + log::info!("SKU {} purchased (ok? {}) {} ops received, {} cubes unlocked", sku.string, is_ok, transaction.gives.len(), new_cubes_len); + } + } + } + } + } + Ok(params) + } +} + +pub(super) fn item_purchase_provider(conf: &oj_rc_core::ConfigImpl) -> SimpleOpImpl<(), crate::UserTy, ItemBundleBuyer> { + SimpleOpImpl::new(ItemBundleBuyer { + resolver: >::shop_entries(conf), + }) +} diff --git a/rc_services_room/src/operations/mod.rs b/rc_services_room/src/operations/mod.rs index 0bbf1a3..5fcb2f2 100644 --- a/rc_services_room/src/operations/mod.rs +++ b/rc_services_room/src/operations/mod.rs @@ -99,6 +99,7 @@ mod garage_slot_name; mod garage_slot_copy; mod steam_promo; mod campaign_save_result; +mod item_shop_purchase; use polariton_server::operations::OperationsHandler; @@ -151,7 +152,7 @@ pub fn handler(init_ctx: &crate::InitConfig) -> OperationsHandler .add(game_event_params::event_system_params_provider(&init_ctx.cubes)) .add(garage_bay_uuid::garage_id_provider()) .add(tech_tree_data::tech_tree_layout_provider(&init_ctx.cubes)) - .add(item_shop_bundles::item_bundle_provider()) + .add(item_shop_bundles::item_bundle_provider(&init_ctx.cubes)) .add(robot_customisations::bay_customisations_provider()) .add(player_data::player_data_provider()) .add(player_robopass::player_robopass_season_provider()) @@ -223,4 +224,5 @@ pub fn handler(init_ctx: &crate::InitConfig) -> OperationsHandler .add(polariton_server::operations::Ack::<90, _>::default()) // TODO handle SubmitCRFRatingRequest instead of ignoring it //.add(polariton_server::operations::Ack::<97, _>::default()) // TODO handle UpdateShopRobotOffsetRequest instead of ignoring it (this seems to break newly-uploaded vehicles for now) .add(steam_promo::steam_promos_provider()) + .add(item_shop_purchase::item_purchase_provider(&init_ctx.cubes)) }