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 client src app session.rs
use std::collections::VecDeque;
use std::time::{Duration, Instant};
use common::JavaRandom;
use common::entity::vehicle::VehicleKind;
use common::protocol::{Connection, Packet};
use common::world::shape::BlockView;
use common::world::{Block, ChunkPos, Material};
use glam::DVec3;
use server::IntegratedServer;
use super::StartError;
use crate::assets::Assets;
use crate::entities::Kind;
use crate::environment;
use crate::interaction::{Interaction, Screen};
use crate::menu::Page;
use crate::player::LocalPlayer;
use crate::render::animated::{AnimatedTextures, Dials, TileUpdate};
use crate::render::{FrameParams, Renderer};
use crate::settings::RenderDistance;
use crate::timer::Timer;
use crate::world::ClientWorld;
/// Time per frame a local game spends applying packets; the rest wait for the next frame.
const RECEIVE_TIME: Duration = Duration::from_millis(3);
pub enum Backend {
Local(IntegratedServer),
Remote(super::remote::Remote),
}
impl Backend {
pub fn local(&self) -> bool {
matches!(self, Self::Local(_))
}
pub fn shutdown(&mut self) -> Result<(), server::save::SaveError> {
match self {
Self::Local(server) => server.shutdown(),
Self::Remote(remote) => {
remote.disconnect();
Ok(())
}
}
}
pub fn set_activity(&self, activity: server::Activity) {
if let Self::Local(server) = self {
server.set_activity(activity);
}
}
pub fn options(&self, options: &crate::settings::Options) {
if let Self::Local(server) = self {
server.set_options(options.view_area(), options.difficulty);
}
}
}
pub struct Session {
pub connection: Connection,
pub backend: Backend,
pub world: ClientWorld,
pub player: LocalPlayer,
pub interaction: Interaction,
entities: crate::entities::Entities,
scene: crate::render::scene::Geometry,
timer: Timer,
animated: AnimatedTextures,
dials: Dials,
tile_updates: Vec<TileUpdate>,
pub render_distance: RenderDistance,
fog_brightness: [f32; 2],
sky_darkness: u32,
/// Received messages with their age in ticks, newest last.
pub chat: Vec<(String, u32)>,
/// `EntityRenderer.rendererUpdateCount`.
ticks: i32,
rain_sound_counter: i32,
pub fancy: bool,
/// `GuiIngame.recordPlaying` and the ticks it stays up.
record: Option<(String, i32)>,
pub logged_in: bool,
/// The dimension entered since the last frame, which reloads the terrain.
pub travelled: Option<i8>,
/// `GameSettings.hideGUI` (F1).
pub hide_gui: bool,
/// `GameSettings.smoothCamera` (F8): mouse turns are eased through `MouseFilter`s.
pub smooth_camera: bool,
pub camera_turn: [f32; 2],
camera_filters: [MouseFilter; 2],
/// `GameSettings.showDebugInfo` (F3).
pub show_debug: bool,
debug: Debug,
statistics: crate::stats::Shared,
/// Local packets left over when [`RECEIVE_TIME`] ran out, applied first next frame.
backlog: VecDeque<Packet>,
}
/// `Minecraft.debug`: frames and chunk updates, counted over each second.
struct Debug {
since: std::time::Instant,
frames: u32,
updates: u32,
summary: String,
}
/// `MouseFilter`: eases a stream of mouse movements.
#[derive(Default)]
struct MouseFilter {
actual: f32,
smoothed: f32,
latency: f32,
}
impl MouseFilter {
fn smooth(&mut self, movement: f32, smoother: f32) -> f32 {
self.actual += movement;
let mut movement = (self.actual - self.smoothed) * smoother;
self.latency += (movement - self.latency) * 0.5;
if movement > 0.0 && movement > self.latency || movement < 0.0 && movement < self.latency {
movement = self.latency;
}
self.smoothed += movement;
movement
}
}
impl Session {
pub fn new(
backend: Backend,
connection: Connection,
seed: i64,
assets: &Assets,
options: &crate::settings::Options,
statistics: crate::stats::Shared,
) -> Self {
backend.options(options);
let remote = !backend.local();
let mut player = LocalPlayer::new(seed);
player.view_bobbing = options.view_bobbing;
Self {
statistics: statistics.clone(),
backend,
connection,
world: ClientWorld::new(
seed,
assets.grass_colors.to_rgb(),
assets.foliage_colors.to_rgb(),
),
player,
interaction: Interaction::new(statistics, remote),
entities: Default::default(),
scene: Default::default(),
timer: Timer::new(),
animated: AnimatedTextures::new(JavaRandom::new(seed).next_long()),
dials: Dials::new(&assets.dials, seed),
tile_updates: Vec::new(),
render_distance: options.render_distance,
fog_brightness: [0.0; 2],
sky_darkness: 0,
chat: Vec::new(),
record: None,
ticks: 0,
rain_sound_counter: 0,
fancy: options.fancy,
logged_in: false,
travelled: None,
hide_gui: false,
smooth_camera: false,
camera_turn: [0.0; 2],
camera_filters: Default::default(),
show_debug: false,
debug: Debug {
since: std::time::Instant::now(),
frames: 0,
updates: 0,
summary: "0 fps, 0 chunk updates".into(),
},
backlog: VecDeque::new(),
}
}
pub fn center(&self) -> ChunkPos {
let pos = self.player.entity.pos;
ChunkPos::containing(
common::math::floor_f64(pos.x),
common::math::floor_f64(pos.z),
)
}
pub fn loading_progress(&self) -> f32 {
if !self.player.started {
return 0.0;
}
let center = self.center();
let loaded = (-1..=1)
.flat_map(|dx| (-1..=1).map(move |dz| center.offset(dx, dz)))
.filter(|&pos| self.world.contains(pos))
.count();
loaded as f32 / 9.0
}
pub fn ready(&self) -> bool {
self.loading_progress() == 1.0
}
/// `NetClientHandler`: applies what the server sent.
pub fn receive(
&mut self,
renderer: &mut Renderer,
audio: &crate::audio::Audio,
) -> Result<(), StartError> {
let local = self.backend.local();
let budget = if local {
usize::MAX
} else {
common::protocol::PACKETS_PER_TICK
};
let (received, closed) = self.connection.receive_some(budget);
let mut packets = std::mem::take(&mut self.backlog);
packets.extend(received);
if closed
&& !packets
.iter()
.any(|p| matches!(p, Packet::Disconnect { .. }))
{
packets.push_back(Packet::Disconnect {
reason: "End of stream".into(),
});
}
let deadline = Instant::now() + RECEIVE_TIME;
while let Some(packet) = packets.pop_front() {
// A burst of terrain waits for the next frame instead of stalling this one
if local && Instant::now() >= deadline {
packets.push_front(packet);
self.backlog = packets;
break;
}
if self.entities.receive(&packet, &self.world) {
self.entity_effects(audio);
continue;
}
// `handleTransaction`: a rejected click is acknowledged so the server accepts more
if let Packet::Transaction {
window,
action,
accepted: false,
} = packet
&& (window == 0 || window == self.interaction.window)
{
self.connection.send(Packet::Transaction {
window,
action,
accepted: true,
});
}
if self.interaction.receive(&packet) {
continue;
}
match packet {
Packet::Disconnect { reason } => return Err(StartError::Disconnected(reason)),
Packet::Login {
version_or_entity,
seed,
dimension,
..
} => {
self.logged_in = true;
tracing::info!("joined world with seed {seed}");
self.world.reset(seed, dimension);
self.player.entity.id = version_or_entity;
}
Packet::Statistic { id, amount } => {
self.statistics.lock().unwrap().server(id, amount)
}
Packet::UpdateTime { time } => self.world.time = time,
Packet::EntityVelocity { entity, velocity } if entity == self.player.entity.id => {
self.player.entity.motion = crate::entities::velocity(velocity)
}
Packet::Sound {
name,
pos,
volume,
pitch,
} => audio.play(&name, DVec3::from_array(pos), volume, pitch),
Packet::Particle {
name,
pos,
velocity,
} => self.world.particles.effect(
&name,
DVec3::from_array(pos),
DVec3::from_array(velocity),
),
Packet::Explosion {
x,
y,
z,
size,
blocks,
} => {
let origin = DVec3::new(x, y, z);
// `Explosion.doExplosionB`; the integrated server sends its own effects
let remote = !self.backend.local();
let rand = &mut self.world.rand;
if remote {
let pitch = (1.0 + (rand.next_float() - rand.next_float()) * 0.2) * 0.7;
audio.play("random.explode", origin, 4.0, pitch);
}
for &[dx, dy, dz] in blocks.iter().rev() {
let block = [
x as i32 + i32::from(dx),
y as i32 + i32::from(dy),
z as i32 + i32::from(dz),
];
if remote {
let rand = &mut self.world.rand;
let point = DVec3::from_array(
block.map(|v| f64::from(v as f32 + rand.next_float())),
);
let delta = point - origin;
let length = f64::from(delta.length() as f32);
let speed = 0.5 / (length / f64::from(size) + 0.1)
* f64::from(rand.next_float() * rand.next_float() + 0.3);
let velocity = delta / length * speed;
let particles = &mut self.world.particles;
particles.effect("explode", (point + origin) / 2.0, velocity);
particles.effect("smoke", point, velocity);
}
self.world
.set_block(block[0], block[1], block[2], Block::AIR, 0);
}
}
Packet::AuxEffect {
effect,
x,
y,
z,
data,
} => self.aux_effect(audio, effect, [x, i32::from(y as u8), z], data),
Packet::PlayNoteBlock {
x,
y,
z,
instrument,
pitch,
} => {
let y = i32::from(y);
if self.world.block(x, y, z) == Block::NOTE_BLOCK {
let center = DVec3::new(f64::from(x), f64::from(y), f64::from(z)) + 0.5;
let name = ["harp", "bd", "snare", "hat", "bassattack"]
.get(usize::from(instrument))
.copied()
.unwrap_or("harp");
let pitch_scale = 2.0_f64.powf((f64::from(pitch) - 12.0) / 12.0) as f32;
audio.play(&format!("note.{name}"), center, 3.0, pitch_scale);
self.world.particles.effect(
"note",
center + DVec3::Y * 0.7,
DVec3::new(f64::from(pitch) / 24.0, 0.0, 0.0),
);
}
}
Packet::EntityStatus {
entity,
status: 2 | 3,
}
| Packet::Animation {
entity,
animation: 2,
} if entity == self.player.entity.id => {
let remote = !self.backend.local();
if let Some(living) = &mut self.player.entity.living {
living.hurt_time = 10;
living.max_hurt_time = 10;
if remote {
living.attacked_at_yaw = 0.0;
}
}
if remote {
audio.play("random.hurt", self.player.entity.pos, 1.0, 1.0);
}
}
Packet::HurtDirection { entity, yaw } if entity == self.player.entity.id => {
if let Some(living) = &mut self.player.entity.living {
living.attacked_at_yaw = yaw;
}
}
// The flags the server keeps for this player, like burning
Packet::EntityMetadata { entity, metadata } if entity == self.player.entity.id => {
for (index, value) in metadata {
if index == 0
&& let common::protocol::MetadataValue::Byte(flags) = value
{
let sneaking = self.player.entity.flag(common::entity::flags::SNEAKING);
self.player.entity.flags = flags as u8;
self.player
.entity
.set_flag(common::entity::flags::SNEAKING, sneaking);
}
}
}
Packet::UpdateHealth { health } => {
if let Some(living) = &mut self.player.entity.living {
if i32::from(health) < living.health {
living.prev_health = living.health;
living.hurt_time = 10;
living.max_hurt_time = 10;
self.player.entity.hearts_life = living.hearts_halves_life;
} else if i32::from(health) > living.health {
// `EntityPlayerSP.setHealth`
self.player.entity.hearts_life = living.hearts_halves_life / 2;
}
living.health = i32::from(health);
}
if health <= 0 {
self.interaction.screen = Some(Screen::Death);
}
}
Packet::Respawn { dimension } => {
let living = self.player.entity.living.as_ref();
let alive = living.is_some_and(|l| l.health > 0);
// Local portal travel keeps the same player (`Minecraft.usePortal`)
let travel = self.backend.local() && alive;
if dimension != self.world.dimension() {
let seed = self.world.seed();
self.world.reset(seed, dimension);
self.entities.0.clear();
audio.record(None, DVec3::ZERO);
self.record = None;
renderer.clear_world();
self.travelled = Some(dimension);
if travel {
let pitch = self.player.entity.rand.next_float() * 0.4 + 0.8;
audio.play_fx("portal.travel", 1.0, pitch);
}
}
let entity = &mut self.player.entity;
entity.dead = false;
if travel {
entity.portal.time = 1.0;
entity.portal.cooldown = 10;
} else {
entity.living = Some(common::entity::Living::new(20));
entity.fire = 0;
entity.air = 300;
entity.portal = Default::default();
}
self.player.started = false;
}
Packet::SpawnPosition { x, y, z } => self.world.spawn = [x, y, z],
Packet::Sleep {
entity,
status: 0,
x,
y,
z,
} if entity == self.player.entity.id => {
let bed = [x, i32::from(y), z];
common::entity::player::lie_down(&mut self.player.entity, &self.world, bed);
}
Packet::Animation {
entity,
animation: 3,
} if entity == self.player.entity.id => common::entity::player::get_up(
&mut self.player.entity,
&self.world,
1.62,
false,
),
Packet::AttachEntity { entity, vehicle } if entity == self.player.entity.id => {
let top = self
.player
.entity
.riding
.and_then(|id| self.entities.top(id));
let vehicle = self.entities.0.contains_key(&vehicle).then_some(vehicle);
self.player.attach(vehicle, top);
}
Packet::GameState { state: 0 } => {
self.message("Your home bed was missing or obstructed".into())
}
Packet::GameState {
state: state @ (1 | 2),
} => {
let weather = &mut self.world.weather;
weather.raining = state == 1;
weather.rain = if weather.raining { 1.0 } else { 0.0 };
weather.previous_rain = weather.rain;
}
Packet::WeatherState {
raining,
thundering,
rain,
thunder,
} => {
let weather = &mut self.world.weather;
weather.raining = raining;
weather.thundering = thundering;
weather.rain = rain;
weather.previous_rain = rain;
weather.thunder = thunder;
weather.previous_thunder = thunder;
}
Packet::Weather {
lightning: true,
x,
y,
z,
..
} => {
let position = DVec3::new(f64::from(x), f64::from(y), f64::from(z)) / 32.0;
let rand = JavaRandom::new(self.world.rand.next_long());
let bolt = crate::weather::Bolt::new(position, rand);
self.world.bolts.push(bolt);
}
Packet::PlayerPositionLook {
x,
y,
z,
yaw,
pitch,
..
} => self.player.teleport(&self.connection, x, y, z, yaw, pitch),
Packet::PreChunk { x, z, load } => {
let pos = ChunkPos::new(x, z);
self.world.pre_chunk(pos, load);
if !load {
renderer.column_unloaded(pos);
}
}
Packet::MapData { item, map, data } if item == common::item::Item::MAP.0 => {
self.world
.maps
.entry(map)
.or_insert_with(|| common::map::Map::new([0, 0], 0))
.receive(&data);
}
Packet::MapChunk {
x,
y,
z,
size,
data,
} => {
for pos in self.world.set_region(x, i32::from(y), z, size, &data) {
renderer.column_loaded(pos, &self.world);
}
}
Packet::BlockChange {
x,
y,
z,
block,
metadata,
} => {
if self
.interaction
.placed_sign
.as_ref()
.is_some_and(|sign| sign.position == [x, i32::from(y), z])
&& !matches!(Block(block), Block::SIGN | Block::WALL_SIGN)
{
self.interaction.placed_sign = None;
}
self.world
.set_block(x, i32::from(y), z, Block(block), metadata);
}
Packet::MultiBlockChange {
chunk_x,
chunk_z,
changes,
} => {
for (packed, block, metadata) in changes {
let packed = packed as u16;
let x = chunk_x * 16 + i32::from(packed >> 12);
let z = chunk_z * 16 + i32::from(packed >> 8 & 15);
self.world
.set_block(x, i32::from(packed & 255), z, Block(block), metadata);
}
}
Packet::TileEntity { x, y, z, data } => self.world.set_tile_entity(x, y, z, *data),
Packet::UpdateSign { x, y, z, lines } => {
let sign = common::world::TileEntity::Sign(lines);
self.world.set_tile_entity(x, i32::from(y), z, sign)
}
Packet::Chat { message } => self.message(message),
Packet::KeepAlive => {
self.connection.send(Packet::KeepAlive);
}
other => tracing::debug!("unhandled packet {other:?}"),
}
}
Ok(())
}
/// `RenderGlobal.playAuxSFX`: sounds and particles the server names by number.
fn aux_effect(
&mut self,
audio: &crate::audio::Audio,
effect: i32,
[x, y, z]: [i32; 3],
data: i32,
) {
let corner = DVec3::new(f64::from(x), f64::from(y), f64::from(z));
let random = &mut self.world.rand;
match effect {
1000 => audio.play("random.click", corner, 1.0, 1.0),
1001 => audio.play("random.click", corner, 1.0, 1.2),
1002 => audio.play("random.bow", corner, 1.0, 1.2),
1003 => {
let name = if self.player.entity.rand.next_double() < 0.5 {
"random.door_open"
} else {
"random.door_close"
};
audio.play(name, corner + 0.5, 1.0, random.next_float() * 0.1 + 0.9);
}
1004 => {
let pitch = 2.6 + (random.next_float() - random.next_float()) * 0.8;
let center = [x, y, z].map(|v| f64::from(v as f32 + 0.5));
audio.play("random.fizz", DVec3::from_array(center), 0.5, pitch);
}
1005 => {
let name = common::item::Item(data as i16).record_name();
if let Some(name) = name {
self.record = Some((format!("Now playing: C418 - {name}"), 60));
}
audio.record(name, corner);
}
2000 => {
let (dx, dz) = (data % 3 - 1, data / 3 % 3 - 1);
let origin = DVec3::new(
f64::from(x) + f64::from(dx) * 0.6 + 0.5,
f64::from(y) + 0.5,
f64::from(z) + f64::from(dz) * 0.6 + 0.5,
);
for _ in 0..10 {
let speed = random.next_double() * 0.2 + 0.01;
let position = DVec3::new(
origin.x
+ f64::from(dx) * 0.01
+ (random.next_double() - 0.5) * f64::from(dz) * 0.5,
origin.y + (random.next_double() - 0.5) * 0.5,
origin.z
+ f64::from(dz) * 0.01
+ (random.next_double() - 0.5) * f64::from(dx) * 0.5,
);
let velocity = DVec3::new(
f64::from(dx) * speed + random.next_gaussian() * 0.01,
-0.03 + random.next_gaussian() * 0.01,
f64::from(dz) * speed + random.next_gaussian() * 0.01,
);
self.world.particles.effect("smoke", position, velocity);
}
}
2001 => self
.world
.break_effect([x, y, z], Block(data as u8), (data >> 8) as u8),
_ => {}
}
}
pub fn message(&mut self, message: String) {
self.chat.push((message, 0));
if self.chat.len() > 50 {
self.chat.remove(0);
}
}
/// Sounds and particles of remote entities; the integrated server makes its own.
fn entity_effects(&mut self, audio: &crate::audio::Audio) {
let sounds = std::mem::take(&mut self.entities.1);
let effects = std::mem::take(&mut self.entities.2);
if self.backend.local() {
return;
}
let rand = &mut self.world.rand;
for (sound, position, volume, pitch) in sounds {
let pitch =
pitch.unwrap_or_else(|| (rand.next_float() - rand.next_float()) * 0.2 + 1.0);
audio.play(sound, position, volume, pitch);
}
for (name, position, velocity) in effects {
self.world.particles.effect(name, position, velocity);
}
}
/// One 20 Hz game tick, mirroring the parts of `Minecraft.runTick` the client has so far.
fn tick(&mut self, audio: &crate::audio::Audio) {
audio.music();
// `EntityPlayerSP.onLivingUpdate`: the inventory hint stays until it is opened
{
let mut stats = self.statistics.lock().unwrap();
if !stats.unlocked(0) {
stats.hint(0);
}
}
for (_, age) in &mut self.chat {
*age += 1;
}
self.world.time += 1;
self.ticks = self.ticks.wrapping_add(1);
crate::weather::rain_particles(
&mut self.world,
self.player.entity.pos,
self.ticks,
self.fancy,
&mut self.rain_sound_counter,
);
if let Some((_, ticks)) = &mut self.record {
*ticks -= 1;
if *ticks <= 0 {
self.record = None;
}
}
self.animated.tick(&mut self.tile_updates);
let e = &self.player.entity;
let [spawn_x, _, spawn_z] = self.world.spawn.map(f64::from);
let targets = self.player.started.then(|| {
[
f64::from(e.yaw - 90.0) * std::f64::consts::PI / 180.0
- (spawn_z - e.pos.z).atan2(spawn_x - e.pos.x),
f64::from(-self.world.celestial_angle(1.0) * std::f32::consts::PI * 2.0),
]
});
self.dials.tick(
targets,
self.world.dimension() == -1,
&mut self.tile_updates,
);
let world = &mut self.world;
world.lightning_flash = (world.lightning_flash - 1).max(0);
world
.bolts
.retain_mut(|bolt| bolt.tick(&mut world.sounds, &mut world.lightning_flash));
if world.has_sky() {
world.weather.fade();
}
let weather = &self.world.weather;
self.sky_darkness = environment::sky_darkness(
self.world.celestial_angle(1.0),
weather.rain_strength(1.0),
weather.thunder_strength(1.0),
);
if self.player.entity.portal.inside && self.interaction.screen.is_some() {
self.interaction.close(&self.connection);
}
let previous_position = self.player.entity.pos;
let previous_ground = self.player.entity.on_ground;
let previous_fall = self.player.entity.fall_distance;
let riding = self.player.entity.riding;
if riding.is_none() {
let area = self.player.entity.bb.inflate(DVec3::new(0.2, 0.0, 0.2));
let pushers = self.entities.pushers(&area);
self.player
.tick(&mut self.world, &self.connection, &pushers);
}
let local = (self.player.entity.id, self.player.entity.pos);
self.entities.tick(&mut self.world, local);
self.entity_effects(audio);
self.world.vehicles.clear();
self.world.vehicles.extend(
self.entities
.0
.iter()
.filter(|(_, entity)| matches!(entity.kind, Kind::Vehicle(VehicleKind::Boat)))
.map(|(&id, entity)| (id, entity.bounds())),
);
if let Some(mount) = riding {
let mount = self.entities.mount(mount);
self.player
.tick_ridden(&mut self.world, &self.connection, mount);
}
self.movement_stats(previous_position, previous_ground, previous_fall);
self.interaction.tick(
&mut self.world,
&mut self.player,
&self.entities,
&self.connection,
);
let mut particles = std::mem::take(&mut self.world.particles);
particles.viewer = self.player.entity.pos;
particles.tick(&self.world);
particles.ambient(&mut self.world, self.player.entity.pos);
particles.spawners(&self.world, self.player.entity.feet());
self.world.tick_spawners(self.player.entity.feet());
self.world.particles = particles;
self.world.tick_pistons(&mut self.player.entity);
self.world.update_light();
self.world.repack_blocks();
for (name, position, volume, pitch) in self.world.sounds.drain(..) {
audio.play(&name, position, volume, pitch);
}
for (name, volume, pitch) in self.world.sound_effects.drain(..) {
audio.play_fx(name, volume, pitch);
}
let eye = self.player.eye(1.0);
let light = self
.world
.light_level(eye.x, eye.y, eye.z, self.sky_darkness);
let brightness = self.world.brightness[usize::from(light)];
let distance_factor = self.render_distance.fog_factor();
let target = brightness * (1.0 - distance_factor) + distance_factor;
self.fog_brightness[1] = self.fog_brightness[0];
self.fog_brightness[0] += (target - self.fog_brightness[0]) * 0.1;
}
/// `SoundManager.setListener`: the interpolated eye and yaw.
pub fn listener(&self) -> (DVec3, f32) {
let partial = self.timer.partial_ticks();
let e = &self.player.entity;
(
self.player.eye(partial),
e.prev_yaw + (e.yaw - e.prev_yaw) * partial,
)
}
/// Runs the game ticks that are due, whether or not a frame gets drawn. Local games pause
/// behind menus; remote ones carry on like the original.
pub fn advance(&mut self, now: Instant, audio: &crate::audio::Audio, menu: &crate::menu::Menu) {
self.tile_updates.clear();
let ticks = if self.backend.local() && menu.page.is_some() {
self.timer.freeze();
// `Minecraft.preloadWorld` lights the terrain before play begins
if menu.page == Some(Page::Loading) {
self.world.update_light();
}
0
} else {
self.timer.advance(now)
};
for _ in 0..ticks {
self.tick(audio);
}
}
pub fn frame(
&mut self,
view: &mut super::View,
menu: &crate::menu::Menu,
options: &crate::settings::Options,
) -> Result<(), StartError> {
let partial = self.timer.partial_ticks();
if self.player.entity.is_inside_opaque_block(&self.world) {
self.player.third_person = false;
}
self.interaction
.refresh_shown(&self.world, &self.player, &self.entities, partial);
if self.smooth_camera {
let smoother = 0.05 * (options.sensitivity * 0.6 + 0.2).powi(3) * 8.0;
let [dx, dy] = std::mem::take(&mut self.camera_turn);
let [x, y] = &mut self.camera_filters;
let turn = (x.smooth(dx, smoother), y.smooth(dy, smoother));
self.player.entity.turn(turn.0, turn.1);
}
self.world.update_light();
for [x, y, z] in self.world.take_dirty() {
let pos = ChunkPos::new(x, z);
if self.world.contains(pos) {
view.renderer.section_changed(pos, y);
}
}
let angle = self.world.celestial_angle(partial);
let eye = self.player.eye(partial);
let temperature = self.world.climate().raw_temperature(
common::math::floor_f64(eye.x),
common::math::floor_f64(eye.z),
);
let weather = &self.world.weather;
let sky_color = crate::weather::sky_color(
environment::sky_color(angle, temperature),
weather,
self.world.lightning_flash,
partial,
);
let blend = 1.0 - (1.0 + 3.0 * self.render_distance.fog_factor()).powf(-0.25);
let mut fog_color = if self.world.has_sky() {
environment::fog_color(angle)
} else {
glam::Vec3::new(0.2, 0.03, 0.03)
};
fog_color += (sky_color - fog_color) * blend;
fog_color = crate::weather::fog_color(fog_color, weather, partial);
let submerged = [Material::Water, Material::Lava]
.into_iter()
.find(|&material| self.player.entity.eyes_in(&self.world, material));
let fog_density = match submerged {
Some(Material::Water) => {
fog_color = glam::Vec3::new(0.02, 0.02, 0.2);
Some(0.1)
}
Some(_) => {
fog_color = glam::Vec3::new(0.6, 0.1, 0.0);
Some(2.0)
}
None => None,
};
let [current, previous_brightness] = self.fog_brightness;
fog_color *= previous_brightness + (current - previous_brightness) * partial;
let extent = view.window.inner_size();
let brightness = self.fancy.then(|| {
let entity = &self.player.entity;
let feet = entity.pos.y - f64::from(entity.y_offset);
let y = feet + (entity.bb.max.y - entity.bb.min.y) * 0.66;
let light = self
.world
.light_level(entity.pos.x, y, entity.pos.z, self.sky_darkness);
self.world.brightness[usize::from(light)]
});
view.gui.build(
&self.interaction,
&self.player,
[extent.width, extent.height],
options.gui_scale,
crate::gui::Overlay {
brightness,
partial,
underwater: self.player.entity.eyes_in(&self.world, Material::Water),
},
);
if let Some((message, ticks)) = &self.record {
view.gui.record(message, *ticks as f32 - partial);
}
let typing = matches!(menu.page, Some(Page::Chat | Page::Sleep));
view.gui.chat(&self.chat, typing);
let hidden = self.hide_gui && self.interaction.screen.is_none() && menu.page.is_none();
if hidden || menu.page == Some(Page::Loading) {
view.gui
.begin([extent.width, extent.height], options.gui_scale);
}
self.scene.hide_hand = self.hide_gui;
self.scene.fancy = self.fancy;
let (rendered, sections, updates) = view.renderer.debug_counts();
let debug = &mut self.debug;
debug.frames += 1;
debug.updates += updates;
if debug.since.elapsed() >= std::time::Duration::from_secs(1) {
debug.summary = format!("{} fps, {} chunk updates", debug.frames, debug.updates);
debug.since += std::time::Duration::from_secs(1);
debug.frames = 0;
debug.updates = 0;
}
if self.show_debug && !hidden {
let entities = self.entities.0.len();
let pos = self.player.entity.pos;
let facing =
common::math::floor_f64(f64::from(self.player.entity.yaw * 4.0 / 360.0) + 0.5) & 3;
let chunks = if self.backend.local() {
"ChunkCache: 1024".to_owned()
} else {
format!("MultiplayerChunkCache: {}", self.world.column_count())
};
view.gui.debug(
&[
format!("Minecraft Beta 1.7.3 ({})", self.debug.summary),
format!("C: {rendered}/{sections}. F: 0, O: 0, E: 0"),
format!(
"P: {}. T: All: {}",
self.world.particles.particles.len(),
entities + 1
),
format!("E: {entities}/{entities}. B: 0, I: 0"),
chunks,
],
&[
format!("x: {:?}", pos.x),
format!("y: {:?}", pos.y),
format!("z: {:?}", pos.z),
format!("f: {facing}"),
],
);
}
if !hidden {
view.gui
.achievement_toast(&self.statistics.lock().unwrap(), options);
}
let cursor = view.gui_space(options.gui_scale).1;
view.gui.menu(menu, options, cursor);
self.scene.build(
&self.entities,
&self.world,
&self.player,
&self.interaction,
partial,
self.sky_darkness,
);
self.scene
.signs(&self.world, eye, &view.gui, self.sky_darkness);
self.scene.map_label(&view.gui);
let scene = &mut self.scene;
scene.names(
&self.entities,
&self.player,
&view.gui,
partial,
self.hide_gui,
);
self.scene.moving_blocks(
&self.world,
eye,
partial,
self.sky_darkness,
crate::render::scene::Style {
fancy: self.fancy,
smooth_lighting: options.smooth_lighting,
},
);
let viewer = crate::render::scene::Viewer {
position: self.player.entity.pos,
eye,
};
self.scene.precipitation(
&self.world,
&viewer,
self.ticks,
partial,
self.fancy,
self.sky_darkness,
);
self.scene.bolts(&self.world, eye);
let entity = &self.player.entity;
let camera = entity.prev_pos.lerp(entity.pos, f64::from(partial));
if self.world.has_sky() {
self.scene.clouds(
DVec3::new(camera.x, eye.y, camera.z),
self.ticks,
partial,
crate::weather::cloud_color(angle, &self.world.weather, partial),
self.fancy,
);
}
let inventory = &self.interaction.inventory;
self.scene
.build_portrait(&self.player, inventory, view.gui.portrait);
view.renderer
.render(
Some(&self.world),
&FrameParams {
eye,
view: self
.player
.view(&self.world, partial, options.view_bobbing, self.ticks),
fov_y_degrees: self.player.fov(&self.world, partial),
view_distance: self.render_distance.blocks(),
fog_color,
fog_density,
sky_color,
rain: self.world.weather.rain_strength(partial),
celestial_angle: angle,
sunrise: environment::sunrise_color(angle),
sky_darkness: self.sky_darkness,
has_sky: self.world.has_sky(),
brightness: self.world.brightness,
tile_updates: &self.tile_updates,
gui: &view.gui,
scene: &self.scene,
},
)
.map_err(Into::into)
}
fn movement_stats(&mut self, previous: DVec3, grounded: bool, fall: f32) {
if !self.player.started {
return;
}
let entity = &self.player.entity;
let delta = entity.pos - previous;
let centimeters = |blocks: f32| (blocks * 100.0).round() as i32;
let distance = centimeters(delta.length() as f32);
let horizontal = centimeters(delta.with_y(0.0).length() as f32);
let mut stats = self.statistics.lock().unwrap();
stats.add(1100, 1);
if let Some(mount) = entity.riding.and_then(|id| self.entities.0.get(&id)) {
match mount.kind {
Kind::Vehicle(VehicleKind::Boat) => stats.add(2007, distance),
Kind::Vehicle(_) => stats.add(2006, distance),
Kind::Mob(common::entity::mob::MobKind::Pig) => stats.add(2008, distance),
_ => {}
}
} else {
if entity.eyes_in(&self.world, Material::Water) {
stats.add(2005, distance);
} else if entity.in_water {
stats.add(2001, horizontal);
} else if entity.is_on_ladder(&self.world) {
stats.add(2003, (delta.y * 100.0).round() as i32);
} else if entity.on_ground {
stats.add(2000, horizontal);
} else if horizontal > 25 {
stats.add(2004, horizontal);
}
if grounded && !entity.on_ground && self.player.keys.jump && entity.motion.y > 0.0 {
stats.add(2010, 1);
}
if entity.on_ground && fall >= 2.0 {
stats.add(2002, (f64::from(fall) * 100.0).round() as i32);
}
}
}
}