use std::ops::Deref;
use std::sync::Arc;
use std::time::{Duration, Instant};
use anyhow::Result;
use arc_swap::ArcSwap;
use cgmath::{Deg, Zero};
use perovskite_core::constants::block_groups::DEFAULT_SOLID;
use perovskite_core::constants::permissions;
use perovskite_core::coordinates::{BlockCoordinate, ChunkCoordinate, PlayerPositionUpdate};
use log::warn;
use parking_lot::{Mutex, RwLockReadGuard};
use perovskite_core::block_id::BlockId;
use perovskite_core::lighting::{ChunkColumn, Lightfield};
use perovskite_core::protocol;
use perovskite_core::protocol::game_rpc::{MapDeltaUpdateBatch, SetClientState};
use perovskite_core::time::TimeState;
use rustc_hash::{FxHashMap, FxHashSet};
use tokio::sync::mpsc;
use tracy_client::span;
use winit::event::Event;
use crate::block_renderer::{fallback_texture, BlockRenderer, ClientBlockTypeManager};
use crate::game_state::chunk::ClientChunk;
use crate::game_ui::egui_ui::EguiUi;
use crate::game_ui::hud::GameHud;
use crate::vulkan::shaders::SceneState;
use self::chat::ChatState;
use self::chunk::{ChunkDataView, SnappyDecodeHelper};
use self::entities::EntityManager;
use self::input::{BoundAction, InputState};
use self::items::{ClientItemManager, InventoryViewManager};
use self::lightcycle::LightCycle;
use self::settings::GameSettings;
use self::tool_controller::{ToolController, ToolState};
pub(crate) mod chat;
pub(crate) mod chunk;
pub(crate) mod entities;
pub(crate) mod input;
pub(crate) mod items;
pub(crate) mod lightcycle;
pub(crate) mod physics;
pub(crate) mod settings;
pub(crate) mod tool_controller;
#[derive(Debug, Clone, Copy)]
pub(crate) struct DigTapAction {
pub(crate) target: BlockCoordinate,
pub(crate) prev: Option<BlockCoordinate>,
pub(crate) item_slot: u32,
pub(crate) player_pos: PlayerPositionUpdate,
}
#[derive(Debug, Clone, Copy)]
pub(crate) struct PlaceAction {
pub(crate) target: BlockCoordinate,
pub(crate) anchor: Option<BlockCoordinate>,
pub(crate) item_slot: u32,
pub(crate) player_pos: PlayerPositionUpdate,
}
#[derive(Debug, Clone, Copy)]
pub(crate) struct InventoryAction {
pub(crate) source_view: u64,
pub(crate) source_slot: usize,
pub(crate) destination_view: u64,
pub(crate) destination_slot: usize,
pub(crate) count: u32,
pub(crate) swap: bool,
}
#[derive(Debug, Clone, Copy)]
pub(crate) struct InteractKeyAction {
pub(crate) target: BlockCoordinate,
#[allow(unused)]
pub(crate) item_slot: u32,
pub(crate) player_pos: PlayerPositionUpdate,
}
#[derive(Debug, Clone)]
pub(crate) enum GameAction {
Dig(DigTapAction),
Tap(DigTapAction),
Place(PlaceAction),
Inventory(InventoryAction),
PopupResponse(perovskite_core::protocol::ui::PopupResponse),
InteractKey(InteractKeyAction),
ChatMessage(String),
}
pub(crate) type ChunkMap = FxHashMap<ChunkCoordinate, Arc<ClientChunk>>;
pub(crate) type LightColumnMap = FxHashMap<(i32, i32), ChunkColumn>;
pub(crate) struct ChunkManager {
chunks: parking_lot::RwLock<ChunkMap>,
renderable_chunks: parking_lot::RwLock<ChunkMap>,
light_columns: parking_lot::RwLock<LightColumnMap>,
}
impl ChunkManager {
pub(crate) fn new() -> ChunkManager {
ChunkManager {
chunks: parking_lot::RwLock::new(FxHashMap::default()),
renderable_chunks: parking_lot::RwLock::new(FxHashMap::default()),
light_columns: parking_lot::RwLock::new(FxHashMap::default()),
}
}
pub(crate) fn read_lock(&self) -> ChunkManagerView {
let _span = span!("Acquire chunk read lock");
let guard = self.chunks.read();
ChunkManagerView { guard }
}
pub(crate) fn renderable_chunks_cloned_view(&self) -> ChunkManagerClonedView {
ChunkManagerClonedView {
data: self.renderable_chunks.read().clone(),
}
}
pub(crate) fn remove(&self, coord: &ChunkCoordinate) -> Option<Arc<ClientChunk>> {
let mut chunks_lock = {
let _span = span!("Acquire global chunk lock");
self.chunks.write()
};
let mut render_chunks_lock = {
let _span = span!("Acquire global renderable chunk lock");
self.renderable_chunks.write()
};
let mut light_columns_lock = {
let _span = span!("Acquire global light column lock");
self.light_columns.write()
};
let column = light_columns_lock.get_mut(&(coord.x, coord.z)).unwrap();
column.remove(coord.y);
if column.is_empty() {
light_columns_lock.remove(&(coord.x, coord.z));
}
render_chunks_lock.remove(coord);
chunks_lock.remove(coord)
}
pub(crate) fn insert_or_update(
&self,
coord: ChunkCoordinate,
proto: protocol::game_rpc::MapChunk,
snappy_helper: &mut SnappyDecodeHelper,
block_types: &ClientBlockTypeManager,
) -> anyhow::Result<usize> {
let mut chunks_lock = {
let _span = span!("Acquire global chunk lock");
self.chunks.write()
};
let mut light_columns_lock = {
let _span = span!("Acquire global light column lock");
self.light_columns.write()
};
let light_column = light_columns_lock
.entry((coord.x, coord.z))
.or_insert_with(ChunkColumn::empty);
let occlusion = match chunks_lock.entry(coord) {
std::collections::hash_map::Entry::Occupied(chunk_entry) => chunk_entry
.get()
.update_from(proto, snappy_helper, block_types)?,
std::collections::hash_map::Entry::Vacant(x) => {
let (chunk, occlusion) =
ClientChunk::from_proto(proto, snappy_helper, block_types)?;
light_column.insert_empty(coord.y);
x.insert(Arc::new(chunk));
occlusion
}
};
let mut light_cursor = light_column.cursor_into(coord.y);
*light_cursor.current_occlusion_mut() = occlusion;
light_cursor.mark_valid();
let extra_chunks = light_cursor.propagate_lighting();
Ok(extra_chunks)
}
pub(crate) fn apply_delta_batch(
&self,
batch: &MapDeltaUpdateBatch,
block_types: &ClientBlockTypeManager,
) -> Result<(FxHashSet<ChunkCoordinate>, Vec<BlockCoordinate>, bool)> {
let mut needs_remesh = FxHashSet::default();
let mut unknown_coords = vec![];
let mut missing_coord = false;
let chunk_lock = self.chunks.read();
let light_lock = self.light_columns.read();
for update in batch.updates.iter() {
let block_coord: BlockCoordinate = match &update.block_coord {
Some(x) => x.into(),
None => {
log::warn!("Got delta with missing block_coord {:?}", update);
missing_coord = true;
continue;
}
};
let chunk_coord = block_coord.chunk();
let extra_chunks = match chunk_lock.get(&chunk_coord) {
Some(x) => {
if !x.apply_delta(update).unwrap() {
None
} else {
let occlusion = x.get_occlusion(block_types);
let light_column = light_lock.get(&(chunk_coord.x, chunk_coord.z)).unwrap();
let mut light_cursor = light_column.cursor_into(chunk_coord.y);
*light_cursor.current_occlusion_mut() = occlusion;
light_cursor.mark_valid();
let extra_chunks = light_cursor.propagate_lighting();
Some(extra_chunks as i32)
}
}
None => {
log::warn!("Got delta for unknown chunk {:?}", block_coord);
unknown_coords.push(block_coord);
None
}
};
if let Some(extra_chunks) = extra_chunks {
let chunk = block_coord.chunk();
needs_remesh.insert(chunk);
for i in -1..=1 {
for j in (-1 - extra_chunks)..=1 {
for k in -1..=1 {
if let Some(neighbor) = chunk.try_delta(i, j, k) {
needs_remesh.insert(neighbor);
}
}
}
}
}
}
Ok((needs_remesh, unknown_coords, missing_coord))
}
pub(crate) fn cloned_neighbors_fast(&self, chunk: ChunkCoordinate) -> FastChunkNeighbors {
let chunk_lock = self.chunks.read();
let lights_lock = self.light_columns.read();
let mut data =
std::array::from_fn(|_| std::array::from_fn(|_| std::array::from_fn(|_| None)));
let mut inbound_lights = [[[Lightfield::zero(); 3]; 3]; 3];
for i in -1..=1 {
for k in -1..=1 {
let light_column = chunk
.try_delta(i, 0, k)
.and_then(|delta| lights_lock.get(&(delta.x, delta.z)));
for j in -1..=1 {
if let Some(delta) = chunk.try_delta(i, j, k) {
data[(k + 1) as usize][(j + 1) as usize][(i + 1) as usize] = chunk_lock
.get(&delta)
.map(|x| Box::new(*x.chunk_data().block_ids()));
if let Some(light_column) = light_column {
inbound_lights[(k + 1) as usize][(j + 1) as usize][(i + 1) as usize] =
light_column
.get_incoming_light(delta.y)
.unwrap_or_else(Lightfield::zero);
}
}
}
}
}
FastChunkNeighbors {
neighbors: data,
center: chunk_lock.get(&chunk).cloned(),
inbound_lights,
}
}
pub(crate) fn maybe_mesh_and_maybe_promote(
&self,
coord: ChunkCoordinate,
renderer: &BlockRenderer,
) -> Result<bool> {
let src = self.chunks.read();
if let Some(chunk) = src.get(&coord) {
if chunk.mesh_with(renderer)? {
let mut dst = self.renderable_chunks.write();
dst.insert(coord, chunk.clone());
} else {
let mut dst = self.renderable_chunks.write();
dst.remove(&coord);
}
Ok(true)
} else {
Ok(false)
}
}
}
pub(crate) struct ChunkManagerView<'a> {
guard: RwLockReadGuard<'a, ChunkMap>,
}
impl<'a> ChunkManagerView<'a> {
pub(crate) fn get(&'a self, coord: &ChunkCoordinate) -> Option<&'a Arc<ClientChunk>> {
self.guard.get(coord)
}
pub(crate) fn contains_key(&self, coord: &ChunkCoordinate) -> bool {
self.guard.contains_key(coord)
}
}
type ChunkWithEdgesBuffer = Box<[BlockId; 18 * 18 * 18]>;
pub(crate) struct FastChunkNeighbors {
center: Option<Arc<ClientChunk>>,
neighbors: [[[Option<ChunkWithEdgesBuffer>; 3]; 3]; 3],
inbound_lights: [[[Lightfield; 3]; 3]; 3],
}
impl FastChunkNeighbors {
pub(crate) fn get(&self, coord_xyz: (i32, i32, i32)) -> Option<&[BlockId; 18 * 18 * 18]> {
assert!((-1..=1).contains(&coord_xyz.0));
assert!((-1..=1).contains(&coord_xyz.1));
assert!((-1..=1).contains(&coord_xyz.2));
self.neighbors[(coord_xyz.2 + 1) as usize][(coord_xyz.1 + 1) as usize]
[(coord_xyz.0 + 1) as usize]
.as_deref()
}
pub(crate) fn center(&self) -> Option<&ClientChunk> {
self.center.as_deref()
}
pub(crate) fn inbound_light(&self, coord_xyz: (i32, i32, i32)) -> Lightfield {
assert!((-1..=1).contains(&coord_xyz.0));
assert!((-1..=1).contains(&coord_xyz.1));
assert!((-1..=1).contains(&coord_xyz.2));
self.inbound_lights[(coord_xyz.2 + 1) as usize][(coord_xyz.1 + 1) as usize]
[(coord_xyz.0 + 1) as usize]
}
}
pub(crate) struct ChunkManagerClonedView {
data: ChunkMap,
}
impl Deref for ChunkManagerClonedView {
type Target = ChunkMap;
fn deref(&self) -> &Self::Target {
&self.data
}
}
pub(crate) struct ClientState {
pub(crate) settings: Arc<ArcSwap<GameSettings>>,
pub(crate) input: Mutex<InputState>,
pub(crate) block_types: Arc<ClientBlockTypeManager>,
pub(crate) items: Arc<ClientItemManager>,
pub(crate) last_update: parking_lot::Mutex<Instant>,
pub(crate) physics_state: parking_lot::Mutex<physics::PhysicsState>,
pub(crate) chunks: ChunkManager,
pub(crate) inventories: parking_lot::Mutex<InventoryViewManager>,
pub(crate) tool_controller: parking_lot::Mutex<ToolController>,
pub(crate) shutdown: tokio_util::sync::CancellationToken,
pub(crate) actions: mpsc::Sender<GameAction>,
pub(crate) light_cycle: Arc<Mutex<LightCycle>>,
pub(crate) chat: Arc<Mutex<ChatState>>,
pub(crate) entities: Mutex<EntityManager>,
pub(crate) block_renderer: Arc<BlockRenderer>,
pub(crate) hud: Arc<Mutex<GameHud>>,
pub(crate) egui: Arc<Mutex<EguiUi>>,
pub(crate) pending_error: Mutex<Option<String>>,
pub(crate) wants_exit_from_game: Mutex<bool>,
pub(crate) last_position_weak: Mutex<PlayerPositionUpdate>,
}
impl ClientState {
pub(crate) fn new(
settings: Arc<ArcSwap<GameSettings>>,
block_types: Arc<ClientBlockTypeManager>,
items: Arc<ClientItemManager>,
action_sender: mpsc::Sender<GameAction>,
hud: GameHud,
egui: EguiUi,
block_renderer: BlockRenderer,
) -> Result<ClientState> {
Ok(ClientState {
settings: settings.clone(),
input: Mutex::new(InputState::new(settings.clone())),
block_types,
items,
last_update: Mutex::new(Instant::now()),
physics_state: Mutex::new(physics::PhysicsState::new(settings)),
chunks: ChunkManager::new(),
inventories: Mutex::new(InventoryViewManager::new()),
tool_controller: Mutex::new(ToolController::new()),
shutdown: tokio_util::sync::CancellationToken::new(),
actions: action_sender,
light_cycle: Arc::new(Mutex::new(LightCycle::new(TimeState::new(
Duration::from_secs(24 * 60),
0.0,
)))),
chat: Arc::new(Mutex::new(ChatState::new())),
entities: Mutex::new(EntityManager::new(&block_renderer)?),
block_renderer: Arc::new(block_renderer),
hud: Arc::new(Mutex::new(hud)),
egui: Arc::new(Mutex::new(egui)),
pending_error: Mutex::new(None),
wants_exit_from_game: Mutex::new(false),
last_position_weak: Mutex::new(PlayerPositionUpdate {
position: cgmath::Vector3::zero(),
velocity: cgmath::Vector3::zero(),
face_direction: (0.0, 0.0),
}),
})
}
pub(crate) fn window_event(&self, event: &Event<()>) {
let mut input = self.input.lock();
input.event(event);
}
pub(crate) fn last_position(&self) -> PlayerPositionUpdate {
let lock = self.physics_state.lock();
PlayerPositionUpdate {
position: lock.pos(),
velocity: cgmath::Vector3::zero(),
face_direction: lock.angle(),
}
}
pub(crate) fn weakly_ordered_last_position(&self) -> PlayerPositionUpdate {
let _lock = self.physics_state.lock();
*self.last_position_weak.lock()
}
pub(crate) fn next_frame(&self, aspect_ratio: f64) -> FrameState {
{
let mut input = self.input.lock();
input.set_modal_active(self.egui.lock().wants_user_events());
if input.take_just_pressed(BoundAction::Inventory) {
self.egui.lock().open_inventory();
} else if input.take_just_pressed(BoundAction::Menu) {
self.egui.lock().open_pause_menu();
} else if input.take_just_pressed(BoundAction::Chat) {
self.egui.lock().open_chat();
} else if input.take_just_pressed(BoundAction::ChatSlash) {
self.egui.lock().open_chat_slash();
}
}
let delta = {
let mut lock = self.last_update.lock();
let now = Instant::now();
let delta = now - *lock;
*lock = now;
delta
};
let (player_position, (az, el)) =
self.physics_state
.lock()
.update_and_get(self, aspect_ratio, delta);
let rotation = cgmath::Matrix4::from_angle_x(Deg(el))
* cgmath::Matrix4::from_angle_y(Deg(180.) - Deg(az));
let coordinate_correction = cgmath::Matrix4::from_nonuniform_scale(-1., 1., 1.);
let projection = cgmath::perspective(Deg(45.0), aspect_ratio, 0.01, 1000.);
let view_proj_matrix = (projection * coordinate_correction * rotation)
.cast()
.unwrap();
let mut tool_state = self.tool_controller.lock().update(self, delta);
if let Some(action) = tool_state.action.take() {
match self.actions.try_send(action) {
Ok(_) => {}
Err(e) => warn!("Failure sending action: {e:?}"),
}
}
*self.last_position_weak.lock() = PlayerPositionUpdate {
position: player_position,
velocity: cgmath::Vector3::zero(),
face_direction: (az, el),
};
let (sky, lighting, light_direction) = self.light_cycle.lock().get_colors();
FrameState {
scene_state: SceneState {
vp_matrix: view_proj_matrix,
clear_color: [sky.x, sky.y, sky.z, 1.],
global_light_color: lighting.into(),
global_light_direction: light_direction,
},
player_position,
tool_state,
}
}
pub(crate) fn cancel_requested(&self) -> bool {
self.shutdown.is_cancelled()
}
pub(crate) fn handle_server_update(&self, state_update: &SetClientState) -> Result<()> {
let position = state_update
.position
.as_ref()
.and_then(|x| x.position.clone());
{
let mut physics_lock = self.physics_state.lock();
if let Some(pos_vector) = position {
physics_lock.set_position(pos_vector.try_into()?);
}
physics_lock.update_permissions(&state_update.permission, self);
}
{
let mut egui_lock = self.egui.lock();
egui_lock.inventory_view = state_update.inventory_popup.clone();
egui_lock.inventory_manipulation_view_id =
Some(state_update.inventory_manipulation_view);
egui_lock.set_allow_inventory_interaction(
state_update
.permission
.iter()
.any(|p| p == permissions::INVENTORY),
)
}
{
let mut tc_lock = self.tool_controller.lock();
tc_lock.update_permissions(&state_update.permission);
}
{
let mut hud_lock = self.hud.lock();
hud_lock.hotbar_view_id = Some(state_update.hotbar_inventory_view);
hud_lock.invalidate_hotbar();
}
{
let mut time_lock = self.light_cycle.lock();
time_lock
.time_state_mut()
.set_time(state_update.time_of_day);
time_lock
.time_state_mut()
.set_day_length(Duration::from_secs_f64(state_update.day_length_sec));
}
Ok(())
}
}
pub(crate) struct FrameState {
pub(crate) scene_state: SceneState,
pub(crate) player_position: cgmath::Vector3<f64>,
pub(crate) tool_state: ToolState,
}
use perovskite_core::protocol::blocks::{self as blocks_proto, CubeVariantEffect};
pub(crate) fn make_fallback_blockdef() -> blocks_proto::BlockTypeDef {
blocks_proto::BlockTypeDef {
render_info: Some(blocks_proto::block_type_def::RenderInfo::Cube(
blocks_proto::CubeRenderInfo {
tex_left: fallback_texture(),
tex_right: fallback_texture(),
tex_top: fallback_texture(),
tex_bottom: fallback_texture(),
tex_front: fallback_texture(),
tex_back: fallback_texture(),
render_mode: blocks_proto::CubeRenderMode::SolidOpaque.into(),
variant_effect: CubeVariantEffect::None.into(),
},
)),
groups: vec![DEFAULT_SOLID.to_string()],
..Default::default()
}
}