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)]
|
||||
|
||||
Reference in New Issue
Block a user