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