ghast_client/version/
mod.rs1use 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#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
23#[non_exhaustive]
24pub enum GameVersion {
25 V1_8_9,
27 V1_16_5,
29}
30
31impl GameVersion {
32 pub const ALL: [GameVersion; 2] = [GameVersion::V1_8_9, GameVersion::V1_16_5];
34
35 #[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 #[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 #[must_use]
56 pub fn eye_height(self) -> f64 {
57 f64::from(1.62_f32)
58 }
59
60 #[must_use]
63 pub fn sneak_eye_height(self) -> f64 {
64 match self {
65 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#[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 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
97pub trait ClientVersion: Version + sealed::Sealed {
101 const GAME: GameVersion;
103 const CHAT_LIMIT: usize;
105
106 fn write_handshake(enc: &mut FrameEncoder, host: &str, port: u16) -> Result<(), EncodeError>;
108 fn write_login_start(enc: &mut FrameEncoder, username: &str) -> Result<(), EncodeError>;
110 fn write_encryption_response(
112 enc: &mut FrameEncoder,
113 shared_secret: &[u8],
114 verify_token: &[u8],
115 ) -> Result<(), EncodeError>;
116 fn write_plugin_response(enc: &mut FrameEncoder, message_id: i32) -> Result<(), EncodeError>;
118 fn decode_login(raw: &RawPacket) -> Result<Option<LoginInbound>, DecodeError>;
120
121 fn play_filter(interests: Interests) -> PacketFilter;
124 fn decode_play(raw: &RawPacket) -> Result<Option<Inbound>, DecodeError>;
126 fn inventory_packet(
130 inv: &mut ghast_inventory::Inventory,
131 raw: &RawPacket,
132 ) -> Result<Option<ghast_inventory::Outgoing>, DecodeError>;
133 const INVENTORY: ghast_inventory::Version;
135 fn write_play(enc: &mut FrameEncoder, out: &Outbound) -> Result<(), EncodeError>;
137 fn apply_teleport(pose: &mut Pose, t: &Teleport);
140}
141
142pub(crate) trait HasSim {
144 type Sim: crate::sim::Sim;
146 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
156pub(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
169pub(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 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}