/* Copyright (C) 2021 Wildfire Games. * This file is part of 0 A.D. * * 0 A.D. is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 2 of the License, or * (at your option) any later version. * * 0 A.D. is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with 0 A.D. If not, see . */ #ifndef INCLUDED_TURNMANAGER #define INCLUDED_TURNMANAGER #include "ps/CStr.h" #include "simulation2/helpers/SimulationCommand.h" #include #include #include #include class CSimulationMessage; class CSimulation2; class IReplayLogger; /** * This file defines the base class of the turn managers for clients, local games and replays. * The basic idea of our turn managing system across a network is as in this article: * http://www.gamasutra.com/view/feature/3094/1500_archers_on_a_288_network_.php?print=1 * * Each player performs the simulation for turn N. * User input is translated into commands scheduled for execution in turn N+2 which are * distributed to all other clients. * After a while, a client wants to perform the simulation for turn N+1, * which first requires that it has all the other clients' commands for turn N+1. * In that case, it does the simulation and tells all the other clients (via the server) * it has finished sending commands for turn N+2, and it starts sending commands for turn N+3. * * Commands are redistributed immediately by the server. * To ensure a consistent execution of commands, they are each associated with a * client session ID (which is globally unique and consistent), which is used to sort them. */ /** * Default turn length in SP & MP. * This value should be as low as possible, while not introducing un-necessary lag. */ inline constexpr u32 DEFAULT_TURN_LENGTH = 200; /** * In single-player, commands are directly scheduled for the next turn. */ inline constexpr u32 COMMAND_DELAY_SP = 1; /** * In multi-player, clients can only compute turn N if all clients have finished sending commands for it, * i.e. N < CurrentTurn + COMMAND_DELAY for all clients. * Commands are sent from client to server to client, and both client and network can lag. * If a client reaches turn CURRENT_TURN + COMMAND_DELAY - 1, it'll freeze while waiting for commands. * To avoid that, we increase the command-delay to make sure that in general players will have received all commands * by the time they reach a given turn. Keep in mind the minimum delay is one turn. * This value should be as low as possible while avoiding 'freezing' in general usage. * TODO: * - this command-delay could vary based on server-client pings * - it ought be possible to send commands in a P2P fashion (with server verification), which would lower the ping. */ inline constexpr u32 COMMAND_DELAY_MP = 4; /** * Common turn system (used by clients and offline games). */ class CTurnManager { NONCOPYABLE(CTurnManager); public: /** * Construct for a given network session ID. */ CTurnManager(CSimulation2& simulation, u32 defaultTurnLength, u32 commandDelay, int clientId, IReplayLogger& replay); virtual ~CTurnManager() { } void ResetState(u32 newCurrentTurn, u32 newReadyTurn); /** * Set the current user's player ID, which will be added into command messages. */ void SetPlayerID(int playerId); /** * Advance the simulation by a certain time. If this brings us past the current * turn length, the next turns are processed and the function returns true. * Otherwise, nothing happens and it returns false. * * @param simFrameLength Length of the previous frame, in simulation seconds * @param maxTurns Maximum number of turns to simulate at once */ bool Update(float simFrameLength, size_t maxTurns); /** * Advance the simulation by as much as possible. Intended for catching up * over a small number of turns when rejoining a multiplayer match. * Returns true if it advanced by at least one turn. */ bool UpdateFastForward(); /** * Advance the graphics by a certain time. * @param simFrameLength Length of the previous frame, in simulation seconds * @param realFrameLength Length of the previous frame, in real time seconds */ void Interpolate(float simFrameLength, float realFrameLength); /** * Called by networking code when a simulation message is received. */ virtual void OnSimulationMessage(CSimulationMessage* msg) = 0; /** * Called by simulation code, to add a new command to be distributed to all clients and executed soon. */ virtual void PostCommand(JS::HandleValue data) = 0; /** * Called when all commands for a given turn have been received. * This allows Update to progress to that turn. */ void FinishedAllCommands(u32 turn, u32 turnLength); /** * Enables the recording of state snapshots every @p numTurns, * which can be jumped back to via RewindTimeWarp(). * If @p numTurns is 0 then recording is disabled. */ void EnableTimeWarpRecording(size_t numTurns); /** * Jumps back to the latest recorded state snapshot (if any). */ void RewindTimeWarp(); void QuickSave(JS::HandleValue GUIMetadata); void QuickLoad(); u32 GetCurrentTurn() const { return m_CurrentTurn; } /** * @return how many turns are ready to be computed. * (used to detect players/observers that fall behind the live game. */ u32 GetPendingTurns() const { return m_ReadyTurn - m_CurrentTurn; } protected: /** * Store a command to be executed at a given turn. */ void AddCommand(int client, int player, JS::HandleValue data, u32 turn); /** * Called when this client has finished sending all its commands scheduled for the given turn. */ virtual void NotifyFinishedOwnCommands(u32 turn) = 0; /** * Called when this client has finished a simulation update. */ virtual void NotifyFinishedUpdate(u32 turn) = 0; /** * Returns whether we should compute a complete state hash for the given turn, * instead of a quick less-complete hash. */ bool TurnNeedsFullHash(u32 turn) const; CSimulation2& m_Simulation2; /// The turn that we have most recently executed u32 m_CurrentTurn; // Current command delay (commands are scheduled for m_CurrentTurn + m_CommandDelay) u32 m_CommandDelay; /// The latest turn for which we have received all commands from all clients u32 m_ReadyTurn; // Current turn length u32 m_TurnLength; /// Commands queued at each turn (index 0 is for m_CurrentTurn+1) std::deque>> m_QueuedCommands; int m_PlayerId; uint m_ClientId; /// Simulation time remaining until we ought to execute the next turn (as a negative value to /// add elapsed time increments to until we reach 0). float m_DeltaSimTime; IReplayLogger& m_Replay; // The number of the last turn that is allowed to be executed (used for replays) u32 m_FinalTurn; private: static const CStr EventNameSavegameLoaded; size_t m_TimeWarpNumTurns; // 0 if disabled std::list m_TimeWarpStates; std::string m_QuickSaveState; // TODO: should implement a proper disk-based quicksave system JS::PersistentRootedValue m_QuickSaveMetadata; }; #endif // INCLUDED_TURNMANAGER