Skip to main content

steel_core/command/builtins/execute/
store.rs

1//! `/execute store` result consumers.
2
3use std::{error::Error, fmt, io::Cursor, sync::Arc};
4
5use simdnbt::{
6    borrow::read_compound as read_borrowed_compound,
7    owned::{NbtCompound, NbtTag},
8};
9use steel_utils::{
10    Identifier,
11    nbt::{NbtPath, NbtPathMutationError},
12};
13
14use super::super::super::{
15    brigadier::{ArgumentType, CommandNodeBuilder, CommandRedirectTarget, CommandSyntaxError},
16    execution::{
17        CommandResultCallback, CommandSource, ExecutionCommandSource as _, ScoreHolderWildcard,
18        SteelArgumentType, SteelCommandContext, SteelCommandRuntime, argument, literal,
19    },
20};
21use super::{
22    condition::{invalid_block_data_source, loaded_block_position},
23    objective, source_command_storage, source_scoreboard,
24};
25use crate::scoreboard::{ScoreHolder, Scoreboard, ScoreboardError, ScoreboardObjective};
26use crate::{block_entity::SharedBlockEntity, command::storage::CommandStorage};
27
28type Builder = CommandNodeBuilder<CommandSource, SteelCommandRuntime>;
29
30const EXECUTE_ROOT: CommandRedirectTarget = CommandRedirectTarget::CommandRoot;
31
32pub(super) fn target(name: &'static str, store_result: bool) -> Builder {
33    // TODO: Add bossbar after Steel has a persistent custom-bossbar manager.
34    // TODO: Add entity after live entity NBT can reload every command-visible field.
35    literal(name)
36        .then(
37            literal("score").then(
38                argument("targets", SteelArgumentType::score_holders()).then(
39                    argument("objective", SteelArgumentType::objective())
40                        .redirects_with(EXECUTE_ROOT, move |context| {
41                            store_score(context, store_result)
42                        }),
43                ),
44            ),
45        )
46        .then(
47            literal("block").then(
48                argument("targetPos", SteelArgumentType::block_pos())
49                    .then(data_path(StoreDataTarget::Block, store_result)),
50            ),
51        )
52        .then(
53            literal("storage").then(
54                argument("target", SteelArgumentType::storage_key())
55                    .then(data_path(StoreDataTarget::Storage, store_result)),
56            ),
57        )
58}
59
60fn data_path(target: StoreDataTarget, store_result: bool) -> Builder {
61    argument("path", SteelArgumentType::nbt_path())
62        .then(data_type("int", StoreDataType::Int, target, store_result))
63        .then(data_type(
64            "float",
65            StoreDataType::Float,
66            target,
67            store_result,
68        ))
69        .then(data_type(
70            "short",
71            StoreDataType::Short,
72            target,
73            store_result,
74        ))
75        .then(data_type("long", StoreDataType::Long, target, store_result))
76        .then(data_type(
77            "double",
78            StoreDataType::Double,
79            target,
80            store_result,
81        ))
82        .then(data_type("byte", StoreDataType::Byte, target, store_result))
83}
84
85fn data_type(
86    name: &'static str,
87    data_type: StoreDataType,
88    target: StoreDataTarget,
89    store_result: bool,
90) -> Builder {
91    literal(name).then(
92        argument("scale", ArgumentType::double(f64::MIN, f64::MAX))
93            .redirects_with(EXECUTE_ROOT, move |context| {
94                store_data(context, target, data_type, store_result)
95            }),
96    )
97}
98
99#[derive(Clone, Copy)]
100enum StoreDataTarget {
101    Block,
102    Storage,
103}
104
105#[derive(Clone, Copy, Debug)]
106enum StoreDataType {
107    Byte,
108    Short,
109    Int,
110    Long,
111    Float,
112    Double,
113}
114
115impl StoreDataType {
116    fn tag(self, value: i32, scale: f64) -> NbtTag {
117        let scaled = f64::from(value) * scale;
118        match self {
119            Self::Byte => NbtTag::Byte((scaled as i32) as i8),
120            Self::Short => NbtTag::Short((scaled as i32) as i16),
121            Self::Int => NbtTag::Int(scaled as i32),
122            Self::Long => NbtTag::Long(scaled as i64),
123            Self::Float => NbtTag::Float(scaled as f32),
124            Self::Double => NbtTag::Double(scaled),
125        }
126    }
127}
128
129fn store_data(
130    context: &SteelCommandContext<CommandSource>,
131    target: StoreDataTarget,
132    data_type: StoreDataType,
133    store_result: bool,
134) -> Result<CommandSource, CommandSyntaxError> {
135    match target {
136        StoreDataTarget::Block => store_block_data(context, data_type, store_result),
137        StoreDataTarget::Storage => store_storage_data(context, data_type, store_result),
138    }
139}
140
141fn store_block_data(
142    context: &SteelCommandContext<CommandSource>,
143    data_type: StoreDataType,
144    store_result: bool,
145) -> Result<CommandSource, CommandSyntaxError> {
146    let position = loaded_block_position(context, "targetPos")?;
147    let source = context.source();
148    let block_entity = source
149        .world()
150        .get_block_entity(position)
151        .ok_or_else(invalid_block_data_source)?;
152    let path = parsed_path(context)?;
153    let scale = parsed_scale(context)?;
154    let world = Arc::clone(source.world());
155    let callback = CommandResultCallback::new(move |success, result| {
156        let value = stored_value(store_result, success, result);
157        if store_block_data_value(&block_entity, &path, data_type.tag(value, scale)).is_ok() {
158            world.send_block_updated(position);
159        }
160    });
161    let callback = CommandResultCallback::chain(source.callback(), callback);
162    Ok(source.with_callback(callback))
163}
164
165fn store_storage_data(
166    context: &SteelCommandContext<CommandSource>,
167    data_type: StoreDataType,
168    store_result: bool,
169) -> Result<CommandSource, CommandSyntaxError> {
170    source_command_storage(context)?;
171    let target = context.identifier("target")?.clone();
172    let path = parsed_path(context)?;
173    let scale = parsed_scale(context)?;
174    let source = context.source();
175    let server = Arc::clone(source.server());
176    let domain = source.world().domain().to_owned();
177    let callback = CommandResultCallback::new(move |success, result| {
178        let Some(storage) = server.command_storage.get(&domain) else {
179            return;
180        };
181        let value = stored_value(store_result, success, result);
182        let _ = store_storage_data_value(storage, &target, &path, data_type.tag(value, scale));
183    });
184    let callback = CommandResultCallback::chain(source.callback(), callback);
185    Ok(source.with_callback(callback))
186}
187
188fn parsed_path(
189    context: &SteelCommandContext<CommandSource>,
190) -> Result<NbtPath, CommandSyntaxError> {
191    context.nbt_path("path").cloned()
192}
193
194fn parsed_scale(context: &SteelCommandContext<CommandSource>) -> Result<f64, CommandSyntaxError> {
195    context.double("scale")
196}
197
198fn store_score(
199    context: &SteelCommandContext<CommandSource>,
200    store_result: bool,
201) -> Result<CommandSource, CommandSyntaxError> {
202    let scoreboard = source_scoreboard(context)?;
203    let objective = objective(context, scoreboard, "objective")?;
204    let holders = context.score_holders("targets", ScoreHolderWildcard::Tracked)?;
205    let source = context.source();
206    let server = Arc::clone(source.server());
207    let domain = source.world().domain().to_owned();
208    let callback = CommandResultCallback::new(move |success, result| {
209        let Some(scoreboard) = server.scoreboards.get(&domain) else {
210            tracing::warn!(%domain, "execute store score domain is no longer available");
211            return;
212        };
213        let value = stored_value(store_result, success, result);
214        if let Err(error) = store_score_value(scoreboard, &holders, &objective, value) {
215            tracing::warn!(%error, "failed to store execute result in scoreboard");
216        }
217    });
218    let callback = CommandResultCallback::chain(source.callback(), callback);
219    Ok(source.with_callback(callback))
220}
221
222fn stored_value(store_result: bool, success: bool, result: i32) -> i32 {
223    if store_result {
224        result
225    } else {
226        i32::from(success)
227    }
228}
229
230fn store_score_value(
231    scoreboard: &Scoreboard,
232    holders: &[ScoreHolder],
233    objective: &ScoreboardObjective,
234    value: i32,
235) -> Result<(), ScoreboardError> {
236    for holder in holders {
237        scoreboard.set_score(holder, objective, value)?;
238    }
239    Ok(())
240}
241
242fn store_storage_data_value(
243    storage: &CommandStorage,
244    id: &Identifier,
245    path: &NbtPath,
246    value: NbtTag,
247) -> Result<(), StoreDataMutationError> {
248    let data = mutate_compound_path(storage.get(id), path, value)?;
249    storage.set(id.clone(), data);
250    Ok(())
251}
252
253fn store_block_data_value(
254    block_entity: &SharedBlockEntity,
255    path: &NbtPath,
256    value: NbtTag,
257) -> Result<(), StoreDataMutationError> {
258    let data = mutate_compound_path(block_entity.save_with_full_metadata(), path, value)?;
259    let mut bytes = Vec::new();
260    data.write(&mut bytes);
261    let borrowed = read_borrowed_compound(&mut Cursor::new(bytes.as_slice()))
262        .map_err(|_| StoreDataMutationError::InvalidWrittenNbt)?;
263    block_entity.load_additional(&borrowed);
264    block_entity.set_changed();
265    Ok(())
266}
267
268fn mutate_compound_path(
269    data: NbtCompound,
270    path: &NbtPath,
271    value: NbtTag,
272) -> Result<NbtCompound, StoreDataMutationError> {
273    let mut root = NbtTag::Compound(data);
274    path.set(&mut root, value)
275        .map_err(StoreDataMutationError::Path)?;
276    match root {
277        NbtTag::Compound(data) => Ok(data),
278        _ => Err(StoreDataMutationError::ExpectedCompoundRoot),
279    }
280}
281
282#[derive(Debug)]
283enum StoreDataMutationError {
284    Path(NbtPathMutationError),
285    ExpectedCompoundRoot,
286    InvalidWrittenNbt,
287}
288
289impl fmt::Display for StoreDataMutationError {
290    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
291        match self {
292            Self::Path(error) => write!(formatter, "{error}"),
293            Self::ExpectedCompoundRoot => write!(formatter, "NBT mutation replaced compound root"),
294            Self::InvalidWrittenNbt => write!(formatter, "mutated NBT could not be reborrowed"),
295        }
296    }
297}
298
299impl Error for StoreDataMutationError {}
300
301#[cfg(test)]
302mod tests {
303    use steel_utils::nbt::parse_nbt_path_argument;
304
305    use super::*;
306
307    #[test]
308    fn score_store_creates_and_updates_each_holder() {
309        let scoreboard = Scoreboard::new();
310        let Ok(objective) = scoreboard.add_objective("result") else {
311            panic!("objective should be created");
312        };
313        let holders = [ScoreHolder::new("one"), ScoreHolder::new("two")];
314
315        assert!(store_score_value(&scoreboard, &holders, &objective, 7).is_ok());
316        assert_eq!(scoreboard.score(&holders[0], &objective), Some(7));
317        assert_eq!(scoreboard.score(&holders[1], &objective), Some(7));
318    }
319
320    #[test]
321    fn stored_value_distinguishes_numeric_results_from_success() {
322        assert_eq!(stored_value(true, true, 17), 17);
323        assert_eq!(stored_value(true, false, 0), 0);
324        assert_eq!(stored_value(false, true, 17), 1);
325        assert_eq!(stored_value(false, false, 17), 0);
326    }
327
328    #[test]
329    fn data_types_match_java_numeric_narrowing() {
330        assert_eq!(StoreDataType::Byte.tag(128, 1.0), NbtTag::Byte(-128));
331        assert_eq!(StoreDataType::Byte.tag(i32::MAX, 1e20), NbtTag::Byte(-1));
332        assert_eq!(StoreDataType::Byte.tag(i32::MIN, 1e20), NbtTag::Byte(0));
333        assert_eq!(
334            StoreDataType::Short.tag(32_768, 1.0),
335            NbtTag::Short(-32_768)
336        );
337        assert_eq!(
338            StoreDataType::Int.tag(i32::MAX, 1e20),
339            NbtTag::Int(i32::MAX)
340        );
341        assert_eq!(
342            StoreDataType::Long.tag(i32::MAX, 1e20),
343            NbtTag::Long(i64::MAX)
344        );
345        assert_eq!(StoreDataType::Float.tag(3, 0.5), NbtTag::Float(1.5));
346        assert_eq!(StoreDataType::Double.tag(3, 0.5), NbtTag::Double(1.5));
347    }
348
349    #[test]
350    fn compound_path_mutation_creates_and_replaces_values() {
351        let (path, _) = parse_nbt_path_argument("result.value").expect("path should parse");
352        let data = mutate_compound_path(NbtCompound::new(), &path, NbtTag::Int(7))
353            .expect("path should create missing compounds");
354        let result = data
355            .compound("result")
356            .expect("result compound should exist");
357        assert_eq!(result.int("value"), Some(7));
358
359        let data = mutate_compound_path(data, &path, NbtTag::Byte(1))
360            .expect("path should replace existing value");
361        let result = data
362            .compound("result")
363            .expect("result compound should exist");
364        assert_eq!(result.byte("value"), Some(1));
365    }
366
367    #[test]
368    fn storage_mutation_reads_and_writes_the_current_compound() {
369        let storage = CommandStorage::new();
370        let key = Identifier::from_steel("store_test");
371        let (first_path, _) = parse_nbt_path_argument("first").expect("first path should parse");
372        let (second_path, _) = parse_nbt_path_argument("second").expect("second path should parse");
373
374        assert!(store_storage_data_value(&storage, &key, &first_path, NbtTag::Int(4)).is_ok());
375        assert!(
376            store_storage_data_value(&storage, &key, &second_path, NbtTag::Double(2.5)).is_ok()
377        );
378
379        let stored = storage.get(&key);
380        assert_eq!(stored.int("first"), Some(4));
381        assert_eq!(stored.double("second"), Some(2.5));
382    }
383}