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"
|
||||
dashmap = "6.0.1"
|
||||
regex = "1.10.5"
|
||||
rustyline-async = "0.4.2"
|
||||
ansi_term = "0.12.1"
|
||||
|
||||
# Internal
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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")]
|
||||
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
|
||||
num_enum.workspace = true
|
||||
dashmap.workspace = true
|
||||
rustyline-async.workspace = true
|
||||
|
||||
# Tracing
|
||||
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,
|
||||
})
|
||||
.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,
|
||||
|
|
|
@ -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<dyn Error>> {
|
||||
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::<NapGSConfig>("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
|
||||
}
|
||||
|
|
|
@ -22,7 +22,8 @@ pub struct SdkState {
|
|||
#[tokio::main]
|
||||
async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
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::<NapSdkConfig>("nap_sdk.toml");
|
||||
|
@ -44,7 +45,6 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
|||
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<Value>) -> String {
|
||||
String::from(r#"{"retcode": 0}"#)
|
||||
pub async fn data_upload(_: Json<Value>) -> &'static str {
|
||||
r#"{"retcode": 0}"#
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue