1use std::panic::{AssertUnwindSafe, catch_unwind};
25use std::sync::Arc;
26use std::time::{Duration, Instant};
27
28use crate::common::{
29 Abilities, ChatMessage, ClientSettings, Controls, GameMode, Health, Inbound, MovePacket,
30 Outbound, Pose, Vec3,
31};
32use crate::hands::{self, BlockHit, Buttons, DigFactors, Hands, HandsIo};
33use crate::interest::Interests;
34use crate::rotation::Rotation;
35use crate::sim::{Sim, WorldView};
36use crate::version::GameVersion;
37
38const MAX_PENDING_CHAT: usize = 512;
41
42pub trait Behavior {
48 fn name(&self) -> &str {
50 "behavior"
51 }
52
53 fn interests(&self) -> Interests {
56 Interests::NONE
57 }
58
59 fn raw_packets(&self, version: GameVersion) -> std::borrow::Cow<'static, [i32]> {
65 let _ = version;
66 std::borrow::Cow::Borrowed(&[])
67 }
68
69 fn start(&mut self, ctx: &mut TickContext<'_>) {
72 let _ = ctx;
73 }
74
75 fn tick(&mut self, ctx: &mut TickContext<'_>);
77
78 fn running(&self) -> bool {
82 true
83 }
84
85 fn after_tick(&mut self, view: &TickView<'_>) {
108 let _ = view;
109 }
110
111 fn stop(&mut self, reason: &str) {
113 let _ = reason;
114 }
115}
116
117impl<B: Behavior + ?Sized> Behavior for Box<B> {
119 fn name(&self) -> &str {
120 (**self).name()
121 }
122 fn interests(&self) -> Interests {
123 (**self).interests()
124 }
125 fn raw_packets(&self, version: GameVersion) -> std::borrow::Cow<'static, [i32]> {
126 (**self).raw_packets(version)
127 }
128 fn start(&mut self, ctx: &mut TickContext<'_>) {
129 (**self).start(ctx);
130 }
131 fn tick(&mut self, ctx: &mut TickContext<'_>) {
132 (**self).tick(ctx);
133 }
134 fn running(&self) -> bool {
135 (**self).running()
136 }
137 fn after_tick(&mut self, view: &TickView<'_>) {
138 (**self).after_tick(view);
139 }
140 fn stop(&mut self, reason: &str) {
141 (**self).stop(reason);
142 }
143}
144
145#[derive(Debug, Clone, Copy, PartialEq, Eq)]
147#[non_exhaustive]
148pub enum ClickRefused {
149 Turning,
151 Inventory(ghast_inventory::ClickError),
153}
154
155impl std::fmt::Display for ClickRefused {
156 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
157 match self {
158 ClickRefused::Turning => f.write_str("the player is turning"),
159 ClickRefused::Inventory(e) => write!(f, "{e}"),
160 }
161 }
162}
163
164impl std::error::Error for ClickRefused {}
165
166#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
169pub struct ClickTicket {
170 pub window_id: i32,
172 pub action: i16,
174}
175
176impl ClickTicket {
177 #[must_use]
179 pub fn matches(&self, r: &ghast_inventory::ClickResult) -> bool {
180 r.window_id == self.window_id && r.action == self.action
181 }
182}
183
184#[derive(Debug, Clone, Copy, Default)]
186pub struct Idle;
187
188impl Behavior for Idle {
189 fn name(&self) -> &str {
190 "idle"
191 }
192
193 fn tick(&mut self, _ctx: &mut TickContext<'_>) {}
194}
195
196#[derive(Debug, Clone, PartialEq, Eq)]
198#[non_exhaustive]
199pub struct RawInbound {
200 pub id: i32,
202 pub body: bytes::Bytes,
204 pub at: Instant,
206}
207
208#[derive(Debug, Clone, Copy, PartialEq)]
210#[non_exhaustive]
211pub struct TeleportEvent {
212 pub teleport: crate::common::Teleport,
215 pub sent_before: Pose,
218 pub pose_before: Pose,
220 pub after: Pose,
222}
223
224const MAX_PENDING_TELEPORTS: usize = 64;
226
227const MAX_PENDING_RAW: usize = 4096;
229
230#[derive(Debug, Clone, Default, PartialEq)]
232#[non_exhaustive]
233pub struct TickEvents {
234 pub chat: Vec<ChatMessage>,
236 pub chat_dropped: usize,
238 pub teleported: bool,
240 pub teleports: Vec<TeleportEvent>,
242 pub joined: bool,
244 pub respawned: bool,
246 pub died: bool,
248 pub health_changed: bool,
250 pub game_mode_changed: bool,
252 pub inventory: ghast_inventory::Events,
254 pub block_updates: Vec<(ghast_protocol::BlockPos, u32)>,
258 pub block_updates_dropped: usize,
260 pub raw: Vec<RawInbound>,
263 pub raw_dropped: usize,
265}
266
267impl TickEvents {
268 fn clear(&mut self) {
269 self.chat.clear();
270 self.chat_dropped = 0;
271 self.teleported = false;
272 self.teleports.clear();
273 self.joined = false;
274 self.respawned = false;
275 self.died = false;
276 self.health_changed = false;
277 self.game_mode_changed = false;
278 self.inventory = ghast_inventory::Events::default();
279 self.block_updates.clear();
280 self.block_updates_dropped = 0;
281 self.raw.clear();
282 self.raw_dropped = 0;
283 }
284}
285
286#[derive(Debug, Clone)]
288pub(crate) struct CoreOptions {
289 pub name: Arc<str>,
290 pub version: GameVersion,
291 pub settings: ClientSettings,
292 pub brand: Option<String>,
293 pub sensitivity: f32,
294 pub silent_hold: Duration,
295 pub inventory: ghast_inventory::Version,
296 pub hold_turn_on_action: bool,
299}
300
301#[derive(Debug)]
303pub(crate) struct Core {
304 opts: CoreOptions,
305 tick: u64,
306 entity_id: i32,
307 joined: bool,
308 teleported_once: bool,
309 ready: bool,
310 pose: Pose,
311 sent: Pose,
312 rotation: Rotation,
313 aimed: bool,
314 controls: Controls,
315 health: Health,
316 dead: bool,
317 abilities: Abilities,
318 game_mode: GameMode,
319 held_slot: u8,
320 sent_slot: u8,
321 open_window: i32,
322 dimension: String,
323 respawn_screen: bool,
324 pending: TickEvents,
325 current: TickEvents,
326 out: Vec<Outbound>,
327 last_inbound: Option<Instant>,
328 silent: bool,
329 quit: Option<String>,
330 status: String,
331 hands: Hands,
332 buttons: Buttons,
333 inv: ghast_inventory::Inventory,
335 turned: bool,
337 effects: std::collections::BTreeMap<i8, i8>,
339 aqua_affinity: bool,
341 broken_now: Vec<ghast_protocol::BlockPos>,
343 want_flying: Option<bool>,
345 held_turns: u64,
347 swings: u64,
349 #[cfg(test)]
351 forced_look: Option<(f32, f32)>,
352}
353
354#[derive(Debug, Default)]
356struct BehaviorState {
357 started: bool,
358 failed: bool,
359 done: bool,
361}
362
363#[derive(Debug)]
365pub(crate) struct Engine<S> {
366 pub core: Core,
367 pub sim: S,
368 state: BehaviorState,
369}
370
371impl Core {
372 fn new(opts: CoreOptions) -> Self {
373 let rotation = Rotation::new(opts.sensitivity);
374 let inv = ghast_inventory::Inventory::new(opts.inventory);
375 Core {
376 opts,
377 tick: 0,
378 entity_id: 0,
379 joined: false,
380 teleported_once: false,
381 ready: false,
382 pose: Pose::default(),
383 sent: Pose::default(),
384 rotation,
385 aimed: false,
386 controls: Controls::default(),
387 health: Health::default(),
388 dead: false,
389 abilities: Abilities::default(),
390 game_mode: GameMode::default(),
391 held_slot: 0,
392 sent_slot: 0,
393 open_window: 0,
394 dimension: String::new(),
395 respawn_screen: true,
396 pending: TickEvents::default(),
397 current: TickEvents::default(),
398 out: Vec::with_capacity(16),
399 last_inbound: None,
400 silent: false,
401 quit: None,
402 status: String::new(),
403 hands: Hands::default(),
404 buttons: Buttons::default(),
405 inv,
406 turned: false,
407 effects: std::collections::BTreeMap::new(),
408 aqua_affinity: false,
409 broken_now: Vec::new(),
410 want_flying: None,
411 held_turns: 0,
412 swings: 0,
413 #[cfg(test)]
414 forced_look: None,
415 }
416 }
417
418 fn reset_player(&mut self, why: &str) {
421 self.teleported_once = false;
422 self.ready = false;
423 self.pose = Pose::default();
424 self.sent = Pose::default();
425 self.controls = Controls::default();
426 self.aimed = false;
427 self.open_window = 0;
428 self.dead = false;
429 self.hands.reset();
430 self.buttons = Buttons::default();
431 self.effects.clear();
432 log::debug!(
433 "[{}] t{} {why}: waiting for the server position",
434 self.opts.name,
435 self.tick
436 );
437 }
438
439 fn column_loaded(&self, world: &dyn WorldView) -> bool {
440 let cx = (self.pose.pos.x.floor() as i64 >> 4) as i32;
442 let cz = (self.pose.pos.z.floor() as i64 >> 4) as i32;
443 world.is_column_loaded(cx, cz)
444 }
445}
446
447fn held_item<W: Sim + ?Sized>(
449 inv: &ghast_inventory::Inventory,
450 sim: &W,
451) -> Option<hands::HeldItem> {
452 let s = inv.held()?;
453 let id = u16::try_from(s.id).ok()?;
454 let count = u8::try_from(s.count.clamp(0, 255)).unwrap_or(0);
455 Some(sim.held_item(id, count, efficiency(s)))
456}
457
458fn collidable(e: &ghast_entity::Entity) -> bool {
462 use ghast_entity::{EntityClass, EntityType};
463 match e.class() {
464 EntityClass::Player | EntityClass::Mob | EntityClass::Painting => true,
465 EntityClass::Item | EntityClass::ExperienceOrb => false,
466 EntityClass::Object => match e.entity_type() {
467 EntityType::LegacyObject(t) => matches!(t, 1 | 10 | 50 | 51 | 70 | 71 | 77 | 78),
468 EntityType::Registry(_) => {
469 ghast_entity::v1_16_5::info(e.entity_type()).is_some_and(|i| {
470 matches!(
471 i.name,
472 "boat"
473 | "tnt"
474 | "end_crystal"
475 | "falling_block"
476 | "item_frame"
477 | "leash_knot"
478 | "armor_stand"
479 | "shulker_bullet"
480 ) || i.name.contains("minecart")
481 })
482 }
483 _ => false,
484 },
485 }
486}
487
488fn entity_pick<W: WorldView + ?Sized>(
491 w: &W,
492 version: GameVersion,
493 eyes: Vec3,
494 look: Vec3,
495 block_distance: Option<f64>,
496) -> Option<hands::EntityHit> {
497 let ents = w.entities()?;
498 let riding = ents.me().and_then(ghast_entity::Entity::vehicle);
499 let border = if version == GameVersion::V1_8_9 {
501 0.1
502 } else {
503 0.0
504 };
505 let cands = ents
506 .within([eyes.x, eyes.y, eyes.z], 8.0)
507 .filter(|e| collidable(e))
508 .filter_map(|e| {
509 let p = e.position();
510 Some((e.id(), e.bounding_box()?, Vec3::new(p[0], p[1], p[2])))
511 });
512 hands::pick_entity(cands, eyes, look, block_distance, border, riding)
513}
514
515fn off_held_item<W: Sim + ?Sized>(
517 inv: &ghast_inventory::Inventory,
518 sim: &W,
519) -> Option<hands::HeldItem> {
520 let s = inv.offhand()?;
521 let id = u16::try_from(s.id).ok()?;
522 let count = u8::try_from(s.count.clamp(0, 255)).unwrap_or(0);
523 Some(sim.held_item(id, count, 0))
524}
525
526fn efficiency(s: &ghast_inventory::Stack) -> u8 {
528 enchant_level(s, 32, "minecraft:efficiency")
529}
530
531fn enchant_level(s: &ghast_inventory::Stack, id_1_8: i32, name: &str) -> u8 {
534 use ghast_protocol::nbt::Tag;
535 let Some(root) = s.nbt.as_ref().and_then(|n| n.parse().ok().flatten()) else {
536 return 0;
537 };
538 let Some(c) = root.tag.as_compound() else {
539 return 0;
540 };
541 let num = |t: Option<&Tag>| match t {
542 Some(Tag::Short(v)) => i32::from(*v),
543 Some(Tag::Int(v)) => *v,
544 Some(Tag::Byte(v)) => i32::from(*v),
545 _ => -1,
546 };
547 for key in ["ench", "Enchantments"] {
548 if let Some(Tag::List(l)) = c.get(key) {
549 for e in l.items.iter().filter_map(Tag::as_compound) {
550 let hit = match e.get("id") {
551 Some(Tag::String(n)) => n == name,
552 other => num(other) == id_1_8,
553 };
554 if hit {
555 return u8::try_from(num(e.get("lvl")).clamp(0, 255)).unwrap_or(0);
556 }
557 }
558 }
559 }
560 0
561}
562
563impl<S: Sim> Engine<S> {
564 pub(crate) fn new(opts: CoreOptions, sim: S) -> Self {
565 Engine {
566 core: Core::new(opts),
567 sim,
568 state: BehaviorState::default(),
569 }
570 }
571
572 pub(crate) fn note_inbound(&mut self, now: Instant) {
574 self.core.last_inbound = Some(now);
575 }
576
577 pub(crate) fn last_inbound(&self) -> Option<Instant> {
579 self.core.last_inbound
580 }
581
582 pub(crate) fn name(&self) -> &str {
584 &self.core.opts.name
585 }
586
587 pub(crate) fn push_raw(&mut self, p: RawInbound) {
589 let ev = &mut self.core.pending;
590 if ev.raw.len() < MAX_PENDING_RAW {
591 ev.raw.push(p);
592 } else {
593 ev.raw_dropped += 1;
594 }
595 }
596
597 pub(crate) fn raw_packets<B: Behavior>(&self, b: &B) -> std::borrow::Cow<'static, [i32]> {
599 if self.state.failed || self.state.done {
600 std::borrow::Cow::Borrowed(&[])
601 } else {
602 b.raw_packets(self.core.opts.version)
603 }
604 }
605
606 pub(crate) fn inventory_mut(&mut self) -> &mut ghast_inventory::Inventory {
608 &mut self.core.inv
609 }
610
611 pub(crate) fn outbound(&mut self) -> &mut Vec<Outbound> {
613 &mut self.core.out
614 }
615
616 pub(crate) fn quit_requested(&self) -> Option<&str> {
618 self.core.quit.as_deref()
619 }
620
621 pub(crate) fn core_name(&self) -> &str {
622 &self.core.opts.name
623 }
624
625 pub(crate) fn tick_count(&self) -> u64 {
626 self.core.tick
627 }
628
629 pub(crate) fn handle(&mut self, p: Inbound) {
632 let c = &mut self.core;
633 let sim = &mut self.sim;
634 match p {
635 Inbound::KeepAlive(id) => c.out.push(Outbound::KeepAlive(id)),
636 Inbound::JoinGame(j) => {
637 c.entity_id = j.entity_id;
638 c.game_mode = j.game_mode;
639 c.respawn_screen = j.respawn_screen;
640 c.dimension = j.dimension;
643 c.joined = true;
644 c.pending.joined = true;
645 c.health = Health::default();
646 c.abilities = Abilities::default();
647 c.reset_player("join game");
648 sim.reset_player();
649 sim.spectator(c.game_mode == GameMode::Spectator);
650 c.out.push(Outbound::Settings(c.opts.settings.clone()));
652 if let Some(brand) = &c.opts.brand {
653 c.out.push(Outbound::Brand(brand.clone()));
654 }
655 log::info!(
656 "[{}] joined: entity {} {:?} dimension {}",
657 c.opts.name,
658 c.entity_id,
659 c.game_mode,
660 c.dimension
661 );
662 }
663 Inbound::Respawn {
664 dimension,
665 game_mode,
666 } => {
667 if dimension != c.dimension {
668 c.dimension = dimension;
669 }
670 c.game_mode = game_mode;
671 c.pending.respawned = true;
672 c.reset_player("respawn");
673 sim.reset_player();
674 sim.spectator(c.game_mode == GameMode::Spectator);
675 }
676 Inbound::Teleport(t) => {
677 if c.teleported_once {
678 log::debug!(
679 "[{}] t{} teleport {:?} {:?} {:?} yaw {:?} pitch {:?} flags {}; we had sent \
680 {:?} {:?} {:?} yaw {:?} pitch {:?} ground {}",
681 c.opts.name,
682 c.tick,
683 t.x,
684 t.y,
685 t.z,
686 t.yaw,
687 t.pitch,
688 t.flags,
689 c.sent.pos.x,
690 c.sent.pos.y,
691 c.sent.pos.z,
692 c.sent.yaw,
693 c.sent.pitch,
694 c.sent.on_ground
695 );
696 }
697 let (sent_before, pose_before) = (c.sent, c.pose);
698 sim.teleport(&mut c.pose, &t);
699 if c.pending.teleports.len() < MAX_PENDING_TELEPORTS {
700 c.pending.teleports.push(TeleportEvent {
701 teleport: t,
702 sent_before,
703 pose_before,
704 after: c.pose,
705 });
706 }
707 if let Some(id) = t.teleport_id {
708 c.out.push(Outbound::TeleportConfirm(id));
709 }
710 c.out.push(Outbound::Move(MovePacket::PositionLook {
711 pos: c.pose.pos,
712 yaw: c.pose.yaw,
713 pitch: c.pose.pitch,
714 on_ground: false,
715 }));
716 c.sent = Pose {
718 on_ground: false,
719 ..c.pose
720 };
721 if !c.teleported_once {
722 log::debug!(
723 "[{}] t{} spawn position {:.3} {:.3} {:.3}",
724 c.opts.name,
725 c.tick,
726 c.pose.pos.x,
727 c.pose.pos.y,
728 c.pose.pos.z
729 );
730 }
731 c.teleported_once = true;
732 c.pending.teleported = true;
733 }
734 Inbound::Chat(m) => {
735 if c.pending.chat.len() < MAX_PENDING_CHAT {
736 c.pending.chat.push(m);
737 } else {
738 c.pending.chat_dropped += 1;
739 }
740 }
741 Inbound::Health(h) => {
742 let was_dead = c.dead;
743 c.health = h;
744 sim.food(h.food);
745 c.pending.health_changed = true;
746 if h.health <= 0.0 && !was_dead {
747 c.dead = true;
748 c.pending.died = true;
749 log::info!("[{}] t{} died", c.opts.name, c.tick);
750 }
751 }
752 Inbound::Died { entity_id } => {
753 if entity_id == c.entity_id && !c.respawn_screen {
754 c.out.push(Outbound::Respawn);
756 }
757 }
758 Inbound::Abilities(a) => {
759 c.abilities = a;
760 c.pose.flying = a.flying();
761 sim.abilities(a);
762 }
763 Inbound::HeldSlot(s) => {
764 if (0..9).contains(&s) {
765 if c.held_slot != s as u8 {
768 c.hands.stop_using();
769 }
770 c.held_slot = s as u8;
771 }
772 }
773 Inbound::GameState { reason, value } => match reason {
774 3 => {
775 c.game_mode = GameMode::from_wire(value as u8);
776 sim.spectator(c.game_mode == GameMode::Spectator);
777 c.pending.game_mode_changed = true;
778 }
779 11 if c.opts.version != GameVersion::V1_8_9 => c.respawn_screen = value == 0.0,
781 _ => {}
782 },
783 Inbound::Transaction { .. } => {}
785 Inbound::OpenWindow(id) => c.open_window = id,
786 Inbound::CloseWindow(_) => c.open_window = 0,
787 Inbound::Velocity { entity_id, motion } => {
788 if entity_id == c.entity_id {
789 sim.velocity(motion, false);
790 }
791 }
792 Inbound::ExplosionPush(motion) => sim.velocity(motion, true),
793 Inbound::MovementSpeed {
794 entity_id,
795 base,
796 modifiers,
797 } => {
798 if entity_id == c.entity_id {
799 log::debug!(
800 "[{}] t{} movement speed base {base:?} modifiers {modifiers:?}",
801 c.opts.name,
802 c.tick
803 );
804 let mods: Vec<(u128, f64, u8)> = modifiers
805 .iter()
806 .map(|&(id, amount, op)| (id.as_u128(), amount, op as u8))
807 .collect();
808 sim.movement_speed(base, &mods);
809 }
810 }
811 Inbound::Effect {
812 entity_id,
813 id,
814 amplifier,
815 } => {
816 if entity_id == c.entity_id {
817 c.effects.insert(id, amplifier);
818 sim.status_effects(&c.effects);
819 }
820 }
821 Inbound::EffectRemoved { entity_id, id } => {
822 if entity_id == c.entity_id {
823 c.effects.remove(&id);
824 sim.status_effects(&c.effects);
825 }
826 }
827 Inbound::EntityStatus { entity_id, status } => {
828 if entity_id == c.entity_id && status == 9 {
830 c.hands.stop_using();
831 }
832 }
833 Inbound::Disconnect(_) | Inbound::SetCompression(_) => {}
834 }
835 }
836
837 pub(crate) fn interests<B: Behavior>(&self, b: &B) -> Interests {
839 if self.state.failed {
840 Interests::NONE
841 } else {
842 b.interests()
843 }
844 }
845
846 pub(crate) fn stop<B: Behavior>(&mut self, b: &mut B, reason: &str) {
848 if self.state.started && !self.state.failed {
849 self.state.failed = true;
850 let name = self.core.opts.name.clone();
851 if catch_unwind(AssertUnwindSafe(|| b.stop(reason))).is_err() {
852 log::error!("[{name}] behaviour {} panicked in stop", b.name());
853 }
854 }
855 }
856
857 pub(crate) fn tick<B: Behavior>(&mut self, b: &mut B, now: Instant) {
859 let c = &mut self.core;
860 c.tick += 1;
861 c.inv.tick();
863 let was_ready = c.ready;
864 c.ready = c.teleported_once && c.column_loaded(&self.sim);
865 if c.ready != was_ready {
866 if c.ready {
867 log::info!(
868 "[{}] t{} in the world at {:.3} {:.3} {:.3}",
869 c.opts.name,
870 c.tick,
871 c.pose.pos.x,
872 c.pose.pos.y,
873 c.pose.pos.z
874 );
875 } else {
876 log::debug!(
877 "[{}] t{} player column not loaded: not ticking",
878 c.opts.name,
879 c.tick
880 );
881 }
882 }
883 if !c.ready {
884 return;
887 }
888
889 let silent_for = c.last_inbound.map(|t| now.saturating_duration_since(t));
890 let silent = silent_for.is_some_and(|d| d > c.opts.silent_hold);
891 if silent != c.silent {
892 c.silent = silent;
893 if silent {
894 log::warn!(
895 "[{}] t{} server silent for {} ms: behaviour on hold",
896 c.opts.name,
897 c.tick,
898 silent_for.unwrap_or_default().as_millis()
899 );
900 } else {
901 log::info!(
902 "[{}] t{} server answering again: behaviour resumes",
903 c.opts.name,
904 c.tick
905 );
906 }
907 }
908
909 if silent {
910 c.controls = Controls::default();
912 c.aimed = false;
913 } else if !self.state.failed && !self.state.done {
914 std::mem::swap(&mut c.pending, &mut c.current);
915 c.pending.clear();
916 c.current.inventory = c.inv.drain_events();
917 c.current.block_updates_dropped =
918 self.sim.drain_block_updates(&mut c.current.block_updates);
919 if c.current.inventory.player_changed {
920 let ench = |a: usize, id: i32, name: &str| {
922 c.inv.armor(a).map_or(0, |s| enchant_level(s, id, name))
923 };
924 let depth = ench(3, 8, "minecraft:depth_strider");
925 let soul = ench(3, -1, "minecraft:soul_speed");
926 c.aqua_affinity = ench(0, 6, "minecraft:aqua_affinity") > 0;
927 self.sim.boots(depth, soul);
928 }
929 let started = self.state.started;
930 self.state.started = true;
931 if !started {
932 c.rotation.reset(c.pose.yaw, c.pose.pitch);
933 }
934 let name = c.opts.name.clone();
935 let result = catch_unwind(AssertUnwindSafe(|| {
936 let held = held_item(&c.inv, &self.sim);
937 let mut ctx = TickContext {
938 core: c,
939 world: &self.sim,
940 held,
941 };
942 if !started {
943 b.start(&mut ctx);
944 }
945 b.tick(&mut ctx);
946 b.running()
947 }));
948 match result {
949 Ok(true) => {}
950 Ok(false) => {
951 self.state.done = true;
952 log::info!("[{name}] behaviour {} finished", b.name());
953 }
954 Err(_) => {
955 log::error!("[{name}] behaviour {} panicked: stopped", b.name());
956 self.stop(b, "panic");
957 }
958 }
959 }
960
961 let c = &mut self.core;
962 if c.held_slot != c.sent_slot {
965 c.sent_slot = c.held_slot;
966 c.out.push(Outbound::HeldSlot(i16::from(c.sent_slot)));
967 }
968 {
970 let gui = c.inv.gui_open();
973 let eye = if c.pose.sneaking {
974 c.opts.version.sneak_eye_height()
975 } else {
976 c.opts.version.eye_height()
977 };
978 let eyes = Vec3::new(c.pose.pos.x, c.pose.pos.y + eye, c.pose.pos.z);
979 let sim = &self.sim;
980 let blocks = |x: i32, y: i32, z: i32| sim.block_query(x, y, z);
981 let look = hands::look_vector(c.pose.yaw, c.pose.pitch);
982 let target = hands::pick(&blocks, eyes, look, hands::REACH);
983 let entity = entity_pick(sim, c.opts.version, eyes, look, target.map(|t| t.distance));
984 let in_water = blocks(
985 eyes.x.floor() as i32,
986 eyes.y.floor() as i32,
987 eyes.z.floor() as i32,
988 )
989 .is_some_and(|b| b.water);
990 let held = held_item(&c.inv, sim);
991 let mut placed = false;
992 let first = c.out.len();
993 let mut io = HandsIo {
994 blocks: &blocks,
995 target,
996 held,
997 held_stack: c.inv.held().cloned(),
998 food: c.health.food,
999 modern: c.opts.version != GameVersion::V1_8_9,
1000 off_held: off_held_item(&c.inv, sim),
1001 off_stack: c.inv.offhand().cloned(),
1002 entity,
1003 factors: DigFactors {
1004 on_ground: c.pose.on_ground,
1005 in_water,
1006 aqua_affinity: c.aqua_affinity,
1007 haste: c.effects.get(&3).map_or(0, |a| (*a as u8).wrapping_add(1)),
1008 fatigue: c.effects.get(&4).map_or(0, |a| (*a as u8).wrapping_add(1)),
1009 },
1010 sneaking: c.pose.sneaking,
1011 out: &mut c.out,
1012 broken: &mut c.broken_now,
1013 placed: &mut placed,
1014 };
1015 let (left, right) = (c.buttons.left && !gui, c.buttons.right && !gui);
1016 c.hands.tick(left, right, &mut io);
1017 let _ = placed;
1019 predict_planting(c, sim, first);
1020 }
1021 for p in c.broken_now.drain(..) {
1022 self.sim.predict_air(p.x, p.y, p.z);
1023 }
1024 if let Some(f) = c.want_flying.take() {
1026 let first = c.out.len();
1027 self.sim.set_flying(f, &mut c.out);
1028 if c.out.len() > first {
1029 c.pose.flying = f;
1030 } else if f && !c.pose.flying {
1031 log::debug!(
1032 "[{}] t{} set_flying(true) refused: flight not allowed",
1033 c.opts.name,
1034 c.tick
1035 );
1036 }
1037 }
1038 c.turned = false;
1040 #[cfg(test)]
1041 if let Some((yaw, pitch)) = c.forced_look.take() {
1042 c.turned = yaw != c.pose.yaw || pitch != c.pose.pitch;
1043 c.pose.yaw = yaw;
1044 c.pose.pitch = pitch;
1045 c.aimed = false;
1046 }
1047 if c.aimed {
1048 let (yaw, pitch) = c.rotation.step(c.pose.yaw, c.pose.pitch);
1049 if c.opts.hold_turn_on_action && !post_look_ok(c, yaw, pitch) {
1050 c.held_turns += 1;
1053 } else {
1054 c.turned = yaw != c.pose.yaw || pitch != c.pose.pitch;
1055 c.pose.yaw = yaw;
1056 c.pose.pitch = pitch;
1057 }
1058 c.aimed = false;
1059 }
1060 let first = c.out.len();
1061 let using = c.hands.using_item();
1062 self.sim
1063 .step(&mut c.pose, c.controls, using, c.entity_id, &mut c.out);
1064 for o in &c.out[first..] {
1065 let Outbound::Move(m) = o else { continue };
1066 match *m {
1067 MovePacket::Position { pos, .. } => c.sent.pos = pos,
1068 MovePacket::Look { yaw, pitch, .. } => {
1069 c.sent.yaw = yaw;
1070 c.sent.pitch = pitch;
1071 }
1072 MovePacket::PositionLook {
1073 pos, yaw, pitch, ..
1074 } => {
1075 c.sent.pos = pos;
1076 c.sent.yaw = yaw;
1077 c.sent.pitch = pitch;
1078 }
1079 MovePacket::Flying { .. } => {}
1080 }
1081 if matches!(
1082 m,
1083 MovePacket::Position { .. } | MovePacket::PositionLook { .. }
1084 ) {
1085 c.sent.motion = c.pose.motion;
1086 }
1087 }
1088 c.sent.on_ground = c.pose.on_ground;
1089 c.sent.sneaking = c.pose.sneaking;
1090 c.sent.sprinting = c.pose.sprinting;
1091 c.sent.flying = c.pose.flying;
1092 c.swings += count_swings(&c.out);
1093
1094 if self.state.started && !self.state.failed {
1095 let name = c.opts.name.clone();
1096 let view = TickView {
1097 core: &self.core,
1098 world: &self.sim,
1099 };
1100 if catch_unwind(AssertUnwindSafe(|| b.after_tick(&view))).is_err() {
1101 log::error!(
1102 "[{name}] behaviour {} panicked in after_tick: stopped",
1103 b.name()
1104 );
1105 self.stop(b, "panic");
1106 }
1107 }
1108 }
1109}
1110
1111fn count_swings(out: &[Outbound]) -> u64 {
1113 out.iter()
1114 .filter(|o| matches!(o, Outbound::Swing | Outbound::SwingOffHand))
1115 .count() as u64
1116}
1117
1118pub struct TickView<'a> {
1120 core: &'a Core,
1121 world: &'a dyn WorldView,
1122}
1123
1124impl std::fmt::Debug for TickView<'_> {
1125 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1126 f.debug_struct("TickView")
1127 .field("tick", &self.core.tick)
1128 .field("sent", &self.core.sent)
1129 .finish_non_exhaustive()
1130 }
1131}
1132
1133impl TickView<'_> {
1134 #[must_use]
1136 pub fn tick(&self) -> u64 {
1137 self.core.tick
1138 }
1139
1140 #[must_use]
1142 pub fn version(&self) -> GameVersion {
1143 self.core.opts.version
1144 }
1145
1146 #[must_use]
1148 pub fn pose(&self) -> &Pose {
1149 &self.core.pose
1150 }
1151
1152 #[must_use]
1154 pub fn sent(&self) -> &Pose {
1155 &self.core.sent
1156 }
1157
1158 #[must_use]
1160 pub fn world(&self) -> &dyn WorldView {
1161 self.world
1162 }
1163
1164 #[must_use]
1166 pub fn entities(&self) -> Option<&ghast_entity::Entities> {
1167 self.world.entities()
1168 }
1169
1170 #[must_use]
1172 pub fn inventory(&self) -> &ghast_inventory::Inventory {
1173 &self.core.inv
1174 }
1175
1176 #[must_use]
1178 pub fn events(&self) -> &TickEvents {
1179 &self.core.current
1180 }
1181
1182 #[must_use]
1184 pub fn entity_id(&self) -> i32 {
1185 self.core.entity_id
1186 }
1187
1188 #[must_use]
1190 pub fn health(&self) -> Health {
1191 self.core.health
1192 }
1193
1194 #[must_use]
1196 pub fn game_mode(&self) -> GameMode {
1197 self.core.game_mode
1198 }
1199
1200 #[must_use]
1202 pub fn held_slot(&self) -> u8 {
1203 self.core.held_slot
1204 }
1205
1206 #[must_use]
1208 pub fn mining(&self) -> Option<(ghast_protocol::BlockPos, f32)> {
1209 self.core.hands.mining()
1210 }
1211
1212 #[must_use]
1214 pub fn swings(&self) -> u64 {
1215 self.core.swings
1216 }
1217
1218 #[must_use]
1220 pub fn broken(&self) -> u64 {
1221 self.core.hands.broken
1222 }
1223
1224 #[must_use]
1227 pub fn dimension(&self) -> &str {
1228 &self.core.dimension
1229 }
1230
1231 #[must_use]
1234 pub fn using_item(&self) -> bool {
1235 self.core.hands.using_item()
1236 }
1237}
1238
1239#[cfg(test)]
1240impl Core {
1241 pub(crate) fn test_set_slot(&mut self, slot: u8) {
1243 self.held_slot = slot;
1244 self.sent_slot = slot;
1245 self.inv.select(slot);
1246 }
1247
1248 pub(crate) fn test_poses(&mut self) -> (&mut Pose, &mut Pose) {
1250 (&mut self.pose, &mut self.sent)
1251 }
1252
1253 pub(crate) fn test_held_turns(&self) -> u64 {
1255 self.held_turns
1256 }
1257}
1258
1259#[cfg(test)]
1260impl TickContext<'_> {
1261 pub(crate) fn test_force_look(&mut self, yaw: f32, pitch: f32) {
1263 self.core.forced_look = Some((yaw, pitch));
1264 }
1265
1266 pub(crate) fn test_rotation(&self) -> Rotation {
1268 self.core.rotation
1269 }
1270}
1271
1272fn predict_planting(c: &mut Core, world: &dyn WorldView, first: usize) {
1278 if c.opts.version != GameVersion::V1_8_9 || c.game_mode == GameMode::Creative {
1279 return;
1280 }
1281 let mut uses = 0u8;
1282 for o in &c.out[first..] {
1283 let Outbound::Place {
1284 pos,
1285 face: crate::common::Face::Up,
1286 held: Some(held),
1287 off_hand: false,
1288 ..
1289 } = o
1290 else {
1291 continue;
1292 };
1293 let soil = match held.id {
1296 295 | 361 | 362 | 391 | 392 => 60,
1297 372 => 88,
1298 _ => continue,
1299 };
1300 let id = |y: i32| world.raw_block(pos.x, y, pos.z).map(|s| s >> 4);
1301 if held.count > i32::from(uses) && id(pos.y) == Some(soil) && id(pos.y + 1) == Some(0) {
1302 uses += 1;
1303 }
1304 }
1305 if uses > 0 {
1306 c.inv.consume_held(uses);
1307 }
1308}
1309
1310fn post_look_ok(c: &Core, yaw: f32, pitch: f32) -> bool {
1314 let looks = [
1315 hands::look_vector(c.pose.yaw, pitch),
1316 hands::look_vector(yaw, pitch),
1317 ];
1318 let heights = [
1319 c.opts.version.eye_height(),
1320 c.opts.version.sneak_eye_height(),
1321 ];
1322 c.out
1323 .iter()
1324 .filter_map(|o| match o {
1325 Outbound::Dig {
1326 status: crate::common::DigStatus::Start | crate::common::DigStatus::Finish,
1327 pos,
1328 ..
1329 }
1330 | Outbound::Place { pos, .. } => Some(*pos),
1331 _ => None,
1332 })
1333 .all(|t| {
1334 [c.sent.pos, c.pose.pos].iter().all(|o| {
1335 heights.iter().any(|h| {
1336 let eyes = Vec3::new(o.x, o.y + *h, o.z);
1337 looks
1338 .iter()
1339 .any(|l| hands::ray_hits_cube(eyes, *l, t, ACTION_LOOK_REACH))
1340 })
1341 })
1342 })
1343}
1344
1345const ACTION_LOOK_REACH: f64 = 6.0;
1347
1348pub struct TickContext<'a> {
1353 core: &'a mut Core,
1354 world: &'a dyn WorldView,
1355 held: Option<hands::HeldItem>,
1356}
1357
1358impl std::fmt::Debug for TickContext<'_> {
1359 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1360 f.debug_struct("TickContext")
1361 .field("tick", &self.core.tick)
1362 .field("pose", &self.core.pose)
1363 .finish_non_exhaustive()
1364 }
1365}
1366
1367impl TickContext<'_> {
1368 #[must_use]
1370 pub fn tick(&self) -> u64 {
1371 self.core.tick
1372 }
1373
1374 #[must_use]
1376 pub fn version(&self) -> GameVersion {
1377 self.core.opts.version
1378 }
1379
1380 #[must_use]
1382 pub fn pose(&self) -> &Pose {
1383 &self.core.pose
1384 }
1385
1386 #[must_use]
1388 pub fn sent(&self) -> &Pose {
1389 &self.core.sent
1390 }
1391
1392 #[must_use]
1394 pub fn eyes(&self, pose: &Pose) -> Vec3 {
1395 let v = self.core.opts.version;
1396 pose.eyes(if pose.sneaking {
1397 v.sneak_eye_height()
1398 } else {
1399 v.eye_height()
1400 })
1401 }
1402
1403 #[must_use]
1405 pub fn world(&self) -> &dyn WorldView {
1406 self.world
1407 }
1408
1409 #[must_use]
1411 pub fn events(&self) -> &TickEvents {
1412 &self.core.current
1413 }
1414
1415 #[must_use]
1417 pub fn entity_id(&self) -> i32 {
1418 self.core.entity_id
1419 }
1420
1421 #[must_use]
1425 pub fn swings(&self) -> u64 {
1426 self.core.swings + count_swings(&self.core.out)
1427 }
1428
1429 #[must_use]
1431 pub fn health(&self) -> Health {
1432 self.core.health
1433 }
1434
1435 #[must_use]
1437 pub fn is_dead(&self) -> bool {
1438 self.core.dead
1439 }
1440
1441 #[must_use]
1443 pub fn game_mode(&self) -> GameMode {
1444 self.core.game_mode
1445 }
1446
1447 #[must_use]
1449 pub fn abilities(&self) -> Abilities {
1450 self.core.abilities
1451 }
1452
1453 #[must_use]
1455 pub fn can_fly(&self) -> bool {
1456 self.core.abilities.may_fly()
1457 }
1458
1459 #[must_use]
1461 pub fn held_slot(&self) -> u8 {
1462 self.core.held_slot
1463 }
1464
1465 #[must_use]
1467 pub fn open_window(&self) -> Option<i32> {
1468 (self.core.open_window != 0).then_some(self.core.open_window)
1469 }
1470
1471 #[must_use]
1473 pub fn dimension(&self) -> &str {
1474 &self.core.dimension
1475 }
1476
1477 #[must_use]
1479 pub fn controls(&self) -> Controls {
1480 self.core.controls
1481 }
1482
1483 #[must_use]
1485 pub fn aim(&self) -> (f32, f32) {
1486 self.core.rotation.target()
1487 }
1488
1489 pub fn chat(&mut self, text: impl Into<String>) {
1494 self.core.out.push(Outbound::Chat(text.into()));
1495 }
1496
1497 pub fn respawn(&mut self) -> bool {
1499 if self.core.dead {
1500 self.core.out.push(Outbound::Respawn);
1501 true
1502 } else {
1503 false
1504 }
1505 }
1506
1507 pub fn select_slot(&mut self, slot: u8) {
1510 if slot < 9 {
1511 if self.core.held_slot != slot {
1512 self.core.hands.stop_using();
1513 self.core.hands.ticks_since_swing = 0;
1514 }
1515 self.core.held_slot = slot;
1516 self.core.inv.select(slot);
1517 if self.core.sent_slot != slot {
1518 self.core.sent_slot = slot;
1519 self.core.out.push(Outbound::HeldSlot(i16::from(slot)));
1520 }
1521 }
1522 }
1523
1524 pub fn swing(&mut self) {
1526 self.core.out.push(Outbound::Swing);
1527 }
1528
1529 pub fn disconnect(&mut self, reason: impl Into<String>) {
1531 self.core.quit = Some(reason.into());
1532 }
1533
1534 pub fn set_aim(&mut self, yaw: f32, pitch: f32) {
1539 self.core.rotation.set_target(yaw, pitch);
1540 self.core.aimed = true;
1541 }
1542
1543 pub fn set_controls(&mut self, controls: Controls) {
1546 self.core.controls = controls;
1547 }
1548
1549 #[must_use]
1551 pub fn inventory(&self) -> &ghast_inventory::Inventory {
1552 &self.core.inv
1553 }
1554
1555 pub fn click(&mut self, click: ghast_inventory::Click) -> Result<ClickTicket, ClickRefused> {
1561 if self.core.turned || self.core.aimed {
1562 return Err(ClickRefused::Turning);
1563 }
1564 let o = self
1565 .core
1566 .inv
1567 .click(click)
1568 .map_err(ClickRefused::Inventory)?;
1569 let ticket = match &o {
1570 ghast_inventory::Outgoing::Click(p) => ClickTicket {
1571 window_id: i32::from(p.window_id),
1572 action: p.action,
1573 },
1574 _ => ClickTicket {
1575 window_id: -1,
1576 action: -1,
1577 },
1578 };
1579 self.core.out.push(Outbound::Inventory(o));
1580 Ok(ticket)
1581 }
1582
1583 pub fn open_inventory(&mut self) -> Result<(), ClickRefused> {
1586 if let Some(o) = self
1587 .core
1588 .inv
1589 .open_inventory()
1590 .map_err(ClickRefused::Inventory)?
1591 {
1592 self.core.out.push(Outbound::Inventory(o));
1593 }
1594 Ok(())
1595 }
1596
1597 pub fn close_window(&mut self) -> Result<(), ClickRefused> {
1599 let o = self.core.inv.close().map_err(ClickRefused::Inventory)?;
1600 self.core.out.push(Outbound::Inventory(o));
1601 self.core.open_window = 0;
1603 Ok(())
1604 }
1605
1606 pub fn set_buttons(&mut self, buttons: Buttons) {
1610 self.core.buttons = buttons;
1611 }
1612
1613 #[must_use]
1615 pub fn buttons(&self) -> Buttons {
1616 self.core.buttons
1617 }
1618
1619 #[must_use]
1622 pub fn target(&self) -> Option<BlockHit> {
1623 let s = &self.core.sent;
1624 let v = self.core.opts.version;
1625 let eye = if s.sneaking {
1626 v.sneak_eye_height()
1627 } else {
1628 v.eye_height()
1629 };
1630 let world = self.world;
1631 let blocks = |x: i32, y: i32, z: i32| world.block_query(x, y, z);
1632 hands::pick(
1633 &blocks,
1634 Vec3::new(s.pos.x, s.pos.y + eye, s.pos.z),
1635 hands::look_vector(s.yaw, s.pitch),
1636 hands::REACH,
1637 )
1638 }
1639
1640 #[must_use]
1642 pub fn entities(&self) -> Option<&ghast_entity::Entities> {
1643 self.world.entities()
1644 }
1645
1646 #[must_use]
1649 pub fn target_entity(&self) -> Option<hands::EntityHit> {
1650 let s = &self.core.sent;
1651 let v = self.core.opts.version;
1652 let eye = if s.sneaking {
1653 v.sneak_eye_height()
1654 } else {
1655 v.eye_height()
1656 };
1657 let eyes = Vec3::new(s.pos.x, s.pos.y + eye, s.pos.z);
1658 let look = hands::look_vector(s.yaw, s.pitch);
1659 let block = self.target().map(|t| t.distance);
1660 entity_pick(self.world, v, eyes, look, block)
1661 }
1662
1663 pub fn attack(&mut self, entity: i32) -> bool {
1667 if self.target_entity().is_none_or(|e| e.id != entity) {
1668 return false;
1669 }
1670 let world = self.world;
1671 let blocks = |x: i32, y: i32, z: i32| world.block_query(x, y, z);
1672 let c = &mut *self.core;
1673 let modern = c.opts.version != GameVersion::V1_8_9;
1674 let mut placed = false;
1675 let mut io = HandsIo {
1676 blocks: &blocks,
1677 target: None,
1678 held: self.held,
1679 held_stack: None,
1680 food: c.health.food,
1681 modern,
1682 off_held: None,
1683 off_stack: None,
1684 entity: None,
1685 factors: DigFactors::default(),
1686 sneaking: c.pose.sneaking,
1687 out: &mut c.out,
1688 broken: &mut c.broken_now,
1689 placed: &mut placed,
1690 };
1691 if !modern {
1692 io.out.push(Outbound::Swing);
1693 }
1694 c.hands.attack(entity, &mut io);
1695 if modern {
1696 c.out.push(Outbound::Swing);
1697 }
1698 true
1699 }
1700
1701 #[must_use]
1704 pub fn attack_strength(&self, attack_speed: f64) -> f32 {
1705 if self.core.opts.version == GameVersion::V1_8_9 {
1706 1.0
1707 } else {
1708 self.core.hands.attack_strength(attack_speed)
1709 }
1710 }
1711
1712 #[must_use]
1714 pub fn mining(&self) -> Option<(ghast_protocol::BlockPos, f32)> {
1715 self.core.hands.mining()
1716 }
1717
1718 #[must_use]
1720 pub fn broken(&self) -> u64 {
1721 self.core.hands.broken
1722 }
1723
1724 #[must_use]
1726 pub fn using_item(&self) -> bool {
1727 self.core.hands.using_item()
1728 }
1729
1730 #[must_use]
1741 pub fn item_facts(&self, stack: &ghast_inventory::Stack) -> Option<hands::HeldItem> {
1742 let id = u16::try_from(stack.id).ok()?;
1743 let count = u8::try_from(stack.count.clamp(0, 255)).unwrap_or(0);
1744 Some(crate::sim::held_item_facts(
1745 self.core.opts.version,
1746 id,
1747 count,
1748 efficiency(stack),
1749 ))
1750 }
1751
1752 #[must_use]
1769 pub fn dig_speed(
1770 &self,
1771 pos: ghast_protocol::BlockPos,
1772 stack: Option<&ghast_inventory::Stack>,
1773 ) -> Option<f32> {
1774 let b = self.world.block_query(pos.x, pos.y, pos.z)?;
1775 if b.air {
1776 return Some(0.0);
1777 }
1778 let c = &*self.core;
1779 let eyes = self.eyes(&c.pose);
1780 let in_water = self
1781 .world
1782 .block_query(
1783 eyes.x.floor() as i32,
1784 eyes.y.floor() as i32,
1785 eyes.z.floor() as i32,
1786 )
1787 .is_some_and(|b| b.water);
1788 let held = stack.and_then(|s| self.item_facts(s));
1789 let factors = DigFactors {
1790 on_ground: c.pose.on_ground,
1791 in_water,
1792 aqua_affinity: c.aqua_affinity,
1793 haste: c.effects.get(&3).map_or(0, |a| (*a as u8).wrapping_add(1)),
1794 fatigue: c.effects.get(&4).map_or(0, |a| (*a as u8).wrapping_add(1)),
1795 };
1796 Some(hands::relative_hardness(&b, held.as_ref(), factors))
1797 }
1798
1799 pub fn dig(&mut self, pos: ghast_protocol::BlockPos, face: crate::common::Face) -> bool {
1803 if !self
1804 .target()
1805 .is_some_and(|t| t.pos == pos && t.face == face)
1806 {
1807 return false;
1808 }
1809 let world = self.world;
1810 let blocks = |x: i32, y: i32, z: i32| world.block_query(x, y, z);
1811 let c = &mut *self.core;
1812 let mut placed = false;
1813 let mut io = HandsIo {
1814 blocks: &blocks,
1815 target: None,
1816 held: self.held,
1817 held_stack: c.inv.held().cloned(),
1818 food: c.health.food,
1819 modern: c.opts.version != GameVersion::V1_8_9,
1820 off_held: None,
1821 off_stack: None,
1822 entity: None,
1823 factors: DigFactors {
1824 on_ground: c.pose.on_ground,
1825 in_water: false,
1826 aqua_affinity: c.aqua_affinity,
1827 haste: c.effects.get(&3).map_or(0, |a| (*a as u8).wrapping_add(1)),
1828 fatigue: c.effects.get(&4).map_or(0, |a| (*a as u8).wrapping_add(1)),
1829 },
1830 sneaking: c.pose.sneaking,
1831 out: &mut c.out,
1832 broken: &mut c.broken_now,
1833 placed: &mut placed,
1834 };
1835 c.hands.click_block(pos, face, &mut io);
1836 c.out.push(Outbound::Swing);
1837 true
1838 }
1839
1840 pub fn place(
1844 &mut self,
1845 support: ghast_protocol::BlockPos,
1846 face: crate::common::Face,
1847 hit: Vec3,
1848 ) -> bool {
1849 let Some(t) = self.target().filter(|t| t.pos == support && t.face == face) else {
1850 return false;
1851 };
1852 let world = self.world;
1853 let Some(b) = world.block_query(support.x, support.y, support.z) else {
1854 return false;
1855 };
1856 let blocks = |x: i32, y: i32, z: i32| world.block_query(x, y, z);
1857 let c = &mut *self.core;
1858 let mut placed = false;
1859 let first = c.out.len();
1860 let mut io = HandsIo {
1861 blocks: &blocks,
1862 target: None,
1863 held: self.held,
1864 held_stack: c.inv.held().cloned(),
1865 food: c.health.food,
1866 modern: c.opts.version != GameVersion::V1_8_9,
1867 off_held: None,
1868 off_stack: None,
1869 entity: None,
1870 factors: DigFactors::default(),
1871 sneaking: c.pose.sneaking,
1872 out: &mut c.out,
1873 broken: &mut c.broken_now,
1874 placed: &mut placed,
1875 };
1876 c.hands
1877 .right_click_block(BlockHit { hit, ..t }, &b, &mut io);
1878 let _ = placed;
1879 predict_planting(c, world, first);
1880 c.out.push(Outbound::Swing);
1881 true
1882 }
1883
1884 #[must_use]
1886 pub fn last_inbound(&self) -> Option<Instant> {
1887 self.core.last_inbound
1888 }
1889
1890 #[must_use]
1896 pub fn planned_look(&self) -> (f32, f32) {
1897 let c = &*self.core;
1898 let now = (c.pose.yaw, c.pose.pitch);
1899 #[cfg(test)]
1900 if let Some(l) = c.forced_look {
1901 return l;
1902 }
1903 if !c.aimed {
1904 return now;
1905 }
1906 let mut r = c.rotation;
1907 let (yaw, pitch) = r.step(c.pose.yaw, c.pose.pitch);
1908 if c.opts.hold_turn_on_action && !post_look_ok(c, yaw, pitch) {
1909 now
1910 } else {
1911 (yaw, pitch)
1912 }
1913 }
1914
1915 pub fn set_flying(&mut self, flying: bool) {
1919 self.core.want_flying = Some(flying);
1920 }
1921
1922 #[must_use]
1925 pub fn held_turns(&self) -> u64 {
1926 self.core.held_turns
1927 }
1928
1929 pub fn status(&mut self, text: &str) {
1931 if self.core.status != text {
1932 text.clone_into(&mut self.core.status);
1933 log::info!(
1934 "[{}] t{} status: {text}",
1935 self.core.opts.name,
1936 self.core.tick
1937 );
1938 }
1939 }
1940}
1941
1942#[cfg(test)]
1943mod tests {
1944 use super::*;
1945 use crate::common::Teleport;
1946 use crate::common::{ChatPosition, EntityAction, JoinGame};
1947 use ghast_protocol::RawPacket;
1948
1949 fn opts(version: GameVersion) -> CoreOptions {
1950 CoreOptions {
1951 name: "test".into(),
1952 version,
1953 settings: ClientSettings::default(),
1954 brand: Some("vanilla".into()),
1955 sensitivity: 0.5,
1956 silent_hold: Duration::from_millis(1500),
1957 hold_turn_on_action: true,
1958 inventory: match version {
1959 GameVersion::V1_8_9 => ghast_inventory::Version::V1_8,
1960 _ => ghast_inventory::Version::V1_16_5,
1961 },
1962 }
1963 }
1964
1965 fn engine(version: GameVersion) -> Engine<Box<dyn Sim>> {
1967 let sim: Box<dyn Sim> = match version {
1968 GameVersion::V1_8_9 => Box::new(crate::sim::Sim18::new()),
1969 _ => Box::new(crate::sim::Sim16::new()),
1970 };
1971 Engine::new(opts(version), sim)
1972 }
1973
1974 fn load(e: &mut Engine<Box<dyn Sim>>, x: i32, z: i32) {
1976 let raw = match e.core.opts.version {
1977 GameVersion::V1_8_9 => crate::sim::test_chunks::v1_8(x, z),
1978 _ => crate::sim::test_chunks::v1_16_5(x, z),
1979 };
1980 e.sim.world_packet(&raw).unwrap();
1981 }
1982
1983 fn join(e: &mut Engine<Box<dyn Sim>>) {
1984 e.handle(Inbound::JoinGame(JoinGame {
1985 entity_id: 7,
1986 game_mode: GameMode::Survival,
1987 dimension: "0".into(),
1988 hardcore: false,
1989 dimension_nbt: None,
1990 respawn_screen: true,
1991 }));
1992 load(e, 0, 0);
1993 e.handle(Inbound::Teleport(Teleport {
1994 x: 8.5,
1995 y: 64.0,
1996 z: 8.5,
1997 yaw: 90.0,
1998 pitch: 0.0,
1999 flags: 0,
2000 teleport_id: None,
2001 }));
2002 e.outbound().clear();
2003 }
2004
2005 fn names(out: &mut Vec<Outbound>) -> Vec<&'static str> {
2007 let v = out
2008 .iter()
2009 .map(|o| match o {
2010 Outbound::Move(m) => m.trace_id(),
2011 Outbound::Chat(_) => "chat",
2012 Outbound::HeldSlot(_) => "held_slot",
2013 Outbound::Swing => "swing",
2014 Outbound::EntityAction { .. } => "entity_action",
2015 Outbound::KeepAlive(_) => "keep_alive",
2016 Outbound::Transaction { .. } => "transaction",
2017 Outbound::TeleportConfirm(_) => "teleport_confirm",
2018 Outbound::Respawn => "respawn",
2019 Outbound::Settings(_) => "settings",
2020 Outbound::Brand(_) => "brand",
2021 Outbound::Abilities { .. } => "abilities",
2022 Outbound::Dig { .. } => "block_dig",
2023 Outbound::Place { .. } | Outbound::UseItem { .. } => "block_place",
2024 Outbound::SwingOffHand => "swing_off",
2025 Outbound::UseEntity { .. } => "use_entity",
2026 Outbound::Inventory(_) => "inventory",
2027 })
2028 .collect();
2029 out.clear();
2030 v
2031 }
2032
2033 struct Script<F: FnMut(&mut TickContext<'_>)>(F);
2034 impl<F: FnMut(&mut TickContext<'_>)> Behavior for Script<F> {
2035 fn tick(&mut self, ctx: &mut TickContext<'_>) {
2036 (self.0)(ctx);
2037 }
2038 }
2039
2040 #[test]
2041 fn join_sends_settings_then_brand() {
2042 let mut e = engine(GameVersion::V1_8_9);
2043 e.handle(Inbound::JoinGame(JoinGame {
2044 entity_id: 1,
2045 game_mode: GameMode::Creative,
2046 dimension: "0".into(),
2047 hardcore: false,
2048 dimension_nbt: None,
2049 respawn_screen: true,
2050 }));
2051 assert_eq!(names(e.outbound()), ["settings", "brand"]);
2052 }
2053
2054 #[test]
2055 fn nothing_before_the_first_teleport() {
2056 let mut e = engine(GameVersion::V1_8_9);
2057 let mut b = Idle;
2058 let t0 = Instant::now();
2059 load(&mut e, 0, 0);
2060 e.outbound().clear();
2061 for i in 0..5 {
2062 e.tick(&mut b, t0 + Duration::from_millis(50 * i));
2063 }
2064 assert!(e.outbound().is_empty());
2065 }
2066
2067 #[test]
2068 fn teleport_reply_per_version() {
2069 let mut e = engine(GameVersion::V1_16_5);
2070 e.handle(Inbound::Teleport(Teleport {
2071 x: 1.0,
2072 y: 2.0,
2073 z: 3.0,
2074 yaw: 450.0,
2075 pitch: 100.0,
2076 flags: 0,
2077 teleport_id: Some(5),
2078 }));
2079 let out = std::mem::take(e.outbound());
2080 assert_eq!(out[0], Outbound::TeleportConfirm(5));
2081 match out[1] {
2082 Outbound::Move(MovePacket::PositionLook {
2083 yaw,
2084 pitch,
2085 on_ground,
2086 ..
2087 }) => {
2088 assert_eq!((yaw, pitch, on_ground), (90.0, 90.0, false));
2089 }
2090 ref o => panic!("unexpected {o:?}"),
2091 }
2092 let mut e = engine(GameVersion::V1_8_9);
2093 e.handle(Inbound::Teleport(Teleport {
2094 x: 1.0,
2095 y: 2.0,
2096 z: 3.0,
2097 yaw: 450.0,
2098 pitch: 100.0,
2099 flags: 0,
2100 teleport_id: None,
2101 }));
2102 let out = std::mem::take(e.outbound());
2103 assert_eq!(out.len(), 1);
2104 assert!(matches!(
2106 out[0],
2107 Outbound::Move(MovePacket::PositionLook {
2108 yaw: 90.0,
2109 pitch: 100.0,
2110 ..
2111 })
2112 ));
2113 }
2114
2115 #[test]
2116 fn actions_precede_the_single_movement_packet() {
2117 let mut e = engine(GameVersion::V1_8_9);
2118 join(&mut e);
2119 let t0 = Instant::now();
2120 e.note_inbound(t0);
2121 let mut b = Script(|ctx: &mut TickContext<'_>| {
2122 if ctx.tick() == 2 {
2123 ctx.select_slot(3);
2124 ctx.chat("hello");
2125 ctx.swing();
2126 ctx.set_controls(Controls {
2127 sneak: true,
2128 ..Controls::default()
2129 });
2130 }
2131 });
2132 e.tick(&mut b, t0);
2133 assert_eq!(names(e.outbound()), ["C06"]);
2134 e.tick(&mut b, t0);
2135 assert_eq!(
2136 names(e.outbound()),
2137 ["held_slot", "chat", "swing", "entity_action", "C03"]
2138 );
2139 for _ in 0..100 {
2140 e.tick(&mut b, t0);
2141 let n = names(e.outbound());
2142 assert_eq!(n.len(), 1, "one movement packet per tick: {n:?}");
2143 }
2144 }
2145
2146 #[test]
2147 fn standing_bot_packets() {
2148 let mut e = engine(GameVersion::V1_8_9);
2149 join(&mut e);
2150 let mut b = Idle;
2151 let t0 = Instant::now();
2152 e.note_inbound(t0);
2153 e.tick(&mut b, t0);
2155 let out = std::mem::take(e.outbound());
2156 assert!(matches!(
2157 out[..],
2158 [Outbound::Move(MovePacket::PositionLook {
2159 on_ground: false,
2160 ..
2161 })]
2162 ));
2163 let mut kinds = Vec::new();
2165 for _ in 0..42 {
2166 e.tick(&mut b, t0);
2167 kinds.extend(names(e.outbound()));
2168 }
2169 assert_eq!(kinds.iter().filter(|k| **k == "C04").count(), 2);
2170 assert_eq!(kinds.iter().filter(|k| **k == "C03").count(), 40);
2171 assert_eq!((kinds[20], kinds[41]), ("C04", "C04"));
2172 assert!(e.core.sent.on_ground);
2173 }
2174
2175 #[test]
2176 fn silent_server_freezes_behaviour() {
2177 let mut e = engine(GameVersion::V1_8_9);
2178 join(&mut e);
2179 let t0 = Instant::now();
2180 e.note_inbound(t0);
2181 let mut ticks = 0;
2182 let mut b = Script(|ctx: &mut TickContext<'_>| {
2183 ticks += 1;
2184 ctx.set_controls(Controls {
2185 sneak: true,
2186 ..Controls::default()
2187 });
2188 });
2189 e.tick(&mut b, t0 + Duration::from_millis(100));
2190 e.tick(&mut b, t0 + Duration::from_millis(1600));
2191 let n = names(e.outbound());
2193 assert_eq!(n, ["entity_action", "C06", "entity_action", "C03"]);
2194 e.note_inbound(t0 + Duration::from_millis(1700));
2195 e.tick(&mut b, t0 + Duration::from_millis(1700));
2196 let _ = b;
2197 assert_eq!(ticks, 2);
2198 }
2199
2200 #[test]
2201 fn keepalive_answered_transactions_left_to_the_inventory() {
2202 let mut e = engine(GameVersion::V1_8_9);
2203 join(&mut e);
2204 e.handle(Inbound::KeepAlive(42));
2205 e.handle(Inbound::Transaction {
2208 window_id: 0,
2209 action: -5,
2210 accepted: false,
2211 });
2212 assert_eq!(std::mem::take(e.outbound()), [Outbound::KeepAlive(42)]);
2213 }
2214
2215 #[test]
2216 fn panicking_behaviour_does_not_stop_the_tick() {
2217 let mut e = engine(GameVersion::V1_8_9);
2218 join(&mut e);
2219 let t0 = Instant::now();
2220 let mut b = Script(|ctx: &mut TickContext<'_>| {
2221 if ctx.tick() == 2 {
2222 panic!("boom");
2223 }
2224 });
2225 let hook = std::panic::take_hook();
2226 std::panic::set_hook(Box::new(|_| {}));
2227 for _ in 0..5 {
2228 e.tick(&mut b, t0);
2229 }
2230 std::panic::set_hook(hook);
2231 assert!(e.state.failed);
2232 assert_eq!(names(e.outbound()).len(), 5);
2233 }
2234
2235 #[test]
2236 fn chat_events_reach_the_behaviour_once() {
2237 let mut e = engine(GameVersion::V1_16_5);
2238 join(&mut e);
2239 e.handle(Inbound::Chat(ChatMessage {
2240 json: r#"{"text":"hi"}"#.into(),
2241 position: ChatPosition::System,
2242 sender: None,
2243 }));
2244 let mut seen = Vec::new();
2245 let mut b = Script(|ctx: &mut TickContext<'_>| {
2246 seen.extend(ctx.events().chat.iter().map(ChatMessage::plain));
2247 });
2248 let t0 = Instant::now();
2249 e.tick(&mut b, t0);
2250 e.tick(&mut b, t0);
2251 let _ = b;
2252 assert_eq!(seen, ["hi"]);
2253 }
2254
2255 #[test]
2256 fn immediate_respawn_1_16() {
2257 let mut e = engine(GameVersion::V1_16_5);
2258 join(&mut e);
2259 e.handle(Inbound::GameState {
2260 reason: 11,
2261 value: 1.0,
2262 });
2263 e.handle(Inbound::Died { entity_id: 7 });
2264 assert_eq!(e.outbound().as_slice(), [Outbound::Respawn]);
2265 }
2266
2267 #[test]
2270 fn dig_with_the_left_button() {
2271 let mut e = engine(GameVersion::V1_8_9);
2272 join(&mut e);
2273 let t0 = Instant::now();
2274 e.note_inbound(t0);
2275 let mut b = Script(|ctx: &mut TickContext<'_>| {
2276 ctx.set_aim(90.0, 89.5);
2277 let down = ctx.target().is_some_and(|t| {
2278 t.pos == ghast_protocol::BlockPos::new(8, 63, 8) && t.face == crate::Face::Up
2279 });
2280 ctx.set_buttons(Buttons {
2281 left: down,
2282 right: false,
2283 });
2284 });
2285 let mut ticks = Vec::new();
2286 for _ in 0..200 {
2287 e.tick(&mut b, t0);
2288 let n = names(e.outbound());
2289 assert_eq!(n.iter().filter(|x| x.starts_with('C')).count(), 1, "{n:?}");
2290 assert!(n.last().unwrap().starts_with('C'), "movement last: {n:?}");
2291 ticks.push(n);
2292 }
2293 let flat: Vec<&str> = ticks.iter().flatten().copied().collect();
2294 assert_eq!(
2295 flat.iter().filter(|x| **x == "block_dig").count(),
2296 2,
2297 "start + finish"
2298 );
2299 assert!(e.core.hands.broken == 1);
2300 let start = ticks.iter().position(|t| t.contains(&"block_dig")).unwrap();
2302 let finish = ticks
2303 .iter()
2304 .rposition(|t| t.contains(&"block_dig"))
2305 .unwrap();
2306 assert_eq!(finish - start, 150);
2307 assert!(e.sim.block_query(8, 63, 8).unwrap().air);
2308 assert!(
2309 e.core.pose.pos.y < 64.0,
2310 "falls into the hole: {:?}",
2311 e.core.pose.pos
2312 );
2313 }
2314
2315 #[test]
2317 fn dig_speed_and_item_facts() {
2318 let mut e = engine(GameVersion::V1_16_5);
2320 join(&mut e);
2321 let t0 = Instant::now();
2322 e.note_inbound(t0);
2323 let mut b = Script(|ctx: &mut TickContext<'_>| {
2324 let id = |n: &str| i32::from(ghast_data::v1_16_5::Item::from_name(n).unwrap().id());
2325 let bread = ctx.item_facts(&ghast_inventory::Stack::new(id("bread"), 3, 0));
2326 assert!(bread.is_some_and(|f| f.food && f.usable && !f.places_block && f.count == 3));
2327 let dirt = ctx.item_facts(&ghast_inventory::Stack::new(id("dirt"), 1, 0));
2328 assert!(dirt.is_some_and(|f| f.places_block && !f.food));
2329 });
2330 e.tick(&mut b, t0);
2331
2332 let mut e = engine(GameVersion::V1_8_9);
2333 join(&mut e);
2334 e.note_inbound(t0);
2335 let got = std::cell::Cell::new(None);
2336 let mut b = Script(|ctx: &mut TickContext<'_>| {
2337 let stone = ghast_protocol::BlockPos::new(8, 63, 8);
2338 let pick = ghast_inventory::Stack::new(278, 1, 0);
2339 let facts = ctx.item_facts(&pick).unwrap();
2340 assert!(!facts.places_block && !facts.food);
2341 got.set(Some((
2342 ctx.dig_speed(stone, None),
2343 ctx.dig_speed(stone, Some(&pick)),
2344 ctx.dig_speed(ghast_protocol::BlockPos::new(8, 64, 8), None),
2345 ctx.dig_speed(ghast_protocol::BlockPos::new(500, 63, 8), None),
2346 )));
2347 });
2348 for _ in 0..40 {
2350 e.tick(&mut b, t0);
2351 }
2352 assert!(e.core.pose.on_ground, "{:?}", e.core.pose);
2353 let (hand, pick, air, unloaded) = got.get().expect("ticked");
2354 assert_eq!(hand, Some(1.0 / 1.5 / 100.0));
2357 assert_eq!(pick, Some(8.0 / 1.5 / 30.0));
2359 assert_eq!(air, Some(0.0));
2360 assert_eq!(unloaded, None);
2361 }
2362
2363 #[test]
2366 fn place_needs_the_sent_look() {
2367 let mut e = engine(GameVersion::V1_8_9);
2368 join(&mut e);
2369 let t0 = Instant::now();
2370 e.note_inbound(t0);
2371 let floor = ghast_protocol::BlockPos::new(8, 63, 8);
2372 let mut results = Vec::new();
2373 let mut b = Script(|ctx: &mut TickContext<'_>| {
2374 ctx.set_aim(90.0, 89.5);
2375 results.push(ctx.place(floor, crate::Face::Up, Vec3::new(8.5, 64.0, 8.5)));
2376 });
2377 e.core
2379 .inv
2380 .on_set_slot(0, 36, Some(ghast_inventory::Stack::new(1, 64, 0)));
2381 let mut placed_tick = None;
2382 for i in 0..20 {
2383 e.tick(&mut b, t0);
2384 let n = names(e.outbound());
2385 if n.contains(&"block_place") {
2386 assert_eq!(n[..2], ["block_place", "swing"], "{n:?}");
2387 placed_tick.get_or_insert(i);
2388 }
2389 }
2390 let _ = b;
2391 let first = results.iter().position(|r| *r).unwrap();
2392 assert!(first > 3, "refused while still turning");
2393 assert_eq!(placed_tick, Some(first));
2394 assert_eq!(e.core.inv.held().map(|s| s.id), Some(1));
2395 }
2396
2397 #[test]
2398 fn sneak_action_order() {
2399 let mut e = engine(GameVersion::V1_8_9);
2400 join(&mut e);
2401 let mut b = Script(|ctx: &mut TickContext<'_>| {
2402 ctx.set_controls(Controls {
2403 sneak: true,
2404 ..Controls::default()
2405 });
2406 });
2407 e.tick(&mut b, Instant::now());
2408 let out = std::mem::take(e.outbound());
2409 assert_eq!(
2410 out[0],
2411 Outbound::EntityAction {
2412 entity_id: 7,
2413 action: EntityAction::StartSneaking
2414 }
2415 );
2416 assert!(matches!(out[1], Outbound::Move(_)));
2417 }
2418
2419 #[test]
2421 fn block_updates_per_version() {
2422 use ghast_protocol::BlockPos;
2423 for v in [GameVersion::V1_8_9, GameVersion::V1_16_5] {
2424 let mut e = engine(v);
2425 join(&mut e);
2426 e.sim.collect_block_updates(true);
2427 let t0 = Instant::now();
2428 e.note_inbound(t0);
2429 let raws = match v {
2430 GameVersion::V1_8_9 => {
2431 use ghast_protocol::v1_8::play::clientbound as cb;
2432 vec![
2433 RawPacket::from_packet(&cb::BlockChange {
2434 location: BlockPos::new(3, 63, 4),
2435 r#type: 59 << 4 | 7,
2436 })
2437 .unwrap(),
2438 RawPacket::from_packet(&cb::MultiBlockChange {
2439 chunk_x: 0,
2440 chunk_z: 0,
2441 records: vec![cb::MultiBlockChangeRecords {
2442 horizontal_pos: 5 << 4 | 6,
2443 y: 62,
2444 block_id: 60 << 4 | 7,
2445 }],
2446 })
2447 .unwrap(),
2448 ]
2449 }
2450 _ => {
2451 use ghast_protocol::v1_16_5::play::clientbound as cb;
2452 let local = (5 << 8 | 6 << 4 | 14) as i64;
2454 vec![
2455 RawPacket::from_packet(&cb::BlockChange {
2456 location: BlockPos::new(3, 63, 4),
2457 r#type: 1,
2458 })
2459 .unwrap(),
2460 RawPacket::from_packet(&cb::MultiBlockChange {
2461 chunk_coordinates: cb::MultiBlockChangeChunkCoordinates {
2462 x: 0,
2463 z: 0,
2464 y: 0xF_FFFF,
2465 },
2466 not_trust_edges: false,
2467 records: vec![9 << 12 | local],
2468 })
2469 .unwrap(),
2470 ]
2471 }
2472 };
2473 for raw in &raws {
2474 e.sim.world_packet(raw).unwrap();
2475 }
2476 let mut seen = Vec::new();
2477 let mut b = Script(|ctx: &mut TickContext<'_>| {
2478 seen.push(ctx.events().block_updates.clone());
2479 });
2480 e.tick(&mut b, t0);
2481 e.tick(&mut b, t0);
2482 let _ = b;
2483 let want = match v {
2484 GameVersion::V1_8_9 => vec![
2485 (BlockPos::new(3, 63, 4), 59 << 4 | 7),
2486 (BlockPos::new(5, 62, 6), 60 << 4 | 7),
2487 ],
2488 _ => vec![(BlockPos::new(3, 63, 4), 1), (BlockPos::new(5, -2, 6), 9)],
2489 };
2490 assert_eq!(seen, [want, Vec::new()], "{v:?}");
2491
2492 e.sim.collect_block_updates(false);
2494 e.sim.world_packet(&raws[0]).unwrap();
2495 let mut n = usize::MAX;
2496 let mut b = Script(|ctx: &mut TickContext<'_>| n = ctx.events().block_updates.len());
2497 e.tick(&mut b, t0);
2498 let _ = b;
2499 assert_eq!(n, 0);
2500 }
2501 }
2502
2503 #[test]
2505 fn set_flying_needs_may_fly() {
2506 for v in [GameVersion::V1_8_9, GameVersion::V1_16_5] {
2507 let mut e = engine(v);
2508 join(&mut e);
2509 let t0 = Instant::now();
2510 e.note_inbound(t0);
2511 e.handle(Inbound::Teleport(Teleport {
2513 x: 8.5,
2514 y: 70.0,
2515 z: 8.5,
2516 yaw: 90.0,
2517 pitch: 0.0,
2518 flags: 0,
2519 teleport_id: Some(2),
2520 }));
2521 e.outbound().clear();
2522 let mut b = Script(|ctx: &mut TickContext<'_>| ctx.set_flying(true));
2523 e.tick(&mut b, t0);
2524 assert!(!names(e.outbound()).contains(&"abilities"), "{v:?}");
2525 assert!(!e.core.pose.flying);
2526 e.handle(Inbound::Abilities(Abilities {
2527 flags: 4,
2528 ..Abilities::default()
2529 }));
2530 e.tick(&mut b, t0);
2531 let out = std::mem::take(e.outbound());
2532 assert_eq!(
2533 out[0],
2534 Outbound::Abilities {
2535 flags: 6,
2536 flying_speed: 0.05,
2537 walking_speed: 0.1
2538 },
2539 "{v:?}"
2540 );
2541 assert!(matches!(out[1], Outbound::Move(_)), "{out:?}");
2542 assert!(e.core.pose.flying && e.core.sent.flying);
2543 e.tick(&mut b, t0);
2545 assert!(!names(e.outbound()).contains(&"abilities"));
2546 let _ = b;
2547 }
2548 }
2549
2550 #[test]
2552 fn finished_behaviour_is_not_ticked() {
2553 struct Three(std::rc::Rc<std::cell::Cell<u32>>);
2554 impl Behavior for Three {
2555 fn tick(&mut self, _ctx: &mut TickContext<'_>) {
2556 self.0.set(self.0.get() + 1);
2557 }
2558 fn running(&self) -> bool {
2559 self.0.get() < 3
2560 }
2561 }
2562 let ticks = std::rc::Rc::new(std::cell::Cell::new(0));
2563 let mut e = engine(GameVersion::V1_8_9);
2564 join(&mut e);
2565 let t0 = Instant::now();
2566 e.note_inbound(t0);
2567 let mut b: Box<dyn Behavior> = Box::new(Three(ticks.clone()));
2568 for _ in 0..10 {
2569 e.tick(&mut b, t0);
2570 let out = std::mem::take(e.outbound());
2572 assert!(
2573 out.iter().any(|o| matches!(o, Outbound::Move(_))),
2574 "{out:?}"
2575 );
2576 }
2577 assert_eq!(
2578 ticks.get(),
2579 3,
2580 "not ticked after running() == false (through the Box)"
2581 );
2582 }
2583
2584 #[test]
2586 fn turn_held_on_a_place_tick() {
2587 let mut e = engine(GameVersion::V1_8_9);
2588 join(&mut e);
2589 let t0 = Instant::now();
2590 e.note_inbound(t0);
2591 e.core
2592 .inv
2593 .on_set_slot(0, 36, Some(ghast_inventory::Stack::new(1, 64, 0)));
2594 let floor = ghast_protocol::BlockPos::new(8, 63, 8);
2595 let mut tick = 0;
2596 let mut placed_at = None;
2597 let mut pitches = Vec::new();
2598 let mut b = Script(|ctx: &mut TickContext<'_>| {
2599 tick += 1;
2600 if placed_at.is_none() {
2601 ctx.set_aim(90.0, 89.5);
2602 if ctx.place(floor, crate::Face::Up, Vec3::new(8.5, 64.0, 8.5)) {
2603 placed_at = Some(tick);
2604 ctx.set_aim(90.0, -60.0);
2606 }
2607 } else {
2608 ctx.set_aim(90.0, -60.0);
2609 }
2610 pitches.push(ctx.pose().pitch);
2611 });
2612 for _ in 0..30 {
2613 e.tick(&mut b, t0);
2614 e.outbound().clear();
2615 }
2616 let _ = b;
2617 let p = placed_at.expect("placed");
2618 assert_eq!(e.core.held_turns, 1);
2619 assert_eq!(pitches[p], pitches[p - 1], "{pitches:?}");
2621 assert!(pitches[p + 1] < pitches[p], "{pitches:?}");
2622 }
2623
2624 #[test]
2626 fn click_ticket_matches_the_confirm() {
2627 let mut e = engine(GameVersion::V1_8_9);
2628 join(&mut e);
2629 let t0 = Instant::now();
2630 e.note_inbound(t0);
2631 e.core
2632 .inv
2633 .on_set_slot(0, 36, Some(ghast_inventory::Stack::new(1, 64, 0)));
2634 let mut ticket = None;
2635 let mut n = 0;
2636 let mut b = Script(|ctx: &mut TickContext<'_>| {
2637 n += 1;
2638 if n == 1 {
2639 ctx.open_inventory().unwrap();
2640 } else if ticket.is_none() {
2641 ticket = ctx
2642 .click(ghast_inventory::Click::Pickup {
2643 slot: 36,
2644 right: false,
2645 })
2646 .ok();
2647 }
2648 });
2649 for _ in 0..3 {
2650 e.tick(&mut b, t0);
2651 }
2652 let _ = b;
2653 let t = ticket.expect("click sent");
2654 assert_eq!(t.window_id, 0);
2655 assert!(t.matches(&ghast_inventory::ClickResult {
2656 window_id: 0,
2657 action: t.action,
2658 outcome: ghast_inventory::ClickOutcome::Accepted,
2659 }));
2660 }
2661
2662 #[test]
2665 fn planting_uses_the_seed_at_once() {
2666 use ghast_protocol::v1_8::play::clientbound as cb;
2667 for (soil, item, used) in [
2668 (60 << 4 | 7, 295, true),
2669 (1 << 4, 295, false),
2670 (60 << 4, 1, false),
2671 ] {
2672 let mut e = engine(GameVersion::V1_8_9);
2673 join(&mut e);
2674 let t0 = Instant::now();
2675 e.note_inbound(t0);
2676 let floor = ghast_protocol::BlockPos::new(8, 63, 8);
2677 let raw = RawPacket::from_packet(&cb::BlockChange {
2678 location: floor,
2679 r#type: soil,
2680 })
2681 .unwrap();
2682 e.sim.world_packet(&raw).unwrap();
2683 e.core
2684 .inv
2685 .on_set_slot(0, 36, Some(ghast_inventory::Stack::new(item, 64, 0)));
2686 let mut done = false;
2687 let mut b = Script(|ctx: &mut TickContext<'_>| {
2688 ctx.set_aim(90.0, 89.5);
2689 if !done {
2690 done = ctx.place(floor, crate::Face::Up, Vec3::new(8.5, 64.0, 8.5));
2691 }
2692 });
2693 let mut sent = None;
2694 for _ in 0..20 {
2695 e.tick(&mut b, t0);
2696 for o in e.outbound().drain(..) {
2697 if let Outbound::Place { held, .. } = o {
2698 sent = held.map(|h| h.count);
2699 }
2700 }
2701 }
2702 let _ = b;
2703 assert_eq!(sent, Some(64), "the packet carries the stack before use");
2704 let left = e.core.inv.held().map(|s| s.count);
2705 assert_eq!(
2706 left,
2707 Some(if used { 63 } else { 64 }),
2708 "soil {soil} item {item}"
2709 );
2710 }
2711 }
2712
2713 #[test]
2716 fn planned_look_is_the_next_look() {
2717 let mut e = engine(GameVersion::V1_8_9);
2718 join(&mut e);
2719 let t0 = Instant::now();
2720 e.note_inbound(t0);
2721 e.core
2722 .inv
2723 .on_set_slot(0, 36, Some(ghast_inventory::Stack::new(1, 64, 0)));
2724 let floor = ghast_protocol::BlockPos::new(8, 63, 8);
2725 let mut n = 0;
2726 let mut placed = false;
2727 let mut plans: Vec<(f32, f32)> = Vec::new();
2728 let mut b = Script(|ctx: &mut TickContext<'_>| {
2729 n += 1;
2730 if n % 5 == 0 {
2731 } else if !placed {
2733 ctx.set_aim(90.0, 89.5);
2734 if ctx.place(floor, crate::Face::Up, Vec3::new(8.5, 64.0, 8.5)) {
2735 placed = true;
2736 ctx.set_aim(90.0, -60.0);
2737 }
2738 } else {
2739 ctx.set_aim(150.0, -60.0);
2740 }
2741 let p = ctx.planned_look();
2743 assert_eq!(p, ctx.planned_look());
2744 plans.push(p);
2745 });
2746 let mut looks = Vec::new();
2747 for _ in 0..40 {
2748 e.tick(&mut b, t0);
2749 e.outbound().clear();
2750 looks.push((e.core.pose.yaw, e.core.pose.pitch));
2751 }
2752 let _ = b;
2753 assert!(placed);
2754 assert_eq!(e.core.test_held_turns(), 1);
2755 assert_eq!(plans, looks);
2756 }
2757
2758 #[test]
2761 fn teleport_event_has_the_sent_pose_before() {
2762 for v in [GameVersion::V1_8_9, GameVersion::V1_16_5] {
2763 let mut e = engine(v);
2764 join(&mut e);
2765 let t0 = Instant::now();
2766 e.note_inbound(t0);
2767 let mut seen = Vec::new();
2768 let mut b = Script(|ctx: &mut TickContext<'_>| {
2769 seen.extend(ctx.events().teleports.iter().copied());
2770 });
2771 e.tick(&mut b, t0);
2772 let sent = e.core.sent;
2773 let tp = Teleport {
2775 x: 10.5,
2776 y: 64.0,
2777 z: 8.5,
2778 yaw: 30.0,
2779 pitch: 0.0,
2780 flags: crate::common::teleport_flags::YAW,
2781 teleport_id: Some(9),
2782 };
2783 e.handle(Inbound::Teleport(tp));
2784 e.tick(&mut b, t0);
2785 e.tick(&mut b, t0);
2786 let _ = b;
2787 assert_eq!(seen.len(), 2, "{v:?}");
2789 let ev = seen[1];
2790 assert_eq!(ev.teleport, tp);
2791 assert_eq!(ev.sent_before, sent);
2792 assert_eq!(ev.after.pos, Vec3::new(10.5, 64.0, 8.5));
2793 assert_eq!(ev.after.yaw, sent.yaw + 30.0, "{v:?}");
2794 }
2795 }
2796
2797 #[test]
2801 fn closed_window_frees_the_buttons() {
2802 for v in [GameVersion::V1_8_9, GameVersion::V1_16_5] {
2803 let mut e = engine(v);
2804 join(&mut e);
2805 let t0 = Instant::now();
2806 e.note_inbound(t0);
2807 if v == GameVersion::V1_16_5 {
2808 use ghast_protocol::v1_16_5::play::clientbound as cb;
2810 let raw = RawPacket::from_packet(&cb::BlockChange {
2811 location: ghast_protocol::BlockPos::new(8, 63, 8),
2812 r#type: 1,
2813 })
2814 .unwrap();
2815 e.sim.world_packet(&raw).unwrap();
2816 }
2817 e.core.inv.on_open_window(
2818 5,
2819 Some(ghast_inventory::WindowKind::Chest { rows: 3 }),
2820 "Vault".into(),
2821 );
2822 e.handle(Inbound::OpenWindow(5));
2823 e.core
2826 .inv
2827 .on_set_slot(-1, -1, Some(ghast_inventory::Stack::new(1, 3, 0)));
2828 let mut n = 0;
2829 let mut closed_at = None;
2830 let mut b = Script(|ctx: &mut TickContext<'_>| {
2831 n += 1;
2832 ctx.set_aim(90.0, 89.5);
2833 ctx.set_buttons(Buttons {
2834 left: true,
2835 right: false,
2836 });
2837 if n == 15 {
2838 assert!(ctx.inventory().cursor().is_some());
2839 ctx.close_window().unwrap();
2840 assert_eq!(ctx.open_window(), None);
2841 let inv = ctx.inventory();
2842 assert!(!inv.gui_open() && inv.window().is_none() && inv.cursor().is_none());
2843 closed_at = Some(n);
2844 }
2845 });
2846 let mut first_dig = None;
2847 for i in 1..=30 {
2848 e.tick(&mut b, t0);
2849 let out = std::mem::take(e.outbound());
2850 if first_dig.is_none()
2851 && out.iter().any(|o| {
2852 matches!(
2853 o,
2854 Outbound::Dig {
2855 status: crate::common::DigStatus::Start,
2856 ..
2857 }
2858 )
2859 })
2860 {
2861 first_dig = Some(i);
2862 }
2863 }
2864 let _ = b;
2865 assert_eq!(closed_at, Some(15));
2866 let d = first_dig.unwrap_or_else(|| panic!("{v:?}: no dig after the close"));
2867 assert!(d >= 15, "{v:?}: dug at tick {d} with the window open");
2869 }
2870 }
2871
2872 #[test]
2874 fn swings_count_every_arm_swing() {
2875 let mut e = engine(GameVersion::V1_8_9);
2876 join(&mut e);
2877 let t0 = Instant::now();
2878 e.note_inbound(t0);
2879 let mut n = 0;
2880 let mut seen = Vec::new();
2881 let mut b = Script(|ctx: &mut TickContext<'_>| {
2882 n += 1;
2883 ctx.set_aim(90.0, 89.5);
2884 if n == 1 {
2885 ctx.swing();
2886 ctx.swing();
2887 }
2888 if n == 20 {
2889 ctx.set_buttons(Buttons {
2891 left: true,
2892 right: false,
2893 });
2894 }
2895 seen.push(ctx.swings());
2896 });
2897 let mut sent = 0u64;
2898 for _ in 0..30 {
2899 e.tick(&mut b, t0);
2900 sent += count_swings(&std::mem::take(e.outbound()));
2901 }
2902 let _ = b;
2903 assert_eq!(seen[0], 2, "this tick's own swings count at once");
2904 assert!(sent > 10, "the held button swings: {sent}");
2905 assert_eq!(*seen.last().unwrap() + 1, sent);
2907 assert_eq!(e.core.swings, sent);
2908 }
2909
2910 #[test]
2912 fn column_view_and_world_config() {
2913 for config in [
2914 None,
2915 Some(
2916 ghast_world::WorldConfig::default()
2917 .with_biomes(true)
2918 .with_light(true),
2919 ),
2920 ] {
2921 let sim = crate::sim::Sim18::with(&crate::sim::SimOptions {
2922 world: config,
2923 ..crate::sim::SimOptions::default()
2924 });
2925 let mut e = Engine::new(opts(GameVersion::V1_8_9), Box::new(sim) as Box<dyn Sim>);
2926 join(&mut e);
2927 let w: &dyn WorldView = &e.sim;
2928 let col = w.column(0, 0).expect("loaded");
2929 assert_eq!(u32::from(col.raw(8, 63, 8)), w.raw_block(8, 63, 8).unwrap());
2930 assert_eq!(col.raw(8, 63, 8), 1 << 4);
2931 assert!(w.column(5, 5).is_none());
2932 let light = col.sky_light(8, 63, 8);
2933 assert_eq!(light.is_some(), config.is_some(), "{config:?}");
2934 let biomes = !matches!(col.biomes(), ghast_world::Biomes::None);
2935 assert_eq!(biomes, config.is_some(), "{config:?}");
2936 }
2937 }
2938
2939 #[test]
2942 fn after_tick_sees_the_sent_pose() {
2943 struct Obs {
2944 n: u32,
2945 views: Vec<(u64, Pose, u64)>,
2946 }
2947 impl Behavior for Obs {
2948 fn tick(&mut self, ctx: &mut TickContext<'_>) {
2949 self.n += 1;
2950 ctx.set_aim(150.0, 10.0);
2951 ctx.set_controls(Controls {
2952 forward: true,
2953 ..Controls::default()
2954 });
2955 }
2956 fn running(&self) -> bool {
2957 self.n < 5
2958 }
2959 fn after_tick(&mut self, view: &TickView<'_>) {
2960 self.views.push((view.tick(), *view.sent(), view.swings()));
2961 assert!(view.world().is_column_loaded(0, 0));
2962 assert_eq!(view.version(), GameVersion::V1_8_9);
2963 }
2964 }
2965 let mut e = engine(GameVersion::V1_8_9);
2966 join(&mut e);
2967 let t0 = Instant::now();
2968 e.note_inbound(t0);
2969 let mut b = Obs {
2970 n: 0,
2971 views: Vec::new(),
2972 };
2973 let mut sent = Vec::new();
2974 for _ in 0..10 {
2975 e.tick(&mut b, t0);
2976 e.outbound().clear();
2977 sent.push(e.core.sent);
2978 }
2979 assert_eq!(b.n, 5);
2980 assert_eq!(b.views.len(), 10, "also after the behaviour finished");
2981 for (i, (_, p, _)) in b.views.iter().enumerate() {
2982 assert_eq!(*p, sent[i]);
2983 }
2984 assert_ne!(
2985 b.views[0].1.yaw, b.views[4].1.yaw,
2986 "the view follows the turn"
2987 );
2988 }
2989
2990 #[test]
2993 fn block_entities_follow_the_world_config() {
2994 use ghast_protocol::v1_8::play::clientbound as cb;
2995 for on in [false, true] {
2996 let sim = crate::sim::Sim18::with(&crate::sim::SimOptions {
2997 world: on.then(|| ghast_world::WorldConfig::default().with_block_entities(true)),
2998 ..crate::sim::SimOptions::default()
2999 });
3000 assert_eq!(sim.world_packet_ids().contains(&cb::id::UPDATE_SIGN), on);
3001 let mut e = Engine::new(opts(GameVersion::V1_8_9), Box::new(sim) as Box<dyn Sim>);
3002 join(&mut e);
3003 let raw = RawPacket::from_packet(&cb::UpdateSign {
3004 location: ghast_protocol::BlockPos::new(3, 64, 4),
3005 text1: "\"Hello\"".into(),
3006 text2: "\"\"".into(),
3007 text3: "\"\"".into(),
3008 text4: "\"\"".into(),
3009 })
3010 .unwrap();
3011 e.sim.world_packet(&raw).unwrap();
3012 let w: &dyn WorldView = &e.sim;
3013 let be = w.column(0, 0).unwrap().block_entity(3, 64, 4);
3014 assert_eq!(be.is_some(), on, "{be:?}");
3015 }
3016 }
3017}