1
0
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:
NG (Graham)
2025-12-28 18:13:14 -05:00
parent 5e05cbdb59
commit 4ec64a9cd9
26 changed files with 802 additions and 48 deletions

View File

@@ -121,3 +121,47 @@ impl AuthErrorCode {
(self as u16).to_string() (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
}

View File

@@ -624,6 +624,57 @@ impl UserData {
} }
Ok(players) 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 const INVALID_ROBOT_ERR: i16 = crate::data::error_codes::WebServicesError::InvalidRobot as i16; // 140

View File

@@ -33,4 +33,11 @@ impl super::CommonUser for UserData {
async fn db_metrics(&self) -> oj_rc_database::DatabaseMetrics { async fn db_metrics(&self) -> oj_rc_database::DatabaseMetrics {
self.db.metrics().await 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),
))
}
} }

View File

@@ -11,7 +11,7 @@ mod inventory;
pub use inventory::UnlockedParts; pub use inventory::UnlockedParts;
mod traits; 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 mod intercom;
pub use intercom::generate_token as generate_intercom_token; pub use intercom::generate_token as generate_intercom_token;
@@ -21,6 +21,7 @@ mod lobby;
pub use lobby::TeamChooser; pub use lobby::TeamChooser;
mod common; mod common;
mod chat; mod chat;
mod social;
pub const TOKEN_SECRET_FILENAME: &str = "token_secret.key"; pub const TOKEN_SECRET_FILENAME: &str = "token_secret.key";

View File

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

View 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)
}
}
}

View File

@@ -61,7 +61,7 @@ pub trait UserAuthenticator {
} }
#[async_trait::async_trait] #[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 unlocked_parts(&self) -> Vec<u32>;
async fn selected_garage(&self) -> (String, u32); async fn selected_garage(&self) -> (String, u32);
async fn select_garage(&self, slot: i32) -> Result<(), i16>; 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 game_players(&self, guid: &str) -> Result<Vec<PlayerDescriptor>, MultiplayerError>;
async fn complete_game(&self, guid: &str) -> Result<(), MultiplayerError>; async fn complete_game(&self, guid: &str) -> Result<(), MultiplayerError>;
async fn game_info(&self, guid: &str) -> Result<Option<GameDescriptor>, 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] #[async_trait::async_trait]
@@ -387,4 +403,38 @@ pub trait CommonUser: Send + Sync {
fn is_royal(&self) -> bool; fn is_royal(&self) -> bool;
fn is_banned(&self) -> bool; fn is_banned(&self) -> bool;
async fn db_metrics(&self) -> oj_rc_database::DatabaseMetrics; 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,
} }

View File

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

View File

@@ -12,6 +12,7 @@ mod m20250713_000002_create_player_table;
mod m20250722_000001_create_game_event_table; mod m20250722_000001_create_game_event_table;
mod m20250816_000001_add_fake_players; mod m20250816_000001_add_fake_players;
mod m20250918_000001_add_player_variant; mod m20250918_000001_add_player_variant;
mod m20251228_000001_create_score_table;
pub struct Migrator; pub struct Migrator;
@@ -31,6 +32,7 @@ impl MigratorTrait for Migrator {
Box::new(m20250722_000001_create_game_event_table::Migration), Box::new(m20250722_000001_create_game_event_table::Migration),
Box::new(m20250816_000001_add_fake_players::Migration), Box::new(m20250816_000001_add_fake_players::Migration),
Box::new(m20250918_000001_add_player_variant::Migration), Box::new(m20250918_000001_add_player_variant::Migration),
Box::new(m20251228_000001_create_score_table::Migration),
] ]
} }
} }

View File

