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 / mod.rs

//! Server-side entities: storage in load order and the world as entities see it.

mod explosion;
mod fireball;
mod fishing;
mod items;
mod lightning;
mod mobs;
mod nbt;
mod painting;
mod path;
mod projectiles;
mod spawning;
mod tracking;
mod vehicles;
pub use fireball::Fireball;
pub use fishing::Hook;
pub use lightning::Lightning;
pub use mobs::Mob;
pub use projectiles::{Arrow, Stuck, Thrown};
pub(crate) use tracking::{Tracked, velocity_packet};
pub use vehicles::{Boat, Cart};

use std::sync::Arc;
use std::sync::atomic::{AtomicI32, Ordering};

use common::JavaRandom;
use common::entity::mob::MobKind;
use common::entity::vehicle::VehicleKind;
use common::entity::{Entity, EntityWorld, Living, material_in};
use common::item::ItemStack;
use common::math::floor_f64;
use common::nbt::Compound;
use common::physics::Aabb;
use common::protocol::Packet;
use common::world::chunk::{self, HEIGHT};
use common::world::shape::{self, BlockView};
use common::world::{Block, ChunkPos, Material, TileEntity};
use glam::DVec3;
use rustc_hash::FxHashMap;

use crate::level::Level;

/// What an entity is, standing in for the `Entity` subclasses.
#[derive(Clone, Debug)]
pub enum Kind {
    Player {
        name: String,
        held: Option<ItemStack>,
        /// Respawns so far: observers recreate the player for each new life.
        life: u32,
    },
    Item {
        stack: ItemStack,
        age: i32,
        pickup_delay: i32,
        health: i32,
    },
    Falling {
        block: Block,
        age: i32,
    },
    Mob(Mob),
    Painting {
        painting: common::entity::painting::Painting,
        check: u32,
    },
    Arrow(Arrow),
    Fireball(Fireball),
    Hook(Hook),
    Boat(Boat),
    Cart(Cart),
    Thrown(Thrown),
    Tnt {
        fuse: i32,
    },
}

#[derive(Clone, Debug)]
pub struct ServerEntity {
    pub entity: Entity,
    pub kind: Kind,
    pub extra: Compound,
}

/// `World.loadedEntityList` with lookup by id.
pub struct Entities {
    map: FxHashMap<i32, ServerEntity>,
    order: Vec<i32>,
    cells: FxHashMap<ChunkPos, Vec<i32>>,
    /// `Entity.nextEntityID`, shared by all dimensions.
    next_id: Arc<AtomicI32>,
    seeds: JavaRandom,
}

impl Level {
    /// `Entity.mountEntity`: `rider` gets on `mount`, or off when it already rides it or
    /// `mount` is `None`.
    pub fn mount(&mut self, rider: i32, mount: Option<i32>) {
        let Some(current) = self.entities.get(rider).map(|e| e.entity.riding) else {
            return;
        };
        let target = mount.filter(|&mount| current != Some(mount));
        if let Some(old) = current {
            let top = self.entities.get_mut(old).map(|m| {
                if m.entity.rider == Some(rider) {
                    m.entity.rider = None;
                }
                DVec3::new(
                    m.entity.pos.x,
                    m.entity.bb.min.y + f64::from(m.entity.height),
                    m.entity.pos.z,
                )
            });
            if target.is_none()
                && let Some(top) = top
                && let Some(mut stored) = self.entities.take_for_tick(rider)
            {
                let (yaw, pitch) = (stored.entity.yaw, stored.entity.pitch);
                stored.entity.set_location_and_angles(top, yaw, pitch);
                self.entities.finish_tick(stored);
            }
        }
        if let Some(mount) = target
            && let Some(previous) = self.entities.get(mount).and_then(|m| m.entity.rider)
            && let Some(other) = self.entities.get_mut(previous)
        {
            other.entity.riding = None;
        }
        if let Some(e) = self.entities.get_mut(rider) {
            e.entity.riding = target;
            e.entity.rider_turn = [0.0; 2];
        }
        if let Some(mount) = target.and_then(|id| self.entities.get_mut(id)) {
            mount.entity.rider = Some(rider);
        }
    }

