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}