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