    /// Adds a new entity and marks its chunk for saving.
    pub fn spawn(&mut self, stored: ServerEntity) {
        self.mark_entity_chunk(stored.chunk());
        self.entities.insert(stored);
    }

    /// The part of the entity's box in water, in fifths, with the box lowered by `shift`.
    pub(super) fn submerged(&self, entity: &Entity, shift: f64) -> f64 {
        let (bb, mut water) = (entity.bb, 0.0);
        for slice in 0..5 {
            let y = |slice: i32| bb.min.y + (bb.max.y - bb.min.y) * f64::from(slice) / 5.0 - shift;
            let bounds = Aabb::new(bb.min.with_y(y(slice)), bb.max.with_y(y(slice + 1)));
            if material_in(self, &bounds, Material::Water) {
                water += 0.2;
            }
        }
        water
    }

    pub(super) fn damage_body(&mut self, entity: &mut Entity, amount: i32) -> bool {
        if entity.spawn_immunity > 0 {
            return false;
        }
        if entity.sleep.sleeping && entity.living.as_ref().is_some_and(|l| l.health > 0) {
            entity.sleep.disturbed = true;
        }
        if entity.is_player() {
            self.add_stat(entity.id, 2021, amount);
        }
        let Some(fresh) = entity.hurt_fresh(amount) else {
            return false;
        };
        if let Some(living) = &entity.living {
            let dead = living.health <= 0;
            for (status, send) in [(2, fresh), (3, dead)] {
                if send {
                    let entity = entity.id;
                    self.events.push(Packet::EntityStatus { entity, status });
                }
            }
            // Extra damage while still immune makes no sound
            let sound = entity
                .mob
                .map_or(Some("random.hurt"), |kind| kind.sound(true, dead))
                .filter(|_| fresh);
            if let Some(sound) = sound {
                let pitch = (entity.rand.next_float() - entity.rand.next_float()) * 0.2 + 1.0;
                let volume = entity.mob.map_or(1.0, |kind| kind.volume());
                self.play_sound(entity, sound, volume, pitch);
            }
        }
        true
    }
}

impl Entities {
    pub fn new(seed: i64, ids: Arc<AtomicI32>) -> Self {
        Self {
            map: FxHashMap::default(),
            order: Vec::new(),
            cells: FxHashMap::default(),
            next_id: ids,
            seeds: JavaRandom::new(seed),
        }
    }

    /// Reserves the next entity id.
    pub fn allocate(&mut self) -> i32 {
        self.next_id.fetch_add(1, Ordering::Relaxed)
    }

    /// A random generator for a new entity's `rand`.
    pub fn random(&mut self) -> JavaRandom {
        JavaRandom::new(self.seeds.next_long())
    }

