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

Implement lobby parts of custom games #38

This commit is contained in:
NG (Graham)
2026-03-17 21:52:15 -04:00
parent af04c4093c
commit 6a953bc613
32 changed files with 1045 additions and 61 deletions

View File

@@ -13,8 +13,9 @@ pub struct AccountProvider {
domain: std::sync::Arc<String>,
cdn: std::sync::Arc<String>,
auth: std::sync::Arc<String>,
intercom: std::sync::Arc<String>,
secret: std::sync::Arc<Vec<u8>>,
pub(super) intercom: std::sync::Arc<String>,
pub(super) intercom_http_client: std::sync::Arc<reqwest::Client>,
pub(super) secret: std::sync::Arc<Vec<u8>>,
db: std::sync::Arc<oj_rc_database::Database>,
}
@@ -38,6 +39,7 @@ impl AccountProvider {
cdn: std::sync::Arc::new(server_settings.cdn_url),
auth: std::sync::Arc::new(server_settings.auth_url),
intercom: std::sync::Arc::new(server_settings.intercom_url),
intercom_http_client: std::sync::Arc::new(reqwest::Client::new()),
secret: std::sync::Arc::new(secret),
db: std::sync::Arc::new(db),
})

View File

@@ -116,6 +116,13 @@ impl super::IntercomUser for super::account_json::UserData {
}
}
async fn update_custom_game(&self, msg: IntercomLobbyCustomGameDataMessage) {
let data = IntercomLobbyStateMessage::CustomGame(msg);
if let Err(e) = self.post_to_intercom(&data, ".oj_lobby", "state").await {
log::error!("Failed to send intercom custom game state message: {}", e);
}
}
async fn update_status(&self, server_name: &str, msg: oj_serdes::ServerStatus) {
if let Err(e) = self.post_to_intercom(&msg, ".status", server_name).await {
log::error!("Failed to send intercom status message: {}", e);
@@ -123,6 +130,91 @@ impl super::IntercomUser for super::account_json::UserData {
}
}
#[derive(Serialize, Deserialize, Clone, Debug)]
#[serde(tag = "state")]
pub enum IntercomLobbyStateMessage {
CustomGame(IntercomLobbyCustomGameDataMessage),
}
#[derive(Serialize, Deserialize, Clone, Debug)]
pub struct IntercomLobbyCustomGameDataMessage {
pub session_id: String,
pub config: IntercomLobbyCustomGameConfig,
pub users: Vec<IntercomLobbyCustomGameUserData>,
}
#[derive(Serialize, Deserialize, Clone, Debug)]
pub enum CustomGameMode {
BattleArena,
TeamDeathmatch,
Pit,
SuddenDeath,
}
#[derive(Serialize, Deserialize, Clone, Debug, Copy)]
pub enum CustomGameVisibility {
Good,
Poor,
Bad,
}
#[derive(Serialize, Deserialize, Clone, Debug)]
pub struct IntercomLobbyCustomGameConfig {
pub game_mode: CustomGameMode,
pub map: String,
pub map_visibility: CustomGameVisibility,
pub health_regen: bool,
pub capture_segment_memory: bool,
pub base_shields_go_down: bool,
pub damage_mult: i32,
pub health_mult: i32,
pub power_mult: i32,
pub game_time: i32, // minutes
pub capture_speed: i32, // seconds
pub points_kill_streak: bool,
pub points_total_required: i32,
pub number_of_kills_to_win: i32,
pub respawn_time: i32,
pub core_appear_frequency: i32,
pub core_health_multiplier: i32,
pub core_destroy_time: i32,
pub protonium_harvest: i32,
pub ceiling_multiplier: i32,
pub min_cpu: i32,
pub max_cpu: i32,
}
impl IntercomLobbyCustomGameConfig {
pub fn as_core(&self) -> super::GameOverrides {
super::GameOverrides {
capture_segment_memory: self.capture_segment_memory,
base_shields_go_down: self.base_shields_go_down,
damage_mult: self.damage_mult,
health_mult: self.health_mult,
power_mult: self.power_mult,
game_time: self.game_time,
capture_speed: self.capture_speed,
points_kill_streak: self.points_kill_streak,
points_total_required: self.points_total_required,
number_of_kills_to_win: self.number_of_kills_to_win,
respawn_time: self.respawn_time,
core_appear_frequency: self.core_appear_frequency,
core_health_multiplier: self.core_health_multiplier,
core_destroy_time: self.core_destroy_time,
protonium_harvest: self.protonium_harvest,
ceiling_multiplier: self.ceiling_multiplier,
min_cpu: self.min_cpu,
max_cpu: self.max_cpu,
}
}
}
#[derive(Serialize, Deserialize, Clone, Debug)]
pub struct IntercomLobbyCustomGameUserData {
pub public_id: String,
pub team: u8,
}
#[derive(Serialize, Deserialize, Clone, Debug)]
pub struct IntercomWebServiceMessage {
pub public_ids: Vec<String>,

View File

@@ -1,3 +1,5 @@
use serde::{Serialize, Deserialize};
use super::account_json::UserData;
pub enum TeamChooser {
@@ -87,6 +89,7 @@ impl super::LobbyUser for UserData {
auto_heal: oj_rc_database::sea_orm::ActiveValue::Set(game.auto_heal),
variant: oj_rc_database::sea_orm::ActiveValue::Set(variant),
is_complete: oj_rc_database::sea_orm::ActiveValue::Set(false),
overrides: oj_rc_database::sea_orm::ActiveValue::Set("".to_owned()),
};
let game_dbo = self.db.insert_game(game_dbo).await.map_err(|e| {
log::error!("Failed to create game {} through user_id {}: {}", game.guid, self.account.id, e);
@@ -165,4 +168,143 @@ impl super::LobbyUser for UserData {
players: forced_fake_players.into_iter().chain(filler_players).collect(),
})
}
async fn start_custom_game(&self, game: super::GameDescriptor, players: Vec<super::PlayerLobbyDescriptor>) -> Result<(), polariton_server::operations::SimpleOpError> {
let now = chrono::Utc::now().timestamp();
let guid = crate::persist::user::str_to_i64(&game.guid)
.ok_or_else(|| polariton_server::operations::SimpleOpError::with_message(
crate::data::error_codes::LobbyReasonCode::UnexpectedError as i16, "Invalid GUID".to_owned()
)
)?;
let variant = if game.is_ranked {
oj_rc_database::schema::multiplayer_game::GameType::Ranked
} else if game.is_custom {
oj_rc_database::schema::multiplayer_game::GameType::Custom
} else {
oj_rc_database::schema::multiplayer_game::GameType::Standard
};
let conf_str = if let Some(config) = game.overrides {
serde_json::to_string_pretty(&CustomGameOverrides::from_user(&config)).unwrap()
} else {
"".to_owned()
};
let game_dbo = 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(guid),
map: oj_rc_database::sea_orm::ActiveValue::Set(game.map),
mode: oj_rc_database::sea_orm::ActiveValue::Set(game.mode.to_db()),
visibility: oj_rc_database::sea_orm::ActiveValue::Set(game.visibility.to_db()),
auto_heal: oj_rc_database::sea_orm::ActiveValue::Set(game.auto_heal),
variant: oj_rc_database::sea_orm::ActiveValue::Set(variant),
is_complete: oj_rc_database::sea_orm::ActiveValue::Set(false),
overrides: oj_rc_database::sea_orm::ActiveValue::Set(conf_str),
};
let game_dbo = self.db.insert_game(game_dbo).await.map_err(|e| {
log::error!("Failed to create custom game {} through user_id {}: {}", game.guid, self.account.id, e);
polariton_server::operations::SimpleOpError::with_message(
crate::data::error_codes::LobbyReasonCode::UnexpectedError as i16,
format!("Failed to create custom game {}: {}", game.guid, e),
)
})?;
let players: Vec<oj_rc_database::schema::multiplayer_game_player::ActiveModel> = players.into_iter()
.enumerate()
.map(|(i, 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(player.user_id)),
game_id: oj_rc_database::sea_orm::ActiveValue::Set(game_dbo.id),
creation_time: oj_rc_database::sea_orm::ActiveValue::Set(now),
player_id: oj_rc_database::sea_orm::ActiveValue::Set((i as u8) as _),
team: oj_rc_database::sea_orm::ActiveValue::Set(player.team),
group: oj_rc_database::sea_orm::ActiveValue::Set(player.group),
is_claimed: oj_rc_database::sea_orm::ActiveValue::Set(false),
public_id: oj_rc_database::sea_orm::ActiveValue::Set(player.public_id),
display_name: oj_rc_database::sea_orm::ActiveValue::Set(player.display_name),
variant: oj_rc_database::sea_orm::ActiveValue::Set(oj_rc_database::schema::multiplayer_game_player::ClientType::Client),
}
})
.collect();
self.db.insert_players(players).await.map_err(|e| {
log::error!("Failed to create game players for {} through user_id {}: {}", game.guid, self.account.id, e);
polariton_server::operations::SimpleOpError::with_message(
crate::data::error_codes::LobbyReasonCode::UnexpectedError as i16,
format!("Failed to create game players for {}: {}", game.guid, e),
)
})?;
Ok(())
}
}
#[derive(Serialize, Deserialize, Clone, Debug)]
pub(super) struct CustomGameOverrides {
pub capture_segment_memory: bool,
pub base_shields_go_down: bool,
pub damage_mult: i32,
pub health_mult: i32,
pub power_mult: i32,
pub game_time: i32, // minutes
pub capture_speed: i32, // seconds
pub points_kill_streak: bool,
pub points_total_required: i32,
pub number_of_kills_to_win: i32,
pub respawn_time: i32,
pub core_appear_frequency: i32,
pub core_health_multiplier: i32,
pub core_destroy_time: i32,
pub protonium_harvest: i32,
pub ceiling_multiplier: i32,
pub min_cpu: i32,
pub max_cpu: i32,
}
impl CustomGameOverrides {
pub(super) fn from_user(intercom: &super::GameOverrides) -> Self {
Self {
capture_segment_memory: intercom.capture_segment_memory,
base_shields_go_down: intercom.base_shields_go_down,
damage_mult: intercom.damage_mult,
health_mult: intercom.health_mult,
power_mult: intercom.power_mult,
game_time: intercom.game_time,
capture_speed: intercom.capture_speed,
points_kill_streak: intercom.points_kill_streak,
points_total_required: intercom.points_total_required,
number_of_kills_to_win: intercom.number_of_kills_to_win,
respawn_time: intercom.respawn_time,
core_appear_frequency: intercom.core_appear_frequency,
core_health_multiplier: intercom.core_health_multiplier,
core_destroy_time: intercom.core_destroy_time,
protonium_harvest: intercom.protonium_harvest,
ceiling_multiplier: intercom.ceiling_multiplier,
min_cpu: intercom.min_cpu,
max_cpu: intercom.max_cpu,
}
}
pub(super) fn to_user(&self) -> super::GameOverrides {
super::GameOverrides {
capture_segment_memory: self.capture_segment_memory,
base_shields_go_down: self.base_shields_go_down,
damage_mult: self.damage_mult,
health_mult: self.health_mult,
power_mult: self.power_mult,
game_time: self.game_time,
capture_speed: self.capture_speed,
points_kill_streak: self.points_kill_streak,
points_total_required: self.points_total_required,
number_of_kills_to_win: self.number_of_kills_to_win,
respawn_time: self.respawn_time,
core_appear_frequency: self.core_appear_frequency,
core_health_multiplier: self.core_health_multiplier,
core_destroy_time: self.core_destroy_time,
protonium_harvest: self.protonium_harvest,
ceiling_multiplier: self.ceiling_multiplier,
min_cpu: self.min_cpu,
max_cpu: self.max_cpu,
}
}
}

