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

Implement non-multiplayer parts of custom games

This commit is contained in:
NG (Graham)
2026-03-12 22:44:04 -04:00
parent 6ce456a739
commit af04c4093c
34 changed files with 1751 additions and 78 deletions

View File

@@ -194,4 +194,22 @@ impl MapVisibility {
Self::Bad => oj_rc_database::schema::multiplayer_game::MapVisibility::Bad,
}
}
#[inline]
pub fn from_u8(num: u8) -> Option<Self> {
match num {
0 => Some(Self::Good),
1 => Some(Self::Poor),
2 => Some(Self::Bad),
_ => None,
}
}
pub fn as_str(&self) -> &'static str {
match self {
Self::Good => "Good",
Self::Poor => "Poor",
Self::Bad => "VeryPoor",
}
}
}

View File

@@ -112,3 +112,16 @@ impl PlayerDatas {
Typed::Bytes(buf.into())
}
}
pub struct AvatarInfo {
pub avatar_id: Option<i32>,
}
impl AvatarInfo {
pub fn as_transmissible<C>(&self) -> Typed<C> {
Typed::HashMap(vec![
(Typed::Str("useCustomAvatar".into()), Typed::Bool(self.avatar_id.is_none())),
(Typed::Str("avatarId".into()), Typed::Int(self.avatar_id.unwrap_or(0))),
].into())
}
}

View File

