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

Add singleplayer and campaign rewards screens #70

This commit is contained in:
NG (Graham)
2026-01-01 08:07:32 -05:00
parent 4ec64a9cd9
commit 782d28efec
11 changed files with 375 additions and 5 deletions

View File

@@ -107,7 +107,7 @@ impl GameMode {
}
#[inline]
pub fn from_persist(mode: crate::persist::config::GameType) -> Self {
pub(crate) fn from_persist(mode: crate::persist::config::GameType) -> Self {
match mode {
crate::persist::config::GameType::BattleArena => Self::BattleArena,
crate::persist::config::GameType::SuddenDeath => Self::SuddenDeath,
@@ -144,6 +144,19 @@ impl GameMode {
Self::Campaign => oj_rc_database::schema::multiplayer_game::GameMode::Campaign,
}
}
pub fn from_u8(b: u8) -> Option<Self> {
match b {
0 => Some(Self::BattleArena),
1 => Some(Self::SuddenDeath),
2 => Some(Self::Pit),
3 => Some(Self::TestMode),
4 => Some(Self::SinglePlayer),
5 => Some(Self::TeamDeathmatch),
6 => Some(Self::Campaign),
_ => None,
}
}
}
#[repr(u8)]

View File

@@ -0,0 +1,172 @@
pub struct GameResult {
pub mode: super::game_mode::GameMode,
pub is_custom: bool,
pub winners: Vec<PlayerAward>,
pub losers: Vec<PlayerAward>,
}
impl GameResult {
pub fn parse(r: &mut dyn std::io::Read) -> std::io::Result<Self> {
let mut bytes_buf = [0u8; 2];
r.read_exact(&mut bytes_buf)?;
let mode = super::game_mode::GameMode::from_u8(bytes_buf[0])
.ok_or_else(|| std::io::Error::new(std::io::ErrorKind::InvalidData, format!("Invalid game mode {}", bytes_buf[0])))?;
let is_custom = bytes_buf[1] != 0;
let mut winners_count_buf = [0u8; 4];
r.read_exact(&mut winners_count_buf)?;
let winners_count = i32::from_le_bytes(winners_count_buf);
let mut winners = Vec::with_capacity(winners_count as _);
for _ in 0..winners_count {
let award = PlayerAward::parse(r)?;
winners.push(award);
}
let mut losers_count_buf = [0u8; 4];
r.read_exact(&mut losers_count_buf)?;
let losers_count = i32::from_le_bytes(losers_count_buf);
let mut losers = Vec::with_capacity(losers_count as _);
for _ in 0..losers_count {
let award = PlayerAward::parse(r)?;
losers.push(award);
}
Ok(Self {
mode,
is_custom,
winners,
losers,
})
}
}
pub struct PlayerAward {
pub player_name: String,
pub mastery: u8,
pub is_alive: bool,
pub is_disconnected: bool,
pub party_members: u8,
pub score: i32,
pub score_position: u8,
pub score_position_in_team: u8,
pub awards: std::collections::HashMap<PlayerAwardId, i32>,
pub weapons: Vec<WeaponUsage>,
pub players_died_before: u8,
}
impl PlayerAward {
pub fn parse(r: &mut dyn std::io::Read) -> std::io::Result<Self> {
let player_name = super::read_str_for_binwriter(r)?;
let mut bytes_buf = [0u8; 4];
r.read_exact(&mut bytes_buf)?;
let mastery = bytes_buf[0];
let is_alive = bytes_buf[1] != 0;
let is_disconnected = bytes_buf[2] != 0;
let party_members = bytes_buf[3];
let mut score_buf = [0u8; 4];
r.read_exact(&mut score_buf)?;
let score = i32::from_le_bytes(score_buf);
let mut bytes_buf = [0u8; 3];
r.read_exact(&mut bytes_buf)?;
let score_position = bytes_buf[0];
let score_position_in_team = bytes_buf[1];
let awards_count = bytes_buf[2];
let mut awards = std::collections::HashMap::with_capacity(awards_count as _);
for _ in 0..awards_count {
let mut bytes_buf = [0u8; 1];
r.read_exact(&mut bytes_buf)?;
let key = PlayerAwardId::from_u8(bytes_buf[0])
.ok_or_else(|| std::io::Error::new(std::io::ErrorKind::InvalidData, format!("Invalid player award {}", bytes_buf[0])))?;
let mut value_buf = [0u8; 4];
r.read_exact(&mut value_buf)?;
let value = i32::from_le_bytes(value_buf);
awards.insert(key, value);
}
let mut bytes_buf = [0u8; 1];
r.read_exact(&mut bytes_buf)?;
let weapons_count = bytes_buf[0];
let mut weapons = Vec::with_capacity(weapons_count as _);
for _ in 0..weapons_count {
let weapon = WeaponUsage::parse(r)?;
weapons.push(weapon);
}
let mut bytes_buf = [0u8; 1];
r.read_exact(&mut bytes_buf)?;
let players_died_before = bytes_buf[0];
Ok(Self {
player_name,
mastery,
is_alive,
is_disconnected,
party_members,
score,
score_position,
score_position_in_team,
awards,
weapons,
players_died_before,
})
}
}
#[repr(u8)]
#[derive(Hash, Eq, PartialEq, Clone, Copy)]
pub enum PlayerAwardId {
DestroyedCubes = 1,
DestroyedCubesInProtection = 2,
DestroyedCubesDefendingBase = 3,
Kill = 4,
KillAssist = 5,
HealCubes = 6,
HealAssist = 7,
// why are there gaps?
Score = 11,
CompletionBonus = 17,
}
impl PlayerAwardId {
pub fn from_u8(b: u8) -> Option<Self> {
match b {
1 => Some(Self::DestroyedCubes),
2 => Some(Self::DestroyedCubesInProtection),
3 => Some(Self::DestroyedCubesDefendingBase),
4 => Some(Self::Kill),
5 => Some(Self::KillAssist),
6 => Some(Self::HealCubes),
7 => Some(Self::HealAssist),
11 => Some(Self::Score),
17 => Some(Self::CompletionBonus),
_ => None,
}
}
pub fn as_u8(&self) -> u8 {
*self as u8
}
}
pub struct WeaponUsage {
pub category: super::weapon_list::ItemCategory,
pub size: super::cube_list::ItemTier,
pub usage_ratio: f32,
}
impl WeaponUsage {
pub fn parse(r: &mut dyn std::io::Read) -> std::io::Result<Self> {
let mut category_buf = [0u8; 4];
r.read_exact(&mut category_buf)?;
let category_num = i32::from_le_bytes(category_buf);
let category = super::weapon_list::ItemCategory::from_smaller(category_num)
.ok_or_else(|| std::io::Error::new(std::io::ErrorKind::InvalidData, format!("Invalid item category {}", category_num)))?;
let mut size_buf = [0u8; 4];
r.read_exact(&mut size_buf)?;
let size_num = i32::from_le_bytes(size_buf);
let size = super::cube_list::ItemTier::from_u32(size_num as _)
.ok_or_else(|| std::io::Error::new(std::io::ErrorKind::InvalidData, format!("Invalid item size/tier {}", size_num)))?;
let mut ratio_buf = [0u8; 4];
r.read_exact(&mut ratio_buf)?;
let usage_ratio = f32::from_le_bytes(ratio_buf);
Ok(Self {
category,
size,
usage_ratio,
})
}
}

View File

@@ -16,6 +16,7 @@ pub mod sanction;
pub mod robot_data;
pub mod lobby;
pub mod battle_arena_config;
pub mod game_result;
pub mod error_codes;

View File

@@ -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, PlayerScore, MultiplayerError, MultiplayerErrorCode, PlayerDescriptor, GameEventSetter, CurrentGameEvent, AuthError, IntercomUser, FakePlayers, ResolvedVehicle, CommonUser, IntercomListener, UserRole, SocialUser, CurrencyType, CurrencyOp, MatchRewards};
pub use traits::{UserProvider, User, UserToken, UserSlots, UserSlotData, VehicleData, UserInfo, UserLoginInfo, ExtraUserInfo, UserAuthenticator, NewSlotData, UserId, RegistrationInfo, VehicleUploadData, ChatUser, AvatarInfo, GetAvatarInfo, ControlData, ControlType, CustomisationData, GetCustomisationData, SetSanction, SanctionType, LobbyUser, GameDescriptor, PlayerLobbyDescriptor, MultiplayerUser, PlayerScore, MultiplayerError, MultiplayerErrorCode, PlayerDescriptor, GameEventSetter, CurrentGameEvent, AuthError, IntercomUser, FakePlayers, ResolvedVehicle, CommonUser, IntercomListener, UserRole, SocialUser, CurrencyType, CurrencyOp, MatchRewards, SingleplayerUser};
pub mod intercom;
pub use intercom::generate_token as generate_intercom_token;
@@ -22,6 +22,7 @@ pub use lobby::TeamChooser;
mod common;
mod chat;
mod social;
mod singleplayer;
pub const TOKEN_SECRET_FILENAME: &str = "token_secret.key";

