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

Allow two or more people to load into the same multiplayer match #30

This commit is contained in:
NG (Graham)
2025-07-20 18:31:08 -04:00
parent cd45c22d0b
commit 37cbc8d85f
24 changed files with 902 additions and 149 deletions

View File

@@ -118,6 +118,32 @@ impl GameMode {
crate::persist::config::GameType::Campaign => Self::Campaign,
}
}
#[inline]
pub(crate) fn from_db(mode: oj_rc_database::schema::multiplayer_game::GameMode) -> Self {
match mode {
oj_rc_database::schema::multiplayer_game::GameMode::BattleArena => Self::BattleArena,
oj_rc_database::schema::multiplayer_game::GameMode::SuddenDeath => Self::SuddenDeath,
oj_rc_database::schema::multiplayer_game::GameMode::Pit => Self::Pit,
oj_rc_database::schema::multiplayer_game::GameMode::TestMode => Self::TestMode,
oj_rc_database::schema::multiplayer_game::GameMode::SinglePlayer => Self::SinglePlayer,
oj_rc_database::schema::multiplayer_game::GameMode::TeamDeathmatch => Self::TeamDeathmatch,
oj_rc_database::schema::multiplayer_game::GameMode::Campaign => Self::Campaign,
}
}
#[inline]
pub(crate) fn to_db(&self) -> oj_rc_database::schema::multiplayer_game::GameMode {
match self {
Self::BattleArena => oj_rc_database::schema::multiplayer_game::GameMode::BattleArena,
Self::SuddenDeath => oj_rc_database::schema::multiplayer_game::GameMode::SuddenDeath,
Self::Pit => oj_rc_database::schema::multiplayer_game::GameMode::Pit,
Self::TestMode => oj_rc_database::schema::multiplayer_game::GameMode::TestMode,
Self::SinglePlayer => oj_rc_database::schema::multiplayer_game::GameMode::SinglePlayer,
Self::TeamDeathmatch => oj_rc_database::schema::multiplayer_game::GameMode::TeamDeathmatch,
Self::Campaign => oj_rc_database::schema::multiplayer_game::GameMode::Campaign,
}
}
}
#[repr(u8)]
@@ -137,4 +163,22 @@ impl MapVisibility {
crate::persist::config::GameVisibility::Bad => Self::Bad,
}
}
#[inline]
pub(crate) fn from_db(mode: oj_rc_database::schema::multiplayer_game::MapVisibility) -> Self {
match mode {
oj_rc_database::schema::multiplayer_game::MapVisibility::Good => Self::Good,
oj_rc_database::schema::multiplayer_game::MapVisibility::Poor => Self::Poor,
oj_rc_database::schema::multiplayer_game::MapVisibility::Bad => Self::Bad,
}
}
#[inline]
pub(crate) fn to_db(&self) -> oj_rc_database::schema::multiplayer_game::MapVisibility {
match self {
Self::Good => oj_rc_database::schema::multiplayer_game::MapVisibility::Good,
Self::Poor => oj_rc_database::schema::multiplayer_game::MapVisibility::Poor,
Self::Bad => oj_rc_database::schema::multiplayer_game::MapVisibility::Bad,
}
}
}

View File

