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