diff --git a/rc_core/src/persist/config/cubes_json.rs b/rc_core/src/persist/config/cubes_json.rs index 702b335..2ff101c 100644 --- a/rc_core/src/persist/config/cubes_json.rs +++ b/rc_core/src/persist/config/cubes_json.rs @@ -508,4 +508,24 @@ impl super::ConfigProvider for CubeConfig { fn shop_entries(&self) -> super::ShopEntriesResolver { super::ShopEntriesResolver::new(self.shop.items.clone()) } + + fn promo_codes(&self) -> std::collections::HashMap { + let mut map = std::collections::HashMap::with_capacity(self.shop.promo_codes.len()); + for (key, val) in self.shop.promo_codes.iter() { + let tx = super::ShopAction { + cost_free: 0, + cost_paid: 0, + gives: val.gives.iter().map(|x| x.to_owned().into()).collect(), + }; + map.insert(key.to_owned(), super::PromoCode { + message: val.message.clone(), + bundle_id: val.bundle_id.to_owned().unwrap_or_else(|| key.to_owned()), + promo_id: val.promo_id.to_owned().unwrap_or_else(|| key.to_owned()), + is_serial: val.is_serial, + value: val.value, + transaction: tx, + }); + } + map + } } diff --git a/rc_core/src/persist/config/mod.rs b/rc_core/src/persist/config/mod.rs index 6a7a2ff..2ac83d4 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, ShopEntriesResolver, ShopAction, ShopGain}; +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, PromoCode}; 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 4fb88e0..4cae703 100644 --- a/rc_core/src/persist/config/traits.rs +++ b/rc_core/src/persist/config/traits.rs @@ -37,6 +37,7 @@ pub trait ConfigProvider { fn pit_settings(&self) -> PitSettings; fn tdm_settings(&self) -> TeamDeathMatchSettings; fn shop_entries(&self) -> ShopEntriesResolver; + fn promo_codes(&self) -> std::collections::HashMap; } pub struct DevMessageProvider { @@ -486,3 +487,13 @@ pub enum ShopGain { PaidCurrency(i64), TechPoints(i32) } + +#[derive(Debug)] +pub struct PromoCode { + pub message: Option, + pub bundle_id: String, + pub promo_id: String, + pub is_serial: bool, + pub value: f32, + pub transaction: ShopAction, +} diff --git a/rc_core/src/persist/item_shop.rs b/rc_core/src/persist/item_shop.rs index 9f791a5..3ff30d8 100644 --- a/rc_core/src/persist/item_shop.rs +++ b/rc_core/src/persist/item_shop.rs @@ -4,6 +4,8 @@ use serde::{Serialize, Deserialize}; pub struct ItemShopConfig { #[serde(default = "default_items")] pub items: Vec, + #[serde(default = "default_codes")] + pub promo_codes: std::collections::HashMap, } impl super::config::SelfValidator for ItemShopConfig { @@ -172,6 +174,19 @@ impl std::convert::From for crate::persist::config::ShopGain { } } +#[derive(Serialize, Deserialize, Clone, Debug)] +pub struct ItemCode { + #[serde(default)] + pub message: Option, + pub bundle_id: Option, + pub promo_id: Option, + #[serde(default)] + pub is_serial: bool, + #[serde(default)] + pub value: f32, + pub gives: Vec, +} + pub fn default_items() -> Vec { vec![ // weekly (top row of 3) @@ -326,3 +341,16 @@ pub fn default_items() -> Vec { }, ] } + +pub fn default_codes() -> std::collections::HashMap { + let mut map = std::collections::HashMap::new(); + map.insert("TEST".to_owned(), ItemCode { + message: Some("Test passed".to_owned()), + bundle_id: None, + promo_id: None, + is_serial: false, + value: 1.5, + gives: vec![] + }); + map +} diff --git a/rc_core/src/persist/user/account_json.rs b/rc_core/src/persist/user/account_json.rs index f990a95..87159f7 100644 --- a/rc_core/src/persist/user/account_json.rs +++ b/rc_core/src/persist/user/account_json.rs @@ -74,7 +74,7 @@ impl AccountProvider { } #[async_trait::async_trait] -impl super::UserProvider for AccountProvider { +impl super::UserProvider for AccountProvider { async fn authenticate(&self, token: super::UserToken) -> Result + Send + Sync>, super::AuthError> { //let new_root = self.root.join(&token.uuid); let secret = jsonwebtoken::DecodingKey::from_secret(&self.secret); @@ -626,13 +626,36 @@ impl UserData { Ok(players) } - pub(super) async fn currency_op(&self, ty: super::CurrencyType, op: super::CurrencyOp) -> Result { - let desc = match ty { + #[inline] + fn currency_ty_to_db(ty: super::CurrencyType) -> oj_rc_database::schema::user_aux::Descriptor { + match ty { super::CurrencyType::Free => oj_rc_database::schema::user_aux::Descriptor::UserFreeCurrency, super::CurrencyType::Paid => oj_rc_database::schema::user_aux::Descriptor::UserPaidCurrency, super::CurrencyType::TechPoints => oj_rc_database::schema::user_aux::Descriptor::TechPoints, super::CurrencyType::Experience => oj_rc_database::schema::user_aux::Descriptor::UserXP, - }; + } + } + + async fn currency_sub_checked(&self, ty: super::CurrencyType, to_sub: u64) -> Result { + let desc = Self::currency_ty_to_db(ty); + let model_opt = self.db.user_aux_by_user_id_and_descriptor(self.account.id, desc.clone()).await?; + if let Some(model) = model_opt { + let existing_funds = model.data.parse::().unwrap_or_default(); + if existing_funds < to_sub { + Ok(false) + } else { + let mut active = model.into_active_model(); + active.data = oj_rc_database::sea_orm::ActiveValue::Set((existing_funds - to_sub).to_string()); + self.db.update_user_aux_by_user_id_and_descriptor(active, self.account.id, desc).await?; + Ok(true) + } + } else { + Ok(false) + } + } + + pub(super) async fn currency_op(&self, ty: super::CurrencyType, op: super::CurrencyOp) -> Result { + let desc = Self::currency_ty_to_db(ty); let model_opt = match op { super::CurrencyOp::Get => { self.db.update_user_aux_by_user_id_and_descriptor_custom( @@ -646,7 +669,6 @@ impl UserData { self.account.id, desc.clone(), move |model| { - use oj_rc_database::sea_orm::IntoActiveModel; let new_currency = model.data.parse::().unwrap_or_default() + to_add; let mut am = model.to_owned().into_active_model(); am.data = oj_rc_database::sea_orm::ActiveValue::Set(new_currency.to_string()); @@ -659,8 +681,7 @@ impl UserData { self.account.id, desc.clone(), move |model| { - use oj_rc_database::sea_orm::IntoActiveModel; - let new_currency = model.data.parse::().unwrap_or_default() - to_sub; + let new_currency = model.data.parse::().unwrap_or_default().saturating_sub(to_sub); let mut am = model.to_owned().into_active_model(); am.data = oj_rc_database::sea_orm::ActiveValue::Set(new_currency.to_string()); Some(am) @@ -672,7 +693,6 @@ impl UserData { 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()); @@ -696,7 +716,7 @@ const DATABASE_ERR: i16 = crate::data::error_codes::WebServicesError::DatabaseEr const UNEXPECTED_ERR: i16 = crate::data::error_codes::WebServicesError::UnexpectedError as i16; // 9 #[async_trait::async_trait] -impl super::User for UserData { +impl super::User for UserData { async fn unlocked_parts(&self) -> Vec { match self.db.user_aux_by_user_id_and_descriptor(self.account.id, oj_rc_database::schema::user_aux::Descriptor::UnlockedParts).await { Ok(Some(parts)) => { @@ -1235,6 +1255,111 @@ impl super::User for UserData { db: self.db.clone(), }) } + + async fn apply_purchase(&self, action: &crate::persist::config::ShopAction) -> Result { + if action.cost_free != 0 { + let is_ok = self.currency_sub_checked(super::CurrencyType::Free, action.cost_free as _).await + .map_err(|e| { + log::error!("Failed to apply free cost for purchase: {}", e); + polariton_server::operations::SimpleOpError::with_message( + crate::data::error_codes::WebServicesError::DatabaseError as i16, + format!("Failed to apply free cost for purchase: {}", e), + ) + })?; + if !is_ok { + log::debug!("Rejected purchase costing {} for user {} (insufficient free funds)", action.cost_free, self.account.id); + return Err(polariton_server::operations::SimpleOpError::with_message( + crate::data::error_codes::WebServicesError::NotEnoughMoney as i16, + "Not enough free funds for purchase".to_owned(), + )) + } + } + if action.cost_paid != 0 { + let is_ok = self.currency_sub_checked(super::CurrencyType::Paid, action.cost_paid as _).await + .map_err(|e| { + log::error!("Failed to apply paid cost for purchase: {}", e); + polariton_server::operations::SimpleOpError::with_message( + crate::data::error_codes::WebServicesError::DatabaseError as i16, + format!("Failed to apply paid cost for purchase: {}", e), + ) + })?; + if !is_ok { + log::debug!("Rejected purchase costing {} for user {} (insufficient paid funds)", action.cost_paid, self.account.id); + return Err(polariton_server::operations::SimpleOpError::with_message( + crate::data::error_codes::WebServicesError::NotEnoughMoney as i16, + "Not enough paid funds for purchase".to_owned(), + )) + } + } + let mut new_cubes = std::collections::HashMap::new(); + let mut paid_currency = 0; + for award in action.gives.iter() { + match award { + crate::persist::config::ShopGain::Cube(x) => { + new_cubes.insert(hex::encode((*x as i32).to_be_bytes()), 1); + }, + crate::persist::config::ShopGain::Experience(xp) => { + self.currency_op( + crate::persist::user::CurrencyType::Experience, + crate::persist::user::CurrencyOp::AddSub(*xp as _), + ).await + .map_err(|e| { + log::error!("Failed to apply experience for purchase: {}", e); + polariton_server::operations::SimpleOpError::with_message( + crate::data::error_codes::WebServicesError::DatabaseError as i16, + format!("Failed to apply experience for purchase: {}", e), + ) + })?; + }, + crate::persist::config::ShopGain::FreeCurrency(c) => { + self.currency_op( + crate::persist::user::CurrencyType::Free, + crate::persist::user::CurrencyOp::AddSub(*c as _), + ).await + .map_err(|e| { + log::error!("Failed to apply free currency for purchase: {}", e); + polariton_server::operations::SimpleOpError::with_message( + crate::data::error_codes::WebServicesError::DatabaseError as i16, + format!("Failed to apply free currency for purchase: {}", e), + ) + })?; + }, + crate::persist::config::ShopGain::PaidCurrency(c) => { + self.currency_op( + crate::persist::user::CurrencyType::Paid, + crate::persist::user::CurrencyOp::AddSub(*c as _), + ).await + .map_err(|e| { + log::error!("Failed to apply paid currency for purchase: {}", e); + polariton_server::operations::SimpleOpError::with_message( + crate::data::error_codes::WebServicesError::DatabaseError as i16, + format!("Failed to apply paid currency for purchase: {}", e), + ) + })?; + paid_currency += *c; + }, + crate::persist::config::ShopGain::TechPoints(tp) => { + self.currency_op( + crate::persist::user::CurrencyType::TechPoints, + crate::persist::user::CurrencyOp::AddSub(*tp as _), + ).await + .map_err(|e| { + log::error!("Failed to apply tech point for purchase: {}", e); + polariton_server::operations::SimpleOpError::with_message( + crate::data::error_codes::WebServicesError::DatabaseError as i16, + format!("Failed to apply tech points for purchase: {}", e), + ) + })?; + }, + } + } + Ok(super::PurchaseResult { + success: true, + cube_awards: new_cubes, + robopass_award: false, + paid_currency_award: paid_currency, + }) + } } struct GameEventSetterImpl { diff --git a/rc_core/src/persist/user/mod.rs b/rc_core/src/persist/user/mod.rs index 2755349..9a6faaa 100644 --- a/rc_core/src/persist/user/mod.rs +++ b/rc_core/src/persist/user/mod.rs @@ -11,7 +11,7 @@ mod inventory; 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}; +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, PurchaseResult}; pub mod intercom; pub use intercom::generate_token as generate_intercom_token; diff --git a/rc_core/src/persist/user/traits.rs b/rc_core/src/persist/user/traits.rs index 18aa45e..103799c 100644 --- a/rc_core/src/persist/user/traits.rs +++ b/rc_core/src/persist/user/traits.rs @@ -84,6 +84,7 @@ pub trait User: ChatUser + SocialUser + LobbyUser + MultiplayerUser + Singlep async fn get_avatar_info(&self) -> Result, i16>; async fn set_avatar_info(&self, info: AvatarInfo) -> Result<(), i16>; fn current_game_event_setter(&self) -> Box; + async fn apply_purchase(&self, action: &crate::persist::config::ShopAction) -> Result; } #[async_trait::async_trait] @@ -198,6 +199,17 @@ pub struct AvatarInfo { pub use_custom: bool, } +pub struct PurchaseResult { + pub success: bool, + //pub result_code: + //pub is_serial_key: bool, + //pub value: f32, + //pub promo_id: String, + pub cube_awards: std::collections::HashMap, // hex id -> count + pub robopass_award: bool, + pub paid_currency_award: i64, +} + #[async_trait::async_trait] pub trait ChatUser: CommonUser + IntercomUser { async fn subscribed_channels(&self) -> Result, i16>; diff --git a/rc_services_room/src/operations/code_redeem.rs b/rc_services_room/src/operations/code_redeem.rs new file mode 100644 index 0000000..a036528 --- /dev/null +++ b/rc_services_room/src/operations/code_redeem.rs @@ -0,0 +1,78 @@ +use polariton_server::operations::{SimpleOpError, SimpleOperation, SimpleOpImpl}; +use polariton::operation::{ParameterTable, Typed}; + +const CODE: u8 = 107; + +const CODE_NAME_PARAM_KEY: u8 = 122; // str; in +const SUCCESS_PARAM_KEY: u8 = 123; // bool; out +const RESULT_CODE_PARAM_KEY: u8 = 124; // int; out +const IS_SERIAL_PARAM_KEY: u8 = 125; // bool; out +const VALUE_PARAM_KEY: u8 = 126; // float; out +const PROMO_ID_PARAM_KEY: u8 = 127; // str; out +const CUBES_AWARDED_PARAM_KEY: u8 = 128; // json as str; out +const MSG_PARAM_KEY: u8 = 133; // str; out +const BUNDLE_ID_PARAM_KEY: u8 = 208; // str; out +const ROBOPASS_PARAM_KEY: u8 = 4; // bool; out +const PAID_CURRENCY_PARAM_KEY: u8 = 86; // long; out + +#[allow(dead_code)] +#[repr(u8)] +enum PromoResultCode { + Success = 0, + InvalidBundleId = 1, + InvalidPromotionId = 2, + Expired = 3, + NotStarted = 4, + AlreadyAwarded = 5, + Consumed = 6, + BundleIdAlreadyAwarded = 7 +} + +pub(super) struct PromoCodeApplier { + code_map: std::collections::HashMap, +} + +#[async_trait::async_trait] +impl SimpleOperation<()> for PromoCodeApplier { + type User = crate::UserTy; + const CODE: u8 = CODE; + + async fn handle(&self, mut params: ParameterTable, user: &Self::User) -> Result { + if let Some(Typed::Str(promo_code)) = params.remove(&CODE_NAME_PARAM_KEY) { + let user_info = user.user()?; + if let Some(code_info) = self.code_map.get(&promo_code.string) { + let result = user_info.apply_purchase(&code_info.transaction).await?; + params.insert(SUCCESS_PARAM_KEY, Typed::Bool(result.success)); + params.insert(RESULT_CODE_PARAM_KEY, Typed::Int(PromoResultCode::Success as _)); + params.insert(IS_SERIAL_PARAM_KEY, Typed::Bool(code_info.is_serial)); + params.insert(VALUE_PARAM_KEY, Typed::Float(code_info.value)); + params.insert(PROMO_ID_PARAM_KEY, Typed::Str(code_info.promo_id.clone().into())); + params.insert(CUBES_AWARDED_PARAM_KEY, Typed::Str(serde_json::to_string(&result.cube_awards).unwrap().into())); + params.insert(MSG_PARAM_KEY, Typed::Str(code_info.message.clone().unwrap_or_default().into())); + params.insert(BUNDLE_ID_PARAM_KEY, Typed::Str(code_info.bundle_id.clone().into())); + params.insert(ROBOPASS_PARAM_KEY, Typed::Bool(result.robopass_award)); + params.insert(PAID_CURRENCY_PARAM_KEY, Typed::Long(result.paid_currency_award)); + log::debug!("Code \"{}\" redeemed by {} (success? {}) {} rewards", promo_code.string, user_info.public_id(), result.success, code_info.transaction.gives.len()); + } else { + params.insert(SUCCESS_PARAM_KEY, Typed::Bool(false)); + params.insert(RESULT_CODE_PARAM_KEY, Typed::Int(PromoResultCode::InvalidPromotionId as _)); + params.insert(IS_SERIAL_PARAM_KEY, Typed::Bool(false)); + params.insert(VALUE_PARAM_KEY, Typed::Float(0.0)); + params.insert(PROMO_ID_PARAM_KEY, Typed::Str(promo_code.clone())); + params.insert(CUBES_AWARDED_PARAM_KEY, Typed::Str("{}".into())); + params.insert(MSG_PARAM_KEY, Typed::Str("".into())); + params.insert(BUNDLE_ID_PARAM_KEY, Typed::Str("RE_bundle_id_01".into())); + params.insert(ROBOPASS_PARAM_KEY, Typed::Bool(false)); + params.insert(PAID_CURRENCY_PARAM_KEY, Typed::Long(0)); + log::debug!("Code \"{}\" not redeemed by {} (code not found)", promo_code.string, user_info.public_id()); + } + } + Ok(params) + } +} + +pub(super) fn code_redeem_provider(conf: &oj_rc_core::ConfigImpl) -> SimpleOpImpl<(), crate::UserTy, PromoCodeApplier> { + SimpleOpImpl::new(PromoCodeApplier { + code_map: >::promo_codes(conf), + }) +} diff --git a/rc_services_room/src/operations/item_shop_purchase.rs b/rc_services_room/src/operations/item_shop_purchase.rs index 1c1b5c6..61708a5 100644 --- a/rc_services_room/src/operations/item_shop_purchase.rs +++ b/rc_services_room/src/operations/item_shop_purchase.rs @@ -1,5 +1,5 @@ use polariton_server::operations::{SimpleOpError, SimpleOperation, SimpleOpImpl}; -use polariton::operation::{ParameterTable, Typed, Arr}; +use polariton::operation::{ParameterTable, Typed}; const CODE: u8 = 189; @@ -23,65 +23,19 @@ impl SimpleOperation<()> for ItemBundleBuyer { 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; - } + let is_ok = (currency.string == "Robits" && transaction.cost_free == price) + || (currency.string == "CosmeticCredits" && transaction.cost_paid == price); 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 { + let purchase_result = user_info.apply_purchase(transaction).await?; + let new_cubes = polariton::operation::Typed::Arr(polariton::operation::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); + items: purchase_result.cube_awards.into_keys() + .map(|k| polariton::operation::Typed::Str(k.into())) + .collect(), + }); + params.insert(NEW_CUBES_PARAM_KEY, new_cubes); + log::debug!("SKU {} purchased (ok? {}) {} ops", sku.string, is_ok, transaction.gives.len()); } } } diff --git a/rc_services_room/src/operations/mod.rs b/rc_services_room/src/operations/mod.rs index 5fcb2f2..068d925 100644 --- a/rc_services_room/src/operations/mod.rs +++ b/rc_services_room/src/operations/mod.rs @@ -100,6 +100,7 @@ mod garage_slot_copy; mod steam_promo; mod campaign_save_result; mod item_shop_purchase; +mod code_redeem; use polariton_server::operations::OperationsHandler; @@ -225,4 +226,5 @@ pub fn handler(init_ctx: &crate::InitConfig) -> OperationsHandler //.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)) + .add(code_redeem::code_redeem_provider(&init_ctx.cubes)) }