From 67c14fe80afd9ea91e2038b6cb2d2f93837ab135 Mon Sep 17 00:00:00 2001 From: "NGnius (Graham)" Date: Tue, 18 Mar 2025 16:47:11 -0400 Subject: [PATCH] Implement non-event part of singleplayer for #7 --- Cargo.lock | 26 + Cargo.toml | 3 +- assets/robocraft/servenvmulti.config | 6 +- rc_services_room/src/data/game_mode.rs | 41 + rc_services_room/src/data/mod.rs | 3 + .../src/data/score_multipliers.rs | 109 +++ .../src/operations/all_customisations_info.rs | 120 ++- .../src/operations/game_mode_config.rs | 15 + rc_services_room/src/operations/mod.rs | 6 + .../src/operations/player_robot_rank.rs | 23 + .../operations/score_multipliers_config.rs | 15 + rc_services_room/src/persist/combat.rs | 69 ++ .../src/persist/config/cubes_json.rs | 5 + rc_services_room/src/persist/config/traits.rs | 1 + .../src/persist/user/account_json.rs | 3 + rc_services_room/src/persist/user/traits.rs | 3 + rc_singleplayer/Cargo.toml | 13 + rc_singleplayer/build_arm64.sh | 3 + rc_singleplayer/run_debug.sh | 3 + rc_singleplayer/src/cli.rs | 31 + rc_singleplayer/src/main.rs | 263 ++++++ rc_singleplayer/src/state.rs | 17 + rc_singleplayer_room/Cargo.toml | 13 + rc_singleplayer_room/build_arm64.sh | 3 + rc_singleplayer_room/run_debug.sh | 3 + rc_singleplayer_room/src/cli.rs | 23 + rc_singleplayer_room/src/data/mod.rs | 23 + rc_singleplayer_room/src/data/player_data.rs | 77 ++ rc_singleplayer_room/src/main.rs | 256 ++++++ rc_singleplayer_room/src/operations/eac.rs | 23 + .../src/operations/load_ai_robots.rs | 855 ++++++++++++++++++ rc_singleplayer_room/src/operations/mod.rs | 13 + .../src/operations/more_auth.rs | 42 + rc_singleplayer_room/src/state.rs | 27 + rc_social_room/src/operations/platoon_data.rs | 8 +- 35 files changed, 2127 insertions(+), 17 deletions(-) create mode 100644 rc_services_room/src/data/game_mode.rs create mode 100644 rc_services_room/src/data/score_multipliers.rs create mode 100644 rc_services_room/src/operations/game_mode_config.rs create mode 100644 rc_services_room/src/operations/player_robot_rank.rs create mode 100644 rc_services_room/src/operations/score_multipliers_config.rs create mode 100644 rc_singleplayer/Cargo.toml create mode 100755 rc_singleplayer/build_arm64.sh create mode 100755 rc_singleplayer/run_debug.sh create mode 100644 rc_singleplayer/src/cli.rs create mode 100644 rc_singleplayer/src/main.rs create mode 100644 rc_singleplayer/src/state.rs create mode 100644 rc_singleplayer_room/Cargo.toml create mode 100755 rc_singleplayer_room/build_arm64.sh create mode 100755 rc_singleplayer_room/run_debug.sh create mode 100644 rc_singleplayer_room/src/cli.rs create mode 100644 rc_singleplayer_room/src/data/mod.rs create mode 100644 rc_singleplayer_room/src/data/player_data.rs create mode 100644 rc_singleplayer_room/src/main.rs create mode 100644 rc_singleplayer_room/src/operations/eac.rs create mode 100644 rc_singleplayer_room/src/operations/load_ai_robots.rs create mode 100644 rc_singleplayer_room/src/operations/mod.rs create mode 100644 rc_singleplayer_room/src/operations/more_auth.rs create mode 100644 rc_singleplayer_room/src/state.rs diff --git a/Cargo.lock b/Cargo.lock index c7ae93b..d86b22d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1787,6 +1787,32 @@ dependencies = [ "tokio", ] +[[package]] +name = "rc_singleplayer" +version = "0.1.0" +dependencies = [ + "clap", + "env_logger", + "log", + "polariton", + "polariton_auth", + "polariton_server", + "tokio", +] + +[[package]] +name = "rc_singleplayer_room" +version = "0.1.0" +dependencies = [ + "clap", + "env_logger", + "log", + "polariton", + "polariton_auth", + "polariton_server", + "tokio", +] + [[package]] name = "rc_social" version = "0.1.0" diff --git a/Cargo.toml b/Cargo.toml index bfd512b..6260bfb 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -10,7 +10,8 @@ members = [ "rc_services", "rc_services_room", "rc_static_data", "rc_microtransactions", "rc_social", "rc_social_room", - "rc_chat", "rc_chat_room" + "rc_chat", "rc_chat_room", + "rc_singleplayer", "rc_singleplayer_room", ] [workspace.dependencies] diff --git a/assets/robocraft/servenvmulti.config b/assets/robocraft/servenvmulti.config index 008ad64..a60934c 100644 --- a/assets/robocraft/servenvmulti.config +++ b/assets/robocraft/servenvmulti.config @@ -6,7 +6,11 @@ 127.0.0.1:4532 127.0.0.1:4533 127.0.0.1:4534 - 127.0.0.1:4535 + 127.0.0.1:4535 + 127.0.0.1:4536 + 127.0.0.1:4537 + 127.0.0.1:4538 + 127.0.0.1:4539 http://127.0.0.1:8001/ http://127.0.0.1:8010/live/data.json diff --git a/rc_services_room/src/data/game_mode.rs b/rc_services_room/src/data/game_mode.rs new file mode 100644 index 0000000..55ed80c --- /dev/null +++ b/rc_services_room/src/data/game_mode.rs @@ -0,0 +1,41 @@ +use polariton::{operation::Typed, serdes::TypePrefix}; + +pub struct GameModeConfig { + pub respawn_heal_duration: f32, + pub respawn_full_heal_duration: f32, + pub kill_limit: i32, + pub game_time_minutes: i32, +} + +impl GameModeConfig { + pub fn as_transmissible(&self) -> Typed { + Typed::HashMap(vec![ + (Typed::Str("respawnHealDuration".into()), Typed::Float(self.respawn_heal_duration)), + (Typed::Str("respawnFullHealDuration".into()), Typed::Float(self.respawn_full_heal_duration)), + (Typed::Str("killLimit".into()), Typed::Int(self.kill_limit)), + (Typed::Str("gameTimeMinutes".into()), Typed::Int(self.game_time_minutes)), + ].into()) + } +} + +pub struct GameModeConfigs { + pub battle_arena: GameModeConfig, + pub elimination: GameModeConfig, + pub the_pit: GameModeConfig, + pub team_deathmatch: GameModeConfig, +} + +impl GameModeConfigs { + pub fn as_transmissible(&self) -> Typed { + Typed::Dict(polariton::operation::Dict { + key_ty: TypePrefix::Str, + val_ty: TypePrefix::HashMap, + items: vec![ + (Typed::Str("BattleArena".into()), self.battle_arena.as_transmissible()), + (Typed::Str("Elimination".into()), self.elimination.as_transmissible()), + (Typed::Str("ThePit".into()), self.the_pit.as_transmissible()), + (Typed::Str("TeamDeathmatch".into()), self.team_deathmatch.as_transmissible()), + ], + }) + } +} diff --git a/rc_services_room/src/data/mod.rs b/rc_services_room/src/data/mod.rs index 3cb0aa2..15e2519 100644 --- a/rc_services_room/src/data/mod.rs +++ b/rc_services_room/src/data/mod.rs @@ -25,8 +25,11 @@ pub mod auto_regen; pub mod voting; pub mod lobby; pub mod error_codes; +pub mod game_mode; +pub mod score_multipliers; pub(self) fn encode_7_bit_i32(mut src: i32) -> Vec { + if src == 0 { return vec![0] } let mut out = Vec::with_capacity(5); while src != 0 { let last_7 = (src & 0x7F) as u8; diff --git a/rc_services_room/src/data/score_multipliers.rs b/rc_services_room/src/data/score_multipliers.rs new file mode 100644 index 0000000..cc4d4ba --- /dev/null +++ b/rc_services_room/src/data/score_multipliers.rs @@ -0,0 +1,109 @@ +use polariton::operation::Typed; + +pub struct ScoreMultipliersData { + pub max_cpu: f32, + pub stat_multipliers: std::collections::HashMap, + pub completed_battle_base_multiplier: f32, + pub completed_battle_bonus_multiplier: f32, + pub delta_scaler: f32, + pub defeat_score: u32, + pub victory_score: u32, + pub max_score_ratio: f32, +} + +impl ScoreMultipliersData { + fn dump(&self, writer: &mut dyn std::io::Write) -> std::io::Result { + writer.write_all(&self.max_cpu.to_le_bytes())?; + for (key, val) in self.stat_multipliers.iter() { + writer.write_all(&(*key as u32).to_le_bytes())?; + writer.write_all(&val.base.to_le_bytes())?; + writer.write_all(&val.bonus.to_le_bytes())?; + } + writer.write_all(&self.completed_battle_base_multiplier.to_le_bytes())?; + writer.write_all(&self.completed_battle_bonus_multiplier.to_le_bytes())?; + writer.write_all(&self.delta_scaler.to_le_bytes())?; + writer.write_all(&self.defeat_score.to_le_bytes())?; + writer.write_all(&self.victory_score.to_le_bytes())?; + writer.write_all(&self.max_score_ratio.to_le_bytes())?; + Ok(28 + (12 * self.stat_multipliers.len())) + } + + pub fn as_transmissible(&self) -> Typed { + let mut buf = Vec::new(); + self.dump(&mut std::io::Cursor::new(&mut buf)).unwrap(); + Typed::Bytes(buf.into()) + } +} + +pub struct ScoreMultiplier { + pub base: f32, + pub bonus: f32, +} + +impl std::default::Default for ScoreMultiplier { + fn default() -> Self { + Self { + base: 0.5, + bonus: 0.9 + } + } +} + +#[allow(dead_code)] +#[repr(u8)] +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub enum InGameStat { + None = 0, + DestroyedCubes = 1, + DestroyedCubesInProtection = 2, + DestroyedCubesDefendingTheBase = 3, + Kill = 4, + KillAssist = 5, + HealCubes = 6, + HealAssist = 7, + DestroyedProtoniumCubes = 8, + BaseCaptureClassicMode = 9, + RobotDestroyed = 10, + Score = 11, + HealthPercentageBonusClassicMode = 12, + Points = 13, + CurrentKillStreak = 14, + BestKillStreak = 15, + CapturePointBattleArenaMode = 16, + EqualiserDestroyedBattleArenaMode = 17, + BattleArenaObjectives = 18, +} + +impl std::default::Default for ScoreMultipliersData { + fn default() -> Self { + Self { + max_cpu: 1000.0, + stat_multipliers: vec![ + (InGameStat::DestroyedCubes, ScoreMultiplier::default()), + (InGameStat::DestroyedCubesInProtection, ScoreMultiplier::default()), + (InGameStat::DestroyedCubesDefendingTheBase, ScoreMultiplier::default()), + (InGameStat::Kill, ScoreMultiplier::default()), + (InGameStat::KillAssist, ScoreMultiplier::default()), + (InGameStat::HealCubes, ScoreMultiplier::default()), + (InGameStat::HealAssist, ScoreMultiplier::default()), + (InGameStat::DestroyedProtoniumCubes, ScoreMultiplier::default()), + (InGameStat::BaseCaptureClassicMode, ScoreMultiplier::default()), + (InGameStat::RobotDestroyed, ScoreMultiplier::default()), + (InGameStat::Score, ScoreMultiplier::default()), + (InGameStat::HealthPercentageBonusClassicMode, ScoreMultiplier::default()), + (InGameStat::Points, ScoreMultiplier::default()), + (InGameStat::CurrentKillStreak, ScoreMultiplier::default()), + (InGameStat::BestKillStreak, ScoreMultiplier::default()), + (InGameStat::CapturePointBattleArenaMode, ScoreMultiplier::default()), + (InGameStat::EqualiserDestroyedBattleArenaMode, ScoreMultiplier::default()), + (InGameStat::BattleArenaObjectives, ScoreMultiplier::default()), + ].into_iter().collect(), + completed_battle_base_multiplier: 1.0, + completed_battle_bonus_multiplier: 1.2, + delta_scaler: 0.5, + defeat_score: 500, + victory_score: 2_000, + max_score_ratio: 2.0, + } + } +} diff --git a/rc_services_room/src/operations/all_customisations_info.rs b/rc_services_room/src/operations/all_customisations_info.rs index d3259ad..ed02faf 100644 --- a/rc_services_room/src/operations/all_customisations_info.rs +++ b/rc_services_room/src/operations/all_customisations_info.rs @@ -20,7 +20,7 @@ pub(super) fn all_customisations_provider() -> SimpleFunc<216, crate::UserTy, im items: vec![ CustomisationData { id: "RC_MothershipSkin_Neptune_01".to_string(), - localised_name: "Neptune 01".to_string(), + localised_name: "strNeptune".to_string(), skin_scene_name: "RC_MothershipSkin_Neptune_01".to_string(), simulation_prefab: "TODO_sim_prefab".to_string(), preview_image_name: "TODO_preview_img".to_string(), @@ -31,12 +31,61 @@ pub(super) fn all_customisations_provider() -> SimpleFunc<216, crate::UserTy, im params.insert(SPAWNS_KEY, Typed::Arr(Arr { ty: TypePrefix::HashMap, // hashtable items: vec![ + // TODO set these up with the correct values (IDs are correct) CustomisationData { - id: "spawn0".to_string(), - localised_name: "Default".to_string(), - skin_scene_name: "TODO_skin".to_string(), - simulation_prefab: "TODO_sim_prefab".to_string(), - preview_image_name: "TODO_preview_img".to_string(), + id: "Spawn".to_string(), + localised_name: "strSpawnFXWarpIn".to_string(), + skin_scene_name: "Splash_Loading_Screen".to_string(), + simulation_prefab: "Respawn_WarpIn".to_string(), + preview_image_name: "RC_Splash_Screen_01_Japanese".to_string(), + is_default: true, + }.as_transmissible(), + CustomisationData { + id: "Spawn_BlackHole".to_string(), + localised_name: "strSpawnFXWarpIn".to_string(), + skin_scene_name: "Splash_Loading_Screen".to_string(), + simulation_prefab: "Respawn_WarpIn".to_string(), + preview_image_name: "RC_Splash_Screen_01_Japanese".to_string(), + is_default: true, + }.as_transmissible(), + CustomisationData { + id: "Spawn_Lander".to_string(), + localised_name: "strSpawnFXWarpIn".to_string(), + skin_scene_name: "Splash_Loading_Screen".to_string(), + simulation_prefab: "Respawn_WarpIn".to_string(), + preview_image_name: "RC_Splash_Screen_01_Japanese".to_string(), + is_default: true, + }.as_transmissible(), + CustomisationData { + id: "Spawn_Lootcrate".to_string(), + localised_name: "strSpawnFXWarpIn".to_string(), + skin_scene_name: "Splash_Loading_Screen".to_string(), + simulation_prefab: "Respawn_WarpIn".to_string(), + preview_image_name: "RC_Splash_Screen_01_Japanese".to_string(), + is_default: true, + }.as_transmissible(), + CustomisationData { + id: "Spawn_Warp".to_string(), + localised_name: "strSpawnFXWarpIn".to_string(), + skin_scene_name: "Splash_Loading_Screen".to_string(), + simulation_prefab: "Respawn_WarpIn".to_string(), + preview_image_name: "RC_Splash_Screen_01_Japanese".to_string(), + is_default: true, + }.as_transmissible(), + CustomisationData { + id: "Spawn_Present".to_string(), + localised_name: "strSpawnFXWarpIn".to_string(), + skin_scene_name: "Splash_Loading_Screen".to_string(), + simulation_prefab: "Respawn_WarpIn".to_string(), + preview_image_name: "RC_Splash_Screen_01_Japanese".to_string(), + is_default: true, + }.as_transmissible(), + CustomisationData { + id: "Spawn_EasterEgg".to_string(), + localised_name: "strSpawnFXWarpIn".to_string(), + skin_scene_name: "Splash_Loading_Screen".to_string(), + simulation_prefab: "Respawn_WarpIn".to_string(), + preview_image_name: "RC_Splash_Screen_01_Japanese".to_string(), is_default: true, }.as_transmissible(), ], @@ -44,12 +93,61 @@ pub(super) fn all_customisations_provider() -> SimpleFunc<216, crate::UserTy, im params.insert(DEATHS_KEY, Typed::Arr(Arr { ty: TypePrefix::HashMap, // hashtable items: vec![ + // TODO set these up with the correct values (IDs are correct) CustomisationData { - id: "death0".to_string(), - localised_name: "Default".to_string(), - skin_scene_name: "TODO_skin".to_string(), - simulation_prefab: "TODO_sim_prefab".to_string(), - preview_image_name: "TODO_preview_img".to_string(), + id: "Explosion".to_string(), + localised_name: "strDeathFXEmergencyWarp".to_string(), + skin_scene_name: "Splash_Loading_Screen".to_string(), + simulation_prefab: "Death_WarpOut".to_string(), + preview_image_name: "RC_Splash_Screen_01_Japanese".to_string(), + is_default: true, + }.as_transmissible(), + CustomisationData { + id: "Explosion_Toon".to_string(), + localised_name: "strDeathFXEmergencyWarp".to_string(), + skin_scene_name: "Splash_Loading_Screen".to_string(), + simulation_prefab: "Death_WarpOut".to_string(), + preview_image_name: "RC_Splash_Screen_01_Japanese".to_string(), + is_default: true, + }.as_transmissible(), + CustomisationData { + id: "Explosion_Feathers_Rainbow".to_string(), + localised_name: "strDeathFXEmergencyWarp".to_string(), + skin_scene_name: "Splash_Loading_Screen".to_string(), + simulation_prefab: "Death_WarpOut".to_string(), + preview_image_name: "RC_Splash_Screen_01_Japanese".to_string(), + is_default: true, + }.as_transmissible(), + CustomisationData { + id: "Explosion_Nuclear".to_string(), + localised_name: "strDeathFXEmergencyWarp".to_string(), + skin_scene_name: "Splash_Loading_Screen".to_string(), + simulation_prefab: "Death_WarpOut".to_string(), + preview_image_name: "RC_Splash_Screen_01_Japanese".to_string(), + is_default: true, + }.as_transmissible(), + CustomisationData { + id: "Explosion_Warp".to_string(), + localised_name: "strDeathFXEmergencyWarp".to_string(), + skin_scene_name: "Splash_Loading_Screen".to_string(), + simulation_prefab: "Death_WarpOut".to_string(), + preview_image_name: "RC_Splash_Screen_01_Japanese".to_string(), + is_default: true, + }.as_transmissible(), + CustomisationData { + id: "Explosion_BlackHole".to_string(), + localised_name: "strDeathFXEmergencyWarp".to_string(), + skin_scene_name: "Splash_Loading_Screen".to_string(), + simulation_prefab: "Death_WarpOut".to_string(), + preview_image_name: "RC_Splash_Screen_01_Japanese".to_string(), + is_default: true, + }.as_transmissible(), + CustomisationData { + id: "Explosion_Firework".to_string(), + localised_name: "strDeathFXEmergencyWarp".to_string(), + skin_scene_name: "Splash_Loading_Screen".to_string(), + simulation_prefab: "Death_WarpOut".to_string(), + preview_image_name: "RC_Splash_Screen_01_Japanese".to_string(), is_default: true, }.as_transmissible(), ], diff --git a/rc_services_room/src/operations/game_mode_config.rs b/rc_services_room/src/operations/game_mode_config.rs new file mode 100644 index 0000000..37e405c --- /dev/null +++ b/rc_services_room/src/operations/game_mode_config.rs @@ -0,0 +1,15 @@ +use polariton_server::operations::SimpleFunc; +use polariton::operation::ParameterTable; + +use crate::persist::config::ConfigProvider; + +const PARAM_KEY: u8 = 1; + +pub(super) fn game_mode_config_provider(conf: &crate::persist::config::ConfigImpl) -> SimpleFunc<113, crate::UserTy, impl (Fn(ParameterTable, &crate::UserTy) -> Result) + Sync + Sync> { + let game_config = conf.game_mode_config(); + SimpleFunc::new(move |params, _| { + let mut params = params.to_dict(); + params.insert(PARAM_KEY, game_config.clone()); + Ok(params.into()) + }) +} diff --git a/rc_services_room/src/operations/mod.rs b/rc_services_room/src/operations/mod.rs index d2cd65c..94f5be0 100644 --- a/rc_services_room/src/operations/mod.rs +++ b/rc_services_room/src/operations/mod.rs @@ -76,6 +76,9 @@ mod regen_config; mod pageantry; mod signup_time; mod validate_machine; +mod game_mode_config; +mod score_multipliers_config; +mod player_robot_rank; use polariton_server::operations::OperationsHandler; @@ -170,4 +173,7 @@ pub fn handler(init_ctx: &crate::InitConfig) -> OperationsHandler .without_state(pageantry::after_battle_vote_thresholds_provider(&init_ctx.cubes)) .without_state(signup_time::user_signup_date_provider()) .without_state(validate_machine::validate_robot_provider()) + .without_state(game_mode_config::game_mode_config_provider(&init_ctx.cubes)) + .without_state(score_multipliers_config::tdm_ai_score_config_provider()) + .without_state(player_robot_rank::player_robot_rank_provider()) } diff --git a/rc_services_room/src/operations/player_robot_rank.rs b/rc_services_room/src/operations/player_robot_rank.rs new file mode 100644 index 0000000..a7f48e0 --- /dev/null +++ b/rc_services_room/src/operations/player_robot_rank.rs @@ -0,0 +1,23 @@ +use polariton_server::operations::SimpleFunc; +use polariton::operation::{ParameterTable, Typed}; + +const USERNAME_PARAM_KEY: u8 = 30; // in; str +const RANK_PARAM_KEY: u8 = 84; // out; int +const CPU_PARAM_KEY: u8 = 177; // out; int +const COSMETIC_CPU_PARAM_KEY: u8 = 176; // out; int + +pub(super) fn player_robot_rank_provider() -> SimpleFunc<79, crate::UserTy, impl (Fn(ParameterTable, &crate::UserTy) -> Result) + Sync + Sync> { + SimpleFunc::new(|params, user: &crate::UserTy| { + let lock = user.read().unwrap(); + let user = lock.user()?; + let mut params = params.to_dict(); + if let Some(Typed::Str(username)) = params.get(&USERNAME_PARAM_KEY) { + log::debug!("Get robot rank for user {}", username.string); + } + let robot = user.slot_by_id(user.selected_garage_slot() as i32)?; + params.insert(RANK_PARAM_KEY, robot.robot_rank); + params.insert(CPU_PARAM_KEY, robot.cpu); + params.insert(COSMETIC_CPU_PARAM_KEY, robot.cosmetic_cpu); + Ok(params.into()) + }) +} diff --git a/rc_services_room/src/operations/score_multipliers_config.rs b/rc_services_room/src/operations/score_multipliers_config.rs new file mode 100644 index 0000000..0582e46 --- /dev/null +++ b/rc_services_room/src/operations/score_multipliers_config.rs @@ -0,0 +1,15 @@ +use polariton_server::operations::SimpleFunc; +use polariton::operation::ParameterTable; + +use crate::data::score_multipliers::*; + +const PARAM_KEY: u8 = 137; + +pub(super) fn tdm_ai_score_config_provider(/*conf: &crate::persist::config::ConfigImpl*/) -> SimpleFunc<117, crate::UserTy, impl (Fn(ParameterTable, &crate::UserTy) -> Result) + Sync + Sync> { + //let game_config = conf.game_mode_config(); + SimpleFunc::new(move |params, _| { + let mut params = params.to_dict(); + params.insert(PARAM_KEY, ScoreMultipliersData::default().as_transmissible()); + Ok(params.into()) + }) +} diff --git a/rc_services_room/src/persist/combat.rs b/rc_services_room/src/persist/combat.rs index de2cd38..3af2115 100644 --- a/rc_services_room/src/persist/combat.rs +++ b/rc_services_room/src/persist/combat.rs @@ -6,6 +6,8 @@ use serde::{Serialize, Deserialize}; pub struct BattleConfig { pub regen: AutoRegenHealth, pub votes: HashMap>, + #[serde(default = "default_game_modes")] + pub games: GameModes, } #[derive(Serialize, Deserialize, Clone, Debug)] @@ -60,3 +62,70 @@ impl std::convert::Into for Vote { } } } + +#[derive(Serialize, Deserialize, Clone, Debug, Copy)] +pub struct GameMode { + pub respawn_heal_duration: f32, + pub respawn_full_heal_duration: f32, + pub kill_limit: i32, + pub game_time_m: i32, +} + +impl std::convert::Into for GameMode { + fn into(self) -> crate::data::game_mode::GameModeConfig { + crate::data::game_mode::GameModeConfig { + respawn_heal_duration: self.respawn_heal_duration, + respawn_full_heal_duration: self.respawn_full_heal_duration, + kill_limit: self.kill_limit, + game_time_minutes: self.game_time_m, + } + } +} + +#[derive(Serialize, Deserialize, Clone, Debug, Copy)] +pub struct GameModes { + pub battle_arena: GameMode, + pub elimination: GameMode, + pub pit: GameMode, + pub team_deathmatch: GameMode, +} + +impl std::convert::Into for GameModes { + fn into(self) -> crate::data::game_mode::GameModeConfigs { + crate::data::game_mode::GameModeConfigs { + battle_arena: self.battle_arena.into(), + elimination: self.elimination.into(), + the_pit: self.pit.into(), + team_deathmatch: self.team_deathmatch.into(), + } + } +} + +fn default_game_modes() -> GameModes { + GameModes { + battle_arena: GameMode { + respawn_heal_duration: 10.0, + respawn_full_heal_duration: 10.0, + kill_limit: 0, + game_time_m: 20, + }, + elimination: GameMode { + respawn_heal_duration: 10.0, + respawn_full_heal_duration: 10.0, + kill_limit: 10, + game_time_m: 10, + }, + pit: GameMode { + respawn_heal_duration: 20.0, + respawn_full_heal_duration: 20.0, + kill_limit: 15, + game_time_m: 15, + }, + team_deathmatch: GameMode { + respawn_heal_duration: 10.0, + respawn_full_heal_duration: 10.0, + kill_limit: 10, + game_time_m: 10, + }, + } +} diff --git a/rc_services_room/src/persist/config/cubes_json.rs b/rc_services_room/src/persist/config/cubes_json.rs index 25326a8..69958e7 100644 --- a/rc_services_room/src/persist/config/cubes_json.rs +++ b/rc_services_room/src/persist/config/cubes_json.rs @@ -177,4 +177,9 @@ impl super::ConfigProvider for CubeConfig { items: vote_data, }) } + + fn game_mode_config(&self) -> Typed { + let game_mode_data: crate::data::game_mode::GameModeConfigs = self.battle.games.into(); + game_mode_data.as_transmissible() + } } diff --git a/rc_services_room/src/persist/config/traits.rs b/rc_services_room/src/persist/config/traits.rs index 729799f..110c2b6 100644 --- a/rc_services_room/src/persist/config/traits.rs +++ b/rc_services_room/src/persist/config/traits.rs @@ -9,4 +9,5 @@ pub trait ConfigProvider { fn ids(&self) -> Vec; fn regen_config(&self) -> Typed; fn after_battle_vote_config(&self) -> Typed; + fn game_mode_config(&self) -> Typed; } diff --git a/rc_services_room/src/persist/user/account_json.rs b/rc_services_room/src/persist/user/account_json.rs index 20dbc32..f8d3be0 100644 --- a/rc_services_room/src/persist/user/account_json.rs +++ b/rc_services_room/src/persist/user/account_json.rs @@ -150,6 +150,9 @@ impl super::User for UserData { control_type: polariton::operation::Typed::Int(control_ty as _), control_options: control_options.as_transmissible(), mastery_level: polariton::operation::Typed::Int(0), // TODO + robot_rank: polariton::operation::Typed::Int(slot.total_robot_ranking as _), + cpu: polariton::operation::Typed::Int(slot.total_robot_cpu as _), + cosmetic_cpu: polariton::operation::Typed::Int(slot.total_cosmetic_cpu as _), }) }, Err(e) => { diff --git a/rc_services_room/src/persist/user/traits.rs b/rc_services_room/src/persist/user/traits.rs index 0ac71b3..1c938a6 100644 --- a/rc_services_room/src/persist/user/traits.rs +++ b/rc_services_room/src/persist/user/traits.rs @@ -38,6 +38,9 @@ pub struct UserSlotData { pub control_type: polariton::operation::Typed, pub control_options: polariton::operation::Typed, pub mastery_level: polariton::operation::Typed, + pub robot_rank: polariton::operation::Typed, + pub cpu: polariton::operation::Typed, + pub cosmetic_cpu: polariton::operation::Typed, } pub struct VehicleData { diff --git a/rc_singleplayer/Cargo.toml b/rc_singleplayer/Cargo.toml new file mode 100644 index 0000000..7e41ab0 --- /dev/null +++ b/rc_singleplayer/Cargo.toml @@ -0,0 +1,13 @@ +[package] +name = "rc_singleplayer" +version = "0.1.0" +edition = "2021" + +[dependencies] +log.workspace = true +env_logger.workspace = true +tokio = { version = "1.43", features = [ "net", "macros", "rt-multi-thread", "io-util" ] } +clap.workspace = true +polariton.workspace = true +polariton_auth = { version = "*", path = "../polariton_auth" } +polariton_server.workspace = true diff --git a/rc_singleplayer/build_arm64.sh b/rc_singleplayer/build_arm64.sh new file mode 100755 index 0000000..7010ff6 --- /dev/null +++ b/rc_singleplayer/build_arm64.sh @@ -0,0 +1,3 @@ +#!/bin/bash + +cargo build --release --target aarch64-unknown-linux-musl diff --git a/rc_singleplayer/run_debug.sh b/rc_singleplayer/run_debug.sh new file mode 100755 index 0000000..cc15e3f --- /dev/null +++ b/rc_singleplayer/run_debug.sh @@ -0,0 +1,3 @@ +#!/bin/bash + +RUST_BACKTRACE=1 RUST_LOG=debug cargo run -- -1 diff --git a/rc_singleplayer/src/cli.rs b/rc_singleplayer/src/cli.rs new file mode 100644 index 0000000..36edfb4 --- /dev/null +++ b/rc_singleplayer/src/cli.rs @@ -0,0 +1,31 @@ +use clap::Parser; + +#[derive(Parser, Debug)] +#[command(version, about, long_about = None)] +pub struct CliArgs { + /// TCP port on which to accept connections + #[arg(short, long, default_value_t = 4538)] + pub port: u16, + + /// IP Address on which to accept connections + #[arg(long, default_value_t = {"127.0.0.1".to_string()})] + pub ip: String, + + /// Domain and port of the game server to send new connections + #[arg(long, default_value_t = {"127.0.0.1:4539".to_string()})] + pub redirect: String, + + /// Name of game server to send new connections + #[arg(long, default_value_t = {"ngram_is_ngnius".to_string()})] + pub room_name: String, + + /// Handle one connection and then exit + #[arg(short = '1', long)] + pub once: bool, +} + +impl CliArgs { + pub fn get() -> Self { + Self::parse() + } +} diff --git a/rc_singleplayer/src/main.rs b/rc_singleplayer/src/main.rs new file mode 100644 index 0000000..ef6bfea --- /dev/null +++ b/rc_singleplayer/src/main.rs @@ -0,0 +1,263 @@ +mod cli; +mod state; + +use polariton_auth::Handshake; +use tokio::net; + +use polariton::packet::{Data, Message, Packet, StandardMessage}; +use polariton::operation::{OperationResponse, Typed}; + +#[tokio::main] +async fn main() -> std::io::Result<()> { + env_logger::init(); + let args = cli::CliArgs::get(); + log::debug!("Got cli args {:?}", args); + + let ip_addr: std::net::IpAddr = args.ip.parse().expect("Invalid IP address"); + + // memory leak, but only once (so not a big deal) + let redirect_static = Box::leak(Box::new(args.redirect.clone())); + let room_name_static = Box::leak(Box::new(args.room_name.clone())); + + let listener = net::TcpListener::bind(std::net::SocketAddr::new(ip_addr, args.port)).await?; + + if args.once { + log::warn!("Handling first connection and then exiting"); + let (socket, address) = listener.accept().await?; + process_socket(socket, address, redirect_static, room_name_static).await; + Ok(()) + } else { + loop { + let (socket, address) = listener.accept().await?; + tokio::spawn(process_socket(socket, address, redirect_static, room_name_static)); + } + } +} + +async fn process_socket(mut socket: net::TcpStream, address: std::net::SocketAddr, redirect_url: &str, lobby_name: &str) { + log::debug!("Accepting connection from address {}", address); + + let enc = match do_connect_handshake(&mut socket, lobby_name, redirect_url).await { + Some(x) => x, + None => { + log::error!("Failed to do connect handshake with {}", address); + return; + } + }; + let sock_state = state::State::new(enc); + while let Ok(packet) = polariton_server::utils::receive_packet_async(&mut socket, &sock_state.serdes_ctx()).await { + match packet { + Packet::Ping(ping) => { + polariton_server::utils::handle_ping_async(ping, &mut socket, &sock_state.serdes_ctx()).await.unwrap_or_default(); + }, + Packet::Packet(packet) => log::warn!("Not handling packet {:?}", packet), + } + } + log::debug!("Goodbye connection from address {}", address); +} + +const APP_ID: &str = "SinglePlayerServer"; + +struct AuthImpl; + +const TOKEN_KEY: u8 = 216; // token;refresh_token +//const UNKNOWN_BYTE_KEY: u8 = 217; +const SERVICE_KEY: u8 = 224; +const USERNAME_KEY: u8 = 225; + +//const CCU_KEY: u8 = 245; + +#[derive(Debug)] +enum AuthError { + WrongService { expected: String, actual: String }, + MissingService, + MissingToken, + MissingUsername, +} + +impl AuthError { + fn log_err(&self) { + match self { + Self::WrongService { expected, actual } => log::error!("(auth fail) Got unexpected service {}, expected {}", actual, expected), + Self::MissingService => log::error!("(auth fail) No service name param ({}) received", SERVICE_KEY), + Self::MissingToken => log::error!("(auth fail) No token param ({}) received", TOKEN_KEY), + Self::MissingUsername => log::error!("(auth fail) No username param ({}) received", USERNAME_KEY), + } + } +} + +impl polariton_auth::AuthProvider for AuthImpl { + fn validate(&mut self, params: &std::collections::HashMap) -> Result, AuthError> { + if let Some(Typed::Str(token)) = params.get(&TOKEN_KEY) { + if let Some(Typed::Str(service)) = params.get(&SERVICE_KEY) { + if let Some(Typed::Str(user)) = params.get(&USERNAME_KEY) { + if service.string == APP_ID { + let params_resp = std::collections::HashMap::::new(); + //params_resp.insert(CCU_KEY, Typed::Byte(0)); + log::debug!("Auth success for {} (token: {})", user.string, token.string); + Ok(params_resp) + } else { Err(AuthError::WrongService { expected: APP_ID.to_owned(), actual: service.string.to_owned() }) } + } else { Err(AuthError::MissingUsername) } + } else { Err(AuthError::MissingService) } + } else { Err(AuthError::MissingToken) } + } +} + +async fn do_connect_handshake( + socket: &mut net::TcpStream, + game_server_name: &str, + game_server_url: &str, +) -> Option { + let handshake = Handshake::new(APP_ID); + // connect + log::debug!("(connect) Handling first packet"); + let packet1 = match polariton_server::utils::receive_packet_async(socket, &Default::default()).await { + Ok(x) => x, + Err(e) => { + log::error!("Failed to read connect packet: {}", e); + return None; + } + }; + let (handshake, to_send) = match handshake.connect(&packet1) { + Ok(x) => (x.handshake, x.extra), + Err(e) => { + log::error!("Failed to handle connect handshake: {:?}", e.extra); + return None; + } + }; + match polariton_server::utils::send_packet_async(&to_send, socket, &Default::default()).await { + Ok(_) => {}, + Err(e) => { + log::error!("Failed to send connect ack packet: {}", e); + return None; + } + } + // encrypt + log::debug!("(connect) Handling second packet"); + let mut packet2 = match polariton_server::utils::receive_packet_async(socket, &Default::default()).await { + Ok(x) => x, + Err(e) => { + log::error!("Failed to read (maybe) public key packet: {}", e); + return None; + } + }; + while let Packet::Ping(ping) = packet2 { + polariton_server::utils::handle_ping_async(ping, socket, &Default::default()).await.unwrap_or_default(); + packet2 = match polariton_server::utils::receive_packet_async(socket, &Default::default()).await { + Ok(x) => x, + Err(e) => { + log::error!("Failed to read (maybe) public key packet: {}", e); + return None; + } + }; + } + let (handshake, to_send, crypto) = match handshake.encrypt(&packet2) { + Ok(x) => (x.handshake, x.extra.0, x.extra.1), + Err(e) => { + log::error!("Failed to handle encryption handshake: {:?}", e.extra); + return None; + } + }; + match polariton_server::utils::send_packet_async(&to_send, socket, &Default::default()).await { + Ok(_) => {}, + Err(e) => { + log::error!("Failed to send encryption ack packet: {}", e); + return None; + } + } + // pre-auth + let handshake = handshake.with_auth(AuthImpl); + let op_ctx = polariton::serdes::SerdesContext::default(); + let ctx = polariton::packet::SerdesContext::new(&op_ctx, &crypto); + // authenticate + log::debug!("(connect) Handling third packet"); + let mut packet3 = match polariton_server::utils::receive_packet_async(socket, &ctx).await { + Ok(x) => x, + Err(e) => { + log::error!("Failed to read (maybe) auth packet: {}", e); + return None; + } + }; + while let Packet::Ping(ping) = packet3 { + polariton_server::utils::handle_ping_async(ping, socket, &Default::default()).await.unwrap_or_default(); + packet3 = match polariton_server::utils::receive_packet_async(socket, &ctx).await { + Ok(x) => x, + Err(e) => { + log::error!("Failed to read (maybe) auth packet: {}", e); + return None; + } + }; + } + let to_send = match handshake.authenticate(&packet3, &crypto) { + Ok(x) => x, + Err(h) => match h.extra { + polariton_auth::AuthError::Validation(e) => { + e.log_err(); + return None; + }, + e => { + log::error!("Failed to handle auth handshake: {:?}", e); + return None; + }, + }, + }; + match polariton_server::utils::send_packet_async(&to_send, socket, &ctx).await { + Ok(_) => {}, + Err(e) => { + log::error!("Failed to send auth ack packet: {}", e); + return None; + } + } + + // redirect to lobby + log::debug!("(connect) Handling fourth packet"); + let mut packet_j = match polariton_server::utils::receive_packet_async(socket, &ctx).await { + Ok(x) => x, + Err(e) => { + log::error!("Failed to read (maybe) join packet: {}", e); + return None; + } + }; + while let Packet::Ping(ping) = packet_j { + polariton_server::utils::handle_ping_async(ping, socket, &ctx).await.unwrap_or_default(); + packet_j = match polariton_server::utils::receive_packet_async(socket, &ctx).await { + Ok(x) => x, + Err(e) => { + log::error!("Failed to read (maybe) join packet: {}", e); + return None; + } + }; + } + log::debug!("(connect) Got fourth packet {:?}", packet_j); + if let Packet::Packet(msg) = &packet_j { + if let Message::Standard(st) = &msg.message { + if let Data::OpReq(req) = &st.data { + if req.code == 226 { // join lobby + log::debug!("Max players from lobby join request: {:?}", req.params.to_owned().to_dict().get(&255)); + let mut params = std::collections::HashMap::::new(); + params.insert(230 /* game server address */, Typed::Str(game_server_url.into())); + params.insert(255 /* room name */, Typed::Str(game_server_name.into())); + let resp = Packet::from_message( + Message::Standard( + StandardMessage { flags: 0, + data: Data::OpResp(OperationResponse { + code: req.code, + return_code: 0, + message: Typed::Null, + params: params.into(), + }), + }.encrypt(true)), 0, true, &ctx).unwrap(); + match polariton_server::utils::send_packet_async(&resp, socket, &ctx).await { + Ok(_) => {}, + Err(e) => { + log::error!("Failed to send lobby ack packet: {}", e); + return None; + } + } + } + } + } + } + + Some(crypto) +} diff --git a/rc_singleplayer/src/state.rs b/rc_singleplayer/src/state.rs new file mode 100644 index 0000000..9d6664f --- /dev/null +++ b/rc_singleplayer/src/state.rs @@ -0,0 +1,17 @@ +const SERDES_SERDES_CTX_REF: &'static polariton::serdes::SerdesContext<(), polariton::serdes::NoCustomSerdes> = &polariton::serdes::SerdesContext::default_const(); + +pub struct State { + pub crypto: polariton_auth::CryptoImpl, +} + +impl State { + pub fn new(c: polariton_auth::CryptoImpl) -> Self { + Self { + crypto: c, + } + } + + pub fn serdes_ctx(&self) -> polariton::packet::SerdesContext<'_, (), polariton::serdes::NoCustomSerdes> { + polariton::packet::SerdesContext::new(SERDES_SERDES_CTX_REF, &self.crypto) + } +} diff --git a/rc_singleplayer_room/Cargo.toml b/rc_singleplayer_room/Cargo.toml new file mode 100644 index 0000000..3c2139a --- /dev/null +++ b/rc_singleplayer_room/Cargo.toml @@ -0,0 +1,13 @@ +[package] +name = "rc_singleplayer_room" +version = "0.1.0" +edition = "2021" + +[dependencies] +log.workspace = true +env_logger.workspace = true +tokio = { version = "1.43", features = [ "net", "macros", "rt-multi-thread", "io-util" ] } +clap.workspace = true +polariton.workspace = true +polariton_auth = { version = "*", path = "../polariton_auth" } +polariton_server.workspace = true diff --git a/rc_singleplayer_room/build_arm64.sh b/rc_singleplayer_room/build_arm64.sh new file mode 100755 index 0000000..7010ff6 --- /dev/null +++ b/rc_singleplayer_room/build_arm64.sh @@ -0,0 +1,3 @@ +#!/bin/bash + +cargo build --release --target aarch64-unknown-linux-musl diff --git a/rc_singleplayer_room/run_debug.sh b/rc_singleplayer_room/run_debug.sh new file mode 100755 index 0000000..cc15e3f --- /dev/null +++ b/rc_singleplayer_room/run_debug.sh @@ -0,0 +1,3 @@ +#!/bin/bash + +RUST_BACKTRACE=1 RUST_LOG=debug cargo run -- -1 diff --git a/rc_singleplayer_room/src/cli.rs b/rc_singleplayer_room/src/cli.rs new file mode 100644 index 0000000..1ecd871 --- /dev/null +++ b/rc_singleplayer_room/src/cli.rs @@ -0,0 +1,23 @@ +use clap::Parser; + +#[derive(Parser, Debug)] +#[command(version, about, long_about = None)] +pub struct CliArgs { + /// TCP port on which to accept connections + #[arg(short, long, default_value_t = 4539)] + pub port: u16, + + /// IP Address on which to accept connections + #[arg(long, default_value_t = {"127.0.0.1".to_string()})] + pub ip: String, + + /// Handle one connection and then exit + #[arg(short = '1', long)] + pub once: bool, +} + +impl CliArgs { + pub fn get() -> Self { + Self::parse() + } +} diff --git a/rc_singleplayer_room/src/data/mod.rs b/rc_singleplayer_room/src/data/mod.rs new file mode 100644 index 0000000..159a924 --- /dev/null +++ b/rc_singleplayer_room/src/data/mod.rs @@ -0,0 +1,23 @@ +pub mod player_data; + +pub(self) fn encode_7_bit_i32(mut src: i32) -> Vec { + if src == 0 { return vec![0] } + let mut out = Vec::with_capacity(5); + while src != 0 { + let last_7 = (src & 0x7F) as u8; + src = src >> 7; + if src != 0 { + out.push(last_7 | 0x80); + } else { + out.push(last_7); + } + } + out +} + +pub(self) fn write_str_for_binreader(s: &str, writer: &mut dyn std::io::Write) -> std::io::Result { + let s_bytes = s.as_bytes(); + let mut total_len = writer.write(&encode_7_bit_i32(s_bytes.len() as i32))?; + total_len += writer.write(s_bytes)?; + Ok(total_len) +} diff --git a/rc_singleplayer_room/src/data/player_data.rs b/rc_singleplayer_room/src/data/player_data.rs new file mode 100644 index 0000000..f863eb3 --- /dev/null +++ b/rc_singleplayer_room/src/data/player_data.rs @@ -0,0 +1,77 @@ +use polariton::operation::Typed; + +pub struct PlayerData { + pub name: String, + pub display_name: String, + pub mastery: i32, + pub tier: i32, + pub robot_name: String, + pub robot_map: Vec, + // -- unused i32 here -- + pub team: i32, + pub has_premium: bool, + pub robot_uuid: String, + pub cpu: i32, + pub weapon_order: Vec, + pub colour_map: Vec, + pub is_ai: bool, + pub spawn_effect: String, + pub death_effect: String, + pub player_rank: i32, + pub weapon_rank: std::collections::HashMap, +} + +impl PlayerData { + fn dump(&self, writer: &mut dyn std::io::Write) -> std::io::Result { + let mut total_len = super::write_str_for_binreader(&self.name, writer)?; + total_len += super::write_str_for_binreader(&self.display_name, writer)?; + writer.write_all(&self.mastery.to_le_bytes())?; + writer.write_all(&self.tier.to_le_bytes())?; + total_len += super::write_str_for_binreader(&self.robot_name, writer)?; + writer.write_all(&(self.robot_map.len() as i32).to_le_bytes())?; + writer.write_all(&self.robot_map)?; + writer.write_all(&[0xDE, 0xAD, 0xBE, 0xEF])?; + writer.write_all(&self.team.to_le_bytes())?; + writer.write_all(&[self.has_premium as u8])?; + total_len += super::write_str_for_binreader(&self.robot_uuid, writer)?; + writer.write_all(&self.cpu.to_le_bytes())?; + writer.write_all(&(self.weapon_order.len() as i32).to_le_bytes())?; + for weapon_key in self.weapon_order.iter() { + writer.write_all(&weapon_key.to_le_bytes())?; + } + writer.write_all(&(self.colour_map.len() as i32).to_le_bytes())?; + writer.write_all(&self.colour_map)?; + writer.write_all(&[self.is_ai as u8])?; + total_len += super::write_str_for_binreader(&self.spawn_effect, writer)?; + total_len += super::write_str_for_binreader(&self.death_effect, writer)?; + writer.write_all(&self.player_rank.to_le_bytes())?; + writer.write_all(&(self.weapon_rank.len() as i32).to_le_bytes())?; + for (key, val) in self.weapon_rank.iter() { + writer.write_all(&key.to_le_bytes())?; + writer.write_all(&val.to_le_bytes())?; + } + Ok(42 + self.robot_map.len() + (self.weapon_order.len() * 4) + self.colour_map.len() + (self.weapon_rank.len() * 8) + total_len) + } +} + +pub struct PlayerDatas { + pub players: Vec, +} + +impl PlayerDatas { + fn dump(&self, writer: &mut dyn std::io::Write) -> std::io::Result { + writer.write_all(&(self.players.len() as i32).to_le_bytes())?; + let mut total_len = 4; + for data in self.players.iter() { + total_len += data.dump(writer)?; + } + Ok(total_len) + } + + pub fn as_transmissible(&self) -> Typed { + let mut buf = Vec::new(); + let write_size = self.dump(&mut std::io::Cursor::new(&mut buf)).unwrap(); + log::debug!("PlayerDatas serialized to {} bytes: {:?}", write_size, buf); + Typed::Bytes(buf.into()) + } +} diff --git a/rc_singleplayer_room/src/main.rs b/rc_singleplayer_room/src/main.rs new file mode 100644 index 0000000..9a0215d --- /dev/null +++ b/rc_singleplayer_room/src/main.rs @@ -0,0 +1,256 @@ +mod cli; +mod state; + +mod data; +mod operations; + +use polariton_auth::Handshake; +use tokio::net; + +use polariton::packet::{Data, Message, Packet, StandardMessage}; +use polariton::operation::{OperationResponse, Typed}; + +pub type UserTy = std::sync::RwLock; + +#[tokio::main] +async fn main() -> std::io::Result<()> { + env_logger::init(); + let args = cli::CliArgs::get(); + log::debug!("Got cli args {:?}", args); + + let server = std::sync::Arc::new(polariton_server::Server::new(operations::handler())); + + 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?; + + if args.once { + log::warn!("Handling first connection and then exiting"); + let (socket, address) = listener.accept().await?; + process_socket(socket, address, server).await; + Ok(()) + } else { + loop { + let (socket, address) = listener.accept().await?; + tokio::spawn(process_socket(socket, address, server.clone())); + } + } +} + +async fn process_socket(mut socket: net::TcpStream, address: std::net::SocketAddr, server: std::sync::Arc>) { + log::debug!("Accepting connection from address {}", address); + let enc = match do_connect_handshake(&mut socket).await { + Some(x) => x, + None => { + log::error!("Failed to do connect handshake with {}", address); + return; + } + }; + let user_state = state::UserState::new(); + server.handle_async(socket, user_state, enc, Default::default()).await; + log::debug!("Goodbye connection from address {}", address); +} + +const APP_ID: &str = "SinglePlayerServer"; + +struct AuthImpl; + +const TOKEN_KEY: u8 = 216; // token;refresh_token +//const UNKNOWN_BYTE_KEY: u8 = 217; +const SERVICE_KEY: u8 = 224; +const USERNAME_KEY: u8 = 225; + +//const CCU_KEY: u8 = 245; + +#[derive(Debug)] +enum AuthError { + WrongService { expected: String, actual: String }, + MissingService, + MissingToken, + MissingUsername, +} + +impl AuthError { + fn log_err(&self) { + match self { + Self::WrongService { expected, actual } => log::error!("(auth fail) Got unexpected service {}, expected {}", actual, expected), + Self::MissingService => log::error!("(auth fail) No service name param ({}) received", SERVICE_KEY), + Self::MissingToken => log::error!("(auth fail) No token param ({}) received", TOKEN_KEY), + Self::MissingUsername => log::error!("(auth fail) No username param ({}) received", USERNAME_KEY), + } + } +} + +impl polariton_auth::AuthProvider for AuthImpl { + fn validate(&mut self, params: &std::collections::HashMap) -> Result, AuthError> { + if let Some(Typed::Str(token)) = params.get(&TOKEN_KEY) { + if let Some(Typed::Str(service)) = params.get(&SERVICE_KEY) { + if let Some(Typed::Str(user)) = params.get(&USERNAME_KEY) { + if service.string == APP_ID { + let params_resp = std::collections::HashMap::::new(); + //params_resp.insert(CCU_KEY, Typed::Byte(0)); + log::debug!("Auth success for {} (token: {})", user.string, token.string); + Ok(params_resp) + } else { Err(AuthError::WrongService { expected: APP_ID.to_owned(), actual: service.string.to_owned() }) } + } else { Err(AuthError::MissingUsername) } + } else { Err(AuthError::MissingService) } + } else { Err(AuthError::MissingToken) } + } +} + +async fn do_connect_handshake( + socket: &mut net::TcpStream, +) -> Option { + let handshake = Handshake::new(APP_ID); + // connect + log::debug!("(connect) Handling first packet"); + let packet1 = match polariton_server::utils::receive_packet_async(socket, &Default::default()).await { + Ok(x) => x, + Err(e) => { + log::error!("Failed to read connect packet: {}", e); + return None; + } + }; + let (handshake, to_send) = match handshake.connect(&packet1) { + Ok(x) => (x.handshake, x.extra), + Err(e) => { + log::error!("Failed to handle connect handshake: {:?}", e.extra); + return None; + } + }; + match polariton_server::utils::send_packet_async(&to_send, socket, &Default::default()).await { + Ok(_) => {}, + Err(e) => { + log::error!("Failed to send connect ack packet: {}", e); + return None; + } + } + // encrypt + log::debug!("(connect) Handling second packet"); + let mut packet2 = match polariton_server::utils::receive_packet_async(socket, &Default::default()).await { + Ok(x) => x, + Err(e) => { + log::error!("Failed to read (maybe) public key packet: {}", e); + return None; + } + }; + while let Packet::Ping(ping) = packet2 { + polariton_server::utils::handle_ping_async(ping, socket, &Default::default()).await.unwrap_or_default(); + packet2 = match polariton_server::utils::receive_packet_async(socket, &Default::default()).await { + Ok(x) => x, + Err(e) => { + log::error!("Failed to read (maybe) public key packet: {}", e); + return None; + } + }; + } + let (handshake, to_send, crypto) = match handshake.encrypt(&packet2) { + Ok(x) => (x.handshake, x.extra.0, x.extra.1), + Err(e) => { + log::error!("Failed to handle encryption handshake: {:?}", e.extra); + return None; + } + }; + match polariton_server::utils::send_packet_async(&to_send, socket, &Default::default()).await { + Ok(_) => {}, + Err(e) => { + log::error!("Failed to send encryption ack packet: {}", e); + return None; + } + } + // pre-auth + let handshake = handshake.with_auth(AuthImpl); + let op_ctx = polariton::serdes::SerdesContext::default(); + let ctx = polariton::packet::SerdesContext::new(&op_ctx, &crypto); + // authenticate + log::debug!("(connect) Handling third packet"); + let mut packet3 = match polariton_server::utils::receive_packet_async(socket, &ctx).await { + Ok(x) => x, + Err(e) => { + log::error!("Failed to read (maybe) auth packet: {}", e); + return None; + } + }; + while let Packet::Ping(ping) = packet3 { + polariton_server::utils::handle_ping_async(ping, socket, &Default::default()).await.unwrap_or_default(); + packet3 = match polariton_server::utils::receive_packet_async(socket, &ctx).await { + Ok(x) => x, + Err(e) => { + log::error!("Failed to read (maybe) auth packet: {}", e); + return None; + } + }; + } + let to_send = match handshake.authenticate(&packet3, &crypto) { + Ok(x) => x, + Err(h) => match h.extra { + polariton_auth::AuthError::Validation(e) => { + e.log_err(); + return None; + }, + e => { + log::error!("Failed to handle auth handshake: {:?}", e); + return None; + }, + }, + }; + match polariton_server::utils::send_packet_async(&to_send, socket, &ctx).await { + Ok(_) => {}, + Err(e) => { + log::error!("Failed to send auth ack packet: {}", e); + return None; + } + } + + // join lobby + log::debug!("(join lobby) Handling fourth packet"); + let mut packet_j = match polariton_server::utils::receive_packet_async(socket, &ctx).await { + Ok(x) => x, + Err(e) => { + log::error!("Failed to read (maybe) join packet: {}", e); + return None; + } + }; + while let Packet::Ping(ping) = packet_j { + polariton_server::utils::handle_ping_async(ping, socket, &Default::default()).await.unwrap_or_default(); + packet_j = match polariton_server::utils::receive_packet_async(socket, &ctx).await { + Ok(x) => x, + Err(e) => { + log::error!("Failed to read (maybe) join packet: {}", e); + return None; + } + }; + } + if let Packet::Packet(msg) = &packet_j { + if let Message::Standard(st) = &msg.message { + if let Data::OpReq(req) = &st.data { + if req.code == 226 { // join lobby (but for real this time) + let mut params = std::collections::HashMap::::new(); + //params.insert(252 /* actors in game */, Typed::Str(game_server_url.into())); + params.insert(254 /* game server address */, Typed::Int(42)); + params.insert(249 /* actor properties */, Typed::HashMap(Vec::new().into())); + params.insert(248 /* game properties */, Typed::HashMap(Vec::new().into())); + let resp = Packet::from_message( + Message::Standard( + StandardMessage { flags: 0, + data: Data::OpResp(OperationResponse { + code: req.code, + return_code: 0, + message: Typed::Null, + params: params.into(), + }), + }.encrypt(true)), 0, true, &ctx).unwrap(); + match polariton_server::utils::send_packet_async(&resp, socket, &ctx).await { + Ok(_) => {}, + Err(e) => { + log::error!("Failed to send lobby ack packet: {}", e); + return None; + } + } + } + } + } + } + + Some(crypto) +} diff --git a/rc_singleplayer_room/src/operations/eac.rs b/rc_singleplayer_room/src/operations/eac.rs new file mode 100644 index 0000000..fc2366f --- /dev/null +++ b/rc_singleplayer_room/src/operations/eac.rs @@ -0,0 +1,23 @@ +use polariton_server::operations::{Operation, OperationCode}; + +pub struct EacChallengeIgnorer; + +impl Operation for EacChallengeIgnorer { + type State = (); + type User = crate::UserTy; + + fn handle(&self, params: polariton::operation::ParameterTable, _: &mut Self::State, _: &Self::User) -> polariton::operation::OperationResponse { + polariton::operation::OperationResponse { + code: 5, // skip the challenge (hopefully) + return_code: 0, + message: polariton::operation::Typed::Null, + params, + } + } +} + +impl OperationCode for EacChallengeIgnorer { + fn op_code() -> u8 { + 4 + } +} diff --git a/rc_singleplayer_room/src/operations/load_ai_robots.rs b/rc_singleplayer_room/src/operations/load_ai_robots.rs new file mode 100644 index 0000000..88a7b93 --- /dev/null +++ b/rc_singleplayer_room/src/operations/load_ai_robots.rs @@ -0,0 +1,855 @@ +use polariton_server::operations::SimpleFunc; +use polariton::operation::ParameterTable; + +use crate::data::player_data::*; + +const PARAM_KEY: u8 = 8; + +const VALID_ROBOT: &[u8] = &[64, + 0, + 0, + 0, + 38, + 190, + 25, + 77, + 24, + 6, + 29, + 0, + 80, + 135, + 103, + 211, + 27, + 4, + 27, + 6, + 80, + 135, + 103, + 211, + 21, + 4, + 27, + 12, + 198, + 224, + 138, + 13, + 24, + 4, + 26, + 0, + 198, + 224, + 138, + 13, + 24, + 4, + 27, + 22, + 198, + 224, + 138, + 13, + 24, + 4, + 28, + 22, + 198, + 224, + 138, + 13, + 24, + 4, + 25, + 0, + 198, + 224, + 138, + 13, + 24, + 4, + 29, + 0, + 198, + 224, + 138, + 13, + 23, + 4, + 29, + 0, + 198, + 224, + 138, + 13, + 23, + 4, + 28, + 0, + 198, + 224, + 138, + 13, + 23, + 4, + 27, + 0, + 198, + 224, + 138, + 13, + 23, + 4, + 26, + 22, + 198, + 224, + 138, + 13, + 23, + 4, + 25, + 22, + 198, + 224, + 138, + 13, + 25, + 4, + 29, + 0, + 198, + 224, + 138, + 13, + 25, + 4, + 27, + 22, + 198, + 224, + 138, + 13, + 25, + 4, + 28, + 22, + 198, + 224, + 138, + 13, + 25, + 4, + 26, + 22, + 198, + 224, + 138, + 13, + 25, + 4, + 25, + 22, + 198, + 224, + 138, + 13, + 26, + 4, + 29, + 0, + 198, + 224, + 138, + 13, + 26, + 4, + 28, + 22, + 198, + 224, + 138, + 13, + 26, + 4, + 27, + 22, + 198, + 224, + 138, + 13, + 26, + 4, + 26, + 22, + 198, + 224, + 138, + 13, + 26, + 4, + 25, + 22, + 198, + 224, + 138, + 13, + 22, + 4, + 25, + 0, + 198, + 224, + 138, + 13, + 22, + 4, + 26, + 0, + 198, + 224, + 138, + 13, + 22, + 4, + 27, + 23, + 198, + 224, + 138, + 13, + 22, + 4, + 28, + 23, + 198, + 224, + 138, + 13, + 22, + 4, + 29, + 0, + 198, + 224, + 138, + 13, + 25, + 5, + 29, + 0, + 198, + 224, + 138, + 13, + 23, + 5, + 29, + 0, + 198, + 224, + 138, + 13, + 24, + 5, + 29, + 0, + 198, + 224, + 138, + 13, + 22, + 5, + 28, + 0, + 198, + 224, + 138, + 13, + 26, + 5, + 28, + 0, + 198, + 224, + 138, + 13, + 25, + 5, + 26, + 0, + 198, + 224, + 138, + 13, + 23, + 5, + 26, + 0, + 198, + 224, + 138, + 13, + 24, + 4, + 24, + 22, + 198, + 224, + 138, + 13, + 24, + 4, + 23, + 22, + 198, + 224, + 138, + 13, + 24, + 4, + 22, + 22, + 198, + 224, + 138, + 13, + 24, + 4, + 21, + 22, + 198, + 224, + 138, + 13, + 24, + 4, + 20, + 22, + 198, + 224, + 138, + 13, + 24, + 4, + 19, + 22, + 198, + 224, + 138, + 13, + 24, + 4, + 18, + 22, + 198, + 224, + 138, + 13, + 24, + 4, + 17, + 22, + 198, + 224, + 138, + 13, + 24, + 4, + 16, + 22, + 198, + 224, + 138, + 13, + 24, + 4, + 15, + 22, + 198, + 224, + 138, + 13, + 24, + 4, + 14, + 22, + 198, + 224, + 138, + 13, + 23, + 4, + 17, + 12, + 198, + 224, + 138, + 13, + 25, + 4, + 17, + 6, + 198, + 224, + 138, + 13, + 23, + 4, + 16, + 12, + 198, + 224, + 138, + 13, + 25, + 4, + 16, + 6, + 198, + 224, + 138, + 13, + 23, + 4, + 15, + 12, + 198, + 224, + 138, + 13, + 25, + 4, + 15, + 6, + 198, + 224, + 138, + 13, + 23, + 4, + 14, + 12, + 198, + 224, + 138, + 13, + 25, + 4, + 14, + 6, + 198, + 224, + 138, + 13, + 22, + 4, + 17, + 12, + 198, + 224, + 138, + 13, + 26, + 4, + 17, + 6, + 198, + 224, + 138, + 13, + 22, + 4, + 16, + 12, + 198, + 224, + 138, + 13, + 26, + 4, + 16, + 6, + 198, + 224, + 138, + 13, + 22, + 4, + 15, + 12, + 198, + 224, + 138, + 13, + 26, + 4, + 15, + 6, + 198, + 224, + 138, + 13, + 22, + 4, + 14, + 12, + 198, + 224, + 138, + 13, + 26, + 4, + 14, + 6, + 240, + 75, + 110, + 137, + 21, + 4, + 15, + 12, + 240, + 75, + 110, + 137, + 27, + 4, + 15, + 6]; + +const VALID_COLOUR: &[u8] = &[64, + 0, + 0, + 0, + 0, + 24, + 6, + 29, + 0, + 27, + 4, + 27, + 0, + 21, + 4, + 27, + 0, + 24, + 4, + 26, + 0, + 24, + 4, + 27, + 0, + 24, + 4, + 28, + 0, + 24, + 4, + 25, + 0, + 24, + 4, + 29, + 0, + 23, + 4, + 29, + 0, + 23, + 4, + 28, + 0, + 23, + 4, + 27, + 0, + 23, + 4, + 26, + 0, + 23, + 4, + 25, + 0, + 25, + 4, + 29, + 0, + 25, + 4, + 27, + 0, + 25, + 4, + 28, + 0, + 25, + 4, + 26, + 0, + 25, + 4, + 25, + 0, + 26, + 4, + 29, + 0, + 26, + 4, + 28, + 0, + 26, + 4, + 27, + 0, + 26, + 4, + 26, + 0, + 26, + 4, + 25, + 0, + 22, + 4, + 25, + 0, + 22, + 4, + 26, + 0, + 22, + 4, + 27, + 0, + 22, + 4, + 28, + 0, + 22, + 4, + 29, + 1, + 25, + 5, + 29, + 1, + 23, + 5, + 29, + 1, + 24, + 5, + 29, + 1, + 22, + 5, + 28, + 1, + 26, + 5, + 28, + 1, + 25, + 5, + 26, + 1, + 23, + 5, + 26, + 0, + 24, + 4, + 24, + 0, + 24, + 4, + 23, + 0, + 24, + 4, + 22, + 0, + 24, + 4, + 21, + 0, + 24, + 4, + 20, + 0, + 24, + 4, + 19, + 0, + 24, + 4, + 18, + 0, + 24, + 4, + 17, + 0, + 24, + 4, + 16, + 0, + 24, + 4, + 15, + 0, + 24, + 4, + 14, + 0, + 23, + 4, + 17, + 0, + 25, + 4, + 17, + 0, + 23, + 4, + 16, + 0, + 25, + 4, + 16, + 0, + 23, + 4, + 15, + 0, + 25, + 4, + 15, + 0, + 23, + 4, + 14, + 0, + 25, + 4, + 14, + 0, + 22, + 4, + 17, + 0, + 26, + 4, + 17, + 0, + 22, + 4, + 16, + 0, + 26, + 4, + 16, + 0, + 22, + 4, + 15, + 0, + 26, + 4, + 15, + 0, + 22, + 4, + 14, + 0, + 26, + 4, + 14, + 0, + 21, + 4, + 15, + 0, + 27, + 4, + 15]; + +pub(super) fn tdm_machines_provider() -> SimpleFunc<1, crate::UserTy, impl (Fn(ParameterTable, &crate::UserTy) -> Result) + Sync + Sync> { + SimpleFunc::new(|params, user: &crate::UserTy| { + let ulock = user.read().unwrap(); + let mut params = params.to_dict(); + params.insert(PARAM_KEY, PlayerDatas { + players: vec![ + PlayerData { + name: ulock.uuid.clone(), + display_name: ulock.uuid.clone(), + mastery: 1, + tier: 1, + robot_name: "RE_machine_name_mine_sp".to_owned(), + robot_map: VALID_ROBOT.iter().map(|x| *x).collect(), + team: 0, + has_premium: false, + robot_uuid: "12345_12345".to_owned(), + cpu: 0, + weapon_order: vec![20000200, 0, 0], + colour_map: VALID_COLOUR.iter().map(|x| *x).collect(), + is_ai: false, + spawn_effect: "Spawn_Warp".to_owned(), + death_effect: "Explosion_Warp".to_owned(), + player_rank: 1, + weapon_rank: vec![(20000200, 1), (0, 0)].into_iter().collect(), + }, + // TODO: use SingleplayerEvent 3 (SpawnRobot) to spawn enemy bots instead + // doing it through this op response seems to have a bug/flaw (intentional?) in the code + /*PlayerData { + name: "RE_username0".to_owned(), + display_name: "RE_displayname0".to_owned(), + mastery: 1, + tier: 1, + robot_name: "RE_machine_name0_sp".to_owned(), + robot_map: VALID_ROBOT.iter().map(|x| *x).collect(), + team: 1, + has_premium: false, + robot_uuid: "123_123".to_owned(), + cpu: 0, + weapon_order: vec![20000200, 0, 0], + colour_map: VALID_COLOUR.iter().map(|x| *x).collect(), + is_ai: true, + spawn_effect: "Spawn_Warp".to_owned(), + death_effect: "Explosion_Warp".to_owned(), + player_rank: 1, + weapon_rank: vec![(20000200, 1), (0, 0)].into_iter().collect(), + }, + PlayerData { + name: "RE_username1".to_owned(), + display_name: "RE_displayname1".to_owned(), + mastery: 1, + tier: 1, + robot_name: "RE_machine_name1_sp".to_owned(), + robot_map: VALID_ROBOT.iter().map(|x| *x).collect(), + team: 1, + has_premium: false, + robot_uuid: "1_1".to_owned(), + cpu: 0, + weapon_order: vec![20000200, 0, 0], + colour_map: VALID_COLOUR.iter().map(|x| *x).collect(), + is_ai: true, + spawn_effect: "Spawn_Warp".to_owned(), + death_effect: "Explosion_Warp".to_owned(), + player_rank: 1, + weapon_rank: vec![(20000200, 1), (0, 0)].into_iter().collect(), + },*/ + ] + }.as_transmissible()); + Ok(params.into()) + }) +} diff --git a/rc_singleplayer_room/src/operations/mod.rs b/rc_singleplayer_room/src/operations/mod.rs new file mode 100644 index 0000000..cfc8f9f --- /dev/null +++ b/rc_singleplayer_room/src/operations/mod.rs @@ -0,0 +1,13 @@ +mod more_auth; +mod eac; +mod load_ai_robots; + +use polariton_server::operations::OperationsHandler; + +pub fn handler() -> OperationsHandler { + OperationsHandler::::new() + .without_state(more_auth::MoreLobbyAuth) + .without_state(eac::EacChallengeIgnorer) + .without_state(load_ai_robots::tdm_machines_provider()) + //.without_state(polariton_server::operations::Ack::<33, _>::default()) // get user clan info (this is equivalent to not being in a clan) +} diff --git a/rc_singleplayer_room/src/operations/more_auth.rs b/rc_singleplayer_room/src/operations/more_auth.rs new file mode 100644 index 0000000..c1fc5ee --- /dev/null +++ b/rc_singleplayer_room/src/operations/more_auth.rs @@ -0,0 +1,42 @@ +use polariton::operation::Typed; +use polariton_server::operations::{Operation, OperationCode}; + +pub struct MoreLobbyAuth; + +impl MoreLobbyAuth { + const AUTH_PAYLOAD_KEY: u8 = 245; +} + +impl Operation for MoreLobbyAuth { + type State = (); + type User = crate::UserTy; + + fn handle(&self, params: polariton::operation::ParameterTable, _: &mut Self::State, user: &Self::User) -> polariton::operation::OperationResponse { + let params_dict = params.to_dict(); + if let Some(Typed::Str(auth_payload)) = params_dict.get(&Self::AUTH_PAYLOAD_KEY) { + let mut write_lock = user.write().unwrap(); + if write_lock.update_with_auth(&auth_payload.string) { + let mut resp_params = std::collections::HashMap::new(); + resp_params.insert(Self::AUTH_PAYLOAD_KEY, polariton::operation::Typed::Byte(0)); + return polariton::operation::OperationResponse { + code: 230, + return_code: 0, + message: polariton::operation::Typed::Null, + params: resp_params.into(), + } + } + } + polariton::operation::OperationResponse { + code: 230, + return_code: 120, + message: polariton::operation::Typed::Null, + params: std::collections::HashMap::new().into(), + } + } +} + +impl OperationCode for MoreLobbyAuth { + fn op_code() -> u8 { + 230 + } +} diff --git a/rc_singleplayer_room/src/state.rs b/rc_singleplayer_room/src/state.rs new file mode 100644 index 0000000..bc39273 --- /dev/null +++ b/rc_singleplayer_room/src/state.rs @@ -0,0 +1,27 @@ +use std::sync::RwLock; + +#[derive(Default, Debug)] +pub struct UserState { + pub uuid: String, + pub token: String, + pub refresh_token: String, +} + +impl UserState { + pub fn update_with_auth(&mut self, auth_str: &str) -> bool { + let splits: Vec<&str> = auth_str.split(';').collect(); + if splits.len() != 3 { + log::warn!("Invalid auth payload: {}", auth_str); + false + } else { + self.uuid = splits[0].to_owned(); + self.token = splits[1].to_owned(); + self.refresh_token = splits[2].to_owned(); + true + } + } + + pub fn new() -> crate::UserTy { + RwLock::new(UserState::default()) + } +} diff --git a/rc_social_room/src/operations/platoon_data.rs b/rc_social_room/src/operations/platoon_data.rs index bd22125..d6ac699 100644 --- a/rc_social_room/src/operations/platoon_data.rs +++ b/rc_social_room/src/operations/platoon_data.rs @@ -1,15 +1,15 @@ use polariton_server::operations::SimpleFunc; use polariton::operation::ParameterTable; -//const PLATOON_ID_PARAM_KEY: u8 = 16; +const PLATOON_ID_PARAM_KEY: u8 = 16; //const PLATOON_LEADER_PARAM_KEY: u8 = 17; //const USER_LIST_PARAM_KEY: u8 = 7; pub(super) fn platoon_provider() -> SimpleFunc<18, crate::UserTy, impl (Fn(ParameterTable, &crate::UserTy) -> Result, i16>) + Sync + Sync, C> { SimpleFunc::new(|params, _| { - //let mut params = params.to_dict(); + let mut params = params.to_dict(); // if platoon ID is not provided, you're not in a platoon - //Ok(params.into()) - Ok(params) + params.insert(PLATOON_ID_PARAM_KEY, polariton::operation::Typed::Null); + Ok(params.into()) }) }