@@ -1331,6 +1331,37 @@ impl <C: Clone + Send> super::User<C> for UserData {
Ok(())
}
async fn list_avatar_info(&self, public_ids: &[String]) -> Result<Vec<super::SocialInfo>, polariton_server::operations::SimpleOpError> {
let users = self.db.users_by_public_id(public_ids.iter()).await
.map_err(|e| {
log::error!("Failed to retrieve friend avatars for user {} : {}", self.account.id, e);
polariton_server::operations::SimpleOpError::with_message(
crate::data::error_codes::WebServicesError::DatabaseError as i16,
format!("Failed to retrieve friend avatars: {}", e),
)
})?;
let user_ids = users.iter().map(|user| user.id);
let user_avatars = self.db.user_auxs_by_user_ids_and_descriptor(user_ids, oj_rc_database::schema::user_aux::Descriptor::AvatarId).await
.map_err(|e| {
log::error!("Failed to retrieve friend avatars for user {} : {}", self.account.id, e);
polariton_server::operations::SimpleOpError::with_message(
crate::data::error_codes::WebServicesError::DatabaseError as i16,
format!("Failed to retrieve friend avatars: {}", e),
)
})?;
let avatar_map: std::collections::HashMap<i32, u32> = user_avatars.iter()
.filter_map(|avatar| avatar.data.parse().ok().map(|avatar_id| (avatar.user_id, avatar_id)))
.collect();
Ok(users.iter()
.map(|user| super::SocialInfo {
public_id: user.public_id.clone(),
display_name: user.display_name.clone(),
avatar_id: avatar_map.get(&user.id).and_then(|&avatar_id| if avatar_id == u32::MAX { None } else { Some(avatar_id as i32) }),
})
.collect()
)
}
fn current_game_event_setter(&self) -> Box<dyn super::GameEventSetter> {
Box::new(GameEventSetterImpl {
db: self.db.clone(),

View File

@@ -95,6 +95,7 @@ pub trait User<C>: ChatUser + SocialUser + SocialUserC<C> + LobbyUser + Multipla
async fn last_seen(&self) -> Result<u64, i16>;
async fn get_avatar_info(&self) -> Result<GetAvatarInfo<C>, i16>;
async fn set_avatar_info(&self, info: AvatarInfo) -> Result<(), i16>;
async fn list_avatar_info(&self, public_ids: &[String]) -> Result<Vec<SocialInfo>, polariton_server::operations::SimpleOpError>;
fn current_game_event_setter(&self) -> Box<dyn GameEventSetter>;
async fn apply_purchase(&self, action: &crate::persist::config::ShopAction) -> Result<PurchaseResult, polariton_server::operations::SimpleOpError>;
async fn currency_debit(&self, ty: CurrencyType, to_sub: u64) -> Result<(), polariton_server::operations::SimpleOpError>;

View File

@@ -1,4 +1,5 @@
#[repr(u8)]
#[derive(Debug)]
pub enum ValidationResultCode {
Invalid = 0,
Ok = 1,

View File

@@ -0,0 +1,613 @@
use crate::data::custom_games::*;
fn custom_game_key(creator: &str) -> String {
let now = chrono::Utc::now().timestamp();
format!("{}_{}_cg", creator, now)
}
pub struct UserInfo {
pub public_id: String,
pub is_invited: bool,
pub team: u8,
pub state: PlayerSessionStatus,
}
pub struct SessionInfo {
pub session_id: String,
pub config: std::collections::HashMap<String, String>,
pub users: Vec<UserInfo>,
}
pub struct KickInfo {
pub session_id: String,
pub was_invited: bool,
}
pub struct CustomGameMesh {
games: tokio::sync::RwLock<std::collections::HashMap<String, GameHandle>>,
user_to_game: tokio::sync::RwLock<std::collections::HashMap<String, String>>,
}
struct GameHandle {
users: Vec<UserHandle>,
config: GameConfig,
}
struct UserHandle {
public_id: String,
is_invited: std::sync::atomic::AtomicBool,
team: u8,
status: std::sync::atomic::AtomicU8,
}
impl CustomGameMesh {
pub fn new() -> Self {
Self {
games: tokio::sync::RwLock::new(std::collections::HashMap::new()),
user_to_game: tokio::sync::RwLock::new(std::collections::HashMap::new()),
}
}
pub async fn create_game(&self, public_id: &str) -> Result<String, 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);
return Err(SessionCreateResponseCode::AlreadyInSession);
}
let game_id = custom_game_key(public_id);
let owner_handle = UserHandle {
public_id: public_id.to_owned(),
is_invited: std::sync::atomic::AtomicBool::new(false),
team: 0,
status: std::sync::atomic::AtomicU8::new(PlayerSessionStatus::Ready.to_u8()),
};
let game_handle = GameHandle {
users: vec![owner_handle],
config: GameConfig::default(),
};
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)
}
pub async fn leave_game(&self, public_id: &str) -> (SessionLeaveResponseCode, Option<SessionInfo>) {
if let Some(game_id) = { self.user_to_game.write().await.remove(public_id) } {
let mut games_lock = self.games.write().await;
if let Some(game) = games_lock.get_mut(&game_id) {
let session = if game.users.len() == 1 || game.users.iter().all(|u| u.public_id == public_id || u.is_invited.load(std::sync::atomic::Ordering::Relaxed)) {
log::debug!("User {} has disbanded custom game {}", public_id, game_id);
if game.users.len() != 1 {
let mut user_lock = self.user_to_game.write().await;
for user in game.users.iter() {
user_lock.remove(&user.public_id);
}
log::debug!("Removed {} invited stragglers from custom game {}", game.users.len() - 1, game_id);
}
let session = Self::session_from_game(&game_id, game);
games_lock.remove(&game_id);
session
} else {
log::debug!("User {} has left custom game {}", public_id, game_id);
game.users.retain(|user| user.public_id != public_id);
Self::session_from_game(&game_id, game)
};
return (SessionLeaveResponseCode::Success, Some(session));
}
}
(SessionLeaveResponseCode::NotInSession, None)
}
pub async fn kick_from_game(&self, kicker: &str, kickee: &str) -> (KickResponseCode, Option<(KickInfo, SessionInfo)>) {
if let Some(game_id) = { self.user_to_game.write().await.remove(kickee) } {
let mut games_lock = self.games.write().await;
if let Some(game) = games_lock.get_mut(&game_id) {
let leader = game.users.first().unwrap();
if leader.public_id != kicker {
return (KickResponseCode::UserIsNotSessionLeader, None);
}
let target = game.users.iter().find(|mem| mem.public_id == kickee).unwrap();
let kick_info = KickInfo {
session_id: game_id.clone(),
was_invited: target.is_invited.load(std::sync::atomic::Ordering::Relaxed),
};
game.users.retain(|user| user.public_id != kickee);
let session = Self::session_from_game(&game_id, game);
return (KickResponseCode::UserRemovedFromSession, Some((kick_info, session)));
}
}
(KickResponseCode::KickTargetIsNotInsession, None)
}
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()
});
}
}
None
}
pub async fn invite_user(&self, inviter: &str, invitee: &str, is_team_a: bool) -> (InviteToCustomGameResponseCode, Option<SessionInfo>) {
if let Some(game_id) = { self.user_to_game.read().await.get(inviter).cloned() } {
if let Some(game) = self.games.write().await.get_mut(&game_id) {
if game.users.iter().find(|x| x.public_id == invitee).is_some() {
let session = Self::session_from_game(&game_id, game);
(InviteToCustomGameResponseCode::InviteeHasAlreadyBeenInvited, Some(session))
} else {
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 },
status: std::sync::atomic::AtomicU8::new(PlayerSessionStatus::Unknown.to_u8())
};
game.users.push(invitee_handle);
let session = Self::session_from_game(&game_id, game);
self.user_to_game.write().await.insert(invitee.to_owned(), game_id);
log::debug!("User {} invited to custom game", invitee);
(InviteToCustomGameResponseCode::UserInvited, Some(session))
}
} else {
(InviteToCustomGameResponseCode::UserIsNotInSession, None)
}
} else {
(InviteToCustomGameResponseCode::UserIsNotInSession, None)
}
}
pub async fn update_invite_user(&self, invitee: &str, is_accept: bool) -> (InviteReplyCustomGameResponseCode, Option<SessionInfo>) {
if let Some(game_id) = { self.user_to_game.read().await.get(invitee).cloned() } {
if is_accept {
// fully join custom game
if let Some(game) = self.games.read().await.get(&game_id) {
let invitee_handle_opt = game.users.iter().find(|user| user.public_id == invitee);
if let Some(invitee_handle) = invitee_handle_opt {
invitee_handle.is_invited.store(false, std::sync::atomic::Ordering::Relaxed);
invitee_handle.status.store(PlayerSessionStatus::Ready.to_u8(), std::sync::atomic::Ordering::Relaxed);
let session = Self::session_from_game(&game_id, game);
(InviteReplyCustomGameResponseCode::Success, Some(session))
} else {
(InviteReplyCustomGameResponseCode::UserIsNoLongerInvited, None)
}
} else {
(InviteReplyCustomGameResponseCode::UserIsNoLongerInvited, None)
}
} else {
// leave custom game
if let Some(game) = self.games.write().await.get_mut(&game_id) {
game.users.retain(|user| user.public_id != invitee);
self.user_to_game.write().await.remove(invitee);
let session = Self::session_from_game(&game_id, game);
(InviteReplyCustomGameResponseCode::Success, Some(session))
} else {
(InviteReplyCustomGameResponseCode::UserIsNoLongerInvited, None)
}
}
} else {
(InviteReplyCustomGameResponseCode::UserIsNotInSession, None)
}
}
pub async fn update_user_status(&self, public_id: &str, status: PlayerSessionStatus) -> 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) {
let target = game.users.iter()
.find(|mem| mem.public_id == public_id)
.unwrap();
target.status.store(status.to_u8(), std::sync::atomic::Ordering::Relaxed);
let session = Self::session_from_game(&game_id, game);
return Some(session);
}
}
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);
}
if let Ok(_) = game.config.set_field(field, value) {
log::debug!("Update custom game session {} config {} to {}", game_id, field, value);
let session = Self::session_from_game(&game_id, game);
return (AdjustCustomGameConfigResponseCode::Success, Some(session));
}
}
return (AdjustCustomGameConfigResponseCode::AdjustmentRejected, None);
} else {
(AdjustCustomGameConfigResponseCode::NotInSession, None)
}
}
fn session_from_game(game_id: &str, game: &GameHandle) -> SessionInfo {
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_or(PlayerSessionStatus::Unknown),
})
.collect()
}
}
}
// multipliers are percents (as in, 100 is no change; 200 is 2x original, 10 is 0.1x)
struct GameConfig {
game_mode: oj_rc_core::data::game_mode::GameMode,
map: String, // TODO maybe this should be validated
map_visibility: oj_rc_core::data::game_mode::MapVisibility,
health_regen: bool,
capture_segment_memory: bool,
base_shields_go_down: bool,
damage_mult: i32,
health_mult: i32,
power_mult: i32,
game_time: i32, // minutes?
capture_speed: i32, // this is two things in one (seconds?)
points_kill_streak: bool,
points_total_required: i32,
number_of_kills_to_win: i32,
respawn_time: i32, // this is three things in one
core_appear_frequency: i32,
core_health_multiplier: i32,
core_destroy_time: i32,
protonium_harvest: i32,
ceiling_multiplier: i32,
min_cpu: i32,
max_cpu: i32,
}
impl core::default::Default for GameConfig {
fn default() -> Self {
Self {
game_mode: oj_rc_core::data::game_mode::GameMode::BattleArena,
map: oj_rc_core::data::game_mode::GameMap::Earth2.as_str().to_owned(),
map_visibility: oj_rc_core::data::game_mode::MapVisibility::Good,
health_regen: true,
capture_segment_memory: true,
base_shields_go_down: true,
damage_mult: 100,
health_mult: 100,
power_mult: 100,
game_time: 10,
capture_speed: 120,
points_kill_streak: false,
points_total_required: 1,
number_of_kills_to_win: 1,
respawn_time: 1,
core_appear_frequency: 1,
core_health_multiplier: 100,
core_destroy_time: 10,
protonium_harvest: 10,
ceiling_multiplier: 100,
min_cpu: 200,
max_cpu: 100_000,
}
}
}
enum ConfigSetError {
ValueParseError,
InvalidField,
}
impl GameConfig {
fn as_map(&self) -> std::collections::HashMap<String, String> {
let mut map = std::collections::HashMap::with_capacity(10);
map.insert("GameMode".to_owned(), self.game_mode.as_str().to_owned());
map.insert("MapChoice".to_owned(), self.map.clone());
map.insert("MapVisibility".to_owned(), self.map_visibility.as_str().to_owned());
map.insert("HealthRegen".to_owned(), if self.health_regen { "True".to_owned() } else { "False".to_owned() });
map.insert("CaptureSegmentMemory".to_owned(), if self.capture_segment_memory { "True".to_owned() } else { "False".to_owned() });
map.insert("BaseShieldsGoDown".to_owned(), if self.base_shields_go_down { "True".to_owned() } else { "False".to_owned() });
map.insert("DamageMultiplier".to_owned(), self.damage_mult.to_string());
map.insert("HealthMultiplier".to_owned(), self.health_mult.to_string());
map.insert("PowerMultiplier".to_owned(), self.power_mult.to_string());
map.insert("GameTime".to_owned(), self.game_time.to_string());
map.insert("CaptureSpeedElimination".to_owned(), self.capture_speed.to_string());
map.insert("PointsKillStreakOnOff".to_owned(), if self.points_kill_streak { "True".to_owned() } else { "False".to_owned() });
map.insert("PointsTotalRequired".to_owned(), self.points_total_required.to_string());
map.insert("NumberOfKillsToWin".to_owned(), self.number_of_kills_to_win.to_string());
map.insert("RespawnTimeBA".to_owned(), self.respawn_time.to_string());
map.insert("RespawnTimeTDM".to_owned(), self.respawn_time.to_string());
map.insert("RespawnTimePit".to_owned(), self.respawn_time.to_string());
map.insert("CoreAppearFrequency".to_owned(), self.core_appear_frequency.to_string());
map.insert("CoreHealthMultiplier".to_owned(), self.core_health_multiplier.to_string());
map.insert("CoreDestroyTimeValue".to_owned(), self.core_destroy_time.to_string());
map.insert("CaptureSpeedBA".to_owned(), self.capture_speed.to_string());
map.insert("ProtoniumHarvestBA".to_owned(), self.protonium_harvest.to_string());
map.insert("CeilingMultiplier".to_owned(), self.ceiling_multiplier.to_string());
map.insert("MinCPU".to_owned(), self.min_cpu.to_string());
map.insert("MaxCPU".to_owned(), self.max_cpu.to_string());
map
}
fn set_field(&mut self, field: &str, value: &str) -> Result<(), ConfigSetError> {
match field {
"GameMode" => {
let val = match value {
"TeamDeathmatch" => Ok(oj_rc_core::data::game_mode::GameMode::TeamDeathmatch),
"BattleArena" => Ok(oj_rc_core::data::game_mode::GameMode::BattleArena),
"Pit" => Ok(oj_rc_core::data::game_mode::GameMode::Pit),
"SuddenDeath" => Ok(oj_rc_core::data::game_mode::GameMode::SuddenDeath),
idk_val => {
log::warn!("Unrecognized game mode {}", idk_val);
Err(ConfigSetError::ValueParseError)
}
}?;
self.game_mode = val;
Ok(())
},
"MapChoice" => {
self.map = value.to_owned();
Ok(())
},
"MapVisibility" => {
let val = match value {
"VeryPoor" => Ok(oj_rc_core::data::game_mode::MapVisibility::Bad),
"Poor" => Ok(oj_rc_core::data::game_mode::MapVisibility::Poor),
"Good" => Ok(oj_rc_core::data::game_mode::MapVisibility::Good),
idk_val => {
log::warn!("Unrecognized map visibility {}", idk_val);
Err(ConfigSetError::ValueParseError)
}
}?;
self.map_visibility = val;
Ok(())
},
"HealthRegen" => {
match value.to_lowercase().parse() { // parsing only accepts "true" or "false", C# uses "True" or "False"
Err(e) => {
log::warn!("Failed to parse value {} as bool for field {} : {}", value, field, e);
Err(ConfigSetError::ValueParseError)
},
Ok(val) => {
self.health_regen = val;
Ok(())
}
}
},
"CaptureSegmentMemory" => {
match value.to_lowercase().parse() { // parsing only accepts "true" or "false", C# uses "True" or "False"
Err(e) => {
log::warn!("Failed to parse value {} as bool for field {} : {}", value, field, e);
Err(ConfigSetError::ValueParseError)
},
Ok(val) => {
self.capture_segment_memory = val;
Ok(())
}
}
},
"BaseShieldsGoDown" => {
match value.to_lowercase().parse() { // parsing only accepts "true" or "false", C# uses "True" or "False"
Err(e) => {
log::warn!("Failed to parse value {} as bool for field {} : {}", value, field, e);
Err(ConfigSetError::ValueParseError)
},
Ok(val) => {
self.base_shields_go_down = val;
Ok(())
}
}
},
"DamageMultiplier" => {
match value.parse() {
Err(e) => {
log::warn!("Failed to parse value {} as i32 for field {} : {}", value, field, e);
Err(ConfigSetError::ValueParseError)
},
Ok(val) => {
self.damage_mult = val;
Ok(())
}
}
},
"HealthMultiplier" => {
match value.parse() {
Err(e) => {
log::warn!("Failed to parse value {} as i32 for field {} : {}", value, field, e);
Err(ConfigSetError::ValueParseError)
},
Ok(val) => {
self.health_mult = val;
Ok(())
}
}
},
"PowerMultiplier" => {
match value.parse() {
Err(e) => {
log::warn!("Failed to parse value {} as i32 for field {} : {}", value, field, e);
Err(ConfigSetError::ValueParseError)
},
Ok(val) => {
self.power_mult = val;
Ok(())
}
}
},
"GameTime" => {
match value.parse() {
Err(e) => {
log::warn!("Failed to parse value {} as i32 for field {} : {}", value, field, e);
Err(ConfigSetError::ValueParseError)
},
Ok(val) => {
self.game_time = val;
Ok(())
}
}
},
"CaptureSpeedElimination" | "CaptureSpeedBA" => {
match value.parse() {
Err(e) => {
log::warn!("Failed to parse value {} as i32 for field {} : {}", value, field, e);
Err(ConfigSetError::ValueParseError)
},
Ok(val) => {
self.capture_speed = val;
Ok(())
}
}
},
"PointsKillStreakOnOff" => {
match value.to_lowercase().parse() { // parsing only accepts "true" or "false", C# uses "True" or "False"
Err(e) => {
log::warn!("Failed to parse value {} as bool for field {} : {}", value, field, e);
Err(ConfigSetError::ValueParseError)
},
Ok(val) => {
self.points_kill_streak = val;
Ok(())
}
}
},
"PointsTotalRequired" => {
match value.parse() {
Err(e) => {
log::warn!("Failed to parse value {} as i32 for field {} : {}", value, field, e);
Err(ConfigSetError::ValueParseError)
},
Ok(val) => {
self.points_total_required = val;
Ok(())
}
}
},
"NumberOfKillsToWin" => {
match value.parse() {
Err(e) => {
log::warn!("Failed to parse value {} as i32 for field {} : {}", value, field, e);
Err(ConfigSetError::ValueParseError)
},
Ok(val) => {
self.number_of_kills_to_win = val;
Ok(())
}
}
},
"RespawnTimeBA" | "RespawnTimeTDM" | "RespawnTimePit" => {
match value.parse() {
Err(e) => {
log::warn!("Failed to parse value {} as i32 for field {} : {}", value, field, e);
Err(ConfigSetError::ValueParseError)
},
Ok(val) => {
self.respawn_time = val;
Ok(())
}
}
},
"CoreAppearFrequency" => {
match value.parse() {
Err(e) => {
log::warn!("Failed to parse value {} as i32 for field {} : {}", value, field, e);
Err(ConfigSetError::ValueParseError)
},
Ok(val) => {
self.core_appear_frequency = val;
Ok(())
}
}
},
"CoreHealthMultiplier" => {
match value.parse() {
Err(e) => {
log::warn!("Failed to parse value {} as i32 for field {} : {}", value, field, e);
Err(ConfigSetError::ValueParseError)
},
Ok(val) => {
self.core_health_multiplier = val;
Ok(())
}
}
},
"CoreDestroyTimeValue" => {
match value.parse() {
Err(e) => {
log::warn!("Failed to parse value {} as i32 for field {} : {}", value, field, e);
Err(ConfigSetError::ValueParseError)
},
Ok(val) => {
self.core_destroy_time = val;
Ok(())
}
}
},
"ProtoniumHarvestBA" => {
match value.parse() {
Err(e) => {
log::warn!("Failed to parse value {} as i32 for field {} : {}", value, field, e);
Err(ConfigSetError::ValueParseError)
},
Ok(val) => {
self.protonium_harvest = val;
Ok(())
}
}
},
"CeilingMultiplier" => {
match value.parse() {
Err(e) => {
log::warn!("Failed to parse value {} as i32 for field {} : {}", value, field, e);
Err(ConfigSetError::ValueParseError)
},
Ok(val) => {
self.ceiling_multiplier = val;
Ok(())
}
}
},
"MinCPU" => {
match value.parse() {
Err(e) => {
log::warn!("Failed to parse value {} as i32 for field {} : {}", value, field, e);
Err(ConfigSetError::ValueParseError)
},
Ok(val) => {
self.min_cpu = val;
Ok(())
}
}
},
"MaxCPU" => {
match value.parse() {
Err(e) => {
log::warn!("Failed to parse value {} as i32 for field {} : {}", value, field, e);
Err(ConfigSetError::ValueParseError)
},
Ok(val) => {
self.max_cpu = val;
Ok(())
}
}
},
_ => {
log::warn!("Unrecognized custom game config field {} (val: {})", field, value);
Err(ConfigSetError::InvalidField)
}
}
}
}

