minect/
lib.rs

1// Minect is library that allows a program to connect to a running Minecraft instance without
2// requiring any Minecraft mods.
3//
4// © Copyright (C) 2021-2023 Adrodoc <adrodoc55@googlemail.com> & skess42 <skagaros@gmail.com>
5//
6// This file is part of Minect.
7//
8// Minect is free software: you can redistribute it and/or modify it under the terms of the GNU
9// General Public License as published by the Free Software Foundation, either version 3 of the
10// License, or (at your option) any later version.
11//
12// Minect is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even
13// the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
14// Public License for more details.
15//
16// You should have received a copy of the GNU General Public License along with Minect.
17// If not, see <http://www.gnu.org/licenses/>.
18
19//! Minect is a library that allows a Rust program to connect to a running Minecraft instance
20//! without requiring any Minecraft mods.
21//!
22//! Using Minect a Rust program can execute commands in Minecraft and listen for command output. This
23//! way a Rust program can control or be controlled by Minecraft.
24//!
25//! The connection requires a building in Minecraft which continuously loads structure files that
26//! contain commands generated by the Rust program. Listening for their output works by polling
27//! Minecraft's log file.
28//!
29//! ## Example
30//!
31//! ```no_run
32//! # use minect::*;
33//! # use minect::command::*;
34//! # use tokio_stream::StreamExt;
35//! # let _ = async {
36//! let identifier = "MyProgram";
37//! let world_dir = "C:/Users/Herobrine/AppData/Roaming/.minecraft/saves/New World";
38//! let mut connection = MinecraftConnection::builder(identifier, world_dir).build();
39//!
40//! println!("If you are connecting for the first time please execute /reload in Minecraft.");
41//! connection.connect().await?;
42//!
43//! let events = connection.add_listener();
44//!
45//! connection.execute_commands([
46//!   Command::new("scoreboard objectives add example dummy"),
47//!   Command::new("scoreboard players set Herobrine example 42"),
48//!   Command::new(query_scoreboard_command("Herobrine", "example")),
49//! ])?;
50//!
51//! let output = events
52//!   .filter_map(|event| event.output.parse::<QueryScoreboardOutput>().ok())
53//!   .next()
54//!   .await
55//!   .expect("Minecraft connection was closed unexpectedly");
56//!
57//! println!("{}'s score is {}", output.entity, output.score);
58//! # Ok::<(), std::io::Error>(())
59//! # };
60//! ```
61
62#[macro_use]
63mod macros;
64
65pub mod command;
66mod connect;
67mod geometry3;
68mod io;
69mod json;
70pub mod log;
71mod on_drop;
72mod placement;
73mod structure;
74mod utils;
75
76pub use crate::connect::ConnectError;
77
78use crate::{
79    command::{
80        enable_logging_command, reset_logging_command, summon_named_entity_command,
81        SummonNamedEntityOutput,
82    },
83    connect::connect,
84    io::{
85        create, create_dir_all, io_error, remove_dir_all, remove_file, rename, write, IoErrorAtPath,
86    },
87    log::{LogEvent, LogObserver},
88    placement::generate_structure,
89    structure::nbt::Structure,
90    utils::io_invalid_data,
91};
92use ::log::error;
93use fs3::FileExt;
94use indexmap::IndexSet;
95use json::create_json_text_component;
96use std::{
97    fmt::Display,
98    fs::{File, OpenOptions},
99    io::{BufWriter, Read, Seek, SeekFrom, Write},
100    path::{Path, PathBuf},
101};
102use tokio_stream::Stream;
103
104/// A builder to create a [MinecraftConnection] is obtained via [MinecraftConnection::builder].
105///
106/// The builder pattern is used to add new parameters without breaking backwards compatibility.
107pub struct MinecraftConnectionBuilder {
108    identifier: String,
109    world_dir: PathBuf,
110    log_file: Option<PathBuf>,
111    enable_logging_automatically: bool,
112}
113
114impl MinecraftConnectionBuilder {
115    fn new(
116        identifier: impl Into<String>,
117        world_dir: impl Into<PathBuf>,
118    ) -> MinecraftConnectionBuilder {
119        let identifier = identifier.into();
120        validate_identifier(&identifier);
121        MinecraftConnectionBuilder {
122            identifier,
123            world_dir: world_dir.into(),
124            log_file: None,
125            enable_logging_automatically: true,
126        }
127    }
128
129    /// The path to Minecraft's log file.
130    ///
131    /// For single player this is typically at these locations:
132    /// * Windows: `C:\Users\Herobrine\AppData\Roaming\.minecraft\logs\latest.log`
133    /// * GNU/Linux: `~/.minecraft/logs/latest.log`
134    /// * Mac: `~/Library/Application Support/minecraft/logs/latest.log`
135    ///
136    /// For servers it is at `logs/latest.log` in the server directory.
137    ///
138    /// Defaults to `../../logs/latest.log` relative to `world_dir`, which is the correct value for
139    /// single player, but usually not for servers.
140    pub fn log_file(mut self, log_file: impl Into<PathBuf>) -> MinecraftConnectionBuilder {
141        self.log_file = Some(log_file.into());
142        self
143    }
144
145    /// Whether logging is automatically enabled for all commands passed to
146    /// [MinecraftConnection::execute_commands]. This works by prepending an
147    /// [enable_logging_command] and appending a [reset_logging_command] to the list of commands.
148    ///
149    /// This setting has no effect for [logged_cart_command](command::logged_cart_command)s. Logging
150    /// still has to be enabled for these manually.
151    ///
152    /// Default: `true`.
153    pub fn enable_logging_automatically(
154        mut self,
155        enable_logging_automatically: impl Into<bool>,
156    ) -> MinecraftConnectionBuilder {
157        self.enable_logging_automatically = enable_logging_automatically.into();
158        self
159    }
160
161    /// Creates a [MinecraftConnection] with the configured parameters.
162    ///
163    /// # Panics
164    ///
165    /// Panics if no [log_file](Self::log_file()) was specified and the
166    /// [world_dir](MinecraftConnection::builder) has less than 2 path compontents. In this case the
167    /// default value of `../../logs/latest.log` can not be resolved.
168    pub fn build(self) -> MinecraftConnection {
169        let world_dir = self.world_dir;
170        let log_file = self
171            .log_file
172            .unwrap_or_else(|| log_file_from_world_dir(&world_dir));
173        MinecraftConnection::new(
174            self.identifier,
175            world_dir,
176            log_file,
177            self.enable_logging_automatically,
178        )
179    }
180}
181
182fn validate_identifier(identifier: &str) {
183    let invalid_chars = identifier
184        .chars()
185        .filter(|c| !is_allowed_in_identifier(*c))
186        .collect::<IndexSet<_>>();
187    if !invalid_chars.is_empty() {
188        panic!(
189            "Invalid characters in MinecraftConnection.identifier: '{}'",
190            invalid_chars
191                .iter()
192                .fold(String::new(), |joined, c| joined + &c.to_string())
193        );
194    }
195}
196fn is_allowed_in_identifier(c: char) -> bool {
197    return c >= '0' && c <= '9'
198        || c >= 'A' && c <= 'Z'
199        || c >= 'a' && c <= 'z'
200        || c == '+'
201        || c == '-'
202        || c == '.'
203        || c == '_';
204}
205
206fn log_file_from_world_dir(world_dir: &PathBuf) -> PathBuf {
207    let panic_invalid_dir = || {
208        panic!(
209            "Expected world_dir to be in .minecraft/saves, but was: {}",
210            world_dir.display()
211        )
212    };
213    let minecraft_dir = world_dir
214        .parent()
215        .unwrap_or_else(panic_invalid_dir)
216        .parent()
217        .unwrap_or_else(panic_invalid_dir);
218    minecraft_dir.join("logs/latest.log")
219}
220
221macro_rules! extract_datapack_file {
222    ($output_path:expr, $relative_path:expr) => {{
223        let path = $output_path.join($relative_path);
224        let contents = include_datapack_template!($relative_path);
225        write(&path, &contents)
226    }};
227}
228
229/// A connection to Minecraft that can [execute commands](MinecraftConnection::execute_commands) in
230/// Minecraft and [listen for command output](MinecraftConnection::add_listener).
231///
232/// If you need to listen for command output, but don't need to execute commands, consider using
233/// [LogObserver] directly.
234///
235/// The connection requires a building in Minecraft which continuously loads structure files that
236/// contain the commands passed to [execute_commands](MinecraftConnection::execute_commands). To
237/// create such a connection building you can call [connect](MinecraftConnection::connect).
238///
239/// Minect supports operating multiple connection buildings in parallel, each with a unique
240/// identifier. A single connection building can be shared between any number of Rust programs, but
241/// it has a limited update frequency. For optimal performance every Rust program can use a
242/// different connection identifier.
243///
244/// The update frequency can be configured globally for all connections in a Minecraft world by
245/// changing the score of `update_delay` for the objective `minect_config`.
246pub struct MinecraftConnection {
247    identifier: String,
248    structures_dir: PathBuf,
249    datapack_dir: PathBuf,
250    log_file: PathBuf,
251    log_observer: Option<LogObserver>,
252    loaded_listener_initialized: bool,
253    enable_logging_automatically: bool,
254    _private: (),
255}
256
257const NAMESPACE: &str = "minect";
258
259impl MinecraftConnection {
260    /// Creates a [MinecraftConnectionBuilder].
261    ///
262    /// `identifier` is a string that uniquely identifies a connection building in Minecraft.
263    /// Because it is used with Minecraft's `tag` command, it may only contain the following
264    /// Characters: `0-9`, `A-Z`, `a-z`, `+`, `-`, `.` & `_`.
265    ///
266    /// `world_dir` is the directory containing the Minecraft world to connect to.
267    /// For single player this is typically a directory within the saves directory:
268    /// * Windows: `C:\Users\Herobrine\AppData\Roaming\.minecraft\saves\`
269    /// * GNU/Linux: `~/.minecraft/saves/`
270    /// * Mac: `~/Library/Application Support/minecraft/saves/`
271    ///
272    /// For servers it is specified in `server.properties`.
273    ///
274    /// # Panics
275    ///
276    /// Panics if `identifier` contains an invalid character.
277    pub fn builder(
278        identifier: impl Into<String>,
279        world_dir: impl Into<PathBuf>,
280    ) -> MinecraftConnectionBuilder {
281        MinecraftConnectionBuilder::new(identifier, world_dir)
282    }
283
284    fn new(
285        identifier: String,
286        world_dir: PathBuf,
287        log_file: PathBuf,
288        enable_logging_automatically: bool,
289    ) -> MinecraftConnection {
290        MinecraftConnection {
291            structures_dir: world_dir
292                .join("generated")
293                .join(NAMESPACE)
294                .join("structures")
295                .join(&identifier),
296            datapack_dir: world_dir.join("datapacks").join(NAMESPACE),
297            identifier,
298            log_file,
299            log_observer: None,
300            loaded_listener_initialized: false,
301            enable_logging_automatically,
302            _private: (),
303        }
304    }
305
306    /// The connection identifier uniquely identifies a connection building in Minecraft.
307    pub fn get_identifier(&self) -> &str {
308        &self.identifier
309    }
310
311    /// The root directory of the datapack used to operate the connection in Minecraft.
312    pub fn get_datapack_dir(&self) -> &Path {
313        &self.datapack_dir
314    }
315
316    /// This function can be used to set up the connection building in Minecraft, which is required
317    /// for [execute_commands](Self::execute_commands).
318    ///
319    /// If the connection can be established, this function simply returns. Note that this still
320    /// requires a running Minecraft instance. Otherwise this function blocks until a connection
321    /// building is created.
322    /// This function also creates an interactive installer that a player can start by executing
323    /// `/reload` in Minecraft.
324    ///
325    /// Because this function blocks indefinately if the connection can't be established, it should
326    /// be called with [tokio::time::timeout] or some other means of cancellation, such as
327    /// [futures::future::select].
328    ///
329    /// # Errors
330    ///
331    /// This function will return an error if the player cancels the installation in the interactive
332    /// installer (can be checked with [ConnectError::is_cancelled]) or if an
333    /// [io::Error](std::io::Error) occurs.
334    pub async fn connect(&mut self) -> Result<(), ConnectError> {
335        connect(self).await
336    }
337
338    /// Creates the [Minect datapack](Self::get_datapack_dir()).
339    pub fn create_datapack(&self) -> Result<(), IoErrorAtPath> {
340        macro_rules! extract {
341            ($relative_path:expr) => {
342                extract_datapack_file!(self.datapack_dir, $relative_path)
343            };
344        }
345
346        extract!("data/minecraft/tags/functions/load.json")?;
347        extract!("data/minecraft/tags/functions/tick.json")?;
348        extract!("data/minect_internal/functions/clean_up.mcfunction")?;
349        extract!("data/minect_internal/functions/connect/align_to_chunk.mcfunction")?;
350        extract!("data/minect_internal/functions/connect/remove_connector.mcfunction")?;
351        extract!("data/minect_internal/functions/cursor/clean_up.mcfunction")?;
352        extract!("data/minect_internal/functions/cursor/initialize.mcfunction")?;
353        extract!("data/minect_internal/functions/cursor/move_and_place_ahead.mcfunction")?;
354        extract!("data/minect_internal/functions/cursor/move.mcfunction")?;
355        extract!("data/minect_internal/functions/cursor/place_ahead.mcfunction")?;
356        extract!("data/minect_internal/functions/cursor/place.mcfunction")?;
357        extract!("data/minect_internal/functions/cursor/try_place_facing_east.mcfunction")?;
358        extract!("data/minect_internal/functions/cursor/try_place_facing_north.mcfunction")?;
359        extract!("data/minect_internal/functions/cursor/try_place_facing_south.mcfunction")?;
360        extract!("data/minect_internal/functions/cursor/try_place_facing_west.mcfunction")?;
361        extract!("data/minect_internal/functions/cursor/try_place_facing_z.mcfunction")?;
362        extract!("data/minect_internal/functions/enable_logging_initially.mcfunction")?;
363        extract!("data/minect_internal/functions/load.mcfunction")?;
364        extract!("data/minect_internal/functions/pulse_redstone.mcfunction")?;
365        extract!("data/minect_internal/functions/reload.mcfunction")?;
366        extract!("data/minect_internal/functions/reset_logging_finally.mcfunction")?;
367        extract!("data/minect_internal/functions/tick.mcfunction")?;
368        extract!("data/minect_internal/functions/update.mcfunction")?;
369        extract!("data/minect_internal/functions/v1_uninstall.mcfunction")?;
370        extract!("data/minect_internal/functions/v2_migrate.mcfunction")?;
371        extract!("data/minect_internal/functions/v2_uninstall.mcfunction")?;
372        extract!("data/minect_internal/functions/v3_install.mcfunction")?;
373        extract!("data/minect_internal/functions/v3_uninstall.mcfunction")?;
374        extract!("data/minect_internal/tags/blocks/command_blocks.json")?;
375        extract!("data/minect/functions/connect/choose_chunk.mcfunction")?;
376        extract!("data/minect/functions/disconnect_self.mcfunction")?;
377        extract!("data/minect/functions/disconnect.mcfunction")?;
378        extract!("data/minect/functions/enable_logging.mcfunction")?;
379        extract!("data/minect/functions/prepare_logged_block.mcfunction")?;
380        extract!("data/minect/functions/reset_logging.mcfunction")?;
381        extract!("data/minect/functions/uninstall_completely.mcfunction")?;
382        extract!("data/minect/functions/uninstall.mcfunction")?;
383        extract!("pack.mcmeta")?;
384        Ok(())
385    }
386
387    /// Removes the [Minect datapack](Self::get_datapack_dir()).
388    pub fn remove_datapack(&self) -> Result<(), IoErrorAtPath> {
389        remove_dir_all(&self.datapack_dir)
390    }
391
392    /// Executes the given `commands` in Minecraft.
393    ///
394    /// # Errors
395    ///
396    /// This function will return an error if an [io::Error](std::io::Error) occurs.
397    pub fn execute_commands(
398        &mut self,
399        commands: impl IntoIterator<IntoIter = impl ExactSizeIterator<Item = Command>>,
400    ) -> Result<(), ExecuteCommandsError> {
401        if !self.datapack_dir.is_dir() {
402            self.create_datapack()?;
403        }
404        if !self.loaded_listener_initialized {
405            self.init_loaded_listener();
406        }
407        create_dir_all(&self.structures_dir)?;
408
409        let id_path = self.structures_dir.join("id.txt");
410        let mut id_file = lock_file(&id_path)?; // Automatically unlocked by dropping id_file at the end of this function.
411
412        let id = read_incremented_id(&mut id_file, &id_path)?;
413        let next_id = id.wrapping_add(1);
414
415        let (commands, commands_len) = add_implicit_commands(
416            commands,
417            &self.identifier,
418            id,
419            self.enable_logging_automatically,
420        );
421        let structure = generate_structure(&self.identifier, next_id, commands, commands_len);
422
423        // To create the structure file as atomically as possible we first write to a temporary file
424        // and then rename it, which is an atomic operation on most operating systems. If Minecraft
425        // would attempt to load a half written file, it would likely cache the file as invalid
426        // (depending on what bytes it sees). Locking the file also causes Minecraft to cache it as
427        // invalid.
428        let tmp_path = self.get_structure_file("tmp");
429        create_structure_file(&tmp_path, structure)?;
430        rename(tmp_path, self.get_structure_file(id))?;
431
432        // We do this at the end to not increment the id on a failure, which would break the connection.
433        write_id(&mut id_file, id_path, id)?;
434
435        Ok(())
436    }
437
438    fn get_structure_file(&self, id: impl Display) -> PathBuf {
439        self.structures_dir.join(format!("{}.nbt", id))
440    }
441
442    /// Returns a [Stream] of all [LogEvent]s. To remove the listener simply drop the stream.
443    ///
444    /// Internally the stream is backed by an unbound channel. This means it should be polled
445    /// regularly to avoid memory leaks.
446    pub fn add_listener(&mut self) -> impl Stream<Item = LogEvent> {
447        self.get_log_observer().add_listener()
448    }
449
450    /// Returns a [Stream] of [LogEvent]s with [executor](LogEvent::executor) equal to the given
451    /// `name`. To remove the listener simply drop the stream.
452    ///
453    /// This can be more memory efficient than [add_listener](Self::add_listener), because only a
454    /// small subset of [LogEvent]s has to be buffered if not that many commands are executed with
455    /// the given `name`.
456    ///
457    /// Internally the stream is backed by an unbound channel. This means it should be polled
458    /// regularly to avoid memory leaks.
459    pub fn add_named_listener(&mut self, name: impl Into<String>) -> impl Stream<Item = LogEvent> {
460        self.get_log_observer().add_named_listener(name)
461    }
462
463    fn init_loaded_listener(&mut self) {
464        let structures_dir = self.structures_dir.clone();
465        let listener = LoadedListener { structures_dir };
466        self.get_log_observer().add_loaded_listener(listener);
467        self.loaded_listener_initialized = true;
468    }
469
470    fn get_log_observer(&mut self) -> &mut LogObserver {
471        if self.log_observer.is_none() {
472            // Start LogObserver only when needed
473            self.log_observer = Some(LogObserver::new(&self.log_file));
474        }
475        self.log_observer.as_mut().unwrap() // Unwrap is safe because we just assigned the value
476    }
477}
478
479fn lock_file(path: impl AsRef<Path>) -> Result<File, IoErrorAtPath> {
480    let file = OpenOptions::new()
481        .create(true)
482        .read(true)
483        .write(true)
484        .open(&path)
485        .map_err(io_error("Failed to open file", path.as_ref()))?;
486    file.lock_exclusive()
487        .map_err(io_error("Failed to lock file", path.as_ref()))?;
488    Ok(file)
489}
490
491fn read_incremented_id(file: &mut File, path: impl AsRef<Path>) -> Result<u64, IoErrorAtPath> {
492    let mut content = String::new();
493    file.read_to_string(&mut content)
494        .map_err(io_error("Failed to read file", path.as_ref()))?;
495    let id = if content.is_empty() {
496        0
497    } else {
498        content
499            .parse::<u64>()
500            .map_err(io_invalid_data)
501            .map_err(io_error(
502                "Failed to parse content as u64 of file",
503                path.as_ref(),
504            ))?
505            .wrapping_add(1)
506    };
507    Ok(id)
508}
509
510fn write_id(file: &mut File, path: impl AsRef<Path>, id: u64) -> Result<(), IoErrorAtPath> {
511    file.set_len(0)
512        .map_err(io_error("Failed to truncate file", path.as_ref()))?;
513    file.seek(SeekFrom::Start(0))
514        .map_err(io_error("Failed to seek beginning of file", path.as_ref()))?;
515    file.write_all(id.to_string().as_bytes())
516        .map_err(io_error("Failed to write to file", path.as_ref()))?;
517    Ok(())
518}
519
520fn create_structure_file(
521    path: impl AsRef<Path>,
522    structure: Structure,
523) -> Result<(), IoErrorAtPath> {
524    let file = create(path)?;
525    let mut writer = BufWriter::new(file);
526    nbt::to_gzip_writer(&mut writer, &structure, None).unwrap();
527    Ok(())
528}
529
530/// The error returned from [MinecraftConnection::execute_commands].
531#[derive(Debug)]
532pub struct ExecuteCommandsError {
533    inner: ExecuteCommandsErrorInner,
534}
535#[derive(Debug)]
536enum ExecuteCommandsErrorInner {
537    Io(IoErrorAtPath),
538    // TODO: Add error for executing too many commands instead of ignoring them
539}
540impl ExecuteCommandsError {
541    fn new(inner: ExecuteCommandsErrorInner) -> ExecuteCommandsError {
542        ExecuteCommandsError { inner }
543    }
544}
545impl From<IoErrorAtPath> for ExecuteCommandsError {
546    fn from(value: IoErrorAtPath) -> ExecuteCommandsError {
547        ExecuteCommandsError::new(ExecuteCommandsErrorInner::Io(value))
548    }
549}
550impl Display for ExecuteCommandsError {
551    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
552        match &self.inner {
553            ExecuteCommandsErrorInner::Io(error) => error.fmt(f),
554        }
555    }
556}
557impl std::error::Error for ExecuteCommandsError {}
558impl From<ExecuteCommandsError> for std::io::Error {
559    fn from(value: ExecuteCommandsError) -> std::io::Error {
560        match value.inner {
561            ExecuteCommandsErrorInner::Io(error) => std::io::Error::from(error),
562        }
563    }
564}
565
566/// A [Command] can be passed to [MinecraftConnection::execute_commands] and contains a Minecraft
567/// command to execute and optionally a custom name.
568///
569/// The custom name can be useful in conjunction with [MinecraftConnection::add_named_listener] to
570/// easily and performantly filter for the correct [LogEvent].
571pub struct Command {
572    name: Option<String>,
573    command: String,
574}
575impl Command {
576    /// Creates a [Command] without custom name. These commands are typically executed under the
577    /// name `@`.
578    pub fn new(command: impl Into<String>) -> Command {
579        Command {
580            name: None,
581            command: command.into(),
582        }
583    }
584
585    /// Creates a [Command] with the given custom `name`.
586    pub fn named(name: impl Into<String>, command: impl Into<String>) -> Command {
587        Command {
588            name: Some(name.into()),
589            command: command.into(),
590        }
591    }
592
593    /// The optional custom name.
594    pub fn get_name(&self) -> Option<&str> {
595        self.name.as_ref().map(|it| it.as_str())
596    }
597
598    /// The Minecraft command.
599    pub fn get_command(&self) -> &str {
600        &self.command
601    }
602
603    fn get_name_as_json(&self) -> Option<String> {
604        self.get_name().map(create_json_text_component)
605    }
606}
607
608struct LoadedListener {
609    structures_dir: PathBuf,
610}
611impl LoadedListener {
612    fn on_event(&self, event: LogEvent) {
613        if let Some(id) = parse_loaded_output(&event) {
614            let structure_file = self.get_structure_file(id);
615            if let Err(error) = remove_file(&structure_file) {
616                error!("{}", error);
617            }
618            // Remove all previous structure files in case they are still there
619            // (for instance because a structure was loaded while no connection was active)
620            let mut i = 1;
621            while let Ok(()) = remove_file(self.get_structure_file(id.wrapping_sub(i))) {
622                i += 1;
623            }
624        }
625    }
626
627    fn get_structure_file(&self, id: impl Display) -> PathBuf {
628        self.structures_dir.join(format!("{}.nbt", id))
629    }
630}
631
632const LOADED_LISTENER_NAME: &str = "minect_loaded";
633const STRUCTURE_LOADED_OUTPUT_PREFIX: &str = "minect_loaded_";
634
635fn parse_loaded_output(event: &LogEvent) -> Option<u64> {
636    if event.executor != LOADED_LISTENER_NAME {
637        return None;
638    }
639    let output = event.output.parse::<SummonNamedEntityOutput>().ok()?;
640    let id = &output.name.strip_prefix(STRUCTURE_LOADED_OUTPUT_PREFIX)?;
641    id.parse().ok()
642}
643
644fn add_implicit_commands(
645    commands: impl IntoIterator<IntoIter = impl ExactSizeIterator<Item = Command>>,
646    connection_id: &str,
647    structure_id: u64,
648    enable_logging_automatically: bool,
649) -> (impl Iterator<Item = Command>, usize) {
650    let mut first_cmds = Vec::from_iter([
651        Command::new(format!(
652            "tag @e[type=area_effect_cloud,tag=minect_connection,tag=!minect_connection+{}] add minect_inactive",
653            connection_id
654        )),
655        Command::new(enable_logging_command()),
656        Command::named(
657            LOADED_LISTENER_NAME,
658            summon_named_entity_command(&format!(
659                "{}{}",
660                STRUCTURE_LOADED_OUTPUT_PREFIX, structure_id
661            )),
662        ),
663    ]);
664    let mut last_cmds = Vec::new();
665    if !enable_logging_automatically {
666        first_cmds.push(Command::new(reset_logging_command()));
667        last_cmds.push(Command::new(enable_logging_command()));
668    }
669    last_cmds.push(Command::new("function minect_internal:clean_up"));
670
671    let commands = commands.into_iter();
672    let commands_len = first_cmds.len() + commands.len();
673    let commands = first_cmds.into_iter().chain(commands).chain(last_cmds);
674    (commands, commands_len)
675}