Compare commits

..

10 Commits

Author SHA1 Message Date
d633820d19
refactor: try removing gc_stop
This used to cause deadlocks, but it's been a while with many things
rewritten. Let's try enabling it again to see if any issues surface.
Performance should increase as this was called on every event (packet?).
2025-02-26 17:38:04 -05:00
9b0f8ec406
feat(client): add menu field 2025-02-26 17:37:50 -05:00
543f0af741
refactor(lua/system): return command output as strings 2025-02-25 18:19:11 -05:00
e081813ba4
fix: flip direction table values 2025-02-25 18:12:33 -05:00
2b9bf1987b
refactor: minor improvements 2025-02-25 18:07:01 -05:00
5691afaf2d
fix(lib/inventory): use client.container 2025-02-24 22:20:07 -05:00
d7f863b680
feat: add set_health event 2025-02-24 18:27:50 -05:00
247612fad0
refactor: use a RwLock for event listeners 2025-02-24 16:56:10 -05:00
36054ced03
refactor: random cleanups and efficiency improvements 2025-02-24 01:31:51 -05:00
2ea7ec9e7e
fix(events): manipulate listeners in a task to avoid deadlocks 2025-02-23 17:18:55 -05:00
15 changed files with 202 additions and 133 deletions

12
Cargo.lock generated
View File

@ -1184,6 +1184,7 @@ dependencies = [
"bevy_log", "bevy_log",
"clap", "clap",
"futures", "futures",
"futures-locks",
"http-body-util", "http-body-util",
"hyper", "hyper",
"hyper-util", "hyper-util",
@ -1317,6 +1318,17 @@ dependencies = [
"pin-project-lite", "pin-project-lite",
] ]
[[package]]
name = "futures-locks"
version = "0.7.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "45ec6fe3675af967e67c5536c0b9d44e34e6c52f86bedc4ea49c5317b8e94d06"
dependencies = [
"futures-channel",
"futures-task",
"tokio",
]
[[package]] [[package]]
name = "futures-macro" name = "futures-macro"
version = "0.3.31" version = "0.3.31"

View File

@ -21,6 +21,7 @@ bevy_app = "0"
bevy_log = "0" bevy_log = "0"
clap = { version = "4", features = ["derive"] } clap = { version = "4", features = ["derive"] }
futures = "0" futures = "0"
futures-locks = "0"
http-body-util = "0" http-body-util = "0"
hyper = { version = "1", features = ["server"] } hyper = { version = "1", features = ["server"] }
hyper-util = "0" hyper-util = "0"

View File

@ -14,9 +14,10 @@ function log_player_positions()
end end
end end
function on_init() add_listener("init", function()
info("client initialized, setting information...") info("client initialized, setting information...")
client:set_client_information({ view_distance = 16 }) client:set_client_information({ view_distance = 16 })
end)
add_listener("login", function() add_listener("login", function()
info("player successfully logged in!") info("player successfully logged in!")
@ -27,4 +28,3 @@ function on_init()
end, "warn_player_died") end, "warn_player_died")
add_listener("tick", log_player_positions) add_listener("tick", log_player_positions)
end

View File

@ -17,7 +17,7 @@ function steal(item_name)
end end
container = nil container = nil
while client.open_container do while client.container do
sleep(50) sleep(50)
end end
end end

View File

