Skip to main content

ghast_client/
hands.rs

1//! The mouse buttons in the world: digging, placing and using items, ported from 1.8.9
2//! `MinecraftClient.tick` (`doAttack`, `doUse`, `handleBlockBreaking`) and
3//! `ClientPlayerInteractionManager` (`attackBlock`, `updateBlockBreakingProgress`,
4//! `cancelBlockBreaking`, `onRightClick`, `interactItem`, `stopUsingItem`), verified against recorded
5//! lab packet traces.
6//!
7//! The hands run once per tick after the behaviour and before the movement step (vanilla's
8//! key processing comes after the tick-start event and before the player's update), so their
9//! packets precede this tick's movement packet and the server judges them with the previous
10//! tick's pose.
11//!
12//! Version-specific facts (block hardness, tool groups, selection boxes, the held stack) come
13//! from the [`BlockQuery`] answers and [`HeldItem`]; the state machine is the 1.8 one for both
14//! supported versions (1.9+ attack cooldowns and off-hand are not modelled yet).
15
16use ghast_data::DigMaterial;
17use ghast_protocol::BlockPos;
18
19use crate::common::{DigStatus, Face, Outbound, Vec3};
20
21/// Survival block reach (`ClientPlayerInteractionManager.getReachDistance`).
22pub const REACH: f64 = 4.5;
23
24/// What the hands need to know about one block state.
25#[derive(Debug, Clone, Copy, PartialEq)]
26#[non_exhaustive]
27pub struct BlockQuery {
28    /// The block is air (`Material.AIR`).
29    pub air: bool,
30    /// The block is water (eyes in it slow digging).
31    pub water: bool,
32    /// Version block id (1.8 `id`, 1.16 registry id).
33    pub block: u16,
34    /// Hardness; negative = unbreakable.
35    pub hardness: f32,
36    /// Tool group for the speed multiplier.
37    pub dig_material: Option<DigMaterial>,
38    /// Item ids that harvest the block (empty = any).
39    pub harvest_tools: &'static [u16],
40    /// Selection (ray) box relative to the block, `None` when rays pass through.
41    pub selection: Option<[f64; 6]>,
42    /// Stairs (1.8): vanilla's eight-pass half-cube ray test; the passes' block-local boxes
43    /// (`None` = skipped pass). When set, `selection` is ignored for rays.
44    pub stairs: Option<[Option<[f64; 6]>; 8]>,
45    /// Right-clicking the block activates it (containers, doors, levers, …).
46    pub activates: bool,
47}
48
49/// The held stack as far as digging and using need it.
50#[derive(Debug, Clone, Copy, Default)]
51#[non_exhaustive]
52pub struct HeldItem {
53    /// Item id.
54    pub id: u16,
55    /// Stack size.
56    pub count: u8,
57    /// Efficiency enchantment level.
58    pub efficiency: u8,
59    /// The item places a block (`BlockItem`).
60    pub places_block: bool,
61    /// The item is used by holding right click (food, potion, bow, sword, milk).
62    pub usable: bool,
63    /// The item is food (eaten only below a full food bar).
64    pub food: bool,
65    /// Food that can be eaten at any food level (golden apple).
66    pub always_edible: bool,
67    /// Tool speed multiplier per dig group, filled by the version (`tool_multiplier`).
68    pub tool_speed: Option<fn(DigMaterial, u16) -> f32>,
69}
70
71impl BlockQuery {
72    /// A solid, instantly breakable block with no tool group, no ray box and no activation;
73    /// set the rest with the `with_*` methods.
74    #[must_use]
75    pub const fn new(block: u16) -> Self {
76        BlockQuery {
77            air: false,
78            water: false,
79            block,
80            hardness: 0.0,
81            dig_material: None,
82            harvest_tools: &[],
83            selection: None,
84            stairs: None,
85            activates: false,
86        }
87    }
88
89    /// Air (id 0, no ray box).
90    pub const AIR: BlockQuery = BlockQuery::new(0).with_air(true);
91
92    /// The block is air.
93    #[must_use]
94    pub const fn with_air(mut self, on: bool) -> Self {
95        self.air = on;
96        self
97    }
98
99    /// The block is water.
100    #[must_use]
101    pub const fn with_water(mut self, on: bool) -> Self {
102        self.water = on;
103        self
104    }
105
106    /// Hardness (negative: unbreakable).
107    #[must_use]
108    pub const fn with_hardness(mut self, hardness: f32) -> Self {
109        self.hardness = hardness;
110        self
111    }
112
113    /// Tool group.
114    #[must_use]
115    pub const fn with_dig_material(mut self, m: Option<DigMaterial>) -> Self {
116        self.dig_material = m;
117        self
118    }
119
120    /// Item ids that harvest the block (empty: any).
121    #[must_use]
122    pub const fn with_harvest_tools(mut self, tools: &'static [u16]) -> Self {
123        self.harvest_tools = tools;
124        self
125    }
126
127    /// Selection (ray) box relative to the block.
128    #[must_use]
129    pub const fn with_selection(mut self, b: Option<[f64; 6]>) -> Self {
130        self.selection = b;
131        self
132    }
133
134    /// 1.8 stairs ray passes.
135    #[must_use]
136    pub const fn with_stairs(mut self, passes: Option<[Option<[f64; 6]>; 8]>) -> Self {
137        self.stairs = passes;
138        self
139    }
140
141    /// Right-clicking activates it.
142    #[must_use]
143    pub const fn with_activates(mut self, on: bool) -> Self {
144        self.activates = on;
145        self
146    }
147}
148
149impl HeldItem {
150    /// One item of `id` with nothing else known; set the rest with the `with_*` methods.
151    #[must_use]
152    pub const fn new(id: u16) -> Self {
153        HeldItem {
154            id,
155            count: 1,
156            efficiency: 0,
157            places_block: false,
158            usable: false,
159            food: false,
160            always_edible: false,
161            tool_speed: None,
162        }
163    }
164
165    /// Stack size.
166    #[must_use]
167    pub const fn with_count(mut self, count: u8) -> Self {
168        self.count = count;
169        self
170    }
171
172    /// Efficiency level.
173    #[must_use]
174    pub const fn with_efficiency(mut self, level: u8) -> Self {
175        self.efficiency = level;
176        self
177    }
178
179    /// The item places a block.
180    #[must_use]
181    pub const fn with_places_block(mut self, on: bool) -> Self {
182        self.places_block = on;
183        self
184    }
185
186    /// The item is used by holding right click.
187    #[must_use]
188    pub const fn with_usable(mut self, on: bool) -> Self {
189        self.usable = on;
190        self
191    }
192
193    /// Food (and whether it can be eaten at a full food bar).
194    #[must_use]
195    pub const fn with_food(mut self, food: bool, always_edible: bool) -> Self {
196        self.food = food;
197        self.always_edible = always_edible;
198        self
199    }
200
201    /// Tool speed per dig group.
202    #[must_use]
203    pub const fn with_tool_speed(mut self, f: Option<fn(DigMaterial, u16) -> f32>) -> Self {
204        self.tool_speed = f;
205        self
206    }
207}
208
209impl DigFactors {
210    /// On ground.
211    #[must_use]
212    pub const fn with_on_ground(mut self, on: bool) -> Self {
213        self.on_ground = on;
214        self
215    }
216
217    /// Eyes in water.
218    #[must_use]
219    pub const fn with_in_water(mut self, on: bool) -> Self {
220        self.in_water = on;
221        self
222    }
223
224    /// Aqua affinity on the helmet.
225    #[must_use]
226    pub const fn with_aqua_affinity(mut self, on: bool) -> Self {
227        self.aqua_affinity = on;
228        self
229    }
230
231    /// Haste level (amplifier + 1), 0 = none.
232    #[must_use]
233    pub const fn with_haste(mut self, level: u8) -> Self {
234        self.haste = level;
235        self
236    }
237
238    /// Mining fatigue level (amplifier + 1), 0 = none.
239    #[must_use]
240    pub const fn with_fatigue(mut self, level: u8) -> Self {
241        self.fatigue = level;
242        self
243    }
244}
245
246/// Mining speed modifiers of the player.
247#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
248#[non_exhaustive]
249pub struct DigFactors {
250    /// On ground.
251    pub on_ground: bool,
252    /// Eyes in water.
253    pub in_water: bool,
254    /// Aqua affinity on the helmet.
255    pub aqua_affinity: bool,
256    /// Haste level (amplifier + 1), 0 = none.
257    pub haste: u8,
258    /// Mining fatigue level (amplifier + 1), 0 = none.
259    pub fatigue: u8,
260}
261
262/// `Block.calcBlockBreakingData` (float maths): break progress per tick.
263#[must_use]
264pub fn relative_hardness(b: &BlockQuery, held: Option<&HeldItem>, f: DigFactors) -> f32 {
265    let h = b.hardness;
266    if h < 0.0 {
267        return 0.0;
268    }
269    let can_harvest =
270        b.harvest_tools.is_empty() || held.is_some_and(|i| b.harvest_tools.contains(&i.id));
271    // PlayerEntity.getMiningSpeed
272    let mut speed = match (held, b.dig_material) {
273        (Some(i), Some(m)) => i.tool_speed.map_or(1.0, |t| t(m, i.id)),
274        _ => 1.0,
275    };
276    if speed > 1.0
277        && let Some(i) = held
278        && i.efficiency > 0
279    {
280        let e = f32::from(i.efficiency);
281        speed += e * e + 1.0;
282    }
283    if f.haste > 0 {
284        speed *= 1.0 + f32::from(f.haste) * 0.2;
285    }
286    if f.fatigue > 0 {
287        speed *= match f.fatigue {
288            1 => 0.3,
289            2 => 0.09,
290            3 => 0.0027,
291            _ => 8.1e-4,
292        };
293    }
294    if f.in_water && !f.aqua_affinity {
295        speed /= 5.0;
296    }
297    if !f.on_ground {
298        speed /= 5.0;
299    }
300    if can_harvest {
301        speed / h / 30.0
302    } else {
303        speed / h / 100.0
304    }
305}
306
307/// `Entity.getRotationVector(pitch, yaw)` with vanilla's float sine table.
308#[must_use]
309pub fn look_vector(yaw: f32, pitch: f32) -> Vec3 {
310    use ghast_physics::math::{cos, sin};
311    const RAD: f32 = 0.017_453_292;
312    let f = cos(-yaw * RAD - std::f32::consts::PI);
313    let f1 = sin(-yaw * RAD - std::f32::consts::PI);
314    let f2 = -cos(-pitch * RAD);
315    let f3 = sin(-pitch * RAD);
316    Vec3::new(f64::from(f1 * f2), f64::from(f3), f64::from(f * f2))
317}
318
319/// A block the crosshair is on.
320#[derive(Debug, Clone, Copy, PartialEq)]
321#[non_exhaustive]
322pub struct BlockHit {
323    /// The block.
324    pub pos: BlockPos,
325    /// The face hit.
326    pub face: Face,
327    /// The point hit (absolute).
328    pub hit: Vec3,
329    /// Distance from the eyes.
330    pub distance: f64,
331}
332
333/// Floor of a coordinate as vanilla's `MathHelper.floor`.
334fn floor(v: f64) -> i32 {
335    let i = v as i32;
336    if v < f64::from(i) { i - 1 } else { i }
337}
338
339/// `Vec3d.lerpForX` and friends: the point on the segment `a → b` whose `axis`
340/// coordinate is `v`, if it lies within the segment.
341fn intermediate(a: Vec3, b: Vec3, axis: usize, v: f64) -> Option<Vec3> {
342    let d = [b.x - a.x, b.y - a.y, b.z - a.z];
343    let o = [a.x, a.y, a.z];
344    if d[axis] * d[axis] < 1.0e-7 {
345        return None;
346    }
347    let t = (v - o[axis]) / d[axis];
348    (0.0..=1.0)
349        .contains(&t)
350        .then(|| Vec3::new(a.x + d[0] * t, a.y + d[1] * t, a.z + d[2] * t))
351}
352
353fn sq_dist(a: Vec3, b: Vec3) -> f64 {
354    let (x, y, z) = (b.x - a.x, b.y - a.y, b.z - a.z);
355    x * x + y * y + z * z
356}
357
358/// `Block.rayTrace`: the segment against the block's ray bounds, in
359/// block-local coordinates; the nearest of the six plane hits (ties go to the earlier plane:
360/// west, east, down, up, north, south).
361fn block_ray(pos: BlockPos, b: &[f64; 6], start: Vec3, end: Vec3) -> Option<(Vec3, Face)> {
362    let off = Vec3::new(f64::from(pos.x), f64::from(pos.y), f64::from(pos.z));
363    let s = Vec3::new(start.x - off.x, start.y - off.y, start.z - off.z);
364    let e = Vec3::new(end.x - off.x, end.y - off.y, end.z - off.z);
365    let in_yz = |v: &Vec3| v.y >= b[1] && v.y <= b[4] && v.z >= b[2] && v.z <= b[5];
366    let in_xz = |v: &Vec3| v.x >= b[0] && v.x <= b[3] && v.z >= b[2] && v.z <= b[5];
367    let in_xy = |v: &Vec3| v.x >= b[0] && v.x <= b[3] && v.y >= b[1] && v.y <= b[4];
368    let candidates = [
369        (intermediate(s, e, 0, b[0]).filter(in_yz), Face::West),
370        (intermediate(s, e, 0, b[3]).filter(in_yz), Face::East),
371        (intermediate(s, e, 1, b[1]).filter(in_xz), Face::Down),
372        (intermediate(s, e, 1, b[4]).filter(in_xz), Face::Up),
373        (intermediate(s, e, 2, b[2]).filter(in_xy), Face::North),
374        (intermediate(s, e, 2, b[5]).filter(in_xy), Face::South),
375    ];
376    let mut best: Option<(Vec3, Face)> = None;
377    for (v, face) in candidates {
378        if let Some(v) = v
379            && best.is_none_or(|(bv, _)| sq_dist(s, v) < sq_dist(s, bv))
380        {
381            best = Some((v, face));
382        }
383    }
384    best.map(|(v, f)| (Vec3::new(v.x + off.x, v.y + off.y, v.z + off.z), f))
385}
386
387/// What a ray found.
388#[derive(Debug, Clone, Copy, PartialEq)]
389#[non_exhaustive]
390pub enum RayResult {
391    /// A block's ray bounds.
392    Block(BlockHit),
393    /// Nothing; the last block the ray passed that rays go through (vanilla's "miss" result
394    /// of the crosshair pick).
395    Miss {
396        /// That block.
397        pos: BlockPos,
398        /// The face the ray entered it through.
399        face: Face,
400        /// The entry point.
401        hit: Vec3,
402    },
403    /// Nothing at all.
404    None,
405}
406
407/// 1.8 `World.rayTrace(start, end, false, false, return_last)`: walks the blocks from `start`
408/// to `end` exactly like vanilla (at most 200 steps; −1e−4 for a −0 plane fraction) and tests
409/// each block whose rays collide (`BlockQuery::selection`). Unloaded blocks read as air like
410/// the vanilla client's empty chunks.
411pub fn ray_trace(
412    blocks: &dyn Fn(i32, i32, i32) -> Option<BlockQuery>,
413    start: Vec3,
414    end: Vec3,
415    return_last: bool,
416) -> RayResult {
417    let nan = |v: Vec3| v.x.is_nan() || v.y.is_nan() || v.z.is_nan();
418    if nan(start) || nan(end) {
419        return RayResult::None;
420    }
421    let dist = |hit: Vec3, eyes: Vec3| sq_dist(eyes, hit).sqrt();
422    let eyes = start;
423    let (ex, ey, ez) = (floor(end.x), floor(end.y), floor(end.z));
424    let (mut x, mut y, mut z) = (floor(start.x), floor(start.y), floor(start.z));
425    let test = |p: BlockPos, s: Vec3| -> Option<BlockHit> {
426        let b = blocks(p.x, p.y, p.z)?;
427        let found = if let Some(passes) = &b.stairs {
428            // StairsBlock.rayTrace: the pass hit farthest from the ray's end wins (strictly;
429            // ties keep the earlier pass).
430            let mut best: Option<(Vec3, Face)> = None;
431            let mut best_d = 0.0;
432            for bounds in passes.iter().flatten() {
433                if let Some((hit, face)) = block_ray(p, bounds, s, end) {
434                    let d = sq_dist(hit, end);
435                    if d > best_d {
436                        best = Some((hit, face));
437                        best_d = d;
438                    }
439                }
440            }
441            best
442        } else {
443            block_ray(p, &b.selection?, s, end)
444        };
445        found.map(|(hit, face)| BlockHit {
446            pos: p,
447            face,
448            hit,
449            distance: dist(hit, eyes),
450        })
451    };
452    let first = BlockPos::new(x, y, z);
453    if let Some(h) = test(first, start) {
454        return RayResult::Block(h);
455    }
456    let mut last = RayResult::None;
457    let mut cur = start;
458    let mut steps = 200;
459    while steps >= 0 {
460        steps -= 1;
461        if nan(cur) {
462            return RayResult::None;
463        }
464        if x == ex && y == ey && z == ez {
465            break;
466        }
467        let plane = |e: i32, c: i32| {
468            if e > c {
469                Some(f64::from(c) + 1.0)
470            } else if e < c {
471                Some(f64::from(c))
472            } else {
473                None
474            }
475        };
476        let (px, py, pz) = (plane(ex, x), plane(ey, y), plane(ez, z));
477        let (dx, dy, dz) = (end.x - cur.x, end.y - cur.y, end.z - cur.z);
478        let frac = |p: Option<f64>, o: f64, d: f64| {
479            let t = p.map_or(999.0, |p| (p - o) / d);
480            // Vanilla's `t == -0.0` also matches +0.0: any zero fraction becomes -1e-4.
481            if t == 0.0 { -1.0e-4 } else { t }
482        };
483        let (tx, ty, tz) = (
484            frac(px, cur.x, dx),
485            frac(py, cur.y, dy),
486            frac(pz, cur.z, dz),
487        );
488        let face;
489        if tx < ty && tx < tz {
490            face = if ex > x { Face::West } else { Face::East };
491            cur = Vec3::new(px.unwrap_or(999.0), cur.y + dy * tx, cur.z + dz * tx);
492        } else if ty < tz {
493            face = if ey > y { Face::Down } else { Face::Up };
494            cur = Vec3::new(cur.x + dx * ty, py.unwrap_or(999.0), cur.z + dz * ty);
495        } else {
496            face = if ez > z { Face::North } else { Face::South };
497            cur = Vec3::new(cur.x + dx * tz, cur.y + dy * tz, pz.unwrap_or(999.0));
498        }
499        x = floor(cur.x) - i32::from(face == Face::East);
500        y = floor(cur.y) - i32::from(face == Face::Up);
501        z = floor(cur.z) - i32::from(face == Face::South);
502        let p = BlockPos::new(x, y, z);
503        match blocks(x, y, z).filter(|b| b.selection.is_some() || b.stairs.is_some()) {
504            Some(_) => {
505                if let Some(h) = test(p, cur) {
506                    return RayResult::Block(h);
507                }
508            }
509            None => {
510                last = RayResult::Miss {
511                    pos: p,
512                    face,
513                    hit: cur,
514                }
515            }
516        }
517    }
518    if return_last { last } else { RayResult::None }
519}
520
521/// The crosshair block: `Entity.rayTrace(reach)` from `eyes` along `look`.
522#[must_use]
523pub fn pick(
524    blocks: &dyn Fn(i32, i32, i32) -> Option<BlockQuery>,
525    eyes: Vec3,
526    look: Vec3,
527    reach: f64,
528) -> Option<BlockHit> {
529    let end = Vec3::new(
530        eyes.x + look.x * reach,
531        eyes.y + look.y * reach,
532        eyes.z + look.z * reach,
533    );
534    match ray_trace(blocks, eyes, end, true) {
535        RayResult::Block(h) => Some(h),
536        _ => None,
537    }
538}
539
540/// An entity the crosshair is on.
541#[derive(Debug, Clone, Copy, PartialEq)]
542#[non_exhaustive]
543pub struct EntityHit {
544    /// The entity id.
545    pub id: i32,
546    /// The point hit (absolute; the eyes when they are inside the box).
547    pub hit: Vec3,
548    /// The entity's position (for interact-at offsets).
549    pub entity_pos: Vec3,
550    /// Distance from the eyes.
551    pub distance: f64,
552}
553
554/// `Box.method_585` (unnamed in Legacy Yarn): the nearest plane hit of the segment `a → b` on the
555/// box (`x` planes, then `y`, then `z`; ties keep the earlier one).
556fn box_intercept(b: &[f64; 6], a: Vec3, e: Vec3) -> Option<Vec3> {
557    let in_yz = |v: &Vec3| v.y >= b[1] && v.y <= b[4] && v.z >= b[2] && v.z <= b[5];
558    let in_xz = |v: &Vec3| v.x >= b[0] && v.x <= b[3] && v.z >= b[2] && v.z <= b[5];
559    let in_xy = |v: &Vec3| v.x >= b[0] && v.x <= b[3] && v.y >= b[1] && v.y <= b[4];
560    let cands = [
561        intermediate(a, e, 0, b[0]).filter(in_yz),
562        intermediate(a, e, 0, b[3]).filter(in_yz),
563        intermediate(a, e, 1, b[1]).filter(in_xz),
564        intermediate(a, e, 1, b[4]).filter(in_xz),
565        intermediate(a, e, 2, b[2]).filter(in_xy),
566        intermediate(a, e, 2, b[5]).filter(in_xy),
567    ];
568    let mut best: Option<Vec3> = None;
569    for v in cands.into_iter().flatten() {
570        if best.is_none_or(|bv| sq_dist(a, v) < sq_dist(a, bv)) {
571            best = Some(v);
572        }
573    }
574    best
575}
576
577/// Whether the ray from `eyes` along `look` (unit) meets the full cube at `pos` within
578/// `reach`, or starts inside it.
579#[must_use]
580pub(crate) fn ray_hits_cube(eyes: Vec3, look: Vec3, pos: BlockPos, reach: f64) -> bool {
581    let (x, y, z) = (f64::from(pos.x), f64::from(pos.y), f64::from(pos.z));
582    let b = [x, y, z, x + 1.0, y + 1.0, z + 1.0];
583    let inside = eyes.x >= b[0]
584        && eyes.x <= b[3]
585        && eyes.y >= b[1]
586        && eyes.y <= b[4]
587        && eyes.z >= b[2]
588        && eyes.z <= b[5];
589    let end = Vec3::new(
590        eyes.x + look.x * reach,
591        eyes.y + look.y * reach,
592        eyes.z + look.z * reach,
593    );
594    inside || box_intercept(&b, eyes, end).is_some()
595}
596
597/// One pickable entity: id, box (absolute), position.
598pub type PickCandidate = (i32, [f64; 6], Vec3);
599
600/// `GameRenderer.updateTargetedEntity`: the entity whose box grown by `border` (1.8:
601/// 0.1, 1.9+: 0) the look ray (reach 4.5) hits first, closer than the block hit; dropped
602/// beyond 3 blocks (survival). `riding` = the vehicle id (only picked when nothing else is).
603#[must_use]
604pub fn pick_entity(
605    candidates: impl Iterator<Item = PickCandidate>,
606    eyes: Vec3,
607    look: Vec3,
608    block_distance: Option<f64>,
609    border: f64,
610    riding: Option<i32>,
611) -> Option<EntityHit> {
612    let d0 = REACH;
613    let d1 = block_distance.unwrap_or(d0);
614    let end = Vec3::new(
615        eyes.x + look.x * d0,
616        eyes.y + look.y * d0,
617        eyes.z + look.z * d0,
618    );
619    let mut best: Option<EntityHit> = None;
620    let mut d2 = d1;
621    for (id, b, pos) in candidates {
622        let g = [
623            b[0] - border,
624            b[1] - border,
625            b[2] - border,
626            b[3] + border,
627            b[4] + border,
628            b[5] + border,
629        ];
630        let hit = box_intercept(&g, eyes, end);
631        let inside = eyes.x > g[0]
632            && eyes.x < g[3]
633            && eyes.y > g[1]
634            && eyes.y < g[4]
635            && eyes.z > g[2]
636            && eyes.z < g[5];
637        if inside {
638            if d2 >= 0.0 {
639                best = Some(EntityHit {
640                    id,
641                    hit: hit.unwrap_or(eyes),
642                    entity_pos: pos,
643                    distance: 0.0,
644                });
645                d2 = 0.0;
646            }
647        } else if let Some(h) = hit {
648            let d3 = sq_dist(eyes, h).sqrt();
649            if d3 < d2 || d2 == 0.0 {
650                if riding == Some(id) {
651                    if d2 == 0.0 {
652                        best = Some(EntityHit {
653                            id,
654                            hit: h,
655                            entity_pos: pos,
656                            distance: d3,
657                        });
658                    }
659                } else {
660                    best = Some(EntityHit {
661                        id,
662                        hit: h,
663                        entity_pos: pos,
664                        distance: d3,
665                    });
666                    d2 = d3;
667                }
668            }
669        }
670    }
671    let e = best?;
672    // Survival: the entity counts only within 3 blocks, and only when closer than the block.
673    if sq_dist(eyes, e.hit).sqrt() > 3.0 {
674        return None;
675    }
676    (d2 < d1 || block_distance.is_none()).then_some(e)
677}
678
679/// Mouse buttons held in the world (kept until changed; released by a join or respawn).
680#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
681pub struct Buttons {
682    /// Left: mine / attack.
683    pub left: bool,
684    /// Right: use / place.
685    pub right: bool,
686}
687
688/// What the hands need from the client each tick.
689pub(crate) struct HandsIo<'a> {
690    /// Block lookup (version-specific facts).
691    pub blocks: &'a dyn Fn(i32, i32, i32) -> Option<BlockQuery>,
692    /// The crosshair now.
693    pub target: Option<BlockHit>,
694    /// The held stack's dig / use facts.
695    pub held: Option<HeldItem>,
696    /// The held stack itself (sent with 1.8 placement packets).
697    pub held_stack: Option<ghast_inventory::Stack>,
698    /// Food level (food is only eaten below 20 unless always edible).
699    pub food: i32,
700    /// 1.9+ rules: swing after the click, both hands tried on right click.
701    pub modern: bool,
702    /// The off-hand stack's facts (1.9+).
703    pub off_held: Option<HeldItem>,
704    /// The off-hand stack itself.
705    pub off_stack: Option<ghast_inventory::Stack>,
706    /// Mining speed modifiers.
707    pub factors: DigFactors,
708    /// Sneaking (skips block activation when holding an item).
709    pub sneaking: bool,
710    /// The entity the crosshair is on (it wins over `target` when set).
711    pub entity: Option<EntityHit>,
712    /// Packets out (in order).
713    pub out: &'a mut Vec<Outbound>,
714    /// Blocks to predict as air (broken on the client).
715    pub broken: &'a mut Vec<BlockPos>,
716    /// The held stack's count went down by one (a block was placed).
717    pub placed: &'a mut bool,
718}
719
720impl HandsIo<'_> {
721    /// The non-air block at `pos` (`None`: air or not loaded). A block broken earlier in this
722    /// tick (by the behaviour's `dig` or the hands) reads as air, like vanilla, which sets it
723    /// to air in the world at once: the held button must not start it again in the same tick.
724    fn solid(&self, pos: BlockPos) -> Option<BlockQuery> {
725        if self.broken.contains(&pos) {
726            return None;
727        }
728        (self.blocks)(pos.x, pos.y, pos.z).filter(|b| !b.air)
729    }
730}
731
732/// `ClientPlayerInteractionManager` + the `MinecraftClient` click state.
733#[derive(Debug, Clone, Default)]
734pub struct Hands {
735    hitting: bool,
736    current: Option<BlockPos>,
737    damage: f32,
738    hit_item: Option<u16>,
739    hit_delay: u32,
740    left_click_counter: u32,
741    right_click_delay: u32,
742    using: bool,
743    left_was: bool,
744    right_was: bool,
745    /// Blocks broken (finish sent).
746    pub broken: u64,
747    /// 1.9+: ticks since the last swing / attack (`ticksSinceLastSwing`, attack cooldown).
748    pub ticks_since_swing: u32,
749}
750
751impl Hands {
752    /// A new player: nothing held, nothing in progress.
753    pub fn reset(&mut self) {
754        let broken = self.broken;
755        *self = Hands::default();
756        self.broken = broken;
757    }
758
759    /// The block being broken and its progress 0..1.
760    #[must_use]
761    pub fn mining(&self) -> Option<(BlockPos, f32)> {
762        match (self.hitting, self.current) {
763            (true, Some(p)) => Some((p, self.damage.min(1.0))),
764            _ => None,
765        }
766    }
767
768    /// Using the held item (eating, blocking, drawing a bow).
769    #[must_use]
770    pub fn using_item(&self) -> bool {
771        self.using
772    }
773
774    /// The server finished our item use (entity status 9) or the held slot changed.
775    pub fn stop_using(&mut self) {
776        self.using = false;
777    }
778
779    /// One tick with the buttons as held now.
780    /// 1.9+ attack strength: `clamp((ticks_since_swing + 0.5) / (20 / attack_speed), 0, 1)`
781    /// (vanilla `getCooledAttackStrength(0.5F)`; attack speed 4.0 with an empty hand).
782    #[must_use]
783    pub fn attack_strength(&self, attack_speed: f64) -> f32 {
784        let period = (1.0 / attack_speed * 20.0) as f32;
785        ((self.ticks_since_swing as f32 + 0.5) / period).clamp(0.0, 1.0)
786    }
787
788    /// `PlayerController.attackEntity`: the attack packet (1.16: then the cooldown resets).
789    pub(crate) fn attack(&mut self, id: i32, io: &mut HandsIo<'_>) {
790        io.out.push(Outbound::UseEntity {
791            target: id,
792            action: crate::common::EntityUse::Attack,
793            off_hand: false,
794            sneaking: io.sneaking,
795        });
796        self.ticks_since_swing = 0;
797    }
798
799    /// Right click on an entity: interact-at then interact (both assumed not handled, so the
800    /// item use follows like vanilla's fall-through for plain entities).
801    fn interact(&mut self, e: EntityHit, off_hand: bool, io: &mut HandsIo<'_>) {
802        let rel = Vec3::new(
803            e.hit.x - e.entity_pos.x,
804            e.hit.y - e.entity_pos.y,
805            e.hit.z - e.entity_pos.z,
806        );
807        io.out.push(Outbound::UseEntity {
808            target: e.id,
809            action: crate::common::EntityUse::InteractAt(rel),
810            off_hand,
811            sneaking: io.sneaking,
812        });
813        io.out.push(Outbound::UseEntity {
814            target: e.id,
815            action: crate::common::EntityUse::Interact,
816            off_hand,
817            sneaking: io.sneaking,
818        });
819    }
820
821    pub(crate) fn tick(&mut self, left: bool, right: bool, io: &mut HandsIo<'_>) {
822        self.ticks_since_swing = self.ticks_since_swing.saturating_add(1);
823        if self.left_click_counter > 0 {
824            self.left_click_counter -= 1;
825        }
826        if self.right_click_delay > 0 {
827            self.right_click_delay -= 1;
828        }
829        let t = io.target;
830        if self.using {
831            if !right {
832                // stopUsingItem
833                io.out.push(Outbound::Dig {
834                    status: DigStatus::ReleaseUseItem,
835                    pos: BlockPos::new(0, 0, 0),
836                    face: Face::Down,
837                });
838                self.using = false;
839            }
840        } else {
841            if left && !self.left_was {
842                self.click_mouse(t, io);
843            }
844            if right && !self.right_was {
845                self.right_click_mouse(t, io);
846            }
847        }
848        if right && self.right_click_delay == 0 && !self.using {
849            self.right_click_mouse(t, io);
850        }
851        self.send_click_block_to_controller(left, t, io);
852        self.left_was = left;
853        self.right_was = right;
854    }
855
856    fn click_mouse(&mut self, t: Option<BlockHit>, io: &mut HandsIo<'_>) {
857        if self.left_click_counter > 0 {
858            return;
859        }
860        if let Some(e) = io.entity {
861            if io.modern {
862                self.attack(e.id, io);
863                io.out.push(Outbound::Swing);
864            } else {
865                io.out.push(Outbound::Swing);
866                self.attack(e.id, io);
867            }
868            return;
869        }
870        if io.modern {
871            // 1.16 MinecraftClient.doAttack: the action, then the swing; a block hit on air falls
872            // through to the miss case.
873            match t {
874                Some(hit) if io.solid(hit.pos).is_some() => {
875                    self.click_block(hit.pos, hit.face, io);
876                }
877                _ => {
878                    self.left_click_counter = 10;
879                    // resetCooldown on a miss.
880                    self.ticks_since_swing = 0;
881                }
882            }
883            io.out.push(Outbound::Swing);
884            return;
885        }
886        io.out.push(Outbound::Swing);
887        match t {
888            Some(hit) => {
889                if io.solid(hit.pos).is_some() {
890                    self.click_block(hit.pos, hit.face, io);
891                }
892            }
893            None => self.left_click_counter = 10,
894        }
895    }
896
897    fn send_click_block_to_controller(
898        &mut self,
899        left: bool,
900        t: Option<BlockHit>,
901        io: &mut HandsIo<'_>,
902    ) {
903        if !left {
904            self.left_click_counter = 0;
905        }
906        if self.left_click_counter > 0 || self.using {
907            return;
908        }
909        match t.filter(|_| io.entity.is_none()) {
910            Some(hit) if left => {
911                let solid = io.solid(hit.pos).is_some();
912                if solid && self.damage_block(hit.pos, hit.face, io) {
913                    io.out.push(Outbound::Swing);
914                }
915            }
916            _ => self.reset_block_removing(io),
917        }
918    }
919
920    fn hitting_position(&self, pos: BlockPos, io: &HandsIo<'_>) -> bool {
921        self.hitting && self.current == Some(pos) && self.hit_item == io.held.map(|h| h.id)
922    }
923
924    /// `attackBlock`: START (aborting a previous block), instant break when fast enough.
925    pub(crate) fn click_block(&mut self, pos: BlockPos, face: Face, io: &mut HandsIo<'_>) {
926        if self.hitting && self.hitting_position(pos, io) {
927            return;
928        }
929        if self.hitting
930            && let Some(cur) = self.current
931        {
932            io.out.push(Outbound::Dig {
933                status: DigStatus::Abort,
934                pos: cur,
935                face,
936            });
937        }
938        io.out.push(Outbound::Dig {
939            status: DigStatus::Start,
940            pos,
941            face,
942        });
943        if let Some(b) = io.solid(pos)
944            && relative_hardness(&b, io.held.as_ref(), io.factors) >= 1.0
945        {
946            self.destroyed(pos, io);
947            return;
948        }
949        self.hitting = true;
950        self.current = Some(pos);
951        self.hit_item = io.held.map(|h| h.id);
952        self.damage = 0.0;
953    }
954
955    /// `updateBlockBreakingProgress`.
956    fn damage_block(&mut self, pos: BlockPos, face: Face, io: &mut HandsIo<'_>) -> bool {
957        if self.hit_delay > 0 {
958            self.hit_delay -= 1;
959            return true;
960        }
961        if !self.hitting_position(pos, io) {
962            self.click_block(pos, face, io);
963            return true;
964        }
965        let Some(b) = io.solid(pos) else {
966            self.hitting = false;
967            return false;
968        };
969        self.damage += relative_hardness(&b, io.held.as_ref(), io.factors);
970        if self.damage >= 1.0 {
971            self.hitting = false;
972            io.out.push(Outbound::Dig {
973                status: DigStatus::Finish,
974                pos,
975                face,
976            });
977            self.destroyed(pos, io);
978            self.damage = 0.0;
979            self.hit_delay = 5;
980        }
981        true
982    }
983
984    /// `breakBlock`: air on the client; the current block moves to y = −1.
985    fn destroyed(&mut self, pos: BlockPos, io: &mut HandsIo<'_>) {
986        io.broken.push(pos);
987        if let Some(c) = self.current {
988            self.current = Some(BlockPos::new(c.x, -1, c.z));
989        }
990        self.broken += 1;
991    }
992
993    /// `cancelBlockBreaking`.
994    pub(crate) fn reset_block_removing(&mut self, io: &mut HandsIo<'_>) {
995        if !self.hitting {
996            return;
997        }
998        if let Some(cur) = self.current {
999            io.out.push(Outbound::Dig {
1000                status: DigStatus::Abort,
1001                pos: cur,
1002                face: Face::Down,
1003            });
1004        }
1005        self.hitting = false;
1006        self.damage = 0.0;
1007    }
1008
1009    /// `doUse` (nothing while a block is being hit).
1010    fn right_click_mouse(&mut self, t: Option<BlockHit>, io: &mut HandsIo<'_>) {
1011        if self.hitting {
1012            return;
1013        }
1014        self.right_click_delay = 4;
1015        if io.modern {
1016            self.right_click_modern(t, io);
1017            return;
1018        }
1019        if let Some(e) = io.entity {
1020            // 1.8: interact-at, interact; not handled -> the item is used.
1021            self.interact(e, false, io);
1022            if let Some(h) = io.held {
1023                io.out.push(Outbound::UseItem {
1024                    held: io.held_stack.clone(),
1025                    off_hand: false,
1026                });
1027                if h.usable && (!h.food || h.always_edible || io.food < 20) {
1028                    self.using = true;
1029                }
1030            }
1031            return;
1032        }
1033        let mut fall_through = true;
1034        if let Some(hit) = t
1035            && let Some(b) = io.solid(hit.pos)
1036        {
1037            if self.right_click_block(hit, &b, io) {
1038                fall_through = false;
1039                io.out.push(Outbound::Swing);
1040            }
1041            if io.held.is_none() {
1042                return;
1043            }
1044        }
1045        if fall_through && let Some(h) = io.held {
1046            // interactItem: the item itself (position -1, face 255).
1047            io.out.push(Outbound::UseItem {
1048                held: io.held_stack.clone(),
1049                off_hand: false,
1050            });
1051            // FoodItem.onStartUse: only when the player can eat.
1052            if h.usable && (!h.food || h.always_edible || io.food < 20) {
1053                self.using = true;
1054            }
1055        }
1056    }
1057
1058    /// 1.9+ `doItemUse` (1.16.5 Yarn): main hand, then off hand; per hand the block (activation, then
1059    /// the item on the block), then the item itself; the first success / consume ends it.
1060    fn right_click_modern(&mut self, t: Option<BlockHit>, io: &mut HandsIo<'_>) {
1061        let any_item = io.held.is_some() || io.off_held.is_some();
1062        for off_hand in [false, true] {
1063            let (held, stack) = if off_hand {
1064                (io.off_held, io.off_stack.clone())
1065            } else {
1066                (io.held, io.held_stack.clone())
1067            };
1068            let swing = if off_hand {
1069                Outbound::SwingOffHand
1070            } else {
1071                Outbound::Swing
1072            };
1073            if let Some(e) = io.entity {
1074                self.interact(e, off_hand, io);
1075            } else if let Some(hit) = t
1076                && let Some(b) = io.solid(hit.pos)
1077            {
1078                let cursor = Vec3::new(
1079                    hit.hit.x - f64::from(hit.pos.x),
1080                    hit.hit.y - f64::from(hit.pos.y),
1081                    hit.hit.z - f64::from(hit.pos.z),
1082                );
1083                io.out.push(Outbound::Place {
1084                    pos: hit.pos,
1085                    face: hit.face,
1086                    cursor,
1087                    held: stack.clone(),
1088                    off_hand,
1089                });
1090                // Sneaking with something in either hand skips the block's activation.
1091                if b.activates && !(io.sneaking && any_item) {
1092                    io.out.push(swing);
1093                    return;
1094                }
1095                if let Some(h) = held
1096                    && h.places_block
1097                    && h.count > 0
1098                {
1099                    if !off_hand {
1100                        *io.placed = true;
1101                    }
1102                    io.out.push(swing);
1103                    return;
1104                }
1105            }
1106            if let Some(h) = held {
1107                io.out.push(Outbound::UseItem {
1108                    held: stack,
1109                    off_hand,
1110                });
1111                if h.usable && (!h.food || h.always_edible || io.food < 20) {
1112                    self.using = true;
1113                    return;
1114                }
1115            }
1116        }
1117    }
1118
1119    /// `onRightClick`: the placement packet; returns whether something was activated or
1120    /// placed (the caller swings).
1121    pub(crate) fn right_click_block(
1122        &mut self,
1123        hit: BlockHit,
1124        b: &BlockQuery,
1125        io: &mut HandsIo<'_>,
1126    ) -> bool {
1127        let cursor = Vec3::new(
1128            f64::from((hit.hit.x - f64::from(hit.pos.x)) as f32),
1129            f64::from((hit.hit.y - f64::from(hit.pos.y)) as f32),
1130            f64::from((hit.hit.z - f64::from(hit.pos.z)) as f32),
1131        );
1132        let activated = b.activates && (!io.sneaking || io.held.is_none());
1133        io.out.push(Outbound::Place {
1134            pos: hit.pos,
1135            face: hit.face,
1136            cursor,
1137            held: io.held_stack.clone(),
1138            off_hand: false,
1139        });
1140        if activated {
1141            return true;
1142        }
1143        match io.held {
1144            Some(h) if h.places_block && h.count > 0 => {
1145                *io.placed = true;
1146                true
1147            }
1148            _ => false,
1149        }
1150    }
1151}
1152
1153#[cfg(test)]
1154mod tests {
1155    use super::*;
1156
1157    fn stone() -> BlockQuery {
1158        BlockQuery {
1159            air: false,
1160            water: false,
1161            block: 1,
1162            hardness: 1.5,
1163            dig_material: Some(DigMaterial::Rock),
1164            harvest_tools: &[270, 274],
1165            selection: Some([0.0, 0.0, 0.0, 1.0, 1.0, 1.0]),
1166            stairs: None,
1167            activates: false,
1168        }
1169    }
1170
1171    #[test]
1172    fn hand_stone_time() {
1173        // Stone by hand, on ground: 1 / 1.5 / 100 per tick; the f32 sum (as vanilla's
1174        // currentBreakingProgress) reaches 1 on the 151st tick, not the 150th.
1175        let f = DigFactors {
1176            on_ground: true,
1177            ..DigFactors::default()
1178        };
1179        let per_tick = relative_hardness(&stone(), None, f);
1180        let mut d = 0.0_f32;
1181        let mut n = 0;
1182        while d < 1.0 {
1183            d += per_tick;
1184            n += 1;
1185        }
1186        assert_eq!(n, 151);
1187    }
1188
1189    #[test]
1190    fn look_south_hits_block() {
1191        let look = look_vector(0.0, 0.0);
1192        assert!(look.z > 0.99 && look.x.abs() < 1e-3);
1193        let blocks = |x: i32, y: i32, z: i32| {
1194            Some(if (x, y, z) == (0, 1, 2) {
1195                stone()
1196            } else {
1197                BlockQuery {
1198                    air: true,
1199                    selection: None,
1200                    ..stone()
1201                }
1202            })
1203        };
1204        let hit = pick(&blocks, Vec3::new(0.5, 1.62, 0.5), look, REACH).unwrap();
1205        assert_eq!(hit.pos, BlockPos::new(0, 1, 2));
1206        assert_eq!(hit.face, Face::North);
1207        assert!((hit.distance - 1.5).abs() < 1e-3);
1208    }
1209
1210    fn io_blocks(x: i32, y: i32, z: i32) -> Option<BlockQuery> {
1211        Some(if (x, y, z) == (0, 0, 0) {
1212            stone()
1213        } else {
1214            BlockQuery {
1215                air: true,
1216                selection: None,
1217                ..stone()
1218            }
1219        })
1220    }
1221
1222    fn kinds(out: &[Outbound]) -> Vec<&'static str> {
1223        out.iter()
1224            .map(|o| match o {
1225                Outbound::Place {
1226                    off_hand: false, ..
1227                } => "place_main",
1228                Outbound::Place { off_hand: true, .. } => "place_off",
1229                Outbound::UseItem {
1230                    off_hand: false, ..
1231                } => "use_main",
1232                Outbound::UseItem { off_hand: true, .. } => "use_off",
1233                Outbound::Swing => "swing",
1234                Outbound::SwingOffHand => "swing_off",
1235                Outbound::Dig { .. } => "dig",
1236                _ => "other",
1237            })
1238            .collect()
1239    }
1240
1241    /// 1.16 right click: empty hands try both hands on the block; a block item places with
1242    /// the main hand and swings.
1243    #[test]
1244    fn modern_right_click_hands() {
1245        let hit = Some(BlockHit {
1246            pos: BlockPos::new(0, 0, 0),
1247            face: Face::Up,
1248            hit: Vec3::new(0.5, 1.0, 0.5),
1249            distance: 1.6,
1250        });
1251        let run = |held: Option<HeldItem>| {
1252            let mut h = Hands::default();
1253            let (mut out, mut broken, mut placed) = (Vec::new(), Vec::new(), false);
1254            let mut io = HandsIo {
1255                blocks: &io_blocks,
1256                target: hit,
1257                held,
1258                held_stack: None,
1259                food: 20,
1260                modern: true,
1261                off_held: None,
1262                off_stack: None,
1263                entity: None,
1264                factors: DigFactors::default(),
1265                sneaking: false,
1266                out: &mut out,
1267                broken: &mut broken,
1268                placed: &mut placed,
1269            };
1270            h.tick(false, true, &mut io);
1271            kinds(&out)
1272        };
1273        assert_eq!(run(None), ["place_main", "place_off"]);
1274        let dirt = HeldItem {
1275            id: 3,
1276            count: 10,
1277            places_block: true,
1278            ..HeldItem::default()
1279        };
1280        assert_eq!(run(Some(dirt)), ["place_main", "swing"]);
1281        // An item that is neither a block nor usable: place on the block, then use it.
1282        let stick = HeldItem {
1283            id: 280,
1284            count: 1,
1285            ..HeldItem::default()
1286        };
1287        assert_eq!(run(Some(stick)), ["place_main", "use_main", "place_off"]);
1288    }
1289
1290    /// Pressing the left button on an instant-break block sends one START: the held-button
1291    /// pass of the same tick sees the block as air (vanilla breaks it in the world at once),
1292    /// for 1.8 and 1.16 alike. Also with a block the behaviour broke earlier in the tick.
1293    #[test]
1294    fn instant_break_press_starts_once() {
1295        fn instant(x: i32, y: i32, z: i32) -> Option<BlockQuery> {
1296            Some(if (x, y, z) == (0, 0, 0) {
1297                BlockQuery {
1298                    hardness: 0.0,
1299                    ..stone()
1300                }
1301            } else {
1302                BlockQuery {
1303                    air: true,
1304                    selection: None,
1305                    ..stone()
1306                }
1307            })
1308        }
1309        let hit = Some(BlockHit {
1310            pos: BlockPos::new(0, 0, 0),
1311            face: Face::Up,
1312            hit: Vec3::new(0.5, 1.0, 0.5),
1313            distance: 1.6,
1314        });
1315        for modern in [false, true] {
1316            for pre_broken in [false, true] {
1317                let mut h = Hands::default();
1318                let (mut out, mut placed) = (Vec::new(), false);
1319                let mut broken = if pre_broken {
1320                    vec![BlockPos::new(0, 0, 0)]
1321                } else {
1322                    Vec::new()
1323                };
1324                let mut io = HandsIo {
1325                    blocks: &instant,
1326                    target: hit,
1327                    held: None,
1328                    held_stack: None,
1329                    food: 20,
1330                    modern,
1331                    off_held: None,
1332                    off_stack: None,
1333                    entity: None,
1334                    factors: DigFactors::default(),
1335                    sneaking: false,
1336                    out: &mut out,
1337                    broken: &mut broken,
1338                    placed: &mut placed,
1339                };
1340                h.tick(true, false, &mut io);
1341                let starts = out
1342                    .iter()
1343                    .filter(|o| {
1344                        matches!(
1345                            o,
1346                            Outbound::Dig {
1347                                status: DigStatus::Start,
1348                                ..
1349                            }
1350                        )
1351                    })
1352                    .count();
1353                let want = usize::from(!pre_broken);
1354                assert_eq!(
1355                    starts, want,
1356                    "modern {modern} pre_broken {pre_broken}: {out:?}"
1357                );
1358                assert_eq!(h.broken, want as u64);
1359            }
1360        }
1361    }
1362
1363    #[test]
1364    fn entity_pick_rules() {
1365        let eyes = Vec3::new(0.5, 1.62, 0.5);
1366        let look = look_vector(0.0, 0.0); // +z
1367        let at = |z: f64| {
1368            (
1369                7,
1370                [0.2, 1.0, z, 0.8, 2.0, z + 0.6],
1371                Vec3::new(0.5, 1.0, z + 0.3),
1372            )
1373        };
1374        // 2 blocks ahead: hit on the north face, grown by the 1.8 border.
1375        let hit = pick_entity([at(2.5)].into_iter(), eyes, look, None, 0.1, None).unwrap();
1376        assert_eq!(hit.id, 7);
1377        assert!((hit.hit.z - 2.4).abs() < 1e-9, "{hit:?}");
1378        // Behind a block hit at 1.5: not picked.
1379        assert!(pick_entity([at(2.5)].into_iter(), eyes, look, Some(1.5), 0.1, None).is_none());
1380        // Beyond 3 blocks: not picked (survival).
1381        assert!(pick_entity([at(3.6)].into_iter(), eyes, look, None, 0.0, None).is_none());
1382        // Eyes inside the box: picked at distance 0.
1383        let inside = (8, [0.0, 1.0, 0.0, 1.0, 2.0, 1.0], Vec3::new(0.5, 1.0, 0.5));
1384        assert_eq!(
1385            pick_entity([inside].into_iter(), eyes, look, None, 0.0, None).map(|h| h.distance),
1386            Some(0.0)
1387        );
1388    }
1389
1390    /// 1.16 left click: block_dig start, then the swing (1.8: swing first).
1391    #[test]
1392    fn modern_left_click_order() {
1393        let hit = Some(BlockHit {
1394            pos: BlockPos::new(0, 0, 0),
1395            face: Face::Up,
1396            hit: Vec3::new(0.5, 1.0, 0.5),
1397            distance: 1.6,
1398        });
1399        for (modern, want) in [(true, ["dig", "swing"]), (false, ["swing", "dig"])] {
1400            let mut h = Hands::default();
1401            let (mut out, mut broken, mut placed) = (Vec::new(), Vec::new(), false);
1402            let mut io = HandsIo {
1403                blocks: &io_blocks,
1404                target: hit,
1405                held: None,
1406                held_stack: None,
1407                food: 20,
1408                modern,
1409                off_held: None,
1410                off_stack: None,
1411                entity: None,
1412                factors: DigFactors::default(),
1413                sneaking: false,
1414                out: &mut out,
1415                broken: &mut broken,
1416                placed: &mut placed,
1417            };
1418            h.click_mouse(hit, &mut io);
1419            assert_eq!(kinds(&out)[..2], want);
1420        }
1421    }
1422
1423    #[test]
1424    fn dig_sequence() {
1425        let blocks = |x: i32, y: i32, z: i32| {
1426            Some(if (x, y, z) == (0, 0, 0) {
1427                BlockQuery {
1428                    hardness: 0.6,
1429                    ..stone()
1430                }
1431            } else {
1432                BlockQuery {
1433                    air: true,
1434                    selection: None,
1435                    ..stone()
1436                }
1437            })
1438        };
1439        let target = Some(BlockHit {
1440            pos: BlockPos::new(0, 0, 0),
1441            face: Face::Up,
1442            hit: Vec3::new(0.5, 1.0, 0.5),
1443            distance: 1.6,
1444        });
1445        let mut h = Hands::default();
1446        let mut out = Vec::new();
1447        let mut broken = Vec::new();
1448        let mut placed = false;
1449        let mut ticks = 0;
1450        while broken.is_empty() && ticks < 200 {
1451            let mut io = HandsIo {
1452                blocks: &blocks,
1453                target,
1454                held: None,
1455                held_stack: None,
1456                food: 20,
1457                modern: false,
1458                off_held: None,
1459                off_stack: None,
1460                entity: None,
1461                factors: DigFactors {
1462                    on_ground: true,
1463                    ..DigFactors::default()
1464                },
1465                sneaking: false,
1466                out: &mut out,
1467                broken: &mut broken,
1468                placed: &mut placed,
1469            };
1470            h.tick(true, false, &mut io);
1471            ticks += 1;
1472        }
1473        // 0.6 hardness, not harvestable by hand: 1/0.6/100 per tick; the f32 sum reaches 1 on
1474        // the 61st tick (damage is added from the START tick on).
1475        assert_eq!(broken, [BlockPos::new(0, 0, 0)]);
1476        let digs: Vec<_> = out
1477            .iter()
1478            .filter_map(|o| match o {
1479                Outbound::Dig { status, .. } => Some(*status),
1480                _ => None,
1481            })
1482            .collect();
1483        assert_eq!(digs, [DigStatus::Start, DigStatus::Finish]);
1484        assert_eq!(ticks, 61);
1485        // The click swings, and every damage tick swings (two on the first tick).
1486        assert_eq!(
1487            out.iter().filter(|o| matches!(o, Outbound::Swing)).count(),
1488            62
1489        );
1490    }
1491}