diff --git a/BUILDING.md b/BUILDING.md index fce025d..e97a551 100644 --- a/BUILDING.md +++ b/BUILDING.md @@ -26,11 +26,15 @@ cmake --build . --config Release --target all ## Using CMake GUI (all destkop platforms) - Download CMake GUI installer for your OS from the official site. (supported on all desktop OS) -- On MacOS, to add Cmake CLI to path, run the following command: +- On MacOS, after installing the GUI, add Cmake CLI to path, run the following command: ```bash sudo "/Applications/CMake.app/Contents/bin/cmake-gui" --install ``` - -- Set "Source Folder" to this project root. Then set "Build Folder" to a NEW _relative_ folder `/build` or `/out`. See image above. Then click "**Configure**", -- In the next screen, Choose **Unix Makefiles** on Linux & Mac. Choose **Visual Studio** on Windows. Then click OK. Click Configure once more, then **Generate**. -- Finally, open build folder, then run `make all` in Terminal (Linux and macOS). On Windows, double-click the generated VS project `.sln` file. +- Refer to the screenshot below: +![cmake_screenshot](cmake/cmake_gui_screenshot.png) + +- Set "Source Folder" (1) to this project root. Then set "Build Folder" (2) to a NEW _relative_ folder `/build` or `/out`. See image above. Then (3) click "**Configure**", +- In the next screen, Choose **Unix Makefiles** on Linux; choose **Xcode** on Mac. Choose **Visual Studio** on Windows. Then click OK to save. +- Make sure CMAKE_BUILD_TYPE is **Release**. +- Click Configure once again, then **Generate**. +- Finally, open build folder, then run `make all` in Terminal (Linux). On Windows and MacOS, click open project for building. \ No newline at end of file diff --git a/CMakeLists.txt b/CMakeLists.txt index 46afe20..feb5134 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -13,7 +13,7 @@ endif() # ==== UPDATE ME HERE =========== # Absolute path where you installed SFML (Required on Windows) -set(SFML_HOME "C:/SFML/SFML-2.6.1") +set(SFML_HOME "C:/SFML/SFML-2.6.1") # ========================== # Collect all sources diff --git a/README.md b/README.md index 0587c78..5567496 100644 --- a/README.md +++ b/README.md @@ -1,9 +1,9 @@ # space - checkers -Offline & Online Multiplayer Checkers game in C++ built with SFML 2.6, Protobuf and ixWebsockets. With very minimal dependencies +Offline & Online Multiplayer Checkers game in C++ built with SFML 2.6, imGui, Protobuf and ixWebsockets. With very minimal dependencies and a simple build process. All dependencies are auto-downloaded (as `.tar.gz`) and built for you using [CPM.cmake](https://github.com/cpm-cmake/CPM.cmake). -This game can connect to both Private and Public game servers. The Server project for this game is on a separate git repo, [available here](#). +This game can connect to both Private and Public game servers. The Server project for this game is on a separate git repo, [available here](#). The only dependency you need pre-installed on your OS is SFML 2.6.x (or newer). ### Main Libraries Used @@ -56,7 +56,7 @@ The only dependency you need pre-installed on your OS is SFML 2.6.x (or newer). ## Building Instructions -Please see [BUILDING.md](BUILDING.md) for detailed instructions. +Please see [BUILDING.md](BUILDING.md) for detailed instructions. For macOS-specific build guide with XCode, please refer to [instructions](cmake/macbundle.cmake) ### License diff --git a/cmake/cmake_gui_screenshot.png b/cmake/cmake_gui_screenshot.png new file mode 100644 index 0000000..0b1b3ee Binary files /dev/null and b/cmake/cmake_gui_screenshot.png differ diff --git a/src/WsClient.cpp b/src/WsClient.cpp new file mode 100644 index 0000000..0dac7c3 --- /dev/null +++ b/src/WsClient.cpp @@ -0,0 +1,475 @@ +#include "WsClient.hpp" + +using namespace chk; + +chk::WsClient::WsClient() +{ + ix::initNetSystem(); + // Our websocket object + this->webSocketPtr = std::make_unique(); + // set inital connection timeout + this->webSocketPtr->setHandshakeTimeout(10); + // once dead, DO NOT try reconnect + this->webSocketPtr->disableAutomaticReconnection(); + // ping server every 30 seconds + this->webSocketPtr->setPingInterval(30); + ix::SocketTLSOptions tlsOptions; +#ifndef _WIN32 + // Currently system CAs are not supported on non-Windows platforms with mbedtls + tlsOptions.caFile = "NONE"; +#endif // _WIN32 + this->webSocketPtr->setTLSOptions(tlsOptions); + // prefetch for public server list + this->asyncFetchPublicServers(); +} + +/** + * Show help tooltip with given message + * @param tip the help message + */ +void chk::WsClient::showHint(const char *tip) +{ + ImGui::TextDisabled("(?)"); + if (ImGui::BeginItemTooltip()) + { + ImGui::PushTextWrapPos(ImGui::GetFontSize() * 35.0f); + ImGui::TextUnformatted(tip); + ImGui::PopTextWrapPos(); + ImGui::EndTooltip(); + } +} + +/** + * Show the imgui connection window, for private server address input + */ +void chk::WsClient::showConnectWindow() +{ + static bool is_secure = false; // switch for enable/disable SSL (PRIVATE servers only) + static bool show_public = true; // whether to show public server list + + if (show_public) + { + this->showPublicServerWindow(show_public); + return; + } + + // =================== PRIVATE SERVERS =============================== + ImGui::SetNextWindowSize(ImVec2{300.0f, 300.0f}); + static char inputUrl[256] = "127.0.0.1:9876/game"; + if (ImGui::Begin("Private Server", nullptr, ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoCollapse)) + { + ImGui::InputText("Server IP", inputUrl, IM_ARRAYSIZE(inputUrl), ImGuiInputTextFlags_CharsNoBlank); + ImGui::SameLine(); + WsClient::showHint("eg: 127.0.0.1:8080 OR myserver.example.org"); + ImGui::Checkbox("Secure", &is_secure); + if (!std::string_view(inputUrl).empty() && ImGui::Button("Connect", ImVec2{100.0f, 0})) + { + const char *suffix = is_secure ? "wss://" : "ws://"; + this->final_address = suffix + std::string{inputUrl}; + this->connClicked = true; + memset(inputUrl, 0, sizeof(inputUrl)); + } + if (ImGui::Button("< Go Back", ImVec2{100.0f, 0})) + { + show_public = true; + } + ImGui::End(); + } +} + +/** + * Show list of PUBLIC game servers, using imgui ListBox + * @param showPublic switch for showing/hiding this window + */ +void WsClient::showPublicServerWindow(bool &showPublic) +{ + // =================== PUBLIC SERVERS =============================== + ImGui::SetNextWindowSize(ImVec2{300.0f, 300.0f}); + if (ImGui::Begin("Public Servers", nullptr, ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoCollapse)) + { + static int current_idx = 0; + if (ImGui::BeginListBox("Select One")) + { + for (int i = 0; i < publicServers.size(); ++i) + { + const bool selected = (i == current_idx); + if (ImGui::Selectable(publicServers.at(i).name.c_str(), selected)) + { + current_idx = i; + } + + // Set the initial focus + if (selected) + { + ImGui::SetItemDefaultFocus(); + } + } + ImGui::EndListBox(); + } + if (!publicServers.empty() && ImGui::Button("Connect", ImVec2{100.0f, 0})) + { + this->final_address = publicServers.at(current_idx).address; + this->connClicked = true; + } + publicServers.empty() ? ImGui::NewLine() : ImGui::SameLine(); + if (ImGui::Button("Refresh", ImVec2{90.0f, 0})) + { + this->asyncFetchPublicServers(); + } + if (ImGui::Button("My Private Server >", ImVec2{150.0f, 0})) + { + showPublic = false; + } + ImGui::End(); + } +} + +/** + * Fetch updated public servers from central cloud storage (Timeout 5000ms) + * @see libcpr official docs: https://docs.libcpr.org/advanced-usage.html + */ +void WsClient::asyncFetchPublicServers() +{ + cpr::GetCallback([this](cpr::Response r) { this->parseServerList(r); }, cpr::Url{chk::cloudfront}, + cpr::Timeout{5000}); +} + +/** + * Parse the JSON response of server list then display them + * @param response From the previous request + */ +void chk::WsClient::parseServerList(const cpr::Response &response) +{ + if (response.status_code != 200) + { + std::scoped_lock lg{this->mut}; + this->deathNote = "httpRequest error: " + response.error.message; + this->isDead = true; + return; + } + + // Parse the JSON response + simdjson::dom::parser jsonParser; + try + { + simdjson::dom::array jsonArray = jsonParser.parse(simdjson::padded_string_view(response.text)); + std::scoped_lock lg{this->mut}; + this->publicServers.clear(); + this->publicServers.reserve(jsonArray.size()); + for (const simdjson::dom::object &elem : jsonArray) + { + chk::ServerLocation location; + location.name = elem.at_key("name").get_c_str(); + location.address = elem.at_key("address").get_c_str(); + this->publicServers.emplace_back(std::move_if_noexcept(location)); + } + } + catch (const simdjson::simdjson_error &ex) + { + this->deathNote = ex.what(); + this->isDead = true; + } +} + +/** + * Reset all local states to FALSE or empty string + */ +void chk::WsClient::resetAllStates() +{ + this->isConnected = false; + this->connClicked = false; + this->isDead = false; + this->haveWinner = false; + this->deathNote.clear(); +} + +/** + * Run main loop of showing connection window, tryConnect, and handle exchanges + */ +void chk::WsClient::runMainLoop() +{ + + // clang-format off + if (!isConnected) { + if (!connClicked) { + this->showConnectWindow(); + } else { + this->tryConnect(final_address); + } + } + // already connected + else { + this->runServerLoop(); + } + + if (this->isDead) { + // some error happened 🙁 + if (this->_onDeathCallback != nullptr) { + _onDeathCallback(this->deathNote); + } + this->showErrorPopup(); + } else if (this->haveWinner) { + this->showWinnerPopup(); + } + // clang-format on +} + +/** + * Try to connect to Server + * @param address server IP or URI + */ +void chk::WsClient::tryConnect(std::string_view address) +{ + this->webSocketPtr->setUrl(address.data()); + if (!this->isConnected) + { + ImGui::SetNextWindowSize(ImVec2{400.0f, 100.0f}); + ImGui::Begin("Loading", nullptr, ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoCollapse); + ImGui::Text("Connecting to online server"); + ImGui::End(); + } + + // Setup a callback to be fired when an Async event is received + this->webSocketPtr->setOnMessageCallback([this](const ix::WebSocketMessagePtr &msg) { + if (msg->type == ix::WebSocketMessageType::Message) + { + std::scoped_lock lg{this->mut}; + this->msgBuffer.addItem(msg->str); + } + else if (msg->type == ix::WebSocketMessageType::Open) + { + spdlog::info("Connection established"); + this->isConnected = true; + } + else if (msg->type == ix::WebSocketMessageType::Close) + { + std::scoped_lock lg{this->mut}; + this->deathNote = "Error: disconnected from Server!" + msg->str; + spdlog::error(this->deathNote); + this->isDead = true; + } + else if (msg->type == ix::WebSocketMessageType::Error) + { + std::scoped_lock lg{this->mut}; + this->deathNote = "Connection error: " + msg->errorInfo.reason; + spdlog::error(this->deathNote); + this->isDead = true; + } + }); + + // Start our b/ground thread and receive messages (if websocket not dead) + if (!this->isDead) + { + this->webSocketPtr->start(); + } + + // Handle any connection error/timeout + if (this->webSocketPtr->getReadyState() != ix::ReadyState::Open && this->isDead) + { + this->webSocketPtr->stop(); + return; + } +} + +/** + * Set the callback to handle created pieces (from server) + * @param callback - the callback function + */ +void WsClient::setOnReadyConnectedCallback(const onConnectedServer &callback) +{ + this->_onReadyConnected = callback; +} + +/** + * Set the callback to handle starting game after signal from server + * @param callback the callback function + */ +void WsClient::setOnReadyStartGameCallback(const onReadyStartGame &callback) +{ + this->_onReadyStartGame = callback; +} + +/** + * Set the callback to handle connection failures or server kickouts + * @param callback the callback function + */ +void WsClient::setOnDeathCallback(const onDeathCallback &callback) +{ + this->_onDeathCallback = callback; +} + +/** + * Set the callback for handling Opponent moving their piece + * @param callback the callback function + */ +void WsClient::setOnMovePieceCallback(const onMovePieceCallback &callback) +{ + this->_onMovePieceCallback = callback; +} + +/** + * Set the callback for handling Opponent capturing my Piece + * @param callback the callback function + */ +void WsClient::setOnCapturePieceCallback(const onCaptureCallback &callback) +{ + this->_onCaptureCallback = callback; +} + +/** + * Set the callback for handling Winner or Loser of match + * @param callback the callback function + */ +void WsClient::setOnWinLoseCallback(const onWinLoseCallback &callback) +{ + this->_onWinLoseCallback = callback; +} + +/** + * Send Protobuf response back to server + * @param payload the request body + * @return TRUE if sent successfully, else FALSE + */ +bool WsClient::replyServer(const chk::payload::BasePayload &payload) const +{ + if (this->isDead || !this->isConnected) + { + return false; + } +#ifndef NDEBUG + spdlog::info("SENDING {}", payload.ShortDebugString()); +#endif // DEBUG + + payload.SerializeToString(&this->protoBucket); + const auto &result = this->webSocketPtr->sendBinary(this->protoBucket); + return result.success; +} + +/** + * Exchange messages with the server and update the game accordingly. if any error happen, close connection + */ +void chk::WsClient::runServerLoop() +{ + for (const auto &msg : this->msgBuffer.getAll()) + { + if (msg.empty()) + { + continue; + } + chk::payload::BasePayload basePayload; + if (!basePayload.ParseFromString(msg)) + { + std::scoped_lock lg(this->mut); + this->deathNote = "Profobuf: Could not parse payload"; + this->isDead = true; + return; + } + + if (basePayload.has_welcome()) + { + /* code */ + chk::payload::WelcomePayload welcome = basePayload.welcome(); + if (this->_onReadyConnected != nullptr) + { + this->_onReadyConnected(welcome, basePayload.notice()); + } + } + else if (basePayload.has_start()) + { + chk::payload::StartPayload startPayload = basePayload.start(); + if (this->_onReadyStartGame != nullptr) + { + this->_onReadyStartGame(startPayload, basePayload.notice()); + } + } + else if (basePayload.has_exit_payload()) + { + std::scoped_lock lg{this->mut}; + this->deathNote = basePayload.notice(); + this->isDead = true; + spdlog::error(basePayload.notice()); + } + else if (basePayload.has_move_payload()) + { + if (this->_onMovePieceCallback != nullptr) + { +#ifndef NDEBUG + spdlog::warn("RECIEVE {}", basePayload.ShortDebugString()); +#endif // DEBUG + this->_onMovePieceCallback(basePayload.move_payload()); + } + } + else if (basePayload.has_capture_payload()) + { + if (this->_onCaptureCallback != nullptr) + { +#ifndef NDEBUG + spdlog::warn("RECIEVE {}", basePayload.ShortDebugString()); +#endif // DEBUG + this->_onCaptureCallback(basePayload.capture_payload()); + } + } + else if (basePayload.has_winlose_payload()) + { + if (this->_onWinLoseCallback != nullptr) + { + this->_onWinLoseCallback(basePayload.notice()); + std::scoped_lock lg(this->mut); + this->deathNote = basePayload.notice(); + this->haveWinner = true; + } + } + } + std::scoped_lock lg{this->mut}; + this->msgBuffer.clean(); +} + +/** + * Show error message from websockets as a popup window + */ +void WsClient::showErrorPopup() +{ + if (this->deathNote.empty()) + { + return; + } + // Always center this next dialog + ImVec2 center = ImGui::GetMainViewport()->GetCenter(); + ImGui::SetNextWindowPos(center, ImGuiCond_Always, ImVec2{0.5f, 0.5f}); + ImGui::OpenPopup("Error", ImGuiPopupFlags_NoOpenOverExistingPopup); + if (ImGui::BeginPopupModal("Error", nullptr, ImGuiWindowFlags_AlwaysAutoResize)) + { + ImGui::Text("%s", this->deathNote.c_str()); + ImGui::Separator(); + if (ImGui::Button("OK", ImVec2{120.0f, 0})) + { + ImGui::CloseCurrentPopup(); + this->resetAllStates(); + this->webSocketPtr->stop(); + } + ImGui::EndPopup(); + } +} + +/** + * Show winner/loser popup window. + */ +void WsClient::showWinnerPopup() +{ + // Always center this next dialog + ImVec2 center = ImGui::GetMainViewport()->GetCenter(); + ImGui::SetNextWindowPos(center, ImGuiCond_Always, ImVec2{0.5f, 0.5f}); + ImGui::OpenPopup("GameOver", ImGuiPopupFlags_NoOpenOverExistingPopup); + if (ImGui::BeginPopupModal("GameOver", nullptr, ImGuiWindowFlags_AlwaysAutoResize)) + { + ImGui::Text("%s", this->deathNote.c_str()); + ImGui::Separator(); + if (ImGui::Button("OK", ImVec2{120.0f, 0})) + { + ImGui::CloseCurrentPopup(); + this->resetAllStates(); + this->webSocketPtr->stop(); + } + ImGui::EndPopup(); + } +} \ No newline at end of file diff --git a/src/WsClient.hpp b/src/WsClient.hpp index 162f05b..cb16e2d 100644 --- a/src/WsClient.hpp +++ b/src/WsClient.hpp @@ -29,7 +29,7 @@ using onMovePieceCallback = std::function; // when we got a winner or loser using onWinLoseCallback = std::function; -// CDN urL +// CDN address constexpr auto cloudfront = "https://d1txhef4jwuosv.cloudfront.net/ws_server_locations.json"; /** @@ -82,474 +82,4 @@ class WsClient final void resetAllStates(); }; -inline chk::WsClient::WsClient() -{ - ix::initNetSystem(); - // Our websocket object - this->webSocketPtr = std::make_unique(); - // set inital connection timeout - this->webSocketPtr->setHandshakeTimeout(10); - // once dead, DO NOT try reconnect - this->webSocketPtr->disableAutomaticReconnection(); - // ping server every 30 seconds - this->webSocketPtr->setPingInterval(30); - ix::SocketTLSOptions tlsOptions; -#ifndef _WIN32 - // Currently system CAs are not supported on non-Windows platforms with mbedtls - tlsOptions.caFile = "NONE"; -#endif // _WIN32 - this->webSocketPtr->setTLSOptions(tlsOptions); - // prefetch for public server list - this->asyncFetchPublicServers(); -} - -/** - * Show help tooltip with given message - * @param tip the help message - */ -inline void WsClient::showHint(const char *tip) -{ - ImGui::TextDisabled("(?)"); - if (ImGui::BeginItemTooltip()) - { - ImGui::PushTextWrapPos(ImGui::GetFontSize() * 35.0f); - ImGui::TextUnformatted(tip); - ImGui::PopTextWrapPos(); - ImGui::EndTooltip(); - } -} - -/** - * Show the imgui connection window, for private server address input - */ -inline void WsClient::showConnectWindow() -{ - static bool is_secure = false; // switch for enable/disable SSL (PRIVATE servers only) - static bool show_public = true; // whether to show public server list - - if (show_public) - { - this->showPublicServerWindow(show_public); - return; - } - - // =================== PRIVATE SERVERS =============================== - ImGui::SetNextWindowSize(ImVec2{300.0f, 300.0f}); - static char inputUrl[256] = "127.0.0.1:9876/game"; - if (ImGui::Begin("Private Server", nullptr, ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoCollapse)) - { - ImGui::InputText("Server IP", inputUrl, IM_ARRAYSIZE(inputUrl), ImGuiInputTextFlags_CharsNoBlank); - ImGui::SameLine(); - WsClient::showHint("eg: 127.0.0.1:8080 OR myserver.example.org"); - ImGui::Checkbox("Secure", &is_secure); - if (!std::string_view(inputUrl).empty() && ImGui::Button("Connect", ImVec2{100.0f, 0})) - { - const char *suffix = is_secure ? "wss://" : "ws://"; - this->final_address = suffix + std::string{inputUrl}; - this->connClicked = true; - memset(inputUrl, 0, sizeof(inputUrl)); - } - if (ImGui::Button("< Go Back", ImVec2{100.0f, 0})) - { - show_public = true; - } - ImGui::End(); - } -} - -/** - * Show list of PUBLIC game servers, using imgui ListBox - * @param showPublic switch for showing/hiding this window - */ -inline void WsClient::showPublicServerWindow(bool &showPublic) -{ - // =================== PUBLIC SERVERS =============================== - ImGui::SetNextWindowSize(ImVec2{300.0f, 300.0f}); - if (ImGui::Begin("Public Servers", nullptr, ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoCollapse)) - { - static int current_idx = 0; - if (ImGui::BeginListBox("Select One")) - { - for (int i = 0; i < publicServers.size(); ++i) - { - const bool selected = (i == current_idx); - if (ImGui::Selectable(publicServers.at(i).name.c_str(), selected)) - { - current_idx = i; - } - - // Set the initial focus - if (selected) - { - ImGui::SetItemDefaultFocus(); - } - } - ImGui::EndListBox(); - } - if (!publicServers.empty() && ImGui::Button("Connect", ImVec2{100.0f, 0})) - { - this->final_address = publicServers.at(current_idx).address; - this->connClicked = true; - } - publicServers.empty() ? ImGui::NewLine() : ImGui::SameLine(); - if (ImGui::Button("Refresh", ImVec2{90.0f, 0})) - { - this->asyncFetchPublicServers(); - } - if (ImGui::Button("My Private Server >", ImVec2{150.0f, 0})) - { - showPublic = false; - } - ImGui::End(); - } -} - -/** - * Fetch updated public servers from central cloud storage (Timeout 5000ms) - * @see libcpr official docs: https://docs.libcpr.org/advanced-usage.html - */ -inline void WsClient::asyncFetchPublicServers() -{ - cpr::GetCallback([this](cpr::Response r) { this->parseServerList(r); }, cpr::Url{chk::cloudfront}, - cpr::Timeout{5000}); -} - -/** - * Parse the JSON response of server list then display them - * @param response From the previous request - */ -inline void WsClient::parseServerList(const cpr::Response &response) -{ - if (response.status_code != 200) - { - std::scoped_lock lg{this->mut}; - this->deathNote = "httpRequest error: " + response.error.message; - this->isDead = true; - return; - } - - // Parse the JSON response - simdjson::dom::parser jsonParser; - try - { - simdjson::dom::array jsonArray = jsonParser.parse(simdjson::padded_string_view(response.text)); - std::scoped_lock lg{this->mut}; - this->publicServers.clear(); - this->publicServers.reserve(jsonArray.size()); - for (const simdjson::dom::object &elem : jsonArray) - { - chk::ServerLocation location; - location.name = elem.at_key("name").get_c_str(); - location.address = elem.at_key("address").get_c_str(); - this->publicServers.emplace_back(std::move_if_noexcept(location)); - } - } - catch (const simdjson::simdjson_error &ex) - { - this->deathNote = ex.what(); - this->isDead = true; - } -} - -/** - * Reset all local states to FALSE or empty string - */ -inline void WsClient::resetAllStates() -{ - this->isConnected = false; - this->connClicked = false; - this->isDead = false; - this->haveWinner = false; - this->deathNote.clear(); -} - -/** - * Run main loop of showing connection window, tryConnect, and handle exchanges - */ -inline void WsClient::runMainLoop() -{ - - // clang-format off - if (!isConnected) { - if (!connClicked) { - this->showConnectWindow(); - } else { - this->tryConnect(final_address); - } - } - // already connected - else { - this->runServerLoop(); - } - - if (this->isDead) { - // some error happened 🙁 - if (this->_onDeathCallback != nullptr) { - _onDeathCallback(this->deathNote); - } - this->showErrorPopup(); - } else if (this->haveWinner) { - this->showWinnerPopup(); - } - // clang-format on -} - -/** - * Try to connect to Server - * @param address server IP or URI - */ -inline void WsClient::tryConnect(std::string_view address) -{ - this->webSocketPtr->setUrl(address.data()); - if (!this->isConnected) - { - ImGui::SetNextWindowSize(ImVec2{400.0f, 100.0f}); - ImGui::Begin("Loading", nullptr, ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoCollapse); - ImGui::Text("Connecting to online server"); - ImGui::End(); - } - - // Setup a callback to be fired when an Async event is received - this->webSocketPtr->setOnMessageCallback([this](const ix::WebSocketMessagePtr &msg) { - if (msg->type == ix::WebSocketMessageType::Message) - { - std::scoped_lock lg{this->mut}; - this->msgBuffer.addItem(msg->str); - } - else if (msg->type == ix::WebSocketMessageType::Open) - { - spdlog::info("Connection established"); - this->isConnected = true; - } - else if (msg->type == ix::WebSocketMessageType::Close) - { - std::scoped_lock lg{this->mut}; - this->deathNote = "Error: disconnected from Server!" + msg->str; - spdlog::error(this->deathNote); - this->isDead = true; - } - else if (msg->type == ix::WebSocketMessageType::Error) - { - std::scoped_lock lg{this->mut}; - this->deathNote = "Connection error: " + msg->errorInfo.reason; - spdlog::error(this->deathNote); - this->isDead = true; - } - }); - - // Start our b/ground thread and receive messages (if websocket not dead) - if (!this->isDead) - { - this->webSocketPtr->start(); - } - - // Handle any connection error/timeout - if (this->webSocketPtr->getReadyState() != ix::ReadyState::Open && this->isDead) - { - this->webSocketPtr->stop(); - return; - } -} - -/** - * Set the callback to handle created pieces (from server) - * @param callback - the callback function - */ -inline void WsClient::setOnReadyConnectedCallback(const onConnectedServer &callback) -{ - this->_onReadyConnected = callback; -} - -/** - * Set the callback to handle starting game after signal from server - * @param callback the callback function - */ -inline void WsClient::setOnReadyStartGameCallback(const onReadyStartGame &callback) -{ - this->_onReadyStartGame = callback; -} - -/** - * Set the callback to handle connection failures or server kickouts - * @param callback the callback function - */ -inline void WsClient::setOnDeathCallback(const onDeathCallback &callback) -{ - this->_onDeathCallback = callback; -} - -/** - * Set the callback for handling Opponent moving their piece - * @param callback the callback function - */ -inline void WsClient::setOnMovePieceCallback(const onMovePieceCallback &callback) -{ - this->_onMovePieceCallback = callback; -} - -/** - * Set the callback for handling Opponent capturing my Piece - * @param callback the callback function - */ -inline void WsClient::setOnCapturePieceCallback(const onCaptureCallback &callback) -{ - this->_onCaptureCallback = callback; -} - -/** - * Set the callback for handling Winner or Loser of match - * @param callback the callback function - */ -inline void WsClient::setOnWinLoseCallback(const onWinLoseCallback &callback) -{ - this->_onWinLoseCallback = callback; -} - -/** - * Send Protobuf response back to server - * @param payload the request body - * @return TRUE if sent successfully, else FALSE - */ -inline bool WsClient::replyServer(const chk::payload::BasePayload &payload) const -{ - if (this->isDead || !this->isConnected) - { - return false; - } -#ifndef NDEBUG - spdlog::info("SENDING {}", payload.ShortDebugString()); -#endif // DEBUG - - payload.SerializeToString(&this->protoBucket); - const auto &result = this->webSocketPtr->sendBinary(this->protoBucket); - return result.success; -} - -/** - * Exchange messages with the server and update the game accordingly. if any error happen, close connection - */ -inline void WsClient::runServerLoop() -{ - for (const auto &msg : this->msgBuffer.getAll()) - { - if (msg.empty()) - { - continue; - } - chk::payload::BasePayload basePayload; - if (!basePayload.ParseFromString(msg)) - { - std::scoped_lock lg(this->mut); - this->deathNote = "Profobuf: Could not parse payload"; - this->isDead = true; - return; - } - - if (basePayload.has_welcome()) - { - /* code */ - chk::payload::WelcomePayload welcome = basePayload.welcome(); - if (this->_onReadyConnected != nullptr) - { - this->_onReadyConnected(welcome, basePayload.notice()); - } - } - else if (basePayload.has_start()) - { - chk::payload::StartPayload startPayload = basePayload.start(); - if (this->_onReadyStartGame != nullptr) - { - this->_onReadyStartGame(startPayload, basePayload.notice()); - } - } - else if (basePayload.has_exit_payload()) - { - std::scoped_lock lg{this->mut}; - this->deathNote = basePayload.notice(); - this->isDead = true; - spdlog::error(basePayload.notice()); - } - else if (basePayload.has_move_payload()) - { - if (this->_onMovePieceCallback != nullptr) - { - spdlog::warn("RECIEVE {}", basePayload.ShortDebugString()); - this->_onMovePieceCallback(basePayload.move_payload()); - } - } - else if (basePayload.has_capture_payload()) - { - if (this->_onCaptureCallback != nullptr) - { -#ifndef NDEBUG - spdlog::warn("RECIEVE {}", basePayload.ShortDebugString()); -#endif // DEBUG - this->_onCaptureCallback(basePayload.capture_payload()); - } - } - else if (basePayload.has_winlose_payload()) - { - if (this->_onWinLoseCallback != nullptr) - { - this->_onWinLoseCallback(basePayload.notice()); - std::scoped_lock lg(this->mut); - this->deathNote = basePayload.notice(); - this->haveWinner = true; - } - } - } - std::scoped_lock lg{this->mut}; - this->msgBuffer.clean(); -} - -/** - * Show error message from websockets as a popup window - */ -inline void WsClient::showErrorPopup() -{ - if (this->deathNote.empty()) - { - return; - } - // Always center this next dialog - ImVec2 center = ImGui::GetMainViewport()->GetCenter(); - ImGui::SetNextWindowPos(center, ImGuiCond_Always, ImVec2{0.5f, 0.5f}); - ImGui::OpenPopup("Error", ImGuiPopupFlags_NoOpenOverExistingPopup); - if (ImGui::BeginPopupModal("Error", nullptr, ImGuiWindowFlags_AlwaysAutoResize)) - { - ImGui::Text("%s", this->deathNote.c_str()); - ImGui::Separator(); - if (ImGui::Button("OK", ImVec2{120.0f, 0})) - { - ImGui::CloseCurrentPopup(); - this->resetAllStates(); - this->webSocketPtr->stop(); - } - ImGui::EndPopup(); - } -} - -/** - * Show winner/loser popup window. - */ -inline void WsClient::showWinnerPopup() -{ - // Always center this next dialog - ImVec2 center = ImGui::GetMainViewport()->GetCenter(); - ImGui::SetNextWindowPos(center, ImGuiCond_Always, ImVec2{0.5f, 0.5f}); - ImGui::OpenPopup("GameOver", ImGuiPopupFlags_NoOpenOverExistingPopup); - if (ImGui::BeginPopupModal("GameOver", nullptr, ImGuiWindowFlags_AlwaysAutoResize)) - { - ImGui::Text("%s", this->deathNote.c_str()); - ImGui::Separator(); - if (ImGui::Button("OK", ImVec2{120.0f, 0})) - { - ImGui::CloseCurrentPopup(); - this->resetAllStates(); - this->webSocketPtr->stop(); - } - ImGui::EndPopup(); - } -} - } // namespace chk