diff --git a/include/blitz/network/EnetServer.h b/include/blitz/network/EnetServer.h index 59dd194..b1751fd 100644 --- a/include/blitz/network/EnetServer.h +++ b/include/blitz/network/EnetServer.h @@ -17,6 +17,8 @@ class EnetServer : private NonCopyable { void Destroy(); + bool IsClosed() const; + void CloseConnection(std::uint16_t a_PeerId); EnetConnection* GetConnection(std::uint16_t a_PeerId); diff --git a/include/client/Client.h b/include/client/Client.h index f5fc634..6d13eaf 100644 --- a/include/client/Client.h +++ b/include/client/Client.h @@ -1,7 +1,6 @@ #pragma once #include -#include #include #include @@ -18,6 +17,9 @@ class Client : private NonCopyable { bool IsConnected(); + NazaraSignal(OnClientReady); + NazaraSignal(OnClientDisconnect); + private: EnttWorld m_World; std::unique_ptr m_NetworkClient; diff --git a/include/client/handlers/PlayerJoinHandler.h b/include/client/handlers/PlayerJoinHandler.h index 6995cc2..a0ed446 100644 --- a/include/client/handlers/PlayerJoinHandler.h +++ b/include/client/handlers/PlayerJoinHandler.h @@ -10,6 +10,8 @@ class PlayerJoinHandler : public protocol::PacketHandler { NazaraSlot(network::EnetConnection, OnPlayerJoin, m_Slot); + NazaraSignal(OnLocalPlayerReady); + private: void Handle(const protocol::data::PlayerJoin&); }; diff --git a/include/client/states/ConnectingState.h b/include/client/states/ConnectingState.h new file mode 100644 index 0000000..1cbf0eb --- /dev/null +++ b/include/client/states/ConnectingState.h @@ -0,0 +1,62 @@ +#pragma once + +#include +#include +#include +#include +#include +#include + +namespace blitz { + +namespace server { +class Server; +} // namespace server + +namespace client { + +class ConnectingState : public AbstractState { + public: + ConnectingState(std::shared_ptr a_StateData, std::shared_ptr a_PreviousState, + const std::string& a_Address, std::uint16_t a_Port, bool a_IntegratedServer = false); + ~ConnectingState(); + + private: + struct ResolvingData { + bool m_HasResult = false; + Nz::Result, std::string /*error*/> m_Result = Nz::Err(""); + std::string m_ServerName; + std::uint16_t m_Port; + }; + + ResolvingData m_ResolvingData; + bool m_IsConnected = false; + std::queue m_Addresses; + Nz::MillisecondClock m_TimeoutClock; + + Nz::ButtonWidget* m_BackButton; + Nz::LabelWidget* m_StatusText; + std::shared_ptr m_NextState; + std::shared_ptr m_PreviousState; + + std::unique_ptr m_Client; + std::unique_ptr m_Server; + + void LayoutWidgets() override; + bool Update(Nz::StateMachine& fsm, Nz::Time elapsedTime) override; + + void TryResolve(); + void TryConnect(const Nz::IpAddress& a_ServerAddress); + void TryNextAddress(); + + void OnBackPressed(); + + void OnConnect(); + void OnConnectFailed(); + + NazaraSlot(Client, OnClientReady, m_ClientReady); + NazaraSlot(Client, OnClientDisconnect, m_ClientDisconnect); +}; + +} // namespace client +} // namespace blitz diff --git a/include/client/states/CreateServerState.h b/include/client/states/CreateServerState.h index c73b73c..b2b7837 100644 --- a/include/client/states/CreateServerState.h +++ b/include/client/states/CreateServerState.h @@ -1,5 +1,6 @@ #pragma once +#include #include #include @@ -13,6 +14,7 @@ class CreateServerState : public AbstractState { private: Nz::ButtonWidget* m_CreateServerButton; + Nz::TextAreaWidget* m_InputPort; Nz::ButtonWidget* m_BackButton; std::shared_ptr m_NextState; std::shared_ptr m_PreviousState; diff --git a/include/client/states/GameState.h b/include/client/states/GameState.h new file mode 100644 index 0000000..35510a2 --- /dev/null +++ b/include/client/states/GameState.h @@ -0,0 +1,27 @@ +#pragma once + +#include +#include + +namespace blitz { + +namespace server { +class Server; +} + +namespace client { + +class Client; + +class GameState : public AbstractState { + public: + GameState(std::shared_ptr a_StateData, std::unique_ptr&& a_Client, std::unique_ptr&& a_Server); + ~GameState(); + + private: + std::unique_ptr m_Client; + std::unique_ptr m_Server; +}; + +} // namespace client +} // namespace blitz diff --git a/include/client/states/JoinServerState.h b/include/client/states/JoinServerState.h index 7d26599..a3b416a 100644 --- a/include/client/states/JoinServerState.h +++ b/include/client/states/JoinServerState.h @@ -1,6 +1,7 @@ #pragma once #include +#include #include namespace blitz { @@ -12,6 +13,7 @@ class JoinServerState : public AbstractState { ~JoinServerState(); private: + Nz::TextAreaWidget* m_InputAddress; Nz::ButtonWidget* m_JoinServerButton; Nz::ButtonWidget* m_BackButton; std::shared_ptr m_NextState; diff --git a/include/server/Server.h b/include/server/Server.h index 686d1d3..eda91b3 100644 --- a/include/server/Server.h +++ b/include/server/Server.h @@ -17,6 +17,8 @@ class Server { network::EnetConnection* GetConnection(std::uint16_t a_PeerId); + bool IsClosed() const; + void CloseConnection(std::uint16_t a_PeerId); void CloseServer(); diff --git a/include/server/systems/DisconnectSystem.h b/include/server/systems/DisconnectSystem.h index 908a222..cdb2572 100644 --- a/include/server/systems/DisconnectSystem.h +++ b/include/server/systems/DisconnectSystem.h @@ -8,9 +8,9 @@ namespace server { class Server; -class DisconectSystem { +class DisconnectSystem { public: - DisconectSystem(entt::registry&, EnttWorld& a_World, Server& a_Server); + DisconnectSystem(entt::registry&, EnttWorld& a_World, Server& a_Server); void Update(Nz::Time elapsedTime); diff --git a/src/blitz/network/EnetClient.cpp b/src/blitz/network/EnetClient.cpp index eda13ed..675c0f2 100644 --- a/src/blitz/network/EnetClient.cpp +++ b/src/blitz/network/EnetClient.cpp @@ -4,7 +4,7 @@ namespace blitz { namespace network { EnetClient::EnetClient(const Nz::IpAddress& address) : m_Running(true) { - m_Host.Create(Nz::IpAddress::LoopbackIpV4, 1); + m_Host.Create(address.GetProtocol() == Nz::NetProtocol::IPv4 ? Nz::IpAddress::LoopbackIpV4 : Nz::IpAddress::LoopbackIpV6, 1); m_Peer = m_Host.Connect(address); m_Thread = std::jthread(&EnetClient::WorkerThread, this); m_Connection.SetPeer(m_Peer); diff --git a/src/blitz/network/EnetServer.cpp b/src/blitz/network/EnetServer.cpp index 9caaf36..fef3daf 100644 --- a/src/blitz/network/EnetServer.cpp +++ b/src/blitz/network/EnetServer.cpp @@ -69,6 +69,8 @@ void EnetServer::Update() { break; } } while (m_Host.CheckEvents(&event)); + } else if (service < 0) { + m_Running = false; } } @@ -102,5 +104,9 @@ void EnetServer::Destroy() { m_Host.Destroy(); } +bool EnetServer::IsClosed() const { + return !m_Running; +} + } // namespace network } // namespace blitz diff --git a/src/client/Client.cpp b/src/client/Client.cpp index 6466fc6..3b0cfe8 100644 --- a/src/client/Client.cpp +++ b/src/client/Client.cpp @@ -18,9 +18,14 @@ Client::~Client() { } void Client::BindHandlers() { + m_NetworkClient->OnDisconnect.Connect([this]() { OnClientDisconnect(); }); + m_NetworkClient->OnDisconnectTimeout.Connect([this]() { OnClientDisconnect(); }); + m_Handlers.push_back(std::make_unique(m_NetworkClient->GetConnection(), m_World)); m_Handlers.push_back(std::make_unique(m_NetworkClient->GetConnection(), m_World)); - m_Handlers.push_back(std::make_unique(m_NetworkClient->GetConnection(), m_World)); + auto playerJoin = std::make_unique(m_NetworkClient->GetConnection(), m_World); + playerJoin->OnLocalPlayerReady.Connect([this]() { OnClientReady(); }); + m_Handlers.push_back(std::move(playerJoin)); m_Handlers.push_back(std::make_unique(m_NetworkClient->GetConnection(), m_World)); m_Handlers.push_back(std::make_unique(m_NetworkClient->GetConnection(), m_World)); } @@ -38,6 +43,7 @@ void Client::Connect(const Nz::IpAddress& a_Ip) { void Client::Disconnect() { if (!m_NetworkClient) return; + m_NetworkClient->GetConnection().SendDisconnect({}); m_NetworkClient->Disconnect(); UnbindHandlers(); m_NetworkClient.reset(nullptr); @@ -51,6 +57,7 @@ bool Client::IsConnected() { } void Client::Login() { + srand(time(0)); if (!m_NetworkClient || !m_NetworkClient->GetConnection().IsConnected()) return; m_NetworkClient->GetConnection().SendPlayerLogin({"Player_" + std::to_string(rand() % 100)}); diff --git a/src/client/handlers/PlayerJoinHandler.cpp b/src/client/handlers/PlayerJoinHandler.cpp index 8541c52..ae0ccea 100644 --- a/src/client/handlers/PlayerJoinHandler.cpp +++ b/src/client/handlers/PlayerJoinHandler.cpp @@ -27,6 +27,7 @@ void PlayerJoinHandler::Handle(const protocol::data::PlayerJoin& a_PlayerJoin) { world->GetRegistry().emplace(localPlayer, a_PlayerJoin.m_Player); // we are now into the game so we can begin to load the world for example + OnLocalPlayerReady(); } PlayerJoinHandler::~PlayerJoinHandler() {} diff --git a/src/client/states/ConnectingState.cpp b/src/client/states/ConnectingState.cpp new file mode 100644 index 0000000..56edaf8 --- /dev/null +++ b/src/client/states/ConnectingState.cpp @@ -0,0 +1,189 @@ +#include "client/states/GameState.h" +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace blitz { +namespace client { + +static const int ConnectTimeout = 5; + +ConnectingState::ConnectingState(std::shared_ptr a_StateData, std::shared_ptr a_PreviousState, + const std::string& a_Address, std::uint16_t a_Port, bool a_Server) : + AbstractState(std::move(a_StateData)), + m_PreviousState(std::move(a_PreviousState)), + m_Client(std::make_unique(*GetStateData().m_World)) { + m_ResolvingData.m_Port = a_Port; + m_ResolvingData.m_ServerName = a_Address; + + Nz::SimpleTextDrawer textDrawer; + textDrawer.SetTextColor({0.0, 0.0, 0.0, 1.0}); + textDrawer.SetCharacterSize(75); + m_BackButton = CreateWidget(); + textDrawer.SetText("Back"); + m_BackButton->UpdateText(textDrawer); + m_BackButton->Resize(m_BackButton->GetPreferredSize()); + m_BackButton->OnButtonTrigger.Connect([this](const Nz::ButtonWidget*) { OnBackPressed(); }); + + m_StatusText = CreateWidget(); + m_StatusText->UpdateText(Nz::SimpleTextDrawer::Draw("Connecting ...", 80)); + + if (a_Server) { + auto& app = GetStateData().m_App; + auto& ecs = app->GetComponent(); + + // create server + Nz::EnttWorld& world = ecs.AddWorld(); + m_Server = std::make_unique(a_Port, world); + } + + m_ClientReady.Connect(m_Client->OnClientReady, [this]() { OnConnect(); }); + m_ClientDisconnect.Connect(m_Client->OnClientDisconnect, [this]() { TryNextAddress(); }); + + TryResolve(); +} + +ConnectingState::~ConnectingState() {} + +bool ConnectingState::Update(Nz::StateMachine& fsm, Nz::Time elapsedTime) { + if (m_NextState) + fsm.ChangeState(std::move(m_NextState)); + + if (m_Server && m_Server->IsClosed()) + OnConnectFailed(); + + if (m_ResolvingData.m_HasResult) { + if (m_ResolvingData.m_Result) { + // Register resolved addresses as next addresses + const auto& addresses = m_ResolvingData.m_Result.GetValue(); + for (auto resultIt = addresses.rbegin(); resultIt != addresses.rend(); ++resultIt) + m_Addresses.emplace(*resultIt); + TryConnect(m_Addresses.front()); + } else { + OnConnectFailed(); + } + m_ResolvingData.m_HasResult = false; + } + + static Nz::MillisecondClock clock; + static int progress = -1; + + if (m_Client && clock.RestartIfOver(Nz::Time::Milliseconds(500))) { + progress++; + progress %= 4; + std::string textStr = "Connecting "; + for (int i = 0; i < progress; i++) { + textStr += "."; + } + m_StatusText->UpdateText(Nz::SimpleTextDrawer::Draw(textStr, 80)); + return true; + } + + if (!m_Addresses.empty() && m_TimeoutClock.RestartIfOver(Nz::Time::Seconds(ConnectTimeout))) { + TryNextAddress(); + } + + if (m_IsConnected) { + fsm.ResetState(std::make_shared(GetStateDataPtr(), std::move(m_Client), std::move(m_Server))); + } + + return true; +} + +void ConnectingState::TryConnect(const Nz::IpAddress& a_ServerAddress) { + if (!m_Client) + return; + LogD("Trying to connect to " + a_ServerAddress.ToString() + " ..."); + m_TimeoutClock.Restart(); + m_Client->Connect(a_ServerAddress); +} + +void ConnectingState::TryNextAddress() { + m_Client->Disconnect(); + m_Addresses.pop(); + if (m_Addresses.empty()) + OnConnectFailed(); + else + TryConnect(m_Addresses.front()); +} + +void ConnectingState::TryResolve() { + Nz::ResolveError resolveError; + auto serverAddresses = Nz::IpAddress::ResolveHostname( + Nz::NetProtocol::Any, m_ResolvingData.m_ServerName, std::to_string(m_ResolvingData.m_Port), &resolveError); + if (serverAddresses.empty()) { + m_ResolvingData.m_Result = Nz::Err(Nz::ErrorToString(resolveError)); + LogD("Failed to resolve " + m_ResolvingData.m_ServerName + " !"); + } else { + std::vector addresses; + addresses.reserve(serverAddresses.size()); + for (auto hostnameInfo : serverAddresses) { + hostnameInfo.address.SetPort(m_ResolvingData.m_Port); + addresses.push_back(hostnameInfo.address); + } + + m_ResolvingData.m_Result = std::move(addresses); + } + + m_ResolvingData.m_HasResult = true; +} + +void ConnectingState::LayoutWidgets() { + Nz::Vector2f canvasSize = GetStateData().m_Canvas->GetSize(); + Nz::Vector2f center = canvasSize / 2.f; + + constexpr float padding = 10.f; + + std::array widgets = {m_StatusText, m_BackButton}; + + float maxWidth = 0.f; + float totalSize = padding * (widgets.size() - 1); + for (Nz::BaseWidget* widget : widgets) { + Nz::Vector2f size = widget->GetSize(); + + maxWidth = std::max(maxWidth, size.x); + totalSize += size.y; + } + + Nz::Vector2f cursor = center; + cursor.y += totalSize / 2.f; + + for (Nz::BaseWidget* widget : widgets) { + widget->Resize({maxWidth, widget->GetHeight()}); + widget->SetPosition({0.f, cursor.y - widget->GetSize().y, 0.f}); + widget->CenterHorizontal(); + cursor.y -= widget->GetSize().y + padding; + } +} + +void ConnectingState::OnBackPressed() { + m_NextState = std::move(m_PreviousState); +} + +void ConnectingState::OnConnect() { + m_IsConnected = true; +} + +void ConnectingState::OnConnectFailed() { + m_StatusText->UpdateText(Nz::SimpleTextDrawer::Draw("Connection failed !", 80)); + m_StatusText->CenterHorizontal(); + m_Server.reset(nullptr); + m_Client.reset(nullptr); +} + +} // namespace client +} // namespace blitz diff --git a/src/client/states/CreateServerState.cpp b/src/client/states/CreateServerState.cpp index 56c5de6..8a7a749 100644 --- a/src/client/states/CreateServerState.cpp +++ b/src/client/states/CreateServerState.cpp @@ -1,7 +1,13 @@ +#include +#include +#include #include #include #include +#include +#include +#include namespace blitz { namespace client { @@ -18,6 +24,18 @@ CreateServerState::CreateServerState(std::shared_ptr a_StateData, std m_CreateServerButton->Resize(m_CreateServerButton->GetPreferredSize()); m_CreateServerButton->OnButtonTrigger.Connect([this](const Nz::ButtonWidget*) { OnCreateServerPressed(); }); + m_InputPort = CreateWidget(); + m_InputPort->SetBackgroundColor(Nz::Color::White()); + m_InputPort->EnableBackground(true); + m_InputPort->EnableMultiline(false); + m_InputPort->SetTextColor(Nz::Color::Black()); + m_InputPort->SetCharacterFilter([](Nz::UInt32 character) { + if (character < U'0' || character > U'9') + return false; + + return true; + }); + m_BackButton = CreateWidget(); textDrawer.SetText("Back"); m_BackButton->UpdateText(textDrawer); @@ -40,7 +58,7 @@ void CreateServerState::LayoutWidgets() { constexpr float padding = 10.f; - std::array widgets = {m_CreateServerButton, m_BackButton}; + std::array widgets = {m_InputPort, m_CreateServerButton, m_BackButton}; float maxWidth = 0.f; float totalSize = padding * (widgets.size() - 1); @@ -62,7 +80,24 @@ void CreateServerState::LayoutWidgets() { } } -void CreateServerState::OnCreateServerPressed() {} +void CreateServerState::OnCreateServerPressed() { + std::string serverPort = m_InputPort->GetText(); + if (serverPort.empty()) { + // UpdateStatus("Error: blank server port", Nz::Color::Red()); + return; + } + + long long rawPort = std::stoi(serverPort); + if (rawPort <= 0 || rawPort > 0xFFFF) { + // UpdateStatus("Error: " + serverPort + " is not a valid port", Nz::Color::Red()); + return; + } + + Nz::IpAddress address = Nz::IpAddress::LoopbackIpV4; + address.SetPort(rawPort); + + m_NextState = std::make_shared(GetStateDataPtr(), shared_from_this(), "localhost", rawPort, true); +} void CreateServerState::OnBackPressed() { m_NextState = std::move(m_PreviousState); diff --git a/src/client/states/GameState.cpp b/src/client/states/GameState.cpp new file mode 100644 index 0000000..a72ba27 --- /dev/null +++ b/src/client/states/GameState.cpp @@ -0,0 +1,16 @@ +#include + +#include +#include + +namespace blitz { +namespace client { + +GameState::GameState( + std::shared_ptr a_StateData, std::unique_ptr&& a_Client, std::unique_ptr&& a_Server) : + AbstractState(a_StateData), m_Client(std::move(a_Client)), m_Server(std::move(a_Server)) {} + +GameState::~GameState() {} + +} // namespace client +} // namespace blitz \ No newline at end of file diff --git a/src/client/states/JoinServerState.cpp b/src/client/states/JoinServerState.cpp index b111a40..52ec962 100644 --- a/src/client/states/JoinServerState.cpp +++ b/src/client/states/JoinServerState.cpp @@ -1,3 +1,6 @@ +#include +#include +#include #include #include @@ -12,6 +15,13 @@ JoinServerState::JoinServerState(std::shared_ptr a_StateData, std::sh textDrawer.SetTextColor({0.0, 0.0, 0.0, 1.0}); textDrawer.SetCharacterSize(75); + m_InputAddress = CreateWidget(); + m_InputAddress->SetBackgroundColor(Nz::Color::White()); + m_InputAddress->EnableBackground(true); + m_InputAddress->EnableMultiline(false); + m_InputAddress->SetTextColor(Nz::Color::Black()); + m_InputAddress->SetText("localhost"); + m_JoinServerButton = CreateWidget(); textDrawer.SetText("Join Server"); m_JoinServerButton->UpdateText(textDrawer); @@ -40,7 +50,7 @@ void JoinServerState::LayoutWidgets() { constexpr float padding = 10.f; - std::array widgets = {m_JoinServerButton, m_BackButton}; + std::array widgets = {m_InputAddress, m_JoinServerButton, m_BackButton}; float maxWidth = 0.f; float totalSize = padding * (widgets.size() - 1); @@ -62,7 +72,21 @@ void JoinServerState::LayoutWidgets() { } } -void JoinServerState::OnJoinServerPressed() {} +void JoinServerState::OnJoinServerPressed() { + std::string address = m_InputAddress->GetText(); + auto separator = address.find(':'); + std::string name = address.substr(0, separator); + std::uint16_t port = 25565; + if (separator != std::string::npos) { + try { + std::string rawPort = address.substr(separator + 1, std::string::npos); + port = std::stoi(rawPort); + } catch (std::exception& e) { + return; + } + } + m_NextState = std::make_shared(GetStateDataPtr(), shared_from_this(), name, port, false); +} void JoinServerState::OnBackPressed() { m_NextState = std::move(m_PreviousState); diff --git a/src/server/Server.cpp b/src/server/Server.cpp index 63eab96..75b4aec 100644 --- a/src/server/Server.cpp +++ b/src/server/Server.cpp @@ -13,8 +13,6 @@ namespace blitz { namespace server { -#define RegisterHandler(Handler) session.m_Handlers.push_back(std::make_unique(*session.m_Connection, m_World)) - Server::Server(std::uint16_t a_Port, Nz::EnttWorld& a_World) : m_World({a_World}), m_NetworkServer(a_Port) { RegisterSystems(); m_NetworkServer.OnClientConnect.Connect(this, &Server::HandleConnect); @@ -22,7 +20,14 @@ Server::Server(std::uint16_t a_Port, Nz::EnttWorld& a_World) : m_World({a_World} m_NetworkServer.OnClientDisconnectTimeout.Connect(this, &Server::HandleDisconnect); } -Server::~Server() {} +Server::~Server() { + CloseServer(); + + AtomicEnttWorld world = m_World; + world->RemoveSystem(); + world->RemoveSystem(); + world->RemoveSystem(); +} void Server::HandleConnect(network::EnetConnection& a_Connection) { Session newSession; @@ -50,7 +55,7 @@ void Server::CreateEntity(network::EnetConnection& a_Connection) { void Server::RegisterSystems() { AtomicEnttWorld world = m_World; world->AddSystem(m_World); - world->AddSystem(m_World, *this); + world->AddSystem(m_World, *this); world->AddSystem(m_World); auto counter = world->CreateEntity(); @@ -58,8 +63,8 @@ void Server::RegisterSystems() { } void Server::RegisterHandlers(Session& session) { - RegisterHandler(KeepAliveHandler); - RegisterHandler(PlayerLoginHandler); + session.m_Handlers.push_back(std::make_unique(*session.m_Connection, m_World)); + session.m_Handlers.push_back(std::make_unique(*session.m_Connection, m_World)); } network::EnetConnection* Server::GetConnection(std::uint16_t a_PeerId) { @@ -81,5 +86,9 @@ void Server::CloseServer() { } } +bool Server::IsClosed() const { + return m_NetworkServer.IsClosed(); +} + } // namespace server } // namespace blitz diff --git a/src/server/systems/DisconnectSystem.cpp b/src/server/systems/DisconnectSystem.cpp index f85e2be..2dc7b92 100644 --- a/src/server/systems/DisconnectSystem.cpp +++ b/src/server/systems/DisconnectSystem.cpp @@ -12,9 +12,9 @@ namespace blitz { namespace server { -DisconectSystem::DisconectSystem(entt::registry&, EnttWorld& a_World, Server& a_Server) : m_World(a_World), m_Server(a_Server) {} +DisconnectSystem::DisconnectSystem(entt::registry&, EnttWorld& a_World, Server& a_Server) : m_World(a_World), m_Server(a_Server) {} -void DisconectSystem::Update(Nz::Time elapsedTime) { +void DisconnectSystem::Update(Nz::Time elapsedTime) { AtomicEnttWorld world = m_World; entt::registry& registry = world->GetRegistry();