Expand description
The team04 server for “Battlefront Tactics”.
The following sections will give you a rough overview over the server’s architecture. For more details on certain topics, check the documentation of the relevant modules.
§Connection Lifecycle
Incoming connections are accepted in server::run.
Each connection spawns a task1 which is responsible
for running server::connection::handle, which handles incoming
messages for that connection.
§pre lobby
In server::connection::handle, the connection is split into a receiving and a sending half.
The pre_lobby function in server::connection implements the connection logic
before the client has joined a lobby, including the process of joining the lobby.
Once the client has joined a lobby, pre_lobby returns the lobby the client is in
and the client’s player id. It also adds the client to the LobbyState,
as a Player (or a Spectator)
containing the sending half of the player’s connection, represented as a Connection.
§in lobby
The lobby phase is handled by server::connection::handle, which interacts
with the lobby state directly, by locking it and calling functions on the LobbyState.
Once the game starts, server::connection::handle calls message_from
for all gameplay-relevant messages it receives, but continues to handle the chat functionality.
§in-game
The gameplay-relevant messages received by a lobby while in-game are handled by
message_from in lobby::state::handle_messages.
Unlike server::connection::handle in the lobby phase, this does not directly change
the lobby’s current state, it only stores players’ decisions or requests in it.
While these decisions and requests are stored inside the LobbyState,
they are only implementation details and not part of the state which would, for example,
be used to generate a GAMESTATE message.
The decisions along with other game logic are actually handled by the lobby’s task, which is explained in the Game Logic section.
§Game Logic
For every created lobby, lobby::game::run is executed in a background task1.
This task handles the clients’ decisions and processes gameplay logic.
§Rounds and Phases
The lobby::game::run function is where the rounds and their phases are implemented.
For every phase which should be executed in the current round, a respective function is called.
After every phase, maybe_pause is called,
which pauses the game if a player has requested a pause.
The phases are:
lightsaber_purchaseunit_purchaseplacementsfightcompletion, which returnstrueif the game should end
The lightsaber purchase, unit purchase, and placement phases start by calling
lightsaber_purchase,
unit_purchase, or
placements, respectively,
which are functions on SharedLobbyState.
These functions send a gamestate message to all clients,
followed by the unit or lightsaber options message (for lightsaber or unit purchase phases),
then wait for either the phase’s timeout to elapse or for all clients to have made their decisions.
They return a LockedLobbyState and guarantee certain things about this state,
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).
With those decisions, the phase handling functions then implement the actual logic of their respective phase, such as adding purchased lightsabers or units to players’ states, or setting players’ unit placements.
The fight and completion phases do not wait for players’ decisions,
because players cannot influence those phases. The completion phase
handler returns a bool which is true if the game should end
(because at most one player still has more than 0 lives).
§Locking
The server has a ServerState, which is passed around and
shared between different tasks as an Arc<SyncedServerState>.
To access the shared server state, call .lock() on SyncedServerState
The ServerState stores LobbyStates,
in the form of shareable SharedLobbyStates, which can be
locked just like the server state.
§holding multiple locks at once
To avoid deadlocks, be careful when holding multiple locks at once:
While LobbyState contains an Arc<SyncedServerState>,
locking the SyncedServerState containing a SharedLobbyState
while also holding a lock on that SharedLobbyState can lead to deadlocks.
The rule is:
- If you have a lock on the
SyncedServerState, you can lock everySharedLobbyStatein it, - if you do not have and other locks, you can always lock a
SyncedServerStateorSharedLobbyState, but - if you have a lock on a
SharedLobbyState, you cannot lock theSyncedServerStateit belongs to.
A task on the tokio runtime, see
tokio::task. Used for connections and the lobby task. ↩This is because holding a lock on a
SharedLobbyStatewould cause a task trying to do 1. to block until you drop that lock. If you then try to acquire a lock on theSyncedServerState, you would have to wait for that task, which is waiting on you, thereby deadlocking the program. ↩
Modules§
- board
- Representation of the board specified by the board config and used in-game, as well as associated functions and data structures. See: Board
- config
- Loading of the config files, including serialization and deserialization.
- lobby
- Definitions of the things that happen inside a lobby, most importantly including the actual gameplay-controlling code.
- log
- This module is used for logging.
- messages
- The messages which can be sent to or received from clients over the websocket connection.
- server
- Contains connection handling, clients joining/leaving lobbies, and creation of new lobbies.
- unit
- Definition of units and fight-related things used by those units.
Macros§
Constants§
- LOCK_
WAIT_ TIME_ MS - If a task waits for a lobby or server lock for more than this many milliseconds, an info or warning level messsage will be written to the log. For warning level, in total, the sum of the two values has to have elapsed.
Functions§
- async_
main 🔒 - main 🔒