    /// A new entity at the origin, sized and flagged by its `Entity` subclass constructor.
    pub fn create(&mut self, kind: Kind) -> ServerEntity {
        let vehicle = match &kind {
            Kind::Boat(_) => Some(VehicleKind::Boat),
            Kind::Cart(cart) => Some(cart.kind),
            _ => None,
        };
        let ([width, height], y_offset) = match &kind {
            Kind::Item { .. } => ([0.25; 2], 0.125),
            Kind::Thrown(_) | Kind::Hook(_) => ([0.25; 2], 0.0),
            Kind::Painting { .. } | Kind::Arrow(_) => ([0.5; 2], 0.0),
            Kind::Fireball(_) => ([1.0; 2], 0.0),
            Kind::Falling { .. } | Kind::Tnt { .. } => ([0.98; 2], 0.49),
            Kind::Boat(_) => (VehicleKind::Boat.size(), VehicleKind::Boat.y_offset()),
            Kind::Cart(cart) => (cart.kind.size(), cart.kind.y_offset()),
            Kind::Mob(mob) => (mob.kind.size(), 0.0),
            Kind::Player { .. } => unreachable!("players are created by their connections"),
        };
        let mut entity = Entity::new(self.allocate(), width, height, self.random());
        entity.y_offset = y_offset;
        entity.full_water_box = matches!(kind, Kind::Item { .. });
        entity.vehicle = vehicle;
        entity.triggers_walking = false;
        if let Kind::Mob(mob) = &kind {
            entity.mob = Some(mob.kind);
            entity.triggers_walking = !matches!(mob.kind, MobKind::Spider | MobKind::Wolf);
            entity.eye = (mob.kind == MobKind::Wolf).then_some(0.64);
            entity.step_height = 0.5;
            entity.living = Some(Living {
                move_speed: mob.kind.speed(),
                ..Living::new(mob.kind.health())
            });
        }
        ServerEntity {
            entity,
            kind,
            extra: Compound::default(),
        }
    }

    pub fn insert(&mut self, entity: ServerEntity) {
        let id = entity.entity.id;
        self.remove(id);
        self.cells.entry(entity.chunk()).or_default().push(id);
        self.map.insert(id, entity);
        self.order.push(id);
    }

    pub fn remove(&mut self, id: i32) -> Option<ServerEntity> {
        let entity = self.take_for_tick(id)?;
        self.order.retain(|&other| other != id);
        Some(entity)
    }

    pub fn get(&self, id: i32) -> Option<&ServerEntity> {
        self.map.get(&id)
    }

    pub fn get_mut(&mut self, id: i32) -> Option<&mut ServerEntity> {
        self.map.get_mut(&id)
    }

    pub fn moved(&mut self, id: i32, previous: ChunkPos) {
        let Some(current) = self.get(id).map(ServerEntity::chunk) else {
            return;
        };
        if previous != current {
            self.unlink(previous, id);
            self.cells.entry(current).or_default().push(id);
        }
    }

    fn unlink(&mut self, pos: ChunkPos, id: i32) {
        if let Some(cell) = self.cells.get_mut(&pos) {
            cell.retain(|&other| other != id);
            if cell.is_empty() {
                self.cells.remove(&pos);
            }
        }
    }

    pub fn extend_ids(&self, out: &mut Vec<i32>) {
        out.extend_from_slice(&self.order);
    }

    pub fn iter(&self) -> impl Iterator<Item = &ServerEntity> {
        self.order.iter().filter_map(|id| self.map.get(id))
    }

    pub fn in_chunk(&self, pos: ChunkPos) -> impl Iterator<Item = &ServerEntity> {
        self.cells
            .get(&pos)
            .into_iter()
            .flatten()
            .filter_map(|id| self.map.get(id))
    }

    /// The entities in the chunks within two blocks of `area`, the candidates of
    /// `World.getEntitiesWithinAABB`.
    pub fn near(&self, area: &Aabb) -> impl Iterator<Item = &ServerEntity> {
        let [x0, z0] = [area.min.x, area.min.z].map(|v| floor_f64(v - 2.0) >> 4);
        let [x1, z1] = [area.max.x, area.max.z].map(|v| floor_f64(v + 2.0) >> 4);
        (x0..=x1).flat_map(move |x| (z0..=z1).flat_map(move |z| self.in_chunk(ChunkPos::new(x, z))))
    }

    pub fn take_for_tick(&mut self, id: i32) -> Option<ServerEntity> {
        let entity = self.map.remove(&id)?;
        self.unlink(entity.chunk(), id);
        Some(entity)
    }

    pub fn finish_tick(&mut self, entity: ServerEntity) {
        let id = entity.entity.id;
        if entity.entity.dead && !matches!(entity.kind, Kind::Player { .. }) {
            self.order.retain(|&other| other != id);
        } else {
            self.cells.entry(entity.chunk()).or_default().push(id);
            self.map.insert(id, entity);
        }
    }
}