View File

@@ -1,6 +1,5 @@
#![allow(dead_code)]
pub use oj_rc_core::data::game_mode::GameMode;
use polariton::operation::Typed;
#[repr(u8)]
#[derive(Copy, Clone)]
@@ -8,3 +7,192 @@ pub enum CustomGameInviteCode {
NoInvite = 0,
PendingInvite = 1,
}
#[repr(u8)]
#[derive(Copy, Clone)]
pub enum SessionCreateResponseCode {
SessionCreated = 0,
AlreadyInSession = 1,
SessionCreateError = 2,
}
#[repr(u8)]
#[derive(Copy, Clone)]
pub enum AdjustCustomGameConfigResponseCode {
Success = 0,
NotInSession = 1,
AdjustmentRejected = 2,
// 3, 4, 5 also exist but seem equivalent to 2
}
#[repr(u8)]
#[derive(Copy, Clone)]
pub enum SessionLeaveResponseCode {
Success = 0,
NotInSession = 1,
}
#[repr(u8)]
#[derive(Copy, Clone)]
pub enum KickResponseCode {
SessionNoLongerExists = 0,
KickTargetIsNotInsession = 1,
UserIsNotSessionLeader = 2,
UserRemovedFromSession = 3,
ErrorKickingFromSession = 4
}
#[repr(u8)]
#[derive(Copy, Clone)]
pub enum SessionRetrieveResponse {
UserNotInAnySession = 0,
SessionRetrieved = 1,
PlayerIsInvitedOnly = 2,
ErrorRetrievingSession = 3,
}
#[repr(u8)]
#[derive(Copy, Clone)]
pub enum InviteToCustomGameResponseCode {
UserIsNotInSession = 0,
UserIsNotSessionLeader = 1,
InviteeHasAlreadyBeenInvited = 2,
UserIsNotOnline = 3,
UserInvited = 4,
ErrorDispatchingMessage = 5,
InviteeIsInAnotherCustomGame = 6,
InviteeIsAlreadyInvitedToAnotherCustomGame = 7,
UserDoesNotExist = 8,
UserOnlyAcceptsInvitesFromFriendsAndClanmates = 9,
UserBlockedYou = 10,
}
#[repr(u8)]
#[derive(Copy, Clone)]
pub enum InviteReplyCustomGameResponseCode {
Success = 1,
Failure2 = 2,
UserIsNotInSession = 4,
UserIsNoLongerInvited = 5,
}
#[repr(u8)]
#[derive(Copy, Clone)]
pub enum CheckCanJoinQueueResponseCode {
UserNotInSession0 = 0,
OnlyOneAllowed = 1,
Unbalanced = 2,
AlreadyInBattle = 3,
Ok = 4,
UserNotInSession5 = 5,
}
#[repr(u8)]
#[derive(Copy, Clone)]
pub enum PlayerSessionStatus {
Unknown = 0,
Ready = 1,
Queuing = 2,
InBattle = 3,
}
impl PlayerSessionStatus {
#[inline]
pub fn from_u8(num: u8) -> Option<Self> {
match num {
0 => Some(Self::Unknown),
1 => Some(Self::Ready),
2 => Some(Self::Queuing),
3 => Some(Self::InBattle),
_ => None,
}
}
#[inline]
pub fn to_u8(self) -> u8 {
self as u8
}
}
pub struct Session {
pub leader: String,
pub session: String,
pub members: Vec<String>,
pub members_display_name: Vec<String>,
pub invited: Vec<String>,
pub team_b_members: Vec<String>,
pub config: std::collections::HashMap<String, String>,
pub avatar_info: std::collections::HashMap<String, oj_rc_core::data::player_data::AvatarInfo>,
pub player_session_state: std::collections::HashMap<String, PlayerSessionStatus>,
}
impl Session {
pub fn as_transmissible<C>(&self) -> Typed<C> {
Typed::HashMap(vec![
(Typed::Str("Leader".into()), Typed::Str(self.leader.clone().into())),
(Typed::Str("SessionID".into()), Typed::Str(self.session.clone().into())),
(Typed::Str("Members".into()), Typed::HashMap(self.members.iter()
.enumerate()
.map(|(i, pub_id)| (Typed::Int(i as _), Typed::Str(pub_id.into())))
.collect::<Vec<(_, _)>>()
.into()
)),
(Typed::Str("MembersDisplayName".into()), Typed::HashMap(self.members_display_name.iter()
.enumerate()
.map(|(i, display_name)| (Typed::Int(i as _), Typed::Str(display_name.into())))
.collect::<Vec<(_, _)>>()
.into()
)),
(Typed::Str("Invited".into()), Typed::HashMap(self.invited.iter()
.enumerate()
.map(|(i, pub_id)| (Typed::Int(i as _), Typed::Str(pub_id.into())))
.collect::<Vec<(_, _)>>()
.into()
)),
(Typed::Str("TeamBMembers".into()), Typed::HashMap(self.team_b_members.iter()
.enumerate()
.map(|(i, pub_id)| (Typed::Int(i as _), Typed::Str(pub_id.into())))
.collect::<Vec<(_, _)>>()
.into()
)),
(Typed::Str("Config".into()), Typed::HashMap(self.config.iter()
.map(|(key, val)| (Typed::Str(key.into()), Typed::Str(val.into())))
.collect::<Vec<(_, _)>>()
.into()
)),
(Typed::Str("AvatarInfo".into()), Typed::HashMap(self.avatar_info.iter()
.map(|(key, val)| (Typed::Str(key.into()), val.as_transmissible()))
.collect::<Vec<(_, _)>>()
.into()
)),
(Typed::Str("PlayerSessionState".into()), Typed::HashMap(self.player_session_state.iter()
.map(|(key, val)| (Typed::Str(key.into()), Typed::Int(*val as _)))
.collect::<Vec<(_, _)>>()
.into()
)),
].into())
}
}
/// not to be confused with the event which contains the same data
/// ... just serialized differently (to be difficult?)
pub struct CustomGameInvite {
pub inviter_public_id: String,
pub inviter_display_name: String,
pub session: String,
pub avatar_id: Option<i32>,
pub invited_to_team_b: bool,
}
impl CustomGameInvite {
pub fn as_transmissible<C>(&self) -> Typed<C> {
Typed::HashMap(vec![
(polariton::operation::Typed::Str("SenderName".into()), polariton::operation::Typed::Str(self.inviter_public_id.clone().into())),
(polariton::operation::Typed::Str("SenderDisplayName".into()), polariton::operation::Typed::Str(self.inviter_display_name.clone().into())),
(polariton::operation::Typed::Str("SessionGUID".into()), polariton::operation::Typed::Str(self.session.clone().into())),
(polariton::operation::Typed::Str("UseCustomAvatar".into()), polariton::operation::Typed::Bool(self.avatar_id.is_none())),
(polariton::operation::Typed::Str("AvatarID".into()), polariton::operation::Typed::Int(self.avatar_id.unwrap_or(0))),
(polariton::operation::Typed::Str("IsInvitedToTeamB".into()), polariton::operation::Typed::Bool(self.invited_to_team_b)),
].into())
}
}

