1
0
mirror of https://git.ngram.ca/OpenJam/rc-servers synced 2026-08-23 23:08:52 +00:00

Refactor item purchase logic into core, implement promo codes #70

This commit is contained in:
NG (Graham)
2026-01-03 11:19:24 -05:00
parent 2c6ef3edbe
commit 2f826835dc
10 changed files with 298 additions and 68 deletions

View File

@@ -508,4 +508,24 @@ impl <C: Clone + Send> super::ConfigProvider<C> for CubeConfig {
fn shop_entries(&self) -> super::ShopEntriesResolver {
super::ShopEntriesResolver::new(self.shop.items.clone())
}
fn promo_codes(&self) -> std::collections::HashMap<String, super::PromoCode> {
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
}
}

View File

@@ -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};

View File

@@ -37,6 +37,7 @@ pub trait ConfigProvider<C: Clone> {
fn pit_settings(&self) -> PitSettings;
fn tdm_settings(&self) -> TeamDeathMatchSettings;
fn shop_entries(&self) -> ShopEntriesResolver;
fn promo_codes(&self) -> std::collections::HashMap<String, PromoCode>;
}
pub struct DevMessageProvider<C: Clone> {
@@ -486,3 +487,13 @@ pub enum ShopGain {
PaidCurrency(i64),
TechPoints(i32)
}
#[derive(Debug)]
pub struct PromoCode {
pub message: Option<String>,
pub bundle_id: String,
pub promo_id: String,
pub is_serial: bool,
pub value: f32,
pub transaction: ShopAction,
}

View File

@@ -4,6 +4,8 @@ use serde::{Serialize, Deserialize};
pub struct ItemShopConfig {
#[serde(default = "default_items")]
pub items: Vec<ItemBundle>,
#[serde(default = "default_codes")]
pub promo_codes: std::collections::HashMap<String, ItemCode>,
}
impl super::config::SelfValidator for ItemShopConfig {
@@ -172,6 +174,19 @@ impl std::convert::From<ItemPurchase> for crate::persist::config::ShopGain {
}
}
#[derive(Serialize, Deserialize, Clone, Debug)]
pub struct ItemCode {
#[serde(default)]
pub message: Option<String>,
pub bundle_id: Option<String>,
pub promo_id: Option<String>,
#[serde(default)]
pub is_serial: bool,
#[serde(default)]
pub value: f32,
pub gives: Vec<ItemPurchase>,
}
pub fn default_items() -> Vec<ItemBundle> {
vec![
// weekly (top row of 3)
@@ -326,3 +341,16 @@ pub fn default_items() -> Vec<ItemBundle> {
},
]
}
pub fn default_codes() -> std::collections::HashMap<String, ItemCode> {
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
}

View File

@@ -74,7 +74,7 @@ impl AccountProvider {
}
#[async_trait::async_trait]
impl <C: Clone> super::UserProvider<C> for AccountProvider {
impl <C: Clone + Send> super::UserProvider<C> for AccountProvider {
async fn authenticate(&self, token: super::UserToken) -> Result<Box<dyn super::User<C> + 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<u64, oj_rc_database::sea_orm::DbErr> {
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<bool, oj_rc_database::sea_orm::DbErr> {
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::<u64>().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<u64, oj_rc_database::sea_orm::DbErr> {
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::<u64>().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::<u64>().unwrap_or_default() - to_sub;
let new_currency = model.data.parse::<u64>().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::<u64>().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 <C: Clone> super::User<C> for UserData {
impl <C: Clone + Send> super::User<C> for UserData {
async fn unlocked_parts(&self) -> Vec<u32> {
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 <C: Clone> super::User<C> for UserData {
db: self.db.clone(),
})
}
async fn apply_purchase(&self, action: &crate::persist::config::ShopAction) -> Result<super::PurchaseResult, polariton_server::operations::SimpleOpError> {
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 {

View File

@@ -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;

View File

@@ -84,6 +84,7 @@ pub trait User<C>: ChatUser + SocialUser + LobbyUser + MultiplayerUser + Singlep
async fn get_avatar_info(&self) -> Result<GetAvatarInfo<C>, i16>;
async fn set_avatar_info(&self, info: AvatarInfo) -> Result<(), i16>;
fn current_game_event_setter(&self) -> Box<dyn GameEventSetter>;
async fn apply_purchase(&self, action: &crate::persist::config::ShopAction) -> Result<PurchaseResult, polariton_server::operations::SimpleOpError>;
}
#[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<String, u32>, // 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<polariton::operation::Typed<()>, i16>;

View File

@@ -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<String, oj_rc_core::persist::config::PromoCode>,
}
#[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<ParameterTable, SimpleOpError> {
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: <oj_rc_core::ConfigImpl as oj_rc_core::ConfigProvider<()>>::promo_codes(conf),
})
}

View File

@@ -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());
}
}
}

View File

@@ -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<crate::UserTy>
//.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))
}