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 pub fn dig(&mut self, pos: ghast_protocol::BlockPos, face: crate::common::Face) -> bool {
1734 if !self
1735 .target()
1736 .is_some_and(|t| t.pos == pos && t.face == face)
1737 {
1738 return false;
1739 }
1740 let world = self.world;
1741 let blocks = |x: i32, y: i32, z: i32| world.block_query(x, y, z);
1742 let c = &mut *self.core;
1743 let mut placed = false;
1744 let mut io = HandsIo {
1745 blocks: &blocks,
1746 target: None,
1747 held: self.held,
1748 held_stack: c.inv.held().cloned(),
1749 food: c.health.food,
1750 modern: c.opts.version != GameVersion::V1_8_9,
1751 off_held: None,
1752 off_stack: None,
1753 entity: None,
1754 factors: DigFactors {
1755 on_ground: c.pose.on_ground,
1756 in_water: false,
1757 aqua_affinity: c.aqua_affinity,
1758 haste: c.effects.get(&3).map_or(0, |a| (*a as u8).wrapping_add(1)),
1759 fatigue: c.effects.get(&4).map_or(0, |a| (*a as u8).wrapping_add(1)),
1760 },
1761 sneaking: c.pose.sneaking,
1762 out: &mut c.out,
1763 broken: &mut c.broken_now,
1764 placed: &mut placed,
1765 };
1766 c.hands.click_block(pos, face, &mut io);
1767 c.out.push(Outbound::Swing);
1768 true
1769 }
1770
1771 pub fn place(
1775 &mut self,
1776 support: ghast_protocol::BlockPos,
1777 face: crate::common::Face,
1778 hit: Vec3,
1779 ) -> bool {
1780 let Some(t) = self.target().filter(|t| t.pos == support && t.face == face) else {
1781 return false;
1782 };
1783 let world = self.world;
1784 let Some(b) = world.block_query(support.x, support.y, support.z) else {
1785 return false;
1786 };
1787 let blocks = |x: i32, y: i32, z: i32| world.block_query(x, y, z);
1788 let c = &mut *self.core;
1789 let mut placed = false;
1790 let first = c.out.len();
1791 let mut io = HandsIo {
1792 blocks: &blocks,
1793 target: None,
1794 held: self.held,
1795 held_stack: c.inv.held().cloned(),
1796 food: c.health.food,
1797 modern: c.opts.version != GameVersion::V1_8_9,
1798 off_held: None,
1799 off_stack: None,
1800 entity: None,
1801 factors: DigFactors::default(),
1802 sneaking: c.pose.sneaking,
1803 out: &mut c.out,
1804 broken: &mut c.broken_now,
1805 placed: &mut placed,
1806 };
1807 c.hands
1808 .right_click_block(BlockHit { hit, ..t }, &b, &mut io);
1809 let _ = placed;
1810 predict_planting(c, world, first);
1811 c.out.push(Outbound::Swing);
1812 true
1813 }
1814
1815 #[must_use]
1817 pub fn last_inbound(&self) -> Option<Instant> {
1818 self.core.last_inbound
1819 }
1820
1821 #[must_use]
1827 pub fn planned_look(&self) -> (f32, f32) {
1828 let c = &*self.core;
1829 let now = (c.pose.yaw, c.pose.pitch);
1830 #[cfg(test)]
1831 if let Some(l) = c.forced_look {
1832 return l;
1833 }
1834 if !c.aimed {
1835 return now;
1836 }
1837 let mut r = c.rotation;
1838 let (yaw, pitch) = r.step(c.pose.yaw, c.pose.pitch);
1839 if c.opts.hold_turn_on_action && !post_look_ok(c, yaw, pitch) {
1840 now
1841 } else {
1842 (yaw, pitch)
1843 }
1844 }
1845
1846 pub fn set_flying(&mut self, flying: bool) {
1850 self.core.want_flying = Some(flying);
1851 }
1852
1853 #[must_use]
1856 pub fn held_turns(&self) -> u64 {
1857 self.core.held_turns
1858 }
1859
1860 pub fn status(&mut self, text: &str) {
1862 if self.core.status != text {
1863 text.clone_into(&mut self.core.status);
1864 log::info!(
1865 "[{}] t{} status: {text}",
1866 self.core.opts.name,
1867 self.core.tick
1868 );
1869 }
1870 }
1871}
1872
1873#[cfg(test)]
1874mod tests {
1875 use super::*;
1876 use crate::common::Teleport;
1877 use crate::common::{ChatPosition, EntityAction, JoinGame};
1878 use ghast_protocol::RawPacket;
1879
1880 fn opts(version: GameVersion) -> CoreOptions {
1881 CoreOptions {
1882 name: "test".into(),
1883 version,
1884 settings: ClientSettings::default(),
1885 brand: Some("vanilla".into()),
1886 sensitivity: 0.5,
1887 silent_hold: Duration::from_millis(1500),
1888 hold_turn_on_action: true,
1889 inventory: match version {
1890 GameVersion::V1_8_9 => ghast_inventory::Version::V1_8,
1891 _ => ghast_inventory::Version::V1_16_5,
1892 },
1893 }
1894 }
1895
1896 fn engine(version: GameVersion) -> Engine<Box<dyn Sim>> {
1898 let sim: Box<dyn Sim> = match version {
1899 GameVersion::V1_8_9 => Box::new(crate::sim::Sim18::new()),
1900 _ => Box::new(crate::sim::Sim16::new()),
1901 };
1902 Engine::new(opts(version), sim)
1903 }
1904
1905 fn load(e: &mut Engine<Box<dyn Sim>>, x: i32, z: i32) {
1907 let raw = match e.core.opts.version {
1908 GameVersion::V1_8_9 => crate::sim::test_chunks::v1_8(x, z),
1909 _ => crate::sim::test_chunks::v1_16_5(x, z),
1910 };
1911 e.sim.world_packet(&raw).unwrap();
1912 }
1913
1914 fn join(e: &mut Engine<Box<dyn Sim>>) {
1915 e.handle(Inbound::JoinGame(JoinGame {
1916 entity_id: 7,
1917 game_mode: GameMode::Survival,
1918 dimension: "0".into(),
1919 hardcore: false,
1920 dimension_nbt: None,
1921 respawn_screen: true,
1922 }));
1923 load(e, 0, 0);
1924 e.handle(Inbound::Teleport(Teleport {
1925 x: 8.5,
1926 y: 64.0,
1927 z: 8.5,
1928 yaw: 90.0,
1929 pitch: 0.0,
1930 flags: 0,
1931 teleport_id: None,
1932 }));
1933 e.outbound().clear();
1934 }
1935
1936 fn names(out: &mut Vec<Outbound>) -> Vec<&'static str> {
1938 let v = out
1939 .iter()
1940 .map(|o| match o {
1941 Outbound::Move(m) => m.trace_id(),
1942 Outbound::Chat(_) => "chat",
1943 Outbound::HeldSlot(_) => "held_slot",
1944 Outbound::Swing => "swing",
1945 Outbound::EntityAction { .. } => "entity_action",
1946 Outbound::KeepAlive(_) => "keep_alive",
1947 Outbound::Transaction { .. } => "transaction",
1948 Outbound::TeleportConfirm(_) => "teleport_confirm",
1949 Outbound::Respawn => "respawn",
1950 Outbound::Settings(_) => "settings",
1951 Outbound::Brand(_) => "brand",
1952 Outbound::Abilities { .. } => "abilities",
1953 Outbound::Dig { .. } => "block_dig",
1954 Outbound::Place { .. } | Outbound::UseItem { .. } => "block_place",
1955 Outbound::SwingOffHand => "swing_off",
1956 Outbound::UseEntity { .. } => "use_entity",
1957 Outbound::Inventory(_) => "inventory",
1958 })
1959 .collect();
1960 out.clear();
1961 v
1962 }
1963
1964 struct Script<F: FnMut(&mut TickContext<'_>)>(F);
1965 impl<F: FnMut(&mut TickContext<'_>)> Behavior for Script<F> {
1966 fn tick(&mut self, ctx: &mut TickContext<'_>) {
1967 (self.0)(ctx);
1968 }
1969 }
1970
1971 #[test]
1972 fn join_sends_settings_then_brand() {
1973 let mut e = engine(GameVersion::V1_8_9);
1974 e.handle(Inbound::JoinGame(JoinGame {
1975 entity_id: 1,
1976 game_mode: GameMode::Creative,
1977 dimension: "0".into(),
1978 hardcore: false,
1979 dimension_nbt: None,
1980 respawn_screen: true,
1981 }));
1982 assert_eq!(names(e.outbound()), ["settings", "brand"]);
1983 }
1984
1985 #[test]
1986 fn nothing_before_the_first_teleport() {
1987 let mut e = engine(GameVersion::V1_8_9);
1988 let mut b = Idle;
1989 let t0 = Instant::now();
1990 load(&mut e, 0, 0);
1991 e.outbound().clear();
1992 for i in 0..5 {
1993 e.tick(&mut b, t0 + Duration::from_millis(50 * i));
1994 }
1995 assert!(e.outbound().is_empty());
1996 }
1997
1998 #[test]
1999 fn teleport_reply_per_version() {
2000 let mut e = engine(GameVersion::V1_16_5);
2001 e.handle(Inbound::Teleport(Teleport {
2002 x: 1.0,
2003 y: 2.0,
2004 z: 3.0,
2005 yaw: 450.0,
2006 pitch: 100.0,
2007 flags: 0,
2008 teleport_id: Some(5),
2009 }));
2010 let out = std::mem::take(e.outbound());
2011 assert_eq!(out[0], Outbound::TeleportConfirm(5));
2012 match out[1] {
2013 Outbound::Move(MovePacket::PositionLook {
2014 yaw,
2015 pitch,
2016 on_ground,
2017 ..
2018 }) => {
2019 assert_eq!((yaw, pitch, on_ground), (90.0, 90.0, false));
2020 }
2021 ref o => panic!("unexpected {o:?}"),
2022 }
2023 let mut e = engine(GameVersion::V1_8_9);
2024 e.handle(Inbound::Teleport(Teleport {
2025 x: 1.0,
2026 y: 2.0,
2027 z: 3.0,
2028 yaw: 450.0,
2029 pitch: 100.0,
2030 flags: 0,
2031 teleport_id: None,
2032 }));
2033 let out = std::mem::take(e.outbound());
2034 assert_eq!(out.len(), 1);
2035 assert!(matches!(
2037 out[0],
2038 Outbound::Move(MovePacket::PositionLook {
2039 yaw: 90.0,
2040 pitch: 100.0,
2041 ..
2042 })
2043 ));
2044 }
2045
2046 #[test]
2047 fn actions_precede_the_single_movement_packet() {
2048 let mut e = engine(GameVersion::V1_8_9);
2049 join(&mut e);
2050 let t0 = Instant::now();
2051 e.note_inbound(t0);
2052 let mut b = Script(|ctx: &mut TickContext<'_>| {
2053 if ctx.tick() == 2 {
2054 ctx.select_slot(3);
2055 ctx.chat("hello");
2056 ctx.swing();
2057 ctx.set_controls(Controls {
2058 sneak: true,
2059 ..Controls::default()
2060 });
2061 }
2062 });
2063 e.tick(&mut b, t0);
2064 assert_eq!(names(e.outbound()), ["C06"]);
2065 e.tick(&mut b, t0);
2066 assert_eq!(
2067 names(e.outbound()),
2068 ["held_slot", "chat", "swing", "entity_action", "C03"]
2069 );
2070 for _ in 0..100 {
2071 e.tick(&mut b, t0);
2072 let n = names(e.outbound());
2073 assert_eq!(n.len(), 1, "one movement packet per tick: {n:?}");
2074 }
2075 }
2076
2077 #[test]
2078 fn standing_bot_packets() {
2079 let mut e = engine(GameVersion::V1_8_9);
2080 join(&mut e);
2081 let mut b = Idle;
2082 let t0 = Instant::now();
2083 e.note_inbound(t0);
2084 e.tick(&mut b, t0);
2086 let out = std::mem::take(e.outbound());
2087 assert!(matches!(
2088 out[..],
2089 [Outbound::Move(MovePacket::PositionLook {
2090 on_ground: false,
2091 ..
2092 })]
2093 ));
2094 let mut kinds = Vec::new();
2096 for _ in 0..42 {
2097 e.tick(&mut b, t0);
2098 kinds.extend(names(e.outbound()));
2099 }
2100 assert_eq!(kinds.iter().filter(|k| **k == "C04").count(), 2);
2101 assert_eq!(kinds.iter().filter(|k| **k == "C03").count(), 40);
2102 assert_eq!((kinds[20], kinds[41]), ("C04", "C04"));
2103 assert!(e.core.sent.on_ground);
2104 }
2105
2106 #[test]
2107 fn silent_server_freezes_behaviour() {
2108 let mut e = engine(GameVersion::V1_8_9);
2109 join(&mut e);
2110 let t0 = Instant::now();
2111 e.note_inbound(t0);
2112 let mut ticks = 0;
2113 let mut b = Script(|ctx: &mut TickContext<'_>| {
2114 ticks += 1;
2115 ctx.set_controls(Controls {
2116 sneak: true,
2117 ..Controls::default()
2118 });
2119 });
2120 e.tick(&mut b, t0 + Duration::from_millis(100));
2121 e.tick(&mut b, t0 + Duration::from_millis(1600));
2122 let n = names(e.outbound());
2124 assert_eq!(n, ["entity_action", "C06", "entity_action", "C03"]);
2125 e.note_inbound(t0 + Duration::from_millis(1700));
2126 e.tick(&mut b, t0 + Duration::from_millis(1700));
2127 let _ = b;
2128 assert_eq!(ticks, 2);
2129 }
2130
2131 #[test]
2132 fn keepalive_answered_transactions_left_to_the_inventory() {
2133 let mut e = engine(GameVersion::V1_8_9);
2134 join(&mut e);
2135 e.handle(Inbound::KeepAlive(42));
2136 e.handle(Inbound::Transaction {
2139 window_id: 0,
2140 action: -5,
2141 accepted: false,
2142 });
2143 assert_eq!(std::mem::take(e.outbound()), [Outbound::KeepAlive(42)]);
2144 }
2145
2146 #[test]
2147 fn panicking_behaviour_does_not_stop_the_tick() {
2148 let mut e = engine(GameVersion::V1_8_9);
2149 join(&mut e);
2150 let t0 = Instant::now();
2151 let mut b = Script(|ctx: &mut TickContext<'_>| {
2152 if ctx.tick() == 2 {
2153 panic!("boom");
2154 }
2155 });
2156 let hook = std::panic::take_hook();
2157 std::panic::set_hook(Box::new(|_| {}));
2158 for _ in 0..5 {
2159 e.tick(&mut b, t0);
2160 }
2161 std::panic::set_hook(hook);
2162 assert!(e.state.failed);
2163 assert_eq!(names(e.outbound()).len(), 5);
2164 }
2165
2166 #[test]
2167 fn chat_events_reach_the_behaviour_once() {
2168 let mut e = engine(GameVersion::V1_16_5);
2169 join(&mut e);
2170 e.handle(Inbound::Chat(ChatMessage {
2171 json: r#"{"text":"hi"}"#.into(),
2172 position: ChatPosition::System,
2173 sender: None,
2174 }));
2175 let mut seen = Vec::new();
2176 let mut b = Script(|ctx: &mut TickContext<'_>| {
2177 seen.extend(ctx.events().chat.iter().map(ChatMessage::plain));
2178 });
2179 let t0 = Instant::now();
2180 e.tick(&mut b, t0);
2181 e.tick(&mut b, t0);
2182 let _ = b;
2183 assert_eq!(seen, ["hi"]);
2184 }
2185
2186 #[test]
2187 fn immediate_respawn_1_16() {
2188 let mut e = engine(GameVersion::V1_16_5);
2189 join(&mut e);
2190 e.handle(Inbound::GameState {
2191 reason: 11,
2192 value: 1.0,
2193 });
2194 e.handle(Inbound::Died { entity_id: 7 });
2195 assert_eq!(e.outbound().as_slice(), [Outbound::Respawn]);
2196 }
2197
2198 #[test]
2201 fn dig_with_the_left_button() {
2202 let mut e = engine(GameVersion::V1_8_9);
2203 join(&mut e);
2204 let t0 = Instant::now();
2205 e.note_inbound(t0);
2206 let mut b = Script(|ctx: &mut TickContext<'_>| {
2207 ctx.set_aim(90.0, 89.5);
2208 let down = ctx.target().is_some_and(|t| {
2209 t.pos == ghast_protocol::BlockPos::new(8, 63, 8) && t.face == crate::Face::Up
2210 });
2211 ctx.set_buttons(Buttons {
2212 left: down,
2213 right: false,
2214 });
2215 });
2216 let mut ticks = Vec::new();
2217 for _ in 0..200 {
2218 e.tick(&mut b, t0);
2219 let n = names(e.outbound());
2220 assert_eq!(n.iter().filter(|x| x.starts_with('C')).count(), 1, "{n:?}");
2221 assert!(n.last().unwrap().starts_with('C'), "movement last: {n:?}");
2222 ticks.push(n);
2223 }
2224 let flat: Vec<&str> = ticks.iter().flatten().copied().collect();
2225 assert_eq!(
2226 flat.iter().filter(|x| **x == "block_dig").count(),
2227 2,
2228 "start + finish"
2229 );
2230 assert!(e.core.hands.broken == 1);
2231 let start = ticks.iter().position(|t| t.contains(&"block_dig")).unwrap();
2233 let finish = ticks
2234 .iter()
2235 .rposition(|t| t.contains(&"block_dig"))
2236 .unwrap();
2237 assert_eq!(finish - start, 150);
2238 assert!(e.sim.block_query(8, 63, 8).unwrap().air);
2239 assert!(
2240 e.core.pose.pos.y < 64.0,
2241 "falls into the hole: {:?}",
2242 e.core.pose.pos
2243 );
2244 }
2245
2246 #[test]
2249 fn place_needs_the_sent_look() {
2250 let mut e = engine(GameVersion::V1_8_9);
2251 join(&mut e);
2252 let t0 = Instant::now();
2253 e.note_inbound(t0);
2254 let floor = ghast_protocol::BlockPos::new(8, 63, 8);
2255 let mut results = Vec::new();
2256 let mut b = Script(|ctx: &mut TickContext<'_>| {
2257 ctx.set_aim(90.0, 89.5);
2258 results.push(ctx.place(floor, crate::Face::Up, Vec3::new(8.5, 64.0, 8.5)));
2259 });
2260 e.core
2262 .inv
2263 .on_set_slot(0, 36, Some(ghast_inventory::Stack::new(1, 64, 0)));
2264 let mut placed_tick = None;
2265 for i in 0..20 {
2266 e.tick(&mut b, t0);
2267 let n = names(e.outbound());
2268 if n.contains(&"block_place") {
2269 assert_eq!(n[..2], ["block_place", "swing"], "{n:?}");
2270 placed_tick.get_or_insert(i);
2271 }
2272 }
2273 let _ = b;
2274 let first = results.iter().position(|r| *r).unwrap();
2275 assert!(first > 3, "refused while still turning");
2276 assert_eq!(placed_tick, Some(first));
2277 assert_eq!(e.core.inv.held().map(|s| s.id), Some(1));
2278 }
2279
2280 #[test]
2281 fn sneak_action_order() {
2282 let mut e = engine(GameVersion::V1_8_9);
2283 join(&mut e);
2284 let mut b = Script(|ctx: &mut TickContext<'_>| {
2285 ctx.set_controls(Controls {
2286 sneak: true,
2287 ..Controls::default()
2288 });
2289 });
2290 e.tick(&mut b, Instant::now());
2291 let out = std::mem::take(e.outbound());
2292 assert_eq!(
2293 out[0],
2294 Outbound::EntityAction {
2295 entity_id: 7,
2296 action: EntityAction::StartSneaking
2297 }
2298 );
2299 assert!(matches!(out[1], Outbound::Move(_)));
2300 }
2301
2302 #[test]
2304 fn block_updates_per_version() {
2305 use ghast_protocol::BlockPos;
2306 for v in [GameVersion::V1_8_9, GameVersion::V1_16_5] {
2307 let mut e = engine(v);
2308 join(&mut e);
2309 e.sim.collect_block_updates(true);
2310 let t0 = Instant::now();
2311 e.note_inbound(t0);
2312 let raws = match v {
2313 GameVersion::V1_8_9 => {
2314 use ghast_protocol::v1_8::play::clientbound as cb;
2315 vec![
2316 RawPacket::from_packet(&cb::BlockChange {
2317 location: BlockPos::new(3, 63, 4),
2318 r#type: 59 << 4 | 7,
2319 })
2320 .unwrap(),
2321 RawPacket::from_packet(&cb::MultiBlockChange {
2322 chunk_x: 0,
2323 chunk_z: 0,
2324 records: vec![cb::MultiBlockChangeRecords {
2325 horizontal_pos: 5 << 4 | 6,
2326 y: 62,
2327 block_id: 60 << 4 | 7,
2328 }],
2329 })
2330 .unwrap(),
2331 ]
2332 }
2333 _ => {
2334 use ghast_protocol::v1_16_5::play::clientbound as cb;
2335 let local = (5 << 8 | 6 << 4 | 14) as i64;
2337 vec![
2338 RawPacket::from_packet(&cb::BlockChange {
2339 location: BlockPos::new(3, 63, 4),
2340 r#type: 1,
2341 })
2342 .unwrap(),
2343 RawPacket::from_packet(&cb::MultiBlockChange {
2344 chunk_coordinates: cb::MultiBlockChangeChunkCoordinates {
2345 x: 0,
2346 z: 0,
2347 y: 0xF_FFFF,
2348 },
2349 not_trust_edges: false,
2350 records: vec![9 << 12 | local],
2351 })
2352 .unwrap(),
2353 ]
2354 }
2355 };
2356 for raw in &raws {
2357 e.sim.world_packet(raw).unwrap();
2358 }
2359 let mut seen = Vec::new();
2360 let mut b = Script(|ctx: &mut TickContext<'_>| {
2361 seen.push(ctx.events().block_updates.clone());
2362 });
2363 e.tick(&mut b, t0);
2364 e.tick(&mut b, t0);
2365 let _ = b;
2366 let want = match v {
2367 GameVersion::V1_8_9 => vec![
2368 (BlockPos::new(3, 63, 4), 59 << 4 | 7),
2369 (BlockPos::new(5, 62, 6), 60 << 4 | 7),
2370 ],
2371 _ => vec![(BlockPos::new(3, 63, 4), 1), (BlockPos::new(5, -2, 6), 9)],
2372 };
2373 assert_eq!(seen, [want, Vec::new()], "{v:?}");
2374
2375 e.sim.collect_block_updates(false);
2377 e.sim.world_packet(&raws[0]).unwrap();
2378 let mut n = usize::MAX;
2379 let mut b = Script(|ctx: &mut TickContext<'_>| n = ctx.events().block_updates.len());
2380 e.tick(&mut b, t0);
2381 let _ = b;
2382 assert_eq!(n, 0);
2383 }
2384 }
2385
2386 #[test]
2388 fn set_flying_needs_may_fly() {
2389 for v in [GameVersion::V1_8_9, GameVersion::V1_16_5] {
2390 let mut e = engine(v);
2391 join(&mut e);
2392 let t0 = Instant::now();
2393 e.note_inbound(t0);
2394 e.handle(Inbound::Teleport(Teleport {
2396 x: 8.5,
2397 y: 70.0,
2398 z: 8.5,
2399 yaw: 90.0,
2400 pitch: 0.0,
2401 flags: 0,
2402 teleport_id: Some(2),
2403 }));
2404 e.outbound().clear();
2405 let mut b = Script(|ctx: &mut TickContext<'_>| ctx.set_flying(true));
2406 e.tick(&mut b, t0);
2407 assert!(!names(e.outbound()).contains(&"abilities"), "{v:?}");
2408 assert!(!e.core.pose.flying);
2409 e.handle(Inbound::Abilities(Abilities {
2410 flags: 4,
2411 ..Abilities::default()
2412 }));
2413 e.tick(&mut b, t0);
2414 let out = std::mem::take(e.outbound());
2415 assert_eq!(
2416 out[0],
2417 Outbound::Abilities {
2418 flags: 6,
2419 flying_speed: 0.05,
2420 walking_speed: 0.1
2421 },
2422 "{v:?}"
2423 );
2424 assert!(matches!(out[1], Outbound::Move(_)), "{out:?}");
2425 assert!(e.core.pose.flying && e.core.sent.flying);
2426 e.tick(&mut b, t0);
2428 assert!(!names(e.outbound()).contains(&"abilities"));
2429 let _ = b;
2430 }
2431 }
2432
2433 #[test]
2435 fn finished_behaviour_is_not_ticked() {
2436 struct Three(std::rc::Rc<std::cell::Cell<u32>>);
2437 impl Behavior for Three {
2438 fn tick(&mut self, _ctx: &mut TickContext<'_>) {
2439 self.0.set(self.0.get() + 1);
2440 }
2441 fn running(&self) -> bool {
2442 self.0.get() < 3
2443 }
2444 }
2445 let ticks = std::rc::Rc::new(std::cell::Cell::new(0));
2446 let mut e = engine(GameVersion::V1_8_9);
2447 join(&mut e);
2448 let t0 = Instant::now();
2449 e.note_inbound(t0);
2450 let mut b: Box<dyn Behavior> = Box::new(Three(ticks.clone()));
2451 for _ in 0..10 {
2452 e.tick(&mut b, t0);
2453 let out = std::mem::take(e.outbound());
2455 assert!(
2456 out.iter().any(|o| matches!(o, Outbound::Move(_))),
2457 "{out:?}"
2458 );
2459 }
2460 assert_eq!(
2461 ticks.get(),
2462 3,
2463 "not ticked after running() == false (through the Box)"
2464 );
2465 }
2466
2467 #[test]
2469 fn turn_held_on_a_place_tick() {
2470 let mut e = engine(GameVersion::V1_8_9);
2471 join(&mut e);
2472 let t0 = Instant::now();
2473 e.note_inbound(t0);
2474 e.core
2475 .inv
2476 .on_set_slot(0, 36, Some(ghast_inventory::Stack::new(1, 64, 0)));
2477 let floor = ghast_protocol::BlockPos::new(8, 63, 8);
2478 let mut tick = 0;
2479 let mut placed_at = None;
2480 let mut pitches = Vec::new();
2481 let mut b = Script(|ctx: &mut TickContext<'_>| {
2482 tick += 1;
2483 if placed_at.is_none() {
2484 ctx.set_aim(90.0, 89.5);
2485 if ctx.place(floor, crate::Face::Up, Vec3::new(8.5, 64.0, 8.5)) {
2486 placed_at = Some(tick);
2487 ctx.set_aim(90.0, -60.0);
2489 }
2490 } else {
2491 ctx.set_aim(90.0, -60.0);
2492 }
2493 pitches.push(ctx.pose().pitch);
2494 });
2495 for _ in 0..30 {
2496 e.tick(&mut b, t0);
2497 e.outbound().clear();
2498 }
2499 let _ = b;
2500 let p = placed_at.expect("placed");
2501 assert_eq!(e.core.held_turns, 1);
2502 assert_eq!(pitches[p], pitches[p - 1], "{pitches:?}");
2504 assert!(pitches[p + 1] < pitches[p], "{pitches:?}");
2505 }
2506
2507 #[test]
2509 fn click_ticket_matches_the_confirm() {
2510 let mut e = engine(GameVersion::V1_8_9);
2511 join(&mut e);
2512 let t0 = Instant::now();
2513 e.note_inbound(t0);
2514 e.core
2515 .inv
2516 .on_set_slot(0, 36, Some(ghast_inventory::Stack::new(1, 64, 0)));
2517 let mut ticket = None;
2518 let mut n = 0;
2519 let mut b = Script(|ctx: &mut TickContext<'_>| {
2520 n += 1;
2521 if n == 1 {
2522 ctx.open_inventory().unwrap();
2523 } else if ticket.is_none() {
2524 ticket = ctx
2525 .click(ghast_inventory::Click::Pickup {
2526 slot: 36,
2527 right: false,
2528 })
2529 .ok();
2530 }
2531 });
2532 for _ in 0..3 {
2533 e.tick(&mut b, t0);
2534 }
2535 let _ = b;
2536 let t = ticket.expect("click sent");
2537 assert_eq!(t.window_id, 0);
2538 assert!(t.matches(&ghast_inventory::ClickResult {
2539 window_id: 0,
2540 action: t.action,
2541 outcome: ghast_inventory::ClickOutcome::Accepted,
2542 }));
2543 }
2544
2545 #[test]
2548 fn planting_uses_the_seed_at_once() {
2549 use ghast_protocol::v1_8::play::clientbound as cb;
2550 for (soil, item, used) in [
2551 (60 << 4 | 7, 295, true),
2552 (1 << 4, 295, false),
2553 (60 << 4, 1, false),
2554 ] {
2555 let mut e = engine(GameVersion::V1_8_9);
2556 join(&mut e);
2557 let t0 = Instant::now();
2558 e.note_inbound(t0);
2559 let floor = ghast_protocol::BlockPos::new(8, 63, 8);
2560 let raw = RawPacket::from_packet(&cb::BlockChange {
2561 location: floor,
2562 r#type: soil,
2563 })
2564 .unwrap();
2565 e.sim.world_packet(&raw).unwrap();
2566 e.core
2567 .inv
2568 .on_set_slot(0, 36, Some(ghast_inventory::Stack::new(item, 64, 0)));
2569 let mut done = false;
2570 let mut b = Script(|ctx: &mut TickContext<'_>| {
2571 ctx.set_aim(90.0, 89.5);
2572 if !done {
2573 done = ctx.place(floor, crate::Face::Up, Vec3::new(8.5, 64.0, 8.5));
2574 }
2575 });
2576 let mut sent = None;
2577 for _ in 0..20 {
2578 e.tick(&mut b, t0);
2579 for o in e.outbound().drain(..) {
2580 if let Outbound::Place { held, .. } = o {
2581 sent = held.map(|h| h.count);
2582 }
2583 }
2584 }
2585 let _ = b;
2586 assert_eq!(sent, Some(64), "the packet carries the stack before use");
2587 let left = e.core.inv.held().map(|s| s.count);
2588 assert_eq!(
2589 left,
2590 Some(if used { 63 } else { 64 }),
2591 "soil {soil} item {item}"
2592 );
2593 }
2594 }
2595
2596 #[test]
2599 fn planned_look_is_the_next_look() {
2600 let mut e = engine(GameVersion::V1_8_9);
2601 join(&mut e);
2602 let t0 = Instant::now();
2603 e.note_inbound(t0);
2604 e.core
2605 .inv
2606 .on_set_slot(0, 36, Some(ghast_inventory::Stack::new(1, 64, 0)));
2607 let floor = ghast_protocol::BlockPos::new(8, 63, 8);
2608 let mut n = 0;
2609 let mut placed = false;
2610 let mut plans: Vec<(f32, f32)> = Vec::new();
2611 let mut b = Script(|ctx: &mut TickContext<'_>| {
2612 n += 1;
2613 if n % 5 == 0 {
2614 } else if !placed {
2616 ctx.set_aim(90.0, 89.5);
2617 if ctx.place(floor, crate::Face::Up, Vec3::new(8.5, 64.0, 8.5)) {
2618 placed = true;
2619 ctx.set_aim(90.0, -60.0);
2620 }
2621 } else {
2622 ctx.set_aim(150.0, -60.0);
2623 }
2624 let p = ctx.planned_look();
2626 assert_eq!(p, ctx.planned_look());
2627 plans.push(p);
2628 });
2629 let mut looks = Vec::new();
2630 for _ in 0..40 {
2631 e.tick(&mut b, t0);
2632 e.outbound().clear();
2633 looks.push((e.core.pose.yaw, e.core.pose.pitch));
2634 }
2635 let _ = b;
2636 assert!(placed);
2637 assert_eq!(e.core.test_held_turns(), 1);
2638 assert_eq!(plans, looks);
2639 }
2640
2641 #[test]
2644 fn teleport_event_has_the_sent_pose_before() {
2645 for v in [GameVersion::V1_8_9, GameVersion::V1_16_5] {
2646 let mut e = engine(v);
2647 join(&mut e);
2648 let t0 = Instant::now();
2649 e.note_inbound(t0);
2650 let mut seen = Vec::new();
2651 let mut b = Script(|ctx: &mut TickContext<'_>| {
2652 seen.extend(ctx.events().teleports.iter().copied());
2653 });
2654 e.tick(&mut b, t0);
2655 let sent = e.core.sent;
2656 let tp = Teleport {
2658 x: 10.5,
2659 y: 64.0,
2660 z: 8.5,
2661 yaw: 30.0,
2662 pitch: 0.0,
2663 flags: crate::common::teleport_flags::YAW,
2664 teleport_id: Some(9),
2665 };
2666 e.handle(Inbound::Teleport(tp));
2667 e.tick(&mut b, t0);
2668 e.tick(&mut b, t0);
2669 let _ = b;
2670 assert_eq!(seen.len(), 2, "{v:?}");
2672 let ev = seen[1];
2673 assert_eq!(ev.teleport, tp);
2674 assert_eq!(ev.sent_before, sent);
2675 assert_eq!(ev.after.pos, Vec3::new(10.5, 64.0, 8.5));
2676 assert_eq!(ev.after.yaw, sent.yaw + 30.0, "{v:?}");
2677 }
2678 }
2679
2680 #[test]
2684 fn closed_window_frees_the_buttons() {
2685 for v in [GameVersion::V1_8_9, GameVersion::V1_16_5] {
2686 let mut e = engine(v);
2687 join(&mut e);
2688 let t0 = Instant::now();
2689 e.note_inbound(t0);
2690 if v == GameVersion::V1_16_5 {
2691 use ghast_protocol::v1_16_5::play::clientbound as cb;
2693 let raw = RawPacket::from_packet(&cb::BlockChange {
2694 location: ghast_protocol::BlockPos::new(8, 63, 8),
2695 r#type: 1,
2696 })
2697 .unwrap();
2698 e.sim.world_packet(&raw).unwrap();
2699 }
2700 e.core.inv.on_open_window(
2701 5,
2702 Some(ghast_inventory::WindowKind::Chest { rows: 3 }),
2703 "Vault".into(),
2704 );
2705 e.handle(Inbound::OpenWindow(5));
2706 e.core
2709 .inv
2710 .on_set_slot(-1, -1, Some(ghast_inventory::Stack::new(1, 3, 0)));
2711 let mut n = 0;
2712 let mut closed_at = None;
2713 let mut b = Script(|ctx: &mut TickContext<'_>| {
2714 n += 1;
2715 ctx.set_aim(90.0, 89.5);
2716 ctx.set_buttons(Buttons {
2717 left: true,
2718 right: false,
2719 });
2720 if n == 15 {
2721 assert!(ctx.inventory().cursor().is_some());
2722 ctx.close_window().unwrap();
2723 assert_eq!(ctx.open_window(), None);
2724 let inv = ctx.inventory();
2725 assert!(!inv.gui_open() && inv.window().is_none() && inv.cursor().is_none());
2726 closed_at = Some(n);
2727 }
2728 });
2729 let mut first_dig = None;
2730 for i in 1..=30 {
2731 e.tick(&mut b, t0);
2732 let out = std::mem::take(e.outbound());
2733 if first_dig.is_none()
2734 && out.iter().any(|o| {
2735 matches!(
2736 o,
2737 Outbound::Dig {
2738 status: crate::common::DigStatus::Start,
2739 ..
2740 }
2741 )
2742 })
2743 {
2744 first_dig = Some(i);
2745 }
2746 }
2747 let _ = b;
2748 assert_eq!(closed_at, Some(15));
2749 let d = first_dig.unwrap_or_else(|| panic!("{v:?}: no dig after the close"));
2750 assert!(d >= 15, "{v:?}: dug at tick {d} with the window open");
2752 }
2753 }
2754
2755 #[test]
2757 fn swings_count_every_arm_swing() {
2758 let mut e = engine(GameVersion::V1_8_9);
2759 join(&mut e);
2760 let t0 = Instant::now();
2761 e.note_inbound(t0);
2762 let mut n = 0;
2763 let mut seen = Vec::new();
2764 let mut b = Script(|ctx: &mut TickContext<'_>| {
2765 n += 1;
2766 ctx.set_aim(90.0, 89.5);
2767 if n == 1 {
2768 ctx.swing();
2769 ctx.swing();
2770 }
2771 if n == 20 {
2772 ctx.set_buttons(Buttons {
2774 left: true,
2775 right: false,
2776 });
2777 }
2778 seen.push(ctx.swings());
2779 });
2780 let mut sent = 0u64;
2781 for _ in 0..30 {
2782 e.tick(&mut b, t0);
2783 sent += count_swings(&std::mem::take(e.outbound()));
2784 }
2785 let _ = b;
2786 assert_eq!(seen[0], 2, "this tick's own swings count at once");
2787 assert!(sent > 10, "the held button swings: {sent}");
2788 assert_eq!(*seen.last().unwrap() + 1, sent);
2790 assert_eq!(e.core.swings, sent);
2791 }
2792
2793 #[test]
2795 fn column_view_and_world_config() {
2796 for config in [
2797 None,
2798 Some(
2799 ghast_world::WorldConfig::default()
2800 .with_biomes(true)
2801 .with_light(true),
2802 ),
2803 ] {
2804 let sim = crate::sim::Sim18::with(&crate::sim::SimOptions {
2805 world: config,
2806 ..crate::sim::SimOptions::default()
2807 });
2808 let mut e = Engine::new(opts(GameVersion::V1_8_9), Box::new(sim) as Box<dyn Sim>);
2809 join(&mut e);
2810 let w: &dyn WorldView = &e.sim;
2811 let col = w.column(0, 0).expect("loaded");
2812 assert_eq!(u32::from(col.raw(8, 63, 8)), w.raw_block(8, 63, 8).unwrap());
2813 assert_eq!(col.raw(8, 63, 8), 1 << 4);
2814 assert!(w.column(5, 5).is_none());
2815 let light = col.sky_light(8, 63, 8);
2816 assert_eq!(light.is_some(), config.is_some(), "{config:?}");
2817 let biomes = !matches!(col.biomes(), ghast_world::Biomes::None);
2818 assert_eq!(biomes, config.is_some(), "{config:?}");
2819 }
2820 }
2821
2822 #[test]
2825 fn after_tick_sees_the_sent_pose() {
2826 struct Obs {
2827 n: u32,
2828 views: Vec<(u64, Pose, u64)>,
2829 }
2830 impl Behavior for Obs {
2831 fn tick(&mut self, ctx: &mut TickContext<'_>) {
2832 self.n += 1;
2833 ctx.set_aim(150.0, 10.0);
2834 ctx.set_controls(Controls {
2835 forward: true,
2836 ..Controls::default()
2837 });
2838 }
2839 fn running(&self) -> bool {
2840 self.n < 5
2841 }
2842 fn after_tick(&mut self, view: &TickView<'_>) {
2843 self.views.push((view.tick(), *view.sent(), view.swings()));
2844 assert!(view.world().is_column_loaded(0, 0));
2845 assert_eq!(view.version(), GameVersion::V1_8_9);
2846 }
2847 }
2848 let mut e = engine(GameVersion::V1_8_9);
2849 join(&mut e);
2850 let t0 = Instant::now();
2851 e.note_inbound(t0);
2852 let mut b = Obs {
2853 n: 0,
2854 views: Vec::new(),
2855 };
2856 let mut sent = Vec::new();
2857 for _ in 0..10 {
2858 e.tick(&mut b, t0);
2859 e.outbound().clear();
2860 sent.push(e.core.sent);
2861 }
2862 assert_eq!(b.n, 5);
2863 assert_eq!(b.views.len(), 10, "also after the behaviour finished");
2864 for (i, (_, p, _)) in b.views.iter().enumerate() {
2865 assert_eq!(*p, sent[i]);
2866 }
2867 assert_ne!(
2868 b.views[0].1.yaw, b.views[4].1.yaw,
2869 "the view follows the turn"
2870 );
2871 }
2872
2873 #[test]
2876 fn block_entities_follow_the_world_config() {
2877 use ghast_protocol::v1_8::play::clientbound as cb;
2878 for on in [false, true] {
2879 let sim = crate::sim::Sim18::with(&crate::sim::SimOptions {
2880 world: on.then(|| ghast_world::WorldConfig::default().with_block_entities(true)),
2881 ..crate::sim::SimOptions::default()
2882 });
2883 assert_eq!(sim.world_packet_ids().contains(&cb::id::UPDATE_SIGN), on);
2884 let mut e = Engine::new(opts(GameVersion::V1_8_9), Box::new(sim) as Box<dyn Sim>);
2885 join(&mut e);
2886 let raw = RawPacket::from_packet(&cb::UpdateSign {
2887 location: ghast_protocol::BlockPos::new(3, 64, 4),
2888 text1: "\"Hello\"".into(),
2889 text2: "\"\"".into(),
2890 text3: "\"\"".into(),
2891 text4: "\"\"".into(),
2892 })
2893 .unwrap();
2894 e.sim.world_packet(&raw).unwrap();
2895 let w: &dyn WorldView = &e.sim;
2896 let be = w.column(0, 0).unwrap().block_entity(3, 64, 4);
2897 assert_eq!(be.is_some(), on, "{be:?}");
2898 }
2899 }
2900}