Index: ps/trunk/source/main.cpp
===================================================================
--- ps/trunk/source/main.cpp (revision 25000)
+++ ps/trunk/source/main.cpp (revision 25001)
@@ -1,749 +1,749 @@
/* 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 .
*/
/*
This module drives the game when running without Atlas (our integrated
map editor). It receives input and OS messages via SDL and feeds them
into the input dispatcher, where they are passed on to the game GUI and
simulation.
It also contains main(), which either runs the above controller or
that of Atlas depending on commandline parameters.
*/
// not for any PCH effort, but instead for the (common) definitions
// included there.
#define MINIMAL_PCH 2
#include "lib/precompiled.h"
#include "lib/debug.h"
#include "lib/status.h"
#include "lib/secure_crt.h"
#include "lib/frequency_filter.h"
#include "lib/input.h"
#include "lib/ogl.h"
#include "lib/timer.h"
#include "lib/external_libraries/libsdl.h"
#include "ps/ArchiveBuilder.h"
#include "ps/CConsole.h"
#include "ps/CLogger.h"
#include "ps/ConfigDB.h"
#include "ps/Filesystem.h"
#include "ps/Game.h"
#include "ps/Globals.h"
#include "ps/Hotkey.h"
#include "ps/Loader.h"
#include "ps/ModInstaller.h"
#include "ps/Profile.h"
#include "ps/Profiler2.h"
#include "ps/Pyrogenesis.h"
#include "ps/Replay.h"
#include "ps/TouchInput.h"
#include "ps/UserReport.h"
#include "ps/Util.h"
#include "ps/VideoMode.h"
#include "ps/World.h"
#include "ps/GameSetup/GameSetup.h"
#include "ps/GameSetup/Atlas.h"
#include "ps/GameSetup/Config.h"
#include "ps/GameSetup/CmdLineArgs.h"
#include "ps/GameSetup/Paths.h"
#include "ps/XML/Xeromyces.h"
#include "network/NetClient.h"
#include "network/NetServer.h"
#include "network/NetSession.h"
#include "lobby/IXmppClient.h"
#include "graphics/Camera.h"
#include "graphics/GameView.h"
#include "graphics/TextureManager.h"
#include "gui/GUIManager.h"
#include "renderer/Renderer.h"
#include "rlinterface/RLInterface.h"
#include "scriptinterface/ScriptEngine.h"
#include "simulation2/Simulation2.h"
#include "simulation2/system/TurnManager.h"
#include "soundmanager/ISoundManager.h"
#if OS_UNIX
#include // geteuid
#endif // OS_UNIX
#if OS_MACOSX
#include "lib/sysdep/os/osx/osx_atlas.h"
#endif
#if MSC_VERSION
#include
#define getpid _getpid // Use the non-deprecated function name
#endif
#include
#include
extern CmdLineArgs g_args;
extern CStrW g_UniqueLogPostfix;
// Marks terrain as modified so the minimap can repaint (is there a cleaner way of handling this?)
bool g_GameRestarted = false;
// Determines the lifetime of the mainloop
enum ShutdownType
{
// The application shall continue the main loop.
None,
// The process shall terminate as soon as possible.
Quit,
// The engine should be restarted in the same process, for instance to activate different mods.
Restart,
// Atlas should be started in the same process.
RestartAsAtlas
};
static ShutdownType g_Shutdown = ShutdownType::None;
// to avoid redundant and/or recursive resizing, we save the new
// size after VIDEORESIZE messages and only update the video mode
// once per frame.
// these values are the latest resize message, and reset to 0 once we've
// updated the video mode
static int g_ResizedW;
static int g_ResizedH;
static std::chrono::high_resolution_clock::time_point lastFrameTime;
bool IsQuitRequested()
{
return g_Shutdown == ShutdownType::Quit;
}
void QuitEngine()
{
g_Shutdown = ShutdownType::Quit;
}
void RestartEngine()
{
g_Shutdown = ShutdownType::Restart;
}
void StartAtlas()
{
g_Shutdown = ShutdownType::RestartAsAtlas;
}
// main app message handler
static InReaction MainInputHandler(const SDL_Event_* ev)
{
switch(ev->ev.type)
{
case SDL_WINDOWEVENT:
switch(ev->ev.window.event)
{
case SDL_WINDOWEVENT_ENTER:
RenderCursor(true);
break;
case SDL_WINDOWEVENT_LEAVE:
RenderCursor(false);
break;
case SDL_WINDOWEVENT_RESIZED:
g_ResizedW = ev->ev.window.data1;
g_ResizedH = ev->ev.window.data2;
break;
case SDL_WINDOWEVENT_MOVED:
g_VideoMode.UpdatePosition(ev->ev.window.data1, ev->ev.window.data2);
}
break;
case SDL_QUIT:
QuitEngine();
break;
case SDL_DROPFILE:
{
char* dropped_filedir = ev->ev.drop.file;
const Paths paths(g_args);
CModInstaller installer(paths.UserData() / "mods", paths.Cache());
installer.Install(std::string(dropped_filedir), g_ScriptContext, true);
SDL_free(dropped_filedir);
if (installer.GetInstalledMods().empty())
LOGERROR("Failed to install mod %s", dropped_filedir);
else
{
LOGMESSAGE("Installed mod %s", installer.GetInstalledMods().front());
RestartEngine();
}
break;
}
case SDL_HOTKEYPRESS:
std::string hotkey = static_cast(ev->ev.user.data1);
if (hotkey == "exit")
{
QuitEngine();
return IN_HANDLED;
}
else if (hotkey == "screenshot")
{
WriteScreenshot(L".png");
return IN_HANDLED;
}
else if (hotkey == "bigscreenshot")
{
int tiles = 4, tileWidth = 256, tileHeight = 256;
CFG_GET_VAL("screenshot.tiles", tiles);
CFG_GET_VAL("screenshot.tilewidth", tileWidth);
CFG_GET_VAL("screenshot.tileheight", tileHeight);
if (tiles > 0 && tileWidth > 0 && tileHeight > 0)
WriteBigScreenshot(L".bmp", tiles, tileWidth, tileHeight);
else
LOGWARNING("Invalid big screenshot size: tiles=%d tileWidth=%d tileHeight=%d", tiles, tileWidth, tileHeight);
return IN_HANDLED;
}
else if (hotkey == "togglefullscreen")
{
g_VideoMode.ToggleFullscreen();
return IN_HANDLED;
}
else if (hotkey == "profile2.toggle")
{
g_Profiler2.Toggle();
return IN_HANDLED;
}
break;
}
return IN_PASS;
}
// dispatch all pending events to the various receivers.
static void PumpEvents()
{
ScriptRequest rq(g_GUI->GetScriptInterface());
PROFILE3("dispatch events");
SDL_Event_ ev;
while (in_poll_event(&ev))
{
PROFILE2("event");
if (g_GUI)
{
JS::RootedValue tmpVal(rq.cx);
ScriptInterface::ToJSVal(rq, &tmpVal, ev);
std::string data = g_GUI->GetScriptInterface()->StringifyJSON(&tmpVal);
PROFILE2_ATTR("%s", data.c_str());
}
in_dispatch_event(&ev);
}
g_TouchInput.Frame();
}
/**
* Optionally throttle the render frequency in order to
* prevent 100% workload of the currently used CPU core.
*/
inline static void LimitFPS()
{
if (g_VSync)
return;
double fpsLimit = 0.0;
CFG_GET_VAL(g_Game && g_Game->IsGameStarted() ? "adaptivefps.session" : "adaptivefps.menu", fpsLimit);
// Keep in sync with options.json
if (fpsLimit < 20.0 || fpsLimit >= 100.0)
return;
double wait = 1000.0 / fpsLimit -
std::chrono::duration_cast(
std::chrono::high_resolution_clock::now() - lastFrameTime).count() / 1000.0;
if (wait > 0.0)
SDL_Delay(wait);
lastFrameTime = std::chrono::high_resolution_clock::now();
}
static int ProgressiveLoad()
{
PROFILE3("progressive load");
wchar_t description[100];
int progress_percent;
try
{
Status ret = LDR_ProgressiveLoad(10e-3, description, ARRAY_SIZE(description), &progress_percent);
switch(ret)
{
// no load active => no-op (skip code below)
case INFO::OK:
return 0;
// current task didn't complete. we only care about this insofar as the
// load process is therefore not yet finished.
case ERR::TIMED_OUT:
break;
// just finished loading
case INFO::ALL_COMPLETE:
g_Game->ReallyStartGame();
wcscpy_s(description, ARRAY_SIZE(description), L"Game is starting..");
// LDR_ProgressiveLoad returns L""; set to valid text to
// avoid problems in converting to JSString
break;
// error!
default:
WARN_RETURN_STATUS_IF_ERR(ret);
// can't do this above due to legit ERR::TIMED_OUT
break;
}
}
catch (PSERROR_Game_World_MapLoadFailed& e)
{
// Map loading failed
// Call script function to do the actual work
// (delete game data, switch GUI page, show error, etc.)
CancelLoad(CStr(e.what()).FromUTF8());
}
GUI_DisplayLoadProgress(progress_percent, description);
return 0;
}
static void RendererIncrementalLoad()
{
PROFILE3("renderer incremental load");
const double maxTime = 0.1f;
double startTime = timer_Time();
bool more;
do {
more = g_Renderer.GetTextureManager().MakeProgress();
}
while (more && timer_Time() - startTime < maxTime);
}
static void Frame()
{
g_Profiler2.RecordFrameStart();
PROFILE2("frame");
g_Profiler2.IncrementFrameNumber();
PROFILE2_ATTR("%d", g_Profiler2.GetFrameNumber());
ogl_WarnIfError();
// get elapsed time
const double time = timer_Time();
g_frequencyFilter->Update(time);
// .. old method - "exact" but contains jumps
#if 0
static double last_time;
const double time = timer_Time();
const float TimeSinceLastFrame = (float)(time-last_time);
last_time = time;
ONCE(return); // first call: set last_time and return
// .. new method - filtered and more smooth, but errors may accumulate
#else
const float realTimeSinceLastFrame = 1.0 / g_frequencyFilter->SmoothedFrequency();
#endif
ENSURE(realTimeSinceLastFrame > 0.0f);
// Decide if update is necessary
bool need_update = true;
// If we are not running a multiplayer game, disable updates when the game is
// minimized or out of focus and relinquish the CPU a bit, in order to make
// debugging easier.
if (g_PauseOnFocusLoss && !g_NetClient && !g_app_has_focus)
{
PROFILE3("non-focus delay");
need_update = false;
// don't use SDL_WaitEvent: don't want the main loop to freeze until app focus is restored
SDL_Delay(10);
}
// this scans for changed files/directories and reloads them, thus
// allowing hotloading (changes are immediately assimilated in-game).
ReloadChangedFiles();
ProgressiveLoad();
RendererIncrementalLoad();
PumpEvents();
// if the user quit by closing the window, the GL context will be broken and
// may crash when we call Render() on some drivers, so leave this loop
// before rendering
if (g_Shutdown != ShutdownType::None)
return;
// respond to pumped resize events
if (g_ResizedW || g_ResizedH)
{
g_VideoMode.ResizeWindow(g_ResizedW, g_ResizedH);
g_ResizedW = g_ResizedH = 0;
}
if (g_NetClient)
g_NetClient->Poll();
ogl_WarnIfError();
g_GUI->TickObjects();
ogl_WarnIfError();
if (g_RLInterface)
g_RLInterface->TryApplyMessage();
if (g_Game && g_Game->IsGameStarted() && need_update)
{
if (!g_RLInterface)
g_Game->Update(realTimeSinceLastFrame);
g_Game->GetView()->Update(float(realTimeSinceLastFrame));
}
// Keep us connected to any XMPP servers
if (g_XmppClient)
g_XmppClient->recv();
g_UserReporter.Update();
g_Console->Update(realTimeSinceLastFrame);
ogl_WarnIfError();
if (g_SoundManager)
g_SoundManager->IdleTask();
if (ShouldRender())
{
Render();
{
PROFILE3("swap buffers");
SDL_GL_SwapWindow(g_VideoMode.GetWindow());
ogl_WarnIfError();
}
g_Renderer.OnSwapBuffers();
}
g_Profiler.Frame();
g_GameRestarted = false;
LimitFPS();
}
static void NonVisualFrame()
{
g_Profiler2.RecordFrameStart();
PROFILE2("frame");
g_Profiler2.IncrementFrameNumber();
PROFILE2_ATTR("%d", g_Profiler2.GetFrameNumber());
static u32 turn = 0;
- debug_printf("Turn %u (%u)...\n", turn++, DEFAULT_TURN_LENGTH_SP);
+ debug_printf("Turn %u (%u)...\n", turn++, DEFAULT_TURN_LENGTH);
- g_Game->GetSimulation2()->Update(DEFAULT_TURN_LENGTH_SP);
+ g_Game->GetSimulation2()->Update(DEFAULT_TURN_LENGTH);
g_Profiler.Frame();
if (g_Game->IsGameFinished())
QuitEngine();
}
static void MainControllerInit()
{
// add additional input handlers only needed by this controller:
// must be registered after gui_handler. Should mayhap even be last.
in_add_handler(MainInputHandler);
}
static void MainControllerShutdown()
{
in_reset_handlers();
}
static void StartRLInterface(CmdLineArgs args)
{
std::string server_address;
CFG_GET_VAL("rlinterface.address", server_address);
if (!args.Get("rl-interface").empty())
server_address = args.Get("rl-interface");
g_RLInterface = std::make_unique(server_address.c_str());
debug_printf("RL interface listening on %s\n", server_address.c_str());
}
// moved into a helper function to ensure args is destroyed before
// exit(), which may result in a memory leak.
static void RunGameOrAtlas(int argc, const char* argv[])
{
CmdLineArgs args(argc, argv);
g_args = args;
if (args.Has("version"))
{
debug_printf("Pyrogenesis %s\n", engine_version);
return;
}
if (args.Has("autostart-nonvisual") && args.Get("autostart").empty() && !args.Has("rl-interface"))
{
LOGERROR("-autostart-nonvisual cant be used alone. A map with -autostart=\"TYPEDIR/MAPNAME\" is needed.");
return;
}
if (args.Has("unique-logs"))
g_UniqueLogPostfix = L"_" + std::to_wstring(std::time(nullptr)) + L"_" + std::to_wstring(getpid());
const bool isVisualReplay = args.Has("replay-visual");
const bool isNonVisualReplay = args.Has("replay");
const bool isNonVisual = args.Has("autostart-nonvisual");
const bool isUsingRLInterface = args.Has("rl-interface");
const OsPath replayFile(
isVisualReplay ? args.Get("replay-visual") :
isNonVisualReplay ? args.Get("replay") : "");
if (isVisualReplay || isNonVisualReplay)
{
if (!FileExists(replayFile))
{
debug_printf("ERROR: The requested replay file '%s' does not exist!\n", replayFile.string8().c_str());
return;
}
if (DirectoryExists(replayFile))
{
debug_printf("ERROR: The requested replay file '%s' is a directory!\n", replayFile.string8().c_str());
return;
}
}
std::vector modsToInstall;
for (const CStr& arg : args.GetArgsWithoutName())
{
const OsPath modPath(arg);
if (!CModInstaller::IsDefaultModExtension(modPath.Extension()))
{
debug_printf("Skipping file '%s' which does not have a mod file extension.\n", modPath.string8().c_str());
continue;
}
if (!FileExists(modPath))
{
debug_printf("ERROR: The mod file '%s' does not exist!\n", modPath.string8().c_str());
continue;
}
if (DirectoryExists(modPath))
{
debug_printf("ERROR: The mod file '%s' is a directory!\n", modPath.string8().c_str());
continue;
}
modsToInstall.emplace_back(std::move(modPath));
}
// We need to initialize SpiderMonkey and libxml2 in the main thread before
// any thread uses them. So initialize them here before we might run Atlas.
ScriptEngine scriptEngine;
CXeromyces::Startup();
if (ATLAS_RunIfOnCmdLine(args, false))
{
CXeromyces::Terminate();
return;
}
if (isNonVisualReplay)
{
if (!args.Has("mod"))
{
LOGERROR("At least one mod should be specified! Did you mean to add the argument '-mod=public'?");
CXeromyces::Terminate();
return;
}
Paths paths(args);
g_VFS = CreateVfs();
g_VFS->Mount(L"cache/", paths.Cache(), VFS_MOUNT_ARCHIVABLE);
MountMods(paths, GetMods(args, INIT_MODS));
{
CReplayPlayer replay;
replay.Load(replayFile);
replay.Replay(
args.Has("serializationtest"),
args.Has("rejointest") ? args.Get("rejointest").ToInt() : -1,
args.Has("ooslog"),
!args.Has("hashtest-full") || args.Get("hashtest-full") == "true",
args.Has("hashtest-quick") && args.Get("hashtest-quick") == "true");
}
g_VFS.reset();
CXeromyces::Terminate();
return;
}
// run in archive-building mode if requested
if (args.Has("archivebuild"))
{
Paths paths(args);
OsPath mod(args.Get("archivebuild"));
OsPath zip;
if (args.Has("archivebuild-output"))
zip = args.Get("archivebuild-output");
else
zip = mod.Filename().ChangeExtension(L".zip");
CArchiveBuilder builder(mod, paths.Cache());
// Add mods provided on the command line
// NOTE: We do not handle mods in the user mod path here
std::vector mods = args.GetMultiple("mod");
for (size_t i = 0; i < mods.size(); ++i)
builder.AddBaseMod(paths.RData()/"mods"/mods[i]);
builder.Build(zip, args.Has("archivebuild-compress"));
CXeromyces::Terminate();
return;
}
const double res = timer_Resolution();
g_frequencyFilter = CreateFrequencyFilter(res, 30.0);
// run the game
int flags = INIT_MODS;
do
{
g_Shutdown = ShutdownType::None;
if (!Init(args, flags))
{
flags &= ~INIT_MODS;
Shutdown(SHUTDOWN_FROM_CONFIG);
continue;
}
std::vector installedMods;
if (!modsToInstall.empty())
{
Paths paths(args);
CModInstaller installer(paths.UserData() / "mods", paths.Cache());
// Install the mods without deleting the pyromod files
for (const OsPath& modPath : modsToInstall)
installer.Install(modPath, g_ScriptContext, true);
installedMods = installer.GetInstalledMods();
}
if (isNonVisual)
{
InitNonVisual(args);
if (isUsingRLInterface)
StartRLInterface(args);
while (g_Shutdown == ShutdownType::None)
{
if (isUsingRLInterface)
g_RLInterface->TryApplyMessage();
else
NonVisualFrame();
}
}
else
{
InitGraphics(args, 0, installedMods);
MainControllerInit();
if (isUsingRLInterface)
StartRLInterface(args);
while (g_Shutdown == ShutdownType::None)
Frame();
}
// Do not install mods again in case of restart (typically from the mod selector)
modsToInstall.clear();
Shutdown(0);
MainControllerShutdown();
flags &= ~INIT_MODS;
} while (g_Shutdown == ShutdownType::Restart);
#if OS_MACOSX
if (g_Shutdown == ShutdownType::RestartAsAtlas)
startNewAtlasProcess();
#else
if (g_Shutdown == ShutdownType::RestartAsAtlas)
ATLAS_RunIfOnCmdLine(args, true);
#endif
CXeromyces::Terminate();
}
#if OS_ANDROID
// In Android we compile the engine as a shared library, not an executable,
// so rename main() to a different symbol that the wrapper library can load
#undef main
#define main pyrogenesis_main
extern "C" __attribute__((visibility ("default"))) int main(int argc, char* argv[]);
#endif
extern "C" int main(int argc, char* argv[])
{
#if OS_UNIX
// Don't allow people to run the game with root permissions,
// because bad things can happen, check before we do anything
if (geteuid() == 0)
{
std::cerr << "********************************************************\n"
<< "WARNING: Attempted to run the game with root permission!\n"
<< "This is not allowed because it can alter home directory \n"
<< "permissions and opens your system to vulnerabilities. \n"
<< "(You received this message because you were either \n"
<<" logged in as root or used e.g. the 'sudo' command.) \n"
<< "********************************************************\n\n";
return EXIT_FAILURE;
}
#endif // OS_UNIX
EarlyInit(); // must come at beginning of main
RunGameOrAtlas(argc, const_cast(argv));
// Shut down profiler initialised by EarlyInit
g_Profiler2.Shutdown();
return EXIT_SUCCESS;
}
Index: ps/trunk/source/network/NetClient.cpp
===================================================================
--- ps/trunk/source/network/NetClient.cpp (revision 25000)
+++ ps/trunk/source/network/NetClient.cpp (revision 25001)
@@ -1,954 +1,962 @@
/* 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 .
*/
#include "precompiled.h"
#include "NetClient.h"
#include "NetClientTurnManager.h"
#include "NetMessage.h"
#include "NetSession.h"
#include "lib/byte_order.h"
#include "lib/external_libraries/enet.h"
#include "lib/external_libraries/libsdl.h"
#include "lib/sysdep/sysdep.h"
#include "lobby/IXmppClient.h"
#include "ps/CConsole.h"
#include "ps/CLogger.h"
#include "ps/Compress.h"
#include "ps/CStr.h"
#include "ps/Game.h"
#include "ps/Loader.h"
#include "ps/Profile.h"
#include "ps/Threading.h"
#include "scriptinterface/ScriptInterface.h"
#include "simulation2/Simulation2.h"
#include "network/StunClient.h"
+/**
+ * Once ping goes above turn length * command delay,
+ * the game will start 'freezing' for other clients while we catch up.
+ * Since commands are sent client -> server -> client, divide by 2.
+ * (duplicated in NetServer.cpp to avoid having to fetch the constants in a header file)
+ */
+constexpr u32 NETWORK_BAD_PING = DEFAULT_TURN_LENGTH * COMMAND_DELAY_MP / 2;
+
CNetClient *g_NetClient = NULL;
/**
* Async task for receiving the initial game state when rejoining an
* in-progress network game.
*/
class CNetFileReceiveTask_ClientRejoin : public CNetFileReceiveTask
{
NONCOPYABLE(CNetFileReceiveTask_ClientRejoin);
public:
CNetFileReceiveTask_ClientRejoin(CNetClient& client)
: m_Client(client)
{
}
virtual void OnComplete()
{
// We've received the game state from the server
// Save it so we can use it after the map has finished loading
m_Client.m_JoinSyncBuffer = m_Buffer;
// Pretend the server told us to start the game
CGameStartMessage start;
m_Client.HandleMessage(&start);
}
private:
CNetClient& m_Client;
};
CNetClient::CNetClient(CGame* game) :
m_Session(NULL),
m_UserName(L"anonymous"),
m_HostID((u32)-1), m_ClientTurnManager(NULL), m_Game(game),
m_GameAttributes(game->GetSimulation2()->GetScriptInterface().GetGeneralJSContext()),
m_LastConnectionCheck(0),
m_ServerAddress(),
m_ServerPort(0),
m_Rejoin(false)
{
m_Game->SetTurnManager(NULL); // delete the old local turn manager so we don't accidentally use it
void* context = this;
JS_AddExtraGCRootsTracer(GetScriptInterface().GetGeneralJSContext(), CNetClient::Trace, this);
// Set up transitions for session
AddTransition(NCS_UNCONNECTED, (uint)NMT_CONNECT_COMPLETE, NCS_CONNECT, (void*)&OnConnect, context);
AddTransition(NCS_CONNECT, (uint)NMT_SERVER_HANDSHAKE, NCS_HANDSHAKE, (void*)&OnHandshake, context);
AddTransition(NCS_HANDSHAKE, (uint)NMT_SERVER_HANDSHAKE_RESPONSE, NCS_AUTHENTICATE, (void*)&OnHandshakeResponse, context);
AddTransition(NCS_AUTHENTICATE, (uint)NMT_AUTHENTICATE, NCS_AUTHENTICATE, (void*)&OnAuthenticateRequest, context);
AddTransition(NCS_AUTHENTICATE, (uint)NMT_AUTHENTICATE_RESULT, NCS_INITIAL_GAMESETUP, (void*)&OnAuthenticate, context);
AddTransition(NCS_INITIAL_GAMESETUP, (uint)NMT_GAME_SETUP, NCS_PREGAME, (void*)&OnGameSetup, context);
AddTransition(NCS_PREGAME, (uint)NMT_CHAT, NCS_PREGAME, (void*)&OnChat, context);
AddTransition(NCS_PREGAME, (uint)NMT_READY, NCS_PREGAME, (void*)&OnReady, context);
AddTransition(NCS_PREGAME, (uint)NMT_GAME_SETUP, NCS_PREGAME, (void*)&OnGameSetup, context);
AddTransition(NCS_PREGAME, (uint)NMT_PLAYER_ASSIGNMENT, NCS_PREGAME, (void*)&OnPlayerAssignment, context);
AddTransition(NCS_PREGAME, (uint)NMT_KICKED, NCS_PREGAME, (void*)&OnKicked, context);
AddTransition(NCS_PREGAME, (uint)NMT_CLIENT_TIMEOUT, NCS_PREGAME, (void*)&OnClientTimeout, context);
AddTransition(NCS_PREGAME, (uint)NMT_CLIENT_PERFORMANCE, NCS_PREGAME, (void*)&OnClientPerformance, context);
AddTransition(NCS_PREGAME, (uint)NMT_GAME_START, NCS_LOADING, (void*)&OnGameStart, context);
AddTransition(NCS_PREGAME, (uint)NMT_JOIN_SYNC_START, NCS_JOIN_SYNCING, (void*)&OnJoinSyncStart, context);
AddTransition(NCS_JOIN_SYNCING, (uint)NMT_CHAT, NCS_JOIN_SYNCING, (void*)&OnChat, context);
AddTransition(NCS_JOIN_SYNCING, (uint)NMT_GAME_SETUP, NCS_JOIN_SYNCING, (void*)&OnGameSetup, context);
AddTransition(NCS_JOIN_SYNCING, (uint)NMT_PLAYER_ASSIGNMENT, NCS_JOIN_SYNCING, (void*)&OnPlayerAssignment, context);
AddTransition(NCS_JOIN_SYNCING, (uint)NMT_KICKED, NCS_JOIN_SYNCING, (void*)&OnKicked, context);
AddTransition(NCS_JOIN_SYNCING, (uint)NMT_CLIENT_TIMEOUT, NCS_JOIN_SYNCING, (void*)&OnClientTimeout, context);
AddTransition(NCS_JOIN_SYNCING, (uint)NMT_CLIENT_PERFORMANCE, NCS_JOIN_SYNCING, (void*)&OnClientPerformance, context);
AddTransition(NCS_JOIN_SYNCING, (uint)NMT_GAME_START, NCS_JOIN_SYNCING, (void*)&OnGameStart, context);
AddTransition(NCS_JOIN_SYNCING, (uint)NMT_SIMULATION_COMMAND, NCS_JOIN_SYNCING, (void*)&OnInGame, context);
AddTransition(NCS_JOIN_SYNCING, (uint)NMT_END_COMMAND_BATCH, NCS_JOIN_SYNCING, (void*)&OnJoinSyncEndCommandBatch, context);
AddTransition(NCS_JOIN_SYNCING, (uint)NMT_LOADED_GAME, NCS_INGAME, (void*)&OnLoadedGame, context);
AddTransition(NCS_LOADING, (uint)NMT_CHAT, NCS_LOADING, (void*)&OnChat, context);
AddTransition(NCS_LOADING, (uint)NMT_GAME_SETUP, NCS_LOADING, (void*)&OnGameSetup, context);
AddTransition(NCS_LOADING, (uint)NMT_PLAYER_ASSIGNMENT, NCS_LOADING, (void*)&OnPlayerAssignment, context);
AddTransition(NCS_LOADING, (uint)NMT_KICKED, NCS_LOADING, (void*)&OnKicked, context);
AddTransition(NCS_LOADING, (uint)NMT_CLIENT_TIMEOUT, NCS_LOADING, (void*)&OnClientTimeout, context);
AddTransition(NCS_LOADING, (uint)NMT_CLIENT_PERFORMANCE, NCS_LOADING, (void*)&OnClientPerformance, context);
AddTransition(NCS_LOADING, (uint)NMT_CLIENTS_LOADING, NCS_LOADING, (void*)&OnClientsLoading, context);
AddTransition(NCS_LOADING, (uint)NMT_LOADED_GAME, NCS_INGAME, (void*)&OnLoadedGame, context);
AddTransition(NCS_INGAME, (uint)NMT_REJOINED, NCS_INGAME, (void*)&OnRejoined, context);
AddTransition(NCS_INGAME, (uint)NMT_KICKED, NCS_INGAME, (void*)&OnKicked, context);
AddTransition(NCS_INGAME, (uint)NMT_CLIENT_TIMEOUT, NCS_INGAME, (void*)&OnClientTimeout, context);
AddTransition(NCS_INGAME, (uint)NMT_CLIENT_PERFORMANCE, NCS_INGAME, (void*)&OnClientPerformance, context);
AddTransition(NCS_INGAME, (uint)NMT_CLIENTS_LOADING, NCS_INGAME, (void*)&OnClientsLoading, context);
AddTransition(NCS_INGAME, (uint)NMT_CLIENT_PAUSED, NCS_INGAME, (void*)&OnClientPaused, context);
AddTransition(NCS_INGAME, (uint)NMT_CHAT, NCS_INGAME, (void*)&OnChat, context);
AddTransition(NCS_INGAME, (uint)NMT_GAME_SETUP, NCS_INGAME, (void*)&OnGameSetup, context);
AddTransition(NCS_INGAME, (uint)NMT_PLAYER_ASSIGNMENT, NCS_INGAME, (void*)&OnPlayerAssignment, context);
AddTransition(NCS_INGAME, (uint)NMT_SIMULATION_COMMAND, NCS_INGAME, (void*)&OnInGame, context);
AddTransition(NCS_INGAME, (uint)NMT_SYNC_ERROR, NCS_INGAME, (void*)&OnInGame, context);
AddTransition(NCS_INGAME, (uint)NMT_END_COMMAND_BATCH, NCS_INGAME, (void*)&OnInGame, context);
// Set first state
SetFirstState(NCS_UNCONNECTED);
}
CNetClient::~CNetClient()
{
// Try to flush messages before dying (probably fails).
if (m_ClientTurnManager)
m_ClientTurnManager->OnDestroyConnection();
DestroyConnection();
JS_RemoveExtraGCRootsTracer(GetScriptInterface().GetGeneralJSContext(), CNetClient::Trace, this);
}
void CNetClient::TraceMember(JSTracer *trc)
{
for (JS::Heap& guiMessage : m_GuiMessageQueue)
JS::TraceEdge(trc, &guiMessage, "m_GuiMessageQueue");
}
void CNetClient::SetUserName(const CStrW& username)
{
ENSURE(!m_Session); // must be called before we start the connection
m_UserName = username;
}
void CNetClient::SetHostingPlayerName(const CStr& hostingPlayerName)
{
m_HostingPlayerName = hostingPlayerName;
}
void CNetClient::SetGamePassword(const CStr& hashedPassword)
{
m_Password = hashedPassword;
}
void CNetClient::SetControllerSecret(const std::string& secret)
{
m_ControllerSecret = secret;
}
bool CNetClient::SetupConnection(ENetHost* enetClient)
{
CNetClientSession* session = new CNetClientSession(*this);
bool ok = session->Connect(m_ServerAddress, m_ServerPort, enetClient);
SetAndOwnSession(session);
m_PollingThread = std::thread(Threading::HandleExceptions::Wrapper, m_Session);
return ok;
}
void CNetClient::SetupServerData(CStr address, u16 port, bool stun)
{
ENSURE(!m_Session);
m_ServerAddress = address;
m_ServerPort = port;
m_UseSTUN = stun;
}
void CNetClient::HandleGetServerDataFailed(const CStr& error)
{
if (m_Session)
return;
PushGuiMessage(
"type", "serverdata",
"status", "failed",
"reason", error
);
}
bool CNetClient::TryToConnect(const CStr& hostJID)
{
if (m_Session)
return false;
if (m_ServerAddress.empty())
{
PushGuiMessage(
"type", "netstatus",
"status", "disconnected",
"reason", static_cast(NDR_SERVER_REFUSED));
return false;
}
ENetHost* enetClient = nullptr;
if (g_XmppClient && m_UseSTUN)
{
// Find an unused port
for (int i = 0; i < 5 && !enetClient; ++i)
{
// Ports below 1024 are privileged on unix
u16 port = 1024 + rand() % (UINT16_MAX - 1024);
ENetAddress hostAddr{ ENET_HOST_ANY, port };
enetClient = enet_host_create(&hostAddr, 1, 1, 0, 0);
++hostAddr.port;
}
if (!enetClient)
{
PushGuiMessage(
"type", "netstatus",
"status", "disconnected",
"reason", static_cast(NDR_STUN_PORT_FAILED));
return false;
}
StunClient::StunEndpoint stunEndpoint;
if (!StunClient::FindStunEndpointJoin(*enetClient, stunEndpoint))
{
PushGuiMessage(
"type", "netstatus",
"status", "disconnected",
"reason", static_cast(NDR_STUN_ENDPOINT_FAILED));
return false;
}
g_XmppClient->SendStunEndpointToHost(stunEndpoint, hostJID);
SDL_Delay(1000);
StunClient::SendHolePunchingMessages(*enetClient, m_ServerAddress, m_ServerPort);
}
if (!g_NetClient->SetupConnection(enetClient))
{
PushGuiMessage(
"type", "netstatus",
"status", "disconnected",
"reason", static_cast(NDR_UNKNOWN));
return false;
}
return true;
}
void CNetClient::SetAndOwnSession(CNetClientSession* session)
{
delete m_Session;
m_Session = session;
}
void CNetClient::DestroyConnection()
{
if (m_Session)
m_Session->Shutdown();
if (m_PollingThread.joinable())
// Use detach() over join() because we don't want to wait for the session
// (which may be polling or trying to send messages).
m_PollingThread.detach();
// The polling thread will cleanup the session on its own,
// mark it as nullptr here so we know we're done using it.
m_Session = nullptr;
}
void CNetClient::Poll()
{
if (!m_Session)
return;
PROFILE3("NetClient::poll");
CheckServerConnection();
m_Session->ProcessPolledMessages();
}
void CNetClient::CheckServerConnection()
{
// Trigger local warnings if the connection to the server is bad.
// At most once per second.
std::time_t now = std::time(nullptr);
if (now <= m_LastConnectionCheck)
return;
m_LastConnectionCheck = now;
// Report if we are losing the connection to the server
u32 lastReceived = m_Session->GetLastReceivedTime();
if (lastReceived > NETWORK_WARNING_TIMEOUT)
{
PushGuiMessage(
"type", "netwarn",
"warntype", "server-timeout",
"lastReceivedTime", lastReceived);
return;
}
- // Report if we have a bad ping to the server
+ // Report if we have a bad ping to the server.
u32 meanRTT = m_Session->GetMeanRTT();
- if (meanRTT > DEFAULT_TURN_LENGTH_MP)
+ if (meanRTT > NETWORK_BAD_PING)
{
PushGuiMessage(
"type", "netwarn",
"warntype", "server-latency",
"meanRTT", meanRTT);
}
}
void CNetClient::GuiPoll(JS::MutableHandleValue ret)
{
if (m_GuiMessageQueue.empty())
{
ret.setUndefined();
return;
}
ret.set(m_GuiMessageQueue.front());
m_GuiMessageQueue.pop_front();
}
std::string CNetClient::TestReadGuiMessages()
{
ScriptRequest rq(GetScriptInterface());
std::string r;
JS::RootedValue msg(rq.cx);
while (true)
{
GuiPoll(&msg);
if (msg.isUndefined())
break;
r += GetScriptInterface().ToString(&msg) + "\n";
}
return r;
}
const ScriptInterface& CNetClient::GetScriptInterface()
{
return m_Game->GetSimulation2()->GetScriptInterface();
}
void CNetClient::PostPlayerAssignmentsToScript()
{
ScriptRequest rq(GetScriptInterface());
JS::RootedValue newAssignments(rq.cx);
ScriptInterface::CreateObject(rq, &newAssignments);
for (const std::pair& p : m_PlayerAssignments)
{
JS::RootedValue assignment(rq.cx);
ScriptInterface::CreateObject(
rq,
&assignment,
"name", p.second.m_Name,
"player", p.second.m_PlayerID,
"status", p.second.m_Status);
GetScriptInterface().SetProperty(newAssignments, p.first.c_str(), assignment);
}
PushGuiMessage(
"type", "players",
"newAssignments", newAssignments);
}
bool CNetClient::SendMessage(const CNetMessage* message)
{
if (!m_Session)
return false;
return m_Session->SendMessage(message);
}
void CNetClient::HandleConnect()
{
Update((uint)NMT_CONNECT_COMPLETE, NULL);
}
void CNetClient::HandleDisconnect(u32 reason)
{
PushGuiMessage(
"type", "netstatus",
"status", "disconnected",
"reason", reason);
DestroyConnection();
// Update the state immediately to UNCONNECTED (don't bother with FSM transitions since
// we'd need one for every single state, and we don't need to use per-state actions)
SetCurrState(NCS_UNCONNECTED);
}
void CNetClient::SendGameSetupMessage(JS::MutableHandleValue attrs, const ScriptInterface& scriptInterface)
{
CGameSetupMessage gameSetup(scriptInterface);
gameSetup.m_Data = attrs;
SendMessage(&gameSetup);
}
void CNetClient::SendAssignPlayerMessage(const int playerID, const CStr& guid)
{
CAssignPlayerMessage assignPlayer;
assignPlayer.m_PlayerID = playerID;
assignPlayer.m_GUID = guid;
SendMessage(&assignPlayer);
}
void CNetClient::SendChatMessage(const std::wstring& text)
{
CChatMessage chat;
chat.m_Message = text;
SendMessage(&chat);
}
void CNetClient::SendReadyMessage(const int status)
{
CReadyMessage readyStatus;
readyStatus.m_Status = status;
SendMessage(&readyStatus);
}
void CNetClient::SendClearAllReadyMessage()
{
CClearAllReadyMessage clearAllReady;
SendMessage(&clearAllReady);
}
void CNetClient::SendStartGameMessage()
{
CGameStartMessage gameStart;
SendMessage(&gameStart);
}
void CNetClient::SendRejoinedMessage()
{
CRejoinedMessage rejoinedMessage;
SendMessage(&rejoinedMessage);
}
void CNetClient::SendKickPlayerMessage(const CStrW& playerName, bool ban)
{
CKickedMessage kickPlayer;
kickPlayer.m_Name = playerName;
kickPlayer.m_Ban = ban;
SendMessage(&kickPlayer);
}
void CNetClient::SendPausedMessage(bool pause)
{
CClientPausedMessage pausedMessage;
pausedMessage.m_Pause = pause;
SendMessage(&pausedMessage);
}
bool CNetClient::HandleMessage(CNetMessage* message)
{
// Handle non-FSM messages first
Status status = m_Session->GetFileTransferer().HandleMessageReceive(*message);
if (status == INFO::OK)
return true;
if (status != INFO::SKIPPED)
return false;
if (message->GetType() == NMT_FILE_TRANSFER_REQUEST)
{
CFileTransferRequestMessage* reqMessage = static_cast(message);
// TODO: we should support different transfer request types, instead of assuming
// it's always requesting the simulation state
std::stringstream stream;
LOGMESSAGERENDER("Serializing game at turn %u for rejoining player", m_ClientTurnManager->GetCurrentTurn());
u32 turn = to_le32(m_ClientTurnManager->GetCurrentTurn());
stream.write((char*)&turn, sizeof(turn));
bool ok = m_Game->GetSimulation2()->SerializeState(stream);
ENSURE(ok);
// Compress the content with zlib to save bandwidth
// (TODO: if this is still too large, compressing with e.g. LZMA works much better)
std::string compressed;
CompressZLib(stream.str(), compressed, true);
m_Session->GetFileTransferer().StartResponse(reqMessage->m_RequestID, compressed);
return true;
}
// Update FSM
bool ok = Update(message->GetType(), message);
if (!ok)
LOGERROR("Net client: Error running FSM update (type=%d state=%d)", (int)message->GetType(), (int)GetCurrState());
return ok;
}
void CNetClient::LoadFinished()
{
if (!m_JoinSyncBuffer.empty())
{
// We're rejoining a game, and just finished loading the initial map,
// so deserialize the saved game state now
std::string state;
DecompressZLib(m_JoinSyncBuffer, state, true);
std::stringstream stream(state);
u32 turn;
stream.read((char*)&turn, sizeof(turn));
turn = to_le32(turn);
LOGMESSAGE("Rejoining client deserializing state at turn %u\n", turn);
bool ok = m_Game->GetSimulation2()->DeserializeState(stream);
ENSURE(ok);
m_ClientTurnManager->ResetState(turn, turn);
PushGuiMessage(
"type", "netstatus",
"status", "join_syncing");
}
else
{
// Connecting at the start of a game, so we'll wait for other players to finish loading
PushGuiMessage(
"type", "netstatus",
"status", "waiting_for_players");
}
CLoadedGameMessage loaded;
loaded.m_CurrentTurn = m_ClientTurnManager->GetCurrentTurn();
SendMessage(&loaded);
}
void CNetClient::SendAuthenticateMessage()
{
CAuthenticateMessage authenticate;
authenticate.m_Name = m_UserName;
authenticate.m_Password = m_Password;
authenticate.m_ControllerSecret = m_ControllerSecret;
SendMessage(&authenticate);
}
bool CNetClient::OnConnect(void* context, CFsmEvent* event)
{
ENSURE(event->GetType() == (uint)NMT_CONNECT_COMPLETE);
CNetClient* client = static_cast(context);
client->PushGuiMessage(
"type", "netstatus",
"status", "connected");
return true;
}
bool CNetClient::OnHandshake(void* context, CFsmEvent* event)
{
ENSURE(event->GetType() == (uint)NMT_SERVER_HANDSHAKE);
CNetClient* client = static_cast(context);
CCliHandshakeMessage handshake;
handshake.m_MagicResponse = PS_PROTOCOL_MAGIC_RESPONSE;
handshake.m_ProtocolVersion = PS_PROTOCOL_VERSION;
handshake.m_SoftwareVersion = PS_PROTOCOL_VERSION;
client->SendMessage(&handshake);
return true;
}
bool CNetClient::OnHandshakeResponse(void* context, CFsmEvent* event)
{
ENSURE(event->GetType() == (uint)NMT_SERVER_HANDSHAKE_RESPONSE);
CNetClient* client = static_cast(context);
CSrvHandshakeResponseMessage* message = static_cast(event->GetParamRef());
client->m_GUID = message->m_GUID;
if (message->m_Flags & PS_NETWORK_FLAG_REQUIRE_LOBBYAUTH)
{
if (g_XmppClient && !client->m_HostingPlayerName.empty())
g_XmppClient->SendIqLobbyAuth(client->m_HostingPlayerName, client->m_GUID);
else
{
client->PushGuiMessage(
"type", "netstatus",
"status", "disconnected",
"reason", static_cast(NDR_LOBBY_AUTH_FAILED));
LOGMESSAGE("Net client: Couldn't send lobby auth xmpp message");
}
return true;
}
client->SendAuthenticateMessage();
return true;
}
bool CNetClient::OnAuthenticateRequest(void* context, CFsmEvent* event)
{
ENSURE(event->GetType() == (uint)NMT_AUTHENTICATE);
CNetClient* client = static_cast(context);
client->SendAuthenticateMessage();
return true;
}
bool CNetClient::OnAuthenticate(void* context, CFsmEvent* event)
{
ENSURE(event->GetType() == (uint)NMT_AUTHENTICATE_RESULT);
CNetClient* client = static_cast(context);
CAuthenticateResultMessage* message = static_cast(event->GetParamRef());
LOGMESSAGE("Net: Authentication result: host=%u, %s", message->m_HostID, utf8_from_wstring(message->m_Message));
client->m_HostID = message->m_HostID;
client->m_Rejoin = message->m_Code == ARC_OK_REJOINING;
client->m_IsController = message->m_IsController;
client->PushGuiMessage(
"type", "netstatus",
"status", "authenticated",
"rejoining", client->m_Rejoin);
return true;
}
bool CNetClient::OnChat(void* context, CFsmEvent* event)
{
ENSURE(event->GetType() == (uint)NMT_CHAT);
CNetClient* client = static_cast(context);
CChatMessage* message = static_cast(event->GetParamRef());
client->PushGuiMessage(
"type", "chat",
"guid", message->m_GUID,
"text", message->m_Message);
return true;
}
bool CNetClient::OnReady(void* context, CFsmEvent* event)
{
ENSURE(event->GetType() == (uint)NMT_READY);
CNetClient* client = static_cast(context);
CReadyMessage* message = static_cast(event->GetParamRef());
client->PushGuiMessage(
"type", "ready",
"guid", message->m_GUID,
"status", message->m_Status);
return true;
}
bool CNetClient::OnGameSetup(void* context, CFsmEvent* event)
{
ENSURE(event->GetType() == (uint)NMT_GAME_SETUP);
CNetClient* client = static_cast(context);
CGameSetupMessage* message = static_cast(event->GetParamRef());
client->m_GameAttributes = message->m_Data;
client->PushGuiMessage(
"type", "gamesetup",
"data", message->m_Data);
return true;
}
bool CNetClient::OnPlayerAssignment(void* context, CFsmEvent* event)
{
ENSURE(event->GetType() == (uint)NMT_PLAYER_ASSIGNMENT);
CNetClient* client = static_cast(context);
CPlayerAssignmentMessage* message = static_cast(event->GetParamRef());
// Unpack the message
PlayerAssignmentMap newPlayerAssignments;
for (size_t i = 0; i < message->m_Hosts.size(); ++i)
{
PlayerAssignment assignment;
assignment.m_Enabled = true;
assignment.m_Name = message->m_Hosts[i].m_Name;
assignment.m_PlayerID = message->m_Hosts[i].m_PlayerID;
assignment.m_Status = message->m_Hosts[i].m_Status;
newPlayerAssignments[message->m_Hosts[i].m_GUID] = assignment;
}
client->m_PlayerAssignments.swap(newPlayerAssignments);
client->PostPlayerAssignmentsToScript();
return true;
}
// This is called either when the host clicks the StartGame button or
// if this client rejoins and finishes the download of the simstate.
bool CNetClient::OnGameStart(void* context, CFsmEvent* event)
{
ENSURE(event->GetType() == (uint)NMT_GAME_START);
CNetClient* client = static_cast(context);
// Find the player assigned to our GUID
int player = -1;
if (client->m_PlayerAssignments.find(client->m_GUID) != client->m_PlayerAssignments.end())
player = client->m_PlayerAssignments[client->m_GUID].m_PlayerID;
client->m_ClientTurnManager = new CNetClientTurnManager(
*client->m_Game->GetSimulation2(), *client, client->m_HostID, client->m_Game->GetReplayLogger());
client->m_Game->SetPlayerID(player);
client->m_Game->StartGame(&client->m_GameAttributes, "");
client->PushGuiMessage("type", "start");
return true;
}
bool CNetClient::OnJoinSyncStart(void* context, CFsmEvent* event)
{
ENSURE(event->GetType() == (uint)NMT_JOIN_SYNC_START);
CNetClient* client = static_cast(context);
// The server wants us to start downloading the game state from it, so do so
client->m_Session->GetFileTransferer().StartTask(
shared_ptr(new CNetFileReceiveTask_ClientRejoin(*client))
);
return true;
}
bool CNetClient::OnJoinSyncEndCommandBatch(void* context, CFsmEvent* event)
{
ENSURE(event->GetType() == (uint)NMT_END_COMMAND_BATCH);
CNetClient* client = static_cast(context);
CEndCommandBatchMessage* endMessage = (CEndCommandBatchMessage*)event->GetParamRef();
client->m_ClientTurnManager->FinishedAllCommands(endMessage->m_Turn, endMessage->m_TurnLength);
// Execute all the received commands for the latest turn
client->m_ClientTurnManager->UpdateFastForward();
return true;
}
bool CNetClient::OnRejoined(void* context, CFsmEvent* event)
{
ENSURE(event->GetType() == (uint)NMT_REJOINED);
CNetClient* client = static_cast(context);
CRejoinedMessage* message = static_cast(event->GetParamRef());
client->PushGuiMessage(
"type", "rejoined",
"guid", message->m_GUID);
return true;
}
bool CNetClient::OnKicked(void *context, CFsmEvent* event)
{
ENSURE(event->GetType() == (uint)NMT_KICKED);
CNetClient* client = static_cast(context);
CKickedMessage* message = static_cast(event->GetParamRef());
client->PushGuiMessage(
"username", message->m_Name,
"type", "kicked",
"banned", message->m_Ban != 0);
return true;
}
bool CNetClient::OnClientTimeout(void *context, CFsmEvent* event)
{
// Report the timeout of some other client
ENSURE(event->GetType() == (uint)NMT_CLIENT_TIMEOUT);
CNetClient* client = static_cast(context);
CClientTimeoutMessage* message = static_cast(event->GetParamRef());
client->PushGuiMessage(
"type", "netwarn",
"warntype", "client-timeout",
"guid", message->m_GUID,
"lastReceivedTime", message->m_LastReceivedTime);
return true;
}
bool CNetClient::OnClientPerformance(void *context, CFsmEvent* event)
{
// Performance statistics for one or multiple clients
ENSURE(event->GetType() == (uint)NMT_CLIENT_PERFORMANCE);
CNetClient* client = static_cast(context);
CClientPerformanceMessage* message = static_cast(event->GetParamRef());
// Display warnings for other clients with bad ping
for (size_t i = 0; i < message->m_Clients.size(); ++i)
{
- if (message->m_Clients[i].m_MeanRTT < DEFAULT_TURN_LENGTH_MP || message->m_Clients[i].m_GUID == client->m_GUID)
+ if (message->m_Clients[i].m_MeanRTT < NETWORK_BAD_PING || message->m_Clients[i].m_GUID == client->m_GUID)
continue;
client->PushGuiMessage(
"type", "netwarn",
"warntype", "client-latency",
"guid", message->m_Clients[i].m_GUID,
"meanRTT", message->m_Clients[i].m_MeanRTT);
}
return true;
}
bool CNetClient::OnClientsLoading(void *context, CFsmEvent *event)
{
ENSURE(event->GetType() == (uint)NMT_CLIENTS_LOADING);
CNetClient* client = static_cast(context);
CClientsLoadingMessage* message = static_cast(event->GetParamRef());
std::vector guids;
guids.reserve(message->m_Clients.size());
for (const CClientsLoadingMessage::S_m_Clients& mClient : message->m_Clients)
guids.push_back(mClient.m_GUID);
client->PushGuiMessage(
"type", "clients-loading",
"guids", guids);
return true;
}
bool CNetClient::OnClientPaused(void *context, CFsmEvent *event)
{
ENSURE(event->GetType() == (uint)NMT_CLIENT_PAUSED);
CNetClient* client = static_cast(context);
CClientPausedMessage* message = static_cast(event->GetParamRef());
client->PushGuiMessage(
"type", "paused",
"pause", message->m_Pause != 0,
"guid", message->m_GUID);
return true;
}
bool CNetClient::OnLoadedGame(void* context, CFsmEvent* event)
{
ENSURE(event->GetType() == (uint)NMT_LOADED_GAME);
CNetClient* client = static_cast(context);
// All players have loaded the game - start running the turn manager
// so that the game begins
client->m_Game->SetTurnManager(client->m_ClientTurnManager);
client->PushGuiMessage(
"type", "netstatus",
"status", "active");
// If we have rejoined an in progress game, send the rejoined message to the server.
if (client->m_Rejoin)
client->SendRejoinedMessage();
return true;
}
bool CNetClient::OnInGame(void *context, CFsmEvent* event)
{
// TODO: should split each of these cases into a separate method
CNetClient* client = static_cast(context);
CNetMessage* message = static_cast(event->GetParamRef());
if (message)
{
if (message->GetType() == NMT_SIMULATION_COMMAND)
{
CSimulationMessage* simMessage = static_cast (message);
client->m_ClientTurnManager->OnSimulationMessage(simMessage);
}
else if (message->GetType() == NMT_SYNC_ERROR)
{
CSyncErrorMessage* syncMessage = static_cast (message);
client->m_ClientTurnManager->OnSyncError(syncMessage->m_Turn, syncMessage->m_HashExpected, syncMessage->m_PlayerNames);
}
else if (message->GetType() == NMT_END_COMMAND_BATCH)
{
CEndCommandBatchMessage* endMessage = static_cast (message);
client->m_ClientTurnManager->FinishedAllCommands(endMessage->m_Turn, endMessage->m_TurnLength);
}
}
return true;
}
Index: ps/trunk/source/network/NetClientTurnManager.cpp
===================================================================
--- ps/trunk/source/network/NetClientTurnManager.cpp (revision 25000)
+++ ps/trunk/source/network/NetClientTurnManager.cpp (revision 25001)
@@ -1,153 +1,153 @@
/* Copyright (C) 2019 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 .
*/
#include "precompiled.h"
#include "NetClientTurnManager.h"
#include "NetClient.h"
#include "ps/CLogger.h"
#include "ps/Pyrogenesis.h"
#include "ps/Replay.h"
#include "ps/Profile.h"
#include "ps/Util.h"
#include "simulation2/Simulation2.h"
#if 0
#define NETCLIENTTURN_LOG(...) debug_printf(__VA_ARGS__)
#else
#define NETCLIENTTURN_LOG(...)
#endif
extern CStrW g_UniqueLogPostfix;
CNetClientTurnManager::CNetClientTurnManager(CSimulation2& simulation, CNetClient& client, int clientId, IReplayLogger& replay)
- : CTurnManager(simulation, DEFAULT_TURN_LENGTH_MP, clientId, replay), m_NetClient(client)
+ : CTurnManager(simulation, DEFAULT_TURN_LENGTH, COMMAND_DELAY_MP, clientId, replay), m_NetClient(client)
{
}
void CNetClientTurnManager::PostCommand(JS::HandleValue data)
{
NETCLIENTTURN_LOG("PostCommand()\n");
// Transmit command to server
- CSimulationMessage msg(m_Simulation2.GetScriptInterface(), m_ClientId, m_PlayerId, m_CurrentTurn + COMMAND_DELAY, data);
+ CSimulationMessage msg(m_Simulation2.GetScriptInterface(), m_ClientId, m_PlayerId, m_CurrentTurn + m_CommandDelay, data);
m_NetClient.SendMessage(&msg);
// Add to our local queue
- //AddCommand(m_ClientId, m_PlayerId, data, m_CurrentTurn + COMMAND_DELAY);
+ //AddCommand(m_ClientId, m_PlayerId, data, m_CurrentTurn + m_CommandDelay);
// TODO: we should do this when the server stops sending our commands back to us
}
void CNetClientTurnManager::NotifyFinishedOwnCommands(u32 turn)
{
NETCLIENTTURN_LOG("NotifyFinishedOwnCommands(%d)\n", turn);
CEndCommandBatchMessage msg;
msg.m_Turn = turn;
// The turn-length field of the CEndCommandBatchMessage is currently only relevant
// when sending it from the server to the clients.
// It could be used to verify that the client simulated the correct turn length.
msg.m_TurnLength = 0;
m_NetClient.SendMessage(&msg);
}
void CNetClientTurnManager::NotifyFinishedUpdate(u32 turn)
{
bool quick = !TurnNeedsFullHash(turn);
std::string hash;
{
PROFILE3("state hash check");
ENSURE(m_Simulation2.ComputeStateHash(hash, quick));
}
NETCLIENTTURN_LOG("NotifyFinishedUpdate(%d, %hs)\n", turn, Hexify(hash).c_str());
m_Replay.Hash(hash, quick);
// Don't send the hash if OOS
if (m_HasSyncError)
return;
// Send message to the server
CSyncCheckMessage msg;
msg.m_Turn = turn;
msg.m_Hash = hash;
m_NetClient.SendMessage(&msg);
}
void CNetClientTurnManager::OnDestroyConnection()
{
// Attempt to flush messages before leaving.
// Notice the sending is not reliable and rarely makes it to the Server.
if (m_NetClient.GetCurrState() == NCS_INGAME)
- NotifyFinishedOwnCommands(m_CurrentTurn + COMMAND_DELAY);
+ NotifyFinishedOwnCommands(m_CurrentTurn + m_CommandDelay);
}
void CNetClientTurnManager::OnSimulationMessage(CSimulationMessage* msg)
{
// Command received from the server - store it for later execution
AddCommand(msg->m_Client, msg->m_Player, msg->m_Data, msg->m_Turn);
}
void CNetClientTurnManager::OnSyncError(u32 turn, const CStr& expectedHash, const std::vector& playerNames)
{
CStr expectedHashHex(Hexify(expectedHash));
NETCLIENTTURN_LOG("OnSyncError(%d, %hs)\n", turn, expectedHashHex.c_str());
// Only complain the first time
if (m_HasSyncError)
return;
m_HasSyncError = true;
std::string hash;
ENSURE(m_Simulation2.ComputeStateHash(hash, !TurnNeedsFullHash(turn)));
OsPath oosdumpPath(psLogDir() / (L"oos_dump" + g_UniqueLogPostfix + L".txt"));
std::ofstream file (OsString(oosdumpPath).c_str(), std::ofstream::out | std::ofstream::trunc);
m_Simulation2.DumpDebugState(file);
file.close();
std::ofstream binfile (OsString(oosdumpPath.ChangeExtension(L".dat")).c_str(), std::ofstream::out | std::ofstream::trunc | std::ofstream::binary);
m_Simulation2.SerializeState(binfile);
binfile.close();
std::stringstream playerNamesString;
std::vector playerNamesStrings;
playerNamesStrings.reserve(playerNames.size());
for (size_t i = 0; i < playerNames.size(); ++i)
{
CStr name = utf8_from_wstring(playerNames[i].m_Name);
playerNamesString << (i == 0 ? "" : ", ") << name;
playerNamesStrings.push_back(name);
}
LOGERROR("Out-Of-Sync on turn %d\nPlayers: %s\nDumping state to %s", turn, playerNamesString.str().c_str(), oosdumpPath.string8());
m_NetClient.PushGuiMessage(
"type", "out-of-sync",
"turn", turn,
"players", playerNamesStrings,
"expectedHash", expectedHashHex,
"hash", Hexify(hash),
"path_oos_dump", wstring_from_utf8(oosdumpPath.string8()),
"path_replay", wstring_from_utf8(m_Replay.GetDirectory().string8()));
}
Index: ps/trunk/source/network/NetServer.cpp
===================================================================
--- ps/trunk/source/network/NetServer.cpp (revision 25000)
+++ ps/trunk/source/network/NetServer.cpp (revision 25001)
@@ -1,1714 +1,1722 @@
/* 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 .
*/
#include "precompiled.h"
#include "NetServer.h"
#include "NetClient.h"
#include "NetMessage.h"
#include "NetSession.h"
#include "NetServerTurnManager.h"
#include "NetStats.h"
#include "lib/external_libraries/enet.h"
#include "lib/types.h"
#include "network/StunClient.h"
#include "ps/CLogger.h"
#include "ps/ConfigDB.h"
#include "ps/GUID.h"
#include "ps/Profile.h"
#include "ps/Threading.h"
#include "scriptinterface/ScriptContext.h"
#include "scriptinterface/ScriptInterface.h"
#include "simulation2/Simulation2.h"
#include "simulation2/system/TurnManager.h"
#if CONFIG2_MINIUPNPC
#include
#include
#include
#include
#endif
#include
/**
* Number of peers to allocate for the enet host.
* Limited by ENET_PROTOCOL_MAXIMUM_PEER_ID (4096).
*
* At most 8 players, 32 observers and 1 temporary connection to send the "server full" disconnect-reason.
*/
#define MAX_CLIENTS 41
#define DEFAULT_SERVER_NAME L"Unnamed Server"
constexpr int CHANNEL_COUNT = 1;
constexpr int FAILED_PASSWORD_TRIES_BEFORE_BAN = 3;
/**
* enet_host_service timeout (msecs).
* Smaller numbers may hurt performance; larger numbers will
* hurt latency responding to messages from game thread.
*/
static const int HOST_SERVICE_TIMEOUT = 50;
+/**
+ * Once ping goes above turn length * command delay,
+ * the game will start 'freezing' for other clients while we catch up.
+ * Since commands are sent client -> server -> client, divide by 2.
+ * (duplicated in NetServer.cpp to avoid having to fetch the constants in a header file)
+ */
+constexpr u32 NETWORK_BAD_PING = DEFAULT_TURN_LENGTH * COMMAND_DELAY_MP / 2;
+
CNetServer* g_NetServer = NULL;
static CStr DebugName(CNetServerSession* session)
{
if (session == NULL)
return "[unknown host]";
if (session->GetGUID().empty())
return "[unauthed host]";
return "[" + session->GetGUID().substr(0, 8) + "...]";
}
/**
* Async task for receiving the initial game state to be forwarded to another
* client that is rejoining an in-progress network game.
*/
class CNetFileReceiveTask_ServerRejoin : public CNetFileReceiveTask
{
NONCOPYABLE(CNetFileReceiveTask_ServerRejoin);
public:
CNetFileReceiveTask_ServerRejoin(CNetServerWorker& server, u32 hostID)
: m_Server(server), m_RejoinerHostID(hostID)
{
}
virtual void OnComplete()
{
// We've received the game state from an existing player - now
// we need to send it onwards to the newly rejoining player
// Find the session corresponding to the rejoining host (if any)
CNetServerSession* session = NULL;
for (CNetServerSession* serverSession : m_Server.m_Sessions)
{
if (serverSession->GetHostID() == m_RejoinerHostID)
{
session = serverSession;
break;
}
}
if (!session)
{
LOGMESSAGE("Net server: rejoining client disconnected before we sent to it");
return;
}
// Store the received state file, and tell the client to start downloading it from us
// TODO: this will get kind of confused if there's multiple clients downloading in parallel;
// they'll race and get whichever happens to be the latest received by the server,
// which should still work but isn't great
m_Server.m_JoinSyncFile = m_Buffer;
CJoinSyncStartMessage message;
session->SendMessage(&message);
}
private:
CNetServerWorker& m_Server;
u32 m_RejoinerHostID;
};
/*
* XXX: We use some non-threadsafe functions from the worker thread.
* See http://trac.wildfiregames.com/ticket/654
*/
CNetServerWorker::CNetServerWorker(bool useLobbyAuth, int autostartPlayers) :
m_AutostartPlayers(autostartPlayers),
m_LobbyAuth(useLobbyAuth),
m_Shutdown(false),
m_ScriptInterface(NULL),
m_NextHostID(1), m_Host(NULL), m_ControllerGUID(), m_Stats(NULL),
m_LastConnectionCheck(0)
{
m_State = SERVER_STATE_UNCONNECTED;
m_ServerTurnManager = NULL;
m_ServerName = DEFAULT_SERVER_NAME;
}
CNetServerWorker::~CNetServerWorker()
{
if (m_State != SERVER_STATE_UNCONNECTED)
{
// Tell the thread to shut down
{
std::lock_guard lock(m_WorkerMutex);
m_Shutdown = true;
}
// Wait for it to shut down cleanly
m_WorkerThread.join();
}
#if CONFIG2_MINIUPNPC
if (m_UPnPThread.joinable())
m_UPnPThread.detach();
#endif
// Clean up resources
delete m_Stats;
for (CNetServerSession* session : m_Sessions)
{
session->DisconnectNow(NDR_SERVER_SHUTDOWN);
delete session;
}
if (m_Host)
enet_host_destroy(m_Host);
delete m_ServerTurnManager;
}
void CNetServerWorker::SetPassword(const CStr& hashedPassword)
{
m_Password = hashedPassword;
}
void CNetServerWorker::SetControllerSecret(const std::string& secret)
{
m_ControllerSecret = secret;
}
bool CNetServerWorker::SetupConnection(const u16 port)
{
ENSURE(m_State == SERVER_STATE_UNCONNECTED);
ENSURE(!m_Host);
// Bind to default host
ENetAddress addr;
addr.host = ENET_HOST_ANY;
addr.port = port;
// Create ENet server
m_Host = enet_host_create(&addr, MAX_CLIENTS, CHANNEL_COUNT, 0, 0);
if (!m_Host)
{
LOGERROR("Net server: enet_host_create failed");
return false;
}
m_Stats = new CNetStatsTable();
if (CProfileViewer::IsInitialised())
g_ProfileViewer.AddRootTable(m_Stats);
m_State = SERVER_STATE_PREGAME;
// Launch the worker thread
m_WorkerThread = std::thread(Threading::HandleExceptions::Wrapper, this);
#if CONFIG2_MINIUPNPC
// Launch the UPnP thread
m_UPnPThread = std::thread(Threading::HandleExceptions::Wrapper);
#endif
return true;
}
#if CONFIG2_MINIUPNPC
void CNetServerWorker::SetupUPnP()
{
debug_SetThreadName("UPnP");
// Values we want to set.
char psPort[6];
sprintf_s(psPort, ARRAY_SIZE(psPort), "%d", PS_DEFAULT_PORT);
const char* leaseDuration = "0"; // Indefinite/permanent lease duration.
const char* description = "0AD Multiplayer";
const char* protocall = "UDP";
char internalIPAddress[64];
char externalIPAddress[40];
// Variables to hold the values that actually get set.
char intClient[40];
char intPort[6];
char duration[16];
// Intermediate variables.
bool allocatedUrls = false;
struct UPNPUrls urls;
struct IGDdatas data;
struct UPNPDev* devlist = NULL;
// Make sure everything is properly freed.
std::function freeUPnP = [&allocatedUrls, &urls, &devlist]()
{
if (allocatedUrls)
FreeUPNPUrls(&urls);
freeUPNPDevlist(devlist);
// IGDdatas does not need to be freed according to UPNP_GetIGDFromUrl
};
// Cached root descriptor URL.
std::string rootDescURL;
CFG_GET_VAL("network.upnprootdescurl", rootDescURL);
if (!rootDescURL.empty())
LOGMESSAGE("Net server: attempting to use cached root descriptor URL: %s", rootDescURL.c_str());
int ret = 0;
// Try a cached URL first
if (!rootDescURL.empty() && UPNP_GetIGDFromUrl(rootDescURL.c_str(), &urls, &data, internalIPAddress, sizeof(internalIPAddress)))
{
LOGMESSAGE("Net server: using cached IGD = %s", urls.controlURL);
ret = 1;
}
// No cached URL, or it did not respond. Try getting a valid UPnP device for 10 seconds.
#if defined(MINIUPNPC_API_VERSION) && MINIUPNPC_API_VERSION >= 14
else if ((devlist = upnpDiscover(10000, 0, 0, 0, 0, 2, 0)) != NULL)
#else
else if ((devlist = upnpDiscover(10000, 0, 0, 0, 0, 0)) != NULL)
#endif
{
ret = UPNP_GetValidIGD(devlist, &urls, &data, internalIPAddress, sizeof(internalIPAddress));
allocatedUrls = ret != 0; // urls is allocated on non-zero return values
}
else
{
LOGMESSAGE("Net server: upnpDiscover failed and no working cached URL.");
freeUPnP();
return;
}
switch (ret)
{
case 0:
LOGMESSAGE("Net server: No IGD found");
break;
case 1:
LOGMESSAGE("Net server: found valid IGD = %s", urls.controlURL);
break;
case 2:
LOGMESSAGE("Net server: found a valid, not connected IGD = %s, will try to continue anyway", urls.controlURL);
break;
case 3:
LOGMESSAGE("Net server: found a UPnP device unrecognized as IGD = %s, will try to continue anyway", urls.controlURL);
break;
default:
debug_warn(L"Unrecognized return value from UPNP_GetValidIGD");
}
// Try getting our external/internet facing IP. TODO: Display this on the game-setup page for conviniance.
ret = UPNP_GetExternalIPAddress(urls.controlURL, data.first.servicetype, externalIPAddress);
if (ret != UPNPCOMMAND_SUCCESS)
{
LOGMESSAGE("Net server: GetExternalIPAddress failed with code %d (%s)", ret, strupnperror(ret));
freeUPnP();
return;
}
LOGMESSAGE("Net server: ExternalIPAddress = %s", externalIPAddress);
// Try to setup port forwarding.
ret = UPNP_AddPortMapping(urls.controlURL, data.first.servicetype, psPort, psPort,
internalIPAddress, description, protocall, 0, leaseDuration);
if (ret != UPNPCOMMAND_SUCCESS)
{
LOGMESSAGE("Net server: AddPortMapping(%s, %s, %s) failed with code %d (%s)",
psPort, psPort, internalIPAddress, ret, strupnperror(ret));
freeUPnP();
return;
}
// Check that the port was actually forwarded.
ret = UPNP_GetSpecificPortMappingEntry(urls.controlURL,
data.first.servicetype,
psPort, protocall,
#if defined(MINIUPNPC_API_VERSION) && MINIUPNPC_API_VERSION >= 10
NULL/*remoteHost*/,
#endif
intClient, intPort, NULL/*desc*/,
NULL/*enabled*/, duration);
if (ret != UPNPCOMMAND_SUCCESS)
{
LOGMESSAGE("Net server: GetSpecificPortMappingEntry() failed with code %d (%s)", ret, strupnperror(ret));
freeUPnP();
return;
}
LOGMESSAGE("Net server: External %s:%s %s is redirected to internal %s:%s (duration=%s)",
externalIPAddress, psPort, protocall, intClient, intPort, duration);
// Cache root descriptor URL to try to avoid discovery next time.
g_ConfigDB.SetValueString(CFG_USER, "network.upnprootdescurl", urls.controlURL);
g_ConfigDB.WriteValueToFile(CFG_USER, "network.upnprootdescurl", urls.controlURL);
LOGMESSAGE("Net server: cached UPnP root descriptor URL as %s", urls.controlURL);
freeUPnP();
}
#endif // CONFIG2_MINIUPNPC
bool CNetServerWorker::SendMessage(ENetPeer* peer, const CNetMessage* message)
{
ENSURE(m_Host);
CNetServerSession* session = static_cast(peer->data);
return CNetHost::SendMessage(message, peer, DebugName(session).c_str());
}
bool CNetServerWorker::Broadcast(const CNetMessage* message, const std::vector& targetStates)
{
ENSURE(m_Host);
bool ok = true;
// TODO: this does lots of repeated message serialisation if we have lots
// of remote peers; could do it more efficiently if that's a real problem
for (CNetServerSession* session : m_Sessions)
if (std::find(targetStates.begin(), targetStates.end(), static_cast(session->GetCurrState())) != targetStates.end() &&
!session->SendMessage(message))
ok = false;
return ok;
}
void CNetServerWorker::RunThread(CNetServerWorker* data)
{
debug_SetThreadName("NetServer");
data->Run();
}
void CNetServerWorker::Run()
{
// The script context uses the profiler and therefore the thread must be registered before the context is created
g_Profiler2.RegisterCurrentThread("Net server");
// We create a new ScriptContext for this network thread, with a single ScriptInterface.
shared_ptr netServerContext = ScriptContext::CreateContext();
m_ScriptInterface = new ScriptInterface("Engine", "Net server", netServerContext);
m_GameAttributes.init(m_ScriptInterface->GetGeneralJSContext(), JS::UndefinedValue());
while (true)
{
if (!RunStep())
break;
// Implement autostart mode
if (m_State == SERVER_STATE_PREGAME && (int)m_PlayerAssignments.size() == m_AutostartPlayers)
StartGame();
// Update profiler stats
m_Stats->LatchHostState(m_Host);
}
// Clear roots before deleting their context
m_SavedCommands.clear();
SAFE_DELETE(m_ScriptInterface);
}
bool CNetServerWorker::RunStep()
{
// Check for messages from the game thread.
// (Do as little work as possible while the mutex is held open,
// to avoid performance problems and deadlocks.)
m_ScriptInterface->GetContext()->MaybeIncrementalGC(0.5f);
ScriptRequest rq(m_ScriptInterface);
std::vector newStartGame;
std::vector newGameAttributes;
std::vector> newLobbyAuths;
std::vector newTurnLength;
{
std::lock_guard lock(m_WorkerMutex);
if (m_Shutdown)
return false;
newStartGame.swap(m_StartGameQueue);
newGameAttributes.swap(m_GameAttributesQueue);
newLobbyAuths.swap(m_LobbyAuthQueue);
newTurnLength.swap(m_TurnLengthQueue);
}
if (!newGameAttributes.empty())
{
JS::RootedValue gameAttributesVal(rq.cx);
GetScriptInterface().ParseJSON(newGameAttributes.back(), &gameAttributesVal);
UpdateGameAttributes(&gameAttributesVal);
}
if (!newTurnLength.empty())
SetTurnLength(newTurnLength.back());
// Do StartGame last, so we have the most up-to-date game attributes when we start
if (!newStartGame.empty())
StartGame();
while (!newLobbyAuths.empty())
{
const std::pair& auth = newLobbyAuths.back();
ProcessLobbyAuth(auth.first, auth.second);
newLobbyAuths.pop_back();
}
// Perform file transfers
for (CNetServerSession* session : m_Sessions)
session->GetFileTransferer().Poll();
CheckClientConnections();
// Process network events:
ENetEvent event;
int status = enet_host_service(m_Host, &event, HOST_SERVICE_TIMEOUT);
if (status < 0)
{
LOGERROR("CNetServerWorker: enet_host_service failed (%d)", status);
// TODO: notify game that the server has shut down
return false;
}
if (status == 0)
{
// Reached timeout with no events - try again
return true;
}
// Process the event:
switch (event.type)
{
case ENET_EVENT_TYPE_CONNECT:
{
// Report the client address
char hostname[256] = "(error)";
enet_address_get_host_ip(&event.peer->address, hostname, ARRAY_SIZE(hostname));
LOGMESSAGE("Net server: Received connection from %s:%u", hostname, (unsigned int)event.peer->address.port);
// Set up a session object for this peer
CNetServerSession* session = new CNetServerSession(*this, event.peer);
m_Sessions.push_back(session);
SetupSession(session);
ENSURE(event.peer->data == NULL);
event.peer->data = session;
HandleConnect(session);
break;
}
case ENET_EVENT_TYPE_DISCONNECT:
{
// If there is an active session with this peer, then reset and delete it
CNetServerSession* session = static_cast(event.peer->data);
if (session)
{
LOGMESSAGE("Net server: Disconnected %s", DebugName(session).c_str());
// Remove the session first, so we won't send player-update messages to it
// when updating the FSM
m_Sessions.erase(remove(m_Sessions.begin(), m_Sessions.end(), session), m_Sessions.end());
session->Update((uint)NMT_CONNECTION_LOST, NULL);
delete session;
event.peer->data = NULL;
}
if (m_State == SERVER_STATE_LOADING)
CheckGameLoadStatus(NULL);
break;
}
case ENET_EVENT_TYPE_RECEIVE:
{
// If there is an active session with this peer, then process the message
CNetServerSession* session = static_cast(event.peer->data);
if (session)
{
// Create message from raw data
CNetMessage* msg = CNetMessageFactory::CreateMessage(event.packet->data, event.packet->dataLength, GetScriptInterface());
if (msg)
{
LOGMESSAGE("Net server: Received message %s of size %lu from %s", msg->ToString().c_str(), (unsigned long)msg->GetSerializedLength(), DebugName(session).c_str());
HandleMessageReceive(msg, session);
delete msg;
}
}
// Done using the packet
enet_packet_destroy(event.packet);
break;
}
case ENET_EVENT_TYPE_NONE:
break;
}
return true;
}
void CNetServerWorker::CheckClientConnections()
{
// Send messages at most once per second
std::time_t now = std::time(nullptr);
if (now <= m_LastConnectionCheck)
return;
m_LastConnectionCheck = now;
for (size_t i = 0; i < m_Sessions.size(); ++i)
{
u32 lastReceived = m_Sessions[i]->GetLastReceivedTime();
u32 meanRTT = m_Sessions[i]->GetMeanRTT();
CNetMessage* message = nullptr;
// Report if we didn't hear from the client since few seconds
if (lastReceived > NETWORK_WARNING_TIMEOUT)
{
CClientTimeoutMessage* msg = new CClientTimeoutMessage();
msg->m_GUID = m_Sessions[i]->GetGUID();
msg->m_LastReceivedTime = lastReceived;
message = msg;
}
// Report if the client has bad ping
- else if (meanRTT > DEFAULT_TURN_LENGTH_MP)
+ else if (meanRTT > NETWORK_BAD_PING)
{
CClientPerformanceMessage* msg = new CClientPerformanceMessage();
CClientPerformanceMessage::S_m_Clients client;
client.m_GUID = m_Sessions[i]->GetGUID();
client.m_MeanRTT = meanRTT;
msg->m_Clients.push_back(client);
message = msg;
}
// Send to all clients except the affected one
// (since that will show the locally triggered warning instead).
// Also send it to clients that finished the loading screen while
// the game is still waiting for other clients to finish the loading screen.
if (message)
for (size_t j = 0; j < m_Sessions.size(); ++j)
{
if (i != j && (
(m_Sessions[j]->GetCurrState() == NSS_PREGAME && m_State == SERVER_STATE_PREGAME) ||
m_Sessions[j]->GetCurrState() == NSS_INGAME))
{
m_Sessions[j]->SendMessage(message);
}
}
SAFE_DELETE(message);
}
}
void CNetServerWorker::HandleMessageReceive(const CNetMessage* message, CNetServerSession* session)
{
// Handle non-FSM messages first
Status status = session->GetFileTransferer().HandleMessageReceive(*message);
if (status != INFO::SKIPPED)
return;
if (message->GetType() == NMT_FILE_TRANSFER_REQUEST)
{
CFileTransferRequestMessage* reqMessage = (CFileTransferRequestMessage*)message;
// Rejoining client got our JoinSyncStart after we received the state from
// another client, and has now requested that we forward it to them
ENSURE(!m_JoinSyncFile.empty());
session->GetFileTransferer().StartResponse(reqMessage->m_RequestID, m_JoinSyncFile);
return;
}
// Update FSM
if (!session->Update(message->GetType(), (void*)message))
LOGERROR("Net server: Error running FSM update (type=%d state=%d)", (int)message->GetType(), (int)session->GetCurrState());
}
void CNetServerWorker::SetupSession(CNetServerSession* session)
{
void* context = session;
// Set up transitions for session
session->AddTransition(NSS_UNCONNECTED, (uint)NMT_CONNECTION_LOST, NSS_UNCONNECTED);
session->AddTransition(NSS_HANDSHAKE, (uint)NMT_CONNECTION_LOST, NSS_UNCONNECTED);
session->AddTransition(NSS_HANDSHAKE, (uint)NMT_CLIENT_HANDSHAKE, NSS_AUTHENTICATE, (void*)&OnClientHandshake, context);
session->AddTransition(NSS_LOBBY_AUTHENTICATE, (uint)NMT_CONNECTION_LOST, NSS_UNCONNECTED);
session->AddTransition(NSS_LOBBY_AUTHENTICATE, (uint)NMT_AUTHENTICATE, NSS_PREGAME, (void*)&OnAuthenticate, context);
session->AddTransition(NSS_AUTHENTICATE, (uint)NMT_CONNECTION_LOST, NSS_UNCONNECTED);
session->AddTransition(NSS_AUTHENTICATE, (uint)NMT_AUTHENTICATE, NSS_PREGAME, (void*)&OnAuthenticate, context);
session->AddTransition(NSS_PREGAME, (uint)NMT_CONNECTION_LOST, NSS_UNCONNECTED, (void*)&OnDisconnect, context);
session->AddTransition(NSS_PREGAME, (uint)NMT_CHAT, NSS_PREGAME, (void*)&OnChat, context);
session->AddTransition(NSS_PREGAME, (uint)NMT_READY, NSS_PREGAME, (void*)&OnReady, context);
session->AddTransition(NSS_PREGAME, (uint)NMT_CLEAR_ALL_READY, NSS_PREGAME, (void*)&OnClearAllReady, context);
session->AddTransition(NSS_PREGAME, (uint)NMT_GAME_SETUP, NSS_PREGAME, (void*)&OnGameSetup, context);
session->AddTransition(NSS_PREGAME, (uint)NMT_ASSIGN_PLAYER, NSS_PREGAME, (void*)&OnAssignPlayer, context);
session->AddTransition(NSS_PREGAME, (uint)NMT_KICKED, NSS_PREGAME, (void*)&OnKickPlayer, context);
session->AddTransition(NSS_PREGAME, (uint)NMT_GAME_START, NSS_PREGAME, (void*)&OnStartGame, context);
session->AddTransition(NSS_PREGAME, (uint)NMT_LOADED_GAME, NSS_INGAME, (void*)&OnLoadedGame, context);
session->AddTransition(NSS_JOIN_SYNCING, (uint)NMT_KICKED, NSS_JOIN_SYNCING, (void*)&OnKickPlayer, context);
session->AddTransition(NSS_JOIN_SYNCING, (uint)NMT_CONNECTION_LOST, NSS_UNCONNECTED, (void*)&OnDisconnect, context);
session->AddTransition(NSS_JOIN_SYNCING, (uint)NMT_LOADED_GAME, NSS_INGAME, (void*)&OnJoinSyncingLoadedGame, context);
session->AddTransition(NSS_INGAME, (uint)NMT_REJOINED, NSS_INGAME, (void*)&OnRejoined, context);
session->AddTransition(NSS_INGAME, (uint)NMT_KICKED, NSS_INGAME, (void*)&OnKickPlayer, context);
session->AddTransition(NSS_INGAME, (uint)NMT_CLIENT_PAUSED, NSS_INGAME, (void*)&OnClientPaused, context);
session->AddTransition(NSS_INGAME, (uint)NMT_CONNECTION_LOST, NSS_UNCONNECTED, (void*)&OnDisconnect, context);
session->AddTransition(NSS_INGAME, (uint)NMT_CHAT, NSS_INGAME, (void*)&OnChat, context);
session->AddTransition(NSS_INGAME, (uint)NMT_SIMULATION_COMMAND, NSS_INGAME, (void*)&OnSimulationCommand, context);
session->AddTransition(NSS_INGAME, (uint)NMT_SYNC_CHECK, NSS_INGAME, (void*)&OnSyncCheck, context);
session->AddTransition(NSS_INGAME, (uint)NMT_END_COMMAND_BATCH, NSS_INGAME, (void*)&OnEndCommandBatch, context);
// Set first state
session->SetFirstState(NSS_HANDSHAKE);
}
bool CNetServerWorker::HandleConnect(CNetServerSession* session)
{
if (std::find(m_BannedIPs.begin(), m_BannedIPs.end(), session->GetIPAddress()) != m_BannedIPs.end())
{
session->Disconnect(NDR_BANNED);
return false;
}
CSrvHandshakeMessage handshake;
handshake.m_Magic = PS_PROTOCOL_MAGIC;
handshake.m_ProtocolVersion = PS_PROTOCOL_VERSION;
handshake.m_SoftwareVersion = PS_PROTOCOL_VERSION;
return session->SendMessage(&handshake);
}
void CNetServerWorker::OnUserJoin(CNetServerSession* session)
{
AddPlayer(session->GetGUID(), session->GetUserName());
CGameSetupMessage gameSetupMessage(GetScriptInterface());
gameSetupMessage.m_Data = m_GameAttributes;
session->SendMessage(&gameSetupMessage);
CPlayerAssignmentMessage assignMessage;
ConstructPlayerAssignmentMessage(assignMessage);
session->SendMessage(&assignMessage);
}
void CNetServerWorker::OnUserLeave(CNetServerSession* session)
{
std::vector::iterator pausing = std::find(m_PausingPlayers.begin(), m_PausingPlayers.end(), session->GetGUID());
if (pausing != m_PausingPlayers.end())
m_PausingPlayers.erase(pausing);
RemovePlayer(session->GetGUID());
if (m_ServerTurnManager && session->GetCurrState() != NSS_JOIN_SYNCING)
m_ServerTurnManager->UninitialiseClient(session->GetHostID()); // TODO: only for non-observers
// TODO: ought to switch the player controlled by that client
// back to AI control, or something?
}
void CNetServerWorker::AddPlayer(const CStr& guid, const CStrW& name)
{
// Find all player IDs in active use; we mustn't give them to a second player (excluding the unassigned ID: -1)
std::set usedIDs;
for (const std::pair& p : m_PlayerAssignments)
if (p.second.m_Enabled && p.second.m_PlayerID != -1)
usedIDs.insert(p.second.m_PlayerID);
// If the player is rejoining after disconnecting, try to give them
// back their old player ID
i32 playerID = -1;
// Try to match GUID first
for (PlayerAssignmentMap::iterator it = m_PlayerAssignments.begin(); it != m_PlayerAssignments.end(); ++it)
{
if (!it->second.m_Enabled && it->first == guid && usedIDs.find(it->second.m_PlayerID) == usedIDs.end())
{
playerID = it->second.m_PlayerID;
m_PlayerAssignments.erase(it); // delete the old mapping, since we've got a new one now
goto found;
}
}
// Try to match username next
for (PlayerAssignmentMap::iterator it = m_PlayerAssignments.begin(); it != m_PlayerAssignments.end(); ++it)
{
if (!it->second.m_Enabled && it->second.m_Name == name && usedIDs.find(it->second.m_PlayerID) == usedIDs.end())
{
playerID = it->second.m_PlayerID;
m_PlayerAssignments.erase(it); // delete the old mapping, since we've got a new one now
goto found;
}
}
// Otherwise leave the player ID as -1 (observer) and let gamesetup change it as needed.
found:
PlayerAssignment assignment;
assignment.m_Enabled = true;
assignment.m_Name = name;
assignment.m_PlayerID = playerID;
assignment.m_Status = 0;
m_PlayerAssignments[guid] = assignment;
// Send the new assignments to all currently active players
// (which does not include the one that's just joining)
SendPlayerAssignments();
}
void CNetServerWorker::RemovePlayer(const CStr& guid)
{
m_PlayerAssignments[guid].m_Enabled = false;
SendPlayerAssignments();
}
void CNetServerWorker::ClearAllPlayerReady()
{
for (std::pair& p : m_PlayerAssignments)
if (p.second.m_Status != 2)
p.second.m_Status = 0;
SendPlayerAssignments();
}
void CNetServerWorker::KickPlayer(const CStrW& playerName, const bool ban)
{
// Find the user with that name
std::vector::iterator it = std::find_if(m_Sessions.begin(), m_Sessions.end(),
[&](CNetServerSession* session) { return session->GetUserName() == playerName; });
// and return if no one or the host has that name
if (it == m_Sessions.end() || (*it)->GetGUID() == m_ControllerGUID)
return;
if (ban)
{
// Remember name
if (std::find(m_BannedPlayers.begin(), m_BannedPlayers.end(), playerName) == m_BannedPlayers.end())
m_BannedPlayers.push_back(m_LobbyAuth ? CStrW(playerName.substr(0, playerName.find(L" ("))) : playerName);
// Remember IP address
u32 ipAddress = (*it)->GetIPAddress();
if (std::find(m_BannedIPs.begin(), m_BannedIPs.end(), ipAddress) == m_BannedIPs.end())
m_BannedIPs.push_back(ipAddress);
}
// Disconnect that user
(*it)->Disconnect(ban ? NDR_BANNED : NDR_KICKED);
// Send message notifying other clients
CKickedMessage kickedMessage;
kickedMessage.m_Name = playerName;
kickedMessage.m_Ban = ban;
Broadcast(&kickedMessage, { NSS_PREGAME, NSS_JOIN_SYNCING, NSS_INGAME });
}
void CNetServerWorker::AssignPlayer(int playerID, const CStr& guid)
{
// Remove anyone who's already assigned to this player
for (std::pair& p : m_PlayerAssignments)
{
if (p.second.m_PlayerID == playerID)
p.second.m_PlayerID = -1;
}
// Update this host's assignment if it exists
if (m_PlayerAssignments.find(guid) != m_PlayerAssignments.end())
m_PlayerAssignments[guid].m_PlayerID = playerID;
SendPlayerAssignments();
}
void CNetServerWorker::ConstructPlayerAssignmentMessage(CPlayerAssignmentMessage& message)
{
for (const std::pair& p : m_PlayerAssignments)
{
if (!p.second.m_Enabled)
continue;
CPlayerAssignmentMessage::S_m_Hosts h;
h.m_GUID = p.first;
h.m_Name = p.second.m_Name;
h.m_PlayerID = p.second.m_PlayerID;
h.m_Status = p.second.m_Status;
message.m_Hosts.push_back(h);
}
}
void CNetServerWorker::SendPlayerAssignments()
{
CPlayerAssignmentMessage message;
ConstructPlayerAssignmentMessage(message);
Broadcast(&message, { NSS_PREGAME, NSS_JOIN_SYNCING, NSS_INGAME });
}
const ScriptInterface& CNetServerWorker::GetScriptInterface()
{
return *m_ScriptInterface;
}
void CNetServerWorker::SetTurnLength(u32 msecs)
{
if (m_ServerTurnManager)
m_ServerTurnManager->SetTurnLength(msecs);
}
void CNetServerWorker::ProcessLobbyAuth(const CStr& name, const CStr& token)
{
LOGMESSAGE("Net Server: Received lobby auth message from %s with %s", name, token);
// Find the user with that guid
std::vector::iterator it = std::find_if(m_Sessions.begin(), m_Sessions.end(),
[&](CNetServerSession* session)
{ return session->GetGUID() == token; });
if (it == m_Sessions.end())
return;
(*it)->SetUserName(name.FromUTF8());
// Send an empty message to request the authentication message from the client
// after its identity has been confirmed via the lobby
CAuthenticateMessage emptyMessage;
(*it)->SendMessage(&emptyMessage);
}
bool CNetServerWorker::OnClientHandshake(void* context, CFsmEvent* event)
{
ENSURE(event->GetType() == (uint)NMT_CLIENT_HANDSHAKE);
CNetServerSession* session = (CNetServerSession*)context;
CNetServerWorker& server = session->GetServer();
CCliHandshakeMessage* message = (CCliHandshakeMessage*)event->GetParamRef();
if (message->m_ProtocolVersion != PS_PROTOCOL_VERSION)
{
session->Disconnect(NDR_INCORRECT_PROTOCOL_VERSION);
return false;
}
CStr guid = ps_generate_guid();
int count = 0;
// Ensure unique GUID
while(std::find_if(
server.m_Sessions.begin(), server.m_Sessions.end(),
[&guid] (const CNetServerSession* session)
{ return session->GetGUID() == guid; }) != server.m_Sessions.end())
{
if (++count > 100)
{
session->Disconnect(NDR_GUID_FAILED);
return true;
}
guid = ps_generate_guid();
}
session->SetGUID(guid);
CSrvHandshakeResponseMessage handshakeResponse;
handshakeResponse.m_UseProtocolVersion = PS_PROTOCOL_VERSION;
handshakeResponse.m_GUID = guid;
handshakeResponse.m_Flags = 0;
if (server.m_LobbyAuth)
{
handshakeResponse.m_Flags |= PS_NETWORK_FLAG_REQUIRE_LOBBYAUTH;
session->SetNextState(NSS_LOBBY_AUTHENTICATE);
}
session->SendMessage(&handshakeResponse);
return true;
}
bool CNetServerWorker::OnAuthenticate(void* context, CFsmEvent* event)
{
ENSURE(event->GetType() == (uint)NMT_AUTHENTICATE);
CNetServerSession* session = (CNetServerSession*)context;
CNetServerWorker& server = session->GetServer();
// Prohibit joins while the game is loading
if (server.m_State == SERVER_STATE_LOADING)
{
LOGMESSAGE("Refused connection while the game is loading");
session->Disconnect(NDR_SERVER_LOADING);
return true;
}
CAuthenticateMessage* message = (CAuthenticateMessage*)event->GetParamRef();
CStrW username = SanitisePlayerName(message->m_Name);
CStrW usernameWithoutRating(username.substr(0, username.find(L" (")));
// Compare the lowercase names as specified by https://xmpp.org/extensions/xep-0029.html#sect-idm139493404168176
// "[...] comparisons will be made in case-normalized canonical form."
if (server.m_LobbyAuth && usernameWithoutRating.LowerCase() != session->GetUserName().LowerCase())
{
LOGERROR("Net server: lobby auth: %s tried joining as %s",
session->GetUserName().ToUTF8(),
usernameWithoutRating.ToUTF8());
session->Disconnect(NDR_LOBBY_AUTH_FAILED);
return true;
}
// Check the password before anything else.
if (server.m_Password != message->m_Password)
{
// Noisy logerror because players are not supposed to be able to get the IP,
// so this might be someone targeting the host for some reason
// (or TODO a dedicated server and we do want to log anyways)
LOGERROR("Net server: user %s tried joining with the wrong password",
session->GetUserName().ToUTF8());
session->Disconnect(NDR_SERVER_REFUSED);
return true;
}
// Either deduplicate or prohibit join if name is in use
bool duplicatePlayernames = false;
CFG_GET_VAL("network.duplicateplayernames", duplicatePlayernames);
// If lobby authentication is enabled, the clients playername has already been registered.
// There also can't be any duplicated names.
if (!server.m_LobbyAuth && duplicatePlayernames)
username = server.DeduplicatePlayerName(username);
else
{
std::vector::iterator it = std::find_if(
server.m_Sessions.begin(), server.m_Sessions.end(),
[&username] (const CNetServerSession* session)
{ return session->GetUserName() == username; });
if (it != server.m_Sessions.end() && (*it) != session)
{
session->Disconnect(NDR_PLAYERNAME_IN_USE);
return true;
}
}
// Disconnect banned usernames
if (std::find(server.m_BannedPlayers.begin(), server.m_BannedPlayers.end(), server.m_LobbyAuth ? usernameWithoutRating : username) != server.m_BannedPlayers.end())
{
session->Disconnect(NDR_BANNED);
return true;
}
int maxObservers = 0;
CFG_GET_VAL("network.observerlimit", maxObservers);
bool isRejoining = false;
bool serverFull = false;
if (server.m_State == SERVER_STATE_PREGAME)
{
// Don't check for maxObservers in the gamesetup, as we don't know yet who will be assigned
serverFull = server.m_Sessions.size() >= MAX_CLIENTS;
}
else
{
bool isObserver = true;
int disconnectedPlayers = 0;
int connectedPlayers = 0;
// (TODO: if GUIDs were stable, we should use them instead)
for (const std::pair& p : server.m_PlayerAssignments)
{
const PlayerAssignment& assignment = p.second;
if (!assignment.m_Enabled && assignment.m_Name == username)
{
isObserver = assignment.m_PlayerID == -1;
isRejoining = true;
}
if (assignment.m_PlayerID == -1)
continue;
if (assignment.m_Enabled)
++connectedPlayers;
else
++disconnectedPlayers;
}
// Optionally allow everyone or only buddies to join after the game has started
if (!isRejoining)
{
CStr observerLateJoin;
CFG_GET_VAL("network.lateobservers", observerLateJoin);
if (observerLateJoin == "everyone")
{
isRejoining = true;
}
else if (observerLateJoin == "buddies")
{
CStr buddies;
CFG_GET_VAL("lobby.buddies", buddies);
std::wstringstream buddiesStream(wstring_from_utf8(buddies));
CStrW buddy;
while (std::getline(buddiesStream, buddy, L','))
{
if (buddy == usernameWithoutRating)
{
isRejoining = true;
break;
}
}
}
}
if (!isRejoining)
{
LOGMESSAGE("Refused connection after game start from not-previously-known user \"%s\"", utf8_from_wstring(username));
session->Disconnect(NDR_SERVER_ALREADY_IN_GAME);
return true;
}
// Ensure all players will be able to rejoin
serverFull = isObserver && (
(int) server.m_Sessions.size() - connectedPlayers > maxObservers ||
(int) server.m_Sessions.size() + disconnectedPlayers >= MAX_CLIENTS);
}
if (serverFull)
{
session->Disconnect(NDR_SERVER_FULL);
return true;
}
u32 newHostID = server.m_NextHostID++;
session->SetUserName(username);
session->SetHostID(newHostID);
CAuthenticateResultMessage authenticateResult;
authenticateResult.m_Code = isRejoining ? ARC_OK_REJOINING : ARC_OK;
authenticateResult.m_HostID = newHostID;
authenticateResult.m_Message = L"Logged in";
authenticateResult.m_IsController = 0;
if (message->m_ControllerSecret == server.m_ControllerSecret)
{
if (server.m_ControllerGUID.empty())
{
server.m_ControllerGUID = session->GetGUID();
authenticateResult.m_IsController = 1;
}
// TODO: we could probably handle having several controllers, or swapping?
}
session->SendMessage(&authenticateResult);
server.OnUserJoin(session);
if (isRejoining)
{
// Request a copy of the current game state from an existing player,
// so we can send it on to the new player
// Assume session 0 is most likely the local player, so they're
// the most efficient client to request a copy from
CNetServerSession* sourceSession = server.m_Sessions.at(0);
sourceSession->GetFileTransferer().StartTask(
shared_ptr(new CNetFileReceiveTask_ServerRejoin(server, newHostID))
);
session->SetNextState(NSS_JOIN_SYNCING);
}
return true;
}
bool CNetServerWorker::OnSimulationCommand(void* context, CFsmEvent* event)
{
ENSURE(event->GetType() == (uint)NMT_SIMULATION_COMMAND);
CNetServerSession* session = (CNetServerSession*)context;
CNetServerWorker& server = session->GetServer();
CSimulationMessage* message = (CSimulationMessage*)event->GetParamRef();
// Ignore messages sent by one player on behalf of another player
// unless cheating is enabled
bool cheatsEnabled = false;
const ScriptInterface& scriptInterface = server.GetScriptInterface();
ScriptRequest rq(scriptInterface);
JS::RootedValue settings(rq.cx);
scriptInterface.GetProperty(server.m_GameAttributes, "settings", &settings);
if (scriptInterface.HasProperty(settings, "CheatsEnabled"))
scriptInterface.GetProperty(settings, "CheatsEnabled", cheatsEnabled);
PlayerAssignmentMap::iterator it = server.m_PlayerAssignments.find(session->GetGUID());
// When cheating is disabled, fail if the player the message claims to
// represent does not exist or does not match the sender's player name
if (!cheatsEnabled && (it == server.m_PlayerAssignments.end() || it->second.m_PlayerID != message->m_Player))
return true;
// Send it back to all clients that have finished
// the loading screen (and the synchronization when rejoining)
server.Broadcast(message, { NSS_INGAME });
// Save all the received commands
if (server.m_SavedCommands.size() < message->m_Turn + 1)
server.m_SavedCommands.resize(message->m_Turn + 1);
server.m_SavedCommands[message->m_Turn].push_back(*message);
// TODO: we shouldn't send the message back to the client that first sent it
return true;
}
bool CNetServerWorker::OnSyncCheck(void* context, CFsmEvent* event)
{
ENSURE(event->GetType() == (uint)NMT_SYNC_CHECK);
CNetServerSession* session = (CNetServerSession*)context;
CNetServerWorker& server = session->GetServer();
CSyncCheckMessage* message = (CSyncCheckMessage*)event->GetParamRef();
server.m_ServerTurnManager->NotifyFinishedClientUpdate(*session, message->m_Turn, message->m_Hash);
return true;
}
bool CNetServerWorker::OnEndCommandBatch(void* context, CFsmEvent* event)
{
ENSURE(event->GetType() == (uint)NMT_END_COMMAND_BATCH);
CNetServerSession* session = (CNetServerSession*)context;
CNetServerWorker& server = session->GetServer();
CEndCommandBatchMessage* message = (CEndCommandBatchMessage*)event->GetParamRef();
// The turn-length field is ignored
server.m_ServerTurnManager->NotifyFinishedClientCommands(*session, message->m_Turn);
return true;
}
bool CNetServerWorker::OnChat(void* context, CFsmEvent* event)
{
ENSURE(event->GetType() == (uint)NMT_CHAT);
CNetServerSession* session = (CNetServerSession*)context;
CNetServerWorker& server = session->GetServer();
CChatMessage* message = (CChatMessage*)event->GetParamRef();
message->m_GUID = session->GetGUID();
server.Broadcast(message, { NSS_PREGAME, NSS_INGAME });
return true;
}
bool CNetServerWorker::OnReady(void* context, CFsmEvent* event)
{
ENSURE(event->GetType() == (uint)NMT_READY);
CNetServerSession* session = (CNetServerSession*)context;
CNetServerWorker& server = session->GetServer();
// Occurs if a client presses not-ready
// in the very last moment before the hosts starts the game
if (server.m_State == SERVER_STATE_LOADING)
return true;
CReadyMessage* message = (CReadyMessage*)event->GetParamRef();
message->m_GUID = session->GetGUID();
server.Broadcast(message, { NSS_PREGAME });
server.m_PlayerAssignments[message->m_GUID].m_Status = message->m_Status;
return true;
}
bool CNetServerWorker::OnClearAllReady(void* context, CFsmEvent* event)
{
ENSURE(event->GetType() == (uint)NMT_CLEAR_ALL_READY);
CNetServerSession* session = (CNetServerSession*)context;
CNetServerWorker& server = session->GetServer();
if (session->GetGUID() == server.m_ControllerGUID)
server.ClearAllPlayerReady();
return true;
}
bool CNetServerWorker::OnGameSetup(void* context, CFsmEvent* event)
{
ENSURE(event->GetType() == (uint)NMT_GAME_SETUP);
CNetServerSession* session = (CNetServerSession*)context;
CNetServerWorker& server = session->GetServer();
// Changing the settings after gamestart is not implemented and would cause an Out-of-sync error.
// This happened when doubleclicking on the startgame button.
if (server.m_State != SERVER_STATE_PREGAME)
return true;
if (session->GetGUID() == server.m_ControllerGUID)
{
CGameSetupMessage* message = (CGameSetupMessage*)event->GetParamRef();
server.UpdateGameAttributes(&(message->m_Data));
}
return true;
}
bool CNetServerWorker::OnAssignPlayer(void* context, CFsmEvent* event)
{
ENSURE(event->GetType() == (uint)NMT_ASSIGN_PLAYER);
CNetServerSession* session = (CNetServerSession*)context;
CNetServerWorker& server = session->GetServer();
if (session->GetGUID() == server.m_ControllerGUID)
{
CAssignPlayerMessage* message = (CAssignPlayerMessage*)event->GetParamRef();
server.AssignPlayer(message->m_PlayerID, message->m_GUID);
}
return true;
}
bool CNetServerWorker::OnStartGame(void* context, CFsmEvent* event)
{
ENSURE(event->GetType() == (uint)NMT_GAME_START);
CNetServerSession* session = (CNetServerSession*)context;
CNetServerWorker& server = session->GetServer();
if (session->GetGUID() == server.m_ControllerGUID)
server.StartGame();
return true;
}
bool CNetServerWorker::OnLoadedGame(void* context, CFsmEvent* event)
{
ENSURE(event->GetType() == (uint)NMT_LOADED_GAME);
CNetServerSession* loadedSession = (CNetServerSession*)context;
CNetServerWorker& server = loadedSession->GetServer();
// We're in the loading state, so wait until every client has loaded
// before starting the game
ENSURE(server.m_State == SERVER_STATE_LOADING);
if (server.CheckGameLoadStatus(loadedSession))
return true;
CClientsLoadingMessage message;
// We always send all GUIDs of clients in the loading state
// so that we don't have to bother about switching GUI pages
for (CNetServerSession* session : server.m_Sessions)
if (session->GetCurrState() != NSS_INGAME && loadedSession->GetGUID() != session->GetGUID())
{
CClientsLoadingMessage::S_m_Clients client;
client.m_GUID = session->GetGUID();
message.m_Clients.push_back(client);
}
// Send to the client who has loaded the game but did not reach the NSS_INGAME state yet
loadedSession->SendMessage(&message);
server.Broadcast(&message, { NSS_INGAME });
return true;
}
bool CNetServerWorker::OnJoinSyncingLoadedGame(void* context, CFsmEvent* event)
{
// A client rejoining an in-progress game has now finished loading the
// map and deserialized the initial state.
// The simulation may have progressed since then, so send any subsequent
// commands to them and set them as an active player so they can participate
// in all future turns.
//
// (TODO: if it takes a long time for them to receive and execute all these
// commands, the other players will get frozen for that time and may be unhappy;
// we could try repeating this process a few times until the client converges
// on the up-to-date state, before setting them as active.)
ENSURE(event->GetType() == (uint)NMT_LOADED_GAME);
CNetServerSession* session = (CNetServerSession*)context;
CNetServerWorker& server = session->GetServer();
CLoadedGameMessage* message = (CLoadedGameMessage*)event->GetParamRef();
u32 turn = message->m_CurrentTurn;
u32 readyTurn = server.m_ServerTurnManager->GetReadyTurn();
// Send them all commands received since their saved state,
// and turn-ended messages for any turns that have already been processed
for (size_t i = turn + 1; i < std::max(readyTurn+1, (u32)server.m_SavedCommands.size()); ++i)
{
if (i < server.m_SavedCommands.size())
for (size_t j = 0; j < server.m_SavedCommands[i].size(); ++j)
session->SendMessage(&server.m_SavedCommands[i][j]);
if (i <= readyTurn)
{
CEndCommandBatchMessage endMessage;
endMessage.m_Turn = i;
endMessage.m_TurnLength = server.m_ServerTurnManager->GetSavedTurnLength(i);
session->SendMessage(&endMessage);
}
}
// Tell the turn manager to expect commands from this new client
server.m_ServerTurnManager->InitialiseClient(session->GetHostID(), readyTurn);
// Tell the client that everything has finished loading and it should start now
CLoadedGameMessage loaded;
loaded.m_CurrentTurn = readyTurn;
session->SendMessage(&loaded);
return true;
}
bool CNetServerWorker::OnRejoined(void* context, CFsmEvent* event)
{
// A client has finished rejoining and the loading screen disappeared.
ENSURE(event->GetType() == (uint)NMT_REJOINED);
CNetServerSession* session = (CNetServerSession*)context;
CNetServerWorker& server = session->GetServer();
// Inform everyone of the client having rejoined
CRejoinedMessage* message = (CRejoinedMessage*)event->GetParamRef();
message->m_GUID = session->GetGUID();
server.Broadcast(message, { NSS_INGAME });
// Send all pausing players to the rejoined client.
for (const CStr& guid : server.m_PausingPlayers)
{
CClientPausedMessage pausedMessage;
pausedMessage.m_GUID = guid;
pausedMessage.m_Pause = true;
session->SendMessage(&pausedMessage);
}
return true;
}
bool CNetServerWorker::OnKickPlayer(void* context, CFsmEvent* event)
{
ENSURE(event->GetType() == (uint)NMT_KICKED);
CNetServerSession* session = (CNetServerSession*)context;
CNetServerWorker& server = session->GetServer();
if (session->GetGUID() == server.m_ControllerGUID)
{
CKickedMessage* message = (CKickedMessage*)event->GetParamRef();
server.KickPlayer(message->m_Name, message->m_Ban);
}
return true;
}
bool CNetServerWorker::OnDisconnect(void* context, CFsmEvent* event)
{
ENSURE(event->GetType() == (uint)NMT_CONNECTION_LOST);
CNetServerSession* session = (CNetServerSession*)context;
CNetServerWorker& server = session->GetServer();
server.OnUserLeave(session);
return true;
}
bool CNetServerWorker::OnClientPaused(void* context, CFsmEvent* event)
{
ENSURE(event->GetType() == (uint)NMT_CLIENT_PAUSED);
CNetServerSession* session = (CNetServerSession*)context;
CNetServerWorker& server = session->GetServer();
CClientPausedMessage* message = (CClientPausedMessage*)event->GetParamRef();
message->m_GUID = session->GetGUID();
// Update the list of pausing players.
std::vector::iterator player = std::find(server.m_PausingPlayers.begin(), server.m_PausingPlayers.end(), session->GetGUID());
if (message->m_Pause)
{
if (player != server.m_PausingPlayers.end())
return true;
server.m_PausingPlayers.push_back(session->GetGUID());
}
else
{
if (player == server.m_PausingPlayers.end())
return true;
server.m_PausingPlayers.erase(player);
}
// Send messages to clients that are in game, and are not the client who paused.
for (CNetServerSession* netSession : server.m_Sessions)
if (netSession->GetCurrState() == NSS_INGAME && message->m_GUID != netSession->GetGUID())
netSession->SendMessage(message);
return true;
}
bool CNetServerWorker::CheckGameLoadStatus(CNetServerSession* changedSession)
{
for (const CNetServerSession* session : m_Sessions)
if (session != changedSession && session->GetCurrState() != NSS_INGAME)
return false;
// Inform clients that everyone has loaded the map and that the game can start
CLoadedGameMessage loaded;
loaded.m_CurrentTurn = 0;
// Notice the changedSession is still in the NSS_PREGAME state
Broadcast(&loaded, { NSS_PREGAME, NSS_INGAME });
m_State = SERVER_STATE_INGAME;
return true;
}
void CNetServerWorker::StartGame()
{
for (std::pair& player : m_PlayerAssignments)
if (player.second.m_Enabled && player.second.m_PlayerID != -1 && player.second.m_Status == 0)
{
LOGERROR("Tried to start the game without player \"%s\" being ready!", utf8_from_wstring(player.second.m_Name).c_str());
return;
}
m_ServerTurnManager = new CNetServerTurnManager(*this);
for (CNetServerSession* session : m_Sessions)
m_ServerTurnManager->InitialiseClient(session->GetHostID(), 0); // TODO: only for non-observers
m_State = SERVER_STATE_LOADING;
// Send the final setup state to all clients
UpdateGameAttributes(&m_GameAttributes);
// Remove players and observers that are not present when the game starts
for (PlayerAssignmentMap::iterator it = m_PlayerAssignments.begin(); it != m_PlayerAssignments.end();)
if (it->second.m_Enabled)
++it;
else
it = m_PlayerAssignments.erase(it);
SendPlayerAssignments();
CGameStartMessage gameStart;
Broadcast(&gameStart, { NSS_PREGAME });
}
void CNetServerWorker::UpdateGameAttributes(JS::MutableHandleValue attrs)
{
m_GameAttributes = attrs;
if (!m_Host)
return;
CGameSetupMessage gameSetupMessage(GetScriptInterface());
gameSetupMessage.m_Data = m_GameAttributes;
Broadcast(&gameSetupMessage, { NSS_PREGAME });
}
CStrW CNetServerWorker::SanitisePlayerName(const CStrW& original)
{
const size_t MAX_LENGTH = 32;
CStrW name = original;
name.Replace(L"[", L"{"); // remove GUI tags
name.Replace(L"]", L"}"); // remove for symmetry
// Restrict the length
if (name.length() > MAX_LENGTH)
name = name.Left(MAX_LENGTH);
// Don't allow surrounding whitespace
name.Trim(PS_TRIM_BOTH);
// Don't allow empty name
if (name.empty())
name = L"Anonymous";
return name;
}
CStrW CNetServerWorker::DeduplicatePlayerName(const CStrW& original)
{
CStrW name = original;
// Try names "Foo", "Foo (2)", "Foo (3)", etc
size_t id = 2;
while (true)
{
bool unique = true;
for (const CNetServerSession* session : m_Sessions)
{
if (session->GetUserName() == name)
{
unique = false;
break;
}
}
if (unique)
return name;
name = original + L" (" + CStrW::FromUInt(id++) + L")";
}
}
void CNetServerWorker::SendHolePunchingMessage(const CStr& ipStr, u16 port)
{
if (m_Host)
StunClient::SendHolePunchingMessages(*m_Host, ipStr, port);
}
CNetServer::CNetServer(bool useLobbyAuth, int autostartPlayers) :
m_Worker(new CNetServerWorker(useLobbyAuth, autostartPlayers)),
m_LobbyAuth(useLobbyAuth), m_UseSTUN(false), m_PublicIp(""), m_PublicPort(20595), m_Password()
{
}
CNetServer::~CNetServer()
{
delete m_Worker;
}
bool CNetServer::GetUseSTUN() const
{
return m_UseSTUN;
}
bool CNetServer::UseLobbyAuth() const
{
return m_LobbyAuth;
}
bool CNetServer::SetupConnection(const u16 port)
{
return m_Worker->SetupConnection(port);
}
u16 CNetServer::GetPublicPort() const
{
return m_PublicPort;
}
CStr CNetServer::GetPublicIp() const
{
return m_PublicIp;
}
void CNetServer::SetConnectionData(const CStr& ip, const u16 port, bool useSTUN)
{
m_PublicIp = ip;
m_PublicPort = port;
m_UseSTUN = useSTUN;
}
bool CNetServer::CheckPasswordAndIncrement(const CStr& password, const std::string& username)
{
std::unordered_map::iterator it = m_FailedAttempts.find(username);
if (m_Password == password)
{
if (it != m_FailedAttempts.end())
it->second = 0;
return true;
}
if (it == m_FailedAttempts.end())
m_FailedAttempts.emplace(username, 1);
else
it->second++;
return false;
}
bool CNetServer::IsBanned(const std::string& username) const
{
std::unordered_map::const_iterator it = m_FailedAttempts.find(username);
return it != m_FailedAttempts.end() && it->second >= FAILED_PASSWORD_TRIES_BEFORE_BAN;
}
void CNetServer::SetPassword(const CStr& password)
{
m_Password = password;
std::lock_guard lock(m_Worker->m_WorkerMutex);
m_Worker->SetPassword(password);
}
void CNetServer::SetControllerSecret(const std::string& secret)
{
std::lock_guard lock(m_Worker->m_WorkerMutex);
m_Worker->SetControllerSecret(secret);
}
void CNetServer::StartGame()
{
std::lock_guard lock(m_Worker->m_WorkerMutex);
m_Worker->m_StartGameQueue.push_back(true);
}
void CNetServer::UpdateGameAttributes(JS::MutableHandleValue attrs, const ScriptInterface& scriptInterface)
{
// Pass the attributes as JSON, since that's the easiest safe
// cross-thread way of passing script data
std::string attrsJSON = scriptInterface.StringifyJSON(attrs, false);
std::lock_guard lock(m_Worker->m_WorkerMutex);
m_Worker->m_GameAttributesQueue.push_back(attrsJSON);
}
void CNetServer::OnLobbyAuth(const CStr& name, const CStr& token)
{
std::lock_guard lock(m_Worker->m_WorkerMutex);
m_Worker->m_LobbyAuthQueue.push_back(std::make_pair(name, token));
}
void CNetServer::SetTurnLength(u32 msecs)
{
std::lock_guard lock(m_Worker->m_WorkerMutex);
m_Worker->m_TurnLengthQueue.push_back(msecs);
}
void CNetServer::SendHolePunchingMessage(const CStr& ip, u16 port)
{
m_Worker->SendHolePunchingMessage(ip, port);
}
Index: ps/trunk/source/network/NetServerTurnManager.cpp
===================================================================
--- ps/trunk/source/network/NetServerTurnManager.cpp (revision 25000)
+++ ps/trunk/source/network/NetServerTurnManager.cpp (revision 25001)
@@ -1,203 +1,205 @@
/* 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 .
*/
#include "precompiled.h"
#include "NetMessage.h"
#include "NetServerTurnManager.h"
#include "NetServer.h"
#include "NetSession.h"
#include "lib/utf8.h"
#include "ps/CLogger.h"
#include "simulation2/system/TurnManager.h"
#if 0
+#include "ps/Util.h"
#define NETSERVERTURN_LOG(...) debug_printf(__VA_ARGS__)
#else
#define NETSERVERTURN_LOG(...)
#endif
CNetServerTurnManager::CNetServerTurnManager(CNetServerWorker& server)
- : m_NetServer(server), m_ReadyTurn(1), m_TurnLength(DEFAULT_TURN_LENGTH_MP), m_HasSyncError(false)
+ : m_NetServer(server), m_ReadyTurn(COMMAND_DELAY_MP - 1), m_TurnLength(DEFAULT_TURN_LENGTH), m_HasSyncError(false)
{
// Turn 0 is not actually executed, store a dummy value.
m_SavedTurnLengths.push_back(0);
- // Turn 1 is special: all clients run it without waiting on a server command batch.
- // Because of this, it is always run with the default MP turn length.
- m_SavedTurnLengths.push_back(m_TurnLength);
+ // Turns [1..COMMAND_DELAY - 1] are special: all clients run them without waiting on a server command batch.
+ // Because of this, they are always run with the default MP turn length.
+ for (u32 i = 1; i < COMMAND_DELAY_MP; ++i)
+ m_SavedTurnLengths.push_back(m_TurnLength);
}
void CNetServerTurnManager::NotifyFinishedClientCommands(CNetServerSession& session, u32 turn)
{
int client = session.GetHostID();
NETSERVERTURN_LOG("NotifyFinishedClientCommands(client=%d, turn=%d)\n", client, turn);
// Must be a client we've already heard of
ENSURE(m_ClientsReady.find(client) != m_ClientsReady.end());
// Clients must advance one turn at a time
if (turn != m_ClientsReady[client] + 1)
{
LOGERROR("NotifyFinishedClientCommands: Client %d (%s) is ready for turn %d, but expected %d",
client,
utf8_from_wstring(session.GetUserName()).c_str(),
turn,
m_ClientsReady[client] + 1);
session.Disconnect(NDR_INCORRECT_READY_TURN_COMMANDS);
}
m_ClientsReady[client] = turn;
// Check whether this was the final client to become ready
CheckClientsReady();
}
void CNetServerTurnManager::CheckClientsReady()
{
// See if all clients (including self) are ready for a new turn
for (const std::pair& clientReady : m_ClientsReady)
{
NETSERVERTURN_LOG(" %d: %d <=? %d\n", clientReady.first, clientReady.second, m_ReadyTurn);
if (clientReady.second <= m_ReadyTurn)
return; // wasn't ready for m_ReadyTurn+1
}
++m_ReadyTurn;
NETSERVERTURN_LOG("CheckClientsReady: ready for turn %d\n", m_ReadyTurn);
// Tell all clients that the next turn is ready
CEndCommandBatchMessage msg;
msg.m_TurnLength = m_TurnLength;
msg.m_Turn = m_ReadyTurn;
m_NetServer.Broadcast(&msg, { NSS_INGAME });
ENSURE(m_SavedTurnLengths.size() == m_ReadyTurn);
m_SavedTurnLengths.push_back(m_TurnLength);
}
void CNetServerTurnManager::NotifyFinishedClientUpdate(CNetServerSession& session, u32 turn, const CStr& hash)
{
int client = session.GetHostID();
const CStrW& playername = session.GetUserName();
// Clients must advance one turn at a time
if (turn != m_ClientsSimulated[client] + 1)
{
LOGERROR("NotifyFinishedClientUpdate: Client %d (%s) is ready for turn %d, but expected %d",
client,
utf8_from_wstring(playername).c_str(),
turn,
m_ClientsReady[client] + 1);
session.Disconnect(NDR_INCORRECT_READY_TURN_SIMULATED);
}
m_ClientsSimulated[client] = turn;
// Check for OOS only if in sync
if (m_HasSyncError)
return;
m_ClientPlayernames[client] = playername;
m_ClientStateHashes[turn][client] = hash;
// Find the newest turn which we know all clients have simulated
u32 newest = std::numeric_limits::max();
for (const std::pair& clientSimulated : m_ClientsSimulated)
if (clientSimulated.second < newest)
newest = clientSimulated.second;
// For every set of state hashes that all clients have simulated, check for OOS
for (const std::pair>& clientStateHash : m_ClientStateHashes)
{
if (clientStateHash.first > newest)
break;
// Assume the host is correct (maybe we should choose the most common instead to help debugging)
std::string expected = clientStateHash.second.begin()->second;
// Find all players that are OOS on that turn
std::vector OOSPlayerNames;
for (const std::pair& hashPair : clientStateHash.second)
{
- NETSERVERTURN_LOG("sync check %d: %d = %hs\n", it->first, cit->first, Hexify(cit->second).c_str());
+ NETSERVERTURN_LOG("sync check %d: %d = %hs\n", clientStateHash.first, hashPair.first, Hexify(hashPair.second).c_str());
if (hashPair.second != expected)
{
// Oh no, out of sync
m_HasSyncError = true;
OOSPlayerNames.push_back(m_ClientPlayernames[hashPair.first]);
}
}
// Tell everyone about it
if (m_HasSyncError)
{
CSyncErrorMessage msg;
msg.m_Turn = clientStateHash.first;
msg.m_HashExpected = expected;
for (const CStrW& oosPlayername : OOSPlayerNames)
{
CSyncErrorMessage::S_m_PlayerNames h;
h.m_Name = oosPlayername;
msg.m_PlayerNames.push_back(h);
}
m_NetServer.Broadcast(&msg, { NSS_INGAME });
break;
}
}
// Delete the saved hashes for all turns that we've already verified
m_ClientStateHashes.erase(m_ClientStateHashes.begin(), m_ClientStateHashes.lower_bound(newest+1));
}
void CNetServerTurnManager::InitialiseClient(int client, u32 turn)
{
NETSERVERTURN_LOG("InitialiseClient(client=%d, turn=%d)\n", client, turn);
ENSURE(m_ClientsReady.find(client) == m_ClientsReady.end());
- m_ClientsReady[client] = turn + 1;
+ m_ClientsReady[client] = turn + COMMAND_DELAY_MP - 1;
m_ClientsSimulated[client] = turn;
}
void CNetServerTurnManager::UninitialiseClient(int client)
{
NETSERVERTURN_LOG("UninitialiseClient(client=%d)\n", client);
ENSURE(m_ClientsReady.find(client) != m_ClientsReady.end());
m_ClientsReady.erase(client);
m_ClientsSimulated.erase(client);
// Check whether we're ready for the next turn now that we're not
// waiting for this client any more
CheckClientsReady();
}
void CNetServerTurnManager::SetTurnLength(u32 msecs)
{
m_TurnLength = msecs;
}
u32 CNetServerTurnManager::GetSavedTurnLength(u32 turn)
{
ENSURE(turn <= m_ReadyTurn);
return m_SavedTurnLengths.at(turn);
}
Index: ps/trunk/source/rlinterface/RLInterface.cpp
===================================================================
--- ps/trunk/source/rlinterface/RLInterface.cpp (revision 25000)
+++ ps/trunk/source/rlinterface/RLInterface.cpp (revision 25001)
@@ -1,445 +1,445 @@
/* 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 .
*/
// Pull in the headers from the default precompiled header,
// even if rlinterface doesn't use precompiled headers.
#include "lib/precompiled.h"
#include "rlinterface/RLInterface.h"
#include "gui/GUIManager.h"
#include "ps/CLogger.h"
#include "ps/Game.h"
#include "ps/GameSetup/GameSetup.h"
#include "ps/Loader.h"
#include "simulation2/Simulation2.h"
#include "simulation2/components/ICmpAIInterface.h"
#include "simulation2/components/ICmpTemplateManager.h"
#include "simulation2/system/LocalTurnManager.h"
#include
#include
#include
// Globally accessible pointer to the RL Interface.
std::unique_ptr g_RLInterface;
namespace RL
{
Interface::Interface(const char* server_address) : m_GameMessage({GameMessageType::None})
{
LOGMESSAGERENDER("Starting RL interface HTTP server");
const char *options[] = {
"listening_ports", server_address,
"num_threads", "1",
nullptr
};
mg_context* mgContext = mg_start(MgCallback, this, options);
ENSURE(mgContext);
}
// Interactions with the game engine (g_Game) must be done in the main
// thread as there are specific checks for this. We will pass messages
// to the main thread to be applied (ie, "GameMessage"s).
std::string Interface::SendGameMessage(GameMessage&& msg)
{
std::unique_lock msgLock(m_MsgLock);
ENSURE(m_GameMessage.type == GameMessageType::None);
m_GameMessage = std::move(msg);
m_MsgApplied.wait(msgLock, [this]() { return m_GameMessage.type == GameMessageType::None; });
return m_ReturnValue;
}
std::string Interface::Step(std::vector&& commands)
{
std::lock_guard lock(m_Lock);
return SendGameMessage({ GameMessageType::Commands, std::move(commands) });
}
std::string Interface::Reset(ScenarioConfig&& scenario)
{
std::lock_guard lock(m_Lock);
m_ScenarioConfig = std::move(scenario);
return SendGameMessage({ GameMessageType::Reset });
}
std::string Interface::Evaluate(std::string&& code)
{
std::lock_guard lock(m_Lock);
m_Code = std::move(code);
return SendGameMessage({ GameMessageType::Evaluate });
}
std::vector Interface::GetTemplates(const std::vector& names) const
{
std::lock_guard lock(m_Lock);
CSimulation2& simulation = *g_Game->GetSimulation2();
CmpPtr cmpTemplateManager(simulation.GetSimContext().GetSystemEntity());
std::vector templates;
for (const std::string& templateName : names)
{
const CParamNode* node = cmpTemplateManager->GetTemplate(templateName);
if (node != nullptr)
templates.push_back(utf8_from_wstring(node->ToXML()));
}
return templates;
}
void* Interface::MgCallback(mg_event event, struct mg_connection *conn, const struct mg_request_info *request_info)
{
Interface* interface = (Interface*)request_info->user_data;
ENSURE(interface);
void* handled = (void*)""; // arbitrary non-NULL pointer to indicate successful handling
const char* header200 =
"HTTP/1.1 200 OK\r\n"
"Access-Control-Allow-Origin: *\r\n"
"Content-Type: text/plain; charset=utf-8\r\n\r\n";
const char* header404 =
"HTTP/1.1 404 Not Found\r\n"
"Content-Type: text/plain; charset=utf-8\r\n\r\n"
"Unrecognised URI";
const char* noPostData =
"HTTP/1.1 400 Bad Request\r\n"
"Content-Type: text/plain; charset=utf-8\r\n\r\n"
"No POST data found.";
const char* notRunningResponse =
"HTTP/1.1 400 Bad Request\r\n"
"Content-Type: text/plain; charset=utf-8\r\n\r\n"
"Game not running. Please create a scenario first.";
switch (event)
{
case MG_NEW_REQUEST:
{
std::stringstream stream;
const std::string uri = request_info->uri;
if (uri == "/reset")
{
std::string data = GetRequestContent(conn);
if (data.empty())
{
mg_printf(conn, "%s", noPostData);
return handled;
}
ScenarioConfig scenario;
const char *query_string = request_info->query_string;
if (query_string != nullptr)
{
const std::string qs(query_string);
scenario.saveReplay = qs.find("saveReplay") != std::string::npos;
scenario.playerID = 1;
char playerID[1];
const int len = mg_get_var(query_string, qs.length(), "playerID", playerID, 1);
if (len != -1)
scenario.playerID = std::stoi(playerID);
}
scenario.content = std::move(data);
const std::string gameState = interface->Reset(std::move(scenario));
stream << gameState.c_str();
}
else if (uri == "/step")
{
if (!interface->IsGameRunning())
{
mg_printf(conn, "%s", notRunningResponse);
return handled;
}
std::string data = GetRequestContent(conn);
std::stringstream postStream(data);
std::string line;
std::vector commands;
while (std::getline(postStream, line, '\n'))
{
const std::size_t splitPos = line.find(";");
if (splitPos == std::string::npos)
continue;
GameCommand cmd;
cmd.playerID = std::stoi(line.substr(0, splitPos));
cmd.json_cmd = line.substr(splitPos + 1);
commands.push_back(std::move(cmd));
}
const std::string gameState = interface->Step(std::move(commands));
if (gameState.empty())
{
mg_printf(conn, "%s", notRunningResponse);
return handled;
}
else
stream << gameState.c_str();
}
else if (uri == "/evaluate")
{
if (!interface->IsGameRunning())
{
mg_printf(conn, "%s", notRunningResponse);
return handled;
}
std::string code = GetRequestContent(conn);
if (code.empty())
{
mg_printf(conn, "%s", noPostData);
return handled;
}
const std::string codeResult = interface->Evaluate(std::move(code));
if (codeResult.empty())
{
mg_printf(conn, "%s", notRunningResponse);
return handled;
}
else
stream << codeResult.c_str();
}
else if (uri == "/templates")
{
if (!interface->IsGameRunning()) {
mg_printf(conn, "%s", notRunningResponse);
return handled;
}
const std::string data = GetRequestContent(conn);
if (data.empty())
{
mg_printf(conn, "%s", noPostData);
return handled;
}
std::stringstream postStream(data);
std::string line;
std::vector templateNames;
while (std::getline(postStream, line, '\n'))
templateNames.push_back(line);
for (std::string templateStr : interface->GetTemplates(templateNames))
stream << templateStr.c_str() << "\n";
}
else
{
mg_printf(conn, "%s", header404);
return handled;
}
mg_printf(conn, "%s", header200);
const std::string str = stream.str();
mg_write(conn, str.c_str(), str.length());
return handled;
}
case MG_HTTP_ERROR:
return nullptr;
case MG_EVENT_LOG:
// Called by Mongoose's cry()
LOGERROR("Mongoose error: %s", request_info->log_message);
return nullptr;
case MG_INIT_SSL:
return nullptr;
default:
debug_warn(L"Invalid Mongoose event type");
return nullptr;
}
}
std::string Interface::GetRequestContent(struct mg_connection *conn)
{
const static std::string NO_CONTENT;
const char* val = mg_get_header(conn, "Content-Length");
if (!val)
{
return NO_CONTENT;
}
const int contentSize = std::atoi(val);
std::string content(contentSize, ' ');
mg_read(conn, content.data(), contentSize);
return content;
}
bool Interface::TryGetGameMessage(GameMessage& msg)
{
if (m_GameMessage.type != GameMessageType::None)
{
msg = m_GameMessage;
m_GameMessage = {GameMessageType::None};
return true;
}
return false;
}
void Interface::TryApplyMessage()
{
const bool isGameStarted = g_Game && g_Game->IsGameStarted();
if (m_NeedsGameState && isGameStarted)
{
m_ReturnValue = GetGameState();
m_MsgApplied.notify_one();
m_MsgLock.unlock();
m_NeedsGameState = false;
}
if (!m_MsgLock.try_lock())
return;
GameMessage msg;
if (!TryGetGameMessage(msg))
{
m_MsgLock.unlock();
return;
}
ApplyMessage(msg);
}
void Interface::ApplyMessage(const GameMessage& msg)
{
const static std::string EMPTY_STATE;
const bool nonVisual = !g_GUI;
const bool isGameStarted = g_Game && g_Game->IsGameStarted();
switch (msg.type)
{
case GameMessageType::Reset:
{
if (isGameStarted)
EndGame();
g_Game = new CGame(m_ScenarioConfig.saveReplay);
ScriptInterface& scriptInterface = g_Game->GetSimulation2()->GetScriptInterface();
ScriptRequest rq(scriptInterface);
JS::RootedValue attrs(rq.cx);
scriptInterface.ParseJSON(m_ScenarioConfig.content, &attrs);
g_Game->SetPlayerID(m_ScenarioConfig.playerID);
g_Game->StartGame(&attrs, "");
if (nonVisual)
{
LDR_NonprogressiveLoad();
ENSURE(g_Game->ReallyStartGame() == PSRETURN_OK);
m_ReturnValue = GetGameState();
m_MsgApplied.notify_one();
m_MsgLock.unlock();
}
else
{
JS::RootedValue initData(rq.cx);
scriptInterface.CreateObject(rq, &initData);
scriptInterface.SetProperty(initData, "attribs", attrs);
JS::RootedValue playerAssignments(rq.cx);
scriptInterface.CreateObject(rq, &playerAssignments);
scriptInterface.SetProperty(initData, "playerAssignments", playerAssignments);
g_GUI->SwitchPage(L"page_loading.xml", &scriptInterface, initData);
m_NeedsGameState = true;
}
break;
}
case GameMessageType::Commands:
{
if (!g_Game)
{
m_ReturnValue = EMPTY_STATE;
m_MsgApplied.notify_one();
m_MsgLock.unlock();
return;
}
const ScriptInterface& scriptInterface = g_Game->GetSimulation2()->GetScriptInterface();
CLocalTurnManager* turnMgr = static_cast(g_Game->GetTurnManager());
for (const GameCommand& command : msg.commands)
{
ScriptRequest rq(scriptInterface);
JS::RootedValue commandJSON(rq.cx);
scriptInterface.ParseJSON(command.json_cmd, &commandJSON);
turnMgr->PostCommand(command.playerID, commandJSON);
}
- const u32 deltaRealTime = DEFAULT_TURN_LENGTH_SP;
+ const u32 deltaRealTime = DEFAULT_TURN_LENGTH;
if (nonVisual)
{
const double deltaSimTime = deltaRealTime * g_Game->GetSimRate();
const size_t maxTurns = static_cast(g_Game->GetSimRate());
g_Game->GetTurnManager()->Update(deltaSimTime, maxTurns);
}
else
g_Game->Update(deltaRealTime);
m_ReturnValue = GetGameState();
m_MsgApplied.notify_one();
m_MsgLock.unlock();
break;
}
case GameMessageType::Evaluate:
{
if (!g_Game)
{
m_ReturnValue = EMPTY_STATE;
m_MsgApplied.notify_one();
m_MsgLock.unlock();
return;
}
const ScriptInterface& scriptInterface = g_Game->GetSimulation2()->GetScriptInterface();
ScriptRequest rq(scriptInterface);
JS::RootedValue ret(rq.cx);
scriptInterface.Eval(m_Code.c_str(), &ret);
m_ReturnValue = scriptInterface.StringifyJSON(&ret, false);
m_MsgApplied.notify_one();
m_MsgLock.unlock();
break;
}
default:
break;
}
}
std::string Interface::GetGameState() const
{
const ScriptInterface& scriptInterface = g_Game->GetSimulation2()->GetScriptInterface();
const CSimContext simContext = g_Game->GetSimulation2()->GetSimContext();
CmpPtr cmpAIInterface(simContext.GetSystemEntity());
ScriptRequest rq(scriptInterface);
JS::RootedValue state(rq.cx);
cmpAIInterface->GetFullRepresentation(&state, true);
return scriptInterface.StringifyJSON(&state, false);
}
bool Interface::IsGameRunning() const
{
return g_Game != nullptr;
}
}
Index: ps/trunk/source/simulation2/system/LocalTurnManager.cpp
===================================================================
--- ps/trunk/source/simulation2/system/LocalTurnManager.cpp (revision 25000)
+++ ps/trunk/source/simulation2/system/LocalTurnManager.cpp (revision 25001)
@@ -1,59 +1,57 @@
/* Copyright (C) 2017 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 .
*/
#include "precompiled.h"
#include "LocalTurnManager.h"
CLocalTurnManager::CLocalTurnManager(CSimulation2& simulation, IReplayLogger& replay)
- : CTurnManager(simulation, DEFAULT_TURN_LENGTH_SP, 0, replay)
+ : CTurnManager(simulation, DEFAULT_TURN_LENGTH, COMMAND_DELAY_SP, 0, replay)
{
}
void CLocalTurnManager::PostCommand(player_id_t playerid, JS::HandleValue data)
{
- AddCommand(m_ClientId, playerid, data, m_CurrentTurn + 1);
+ AddCommand(m_ClientId, playerid, data, m_CurrentTurn + m_CommandDelay);
}
void CLocalTurnManager::PostCommand(JS::HandleValue data)
{
- // Add directly to the next turn, ignoring COMMAND_DELAY,
- // because we don't need to compensate for network latency
- AddCommand(m_ClientId, m_PlayerId, data, m_CurrentTurn + 1);
+ AddCommand(m_ClientId, m_PlayerId, data, m_CurrentTurn + m_CommandDelay);
}
void CLocalTurnManager::NotifyFinishedOwnCommands(u32 turn)
{
FinishedAllCommands(turn, m_TurnLength);
}
void CLocalTurnManager::NotifyFinishedUpdate(u32 UNUSED(turn))
{
#if 0 // this hurts performance and is only useful for verifying log replays
std::string hash;
{
PROFILE3("state hash check");
ENSURE(m_Simulation2.ComputeStateHash(hash));
}
m_Replay.Hash(hash);
#endif
}
void CLocalTurnManager::OnSimulationMessage(CSimulationMessage* UNUSED(msg))
{
debug_warn(L"This should never be called");
}
Index: ps/trunk/source/simulation2/system/TurnManager.cpp
===================================================================
--- ps/trunk/source/simulation2/system/TurnManager.cpp (revision 25000)
+++ ps/trunk/source/simulation2/system/TurnManager.cpp (revision 25001)
@@ -1,362 +1,367 @@
/* Copyright (C) 2020 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 .
*/
#include "precompiled.h"
#include "TurnManager.h"
#include "gui/GUIManager.h"
#include "maths/MathUtil.h"
#include "ps/Pyrogenesis.h"
#include "ps/Profile.h"
#include "ps/CLogger.h"
#include "ps/Replay.h"
#include "ps/Util.h"
#include "scriptinterface/ScriptExtraHeaders.h" // StructuredClone
#include "scriptinterface/ScriptInterface.h"
#include "simulation2/Simulation2.h"
-const u32 DEFAULT_TURN_LENGTH_MP = 500;
-const u32 DEFAULT_TURN_LENGTH_SP = 200;
-
-const int COMMAND_DELAY = 2;
-
#if 0
#define NETTURN_LOG(...) debug_printf(__VA_ARGS__)
#else
#define NETTURN_LOG(...)
#endif
+/**
+ * Maximum number of turns between two clients.
+ * When we are on turn n, we schedule new commands for n+COMMAND_DELAY.
+ * We know that all other clients have finished scheduling commands for n,
+ * else we couldn't have got here, which means they're at least on turn n-COMMAND_DELAY+1.
+ * We know we have not yet finished scheduling commands for n+COMMAND_DELAY, so no client can be there.
+ * Hence other clients can be on turns [n-COMMAND_DELAY+1, ..., n+COMMAND_DELAY-1], and no other,
+ * hence any two clients can only be this many turns apart.
+ */
+constexpr int MaxClientTurnDelta(int commandDelay)
+{
+ return 2 * (commandDelay - 1);
+}
+
const CStr CTurnManager::EventNameSavegameLoaded = "SavegameLoaded";
-CTurnManager::CTurnManager(CSimulation2& simulation, u32 defaultTurnLength, int clientId, IReplayLogger& replay)
- : m_Simulation2(simulation), m_CurrentTurn(0), m_ReadyTurn(1), m_TurnLength(defaultTurnLength),
+CTurnManager::CTurnManager(CSimulation2& simulation, u32 defaultTurnLength, u32 commandDelay, int clientId, IReplayLogger& replay)
+ : m_Simulation2(simulation), m_CurrentTurn(0), m_CommandDelay(commandDelay), m_ReadyTurn(commandDelay - 1), m_TurnLength(defaultTurnLength),
m_PlayerId(-1), m_ClientId(clientId), m_DeltaSimTime(0), m_HasSyncError(false), m_Replay(replay),
m_FinalTurn(std::numeric_limits::max()), m_TimeWarpNumTurns(0),
m_QuickSaveMetadata(m_Simulation2.GetScriptInterface().GetGeneralJSContext())
{
- // When we are on turn n, we schedule new commands for n+2.
- // We know that all other clients have finished scheduling commands for n (else we couldn't have got here).
- // We know we have not yet finished scheduling commands for n+2.
- // Hence other clients can be on turn n-1, n, n+1, and no other.
- // So they can be sending us commands scheduled for n+1, n+2, n+3.
- // So we need a 3-element buffer:
- m_QueuedCommands.resize(COMMAND_DELAY + 1);
+ // Lag between any two clients is bounded. Add 1 for inclusive bounds.
+ m_QueuedCommands.resize(MaxClientTurnDelta(m_CommandDelay) + 1);
}
void CTurnManager::ResetState(u32 newCurrentTurn, u32 newReadyTurn)
{
m_CurrentTurn = newCurrentTurn;
m_ReadyTurn = newReadyTurn;
m_DeltaSimTime = 0;
size_t queuedCommandsSize = m_QueuedCommands.size();
m_QueuedCommands.clear();
m_QueuedCommands.resize(queuedCommandsSize);
}
void CTurnManager::SetPlayerID(int playerId)
{
m_PlayerId = playerId;
}
bool CTurnManager::WillUpdate(float simFrameLength) const
{
// Keep this in sync with the return value of Update()
if (m_CurrentTurn > m_FinalTurn)
return false;
if (m_DeltaSimTime + simFrameLength < 0)
return false;
if (m_ReadyTurn <= m_CurrentTurn)
return false;
return true;
}
bool CTurnManager::Update(float simFrameLength, size_t maxTurns)
{
if (m_CurrentTurn > m_FinalTurn)
return false;
m_DeltaSimTime += simFrameLength;
// If the game becomes laggy, m_DeltaSimTime increases progressively.
// The engine will fast forward accordingly to catch up.
// To keep the game playable, stop fast forwarding after 2 turn lengths.
m_DeltaSimTime = std::min(m_DeltaSimTime, 2.0f * m_TurnLength / 1000.0f);
// If we haven't reached the next turn yet, do nothing
if (m_DeltaSimTime < 0)
return false;
NETTURN_LOG("Update current=%d ready=%d\n", m_CurrentTurn, m_ReadyTurn);
// Check that the next turn is ready for execution
- if (m_ReadyTurn <= m_CurrentTurn)
+ if (m_ReadyTurn <= m_CurrentTurn && m_CommandDelay > 1)
{
// Oops, we wanted to start the next turn but it's not ready yet -
// there must be too much network lag.
// TODO: complain to the user.
// TODO: send feedback to the server to increase the turn length.
// Reset the next-turn timer to 0 so we try again next update but
// so we don't rush to catch up in subsequent turns.
// TODO: we should do clever rate adjustment instead of just pausing like this.
m_DeltaSimTime = 0;
return false;
}
maxTurns = std::max((size_t)1, maxTurns); // always do at least one turn
for (size_t i = 0; i < maxTurns; ++i)
{
// Check that we've reached the i'th next turn
if (m_DeltaSimTime < 0)
break;
// Check that the i'th next turn is still ready
- if (m_ReadyTurn <= m_CurrentTurn)
+ if (m_ReadyTurn <= m_CurrentTurn && m_CommandDelay > 1)
break;
- NotifyFinishedOwnCommands(m_CurrentTurn + COMMAND_DELAY);
+ NotifyFinishedOwnCommands(m_CurrentTurn + m_CommandDelay);
// Increase now, so Update can send new commands for a subsequent turn
++m_CurrentTurn;
// Clean up any destroyed entities since the last turn (e.g. placement previews
// or rally point flags generated by the GUI). (Must do this before the time warp
// serialization.)
m_Simulation2.FlushDestroyedEntities();
// Save the current state for rewinding, if enabled
if (m_TimeWarpNumTurns && (m_CurrentTurn % m_TimeWarpNumTurns) == 0)
{
PROFILE3("time warp serialization");
std::stringstream stream;
m_Simulation2.SerializeState(stream);
m_TimeWarpStates.push_back(stream.str());
}
// Put all the client commands into a single list, in a globally consistent order
std::vector commands;
for (std::pair>& p : m_QueuedCommands[0])
commands.insert(commands.end(), std::make_move_iterator(p.second.begin()), std::make_move_iterator(p.second.end()));
m_QueuedCommands.pop_front();
m_QueuedCommands.resize(m_QueuedCommands.size() + 1);
m_Replay.Turn(m_CurrentTurn-1, m_TurnLength, commands);
NETTURN_LOG("Running %d cmds\n", commands.size());
m_Simulation2.Update(m_TurnLength, commands);
NotifyFinishedUpdate(m_CurrentTurn);
// Set the time for the next turn update
m_DeltaSimTime -= m_TurnLength / 1000.f;
}
return true;
}
bool CTurnManager::UpdateFastForward()
{
m_DeltaSimTime = 0;
NETTURN_LOG("UpdateFastForward current=%d ready=%d\n", m_CurrentTurn, m_ReadyTurn);
// Check that the next turn is ready for execution
if (m_ReadyTurn <= m_CurrentTurn)
return false;
while (m_ReadyTurn > m_CurrentTurn)
{
// TODO: It would be nice to remove some of the duplication with Update()
// (This is similar but doesn't call any Notify functions or update DeltaTime,
// it just updates the simulation state)
++m_CurrentTurn;
m_Simulation2.FlushDestroyedEntities();
// Put all the client commands into a single list, in a globally consistent order
std::vector commands;
for (std::pair>& p : m_QueuedCommands[0])
commands.insert(commands.end(), std::make_move_iterator(p.second.begin()), std::make_move_iterator(p.second.end()));
m_QueuedCommands.pop_front();
m_QueuedCommands.resize(m_QueuedCommands.size() + 1);
m_Replay.Turn(m_CurrentTurn-1, m_TurnLength, commands);
NETTURN_LOG("Running %d cmds\n", commands.size());
m_Simulation2.Update(m_TurnLength, commands);
}
return true;
}
void CTurnManager::Interpolate(float simFrameLength, float realFrameLength)
{
// TODO: using m_TurnLength might be a bit dodgy when length changes - maybe
// we need to save the previous turn length?
float offset = Clamp(m_DeltaSimTime / (m_TurnLength / 1000.f) + 1.0, 0.0, 1.0);
// Stop animations while still updating the selection highlight
if (m_CurrentTurn > m_FinalTurn)
simFrameLength = 0;
m_Simulation2.Interpolate(simFrameLength, offset, realFrameLength);
}
void CTurnManager::AddCommand(int client, int player, JS::HandleValue data, u32 turn)
{
- NETTURN_LOG("AddCommand(client=%d player=%d turn=%d)\n", client, player, turn);
+ NETTURN_LOG("AddCommand(client=%d player=%d turn=%d current=%d, ready=%d)\n", client, player, turn, m_CurrentTurn, m_ReadyTurn);
- if (!(m_CurrentTurn < turn && turn <= m_CurrentTurn + COMMAND_DELAY + 1))
+ // Reject commands for turns that we should not be able to compute (in the past or too far future).
+ if (m_CurrentTurn >= turn || turn > m_CurrentTurn + MaxClientTurnDelta(m_CommandDelay) + 1)
{
debug_warn(L"Received command for invalid turn");
return;
}
m_Simulation2.GetScriptInterface().FreezeObject(data, true);
ScriptRequest rq(m_Simulation2.GetScriptInterface());
m_QueuedCommands[turn - (m_CurrentTurn+1)][client].emplace_back(player, rq.cx, data);
}
void CTurnManager::FinishedAllCommands(u32 turn, u32 turnLength)
{
NETTURN_LOG("FinishedAllCommands(%d, %d)\n", turn, turnLength);
ENSURE(turn == m_ReadyTurn + 1);
m_ReadyTurn = turn;
m_TurnLength = turnLength;
}
bool CTurnManager::TurnNeedsFullHash(u32 turn) const
{
// Check immediately for errors caused by e.g. inconsistent game versions
// (The hash is computed after the first sim update, so we start at turn == 1)
if (turn == 1)
return true;
// Otherwise check the full state every ~10 seconds in multiplayer games
// (TODO: should probably remove this when we're reasonably sure the game
// isn't too buggy, since the full hash is still pretty slow)
if (turn % 20 == 0)
return true;
return false;
}
void CTurnManager::EnableTimeWarpRecording(size_t numTurns)
{
m_TimeWarpStates.clear();
m_TimeWarpNumTurns = numTurns;
}
void CTurnManager::RewindTimeWarp()
{
if (m_TimeWarpStates.empty())
return;
std::stringstream stream(m_TimeWarpStates.back());
m_Simulation2.DeserializeState(stream);
m_TimeWarpStates.pop_back();
// Reset the turn manager state, so we won't execute stray commands and
// won't do the next snapshot until the appropriate time.
// (Ideally we ought to serialise the turn manager state and restore it
// here, but this is simpler for now.)
ResetState(0, 1);
}
void CTurnManager::QuickSave(JS::HandleValue GUIMetadata)
{
TIMER(L"QuickSave");
std::stringstream stream;
if (!m_Simulation2.SerializeState(stream))
{
LOGERROR("Failed to quicksave game");
return;
}
m_QuickSaveState = stream.str();
ScriptRequest rq(m_Simulation2.GetScriptInterface());
if (JS_StructuredClone(rq.cx, GUIMetadata, &m_QuickSaveMetadata, nullptr, nullptr))
{
// Freeze state to ensure that consectuvie loads don't modify the state
m_Simulation2.GetScriptInterface().FreezeObject(m_QuickSaveMetadata, true);
}
else
{
LOGERROR("Could not copy savegame GUI metadata");
m_QuickSaveMetadata = JS::UndefinedValue();
}
LOGMESSAGERENDER("Quicksaved game");
}
void CTurnManager::QuickLoad()
{
TIMER(L"QuickLoad");
if (m_QuickSaveState.empty())
{
LOGERROR("Cannot quickload game - no game was quicksaved");
return;
}
std::stringstream stream(m_QuickSaveState);
if (!m_Simulation2.DeserializeState(stream))
{
LOGERROR("Failed to quickload game");
return;
}
// See RewindTimeWarp
ResetState(0, 1);
if (!g_GUI)
return;
ScriptRequest rq(m_Simulation2.GetScriptInterface());
// Provide a copy, so that GUI components don't have to clone to get mutable objects
JS::RootedValue quickSaveMetadataClone(rq.cx);
if (!JS_StructuredClone(rq.cx, m_QuickSaveMetadata, &quickSaveMetadataClone, nullptr, nullptr))
{
LOGERROR("Failed to clone quicksave state!");
return;
}
JS::RootedValueArray<1> paramData(rq.cx);
paramData[0].set(quickSaveMetadataClone);
g_GUI->SendEventToAll(EventNameSavegameLoaded, paramData);
LOGMESSAGERENDER("Quickloaded game");
}
Index: ps/trunk/source/simulation2/system/TurnManager.h
===================================================================
--- ps/trunk/source/simulation2/system/TurnManager.h (revision 25000)
+++ ps/trunk/source/simulation2/system/TurnManager.h (revision 25001)
@@ -1,199 +1,222 @@
/* Copyright (C) 2020 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