Skip to main content

ghast_client/
client.rs

1//! Connection setup and the session driver: connect → handshake → login → play loop.
2
3use 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
21/// Length of one client tick.
22pub const TICK: Duration = Duration::from_millis(50);
23/// Minimum free space requested from the socket per read.
24const READ_CHUNK: usize = 16 * 1024;
25
26/// The account a client logs in with.
27#[derive(Clone)]
28#[non_exhaustive]
29pub enum Account {
30    /// Offline mode (the server does not authenticate; the UUID derives from the name).
31    Offline(OfflineProfile),
32    /// A Microsoft account session (needed for online-mode servers).
33    #[cfg(feature = "microsoft")]
34    Microsoft {
35        /// The logged-in session (access token and profile).
36        session: ghast_auth::microsoft::MinecraftSession,
37        /// The client that makes the session-server `join` call.
38        auth: Arc<ghast_auth::microsoft::MicrosoftAuth>,
39    },
40}
41
42impl Account {
43    /// An offline account (the name is checked like vanilla: 1–16 of `[A-Za-z0-9_]`).
44    pub fn offline(name: &str) -> Result<Self, ClientError> {
45        Ok(Account::Offline(OfflineProfile::new(name)?))
46    }
47
48    /// The player name.
49    #[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/// A configured client: where to connect, which version, which account, and how.
73///
74/// One `Client` can run any number of sessions one after another (reconnects); run several
75/// clients concurrently for several bots (one per process, or many tasks in one process —
76/// a failing session never affects another).
77#[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    /// A client with vanilla defaults: direct connection (see [`Client::connector`]),
97    /// brand `vanilla`, sensitivity 0.5, 30 s read timeout.
98    #[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    /// How to open the TCP connection (SOCKS5 proxy, proxy policy, timeouts).
119    #[must_use]
120    pub fn connector(mut self, connector: Connector) -> Self {
121        self.connector = connector;
122        self
123    }
124
125    /// Client settings sent after joining.
126    #[must_use]
127    pub fn settings(mut self, settings: ClientSettings) -> Self {
128        self.settings = settings;
129        self
130    }
131
132    /// The client brand sent after joining (`None`: send none).
133    #[must_use]
134    pub fn brand(mut self, brand: Option<String>) -> Self {
135        self.brand = brand;
136        self
137    }
138
139    /// Mouse sensitivity (options.txt value, 0 … 1) for rotation steps.
140    #[must_use]
141    pub fn sensitivity(mut self, sensitivity: f32) -> Self {
142        self.sensitivity = sensitivity;
143        self
144    }
145
146    /// The behaviour is frozen after this long without data from the server (default 1.5 s).
147    #[must_use]
148    pub fn silent_hold(mut self, d: Duration) -> Self {
149        self.silent_hold = d;
150        self
151    }
152
153    /// The session fails after this long without data from the server (default 30 s, like
154    /// vanilla's read timeout).
155    #[must_use]
156    pub fn read_timeout(mut self, d: Duration) -> Self {
157        self.read_timeout = d;
158        self
159    }
160
161    /// Connecting plus login must finish within this time (default 30 s).
162    #[must_use]
163    pub fn login_timeout(mut self, d: Duration) -> Self {
164        self.login_timeout = d;
165        self
166    }
167
168    /// Records every session's movement in the oracle's replay format (1.8.9; the file is
169    /// overwritten by each session). For debugging anticheat flags against vanilla.
170    #[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    /// What the world keeps besides block states. Default: nothing (no light, no biomes:
177    /// the bot does not need them, and they cost memory and decoding time). A viewer that
178    /// draws the world can turn them on; [`crate::WorldView::column`] gives the data.
179    #[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    /// Keep the look still on a tick that dug or placed when this tick's turn would take the
186    /// crosshair off the clicked block (on by default). Grim's
187    /// RotationPlace re-checks a dig / place with the look of the movement packet that follows
188    /// it; holding the mouse for one tick is what a player does anyway. The held turn
189    /// continues on the next tick the behaviour aims.
190    #[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    /// **Not vanilla** (Grim 2.3 compatibility, off by default): on a tick whose feet height
197    /// changed (a step-up or landing) but whose move stayed under the 0.03-block resend
198    /// threshold, send the position anyway. Vanilla sends only the look / on-ground there,
199    /// which Grim 2.3.73 sets back (soul sand → grass hops).
200    #[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    /// **Not vanilla**: [`Client::force_step_position`] with a choice of rule, e.g.
211    /// [`ForceStepPosition::StepUpOnly`](crate::ForceStepPosition::StepUpOnly), which forces the
212    /// position only on step-ups (on ground after the move, feet higher) and leaves sub-0.03
213    /// falls and landings vanilla.
214    #[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    /// The server address.
221    #[must_use]
222    pub fn address(&self) -> &TargetAddr {
223        &self.address
224    }
225
226    /// The protocol version.
227    #[must_use]
228    pub fn version(&self) -> GameVersion {
229        self.version
230    }
231
232    /// The account.
233    #[must_use]
234    pub fn account(&self) -> &Account {
235        &self.account
236    }
237
238    /// Runs one session with `behavior` until the server ends it, the behaviour quits or an
239    /// error occurs.
240    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    /// Like [`Client::run`], but also ends cleanly (the behaviour is stopped, the socket shut
245    /// down) when `shutdown` completes.
246    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
263/// Reads more bytes into the decoder. Cancel safe.
264async 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
277/// Writes everything the encoder has queued.
278async 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
368/// Handshake and login. Returns once the server sent login success.
369async 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/// Answers an encryption request; returns the shared secret to enable.
433// `V` is only used for online mode.
434#[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
469/// The play state of one session.
470struct 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    /// Packet ids copied for the behaviour (`Behavior::raw_packets`), as a bit set.
479    raw_ids: Vec<i32>,
480    raw_mask: [u64; 4],
481    /// World packets the world rejected (malformed chunk data).
482    world_errors: u64,
483    _v: std::marker::PhantomData<V>,
484}
485
486/// The entity tracking the interests ask for: positions and metadata (item stacks, sneak /
487/// pose flags); equipment and other entities' effects are not decoded (cheaper).
488fn 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
500/// What woke the play loop.
501enum 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        // Entity tracking follows the interests at run time (a tracker started late begins
526        // empty: entities spawned before are not known until the server sends them again).
527        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    /// Logs (rate-limited) and optionally dumps a world packet the world rejected.
542    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        // Opt-in capture for bug reports: GHAST_DUMP_BAD_CHUNK=<dir> writes the first few
554        // rejected bodies there.
555        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    /// Encodes the engine's outbound packets.
571    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    /// Decodes and applies everything buffered. Returns a session end on kick.
585    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                    // Malformed chunk data for one column: the world has unloaded that column
609                    // (never guessed as air); the session goes on, like vanilla, which logs the
610                    // failed task and keeps playing.
611                    self.bad_world_packet(&raw, &e);
612                    continue;
613                }
614            }
615            // The inventory answers window confirms itself (exactly once).
616            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                    // 1.8 play-state Set Compression: applies to the next frame both ways.
645                    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    /// Reads if data is available right now; `None` if the read would wait.
656    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        // Frames that arrived together with login success.
677        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        // After a stall the missed ticks are skipped, never caught up: a burst of movement
685        // packets is what anti-cheat timer checks flag.
686        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                    // Replies (keep-alive, teleport, confirms) go out right away, like
712                    // vanilla's per-frame packet handling.
713                    self.encode_outbound()?;
714                    flush(&mut self.wr, &mut self.enc).await?;
715                }
716                Wake::Tick => {
717                    // Apply what arrived since the last read first (inbound before the tick),
718                    // without waiting for more.
719                    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    /// The interests map to the tracker configuration (none without `ENTITIES`).
763    #[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}