1mod commands;
26pub mod killaura;
27pub mod mspt;
28
29use std::{env, process, sync::Arc, thread, time::Duration};
30
31use azalea::{
32 ClientInformation, EntityRef,
33 brigadier::command_dispatcher::CommandDispatcher,
34 ecs::prelude::*,
35 pathfinder::{
36 PathfinderOpts,
37 debug::PathfinderDebugParticles,
38 execute::simulation::SimulationPathfinderExecutionPlugin,
39 goals::{Goal, RadiusGoal},
40 },
41 prelude::*,
42 swarm::prelude::*,
43};
44use commands::{CommandSource, register_commands};
45use parking_lot::Mutex;
46
47#[tokio::main]
48async fn main() -> AppExit {
49 let args = parse_args();
50
51 thread::spawn(deadlock_detection_thread);
52
53 let join_address = args.server.clone();
54
55 let mut builder = SwarmBuilder::new()
56 .set_handler(handle)
57 .set_swarm_handler(swarm_handle);
58
59 if args.simulation_pathfinder {
60 builder = builder.add_plugins(SimulationPathfinderExecutionPlugin);
61 }
62
63 for username_or_email in &args.accounts {
64 let account = if username_or_email.contains('@') {
65 Account::microsoft(username_or_email).await.unwrap()
66 } else {
67 Account::offline(username_or_email)
68 };
69
70 builder = builder.add_account_with_state(account, State::new());
71 }
72
73 let mut commands = CommandDispatcher::new();
74 register_commands(&mut commands);
75
76 builder
77 .join_delay(Duration::from_millis(100))
78 .set_swarm_state(SwarmState {
79 args: args.into(),
80 commands: commands.into(),
81 })
82 .start(join_address)
84 .await
85}
86
87fn deadlock_detection_thread() {
93 loop {
94 thread::sleep(Duration::from_secs(10));
95 let deadlocks = parking_lot::deadlock::check_deadlock();
96 if deadlocks.is_empty() {
97 continue;
98 }
99
100 println!("{} deadlocks detected", deadlocks.len());
101 for (i, threads) in deadlocks.iter().enumerate() {
102 println!("Deadlock #{i}");
103 for t in threads {
104 println!("Thread Id {:#?}", t.thread_id());
105 println!("{:#?}", t.backtrace());
106 }
107 }
108 }
109}
110
111#[derive(Clone, Component, Default)]
112pub struct State {
113 pub killaura: bool,
114 pub following_entity: Arc<Mutex<Option<EntityRef>>>,
115}
116
117impl State {
118 fn new() -> Self {
119 Self {
120 killaura: false,
121 following_entity: Default::default(),
122 }
123 }
124}
125
126#[derive(Clone, Default, Resource)]
127struct SwarmState {
128 pub args: Arc<Args>,
129 pub commands: Arc<commands::Dispatcher>,
130}
131
132async fn handle(bot: Client, event: azalea::Event, state: State) -> eyre::Result<()> {
133 let swarm = bot.resource::<SwarmState>();
134
135 match event {
136 azalea::Event::Init => {
137 bot.set_client_information(ClientInformation {
138 view_distance: 32,
139 ..Default::default()
140 })?;
141 if swarm.args.pathfinder_debug_particles {
142 bot.ecs
143 .write()
144 .entity_mut(bot.entity)
145 .insert(PathfinderDebugParticles);
146 }
147 }
148 azalea::Event::Chat(chat) => {
149 let (Some(username), content) = chat.split_sender_and_content() else {
150 return Ok(());
151 };
152 if username != swarm.args.owner_username {
153 return Ok(());
154 }
155
156 println!("{:?}", chat.message());
157
158 let command = if chat.is_whisper() {
159 Some(content)
160 } else {
161 content.strip_prefix('!').map(|s| s.to_owned())
162 };
163 if let Some(command) = command {
164 match swarm.commands.execute(
165 command,
166 Mutex::new(CommandSource {
167 bot: bot.clone(),
168 chat: chat.clone(),
169 state: state.clone(),
170 }),
171 ) {
172 Ok(Ok(_)) => {}
173 Ok(Err(err)) => {
174 eprintln!("azalea error: {err:?}");
175 let command_source = CommandSource {
176 bot,
177 chat: chat.clone(),
178 state: state.clone(),
179 };
180 command_source.reply(format!("azalea error: {err:?}"));
181 }
182 Err(err) => {
183 eprintln!("{err:?}");
184 let command_source = CommandSource {
185 bot,
186 chat: chat.clone(),
187 state: state.clone(),
188 };
189 command_source.reply(format!("{err:?}"));
190 }
191 }
192 }
193 }
194 azalea::Event::Tick => {
195 killaura::tick(bot.clone(), state.clone())?;
196
197 if bot.ticks_connected().is_multiple_of(5) {
198 if let Some(following) = &*state.following_entity.lock()
199 && following.is_alive()
200 {
201 let goal = RadiusGoal::new(following.position()?, 3.);
202 if bot.is_calculating_path() {
203 } else if !goal.success(bot.position()?.into()) || bot.is_executing_path() {
205 bot.start_goto_with_opts(
206 goal,
207 PathfinderOpts::new()
208 .retry_on_no_path(false)
209 .max_timeout(Duration::from_secs(1)),
210 );
211 } else {
212 following.look_at()?;
213 }
214 }
215 }
216 }
217 azalea::Event::Login => {
218 println!("Got login event")
219 }
220 _ => {}
221 }
222
223 Ok(())
224}
225async fn swarm_handle(_swarm: Swarm, event: SwarmEvent, _state: SwarmState) -> eyre::Result<()> {
226 match &event {
227 SwarmEvent::Disconnect(account, _join_opts) => {
228 println!("bot got kicked! {}", account.username());
229 }
230 SwarmEvent::Chat(chat) => {
231 if chat.message().to_string() == "The particle was not visible for anybody" {
232 return Ok(());
233 }
234 println!("{}", chat.message().to_ansi());
235 }
236 _ => {}
237 }
238
239 Ok(())
240}
241
242#[derive(Clone, Debug, Default)]
243pub struct Args {
244 pub owner_username: String,
245 pub accounts: Vec<String>,
246 pub server: String,
247 pub pathfinder_debug_particles: bool,
248 pub simulation_pathfinder: bool,
249}
250
251fn parse_args() -> Args {
252 let mut owner_username = "admin".to_owned();
253 let mut accounts = Vec::new();
254 let mut server = "localhost".to_owned();
255 let mut pathfinder_debug_particles = false;
256 let mut simulation_pathfinder = false;
257
258 let mut args = env::args().skip(1);
259 while let Some(arg) = args.next() {
260 match arg.as_str() {
261 "--owner" | "-O" => {
262 owner_username = args.next().expect("Missing owner username");
263 }
264 "--account" | "-A" => {
265 for account in args.next().expect("Missing account").split(',') {
266 accounts.push(account.to_string());
267 }
268 }
269 "--server" | "-S" => {
270 server = args.next().expect("Missing server address");
271 }
272 "--pathfinder-debug-particles" | "-P" => {
273 pathfinder_debug_particles = true;
274 }
275 "--simulation-pathfinder" => {
276 simulation_pathfinder = true;
277 }
278 _ => {
279 eprintln!("Unknown argument: {arg}");
280 process::exit(1);
281 }
282 }
283 }
284
285 if accounts.is_empty() {
286 accounts.push("azalea".to_owned());
287 }
288
289 Args {
290 owner_username,
291 accounts,
292 server,
293 pathfinder_debug_particles,
294 simulation_pathfinder,
295 }
296}