View File

@@ -11,7 +11,7 @@ mod inventory;
pub use inventory::{UnlockedParts, UnlockOverride};
mod traits;
pub use traits::{UserProvider, User, UserToken, UserSlots, UserSlotData, VehicleData, UserAuthInfo, UserLoginInfo, 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, SocialUserC, CurrencyType, CurrencyOp, MatchRewards, SingleplayerUser, PurchaseResult, FactoryUser, FriendInviteReturn, FriendData, FriendInviteStatus, SocialInfo, ClanData, ClanMember, ClanMemberRank, ClanType, ClanSearchQuery, ClanInviteData};
pub use traits::{UserProvider, User, UserToken, UserSlots, UserSlotData, VehicleData, UserAuthInfo, UserLoginInfo, 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, SocialUserC, CurrencyType, CurrencyOp, MatchRewards, SingleplayerUser, PurchaseResult, FactoryUser, FriendInviteReturn, FriendData, FriendInviteStatus, SocialInfo, ClanData, ClanMember, ClanMemberRank, ClanType, ClanSearchQuery, ClanInviteData, Userless, GameOverrides};
pub mod intercom;
pub use intercom::generate_token as generate_intercom_token;
@@ -24,6 +24,7 @@ mod chat;
mod social;
mod singleplayer;
mod factory;
mod userless;
pub const TOKEN_SECRET_FILENAME: &str = "token_secret.key";

