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