diff --git a/Cargo.lock b/Cargo.lock index 31d36a9..23aae7b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1179,7 +1179,6 @@ dependencies = [ "anyhow", "azalea", "clap", - "futures", "http-body-util", "hyper", "hyper-util", diff --git a/Cargo.toml b/Cargo.toml index e6f67b5..b5c811c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -19,7 +19,6 @@ strip = true anyhow = "1" azalea = { git = "https://github.com/azalea-rs/azalea.git" } clap = { version = "4", features = ["derive"] } -futures = "0" http-body-util = "0" hyper = { version = "1", features = ["server"] } hyper-util = "0" diff --git a/src/arguments.rs b/src/arguments.rs index 85de8e5..626f85e 100644 --- a/src/arguments.rs +++ b/src/arguments.rs @@ -9,6 +9,6 @@ pub struct Arguments { pub script: Option, /// Socket address to bind HTTP server to - #[arg(short, long)] - pub address: Option, + #[arg(short = 'a', long)] + pub http_address: Option, } diff --git a/src/commands.rs b/src/commands.rs index aa7261c..4015952 100644 --- a/src/commands.rs +++ b/src/commands.rs @@ -19,13 +19,14 @@ pub struct CommandSource { impl CommandSource { pub fn reply(&self, message: &str) { - if self.message.is_whisper() + let response = if self.message.is_whisper() && let Some(username) = self.message.username() { - self.client.chat(&format!("/w {username} {message}")); + &format!("/w {username} {message}") } else { - self.client.chat(message); - } + message + }; + self.client.chat(response); } pub fn _entity(&mut self) -> Option { diff --git a/src/events.rs b/src/events.rs index 69a6d43..23c8856 100644 --- a/src/events.rs +++ b/src/events.rs @@ -1,9 +1,9 @@ -use crate::{State, commands::CommandSource, http::handle, scripting}; +use crate::{State, commands::CommandSource, http::serve, scripting}; use azalea::prelude::*; use hyper::{server::conn::http1, service::service_fn}; use hyper_util::rt::TokioIo; -use log::{error, info}; use mlua::Function; +use log::{debug, error, info, trace}; use tokio::net::TcpListener; pub async fn handle_event(client: Client, event: Event, state: State) -> anyhow::Result<()> { @@ -33,38 +33,54 @@ pub async fn handle_event(client: Client, event: Event, state: State) -> anyhow: state, } .reply(&format!("{error:?}")); - }; + } } } Event::Init => { + debug!("client initialized"); + globals.set( "client", scripting::client::Client { inner: Some(client), }, )?; - globals.get::("Init")?.call::<()>(())?; + if let Ok(on_init) = globals.get::("on_init") + && let Err(error) = on_init.call::<()>(()) + { + error!("failed to call lua on_init function: {error:?}"); + }; + + if let Some(address) = state.http_address { + let listener = TcpListener::bind(address).await.map_err(|error| { + error!("failed to listen on {address}: {error:?}"); + error + })?; + debug!("http server listening on {address}"); - if let Some(address) = state.address { - let listener = TcpListener::bind(address).await?; loop { - let (stream, _) = listener.accept().await?; - let io = TokioIo::new(stream); + let (stream, peer) = listener.accept().await?; + trace!("http server got connection from {peer}"); - let state = state.clone(); + let conn_state = state.clone(); let service = service_fn(move |request| { - let state = state.clone(); - async move { handle(request, state).await } + let request_state = conn_state.clone(); + async move { serve(request, request_state).await } }); - if let Err(error) = http1::Builder::new().serve_connection(io, service).await { - error!("failed to serve connection: {error:?}"); - } + tokio::task::spawn(async move { + if let Err(error) = http1::Builder::new() + .serve_connection(TokioIo::new(stream), service) + .await + { + error!("failed to serve connection: {error:?}"); + } + }); } } } _ => (), - }; + } Ok(()) } diff --git a/src/http.rs b/src/http.rs index d774014..a57a5ff 100644 --- a/src/http.rs +++ b/src/http.rs @@ -5,24 +5,24 @@ use crate::{ use http_body_util::{BodyExt, Empty, Full, combinators::BoxBody}; use hyper::{Method, Request, Response, StatusCode, body::Bytes}; -pub async fn handle( +pub async fn serve( request: Request, state: State, ) -> Result>, hyper::Error> { let path = request.uri().path().to_owned(); - match (request.method(), path.as_str()) { - (&Method::POST, "/reload") => Ok(match reload(&state.lua.lock()) { + Ok(match (request.method(), path.as_str()) { + (&Method::POST, "/reload") => match reload(&state.lua.lock()) { Ok(()) => Response::new(empty()), Err(error) => status_code_response( StatusCode::INTERNAL_SERVER_ERROR, - Some(full(format!("{error:?}"))), + full(format!("{error:?}")), ), - }), + }, (&Method::POST, "/eval" | "/exec") => { let bytes = request.into_body().collect().await?.to_bytes(); - Ok(match std::str::from_utf8(&bytes) { + match std::str::from_utf8(&bytes) { Ok(code) => { let lua = state.lua.lock(); Response::new(full(match path.as_str() { @@ -31,26 +31,24 @@ pub async fn handle( _ => unreachable!(), })) } - Err(error) => { - return Ok(status_code_response( - StatusCode::BAD_REQUEST, - Some(full(format!("invalid utf-8 data received: {error:?}"))), - )); - } - }) + Err(error) => status_code_response( + StatusCode::BAD_REQUEST, + full(format!("invalid utf-8 data received: {error:?}")), + ), + } } - (&Method::GET, "/ping") => Ok(Response::new(full("pong!"))), + (&Method::GET, "/ping") => Response::new(full("pong!")), - _ => Ok(status_code_response(StatusCode::NOT_FOUND, None)), - } + _ => status_code_response(StatusCode::NOT_FOUND, empty()), + }) } fn status_code_response( status_code: StatusCode, - bytes: Option>, + bytes: BoxBody, ) -> Response> { - let mut response = Response::new(bytes.unwrap_or(empty())); + let mut response = Response::new(bytes); *response.status_mut() = status_code; response } diff --git a/src/main.rs b/src/main.rs index fb1439c..28fa6b0 100644 --- a/src/main.rs +++ b/src/main.rs @@ -12,62 +12,29 @@ use commands::{CommandSource, register}; use events::handle_event; use mlua::Lua; use parking_lot::Mutex; -use std::{net::SocketAddr, path::PathBuf, process::ExitCode, sync::Arc}; +use std::{net::SocketAddr, path::PathBuf, sync::Arc}; #[derive(Default, Clone, Component)] pub struct State { commands: Arc>>, lua: Arc>, - address: Option, + http_address: Option, } #[tokio::main] -async fn main() -> ExitCode { +async fn main() -> anyhow::Result<()> { let args = arguments::Arguments::parse(); - let lua = Lua::new(); + let script_path = args.script.unwrap_or(PathBuf::from("errornowatcher.lua")); - let config_path = args.script.unwrap_or(PathBuf::from("errornowatcher.lua")); - if let Err(error) = match &std::fs::read_to_string(&config_path) { - Ok(string) => lua.load(string).exec(), - Err(error) => { - eprintln!("failed to read {config_path:?}: {error:?}"); - return ExitCode::FAILURE; - } - } { - eprintln!("failed to execute configuration as lua code: {error:?}"); - return ExitCode::FAILURE; - } + let lua = Lua::new(); + lua.load(std::fs::read_to_string(&script_path)?).exec()?; let globals = lua.globals(); - let Ok(server) = globals.get::("SERVER") else { - eprintln!("no server defined in lua globals!"); - return ExitCode::FAILURE; - }; - let Ok(username) = globals.get::("USERNAME") else { - eprintln!("no username defined in lua globals!"); - return ExitCode::FAILURE; - }; + let server = globals.get::("SERVER")?; + let username = globals.get::("USERNAME")?; - if let Err(error) = globals.set("config_path", config_path) { - eprintln!("failed to set config_path in lua globals: {error:?}"); - return ExitCode::FAILURE; - }; - if let Err(error) = scripting::logging::init(&lua, &globals) { - eprintln!("failed to set up logging wrappers: {error:?}"); - return ExitCode::FAILURE; - }; - - let account = if username.contains('@') { - match Account::microsoft(&username).await { - Ok(a) => a, - Err(error) => { - eprintln!("failed to login using microsoft account: {error:?}"); - return ExitCode::FAILURE; - } - } - } else { - Account::offline(&username) - }; + globals.set("script_path", script_path)?; + scripting::logging::register(&lua, &globals)?; let mut commands = CommandDispatcher::new(); register(&mut commands); @@ -77,11 +44,18 @@ async fn main() -> ExitCode { .set_state(State { commands: Arc::new(commands), lua: Arc::new(Mutex::new(lua)), - address: args.address, + http_address: args.http_address, }) - .start(account, server.as_ref()) + .start( + if username.contains('@') { + Account::microsoft(&username).await? + } else { + Account::offline(&username) + }, + server.as_ref(), + ) .await; eprintln!("{error:?}"); - ExitCode::SUCCESS + Ok(()) } diff --git a/src/scripting/client.rs b/src/scripting/client.rs deleted file mode 100644 index cbe99bc..0000000 --- a/src/scripting/client.rs +++ /dev/null @@ -1,109 +0,0 @@ -use super::position::{from_table, to_table}; -use azalea::{ - BlockPos, Client as AzaleaClient, ClientInformation, - ecs::query::Without, - entity::{Dead, EntityKind, EntityUuid, Position, metadata::CustomName}, - pathfinder::goals::BlockPosGoal, - prelude::PathfinderClientExt, - world::MinecraftEntityId, -}; -use mlua::{Error, Function, Lua, Result, Table, UserData, UserDataRef}; - -pub struct Client { - pub inner: Option, -} - -impl UserData for Client { - fn add_fields>(fields: &mut F) { - fields.add_field_method_get("pos", |lua, this| { - let pos = this.inner.as_ref().unwrap().position(); - to_table(lua, pos.x, pos.y, pos.z) - }); - } - - fn add_methods>(methods: &mut M) { - methods.add_async_method("set_client_information", set_client_information); - methods.add_method("get_entity", get_entity); - methods.add_method_mut("get_entity_position", get_entity_position); - methods.add_method_mut("goto", goto); - methods.add_method_mut("stop", stop); - } -} - -async fn set_client_information( - _lua: Lua, - client: UserDataRef, - client_information: Table, -) -> Result<()> { - client - .inner - .as_ref() - .unwrap() - .set_client_information(ClientInformation { - view_distance: client_information.get("view_distance")?, - ..ClientInformation::default() - }) - .await - .unwrap(); - Ok(()) -} - -fn get_entity(lua: &Lua, client: &Client, filter_fn: Function) -> Result { - let mut ecs = client.inner.as_ref().unwrap().ecs.lock(); - let mut query = ecs.query_filtered::<( - &MinecraftEntityId, - &EntityUuid, - &EntityKind, - &Position, - &CustomName, - ), Without>(); - - for (&entity_id, entity_uuid, entity_kind, pos, custom_name) in query.iter(&ecs) { - let entity = lua.create_table()?; - - entity.set("id", entity_id.0)?; - entity.set("uuid", entity_uuid.to_string())?; - entity.set("kind", entity_kind.0.to_string())?; - entity.set("pos", to_table(lua, pos.x, pos.y, pos.z)?)?; - if let Some(n) = &**custom_name { - entity.set("custom_name", n.to_string())?; - } - - if filter_fn.call::(entity).unwrap() { - return Ok(entity_id.0); - }; - } - - Err(Error::RuntimeError("entity not found".to_string())) -} - -fn get_entity_position(lua: &Lua, client: &mut Client, entity_id: u32) -> Result { - let client = client.inner.as_mut().unwrap(); - let entity = client - .entity_by::, &MinecraftEntityId>(|query_entity_id: &&MinecraftEntityId| { - query_entity_id.0 == entity_id - }) - .unwrap(); - let pos = client.entity_component::(entity); - to_table(lua, pos.x, pos.y, pos.z) -} - -fn goto(_lua: &Lua, client: &mut Client, pos_table: Table) -> Result<()> { - let pos = from_table(&pos_table)?; - #[allow(clippy::cast_possible_truncation)] - client - .inner - .as_ref() - .unwrap() - .goto(BlockPosGoal(BlockPos::new( - pos.0 as i32, - pos.1 as i32, - pos.2 as i32, - ))); - Ok(()) -} - -fn stop(_lua: &Lua, client: &mut Client, _: ()) -> Result<()> { - client.inner.as_ref().unwrap().stop_pathfinding(); - Ok(()) -} diff --git a/src/scripting/client/mod.rs b/src/scripting/client/mod.rs new file mode 100644 index 0000000..ff3f7c9 --- /dev/null +++ b/src/scripting/client/mod.rs @@ -0,0 +1,68 @@ +mod pathfinding; +mod state; + +use super::{entity::Entity, position::Position}; +use azalea::{ + Client as AzaleaClient, + ecs::query::Without, + entity::{Dead, EntityKind, EntityUuid, Position as AzaleaPosition, metadata::CustomName}, + world::MinecraftEntityId, +}; +use mlua::{Function, Lua, Result, UserData}; + +pub struct Client { + pub inner: Option, +} + +impl UserData for Client { + fn add_fields>(fields: &mut F) { + fields.add_field_method_get("position", |_, this| { + let position = this.inner.as_ref().unwrap().position(); + Ok(Position { + x: position.x, + y: position.y, + z: position.z, + }) + }); + } + + fn add_methods>(methods: &mut M) { + methods.add_async_method("set_client_information", state::set_client_information); + methods.add_method("find_entities", find_entities); + methods.add_method("stop_pathfinding", pathfinding::stop_pathfinding); + methods.add_method_mut("goto", pathfinding::goto); + } +} + +fn find_entities(_lua: &Lua, client: &Client, filter_fn: Function) -> Result> { + let mut matched = Vec::new(); + + let mut ecs = client.inner.as_ref().unwrap().ecs.lock(); + let mut query = ecs.query_filtered::<( + &MinecraftEntityId, + &EntityUuid, + &EntityKind, + &AzaleaPosition, + &CustomName, + ), Without>(); + + for (&id, uuid, kind, position, custom_name) in query.iter(&ecs) { + let entity = Entity { + id: id.0, + uuid: uuid.to_string(), + kind: kind.to_string(), + position: Position { + x: position.x, + y: position.y, + z: position.z, + }, + custom_name: custom_name.as_ref().map(ToString::to_string), + }; + + if filter_fn.call::(entity.clone()).unwrap() { + matched.push(entity); + } + } + + Ok(matched) +} diff --git a/src/scripting/client/pathfinding.rs b/src/scripting/client/pathfinding.rs new file mode 100644 index 0000000..95fe164 --- /dev/null +++ b/src/scripting/client/pathfinding.rs @@ -0,0 +1,22 @@ +use super::{Client, Position}; +use azalea::{BlockPos, pathfinder::goals::BlockPosGoal, prelude::*}; +use mlua::{Lua, Result}; + +pub fn goto(_lua: &Lua, client: &mut Client, position: Position) -> Result<()> { + #[allow(clippy::cast_possible_truncation)] + client + .inner + .as_ref() + .unwrap() + .goto(BlockPosGoal(BlockPos::new( + position.x as i32, + position.y as i32, + position.z as i32, + ))); + Ok(()) +} + +pub fn stop_pathfinding(_lua: &Lua, client: &Client, _: ()) -> Result<()> { + client.inner.as_ref().unwrap().stop_pathfinding(); + Ok(()) +} diff --git a/src/scripting/client/state.rs b/src/scripting/client/state.rs new file mode 100644 index 0000000..cf7af8a --- /dev/null +++ b/src/scripting/client/state.rs @@ -0,0 +1,21 @@ +use super::Client; +use azalea::ClientInformation; +use mlua::{Lua, Result, Table, UserDataRef}; + +pub async fn set_client_information( + _lua: Lua, + client: UserDataRef, + client_information: Table, +) -> Result<()> { + client + .inner + .as_ref() + .unwrap() + .set_client_information(ClientInformation { + view_distance: client_information.get("view_distance")?, + ..ClientInformation::default() + }) + .await + .unwrap(); + Ok(()) +} diff --git a/src/scripting/entity.rs b/src/scripting/entity.rs new file mode 100644 index 0000000..d74a9e6 --- /dev/null +++ b/src/scripting/entity.rs @@ -0,0 +1,43 @@ +use super::position::Position; +use mlua::{FromLua, IntoLua, Lua, Result, Value}; + +#[derive(Clone)] +pub struct Entity { + pub id: u32, + pub uuid: String, + pub kind: String, + pub position: Position, + pub custom_name: Option, +} + +impl IntoLua for Entity { + fn into_lua(self, lua: &Lua) -> Result { + let entity = lua.create_table()?; + entity.set("id", self.id)?; + entity.set("uuid", self.uuid)?; + entity.set("kind", self.kind)?; + entity.set("position", self.position)?; + entity.set("custom_name", self.custom_name)?; + Ok(Value::Table(entity)) + } +} + +impl FromLua for Entity { + fn from_lua(value: Value, _lua: &Lua) -> Result { + if let Value::Table(table) = value { + Ok(Self { + id: table.get("id")?, + uuid: table.get("uuid")?, + kind: table.get("kind")?, + position: table.get("position")?, + custom_name: table.get("custom_name")?, + }) + } else { + Err(mlua::Error::FromLuaConversionError { + from: value.type_name(), + to: "Position".to_string(), + message: None, + }) + } + } +} diff --git a/src/scripting/logging.rs b/src/scripting/logging.rs index cb9bf7b..c0c78e9 100644 --- a/src/scripting/logging.rs +++ b/src/scripting/logging.rs @@ -1,7 +1,7 @@ use log::{debug, error, info, trace, warn}; use mlua::{Lua, Result, Table}; -pub fn init(lua: &Lua, globals: &Table) -> Result<()> { +pub fn register(lua: &Lua, globals: &Table) -> Result<()> { globals.set( "error", lua.create_function(|_, message: String| { diff --git a/src/scripting/mod.rs b/src/scripting/mod.rs index 498a20e..e4b1a84 100644 --- a/src/scripting/mod.rs +++ b/src/scripting/mod.rs @@ -1,4 +1,5 @@ pub mod client; +pub mod entity; pub mod logging; pub mod position; @@ -7,19 +8,19 @@ use mlua::Lua; #[derive(Debug)] #[allow(dead_code)] pub enum Error { - MissingGlobal(mlua::Error), - ReadFile(std::io::Error), - LoadChunk(mlua::Error), EvalChunk(mlua::Error), ExecChunk(mlua::Error), + LoadChunk(mlua::Error), + MissingPath(mlua::Error), + ReadFile(std::io::Error), } pub fn reload(lua: &Lua) -> Result<(), Error> { lua.load( &std::fs::read_to_string( lua.globals() - .get::("config_path") - .map_err(Error::MissingGlobal)?, + .get::("script_path") + .map_err(Error::MissingPath)?, ) .map_err(Error::ReadFile)?, ) diff --git a/src/scripting/position.rs b/src/scripting/position.rs index 89133ff..16df16e 100644 --- a/src/scripting/position.rs +++ b/src/scripting/position.rs @@ -1,13 +1,36 @@ -use mlua::{Lua, Result, Table}; +use mlua::{FromLua, IntoLua, Lua, Result, Value}; -pub fn to_table(lua: &Lua, x: f64, y: f64, z: f64) -> Result
{ - let table = lua.create_table()?; - table.set("x", x)?; - table.set("y", y)?; - table.set("z", z)?; - Ok(table) +#[derive(Clone)] +pub struct Position { + pub x: f64, + pub y: f64, + pub z: f64, } -pub fn from_table(table: &Table) -> Result<(f64, f64, f64)> { - Ok((table.get("x")?, table.get("y")?, table.get("z")?)) +impl IntoLua for Position { + fn into_lua(self, lua: &Lua) -> Result { + let table = lua.create_table()?; + table.set("x", self.x)?; + table.set("y", self.y)?; + table.set("z", self.z)?; + Ok(Value::Table(table)) + } +} + +impl FromLua for Position { + fn from_lua(value: Value, _lua: &Lua) -> Result { + if let Value::Table(table) = value { + Ok(Self { + x: table.get("x")?, + y: table.get("y")?, + z: table.get("z")?, + }) + } else { + Err(mlua::Error::FromLuaConversionError { + from: value.type_name(), + to: "Position".to_string(), + message: None, + }) + } + } }