flatland-client-lib 0.1.0

Flatland3 remote game client library (TCP session, bots, game state)
Documentation
use std::collections::VecDeque;

use flatland_protocol::{EntityId, EntityState, Intent, Seq, SessionId, Tick};

use crate::session::{PlayConnection, SessionEvent};

const MAX_LOG_LINES: usize = 200;

#[derive(Debug, Clone)]
pub struct GameState {
    pub session_id: SessionId,
    pub entity_id: EntityId,
    pub tick: Tick,
    pub chunk_rev: u64,
    pub entities: Vec<EntityState>,
    pub player: Option<EntityState>,
    pub logs: VecDeque<String>,
    pub intents_sent: u64,
    pub ticks_received: u64,
    pub connected: bool,
}

impl GameState {
    fn push_log(&mut self, line: impl Into<String>) {
        self.logs.push_back(line.into());
        while self.logs.len() > MAX_LOG_LINES {
            self.logs.pop_front();
        }
    }
}

pub struct GameClient<S: PlayConnection> {
    session: S,
    seq: Seq,
    pub state: GameState,
}

impl<S: PlayConnection> GameClient<S> {
    pub fn new(session: S) -> Self {
        let session_id = session.session_id();
        let entity_id = session.entity_id();
        Self {
            session,
            seq: 0,
            state: GameState {
                session_id,
                entity_id,
                tick: 0,
                chunk_rev: 0,
                entities: Vec::new(),
                player: None,
                logs: VecDeque::new(),
                intents_sent: 0,
                ticks_received: 0,
                connected: false,
            },
        }
    }

    pub fn entity_id(&self) -> EntityId {
        self.state.entity_id
    }

    pub async fn wait_until_ready(&mut self) -> anyhow::Result<()> {
        if self.state.connected {
            return Ok(());
        }

        loop {
            match self.session.next_event().await {
                Some(SessionEvent::Welcome {
                    session_id,
                    entity_id,
                    snapshot,
                }) => {
                    self.state.session_id = session_id;
                    self.state.entity_id = entity_id;
                    self.state.tick = snapshot.tick;
                    self.state.chunk_rev = snapshot.chunk_rev;
                    self.apply_entities(snapshot.entities);
                    self.state.connected = true;
                    self.state.push_log(format!(
                        "Connected — session {session_id}, entity {entity_id}"
                    ));
                    return Ok(());
                }
                Some(SessionEvent::Disconnected) => {
                    anyhow::bail!("disconnected before welcome");
                }
                Some(_) => continue,
                None => anyhow::bail!("session closed before welcome"),
            }
        }
    }

    /// Drain all pending server events (non-blocking).
    pub fn drain_events(&mut self) {
        while let Some(event) = self.session.try_next_event() {
            if self.handle_event_sync(event).is_err() {
                break;
            }
        }
    }

    /// Wait for the next server event.
    pub async fn next_event(&mut self) -> Option<SessionEvent> {
        self.session.next_event().await
    }

    pub async fn handle_event(&mut self, event: SessionEvent) -> anyhow::Result<()> {
        self.handle_event_sync(event)
    }

    fn handle_event_sync(&mut self, event: SessionEvent) -> anyhow::Result<()> {
        match event {
            SessionEvent::Welcome {
                session_id,
                entity_id,
                snapshot,
            } => {
                self.state.session_id = session_id;
                self.state.entity_id = entity_id;
                self.state.tick = snapshot.tick;
                self.state.chunk_rev = snapshot.chunk_rev;
                self.apply_entities(snapshot.entities);
                self.state.connected = true;
            }
            SessionEvent::Tick(delta) => {
                self.state.tick = delta.tick;
                self.apply_entities(delta.entities);
                self.state.ticks_received += 1;
            }
            SessionEvent::IntentAck { .. } => {}
            SessionEvent::Disconnected => {
                self.state.connected = false;
                self.state.push_log("Disconnected from server");
                anyhow::bail!("disconnected");
            }
        }
        Ok(())
    }

    pub async fn move_by(&mut self, forward: f32, strafe: f32) -> anyhow::Result<()> {
        self.seq += 1;
        self.session
            .submit_intent(Intent::Move {
                entity_id: self.state.entity_id,
                forward,
                strafe,
                seq: self.seq,
            })
            .await?;
        self.state.intents_sent += 1;
        Ok(())
    }

    pub async fn stop(&mut self) -> anyhow::Result<()> {
        self.seq += 1;
        self.session
            .submit_intent(Intent::Stop {
                entity_id: self.state.entity_id,
                seq: self.seq,
            })
            .await?;
        self.state.intents_sent += 1;
        Ok(())
    }

    pub fn disconnect(&self) {
        self.session.disconnect();
    }

    fn apply_entities(&mut self, entities: Vec<EntityState>) {
        self.state.player = entities
            .iter()
            .find(|e| e.id == self.state.entity_id)
            .cloned();
        self.state.entities = entities;
    }
}