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

Implement lobby parts of custom games #38

This commit is contained in:
NG (Graham)
2026-03-17 21:52:15 -04:00
parent af04c4093c
commit 6a953bc613
32 changed files with 1045 additions and 61 deletions

1
Cargo.lock generated
View File

@@ -2576,6 +2576,7 @@ dependencies = [
"chrono",
"clap",
"env_logger",
"futures",
"git-version",
"log",
"oj_polariton_auth",

View File

@@ -0,0 +1,63 @@
use actix_web::{rt, web::{Payload, Data, Json, Path}, Error, HttpRequest, HttpResponse, get, post};
//use actix_ws::AggregatedMessage;
//use futures::StreamExt as _;
#[get("/intercom/userless/.oj_lobby")]
pub async fn lobby_state_ws(req: HttpRequest, stream: Payload, auth: Data<super::IntercomAuth>, reg: Data<super::Users>) -> Result<HttpResponse, Error> {
auth.validate(&req, "state/.oj_lobby")?;
let (res, mut session, _stream) = actix_ws::handle(&req, stream)?;
/*let mut stream = stream
.aggregate_continuations()
.max_continuation_size(2_usize.pow(20)); // aggregate continuation frames up to 1MiB
*/
let (tx, mut rx) = tokio::sync::mpsc::channel(16);
reg.register_lobby_state_service(tx).await;
log::debug!("Registered lobby state intercom websocket");
// start task but don't wait for it
rt::spawn(async move {
let mut is_ok = false;
while let Some(op) = rx.recv().await {
match op {
super::IntercomOp::Message(msg) => {
if let Err(e) = session.text(serde_json::to_string(&msg).unwrap()).await {
log::warn!("Failed to send lobby state intercom message: {}", e);
break;
}
},
super::IntercomOp::Info(info) => {
match info {
super::IntercomInfo::Close => {
is_ok = true;
break;
},
}
}
}
}
if !is_ok {
reg.remove_lobby_state_service().await;
}
rx.close();
session.close(Some(actix_ws::CloseReason {
code: actix_ws::CloseCode::Normal,
description: Some("End of channel".to_owned()),
})).await.expect("Failed to close a services intercom websocket session");
log::debug!("Lobby state intercom websocket closed");
});
// respond immediately with response connected to WS session
Ok(res)
}
#[post("/intercom/.oj_lobby/{name}/state")]
pub async fn lobby_state_msg(req: HttpRequest, body: Json<oj_rc_core::persist::user::intercom::IntercomLobbyStateMessage>, auth: Data<super::IntercomAuth>, reg: Data<super::Users>, name: Path<String>) -> Result<HttpResponse, super::IntercomOpError> {
log::debug!("Got lobby state intercom message");
auth.validate(&req, &format!(".oj_lobby/{}/state", name))?;
log::debug!("Authenticated intercom lobby state message from {}", name);
reg.broadcast_lobby_message(body.0).await;
Ok(HttpResponse::NoContent().finish())
}

View File

@@ -10,11 +10,17 @@ pub use user_registry::Users;
mod status;
pub use status::{status_set, status_get};
enum IntercomOp {
Message(oj_rc_core::persist::user::intercom::IntercomWebServiceUserMessage),
mod lobby;
pub use lobby::{lobby_state_ws, lobby_state_msg};
enum IntercomOp<M: serde::Serialize + serde::Deserialize<'static> + 'static> {
Message(M),
Info(IntercomInfo),
}
enum IntercomInfo {
Close,
}
type WebServicesIntercomOp = IntercomOp<oj_rc_core::persist::user::intercom::IntercomWebServiceUserMessage>;
type LobbyStateIntercomOp = IntercomOp<oj_rc_core::persist::user::intercom::IntercomLobbyStateMessage>;

View File

@@ -13,7 +13,7 @@ pub async fn services_ws(req: HttpRequest, stream: Payload, auth: Data<super::In
*/
let (tx, mut rx) = tokio::sync::mpsc::channel(16);
reg.register_service(name.clone(), tx).await;
reg.register_web_service(name.clone(), tx).await;
log::debug!("Registered web services intercom websocket for user {}", name);
// start task but don't wait for it
@@ -39,7 +39,7 @@ pub async fn services_ws(req: HttpRequest, stream: Payload, auth: Data<super::In
}
if !is_ok {
reg.remove_service(name.clone()).await;
reg.remove_web_service(name.clone()).await;
}
rx.close();
session.close(Some(actix_ws::CloseReason {
@@ -58,6 +58,6 @@ pub async fn service_msg(req: HttpRequest, body: Json<oj_rc_core::persist::user:
log::debug!("Got intercom message from {} to {:?}", name, body.public_ids.as_slice());
auth.validate(&req, &format!(".oj_services/{}/messages", name))?;
log::debug!("Authenticated intercom message from {} to {:?}", name, body.public_ids.as_slice());
reg.broadcast_service_message(body.0).await;
reg.broadcast_web_service_message(body.0).await;
Ok(HttpResponse::NoContent().finish())
}

View File

@@ -1,18 +1,26 @@
struct ServiceListeners {
web_services: tokio::sync::RwLock<std::collections::HashMap<String, tokio::sync::mpsc::Sender<super::WebServicesIntercomOp>>>,
lobby_state: tokio::sync::RwLock<Option<tokio::sync::mpsc::Sender<super::LobbyStateIntercomOp>>>,
}
pub struct Users {
service_listeners: tokio::sync::RwLock<std::collections::HashMap<String, tokio::sync::mpsc::Sender<super::IntercomOp>>>,
service_listeners: ServiceListeners,
service_status: tokio::sync::RwLock<std::collections::HashMap<String, oj_serdes::ServerStatus>>,
}
impl Users {
pub fn new() -> Self {
Self {
service_listeners: tokio::sync::RwLock::new(std::collections::HashMap::with_capacity(16)),
service_listeners: ServiceListeners {
web_services: tokio::sync::RwLock::new(std::collections::HashMap::with_capacity(16)),
lobby_state: tokio::sync::RwLock::new(None),
},
service_status: tokio::sync::RwLock::new(std::collections::HashMap::with_capacity(16)),
}
}
pub(super) async fn register_service(&self, public_id: String, sender: tokio::sync::mpsc::Sender<super::IntercomOp>) {
let mut write_lock = self.service_listeners.write().await;
pub(super) async fn register_web_service(&self, public_id: String, sender: tokio::sync::mpsc::Sender<super::WebServicesIntercomOp>) {
let mut write_lock = self.service_listeners.web_services.write().await;
if let Some(old_sender) = write_lock.insert(public_id.clone(), sender) {
if !old_sender.is_closed() {
log::warn!("Replaced web services intercom channel for user {} (why duplicate!?)", public_id);
@@ -21,26 +29,43 @@ impl Users {
}
}
pub async fn remove_service(&self, public_id: String) {
let mut write_lock = self.service_listeners.write().await;
pub(super) async fn register_lobby_state_service(&self, sender: tokio::sync::mpsc::Sender<super::LobbyStateIntercomOp>) {
let mut write_lock = self.service_listeners.lobby_state.write().await;
if let Some(old_sender) = write_lock.replace(sender) {
if !old_sender.is_closed() {
log::warn!("Replaced lobby state intercom channel (why duplicate!?)");
old_sender.send(super::IntercomOp::Info(super::IntercomInfo::Close)).await.unwrap_or_default()
}
}
}
pub async fn remove_web_service(&self, public_id: String) {
let mut write_lock = self.service_listeners.web_services.write().await;
if write_lock.remove(&public_id).is_none() {
log::warn!("Tried to remove web services intercom channel for user {} without listener", public_id);
}
}
pub async fn broadcast_service_message(&self, msg: oj_rc_core::persist::user::intercom::IntercomWebServiceMessage) {
let read_lock = self.service_listeners.read().await;
pub async fn remove_lobby_state_service(&self) {
let mut write_lock = self.service_listeners.lobby_state.write().await;
if write_lock.take().is_none() {
log::warn!("Tried to remove lobby state intercom channel without listener");
}
}
pub async fn broadcast_web_service_message(&self, msg: oj_rc_core::persist::user::intercom::IntercomWebServiceMessage) {
let read_lock = self.service_listeners.web_services.read().await;
if msg.everyone {
if !msg.public_ids.is_empty() { return; } // invalid
for (public_id, tx) in read_lock.iter() {
if let Err(e) = tx.send(super::IntercomOp::Message(msg.data.clone())).await {
if let Err(e) = tx.send(super::WebServicesIntercomOp::Message(msg.data.clone())).await {
log::error!("Failed to send web service intercom message to {}: {}", public_id, e);
}
}
} else {
for public_id in msg.public_ids {
if let Some(tx) = read_lock.get(&public_id) {
if let Err(e) = tx.send(super::IntercomOp::Message(msg.data.clone())).await {
if let Err(e) = tx.send(super::WebServicesIntercomOp::Message(msg.data.clone())).await {
log::error!("Failed to send web service intercom message to {}: {}", public_id, e);
}
} else {
@@ -50,6 +75,15 @@ impl Users {
}
}
pub async fn broadcast_lobby_message(&self, msg: oj_rc_core::persist::user::intercom::IntercomLobbyStateMessage) {
let read_lock = self.service_listeners.lobby_state.read().await;
if let Some(listener) = &*read_lock {
if let Err(e) = listener.send(super::LobbyStateIntercomOp::Message(msg)).await {
log::error!("Failed to send lobby state intercom message: {}", e);
}
}
}
pub async fn statuses(&self) -> std::collections::HashMap<String, oj_serdes::ServerStatus> {
self.service_status.read().await.clone()
}

View File

@@ -13,8 +13,9 @@ pub struct AccountProvider {
domain: std::sync::Arc<String>,
cdn: std::sync::Arc<String>,
auth: std::sync::Arc<String>,
intercom: std::sync::Arc<String>,
secret: std::sync::Arc<Vec<u8>>,
pub(super) intercom: std::sync::Arc<String>,
pub(super) intercom_http_client: std::sync::Arc<reqwest::Client>,
pub(super) secret: std::sync::Arc<Vec<u8>>,
db: std::sync::Arc<oj_rc_database::Database>,
}
@@ -38,6 +39,7 @@ impl AccountProvider {
cdn: std::sync::Arc::new(server_settings.cdn_url),
auth: std::sync::Arc::new(server_settings.auth_url),
intercom: std::sync::Arc::new(server_settings.intercom_url),
intercom_http_client: std::sync::Arc::new(reqwest::Client::new()),
secret: std::sync::Arc::new(secret),
db: std::sync::Arc::new(db),
})

View File

@@ -116,6 +116,13 @@ impl super::IntercomUser for super::account_json::UserData {
}
}
async fn update_custom_game(&self, msg: IntercomLobbyCustomGameDataMessage) {
let data = IntercomLobbyStateMessage::CustomGame(msg);
if let Err(e) = self.post_to_intercom(&data, ".oj_lobby", "state").await {
log::error!("Failed to send intercom custom game state message: {}", e);
}
}
async fn update_status(&self, server_name: &str, msg: oj_serdes::ServerStatus) {
if let Err(e) = self.post_to_intercom(&msg, ".status", server_name).await {
log::error!("Failed to send intercom status message: {}", e);
@@ -123,6 +130,91 @@ impl super::IntercomUser for super::account_json::UserData {
}
}
#[derive(Serialize, Deserialize, Clone, Debug)]
#[serde(tag = "state")]
pub enum IntercomLobbyStateMessage {
CustomGame(IntercomLobbyCustomGameDataMessage),
}
#[derive(Serialize, Deserialize, Clone, Debug)]
pub struct IntercomLobbyCustomGameDataMessage {
pub session_id: String,
pub config: IntercomLobbyCustomGameConfig,
pub users: Vec<IntercomLobbyCustomGameUserData>,
}
#[derive(Serialize, Deserialize, Clone, Debug)]
pub enum CustomGameMode {
BattleArena,
TeamDeathmatch,
Pit,
SuddenDeath,
}
#[derive(Serialize, Deserialize, Clone, Debug, Copy)]
pub enum CustomGameVisibility {
Good,
Poor,
Bad,
}
#[derive(Serialize, Deserialize, Clone, Debug)]
pub struct IntercomLobbyCustomGameConfig {
pub game_mode: CustomGameMode,
pub map: String,
pub map_visibility: CustomGameVisibility,
pub health_regen: bool,
pub capture_segment_memory: bool,
pub base_shields_go_down: bool,
pub damage_mult: i32,
pub health_mult: i32,
pub power_mult: i32,
pub game_time: i32, // minutes
pub capture_speed: i32, // seconds
pub points_kill_streak: bool,
pub points_total_required: i32,
pub number_of_kills_to_win: i32,
pub respawn_time: i32,
pub core_appear_frequency: i32,
pub core_health_multiplier: i32,
pub core_destroy_time: i32,
pub protonium_harvest: i32,
pub ceiling_multiplier: i32,
pub min_cpu: i32,
pub max_cpu: i32,
}
impl IntercomLobbyCustomGameConfig {
pub fn as_core(&self) -> super::GameOverrides {
super::GameOverrides {
capture_segment_memory: self.capture_segment_memory,
base_shields_go_down: self.base_shields_go_down,
damage_mult: self.damage_mult,
health_mult: self.health_mult,
power_mult: self.power_mult,
game_time: self.game_time,
capture_speed: self.capture_speed,
points_kill_streak: self.points_kill_streak,
points_total_required: self.points_total_required,
number_of_kills_to_win: self.number_of_kills_to_win,
respawn_time: self.respawn_time,
core_appear_frequency: self.core_appear_frequency,
core_health_multiplier: self.core_health_multiplier,
core_destroy_time: self.core_destroy_time,
protonium_harvest: self.protonium_harvest,
ceiling_multiplier: self.ceiling_multiplier,
min_cpu: self.min_cpu,
max_cpu: self.max_cpu,
}
}
}
#[derive(Serialize, Deserialize, Clone, Debug)]
pub struct IntercomLobbyCustomGameUserData {
pub public_id: String,
pub team: u8,
}
#[derive(Serialize, Deserialize, Clone, Debug)]
pub struct IntercomWebServiceMessage {
pub public_ids: Vec<String>,

View File

@@ -1,3 +1,5 @@
use serde::{Serialize, Deserialize};
use super::account_json::UserData;
pub enum TeamChooser {
@@ -87,6 +89,7 @@ impl super::LobbyUser for UserData {
auto_heal: oj_rc_database::sea_orm::ActiveValue::Set(game.auto_heal),
variant: oj_rc_database::sea_orm::ActiveValue::Set(variant),
is_complete: oj_rc_database::sea_orm::ActiveValue::Set(false),
overrides: oj_rc_database::sea_orm::ActiveValue::Set("".to_owned()),
};
let game_dbo = self.db.insert_game(game_dbo).await.map_err(|e| {
log::error!("Failed to create game {} through user_id {}: {}", game.guid, self.account.id, e);
@@ -165,4 +168,143 @@ impl super::LobbyUser for UserData {
players: forced_fake_players.into_iter().chain(filler_players).collect(),
})
}
async fn start_custom_game(&self, game: super::GameDescriptor, players: Vec<super::PlayerLobbyDescriptor>) -> Result<(), polariton_server::operations::SimpleOpError> {
let now = chrono::Utc::now().timestamp();
let guid = crate::persist::user::str_to_i64(&game.guid)
.ok_or_else(|| polariton_server::operations::SimpleOpError::with_message(
crate::data::error_codes::LobbyReasonCode::UnexpectedError as i16, "Invalid GUID".to_owned()
)
)?;
let variant = if game.is_ranked {
oj_rc_database::schema::multiplayer_game::GameType::Ranked
} else if game.is_custom {
oj_rc_database::schema::multiplayer_game::GameType::Custom
} else {
oj_rc_database::schema::multiplayer_game::GameType::Standard
};
let conf_str = if let Some(config) = game.overrides {
serde_json::to_string_pretty(&CustomGameOverrides::from_user(&config)).unwrap()
} else {
"".to_owned()
};
let game_dbo = oj_rc_database::schema::multiplayer_game::ActiveModel {
id: oj_rc_database::sea_orm::ActiveValue::NotSet,
creation_time: oj_rc_database::sea_orm::ActiveValue::Set(now),
guid: oj_rc_database::sea_orm::ActiveValue::Set(guid),
map: oj_rc_database::sea_orm::ActiveValue::Set(game.map),
mode: oj_rc_database::sea_orm::ActiveValue::Set(game.mode.to_db()),
visibility: oj_rc_database::sea_orm::ActiveValue::Set(game.visibility.to_db()),
auto_heal: oj_rc_database::sea_orm::ActiveValue::Set(game.auto_heal),
variant: oj_rc_database::sea_orm::ActiveValue::Set(variant),
is_complete: oj_rc_database::sea_orm::ActiveValue::Set(false),
overrides: oj_rc_database::sea_orm::ActiveValue::Set(conf_str),
};
let game_dbo = self.db.insert_game(game_dbo).await.map_err(|e| {
log::error!("Failed to create custom game {} through user_id {}: {}", game.guid, self.account.id, e);
polariton_server::operations::SimpleOpError::with_message(
crate::data::error_codes::LobbyReasonCode::UnexpectedError as i16,
format!("Failed to create custom game {}: {}", game.guid, e),
)
})?;
let players: Vec<oj_rc_database::schema::multiplayer_game_player::ActiveModel> = players.into_iter()
.enumerate()
.map(|(i, player)| {
oj_rc_database::schema::multiplayer_game_player::ActiveModel {
id: oj_rc_database::sea_orm::ActiveValue::NotSet,
user_id: oj_rc_database::sea_orm::ActiveValue::Set(Some(player.user_id)),
game_id: oj_rc_database::sea_orm::ActiveValue::Set(game_dbo.id),
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),
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),
variant: oj_rc_database::sea_orm::ActiveValue::Set(oj_rc_database::schema::multiplayer_game_player::ClientType::Client),
}
})
.collect();
self.db.insert_players(players).await.map_err(|e| {
log::error!("Failed to create game players for {} through user_id {}: {}", game.guid, self.account.id, e);
polariton_server::operations::SimpleOpError::with_message(
crate::data::error_codes::LobbyReasonCode::UnexpectedError as i16,
format!("Failed to create game players for {}: {}", game.guid, e),
)
})?;
Ok(())
}
}
#[derive(Serialize, Deserialize, Clone, Debug)]
pub(super) struct CustomGameOverrides {
pub capture_segment_memory: bool,
pub base_shields_go_down: bool,
pub damage_mult: i32,
pub health_mult: i32,
pub power_mult: i32,
pub game_time: i32, // minutes
pub capture_speed: i32, // seconds
pub points_kill_streak: bool,
pub points_total_required: i32,
pub number_of_kills_to_win: i32,
pub respawn_time: i32,
pub core_appear_frequency: i32,
pub core_health_multiplier: i32,
pub core_destroy_time: i32,
pub protonium_harvest: i32,
pub ceiling_multiplier: i32,
pub min_cpu: i32,
pub max_cpu: i32,
}
impl CustomGameOverrides {
pub(super) fn from_user(intercom: &super::GameOverrides) -> Self {
Self {
capture_segment_memory: intercom.capture_segment_memory,
base_shields_go_down: intercom.base_shields_go_down,
damage_mult: intercom.damage_mult,
health_mult: intercom.health_mult,
power_mult: intercom.power_mult,
game_time: intercom.game_time,
capture_speed: intercom.capture_speed,
points_kill_streak: intercom.points_kill_streak,
points_total_required: intercom.points_total_required,
number_of_kills_to_win: intercom.number_of_kills_to_win,
respawn_time: intercom.respawn_time,
core_appear_frequency: intercom.core_appear_frequency,
core_health_multiplier: intercom.core_health_multiplier,
core_destroy_time: intercom.core_destroy_time,
protonium_harvest: intercom.protonium_harvest,
ceiling_multiplier: intercom.ceiling_multiplier,
min_cpu: intercom.min_cpu,
max_cpu: intercom.max_cpu,
}
}
pub(super) fn to_user(&self) -> super::GameOverrides {
super::GameOverrides {
capture_segment_memory: self.capture_segment_memory,
base_shields_go_down: self.base_shields_go_down,
damage_mult: self.damage_mult,
health_mult: self.health_mult,
power_mult: self.power_mult,
game_time: self.game_time,
capture_speed: self.capture_speed,
points_kill_streak: self.points_kill_streak,
points_total_required: self.points_total_required,
number_of_kills_to_win: self.number_of_kills_to_win,
respawn_time: self.respawn_time,
core_appear_frequency: self.core_appear_frequency,
core_health_multiplier: self.core_health_multiplier,
core_destroy_time: self.core_destroy_time,
protonium_harvest: self.protonium_harvest,
ceiling_multiplier: self.ceiling_multiplier,
min_cpu: self.min_cpu,
max_cpu: self.max_cpu,
}
}
}

View File

@@ -11,7 +11,7 @@ mod inventory;
pub use inventory::{UnlockedParts, UnlockOverride};
mod traits;
pub use traits::{UserProvider, User, UserToken, UserSlots, UserSlotData, VehicleData, UserAuthInfo, UserLoginInfo, UserAuthenticator, NewSlotData, UserId, RegistrationInfo, VehicleUploadData, ChatUser, AvatarInfo, GetAvatarInfo, ControlData, ControlType, CustomisationData, GetCustomisationData, SetSanction, SanctionType, LobbyUser, GameDescriptor, PlayerLobbyDescriptor, MultiplayerUser, PlayerScore, MultiplayerError, MultiplayerErrorCode, PlayerDescriptor, GameEventSetter, CurrentGameEvent, AuthError, IntercomUser, FakePlayers, ResolvedVehicle, CommonUser, IntercomListener, UserRole, SocialUser, SocialUserC, CurrencyType, CurrencyOp, MatchRewards, SingleplayerUser, PurchaseResult, FactoryUser, FriendInviteReturn, FriendData, FriendInviteStatus, SocialInfo, ClanData, ClanMember, ClanMemberRank, ClanType, ClanSearchQuery, ClanInviteData};
pub use traits::{UserProvider, User, UserToken, UserSlots, UserSlotData, VehicleData, UserAuthInfo, UserLoginInfo, UserAuthenticator, NewSlotData, UserId, RegistrationInfo, VehicleUploadData, ChatUser, AvatarInfo, GetAvatarInfo, ControlData, ControlType, CustomisationData, GetCustomisationData, SetSanction, SanctionType, LobbyUser, GameDescriptor, PlayerLobbyDescriptor, MultiplayerUser, PlayerScore, MultiplayerError, MultiplayerErrorCode, PlayerDescriptor, GameEventSetter, CurrentGameEvent, AuthError, IntercomUser, FakePlayers, ResolvedVehicle, CommonUser, IntercomListener, UserRole, SocialUser, SocialUserC, CurrencyType, CurrencyOp, MatchRewards, SingleplayerUser, PurchaseResult, FactoryUser, FriendInviteReturn, FriendData, FriendInviteStatus, SocialInfo, ClanData, ClanMember, ClanMemberRank, ClanType, ClanSearchQuery, ClanInviteData, Userless, GameOverrides};
pub mod intercom;
pub use intercom::generate_token as generate_intercom_token;
@@ -24,6 +24,7 @@ mod chat;
mod social;
mod singleplayer;
mod factory;
mod userless;
pub const TOKEN_SECRET_FILENAME: &str = "token_secret.key";

View File

@@ -134,6 +134,15 @@ impl super::MultiplayerUser for UserData {
is_ranked: matches!(game.variant, oj_rc_database::schema::multiplayer_game::GameType::Ranked),
is_custom: matches!(game.variant, oj_rc_database::schema::multiplayer_game::GameType::Custom),
is_complete: game.is_complete,
overrides: if game.overrides.is_empty() { None } else {
match serde_json::from_str::<super::lobby::CustomGameOverrides>(&game.overrides) {
Ok(x) => Some(x.to_user()),
Err(e) => {
log::warn!("Failed to parse overrides JSON: {}\n{}", e, game.overrides);
None
}
}
}
}))
}
@@ -204,6 +213,15 @@ impl super::MultiplayerUser for UserData {
is_ranked: matches!(game.variant, oj_rc_database::schema::multiplayer_game::GameType::Ranked),
is_custom: matches!(game.variant, oj_rc_database::schema::multiplayer_game::GameType::Custom),
is_complete: game.is_complete,
overrides: if game.overrides.is_empty() { None } else {
match serde_json::from_str::<super::lobby::CustomGameOverrides>(&game.overrides) {
Ok(x) => Some(x.to_user()),
Err(e) => {
log::warn!("Failed to parse overrides JSON: {}\n{}", e, game.overrides);
None
}
}
}
}))
} else {
Err(super::MultiplayerError {

View File

@@ -23,6 +23,7 @@ impl super::SingleplayerUser for UserData {
auto_heal: oj_rc_database::sea_orm::ActiveValue::Set(false),
variant: oj_rc_database::sea_orm::ActiveValue::Set(oj_rc_database::schema::multiplayer_game::GameType::Standard),
is_complete: oj_rc_database::sea_orm::ActiveValue::Set(true),
overrides: oj_rc_database::sea_orm::ActiveValue::Set("".to_owned()),
}).await
.map_err(|e| {
log::error!("Failed to create singleplayer game {} for user {}: {}", guid, self.account.id, e);

View File

@@ -282,6 +282,8 @@ pub trait LobbyUser {
async fn team_chooser(&self, game: &GameDescriptor) -> super::TeamChooser;
#[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>;
#[allow(clippy::too_many_arguments)]
async fn start_custom_game(&self, game: GameDescriptor, players: Vec<PlayerLobbyDescriptor>) -> Result<(), polariton_server::operations::SimpleOpError>;
}
pub struct FakePlayers {
@@ -306,6 +308,28 @@ pub struct GameDescriptor {
pub is_ranked: bool,
pub is_custom: bool,
pub is_complete: bool,
pub overrides: Option<GameOverrides>,
}
pub struct GameOverrides {
pub capture_segment_memory: bool,
pub base_shields_go_down: bool,
pub damage_mult: i32,
pub health_mult: i32,
pub power_mult: i32,
pub game_time: i32, // minutes
pub capture_speed: i32, // seconds
pub points_kill_streak: bool,
pub points_total_required: i32,
pub number_of_kills_to_win: i32,
pub respawn_time: i32,
pub core_appear_frequency: i32,
pub core_health_multiplier: i32,
pub core_destroy_time: i32,
pub protonium_harvest: i32,
pub ceiling_multiplier: i32,
pub min_cpu: i32,
pub max_cpu: i32,
}
pub struct PlayerLobbyDescriptor {
@@ -394,6 +418,7 @@ pub trait IntercomUser: CommonUser {
async fn webservice_listener(&self) -> Result<IntercomListener<super::intercom::IntercomWebServiceUserMessage>, polariton_server::operations::SimpleOpError>;
async fn show_dev_message(&self, msg: super::intercom::IntercomDevMessage, to: Vec<String>);
async fn enter_maintenance(&self, msg: super::intercom::IntercomMaintenanceMessage, to: Vec<String>);
async fn update_custom_game(&self, msg: super::intercom::IntercomLobbyCustomGameDataMessage);
async fn update_status(&self, server_name: &str, msg: oj_serdes::ServerStatus);
}
@@ -643,3 +668,8 @@ pub trait FactoryUser {
async fn prepare_factory_upload(&self, vehicle: VehicleUploadData) -> Result<oj_rc_factory::VehicleUploadInfo, polariton_server::operations::SimpleOpError>;
async fn rate_vehicle(&self, slot: i32, combat: i32, cosmetic: i32) -> Result<Option<i32>, polariton_server::operations::SimpleOpError>;
}
#[async_trait::async_trait]
pub trait Userless: Send + Sync {
async fn lobby_state_listener(&self) -> Result<super::IntercomListener<super::intercom::IntercomLobbyStateMessage>, reqwest_websocket::Error>;
}

View File

@@ -0,0 +1,29 @@
//use serde::{Serialize, Deserialize};
impl super::AccountProvider {
async fn listen_on_websocket<D: serde::de::DeserializeOwned>(&self, server_name: &str) -> Result<super::IntercomListener<D>, reqwest_websocket::Error> {
use reqwest_websocket::RequestBuilderExt;
let token = super::generate_intercom_token(format!("state/{}", server_name).as_bytes(), &self.secret);
let auth_header_val = format!("Internal {}", token);
let url = format!("{}/intercom/userless/{}", self.intercom, server_name);
log::debug!("Listening on websocket {}", url);
let websocket = self.intercom_http_client.get(url)
.header("Authorization", auth_header_val)
.upgrade()
.send()
.await?
.into_websocket()
.await?;
Ok(super::IntercomListener {
websocket,
_d: Default::default(),
})
}
}
#[async_trait::async_trait]
impl super::Userless for super::AccountProvider {
async fn lobby_state_listener(&self) -> Result<super::IntercomListener<super::intercom::IntercomLobbyStateMessage>, reqwest_websocket::Error> {
self.listen_on_websocket(".oj_lobby").await
}
}

View File

@@ -0,0 +1,36 @@
use sea_orm_migration::prelude::*;
pub struct Migration;
impl MigrationName for Migration {
fn name(&self) -> &str {
"m20260317_000001_add_game_overrides"
}
}
#[async_trait::async_trait]
impl MigrationTrait for Migration {
// Define how to apply this migration: Add game overrides columns
async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> {
manager
.alter_table(
Table::alter()
.table(crate::schema::multiplayer_game::Entity)
.add_column(ColumnDef::new(crate::schema::multiplayer_game::Column::Overrides).string().not_null().default("".to_owned()))
.to_owned()
)
.await
}
// Define how to rollback this migration: Drop the added column
async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> {
manager
.alter_table(
Table::alter()
.table(crate::schema::multiplayer_game::Entity)
.drop_column(crate::schema::multiplayer_game::Column::Overrides)
.to_owned()
)
.await
}
}

View File

@@ -18,6 +18,7 @@ mod m20260126_000001_create_factory_vehicle_table;
mod m20260215_000001_create_friend_table;
mod m20260221_000001_create_clan_table;
mod m20260221_000002_create_clan_member_table;
mod m20260317_000001_add_game_overrides;
pub struct Migrator;
@@ -43,6 +44,7 @@ impl MigratorTrait for Migrator {
Box::new(m20260215_000001_create_friend_table::Migration),
Box::new(m20260221_000001_create_clan_table::Migration),
Box::new(m20260221_000002_create_clan_member_table::Migration),
Box::new(m20260317_000001_add_game_overrides::Migration),
]
}
}

View File

@@ -13,6 +13,7 @@ pub struct Model {
pub auto_heal: bool,
pub variant: GameType,
pub is_complete: bool,
pub overrides: String, // JSON-encoded oj_rc_ore::persist::user::lobby::CustomGameConfig
}
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]

View File

@@ -21,3 +21,4 @@ async-trait.workspace = true
git-version.workspace = true
chrono.workspace = true
oj_serdes.workspace = true
futures.workspace = true

View File

@@ -0,0 +1,74 @@
//use oj_rc_core::persist::user::IntercomListener;
use oj_rc_core::persist::user::intercom::IntercomLobbyStateMessage;
const RETRY_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(15);
pub struct IntercomHandler<U: oj_rc_core::persist::user::Userless> {
userless: std::sync::Arc<U>,
lobby: std::sync::Arc<crate::QueueHandler>,
}
impl <U: oj_rc_core::persist::user::Userless + 'static> IntercomHandler<U> {
pub fn new(
userless: std::sync::Arc<U>,
lobby: std::sync::Arc<crate::QueueHandler>,
) -> Self {
Self {
userless,
lobby,
}
}
async fn run_loop(
userless: std::sync::Arc<U>,
lobby: std::sync::Arc<crate::QueueHandler>,
) {
loop {
let listener = match userless.lobby_state_listener().await {
Ok(listener) => {
log::debug!("Connected lobby state intercom listener");
listener
},
Err(e) => {
log::error!("Failed to connect to lobby state intercom: {} (retrying in {}s)", e, RETRY_TIMEOUT.as_secs());
tokio::time::sleep(RETRY_TIMEOUT).await;
continue;
}
};
use futures::StreamExt;
let mut listener = listener.listen().await;
while let Some(msg) = listener.next().await {
match msg {
Ok(msg) => {
match msg {
IntercomLobbyStateMessage::CustomGame(state) => {
if state.users.is_empty() {
// disband
lobby.remove_custom_queue(&state.session_id).await;
} else {
let is_create = lobby.update_custom_queue(
&state.session_id,
state.users.iter().map(|user| (user.public_id.clone(), user.team)),
state.config,
).await;
if is_create {
log::debug!("Created custom game {} lobby data to {} members", state.session_id, state.users.len());
} else {
log::debug!("Updated custom game {} lobby data to {} members", state.session_id, state.users.len());
}
}
},
}
},
Err(e) => {
log::error!("Bad intercom message received: {}", e);
}
}
}
}
}
pub fn run(self) -> tokio::task::JoinHandle<()> {
tokio::spawn(Self::run_loop(self.userless, self.lobby))
}
}

View File

@@ -1,3 +1,6 @@
pub mod battle_found;
pub mod battle_enter;
pub mod enqueue_error;
mod handler;
pub use handler::IntercomHandler;

View File

@@ -63,8 +63,21 @@ struct QueueUser {
user: std::sync::Arc<Box<dyn oj_rc_core::persist::user::User<()> + Send + Sync>>,
}
struct CustomGameQueue {
config: oj_rc_core::persist::user::intercom::IntercomLobbyCustomGameConfig,
users: Vec<CustomGameQueueUser>,
}
struct CustomGameQueueUser {
public_id: String,
team: u8,
queue_user: Option<QueueUser>, // if None, the user is not enqueued
}
pub struct QueueHandler {
users_in_queue: std::sync::Arc<tokio::sync::Mutex<HashMap<QueueKey, Vec<QueueUser>>>>,
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,
is_enabled: bool,
hostname: String,
@@ -84,6 +97,8 @@ impl QueueHandler {
let mp_settings = oj_rc_core::ConfigProvider::<()>::multiplayer_settings(conf);
Self {
users_in_queue: std::sync::Arc::new(tokio::sync::Mutex::new(HashMap::new())),
users_in_custom_games_queue: std::sync::Arc::new(tokio::sync::Mutex::new(HashMap::new())),
custom_game_for_user: std::sync::Arc::new(tokio::sync::RwLock::new(HashMap::new())),
users_per_game: oj_rc_core::ConfigProvider::<()>::players_per_game(conf),
is_enabled: mp_settings.is_enabled,
hostname: domain.to_owned(),
@@ -220,6 +235,7 @@ impl QueueHandler {
is_ranked: false,
is_custom: false,
is_complete: false,
overrides: None,
};
let team_picker = user.team_chooser(&game_desc).await;
/*let team_picker = match key.mode {
@@ -272,7 +288,88 @@ impl QueueHandler {
}
}
}
async fn enter_custom_match(
&self,
_session_id: &str,
session: &mut CustomGameQueue,
user: &(dyn oj_rc_core::persist::user::LobbyUser + Send + Sync),
) {
let mode = match session.config.game_mode {
oj_rc_core::persist::user::intercom::CustomGameMode::BattleArena => oj_rc_core::data::game_mode::GameMode::BattleArena,
oj_rc_core::persist::user::intercom::CustomGameMode::TeamDeathmatch => oj_rc_core::data::game_mode::GameMode::TeamDeathmatch,
oj_rc_core::persist::user::intercom::CustomGameMode::Pit => oj_rc_core::data::game_mode::GameMode::Pit,
oj_rc_core::persist::user::intercom::CustomGameMode::SuddenDeath => oj_rc_core::data::game_mode::GameMode::SuddenDeath,
};
let visibility = match session.config.map_visibility {
oj_rc_core::persist::user::intercom::CustomGameVisibility::Good => oj_rc_core::data::game_mode::MapVisibility::Good,
oj_rc_core::persist::user::intercom::CustomGameVisibility::Poor => oj_rc_core::data::game_mode::MapVisibility::Poor,
oj_rc_core::persist::user::intercom::CustomGameVisibility::Bad => oj_rc_core::data::game_mode::MapVisibility::Bad,
};
let key = QueueKey {
map: session.config.map.clone(),
mode,
visibility,
auto_heal: session.config.health_regen,
};
let guid_str = key.unique_guid();
let game_desc = oj_rc_core::persist::user::GameDescriptor {
guid: guid_str.clone(),
map: session.config.map.clone(),
mode,
visibility,
auto_heal: session.config.health_regen,
is_ranked: false,
is_custom: true,
is_complete: false,
overrides: Some(session.config.as_core()),
};
let player_descs = session.users.iter()
.map(|user| {
let q_user = user.queue_user.as_ref().unwrap();
oj_rc_core::persist::user::PlayerLobbyDescriptor {
user_id: q_user.user_id,
team: user.team as i32,
group: None,
public_id: q_user.player.name.clone(),
display_name: q_user.player.display_name.clone(),
}
})
.collect();
match user.start_custom_game(game_desc, player_descs).await {
Ok(_) => {
let player_datas = session.users.iter()
.map(|x| x.queue_user.as_ref().unwrap().player.clone())
.collect();
let enter_battle_ev = crate::events::battle_enter::BattleEnter {
host: self.hostname.clone(),
port: self.hostport,
map: session.config.map.clone(),
mode,
guid: guid_str.clone(),
is_ranked: false,
is_custom: true,
visibility: Some(key.visibility),
auto_heal: key.auto_heal,
player_datas,
network_config: self.network_conf.clone(),
};
let arc_event = std::sync::Arc::new(enter_battle_ev);
for user in session.users.iter_mut() {
let player = user.queue_user.take().unwrap();
tokio::spawn(Self::send_events_to_player(arc_event.clone(), player.emitter.clone()));
}
log::info!("{} players are entering custom match {}; {}", session.users.len(), guid_str, key.short());
},
Err(e) => {
if let Some(msg) = e.error_msg() {
log::error!("Cannot send enter battle events to players since LobbyUser.start_game(...) failed: {} ({})", msg, e.error_code());
} else {
log::error!("Cannot send enter battle events to players since LobbyUser.start_game(...) failed ({})", e.error_code());
}
}
}
}
async fn send_events_to_player(enter_event: std::sync::Arc<crate::events::battle_enter::BattleEnter>, sender: polariton_server::events::EventEmitter) {
@@ -283,6 +380,118 @@ impl QueueHandler {
}
}
pub async fn join_custom_queue(&self, user: std::sync::Arc<Box<dyn oj_rc_core::persist::user::User<()> + Send + Sync>>, event_emitter: polariton_server::events::EventEmitter) {
if !self.is_enabled {
event_emitter.emit(crate::events::enqueue_error::QueueJoinError {
code: oj_rc_core::data::error_codes::LobbyReasonCode::NoSuitableLobbyFound as i16,
text: "Multiplayer is not enabled".to_owned(),
});
return;
}
let public_id = user.public_id();
if let Some(session_id) = self.custom_game_for_user.read().await.get(public_id) {
let mut lock = self.users_in_custom_games_queue.lock().await;
let session = lock.get_mut(session_id).unwrap();
let lobby_user = user.as_ref().as_ref();
match lobby_user.player_data(&self.cpu_counter).await {
Ok(mut player_data) => {
let target_queuer = session.users.iter_mut()
.find(|user| user.public_id == public_id)
.unwrap();
player_data.team = target_queuer.team as i32;
let new_player = QueueUser {
emitter: event_emitter,
player: player_data,
user_id: oj_rc_core::persist::user::LobbyUser::user_id(lobby_user),
enqueued_at: chrono::Utc::now(),
user: user.clone(),
};
target_queuer.queue_user = Some(new_player);
},
Err(e) => {
event_emitter.emit(crate::events::enqueue_error::QueueJoinError {
code: e.error_code(),
text: e.error_msg().map(|x| x.to_owned()).unwrap_or_else(|| "Unknown queue join error".to_owned()),
});
return;
}
}
let is_all_enqueued = session.users.iter().all(|user| user.queue_user.is_some());
if is_all_enqueued {
self.enter_custom_match(session_id, session, lobby_user).await;
}
} else {
log::debug!("Rejecting join queue for unknown custom game for user {}", public_id);
event_emitter.emit(crate::events::enqueue_error::QueueJoinError {
code: oj_rc_core::data::error_codes::LobbyReasonCode::NoSuitableLobbyFound as i16,
text: "User is not in a custom game".to_owned(),
});
}
}
pub async fn remove_custom_queue(&self, session_id: &str) -> bool {
log::debug!("Disbanding custom game {}", session_id);
self.custom_game_for_user.write().await.retain(|_key, val| val != session_id);
let mut lock = self.users_in_custom_games_queue.lock().await;
lock.remove(session_id).is_some()
}
pub async fn update_custom_queue(&self, session_id: &str, members: impl std::iter::Iterator<Item = (String, u8)>, config: oj_rc_core::persist::user::intercom::IntercomLobbyCustomGameConfig) -> bool {
let mut lock = self.users_in_custom_games_queue.lock().await;
let members: std::collections::HashMap<_, _> = members.collect();
let mut user_map_lock = self.custom_game_for_user.write().await;
if let Some(session) = lock.get_mut(session_id) {
log::debug!("Updating existing custom game {}", session_id);
let member_ids: std::collections::HashSet<_> = members.keys().collect();
// remove users who are no longer part of the custom game
user_map_lock.retain(|key, val| (members.contains_key(key) && val == session_id) || (!members.contains_key(key) && val != session_id));
session.users.retain(|user| member_ids.contains(&user.public_id));
// update team assignments
session.users.iter_mut()
.for_each(|user| {
let new_team = *members.get(&user.public_id).unwrap();
user.team = new_team;
if let Some(q_user) = &mut user.queue_user {
q_user.player.team = new_team as i32;
}
});
// collect users which are still members
let existing_ids: std::collections::HashSet<_> = session.users.iter()
.map(|mem| mem.public_id.clone())
.collect();
// add new members
for (id, team) in members.iter() {
if !existing_ids.contains(id) {
session.users.push(CustomGameQueueUser {
public_id: id.to_owned(),
team: *team,
queue_user: None,
});
user_map_lock.insert(id.to_owned(), session_id.to_owned());
}
}
// update config overrides
session.config = config;
false
} else {
log::debug!("Creating new custom game {}", session_id);
lock.insert(session_id.to_owned(), CustomGameQueue {
config,
users: members.iter().map(|(mem_id, team)| {
user_map_lock.insert(mem_id.to_owned(), session_id.to_owned());
CustomGameQueueUser {
public_id: mem_id.to_owned(),
team: *team,
queue_user: None,
}
})
.collect()
});
true
}
}
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) {
if !self.is_enabled {
event_emitter.emit(crate::events::enqueue_error::QueueJoinError {
@@ -378,12 +587,23 @@ impl QueueHandler {
}
pub async fn leave_queue(&self, user: std::sync::Arc<Box<dyn oj_rc_core::persist::user::User<()> + Send + Sync>>) {
let public_id = user.public_id();
let user_id = oj_rc_core::persist::user::LobbyUser::user_id(user.as_ref().as_ref());
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);
log::info!("User {} was removed from queue {}", user_id, queue_key.short());
break;
if let Some(session_id) = self.custom_game_for_user.read().await.get(public_id) {
// player is in custom game session
let mut lock = self.users_in_custom_games_queue.lock().await;
let session = lock.get_mut(session_id).unwrap();
let target = session.users.iter_mut().find(|user| user.public_id == public_id).unwrap();
target.queue_user = None;
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);
log::info!("User {} was removed from queue {}", user_id, queue_key.short());
break;
}
}
}
}

View File

@@ -46,6 +46,11 @@ async fn main() -> std::io::Result<()> {
let server = std::sync::Arc::new(polariton_server::Server::new(operations::handler(&init_ctx), polariton_server::events::EventsHandler::new()));
crate::events::IntercomHandler::new(
init_ctx.users.clone(),
init_ctx.queue.clone(),
).run();
let ip_addr: std::net::IpAddr = args.ip.parse().expect("Invalid IP address");
let listener = net::TcpListener::bind(std::net::SocketAddr::new(ip_addr, args.port)).await?;

View File

@@ -30,24 +30,49 @@ impl <C: Send + 'static> SimpleOperation<C> for QueueJoinProvider {
if let Some(Typed::Bool(is_leader)) = params.remove(&IS_GROUP_LEADER_PARAM_KEY) {
if let Some(Typed::Int(lobby_ty)) = params.remove(&LOBBY_TY_PARAM_KEY) {
let lobby_ty = oj_rc_core::data::lobby::LobbyType::from_int(lobby_ty)?;
if let Some(Typed::Str(event_to_join)) = params.remove(&EVENT_TO_JOIN_PARAM_KEY) {
log::debug!("Got lobby join queue request of platoon {} ({} players, is_leader:{}) slot {} lobby {:?} event {}", group_id.string, group_size, is_leader, slot_id, lobby_ty, event_to_join.string);
params.insert(ESTIMATED_QUEUE_TIME_PARAM_KEY, Typed::Int(42));
params.insert(PERSONAL_RANKING_PARAM_KEY, Typed::Double(42.0));
let events = user.event_sender();
let user_info = user.user()?;
if let Some(current_lobby) = user_info.current_game_event_setter().get_multiplayer().await {
self.queue_handler.join_queue(
current_lobby.map,
current_lobby.mode,
current_lobby.visibility,
current_lobby.auto_heal,
log::debug!("Got lobby join queue request of platoon {} ({} players, is_leader:{}) slot {} lobby {:?}", group_id.string, group_size, is_leader, slot_id, lobby_ty);
match lobby_ty {
oj_rc_core::data::lobby::LobbyType::None => {
log::warn!("Cannot join queue for None mode (???)");
},
oj_rc_core::data::lobby::LobbyType::CustomGame => {
log::debug!("Joining platoon {} to custom game lobby", group_id.string);
params.insert(ESTIMATED_QUEUE_TIME_PARAM_KEY, Typed::Int(42));
params.insert(PERSONAL_RANKING_PARAM_KEY, Typed::Double(42.0));
let events = user.event_sender();
let user_info = user.user()?;
self.queue_handler.join_custom_queue(
user_info.clone(),
events.to_owned(),
).await;
}
},
oj_rc_core::data::lobby::LobbyType::QuickPlay => {
// regular multiplayer
if let Some(Typed::Str(event_to_join)) = params.remove(&EVENT_TO_JOIN_PARAM_KEY) {
log::debug!("Joining platoon {} to multiplayer lobby with event {}", group_id.string, event_to_join.string);
params.insert(ESTIMATED_QUEUE_TIME_PARAM_KEY, Typed::Int(42));
params.insert(PERSONAL_RANKING_PARAM_KEY, Typed::Double(42.0));
let events = user.event_sender();
let user_info = user.user()?;
if let Some(current_lobby) = user_info.current_game_event_setter().get_multiplayer().await {
self.queue_handler.join_queue(
current_lobby.map,
current_lobby.mode,
current_lobby.visibility,
current_lobby.auto_heal,
user_info.clone(),
events.to_owned(),
).await;
}
} else {
log::warn!("Missing multiplayer event, not joining platoon {} to multiplayer lobby queue", group_id.string);
}
},
oj_rc_core::data::lobby::LobbyType::Solo => {
log::warn!("Cannot join queue for solo mode (it's singleplayer!)");
},
}
}
}
}

View File

@@ -24,6 +24,11 @@ async fn main() -> std::io::Result<()> {
HttpServer::new(move || {
App::new()
.wrap_fn(|req, srv| {
use actix_web::dev::Service;
log::trace!("Request {} {}", req.method(), req.path());
srv.call(req)
})
.app_data(cli_args2.clone())
.service(index)
.service(robocraft::robopay_store)

View File

@@ -15,6 +15,7 @@ pub struct UserInfo {
pub struct SessionInfo {
pub session_id: String,
pub config: std::collections::HashMap<String, String>,
pub config_core: oj_rc_core::persist::user::intercom::IntercomLobbyCustomGameConfig,
pub users: Vec<UserInfo>,
}
@@ -36,7 +37,7 @@ struct GameHandle {
struct UserHandle {
public_id: String,
is_invited: std::sync::atomic::AtomicBool,
team: u8,
team: std::sync::atomic::AtomicU8,
status: std::sync::atomic::AtomicU8,
}
@@ -48,7 +49,7 @@ impl CustomGameMesh {
}
}
pub async fn create_game(&self, public_id: &str) -> Result<String, SessionCreateResponseCode> {
pub async fn create_game(&self, public_id: &str) -> Result<SessionInfo, SessionCreateResponseCode> {
let mut user_lock = self.user_to_game.write().await;
if user_lock.contains_key(public_id) {
log::debug!("Rejected custom game session create; user {} is already in a session", public_id);
@@ -58,18 +59,19 @@ impl CustomGameMesh {
let owner_handle = UserHandle {
public_id: public_id.to_owned(),
is_invited: std::sync::atomic::AtomicBool::new(false),
team: 0,
team: std::sync::atomic::AtomicU8::new(0),
status: std::sync::atomic::AtomicU8::new(PlayerSessionStatus::Ready.to_u8()),
};
let game_handle = GameHandle {
users: vec![owner_handle],
config: GameConfig::default(),
};
let session = Self::session_from_game(&game_id, &game_handle);
let mut games_lock = self.games.write().await;
user_lock.insert(public_id.to_owned(), game_id.clone());
games_lock.insert(game_id.clone(), game_handle);
log::debug!("Custom game session {} created", game_id);
Ok(game_id)
Ok(session)
}
pub async fn leave_game(&self, public_id: &str) -> (SessionLeaveResponseCode, Option<SessionInfo>) {
@@ -85,6 +87,7 @@ impl CustomGameMesh {
}
log::debug!("Removed {} invited stragglers from custom game {}", game.users.len() - 1, game_id);
}
game.users.clear();
let session = Self::session_from_game(&game_id, game);
games_lock.remove(&game_id);
session
@@ -123,18 +126,7 @@ impl CustomGameMesh {
pub async fn get_user_game(&self, public_id: &str) -> Option<SessionInfo> {
if let Some(game_id) = self.user_to_game.read().await.get(public_id) {
if let Some(game) = self.games.read().await.get(game_id) {
return Some(SessionInfo {
session_id: game_id.to_owned(),
config: game.config.as_map(),
users: game.users.iter()
.map(|user| UserInfo {
public_id: user.public_id.clone(),
is_invited: user.is_invited.load(std::sync::atomic::Ordering::Relaxed),
team: user.team,
state: PlayerSessionStatus::from_u8(user.status.load(std::sync::atomic::Ordering::Relaxed)).unwrap(),
})
.collect()
});
return Some(Self::session_from_game(&game_id, game));
}
}
None
@@ -150,7 +142,7 @@ impl CustomGameMesh {
let invitee_handle = UserHandle {
public_id: invitee.to_owned(),
is_invited: std::sync::atomic::AtomicBool::new(true),
team: if is_team_a { 0 } else { 1 },
team: std::sync::atomic::AtomicU8::new(if is_team_a { 0 } else { 1 }),
status: std::sync::atomic::AtomicU8::new(PlayerSessionStatus::Unknown.to_u8())
};
game.users.push(invitee_handle);
@@ -215,12 +207,45 @@ impl CustomGameMesh {
None
}
pub async fn update_user_team(&self, updater: &str, updatee: &str, is_team_b: bool) -> (ChangeTeamResponseCode, Option<SessionInfo>) {
if let Some(game_id) = self.user_to_game.read().await.get(updatee) {
if let Some(game) = self.games.read().await.get(game_id) {
let leader = game.users.first().unwrap();
if leader.public_id != updater {
return (ChangeTeamResponseCode::UserIsNotSessionLeader, None);
}
let target = game.users.iter()
.find(|mem| mem.public_id == updatee)
.unwrap();
let team = if is_team_b { 1 } else { 0 };
target.team.store(team, std::sync::atomic::Ordering::Relaxed);
let session = Self::session_from_game(&game_id, game);
return (ChangeTeamResponseCode::Success, Some(session));
}
}
(ChangeTeamResponseCode::UnknownFail0, None)
}
pub async fn set_config_field(&self, public_id: &str, field: &str, value: &str) -> (AdjustCustomGameConfigResponseCode, Option<SessionInfo>) {
if let Some(game_id) = self.user_to_game.read().await.get(public_id) {
if let Some(game) = self.games.write().await.get_mut(game_id) {
if game.users[0].public_id != public_id {
log::debug!("Update custom game session {} config rejected (not leader)", game_id);
return (AdjustCustomGameConfigResponseCode::AdjustmentRejected, None);
// the first time a user joins/creates a custom game, the game client
// automatically tries to set these fields (regardless if the user is not the leader)
const STUPID_FIELDS: &[&str] = &[
"DamageMultiplier",
"HealthMultiplier",
"PowerMultiplier",
"GameTime",
];
if STUPID_FIELDS.contains(&field /* &&str lol */) {
log::debug!("Update custom game session {} config ignored (not leader; client bug with field {})", game_id, field);
return (AdjustCustomGameConfigResponseCode::Success, None);
} else {
log::debug!("Update custom game session {} config rejected (not leader) {} = {}", game_id, field, value);
return (AdjustCustomGameConfigResponseCode::AdjustmentRejected, None);
}
}
if let Ok(_) = game.config.set_field(field, value) {
log::debug!("Update custom game session {} config {} to {}", game_id, field, value);
@@ -238,11 +263,12 @@ impl CustomGameMesh {
SessionInfo {
session_id: game_id.to_owned(),
config: game.config.as_map(),
config_core: game.config.as_core(),
users: game.users.iter()
.map(|user| UserInfo {
public_id: user.public_id.clone(),
is_invited: user.is_invited.load(std::sync::atomic::Ordering::Relaxed),
team: user.team,
team: user.team.load(std::sync::atomic::Ordering::Relaxed),
state: PlayerSessionStatus::from_u8(user.status.load(std::sync::atomic::Ordering::Relaxed))
.unwrap_or(PlayerSessionStatus::Unknown),
})
@@ -610,4 +636,44 @@ impl GameConfig {
}
}
}
fn as_core(&self) -> oj_rc_core::persist::user::intercom::IntercomLobbyCustomGameConfig {
oj_rc_core::persist::user::intercom::IntercomLobbyCustomGameConfig {
game_mode: match self.game_mode {
oj_rc_core::data::game_mode::GameMode::BattleArena => oj_rc_core::persist::user::intercom::CustomGameMode::BattleArena,
oj_rc_core::data::game_mode::GameMode::TeamDeathmatch => oj_rc_core::persist::user::intercom::CustomGameMode::TeamDeathmatch,
oj_rc_core::data::game_mode::GameMode::Pit => oj_rc_core::persist::user::intercom::CustomGameMode::Pit,
oj_rc_core::data::game_mode::GameMode::SuddenDeath => oj_rc_core::persist::user::intercom::CustomGameMode::SuddenDeath,
invalid => {
log::warn!("Custom game set to invalid mode {:?} (using sudden death as fallback)", invalid);
oj_rc_core::persist::user::intercom::CustomGameMode::SuddenDeath
},
},
map: self.map.clone(),
map_visibility: match self.map_visibility {
oj_rc_core::data::game_mode::MapVisibility::Good => oj_rc_core::persist::user::intercom::CustomGameVisibility::Good,
oj_rc_core::data::game_mode::MapVisibility::Poor => oj_rc_core::persist::user::intercom::CustomGameVisibility::Poor,
oj_rc_core::data::game_mode::MapVisibility::Bad => oj_rc_core::persist::user::intercom::CustomGameVisibility::Bad,
},
health_regen: self.health_regen,
capture_segment_memory: self.capture_segment_memory,
base_shields_go_down: self.base_shields_go_down,
damage_mult: self.damage_mult,
health_mult: self.health_mult,
power_mult: self.power_mult,
game_time: self.game_time,
capture_speed: self.capture_speed,
points_kill_streak: self.points_kill_streak,
points_total_required: self.points_total_required,
number_of_kills_to_win: self.number_of_kills_to_win,
respawn_time: self.respawn_time,
core_appear_frequency: self.core_appear_frequency,
core_health_multiplier: self.core_health_multiplier,
core_destroy_time: self.core_destroy_time,
protonium_harvest: self.protonium_harvest,
ceiling_multiplier: self.ceiling_multiplier,
min_cpu: self.min_cpu,
max_cpu: self.max_cpu,
}
}
}

View File

@@ -87,6 +87,17 @@ pub enum CheckCanJoinQueueResponseCode {
UserNotInSession5 = 5,
}
#[repr(u8)]
#[derive(Copy, Clone)]
pub enum ChangeTeamResponseCode {
UnknownFail0 = 0,
UserIsNotSessionLeader = 1,
UnknownFail2 = 2,
UnknownFail3 = 3,
Success = 4,
UnknownFail5 = 5,
}
#[repr(u8)]
#[derive(Copy, Clone)]
pub enum PlayerSessionStatus {

View File

@@ -32,6 +32,17 @@ impl <C: Send + 'static> SimpleOperation<C> for CustomGameConfigChanger {
.filter(|mem| !mem.is_invited && mem.public_id != my_pub_id)
.map(|mem| &mem.public_id as &str);
self.mesh.broadcast_event_to(session_members_iter, event).await;
user_info.update_custom_game(oj_rc_core::persist::user::intercom::IntercomLobbyCustomGameDataMessage {
session_id: session.session_id.clone(),
config: session.config_core,
users: session.users.iter()
.filter(|user| !user.is_invited)
.map(|user| oj_rc_core::persist::user::intercom::IntercomLobbyCustomGameUserData {
public_id: user.public_id.clone(),
team: user.team,
})
.collect(),
}).await;
}
params.insert(RESPONSE_CODE_PARAM_KEY, Typed::Int(resp_code as _));
}

View File

@@ -22,9 +22,19 @@ impl <C: Send + 'static> SimpleOperation<C> for CustomGameCreator {
Err(e) => {
params.insert(RESPONSE_CODE_PARAM_KEY, Typed::Int(e as _));
},
Ok(game_id) => {
Ok(session) => {
user_info.update_custom_game(oj_rc_core::persist::user::intercom::IntercomLobbyCustomGameDataMessage {
session_id: session.session_id.clone(),
config: session.config_core,
users: vec![
oj_rc_core::persist::user::intercom::IntercomLobbyCustomGameUserData {
public_id: user_info.public_id().to_owned(),
team: 0,
}
],
}).await;
params.insert(RESPONSE_CODE_PARAM_KEY, Typed::Int(crate::data::custom_games::SessionCreateResponseCode::SessionCreated as _));
params.insert(IDK_PARAM_KEY, Typed::Str(game_id.into()));
params.insert(IDK_PARAM_KEY, Typed::Str(session.session_id.into()));
}
}
Ok(params)

View File

@@ -30,6 +30,18 @@ impl <C: Send + 'static> SimpleOperation<C> for CustomGameInviteResponder {
.filter(|mem| !mem.is_invited && mem.public_id != my_pub_id)
.map(|mem| &mem.public_id as &str);
self.mesh.broadcast_event_to(session_members_iter, event).await;
} else {
user_info.update_custom_game(oj_rc_core::persist::user::intercom::IntercomLobbyCustomGameDataMessage {
session_id: session.session_id.clone(),
config: session.config_core,
users: session.users.iter()
.filter(|user| !user.is_invited)
.map(|user| oj_rc_core::persist::user::intercom::IntercomLobbyCustomGameUserData {
public_id: user.public_id.clone(),
team: user.team,
})
.collect()
}).await;
}
let event = crate::events::CustomGameRefresh {
session: session.session_id,

View File

@@ -29,11 +29,22 @@ impl <C: Send + 'static> SimpleOperation<C> for CustomGameMemberKicker {
};
self.mesh.send_event_to(&user_to_kick.string, kick_event).await;
let update_event = crate::events::CustomGameRefresh {
session: session.session_id,
session: session.session_id.clone(),
};
let members_iter = session.users.iter()
.map(|mem| &mem.public_id as &str);
self.mesh.broadcast_event_to(members_iter, update_event).await;
user_info.update_custom_game(oj_rc_core::persist::user::intercom::IntercomLobbyCustomGameDataMessage {
session_id: session.session_id,
config: session.config_core,
users: session.users.iter()
.filter(|user| !user.is_invited)
.map(|user| oj_rc_core::persist::user::intercom::IntercomLobbyCustomGameUserData {
public_id: user.public_id.clone(),
team: user.team,
})
.collect()
}).await;
}
params.insert(RESPONSE_CODE_PARAM_KEY, Typed::Int(resp_code as _));
}

View File

@@ -20,11 +20,22 @@ impl <C: Send + 'static> SimpleOperation<C> for CustomGameLeaver {
let (resp_code, session_opt) = self.games.leave_game(user_info.public_id()).await;
if let Some(session) = session_opt {
let event = crate::events::CustomGameRefresh {
session: session.session_id,
session: session.session_id.clone(),
};
let session_members = session.users.iter()
.map(|mem| &mem.public_id as &str);
self.mesh.broadcast_event_to(session_members, event).await;
user_info.update_custom_game(oj_rc_core::persist::user::intercom::IntercomLobbyCustomGameDataMessage {
session_id: session.session_id,
config: session.config_core,
users: session.users.iter()
.filter(|user| !user.is_invited)
.map(|user| oj_rc_core::persist::user::intercom::IntercomLobbyCustomGameUserData {
public_id: user.public_id.clone(),
team: user.team,
})
.collect()
}).await;
}
params.insert(RESPONSE_CODE_PARAM_KEY, Typed::Int(resp_code as _));
Ok(params)

View File

@@ -0,0 +1,59 @@
use polariton_server::operations::{SimpleOpError, SimpleOperation, SimpleOpImpl};
use polariton::operation::{ParameterTable, Typed};
const CODE: u8 = 151;
const USERNAME_PARAM_KEY: u8 = 185; // str; in
//const CHANGER_PARAM_KEY: u8 = 186; // always null?; in
const IS_TEAM_B_PARAM_KEY: u8 = 187; // bool; in
const RESPONSE_CODE_PARAM_KEY: u8 = 168; // int enum; out
pub(super) struct CustomGameTeamChanger {
games: std::sync::Arc<crate::custom_game_tracker::CustomGameMesh>,
mesh: std::sync::Arc<crate::user_service::UserMesh>,
}
#[async_trait::async_trait]
impl <C: Send + 'static> SimpleOperation<C> for CustomGameTeamChanger {
type User = crate::UserTy;
const CODE: u8 = CODE;
async fn handle(&self, mut params: ParameterTable<C>, user: &Self::User) -> Result<ParameterTable<C>, SimpleOpError> {
if let Some(Typed::Str(username)) = params.remove(&USERNAME_PARAM_KEY) {
if let Some(Typed::Bool(is_team_b)) = params.remove(&IS_TEAM_B_PARAM_KEY) {
let user_info = user.user()?;
let my_pub_id = user_info.public_id();
let (resp_code, session_opt) = self.games.update_user_team(my_pub_id, &username.string, is_team_b).await;
if let Some(session) = session_opt {
let event = crate::events::CustomGameRefresh {
session: session.session_id.clone(),
};
let session_members_iter = session.users.iter()
.filter(|mem| !mem.is_invited)
.map(|mem| &mem.public_id as &str);
self.mesh.broadcast_event_to(session_members_iter, event).await;
user_info.update_custom_game(oj_rc_core::persist::user::intercom::IntercomLobbyCustomGameDataMessage {
session_id: session.session_id,
config: session.config_core,
users: session.users.iter()
.filter(|user| !user.is_invited)
.map(|user| oj_rc_core::persist::user::intercom::IntercomLobbyCustomGameUserData {
public_id: user.public_id.clone(),
team: user.team,
})
.collect()
}).await;
}
params.insert(RESPONSE_CODE_PARAM_KEY, Typed::Int(resp_code as _));
}
}
Ok(params)
}
}
pub(super) fn game_team_change_provider<C: Send + 'static>(init_ctx: &crate::InitConfig) -> SimpleOpImpl<C, crate::UserTy, CustomGameTeamChanger> {
SimpleOpImpl::new(CustomGameTeamChanger {
games: init_ctx.custom_games.clone(),
mesh: init_ctx.user_mesh.clone(),
})
}

View File

@@ -116,6 +116,7 @@ mod custom_game_invite_respond;
mod custom_game_kick;
mod custom_game_player_state;
mod custom_game_can_join_queue;
mod custom_game_team;
use polariton_server::operations::OperationsHandler;
@@ -256,4 +257,5 @@ pub fn handler(init_ctx: &crate::InitConfig) -> OperationsHandler<crate::UserTy>
.add(custom_game_kick::game_kick_provider(init_ctx))
.add(custom_game_player_state::game_player_status_update_provider(init_ctx))
.add(custom_game_can_join_queue::game_can_queue_provider(init_ctx))
.add(custom_game_team::game_team_change_provider(init_ctx))
}