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 player mod.rs
//! A connected player: the parts of `NetServerHandler` and `EntityPlayerMP` that talk to the client.
mod combat;
mod container;
mod interaction;
mod maps;
mod sleep;
mod stats;
mod survival;
mod travel;
use common::entity::{Entity, flags};
use common::inventory::{Container, ContainerKind, PlayerInventory};
use common::math::floor_f64;
use common::nbt::{Compound, Tag};
use common::protocol::{Connection, PACKETS_PER_TICK, Packet};
use common::text::ALLOWED_CHARACTERS;
use common::world::shape::BlockView;
use common::world::{ChunkPos, TileEntity};
use glam::DVec3;
use rustc_hash::{FxHashMap, FxHashSet};
use crate::entity::{Kind, ServerEntity};
use crate::level::Level;
pub use travel::Departure;
pub struct ServerPlayer {
pub inventory: PlayerInventory,
container: Container,
window: i8,
next_window: i8,
mining: Option<interaction::Mining>,
health_sent: i32,
died: bool,
pub(crate) spawn: [i32; 3],
/// The armor at the last [`Self::reconcile_armor`].
armor: [Option<common::item::ItemStack>; 4],
/// `spawnChunk`: the bed to respawn at.
bed_spawn: Option<[i32; 3]>,
/// The open container's blocks with the generation of their tile entities.
container_blocks: Vec<[i32; 3]>,
container_generations: Vec<Option<u64>>,
container_entity: Option<i32>,
map_tracking: FxHashMap<i16, maps::Tracking>,
minecart_start: Option<DVec3>,
last_action: Option<i16>,
last_contents: Vec<Option<common::item::ItemStack>>,
last_bars: [i16; 3],
connection: Connection,
pub name: String,
pub entity_id: i32,
/// Position before the last move, to reset to (`lastPosX`...).
last_pos: DVec3,
/// False until the client confirmed the last teleport.
has_moved: bool,
pub sent_chunks: FxHashSet<ChunkPos>,
pub(crate) stream: crate::chunks::StreamCursor,
pub disconnected: bool,
pub(crate) allow_flight: bool,
floating_packets: u32,
departure: Option<Departure>,
pub(crate) tracked: FxHashMap<i32, crate::entity::Tracked>,
/// The height above the feet that the stored `Pos` uses.
nbt_eye: f64,
/// Listed in `ops.txt`.
pub(crate) op: bool,
pub(crate) messages: Vec<Message>,
/// The entity flags last sent to this player about itself.
pub(crate) own_flags: u8,
}
impl ServerPlayer {
/// Spawns the player entity and sends the login sequence of `NetLoginHandler.doLogin`.
pub fn join(
connection: Connection,
name: &str,
level: &mut Level,
stored: Option<&Compound>,
spawn: [i32; 3],
nbt_eye: f64,
) -> Self {
let id = level.entities.allocate();
let mut entity = Entity::new_server_player(id, level.entities.random());
let [x, y, z] = spawn;
let feet = DVec3::new(f64::from(x) + 0.5, f64::from(y + 1), f64::from(z) + 0.5);
entity.set_location_and_angles(feet, 0.0, 0.0);
match stored {
Some(stored) => read_player_nbt(&mut entity, stored, nbt_eye),
None => {
if level.rules.dedicated_spawn.is_some() {
crate::spawn::multiplayer(level, &mut entity, spawn);
}
crate::spawn::prepare_player(level, &mut entity);
}
}
if level.rules.dedicated_spawn.is_some() {
entity.spawn_immunity = 60;
}
let bed_spawn = stored
.filter(|nbt| SPAWN_KEYS.iter().all(|key| nbt.contains(key)))
.map(|nbt| SPAWN_KEYS.map(|key| nbt.int(key)));
let mut player = Self {
inventory: stored.map_or_else(PlayerInventory::default, PlayerInventory::read),
container: Container::new(ContainerKind::Inventory),
window: 0,
next_window: 0,
mining: None,
health_sent: i32::MIN,
died: false,
spawn,
armor: [None; 4],
bed_spawn,
container_blocks: Vec::new(),
container_generations: Vec::new(),
container_entity: None,
map_tracking: Default::default(),
minecart_start: None,
last_action: None,
last_contents: Vec::new(),
last_bars: [-1; 3],
connection,
name: name.to_owned(),
entity_id: id,
last_pos: entity.pos,
has_moved: false,
sent_chunks: FxHashSet::default(),
stream: Default::default(),
disconnected: false,
allow_flight: true,
floating_packets: 0,
departure: None,
tracked: FxHashMap::default(),
nbt_eye,
op: false,
messages: Vec::new(),
own_flags: 0,
};
player.send(Packet::Login {
version_or_entity: id,
username: String::new(),
seed: level.seed(),
dimension: level.dimension(),
});
player.send(Packet::SpawnPosition { x, y, z });
player.send(Packet::UpdateTime { time: level.time });
if entity.sleep.sleeping {
sleep::leave_bed(level, &mut entity, true);
}
let (pos, yaw, pitch) = (entity.pos, entity.yaw, entity.pitch);
level.entities.insert(ServerEntity {
entity,
kind: Kind::Player {
name: name.to_owned(),
held: player.inventory.held(),
life: 0,
},
extra: stored.cloned().unwrap_or_default(),
});
player.reconcile_armor(level);
player.teleport(level, pos, yaw, pitch);
player.sync_container(level);
let health = player
.entity(level)
.and_then(|e| e.living.as_ref())
.map_or(20, |l| l.health as i16);
player.send(Packet::UpdateHealth { health });
player
}
/// `NetServerHandler.handleUpdateSign`: the text of a sign this player just placed, with
/// overlong or unprintable lines replaced.
fn update_sign(&mut self, level: &mut Level, [x, y, z]: [i32; 3], lines: [String; 4]) {
if !level.is_loaded(ChunkPos::containing(x, z)) {
return;
}
if level.editable_signs.get(&[x, y, z]) != Some(&self.entity_id) {
tracing::warn!(
"Player {} just tried to change non-editable sign",
self.name
);
return;
}
level.editable_signs.remove(&[x, y, z]);
let lines = lines.map(|line| {
let valid =
line.chars().count() <= 15 && line.chars().all(|c| ALLOWED_CHARACTERS.contains(c));
if valid { line } else { "!?".into() }
});
if let Some(TileEntity::Sign(text)) = level.tile_entity_mut(x, y, z) {
*text = lines.clone();
level.events.push(Packet::UpdateSign {
x,
y: y as i16,
z,
lines,
});
}
}
/// The rain flag for remote clients, then the full weather the local client uses.
pub fn send_weather(&mut self, level: &Level, was_raining: bool) {
if let Some(packet) = level.rain_packet(was_raining) {
self.send(packet);
}
self.send(level.weather_packet());
}
pub fn terrain_backlog(&self) -> usize {
self.connection.terrain_backlog()
}
pub fn send(&mut self, packet: Packet) {
if !self.connection.send(packet) {
self.disconnected = true;
}
}
pub fn entity<'a>(&self, level: &'a Level) -> Option<&'a Entity> {
level.entities.get(self.entity_id).map(|e| &e.entity)
}
/// The player's entity position, at the feet.
pub fn position(&self, level: &Level) -> DVec3 {
self.entity(level).map_or(self.last_pos, |e| e.pos)
}
/// `NetServerHandler.teleportTo`: moves the player and waits for the client to confirm.
pub fn teleport(&mut self, level: &mut Level, pos: DVec3, yaw: f32, pitch: f32) {
self.has_moved = false;
self.last_pos = pos;
if let Some(e) = level.entities.get_mut(self.entity_id) {
let previous = e.chunk();
e.entity.set_position_and_rotation(pos, yaw, pitch);
self.send(Packet::PlayerPositionLook {
x: pos.x,
y: pos.y + f64::from(1.62_f32),
stance: pos.y,
z: pos.z,
yaw,
pitch,
on_ground: false,
});
level.entities.moved(self.entity_id, previous);
}
}
pub fn receive(&mut self) -> Vec<Packet> {
let (packets, closed) = self.connection.receive_some(PACKETS_PER_TICK);
// A vanished transport leaves like a lost connection
self.disconnected |= closed;
packets
}
/// `NetServerHandler.handlePackets`.
pub fn handle(&mut self, level: &mut Level, packet: Packet) {
match packet {
Packet::UseEntity {
player,
target,
left_click,
} if player == self.entity_id => self.use_entity(level, target, left_click),
Packet::Respawn { .. } => self.respawn(level),
Packet::BlockDig {
status,
x,
y,
z,
face,
} => self.dig(level, status, [x, i32::from(y), z], face),
Packet::Place {
x, y, z, direction, ..
} => self.place(level, [x, i32::from(y), z], direction),
Packet::BlockItemSwitch { slot } if (0..9).contains(&slot) => {
self.inventory.selected = slot as usize
}
Packet::WindowClick {
window,
slot,
button,
action,
shift,
item,
} => self.click_container(level, window, slot, button, action, shift, item),
Packet::CloseWindow { window } if window == self.window => self.close_container(level),
Packet::Flying { on_ground } => self.handle_flying(level, None, None, on_ground),
Packet::PlayerPosition {
x,
y,
stance,
z,
on_ground,
} => self.handle_flying(level, Some((x, y, stance, z)), None, on_ground),
Packet::PlayerLook {
yaw,
pitch,
on_ground,
} => self.handle_flying(level, None, Some((yaw, pitch)), on_ground),
Packet::PlayerPositionLook {
x,
y,
stance,
z,
yaw,
pitch,
on_ground,
} => self.handle_flying(
level,
Some((x, y, stance, z)),
Some((yaw, pitch)),
on_ground,
),
Packet::EntityAction { action, .. } => {
if action == 3 {
self.wake_up(level, false, true);
self.has_moved = false;
} else if let Some(e) = level.entities.get_mut(self.entity_id)
&& (1..=2).contains(&action)
{
e.entity.set_flag(flags::SNEAKING, action == 1);
}
}
Packet::Animation { animation: 1, .. } => {
level.events.push(Packet::Animation {
entity: self.entity_id,
animation: 1,
});
}
Packet::Chat { message } => self.handle_chat(level, &message),
Packet::UpdateSign { x, y, z, lines } => {
self.update_sign(level, [x, i32::from(y), z], lines)
}
Packet::KeepAlive => {}
Packet::Disconnect { .. } => self.disconnected = true,
other => tracing::debug!("unhandled packet from {}: {other:?}", self.name),
}
self.reconcile_armor(level);
}
pub fn write_nbt(&self, level: &Level, nbt: &mut Compound) {
if let Some(entity) = self.entity(level) {
write_player_nbt(entity, nbt, self.nbt_eye);
nbt.set_int("Dimension", i32::from(level.dimension()));
self.inventory.write(nbt);
for (i, key) in SPAWN_KEYS.into_iter().enumerate() {
if let Some(spawn) = self.bed_spawn {
nbt.set_int(key, spawn[i]);
} else {
nbt.remove(key);
}
}
}
}
/// `NetServerHandler.handleFlying`: accepts the client's position once the last teleport
/// is confirmed. The client-side `y` is the feet and `stance` the eyes.
fn handle_flying(
&mut self,
level: &mut Level,
position: Option<(f64, f64, f64, f64)>,
look: Option<(f32, f32)>,
on_ground: bool,
) {
if position
.is_some_and(|(x, y, stance, z)| ![x, y, stance, z].iter().all(|v| v.is_finite()))
|| look.is_some_and(|(yaw, pitch)| !yaw.is_finite() || !pitch.is_finite())
{
return;
}
if !self.has_moved {
let Some((x, y, _, z)) = position else { return };
let dy = y - self.last_pos.y;
if x != self.last_pos.x || dy * dy >= 0.01 || z != self.last_pos.z {
return;
}
self.has_moved = true;
}
if let Some(mount) = self.entity(level).and_then(|e| e.riding) {
return self.ride(level, mount, position, look, on_ground);
}
// A riding packet sent before the client learned that its vehicle is gone
let position = position.filter(|&(_, y, stance, _)| !(y == -999.0 && stance == -999.0));
let Some(mut stored) = level.entities.take_for_tick(self.entity_id) else {
return;
};
let rejected = self.move_player(level, &mut stored.entity, position, look, on_ground);
let (pos, yaw, pitch) = (stored.entity.pos, stored.entity.yaw, stored.entity.pitch);
level.entities.finish_tick(stored);
match rejected {
Some(Rejection::Kick(reason)) => self.kick(reason),
Some(Rejection::Correct) => self.teleport(level, self.last_pos, yaw, pitch),
None if !self.sleeping(level) => self.last_pos = pos,
None => {}
}
}
/// The validated move of `NetServerHandler.handleFlying`: the server moves the player
/// itself, colliding with blocks and triggering walking effects, and rejects moves that
/// are too fast, end too far from the claimed position or enter blocks.
fn move_player(
&mut self,
level: &mut Level,
entity: &mut Entity,
position: Option<(f64, f64, f64, f64)>,
look: Option<(f32, f32)>,
on_ground: bool,
) -> Option<Rejection> {
if entity.living.as_ref().is_some_and(|l| l.health <= 0) {
return None;
}
if entity.sleep.sleeping {
entity.set_position_and_rotation(self.last_pos, entity.yaw, entity.pitch);
return None;
}
let start_y = entity.pos.y;
self.last_pos = entity.pos;
let mut target = entity.pos;
if let Some((x, y, stance, z)) = position {
let height = stance - y;
if !(0.1..=1.65).contains(&height) {
tracing::warn!("{} had an illegal stance: {height}", self.name);
return Some(Rejection::Kick("Illegal stance"));
}
if x.abs() > 3.2e7 || z.abs() > 3.2e7 {
return Some(Rejection::Kick("Illegal position"));
}
target = DVec3::new(x, y, z);
}
let (yaw, pitch) = look.unwrap_or((entity.yaw, entity.pitch));
// The living update that `onUpdateEntity` runs before each move resets falls on ladders
if entity.is_on_ladder(level) {
entity.fall_distance = 0.0;
}
entity.y_size = 0.0;
entity.set_position_and_rotation(self.last_pos, yaw, pitch);
let delta = target - entity.pos;
if delta.length_squared() > 100.0 {
tracing::warn!("{} moved too quickly!", self.name);
return Some(Rejection::Kick("You moved too quickly :( (Hacking?)"));
}
let inset = DVec3::splat(1.0 / 16.0);
let was_free = level.block_boxes(&entity.bb.deflate(inset)).is_empty();
entity.move_entity(level, delta);
entity.motion.y = delta.y;
// The original ignores the vertical error, since `dy > -0.5 || dy < 0.5` always holds
let miss = (target - entity.pos) * DVec3::new(1.0, 0.0, 1.0);
let wrong = miss.length_squared() > 1.0 / 16.0;
if wrong {
tracing::warn!("{} moved wrongly!", self.name);
}
entity.set_position_and_rotation(target, yaw, pitch);
let now_free = level.block_boxes(&entity.bb.deflate(inset)).is_empty();
if was_free && (wrong || !now_free) {
return Some(Rejection::Correct);
}
if !self.allow_flight {
let support = entity.bb.inflate(inset).expand_towards(DVec3::NEG_Y * 0.55);
let min = support.min.to_array().map(floor_f64);
let max = (support.max + DVec3::ONE).to_array().map(floor_f64);
let supported = (min[0]..max[0]).any(|x| {
(min[1]..max[1])
.any(|y| (min[2]..max[2]).any(|z| !BlockView::block(level, x, y, z).is_air()))
});
if supported {
self.floating_packets = 0;
} else if delta.y >= -0.03125 {
self.floating_packets += 1;
if self.floating_packets > 80 {
return Some(Rejection::Kick("Flying is not enabled on this server"));
}
}
}
if entity.in_water {
entity.fall_distance = 0.0;
}
entity.on_ground = on_ground;
entity.update_fall_state(level, entity.pos.y - start_y);
None
}
/// The riding part of `NetServerHandler.handleFlying`: a rider only reports its look and,
/// as a position with a height of -999, its steering motion.
fn ride(
&mut self,
level: &mut Level,
mount: i32,
position: Option<(f64, f64, f64, f64)>,
look: Option<(f32, f32)>,
on_ground: bool,
) {
let seat = level.entities.get(mount).map(|m| m.seat_position());
let Some(mut stored) = level.entities.take_for_tick(self.entity_id) else {
return;
};
let entity = &mut stored.entity;
if let Some(seat) = seat {
entity.set_position(seat + DVec3::Y * entity.riding_offset());
}
if let Some((yaw, pitch)) = look {
entity.set_rotation(yaw, pitch);
}
if let Some((x, _, _, z)) =
position.filter(|&(_, y, stance, _)| y == -999.0 && stance == -999.0)
{
entity.motion.x = x;
entity.motion.z = z;
}
entity.on_ground = on_ground;
self.last_pos = entity.pos;
level.entities.finish_tick(stored);
}
/// `EntityPlayerMP.mountEntity`.
pub(super) fn mount(&mut self, level: &mut Level, mount: Option<i32>) {
level.mount(self.entity_id, mount);
let Some(entity) = self.entity(level) else {
return;
};
let (riding, pos, yaw, pitch) = (entity.riding, entity.pos, entity.yaw, entity.pitch);
self.send(Packet::AttachEntity {
entity: self.entity_id,
vehicle: riding.unwrap_or(-1),
});
self.teleport(level, pos, yaw, pitch);
}
/// `NetServerHandler.handleChat`.
fn handle_chat(&mut self, level: &mut Level, message: &str) {
if message.encode_utf16().count() > 100 {
return self.kick("Chat message too long");
}
let message = java_trim(message);
if !message.chars().all(|ch| ALLOWED_CHARACTERS.contains(ch)) {
return self.kick("Illegal characters in chat");
}
if let Some(command) = message.strip_prefix('/') {
self.slash_command(level, message, command);
} else {
let message = format!("<{}> {message}", self.name);
tracing::info!("{message}");
self.messages.push(Message::All(message));
}
}
/// `NetServerHandler.handleSlashCommand`.
fn slash_command(&mut self, level: &mut Level, message: &str, command: &str) {
let lower = message.to_lowercase();
if lower.starts_with("/me ") {
let message = format!("* {} {}", self.name, after_word(message));
tracing::info!("{message}");
self.messages.push(Message::All(message));
} else if lower.starts_with("/kill") {
level.attack_entity(self.entity_id, None, 1000);
} else if lower.starts_with("/tell ") {
let words = java_split(message);
if words.len() >= 3 {
let text = after_word(after_word(message));
let message = format!("§7{} whispers {text}", self.name);
tracing::info!("{message} to {}", words[1]);
self.messages.push(Message::Tell {
to: words[1].into(),
message,
});
}
} else if self.op {
tracing::info!("{} issued server command: {command}", self.name);
self.messages.push(Message::Command(command.into()));
} else {
tracing::info!("{} tried command: {command}", self.name);
}
}
/// `NetServerHandler.kickPlayer`.
pub(crate) fn kick(&mut self, reason: &str) {
tracing::info!("disconnecting {}: {reason}", self.name);
self.send(Packet::Disconnect {
reason: reason.into(),
});
self.disconnected = true;
}
}
/// Chat for the server to deliver beyond this player's connection.
pub(crate) enum Message {
/// To every player on the server.
All(String),
/// `/tell`: to the named player, or an error back to the sender.
Tell { to: String, message: String },
/// An operator's server command.
Command(String),
}
/// `String.trim`.
pub(crate) fn java_trim(text: &str) -> &str {
text.trim_matches(|ch| ch <= ' ')
}
/// `String.split(" ")`, which drops trailing empty words.
pub(crate) fn java_split(text: &str) -> Vec<&str> {
let mut words: Vec<&str> = text.split(' ').collect();
while words.last().is_some_and(|word| word.is_empty()) {
words.pop();
}
words
}
/// `text.substring(text.indexOf(" ")).trim()`: what follows the first word.
pub(crate) fn after_word(text: &str) -> &str {
java_trim(text.find(' ').map_or(text, |space| &text[space..]))
}
/// Why the server refused a move.
enum Rejection {
Kick(&'static str),
/// Sent back to where it was (`teleportTo` the last position).
Correct,
}
const SPAWN_KEYS: [&str; 3] = ["SpawnX", "SpawnY", "SpawnZ"];
/// Singleplayer stores the player at eye height; the server's player and dedicated saves use
/// its feet.
pub const EYE: f64 = 1.62;
/// Restores what `EntityPlayer.readFromNBT` stores, as far as the server tracks it.
fn read_player_nbt(entity: &mut Entity, nbt: &Compound, eye: f64) {
let sleeping = nbt.bool("Sleeping");
if let [Tag::Double(x), Tag::Double(y), Tag::Double(z)] = *nbt.list("Pos") {
// Players are saved while lying down at their actual position
let pos = DVec3::new(x, if sleeping { y } else { y - eye }, z);
let (yaw, pitch) = match *nbt.list("Rotation") {
[Tag::Float(yaw), Tag::Float(pitch)] => (yaw, pitch),
_ => (0.0, 0.0),
};
entity.prev_pos = pos;
entity.last_tick_pos = pos;
entity.set_position(pos);
entity.set_rotation(yaw, pitch);
entity.prev_yaw = yaw;
entity.prev_pitch = pitch;
}
if let [Tag::Double(x), Tag::Double(y), Tag::Double(z)] = *nbt.list("Motion") {
entity.motion = DVec3::new(x, y, z).map(|v| if v.abs() > 10.0 { 0.0 } else { v });
}
entity.fall_distance = nbt.float("FallDistance");
entity.fire = i32::from(nbt.short("Fire"));
entity.air = i32::from(nbt.short("Air"));
entity.on_ground = nbt.bool("OnGround");
if sleeping {
entity.sleep.sleeping = true;
entity.sleep.timer = i32::from(nbt.short("SleepTimer"));
entity.sleep.bed = Some(entity.pos.to_array().map(floor_f64));
}
if let Some(living) = &mut entity.living {
living.health = if nbt.contains("Health") {
i32::from(nbt.short("Health"))
} else {
10
};
living.hurt_time = i32::from(nbt.short("HurtTime"));
living.death_time = i32::from(nbt.short("DeathTime"));
living.attack_time = i32::from(nbt.short("AttackTime"));
}
}
/// `EntityPlayer.writeToNBT`, merged into the stored compound so unported fields survive.
pub fn write_player_nbt(entity: &Entity, nbt: &mut Compound, eye: f64) {
let eye = if entity.sleep.sleeping { 0.0 } else { eye };
let y = entity.pos.y + f64::from(entity.y_size) + eye;
nbt.set_list(
"Pos",
[entity.pos.x, y, entity.pos.z].map(Tag::Double).to_vec(),
);
nbt.set_list("Motion", entity.motion.to_array().map(Tag::Double).to_vec());
let rotation = [entity.yaw, entity.pitch].map(Tag::Float);
nbt.set_list("Rotation", rotation.to_vec());
nbt.set_float("FallDistance", entity.fall_distance);
nbt.set_short("Fire", entity.fire as i16);
nbt.set_short("Air", entity.air as i16);
nbt.set_bool("OnGround", entity.on_ground);
if let Some(living) = &entity.living {
nbt.set_short("Health", living.health as i16);
nbt.set_short("HurtTime", living.hurt_time as i16);
nbt.set_short("DeathTime", living.death_time as i16);
nbt.set_short("AttackTime", living.attack_time as i16);
}
if !nbt.contains("Inventory") {
nbt.set_list("Inventory", Vec::new());
}
nbt.set_bool("Sleeping", entity.sleep.sleeping);
nbt.set_short("SleepTimer", entity.sleep.timer as i16);
}