feat: start work on 0.2.0

This commit is contained in:
Ryan 2025-01-19 05:13:25 -05:00
parent 771057925d
commit 4fa508ec81
Signed by: ErrorNoInternet
GPG Key ID: 2486BFB7B1E6A4A3
16 changed files with 2363 additions and 4046 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"]

6
.gitignore vendored
View File

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

3948
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -1,27 +1,25 @@
[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
panic = "abort"
strip = true
[dependencies]
anyhow = "1"
azalea = { git = "https://github.com/azalea-rs/azalea.git" }
clap = { version = "4", features = ["derive"] }
futures = "0"
mlua = { version = "0", features = ["async", "luau", "send"] }
parking_lot = { version = "0" }
tokio = { version = "1", features = ["macros"] }

View File

@ -1,71 +0,0 @@
<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>
## Compiling
```sh
git clone https://github.com/ErrorNoInternet/ErrorNoWatcher
cd ErrorNoWatcher
cargo build --release
```
The compiled executable can be found at `./target/release/errornowatcher`
## 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>"]
```
### 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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.5 KiB

10
src/arguments.rs Normal file
View File

@ -0,0 +1,10 @@
use clap::Parser;
use std::path::PathBuf;
/// A Minecraft utility bot
#[derive(Parser)]
pub struct Arguments {
/// Path to main Lua file
#[arg(short, long)]
pub script: Option<PathBuf>,
}

1104
src/bot.rs

File diff suppressed because it is too large Load Diff

101
src/commands.rs Normal file
View File

@ -0,0 +1,101 @@
use crate::State;
use azalea::{
GameProfileComponent, brigadier::prelude::*, chat::ChatPacket, entity::metadata::Player,
prelude::*,
};
use bevy_ecs::{entity::Entity, query::With};
use parking_lot::Mutex;
pub type Ctx<'a> = CommandContext<Mutex<CommandSource>>;
pub struct CommandSource {
pub client: Client,
pub message: ChatPacket,
pub state: State,
}
impl CommandSource {
pub fn reply(&self, message: &str) {
if self.message.is_whisper()
&& let Some(username) = self.message.username()
{
self.client.chat(&format!("/w {username} {message}"));
} else {
self.client.chat(message);
}
}
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.lock();
let lua = source.state.lua.lock();
let config_path = match lua.globals().get::<String>("config_path") {
Ok(path) => path,
Err(error) => {
source.reply(&format!(
"failed to get config_path from lua globals: {error:?}"
));
return 0;
}
};
if let Err(error) = match &std::fs::read_to_string(&config_path) {
Ok(string) => lua.load(string).exec(),
Err(error) => {
source.reply(&format!("failed to read {config_path:?}: {error:?}"));
return 0;
}
} {
source.reply(&format!(
"failed to execute configuration as lua code: {error:?}"
));
return 0;
}
1
}));
commands.register(
literal("eval").then(argument("expr", string()).executes(|ctx: &Ctx| {
let source = ctx.source.lock();
source.reply(&format!(
"{:?}",
source
.state
.lua
.lock()
.load(get_string(ctx, "expr").unwrap())
.eval::<String>()
));
1
})),
);
commands.register(
literal("exec").then(argument("code", string()).executes(|ctx: &Ctx| {
let source = ctx.source.lock();
source.reply(&format!(
"{:?}",
source
.state
.lua
.lock()
.load(get_string(ctx, "code").unwrap())
.exec()
));
1
})),
);
commands.register(literal("ping").executes(|ctx: &Ctx| {
ctx.source.lock().reply("pong!");
1
}));
}

48
src/events.rs Normal file
View File

@ -0,0 +1,48 @@
use crate::{State, commands::CommandSource, scripting};
use azalea::prelude::*;
use mlua::Function;
pub async fn handle_event(client: Client, event: Event, state: State) -> anyhow::Result<()> {
let globals = state.lua.lock().globals();
match event {
Event::Chat(message) => {
println!("{}", message.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,
}
.reply(&format!("{error:?}"));
};
}
}
Event::Init => {
globals.set(
"client",
scripting::client::Client {
inner: Some(client),
},
)?;
globals.get::<Function>("Init")?.call::<()>(())?
}
_ => (),
};
Ok(())
}

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())
}

View File

@ -1,660 +1,89 @@
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 scripting;
#[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 mlua::Lua;
use parking_lot::Mutex;
use std::{path::PathBuf, process::ExitCode, sync::Arc};
#[derive(Default, Debug, Clone, Deserialize, Serialize)]
pub struct MatrixConfiguration {
enabled: bool,
homeserver_url: String,
username: String,
password: String,
bot_owners: Vec<String>,
}
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()],
},
}
}
#[derive(Default, Clone, Component)]
pub struct State {
lua: Arc<Mutex<Lua>>,
commands: Arc<CommandDispatcher<Mutex<CommandSource>>>,
}
#[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(_) => (),
async fn main() -> ExitCode {
let args = arguments::Arguments::parse();
let lua = Lua::new();
let config_path = args.script.unwrap_or(PathBuf::from("errornowatcher.lua"));
if let Err(error) = match &std::fs::read_to_string(&config_path) {
Ok(string) => lua.load(string).exec(),
Err(error) => {
log_message(
Error,
&format!("Unable to save configuration file: {}", error),
);
return;
eprintln!("failed to read {config_path:?}: {error:?}");
return ExitCode::FAILURE;
}
} {
eprintln!("failed to execute configuration as lua code: {error:?}");
return ExitCode::FAILURE;
}
let globals = lua.globals();
let Ok(server) = globals.get::<String>("SERVER") else {
eprintln!("no server defined in lua globals!");
return ExitCode::FAILURE;
};
default_configuration
}
let Ok(username) = globals.get::<String>("USERNAME") else {
eprintln!("no username defined in lua globals!");
return ExitCode::FAILURE;
};
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 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()));
}
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),
}
} else {
log_message(
Error,
&"Unable to parse server address! Quitting...".to_string(),
);
return;
}
},
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(),
)
.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(),
if let Err(error) = globals.set("config_path", config_path) {
eprintln!("failed to set config_path in lua globals: {error:?}");
return ExitCode::FAILURE;
};
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 Ok(server) = globals.get::<String>("Server") else {
eprintln!("no server defined in lua globals!");
return ExitCode::FAILURE;
};
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 Ok(username) = globals.get::<String>("Username") else {
eprintln!("no username defined in lua globals!");
return ExitCode::FAILURE;
};
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;
}
}
let account = if username.contains('@') {
match Account::microsoft(&username).await {
Ok(a) => a,
Err(error) => {
eprintln!("failed to login using microsoft account: {error:?}");
return ExitCode::FAILURE;
}
}
} else {
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,
Account::offline(&username)
};
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(());
}
let mut commands = CommandDispatcher::new();
register(&mut commands);
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())
let Err(error) = ClientBuilder::new()
.set_handler(handle_event)
.set_state(State {
lua: Arc::new(Mutex::new(lua)),
commands: Arc::new(commands),
})
.start(account, server.as_ref())
.await;
log_error(
client
.send_command_packet(&format!("msg {} {}", bot_owner, return_value))
.await,
);
}
eprintln!("{error:?}");
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;
}
}
_ => {}
}
Ok(())
ExitCode::SUCCESS
}

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,
);
});
}
}
}

109
src/scripting/client.rs Normal file
View File

@ -0,0 +1,109 @@
use super::position::{from_table, to_table};
use azalea::{
BlockPos, Client as AzaleaClient, ClientInformation,
ecs::query::Without,
entity::{Dead, EntityKind, EntityUuid, Position, metadata::CustomName},
pathfinder::goals::BlockPosGoal,
prelude::PathfinderClientExt,
world::MinecraftEntityId,
};
use mlua::{Error, Function, Lua, Result, Table, UserData, UserDataRef};
pub struct Client {
pub inner: Option<AzaleaClient>,
}
impl UserData for Client {
fn add_fields<F: mlua::UserDataFields<Self>>(fields: &mut F) {
fields.add_field_method_get("pos", |lua, this| {
let pos = this.inner.as_ref().unwrap().position();
to_table(lua, pos.x, pos.y, pos.z)
});
}
fn add_methods<M: mlua::UserDataMethods<Self>>(methods: &mut M) {
methods.add_async_method("set_client_information", set_client_information);
methods.add_method("get_entity", get_entity);
methods.add_method_mut("get_entity_position", get_entity_position);
methods.add_method_mut("goto", goto);
methods.add_method_mut("stop", stop);
}
}
async fn set_client_information(
_lua: Lua,
client: UserDataRef<Client>,
client_information: Table,
) -> Result<()> {
client
.inner
.as_ref()
.unwrap()
.set_client_information(ClientInformation {
view_distance: client_information.get("view_distance")?,
..ClientInformation::default()
})
.await
.unwrap();
Ok(())
}
fn get_entity(lua: &Lua, client: &Client, filter_fn: Function) -> Result<u32> {
let mut ecs = client.inner.as_ref().unwrap().ecs.lock();
let mut query = ecs.query_filtered::<(
&MinecraftEntityId,
&EntityUuid,
&EntityKind,
&Position,
&CustomName,
), Without<Dead>>();
for (&entity_id, entity_uuid, entity_kind, pos, custom_name) in query.iter(&ecs) {
let entity = lua.create_table()?;
entity.set("id", entity_id.0)?;
entity.set("uuid", entity_uuid.to_string())?;
entity.set("kind", entity_kind.0.to_string())?;
entity.set("pos", to_table(lua, pos.x, pos.y, pos.z)?)?;
if let Some(n) = &**custom_name {
entity.set("custom_name", n.to_string())?;
}
if filter_fn.call::<bool>(entity).unwrap() {
return Ok(entity_id.0);
};
}
Err(Error::RuntimeError("entity not found".to_string()))
}
fn get_entity_position(lua: &Lua, client: &mut Client, entity_id: u32) -> Result<Table> {
let client = client.inner.as_mut().unwrap();
let entity = client
.entity_by::<Without<Dead>, &MinecraftEntityId>(|query_entity_id: &&MinecraftEntityId| {
query_entity_id.0 == entity_id
})
.unwrap();
let pos = client.entity_component::<Position>(entity);
to_table(lua, pos.x, pos.y, pos.z)
}
fn goto(_lua: &Lua, client: &mut Client, pos_table: Table) -> Result<()> {
let pos = from_table(&pos_table)?;
#[allow(clippy::cast_possible_truncation)]
client
.inner
.as_ref()
.unwrap()
.goto(BlockPosGoal(BlockPos::new(
pos.0 as i32,
pos.1 as i32,
pos.2 as i32,
)));
Ok(())
}
fn stop(_lua: &Lua, client: &mut Client, _: ()) -> Result<()> {
client.inner.as_ref().unwrap().stop_pathfinding();
Ok(())
}

2
src/scripting/mod.rs Normal file
View File

@ -0,0 +1,2 @@
pub mod client;
pub mod position;

13
src/scripting/position.rs Normal file
View File

@ -0,0 +1,13 @@
use mlua::{Lua, Result, Table};
pub fn to_table(lua: &Lua, x: f64, y: f64, z: f64) -> Result<Table> {
let table = lua.create_table()?;
table.set("x", x)?;
table.set("y", y)?;
table.set("z", z)?;
Ok(table)
}
pub fn from_table(table: &Table) -> Result<(f64, f64, f64)> {
Ok((table.get("x")?, table.get("y")?, table.get("z")?))
}