Potent sulfur: find the mobs near a geyser from a grid

Every gassing geyser scanned every mob every 10 ticks, all on the same tick,
and every erupting one every tick. One grid of mob boxes is kept for the block
entity pass, until the mob list's next write; a geyser's own effects and
pushes move no mob, so the grid holds after them.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
bulkandClaude Opus 5.5 committed 2026-11-07 17:33:25 +00:00
1 parent 3613b40c6b
commit 2bad442a93
3 files changed
+84 -18

No files matched your search

+3
View File
@@ -3118,6 +3118,8 @@ struct Game<'a> {
mod_chunk_tickets: HashMap<Dim, HashMap<(i32, i32), std::collections::BTreeSet<String>>>,
/// Where item entities are, while the block entities tick (hoppers).
item_columns: Option<machines::ItemColumns>,
/// Where the mobs are, while the block entities tick (potent sulfur gas).
pass_mobs: Option<potent_sulfur::PassMobs>,
creative_screen: Creative,
/// `CreativeModeInventoryScreen.ignoreTextInput`: the chat key opened the
/// search tab, so the letter it carries is not typed into the box.
@@ -20551,6 +20553,7 @@ impl<'a> Game<'a> {
mod_container_viewers: HashMap::new(),
mod_chunk_tickets: HashMap::new(),
item_columns: None,
pass_mobs: None,
creative_screen: prepared.creative_screen,
creative_ignore_text: false,
control: None,
@@ -43,6 +43,7 @@ impl Game<'_> {
let mut spawners = Vec::new();
let mut trial_spawners = Vec::new();
self.item_columns = Some(Default::default());
self.pass_mobs = Some(Default::default());
for slot in self.simulation.world.block_entity_tickers().iter().copied() {
let pos = slot.0;
// Ticks before this one can remove it (`TickingBlockEntity.isRemoved`).
@@ -89,6 +90,7 @@ impl Game<'_> {
}
// Items move between passes: the columns hold for this one only.
self.item_columns = None;
self.pass_mobs = None;
self.update_song_volumes();
self.scan_for_hearts();
// The client ticks (particles, sounds, the turning mob) are for a
+79 -18
View File
@@ -13,7 +13,8 @@ use horizon_client::{
block_entities::Pos,
containers::BlockEntity,
effects::EffectInstance,
mobs::Mob,
join_seq::{JoinList, ListStamp},
mobs::{Mob, seen_grid::SeenGrid},
particles::{ParticleLevel, ParticleOptions, ParticleType as T},
potent_sulfur::{
can_be_reached_by_noxious_gas, find_noxious_gas_source_block, unobstructed_block_count,
@@ -26,6 +27,22 @@ use horizon_core::{
};
use horizon_world::{level::flags, registry::BlockStateId};
/// The mobs' boxes for one block entity pass, built when a geyser's gas first
/// looks for mobs and kept until the mob list's next write (a spawner's mob, a
/// launch), so every geyser of the pass reads its neighbours, not every mob.
#[derive(Default)]
pub(super) struct PassMobs {
grid: Option<(ListStamp, SeenGrid)>,
}
impl PassMobs {
/// The list was written without moving, adding or removing a mob.
fn wrote(&mut self, mobs: &JoinList<Mob>) {
if let Some((stamp, _)) = &mut self.grid {
*stamp = mobs.stamp();
}
}
}
/// `PotentSulfurBlockEntity.GEYSER_SALT`.
const GEYSER_SALT: i64 = -904_011_478;
/// `PotentSulfurBlockEntity.EFFECT_APPLICATION_FREQUENCY_TICKS`.
@@ -172,24 +189,58 @@ impl Game<'_> {
{
self.as_fight_player(player.who, |g| g.apply_nausea_effect_to_player(source));
}
let mut near = Vec::new();
if !self.mobs_in_pass(area, &mut near) {
near.extend(0..self.simulation.mobs.len() as u32);
}
let level = ParticleLevel {
world: &self.simulation.world,
facts: self.res.facts(),
collision: self.collision,
player: None,
};
let tags = &self.res.stations().enchantments.entity_tags;
for mob in self
.simulation
.mobs
.iter_mut()
.filter(|m| m.alive() && m.bounds().intersects(area))
{
let eye = [mob.position.x, mob.eye_y(), mob.position.z];
if can_be_reached_by_noxious_gas(&level, source, eye) {
apply_nausea_effect_to_mob(mob, tags);
}
// The gas reads blocks only, so the mobs it reaches can be found
// before any gets its effect.
near.retain(|&j| {
let mob = &self.simulation.mobs[j as usize];
mob.alive()
&& mob.bounds().intersects(area)
&& can_be_reached_by_noxious_gas(
&level,
source,
[mob.position.x, mob.eye_y(), mob.position.z],
)
});
if near.is_empty() {
return;
}
let tags = &self.res.stations().enchantments.entity_tags;
for j in near {
apply_nausea_effect_to_mob(&mut self.simulation.mobs[j as usize], tags);
}
// An effect moves no mob and changes no list: the grid still holds.
if let Some(pass) = self.pass_mobs.as_mut() {
pass.wrote(&self.simulation.mobs);
}
}
/// The slots of the mobs whose box may meet `area`, from a grid kept for
/// the block entity pass; false outside the pass: scan them all.
fn mobs_in_pass(&mut self, area: Aabb, out: &mut Vec<u32>) -> bool {
let Some(pass) = self.pass_mobs.as_mut() else {
return false;
};
let mobs = &self.simulation.mobs;
if pass
.grid
.as_ref()
.is_none_or(|(stamp, _)| *stamp != mobs.stamp())
{
pass.grid = Some((mobs.stamp(), SeenGrid::new(mobs.iter().map(Mob::bounds))));
}
pass.grid
.as_ref()
.is_some_and(|(_, grid)| grid.candidates(area, out))
}
/// `PotentSulfurBlockEntity.applyNauseaEffect` on the player acting (a
@@ -314,14 +365,18 @@ impl Game<'_> {
let area = launch_area(pos, height);
let limit = f64::from(0.3f32) + f64::from(water_blocks) * 0.1;
let push = f64::from(0.2f32);
let mut near = Vec::new();
if !self.mobs_in_pass(area, &mut near) {
near.extend(0..self.simulation.mobs.len() as u32);
}
near.retain(|&j| {
let m = &self.simulation.mobs[j as usize];
m.alive() && m.bounds().intersects(area)
});
let tags = &self.res.stations().enchantments.entity_tags;
let immune = tags.get("minecraft:not_affected_by_geysers");
for mob in self
.simulation
.mobs
.iter_mut()
.filter(|m| m.alive() && m.bounds().intersects(area))
{
for &j in &near {
let mob = &mut self.simulation.mobs[j as usize];
// Entity.checkFallDistanceAccumulation.
if mob.velocity.y > -0.5 && mob.fall_distance > 1.0 {
mob.fall_distance = 1.0;
@@ -333,6 +388,12 @@ impl Game<'_> {
mob.velocity.y += push;
}
}
// A push changes velocities, not boxes: the grid still holds.
if !near.is_empty()
&& let Some(pass) = self.pass_mobs.as_mut()
{
pass.wrote(&self.simulation.mobs);
}
for item in self
.simulation
.dropped