Skip to main content

ghast_client/version/
mod.rs

1//! Per-version glue between the generated packets of `ghast-protocol` and the common layer.
2//!
3//! [`ClientVersion`] is implemented for the protocol marker types ([`V1_8`], [`V1_16_5`]).
4//! The connection and its tick loop are generic over it, so every hot path is monomorphised
5//! per version (no trait objects); the bot API above it only sees [`crate::common`] types.
6
7use std::fmt;
8use std::str::FromStr;
9
10use ghast_protocol::{DecodeError, EncodeError, FrameEncoder, PacketFilter, RawPacket, Version};
11
12use crate::common::{Inbound, LoginInbound, Outbound, Pose, Teleport};
13use crate::interest::Interests;
14
15mod v1_16_5;
16mod v1_8;
17
18pub use ghast_protocol::v1_8::V1_8;
19pub use ghast_protocol::v1_16_5::V1_16_5;
20
21/// A protocol version the client supports, chosen at run time.
22#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
23#[non_exhaustive]
24pub enum GameVersion {
25    /// 1.8 – 1.8.9 (protocol 47).
26    V1_8_9,
27    /// 1.16.4 – 1.16.5 (protocol 754).
28    V1_16_5,
29}
30
31impl GameVersion {
32    /// Every supported version.
33    pub const ALL: [GameVersion; 2] = [GameVersion::V1_8_9, GameVersion::V1_16_5];
34
35    /// Protocol number.
36    #[must_use]
37    pub const fn protocol(self) -> i32 {
38        match self {
39            Self::V1_8_9 => <V1_8 as Version>::PROTOCOL,
40            Self::V1_16_5 => <V1_16_5 as Version>::PROTOCOL,
41        }
42    }
43
44    /// Display name (`"1.8.9"`, `"1.16.5"`).
45    #[must_use]
46    pub const fn name(self) -> &'static str {
47        match self {
48            Self::V1_8_9 => "1.8.9",
49            Self::V1_16_5 => "1.16.5",
50        }
51    }
52
53    /// Eye height above the feet while standing: `1.62F` widened to double (every supported
54    /// version).
55    #[must_use]
56    pub fn eye_height(self) -> f64 {
57        f64::from(1.62_f32)
58    }
59
60    /// Eye height while sneaking: 1.54 in 1.8 (`getEyeHeight` − 0.08), 1.27 in 1.16 (the
61    /// crouching pose).
62    #[must_use]
63    pub fn sneak_eye_height(self) -> f64 {
64        match self {
65            // `1.62F - 0.08F` in float.
66            Self::V1_8_9 => f64::from(1.62_f32 - 0.08_f32),
67            Self::V1_16_5 => f64::from(1.27_f32),
68        }
69    }
70}
71
72impl fmt::Display for GameVersion {
73    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
74        f.write_str(self.name())
75    }
76}
77
78/// Error parsing a [`GameVersion`].
79#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
80#[error("unsupported Minecraft version `{0}` (supported: 1.8.9, 1.16.5)")]
81pub struct UnknownVersion(pub String);
82
83impl FromStr for GameVersion {
84    type Err = UnknownVersion;
85
86    /// Accepts `1.8`, `1.8.x`, `47`, `1.16.4`, `1.16.5` and `754`.
87    fn from_str(s: &str) -> Result<Self, Self::Err> {
88        match s.trim() {
89            "1.8" | "1.8.0" | "1.8.1" | "1.8.2" | "1.8.3" | "1.8.4" | "1.8.5" | "1.8.6"
90            | "1.8.7" | "1.8.8" | "1.8.9" | "47" => Ok(Self::V1_8_9),
91            "1.16.4" | "1.16.5" | "754" => Ok(Self::V1_16_5),
92            other => Err(UnknownVersion(other.to_owned())),
93        }
94    }
95}
96
97/// What a protocol version needs to provide to the client.
98///
99/// Sealed: implemented for [`V1_8`] and [`V1_16_5`] only.
100pub trait ClientVersion: Version + sealed::Sealed {
101    /// The run-time tag of this version.
102    const GAME: GameVersion;
103    /// Longest chat line the server accepts, in UTF-16 units (vanilla truncates to it).
104    const CHAT_LIMIT: usize;
105
106    /// Queues the handshake packet (next state = login).
107    fn write_handshake(enc: &mut FrameEncoder, host: &str, port: u16) -> Result<(), EncodeError>;
108    /// Queues the login start packet.
109    fn write_login_start(enc: &mut FrameEncoder, username: &str) -> Result<(), EncodeError>;
110    /// Queues the encryption response.
111    fn write_encryption_response(
112        enc: &mut FrameEncoder,
113        shared_secret: &[u8],
114        verify_token: &[u8],
115    ) -> Result<(), EncodeError>;
116    /// Queues a "not understood" answer to a login plugin request.
117    fn write_plugin_response(enc: &mut FrameEncoder, message_id: i32) -> Result<(), EncodeError>;
118    /// Decodes a login-state packet; `Ok(None)` for packets the client ignores.
119    fn decode_login(raw: &RawPacket) -> Result<Option<LoginInbound>, DecodeError>;
120
121    /// The play-state packet filter for the given interests: everything the core needs plus
122    /// what the interests ask for. Everything else is dropped before it is decoded.
123    fn play_filter(interests: Interests) -> PacketFilter;
124    /// Decodes a play-state packet; `Ok(None)` for packets the core does not use.
125    fn decode_play(raw: &RawPacket) -> Result<Option<Inbound>, DecodeError>;
126    /// Applies an inventory packet (windows, slots, confirms, held slot, login / respawn
127    /// reset) to the inventory; returns the confirm answer to send now, if any. Other packets
128    /// are ignored.
129    fn inventory_packet(
130        inv: &mut ghast_inventory::Inventory,
131        raw: &RawPacket,
132    ) -> Result<Option<ghast_inventory::Outgoing>, DecodeError>;
133    /// The inventory version.
134    const INVENTORY: ghast_inventory::Version;
135    /// Queues a play-state packet.
136    fn write_play(enc: &mut FrameEncoder, out: &Outbound) -> Result<(), EncodeError>;
137    /// Applies a server teleport to the pose the way this version's client does
138    /// (`onPlayerPositionLook` + `Entity.updatePositionAndAngles`).
139    fn apply_teleport(pose: &mut Pose, t: &Teleport);
140}
141
142/// The world and movement implementation of a version (crate-internal).
143pub(crate) trait HasSim {
144    /// World + mover.
145    type Sim: crate::sim::Sim;
146    /// A fresh world and player.
147    fn new_sim(opts: &crate::sim::SimOptions) -> Self::Sim;
148}
149
150mod sealed {
151    pub trait Sealed {}
152    impl Sealed for super::V1_8 {}
153    impl Sealed for super::V1_16_5 {}
154}
155
156/// Truncates `s` to at most `max` UTF-16 units (Java's `String.length()`), at a character
157/// boundary.
158pub(crate) fn truncate_utf16(s: &str, max: usize) -> &str {
159    let mut units = 0;
160    for (i, c) in s.char_indices() {
161        units += c.len_utf16();
162        if units > max {
163            return &s[..i];
164        }
165    }
166    s
167}
168
169/// The common part of vanilla's teleport handling: relative axes are added, absolute axes
170/// zero that axis' motion; rotations are added when relative. Returns the new yaw / pitch
171/// before the version's `updatePositionAndAngles` normalisation.
172pub(crate) fn teleport_target(pose: &mut Pose, t: &Teleport) -> (f32, f32) {
173    use crate::common::teleport_flags as fl;
174    let mut x = t.x;
175    let mut y = t.y;
176    let mut z = t.z;
177    if t.flags & fl::X != 0 {
178        x += pose.pos.x;
179    } else {
180        pose.motion.x = 0.0;
181    }
182    if t.flags & fl::Y != 0 {
183        y += pose.pos.y;
184    } else {
185        pose.motion.y = 0.0;
186    }
187    if t.flags & fl::Z != 0 {
188        z += pose.pos.z;
189    } else {
190        pose.motion.z = 0.0;
191    }
192    pose.pos = crate::common::Vec3::new(x, y, z);
193    let mut yaw = t.yaw;
194    let mut pitch = t.pitch;
195    if t.flags & fl::PITCH != 0 {
196        pitch += pose.pitch;
197    }
198    if t.flags & fl::YAW != 0 {
199        yaw += pose.yaw;
200    }
201    (yaw, pitch)
202}
203
204#[cfg(test)]
205mod tests {
206    use super::*;
207
208    #[test]
209    fn parse_versions() {
210        assert_eq!("1.8.8".parse::<GameVersion>().unwrap(), GameVersion::V1_8_9);
211        assert_eq!("754".parse::<GameVersion>().unwrap(), GameVersion::V1_16_5);
212        assert!("1.12.2".parse::<GameVersion>().is_err());
213        assert_eq!(GameVersion::V1_8_9.protocol(), 47);
214        assert_eq!(GameVersion::V1_16_5.protocol(), 754);
215    }
216
217    #[test]
218    fn utf16_truncation() {
219        assert_eq!(truncate_utf16("hello", 3), "hel");
220        assert_eq!(truncate_utf16("hello", 10), "hello");
221        // U+1F600 is two UTF-16 units: never split.
222        assert_eq!(truncate_utf16("a\u{1F600}b", 2), "a");
223        assert_eq!(truncate_utf16("a\u{1F600}b", 3), "a\u{1F600}");
224    }
225}