@@ -9,6 +9,7 @@ pub mod sanction;
pub mod multiplayer_game; pub mod multiplayer_game;
pub mod multiplayer_game_player; pub mod multiplayer_game_player;
pub mod game_event; pub mod game_event;
pub mod multiplayer_game_score;
pub fn parse_int_csv(s: &str) -> Vec<u32> { pub fn parse_int_csv(s: &str) -> Vec<u32> {
s.split(',').filter_map(|i_as_s| { s.split(',').filter_map(|i_as_s| {

View File

@@ -31,6 +31,8 @@ pub enum Relation {
to = "super::user::Column::Id" to = "super::user::Column::Id"
)] )]
User, User,
#[sea_orm(has_one = "super::multiplayer_game_score::Entity")]
Score,
} }
impl Related<super::multiplayer_game::Entity> for Entity { impl Related<super::multiplayer_game::Entity> for Entity {
@@ -45,6 +47,12 @@ impl Related<super::user::Entity> for Entity {
} }
} }
impl Related<super::multiplayer_game_score::Entity> for Entity {
fn to() -> RelationDef {
Relation::Score.def()
}
}
impl ActiveModelBehavior for ActiveModel {} impl ActiveModelBehavior for ActiveModel {}
#[derive(Clone, Copy, Debug, PartialEq, Eq, EnumIter, DeriveActiveEnum)] #[derive(Clone, Copy, Debug, PartialEq, Eq, EnumIter, DeriveActiveEnum)]

View File

@@ -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<super::multiplayer_game_player::Entity> for Entity {
fn to() -> RelationDef {
Relation::Player.def()
}
}
impl ActiveModelBehavior for ActiveModel {}

View File

