diff --git a/Cargo.lock b/Cargo.lock index 40e70df5c8..238ee3965d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1510,6 +1510,12 @@ dependencies = [ "cfg-if 1.0.0", ] +[[package]] +name = "endian-type" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c34f04666d835ff5d62e058c3995147c06f42fe86ff053337632bca83e42702d" + [[package]] name = "enum-iterator" version = "0.6.0" @@ -1708,6 +1714,16 @@ dependencies = [ "percent-encoding", ] +[[package]] +name = "fs2" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9564fc758e15025b46aa6643b1b77d047d1a56a1aea6e01002ac0c7026876213" +dependencies = [ + "libc", + "winapi 0.3.9", +] + [[package]] name = "fsevent" version = "0.4.0" @@ -3137,6 +3153,15 @@ dependencies = [ "winapi 0.3.9", ] +[[package]] +name = "nibble_vec" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77a5d83df9f36fe23f0c3648c6bbb8b0298bb5f1939c8f2704431371f4b84d43" +dependencies = [ + "smallvec", +] + [[package]] name = "nix" version = "0.15.0" @@ -3972,6 +3997,16 @@ version = "0.6.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "643f8f41a8ebc4c5dc4515c82bb8abd397b527fc20fd681b7c011c2aee5d44fb" +[[package]] +name = "radix_trie" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c069c179fcdc6a2fe24d8d18305cf085fdbd4f922c041943e203685d6a1c58fd" +dependencies = [ + "endian-type", + "nibble_vec", +] + [[package]] name = "rand" version = "0.7.3" @@ -4333,6 +4368,29 @@ dependencies = [ "winapi 0.3.9", ] +[[package]] +name = "rustyline" +version = "8.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9e1b597fcd1eeb1d6b25b493538e5aa19629eb08932184b85fef931ba87e893" +dependencies = [ + "bitflags", + "cfg-if 1.0.0", + "dirs-next", + "fs2", + "libc", + "log", + "memchr", + "nix 0.20.0", + "radix_trie", + "scopeguard", + "smallvec", + "unicode-segmentation", + "unicode-width", + "utf8parse", + "winapi 0.3.9", +] + [[package]] name = "ryu" version = "1.0.5" @@ -5410,6 +5468,12 @@ dependencies = [ "percent-encoding", ] +[[package]] +name = "utf8parse" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "936e4b492acfd135421d8dca4b1aa80a7bfc26e702ef3af710e0752684df5372" + [[package]] name = "uuid" version = "0.8.2" @@ -5458,6 +5522,30 @@ dependencies = [ "static_assertions", ] +[[package]] +name = "veloren-botclient" +version = "0.8.0" +dependencies = [ + "authc", + "clap", + "hashbrown", + "ron", + "rustyline", + "serde", + "termcolor", + "tokio 1.3.0", + "tracing", + "tracing-appender", + "tracing-log", + "tracing-subscriber", + "veloren-client", + "veloren-common", + "veloren-common-base", + "veloren-common-ecs", + "veloren-common-net", + "veloren-common-sys", +] + [[package]] name = "veloren-client" version = "0.8.0" diff --git a/Cargo.toml b/Cargo.toml index 793897b575..271f814cb7 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -2,22 +2,23 @@ cargo-features = ["named-profiles","profile-overrides"] [workspace] members = [ - "common", - "common/base", - "common/ecs", - "common/net", - "common/sys", - "client", - "plugin/api", - "plugin/derive", - "plugin/rt", - "server", - "server-cli", - "voxygen", - "voxygen/anim", - "world", - "network", - "network/protocol" + "common", + "common/base", + "common/ecs", + "common/net", + "common/sys", + "client", + "botclient", + "plugin/api", + "plugin/derive", + "plugin/rt", + "server", + "server-cli", + "voxygen", + "voxygen/anim", + "world", + "network", + "network/protocol" ] # default profile for devs, fast to compile, okay enough to run, no debug information diff --git a/botclient/Cargo.toml b/botclient/Cargo.toml new file mode 100644 index 0000000000..999a48b678 --- /dev/null +++ b/botclient/Cargo.toml @@ -0,0 +1,29 @@ +[package] +authors = ["Avi Weinstock "] +edition = "2018" +name = "veloren-botclient" +version = "0.8.0" + +[dependencies] +authc = { git = "https://gitlab.com/veloren/auth.git", rev = "bffb5181a35c19ddfd33ee0b4aedba741aafb68d" } +client = { package = "veloren-client", path = "../client" } +common = { package = "veloren-common", path = "../common", features = ["no-assets"] } +common-base = { package = "veloren-common-base", path = "../common/base" } +common-ecs = {package = "veloren-common-ecs", path = "../common/ecs"} +common-net = { package = "veloren-common-net", path = "../common/net" } +common-sys = { package = "veloren-common-sys", path = "../common/sys", default-features = false } + +hashbrown = {version = "0.9", features = ["rayon", "serde", "nightly"]} +tokio = { version = "1", default-features = false, features = ["rt-multi-thread"] } +serde = {version = "1.0", features = [ "rc", "derive" ]} +ron = {version = "0.6", default-features = false} + +clap = "2.33" +rustyline = "8.0.0" + +# logging +termcolor = "1.1" +tracing = "0.1" +tracing-appender = "0.1" +tracing-log = "0.1.1" +tracing-subscriber = {version = "0.2.3", default-features = false, features = ["env-filter", "fmt", "chrono", "ansi", "smallvec", "tracing-log"]} diff --git a/botclient/src/main.rs b/botclient/src/main.rs new file mode 100644 index 0000000000..762cd49c1f --- /dev/null +++ b/botclient/src/main.rs @@ -0,0 +1,241 @@ +#[macro_use] extern crate serde; + +use authc::AuthClient; +use clap::{App, AppSettings, Arg, SubCommand}; +use client::{addr::ConnectionArgs, Client}; +use common::{ + comp, + clock::Clock, +}; +use hashbrown::HashMap; +use serde::{Deserialize, Serialize}; +use std::sync::Arc; +use std::time::Duration; +use tokio::runtime::Runtime; +use tracing::{error, info, warn}; + +mod settings; + +use settings::Settings; + +pub fn init_logging() { + use termcolor::{ColorChoice, StandardStream}; + use tracing::Level; + use tracing_subscriber::{filter::LevelFilter, EnvFilter, FmtSubscriber}; + const RUST_LOG_ENV: &str = "RUST_LOG"; + let filter = EnvFilter::from_env(RUST_LOG_ENV).add_directive(LevelFilter::INFO.into()); + let subscriber = FmtSubscriber::builder() + .with_max_level(Level::ERROR) + .with_env_filter(filter); + + subscriber + .with_writer(|| StandardStream::stdout(ColorChoice::Auto)) + .init(); +} + +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct BotCreds { + username: String, + password: String, +} + +pub fn main() { + init_logging(); + + let settings = Settings::load(); + info!("Settings: {:?}", settings); + + let mut bc = BotClient::new(settings); + bc.repl(); +} + +pub struct BotClient { + settings: Settings, + readline: rustyline::Editor<()>, + runtime: Arc, + menu_client: Client, + bot_clients: HashMap, + clock: Clock, +} + +pub fn make_client(runtime: &Arc, server: &str) -> Client { + let runtime2 = Arc::clone(&runtime); + let view_distance: Option = None; + runtime.block_on(async { + let connection_args = ConnectionArgs::resolve(server, false) + .await + .expect("DNS resolution failed"); + Client::new(connection_args, view_distance, runtime2) + .await + .expect("Failed to connect to server") + }) +} + +impl BotClient { + pub fn new(settings: Settings) -> BotClient { + let readline = rustyline::Editor::<()>::new(); + let runtime = Arc::new(Runtime::new().unwrap()); + let menu_client: Client = make_client(&runtime, &settings.server); + let clock = Clock::new(Duration::from_secs_f64(1.0 / 60.0)); + BotClient { + settings, + readline, + runtime, + menu_client, + bot_clients: HashMap::new(), + clock, + } + } + + pub fn repl(&mut self) { + loop { + match self.readline.readline("\n\nbotclient> ") { + Ok(cmd) => { + let keep_going = self.process_command(&cmd); + self.readline.add_history_entry(cmd); + if !keep_going { + break; + } + }, + Err(_) => break, + } + } + } + + pub fn process_command(&mut self, cmd: &str) -> bool { + let matches = App::new("veloren-botclient") + .version(common::util::DISPLAY_VERSION_LONG.as_str()) + .author("The veloren devs ") + .about("The veloren bot client allows logging in as a horde of bots for load-testing") + .setting(AppSettings::NoBinaryName) + .subcommand( + SubCommand::with_name("register") + .about("Register more bots with the auth server") + .args(&[ + Arg::with_name("prefix").required(true), + Arg::with_name("password").required(true), + Arg::with_name("count"), + ]), + ) + .subcommand( + SubCommand::with_name("login") + .about("Login all registered bots whose username starts with a prefix") + .args(&[Arg::with_name("prefix").required(true)]), + ) + .subcommand( + SubCommand::with_name("tick") + .about("Handle ticks for all logged in bots") + ) + .get_matches_from_safe(cmd.split(" ")); + use clap::ErrorKind::*; + match matches { + Ok(matches) => match matches.subcommand() { + ("register", Some(matches)) => self.handle_register( + matches.value_of("prefix").unwrap(), + matches.value_of("password").unwrap(), + matches + .value_of("count") + .and_then(|x| x.parse::().ok()), + ), + ("login", Some(matches)) => self.handle_login(matches.value_of("prefix").unwrap()), + ("tick", _) => self.handle_tick(), + _ => {}, + }, + Err(e) + if [HelpDisplayed, MissingRequiredArgument, UnknownArgument].contains(&e.kind) => + { + println!("{}", e.message); + } + Err(e) => { + error!("{:?}", e); + return false; + }, + } + true + } + + pub fn handle_register(&mut self, prefix: &str, password: &str, count: Option) { + let usernames = match count { + Some(n) => (0..n) + .into_iter() + .map(|i| format!("{}{}", prefix, i)) + .collect::>(), + None => vec![prefix.to_string()], + }; + info!("usernames: {:?}", usernames); + if let Some(auth_addr) = self.menu_client.server_info().auth_provider.as_ref() { + let authc = AuthClient::new(&*auth_addr).expect("couldn't connect to auth_addr"); + for username in usernames.iter() { + if self + .settings + .bot_logins + .iter() + .any(|x| &*x.username == &*username) + { + continue; + } + match authc.register(username, password) { + Ok(()) => { + self.settings.bot_logins.push(BotCreds { + username: username.to_string(), + password: password.to_string(), + }); + self.settings.save_to_file_warn(); + }, + Err(e) => { + warn!("error registering {:?}: {:?}", username, e); + break; + }, + } + } + } else { + warn!("Server's auth_provider is None"); + } + } + + pub fn client_for_bot(&mut self, username: &str) -> &mut Client { + let runtime = Arc::clone(&self.runtime); + let server = self.settings.server.clone(); + self.bot_clients.entry(username.to_string()).or_insert_with(|| make_client(&runtime, &server)) + } + + pub fn handle_login(&mut self, prefix: &str) { + let creds: Vec<_> = self.settings.bot_logins.iter().filter(|x| x.username.starts_with(prefix)).cloned().collect(); + for cred in creds.iter() { + let runtime = Arc::clone(&self.runtime); + let client = self.client_for_bot(&cred.username); + // TODO: log the clients in in parallel instead of in series + if let Err(e) = runtime.block_on(client.register(cred.username.clone(), cred.password.clone(), |_| true)) { + warn!("error logging in {:?}: {:?}", cred.username, e); + } + /*let body = comp::body::biped_large::Body { + species: comp::body::biped_large::Species::Dullahan, + body_type: comp::body::biped_large::BodyType::Male, + };*/ + let body = comp::body::humanoid::Body { + species: comp::body::humanoid::Species::Human, + body_type: comp::body::humanoid::BodyType::Male, + hair_style: 0, + beard: 0, + eyes: 0, + accessory: 0, + hair_color: 0, + skin: 0, + eye_color: 0, + }; + client.create_character(cred.username.clone(), Some("common.items.weapons.sword.starter".to_string()), body.into()); + //client.create_character(cred.username.clone(), Some("common.items.debug.admin_stick".to_string()), body.into()); + } + } + + // TODO: maybe do this automatically in a threadpool instead of as a command + pub fn handle_tick(&mut self) { + self.clock.tick(); + for (username, client) in self.bot_clients.iter_mut() { + info!("cl {:?}: {:?}", username, client.character_list()); + let msgs: Result, client::Error> = + client.tick(comp::ControllerInputs::default(), self.clock.dt(), |_| {}); + info!("msgs {:?}: {:?} {:?}", username, msgs, client.character_list()); + } + } +} diff --git a/botclient/src/settings.rs b/botclient/src/settings.rs new file mode 100644 index 0000000000..1fa34730f2 --- /dev/null +++ b/botclient/src/settings.rs @@ -0,0 +1,74 @@ +use super::BotCreds; +use std::{fs, path::PathBuf}; +use tracing::warn; + +pub fn data_dir() -> PathBuf { + let mut path = common_base::userdata_dir_workspace!(); + path.push("botclient"); + path +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct Settings { + pub server: String, + pub bot_logins: Vec, +} + +impl Default for Settings { + fn default() -> Settings { + Settings { + server: "localhost".to_string(), + bot_logins: Vec::new(), + } + } +} + +impl Settings { + pub fn load() -> Self { + let path = Self::get_settings_path(); + + if let Ok(file) = fs::File::open(&path) { + match ron::de::from_reader(file) { + Ok(s) => return s, + Err(e) => { + warn!(?e, "Failed to parse setting file! Fallback to default."); + // Rename the corrupted settings file + let mut new_path = path.to_owned(); + new_path.pop(); + new_path.push("settings.invalid.ron"); + if let Err(e) = std::fs::rename(&path, &new_path) { + warn!(?e, ?path, ?new_path, "Failed to rename settings file."); + } + }, + } + } + // This is reached if either: + // - The file can't be opened (presumably it doesn't exist) + // - Or there was an error parsing the file + let default_settings = Self::default(); + default_settings.save_to_file_warn(); + default_settings + } + + pub fn save_to_file_warn(&self) { + if let Err(e) = self.save_to_file() { + warn!(?e, "Failed to save settings"); + } + } + + fn save_to_file(&self) -> std::io::Result<()> { + let path = Self::get_settings_path(); + if let Some(dir) = path.parent() { + fs::create_dir_all(dir)?; + } + + let ron = ron::ser::to_string_pretty(self, ron::ser::PrettyConfig::default()).unwrap(); + fs::write(path, ron.as_bytes()) + } + + pub fn get_settings_path() -> PathBuf { + let mut path = data_dir(); + path.push("settings.ron"); + path + } +} diff --git a/client/src/lib.rs b/client/src/lib.rs index 90433aa15f..4a1e6ca740 100644 --- a/client/src/lib.rs +++ b/client/src/lib.rs @@ -68,6 +68,7 @@ use vek::*; const PING_ROLLING_AVERAGE_SECS: usize = 10; +#[derive(Debug)] pub enum Event { Chat(comp::ChatMsg), InviteComplete { @@ -177,7 +178,7 @@ pub struct Client { /// Holds data related to the current players characters, as well as some /// additional state to handle UI. -#[derive(Default)] +#[derive(Debug, Default)] pub struct CharacterList { pub characters: Vec, pub loading: bool,