chore: merge branch v0.2.0 into main

This commit is contained in:
Ryan 2025-02-21 23:18:06 -05:00
commit aadc9a919e
Signed by: ErrorNoInternet
GPG Key ID: 2486BFB7B1E6A4A3
35 changed files with 3790 additions and 4033 deletions

View File

@ -1,10 +1,3 @@
[target.x86_64-unknown-linux-gnu]
linker = "/usr/bin/clang"
linker = "clang"
rustflags = ["-Clink-arg=-fuse-ld=mold"]
[target.x86_64-apple-darwin]
rustflags = ["-C", "link-arg=-fuse-ld=/usr/local/bin/zld"]
[target.x86_64-pc-windows-msvc]
linker = "rust-lld.exe"
rustflags = ["-Zshare-generics=y"]

5
.gitignore vendored
View File

@ -1,3 +1,2 @@
/target
/scripts
/bot_configuration.toml
.luarc.json
target

3946
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -1,27 +1,27 @@
[package]
name = "errornowatcher"
version = "0.1.0"
edition = "2021"
[dependencies]
azalea = "0.5.0"
azalea-protocol = "0.5.0"
azalea-block = "0.5.0"
azalea-core = "0.5.0"
toml = "0.5.10"
serde = { version = "1.0", features = ["derive"] }
tokio = { version = "1.24.2", features = ["macros", "rt-multi-thread"] }
anyhow = "1.0.68"
colored = "2.0.0"
chrono = "0.4.23"
strum = "0.24.1"
strum_macros = "0.24.1"
async-recursion = "1.0.0"
rand = "0.8.5"
matrix-sdk = "0.6.2"
version = "0.2.0"
edition = "2024"
[profile.dev]
opt-level = 1
[profile.dev.package."*"]
opt-level = 3
[profile.release]
codegen-units = 1
lto = true
strip = true
[dependencies]
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"
log = { version = "0" }
mlua = { version = "0", features = ["async", "luau", "send"] }
tokio = { version = "1", features = ["macros"] }

View File