@@ -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<crate::schema::user_aux::ActiveModel>) + Send + 'static) -> Result<Option<crate::schema::user_aux::Model>, 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<Option<crate::schema::permissions::Model>, sea_orm::DbErr> { pub async fn perms_by_user_id(&self, user_id: i32) -> Result<Option<crate::schema::permissions::Model>, sea_orm::DbErr> {
crate::schema::permissions::Entity::find() crate::schema::permissions::Entity::find()
.filter(crate::schema::permissions::Column::UserId.eq(user_id)) .filter(crate::schema::permissions::Column::UserId.eq(user_id))
@@ -404,7 +433,7 @@ impl Database {
.collect()) .collect())
} }
pub async fn players_by_game_id_and_completion(&self, game_id: i32) -> Result<Vec<crate::schema::multiplayer_game_player::Model>, sea_orm::DbErr> { pub async fn players_by_game_id(&self, game_id: i32) -> Result<Vec<crate::schema::multiplayer_game_player::Model>, sea_orm::DbErr> {
Ok(crate::schema::multiplayer_game_player::Entity::find() Ok(crate::schema::multiplayer_game_player::Entity::find()
.find_also_related(crate::schema::multiplayer_game::Entity) .find_also_related(crate::schema::multiplayer_game::Entity)
.filter(crate::schema::multiplayer_game::Column::Id.eq(game_id)) .filter(crate::schema::multiplayer_game::Column::Id.eq(game_id))
@@ -415,11 +444,33 @@ impl Database {
.collect()) .collect())
} }
pub async fn player_by_user_id_and_game_guid(&self, user_id: i32, game_guid: i64) -> Result<Option<crate::schema::multiplayer_game_player::Model>, 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<crate::schema::multiplayer_game_player::ActiveModel>) -> Result<(), sea_orm::DbErr> { pub async fn insert_players(&self, entities: Vec<crate::schema::multiplayer_game_player::ActiveModel>) -> Result<(), sea_orm::DbErr> {
crate::schema::multiplayer_game_player::Entity::insert_many(entities.into_iter()).exec(&self.orm).await?; crate::schema::multiplayer_game_player::Entity::insert_many(entities.into_iter()).exec(&self.orm).await?;
Ok(()) 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<Option<crate::schema::game_event::Model>, sea_orm::DbErr> { pub async fn game_event_at_time(&self, time: i64, variant: crate::schema::game_event::EventVariant) -> Result<Option<crate::schema::game_event::Model>, sea_orm::DbErr> {
crate::schema::game_event::Entity::find() crate::schema::game_event::Entity::find()
.filter(crate::schema::game_event::Column::Start.lte(time)) .filter(crate::schema::game_event::Column::Start.lte(time))
@@ -434,6 +485,53 @@ impl Database {
entity.insert(&self.orm).await entity.insert(&self.orm).await
} }
pub async fn score_by_player_id(&self, player_id: i32) -> Result<Option<crate::schema::multiplayer_game_score::Model>, 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<Option<crate::schema::multiplayer_game_score::Model>, 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<crate::schema::multiplayer_game_score::Model, sea_orm::DbErr> {
entity.insert(&self.orm).await
}
pub async fn update_score(&self, entity: crate::schema::multiplayer_game_score::ActiveModel) -> Result<crate::schema::multiplayer_game_score::Model, sea_orm::DbErr> {
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<u64, 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)
.count(&self.orm)
.await
}
pub async fn metrics(&self) -> super::DatabaseMetrics { pub async fn metrics(&self) -> super::DatabaseMetrics {
self.metrics.lock().unwrap().snapshot() self.metrics.lock().unwrap().snapshot()
} }

View File

@@ -53,6 +53,7 @@ async fn main() -> std::io::Result<()> {
let start_time = chrono::Utc::now(); let start_time = chrono::Utc::now();
START_TIMESTAMP_S.store(start_time.timestamp(), std::sync::atomic::Ordering::Relaxed); START_TIMESTAMP_S.store(start_time.timestamp(), std::sync::atomic::Ordering::Relaxed);
log::info!("lobby_room ready");
if args.once { if args.once {
log::warn!("Handling first connection and then exiting"); log::warn!("Handling first connection and then exiting");
let (socket, address) = listener.accept().await?; let (socket, address) = listener.accept().await?;

View File

@@ -115,6 +115,7 @@ impl Location {
} }
pub(super) struct UserData { pub(super) struct UserData {
pub score_id: std::sync::atomic::AtomicI64,
pub kills: std::sync::atomic::AtomicU32, pub kills: std::sync::atomic::AtomicU32,
pub deaths: std::sync::atomic::AtomicU32, pub deaths: std::sync::atomic::AtomicU32,
pub assists: std::sync::atomic::AtomicU32, pub assists: std::sync::atomic::AtomicU32,
@@ -130,6 +131,7 @@ pub(super) struct UserData {
impl UserData { impl UserData {
fn new() -> Self { fn new() -> Self {
Self { Self {
score_id: std::sync::atomic::AtomicI64::new(i64::MIN),
kills: std::sync::atomic::AtomicU32::new(0), kills: std::sync::atomic::AtomicU32::new(0),
deaths: std::sync::atomic::AtomicU32::new(0), deaths: std::sync::atomic::AtomicU32::new(0),
assists: 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), 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)] #[repr(u8)]
@@ -521,6 +540,9 @@ impl <L: super::CustomGameLogic> GenericGamemodeEngine<L> {
).await); ).await);
log::debug!("User {} is validated to play game {}", new_user.user.user_id(), game_guid); log::debug!("User {} is validated to play game {}", new_user.user.user_id(), game_guid);
let new_user = std::sync::Arc::new(new_user); 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; let mut users = self.users.write().await;
users.insert(id, new_user.clone()); users.insert(id, new_user.clone());
for fake_id in new_user.aliases.iter() { for fake_id in new_user.aliases.iter() {
@@ -558,17 +580,16 @@ impl <L: super::CustomGameLogic> GenericGamemodeEngine<L> {
} }
} }
let is_game_complete = self.is_complete.load(std::sync::atomic::Ordering::Relaxed); 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 { 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 // 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 // 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) // (otherwise it waits for the multiplayer server to disconnect via timeout)
@@ -577,6 +598,15 @@ impl <L: super::CustomGameLogic> GenericGamemodeEngine<L> {
literustlib::packet::Property::ReliableOrdered, literustlib::packet::Property::ReliableOrdered,
&conn.connection.connection, &conn.connection.connection,
).await); ).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; let mut has_active_connections = false;
@@ -594,6 +624,9 @@ impl <L: super::CustomGameLogic> GenericGamemodeEngine<L> {
} }
} }
} }
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; conn.connection.connection.goodbye(&conn.connection.sender).await;
return has_active_connections; return has_active_connections;
} }

