Skip to main content

ghast_client/version/
v1_8.rs

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