use std::{collections::HashMap, sync::Arc};
use anyhow::{Context, Result};
use perovskite_core::protocol::items::item_stack::QuantityType;
use texture_packer::Rect;
use crate::{
client_state::{items::ClientItemManager, ClientState},
vulkan::{
shaders::flat_texture::{FlatTextureDrawBuilder, FlatTextureDrawCall},
Texture2DHolder, VulkanWindow,
},
};
use super::{get_texture, CROSSHAIR, DIGIT_ATLAS, FRAME_SELECTED, FRAME_UNSELECTED};
pub(crate) struct GameHud {
pub(crate) texture_coords: HashMap<String, Rect>,
pub(crate) texture_atlas: Arc<Texture2DHolder>,
pub(crate) item_defs: Arc<ClientItemManager>,
pub(crate) last_size: (u32, u32),
pub(crate) hotbar_slot: u32,
pub(crate) hotbar_view_id: Option<u64>,
pub(crate) crosshair_draw_call: Option<FlatTextureDrawCall>,
pub(crate) hotbar_draw_call: Option<FlatTextureDrawCall>,
pub(crate) fps_counter: fps_counter::FPSCounter,
}
impl GameHud {
pub(crate) fn hotbar_slot(&self) -> u32 {
self.hotbar_slot
}
pub(crate) fn update_and_render(
&mut self,
ctx: &VulkanWindow,
client_state: &ClientState,
) -> Result<Vec<FlatTextureDrawCall>> {
if let Some(total_slots) = self.hotbar_view_id.and_then(|x| {
client_state
.inventories
.lock()
.inventory_views
.get(&x)
.map(|x| x.dimensions.1)
}) {
let (slot_delta, slot_selection) = {
let mut input_lock = client_state.input.lock();
(
input_lock.take_scroll_slots(),
input_lock.take_hotbar_selection(),
)
};
if slot_delta != 0 {
{
let new_slot =
(self.hotbar_slot as i32 - slot_delta).rem_euclid(total_slots.try_into()?);
self.set_slot(new_slot.try_into()?, client_state);
}
}
if let Some(x) = slot_selection {
if x < total_slots {
self.set_slot(x, client_state);
}
}
}
let window_size = ctx.window_size();
if self.crosshair_draw_call.is_none() || window_size != self.last_size {
self.crosshair_draw_call = Some(self.recreate_crosshair(ctx, window_size)?);
}
if self.hotbar_draw_call.is_none() || window_size != self.last_size {
self.hotbar_draw_call = self.recreate_hotbar(ctx, window_size, client_state)?;
}
self.last_size = window_size;
let mut outputs = vec![];
outputs.push(self.crosshair_draw_call.as_ref().unwrap().clone());
if let Some(x) = self.hotbar_draw_call.as_ref() {
outputs.push(x.clone());
}
let mut fps_builder = FlatTextureDrawBuilder::new();
let fps = self.fps_counter.tick() as u32;
render_number(
(window_size.0, 0),
fps,
&mut fps_builder,
&self.texture_coords,
&self.texture_atlas,
);
outputs.push(fps_builder.build(ctx)?);
let mut net_err_builder = FlatTextureDrawBuilder::new();
let net_err = client_state.timekeeper.get_offset();
render_number(
(window_size.0, 16),
net_err as u32,
&mut net_err_builder,
&self.texture_coords,
&self.texture_atlas,
);
outputs.push(net_err_builder.build(ctx)?);
Ok(outputs)
}
pub(crate) fn clone_atlas(&self) -> Arc<Texture2DHolder> {
self.texture_atlas.clone()
}
fn recreate_crosshair(
&self,
ctx: &VulkanWindow,
window_size: (u32, u32),
) -> Result<FlatTextureDrawCall> {
let mut builder = FlatTextureDrawBuilder::new();
builder.centered_rect(
(window_size.0 / 2, window_size.1 / 2),
*self.texture_coords.get(CROSSHAIR).unwrap(),
self.texture_atlas.dimensions(),
1,
);
builder.build(ctx)
}
fn recreate_hotbar(
&self,
ctx: &VulkanWindow,
window_size: (u32, u32),
client_state: &ClientState,
) -> Result<Option<FlatTextureDrawCall>> {
let mut builder = FlatTextureDrawBuilder::new();
let unselected_frame = *self.texture_coords.get(FRAME_UNSELECTED).unwrap();
let selected_frame = *self.texture_coords.get(FRAME_SELECTED).unwrap();
let w = unselected_frame.w;
let h = unselected_frame.h;
let inv_lock = client_state.inventories.lock();
let view_id = match self.hotbar_view_id {
Some(x) => x,
None => return Ok(None),
};
let main_inv = inv_lock
.inventory_views
.get(&view_id)
.with_context(|| "Couldn't find main player inventory")?;
if main_inv.dimensions.0 == 0 {
return Ok(None);
}
if (main_inv.dimensions.0 as usize) > main_inv.contents().len() {
log::warn!("Hotbar contents vec is too small for the hotbar dimension");
return Ok(None);
}
let hotbar_slots = main_inv.dimensions.1;
let left_offset = 0.5 * (hotbar_slots as f64) * (w as f64);
let frame0_corner = (
(window_size.0 / 2).saturating_sub(left_offset as u32),
window_size.1.saturating_sub(h),
);
for i in 0..hotbar_slots {
let offset = i * w;
let item_rect = Rect::new(
frame0_corner.0 + 2 + offset,
frame0_corner.1 + 2,
w - 4,
h - 4,
);
let stack = &main_inv.contents()[i as usize];
if let Some(stack) = stack {
let tex_coord = self.get_texture(stack);
builder.rect(item_rect, tex_coord, self.clone_atlas().dimensions());
let frame_topright = (frame0_corner.0 + offset + w - 2, frame0_corner.1 + 2);
let frame_bottomleft = (frame0_corner.0 + offset + 2, frame0_corner.1 + h - 8);
match stack.quantity_type {
Some(QuantityType::Stack(_)) => {
if stack.quantity != 1 {
render_number(
frame_topright,
stack.quantity,
&mut builder,
&self.texture_coords,
&self.texture_atlas,
);
}
}
Some(QuantityType::Wear(total_wear)) => render_wear_bar(
frame_bottomleft,
w - 4,
stack.current_wear,
total_wear,
&mut builder,
&self.texture_coords,
&self.texture_atlas,
),
None => {}
}
}
let frame_rect = Rect::new(frame0_corner.0 + offset, frame0_corner.1, w, h);
if i == self.hotbar_slot {
builder.rect(
frame_rect,
selected_frame,
self.texture_atlas().dimensions(),
)
} else {
builder.rect(
frame_rect,
unselected_frame,
self.texture_atlas().dimensions(),
)
}
}
Ok(Some(builder.build(ctx)?))
}
pub(crate) fn invalidate_hotbar(&mut self) {
self.hotbar_draw_call = None;
}
pub(crate) fn get_texture(&self, item: &perovskite_core::protocol::items::ItemStack) -> Rect {
get_texture(item, &self.texture_coords, &self.item_defs)
}
fn set_slot(&mut self, slot: u32, client_state: &ClientState) {
self.hotbar_slot = slot;
let stack = self.hotbar_view_id.and_then(|x| {
client_state
.inventories
.lock()
.inventory_views
.get(&x)
.and_then(|x| x.contents()[slot as usize].clone())
});
let item = stack
.and_then(|x| client_state.items.get(&x.item_name))
.cloned();
client_state
.tool_controller
.lock()
.change_held_item(client_state, slot, item);
self.hotbar_draw_call = None;
}
pub(crate) fn texture_atlas(&self) -> &Texture2DHolder {
self.texture_atlas.as_ref()
}
}
fn render_wear_bar(
frame_bottomleft: (u32, u32),
total_width: u32,
current_wear: u32,
max_wear: u32,
builder: &mut FlatTextureDrawBuilder,
texture_coords: &HashMap<String, Rect>,
texture_atlas: &Texture2DHolder,
) {
let wear_level = ((current_wear as f32) / (max_wear as f32)).clamp(0.0, 1.0);
let draw_width = (wear_level * total_width as f32) as u32;
let wear_bucket = ((wear_level * 8.0) as u8).clamp(0, 7);
let wear_texture = format!("builtin:wear_{}", wear_bucket);
let wear_uv = texture_coords.get(&wear_texture).copied().unwrap();
builder.rect(
Rect::new(frame_bottomleft.0, frame_bottomleft.1, draw_width, 6),
wear_uv,
texture_atlas.dimensions(),
);
}
pub(crate) fn render_number(
pos: (u32, u32),
mut number: u32,
builder: &mut FlatTextureDrawBuilder,
atlas_coords: &HashMap<String, Rect>,
atlas: &Texture2DHolder,
) {
let digits_frame = atlas_coords[DIGIT_ATLAS];
let mut x = pos.0.saturating_sub(DIGIT_WIDTH);
loop {
let digit = number % 10;
builder.rect(
Rect::new(x, pos.1, DIGIT_WIDTH, digits_frame.h),
Rect::new(
digits_frame.x + digit * DIGIT_WIDTH,
digits_frame.y,
DIGIT_WIDTH,
digits_frame.h,
),
atlas.dimensions(),
);
if x < DIGIT_WIDTH {
break;
}
x -= DIGIT_WIDTH - 1;
number /= 10;
if number == 0 {
return;
}
}
}
const DIGIT_WIDTH: u32 = 13;