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}