steel_core/block_entity/entities/
potent_sulfur.rs1use std::sync::{Arc, Weak};
4
5use simdnbt::borrow::{BaseNbtCompound as BorrowedNbtCompound, NbtCompound as NbtCompoundView};
6use simdnbt::owned::NbtCompound;
7use steel_registry::blocks::block_state_ext::BlockStateExt as _;
8use steel_registry::blocks::properties::{BlockStateProperties, PotentSulfurState};
9use steel_registry::vanilla_block_entity_types;
10use steel_registry::vanilla_game_events;
11use steel_registry::{
12 REGISTRY, TaggedRegistryExt as _, vanilla_blocks, vanilla_entity_type_tags::EntityTypeTag,
13};
14use steel_utils::random::xoroshiro::Xoroshiro;
15use steel_utils::random::{PositionalRandom, Random, RandomSource, RandomSplitter};
16use steel_utils::types::UpdateFlags;
17use steel_utils::{
18 BlockPos, BlockStateId, DowncastType, DowncastTypeKey, WorldAabb, locks::SyncMutex,
19};
20
21use crate::behavior::{BLOCK_BEHAVIORS, BlockCollisionContext};
22use crate::block_entity::{BlockEntity, BlockEntityBase, BlockEntityLifecycleExt as _};
23use crate::fluid::FluidStateExt as _;
24use crate::world::World;
25
26const GEYSER_SALT: i64 = -904_011_478;
27const COUNTDOWN_FREQUENCY_TICKS: i64 = 20;
28const LAUNCH_FORCE: f64 = 0.2;
29const BASE_VELOCITY_THRESHOLD: f64 = 0.3;
30const VELOCITY_THRESHOLD_SCALE: f64 = 0.1;
31const MAX_WATER_BLOCKS_ABOVE: i32 = 4;
32const FORCE_HEIGHT_MULTIPLIER: i32 = 6;
33
34pub struct PotentSulfurBlockEntity {
36 base: BlockEntityBase,
37 sulfur: SyncMutex<PotentSulfurData>,
38}
39
40struct PotentSulfurData {
41 waiting_countdown: i32,
43 eruption_tick: i64,
45}
46
47unsafe impl DowncastType for PotentSulfurBlockEntity {
49 const TYPE_KEY: DowncastTypeKey = DowncastTypeKey::new("steel:block_entity/potent_sulfur");
50}
51
52impl PotentSulfurBlockEntity {
53 #[must_use]
55 pub fn new(world: Weak<World>, pos: BlockPos, state: BlockStateId) -> Self {
56 let eruption_tick = world.upgrade().map_or(-1, |w| w.game_time());
57 Self {
58 base: BlockEntityBase::new(
59 &vanilla_block_entity_types::POTENT_SULFUR,
60 world,
61 pos,
62 state,
63 ),
64 sulfur: SyncMutex::new(PotentSulfurData {
65 waiting_countdown: -1,
66 eruption_tick,
67 }),
68 }
69 }
70
71 pub fn reset_countdown(&self) {
73 self.sulfur.lock().waiting_countdown = -1;
74 }
75
76 pub fn set_eruption_tick(&self, eruption_tick: i64) {
78 self.sulfur.lock().eruption_tick = eruption_tick;
79 }
80
81 fn geyser_positional_rng(seed: i64, pos: BlockPos) -> Xoroshiro {
82 let mut base = Xoroshiro::from_seed((seed ^ GEYSER_SALT) as u64);
83 let RandomSplitter::Xoroshiro(splitter) = base.next_positional() else {
84 unreachable!("Xoroshiro always produces Xoroshiro splitter")
85 };
86 match splitter.at(pos.x(), pos.y(), pos.z()) {
87 RandomSource::Xoroshiro(r) => r,
88 RandomSource::Legacy(_) => {
89 unreachable!("XoroshiroSplitter::at always returns Xoroshiro")
90 }
91 }
92 }
93
94 fn is_geyser_passable(world: &World, pos: BlockPos, context: BlockCollisionContext) -> bool {
95 let state = world.get_block_state(pos);
96 if state.is_air() || state.get_block() == &vanilla_blocks::WATER {
97 return true;
98 }
99
100 let behavior = BLOCK_BEHAVIORS.get_behavior(state.get_block());
101 behavior
102 .get_collision_shape(state, world, pos, context)
103 .is_empty()
104 }
105
106 fn find_source_block(world: &World, origin: BlockPos) -> Option<BlockPos> {
107 let max_y = origin.y() + MAX_WATER_BLOCKS_ABOVE + 1;
108 let geyser_position_context =
109 BlockCollisionContext::position_context(f64::from(origin.y()));
110 let mut pos = BlockPos::new(origin.x(), origin.y() + 1, origin.z());
111
112 while pos.y() <= max_y {
113 let state = world.get_block_state(pos);
114 let fluid = state.get_fluid_state();
115 let is_water_source = fluid.is_source() && fluid.is_water();
116
117 if is_water_source
118 && (state.get_block() == &vanilla_blocks::WATER
119 || Self::is_geyser_passable(world, pos, geyser_position_context))
120 {
121 pos = BlockPos::new(pos.x(), pos.y() + 1, pos.z());
122 continue;
123 }
124
125 if state.is_air() || Self::is_geyser_passable(world, pos, geyser_position_context) {
126 return Some(pos);
127 }
128
129 break; }
131
132 None
133 }
134
135 fn unobstructed_block_count(world: &World, start: BlockPos, water_blocks: i32) -> i32 {
136 let max_height = FORCE_HEIGHT_MULTIPLIER * water_blocks;
137 let geyser_position_context =
138 BlockCollisionContext::position_context(f64::from(start.y() - 1));
139 for i in 0..max_height {
140 let check = BlockPos::new(start.x(), start.y() + i, start.z());
141 if !Self::is_geyser_passable(world, check, geyser_position_context) {
142 return i;
143 }
144 }
145 max_height
146 }
147
148 fn tick_countdown(
149 &self,
150 world: &World,
151 pos: BlockPos,
152 state: BlockStateId,
153 ) -> Option<(BlockStateId, bool)> {
154 let source = Self::find_source_block(world, pos)?;
155 let water_blocks = source.y() - pos.y() - 1;
156 let mut rng = Self::geyser_positional_rng(world.seed(), pos);
157 let game_time = world.game_time();
158 let mut sulfur = self.sulfur.lock();
159
160 if sulfur.waiting_countdown <= 0 {
161 let current_state = state.get_value(&BlockStateProperties::POTENT_SULFUR_STATE);
162
163 sulfur.waiting_countdown = if current_state == PotentSulfurState::Dormant {
164 10 * (water_blocks - 1) + rng.next_i32_between(15, 30)
165 } else {
166 rng.next_i32();
167 (water_blocks - 1) + rng.next_i32_between(1, 2)
168 };
169 }
170
171 if sulfur.waiting_countdown > 0 {
172 sulfur.waiting_countdown -= 1;
173 }
174
175 if sulfur.waiting_countdown == 0 {
176 let current_state = state.get_value(&BlockStateProperties::POTENT_SULFUR_STATE);
177 let next_state = if current_state == PotentSulfurState::Dormant {
178 PotentSulfurState::Erupting
179 } else {
180 PotentSulfurState::Dormant
181 };
182 let deactivates = next_state == PotentSulfurState::Dormant;
183 let activates = next_state == PotentSulfurState::Erupting;
184 let new_state = state.set_value(&BlockStateProperties::POTENT_SULFUR_STATE, next_state);
185 if activates {
186 sulfur.eruption_tick = game_time;
187 }
188 return Some((new_state, deactivates));
189 }
190
191 None
192 }
193
194 fn tick_launch(world: &Arc<World>, pos: BlockPos) {
195 let Some(source) = Self::find_source_block(world, pos) else {
196 return;
197 };
198
199 let water_blocks = source.y() - pos.y() - 1;
200 let above = BlockPos::new(pos.x(), pos.y() + 1, pos.z());
201 let force_height = Self::unobstructed_block_count(world, above, water_blocks);
202
203 let aabb = WorldAabb::new(
204 f64::from(pos.x()),
205 f64::from(pos.y() + 1),
206 f64::from(pos.z()),
207 f64::from(pos.x() + 1),
208 f64::from(pos.y() + 1) + f64::from(force_height),
209 f64::from(pos.z() + 1),
210 );
211
212 let velocity_threshold =
213 BASE_VELOCITY_THRESHOLD + f64::from(water_blocks) * VELOCITY_THRESHOLD_SCALE;
214
215 for entity in world.get_entities_in_aabb(&aabb) {
216 if !entity.is_alive() || entity.is_spectator() {
217 continue;
218 }
219 let vel = entity.velocity();
220 entity.check_fall_distance_accumulation();
221
222 if !entity.can_simulate_movement() {
223 continue;
224 }
225 if entity.is_flying_player() {
226 continue;
227 }
228 if entity.is_passenger() {
229 continue;
230 }
231 if REGISTRY.entity_types.is_in_tag(
232 entity.entity_type(),
233 &EntityTypeTag::NOT_AFFECTED_BY_GEYSERS,
234 ) {
235 continue;
236 }
237 if vel.y >= velocity_threshold {
238 continue;
239 }
240
241 entity.set_velocity(glam::DVec3::new(vel.x, vel.y + LAUNCH_FORCE, vel.z));
242 entity.mark_velocity_sync();
243 }
244 }
245}
246
247impl BlockEntity for PotentSulfurBlockEntity {
248 fn base(&self) -> &BlockEntityBase {
249 &self.base
250 }
251
252 fn load_additional(&self, nbt: &BorrowedNbtCompound<'_>) {
253 let nbt: NbtCompoundView<'_, '_> = nbt.into();
254 if let Some(countdown) = nbt.int("countdown") {
255 self.sulfur.lock().waiting_countdown = countdown;
256 }
257 }
258
259 fn save_additional(&self, nbt: &mut NbtCompound) {
260 nbt.insert("countdown", self.sulfur.lock().waiting_countdown);
261 }
262
263 fn tick(&self, world: &Arc<World>) {
264 let pos = self.get_block_pos();
265 let state = world.get_block_state(pos);
266 if state.get_block() != &vanilla_blocks::POTENT_SULFUR {
267 self.set_removed();
268 return;
269 }
270
271 let current = state.get_value(&BlockStateProperties::POTENT_SULFUR_STATE);
272
273 if current == PotentSulfurState::Dry {
274 return;
275 }
276
277 let game_time = world.game_time();
278
279 let action = if matches!(
282 ¤t,
283 PotentSulfurState::Dormant | PotentSulfurState::Erupting
284 ) && game_time % COUNTDOWN_FREQUENCY_TICKS == 0
285 {
286 self.tick_countdown(world, pos, state)
287 } else {
288 None
289 };
290
291 if matches!(
292 ¤t,
293 PotentSulfurState::Erupting | PotentSulfurState::Continuous
294 ) {
295 Self::tick_launch(world, pos);
296 }
297
298 if let Some((new_state, deactivates)) = action {
299 world.set_block(pos, new_state, UpdateFlags::UPDATE_ALL);
300 if deactivates {
301 world.game_event(
302 &vanilla_game_events::BLOCK_DEACTIVATE,
303 pos,
304 &crate::world::game_event::GameEventContext::new(None, Some(state)),
305 );
306 }
307 }
308 }
309}