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 / client / src / player.rs

//! The local player: ports of `EntityPlayerSP`, `EntityClientPlayerMP` and the camera parts
//! of `EntityRenderer`.

use std::f32::consts::PI;

use common::JavaRandom;
use common::entity::player::{self, MovementKeys, PlayerState};
use common::entity::{Entity, flags};
use common::math::{self, floor_f64};
use common::protocol::{Connection, Packet};
use common::world::shape::{self, BlockView};
use glam::{DVec3, Mat4, Vec3};

use crate::world::ClientWorld;

/// Where the vehicle seats its rider this tick, and how much it turned.
#[derive(Clone, Copy)]
pub struct Mount {
    pub seat: DVec3,
    pub turn: [f32; 2],
}

pub struct LocalPlayer {
    pub entity: Entity,
    pub keys: MovementKeys,
    pub state: PlayerState,
    // `EntityClientPlayerMP.sendMotionUpdates` state.
    old_pos: DVec3,
    old_min_y: f64,
    old_yaw: f32,
    old_pitch: f32,
    was_on_ground: bool,
    was_sneaking: bool,
    time_since_moved: i32,
    /// Whether the server placed the player yet.
    pub started: bool,
    pub third_person: bool,
    pub view_bobbing: bool,
}

impl LocalPlayer {
    pub fn new(seed: i64) -> Self {
        Self {
            entity: Entity::new_player(0, JavaRandom::new(seed)),
            keys: MovementKeys::default(),
            state: PlayerState::default(),
            old_pos: DVec3::ZERO,
            old_min_y: 0.0,
            old_yaw: 0.0,
            old_pitch: 0.0,
            was_on_ground: false,
            was_sneaking: false,
            time_since_moved: 0,
            started: false,
            third_person: false,
            view_bobbing: true,
        }
    }

    /// `NetClientHandler.handleFlying`: the server moved the player; confirm the new position.
    pub fn teleport(
        &mut self,
        connection: &Connection,
        x: f64,
        eye_y: f64,
        z: f64,
        yaw: f32,
        pitch: f32,
    ) {
        let e = &mut self.entity;
        e.y_size = 0.0;
        e.motion = DVec3::ZERO;
        e.set_position_and_rotation(DVec3::new(x, eye_y, z), yaw, pitch);
        connection.send(Packet::PlayerPositionLook {
            x: e.pos.x,
            y: e.bb.min.y,
            stance: e.pos.y,
            z: e.pos.z,
            yaw,
            pitch,
            on_ground: false,
        });
        if !self.started {
            e.prev_pos = e.pos;
            e.last_tick_pos = e.pos;
            self.started = true;
        }
    }

    /// `EntityClientPlayerMP.onUpdate`: one tick of the player, then its motion packets.
    /// `pushers` are the entities touching the player, which push it away: ids and positions.
    pub fn tick(
        &mut self,
        world: &mut ClientWorld,
        connection: &Connection,
        pushers: &[(i32, DVec3)],
    ) {
        let e = &self.entity;
        if !self.started || !world.block_exists(floor_f64(e.pos.x), 64, floor_f64(e.pos.z)) {
            return;
        }
        self.entity.last_tick_pos = self.entity.pos;
        self.entity
            .set_flag(flags::RIDING, self.entity.riding.is_some());
        let portal = &mut self.entity.portal;
        if portal.inside && portal.time == 0.0 {
            let pitch = self.entity.rand.next_float() * 0.4 + 0.8;
            world.sound_effects.push(("portal.trigger", 1.0, pitch));
        }
        self.entity.portal.tick();
        player::tick(&mut self.entity, &mut self.state, self.keys, world);
        // `EntityLiving.onLivingUpdate` pushes the player away from what it touches
        for &(id, position) in pushers {
            if let Some(push) = self.entity.collision_push(id, position, 0.0) {
                self.entity.motion += push;
            }
        }
        self.send_motion_updates(connection);
    }

    /// `EntityPlayer.updateRidden`: the tick of a rider, which then sits on its mount's `seat`.
    pub fn tick_ridden(
        &mut self,
        world: &mut ClientWorld,
        connection: &Connection,
        mount: Option<Mount>,
    ) {
        let Some(Mount { seat, turn }) = mount else {
            self.entity.riding = None;
            return;
        };
        self.entity.motion = DVec3::ZERO;
        self.tick(world, connection, &[]);
        if self.entity.riding.is_some() {
            self.entity.follow_mount(seat, turn);
        }
        self.state.prev_camera_yaw = self.state.camera_yaw;
        self.state.camera_yaw = 0.0;
    }

