team04_server/
main.rs

1//! The team04 server for "Battlefront Tactics".
2//!
3//! The following sections will give you a rough overview over the server's architecture.
4//! For more details on certain topics, check the documentation of the relevant modules.
5//!
6//! # Connection Lifecycle
7//!
8//! Incoming connections are accepted in [`server::run`].
9//!
10//! Each connection spawns a task[^tokio_task] which is responsible
11//! for running [`server::connection::handle`], which handles incoming
12//! messages for that connection.
13//!
14//! ## pre lobby
15//!
16//! In [`server::connection::handle`], the connection is split into a receiving and a sending half.
17//!
18//! The `pre_lobby` function in [`server::connection`] implements the connection logic
19//! before the client has joined a lobby, including the process of joining the lobby.
20//!
21//! Once the client has joined a lobby, `pre_lobby` returns the lobby the client is in
22//! and the client's player id. It also adds the client to the [`LobbyState`](lobby::state::LobbyState),
23//! as a [`Player`](lobby::state::players::Player) (or a [`Spectator`](lobby::state::clients::Spectator))
24//! containing the sending half of the player's connection, represented as a [`Connection`](server::Connection).
25//!
26//! ## in lobby
27//!
28//! The lobby phase is handled by [`server::connection::handle`], which interacts
29//! with the lobby state directly, by locking it and calling functions on the [`LobbyState`](lobby::state::LobbyState).
30//!
31//! Once the game starts, [`server::connection::handle`] calls [`message_from`](lobby::state::LobbyState::message_from)
32//! for all gameplay-relevant messages it receives, but continues to handle the chat functionality.
33//!
34//!
35//! ## in-game
36//!
37//! The gameplay-relevant messages received by a lobby while in-game are handled by
38//! [`message_from`](lobby::state::LobbyState::message_from) in [`lobby::state::handle_messages`].
39//! Unlike [`server::connection::handle`] in the lobby phase, this does not directly change
40//! the lobby's current state, it only stores players' decisions or requests in it.
41//! While these decisions and requests are stored inside the [`LobbyState`](lobby::state::LobbyState),
42//! they are only implementation details and not part of the state which would, for example,
43//! be used to generate a [`GAMESTATE`](messages::game_state::GameState) message.
44//!
45//! The decisions along with other game logic are actually handled by
46//! the lobby's task, which is explained in the [Game Logic](#game-logic) section.
47//!
48//! # Game Logic
49//!
50//! For every created lobby, [`lobby::game::run`] is executed in a background task[^tokio_task].
51//! This task handles the clients' decisions and processes gameplay logic.
52//!
53//! ## Rounds and Phases
54//!
55//! The [`lobby::game::run`] function is where the rounds and their phases are implemented.
56//! For every phase which should be executed in the current round, a respective function is called.
57//!
58//! After every phase, [`maybe_pause`](lobby::game::maybe_pause) is called,
59//! which pauses the game if a player has requested a pause.
60//!
61//! The phases are:
62//!
63//! - [`lightsaber_purchase`](lobby::game::lightsaber_purchase())
64//! - [`unit_purchase`](lobby::game::unit_purchase())
65//! - [`placements`](lobby::game::placements())
66//! - [`fight`](lobby::game::fight::fight())
67//! - [`completion`](lobby::game::completion::completion()), which returns `true` if the game should end
68//!
69//! The lightsaber purchase, unit purchase, and placement phases start by calling
70//! [`lightsaber_purchase`](lobby::state::SharedLobbyState::lightsaber_purchase),
71//! [`unit_purchase`](lobby::state::SharedLobbyState::unit_purchase), or
72//! [`placements`](lobby::state::SharedLobbyState::placements), respectively,
73//! which are functions on [`SharedLobbyState`](lobby::state::SharedLobbyState).
74//! These functions send a gamestate message to all clients,
75//! followed by the unit or lightsaber options message (for lightsaber or unit purchase phases),
76//! then wait for either the phase's timeout to elapse or for all clients to have made their decisions.
77//! They return a [`LockedLobbyState`](lobby::state::LockedLobbyState) and guarantee certain things about this state,
78//! notable that every player has made a decision (if no decision was received, a random one is made or the previous unit placement is reused).
79//!
80//! With those decisions, the phase handling functions then implement the
81//! actual logic of their respective phase, such as adding purchased lightsabers or units
82//! to players' states, or setting players' unit placements.
83//!
84//! The fight and completion phases do not wait for players' decisions,
85//! because players cannot influence those phases. The completion phase
86//! handler returns a `bool` which is `true` if the game should end
87//! (because at most one player still has more than `0` lives).
88//!
89//! # Locking
90//!
91//! The server has a [`ServerState`](server::state::ServerState), which is passed around and
92//! shared between different tasks as an [`Arc<SyncedServerState>`](server::state::SyncedServerState).
93//! To access the shared server state, call [`.lock()`](server::state::SyncedServerState::lock) on [`SyncedServerState`](server::state::SyncedServerState)
94//!
95//! The [`ServerState`](server::state::ServerState) stores [`LobbyState`](lobby::state::LobbyState)s,
96//! in the form of shareable [`SharedLobbyState`](lobby::state::SharedLobbyState)s, which can be
97//! [`lock`](lobby::state::SharedLobbyState::lock)ed just like the server state.
98//!
99//! ## holding multiple locks at once
100//!
101//! To avoid deadlocks, be careful when holding multiple locks at once:
102//!
103//! While [`LobbyState`](lobby::state::LobbyState) contains an [`Arc<SyncedServerState>`](server::state::SyncedServerState),
104//! locking the [`SyncedServerState`](server::state::SyncedServerState) containing a [`SharedLobbyState`](lobby::state::SharedLobbyState)
105//! while also holding a lock on that [`SharedLobbyState`](lobby::state::SharedLobbyState) can lead to deadlocks.
106//!
107//! The rule is:
108//!
109//! 1. If you have a lock on the [`SyncedServerState`](server::state::SyncedServerState),
110//!    you can lock every [`SharedLobbyState`](lobby::state::SharedLobbyState) in it,
111//! 1. if you do not have and other locks, you can always lock a
112//!    [`SyncedServerState`](server::state::SyncedServerState) or [`SharedLobbyState`](lobby::state::SharedLobbyState), but
113//! 2. if you have a lock on a [`SharedLobbyState`](lobby::state::SharedLobbyState),
114//!    you **cannot** lock the [`SyncedServerState`](server::state::SyncedServerState) it belongs to.
115//!
116//! [^tokio_task]: A task on the tokio runtime, see [`tokio::task`].
117//! Used for [connections](#connection-lifecycle) and the [lobby task](#game-logic).
118//!
119//! [^deadlock_reason]: This is because holding a lock on a [`SharedLobbyState`](lobby::state::SharedLobbyState)
120//! would cause a task trying to do 1. to block until you drop that lock. If you then try to acquire
121//! a lock on the [`SyncedServerState`](server::state::SyncedServerState), you would have to wait for that task,
122//! which is waiting on you, thereby deadlocking the program.
123//!
124
125#![allow(dead_code)]
126
127use log::_impl::LogLevel;
128
129#[cfg(test)]
130use crate::mock::TcpListener;
131#[cfg(not(test))]
132use tokio::net::TcpListener;
133
134pub mod board;
135pub mod config;
136pub mod lobby;
137pub mod log;
138pub mod messages;
139pub mod server;
140pub mod unit;
141
142#[cfg(test)]
143mod integration_tests;
144#[cfg(test)]
145pub(crate) mod mock;
146
147/// If a task waits for a lobby or server lock for more than this many milliseconds,
148/// an info or warning level messsage will be written to the log.
149/// For warning level, in total, the sum of the two values has to have elapsed.
150pub const LOCK_WAIT_TIME_MS: (u64, u64) = (10, 90);
151
152#[tokio::main]
153async fn main() -> tokio::io::Result<()> {
154    async_main().await
155}
156// the tokio::main macro does magic, where `fn main` looks async, but is not;
157// but `fn async_main` definitely is, so it can be used in integration tests.
158async fn async_main() -> tokio::io::Result<()> {
159    // load env vars
160    let mut addr = std::env::var("ADDR").unwrap_or("::".to_owned());
161    if addr.contains(':') {
162        // for ipv6 without surrounding brackets, add brackets
163        let a = addr.trim();
164        addr = match (a.starts_with('['), a.ends_with(']')) {
165            (true, true) => addr,
166            (false, false) => format!("[{a}]"),
167            (true, false) => format!("{a}]"),
168            (false, true) => format!("[{a}"),
169        }
170    }
171    let port = std::env::var("PORT").unwrap_or("1992".to_owned());
172    let addr = format!("{addr}:{port}");
173
174    let pfx = log::pfx();
175
176    // setup listener
177    let listener = TcpListener::bind(&addr).await;
178    let addr = listener
179        .as_ref()
180        .ok()
181        .and_then(|l| l.local_addr().ok())
182        .map(|v| v.to_string())
183        .unwrap_or(addr.to_string());
184
185    // show welcome message
186    if log::_impl::logger().colors {
187        #[rustfmt::skip]
188        let level = match log::_impl::logger().level {
189            LogLevel::Fatal => "Fatal  ",
190            LogLevel::Error => "Error  ",
191            LogLevel::Warn  => "Warning",
192            LogLevel::Info  => "Info   ",
193            LogLevel::Debug => "Debug  ",
194        };
195        let width = "
196│                                      │"
197            .trim_matches(['\r', '\n', '│'])
198            .len();
199        let addr_padding = " ".repeat(
200            width.saturating_sub(" listening: ws://".chars().count() + addr.chars().count()),
201        );
202        log::debug_force!(r#"
203┌──────────────────────────────────────┐
204│ Welcome to the team-04 ("sopra.fun") │
205│      battlefront tactics server      │
206│  -  -  -  -  -  -  -  -  -  -  -  -  │
207│        Environment Variables         │
208│ CONFIG=<path, default=/config>       │
209│ ADDR=<server address, default=::>    │
210│ PORT=<websocket port, default=1992>  │
211│ LOG_LEVEL=fatal/error/Warn/info/dbg  │
212│ LOG_COLORS=Yes/no                    │
213│  -  -  -  -  -  -  -  -  -  -  -  -  │
214│ listening: ws://{addr}{addr_padding}│
215│ current log level: {level}           │
216│ (set LOG_COLORS=no to hide this box) │
217└──────────────────────────────────────┘
218"#; &pfx);
219    }
220
221    // load configs
222    let configs = match config::load_configs() {
223        Err(e) => {
224            log::fatal!("Failed to load config files:\n{}", e; &pfx);
225            return Err(tokio::io::Error::from(tokio::io::ErrorKind::InvalidData));
226        }
227        Ok(c) => c,
228    };
229    log::info!("Loaded {} configs", configs.len(); &pfx);
230    for (id, config) in configs.iter() {
231        log::debug!(
232            "lobby{}: game config: {}",
233            id,
234            serde_json::to_string_pretty(&config.game_config).unwrap();
235            &pfx
236        );
237        log::debug!(
238            "lobby{}: board config: {}",
239            id,
240            serde_json::to_string_pretty(&config.board_config).unwrap();
241            &pfx
242        );
243        log::debug!(
244            "lobby{}: unit config: {}",
245            id,
246            serde_json::to_string_pretty(&config.unit_config).unwrap();
247            &pfx
248        );
249    }
250
251    // run server
252    server::run(listener?, &addr, configs).await
253}
254
255#[cfg(test)]
256mod test {
257    use crate::async_main;
258
259    #[tokio::test]
260    async fn test_main() {
261        // you can see these in the log output
262        for addr in ["1.2.3", "[1:2:4:5:6:7]", "[192.168.300.5", "4:5:7:9:1]"] {
263            unsafe {
264                std::env::set_var("ADDR", addr);
265            }
266            let _socket = crate::mock::init_err(tokio::io::ErrorKind::OutOfMemory).await;
267            assert_eq!(
268                async_main().await.err().map(|e| e.kind()),
269                Some(tokio::io::ErrorKind::OutOfMemory)
270            );
271        }
272    }
273}