mirror of
https://github.com/vcmi/vcmi.git
synced 2025-05-13 22:06:58 +02:00
Initial version of global lobby server available in client
This commit is contained in:
parent
c473e65b0f
commit
c2c43602ea
@ -89,6 +89,10 @@ if(NOT APPLE_IOS AND NOT ANDROID)
|
||||
option(ENABLE_MONOLITHIC_INSTALL "Install everything in single directory on Linux and Mac" OFF)
|
||||
endif()
|
||||
|
||||
if(NOT APPLE_IOS AND NOT ANDROID)
|
||||
option(ENABLE_LOBBY "Enable compilation of lobby server" ON)
|
||||
endif()
|
||||
|
||||
option(ENABLE_CCACHE "Speed up recompilation by caching previous compilations" OFF)
|
||||
if(ENABLE_CCACHE)
|
||||
find_program(CCACHE ccache REQUIRED)
|
||||
@ -475,6 +479,10 @@ if(TARGET SDL2_ttf::SDL2_ttf)
|
||||
add_library(SDL2::TTF ALIAS SDL2_ttf::SDL2_ttf)
|
||||
endif()
|
||||
|
||||
if(ENABLE_LOBBY)
|
||||
find_package(SQLite3 REQUIRED)
|
||||
endif()
|
||||
|
||||
if(ENABLE_LAUNCHER OR ENABLE_EDITOR)
|
||||
# Widgets finds its own dependencies (QtGui and QtCore).
|
||||
find_package(QT NAMES Qt6 Qt5 REQUIRED COMPONENTS Widgets Network)
|
||||
@ -622,6 +630,9 @@ endif()
|
||||
if(ENABLE_EDITOR)
|
||||
add_subdirectory(mapeditor)
|
||||
endif()
|
||||
if(ENABLE_LOBBY)
|
||||
add_subdirectory(lobby)
|
||||
endif()
|
||||
add_subdirectory(client)
|
||||
add_subdirectory(server)
|
||||
if(ENABLE_TEST)
|
||||
|
@ -95,6 +95,8 @@ set(client_SRCS
|
||||
renderSDL/ScreenHandler.cpp
|
||||
renderSDL/SDL_Extensions.cpp
|
||||
|
||||
serverLobby/LobbyWindow.cpp
|
||||
|
||||
widgets/Buttons.cpp
|
||||
widgets/CArtifactHolder.cpp
|
||||
widgets/CComponent.cpp
|
||||
@ -270,6 +272,8 @@ set(client_HEADERS
|
||||
renderSDL/SDL_Extensions.h
|
||||
renderSDL/SDL_PixelAccess.h
|
||||
|
||||
serverLobby/LobbyWindow.h
|
||||
|
||||
widgets/Buttons.h
|
||||
widgets/CArtifactHolder.h
|
||||
widgets/CComponent.h
|
||||
|
@ -129,6 +129,9 @@ void InterfaceObjectConfigurable::build(const JsonNode &config)
|
||||
|
||||
for(const auto & item : items->Vector())
|
||||
addWidget(item["name"].String(), buildWidget(item));
|
||||
|
||||
pos.w = config["width"].Integer();
|
||||
pos.h = config["height"].Integer();
|
||||
}
|
||||
|
||||
void InterfaceObjectConfigurable::addConditional(const std::string & name, bool active)
|
||||
|
@ -24,6 +24,7 @@
|
||||
#include "../gui/Shortcut.h"
|
||||
#include "../gui/WindowHandler.h"
|
||||
#include "../render/Canvas.h"
|
||||
#include "../serverLobby/LobbyWindow.h"
|
||||
#include "../widgets/CComponent.h"
|
||||
#include "../widgets/Buttons.h"
|
||||
#include "../widgets/MiscWidgets.h"
|
||||
@ -459,9 +460,17 @@ CMultiMode::CMultiMode(ESelectionScreen ScreenType)
|
||||
buttonHotseat = std::make_shared<CButton>(Point(373, 78), AnimationPath::builtin("MUBHOT.DEF"), CGI->generaltexth->zelp[266], std::bind(&CMultiMode::hostTCP, this));
|
||||
buttonHost = std::make_shared<CButton>(Point(373, 78 + 57 * 1), AnimationPath::builtin("MUBHOST.DEF"), CButton::tooltip(CGI->generaltexth->translate("vcmi.mainMenu.hostTCP"), ""), std::bind(&CMultiMode::hostTCP, this));
|
||||
buttonJoin = std::make_shared<CButton>(Point(373, 78 + 57 * 2), AnimationPath::builtin("MUBJOIN.DEF"), CButton::tooltip(CGI->generaltexth->translate("vcmi.mainMenu.joinTCP"), ""), std::bind(&CMultiMode::joinTCP, this));
|
||||
buttonLobby = std::make_shared<CButton>(Point(373, 78 + 57 * 4), AnimationPath::builtin("MUBONL.DEF"), CGI->generaltexth->zelp[265], std::bind(&CMultiMode::openLobby, this));
|
||||
|
||||
buttonCancel = std::make_shared<CButton>(Point(373, 424), AnimationPath::builtin("MUBCANC.DEF"), CGI->generaltexth->zelp[288], [=](){ close();}, EShortcut::GLOBAL_CANCEL);
|
||||
}
|
||||
|
||||
void CMultiMode::openLobby()
|
||||
{
|
||||
close();
|
||||
GH.windows().createAndPushWindow<LobbyWindow>();
|
||||
}
|
||||
|
||||
void CMultiMode::hostTCP()
|
||||
{
|
||||
auto savedScreenType = screenType;
|
||||
|
@ -86,12 +86,14 @@ public:
|
||||
std::shared_ptr<CPicture> picture;
|
||||
std::shared_ptr<CTextInput> playerName;
|
||||
std::shared_ptr<CButton> buttonHotseat;
|
||||
std::shared_ptr<CButton> buttonLobby;
|
||||
std::shared_ptr<CButton> buttonHost;
|
||||
std::shared_ptr<CButton> buttonJoin;
|
||||
std::shared_ptr<CButton> buttonCancel;
|
||||
std::shared_ptr<CGStatusBar> statusBar;
|
||||
|
||||
CMultiMode(ESelectionScreen ScreenType);
|
||||
void openLobby();
|
||||
void hostTCP();
|
||||
void joinTCP();
|
||||
std::string getPlayerName();
|
||||
|
40
client/serverLobby/LobbyWindow.cpp
Normal file
40
client/serverLobby/LobbyWindow.cpp
Normal file
@ -0,0 +1,40 @@
|
||||
/*
|
||||
* LobbyWindow.cpp, part of VCMI engine
|
||||
*
|
||||
* Authors: listed in file AUTHORS in main folder
|
||||
*
|
||||
* License: GNU General Public License v2.0 or later
|
||||
* Full text of license available in license.txt file, in main folder
|
||||
*
|
||||
*/
|
||||
|
||||
#include "StdInc.h"
|
||||
#include "LobbyWindow.h"
|
||||
|
||||
#include "../gui/CGuiHandler.h"
|
||||
#include "../gui/WindowHandler.h"
|
||||
|
||||
void LobbyClient::onPacketReceived(const std::vector<uint8_t> & message)
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
LobbyWidget::LobbyWidget()
|
||||
{
|
||||
addCallback("closeWindow", [](int) { GH.windows().popWindows(1); });
|
||||
|
||||
const JsonNode config(JsonPath::builtin("config/widgets/lobbyWindow.json"));
|
||||
build(config);
|
||||
}
|
||||
|
||||
LobbyWindow::LobbyWindow():
|
||||
CWindowObject(BORDERED)
|
||||
{
|
||||
OBJ_CONSTRUCTION_CAPTURING_ALL_NO_DISPOSE;
|
||||
widget = std::make_shared<LobbyWidget>();
|
||||
pos = widget->pos;
|
||||
center();
|
||||
connection = std::make_shared<LobbyClient>();
|
||||
|
||||
connection->start("127.0.0.1", 30303);
|
||||
}
|
37
client/serverLobby/LobbyWindow.h
Normal file
37
client/serverLobby/LobbyWindow.h
Normal file
@ -0,0 +1,37 @@
|
||||
/*
|
||||
* LobbyWindow.h, part of VCMI engine
|
||||
*
|
||||
* Authors: listed in file AUTHORS in main folder
|
||||
*
|
||||
* License: GNU General Public License v2.0 or later
|
||||
* Full text of license available in license.txt file, in main folder
|
||||
*
|
||||
*/
|
||||
#pragma once
|
||||
|
||||
#include "../gui/InterfaceObjectConfigurable.h"
|
||||
#include "../windows/CWindowObject.h"
|
||||
|
||||
#include "../../lib/network/NetworkClient.h"
|
||||
|
||||
class LobbyWidget : public InterfaceObjectConfigurable
|
||||
{
|
||||
public:
|
||||
LobbyWidget();
|
||||
};
|
||||
|
||||
class LobbyClient : public NetworkClient
|
||||
{
|
||||
void onPacketReceived(const std::vector<uint8_t> & message) override;
|
||||
public:
|
||||
LobbyClient() = default;
|
||||
};
|
||||
|
||||
class LobbyWindow : public CWindowObject
|
||||
{
|
||||
std::shared_ptr<LobbyWidget> widget;
|
||||
std::shared_ptr<LobbyClient> connection;
|
||||
|
||||
public:
|
||||
LobbyWindow();
|
||||
};
|
@ -124,6 +124,10 @@ macro(add_main_lib TARGET_NAME LIBRARY_TYPE)
|
||||
${MAIN_LIB_DIR}/modding/IdentifierStorage.cpp
|
||||
${MAIN_LIB_DIR}/modding/ModUtility.cpp
|
||||
|
||||
${MAIN_LIB_DIR}/network/NetworkClient.cpp
|
||||
${MAIN_LIB_DIR}/network/NetworkConnection.cpp
|
||||
${MAIN_LIB_DIR}/network/NetworkServer.cpp
|
||||
|
||||
${MAIN_LIB_DIR}/networkPacks/NetPacksLib.cpp
|
||||
|
||||
${MAIN_LIB_DIR}/pathfinder/CGPathNode.cpp
|
||||
@ -471,6 +475,11 @@ macro(add_main_lib TARGET_NAME LIBRARY_TYPE)
|
||||
${MAIN_LIB_DIR}/modding/ModUtility.h
|
||||
${MAIN_LIB_DIR}/modding/ModVerificationInfo.h
|
||||
|
||||
${MAIN_LIB_DIR}/network/NetworkClient.h
|
||||
${MAIN_LIB_DIR}/network/NetworkConnection.h
|
||||
${MAIN_LIB_DIR}/network/NetworkDefines.h
|
||||
${MAIN_LIB_DIR}/network/NetworkServer.h
|
||||
|
||||
${MAIN_LIB_DIR}/networkPacks/ArtifactLocation.h
|
||||
${MAIN_LIB_DIR}/networkPacks/BattleChanges.h
|
||||
${MAIN_LIB_DIR}/networkPacks/Component.h
|
||||
|
186
config/widgets/lobbyWindow.json
Normal file
186
config/widgets/lobbyWindow.json
Normal file
@ -0,0 +1,186 @@
|
||||
{
|
||||
"customTypes" : {
|
||||
"labelTitleMain" : {
|
||||
"type": "label",
|
||||
"font": "big",
|
||||
"alignment": "left",
|
||||
"color": "yellow"
|
||||
},
|
||||
"labelTitle" : {
|
||||
"type": "label",
|
||||
"font": "small",
|
||||
"alignment": "left",
|
||||
"color": "yellow"
|
||||
},
|
||||
"backgroundTexture" : {
|
||||
"type": "texture",
|
||||
"font": "tiny",
|
||||
"color" : "blue",
|
||||
"image": "DIBOXBCK"
|
||||
},
|
||||
"areaFilled":{
|
||||
"type": "transparentFilledRectangle",
|
||||
"color": [0, 0, 0, 75],
|
||||
"colorLine": [64, 80, 128, 255]
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
"width": 1024,
|
||||
"height": 600,
|
||||
|
||||
"items":
|
||||
[
|
||||
{
|
||||
"type": "backgroundTexture",
|
||||
"rect": {"w": 1024, "h": 600}
|
||||
},
|
||||
|
||||
{
|
||||
"type": "areaFilled",
|
||||
"rect": {"x": 5, "y": 5, "w": 250, "h": 40}
|
||||
},
|
||||
{
|
||||
"type": "labelTitleMain",
|
||||
"position": {"x": 15, "y": 10},
|
||||
"text" : "Player Name"
|
||||
},
|
||||
|
||||
{
|
||||
"type": "areaFilled",
|
||||
"rect": {"x": 5, "y": 50, "w": 250, "h": 150}
|
||||
},
|
||||
{
|
||||
"type": "labelTitle",
|
||||
"position": {"x": 15, "y": 53},
|
||||
"text" : "Match Filter"
|
||||
},
|
||||
|
||||
{
|
||||
"type": "areaFilled",
|
||||
"rect": {"x": 5, "y": 210, "w": 250, "h": 310}
|
||||
},
|
||||
{
|
||||
"type": "labelTitle",
|
||||
"position": {"x": 15, "y": 213},
|
||||
"text" : "Match List"
|
||||
},
|
||||
|
||||
{
|
||||
"type": "areaFilled",
|
||||
"rect": {"x": 270, "y": 50, "w": 150, "h": 540}
|
||||
},
|
||||
{
|
||||
"type": "labelTitle",
|
||||
"position": {"x": 280, "y": 53},
|
||||
"text" : "Channel List"
|
||||
},
|
||||
|
||||
{
|
||||
"type": "areaFilled",
|
||||
"rect": {"x": 430, "y": 50, "w": 430, "h": 515}
|
||||
},
|
||||
{
|
||||
"type": "labelTitle",
|
||||
"position": {"x": 440, "y": 53},
|
||||
"text" : "Game Chat"
|
||||
},
|
||||
|
||||
{
|
||||
"type": "areaFilled",
|
||||
"rect": {"x": 430, "y": 565, "w": 395, "h": 25}
|
||||
},
|
||||
{
|
||||
"type": "labelTitle",
|
||||
"position": {"x": 440, "y": 568},
|
||||
"text" : "Enter Message"
|
||||
},
|
||||
|
||||
{
|
||||
"type": "areaFilled",
|
||||
"rect": {"x": 870, "y": 50, "w": 150, "h": 540}
|
||||
},
|
||||
{
|
||||
"type": "labelTitle",
|
||||
"position": {"x": 880, "y": 53},
|
||||
"text" : "Player List"
|
||||
},
|
||||
|
||||
{
|
||||
"type": "button",
|
||||
"position": {"x": 940, "y": 10},
|
||||
"image": "settingsWindow/button80",
|
||||
"help": "core.help.288",
|
||||
"callback": "closeWindow",
|
||||
"hotkey": "globalReturn",
|
||||
"items":
|
||||
[
|
||||
{
|
||||
"type": "label",
|
||||
"font": "medium",
|
||||
"alignment": "center",
|
||||
"color": "yellow",
|
||||
"text": "Exit"
|
||||
}
|
||||
]
|
||||
},
|
||||
|
||||
{
|
||||
"type": "button",
|
||||
"position": {"x": 828, "y": 565},
|
||||
"image": "settingsWindow/button32",
|
||||
"help": "core.help.288",
|
||||
"callback": "closeWindow",
|
||||
"hotkey": "globalReturn",
|
||||
"items":
|
||||
[
|
||||
{
|
||||
"type": "label",
|
||||
"font": "medium",
|
||||
"alignment": "center",
|
||||
"color": "yellow",
|
||||
"text": ">"
|
||||
}
|
||||
]
|
||||
},
|
||||
|
||||
{
|
||||
"type": "button",
|
||||
"position": {"x": 10, "y": 520},
|
||||
"image": "settingsWindow/button190",
|
||||
"help": "core.help.288",
|
||||
"callback": "closeWindow",
|
||||
"hotkey": "globalReturn",
|
||||
"items":
|
||||
[
|
||||
{
|
||||
"type": "label",
|
||||
"font": "medium",
|
||||
"alignment": "center",
|
||||
"color": "yellow",
|
||||
"text": "Start Public Game"
|
||||
}
|
||||
]
|
||||
},
|
||||
|
||||
{
|
||||
"type": "button",
|
||||
"position": {"x": 10, "y": 555},
|
||||
"image": "settingsWindow/button190",
|
||||
"help": "core.help.288",
|
||||
"callback": "closeWindow",
|
||||
"hotkey": "globalReturn",
|
||||
"items":
|
||||
[
|
||||
{
|
||||
"type": "label",
|
||||
"font": "medium",
|
||||
"alignment": "center",
|
||||
"color": "yellow",
|
||||
"text": "Start Private Game"
|
||||
}
|
||||
]
|
||||
},
|
||||
|
||||
]
|
||||
}
|
47
lib/network/NetworkClient.cpp
Normal file
47
lib/network/NetworkClient.cpp
Normal file
@ -0,0 +1,47 @@
|
||||
/*
|
||||
* NetworkClient.cpp, part of VCMI engine
|
||||
*
|
||||
* Authors: listed in file AUTHORS in main folder
|
||||
*
|
||||
* License: GNU General Public License v2.0 or later
|
||||
* Full text of license available in license.txt file, in main folder
|
||||
*
|
||||
*/
|
||||
#include "StdInc.h"
|
||||
#include "NetworkClient.h"
|
||||
#include "NetworkConnection.h"
|
||||
|
||||
VCMI_LIB_NAMESPACE_BEGIN
|
||||
|
||||
NetworkClient::NetworkClient()
|
||||
: io(new NetworkService)
|
||||
, socket(new NetworkSocket(*io))
|
||||
// , timer(new NetworkTimer(*io))
|
||||
{
|
||||
}
|
||||
|
||||
void NetworkClient::start(const std::string & host, uint16_t port)
|
||||
{
|
||||
boost::asio::ip::tcp::resolver resolver(*io);
|
||||
auto endpoints = resolver.resolve(host, std::to_string(port));
|
||||
|
||||
boost::asio::async_connect(*socket, endpoints, std::bind(&NetworkClient::onConnected, this, _1));
|
||||
}
|
||||
|
||||
void NetworkClient::onConnected(const boost::system::error_code & ec)
|
||||
{
|
||||
connection = std::make_shared<NetworkConnection>(socket);
|
||||
connection->start();
|
||||
}
|
||||
|
||||
void NetworkClient::run()
|
||||
{
|
||||
io->run();
|
||||
}
|
||||
|
||||
void NetworkClient::sendPacket(const std::vector<uint8_t> & message)
|
||||
{
|
||||
connection->sendPacket(message);
|
||||
}
|
||||
|
||||
VCMI_LIB_NAMESPACE_END
|
37
lib/network/NetworkClient.h
Normal file
37
lib/network/NetworkClient.h
Normal file
@ -0,0 +1,37 @@
|
||||
/*
|
||||
* NetworkClient.h, part of VCMI engine
|
||||
*
|
||||
* Authors: listed in file AUTHORS in main folder
|
||||
*
|
||||
* License: GNU General Public License v2.0 or later
|
||||
* Full text of license available in license.txt file, in main folder
|
||||
*
|
||||
*/
|
||||
#pragma once
|
||||
|
||||
#include "NetworkDefines.h"
|
||||
|
||||
VCMI_LIB_NAMESPACE_BEGIN
|
||||
|
||||
class NetworkConnection;
|
||||
|
||||
class DLL_LINKAGE NetworkClient : boost::noncopyable
|
||||
{
|
||||
std::shared_ptr<NetworkService> io;
|
||||
std::shared_ptr<NetworkSocket> socket;
|
||||
std::shared_ptr<NetworkConnection> connection;
|
||||
std::shared_ptr<NetworkTimer> timer;
|
||||
|
||||
void onConnected(const boost::system::error_code & ec);
|
||||
protected:
|
||||
virtual void onPacketReceived(const std::vector<uint8_t> & message) = 0;
|
||||
public:
|
||||
NetworkClient();
|
||||
virtual ~NetworkClient() = default;
|
||||
|
||||
void start(const std::string & host, uint16_t port);
|
||||
void sendPacket(const std::vector<uint8_t> & message);
|
||||
void run();
|
||||
};
|
||||
|
||||
VCMI_LIB_NAMESPACE_END
|
97
lib/network/NetworkConnection.cpp
Normal file
97
lib/network/NetworkConnection.cpp
Normal file
@ -0,0 +1,97 @@
|
||||
/*
|
||||
* NetworkConnection.cpp, part of VCMI engine
|
||||
*
|
||||
* Authors: listed in file AUTHORS in main folder
|
||||
*
|
||||
* License: GNU General Public License v2.0 or later
|
||||
* Full text of license available in license.txt file, in main folder
|
||||
*
|
||||
*/
|
||||
#include "StdInc.h"
|
||||
#include "NetworkConnection.h"
|
||||
|
||||
VCMI_LIB_NAMESPACE_BEGIN
|
||||
|
||||
NetworkConnection::NetworkConnection(const std::shared_ptr<NetworkSocket> & socket)
|
||||
: socket(socket)
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
void NetworkConnection::start()
|
||||
{
|
||||
boost::asio::async_read(*socket,
|
||||
readBuffer,
|
||||
boost::asio::transfer_exactly(messageHeaderSize),
|
||||
std::bind(&NetworkConnection::onHeaderReceived,this, _1));
|
||||
}
|
||||
|
||||
void NetworkConnection::onHeaderReceived(const boost::system::error_code & ec)
|
||||
{
|
||||
uint32_t messageSize = readPacketSize(ec);
|
||||
|
||||
boost::asio::async_read(*socket,
|
||||
readBuffer,
|
||||
boost::asio::transfer_exactly(messageSize),
|
||||
std::bind(&NetworkConnection::onPacketReceived,this, _1, messageSize));
|
||||
}
|
||||
|
||||
uint32_t NetworkConnection::readPacketSize(const boost::system::error_code & ec)
|
||||
{
|
||||
if (ec)
|
||||
{
|
||||
throw std::runtime_error("Connection aborted!");
|
||||
}
|
||||
|
||||
if (readBuffer.size() < messageHeaderSize)
|
||||
{
|
||||
throw std::runtime_error("Failed to read header!");
|
||||
}
|
||||
|
||||
std::istream istream(&readBuffer);
|
||||
|
||||
uint32_t messageSize;
|
||||
istream.read(reinterpret_cast<char *>(&messageSize), messageHeaderSize);
|
||||
|
||||
if (messageSize > messageMaxSize)
|
||||
{
|
||||
throw std::runtime_error("Invalid packet size!");
|
||||
}
|
||||
|
||||
return messageSize;
|
||||
}
|
||||
|
||||
void NetworkConnection::onPacketReceived(const boost::system::error_code & ec, uint32_t expectedPacketSize)
|
||||
{
|
||||
if (ec)
|
||||
{
|
||||
throw std::runtime_error("Connection aborted!");
|
||||
}
|
||||
|
||||
if (readBuffer.size() < expectedPacketSize)
|
||||
{
|
||||
throw std::runtime_error("Failed to read header!");
|
||||
}
|
||||
|
||||
std::vector<uint8_t> message;
|
||||
|
||||
message.resize(expectedPacketSize);
|
||||
std::istream istream(&readBuffer);
|
||||
istream.read(reinterpret_cast<char *>(message.data()), messageHeaderSize);
|
||||
|
||||
start();
|
||||
}
|
||||
|
||||
void NetworkConnection::sendPacket(const std::vector<uint8_t> & message)
|
||||
{
|
||||
NetworkBuffer writeBuffer;
|
||||
|
||||
std::ostream ostream(&writeBuffer);
|
||||
uint32_t messageSize = message.size();
|
||||
ostream.write(reinterpret_cast<const char *>(&messageSize), messageHeaderSize);
|
||||
ostream.write(reinterpret_cast<const char *>(message.data()), message.size());
|
||||
|
||||
boost::asio::write(*socket, writeBuffer );
|
||||
}
|
||||
|
||||
VCMI_LIB_NAMESPACE_END
|
36
lib/network/NetworkConnection.h
Normal file
36
lib/network/NetworkConnection.h
Normal file
@ -0,0 +1,36 @@
|
||||
/*
|
||||
* NetworkConnection.h, part of VCMI engine
|
||||
*
|
||||
* Authors: listed in file AUTHORS in main folder
|
||||
*
|
||||
* License: GNU General Public License v2.0 or later
|
||||
* Full text of license available in license.txt file, in main folder
|
||||
*
|
||||
*/
|
||||
#pragma once
|
||||
|
||||
#include "NetworkDefines.h"
|
||||
|
||||
VCMI_LIB_NAMESPACE_BEGIN
|
||||
|
||||
class DLL_LINKAGE NetworkConnection : boost::noncopyable
|
||||
{
|
||||
static const int messageHeaderSize = sizeof(uint32_t);
|
||||
static const int messageMaxSize = 1024;
|
||||
|
||||
std::shared_ptr<NetworkSocket> socket;
|
||||
|
||||
NetworkBuffer readBuffer;
|
||||
|
||||
void onHeaderReceived(const boost::system::error_code & ec);
|
||||
void onPacketReceived(const boost::system::error_code & ec, uint32_t expectedPacketSize);
|
||||
uint32_t readPacketSize(const boost::system::error_code & ec);
|
||||
|
||||
public:
|
||||
NetworkConnection(const std::shared_ptr<NetworkSocket> & socket);
|
||||
|
||||
void start();
|
||||
void sendPacket(const std::vector<uint8_t> & message);
|
||||
};
|
||||
|
||||
VCMI_LIB_NAMESPACE_END
|
22
lib/network/NetworkDefines.h
Normal file
22
lib/network/NetworkDefines.h
Normal file
@ -0,0 +1,22 @@
|
||||
/*
|
||||
* NetworkDefines.h, part of VCMI engine
|
||||
*
|
||||
* Authors: listed in file AUTHORS in main folder
|
||||
*
|
||||
* License: GNU General Public License v2.0 or later
|
||||
* Full text of license available in license.txt file, in main folder
|
||||
*
|
||||
*/
|
||||
#pragma once
|
||||
|
||||
#include <boost/asio.hpp>
|
||||
|
||||
VCMI_LIB_NAMESPACE_BEGIN
|
||||
|
||||
using NetworkService = boost::asio::io_service;
|
||||
using NetworkSocket = boost::asio::basic_stream_socket<boost::asio::ip::tcp>;
|
||||
using NetworkAcceptor = boost::asio::basic_socket_acceptor<boost::asio::ip::tcp>;
|
||||
using NetworkBuffer = boost::asio::streambuf;
|
||||
using NetworkTimer = boost::asio::steady_timer;
|
||||
|
||||
VCMI_LIB_NAMESPACE_END
|
63
lib/network/NetworkServer.cpp
Normal file
63
lib/network/NetworkServer.cpp
Normal file
@ -0,0 +1,63 @@
|
||||
/*
|
||||
* NetworkServer.cpp, part of VCMI engine
|
||||
*
|
||||
* Authors: listed in file AUTHORS in main folder
|
||||
*
|
||||
* License: GNU General Public License v2.0 or later
|
||||
* Full text of license available in license.txt file, in main folder
|
||||
*
|
||||
*/
|
||||
#include "StdInc.h"
|
||||
#include "NetworkServer.h"
|
||||
#include "NetworkConnection.h"
|
||||
|
||||
VCMI_LIB_NAMESPACE_BEGIN
|
||||
|
||||
void NetworkServer::start(uint16_t port)
|
||||
{
|
||||
io = std::make_shared<boost::asio::io_service>();
|
||||
acceptor = std::make_shared<NetworkAcceptor>(*io, boost::asio::ip::tcp::endpoint(boost::asio::ip::tcp::v4(), port));
|
||||
|
||||
startAsyncAccept();
|
||||
}
|
||||
|
||||
void NetworkServer::startAsyncAccept()
|
||||
{
|
||||
std::shared_ptr<NetworkSocket> upcomingConnection = std::make_shared<NetworkSocket>(*io);
|
||||
acceptor->async_accept(*upcomingConnection, std::bind(&NetworkServer::connectionAccepted, this, upcomingConnection, _1));
|
||||
}
|
||||
|
||||
void NetworkServer::run()
|
||||
{
|
||||
io->run();
|
||||
}
|
||||
|
||||
void NetworkServer::connectionAccepted(std::shared_ptr<NetworkSocket> upcomingConnection, const boost::system::error_code & ec)
|
||||
{
|
||||
if(ec)
|
||||
{
|
||||
logNetwork->info("Something wrong during accepting: %s", ec.message());
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
logNetwork->info("We got a new connection! :)");
|
||||
auto connection = std::make_shared<NetworkConnection>(upcomingConnection);
|
||||
connections.insert(connection);
|
||||
connection->start();
|
||||
}
|
||||
catch(std::exception & e)
|
||||
{
|
||||
logNetwork->error("Failure processing new connection! %s", e.what());
|
||||
}
|
||||
|
||||
startAsyncAccept();
|
||||
}
|
||||
|
||||
void NetworkServer::sendPacket(const std::shared_ptr<NetworkConnection> & connection, const std::vector<uint8_t> & message)
|
||||
{
|
||||
connection->sendPacket(message);
|
||||
}
|
||||
|
||||
VCMI_LIB_NAMESPACE_END
|
39
lib/network/NetworkServer.h
Normal file
39
lib/network/NetworkServer.h
Normal file
@ -0,0 +1,39 @@
|
||||
/*
|
||||
* NetworkServer.h, part of VCMI engine
|
||||
*
|
||||
* Authors: listed in file AUTHORS in main folder
|
||||
*
|
||||
* License: GNU General Public License v2.0 or later
|
||||
* Full text of license available in license.txt file, in main folder
|
||||
*
|
||||
*/
|
||||
#pragma once
|
||||
|
||||
#include "NetworkDefines.h"
|
||||
|
||||
VCMI_LIB_NAMESPACE_BEGIN
|
||||
|
||||
class NetworkConnection;
|
||||
|
||||
class DLL_LINKAGE NetworkServer : boost::noncopyable
|
||||
{
|
||||
std::shared_ptr<NetworkService> io;
|
||||
std::shared_ptr<NetworkAcceptor> acceptor;
|
||||
std::set<std::shared_ptr<NetworkConnection>> connections;
|
||||
|
||||
void connectionAccepted(std::shared_ptr<NetworkSocket>, const boost::system::error_code & ec);
|
||||
void startAsyncAccept();
|
||||
protected:
|
||||
virtual void onNewConnection(std::shared_ptr<NetworkConnection>) = 0;
|
||||
virtual void onPacketReceived(std::shared_ptr<NetworkConnection>, const std::vector<uint8_t> & message) = 0;
|
||||
|
||||
void sendPacket(const std::shared_ptr<NetworkConnection> &, const std::vector<uint8_t> & message);
|
||||
|
||||
public:
|
||||
virtual ~NetworkServer() = default;
|
||||
|
||||
void start(uint16_t port);
|
||||
void run();
|
||||
};
|
||||
|
||||
VCMI_LIB_NAMESPACE_END
|
39
lobby/CMakeLists.txt
Normal file
39
lobby/CMakeLists.txt
Normal file
@ -0,0 +1,39 @@
|
||||
set(lobby_SRCS
|
||||
StdInc.cpp
|
||||
LobbyServer.cpp
|
||||
SQLiteConnection.cpp
|
||||
)
|
||||
|
||||
set(lobby_HEADERS
|
||||
StdInc.h
|
||||
LobbyServer.h
|
||||
SQLiteConnection.h
|
||||
)
|
||||
|
||||
assign_source_group(${lobby_SRCS} ${lobby_HEADERS})
|
||||
|
||||
add_executable(vcmilobby ${lobby_SRCS} ${lobby_HEADERS})
|
||||
set(lobby_LIBS vcmi)
|
||||
|
||||
if(CMAKE_SYSTEM_NAME MATCHES FreeBSD OR HAIKU)
|
||||
set(lobby_LIBS execinfo ${lobby_LIBS})
|
||||
endif()
|
||||
|
||||
target_link_libraries(vcmilobby PRIVATE ${lobby_LIBS} ${SQLite3_LIBRARIES})
|
||||
|
||||
target_include_directories(vcmilobby PRIVATE ${SQLite3_INCLUDE_DIRS})
|
||||
target_include_directories(vcmilobby PUBLIC ${CMAKE_CURRENT_SOURCE_DIR})
|
||||
|
||||
if(WIN32)
|
||||
set_target_properties(vcmilobby
|
||||
PROPERTIES
|
||||
OUTPUT_NAME "VCMI_lobby"
|
||||
PROJECT_LABEL "VCMI_lobby"
|
||||
)
|
||||
endif()
|
||||
|
||||
vcmi_set_output_dir(vcmilobby "")
|
||||
enable_pch(vcmilobby)
|
||||
|
||||
install(TARGETS vcmilobby DESTINATION ${BIN_DIR})
|
||||
|
45
lobby/LobbyServer.cpp
Normal file
45
lobby/LobbyServer.cpp
Normal file
@ -0,0 +1,45 @@
|
||||
/*
|
||||
* LobbyServer.cpp, part of VCMI engine
|
||||
*
|
||||
* Authors: listed in file AUTHORS in main folder
|
||||
*
|
||||
* License: GNU General Public License v2.0 or later
|
||||
* Full text of license available in license.txt file, in main folder
|
||||
*
|
||||
*/
|
||||
#include "StdInc.h"
|
||||
#include "LobbyServer.h"
|
||||
|
||||
#include "SQLiteConnection.h"
|
||||
|
||||
#include <boost/uuid/uuid_generators.hpp>
|
||||
#include <boost/uuid/uuid_io.hpp>
|
||||
|
||||
static const std::string DATABASE_PATH = "~/vcmi.db";
|
||||
static const int LISTENING_PORT = 30303;
|
||||
//static const std::string SERVER_NAME = GameConstants::VCMI_VERSION + " (server)";
|
||||
//static const std::string SERVER_UUID = boost::uuids::to_string(boost::uuids::random_generator()());
|
||||
|
||||
void LobbyServer::onNewConnection(std::shared_ptr<NetworkConnection>)
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
void LobbyServer::onPacketReceived(std::shared_ptr<NetworkConnection>, const std::vector<uint8_t> & message)
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
LobbyServer::LobbyServer()
|
||||
{
|
||||
database = SQLiteInstance::open(DATABASE_PATH, true);
|
||||
|
||||
}
|
||||
|
||||
int main(int argc, const char * argv[])
|
||||
{
|
||||
LobbyServer server;
|
||||
|
||||
server.start(LISTENING_PORT);
|
||||
server.run();
|
||||
}
|
24
lobby/LobbyServer.h
Normal file
24
lobby/LobbyServer.h
Normal file
@ -0,0 +1,24 @@
|
||||
/*
|
||||
* LobbyServer.h, part of VCMI engine
|
||||
*
|
||||
* Authors: listed in file AUTHORS in main folder
|
||||
*
|
||||
* License: GNU General Public License v2.0 or later
|
||||
* Full text of license available in license.txt file, in main folder
|
||||
*
|
||||
*/
|
||||
#pragma once
|
||||
|
||||
#include "../lib/network/NetworkServer.h"
|
||||
|
||||
class SQLiteInstance;
|
||||
|
||||
class LobbyServer : public NetworkServer
|
||||
{
|
||||
std::unique_ptr<SQLiteInstance> database;
|
||||
|
||||
void onNewConnection(std::shared_ptr<NetworkConnection>) override;
|
||||
void onPacketReceived(std::shared_ptr<NetworkConnection>, const std::vector<uint8_t> & message) override;
|
||||
public:
|
||||
LobbyServer();
|
||||
};
|
197
lobby/SQLiteConnection.cpp
Normal file
197
lobby/SQLiteConnection.cpp
Normal file
@ -0,0 +1,197 @@
|
||||
/*
|
||||
* SQLiteConnection.cpp, part of VCMI engine
|
||||
*
|
||||
* Authors: listed in file AUTHORS in main folder
|
||||
*
|
||||
* License: GNU General Public License v2.0 or later
|
||||
* Full text of license available in license.txt file, in main folder
|
||||
*
|
||||
*/
|
||||
#include "StdInc.h"
|
||||
#include "SQLiteConnection.h"
|
||||
|
||||
#include <sqlite3.h>
|
||||
|
||||
static void on_sqlite_error( sqlite3 * connection, [[maybe_unused]] int result )
|
||||
{
|
||||
if ( result != SQLITE_OK )
|
||||
{
|
||||
printf( "sqlite error: %s\n", sqlite3_errmsg( connection ) );
|
||||
}
|
||||
|
||||
assert( result == SQLITE_OK );
|
||||
}
|
||||
|
||||
SQLiteStatement::SQLiteStatement( SQLiteInstance & instance, sqlite3_stmt * statement ):
|
||||
m_instance( instance ),
|
||||
m_statement( statement )
|
||||
{ }
|
||||
|
||||
SQLiteStatement::~SQLiteStatement( )
|
||||
{
|
||||
int result = sqlite3_finalize( m_statement );
|
||||
on_sqlite_error( m_instance.m_connection, result );
|
||||
}
|
||||
|
||||
bool SQLiteStatement::execute( )
|
||||
{
|
||||
int result = sqlite3_step( m_statement );
|
||||
|
||||
switch ( result )
|
||||
{
|
||||
case SQLITE_DONE:
|
||||
return false;
|
||||
case SQLITE_ROW:
|
||||
return true;
|
||||
default:
|
||||
on_sqlite_error( m_instance.m_connection, result );
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
void SQLiteStatement::reset( )
|
||||
{
|
||||
int result = sqlite3_reset( m_statement );
|
||||
on_sqlite_error( m_instance.m_connection, result );
|
||||
}
|
||||
|
||||
void SQLiteStatement::clear()
|
||||
{
|
||||
int result = sqlite3_clear_bindings(m_statement);
|
||||
on_sqlite_error(m_instance.m_connection, result);
|
||||
}
|
||||
|
||||
void SQLiteStatement::setBindSingle( size_t index, double const & value )
|
||||
{
|
||||
int result = sqlite3_bind_double( m_statement, static_cast<int>(index), value );
|
||||
on_sqlite_error( m_instance.m_connection, result );
|
||||
}
|
||||
|
||||
void SQLiteStatement::setBindSingle(size_t index, uint8_t const & value)
|
||||
{
|
||||
int result = sqlite3_bind_int(m_statement, static_cast<int>(index), value);
|
||||
on_sqlite_error(m_instance.m_connection, result);
|
||||
}
|
||||
|
||||
void SQLiteStatement::setBindSingle(size_t index, uint16_t const & value)
|
||||
{
|
||||
int result = sqlite3_bind_int(m_statement, static_cast<int>(index), value);
|
||||
on_sqlite_error(m_instance.m_connection, result);
|
||||
}
|
||||
void SQLiteStatement::setBindSingle(size_t index, uint32_t const & value)
|
||||
{
|
||||
int result = sqlite3_bind_int(m_statement, static_cast<int>(index), value);
|
||||
on_sqlite_error(m_instance.m_connection, result);
|
||||
}
|
||||
|
||||
void SQLiteStatement::setBindSingle( size_t index, int32_t const & value )
|
||||
{
|
||||
int result = sqlite3_bind_int( m_statement, static_cast<int>( index ), value );
|
||||
on_sqlite_error( m_instance.m_connection, result );
|
||||
}
|
||||
|
||||
void SQLiteStatement::setBindSingle( size_t index, int64_t const & value )
|
||||
{
|
||||
int result = sqlite3_bind_int64( m_statement, static_cast<int>( index ), value );
|
||||
on_sqlite_error( m_instance.m_connection, result );
|
||||
}
|
||||
|
||||
void SQLiteStatement::setBindSingle( size_t index, std::string const & value )
|
||||
{
|
||||
int result = sqlite3_bind_text( m_statement, static_cast<int>( index ), value.data(), static_cast<int>( value.size() ), SQLITE_STATIC );
|
||||
on_sqlite_error( m_instance.m_connection, result );
|
||||
}
|
||||
|
||||
void SQLiteStatement::setBindSingle( size_t index, char const * value )
|
||||
{
|
||||
int result = sqlite3_bind_text( m_statement, static_cast<int>( index ), value, -1, SQLITE_STATIC );
|
||||
on_sqlite_error( m_instance.m_connection, result );
|
||||
}
|
||||
|
||||
void SQLiteStatement::getColumnSingle( size_t index, double & value )
|
||||
{
|
||||
value = sqlite3_column_double( m_statement, static_cast<int>( index ) );
|
||||
}
|
||||
|
||||
void SQLiteStatement::getColumnSingle( size_t index, uint8_t & value )
|
||||
{
|
||||
value = static_cast<uint8_t>(sqlite3_column_int( m_statement, static_cast<int>( index ) ));
|
||||
}
|
||||
|
||||
void SQLiteStatement::getColumnSingle(size_t index, uint16_t & value)
|
||||
{
|
||||
value = static_cast<uint16_t>(sqlite3_column_int(m_statement, static_cast<int>(index)));
|
||||
}
|
||||
|
||||
void SQLiteStatement::getColumnSingle( size_t index, int32_t & value )
|
||||
{
|
||||
value = sqlite3_column_int( m_statement, static_cast<int>( index ) );
|
||||
}
|
||||
|
||||
void SQLiteStatement::getColumnSingle(size_t index, uint32_t & value)
|
||||
{
|
||||
value = sqlite3_column_int(m_statement, static_cast<int>(index));
|
||||
}
|
||||
|
||||
void SQLiteStatement::getColumnSingle( size_t index, int64_t & value )
|
||||
{
|
||||
value = sqlite3_column_int64( m_statement, static_cast<int>( index ) );
|
||||
}
|
||||
|
||||
void SQLiteStatement::getColumnSingle( size_t index, std::string & value )
|
||||
{
|
||||
auto value_raw = sqlite3_column_text(m_statement, static_cast<int>(index));
|
||||
value = reinterpret_cast<char const*>( value_raw );
|
||||
}
|
||||
|
||||
void SQLiteStatement::getColumnBlob(size_t index, std::byte * value, [[maybe_unused]] size_t capacity)
|
||||
{
|
||||
auto * blob_data = sqlite3_column_blob(m_statement, static_cast<int>(index));
|
||||
size_t blob_size = sqlite3_column_bytes(m_statement, static_cast<int>(index));
|
||||
|
||||
assert(blob_size < capacity);
|
||||
|
||||
std::copy_n(static_cast<std::byte const*>(blob_data), blob_size, value);
|
||||
value[blob_size] = std::byte(0);
|
||||
}
|
||||
|
||||
SQLiteInstancePtr SQLiteInstance::open( std::string const & db_path, bool allow_write)
|
||||
{
|
||||
int flags = allow_write ? (SQLITE_OPEN_READWRITE | SQLITE_OPEN_CREATE) : SQLITE_OPEN_READONLY;
|
||||
|
||||
sqlite3 * connection;
|
||||
int result = sqlite3_open_v2( db_path.c_str( ), &connection, flags, nullptr );
|
||||
|
||||
on_sqlite_error( connection, result );
|
||||
|
||||
assert(result == SQLITE_OK);
|
||||
|
||||
if ( result == SQLITE_OK )
|
||||
return SQLiteInstancePtr( new SQLiteInstance( connection ) );
|
||||
|
||||
sqlite3_close( connection );
|
||||
return SQLiteInstancePtr( );
|
||||
}
|
||||
|
||||
SQLiteInstance::SQLiteInstance( sqlite3 * connection ):
|
||||
m_connection( connection )
|
||||
{ }
|
||||
|
||||
SQLiteInstance::~SQLiteInstance( )
|
||||
{
|
||||
int result = sqlite3_close( m_connection );
|
||||
on_sqlite_error( m_connection, result );
|
||||
}
|
||||
|
||||
SQLiteStatementPtr SQLiteInstance::prepare( std::string const & sql_text )
|
||||
{
|
||||
sqlite3_stmt * statement;
|
||||
int result = sqlite3_prepare_v2( m_connection, sql_text.data(), static_cast<int>( sql_text.size()), &statement, nullptr );
|
||||
|
||||
on_sqlite_error( m_connection, result );
|
||||
|
||||
if ( result == SQLITE_OK )
|
||||
return SQLiteStatementPtr( new SQLiteStatement( *this, statement ) );
|
||||
sqlite3_finalize( statement );
|
||||
return SQLiteStatementPtr( );
|
||||
}
|
99
lobby/SQLiteConnection.h
Normal file
99
lobby/SQLiteConnection.h
Normal file
@ -0,0 +1,99 @@
|
||||
/*
|
||||
* SQLiteConnection.h, part of VCMI engine
|
||||
*
|
||||
* Authors: listed in file AUTHORS in main folder
|
||||
*
|
||||
* License: GNU General Public License v2.0 or later
|
||||
* Full text of license available in license.txt file, in main folder
|
||||
*
|
||||
*/
|
||||
#pragma once
|
||||
|
||||
typedef struct sqlite3 sqlite3;
|
||||
typedef struct sqlite3_stmt sqlite3_stmt;
|
||||
|
||||
class SQLiteInstance;
|
||||
class SQLiteStatement;
|
||||
|
||||
using SQLiteInstancePtr = std::unique_ptr< SQLiteInstance >;
|
||||
using SQLiteStatementPtr = std::unique_ptr< SQLiteStatement >;
|
||||
|
||||
class SQLiteStatement : boost::noncopyable
|
||||
{
|
||||
public:
|
||||
friend class SQLiteInstance;
|
||||
|
||||
bool execute( );
|
||||
void reset( );
|
||||
void clear( );
|
||||
|
||||
~SQLiteStatement();
|
||||
|
||||
template<typename ... Args >
|
||||
void setBinds( Args const & ... args )
|
||||
{
|
||||
setBindSingle( 1, args... ); // The leftmost SQL parameter has an index of 1
|
||||
}
|
||||
|
||||
template<typename ... Args >
|
||||
void getColumns( Args & ... args )
|
||||
{
|
||||
getColumnSingle( 0, args... ); // The leftmost column of the result set has the index 0
|
||||
}
|
||||
|
||||
private:
|
||||
void setBindSingle( size_t index, double const & value );
|
||||
void setBindSingle( size_t index, uint8_t const & value );
|
||||
void setBindSingle( size_t index, uint16_t const & value );
|
||||
void setBindSingle( size_t index, uint32_t const & value );
|
||||
void setBindSingle( size_t index, int32_t const & value );
|
||||
void setBindSingle( size_t index, int64_t const & value );
|
||||
void setBindSingle( size_t index, std::string const & value );
|
||||
void setBindSingle( size_t index, char const * value );
|
||||
|
||||
void getColumnSingle( size_t index, double & value );
|
||||
void getColumnSingle( size_t index, uint8_t & value );
|
||||
void getColumnSingle( size_t index, uint16_t & value );
|
||||
void getColumnSingle( size_t index, uint32_t & value );
|
||||
void getColumnSingle( size_t index, int32_t & value );
|
||||
void getColumnSingle( size_t index, int64_t & value );
|
||||
void getColumnSingle( size_t index, std::string & value );
|
||||
|
||||
SQLiteStatement( SQLiteInstance & instance, sqlite3_stmt * statement );
|
||||
|
||||
template<typename T, typename ... Args >
|
||||
void setBindSingle( size_t index, T const & arg, Args const & ... args )
|
||||
{
|
||||
setBindSingle( index, arg );
|
||||
setBindSingle( index + 1, args... );
|
||||
}
|
||||
|
||||
template<typename T, typename ... Args >
|
||||
void getColumnSingle( size_t index, T & arg, Args & ... args )
|
||||
{
|
||||
getColumnSingle( index, arg );
|
||||
getColumnSingle( index + 1, args... );
|
||||
}
|
||||
|
||||
void getColumnBlob(size_t index, std::byte * value, size_t capacity);
|
||||
|
||||
SQLiteInstance & m_instance;
|
||||
sqlite3_stmt * m_statement;
|
||||
};
|
||||
|
||||
class SQLiteInstance : boost::noncopyable
|
||||
{
|
||||
public:
|
||||
friend class SQLiteStatement;
|
||||
|
||||
static SQLiteInstancePtr open(std::string const & db_path, bool allow_write );
|
||||
|
||||
~SQLiteInstance( );
|
||||
|
||||
SQLiteStatementPtr prepare( std::string const & statement );
|
||||
|
||||
private:
|
||||
SQLiteInstance( sqlite3 * connection );
|
||||
|
||||
sqlite3 * m_connection;
|
||||
};
|
2
lobby/StdInc.cpp
Normal file
2
lobby/StdInc.cpp
Normal file
@ -0,0 +1,2 @@
|
||||
// Creates the precompiled header
|
||||
#include "StdInc.h"
|
14
lobby/StdInc.h
Normal file
14
lobby/StdInc.h
Normal file
@ -0,0 +1,14 @@
|
||||
/*
|
||||
* StdInc.h, part of VCMI engine
|
||||
*
|
||||
* Authors: listed in file AUTHORS in main folder
|
||||
*
|
||||
* License: GNU General Public License v2.0 or later
|
||||
* Full text of license available in license.txt file, in main folder
|
||||
*
|
||||
*/
|
||||
#pragma once
|
||||
|
||||
#include "../Global.h"
|
||||
|
||||
VCMI_LIB_USING_NAMESPACE
|
Loading…
x
Reference in New Issue
Block a user