Theme

alex/beta-minecraft-rust

Minecraft Beta 1.7.3 Rewrite

BRANCH main
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
Download file

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)
    }
}