diff --git a/Cargo.toml b/Cargo.toml index c6cf57e..db17799 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -61,6 +61,7 @@ atomic_enum = "0.3.0" num_enum = "0.7.2" dashmap = "6.0.1" regex = "1.10.5" +rustyline-async = "0.4.2" ansi_term = "0.12.1" # Internal diff --git a/nap_common/Cargo.toml b/nap_common/Cargo.toml index f833e6d..f44d6b6 100644 --- a/nap_common/Cargo.toml +++ b/nap_common/Cargo.toml @@ -16,8 +16,7 @@ rand_mt.workspace = true # Database sqlx.workspace = true - -# Util +rustyline-async.workspace = true thiserror.workspace = true rand.workspace = true byteorder.workspace = true diff --git a/nap_common/src/logging.rs b/nap_common/src/logging.rs index 3072a93..53f1ef4 100644 --- a/nap_common/src/logging.rs +++ b/nap_common/src/logging.rs @@ -1,6 +1,18 @@ -pub fn init_tracing() { +use env_logger::Target; +use rustyline_async::SharedWriter; +use tracing_log::log::LevelFilter; + +pub fn init_tracing(out: Option) { #[cfg(target_os = "windows")] ansi_term::enable_ansi_support().unwrap(); - env_logger::init_from_env(env_logger::Env::new().default_filter_or("info")); + let target = match out { + Some(out) => Target::Pipe(Box::new(out)), + None => Target::Stdout, + }; + + env_logger::builder() + .target(target) + .filter(None, LevelFilter::Info) + .init(); } diff --git a/nap_gameserver/Cargo.toml b/nap_gameserver/Cargo.toml index a8c5131..fafe57c 100644 --- a/nap_gameserver/Cargo.toml +++ b/nap_gameserver/Cargo.toml @@ -28,6 +28,7 @@ rand.workspace = true atomic_enum.workspace = true num_enum.workspace = true dashmap.workspace = true +rustyline-async.workspace = true # Tracing tracing.workspace = true diff --git a/nap_gameserver/src/commands/mod.rs b/nap_gameserver/src/commands/mod.rs new file mode 100644 index 0000000..452c994 --- /dev/null +++ b/nap_gameserver/src/commands/mod.rs @@ -0,0 +1,83 @@ +use std::io::Write; +use std::sync::Arc; + +use rustyline_async::{Readline, ReadlineEvent, SharedWriter}; + +use crate::ServerState; + +mod player; + +pub struct CommandManager { + state: Arc, +} + +macro_rules! commands { + ($($category:ident::$action:ident $usage:tt $desc:tt;)*) => { + async fn exec(state: &ServerState, cmd: &str) -> String { + let input = cmd.split(" ").collect::>(); + + if input.len() == 1 && *input.get(0).unwrap() == "help" { + return Self::help_message(); + } + + let (Some(category), Some(action)) = (input.get(0), input.get(1)) else { + return String::new(); + }; + + let args = &input[2..]; + match match (*category, *action) { + $( + (stringify!($category), stringify!($action)) => { + $category::$action(args, state).await + } + )*, + _ => { + ::tracing::info!("unknown command"); + return Self::help_message(); + } + } { + Ok(s) => s, + Err(err) => format!("failed to execute command: {err}"), + } + } + + fn help_message() -> String { + concat!("available commands:\n", + $(stringify!($category), " ", stringify!($action), " ", $usage, " - ", $desc, "\n",)* + "help - shows this message" + ).to_string() + } + }; +} + +impl CommandManager { + pub fn new(state: Arc) -> Self { + Self { state } + } + + pub async fn run(&self, mut rl: Readline, mut out: SharedWriter) { + loop { + match rl.readline().await { + Ok(ReadlineEvent::Line(line)) => { + let str = Self::exec(&self.state, &line).await; + writeln!(&mut out, "{str}").unwrap(); + + rl.add_history_entry(line); + } + Ok(ReadlineEvent::Eof) | Ok(ReadlineEvent::Interrupted) => { + rl.flush().unwrap(); + drop(rl); + + // TODO: maybe disconnect and save all players + + std::process::exit(0); + } + _ => continue, + } + } + } + + commands! { + player::avatar "[player_uid] [avatar_id]" "changes player avatar for main city"; + } +} diff --git a/nap_gameserver/src/commands/player.rs b/nap_gameserver/src/commands/player.rs new file mode 100644 index 0000000..150a0ad --- /dev/null +++ b/nap_gameserver/src/commands/player.rs @@ -0,0 +1,39 @@ +use data::tables; + +use crate::ServerState; + +pub async fn avatar( + args: &[&str], + state: &ServerState, +) -> Result> { + const USAGE: &str = "Usage: player avatar [player_uid] [avatar_id]"; + + if args.len() != 2 { + return Ok(USAGE.to_string()); + } + + let uid = args[0].parse::()?; + let avatar_id = args[1].parse::()?; + + let Some(player_lock) = state.player_mgr.get_player(uid).await else { + return Ok(String::from("player not found")); + }; + + if !tables::avatar_base_template_tb::iter().any(|tmpl| tmpl.id == avatar_id) { + return Ok(format!("section with id {avatar_id} doesn't exist")); + } + + let should_save = { + let mut player = player_lock.lock().await; + player.basic_data_model.frontend_avatar_id = avatar_id as i32; + player.current_session_id().is_none() + }; + + if should_save { + state.player_mgr.save_and_remove(uid).await; + } + + Ok(format!( + "changed frontend_avatar_id, you have to re-enter main city now" + )) +} diff --git a/nap_gameserver/src/logic/role/avatar.rs b/nap_gameserver/src/logic/role/avatar.rs index 03e8b66..cbd2ff9 100644 --- a/nap_gameserver/src/logic/role/avatar.rs +++ b/nap_gameserver/src/logic/role/avatar.rs @@ -78,7 +78,7 @@ impl Avatar { level: s.level, }) .collect(), - exp: self.exp, // exp? + exp: self.exp, rank: self.rank, talent_switch_list: self.talent_switch.to_vec(), unlocked_talent_num: self.unlocked_talent_num, diff --git a/nap_gameserver/src/main.rs b/nap_gameserver/src/main.rs index 896e619..a42ef39 100644 --- a/nap_gameserver/src/main.rs +++ b/nap_gameserver/src/main.rs @@ -1,11 +1,14 @@ use std::{error::Error, sync::Arc}; +use commands::CommandManager; use common::{init_tracing, splash, util::load_or_create_config}; use config::NapGSConfig; use data::init_data; use logic::player::PlayerManager; use net::NetSessionManager; +use rustyline_async::Readline; +mod commands; mod config; mod database; mod handlers; @@ -21,25 +24,27 @@ pub struct ServerState { #[tokio::main] async fn main() -> Result<(), Box> { splash::print("GameServer"); - init_tracing(); + + let (rl, out) = Readline::new(String::from(">> ")).unwrap(); + + init_tracing(Some(out.clone())); tracing::info!("don't forget to visit https://discord.xeondev.com/"); let config = load_or_create_config::("nap_gameserver.toml"); init_data(&config.assets).expect("failed to init data"); - let pg_pool = match common::database::init(&config.database_credentials).await { - Ok(pool) => pool, - Err(err) => { - tracing::error!("failed to connect to database: {err}"); - std::process::exit(1); - } - }; + let pg_pool = common::database::init(&config.database_credentials) + .await + .expect("failed to connect to database"); - let state = ServerState { + let state = Arc::new(ServerState { session_mgr: NetSessionManager::new(), player_mgr: PlayerManager::new(pg_pool), config, - }; + }); - net::listen(Arc::new(state)).await + let command_mgr = CommandManager::new(state.clone()); + tokio::spawn(async move { command_mgr.run(rl, out).await }); + + net::listen(state).await } diff --git a/nap_sdk/src/main.rs b/nap_sdk/src/main.rs index 02d76a0..2feb652 100644 --- a/nap_sdk/src/main.rs +++ b/nap_sdk/src/main.rs @@ -22,7 +22,8 @@ pub struct SdkState { #[tokio::main] async fn main() -> Result<(), Box> { splash::print("SDKServer"); - init_tracing(); + + init_tracing(None); tracing::info!("don't forget to visit https://discord.xeondev.com/"); let config = load_or_create_config::("nap_sdk.toml"); @@ -44,7 +45,6 @@ async fn main() -> Result<(), Box> { tracing::info!("listening at {}", &config.http_addr); axum::serve(tcp_listener, router.into_make_service()).await?; - Ok(()) } @@ -53,6 +53,6 @@ async fn fallback(uri: Uri) -> impl IntoResponse { StatusCode::NOT_FOUND } -pub async fn data_upload(_: Json) -> String { - String::from(r#"{"retcode": 0}"#) +pub async fn data_upload(_: Json) -> &'static str { + r#"{"retcode": 0}"# }