1use ghast_protocol::{BlockPos, DecodeError, RawPacket};
9use ghast_world::{WorldConfig, WorldError};
10
11use crate::common::{
12 Abilities, Controls, EntityAction, MovePacket, Outbound, Pose, Teleport, Vec3,
13};
14use crate::hands::{BlockQuery, HeldItem};
15
16#[derive(Debug)]
18pub(crate) enum SimError {
19 Decode(DecodeError),
21 World(WorldError),
23}
24
25impl From<DecodeError> for SimError {
26 fn from(e: DecodeError) -> Self {
27 SimError::Decode(e)
28 }
29}
30
31impl From<WorldError> for SimError {
32 fn from(e: WorldError) -> Self {
33 SimError::World(e)
34 }
35}
36
37pub trait WorldView {
39 fn is_column_loaded(&self, chunk_x: i32, chunk_z: i32) -> bool;
41
42 fn raw_block(&self, x: i32, y: i32, z: i32) -> Option<u32>;
46
47 fn loaded_columns(&self) -> usize;
49
50 fn entities(&self) -> Option<&ghast_entity::Entities>;
52
53 fn block_query(&self, x: i32, y: i32, z: i32) -> Option<BlockQuery>;
56
57 fn column(&self, chunk_x: i32, chunk_z: i32) -> Option<&ghast_world::Column> {
61 let _ = (chunk_x, chunk_z);
62 None
63 }
64}
65
66#[derive(Debug, Default)]
69struct SessionPackets {
70 login: Option<RawPacket>,
71 respawn: Option<RawPacket>,
72}
73
74impl SessionPackets {
75 fn note(&mut self, raw: &RawPacket, login: i32, respawn: i32) {
76 if raw.id == login {
77 self.login = Some(raw.clone());
78 self.respawn = None;
79 } else if raw.id == respawn {
80 self.respawn = Some(raw.clone());
81 }
82 }
83
84 fn replay(&self, mut apply: impl FnMut(&RawPacket)) {
86 for r in [&self.login, &self.respawn].into_iter().flatten() {
87 apply(r);
88 }
89 }
90}
91
92const MAX_BLOCK_UPDATES: usize = 4096;
94
95#[derive(Debug, Default)]
97struct BlockUpdates {
98 on: bool,
99 list: Vec<(BlockPos, u32)>,
100 dropped: usize,
101}
102
103impl BlockUpdates {
104 fn push(&mut self, pos: BlockPos, state: u32) {
105 if self.on {
106 if self.list.len() < MAX_BLOCK_UPDATES {
107 self.list.push((pos, state));
108 } else {
109 self.dropped += 1;
110 }
111 }
112 }
113}
114
115pub(crate) trait Sim: WorldView + std::fmt::Debug {
116 fn world_packet(&mut self, raw: &RawPacket) -> Result<(), SimError>;
119 fn reset_player(&mut self);
121 fn teleport(&mut self, pose: &mut Pose, t: &Teleport);
123 fn abilities(&mut self, a: Abilities);
125 fn food(&mut self, food: i32);
127 fn velocity(&mut self, v: Vec3, add: bool);
130 fn movement_speed(&mut self, base: f64, modifiers: &[(u128, f64, u8)]);
134 fn status_effects(&mut self, effects: &std::collections::BTreeMap<i8, i8>);
136 fn boots(&mut self, depth_strider: u8, soul_speed: u8);
138 fn spectator(&mut self, on: bool);
140 fn step(
143 &mut self,
144 pose: &mut Pose,
145 controls: Controls,
146 using_item: bool,
147 entity_id: i32,
148 out: &mut Vec<Outbound>,
149 );
150 fn entity_packet_ids(&self) -> Vec<i32>;
152 fn world_packet_ids(&self) -> Vec<i32>;
154 fn entity_config(&self) -> Option<ghast_entity::Config>;
156 fn set_entities(&mut self, config: Option<ghast_entity::Config>);
159 fn set_flying(&mut self, flying: bool, out: &mut Vec<Outbound>);
162 fn collect_block_updates(&mut self, on: bool);
164 fn drain_block_updates(&mut self, out: &mut Vec<(BlockPos, u32)>) -> usize;
167 fn predict_air(&mut self, x: i32, y: i32, z: i32);
169 fn held_item(&self, id: u16, count: u8, efficiency: u8) -> HeldItem;
171}
172
173#[derive(Debug, Clone, Default)]
175pub(crate) struct SimOptions {
176 pub record: Option<std::path::PathBuf>,
178 pub force_step_position: ForceStepPosition,
181 pub entities: Option<ghast_entity::Config>,
183 pub world: Option<WorldConfig>,
185}
186
187#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
190#[non_exhaustive]
191pub enum ForceStepPosition {
192 #[default]
194 Off,
195 Always,
197 StepUpOnly,
200}
201
202fn force_position(
206 packets: &mut ghast_physics::Packets,
207 cur: [f64; 3],
208 sent_pos: &mut [f64; 3],
209 ticks_since_pos: &mut i32,
210 mode: ForceStepPosition,
211) {
212 use ghast_physics::Packet as P;
213 if mode == ForceStepPosition::Off || cur[1] == sent_pos[1] {
214 return;
215 }
216 let Some(last) = packets.as_slice().last().copied() else {
217 return;
218 };
219 if mode == ForceStepPosition::StepUpOnly {
220 let on_ground = matches!(
221 last,
222 P::Flying { on_ground: true }
223 | P::Look {
224 on_ground: true,
225 ..
226 }
227 );
228 if !on_ground || cur[1] <= sent_pos[1] {
229 return;
230 }
231 }
232 let new = match last {
233 P::Flying { on_ground } => P::Position {
234 x: cur[0],
235 y: cur[1],
236 z: cur[2],
237 on_ground,
238 },
239 P::Look {
240 yaw,
241 pitch,
242 on_ground,
243 } => P::PositionLook {
244 x: cur[0],
245 y: cur[1],
246 z: cur[2],
247 yaw,
248 pitch,
249 on_ground,
250 },
251 _ => return,
252 };
253 let mut out = ghast_physics::Packets::new();
254 let n = packets.len();
255 for p in &packets.as_slice()[..n - 1] {
256 out.push(*p);
257 }
258 out.push(new);
259 *packets = out;
260 *sent_pos = cur;
261 *ticks_since_pos = 0;
262}
263
264fn activates_1_16(name: &str) -> bool {
267 const EXACT: &[&str] = &[
268 "chest",
269 "trapped_chest",
270 "ender_chest",
271 "crafting_table",
272 "furnace",
273 "blast_furnace",
274 "smoker",
275 "barrel",
276 "anvil",
277 "chipped_anvil",
278 "damaged_anvil",
279 "enchanting_table",
280 "brewing_stand",
281 "hopper",
282 "dispenser",
283 "dropper",
284 "repeater",
285 "comparator",
286 "note_block",
287 "jukebox",
288 "cake",
289 "loom",
290 "stonecutter",
291 "grindstone",
292 "cartography_table",
293 "smithing_table",
294 "lectern",
295 "bell",
296 "beacon",
297 "daylight_detector",
298 "respawn_anchor",
299 "composter",
300 "lever",
301 "command_block",
302 "chain_command_block",
303 "repeating_command_block",
304 "structure_block",
305 "jigsaw",
306 "dragon_egg",
307 "flower_pot",
308 "sweet_berry_bush",
309 ];
310 EXACT.contains(&name)
311 || name.ends_with("_shulker_box")
312 || name == "shulker_box"
313 || name.ends_with("_bed")
314 || name.ends_with("_button")
315 || name.ends_with("_fence_gate")
316 || (name.ends_with("_door") && name != "iron_door")
317 || (name.ends_with("_trapdoor") && name != "iron_trapdoor")
318 || name.starts_with("potted_")
319}
320
321fn union(boxes: &[ghast_data::Aabb]) -> Option<[f64; 6]> {
323 let mut u = [1.0_f64, 1.0, 1.0, 0.0, 0.0, 0.0];
324 for b in boxes {
325 u[0] = u[0].min(b.min_x);
326 u[1] = u[1].min(b.min_y);
327 u[2] = u[2].min(b.min_z);
328 u[3] = u[3].max(b.max_x.min(1.0));
329 u[4] = u[4].max(b.max_y.min(1.0));
330 u[5] = u[5].max(b.max_z.min(1.0));
331 }
332 (!boxes.is_empty()).then_some(u)
333}
334
335const ACTIVATED_1_8: &[u8] = &[
337 23, 25, 26, 54, 58, 61, 62, 64, 69, 71, 77, 84, 92, 93, 94, 96, 107, 116, 117, 118, 120, 130,
338 138, 143, 145, 146, 149, 150, 151, 154, 158, 167, 178, 183, 184, 185, 186, 187, 193, 194, 195,
339 196, 197,
340];
341
342const USABLE_1_8: &[u16] = &[373, 335, 261, 267, 268, 272, 276, 283];
344
345fn arr(b: &ghast_physics::math::Aabb) -> [f64; 6] {
347 [b.min_x, b.min_y, b.min_z, b.max_x, b.max_y, b.max_z]
348}
349
350fn ray_1_8(w: &ghast_world::v1_8::World, x: i32, y: i32, z: i32) -> RayBox {
354 use ghast_physics::v1_8::RayShape;
355 match ghast_physics::v1_8::ray_shape(w, x, y, z, false) {
356 RayShape::None => (None, None),
357 RayShape::Bounds(b) => (Some(arr(&b)), None),
358 RayShape::Stairs(p) => (None, Some(p.boxes().map(|b| b.as_ref().map(arr)))),
359 }
360}
361
362type RayBox = (Option<[f64; 6]>, Option<[Option<[f64; 6]>; 8]>);
364
365fn config() -> WorldConfig {
366 WorldConfig::default()
368 .with_biomes(false)
369 .with_light(false)
370 .with_block_entities(false)
371}
372
373fn abilities_flags(a: &ghast_physics::Abilities) -> i8 {
374 let mut f = 0;
375 if a.invulnerable {
376 f |= 1;
377 }
378 if a.flying {
379 f |= 2;
380 }
381 if a.allow_flying {
382 f |= 4;
383 }
384 if a.creative {
385 f |= 8;
386 }
387 f
388}
389
390pub(crate) struct Sim18 {
394 world: ghast_world::v1_8::World,
395 player: ghast_physics::v1_8::PlayerState,
396 packets: ghast_physics::Packets,
397 rec: Option<crate::record::Recorder>,
398 force_step_position: ForceStepPosition,
399 entities: Option<ghast_entity::v1_8::Tracker>,
400 updates: BlockUpdates,
402 session: SessionPackets,
405 block_entities: bool,
407}
408
409impl std::fmt::Debug for Sim18 {
410 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
411 f.debug_struct("Sim18")
412 .field("world", &self.world)
413 .field("pos", &self.player.pos)
414 .finish_non_exhaustive()
415 }
416}
417
418#[cfg(test)]
419impl Sim18 {
420 pub(crate) fn test_player(&self) -> &ghast_physics::v1_8::PlayerState {
422 &self.player
423 }
424
425 pub(crate) fn test_set_player(&mut self, pose: &mut Pose, p: ghast_physics::v1_8::PlayerState) {
427 self.player = p;
428 sync_pose(pose, &self.player);
429 }
430
431 pub(crate) fn test_world(&self) -> &ghast_world::v1_8::World {
433 &self.world
434 }
435}
436
437impl Sim18 {
438 pub(crate) fn new() -> Self {
439 Sim18 {
440 world: ghast_world::v1_8::World::new(config()),
441 player: ghast_physics::v1_8::PlayerState::new([0.0; 3], 0.0, 0.0),
442 packets: ghast_physics::Packets::new(),
443 rec: None,
444 force_step_position: ForceStepPosition::Off,
445 entities: None,
446 updates: BlockUpdates::default(),
447 session: SessionPackets::default(),
448 block_entities: false,
449 }
450 }
451
452 pub(crate) fn with(opts: &SimOptions) -> Self {
454 let mut s = Self::new();
455 s.force_step_position = opts.force_step_position;
456 s.entities = opts.entities.map(ghast_entity::v1_8::Tracker::new);
457 if let Some(c) = opts.world {
458 s.world = ghast_world::v1_8::World::new(c);
459 s.block_entities = c.block_entities;
460 }
461 if let Some(p) = opts.record.as_deref() {
462 match crate::record::Recorder::create(p) {
463 Ok(r) => s.rec = Some(r),
464 Err(e) => log::warn!("cannot record to {}: {e}", p.display()),
465 }
466 }
467 s
468 }
469}
470
471impl WorldView for Sim18 {
472 fn column(&self, chunk_x: i32, chunk_z: i32) -> Option<&ghast_world::Column> {
473 self.world.column(chunk_x, chunk_z)
474 }
475
476 fn is_column_loaded(&self, chunk_x: i32, chunk_z: i32) -> bool {
477 self.world.is_column_loaded(chunk_x, chunk_z)
478 }
479
480 fn raw_block(&self, x: i32, y: i32, z: i32) -> Option<u32> {
481 self.world.block_state(x, y, z).map(|s| u32::from(s.raw()))
482 }
483
484 fn entities(&self) -> Option<&ghast_entity::Entities> {
485 self.entities.as_deref()
486 }
487
488 fn loaded_columns(&self) -> usize {
489 self.world.columns().len()
490 }
491
492 fn block_query(&self, x: i32, y: i32, z: i32) -> Option<BlockQuery> {
493 let s = self.world.block_state(x, y, z)?;
494 let b = s.block();
495 let info = b.info();
496 let ray = ray_1_8(&self.world, x, y, z);
497 Some(BlockQuery {
498 air: s.is_air(),
499 water: matches!(b.id(), 8 | 9),
500 block: u16::from(b.id()),
501 hardness: info.hardness.unwrap_or(-1.0),
502 dig_material: info.material,
503 harvest_tools: info.harvest_tools,
504 selection: ray.0,
505 stairs: ray.1,
506 activates: ACTIVATED_1_8.contains(&b.id()),
507 })
508 }
509}
510
511impl Sim for Sim18 {
512 fn world_packet(&mut self, raw: &RawPacket) -> Result<(), SimError> {
513 {
514 use ghast_protocol::v1_8::play::clientbound::id;
515 self.session.note(raw, id::LOGIN, id::RESPAWN);
516 }
517 if let Some(t) = &mut self.entities
518 && t.wants(raw.id)
519 {
520 t.apply(raw)?;
521 }
522 use ghast_protocol::v1_8::play::clientbound as cb;
523 let w = &mut self.world;
524 match raw.id {
525 cb::id::LOGIN => {
526 w.apply_login(&raw.decode_as()?);
527 }
528 cb::id::RESPAWN => {
529 w.apply_respawn(&raw.decode_as()?);
530 }
531 cb::id::MAP_CHUNK => {
532 w.apply_map_chunk(&raw.decode_as()?)?;
533 }
534 cb::id::MAP_CHUNK_BULK => {
535 w.apply_map_chunk_bulk(&raw.decode_as()?)?;
536 }
537 cb::id::BLOCK_CHANGE => {
538 let p: cb::BlockChange = raw.decode_as()?;
539 self.updates.push(p.location, p.r#type as u32);
540 w.apply_block_change(&p);
541 }
542 cb::id::MULTI_BLOCK_CHANGE => {
543 let p: cb::MultiBlockChange = raw.decode_as()?;
544 for r in &p.records {
545 let x = p.chunk_x * 16 + i32::from(r.horizontal_pos >> 4);
546 let z = p.chunk_z * 16 + i32::from(r.horizontal_pos & 15);
547 self.updates
548 .push(BlockPos::new(x, i32::from(r.y), z), r.block_id as u32);
549 }
550 w.apply_multi_block_change(&p);
551 }
552 cb::id::EXPLOSION => {
553 w.apply_explosion(&raw.decode_as()?);
554 }
555 cb::id::WORLD_BORDER => {
556 w.apply_world_border(&raw.decode_as()?);
557 }
558 cb::id::UPDATE_SIGN if self.block_entities => {
559 w.apply_update_sign(&raw.decode_as()?);
560 }
561 cb::id::TILE_ENTITY_DATA if self.block_entities => {
562 w.apply_tile_entity_data(&raw.decode_as()?);
563 }
564 _ => {}
565 }
566 Ok(())
567 }
568
569 fn reset_player(&mut self) {
570 let abilities = self.player.abilities;
573 let food = self.player.food_level;
574 self.player = ghast_physics::v1_8::PlayerState::new([0.0; 3], 0.0, 0.0);
575 self.player.abilities = abilities;
576 self.player.food_level = food;
577 }
578
579 fn teleport(&mut self, pose: &mut Pose, t: &Teleport) {
580 use crate::common::teleport_flags as fl;
581 let rel = [
582 t.flags & fl::X != 0,
583 t.flags & fl::Y != 0,
584 t.flags & fl::Z != 0,
585 t.flags & fl::YAW != 0,
586 t.flags & fl::PITCH != 0,
587 ];
588 self.player.yaw = pose.yaw;
590 self.player.pitch = pose.pitch;
591 let _reply = self
592 .player
593 .apply_teleport([t.x, t.y, t.z], t.yaw, t.pitch, rel);
594 if let Some(r) = &mut self.rec {
595 let p = &self.player;
596 r.teleport(
597 [t.x, t.y, t.z],
598 t.yaw,
599 t.pitch,
600 t.flags,
601 t.teleport_id,
602 [p.pos[0], p.bb.min_y, p.pos[2]],
603 p.yaw,
604 p.pitch,
605 );
606 }
607 sync_pose(pose, &self.player);
608 }
609
610 fn abilities(&mut self, a: Abilities) {
611 let a = ghast_physics::Abilities {
612 invulnerable: a.flags & 1 != 0,
613 flying: a.flags & 2 != 0,
614 allow_flying: a.flags & 4 != 0,
615 creative: a.flags & 8 != 0,
616 fly_speed: a.flying_speed,
617 walk_speed: a.walking_speed,
618 };
619 if let Some(r) = &mut self.rec {
620 r.abilities(&a);
621 }
622 self.player.apply_abilities(a);
623 }
624
625 fn food(&mut self, food: i32) {
626 self.player.food_level = food;
627 }
628
629 fn velocity(&mut self, v: Vec3, add: bool) {
630 if let Some(r) = &mut self.rec {
631 if add {
632 r.note("explosion push not representable");
633 } else {
634 r.velocity([
635 (v.x * 8000.0).round() as i32,
636 (v.y * 8000.0).round() as i32,
637 (v.z * 8000.0).round() as i32,
638 ]);
639 }
640 }
641 let p = &mut self.player;
642 if add {
643 p.vel[0] += v.x;
644 p.vel[1] += v.y;
645 p.vel[2] += v.z;
646 } else {
647 p.vel = [v.x, v.y, v.z];
648 }
649 }
650
651 fn movement_speed(&mut self, base: f64, modifiers: &[(u128, f64, u8)]) {
652 if let Some(r) = &mut self.rec {
653 r.speed_attribute(base, modifiers);
654 }
655 self.player.apply_speed_attribute(base, modifiers);
656 }
657
658 fn status_effects(&mut self, effects: &std::collections::BTreeMap<i8, i8>) {
659 if let Some(r) = &mut self.rec {
660 r.effects(effects);
661 }
662 let e = &mut self.player.effects;
663 e.jump_boost = effects.get(&8).map(|a| i32::from(*a));
664 e.blindness = effects.contains_key(&15);
665 }
666
667 fn boots(&mut self, depth_strider: u8, _soul_speed: u8) {
668 self.player.depth_strider = depth_strider;
670 }
671
672 fn spectator(&mut self, on: bool) {
673 self.player.spectator = on;
674 }
675
676 fn set_flying(&mut self, flying: bool, out: &mut Vec<Outbound>) {
677 let a = &mut self.player.abilities;
678 if a.flying == flying || (flying && !a.allow_flying) {
679 return;
680 }
681 a.flying = flying;
682 let a = *a;
683 if let Some(r) = &mut self.rec {
684 r.abilities(&a);
685 }
686 out.push(Outbound::Abilities {
687 flags: abilities_flags(&a),
688 flying_speed: a.fly_speed,
689 walking_speed: a.walk_speed,
690 });
691 }
692
693 fn collect_block_updates(&mut self, on: bool) {
694 self.updates.on = on;
695 if !on {
696 self.updates.list.clear();
697 }
698 }
699
700 fn drain_block_updates(&mut self, out: &mut Vec<(BlockPos, u32)>) -> usize {
701 out.append(&mut self.updates.list);
702 std::mem::take(&mut self.updates.dropped)
703 }
704
705 fn world_packet_ids(&self) -> Vec<i32> {
706 self.world.packet_ids().collect()
707 }
708
709 fn entity_config(&self) -> Option<ghast_entity::Config> {
710 self.entities.as_ref().map(|t| t.config())
711 }
712
713 fn set_entities(&mut self, config: Option<ghast_entity::Config>) {
714 if self.entity_config() == config {
715 return;
716 }
717 self.entities = config.map(|c| {
718 let mut t = ghast_entity::v1_8::Tracker::new(c);
719 self.session.replay(|r| {
720 if let Err(e) = t.apply(r) {
721 log::debug!("entity tracker: session packet 0x{:02x}: {e}", r.id);
722 }
723 });
724 t
725 });
726 }
727
728 fn entity_packet_ids(&self) -> Vec<i32> {
729 self.entities
730 .as_ref()
731 .map(|t| t.packet_ids().collect())
732 .unwrap_or_default()
733 }
734
735 fn predict_air(&mut self, x: i32, y: i32, z: i32) {
736 self.world
737 .set_block_state(x, y, z, ghast_data::v1_8::BlockState::AIR);
738 }
739
740 fn held_item(&self, id: u16, count: u8, efficiency: u8) -> HeldItem {
741 HeldItem {
742 id,
743 count,
744 efficiency,
745 places_block: id < 256,
746 usable: ghast_data::v1_8::food(id).is_some() || USABLE_1_8.contains(&id),
747 food: ghast_data::v1_8::food(id).is_some(),
748 always_edible: id == 322,
750 tool_speed: Some(ghast_data::v1_8::tool_multiplier),
751 }
752 }
753
754 fn step(
755 &mut self,
756 pose: &mut Pose,
757 controls: Controls,
758 using_item: bool,
759 entity_id: i32,
760 out: &mut Vec<Outbound>,
761 ) {
762 let input = ghast_physics::TickInput {
763 keys: keys(controls),
764 yaw: pose.yaw,
765 pitch: pose.pitch,
766 using_item,
767 };
768 self.packets.clear();
769 if let Some(r) = &mut self.rec
770 && !r.started()
771 {
772 r.start_1_8(&self.player);
773 }
774 if let Some(r) = &mut self.rec {
775 let w = &self.world;
776 r.observe(self.player.pos, |x, y, z| {
777 w.is_column_loaded(x >> 4, z >> 4)
778 .then(|| w.block_state(x, y, z).map_or(0, |s| u32::from(s.raw())))
779 });
780 }
781 ghast_physics::v1_8::tick(&mut self.player, &input, &self.world, &mut self.packets);
782 if self.force_step_position != ForceStepPosition::Off {
783 let p = &mut self.player;
784 let cur = [p.pos[0], p.bb.min_y, p.pos[2]];
785 force_position(
786 &mut self.packets,
787 cur,
788 &mut p.sent.pos,
789 &mut p.sent.ticks_since_pos,
790 self.force_step_position,
791 );
792 }
793 if let Some(r) = &mut self.rec {
794 r.tick(
795 &input,
796 &crate::record::state_1_8(&self.player),
797 &self.packets,
798 );
799 }
800 push_packets(&self.packets, entity_id, out);
801 sync_pose(pose, &self.player);
802 self.world.advance_time(50);
804 }
805}
806
807fn push_packets(packets: &ghast_physics::Packets, entity_id: i32, out: &mut Vec<Outbound>) {
809 for p in packets {
810 use ghast_physics::Packet as P;
811 let action = |action| Outbound::EntityAction { entity_id, action };
812 out.push(match *p {
813 P::StartSneaking => action(EntityAction::StartSneaking),
814 P::StopSneaking => action(EntityAction::StopSneaking),
815 P::StartSprinting => action(EntityAction::StartSprinting),
816 P::StopSprinting => action(EntityAction::StopSprinting),
817 P::Abilities(a) => Outbound::Abilities {
818 flags: abilities_flags(&a),
819 flying_speed: a.fly_speed,
820 walking_speed: a.walk_speed,
821 },
822 P::Flying { on_ground } => Outbound::Move(MovePacket::Flying { on_ground }),
823 P::Position { x, y, z, on_ground } => Outbound::Move(MovePacket::Position {
824 pos: Vec3::new(x, y, z),
825 on_ground,
826 }),
827 P::Look {
828 yaw,
829 pitch,
830 on_ground,
831 } => Outbound::Move(MovePacket::Look {
832 yaw,
833 pitch,
834 on_ground,
835 }),
836 P::PositionLook {
837 x,
838 y,
839 z,
840 yaw,
841 pitch,
842 on_ground,
843 } => Outbound::Move(MovePacket::PositionLook {
844 pos: Vec3::new(x, y, z),
845 yaw,
846 pitch,
847 on_ground,
848 }),
849 });
850 }
851}
852
853fn physics_abilities(a: Abilities) -> ghast_physics::Abilities {
854 ghast_physics::Abilities {
855 invulnerable: a.flags & 1 != 0,
856 flying: a.flags & 2 != 0,
857 allow_flying: a.flags & 4 != 0,
858 creative: a.flags & 8 != 0,
859 fly_speed: a.flying_speed,
860 walk_speed: a.walking_speed,
861 }
862}
863
864fn keys(c: Controls) -> ghast_physics::Keys {
865 ghast_physics::Keys {
866 forward: c.forward,
867 back: c.back,
868 left: c.left,
869 right: c.right,
870 jump: c.jump,
871 sneak: c.sneak,
872 sprint: c.sprint,
873 }
874}
875
876fn rel_flags(flags: i8) -> [bool; 5] {
877 use crate::common::teleport_flags as fl;
878 [
879 flags & fl::X != 0,
880 flags & fl::Y != 0,
881 flags & fl::Z != 0,
882 flags & fl::YAW != 0,
883 flags & fl::PITCH != 0,
884 ]
885}
886
887fn sync_pose(pose: &mut Pose, p: &ghast_physics::v1_8::PlayerState) {
888 pose.pos = Vec3::new(p.pos[0], p.bb.min_y, p.pos[2]);
889 pose.motion = Vec3::new(p.vel[0], p.vel[1], p.vel[2]);
890 pose.yaw = p.yaw;
891 pose.pitch = p.pitch;
892 pose.on_ground = p.on_ground;
893 pose.flying = p.abilities.flying;
894 pose.sneaking = p.sneaking();
895 pose.sprinting = p.sprinting;
896}
897
898pub(crate) struct Sim16 {
902 world: ghast_world::v1_16_5::World,
903 player: ghast_physics::v1_16_5::PlayerState,
904 packets: ghast_physics::Packets,
905 rec: Option<crate::record::Recorder>,
906 force_step_position: ForceStepPosition,
907 entities: Option<ghast_entity::v1_16_5::Tracker>,
908 updates: BlockUpdates,
910 session: SessionPackets,
913 block_entities: bool,
915}
916
917impl std::fmt::Debug for Sim16 {
918 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
919 f.debug_struct("Sim16")
920 .field("world", &self.world)
921 .finish_non_exhaustive()
922 }
923}
924
925impl Sim16 {
926 pub(crate) fn new() -> Self {
927 Sim16 {
928 world: ghast_world::v1_16_5::World::new(config()),
929 player: ghast_physics::v1_16_5::PlayerState::new([0.0; 3], 0.0, 0.0),
930 packets: ghast_physics::Packets::new(),
931 rec: None,
932 force_step_position: ForceStepPosition::Off,
933 entities: None,
934 updates: BlockUpdates::default(),
935 session: SessionPackets::default(),
936 block_entities: false,
937 }
938 }
939
940 pub(crate) fn with(opts: &SimOptions) -> Self {
942 let mut s = Self::new();
943 s.force_step_position = opts.force_step_position;
944 s.entities = opts.entities.map(ghast_entity::v1_16_5::Tracker::new);
945 if let Some(c) = opts.world {
946 s.world = ghast_world::v1_16_5::World::new(c);
947 s.block_entities = c.block_entities;
948 }
949 if let Some(p) = opts.record.as_deref() {
950 match crate::record::Recorder::create(p) {
951 Ok(r) => s.rec = Some(r),
952 Err(e) => log::warn!("cannot record to {}: {e}", p.display()),
953 }
954 }
955 s
956 }
957}
958
959impl WorldView for Sim16 {
960 fn column(&self, chunk_x: i32, chunk_z: i32) -> Option<&ghast_world::Column> {
961 self.world.column(chunk_x, chunk_z)
962 }
963
964 fn is_column_loaded(&self, chunk_x: i32, chunk_z: i32) -> bool {
965 self.world.is_column_loaded(chunk_x, chunk_z)
966 }
967
968 fn raw_block(&self, x: i32, y: i32, z: i32) -> Option<u32> {
969 self.world.block_state(x, y, z).map(|s| u32::from(s.id()))
970 }
971
972 fn entities(&self) -> Option<&ghast_entity::Entities> {
973 self.entities.as_deref()
974 }
975
976 fn loaded_columns(&self) -> usize {
977 self.world.columns().len()
978 }
979
980 fn block_query(&self, x: i32, y: i32, z: i32) -> Option<BlockQuery> {
981 let s = self.world.block_state(x, y, z)?;
982 let info = s.block().info();
983 let air = s.block().is_air();
984 let passable = matches!(
985 info.name,
986 "water" | "lava" | "fire" | "nether_portal" | "end_portal"
987 );
988 Some(BlockQuery {
989 air,
990 water: info.name == "water",
991 block: s.block().id(),
992 hardness: if info.diggable {
993 info.hardness.unwrap_or(-1.0)
994 } else {
995 -1.0
996 },
997 dig_material: info.material,
998 harvest_tools: info.harvest_tools,
999 selection: if air || passable {
1000 None
1001 } else {
1002 Some(union(s.shape()).unwrap_or([0.0, 0.0, 0.0, 1.0, 1.0, 1.0]))
1003 },
1004 stairs: None,
1005 activates: activates_1_16(info.name),
1006 })
1007 }
1008}
1009
1010impl Sim for Sim16 {
1011 fn world_packet(&mut self, raw: &RawPacket) -> Result<(), SimError> {
1012 {
1013 use ghast_protocol::v1_16_5::play::clientbound::id;
1014 self.session.note(raw, id::LOGIN, id::RESPAWN);
1015 }
1016 if let Some(t) = &mut self.entities
1017 && t.wants(raw.id)
1018 {
1019 t.apply(raw)?;
1020 }
1021 use ghast_protocol::v1_16_5::play::clientbound as cb;
1022 let w = &mut self.world;
1023 match raw.id {
1024 cb::id::LOGIN => {
1025 w.apply_login(&raw.decode_as()?);
1026 }
1027 cb::id::RESPAWN => {
1028 w.apply_respawn(&raw.decode_as()?);
1029 }
1030 cb::id::MAP_CHUNK => {
1031 w.apply_map_chunk(&raw.decode_as()?)?;
1032 }
1033 cb::id::UNLOAD_CHUNK => {
1034 w.apply_unload_chunk(&raw.decode_as()?);
1035 }
1036 cb::id::UPDATE_VIEW_POSITION => {
1037 w.apply_view_position(&raw.decode_as()?);
1038 }
1039 cb::id::UPDATE_VIEW_DISTANCE => {
1040 w.apply_view_distance(&raw.decode_as()?);
1041 }
1042 cb::id::BLOCK_CHANGE => {
1043 let p: cb::BlockChange = raw.decode_as()?;
1044 self.updates.push(p.location, p.r#type as u32);
1045 w.apply_block_change(&p);
1046 }
1047 cb::id::MULTI_BLOCK_CHANGE => {
1048 let p: cb::MultiBlockChange = raw.decode_as()?;
1049 let c = &p.chunk_coordinates;
1050 let sy = ((c.y << 12) as i32) >> 12;
1051 for &r in &p.records {
1052 let local = (r & 0xFFF) as i32;
1053 let pos = BlockPos::new(
1054 c.x * 16 + ((local >> 8) & 15),
1055 sy.wrapping_mul(16).wrapping_add(local & 15),
1056 c.z * 16 + ((local >> 4) & 15),
1057 );
1058 self.updates.push(pos, ((r as u64) >> 12) as u32);
1059 }
1060 w.apply_multi_block_change(&p);
1061 }
1062 cb::id::EXPLOSION => {
1063 w.apply_explosion(&raw.decode_as()?);
1064 }
1065 cb::id::WORLD_BORDER => {
1066 w.apply_world_border(&raw.decode_as()?);
1067 }
1068 cb::id::TILE_ENTITY_DATA if self.block_entities => {
1069 w.apply_tile_entity_data(&raw.decode_as()?);
1070 }
1071 _ => {}
1072 }
1073 Ok(())
1074 }
1075
1076 fn reset_player(&mut self) {
1077 let abilities = self.player.abilities;
1078 let food = self.player.food_level;
1079 self.player = ghast_physics::v1_16_5::PlayerState::new([0.0; 3], 0.0, 0.0);
1080 self.player.abilities = abilities;
1081 self.player.food_level = food;
1082 }
1083
1084 fn teleport(&mut self, pose: &mut Pose, t: &Teleport) {
1085 self.player.yaw = pose.yaw;
1086 self.player.pitch = pose.pitch;
1087 let _reply =
1088 self.player
1089 .apply_teleport([t.x, t.y, t.z], t.yaw, t.pitch, rel_flags(t.flags));
1090 if let Some(r) = &mut self.rec {
1091 let p = &self.player;
1092 r.teleport(
1093 [t.x, t.y, t.z],
1094 t.yaw,
1095 t.pitch,
1096 t.flags,
1097 t.teleport_id,
1098 p.pos,
1099 p.yaw,
1100 p.pitch,
1101 );
1102 }
1103 sync_pose_16(pose, &self.player);
1104 }
1105
1106 fn abilities(&mut self, a: Abilities) {
1107 let a = physics_abilities(a);
1108 if let Some(r) = &mut self.rec {
1109 r.abilities(&a);
1110 }
1111 self.player.apply_abilities(a);
1112 }
1113
1114 fn food(&mut self, food: i32) {
1115 self.player.food_level = food;
1116 }
1117
1118 fn velocity(&mut self, v: Vec3, add: bool) {
1119 if let Some(r) = &mut self.rec {
1120 if add {
1121 r.note("explosion push not representable");
1122 } else {
1123 r.velocity([
1124 (v.x * 8000.0).round() as i32,
1125 (v.y * 8000.0).round() as i32,
1126 (v.z * 8000.0).round() as i32,
1127 ]);
1128 }
1129 }
1130 let p = &mut self.player;
1131 if add {
1132 p.vel[0] += v.x;
1133 p.vel[1] += v.y;
1134 p.vel[2] += v.z;
1135 } else {
1136 p.vel = [v.x, v.y, v.z];
1137 }
1138 }
1139
1140 fn movement_speed(&mut self, base: f64, modifiers: &[(u128, f64, u8)]) {
1141 if let Some(r) = &mut self.rec {
1142 r.speed_attribute(base, modifiers);
1143 }
1144 self.player.apply_speed_attribute(base, modifiers);
1145 }
1146
1147 fn status_effects(&mut self, effects: &std::collections::BTreeMap<i8, i8>) {
1148 if let Some(r) = &mut self.rec {
1149 r.effects(effects);
1150 }
1151 let e = &mut self.player.effects;
1152 e.jump_boost = effects.get(&8).map(|a| i32::from(*a));
1153 e.blindness = effects.contains_key(&15);
1154 e.levitation = effects.get(&25).map(|a| i32::from(*a));
1155 e.slow_falling = effects.contains_key(&28);
1156 e.dolphins_grace = effects.contains_key(&30);
1157 }
1158
1159 fn boots(&mut self, depth_strider: u8, soul_speed: u8) {
1160 self.player.enchantments.depth_strider = depth_strider;
1161 self.player.enchantments.soul_speed = soul_speed;
1162 }
1163
1164 fn spectator(&mut self, on: bool) {
1165 self.player.spectator = on;
1166 }
1167
1168 fn set_flying(&mut self, flying: bool, out: &mut Vec<Outbound>) {
1169 let a = &mut self.player.abilities;
1170 if a.flying == flying || (flying && !a.allow_flying) {
1171 return;
1172 }
1173 a.flying = flying;
1174 let a = *a;
1175 if let Some(r) = &mut self.rec {
1176 r.abilities(&a);
1177 }
1178 out.push(Outbound::Abilities {
1179 flags: abilities_flags(&a),
1180 flying_speed: a.fly_speed,
1181 walking_speed: a.walk_speed,
1182 });
1183 }
1184
1185 fn collect_block_updates(&mut self, on: bool) {
1186 self.updates.on = on;
1187 if !on {
1188 self.updates.list.clear();
1189 }
1190 }
1191
1192 fn drain_block_updates(&mut self, out: &mut Vec<(BlockPos, u32)>) -> usize {
1193 out.append(&mut self.updates.list);
1194 std::mem::take(&mut self.updates.dropped)
1195 }
1196
1197 fn world_packet_ids(&self) -> Vec<i32> {
1198 self.world.packet_ids().collect()
1199 }
1200
1201 fn entity_config(&self) -> Option<ghast_entity::Config> {
1202 self.entities.as_ref().map(|t| t.config())
1203 }
1204
1205 fn set_entities(&mut self, config: Option<ghast_entity::Config>) {
1206 if self.entity_config() == config {
1207 return;
1208 }
1209 self.entities = config.map(|c| {
1210 let mut t = ghast_entity::v1_16_5::Tracker::new(c);
1211 self.session.replay(|r| {
1212 if let Err(e) = t.apply(r) {
1213 log::debug!("entity tracker: session packet 0x{:02x}: {e}", r.id);
1214 }
1215 });
1216 t
1217 });
1218 }
1219
1220 fn entity_packet_ids(&self) -> Vec<i32> {
1221 self.entities
1222 .as_ref()
1223 .map(|t| t.packet_ids().collect())
1224 .unwrap_or_default()
1225 }
1226
1227 fn predict_air(&mut self, x: i32, y: i32, z: i32) {
1228 self.world
1229 .set_block_state(x, y, z, ghast_data::v1_16_5::BlockState::AIR);
1230 }
1231
1232 fn held_item(&self, id: u16, count: u8, efficiency: u8) -> HeldItem {
1233 HeldItem {
1234 id,
1235 count,
1236 efficiency,
1237 places_block: ghast_data::v1_16_5::Item::from_id(id)
1238 .is_some_and(|i| ghast_data::v1_16_5::Block::from_name(i.info().name).is_some()),
1239 usable: ghast_data::v1_16_5::food(id).is_some()
1240 || ghast_data::v1_16_5::Item::from_id(id).is_some_and(|i| {
1241 matches!(
1242 i.info().name,
1243 "potion"
1244 | "milk_bucket"
1245 | "honey_bottle"
1246 | "bow"
1247 | "crossbow"
1248 | "shield"
1249 | "trident"
1250 )
1251 }),
1252 food: ghast_data::v1_16_5::food(id).is_some(),
1253 always_edible: ghast_data::v1_16_5::Item::from_id(id).is_some_and(|i| {
1254 matches!(
1255 i.info().name,
1256 "golden_apple" | "enchanted_golden_apple" | "chorus_fruit"
1257 )
1258 }),
1259 tool_speed: Some(ghast_data::v1_16_5::tool_multiplier),
1260 }
1261 }
1262
1263 fn step(
1264 &mut self,
1265 pose: &mut Pose,
1266 controls: Controls,
1267 using_item: bool,
1268 entity_id: i32,
1269 out: &mut Vec<Outbound>,
1270 ) {
1271 let input = ghast_physics::TickInput {
1272 keys: keys(controls),
1273 yaw: pose.yaw,
1274 pitch: pose.pitch,
1275 using_item,
1276 };
1277 self.packets.clear();
1278 if let Some(r) = &mut self.rec
1279 && !r.started()
1280 {
1281 r.start_1_16(&self.player);
1282 }
1283 if let Some(r) = &mut self.rec {
1284 let w = &self.world;
1285 r.observe(self.player.pos, |x, y, z| {
1286 w.is_column_loaded(x >> 4, z >> 4)
1287 .then(|| w.block_state(x, y, z).map_or(0, |s| u32::from(s.id())))
1288 });
1289 }
1290 ghast_physics::v1_16_5::tick(&mut self.player, &input, &self.world, &mut self.packets);
1291 if self.force_step_position != ForceStepPosition::Off {
1292 let p = &mut self.player;
1293 let cur = p.pos;
1294 force_position(
1295 &mut self.packets,
1296 cur,
1297 &mut p.sent.pos,
1298 &mut p.sent.ticks_since_pos,
1299 self.force_step_position,
1300 );
1301 }
1302 if let Some(r) = &mut self.rec {
1303 r.tick(
1304 &input,
1305 &crate::record::state_1_16(&self.player),
1306 &self.packets,
1307 );
1308 }
1309 push_packets(&self.packets, entity_id, out);
1310 sync_pose_16(pose, &self.player);
1311 self.world.advance_time(50);
1312 }
1313}
1314
1315#[cfg(test)]
1317pub(crate) mod test_chunks {
1318 use bytes::{BufMut, Bytes, BytesMut};
1319 use ghast_protocol::RawPacket;
1320
1321 pub(crate) fn v1_8(x: i32, z: i32) -> RawPacket {
1323 let mut d = BytesMut::with_capacity(12_544);
1324 for i in 0..4096 {
1325 d.put_u16_le(if i >> 8 == 15 { 1 << 4 } else { 0 });
1327 }
1328 d.put_bytes(0, 2048); d.put_bytes(0xff, 2048); d.put_bytes(1, 256); RawPacket::from_packet(&ghast_protocol::v1_8::play::clientbound::MapChunk {
1332 x,
1333 z,
1334 ground_up: true,
1335 bit_map: 1 << 3,
1336 chunk_data: d.freeze(),
1337 })
1338 .unwrap()
1339 }
1340
1341 pub(crate) fn v1_16_5(x: i32, z: i32) -> RawPacket {
1343 RawPacket::from_packet(&ghast_protocol::v1_16_5::play::clientbound::MapChunk {
1344 x,
1345 z,
1346 ground_up: true,
1347 bit_map: 0,
1348 heightmaps: ghast_protocol::nbt::RawNbt::from_bytes(Bytes::from_static(&[10, 0, 0, 0]))
1349 .unwrap(),
1350 biomes: Some(vec![1; 1024]),
1351 chunk_data: Bytes::new(),
1352 block_entities: Vec::new(),
1353 })
1354 .unwrap()
1355 }
1356}
1357
1358#[cfg(test)]
1359impl WorldView for Box<dyn Sim> {
1360 fn column(&self, chunk_x: i32, chunk_z: i32) -> Option<&ghast_world::Column> {
1361 (**self).column(chunk_x, chunk_z)
1362 }
1363 fn is_column_loaded(&self, chunk_x: i32, chunk_z: i32) -> bool {
1364 (**self).is_column_loaded(chunk_x, chunk_z)
1365 }
1366 fn raw_block(&self, x: i32, y: i32, z: i32) -> Option<u32> {
1367 (**self).raw_block(x, y, z)
1368 }
1369 fn entities(&self) -> Option<&ghast_entity::Entities> {
1370 (**self).entities()
1371 }
1372 fn loaded_columns(&self) -> usize {
1373 (**self).loaded_columns()
1374 }
1375
1376 fn block_query(&self, x: i32, y: i32, z: i32) -> Option<BlockQuery> {
1377 (**self).block_query(x, y, z)
1378 }
1379}
1380
1381#[cfg(test)]
1382impl Sim for Box<dyn Sim> {
1383 fn world_packet(&mut self, raw: &RawPacket) -> Result<(), SimError> {
1384 (**self).world_packet(raw)
1385 }
1386 fn reset_player(&mut self) {
1387 (**self).reset_player();
1388 }
1389 fn teleport(&mut self, pose: &mut Pose, t: &Teleport) {
1390 (**self).teleport(pose, t);
1391 }
1392 fn abilities(&mut self, a: Abilities) {
1393 (**self).abilities(a);
1394 }
1395 fn food(&mut self, food: i32) {
1396 (**self).food(food);
1397 }
1398 fn velocity(&mut self, v: Vec3, add: bool) {
1399 (**self).velocity(v, add);
1400 }
1401 fn step(
1402 &mut self,
1403 pose: &mut Pose,
1404 controls: Controls,
1405 using_item: bool,
1406 entity_id: i32,
1407 out: &mut Vec<Outbound>,
1408 ) {
1409 (**self).step(pose, controls, using_item, entity_id, out);
1410 }
1411 fn set_flying(&mut self, flying: bool, out: &mut Vec<Outbound>) {
1412 (**self).set_flying(flying, out);
1413 }
1414 fn collect_block_updates(&mut self, on: bool) {
1415 (**self).collect_block_updates(on);
1416 }
1417 fn drain_block_updates(&mut self, out: &mut Vec<(BlockPos, u32)>) -> usize {
1418 (**self).drain_block_updates(out)
1419 }
1420 fn world_packet_ids(&self) -> Vec<i32> {
1421 (**self).world_packet_ids()
1422 }
1423 fn entity_config(&self) -> Option<ghast_entity::Config> {
1424 (**self).entity_config()
1425 }
1426 fn set_entities(&mut self, config: Option<ghast_entity::Config>) {
1427 (**self).set_entities(config);
1428 }
1429 fn entity_packet_ids(&self) -> Vec<i32> {
1430 (**self).entity_packet_ids()
1431 }
1432 fn predict_air(&mut self, x: i32, y: i32, z: i32) {
1433 (**self).predict_air(x, y, z);
1434 }
1435 fn movement_speed(&mut self, base: f64, modifiers: &[(u128, f64, u8)]) {
1436 (**self).movement_speed(base, modifiers);
1437 }
1438 fn status_effects(&mut self, effects: &std::collections::BTreeMap<i8, i8>) {
1439 (**self).status_effects(effects);
1440 }
1441 fn boots(&mut self, depth_strider: u8, soul_speed: u8) {
1442 (**self).boots(depth_strider, soul_speed);
1443 }
1444 fn spectator(&mut self, on: bool) {
1445 (**self).spectator(on);
1446 }
1447 fn held_item(&self, id: u16, count: u8, efficiency: u8) -> HeldItem {
1448 (**self).held_item(id, count, efficiency)
1449 }
1450}
1451
1452#[cfg(test)]
1453mod tests {
1454 use super::*;
1455 use crate::version::{ClientVersion, V1_8};
1456
1457 #[test]
1459 fn teleport_rules_agree() {
1460 let cases = [
1461 (1.5, 64.0, -2.5, 450.0_f32, 100.0_f32, 0_i8),
1462 (0.25, -1.0, 0.5, -30.0, -95.0, 0x1f),
1463 (3.0, 70.0, 3.0, 720.5, 10.0, 0x08),
1464 ];
1465 for (x, y, z, yaw, pitch, flags) in cases {
1466 let t = Teleport {
1467 x,
1468 y,
1469 z,
1470 yaw,
1471 pitch,
1472 flags,
1473 teleport_id: None,
1474 };
1475 let start = Pose {
1476 pos: Vec3::new(10.0, 65.0, 10.0),
1477 yaw: 33.0,
1478 pitch: 5.0,
1479 motion: Vec3::new(0.1, -0.0784, 0.2),
1480 ..Pose::default()
1481 };
1482 let mut a = start;
1483 <V1_8 as ClientVersion>::apply_teleport(&mut a, &t);
1484 let mut sim = Sim18::new();
1485 let mut b = start;
1486 sim.player
1487 .set_position(start.pos.x, start.pos.y, start.pos.z);
1488 sim.player.vel = [start.motion.x, start.motion.y, start.motion.z];
1489 sim.teleport(&mut b, &t);
1490 assert_eq!(
1491 (a.pos, a.yaw, a.pitch, a.motion),
1492 (b.pos, b.yaw, b.pitch, b.motion),
1493 "{t:?}"
1494 );
1495 }
1496 }
1497
1498 #[test]
1499 fn force_position_only_on_height_change() {
1500 use ghast_physics::Packet as P;
1501 let mut pk = ghast_physics::Packets::new();
1502 pk.push(P::StopSprinting);
1503 pk.push(P::Look {
1504 yaw: 1.0,
1505 pitch: 2.0,
1506 on_ground: true,
1507 });
1508 let mut sent = [1.0, 3.979, 8.3];
1509 let mut ticks = 3;
1510 force_position(
1512 &mut pk,
1513 [1.01, 3.979, 8.3],
1514 &mut sent,
1515 &mut ticks,
1516 ForceStepPosition::Always,
1517 );
1518 assert!(matches!(pk.as_slice()[1], P::Look { .. }));
1519 force_position(
1521 &mut pk,
1522 [1.01, 4.0, 8.28],
1523 &mut sent,
1524 &mut ticks,
1525 ForceStepPosition::Always,
1526 );
1527 assert_eq!(pk.as_slice()[0], P::StopSprinting);
1528 assert!(matches!(
1529 pk.as_slice()[1],
1530 P::PositionLook {
1531 y: 4.0,
1532 on_ground: true,
1533 ..
1534 }
1535 ));
1536 assert_eq!((sent, ticks), ([1.01, 4.0, 8.28], 0));
1537 }
1538
1539 #[test]
1540 fn force_position_step_up_only() {
1541 use ghast_physics::Packet as P;
1542 let look = |on_ground| P::Look {
1543 yaw: 1.0,
1544 pitch: 2.0,
1545 on_ground,
1546 };
1547 let mode = ForceStepPosition::StepUpOnly;
1548 let mut pk = ghast_physics::Packets::new();
1550 pk.push(look(true));
1551 let (mut sent, mut ticks) = ([1.0, 4.0, 8.3], 3);
1552 force_position(&mut pk, [1.01, 3.979, 8.3], &mut sent, &mut ticks, mode);
1553 assert!(matches!(pk.as_slice()[0], P::Look { .. }));
1554 force_position(&mut pk, [1.01, 4.01, 8.3], &mut sent, &mut ticks, mode);
1556 let mut air = ghast_physics::Packets::new();
1557 air.push(look(false));
1558 force_position(&mut air, [1.01, 4.01, 8.3], &mut sent, &mut ticks, mode);
1559 assert!(matches!(air.as_slice()[0], P::Look { .. }));
1560 let mut up = ghast_physics::Packets::new();
1562 up.push(look(true));
1563 let (mut sent, mut ticks) = ([1.0, 3.979, 8.3], 3);
1564 force_position(&mut up, [1.01, 4.0, 8.28], &mut sent, &mut ticks, mode);
1565 assert!(matches!(up.as_slice()[0], P::PositionLook { y: 4.0, .. }));
1566 assert_eq!((sent, ticks), ([1.01, 4.0, 8.28], 0));
1567 }
1568
1569 #[test]
1570 fn physics_stands_on_the_test_floor() {
1571 let mut sim = Sim18::new();
1572 sim.world_packet(&test_chunks::v1_8(0, 0)).unwrap();
1573 let mut pose = Pose::default();
1574 sim.teleport(
1575 &mut pose,
1576 &Teleport {
1577 x: 8.5,
1578 y: 64.0,
1579 z: 8.5,
1580 yaw: 0.0,
1581 pitch: 0.0,
1582 flags: 0,
1583 teleport_id: None,
1584 },
1585 );
1586 let mut out = Vec::new();
1587 for _ in 0..30 {
1588 sim.step(&mut pose, Controls::NONE, false, 1, &mut out);
1589 }
1590 assert_eq!(pose.pos, Vec3::new(8.5, 64.0, 8.5));
1591 assert!(pose.on_ground);
1592 for _ in 0..10 {
1594 sim.step(
1595 &mut pose,
1596 Controls {
1597 forward: true,
1598 ..Controls::NONE
1599 },
1600 false,
1601 1,
1602 &mut out,
1603 );
1604 }
1605 assert!(pose.pos.z > 9.0, "{pose:?}");
1606 }
1607}
1608
1609fn sync_pose_16(pose: &mut Pose, p: &ghast_physics::v1_16_5::PlayerState) {
1610 pose.pos = Vec3::new(p.pos[0], p.pos[1], p.pos[2]);
1611 pose.motion = Vec3::new(p.vel[0], p.vel[1], p.vel[2]);
1612 pose.yaw = p.yaw;
1613 pose.pitch = p.pitch;
1614 pose.on_ground = p.on_ground;
1615 pose.flying = p.abilities.flying;
1616 pose.sneaking = p.sneaking();
1617 pose.sprinting = p.sprinting;
1618}