diff --git a/Cargo.lock b/Cargo.lock index 7c24458..d615fc0 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2608,6 +2608,7 @@ dependencies = [ "oj_polariton_auth", "oj_rc_core", "oj_rc_factory", + "oj_rc_plugins", "oj_serdes", "polariton", "polariton_server", diff --git a/rc_core/src/persist/combat.rs b/rc_core/src/persist/combat.rs index aedbdf5..a534a24 100644 --- a/rc_core/src/persist/combat.rs +++ b/rc_core/src/persist/combat.rs @@ -89,12 +89,13 @@ impl std::convert::From for crate::data::voting::Vote { } } -#[derive(Serialize, Deserialize, Clone, Debug, Copy)] +#[derive(Serialize, Deserialize, Clone, Debug)] pub struct GameMode { pub respawn_heal_duration: f32, pub respawn_full_heal_duration: f32, pub kill_limit: i32, pub game_time_m: i32, + pub team_chooser: super::TeamChooser, } impl std::convert::From for crate::data::game_mode::GameModeConfig { @@ -108,7 +109,7 @@ impl std::convert::From for crate::data::game_mode::GameModeConfig { } } -#[derive(Serialize, Deserialize, Clone, Debug, Copy)] +#[derive(Serialize, Deserialize, Clone, Debug)] pub struct GameModes { pub battle_arena: GameMode, pub elimination: GameMode, @@ -337,24 +338,28 @@ fn default_game_modes() -> GameModes { respawn_full_heal_duration: 0.5, kill_limit: 0, game_time_m: 20, + team_chooser: super::TeamChooser::Alternating, }, elimination: GameMode { respawn_heal_duration: 10.0, respawn_full_heal_duration: 0.5, kill_limit: 10, game_time_m: 10, + team_chooser: super::TeamChooser::Alternating, }, pit: GameMode { respawn_heal_duration: 20.0, respawn_full_heal_duration: 0.5, kill_limit: 0, game_time_m: 15, + team_chooser: super::TeamChooser::OneOnAll, }, team_deathmatch: GameMode { respawn_heal_duration: 10.0, respawn_full_heal_duration: 0.5, kill_limit: 10, game_time_m: 10, + team_chooser: super::TeamChooser::Alternating, }, } } diff --git a/rc_core/src/persist/config/cubes_json.rs b/rc_core/src/persist/config/cubes_json.rs index 846cf05..28fb35b 100644 --- a/rc_core/src/persist/config/cubes_json.rs +++ b/rc_core/src/persist/config/cubes_json.rs @@ -241,7 +241,7 @@ impl super::ConfigProvider for CubeConfig { } fn game_mode_config(&self) -> Typed { - let game_mode_data: crate::data::game_mode::GameModeConfigs = self.battle.games.into(); + let game_mode_data: crate::data::game_mode::GameModeConfigs = self.battle.games.clone().into(); game_mode_data.as_transmissible() } @@ -347,7 +347,7 @@ impl super::ConfigProvider for CubeConfig { } fn gamemodes(&self) -> crate::data::game_mode::GameModeConfigs { - self.battle.games.into() + self.battle.games.clone().into() } fn singleplayer_details(&self) -> super::SingleplayerConfig { @@ -569,4 +569,13 @@ impl super::ConfigProvider for CubeConfig { fn garage_slot_limit(&self) -> i32 { self.settings.gameplay.garages_limit } + + fn team_choosers(&self) -> super::TeamChoosers { + super::TeamChoosers { + battle_arena: self.battle.games.battle_arena.team_chooser.clone(), + elimination: self.battle.games.elimination.team_chooser.clone(), + pit: self.battle.games.pit.team_chooser.clone(), + team_deathmatch: self.battle.games.team_deathmatch.team_chooser.clone(), + } + } } diff --git a/rc_core/src/persist/config/mod.rs b/rc_core/src/persist/config/mod.rs index 5b7b645..95cf497 100644 --- a/rc_core/src/persist/config/mod.rs +++ b/rc_core/src/persist/config/mod.rs @@ -2,7 +2,7 @@ mod cubes_json; pub use cubes_json::CubeConfig; mod traits; -pub use traits::{ConfigProvider, DevMessageProvider, ServerConfig, GarageUpgrades, GarageUpgradeIncrement, ChatSystemConfig, GameEventSequence, GameEvents, GameRotationStrategy, GameEvent, GameMap, GameVisibility, GameType, SingleplayerConfig, VehicleInfo, VehicleDescriptor, QueueChangeMode, Point, Sphere, MapConfig, LinksConfig, FakePlayer, ClientEmulator, EnergyConfig, BattleArenaResolver, PitSettings, PitWinCondition, TeamDeathMatchSettings, ShopEntriesResolver, ShopAction, ShopGain, PromoCode, MultiplayerSettings, BattleArenaCrystalParams, VehicleValidators}; +pub use traits::{ConfigProvider, DevMessageProvider, ServerConfig, GarageUpgrades, GarageUpgradeIncrement, ChatSystemConfig, GameEventSequence, GameEvents, GameRotationStrategy, GameEvent, GameMap, GameVisibility, GameType, SingleplayerConfig, VehicleInfo, VehicleDescriptor, QueueChangeMode, Point, Sphere, MapConfig, LinksConfig, FakePlayer, ClientEmulator, EnergyConfig, BattleArenaResolver, PitSettings, PitWinCondition, TeamDeathMatchSettings, ShopEntriesResolver, ShopAction, ShopGain, PromoCode, MultiplayerSettings, BattleArenaCrystalParams, VehicleValidators, TeamChoosers}; mod validation; pub use validation::{SelfValidator, ValidationInfo, ValidationMessage}; diff --git a/rc_core/src/persist/config/traits.rs b/rc_core/src/persist/config/traits.rs index ecf3242..24bedab 100644 --- a/rc_core/src/persist/config/traits.rs +++ b/rc_core/src/persist/config/traits.rs @@ -42,6 +42,7 @@ pub trait ConfigProvider { fn promo_codes(&self) -> std::collections::HashMap; fn vehicle_validation(&self) -> VehicleValidators; fn garage_slot_limit(&self) -> i32; + fn team_choosers(&self) -> TeamChoosers; } pub struct DevMessageProvider { @@ -547,3 +548,10 @@ pub struct VehicleValidators { pub singleplayer: crate::persist::VehicleValidator, pub campaigns: std::collections::HashMap, } + +pub struct TeamChoosers { + pub battle_arena: crate::persist::TeamChooser, + pub elimination: crate::persist::TeamChooser, + pub pit: crate::persist::TeamChooser, + pub team_deathmatch: crate::persist::TeamChooser, +} diff --git a/rc_core/src/persist/mod.rs b/rc_core/src/persist/mod.rs index 2e87a1f..1b934b9 100644 --- a/rc_core/src/persist/mod.rs +++ b/rc_core/src/persist/mod.rs @@ -44,6 +44,9 @@ pub use maps::{MapsConfig, MapConfig}; mod item_shop; pub use item_shop::{ItemShopConfig, ItemBundle}; +mod team_chooser; +pub use team_chooser::TeamChooser; + mod vehicle_validator; pub use vehicle_validator::VehicleValidator; diff --git a/rc_core/src/persist/team_chooser.rs b/rc_core/src/persist/team_chooser.rs new file mode 100644 index 0000000..c72afee --- /dev/null +++ b/rc_core/src/persist/team_chooser.rs @@ -0,0 +1,17 @@ +use serde::{Serialize, Deserialize}; + +#[derive(Serialize, Deserialize, Clone, Debug)] +#[serde(tag = "choice")] +pub enum TeamChooser { + Alternating, + AllOnOne { + team: u8, + }, + #[serde(alias = "Pit")] + OneOnAll, + // TODO more built-in choosers + Custom { + #[serde(alias = "library")] + path: String, + }, +} diff --git a/rc_core/src/persist/user/lobby.rs b/rc_core/src/persist/user/lobby.rs index 240099d..ffbfc24 100644 --- a/rc_core/src/persist/user/lobby.rs +++ b/rc_core/src/persist/user/lobby.rs @@ -22,17 +22,9 @@ impl super::LobbyUser for UserData { } else { polariton_server::operations::SimpleOpError::with_code(crate::data::error_codes::LobbyReasonCode::from_service_error(e.error_code()) as i16) } - }) } - async fn team_chooser(&self, game: &super::GameDescriptor) -> super::StandardTeamChooser { - match game.mode { - crate::data::game_mode::GameMode::Pit => super::StandardTeamChooser::OnePer, - _ => super::StandardTeamChooser::alternating(), - } - } - async fn start_game( &self, game: super::GameDescriptor, diff --git a/rc_core/src/persist/user/team.rs b/rc_core/src/persist/user/team.rs index 4138ee1..7851897 100644 --- a/rc_core/src/persist/user/team.rs +++ b/rc_core/src/persist/user/team.rs @@ -9,7 +9,7 @@ pub enum StandardTeamChooser { AllOn(u8), /// Each player will be put on their own team (like in Pit mode) OnePer, - //Custom(Box), + Custom(Box), } impl StandardTeamChooser { @@ -24,7 +24,7 @@ impl TeamChooser for StandardTeamChooser { Self::Alternating(t) => t.choose_team(game, index, player), Self::AllOn(team) => *team as i32, Self::OnePer => index as i32, - //Self::Custom(t) => t.choose_team(game, index, player), + Self::Custom(t) => t.choose_team(game, index, player), } } } diff --git a/rc_core/src/persist/user/traits.rs b/rc_core/src/persist/user/traits.rs index 8807528..7dc01a3 100644 --- a/rc_core/src/persist/user/traits.rs +++ b/rc_core/src/persist/user/traits.rs @@ -279,7 +279,6 @@ pub enum UserRole { pub trait LobbyUser { fn user_id(&self) -> i32; async fn player_data(&self, cpu_counter: &crate::cubes::CpuListParser) -> Result; - async fn team_chooser(&self, game: &GameDescriptor) -> super::StandardTeamChooser; #[allow(clippy::too_many_arguments)] async fn start_game(&self, game: GameDescriptor, players: Vec, factory: &dyn oj_rc_factory::VehicleFactoryAdapter, cpu_counter: &crate::cubes::CpuListParser, weapon_lister: &crate::cubes::WeaponListParser, team_chooser: &dyn super::TeamChooser, missing_players: usize) -> Result; #[allow(clippy::too_many_arguments)] diff --git a/rc_lobby_room/Cargo.toml b/rc_lobby_room/Cargo.toml index 64b0128..5ee4507 100644 --- a/rc_lobby_room/Cargo.toml +++ b/rc_lobby_room/Cargo.toml @@ -22,3 +22,4 @@ git-version.workspace = true chrono.workspace = true oj_serdes.workspace = true futures.workspace = true +oj_rc_plugins = { version = "*", path = "../rc_plugins" } diff --git a/rc_lobby_room/src/lobby.rs b/rc_lobby_room/src/lobby.rs index 6f85d62..2be7bdc 100644 --- a/rc_lobby_room/src/lobby.rs +++ b/rc_lobby_room/src/lobby.rs @@ -126,10 +126,18 @@ pub struct QueueHandler { change_strategy: GamemodeChangeStrategy, autostart_after: Option, autostart_task_started: std::sync::atomic::AtomicBool, + team_choosers: std::sync::Arc, } impl QueueHandler { - pub fn new(conf: &oj_rc_core::ConfigImpl, game_host: &str, factory: std::sync::Arc, cpu_counter: std::sync::Arc, weapon_guesser: std::sync::Arc,) -> Self { + pub fn new( + conf: &oj_rc_core::ConfigImpl, + game_host: &str, + factory: std::sync::Arc, + cpu_counter: std::sync::Arc, + weapon_guesser: std::sync::Arc, + team_choosers: crate::team_selection::InitedTeamChoosers, + ) -> Self { let (domain, port_str) = game_host.split_once(':').expect("Invalid redirect address (must be domain:port)"); let mp_settings = oj_rc_core::ConfigProvider::<()>::multiplayer_settings(conf); Self { @@ -147,6 +155,7 @@ impl QueueHandler { change_strategy: GamemodeChangeStrategy::from_core(>::server_config(conf).queue_mode), autostart_after: mp_settings.lobby_autostart_after, autostart_task_started: std::sync::atomic::AtomicBool::new(false), + team_choosers: std::sync::Arc::new(team_choosers), } } @@ -164,6 +173,7 @@ impl QueueHandler { let cpu_counter = self.cpu_counter.clone(); let weapon_guesser = self.weapon_guesser.clone(); let autostart_after = self.autostart_after.unwrap(); + let team_choosers = self.team_choosers.clone(); tokio::spawn(async move { loop { @@ -228,6 +238,7 @@ impl QueueHandler { key, q_entry, starter.as_ref().as_ref(), + team_choosers.as_ref(), ).await; } @@ -258,11 +269,37 @@ impl QueueHandler { key, q_entry, user, + &self.team_choosers ).await } + fn team_selector(choosers: &crate::team_selection::InitedTeamChoosers, mode: oj_rc_core::data::game_mode::GameMode) -> &'_ oj_rc_core::persist::user::StandardTeamChooser { + match mode { + oj_rc_core::data::game_mode::GameMode::BattleArena => &choosers.battle_arena, + oj_rc_core::data::game_mode::GameMode::SuddenDeath => &choosers.elimination, + oj_rc_core::data::game_mode::GameMode::TeamDeathmatch => &choosers.team_deathmatch, + oj_rc_core::data::game_mode::GameMode::Pit => &choosers.pit, + x => { + log::warn!("No team selector available for multiplayer mode {:?}; using elimination", x); + &choosers.elimination + }, + } + } + #[allow(clippy::too_many_arguments)] - async fn enter_match_static(hostname: String, hostport: u16, network_conf: crate::data::network::NetworkConfigData, factory: std::sync::Arc, cpu_counter: std::sync::Arc, weapon_guesser: std::sync::Arc, users_per_game: usize, key: QueueKey, mut q_entry: Queue, user: &(dyn oj_rc_core::persist::user::LobbyUser + Send + Sync)) { + async fn enter_match_static( + hostname: String, + hostport: u16, + network_conf: crate::data::network::NetworkConfigData, + factory: std::sync::Arc, + cpu_counter: std::sync::Arc, + weapon_guesser: std::sync::Arc, + users_per_game: usize, + key: QueueKey, + mut q_entry: Queue, + user: &(dyn oj_rc_core::persist::user::LobbyUser + Send + Sync), + team_choosers: &crate::team_selection::InitedTeamChoosers, + ) { let guid_str = key.unique_guid(); let game_desc = oj_rc_core::persist::user::GameDescriptor { guid: guid_str.clone(), @@ -275,11 +312,7 @@ impl QueueHandler { is_complete: false, overrides: None, }; - let team_picker = user.team_chooser(&game_desc).await; - /*let team_picker = match key.mode { - oj_rc_core::data::game_mode::GameMode::Pit => |i| i as i32, // each player is on a different team - _ => |i| (i % 2) as i32, // alternate teams - };*/ + let team_picker = Self::team_selector(team_choosers, game_desc.mode); let mut player_descs = Vec::with_capacity(q_entry.users.len()); for (i, player) in q_entry.users.iter_mut().enumerate() { let mut lobby_desc = oj_rc_core::persist::user::PlayerLobbyDescriptor { @@ -297,7 +330,7 @@ impl QueueHandler { let missing = users_per_game.saturating_sub(q_entry.users.len()); - match user.start_game(game_desc, player_descs, factory.as_ref(), &cpu_counter, &weapon_guesser, &team_picker, missing).await { + match user.start_game(game_desc, player_descs, factory.as_ref(), &cpu_counter, &weapon_guesser, team_picker, missing).await { Ok(fakes) => { let player_datas = q_entry.users.iter().map(|x| x.player.clone()) .chain(fakes.players.into_iter().map(|(desc, _emu)| desc),) diff --git a/rc_lobby_room/src/main.rs b/rc_lobby_room/src/main.rs index 8e7bb55..fb1f1e1 100644 --- a/rc_lobby_room/src/main.rs +++ b/rc_lobby_room/src/main.rs @@ -6,6 +6,7 @@ pub use lobby::QueueHandler; mod data; mod operations; mod events; +mod team_selection; use oj_polariton_auth::Handshake; use tokio::net; @@ -35,7 +36,12 @@ async fn main() -> std::io::Result<()> { 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, &|| users.factory_impl()).await.expect("Bad vehicle factory (CRF) config")); let parsers = oj_rc_core::cubes::CubeParsers::new(&config); - let queue = std::sync::Arc::new(QueueHandler::new(&config, &args.redirect, factory.clone(), parsers.cpu_counter(), parsers.weapon_order())); + let team_selector_plugins_path = std::path::PathBuf::from(&args.data).join("plugins/team_select"); + let team_choosers = crate::team_selection::choosers_from_conf( + &>::team_choosers(&config), + team_selector_plugins_path, + ); + let queue = std::sync::Arc::new(QueueHandler::new(&config, &args.redirect, factory.clone(), parsers.cpu_counter(), parsers.weapon_order(), team_choosers)); let init_ctx = InitConfig { config, diff --git a/rc_lobby_room/src/team_selection.rs b/rc_lobby_room/src/team_selection.rs new file mode 100644 index 0000000..5603888 --- /dev/null +++ b/rc_lobby_room/src/team_selection.rs @@ -0,0 +1,48 @@ +pub struct InitedTeamChoosers { + pub battle_arena: oj_rc_core::persist::user::StandardTeamChooser, + pub elimination: oj_rc_core::persist::user::StandardTeamChooser, + pub pit: oj_rc_core::persist::user::StandardTeamChooser, + pub team_deathmatch: oj_rc_core::persist::user::StandardTeamChooser, +} + +pub fn choosers_from_conf(conf: &oj_rc_core::persist::config::TeamChoosers, plugins_path: impl AsRef) -> InitedTeamChoosers { + InitedTeamChoosers { + battle_arena: chooser_from_conf(&conf.battle_arena, &plugins_path), + elimination: chooser_from_conf(&conf.elimination, &plugins_path), + pit: chooser_from_conf(&conf.pit, &plugins_path), + team_deathmatch: chooser_from_conf(&conf.team_deathmatch, &plugins_path), + } +} + +fn chooser_from_conf(conf: &oj_rc_core::persist::TeamChooser, plugins_path: impl AsRef) -> oj_rc_core::persist::user::StandardTeamChooser { + match conf { + oj_rc_core::persist::TeamChooser::Alternating => oj_rc_core::persist::user::StandardTeamChooser::alternating(), + oj_rc_core::persist::TeamChooser::AllOnOne { team } => oj_rc_core::persist::user::StandardTeamChooser::AllOn(*team), + oj_rc_core::persist::TeamChooser::OneOnAll => oj_rc_core::persist::user::StandardTeamChooser::OnePer, + oj_rc_core::persist::TeamChooser::Custom { path } => { + let full_path = plugins_path.as_ref().join(path); + log::warn!("Custom team selector plugin {} is experimental and insecure", full_path.display()); + let result = oj_rc_plugins::team_selection::TeamSelectorCPlugin::new(&full_path); + match result { + Ok(c_plugin) => oj_rc_core::persist::user::StandardTeamChooser::Custom(Box::new(TeamSelectionPluginWrapper(c_plugin)) as _), + Err(e) => { + log::error!("Failed to load custom team selector plugin {}: {} (crashing!)", full_path.display(), e); + panic!("Failed to load custom team selector plugin {}: {}", full_path.display(), e) + } + } + } + } +} + +struct TeamSelectionPluginWrapper(T); + +impl oj_rc_core::persist::user::TeamChooser for TeamSelectionPluginWrapper { + fn choose_team(&self, game: &str, index: usize, player: &oj_rc_core::persist::user::PlayerLobbyDescriptor) -> i32 { + self.0.select_team( + game, + index, + if player.user_id >= 0 { Some(player.user_id) } else { None }, + player.group.clone(), + ) as i32 + } +} diff --git a/rc_plugins/src/lib.rs b/rc_plugins/src/lib.rs index 4b0b175..ffcaa56 100644 --- a/rc_plugins/src/lib.rs +++ b/rc_plugins/src/lib.rs @@ -1,5 +1,6 @@ pub mod chat; pub mod vehicle_validation; +pub mod team_selection; pub trait Plugin: Send + Sync { fn self_check(&self) -> bool { diff --git a/rc_plugins/src/team_selection/c_binding.rs b/rc_plugins/src/team_selection/c_binding.rs new file mode 100644 index 0000000..7c34f97 --- /dev/null +++ b/rc_plugins/src/team_selection/c_binding.rs @@ -0,0 +1,57 @@ +//! The foreign function interface implementation for assigning a team to a player entering a match in different shared objects/libraries. +use std::ffi::{CString, c_char}; + +const SELECT_TEAM_SYMBOL_NAME: &[u8] = b"oj_rc_select_team"; +const SELECT_TEAM_SYMBOL_NAME_STR: &str = "oj_rc_select_team"; + +pub struct TeamSelectorCPlugin { + dll: libloading::Library, + pretty_name: String, +} + +impl TeamSelectorCPlugin { + pub fn new(file: impl AsRef) -> Result { + let dll = unsafe { libloading::Library::new(file.as_ref()) }?; + Ok(Self { + dll, + pretty_name: file.as_ref().display().to_string(), + }) + } +} + +impl super::TeamSelector for TeamSelectorCPlugin { + fn select_team(&self, game: &str, index: usize, user_id: Option, group: Option) -> u8 { + let func: libloading::Symbol u8> = match unsafe { self.dll.get(SELECT_TEAM_SYMBOL_NAME) } { + Ok(x) => x, + Err(e) => { + log::error!("Failed to find symbol {} in library {}: {}", SELECT_TEAM_SYMBOL_NAME_STR, self.pretty_name, e); + return 0; + } + }; + let game_c = CString::new(game).unwrap_or_default(); + let index_c = index as u64; + let user_id_c = if let Some(user_id) = &user_id { + std::ptr::from_ref(user_id) + } else { + std::ptr::null() + }; + let group_c = group.map(|group| CString::new(group).unwrap_or_default()); + unsafe { + func( + game_c.as_ptr(), + index_c, + user_id_c, + group_c.map(|x| x.as_ptr()) + .unwrap_or(std::ptr::null()), + ) + } + } +} + +impl crate::Plugin for TeamSelectorCPlugin { + fn self_check(&self) -> bool { + unsafe { + self.dll.get:: u8>(SELECT_TEAM_SYMBOL_NAME) + }.is_ok() + } +} diff --git a/rc_plugins/src/team_selection/mod.rs b/rc_plugins/src/team_selection/mod.rs new file mode 100644 index 0000000..667011e --- /dev/null +++ b/rc_plugins/src/team_selection/mod.rs @@ -0,0 +1,5 @@ +mod plugin; +pub use plugin::TeamSelector; + +mod c_binding; +pub use c_binding::TeamSelectorCPlugin; diff --git a/rc_plugins/src/team_selection/plugin.rs b/rc_plugins/src/team_selection/plugin.rs new file mode 100644 index 0000000..d0bdb94 --- /dev/null +++ b/rc_plugins/src/team_selection/plugin.rs @@ -0,0 +1,3 @@ +pub trait TeamSelector: crate::Plugin { + fn select_team(&self, game: &str, index: usize, user_id: Option, group: Option) -> u8; +} diff --git a/rc_plugins/src/vehicle_validation/c_binding.rs b/rc_plugins/src/vehicle_validation/c_binding.rs index 7bb9184..0688524 100644 --- a/rc_plugins/src/vehicle_validation/c_binding.rs +++ b/rc_plugins/src/vehicle_validation/c_binding.rs @@ -1,4 +1,4 @@ -//! The foreign function interface implementation for validation vehicles in different shared objects/libraries. +//! The foreign function interface implementation for validating vehicles in different shared objects/libraries. //use std::ffi::{CString, c_char, CStr}; const VALIDATE_VEHICLE_SYMBOL_NAME: &[u8] = b"oj_rc_validate_vehicle";