mirror of
https://git.ngram.ca/OpenJam/rc-servers
synced 2026-08-23 23:08:52 +00:00
Add basic currency support and plumb multiplayer rewards screen #70
This commit is contained in:
@@ -121,3 +121,47 @@ impl AuthErrorCode {
|
||||
(self as u16).to_string()
|
||||
}
|
||||
}
|
||||
|
||||
#[repr(u16)] // doesn't really matter
|
||||
#[derive(Debug)]
|
||||
pub enum SocialErrorCode {
|
||||
None = 0,
|
||||
UnexpectedError = 1,
|
||||
UserDoesNotExist = 2,
|
||||
UserAlreadyFriends = 3,
|
||||
MaxFriends = 4,
|
||||
TargetMaxFriends = 5,
|
||||
UserNotFriend = 6,
|
||||
InviteAlreadySent = 7,
|
||||
UserIsSelf = 8,
|
||||
DatabaseError = 9,
|
||||
UserNotOnline = 10,
|
||||
AutoDeclinedFriendOrClan = 11,
|
||||
UserAcceptsPlatoonInvitesFromFriendsAndClansOnly = 12,
|
||||
PlatoonIsFull = 13,
|
||||
UserNotInPlatoon = 14,
|
||||
UserNotPlatoonFound = 15,
|
||||
NoInvite = 16,
|
||||
AlreadyInvited = 17,
|
||||
TheyNotInPlatoon = 18,
|
||||
TheyNotPlatoonLeader = 19,
|
||||
NoClansFound = 20,
|
||||
UserNotInClan = 21,
|
||||
AlreadyInClan = 22,
|
||||
ClanRankTooLow = 23,
|
||||
ClanFull = 24,
|
||||
ClanNotFound = 25,
|
||||
ClanClosed = 26,
|
||||
UserNotFoundInClan = 28,
|
||||
NotClanLeader = 29,
|
||||
TargetAlreadyInClan = 30,
|
||||
ClanAlreadyExists = 31,
|
||||
InvalidClanName = 32,
|
||||
InvalidUsername = 33,
|
||||
WaitingForInviteResponse = 34,
|
||||
AlreadyInPlatoon = 35,
|
||||
NotInPlatoon = 36,
|
||||
NotPlatoonLeader = 37,
|
||||
UserBlockedYou = 38,
|
||||
NoConnection = 39
|
||||
}
|
||||
|
||||
@@ -624,6 +624,57 @@ 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 {
|
||||
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,
|
||||
};
|
||||
let model_opt = match op {
|
||||
super::CurrencyOp::Get => {
|
||||
self.db.update_user_aux_by_user_id_and_descriptor_custom(
|
||||
self.account.id,
|
||||
desc.clone(),
|
||||
|_model| None
|
||||
).await?
|
||||
},
|
||||
super::CurrencyOp::Add(to_add) => {
|
||||
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() + to_add;
|
||||
let mut am = model.to_owned().into_active_model();
|
||||
am.data = oj_rc_database::sea_orm::ActiveValue::Set(new_currency.to_string());
|
||||
Some(am)
|
||||
}
|
||||
).await?
|
||||
},
|
||||
super::CurrencyOp::Sub(to_sub) => {
|
||||
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() - 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)
|
||||
}
|
||||
).await?
|
||||
},
|
||||
};
|
||||
let num: u64 = if let Some(model) = model_opt {
|
||||
model.data.parse().unwrap_or_default()
|
||||
} else {
|
||||
log::warn!("No {:?} user_aux found for user {}", desc, self.account.id);
|
||||
0
|
||||
};
|
||||
Ok(num)
|
||||
}
|
||||
}
|
||||
|
||||
const INVALID_ROBOT_ERR: i16 = crate::data::error_codes::WebServicesError::InvalidRobot as i16; // 140
|
||||
|
||||
@@ -33,4 +33,11 @@ impl super::CommonUser for UserData {
|
||||
async fn db_metrics(&self) -> oj_rc_database::DatabaseMetrics {
|
||||
self.db.metrics().await
|
||||
}
|
||||
|
||||
async fn currency(&self, ty: super::CurrencyType, op: super::CurrencyOp) -> Result<u64, polariton_server::operations::SimpleOpError> {
|
||||
self.currency_op(ty, op).await.map_err(|e| polariton_server::operations::SimpleOpError::with_message(
|
||||
crate::data::error_codes::WebServicesError::DatabaseError as i16,
|
||||
format!("Currency operation failed for user {}: {}", self.account.id, e),
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,7 +11,7 @@ mod inventory;
|
||||
pub use inventory::UnlockedParts;
|
||||
|
||||
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, MultiplayerError, MultiplayerErrorCode, PlayerDescriptor, GameEventSetter, CurrentGameEvent, AuthError, IntercomUser, FakePlayers, ResolvedVehicle, CommonUser, IntercomListener, UserRole};
|
||||
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};
|
||||
|
||||
pub mod intercom;
|
||||
pub use intercom::generate_token as generate_intercom_token;
|
||||
@@ -21,6 +21,7 @@ mod lobby;
|
||||
pub use lobby::TeamChooser;
|
||||
mod common;
|
||||
mod chat;
|
||||
mod social;
|
||||
|
||||
pub const TOKEN_SECRET_FILENAME: &str = "token_secret.key";
|
||||
|
||||
|
||||
@@ -167,4 +167,117 @@ impl super::MultiplayerUser for UserData {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
async fn update_game_score(&self, guid: &str, score: super::PlayerScore) -> Result<i32, super::MultiplayerError> {
|
||||
if let Some(guid) = crate::persist::user::str_to_i64(guid) {
|
||||
if let Some(score_id) = score.id {
|
||||
let model = oj_rc_database::schema::multiplayer_game_score::ActiveModel {
|
||||
id: oj_rc_database::sea_orm::Set(score_id),
|
||||
player_id: oj_rc_database::sea_orm::NotSet,
|
||||
creation_time: oj_rc_database::sea_orm::NotSet,
|
||||
is_claimed: oj_rc_database::sea_orm::NotSet,
|
||||
kills: oj_rc_database::sea_orm::Set(score.kills as i32),
|
||||
deaths: oj_rc_database::sea_orm::Set(score.deaths as i32),
|
||||
assists: oj_rc_database::sea_orm::Set(score.assists as i32),
|
||||
heal_assists: oj_rc_database::sea_orm::Set(score.heal_assists as i32),
|
||||
healed: oj_rc_database::sea_orm::Set(score.healed as i32),
|
||||
received_healed: oj_rc_database::sea_orm::Set(score.received_healed as i32),
|
||||
damaged: oj_rc_database::sea_orm::Set(score.damaged as i32),
|
||||
received_damaged: oj_rc_database::sea_orm::Set(score.received_damaged as i32),
|
||||
crystals: oj_rc_database::sea_orm::Set(score.crystals as i32),
|
||||
total: oj_rc_database::sea_orm::Set(score.total as i32),
|
||||
};
|
||||
let persisted_model = self.db.update_score(model).await
|
||||
.map_err(|e| {
|
||||
log::error!("Failed to update player score for user {} in game {}: {}", self.account.id, guid, e);
|
||||
super::MultiplayerError {
|
||||
code: super::MultiplayerErrorCode::CustomString,
|
||||
message: format!("Failed to update player score for user {} in game {}: {}", self.account.id, guid, e),
|
||||
}
|
||||
})?;
|
||||
Ok(persisted_model.id)
|
||||
} else {
|
||||
let player_opt = self.db.player_by_user_id_and_game_guid(self.account.id, guid).await
|
||||
.map_err(|e| {
|
||||
log::error!("Failed to retrieve player for user {} in game {}: {}", self.account.id, guid, e);
|
||||
super::MultiplayerError {
|
||||
code: super::MultiplayerErrorCode::CustomString,
|
||||
message: format!("Failed to retrieve player for user {} in game {}: {}", self.account.id, guid, e),
|
||||
}
|
||||
})?;
|
||||
if let Some(player) = player_opt {
|
||||
let model = oj_rc_database::schema::multiplayer_game_score::ActiveModel {
|
||||
id: oj_rc_database::sea_orm::NotSet,
|
||||
player_id: oj_rc_database::sea_orm::Set(player.id),
|
||||
creation_time: oj_rc_database::sea_orm::Set(chrono::Utc::now().timestamp()),
|
||||
is_claimed: oj_rc_database::sea_orm::Set(false),
|
||||
kills: oj_rc_database::sea_orm::Set(score.kills as i32),
|
||||
deaths: oj_rc_database::sea_orm::Set(score.deaths as i32),
|
||||
assists: oj_rc_database::sea_orm::Set(score.assists as i32),
|
||||
heal_assists: oj_rc_database::sea_orm::Set(score.heal_assists as i32),
|
||||
healed: oj_rc_database::sea_orm::Set(score.healed as i32),
|
||||
received_healed: oj_rc_database::sea_orm::Set(score.received_healed as i32),
|
||||
damaged: oj_rc_database::sea_orm::Set(score.damaged as i32),
|
||||
received_damaged: oj_rc_database::sea_orm::Set(score.received_damaged as i32),
|
||||
crystals: oj_rc_database::sea_orm::Set(score.crystals as i32),
|
||||
total: oj_rc_database::sea_orm::Set(score.total as i32),
|
||||
};
|
||||
let persisted_model = self.db.insert_score(model).await
|
||||
.map_err(|e| {
|
||||
log::error!("Failed to insert player score for user {} in game {}: {}", self.account.id, guid, e);
|
||||
super::MultiplayerError {
|
||||
code: super::MultiplayerErrorCode::CustomString,
|
||||
message: format!("Failed to insert player score for user {} in game {}: {}", self.account.id, guid, e),
|
||||
}
|
||||
})?;
|
||||
Ok(persisted_model.id)
|
||||
} else {
|
||||
Err(super::MultiplayerError {
|
||||
code: super::MultiplayerErrorCode::IncorrectGameGuid,
|
||||
message: format!("Failed to find user {} in game {}", self.account.id, guid),
|
||||
})
|
||||
}
|
||||
}
|
||||
} else {
|
||||
Err(super::MultiplayerError {
|
||||
code: super::MultiplayerErrorCode::IncorrectGameGuid,
|
||||
message: format!("Failed to parse game GUID {}", guid),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
async fn save_player_connected_status(&self, guid: &str, is_connected: bool) -> Result<(), super::MultiplayerError> {
|
||||
if let Some(guid) = crate::persist::user::str_to_i64(guid) {
|
||||
let player_opt = self.db.player_by_user_id_and_game_guid(self.account.id, guid).await
|
||||
.map_err(|e| {
|
||||
log::error!("Failed to retrieve player for game {} and user {}: {}", guid, self.account.id, e);
|
||||
super::MultiplayerError {
|
||||
code: super::MultiplayerErrorCode::CustomString,
|
||||
message: format!("Failed to retrieve user's player for game {}: {}", guid, e),
|
||||
}
|
||||
})?;
|
||||
if let Some(player) = player_opt {
|
||||
self.db.player_claim(player.id, is_connected).await
|
||||
.map_err(|e| {
|
||||
log::error!("Failed to claim player for game {} and user {}: {}", guid, self.account.id, e);
|
||||
super::MultiplayerError {
|
||||
code: super::MultiplayerErrorCode::CustomString,
|
||||
message: format!("Failed to claim user's player for game {}: {}", guid, e),
|
||||
}
|
||||
})?;
|
||||
Ok(())
|
||||
} else {
|
||||
log::warn!("Failed to find to-be-claimed player for user {} in game {}", self.account.id, guid);
|
||||
Err(super::MultiplayerError {
|
||||
code: super::MultiplayerErrorCode::CustomString,
|
||||
message: "Failed to find user's player to update connected status".to_string(),
|
||||
})
|
||||
}
|
||||
} else {
|
||||
Err(super::MultiplayerError {
|
||||
code: super::MultiplayerErrorCode::IncorrectGameGuid,
|
||||
message: format!("Failed to parse game GUID {}", guid),
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
91
rc_core/src/persist/user/social.rs
Normal file
91
rc_core/src/persist/user/social.rs
Normal file
@@ -0,0 +1,91 @@
|
||||
use super::account_json::UserData;
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl super::SocialUser for UserData {
|
||||
async fn has_unclaimed_match_rewards(&self) -> Result<bool, polariton_server::operations::SimpleOpError> {
|
||||
let count = self.db.count_score_by_user_id_and_claimed(self.account.id, false).await
|
||||
.map_err(|e| {
|
||||
log::error!("Failed to count unclaimed matches for user {} : {}", self.account.id, e);
|
||||
polariton_server::operations::SimpleOpError::with_message(
|
||||
crate::data::error_codes::SocialErrorCode::DatabaseError as i16,
|
||||
format!("Failed to count unclaimed matches: {}", e),
|
||||
)
|
||||
})?;
|
||||
Ok(count > 0)
|
||||
}
|
||||
|
||||
async fn get_unclaimed_match_rewards(&self) -> Result<super::MatchRewards, polariton_server::operations::SimpleOpError> {
|
||||
let unclaimed_score_opt = self.db.score_by_user_id_and_claimed_oldest(self.account.id, false).await
|
||||
.map_err(|e| {
|
||||
log::error!("Failed to retrieve unclaimed player score for user {} : {}", self.account.id, e);
|
||||
polariton_server::operations::SimpleOpError::with_message(
|
||||
crate::data::error_codes::SocialErrorCode::DatabaseError as i16,
|
||||
format!("Failed to retrieve unclaimed player score: {}", e),
|
||||
)
|
||||
})?;
|
||||
|
||||
if let Some(unclaimed_score) = unclaimed_score_opt {
|
||||
// TODO calculate these values
|
||||
return Ok(super::MatchRewards {
|
||||
season_experience: unclaimed_score.total,
|
||||
experience_award_base: unclaimed_score.total,
|
||||
experience_award_premium: unclaimed_score.total, // FIXME actually figure out if player has premium
|
||||
experience_award_party: 0,
|
||||
experience_award_tier: 0,
|
||||
robits_total: unclaimed_score.total,
|
||||
average_experience: unclaimed_score.total,
|
||||
clan_experience: unclaimed_score.total,
|
||||
robits_earned: unclaimed_score.total,
|
||||
premium_robits_earned: unclaimed_score.total,
|
||||
});
|
||||
}
|
||||
log::warn!("No unclaimed match rewards found for user {}", self.account.id);
|
||||
Err(polariton_server::operations::SimpleOpError::with_message(
|
||||
crate::data::error_codes::SocialErrorCode::UnexpectedError as i16,
|
||||
"No remaining unclaimed match rewards for current user".to_owned(),
|
||||
))
|
||||
}
|
||||
|
||||
async fn claim_match_rewards(&self) -> Result<bool, polariton_server::operations::SimpleOpError> {
|
||||
let unclaimed_score_opt = self.db.score_by_user_id_and_claimed_oldest(self.account.id, false).await
|
||||
.map_err(|e| {
|
||||
log::error!("Failed to retrieve to-be-claimed player score for user {}: {}", self.account.id, e);
|
||||
polariton_server::operations::SimpleOpError::with_message(
|
||||
crate::data::error_codes::SocialErrorCode::DatabaseError as i16,
|
||||
format!("Failed to retrieve unclaimed player score: {}", e),
|
||||
)
|
||||
})?;
|
||||
if let Some(unclaimed_score) = unclaimed_score_opt {
|
||||
self.db.score_claim(unclaimed_score.id).await
|
||||
.map_err(|e| {
|
||||
log::error!("Failed to claim player score {} for user {} : {}", unclaimed_score.id, self.account.id, e);
|
||||
polariton_server::operations::SimpleOpError::with_message(
|
||||
crate::data::error_codes::SocialErrorCode::DatabaseError as i16,
|
||||
format!("Failed to claim player score: {}", e),
|
||||
)
|
||||
})?;
|
||||
// TODO calculate currency rewards
|
||||
let reward = unclaimed_score.total as u64;
|
||||
self.currency_op(super::CurrencyType::Free, super::CurrencyOp::Add(reward)).await
|
||||
.map_err(|e| {
|
||||
log::error!("Failed to save currency reward for user {}: {}", self.account.id, e);
|
||||
polariton_server::operations::SimpleOpError::with_message(
|
||||
crate::data::error_codes::SocialErrorCode::DatabaseError as i16,
|
||||
format!("Failed to save currency reward: {}", e),
|
||||
)
|
||||
})?;
|
||||
let experience = unclaimed_score.total as u64 * 4;
|
||||
self.currency_op(super::CurrencyType::Experience, super::CurrencyOp::Add(experience)).await
|
||||
.map_err(|e| {
|
||||
log::error!("Failed to save experience reward for user {}: {}", self.account.id, e);
|
||||
polariton_server::operations::SimpleOpError::with_message(
|
||||
crate::data::error_codes::SocialErrorCode::DatabaseError as i16,
|
||||
format!("Failed to save experience reward: {}", e),
|
||||
)
|
||||
})?;
|
||||
Ok(self.db.count_score_by_user_id_and_claimed(self.account.id, false).await.is_ok_and(|x| x > 0))
|
||||
} else {
|
||||
Ok(false)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -61,7 +61,7 @@ pub trait UserAuthenticator {
|
||||
}
|
||||
|
||||
#[async_trait::async_trait]
|
||||
pub trait User<C>: ChatUser + LobbyUser + MultiplayerUser + IntercomUser + CommonUser {
|
||||
pub trait User<C>: ChatUser + SocialUser + LobbyUser + MultiplayerUser + IntercomUser + CommonUser {
|
||||
async fn unlocked_parts(&self) -> Vec<u32>;
|
||||
async fn selected_garage(&self) -> (String, u32);
|
||||
async fn select_garage(&self, slot: i32) -> Result<(), i16>;
|
||||
@@ -339,6 +339,22 @@ pub trait MultiplayerUser: IntercomUser + CommonUser {
|
||||
async fn game_players(&self, guid: &str) -> Result<Vec<PlayerDescriptor>, MultiplayerError>;
|
||||
async fn complete_game(&self, guid: &str) -> Result<(), MultiplayerError>;
|
||||
async fn game_info(&self, guid: &str) -> Result<Option<GameDescriptor>, MultiplayerError>;
|
||||
async fn update_game_score(&self, guid: &str, score: PlayerScore) -> Result<i32, MultiplayerError>;
|
||||
async fn save_player_connected_status(&self, guid: &str, is_connected: bool) -> Result<(), MultiplayerError>;
|
||||
}
|
||||
|
||||
pub struct PlayerScore {
|
||||
pub id: Option<i32>,
|
||||
pub kills: u32,
|
||||
pub deaths: u32,
|
||||
pub assists: u32,
|
||||
pub heal_assists: u32,
|
||||
pub healed: u32,
|
||||
pub received_healed: u32,
|
||||
pub damaged: u32,
|
||||
pub received_damaged: u32,
|
||||
pub crystals: u32, // crystals destroyed
|
||||
pub total: u32,
|
||||
}
|
||||
|
||||
#[async_trait::async_trait]
|
||||
@@ -387,4 +403,38 @@ pub trait CommonUser: Send + Sync {
|
||||
fn is_royal(&self) -> bool;
|
||||
fn is_banned(&self) -> bool;
|
||||
async fn db_metrics(&self) -> oj_rc_database::DatabaseMetrics;
|
||||
async fn currency(&self, ty: CurrencyType, op: CurrencyOp) -> Result<u64, polariton_server::operations::SimpleOpError>;
|
||||
}
|
||||
|
||||
pub enum CurrencyType {
|
||||
Free,
|
||||
Paid,
|
||||
TechPoints,
|
||||
Experience,
|
||||
}
|
||||
|
||||
pub enum CurrencyOp {
|
||||
Get,
|
||||
Add(u64),
|
||||
Sub(u64),
|
||||
}
|
||||
|
||||
#[async_trait::async_trait]
|
||||
pub trait SocialUser: Send + Sync {
|
||||
async fn has_unclaimed_match_rewards(&self) -> Result<bool, polariton_server::operations::SimpleOpError>;
|
||||
async fn get_unclaimed_match_rewards(&self) -> Result<MatchRewards, polariton_server::operations::SimpleOpError>;
|
||||
async fn claim_match_rewards(&self) -> Result<bool, polariton_server::operations::SimpleOpError>;
|
||||
}
|
||||
|
||||
pub struct MatchRewards {
|
||||
pub season_experience: i32,
|
||||
pub experience_award_base: i32,
|
||||
pub experience_award_premium: i32,
|
||||
pub experience_award_party: i32,
|
||||
pub experience_award_tier: i32,
|
||||
pub robits_total: i32,
|
||||
pub average_experience: i32,
|
||||
pub clan_experience: i32,
|
||||
pub robits_earned: i32,
|
||||
pub premium_robits_earned: i32,
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user