@@ -8,7 +8,7 @@ pub struct PlayerData {
pub tier: i32,
pub robot_name: String,
pub robot_map: Vec<u8>,
// -- unused i32 here --
pub group: Option<String>, // unused i32 too???
pub team: i32,
pub has_premium: bool,
pub robot_uuid: String,
@@ -70,7 +70,7 @@ impl PlayerData {
(Typed::Str("spawnEffect".into()), Typed::Str(self.spawn_effect.clone().into())),
(Typed::Str("deathEffect".into()), Typed::Str(self.death_effect.clone().into())),
//(Typed::Str("groupId".into()), Typed::Int(self.group)), // FIXME
(Typed::Str("groupId".into()), Typed::Int(self.team)), // not strongly enforced in EnterBattleEventListener
(Typed::Str("groupId".into()), Typed::Str(self.group.clone().unwrap_or_default().into())),
(Typed::Str("team".into()), Typed::Int(self.team)), // not strongly enforced in EnterBattleEventListener (but has to be parsable into an i32)
(Typed::Str("hasPremium".into()), Typed::Bool(self.has_premium)),
(Typed::Str("weaponOrder".into()), Typed::IntArr(self.weapon_order.clone().into())),

View File

@@ -477,7 +477,7 @@ fn default_rotation() -> GameEventSequence {
fn default_multiplayer() -> super::MultiplayerConfig {
super::MultiplayerConfig {
players_per_game: 1,
players_per_game: 2,
enabled: true,
network: super::multiplayer::default_net_conf(),
}

View File

@@ -27,6 +27,10 @@ impl AccountProvider {
})
}
pub async fn multiplayer_init(&self) -> Result<(), oj_rc_database::sea_orm::DbErr> {
self.db.complete_all_games().await
}
/*pub fn fake_user<C: Clone>(&self) -> Box<dyn super::User<C> + Send + Sync> {
Box::new(UserData {
token: super::UserToken { uuid: "fake user!".to_owned(), token: "".to_owned(), refresh_token: "".to_owned() },
@@ -306,6 +310,7 @@ impl UserData {
tier: 1, // FIXME
robot_name: current_slot.name,
robot_map: current_slot.robot_data.clone(),
group: None, // no platoon
team: 0,
has_premium: false, // FIXME
robot_uuid: super::i64_as_uuid_str(current_slot.uuid).into(),
@@ -358,6 +363,7 @@ impl UserData {
tier: 1, // FIXME
robot_name: vehicle.name.clone().unwrap_or_else(|| factory_vehicle.1.name.clone()),
robot_map: factory_vehicle.0.cube_data,
group: None,
team: team_num,
has_premium: false,
robot_uuid: uuid_str,
@@ -392,6 +398,7 @@ impl UserData {
tier: 1, // FIXME
robot_name: vehicle.name.clone().unwrap_or_else(|| db_vehicle.name.clone()),
robot_map: db_vehicle.robot_data,
group: None,
team: team_num,
has_premium: false,
robot_uuid: uuid_str,
@@ -430,6 +437,7 @@ impl UserData {
tier: 1, // FIXME
robot_name: vehicle.name.clone().unwrap_or_else(|| "Raw Robot".to_owned()),
robot_map: cube_data.to_owned(),
group: None,
team: team_num,
has_premium: false,
robot_uuid: uuid_str,
@@ -1109,6 +1117,10 @@ impl super::ChatUser for UserData {
#[async_trait::async_trait]
impl super::LobbyUser for UserData {
fn user_id(&self) -> i32 {
self.account.id
}
async fn player_data(&self) -> Result<crate::data::player_data::PlayerData, polariton_server::operations::SimpleOpError> {
self.user_player_data().await.map_err(|e| {
if let Some(msg) = e.error_msg() {
@@ -1119,6 +1131,66 @@ impl super::LobbyUser for UserData {
})
}
async fn start_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 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),
};
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);
polariton_server::operations::SimpleOpError::with_message(
crate::data::error_codes::LobbyReasonCode::UnexpectedError as i16,
format!("Failed to create 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(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),
}
})
.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(())
}
}
#[async_trait::async_trait]
@@ -1135,4 +1207,72 @@ impl super::MultiplayerUser for UserData {
fn display_name(&self) -> &'_ str {
&self.account.display_name
}
async fn current_game(&self) -> Result<Option<super::GameDescriptor>, super::MultiplayerError> {
Ok(self.db.game_by_user_id_and_completion(self.account.id, false).await
.map_err(|e| {
log::error!("Failed to retrieve ongoing game for user {}: {}", self.account.id, e);
super::MultiplayerError {
code: super::MultiplayerErrorCode::CustomString,
message: format!("Failed to retrieve ongoing game: {}", e),
}
})?
.map(|game| super::GameDescriptor {
guid: crate::persist::user::i64_as_uuid_str(game.guid),
map: game.map,
mode: crate::data::game_mode::GameMode::from_db(game.mode),
visibility: crate::data::game_mode::MapVisibility::from_db(game.visibility),
auto_heal: game.auto_heal,
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,
}))
}
async fn game_players(&self, guid: &str) -> Result<Vec<super::PlayerDescriptor>, super::MultiplayerError> {
if let Some(guid) = crate::persist::user::str_to_i64(guid) {
let players = self.db.players_by_game_guid_and_completion_heavy(guid, false).await
.map_err(|e| {
log::error!("Failed to retrieve players for game {} for user {}: {}", guid, self.account.id, e);
super::MultiplayerError {
code: super::MultiplayerErrorCode::CustomString,
message: format!("Failed to retrieve players for game {}: {}", guid, e),
}
})?;
Ok(players.into_iter()
.map(|(player, user)| super::PlayerDescriptor {
user_id: player.user_id,
player_id: player.player_id as u8,
team: player.team,
group: player.group,
is_rewards_claimed: player.is_claimed,
display_name: user.display_name,
public_id: user.public_id,
})
.collect())
} else {
Err(super::MultiplayerError {
code: super::MultiplayerErrorCode::IncorrectGameGuid,
message: format!("Failed to parse game GUID {}", guid),
})
}
}
async fn complete_game(&self, guid: &str) -> Result<(), super::MultiplayerError> {
if let Some(guid) = crate::persist::user::str_to_i64(guid) {
self.db.update_complete_game_by_game_guid(guid).await
.map_err(|e| {
log::error!("Failed to complete ongoing game with user {}: {}", self.account.id, e);
super::MultiplayerError {
code: super::MultiplayerErrorCode::CustomString,
message: format!("Failed to complete ongoing game: {}", e),
}
})
} else {
Err(super::MultiplayerError {
code: super::MultiplayerErrorCode::IncorrectGameGuid,
message: format!("Failed to parse game GUID {}", guid),
})
}
}
}

View File

@@ -11,7 +11,7 @@ mod inventory;
pub use inventory::UnlockedParts;
mod traits;
pub use traits::{UserProvider, User, UserToken, UserSlots, UserSlotData, VehicleData, UserInfo, UserLoginInfo, ExtraUserInfo, UserAuthenticator, NewSlotData, UserId, RegistrationInfo, VehicleUploadData, ChatUser, AvatarInfo, GetAvatarInfo, ControlData, ControlType, CustomisationData, GetCustomisationData, SetSanction, SanctionType, LobbyUser, MultiplayerUser};
pub use traits::{UserProvider, User, UserToken, UserSlots, UserSlotData, VehicleData, UserInfo, UserLoginInfo, ExtraUserInfo, UserAuthenticator, NewSlotData, UserId, RegistrationInfo, VehicleUploadData, ChatUser, AvatarInfo, GetAvatarInfo, ControlData, ControlType, CustomisationData, GetCustomisationData, SetSanction, SanctionType, LobbyUser, GameDescriptor, PlayerLobbyDescriptor, MultiplayerUser, MultiplayerError, MultiplayerErrorCode, PlayerDescriptor};
pub const TOKEN_SECRET_FILENAME: &str = "token_secret.key";

View File

@@ -229,13 +229,76 @@ impl SanctionType {
#[async_trait::async_trait]
pub trait LobbyUser {
fn user_id(&self) -> i32;
async fn player_data(&self) -> Result<crate::data::player_data::PlayerData, polariton_server::operations::SimpleOpError>;
async fn start_game(&self, game: GameDescriptor, players: Vec<PlayerLobbyDescriptor>) -> Result<(), polariton_server::operations::SimpleOpError>;
}
pub struct GameDescriptor {
pub guid: String,
pub map: String,
pub mode: crate::data::game_mode::GameMode,
pub visibility: crate::data::game_mode::MapVisibility,
pub auto_heal: bool,
pub is_ranked: bool,
pub is_custom: bool,
pub is_complete: bool,
}
pub struct PlayerLobbyDescriptor {
pub user_id: i32,
pub team: i32,
pub group: Option<i32>,
}
pub struct PlayerDescriptor {
pub user_id: i32,
pub player_id: u8,
pub team: i32,
pub group: Option<i32>,
pub public_id: String,
pub display_name: String,
pub is_rewards_claimed: bool,
}
#[derive(Debug)]
pub struct MultiplayerError {
pub code: MultiplayerErrorCode,
pub message: String,
}
impl core::fmt::Display for MultiplayerError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{:?}: {}", self.code, self.message)
}
}
impl core::error::Error for MultiplayerError {}
#[repr(u8)]
#[derive(Debug)]
pub enum MultiplayerErrorCode {
HaxSpeed = 0,
HaxException = 1,
HaxTeleport = 2,
HaxEacViolation = 6,
HaxAfk = 7,
HaxFirerange = 8,
HaxFiredamage = 9,
HaxFirerate = 10,
HaxFireposition = 11,
IncorrectGameGuid = 12,
CustomString = 13,
TimedOut = 14,
GameEnded = 15,
}
#[async_trait::async_trait]
pub trait MultiplayerUser {
// TODO
fn user_id(&self) -> i32;
fn user_name(&self) -> &'_ str;
fn display_name(&self) -> &'_ str;
async fn current_game(&self) -> Result<Option<GameDescriptor>, MultiplayerError>;
async fn game_players(&self, guid: &str) -> Result<Vec<PlayerDescriptor>, MultiplayerError>;
async fn complete_game(&self, guid: &str) -> Result<(), MultiplayerError>;
}