1use 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 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}