View File

@@ -0,0 +1,25 @@
use polariton::operation::Typed;
#[derive(Clone)]
pub struct CustomGameConfigRefresh {
pub field: String,
pub value: String,
}
impl <C: Send + Sync + 'static> polariton_server::events::IntoEvent<C> for CustomGameConfigRefresh {
const CHANNEL: u8 = 0;
const ENCRYPT: bool = true;
const RELIABLE: bool = true;
fn into_event(self) -> polariton::operation::Event<C> {
let mut params = polariton::operation::ParameterTable::with_capacity(1);
params.insert(181, polariton::operation::Typed::HashMap(vec![
(Typed::Str("Field".into()), Typed::Str(self.field.into())),
(Typed::Str("Value".into()), Typed::Str(self.value.into())),
].into()));
polariton::operation::Event {
code: 9,
params,
}
}
}

View File

@@ -0,0 +1,30 @@
//#[derive(Clone)]
pub struct CustomGameInvite {
pub inviter_public_id: String,
pub inviter_display_name: String,
pub session: String,
pub avatar_id: Option<i32>,
pub invited_to_team_a: bool,
}
impl <C: Send + Sync + 'static> polariton_server::events::IntoEvent<C> for CustomGameInvite {
const CHANNEL: u8 = 0;
const ENCRYPT: bool = true;
const RELIABLE: bool = true;
fn into_event(self) -> polariton::operation::Event<C> {
let mut params = polariton::operation::ParameterTable::with_capacity(1);
params.insert(172, polariton::operation::Typed::HashMap(vec![
(polariton::operation::Typed::Str("Inviter".into()), polariton::operation::Typed::Str(self.inviter_public_id.into())),
(polariton::operation::Typed::Str("DisplayName".into()), polariton::operation::Typed::Str(self.inviter_display_name.into())),
(polariton::operation::Typed::Str("SessionID".into()), polariton::operation::Typed::Str(self.session.into())),
(polariton::operation::Typed::Str("UseCustomAvatar".into()), polariton::operation::Typed::Bool(self.avatar_id.is_none())),
(polariton::operation::Typed::Str("AvatarID".into()), polariton::operation::Typed::Int(self.avatar_id.unwrap_or(0))),
(polariton::operation::Typed::Str("InvitedToTeamA".into()), polariton::operation::Typed::Bool(self.invited_to_team_a)),
].into()));
polariton::operation::Event {
code: 7,
params,
}
}
}

View File

@@ -0,0 +1,23 @@
use polariton::operation::Typed;
#[derive(Clone)]
pub struct CustomGameInviteDecline {
pub public_id: String,
}
impl <C: Send + Sync + 'static> polariton_server::events::IntoEvent<C> for CustomGameInviteDecline {
const CHANNEL: u8 = 0;
const ENCRYPT: bool = true;
const RELIABLE: bool = true;
fn into_event(self) -> polariton::operation::Event<C> {
let mut params = polariton::operation::ParameterTable::with_capacity(1);
params.insert(190, polariton::operation::Typed::HashMap(vec![
(Typed::Str("UserName".into()), Typed::Str(self.public_id.into()))
].into()));
polariton::operation::Event {
code: 12,
params,
}
}
}

View File

@@ -0,0 +1,25 @@
use polariton::operation::Typed;
//#[derive(Clone)]
pub struct CustomGameKick {
pub session: String,
pub was_invited: bool,
}
impl <C: Send + Sync + 'static> polariton_server::events::IntoEvent<C> for CustomGameKick {
const CHANNEL: u8 = 0;
const ENCRYPT: bool = true;
const RELIABLE: bool = true;
fn into_event(self) -> polariton::operation::Event<C> {
let mut params = polariton::operation::ParameterTable::with_capacity(2);
params.insert(184, polariton::operation::Typed::HashMap(vec![
(Typed::Str("Session".into()), Typed::Str(self.session.into())),
(Typed::Str("WasInvited".into()), Typed::Bool(self.was_invited)),
].into()));
polariton::operation::Event {
code: 11,
params,
}
}
}

View File

@@ -0,0 +1,19 @@
#[derive(Clone)]
pub struct CustomGameRefresh {
pub session: String,
}
impl <C: Send + Sync + 'static> polariton_server::events::IntoEvent<C> for CustomGameRefresh {
const CHANNEL: u8 = 0;
const ENCRYPT: bool = true;
const RELIABLE: bool = true;
fn into_event(self) -> polariton::operation::Event<C> {
let mut params = polariton::operation::ParameterTable::with_capacity(1);
params.insert(172, polariton::operation::Typed::Str(self.session.into()));
polariton::operation::Event {
code: 8,
params,
}
}
}

View File

@@ -4,7 +4,7 @@ use oj_rc_core::persist::user::intercom::IntercomWebServiceUserMessage;
pub struct IntercomHandler {
listener: IntercomListener<IntercomWebServiceUserMessage>,
user: std::sync::Weak<Box<dyn oj_rc_core::persist::user::User<()> + Send + Sync>>,
emitter: polariton_server::events::EventEmitter<()>,
emitter: polariton_server::events::WeakEventEmitter<()>,
}
impl IntercomHandler {
@@ -16,14 +16,14 @@ impl IntercomHandler {
Self {
listener,
user: std::sync::Arc::downgrade(user),
emitter: emitter.to_owned(),
emitter: emitter.to_owned().downgrade(),
}
}
async fn run_loop(
listener: IntercomListener<IntercomWebServiceUserMessage>,
user: std::sync::Weak<Box<dyn oj_rc_core::persist::user::User<()> + Send + Sync>>,
emitter: polariton_server::events::EventEmitter<()>
emitter: polariton_server::events::WeakEventEmitter<()>
) {
use futures::StreamExt;
let mut listener = listener.listen().await;

View File

@@ -6,3 +6,18 @@ pub use dev_message::DevMessage;
mod maintenance_mode;
pub use maintenance_mode::MaintenanceMode;
mod custom_game_invite;
pub use custom_game_invite::CustomGameInvite;
mod custom_game_refresh;
pub use custom_game_refresh::CustomGameRefresh;
mod custom_game_config;
pub use custom_game_config::CustomGameConfigRefresh;
mod custom_game_invite_decline;
pub use custom_game_invite_decline::CustomGameInviteDecline;
mod custom_game_kick;
pub use custom_game_kick::CustomGameKick;

View File

@@ -5,6 +5,8 @@ mod data;
mod events;
mod operations;
mod vehicle_validators;
mod custom_game_tracker;
mod user_service;
use oj_polariton_auth::Handshake;
use tokio::net;
@@ -25,6 +27,8 @@ pub struct InitConfig {
pub factory: std::sync::Arc<oj_rc_core::factory::Factory>,
pub parsers: oj_rc_core::cubes::CubeParsers,
pub vehicle_validators: vehicle_validators::InitedVehicleValidators,
pub custom_games: std::sync::Arc<custom_game_tracker::CustomGameMesh>,
pub user_mesh: std::sync::Arc<user_service::UserMesh>,
}
#[tokio::main]
@@ -48,7 +52,9 @@ async fn main() -> std::io::Result<()> {
users,
factory,
parsers,
vehicle_validators
vehicle_validators,
custom_games: std::sync::Arc::new(custom_game_tracker::CustomGameMesh::new()),
user_mesh: std::sync::Arc::new(user_service::UserMesh::new()),
});
let server = std::sync::Arc::new(polariton_server::Server::new(operations::handler(&init_ctx), polariton_server::events::EventsHandler::new()));
@@ -77,7 +83,7 @@ async fn main() -> std::io::Result<()> {
}
async fn process_socket(mut socket: net::TcpStream, address: std::net::SocketAddr, server: std::sync::Arc<polariton_server::Server<crate::UserTy>>, init_ctx: std::sync::Arc<InitConfig>) {
let login_num = std::sync::Arc::new(LOGINS.fetch_add(1, std::sync::atomic::Ordering::SeqCst));
let login_num = LOGINS.fetch_add(1, std::sync::atomic::Ordering::SeqCst);
log::debug!("Accepting connection from address {} (login #{})", address, login_num);
let enc = match do_connect_handshake(&mut socket).await {
Some(x) => x,
@@ -86,31 +92,25 @@ async fn process_socket(mut socket: net::TcpStream, address: std::net::SocketAdd
return;
}
};
{
if let Ok(mut handles) = USER_HANDLES.lock() {
handles.push(std::sync::Arc::downgrade(&login_num));
ONLINE_USERS.store(handles.len() as u64, std::sync::atomic::Ordering::SeqCst);
} else {
// this should never happen
log::warn!("USER_HANDLES lock is poisoned, cannot track online users anymore (please restart)");
}
}
let (socket_r, socket_w) = socket.into_split();
let (chann_tx, chann_rx) = tokio::sync::mpsc::unbounded_channel();
let user_state = std::sync::Arc::new(oj_rc_core::UserState::<()>::new(init_ctx.users.clone(), chann_tx.clone()));
let ctx = polariton::packet::SerdesContext::from_boxed(Default::default(), enc);
server.handle_async_with_channel_join(socket_r, socket_w, user_state.clone(), ctx, chann_tx, chann_rx).await;
log::debug!("Goodbye connection from address {} (login #{})", address, login_num);
drop(login_num); // explicit for good measure
{
if let Ok(mut handles) = USER_HANDLES.lock() {
handles.retain(|x| x.strong_count() != 0);
ONLINE_USERS.store(handles.len() as u64, std::sync::atomic::Ordering::SeqCst);
} else {
// this should never happen
ONLINE_USERS.fetch_sub(1, std::sync::atomic::Ordering::SeqCst);
log::warn!("USER_HANDLES lock is poisoned, cannot track online users anymore (please restart)");
if let Ok(user) = user_state.user() {
let pub_id = user.public_id();
init_ctx.user_mesh.remove_user(pub_id.to_owned()).await;
if let Some(session) = init_ctx.custom_games.leave_game(pub_id).await.1 {
let session_members = session.users.iter()
.filter(|mem| !mem.is_invited)
.map(|mem| &mem.public_id as &str);
let update_event = crate::events::CustomGameRefresh {
session: session.session_id,
};
init_ctx.user_mesh.broadcast_event_to(session_members, update_event).await;
}
ONLINE_USERS.store(init_ctx.user_mesh.user_count().await as u64, std::sync::atomic::Ordering::SeqCst);
}
if let Ok(user_info) = user_state.user() {
update_status(user_info.as_ref().as_ref()).await;

View File

@@ -0,0 +1,50 @@
use polariton_server::operations::{SimpleOpError, SimpleOperation, SimpleOpImpl};
use polariton::operation::{ParameterTable, Typed};
const CODE: u8 = 149;
const FIELD_PARAM_KEY: u8 = 179; // str; in
const VALUE_PARAM_KEY: u8 = 180; // any; in
const RESPONSE_CODE_PARAM_KEY: u8 = 168; // int enum; out
pub(super) struct CustomGameConfigChanger {
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 CustomGameConfigChanger {
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(field_name)) = params.remove(&FIELD_PARAM_KEY) {
if let Some(Typed::Str(value)) = params.remove(&VALUE_PARAM_KEY) {
let user_info = user.user()?;
let my_pub_id = user_info.public_id();
let (resp_code, session_opt) = self.games.set_config_field(my_pub_id, &field_name.string, &value.string).await;
if let Some(session) = session_opt {
let event = crate::events::CustomGameConfigRefresh {
field: field_name.string.clone(),
value: value.string.clone(),
};
let session_members_iter = session.users.iter()
.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;
}
params.insert(RESPONSE_CODE_PARAM_KEY, Typed::Int(resp_code as _));
}
} else {
log::warn!("Missing custom game field name parameter string");
}
Ok(params)
}
}
pub(super) fn game_adjust_provider<C: Send + 'static>(init_ctx: &crate::InitConfig) -> SimpleOpImpl<C, crate::UserTy, CustomGameConfigChanger> {
SimpleOpImpl::new(CustomGameConfigChanger {
games: init_ctx.custom_games.clone(),
mesh: init_ctx.user_mesh.clone(),
})
}

View File

@@ -0,0 +1,43 @@
use polariton_server::operations::{SimpleOpError, SimpleOperation, SimpleOpImpl};
use polariton::operation::{ParameterTable, Typed};
const CODE: u8 = 153;
const RESPONSE_CODE_PARAM_KEY: u8 = 168; // int enum; out
pub(super) struct CustomGameCanJoinQueue {
games: std::sync::Arc<crate::custom_game_tracker::CustomGameMesh>,
validator: std::sync::Arc<crate::vehicle_validators::InitedVehicleValidator>,
}
#[async_trait::async_trait]
impl <C: Send + 'static> SimpleOperation<C> for CustomGameCanJoinQueue {
type User = crate::UserTy;
const CODE: u8 = CODE;
async fn handle(&self, mut params: ParameterTable<C>, user: &Self::User) -> Result<ParameterTable<C>, SimpleOpError> {
let user_info = user.user()?;
let my_pub_id = user_info.public_id();
let resp_code = if let Some(session) = self.games.get_user_game(my_pub_id).await {
let vehicle = user_info.selected_vehicle_data().await?;
match self.validator.validate(&vehicle.robot_data, &vehicle.colour_data) {
oj_rc_plugins::vehicle_validation::ValidationResultCode::Ok => crate::data::custom_games::CheckCanJoinQueueResponseCode::Ok,
err_code => {
log::debug!("Failed to validate user {} vehicle for custom game {}: {:?}", my_pub_id, session.session_id, err_code);
crate::data::custom_games::CheckCanJoinQueueResponseCode::Unbalanced
}
}
} else {
crate::data::custom_games::CheckCanJoinQueueResponseCode::UserNotInSession0
};
params.insert(RESPONSE_CODE_PARAM_KEY, Typed::Int(resp_code as _));
Ok(params)
}
}
pub(super) fn game_can_queue_provider<C: Send + 'static>(init_ctx: &crate::InitConfig) -> SimpleOpImpl<C, crate::UserTy, CustomGameCanJoinQueue> {
SimpleOpImpl::new(CustomGameCanJoinQueue {
games: init_ctx.custom_games.clone(),
validator: init_ctx.vehicle_validators.custom_game.clone(),
})
}

View File

@@ -0,0 +1,38 @@
use polariton_server::operations::{SimpleOpError, SimpleOperation, SimpleOpImpl};
use polariton::operation::{ParameterTable, Typed};
const CODE: u8 = 143;
const RESPONSE_CODE_PARAM_KEY: u8 = 168; // int enum; out
const IDK_PARAM_KEY: u8 = 169; // ???; out (let's send back the game id)
pub(super) struct CustomGameCreator {
games: std::sync::Arc<crate::custom_game_tracker::CustomGameMesh>,
}
#[async_trait::async_trait]
impl <C: Send + 'static> SimpleOperation<C> for CustomGameCreator {
type User = crate::UserTy;
const CODE: u8 = CODE;
async fn handle(&self, mut params: ParameterTable<C>, user: &Self::User) -> Result<ParameterTable<C>, SimpleOpError> {
let user_info = user.user()?;
let result = self.games.create_game(user_info.public_id()).await;
match result {
Err(e) => {
params.insert(RESPONSE_CODE_PARAM_KEY, Typed::Int(e as _));
},
Ok(game_id) => {
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()));
}
}
Ok(params)
}
}
pub(super) fn game_create_provider<C: Send + 'static>(games: &std::sync::Arc<crate::custom_game_tracker::CustomGameMesh>) -> SimpleOpImpl<C, crate::UserTy, CustomGameCreator> {
SimpleOpImpl::new(CustomGameCreator {
games: games.to_owned(),
})
}