@ -1,73 +1,24 @@
<p align="center">
<img src="/images/icon.png">
<h3 align="center">ErrorNoWatcher</h3>
<p align="center">
ErrorNoWatcher is a Minecraft bot (written in Rust with <a href="https://github.com/mat-1/azalea">azalea</a>) that alerts you when players are near your base. You can customize the size and location of your base, change the players that will receive a private message in-game, and even run custom shell commands! It also has other features such as an entity finder, a pathfinder that can follow players and go to coordinates, the ability to interact with blocks, a scripting system to run commands from a file, the ability to accept and respond to commands from Matrix, and much more.
</p>
</p>
# ErrorNoWatcher
**:warning: ErrorNoWatcher is undergoing a major rewrite with Lua scripting support (`v0.2.0` branch)**
A Minecraft bot with Lua scripting support, written in Rust with [azalea](https://github.com/azalea-rs/azalea).
## Compiling
```sh
git clone https://github.com/ErrorNoInternet/ErrorNoWatcher
cd ErrorNoWatcher
cargo build --release
```
The compiled executable can be found at `./target/release/errornowatcher`
## Features
- Running Lua from
- `errornowatcher.lua`
- in-game chat messages
- POST requests to HTTP server
- Listening to in-game events
- Pathfinding (from azalea)
- Entity and chest interaction
## Usage
### Configuration
Running the bot will create the `bot_configuration.toml` file, where you can change several options:
```toml
username = "<bot's username>" # offline username
server_address = "<server address>"
register_keyword = "Register using"
register_command = "register MyPassword MyPassword"
login_keyword = "Login using"
login_command = "login MyPassword"
bot_owners = ["ErrorNoInternet", "<Minecraft usernames that are allowed to run commands>"]
whitelist = [
"ErrorNoInternet",
"<won't be triggered by the alert system>"
]
alert_players = ["ErrorNoInternet", "<players to send a message to>"]
alert_location = [0, 0] # coordinates of your base (X and Y position)
alert_radius = 192 # the radius of your base (-192, -192 to 192, 192)
alert_command = [
"curl",
"-s",
"-m 5",
"-HTitle: Intruder Alert",
"-HPriority: urgent",
"-HTags: warning",
"-d{player_name} is near your base! Their coordinates are {x} {y} {z}.",
"<your URL here (or a service such as ntfy.sh)>"
]
alert_pause_time = 5 # the amount of seconds to wait between alert messages
cleanup_interval = 300 # the amount of seconds to wait before checking for idle entities
mob_expiry_time = 300 # the maximum amount of time a mob can stay idle before getting cleared
mob_packet_drop_level = 5 # the amount of mob packets to drop (0 = 0%, 5 = 50%, 10 = 100%)
[matrix]
enabled = false
homeserver_url = "https://matrix.example.com"
username = "errornowatcher"
password = "MyMatrixPassword"
bot_owners = ["@zenderking:envs.net", "<Matrix user IDs that are allowed to run commands>"]
```sh
$ git clone https://github.com/ErrorNoInternet/ErrorNoWatcher
$ cd ErrorNoWatcher
$ cargo build --release
$ # ./target/release/errornowatcher
```
### Example commands
- `/msg ErrorNoWatcher help 1` - list the first page of usable commands
- `/msg ErrorNoWatcher bot_status` - display the bot's health, food & saturation levels, etc
- `/msg ErrorNoWatcher goto 20 64 10` - go to X20 Y64 Z10 (using the pathfinder)
- `/msg ErrorNoWatcher script sleep.txt` - run all commands in the file `sleep.txt`
- `/msg ErrorNoWatcher attack ErrorNoInternet` - attack the player named ErrorNoInternet
- `/msg ErrorNoWatcher look 180 0` - rotate the bot's head to 180 (yaw) 0 (pitch)
- `/msg ErrorNoWatcher whitelist_add Notch` - temporarily add Notch to the whitelist
- `/msg ErrorNoWatcher sprint forward 5000` - sprint forward for 5 seconds
- `/msg ErrorNoWatcher drop_item` - drop the currently held item (or `drop_stack`)
- `/msg ErrorNoWatcher last_location 1` - show the first page of players sorted by join time
- `/msg ErrorNoWatcher last_location ErrorNoInternet` - display the last seen location
- `/msg ErrorNoWatcher follow_player ErrorNoInternet` - start following ErrorNoInternet
- `/msg ErrorNoWatcher slot 1` - switch to the first inventory slot (hold the item)
- `/msg ErrorNoWatcher place_block 0 64 2 top` - places a block on top of the block at 0 64 2
Make sure the `SERVER` and `USERNAME` globals are defined in `errornowatcher.lua` before starting the bot.

17
errornowatcher.lua Normal file
View File

@ -0,0 +1,17 @@
SERVER = "localhost"
USERNAME = "ErrorNoWatcher"
OWNERS = { "ErrorNoInternet" }
for _, module in
{
"enum",
"events",
"inventory",
"movement",
"utils",
}
do
module = "lib/" .. module
package.loaded[module] = nil
require(module)
end

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.5 KiB

34
lib/enum.lua Normal file
View File

@ -0,0 +1,34 @@
NONE = 0
FORWARD = 1
BACKWARD = 2
LEFT = 3
RIGHT = 4
FORWARD_LEFT = 5
FORWARD_RIGHT = 6
BACKWARD_LEFT = 7
BACKWARD_RIGHT = 8
BLOCK_POS_GOAL = 0
RADIUS_GOAL = 1
REACH_BLOCK_POS_GOAL = 2
XZ_GOAL = 3
Y_GOAL = 4
PICKUP_LEFT = 0
PICKUP_RIGHT = 1
PICKUP_LEFT_OUTSIDE = 2
PICKUP_RIGHT_OUTSIDE = 3
QUICK_MOVE_LEFT = 4
QUICK_MOVE_RIGHT = 5
SWAP = 6
CLONE = 7
THROW_SINGLE = 8
THROW_ALL = 9
QUICK_CRAFT = 10
QUICK_CRAFT_LEFT = 0
QUICK_CRAFT_RIGHT = 1
QUICK_CRAFT_MIDDLE = 2
QUICK_CRAFT_START = 0
QUICK_CRAFT_ADD = 1
QUICK_CRAFT_END = 2
PICKUP_ALL = 11

30
lib/events.lua Normal file
View File

@ -0,0 +1,30 @@
local center = { x = 0, z = 0 }
local radius = 100
function log_player_positions()
local entities = client:find_entities(function(e)
return e.kind == "minecraft:player"
and e.position.x > center.x - radius + 1
and e.position.x < center.x + radius
and e.position.z > center.z - radius
and e.position.z < center.z + radius
end)
for _, e in entities do
client:chat(string.format("%s (%s) at %.1f %.1f %.1f", e.kind, e.id, e.position.x, e.position.y, e.position.z))
end
end
function on_init()
info("client initialized, setting information...")
client:set_client_information({ view_distance = 16 })
add_listener("login", function()
info("player successfully logged in!")
end)
add_listener("death", function()
warn("player died!")
end, "warn_player_died")
add_listener("tick", log_player_positions)
end

38
lib/inventory.lua Normal file
View File

@ -0,0 +1,38 @@
function steal(item_name)
for _, chest_pos in client:find_blocks(client.position, get_block_states({ "chest" })) do
client:chat(dump(chest_pos))
client:goto({ position = chest_pos, radius = 3 }, { type = RADIUS_GOAL })
while client.pathfinder.is_calculating or client.pathfinder.is_executing do
sleep(50)
end
client:look_at(chest_pos)
local container = client:open_container_at(chest_pos)
for index, item in container.contents do
if item.kind == item_name then
container:click({slot = index - 1}, THROW_ALL)
sleep(50)
end
end
container = nil
while client.open_container do
sleep(50)
end
end
end
function drop_all_hotbar()
local inventory = client:open_inventory()
for i = 0, 9 do
inventory:click({slot = 36 + i}, THROW_ALL)
end
end
function drop_all_inventory()
local inventory = client:open_inventory()
for i = 0, 45 do
inventory:click({slot = i}, THROW_ALL)
end
end

18
lib/movement.lua Normal file
View File

@ -0,0 +1,18 @@
function look_at_player(name)
local player = get_player(name)
if player then
player.position.y = player.position.y + 1
client:look_at(player.position)
else
client:chat("player not found!")
end
end
function goto_player(name)
local player = get_player(name)
if player then
client:goto(player.position)
else
client:chat("player not found!")
end
end

48
lib/utils.lua Normal file
View File

@ -0,0 +1,48 @@
function get_player(name)
local target_uuid = nil
for _, player in client.tab_list do
if player.name == name then
target_uuid = player.uuid
break
end
end
return client:find_entities(function(e)
return e.kind == "minecraft:player" and e.uuid == target_uuid
end)[1]
end
function distance(p1, p2)
return math.sqrt((p2.x - p1.x) ^ 2 + (p2.y - p1.y) ^ 2 + (p2.z - p1.z) ^ 2)
end
function dump(object)
if type(object) == "table" then
local string = "{ "
local parts = {}
for key, value in pairs(object) do
table.insert(parts, key .. " = " .. dump(value))
end
string = string .. table.concat(parts, ", ")
return string .. " " .. "}"
else
return tostring(object)
end
end
function dump_pretty(object, level)
if not level then
level = 0
end
if type(object) == "table" then
local string = "{\n" .. string.rep(" ", level + 1)
local parts = {}
for key, value in pairs(object) do
table.insert(parts, key .. " = " .. dump_pretty(value, level + 1))
end
string = string .. table.concat(parts, ",\n" .. string.rep(" ", level + 1))
return string .. "\n" .. string.rep(" ", level) .. "}"
else
return tostring(object)
end
end

14
src/arguments.rs Normal file
View File

@ -0,0 +1,14 @@
use clap::Parser;
use std::{net::SocketAddr, path::PathBuf};
/// A Minecraft utility bot
#[derive(Parser)]
pub struct Arguments {
/// Path to main Lua file
#[arg(short, long)]
pub script: Option<PathBuf>,
/// Socket address to bind HTTP server to
#[arg(short = 'a', long)]
pub http_address: Option<SocketAddr>,
}

1104
src/bot.rs

File diff suppressed because it is too large Load Diff

90
src/commands.rs Normal file
View File

@ -0,0 +1,90 @@
use crate::{
State,
lua::{eval, exec, reload},
};
use azalea::{
GameProfileComponent, brigadier::prelude::*, chat::ChatPacket, entity::metadata::Player,
prelude::*,
};
use bevy_ecs::{entity::Entity, query::With};
use futures::lock::Mutex;
pub type Ctx = CommandContext<Mutex<CommandSource>>;
pub struct CommandSource {
pub client: Client,
pub message: ChatPacket,
pub state: State,
}
impl CommandSource {
pub fn reply(&self, message: &str) {
for chunk in message
.chars()
.collect::<Vec<char>>()
.chunks(236)
.map(|chars| chars.iter().collect::<String>())
{
self.client.chat(
&(if self.message.is_whisper()
&& let Some(username) = self.message.username()
{
format!("/w {username} {chunk}")
} else {
chunk
}),
);
}
}
pub fn _entity(&mut self) -> Option<Entity> {
let username = self.message.username()?;
self.client
.entity_by::<With<Player>, &GameProfileComponent>(|profile: &&GameProfileComponent| {
profile.name == username
})
}
}
pub fn register(commands: &mut CommandDispatcher<Mutex<CommandSource>>) {
commands.register(literal("reload").executes(|ctx: &Ctx| {
let source = ctx.source.clone();
tokio::spawn(async move {
let source = source.lock().await;
source.reply(&format!("{:?}", reload(&source.state.lua)));
});
1
}));
commands.register(
literal("eval").then(argument("code", string()).executes(|ctx: &Ctx| {
let source = ctx.source.clone();
let code = get_string(ctx, "code").expect("argument should exist");
tokio::spawn(async move {
let source = source.lock().await;
source.reply(&format!("{:?}", eval(&source.state.lua, &code).await));
});
1
})),
);
commands.register(
literal("exec").then(argument("code", string()).executes(|ctx: &Ctx| {
let source = ctx.source.clone();
let code = get_string(ctx, "code").expect("argument should exist");
tokio::spawn(async move {
let source = source.lock().await;
source.reply(&format!("{:?}", exec(&source.state.lua, &code).await));
});
1
})),
);
commands.register(literal("ping").executes(|ctx: &Ctx| {
let source = ctx.source.clone();
tokio::spawn(async move {
source.lock().await.reply("pong!");
});
1
}));
}

129
src/events.rs Normal file
View File

@ -0,0 +1,129 @@
use std::process::exit;
use crate::{
State,
commands::CommandSource,
http::serve,
lua::{self, events::register_functions, player::Player},
};
use azalea::prelude::*;
use hyper::{server::conn::http1, service::service_fn};
use hyper_util::rt::TokioIo;
use log::{debug, error, info, trace};
use mlua::{Function, IntoLuaMulti};
use tokio::net::TcpListener;
pub async fn handle_event(client: Client, event: Event, state: State) -> anyhow::Result<()> {
state.lua.gc_stop();
let globals = state.lua.globals();
match event {
Event::AddPlayer(player_info) => {
call_listeners(&state, "add_player", Player::from(player_info)).await;
}
Event::Chat(message) => {
let formatted_message = message.message();
info!("{}", formatted_message.to_ansi());
let owners = globals.get::<Vec<String>>("OWNERS")?;
if message.is_whisper()
&& let (Some(sender), content) = message.split_sender_and_content()
&& owners.contains(&sender)
{
if let Err(error) = state.commands.execute(
content,
CommandSource {
client: client.clone(),
message: message.clone(),
state: state.clone(),
}
.into(),
) {
CommandSource {
client,
message,
state: state.clone(),
}
.reply(&format!("{error:?}"));
}
}
call_listeners(&state, "chat", formatted_message.to_string()).await;
}
Event::Death(Some(packet)) => {
let death_data = state.lua.create_table()?;
death_data.set("message", packet.message.to_string())?;
death_data.set("player_id", packet.player_id)?;
call_listeners(&state, "death", death_data).await;
}
Event::Disconnect(message) => {
call_listeners(&state, "disconnect", message.map(|m| m.to_string())).await;
exit(1)
}
Event::Login => call_listeners(&state, "login", ()).await,
Event::RemovePlayer(player_info) => {
call_listeners(&state, "remove_player", Player::from(player_info)).await;
}
Event::Tick => call_listeners(&state, "tick", ()).await,
Event::UpdatePlayer(player_info) => {
call_listeners(&state, "update_player", Player::from(player_info)).await;
}
Event::Init => {
debug!("client initialized");
globals.set(
"client",
lua::client::Client {
inner: Some(client),
},
)?;
register_functions(&state.lua, &globals, state.clone()).await?;
if let Ok(on_init) = globals.get::<Function>("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}");
loop {
let (stream, peer) = listener.accept().await?;
trace!("http server got connection from {peer}");
let conn_state = state.clone();
let service = service_fn(move |request| {
let request_state = conn_state.clone();
async move { serve(request, request_state).await }
});
tokio::spawn(async move {
if let Err(error) = http1::Builder::new()
.serve_connection(TokioIo::new(stream), service)
.await
{
error!("failed to serve connection: {error:?}");
}
});
}
}
}
_ => (),
}
Ok(())
}
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) {
for (_, listener) in listeners {
if let Err(error) = listener.call_async::<()>(data.clone()).await {
error!("failed to call lua event listener for {event_type}: {error:?}");
}
}
}
}

63
src/http.rs Normal file
View File

@ -0,0 +1,63 @@
use crate::{
State,
lua::{eval, exec, reload},
};
use http_body_util::{BodyExt, Empty, Full, combinators::BoxBody};
use hyper::{Method, Request, Response, StatusCode, body::Bytes};
pub async fn serve(
request: Request<hyper::body::Incoming>,
state: State,
) -> Result<Response<BoxBody<Bytes, hyper::Error>>, hyper::Error> {
let path = request.uri().path().to_owned();
Ok(match (request.method(), path.as_str()) {
(&Method::POST, "/reload") => match reload(&state.lua) {
Ok(()) => Response::new(empty()),
Err(error) => status_code_response(
StatusCode::INTERNAL_SERVER_ERROR,
full(format!("{error:?}")),
),
},
(&Method::POST, "/eval" | "/exec") => {
let bytes = request.into_body().collect().await?.to_bytes();
match std::str::from_utf8(&bytes) {
Ok(code) => Response::new(full(match path.as_str() {
"/eval" => format!("{:#?}", eval(&state.lua, code).await),
"/exec" => format!("{:#?}", exec(&state.lua, code).await),
_ => unreachable!(),
})),
Err(error) => status_code_response(
StatusCode::BAD_REQUEST,
full(format!("invalid utf-8 data received: {error:?}")),
),
}
}
(&Method::GET, "/ping") => Response::new(full("pong!")),
_ => status_code_response(StatusCode::NOT_FOUND, empty()),
})
}
fn status_code_response(
status_code: StatusCode,
bytes: BoxBody<Bytes, hyper::Error>,
) -> Response<BoxBody<Bytes, hyper::Error>> {
let mut response = Response::new(bytes);
*response.status_mut() = status_code;
response
}
fn full<T: Into<Bytes>>(chunk: T) -> BoxBody<Bytes, hyper::Error> {
Full::new(chunk.into())
.map_err(|never| match never {})
.boxed()
}
fn empty() -> BoxBody<Bytes, hyper::Error> {
Empty::<Bytes>::new()
.map_err(|never| match never {})
.boxed()
}

View File

@ -1,75 +0,0 @@
use chrono::Local;
use colored::*;
pub enum LogMessageType {
Bot,
Chat,
Matrix,
Error,
MatrixError,
}
pub fn log_error<T, E: std::fmt::Display>(result: Result<T, E>) {
match result {
Ok(_) => (),
Err(error) => log_message(LogMessageType::Error, &error.to_string()),
}
}
pub fn log_message(message_type: LogMessageType, message: &String) {
match message_type {
LogMessageType::Bot => {
println!(
"{} {} {}",
current_time(),
colored_brackets(&"BOT".bold().blue()),
message
)
}
LogMessageType::Chat => {
println!(
"{} {} {}",
current_time(),
colored_brackets(&"CHAT".bold().blue()),
message
)
}
LogMessageType::Matrix => {
println!(
"{} {} {}",
current_time(),
colored_brackets(&"MATRIX".bold().green()),
message
)
}
LogMessageType::Error => println!(
"{} {} {}",
current_time(),
colored_brackets(&"ERROR".bold().red()),
message.red()
),
LogMessageType::MatrixError => println!(
"{} {} {}",
current_time(),
colored_brackets(&"ERROR (Matrix)".bold().red()),
message.red()
),
}
}
fn current_time() -> String {
format!(
"{}{}{}",
"[".bold().white(),
Local::now()
.format("%Y/%m/%d %H:%M:%S")
.to_string()
.bold()
.white(),
"]".bold().white()
)
}
fn colored_brackets(text: &ColoredString) -> String {
format!("{}{}{}", "[".bold().yellow(), text, "]".bold().yellow())
}

62
src/lua/block.rs Normal file
View File

@ -0,0 +1,62 @@
use azalea::blocks::{
Block as AzaleaBlock, BlockState,
properties::{ChestType, Facing, LightLevel},
};
use mlua::{Function, Lua, Result, Table};
pub fn register_functions(lua: &Lua, globals: &Table) -> Result<()> {
globals.set(
"get_block_from_state",
lua.create_function(get_block_from_state)?,
)?;
globals.set("get_block_states", lua.create_function(get_block_states)?)?;
Ok(())
}
pub fn get_block_from_state(lua: &Lua, state: u32) -> Result<Option<Table>> {
let Ok(state) = BlockState::try_from(state) else {
return Ok(None);
};
let b: Box<dyn AzaleaBlock> = state.into();
let bh = b.behavior();
let block = lua.create_table()?;
block.set("id", b.id())?;
block.set("friction", bh.friction)?;
block.set("jump_factor", bh.jump_factor)?;
block.set("destroy_time", bh.destroy_time)?;
block.set("explosion_resistance", bh.explosion_resistance)?;
block.set(
"requires_correct_tool_for_drops",
bh.requires_correct_tool_for_drops,
)?;
Ok(Some(block))
}
pub fn get_block_states(
lua: &Lua,
(block_names, filter_fn): (Vec<String>, Option<Function>),
) -> Result<Vec<u16>> {
let mut matched = Vec::new();
for block_name in block_names {
for b in
(u32::MIN..u32::MAX).map_while(|possible_id| BlockState::try_from(possible_id).ok())
{
if block_name == Into::<Box<dyn AzaleaBlock>>::into(b).id()
&& (if let Some(filter_fn) = &filter_fn {
let p = lua.create_table()?;
p.set("chest_type", b.property::<ChestType>().map(|v| v as u8))?;
p.set("facing", b.property::<Facing>().map(|v| v as u8))?;
p.set("light_level", b.property::<LightLevel>().map(|v| v as u8))?;
filter_fn.call::<bool>(p.clone())?
} else {
true
})
{
matched.push(b.id);
}
}
}
Ok(matched)
}

View File

@ -0,0 +1,67 @@
use super::{Client, Container, ContainerRef, ItemStack, Vec3};
use azalea::{
BlockPos, inventory::Inventory, prelude::ContainerClientExt,
protocol::packets::game::ServerboundSetCarriedItem,
};
use log::error;
use mlua::{Lua, Result, UserDataRef};
pub fn container(_lua: &Lua, client: &Client) -> Result<Option<ContainerRef>> {
Ok(client
.get_open_container()
.map(|c| ContainerRef { inner: c }))
}
pub fn held_item(_lua: &Lua, client: &Client) -> Result<ItemStack> {
Ok(ItemStack {
inner: client.component::<Inventory>().held_item(),
})
}
pub fn held_slot(_lua: &Lua, client: &Client) -> Result<u8> {
Ok(client.component::<Inventory>().selected_hotbar_slot)
}
pub async fn open_container_at(
_lua: Lua,
client: UserDataRef<Client>,
position: Vec3,
) -> Result<Option<Container>> {
#[allow(clippy::cast_possible_truncation)]
Ok(client
.clone()
.open_container_at(BlockPos::new(
position.x as i32,
position.y as i32,
position.z as i32,
))
.await
.map(|c| Container { inner: c }))
}
pub fn open_inventory(_lua: &Lua, client: &mut Client, _: ()) -> Result<Option<Container>> {
Ok(client.open_inventory().map(|c| Container { inner: c }))
}
pub fn set_held_slot(_lua: &Lua, client: &Client, slot: u8) -> Result<()> {
if slot > 8 {
return Ok(());
}
{
let mut ecs = client.ecs.lock();
let mut inventory = client.query::<&mut Inventory>(&mut ecs);
if inventory.selected_hotbar_slot == slot {
return Ok(());
}
inventory.selected_hotbar_slot = slot;
};
if let Err(error) = client.write_packet(ServerboundSetCarriedItem {
slot: u16::from(slot),
}) {
error!("failed to send SetCarriedItem packet: {error:?}");
}
Ok(())
}

View File

@ -0,0 +1,84 @@
use super::{Client, Vec3};
use azalea::{
BlockPos, BotClientExt,
attack::AttackEvent,
protocol::packets::game::{ServerboundUseItem, s_interact::InteractionHand},
world::MinecraftEntityId,
};
use log::error;
use mlua::{Lua, Result, UserDataRef};
pub async fn attack(_lua: Lua, client: UserDataRef<Client>, entity_id: u32) -> Result<()> {
client.clone().attack(MinecraftEntityId(entity_id));
while client.get_tick_broadcaster().recv().await.is_ok() {
if client
.ecs
.lock()
.get::<AttackEvent>(client.entity)
.is_none()
{
break;
}
}
Ok(())
}
pub fn block_interact(_lua: &Lua, client: &mut Client, position: Vec3) -> Result<()> {
#[allow(clippy::cast_possible_truncation)]
client.block_interact(BlockPos::new(
position.x as i32,
position.y as i32,
position.z as i32,
));
Ok(())
}
pub fn has_attack_cooldown(_lua: &Lua, client: &Client) -> Result<bool> {
Ok(client.has_attack_cooldown())
}
pub async fn mine(_lua: Lua, client: UserDataRef<Client>, position: Vec3) -> Result<()> {
#[allow(clippy::cast_possible_truncation)]
client
.clone()
.mine(BlockPos::new(
position.x as i32,
position.y as i32,
position.z as i32,
))
.await;
Ok(())
}
pub fn set_mining(_lua: &Lua, client: &Client, mining: bool) -> Result<()> {
client.left_click_mine(mining);
Ok(())
}
pub fn start_mining(_lua: &Lua, client: &mut Client, position: Vec3) -> Result<()> {
#[allow(clippy::cast_possible_truncation)]
client.start_mining(BlockPos::new(
position.x as i32,
position.y as i32,
position.z as i32,
));
Ok(())
}
pub fn use_item(_lua: &Lua, client: &Client, hand: Option<u8>) -> Result<()> {
let d = client.direction();
if let Err(error) = client.write_packet(ServerboundUseItem {
hand: match hand {
Some(1) => InteractionHand::OffHand,
_ => InteractionHand::MainHand,
},
sequence: 0,
yaw: d.0,
pitch: d.1,
}) {
error!("failed to send UseItem packet: {error:?}");
}
Ok(())
}

110
src/lua/client/mod.rs Normal file
View File

@ -0,0 +1,110 @@
mod container;
mod interaction;
mod movement;
mod state;
mod world;
use super::{
container::{Container, ContainerRef, item_stack::ItemStack},
direction::Direction,
player::Player,
vec3::Vec3,
};
use azalea::Client as AzaleaClient;
use mlua::{Lua, Result, UserData, UserDataFields, UserDataMethods};
use std::ops::{Deref, DerefMut};
pub struct Client {
pub inner: Option<AzaleaClient>,
}
impl Deref for Client {
type Target = AzaleaClient;
fn deref(&self) -> &Self::Target {
self.inner
.as_ref()
.expect("should have received init event")
}
}
impl DerefMut for Client {
fn deref_mut(&mut self) -> &mut Self::Target {
self.inner
.as_mut()
.expect("should have received init event")
}
}
impl UserData for Client {
fn add_fields<F: UserDataFields<Self>>(f: &mut F) {
f.add_field_method_get("air_supply", state::air_supply);
f.add_field_method_get("container", container::container);
f.add_field_method_get("dimension", world::dimension);
f.add_field_method_get("direction", movement::direction);
f.add_field_method_get("eye_position", movement::eye_position);
f.add_field_method_get("has_attack_cooldown", interaction::has_attack_cooldown);
f.add_field_method_get("health", state::health);
f.add_field_method_get("held_item", container::held_item);
f.add_field_method_get("held_slot", container::held_slot);
f.add_field_method_get("hunger", state::hunger);
f.add_field_method_get("looking_at", movement::looking_at);
f.add_field_method_get("pathfinder", movement::pathfinder);
f.add_field_method_get("position", movement::position);
f.add_field_method_get("score", state::score);
f.add_field_method_get("tab_list", tab_list);
f.add_field_method_get("uuid", uuid);
}
fn add_methods<M: UserDataMethods<Self>>(m: &mut M) {
m.add_async_method("attack", interaction::attack);
m.add_async_method("goto", movement::goto);
m.add_async_method("look_at", movement::look_at);
m.add_async_method("mine", interaction::mine);
m.add_async_method("open_container_at", container::open_container_at);
m.add_async_method("set_client_information", state::set_client_information);
m.add_method("best_tool_for_block", world::best_tool_for_block);
m.add_method("chat", chat);
m.add_method("disconnect", disconnect);
m.add_method("find_blocks", world::find_blocks);
m.add_method("find_entities", world::find_entities);
m.add_method("get_block_state", world::get_block_state);
m.add_method("get_fluid_state", world::get_fluid_state);
m.add_method("set_held_slot", container::set_held_slot);
m.add_method("set_mining", interaction::set_mining);
m.add_method("set_sneaking", movement::set_sneaking);
m.add_method("stop_pathfinding", movement::stop_pathfinding);
m.add_method("stop_sleeping", movement::stop_sleeping);
m.add_method("use_item", interaction::use_item);
m.add_method_mut("block_interact", interaction::block_interact);
m.add_method_mut("jump", movement::jump);
m.add_method_mut("open_inventory", container::open_inventory);
m.add_method_mut("set_direction", movement::set_direction);
m.add_method_mut("set_jumping", movement::set_jumping);
m.add_method_mut("sprint", movement::sprint);
m.add_method_mut("start_mining", interaction::start_mining);
m.add_method_mut("walk", movement::walk);
}
}
fn chat(_lua: &Lua, client: &Client, message: String) -> Result<()> {
client.chat(&message);
Ok(())
}
fn disconnect(_lua: &Lua, client: &Client, _: ()) -> Result<()> {
client.disconnect();
Ok(())
}
fn tab_list(_lua: &Lua, client: &Client) -> Result<Vec<Player>> {
let mut tab_list = Vec::new();
for (_, player_info) in client.tab_list() {
tab_list.push(Player::from(player_info));
}
Ok(tab_list)
}
fn uuid(_lua: &Lua, client: &Client) -> Result<String> {
Ok(client.uuid().to_string())
}

240
src/lua/client/movement.rs Normal file
View File

@ -0,0 +1,240 @@
use super::{Client, Direction, Vec3};
use azalea::{
BlockPos, BotClientExt, LookAtEvent, SprintDirection, WalkDirection,
entity::Position,
interact::HitResultComponent,
pathfinder::{
ExecutingPath, GotoEvent, Pathfinder, PathfinderClientExt,
goals::{BlockPosGoal, Goal, RadiusGoal, ReachBlockPosGoal, XZGoal, YGoal},
},
protocol::packets::game::{ServerboundPlayerCommand, s_player_command::Action},
};
use log::error;
use mlua::{FromLua, Lua, Result, Table, UserDataRef, Value};
pub fn direction(_lua: &Lua, client: &Client) -> Result<Direction> {
let d = client.direction();
Ok(Direction { x: d.0, y: d.1 })
}
pub fn eye_position(_lua: &Lua, client: &Client) -> Result<Vec3> {
Ok(Vec3::from(client.eye_position()))
}
pub async fn goto(
lua: Lua,
client: UserDataRef<Client>,
(data, metadata): (Value, Option<Table>),
) -> Result<()> {
fn g(client: &Client, without_mining: bool, goal: impl Goal + Send + Sync + 'static) {
if without_mining {
client.goto_without_mining(goal);
} else {
client.goto(goal);
}
}
let error = mlua::Error::FromLuaConversionError {
from: data.type_name(),
to: "Table".to_string(),
message: None,
};
let (goal_type, without_mining) = metadata
.map(|t| {
(
t.get("type").unwrap_or_default(),
t.get("without_mining").unwrap_or_default(),
)
})
.unwrap_or_default();
#[allow(clippy::cast_possible_truncation)]
match goal_type {
1 => {
let t = data.as_table().ok_or(error)?;
let p = Vec3::from_lua(t.get("position")?, &lua)?;
g(
&client,
without_mining,
RadiusGoal {
pos: azalea::Vec3::new(p.x, p.y, p.z),
radius: t.get("radius")?,
},
);
}
2 => {
let p = Vec3::from_lua(data, &lua)?;
g(
&client,
without_mining,
ReachBlockPosGoal {
pos: BlockPos::new(p.x as i32, p.y as i32, p.z as i32),
chunk_storage: client.world().read().chunks.clone(),
},
);
}
3 => {
let t = data.as_table().ok_or(error)?;
g(
&client,
without_mining,
XZGoal {
x: t.get("x")?,
z: t.get("z")?,
},
);
}
4 => g(
&client,
without_mining,
YGoal {
y: data.as_integer().ok_or(error)?,
},
),
_ => {
let p = Vec3::from_lua(data, &lua)?;
g(
&client,
without_mining,
BlockPosGoal(BlockPos::new(p.x as i32, p.y as i32, p.z as i32)),
);
}
}
while client.get_tick_broadcaster().recv().await.is_ok() {
if client.ecs.lock().get::<GotoEvent>(client.entity).is_none() {
break;
}
}
Ok(())
}
pub fn jump(_lua: &Lua, client: &mut Client, _: ()) -> Result<()> {
client.jump();
Ok(())
}
pub fn looking_at(lua: &Lua, client: &Client) -> Result<Option<Table>> {
let r = client.component::<HitResultComponent>();
Ok(if r.miss {
None
} else {
let result = lua.create_table()?;
result.set("position", Vec3::from(r.block_pos))?;
result.set("inside", r.inside)?;
result.set("world_border", r.world_border)?;
Some(result)
})
}
pub async fn look_at(_lua: Lua, client: UserDataRef<Client>, position: Vec3) -> Result<()> {
client
.clone()
.look_at(azalea::Vec3::new(position.x, position.y, position.z));
while client.get_tick_broadcaster().recv().await.is_ok() {
if client
.ecs
.lock()
.get::<LookAtEvent>(client.entity)
.is_none()
{
break;
}
}
Ok(())
}
pub fn pathfinder(lua: &Lua, client: &Client) -> Result<Table> {
let pathfinder = lua.create_table()?;
pathfinder.set(
"is_calculating",
client.component::<Pathfinder>().is_calculating,
)?;
pathfinder.set(
"is_executing",
if let Some(p) = client.get_component::<ExecutingPath>() {
pathfinder.set("last_reached_node", Vec3::from(p.last_reached_node))?;
pathfinder.set(
"last_node_reach_elapsed",
p.last_node_reached_at.elapsed().as_millis(),
)?;
pathfinder.set("is_path_partial", p.is_path_partial)?;
true
} else {
false
},
)?;
Ok(pathfinder)
}
pub fn position(_lua: &Lua, client: &Client) -> Result<Vec3> {
Ok(Vec3::from(&client.component::<Position>()))
}
pub fn set_direction(_lua: &Lua, client: &mut Client, direction: (f32, f32)) -> Result<()> {
client.set_direction(direction.0, direction.1);
Ok(())
}
pub fn set_jumping(_lua: &Lua, client: &mut Client, jumping: bool) -> Result<()> {
client.set_jumping(jumping);
Ok(())
}
pub fn set_sneaking(_lua: &Lua, client: &Client, sneaking: bool) -> Result<()> {
if let Err(error) = client.write_packet(ServerboundPlayerCommand {
id: client.entity.index(),
action: if sneaking {
Action::PressShiftKey
} else {
Action::ReleaseShiftKey
},
data: 0,
}) {
error!("failed to send PlayerCommand packet: {error:?}");
}
Ok(())
}
pub fn sprint(_lua: &Lua, client: &mut Client, direction: u8) -> Result<()> {
client.sprint(match direction {
5 => SprintDirection::ForwardRight,
6 => SprintDirection::ForwardLeft,
_ => SprintDirection::Forward,
});
Ok(())
}
pub fn stop_pathfinding(_lua: &Lua, client: &Client, _: ()) -> Result<()> {
client.stop_pathfinding();
Ok(())
}
pub fn stop_sleeping(_lua: &Lua, client: &Client, _: ()) -> Result<()> {
if let Err(error) = client.write_packet(ServerboundPlayerCommand {
id: client.entity.index(),
action: Action::StopSleeping,
data: 0,
}) {
error!("failed to send PlayerCommand packet: {error:?}");
}
Ok(())
}
pub fn walk(_lua: &Lua, client: &mut Client, direction: u8) -> Result<()> {
client.walk(match direction {
1 => WalkDirection::Forward,
2 => WalkDirection::Backward,
3 => WalkDirection::Left,
4 => WalkDirection::Right,
5 => WalkDirection::ForwardRight,
6 => WalkDirection::ForwardLeft,
7 => WalkDirection::BackwardRight,
8 => WalkDirection::BackwardLeft,
_ => WalkDirection::None,
});
Ok(())
}

45
src/lua/client/state.rs Normal file
View File

@ -0,0 +1,45 @@
use super::Client;
use azalea::{
ClientInformation,
entity::metadata::{AirSupply, Score},
};
use log::error;
use mlua::{Lua, Result, Table, UserDataRef};
pub fn air_supply(_lua: &Lua, client: &Client) -> Result<i32> {
Ok(client.component::<AirSupply>().0)
}
pub fn health(_lua: &Lua, client: &Client) -> Result<f32> {
Ok(client.health())
}
pub fn hunger(lua: &Lua, client: &Client) -> Result<Table> {
let h = client.hunger();
let hunger = lua.create_table()?;
hunger.set("food", h.food)?;
hunger.set("saturation", h.saturation)?;
Ok(hunger)
}
pub fn score(_lua: &Lua, client: &Client) -> Result<i32> {
Ok(client.component::<Score>().0)
}
pub async fn set_client_information(
_lua: Lua,
client: UserDataRef<Client>,
client_information: Table,
) -> Result<()> {
if let Err(error) = client
.set_client_information(ClientInformation {
view_distance: client_information.get("view_distance")?,
..ClientInformation::default()
})
.await
{
error!("failed to set client client information: {error:?}");
}
Ok(())
}

106
src/lua/client/world.rs Normal file
View File

@ -0,0 +1,106 @@
use super::{Client, Vec3};
use azalea::{
BlockPos,
auto_tool::AutoToolClientExt,
blocks::{BlockState, BlockStates},
ecs::query::Without,
entity::{Dead, EntityKind, EntityUuid, Position as AzaleaPosition, metadata::CustomName},
world::{InstanceName, MinecraftEntityId},
};
use mlua::{Function, Lua, Result, Table};
pub fn best_tool_for_block(lua: &Lua, client: &Client, block_state: u16) -> Result<Table> {
let tr = client.best_tool_in_hotbar_for_block(BlockState { id: block_state });
let tool_result = lua.create_table()?;
tool_result.set("index", tr.index)?;
tool_result.set("percentage_per_tick", tr.percentage_per_tick)?;
Ok(tool_result)
}
pub fn dimension(_lua: &Lua, client: &Client) -> Result<String> {
Ok(client.component::<InstanceName>().to_string())
}
pub fn find_blocks(
_lua: &Lua,
client: &Client,
(nearest_to, block_states): (Vec3, Vec<u16>),
) -> Result<Vec<Vec3>> {
#[allow(clippy::cast_possible_truncation)]
Ok(client
.world()
.read()
.find_blocks(
BlockPos::new(
nearest_to.x as i32,
nearest_to.y as i32,
nearest_to.z as i32,
),
&BlockStates {
set: block_states.iter().map(|&id| BlockState { id }).collect(),
},
)
.map(Vec3::from)
.collect())
}
pub fn find_entities(lua: &Lua, client: &Client, filter_fn: Function) -> Result<Vec<Table>> {
let mut matched = Vec::new();
let mut ecs = client.ecs.lock();
let mut query = ecs.query_filtered::<(
&MinecraftEntityId,
&EntityUuid,
&EntityKind,
&AzaleaPosition,
&CustomName,
), Without<Dead>>();
for (&id, uuid, kind, position, custom_name) in query.iter(&ecs) {
let entity = lua.create_table()?;
entity.set("id", id.0)?;
entity.set("uuid", uuid.to_string())?;
entity.set("kind", kind.to_string())?;
entity.set("position", Vec3::from(position))?;
entity.set("custom_name", custom_name.as_ref().map(ToString::to_string))?;
if filter_fn.call::<bool>(&entity)? {
matched.push(entity);
}
}
Ok(matched)
}
pub fn get_block_state(_lua: &Lua, client: &Client, position: Vec3) -> Result<Option<u16>> {
#[allow(clippy::cast_possible_truncation)]
Ok(client
.world()
.read()
.get_block_state(&BlockPos::new(
position.x as i32,
position.y as i32,
position.z as i32,
))
.map(|b| b.id))
}
pub fn get_fluid_state(lua: &Lua, client: &Client, position: Vec3) -> Result<Option<Table>> {
#[allow(clippy::cast_possible_truncation)]
Ok(
if let Some(fs) = client.world().read().get_fluid_state(&BlockPos::new(
position.x as i32,
position.y as i32,
position.z as i32,
)) {
let fluid_state = lua.create_table()?;
fluid_state.set("kind", fs.kind as u8)?;
fluid_state.set("amount", fs.amount)?;
fluid_state.set("falling", fs.falling)?;
Some(fluid_state)
} else {
None
},
)
}

View File

@ -0,0 +1,50 @@
use azalea::inventory::components::{CustomName, Damage, MaxDamage};
use mlua::{UserData, UserDataFields, UserDataMethods};
pub struct ItemStack {
pub inner: azalea::inventory::ItemStack,
}
impl UserData for ItemStack {
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_present", |_, this| Ok(this.inner.is_present()));
f.add_field_method_get("count", |_, this| Ok(this.inner.count()));
f.add_field_method_get("kind", |_, this| Ok(this.inner.kind().to_string()));
f.add_field_method_get("custom_name", |_, this| {
Ok(if let Some(data) = this.inner.as_present() {
data.components
.get::<CustomName>()
.map(|n| n.name.to_string())
} else {
None
})
});
f.add_field_method_get("damage", |_, this| {
Ok(if let Some(data) = this.inner.as_present() {
data.components.get::<Damage>().map(|d| d.amount)
} else {
None
})
});
f.add_field_method_get("max_damage", |_, this| {
Ok(if let Some(data) = this.inner.as_present() {
data.components.get::<MaxDamage>().map(|d| d.amount)
} else {
None
})
});
}
fn add_methods<M: UserDataMethods<Self>>(m: &mut M) {
m.add_method_mut("split", |_, this, count: u32| {
Ok(ItemStack {
inner: this.inner.split(count),
})
});
m.add_method_mut("update_empty", |_, this, (): ()| {
this.inner.update_empty();
Ok(())
});
}
}

132
src/lua/container/mod.rs Normal file
View File

@ -0,0 +1,132 @@
pub mod item_stack;
use azalea::{
container::{ContainerHandle, ContainerHandleRef},
inventory::operations::{
ClickOperation, CloneClick, PickupAllClick, PickupClick, QuickCraftClick, QuickCraftKind,
QuickCraftStatus, QuickMoveClick, SwapClick, ThrowClick,
},
};
use item_stack::ItemStack;
use mlua::{Result, Table, UserData, UserDataFields, UserDataMethods};
pub struct Container {
pub inner: ContainerHandle,
}
impl UserData for Container {
fn add_fields<F: UserDataFields<Self>>(f: &mut F) {
f.add_field_method_get("id", |_, this| Ok(this.inner.id()));
f.add_field_method_get("menu", |_, this| {
Ok(this.inner.menu().map(|m| format!("{m:?}")))
});
f.add_field_method_get("contents", |_, this| {
Ok(this.inner.contents().map(|v| {
v.iter()
.map(|i| ItemStack { inner: i.clone() })
.collect::<Vec<_>>()
}))
});
}
fn add_methods<M: UserDataMethods<Self>>(m: &mut M) {
m.add_method(
"click",
|_, this, (operation, operation_type): (Table, Option<u8>)| {
this.inner
.click(click_operation_from_table(operation, operation_type)?);
Ok(())
},
);
}
}
pub struct ContainerRef {
pub inner: ContainerHandleRef,
}
impl UserData for ContainerRef {
fn add_fields<F: UserDataFields<Self>>(f: &mut F) {
f.add_field_method_get("id", |_, this| Ok(this.inner.id()));
f.add_field_method_get("menu", |_, this| {
Ok(this.inner.menu().map(|m| format!("{m:?}")))
});
f.add_field_method_get("contents", |_, this| {
Ok(this.inner.contents().map(|v| {
v.iter()
.map(|i| ItemStack { inner: i.clone() })
.collect::<Vec<_>>()
}))
});
}
fn add_methods<M: UserDataMethods<Self>>(m: &mut M) {
m.add_method("close", |_, this, (): ()| {
this.inner.close();
Ok(())
});
m.add_method(
"click",
|_, this, (operation, operation_type): (Table, Option<u8>)| {
this.inner
.click(click_operation_from_table(operation, operation_type)?);
Ok(())
},
);
}
}
fn click_operation_from_table(op: Table, op_type: Option<u8>) -> Result<ClickOperation> {
Ok(match op_type.unwrap_or_default() {
0 => ClickOperation::Pickup(PickupClick::Left {
slot: op.get("slot")?,
}),
1 => ClickOperation::Pickup(PickupClick::Right {
slot: op.get("slot")?,
}),
2 => ClickOperation::Pickup(PickupClick::LeftOutside),
3 => ClickOperation::Pickup(PickupClick::RightOutside),
5 => ClickOperation::QuickMove(QuickMoveClick::Right {
slot: op.get("slot")?,
}),
6 => ClickOperation::Swap(SwapClick {
source_slot: op.get("source_slot")?,
target_slot: op.get("target_slot")?,
}),
7 => ClickOperation::Clone(CloneClick {
slot: op.get("slot")?,
}),
8 => ClickOperation::Throw(ThrowClick::Single {
slot: op.get("slot")?,
}),
9 => ClickOperation::Throw(ThrowClick::All {
slot: op.get("slot")?,
}),
10 => ClickOperation::QuickCraft(QuickCraftClick {
kind: match op.get("kind").unwrap_or_default() {
1 => QuickCraftKind::Right,
2 => QuickCraftKind::Middle,
_ => QuickCraftKind::Left,
},
status: match op.get("status").unwrap_or_default() {
1 => QuickCraftStatus::Add {
slot: op.get("slot")?,
},
2 => QuickCraftStatus::End,
_ => QuickCraftStatus::Start,
},
}),
11 => ClickOperation::PickupAll(PickupAllClick {
slot: op.get("slot")?,
reversed: op.get("reversed").unwrap_or_default(),
}),
_ => ClickOperation::QuickMove(QuickMoveClick::Left {
slot: op.get("slot")?,
}),
})
}

37
src/lua/direction.rs Normal file
View File

@ -0,0 +1,37 @@
use mlua::{FromLua, IntoLua, Lua, Result, Value};
#[derive(Clone)]
pub struct Direction {
pub x: f32,
pub y: f32,
}
impl IntoLua for Direction {
fn into_lua(self, lua: &Lua) -> Result<Value> {
let table = lua.create_table()?;
table.set("x", self.x)?;
table.set("y", self.y)?;
Ok(Value::Table(table))
}
}
impl FromLua for Direction {
fn from_lua(value: Value, _lua: &Lua) -> Result<Self> {
if let Value::Table(table) = value {
Ok(if let (Ok(x), Ok(y)) = (table.get(1), table.get(2)) {
Self { x, y }
} else {
Self {
x: table.get("x")?,
y: table.get("y")?,
}
})
} else {
Err(mlua::Error::FromLuaConversionError {
from: value.type_name(),
to: "Direction".to_string(),
message: None,
})
}
}
}

79
src/lua/events.rs Normal file
View File

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

42
src/lua/logging.rs Normal file
View File

@ -0,0 +1,42 @@
use log::{debug, error, info, trace, warn};
use mlua::{Lua, Result, Table};
pub fn register_functions(lua: &Lua, globals: &Table) -> Result<()> {
globals.set(
"error",
lua.create_function(|_, message: String| {
error!("{message}");
Ok(())
})?,
)?;
globals.set(
"warn",
lua.create_function(|_, message: String| {
warn!("{message}");
Ok(())
})?,
)?;
globals.set(
"info",
lua.create_function(|_, message: String| {
info!("{message}");
Ok(())
})?,
)?;
globals.set(
"debug",
lua.create_function(|_, message: String| {
debug!("{message}");
Ok(())
})?,
)?;
globals.set(
"trace",
lua.create_function(|_, message: String| {
trace!("{message}");
Ok(())
})?,
)?;
Ok(())
}

57
src/lua/mod.rs Normal file
View File

@ -0,0 +1,57 @@
pub mod block;
pub mod client;
pub mod container;
pub mod direction;
pub mod events;
pub mod logging;
pub mod player;
pub mod vec3;
use mlua::{Lua, Table};
#[derive(Debug)]
#[allow(dead_code)]
pub enum Error {
EvalChunk(mlua::Error),
ExecChunk(mlua::Error),
LoadChunk(mlua::Error),
MissingPath(mlua::Error),
ReadFile(std::io::Error),
}
pub fn register_functions(lua: &Lua, globals: &Table) -> mlua::Result<()> {
globals.set(
"sleep",
lua.create_async_function(async |_, duration: u64| {
tokio::time::sleep(std::time::Duration::from_millis(duration)).await;
Ok(())
})?,
)?;
block::register_functions(lua, globals)?;
logging::register_functions(lua, globals)
}
pub fn reload(lua: &Lua) -> Result<(), Error> {
lua.load(
&std::fs::read_to_string(
lua.globals()
.get::<String>("script_path")
.map_err(Error::MissingPath)?,
)
.map_err(Error::ReadFile)?,
)
.exec()
.map_err(Error::LoadChunk)
}
pub async fn eval(lua: &Lua, code: &str) -> Result<String, Error> {
lua.load(code)
.eval_async::<String>()
.await
.map_err(Error::EvalChunk)
}
pub async fn exec(lua: &Lua, code: &str) -> Result<(), Error> {
lua.load(code).exec_async().await.map_err(Error::ExecChunk)
}

35
src/lua/player.rs Normal file
View File

@ -0,0 +1,35 @@
use azalea::PlayerInfo;
use mlua::{IntoLua, Lua, Result, Value};
#[derive(Clone)]
pub struct Player {
pub display_name: Option<String>,
pub gamemode: String,
pub latency: i32,
pub name: String,
pub uuid: String,
}
impl From<PlayerInfo> for Player {
fn from(p: PlayerInfo) -> Self {
Self {
display_name: p.display_name.map(|n| n.to_string()),
gamemode: p.gamemode.name().to_owned(),
latency: p.latency,
name: p.profile.name,
uuid: p.uuid.to_string(),
}
}
}
impl IntoLua for Player {
fn into_lua(self, lua: &Lua) -> Result<Value> {
let table = lua.create_table()?;
table.set("display_name", self.display_name)?;
table.set("gamemode", self.gamemode)?;
table.set("latency", self.latency)?;
table.set("name", self.name)?;
table.set("uuid", self.uuid)?;
Ok(Value::Table(table))
}
}

77
src/lua/vec3.rs Normal file
View File

@ -0,0 +1,77 @@
use azalea::{BlockPos, entity::Position};
use mlua::{FromLua, IntoLua, Lua, Result, Value};
#[derive(Clone)]
pub struct Vec3 {
pub x: f64,
pub y: f64,
pub z: f64,
}
impl IntoLua for Vec3 {
fn into_lua(self, lua: &Lua) -> Result<Value> {
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 From<azalea::Vec3> for Vec3 {
fn from(v: azalea::Vec3) -> Self {
Self {
x: v.x,
y: v.y,
z: v.z,
}
}
}
impl From<&Position> for Vec3 {
fn from(p: &Position) -> Self {
Self {
x: p.x,
y: p.y,
z: p.z,
}
}
}
impl From<BlockPos> for Vec3 {
fn from(p: BlockPos) -> Self {
Vec3 {
x: f64::from(p.x),
y: f64::from(p.y),
z: f64::from(p.z),
}
}
}
impl FromLua for Vec3 {
fn from_lua(value: Value, _lua: &Lua) -> Result<Self> {
match value {
Value::Table(table) => Ok(
if let (Ok(x), Ok(y), Ok(z)) = (table.get(1), table.get(2), table.get(3)) {
Self { x, y, z }
} else {
Self {
x: table.get("x")?,
y: table.get("y")?,
z: table.get("z")?,
}
},
),
Value::Vector(vector) => Ok(Self {
x: vector.x().into(),
y: vector.y().into(),
z: vector.z().into(),
}),
_ => Err(mlua::Error::FromLuaConversionError {
from: value.type_name(),
to: "Vec3".to_string(),
message: None,
}),
}
}
}

View File

@ -1,660 +1,75 @@
mod bot;
mod logging;
mod matrix;
#![feature(let_chains)]
use azalea::pathfinder::BlockPosGoal;
use azalea::{prelude::*, BlockPos, ClientInformation, Vec3};
use azalea_protocol::packets::game::serverbound_client_command_packet::{
Action::PerformRespawn, ServerboundClientCommandPacket,
};
use azalea_protocol::packets::game::ClientboundGamePacket;
use azalea_protocol::ServerAddress;
use logging::LogMessageType::*;
use logging::{log_error, log_message};
use matrix::login_and_sync;
use rand::Rng;
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::sync::{Arc, Mutex};
use std::time::{SystemTime, UNIX_EPOCH};
mod arguments;
mod commands;
mod events;
mod http;
mod lua;
#[derive(Debug, Clone, Deserialize, Serialize)]
struct BotConfiguration {
username: String,
server_address: String,
register_keyword: String,
register_command: String,
login_keyword: String,
login_command: String,
bot_owners: Vec<String>,
whitelist: Vec<String>,
alert_players: Vec<String>,
alert_location: Vec<i32>,
alert_radius: u32,
alert_command: Vec<String>,
alert_pause_time: u32,
cleanup_interval: u32,
mob_expiry_time: u64,
mob_packet_drop_level: u8,
matrix: MatrixConfiguration,
}
use azalea::{brigadier::prelude::CommandDispatcher, prelude::*};
use clap::Parser;
use commands::{CommandSource, register};
use events::handle_event;
use futures::lock::Mutex;
use mlua::{Function, Lua};
use std::{collections::HashMap, net::SocketAddr, path::PathBuf, sync::Arc};
#[derive(Default, Debug, Clone, Deserialize, Serialize)]
pub struct MatrixConfiguration {
enabled: bool,
homeserver_url: String,
username: String,
password: String,
bot_owners: Vec<String>,
}
const DEFAULT_SCRIPT_PATH: &str = "errornowatcher.lua";
impl Default for BotConfiguration {
fn default() -> BotConfiguration {
BotConfiguration {
username: "ErrorNoWatcher".to_string(),
server_address: "localhost".to_string(),
register_keyword: "/register".to_string(),
register_command: "register 1VerySafePassword!!! 1VerySafePassword!!!".to_string(),
login_keyword: "/login".to_string(),
login_command: "login 1VerySafePassword!!!".to_string(),
bot_owners: vec![],
whitelist: vec![],
alert_players: vec![],
alert_location: vec![0, 0],
alert_radius: 100,
alert_command: Vec::new(),
alert_pause_time: 5,
cleanup_interval: 300,
mob_expiry_time: 300,
mob_packet_drop_level: 5,
matrix: MatrixConfiguration {
enabled: false,
homeserver_url: "https://matrix.example.com".to_string(),
username: "errornowatcher".to_string(),
password: "MyMatrixPassword".to_string(),
bot_owners: vec!["@zenderking:envs.net".to_string()],
},
}
}
type ListenerMap = HashMap<String, Vec<(String, Function)>>;
#[derive(Default, Clone, Component)]
pub struct State {
lua: Lua,
event_listeners: Arc<Mutex<ListenerMap>>,
commands: Arc<CommandDispatcher<Mutex<CommandSource>>>,
http_address: Option<SocketAddr>,
}
#[tokio::main]
async fn main() {
let bot_configuration: BotConfiguration = match toml::from_str(
&std::fs::read_to_string("bot_configuration.toml").unwrap_or_default(),
) {
Ok(bot_configuration) => bot_configuration,
Err(_) => {
let default_configuration = BotConfiguration::default();
std::fs::copy("bot_configuration.toml", "bot_configuration.toml.bak")
.unwrap_or_default();
match std::fs::write(
"bot_configuration.toml",
toml::to_string(&default_configuration).unwrap(),
) {
Ok(_) => (),
Err(error) => {
log_message(
Error,
&format!("Unable to save configuration file: {}", error),
);
return;
}
};
default_configuration
}
};
async fn main() -> anyhow::Result<()> {
let args = arguments::Arguments::parse();
let script_path = args.script.unwrap_or(PathBuf::from(DEFAULT_SCRIPT_PATH));
let original_state = State {
client: Arc::new(Mutex::new(None)),
bot_configuration: bot_configuration.clone(),
whitelist: Arc::new(Mutex::new(bot_configuration.clone().whitelist)),
bot_status: Arc::new(Mutex::new(BotStatus::default())),
stop_scripts: Arc::new(Mutex::new(false)),
tick_counter: Arc::new(Mutex::new(0)),
alert_second_counter: Arc::new(Mutex::new(0)),
cleanup_second_counter: Arc::new(Mutex::new(0)),
followed_player: Arc::new(Mutex::new(None)),
looked_player: Arc::new(Mutex::new(None)),
player_locations: Arc::new(Mutex::new(HashMap::new())),
mob_locations: Arc::new(Mutex::new(HashMap::new())),
player_timestamps: Arc::new(Mutex::new(HashMap::new())),
alert_players: Arc::new(Mutex::new(bot_configuration.clone().alert_players)),
alert_queue: Arc::new(Mutex::new(HashMap::new())),
bot_status_players: Arc::new(Mutex::new(Vec::new())),
};
let state = Arc::new(original_state);
let lua = Lua::new();
lua.load(
std::fs::read_to_string(&script_path)
.expect(&(DEFAULT_SCRIPT_PATH.to_owned() + " should be in current directory")),
)
.exec()?;
let matrix_configuration = bot_configuration.matrix.to_owned();
if matrix_configuration.enabled {
log_message(Matrix, &"Matrix is enabled! Logging in...".to_string());
tokio::spawn(login_and_sync(matrix_configuration, state.clone()));
}
let globals = lua.globals();
let server = globals
.get::<String>("SERVER")
.expect("SERVER should be in lua globals");
let username = globals
.get::<String>("USERNAME")
.expect("USERNAME should be in lua globals");
loop {
match azalea::start(azalea::Options {
account: Account::offline(&bot_configuration.username),
address: {
let segments: Vec<String> = bot_configuration
.server_address
.split(":")
.map(|item| item.to_string())
.collect();
if segments.len() == 1 {
ServerAddress {
host: segments[0].to_owned(),
port: 25565,
}
} else if segments.len() == 2 {
ServerAddress {
host: segments[0].to_owned(),
port: segments[1].to_owned().parse().unwrap_or(25565),
}
globals.set("script_path", script_path)?;
lua::register_functions(&lua, &globals)?;
let mut commands = CommandDispatcher::new();
register(&mut commands);
let Err(error) = ClientBuilder::new()
.set_handler(handle_event)
.set_state(State {
lua,
event_listeners: Arc::new(Mutex::new(HashMap::new())),
commands: Arc::new(commands),
http_address: args.http_address,
})
.start(
if username.contains('@') {
Account::microsoft(&username).await?
} else {
log_message(
Error,
&"Unable to parse server address! Quitting...".to_string(),
);
return;
}
Account::offline(&username)
},
state: state.clone(),
plugins: plugins![],
handle,
})
.await
{
Ok(_) => (),
Err(error) => log_message(Error, &format!("An error occurred: {}", error)),
}
log_message(
Bot,
&"ErrorNoWatcher has lost connection, reconnecting in 5 seconds...".to_string(),
);
std::thread::sleep(std::time::Duration::from_secs(5));
}
}
#[derive(Eq, Hash, PartialEq, PartialOrd, Default, Debug, Clone)]
pub struct Player {
uuid: String,
entity_id: u32,
username: String,
}
#[derive(Eq, Hash, PartialEq, PartialOrd, Default, Debug, Clone)]
pub struct Entity {
id: u32,
uuid: String,
entity_type: String,
}
#[derive(Default, Debug, Clone)]
pub struct PositionTimeData {
position: Vec<i32>,
time: u64,
}
#[derive(Eq, Hash, PartialEq, PartialOrd, Default, Debug, Clone)]
pub struct PlayerTimeData {
join_time: u64,
chat_message_time: u64,
leave_time: u64,
}
#[derive(Default, Debug, Clone)]
pub struct BotStatus {
health: f32,
food: u32,
saturation: f32,
}
#[derive(Clone)]
pub struct State {
client: Arc<Mutex<Option<azalea::Client>>>,
bot_configuration: BotConfiguration,
whitelist: Arc<Mutex<Vec<String>>>,
bot_status: Arc<Mutex<BotStatus>>,
stop_scripts: Arc<Mutex<bool>>,
tick_counter: Arc<Mutex<u8>>,
alert_second_counter: Arc<Mutex<u16>>,
cleanup_second_counter: Arc<Mutex<u16>>,
followed_player: Arc<Mutex<Option<Player>>>,
looked_player: Arc<Mutex<Option<Player>>>,
player_locations: Arc<Mutex<HashMap<Player, PositionTimeData>>>,
mob_locations: Arc<Mutex<HashMap<Entity, PositionTimeData>>>,
player_timestamps: Arc<Mutex<HashMap<String, PlayerTimeData>>>,
alert_players: Arc<Mutex<Vec<String>>>,
alert_queue: Arc<Mutex<HashMap<String, Vec<i32>>>>,
bot_status_players: Arc<Mutex<Vec<String>>>,
}
async fn handle(mut client: Client, event: Event, state: Arc<State>) -> anyhow::Result<()> {
match event {
Event::Login => {
*state.client.lock().unwrap() = Some(client.clone());
log_message(
Bot,
&"Successfully joined server, receiving initial data...".to_string(),
);
log_error(
client
.set_client_information(ClientInformation {
view_distance: (state.bot_configuration.alert_radius as f32 / 16.0).ceil()
as u8,
..Default::default()
})
.await,
);
}
Event::Death(_) => {
log_message(
Bot,
&"Player has died! Automatically respawning...".to_string(),
);
client
.write_packet(
ServerboundClientCommandPacket {
action: PerformRespawn,
}
.get(),
server.as_ref(),
)
.await?
}
Event::AddPlayer(player) => {
let mut player_timestamps = state.player_timestamps.lock().unwrap().to_owned();
let mut current_player = player_timestamps
.get(&player.profile.name)
.unwrap_or(&PlayerTimeData {
join_time: 0,
chat_message_time: 0,
leave_time: 0,
})
.to_owned();
current_player.join_time = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap()
.as_secs();
player_timestamps.insert(player.profile.name, current_player);
*state.player_timestamps.lock().unwrap() = player_timestamps;
}
Event::RemovePlayer(player) => {
let mut player_timestamps = state.player_timestamps.lock().unwrap().to_owned();
let mut current_player = player_timestamps
.get(&player.profile.name)
.unwrap_or(&PlayerTimeData {
join_time: 0,
chat_message_time: 0,
leave_time: 0,
})
.to_owned();
current_player.leave_time = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap()
.as_secs();
player_timestamps.insert(player.profile.name, current_player);
*state.player_timestamps.lock().unwrap() = player_timestamps;
}
Event::Tick => {
*state.tick_counter.lock().unwrap() += 1;
if *state.tick_counter.lock().unwrap() >= 20 {
*state.tick_counter.lock().unwrap() = 0;
*state.alert_second_counter.lock().unwrap() += 1;
*state.cleanup_second_counter.lock().unwrap() += 1;
let followed_player = state.followed_player.lock().unwrap().to_owned();
if followed_player.is_some() {
let player_locations = state.player_locations.lock().unwrap().to_owned();
match player_locations.get(&followed_player.unwrap()) {
Some(position_time_data) => client.goto(BlockPosGoal {
pos: BlockPos {
x: position_time_data.position[0],
y: position_time_data.position[1],
z: position_time_data.position[2],
},
}),
None => (),
}
}
let looked_player = state.looked_player.lock().unwrap().to_owned();
if looked_player.is_some() {
let player_locations = state.player_locations.lock().unwrap().to_owned();
match player_locations.get(&looked_player.unwrap()) {
Some(position_time_data) => client.look_at(&Vec3 {
x: position_time_data.position[0] as f64,
y: position_time_data.position[1] as f64,
z: position_time_data.position[2] as f64,
}),
None => (),
}
}
}
if *state.alert_second_counter.lock().unwrap() as u32
>= state.bot_configuration.alert_pause_time
{
*state.alert_second_counter.lock().unwrap() = 0;
let alert_queue = state.alert_queue.lock().unwrap().to_owned();
for (intruder, position) in alert_queue {
log_message(
Bot,
&format!(
"{} is in the specified alert radius at {} {} {}!",
intruder, position[0], position[1], position[2]
),
);
let alert_players = state.alert_players.lock().unwrap().to_vec();
for alert_player in alert_players {
log_error(
client
.send_command_packet(&format!(
"msg {} {}",
alert_player,
format!(
"{} is near our base at {} {} {}!",
intruder, position[0], position[1], position[2],
)
))
.await,
);
}
let mut alert_command = state.bot_configuration.alert_command.to_vec();
for argument in alert_command.iter_mut() {
*argument = argument.replace("{player_name}", &intruder);
*argument = argument.replace("{x}", &(position[0]).to_string());
*argument = argument.replace("{y}", &(position[1]).to_string());
*argument = argument.replace("{z}", &(position[2]).to_string());
}
if alert_command.len() >= 1 {
log_message(Bot, &"Executing alert shell command...".to_string());
let command_name = alert_command[0].to_owned();
alert_command.remove(0);
log_error(
std::process::Command::new(command_name)
.args(alert_command)
.stdin(std::process::Stdio::null())
.stdout(std::process::Stdio::null())
.stderr(std::process::Stdio::null())
.spawn(),
);
}
}
*state.alert_queue.lock().unwrap() = HashMap::new();
}
if *state.cleanup_second_counter.lock().unwrap() as u32
>= state.bot_configuration.cleanup_interval
{
*state.cleanup_second_counter.lock().unwrap() = 0;
log_message(Bot, &"Cleaning up mob locations...".to_string());
let mut mob_locations = state.mob_locations.lock().unwrap().to_owned();
let before_count = mob_locations.len();
let current_time = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap()
.as_secs();
for (mob, position_time_data) in mob_locations.to_owned() {
if current_time - position_time_data.time
> state.bot_configuration.mob_expiry_time
{
mob_locations.remove(&mob);
}
}
let after_count = mob_locations.len();
*state.mob_locations.lock().unwrap() = mob_locations;
let removed_count = before_count - after_count;
let mut label = "mobs";
if removed_count == 1 {
label = "mob";
}
log_message(
Bot,
&format!(
"Successfully removed {} {} ({} -> {})",
removed_count, label, before_count, after_count
),
);
}
}
Event::Packet(packet) => match packet.as_ref() {
ClientboundGamePacket::AddEntity(packet) => {
if packet.entity_type.to_string() != "Player" {
let entity = Entity {
id: packet.id,
uuid: packet.uuid.as_hyphenated().to_string(),
entity_type: packet.entity_type.to_string().to_lowercase(),
};
let mut mob_locations = state.mob_locations.lock().unwrap().to_owned();
mob_locations.insert(
entity,
PositionTimeData {
position: vec![packet.x as i32, packet.y as i32, packet.z as i32],
time: SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap()
.as_secs(),
},
);
*state.mob_locations.lock().unwrap() = mob_locations;
}
}
ClientboundGamePacket::MoveEntityPos(packet) => {
let world = client.world.read();
let raw_entity = match world.entity(packet.entity_id) {
Some(raw_entity) => raw_entity,
None => return Ok(()),
};
let entity_type = format!("{:?}", raw_entity.metadata)
.split("(")
.map(|item| item.to_owned())
.collect::<Vec<String>>()[0]
.to_lowercase();
if entity_type != "player" {
if rand::thread_rng().gen_range(0..10) + 1
> 10 - state.bot_configuration.mob_packet_drop_level
{
return Ok(());
}
}
let entity = Entity {
id: raw_entity.id,
uuid: raw_entity.uuid.as_hyphenated().to_string(),
entity_type: entity_type.to_owned(),
};
let entity_position = raw_entity.pos();
if entity_type == "player" {
let players = client.players.read().to_owned();
for (uuid, player) in players.iter().map(|item| item.to_owned()) {
if uuid.as_hyphenated().to_string() == entity.uuid {
let mut player_locations =
state.player_locations.lock().unwrap().to_owned();
let username = player.profile.name.to_owned();
for (player, _) in player_locations.to_owned() {
if player.username == username {
player_locations.remove(&player);
}
}
player_locations.insert(
Player {
uuid: uuid.as_hyphenated().to_string(),
entity_id: entity.id,
username,
},
PositionTimeData {
position: vec![
entity_position.x as i32,
entity_position.y as i32,
entity_position.z as i32,
],
time: SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap()
.as_secs(),
},
);
*state.player_locations.lock().unwrap() = player_locations;
if ((state.bot_configuration.alert_location[0]
- state.bot_configuration.alert_radius as i32)
..(state.bot_configuration.alert_location[0]
+ state.bot_configuration.alert_radius as i32))
.contains(&(entity_position.x as i32))
&& ((state.bot_configuration.alert_location[1]
- state.bot_configuration.alert_radius as i32)
..(state.bot_configuration.alert_location[1]
+ state.bot_configuration.alert_radius as i32))
.contains(&(entity_position.z as i32))
{
if !state
.whitelist
.lock()
.unwrap()
.contains(&player.profile.name)
{
let mut alert_queue =
state.alert_queue.lock().unwrap().to_owned();
alert_queue.insert(
player.profile.name.to_owned(),
vec![
entity_position.x as i32,
entity_position.y as i32,
entity_position.z as i32,
],
);
*state.alert_queue.lock().unwrap() = alert_queue;
}
}
}
}
} else {
let mut mob_locations = state.mob_locations.lock().unwrap().to_owned();
mob_locations.insert(
entity,
PositionTimeData {
position: vec![
entity_position.x as i32,
entity_position.y as i32,
entity_position.z as i32,
],
time: SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap()
.as_secs(),
},
);
*state.mob_locations.lock().unwrap() = mob_locations;
}
}
ClientboundGamePacket::SetHealth(packet) => {
*state.bot_status.lock().unwrap() = BotStatus {
health: packet.health,
food: packet.food,
saturation: packet.saturation,
};
let bot_status_players: Vec<String> = state
.bot_status_players
.lock()
.unwrap()
.iter()
.map(|item| item.to_owned())
.collect();
for player in bot_status_players {
log_error(
client
.send_command_packet(&format!(
"msg {} {}",
player,
format!(
"Health: {:.1}/20.0, Food: {}/20, Saturation: {:.1}/20.0",
packet.health, packet.food, packet.saturation
),
))
.await,
);
}
}
_ => (),
},
Event::Chat(message) => {
log_message(Chat, &message.message().to_ansi());
if message.username().is_none() {
if message
.content()
.contains(&state.bot_configuration.register_keyword)
{
log_message(
Bot,
&"Detected register keyword! Registering...".to_string(),
);
log_error(
client
.send_command_packet(&state.bot_configuration.register_command)
.await,
)
} else if message
.content()
.contains(&state.bot_configuration.login_keyword)
{
log_message(Bot, &"Detected login keyword! Logging in...".to_string());
log_error(
client
.send_command_packet(&state.bot_configuration.login_command)
.await,
)
}
return Ok(());
}
for bot_owner in state.bot_configuration.bot_owners.to_owned() {
if message
.message()
.to_string()
.starts_with(&format!("{} whispers to you: ", bot_owner))
{
let command = message
.message()
.to_string()
.split("whispers to you: ")
.nth(1)
.unwrap_or("")
.to_string();
let return_value =
&bot::process_command(&command, &bot_owner, &mut client, state.clone())
.await;
log_error(
client
.send_command_packet(&format!("msg {} {}", bot_owner, return_value))
.await,
);
}
let mut player_timestamps = state.player_timestamps.lock().unwrap().to_owned();
let mut current_player = player_timestamps
.get(&message.username().unwrap())
.unwrap_or(&PlayerTimeData {
join_time: 0,
chat_message_time: 0,
leave_time: 0,
})
.to_owned();
current_player.chat_message_time = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap()
.as_secs();
player_timestamps.insert(message.username().unwrap(), current_player);
*state.player_timestamps.lock().unwrap() = player_timestamps;
}
}
_ => {}
}
eprintln!("{error:?}");
Ok(())
}

View File

@ -1,150 +0,0 @@
use crate::logging::{log_error, LogMessageType::*};
use crate::{log_message, MatrixConfiguration, State};
use matrix_sdk::config::SyncSettings;
use matrix_sdk::event_handler::Ctx;
use matrix_sdk::room::Room;
use matrix_sdk::ruma::events::room::message::{
MessageType, OriginalSyncRoomMessageEvent, RoomMessageEventContent,
};
use std::sync::Arc;
#[derive(Clone)]
struct MatrixState {
bot_state: Arc<State>,
matrix_configuration: MatrixConfiguration,
display_name: String,
}
pub async fn login_and_sync(matrix_configuration: MatrixConfiguration, bot_state: Arc<State>) {
loop {
let client_builder =
matrix_sdk::Client::builder().homeserver_url(&matrix_configuration.homeserver_url);
let client = match client_builder.build().await {
Ok(client) => client,
Err(error) => {
log_message(MatrixError, &format!("Unable to build client: {}", error));
return;
}
};
match client
.login_username(
&matrix_configuration.username,
&matrix_configuration.password,
)
.device_id("ERRORNOWATCHER")
.initial_device_display_name("ErrorNoWatcher")
.send()
.await
{
Ok(_) => (),
Err(error) => {
log_message(MatrixError, &format!("Unable to login: {}", error));
return;
}
};
let response = match client.sync_once(SyncSettings::default()).await {
Ok(response) => response,
Err(error) => {
log_message(MatrixError, &format!("Unable to synchronize: {}", error));
return;
}
};
let display_name = match client.account().get_display_name().await {
Ok(display_name) => display_name.unwrap_or(match client.user_id() {
Some(user_id) => user_id.to_string(),
None => matrix_configuration.username.to_owned(),
}),
Err(error) => {
log_message(
MatrixError,
&format!("Unable to get display name: {}", error),
);
return;
}
};
log_message(
Matrix,
&format!("Successfully logged in as {}!", display_name),
);
let matrix_state = MatrixState {
bot_state: bot_state.clone(),
matrix_configuration: matrix_configuration.clone(),
display_name,
};
client.add_event_handler_context(matrix_state);
client.add_event_handler(room_message_handler);
let settings = SyncSettings::default().token(response.next_batch);
match client.sync(settings).await {
Ok(_) => (),
Err(error) => log_message(MatrixError, &format!("Unable to synchronize: {}", error)),
};
}
}
async fn room_message_handler(
event: OriginalSyncRoomMessageEvent,
room: Room,
state: Ctx<MatrixState>,
) {
if let Room::Joined(room) = room {
let MessageType::Text(text_content) = event.content.msgtype else {
return;
};
if state
.matrix_configuration
.bot_owners
.contains(&event.sender.to_string())
&& text_content.body.starts_with(&state.display_name)
{
let bot_state = state.bot_state.clone();
let client = bot_state.client.lock().unwrap().to_owned();
let mut client = match client {
Some(client) => client,
None => {
log_error(
room.send(
RoomMessageEventContent::text_plain(
"I am still joining the Minecraft server!",
),
None,
)
.await,
);
return;
}
};
let command = text_content
.body
.trim_start_matches(&state.display_name)
.trim_start_matches(":")
.trim()
.to_string();
log_message(
Matrix,
&format!(
"Executing command from {}: {}",
event.sender.to_string(),
command
),
);
tokio::task::spawn(async move {
log_error(
room.send(
RoomMessageEventContent::text_plain(
&crate::bot::process_command(
&command,
&event.sender.to_string(),
&mut client,
bot_state.clone(),
)
.await,
),
None,
)
.await,
);
});
}
}
}