Skip to main content

ghast_client/
sim.rs

1//! Per-version world and player movement ("simulation"): the chunk store of `ghast-world` and
2//! the movement step — `ghast-physics` for 1.8.9 (vanilla-exact), the stand-still mover for
3//! 1.16.5 until its physics exists.
4//!
5//! The engine is generic over [`Sim`]; the behaviour sees the world through the
6//! version-free [`WorldView`].
7
8use 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/// Why a world packet could not be applied.
18#[derive(Debug)]
19pub(crate) enum SimError {
20    /// The packet itself did not decode.
21    Decode(DecodeError),
22    /// The chunk data inside it was malformed.
23    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
38/// Block access for behaviours, independent of the protocol version.
39pub trait WorldView {
40    /// Whether the chunk column is loaded (sent by the server and not unloaded).
41    fn is_column_loaded(&self, chunk_x: i32, chunk_z: i32) -> bool;
42
43    /// The block state at a position as the version's raw id: the 1.8 `id << 4 | meta` or the
44    /// 1.16 global palette id (decode it with `ghast-data`). `None` when the column is not
45    /// loaded — never treat that as air.
46    fn raw_block(&self, x: i32, y: i32, z: i32) -> Option<u32>;
47
48    /// Number of loaded columns.
49    fn loaded_columns(&self) -> usize;
50
51    /// The tracked entities (`None` unless the behaviour asked for [`crate::Interests::ENTITIES`]).
52    fn entities(&self) -> Option<&ghast_entity::Entities>;
53
54    /// Dig / place facts of a block (hardness, tool group, ray box); `None` when the column
55    /// is not loaded.
56    fn block_query(&self, x: i32, y: i32, z: i32) -> Option<BlockQuery>;
57
58    /// The whole loaded column (sections, light / biomes when kept, see
59    /// [`crate::Client::world_config`]) for bulk reads without a call per block. Block states
60    /// are the version's raw ids, as in [`WorldView::raw_block`]. `None` when not loaded.
61    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/// World + movement of one version.
68/// The join / respawn packets of the session (cheap `Bytes` copies).
69#[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    /// Feeds them to a fresh tracker.
86    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
93/// Most block changes kept between two behaviour ticks (a chunk-sized burst is 65536).
94const MAX_BLOCK_UPDATES: usize = 4096;
95
96/// Block changes buffered for the behaviour.
97#[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    /// Applies a world packet (chunks, block changes, join / respawn / view changes).
118    /// Other packets are ignored.
119    fn world_packet(&mut self, raw: &RawPacket) -> Result<(), SimError>;
120    /// A new client player (join, respawn).
121    fn reset_player(&mut self);
122    /// A server teleport: updates the pose like vanilla (the engine sends the reply).
123    fn teleport(&mut self, pose: &mut Pose, t: &Teleport);
124    /// Server abilities.
125    fn abilities(&mut self, a: Abilities);
126    /// Food level (sprinting needs more than 6).
127    fn food(&mut self, food: i32);
128    /// Velocity from the server (entity velocity for our entity, explosion push), in blocks
129    /// per tick; `add` adds to the current velocity (explosion) instead of setting it.
130    fn velocity(&mut self, v: Vec3, add: bool);
131    /// Our movement speed attribute: base and every modifier `(uuid, amount, operation)` in
132    /// packet order (the server's list includes the sprint modifier while sprinting; vanilla
133    /// replaces the client's set with it).
134    fn movement_speed(&mut self, base: f64, modifiers: &[(u128, f64, u8)]);
135    /// Our active status effects (id → amplifier).
136    fn status_effects(&mut self, effects: &std::collections::BTreeMap<i8, i8>);
137    /// Boots enchantments that change movement.
138    fn boots(&mut self, depth_strider: u8, soul_speed: u8);
139    /// Spectator game mode (no collision).
140    fn spectator(&mut self, on: bool);
141    /// One movement step: physics, then the sprint / sneak / abilities packets and exactly one
142    /// movement packet appended to `out`. Updates `pose`.
143    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    /// Packet ids the entity tracker wants (empty without tracking).
152    fn entity_packet_ids(&self) -> Vec<i32>;
153    /// Packet ids the world consumes with its configuration.
154    fn world_packet_ids(&self) -> Vec<i32>;
155    /// The entity tracking configuration (`None`: not tracking).
156    fn entity_config(&self) -> Option<ghast_entity::Config>;
157    /// Starts (empty, our entity id and dimension from the session's join / respawn), stops
158    /// (frees the store) or reconfigures entity tracking mid-session.
159    fn set_entities(&mut self, config: Option<ghast_entity::Config>);
160    /// Client-side flight toggle: sets the physics' flying flag and appends the `abilities`
161    /// packet when it changes (starting needs "may fly").
162    fn set_flying(&mut self, flying: bool, out: &mut Vec<Outbound>);
163    /// Whether block changes are collected for [`Sim::drain_block_updates`].
164    fn collect_block_updates(&mut self, on: bool);
165    /// Moves the block changes (position, version raw state) received since the last call
166    /// into `out`; returns how many were dropped because the buffer was full.
167    fn drain_block_updates(&mut self, out: &mut Vec<(BlockPos, u32)>) -> usize;
168    /// The block was broken on the client (air until the server says otherwise).
169    fn predict_air(&mut self, x: i32, y: i32, z: i32);
170    /// A held item's version facts (tool speeds, block item, usable) from its id.
171    fn held_item(&self, id: u16, count: u8, efficiency: u8) -> HeldItem;
172}
173
174/// Options of the per-version sim.
175#[derive(Debug, Clone, Default)]
176pub(crate) struct SimOptions {
177    /// Write a movement recording there.
178    pub record: Option<std::path::PathBuf>,
179    /// Grim compatibility (not vanilla): send the position on ticks whose feet height changed
180    /// although the move stayed under the 0.03 resend threshold.
181    pub force_step_position: ForceStepPosition,
182    /// Track entities (and the tab list).
183    pub entities: Option<ghast_entity::Config>,
184    /// What the world keeps besides block states (default: nothing, see [`config`]).
185    pub world: Option<WorldConfig>,
186}
187
188/// Grim compatibility (**not vanilla**): when to send the position on a tick whose move stayed
189/// under vanilla's 0.03-block resend threshold. See `crates/ghast-physics/GRIM_NOTES.md`.
190#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
191#[non_exhaustive]
192pub enum ForceStepPosition {
193    /// Vanilla: never.
194    #[default]
195    Off,
196    /// Whenever the feet height changed since the last sent position (step-ups and landings).
197    Always,
198    /// Only step-ups: on ground after the move and the feet went up. Falls, landings and
199    /// descents stay vanilla.
200    StepUpOnly,
201}
202
203/// Grim compatibility: when the tick's movement packet carries no position but the feet height
204/// changed since the last sent position (a step-up or landing under the 0.03 threshold),
205/// upgrade it to `position` / `position_look` and update the sent bookkeeping.
206fn 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
265/// 1.16 blocks whose `onUse` succeeds for a plain right click (GUIs, doors,
266/// switches, …). Name-based approximation of vanilla's per-block `use` overrides.
267fn 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
322/// Union of the collision boxes as a ray box (`None` when there is none).
323fn 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
336/// 1.8 blocks whose client `Block.onUse` answers a right click.
337const 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
343/// 1.8 items used by holding right click besides food: potion, milk, bow, swords.
344const USABLE_1_8: &[u16] = &[373, 335, 261, 267, 268, 272, 276, 283];
345
346/// A held item's version facts (tool speeds, block item, usable) from its id.
347pub(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                // Golden apple.
363                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
398/// Block-local `[min, max]` of a physics box.
399fn 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
403/// The 1.8 ray shape at a position (ghast-physics: vanilla's bounds after
404/// `Block.setBoundingBox`, neighbour-dependent shapes and the stairs passes):
405/// `(selection, stairs passes)`.
406fn 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
415/// A ray shape as the hands take it.
416type RayBox = (Option<[f64; 6]>, Option<[Option<[f64; 6]>; 8]>);
417
418fn config() -> WorldConfig {
419    // The bot needs block states only: no biomes, no light.
420    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
443// ---- 1.8.9: ghast-physics ---------------------------------------------------------------
444
445/// 1.8.9: the chunk store and the vanilla-exact physics.
446pub(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    /// Block changes since the last drain (position, version raw state), when collected.
454    updates: BlockUpdates,
455    /// The session's last join and respawn packets: a tracker started mid-session replays
456    /// them to learn our entity id and dimension.
457    session: SessionPackets,
458    /// The world keeps block entities (signs, tile entity NBT).
459    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    /// Test hook: the physics player.
474    pub(crate) fn test_player(&self) -> &ghast_physics::v1_8::PlayerState {
475        &self.player
476    }
477
478    /// Test hook: replaces the physics player and syncs `pose`.
479    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    /// Test hook: the chunk store.
485    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    /// A sim with options (recording, Grim compatibility).
506    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        // A fresh ClientPlayerEntity: at the origin, lastReported* zero; abilities and food are
624        // re-sent by the server.
625        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        // The engine keeps the look (its rotation may have moved it since the last tick).
642        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        // Soul speed does not exist in 1.8.
722        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        // A moving world border advances with the client's clock.
846        self.world.advance_time(50);
847    }
848}
849
850/// The physics' packets of a tick, in order, as wire packets.
851fn 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
941// ---- 1.16.5: world + stand-still mover --------------------------------------------------
942
943/// 1.16.5: the chunk store and the stand-still mover (no 1.16 physics yet).
944pub(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    /// Block changes since the last drain (position, version raw state), when collected.
952    updates: BlockUpdates,
953    /// The session's last join and respawn packets: a tracker started mid-session replays
954    /// them to learn our entity id and dimension.
955    session: SessionPackets,
956    /// The world keeps block entities (signs, tile entity NBT).
957    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    /// A sim with options (recording, Grim compatibility).
984    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/// Chunk packets for tests.
1332#[cfg(test)]
1333pub(crate) mod test_chunks {
1334    use bytes::{BufMut, Bytes, BytesMut};
1335    use ghast_protocol::RawPacket;
1336
1337    /// A 1.8 ground-up chunk with one section (y 48–63) whose top layer (y = 63) is stone.
1338    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            // Index = y << 8 | z << 4 | x; local y 15 is the top layer. Stone = 1 << 4.
1342            d.put_u16_le(if i >> 8 == 15 { 1 << 4 } else { 0 });
1343        }
1344        d.put_bytes(0, 2048); // block light
1345        d.put_bytes(0xff, 2048); // sky light
1346        d.put_bytes(1, 256); // biomes
1347        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    /// A 1.16.5 full chunk without sections (all air).
1358    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    /// The 1.8 physics' teleport matches the version's documented rules.
1474    #[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        // Same height: untouched.
1527        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        // Stepped up: the look becomes a position_look, the bookkeeping follows.
1536        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        // Feet went down (a landing under the threshold): stays vanilla.
1565        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        // Went up but airborne: stays vanilla.
1571        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        // A step-up (up and on ground): forced.
1577        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        // Walking forward moves the player.
1609        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}