minect/command.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//! Functions for generating Minecraft commands that produce [LogEvent](crate::log::LogEvent)s.
20//!
21//! [LogEvents](crate::log::LogEvent) are only produced when the output of a command is written to
22//! Minecraft's log file. For this to happen a number of preconditions have to be met:
23//! 1. The command has to be executed by a player, command block or command block minecart. The
24//! output of a command executed by a `mcfunction` is never logged.
25//! 2. The gamerule `logAdminCommands` has to be `true`. If the command block is executed by a
26//! command block or command block minecart then the gamerule `commandBlockOutput` also has to be
27//! `true`.
28//!
29//! # Set Gamerules appropriately for Logging
30//!
31//! It is typically not a good idea to enable the gamerule `commandBlockOutput` for longer than
32//! neccessary. The reason for this is that the output of commands is also written to the chat when
33//! the gamerule `sendCommandFeedback` is enabled. This will likely annoy players as it makes the
34//! chat unusable and causes it to take up a big part of the screen, even when only a single command
35//! logs it's output every game tick. So whenever `commandBlockOutput` is enabled,
36//! `sendCommandFeedback` should be disabled. But `sendCommandFeedback` should not be disabled for
37//! longer than neccessary, because without it players will not get any output from commands they
38//! execute.
39//!
40//! Ideally whenever the output of one or more commands should be logged, the three gamerules should
41//! first be set to enable logging without spamming the chat and after the commands are executed,
42//! the gamerules should be reset to their previous values to preserve the world configuration. This
43//! can be done with [enable_logging_command] and [reset_logging_command].
44//!
45//! # Logging Command Output from Minecraft Function Files
46//!
47//! Minect offers two ways to work around the limitation of `mcfunction` files. To log the output of
48//! a command from a `mcfunction` file you can either use [logged_block_commands] or a
49//! [logged_cart_command].
50//!
51//! # Common Commands with useful Output
52//!
53//! Minect offers a few functions to generate commands commonly used to retrieve information from
54//! Minecraft. Their output can then be parsed into predefined structs:
55//! * [summon_named_entity_command] -> [SummonNamedEntityOutput]
56//! * [add_tag_command] -> [AddTagOutput]
57//! * [query_scoreboard_command] -> [QueryScoreboardOutput]
58
59use crate::json::{create_json_text_component, escape_json};
60use std::{
61 fmt::{self, Display},
62 str::FromStr,
63};
64
65/// Generates a Minecraft command that ensures [LogEvent](crate::log::LogEvent)s are created for all
66/// commands until a [reset_logging_command] is executed. These two commands are executed
67/// automatically by [execute_commands](crate::MinecraftConnection::execute_commands) if
68/// [enable_logging_automatically](crate::MinecraftConnectionBuilder::enable_logging_automatically)
69/// is `true` (which is the default).
70///
71/// This command sets the following three gamerules:
72/// 1. `logAdminCommands`: This must be `true` for Minecraft to write the output of commands to the
73/// log file.
74/// 2. `commandBlockOutput`: This must be `true` for command blocks and command block minecarts to
75/// broadcast the output of their commands.
76/// 3. `sendCommandFeedback`: This is set to `false` to prevent the output to to also be written to
77/// the chat which would likely annoy players.
78///
79/// This changes the logging configuration of the world in such a way that a player does not get any
80/// output from any command (including commands the player executes). So the original values of the
81/// gamerules are stored and can be restored by executing a [reset_logging_command].
82///
83/// After executing multiple [enable_logging_command]s, the same number of [reset_logging_command]s
84/// has to be executed to reset logging.
85pub fn enable_logging_command() -> String {
86 "function minect:enable_logging".to_string()
87}
88
89/// Generates a Minecraft command that restores the logging gamerules to their values before the
90/// last [enable_logging_command] was executed.
91pub fn reset_logging_command() -> String {
92 "function minect:reset_logging".to_string()
93}
94
95/// Generates two Minecraft commands that cause the given command to be executed from a command
96/// block. This can be used to log the output of a command when running in a `mcfunction`.
97///
98/// The two commands are also available individually through [prepare_logged_block_command] and
99/// [logged_block_command]. To work properly each [logged_block_command] has to be preceded by a
100/// single [prepare_logged_block_command], otherwise it may overwrite a previous command or not be
101/// executed at all.
102///
103/// There are two variants of this function that also define the name of the command block:
104/// [named_logged_block_commands] and [json_named_logged_block_commands]. They can be used to allow
105/// easy filtering of [LogEvent](crate::log::LogEvent)s with
106/// [MinecraftConnection::add_named_listener](crate::MinecraftConnection::add_named_listener) or
107/// [LogObserver::add_named_listener](crate::log::LogObserver::add_named_listener).
108///
109/// When the command block executes, the gamerules will be set appropriately for logging. So there
110/// is no need to execute an [enable_logging_command] and a [reset_logging_command].
111///
112/// # Example
113///
114/// ```no_run
115/// # use minect::*;
116/// # use minect::command::*;
117/// let mut commands = Vec::new();
118/// commands.push("say querying scoreboard ...".to_string());
119/// commands.extend(logged_block_commands(query_scoreboard_command("@p", "my_scoreboard")));
120/// let my_function = commands.join("\n");
121///
122/// // Generate datapack containing my_function ...
123///
124/// // Call my_function (could also be done in Minecraft)
125/// # let mut connection = MinecraftConnection::builder("", "").build();
126/// connection.execute_commands([Command::new("function my_namespace:my_function")])?;
127/// # Ok::<(), std::io::Error>(())
128/// ```
129///
130/// # Timing
131///
132/// The command block executes delayed, but it is guaranteed to execute within the same gametick as
133/// the `mcfunction` in the following cases:
134/// * The `mcfunction` is executed by a `function` command passed to
135/// [execute_commands](crate::MinecraftConnection::execute_commands).
136/// * The `mcfunction` is executed by a `function` command passed to [logged_block_commands].
137/// * The `mcfunction` is executed by a `schedule` command.
138///
139/// Otherwise the command block may execute in the next game tick. Examples include, but are not
140/// limited to:
141/// * The `mcfunction` is executed by the function tag `#minecraft:tick`.
142/// * The `mcfunction` is executed by a custom command block.
143pub fn logged_block_commands(command: impl AsRef<str>) -> [String; 2] {
144 [
145 prepare_logged_block_command(),
146 logged_block_command(command),
147 ]
148}
149
150/// The same as [logged_block_commands], but also defines the name of the command block to allow
151/// easy filtering of [LogEvent](crate::log::LogEvent)s with
152/// [MinecraftConnection::add_named_listener](crate::MinecraftConnection::add_named_listener) or
153/// [LogObserver::add_named_listener](crate::log::LogObserver::add_named_listener).
154pub fn named_logged_block_commands(name: impl AsRef<str>, command: impl AsRef<str>) -> [String; 2] {
155 [
156 prepare_logged_block_command(),
157 named_logged_block_command(name, command),
158 ]
159}
160
161/// The same as [named_logged_block_commands], but the name of the command block is given as a JSON
162/// text component.
163pub fn json_named_logged_block_commands(
164 name: impl AsRef<str>,
165 command: impl AsRef<str>,
166) -> [String; 2] {
167 [
168 prepare_logged_block_command(),
169 json_named_logged_block_command(name, command),
170 ]
171}
172
173/// Generates a Minecraft command that prepares the next [logged_block_command],
174/// [named_logged_block_command] or [json_named_logged_block_command].
175pub fn prepare_logged_block_command() -> String {
176 "function minect:prepare_logged_block".to_string()
177}
178
179const EXECUTE_AT_CURSOR: &str = "execute at @e[type=area_effect_cloud,tag=minect_cursor] run";
180
181/// See [logged_block_commands]. Must be preceded by a [prepare_logged_block_command].
182pub fn logged_block_command(command: impl AsRef<str>) -> String {
183 format!(
184 "{} data modify block ~ ~ ~ Command set value \"{}\"",
185 EXECUTE_AT_CURSOR,
186 escape_json(command.as_ref()),
187 )
188}
189
190/// See [named_logged_block_commands]. Must be preceded by a [prepare_logged_block_command].
191pub fn named_logged_block_command(name: impl AsRef<str>, command: impl AsRef<str>) -> String {
192 json_named_logged_block_command(&create_json_text_component(name.as_ref()), command)
193}
194
195/// See [json_named_logged_block_commands]. Must be preceded by a [prepare_logged_block_command].
196pub fn json_named_logged_block_command(name: impl AsRef<str>, command: impl AsRef<str>) -> String {
197 format!(
198 "{} data modify block ~ ~ ~ {{}} merge value {{CustomName:\"{}\",Command:\"{}\"}}",
199 EXECUTE_AT_CURSOR,
200 escape_json(name.as_ref()),
201 escape_json(command.as_ref()),
202 )
203}
204
205/// Generates a Minecraft command that causes the given command to be executed from a command block
206/// minecart. This can be used to log the output of a command when running in a `mcfunction`.
207///
208/// There are two variants of this function that also define the name of the command block:
209/// [named_logged_cart_command] and [json_named_logged_cart_command]. They can be used to allow easy
210/// filtering of [LogEvent](crate::log::LogEvent)s with
211/// [MinecraftConnection::add_named_listener](crate::MinecraftConnection::add_named_listener) or
212/// [LogObserver::add_named_listener](crate::log::LogObserver::add_named_listener).
213///
214/// To ensure [LogEvent](crate::log::LogEvent)s are created, the first logged command should be an
215/// [enable_logging_command] and the last one should be a [reset_logging_command]:
216/// ```no_run
217/// # use minect::*;
218/// # use minect::command::*;
219/// let my_function = [
220/// logged_cart_command(enable_logging_command()),
221/// logged_cart_command(query_scoreboard_command("@p", "my_scoreboard")),
222/// logged_cart_command(reset_logging_command()),
223/// ].join("\n");
224///
225/// // Generate datapack containing my_function ...
226///
227/// // Call my_function (could also be done in Minecraft)
228/// # let mut connection = MinecraftConnection::builder("", "").build();
229/// connection.execute_commands([Command::new("function my_namespace:my_function")])?;
230/// # Ok::<(), std::io::Error>(())
231/// ```
232///
233/// # Timing
234///
235/// Command block minecarts always execute with a 4 tick delay, so it is generally better to use
236/// [logged_block_commands].
237pub fn logged_cart_command(command: impl AsRef<str>) -> String {
238 build_logged_cart_command(None, command.as_ref())
239}
240
241/// The same as [logged_cart_command], but also defines the name of the command block minecart to
242/// allow easy filtering of [LogEvent](crate::log::LogEvent)s with
243/// [MinecraftConnection::add_named_listener](crate::MinecraftConnection::add_named_listener) or
244/// [LogObserver::add_named_listener](crate::log::LogObserver::add_named_listener).
245pub fn named_logged_cart_command(name: impl AsRef<str>, command: impl AsRef<str>) -> String {
246 json_named_logged_cart_command(&create_json_text_component(name.as_ref()), command)
247}
248
249/// The same as [named_logged_cart_command], but the name of the command block minecart is given as
250/// a JSON text component.
251pub fn json_named_logged_cart_command(name: impl AsRef<str>, command: impl AsRef<str>) -> String {
252 build_logged_cart_command(Some(name.as_ref()), command.as_ref())
253}
254
255fn build_logged_cart_command(name: Option<&str>, command: &str) -> String {
256 let custom_name_entry = if let Some(name) = name {
257 format!("CustomName:\"{}\",", escape_json(name))
258 } else {
259 "".to_string()
260 };
261
262 format!(
263 "execute at @e[type=area_effect_cloud,tag=minect_connection,limit=1] run \
264 summon command_block_minecart ~ ~ ~ {{\
265 {}\
266 Command:\"{}\",\
267 Tags:[minect,minect_impulse],\
268 LastExecution:1L,\
269 TrackOutput:false,\
270 }}",
271 custom_name_entry,
272 escape_json(command),
273 )
274}
275
276/// Generates a Minecraft command that summons an area effect cloud with the given `name`.
277///
278/// The resulting [LogEvent::output](crate::log::LogEvent::output) can be parsed into a
279/// [SummonNamedEntityOutput].
280///
281/// `name` is interpreted as a string, not a JSON text component.
282///
283/// By using a unique `name` this command can be used inside an `execute if` command to check if
284/// some condition is true in Minecraft. A good way to generate a unique `name` is to use a UUID.
285///
286/// When using [logged_cart_command]s, [add_tag_command] is usually a better alternative in terms of
287/// performance, because it avoids the overhead of summoning a new entity.
288pub fn summon_named_entity_command(name: &str) -> String {
289 let custom_name = create_json_text_component(name);
290 format!(
291 "summon area_effect_cloud ~ ~ ~ {{\"CustomName\":\"{}\"}}",
292 escape_json(&custom_name)
293 )
294}
295
296/// The output of a [summon_named_entity_command]. This can be parsed from a
297/// [LogEvent::output](crate::log::LogEvent::output).
298///
299/// The output has the following format:
300/// ```none
301/// Summoned new <name>
302/// ```
303///
304/// For example:
305/// ```none
306/// Summoned new my_name
307/// ```
308#[derive(Clone, Debug, Eq, PartialEq)]
309pub struct SummonNamedEntityOutput {
310 /// The name of the summoned entity.
311 pub name: String,
312 _private: (),
313}
314impl FromStr for SummonNamedEntityOutput {
315 type Err = ();
316
317 fn from_str(output: &str) -> Result<Self, Self::Err> {
318 fn from_str_opt(output: &str) -> Option<SummonNamedEntityOutput> {
319 let name = output.strip_prefix("Summoned new ")?;
320
321 Some(SummonNamedEntityOutput {
322 name: name.to_string(),
323 _private: (),
324 })
325 }
326 from_str_opt(output).ok_or(())
327 }
328}
329impl Display for SummonNamedEntityOutput {
330 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
331 write!(f, "Summoned new {}", self.name)
332 }
333}
334
335/// Generates a Minecraft command that adds the given `tag` to the given `entity`.
336///
337/// The resulting [LogEvent::output](crate::log::LogEvent::output) can be parsed into an
338/// [AddTagOutput].
339///
340/// `entity` can be any selector or name.
341///
342/// For a [logged_cart_command] that only uses this tag as a means to know when/if the command is
343/// executed (for example inside an `execute if` command) it can be useful to add a tag to the `@s`
344/// entity. This saves the trouble of removing the tag again, because the command block minecart is
345/// killed after the command is executed. Otherwise the tag will likely need to be removed, because
346/// adding a tag twice to the same entity fails, thus preventing further
347/// [LogEvent](crate::log::LogEvent)s.
348pub fn add_tag_command(entity: impl Display, tag: impl Display) -> String {
349 format!("tag {} add {}", entity, tag)
350}
351
352/// The output of an [add_tag_command]. This can be parsed from a
353/// [LogEvent::output](crate::log::LogEvent::output).
354///
355/// The output has the following format:
356/// ```none
357/// Added tag '<tag>' to <entity>
358/// ```
359///
360/// For example:
361/// ```none
362/// Added tag 'my_tag' to my_entity
363/// ```
364#[derive(Clone, Debug, Eq, PartialEq)]
365pub struct AddTagOutput {
366 /// The tag that was added.
367 pub tag: String,
368 /// The custom name or UUID of the entity the tag was added to.
369 pub entity: String,
370 _private: (),
371}
372impl FromStr for AddTagOutput {
373 type Err = ();
374
375 fn from_str(output: &str) -> Result<Self, Self::Err> {
376 fn from_str_opt(output: &str) -> Option<AddTagOutput> {
377 let suffix = output.strip_prefix("Added tag '")?;
378 let (tag, entity) = suffix.split_once("' to ")?;
379
380 Some(AddTagOutput {
381 tag: tag.to_string(),
382 entity: entity.to_string(),
383 _private: (),
384 })
385 }
386 from_str_opt(output).ok_or(())
387 }
388}
389impl Display for AddTagOutput {
390 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
391 write!(f, "Added tag '{}' to {}", self.tag, self.entity)
392 }
393}
394
395/// Generates a Minecraft command that queries the score of `entity` in `scoreboard`.
396///
397/// The resulting [LogEvent::output](crate::log::LogEvent::output) can be parsed into a
398/// [QueryScoreboardOutput].
399///
400/// `entity` can be any selector or name.
401pub fn query_scoreboard_command(entity: impl Display, scoreboard: impl Display) -> String {
402 format!("scoreboard players add {} {} 0", entity, scoreboard)
403}
404
405/// The output of a [query_scoreboard_command]. This can be parsed from
406/// [LogEvent::output](crate::log::LogEvent::output).
407///
408/// The output has the following format:
409/// ```none
410/// Added 0 to [<scoreboard>] for <entity> (now <score>)
411/// ```
412///
413/// For example:
414/// ```none
415/// Added 0 to [my_scoreboard] for my_entity (now 42)
416/// ```
417#[derive(Clone, Debug, Eq, PartialEq)]
418pub struct QueryScoreboardOutput {
419 /// The scoreboard.
420 pub scoreboard: String,
421 /// The name of the player or UUID of the entity.
422 pub entity: String,
423 /// The score of the entity.
424 pub score: i32,
425 _private: (),
426}
427impl FromStr for QueryScoreboardOutput {
428 type Err = ();
429
430 fn from_str(output: &str) -> Result<Self, Self::Err> {
431 fn from_str_opt(output: &str) -> Option<QueryScoreboardOutput> {
432 let suffix = output.strip_prefix("Added 0 to [")?;
433 const FOR: &str = "] for ";
434 let index = suffix.find(FOR)?;
435 let (scoreboard, suffix) = suffix.split_at(index);
436 let suffix = suffix.strip_prefix(FOR)?;
437
438 const NOW: &str = " (now ";
439 let index = suffix.rfind(NOW)?;
440 let (entity, suffix) = suffix.split_at(index);
441 let suffix = suffix.strip_prefix(NOW)?;
442 let score = suffix.strip_suffix(')')?;
443 let score = score.parse().ok()?;
444
445 Some(QueryScoreboardOutput {
446 scoreboard: scoreboard.to_string(),
447 entity: entity.to_string(),
448 score,
449 _private: (),
450 })
451 }
452 from_str_opt(output).ok_or(())
453 }
454}
455impl Display for QueryScoreboardOutput {
456 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
457 write!(
458 f,
459 "Added 0 to [{}] for {} (now {})",
460 self.scoreboard, self.entity, self.score
461 )
462 }
463}
464
465#[cfg(test)]
466mod tests;