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