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
172        .identifier("target")
173        .ok_or_else(|| missing_argument("target"))?
174        .clone();
175    let path = parsed_path(context)?;
176    let scale = parsed_scale(context)?;
177    let source = context.source();
178    let server = Arc::clone(source.server());
179    let domain = source.world().domain().to_owned();
180    let callback = CommandResultCallback::new(move |success, result| {
181        let Some(storage) = server.command_storage.get(&domain) else {
182            return;
183        };
184        let value = stored_value(store_result, success, result);
185        let _ = store_storage_data_value(storage, &target, &path, data_type.tag(value, scale));
186    });
187    let callback = CommandResultCallback::chain(source.callback(), callback);
188    Ok(source.with_callback(callback))
189}
190
191fn parsed_path(
192    context: &SteelCommandContext<CommandSource>,
193) -> Result<NbtPath, CommandSyntaxError> {
194    context
195        .nbt_path("path")
196        .cloned()
197        .ok_or_else(|| missing_argument("path"))
198}
199
200fn parsed_scale(context: &SteelCommandContext<CommandSource>) -> Result<f64, CommandSyntaxError> {
201    context
202        .double("scale")
203        .ok_or_else(|| missing_argument("scale"))
204}
205
206fn missing_argument(name: &str) -> CommandSyntaxError {
207    CommandSyntaxError::dynamic(format!(
208        "Parsed value for {name} is missing from the command context"
209    ))
210}
211
212fn store_score(
213    context: &SteelCommandContext<CommandSource>,
214    store_result: bool,
215) -> Result<CommandSource, CommandSyntaxError> {
216    let scoreboard = source_scoreboard(context)?;
217    let objective = objective(context, scoreboard, "objective")?;
218    let holders = context.score_holders("targets", ScoreHolderWildcard::Tracked)?;
219    let source = context.source();
220    let server = Arc::clone(source.server());
221    let domain = source.world().domain().to_owned();
222    let callback = CommandResultCallback::new(move |success, result| {
223        let Some(scoreboard) = server.scoreboards.get(&domain) else {
224            tracing::warn!(%domain, "execute store score domain is no longer available");
225            return;
226        };
227        let value = stored_value(store_result, success, result);
228        if let Err(error) = store_score_value(scoreboard, &holders, &objective, value) {
229            tracing::warn!(%error, "failed to store execute result in scoreboard");
230        }
231    });
232    let callback = CommandResultCallback::chain(source.callback(), callback);
233    Ok(source.with_callback(callback))
234}
235
236fn stored_value(store_result: bool, success: bool, result: i32) -> i32 {
237    if store_result {
238        result
239    } else {
240        i32::from(success)
241    }
242}
243
244fn store_score_value(
245    scoreboard: &Scoreboard,
246    holders: &[ScoreHolder],
247    objective: &ScoreboardObjective,
248    value: i32,
249) -> Result<(), ScoreboardError> {
250    for holder in holders {
251        scoreboard.set_score(holder, objective, value)?;
252    }
253    Ok(())
254}
255
256fn store_storage_data_value(
257    storage: &CommandStorage,
258    id: &Identifier,
259    path: &NbtPath,
260    value: NbtTag,
261) -> Result<(), StoreDataMutationError> {
262    let data = mutate_compound_path(storage.get(id), path, value)?;
263    storage.set(id.clone(), data);
264    Ok(())
265}
266
267fn store_block_data_value(
268    block_entity: &SharedBlockEntity,
269    path: &NbtPath,
270    value: NbtTag,
271) -> Result<(), StoreDataMutationError> {
272    let data = mutate_compound_path(block_entity.save_with_full_metadata(), path, value)?;
273    let mut bytes = Vec::new();
274    data.write(&mut bytes);
275    let borrowed = read_borrowed_compound(&mut Cursor::new(bytes.as_slice()))
276        .map_err(|_| StoreDataMutationError::InvalidWrittenNbt)?;
277    block_entity.load_additional(&borrowed);
278    block_entity.set_changed();
279    Ok(())
280}
281
282fn mutate_compound_path(
283    data: NbtCompound,
284    path: &NbtPath,
285    value: NbtTag,
286) -> Result<NbtCompound, StoreDataMutationError> {
287    let mut root = NbtTag::Compound(data);
288    path.set(&mut root, value)
289        .map_err(StoreDataMutationError::Path)?;
290    match root {
291        NbtTag::Compound(data) => Ok(data),
292        _ => Err(StoreDataMutationError::ExpectedCompoundRoot),
293    }
294}
295
296#[derive(Debug)]
297enum StoreDataMutationError {
298    Path(NbtPathMutationError),
299    ExpectedCompoundRoot,
300    InvalidWrittenNbt,
301}
302
303impl fmt::Display for StoreDataMutationError {
304    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
305        match self {
306            Self::Path(error) => write!(formatter, "{error}"),
307            Self::ExpectedCompoundRoot => write!(formatter, "NBT mutation replaced compound root"),
308            Self::InvalidWrittenNbt => write!(formatter, "mutated NBT could not be reborrowed"),
309        }
310    }
311}
312
313impl Error for StoreDataMutationError {}
314
315#[cfg(test)]
316mod tests {
317    use steel_utils::nbt::parse_nbt_path_argument;
318
319    use super::*;
320
321    #[test]
322    fn score_store_creates_and_updates_each_holder() {
323        let scoreboard = Scoreboard::new();
324        let Ok(objective) = scoreboard.add_objective("result") else {
325            panic!("objective should be created");
326        };
327        let holders = [ScoreHolder::new("one"), ScoreHolder::new("two")];
328
329        assert!(store_score_value(&scoreboard, &holders, &objective, 7).is_ok());
330        assert_eq!(scoreboard.score(&holders[0], &objective), Some(7));
331        assert_eq!(scoreboard.score(&holders[1], &objective), Some(7));
332    }
333
334    #[test]
335    fn stored_value_distinguishes_numeric_results_from_success() {
336        assert_eq!(stored_value(true, true, 17), 17);
337        assert_eq!(stored_value(true, false, 0), 0);
338        assert_eq!(stored_value(false, true, 17), 1);
339        assert_eq!(stored_value(false, false, 17), 0);
340    }
341
342    #[test]
343    fn data_types_match_java_numeric_narrowing() {
344        assert_eq!(StoreDataType::Byte.tag(128, 1.0), NbtTag::Byte(-128));
345        assert_eq!(StoreDataType::Byte.tag(i32::MAX, 1e20), NbtTag::Byte(-1));
346        assert_eq!(StoreDataType::Byte.tag(i32::MIN, 1e20), NbtTag::Byte(0));
347        assert_eq!(
348            StoreDataType::Short.tag(32_768, 1.0),
349            NbtTag::Short(-32_768)
350        );
351        assert_eq!(
352            StoreDataType::Int.tag(i32::MAX, 1e20),
353            NbtTag::Int(i32::MAX)
354        );
355        assert_eq!(
356            StoreDataType::Long.tag(i32::MAX, 1e20),
357            NbtTag::Long(i64::MAX)
358        );
359        assert_eq!(StoreDataType::Float.tag(3, 0.5), NbtTag::Float(1.5));
360        assert_eq!(StoreDataType::Double.tag(3, 0.5), NbtTag::Double(1.5));
361    }
362
363    #[test]
364    fn compound_path_mutation_creates_and_replaces_values() {
365        let (path, _) = parse_nbt_path_argument("result.value").expect("path should parse");
366        let data = mutate_compound_path(NbtCompound::new(), &path, NbtTag::Int(7))
367            .expect("path should create missing compounds");
368        let result = data
369            .compound("result")
370            .expect("result compound should exist");
371        assert_eq!(result.int("value"), Some(7));
372
373        let data = mutate_compound_path(data, &path, NbtTag::Byte(1))
374            .expect("path should replace existing value");
375        let result = data
376            .compound("result")
377            .expect("result compound should exist");
378        assert_eq!(result.byte("value"), Some(1));
379    }
380
381    #[test]
382    fn storage_mutation_reads_and_writes_the_current_compound() {
383        let storage = CommandStorage::new();
384        let key = Identifier::from_steel("store_test");
385        let (first_path, _) = parse_nbt_path_argument("first").expect("first path should parse");
386        let (second_path, _) = parse_nbt_path_argument("second").expect("second path should parse");
387
388        assert!(store_storage_data_value(&storage, &key, &first_path, NbtTag::Int(4)).is_ok());
389        assert!(
390            store_storage_data_value(&storage, &key, &second_path, NbtTag::Double(2.5)).is_ok()
391        );
392
393        let stored = storage.get(&key);
394        assert_eq!(stored.int("first"), Some(4));
395        assert_eq!(stored.double("second"), Some(2.5));
396    }
397}