chore: merge branch v0.2.0 into main
This commit is contained in:
commit
aadc9a919e
@ -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
5
.gitignore
vendored
@ -1,3 +1,2 @@
|
||||
/target
|
||||
/scripts
|
||||
/bot_configuration.toml
|
||||
.luarc.json
|
||||
target
|
||||
|
3946
Cargo.lock
generated
3946
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
38
Cargo.toml
38
Cargo.toml
@ -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"] }
|
||||
|
85
README.md
85
README.md
@ -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
17
errornowatcher.lua
Normal 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
|
BIN
images/icon.png
BIN
images/icon.png
Binary file not shown.
Before Width: | Height: | Size: 6.5 KiB |
34
lib/enum.lua
Normal file
34
lib/enum.lua
Normal 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
30
lib/events.lua
Normal 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
38
lib/inventory.lua
Normal 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
18
lib/movement.lua
Normal 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
48
lib/utils.lua
Normal 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
14
src/arguments.rs
Normal 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
1104
src/bot.rs
File diff suppressed because it is too large
Load Diff
90
src/commands.rs
Normal file
90
src/commands.rs
Normal 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
129
src/events.rs
Normal 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
63
src/http.rs
Normal 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()
|
||||
}
|
@ -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
62
src/lua/block.rs
Normal 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)
|
||||
}
|
67
src/lua/client/container.rs
Normal file
67
src/lua/client/container.rs
Normal 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(())
|
||||
}
|
84
src/lua/client/interaction.rs
Normal file
84
src/lua/client/interaction.rs
Normal 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
110
src/lua/client/mod.rs
Normal 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
240
src/lua/client/movement.rs
Normal 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
45
src/lua/client/state.rs
Normal 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
106
src/lua/client/world.rs
Normal 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
|
||||
},
|
||||
)
|
||||
}
|
50
src/lua/container/item_stack.rs
Normal file
50
src/lua/container/item_stack.rs
Normal 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
132
src/lua/container/mod.rs
Normal 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
37
src/lua/direction.rs
Normal 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
79
src/lua/events.rs
Normal 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
42
src/lua/logging.rs
Normal 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
57
src/lua/mod.rs
Normal 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
35
src/lua/player.rs
Normal 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
77
src/lua/vec3.rs
Normal 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,
|
||||
}),
|
||||
}
|
||||
}
|
||||
}
|
701
src/main.rs
701
src/main.rs
@ -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(())
|
||||
}
|
||||
|
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,
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user