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

Factor in platoons during team assignment #88

This commit is contained in:
NG (Graham)
2026-03-21 16:06:57 -04:00
parent 18e3f1f6f7
commit 289827ec3b
10 changed files with 392 additions and 112 deletions

View File

@@ -2,25 +2,6 @@ use serde::{Serialize, Deserialize};
use super::account_json::UserData; use super::account_json::UserData;
pub enum TeamChooser {
/// Alternating between team 0 and team 1
Alternating,
/// All players will be put on the specified team
AllOn(u8),
/// Each player will be put on their own team (like in Pit mode)
OnePer,
}
impl TeamChooser {
pub fn team(&self, index: usize) -> i32 {
match self {
Self::Alternating => (index % 2) as i32,
Self::AllOn(team) => *team as i32,
Self::OnePer => index as i32,
}
}
}
fn fake_impl_to_db(client_emu: &crate::persist::config::ClientEmulator) -> oj_rc_database::schema::multiplayer_game_player::ClientType { fn fake_impl_to_db(client_emu: &crate::persist::config::ClientEmulator) -> oj_rc_database::schema::multiplayer_game_player::ClientType {
match client_emu { match client_emu {
crate::persist::config::ClientEmulator::Experiment => oj_rc_database::schema::multiplayer_game_player::ClientType::ServerExperimental, crate::persist::config::ClientEmulator::Experiment => oj_rc_database::schema::multiplayer_game_player::ClientType::ServerExperimental,
@@ -45,10 +26,10 @@ impl super::LobbyUser for UserData {
}) })
} }
async fn team_chooser(&self, game: &super::GameDescriptor) -> TeamChooser { async fn team_chooser(&self, game: &super::GameDescriptor) -> super::StandardTeamChooser {
match game.mode { match game.mode {
crate::data::game_mode::GameMode::Pit => TeamChooser::OnePer, crate::data::game_mode::GameMode::Pit => super::StandardTeamChooser::OnePer,
_ => TeamChooser::Alternating, _ => super::StandardTeamChooser::alternating(),
} }
} }
@@ -59,7 +40,7 @@ impl super::LobbyUser for UserData {
factory: &dyn oj_rc_factory::VehicleFactoryAdapter, factory: &dyn oj_rc_factory::VehicleFactoryAdapter,
cpu_counter: &crate::cubes::CpuListParser, cpu_counter: &crate::cubes::CpuListParser,
weapon_lister: &crate::cubes::WeaponListParser, weapon_lister: &crate::cubes::WeaponListParser,
chooser: &TeamChooser, chooser: &dyn super::TeamChooser,
missing_players: usize, missing_players: usize,
) -> Result<super::FakePlayers, polariton_server::operations::SimpleOpError> { ) -> Result<super::FakePlayers, polariton_server::operations::SimpleOpError> {
let now = chrono::Utc::now().timestamp(); let now = chrono::Utc::now().timestamp();
@@ -76,8 +57,8 @@ impl super::LobbyUser for UserData {
oj_rc_database::schema::multiplayer_game::GameType::Standard oj_rc_database::schema::multiplayer_game::GameType::Standard
}; };
let forced_fake_players = self.generate_forced_fake_players_data(guid, &players, factory, cpu_counter, weapon_lister, chooser).await?; let forced_fake_players = self.generate_forced_fake_players_data(&game.guid, &players, factory, cpu_counter, weapon_lister, chooser).await?;
let filler_players = self.generate_filler_players_data(guid, &players, factory, cpu_counter, weapon_lister, chooser, missing_players).await?; let filler_players = self.generate_filler_players_data(&game.guid, factory, cpu_counter, weapon_lister, chooser, missing_players, players.len() + forced_fake_players.len()).await?;
let game_dbo = oj_rc_database::schema::multiplayer_game::ActiveModel { let game_dbo = oj_rc_database::schema::multiplayer_game::ActiveModel {
id: oj_rc_database::sea_orm::ActiveValue::NotSet, id: oj_rc_database::sea_orm::ActiveValue::NotSet,
@@ -102,6 +83,17 @@ impl super::LobbyUser for UserData {
let players_len = players.len(); let players_len = players.len();
let forced_fake_players_len = forced_fake_players.len(); let forced_fake_players_len = forced_fake_players.len();
let mut group_map = std::collections::HashMap::new();
let mut group_num = 1;
for player in players.iter() {
if let Some(group) = &player.group {
if !group_map.contains_key(group) {
group_map.insert(group.to_owned(), group_num);
group_num += 1;
}
}
}
let players: Vec<oj_rc_database::schema::multiplayer_game_player::ActiveModel> = players.into_iter() let players: Vec<oj_rc_database::schema::multiplayer_game_player::ActiveModel> = players.into_iter()
.enumerate() .enumerate()
.map(|(i, player)| { .map(|(i, player)| {
@@ -112,7 +104,7 @@ impl super::LobbyUser for UserData {
creation_time: oj_rc_database::sea_orm::ActiveValue::Set(now), creation_time: oj_rc_database::sea_orm::ActiveValue::Set(now),
player_id: oj_rc_database::sea_orm::ActiveValue::Set((i as u8) as _), player_id: oj_rc_database::sea_orm::ActiveValue::Set((i as u8) as _),
team: oj_rc_database::sea_orm::ActiveValue::Set(player.team), team: oj_rc_database::sea_orm::ActiveValue::Set(player.team),
group: oj_rc_database::sea_orm::ActiveValue::Set(player.group), group: oj_rc_database::sea_orm::ActiveValue::Set(player.group.as_ref().and_then(|x| group_map.get(x)).copied()),
is_claimed: oj_rc_database::sea_orm::ActiveValue::Set(false), is_claimed: oj_rc_database::sea_orm::ActiveValue::Set(false),
public_id: oj_rc_database::sea_orm::ActiveValue::Set(player.public_id), public_id: oj_rc_database::sea_orm::ActiveValue::Set(player.public_id),
display_name: oj_rc_database::sea_orm::ActiveValue::Set(player.display_name), display_name: oj_rc_database::sea_orm::ActiveValue::Set(player.display_name),
@@ -220,7 +212,7 @@ impl super::LobbyUser for UserData {
creation_time: oj_rc_database::sea_orm::ActiveValue::Set(now), creation_time: oj_rc_database::sea_orm::ActiveValue::Set(now),
player_id: oj_rc_database::sea_orm::ActiveValue::Set((i as u8) as _), player_id: oj_rc_database::sea_orm::ActiveValue::Set((i as u8) as _),
team: oj_rc_database::sea_orm::ActiveValue::Set(player.team), team: oj_rc_database::sea_orm::ActiveValue::Set(player.team),
group: oj_rc_database::sea_orm::ActiveValue::Set(player.group), group: oj_rc_database::sea_orm::ActiveValue::Set(None),
is_claimed: oj_rc_database::sea_orm::ActiveValue::Set(false), is_claimed: oj_rc_database::sea_orm::ActiveValue::Set(false),
public_id: oj_rc_database::sea_orm::ActiveValue::Set(player.public_id), public_id: oj_rc_database::sea_orm::ActiveValue::Set(player.public_id),
display_name: oj_rc_database::sea_orm::ActiveValue::Set(player.display_name), display_name: oj_rc_database::sea_orm::ActiveValue::Set(player.display_name),

View File

@@ -18,13 +18,14 @@ pub use intercom::generate_token as generate_intercom_token;
mod multiplayer; mod multiplayer;
mod lobby; mod lobby;
pub use lobby::TeamChooser;
mod common; mod common;
mod chat; mod chat;
mod social; mod social;
mod singleplayer; mod singleplayer;
mod factory; mod factory;
mod userless; mod userless;
mod team;
pub use team::{TeamChooser, StandardTeamChooser};
pub const TOKEN_SECRET_FILENAME: &str = "token_secret.key"; pub const TOKEN_SECRET_FILENAME: &str = "token_secret.key";

View File

@@ -12,18 +12,24 @@ impl UserData {
#[allow(clippy::too_many_arguments)] #[allow(clippy::too_many_arguments)]
pub(super) async fn generate_fake_players_data( pub(super) async fn generate_fake_players_data(
&self, &self,
_guid: i64, guid_str: &str,
real_players: &[super::PlayerLobbyDescriptor],
factory: &dyn oj_rc_factory::VehicleFactoryAdapter, factory: &dyn oj_rc_factory::VehicleFactoryAdapter,
cpu_counter: &crate::cubes::CpuListParser, cpu_counter: &crate::cubes::CpuListParser,
weapon_lister: &crate::cubes::WeaponListParser, weapon_lister: &crate::cubes::WeaponListParser,
chooser: &super::TeamChooser, chooser: &dyn super::TeamChooser,
fake_players: &[crate::persist::config::FakePlayer], fake_players: &[crate::persist::config::FakePlayer],
total_offset: usize,
) -> Result<Vec<(crate::data::player_data::PlayerData, crate::persist::config::ClientEmulator)>, polariton_server::operations::SimpleOpError> { ) -> Result<Vec<(crate::data::player_data::PlayerData, crate::persist::config::ClientEmulator)>, polariton_server::operations::SimpleOpError> {
let mut fakes = Vec::with_capacity(fake_players.len()); let mut fakes = Vec::with_capacity(fake_players.len());
let mut fake_i = real_players.len(); for (i, fake) in fake_players.iter().enumerate() {
for fake in fake_players.iter() {
let vehicle = self.resolve_vehicle(&fake.vehicle, factory, weapon_lister, cpu_counter).await?; let vehicle = self.resolve_vehicle(&fake.vehicle, factory, weapon_lister, cpu_counter).await?;
let fake_lobby_desc = super::PlayerLobbyDescriptor {
user_id: -1,
team: -1,
public_id: fake.vehicle.username.clone(),
display_name: fake.vehicle.username.clone(),
group: None,
};
let out = ( let out = (
crate::data::player_data::PlayerData { crate::data::player_data::PlayerData {
name: fake.vehicle.username.clone(), name: fake.vehicle.username.clone(),
@@ -33,12 +39,7 @@ impl UserData {
robot_name: vehicle.robot_name, robot_name: vehicle.robot_name,
robot_map: vehicle.robot_map, robot_map: vehicle.robot_map,
group: None, group: None,
team: fake.team.map(|t| t as i32) team: chooser.choose_team(guid_str, total_offset + i, &fake_lobby_desc),
.unwrap_or_else(|| {
let assigned_team = chooser.team(fake_i);
fake_i += 1;
assigned_team
}),
has_premium: true, has_premium: true,
robot_uuid: vehicle.robot_uuid, robot_uuid: vehicle.robot_uuid,
cpu: vehicle.cpu, cpu: vehicle.cpu,
@@ -61,43 +62,44 @@ impl UserData {
#[allow(clippy::too_many_arguments)] #[allow(clippy::too_many_arguments)]
pub(super) async fn generate_forced_fake_players_data( pub(super) async fn generate_forced_fake_players_data(
&self, &self,
guid: i64, guid_str: &str,
real_players: &[super::PlayerLobbyDescriptor], real_players: &[super::PlayerLobbyDescriptor],
factory: &dyn oj_rc_factory::VehicleFactoryAdapter, factory: &dyn oj_rc_factory::VehicleFactoryAdapter,
cpu_counter: &crate::cubes::CpuListParser, cpu_counter: &crate::cubes::CpuListParser,
weapon_lister: &crate::cubes::WeaponListParser, weapon_lister: &crate::cubes::WeaponListParser,
chooser: &super::TeamChooser, chooser: &dyn super::TeamChooser,
) -> Result<Vec<(crate::data::player_data::PlayerData, crate::persist::config::ClientEmulator)>, polariton_server::operations::SimpleOpError> { ) -> Result<Vec<(crate::data::player_data::PlayerData, crate::persist::config::ClientEmulator)>, polariton_server::operations::SimpleOpError> {
let offset = real_players.len();
self.generate_fake_players_data( self.generate_fake_players_data(
guid, guid_str,
real_players,
factory, factory,
cpu_counter, cpu_counter,
weapon_lister, weapon_lister,
chooser, chooser,
&self.fake_players, &self.fake_players,
offset,
).await ).await
} }
#[allow(clippy::too_many_arguments)] #[allow(clippy::too_many_arguments)]
pub(super) async fn generate_filler_players_data( pub(super) async fn generate_filler_players_data(
&self, &self,
guid: i64, guid_str: &str,
real_players: &[super::PlayerLobbyDescriptor],
factory: &dyn oj_rc_factory::VehicleFactoryAdapter, factory: &dyn oj_rc_factory::VehicleFactoryAdapter,
cpu_counter: &crate::cubes::CpuListParser, cpu_counter: &crate::cubes::CpuListParser,
weapon_lister: &crate::cubes::WeaponListParser, weapon_lister: &crate::cubes::WeaponListParser,
chooser: &super::TeamChooser, chooser: &dyn super::TeamChooser,
count: usize, count: usize,
total_offset: usize,
) -> Result<Vec<(crate::data::player_data::PlayerData, crate::persist::config::ClientEmulator)>, polariton_server::operations::SimpleOpError> { ) -> Result<Vec<(crate::data::player_data::PlayerData, crate::persist::config::ClientEmulator)>, polariton_server::operations::SimpleOpError> {
self.generate_fake_players_data( self.generate_fake_players_data(
guid, guid_str,
real_players,
factory, factory,
cpu_counter, cpu_counter,
weapon_lister, weapon_lister,
chooser, chooser,
&self.filler_players[0..count], &self.filler_players[0..count],
total_offset
).await ).await
} }
} }

View File

@@ -0,0 +1,116 @@
pub trait TeamChooser: Send + Sync {
fn choose_team(&self, game: &str, index: usize, player: &super::PlayerLobbyDescriptor) -> i32;
}
pub enum StandardTeamChooser {
/// Alternating between team 0 and team 1
Alternating(TwoTeamPartyAware),
/// All players will be put on the specified team
AllOn(u8),
/// Each player will be put on their own team (like in Pit mode)
OnePer,
//Custom(Box<dyn TeamChooser>),
}
impl StandardTeamChooser {
pub fn alternating() -> Self {
Self::Alternating(TwoTeamPartyAware::new())
}
}
impl TeamChooser for StandardTeamChooser {
fn choose_team(&self, game: &str, index: usize, player: &super::PlayerLobbyDescriptor) -> i32 {
match self {
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),
}
}
}
pub struct TwoTeamPartyAware {
tracker: std::sync::Mutex<std::collections::HashMap<String, BalanceInfo>>,
}
struct BalanceInfo {
overall: i64,
/// platoon ID -> team
platoons: std::collections::HashMap<String, i8>,
created_at: chrono::DateTime<chrono::Utc>,
}
impl TwoTeamPartyAware {
pub fn new() -> Self {
Self {
tracker: std::sync::Mutex::new(std::collections::HashMap::new()),
}
}
/// Manually remove stale team infos
fn do_cleanup(tracker: &mut std::collections::HashMap<String, BalanceInfo>) {
let max_ttl = chrono::TimeDelta::from_std(std::time::Duration::from_secs(10)).unwrap();
let now = chrono::Utc::now();
tracker.retain(|_, bal_info| (now - bal_info.created_at) < max_ttl);
}
}
impl TeamChooser for TwoTeamPartyAware {
fn choose_team(&self, game: &str, _index: usize, player: &super::PlayerLobbyDescriptor) -> i32 {
let mut lock = self.tracker.lock().unwrap();
// the algorithm is basically:
// if player is in platoon AND platoon is known platoon:
// use platoon's team assignment
// else if overall > 0:
// choose team 1 for player, remember team assignment for all future platoon members
// overall -= 1
// else:
// choose team 0 for player, remember team assignment for all future platoon members
// overall += 1
//
// this roughly balances player count between the two teams
// while ensuring players on the same platoon are put on the same team
if let Some(bal_info) = lock.get_mut(game) {
if let Some(platoon_id) = &player.group {
if let Some(&platoon_team) = bal_info.platoons.get(platoon_id) {
if platoon_team == 0 {
bal_info.overall += 1;
} else {
bal_info.overall -= 1;
}
return platoon_team as i32;
}
}
let chosen_team = if bal_info.overall > 0 {
bal_info.overall -= 1;
1
} else {
bal_info.overall += 1;
0
};
if let Some(platoon_id) = &player.group {
bal_info.platoons.insert(platoon_id.to_owned(), chosen_team);
}
return chosen_team as i32;
} else {
Self::do_cleanup(&mut lock);
let init_info = if let Some(platoon) = &player.group {
let mut platoon_teams = std::collections::HashMap::new();
platoon_teams.insert(platoon.to_owned(), 0);
BalanceInfo {
overall: 1,
platoons: platoon_teams,
created_at: chrono::Utc::now(),
}
} else {
BalanceInfo {
overall: 1,
platoons: std::collections::HashMap::new(),
created_at: chrono::Utc::now(),
}
};
lock.insert(game.to_owned(), init_info);
0
}
}
}

View File

@@ -279,9 +279,9 @@ pub enum UserRole {
pub trait LobbyUser { pub trait LobbyUser {
fn user_id(&self) -> i32; fn user_id(&self) -> i32;
async fn player_data(&self, cpu_counter: &crate::cubes::CpuListParser) -> Result<crate::data::player_data::PlayerData, polariton_server::operations::SimpleOpError>; async fn player_data(&self, cpu_counter: &crate::cubes::CpuListParser) -> Result<crate::data::player_data::PlayerData, polariton_server::operations::SimpleOpError>;
async fn team_chooser(&self, game: &GameDescriptor) -> super::TeamChooser; async fn team_chooser(&self, game: &GameDescriptor) -> super::StandardTeamChooser;
#[allow(clippy::too_many_arguments)] #[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>; 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: &dyn super::TeamChooser, missing_players: usize) -> Result<FakePlayers, polariton_server::operations::SimpleOpError>;
#[allow(clippy::too_many_arguments)] #[allow(clippy::too_many_arguments)]
async fn start_custom_game(&self, game: GameDescriptor, players: Vec<PlayerLobbyDescriptor>) -> Result<(), polariton_server::operations::SimpleOpError>; async fn start_custom_game(&self, game: GameDescriptor, players: Vec<PlayerLobbyDescriptor>) -> Result<(), polariton_server::operations::SimpleOpError>;
} }
@@ -337,7 +337,7 @@ pub struct PlayerLobbyDescriptor {
pub team: i32, pub team: i32,
pub public_id: String, pub public_id: String,
pub display_name: String, pub display_name: String,
pub group: Option<i32>, pub group: Option<String>,
} }
#[derive(Clone)] #[derive(Clone)]

View File

@@ -1,5 +1,7 @@
use std::{collections::HashMap, hash::Hash}; use std::{collections::HashMap, hash::Hash};
use oj_rc_core::persist::user::TeamChooser;
#[derive(Clone, Copy)] #[derive(Clone, Copy)]
pub enum GamemodeChangeStrategy { pub enum GamemodeChangeStrategy {
Upgrade, // move enqueued players into newer gamemode Upgrade, // move enqueued players into newer gamemode
@@ -55,6 +57,41 @@ impl QueueKey {
} }
} }
struct Queue {
users: Vec<QueueUser>,
platoons: HashMap<String, Platoon>,
}
struct Platoon {
total: u8,
members: Vec<PlatoonUser>,
}
/// A QueueUser wrapper for platoon members
struct PlatoonUser(QueueUser);
impl PlatoonUser {
fn from_queue_user(qu: QueueUser) -> Self {
Self(qu)
}
fn clone_for_queue(&self) -> QueueUser {
QueueUser {
emitter: self.0.emitter.clone(),
player: self.0.player.clone(),
user_id: self.0.user_id,
enqueued_at: self.0.enqueued_at,
user: self.0.user.clone(),
}
}
}
pub struct PlatoonInfo {
pub total: u8,
pub platoon_id: String,
pub is_leader: bool,
}
struct QueueUser { struct QueueUser {
emitter: polariton_server::events::EventEmitter, emitter: polariton_server::events::EventEmitter,
player: oj_rc_core::data::player_data::PlayerData, player: oj_rc_core::data::player_data::PlayerData,
@@ -75,7 +112,7 @@ struct CustomGameQueueUser {
} }
pub struct QueueHandler { pub struct QueueHandler {
users_in_queue: std::sync::Arc<tokio::sync::Mutex<HashMap<QueueKey, Vec<QueueUser>>>>, users_in_queue: std::sync::Arc<tokio::sync::Mutex<HashMap<QueueKey, Queue>>>,
users_in_custom_games_queue: std::sync::Arc<tokio::sync::Mutex<HashMap<String, CustomGameQueue>>>, users_in_custom_games_queue: std::sync::Arc<tokio::sync::Mutex<HashMap<String, CustomGameQueue>>>,
custom_game_for_user: std::sync::Arc<tokio::sync::RwLock<HashMap<String, String>>>, custom_game_for_user: std::sync::Arc<tokio::sync::RwLock<HashMap<String, String>>>,
users_per_game: usize, users_per_game: usize,
@@ -131,23 +168,24 @@ impl QueueHandler {
tokio::spawn(async move { tokio::spawn(async move {
loop { loop {
let now = chrono::Utc::now(); let now = chrono::Utc::now();
let mut to_start: Vec<(QueueKey, Vec<QueueUser>)> = Vec::new(); let mut to_start: Vec<(QueueKey, Queue)> = Vec::new();
let mut next_deadline: Option<chrono::DateTime<chrono::Utc>> = None; let mut next_deadline: Option<chrono::DateTime<chrono::Utc>> = None;
{ {
let mut lock = users_in_queue.lock().await; let mut lock = users_in_queue.lock().await;
let mut empty_keys: Vec<QueueKey> = Vec::new(); //let mut empty_keys: Vec<QueueKey> = Vec::new();
let mut expired_keys: Vec<QueueKey> = Vec::new(); let mut expired_keys: Vec<QueueKey> = Vec::new();
for (key, users) in lock.iter() { for (key, q_entry) in lock.iter() {
if users.is_empty() { if q_entry.users.is_empty() { continue; }
/*if q_entry.users.is_empty() && q_entry.platoons.is_empty() {
empty_keys.push(key.clone()); empty_keys.push(key.clone());
continue; continue;
} }*/
// first user is always oldest within a queue // first user is always oldest within a queue
let deadline = users[0].enqueued_at + autostart_after; let deadline = q_entry.users[0].enqueued_at + autostart_after;
if now >= deadline { if now >= deadline {
expired_keys.push(key.clone()); expired_keys.push(key.clone());
} else { } else {
@@ -158,23 +196,23 @@ impl QueueHandler {
} }
} }
for k in empty_keys { /*for k in empty_keys {
lock.remove(&k); lock.remove(&k);
} }*/
for k in expired_keys { for k in expired_keys {
if let Some(players) = lock.remove(&k) { if let Some(q_entry) = lock.remove(&k) {
if !players.is_empty() { if !q_entry.users.is_empty() {
to_start.push((k, players)); to_start.push((k, q_entry));
} }
} }
} }
} }
// start expired queues outside the lock // start expired queues outside the lock
for (key, players) in to_start { for (key, q_entry) in to_start {
// choose the oldest queued user's LobbyUser handle // choose the oldest queued user's LobbyUser handle
let starter = match players.first() { let starter = match q_entry.users.first() {
Some(p) => p.user.clone(), Some(p) => p.user.clone(),
None => continue, None => continue,
}; };
@@ -188,7 +226,7 @@ impl QueueHandler {
weapon_guesser.clone(), weapon_guesser.clone(),
users_per_game, users_per_game,
key, key,
players, q_entry,
starter.as_ref().as_ref(), starter.as_ref().as_ref(),
).await; ).await;
} }
@@ -208,7 +246,7 @@ impl QueueHandler {
}); });
} }
async fn enter_match(&self, key: QueueKey, players: Vec<QueueUser>, user: &(dyn oj_rc_core::persist::user::LobbyUser + Send + Sync)) { async fn enter_match(&self, key: QueueKey, q_entry: Queue, user: &(dyn oj_rc_core::persist::user::LobbyUser + Send + Sync)) {
Self::enter_match_static( Self::enter_match_static(
self.hostname.clone(), self.hostname.clone(),
self.hostport, self.hostport,
@@ -218,13 +256,13 @@ impl QueueHandler {
self.weapon_guesser.clone(), self.weapon_guesser.clone(),
self.users_per_game, self.users_per_game,
key, key,
players, q_entry,
user, user,
).await ).await
} }
#[allow(clippy::too_many_arguments)] #[allow(clippy::too_many_arguments)]
async fn enter_match_static(hostname: String, hostport: u16, network_conf: crate::data::network::NetworkConfigData, factory: std::sync::Arc<oj_rc_core::factory::Factory>, cpu_counter: std::sync::Arc<oj_rc_core::cubes::CpuListParser>, weapon_guesser: std::sync::Arc<oj_rc_core::cubes::WeaponListParser>, users_per_game: usize, key: QueueKey, mut players: Vec<QueueUser>, 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<oj_rc_core::factory::Factory>, cpu_counter: std::sync::Arc<oj_rc_core::cubes::CpuListParser>, weapon_guesser: std::sync::Arc<oj_rc_core::cubes::WeaponListParser>, users_per_game: usize, key: QueueKey, mut q_entry: Queue, user: &(dyn oj_rc_core::persist::user::LobbyUser + Send + Sync)) {
let guid_str = key.unique_guid(); let guid_str = key.unique_guid();
let game_desc = oj_rc_core::persist::user::GameDescriptor { let game_desc = oj_rc_core::persist::user::GameDescriptor {
guid: guid_str.clone(), guid: guid_str.clone(),
@@ -242,22 +280,26 @@ impl QueueHandler {
oj_rc_core::data::game_mode::GameMode::Pit => |i| i as i32, // each player is on a different team 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 _ => |i| (i % 2) as i32, // alternate teams
};*/ };*/
for (i, player) in players.iter_mut().enumerate() { let mut player_descs = Vec::with_capacity(q_entry.users.len());
player.player.team = team_picker.team(i); for (i, player) in q_entry.users.iter_mut().enumerate() {
let mut lobby_desc = oj_rc_core::persist::user::PlayerLobbyDescriptor {
user_id: player.user_id,
team: -1,
group: player.player.group.clone(),
public_id: player.player.name.clone(),
display_name: player.player.display_name.clone(),
};
let team = team_picker.choose_team(&guid_str, i, &lobby_desc);
lobby_desc.team = team;
player.player.team = team;
player_descs.push(lobby_desc);
} }
let player_descs = players.iter().map(|x| oj_rc_core::persist::user::PlayerLobbyDescriptor {
user_id: x.user_id,
team: x.player.team,
group: None, // TODO support platoons
public_id: x.player.name.clone(),
display_name: x.player.display_name.clone(),
}).collect();
let missing = users_per_game.saturating_sub(players.len()); 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) => { Ok(fakes) => {
let player_datas = players.iter().map(|x| x.player.clone()) let player_datas = q_entry.users.iter().map(|x| x.player.clone())
.chain(fakes.players.into_iter().map(|(desc, _emu)| desc),) .chain(fakes.players.into_iter().map(|(desc, _emu)| desc),)
.collect(); .collect();
let enter_battle_ev = crate::events::battle_enter::BattleEnter { let enter_battle_ev = crate::events::battle_enter::BattleEnter {
@@ -274,10 +316,10 @@ impl QueueHandler {
network_config: network_conf.clone(), network_config: network_conf.clone(),
}; };
let arc_event = std::sync::Arc::new(enter_battle_ev); let arc_event = std::sync::Arc::new(enter_battle_ev);
for player in players.iter() { for player in q_entry.users.iter() {
tokio::spawn(Self::send_events_to_player(arc_event.clone(), player.emitter.clone())); tokio::spawn(Self::send_events_to_player(arc_event.clone(), player.emitter.clone()));
} }
log::info!("{} players are entering match {}", players.len(), guid_str); log::info!("{} players are entering match {}", q_entry.users.len(), guid_str);
}, },
Err(e) => { Err(e) => {
if let Some(msg) = e.error_msg() { if let Some(msg) = e.error_msg() {
@@ -491,8 +533,55 @@ impl QueueHandler {
} }
} }
fn join_or_create_platoon(&self, new_player: QueueUser, platoon: PlatoonInfo, q_entry: &mut Queue) {
let new_player = PlatoonUser::from_queue_user(new_player);
log::debug!("Existing platoon count {}", q_entry.platoons.len());
if let Some(p_entry) = q_entry.platoons.get_mut(&platoon.platoon_id) {
// update
log::debug!("Adding user {} to existing platoon {} ({}/{})", new_player.0.user_id, platoon.platoon_id, p_entry.members.len() + 1, p_entry.total);
if platoon.is_leader {
p_entry.total = platoon.total;
}
p_entry.members.push(new_player);
let wants_usize = p_entry.total as usize;
if wants_usize == p_entry.members.len() {
log::info!("All members of platoon {} are in queue, actually adding them to queue", platoon.platoon_id);
for member in p_entry.members.iter() {
q_entry.users.push(member.clone_for_queue());
}
q_entry.users.sort_by_key(|u| u.enqueued_at);
}
} else {
// create
log::debug!("Adding user {} to new platoon {} (1/{})", new_player.0.user_id, platoon.platoon_id, platoon.total);
q_entry.platoons.insert(platoon.platoon_id.clone(), Platoon {
total: if platoon.is_leader { platoon.total } else { u8::MAX },
members: vec![new_player],
});
}
}
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: std::sync::Arc<Box<dyn oj_rc_core::persist::user::User<()> + Send + Sync>>, event_emitter: polariton_server::events::EventEmitter) { fn platoon_leave_queue(&self, user_id: i32, platoon_id: String, q_entry: &mut Queue) {
let needs_remove = if let Some(platoon) = q_entry.platoons.get_mut(&platoon_id) {
let wants_usize = platoon.total as usize;
if wants_usize == platoon.members.len() {
// other platoon members are in queue, they also need to be removed
let some_group = Some(platoon_id.clone());
q_entry.users.retain(|q_user| q_user.player.group != some_group);
log::info!("Removed platoon {} from actual queue because user {} left queue", platoon_id, user_id);
}
platoon.members.retain(|p_mem| p_mem.0.user_id != user_id);
platoon.members.is_empty()
} else {
log::error!("Failed to find platoon {} to remove user {}: the lobby is in a bad state", platoon_id, user_id);
false
};
if needs_remove {
q_entry.platoons.remove(&platoon_id);
}
}
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: std::sync::Arc<Box<dyn oj_rc_core::persist::user::User<()> + Send + Sync>>, event_emitter: polariton_server::events::EventEmitter, platoon: Option<PlatoonInfo>) {
if !self.is_enabled { if !self.is_enabled {
event_emitter.emit(crate::events::enqueue_error::QueueJoinError { event_emitter.emit(crate::events::enqueue_error::QueueJoinError {
code: oj_rc_core::data::error_codes::LobbyReasonCode::NoSuitableLobbyFound as i16, code: oj_rc_core::data::error_codes::LobbyReasonCode::NoSuitableLobbyFound as i16,
@@ -508,7 +597,10 @@ impl QueueHandler {
}; };
let lobby_user = user.as_ref().as_ref(); let lobby_user = user.as_ref().as_ref();
match lobby_user.player_data(&self.cpu_counter).await { match lobby_user.player_data(&self.cpu_counter).await {
Ok(player_data) => { Ok(mut player_data) => {
if let Some(platoon) = &platoon {
player_data.group = Some(platoon.platoon_id.clone());
}
let new_player = QueueUser { let new_player = QueueUser {
emitter: event_emitter, emitter: event_emitter,
player: player_data, player: player_data,
@@ -516,25 +608,30 @@ impl QueueHandler {
enqueued_at: chrono::Utc::now(), enqueued_at: chrono::Utc::now(),
user: user.clone(), user: user.clone(),
}; };
let mut lock = self.users_in_queue.lock().await; let mut lock = self.users_in_queue.lock().await;
for q_key in lock.keys() {
log::debug!("Existing queue for {} (matches joiner? {})", q_key.short(), q_key.short().to_string() == key.short().to_string());
}
// handle game event change // handle game event change
if !lock.contains_key(&key) && lock.len() == 1 { if !lock.contains_key(&key) && !lock.is_empty() {
log::debug!("Game event change detected"); log::debug!("Game event change detected");
match self.change_strategy { match self.change_strategy {
GamemodeChangeStrategy::Upgrade => { GamemodeChangeStrategy::Upgrade => {
let mut new_queue_map = std::collections::HashMap::<QueueKey, Vec<QueueUser>>::with_capacity(lock.len()); let mut new_queue_map = std::collections::HashMap::<QueueKey, Queue>::with_capacity(lock.len());
let mut count = 0; let mut count = 0;
for (_key, mut users) in lock.drain() { for (_key, mut q_entry) in lock.drain() {
count += users.len(); count += q_entry.users.len();
if let Some(values) = new_queue_map.get_mut(&key) { if let Some(values) = new_queue_map.get_mut(&key) {
values.append(&mut users); values.users.append(&mut q_entry.users);
values.platoons.extend(q_entry.platoons.into_iter());
} else { } else {
new_queue_map.insert(key.clone(), users); new_queue_map.insert(key.clone(), q_entry);
} }
} }
for users in new_queue_map.values_mut() { for q_entry in new_queue_map.values_mut() {
users.sort_by_key(|u| u.enqueued_at); q_entry.users.sort_by_key(|u| u.enqueued_at);
} }
*lock = new_queue_map; *lock = new_queue_map;
if count != 0 { if count != 0 {
@@ -542,18 +639,31 @@ impl QueueHandler {
} }
}, },
GamemodeChangeStrategy::Notify => { GamemodeChangeStrategy::Notify => {
let mut count = 0; let mut seen = std::collections::HashSet::new();
for (_key, users) in lock.drain() { for (_key, q_entry) in lock.drain() {
count += users.len(); for player in q_entry.users {
for player in users { if seen.contains(&player.user_id) { continue; }
log::debug!("Notifying user {} of event expiry", player.user_id);
player.emitter.emit(crate::events::enqueue_error::QueueJoinError { player.emitter.emit(crate::events::enqueue_error::QueueJoinError {
code: oj_rc_core::data::error_codes::LobbyReasonCode::EventSystemExpired as i16, code: oj_rc_core::data::error_codes::LobbyReasonCode::EventSystemExpired as i16,
text: "Please requeue".to_owned(), text: "Please requeue".to_owned(),
}); });
seen.insert(player.user_id);
}
for platoon in q_entry.platoons.into_values() {
for player in platoon.members {
if seen.contains(&player.0.user_id) { continue; }
log::debug!("Notifying user {} of event expiry", player.0.user_id);
player.0.emitter.emit(crate::events::enqueue_error::QueueJoinError {
code: oj_rc_core::data::error_codes::LobbyReasonCode::EventSystemExpired as i16,
text: "Please requeue".to_owned(),
});
seen.insert(player.0.user_id);
}
} }
} }
if count != 0 { if !seen.is_empty() {
log::info!("Notified {} users in queue of new gamemode {}", count, key.short()); log::info!("Notified {} users in queue of new gamemode {}", seen.len(), key.short());
} }
}, },
GamemodeChangeStrategy::Ignore => { GamemodeChangeStrategy::Ignore => {
@@ -561,21 +671,44 @@ impl QueueHandler {
} }
} }
} }
let players_len = if let Some(players) = lock.get_mut(&key) { let players_len = if let Some(q_entry) = lock.get_mut(&key) {
log::info!("User {} entered queue for existing match {}", new_player.user_id, key.short()); if let Some(platoon) = platoon {
players.push(new_player); log::info!("User {} platooned {} queue for existing match {}", new_player.user_id, platoon.platoon_id, key.short());
players.len() self.join_or_create_platoon(new_player, platoon, q_entry);
} else {
log::info!("User {} entered queue for existing match {}", new_player.user_id, key.short());
q_entry.users.push(new_player);
}
q_entry.users.len()
} else { } else {
log::info!("User {} entered queue for new match {}", new_player.user_id, key.short()); if let Some(platoon) = platoon {
lock.insert(key.clone(), vec![new_player]); log::info!("User {} platooned {} queue for new match {}", new_player.user_id, platoon.platoon_id, key.short());
1 let mut q_entry = Queue {
users: Vec::default(),
platoons: HashMap::default(),
};
self.join_or_create_platoon(new_player, platoon, &mut q_entry);
lock.insert(key.clone(), q_entry);
0
} else {
log::info!("User {} entered queue for new match {}", new_player.user_id, key.short());
lock.insert(key.clone(), Queue {
users: vec![new_player],
platoons: HashMap::default(),
});
1
}
}; };
for q_key in lock.keys() {
log::debug!("Now queue for {} (matches joiner? {})", q_key.short(), q_key.short().to_string() == key.short().to_string());
}
let game_ready = players_len >= self.users_per_game; let game_ready = players_len >= self.users_per_game;
let players = if game_ready { lock.remove(&key) } else { None }; let players = if game_ready { lock.remove(&key) } else { None };
drop(lock); drop(lock);
if let Some(players) = players { if let Some(players) = players {
self.enter_match(key, players, lobby_user).await; self.enter_match(key, players, lobby_user).await;
} }
log::debug!("join_queue complete success");
}, },
Err(e) => { Err(e) => {
event_emitter.emit(crate::events::enqueue_error::QueueJoinError { event_emitter.emit(crate::events::enqueue_error::QueueJoinError {
@@ -598,13 +731,24 @@ impl QueueHandler {
log::info!("User {} was removed from custom game {} queue", user_id, session_id); log::info!("User {} was removed from custom game {} queue", user_id, session_id);
} else { } else {
// fallback to regular multiplayer // fallback to regular multiplayer
for (queue_key, queue) in self.users_in_queue.lock().await.iter_mut() { let mut to_remove = Vec::new();
if let Some((i, _)) = queue.iter().enumerate().find(|(_, user)| user.user_id == user_id) { let mut q_lock = self.users_in_queue.lock().await;
queue.remove(i); for (queue_key, queue) in q_lock.iter_mut() {
if let Some((i, _)) = queue.users.iter().enumerate().find(|(_, user)| user.user_id == user_id) {
let q_user = queue.users.remove(i);
if let Some(platoon_id) = q_user.player.group {
self.platoon_leave_queue(user_id, platoon_id, queue);
}
if queue.users.is_empty() && queue.platoons.is_empty() {
to_remove.push(queue_key.to_owned());
}
log::info!("User {} was removed from queue {}", user_id, queue_key.short()); log::info!("User {} was removed from queue {}", user_id, queue_key.short());
break; break;
} }
} }
for key in to_remove {
q_lock.remove(&key);
}
} }
} }
} }

View File

@@ -62,6 +62,15 @@ impl <C: Send + 'static> SimpleOperation<C> for QueueJoinProvider {
current_lobby.auto_heal, current_lobby.auto_heal,
user_info.clone(), user_info.clone(),
events.to_owned(), events.to_owned(),
if group_size > 1 {
Some(crate::lobby::PlatoonInfo {
total: group_size as _,
platoon_id: group_id.string,
is_leader,
})
} else {
None
}
).await; ).await;
} }
} else { } else {

View File

@@ -10,7 +10,7 @@ pub fn handler(init_ctx: &crate::InitConfig) -> OperationsHandler<crate::UserTy>
.modify(oj_rc_core::polariton::RcOpModifier) .modify(oj_rc_core::polariton::RcOpModifier)
.add(more_auth::MoreLobbyAuth) .add(more_auth::MoreLobbyAuth)
//.add(eac::EacChallengeIgnorer) //.add(eac::EacChallengeIgnorer)
//.add(polariton_server::operations::Ack::<2, _>::default()) .add(polariton_server::operations::Ack::<2, _>::default())
.add(no_quit::quit_blocker_provider()) .add(no_quit::quit_blocker_provider())
.add(join_queue::join_queue_provider(&init_ctx.queue)) .add(join_queue::join_queue_provider(&init_ctx.queue))
} }

View File

@@ -23,7 +23,7 @@ impl SimpleOperation<crate::data::custom::CustomType> for PlatoonPendingInviter
let user_info = user.user()?; let user_info = user.user()?;
let my_public_id = user_info.public_id(); let my_public_id = user_info.public_id();
log::debug!("Checking for pending platoon invite of user {}", my_public_id); log::debug!("Checking for pending platoon invite of user {}", my_public_id);
if let Some(platoon_id) = self.social.platoon_of_user(my_public_id).await { if let Some(platoon_id) = self.social.platoon_invite_of_user(my_public_id).await {
let members = self.social.users_of_platoon(&platoon_id).await; let members = self.social.users_of_platoon(&platoon_id).await;
let social_infos = user_info.list_social_info(&[members[0].public_id.clone()]).await?; let social_infos = user_info.list_social_info(&[members[0].public_id.clone()]).await?;
params.insert(INVITER_NAME_PARAM_KEY, Typed::Str(social_infos[0].public_id.clone().into())); params.insert(INVITER_NAME_PARAM_KEY, Typed::Str(social_infos[0].public_id.clone().into()));

View File

@@ -92,6 +92,20 @@ impl SocialMesh {
.map(|x| x.to_owned()) .map(|x| x.to_owned())
} }
pub async fn platoon_invite_of_user(&self, public_id: &str) -> Option<String> {
if let Some(platoon_id) = self.platoons.platoon_by_user.read().await.get(public_id) {
if let Some(platoon) = self.platoons.platoon_by_id.read().await.get(platoon_id) {
let myself = platoon.iter()
.find(|mem| mem.public_id == public_id)
.unwrap();
if matches!(myself.status, crate::data::platoon::MemberStatus::Invited) {
return Some(platoon_id.to_owned());
}
}
}
None
}
pub async fn users_of_platoon(&self, platoon_id: &str) -> Vec<PlatoonMemberInfo> { pub async fn users_of_platoon(&self, platoon_id: &str) -> Vec<PlatoonMemberInfo> {
self.platoons.platoon_by_id.read().await self.platoons.platoon_by_id.read().await
.get(platoon_id) .get(platoon_id)
@@ -150,12 +164,14 @@ impl SocialMesh {
} }
pub async fn remove_user_from_platoon(&self, public_id: &str) -> bool { pub async fn remove_user_from_platoon(&self, public_id: &str) -> bool {
if let Some(platoon_id) = self.platoons.platoon_by_user.write().await.remove(public_id) { let mut platoon_by_user_lock = self.platoons.platoon_by_user.write().await;
if let Some(platoon_id) = platoon_by_user_lock.remove(public_id) {
let mut platoon_by_id_lock = self.platoons.platoon_by_id.write().await; let mut platoon_by_id_lock = self.platoons.platoon_by_id.write().await;
if let Some(platoon) = platoon_by_id_lock.get_mut(&platoon_id) { if let Some(platoon) = platoon_by_id_lock.get_mut(&platoon_id) {
platoon.retain(|member| member.public_id != public_id); platoon.retain(|member| member.public_id != public_id);
if platoon.is_empty() { if platoon.len() <= 1 {
platoon_by_id_lock.remove(&platoon_id); platoon_by_id_lock.remove(&platoon_id);
platoon_by_user_lock.retain(|_pub_id, platoon_id_val| platoon_id_val != &platoon_id);
} }
true true
} else { } else {