impl ServerEntity {
    /// `preventEntitySpawning` of a live entity: nothing may be placed or spawned inside it.
    pub fn prevents_spawning(&self) -> bool {
        !self.entity.dead
            && matches!(
                self.kind,
                Kind::Player { .. }
                    | Kind::Mob(_)
                    | Kind::Falling { .. }
                    | Kind::Boat(_)
                    | Kind::Cart(_)
                    | Kind::Tnt { .. }
            )
    }

    /// `canBeCollidedWith`: what projectiles and players' clicks can hit.
    pub fn can_be_collided(&self) -> bool {
        !self.entity.dead
            && !matches!(
                self.kind,
                Kind::Item { .. } | Kind::Arrow(_) | Kind::Hook(_) | Kind::Thrown(_)
            )
    }

    pub fn is_minecart(&self) -> bool {
        matches!(self.kind, Kind::Cart(_))
    }

    /// `getMountedYOffset`.
    pub fn mounted_y_offset(&self) -> f64 {
        match &self.kind {
            Kind::Boat(_) | Kind::Cart(_) => -f64::from(0.3_f32),
            Kind::Mob(mob) => mob.kind.mounted_y_offset(),
            _ => f64::from(self.entity.height) * 0.75,
        }
    }

    pub fn seat_position(&self) -> DVec3 {
        self.entity.vehicle.map_or(
            self.entity.pos + DVec3::Y * self.mounted_y_offset(),
            |kind| kind.seat(self.entity.pos, self.entity.yaw),
        )
    }

    pub fn chunk(&self) -> ChunkPos {
        ChunkPos::containing(floor_f64(self.entity.pos.x), floor_f64(self.entity.pos.z))
    }
}

impl BlockView for Level {
    /// Block reads for physics, which never load chunks.
    fn block(&self, x: i32, y: i32, z: i32) -> Block {
        self.peek(x, y, z)
            .map_or(Block::AIR, |(chunk, index)| chunk.blocks().get(index))
    }

    fn metadata(&self, x: i32, y: i32, z: i32) -> u8 {
        self.peek(x, y, z)
            .map_or(0, |(chunk, index)| chunk.metadata.get(index))
    }

    fn tile_entity(&self, x: i32, y: i32, z: i32) -> Option<&TileEntity> {
        Level::tile_entity(self, x, y, z)
    }
}

impl Level {
    fn peek(&self, x: i32, y: i32, z: i32) -> Option<(&common::world::Chunk, usize)> {
        if !(0..HEIGHT as i32).contains(&y) {
            return None;
        }
        let chunk = self.chunk(ChunkPos::containing(x, z))?;
        Some((
            chunk,
            chunk::index((x & 15) as usize, y as usize, (z & 15) as usize),
        ))
    }

    /// `World.getCollidingBoundingBoxes` over blocks only.
    pub fn block_boxes(&self, region: &Aabb) -> Vec<Aabb> {
        let mut boxes = Vec::new();
        let (x0, x1) = (floor_f64(region.min.x), floor_f64(region.max.x + 1.0));
        let (y0, y1) = (floor_f64(region.min.y), floor_f64(region.max.y + 1.0));
        let (z0, z1) = (floor_f64(region.min.z), floor_f64(region.max.z + 1.0));
        for x in x0..x1 {
            for z in z0..z1 {
                if !self.is_loaded(ChunkPos::containing(x, z)) {
                    continue;
                }
                for y in y0 - 1..y1 {
                    let block = BlockView::block(self, x, y, z);
                    if !block.is_air() {
                        shape::collision_boxes(self, block, x, y, z, region, &mut boxes);
                    }
                }
            }
        }
        boxes
    }
}

impl EntityWorld for Level {
    fn is_remote(&self) -> bool {
        false
    }

