Draft of botclient.

This commit is contained in:
Avi Weinstock 2021-03-10 13:32:45 -05:00 committed by Marcel Märtens
parent d8f2d55923
commit 7301182695
6 changed files with 451 additions and 17 deletions

88
Cargo.lock generated
View File

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

View File

@ -8,6 +8,7 @@ members = [
"common/net",
"common/sys",
"client",
"botclient",
"plugin/api",
"plugin/derive",
"plugin/rt",

29
botclient/Cargo.toml Normal file
View File

@ -0,0 +1,29 @@
[package]
authors = ["Avi Weinstock <aweinstock314@gmail.com>"]
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"]}

241
botclient/src/main.rs Normal file
View File

@ -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<Runtime>,
menu_client: Client,
bot_clients: HashMap<String, Client>,
clock: Clock,
}
pub fn make_client(runtime: &Arc<Runtime>, server: &str) -> Client {
let runtime2 = Arc::clone(&runtime);
let view_distance: Option<u32> = 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 <https://gitlab.com/veloren/veloren>")
.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::<usize>().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<usize>) {
let usernames = match count {
Some(n) => (0..n)
.into_iter()
.map(|i| format!("{}{}", prefix, i))
.collect::<Vec<String>>(),
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<Vec<client::Event>, client::Error> =
client.tick(comp::ControllerInputs::default(), self.clock.dt(), |_| {});
info!("msgs {:?}: {:?} {:?}", username, msgs, client.character_list());
}
}
}

74
botclient/src/settings.rs Normal file
View File

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

View File

@ -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<CharacterItem>,
pub loading: bool,