Skip to main content

ghast_client/
common.rs

1//! The version-agnostic layer: what the client core receives from and sends to the server,
2//! independent of the protocol version.
3//!
4//! Each supported version (see [`crate::version`]) maps its generated packets to [`Inbound`]
5//! and [`Outbound`] values. Only the packets the core or the behaviour needs are mapped; the
6//! rest is dropped by the frame decoder's filter before it is decoded.
7
8use bytes::Bytes;
9use ghast_protocol::types::ByteString;
10use uuid::Uuid;
11
12/// A position or direction in world space.
13#[derive(Debug, Clone, Copy, Default, PartialEq)]
14pub struct Vec3 {
15    /// East (+) / west (−).
16    pub x: f64,
17    /// Up (+) / down (−).
18    pub y: f64,
19    /// South (+) / north (−).
20    pub z: f64,
21}
22
23impl Vec3 {
24    /// A vector from its components.
25    #[must_use]
26    pub const fn new(x: f64, y: f64, z: f64) -> Self {
27        Vec3 { x, y, z }
28    }
29}
30
31/// The player's pose: feet position, look, motion and movement flags.
32///
33/// `yaw` / `pitch` are degrees in vanilla's convention (yaw is an unbounded `f32` like the
34/// client's `Entity.yaw`; 0 = south, 90 = west; pitch 90 = straight down).
35#[derive(Debug, Clone, Copy, Default, PartialEq)]
36pub struct Pose {
37    /// Feet position (bounding box minimum Y).
38    pub pos: Vec3,
39    /// Yaw in degrees.
40    pub yaw: f32,
41    /// Pitch in degrees.
42    pub pitch: f32,
43    /// Motion per tick.
44    pub motion: Vec3,
45    /// On ground as the client computes it (and sends it).
46    pub on_ground: bool,
47    /// Creative / ability flight.
48    pub flying: bool,
49    /// Sneaking (as sent with `entity_action`).
50    pub sneaking: bool,
51    /// Sprinting (as sent with `entity_action`).
52    pub sprinting: bool,
53}
54
55impl Pose {
56    /// Eye position: feet + 1.62 (1.54 while sneaking in 1.8; 1.16.5 uses a crouching pose
57    /// with eyes at 1.27, see [`crate::GameVersion::sneak_eye_height`]).
58    #[must_use]
59    pub fn eyes(&self, eye_height: f64) -> Vec3 {
60        Vec3::new(self.pos.x, self.pos.y + eye_height, self.pos.z)
61    }
62}
63
64/// Movement keys held for the next movement step (the keyboard state in vanilla).
65#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Hash)]
66pub struct Controls {
67    /// Walk forward (W).
68    pub forward: bool,
69    /// Walk backward (S).
70    pub back: bool,
71    /// Strafe left (A).
72    pub left: bool,
73    /// Strafe right (D).
74    pub right: bool,
75    /// Jump (space).
76    pub jump: bool,
77    /// Sneak (shift).
78    pub sneak: bool,
79    /// Sprint key (ctrl).
80    pub sprint: bool,
81}
82
83impl Controls {
84    /// No key held.
85    pub const NONE: Controls = Controls {
86        forward: false,
87        back: false,
88        left: false,
89        right: false,
90        jump: false,
91        sneak: false,
92        sprint: false,
93    };
94}
95
96/// Where a chat message is shown.
97#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
98#[non_exhaustive]
99pub enum ChatPosition {
100    /// A player chat message.
101    Chat,
102    /// A system message (command feedback, server announcements).
103    System,
104    /// The action bar above the hotbar.
105    ActionBar,
106    /// A value this client does not know.
107    Other(i8),
108}
109
110impl ChatPosition {
111    /// From the wire byte (same values in every supported version).
112    #[must_use]
113    pub fn from_wire(v: i8) -> Self {
114        match v {
115            0 => Self::Chat,
116            1 => Self::System,
117            2 => Self::ActionBar,
118            v => Self::Other(v),
119        }
120    }
121}
122
123/// A chat message from the server. The text stays raw JSON until asked for.
124#[derive(Debug, Clone, PartialEq, Eq)]
125#[non_exhaustive]
126pub struct ChatMessage {
127    /// The chat component as the server sent it (JSON).
128    pub json: ByteString,
129    /// Where it is shown.
130    pub position: ChatPosition,
131    /// Sender UUID (1.16+; `None` on 1.8 and for system messages with the nil UUID).
132    pub sender: Option<Uuid>,
133}
134
135impl ChatMessage {
136    /// The message as plain text, with legacy `§` formatting codes kept.
137    #[must_use]
138    pub fn text(&self) -> String {
139        ghast_protocol::chat::to_plain_text(&self.json)
140    }
141
142    /// The message as plain text without `§` formatting codes.
143    #[must_use]
144    pub fn plain(&self) -> String {
145        ghast_protocol::chat::strip_formatting(&self.text())
146    }
147}
148
149/// Converts a chat component (JSON) to plain text without formatting codes.
150#[must_use]
151pub fn chat_to_plain(json: &str) -> String {
152    ghast_protocol::chat::strip_formatting(&ghast_protocol::chat::to_plain_text(json))
153}
154
155/// Game mode.
156#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
157pub enum GameMode {
158    /// Survival.
159    #[default]
160    Survival,
161    /// Creative.
162    Creative,
163    /// Adventure.
164    Adventure,
165    /// Spectator.
166    Spectator,
167    /// A value this client does not know.
168    Other(u8),
169}
170
171impl GameMode {
172    /// From the wire value (the hardcore bit 0x08 of 1.8 is ignored).
173    #[must_use]
174    pub fn from_wire(v: u8) -> Self {
175        match v & 0x07 {
176            0 => Self::Survival,
177            1 => Self::Creative,
178            2 => Self::Adventure,
179            3 => Self::Spectator,
180            _ => Self::Other(v),
181        }
182    }
183}
184
185/// Player abilities (`abilities` packet).
186#[derive(Debug, Clone, Copy, PartialEq)]
187#[non_exhaustive]
188pub struct Abilities {
189    /// Raw flags: 1 invulnerable, 2 flying, 4 may fly, 8 creative (instant break).
190    pub flags: i8,
191    /// Flying speed (0.05 by default).
192    pub flying_speed: f32,
193    /// Walking speed (0.1 by default).
194    pub walking_speed: f32,
195}
196
197impl Default for Abilities {
198    fn default() -> Self {
199        Abilities {
200            flags: 0,
201            flying_speed: 0.05,
202            walking_speed: 0.1,
203        }
204    }
205}
206
207impl Abilities {
208    /// The server allows flight.
209    #[must_use]
210    pub fn may_fly(&self) -> bool {
211        self.flags & 4 != 0
212    }
213
214    /// The server says the player is flying.
215    #[must_use]
216    pub fn flying(&self) -> bool {
217        self.flags & 2 != 0
218    }
219}
220
221/// Health, food and saturation (`update_health`).
222#[derive(Debug, Clone, Copy, PartialEq)]
223#[non_exhaustive]
224pub struct Health {
225    /// Health points (20 = full; ≤ 0 = dead).
226    pub health: f32,
227    /// Food level 0 … 20.
228    pub food: i32,
229    /// Food saturation.
230    pub saturation: f32,
231}
232
233impl Default for Health {
234    fn default() -> Self {
235        Health {
236            health: 20.0,
237            food: 20,
238            saturation: 5.0,
239        }
240    }
241}
242
243/// Relative-teleport flag bits of the server's position packet.
244pub mod teleport_flags {
245    /// X is relative.
246    pub const X: i8 = 0x01;
247    /// Y is relative.
248    pub const Y: i8 = 0x02;
249    /// Z is relative.
250    pub const Z: i8 = 0x04;
251    /// Yaw is relative.
252    pub const YAW: i8 = 0x08;
253    /// Pitch is relative.
254    pub const PITCH: i8 = 0x10;
255}
256
257/// A server teleport (position packet), before it is applied to the pose.
258#[derive(Debug, Clone, Copy, PartialEq)]
259#[non_exhaustive]
260pub struct Teleport {
261    /// X (absolute or relative, see `flags`).
262    pub x: f64,
263    /// Y.
264    pub y: f64,
265    /// Z.
266    pub z: f64,
267    /// Yaw.
268    pub yaw: f32,
269    /// Pitch.
270    pub pitch: f32,
271    /// [`teleport_flags`] bits.
272    pub flags: i8,
273    /// Teleport id to confirm (1.9+).
274    pub teleport_id: Option<i32>,
275}
276
277/// Login-state packets from the server, common to every version.
278#[derive(Debug, Clone, PartialEq)]
279#[non_exhaustive]
280pub enum LoginInbound {
281    /// The server refused the login (chat JSON).
282    Disconnect(ByteString),
283    /// Online mode: the server asks for encryption.
284    EncryptionRequest {
285        /// Server id (usually empty) for the session hash.
286        server_id: ByteString,
287        /// The server's RSA public key (DER).
288        public_key: Bytes,
289        /// Token to encrypt and send back.
290        verify_token: Bytes,
291    },
292    /// Compression starts with the next packet.
293    Compress(i32),
294    /// Login finished; the connection is in the play state.
295    Success {
296        /// The player's UUID as the server assigned it.
297        uuid: Option<Uuid>,
298        /// The player's name.
299        username: ByteString,
300    },
301    /// A login plugin request (1.13+): answered with "not understood".
302    PluginRequest {
303        /// Message id to answer.
304        message_id: i32,
305        /// Channel name.
306        channel: ByteString,
307    },
308}
309
310/// Static information from the join game packet.
311#[derive(Debug, Clone, PartialEq)]
312#[non_exhaustive]
313pub struct JoinGame {
314    /// The player's entity id.
315    pub entity_id: i32,
316    /// Game mode.
317    pub game_mode: GameMode,
318    /// Dimension: the 1.8 id (−1 nether, 0 overworld, 1 end) as a string, or the 1.16 world
319    /// name (e.g. `minecraft:overworld`).
320    pub dimension: String,
321    /// Hardcore world.
322    pub hardcore: bool,
323    /// 1.16: the dimension codec and dimension type NBT, kept raw (not parsed).
324    pub dimension_nbt: Option<(ghast_protocol::nbt::RawNbt, ghast_protocol::nbt::RawNbt)>,
325    /// 1.16: the respawn screen is shown (false = immediate respawn gamerule).
326    pub respawn_screen: bool,
327}
328
329/// Clientbound play packets the core understands, common to every version.
330#[derive(Debug, Clone, PartialEq)]
331#[non_exhaustive]
332pub enum Inbound {
333    /// Keep-alive to echo.
334    KeepAlive(i64),
335    /// Join game (also sent again on a proxy server switch).
336    JoinGame(JoinGame),
337    /// Respawn / dimension change.
338    Respawn {
339        /// Dimension key (see [`JoinGame::dimension`]).
340        dimension: String,
341        /// Game mode after the respawn.
342        game_mode: GameMode,
343    },
344    /// Server teleport / position correction.
345    Teleport(Teleport),
346    /// Chat message.
347    Chat(ChatMessage),
348    /// The server ends the session (chat JSON).
349    Disconnect(ByteString),
350    /// Health, food and saturation.
351    Health(Health),
352    /// Abilities.
353    Abilities(Abilities),
354    /// The server selects a hotbar slot.
355    HeldSlot(i8),
356    /// Game state change (`reason`, `value`), e.g. reason 3 = game mode.
357    GameState {
358        /// Reason id (same numbering in all supported versions).
359        reason: u8,
360        /// Value.
361        value: f32,
362    },
363    /// Window confirm transaction.
364    Transaction {
365        /// Window id.
366        window_id: i8,
367        /// Action number.
368        action: i16,
369        /// Accepted.
370        accepted: bool,
371    },
372    /// A window opened.
373    OpenWindow(i32),
374    /// The open window was closed by the server.
375    CloseWindow(i32),
376    /// Compression threshold change in the play state (1.8).
377    SetCompression(i32),
378    /// Entity velocity (blocks per tick; the wire's 1/8000 units converted).
379    Velocity {
380        /// The entity.
381        entity_id: i32,
382        /// Motion per tick.
383        motion: Vec3,
384    },
385    /// The player motion an explosion adds.
386    ExplosionPush(Vec3),
387    /// Movement speed attribute of an entity (entity properties packet).
388    MovementSpeed {
389        /// The entity.
390        entity_id: i32,
391        /// Base value.
392        base: f64,
393        /// Modifiers `(uuid, amount, operation)` in packet order.
394        modifiers: Vec<(Uuid, f64, i8)>,
395    },
396    /// A status effect was applied to an entity.
397    Effect {
398        /// The entity.
399        entity_id: i32,
400        /// Effect id.
401        id: i8,
402        /// Amplifier (level − 1).
403        amplifier: i8,
404    },
405    /// A status effect ended.
406    EffectRemoved {
407        /// The entity.
408        entity_id: i32,
409        /// Effect id.
410        id: i8,
411    },
412    /// Entity status (e.g. 9 = our item use finished).
413    EntityStatus {
414        /// The entity.
415        entity_id: i32,
416        /// Status id.
417        status: i8,
418    },
419    /// An entity (the player, if it is our id) died (1.16 `combat_event` "entity dead").
420    Died {
421        /// The dead player's entity id.
422        entity_id: i32,
423    },
424}
425
426/// Sneak / sprint `entity_action` values.
427#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
428#[non_exhaustive]
429pub enum EntityAction {
430    /// Start sneaking.
431    StartSneaking,
432    /// Stop sneaking.
433    StopSneaking,
434    /// Start sprinting.
435    StartSprinting,
436    /// Stop sprinting.
437    StopSprinting,
438}
439
440/// The one movement packet of a tick.
441#[derive(Debug, Clone, Copy, PartialEq)]
442pub enum MovePacket {
443    /// `flying` (C03): only on-ground.
444    Flying {
445        /// On ground.
446        on_ground: bool,
447    },
448    /// `position` (C04).
449    Position {
450        /// Feet position.
451        pos: Vec3,
452        /// On ground.
453        on_ground: bool,
454    },
455    /// `look` (C05).
456    Look {
457        /// Yaw.
458        yaw: f32,
459        /// Pitch.
460        pitch: f32,
461        /// On ground.
462        on_ground: bool,
463    },
464    /// `position_look` (C06).
465    PositionLook {
466        /// Feet position.
467        pos: Vec3,
468        /// Yaw.
469        yaw: f32,
470        /// Pitch.
471        pitch: f32,
472        /// On ground.
473        on_ground: bool,
474    },
475}
476
477impl MovePacket {
478    /// The 1.8 trace name (`C03` … `C06`).
479    #[must_use]
480    pub fn trace_id(&self) -> &'static str {
481        match self {
482            Self::Flying { .. } => "C03",
483            Self::Position { .. } => "C04",
484            Self::Look { .. } => "C05",
485            Self::PositionLook { .. } => "C06",
486        }
487    }
488}
489
490/// Client settings (`settings` packet), sent right after join game like vanilla.
491#[derive(Debug, Clone, PartialEq)]
492#[non_exhaustive]
493pub struct ClientSettings {
494    /// Locale, e.g. `en_US` (1.8) / `en_us` (1.16).
495    pub locale: String,
496    /// View distance in chunks.
497    pub view_distance: i8,
498    /// Chat visibility: 0 full, 1 commands only, 2 hidden.
499    pub chat_mode: i8,
500    /// Chat colours.
501    pub chat_colors: bool,
502    /// Displayed skin parts (bit mask; 0x7f = all).
503    pub skin_parts: u8,
504    /// Main hand (1.9+): 0 left, 1 right.
505    pub main_hand: i32,
506}
507
508impl ClientSettings {
509    /// The locale, e.g. `en_US` (1.8) / `en_us` (1.16).
510    #[must_use]
511    pub fn with_locale(mut self, locale: impl Into<String>) -> Self {
512        self.locale = locale.into();
513        self
514    }
515
516    /// The view distance in chunks.
517    #[must_use]
518    pub const fn with_view_distance(mut self, chunks: i8) -> Self {
519        self.view_distance = chunks;
520        self
521    }
522
523    /// Chat visibility: 0 full, 1 commands only, 2 hidden.
524    #[must_use]
525    pub const fn with_chat_mode(mut self, mode: i8) -> Self {
526        self.chat_mode = mode;
527        self
528    }
529
530    /// Chat colours.
531    #[must_use]
532    pub const fn with_chat_colors(mut self, on: bool) -> Self {
533        self.chat_colors = on;
534        self
535    }
536
537    /// Displayed skin parts (bit mask; 0x7f = all).
538    #[must_use]
539    pub const fn with_skin_parts(mut self, mask: u8) -> Self {
540        self.skin_parts = mask;
541        self
542    }
543
544    /// Main hand (1.9+): 0 left, 1 right.
545    #[must_use]
546    pub const fn with_main_hand(mut self, hand: i32) -> Self {
547        self.main_hand = hand;
548        self
549    }
550}
551
552impl Default for ClientSettings {
553    fn default() -> Self {
554        ClientSettings {
555            locale: "en_US".to_owned(),
556            view_distance: 8,
557            chat_mode: 0,
558            chat_colors: true,
559            skin_parts: 0x7f,
560            main_hand: 1,
561        }
562    }
563}
564
565/// A block face, as in the dig / place packets.
566#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
567pub enum Face {
568    /// −Y (0).
569    Down,
570    /// +Y (1).
571    Up,
572    /// −Z (2).
573    North,
574    /// +Z (3).
575    South,
576    /// −X (4).
577    West,
578    /// +X (5).
579    East,
580}
581
582impl Face {
583    /// The wire value 0 … 5.
584    #[must_use]
585    pub const fn id(self) -> i8 {
586        match self {
587            Face::Down => 0,
588            Face::Up => 1,
589            Face::North => 2,
590            Face::South => 3,
591            Face::West => 4,
592            Face::East => 5,
593        }
594    }
595
596    /// From the wire value.
597    #[must_use]
598    pub const fn from_id(id: i8) -> Option<Face> {
599        Some(match id {
600            0 => Face::Down,
601            1 => Face::Up,
602            2 => Face::North,
603            3 => Face::South,
604            4 => Face::West,
605            5 => Face::East,
606            _ => return None,
607        })
608    }
609
610    /// The neighbouring block in this direction.
611    #[must_use]
612    pub const fn offset(self, p: ghast_protocol::BlockPos) -> ghast_protocol::BlockPos {
613        let (x, y, z) = match self {
614            Face::Down => (0, -1, 0),
615            Face::Up => (0, 1, 0),
616            Face::North => (0, 0, -1),
617            Face::South => (0, 0, 1),
618            Face::West => (-1, 0, 0),
619            Face::East => (1, 0, 0),
620        };
621        ghast_protocol::BlockPos::new(p.x + x, p.y + y, p.z + z)
622    }
623}
624
625/// The `use_entity` action.
626#[derive(Debug, Clone, Copy, PartialEq)]
627#[non_exhaustive]
628pub enum EntityUse {
629    /// Right click (0).
630    Interact,
631    /// Left click (1).
632    Attack,
633    /// Right click at a point relative to the entity position (2).
634    InteractAt(Vec3),
635}
636
637/// `block_dig` status values used by the client.
638#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
639#[non_exhaustive]
640pub enum DigStatus {
641    /// Start destroying a block (0).
642    Start,
643    /// Abort destroying (1).
644    Abort,
645    /// Finish destroying (2).
646    Finish,
647    /// Release the used item: stop eating / blocking / drawing (5).
648    ReleaseUseItem,
649}
650
651impl DigStatus {
652    /// The wire value.
653    #[must_use]
654    pub const fn id(self) -> i32 {
655        match self {
656            DigStatus::Start => 0,
657            DigStatus::Abort => 1,
658            DigStatus::Finish => 2,
659            DigStatus::ReleaseUseItem => 5,
660        }
661    }
662}
663
664/// Serverbound play packets the core writes, common to every version.
665#[derive(Debug, Clone, PartialEq)]
666#[non_exhaustive]
667pub enum Outbound {
668    /// Keep-alive answer.
669    KeepAlive(i64),
670    /// Teleport confirm (1.9+; never produced for 1.8).
671    TeleportConfirm(i32),
672    /// The movement packet.
673    Move(MovePacket),
674    /// Sneak / sprint change.
675    EntityAction {
676        /// Our entity id.
677        entity_id: i32,
678        /// The action.
679        action: EntityAction,
680    },
681    /// Chat line or command (already truncated to the version's limit).
682    Chat(String),
683    /// Respawn request (client command "perform respawn").
684    Respawn,
685    /// Window confirm transaction answer.
686    Transaction {
687        /// Window id.
688        window_id: i8,
689        /// Action number.
690        action: i16,
691        /// Accepted.
692        accepted: bool,
693    },
694    /// Abilities (sent when the client toggles flight).
695    Abilities {
696        /// Flags (1 invulnerable, 2 flying, 4 may fly, 8 creative).
697        flags: i8,
698        /// Flying speed.
699        flying_speed: f32,
700        /// Walking speed.
701        walking_speed: f32,
702    },
703    /// Select a hotbar slot (0 … 8).
704    HeldSlot(i16),
705    /// Arm swing (main hand).
706    Swing,
707    /// `block_dig`.
708    Dig {
709        /// Status.
710        status: DigStatus,
711        /// Block.
712        pos: ghast_protocol::BlockPos,
713        /// Face.
714        face: Face,
715    },
716    /// `block_place` on a block face (1.8 with the held stack; 1.16 main hand).
717    Place {
718        /// The block clicked.
719        pos: ghast_protocol::BlockPos,
720        /// The face clicked.
721        face: Face,
722        /// Hit point relative to the block (floats, 0 … 1).
723        cursor: Vec3,
724        /// The held stack (1.8 sends it; vanilla servers / Grim compare it with the hand).
725        held: Option<ghast_inventory::Stack>,
726        /// 1.9+: the off hand.
727        off_hand: bool,
728    },
729    /// Use the held item (1.8 `block_place` at −1/−1/−1 face 255 with the held stack; 1.16
730    /// `use_item`).
731    UseItem {
732        /// The held stack.
733        held: Option<ghast_inventory::Stack>,
734        /// 1.9+: the off hand.
735        off_hand: bool,
736    },
737    /// 1.9+: off-hand arm swing.
738    SwingOffHand,
739    /// `use_entity`: attack or interact with an entity.
740    UseEntity {
741        /// The entity.
742        target: i32,
743        /// What to do.
744        action: EntityUse,
745        /// 1.9+: the off hand (interactions).
746        off_hand: bool,
747        /// 1.16: the sneak key is held.
748        sneaking: bool,
749    },
750    /// Client settings.
751    Settings(ClientSettings),
752    /// A packet of the inventory (window click, confirm answer, close, open inventory).
753    Inventory(ghast_inventory::Outgoing),
754    /// The client brand plugin message (`MC|Brand` on 1.8, `minecraft:brand` on 1.13+).
755    Brand(String),
756}