simple CLI command system

Implement CLI command system
command for changing player's frontend avatar
This commit is contained in:
xeon 2024-07-22 19:47:04 +03:00
parent 2de8ee290a
commit 9af61cd33f
9 changed files with 160 additions and 20 deletions

View file

@ -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

View file

@ -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

View file

@ -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();
}

View file

@ -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

View 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";
}
}

View 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"
))
}

View file

@ -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,

View file

@ -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
}

View file

@ -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}"#
}