    fn chunks_exist(&self, min: [i32; 3], max: [i32; 3]) -> bool {
        Level::chunks_exist(self, (min[0], min[1], min[2]), (max[0], max[1], max[2]))
    }

    fn colliding_boxes(&self, entity: &Entity, region: &Aabb) -> Vec<Aabb> {
        let mut boxes = self.block_boxes(region);
        boxes.extend(
            self.entities
                .near(region)
                .filter(|other| {
                    let id = Some(other.entity.id);
                    !other.entity.dead
                        && ![entity.riding, entity.rider, Some(entity.id)].contains(&id)
                        && (matches!(other.kind, Kind::Boat(_)) || entity.vehicle.is_some())
                        && other.entity.bb.intersects(region)
                })
                .map(|other| other.entity.bb),
        );
        boxes
    }

    fn rained_on(&self, x: i32, y: i32, z: i32) -> bool {
        Level::rained_on(self, x, y, z)
    }

    fn play_sound(&mut self, entity: &Entity, sound: &str, volume: f32, pitch: f32) {
        self.events.push(Packet::Sound {
            name: sound.into(),
            pos: entity.pos.to_array(),
            volume,
            pitch,
        });
    }

    /// Players hear their own steps on the client, which moves them first.
    fn step_sound(&mut self, entity: &Entity, sound: &str, volume: f32, pitch: f32) {
        if entity.mob.is_some() || entity.living.is_none() {
            self.play_sound(entity, sound, volume, pitch);
        }
    }

    fn damage(&mut self, entity: &mut Entity, amount: i32) {
        let accepted = self.damage_body(entity, amount);
        if entity.mob.is_some() {
            self.unattributed_damage.push((entity.id, accepted));
        }
    }

    fn living_sound(&mut self, entity: &mut Entity) {
        if let Some(kind) = entity.mob
            && let Some(sound) = kind.sound(false, false)
        {
            let pitch = (entity.rand.next_float() - entity.rand.next_float()) * 0.2 + 1.0;
            self.play_sound(entity, sound, kind.volume(), pitch);
        }
    }

    fn spawn_particle(&mut self, name: &str, pos: DVec3, velocity: DVec3) {
        self.events.push(Packet::Particle {
            name: name.into(),
            pos: pos.to_array(),
            velocity: velocity.to_array(),
        });
    }

    fn on_walked(&mut self, block: Block, [x, y, z]: [i32; 3], _entity: &mut Entity) {
        match block {
            Block::FARMLAND if self.rand.next_int(4) == 0 => {
                self.set_block_notify(x, y, z, Block::DIRT);
            }
            Block::REDSTONE_ORE | Block::LIT_REDSTONE_ORE => {
                crate::blocks::glow_redstone_ore(self, x, y, z)
            }
            _ => {}
        }
    }

    fn award_achievement(&mut self, player: i32, index: usize) {
        self.achievement(player, index);
    }

    fn rider_fell(&mut self, rider: i32, distance: f32) {
        self.rider_falls.push((rider, distance));
    }

    fn on_collided(&mut self, block: Block, [x, y, z]: [i32; 3], entity: &mut Entity) {
        match block {
            Block::STONE_PRESSURE_PLATE | Block::WOODEN_PRESSURE_PLATE
                if BlockView::metadata(self, x, y, z) != 1 =>
            {
                crate::blocks::update_plate(self, block, x, y, z, Some(entity));
            }
            Block::CACTUS => self.damage(entity, 1),
            Block::DETECTOR_RAIL if BlockView::metadata(self, x, y, z) & 8 == 0 => {
                crate::blocks::detector_touched(self, [x, y, z], entity);
            }
            Block::WEB => entity.in_web = true,
            Block::PORTAL => entity.touch_portal(),
            Block::SOUL_SAND => {
                entity.motion.x *= 0.4;
                entity.motion.z *= 0.4;
            }
            _ => {}
        }
    }
}