View File

@@ -134,6 +134,15 @@ impl super::MultiplayerUser for UserData {
is_ranked: matches!(game.variant, oj_rc_database::schema::multiplayer_game::GameType::Ranked),
is_custom: matches!(game.variant, oj_rc_database::schema::multiplayer_game::GameType::Custom),
is_complete: game.is_complete,
overrides: if game.overrides.is_empty() { None } else {
match serde_json::from_str::<super::lobby::CustomGameOverrides>(&game.overrides) {
Ok(x) => Some(x.to_user()),
Err(e) => {
log::warn!("Failed to parse overrides JSON: {}\n{}", e, game.overrides);
None
}
}
}
}))
}
@@ -204,6 +213,15 @@ impl super::MultiplayerUser for UserData {
is_ranked: matches!(game.variant, oj_rc_database::schema::multiplayer_game::GameType::Ranked),
is_custom: matches!(game.variant, oj_rc_database::schema::multiplayer_game::GameType::Custom),
is_complete: game.is_complete,
overrides: if game.overrides.is_empty() { None } else {
match serde_json::from_str::<super::lobby::CustomGameOverrides>(&game.overrides) {
Ok(x) => Some(x.to_user()),
Err(e) => {
log::warn!("Failed to parse overrides JSON: {}\n{}", e, game.overrides);
None
}
}
}
}))
} else {
Err(super::MultiplayerError {

View File

@@ -23,6 +23,7 @@ impl super::SingleplayerUser for UserData {
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),
overrides: oj_rc_database::sea_orm::ActiveValue::Set("".to_owned()),
}).await
.map_err(|e| {
log::error!("Failed to create singleplayer game {} for user {}: {}", guid, self.account.id, e);

View File

@@ -282,6 +282,8 @@ pub trait LobbyUser {
async fn team_chooser(&self, game: &GameDescriptor) -> super::TeamChooser;
#[allow(clippy::too_many_arguments)]
async fn start_game(&self, game: GameDescriptor, players: Vec<PlayerLobbyDescriptor>, factory: &dyn oj_rc_factory::VehicleFactoryAdapter, cpu_counter: &crate::cubes::CpuListParser, weapon_lister: &crate::cubes::WeaponListParser, team_chooser: &super::TeamChooser, missing_players: usize) -> Result<FakePlayers, polariton_server::operations::SimpleOpError>;
#[allow(clippy::too_many_arguments)]
async fn start_custom_game(&self, game: GameDescriptor, players: Vec<PlayerLobbyDescriptor>) -> Result<(), polariton_server::operations::SimpleOpError>;
}
pub struct FakePlayers {
@@ -306,6 +308,28 @@ pub struct GameDescriptor {
pub is_ranked: bool,
pub is_custom: bool,
pub is_complete: bool,
pub overrides: Option<GameOverrides>,
}
pub struct GameOverrides {
pub capture_segment_memory: bool,
pub base_shields_go_down: bool,
pub damage_mult: i32,
pub health_mult: i32,
pub power_mult: i32,
pub game_time: i32, // minutes
pub capture_speed: i32, // seconds
pub points_kill_streak: bool,
pub points_total_required: i32,
pub number_of_kills_to_win: i32,
pub respawn_time: i32,
pub core_appear_frequency: i32,
pub core_health_multiplier: i32,
pub core_destroy_time: i32,
pub protonium_harvest: i32,
pub ceiling_multiplier: i32,
pub min_cpu: i32,
pub max_cpu: i32,
}
pub struct PlayerLobbyDescriptor {
@@ -394,6 +418,7 @@ pub trait IntercomUser: CommonUser {
async fn webservice_listener(&self) -> Result<IntercomListener<super::intercom::IntercomWebServiceUserMessage>, polariton_server::operations::SimpleOpError>;
async fn show_dev_message(&self, msg: super::intercom::IntercomDevMessage, to: Vec<String>);
async fn enter_maintenance(&self, msg: super::intercom::IntercomMaintenanceMessage, to: Vec<String>);
async fn update_custom_game(&self, msg: super::intercom::IntercomLobbyCustomGameDataMessage);
async fn update_status(&self, server_name: &str, msg: oj_serdes::ServerStatus);
}
@@ -643,3 +668,8 @@ pub trait FactoryUser {
async fn prepare_factory_upload(&self, vehicle: VehicleUploadData) -> Result<oj_rc_factory::VehicleUploadInfo, polariton_server::operations::SimpleOpError>;
async fn rate_vehicle(&self, slot: i32, combat: i32, cosmetic: i32) -> Result<Option<i32>, polariton_server::operations::SimpleOpError>;
}
#[async_trait::async_trait]
pub trait Userless: Send + Sync {
async fn lobby_state_listener(&self) -> Result<super::IntercomListener<super::intercom::IntercomLobbyStateMessage>, reqwest_websocket::Error>;
}

View File

@@ -0,0 +1,29 @@
//use serde::{Serialize, Deserialize};
impl super::AccountProvider {
async fn listen_on_websocket<D: serde::de::DeserializeOwned>(&self, server_name: &str) -> Result<super::IntercomListener<D>, reqwest_websocket::Error> {
use reqwest_websocket::RequestBuilderExt;
let token = super::generate_intercom_token(format!("state/{}", server_name).as_bytes(), &self.secret);
let auth_header_val = format!("Internal {}", token);
let url = format!("{}/intercom/userless/{}", self.intercom, server_name);
log::debug!("Listening on websocket {}", url);
let websocket = self.intercom_http_client.get(url)
.header("Authorization", auth_header_val)
.upgrade()
.send()
.await?
.into_websocket()
.await?;
Ok(super::IntercomListener {
websocket,
_d: Default::default(),
})
}
}
#[async_trait::async_trait]
impl super::Userless for super::AccountProvider {
async fn lobby_state_listener(&self) -> Result<super::IntercomListener<super::intercom::IntercomLobbyStateMessage>, reqwest_websocket::Error> {
self.listen_on_websocket(".oj_lobby").await
}
}