View File

@@ -49,6 +49,7 @@ async fn main() -> std::io::Result<()> {
let start_time = chrono::Utc::now(); let start_time = chrono::Utc::now();
START_TIMESTAMP_S.store(start_time.timestamp(), std::sync::atomic::Ordering::Relaxed); START_TIMESTAMP_S.store(start_time.timestamp(), std::sync::atomic::Ordering::Relaxed);
log::info!("services_room ready");
if args.once { if args.once {
log::warn!("Handling first connection and then exiting"); log::warn!("Handling first connection and then exiting");
let (socket, address) = listener.accept().await?; let (socket, address) = listener.accept().await?;

View File

@@ -1,14 +1,36 @@
use polariton_server::operations::SimpleFunc; use polariton_server::operations::{SimpleOpError, SimpleOperation, SimpleOpImpl};
use polariton::operation::{ParameterTable, Typed}; use polariton::operation::{ParameterTable, Typed};
const FREE_BALANCE_PARAM_KEY: u8 = 74; const FREE_BALANCE_PARAM_KEY: u8 = 74;
const PAID_BALANCE_PARAM_KEY: u8 = 87; const PAID_BALANCE_PARAM_KEY: u8 = 87;
pub(super) fn balance_wallet_provider() -> SimpleFunc<66, crate::UserTy, impl (Fn(ParameterTable, &crate::UserTy) -> Result<ParameterTable, i16>) + Sync + Sync> { const CODE: u8 = 66;
SimpleFunc::new(|params, _| {
let mut params = params.to_dict(); pub(super) struct WalletBallancer;
params.insert(FREE_BALANCE_PARAM_KEY, Typed::Long(31_337_000));
params.insert(PAID_BALANCE_PARAM_KEY, Typed::Long(1)); #[async_trait::async_trait]
Ok(params.into()) impl <C: Send + 'static> SimpleOperation<C> for WalletBallancer {
}) type User = crate::UserTy;
const CODE: u8 = CODE;
async fn handle(&self, _params: ParameterTable<C>, user: &Self::User) -> Result<ParameterTable<C>, 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<C: Send + 'static>() -> SimpleOpImpl<C, crate::UserTy, WalletBallancer> {
SimpleOpImpl::new(WalletBallancer)
}

View File

@@ -10,8 +10,12 @@ pub(super) fn player_level_info_provider() -> SimpleFunc<3, crate::UserTy, impl
key_ty: TypePrefix::Int, // int key_ty: TypePrefix::Int, // int
val_ty: TypePrefix::Int, // int val_ty: TypePrefix::Int, // int
items: vec![ items: vec![
(Typed::Int(0), Typed::Int(99)), // FIXME load this from config
(Typed::Int(10_000), Typed::Int(99_000)), // 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()) Ok(params.into())
}) })

View File

@@ -1,16 +1,34 @@
use polariton_server::operations::SimpleFunc; use polariton_server::operations::SimpleFunc;
use polariton::operation::{ParameterTable, Typed}; use polariton::operation::{ParameterTable, Typed};
use polariton_server::operations::{SimpleOpError, SimpleOperation, SimpleOpImpl};
const CURRENT_CODE: u8 = 187;
const CURRENT_PARAM_KEY: u8 = 214; const CURRENT_PARAM_KEY: u8 = 214;
pub(super) fn tech_points_provider() -> SimpleFunc<187, crate::UserTy, impl (Fn(ParameterTable, &crate::UserTy) -> Result<ParameterTable, i16>) + Sync + Sync> { pub(super) struct CurrentTechPointers;
SimpleFunc::new(|params, _| {
let mut params = params.to_dict(); #[async_trait::async_trait]
params.insert(CURRENT_PARAM_KEY, Typed::Int(1337)); impl <C: Send + 'static> SimpleOperation<C> for CurrentTechPointers {
Ok(params.into()) type User = crate::UserTy;
}) const CODE: u8 = CURRENT_CODE;
async fn handle(&self, _params: ParameterTable<C>, user: &Self::User) -> Result<ParameterTable<C>, 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<C: Send + 'static>() -> SimpleOpImpl<C, crate::UserTy, CurrentTechPointers> {
SimpleOpImpl::new(CurrentTechPointers)
}
const UNCLAIMED_PARAM_KEY: u8 = 212; const UNCLAIMED_PARAM_KEY: u8 = 212;
pub(super) fn tech_points_awards_provider() -> SimpleFunc<185, crate::UserTy, impl (Fn(ParameterTable, &crate::UserTy) -> Result<ParameterTable, i16>) + Sync + Sync> { pub(super) fn tech_points_awards_provider() -> SimpleFunc<185, crate::UserTy, impl (Fn(ParameterTable, &crate::UserTy) -> Result<ParameterTable, i16>) + Sync + Sync> {

View File

@@ -1,12 +1,29 @@
use polariton_server::operations::SimpleFunc; use polariton_server::operations::{SimpleOpError, SimpleOperation, SimpleOpImpl};
use polariton::operation::{ParameterTable, Typed}; use polariton::operation::{ParameterTable, Typed};
const CODE: u8 = 83;
const PARAM_KEY: u8 = 8; const PARAM_KEY: u8 = 8;
pub(super) fn get_user_xp_provider() -> SimpleFunc<83, crate::UserTy, impl (Fn(ParameterTable, &crate::UserTy) -> Result<ParameterTable, i16>) + Sync + Sync> { pub(super) struct UserTotalExperiencer;
SimpleFunc::new(|params, _| {
let mut params = params.to_dict(); #[async_trait::async_trait]
params.insert(PARAM_KEY, Typed::Int(31337)); impl <C: Send + 'static> SimpleOperation<C> for UserTotalExperiencer {
Ok(params.into()) type User = crate::UserTy;
}) const CODE: u8 = CODE;
async fn handle(&self, _params: ParameterTable<C>, user: &Self::User) -> Result<ParameterTable<C>, 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<C: Send + 'static>() -> SimpleOpImpl<C, crate::UserTy, UserTotalExperiencer> {
SimpleOpImpl::new(UserTotalExperiencer)
} }

View File

@@ -1,3 +1,4 @@
#![allow(unused)]
use polariton::operation::Typed; use polariton::operation::Typed;
pub struct ClanInviteInfo { pub struct ClanInviteInfo {

View File

@@ -1,7 +1,7 @@
use polariton_server::operations::SimpleFunc; use polariton_server::operations::SimpleFunc;
use polariton::operation::{ParameterTable, Typed, Arr}; use polariton::operation::{ParameterTable, Typed, Arr};
use crate::data::clan_invite::*; //use crate::data::clan_invite::*;
const PARAM_KEY: u8 = 42; const PARAM_KEY: u8 = 42;
@@ -10,7 +10,7 @@ pub(super) fn clan_invites_provider<C: Send + Sync>() -> SimpleFunc<39, crate::U
let mut params = params.to_dict(); let mut params = params.to_dict();
params.insert(PARAM_KEY, Typed::<C>::Arr(Arr { params.insert(PARAM_KEY, Typed::<C>::Arr(Arr {
ty: polariton::serdes::TypePrefix::HashMap, // hashmap ty: polariton::serdes::TypePrefix::HashMap, // hashmap
items: vec![ /*items: vec![
ClanInviteInfo { ClanInviteInfo {
username: "RE_user1".to_owned(), username: "RE_user1".to_owned(),
display_name: "RE_user1".to_owned(), display_name: "RE_user1".to_owned(),
@@ -19,7 +19,8 @@ pub(super) fn clan_invites_provider<C: Send + Sync>() -> SimpleFunc<39, crate::U
use_custom_avatar: false, use_custom_avatar: false,
avatar_id: 0, avatar_id: 0,
}.as_transmissible() }.as_transmissible()
], ],*/
items: vec![],
})); }));
Ok(params.into()) Ok(params.into())
}) })

View File

@@ -8,6 +8,8 @@ mod season_rewards;
mod previous_battle_rewards; mod previous_battle_rewards;
mod platoon_data; mod platoon_data;
mod calculate_mmr; mod calculate_mmr;
mod previous_battle_rewards_get;
mod previous_battle_rewards_claim;
use polariton_server::operations::OperationsHandler; use polariton_server::operations::OperationsHandler;
@@ -31,4 +33,6 @@ pub fn handler() -> OperationsHandler<crate::UserTy, crate::data::custom::Custom
.add(calculate_mmr::mmr_provider()) .add(calculate_mmr::mmr_provider())
.add(polariton_server::operations::Ack::<25, _>::default()) // save social settings, sent on escape menu settings save (should probably be saved someday...) .add(polariton_server::operations::Ack::<25, _>::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(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())
} }