View File

@@ -0,0 +1,53 @@
use polariton_server::operations::{SimpleOpError, SimpleOperation, SimpleOpImpl};
use polariton::operation::{ParameterTable, Typed};
const CODE: u8 = 148;
const ACCEPT_PARAM_KEY: u8 = 173; // bool; in
const RESPONSE_CODE_PARAM_KEY: u8 = 168; // int enum; out
pub(super) struct CustomGameInviteResponder {
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 CustomGameInviteResponder {
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::Bool(is_accept)) = params.remove(&ACCEPT_PARAM_KEY) {
let user_info = user.user()?;
let my_pub_id = user_info.public_id();
let (resp_code, session_opt) = self.games.update_invite_user(my_pub_id, is_accept).await;
if let Some(session) = session_opt {
if !is_accept {
let event = crate::events::CustomGameInviteDecline {
public_id: my_pub_id.to_owned(),
};
let session_members_iter = session.users.iter()
.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;
}
let event = crate::events::CustomGameRefresh {
session: session.session_id,
};
let other_session_members_iter = session.users.iter()
.filter(|mem| !mem.is_invited && mem.public_id != my_pub_id)
.map(|mem| &mem.public_id as &str);
self.mesh.broadcast_event_to(other_session_members_iter, event).await;
}
params.insert(RESPONSE_CODE_PARAM_KEY, Typed::Int(resp_code as _));
}
Ok(params)
}
}
pub(super) fn game_invite_respond_provider<C: Send + 'static>(init_ctx: &crate::InitConfig) -> SimpleOpImpl<C, crate::UserTy, CustomGameInviteResponder> {
SimpleOpImpl::new(CustomGameInviteResponder {
games: init_ctx.custom_games.clone(),
mesh: init_ctx.user_mesh.clone(),
})
}

View File

@@ -0,0 +1,70 @@
use polariton_server::operations::{SimpleOpError, SimpleOperation, SimpleOpImpl};
use polariton::operation::{ParameterTable, Typed};
const CODE: u8 = 147;
const INVITEE_PARAM_KEY: u8 = 171; // str; in
const IS_TEAM_A_PARAM_KEY: u8 = 175; // bool; in
const RESPONSE_CODE_PARAM_KEY: u8 = 168; // int enum; out
pub(super) struct CustomGameInviter {
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 CustomGameInviter {
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(invitee_id)) = params.remove(&INVITEE_PARAM_KEY) {
if let Some(Typed::Bool(is_team_a)) = params.remove(&IS_TEAM_A_PARAM_KEY) {
let user_info = user.user()?;
let my_pub_id = user_info.public_id();
let avatars = user_info.list_avatar_info(&[my_pub_id.to_owned(), invitee_id.string.clone()]).await?;
// TODO resolve public_id of invitee by provided display name
let resp_code = if avatars.iter().find(|x| x.public_id == invitee_id.string).is_none() {
crate::data::custom_games::InviteToCustomGameResponseCode::UserDoesNotExist
} else {
let (resp_code, session_opt) = self.games.invite_user(my_pub_id, &invitee_id.string, is_team_a).await;
if let Some(session) = session_opt {
if self.mesh.is_user_online(&invitee_id.string).await {
let my_avatar = avatars.iter().find(|x| x.public_id == my_pub_id).unwrap();
log::debug!("User {} invited {} to custom game {}", my_pub_id, invitee_id.string, session.session_id);
let event = crate::events::CustomGameInvite {
inviter_public_id: my_pub_id.to_owned(),
inviter_display_name: user_info.display_name().to_owned(),
session: session.session_id.clone(),
avatar_id: my_avatar.avatar_id,
invited_to_team_a: is_team_a,
};
self.mesh.send_event_to(&invitee_id.string, event).await;
let event = crate::events::CustomGameRefresh {
session: session.session_id,
};
let other_members = session.users.iter()
.filter(|mem| !mem.is_invited)
.map(|mem| &mem.public_id as &str);
self.mesh.broadcast_event_to(other_members, event).await;
resp_code
} else {
crate::data::custom_games::InviteToCustomGameResponseCode::UserIsNotOnline
}
} else {
resp_code
}
};
params.insert(RESPONSE_CODE_PARAM_KEY, Typed::Int(resp_code as _));
}
}
Ok(params)
}
}
pub(super) fn game_invite_provider<C: Send + 'static>(init_ctx: &crate::InitConfig) -> SimpleOpImpl<C, crate::UserTy, CustomGameInviter> {
SimpleOpImpl::new(CustomGameInviter {
games: init_ctx.custom_games.clone(),
mesh: init_ctx.user_mesh.clone(),
})
}

View File

