1use 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 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 .with(cb::id::ENTITY_VELOCITY)
102 .with(cb::id::ENTITY_STATUS)
104 .with(cb::id::UPDATE_ATTRIBUTES)
106 .with(cb::id::ENTITY_EFFECT)
107 .with(cb::id::REMOVE_ENTITY_EFFECT)
108 .with(cb::id::EXPLOSION);
109 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 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 pose.yaw = yaw % 360.0;
438 pose.pitch = pitch % 360.0;
439 }
440}
441
442fn 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
458fn 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}