From 4ec64a9cd9f52369de82dfe7bb8a37e7d6934be4 Mon Sep 17 00:00:00 2001 From: "NG (Graham)" Date: Sun, 28 Dec 2025 18:13:14 -0500 Subject: [PATCH] Add basic currency support and plumb multiplayer rewards screen #70 --- rc_core/src/data/error_codes.rs | 44 +++++++ rc_core/src/persist/user/account_json.rs | 51 ++++++++ rc_core/src/persist/user/common.rs | 7 ++ rc_core/src/persist/user/mod.rs | 3 +- rc_core/src/persist/user/multiplayer.rs | 113 ++++++++++++++++++ rc_core/src/persist/user/social.rs | 91 ++++++++++++++ rc_core/src/persist/user/traits.rs | 52 +++++++- .../m20251228_000001_create_score_table.rs | 56 +++++++++ rc_database/src/migration/mod.rs | 2 + rc_database/src/schema/mod.rs | 1 + .../src/schema/multiplayer_game_player.rs | 8 ++ .../src/schema/multiplayer_game_score.rs | 39 ++++++ rc_database/src/wrapper.rs | 100 +++++++++++++++- rc_lobby_room/src/main.rs | 1 + rc_multiplayer/src/matches/generic.rs | 53 ++++++-- rc_services_room/src/main.rs | 1 + .../src/operations/balance_info.rs | 38 ++++-- .../src/operations/player_level.rs | 8 +- .../src/operations/tech_points.rs | 30 ++++- rc_services_room/src/operations/user_xp.rs | 31 +++-- rc_social_room/src/data/clan_invite.rs | 1 + rc_social_room/src/operations/clan_invite.rs | 7 +- rc_social_room/src/operations/mod.rs | 4 + .../src/operations/previous_battle_rewards.rs | 32 +++-- .../previous_battle_rewards_claim.rs | 28 +++++ .../operations/previous_battle_rewards_get.rs | 49 ++++++++ 26 files changed, 802 insertions(+), 48 deletions(-) create mode 100644 rc_core/src/persist/user/social.rs create mode 100644 rc_database/src/migration/m20251228_000001_create_score_table.rs create mode 100644 rc_database/src/schema/multiplayer_game_score.rs create mode 100644 rc_social_room/src/operations/previous_battle_rewards_claim.rs create mode 100644 rc_social_room/src/operations/previous_battle_rewards_get.rs diff --git a/rc_core/src/data/error_codes.rs b/rc_core/src/data/error_codes.rs index b78b410..96e0d13 100644 --- a/rc_core/src/data/error_codes.rs +++ b/rc_core/src/data/error_codes.rs @@ -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 +} diff --git a/rc_core/src/persist/user/account_json.rs b/rc_core/src/persist/user/account_json.rs index e6a3c6b..560fdb6 100644 --- a/rc_core/src/persist/user/account_json.rs +++ b/rc_core/src/persist/user/account_json.rs @@ -624,6 +624,57 @@ impl UserData { } Ok(players) } + + pub(super) async fn currency_op(&self, ty: super::CurrencyType, op: super::CurrencyOp) -> Result { + 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::().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::().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 diff --git a/rc_core/src/persist/user/common.rs b/rc_core/src/persist/user/common.rs index 2351810..9e4e5de 100644 --- a/rc_core/src/persist/user/common.rs +++ b/rc_core/src/persist/user/common.rs @@ -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 { + 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), + )) + } } diff --git a/rc_core/src/persist/user/mod.rs b/rc_core/src/persist/user/mod.rs index 9f907ec..1c90fdb 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; 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"; diff --git a/rc_core/src/persist/user/multiplayer.rs b/rc_core/src/persist/user/multiplayer.rs index 1a60b86..e45db7f 100644 --- a/rc_core/src/persist/user/multiplayer.rs +++ b/rc_core/src/persist/user/multiplayer.rs @@ -167,4 +167,117 @@ impl super::MultiplayerUser for UserData { }) } } + + async fn update_game_score(&self, guid: &str, score: super::PlayerScore) -> Result { + 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), + }) + } + } } diff --git a/rc_core/src/persist/user/social.rs b/rc_core/src/persist/user/social.rs new file mode 100644 index 0000000..0335dc0 --- /dev/null +++ b/rc_core/src/persist/user/social.rs @@ -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 { + 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 { + 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 { + 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) + } + } +} diff --git a/rc_core/src/persist/user/traits.rs b/rc_core/src/persist/user/traits.rs index 4142c4e..261327a 100644 --- a/rc_core/src/persist/user/traits.rs +++ b/rc_core/src/persist/user/traits.rs @@ -61,7 +61,7 @@ pub trait UserAuthenticator { } #[async_trait::async_trait] -pub trait User: ChatUser + LobbyUser + MultiplayerUser + IntercomUser + CommonUser { +pub trait User: ChatUser + SocialUser + LobbyUser + MultiplayerUser + IntercomUser + CommonUser { async fn unlocked_parts(&self) -> Vec; 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, MultiplayerError>; async fn complete_game(&self, guid: &str) -> Result<(), MultiplayerError>; async fn game_info(&self, guid: &str) -> Result, MultiplayerError>; + async fn update_game_score(&self, guid: &str, score: PlayerScore) -> Result; + async fn save_player_connected_status(&self, guid: &str, is_connected: bool) -> Result<(), MultiplayerError>; +} + +pub struct PlayerScore { + pub id: Option, + 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; +} + +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; + async fn get_unclaimed_match_rewards(&self) -> Result; + async fn claim_match_rewards(&self) -> Result; +} + +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, } diff --git a/rc_database/src/migration/m20251228_000001_create_score_table.rs b/rc_database/src/migration/m20251228_000001_create_score_table.rs new file mode 100644 index 0000000..dc3c64e --- /dev/null +++ b/rc_database/src/migration/m20251228_000001_create_score_table.rs @@ -0,0 +1,56 @@ +use sea_orm_migration::prelude::*; + +pub struct Migration; + +impl MigrationName for Migration { + fn name(&self) -> &str { + "m20251228_000001_create_score_table" + } +} + +#[async_trait::async_trait] +impl MigrationTrait for Migration { + // Define how to apply this migration: Create the Scores table. + async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> { + manager + .create_table( + Table::create() + .table(crate::schema::multiplayer_game_score::Entity) + .col( + ColumnDef::new(crate::schema::multiplayer_game_score::Column::Id) + .integer() + .not_null() + .auto_increment() + .primary_key(), + ) + .col(ColumnDef::new(crate::schema::multiplayer_game_score::Column::PlayerId).integer().not_null()) + .foreign_key( + ForeignKey::create() + .name("fk-scores-player_id") + .from(crate::schema::multiplayer_game_score::Entity, crate::schema::multiplayer_game_score::Column::PlayerId) + .to(crate::schema::multiplayer_game_player::Entity, crate::schema::multiplayer_game_player::Column::Id), + ) + .col(ColumnDef::new(crate::schema::multiplayer_game_score::Column::CreationTime).big_integer().not_null()) + .col(ColumnDef::new(crate::schema::multiplayer_game_score::Column::IsClaimed).boolean().default(false)) + .col(ColumnDef::new(crate::schema::multiplayer_game_score::Column::Kills).integer().default(0)) + .col(ColumnDef::new(crate::schema::multiplayer_game_score::Column::Deaths).integer().default(0)) + .col(ColumnDef::new(crate::schema::multiplayer_game_score::Column::Assists).integer().default(0)) + .col(ColumnDef::new(crate::schema::multiplayer_game_score::Column::HealAssists).integer().default(0)) + .col(ColumnDef::new(crate::schema::multiplayer_game_score::Column::Healed).integer().default(0)) + .col(ColumnDef::new(crate::schema::multiplayer_game_score::Column::ReceivedHealed).integer().default(0)) + .col(ColumnDef::new(crate::schema::multiplayer_game_score::Column::Damaged).integer().default(0)) + .col(ColumnDef::new(crate::schema::multiplayer_game_score::Column::ReceivedDamaged).integer().default(0)) + .col(ColumnDef::new(crate::schema::multiplayer_game_score::Column::Crystals).integer().default(0)) + .col(ColumnDef::new(crate::schema::multiplayer_game_score::Column::Total).integer().default(0)) + .to_owned(), + ) + .await + } + + // Define how to rollback this migration: Drop the Scores table. + async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> { + manager + .drop_table(Table::drop().table(crate::schema::multiplayer_game_score::Entity).to_owned()) + .await + } +} diff --git a/rc_database/src/migration/mod.rs b/rc_database/src/migration/mod.rs index 6a853de..e1af97c 100644 --- a/rc_database/src/migration/mod.rs +++ b/rc_database/src/migration/mod.rs @@ -12,6 +12,7 @@ mod m20250713_000002_create_player_table; mod m20250722_000001_create_game_event_table; mod m20250816_000001_add_fake_players; mod m20250918_000001_add_player_variant; +mod m20251228_000001_create_score_table; pub struct Migrator; @@ -31,6 +32,7 @@ impl MigratorTrait for Migrator { Box::new(m20250722_000001_create_game_event_table::Migration), Box::new(m20250816_000001_add_fake_players::Migration), Box::new(m20250918_000001_add_player_variant::Migration), + Box::new(m20251228_000001_create_score_table::Migration), ] } } diff --git a/rc_database/src/schema/mod.rs b/rc_database/src/schema/mod.rs index fc4c34a..ceded99 100644 --- a/rc_database/src/schema/mod.rs +++ b/rc_database/src/schema/mod.rs @@ -9,6 +9,7 @@ pub mod sanction; pub mod multiplayer_game; pub mod multiplayer_game_player; pub mod game_event; +pub mod multiplayer_game_score; pub fn parse_int_csv(s: &str) -> Vec { s.split(',').filter_map(|i_as_s| { diff --git a/rc_database/src/schema/multiplayer_game_player.rs b/rc_database/src/schema/multiplayer_game_player.rs index 69e4872..216bec3 100644 --- a/rc_database/src/schema/multiplayer_game_player.rs +++ b/rc_database/src/schema/multiplayer_game_player.rs @@ -31,6 +31,8 @@ pub enum Relation { to = "super::user::Column::Id" )] User, + #[sea_orm(has_one = "super::multiplayer_game_score::Entity")] + Score, } impl Related for Entity { @@ -45,6 +47,12 @@ impl Related for Entity { } } +impl Related for Entity { + fn to() -> RelationDef { + Relation::Score.def() + } +} + impl ActiveModelBehavior for ActiveModel {} #[derive(Clone, Copy, Debug, PartialEq, Eq, EnumIter, DeriveActiveEnum)] diff --git a/rc_database/src/schema/multiplayer_game_score.rs b/rc_database/src/schema/multiplayer_game_score.rs new file mode 100644 index 0000000..4777268 --- /dev/null +++ b/rc_database/src/schema/multiplayer_game_score.rs @@ -0,0 +1,39 @@ +use sea_orm::entity::prelude::*; + +#[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel)] +#[sea_orm(table_name = "scores")] +pub struct Model { + #[sea_orm(primary_key)] + pub id: i32, + pub player_id: i32, + pub creation_time: i64, // seconds since unix epoch + pub is_claimed: bool, + pub kills: i32, + pub deaths: i32, + pub assists: i32, + pub heal_assists: i32, + pub healed: i32, + pub received_healed: i32, + pub damaged: i32, + pub received_damaged: i32, + pub crystals: i32, + pub total: i32, +} + +#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] +pub enum Relation { + #[sea_orm( + belongs_to = "super::multiplayer_game_player::Entity", + from = "Column::PlayerId", + to = "super::multiplayer_game_player::Column::Id" + )] + Player, +} + +impl Related for Entity { + fn to() -> RelationDef { + Relation::Player.def() + } +} + +impl ActiveModelBehavior for ActiveModel {} diff --git a/rc_database/src/wrapper.rs b/rc_database/src/wrapper.rs index d44c5e6..013cc95 100644 --- a/rc_database/src/wrapper.rs +++ b/rc_database/src/wrapper.rs @@ -110,6 +110,35 @@ impl Database { } } + pub async fn update_user_aux_by_user_id_and_descriptor_custom(&self, user_id: i32, descriptor: crate::schema::user_aux::Descriptor, custom: impl (FnOnce(&crate::schema::user_aux::Model) -> Option) + Send + 'static) -> Result, sea_orm::DbErr> { + self.orm.transaction(|txn| { + Box::pin(async move { + let opt = crate::schema::user_aux::Entity::find() + .filter(crate::schema::user_aux::Column::UserId.eq(user_id)) + .filter(crate::schema::user_aux::Column::Descriptor.eq(descriptor)) + .one(txn) + .await?; + + if let Some(model) = opt { + if let Some(updated_model) = custom(&model) { + Ok(Some(crate::schema::user_aux::Entity::update(updated_model) + .exec(txn) + .await?)) + } else { + Ok(Some(model)) + } + } else { + Ok(None) + } + }) + }).await.map_err(|e| { + match e { + sea_orm::TransactionError::Connection(db) => db, + sea_orm::TransactionError::Transaction(txn) => txn, + } + }) + } + pub async fn perms_by_user_id(&self, user_id: i32) -> Result, sea_orm::DbErr> { crate::schema::permissions::Entity::find() .filter(crate::schema::permissions::Column::UserId.eq(user_id)) @@ -404,7 +433,7 @@ impl Database { .collect()) } - pub async fn players_by_game_id_and_completion(&self, game_id: i32) -> Result, sea_orm::DbErr> { + pub async fn players_by_game_id(&self, game_id: i32) -> Result, sea_orm::DbErr> { Ok(crate::schema::multiplayer_game_player::Entity::find() .find_also_related(crate::schema::multiplayer_game::Entity) .filter(crate::schema::multiplayer_game::Column::Id.eq(game_id)) @@ -415,11 +444,33 @@ impl Database { .collect()) } + pub async fn player_by_user_id_and_game_guid(&self, user_id: i32, game_guid: i64) -> Result, sea_orm::DbErr> { + Ok(crate::schema::multiplayer_game::Entity::find() + .find_also_related(crate::schema::multiplayer_game_player::Entity) + .filter(crate::schema::multiplayer_game::Column::Guid.eq(game_guid)) + .filter(crate::schema::multiplayer_game_player::Column::UserId.eq(user_id)) + //.order_by_asc(crate::schema::multiplayer_game::Column::CreationTime) + .one(&self.orm) + .await? + .and_then(|(_, player)| player)) + } + pub async fn insert_players(&self, entities: Vec) -> Result<(), sea_orm::DbErr> { crate::schema::multiplayer_game_player::Entity::insert_many(entities.into_iter()).exec(&self.orm).await?; Ok(()) } + pub async fn player_claim(&self, player_id: i32, is_claimed: bool) -> Result<(), sea_orm::DbErr> { + crate::schema::multiplayer_game_player::Entity::update(crate::schema::multiplayer_game_player::ActiveModel { + id: sea_orm::ActiveValue::Set(player_id), + is_claimed: sea_orm::Set(is_claimed), + ..Default::default() + }) + .exec(&self.orm) + .await?; + Ok(()) + } + pub async fn game_event_at_time(&self, time: i64, variant: crate::schema::game_event::EventVariant) -> Result, sea_orm::DbErr> { crate::schema::game_event::Entity::find() .filter(crate::schema::game_event::Column::Start.lte(time)) @@ -434,6 +485,53 @@ impl Database { entity.insert(&self.orm).await } + pub async fn score_by_player_id(&self, player_id: i32) -> Result, sea_orm::DbErr> { + crate::schema::multiplayer_game_score::Entity::find() + .filter(crate::schema::multiplayer_game_score::Column::PlayerId.eq(player_id)) + .one(&self.orm) + .await + } + + pub async fn score_by_user_id_and_claimed_oldest(&self, user_id: i32, is_claimed: bool) -> Result, sea_orm::DbErr> { + crate::schema::multiplayer_game_player::Entity::find() + .find_also_related(crate::schema::multiplayer_game_score::Entity) + .filter(crate::schema::multiplayer_game_player::Column::UserId.eq(user_id)) + .filter(crate::schema::multiplayer_game_score::Column::IsClaimed.eq(is_claimed)) + .order_by_asc(crate::schema::multiplayer_game_player::Column::CreationTime) + .one(&self.orm) + .await + .map(|opt| opt.and_then(|(_player, score)| score)) + } + + pub async fn insert_score(&self, entity: crate::schema::multiplayer_game_score::ActiveModel) -> Result { + entity.insert(&self.orm).await + } + + pub async fn update_score(&self, entity: crate::schema::multiplayer_game_score::ActiveModel) -> Result { + crate::schema::multiplayer_game_score::Entity::update(entity).exec(&self.orm).await + } + + pub async fn score_claim(&self, score_id: i32) -> Result<(), sea_orm::DbErr> { + crate::schema::multiplayer_game_score::Entity::update(crate::schema::multiplayer_game_score::ActiveModel { + id: sea_orm::ActiveValue::Set(score_id), + is_claimed: sea_orm::Set(true), + ..Default::default() + }) + .exec(&self.orm) + .await?; + Ok(()) + } + + pub async fn count_score_by_user_id_and_claimed(&self, user_id: i32, is_claimed: bool) -> Result { + crate::schema::multiplayer_game_player::Entity::find() + .find_also_related(crate::schema::multiplayer_game_score::Entity) + .filter(crate::schema::multiplayer_game_player::Column::UserId.eq(user_id)) + .filter(crate::schema::multiplayer_game_score::Column::IsClaimed.eq(is_claimed)) + .order_by_asc(crate::schema::multiplayer_game_player::Column::CreationTime) + .count(&self.orm) + .await + } + pub async fn metrics(&self) -> super::DatabaseMetrics { self.metrics.lock().unwrap().snapshot() } diff --git a/rc_lobby_room/src/main.rs b/rc_lobby_room/src/main.rs index 63a2768..afd4bf4 100644 --- a/rc_lobby_room/src/main.rs +++ b/rc_lobby_room/src/main.rs @@ -53,6 +53,7 @@ async fn main() -> std::io::Result<()> { let start_time = chrono::Utc::now(); START_TIMESTAMP_S.store(start_time.timestamp(), std::sync::atomic::Ordering::Relaxed); + log::info!("lobby_room ready"); if args.once { log::warn!("Handling first connection and then exiting"); let (socket, address) = listener.accept().await?; diff --git a/rc_multiplayer/src/matches/generic.rs b/rc_multiplayer/src/matches/generic.rs index 8ca81c7..e1237e5 100644 --- a/rc_multiplayer/src/matches/generic.rs +++ b/rc_multiplayer/src/matches/generic.rs @@ -115,6 +115,7 @@ impl Location { } pub(super) struct UserData { + pub score_id: std::sync::atomic::AtomicI64, pub kills: std::sync::atomic::AtomicU32, pub deaths: std::sync::atomic::AtomicU32, pub assists: std::sync::atomic::AtomicU32, @@ -130,6 +131,7 @@ pub(super) struct UserData { impl UserData { fn new() -> Self { Self { + score_id: std::sync::atomic::AtomicI64::new(i64::MIN), kills: std::sync::atomic::AtomicU32::new(0), deaths: std::sync::atomic::AtomicU32::new(0), assists: std::sync::atomic::AtomicU32::new(0), @@ -173,6 +175,23 @@ impl UserData { delta_score: delta.unwrap_or(backup_delta), } } + + fn as_core(&self) -> oj_rc_core::persist::user::PlayerScore { + let id_maybe = self.score_id.load(std::sync::atomic::Ordering::Relaxed); + oj_rc_core::persist::user::PlayerScore { + id: id_maybe.try_into().ok(), + kills: self.kills.load(std::sync::atomic::Ordering::Relaxed), + deaths: self.deaths.load(std::sync::atomic::Ordering::Relaxed), + assists: self.assists.load(std::sync::atomic::Ordering::Relaxed), + heal_assists: self.heal_assists.load(std::sync::atomic::Ordering::Relaxed), + healed: self.healed.load(std::sync::atomic::Ordering::Relaxed), + received_healed: self.received_healed.load(std::sync::atomic::Ordering::Relaxed), + damaged: self.cubes.load(std::sync::atomic::Ordering::Relaxed), + received_damaged: self.received_cubes.load(std::sync::atomic::Ordering::Relaxed), + crystals: self.crystals.load(std::sync::atomic::Ordering::Relaxed), + total: self.generic_score(), + } + } } #[repr(u8)] @@ -521,6 +540,9 @@ impl GenericGamemodeEngine { ).await); log::debug!("User {} is validated to play game {}", new_user.user.user_id(), game_guid); let new_user = std::sync::Arc::new(new_user); + if let Err(e) = new_user.user.save_player_connected_status(self.game_guid(), true).await { + log::error!("Failed to mark player {} (user {}) as connected to game {}: {}", id, user_id, self.game_guid(), e); + } let mut users = self.users.write().await; users.insert(id, new_user.clone()); for fake_id in new_user.aliases.iter() { @@ -558,17 +580,16 @@ impl GenericGamemodeEngine { } } let is_game_complete = self.is_complete.load(std::sync::atomic::Ordering::Relaxed); - for disconnecter in disconnecting_players { - if !is_game_complete { - self.broadcast( - rlnl::event_code::NetworkEvent::OnAnotherClientDisconnected, - literustlib::packet::Property::ReliableOrdered, - &rlnl::events::ingame::PlayerId { player: disconnecter }, - true, - ).await; - } - } if is_game_complete { + // save score to database as they disconnect + // this happens before they return to the main menu + let scores = user_info.counters.as_core(); + match conn.user.update_game_score(self.game_guid(), scores).await { + Ok(score_id) => user_info.counters.score_id.store(score_id as i64, std::sync::atomic::Ordering::Relaxed), + Err(e) => { + log::warn!("Failed to save score for player {} (user {}) after end of game {}: {}", player_id, user_id, self.game_guid(), e); + }, + } // in every other case this packet would've already been sent // this makes the end-of-match "continue" button send you back to the main menu a bit sooner // (otherwise it waits for the multiplayer server to disconnect via timeout) @@ -577,6 +598,15 @@ impl GenericGamemodeEngine { literustlib::packet::Property::ReliableOrdered, &conn.connection.connection, ).await); + } else { + for disconnecter in disconnecting_players { + self.broadcast( + rlnl::event_code::NetworkEvent::OnAnotherClientDisconnected, + literustlib::packet::Property::ReliableOrdered, + &rlnl::events::ingame::PlayerId { player: disconnecter }, + true, + ).await; + } } let mut has_active_connections = false; @@ -594,6 +624,9 @@ impl GenericGamemodeEngine { } } } + if let Err(e) = conn.user.save_player_connected_status(self.game_guid(), false).await { + log::error!("Failed to mark player {} (user {}) as disconnected in game {}: {}", player_id, user_id, self.game_guid(), e); + } conn.connection.connection.goodbye(&conn.connection.sender).await; return has_active_connections; } diff --git a/rc_services_room/src/main.rs b/rc_services_room/src/main.rs index 00edef9..b5acac1 100644 --- a/rc_services_room/src/main.rs +++ b/rc_services_room/src/main.rs @@ -49,6 +49,7 @@ async fn main() -> std::io::Result<()> { let start_time = chrono::Utc::now(); START_TIMESTAMP_S.store(start_time.timestamp(), std::sync::atomic::Ordering::Relaxed); + log::info!("services_room ready"); if args.once { log::warn!("Handling first connection and then exiting"); let (socket, address) = listener.accept().await?; diff --git a/rc_services_room/src/operations/balance_info.rs b/rc_services_room/src/operations/balance_info.rs index e7fbe95..0d6bfc3 100644 --- a/rc_services_room/src/operations/balance_info.rs +++ b/rc_services_room/src/operations/balance_info.rs @@ -1,14 +1,36 @@ -use polariton_server::operations::SimpleFunc; +use polariton_server::operations::{SimpleOpError, SimpleOperation, SimpleOpImpl}; use polariton::operation::{ParameterTable, Typed}; const FREE_BALANCE_PARAM_KEY: u8 = 74; const PAID_BALANCE_PARAM_KEY: u8 = 87; -pub(super) fn balance_wallet_provider() -> SimpleFunc<66, crate::UserTy, impl (Fn(ParameterTable, &crate::UserTy) -> Result) + Sync + Sync> { - SimpleFunc::new(|params, _| { - let mut params = params.to_dict(); - params.insert(FREE_BALANCE_PARAM_KEY, Typed::Long(31_337_000)); - params.insert(PAID_BALANCE_PARAM_KEY, Typed::Long(1)); - Ok(params.into()) - }) +const CODE: u8 = 66; + +pub(super) struct WalletBallancer; + +#[async_trait::async_trait] +impl SimpleOperation for WalletBallancer { + type User = crate::UserTy; + const CODE: u8 = CODE; + + async fn handle(&self, _params: ParameterTable, user: &Self::User) -> Result, SimpleOpError> { + let mut params = ParameterTable::with_capacity(3); + let user_info = user.user()?; + let free = user_info.currency( + oj_rc_core::persist::user::CurrencyType::Free, + oj_rc_core::persist::user::CurrencyOp::Get, + ).await?; + let paid = user_info.currency( + oj_rc_core::persist::user::CurrencyType::Paid, + oj_rc_core::persist::user::CurrencyOp::Get, + ).await?; + params.insert(FREE_BALANCE_PARAM_KEY, Typed::Long(free as _)); + params.insert(PAID_BALANCE_PARAM_KEY, Typed::Long(paid as _)); + Ok(params) + } } + +pub(super) fn balance_wallet_provider() -> SimpleOpImpl { + SimpleOpImpl::new(WalletBallancer) +} + diff --git a/rc_services_room/src/operations/player_level.rs b/rc_services_room/src/operations/player_level.rs index 9c10ae5..6ffa62a 100644 --- a/rc_services_room/src/operations/player_level.rs +++ b/rc_services_room/src/operations/player_level.rs @@ -10,8 +10,12 @@ pub(super) fn player_level_info_provider() -> SimpleFunc<3, crate::UserTy, impl key_ty: TypePrefix::Int, // int val_ty: TypePrefix::Int, // int items: vec![ - (Typed::Int(0), Typed::Int(99)), - (Typed::Int(10_000), Typed::Int(99_000)), + // FIXME load this from config + // these are interpolated by the client + (Typed::Int(0), Typed::Int(0)), + // this should interpolate to display a level of 1337 + (Typed::Int(2674), Typed::Int(1)), + (Typed::Int(10_000), Typed::Int(i32::MAX / 2)), ] })); Ok(params.into()) }) diff --git a/rc_services_room/src/operations/tech_points.rs b/rc_services_room/src/operations/tech_points.rs index 16063a2..d749f23 100644 --- a/rc_services_room/src/operations/tech_points.rs +++ b/rc_services_room/src/operations/tech_points.rs @@ -1,16 +1,34 @@ use polariton_server::operations::SimpleFunc; use polariton::operation::{ParameterTable, Typed}; +use polariton_server::operations::{SimpleOpError, SimpleOperation, SimpleOpImpl}; +const CURRENT_CODE: u8 = 187; const CURRENT_PARAM_KEY: u8 = 214; -pub(super) fn tech_points_provider() -> SimpleFunc<187, crate::UserTy, impl (Fn(ParameterTable, &crate::UserTy) -> Result) + Sync + Sync> { - SimpleFunc::new(|params, _| { - let mut params = params.to_dict(); - params.insert(CURRENT_PARAM_KEY, Typed::Int(1337)); - Ok(params.into()) - }) +pub(super) struct CurrentTechPointers; + +#[async_trait::async_trait] +impl SimpleOperation for CurrentTechPointers { + type User = crate::UserTy; + const CODE: u8 = CURRENT_CODE; + + async fn handle(&self, _params: ParameterTable, user: &Self::User) -> Result, SimpleOpError> { + let mut table = ParameterTable::with_capacity(2); + let user_info = user.user()?; + let exp = user_info.currency( + oj_rc_core::persist::user::CurrencyType::TechPoints, + oj_rc_core::persist::user::CurrencyOp::Get, + ).await?; + table.insert(CURRENT_PARAM_KEY, Typed::Int(exp as _)); + Ok(table) + } } +pub(super) fn tech_points_provider() -> SimpleOpImpl { + SimpleOpImpl::new(CurrentTechPointers) +} + + const UNCLAIMED_PARAM_KEY: u8 = 212; pub(super) fn tech_points_awards_provider() -> SimpleFunc<185, crate::UserTy, impl (Fn(ParameterTable, &crate::UserTy) -> Result) + Sync + Sync> { diff --git a/rc_services_room/src/operations/user_xp.rs b/rc_services_room/src/operations/user_xp.rs index 3b356fa..0980536 100644 --- a/rc_services_room/src/operations/user_xp.rs +++ b/rc_services_room/src/operations/user_xp.rs @@ -1,12 +1,29 @@ -use polariton_server::operations::SimpleFunc; +use polariton_server::operations::{SimpleOpError, SimpleOperation, SimpleOpImpl}; use polariton::operation::{ParameterTable, Typed}; +const CODE: u8 = 83; + const PARAM_KEY: u8 = 8; -pub(super) fn get_user_xp_provider() -> SimpleFunc<83, crate::UserTy, impl (Fn(ParameterTable, &crate::UserTy) -> Result) + Sync + Sync> { - SimpleFunc::new(|params, _| { - let mut params = params.to_dict(); - params.insert(PARAM_KEY, Typed::Int(31337)); - Ok(params.into()) - }) +pub(super) struct UserTotalExperiencer; + +#[async_trait::async_trait] +impl SimpleOperation for UserTotalExperiencer { + type User = crate::UserTy; + const CODE: u8 = CODE; + + async fn handle(&self, _params: ParameterTable, user: &Self::User) -> Result, SimpleOpError> { + let mut table = ParameterTable::with_capacity(2); + let user_info = user.user()?; + let exp = user_info.currency( + oj_rc_core::persist::user::CurrencyType::Experience, + oj_rc_core::persist::user::CurrencyOp::Get, + ).await?; + table.insert(PARAM_KEY, Typed::Int(exp as _)); + Ok(table) + } +} + +pub(super) fn get_user_xp_provider() -> SimpleOpImpl { + SimpleOpImpl::new(UserTotalExperiencer) } diff --git a/rc_social_room/src/data/clan_invite.rs b/rc_social_room/src/data/clan_invite.rs index bf13510..f1ac443 100644 --- a/rc_social_room/src/data/clan_invite.rs +++ b/rc_social_room/src/data/clan_invite.rs @@ -1,3 +1,4 @@ +#![allow(unused)] use polariton::operation::Typed; pub struct ClanInviteInfo { diff --git a/rc_social_room/src/operations/clan_invite.rs b/rc_social_room/src/operations/clan_invite.rs index e6fc2db..54c0864 100644 --- a/rc_social_room/src/operations/clan_invite.rs +++ b/rc_social_room/src/operations/clan_invite.rs @@ -1,7 +1,7 @@ use polariton_server::operations::SimpleFunc; use polariton::operation::{ParameterTable, Typed, Arr}; -use crate::data::clan_invite::*; +//use crate::data::clan_invite::*; const PARAM_KEY: u8 = 42; @@ -10,7 +10,7 @@ pub(super) fn clan_invites_provider() -> SimpleFunc<39, crate::U let mut params = params.to_dict(); params.insert(PARAM_KEY, Typed::::Arr(Arr { ty: polariton::serdes::TypePrefix::HashMap, // hashmap - items: vec![ + /*items: vec![ ClanInviteInfo { username: "RE_user1".to_owned(), display_name: "RE_user1".to_owned(), @@ -19,7 +19,8 @@ pub(super) fn clan_invites_provider() -> SimpleFunc<39, crate::U use_custom_avatar: false, avatar_id: 0, }.as_transmissible() - ], + ],*/ + items: vec![], })); Ok(params.into()) }) diff --git a/rc_social_room/src/operations/mod.rs b/rc_social_room/src/operations/mod.rs index b05eb0e..2df2601 100644 --- a/rc_social_room/src/operations/mod.rs +++ b/rc_social_room/src/operations/mod.rs @@ -8,6 +8,8 @@ mod season_rewards; mod previous_battle_rewards; mod platoon_data; mod calculate_mmr; +mod previous_battle_rewards_get; +mod previous_battle_rewards_claim; use polariton_server::operations::OperationsHandler; @@ -31,4 +33,6 @@ pub fn handler() -> OperationsHandler::default()) // save social settings, sent on escape menu settings save (should probably be saved someday...) .add(polariton_server::operations::Ack::<0, _>::default()) // send friend request, can be sent from match leaderboard + .add(previous_battle_rewards_get::get_battle_rewards_provider()) + .add(previous_battle_rewards_claim::claim_battle_rewards_provider()) } diff --git a/rc_social_room/src/operations/previous_battle_rewards.rs b/rc_social_room/src/operations/previous_battle_rewards.rs index 8574409..da15d25 100644 --- a/rc_social_room/src/operations/previous_battle_rewards.rs +++ b/rc_social_room/src/operations/previous_battle_rewards.rs @@ -1,13 +1,27 @@ -use polariton_server::operations::SimpleFunc; +use polariton_server::operations::{SimpleOpError, SimpleOperation, SimpleOpImpl}; use polariton::operation::{ParameterTable, Typed}; -const PARAM_KEY: u8 = 60; -//const USER_PARAM_KEY: u8 = 1; // str (username) +const CODE: u8 = 54; -pub(super) fn pending_battle_rewards_provider() -> SimpleFunc<54, crate::UserTy, impl (Fn(ParameterTable, &crate::UserTy) -> Result, i16>) + Sync + Sync, C> { - SimpleFunc::new(|params, _| { - let mut params = params.to_dict(); - params.insert(PARAM_KEY, Typed::Bool(false)); - Ok(params.into()) - }) +// const USERNAME_PARAM_KEY: u8 = 1; // in; string +const PARAM_KEY: u8 = 60; + +pub(super) struct PreviousBattleRewarder; + +#[async_trait::async_trait] +impl SimpleOperation for PreviousBattleRewarder { + type User = crate::UserTy; + const CODE: u8 = CODE; + + async fn handle(&self, _params: ParameterTable, user: &Self::User) -> Result, SimpleOpError> { + let mut table = ParameterTable::with_capacity(2); + let user_info = user.user()?; + table.insert(PARAM_KEY, Typed::Bool(user_info.has_unclaimed_match_rewards().await?)); + Ok(table) + } } + +pub(super) fn pending_battle_rewards_provider() -> SimpleOpImpl { + SimpleOpImpl::new(PreviousBattleRewarder) +} + diff --git a/rc_social_room/src/operations/previous_battle_rewards_claim.rs b/rc_social_room/src/operations/previous_battle_rewards_claim.rs new file mode 100644 index 0000000..b0a6e36 --- /dev/null +++ b/rc_social_room/src/operations/previous_battle_rewards_claim.rs @@ -0,0 +1,28 @@ +use polariton_server::operations::{SimpleOpError, SimpleOperation, SimpleOpImpl}; +use polariton::operation::{ParameterTable, Typed}; + +const CODE: u8 = 55; + +// const USERNAME_PARAM_KEY: u8 = 1; // in; string +const PARAM_KEY: u8 = 60; + +pub(super) struct ClaimPreviousBattleRewards; + +#[async_trait::async_trait] +impl SimpleOperation for ClaimPreviousBattleRewards { + type User = crate::UserTy; + const CODE: u8 = CODE; + + async fn handle(&self, _params: ParameterTable, user: &Self::User) -> Result, SimpleOpError> { + let mut table = ParameterTable::with_capacity(2); + let user_info = user.user()?; + let is_success = user_info.claim_match_rewards().await?; + table.insert(PARAM_KEY, Typed::Bool(!is_success || user_info.has_unclaimed_match_rewards().await?)); + Ok(table) + } +} + +pub(super) fn claim_battle_rewards_provider() -> SimpleOpImpl { + SimpleOpImpl::new(ClaimPreviousBattleRewards) +} + diff --git a/rc_social_room/src/operations/previous_battle_rewards_get.rs b/rc_social_room/src/operations/previous_battle_rewards_get.rs new file mode 100644 index 0000000..c8078e3 --- /dev/null +++ b/rc_social_room/src/operations/previous_battle_rewards_get.rs @@ -0,0 +1,49 @@ +use polariton_server::operations::{SimpleOpError, SimpleOperation, SimpleOpImpl}; +use polariton::operation::{ParameterTable, Typed}; + +const CODE: u8 = 53; + +// const USERNAME_PARAM_KEY: u8 = 1; // in; string +//const PARAM_KEY: u8 = 60; +const NEW_SEASON_XP_PARAM_KEY: u8 = 57; // int; out +const XP_AWARD_BASE_PARAM_KEY: u8 = 58; // int; out +const XP_AWARD_PREMIUM_PARAM_KEY: u8 = 59; // int; out +const XP_AWARD_PARTY_PARAM_KEY: u8 = 61; // int; out +const XP_AWARD_TIER_PARAM_KEY: u8 = 52; // int; out +const ROBITS_TOTAL_PARAM_KEY: u8 = 50; // int; out +const AVERAGE_XP_PARAM_KEY: u8 = 54; // int; out +const CLAN_TOTAL_XP_PARAM_KEY: u8 = 55; // int; out +const ROBITS_PARAM_KEY: u8 = 68; // int; out +const PREMIUM_ROBITS_PARAM_KEY: u8 = 69; // int; out +const LONG_PLAY_MULTIPLIER_PARAM_KEY: u8 = 72; // float; out + +pub(super) struct GetPreviousBattleRewards; + +#[async_trait::async_trait] +impl SimpleOperation for GetPreviousBattleRewards { + type User = crate::UserTy; + const CODE: u8 = CODE; + + async fn handle(&self, _params: ParameterTable, user: &Self::User) -> Result, SimpleOpError> { + let mut table = ParameterTable::with_capacity(12); + let user_info = user.user()?; + let next_rewards = user_info.get_unclaimed_match_rewards().await?; + table.insert(NEW_SEASON_XP_PARAM_KEY, Typed::Int(next_rewards.season_experience)); + table.insert(XP_AWARD_BASE_PARAM_KEY, Typed::Int(next_rewards.experience_award_base)); + table.insert(XP_AWARD_PREMIUM_PARAM_KEY, Typed::Int(next_rewards.experience_award_premium)); + table.insert(XP_AWARD_PARTY_PARAM_KEY, Typed::Int(next_rewards.experience_award_party)); + table.insert(XP_AWARD_TIER_PARAM_KEY, Typed::Int(next_rewards.experience_award_tier)); + table.insert(ROBITS_TOTAL_PARAM_KEY, Typed::Int(next_rewards.robits_total)); + table.insert(AVERAGE_XP_PARAM_KEY, Typed::Int(next_rewards.average_experience)); + table.insert(CLAN_TOTAL_XP_PARAM_KEY, Typed::Int(next_rewards.clan_experience)); + table.insert(ROBITS_PARAM_KEY, Typed::Int(next_rewards.robits_earned)); + table.insert(PREMIUM_ROBITS_PARAM_KEY, Typed::Int(next_rewards.premium_robits_earned)); + table.insert(LONG_PLAY_MULTIPLIER_PARAM_KEY, Typed::Float(1.0)); + Ok(table) + } +} + +pub(super) fn get_battle_rewards_provider() -> SimpleOpImpl { + SimpleOpImpl::new(GetPreviousBattleRewards) +} +