dreamwell-runtime 1.0.0

Dreamwell Runtime — cross-platform GPU-accelerated game client
Documentation
//! Local authority backend — lightweight simulation shell for standalone,
//! test, offline, and editor preview modes.
//!
//! Uses the same intent/event contracts as the remote backend.
//! Must not diverge semantically from the remote protocol.

use super::protocol::{AuthorityClient, AuthorityEvent, ConnectionState};
use dreamwell_fabric::packets::ClientIntent;

/// Local authority backend. Immediately acks intents and applies them locally.
/// For standalone, test, offline, and editor preview.
pub struct LocalAuthority {
    state: ConnectionState,
    next_seq: u64,
    pending_events: Vec<AuthorityEvent>,
}

impl LocalAuthority {
    pub fn new() -> Self {
        Self {
            state: ConnectionState::Connected, // Local is always "connected"
            next_seq: 0,
            pending_events: Vec::new(),
        }
    }
}

impl Default for LocalAuthority {
    fn default() -> Self {
        Self::new()
    }
}

impl AuthorityClient for LocalAuthority {
    fn submit_intents(&mut self, intents: &[ClientIntent]) {
        for _intent in intents {
            // Local authority immediately acks each intent.
            self.pending_events.push(AuthorityEvent::Ack { seq: self.next_seq });
            self.next_seq += 1;
        }
    }

    fn poll_events(&mut self, out: &mut Vec<AuthorityEvent>) {
        out.append(&mut self.pending_events);
    }

    fn request_snapshot(&mut self) {
        // Local authority sends an empty snapshot (state is already local).
        self.pending_events.push(AuthorityEvent::SnapshotChunk {
            chunk_id: 0,
            total_chunks: 1,
            data: Vec::new(),
        });
    }

    fn connection_state(&self) -> ConnectionState {
        self.state
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use dreamwell_fabric::packets::IntentKind;

    #[test]
    fn local_authority_always_connected() {
        let auth = LocalAuthority::new();
        assert_eq!(auth.connection_state(), ConnectionState::Connected);
    }

    #[test]
    fn submit_intents_produces_acks() {
        let mut auth = LocalAuthority::new();
        let intents = vec![
            ClientIntent {
                actor_id: 1,
                tick_hint: 0,
                action: IntentKind::Move { dx: 1, dy: 0 },
            },
            ClientIntent {
                actor_id: 1,
                tick_hint: 1,
                action: IntentKind::Ability { slot: 2 },
            },
        ];
        auth.submit_intents(&intents);

        let mut events = Vec::new();
        auth.poll_events(&mut events);
        assert_eq!(events.len(), 2);
        assert!(matches!(events[0], AuthorityEvent::Ack { seq: 0 }));
        assert!(matches!(events[1], AuthorityEvent::Ack { seq: 1 }));
    }

    #[test]
    fn poll_events_drains() {
        let mut auth = LocalAuthority::new();
        auth.submit_intents(&[ClientIntent {
            actor_id: 1,
            tick_hint: 0,
            action: IntentKind::UiCommand { id: 42 },
        }]);

        let mut first = Vec::new();
        auth.poll_events(&mut first);
        assert_eq!(first.len(), 1);

        let mut second = Vec::new();
        auth.poll_events(&mut second);
        assert!(second.is_empty());
    }

    #[test]
    fn request_snapshot_produces_chunk() {
        let mut auth = LocalAuthority::new();
        auth.request_snapshot();

        let mut events = Vec::new();
        auth.poll_events(&mut events);
        assert_eq!(events.len(), 1);
        assert!(matches!(
            events[0],
            AuthorityEvent::SnapshotChunk {
                chunk_id: 0,
                total_chunks: 1,
                ..
            }
        ));
    }
}