feat: start work on 0.2.0
This commit is contained in:
parent
771057925d
commit
4fa508ec81
@ -1,10 +1,3 @@
|
|||||||
[target.x86_64-unknown-linux-gnu]
|
[target.x86_64-unknown-linux-gnu]
|
||||||
linker = "/usr/bin/clang"
|
linker = "clang"
|
||||||
rustflags = ["-Clink-arg=-fuse-ld=mold"]
|
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
6
.gitignore
vendored
@ -1,3 +1,3 @@
|
|||||||
/target
|
*.lua
|
||||||
/scripts
|
.luarc.json
|
||||||
/bot_configuration.toml
|
target
|
||||||
|
3948
Cargo.lock
generated
3948
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
36
Cargo.toml
36
Cargo.toml
@ -1,27 +1,25 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "errornowatcher"
|
name = "errornowatcher"
|
||||||
version = "0.1.0"
|
version = "0.2.0"
|
||||||
edition = "2021"
|
edition = "2024"
|
||||||
|
|
||||||
[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"
|
|
||||||
|
|
||||||
[profile.dev]
|
[profile.dev]
|
||||||
opt-level = 1
|
opt-level = 1
|
||||||
|
|
||||||
[profile.dev.package."*"]
|
[profile.dev.package."*"]
|
||||||
opt-level = 3
|
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"] }
|
||||||
|
71
README.md
71
README.md
@ -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
|
|
BIN
images/icon.png
BIN
images/icon.png
Binary file not shown.
Before Width: | Height: | Size: 6.5 KiB |
10
src/arguments.rs
Normal file
10
src/arguments.rs
Normal 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
1104
src/bot.rs
File diff suppressed because it is too large
Load Diff
101
src/commands.rs
Normal file
101
src/commands.rs
Normal 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
48
src/events.rs
Normal 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(())
|
||||||
|
}
|
@ -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())
|
|
||||||
}
|
|
727
src/main.rs
727
src/main.rs
@ -1,660 +1,89 @@
|
|||||||
mod bot;
|
#![feature(let_chains)]
|
||||||
mod logging;
|
|
||||||
mod matrix;
|
|
||||||
|
|
||||||
use azalea::pathfinder::BlockPosGoal;
|
mod arguments;
|
||||||
use azalea::{prelude::*, BlockPos, ClientInformation, Vec3};
|
mod commands;
|
||||||
use azalea_protocol::packets::game::serverbound_client_command_packet::{
|
mod events;
|
||||||
Action::PerformRespawn, ServerboundClientCommandPacket,
|
mod scripting;
|
||||||
};
|
|
||||||
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};
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, Deserialize, Serialize)]
|
use azalea::{brigadier::prelude::CommandDispatcher, prelude::*};
|
||||||
struct BotConfiguration {
|
use clap::Parser;
|
||||||
username: String,
|
use commands::{CommandSource, register};
|
||||||
server_address: String,
|
use events::handle_event;
|
||||||
register_keyword: String,
|
use mlua::Lua;
|
||||||
register_command: String,
|
use parking_lot::Mutex;
|
||||||
login_keyword: String,
|
use std::{path::PathBuf, process::ExitCode, sync::Arc};
|
||||||
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,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Default, Debug, Clone, Deserialize, Serialize)]
|
#[derive(Default, Clone, Component)]
|
||||||
pub struct MatrixConfiguration {
|
pub struct State {
|
||||||
enabled: bool,
|
lua: Arc<Mutex<Lua>>,
|
||||||
homeserver_url: String,
|
commands: Arc<CommandDispatcher<Mutex<CommandSource>>>,
|
||||||
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()],
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::main]
|
#[tokio::main]
|
||||||
async fn main() {
|
async fn main() -> ExitCode {
|
||||||
let bot_configuration: BotConfiguration = match toml::from_str(
|
let args = arguments::Arguments::parse();
|
||||||
&std::fs::read_to_string("bot_configuration.toml").unwrap_or_default(),
|
let lua = Lua::new();
|
||||||
) {
|
|
||||||
Ok(bot_configuration) => bot_configuration,
|
let config_path = args.script.unwrap_or(PathBuf::from("errornowatcher.lua"));
|
||||||
Err(_) => {
|
if let Err(error) = match &std::fs::read_to_string(&config_path) {
|
||||||
let default_configuration = BotConfiguration::default();
|
Ok(string) => lua.load(string).exec(),
|
||||||
std::fs::copy("bot_configuration.toml", "bot_configuration.toml.bak")
|
Err(error) => {
|
||||||
.unwrap_or_default();
|
eprintln!("failed to read {config_path:?}: {error:?}");
|
||||||
match std::fs::write(
|
return ExitCode::FAILURE;
|
||||||
"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
|
|
||||||
}
|
}
|
||||||
};
|
} {
|
||||||
|
eprintln!("failed to execute configuration as lua code: {error:?}");
|
||||||
let original_state = State {
|
return ExitCode::FAILURE;
|
||||||
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 {
|
let globals = lua.globals();
|
||||||
match azalea::start(azalea::Options {
|
let Ok(server) = globals.get::<String>("SERVER") else {
|
||||||
account: Account::offline(&bot_configuration.username),
|
eprintln!("no server defined in lua globals!");
|
||||||
address: {
|
return ExitCode::FAILURE;
|
||||||
let segments: Vec<String> = bot_configuration
|
};
|
||||||
.server_address
|
let Ok(username) = globals.get::<String>("USERNAME") else {
|
||||||
.split(":")
|
eprintln!("no username defined in lua globals!");
|
||||||
.map(|item| item.to_string())
|
return ExitCode::FAILURE;
|
||||||
.collect();
|
};
|
||||||
if segments.len() == 1 {
|
|
||||||
ServerAddress {
|
if let Err(error) = globals.set("config_path", config_path) {
|
||||||
host: segments[0].to_owned(),
|
eprintln!("failed to set config_path in lua globals: {error:?}");
|
||||||
port: 25565,
|
return ExitCode::FAILURE;
|
||||||
}
|
};
|
||||||
} else if segments.len() == 2 {
|
|
||||||
ServerAddress {
|
let Ok(server) = globals.get::<String>("Server") else {
|
||||||
host: segments[0].to_owned(),
|
eprintln!("no server defined in lua globals!");
|
||||||
port: segments[1].to_owned().parse().unwrap_or(25565),
|
return ExitCode::FAILURE;
|
||||||
}
|
};
|
||||||
} else {
|
let Ok(username) = globals.get::<String>("Username") else {
|
||||||
log_message(
|
eprintln!("no username defined in lua globals!");
|
||||||
Error,
|
return ExitCode::FAILURE;
|
||||||
&"Unable to parse server address! Quitting...".to_string(),
|
};
|
||||||
);
|
|
||||||
return;
|
let account = if username.contains('@') {
|
||||||
}
|
match Account::microsoft(&username).await {
|
||||||
},
|
Ok(a) => a,
|
||||||
state: state.clone(),
|
Err(error) => {
|
||||||
plugins: plugins![],
|
eprintln!("failed to login using microsoft account: {error:?}");
|
||||||
handle,
|
return ExitCode::FAILURE;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Account::offline(&username)
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut commands = CommandDispatcher::new();
|
||||||
|
register(&mut commands);
|
||||||
|
|
||||||
|
let Err(error) = ClientBuilder::new()
|
||||||
|
.set_handler(handle_event)
|
||||||
|
.set_state(State {
|
||||||
|
lua: Arc::new(Mutex::new(lua)),
|
||||||
|
commands: Arc::new(commands),
|
||||||
})
|
})
|
||||||
.await
|
.start(account, server.as_ref())
|
||||||
{
|
.await;
|
||||||
Ok(_) => (),
|
eprintln!("{error:?}");
|
||||||
Err(error) => log_message(Error, &format!("An error occurred: {}", error)),
|
|
||||||
}
|
ExitCode::SUCCESS
|
||||||
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(),
|
|
||||||
};
|
|
||||||
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
_ => {}
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
}
|
||||||
|
150
src/matrix.rs
150
src/matrix.rs
@ -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
109
src/scripting/client.rs
Normal 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
2
src/scripting/mod.rs
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
pub mod client;
|
||||||
|
pub mod position;
|
13
src/scripting/position.rs
Normal file
13
src/scripting/position.rs
Normal 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")?))
|
||||||
|
}
|
Loading…
x
Reference in New Issue
Block a user