1use std::sync::Arc;
2
3use rand::{Rng, RngExt};
4use steel_macros::block_behavior;
5use steel_registry::{
6 blocks::{
7 BlockRef,
8 block_state_ext::BlockStateExt,
9 properties::{BlockStateProperties, EnumProperty, IntProperty},
10 },
11 item_stack::ItemStack,
12 items::ItemRef,
13};
14use steel_utils::{BlockPos, BlockStateId, Direction, Identifier, types::UpdateFlags};
15
16use crate::{
17 behavior::{
18 BlockBehavior, BlockPlaceContext,
19 blocks::vegetation::{
20 Vegetation,
21 crop_block::{CROP_GROWTH_CHANCE_BASE, crop_growth_speed},
22 default_surviving_state,
23 vegetation_block::{survival_update_shape, vegetation_can_survive},
24 },
25 },
26 world::{LevelAccessor, LevelReader, ScheduledTickAccess, World},
27};
28
29const AGE: &IntProperty = &BlockStateProperties::AGE_7;
30const FACING: &EnumProperty<Direction> = &BlockStateProperties::HORIZONTAL_FACING;
31const MAX_AGE: u8 = 7;
32
33#[block_behavior]
35pub struct StemBlock {
36 block: BlockRef,
37 #[json_arg(vanilla_blocks)]
38 fruit: BlockRef,
39 #[json_arg(vanilla_blocks)]
40 attached_stem: BlockRef,
41 #[json_arg(vanilla_items)]
42 seed: ItemRef,
43 #[json_arg(vanilla_block_tags)]
44 stem_support_blocks: Identifier,
45 #[json_arg(vanilla_block_tags)]
46 fruit_support_blocks: Identifier,
47}
48
49impl StemBlock {
50 #[must_use]
52 pub const fn new(
53 block: BlockRef,
54 fruit: BlockRef,
55 attached_stem: BlockRef,
56 seed: ItemRef,
57 stem_support_blocks: Identifier,
58 fruit_support_blocks: Identifier,
59 ) -> Self {
60 Self {
61 block,
62 fruit,
63 attached_stem,
64 seed,
65 stem_support_blocks,
66 fruit_support_blocks,
67 }
68 }
69
70 fn age_after_bonemeal(age: u8, increase: u8) -> u8 {
71 age.saturating_add(increase).min(MAX_AGE)
72 }
73
74 fn random_tick_with_rng(
75 &self,
76 state: BlockStateId,
77 world: &dyn LevelAccessor,
78 pos: BlockPos,
79 rng: &mut dyn Rng,
80 ) {
81 if world.raw_brightness(pos, 0) < 9 {
82 return;
83 }
84
85 let growth_speed = crop_growth_speed(self.block, world, pos);
86 let growth_chance = (CROP_GROWTH_CHANCE_BASE / growth_speed) as u32 + 1;
87 if rng.random_range(0..growth_chance) != 0 {
88 return;
89 }
90
91 let age = state.get_value(AGE);
92 if age < MAX_AGE {
93 world.set_block_state(
94 pos,
95 state.set_value(AGE, age + 1),
96 UpdateFlags::UPDATE_CLIENTS,
97 );
98 return;
99 }
100
101 let direction = Direction::HORIZONTAL[rng.random_range(0..Direction::HORIZONTAL.len())];
102 let fruit_pos = pos.relative(direction);
103 if !world.get_block_state(fruit_pos).is_air()
104 || !world
105 .get_block_state(fruit_pos.below())
106 .get_block()
107 .has_tag(&self.fruit_support_blocks)
108 {
109 return;
110 }
111
112 world.set_block_state(
113 fruit_pos,
114 self.fruit.default_state(),
115 UpdateFlags::UPDATE_ALL,
116 );
117 world.set_block_state(
118 pos,
119 self.attached_stem
120 .default_state()
121 .set_value(FACING, direction),
122 UpdateFlags::UPDATE_ALL,
123 );
124 }
125
126 fn perform_bonemeal_with_rng(
127 &self,
128 state: BlockStateId,
129 world: &dyn LevelAccessor,
130 rng: &mut dyn Rng,
131 pos: BlockPos,
132 ) {
133 let age = state.get_value(AGE);
134 let new_age = Self::age_after_bonemeal(age, rng.random_range(2..=5));
135 let new_state = state.set_value(AGE, new_age);
136 world.set_block_state(pos, new_state, UpdateFlags::UPDATE_CLIENTS);
137
138 if new_age == MAX_AGE {
139 self.random_tick_with_rng(new_state, world, pos, rng);
140 }
141 }
142}
143
144impl BlockBehavior for StemBlock {
145 fn get_state_for_placement(&self, context: &BlockPlaceContext<'_>) -> Option<BlockStateId> {
146 default_surviving_state(self.block, self, context)
147 }
148
149 fn can_survive(&self, state: BlockStateId, world: &dyn LevelReader, pos: BlockPos) -> bool {
150 vegetation_can_survive(self, state, world, pos)
151 }
152
153 fn update_shape(
154 &self,
155 state: BlockStateId,
156 world: &dyn ScheduledTickAccess,
157 pos: BlockPos,
158 _direction: Direction,
159 _neighbor_pos: BlockPos,
160 _neighbor_state: BlockStateId,
161 ) -> BlockStateId {
162 survival_update_shape(self, state, world, pos)
163 }
164
165 fn random_tick(&self, state: BlockStateId, world: &Arc<World>, pos: BlockPos) {
166 self.random_tick_with_rng(state, world, pos, &mut rand::rng());
167 }
168
169 fn get_clone_item_stack(
170 &self,
171 _block: BlockRef,
172 _state: BlockStateId,
173 _include_data: bool,
174 ) -> Option<ItemStack> {
175 Some(ItemStack::new(self.seed))
176 }
177
178 fn as_bonemealable(&self) -> Option<&dyn super::bonemealable::Bonemealable> {
179 Some(self)
180 }
181}
182
183impl Vegetation for StemBlock {
184 fn may_place_on(&self, state: BlockStateId, _world: &dyn LevelReader, _pos: BlockPos) -> bool {
185 state.get_block().has_tag(&self.stem_support_blocks)
186 }
187}
188
189impl super::bonemealable::Bonemealable for StemBlock {
190 fn get_bonemeal_age_increase(&self, _world: &Arc<World>, rng: &mut dyn Rng) -> u8 {
191 rng.random_range(2..=5)
192 }
193
194 fn is_valid_bonemeal_target(
195 &self,
196 state: BlockStateId,
197 _world: &dyn LevelReader,
198 _pos: BlockPos,
199 ) -> bool {
200 state.get_value(AGE) != MAX_AGE
201 }
202
203 fn perform_bonemeal(
204 &self,
205 state: BlockStateId,
206 world: &Arc<World>,
207 rng: &mut dyn Rng,
208 pos: BlockPos,
209 ) {
210 self.perform_bonemeal_with_rng(state, world, rng, pos);
211 }
212}
213
214#[cfg(test)]
215mod tests {
216 use std::convert::Infallible;
217
218 use rand::TryRng;
219 use steel_registry::{
220 init_vanilla_registry, vanilla_block_tags::BlockTag, vanilla_blocks, vanilla_items,
221 };
222
223 use crate::{chunk::light::MAX_LIGHT_LEVEL, test_support::TestLevel};
224
225 use super::super::bonemealable::Bonemealable;
226 use super::*;
227
228 #[derive(Default)]
229 struct ZeroRng;
230
231 impl TryRng for ZeroRng {
232 type Error = Infallible;
233
234 fn try_next_u32(&mut self) -> Result<u32, Self::Error> {
235 Ok(0)
236 }
237
238 fn try_next_u64(&mut self) -> Result<u64, Self::Error> {
239 Ok(0)
240 }
241
242 fn try_fill_bytes(&mut self, dst: &mut [u8]) -> Result<(), Self::Error> {
243 dst.fill(0);
244 Ok(())
245 }
246 }
247
248 fn pumpkin_stem() -> StemBlock {
249 StemBlock::new(
250 &vanilla_blocks::PUMPKIN_STEM,
251 &vanilla_blocks::PUMPKIN,
252 &vanilla_blocks::ATTACHED_PUMPKIN_STEM,
253 &vanilla_items::PUMPKIN_SEEDS,
254 BlockTag::SUPPORTS_PUMPKIN_STEM,
255 BlockTag::SUPPORTS_PUMPKIN_STEM_FRUIT,
256 )
257 }
258
259 fn melon_stem() -> StemBlock {
260 StemBlock::new(
261 &vanilla_blocks::MELON_STEM,
262 &vanilla_blocks::MELON,
263 &vanilla_blocks::ATTACHED_MELON_STEM,
264 &vanilla_items::MELON_SEEDS,
265 BlockTag::SUPPORTS_MELON_STEM,
266 BlockTag::SUPPORTS_MELON_STEM_FRUIT,
267 )
268 }
269
270 #[test]
271 fn survival_uses_extracted_support_without_a_light_requirement() {
272 init_vanilla_registry();
273 let stem = pumpkin_stem();
274 let state = vanilla_blocks::PUMPKIN_STEM.default_state();
275 let farmland = TestLevel::default()
276 .with_block(
277 BlockPos::ZERO.below(),
278 vanilla_blocks::FARMLAND.default_state(),
279 )
280 .with_raw_brightness(0);
281 let dirt = TestLevel::default()
282 .with_block(BlockPos::ZERO.below(), vanilla_blocks::DIRT.default_state())
283 .with_raw_brightness(MAX_LIGHT_LEVEL);
284
285 assert!(stem.can_survive(state, &farmland, BlockPos::ZERO));
286 assert!(!stem.can_survive(state, &dirt, BlockPos::ZERO));
287 }
288
289 #[test]
290 fn random_growth_requires_light_nine_and_advances_one_age() {
291 init_vanilla_registry();
292 let stem = pumpkin_stem();
293 let state = vanilla_blocks::PUMPKIN_STEM
294 .default_state()
295 .set_value(AGE, 3);
296 let dark = TestLevel::default()
297 .with_block(
298 BlockPos::ZERO.below(),
299 vanilla_blocks::FARMLAND.default_state(),
300 )
301 .with_raw_brightness(8);
302 stem.random_tick_with_rng(state, &dark, BlockPos::ZERO, &mut ZeroRng);
303 assert!(dark.placed_blocks.borrow().is_empty());
304
305 let bright = TestLevel::default()
306 .with_block(
307 BlockPos::ZERO.below(),
308 vanilla_blocks::FARMLAND.default_state(),
309 )
310 .with_raw_brightness(9);
311 stem.random_tick_with_rng(state, &bright, BlockPos::ZERO, &mut ZeroRng);
312 let placed = bright.placed_blocks.borrow();
313 assert_eq!(placed.len(), 1);
314 assert_eq!(placed[0].state.get_value(AGE), 4);
315 assert_eq!(placed[0].flags, UpdateFlags::UPDATE_CLIENTS);
316 }
317
318 #[test]
319 fn mature_stems_place_their_own_fruit_and_attached_family() {
320 init_vanilla_registry();
321
322 for (stem, mature_state, fruit, attached) in [
323 (
324 pumpkin_stem(),
325 vanilla_blocks::PUMPKIN_STEM
326 .default_state()
327 .set_value(AGE, MAX_AGE),
328 &vanilla_blocks::PUMPKIN,
329 &vanilla_blocks::ATTACHED_PUMPKIN_STEM,
330 ),
331 (
332 melon_stem(),
333 vanilla_blocks::MELON_STEM
334 .default_state()
335 .set_value(AGE, MAX_AGE),
336 &vanilla_blocks::MELON,
337 &vanilla_blocks::ATTACHED_MELON_STEM,
338 ),
339 ] {
340 let fruit_pos = BlockPos::ZERO.north();
341 let level = TestLevel::default()
342 .with_block(
343 BlockPos::ZERO.below(),
344 vanilla_blocks::FARMLAND.default_state(),
345 )
346 .with_block(fruit_pos.below(), vanilla_blocks::DIRT.default_state())
347 .with_raw_brightness(9);
348 stem.random_tick_with_rng(mature_state, &level, BlockPos::ZERO, &mut ZeroRng);
349
350 let placed = level.placed_blocks.borrow();
351 assert_eq!(placed.len(), 2);
352 assert_eq!(placed[0].pos, fruit_pos);
353 assert_eq!(placed[0].state.get_block(), fruit);
354 assert_eq!(placed[0].flags, UpdateFlags::UPDATE_ALL);
355 assert_eq!(placed[1].pos, BlockPos::ZERO);
356 assert_eq!(placed[1].state.get_block(), attached);
357 assert_eq!(placed[1].state.get_value(FACING), Direction::North);
358 assert_eq!(placed[1].flags, UpdateFlags::UPDATE_ALL);
359 }
360 }
361
362 #[test]
363 fn mature_growth_requires_air_and_fruit_support_and_does_not_scan() {
364 init_vanilla_registry();
365 let stem = pumpkin_stem();
366 let mature = vanilla_blocks::PUMPKIN_STEM
367 .default_state()
368 .set_value(AGE, MAX_AGE);
369
370 let unsupported = TestLevel::default()
371 .with_block(
372 BlockPos::ZERO.below(),
373 vanilla_blocks::FARMLAND.default_state(),
374 )
375 .with_block(
376 BlockPos::ZERO.north().below(),
377 vanilla_blocks::STONE.default_state(),
378 )
379 .with_raw_brightness(9);
380 stem.random_tick_with_rng(mature, &unsupported, BlockPos::ZERO, &mut ZeroRng);
381 assert!(unsupported.placed_blocks.borrow().is_empty());
382
383 let blocked_north = TestLevel::default()
384 .with_block(
385 BlockPos::ZERO.below(),
386 vanilla_blocks::FARMLAND.default_state(),
387 )
388 .with_block(
389 BlockPos::ZERO.north(),
390 vanilla_blocks::STONE.default_state(),
391 )
392 .with_block(
393 BlockPos::ZERO.east().below(),
394 vanilla_blocks::DIRT.default_state(),
395 )
396 .with_block(
397 BlockPos::ZERO.south().below(),
398 vanilla_blocks::DIRT.default_state(),
399 )
400 .with_block(
401 BlockPos::ZERO.west().below(),
402 vanilla_blocks::DIRT.default_state(),
403 )
404 .with_raw_brightness(9);
405 stem.random_tick_with_rng(mature, &blocked_north, BlockPos::ZERO, &mut ZeroRng);
406 assert!(blocked_north.placed_blocks.borrow().is_empty());
407 }
408
409 #[test]
410 fn bonemeal_bounds_age_and_runs_the_mature_tick_with_the_same_rng() {
411 init_vanilla_registry();
412 let stem = pumpkin_stem();
413 assert_eq!(StemBlock::age_after_bonemeal(0, 2), 2);
414 assert_eq!(StemBlock::age_after_bonemeal(6, 5), MAX_AGE);
415
416 let mature = vanilla_blocks::PUMPKIN_STEM
417 .default_state()
418 .set_value(AGE, MAX_AGE);
419 assert!(!stem.is_valid_bonemeal_target(mature, &TestLevel::default(), BlockPos::ZERO));
420
421 let state = vanilla_blocks::PUMPKIN_STEM
422 .default_state()
423 .set_value(AGE, 5);
424 let fruit_pos = BlockPos::ZERO.north();
425 let level = TestLevel::default()
426 .with_block(
427 BlockPos::ZERO.below(),
428 vanilla_blocks::FARMLAND.default_state(),
429 )
430 .with_block(fruit_pos.below(), vanilla_blocks::DIRT.default_state())
431 .with_raw_brightness(9);
432 stem.perform_bonemeal_with_rng(state, &level, &mut ZeroRng, BlockPos::ZERO);
433
434 let placed = level.placed_blocks.borrow();
435 assert_eq!(placed.len(), 3);
436 assert_eq!(placed[0].state.get_value(AGE), MAX_AGE);
437 assert_eq!(placed[0].flags, UpdateFlags::UPDATE_CLIENTS);
438 assert_eq!(placed[1].state.get_block(), &vanilla_blocks::PUMPKIN);
439 assert_eq!(
440 placed[2].state.get_block(),
441 &vanilla_blocks::ATTACHED_PUMPKIN_STEM
442 );
443 }
444
445 #[test]
446 fn clone_items_match_each_stem_family() {
447 init_vanilla_registry();
448 let pumpkin = pumpkin_stem()
449 .get_clone_item_stack(
450 &vanilla_blocks::PUMPKIN_STEM,
451 vanilla_blocks::PUMPKIN_STEM.default_state(),
452 false,
453 )
454 .expect("pumpkin stem has a clone item");
455 let melon = melon_stem()
456 .get_clone_item_stack(
457 &vanilla_blocks::MELON_STEM,
458 vanilla_blocks::MELON_STEM.default_state(),
459 false,
460 )
461 .expect("melon stem has a clone item");
462
463 assert_eq!(pumpkin.item(), &*vanilla_items::PUMPKIN_SEEDS);
464 assert_eq!(melon.item(), &*vanilla_items::MELON_SEEDS);
465 }
466
467 #[test]
468 fn extracted_constructor_mappings_cover_both_families() {
469 let classes: serde_json::Value =
470 serde_json::from_str(include_str!("../../../../build/classes.json"))
471 .expect("extracted classes.json is valid JSON");
472 let blocks = classes["blocks"]
473 .as_array()
474 .expect("classes.json contains blocks");
475
476 let expected = [
477 (
478 "pumpkin_stem",
479 "pumpkin",
480 "attached_pumpkin_stem",
481 "pumpkin_seeds",
482 "supports_pumpkin_stem",
483 "supports_pumpkin_stem_fruit",
484 ),
485 (
486 "melon_stem",
487 "melon",
488 "attached_melon_stem",
489 "melon_seeds",
490 "supports_melon_stem",
491 "supports_melon_stem_fruit",
492 ),
493 ];
494
495 for (name, fruit, attached, seed, stem_support, fruit_support) in expected {
496 let block = blocks
497 .iter()
498 .find(|block| block["name"] == name)
499 .expect("stem mapping exists");
500 assert_eq!(block["fruit"], fruit);
501 assert_eq!(block["attached_stem"], attached);
502 assert_eq!(block["seed"], seed);
503 assert_eq!(block["stem_support_blocks"], stem_support);
504 assert_eq!(block["fruit_support_blocks"], fruit_support);
505 }
506 }
507}