@@ -0,0 +1,49 @@
use polariton_server::operations::{SimpleOpError, SimpleOperation, SimpleOpImpl};
use polariton::operation::{ParameterTable, Typed};
const CODE: u8 = 150;
const TO_KICK_PARAM_KEY: u8 = 183; // str; in
const RESPONSE_CODE_PARAM_KEY: u8 = 168; // int enum; out
pub(super) struct CustomGameMemberKicker {
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 CustomGameMemberKicker {
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(user_to_kick)) = params.remove(&TO_KICK_PARAM_KEY) {
let user_info = user.user()?;
let my_pub_id = user_info.public_id();
let (resp_code, session_opt) = self.games.kick_from_game(my_pub_id, &user_to_kick.string).await;
if let Some((kick_info, session)) = session_opt {
log::debug!("User {} kicked {} from custom game {}", my_pub_id, user_to_kick.string, session.session_id);
let kick_event = crate::events::CustomGameKick {
session: kick_info.session_id,
was_invited: kick_info.was_invited,
};
self.mesh.send_event_to(&user_to_kick.string, kick_event).await;
let update_event = crate::events::CustomGameRefresh {
session: session.session_id,
};
let members_iter = session.users.iter()
.map(|mem| &mem.public_id as &str);
self.mesh.broadcast_event_to(members_iter, update_event).await;
}
params.insert(RESPONSE_CODE_PARAM_KEY, Typed::Int(resp_code as _));
}
Ok(params)
}
}
pub(super) fn game_kick_provider<C: Send + 'static>(init_ctx: &crate::InitConfig) -> SimpleOpImpl<C, crate::UserTy, CustomGameMemberKicker> {
SimpleOpImpl::new(CustomGameMemberKicker {
games: init_ctx.custom_games.clone(),
mesh: init_ctx.user_mesh.clone(),
})
}

View File

@@ -0,0 +1,39 @@
use polariton_server::operations::{SimpleOpError, SimpleOperation, SimpleOpImpl};
use polariton::operation::{ParameterTable, Typed};
const CODE: u8 = 145;
const RESPONSE_CODE_PARAM_KEY: u8 = 168; // int enum; out
pub(super) struct CustomGameLeaver {
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 CustomGameLeaver {
type User = crate::UserTy;
const CODE: u8 = CODE;
async fn handle(&self, mut params: ParameterTable<C>, user: &Self::User) -> Result<ParameterTable<C>, SimpleOpError> {
let user_info = user.user()?;
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,
};
let session_members = session.users.iter()
.map(|mem| &mem.public_id as &str);
self.mesh.broadcast_event_to(session_members, event).await;
}
params.insert(RESPONSE_CODE_PARAM_KEY, Typed::Int(resp_code as _));
Ok(params)
}
}
pub(super) fn game_leave_provider<C: Send + 'static>(init_ctx: &crate::InitConfig) -> SimpleOpImpl<C, crate::UserTy, CustomGameLeaver> {
SimpleOpImpl::new(CustomGameLeaver {
games: init_ctx.custom_games.clone(),
mesh: init_ctx.user_mesh.clone(),
})
}

View File

@@ -0,0 +1,46 @@
use polariton_server::operations::{SimpleOpError, SimpleOperation, SimpleOpImpl};
use polariton::operation::{ParameterTable, Typed};
const CODE: u8 = 152;
const NEW_STATE_PARAM_KEY: u8 = 188; // int enum; in
pub(super) struct CustomGameMemberStateUpdate {
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 CustomGameMemberStateUpdate {
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::Int(new_state)) = params.remove(&NEW_STATE_PARAM_KEY) {
let new_status_enum = crate::data::custom_games::PlayerSessionStatus::from_u8(new_state as _)
.ok_or_else(|| SimpleOpError::with_message(
oj_rc_core::data::error_codes::WebServicesError::UnexpectedError as _,
format!("Unrecognized PlayerSessionStatus {}", new_state),
))?;
let user_info = user.user()?;
let my_pub_id = user_info.public_id();
let session_opt = self.games.update_user_status(my_pub_id, new_status_enum).await;
if let Some(session) = session_opt {
let update_event = crate::events::CustomGameRefresh {
session: session.session_id,
};
let members_iter = session.users.iter()
.map(|mem| &mem.public_id as &str);
self.mesh.broadcast_event_to(members_iter, update_event).await;
}
}
Ok(params)
}
}
pub(super) fn game_player_status_update_provider<C: Send + 'static>(init_ctx: &crate::InitConfig) -> SimpleOpImpl<C, crate::UserTy, CustomGameMemberStateUpdate> {
SimpleOpImpl::new(CustomGameMemberStateUpdate {
games: init_ctx.custom_games.clone(),
mesh: init_ctx.user_mesh.clone(),
})
}

View File

