nimble_host_logic/
lib.rs

1/*
2 * Copyright (c) Peter Bjorklund. All rights reserved. https://github.com/nimble-rust/nimble
3 * Licensed under the MIT License. See LICENSE in the project root for license information.
4 */
5pub mod combinator;
6mod combine;
7pub mod connection;
8pub mod err;
9pub mod session;
10
11use crate::connection::Connection;
12use crate::err::HostLogicError;
13use crate::session::GameSession;
14use app_version::Version;
15use flood_rs::{Deserialize, Serialize};
16use freelist_rs::FreeList;
17use log::trace;
18use monotonic_time_rs::Millis;
19use nimble_protocol::host_to_client::PongInfo;
20use nimble_protocol::prelude::{ClientToHostCommands, HostToClientCommands};
21use nimble_protocol::NIMBLE_PROTOCOL_VERSION;
22use nimble_step::Step;
23use std::collections::HashMap;
24use std::fmt::{Debug, Display};
25use tick_id::TickId;
26
27pub trait GameStateProvider {
28    fn state(&self, tick_id: TickId) -> (TickId, Vec<u8>);
29}
30
31#[derive(Debug, PartialEq, Eq)]
32pub enum Phase {
33    WaitingForValidConnectRequest,
34    Connected,
35}
36
37pub const NIMBLE_VERSION: Version = Version::new(
38    NIMBLE_PROTOCOL_VERSION.major,
39    NIMBLE_PROTOCOL_VERSION.minor,
40    NIMBLE_PROTOCOL_VERSION.patch,
41);
42
43/// Identifier for a host connection.
44///
45/// Wraps a `u8` value representing the unique connection ID.
46#[derive(Debug, Copy, Clone)]
47pub struct HostConnectionId(pub u8);
48
49/// Core logic handler for the Nimble host.
50///
51/// Manages connections, game sessions, and processes client commands.
52pub struct HostLogic<StepT: Clone + Eq + Debug + Deserialize + Serialize + Display> {
53    #[allow(unused)]
54    connections: HashMap<u8, Connection<StepT>>,
55    session: GameSession<StepT>,
56    free_list: FreeList<u8>,
57    deterministic_simulation_version: Version,
58}
59
60impl<StepT: Clone + Eq + Debug + Deserialize + Serialize + Display> HostLogic<StepT> {
61    /// Creates a new instance of `HostLogic`.
62    ///
63    /// # Parameters
64    ///
65    /// - `tick_id`: The initial tick identifier for the game session.
66    /// - `deterministic_simulation_version`: The version of the deterministic simulation.
67    ///
68    /// # Returns
69    ///
70    /// A new `HostLogic` instance.
71    #[must_use]
72    pub fn new(tick_id: TickId, deterministic_simulation_version: Version) -> Self {
73        Self {
74            connections: HashMap::new(),
75            session: GameSession::new(tick_id),
76            free_list: FreeList::<u8>::new(0xff),
77            deterministic_simulation_version,
78        }
79    }
80
81    /// Creates a new connection and returns its identifier.
82    ///
83    /// Allocates a unique `HostConnectionId` for a new client connection.
84    ///
85    /// # Returns
86    ///
87    /// An `Option` containing the new `HostConnectionId` if allocation is successful, or `None` if the limit is reached.
88    pub fn create_connection(&mut self) -> Option<HostConnectionId> {
89        let new_connection_id = self.free_list.allocate();
90        if let Some(id) = new_connection_id {
91            self.connections.insert(id, Connection::new());
92            Some(HostConnectionId(id))
93        } else {
94            None
95        }
96    }
97
98    /// Retrieves a reference to a connection by its identifier.
99    ///
100    /// # Parameters
101    ///
102    /// - `connection_id`: The `HostConnectionId` of the connection to retrieve.
103    ///
104    /// # Returns
105    ///
106    /// An `Option` containing a reference to the `Connection` if found, or `None` otherwise.
107    #[must_use]
108    pub fn get(&self, connection_id: HostConnectionId) -> Option<&Connection<StepT>> {
109        self.connections.get(&connection_id.0)
110    }
111
112    /// Destroys a connection, freeing its identifier.
113    ///
114    /// # Parameters
115    ///
116    /// - `connection_id`: The `HostConnectionId` of the connection to destroy.
117    ///
118    /// # Returns
119    ///
120    /// A `Result` indicating success or a `HostLogicError` if the connection ID is invalid.
121    ///
122    /// # Errors
123    ///
124    /// `HostLogicError` // TODO:
125    pub fn destroy_connection(
126        &mut self,
127        connection_id: HostConnectionId,
128    ) -> Result<(), HostLogicError> {
129        self.free_list
130            .free(connection_id.0)
131            .map_err(|err| HostLogicError::FreeListError {
132                connection_id,
133                message: err,
134            })?;
135
136        if self.connections.remove(&connection_id.0).is_some() {
137            Ok(())
138        } else {
139            Err(HostLogicError::UnknownConnectionId(connection_id))
140        }
141    }
142
143    /// Retrieves a reference to the current game session.
144    ///
145    /// # Returns
146    ///
147    /// A reference to the `GameSession`.
148    #[must_use]
149    pub const fn session(&self) -> &GameSession<StepT> {
150        &self.session
151    }
152
153    /// Performs post-update operations after the main `update` cycle.
154    ///
155    /// Specifically, it triggers the production of authoritative steps within the session's combinator.
156    pub fn post_update(&mut self) {
157        self.session.combinator.produce_authoritative_steps();
158    }
159
160    /// Processes an update from a client connection.
161    ///
162    /// Handles incoming client commands and updates the game state accordingly.
163    ///
164    /// # Parameters
165    ///
166    /// - `connection_id`: The `HostConnectionId` of the client sending the commands.
167    /// - `now`: The current absolute time in milliseconds precision.
168    /// - `request`: The `ClientToHostCommands` sent by the client.
169    /// - `state_provider`: An implementation of `GameStateProvider` to supply game state data.
170    ///
171    /// # Returns
172    ///
173    /// A `Result` containing a vector of `HostToClientCommands` to be sent back to the client,
174    /// or a `HostLogicError` if processing fails.
175    ///
176    /// # Errors
177    ///
178    /// `HostLogicError` // TODO:
179    pub fn update(
180        &mut self,
181        connection_id: HostConnectionId,
182        now: Millis,
183        request: &ClientToHostCommands<StepT>,
184        state_provider: &impl GameStateProvider,
185    ) -> Result<Vec<HostToClientCommands<Step<StepT>>>, HostLogicError> {
186        trace!("host_logic: receive: \n{request}");
187        if let Some(ref mut connection) = self.connections.get_mut(&connection_id.0) {
188            match &connection.phase {
189                Phase::Connected => {
190                    match request {
191                        ClientToHostCommands::JoinGameType(join_game_request) => Ok(vec![
192                            connection.on_join(&mut self.session, join_game_request)?,
193                        ]),
194                        ClientToHostCommands::Steps(add_steps_request) => {
195                            Ok(vec![connection.on_steps(
196                                &mut self.session.combinator,
197                                add_steps_request,
198                            )?])
199                        }
200                        ClientToHostCommands::DownloadGameState(download_game_state_request) => {
201                            Ok(connection.on_download(
202                                self.session.combinator.tick_id_to_produce(),
203                                now,
204                                download_game_state_request,
205                                state_provider,
206                            )?)
207                        }
208                        ClientToHostCommands::BlobStreamChannel(blob_stream_command) => {
209                            connection.on_blob_stream(now, blob_stream_command)
210                        }
211                        ClientToHostCommands::ConnectType(connect_request) => {
212                            trace!("notice: got connection request, even though we are connected, but will send response anyway");
213                            connection
214                                .on_connect(connect_request, &self.deterministic_simulation_version)
215                        }
216                        ClientToHostCommands::Ping(ping_info) => Ok(Self::on_ping(*ping_info)),
217                    }
218                }
219                Phase::WaitingForValidConnectRequest => match request {
220                    ClientToHostCommands::ConnectType(connect_request) => connection
221                        .on_connect(connect_request, &self.deterministic_simulation_version),
222                    _ => Err(HostLogicError::NeedConnectRequestFirst),
223                },
224            }
225        } else {
226            Err(HostLogicError::UnknownConnectionId(connection_id))
227        }
228    }
229
230    fn on_ping(lower_millis: u16) -> Vec<HostToClientCommands<Step<StepT>>> {
231        vec![HostToClientCommands::Pong(PongInfo { lower_millis })]
232    }
233}