    /// `Entity.mountEntity` for an `AttachEntity` packet: gets on `vehicle`, or off onto `top`,
    /// the top of the current vehicle.
    pub fn attach(&mut self, vehicle: Option<i32>, top: Option<DVec3>) {
        let e = &mut self.entity;
        if vehicle.is_some() && vehicle != e.riding {
            e.riding = vehicle;
        } else {
            if e.riding.is_some()
                && let Some(top) = top
            {
                let (yaw, pitch) = (e.yaw, e.pitch);
                e.set_location_and_angles(top, yaw, pitch);
            }
            e.riding = None;
        }
        e.rider_turn = [0.0; 2];
    }

    /// `EntityClientPlayerMP.sendMotionUpdates`.
    fn send_motion_updates(&mut self, connection: &Connection) {
        let e = &self.entity;
        let sneaking = e.is_sneaking();
        if sneaking != self.was_sneaking {
            connection.send(Packet::EntityAction {
                entity: e.id,
                action: if sneaking { 1 } else { 2 },
            });
            self.was_sneaking = sneaking;
        }
        if e.riding.is_some() {
            // The original drops the look while turning; riders always send it
            connection.send(Packet::PlayerPositionLook {
                x: e.motion.x,
                y: -999.0,
                stance: -999.0,
                z: e.motion.z,
                yaw: e.yaw,
                pitch: e.pitch,
                on_ground: e.on_ground,
            });
            self.was_on_ground = e.on_ground;
            self.old_yaw = e.yaw;
            self.old_pitch = e.pitch;
            return;
        }
        let dx = e.pos.x - self.old_pos.x;
        let dmin_y = e.bb.min.y - self.old_min_y;
        let dy = e.pos.y - self.old_pos.y;
        let dz = e.pos.z - self.old_pos.z;
        let dyaw = f64::from(e.yaw - self.old_yaw);
        let dpitch = f64::from(e.pitch - self.old_pitch);
        let moved = dmin_y != 0.0 || dy != 0.0 || dx != 0.0 || dz != 0.0;
        let turned = dyaw != 0.0 || dpitch != 0.0;
        let packet = match (moved, turned) {
            (true, true) => Packet::PlayerPositionLook {
                x: e.pos.x,
                y: e.bb.min.y,
                stance: e.pos.y,
                z: e.pos.z,
                yaw: e.yaw,
                pitch: e.pitch,
                on_ground: e.on_ground,
            },
            (true, false) => Packet::PlayerPosition {
                x: e.pos.x,
                y: e.bb.min.y,
                stance: e.pos.y,
                z: e.pos.z,
                on_ground: e.on_ground,
            },
            (false, true) => Packet::PlayerLook {
                yaw: e.yaw,
                pitch: e.pitch,
                on_ground: e.on_ground,
            },
            (false, false) => Packet::Flying {
                on_ground: e.on_ground,
            },
        };
        if moved || turned {
            self.time_since_moved = 0;
        } else if self.was_on_ground == e.on_ground && self.time_since_moved <= 200 {
            self.time_since_moved += 1;
        } else {
            self.time_since_moved = 0;
        }
        connection.send(packet);
        self.was_on_ground = e.on_ground;
        if moved {
            self.old_pos = e.pos;
            self.old_min_y = e.bb.min.y;
        }
        if turned {
            self.old_yaw = e.yaw;
            self.old_pitch = e.pitch;
        }
    }

    /// The interpolated eye position, which is lower when lying in bed.
    pub fn eye(&self, partial: f32) -> DVec3 {
        let e = &self.entity;
        let partial = f64::from(partial);
        let pos = e.prev_pos + (e.pos - e.prev_pos) * partial;
        let lowered = if e.sleep.sleeping { 1.0 } else { 0.0 };
        pos - DVec3::new(0.0, f64::from(e.y_offset - 1.62 + lowered), 0.0)
    }

    /// The direction of the bed the living player sleeps in (`getBedOrientationInDegrees`).
    pub fn bed_direction(&self, world: &impl BlockView) -> Option<usize> {
        let e = &self.entity;
        let [x, y, z] = e.sleep.bed.filter(|_| {
            e.sleep.sleeping && !e.dead && e.living.as_ref().is_some_and(|l| l.health > 0)
        })?;
        Some(common::world::bed::direction(world.metadata(x, y, z)))
    }

    fn camera_distance(&self, world: &impl BlockView, partial: f32) -> f32 {
        if !self.third_person {
            return 0.1;
        }
        let e = &self.entity;
        let eye = self.eye(partial);
        let yaw = e.prev_yaw + (e.yaw - e.prev_yaw) * partial;
        let pitch = e.prev_pitch + (e.pitch - e.prev_pitch) * partial;
        let behind = -common::interaction::look(yaw, pitch) * 4.0;
        let mut distance = 4.0_f64;
        for corner in 0..8 {
            let offset = DVec3::new(
                f64::from((corner & 1) * 2 - 1),
                f64::from((corner >> 1 & 1) * 2 - 1),
                f64::from((corner >> 2 & 1) * 2 - 1),
            ) * f64::from(0.1_f32);
            let start = eye + offset;
            if let Some(hit) = shape::ray_trace(world, start, start + behind, false, false) {
                distance = distance.min(hit.point.distance(start));
            }
        }
        distance as f32
    }

