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.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}