From 9f74a2e76aa4bc64b772d6f064411220f729a72e Mon Sep 17 00:00:00 2001 From: "NG (Graham)" Date: Thu, 19 Jun 2025 22:05:16 -0400 Subject: [PATCH] Add support for matchmaker sending clients off to game server --- Cargo.lock | 2 +- Cargo.toml | 2 +- rc_core/src/data/error_codes.rs | 29 ++++++ rc_core/src/data/game_mode.rs | 4 +- rc_core/src/data/player_data.rs | 37 +++++++ rc_core/src/persist/combat.rs | 10 ++ rc_core/src/persist/config/cubes_json.rs | 12 +++ rc_core/src/persist/config/traits.rs | 5 +- rc_core/src/persist/mod.rs | 3 + rc_core/src/persist/multiplayer.rs | 48 +++++++++ rc_core/src/persist/user/account_json.rs | 92 +++++++++++------ rc_core/src/persist/user/mod.rs | 2 +- rc_core/src/persist/user/traits.rs | 7 +- rc_lobby_room/src/cli.rs | 4 + rc_lobby_room/src/data/mod.rs | 1 + rc_lobby_room/src/data/network.rs | 60 +++++++++++ rc_lobby_room/src/events/battle_enter.rs | 77 +++++++++++++++ rc_lobby_room/src/events/battle_found.rs | 18 ++++ rc_lobby_room/src/events/enqueue_error.rs | 27 +++++ rc_lobby_room/src/events/mod.rs | 3 + rc_lobby_room/src/lobby.rs | 110 +++++++++++++++++++++ rc_lobby_room/src/main.rs | 9 +- rc_lobby_room/src/operations/join_queue.rs | 22 ++++- rc_lobby_room/src/operations/mod.rs | 4 +- 24 files changed, 543 insertions(+), 45 deletions(-) create mode 100644 rc_core/src/persist/multiplayer.rs create mode 100644 rc_lobby_room/src/data/network.rs create mode 100644 rc_lobby_room/src/events/battle_enter.rs create mode 100644 rc_lobby_room/src/events/battle_found.rs create mode 100644 rc_lobby_room/src/events/enqueue_error.rs create mode 100644 rc_lobby_room/src/events/mod.rs create mode 100644 rc_lobby_room/src/lobby.rs diff --git a/Cargo.lock b/Cargo.lock index f97daed..d980604 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2983,7 +2983,7 @@ checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" [[package]] name = "polariton" -version = "0.2.0" +version = "0.3.0" dependencies = [ "tokio", ] diff --git a/Cargo.toml b/Cargo.toml index 1aab32e..6ac0b19 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -33,7 +33,7 @@ libfj = { version = "0.8" } log = "0.4" env_logger = "0.11" clap = { version = "4.5", features = [ "derive" ] } -polariton = { version = "0.2", path = "../polariton", features = [ "tokio-async" ] } +polariton = { version = "0.3", path = "../polariton", features = [ "tokio-async" ] } polariton_server = { version = "0.3", path = "../polariton/server", features = [ "tokio-async" ] } #polariton = { version = "0.2", features = [ "tokio-async" ] } #polariton_server = { version = "0.3", features = [ "tokio-async" ] } diff --git a/rc_core/src/data/error_codes.rs b/rc_core/src/data/error_codes.rs index 4bd23a9..f4ad55d 100644 --- a/rc_core/src/data/error_codes.rs +++ b/rc_core/src/data/error_codes.rs @@ -72,3 +72,32 @@ pub enum SingleplayerErrorCode { MaintenanceMode = 4, DuplicateLogin = 5, } + +#[repr(i16)] +#[allow(dead_code)] +#[derive(Debug)] +pub enum LobbyReasonCode { + UnexpectedError = -1, + Ok = 0, + MaintenanceMode = 1, + RobotValidationError = 2, + LoggedInOtherLocation = 3, + GroupFailedChecks = 4, + ConnectionTestFailed = 5, + WrongGameModeForParty = 6, + BrawlConnectionTestFailed = 7, + PartyNotAllowed = 8, + NoSuitableLobbyFound = 9, + EventSystemExpired = 10 +} + +impl LobbyReasonCode { + pub(crate) fn from_service_error(err: i16) -> Self { + match err { + 0 /* None */ => Self::Ok, + 125 => Self::MaintenanceMode, + 140 => Self::RobotValidationError, + _ => Self::UnexpectedError, + } + } +} diff --git a/rc_core/src/data/game_mode.rs b/rc_core/src/data/game_mode.rs index 68475cf..ecc5b81 100644 --- a/rc_core/src/data/game_mode.rs +++ b/rc_core/src/data/game_mode.rs @@ -82,7 +82,7 @@ impl GameMap { } #[repr(u8)] -#[derive(Copy, Clone)] +#[derive(Copy, Clone, Hash, Eq, PartialEq)] pub enum GameMode { BattleArena = 0, SuddenDeath = 1, @@ -121,7 +121,7 @@ impl GameMode { } #[repr(u8)] -#[derive(Copy, Clone)] +#[derive(Copy, Clone, Hash, Eq, PartialEq)] pub enum MapVisibility { Good = 0, Poor = 1, diff --git a/rc_core/src/data/player_data.rs b/rc_core/src/data/player_data.rs index f863eb3..e290de6 100644 --- a/rc_core/src/data/player_data.rs +++ b/rc_core/src/data/player_data.rs @@ -1,5 +1,6 @@ use polariton::operation::Typed; +#[derive(Clone)] pub struct PlayerData { pub name: String, pub display_name: String, @@ -12,6 +13,7 @@ pub struct PlayerData { pub has_premium: bool, pub robot_uuid: String, pub cpu: i32, + pub avatar_id: Option, pub weapon_order: Vec, pub colour_map: Vec, pub is_ai: bool, @@ -52,6 +54,41 @@ impl PlayerData { } Ok(42 + self.robot_map.len() + (self.weapon_order.len() * 4) + self.colour_map.len() + (self.weapon_rank.len() * 8) + total_len) } + + pub fn as_transmissible(&self) -> Typed { + let weapon_ranks = Typed::Dict(polariton::operation::Dict { + key_ty: polariton::serdes::TypePrefix::Int, + val_ty: polariton::serdes::TypePrefix::Int, + items: self.weapon_rank.clone().into_iter().map(|(k, v)| (Typed::Int(k), Typed::Int(v))).collect(), + }); + Typed::HashMap(vec![ + (Typed::Str("name".into()), Typed::Str(self.name.clone().into())), + (Typed::Str("displayName".into()), Typed::Str(self.display_name.clone().into())), + (Typed::Str("robotName".into()), Typed::Str(self.robot_name.clone().into())), + (Typed::Str("cubeMap".into()), Typed::Bytes(self.robot_map.clone().into())), + (Typed::Str("colourMap".into()), Typed::Bytes(self.colour_map.clone().into())), + (Typed::Str("spawnEffect".into()), Typed::Str(self.spawn_effect.clone().into())), + (Typed::Str("deathEffect".into()), Typed::Str(self.death_effect.clone().into())), + //(Typed::Str("groupId".into()), Typed::Int(self.group)), // FIXME + (Typed::Str("groupId".into()), Typed::Int(self.team)), // not strongly enforced in EnterBattleEventListener + (Typed::Str("team".into()), Typed::Int(self.team)), // not strongly enforced in EnterBattleEventListener (but has to be parsable into an i32) + (Typed::Str("hasPremium".into()), Typed::Bool(self.has_premium)), + (Typed::Str("weaponOrder".into()), Typed::IntArr(self.weapon_order.clone().into())), + (Typed::Str("robotUniqueID".into()), Typed::Str(self.robot_uuid.clone().into())), + (Typed::Str("cpu".into()), Typed::Int(self.cpu)), // not strongly enforced in EnterBattleEventListener + (Typed::Str("useCustomAvatar".into()), Typed::Bool(self.avatar_id.is_none())), + (Typed::Str("avatarId".into()), Typed::Int(self.avatar_id.unwrap_or(0))), + (Typed::Str("masteryLevel".into()), Typed::Int(self.mastery)), + (Typed::Str("tier".into()), Typed::Int(self.tier)), + (Typed::Str("playerRank".into()), Typed::Int(self.player_rank)), + (Typed::Str("weaponRanks".into()), weapon_ranks), + (Typed::Str("isAI".into()), Typed::Bool(self.is_ai)), + // only required if clanName exists + /*(Typed::Str("clanName".into()), Typed::Str(todo!())), + (Typed::Str("clanUseCustomAvatar".into()), Typed::Bool(todo!())), + (Typed::Str("clanDefaultAvatarID".into()), Typed::Int(todo!())),*/ + ].into()) + } } pub struct PlayerDatas { diff --git a/rc_core/src/persist/combat.rs b/rc_core/src/persist/combat.rs index 03df2dd..fd4efeb 100644 --- a/rc_core/src/persist/combat.rs +++ b/rc_core/src/persist/combat.rs @@ -12,6 +12,8 @@ pub struct BattleConfig { pub singleplayer: super::SingleplayerConfig, #[serde(default = "default_rotation")] pub rotation: GameEventSequence, + #[serde(default = "default_multiplayer")] + pub multiplayer: super::MultiplayerConfig, } #[derive(Serialize, Deserialize, Clone, Debug)] @@ -472,3 +474,11 @@ fn default_rotation() -> GameEventSequence { ] } } + +fn default_multiplayer() -> super::MultiplayerConfig { + super::MultiplayerConfig { + players_per_game: 1, + enabled: true, + network: super::multiplayer::default_net_conf(), + } +} diff --git a/rc_core/src/persist/config/cubes_json.rs b/rc_core/src/persist/config/cubes_json.rs index 5b68642..08e9bff 100644 --- a/rc_core/src/persist/config/cubes_json.rs +++ b/rc_core/src/persist/config/cubes_json.rs @@ -353,4 +353,16 @@ impl super::ConfigProvider for CubeConfig { items: id_map, }) }*/ + + fn players_per_game(&self) -> usize { + self.battle.multiplayer.players_per_game + } + + fn is_multiplayer_enabled(&self) -> bool { + self.battle.multiplayer.enabled + } + + fn network_config(&self) -> crate::persist::NetworkConf { + self.battle.multiplayer.network.clone() + } } diff --git a/rc_core/src/persist/config/traits.rs b/rc_core/src/persist/config/traits.rs index 2322421..fff0745 100644 --- a/rc_core/src/persist/config/traits.rs +++ b/rc_core/src/persist/config/traits.rs @@ -25,8 +25,11 @@ pub trait ConfigProvider { fn cubes(&self) -> &'_ std::collections::HashMap; fn chat_system_config(&self) -> ChatSystemConfig; fn gamemode_events(&self) -> GameEventSequence; - // FIXME don't use serializable types in traits fn singleplayer_details(&self) -> SingleplayerConfig; + fn players_per_game(&self) -> usize; + fn is_multiplayer_enabled(&self) -> bool; + // FIXME don't use serializable types in traits + fn network_config(&self) -> crate::persist::NetworkConf; } pub struct CompleteCampaignProvider { diff --git a/rc_core/src/persist/mod.rs b/rc_core/src/persist/mod.rs index 217623a..4c99291 100644 --- a/rc_core/src/persist/mod.rs +++ b/rc_core/src/persist/mod.rs @@ -35,6 +35,9 @@ pub use chat::{ChatConfig, ChatCommand, ChatOperation, BuiltInChatOperation}; mod vehicle_factory; pub use vehicle_factory::{FactoryConfig, AdapterSettings, ArcFactorySettings}; +mod multiplayer; +pub use multiplayer::{MultiplayerConfig, NetworkConf}; + pub(self) const VALID_ROBOT: &[u8] = &[64, 0, 0, diff --git a/rc_core/src/persist/multiplayer.rs b/rc_core/src/persist/multiplayer.rs new file mode 100644 index 0000000..e9bd6be --- /dev/null +++ b/rc_core/src/persist/multiplayer.rs @@ -0,0 +1,48 @@ +use serde::{Serialize, Deserialize}; + +#[derive(Serialize, Deserialize, Clone, Debug)] +pub struct MultiplayerConfig { + pub players_per_game: usize, + pub enabled: bool, + #[serde(default = "default_net_conf")] + pub network: NetworkConf, +} + +#[derive(Serialize, Deserialize, Clone, Debug)] +pub struct NetworkConf { + pub network_channel_ty: String, + pub max_sent_message_queue_size: u16, + pub is_acks_long: bool, + pub network_drop_threshold: u8, + pub packet_size: u16, + pub max_combined_reliable_message_count: u16, + pub max_combined_reliable_message_size: u16, + pub min_update_timeout: u16, + pub max_delay: i64, + pub overflow_threshold: u8, + pub max_packet_size: u16, + pub resend_delay_base: f64, + pub resend_delay_rtt_mult: f64, + pub network_peer_update_interval: i32, + pub max_delay_for_disconnect_ms: i32, +} + +pub(super) fn default_net_conf() -> NetworkConf { + NetworkConf { + network_channel_ty: "3113".to_owned(), + max_sent_message_queue_size: 64, + is_acks_long: true, + network_drop_threshold: 80, + packet_size: 1200, + max_combined_reliable_message_count: 20, + max_combined_reliable_message_size: 200, + min_update_timeout: 1, + max_delay: 1, + overflow_threshold: 10, + max_packet_size: 5888, + resend_delay_base: 0.1, + resend_delay_rtt_mult: 0.5, + network_peer_update_interval: 1, + max_delay_for_disconnect_ms: 1000, + } +} diff --git a/rc_core/src/persist/user/account_json.rs b/rc_core/src/persist/user/account_json.rs index cedde33..94dfb80 100644 --- a/rc_core/src/persist/user/account_json.rs +++ b/rc_core/src/persist/user/account_json.rs @@ -261,6 +261,50 @@ impl UserData { self.perms.administrator | self.perms.developer } + async fn user_player_data(&self) -> Result { + let current_slot = self.db.garage_selected(self.account.id).await.map_err(|e| { + log::error!("Failed to retrieve selected vehicle for user_id {} (user_player_data): {}", self.account.id, e); + polariton_server::operations::SimpleOpError::with_message(DATABASE_ERR, format!("Could not retrieve selected garage: {}", e)) + })? + .ok_or_else(|| { + log::error!("Failed to find selected vehicle for user_id {} (user_player_data)", self.account.id); + polariton_server::operations::SimpleOpError::with_message(INVALID_ROBOT_ERR, "No selected garage".to_owned()) + })?; + let user_uuid = self.token.uuid.clone(); + let weapon_order = oj_rc_database::schema::parse_int_csv(¤t_slot.weapon_order).into_iter().map(|x| x as i32).collect::>(); + let user_avatar_aux = self.db.user_aux_by_user_id_and_descriptor(self.account.id, oj_rc_database::schema::user_aux::Descriptor::AvatarId).await.map_err(|e| { + log::error!("Failed to retrieve avatar for user_id {} (user_player_data): {}", self.account.id, e); + polariton_server::operations::SimpleOpError::with_message(DATABASE_ERR, format!("Could not retrieve avatar: {}", e)) + })? + .ok_or_else(|| { + log::error!("Failed to find avatar for user_id {} (user_player_data)", self.account.id); + polariton_server::operations::SimpleOpError::with_message(UNEXPECTED_ERR, "No avatar".to_owned()) + })?; + let avatar_id: Result = user_avatar_aux.data.parse(); + + // real user MUST be last + Ok(crate::data::player_data::PlayerData { + name: user_uuid.clone(), + display_name: self.account.display_name.clone(), + mastery: current_slot.mastery_level as i32, + tier: 1, // FIXME + robot_name: current_slot.name, + robot_map: current_slot.robot_data.clone(), + team: 0, + has_premium: false, // FIXME + robot_uuid: super::i64_as_uuid_str(current_slot.uuid).into(), + cpu: current_slot.total_robot_cpu as i32, + avatar_id: avatar_id.ok(), + weapon_order: weapon_order.clone(), + colour_map: current_slot.colour_data.clone(), + is_ai: false, + spawn_effect: current_slot.spawn_animation_id, + death_effect: current_slot.death_animation_id, + player_rank: 1, // FIXME + weapon_rank: oj_rc_database::schema::parse_int_csv(¤t_slot.weapon_order).into_iter().map(|x| (x as i32, if x == 0 { 0 } else { 1 })).collect(), + }) + } + async fn resolve_some_singleplayer_vehicles(&self, factory: &dyn oj_rc_factory::VehicleFactoryAdapter, weapon_order: &crate::cubes::WeaponListParser, singleplayer_config: &crate::persist::config::SingleplayerConfig) -> Result, i16> { use rand::seq::IndexedRandom; let mut players = Vec::with_capacity((singleplayer_config.max_enemies + singleplayer_config.max_teammates + 1) as usize); @@ -302,6 +346,7 @@ impl UserData { has_premium: false, robot_uuid: uuid_str, cpu: 420, + avatar_id: None, // not serialised weapon_order: weapons_guess, colour_map: factory_vehicle.0.colour_data, is_ai: true, @@ -335,6 +380,7 @@ impl UserData { has_premium: false, robot_uuid: uuid_str, cpu: db_vehicle.total_robot_cpu as i32, + avatar_id: None, // not serialised weapon_order: oj_rc_database::schema::parse_int_csv(&db_vehicle.weapon_order).into_iter().map(|x| x as i32).collect::>(), colour_map: db_vehicle.colour_data, is_ai: true, @@ -372,6 +418,7 @@ impl UserData { has_premium: false, robot_uuid: uuid_str, cpu: 420, // FIXME + avatar_id: None, // not serialised weapon_order: weapons_guess, colour_map: colour_data.to_owned(), is_ai: true, @@ -790,37 +837,10 @@ impl super::User for UserData { async fn singleplayer_robots(&self, factory: &dyn oj_rc_factory::VehicleFactoryAdapter, weapon_order: &crate::cubes::WeaponListParser, singleplayer_config: &crate::persist::config::SingleplayerConfig) -> Result, i16> { //self.err_on_banned().await?; let mut vehicles = self.resolve_some_singleplayer_vehicles(factory, weapon_order, singleplayer_config).await?; - let current_slot = self.db.garage_selected(self.account.id).await.map_err(|e| { - log::error!("Failed to retrieve selected vehicle for user_id {} (singleplayer_robots): {}", self.account.id, e); - DATABASE_ERR - })? - .ok_or_else(|| { - log::error!("Failed to find selected vehicle for user_id {} (singleplayer_robots)", self.account.id); - INVALID_ROBOT_ERR - })?; - let user_uuid = self.token.uuid.clone(); - let weapon_order = oj_rc_database::schema::parse_int_csv(¤t_slot.weapon_order).into_iter().map(|x| x as i32).collect::>(); + let user_bot = self.user_player_data().await?; // real user MUST be last - vehicles.push(crate::data::player_data::PlayerData { - name: user_uuid.clone(), - display_name: self.account.display_name.clone(), - mastery: current_slot.mastery_level as i32, - tier: 1, // FIXME - robot_name: current_slot.name, - robot_map: current_slot.robot_data.clone(), - team: 0, - has_premium: false, // FIXME - robot_uuid: super::i64_as_uuid_str(current_slot.uuid).into(), - cpu: current_slot.total_robot_cpu as i32, - weapon_order: weapon_order.clone(), - colour_map: current_slot.colour_data.clone(), - is_ai: false, - spawn_effect: "Spawn_Warp".to_owned(), // FIXME - death_effect: "Explosion_Warp".to_owned(), // FIXME - player_rank: 1, // FIXME - weapon_rank: oj_rc_database::schema::parse_int_csv(¤t_slot.weapon_order).into_iter().map(|x| (x as i32, if x == 0 { 0 } else { 1 })).collect(), - }); + vehicles.push(user_bot); Ok(crate::data::player_data::PlayerDatas { players: vehicles, }.as_transmissible()) @@ -1074,3 +1094,17 @@ impl super::ChatUser for UserData { } } } + +#[async_trait::async_trait] +impl super::LobbyUser for UserData { + async fn player_data(&self) -> Result { + self.user_player_data().await.map_err(|e| { + if let Some(msg) = e.error_msg() { + polariton_server::operations::SimpleOpError::with_message(crate::data::error_codes::LobbyReasonCode::from_service_error(e.error_code()) as i16, msg.to_owned()) + } else { + polariton_server::operations::SimpleOpError::with_code(crate::data::error_codes::LobbyReasonCode::from_service_error(e.error_code()) as i16) + } + + }) + } +} diff --git a/rc_core/src/persist/user/mod.rs b/rc_core/src/persist/user/mod.rs index f13a82a..b857413 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}; +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}; pub const TOKEN_SECRET_FILENAME: &str = "token_secret.key"; diff --git a/rc_core/src/persist/user/traits.rs b/rc_core/src/persist/user/traits.rs index c3ad920..ea429c8 100644 --- a/rc_core/src/persist/user/traits.rs +++ b/rc_core/src/persist/user/traits.rs @@ -54,7 +54,7 @@ pub trait UserAuthenticator { } #[async_trait::async_trait] -pub trait User: ChatUser { +pub trait User: ChatUser + LobbyUser { fn ext(&self, ty: std::any::TypeId) -> Option<&'_ (dyn std::any::Any + Send + Sync + 'static)>; fn token(&self) -> &'_ super::UserToken; fn is_mod(&self) -> bool; @@ -225,3 +225,8 @@ impl SanctionType { } } } + +#[async_trait::async_trait] +pub trait LobbyUser { + async fn player_data(&self) -> Result; +} diff --git a/rc_lobby_room/src/cli.rs b/rc_lobby_room/src/cli.rs index a269e71..eee5d43 100644 --- a/rc_lobby_room/src/cli.rs +++ b/rc_lobby_room/src/cli.rs @@ -19,6 +19,10 @@ pub struct CliArgs { #[arg(long, default_value_t = {"../data/robocraft".to_string()})] pub data: String, + /// Domain and port of the game match server + #[arg(long, default_value_t = {"127.0.0.1:4542".to_string()})] + pub redirect: String, + /// Handle one connection and then exit #[arg(short = '1', long)] pub once: bool, diff --git a/rc_lobby_room/src/data/mod.rs b/rc_lobby_room/src/data/mod.rs index e69de29..a61610b 100644 --- a/rc_lobby_room/src/data/mod.rs +++ b/rc_lobby_room/src/data/mod.rs @@ -0,0 +1 @@ +pub mod network; diff --git a/rc_lobby_room/src/data/network.rs b/rc_lobby_room/src/data/network.rs new file mode 100644 index 0000000..25f4b5f --- /dev/null +++ b/rc_lobby_room/src/data/network.rs @@ -0,0 +1,60 @@ +#[derive(Clone)] +pub struct NetworkConfigData { + pub network_channel_ty: String, + pub max_sent_message_queue_size: u16, + pub is_acks_long: bool, + pub network_drop_threshold: u8, + pub packet_size: u16, + pub max_combined_reliable_message_count: u16, + pub max_combined_reliable_message_size: u16, + pub min_update_timeout: u16, // stored as a u32 for some reason + pub max_delay: i64, + pub overflow_threshold: u8, + pub max_packet_size: u16, + pub resend_delay_base: f64, + pub resend_delay_rtt_mult: f64, + pub network_peer_update_interval: i32, + pub max_delay_for_disconnect: i32, // milliseconds +} + +impl NetworkConfigData { + pub fn as_transmissible(&self) -> polariton::operation::Typed { + polariton::operation::Typed::HashMap(vec![ + (polariton::operation::Typed::Str("NetworkChannelTypes".into()), polariton::operation::Typed::Str(self.network_channel_ty.clone().into())), + (polariton::operation::Typed::Str("MaxSentMessageQueueSize".into()), polariton::operation::Typed::Long(self.max_sent_message_queue_size as _)), + (polariton::operation::Typed::Str("IsAcksLong".into()), polariton::operation::Typed::Long(self.is_acks_long as _)), + (polariton::operation::Typed::Str("NetworkDropThreshold".into()), polariton::operation::Typed::Long(self.network_drop_threshold as _)), + (polariton::operation::Typed::Str("PacketSize".into()), polariton::operation::Typed::Long(self.packet_size as _)), + (polariton::operation::Typed::Str("MaxCombinedReliableMessageCount".into()), polariton::operation::Typed::Long(self.max_combined_reliable_message_count as _)), + (polariton::operation::Typed::Str("MaxCombinedReliableMessageSize".into()), polariton::operation::Typed::Long(self.max_combined_reliable_message_size as _)), + (polariton::operation::Typed::Str("MinUpdateTimeout".into()), polariton::operation::Typed::Long(self.min_update_timeout as _)), + (polariton::operation::Typed::Str("MaxDelay".into()), polariton::operation::Typed::Long(self.max_delay)), + (polariton::operation::Typed::Str("OverflowThreshold".into()), polariton::operation::Typed::Long(self.overflow_threshold as _)), + (polariton::operation::Typed::Str("MaxPacketSize".into()), polariton::operation::Typed::Long(self.max_packet_size as _)), + (polariton::operation::Typed::Str("ResendDelayBase".into()), polariton::operation::Typed::Double(self.resend_delay_base)), + (polariton::operation::Typed::Str("ResendDelayRttMult".into()), polariton::operation::Typed::Double(self.resend_delay_rtt_mult)), + (polariton::operation::Typed::Str("NetworkPeerUpdateInterval".into()), polariton::operation::Typed::Long(self.network_peer_update_interval as _)), + (polariton::operation::Typed::Str("MaxMillisecondsDelayForBeingDisconnected".into()), polariton::operation::Typed::Long(self.max_delay_for_disconnect as _)), + ].into()) + } + + pub fn from_conf(conf: oj_rc_core::persist::NetworkConf) -> Self { + Self { + network_channel_ty: conf.network_channel_ty, + max_sent_message_queue_size: conf.max_sent_message_queue_size, + is_acks_long: conf.is_acks_long, + network_drop_threshold: conf.network_drop_threshold, + packet_size: conf.packet_size, + max_combined_reliable_message_count: conf.max_combined_reliable_message_count, + max_combined_reliable_message_size: conf.max_combined_reliable_message_size, + min_update_timeout: conf.min_update_timeout, + max_delay: conf.max_delay, + overflow_threshold: conf.overflow_threshold, + max_packet_size: conf.max_packet_size, + resend_delay_base: conf.resend_delay_base, + resend_delay_rtt_mult: conf.resend_delay_rtt_mult, + network_peer_update_interval: conf.network_peer_update_interval, + max_delay_for_disconnect: conf.max_delay_for_disconnect_ms, + } + } +} diff --git a/rc_lobby_room/src/events/battle_enter.rs b/rc_lobby_room/src/events/battle_enter.rs new file mode 100644 index 0000000..e80f5c4 --- /dev/null +++ b/rc_lobby_room/src/events/battle_enter.rs @@ -0,0 +1,77 @@ +const HOST_IP_PARAM_KEY: u8 = 6; // seems to actually be an IP address or domain name +const HOST_PORT_PARAM_KEY: u8 = 7; +const MAP_NAME_PARAM_KEY: u8 = 8; +const GAME_MODE_PARAM_KEY: u8 = 1; +const GAME_GUID_PARAM_KEY: u8 = 40; +const IS_RANKED_PARAM_KEY: u8 = 25; +const IS_CUSTOM_GAME_PARAM_KEY: u8 = 27; +const MAP_VISIBILITY_PARAM_KEY: u8 = 28; +const IS_AUTO_HEAL_PARAM_KEY: u8 = 42; +const PLAYER_DATA_PARAM_KEY: u8 = 5; +const NETWORK_CONFIG_PARAM_KEY: u8 = 23; + +pub struct BattleEnter { + pub host: String, + pub port: u16, + pub map: String, + pub mode: oj_rc_core::data::game_mode::GameMode, + pub guid: String, + pub is_ranked: bool, + pub is_custom: bool, + pub visibility: Option, // ? + pub auto_heal: bool, + pub player_datas: Vec, + pub network_config: crate::data::network::NetworkConfigData, +} + +impl BattleEnter { + const CODE: u8 = 5; + + fn as_transmissible(&self) -> Vec<(u8, polariton::operation::Typed)> { + let player_datas = polariton::operation::Typed::Arr(polariton::operation::Arr { + ty: polariton::serdes::TypePrefix::HashMap, + items: self.player_datas.iter().map(|x| x.as_transmissible()).collect() + }); + let mut vec = Vec::with_capacity(11); + vec.push((HOST_IP_PARAM_KEY, polariton::operation::Typed::Str(self.host.clone().into()))); + vec.push((HOST_PORT_PARAM_KEY, polariton::operation::Typed::Int(self.port as _))); + vec.push((MAP_NAME_PARAM_KEY, polariton::operation::Typed::Str(self.map.clone().into()))); + vec.push((GAME_MODE_PARAM_KEY, polariton::operation::Typed::Int(self.mode as i32))); + vec.push((GAME_GUID_PARAM_KEY, polariton::operation::Typed::Str(self.guid.clone().into()))); + vec.push((IS_RANKED_PARAM_KEY, polariton::operation::Typed::Bool(self.is_ranked))); + vec.push((IS_CUSTOM_GAME_PARAM_KEY, polariton::operation::Typed::Bool(self.is_custom))); + if let Some(visibility) = self.visibility { + vec.push((MAP_VISIBILITY_PARAM_KEY, polariton::operation::Typed::Int(visibility as i32))); + } + vec.push((IS_AUTO_HEAL_PARAM_KEY, polariton::operation::Typed::Bool(self.auto_heal))); + vec.push((PLAYER_DATA_PARAM_KEY, player_datas)); + vec.push((NETWORK_CONFIG_PARAM_KEY, self.network_config.as_transmissible())); + vec + } +} + +impl polariton_server::events::IntoEvent for BattleEnter { + const CHANNEL: u8 = 0; + const ENCRYPT: bool = true; + const RELIABLE: bool = true; + + fn into_event(self) -> polariton::operation::Event { + polariton::operation::Event { + code: Self::CODE, + params: self.as_transmissible().into(), + } + } +} + +impl polariton_server::events::IntoEvent for &BattleEnter { + const CHANNEL: u8 = 0; + const ENCRYPT: bool = true; + const RELIABLE: bool = true; + + fn into_event(self) -> polariton::operation::Event { + polariton::operation::Event { + code: BattleEnter::CODE, + params: self.as_transmissible().into(), + } + } +} diff --git a/rc_lobby_room/src/events/battle_found.rs b/rc_lobby_room/src/events/battle_found.rs new file mode 100644 index 0000000..9e136d3 --- /dev/null +++ b/rc_lobby_room/src/events/battle_found.rs @@ -0,0 +1,18 @@ +pub struct BattleFound; + +impl BattleFound { + const CODE: u8 = 3; +} + +impl polariton_server::events::IntoEvent for BattleFound { + const CHANNEL: u8 = 0; + const ENCRYPT: bool = true; + const RELIABLE: bool = true; + + fn into_event(self) -> polariton::operation::Event { + polariton::operation::Event { + code: Self::CODE, + params: polariton::operation::ParameterTable::with_capacity(0), + } + } +} diff --git a/rc_lobby_room/src/events/enqueue_error.rs b/rc_lobby_room/src/events/enqueue_error.rs new file mode 100644 index 0000000..890e686 --- /dev/null +++ b/rc_lobby_room/src/events/enqueue_error.rs @@ -0,0 +1,27 @@ +const DATA_PARAM_KEY: u8 = 21; +const TEXT_PARAM_KEY: u8 = 22; + +pub struct QueueJoinError { + pub code: i16, + pub text: String, +} + +impl QueueJoinError { + const CODE: u8 = 1; +} + +impl polariton_server::events::IntoEvent for QueueJoinError { + const CHANNEL: u8 = 0; + const ENCRYPT: bool = true; + const RELIABLE: bool = true; + + fn into_event(self) -> polariton::operation::Event { + polariton::operation::Event { + code: Self::CODE, + params: vec![ + (DATA_PARAM_KEY, polariton::operation::Typed::Short(self.code)), + (TEXT_PARAM_KEY, polariton::operation::Typed::Str(self.text.into())), + ].into(), + } + } +} diff --git a/rc_lobby_room/src/events/mod.rs b/rc_lobby_room/src/events/mod.rs new file mode 100644 index 0000000..cf0aa0b --- /dev/null +++ b/rc_lobby_room/src/events/mod.rs @@ -0,0 +1,3 @@ +pub mod battle_found; +pub mod battle_enter; +pub mod enqueue_error; diff --git a/rc_lobby_room/src/lobby.rs b/rc_lobby_room/src/lobby.rs new file mode 100644 index 0000000..7981ebc --- /dev/null +++ b/rc_lobby_room/src/lobby.rs @@ -0,0 +1,110 @@ +use std::{collections::HashMap, hash::Hash}; + +#[derive(Hash, Eq, PartialEq, Clone)] +struct QueueKey { + map: String, + mode: oj_rc_core::data::game_mode::GameMode, + visibility: oj_rc_core::data::game_mode::MapVisibility, + auto_heal: bool, +} + +struct QueueUser { + emitter: polariton_server::events::EventEmitter, + player: oj_rc_core::data::player_data::PlayerData, +} + +pub struct QueueHandler { + users_in_queue: std::sync::Mutex>>, + users_per_game: usize, + is_enabled: bool, + hostname: String, + hostport: u16, + network_conf: crate::data::network::NetworkConfigData, +} + +impl QueueHandler { + pub fn new(conf: &oj_rc_core::ConfigImpl, game_host: &str) -> Self { + let (domain, port_str) = game_host.split_once(':').expect("Invalid redirect address (must be domain:port)"); + Self { + users_in_queue: std::sync::Mutex::new(HashMap::new()), + users_per_game: oj_rc_core::ConfigProvider::<()>::players_per_game(conf), + is_enabled: oj_rc_core::ConfigProvider::<()>::is_multiplayer_enabled(conf), + hostname: domain.to_owned(), + hostport: port_str.parse().expect("Invalid redirect port"), + network_conf: crate::data::network::NetworkConfigData::from_conf(oj_rc_core::ConfigProvider::<()>::network_config(conf)), + } + } + + fn enter_match(&self, key: &QueueKey, players: &Vec) { + use std::hash::Hasher; + let mut hasher = std::hash::DefaultHasher::new(); + key.hash(&mut hasher); + let guid = oj_rc_core::persist::user::uuid_sanitize(hasher.finish() as i64); + let guid_str = oj_rc_core::persist::user::i64_as_uuid_str(guid); + let player_datas = players.iter().map(|x| x.player.clone()).collect(); + let enter_battle_ev = crate::events::battle_enter::BattleEnter { + host: self.hostname.clone(), + port: self.hostport, + map: key.map.clone(), + mode: key.mode, + guid: guid_str, + is_ranked: false, + is_custom: false, + visibility: Some(key.visibility), + auto_heal: key.auto_heal, + player_datas, + network_config: self.network_conf.clone(), + }; + let arc_event = std::sync::Arc::new(enter_battle_ev); + for player in players.iter() { + tokio::spawn(Self::send_events_to_player(arc_event.clone(), player.emitter.clone())); + } + } + + async fn send_events_to_player(enter_event: std::sync::Arc, sender: polariton_server::events::EventEmitter) { + const WAIT_BEFORE_ENTER: std::time::Duration = std::time::Duration::from_secs(2); + if sender.emit(crate::events::battle_found::BattleFound) { + tokio::time::sleep(WAIT_BEFORE_ENTER).await; + sender.emit(enter_event.as_ref()); + } + } + + pub async fn join_queue(&self, map: String, mode: oj_rc_core::data::game_mode::GameMode, visibility: oj_rc_core::data::game_mode::MapVisibility, auto_heal: bool, user: &(dyn oj_rc_core::persist::user::User<()> + Send + Sync), event_emitter: polariton_server::events::EventEmitter) { + if !self.is_enabled { + event_emitter.emit(crate::events::enqueue_error::QueueJoinError { + code: oj_rc_core::data::error_codes::LobbyReasonCode::NoSuitableLobbyFound as i16, + text: "Multiplayer is not enabled".to_owned(), + }); + return; + } + let key = QueueKey { + map, mode, visibility, auto_heal, + }; + match user.player_data().await { + Ok(player_data) => { + let new_player = QueueUser { + emitter: event_emitter, + player: player_data, + }; + let mut lock = self.users_in_queue.lock().unwrap(); + let players = if let Some(players) = lock.get_mut(&key) { + players.push(new_player); + players + } else { + lock.insert(key.clone(), vec![new_player]); + lock.get(&key).unwrap() + }; + if players.len() >= self.users_per_game { + self.enter_match(&key, players); + lock.remove(&key); + } + }, + Err(e) => { + event_emitter.emit(crate::events::enqueue_error::QueueJoinError { + code: e.error_code(), + text: e.error_msg().map(|x| x.to_owned()).unwrap_or_else(|| "Unknown queue join error".to_owned()), + }); + } + } + } +} diff --git a/rc_lobby_room/src/main.rs b/rc_lobby_room/src/main.rs index b6f4a47..794c78b 100644 --- a/rc_lobby_room/src/main.rs +++ b/rc_lobby_room/src/main.rs @@ -1,8 +1,11 @@ #![forbid(unsafe_code)] mod cli; +mod lobby; +pub use lobby::QueueHandler; mod data; mod operations; +mod events; use oj_polariton_auth::Handshake; use tokio::net; @@ -13,8 +16,8 @@ use polariton::operation::{OperationResponse, Typed}; pub struct InitConfig { pub config: oj_rc_core::persist::config::ConfigImpl, pub users: std::sync::Arc, - pub factory: std::sync::Arc, pub parsers: oj_rc_core::cubes::CubeParsers, + pub queue: std::sync::Arc, } pub type UserTy = oj_rc_core::UserState<()>; @@ -27,14 +30,14 @@ async fn main() -> std::io::Result<()> { let config = oj_rc_core::persist::config::ConfigImpl::load(&args.assets).expect("Bad config data"); let users = std::sync::Arc::new(oj_rc_core::persist::user::UserImpl::load(&args.data, &config).await.expect("Bad user data")); - let factory = std::sync::Arc::new(>::factory::<'_, '_>(&config).await.expect("Bad vehicle factory (CRF) config")); + let queue = std::sync::Arc::new(QueueHandler::new(&config, &args.redirect)); let parsers = oj_rc_core::cubes::CubeParsers::new(&config); let init_ctx = InitConfig { config, users, - factory, parsers, + queue, }; let server = std::sync::Arc::new(polariton_server::Server::new(operations::handler(&init_ctx), polariton_server::events::EventsHandler::new())); diff --git a/rc_lobby_room/src/operations/join_queue.rs b/rc_lobby_room/src/operations/join_queue.rs index cdf3887..90b0945 100644 --- a/rc_lobby_room/src/operations/join_queue.rs +++ b/rc_lobby_room/src/operations/join_queue.rs @@ -13,14 +13,16 @@ const EVENT_TO_JOIN_PARAM_KEY: u8 = 41; // str; in const ESTIMATED_QUEUE_TIME_PARAM_KEY: u8 = 13; // int (seconds); out const PERSONAL_RANKING_PARAM_KEY: u8 = 17; // double; out -pub(super) struct QueueJoinProvider; +pub(super) struct QueueJoinProvider { + queue_handler: std::sync::Arc, +} #[async_trait::async_trait] impl SimpleOperation for QueueJoinProvider { type User = crate::UserTy; const CODE: u8 = CODE; - async fn handle(&self, params: ParameterTable, _user: &Self::User) -> Result, SimpleOpError> { + async fn handle(&self, params: ParameterTable, user: &Self::User) -> Result, SimpleOpError> { let mut params = params.to_dict(); if let Some(Typed::Str(group_id)) = params.remove(&GROUP_ID_PARAM_KEY) { if let Some(Typed::Int(slot_id)) = params.remove(&GARAGE_SLOT_PARAM_KEY) { @@ -32,6 +34,16 @@ impl SimpleOperation for QueueJoinProvider { log::debug!("Got lobby join queue request of platoon {} ({} players, is_leader:{}) slot {} lobby {:?} event {}", group_id.string, group_size, is_leader, slot_id, lobby_ty, event_to_join.string); params.insert(ESTIMATED_QUEUE_TIME_PARAM_KEY, Typed::Int(42)); params.insert(PERSONAL_RANKING_PARAM_KEY, Typed::Double(42.0)); + let events = user.event_sender(); + let user_info = user.user()?; + self.queue_handler.join_queue( + "FIXME_map".to_owned(), + oj_rc_core::data::game_mode::GameMode::BattleArena, // FIXME + oj_rc_core::data::game_mode::MapVisibility::Good, // FIXME + true, // FIXME + user_info.as_ref().as_ref(), + events.to_owned(), + ).await; } } } @@ -42,6 +54,8 @@ impl SimpleOperation for QueueJoinProvider { } } -pub(super) fn join_queue_provider() -> SimpleOpImpl { - SimpleOpImpl::new(QueueJoinProvider) +pub(super) fn join_queue_provider(queue_handler: &std::sync::Arc) -> SimpleOpImpl { + SimpleOpImpl::new(QueueJoinProvider { + queue_handler: queue_handler.to_owned(), + }) } diff --git a/rc_lobby_room/src/operations/mod.rs b/rc_lobby_room/src/operations/mod.rs index c0d064b..cf83a61 100644 --- a/rc_lobby_room/src/operations/mod.rs +++ b/rc_lobby_room/src/operations/mod.rs @@ -5,12 +5,12 @@ mod join_queue; use polariton_server::operations::OperationsHandler; -pub fn handler(_init_ctx: &crate::InitConfig) -> OperationsHandler { +pub fn handler(init_ctx: &crate::InitConfig) -> OperationsHandler { OperationsHandler::::new() .modify(oj_rc_core::polariton::RcOpModifier) .add(more_auth::MoreLobbyAuth) //.add(eac::EacChallengeIgnorer) //.add(polariton_server::operations::Ack::<2, _>::default()) .add(no_quit::quit_blocker_provider()) - .add(join_queue::join_queue_provider()) + .add(join_queue::join_queue_provider(&init_ctx.queue)) }