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

Implement item shop #70

This commit is contained in:
NG (Graham)
2026-01-01 20:34:12 -05:00
parent 83843377d7
commit 3aadbb91ba
16 changed files with 602 additions and 151 deletions

View File

@@ -0,0 +1,110 @@
#![allow(dead_code)]
use std::io::Write;
use polariton::operation::Typed;
pub struct ItemShopBundle {
pub sku: String,
pub bundle_name_key: String,
pub sprite: String,
pub is_sprite_full_size: bool,
pub category: ItemShopCategory,
pub currency: CurrencyType, // str
pub price: i32,
pub discount_time: i64, // seconds since unix epoch
pub discount_price: i32,
pub recurrence: ItemShopRecurrence,
pub owns_required_cube: bool,
//pub is_discounted: bool,
pub is_limited_edition: bool,
}
impl ItemShopBundle {
pub fn as_transmissible(&self) -> Typed {
let mut buf = Vec::new();
let mut writer = std::io::Cursor::new(&mut buf);
self.dump(&mut writer).unwrap();
Typed::Bytes(buf.into())
}
fn dump(&self, writer: &mut dyn Write) -> std::io::Result<usize> {
let sku_bytes = self.sku.as_bytes();
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(&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(&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(&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())?;
total_len += writer.write(&self.discount_time.to_le_bytes())?;
total_len += writer.write(&self.discount_price.to_le_bytes())?;
total_len += writer.write(&(self.recurrence as i32).to_le_bytes())?;
total_len += writer.write(&[self.owns_required_cube as u8])?;
total_len += writer.write(&(self.category as i32).to_le_bytes())?;
total_len += writer.write(&[self.is_limited_edition as u8])?;
Ok(total_len)
}
pub fn as_transmissible_vec<C>(items: Vec<Self>) -> Typed<C> {
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();
for item in items.iter() {
item.dump(&mut writer).unwrap();
}
Typed::Bytes(buf.into())
}
}
#[repr(i32)]
#[derive(Copy, Clone)]
pub enum ItemShopCategory {
Cube = 0,
GarageBaySkin = 1,
Bundle = 2,
DeathEffect = 3,
SpawnEffect = 4,
Emotigram = 5,
}
#[repr(i32)]
#[derive(Copy, Clone)]
pub enum ItemShopRecurrence {
Daily = 0,
Weekly = 1,
}
#[repr(i32)]
#[derive(Copy, Clone)]
pub enum CurrencyType {
Robits = 0,
CosmeticCredits = 1,
}
impl CurrencyType {
pub fn as_str(&self) -> &'static str {
match self {
Self::Robits => "Robits",
Self::CosmeticCredits => "CosmeticCredits",
}
}
}

View File

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

View File

@@ -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,
},
},*/
]
}
}

View File

@@ -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 <C: Clone + Send> super::ConfigProvider<C> 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())
}
}

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

View File

@@ -36,6 +36,7 @@ pub trait ConfigProvider<C: Clone> {
fn ba_settings(&self) -> BattleArenaResolver;
fn pit_settings(&self) -> PitSettings;
fn tdm_settings(&self) -> TeamDeathMatchSettings;
fn shop_entries(&self) -> ShopEntriesResolver;
}
pub struct DevMessageProvider<C: Clone> {
@@ -428,3 +429,60 @@ pub struct TeamDeathMatchSettings {
pub respawn_time_seconds: u64,
pub self_destruct_is_kill: bool,
}
pub struct ShopEntriesResolver {
items: Vec<crate::persist::item_shop::ItemBundle>,
}
pub(super) fn item_shop_sku(i: usize) -> String {
format!("item-shop-bundle-{}", i)
}
impl ShopEntriesResolver {
pub async fn resolve_entries<C>(&self, user: &dyn crate::persist::user::User<C>) -> Typed<C> {
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<String, super::ShopAction> {
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<crate::persist::item_shop::ItemBundle>) -> Self {
Self {
items,
}
}
}
#[derive(Debug)]
pub struct ShopAction {
pub cost_free: i32,
pub cost_paid: i32,
pub gives: Vec<ShopGain>,
}
#[derive(Debug)]
pub enum ShopGain {
Cube(u32),
Experience(i64),
FreeCurrency(i64),
PaidCurrency(i64),
TechPoints(i32)
}

View File

@@ -0,0 +1,328 @@
use serde::{Serialize, Deserialize};
#[derive(Serialize, Deserialize, Clone, Debug)]
pub struct ItemShopConfig {
#[serde(default = "default_items")]
pub items: Vec<ItemBundle>,
}
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<u32>,
pub gives: Vec<ItemPurchase>,
}
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<ShopCategory> 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<Recurrence> 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<Currency> 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<ItemPurchase> 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<ItemBundle> {
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![],
},
]
}

View File

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

View File

@@ -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::<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());
Some(am)
}
).await?
},
};
let num: u64 = if let Some(model) = model_opt {
model.data.parse().unwrap_or_default()
@@ -708,6 +722,58 @@ impl <C: Clone> super::User<C> 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::<super::inventory::UnlockedParts>(&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),

View File

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

View File

@@ -63,6 +63,7 @@ pub trait UserAuthenticator {
#[async_trait::async_trait]
pub trait User<C>: ChatUser + SocialUser + LobbyUser + MultiplayerUser + SingleplayerUser + IntercomUser + CommonUser {
async fn unlocked_parts(&self) -> Vec<u32>;
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<C>;
@@ -417,6 +418,7 @@ pub enum CurrencyOp {
Get,
Add(u64),
Sub(u64),
AddSub(i64),
}
#[async_trait::async_trait]