@ -4,20 +4,17 @@ use crate::{
State, State,
commands::CommandSource, commands::CommandSource,
http::serve, http::serve,
lua::{self, events::register_functions, player::Player}, lua::{self, player::Player},
}; };
use azalea::{prelude::*, protocol::packets::game::ClientboundGamePacket}; use azalea::{prelude::*, protocol::packets::game::ClientboundGamePacket};
use hyper::{server::conn::http1, service::service_fn}; use hyper::{server::conn::http1, service::service_fn};
use hyper_util::rt::TokioIo; use hyper_util::rt::TokioIo;
use log::{debug, error, info, trace}; use log::{debug, error, info, trace};
use mlua::{Function, IntoLuaMulti}; use mlua::IntoLuaMulti;
use tokio::net::TcpListener; use tokio::net::TcpListener;
#[allow(clippy::too_many_lines)] #[allow(clippy::too_many_lines)]
pub async fn handle_event(client: Client, event: Event, state: State) -> anyhow::Result<()> { pub async fn handle_event(client: Client, event: Event, state: State) -> anyhow::Result<()> {
state.lua.gc_stop();
let globals = state.lua.globals();
match event { match event {
Event::AddPlayer(player_info) => { Event::AddPlayer(player_info) => {
call_listeners(&state, "add_player", Player::from(player_info)).await; call_listeners(&state, "add_player", Player::from(player_info)).await;
@ -26,7 +23,7 @@ pub async fn handle_event(client: Client, event: Event, state: State) -> anyhow:
let formatted_message = message.message(); let formatted_message = message.message();
info!("{}", formatted_message.to_ansi()); info!("{}", formatted_message.to_ansi());
let owners = globals.get::<Vec<String>>("Owners")?; let owners = state.lua.globals().get::<Vec<String>>("Owners")?;
if message.is_whisper() if message.is_whisper()
&& let (Some(sender), content) = message.split_sender_and_content() && let (Some(sender), content) = message.split_sender_and_content()
&& owners.contains(&sender) && owners.contains(&sender)
@ -69,31 +66,38 @@ pub async fn handle_event(client: Client, event: Event, state: State) -> anyhow:
Event::UpdatePlayer(player_info) => { Event::UpdatePlayer(player_info) => {
call_listeners(&state, "update_player", Player::from(player_info)).await; call_listeners(&state, "update_player", Player::from(player_info)).await;
} }
Event::Packet(packet) => { Event::Packet(packet) => match packet.as_ref() {
if let ClientboundGamePacket::SetPassengers(packet) = packet.as_ref() { ClientboundGamePacket::SetPassengers(packet) => {
let table = state.lua.create_table()?; let table = state.lua.create_table()?;
table.set("vehicle", packet.vehicle)?; table.set("vehicle", packet.vehicle)?;
table.set("passengers", &*packet.passengers)?; table.set("passengers", &*packet.passengers)?;
call_listeners(&state, "set_passengers", table).await; call_listeners(&state, "set_passengers", table).await;
} }
ClientboundGamePacket::SetHealth(packet) => {
let table = state.lua.create_table()?;
table.set("food", packet.food)?;
table.set("health", packet.health)?;
table.set("saturation", packet.saturation)?;
call_listeners(&state, "set_health", table).await;
} }
_ => (),
},
Event::Init => { Event::Init => {
debug!("client initialized"); debug!("received initialize event");
let globals = state.lua.globals();
globals.set( globals.set(
"client", "client",
lua::client::Client { lua::client::Client {
inner: Some(client), inner: Some(client),
}, },
)?; )?;
register_functions(&state.lua, &globals, state.clone()).await?; call_listeners(&state, "init", ()).await;
if let Ok(on_init) = globals.get::<Function>("on_init")
&& let Err(error) = on_init.call::<()>(()) let Some(address) = state.http_address else {
{ return Ok(());
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| { let listener = TcpListener::bind(address).await.map_err(|error| {
error!("failed to listen on {address}: {error:?}"); error!("failed to listen on {address}: {error:?}");
error error
@ -120,7 +124,6 @@ pub async fn handle_event(client: Client, event: Event, state: State) -> anyhow:
}); });
} }
} }
}
_ => (), _ => (),
} }
@ -128,10 +131,10 @@ pub async fn handle_event(client: Client, event: Event, state: State) -> anyhow:
} }
async fn call_listeners<T: Clone + IntoLuaMulti>(state: &State, event_type: &str, data: T) { async fn call_listeners<T: Clone + IntoLuaMulti>(state: &State, event_type: &str, data: T) {
if let Some(listeners) = state.event_listeners.lock().await.get(event_type) { if let Some(listeners) = state.event_listeners.read().await.get(event_type) {
for (_, listener) in listeners { for (id, callback) in listeners {
if let Err(error) = listener.call_async::<()>(data.clone()).await { if let Err(error) = callback.call_async::<()>(data.clone()).await {
error!("failed to call lua event listener for {event_type}: {error:?}"); error!("failed to call lua event listener {id} for {event_type}: {error:?}");
} }
} }
} }

View File

@ -1,10 +1,12 @@
use super::{Client, Container, ContainerRef, ItemStack, Vec3}; use super::{Client, Container, ContainerRef, ItemStack, Vec3};
use azalea::{ use azalea::{
BlockPos, inventory::Inventory, prelude::ContainerClientExt, BlockPos,
inventory::{Inventory, Menu, Player, SlotList},
prelude::ContainerClientExt,
protocol::packets::game::ServerboundSetCarriedItem, protocol::packets::game::ServerboundSetCarriedItem,
}; };
use log::error; use log::error;
use mlua::{Lua, Result, UserDataRef}; use mlua::{Lua, Result, Table, UserDataRef};
pub fn container(_lua: &Lua, client: &Client) -> Result<Option<ContainerRef>> { pub fn container(_lua: &Lua, client: &Client) -> Result<Option<ContainerRef>> {
Ok(client Ok(client
@ -13,15 +15,46 @@ pub fn container(_lua: &Lua, client: &Client) -> Result<Option<ContainerRef>> {
} }
pub fn held_item(_lua: &Lua, client: &Client) -> Result<ItemStack> { pub fn held_item(_lua: &Lua, client: &Client) -> Result<ItemStack> {
Ok(ItemStack { Ok(ItemStack::from(client.component::<Inventory>().held_item()))
inner: client.component::<Inventory>().held_item(),
})
} }
pub fn held_slot(_lua: &Lua, client: &Client) -> Result<u8> { pub fn held_slot(_lua: &Lua, client: &Client) -> Result<u8> {
Ok(client.component::<Inventory>().selected_hotbar_slot) Ok(client.component::<Inventory>().selected_hotbar_slot)
} }
pub fn menu(lua: &Lua, client: &Client) -> Result<Table> {
fn from_slot_list<const N: usize>(s: SlotList<N>) -> Vec<ItemStack> {
s.iter()
.map(|i| ItemStack::from(i.to_owned()))
.collect::<Vec<_>>()
}
let table = lua.create_table()?;
match client.menu() {
Menu::Player(Player {
craft_result,
craft,
armor,
inventory,
offhand,
}) => {
table.set("type", 0)?;
table.set("craft_result", ItemStack::from(craft_result))?;
table.set("craft", from_slot_list(craft))?;
table.set("armor", from_slot_list(armor))?;
table.set("inventory", from_slot_list(inventory))?;
table.set("offhand", ItemStack::from(offhand))?;
}
Menu::Generic9x6 { contents, player } => {
table.set("type", 6)?;
table.set("contents", from_slot_list(contents))?;
table.set("player", from_slot_list(player))?;
}
_ => (),
}
Ok(table)
}
pub async fn open_container_at( pub async fn open_container_at(
_lua: Lua, _lua: Lua,
client: UserDataRef<Client>, client: UserDataRef<Client>,

View File

@ -49,6 +49,7 @@ impl UserData for Client {
f.add_field_method_get("held_slot", container::held_slot); f.add_field_method_get("held_slot", container::held_slot);
f.add_field_method_get("hunger", state::hunger); f.add_field_method_get("hunger", state::hunger);
f.add_field_method_get("looking_at", movement::looking_at); f.add_field_method_get("looking_at", movement::looking_at);
f.add_field_method_get("menu", container::menu);
f.add_field_method_get("pathfinder", movement::pathfinder); f.add_field_method_get("pathfinder", movement::pathfinder);
f.add_field_method_get("position", movement::position); f.add_field_method_get("position", movement::position);
f.add_field_method_get("score", state::score); f.add_field_method_get("score", state::score);

View File

@ -15,7 +15,7 @@ use mlua::{FromLua, Lua, Result, Table, UserDataRef, Value};
pub fn direction(_lua: &Lua, client: &Client) -> Result<Direction> { pub fn direction(_lua: &Lua, client: &Client) -> Result<Direction> {
let d = client.direction(); let d = client.direction();
Ok(Direction { x: d.0, y: d.1 }) Ok(Direction { y: d.0, x: d.1 })
} }
pub fn eye_position(_lua: &Lua, client: &Client) -> Result<Vec3> { pub fn eye_position(_lua: &Lua, client: &Client) -> Result<Vec3> {

View File

@ -5,6 +5,12 @@ pub struct ItemStack {
pub inner: azalea::inventory::ItemStack, pub inner: azalea::inventory::ItemStack,
} }
impl From<azalea::inventory::ItemStack> for ItemStack {
fn from(inner: azalea::inventory::ItemStack) -> Self {
Self { inner }
}
}
impl UserData for ItemStack { impl UserData for ItemStack {
fn add_fields<F: UserDataFields<Self>>(f: &mut F) { fn add_fields<F: UserDataFields<Self>>(f: &mut F) {
f.add_field_method_get("is_empty", |_, this| Ok(this.inner.is_empty())); f.add_field_method_get("is_empty", |_, this| Ok(this.inner.is_empty()));
@ -38,9 +44,7 @@ impl UserData for ItemStack {
fn add_methods<M: UserDataMethods<Self>>(m: &mut M) { fn add_methods<M: UserDataMethods<Self>>(m: &mut M) {
m.add_method_mut("split", |_, this, count: u32| { m.add_method_mut("split", |_, this, count: u32| {
Ok(ItemStack { Ok(ItemStack::from(this.inner.split(count)))
inner: this.inner.split(count),
})
}); });
m.add_method_mut("update_empty", |_, this, (): ()| { m.add_method_mut("update_empty", |_, this, (): ()| {
this.inner.update_empty(); this.inner.update_empty();

View File

@ -25,7 +25,7 @@ impl UserData for Container {
f.add_field_method_get("contents", |_, this| { f.add_field_method_get("contents", |_, this| {
Ok(this.inner.contents().map(|v| { Ok(this.inner.contents().map(|v| {
v.iter() v.iter()
.map(|i| ItemStack { inner: i.clone() }) .map(|i| ItemStack::from(i.to_owned()))
.collect::<Vec<_>>() .collect::<Vec<_>>()
})) }))
}); });
@ -58,7 +58,7 @@ impl UserData for ContainerRef {
f.add_field_method_get("contents", |_, this| { f.add_field_method_get("contents", |_, this| {
Ok(this.inner.contents().map(|v| { Ok(this.inner.contents().map(|v| {
v.iter() v.iter()
.map(|i| ItemStack { inner: i.clone() }) .map(|i| ItemStack::from(i.to_owned()))
.collect::<Vec<_>>() .collect::<Vec<_>>()
})) }))
}); });

View File

@ -3,15 +3,15 @@ use mlua::{FromLua, IntoLua, Lua, Result, Value};
#[derive(Clone)] #[derive(Clone)]
pub struct Direction { pub struct Direction {
pub x: f32,
pub y: f32, pub y: f32,
pub x: f32,
} }
impl From<&LookDirection> for Direction { impl From<&LookDirection> for Direction {
fn from(d: &LookDirection) -> Self { fn from(d: &LookDirection) -> Self {
Self { Self {
x: d.x_rot,
y: d.y_rot, y: d.y_rot,
x: d.x_rot,
} }
} }
} }
@ -19,8 +19,8 @@ impl From<&LookDirection> for Direction {
impl IntoLua for Direction { impl IntoLua for Direction {
fn into_lua(self, lua: &Lua) -> Result<Value> { fn into_lua(self, lua: &Lua) -> Result<Value> {
let table = lua.create_table()?; let table = lua.create_table()?;
table.set("x", self.x)?;
table.set("y", self.y)?; table.set("y", self.y)?;
table.set("x", self.x)?;
Ok(Value::Table(table)) Ok(Value::Table(table))
} }
} }
@ -28,12 +28,12 @@ impl IntoLua for Direction {
impl FromLua for Direction { impl FromLua for Direction {
fn from_lua(value: Value, _lua: &Lua) -> Result<Self> { fn from_lua(value: Value, _lua: &Lua) -> Result<Self> {
if let Value::Table(table) = value { if let Value::Table(table) = value {
Ok(if let (Ok(x), Ok(y)) = (table.get(1), table.get(2)) { Ok(if let (Ok(y), Ok(x)) = (table.get(1), table.get(2)) {
Self { x, y } Self { y, x }
} else { } else {
Self { Self {
x: table.get("x")?,
y: table.get("y")?, y: table.get("y")?,
x: table.get("x")?,
} }
}) })
} else { } else {

View File

@ -1,47 +1,53 @@
use crate::State; use crate::ListenerMap;
use futures::executor::block_on; use futures::executor::block_on;
use mlua::{Function, Lua, Result, Table}; use mlua::{Function, Lua, Result, Table};
use std::time::{SystemTime, UNIX_EPOCH}; use std::time::{SystemTime, UNIX_EPOCH};
pub async fn register_functions(lua: &Lua, globals: &Table, state: State) -> Result<()> { pub fn register_functions(lua: &Lua, globals: &Table, event_listeners: ListenerMap) -> Result<()> {
let l = state.event_listeners.clone(); let m = event_listeners.clone();
globals.set( globals.set(
"add_listener", "add_listener",
lua.create_function( lua.create_function(
move |_, (event_type, callback, id): (String, Function, Option<String>)| { move |_, (event_type, callback, optional_id): (String, Function, Option<String>)| {
let mut l = block_on(l.lock()); let m = m.clone();
let id = optional_id.unwrap_or_else(|| {
l.entry(event_type).or_default().push(( callback.info().name.unwrap_or(format!(
id.unwrap_or(callback.info().name.unwrap_or(format!(
"anonymous @ {}", "anonymous @ {}",
SystemTime::now() SystemTime::now()
.duration_since(UNIX_EPOCH) .duration_since(UNIX_EPOCH)
.unwrap_or_default() .unwrap_or_default()
.as_millis() .as_millis()
))), ))
callback, });
)); tokio::spawn(async move {
m.write()
.await
.entry(event_type)
.or_default()
.push((id, callback));
});
Ok(()) Ok(())
}, },
)?, )?,
)?; )?;
let l = state.event_listeners.clone(); let m = event_listeners.clone();
globals.set( globals.set(
"remove_listener", "remove_listener",
lua.create_function(move |_, (event_type, target_id): (String, String)| { lua.create_function(move |_, (event_type, target_id): (String, String)| {
let mut l = block_on(l.lock()); let m = m.clone();
tokio::spawn(async move {
let empty = if let Some(listeners) = l.get_mut(&event_type) { let mut m = m.write().await;
let empty = if let Some(listeners) = m.get_mut(&event_type) {
listeners.retain(|(id, _)| target_id != *id); listeners.retain(|(id, _)| target_id != *id);
listeners.is_empty() listeners.is_empty()
} else { } else {
false false
}; };
if empty { if empty {
l.remove(&event_type); m.remove(&event_type);
} }
});
Ok(()) Ok(())
})?, })?,
)?; )?;
@ -49,10 +55,10 @@ pub async fn register_functions(lua: &Lua, globals: &Table, state: State) -> Res
globals.set( globals.set(
"get_listeners", "get_listeners",
lua.create_function(move |lua, (): ()| { lua.create_function(move |lua, (): ()| {
let l = block_on(state.event_listeners.lock()); let m = block_on(event_listeners.read());
let listeners = lua.create_table()?; let listeners = lua.create_table()?;
for (event_type, callbacks) in l.iter() { for (event_type, callbacks) in m.iter() {
let type_listeners = lua.create_table()?; let type_listeners = lua.create_table()?;
for (id, callback) in callbacks { for (id, callback) in callbacks {
let listener = lua.create_table()?; let listener = lua.create_table()?;

View File

@ -8,7 +8,9 @@ pub mod player;
pub mod system; pub mod system;
pub mod vec3; pub mod vec3;
use crate::ListenerMap;
use mlua::{Lua, Table}; use mlua::{Lua, Table};
use std::{io, time::Duration};
#[derive(Debug)] #[derive(Debug)]
#[allow(dead_code)] #[allow(dead_code)]
@ -18,19 +20,24 @@ pub enum Error {
ExecChunk(mlua::Error), ExecChunk(mlua::Error),
LoadChunk(mlua::Error), LoadChunk(mlua::Error),
MissingPath(mlua::Error), MissingPath(mlua::Error),
ReadFile(std::io::Error), ReadFile(io::Error),
} }
pub fn register_functions(lua: &Lua, globals: &Table) -> mlua::Result<()> { pub fn register_functions(
lua: &Lua,
globals: &Table,
event_listeners: ListenerMap,
) -> mlua::Result<()> {
globals.set( globals.set(
"sleep", "sleep",
lua.create_async_function(async |_, duration: u64| { lua.create_async_function(async |_, duration: u64| {
tokio::time::sleep(std::time::Duration::from_millis(duration)).await; tokio::time::sleep(Duration::from_millis(duration)).await;
Ok(()) Ok(())
})?, })?,
)?; )?;
block::register_functions(lua, globals)?; block::register_functions(lua, globals)?;
events::register_functions(lua, globals, event_listeners)?;
logging::register_functions(lua, globals)?; logging::register_functions(lua, globals)?;
system::register_functions(lua, globals) system::register_functions(lua, globals)
} }

View File

@ -48,12 +48,12 @@ pub fn register_functions(lua: &Lua, globals: &Table) -> Result<()> {
.args(args.unwrap_or_default().iter()) .args(args.unwrap_or_default().iter())
.output() .output()
{ {
Ok(o) => { Ok(output) => {
let output = lua.create_table()?; let table = lua.create_table()?;
output.set("status", o.status.code())?; table.set("status", output.status.code())?;
output.set("stdout", o.stdout)?; table.set("stdout", lua.create_string(output.stdout)?)?;
output.set("stderr", o.stderr)?; table.set("stderr", lua.create_string(output.stderr)?)?;
Some(output) Some(table)
} }
Err(error) => { Err(error) => {
error!("failed to run system command: {error:?}"); error!("failed to run system command: {error:?}");

View File

@ -16,8 +16,8 @@ use bevy_log::{
}; };
use clap::Parser; use clap::Parser;
use commands::{CommandSource, register}; use commands::{CommandSource, register};
use events::handle_event;
use futures::lock::Mutex; use futures::lock::Mutex;
use futures_locks::RwLock;
use mlua::{Function, Lua}; use mlua::{Function, Lua};
use std::{ use std::{
collections::HashMap, collections::HashMap,
@ -30,12 +30,12 @@ use std::{
const DEFAULT_SCRIPT_PATH: &str = "errornowatcher.lua"; const DEFAULT_SCRIPT_PATH: &str = "errornowatcher.lua";
type ListenerMap = HashMap<String, Vec<(String, Function)>>; type ListenerMap = Arc<RwLock<HashMap<String, Vec<(String, Function)>>>>;
#[derive(Default, Clone, Component)] #[derive(Default, Clone, Component)]
pub struct State { pub struct State {
lua: Lua, lua: Lua,
event_listeners: Arc<Mutex<ListenerMap>>, event_listeners: ListenerMap,
commands: Arc<CommandDispatcher<Mutex<CommandSource>>>, commands: Arc<CommandDispatcher<Mutex<CommandSource>>>,
http_address: Option<SocketAddr>, http_address: Option<SocketAddr>,
} }
@ -43,13 +43,14 @@ pub struct State {
#[tokio::main] #[tokio::main]
async fn main() -> anyhow::Result<()> { async fn main() -> anyhow::Result<()> {
let args = arguments::Arguments::parse(); let args = arguments::Arguments::parse();
let script_path = args.script.unwrap_or(PathBuf::from(DEFAULT_SCRIPT_PATH)); let script_path = args.script.unwrap_or(PathBuf::from(DEFAULT_SCRIPT_PATH));
let event_listeners = Arc::new(RwLock::new(HashMap::new()));
let lua = Lua::new(); let lua = Lua::new();
let globals = lua.globals(); let globals = lua.globals();
globals.set("script_path", &*script_path)?; globals.set("script_path", &*script_path)?;
lua::register_functions(&lua, &globals)?; lua::register_functions(&lua, &globals, event_listeners.clone())?;
lua.load( lua.load(
read_to_string(script_path) read_to_string(script_path)
.expect(&(DEFAULT_SCRIPT_PATH.to_owned() + " should be in current directory")), .expect(&(DEFAULT_SCRIPT_PATH.to_owned() + " should be in current directory")),
@ -65,8 +66,7 @@ async fn main() -> anyhow::Result<()> {
let mut commands = CommandDispatcher::new(); let mut commands = CommandDispatcher::new();
register(&mut commands); register(&mut commands);
let Err(error) = ClientBuilder::new_without_plugins() let log_plugin = LogPlugin {
.add_plugins(DefaultPlugins.set(LogPlugin {
custom_layer: |_| { custom_layer: |_| {
env::var("LOG_FILE").ok().map(|log_file| { env::var("LOG_FILE").ok().map(|log_file| {
layer() layer()
@ -75,18 +75,20 @@ async fn main() -> anyhow::Result<()> {
.append(true) .append(true)
.create(true) .create(true)
.open(log_file) .open(log_file)
.expect("should have been able to open log file"), .expect("log file should be accessible"),
) )
.boxed() .boxed()
}) })
}, },
..Default::default() ..Default::default()
})) };
let Err(error) = ClientBuilder::new_without_plugins()
.add_plugins(DefaultPlugins.set(log_plugin))
.add_plugins(DefaultBotPlugins) .add_plugins(DefaultBotPlugins)
.set_handler(handle_event) .set_handler(events::handle_event)
.set_state(State { .set_state(State {
lua, lua,
event_listeners: Arc::new(Mutex::new(HashMap::new())), event_listeners,
commands: Arc::new(commands), commands: Arc::new(commands),
http_address: args.http_address, http_address: args.http_address,
}) })