aboutsummaryrefslogtreecommitdiff
path: root/apps/console/src/bin/padc.rs
diff options
context:
space:
mode:
author1992414357@qq.com <1992414357@qq.com>2025-06-13 19:20:06 +0800
committer1992414357@qq.com <1992414357@qq.com>2025-06-13 19:20:06 +0800
commita210ab86ee43dea3b4d9d0f2af5750c5a4adf096 (patch)
tree7111a1c93d1f261d097ff0288c8b879db0830085 /apps/console/src/bin/padc.rs
parent98dad6afa316b9a44ff76b305520339ddc44c789 (diff)
.
Diffstat (limited to 'apps/console/src/bin/padc.rs')
-rw-r--r--apps/console/src/bin/padc.rs742
1 files changed, 742 insertions, 0 deletions
diff --git a/apps/console/src/bin/padc.rs b/apps/console/src/bin/padc.rs
new file mode 100644
index 0000000..e44f0f9
--- /dev/null
+++ b/apps/console/src/bin/padc.rs
@@ -0,0 +1,742 @@
+use clap::{Args, ColorChoice, CommandFactory, Parser, Subcommand};
+use nogamepads::string_utils::process_id_text;
+use nogamepads_console::utils::{confirm, read_password, read_password_and_confirm};
+use serde::{Deserialize, Serialize};
+use std::collections::HashMap;
+use std::env::current_dir;
+use std::fs::File;
+use std::io;
+use std::io::{BufReader, Write};
+use std::net::SocketAddr;
+use std::path::PathBuf;
+use std::process::exit;
+use std::str::FromStr;
+use std::sync::{Arc, Mutex};
+use std::sync::atomic::Ordering::SeqCst;
+use std::time::Duration;
+use clap_complete::{generate, Shell};
+use log::{info, LevelFilter};
+use tokio::{select, spawn};
+use tokio::signal::ctrl_c;
+use tokio::time::sleep;
+use nogamepads::entry_mutex;
+use nogamepads::logger_utils::logger_build;
+use nogamepads_core::data::controller::controller_cli::{process_controller_cli, ControllerCli};
+use nogamepads_core::data::controller::controller_data::ControllerData;
+use nogamepads_core::data::game::game_cli::{process_game_cli, GameCli};
+use nogamepads_core::data::game::game_data::{GameData, GameRuntimeDataArchive};
+use nogamepads_core::data::player::player_data::Player;
+use nogamepads_core::service::cli_addition::runtime_consoles::RuntimeConsole;
+use nogamepads_core::service::service_runner::{NoGamepadsService, ServiceRunner};
+use nogamepads_core::service::tcp_network::DEFAULT_PORT;
+use nogamepads_core::service::tcp_network::pad_client::pad_client_service::PadClientNetwork;
+use nogamepads_core::service::tcp_network::pad_server::pad_server_service::PadServerNetwork;
+use nogamepads_core::service::tcp_network::utils::tokio_utils::build_tokio_runtime;
+
+#[derive(Parser, Debug)]
+#[command(version, color = ColorChoice::Auto)]
+struct Padc {
+ #[command(subcommand)]
+ command: Commands,
+}
+
+#[derive(Subcommand, Debug)]
+enum Commands {
+
+ GenerateCompletion {
+ #[arg(value_enum)]
+ shell: Shell,
+ },
+
+ #[command(subcommand, about = "Manage accounts")]
+ Account(AccountCommands),
+
+ #[command(about = "List accounts")]
+ Accounts,
+
+ #[command(subcommand, about = "Manage game infos")]
+ Game(GameCommands),
+
+ #[command(about = "List all games")]
+ Games,
+
+ #[command(about = "Connect to a service")]
+ Connect(ConnectArgs),
+
+ #[command(about = "Start a service")]
+ Listen(ListenArgs)
+}
+
+#[derive(Subcommand, Debug)]
+enum AccountCommands {
+
+ #[command(about = "Add a account")]
+ Add(AccountArgs),
+
+ #[command(about = "Remove a account")]
+ Remove(AccountArgs),
+
+ #[command(about = "Edit the profile of account")]
+ Edit(EditAccountArgs)
+}
+
+#[derive(Args, Debug)]
+struct AccountArgs{
+
+ #[arg(value_name = "Account")]
+ account: String,
+
+ #[arg(short, long, value_name = "Password")]
+ password: Option<String>
+}
+
+#[derive(Args, Debug)]
+struct EditAccountArgs{
+
+ #[arg(value_name = "Account")]
+ account: String,
+
+ #[arg(short, long, help = "Nickname")]
+ nickname: Option<String>,
+
+ #[arg(short = 'c', long = "color", num_args = 3, value_names = ["H", "S", "V"], help = "Player color, h(0 - 360), s(0 - 1), v(0 - 1)")]
+ color: Option<Vec<f64>>
+}
+
+#[derive(Subcommand, Debug)]
+enum GameCommands {
+
+ #[command(about = "Add a game")]
+ Add(GameArgs),
+
+ #[command(about = "Remove a game")]
+ Remove(GameArgs),
+
+ #[command(about = "Edit the profile of game")]
+ Edit(EditGameArgs),
+
+ #[command(subcommand, about = "Register keys to game")]
+ Register(RegisterKeysCommands)
+}
+
+#[derive(Args, Debug)]
+struct GameArgs {
+
+ #[arg(value_name = "Game Name")]
+ name: String,
+
+ #[arg(short, long)]
+ confirm: bool
+}
+
+#[derive(Args, Debug)]
+struct EditGameArgs {
+
+ #[arg(value_name = "Game Name")]
+ name: String,
+
+ #[arg(value_name = "Key")]
+ key: String,
+
+ #[arg(value_name = "Value")]
+ value: String,
+}
+
+#[derive(Subcommand, Debug)]
+enum RegisterKeysCommands {
+
+ #[command(subcommand, about = "Register a button")]
+ Button(KeyManageCommands),
+
+ #[command(subcommand, about = "Register a axis")]
+ Axis(KeyManageCommands),
+
+ #[command(subcommand, about = "Register a direction")]
+ Direction(KeyManageCommands)
+}
+
+#[derive(Subcommand, Debug)]
+enum KeyManageCommands {
+
+ #[command(about = "Add or rename a key")]
+ Add(AddKeyArgs),
+
+ #[command(about = "Remove a key")]
+ Remove(RemoveKeyArgs),
+
+ #[command(about = "List all")]
+ List(ListKeyArgs)
+}
+
+#[derive(Args, Debug)]
+struct AddKeyArgs{
+
+ #[arg(value_name = "Game Name")]
+ name: String,
+
+ #[arg(value_name = "Key")]
+ key: u8,
+
+ #[arg(value_name = "Name")]
+ key_name: String
+}
+
+#[derive(Args, Debug)]
+struct RemoveKeyArgs{
+
+ #[arg(value_name = "Game Name")]
+ name: String,
+
+ #[arg(value_name = "Key")]
+ key: u8,
+}
+
+#[derive(Args, Debug)]
+struct ListKeyArgs{
+
+ #[arg(value_name = "Game Name")]
+ name: String
+}
+
+#[derive(Args, Debug)]
+struct ConnectArgs {
+
+ #[arg(short, long, value_name = "Account")]
+ account: Option<String>,
+
+ #[arg(short, long, value_name = "Address")]
+ tcp_addr: Option<String>,
+
+ #[arg(short, long, value_name = "Methods")]
+ method: Option<String>,
+
+ #[arg(long)]
+ cmd: bool,
+
+ #[arg(long)]
+ debug: bool,
+}
+
+#[derive(Args, Debug, Clone)]
+struct ListenArgs {
+
+ #[arg(value_name = "Game")]
+ game: String,
+
+ #[arg(short = 'a', long, value_name = "Address")]
+ tcp_addr: Option<String>,
+
+ #[arg(long)]
+ cmd: bool,
+
+ #[arg(long)]
+ tcp: bool,
+
+ #[arg(long)]
+ bluetooth: bool,
+
+ #[arg(long)]
+ usb: bool,
+
+ #[arg(long)]
+ debug: bool,
+}
+
+#[derive(Default, Serialize, Deserialize, PartialEq, Debug)]
+struct LocalData {
+ game_data: LocalGameData,
+ controller_data: LocalControllerData,
+}
+
+#[derive(Default, Serialize, Deserialize, PartialEq, Debug)]
+struct LocalGameData {
+ games: HashMap<String, GameData>
+}
+
+#[derive(Default, Serialize, Deserialize, PartialEq, Debug)]
+struct LocalControllerData {
+ players: HashMap<String, Player>
+}
+
+fn main () {
+ let cli = Padc::parse();
+
+ let mut data = read();
+
+ match cli.command {
+ Commands::GenerateCompletion {shell} => {
+ let cmd = Padc::command();
+ generate(shell, &mut cmd.clone(), "padc", &mut io::stdout());
+ }
+
+ Commands::Account(cmds) => {
+ match cmds {
+ AccountCommands::Add(args) => {
+ add_player(&mut data, args.account, args.password);
+ }
+
+ AccountCommands::Remove(args) => {
+ remove_player(&mut data, args.account, args.password);
+ }
+
+ AccountCommands::Edit(args) => {
+ edit_player(&mut data, args);
+ }
+ }
+ }
+
+ Commands::Accounts => {
+ for player in data.controller_data.players.values() {
+ let id = &player.account.id;
+ if player.customize.is_some() {
+ let nickname = &player.clone().customize.unwrap().nickname;
+ println!("{}({})", id, nickname);
+ } else {
+ println!("{}", id);
+ }
+ }
+ }
+
+ Commands::Game(cmds) => {
+ match cmds {
+ GameCommands::Add(args) => {
+ let name = process_id_text(args.name);
+ data.game_data.games.entry(name.clone())
+ .or_insert_with(GameData::default);
+ println!("Game configuration added or reset: \"{}\"", name.clone());
+ }
+
+ GameCommands::Remove(args) => {
+ if ! args.confirm {
+ if ! confirm("Confirm ") {
+ exit(1);
+ }
+ }
+
+ let name = process_id_text(args.name);
+ let game = data.game_data.games.remove(&name);
+ if game.is_none() {
+ eprintln!("Removal of the game \"{}\" failed: game not found.", name.clone());
+ } else {
+ println!("The game \"{}\" has been removed.", name.clone());
+ }
+ }
+
+ GameCommands::Edit(args) => {
+ let name = process_id_text(args.name);
+ let game = data.game_data.games.get_mut(&name);
+ if game.is_none() {
+ eprintln!("Edit the game \"{}\" failed: game not found.", name.clone());
+ } else {
+ let game = game.unwrap();
+ game.info(args.key.clone(), args.value.clone());
+ println!("Set the game info \"{}\" to \"{}\".", args.key, args.value);
+ }
+ }
+
+ GameCommands::Register(cmds) => {
+ match cmds {
+ RegisterKeysCommands::Button(cmds) => {
+ manage_keys(&mut data, cmds,
+ |game| &mut game.control.button_keys);
+ }
+
+ RegisterKeysCommands::Axis(cmds) => {
+ manage_keys(&mut data, cmds,
+ |game| &mut game.control.axis_keys);
+ }
+
+ RegisterKeysCommands::Direction(cmds) => {
+ manage_keys(&mut data, cmds,
+ |game| &mut game.control.direction_keys);
+ }
+ }
+ }
+ }
+ }
+
+ Commands::Games => {
+ for (key, _) in data.game_data.games.iter() {
+ println!("{}", key);
+ }
+ }
+
+ Commands::Connect(args) => {
+ connect(&mut data, args);
+ }
+
+ Commands::Listen(args) => {
+ let archive = listen(&mut data, args.clone());
+ if let Some(archive) = archive {
+ if let Some(game) = data.game_data.games.get_mut(&process_id_text(args.game)) {
+ game.archive = archive;
+ info!("Game data archived.");
+ }
+ }
+ }
+ }
+
+ write(data);
+}
+
+fn connect(data: &mut LocalData, args: ConnectArgs) {
+
+ let mut result: Option<&Player> = None;
+ if args.account.is_some() {
+ // Account specified
+ let id = &args.account.unwrap();
+ let player = data.controller_data.players.get(id);
+
+ // Account not found
+ if player.is_none() {
+ eprintln!("Account not found: \"{}\"", id);
+ exit(1);
+ } else {
+ // Account exists
+ result = Some(player.unwrap());
+ }
+ } else {
+ // Account not specified
+ for found in data.controller_data.players.values() {
+ // Found a replaceable account
+ result = Some(found);
+ println!("Account not specified! Using account \"{}\" instead!", found.account.id);
+ break;
+ }
+ // No replaceable account found
+ if result.is_none() {
+ eprintln!("Cannot find any replaceable account! Please ensure at least one account exists locally!");
+ exit(1);
+ }
+ }
+
+ let player = result.unwrap().clone();
+
+ let mut controller = ControllerData::default();
+ controller.bind_player(player);
+
+ let runtime = controller.runtime();
+
+ let method = args.method.unwrap_or("tcp".to_string());
+ let mut entry: Option<NoGamepadsService> = None;
+
+ match method.trim().to_lowercase().as_str() {
+ "tcp" => {
+ println!("Using TCP connection.");
+ let mut client = PadClientNetwork::build(Arc::clone(&runtime));
+ let addr = args.tcp_addr.unwrap_or(format!("127.0.0.1:{}", DEFAULT_PORT));
+ client.bind_addr(SocketAddr::from_str(&addr).unwrap_or(
+ SocketAddr::from(([127, 0, 0, 1], DEFAULT_PORT)),
+ ));
+
+ entry = Some(client.build_entry());
+ },
+
+ "bluetooth" => {
+ println!("Using Bluetooth connection.");
+ // TODO :: BLUETOOTH METHOD
+ },
+
+ "usb" => {
+ println!("Using USB connection.");
+ // TODO :: USB METHOD
+ },
+
+ _ => {
+ eprintln!("Unknown connection method: \"{}\"", method);
+ exit(1);
+ }
+ }
+
+ if let Some(entry) = entry {
+ let mut services = Vec::new();
+ services.push(entry);
+
+ if args.cmd {
+ println!("Enable command line.");
+
+ services.push(RuntimeConsole::build(
+ ControllerCli::command(), "ControllerCli".to_string(), Arc::clone(&runtime),
+
+ // Process command line
+ |runtime, cmd| {
+ process_controller_cli(runtime, cmd)
+ },
+
+ // Check close
+ |runtime| {
+ let mut close = false;
+ entry_mutex!(runtime, |guard| {
+ close = guard.close.load(SeqCst);
+ });
+ close
+ },
+
+ // Close
+ |runtime| {
+ entry_mutex!(runtime, |guard| {
+ guard.close();
+ });
+ },
+
+ ).build_entry());
+ }
+
+ if args.debug {
+ logger_build(LevelFilter::Trace);
+ } else {
+ logger_build(LevelFilter::Info);
+ }
+
+ ServiceRunner::run(services);
+ }
+}
+
+fn listen(data: &mut LocalData, args: ListenArgs) -> Option<GameRuntimeDataArchive> {
+ let id = process_id_text(args.game);
+ let game = data.game_data.games.get(&id);
+ if game.is_none() {
+ eprintln!("Game not found: \"{}\"", id);
+ exit(1);
+ }
+
+ let game_data = game.unwrap().clone();
+
+ let runtime = game_data.runtime();
+
+ let mut services = Vec::new();
+
+ if args.tcp {
+ let mut server = PadServerNetwork::build(Arc::clone(&runtime));
+ if args.tcp_addr.is_some() {
+ let addr = SocketAddr::from_str(&args.tcp_addr.unwrap())
+ .unwrap_or(SocketAddr::from(([127, 0, 0, 1], DEFAULT_PORT)));
+ server.bind_ip(addr.ip());
+ server.bind_port(addr.port());
+ }
+ services.push(server.build_entry());
+ println!("Setup TCP Service!")
+ }
+
+ if args.bluetooth {
+
+ }
+
+ if args.usb {
+
+ }
+
+ if args.cmd {
+ services.push(RuntimeConsole::build(
+ GameCli::command(), "GameCli".to_string(), Arc::clone(&runtime),
+
+ // Process command line
+ |runtime, cmd| {
+ process_game_cli(runtime, cmd)
+ },
+
+ // Check close
+ |runtime| {
+ let mut close = false;
+ entry_mutex!(runtime, |guard| {
+ close = guard.data.close.load(SeqCst);
+ });
+ close
+ },
+
+ // Close
+ |runtime| {
+ entry_mutex!(runtime, |guard| {
+ guard.close_game();
+ });
+ },
+
+ ).build_entry());
+ }
+
+ if args.debug {
+ logger_build(LevelFilter::Trace);
+ } else {
+ logger_build(LevelFilter::Info);
+ }
+
+ ServiceRunner::run(services);
+
+ let mut archived_data: Option<GameRuntimeDataArchive> = None;
+ if let Ok(mutex) = Arc::try_unwrap(runtime) {
+ let rt = mutex.into_inner()
+ .unwrap_or_else(|poison_error| poison_error.into_inner());
+ archived_data = Some(GameRuntimeDataArchive::from(rt.data));
+ }
+ archived_data
+}
+
+fn add_player(data: &mut LocalData, account_args: String, password_args: Option<String>) {
+ if data.controller_data.players.contains_key(process_id_text(account_args.clone()).as_str()) {
+ eprintln!("This account already exists. Please do not create it again.");
+ exit(1);
+ } else {
+ // Read password
+ let mut password = "".to_string();
+ if password_args.is_none() {
+ let input = read_password_and_confirm("Enter password: ", "Confirm: ");
+ if input.is_some() {
+ password = input.unwrap();
+ }
+ } else if password_args.is_some() {
+ password = password_args.unwrap();
+ }
+
+ // Create player
+ let player = Player::register(account_args, password);
+ let player_key = player.clone().account.id;
+
+ data.controller_data.players.insert(player_key, player.clone());
+ }
+}
+
+fn remove_player(data: &mut LocalData, account_args: String, password_args: Option<String>) {
+ // Read password
+ let mut password = "".to_string();
+ if password_args.is_none() {
+ password = read_password("Enter password: ").unwrap_or("".to_string());
+ } else if password_args.is_some() {
+ password = password_args.unwrap();
+ }
+
+ // Remove
+ let player_id = process_id_text(account_args.clone());
+
+ let player = data.controller_data.players.get(&player_id);
+ if player.is_none() {
+ eprintln!("Failed to remove account \"{}\": Account not found.", account_args.clone());
+ exit(1);
+ }
+ let player = player.unwrap();
+
+ if player.check(password.clone()) {
+ data.controller_data.players.remove(player_id.as_str());
+ println!("The account \"{}\" has been removed!", player_id);
+ } else {
+ eprintln!("Failed to remove account \"{}\": Password is incorrect!", player_id)
+ }
+}
+
+fn edit_player(data: &mut LocalData, args: EditAccountArgs) {
+ let account_id = process_id_text(args.account);
+ if ! data.controller_data.players.contains_key(&account_id) {
+ eprintln!("Edit failed: Account \"{}\" not found!", account_id);
+ exit(1);
+ }
+
+ let player = data.controller_data.players.get_mut(&account_id).unwrap();
+
+ if args.nickname.is_some() {
+ let nickname = args.nickname.unwrap();
+ player.nickname(&nickname);
+ println!("Change the nickname of account \"{}\" to \"{}\"", account_id, &nickname);
+ }
+
+ if args.color.is_some() {
+ let color = args.color.unwrap();
+ let hue = color[0].round().clamp(0.0, 360.0);
+ let sat = color[1].clamp(0.0, 1.0);
+ let val = color[2].clamp(0.0, 1.0);
+ player.hsv(hue as i32, sat, val);
+ println!("Change the color of account \"{}\" to \"{}\"", account_id, hsv_to_hex(hue as i32, sat, val));
+ }
+}
+
+macro_rules! check_game {
+ ($data:expr, $args:expr, |$game:ident| $code:block) => {
+ let name = process_id_text($args.name);
+ let game = $data.game_data.games.get_mut(&name);
+ if game.is_none() {
+ eprintln!("Edit the game \"{}\" failed: game not found.", name.clone());
+ exit(1);
+ } else {
+ let $game = game.unwrap();
+ $code
+ }
+ };
+}
+
+fn manage_keys(data: &mut LocalData, cmds: KeyManageCommands, get_map: fn(game: &mut GameData) -> &mut HashMap<u8, String>) {
+ match cmds {
+ KeyManageCommands::Add(args) => {
+ check_game!(data, args, |game| {
+ get_map(game).entry(args.key)
+ .or_insert_with(|| args.key_name.clone());
+ println!("Registered key \"{}\"", args.key_name);
+ });
+ }
+
+ KeyManageCommands::Remove(args) => {
+ check_game!(data, args, |game| {
+ let result = get_map(game).remove(&args.key);
+ if result.is_some() {
+ println!("Removed key \"{}\"", result.unwrap());
+ }
+ });
+ }
+
+ KeyManageCommands::List(args) => {
+ check_game!(data, args, |game| {
+ for (key, key_name) in get_map(game).iter() {
+ println!("{} - \"{}\"", key, key_name);
+ }
+ });
+ }
+ }
+}
+
+fn local_config() -> PathBuf {
+ current_dir().unwrap().join("../../../../nogamepads.yaml")
+}
+
+fn read() -> LocalData {
+ let file_path = local_config();
+
+ if ! file_path.exists() {
+ let data = LocalData::default();
+ let content = serde_yaml::to_string(&data).unwrap();
+ File::create(&file_path).unwrap().write_all(content.as_bytes()).unwrap();
+ data
+ } else {
+ let file = File::open(&file_path).unwrap();
+ let reader = BufReader::new(file);
+ serde_yaml::from_reader(reader).unwrap()
+ }
+}
+
+fn write(config: LocalData) {
+ let file_path = local_config();
+ let content = serde_yaml::to_string(&config).unwrap();
+
+ File::create(file_path).unwrap().write_all(content.as_bytes()).unwrap();
+}
+
+fn hsv_to_hex(h: i32, s: f64, v: f64) -> String {
+ let h = (h as f64).clamp(0.0, 360.0);
+ let s = s.clamp(0.0, 1.0);
+ let v = v.clamp(0.0, 1.0);
+
+ let c = v * s;
+ let x = c * (1.0 - ((h / 60.0) % 2.0 - 1.0).abs());
+ let m = v - c;
+
+ let (r, g, b) = match (h / 60.0) as usize {
+ 0 => (c, x, 0.0),
+ 1 => (x, c, 0.0),
+ 2 => (0.0, c, x),
+ 3 => (0.0, x, c),
+ 4 => (x, 0.0, c),
+ 5 => (c, 0.0, x),
+ _ => (0.0, 0.0, 0.0),
+ };
+
+ let r = ((r + m) * 255.0).round() as u8;
+ let g = ((g + m) * 255.0).round() as u8;
+ let b = ((b + m) * 255.0).round() as u8;
+ format!("#{:02X}{:02X}{:02X}", r, g, b)
+} \ No newline at end of file