├── .gitignore ├── rustfmt.toml ├── rust-toolchain.toml ├── src ├── replay │ ├── mod.rs │ ├── plugin.rs │ └── recorder.rs ├── lua │ ├── matrix │ │ ├── mod.rs │ │ ├── member.rs │ │ ├── client.rs │ │ └── room.rs │ ├── nochatreports │ │ ├── key.rs │ │ ├── crypt.rs │ │ └── mod.rs │ ├── thread.rs │ ├── player.rs │ ├── logging.rs │ ├── direction.rs │ ├── client │ │ ├── world │ │ │ ├── mod.rs │ │ │ ├── queries.rs │ │ │ └── find.rs │ │ ├── interaction.rs │ │ ├── state.rs │ │ ├── container.rs │ │ ├── mod.rs │ │ └── movement.rs │ ├── vec3.rs │ ├── container │ │ ├── click.rs │ │ ├── mod.rs │ │ └── item_stack.rs │ ├── system.rs │ ├── block.rs │ ├── events.rs │ └── mod.rs ├── build_info.rs ├── arguments.rs ├── hacks │ ├── anti_knockback.rs │ └── mod.rs ├── http.rs ├── commands.rs ├── matrix │ ├── bot.rs │ ├── mod.rs │ └── verification.rs ├── main.rs ├── particle.rs └── events.rs ├── .cargo └── config.toml ├── lib ├── movement.lua ├── enum.lua ├── lib.lua ├── events.lua ├── inventory.lua ├── automation.lua └── utils.lua ├── main.lua ├── README.md ├── Cargo.toml ├── .github └── workflows │ ├── build.yaml │ └── lint.yaml └── LICENSE /.gitignore: -------------------------------------------------------------------------------- 1 | .luarc.json 2 | target 3 | -------------------------------------------------------------------------------- /rustfmt.toml: -------------------------------------------------------------------------------- 1 | group_imports = "StdExternalCrate" 2 | -------------------------------------------------------------------------------- /rust-toolchain.toml: -------------------------------------------------------------------------------- 1 | [toolchain] 2 | channel = "nightly" 3 | -------------------------------------------------------------------------------- /src/replay/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod plugin; 2 | pub mod recorder; 3 | -------------------------------------------------------------------------------- /src/lua/matrix/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod client; 2 | pub mod member; 3 | pub mod room; 4 | -------------------------------------------------------------------------------- /.cargo/config.toml: -------------------------------------------------------------------------------- 1 | [target.x86_64-unknown-linux-gnu] 2 | linker = "clang" 3 | rustflags = ["-Clink-arg=-fuse-ld=mold", "-Zshare-generics=y"] 4 | 5 | [target.aarch64-unknown-linux-gnu] 6 | linker = "clang" 7 | rustflags = ["-Clink-arg=-fuse-ld=mold", "-Zshare-generics=y"] 8 | -------------------------------------------------------------------------------- /src/build_info.rs: -------------------------------------------------------------------------------- 1 | pub mod built { 2 | include!(concat!(env!("OUT_DIR"), "/built.rs")); 3 | } 4 | 5 | pub fn version_formatted() -> String { 6 | format!( 7 | "v{} ({})", 8 | env!("CARGO_PKG_VERSION"), 9 | built::GIT_COMMIT_HASH_SHORT.unwrap_or("unknown commit") 10 | ) 11 | } 12 | -------------------------------------------------------------------------------- /lib/movement.lua: -------------------------------------------------------------------------------- 1 | function look_at_player(name) 2 | local player = get_player(name) 3 | if player then 4 | player.position.y = player.position.y + 1 5 | client:look_at(player.position) 6 | end 7 | end 8 | 9 | function go_to_player(name, go_to_opts) 10 | client:go_to(get_player(name).position, go_to_opts) 11 | end 12 | -------------------------------------------------------------------------------- /src/lua/nochatreports/key.rs: -------------------------------------------------------------------------------- 1 | use mlua::{UserData, UserDataFields}; 2 | 3 | pub struct AesKey(pub ncr::AesKey); 4 | 5 | impl UserData for AesKey { 6 | fn add_fields>(f: &mut F) { 7 | f.add_field_method_get("base64", |_, this| Ok(this.0.encode_base64())); 8 | f.add_field_method_get("bytes", |_, this| Ok(this.0.as_ref().to_vec())); 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /main.lua: -------------------------------------------------------------------------------- 1 | Server = "localhost" 2 | Username = "ErrorNoWatcher" 3 | HttpAddress = "127.0.0.1:8080" 4 | Owners = { "ErrorNoInternet" } 5 | MatrixOptions = { owners = { "@errornointernet:envs.net" } } 6 | 7 | for _, module in ipairs({ 8 | "lib", 9 | "automation", 10 | "enum", 11 | "events", 12 | "inventory", 13 | "movement", 14 | "utils", 15 | }) do 16 | module = "lib/" .. module 17 | package.loaded[module] = nil 18 | require(module) 19 | end 20 | 21 | update_listeners() 22 | -------------------------------------------------------------------------------- /src/arguments.rs: -------------------------------------------------------------------------------- 1 | use std::path::PathBuf; 2 | 3 | use clap::Parser; 4 | 5 | use crate::build_info; 6 | 7 | /// A Minecraft bot with Lua scripting support 8 | #[derive(Parser)] 9 | #[command(version = build_info::version_formatted())] 10 | pub struct Arguments { 11 | /// Path to main Lua file 12 | #[arg(short, long)] 13 | pub script: Option, 14 | 15 | /// Code to execute (after script) 16 | #[arg(short, long)] 17 | pub exec: Option, 18 | } 19 | -------------------------------------------------------------------------------- /src/lua/matrix/member.rs: -------------------------------------------------------------------------------- 1 | use matrix_sdk::room::RoomMember; 2 | use mlua::{UserData, UserDataFields}; 3 | 4 | pub struct Member(pub RoomMember); 5 | 6 | impl UserData for Member { 7 | fn add_fields>(f: &mut F) { 8 | f.add_field_method_get("id", |_, this| Ok(this.0.user_id().to_string())); 9 | f.add_field_method_get("name", |_, this| Ok(this.0.name().to_owned())); 10 | f.add_field_method_get("power_level", |_, this| Ok(this.0.power_level())); 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/hacks/anti_knockback.rs: -------------------------------------------------------------------------------- 1 | use azalea::{ 2 | Vec3, 3 | movement::{KnockbackEvent, KnockbackType}, 4 | prelude::Component, 5 | }; 6 | use bevy_ecs::{event::EventMutator, query::With, system::Query}; 7 | 8 | #[derive(Component)] 9 | pub struct AntiKnockback; 10 | 11 | pub fn anti_knockback( 12 | mut events: EventMutator, 13 | entity_query: Query<(), With>, 14 | ) { 15 | for event in events.read() { 16 | if entity_query.get(event.entity).is_ok() { 17 | event.knockback = KnockbackType::Add(Vec3::default()); 18 | } 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/hacks/mod.rs: -------------------------------------------------------------------------------- 1 | #![allow(clippy::needless_pass_by_value)] 2 | 3 | pub mod anti_knockback; 4 | 5 | use anti_knockback::anti_knockback; 6 | use azalea::{movement::handle_knockback, packet::game::process_packet_events}; 7 | use bevy_app::{App, Plugin, PreUpdate}; 8 | use bevy_ecs::schedule::IntoSystemConfigs; 9 | 10 | pub struct HacksPlugin; 11 | 12 | impl Plugin for HacksPlugin { 13 | fn build(&self, app: &mut App) { 14 | app.add_systems( 15 | PreUpdate, 16 | anti_knockback 17 | .after(process_packet_events) 18 | .before(handle_knockback), 19 | ); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ErrorNoWatcher 2 | 3 | A Minecraft bot with Lua scripting support, written in Rust with [azalea](https://github.com/azalea-rs/azalea). 4 | 5 | ## Features 6 | 7 | - Running Lua from 8 | - in-game chat messages 9 | - Matrix chat messages 10 | - POST requests to HTTP server 11 | - Listening to in-game events 12 | - Pathfinding (from azalea) 13 | - Entity and chest interaction 14 | - NoChatReports encryption 15 | - Saving ReplayMod recordings 16 | - Matrix integration (w/ E2EE) 17 | 18 | ## Usage 19 | 20 | ```sh 21 | $ git clone https://github.com/ErrorNoInternet/ErrorNoWatcher 22 | $ cd ErrorNoWatcher 23 | $ cargo build --release 24 | $ # ./target/release/errornowatcher 25 | ``` 26 | 27 | Make sure the `Server` and `Username` globals are defined in `main.lua` before starting the bot. 28 | -------------------------------------------------------------------------------- /src/lua/thread.rs: -------------------------------------------------------------------------------- 1 | use std::time::Duration; 2 | 3 | use mlua::{Error, Function, Lua, Result, Table}; 4 | use tokio::time::{sleep, timeout}; 5 | 6 | pub fn register_globals(lua: &Lua, globals: &Table) -> Result<()> { 7 | globals.set( 8 | "sleep", 9 | lua.create_async_function(async |_, duration: u64| { 10 | sleep(Duration::from_millis(duration)).await; 11 | Ok(()) 12 | })?, 13 | )?; 14 | 15 | globals.set( 16 | "timeout", 17 | lua.create_async_function(async |_, (duration, function): (u64, Function)| { 18 | timeout( 19 | Duration::from_millis(duration), 20 | function.call_async::<()>(()), 21 | ) 22 | .await 23 | .map_err(Error::external) 24 | })?, 25 | )?; 26 | 27 | Ok(()) 28 | } 29 | -------------------------------------------------------------------------------- /lib/enum.lua: -------------------------------------------------------------------------------- 1 | NONE = 0 2 | FORWARD = 1 3 | BACKWARD = 2 4 | LEFT = 3 5 | RIGHT = 4 6 | FORWARD_LEFT = 5 7 | FORWARD_RIGHT = 6 8 | BACKWARD_LEFT = 7 9 | BACKWARD_RIGHT = 8 10 | 11 | BLOCK_POS_GOAL = 0 12 | RADIUS_GOAL = 1 13 | REACH_BLOCK_POS_GOAL = 2 14 | XZ_GOAL = 3 15 | Y_GOAL = 4 16 | 17 | PICKUP_LEFT = 0 18 | PICKUP_RIGHT = 1 19 | PICKUP_LEFT_OUTSIDE = 2 20 | PICKUP_RIGHT_OUTSIDE = 3 21 | QUICK_MOVE_LEFT = 4 22 | QUICK_MOVE_RIGHT = 5 23 | SWAP = 6 24 | CLONE = 7 25 | THROW_SINGLE = 8 26 | THROW_ALL = 9 27 | QUICK_CRAFT = 10 28 | QUICK_CRAFT_LEFT = 0 29 | QUICK_CRAFT_RIGHT = 1 30 | QUICK_CRAFT_MIDDLE = 2 31 | QUICK_CRAFT_START = 0 32 | QUICK_CRAFT_ADD = 1 33 | QUICK_CRAFT_END = 2 34 | PICKUP_ALL = 11 35 | 36 | POSE_NAMES = { 37 | "standing", 38 | "flying", 39 | "sleeping", 40 | "swimming", 41 | "attacking", 42 | "sneaking", 43 | "jumping", 44 | "dying", 45 | } 46 | -------------------------------------------------------------------------------- /src/lua/nochatreports/crypt.rs: -------------------------------------------------------------------------------- 1 | #[macro_export] 2 | macro_rules! crypt { 3 | ($op:ident, $options:expr, $text:expr) => {{ 4 | macro_rules! crypt_with { 5 | ($algo:ident) => {{ 6 | let key = &$options.get::>("key")?.0; 7 | match $options.get("encoding").unwrap_or_default() { 8 | 1 => $algo::::$op($text, &key), 9 | 2 => $algo::::$op($text, &key), 10 | _ => $algo::::$op($text, &key), 11 | } 12 | .map_err(|error| Error::external(error.to_string()))? 13 | }}; 14 | } 15 | 16 | match $options.get("encryption").unwrap_or_default() { 17 | 1 => CaesarEncryption::$op(&$text, &$options.get("key")?) 18 | .map_err(|error| Error::external(error.to_string()))?, 19 | 2 => crypt_with!(EcbEncryption), 20 | 3 => crypt_with!(GcmEncryption), 21 | _ => crypt_with!(Cfb8Encryption), 22 | } 23 | }}; 24 | } 25 | -------------------------------------------------------------------------------- /src/lua/player.rs: -------------------------------------------------------------------------------- 1 | use azalea::PlayerInfo; 2 | use mlua::{IntoLua, Lua, Result, Value}; 3 | 4 | #[derive(Clone)] 5 | pub struct Player { 6 | pub display_name: Option, 7 | pub gamemode: u8, 8 | pub latency: i32, 9 | pub name: String, 10 | pub uuid: String, 11 | } 12 | 13 | impl From for Player { 14 | fn from(p: PlayerInfo) -> Self { 15 | Self { 16 | display_name: p.display_name.map(|text| text.to_string()), 17 | gamemode: p.gamemode.to_id(), 18 | latency: p.latency, 19 | name: p.profile.name, 20 | uuid: p.uuid.to_string(), 21 | } 22 | } 23 | } 24 | 25 | impl IntoLua for Player { 26 | fn into_lua(self, lua: &Lua) -> Result { 27 | let table = lua.create_table()?; 28 | table.set("display_name", self.display_name)?; 29 | table.set("gamemode", self.gamemode)?; 30 | table.set("latency", self.latency)?; 31 | table.set("name", self.name)?; 32 | table.set("uuid", self.uuid)?; 33 | Ok(Value::Table(table)) 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/lua/logging.rs: -------------------------------------------------------------------------------- 1 | use log::{debug, error, info, trace, warn}; 2 | use mlua::{Lua, Result, Table}; 3 | 4 | pub fn register_globals(lua: &Lua, globals: &Table) -> Result<()> { 5 | globals.set( 6 | "error", 7 | lua.create_function(|_, message: String| { 8 | error!("{message}"); 9 | Ok(()) 10 | })?, 11 | )?; 12 | globals.set( 13 | "warn", 14 | lua.create_function(|_, message: String| { 15 | warn!("{message}"); 16 | Ok(()) 17 | })?, 18 | )?; 19 | globals.set( 20 | "info", 21 | lua.create_function(|_, message: String| { 22 | info!("{message}"); 23 | Ok(()) 24 | })?, 25 | )?; 26 | globals.set( 27 | "debug", 28 | lua.create_function(|_, message: String| { 29 | debug!("{message}"); 30 | Ok(()) 31 | })?, 32 | )?; 33 | globals.set( 34 | "trace", 35 | lua.create_function(|_, message: String| { 36 | trace!("{message}"); 37 | Ok(()) 38 | })?, 39 | )?; 40 | 41 | Ok(()) 42 | } 43 | -------------------------------------------------------------------------------- /src/lua/direction.rs: -------------------------------------------------------------------------------- 1 | use azalea::entity::LookDirection; 2 | use mlua::{Error, FromLua, IntoLua, Lua, Result, Value}; 3 | 4 | #[derive(Clone)] 5 | pub struct Direction { 6 | pub y: f32, 7 | pub x: f32, 8 | } 9 | 10 | impl From<&LookDirection> for Direction { 11 | fn from(d: &LookDirection) -> Self { 12 | Self { 13 | y: d.y_rot, 14 | x: d.x_rot, 15 | } 16 | } 17 | } 18 | 19 | impl IntoLua for Direction { 20 | fn into_lua(self, lua: &Lua) -> Result { 21 | let table = lua.create_table()?; 22 | table.set("y", self.y)?; 23 | table.set("x", self.x)?; 24 | Ok(Value::Table(table)) 25 | } 26 | } 27 | 28 | impl FromLua for Direction { 29 | fn from_lua(value: Value, _lua: &Lua) -> Result { 30 | if let Value::Table(table) = value { 31 | Ok(if let (Ok(y), Ok(x)) = (table.get(1), table.get(2)) { 32 | Self { y, x } 33 | } else { 34 | Self { 35 | y: table.get("y")?, 36 | x: table.get("x")?, 37 | } 38 | }) 39 | } else { 40 | Err(Error::FromLuaConversionError { 41 | from: value.type_name(), 42 | to: "Direction".to_string(), 43 | message: None, 44 | }) 45 | } 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "errornowatcher" 3 | version = "0.2.0" 4 | edition = "2024" 5 | build = "build.rs" 6 | 7 | [profile.dev] 8 | opt-level = 1 9 | 10 | [profile.dev.package."*"] 11 | opt-level = 3 12 | 13 | [profile.release] 14 | codegen-units = 1 15 | lto = true 16 | strip = true 17 | 18 | [build-dependencies] 19 | built = { git = "https://github.com/lukaslueg/built", features = ["git2"] } 20 | 21 | [dependencies] 22 | anyhow = "1" 23 | azalea = { git = "https://github.com/azalea-rs/azalea" } 24 | bevy_app = "0" 25 | bevy_ecs = "0" 26 | bevy_log = "0" 27 | clap = { version = "4", features = ["derive", "string"] } 28 | console-subscriber = { version = "0", optional = true } 29 | ctrlc = "3" 30 | dirs = { version = "6", optional = true } 31 | futures = "0" 32 | futures-locks = "0" 33 | http-body-util = "0" 34 | hyper = { version = "1", features = ["server"] } 35 | hyper-util = "0" 36 | log = "0" 37 | matrix-sdk = { version = "0", features = ["anyhow"], optional = true } 38 | mimalloc = { version = "0", optional = true } 39 | mlua = { version = "0", features = ["async", "luajit", "send"] } 40 | ncr = { version = "0", features = ["cfb8", "ecb", "gcm"] } 41 | parking_lot = "0" 42 | serde = "1" 43 | serde_json = "1" 44 | tokio = { version = "1", features = ["full"] } 45 | zip = { version = "2", default-features = false, features = ["flate2"] } 46 | 47 | [features] 48 | console-subscriber = ["dep:console-subscriber"] 49 | default = ["matrix"] 50 | matrix = ["dep:dirs", "dep:matrix-sdk"] 51 | mimalloc = ["dep:mimalloc"] 52 | -------------------------------------------------------------------------------- /.github/workflows/build.yaml: -------------------------------------------------------------------------------- 1 | name: Build 2 | on: 3 | push: 4 | paths: 5 | - "**.rs" 6 | - "**.toml" 7 | - "Cargo.*" 8 | pull_request: 9 | workflow_dispatch: 10 | 11 | jobs: 12 | errornowatcher: 13 | name: errornowatcher (${{ matrix.os }}, ${{ matrix.feature.name }}) 14 | runs-on: ${{ matrix.os }} 15 | 16 | strategy: 17 | matrix: 18 | os: [ubuntu-24.04, ubuntu-24.04-arm] 19 | feature: 20 | - name: default 21 | 22 | - name: mimalloc 23 | flags: "-F mimalloc" 24 | 25 | steps: 26 | - name: Clone repository 27 | uses: actions/checkout@v4 28 | 29 | - name: Install build dependencies 30 | run: sudo apt install -y libluajit-5.1-dev mold 31 | 32 | - name: Set up build cache 33 | uses: actions/cache@v4 34 | with: 35 | path: | 36 | ~/.cargo/bin/ 37 | ~/.cargo/registry/index/ 38 | ~/.cargo/registry/cache/ 39 | ~/.cargo/git/db/ 40 | target/ 41 | key: build_${{ matrix.os }}_${{ matrix.feature.name }}_${{ hashFiles('**.toml', 'Cargo.*') }} 42 | 43 | - name: Switch to nightly toolchain 44 | run: rustup default nightly 45 | 46 | - run: cargo build --release ${{ matrix.feature.flags }} 47 | 48 | - name: Upload build artifacts 49 | uses: actions/upload-artifact@v4 50 | with: 51 | name: errornowatcher_${{ matrix.feature.name }}_${{ matrix.os }} 52 | path: target/release/errornowatcher 53 | -------------------------------------------------------------------------------- /src/lua/client/world/mod.rs: -------------------------------------------------------------------------------- 1 | #[macro_use] 2 | mod queries; 3 | pub mod find; 4 | 5 | use azalea::{BlockPos, auto_tool::AutoToolClientExt, blocks::BlockState, world::InstanceName}; 6 | use mlua::{Lua, Result, Table}; 7 | 8 | use super::{Client, Direction, Vec3}; 9 | 10 | pub fn best_tool_for_block(lua: &Lua, client: &Client, block_state: u16) -> Result { 11 | let result = client.best_tool_in_hotbar_for_block(BlockState { id: block_state }); 12 | let table = lua.create_table()?; 13 | table.set("index", result.index)?; 14 | table.set("percentage_per_tick", result.percentage_per_tick)?; 15 | Ok(table) 16 | } 17 | 18 | pub fn dimension(_lua: &Lua, client: &Client) -> Result { 19 | Ok(client.component::().to_string()) 20 | } 21 | 22 | pub fn get_block_state(_lua: &Lua, client: &Client, position: Vec3) -> Result> { 23 | #[allow(clippy::cast_possible_truncation)] 24 | Ok(client 25 | .world() 26 | .read() 27 | .get_block_state(&BlockPos::new( 28 | position.x as i32, 29 | position.y as i32, 30 | position.z as i32, 31 | )) 32 | .map(|block| block.id)) 33 | } 34 | 35 | #[allow(clippy::cast_possible_truncation)] 36 | pub fn get_fluid_state(lua: &Lua, client: &Client, position: Vec3) -> Result> { 37 | let fluid_state = client.world().read().get_fluid_state(&BlockPos::new( 38 | position.x as i32, 39 | position.y as i32, 40 | position.z as i32, 41 | )); 42 | Ok(if let Some(state) = fluid_state { 43 | let table = lua.create_table()?; 44 | table.set("kind", state.kind as u8)?; 45 | table.set("amount", state.amount)?; 46 | table.set("falling", state.falling)?; 47 | Some(table) 48 | } else { 49 | None 50 | }) 51 | } 52 | -------------------------------------------------------------------------------- /lib/lib.lua: -------------------------------------------------------------------------------- 1 | function clock_gettime(clock) 2 | local status, module = pcall(require, "posix") 3 | posix = status and module or nil 4 | 5 | if posix then 6 | local s, ns = posix.clock_gettime(clock) 7 | return s + ns / (10 ^ (math.floor(math.log10(ns)) + 1)) 8 | else 9 | warn("failed to load posix module! falling back to os.time()") 10 | return os.time() 11 | end 12 | end 13 | 14 | function distance(p1, p2) 15 | return math.sqrt((p2.x - p1.x) ^ 2 + (p2.y - p1.y) ^ 2 + (p2.z - p1.z) ^ 2) 16 | end 17 | 18 | function table.shallow_copy(t) 19 | local t2 = {} 20 | for k, v in pairs(t) do 21 | t2[k] = v 22 | end 23 | return t2 24 | end 25 | 26 | function table.map(t, f) 27 | local t2 = {} 28 | for k, v in pairs(t) do 29 | t2[k] = f(v) 30 | end 31 | return t2 32 | end 33 | 34 | function table.contains(t, target) 35 | for _, v in pairs(t) do 36 | if v == target then 37 | return true 38 | end 39 | end 40 | return false 41 | end 42 | 43 | function dump(object) 44 | if type(object) == "table" then 45 | local string = "{ " 46 | local parts = {} 47 | for key, value in pairs(object) do 48 | table.insert(parts, key .. " = " .. dump(value)) 49 | end 50 | string = string .. table.concat(parts, ", ") 51 | return string .. " " .. "}" 52 | else 53 | return tostring(object) 54 | end 55 | end 56 | 57 | function dumpp(object, level) 58 | if not level then 59 | level = 0 60 | end 61 | if type(object) == "table" then 62 | local string = "{\n" .. string.rep(" ", level + 1) 63 | local parts = {} 64 | for key, value in pairs(object) do 65 | table.insert(parts, key .. " = " .. dumpp(value, level + 1)) 66 | end 67 | string = string .. table.concat(parts, ",\n" .. string.rep(" ", level + 1)) 68 | return string .. "\n" .. string.rep(" ", level) .. "}" 69 | else 70 | return tostring(object) 71 | end 72 | end 73 | -------------------------------------------------------------------------------- /src/lua/client/world/queries.rs: -------------------------------------------------------------------------------- 1 | #[macro_export] 2 | macro_rules! get_entities { 3 | ($client:ident) => {{ 4 | let mut ecs = $client.ecs.lock(); 5 | ecs.query::<( 6 | &AzaleaPosition, 7 | &CustomName, 8 | &EntityKind, 9 | &EntityUuid, 10 | &LookDirection, 11 | &MinecraftEntityId, 12 | Option<&Owneruuid>, 13 | &Pose, 14 | )>() 15 | .iter(&ecs) 16 | .map( 17 | |(position, custom_name, kind, uuid, direction, id, owner_uuid, pose)| { 18 | ( 19 | Vec3::from(position), 20 | custom_name.as_ref().map(ToString::to_string), 21 | kind.to_string(), 22 | uuid.to_string(), 23 | Direction::from(direction), 24 | id.0, 25 | owner_uuid.map(ToOwned::to_owned), 26 | *pose as u8, 27 | ) 28 | }, 29 | ) 30 | .collect::>() 31 | }}; 32 | } 33 | 34 | #[macro_export] 35 | macro_rules! get_players { 36 | ($client:ident) => {{ 37 | let mut ecs = $client.ecs.lock(); 38 | ecs.query_filtered::<( 39 | &MinecraftEntityId, 40 | &EntityUuid, 41 | &EntityKind, 42 | &AzaleaPosition, 43 | &LookDirection, 44 | &Pose, 45 | ), (With, Without)>() 46 | .iter(&ecs) 47 | .map(|(id, uuid, kind, position, direction, pose)| { 48 | ( 49 | id.0, 50 | uuid.to_string(), 51 | kind.to_string(), 52 | Vec3::from(position), 53 | Direction::from(direction), 54 | *pose as u8, 55 | ) 56 | }) 57 | .collect::>() 58 | }}; 59 | } 60 | -------------------------------------------------------------------------------- /src/lua/vec3.rs: -------------------------------------------------------------------------------- 1 | use azalea::{BlockPos, entity::Position}; 2 | use mlua::{Error, FromLua, IntoLua, Lua, Result, Value}; 3 | 4 | #[derive(Clone)] 5 | pub struct Vec3 { 6 | pub x: f64, 7 | pub y: f64, 8 | pub z: f64, 9 | } 10 | 11 | impl IntoLua for Vec3 { 12 | fn into_lua(self, lua: &Lua) -> Result { 13 | let table = lua.create_table()?; 14 | table.set("x", self.x)?; 15 | table.set("y", self.y)?; 16 | table.set("z", self.z)?; 17 | Ok(Value::Table(table)) 18 | } 19 | } 20 | 21 | impl From for Vec3 { 22 | fn from(v: azalea::Vec3) -> Self { 23 | Self { 24 | x: v.x, 25 | y: v.y, 26 | z: v.z, 27 | } 28 | } 29 | } 30 | 31 | impl From<&Position> for Vec3 { 32 | fn from(p: &Position) -> Self { 33 | Self { 34 | x: p.x, 35 | y: p.y, 36 | z: p.z, 37 | } 38 | } 39 | } 40 | 41 | impl From for Vec3 { 42 | fn from(p: BlockPos) -> Self { 43 | Self { 44 | x: f64::from(p.x), 45 | y: f64::from(p.y), 46 | z: f64::from(p.z), 47 | } 48 | } 49 | } 50 | 51 | impl FromLua for Vec3 { 52 | fn from_lua(value: Value, _lua: &Lua) -> Result { 53 | if let Value::Table(table) = value { 54 | Ok( 55 | if let (Ok(x), Ok(y), Ok(z)) = (table.get(1), table.get(2), table.get(3)) { 56 | Self { x, y, z } 57 | } else { 58 | Self { 59 | x: table.get("x")?, 60 | y: table.get("y")?, 61 | z: table.get("z")?, 62 | } 63 | }, 64 | ) 65 | } else { 66 | Err(Error::FromLuaConversionError { 67 | from: value.type_name(), 68 | to: "Vec3".to_string(), 69 | message: None, 70 | }) 71 | } 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /src/lua/nochatreports/mod.rs: -------------------------------------------------------------------------------- 1 | #[macro_use] 2 | pub mod crypt; 3 | pub mod key; 4 | 5 | use key::AesKey; 6 | use mlua::{Error, Lua, Result, Table, UserDataRef}; 7 | use ncr::{ 8 | encoding::{Base64Encoding, Base64rEncoding, NewBase64rEncoding}, 9 | encryption::{CaesarEncryption, Cfb8Encryption, EcbEncryption, Encryption, GcmEncryption}, 10 | utils::{prepend_header, trim_header}, 11 | }; 12 | 13 | pub fn register_globals(lua: &Lua, globals: &Table) -> Result<()> { 14 | globals.set( 15 | "ncr_aes_key_from_passphrase", 16 | lua.create_function(|_, passphrase: Vec| { 17 | Ok(AesKey(ncr::AesKey::gen_from_passphrase(&passphrase))) 18 | })?, 19 | )?; 20 | 21 | globals.set( 22 | "ncr_aes_key_from_base64", 23 | lua.create_function(|_, base64: String| { 24 | Ok(AesKey( 25 | ncr::AesKey::decode_base64(&base64) 26 | .map_err(|error| Error::external(error.to_string()))?, 27 | )) 28 | })?, 29 | )?; 30 | 31 | globals.set( 32 | "ncr_generate_random_aes_key", 33 | lua.create_function(|_, (): ()| Ok(AesKey(ncr::AesKey::gen_random_key())))?, 34 | )?; 35 | 36 | globals.set( 37 | "ncr_encrypt", 38 | lua.create_function(|_, (options, plaintext): (Table, String)| { 39 | Ok(crypt!(encrypt, options, &plaintext)) 40 | })?, 41 | )?; 42 | 43 | globals.set( 44 | "ncr_decrypt", 45 | lua.create_function(|_, (options, ciphertext): (Table, String)| { 46 | Ok(crypt!(decrypt, options, &ciphertext)) 47 | })?, 48 | )?; 49 | 50 | globals.set( 51 | "ncr_prepend_header", 52 | lua.create_function(|_, text: String| Ok(prepend_header(&text)))?, 53 | )?; 54 | 55 | globals.set( 56 | "ncr_trim_header", 57 | lua.create_function(|_, text: String| { 58 | Ok(trim_header(&text) 59 | .map_err(|error| Error::external(error.to_string()))? 60 | .to_owned()) 61 | })?, 62 | )?; 63 | 64 | Ok(()) 65 | } 66 | -------------------------------------------------------------------------------- /src/http.rs: -------------------------------------------------------------------------------- 1 | use http_body_util::{BodyExt, Empty, Full, combinators::BoxBody}; 2 | use hyper::{ 3 | Error, Method, Request, Response, StatusCode, 4 | body::{Bytes, Incoming}, 5 | }; 6 | 7 | use crate::{ 8 | State, 9 | lua::{eval, exec, reload}, 10 | }; 11 | 12 | pub async fn serve( 13 | request: Request, 14 | state: State, 15 | ) -> Result>, Error> { 16 | Ok(match (request.method(), request.uri().path()) { 17 | (&Method::POST, "/reload") => Response::new( 18 | reload(&state.lua, None).map_or_else(|error| full(error.to_string()), |()| empty()), 19 | ), 20 | (&Method::POST, "/eval") => Response::new(full( 21 | eval( 22 | &state.lua, 23 | &String::from_utf8_lossy(&request.into_body().collect().await?.to_bytes()), 24 | None, 25 | ) 26 | .await 27 | .unwrap_or_else(|error| error.to_string()), 28 | )), 29 | (&Method::POST, "/exec") => Response::new( 30 | exec( 31 | &state.lua, 32 | &String::from_utf8_lossy(&request.into_body().collect().await?.to_bytes()), 33 | None, 34 | ) 35 | .await 36 | .map_or_else(|error| full(error.to_string()), |()| empty()), 37 | ), 38 | (&Method::GET, "/ping") => Response::new(full("pong!")), 39 | _ => status_code_response(StatusCode::NOT_FOUND, empty()), 40 | }) 41 | } 42 | 43 | fn status_code_response( 44 | status_code: StatusCode, 45 | bytes: BoxBody, 46 | ) -> Response> { 47 | let mut response = Response::new(bytes); 48 | *response.status_mut() = status_code; 49 | response 50 | } 51 | 52 | fn full>(chunk: T) -> BoxBody { 53 | Full::new(chunk.into()) 54 | .map_err(|never| match never {}) 55 | .boxed() 56 | } 57 | 58 | fn empty() -> BoxBody { 59 | Empty::::new() 60 | .map_err(|never| match never {}) 61 | .boxed() 62 | } 63 | -------------------------------------------------------------------------------- /src/lua/matrix/client.rs: -------------------------------------------------------------------------------- 1 | use std::sync::Arc; 2 | 3 | use matrix_sdk::{ 4 | Client as MatrixClient, 5 | ruma::{RoomId, UserId}, 6 | }; 7 | use mlua::{Error, UserData, UserDataFields, UserDataMethods}; 8 | 9 | use super::room::Room; 10 | 11 | pub struct Client(pub Arc); 12 | 13 | impl UserData for Client { 14 | fn add_fields>(f: &mut F) { 15 | f.add_field_method_get("invited_rooms", |_, this| { 16 | Ok(this 17 | .0 18 | .invited_rooms() 19 | .into_iter() 20 | .map(Room) 21 | .collect::>()) 22 | }); 23 | f.add_field_method_get("joined_rooms", |_, this| { 24 | Ok(this 25 | .0 26 | .joined_rooms() 27 | .into_iter() 28 | .map(Room) 29 | .collect::>()) 30 | }); 31 | f.add_field_method_get("left_rooms", |_, this| { 32 | Ok(this 33 | .0 34 | .left_rooms() 35 | .into_iter() 36 | .map(Room) 37 | .collect::>()) 38 | }); 39 | f.add_field_method_get("rooms", |_, this| { 40 | Ok(this.0.rooms().into_iter().map(Room).collect::>()) 41 | }); 42 | f.add_field_method_get("user_id", |_, this| { 43 | Ok(this.0.user_id().map(ToString::to_string)) 44 | }); 45 | } 46 | 47 | fn add_methods>(m: &mut M) { 48 | m.add_async_method("create_dm", async |_, this, user_id: String| { 49 | this.0 50 | .create_dm(&UserId::parse(user_id).map_err(Error::external)?) 51 | .await 52 | .map_err(Error::external) 53 | .map(Room) 54 | }); 55 | m.add_async_method("join_room_by_id", async |_, this, room_id: String| { 56 | this.0 57 | .join_room_by_id(&RoomId::parse(room_id).map_err(Error::external)?) 58 | .await 59 | .map_err(Error::external) 60 | .map(Room) 61 | }); 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /src/lua/client/interaction.rs: -------------------------------------------------------------------------------- 1 | use azalea::{ 2 | BlockPos, BotClientExt, 3 | protocol::packets::game::{ServerboundUseItem, s_interact::InteractionHand}, 4 | world::MinecraftEntityId, 5 | }; 6 | use log::error; 7 | use mlua::{Lua, Result, UserDataRef}; 8 | 9 | use super::{Client, Vec3}; 10 | 11 | pub fn attack(_lua: &Lua, client: &Client, entity_id: i32) -> Result<()> { 12 | client.attack(MinecraftEntityId(entity_id)); 13 | Ok(()) 14 | } 15 | 16 | pub fn block_interact(_lua: &Lua, client: &Client, position: Vec3) -> Result<()> { 17 | #[allow(clippy::cast_possible_truncation)] 18 | client.block_interact(BlockPos::new( 19 | position.x as i32, 20 | position.y as i32, 21 | position.z as i32, 22 | )); 23 | Ok(()) 24 | } 25 | 26 | pub fn has_attack_cooldown(_lua: &Lua, client: &Client) -> Result { 27 | Ok(client.has_attack_cooldown()) 28 | } 29 | 30 | pub async fn mine(_lua: Lua, client: UserDataRef, position: Vec3) -> Result<()> { 31 | #[allow(clippy::cast_possible_truncation)] 32 | client 33 | .clone() 34 | .mine(BlockPos::new( 35 | position.x as i32, 36 | position.y as i32, 37 | position.z as i32, 38 | )) 39 | .await; 40 | Ok(()) 41 | } 42 | 43 | pub fn set_mining(_lua: &Lua, client: &Client, mining: bool) -> Result<()> { 44 | client.left_click_mine(mining); 45 | Ok(()) 46 | } 47 | 48 | pub fn start_mining(_lua: &Lua, client: &Client, position: Vec3) -> Result<()> { 49 | #[allow(clippy::cast_possible_truncation)] 50 | client.start_mining(BlockPos::new( 51 | position.x as i32, 52 | position.y as i32, 53 | position.z as i32, 54 | )); 55 | Ok(()) 56 | } 57 | 58 | pub fn use_item(_lua: &Lua, client: &Client, hand: Option) -> Result<()> { 59 | let direction = client.direction(); 60 | if let Err(error) = client.write_packet(ServerboundUseItem { 61 | hand: match hand { 62 | Some(1) => InteractionHand::OffHand, 63 | _ => InteractionHand::MainHand, 64 | }, 65 | sequence: 0, 66 | yaw: direction.0, 67 | pitch: direction.1, 68 | }) { 69 | error!("failed to send UseItem packet: {error:?}"); 70 | } 71 | Ok(()) 72 | } 73 | -------------------------------------------------------------------------------- /lib/events.lua: -------------------------------------------------------------------------------- 1 | Center = { x = 0, y = 64, z = 0 } 2 | Radius = 100 3 | Whitelist = table.shallow_copy(Owners) 4 | Ticks = -1 5 | 6 | function check_radius() 7 | Ticks = Ticks + 1 8 | if Ticks % 20 ~= 0 then 9 | return 10 | end 11 | 12 | local self_id = client.id 13 | local players = client:find_players(function(p) 14 | return self_id ~= p.id 15 | and p.position.x > Center.x - Radius + 1 16 | and p.position.x < Center.x + Radius 17 | and p.position.z > Center.z - Radius 18 | and p.position.z < Center.z + Radius 19 | end) 20 | 21 | local tab_list = client.tab_list 22 | for _, player in ipairs(players) do 23 | local target 24 | for _, tab_player in ipairs(tab_list) do 25 | if tab_player.uuid == player.uuid and not table.contains(Whitelist, tab_player.name) then 26 | target = tab_player 27 | break 28 | end 29 | end 30 | if not target then 31 | goto continue 32 | end 33 | 34 | client:chat( 35 | string.format( 36 | "%s is %s %d blocks away at %.2f %.2f %.2f facing %.2f %.2f", 37 | target.name, 38 | POSE_NAMES[player.pose + 1], 39 | distance(Center, player.position), 40 | player.position.x, 41 | player.position.y, 42 | player.position.z, 43 | player.direction.x, 44 | player.direction.y 45 | ) 46 | ) 47 | 48 | ::continue:: 49 | end 50 | end 51 | 52 | function update_listeners() 53 | for type, listeners in pairs(get_listeners()) do 54 | for id, _ in pairs(listeners) do 55 | remove_listeners(type, id) 56 | end 57 | end 58 | 59 | for type, listeners in pairs({ 60 | login = { 61 | message = function() 62 | info("bot successfully logged in!") 63 | end, 64 | eat = function() 65 | sleep(5000) 66 | check_food() 67 | end, 68 | }, 69 | death = { 70 | warn_bot_died = function() 71 | warn( 72 | string.format( 73 | "bot died at %.2f %.2f %.2f facing %.2f %.2f!", 74 | client.position.x, 75 | client.position.y, 76 | client.position.z, 77 | client.direction.x, 78 | client.direction.y 79 | ) 80 | ) 81 | end, 82 | }, 83 | set_health = { auto_eat = check_food }, 84 | tick = { log_player_positions = check_radius }, 85 | }) do 86 | for id, callback in pairs(listeners) do 87 | add_listener(type, callback, id) 88 | end 89 | end 90 | end 91 | -------------------------------------------------------------------------------- /src/lua/container/click.rs: -------------------------------------------------------------------------------- 1 | use azalea::inventory::operations::{ 2 | ClickOperation, CloneClick, PickupAllClick, PickupClick, QuickCraftClick, QuickCraftKind, 3 | QuickCraftStatus, QuickMoveClick, SwapClick, ThrowClick, 4 | }; 5 | use mlua::{Result, Table}; 6 | 7 | pub fn operation_from_table(op: &Table, op_type: Option) -> Result { 8 | Ok(match op_type.unwrap_or_default() { 9 | 0 => ClickOperation::Pickup(PickupClick::Left { 10 | slot: op.get("slot")?, 11 | }), 12 | 1 => ClickOperation::Pickup(PickupClick::Right { 13 | slot: op.get("slot")?, 14 | }), 15 | 2 => ClickOperation::Pickup(PickupClick::LeftOutside), 16 | 3 => ClickOperation::Pickup(PickupClick::RightOutside), 17 | 5 => ClickOperation::QuickMove(QuickMoveClick::Right { 18 | slot: op.get("slot")?, 19 | }), 20 | 6 => ClickOperation::Swap(SwapClick { 21 | source_slot: op.get("source_slot")?, 22 | target_slot: op.get("target_slot")?, 23 | }), 24 | 7 => ClickOperation::Clone(CloneClick { 25 | slot: op.get("slot")?, 26 | }), 27 | 8 => ClickOperation::Throw(ThrowClick::Single { 28 | slot: op.get("slot")?, 29 | }), 30 | 9 => ClickOperation::Throw(ThrowClick::All { 31 | slot: op.get("slot")?, 32 | }), 33 | 10 => ClickOperation::QuickCraft(QuickCraftClick { 34 | kind: match op.get("kind").unwrap_or_default() { 35 | 1 => QuickCraftKind::Right, 36 | 2 => QuickCraftKind::Middle, 37 | _ => QuickCraftKind::Left, 38 | }, 39 | status: match op.get("status").unwrap_or_default() { 40 | 1 => QuickCraftStatus::Add { 41 | slot: op.get("slot")?, 42 | }, 43 | 2 => QuickCraftStatus::End, 44 | _ => QuickCraftStatus::Start, 45 | }, 46 | }), 47 | 11 => ClickOperation::PickupAll(PickupAllClick { 48 | slot: op.get("slot")?, 49 | reversed: op.get("reversed").unwrap_or_default(), 50 | }), 51 | _ => ClickOperation::QuickMove(QuickMoveClick::Left { 52 | slot: op.get("slot")?, 53 | }), 54 | }) 55 | } 56 | -------------------------------------------------------------------------------- /.github/workflows/lint.yaml: -------------------------------------------------------------------------------- 1 | name: Lint 2 | on: 3 | push: 4 | paths: 5 | - "**.rs" 6 | - "**.toml" 7 | - "Cargo.*" 8 | pull_request: 9 | workflow_dispatch: 10 | 11 | jobs: 12 | cargo-toml: 13 | name: Cargo.toml 14 | runs-on: ubuntu-24.04 15 | 16 | steps: 17 | - name: Clone repository 18 | uses: actions/checkout@v4 19 | 20 | - name: Install taplo 21 | uses: uncenter/setup-taplo@v1 22 | 23 | - name: Run taplo lint 24 | run: taplo lint Cargo.toml 25 | 26 | - name: Run taplo fmt 27 | if: always() 28 | run: taplo fmt --check Cargo.toml 29 | 30 | rust: 31 | name: Rust (${{ matrix.feature.name }}) 32 | runs-on: ubuntu-24.04 33 | 34 | strategy: 35 | matrix: 36 | feature: 37 | - name: default 38 | 39 | - name: minimal+mimalloc 40 | flags: "--no-default-features -F mimalloc" 41 | 42 | - name: minimal 43 | flags: "--no-default-features" 44 | 45 | - name: mimalloc 46 | flags: "-F mimalloc" 47 | 48 | steps: 49 | - name: Clone repository 50 | uses: actions/checkout@v4 51 | 52 | - name: Install build dependencies 53 | run: sudo apt install -y libluajit-5.1-dev mold 54 | 55 | - name: Set up build cache 56 | uses: actions/cache@v4 57 | with: 58 | path: | 59 | ~/.cargo/bin/ 60 | ~/.cargo/registry/index/ 61 | ~/.cargo/registry/cache/ 62 | ~/.cargo/git/db/ 63 | target/ 64 | key: lint_${{ matrix.feature.name }}_${{ hashFiles('**.toml', 'Cargo.*') }} 65 | 66 | - name: Switch to nightly toolchain 67 | run: rustup default nightly 68 | 69 | - name: Install components 70 | run: rustup component add clippy rustfmt 71 | 72 | - run: cargo clippy ${{ matrix.feature.flags }} -- -D warnings -D clippy::pedantic 73 | 74 | - if: always() 75 | run: cargo fmt --check 76 | -------------------------------------------------------------------------------- /lib/inventory.lua: -------------------------------------------------------------------------------- 1 | function hold_items_in_hotbar(target_kinds, inventory) 2 | if not inventory then 3 | inventory = client:open_inventory() 4 | end 5 | for index, item in ipairs(inventory.contents) do 6 | if index >= 37 and index <= 45 and table.contains(target_kinds, item.kind) then 7 | inventory = nil 8 | sleep(500) 9 | client:set_held_slot(index - 37) 10 | return true 11 | end 12 | end 13 | return false 14 | end 15 | 16 | function hold_items(target_kinds) 17 | local inventory = client:open_inventory() 18 | if hold_items_in_hotbar(target_kinds, inventory) then 19 | return true 20 | end 21 | for index, item in ipairs(inventory.contents) do 22 | if table.contains(target_kinds, item.kind) then 23 | inventory:click({ source_slot = index - 1, target_slot = client.held_slot }, SWAP) 24 | sleep(100) 25 | inventory = nil 26 | sleep(500) 27 | return true 28 | end 29 | end 30 | inventory = nil 31 | sleep(500) 32 | return false 33 | end 34 | 35 | function steal(item_name) 36 | for _, chest_pos in ipairs(client:find_blocks(client.position, get_block_states({ "chest" }))) do 37 | client:go_to({ position = chest_pos, radius = 3 }, { type = RADIUS_GOAL }) 38 | client:look_at(chest_pos) 39 | 40 | local container = client:open_container_at(chest_pos) 41 | for index, item in ipairs(container.contents) do 42 | if item.kind == item_name then 43 | container:click({ slot = index - 1 }, THROW_ALL) 44 | sleep(50) 45 | end 46 | end 47 | 48 | container = nil 49 | while client.container do 50 | sleep(50) 51 | end 52 | end 53 | end 54 | 55 | function dump_inventory(hide_empty) 56 | local inventory = client:open_inventory() 57 | for index, item in ipairs(inventory.contents) do 58 | if hide_empty and item.count == 0 then 59 | goto continue 60 | end 61 | 62 | local item_damage = "" 63 | if item.damage then 64 | item_damage = item.damage 65 | end 66 | info(string.format("%-2d = %2dx %-32s %s", index - 1, item.count, item.kind, item_damage)) 67 | 68 | ::continue:: 69 | end 70 | end 71 | 72 | function drop_all_hotbar() 73 | local inventory = client:open_inventory() 74 | for i = 0, 9 do 75 | inventory:click({ slot = 36 + i }, THROW_ALL) 76 | end 77 | end 78 | 79 | function drop_all_inventory() 80 | local inventory = client:open_inventory() 81 | for i = 0, 45 do 82 | inventory:click({ slot = i }, THROW_ALL) 83 | end 84 | end 85 | -------------------------------------------------------------------------------- /src/lua/system.rs: -------------------------------------------------------------------------------- 1 | use std::{ 2 | ffi::OsString, 3 | process::{Command, Stdio}, 4 | thread, 5 | }; 6 | 7 | use log::error; 8 | use mlua::{Lua, Result, Table}; 9 | 10 | pub fn register_globals(lua: &Lua, globals: &Table) -> Result<()> { 11 | globals.set( 12 | "system", 13 | lua.create_function(|_, (command, args): (String, Option>)| { 14 | thread::spawn(|| { 15 | if let Err(error) = Command::new(command) 16 | .args(args.unwrap_or_default().iter()) 17 | .stdin(Stdio::null()) 18 | .stdout(Stdio::null()) 19 | .stderr(Stdio::null()) 20 | .spawn() 21 | { 22 | error!("failed to run system command: {error:?}"); 23 | } 24 | }); 25 | Ok(()) 26 | })?, 27 | )?; 28 | 29 | globals.set( 30 | "system_print_output", 31 | lua.create_function(|_, (command, args): (String, Option>)| { 32 | thread::spawn(|| { 33 | if let Err(error) = Command::new(command) 34 | .args(args.unwrap_or_default().iter()) 35 | .spawn() 36 | { 37 | error!("failed to run system command: {error:?}"); 38 | } 39 | }); 40 | Ok(()) 41 | })?, 42 | )?; 43 | 44 | globals.set( 45 | "system_with_output", 46 | lua.create_function(|lua, (command, args): (String, Option>)| { 47 | Ok( 48 | match Command::new(command) 49 | .args(args.unwrap_or_default().iter()) 50 | .output() 51 | { 52 | Ok(output) => { 53 | let table = lua.create_table()?; 54 | table.set("status", output.status.code())?; 55 | table.set("stdout", lua.create_string(output.stdout)?)?; 56 | table.set("stderr", lua.create_string(output.stderr)?)?; 57 | Some(table) 58 | } 59 | Err(error) => { 60 | error!("failed to run system command: {error:?}"); 61 | None 62 | } 63 | }, 64 | ) 65 | })?, 66 | )?; 67 | 68 | Ok(()) 69 | } 70 | -------------------------------------------------------------------------------- /src/lua/container/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod click; 2 | pub mod item_stack; 3 | 4 | use azalea::container::{ContainerHandle, ContainerHandleRef}; 5 | use click::operation_from_table; 6 | use item_stack::ItemStack; 7 | use mlua::{Table, UserData, UserDataFields, UserDataMethods}; 8 | 9 | pub struct Container(pub ContainerHandle); 10 | 11 | impl UserData for Container { 12 | fn add_fields>(f: &mut F) { 13 | f.add_field_method_get("id", |_, this| Ok(this.0.id())); 14 | 15 | f.add_field_method_get("menu", |_, this| { 16 | Ok(this.0.menu().map(|m| format!("{m:?}"))) 17 | }); 18 | 19 | f.add_field_method_get("contents", |_, this| { 20 | Ok(this.0.contents().map(|v| { 21 | v.iter() 22 | .map(|i| ItemStack(i.to_owned())) 23 | .collect::>() 24 | })) 25 | }); 26 | } 27 | 28 | fn add_methods>(m: &mut M) { 29 | m.add_method( 30 | "click", 31 | |_, this, (operation, operation_type): (Table, Option)| { 32 | this.0 33 | .click(operation_from_table(&operation, operation_type)?); 34 | Ok(()) 35 | }, 36 | ); 37 | } 38 | } 39 | 40 | pub struct ContainerRef(pub ContainerHandleRef); 41 | 42 | impl UserData for ContainerRef { 43 | fn add_fields>(f: &mut F) { 44 | f.add_field_method_get("id", |_, this| Ok(this.0.id())); 45 | 46 | f.add_field_method_get("menu", |_, this| { 47 | Ok(this.0.menu().map(|m| format!("{m:?}"))) 48 | }); 49 | 50 | f.add_field_method_get("contents", |_, this| { 51 | Ok(this.0.contents().map(|v| { 52 | v.iter() 53 | .map(|i| ItemStack(i.to_owned())) 54 | .collect::>() 55 | })) 56 | }); 57 | } 58 | 59 | fn add_methods>(m: &mut M) { 60 | m.add_method("close", |_, this, (): ()| { 61 | this.0.close(); 62 | Ok(()) 63 | }); 64 | 65 | m.add_method( 66 | "click", 67 | |_, this, (operation, operation_type): (Table, Option)| { 68 | this.0 69 | .click(operation_from_table(&operation, operation_type)?); 70 | Ok(()) 71 | }, 72 | ); 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /src/lua/block.rs: -------------------------------------------------------------------------------- 1 | use azalea::blocks::{ 2 | Block as AzaleaBlock, BlockState, 3 | properties::{ChestType, Facing, LightLevel}, 4 | }; 5 | use mlua::{Function, Lua, Result, Table}; 6 | 7 | pub fn register_globals(lua: &Lua, globals: &Table) -> Result<()> { 8 | globals.set( 9 | "get_block_from_state", 10 | lua.create_function(get_block_from_state)?, 11 | )?; 12 | globals.set( 13 | "get_block_states", 14 | lua.create_async_function(get_block_states)?, 15 | )?; 16 | 17 | Ok(()) 18 | } 19 | 20 | pub fn get_block_from_state(lua: &Lua, state: u32) -> Result> { 21 | let Ok(state) = BlockState::try_from(state) else { 22 | return Ok(None); 23 | }; 24 | let block: Box = state.into(); 25 | let behavior = block.behavior(); 26 | 27 | let table = lua.create_table()?; 28 | table.set("id", block.id())?; 29 | table.set("friction", behavior.friction)?; 30 | table.set("jump_factor", behavior.jump_factor)?; 31 | table.set("destroy_time", behavior.destroy_time)?; 32 | table.set("explosion_resistance", behavior.explosion_resistance)?; 33 | table.set( 34 | "requires_correct_tool_for_drops", 35 | behavior.requires_correct_tool_for_drops, 36 | )?; 37 | Ok(Some(table)) 38 | } 39 | 40 | pub async fn get_block_states( 41 | lua: Lua, 42 | (block_names, filter_fn): (Vec, Option), 43 | ) -> Result> { 44 | let mut matched = Vec::with_capacity(16); 45 | for block_name in block_names { 46 | for block in 47 | (u32::MIN..u32::MAX).map_while(|possible_id| BlockState::try_from(possible_id).ok()) 48 | { 49 | if block_name == Into::>::into(block).id() 50 | && (if let Some(filter_fn) = &filter_fn { 51 | let table = lua.create_table()?; 52 | table.set("chest_type", block.property::().map(|v| v as u8))?; 53 | table.set("facing", block.property::().map(|v| v as u8))?; 54 | table.set( 55 | "light_level", 56 | block.property::().map(|v| v as u8), 57 | )?; 58 | filter_fn.call_async::(table).await? 59 | } else { 60 | true 61 | }) 62 | { 63 | matched.push(block.id); 64 | } 65 | } 66 | } 67 | Ok(matched) 68 | } 69 | -------------------------------------------------------------------------------- /src/replay/plugin.rs: -------------------------------------------------------------------------------- 1 | #![allow(clippy::needless_pass_by_value)] 2 | 3 | use std::sync::Arc; 4 | 5 | use azalea::{ 6 | ecs::{event::EventReader, system::Query}, 7 | packet::{ 8 | config::ReceiveConfigPacketEvent, 9 | game::emit_receive_packet_events, 10 | login::{LoginPacketEvent, process_packet_events}, 11 | }, 12 | protocol::packets::login::ClientboundLoginPacket, 13 | raw_connection::RawConnection, 14 | }; 15 | use bevy_app::{App, First, Plugin}; 16 | use bevy_ecs::{schedule::IntoSystemConfigs, system::ResMut}; 17 | use log::error; 18 | use parking_lot::Mutex; 19 | 20 | use super::recorder::Recorder; 21 | 22 | pub struct RecordPlugin { 23 | pub recorder: Arc>>, 24 | } 25 | 26 | impl Plugin for RecordPlugin { 27 | fn build(&self, app: &mut App) { 28 | let recorder = self.recorder.lock().take(); 29 | if let Some(recorder) = recorder { 30 | app.insert_resource(recorder) 31 | .add_systems(First, record_login_packets.before(process_packet_events)) 32 | .add_systems(First, record_configuration_packets) 33 | .add_systems( 34 | First, 35 | record_game_packets.before(emit_receive_packet_events), 36 | ); 37 | } 38 | } 39 | } 40 | 41 | fn record_login_packets( 42 | recorder: Option>, 43 | mut events: EventReader, 44 | ) { 45 | if let Some(mut recorder) = recorder { 46 | for event in events.read() { 47 | if recorder.should_ignore_compression 48 | && let ClientboundLoginPacket::LoginCompression(_) = *event.packet 49 | { 50 | continue; 51 | } 52 | 53 | if let Err(error) = recorder.save_packet(event.packet.as_ref()) { 54 | error!("failed to record login packet: {error:?}"); 55 | } 56 | } 57 | } 58 | } 59 | 60 | fn record_configuration_packets( 61 | recorder: Option>, 62 | mut events: EventReader, 63 | ) { 64 | if let Some(mut recorder) = recorder { 65 | for event in events.read() { 66 | if let Err(error) = recorder.save_packet(&event.packet) { 67 | error!("failed to record configuration packet: {error:?}"); 68 | } 69 | } 70 | } 71 | } 72 | 73 | fn record_game_packets(recorder: Option>, query: Query<&RawConnection>) { 74 | if let Some(mut recorder) = recorder 75 | && let Ok(raw_conn) = query.get_single() 76 | { 77 | let queue = raw_conn.incoming_packet_queue(); 78 | for raw_packet in queue.lock().iter() { 79 | if let Err(error) = recorder.save_raw_packet(raw_packet) { 80 | error!("failed to record game packet: {error:?}"); 81 | } 82 | } 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /src/lua/client/state.rs: -------------------------------------------------------------------------------- 1 | use azalea::{ 2 | ClientInformation, 3 | entity::metadata::{AirSupply, Score}, 4 | pathfinder::PathfinderDebugParticles, 5 | protocol::common::client_information::ModelCustomization, 6 | }; 7 | use mlua::{Error, Lua, Result, Table, UserDataRef}; 8 | 9 | use super::Client; 10 | use crate::hacks::anti_knockback::AntiKnockback; 11 | 12 | pub fn air_supply(_lua: &Lua, client: &Client) -> Result { 13 | Ok(client.component::().0) 14 | } 15 | 16 | pub fn health(_lua: &Lua, client: &Client) -> Result { 17 | Ok(client.health()) 18 | } 19 | 20 | pub fn hunger(lua: &Lua, client: &Client) -> Result
{ 21 | let hunger = client.hunger(); 22 | let table = lua.create_table()?; 23 | table.set("food", hunger.food)?; 24 | table.set("saturation", hunger.saturation)?; 25 | Ok(table) 26 | } 27 | 28 | pub fn score(_lua: &Lua, client: &Client) -> Result { 29 | Ok(client.component::().0) 30 | } 31 | 32 | pub async fn set_client_information( 33 | _lua: Lua, 34 | client: UserDataRef, 35 | info: Table, 36 | ) -> Result<()> { 37 | let get_bool = |table: &Table, name| table.get(name).unwrap_or(true); 38 | client 39 | .set_client_information(ClientInformation { 40 | allows_listing: info.get("allows_listing")?, 41 | model_customization: info 42 | .get::
("model_customization") 43 | .map(|t| ModelCustomization { 44 | cape: get_bool(&t, "cape"), 45 | jacket: get_bool(&t, "jacket"), 46 | left_sleeve: get_bool(&t, "left_sleeve"), 47 | right_sleeve: get_bool(&t, "right_sleeve"), 48 | left_pants: get_bool(&t, "left_pants"), 49 | right_pants: get_bool(&t, "right_pants"), 50 | hat: get_bool(&t, "hat"), 51 | }) 52 | .unwrap_or_default(), 53 | view_distance: info.get("view_distance").unwrap_or(8), 54 | ..ClientInformation::default() 55 | }) 56 | .await 57 | .map_err(Error::external) 58 | } 59 | 60 | pub fn set_component( 61 | _lua: &Lua, 62 | client: &Client, 63 | (name, enabled): (String, Option), 64 | ) -> Result<()> { 65 | macro_rules! set { 66 | ($name:ident) => {{ 67 | let mut ecs = client.ecs.lock(); 68 | let mut entity = ecs.entity_mut(client.entity); 69 | if enabled.unwrap_or(true) { 70 | entity.insert($name) 71 | } else { 72 | entity.remove::<$name>() 73 | }; 74 | Ok(()) 75 | }}; 76 | } 77 | 78 | match name.as_str() { 79 | "AntiKnockback" => set!(AntiKnockback), 80 | "PathfinderDebugParticles" => set!(PathfinderDebugParticles), 81 | _ => Err(Error::external("invalid component")), 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /src/lua/events.rs: -------------------------------------------------------------------------------- 1 | use std::time::{SystemTime, UNIX_EPOCH}; 2 | 3 | use futures::executor::block_on; 4 | use mlua::{Function, Lua, Result, Table}; 5 | 6 | use crate::ListenerMap; 7 | 8 | pub fn register_globals(lua: &Lua, globals: &Table, event_listeners: ListenerMap) -> Result<()> { 9 | let m = event_listeners.clone(); 10 | globals.set( 11 | "add_listener", 12 | lua.create_function( 13 | move |_, (event_type, callback, optional_id): (String, Function, Option)| { 14 | let m = m.clone(); 15 | let id = optional_id.unwrap_or_else(|| { 16 | callback.info().name.unwrap_or_else(|| { 17 | format!( 18 | "anonymous @ {}", 19 | SystemTime::now() 20 | .duration_since(UNIX_EPOCH) 21 | .unwrap_or_default() 22 | .as_millis() 23 | ) 24 | }) 25 | }); 26 | tokio::spawn(async move { 27 | m.write() 28 | .await 29 | .entry(event_type) 30 | .or_default() 31 | .push((id, callback)); 32 | }); 33 | Ok(()) 34 | }, 35 | )?, 36 | )?; 37 | 38 | let m = event_listeners.clone(); 39 | globals.set( 40 | "remove_listeners", 41 | lua.create_function(move |_, (event_type, target_id): (String, String)| { 42 | let m = m.clone(); 43 | tokio::spawn(async move { 44 | let mut m = m.write().await; 45 | let empty = m.get_mut(&event_type).is_some_and(|listeners| { 46 | listeners.retain(|(id, _)| target_id != *id); 47 | listeners.is_empty() 48 | }); 49 | if empty { 50 | m.remove(&event_type); 51 | } 52 | }); 53 | Ok(()) 54 | })?, 55 | )?; 56 | 57 | globals.set( 58 | "get_listeners", 59 | lua.create_function(move |lua, (): ()| { 60 | let m = block_on(event_listeners.read()); 61 | let listeners_table = lua.create_table()?; 62 | for (event_type, callbacks) in m.iter() { 63 | let type_listeners_table = lua.create_table()?; 64 | for (id, callback) in callbacks { 65 | let info = callback.info(); 66 | let table = lua.create_table()?; 67 | table.set("name", info.name)?; 68 | table.set("line_defined", info.line_defined)?; 69 | table.set("source", info.source)?; 70 | type_listeners_table.set(id.to_owned(), table)?; 71 | } 72 | listeners_table.set(event_type.to_owned(), type_listeners_table)?; 73 | } 74 | Ok(listeners_table) 75 | })?, 76 | )?; 77 | 78 | Ok(()) 79 | } 80 | -------------------------------------------------------------------------------- /src/replay/recorder.rs: -------------------------------------------------------------------------------- 1 | use std::{ 2 | fs::File, 3 | io::{BufWriter, Write}, 4 | time::{Instant, SystemTime, UNIX_EPOCH}, 5 | }; 6 | 7 | use anyhow::Result; 8 | use azalea::{ 9 | buf::AzaleaWriteVar, 10 | prelude::Resource, 11 | protocol::packets::{PROTOCOL_VERSION, ProtocolPacket, VERSION_NAME}, 12 | }; 13 | use log::debug; 14 | use serde_json::json; 15 | use zip::{ZipWriter, write::SimpleFileOptions}; 16 | 17 | use crate::build_info; 18 | 19 | #[derive(Resource)] 20 | pub struct Recorder { 21 | zip_writer: BufWriter>, 22 | start: Instant, 23 | server: String, 24 | pub should_ignore_compression: bool, 25 | } 26 | 27 | impl Recorder { 28 | pub fn new(path: String, server: String, should_ignore_compression: bool) -> Result { 29 | let mut zip_writer = ZipWriter::new( 30 | File::options() 31 | .write(true) 32 | .create(true) 33 | .truncate(true) 34 | .open(path)?, 35 | ); 36 | zip_writer.start_file("recording.tmcpr", SimpleFileOptions::default())?; 37 | Ok(Self { 38 | zip_writer: BufWriter::new(zip_writer), 39 | start: Instant::now(), 40 | server, 41 | should_ignore_compression, 42 | }) 43 | } 44 | 45 | pub fn finish(self) -> Result<()> { 46 | debug!("finishing replay recording"); 47 | 48 | let elapsed = self.start.elapsed().as_millis(); 49 | let mut zip_writer = self.zip_writer.into_inner()?; 50 | zip_writer.start_file("metaData.json", SimpleFileOptions::default())?; 51 | zip_writer.write_all( 52 | json!({ 53 | "singleplayer": false, 54 | "serverName": self.server, 55 | "duration": elapsed, 56 | "date": SystemTime::now().duration_since(UNIX_EPOCH)?.as_millis() - elapsed, 57 | "mcversion": VERSION_NAME, 58 | "fileFormat": "MCPR", 59 | "fileFormatVersion": 14, 60 | "protocol": PROTOCOL_VERSION, 61 | "generator": format!("ErrorNoWatcher {}", build_info::version_formatted()), 62 | }) 63 | .to_string() 64 | .as_bytes(), 65 | )?; 66 | zip_writer.finish()?; 67 | 68 | debug!("finished replay recording"); 69 | Ok(()) 70 | } 71 | 72 | pub fn save_raw_packet(&mut self, raw_packet: &[u8]) -> Result<()> { 73 | self.zip_writer.write_all( 74 | &TryInto::::try_into(self.start.elapsed().as_millis())?.to_be_bytes(), 75 | )?; 76 | self.zip_writer 77 | .write_all(&TryInto::::try_into(raw_packet.len())?.to_be_bytes())?; 78 | self.zip_writer.write_all(raw_packet)?; 79 | Ok(()) 80 | } 81 | 82 | pub fn save_packet(&mut self, packet: &T) -> Result<()> { 83 | let mut raw_packet = Vec::with_capacity(64); 84 | packet.id().azalea_write_var(&mut raw_packet)?; 85 | packet.write(&mut raw_packet)?; 86 | self.save_raw_packet(&raw_packet) 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /src/lua/container/item_stack.rs: -------------------------------------------------------------------------------- 1 | use azalea::inventory::{ 2 | self, 3 | components::{Consumable, CustomName, Damage, Food, MaxDamage}, 4 | }; 5 | use mlua::{UserData, UserDataFields, UserDataMethods}; 6 | 7 | pub struct ItemStack(pub inventory::ItemStack); 8 | 9 | impl UserData for ItemStack { 10 | fn add_fields>(f: &mut F) { 11 | f.add_field_method_get("is_empty", |_, this| Ok(this.0.is_empty())); 12 | f.add_field_method_get("is_present", |_, this| Ok(this.0.is_present())); 13 | f.add_field_method_get("count", |_, this| Ok(this.0.count())); 14 | f.add_field_method_get("kind", |_, this| Ok(this.0.kind().to_string())); 15 | f.add_field_method_get("custom_name", |_, this| { 16 | Ok(this.0.as_present().map(|data| { 17 | data.components 18 | .get::() 19 | .map(|c| c.name.to_string()) 20 | })) 21 | }); 22 | f.add_field_method_get("damage", |_, this| { 23 | Ok(this 24 | .0 25 | .as_present() 26 | .map(|data| data.components.get::().map(|d| d.amount))) 27 | }); 28 | f.add_field_method_get("max_damage", |_, this| { 29 | Ok(this 30 | .0 31 | .as_present() 32 | .map(|data| data.components.get::().map(|d| d.amount))) 33 | }); 34 | 35 | f.add_field_method_get("consumable", |lua, this| { 36 | Ok( 37 | if let Some(consumable) = this 38 | .0 39 | .as_present() 40 | .and_then(|data| data.components.get::()) 41 | { 42 | let table = lua.create_table()?; 43 | table.set("animation", consumable.animation as u8)?; 44 | table.set("consume_seconds", consumable.consume_seconds)?; 45 | table.set("has_consume_particles", consumable.has_consume_particles)?; 46 | Some(table) 47 | } else { 48 | None 49 | }, 50 | ) 51 | }); 52 | 53 | f.add_field_method_get("food", |lua, this| { 54 | Ok( 55 | if let Some(food) = this 56 | .0 57 | .as_present() 58 | .and_then(|data| data.components.get::()) 59 | { 60 | let table = lua.create_table()?; 61 | table.set("nutrition", food.nutrition)?; 62 | table.set("saturation", food.saturation)?; 63 | table.set("can_always_eat", food.can_always_eat)?; 64 | table.set("eat_seconds", food.eat_seconds)?; 65 | Some(table) 66 | } else { 67 | None 68 | }, 69 | ) 70 | }); 71 | } 72 | 73 | fn add_methods>(m: &mut M) { 74 | m.add_method_mut("split", |_, this, count: u32| Ok(Self(this.0.split(count)))); 75 | m.add_method_mut("update_empty", |_, this, (): ()| { 76 | this.0.update_empty(); 77 | Ok(()) 78 | }); 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /src/lua/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod block; 2 | pub mod client; 3 | pub mod container; 4 | pub mod direction; 5 | pub mod events; 6 | pub mod logging; 7 | pub mod nochatreports; 8 | pub mod player; 9 | pub mod system; 10 | pub mod thread; 11 | pub mod vec3; 12 | 13 | #[cfg(feature = "matrix")] 14 | pub mod matrix; 15 | 16 | use std::{ 17 | fmt::{self, Display, Formatter}, 18 | io, 19 | }; 20 | 21 | use mlua::{Lua, Table}; 22 | 23 | use crate::{ListenerMap, build_info::built}; 24 | 25 | #[derive(Debug)] 26 | pub enum Error { 27 | CreateEnv(mlua::Error), 28 | EvalChunk(mlua::Error), 29 | ExecChunk(mlua::Error), 30 | LoadChunk(mlua::Error), 31 | MissingPath(mlua::Error), 32 | ReadFile(io::Error), 33 | } 34 | 35 | impl Display for Error { 36 | fn fmt(&self, formatter: &mut Formatter<'_>) -> fmt::Result { 37 | write!( 38 | formatter, 39 | "failed to {}", 40 | match self { 41 | Self::CreateEnv(error) => format!("create environment: {error}"), 42 | Self::EvalChunk(error) => format!("evaluate chunk: {error}"), 43 | Self::ExecChunk(error) => format!("execute chunk: {error}"), 44 | Self::LoadChunk(error) => format!("load chunk: {error}"), 45 | Self::MissingPath(error) => format!("get SCRIPT_PATH global: {error}"), 46 | Self::ReadFile(error) => format!("read script file: {error}"), 47 | } 48 | ) 49 | } 50 | } 51 | 52 | pub fn register_globals( 53 | lua: &Lua, 54 | globals: &Table, 55 | event_listeners: ListenerMap, 56 | ) -> mlua::Result<()> { 57 | globals.set("CARGO_PKG_VERSION", env!("CARGO_PKG_VERSION"))?; 58 | globals.set("GIT_COMMIT_HASH", built::GIT_COMMIT_HASH)?; 59 | globals.set("GIT_COMMIT_HASH_SHORT", built::GIT_COMMIT_HASH_SHORT)?; 60 | 61 | block::register_globals(lua, globals)?; 62 | events::register_globals(lua, globals, event_listeners)?; 63 | logging::register_globals(lua, globals)?; 64 | nochatreports::register_globals(lua, globals)?; 65 | system::register_globals(lua, globals)?; 66 | thread::register_globals(lua, globals) 67 | } 68 | 69 | pub fn reload(lua: &Lua, sender: Option) -> Result<(), Error> { 70 | lua.load( 71 | &std::fs::read_to_string( 72 | lua.globals() 73 | .get::("SCRIPT_PATH") 74 | .map_err(Error::MissingPath)?, 75 | ) 76 | .map_err(Error::ReadFile)?, 77 | ) 78 | .set_environment(create_env(lua, sender)?) 79 | .exec() 80 | .map_err(Error::LoadChunk) 81 | } 82 | 83 | pub async fn eval(lua: &Lua, code: &str, sender: Option) -> Result { 84 | lua.load(code) 85 | .set_environment(create_env(lua, sender)?) 86 | .eval_async::() 87 | .await 88 | .map_err(Error::EvalChunk) 89 | } 90 | 91 | pub async fn exec(lua: &Lua, code: &str, sender: Option) -> Result<(), Error> { 92 | lua.load(code) 93 | .set_environment(create_env(lua, sender)?) 94 | .exec_async() 95 | .await 96 | .map_err(Error::ExecChunk) 97 | } 98 | 99 | fn create_env(lua: &Lua, sender: Option) -> Result { 100 | let globals = lua.globals(); 101 | globals.set("sender", sender).map_err(Error::CreateEnv)?; 102 | Ok(globals) 103 | } 104 | -------------------------------------------------------------------------------- /src/lua/matrix/room.rs: -------------------------------------------------------------------------------- 1 | use matrix_sdk::{ 2 | RoomMemberships, 3 | room::Room as MatrixRoom, 4 | ruma::{EventId, UserId, events::room::message::RoomMessageEventContent}, 5 | }; 6 | use mlua::{Error, UserData, UserDataFields, UserDataMethods}; 7 | 8 | use super::member::Member; 9 | 10 | pub struct Room(pub MatrixRoom); 11 | 12 | impl UserData for Room { 13 | fn add_fields>(f: &mut F) { 14 | f.add_field_method_get("id", |_, this| Ok(this.0.room_id().to_string())); 15 | f.add_field_method_get("name", |_, this| Ok(this.0.name())); 16 | f.add_field_method_get("topic", |_, this| Ok(this.0.topic())); 17 | f.add_field_method_get("type", |_, this| { 18 | Ok(this.0.room_type().map(|room_type| room_type.to_string())) 19 | }); 20 | } 21 | 22 | fn add_methods>(m: &mut M) { 23 | m.add_async_method( 24 | "ban_user", 25 | async |_, this, (user_id, reason): (String, Option)| { 26 | this.0 27 | .ban_user( 28 | &UserId::parse(user_id).map_err(Error::external)?, 29 | reason.as_deref(), 30 | ) 31 | .await 32 | .map_err(Error::external) 33 | }, 34 | ); 35 | m.add_async_method("get_members", async |_, this, (): ()| { 36 | this.0 37 | .members(RoomMemberships::all()) 38 | .await 39 | .map_err(Error::external) 40 | .map(|members| members.into_iter().map(Member).collect::>()) 41 | }); 42 | m.add_async_method( 43 | "kick_user", 44 | async |_, this, (user_id, reason): (String, Option)| { 45 | this.0 46 | .kick_user( 47 | &UserId::parse(user_id).map_err(Error::external)?, 48 | reason.as_deref(), 49 | ) 50 | .await 51 | .map_err(Error::external) 52 | }, 53 | ); 54 | m.add_async_method("leave", async |_, this, (): ()| { 55 | this.0.leave().await.map_err(Error::external) 56 | }); 57 | m.add_async_method( 58 | "redact", 59 | async |_, this, (event_id, reason): (String, Option)| { 60 | this.0 61 | .redact( 62 | &EventId::parse(event_id).map_err(Error::external)?, 63 | reason.as_deref(), 64 | None, 65 | ) 66 | .await 67 | .map_err(Error::external) 68 | .map(|response| response.event_id.to_string()) 69 | }, 70 | ); 71 | m.add_async_method("send", async |_, this, body: String| { 72 | this.0 73 | .send(RoomMessageEventContent::text_plain(body)) 74 | .await 75 | .map_err(Error::external) 76 | .map(|response| response.event_id.to_string()) 77 | }); 78 | m.add_async_method( 79 | "send_html", 80 | async |_, this, (body, html_body): (String, String)| { 81 | this.0 82 | .send(RoomMessageEventContent::text_html(body, html_body)) 83 | .await 84 | .map_err(Error::external) 85 | .map(|response| response.event_id.to_string()) 86 | }, 87 | ); 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /lib/automation.lua: -------------------------------------------------------------------------------- 1 | FishingBobber = nil 2 | FishingTicks = 0 3 | FishLastCaught = 0 4 | LastEaten = 0 5 | 6 | function auto_fish() 7 | stop_auto_fish() 8 | FishingTicks = 0 9 | 10 | function hold_fishing_rod() 11 | if client.held_item.kind == "minecraft:fishing_rod" or hold_items({ "minecraft:fishing_rod" }) then 12 | return true 13 | end 14 | warn("no fishing rod found!") 15 | end 16 | 17 | if not hold_fishing_rod() then 18 | return 19 | end 20 | 21 | add_listener("add_entity", function(entity) 22 | if entity.kind == "minecraft:fishing_bobber" and entity.data == client.id then 23 | FishingBobber = entity 24 | end 25 | end, "auto-fish_watch-bobber") 26 | 27 | add_listener("remove_entities", function(entity_ids) 28 | if table.contains(entity_ids, FishingBobber.id) then 29 | if os.time() - LastEaten < 3 then 30 | sleep(3000) 31 | end 32 | hold_fishing_rod() 33 | client:use_item() 34 | end 35 | end, "auto-fish_watch-bobber") 36 | 37 | add_listener("level_particles", function(particle) 38 | if particle.kind == 30 and particle.count == 6 then 39 | local current_bobber = client:find_entities(function(e) 40 | return e.id == FishingBobber.id 41 | end)[1] 42 | if distance(current_bobber.position, particle.position) <= 0.75 then 43 | FishLastCaught = os.time() 44 | client:use_item() 45 | end 46 | end 47 | end, "auto-fish") 48 | 49 | add_listener("tick", function() 50 | FishingTicks = FishingTicks + 1 51 | if FishingTicks % 600 ~= 0 then 52 | return 53 | end 54 | 55 | if os.time() - FishLastCaught >= 60 then 56 | hold_fishing_rod() 57 | client:use_item() 58 | end 59 | end, "auto-fish_watchdog") 60 | 61 | client:use_item() 62 | end 63 | 64 | function stop_auto_fish() 65 | remove_listeners("add_entity", "auto-fish_watch-bobber") 66 | remove_listeners("remove_entities", "auto-fish_watch-bobber") 67 | remove_listeners("level_particles", "auto-fish") 68 | remove_listeners("tick", "auto-fish_watchdog") 69 | 70 | if FishingBobber and client:find_entities(function(e) 71 | return e.id == FishingBobber.id 72 | end)[1] then 73 | FishingBobber = nil 74 | client:use_item() 75 | end 76 | end 77 | 78 | function attack_entities(target_kind, minimum) 79 | if not minimum then 80 | minimum = 0 81 | end 82 | 83 | function hold_sword() 84 | if client.held_item.kind == "minecraft:iron_sword" or hold_items({ "minecraft:iron_sword" }) then 85 | return true 86 | end 87 | warn("no sword found!") 88 | end 89 | 90 | while true do 91 | local self_pos = client.position 92 | local entities = client:find_entities(function(e) 93 | return e.kind == target_kind and distance(e.position, self_pos) < 5 94 | end) 95 | 96 | if #entities > minimum then 97 | local e = entities[1] 98 | local pos = e.position 99 | pos.y = pos.y + 1.5 100 | 101 | hold_sword() 102 | client:look_at(pos) 103 | client:attack(e.id) 104 | while client.has_attack_cooldown do 105 | sleep(100) 106 | end 107 | else 108 | sleep(1000) 109 | end 110 | end 111 | end 112 | 113 | function check_food(hunger) 114 | if not hunger then 115 | hunger = client.hunger 116 | end 117 | 118 | if hunger.food >= 20 then 119 | return 120 | end 121 | 122 | local current_time = os.time() 123 | if current_time - LastEaten >= 3 then 124 | LastEaten = current_time 125 | 126 | while not hold_items({ 127 | "minecraft:golden_carrot", 128 | "minecraft:cooked_beef", 129 | "minecraft:bread", 130 | }) do 131 | sleep(1000) 132 | LastEaten = current_time 133 | end 134 | client:use_item() 135 | end 136 | end 137 | -------------------------------------------------------------------------------- /src/commands.rs: -------------------------------------------------------------------------------- 1 | use azalea::{brigadier::prelude::*, chat::ChatPacket, prelude::*}; 2 | use futures::lock::Mutex; 3 | use mlua::{Error, Result, Table, UserDataRef}; 4 | use ncr::{ 5 | encoding::{Base64Encoding, Base64rEncoding, NewBase64rEncoding}, 6 | encryption::{CaesarEncryption, Cfb8Encryption, EcbEncryption, Encryption, GcmEncryption}, 7 | utils::prepend_header, 8 | }; 9 | 10 | use crate::{ 11 | State, crypt, 12 | lua::{eval, exec, nochatreports::key::AesKey, reload}, 13 | }; 14 | 15 | pub type Ctx = CommandContext>; 16 | 17 | pub struct CommandSource { 18 | pub client: Client, 19 | pub message: ChatPacket, 20 | pub state: State, 21 | pub ncr_options: Option
, 22 | } 23 | 24 | impl CommandSource { 25 | pub fn reply(&self, message: &str) { 26 | fn encrypt(options: &Table, plaintext: &str) -> Result { 27 | Ok(crypt!(encrypt, options, &prepend_header(plaintext))) 28 | } 29 | 30 | for mut chunk in message 31 | .chars() 32 | .collect::>() 33 | .chunks(if self.ncr_options.is_some() { 150 } else { 236 }) 34 | .map(|chars| chars.iter().collect::()) 35 | { 36 | if let Some(ciphertext) = self 37 | .ncr_options 38 | .as_ref() 39 | .and_then(|options| encrypt(options, &chunk).ok()) 40 | { 41 | chunk = ciphertext; 42 | } 43 | self.client.chat( 44 | &(if self.message.is_whisper() 45 | && let Some(username) = self.message.sender() 46 | { 47 | format!("/w {username} {chunk}") 48 | } else { 49 | chunk 50 | }), 51 | ); 52 | } 53 | } 54 | } 55 | 56 | pub fn register(commands: &mut CommandDispatcher>) { 57 | commands.register(literal("reload").executes(|ctx: &Ctx| { 58 | let source = ctx.source.clone(); 59 | tokio::spawn(async move { 60 | let source = source.lock().await; 61 | source.reply( 62 | &reload(&source.state.lua, source.message.sender()) 63 | .map_or_else(|error| error.to_string(), |()| String::from("ok")), 64 | ); 65 | }); 66 | 1 67 | })); 68 | 69 | commands.register( 70 | literal("eval").then(argument("code", string()).executes(|ctx: &Ctx| { 71 | let source = ctx.source.clone(); 72 | let code = get_string(ctx, "code").expect("argument should exist"); 73 | tokio::spawn(async move { 74 | let source = source.lock().await; 75 | source.reply( 76 | &eval(&source.state.lua, &code, source.message.sender()) 77 | .await 78 | .unwrap_or_else(|error| error.to_string()), 79 | ); 80 | }); 81 | 1 82 | })), 83 | ); 84 | 85 | commands.register( 86 | literal("exec").then(argument("code", string()).executes(|ctx: &Ctx| { 87 | let source = ctx.source.clone(); 88 | let code = get_string(ctx, "code").expect("argument should exist"); 89 | tokio::spawn(async move { 90 | let source = source.lock().await; 91 | source.reply( 92 | &exec(&source.state.lua, &code, source.message.sender()) 93 | .await 94 | .map_or_else(|error| error.to_string(), |()| String::from("ok")), 95 | ); 96 | }); 97 | 1 98 | })), 99 | ); 100 | 101 | commands.register(literal("ping").executes(|ctx: &Ctx| { 102 | let source = ctx.source.clone(); 103 | tokio::spawn(async move { 104 | source.lock().await.reply("pong!"); 105 | }); 106 | 1 107 | })); 108 | } 109 | -------------------------------------------------------------------------------- /src/matrix/bot.rs: -------------------------------------------------------------------------------- 1 | use std::time::Duration; 2 | 3 | use anyhow::Result; 4 | use log::{debug, error}; 5 | use matrix_sdk::{ 6 | Client, Room, RoomState, 7 | event_handler::Ctx, 8 | ruma::events::room::{ 9 | member::StrippedRoomMemberEvent, 10 | message::{MessageType, OriginalSyncRoomMessageEvent, RoomMessageEventContent}, 11 | }, 12 | }; 13 | use tokio::time::sleep; 14 | 15 | use super::Context; 16 | use crate::{ 17 | events::call_listeners, 18 | lua::{eval, exec, matrix::room::Room as LuaRoom, reload}, 19 | }; 20 | 21 | pub async fn on_regular_room_message( 22 | event: OriginalSyncRoomMessageEvent, 23 | room: Room, 24 | ctx: Ctx, 25 | ) -> Result<()> { 26 | if room.state() != RoomState::Joined { 27 | return Ok(()); 28 | } 29 | let MessageType::Text(text_content) = event.content.msgtype else { 30 | return Ok(()); 31 | }; 32 | 33 | if text_content.body.starts_with(&ctx.name) && ctx.is_owner(&event.sender.to_string()) { 34 | let body = text_content.body[ctx.name.len()..] 35 | .trim_start_matches(':') 36 | .trim(); 37 | let split = body.split_once(char::is_whitespace).unzip(); 38 | let code = split 39 | .1 40 | .map(|body| body.trim_start_matches("```lua").trim_matches(['`', '\n'])); 41 | 42 | let mut output = None; 43 | match split.0.unwrap_or(body).to_lowercase().as_str() { 44 | "reload" => { 45 | output = Some( 46 | reload(&ctx.state.lua, None) 47 | .map_or_else(|error| error.to_string(), |()| String::from("ok")), 48 | ); 49 | } 50 | "eval" if let Some(code) = code => { 51 | output = Some( 52 | eval(&ctx.state.lua, code, None) 53 | .await 54 | .unwrap_or_else(|error| error.to_string()), 55 | ); 56 | } 57 | "exec" if let Some(code) = code => { 58 | output = Some( 59 | exec(&ctx.state.lua, code, None) 60 | .await 61 | .map_or_else(|error| error.to_string(), |()| String::from("ok")), 62 | ); 63 | } 64 | "ping" => { 65 | room.send(RoomMessageEventContent::text_plain("pong!")) 66 | .await?; 67 | } 68 | _ => (), 69 | } 70 | 71 | if let Some(output) = output { 72 | room.send(RoomMessageEventContent::text_html( 73 | &output, 74 | format!("
{output}
"), 75 | )) 76 | .await?; 77 | } 78 | } 79 | 80 | call_listeners(&ctx.state, "matrix_chat", || { 81 | let table = ctx.state.lua.create_table()?; 82 | table.set("room", LuaRoom(room))?; 83 | table.set("sender_id", event.sender.to_string())?; 84 | table.set("body", text_content.body)?; 85 | Ok(table) 86 | }) 87 | .await 88 | } 89 | 90 | pub async fn on_stripped_state_member( 91 | member: StrippedRoomMemberEvent, 92 | client: Client, 93 | room: Room, 94 | ctx: Ctx, 95 | ) -> Result<()> { 96 | if let Some(user_id) = client.user_id() 97 | && member.state_key == user_id 98 | && ctx.is_owner(&member.sender.to_string()) 99 | { 100 | debug!("joining room {}", room.room_id()); 101 | while let Err(error) = room.join().await { 102 | error!("failed to join room {}: {error:?}", room.room_id()); 103 | sleep(Duration::from_secs(10)).await; 104 | } 105 | debug!("successfully joined room {}", room.room_id()); 106 | 107 | call_listeners(&ctx.state, "matrix_join_room", || { 108 | let table = ctx.state.lua.create_table()?; 109 | table.set("room", LuaRoom(room))?; 110 | table.set("sender", member.sender.to_string())?; 111 | Ok(table) 112 | }) 113 | .await?; 114 | } 115 | 116 | Ok(()) 117 | } 118 | -------------------------------------------------------------------------------- /src/lua/client/world/find.rs: -------------------------------------------------------------------------------- 1 | use azalea::{ 2 | BlockPos, 3 | blocks::{BlockState, BlockStates}, 4 | ecs::query::{With, Without}, 5 | entity::{ 6 | Dead, EntityKind, EntityUuid, LookDirection, Pose, Position as AzaleaPosition, 7 | metadata::{CustomName, Owneruuid, Player}, 8 | }, 9 | world::MinecraftEntityId, 10 | }; 11 | use mlua::{Function, Lua, Result, Table, UserDataRef}; 12 | 13 | use super::{Client, Direction, Vec3}; 14 | 15 | pub fn blocks( 16 | _lua: &Lua, 17 | client: &Client, 18 | (nearest_to, block_states): (Vec3, Vec), 19 | ) -> Result> { 20 | #[allow(clippy::cast_possible_truncation)] 21 | Ok(client 22 | .world() 23 | .read() 24 | .find_blocks( 25 | BlockPos::new( 26 | nearest_to.x as i32, 27 | nearest_to.y as i32, 28 | nearest_to.z as i32, 29 | ), 30 | &BlockStates { 31 | set: block_states.iter().map(|&id| BlockState { id }).collect(), 32 | }, 33 | ) 34 | .map(Vec3::from) 35 | .collect()) 36 | } 37 | 38 | pub async fn all_entities(lua: Lua, client: UserDataRef, (): ()) -> Result> { 39 | let mut matched = Vec::with_capacity(256); 40 | for (position, custom_name, kind, uuid, direction, id, owner_uuid, pose) in 41 | get_entities!(client) 42 | { 43 | let table = lua.create_table()?; 44 | table.set("position", position)?; 45 | table.set("custom_name", custom_name)?; 46 | table.set("kind", kind)?; 47 | table.set("uuid", uuid)?; 48 | table.set("direction", direction)?; 49 | table.set("id", id)?; 50 | table.set( 51 | "owner_uuid", 52 | owner_uuid.and_then(|v| *v).map(|v| v.to_string()), 53 | )?; 54 | table.set("pose", pose)?; 55 | matched.push(table); 56 | } 57 | Ok(matched) 58 | } 59 | 60 | pub async fn entities( 61 | lua: Lua, 62 | client: UserDataRef, 63 | filter_fn: Function, 64 | ) -> Result> { 65 | let mut matched = Vec::new(); 66 | for (position, custom_name, kind, uuid, direction, id, owner_uuid, pose) in 67 | get_entities!(client) 68 | { 69 | let table = lua.create_table()?; 70 | table.set("position", position)?; 71 | table.set("custom_name", custom_name)?; 72 | table.set("kind", kind)?; 73 | table.set("uuid", uuid)?; 74 | table.set("direction", direction)?; 75 | table.set("id", id)?; 76 | table.set( 77 | "owner_uuid", 78 | owner_uuid.and_then(|v| *v).map(|v| v.to_string()), 79 | )?; 80 | table.set("pose", pose)?; 81 | if filter_fn.call_async::(&table).await? { 82 | matched.push(table); 83 | } 84 | } 85 | Ok(matched) 86 | } 87 | 88 | pub async fn all_players(lua: Lua, client: UserDataRef, (): ()) -> Result> { 89 | let mut matched = Vec::new(); 90 | for (id, uuid, kind, position, direction, pose) in get_players!(client) { 91 | let table = lua.create_table()?; 92 | table.set("id", id)?; 93 | table.set("uuid", uuid)?; 94 | table.set("kind", kind)?; 95 | table.set("position", position)?; 96 | table.set("direction", direction)?; 97 | table.set("pose", pose)?; 98 | matched.push(table); 99 | } 100 | Ok(matched) 101 | } 102 | 103 | pub async fn players( 104 | lua: Lua, 105 | client: UserDataRef, 106 | filter_fn: Function, 107 | ) -> Result> { 108 | let mut matched = Vec::new(); 109 | for (id, uuid, kind, position, direction, pose) in get_players!(client) { 110 | let table = lua.create_table()?; 111 | table.set("id", id)?; 112 | table.set("uuid", uuid)?; 113 | table.set("kind", kind)?; 114 | table.set("position", position)?; 115 | table.set("direction", direction)?; 116 | table.set("pose", pose)?; 117 | if filter_fn.call_async::(&table).await? { 118 | matched.push(table); 119 | } 120 | } 121 | Ok(matched) 122 | } 123 | -------------------------------------------------------------------------------- /src/lua/client/container.rs: -------------------------------------------------------------------------------- 1 | use azalea::{ 2 | BlockPos, 3 | inventory::{Inventory, Menu, Player, SlotList}, 4 | prelude::ContainerClientExt, 5 | protocol::packets::game::ServerboundSetCarriedItem, 6 | }; 7 | use log::error; 8 | use mlua::{Lua, Result, UserDataRef, Value}; 9 | 10 | use super::{Client, Container, ContainerRef, ItemStack, Vec3}; 11 | 12 | pub fn container(_lua: &Lua, client: &Client) -> Result> { 13 | Ok(client.get_open_container().map(ContainerRef)) 14 | } 15 | 16 | pub fn held_item(_lua: &Lua, client: &Client) -> Result { 17 | Ok(ItemStack(client.component::().held_item())) 18 | } 19 | 20 | pub fn held_slot(_lua: &Lua, client: &Client) -> Result { 21 | Ok(client.component::().selected_hotbar_slot) 22 | } 23 | 24 | #[allow(clippy::too_many_lines)] 25 | pub fn menu(lua: &Lua, client: &Client) -> Result { 26 | fn from_slot_list(slot_list: SlotList) -> Vec { 27 | slot_list 28 | .iter() 29 | .map(|item_stack| ItemStack(item_stack.to_owned())) 30 | .collect::>() 31 | } 32 | 33 | let table = lua.create_table()?; 34 | match client.menu() { 35 | Menu::Player(Player { 36 | craft_result, 37 | craft, 38 | armor, 39 | inventory, 40 | offhand, 41 | }) => { 42 | table.set("type", 0)?; 43 | table.set("craft_result", ItemStack(craft_result))?; 44 | table.set("craft", from_slot_list(craft))?; 45 | table.set("armor", from_slot_list(armor))?; 46 | table.set("inventory", from_slot_list(inventory))?; 47 | table.set("offhand", ItemStack(offhand))?; 48 | } 49 | Menu::Generic9x3 { contents, player } => { 50 | table.set("type", 3)?; 51 | table.set("contents", from_slot_list(contents))?; 52 | table.set("player", from_slot_list(player))?; 53 | } 54 | Menu::Generic9x6 { contents, player } => { 55 | table.set("type", 6)?; 56 | table.set("contents", from_slot_list(contents))?; 57 | table.set("player", from_slot_list(player))?; 58 | } 59 | Menu::Crafting { 60 | result, 61 | grid, 62 | player, 63 | } => { 64 | table.set("type", 13)?; 65 | table.set("result", ItemStack(result))?; 66 | table.set("grid", from_slot_list(grid))?; 67 | table.set("player", from_slot_list(player))?; 68 | } 69 | Menu::Hopper { contents, player } => { 70 | table.set("type", 17)?; 71 | table.set("contents", from_slot_list(contents))?; 72 | table.set("player", from_slot_list(player))?; 73 | } 74 | Menu::Merchant { 75 | payments, 76 | result, 77 | player, 78 | } => { 79 | table.set("type", 20)?; 80 | table.set("payments", from_slot_list(payments))?; 81 | table.set("result", ItemStack(result))?; 82 | table.set("player", from_slot_list(player))?; 83 | } 84 | Menu::ShulkerBox { contents, player } => { 85 | table.set("type", 21)?; 86 | table.set("contents", from_slot_list(contents))?; 87 | table.set("player", from_slot_list(player))?; 88 | } 89 | _ => return Ok(Value::Nil), 90 | } 91 | Ok(Value::Table(table)) 92 | } 93 | 94 | pub async fn open_container_at( 95 | _lua: Lua, 96 | client: UserDataRef, 97 | position: Vec3, 98 | ) -> Result> { 99 | #[allow(clippy::cast_possible_truncation)] 100 | Ok(client 101 | .clone() 102 | .open_container_at(BlockPos::new( 103 | position.x as i32, 104 | position.y as i32, 105 | position.z as i32, 106 | )) 107 | .await 108 | .map(Container)) 109 | } 110 | 111 | pub fn open_inventory(_lua: &Lua, client: &Client, (): ()) -> Result> { 112 | Ok(client.open_inventory().map(Container)) 113 | } 114 | 115 | pub fn set_held_slot(_lua: &Lua, client: &Client, slot: u8) -> Result<()> { 116 | if slot > 8 { 117 | return Ok(()); 118 | } 119 | 120 | { 121 | let mut ecs = client.ecs.lock(); 122 | let mut inventory = client.query::<&mut Inventory>(&mut ecs); 123 | if inventory.selected_hotbar_slot == slot { 124 | return Ok(()); 125 | } 126 | inventory.selected_hotbar_slot = slot; 127 | }; 128 | 129 | if let Err(error) = client.write_packet(ServerboundSetCarriedItem { 130 | slot: u16::from(slot), 131 | }) { 132 | error!("failed to send SetCarriedItem packet: {error:?}"); 133 | } 134 | 135 | Ok(()) 136 | } 137 | -------------------------------------------------------------------------------- /src/main.rs: -------------------------------------------------------------------------------- 1 | #![feature(if_let_guard, let_chains)] 2 | #![warn(clippy::pedantic, clippy::nursery)] 3 | #![allow(clippy::significant_drop_tightening)] 4 | 5 | mod arguments; 6 | mod build_info; 7 | mod commands; 8 | mod events; 9 | mod hacks; 10 | mod http; 11 | mod lua; 12 | mod particle; 13 | mod replay; 14 | 15 | #[cfg(feature = "matrix")] 16 | mod matrix; 17 | 18 | use std::{ 19 | collections::HashMap, 20 | env, 21 | fs::{OpenOptions, read_to_string}, 22 | sync::Arc, 23 | }; 24 | 25 | use anyhow::Context; 26 | use arguments::Arguments; 27 | use azalea::{ 28 | DefaultBotPlugins, DefaultPlugins, brigadier::prelude::CommandDispatcher, prelude::*, 29 | }; 30 | use bevy_app::PluginGroup; 31 | use bevy_log::{ 32 | LogPlugin, 33 | tracing_subscriber::{Layer, fmt::layer}, 34 | }; 35 | use clap::Parser; 36 | use commands::{CommandSource, register}; 37 | use futures::lock::Mutex; 38 | use futures_locks::RwLock; 39 | use hacks::HacksPlugin; 40 | use log::debug; 41 | use mlua::{Function, Lua, Table}; 42 | use replay::{plugin::RecordPlugin, recorder::Recorder}; 43 | 44 | #[cfg(feature = "mimalloc")] 45 | #[global_allocator] 46 | static GLOBAL: mimalloc::MiMalloc = mimalloc::MiMalloc; 47 | 48 | type ListenerMap = Arc>>>; 49 | 50 | #[derive(Default, Clone, Component)] 51 | struct State { 52 | lua: Arc, 53 | event_listeners: ListenerMap, 54 | commands: Arc>>, 55 | } 56 | 57 | #[tokio::main] 58 | async fn main() -> anyhow::Result<()> { 59 | #[cfg(feature = "console-subscriber")] 60 | console_subscriber::init(); 61 | 62 | let args = Arguments::parse(); 63 | let event_listeners = Arc::new(RwLock::new(HashMap::new())); 64 | let lua = unsafe { Lua::unsafe_new() }; 65 | let globals = lua.globals(); 66 | lua::register_globals(&lua, &globals, event_listeners.clone())?; 67 | 68 | if let Some(path) = args.script { 69 | globals.set("SCRIPT_PATH", &*path)?; 70 | lua.load(read_to_string(path)?).exec()?; 71 | } else if let Some(code) = ["main.lua", "errornowatcher.lua"].iter().find_map(|path| { 72 | debug!("trying to load code from {path}"); 73 | globals.set("SCRIPT_PATH", *path).ok()?; 74 | read_to_string(path).ok() 75 | }) { 76 | lua.load(code).exec()?; 77 | } 78 | if let Some(code) = args.exec { 79 | lua.load(code).exec()?; 80 | } 81 | 82 | let server = globals 83 | .get::("Server") 84 | .context("lua globals missing Server variable")?; 85 | let username = globals 86 | .get::("Username") 87 | .context("lua globals missing Username variable")?; 88 | 89 | let mut commands = CommandDispatcher::new(); 90 | register(&mut commands); 91 | 92 | let default_plugins = if cfg!(feature = "console-subscriber") { 93 | DefaultPlugins.build().disable::() 94 | } else { 95 | DefaultPlugins.set(LogPlugin { 96 | custom_layer: |_| { 97 | env::var("LOG_FILE").ok().map(|path| { 98 | layer() 99 | .with_writer( 100 | OpenOptions::new() 101 | .append(true) 102 | .create(true) 103 | .open(&path) 104 | .expect(&(path + " should be accessible")), 105 | ) 106 | .boxed() 107 | }) 108 | }, 109 | ..Default::default() 110 | }) 111 | }; 112 | let record_plugin = RecordPlugin { 113 | recorder: Arc::new(parking_lot::Mutex::new( 114 | if let Ok(options) = globals.get::
("ReplayRecordingOptions") 115 | && let Ok(path) = options.get::("path") 116 | { 117 | Some(Recorder::new( 118 | path, 119 | server.clone(), 120 | options 121 | .get::("ignore_compression") 122 | .unwrap_or_default(), 123 | )?) 124 | } else { 125 | None 126 | }, 127 | )), 128 | }; 129 | let account = if username.contains('@') { 130 | Account::microsoft(&username).await? 131 | } else { 132 | Account::offline(&username) 133 | }; 134 | 135 | let Err(error) = ClientBuilder::new_without_plugins() 136 | .add_plugins(DefaultBotPlugins) 137 | .add_plugins(HacksPlugin) 138 | .add_plugins(default_plugins) 139 | .add_plugins(record_plugin) 140 | .set_handler(events::handle_event) 141 | .set_state(State { 142 | lua: Arc::new(lua), 143 | event_listeners, 144 | commands: Arc::new(commands), 145 | }) 146 | .start(account, server) 147 | .await; 148 | eprintln!("{error}"); 149 | 150 | Ok(()) 151 | } 152 | -------------------------------------------------------------------------------- /src/matrix/mod.rs: -------------------------------------------------------------------------------- 1 | mod bot; 2 | mod verification; 3 | 4 | use std::{path::Path, sync::Arc, time::Duration}; 5 | 6 | use anyhow::{Context as _, Result}; 7 | use bot::{on_regular_room_message, on_stripped_state_member}; 8 | use log::{error, warn}; 9 | use matrix_sdk::{ 10 | Client, Error, LoopCtrl, authentication::matrix::MatrixSession, config::SyncSettings, 11 | }; 12 | use mlua::Table; 13 | use serde::{Deserialize, Serialize}; 14 | use tokio::fs; 15 | use verification::{on_device_key_verification_request, on_room_message_verification_request}; 16 | 17 | use crate::{State, events::call_listeners, lua::matrix::client::Client as LuaClient}; 18 | 19 | #[derive(Clone)] 20 | struct Context { 21 | state: State, 22 | name: String, 23 | } 24 | 25 | impl Context { 26 | fn is_owner(&self, name: &String) -> bool { 27 | self.state 28 | .lua 29 | .globals() 30 | .get::
("MatrixOptions") 31 | .ok() 32 | .and_then(|options| { 33 | options 34 | .get::>("owners") 35 | .ok() 36 | .and_then(|owners| owners.contains(name).then_some(())) 37 | }) 38 | .is_some() 39 | } 40 | } 41 | 42 | #[derive(Clone, Serialize, Deserialize)] 43 | struct Session { 44 | #[serde(skip_serializing_if = "Option::is_none")] 45 | sync_token: Option, 46 | user_session: MatrixSession, 47 | } 48 | 49 | async fn persist_sync_token( 50 | session_file: &Path, 51 | session: &mut Session, 52 | sync_token: String, 53 | ) -> Result<()> { 54 | session.sync_token = Some(sync_token); 55 | fs::write(session_file, serde_json::to_string(&session)?).await?; 56 | Ok(()) 57 | } 58 | 59 | pub async fn login(state: &State, options: &Table, globals: &Table, name: String) -> Result<()> { 60 | let (homeserver_url, username, password, sync_timeout) = ( 61 | options.get::("homeserver_url")?, 62 | options.get::("username")?, 63 | &options.get::("password")?, 64 | options.get::("sync_timeout"), 65 | ); 66 | let root_dir = dirs::data_dir() 67 | .context("no data directory")? 68 | .join("errornowatcher") 69 | .join(&name) 70 | .join("matrix"); 71 | 72 | let mut builder = Client::builder().homeserver_url(homeserver_url); 73 | if !fs::try_exists(&root_dir).await.unwrap_or_default() 74 | && let Err(error) = fs::create_dir_all(&root_dir).await 75 | { 76 | warn!("failed to create directory for matrix sqlite store: {error:?}"); 77 | } else { 78 | builder = builder.sqlite_store(&root_dir, None); 79 | } 80 | let client = builder.build().await?; 81 | 82 | let mut sync_settings = SyncSettings::new(); 83 | if let Ok(seconds) = sync_timeout { 84 | sync_settings = sync_settings.timeout(Duration::from_secs(seconds)); 85 | } 86 | 87 | let mut new_session; 88 | let session_file = root_dir.join("session.json"); 89 | if let Some(session) = fs::read_to_string(&session_file) 90 | .await 91 | .ok() 92 | .and_then(|data| serde_json::from_str::(&data).ok()) 93 | { 94 | new_session = session.clone(); 95 | if let Some(sync_token) = session.sync_token { 96 | sync_settings = sync_settings.token(sync_token); 97 | } 98 | client.restore_session(session.user_session).await?; 99 | } else { 100 | let matrix_auth = client.matrix_auth(); 101 | matrix_auth 102 | .login_username(username, password) 103 | .initial_device_display_name(&name) 104 | .await?; 105 | 106 | new_session = Session { 107 | user_session: matrix_auth.session().context("should have session")?, 108 | sync_token: None, 109 | }; 110 | fs::write(&session_file, serde_json::to_string(&new_session)?).await?; 111 | } 112 | 113 | client.add_event_handler_context(Context { 114 | state: state.to_owned(), 115 | name, 116 | }); 117 | client.add_event_handler(on_stripped_state_member); 118 | loop { 119 | match client.sync_once(sync_settings.clone()).await { 120 | Ok(response) => { 121 | sync_settings = sync_settings.token(response.next_batch.clone()); 122 | persist_sync_token(&session_file, &mut new_session, response.next_batch).await?; 123 | break; 124 | } 125 | Err(error) => { 126 | error!("failed to do initial sync: {error:?}"); 127 | } 128 | } 129 | } 130 | 131 | client.add_event_handler(on_device_key_verification_request); 132 | client.add_event_handler(on_room_message_verification_request); 133 | client.add_event_handler(on_regular_room_message); 134 | 135 | let client = Arc::new(client); 136 | globals.set("matrix", LuaClient(client.clone()))?; 137 | call_listeners(state, "matrix_init", || Ok(())).await?; 138 | 139 | client 140 | .sync_with_result_callback(sync_settings, |sync_result| async { 141 | let mut new_session = new_session.clone(); 142 | persist_sync_token(&session_file, &mut new_session, sync_result?.next_batch) 143 | .await 144 | .map_err(|err| Error::UnknownError(err.into()))?; 145 | Ok(LoopCtrl::Continue) 146 | }) 147 | .await?; 148 | Ok(()) 149 | } 150 | -------------------------------------------------------------------------------- /src/lua/client/mod.rs: -------------------------------------------------------------------------------- 1 | #![allow(clippy::needless_pass_by_value, clippy::unnecessary_wraps)] 2 | 3 | mod container; 4 | mod interaction; 5 | mod movement; 6 | mod state; 7 | mod world; 8 | 9 | use std::ops::{Deref, DerefMut}; 10 | 11 | use azalea::{Client as AzaleaClient, world::MinecraftEntityId}; 12 | use mlua::{Lua, Result, UserData, UserDataFields, UserDataMethods}; 13 | 14 | use super::{ 15 | container::{Container, ContainerRef, item_stack::ItemStack}, 16 | direction::Direction, 17 | player::Player, 18 | vec3::Vec3, 19 | }; 20 | 21 | pub struct Client(pub Option); 22 | 23 | impl Deref for Client { 24 | type Target = AzaleaClient; 25 | 26 | fn deref(&self) -> &Self::Target { 27 | self.0.as_ref().expect("should have received init event") 28 | } 29 | } 30 | 31 | impl DerefMut for Client { 32 | fn deref_mut(&mut self) -> &mut Self::Target { 33 | self.0.as_mut().expect("should have received init event") 34 | } 35 | } 36 | 37 | impl UserData for Client { 38 | fn add_fields>(f: &mut F) { 39 | f.add_field_method_get("air_supply", state::air_supply); 40 | f.add_field_method_get("container", container::container); 41 | f.add_field_method_get("dimension", world::dimension); 42 | f.add_field_method_get("direction", movement::direction); 43 | f.add_field_method_get("eye_position", movement::eye_position); 44 | f.add_field_method_get("go_to_reached", movement::go_to_reached); 45 | f.add_field_method_get("has_attack_cooldown", interaction::has_attack_cooldown); 46 | f.add_field_method_get("health", state::health); 47 | f.add_field_method_get("held_item", container::held_item); 48 | f.add_field_method_get("held_slot", container::held_slot); 49 | f.add_field_method_get("hunger", state::hunger); 50 | f.add_field_method_get("id", id); 51 | f.add_field_method_get("looking_at", movement::looking_at); 52 | f.add_field_method_get("menu", container::menu); 53 | f.add_field_method_get("pathfinder", movement::pathfinder); 54 | f.add_field_method_get("position", movement::position); 55 | f.add_field_method_get("score", state::score); 56 | f.add_field_method_get("tab_list", tab_list); 57 | f.add_field_method_get("username", username); 58 | f.add_field_method_get("uuid", uuid); 59 | } 60 | 61 | fn add_methods>(m: &mut M) { 62 | m.add_async_method("find_all_entities", world::find::all_entities); 63 | m.add_async_method("find_all_players", world::find::all_players); 64 | m.add_async_method("find_entities", world::find::entities); 65 | m.add_async_method("find_players", world::find::players); 66 | m.add_async_method("go_to", movement::go_to); 67 | m.add_async_method( 68 | "go_to_wait_until_reached", 69 | movement::go_to_wait_until_reached, 70 | ); 71 | m.add_async_method("mine", interaction::mine); 72 | m.add_async_method("open_container_at", container::open_container_at); 73 | m.add_async_method("set_client_information", state::set_client_information); 74 | m.add_async_method("start_go_to", movement::start_go_to); 75 | m.add_method("attack", interaction::attack); 76 | m.add_method("best_tool_for_block", world::best_tool_for_block); 77 | m.add_method("block_interact", interaction::block_interact); 78 | m.add_method("chat", chat); 79 | m.add_method("disconnect", disconnect); 80 | m.add_method("find_blocks", world::find::blocks); 81 | m.add_method("get_block_state", world::get_block_state); 82 | m.add_method("get_fluid_state", world::get_fluid_state); 83 | m.add_method("jump", movement::jump); 84 | m.add_method("look_at", movement::look_at); 85 | m.add_method("open_inventory", container::open_inventory); 86 | m.add_method("set_component", state::set_component); 87 | m.add_method("set_direction", movement::set_direction); 88 | m.add_method("set_held_slot", container::set_held_slot); 89 | m.add_method("set_jumping", movement::set_jumping); 90 | m.add_method("set_mining", interaction::set_mining); 91 | m.add_method("set_position", movement::set_position); 92 | m.add_method("set_sneaking", movement::set_sneaking); 93 | m.add_method("sprint", movement::sprint); 94 | m.add_method("start_mining", interaction::start_mining); 95 | m.add_method("stop_pathfinding", movement::stop_pathfinding); 96 | m.add_method("stop_sleeping", movement::stop_sleeping); 97 | m.add_method("use_item", interaction::use_item); 98 | m.add_method("walk", movement::walk); 99 | } 100 | } 101 | 102 | fn chat(_lua: &Lua, client: &Client, message: String) -> Result<()> { 103 | client.chat(&message); 104 | Ok(()) 105 | } 106 | 107 | fn disconnect(_lua: &Lua, client: &Client, (): ()) -> Result<()> { 108 | client.disconnect(); 109 | Ok(()) 110 | } 111 | 112 | fn id(_lua: &Lua, client: &Client) -> Result { 113 | Ok(client.component::().0) 114 | } 115 | 116 | fn tab_list(_lua: &Lua, client: &Client) -> Result> { 117 | Ok(client.tab_list().into_values().map(Player::from).collect()) 118 | } 119 | 120 | fn username(_lua: &Lua, client: &Client) -> Result { 121 | Ok(client.username()) 122 | } 123 | 124 | fn uuid(_lua: &Lua, client: &Client) -> Result { 125 | Ok(client.uuid().to_string()) 126 | } 127 | -------------------------------------------------------------------------------- /src/matrix/verification.rs: -------------------------------------------------------------------------------- 1 | use std::time::Duration; 2 | 3 | use anyhow::{Context, Result}; 4 | use futures::StreamExt; 5 | use log::{error, info, warn}; 6 | use matrix_sdk::{ 7 | Client, 8 | crypto::{Emoji, SasState, format_emojis}, 9 | encryption::verification::{ 10 | SasVerification, Verification, VerificationRequest, VerificationRequestState, 11 | }, 12 | ruma::{ 13 | UserId, 14 | events::{ 15 | key::verification::request::ToDeviceKeyVerificationRequestEvent, 16 | room::message::{MessageType, OriginalSyncRoomMessageEvent}, 17 | }, 18 | }, 19 | }; 20 | use tokio::time::sleep; 21 | 22 | async fn confirm_emojis(sas: SasVerification, emoji: [Emoji; 7]) { 23 | info!("\n{}", format_emojis(emoji)); 24 | warn!("automatically confirming emojis in 10 seconds"); 25 | sleep(Duration::from_secs(10)).await; 26 | if let Err(error) = sas.confirm().await { 27 | error!("failed to confirm emojis: {error:?}"); 28 | } 29 | } 30 | 31 | async fn print_devices(user_id: &UserId, client: &Client) -> Result<()> { 32 | info!("devices of user {user_id}"); 33 | 34 | let own_id = client.device_id().context("missing own device id")?; 35 | for device in client 36 | .encryption() 37 | .get_user_devices(user_id) 38 | .await? 39 | .devices() 40 | .filter(|device| device.device_id() != own_id) 41 | { 42 | info!( 43 | "\t{:<10} {:<30} {:<}", 44 | device.device_id(), 45 | device.display_name().unwrap_or("-"), 46 | if device.is_verified() { "✅" } else { "❌" } 47 | ); 48 | } 49 | 50 | Ok(()) 51 | } 52 | 53 | async fn sas_verification_handler(client: Client, sas: SasVerification) -> Result<()> { 54 | info!( 55 | "starting verification with {} {}", 56 | &sas.other_device().user_id(), 57 | &sas.other_device().device_id() 58 | ); 59 | print_devices(sas.other_device().user_id(), &client).await?; 60 | sas.accept().await?; 61 | 62 | while let Some(state) = sas.changes().next().await { 63 | match state { 64 | SasState::KeysExchanged { 65 | emojis, 66 | decimals: _, 67 | } => { 68 | tokio::spawn(confirm_emojis( 69 | sas.clone(), 70 | emojis.context("only emoji verification supported")?.emojis, 71 | )); 72 | } 73 | SasState::Done { .. } => { 74 | let device = sas.other_device(); 75 | info!( 76 | "successfully verified device {} {} with trust {:?}", 77 | device.user_id(), 78 | device.device_id(), 79 | device.local_trust_state() 80 | ); 81 | print_devices(sas.other_device().user_id(), &client).await?; 82 | break; 83 | } 84 | SasState::Cancelled(info) => { 85 | warn!("verification cancelled: {}", info.reason()); 86 | break; 87 | } 88 | SasState::Created { .. } 89 | | SasState::Started { .. } 90 | | SasState::Accepted { .. } 91 | | SasState::Confirmed => (), 92 | } 93 | } 94 | 95 | Ok(()) 96 | } 97 | 98 | async fn request_verification_handler(client: Client, request: VerificationRequest) { 99 | info!( 100 | "accepting verification request from {}", 101 | request.other_user_id() 102 | ); 103 | if let Err(error) = request.accept().await { 104 | error!("failed to accept verification request: {error:?}"); 105 | return; 106 | } 107 | 108 | while let Some(state) = request.changes().next().await { 109 | match state { 110 | VerificationRequestState::Created { .. } 111 | | VerificationRequestState::Requested { .. } 112 | | VerificationRequestState::Ready { .. } => (), 113 | VerificationRequestState::Transitioned { verification } => { 114 | if let Verification::SasV1(sas) = verification { 115 | tokio::spawn(async move { 116 | if let Err(error) = sas_verification_handler(client, sas).await { 117 | error!("failed to handle sas verification request: {error:?}"); 118 | } 119 | }); 120 | break; 121 | } 122 | } 123 | VerificationRequestState::Done | VerificationRequestState::Cancelled(_) => break, 124 | } 125 | } 126 | } 127 | 128 | pub async fn on_device_key_verification_request( 129 | event: ToDeviceKeyVerificationRequestEvent, 130 | client: Client, 131 | ) -> Result<()> { 132 | let request = client 133 | .encryption() 134 | .get_verification_request(&event.sender, &event.content.transaction_id) 135 | .await 136 | .context("request object wasn't created")?; 137 | tokio::spawn(request_verification_handler(client, request)); 138 | 139 | Ok(()) 140 | } 141 | 142 | pub async fn on_room_message_verification_request( 143 | event: OriginalSyncRoomMessageEvent, 144 | client: Client, 145 | ) -> Result<()> { 146 | if let MessageType::VerificationRequest(_) = &event.content.msgtype { 147 | let request = client 148 | .encryption() 149 | .get_verification_request(&event.sender, &event.event_id) 150 | .await 151 | .context("request object wasn't created")?; 152 | tokio::spawn(request_verification_handler(client, request)); 153 | } 154 | 155 | Ok(()) 156 | } 157 | -------------------------------------------------------------------------------- /lib/utils.lua: -------------------------------------------------------------------------------- 1 | SpeedTracking = {} 2 | TpsTracking = {} 3 | 4 | function entity_speed(uuid, seconds) 5 | if not seconds then 6 | seconds = 1 7 | end 8 | 9 | local callback = function() 10 | local old_entity = SpeedTracking[uuid] 11 | local new_entity = client:find_entities(function(e) 12 | return e.uuid == uuid 13 | end)[1] 14 | 15 | if not new_entity then 16 | remove_listeners("tick", "speed-tracking_" .. uuid) 17 | SpeedTracking[uuid] = -1 18 | return 19 | end 20 | 21 | if old_entity then 22 | old_entity._distance = old_entity._distance + distance(old_entity.position, new_entity.position) 23 | old_entity.position = new_entity.position 24 | 25 | if old_entity._ticks < seconds * 20 then 26 | old_entity._ticks = old_entity._ticks + 1 27 | else 28 | remove_listeners("tick", "speed-tracking_" .. uuid) 29 | SpeedTracking[uuid] = old_entity._distance / seconds 30 | end 31 | else 32 | new_entity._ticks = 1 33 | new_entity._distance = 0 34 | SpeedTracking[uuid] = new_entity 35 | end 36 | end 37 | add_listener("tick", callback, "speed-tracking_" .. uuid) 38 | 39 | repeat 40 | sleep(seconds * 1000 / 10) 41 | until type(SpeedTracking[uuid]) == "number" 42 | 43 | local speed = SpeedTracking[uuid] 44 | SpeedTracking[uuid] = nil 45 | return speed 46 | end 47 | 48 | function tps(ms) 49 | if not ms then 50 | ms = 1000 51 | end 52 | 53 | add_listener("tick", function() 54 | if not TpsTracking.ticks then 55 | TpsTracking.ticks = 0 56 | sleep(ms) 57 | TpsTracking.result = TpsTracking.ticks 58 | remove_listeners("tick", "tps_tracking") 59 | else 60 | TpsTracking.ticks = TpsTracking.ticks + 1 61 | end 62 | end, "tps_tracking") 63 | 64 | sleep(ms) 65 | repeat 66 | sleep(20) 67 | until TpsTracking.result 68 | 69 | local tps = TpsTracking.result / (ms / 1000) 70 | TpsTracking = {} 71 | return tps 72 | end 73 | 74 | function nether_travel(pos, go_to_opts) 75 | info(string.format("going to %.2f %.2f %.2f through nether", pos.x, pos.y, pos.z)) 76 | 77 | local portal_block_states = get_block_states({ "nether_portal" }) 78 | local nether_pos = table.shallow_copy(pos) 79 | nether_pos.x = nether_pos.x / 8 80 | nether_pos.z = nether_pos.z / 8 81 | 82 | if client.dimension == "minecraft:overworld" then 83 | info("currently in overworld, finding nearest portal") 84 | local portals = client:find_blocks(client.position, portal_block_states) 85 | 86 | info(string.format("going to %.2f %.2f %.2f through nether", portals[1].x, portals[1].y, portals[1].z)) 87 | client:go_to(portals[1], go_to_opts) 88 | while client.dimension ~= "minecraft:the_nether" do 89 | sleep(1000) 90 | end 91 | sleep(3000) 92 | end 93 | 94 | info(string.format("currently in nether, going to %.2f %.2f", nether_pos.x, nether_pos.z)) 95 | client:go_to(nether_pos, { type = XZ_GOAL }) 96 | 97 | info("arrived, looking for nearest portal") 98 | local portals_nether = client:find_blocks(client.position, portal_block_states) 99 | if not next(portals_nether) then 100 | warn("failed to find portals in the nether") 101 | return 102 | end 103 | 104 | local found_portal = false 105 | for _, portal in ipairs(portals_nether) do 106 | if (client.position.y > 127) == (portal.y > 127) then 107 | found_portal = true 108 | 109 | info(string.format("found valid portal, going to %.2f %.2f %.2f", portal.x, portal.y, portal.z)) 110 | client:go_to(portal) 111 | while client.dimension ~= "minecraft:overworld" do 112 | sleep(1000) 113 | end 114 | sleep(3000) 115 | end 116 | 117 | if found_portal then 118 | break 119 | end 120 | end 121 | if not found_portal then 122 | warn("failed to find valid portals in the nether") 123 | return 124 | end 125 | 126 | info(string.format("back in overworld, going to %.2f %.2f %.2f", pos.x, pos.y, pos.z)) 127 | client:go_to(pos, go_to_opts) 128 | end 129 | 130 | function interact_bed() 131 | local bed = client:find_blocks( 132 | client.position, 133 | get_block_states({ 134 | "brown_bed", 135 | "white_bed", 136 | "yellow_bed", 137 | }) 138 | )[1] 139 | if not bed then 140 | return 141 | end 142 | 143 | client:go_to({ position = bed, radius = 2 }, { type = RADIUS_GOAL, options = { without_mining = true } }) 144 | client:look_at(bed) 145 | client:block_interact(bed) 146 | end 147 | 148 | function closest_entity(target_kind) 149 | local self_pos = client.position 150 | local entities = client:find_entities(function(e) 151 | return e.kind == target_kind 152 | end) 153 | 154 | local closest_entity = entities[1] 155 | local closest_distance = distance(closest_entity.position, self_pos) 156 | for _, entity in ipairs(entities) do 157 | local dist = distance(entity.position, self_pos) 158 | if dist <= closest_distance then 159 | closest_entity = entity 160 | closest_distance = dist 161 | end 162 | end 163 | return closest_entity 164 | end 165 | 166 | function get_player(name) 167 | local target_uuid = nil 168 | for _, player in ipairs(client.tab_list) do 169 | if player.name == name then 170 | target_uuid = player.uuid 171 | break 172 | end 173 | end 174 | 175 | return client:find_entities(function(e) 176 | return e.kind == "minecraft:player" and e.uuid == target_uuid 177 | end)[1] 178 | end 179 | 180 | function distance(p1, p2) 181 | return math.sqrt((p2.x - p1.x) ^ 2 + (p2.y - p1.y) ^ 2 + (p2.z - p1.z) ^ 2) 182 | end 183 | 184 | function dump(object) 185 | if type(object) == "table" then 186 | local string = "{ " 187 | local parts = {} 188 | for key, value in pairs(object) do 189 | table.insert(parts, key .. " = " .. dump(value)) 190 | end 191 | string = string .. table.concat(parts, ", ") 192 | return string .. " " .. "}" 193 | else 194 | return tostring(object) 195 | end 196 | end 197 | 198 | function dump_pretty(object, level) 199 | if not level then 200 | level = 0 201 | end 202 | if type(object) == "table" then 203 | local string = "{\n" .. string.rep(" ", level + 1) 204 | local parts = {} 205 | for key, value in pairs(object) do 206 | table.insert(parts, key .. " = " .. dump_pretty(value, level + 1)) 207 | end 208 | string = string .. table.concat(parts, ",\n" .. string.rep(" ", level + 1)) 209 | return string .. "\n" .. string.rep(" ", level) .. "}" 210 | else 211 | return tostring(object) 212 | end 213 | end 214 | -------------------------------------------------------------------------------- /src/particle.rs: -------------------------------------------------------------------------------- 1 | use azalea::{entity::particle::Particle, registry::ParticleKind}; 2 | 3 | #[allow(clippy::too_many_lines)] 4 | pub const fn to_kind(particle: &Particle) -> ParticleKind { 5 | match particle { 6 | Particle::AngryVillager => ParticleKind::AngryVillager, 7 | Particle::Block(_) => ParticleKind::Block, 8 | Particle::BlockMarker(_) => ParticleKind::BlockMarker, 9 | Particle::Bubble => ParticleKind::Bubble, 10 | Particle::Cloud => ParticleKind::Cloud, 11 | Particle::Crit => ParticleKind::Crit, 12 | Particle::DamageIndicator => ParticleKind::DamageIndicator, 13 | Particle::DragonBreath => ParticleKind::DragonBreath, 14 | Particle::DrippingLava => ParticleKind::DrippingLava, 15 | Particle::FallingLava => ParticleKind::FallingLava, 16 | Particle::LandingLava => ParticleKind::LandingLava, 17 | Particle::DrippingWater => ParticleKind::DrippingWater, 18 | Particle::FallingWater => ParticleKind::FallingWater, 19 | Particle::Dust(_) => ParticleKind::Dust, 20 | Particle::DustColorTransition(_) => ParticleKind::DustColorTransition, 21 | Particle::Effect => ParticleKind::Effect, 22 | Particle::ElderGuardian => ParticleKind::ElderGuardian, 23 | Particle::EnchantedHit => ParticleKind::EnchantedHit, 24 | Particle::Enchant => ParticleKind::Enchant, 25 | Particle::EndRod => ParticleKind::EndRod, 26 | Particle::EntityEffect(_) => ParticleKind::EntityEffect, 27 | Particle::ExplosionEmitter => ParticleKind::ExplosionEmitter, 28 | Particle::Explosion => ParticleKind::Explosion, 29 | Particle::Gust => ParticleKind::Gust, 30 | Particle::SonicBoom => ParticleKind::SonicBoom, 31 | Particle::FallingDust(_) => ParticleKind::FallingDust, 32 | Particle::Firework => ParticleKind::Firework, 33 | Particle::Fishing => ParticleKind::Fishing, 34 | Particle::Flame => ParticleKind::Flame, 35 | Particle::CherryLeaves => ParticleKind::CherryLeaves, 36 | Particle::PaleOakLeaves => ParticleKind::PaleOakLeaves, 37 | Particle::TintedLeaves => ParticleKind::TintedLeaves, 38 | Particle::SculkSoul => ParticleKind::SculkSoul, 39 | Particle::SculkCharge(_) => ParticleKind::SculkCharge, 40 | Particle::SculkChargePop => ParticleKind::SculkChargePop, 41 | Particle::SoulFireFlame => ParticleKind::SoulFireFlame, 42 | Particle::Soul => ParticleKind::Soul, 43 | Particle::Flash => ParticleKind::Flash, 44 | Particle::HappyVillager => ParticleKind::HappyVillager, 45 | Particle::Composter => ParticleKind::Composter, 46 | Particle::Heart => ParticleKind::Heart, 47 | Particle::InstantEffect => ParticleKind::InstantEffect, 48 | Particle::Item(_) => ParticleKind::Item, 49 | Particle::Vibration(_) => ParticleKind::Vibration, 50 | Particle::ItemSlime => ParticleKind::ItemSlime, 51 | Particle::ItemSnowball => ParticleKind::ItemSnowball, 52 | Particle::LargeSmoke => ParticleKind::LargeSmoke, 53 | Particle::Lava => ParticleKind::Lava, 54 | Particle::Mycelium => ParticleKind::Mycelium, 55 | Particle::Note => ParticleKind::Note, 56 | Particle::Poof => ParticleKind::Poof, 57 | Particle::Portal => ParticleKind::Portal, 58 | Particle::Rain => ParticleKind::Rain, 59 | Particle::Smoke => ParticleKind::Smoke, 60 | Particle::WhiteSmoke => ParticleKind::WhiteSmoke, 61 | Particle::Sneeze => ParticleKind::Sneeze, 62 | Particle::Spit => ParticleKind::Spit, 63 | Particle::SquidInk => ParticleKind::SquidInk, 64 | Particle::SweepAttack => ParticleKind::SweepAttack, 65 | Particle::TotemOfUndying => ParticleKind::TotemOfUndying, 66 | Particle::Underwater => ParticleKind::Underwater, 67 | Particle::Splash => ParticleKind::Splash, 68 | Particle::Witch => ParticleKind::Witch, 69 | Particle::BubblePop => ParticleKind::BubblePop, 70 | Particle::CurrentDown => ParticleKind::CurrentDown, 71 | Particle::BubbleColumnUp => ParticleKind::BubbleColumnUp, 72 | Particle::Nautilus => ParticleKind::Nautilus, 73 | Particle::Dolphin => ParticleKind::Dolphin, 74 | Particle::CampfireCosySmoke => ParticleKind::CampfireCosySmoke, 75 | Particle::CampfireSignalSmoke => ParticleKind::CampfireSignalSmoke, 76 | Particle::DrippingHoney => ParticleKind::DrippingHoney, 77 | Particle::FallingHoney => ParticleKind::FallingHoney, 78 | Particle::LandingHoney => ParticleKind::LandingHoney, 79 | Particle::FallingNectar => ParticleKind::FallingNectar, 80 | Particle::FallingSporeBlossom => ParticleKind::FallingSporeBlossom, 81 | Particle::Ash => ParticleKind::Ash, 82 | Particle::CrimsonSpore => ParticleKind::CrimsonSpore, 83 | Particle::WarpedSpore => ParticleKind::WarpedSpore, 84 | Particle::SporeBlossomAir => ParticleKind::SporeBlossomAir, 85 | Particle::DrippingObsidianTear => ParticleKind::DrippingObsidianTear, 86 | Particle::FallingObsidianTear => ParticleKind::FallingObsidianTear, 87 | Particle::LandingObsidianTear => ParticleKind::LandingObsidianTear, 88 | Particle::ReversePortal => ParticleKind::ReversePortal, 89 | Particle::WhiteAsh => ParticleKind::WhiteAsh, 90 | Particle::SmallFlame => ParticleKind::SmallFlame, 91 | Particle::Snowflake => ParticleKind::Snowflake, 92 | Particle::DrippingDripstoneLava => ParticleKind::DrippingDripstoneLava, 93 | Particle::FallingDripstoneLava => ParticleKind::FallingDripstoneLava, 94 | Particle::DrippingDripstoneWater => ParticleKind::DrippingDripstoneWater, 95 | Particle::FallingDripstoneWater => ParticleKind::FallingDripstoneWater, 96 | Particle::GlowSquidInk => ParticleKind::GlowSquidInk, 97 | Particle::Glow => ParticleKind::Glow, 98 | Particle::WaxOn => ParticleKind::WaxOn, 99 | Particle::WaxOff => ParticleKind::WaxOff, 100 | Particle::ElectricSpark => ParticleKind::ElectricSpark, 101 | Particle::Scrape => ParticleKind::Scrape, 102 | Particle::Shriek(_) => ParticleKind::Shriek, 103 | Particle::EggCrack => ParticleKind::EggCrack, 104 | Particle::DustPlume => ParticleKind::DustPlume, 105 | Particle::SmallGust => ParticleKind::SmallGust, 106 | Particle::GustEmitterLarge => ParticleKind::GustEmitterLarge, 107 | Particle::GustEmitterSmall => ParticleKind::GustEmitterSmall, 108 | Particle::Infested => ParticleKind::Infested, 109 | Particle::ItemCobweb => ParticleKind::ItemCobweb, 110 | Particle::TrialSpawnerDetection => ParticleKind::TrialSpawnerDetection, 111 | Particle::TrialSpawnerDetectionOminous => ParticleKind::TrialSpawnerDetectionOminous, 112 | Particle::VaultConnection => ParticleKind::VaultConnection, 113 | Particle::DustPillar => ParticleKind::DustPillar, 114 | Particle::OminousSpawning => ParticleKind::OminousSpawning, 115 | Particle::RaidOmen => ParticleKind::RaidOmen, 116 | Particle::TrialOmen => ParticleKind::TrialOmen, 117 | Particle::Trail => ParticleKind::Trail, 118 | Particle::BlockCrumble => ParticleKind::BlockCrumble, 119 | Particle::Firefly => ParticleKind::Firefly, 120 | } 121 | } 122 | -------------------------------------------------------------------------------- /src/lua/client/movement.rs: -------------------------------------------------------------------------------- 1 | use azalea::{ 2 | BlockPos, BotClientExt, SprintDirection, WalkDirection, 3 | entity::Position, 4 | interact::HitResultComponent, 5 | pathfinder::{ 6 | ExecutingPath, GotoEvent, Pathfinder, PathfinderClientExt, 7 | goals::{BlockPosGoal, Goal, InverseGoal, RadiusGoal, ReachBlockPosGoal, XZGoal, YGoal}, 8 | }, 9 | protocol::packets::game::{ServerboundPlayerCommand, s_player_command::Action}, 10 | world::MinecraftEntityId, 11 | }; 12 | use log::error; 13 | use mlua::{FromLua, Lua, Result, Table, UserDataRef, Value}; 14 | 15 | use super::{Client, Direction, Vec3}; 16 | 17 | #[derive(Debug)] 18 | struct AnyGoal(Box); 19 | 20 | impl Goal for AnyGoal { 21 | fn success(&self, n: BlockPos) -> bool { 22 | self.0.success(n) 23 | } 24 | 25 | fn heuristic(&self, n: BlockPos) -> f32 { 26 | self.0.heuristic(n) 27 | } 28 | } 29 | 30 | #[allow(clippy::cast_possible_truncation)] 31 | fn to_goal(lua: &Lua, client: &Client, data: Table, options: &Table, kind: u8) -> Result { 32 | let goal: Box = match kind { 33 | 1 => { 34 | let pos = Vec3::from_lua(data.get("position")?, lua)?; 35 | Box::new(RadiusGoal { 36 | pos: azalea::Vec3::new(pos.x, pos.y, pos.z), 37 | radius: data.get("radius")?, 38 | }) 39 | } 40 | 2 => { 41 | let pos = Vec3::from_lua(Value::Table(data), lua)?; 42 | Box::new(ReachBlockPosGoal { 43 | pos: BlockPos::new(pos.x as i32, pos.y as i32, pos.z as i32), 44 | chunk_storage: client.world().read().chunks.clone(), 45 | }) 46 | } 47 | 3 => Box::new(XZGoal { 48 | x: data.get("x")?, 49 | z: data.get("z")?, 50 | }), 51 | 4 => Box::new(YGoal { y: data.get("y")? }), 52 | _ => { 53 | let pos = Vec3::from_lua(Value::Table(data), lua)?; 54 | Box::new(BlockPosGoal(BlockPos::new( 55 | pos.x as i32, 56 | pos.y as i32, 57 | pos.z as i32, 58 | ))) 59 | } 60 | }; 61 | 62 | Ok(AnyGoal(if options.get("inverse").unwrap_or_default() { 63 | Box::new(InverseGoal(AnyGoal(goal))) 64 | } else { 65 | goal 66 | })) 67 | } 68 | 69 | pub fn go_to_reached(_lua: &Lua, client: &Client) -> Result { 70 | Ok(client.is_goto_target_reached()) 71 | } 72 | 73 | pub async fn go_to_wait_until_reached( 74 | _lua: Lua, 75 | client: UserDataRef, 76 | (): (), 77 | ) -> Result<()> { 78 | client.wait_until_goto_target_reached().await; 79 | Ok(()) 80 | } 81 | 82 | pub async fn go_to( 83 | lua: Lua, 84 | client: UserDataRef, 85 | (data, metadata): (Table, Option
), 86 | ) -> Result<()> { 87 | let metadata = metadata.unwrap_or(lua.create_table()?); 88 | let options = metadata.get("options").unwrap_or(lua.create_table()?); 89 | let goal = to_goal( 90 | &lua, 91 | &client, 92 | data, 93 | &options, 94 | metadata.get("type").unwrap_or_default(), 95 | )?; 96 | if options.get("without_mining").unwrap_or_default() { 97 | client.start_goto_without_mining(goal); 98 | client.wait_until_goto_target_reached().await; 99 | } else { 100 | client.goto(goal).await; 101 | } 102 | Ok(()) 103 | } 104 | 105 | pub async fn start_go_to( 106 | lua: Lua, 107 | client: UserDataRef, 108 | (data, metadata): (Table, Option
), 109 | ) -> Result<()> { 110 | let metadata = metadata.unwrap_or(lua.create_table()?); 111 | let options = metadata.get("options").unwrap_or(lua.create_table()?); 112 | let goal = to_goal( 113 | &lua, 114 | &client, 115 | data, 116 | &options, 117 | metadata.get("type").unwrap_or_default(), 118 | )?; 119 | if options.get("without_mining").unwrap_or_default() { 120 | client.start_goto_without_mining(goal); 121 | } else { 122 | client.start_goto(goal); 123 | } 124 | while client.get_tick_broadcaster().recv().await.is_ok() { 125 | if client.ecs.lock().get::(client.entity).is_none() { 126 | break; 127 | } 128 | } 129 | 130 | Ok(()) 131 | } 132 | 133 | pub fn direction(_lua: &Lua, client: &Client) -> Result { 134 | let direction = client.direction(); 135 | Ok(Direction { 136 | y: direction.0, 137 | x: direction.1, 138 | }) 139 | } 140 | 141 | pub fn eye_position(_lua: &Lua, client: &Client) -> Result { 142 | Ok(Vec3::from(client.eye_position())) 143 | } 144 | 145 | pub fn jump(_lua: &Lua, client: &Client, (): ()) -> Result<()> { 146 | client.jump(); 147 | Ok(()) 148 | } 149 | 150 | pub fn looking_at(lua: &Lua, client: &Client) -> Result> { 151 | let result = client.component::(); 152 | Ok(if result.miss { 153 | None 154 | } else { 155 | let table = lua.create_table()?; 156 | table.set("position", Vec3::from(result.block_pos))?; 157 | table.set("inside", result.inside)?; 158 | table.set("world_border", result.world_border)?; 159 | Some(table) 160 | }) 161 | } 162 | 163 | pub fn look_at(_lua: &Lua, client: &Client, position: Vec3) -> Result<()> { 164 | client.look_at(azalea::Vec3::new(position.x, position.y, position.z)); 165 | Ok(()) 166 | } 167 | 168 | pub fn pathfinder(lua: &Lua, client: &Client) -> Result
{ 169 | let table = lua.create_table()?; 170 | table.set( 171 | "is_calculating", 172 | client.component::().is_calculating, 173 | )?; 174 | table.set( 175 | "is_executing", 176 | if let Some(pathfinder) = client.get_component::() { 177 | table.set( 178 | "last_reached_node", 179 | Vec3::from(pathfinder.last_reached_node), 180 | )?; 181 | table.set( 182 | "last_node_reach_elapsed", 183 | pathfinder.last_node_reached_at.elapsed().as_millis(), 184 | )?; 185 | table.set("is_path_partial", pathfinder.is_path_partial)?; 186 | true 187 | } else { 188 | false 189 | }, 190 | )?; 191 | Ok(table) 192 | } 193 | 194 | pub fn position(_lua: &Lua, client: &Client) -> Result { 195 | Ok(Vec3::from(&client.component::())) 196 | } 197 | 198 | pub fn set_direction(_lua: &Lua, client: &Client, direction: Direction) -> Result<()> { 199 | client.set_direction(direction.y, direction.x); 200 | Ok(()) 201 | } 202 | 203 | pub fn set_jumping(_lua: &Lua, client: &Client, jumping: bool) -> Result<()> { 204 | client.set_jumping(jumping); 205 | Ok(()) 206 | } 207 | 208 | pub fn set_position(_lua: &Lua, client: &Client, new_position: Vec3) -> Result<()> { 209 | let mut ecs = client.ecs.lock(); 210 | let mut position = client.query::<&mut Position>(&mut ecs); 211 | position.x = new_position.x; 212 | position.y = new_position.y; 213 | position.z = new_position.z; 214 | Ok(()) 215 | } 216 | 217 | pub fn set_sneaking(_lua: &Lua, client: &Client, sneaking: bool) -> Result<()> { 218 | if let Err(error) = client.write_packet(ServerboundPlayerCommand { 219 | id: client.component::(), 220 | action: if sneaking { 221 | Action::PressShiftKey 222 | } else { 223 | Action::ReleaseShiftKey 224 | }, 225 | data: 0, 226 | }) { 227 | error!("failed to send PlayerCommand packet: {error:?}"); 228 | } 229 | Ok(()) 230 | } 231 | 232 | pub fn sprint(_lua: &Lua, client: &Client, direction: u8) -> Result<()> { 233 | client.sprint(match direction { 234 | 5 => SprintDirection::ForwardRight, 235 | 6 => SprintDirection::ForwardLeft, 236 | _ => SprintDirection::Forward, 237 | }); 238 | Ok(()) 239 | } 240 | 241 | pub fn stop_pathfinding(_lua: &Lua, client: &Client, (): ()) -> Result<()> { 242 | client.stop_pathfinding(); 243 | Ok(()) 244 | } 245 | 246 | pub fn stop_sleeping(_lua: &Lua, client: &Client, (): ()) -> Result<()> { 247 | if let Err(error) = client.write_packet(ServerboundPlayerCommand { 248 | id: client.component::(), 249 | action: Action::StopSleeping, 250 | data: 0, 251 | }) { 252 | error!("failed to send PlayerCommand packet: {error:?}"); 253 | } 254 | Ok(()) 255 | } 256 | 257 | pub fn walk(_lua: &Lua, client: &Client, direction: u8) -> Result<()> { 258 | client.walk(match direction { 259 | 1 => WalkDirection::Forward, 260 | 2 => WalkDirection::Backward, 261 | 3 => WalkDirection::Left, 262 | 4 => WalkDirection::Right, 263 | 5 => WalkDirection::ForwardRight, 264 | 6 => WalkDirection::ForwardLeft, 265 | 7 => WalkDirection::BackwardRight, 266 | 8 => WalkDirection::BackwardLeft, 267 | _ => WalkDirection::None, 268 | }); 269 | Ok(()) 270 | } 271 | -------------------------------------------------------------------------------- /src/events.rs: -------------------------------------------------------------------------------- 1 | use std::{net::SocketAddr, process::exit}; 2 | 3 | use anyhow::{Context, Result}; 4 | use azalea::{ 5 | brigadier::exceptions::BuiltInExceptions::DispatcherUnknownCommand, prelude::*, 6 | protocol::packets::game::ClientboundGamePacket, 7 | }; 8 | use hyper::{server::conn::http1, service::service_fn}; 9 | use hyper_util::rt::TokioIo; 10 | use log::{debug, error, info, trace}; 11 | use mlua::{Error, Function, IntoLuaMulti, Table}; 12 | use ncr::utils::trim_header; 13 | use tokio::net::TcpListener; 14 | #[cfg(feature = "matrix")] 15 | use {crate::matrix, std::time::Duration, tokio::time::sleep}; 16 | 17 | use crate::{ 18 | State, 19 | commands::CommandSource, 20 | http::serve, 21 | lua::{client, direction::Direction, player::Player, vec3::Vec3}, 22 | particle, 23 | replay::recorder::Recorder, 24 | }; 25 | 26 | #[allow(clippy::cognitive_complexity, clippy::too_many_lines)] 27 | pub async fn handle_event(client: Client, event: Event, state: State) -> Result<()> { 28 | match event { 29 | Event::AddPlayer(player_info) => { 30 | call_listeners(&state, "add_player", || Ok(Player::from(player_info))).await 31 | } 32 | Event::Chat(message) => { 33 | let globals = state.lua.globals(); 34 | let (sender, mut content) = message.split_sender_and_content(); 35 | let uuid = message.sender_uuid().map(|uuid| uuid.to_string()); 36 | let is_whisper = message.is_whisper(); 37 | let text = message.message(); 38 | let ansi_text = text.to_ansi(); 39 | info!("{ansi_text}"); 40 | 41 | let mut is_encrypted = false; 42 | if let Some(ref sender) = sender { 43 | let mut ncr_options = None; 44 | if let Ok(options) = globals.get::
("NcrOptions") 45 | && let Ok(decrypt) = globals.get::("ncr_decrypt") 46 | && let Some(plaintext) = decrypt 47 | .call::((options.clone(), content.clone())) 48 | .ok() 49 | .as_deref() 50 | .and_then(|string| trim_header(string).ok()) 51 | { 52 | is_encrypted = true; 53 | ncr_options = Some(options); 54 | plaintext.clone_into(&mut content); 55 | info!("decrypted message from {sender}: {content}"); 56 | } 57 | 58 | if is_whisper 59 | && globals 60 | .get::>("Owners") 61 | .unwrap_or_default() 62 | .contains(sender) 63 | && let Err(error) = state.commands.execute( 64 | content.clone(), 65 | CommandSource { 66 | client: client.clone(), 67 | message: message.clone(), 68 | state: state.clone(), 69 | ncr_options: ncr_options.clone(), 70 | } 71 | .into(), 72 | ) 73 | && error.type_ != DispatcherUnknownCommand 74 | { 75 | CommandSource { 76 | client, 77 | message, 78 | state: state.clone(), 79 | ncr_options, 80 | } 81 | .reply(&format!("{error:?}")); 82 | } 83 | } 84 | 85 | call_listeners(&state, "chat", || { 86 | let table = state.lua.create_table()?; 87 | table.set("text", text.to_string())?; 88 | table.set("ansi_text", ansi_text)?; 89 | table.set("sender", sender)?; 90 | table.set("content", content)?; 91 | table.set("uuid", uuid)?; 92 | table.set("is_whisper", is_whisper)?; 93 | table.set("is_encrypted", is_encrypted)?; 94 | Ok(table) 95 | }) 96 | .await 97 | } 98 | Event::Death(packet) => { 99 | if let Some(packet) = packet { 100 | call_listeners(&state, "death", || { 101 | let message_table = state.lua.create_table()?; 102 | message_table.set("text", packet.message.to_string())?; 103 | message_table.set("ansi_text", packet.message.to_ansi())?; 104 | let table = state.lua.create_table()?; 105 | table.set("message", message_table)?; 106 | table.set("player_id", packet.player_id.0)?; 107 | Ok(table) 108 | }) 109 | .await 110 | } else { 111 | call_listeners(&state, "death", || Ok(())).await 112 | } 113 | } 114 | Event::Disconnect(message) => { 115 | if let Some(message) = message { 116 | call_listeners(&state, "disconnect", || { 117 | let table = state.lua.create_table()?; 118 | table.set("text", message.to_string())?; 119 | table.set("ansi_text", message.to_ansi())?; 120 | Ok(table) 121 | }) 122 | .await 123 | } else { 124 | call_listeners(&state, "disconnect", || Ok(())).await 125 | } 126 | } 127 | Event::KeepAlive(id) => call_listeners(&state, "keep_alive", || Ok(id)).await, 128 | Event::Login => call_listeners(&state, "login", || Ok(())).await, 129 | Event::RemovePlayer(player_info) => { 130 | call_listeners(&state, "remove_player", || Ok(Player::from(player_info))).await 131 | } 132 | Event::Spawn => call_listeners(&state, "spawn", || Ok(())).await, 133 | Event::Tick => call_listeners(&state, "tick", || Ok(())).await, 134 | Event::UpdatePlayer(player_info) => { 135 | call_listeners(&state, "update_player", || Ok(Player::from(player_info))).await 136 | } 137 | Event::Packet(packet) => match packet.as_ref() { 138 | ClientboundGamePacket::AddEntity(packet) => { 139 | call_listeners(&state, "add_entity", || { 140 | let table = state.lua.create_table()?; 141 | table.set("id", packet.id.0)?; 142 | table.set("uuid", packet.uuid.to_string())?; 143 | table.set("kind", packet.entity_type.to_string())?; 144 | table.set("position", Vec3::from(packet.position))?; 145 | table.set( 146 | "direction", 147 | Direction { 148 | y: f32::from(packet.y_rot) / (256.0 / 360.0), 149 | x: f32::from(packet.x_rot) / (256.0 / 360.0), 150 | }, 151 | )?; 152 | table.set("data", packet.data)?; 153 | Ok(table) 154 | }) 155 | .await 156 | } 157 | ClientboundGamePacket::LevelParticles(packet) => { 158 | call_listeners(&state, "level_particles", || { 159 | let table = state.lua.create_table()?; 160 | table.set("position", Vec3::from(packet.pos))?; 161 | table.set("count", packet.count)?; 162 | table.set("kind", particle::to_kind(&packet.particle) as u8)?; 163 | Ok(table) 164 | }) 165 | .await 166 | } 167 | ClientboundGamePacket::RemoveEntities(packet) => { 168 | call_listeners(&state, "remove_entities", || { 169 | Ok(packet.entity_ids.iter().map(|id| id.0).collect::>()) 170 | }) 171 | .await 172 | } 173 | ClientboundGamePacket::SetHealth(packet) => { 174 | call_listeners(&state, "set_health", || { 175 | let table = state.lua.create_table()?; 176 | table.set("food", packet.food)?; 177 | table.set("health", packet.health)?; 178 | table.set("saturation", packet.saturation)?; 179 | Ok(table) 180 | }) 181 | .await 182 | } 183 | ClientboundGamePacket::SetPassengers(packet) => { 184 | call_listeners(&state, "set_passengers", || { 185 | let table = state.lua.create_table()?; 186 | table.set("vehicle", packet.vehicle)?; 187 | table.set("passengers", &*packet.passengers)?; 188 | Ok(table) 189 | }) 190 | .await 191 | } 192 | ClientboundGamePacket::SetTime(packet) => { 193 | call_listeners(&state, "set_time", || { 194 | let table = state.lua.create_table()?; 195 | table.set("day_time", packet.day_time)?; 196 | table.set("game_time", packet.game_time)?; 197 | table.set("tick_day_time", packet.tick_day_time)?; 198 | Ok(table) 199 | }) 200 | .await 201 | } 202 | _ => Ok(()), 203 | }, 204 | Event::Init => { 205 | debug!("received init event"); 206 | 207 | let ecs = client.ecs.clone(); 208 | ctrlc::set_handler(move || { 209 | ecs.lock() 210 | .remove_resource::() 211 | .map(Recorder::finish); 212 | exit(0); 213 | })?; 214 | 215 | #[cfg(feature = "matrix")] 216 | matrix_init(&client, state.clone()); 217 | 218 | let globals = state.lua.globals(); 219 | lua_init(client, &state, &globals).await?; 220 | 221 | let Some(address): Option = globals 222 | .get::("HttpAddress") 223 | .ok() 224 | .and_then(|string| string.parse().ok()) 225 | else { 226 | return Ok(()); 227 | }; 228 | 229 | let listener = TcpListener::bind(address).await.inspect_err(|error| { 230 | error!("failed to listen on {address}: {error:?}"); 231 | })?; 232 | debug!("http server listening on {address}"); 233 | 234 | loop { 235 | let (stream, peer) = match listener.accept().await { 236 | Ok(pair) => pair, 237 | Err(error) => { 238 | error!("failed to accept connection: {error:?}"); 239 | continue; 240 | } 241 | }; 242 | trace!("http server got connection from {peer}"); 243 | 244 | let conn_state = state.clone(); 245 | let service = service_fn(move |request| { 246 | let request_state = conn_state.clone(); 247 | async move { serve(request, request_state).await } 248 | }); 249 | 250 | tokio::spawn(async move { 251 | if let Err(error) = http1::Builder::new() 252 | .serve_connection(TokioIo::new(stream), service) 253 | .await 254 | { 255 | error!("failed to serve connection: {error:?}"); 256 | } 257 | }); 258 | } 259 | } 260 | _ => todo!(), 261 | } 262 | } 263 | 264 | async fn lua_init(client: Client, state: &State, globals: &Table) -> Result<()> { 265 | let ecs = client.ecs.clone(); 266 | globals.set( 267 | "finish_replay_recording", 268 | state.lua.create_function_mut(move |_, (): ()| { 269 | ecs.lock() 270 | .remove_resource::() 271 | .context("recording not active") 272 | .map_err(Error::external)? 273 | .finish() 274 | .map_err(Error::external) 275 | })?, 276 | )?; 277 | globals.set("client", client::Client(Some(client)))?; 278 | call_listeners(state, "init", || Ok(())).await 279 | } 280 | 281 | #[cfg(feature = "matrix")] 282 | fn matrix_init(client: &Client, state: State) { 283 | let globals = state.lua.globals(); 284 | if let Ok(options) = globals.get::
("MatrixOptions") { 285 | let name = client.username(); 286 | tokio::spawn(async move { 287 | loop { 288 | let name = name.clone(); 289 | if let Err(error) = matrix::login(&state, &options, &globals, name).await { 290 | error!("failed to log into matrix: {error:?}"); 291 | } 292 | sleep(Duration::from_secs(10)).await; 293 | } 294 | }); 295 | } 296 | } 297 | 298 | pub async fn call_listeners(state: &State, event_type: &'static str, getter: F) -> Result<()> 299 | where 300 | T: Clone + IntoLuaMulti + Send + 'static, 301 | F: FnOnce() -> Result, 302 | { 303 | if let Some(listeners) = state.event_listeners.read().await.get(event_type).cloned() { 304 | let data = getter()?; 305 | for (id, callback) in listeners { 306 | let data = data.clone(); 307 | tokio::spawn(async move { 308 | if let Err(error) = callback.call_async::<()>(data).await { 309 | error!("failed to call lua event listener {id} for {event_type}: {error}"); 310 | } 311 | }); 312 | } 313 | } 314 | Ok(()) 315 | } 316 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | GNU GENERAL PUBLIC LICENSE 2 | Version 3, 29 June 2007 3 | 4 | Copyright (C) 2007 Free Software Foundation, Inc. 5 | Everyone is permitted to copy and distribute verbatim copies 6 | of this license document, but changing it is not allowed. 7 | 8 | Preamble 9 | 10 | The GNU General Public License is a free, copyleft license for 11 | software and other kinds of works. 12 | 13 | The licenses for most software and other practical works are designed 14 | to take away your freedom to share and change the works. By contrast, 15 | the GNU General Public License is intended to guarantee your freedom to 16 | share and change all versions of a program--to make sure it remains free 17 | software for all its users. We, the Free Software Foundation, use the 18 | GNU General Public License for most of our software; it applies also to 19 | any other work released this way by its authors. You can apply it to 20 | your programs, too. 21 | 22 | When we speak of free software, we are referring to freedom, not 23 | price. Our General Public Licenses are designed to make sure that you 24 | have the freedom to distribute copies of free software (and charge for 25 | them if you wish), that you receive source code or can get it if you 26 | want it, that you can change the software or use pieces of it in new 27 | free programs, and that you know you can do these things. 28 | 29 | To protect your rights, we need to prevent others from denying you 30 | these rights or asking you to surrender the rights. Therefore, you have 31 | certain responsibilities if you distribute copies of the software, or if 32 | you modify it: responsibilities to respect the freedom of others. 33 | 34 | For example, if you distribute copies of such a program, whether 35 | gratis or for a fee, you must pass on to the recipients the same 36 | freedoms that you received. You must make sure that they, too, receive 37 | or can get the source code. And you must show them these terms so they 38 | know their rights. 39 | 40 | Developers that use the GNU GPL protect your rights with two steps: 41 | (1) assert copyright on the software, and (2) offer you this License 42 | giving you legal permission to copy, distribute and/or modify it. 43 | 44 | For the developers' and authors' protection, the GPL clearly explains 45 | that there is no warranty for this free software. For both users' and 46 | authors' sake, the GPL requires that modified versions be marked as 47 | changed, so that their problems will not be attributed erroneously to 48 | authors of previous versions. 49 | 50 | Some devices are designed to deny users access to install or run 51 | modified versions of the software inside them, although the manufacturer 52 | can do so. This is fundamentally incompatible with the aim of 53 | protecting users' freedom to change the software. The systematic 54 | pattern of such abuse occurs in the area of products for individuals to 55 | use, which is precisely where it is most unacceptable. Therefore, we 56 | have designed this version of the GPL to prohibit the practice for those 57 | products. If such problems arise substantially in other domains, we 58 | stand ready to extend this provision to those domains in future versions 59 | of the GPL, as needed to protect the freedom of users. 60 | 61 | Finally, every program is threatened constantly by software patents. 62 | States should not allow patents to restrict development and use of 63 | software on general-purpose computers, but in those that do, we wish to 64 | avoid the special danger that patents applied to a free program could 65 | make it effectively proprietary. To prevent this, the GPL assures that 66 | patents cannot be used to render the program non-free. 67 | 68 | The precise terms and conditions for copying, distribution and 69 | modification follow. 70 | 71 | TERMS AND CONDITIONS 72 | 73 | 0. Definitions. 74 | 75 | "This License" refers to version 3 of the GNU General Public License. 76 | 77 | "Copyright" also means copyright-like laws that apply to other kinds of 78 | works, such as semiconductor masks. 79 | 80 | "The Program" refers to any copyrightable work licensed under this 81 | License. Each licensee is addressed as "you". "Licensees" and 82 | "recipients" may be individuals or organizations. 83 | 84 | To "modify" a work means to copy from or adapt all or part of the work 85 | in a fashion requiring copyright permission, other than the making of an 86 | exact copy. The resulting work is called a "modified version" of the 87 | earlier work or a work "based on" the earlier work. 88 | 89 | A "covered work" means either the unmodified Program or a work based 90 | on the Program. 91 | 92 | To "propagate" a work means to do anything with it that, without 93 | permission, would make you directly or secondarily liable for 94 | infringement under applicable copyright law, except executing it on a 95 | computer or modifying a private copy. Propagation includes copying, 96 | distribution (with or without modification), making available to the 97 | public, and in some countries other activities as well. 98 | 99 | To "convey" a work means any kind of propagation that enables other 100 | parties to make or receive copies. Mere interaction with a user through 101 | a computer network, with no transfer of a copy, is not conveying. 102 | 103 | An interactive user interface displays "Appropriate Legal Notices" 104 | to the extent that it includes a convenient and prominently visible 105 | feature that (1) displays an appropriate copyright notice, and (2) 106 | tells the user that there is no warranty for the work (except to the 107 | extent that warranties are provided), that licensees may convey the 108 | work under this License, and how to view a copy of this License. If 109 | the interface presents a list of user commands or options, such as a 110 | menu, a prominent item in the list meets this criterion. 111 | 112 | 1. Source Code. 113 | 114 | The "source code" for a work means the preferred form of the work 115 | for making modifications to it. "Object code" means any non-source 116 | form of a work. 117 | 118 | A "Standard Interface" means an interface that either is an official 119 | standard defined by a recognized standards body, or, in the case of 120 | interfaces specified for a particular programming language, one that 121 | is widely used among developers working in that language. 122 | 123 | The "System Libraries" of an executable work include anything, other 124 | than the work as a whole, that (a) is included in the normal form of 125 | packaging a Major Component, but which is not part of that Major 126 | Component, and (b) serves only to enable use of the work with that 127 | Major Component, or to implement a Standard Interface for which an 128 | implementation is available to the public in source code form. A 129 | "Major Component", in this context, means a major essential component 130 | (kernel, window system, and so on) of the specific operating system 131 | (if any) on which the executable work runs, or a compiler used to 132 | produce the work, or an object code interpreter used to run it. 133 | 134 | The "Corresponding Source" for a work in object code form means all 135 | the source code needed to generate, install, and (for an executable 136 | work) run the object code and to modify the work, including scripts to 137 | control those activities. However, it does not include the work's 138 | System Libraries, or general-purpose tools or generally available free 139 | programs which are used unmodified in performing those activities but 140 | which are not part of the work. For example, Corresponding Source 141 | includes interface definition files associated with source files for 142 | the work, and the source code for shared libraries and dynamically 143 | linked subprograms that the work is specifically designed to require, 144 | such as by intimate data communication or control flow between those 145 | subprograms and other parts of the work. 146 | 147 | The Corresponding Source need not include anything that users 148 | can regenerate automatically from other parts of the Corresponding 149 | Source. 150 | 151 | The Corresponding Source for a work in source code form is that 152 | same work. 153 | 154 | 2. Basic Permissions. 155 | 156 | All rights granted under this License are granted for the term of 157 | copyright on the Program, and are irrevocable provided the stated 158 | conditions are met. This License explicitly affirms your unlimited 159 | permission to run the unmodified Program. The output from running a 160 | covered work is covered by this License only if the output, given its 161 | content, constitutes a covered work. This License acknowledges your 162 | rights of fair use or other equivalent, as provided by copyright law. 163 | 164 | You may make, run and propagate covered works that you do not 165 | convey, without conditions so long as your license otherwise remains 166 | in force. You may convey covered works to others for the sole purpose 167 | of having them make modifications exclusively for you, or provide you 168 | with facilities for running those works, provided that you comply with 169 | the terms of this License in conveying all material for which you do 170 | not control copyright. Those thus making or running the covered works 171 | for you must do so exclusively on your behalf, under your direction 172 | and control, on terms that prohibit them from making any copies of 173 | your copyrighted material outside their relationship with you. 174 | 175 | Conveying under any other circumstances is permitted solely under 176 | the conditions stated below. Sublicensing is not allowed; section 10 177 | makes it unnecessary. 178 | 179 | 3. Protecting Users' Legal Rights From Anti-Circumvention Law. 180 | 181 | No covered work shall be deemed part of an effective technological 182 | measure under any applicable law fulfilling obligations under article 183 | 11 of the WIPO copyright treaty adopted on 20 December 1996, or 184 | similar laws prohibiting or restricting circumvention of such 185 | measures. 186 | 187 | When you convey a covered work, you waive any legal power to forbid 188 | circumvention of technological measures to the extent such circumvention 189 | is effected by exercising rights under this License with respect to 190 | the covered work, and you disclaim any intention to limit operation or 191 | modification of the work as a means of enforcing, against the work's 192 | users, your or third parties' legal rights to forbid circumvention of 193 | technological measures. 194 | 195 | 4. Conveying Verbatim Copies. 196 | 197 | You may convey verbatim copies of the Program's source code as you 198 | receive it, in any medium, provided that you conspicuously and 199 | appropriately publish on each copy an appropriate copyright notice; 200 | keep intact all notices stating that this License and any 201 | non-permissive terms added in accord with section 7 apply to the code; 202 | keep intact all notices of the absence of any warranty; and give all 203 | recipients a copy of this License along with the Program. 204 | 205 | You may charge any price or no price for each copy that you convey, 206 | and you may offer support or warranty protection for a fee. 207 | 208 | 5. Conveying Modified Source Versions. 209 | 210 | You may convey a work based on the Program, or the modifications to 211 | produce it from the Program, in the form of source code under the 212 | terms of section 4, provided that you also meet all of these conditions: 213 | 214 | a) The work must carry prominent notices stating that you modified 215 | it, and giving a relevant date. 216 | 217 | b) The work must carry prominent notices stating that it is 218 | released under this License and any conditions added under section 219 | 7. This requirement modifies the requirement in section 4 to 220 | "keep intact all notices". 221 | 222 | c) You must license the entire work, as a whole, under this 223 | License to anyone who comes into possession of a copy. This 224 | License will therefore apply, along with any applicable section 7 225 | additional terms, to the whole of the work, and all its parts, 226 | regardless of how they are packaged. This License gives no 227 | permission to license the work in any other way, but it does not 228 | invalidate such permission if you have separately received it. 229 | 230 | d) If the work has interactive user interfaces, each must display 231 | Appropriate Legal Notices; however, if the Program has interactive 232 | interfaces that do not display Appropriate Legal Notices, your 233 | work need not make them do so. 234 | 235 | A compilation of a covered work with other separate and independent 236 | works, which are not by their nature extensions of the covered work, 237 | and which are not combined with it such as to form a larger program, 238 | in or on a volume of a storage or distribution medium, is called an 239 | "aggregate" if the compilation and its resulting copyright are not 240 | used to limit the access or legal rights of the compilation's users 241 | beyond what the individual works permit. Inclusion of a covered work 242 | in an aggregate does not cause this License to apply to the other 243 | parts of the aggregate. 244 | 245 | 6. Conveying Non-Source Forms. 246 | 247 | You may convey a covered work in object code form under the terms 248 | of sections 4 and 5, provided that you also convey the 249 | machine-readable Corresponding Source under the terms of this License, 250 | in one of these ways: 251 | 252 | a) Convey the object code in, or embodied in, a physical product 253 | (including a physical distribution medium), accompanied by the 254 | Corresponding Source fixed on a durable physical medium 255 | customarily used for software interchange. 256 | 257 | b) Convey the object code in, or embodied in, a physical product 258 | (including a physical distribution medium), accompanied by a 259 | written offer, valid for at least three years and valid for as 260 | long as you offer spare parts or customer support for that product 261 | model, to give anyone who possesses the object code either (1) a 262 | copy of the Corresponding Source for all the software in the 263 | product that is covered by this License, on a durable physical 264 | medium customarily used for software interchange, for a price no 265 | more than your reasonable cost of physically performing this 266 | conveying of source, or (2) access to copy the 267 | Corresponding Source from a network server at no charge. 268 | 269 | c) Convey individual copies of the object code with a copy of the 270 | written offer to provide the Corresponding Source. This 271 | alternative is allowed only occasionally and noncommercially, and 272 | only if you received the object code with such an offer, in accord 273 | with subsection 6b. 274 | 275 | d) Convey the object code by offering access from a designated 276 | place (gratis or for a charge), and offer equivalent access to the 277 | Corresponding Source in the same way through the same place at no 278 | further charge. You need not require recipients to copy the 279 | Corresponding Source along with the object code. If the place to 280 | copy the object code is a network server, the Corresponding Source 281 | may be on a different server (operated by you or a third party) 282 | that supports equivalent copying facilities, provided you maintain 283 | clear directions next to the object code saying where to find the 284 | Corresponding Source. Regardless of what server hosts the 285 | Corresponding Source, you remain obligated to ensure that it is 286 | available for as long as needed to satisfy these requirements. 287 | 288 | e) Convey the object code using peer-to-peer transmission, provided 289 | you inform other peers where the object code and Corresponding 290 | Source of the work are being offered to the general public at no 291 | charge under subsection 6d. 292 | 293 | A separable portion of the object code, whose source code is excluded 294 | from the Corresponding Source as a System Library, need not be 295 | included in conveying the object code work. 296 | 297 | A "User Product" is either (1) a "consumer product", which means any 298 | tangible personal property which is normally used for personal, family, 299 | or household purposes, or (2) anything designed or sold for incorporation 300 | into a dwelling. In determining whether a product is a consumer product, 301 | doubtful cases shall be resolved in favor of coverage. For a particular 302 | product received by a particular user, "normally used" refers to a 303 | typical or common use of that class of product, regardless of the status 304 | of the particular user or of the way in which the particular user 305 | actually uses, or expects or is expected to use, the product. A product 306 | is a consumer product regardless of whether the product has substantial 307 | commercial, industrial or non-consumer uses, unless such uses represent 308 | the only significant mode of use of the product. 309 | 310 | "Installation Information" for a User Product means any methods, 311 | procedures, authorization keys, or other information required to install 312 | and execute modified versions of a covered work in that User Product from 313 | a modified version of its Corresponding Source. The information must 314 | suffice to ensure that the continued functioning of the modified object 315 | code is in no case prevented or interfered with solely because 316 | modification has been made. 317 | 318 | If you convey an object code work under this section in, or with, or 319 | specifically for use in, a User Product, and the conveying occurs as 320 | part of a transaction in which the right of possession and use of the 321 | User Product is transferred to the recipient in perpetuity or for a 322 | fixed term (regardless of how the transaction is characterized), the 323 | Corresponding Source conveyed under this section must be accompanied 324 | by the Installation Information. But this requirement does not apply 325 | if neither you nor any third party retains the ability to install 326 | modified object code on the User Product (for example, the work has 327 | been installed in ROM). 328 | 329 | The requirement to provide Installation Information does not include a 330 | requirement to continue to provide support service, warranty, or updates 331 | for a work that has been modified or installed by the recipient, or for 332 | the User Product in which it has been modified or installed. Access to a 333 | network may be denied when the modification itself materially and 334 | adversely affects the operation of the network or violates the rules and 335 | protocols for communication across the network. 336 | 337 | Corresponding Source conveyed, and Installation Information provided, 338 | in accord with this section must be in a format that is publicly 339 | documented (and with an implementation available to the public in 340 | source code form), and must require no special password or key for 341 | unpacking, reading or copying. 342 | 343 | 7. Additional Terms. 344 | 345 | "Additional permissions" are terms that supplement the terms of this 346 | License by making exceptions from one or more of its conditions. 347 | Additional permissions that are applicable to the entire Program shall 348 | be treated as though they were included in this License, to the extent 349 | that they are valid under applicable law. If additional permissions 350 | apply only to part of the Program, that part may be used separately 351 | under those permissions, but the entire Program remains governed by 352 | this License without regard to the additional permissions. 353 | 354 | When you convey a copy of a covered work, you may at your option 355 | remove any additional permissions from that copy, or from any part of 356 | it. (Additional permissions may be written to require their own 357 | removal in certain cases when you modify the work.) You may place 358 | additional permissions on material, added by you to a covered work, 359 | for which you have or can give appropriate copyright permission. 360 | 361 | Notwithstanding any other provision of this License, for material you 362 | add to a covered work, you may (if authorized by the copyright holders of 363 | that material) supplement the terms of this License with terms: 364 | 365 | a) Disclaiming warranty or limiting liability differently from the 366 | terms of sections 15 and 16 of this License; or 367 | 368 | b) Requiring preservation of specified reasonable legal notices or 369 | author attributions in that material or in the Appropriate Legal 370 | Notices displayed by works containing it; or 371 | 372 | c) Prohibiting misrepresentation of the origin of that material, or 373 | requiring that modified versions of such material be marked in 374 | reasonable ways as different from the original version; or 375 | 376 | d) Limiting the use for publicity purposes of names of licensors or 377 | authors of the material; or 378 | 379 | e) Declining to grant rights under trademark law for use of some 380 | trade names, trademarks, or service marks; or 381 | 382 | f) Requiring indemnification of licensors and authors of that 383 | material by anyone who conveys the material (or modified versions of 384 | it) with contractual assumptions of liability to the recipient, for 385 | any liability that these contractual assumptions directly impose on 386 | those licensors and authors. 387 | 388 | All other non-permissive additional terms are considered "further 389 | restrictions" within the meaning of section 10. If the Program as you 390 | received it, or any part of it, contains a notice stating that it is 391 | governed by this License along with a term that is a further 392 | restriction, you may remove that term. If a license document contains 393 | a further restriction but permits relicensing or conveying under this 394 | License, you may add to a covered work material governed by the terms 395 | of that license document, provided that the further restriction does 396 | not survive such relicensing or conveying. 397 | 398 | If you add terms to a covered work in accord with this section, you 399 | must place, in the relevant source files, a statement of the 400 | additional terms that apply to those files, or a notice indicating 401 | where to find the applicable terms. 402 | 403 | Additional terms, permissive or non-permissive, may be stated in the 404 | form of a separately written license, or stated as exceptions; 405 | the above requirements apply either way. 406 | 407 | 8. Termination. 408 | 409 | You may not propagate or modify a covered work except as expressly 410 | provided under this License. Any attempt otherwise to propagate or 411 | modify it is void, and will automatically terminate your rights under 412 | this License (including any patent licenses granted under the third 413 | paragraph of section 11). 414 | 415 | However, if you cease all violation of this License, then your 416 | license from a particular copyright holder is reinstated (a) 417 | provisionally, unless and until the copyright holder explicitly and 418 | finally terminates your license, and (b) permanently, if the copyright 419 | holder fails to notify you of the violation by some reasonable means 420 | prior to 60 days after the cessation. 421 | 422 | Moreover, your license from a particular copyright holder is 423 | reinstated permanently if the copyright holder notifies you of the 424 | violation by some reasonable means, this is the first time you have 425 | received notice of violation of this License (for any work) from that 426 | copyright holder, and you cure the violation prior to 30 days after 427 | your receipt of the notice. 428 | 429 | Termination of your rights under this section does not terminate the 430 | licenses of parties who have received copies or rights from you under 431 | this License. If your rights have been terminated and not permanently 432 | reinstated, you do not qualify to receive new licenses for the same 433 | material under section 10. 434 | 435 | 9. Acceptance Not Required for Having Copies. 436 | 437 | You are not required to accept this License in order to receive or 438 | run a copy of the Program. Ancillary propagation of a covered work 439 | occurring solely as a consequence of using peer-to-peer transmission 440 | to receive a copy likewise does not require acceptance. However, 441 | nothing other than this License grants you permission to propagate or 442 | modify any covered work. These actions infringe copyright if you do 443 | not accept this License. Therefore, by modifying or propagating a 444 | covered work, you indicate your acceptance of this License to do so. 445 | 446 | 10. Automatic Licensing of Downstream Recipients. 447 | 448 | Each time you convey a covered work, the recipient automatically 449 | receives a license from the original licensors, to run, modify and 450 | propagate that work, subject to this License. You are not responsible 451 | for enforcing compliance by third parties with this License. 452 | 453 | An "entity transaction" is a transaction transferring control of an 454 | organization, or substantially all assets of one, or subdividing an 455 | organization, or merging organizations. If propagation of a covered 456 | work results from an entity transaction, each party to that 457 | transaction who receives a copy of the work also receives whatever 458 | licenses to the work the party's predecessor in interest had or could 459 | give under the previous paragraph, plus a right to possession of the 460 | Corresponding Source of the work from the predecessor in interest, if 461 | the predecessor has it or can get it with reasonable efforts. 462 | 463 | You may not impose any further restrictions on the exercise of the 464 | rights granted or affirmed under this License. For example, you may 465 | not impose a license fee, royalty, or other charge for exercise of 466 | rights granted under this License, and you may not initiate litigation 467 | (including a cross-claim or counterclaim in a lawsuit) alleging that 468 | any patent claim is infringed by making, using, selling, offering for 469 | sale, or importing the Program or any portion of it. 470 | 471 | 11. Patents. 472 | 473 | A "contributor" is a copyright holder who authorizes use under this 474 | License of the Program or a work on which the Program is based. The 475 | work thus licensed is called the contributor's "contributor version". 476 | 477 | A contributor's "essential patent claims" are all patent claims 478 | owned or controlled by the contributor, whether already acquired or 479 | hereafter acquired, that would be infringed by some manner, permitted 480 | by this License, of making, using, or selling its contributor version, 481 | but do not include claims that would be infringed only as a 482 | consequence of further modification of the contributor version. For 483 | purposes of this definition, "control" includes the right to grant 484 | patent sublicenses in a manner consistent with the requirements of 485 | this License. 486 | 487 | Each contributor grants you a non-exclusive, worldwide, royalty-free 488 | patent license under the contributor's essential patent claims, to 489 | make, use, sell, offer for sale, import and otherwise run, modify and 490 | propagate the contents of its contributor version. 491 | 492 | In the following three paragraphs, a "patent license" is any express 493 | agreement or commitment, however denominated, not to enforce a patent 494 | (such as an express permission to practice a patent or covenant not to 495 | sue for patent infringement). To "grant" such a patent license to a 496 | party means to make such an agreement or commitment not to enforce a 497 | patent against the party. 498 | 499 | If you convey a covered work, knowingly relying on a patent license, 500 | and the Corresponding Source of the work is not available for anyone 501 | to copy, free of charge and under the terms of this License, through a 502 | publicly available network server or other readily accessible means, 503 | then you must either (1) cause the Corresponding Source to be so 504 | available, or (2) arrange to deprive yourself of the benefit of the 505 | patent license for this particular work, or (3) arrange, in a manner 506 | consistent with the requirements of this License, to extend the patent 507 | license to downstream recipients. "Knowingly relying" means you have 508 | actual knowledge that, but for the patent license, your conveying the 509 | covered work in a country, or your recipient's use of the covered work 510 | in a country, would infringe one or more identifiable patents in that 511 | country that you have reason to believe are valid. 512 | 513 | If, pursuant to or in connection with a single transaction or 514 | arrangement, you convey, or propagate by procuring conveyance of, a 515 | covered work, and grant a patent license to some of the parties 516 | receiving the covered work authorizing them to use, propagate, modify 517 | or convey a specific copy of the covered work, then the patent license 518 | you grant is automatically extended to all recipients of the covered 519 | work and works based on it. 520 | 521 | A patent license is "discriminatory" if it does not include within 522 | the scope of its coverage, prohibits the exercise of, or is 523 | conditioned on the non-exercise of one or more of the rights that are 524 | specifically granted under this License. You may not convey a covered 525 | work if you are a party to an arrangement with a third party that is 526 | in the business of distributing software, under which you make payment 527 | to the third party based on the extent of your activity of conveying 528 | the work, and under which the third party grants, to any of the 529 | parties who would receive the covered work from you, a discriminatory 530 | patent license (a) in connection with copies of the covered work 531 | conveyed by you (or copies made from those copies), or (b) primarily 532 | for and in connection with specific products or compilations that 533 | contain the covered work, unless you entered into that arrangement, 534 | or that patent license was granted, prior to 28 March 2007. 535 | 536 | Nothing in this License shall be construed as excluding or limiting 537 | any implied license or other defenses to infringement that may 538 | otherwise be available to you under applicable patent law. 539 | 540 | 12. No Surrender of Others' Freedom. 541 | 542 | If conditions are imposed on you (whether by court order, agreement or 543 | otherwise) that contradict the conditions of this License, they do not 544 | excuse you from the conditions of this License. If you cannot convey a 545 | covered work so as to satisfy simultaneously your obligations under this 546 | License and any other pertinent obligations, then as a consequence you may 547 | not convey it at all. For example, if you agree to terms that obligate you 548 | to collect a royalty for further conveying from those to whom you convey 549 | the Program, the only way you could satisfy both those terms and this 550 | License would be to refrain entirely from conveying the Program. 551 | 552 | 13. Use with the GNU Affero General Public License. 553 | 554 | Notwithstanding any other provision of this License, you have 555 | permission to link or combine any covered work with a work licensed 556 | under version 3 of the GNU Affero General Public License into a single 557 | combined work, and to convey the resulting work. The terms of this 558 | License will continue to apply to the part which is the covered work, 559 | but the special requirements of the GNU Affero General Public License, 560 | section 13, concerning interaction through a network will apply to the 561 | combination as such. 562 | 563 | 14. Revised Versions of this License. 564 | 565 | The Free Software Foundation may publish revised and/or new versions of 566 | the GNU General Public License from time to time. Such new versions will 567 | be similar in spirit to the present version, but may differ in detail to 568 | address new problems or concerns. 569 | 570 | Each version is given a distinguishing version number. If the 571 | Program specifies that a certain numbered version of the GNU General 572 | Public License "or any later version" applies to it, you have the 573 | option of following the terms and conditions either of that numbered 574 | version or of any later version published by the Free Software 575 | Foundation. If the Program does not specify a version number of the 576 | GNU General Public License, you may choose any version ever published 577 | by the Free Software Foundation. 578 | 579 | If the Program specifies that a proxy can decide which future 580 | versions of the GNU General Public License can be used, that proxy's 581 | public statement of acceptance of a version permanently authorizes you 582 | to choose that version for the Program. 583 | 584 | Later license versions may give you additional or different 585 | permissions. However, no additional obligations are imposed on any 586 | author or copyright holder as a result of your choosing to follow a 587 | later version. 588 | 589 | 15. Disclaimer of Warranty. 590 | 591 | THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY 592 | APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT 593 | HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY 594 | OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, 595 | THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR 596 | PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM 597 | IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF 598 | ALL NECESSARY SERVICING, REPAIR OR CORRECTION. 599 | 600 | 16. Limitation of Liability. 601 | 602 | IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING 603 | WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS 604 | THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY 605 | GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE 606 | USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF 607 | DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD 608 | PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), 609 | EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF 610 | SUCH DAMAGES. 611 | 612 | 17. Interpretation of Sections 15 and 16. 613 | 614 | If the disclaimer of warranty and limitation of liability provided 615 | above cannot be given local legal effect according to their terms, 616 | reviewing courts shall apply local law that most closely approximates 617 | an absolute waiver of all civil liability in connection with the 618 | Program, unless a warranty or assumption of liability accompanies a 619 | copy of the Program in return for a fee. 620 | 621 | END OF TERMS AND CONDITIONS 622 | 623 | How to Apply These Terms to Your New Programs 624 | 625 | If you develop a new program, and you want it to be of the greatest 626 | possible use to the public, the best way to achieve this is to make it 627 | free software which everyone can redistribute and change under these terms. 628 | 629 | To do so, attach the following notices to the program. It is safest 630 | to attach them to the start of each source file to most effectively 631 | state the exclusion of warranty; and each file should have at least 632 | the "copyright" line and a pointer to where the full notice is found. 633 | 634 | 635 | Copyright (C) 636 | 637 | This program is free software: you can redistribute it and/or modify 638 | it under the terms of the GNU General Public License as published by 639 | the Free Software Foundation, either version 3 of the License, or 640 | (at your option) any later version. 641 | 642 | This program is distributed in the hope that it will be useful, 643 | but WITHOUT ANY WARRANTY; without even the implied warranty of 644 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 645 | GNU General Public License for more details. 646 | 647 | You should have received a copy of the GNU General Public License 648 | along with this program. If not, see . 649 | 650 | Also add information on how to contact you by electronic and paper mail. 651 | 652 | If the program does terminal interaction, make it output a short 653 | notice like this when it starts in an interactive mode: 654 | 655 | Copyright (C) 656 | This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. 657 | This is free software, and you are welcome to redistribute it 658 | under certain conditions; type `show c' for details. 659 | 660 | The hypothetical commands `show w' and `show c' should show the appropriate 661 | parts of the General Public License. Of course, your program's commands 662 | might be different; for a GUI interface, you would use an "about box". 663 | 664 | You should also get your employer (if you work as a programmer) or school, 665 | if any, to sign a "copyright disclaimer" for the program, if necessary. 666 | For more information on this, and how to apply and follow the GNU GPL, see 667 | . 668 | 669 | The GNU General Public License does not permit incorporating your program 670 | into proprietary programs. If your program is a subroutine library, you 671 | may consider it more useful to permit linking proprietary applications with 672 | the library. If this is what you want to do, use the GNU Lesser General 673 | Public License instead of this License. But first, please read 674 | . 675 | --------------------------------------------------------------------------------