1use std::future::Future;
4use std::pin::Pin;
5use std::sync::Arc;
6use std::time::Duration;
7
8use ghast_auth::connect::{Connector, TargetAddr};
9use ghast_auth::offline::OfflineProfile;
10use ghast_protocol::{FrameDecoder, FrameEncoder, PacketFilter, RawPacket, State};
11use tokio::io::{AsyncRead, AsyncReadExt, AsyncWrite, AsyncWriteExt};
12use tokio::time::{Instant, MissedTickBehavior};
13
14use crate::common::{ClientSettings, Inbound, LoginInbound, chat_to_plain};
15use crate::engine::{Behavior, CoreOptions, Engine};
16use crate::error::{ClientError, SessionEnd};
17use crate::interest::Interests;
18use crate::sim::{Sim, SimError};
19use crate::version::{ClientVersion, GameVersion, HasSim, V1_8, V1_16_5};
20
21pub const TICK: Duration = Duration::from_millis(50);
23const READ_CHUNK: usize = 16 * 1024;
25
26#[derive(Clone)]
28#[non_exhaustive]
29pub enum Account {
30 Offline(OfflineProfile),
32 #[cfg(feature = "microsoft")]
34 Microsoft {
35 session: ghast_auth::microsoft::MinecraftSession,
37 auth: Arc<ghast_auth::microsoft::MicrosoftAuth>,
39 },
40}
41
42impl Account {
43 pub fn offline(name: &str) -> Result<Self, ClientError> {
45 Ok(Account::Offline(OfflineProfile::new(name)?))
46 }
47
48 #[must_use]
50 pub fn name(&self) -> &str {
51 match self {
52 Account::Offline(p) => p.name(),
53 #[cfg(feature = "microsoft")]
54 Account::Microsoft { session, .. } => &session.profile.name,
55 }
56 }
57}
58
59impl std::fmt::Debug for Account {
60 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
61 match self {
62 Account::Offline(p) => f.debug_tuple("Offline").field(&p.name()).finish(),
63 #[cfg(feature = "microsoft")]
64 Account::Microsoft { session, .. } => f
65 .debug_struct("Microsoft")
66 .field("name", &session.profile.name)
67 .finish_non_exhaustive(),
68 }
69 }
70}
71
72#[derive(Debug, Clone)]
78pub struct Client {
79 address: TargetAddr,
80 version: GameVersion,
81 account: Account,
82 connector: Connector,
83 settings: ClientSettings,
84 brand: Option<String>,
85 sensitivity: f32,
86 silent_hold: Duration,
87 read_timeout: Duration,
88 login_timeout: Duration,
89 record: Option<std::path::PathBuf>,
90 force_step_position: crate::sim::ForceStepPosition,
91 hold_turn_on_action: bool,
92 world_config: Option<ghast_world::WorldConfig>,
93}
94
95impl Client {
96 #[must_use]
99 pub fn new(address: TargetAddr, version: GameVersion, account: Account) -> Self {
100 Client {
101 address,
102 version,
103 account,
104 connector: Connector::new(),
105 settings: ClientSettings::default(),
106 brand: Some("vanilla".to_owned()),
107 sensitivity: 0.5,
108 silent_hold: Duration::from_millis(1500),
109 read_timeout: Duration::from_secs(30),
110 login_timeout: Duration::from_secs(30),
111 record: None,
112 force_step_position: crate::sim::ForceStepPosition::Off,
113 hold_turn_on_action: true,
114 world_config: None,
115 }
116 }
117
118 #[must_use]
120 pub fn connector(mut self, connector: Connector) -> Self {
121 self.connector = connector;
122 self
123 }
124
125 #[must_use]
127 pub fn settings(mut self, settings: ClientSettings) -> Self {
128 self.settings = settings;
129 self
130 }
131
132 #[must_use]
134 pub fn brand(mut self, brand: Option<String>) -> Self {
135 self.brand = brand;
136 self
137 }
138
139 #[must_use]
141 pub fn sensitivity(mut self, sensitivity: f32) -> Self {
142 self.sensitivity = sensitivity;
143 self
144 }
145
146 #[must_use]
148 pub fn silent_hold(mut self, d: Duration) -> Self {
149 self.silent_hold = d;
150 self
151 }
152
153 #[must_use]
156 pub fn read_timeout(mut self, d: Duration) -> Self {
157 self.read_timeout = d;
158 self
159 }
160
161 #[must_use]
163 pub fn login_timeout(mut self, d: Duration) -> Self {
164 self.login_timeout = d;
165 self
166 }
167
168 #[must_use]
171 pub fn record(mut self, path: impl Into<std::path::PathBuf>) -> Self {
172 self.record = Some(path.into());
173 self
174 }
175
176 #[must_use]
180 pub fn world_config(mut self, config: ghast_world::WorldConfig) -> Self {
181 self.world_config = Some(config);
182 self
183 }
184
185 #[must_use]
191 pub fn hold_turn_on_action(mut self, on: bool) -> Self {
192 self.hold_turn_on_action = on;
193 self
194 }
195
196 #[must_use]
201 pub fn force_step_position(mut self, on: bool) -> Self {
202 self.force_step_position = if on {
203 crate::sim::ForceStepPosition::Always
204 } else {
205 crate::sim::ForceStepPosition::Off
206 };
207 self
208 }
209
210 #[must_use]
215 pub fn force_step_position_mode(mut self, mode: crate::sim::ForceStepPosition) -> Self {
216 self.force_step_position = mode;
217 self
218 }
219
220 #[must_use]
222 pub fn address(&self) -> &TargetAddr {
223 &self.address
224 }
225
226 #[must_use]
228 pub fn version(&self) -> GameVersion {
229 self.version
230 }
231
232 #[must_use]
234 pub fn account(&self) -> &Account {
235 &self.account
236 }
237
238 pub async fn run<B: Behavior>(&self, behavior: &mut B) -> Result<SessionEnd, ClientError> {
241 self.run_until(behavior, std::future::pending()).await
242 }
243
244 pub async fn run_until<B, S>(
247 &self,
248 behavior: &mut B,
249 shutdown: S,
250 ) -> Result<SessionEnd, ClientError>
251 where
252 B: Behavior,
253 S: Future<Output = ()>,
254 {
255 let shutdown = std::pin::pin!(shutdown);
256 match self.version {
257 GameVersion::V1_8_9 => session::<V1_8, B, S>(self, behavior, shutdown).await,
258 GameVersion::V1_16_5 => session::<V1_16_5, B, S>(self, behavior, shutdown).await,
259 }
260 }
261}
262
263async fn read_more<R: AsyncRead + Unpin>(
265 rd: &mut R,
266 dec: &mut FrameDecoder,
267) -> std::io::Result<usize> {
268 let want = dec.needed().unwrap_or(0).max(READ_CHUNK);
269 let buf = dec.buffer_mut();
270 buf.reserve(want);
271 let old = buf.len();
272 let n = rd.read_buf(buf).await?;
273 dec.commit(old);
274 Ok(n)
275}
276
277async fn flush<W: AsyncWrite + Unpin>(wr: &mut W, enc: &mut FrameEncoder) -> std::io::Result<()> {
279 let out = enc.out_mut();
280 if !out.is_empty() {
281 wr.write_all_buf(out).await?;
282 }
283 Ok(())
284}
285
286async fn session<V, B, S>(
287 client: &Client,
288 behavior: &mut B,
289 mut shutdown: Pin<&mut S>,
290) -> Result<SessionEnd, ClientError>
291where
292 V: ClientVersion + HasSim,
293 B: Behavior,
294 S: Future<Output = ()>,
295{
296 let name: Arc<str> = client.account.name().into();
297 let mut dec = FrameDecoder::new();
298 let mut enc = FrameEncoder::new();
299
300 let login = async {
301 let stream = client.connector.connect(&client.address).await?;
302 let (mut rd, mut wr) = stream.into_split();
303 login::<V, _, _>(client, &mut rd, &mut wr, &mut dec, &mut enc).await?;
304 Ok::<_, ClientError>((rd, wr))
305 };
306 let (rd, wr) = tokio::select! {
307 r = tokio::time::timeout(client.login_timeout, login) => match r {
308 Ok(r) => r?,
309 Err(_) => return Err(ClientError::Timeout("connect and login")),
310 },
311 () = shutdown.as_mut() => return Ok(SessionEnd::Shutdown),
312 };
313 log::info!("[{name}] logged in to {} ({})", client.address, V::GAME);
314
315 let opts = CoreOptions {
316 name,
317 version: V::GAME,
318 settings: client.settings.clone(),
319 brand: client.brand.clone(),
320 sensitivity: client.sensitivity,
321 silent_hold: client.silent_hold,
322 inventory: V::INVENTORY,
323 hold_turn_on_action: client.hold_turn_on_action,
324 };
325 let mut play = Play::<V, _, _> {
326 rd,
327 wr,
328 dec,
329 enc,
330 engine: Engine::new(
331 opts,
332 V::new_sim(&crate::sim::SimOptions {
333 record: client.record.clone(),
334 force_step_position: client.force_step_position,
335 entities: entity_config(behavior.interests()),
336 world: client.world_config,
337 }),
338 ),
339 read_timeout: client.read_timeout,
340 interests: Interests::NONE,
341 raw_ids: Vec::new(),
342 raw_mask: [0; 4],
343 world_errors: 0,
344 _v: std::marker::PhantomData,
345 };
346 let result = play.run(behavior, shutdown).await;
347 let reason = match &result {
348 Ok(SessionEnd::Kicked { reason, .. }) => format!("kicked: {reason}"),
349 Ok(SessionEnd::ServerClosed) => "server closed the connection".to_owned(),
350 Ok(SessionEnd::Quit(r)) => format!("quit: {r}"),
351 Ok(SessionEnd::Shutdown) => "shutdown".to_owned(),
352 Err(e) => format!("error: {e}"),
353 };
354 play.engine.stop(behavior, &reason);
355 let stats = play.dec.stats();
356 log::info!(
357 "[{}] session ended after {} ticks ({reason}); frames {} skipped {} (compressed {}), inflated {} B",
358 play.engine.core_name(),
359 play.engine.tick_count(),
360 stats.frames,
361 stats.skipped,
362 stats.skipped_compressed,
363 stats.inflated_bytes
364 );
365 result
366}
367
368async fn login<V, R, W>(
370 client: &Client,
371 rd: &mut R,
372 wr: &mut W,
373 dec: &mut FrameDecoder,
374 enc: &mut FrameEncoder,
375) -> Result<(), ClientError>
376where
377 V: ClientVersion,
378 R: AsyncRead + Unpin,
379 W: AsyncWrite + Unpin,
380{
381 V::write_handshake(enc, &client.address.host(), client.address.port())?;
382 V::write_login_start(enc, client.account.name())?;
383 flush(wr, enc).await?;
384 loop {
385 while let Some(raw) = dec.next_packet()? {
386 let p = V::decode_login(&raw).map_err(|source| ClientError::Decode {
387 state: State::Login,
388 id: raw.id,
389 source,
390 })?;
391 match p {
392 None => {}
393 Some(LoginInbound::Disconnect(reason)) => {
394 return Err(ClientError::LoginRefused(chat_to_plain(&reason)));
395 }
396 Some(LoginInbound::Compress(threshold)) => {
397 dec.set_compression(threshold);
398 enc.set_compression(threshold);
399 }
400 Some(LoginInbound::PluginRequest {
401 message_id,
402 channel,
403 }) => {
404 log::debug!("login plugin request {message_id} on {channel}: not understood");
405 V::write_plugin_response(enc, message_id)?;
406 flush(wr, enc).await?;
407 }
408 Some(LoginInbound::EncryptionRequest {
409 server_id,
410 public_key,
411 verify_token,
412 }) => {
413 let secret =
414 encryption::<V>(client, enc, &server_id, &public_key, &verify_token)
415 .await?;
416 flush(wr, enc).await?;
417 enc.enable_encryption(&secret);
418 dec.enable_encryption(&secret);
419 }
420 Some(LoginInbound::Success { uuid, username }) => {
421 log::debug!("login success: {username} {uuid:?}");
422 return Ok(());
423 }
424 }
425 }
426 if read_more(rd, dec).await? == 0 {
427 return Err(ClientError::LoginClosed);
428 }
429 }
430}
431
432#[cfg_attr(
435 not(feature = "microsoft"),
436 allow(clippy::extra_unused_type_parameters)
437)]
438async fn encryption<V: ClientVersion>(
439 client: &Client,
440 enc: &mut FrameEncoder,
441 server_id: &str,
442 public_key: &[u8],
443 verify_token: &[u8],
444) -> Result<[u8; 16], ClientError> {
445 match &client.account {
446 Account::Offline(_) => {
447 let _ = (enc, server_id, public_key, verify_token);
448 Err(ClientError::OnlineModeRequired)
449 }
450 #[cfg(feature = "microsoft")]
451 Account::Microsoft { session, auth } => {
452 let resp = ghast_auth::crypto::respond_to_encryption_request(
453 server_id,
454 public_key,
455 verify_token,
456 )?;
457 auth.join_server(&session.access_token, session.profile.id, &resp.server_hash)
458 .await?;
459 V::write_encryption_response(
460 enc,
461 &resp.encrypted_shared_secret,
462 &resp.encrypted_verify_token,
463 )?;
464 Ok(*resp.shared_secret.as_bytes())
465 }
466 }
467}
468
469struct Play<V: HasSim, R, W> {
471 rd: R,
472 wr: W,
473 dec: FrameDecoder,
474 enc: FrameEncoder,
475 engine: Engine<V::Sim>,
476 read_timeout: Duration,
477 interests: Interests,
478 raw_ids: Vec<i32>,
480 raw_mask: [u64; 4],
481 world_errors: u64,
483 _v: std::marker::PhantomData<V>,
484}
485
486fn entity_config(interests: Interests) -> Option<ghast_entity::Config> {
489 interests.contains(Interests::ENTITIES).then(|| {
490 ghast_entity::Config::default()
491 .with_metadata(true)
492 .with_equipment(false)
493 .with_effects(false)
494 .with_player_list(interests.contains(Interests::PLAYER_LIST))
495 .with_events(false)
496 .with_skins(interests.contains(Interests::PLAYER_SKINS))
497 })
498}
499
500enum Wake {
502 Read(std::io::Result<usize>),
503 Tick,
504 Shutdown,
505}
506
507impl<V, R, W> Play<V, R, W>
508where
509 V: ClientVersion + HasSim,
510 R: AsyncRead + Unpin,
511 W: AsyncWrite + Unpin,
512{
513 fn set_filter(&mut self, interests: Interests, raw: &[i32]) {
514 self.interests = interests;
515 raw.clone_into(&mut self.raw_ids);
516 self.raw_mask = [0; 4];
517 for &id in raw {
518 if (0..256).contains(&id) {
519 self.raw_mask[id as usize >> 6] |= 1 << (id & 63);
520 }
521 }
522 self.engine
523 .sim
524 .collect_block_updates(interests.contains(Interests::BLOCK_UPDATES));
525 self.engine.sim.set_entities(entity_config(interests));
528 let mut filter: PacketFilter = V::play_filter(interests);
529 for id in self.engine.sim.entity_packet_ids() {
530 filter.insert(id);
531 }
532 for id in self.engine.sim.world_packet_ids() {
533 filter.insert(id);
534 }
535 for &id in raw {
536 filter.insert(id);
537 }
538 self.dec.set_filter(filter);
539 }
540
541 fn bad_world_packet(&mut self, raw: &RawPacket, e: &ghast_world::WorldError) {
543 self.world_errors += 1;
544 let n = self.world_errors;
545 if n <= 3 || n.is_power_of_two() {
546 log::warn!(
547 "[{}] bad world packet 0x{:02x} ({} bytes), column unloaded: {e} ({n} so far)",
548 self.engine.name(),
549 raw.id,
550 raw.body.len()
551 );
552 }
553 if n <= 8
556 && let Some(dir) = std::env::var_os("GHAST_DUMP_BAD_CHUNK")
557 {
558 let path = std::path::Path::new(&dir).join(format!(
559 "ghast-bad-world-{}-0x{:02x}-{n}.bin",
560 std::process::id(),
561 raw.id
562 ));
563 match std::fs::write(&path, &raw.body) {
564 Ok(()) => log::warn!("[{}] wrote {}", self.engine.name(), path.display()),
565 Err(err) => log::warn!("cannot write {}: {err}", path.display()),
566 }
567 }
568 }
569
570 fn encode_outbound(&mut self) -> Result<(), ClientError> {
572 let out = self.engine.outbound();
573 let mut res = Ok(());
574 for p in out.drain(..) {
575 if let Err(e) = V::write_play(&mut self.enc, &p) {
576 res = Err(e);
577 break;
578 }
579 }
580 out.clear();
581 res.map_err(ClientError::from)
582 }
583
584 fn drain_inbound(&mut self) -> Result<Option<SessionEnd>, ClientError> {
586 let mut at = None;
587 while let Some(raw) = self.dec.next_packet()? {
588 if (0..256).contains(&raw.id)
589 && self.raw_mask[raw.id as usize >> 6] & (1 << (raw.id & 63)) != 0
590 {
591 let at = *at.get_or_insert_with(std::time::Instant::now);
592 self.engine.push_raw(crate::engine::RawInbound {
593 id: raw.id,
594 body: raw.body.clone(),
595 at,
596 });
597 }
598 match self.engine.sim.world_packet(&raw) {
599 Ok(()) => {}
600 Err(SimError::Decode(source)) => {
601 return Err(ClientError::Decode {
602 state: State::Play,
603 id: raw.id,
604 source,
605 });
606 }
607 Err(SimError::World(e)) => {
608 self.bad_world_packet(&raw, &e);
612 continue;
613 }
614 }
615 let inv = V::inventory_packet(self.engine.inventory_mut(), &raw).map_err(|source| {
617 ClientError::Decode {
618 state: State::Play,
619 id: raw.id,
620 source,
621 }
622 })?;
623 if let Some(o) = inv {
624 self.engine
625 .outbound()
626 .push(crate::common::Outbound::Inventory(o));
627 }
628 let p = V::decode_play(&raw).map_err(|source| ClientError::Decode {
629 state: State::Play,
630 id: raw.id,
631 source,
632 })?;
633 match p {
634 None => {}
635 Some(Inbound::Disconnect(json)) => {
636 let reason = chat_to_plain(&json);
637 log::warn!("[{}] kicked: {reason}", self.engine.core_name());
638 return Ok(Some(SessionEnd::Kicked {
639 reason,
640 json: json.to_string(),
641 }));
642 }
643 Some(Inbound::SetCompression(t)) => {
644 self.dec.set_compression(t);
646 self.encode_outbound()?;
647 self.enc.set_compression(t);
648 }
649 Some(p) => self.engine.handle(p),
650 }
651 }
652 Ok(None)
653 }
654
655 async fn read_ready(&mut self) -> Option<std::io::Result<usize>> {
657 let mut fut = std::pin::pin!(read_more(&mut self.rd, &mut self.dec));
658 std::future::poll_fn(|cx| {
659 std::task::Poll::Ready(match fut.as_mut().poll(cx) {
660 std::task::Poll::Ready(r) => Some(r),
661 std::task::Poll::Pending => None,
662 })
663 })
664 .await
665 }
666
667 async fn run<B: Behavior, S: Future<Output = ()>>(
668 &mut self,
669 behavior: &mut B,
670 mut shutdown: Pin<&mut S>,
671 ) -> Result<SessionEnd, ClientError> {
672 let interests = self.engine.interests(behavior);
673 let raw = self.engine.raw_packets(behavior);
674 self.set_filter(interests, &raw);
675 self.engine.note_inbound(Instant::now().into_std());
676 if let Some(end) = self.drain_inbound()? {
678 return Ok(end);
679 }
680 self.encode_outbound()?;
681 flush(&mut self.wr, &mut self.enc).await?;
682
683 let mut ticker = tokio::time::interval_at(Instant::now() + TICK, TICK);
684 ticker.set_missed_tick_behavior(MissedTickBehavior::Skip);
687
688 loop {
689 let wake = tokio::select! {
690 biased;
691 () = shutdown.as_mut() => Wake::Shutdown,
692 _ = ticker.tick() => Wake::Tick,
693 r = read_more(&mut self.rd, &mut self.dec) => Wake::Read(r),
694 };
695 match wake {
696 Wake::Shutdown => {
697 self.encode_outbound()?;
698 flush(&mut self.wr, &mut self.enc).await?;
699 let _ = self.wr.shutdown().await;
700 return Ok(SessionEnd::Shutdown);
701 }
702 Wake::Read(r) => {
703 if r? == 0 {
704 return Ok(SessionEnd::ServerClosed);
705 }
706 let now = Instant::now().into_std();
707 self.engine.note_inbound(now);
708 if let Some(end) = self.drain_inbound()? {
709 return Ok(end);
710 }
711 self.encode_outbound()?;
714 flush(&mut self.wr, &mut self.enc).await?;
715 }
716 Wake::Tick => {
717 for _ in 0..64 {
720 match self.read_ready().await {
721 None => break,
722 Some(r) => {
723 if r? == 0 {
724 return Ok(SessionEnd::ServerClosed);
725 }
726 self.engine.note_inbound(Instant::now().into_std());
727 if let Some(end) = self.drain_inbound()? {
728 return Ok(end);
729 }
730 }
731 }
732 }
733 let now = Instant::now().into_std();
734 if let Some(last) = self.engine.last_inbound()
735 && now.saturating_duration_since(last) > self.read_timeout
736 {
737 return Err(ClientError::Timeout("no data from the server"));
738 }
739 self.engine.tick(behavior, now);
740 self.encode_outbound()?;
741 flush(&mut self.wr, &mut self.enc).await?;
742 if let Some(reason) = self.engine.quit_requested() {
743 let reason = reason.to_owned();
744 let _ = self.wr.shutdown().await;
745 return Ok(SessionEnd::Quit(reason));
746 }
747 let interests = self.engine.interests(behavior);
748 let raw = self.engine.raw_packets(behavior);
749 if interests != self.interests || *raw != *self.raw_ids {
750 self.set_filter(interests, &raw);
751 }
752 }
753 }
754 }
755 }
756}
757
758#[cfg(test)]
759mod tests {
760 use super::*;
761
762 #[test]
764 fn entity_config_follows_the_interests() {
765 assert_eq!(entity_config(Interests::PLAYER_LIST), None);
766 let c = entity_config(Interests::ENTITIES).unwrap();
767 assert!(c.metadata && !c.player_list && !c.skins);
768 let c =
769 entity_config(Interests::ENTITIES | Interests::PLAYER_LIST | Interests::PLAYER_SKINS)
770 .unwrap();
771 assert!(c.player_list && c.skins);
772 }
773}