use std::time::{Duration, Instant};
use cgmath::vec3;
use perovskite_core::constants::permissions;
use perovskite_core::protocol::blocks::block_type_def::PhysicsInfo;
use perovskite_core::{block_id::BlockId, coordinates::PlayerPositionUpdate};
use perovskite_core::constants::items::default_item_interaction_rules;
use perovskite_core::coordinates::BlockCoordinate;
use line_drawing::WalkVoxels;
use perovskite_core::protocol::blocks::BlockTypeDef;
use perovskite_core::protocol::items::interaction_rule::DigBehavior;
use perovskite_core::protocol::items::ItemDef;
use rustc_hash::FxHashSet;
use super::physics::apply_aabox_transformation;
use super::{input::BoundAction, make_fallback_blockdef, ClientState, GameAction};
struct DigState {
progress: f64,
coord: BlockCoordinate,
id: BlockId,
item_dig_behavior: Option<DigBehavior>,
base_durability: f64,
}
pub(crate) struct ToolController {
dig_progress: Option<DigState>,
current_item: ItemDef,
current_slot: u32,
current_item_interacting_groups: Vec<Vec<String>>,
fallback_blockdef: BlockTypeDef,
can_dig_place: bool,
can_tap_interact: bool,
futile_dig_since: Option<Instant>,
}
impl ToolController {
pub(crate) fn new() -> ToolController {
ToolController {
dig_progress: None,
current_item: default_item(),
current_slot: 0,
current_item_interacting_groups: get_dig_interacting_groups(&default_item()),
fallback_blockdef: make_fallback_blockdef(),
can_dig_place: false,
can_tap_interact: false,
futile_dig_since: None,
}
}
pub(crate) fn update_permissions(&mut self, permissions: &[String]) {
self.can_dig_place = permissions
.iter()
.any(|p| p.contains(permissions::DIG_PLACE));
self.can_tap_interact = permissions
.iter()
.any(|p| p.contains(permissions::TAP_INTERACT));
}
pub(crate) fn update_item(
&mut self,
client_state: &ClientState,
slot: u32,
item: Option<ItemDef>,
) {
let item = item.unwrap_or(default_item());
self.current_slot = slot;
self.current_item_interacting_groups = get_dig_interacting_groups(&item);
if let Some(progress) = &mut self.dig_progress {
progress.item_dig_behavior = Self::compute_dig_behavior(
&item,
client_state.block_types.get_blockdef(progress.id).unwrap(),
);
}
self.current_item = item;
}
pub(crate) fn update(&mut self, client_state: &ClientState, delta: Duration) -> ToolState {
let player_pos = client_state.last_position();
let (pointee, neighbor, block_def) = match self.compute_pointee(client_state, &player_pos) {
Some(x) => x,
None => {
let mut input = client_state.input.lock();
input.take_just_pressed(BoundAction::Dig);
input.take_just_released(BoundAction::Dig);
input.take_just_pressed(BoundAction::Place);
input.take_just_pressed(BoundAction::Interact);
self.dig_progress = None;
return ToolState {
pointee: None,
neighbor: None,
action: None,
};
}
};
let mut action = None;
let mut input = client_state.input.lock();
if input.is_pressed(BoundAction::Dig) && self.can_dig_place {
if self.dig_progress.as_ref().map_or(true, |x: &DigState| {
x.coord != pointee || !x.id.equals_ignore_variant(BlockId(block_def.id))
}) {
let behavior = Self::compute_dig_behavior(&self.current_item, block_def);
if let Some(DigBehavior::InstantDig(_)) = behavior {
action = Some(GameAction::Dig(super::DigTapAction {
target: pointee,
prev: neighbor,
item_slot: self.current_slot,
player_pos,
}));
}
self.dig_progress = behavior.map(|behavior| DigState {
progress: 0.,
coord: pointee,
id: BlockId(block_def.id),
item_dig_behavior: Some(behavior),
base_durability: block_def.base_dig_time,
});
} else if let Some(dig_progress) = &mut self.dig_progress {
let delta_progress = match dig_progress.item_dig_behavior {
None => 0.,
Some(DigBehavior::InstantDig(_)) => 0.,
Some(DigBehavior::InstantDigOneshot(_)) => {
if input.take_just_pressed(BoundAction::Dig) {
action = Some(GameAction::Dig(super::DigTapAction {
target: pointee,
prev: neighbor,
item_slot: self.current_slot,
player_pos,
}));
}
0.
}
Some(DigBehavior::ConstantTime(dig_seconds)) => {
delta.as_secs_f64() / dig_seconds
}
Some(DigBehavior::ScaledTime(scale)) => {
delta.as_secs_f64() / (scale * dig_progress.base_durability)
}
Some(DigBehavior::Undiggable(_)) => 0.,
};
dig_progress.progress += delta_progress;
if dig_progress.progress >= 1.0 {
dig_progress.progress = f64::NEG_INFINITY;
action = Some(GameAction::Dig(super::DigTapAction {
target: pointee,
prev: neighbor,
item_slot: self.current_slot,
player_pos,
}));
}
}
} else {
self.dig_progress = None;
}
if input.take_just_released(BoundAction::Dig) && self.can_tap_interact {
action = Some(GameAction::Tap(super::DigTapAction {
target: pointee,
prev: neighbor,
item_slot: self.current_slot,
player_pos,
}))
} else if input.take_just_pressed(BoundAction::Place)
&& neighbor.is_some()
&& self.can_dig_place
{
action = Some(GameAction::Place(super::PlaceAction {
target: neighbor.unwrap(),
anchor: Some(pointee),
item_slot: self.current_slot,
player_pos,
}))
} else if input.take_just_pressed(BoundAction::Interact) && self.can_tap_interact {
action = Some(GameAction::InteractKey(super::InteractKeyAction {
target: pointee,
item_slot: self.current_slot,
player_pos,
}))
}
if !self.can_dig_place {
if input.peek_just_pressed(BoundAction::Dig) {
self.futile_dig_since = Some(Instant::now());
} else if !input.is_pressed(BoundAction::Dig) {
self.futile_dig_since = None;
}
if self.futile_dig_since.map_or(false, |x: Instant| {
x.elapsed() > Duration::from_secs_f64(0.5)
}) {
self.futile_dig_since = None;
client_state
.chat
.lock()
.show_client_message("You don't have permission to dig".to_string());
}
if input.peek_just_pressed(BoundAction::Place) {
client_state
.chat
.lock()
.show_client_message("You don't have permission to place".to_string());
}
}
if let Some(action) = &action {
log::info!("Sending player action: {:?}", action);
}
ToolState {
pointee: Some(pointee),
neighbor,
action,
}
}
fn compute_dig_behavior(item: &ItemDef, block_def: &BlockTypeDef) -> Option<DigBehavior> {
let block_groups = block_def.groups.iter().collect::<FxHashSet<_>>();
for rule in item.interaction_rules.iter() {
if rule.block_group.iter().all(|x| block_groups.contains(x)) {
return rule.dig_behavior.clone();
}
}
None
}
fn compute_pointee<'a>(
&'a self,
client_state: &'a ClientState,
last_pos: &PlayerPositionUpdate,
) -> Option<(BlockCoordinate, Option<BlockCoordinate>, &'a BlockTypeDef)> {
let pos = last_pos.position;
let end = pos + (POINTEE_DISTANCE * last_pos.face_unit_vector());
let delta_inv = vec3(
1.0 / (end.x - pos.x),
1.0 / (end.y - pos.y),
1.0 / (end.z - pos.z),
);
let chunks = client_state.chunks.read_lock();
let mut prev = None;
for (x, y, z) in WalkVoxels::<f64, i64>::new(
(pos + RAYCAST_FUDGE_VEC).into(),
(end + RAYCAST_FUDGE_VEC).into(),
&line_drawing::VoxelOrigin::Corner,
) {
let coord = BlockCoordinate {
x: x.try_into().ok()?,
y: y.try_into().ok()?,
z: z.try_into().ok()?,
};
let chunk = chunks.get(&coord.chunk());
if let Some(chunk) = chunk {
let id = chunk.get_single(coord.offset());
let block_def = client_state
.block_types
.get_blockdef(id)
.unwrap_or(&self.fallback_blockdef);
if check_intersection(
vec3(coord.x as f64, coord.y as f64, coord.z as f64),
block_def,
pos,
delta_inv,
id.variant(),
) {
for rule in self.current_item_interacting_groups.iter() {
if rule.iter().all(|x| block_def.groups.contains(x)) {
return Some((coord, prev, block_def));
}
}
}
prev = Some(coord);
}
}
None
}
}
fn check_intersection(
block_coord: cgmath::Vector3<f64>,
block_def: &BlockTypeDef,
pos: cgmath::Vector3<f64>,
delta_inv: cgmath::Vector3<f64>,
variant: u16,
) -> bool {
match &block_def.physics_info {
Some(PhysicsInfo::SolidCustomCollisionboxes(boxes)) => boxes.boxes.iter().any(|aa_box| {
if aa_box.variant_mask != 0 && aa_box.variant_mask & variant as u32 == 0 {
return false;
}
let (min, max) = apply_aabox_transformation(aa_box, block_coord, variant);
let tx1 = (min.x - pos.x) * delta_inv.x;
let tx2 = (max.x - pos.x) * delta_inv.x;
let t_min = tx1.min(tx2);
let t_max = tx1.max(tx2);
let ty1 = (min.y - pos.y) * delta_inv.y;
let ty2 = (max.y - pos.y) * delta_inv.y;
let t_min = t_min.max(ty1.min(ty2));
let t_max = t_max.min(ty1.max(ty2));
let tz1 = (min.z - pos.z) * delta_inv.z;
let tz2 = (max.z - pos.z) * delta_inv.z;
let t_min = t_min.max(tz1.min(tz2));
let t_max = t_max.min(tz1.max(tz2));
t_max >= t_min && t_max >= 0.
}),
_ => true,
}
}
fn get_dig_interacting_groups(item: &ItemDef) -> Vec<Vec<String>> {
let mut result = vec![];
for rule in &item.interaction_rules {
if rule.dig_behavior.is_some() {
result.push(rule.block_group.clone())
}
}
result
}
fn default_item() -> ItemDef {
let rules = default_item_interaction_rules();
ItemDef {
short_name: String::new(),
display_name: String::new(),
inventory_texture: None,
groups: vec![],
interaction_rules: rules,
quantity_type: None,
block_apperance: "".to_string(),
}
}
#[derive(Clone, Debug)]
pub(crate) struct ToolState {
pub(crate) pointee: Option<BlockCoordinate>,
pub(crate) neighbor: Option<BlockCoordinate>,
pub(crate) action: Option<GameAction>,
}
const POINTEE_DISTANCE: f64 = 6.;
const RAYCAST_FUDGE_VEC: cgmath::Vector3<f64> = cgmath::vec3(0.5, 0.5, 0.5);