Index: binaries/data/mods/public/gui/gamesetup_mp/gamesetup_mp.js =================================================================== --- binaries/data/mods/public/gui/gamesetup_mp/gamesetup_mp.js +++ binaries/data/mods/public/gui/gamesetup_mp/gamesetup_mp.js @@ -30,6 +30,17 @@ switch (attribs.multiplayerGameType) { + case "dedicated": + { + if (!Engine.HasXmppClient()) + { + switchSetupPage("pageJoin"); + break; + } + if (startJoinFromLobby(attribs.name, attribs.hostJID, attribs.pass, attribs.secret)) + switchSetupPage("pageConnecting"); + break; + } case "join": { if (!Engine.HasXmppClient()) @@ -424,7 +435,7 @@ /** * Connect via the lobby. */ -function startJoinFromLobby(playername, hostJID, password) +function startJoinFromLobby(playername, hostJID, password, secret) { if (!Engine.HasXmppClient()) { @@ -439,7 +450,7 @@ try { - Engine.StartNetworkJoinLobby(playername + (g_UserRating ? " (" + g_UserRating + ")" : ""), hostJID, password); + Engine.StartNetworkJoinLobby(playername + (g_UserRating ? " (" + g_UserRating + ")" : ""), hostJID, password, secret); } catch (e) { Index: source/main.cpp =================================================================== --- source/main.cpp +++ source/main.cpp @@ -44,6 +44,7 @@ #include "ps/CConsole.h" #include "ps/CLogger.h" #include "ps/ConfigDB.h" +#include "ps/DedicatedServer.h" #include "ps/Filesystem.h" #include "ps/Game.h" #include "ps/Globals.h" @@ -533,6 +534,7 @@ const bool isNonVisualReplay = args.Has("replay"); const bool isNonVisual = args.Has("autostart-nonvisual"); const bool isUsingRLInterface = args.Has("rl-interface"); + const bool isDedicated = args.Has("dedicated"); const OsPath replayFile( isVisualReplay ? args.Get("replay-visual") : @@ -672,12 +674,19 @@ installedMods = installer.GetInstalledMods(); } - if (isNonVisual) + if (isNonVisual || isDedicated) { InitNonVisual(args); if (isUsingRLInterface) StartRLInterface(args); + // This runs its own inner-loop. + if (isDedicated) + { + RunDedicatedServer(args); + g_Shutdown = ShutdownType::Quit; + } + while (g_Shutdown == ShutdownType::None) { if (isUsingRLInterface) Index: source/network/NetClient.h =================================================================== --- source/network/NetClient.h +++ source/network/NetClient.h @@ -69,6 +69,7 @@ * The game must exist for the lifetime of this object. */ CNetClient(CGame* game); + CNetClient(CGame* game, const ScriptInterface& scriptInterface); virtual ~CNetClient(); @@ -294,6 +295,9 @@ void PostPlayerAssignmentsToScript(); CGame *m_Game; + + const ScriptInterface& m_ScriptInterface; + CStrW m_UserName; CStr m_HostJID; Index: source/network/NetClient.cpp =================================================================== --- source/network/NetClient.cpp +++ source/network/NetClient.cpp @@ -22,6 +22,7 @@ #include "NetClientTurnManager.h" #include "NetMessage.h" #include "NetSession.h" +#include "NetServer.h" #include "lib/byte_order.h" #include "lib/external_libraries/enet.h" @@ -82,8 +83,11 @@ CStr m_InitAttributes; }; -CNetClient::CNetClient(CGame* game) : +CNetClient::CNetClient(CGame* game) : CNetClient(game, game->GetSimulation2()->GetScriptInterface()) {} + +CNetClient::CNetClient(CGame* game, const ScriptInterface& scriptInterface) : m_Session(NULL), + m_ScriptInterface(scriptInterface), m_UserName(L"anonymous"), m_HostID((u32)-1), m_ClientTurnManager(NULL), m_Game(game), m_LastConnectionCheck(0), @@ -91,7 +95,8 @@ m_ServerPort(0), m_Rejoin(false) { - m_Game->SetTurnManager(NULL); // delete the old local turn manager so we don't accidentally use it + if (m_Game) + m_Game->SetTurnManager(NULL); // delete the old local turn manager so we don't accidentally use it void* context = this; @@ -394,7 +399,7 @@ const ScriptInterface& CNetClient::GetScriptInterface() { - return m_Game->GetSimulation2()->GetScriptInterface(); + return m_ScriptInterface; } void CNetClient::PostPlayerAssignmentsToScript() @@ -592,7 +597,7 @@ } CLoadedGameMessage loaded; - loaded.m_CurrentTurn = m_ClientTurnManager->GetCurrentTurn(); + loaded.m_CurrentTurn = m_ClientTurnManager ? m_ClientTurnManager->GetCurrentTurn() : 0; SendMessage(&loaded); } @@ -644,7 +649,14 @@ if (message->m_Flags & PS_NETWORK_FLAG_REQUIRE_LOBBYAUTH) { - if (g_XmppClient && !client->m_HostJID.empty()) + if (!client->m_Game) + { + // This is a fake, headless client for the netserver. + ENSURE(g_NetServer); + LOGMESSAGE("sending fake Auth"); + g_NetServer->OnLobbyAuth("Fake Observer", client->m_GUID); + } + else if (g_XmppClient && !client->m_HostJID.empty()) g_XmppClient->SendIqLobbyAuth(client->m_HostJID, client->m_GUID); else { @@ -776,17 +788,21 @@ 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( + if (client->m_Game) + client->m_ClientTurnManager = new CNetClientTurnManager( *client->m_Game->GetSimulation2(), *client, client->m_HostID, client->m_Game->GetReplayLogger()); // Parse init attributes. - const ScriptInterface& scriptInterface = client->m_Game->GetSimulation2()->GetScriptInterface(); + const ScriptInterface& scriptInterface = client->GetScriptInterface(); ScriptRequest rq(scriptInterface); JS::RootedValue initAttribs(rq.cx); Script::ParseJSON(rq, message->m_InitAttributes, &initAttribs); - client->m_Game->SetPlayerID(player); - client->m_Game->StartGame(&initAttribs, ""); + if (client->m_Game) + { + client->m_Game->SetPlayerID(player); + client->m_Game->StartGame(&initAttribs, ""); + } client->PushGuiMessage("type", "start", "initAttributes", initAttribs); @@ -939,7 +955,8 @@ // All players have loaded the game - start running the turn manager // so that the game begins - client->m_Game->SetTurnManager(client->m_ClientTurnManager); + if (client->m_Game) + client->m_Game->SetTurnManager(client->m_ClientTurnManager); client->PushGuiMessage( "type", "netstatus", @@ -959,6 +976,9 @@ CNetClient* client = static_cast(context); CNetMessage* message = static_cast(event->GetParamRef()); + if (!client->m_ClientTurnManager) + return true; + if (message) { if (message->GetType() == NMT_SIMULATION_COMMAND) Index: source/network/scripting/JSInterface_Network.cpp =================================================================== --- source/network/scripting/JSInterface_Network.cpp +++ source/network/scripting/JSInterface_Network.cpp @@ -178,7 +178,7 @@ * This is needed to not force server to share it's public ip with all potential clients in the lobby. * XmppClient will also handle logic after receiving the answer. */ -void StartNetworkJoinLobby(const CStrW& playerName, const CStr& hostJID, const CStr& password) +void StartNetworkJoinLobby(const CStrW& playerName, const CStr& hostJID, const CStr& password, const std::string& secret) { ENSURE(!!g_XmppClient); ENSURE(!g_NetClient); @@ -191,6 +191,7 @@ g_NetClient->SetUserName(playerName); g_NetClient->SetHostJID(hostJID); g_NetClient->SetGamePassword(hashedPass); + g_NetClient->SetControllerSecret(secret); g_XmppClient->SendIqGetConnectionData(hostJID, hashedPass.c_str(), false); } Index: source/ps/DedicatedServer.h =================================================================== --- /dev/null +++ source/ps/DedicatedServer.h @@ -0,0 +1,25 @@ +/* Copyright (C) 2021 Wildfire Games. + * This file is part of 0 A.D. + * + * 0 A.D. is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 2 of the License, or + * (at your option) any later version. + * + * 0 A.D. is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with 0 A.D. If not, see . + */ + +#ifndef INCLUDED_DEDICATEDSERVER +#define INCLUDED_DEDICATEDSERVER + +class CmdLineArgs; + +void RunDedicatedServer(const CmdLineArgs& args); + +#endif // INCLUDED_DEDICATEDSERVER Index: source/ps/DedicatedServer.cpp =================================================================== --- /dev/null +++ source/ps/DedicatedServer.cpp @@ -0,0 +1,151 @@ +/* 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 "DedicatedServer.h" + +#include "lib/input.h" +#include "lobby/IXmppClient.h" +#include "lobby/scripting/JSInterface_Lobby.h" +#include "network/NetClient.h" +#include "network/NetServer.h" +#include "network/StunClient.h" +#include "network/scripting/JSInterface_Network.h" +#include "ps/ConfigDB.h" +#include "ps/Game.h" +#include "ps/GUID.h" +#include "ps/GameSetup/CmdLineArgs.h" +#include "scriptinterface/FunctionWrapper.h" +#include "scriptinterface/ScriptContext.h" +#include "scriptinterface/ScriptInterface.h" +#include "simulation2/Simulation2.h" + +#include "lib/external_libraries/libsdl.h" + +#include +#include + +namespace +{ +void LoadFinished() +{ + if (!g_NetClient) + return; + g_NetClient->LoadFinished(); +} +} + +void RunDedicatedServer(const CmdLineArgs& args) +{ + ScriptInterface scriptInterface("Engine", "DedicatedServer", g_ScriptContext); + if (!scriptInterface.LoadGlobalScriptFile("dedicatedserver/DedicatedServer.js")) + { + LOGERROR("Could not load dedicated server script"); + return; + } + + { + ScriptRequest rq(scriptInterface); + JSI_Lobby::RegisterScriptFunctions(rq); + JSI_Network::RegisterScriptFunctions(rq); + ScriptFunction::Register<&LoadFinished>(rq, "MarkLoadFinished"); + } + + std::string lobbyPassword; + std::string lobbyLogin; + std::string lobbyRoom; + CFG_GET_VAL("lobby.password", lobbyPassword); + CFG_GET_VAL("lobby.login", lobbyLogin); + CFG_GET_VAL("lobby.room", lobbyRoom); + + g_XmppClient = + IXmppClient::create(&scriptInterface, + lobbyLogin, + lobbyPassword, + lobbyRoom, + lobbyLogin); + g_XmppClient->connect(); + + g_NetServer = new CNetServer(true); + CStr ip; + u16 port = 20595; + if (!StunClient::FindStunEndpointHost(ip, port)) + { + SAFE_DELETE(g_NetServer); + SAFE_DELETE(g_XmppClient); + return; + } + g_NetServer->SetConnectionData(ip, port, true); + + if (!g_NetServer->SetupConnection(port)) + { + SAFE_DELETE(g_NetServer); + SAFE_DELETE(g_XmppClient); + return; + } + + // Generate a secret to identify the host client. + std::string secret = ps_generate_guid(); + g_NetServer->SetControllerSecret(secret); + + LOGWARNING("Hosting at %s:%i, JID %s, secret %s", ip, port, g_XmppClient->GetJID(), secret); + LOGWARNING("Engine.PushGuiPage('page_gamesetup_mp.xml', { 'multiplayerGameType': 'dedicated', 'name': '%s', 'pass': '', 'hostJID': '%s', 'secret': '%s' });", lobbyLogin, g_XmppClient->GetJID(), secret); + + g_NetClient = new CNetClient(nullptr, scriptInterface); + g_NetClient->SetUserName(L"Fake observer"); + g_NetClient->SetHostJID("Fake observer"); + g_NetClient->SetGamePassword(""); + g_NetClient->SetControllerSecret(""); + g_NetClient->SetupServerData("127.0.0.1", port, false); + if (!g_NetClient->SetupConnection(nullptr)) + LOGWARNING("failed to connect client"); + + while (g_NetServer) + { + if (g_XmppClient) + g_XmppClient->recv(); + + g_NetClient->Poll(); + + { + ScriptRequest rq(scriptInterface); + JS::RootedValue glob(rq.cx, rq.globalValue()); + bool shouldQuit = false; + ScriptFunction::Call(rq, glob, "Tick", shouldQuit); + if (shouldQuit) + break; + } + + SDL_Event_ ev; + while (in_poll_event(&ev)) + in_dispatch_event(&ev); + + std::this_thread::sleep_for(std::chrono::milliseconds(100)); + }; + + g_NetClient->DestroyConnection(); + SAFE_DELETE(g_NetClient); + SAFE_DELETE(g_NetServer); + + if (g_XmppClient) + g_XmppClient->recv(); + + SAFE_DELETE(g_XmppClient); + + return; +}