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:
@@ -2,25 +2,6 @@ use serde::{Serialize, Deserialize};
|
||||
|
||||
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 {
|
||||
match client_emu {
|
||||
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 {
|
||||
crate::data::game_mode::GameMode::Pit => TeamChooser::OnePer,
|
||||
_ => TeamChooser::Alternating,
|
||||
crate::data::game_mode::GameMode::Pit => super::StandardTeamChooser::OnePer,
|
||||
_ => super::StandardTeamChooser::alternating(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -59,7 +40,7 @@ impl super::LobbyUser for UserData {
|
||||
factory: &dyn oj_rc_factory::VehicleFactoryAdapter,
|
||||
cpu_counter: &crate::cubes::CpuListParser,
|
||||
weapon_lister: &crate::cubes::WeaponListParser,
|
||||
chooser: &TeamChooser,
|
||||
chooser: &dyn super::TeamChooser,
|
||||
missing_players: usize,
|
||||
) -> Result<super::FakePlayers, polariton_server::operations::SimpleOpError> {
|
||||
let now = chrono::Utc::now().timestamp();
|
||||
@@ -76,8 +57,8 @@ impl super::LobbyUser for UserData {
|
||||
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 filler_players = self.generate_filler_players_data(guid, &players, factory, cpu_counter, weapon_lister, chooser, missing_players).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(&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 {
|
||||
id: oj_rc_database::sea_orm::ActiveValue::NotSet,
|
||||
@@ -102,6 +83,17 @@ impl super::LobbyUser for UserData {
|
||||
let players_len = 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()
|
||||
.enumerate()
|
||||
.map(|(i, player)| {
|
||||
@@ -112,7 +104,7 @@ impl super::LobbyUser for UserData {
|
||||
creation_time: oj_rc_database::sea_orm::ActiveValue::Set(now),
|
||||
player_id: oj_rc_database::sea_orm::ActiveValue::Set((i as u8) as _),
|
||||
team: oj_rc_database::sea_orm::ActiveValue::Set(player.team),
|
||||
group: oj_rc_database::sea_orm::ActiveValue::Set(player.group),
|
||||
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),
|
||||
public_id: oj_rc_database::sea_orm::ActiveValue::Set(player.public_id),
|
||||
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),
|
||||
player_id: oj_rc_database::sea_orm::ActiveValue::Set((i as u8) as _),
|
||||
team: oj_rc_database::sea_orm::ActiveValue::Set(player.team),
|
||||
group: oj_rc_database::sea_orm::ActiveValue::Set(player.group),
|
||||
group: oj_rc_database::sea_orm::ActiveValue::Set(None),
|
||||
is_claimed: oj_rc_database::sea_orm::ActiveValue::Set(false),
|
||||
public_id: oj_rc_database::sea_orm::ActiveValue::Set(player.public_id),
|
||||
display_name: oj_rc_database::sea_orm::ActiveValue::Set(player.display_name),
|
||||
|
||||
@@ -18,13 +18,14 @@ pub use intercom::generate_token as generate_intercom_token;
|
||||
|
||||
mod multiplayer;
|
||||
mod lobby;
|
||||
pub use lobby::TeamChooser;
|
||||
mod common;
|
||||
mod chat;
|
||||
mod social;
|
||||
mod singleplayer;
|
||||
mod factory;
|
||||
mod userless;
|
||||
mod team;
|
||||
pub use team::{TeamChooser, StandardTeamChooser};
|
||||
|
||||
pub const TOKEN_SECRET_FILENAME: &str = "token_secret.key";
|
||||
|
||||
|
||||
@@ -12,18 +12,24 @@ impl UserData {
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub(super) async fn generate_fake_players_data(
|
||||
&self,
|
||||
_guid: i64,
|
||||
real_players: &[super::PlayerLobbyDescriptor],
|
||||
guid_str: &str,
|
||||
factory: &dyn oj_rc_factory::VehicleFactoryAdapter,
|
||||
cpu_counter: &crate::cubes::CpuListParser,
|
||||
weapon_lister: &crate::cubes::WeaponListParser,
|
||||
chooser: &super::TeamChooser,
|
||||
chooser: &dyn super::TeamChooser,
|
||||
fake_players: &[crate::persist::config::FakePlayer],
|
||||
total_offset: usize,
|
||||
) -> 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 fake_i = real_players.len();
|
||||
for fake in fake_players.iter() {
|
||||
for (i, fake) in fake_players.iter().enumerate() {
|
||||
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 = (
|
||||
crate::data::player_data::PlayerData {
|
||||
name: fake.vehicle.username.clone(),
|
||||
@@ -33,12 +39,7 @@ impl UserData {
|
||||
robot_name: vehicle.robot_name,
|
||||
robot_map: vehicle.robot_map,
|
||||
group: None,
|
||||
team: fake.team.map(|t| t as i32)
|
||||
.unwrap_or_else(|| {
|
||||
let assigned_team = chooser.team(fake_i);
|
||||
fake_i += 1;
|
||||
assigned_team
|
||||
}),
|
||||
team: chooser.choose_team(guid_str, total_offset + i, &fake_lobby_desc),
|
||||
has_premium: true,
|
||||
robot_uuid: vehicle.robot_uuid,
|
||||
cpu: vehicle.cpu,
|
||||
@@ -61,43 +62,44 @@ impl UserData {
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub(super) async fn generate_forced_fake_players_data(
|
||||
&self,
|
||||
guid: i64,
|
||||
guid_str: &str,
|
||||
real_players: &[super::PlayerLobbyDescriptor],
|
||||
factory: &dyn oj_rc_factory::VehicleFactoryAdapter,
|
||||
cpu_counter: &crate::cubes::CpuListParser,
|
||||
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> {
|
||||
let offset = real_players.len();
|
||||
self.generate_fake_players_data(
|
||||
guid,
|
||||
real_players,
|
||||
guid_str,
|
||||
factory,
|
||||
cpu_counter,
|
||||
weapon_lister,
|
||||
chooser,
|
||||
&self.fake_players,
|
||||
offset,
|
||||
).await
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub(super) async fn generate_filler_players_data(
|
||||
&self,
|
||||
guid: i64,
|
||||
real_players: &[super::PlayerLobbyDescriptor],
|
||||
guid_str: &str,
|
||||
factory: &dyn oj_rc_factory::VehicleFactoryAdapter,
|
||||
cpu_counter: &crate::cubes::CpuListParser,
|
||||
weapon_lister: &crate::cubes::WeaponListParser,
|
||||
chooser: &super::TeamChooser,
|
||||
chooser: &dyn super::TeamChooser,
|
||||
count: usize,
|
||||
total_offset: usize,
|
||||
) -> Result<Vec<(crate::data::player_data::PlayerData, crate::persist::config::ClientEmulator)>, polariton_server::operations::SimpleOpError> {
|
||||
self.generate_fake_players_data(
|
||||
guid,
|
||||
real_players,
|
||||
guid_str,
|
||||
factory,
|
||||
cpu_counter,
|
||||
weapon_lister,
|
||||
chooser,
|
||||
&self.filler_players[0..count],
|
||||
total_offset
|
||||
).await
|
||||
}
|
||||
}
|
||||
|
||||
116
rc_core/src/persist/user/team.rs
Normal file
116
rc_core/src/persist/user/team.rs
Normal 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
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -279,9 +279,9 @@ pub enum UserRole {
|
||||
pub trait LobbyUser {
|
||||
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 team_chooser(&self, game: &GameDescriptor) -> super::TeamChooser;
|
||||
async fn team_chooser(&self, game: &GameDescriptor) -> super::StandardTeamChooser;
|
||||
#[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)]
|
||||
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 public_id: String,
|
||||
pub display_name: String,
|
||||
pub group: Option<i32>,
|
||||
pub group: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
use std::{collections::HashMap, hash::Hash};
|
||||
|
||||
use oj_rc_core::persist::user::TeamChooser;
|
||||
|
||||
#[derive(Clone, Copy)]
|
||||
pub enum GamemodeChangeStrategy {
|
||||
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 {
|
||||
emitter: polariton_server::events::EventEmitter,
|
||||
player: oj_rc_core::data::player_data::PlayerData,
|
||||
@@ -75,7 +112,7 @@ struct CustomGameQueueUser {
|
||||
}
|
||||
|
||||
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>>>,
|
||||
custom_game_for_user: std::sync::Arc<tokio::sync::RwLock<HashMap<String, String>>>,
|
||||
users_per_game: usize,
|
||||
@@ -131,23 +168,24 @@ impl QueueHandler {
|
||||
tokio::spawn(async move {
|
||||
loop {
|
||||
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 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();
|
||||
|
||||
for (key, users) in lock.iter() {
|
||||
if users.is_empty() {
|
||||
for (key, q_entry) in lock.iter() {
|
||||
if q_entry.users.is_empty() { continue; }
|
||||
/*if q_entry.users.is_empty() && q_entry.platoons.is_empty() {
|
||||
empty_keys.push(key.clone());
|
||||
continue;
|
||||
}
|
||||
}*/
|
||||
|
||||
// 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 {
|
||||
expired_keys.push(key.clone());
|
||||
} else {
|
||||
@@ -158,23 +196,23 @@ impl QueueHandler {
|
||||
}
|
||||
}
|
||||
|
||||
for k in empty_keys {
|
||||
/*for k in empty_keys {
|
||||
lock.remove(&k);
|
||||
}
|
||||
}*/
|
||||
|
||||
for k in expired_keys {
|
||||
if let Some(players) = lock.remove(&k) {
|
||||
if !players.is_empty() {
|
||||
to_start.push((k, players));
|
||||
if let Some(q_entry) = lock.remove(&k) {
|
||||
if !q_entry.users.is_empty() {
|
||||
to_start.push((k, q_entry));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 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
|
||||
let starter = match players.first() {
|
||||
let starter = match q_entry.users.first() {
|
||||
Some(p) => p.user.clone(),
|
||||
None => continue,
|
||||
};
|
||||
@@ -188,7 +226,7 @@ impl QueueHandler {
|
||||
weapon_guesser.clone(),
|
||||
users_per_game,
|
||||
key,
|
||||
players,
|
||||
q_entry,
|
||||
starter.as_ref().as_ref(),
|
||||
).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.hostname.clone(),
|
||||
self.hostport,
|
||||
@@ -218,13 +256,13 @@ impl QueueHandler {
|
||||
self.weapon_guesser.clone(),
|
||||
self.users_per_game,
|
||||
key,
|
||||
players,
|
||||
q_entry,
|
||||
user,
|
||||
).await
|
||||
}
|
||||
|
||||
#[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 game_desc = oj_rc_core::persist::user::GameDescriptor {
|
||||
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
|
||||
_ => |i| (i % 2) as i32, // alternate teams
|
||||
};*/
|
||||
for (i, player) in players.iter_mut().enumerate() {
|
||||
player.player.team = team_picker.team(i);
|
||||
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 {
|
||||
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 {
|
||||
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),)
|
||||
.collect();
|
||||
let enter_battle_ev = crate::events::battle_enter::BattleEnter {
|
||||
@@ -274,10 +316,10 @@ impl QueueHandler {
|
||||
network_config: network_conf.clone(),
|
||||
};
|
||||
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()));
|
||||
}
|
||||
log::info!("{} players are entering match {}", players.len(), guid_str);
|
||||
log::info!("{} players are entering match {}", q_entry.users.len(), guid_str);
|
||||
},
|
||||
Err(e) => {
|
||||
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 {
|
||||
event_emitter.emit(crate::events::enqueue_error::QueueJoinError {
|
||||
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();
|
||||
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 {
|
||||
emitter: event_emitter,
|
||||
player: player_data,
|
||||
@@ -516,25 +608,30 @@ impl QueueHandler {
|
||||
enqueued_at: chrono::Utc::now(),
|
||||
user: user.clone(),
|
||||
};
|
||||
|
||||
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
|
||||
if !lock.contains_key(&key) && lock.len() == 1 {
|
||||
if !lock.contains_key(&key) && !lock.is_empty() {
|
||||
log::debug!("Game event change detected");
|
||||
match self.change_strategy {
|
||||
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;
|
||||
for (_key, mut users) in lock.drain() {
|
||||
count += users.len();
|
||||
for (_key, mut q_entry) in lock.drain() {
|
||||
count += q_entry.users.len();
|
||||
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 {
|
||||
new_queue_map.insert(key.clone(), users);
|
||||
new_queue_map.insert(key.clone(), q_entry);
|
||||
}
|
||||
}
|
||||
|
||||
for users in new_queue_map.values_mut() {
|
||||
users.sort_by_key(|u| u.enqueued_at);
|
||||
for q_entry in new_queue_map.values_mut() {
|
||||
q_entry.users.sort_by_key(|u| u.enqueued_at);
|
||||
}
|
||||
*lock = new_queue_map;
|
||||
if count != 0 {
|
||||
@@ -542,18 +639,31 @@ impl QueueHandler {
|
||||
}
|
||||
},
|
||||
GamemodeChangeStrategy::Notify => {
|
||||
let mut count = 0;
|
||||
for (_key, users) in lock.drain() {
|
||||
count += users.len();
|
||||
for player in users {
|
||||
let mut seen = std::collections::HashSet::new();
|
||||
for (_key, q_entry) in lock.drain() {
|
||||
for player in q_entry.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 {
|
||||
code: oj_rc_core::data::error_codes::LobbyReasonCode::EventSystemExpired as i16,
|
||||
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 {
|
||||
log::info!("Notified {} users in queue of new gamemode {}", count, key.short());
|
||||
if !seen.is_empty() {
|
||||
log::info!("Notified {} users in queue of new gamemode {}", seen.len(), key.short());
|
||||
}
|
||||
},
|
||||
GamemodeChangeStrategy::Ignore => {
|
||||
@@ -561,21 +671,44 @@ impl QueueHandler {
|
||||
}
|
||||
}
|
||||
}
|
||||
let players_len = if let Some(players) = lock.get_mut(&key) {
|
||||
log::info!("User {} entered queue for existing match {}", new_player.user_id, key.short());
|
||||
players.push(new_player);
|
||||
players.len()
|
||||
let players_len = if let Some(q_entry) = lock.get_mut(&key) {
|
||||
if let Some(platoon) = platoon {
|
||||
log::info!("User {} platooned {} queue for existing match {}", new_player.user_id, platoon.platoon_id, key.short());
|
||||
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 {
|
||||
log::info!("User {} entered queue for new match {}", new_player.user_id, key.short());
|
||||
lock.insert(key.clone(), vec![new_player]);
|
||||
1
|
||||
if let Some(platoon) = platoon {
|
||||
log::info!("User {} platooned {} queue for new match {}", new_player.user_id, platoon.platoon_id, key.short());
|
||||
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 players = if game_ready { lock.remove(&key) } else { None };
|
||||
drop(lock);
|
||||
if let Some(players) = players {
|
||||
self.enter_match(key, players, lobby_user).await;
|
||||
}
|
||||
log::debug!("join_queue complete success");
|
||||
},
|
||||
Err(e) => {
|
||||
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);
|
||||
} else {
|
||||
// fallback to regular multiplayer
|
||||
for (queue_key, queue) in self.users_in_queue.lock().await.iter_mut() {
|
||||
if let Some((i, _)) = queue.iter().enumerate().find(|(_, user)| user.user_id == user_id) {
|
||||
queue.remove(i);
|
||||
let mut to_remove = Vec::new();
|
||||
let mut q_lock = self.users_in_queue.lock().await;
|
||||
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());
|
||||
break;
|
||||
}
|
||||
}
|
||||
for key in to_remove {
|
||||
q_lock.remove(&key);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -62,6 +62,15 @@ impl <C: Send + 'static> SimpleOperation<C> for QueueJoinProvider {
|
||||
current_lobby.auto_heal,
|
||||
user_info.clone(),
|
||||
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;
|
||||
}
|
||||
} else {
|
||||
|
||||
@@ -10,7 +10,7 @@ pub fn handler(init_ctx: &crate::InitConfig) -> OperationsHandler<crate::UserTy>
|
||||
.modify(oj_rc_core::polariton::RcOpModifier)
|
||||
.add(more_auth::MoreLobbyAuth)
|
||||
//.add(eac::EacChallengeIgnorer)
|
||||
//.add(polariton_server::operations::Ack::<2, _>::default())
|
||||
.add(polariton_server::operations::Ack::<2, _>::default())
|
||||
.add(no_quit::quit_blocker_provider())
|
||||
.add(join_queue::join_queue_provider(&init_ctx.queue))
|
||||
}
|
||||
|
||||
@@ -23,7 +23,7 @@ impl SimpleOperation<crate::data::custom::CustomType> for PlatoonPendingInviter
|
||||
let user_info = user.user()?;
|
||||
let my_public_id = user_info.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 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()));
|
||||
|
||||
@@ -92,6 +92,20 @@ impl SocialMesh {
|
||||
.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> {
|
||||
self.platoons.platoon_by_id.read().await
|
||||
.get(platoon_id)
|
||||
@@ -150,12 +164,14 @@ impl SocialMesh {
|
||||
}
|
||||
|
||||
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;
|
||||
if let Some(platoon) = platoon_by_id_lock.get_mut(&platoon_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_user_lock.retain(|_pub_id, platoon_id_val| platoon_id_val != &platoon_id);
|
||||
}
|
||||
true
|
||||
} else {
|
||||
|
||||
Reference in New Issue
Block a user