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 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;
}
_ => {}
}
}
}