alex/beta-minecraft-rust
Minecraft Beta 1.7.3 Rewrite
BRANCH main
SNAPSHOT Draw terrain GPU-driven from packed per-section buffers with GPU occlusion culling, add a render distance slider (2-128) with a quad/disk shape, cut chunk bookkeeping at high render distances, and fix a light underflow at y=0
DOWNLOAD
root crates server src entity spawning.rs
use common::biome::Biome;
use common::entity::mob::MobKind;
use common::entity::{Entity, EntityWorld, material_in};
use common::math::{floor_f32, floor_f64};
use common::physics::Aabb;
use common::world::light::LightKind;
use common::world::shape::BlockView;
use common::world::{Block, ChunkPos, Material, TileEntity, bed};
use glam::DVec3;
use super::{Kind, ServerEntity};
use crate::level::Level;
impl Level {
pub fn spawn_mobs(&mut self, centers: &[ChunkPos], spawn: [i32; 3]) {
let mut chunks = std::mem::take(&mut self.spawn_chunks);
chunks.extend(self.loaded_chunks().filter(|pos| {
centers
.iter()
.any(|center| (pos.x - center.x).abs().max((pos.z - center.z).abs()) <= 8)
}));
chunks.sort_unstable_by_key(|p| (p.x, p.z));
// Monsters, creatures and water creatures with their caps per 256 chunks
for (hostile, water, cap) in [(true, false, 70), (false, false, 15), (false, true, 5)] {
let allowed = if hostile {
self.difficulty != 0 && self.rules.spawn_monsters
} else {
self.has_sky() && self.rules.spawn_animals
};
if !allowed {
continue;
}
let count = self
.entities
.iter()
.filter(|e| match &e.kind {
Kind::Mob(mob) if water => mob.kind == MobKind::Squid,
Kind::Mob(mob) => mob.kind.hostile() == hostile && mob.kind != MobKind::Squid,
_ => false,
})
.count();
if count > cap * chunks.len() / 256 {
continue;
}
'chunk: for &chunk in &chunks {
let kind = if water {
MobKind::Squid
} else if !self.has_sky() {
if self.rand.next_int(20) < 10 {
MobKind::Ghast
} else {
MobKind::PigZombie
}
} else if hostile {
match self.rand.next_int(50) / 10 {
0 => MobKind::Spider,
1 => MobKind::Zombie,
2 => MobKind::Skeleton,
3 => MobKind::Creeper,
_ => MobKind::Slime,
}
} else {
let biome = match self.spawn_biomes.get(&chunk).copied() {
Some(biome) => biome,
None => {
let biome = self.generator().biomes(
chunk.min_block_x(),
chunk.min_block_z(),
1,
1,
)[0];
self.spawn_biomes.insert(chunk, biome);
biome
}
};
match self
.rand
.next_int(if matches!(biome, Biome::Forest | Biome::Taiga) {
42
} else {
40
}) {
0..12 => MobKind::Sheep,
12..22 => MobKind::Pig,
22..32 => MobKind::Chicken,
32..40 => MobKind::Cow,
_ => MobKind::Wolf,
}
};
let start = [
chunk.min_block_x() + self.rand.next_int(16),
self.rand.next_int(128),
chunk.min_block_z() + self.rand.next_int(16),
];
let medium = if water {
Material::Water
} else {
Material::Air
};
if BlockView::block(self, start[0], start[1], start[2]).material() != medium {
continue;
}
let group = match kind {
MobKind::Ghast => 1,
MobKind::Wolf => 8,
_ => 4,
};
let mut spawned = 0;
for _ in 0..3 {
let [mut x, mut y, mut z] = start;
for _ in 0..4 {
x += self.rand.next_int(6) - self.rand.next_int(6);
y += self.rand.next_int(1) - self.rand.next_int(1);
z += self.rand.next_int(6) - self.rand.next_int(6);
if !self.is_loaded(ChunkPos::containing(x, z)) {
continue;
}
let fits = if water {
BlockView::block(self, x, y, z).material() == Material::Water
&& !BlockView::block(self, x, y + 1, z).is_normal_cube()
} else {
self.can_spawn_at(x, y, z)
};
if !fits {
continue;
}
let pos = DVec3::new(f64::from(x) + 0.5, f64::from(y), f64::from(z) + 0.5);
if self.nearest_player(pos, 24.0).is_some()
|| pos.distance_squared(DVec3::from_array(spawn.map(f64::from))) < 576.0
{
continue;
}
let yaw = self.rand.next_float() * 360.0;
let mut entity = self.mob_entity(kind, pos, yaw);
if !self.mob_can_spawn(&mut entity.entity, kind, [x, y, z]) {
continue;
}
self.spawn_initialized(entity, pos);
spawned += 1;
if spawned >= group {
continue 'chunk;
}
}
}
}
}
chunks.clear();
self.spawn_chunks = chunks;
}
/// Adds a naturally spawned mob with `SpawnerAnimals.creatureSpecificInit`: a sheep's
/// fleece color, or the skeleton riding one in a hundred spiders.
fn spawn_initialized(&mut self, mut mob: ServerEntity, at: DVec3) {
let Kind::Mob(state) = &mut mob.kind else {
return;
};
let kind = state.kind;
if kind == MobKind::Sheep {
state.color = match self.rand.next_int(100) {
0..5 => 15,
5..10 => 7,
10..15 => 8,
15..18 => 12,
_ if self.rand.next_int(500) == 0 => 6,
_ => 0,
};
}
let (id, yaw) = (mob.entity.id, mob.entity.yaw);
self.spawn(mob);
if kind == MobKind::Spider && self.rand.next_int(100) == 0 {
let skeleton = self.mob_entity(MobKind::Skeleton, at, yaw);
let rider = skeleton.entity.id;
self.entities.insert(skeleton);
self.mount(rider, Some(id));
}
}
/// `SpawnerAnimals.canCreatureTypeSpawnAtLocation` for land creatures.
fn can_spawn_at(&self, x: i32, y: i32, z: i32) -> bool {
BlockView::block(self, x, y - 1, z).is_normal_cube()
&& !BlockView::block(self, x, y, z).is_normal_cube()
&& !BlockView::block(self, x, y, z).material().is_liquid()
&& !BlockView::block(self, x, y + 1, z).is_normal_cube()
}
/// `SpawnerAnimals.performSleepSpawning`: tries to spawn a monster that can walk up to each
/// sleeping player, returning the players it woke up.
pub fn sleep_spawn(&mut self, players: &[i32]) -> Vec<i32> {
const KINDS: [MobKind; 3] = [MobKind::Spider, MobKind::Zombie, MobKind::Skeleton];
let mut woken = Vec::new();
for &id in players {
let Some((pos, feet)) = self
.entities
.get(id)
.map(|e| (e.entity.pos, e.entity.bb.min.y))
else {
continue;
};
for _ in 0..20 {
let x = floor_f64(pos.x) + self.rand.next_int(32) - self.rand.next_int(32);
let z = floor_f64(pos.z) + self.rand.next_int(32) - self.rand.next_int(32);
let top = (floor_f64(pos.y) + self.rand.next_int(16) - self.rand.next_int(16))
.clamp(1, 128);
let kind = KINDS[self.rand.next_int(3) as usize];
let mut y = top;
while y > 2 && !BlockView::block(self, x, y - 1, z).is_normal_cube() {
y -= 1;
}
while !self.can_spawn_at(x, y, z) && y < top + 16 && y < 128 {
y += 1;
}
if y >= top + 16 || y >= 128 {
continue;
}
let at = DVec3::new(
f64::from(x as f32 + 0.5),
f64::from(y),
f64::from(z as f32 + 0.5),
);
let yaw = self.rand.next_float() * 360.0;
let mut mob = self.mob_entity(kind, at, yaw);
if !self.mob_can_spawn(&mut mob.entity, kind, [x, y, z]) {
continue;
}
let path =
super::path::find(self, &mob.entity, DVec3::new(pos.x, feet, pos.z), 32.0);
let half = f64::from(floor_f32(mob.entity.width + 1.0)) * 0.5;
let reaches = path.len() > 1
&& path.back().is_some_and(|end| {
(end.x - half - pos.x).abs() < 1.5
&& (end.y - pos.y).abs() < 1.5
&& (end.z - half - pos.z).abs() < 1.5
});
if !reaches {
continue;
}
let bed = pos.to_array().map(floor_f64);
let [sx, sy, sz] = bed::standing_spot(self, bed, 1).unwrap_or([x, y + 1, z]);
mob.entity.set_location_and_angles(
DVec3::new(
f64::from(sx as f32 + 0.5),
f64::from(sy),
f64::from(sz as f32 + 0.5),
),
0.0,
0.0,
);
self.living_sound(&mut mob.entity);
let at = mob.entity.pos;
self.spawn_initialized(mob, at);
woken.push(id);
break;
}
}
woken
}
/// `TileEntityMobSpawner.updateEntity`: spawns up to four mobs of its kind every 10 to 40
/// seconds while a player is within 16 blocks.
pub(crate) fn tick_spawner(&mut self, [x, y, z]: [i32; 3]) {
let center = DVec3::new(f64::from(x), f64::from(y), f64::from(z)) + 0.5;
if self.nearest_player(center, 16.0).is_none() {
return;
}
// The cage particles are the client's; keep the world random in step
for _ in 0..3 {
self.rand.next_float();
}
let Some(TileEntity::MobSpawner { entity_id, delay }) = self.tile_entity(x, y, z) else {
return;
};
let (kind, mut delay) = (MobKind::from_name(entity_id), *delay);
if delay == -1 {
delay = self.spawner_delay();
}
if delay > 0 {
delay -= 1;
} else if let Some(kind) = kind {
let area = Aabb::from_coords(
f64::from(x),
f64::from(y),
f64::from(z),
f64::from(x + 1),
f64::from(y + 1),
f64::from(z + 1),
)
.inflate(DVec3::new(8.0, 4.0, 8.0));
for _ in 0..4 {
let nearby = self
.entities
.iter()
.filter(|e| {
// Counted by class: zombie pigmen are zombies too
matches!(&e.kind, Kind::Mob(mob) if mob.kind.is_a(kind))
&& e.entity.bb.intersects(&area)
})
.count();
if nearby >= 6 {
delay = self.spawner_delay();
break;
}
let dx = (self.rand.next_double() - self.rand.next_double()) * 4.0;
let dy = self.rand.next_int(3) - 1;
let dz = (self.rand.next_double() - self.rand.next_double()) * 4.0;
let at = DVec3::new(f64::from(x) + dx, f64::from(y + dy), f64::from(z) + dz);
let yaw = self.rand.next_float() * 360.0;
let mut mob = self.mob_entity(kind, at, yaw);
let cell = [floor_f64(at.x), y + dy, floor_f64(at.z)];
if !self.mob_can_spawn(&mut mob.entity, kind, cell) {
continue;
}
for _ in 0..20 {
let mut offset = || f64::from(self.rand.next_float()) - 0.5;
let point = center + DVec3::new(offset(), offset(), offset()) * 2.0;
self.spawn_particle("smoke", point, DVec3::ZERO);
self.spawn_particle("flame", point, DVec3::ZERO);
}
self.explosion_particles(&mut mob.entity);
self.spawn(mob);
delay = self.spawner_delay();
}
} else {
return;
}
if let Some(TileEntity::MobSpawner { delay: stored, .. }) =
self.chunk_tile_entity_mut(x, y, z)
{
*stored = delay;
}
}
fn spawner_delay(&mut self) -> i16 {
200 + self.rand.next_int(600) as i16
}
/// `EntityLiving.spawnExplosionParticle`.
fn explosion_particles(&mut self, entity: &mut Entity) {
for _ in 0..20 {
let velocity =
DVec3::from_array(std::array::from_fn(|_| entity.rand.next_gaussian())) * 0.02;
let (width, height) = (entity.width, entity.height);
let x = entity.pos.x + f64::from(entity.rand.next_float() * width * 2.0)
- f64::from(width)
- velocity.x * 10.0;
let y = entity.pos.y + f64::from(entity.rand.next_float() * height) - velocity.y * 10.0;
let z = entity.pos.z + f64::from(entity.rand.next_float() * width * 2.0)
- f64::from(width)
- velocity.z * 10.0;
self.spawn_particle("explode", DVec3::new(x, y, z), velocity);
}
}
pub(crate) fn mob_can_spawn(
&self,
entity: &mut Entity,
kind: MobKind,
[x, y, z]: [i32; 3],
) -> bool {
if kind == MobKind::Slime {
let size = entity.width / 0.6;
let pos = ChunkPos::containing(x, z);
let seed = self
.seed()
.wrapping_add(i64::from(pos.x.wrapping_mul(pos.x).wrapping_mul(4987142)))
.wrapping_add(i64::from(pos.x.wrapping_mul(5947611)))
.wrapping_add(i64::from(pos.z.wrapping_mul(pos.z)).wrapping_mul(4392871))
.wrapping_add(i64::from(pos.z.wrapping_mul(389711)))
^ 987234911;
return (size < 1.5 || self.difficulty > 0)
&& entity.rand.next_int(10) == 0
&& common::JavaRandom::new(seed).next_int(10) == 0
&& entity.pos.y < 16.0;
}
let clear = |level: &Self, entity: &Entity| {
!level
.entities
.iter()
.any(|e| e.prevents_spawning() && e.entity.bb.intersects(&entity.bb))
};
if kind == MobKind::Squid {
return clear(self, entity);
}
if kind == MobKind::PigZombie && self.difficulty == 0 {
return false;
}
if kind == MobKind::Ghast && (self.difficulty == 0 || entity.rand.next_int(20) != 0) {
return false;
}
if kind.hostile() && !matches!(kind, MobKind::PigZombie | MobKind::Ghast) {
if self.difficulty == 0 {
return false;
}
if i32::from(self.saved_light(LightKind::Sky, x, y, z)) > entity.rand.next_int(32)
|| i32::from(if self.weather.is_thundering() {
self.daylight_with(x, y, z, 10)
} else {
self.daylight(x, y, z)
}) > entity.rand.next_int(8)
{
return false;
}
} else if !kind.hostile()
&& (BlockView::block(self, x, y - 1, z) != Block::GRASS
|| self
.saved_light(LightKind::Sky, x, y, z)
.max(self.saved_light(LightKind::Block, x, y, z))
<= 8)
{
return false;
}
self.block_boxes(&entity.bb).is_empty()
&& !material_in(self, &entity.bb, Material::Water)
&& !material_in(self, &entity.bb, Material::Lava)
&& clear(self, entity)
// `EntityCreature.getCanSpawnHere`: giants seek light, which monsters spawn without
&& (kind != MobKind::Giant || self.brightness_at(x, y, z) >= 0.5)
}
}