View File

@@ -1,13 +1,27 @@
use polariton_server::operations::SimpleFunc; use polariton_server::operations::{SimpleOpError, SimpleOperation, SimpleOpImpl};
use polariton::operation::{ParameterTable, Typed}; use polariton::operation::{ParameterTable, Typed};
const PARAM_KEY: u8 = 60; const CODE: u8 = 54;
//const USER_PARAM_KEY: u8 = 1; // str (username)
pub(super) fn pending_battle_rewards_provider<C: Send + Sync>() -> SimpleFunc<54, crate::UserTy, impl (Fn(ParameterTable<C>, &crate::UserTy) -> Result<ParameterTable<C>, i16>) + Sync + Sync, C> { // const USERNAME_PARAM_KEY: u8 = 1; // in; string
SimpleFunc::new(|params, _| { const PARAM_KEY: u8 = 60;
let mut params = params.to_dict();
params.insert(PARAM_KEY, Typed::Bool(false)); pub(super) struct PreviousBattleRewarder;
Ok(params.into())
}) #[async_trait::async_trait]
impl <C: Send + 'static> SimpleOperation<C> for PreviousBattleRewarder {
type User = crate::UserTy;
const CODE: u8 = CODE;
async fn handle(&self, _params: ParameterTable<C>, user: &Self::User) -> Result<ParameterTable<C>, 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<C: Send + 'static>() -> SimpleOpImpl<C, crate::UserTy, PreviousBattleRewarder> {
SimpleOpImpl::new(PreviousBattleRewarder)
}

View File

@@ -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 <C: Send + 'static> SimpleOperation<C> for ClaimPreviousBattleRewards {
type User = crate::UserTy;
const CODE: u8 = CODE;
async fn handle(&self, _params: ParameterTable<C>, user: &Self::User) -> Result<ParameterTable<C>, 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<C: Send + 'static>() -> SimpleOpImpl<C, crate::UserTy, ClaimPreviousBattleRewards> {
SimpleOpImpl::new(ClaimPreviousBattleRewards)
}

View File

@@ -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 <C: Send + 'static> SimpleOperation<C> for GetPreviousBattleRewards {
type User = crate::UserTy;
const CODE: u8 = CODE;
async fn handle(&self, _params: ParameterTable<C>, user: &Self::User) -> Result<ParameterTable<C>, 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<C: Send + 'static>() -> SimpleOpImpl<C, crate::UserTy, GetPreviousBattleRewards> {
SimpleOpImpl::new(GetPreviousBattleRewards)
}