Skip to main content

ghast_client/version/
v1_16_5.rs

1//! Protocol 754 (1.16.4 – 1.16.5).
2
3use bytes::BytesMut;
4use ghast_protocol::types::ByteString;
5use ghast_protocol::v1_16_5::{handshaking, login, play};
6use ghast_protocol::{
7    DecodeError, EncodeError, FrameEncoder, McWrite, PacketFilter, PacketSet, RawPacket,
8};
9
10use super::{ClientVersion, GameVersion, V1_16_5, teleport_target, truncate_utf16};
11use crate::common::{
12    Abilities, ChatMessage, ChatPosition, EntityAction, GameMode, Health, Inbound, JoinGame,
13    LoginInbound, MovePacket, Outbound, Pose, Teleport, Vec3,
14};
15use crate::interest::Interests;
16
17use play::clientbound as cb;
18use play::serverbound as sb;
19
20/// `combat_event` event id for "entity dead".
21const COMBAT_ENTITY_DEAD: i32 = 2;
22
23impl ClientVersion for V1_16_5 {
24    const GAME: GameVersion = GameVersion::V1_16_5;
25    const CHAT_LIMIT: usize = 256;
26
27    fn write_handshake(enc: &mut FrameEncoder, host: &str, port: u16) -> Result<(), EncodeError> {
28        enc.write_packet(&handshaking::serverbound::SetProtocol {
29            protocol_version: 754,
30            server_host: ByteString::from(host),
31            server_port: port,
32            next_state: 2,
33        })
34    }
35
36    fn write_login_start(enc: &mut FrameEncoder, username: &str) -> Result<(), EncodeError> {
37        enc.write_packet(&login::serverbound::LoginStart {
38            username: ByteString::from(username),
39        })
40    }
41
42    fn write_encryption_response(
43        enc: &mut FrameEncoder,
44        shared_secret: &[u8],
45        verify_token: &[u8],
46    ) -> Result<(), EncodeError> {
47        enc.write_packet(&login::serverbound::EncryptionBegin {
48            shared_secret: bytes::Bytes::copy_from_slice(shared_secret),
49            verify_token: bytes::Bytes::copy_from_slice(verify_token),
50        })
51    }
52
53    fn write_plugin_response(enc: &mut FrameEncoder, message_id: i32) -> Result<(), EncodeError> {
54        // Vanilla answers every login plugin request with "not understood" (no data).
55        enc.write_packet(&login::serverbound::LoginPluginResponse {
56            message_id,
57            data: None,
58        })
59    }
60
61    fn decode_login(raw: &RawPacket) -> Result<Option<LoginInbound>, DecodeError> {
62        use login::clientbound as lc;
63        Ok(Some(match raw.id {
64            lc::id::DISCONNECT => {
65                LoginInbound::Disconnect(raw.decode_as::<lc::Disconnect>()?.reason)
66            }
67            lc::id::ENCRYPTION_BEGIN => {
68                let p: lc::EncryptionBegin = raw.decode_as()?;
69                LoginInbound::EncryptionRequest {
70                    server_id: p.server_id,
71                    public_key: p.public_key,
72                    verify_token: p.verify_token,
73                }
74            }
75            lc::id::SUCCESS => {
76                let p: lc::Success = raw.decode_as()?;
77                LoginInbound::Success {
78                    uuid: Some(p.uuid),
79                    username: p.username,
80                }
81            }
82            lc::id::COMPRESS => LoginInbound::Compress(raw.decode_as::<lc::Compress>()?.threshold),
83            lc::id::LOGIN_PLUGIN_REQUEST => {
84                let p: lc::LoginPluginRequest = raw.decode_as()?;
85                LoginInbound::PluginRequest {
86                    message_id: p.message_id,
87                    channel: p.channel,
88                }
89            }
90            _ => return Ok(None),
91        }))
92    }
93
94    fn play_filter(interests: Interests) -> PacketFilter {
95        let mut f = PacketFilter::none()
96            .with(cb::id::KEEP_ALIVE)
97            .with(cb::id::LOGIN)
98            .with(cb::id::UPDATE_HEALTH)
99            .with(cb::id::RESPAWN)
100            .with(cb::id::POSITION)
101            .with(cb::id::HELD_ITEM_SLOT)
102            .with(cb::id::GAME_STATE_CHANGE)
103            .with(cb::id::OPEN_WINDOW)
104            .with(cb::id::OPEN_HORSE_WINDOW)
105            .with(cb::id::CLOSE_WINDOW)
106            .with(cb::id::TRANSACTION)
107            .with(cb::id::WINDOW_ITEMS)
108            .with(cb::id::SET_SLOT)
109            .with(cb::id::CRAFT_PROGRESS_BAR)
110            .with(cb::id::ABILITIES)
111            .with(cb::id::KICK_DISCONNECT)
112            .with(cb::id::COMBAT_EVENT)
113            .with(cb::id::ENTITY_STATUS)
114            // Our speed attribute and status effects (physics, dig speed).
115            .with(cb::id::ENTITY_UPDATE_ATTRIBUTES)
116            .with(cb::id::ENTITY_EFFECT)
117            .with(cb::id::REMOVE_ENTITY_EFFECT)
118            // Knockback (every entity's velocity passes the id filter; only ours is applied).
119            .with(cb::id::ENTITY_VELOCITY);
120        // The world: chunks, view window, block changes, border (the 1.16 client player does
121        // not tick outside a loaded column).
122        for id in ghast_world::v1_16_5::PACKET_IDS {
123            f.insert(id);
124        }
125        if interests.contains(Interests::CHAT) {
126            f.insert(cb::id::CHAT);
127        }
128        f
129    }
130
131    fn decode_play(raw: &RawPacket) -> Result<Option<Inbound>, DecodeError> {
132        Ok(Some(match raw.id {
133            cb::id::KEEP_ALIVE => {
134                Inbound::KeepAlive(raw.decode_as::<cb::KeepAlive>()?.keep_alive_id)
135            }
136            cb::id::LOGIN => {
137                let p: cb::Login = raw.decode_as()?;
138                Inbound::JoinGame(JoinGame {
139                    entity_id: p.entity_id,
140                    game_mode: GameMode::from_wire(p.game_mode),
141                    dimension: p.world_name.to_string(),
142                    hardcore: p.is_hardcore,
143                    dimension_nbt: Some((p.dimension_codec, p.dimension)),
144                    respawn_screen: p.enable_respawn_screen,
145                })
146            }
147            cb::id::CHAT => {
148                let p: cb::Chat = raw.decode_as()?;
149                Inbound::Chat(ChatMessage {
150                    json: p.message,
151                    position: ChatPosition::from_wire(p.position),
152                    sender: (!p.sender.is_nil()).then_some(p.sender),
153                })
154            }
155            cb::id::UPDATE_HEALTH => {
156                let p: cb::UpdateHealth = raw.decode_as()?;
157                Inbound::Health(Health {
158                    health: p.health,
159                    food: p.food,
160                    saturation: p.food_saturation,
161                })
162            }
163            cb::id::RESPAWN => {
164                let p: cb::Respawn = raw.decode_as()?;
165                Inbound::Respawn {
166                    dimension: p.world_name.to_string(),
167                    game_mode: GameMode::from_wire(p.gamemode),
168                }
169            }
170            cb::id::POSITION => {
171                let p: cb::Position = raw.decode_as()?;
172                Inbound::Teleport(Teleport {
173                    x: p.x,
174                    y: p.y,
175                    z: p.z,
176                    yaw: p.yaw,
177                    pitch: p.pitch,
178                    flags: p.flags,
179                    teleport_id: Some(p.teleport_id),
180                })
181            }
182            cb::id::HELD_ITEM_SLOT => Inbound::HeldSlot(raw.decode_as::<cb::HeldItemSlot>()?.slot),
183            cb::id::GAME_STATE_CHANGE => {
184                let p: cb::GameStateChange = raw.decode_as()?;
185                Inbound::GameState {
186                    reason: p.reason.0,
187                    value: p.game_mode,
188                }
189            }
190            cb::id::OPEN_WINDOW => {
191                Inbound::OpenWindow(raw.decode_as::<cb::OpenWindow>()?.window_id)
192            }
193            cb::id::OPEN_HORSE_WINDOW => {
194                Inbound::OpenWindow(i32::from(raw.decode_as::<cb::OpenHorseWindow>()?.window_id))
195            }
196            cb::id::CLOSE_WINDOW => {
197                Inbound::CloseWindow(i32::from(raw.decode_as::<cb::CloseWindow>()?.window_id))
198            }
199            cb::id::TRANSACTION => {
200                let p: cb::Transaction = raw.decode_as()?;
201                Inbound::Transaction {
202                    window_id: p.window_id,
203                    action: p.action,
204                    accepted: p.accepted,
205                }
206            }
207            cb::id::ABILITIES => {
208                let p: cb::Abilities = raw.decode_as()?;
209                Inbound::Abilities(Abilities {
210                    flags: p.flags,
211                    flying_speed: p.flying_speed,
212                    walking_speed: p.walking_speed,
213                })
214            }
215            cb::id::KICK_DISCONNECT => {
216                Inbound::Disconnect(raw.decode_as::<cb::KickDisconnect>()?.reason)
217            }
218            cb::id::ENTITY_VELOCITY => {
219                let p: cb::EntityVelocity = raw.decode_as()?;
220                Inbound::Velocity {
221                    entity_id: p.entity_id,
222                    motion: Vec3::new(
223                        f64::from(p.velocity.x) / 8000.0,
224                        f64::from(p.velocity.y) / 8000.0,
225                        f64::from(p.velocity.z) / 8000.0,
226                    ),
227                }
228            }
229            cb::id::EXPLOSION => {
230                let p: cb::Explosion = raw.decode_as()?;
231                Inbound::ExplosionPush(Vec3::new(
232                    f64::from(p.player_motion_x),
233                    f64::from(p.player_motion_y),
234                    f64::from(p.player_motion_z),
235                ))
236            }
237            cb::id::ENTITY_UPDATE_ATTRIBUTES => {
238                let p: cb::EntityUpdateAttributes = raw.decode_as()?;
239                let Some(speed) = p.properties.iter().find(|a| {
240                    a.key.ends_with("generic.movementSpeed")
241                        || a.key.ends_with("generic.movement_speed")
242                }) else {
243                    return Ok(None);
244                };
245                Inbound::MovementSpeed {
246                    entity_id: p.entity_id,
247                    base: speed.value,
248                    modifiers: speed
249                        .modifiers
250                        .iter()
251                        .map(|m| (m.uuid, m.amount, m.operation))
252                        .collect(),
253                }
254            }
255            cb::id::ENTITY_EFFECT => {
256                let p: cb::EntityEffect = raw.decode_as()?;
257                Inbound::Effect {
258                    entity_id: p.entity_id,
259                    id: p.effect_id,
260                    amplifier: p.amplifier,
261                }
262            }
263            cb::id::REMOVE_ENTITY_EFFECT => {
264                let p: cb::RemoveEntityEffect = raw.decode_as()?;
265                Inbound::EffectRemoved {
266                    entity_id: p.entity_id,
267                    id: p.effect_id,
268                }
269            }
270            cb::id::ENTITY_STATUS => {
271                let p: cb::EntityStatus = raw.decode_as()?;
272                Inbound::EntityStatus {
273                    entity_id: p.entity_id,
274                    status: p.entity_status,
275                }
276            }
277            cb::id::COMBAT_EVENT => {
278                let p: cb::CombatEvent = raw.decode_as()?;
279                match (p.event, p.player_id) {
280                    (COMBAT_ENTITY_DEAD, Some(entity_id)) => Inbound::Died { entity_id },
281                    _ => return Ok(None),
282                }
283            }
284            _ => return Ok(None),
285        }))
286    }
287
288    const INVENTORY: ghast_inventory::Version = ghast_inventory::Version::V1_16_5;
289
290    fn inventory_packet(
291        inv: &mut ghast_inventory::Inventory,
292        raw: &RawPacket,
293    ) -> Result<Option<ghast_inventory::Outgoing>, DecodeError> {
294        match raw.id {
295            cb::id::LOGIN
296            | cb::id::RESPAWN
297            | cb::id::OPEN_WINDOW
298            | cb::id::CLOSE_WINDOW
299            | cb::id::WINDOW_ITEMS
300            | cb::id::SET_SLOT
301            | cb::id::TRANSACTION
302            | cb::id::HELD_ITEM_SLOT
303            | cb::id::CRAFT_PROGRESS_BAR
304            | cb::id::OPEN_HORSE_WINDOW => {
305                let p = cb::ClientboundPlayPacket::decode_raw(raw)?;
306                Ok(inv.apply_v1_16_5(&p))
307            }
308            _ => Ok(None),
309        }
310    }
311
312    fn write_play(enc: &mut FrameEncoder, out: &Outbound) -> Result<(), EncodeError> {
313        match out {
314            Outbound::KeepAlive(id) => enc.write_packet(&sb::KeepAlive { keep_alive_id: *id }),
315            Outbound::TeleportConfirm(teleport_id) => enc.write_packet(&sb::TeleportConfirm {
316                teleport_id: *teleport_id,
317            }),
318            Outbound::Move(m) => match *m {
319                MovePacket::Flying { on_ground } => enc.write_packet(&sb::Flying { on_ground }),
320                MovePacket::Position { pos, on_ground } => enc.write_packet(&sb::Position {
321                    x: pos.x,
322                    y: pos.y,
323                    z: pos.z,
324                    on_ground,
325                }),
326                MovePacket::Look {
327                    yaw,
328                    pitch,
329                    on_ground,
330                } => enc.write_packet(&sb::Look {
331                    yaw,
332                    pitch,
333                    on_ground,
334                }),
335                MovePacket::PositionLook {
336                    pos,
337                    yaw,
338                    pitch,
339                    on_ground,
340                } => enc.write_packet(&sb::PositionLook {
341                    x: pos.x,
342                    y: pos.y,
343                    z: pos.z,
344                    yaw,
345                    pitch,
346                    on_ground,
347                }),
348            },
349            Outbound::EntityAction { entity_id, action } => {
350                let action_id = match action {
351                    EntityAction::StartSneaking => sb::EntityActionActionId::START_SNEAKING,
352                    EntityAction::StopSneaking => sb::EntityActionActionId::STOP_SNEAKING,
353                    EntityAction::StartSprinting => sb::EntityActionActionId::START_SPRINTING,
354                    EntityAction::StopSprinting => sb::EntityActionActionId::STOP_SPRINTING,
355                };
356                enc.write_packet(&sb::EntityAction {
357                    entity_id: *entity_id,
358                    action_id,
359                    jump_boost: 0,
360                })
361            }
362            Outbound::Chat(text) => enc.write_packet(&sb::Chat {
363                message: ByteString::from(truncate_utf16(text, Self::CHAT_LIMIT)),
364            }),
365            Outbound::Respawn => enc.write_packet(&sb::ClientCommand {
366                action_id: sb::ClientCommandActionId::PERFORM_RESPAWN,
367            }),
368            Outbound::Transaction {
369                window_id,
370                action,
371                accepted,
372            } => enc.write_packet(&sb::Transaction {
373                window_id: *window_id,
374                action: *action,
375                accepted: *accepted,
376            }),
377            Outbound::Abilities { flags, .. } => enc.write_packet(&sb::Abilities { flags: *flags }),
378            Outbound::Inventory(o) => match o.to_v1_16_5() {
379                Some(p) => enc.write_set(&p),
380                None => Ok(()),
381            },
382            Outbound::UseEntity {
383                target,
384                action,
385                off_hand,
386                sneaking,
387            } => {
388                let (mouse, at) = match action {
389                    crate::common::EntityUse::Interact => (0, None),
390                    crate::common::EntityUse::Attack => (1, None),
391                    crate::common::EntityUse::InteractAt(v) => (2, Some(*v)),
392                };
393                enc.write_packet(&sb::UseEntity {
394                    target: *target,
395                    mouse,
396                    x: at.map(|v| v.x as f32),
397                    y: at.map(|v| v.y as f32),
398                    z: at.map(|v| v.z as f32),
399                    hand: match action {
400                        crate::common::EntityUse::Attack => None,
401                        _ => Some(i32::from(*off_hand)),
402                    },
403                    sneaking: *sneaking,
404                })
405            }
406            Outbound::HeldSlot(slot) => enc.write_packet(&sb::HeldItemSlot { slot_id: *slot }),
407            Outbound::Swing => enc.write_packet(&sb::ArmAnimation { hand: 0 }),
408            Outbound::Dig { status, pos, face } => enc.write_packet(&sb::BlockDig {
409                status: status.id(),
410                location: *pos,
411                face: face.id(),
412            }),
413            Outbound::Place {
414                pos,
415                face,
416                cursor,
417                off_hand,
418                ..
419            } => enc.write_packet(&sb::BlockPlace {
420                hand: i32::from(*off_hand),
421                location: *pos,
422                direction: i32::from(face.id()),
423                cursor_x: cursor.x as f32,
424                cursor_y: cursor.y as f32,
425                cursor_z: cursor.z as f32,
426                inside_block: false,
427            }),
428            Outbound::UseItem { off_hand, .. } => enc.write_packet(&sb::UseItem {
429                hand: i32::from(*off_hand),
430            }),
431            Outbound::SwingOffHand => enc.write_packet(&sb::ArmAnimation { hand: 1 }),
432            Outbound::Settings(s) => enc.write_packet(&sb::Settings {
433                // 1.13+ locales are lower case (`en_us`).
434                locale: ByteString::from(s.locale.to_ascii_lowercase()),
435                view_distance: s.view_distance,
436                chat_flags: i32::from(s.chat_mode),
437                chat_colors: s.chat_colors,
438                skin_parts: s.skin_parts,
439                main_hand: s.main_hand,
440            }),
441            Outbound::Brand(brand) => {
442                let mut data = BytesMut::with_capacity(brand.len() + 1);
443                data.put_string(brand)?;
444                enc.write_packet(&sb::CustomPayload {
445                    channel: ByteString::from_static("minecraft:brand"),
446                    data: data.freeze(),
447                })
448            }
449        }
450    }
451
452    fn apply_teleport(pose: &mut Pose, t: &Teleport) {
453        let (yaw, pitch) = teleport_target(pose, t);
454        // Entity.updatePositionAndAngles (1.16): x / z clamped to ±3e7, pitch clamped to ±90,
455        // both angles `% 360`.
456        pose.pos.x = pose.pos.x.clamp(-3.0e7, 3.0e7);
457        pose.pos.z = pose.pos.z.clamp(-3.0e7, 3.0e7);
458        pose.yaw = yaw % 360.0;
459        pose.pitch = pitch.clamp(-90.0, 90.0) % 360.0;
460    }
461}
462
463impl super::HasSim for V1_16_5 {
464    type Sim = crate::sim::Sim16;
465    fn new_sim(opts: &crate::sim::SimOptions) -> Self::Sim {
466        crate::sim::Sim16::with(opts)
467    }
468}