    /// `setupViewBobbing` and `orientCamera`, relative to the player's eye
    pub fn view(&self, world: &impl BlockView, partial: f32, bobbing: bool, ticks: i32) -> Mat4 {
        let e = &self.entity;
        let effects = self.camera_effects(partial, bobbing) * self.portal_warp(partial, ticks);
        if e.sleep.sleeping {
            let [x, y, z] = e.pos.to_array().map(floor_f64);
            let turn = if world.block(x, y, z) == common::world::Block::BED {
                common::world::bed::direction(world.metadata(x, y, z)) as f32 * 90.0
            } else {
                0.0
            };
            return effects
                * Mat4::from_translation(Vec3::new(0.0, 0.3, 0.0))
                * Mat4::from_rotation_y(turn.to_radians());
        }
        let pitch = e.prev_pitch + (e.pitch - e.prev_pitch) * partial;
        let yaw = e.prev_yaw + (e.yaw - e.prev_yaw) * partial;
        effects
            * Mat4::from_translation(Vec3::new(0.0, 0.0, -self.camera_distance(world, partial)))
            * Mat4::from_rotation_x(pitch.to_radians())
            * Mat4::from_rotation_y((yaw + 180.0).to_radians())
    }

    /// The swaying stretch of the world while standing in a portal (`setupCameraTransform`).
    fn portal_warp(&self, partial: f32, ticks: i32) -> Mat4 {
        let portal = &self.entity.portal;
        let time = portal.prev_time + (portal.time - portal.prev_time) * partial;
        if time <= 0.0 {
            return Mat4::IDENTITY;
        }
        let scale = (5.0 / (time * time + 5.0) - time * 0.04).powi(2);
        let axis = Vec3::new(0.0, 1.0, 1.0).normalize();
        let angle = ((ticks as f32 + partial) * 20.0).to_radians();
        Mat4::from_axis_angle(axis, angle)
            * Mat4::from_scale(Vec3::new(1.0 / scale, 1.0, 1.0))
            * Mat4::from_axis_angle(axis, -angle)
    }

    pub fn camera_effects(&self, partial: f32, bobbing: bool) -> Mat4 {
        let e = &self.entity;
        let mut view = Mat4::IDENTITY;
        if let Some(living) = &e.living {
            if living.health <= 0 {
                view *= Mat4::from_rotation_z(
                    (40.0 - 8000.0 / (living.death_time as f32 + partial + 200.0)).to_radians(),
                );
            }
            let hurt = living.hurt_time as f32 - partial;
            if hurt >= 0.0 && living.max_hurt_time > 0 {
                let roll = math::sin((hurt / living.max_hurt_time as f32).powi(4) * PI) * -14.0;
                view *= Mat4::from_rotation_y(-living.attacked_at_yaw.to_radians())
                    * Mat4::from_rotation_z(roll.to_radians())
                    * Mat4::from_rotation_y(living.attacked_at_yaw.to_radians());
            }
        }
        if bobbing {
            let walked = e.distance_walked - e.prev_distance_walked;
            let phase = -(e.distance_walked + walked * partial);
            let bob = self.state.prev_camera_yaw
                + (self.state.camera_yaw - self.state.prev_camera_yaw) * partial;
            let tilt = e.living.as_ref().map_or(0.0, |l| {
                l.prev_camera_pitch + (l.camera_pitch - l.prev_camera_pitch) * partial
            });
            view *= Mat4::from_translation(Vec3::new(
                math::sin(phase * PI) * bob * 0.5,
                -(math::cos(phase * PI) * bob).abs(),
                0.0,
            ));
            view *= Mat4::from_rotation_z((math::sin(phase * PI) * bob * 3.0).to_radians());
            view *= Mat4::from_rotation_x(
                ((math::cos(phase * PI - 0.2) * bob).abs() * 5.0).to_radians(),
            );
            view *= Mat4::from_rotation_x(tilt.to_radians());
        }
        view
    }

    /// `EntityRenderer.getFOVModifier`: narrower under water, closing in while dying.
    pub fn fov(&self, world: &ClientWorld, partial: f32) -> f32 {
        let fov = if self.entity.eyes_in(world, common::world::Material::Water) {
            60.0
        } else {
            70.0
        };
        match &self.entity.living {
            Some(living) if living.health <= 0 => {
                let time = living.death_time as f32 + partial;
                fov / ((1.0 - 500.0 / (time + 500.0)) * 2.0 + 1.0)
            }
            _ => fov,
        }
    }
}