View File

@@ -0,0 +1,85 @@
use super::account_json::UserData;
#[inline]
fn award_to_active_value(opt: Option<&i32>) -> oj_rc_database::sea_orm::ActiveValue<i32> {
match opt {
Some(t) => oj_rc_database::sea_orm::ActiveValue::Set(*t),
None => oj_rc_database::sea_orm::ActiveValue::Set(0),
}
}
#[async_trait::async_trait]
impl super::SingleplayerUser for UserData {
async fn save_game_result(&self, guid: &str, result: crate::data::game_result::GameResult) -> Result<(), polariton_server::operations::SimpleOpError> {
let now = chrono::Utc::now().timestamp();
// TODO make this a single transaction
let game = self.db.insert_game(oj_rc_database::schema::multiplayer_game::ActiveModel {
id: oj_rc_database::sea_orm::ActiveValue::NotSet,
creation_time: oj_rc_database::sea_orm::ActiveValue::Set(now),
guid: oj_rc_database::sea_orm::ActiveValue::Set(0),
map: oj_rc_database::sea_orm::ActiveValue::Set(format!("singleplayer|guid:{}", guid)), // invalid by design
mode: oj_rc_database::sea_orm::ActiveValue::Set(result.mode.to_db()),
visibility: oj_rc_database::sea_orm::ActiveValue::Set(oj_rc_database::schema::multiplayer_game::MapVisibility::Good),
auto_heal: oj_rc_database::sea_orm::ActiveValue::Set(false),
variant: oj_rc_database::sea_orm::ActiveValue::Set(oj_rc_database::schema::multiplayer_game::GameType::Standard),
is_complete: oj_rc_database::sea_orm::ActiveValue::Set(true),
}).await
.map_err(|e| {
log::error!("Failed to create singleplayer game {} for user {}: {}", guid, self.account.id, e);
polariton_server::operations::SimpleOpError::with_message(
crate::data::error_codes::SingleplayerErrorCode::DatabaseError as i16,
format!("Failed to create singleplayer game {}: {}", guid, e),
)
})?;
let iter = result.winners.iter().map(|p| (0, p)).chain(result.losers.iter().map(|p| (1, p)));
for (player_team, player_result) in iter {
if player_result.player_name != self.account.display_name { continue; }
let player = self.db.insert_player(oj_rc_database::schema::multiplayer_game_player::ActiveModel {
id: oj_rc_database::sea_orm::ActiveValue::NotSet,
user_id: oj_rc_database::sea_orm::ActiveValue::Set(Some(self.account.id)),
game_id: oj_rc_database::sea_orm::ActiveValue::Set(game.id),
creation_time: oj_rc_database::sea_orm::ActiveValue::Set(now),
player_id: oj_rc_database::sea_orm::ActiveValue::Set(0),
team: oj_rc_database::sea_orm::ActiveValue::Set(player_team),
group: oj_rc_database::sea_orm::ActiveValue::Set(None),
is_claimed: oj_rc_database::sea_orm::ActiveValue::Set(true),
public_id: oj_rc_database::sea_orm::ActiveValue::Set("".to_owned()), // no point in adding this info
display_name: oj_rc_database::sea_orm::ActiveValue::Set("".to_owned()),
variant: oj_rc_database::sea_orm::ActiveValue::Set(oj_rc_database::schema::multiplayer_game_player::ClientType::Client),
}).await
.map_err(|e| {
log::error!("Failed to create singleplayer game {} player for user {}: {}", guid, self.account.id, e);
polariton_server::operations::SimpleOpError::with_message(
crate::data::error_codes::SingleplayerErrorCode::DatabaseError as i16,
format!("Failed to create singleplayer game {} player: {}", guid, e),
)
})?;
self.db.insert_score(oj_rc_database::schema::multiplayer_game_score::ActiveModel {
id: oj_rc_database::sea_orm::ActiveValue::NotSet,
player_id: oj_rc_database::sea_orm::ActiveValue::Set(player.id),
creation_time: oj_rc_database::sea_orm::ActiveValue::Set(now),
is_claimed: oj_rc_database::sea_orm::ActiveValue::Set(false),
kills: award_to_active_value(player_result.awards.get(&crate::data::game_result::PlayerAwardId::Kill)),
deaths: oj_rc_database::sea_orm::ActiveValue::Set(0),
assists: award_to_active_value(player_result.awards.get(&crate::data::game_result::PlayerAwardId::KillAssist)),
heal_assists: award_to_active_value(player_result.awards.get(&crate::data::game_result::PlayerAwardId::HealAssist)),
healed: award_to_active_value(player_result.awards.get(&crate::data::game_result::PlayerAwardId::HealCubes)),
received_healed: oj_rc_database::sea_orm::ActiveValue::Set(0),
damaged: award_to_active_value(player_result.awards.get(&crate::data::game_result::PlayerAwardId::DestroyedCubes)),
received_damaged: oj_rc_database::sea_orm::ActiveValue::Set(0),
crystals: oj_rc_database::sea_orm::ActiveValue::Set(0),
total: award_to_active_value(player_result.awards.get(&crate::data::game_result::PlayerAwardId::Score)),
}).await
.map_err(|e| {
log::error!("Failed to create singleplayer game {} score for user {}: {}", guid, self.account.id, e);
polariton_server::operations::SimpleOpError::with_message(
crate::data::error_codes::SingleplayerErrorCode::DatabaseError as i16,
format!("Failed to create singleplayer game {} score: {}", guid, e),
)
})?;
break;
}
Ok(())
}
}

View File

@@ -61,7 +61,7 @@ pub trait UserAuthenticator {
}
#[async_trait::async_trait]
pub trait User<C>: ChatUser + SocialUser + LobbyUser + MultiplayerUser + IntercomUser + CommonUser {
pub trait User<C>: ChatUser + SocialUser + LobbyUser + MultiplayerUser + SingleplayerUser + IntercomUser + CommonUser {
async fn unlocked_parts(&self) -> Vec<u32>;
async fn selected_garage(&self) -> (String, u32);
async fn select_garage(&self, slot: i32) -> Result<(), i16>;
@@ -438,3 +438,9 @@ pub struct MatchRewards {
pub robits_earned: i32,
pub premium_robits_earned: i32,
}
#[async_trait::async_trait]
pub trait SingleplayerUser: Send + Sync {
// regular singleplayer and campaign mode
async fn save_game_result(&self, guid: &str, result: crate::data::game_result::GameResult) -> Result<(), polariton_server::operations::SimpleOpError>;
}