@@ -1,13 +1,71 @@
use polariton_server::operations::SimpleFunc;
use polariton_server::operations::{SimpleOpError, SimpleOperation, SimpleOpImpl};
use polariton::operation::{ParameterTable, Typed};
const RESPONSE_CODE_PARAM_KEY: u8 = 168;
//const CUSTOM_GAME_DATA_PARAM_KEY: u8 = 169;
const CODE: u8 = 144;
pub(super) fn get_custom_session_provider() -> SimpleFunc<144, crate::UserTy, impl (Fn(ParameterTable, &crate::UserTy) -> Result<ParameterTable, i16>) + Sync + Sync> {
SimpleFunc::new(|params, _| {
let mut params = params.to_dict();
params.insert(RESPONSE_CODE_PARAM_KEY, Typed::Int(0 /* Not in any session */));
Ok(params.into())
const RESPONSE_CODE_PARAM_KEY: u8 = 168; // int enum; out
const RESPONSE_DATA_PARAM_KEY: u8 = 169; // hashtable; out
pub(super) struct CustomGameRetriever {
games: std::sync::Arc<crate::custom_game_tracker::CustomGameMesh>,
}
#[async_trait::async_trait]
impl <C: Send + 'static> SimpleOperation<C> for CustomGameRetriever {
type User = crate::UserTy;
const CODE: u8 = CODE;
async fn handle(&self, mut params: ParameterTable<C>, user: &Self::User) -> Result<ParameterTable<C>, SimpleOpError> {
let user_info = user.user()?;
let my_pub_id = user_info.public_id();
let game_opt = self.games.get_user_game(my_pub_id).await;
if let Some(game) = game_opt {
log::debug!("User {} retrieved their custom game session {} info", my_pub_id, game.session_id);
params.insert(RESPONSE_CODE_PARAM_KEY, Typed::Int(crate::data::custom_games::SessionRetrieveResponse::SessionRetrieved as _));
let pub_ids: Vec<String> = game.users.iter()
.map(|member| member.public_id.clone())
.collect();
let avatars = user_info.list_avatar_info(&pub_ids).await?;
let avatar_map: std::collections::HashMap<_, _> = avatars.into_iter()
.map(|avatar| (avatar.public_id.clone(), avatar)).collect();
let resp = crate::data::custom_games::Session {
leader: game.users.first().map(|leader| leader.public_id.clone()).unwrap_or_default(),
session: game.session_id,
members: game.users.iter().map(|mem| mem.public_id.clone()).collect(),
members_display_name: game.users.iter()
.filter_map(|mem| avatar_map.get(&mem.public_id))
.map(|mem| mem.display_name.clone())
.collect(),
invited: game.users.iter()
.filter(|mem| mem.is_invited)
.map(|mem| mem.public_id.clone())
.collect(),
team_b_members: game.users.iter()
.filter(|mem| mem.team == 1)
.map(|mem| mem.public_id.clone())
.collect(),
config: game.config,
avatar_info: avatar_map.iter()
.map(|(pub_id, avatar)| (pub_id.to_owned(), oj_rc_core::data::player_data::AvatarInfo {
avatar_id: avatar.avatar_id,
}))
.collect(),
player_session_state: game.users.iter()
.map(|mem| (mem.public_id.clone(), mem.state))
.collect(),
};
params.insert(RESPONSE_DATA_PARAM_KEY, resp.as_transmissible());
} else {
log::debug!("User {} is not in any custom game session", my_pub_id);
params.insert(RESPONSE_CODE_PARAM_KEY, Typed::Int(crate::data::custom_games::SessionRetrieveResponse::UserNotInAnySession as _));
}
Ok(params)
}
}
pub(super) fn custom_session_provider<C: Send + 'static>(games: &std::sync::Arc<crate::custom_game_tracker::CustomGameMesh>) -> SimpleOpImpl<C, crate::UserTy, CustomGameRetriever> {
SimpleOpImpl::new(CustomGameRetriever {
games: games.to_owned(),
})
}

View File

@@ -1,15 +1,56 @@
use polariton_server::operations::SimpleFunc;
use polariton_server::operations::{SimpleOpError, SimpleOperation, SimpleOpImpl};
use polariton::operation::{ParameterTable, Typed};
use crate::data::custom_games::*;
const PARAM_KEY: u8 = 168;
//const INVITE_PARAM_KEY: u8 = 189; // hashtable (refer to C# CheckIfHasBeenInvitedToCustomGameSessionRequest)
const CODE: u8 = 0;
pub(super) fn pending_invite_provider() -> SimpleFunc<0, crate::UserTy, impl (Fn(ParameterTable, &crate::UserTy) -> Result<ParameterTable, i16>) + Sync + Sync> {
SimpleFunc::new(|params, _| {
let mut params = params.to_dict();
params.insert(PARAM_KEY, Typed::Int(CustomGameInviteCode::NoInvite as _));
Ok(params.into())
const RESULT_CODE_PARAM_KEY: u8 = 168; // int enum; out
const INVITE_PARAM_KEY: u8 = 189; // hashtable (refer to C# CheckIfHasBeenInvitedToCustomGameSessionRequest)
pub(super) struct CustomGamePendingInvites {
games: std::sync::Arc<crate::custom_game_tracker::CustomGameMesh>,
}
#[async_trait::async_trait]
impl <C: Send + 'static> SimpleOperation<C> for CustomGamePendingInvites {
type User = crate::UserTy;
const CODE: u8 = CODE;
async fn handle(&self, mut params: ParameterTable<C>, user: &Self::User) -> Result<ParameterTable<C>, SimpleOpError> {
let user_info = user.user()?;
let my_pub_id = user_info.public_id();
if let Some(session) = self.games.get_user_game(my_pub_id).await {
let myself = session.users.iter().find(|u| u.public_id == my_pub_id).unwrap();
if myself.is_invited {
log::debug!("User {} has checked and is invited to custom game {}", my_pub_id, session.session_id);
params.insert(RESULT_CODE_PARAM_KEY, Typed::Int(CustomGameInviteCode::PendingInvite as _));
// build invite data
let leader = session.users.first().unwrap();
let leader_avatar = user_info.list_avatar_info(&[leader.public_id.clone()]).await?;
let resp = CustomGameInvite {
inviter_public_id: leader_avatar[0].public_id.clone(),
inviter_display_name: leader_avatar[0].display_name.clone(),
session: session.session_id,
avatar_id: leader_avatar[0].avatar_id,
invited_to_team_b: myself.team == 1,
};
params.insert(INVITE_PARAM_KEY, resp.as_transmissible());
} else {
log::debug!("User {} has already accepted invite to custom game {}", my_pub_id, session.session_id);
params.insert(RESULT_CODE_PARAM_KEY, Typed::Int(CustomGameInviteCode::NoInvite as _));
}
} else {
log::debug!("User {} is not invited to any custom game", my_pub_id);
params.insert(RESULT_CODE_PARAM_KEY, Typed::Int(CustomGameInviteCode::NoInvite as _));
}
Ok(params)
}
}
pub(super) fn pending_invite_provider<C: Send + 'static>(init_ctx: &crate::InitConfig) -> SimpleOpImpl<C, crate::UserTy, CustomGamePendingInvites> {
SimpleOpImpl::new(CustomGamePendingInvites {
games: init_ctx.custom_games.clone(),
})
}

View File

@@ -1,11 +1,20 @@
use polariton_server::operations::SimpleFunc;
use polariton::{operation::{Dict, ParameterTable, Typed}, serdes::TypePrefix};
use crate::data::custom_games::*;
const MODE_MAP_PARAM_KEY: u8 = 170;
const MAP_NAMES_PARAM_KEY: u8 = 178;
const ALL_MAPS: &[&str] = &[
"RC_Planet_Mars_01_CTF", // og flat mars
"RC_Planet_Mars_02_BA", // the one with the bridge in the middle
"RC_Planet_Mars_03_BA", // tharsis rift without the rift
"RC_Planet_Neptune_01_CTF", // og flat GJ1214b gliese lake without the lake
"RC_Planet_Neptune_02_BA", // the one with the cave
"RC_Planet_Neptune_03_BA", // spitzer dam
"RC_Planet_Earth_01_BA", // birmingham power station
"RC_Planet_Earth_02_BA", // vanguard
];
pub(super) fn allowed_maps_provider() -> SimpleFunc<146, crate::UserTy, impl (Fn(ParameterTable, &crate::UserTy) -> Result<ParameterTable, i16>) + Sync + Sync> {
SimpleFunc::new(|params, _| {
let mut params = params.to_dict();
@@ -13,19 +22,33 @@ pub(super) fn allowed_maps_provider() -> SimpleFunc<146, crate::UserTy, impl (Fn
key_ty: TypePrefix::Str, // str
val_ty: TypePrefix::ObjArr, // obj arr
items: vec![
(Typed::Str(GameMode::BattleArena.as_str().into()), Typed::ObjArr(vec![
Typed::Str("RC_Planet_Neptune_02_BA".into()),
Typed::Str("RC_Planet_Mars_03_BA".into()),
Typed::Str("RC_Planet_Mars_02_BA".into()),
Typed::Str("RC_Planet_Earth_02_BA".into()),
Typed::Str("RC_Planet_Earth_01_BA".into()),
Typed::Str("RC_Planet_Neptune_03_BA".into()),
].into())
(Typed::Str(oj_rc_core::data::game_mode::GameMode::BattleArena.as_str().into()), Typed::ObjArr(
ALL_MAPS.iter()
.map(|x| Typed::Str(x.into()))
.collect::<Vec<_>>()
.into()
)
),
(Typed::Str(GameMode::TeamDeathmatch.as_str().into()), Typed::ObjArr(vec![
Typed::Str("RC_Planet_Neptune_01_CTF".into()),
Typed::Str("RC_Planet_Mars_01_CTF".into()),
].into())
(Typed::Str(oj_rc_core::data::game_mode::GameMode::TeamDeathmatch.as_str().into()), Typed::ObjArr(
ALL_MAPS.iter()
.map(|x| Typed::Str(x.into()))
.collect::<Vec<_>>()
.into()
)
),
(Typed::Str(oj_rc_core::data::game_mode::GameMode::Pit.as_str().into()), Typed::ObjArr(
ALL_MAPS.iter()
.map(|x| Typed::Str(x.into()))
.collect::<Vec<_>>()
.into()
)
),
(Typed::Str(oj_rc_core::data::game_mode::GameMode::SuddenDeath.as_str().into()), Typed::ObjArr(
ALL_MAPS.iter()
.map(|x| Typed::Str(x.into()))
.collect::<Vec<_>>()
.into()
)
),
],
}));

View File

@@ -1,8 +1,6 @@
use polariton_server::operations::SimpleFunc;
use polariton::{operation::{Dict, ParameterTable, Typed}, serdes::TypePrefix};
use crate::data::custom_games::*;
const PARAM_KEY: u8 = 168;
pub(super) fn team_setup_provider() -> SimpleFunc<162, crate::UserTy, impl (Fn(ParameterTable, &crate::UserTy) -> Result<ParameterTable, i16>) + Sync + Sync> {
@@ -12,13 +10,13 @@ pub(super) fn team_setup_provider() -> SimpleFunc<162, crate::UserTy, impl (Fn(P
key_ty: TypePrefix::Str, // str
val_ty: TypePrefix::Int, // int
items: vec![
(Typed::Str(GameMode::BattleArena.as_str().into()), Typed::Int(10)),
(Typed::Str(GameMode::SuddenDeath.as_str().into()), Typed::Int(10)),
(Typed::Str(GameMode::Pit.as_str().into()), Typed::Int(10)),
(Typed::Str(GameMode::TestMode.as_str().into()), Typed::Int(10)),
(Typed::Str(GameMode::SinglePlayer.as_str().into()), Typed::Int(1)),
(Typed::Str(GameMode::TeamDeathmatch.as_str().into()), Typed::Int(10)),
(Typed::Str(GameMode::Campaign.as_str().into()), Typed::Int(6)),
(Typed::Str(oj_rc_core::data::game_mode::GameMode::BattleArena.as_str().into()), Typed::Int(10)),
(Typed::Str(oj_rc_core::data::game_mode::GameMode::SuddenDeath.as_str().into()), Typed::Int(10)),
(Typed::Str(oj_rc_core::data::game_mode::GameMode::Pit.as_str().into()), Typed::Int(20)),
(Typed::Str(oj_rc_core::data::game_mode::GameMode::TestMode.as_str().into()), Typed::Int(10)),
(Typed::Str(oj_rc_core::data::game_mode::GameMode::SinglePlayer.as_str().into()), Typed::Int(1)),
(Typed::Str(oj_rc_core::data::game_mode::GameMode::TeamDeathmatch.as_str().into()), Typed::Int(10)),
(Typed::Str(oj_rc_core::data::game_mode::GameMode::Campaign.as_str().into()), Typed::Int(1)),
] }));
Ok(params.into())
})

View File

@@ -108,6 +108,14 @@ mod crf_make_featured;
mod crf_unmake_featured;
mod tech_tree_unlock_cube;
mod crf_remove;
mod custom_game_create;
mod custom_game_adjust;
mod custom_game_leave;
mod custom_game_invite_to;
mod custom_game_invite_respond;
mod custom_game_kick;
mod custom_game_player_state;
mod custom_game_can_join_queue;
use polariton_server::operations::OperationsHandler;
@@ -115,7 +123,7 @@ pub fn handler(init_ctx: &crate::InitConfig) -> OperationsHandler<crate::UserTy>
OperationsHandler::new()
.modify(oj_rc_core::polariton::RcOpModifier)
.add(eac::EacChallengeIgnorer)
.add(more_auth::MoreLobbyAuth)
.add(more_auth::more_auth_provider(&init_ctx.user_mesh))
.add(versioner::version_teller(&init_ctx.cubes))
.add(maintenancer::maintenace_teller(&init_ctx.cubes))
.add(game_quality::QualityConfigTeller)
@@ -154,7 +162,7 @@ pub fn handler(init_ctx: &crate::InitConfig) -> OperationsHandler<crate::UserTy>
.add(dev_message::dev_message_provider(&init_ctx.cubes))
.add(custom_games_maps::allowed_maps_provider())
.add(avatar_info::avatar_get_provider())
.add(custom_game_session::get_custom_session_provider())
.add(custom_game_session::custom_session_provider(&init_ctx.custom_games))
.add(user_xp::get_user_xp_provider())
.add(garage_upgrades::garage_upgrades_provider(&init_ctx.cubes))
.add(game_event_params::event_system_params_provider(&init_ctx.cubes))
@@ -174,8 +182,8 @@ pub fn handler(init_ctx: &crate::InitConfig) -> OperationsHandler<crate::UserTy>
.add(robot_mastery_settings::robot_mastery_settings_provider())
.add(player_started_purchase::started_purchase_provider())
.add(custom_games_team::team_setup_provider())
.add(polariton_server::operations::Ack::<152, _>::default()) // custom game player state changed (188 is desired state)
.add(custom_games_invite::pending_invite_provider())
//.add(polariton_server::operations::Ack::<152, _>::default()) // custom game player state changed (188 is desired state)
.add(custom_games_invite::pending_invite_provider(init_ctx))
.add(chat_settings::chat_settings_provider())
.add(polariton_server::operations::Ack::<19, _>::default()) // save chat settings
.add(prebuilt_robots::garage_robot_data_provider())
@@ -240,4 +248,12 @@ pub fn handler(init_ctx: &crate::InitConfig) -> OperationsHandler<crate::UserTy>
.add(tech_tree_unlock_cube::tech_tree_cube_unlock_provider(&init_ctx.cubes))
.add(crf_remove::factory_remove_provider(&init_ctx.factory))
.add(polariton_server::operations::Ack::<94, _>::default()) // ReportCommunityShopItemRequest FIXME: log reports
.add(custom_game_create::game_create_provider(&init_ctx.custom_games))
.add(custom_game_adjust::game_adjust_provider(init_ctx))
.add(custom_game_leave::game_leave_provider(init_ctx))
.add(custom_game_invite_to::game_invite_provider(init_ctx))
.add(custom_game_invite_respond::game_invite_respond_provider(init_ctx))
.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))
}

