simple CLI command system
Implement CLI command system command for changing player's frontend avatar
This commit is contained in:
parent
2de8ee290a
commit
9af61cd33f
9 changed files with 160 additions and 20 deletions
|
@ -61,6 +61,7 @@ atomic_enum = "0.3.0"
|
||||||
num_enum = "0.7.2"
|
num_enum = "0.7.2"
|
||||||
dashmap = "6.0.1"
|
dashmap = "6.0.1"
|
||||||
regex = "1.10.5"
|
regex = "1.10.5"
|
||||||
|
rustyline-async = "0.4.2"
|
||||||
ansi_term = "0.12.1"
|
ansi_term = "0.12.1"
|
||||||
|
|
||||||
# Internal
|
# Internal
|
||||||
|
|
|
@ -16,8 +16,7 @@ rand_mt.workspace = true
|
||||||
|
|
||||||
# Database
|
# Database
|
||||||
sqlx.workspace = true
|
sqlx.workspace = true
|
||||||
|
rustyline-async.workspace = true
|
||||||
# Util
|
|
||||||
thiserror.workspace = true
|
thiserror.workspace = true
|
||||||
rand.workspace = true
|
rand.workspace = true
|
||||||
byteorder.workspace = true
|
byteorder.workspace = true
|
||||||
|
|
|
@ -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<SharedWriter>) {
|
||||||
#[cfg(target_os = "windows")]
|
#[cfg(target_os = "windows")]
|
||||||
ansi_term::enable_ansi_support().unwrap();
|
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();
|
||||||
}
|
}
|
||||||
|
|
|
@ -28,6 +28,7 @@ rand.workspace = true
|
||||||
atomic_enum.workspace = true
|
atomic_enum.workspace = true
|
||||||
num_enum.workspace = true
|
num_enum.workspace = true
|
||||||
dashmap.workspace = true
|
dashmap.workspace = true
|
||||||
|
rustyline-async.workspace = true
|
||||||
|
|
||||||
# Tracing
|
# Tracing
|
||||||
tracing.workspace = true
|
tracing.workspace = true
|
||||||
|
|
83
nap_gameserver/src/commands/mod.rs
Normal file
83
nap_gameserver/src/commands/mod.rs
Normal file
|
@ -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<ServerState>,
|
||||||
|
}
|
||||||
|
|
||||||
|
macro_rules! commands {
|
||||||
|
($($category:ident::$action:ident $usage:tt $desc:tt;)*) => {
|
||||||
|
async fn exec(state: &ServerState, cmd: &str) -> String {
|
||||||
|
let input = cmd.split(" ").collect::<Vec<&str>>();
|
||||||
|
|
||||||
|
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<ServerState>) -> 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";
|
||||||
|
}
|
||||||
|
}
|
39
nap_gameserver/src/commands/player.rs
Normal file
39
nap_gameserver/src/commands/player.rs
Normal file
|
@ -0,0 +1,39 @@
|
||||||
|
use data::tables;
|
||||||
|
|
||||||
|
use crate::ServerState;
|
||||||
|
|
||||||
|
pub async fn avatar(
|
||||||
|
args: &[&str],
|
||||||
|
state: &ServerState,
|
||||||
|
) -> Result<String, Box<dyn std::error::Error>> {
|
||||||
|
const USAGE: &str = "Usage: player avatar [player_uid] [avatar_id]";
|
||||||
|
|
||||||
|
if args.len() != 2 {
|
||||||
|
return Ok(USAGE.to_string());
|
||||||
|
}
|
||||||
|
|
||||||
|
let uid = args[0].parse::<u32>()?;
|
||||||
|
let avatar_id = args[1].parse::<u32>()?;
|
||||||
|
|
||||||
|
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"
|
||||||
|
))
|
||||||
|
}
|
|
@ -78,7 +78,7 @@ impl Avatar {
|
||||||
level: s.level,
|
level: s.level,
|
||||||
})
|
})
|
||||||
.collect(),
|
.collect(),
|
||||||
exp: self.exp, // exp?
|
exp: self.exp,
|
||||||
rank: self.rank,
|
rank: self.rank,
|
||||||
talent_switch_list: self.talent_switch.to_vec(),
|
talent_switch_list: self.talent_switch.to_vec(),
|
||||||
unlocked_talent_num: self.unlocked_talent_num,
|
unlocked_talent_num: self.unlocked_talent_num,
|
||||||
|
|
|
@ -1,11 +1,14 @@
|
||||||
use std::{error::Error, sync::Arc};
|
use std::{error::Error, sync::Arc};
|
||||||
|
|
||||||
|
use commands::CommandManager;
|
||||||
use common::{init_tracing, splash, util::load_or_create_config};
|
use common::{init_tracing, splash, util::load_or_create_config};
|
||||||
use config::NapGSConfig;
|
use config::NapGSConfig;
|
||||||
use data::init_data;
|
use data::init_data;
|
||||||
use logic::player::PlayerManager;
|
use logic::player::PlayerManager;
|
||||||
use net::NetSessionManager;
|
use net::NetSessionManager;
|
||||||
|
use rustyline_async::Readline;
|
||||||
|
|
||||||
|
mod commands;
|
||||||
mod config;
|
mod config;
|
||||||
mod database;
|
mod database;
|
||||||
mod handlers;
|
mod handlers;
|
||||||
|
@ -21,25 +24,27 @@ pub struct ServerState {
|
||||||
#[tokio::main]
|
#[tokio::main]
|
||||||
async fn main() -> Result<(), Box<dyn Error>> {
|
async fn main() -> Result<(), Box<dyn Error>> {
|
||||||
splash::print("GameServer");
|
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/");
|
tracing::info!("don't forget to visit https://discord.xeondev.com/");
|
||||||
|
|
||||||
let config = load_or_create_config::<NapGSConfig>("nap_gameserver.toml");
|
let config = load_or_create_config::<NapGSConfig>("nap_gameserver.toml");
|
||||||
init_data(&config.assets).expect("failed to init data");
|
init_data(&config.assets).expect("failed to init data");
|
||||||
|
|
||||||
let pg_pool = match common::database::init(&config.database_credentials).await {
|
let pg_pool = common::database::init(&config.database_credentials)
|
||||||
Ok(pool) => pool,
|
.await
|
||||||
Err(err) => {
|
.expect("failed to connect to database");
|
||||||
tracing::error!("failed to connect to database: {err}");
|
|
||||||
std::process::exit(1);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
let state = ServerState {
|
let state = Arc::new(ServerState {
|
||||||
session_mgr: NetSessionManager::new(),
|
session_mgr: NetSessionManager::new(),
|
||||||
player_mgr: PlayerManager::new(pg_pool),
|
player_mgr: PlayerManager::new(pg_pool),
|
||||||
config,
|
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
|
||||||
}
|
}
|
||||||
|
|
|
@ -22,7 +22,8 @@ pub struct SdkState {
|
||||||
#[tokio::main]
|
#[tokio::main]
|
||||||
async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||||
splash::print("SDKServer");
|
splash::print("SDKServer");
|
||||||
init_tracing();
|
|
||||||
|
init_tracing(None);
|
||||||
tracing::info!("don't forget to visit https://discord.xeondev.com/");
|
tracing::info!("don't forget to visit https://discord.xeondev.com/");
|
||||||
|
|
||||||
let config = load_or_create_config::<NapSdkConfig>("nap_sdk.toml");
|
let config = load_or_create_config::<NapSdkConfig>("nap_sdk.toml");
|
||||||
|
@ -44,7 +45,6 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||||
tracing::info!("listening at {}", &config.http_addr);
|
tracing::info!("listening at {}", &config.http_addr);
|
||||||
|
|
||||||
axum::serve(tcp_listener, router.into_make_service()).await?;
|
axum::serve(tcp_listener, router.into_make_service()).await?;
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -53,6 +53,6 @@ async fn fallback(uri: Uri) -> impl IntoResponse {
|
||||||
StatusCode::NOT_FOUND
|
StatusCode::NOT_FOUND
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn data_upload(_: Json<Value>) -> String {
|
pub async fn data_upload(_: Json<Value>) -> &'static str {
|
||||||
String::from(r#"{"retcode": 0}"#)
|
r#"{"retcode": 0}"#
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue