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