View File

@@ -1,7 +1,15 @@
use polariton::operation::Typed;
use polariton_server::operations::{Operation, OperationCode};
pub struct MoreLobbyAuth;
pub struct MoreLobbyAuth {
mesh: std::sync::Arc<crate::user_service::UserMesh>,
}
pub fn more_auth_provider(mesh: &std::sync::Arc<crate::user_service::UserMesh>) -> MoreLobbyAuth {
MoreLobbyAuth {
mesh: mesh.to_owned()
}
}
impl MoreLobbyAuth {
const AUTH_PAYLOAD_KEY: u8 = 245;
@@ -27,6 +35,11 @@ impl <C: Send + 'static> Operation<C> for MoreLobbyAuth {
} else {
match user_info.webservice_listener().await {
Ok(listener) => {
self.mesh.add_user(
user_info.public_id().to_owned(),
user.event_sender().to_owned().downgrade(),
).await;
crate::ONLINE_USERS.store(self.mesh.user_count().await as u64, std::sync::atomic::Ordering::SeqCst);
crate::update_status(user_info.as_ref().as_ref()).await;
let mut resp_params = std::collections::HashMap::with_capacity(1);
resp_params.insert(Self::AUTH_PAYLOAD_KEY, polariton::operation::Typed::Byte(0));
@@ -49,7 +62,8 @@ impl <C: Send + 'static> Operation<C> for MoreLobbyAuth {
},
}
}
} else {
log::debug!("Authentication failed for user");
}
}
polariton::operation::OperationResponse {

View File

@@ -0,0 +1,82 @@
pub struct UserMesh {
online_users: tokio::sync::RwLock<std::collections::HashMap<String, UserHandle>>,
}
struct UserHandle {
emitter: polariton_server::events::WeakEventEmitter,
is_alive: std::sync::atomic::AtomicBool,
}
impl UserMesh {
pub fn new() -> Self {
Self {
online_users: tokio::sync::RwLock::new(std::collections::HashMap::new()),
}
}
pub async fn user_count(&self) -> usize {
self.online_users.read().await.values()
.filter(|u| u.is_alive.load(std::sync::atomic::Ordering::Relaxed))
.count()
}
/// returns whether the user was replaced (true) or new (false)
pub async fn add_user(
&self,
public_id: String,
emitter: polariton_server::events::WeakEventEmitter,
) -> bool {
let handle = UserHandle {
emitter,
is_alive: std::sync::atomic::AtomicBool::new(true),
};
self.online_users.write().await
.insert(public_id, handle)
.is_some()
}
/// returns whether the user existing (true) or not (false)
pub async fn remove_user(
&self,
public_id: String,
) -> bool {
self.online_users.write().await
.remove(&public_id)
.is_some()
}
pub async fn broadcast_event_to(&self, public_ids: impl std::iter::Iterator<Item = &str>, event: impl polariton_server::events::IntoEvent<()> + Clone) -> bool {
let user_lock = self.online_users.read().await;
let mut total_success = true;
for public_id in public_ids {
let is_success = if let Some(user_handle) = user_lock.get(public_id) {
let is_success = user_handle.emitter.emit(event.clone());
user_handle.is_alive.swap(is_success, std::sync::atomic::Ordering::SeqCst);
is_success
} else {
false
};
total_success &= is_success;
}
total_success
}
pub async fn send_event_to(&self, public_id: &str, event: impl polariton_server::events::IntoEvent<()>) -> bool {
let user_lock = self.online_users.read().await;
if let Some(user_handle) = user_lock.get(public_id) {
let is_success = user_handle.emitter.emit(event);
user_handle.is_alive.swap(is_success, std::sync::atomic::Ordering::SeqCst);
is_success
} else {
false
}
}
pub async fn is_user_online(&self, public_id: &str) -> bool {
if let Some(user) = self.online_users.read().await.get(public_id) {
user.is_alive.load(std::sync::atomic::Ordering::Relaxed)
} else {
false
}
}
}

View File

@@ -36,6 +36,7 @@ mod clan_invite_decline_all;
mod clan_invite_cancel;
mod clan_member_rerank;
mod clan_experience_poll;
mod user_can_be_custom_gamed;
use polariton_server::operations::OperationsHandler;
@@ -66,6 +67,7 @@ pub fn handler(init_ctx: &crate::InitConfig) -> OperationsHandler<crate::UserTy,
.add(friend_remove::friend_remove_provider(init_ctx))
.add(platoon_invite_to::platoon_invite_provider(init_ctx))
.add(user_can_be_platooned::can_invite_to_platoon_provider(init_ctx))
.add(user_can_be_custom_gamed::can_invite_to_custom_game_provider(init_ctx))
.add(platoon_invite_accept::platoon_accepter_provider(init_ctx))
.add(platoon_invite_decline::platoon_decliner_provider(init_ctx))
.add(platoon_leave::platoon_leave_provider(init_ctx))

View File

@@ -96,25 +96,26 @@ impl SimpleOperation<crate::data::custom::CustomType> for PlatoonInviter {
}
} else {
// create new platoon
let platoon_id = format!("{}-{}", user_info.public_id(), chrono::Utc::now().timestamp());
log::debug!("Creating new platoon {} for user {} to invite {}", platoon_id, user_info.public_id(), username.string);
let social_infos = user_info.list_social_info(&[
username.string.clone(),
user_info.public_id().to_owned(),
]).await?;
if social_infos.len() != 2 {
log::debug!("User {} info could not be retrieved while creating platoon {}", username.string, platoon_id);
log::debug!("User {} info could not be retrieved while creating new platoon", username.string);
return Err(SimpleOpError::with_message(
oj_rc_core::data::error_codes::SocialErrorCode::UserDoesNotExist as i16,
"User's info could not be retrieved".to_owned(),
));
}
if self.social.create_platoon(&platoon_id, user_info.public_id()).await.is_none() {
let platoon_id = if let Some((_, platoon_key)) = self.social.create_platoon(user_info.public_id()).await {
log::debug!("Created new platoon {} for user {} to invite {}", platoon_key, user_info.public_id(), username.string);
platoon_key
} else {
return Err(SimpleOpError::with_message(
oj_rc_core::data::error_codes::SocialErrorCode::UserNotInPlatoon as i16,
"Failed to create platoon".to_owned(),
));
}
};
if let Some(timestamp) = self.social.add_user_to_platoon(&username.string, &platoon_id, crate::data::platoon::MemberStatus::Invited).await {
(platoon_id, social_infos, timestamp)
} else {

View File

@@ -0,0 +1,42 @@
use polariton_server::operations::{SimpleOpError, SimpleOperation, SimpleOpImpl};
use polariton::operation::{ParameterTable, Typed};
const CODE: u8 = 58;
const USERNAME_PARAM_KEY: u8 = 65; // str; in
const RESPONSE_CODE_PARAM_KEY: u8 = 66; // bool; out
pub(super) struct UserCanBeInvitedToCustoGameGetter {
social: std::sync::Arc<crate::SocialMesh>,
}
#[async_trait::async_trait]
impl SimpleOperation<crate::data::custom::CustomType> for UserCanBeInvitedToCustoGameGetter {
type User = crate::UserTy;
const CODE: u8 = CODE;
async fn handle(&self, mut params: ParameterTable<crate::data::custom::CustomType>, user: &Self::User) -> Result<ParameterTable<crate::data::custom::CustomType>, SimpleOpError> {
if let Some(Typed::Str(username)) = params.remove(&USERNAME_PARAM_KEY) {
let _user_info = user.user()?; // just to validate request is authenticated
let mut set = std::collections::HashSet::with_capacity(1);
set.insert(username.string.clone());
self.social.filter_online_only(&mut set).await;
if set.is_empty() {
params.insert(RESPONSE_CODE_PARAM_KEY, Typed::Bool(false));
return Ok(params);
}
if self.social.platoon_of_user(&username.string).await.is_some() {
params.insert(RESPONSE_CODE_PARAM_KEY, Typed::Bool(false));
return Ok(params);
}
params.insert(RESPONSE_CODE_PARAM_KEY, Typed::Bool(true));
}
Ok(params)
}
}
pub(super) fn can_invite_to_custom_game_provider(init_ctx: &crate::InitConfig) -> SimpleOpImpl<crate::data::custom::CustomType, crate::UserTy, UserCanBeInvitedToCustoGameGetter> {
SimpleOpImpl::new(UserCanBeInvitedToCustoGameGetter {
social: init_ctx.social.clone(),
})
}

View File

@@ -27,6 +27,11 @@ pub struct PlatoonMemberInfo {
pub timestamp: i64,
}
fn platoon_key(creator: &str) -> String {
let now = chrono::Utc::now().timestamp();
format!("{}_{}_p", creator, now)
}
impl SocialMesh {
#[allow(clippy::new_without_default)]
pub fn new() -> Self {
@@ -121,13 +126,14 @@ impl SocialMesh {
}
}
pub async fn create_platoon(&self, platoon_id: &str, public_id: &str) -> Option<i64> {
pub async fn create_platoon(&self, public_id: &str) -> Option<(i64, String)> {
/*let user_handle = if let Some(handle) = self.users.read().await.get(public_id) {
handle.to_owned()
} else {
return None;
};*/
if self.platoons.platoon_by_id.read().await.contains_key(platoon_id) {
let platoon_id = platoon_key(public_id);
if self.platoons.platoon_by_id.read().await.contains_key(&platoon_id) {
return None;
}
let mut platoon_members = Vec::with_capacity(5);
@@ -140,7 +146,7 @@ impl SocialMesh {
});
self.platoons.platoon_by_id.write().await.insert(platoon_id.to_owned(), platoon_members);
self.platoons.platoon_by_user.write().await.insert(public_id.to_owned(), platoon_id.to_owned());
Some(timestamp)
Some((timestamp, platoon_id))
}
pub async fn remove_user_from_platoon(&self, public_id: &str) -> bool {