├── .gitignore ├── .gitmodules ├── App-Client ├── App-Client.aps ├── App-Client.rc ├── Build-App-Client.lua ├── Walnut-Icon.ico ├── imgui.ini ├── res │ └── Walnut-Icon.png ├── resource.h └── src │ ├── ClientApp.cpp │ ├── ClientLayer.cpp │ └── ClientLayer.h ├── App-Common ├── Build-App-Common-Headless.lua ├── Build-App-Common.lua └── Source │ ├── ServerPacket.cpp │ ├── ServerPacket.h │ ├── UserInfo.cpp │ └── UserInfo.h ├── App-Server ├── Build-App-Server-Headless.lua ├── Build-App-Server.lua ├── Source │ ├── HeadlessConsole.cpp │ ├── HeadlessConsole.h │ ├── ServerApp.cpp │ ├── ServerLayer.cpp │ └── ServerLayer.h └── imgui.ini ├── Build-Headless.lua ├── Build.lua ├── LICENSE.txt ├── README.md └── scripts ├── Setup.bat └── Setup.sh /.gitignore: -------------------------------------------------------------------------------- 1 | # Directories 2 | .vs/ 3 | bin/ 4 | bin-int/ 5 | 6 | # Files 7 | *.vcxproj 8 | *.vcxproj.user 9 | *.vcxproj.filters 10 | *.sln 11 | *.log 12 | 13 | # Exclude 14 | !vendor/bin -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "Walnut"] 2 | path = Walnut 3 | url = https://github.com/TheCherno/Walnut 4 | -------------------------------------------------------------------------------- /App-Client/App-Client.aps: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TheCherno/Walnut-Chat/2a32d51943becee8b8d497a1838dc830d71f65d5/App-Client/App-Client.aps -------------------------------------------------------------------------------- /App-Client/App-Client.rc: -------------------------------------------------------------------------------- 1 | // Microsoft Visual C++ generated resource script. 2 | // 3 | #include "resource.h" 4 | 5 | #define APSTUDIO_READONLY_SYMBOLS 6 | ///////////////////////////////////////////////////////////////////////////// 7 | // 8 | // Generated from the TEXTINCLUDE 2 resource. 9 | // 10 | #include "winres.h" 11 | 12 | ///////////////////////////////////////////////////////////////////////////// 13 | #undef APSTUDIO_READONLY_SYMBOLS 14 | 15 | ///////////////////////////////////////////////////////////////////////////// 16 | // English (United States) resources 17 | 18 | #if !defined(AFX_RESOURCE_DLL) || defined(AFX_TARG_ENU) 19 | LANGUAGE LANG_ENGLISH, SUBLANG_ENGLISH_US 20 | #pragma code_page(1252) 21 | 22 | #ifdef APSTUDIO_INVOKED 23 | ///////////////////////////////////////////////////////////////////////////// 24 | // 25 | // TEXTINCLUDE 26 | // 27 | 28 | 1 TEXTINCLUDE 29 | BEGIN 30 | "resource.h\0" 31 | END 32 | 33 | 2 TEXTINCLUDE 34 | BEGIN 35 | "#include ""winres.h""\r\n" 36 | "\0" 37 | END 38 | 39 | 3 TEXTINCLUDE 40 | BEGIN 41 | "\r\n" 42 | "\0" 43 | END 44 | 45 | #endif // APSTUDIO_INVOKED 46 | 47 | 48 | ///////////////////////////////////////////////////////////////////////////// 49 | // 50 | // Icon 51 | // 52 | 53 | // Icon with lowest ID value placed first to ensure application icon 54 | // remains consistent on all systems. 55 | MAINICON ICON "Walnut-Icon.ico" 56 | 57 | 58 | ///////////////////////////////////////////////////////////////////////////// 59 | // 60 | // Version 61 | // 62 | 63 | VS_VERSION_INFO VERSIONINFO 64 | FILEVERSION 1,2,0,0 65 | PRODUCTVERSION 1,2,0,0 66 | FILEFLAGSMASK 0x3fL 67 | #ifdef _DEBUG 68 | FILEFLAGS 0x1L 69 | #else 70 | FILEFLAGS 0x0L 71 | #endif 72 | FILEOS 0x4L 73 | FILETYPE 0x1L 74 | FILESUBTYPE 0x0L 75 | BEGIN 76 | BLOCK "StringFileInfo" 77 | BEGIN 78 | BLOCK "040904e4" 79 | BEGIN 80 | VALUE "CompanyName", "Studio Cherno" 81 | VALUE "FileDescription", "Walnut Chat Client" 82 | VALUE "FileVersion", "1.2.0.0" 83 | VALUE "LegalCopyright", "� Studio Cherno" 84 | VALUE "ProductName", "Walnut Chat Client" 85 | VALUE "ProductVersion", "1.2.0.0" 86 | END 87 | END 88 | BLOCK "VarFileInfo" 89 | BEGIN 90 | VALUE "Translation", 0x409, 1252 91 | END 92 | END 93 | 94 | #endif // English (United States) resources 95 | ///////////////////////////////////////////////////////////////////////////// 96 | 97 | 98 | 99 | #ifndef APSTUDIO_INVOKED 100 | ///////////////////////////////////////////////////////////////////////////// 101 | // 102 | // Generated from the TEXTINCLUDE 3 resource. 103 | // 104 | 105 | 106 | ///////////////////////////////////////////////////////////////////////////// 107 | #endif // not APSTUDIO_INVOKED 108 | 109 | -------------------------------------------------------------------------------- /App-Client/Build-App-Client.lua: -------------------------------------------------------------------------------- 1 | project "App-Client" 2 | kind "ConsoleApp" 3 | language "C++" 4 | cppdialect "C++20" 5 | targetdir "bin/%{cfg.buildcfg}" 6 | staticruntime "off" 7 | 8 | files { "src/**.h", "src/**.cpp" } 9 | 10 | includedirs 11 | { 12 | "../App-Common/Source", 13 | 14 | "../Walnut/vendor/imgui", 15 | "../Walnut/vendor/glfw/include", 16 | "../Walnut/vendor/glm", 17 | 18 | "../Walnut/Walnut/Source", 19 | "../Walnut/Walnut/Platform/GUI", 20 | 21 | "%{IncludeDir.VulkanSDK}", 22 | "../Walnut/vendor/spdlog/include", 23 | "../Walnut/vendor/yaml-cpp/include", 24 | 25 | -- Walnut-Networking 26 | "../Walnut/Walnut-Modules/Walnut-Networking/Source", 27 | "../Walnut/Walnut-Modules/Walnut-Networking/vendor/GameNetworkingSockets/include" 28 | } 29 | 30 | links 31 | { 32 | "App-Common", 33 | 34 | "yaml-cpp", 35 | } 36 | 37 | defines 38 | { 39 | "YAML_CPP_STATIC_DEFINE" 40 | } 41 | 42 | targetdir ("../bin/" .. outputdir .. "/%{prj.name}") 43 | objdir ("../bin-int/" .. outputdir .. "/%{prj.name}") 44 | 45 | filter "system:windows" 46 | systemversion "latest" 47 | defines { "WL_PLATFORM_WINDOWS" } 48 | 49 | postbuildcommands 50 | { 51 | '{COPY} "../%{WalnutNetworkingBinDir}/GameNetworkingSockets.dll" "%{cfg.targetdir}"', 52 | '{COPY} "../%{WalnutNetworkingBinDir}/libcrypto-3-x64.dll" "%{cfg.targetdir}"', 53 | '{COPY} "../%{WalnutNetworkingBinDir}/libprotobufd.dll" "%{cfg.targetdir}"', 54 | } 55 | 56 | filter "configurations:Debug" 57 | defines { "WL_DEBUG" } 58 | runtime "Debug" 59 | symbols "On" 60 | 61 | filter "configurations:Release" 62 | defines { "WL_RELEASE" } 63 | runtime "Release" 64 | optimize "On" 65 | symbols "On" 66 | 67 | filter "configurations:Dist" 68 | kind "WindowedApp" 69 | defines { "WL_DIST" } 70 | runtime "Release" 71 | optimize "On" 72 | symbols "Off" -------------------------------------------------------------------------------- /App-Client/Walnut-Icon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TheCherno/Walnut-Chat/2a32d51943becee8b8d497a1838dc830d71f65d5/App-Client/Walnut-Icon.ico -------------------------------------------------------------------------------- /App-Client/imgui.ini: -------------------------------------------------------------------------------- 1 | [Window][DockSpace Demo] 2 | Size=1600,900 3 | Collapsed=0 4 | 5 | [Window][Example: Console] 6 | Pos=0,26 7 | Size=1600,874 8 | Collapsed=0 9 | DockId=0xF2944C7F,0 10 | 11 | [Window][Debug##Default] 12 | Pos=1116,131 13 | Size=400,400 14 | Collapsed=0 15 | 16 | [Window][Connect to server] 17 | Pos=616,332 18 | Size=435,250 19 | Collapsed=0 20 | 21 | [Window][Chat] 22 | Pos=1,58 23 | Size=1338,927 24 | Collapsed=0 25 | DockId=0x00000001,0 26 | 27 | [Window][Users Online] 28 | Pos=1341,58 29 | Size=278,927 30 | Collapsed=0 31 | DockId=0x00000002,0 32 | 33 | [Window][DockSpaceWindow] 34 | Pos=0,0 35 | Size=1620,986 36 | Collapsed=0 37 | 38 | [Docking][Data] 39 | DockSpace ID=0xCDC34B94 Window=0x41896AB8 Pos=471,252 Size=1618,927 Split=X Selected=0x1D115A51 40 | DockNode ID=0x00000001 Parent=0xCDC34B94 SizeRef=1326,876 CentralNode=1 HiddenTabBar=1 Selected=0x1D115A51 41 | DockNode ID=0x00000002 Parent=0xCDC34B94 SizeRef=278,876 HiddenTabBar=1 Selected=0xE7D0EE20 42 | DockSpace ID=0xF2944C7F Pos=242,297 Size=1600,868 CentralNode=1 Selected=0x1D115A51 43 | 44 | -------------------------------------------------------------------------------- /App-Client/res/Walnut-Icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TheCherno/Walnut-Chat/2a32d51943becee8b8d497a1838dc830d71f65d5/App-Client/res/Walnut-Icon.png -------------------------------------------------------------------------------- /App-Client/resource.h: -------------------------------------------------------------------------------- 1 | //{{NO_DEPENDENCIES}} 2 | // Microsoft Visual C++ generated include file. 3 | // Used by App-Client.rc 4 | // 5 | 6 | // Next default values for new objects 7 | // 8 | #ifdef APSTUDIO_INVOKED 9 | #ifndef APSTUDIO_READONLY_SYMBOLS 10 | #define _APS_NEXT_RESOURCE_VALUE 102 11 | #define _APS_NEXT_COMMAND_VALUE 40001 12 | #define _APS_NEXT_CONTROL_VALUE 1001 13 | #define _APS_NEXT_SYMED_VALUE 101 14 | #endif 15 | #endif 16 | -------------------------------------------------------------------------------- /App-Client/src/ClientApp.cpp: -------------------------------------------------------------------------------- 1 | #include "Walnut/ApplicationGUI.h" 2 | #include "Walnut/EntryPoint.h" 3 | 4 | #include "ClientLayer.h" 5 | 6 | const static uint64_t s_BufferSize = 1024; 7 | static uint8_t* s_Buffer = new uint8_t[s_BufferSize]; 8 | 9 | Walnut::Application* Walnut::CreateApplication(int argc, char** argv) 10 | { 11 | Walnut::ApplicationSpecification spec; 12 | spec.Name = "Walnut Chat Client 1.2"; 13 | spec.IconPath = "res/Walnut-Icon.png"; 14 | spec.CustomTitlebar = true; 15 | spec.CenterWindow = true; 16 | 17 | Walnut::Application* app = new Walnut::Application(spec); 18 | std::shared_ptr clientLayer = std::make_shared(); 19 | app->PushLayer(clientLayer); 20 | app->SetMenubarCallback([app, clientLayer]() 21 | { 22 | if (ImGui::BeginMenu("File")) 23 | { 24 | if (ImGui::MenuItem("Disconnect")) 25 | clientLayer->OnDisconnectButton(); 26 | 27 | if (ImGui::MenuItem("Exit")) 28 | app->Close(); 29 | ImGui::EndMenu(); 30 | } 31 | }); 32 | return app; 33 | } -------------------------------------------------------------------------------- /App-Client/src/ClientLayer.cpp: -------------------------------------------------------------------------------- 1 | #include "ClientLayer.h" 2 | 3 | #include "ServerPacket.h" 4 | 5 | #include "Walnut/Application.h" 6 | #include "Walnut/UI/UI.h" 7 | #include "Walnut/Serialization/BufferStream.h" 8 | #include "Walnut/Networking/NetworkingUtils.h" 9 | #include "Walnut/Utils/StringUtils.h" 10 | 11 | #include "misc/cpp/imgui_stdlib.h" 12 | 13 | #include 14 | 15 | #include 16 | #include 17 | 18 | void ClientLayer::OnAttach() 19 | { 20 | m_ScratchBuffer.Allocate(1024); 21 | 22 | m_Client = std::make_unique(); 23 | m_Client->SetServerConnectedCallback([this]() { OnConnected(); }); 24 | m_Client->SetServerDisconnectedCallback([this]() { OnDisconnected(); }); 25 | m_Client->SetDataReceivedCallback([this](const Walnut::Buffer data) { OnDataReceived(data); }); 26 | 27 | m_Console.SetMessageSendCallback([this](std::string_view message) { SendChatMessage(message); }); 28 | 29 | LoadConnectionDetails(m_ConnectionDetailsFilePath); 30 | } 31 | 32 | void ClientLayer::OnDetach() 33 | { 34 | m_Client->Disconnect(); 35 | // ^ currently disconnect is blocking 36 | 37 | m_ScratchBuffer.Release(); 38 | } 39 | 40 | void ClientLayer::OnUIRender() 41 | { 42 | UI_ConnectionModal(); 43 | 44 | m_Console.OnUIRender(); 45 | UI_ClientList(); 46 | } 47 | 48 | bool ClientLayer::IsConnected() const 49 | { 50 | return m_Client->GetConnectionStatus() == Walnut::Client::ConnectionStatus::Connected; 51 | } 52 | 53 | void ClientLayer::OnDisconnectButton() 54 | { 55 | m_Client->Disconnect(); 56 | } 57 | 58 | void ClientLayer::UI_ConnectionModal() 59 | { 60 | if (!m_ConnectionModalOpen && m_Client->GetConnectionStatus() != Walnut::Client::ConnectionStatus::Connected) 61 | { 62 | ImGui::OpenPopup("Connect to server"); 63 | } 64 | 65 | m_ConnectionModalOpen = ImGui::BeginPopupModal("Connect to server", nullptr, ImGuiWindowFlags_AlwaysAutoResize); 66 | if (m_ConnectionModalOpen) 67 | { 68 | ImGui::Text("Your Name"); 69 | ImGui::InputText("##username", &m_Username); 70 | 71 | ImGui::Text("Pick a color"); 72 | ImGui::SameLine(); 73 | ImGui::ColorEdit4("##color", m_ColorBuffer); 74 | 75 | ImGui::Text("Server Address"); 76 | ImGui::InputText("##address", &m_ServerIP); 77 | ImGui::SameLine(); 78 | if (ImGui::Button("Connect")) 79 | { 80 | m_Color = IM_COL32(m_ColorBuffer[0] * 255.0f, m_ColorBuffer[1] * 255.0f, m_ColorBuffer[2] * 255.0f, m_ColorBuffer[3] * 255.0f); 81 | 82 | if (Walnut::Utils::IsValidIPAddress(m_ServerIP)) 83 | { 84 | m_Client->ConnectToServer(m_ServerIP); 85 | } 86 | else 87 | { 88 | // Try resolve domain name 89 | auto ipTokens = Walnut::Utils::SplitString(m_ServerIP, ':'); // [0] == hostname, [1] (optional) == port 90 | std::string serverIP = Walnut::Utils::ResolveDomainName(ipTokens[0]); 91 | if (ipTokens.size() != 2) 92 | serverIP = fmt::format("{}:{}", serverIP, 8192); // Add default port if hostname doesn't contain port 93 | else 94 | serverIP = fmt::format("{}:{}", serverIP, ipTokens[1]); // Add specified port 95 | 96 | m_Client->ConnectToServer(serverIP); 97 | } 98 | 99 | } 100 | 101 | if (Walnut::UI::ButtonCentered("Quit")) 102 | Walnut::Application::Get().Close(); 103 | 104 | if (m_Client->GetConnectionStatus() == Walnut::Client::ConnectionStatus::Connected) 105 | { 106 | // Send username 107 | Walnut::BufferStreamWriter stream(m_ScratchBuffer); 108 | stream.WriteRaw(PacketType::ClientConnectionRequest); 109 | stream.WriteRaw(m_Color); // Color 110 | stream.WriteString(m_Username); // Username 111 | 112 | m_Client->SendBuffer(stream.GetBuffer()); 113 | 114 | SaveConnectionDetails(m_ConnectionDetailsFilePath); 115 | 116 | // Wait for response 117 | ImGui::CloseCurrentPopup(); 118 | } 119 | else if (m_Client->GetConnectionStatus() == Walnut::Client::ConnectionStatus::FailedToConnect) 120 | { 121 | ImGui::TextColored(ImVec4(0.9f, 0.2f, 0.1f, 1.0f), "Connection failed."); 122 | const auto& debugMessage = m_Client->GetConnectionDebugMessage(); 123 | if (!debugMessage.empty()) 124 | ImGui::TextColored(ImVec4(0.9f, 0.2f, 0.1f, 1.0f), debugMessage.c_str()); 125 | } 126 | else if (m_Client->GetConnectionStatus() == Walnut::Client::ConnectionStatus::Connecting) 127 | { 128 | ImGui::TextColored(ImVec4(0.8f, 0.8f, 0.8f, 1.0f), "Connecting..."); 129 | } 130 | 131 | ImGui::EndPopup(); 132 | } 133 | } 134 | 135 | void ClientLayer::UI_ClientList() 136 | { 137 | ImGui::Begin("Users Online"); 138 | ImGui::Text("Online: %d", m_ConnectedClients.size()); 139 | 140 | static bool selected = false; 141 | for (const auto& [username, clientInfo] : m_ConnectedClients) 142 | { 143 | if (username.empty()) 144 | continue; 145 | 146 | ImGui::PushStyleColor(ImGuiCol_Text, ImColor(clientInfo.Color).Value); 147 | ImGui::Selectable(username.c_str(), &selected); 148 | ImGui::PopStyleColor(); 149 | } 150 | ImGui::End(); 151 | } 152 | 153 | void ClientLayer::OnConnected() 154 | { 155 | m_Console.ClearLog(); 156 | // Welcome message sent in PacketType::ClientConnectionRequest response handling 157 | } 158 | 159 | void ClientLayer::OnDisconnected() 160 | { 161 | m_Console.AddItalicMessageWithColor(0xff8a8a8a, "Lost connection to server!"); 162 | } 163 | 164 | void ClientLayer::OnDataReceived(const Walnut::Buffer buffer) 165 | { 166 | Walnut::BufferStreamReader stream(buffer); 167 | 168 | PacketType type; 169 | stream.ReadRaw(type); 170 | 171 | switch (type) 172 | { 173 | case PacketType::Message: 174 | { 175 | std::string fromUsername, message; 176 | stream.ReadString(fromUsername); 177 | stream.ReadString(message); 178 | 179 | // Find user 180 | if (m_ConnectedClients.contains(fromUsername)) 181 | { 182 | const auto& clientInfo = m_ConnectedClients.at(fromUsername); 183 | m_Console.AddTaggedMessageWithColor(clientInfo.Color, fromUsername, message); 184 | } 185 | else if (fromUsername == "SERVER") // special message from server 186 | { 187 | m_Console.AddTaggedMessage(fromUsername, message); 188 | } 189 | else 190 | { 191 | std::cout << "[ERROR] Message from unknown user? This shouldn't happen..." << std::endl; 192 | // display message anyway 193 | m_Console.AddTaggedMessage(fromUsername, message); 194 | } 195 | 196 | break; 197 | } 198 | case PacketType::ClientConnectionRequest: 199 | { 200 | bool requestStatus; 201 | stream.ReadRaw(requestStatus); 202 | if (requestStatus) 203 | { 204 | // Defer connection message to after message history is received 205 | m_ShowSuccessfulConnectionMessage = true; 206 | // m_Console.AddItalicMessageWithColor(0xff8a8a8a, "Successfully connected to {} with username {}", m_ServerIP, m_Username); 207 | } 208 | else 209 | { 210 | m_Console.AddItalicMessageWithColor(0xfffa4a4a, "Server rejected connection with username {}", m_Username); 211 | } 212 | break; 213 | } 214 | case PacketType::ConnectionStatus: 215 | break; 216 | case PacketType::ClientList: 217 | { 218 | std::vector clientList; 219 | stream.ReadArray(clientList); 220 | 221 | // Update our client list 222 | m_ConnectedClients.clear(); 223 | for (const auto& client : clientList) 224 | m_ConnectedClients[client.Username] = client; 225 | 226 | break; 227 | } 228 | case PacketType::ClientConnect: 229 | { 230 | UserInfo newClient; 231 | stream.ReadObject(newClient); 232 | 233 | m_ConnectedClients[newClient.Username] = newClient; 234 | m_Console.AddItalicMessageWithColor(newClient.Color, "Welcome {}!", newClient.Username); 235 | 236 | break; 237 | } 238 | case PacketType::ClientUpdate: 239 | break; 240 | case PacketType::ClientDisconnect: 241 | { 242 | UserInfo disconnectedClient; 243 | stream.ReadObject(disconnectedClient); 244 | 245 | m_ConnectedClients.erase(disconnectedClient.Username); 246 | m_Console.AddItalicMessageWithColor(disconnectedClient.Color, "Goodbye {}!", disconnectedClient.Username); 247 | break; 248 | } 249 | case PacketType::ClientUpdateResponse: 250 | break; 251 | case PacketType::MessageHistory: 252 | { 253 | std::vector messageHistory; 254 | stream.ReadArray(messageHistory); 255 | for (const auto& message : messageHistory) 256 | { 257 | // find user color if connected 258 | uint32_t userColor = 0xffffffff; 259 | if (m_ConnectedClients.contains(message.Username)) 260 | userColor = m_ConnectedClients.at(message.Username).Color; 261 | 262 | m_Console.AddTaggedMessageWithColor(userColor, message.Username, message.Message); 263 | } 264 | 265 | if (m_ShowSuccessfulConnectionMessage) 266 | { 267 | m_ShowSuccessfulConnectionMessage = false; 268 | m_Console.AddItalicMessageWithColor(0xff8a8a8a, "Successfully connected to {} with username {}", m_ServerIP, m_Username); 269 | } 270 | 271 | break; 272 | } 273 | case PacketType::ServerShutdown: 274 | { 275 | m_Console.AddItalicMessage("Server is shutting down... goodbye!"); 276 | m_Client->Disconnect(); 277 | break; 278 | } 279 | case PacketType::ClientKick: 280 | { 281 | m_Console.AddItalicMessage("You have been kicked by server!"); 282 | std::string reason; 283 | stream.ReadString(reason); 284 | if (!reason.empty()) 285 | m_Console.AddItalicMessage("Reason: {}", reason); 286 | 287 | m_Client->Disconnect(); 288 | break; 289 | } 290 | default: 291 | break; 292 | } 293 | } 294 | 295 | void ClientLayer::SendChatMessage(std::string_view message) 296 | { 297 | std::string messageToSend(message); 298 | if (IsValidMessage(messageToSend)) 299 | { 300 | Walnut::BufferStreamWriter stream(m_ScratchBuffer); 301 | stream.WriteRaw(PacketType::Message); 302 | stream.WriteString(messageToSend); 303 | m_Client->SendBuffer(stream.GetBuffer()); 304 | 305 | // echo in own console 306 | m_Console.AddTaggedMessageWithColor(m_Color | 0xff000000, m_Username, messageToSend); 307 | } 308 | } 309 | 310 | void ClientLayer::SaveConnectionDetails(const std::filesystem::path& filepath) 311 | { 312 | YAML::Emitter out; 313 | { 314 | out << YAML::BeginMap; // Root 315 | out << YAML::Key << "ConnectionDetails" << YAML::Value; 316 | 317 | out << YAML::BeginMap; 318 | out << YAML::Key << "Username" << YAML::Value << m_Username; 319 | out << YAML::Key << "Color" << YAML::Value << m_Color; 320 | out << YAML::Key << "ServerIP" << YAML::Value << m_ServerIP; 321 | out << YAML::EndMap; 322 | 323 | out << YAML::EndMap; // Root 324 | } 325 | 326 | std::ofstream fout(filepath); 327 | fout << out.c_str(); 328 | } 329 | 330 | bool ClientLayer::LoadConnectionDetails(const std::filesystem::path& filepath) 331 | { 332 | if (!std::filesystem::exists(filepath)) 333 | return false; 334 | 335 | YAML::Node data; 336 | try 337 | { 338 | data = YAML::LoadFile(filepath.string()); 339 | } 340 | catch (YAML::ParserException e) 341 | { 342 | std::cout << "[ERROR] Failed to load message history " << filepath << std::endl << e.what() << std::endl; 343 | return false; 344 | } 345 | 346 | auto rootNode = data["ConnectionDetails"]; 347 | if (!rootNode) 348 | return false; 349 | 350 | m_Username = rootNode["Username"].as(); 351 | 352 | m_Color = rootNode["Color"].as(); 353 | ImVec4 color = ImColor(m_Color).Value; 354 | m_ColorBuffer[0] = color.x; 355 | m_ColorBuffer[1] = color.y; 356 | m_ColorBuffer[2] = color.z; 357 | m_ColorBuffer[3] = color.w; 358 | 359 | m_ServerIP = rootNode["ServerIP"].as(); 360 | 361 | return true; 362 | } 363 | -------------------------------------------------------------------------------- /App-Client/src/ClientLayer.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "Walnut/Layer.h" 4 | #include "Walnut/Networking/Client.h" 5 | 6 | #include "Walnut/UI/Console.h" 7 | 8 | #include "UserInfo.h" 9 | 10 | #include 11 | #include 12 | 13 | class ClientLayer : public Walnut::Layer 14 | { 15 | public: 16 | virtual void OnAttach() override; 17 | virtual void OnDetach() override; 18 | virtual void OnUIRender() override; 19 | 20 | bool IsConnected() const; 21 | void OnDisconnectButton(); 22 | private: 23 | // UI 24 | void UI_ConnectionModal(); 25 | void UI_ClientList(); 26 | 27 | // Server event callbacks 28 | void OnConnected(); 29 | void OnDisconnected(); 30 | void OnDataReceived(const Walnut::Buffer buffer); 31 | 32 | void SendChatMessage(std::string_view message); 33 | 34 | private: 35 | void SaveConnectionDetails(const std::filesystem::path& filepath); 36 | bool LoadConnectionDetails(const std::filesystem::path& filepath); 37 | private: 38 | std::unique_ptr m_Client; 39 | Walnut::UI::Console m_Console{ "Chat" }; 40 | std::string m_ServerIP; 41 | std::filesystem::path m_ConnectionDetailsFilePath = "ConnectionDetails.yaml"; 42 | 43 | Walnut::Buffer m_ScratchBuffer; 44 | 45 | float m_ColorBuffer[4] = { 1.0f, 1.0f, 1.0f, 1.0f }; 46 | 47 | std::string m_Username; 48 | uint32_t m_Color = 0xffffffff; 49 | 50 | std::map m_ConnectedClients; 51 | bool m_ConnectionModalOpen = false; 52 | bool m_ShowSuccessfulConnectionMessage = false; 53 | }; -------------------------------------------------------------------------------- /App-Common/Build-App-Common-Headless.lua: -------------------------------------------------------------------------------- 1 | project "App-Common-Headless" 2 | kind "StaticLib" 3 | language "C++" 4 | cppdialect "C++20" 5 | targetdir "bin/%{cfg.buildcfg}" 6 | staticruntime "off" 7 | 8 | files { "Source/**.h", "Source/**.cpp" } 9 | 10 | includedirs 11 | { 12 | "../Walnut/vendor/glm", 13 | 14 | "../Walnut/Walnut/Source", 15 | "../Walnut-Networking/Source", 16 | 17 | "../Walnut/vendor/spdlog/include", 18 | 19 | "../vendor/GameNetworkingSockets/include" 20 | } 21 | 22 | links 23 | { 24 | "Walnut-Headless", 25 | "Walnut-Networking", 26 | } 27 | 28 | targetdir ("../bin/" .. outputdir .. "/%{prj.name}") 29 | objdir ("../bin-int/" .. outputdir .. "/%{prj.name}") 30 | 31 | filter "system:windows" 32 | systemversion "latest" 33 | defines { "WL_PLATFORM_WINDOWS" } 34 | 35 | filter "configurations:Debug" 36 | defines { "WL_DEBUG" } 37 | runtime "Debug" 38 | symbols "On" 39 | 40 | filter "configurations:Release" 41 | defines { "WL_RELEASE" } 42 | runtime "Release" 43 | optimize "On" 44 | symbols "On" 45 | 46 | filter "configurations:Dist" 47 | defines { "WL_DIST" } 48 | runtime "Release" 49 | optimize "On" 50 | symbols "Off" -------------------------------------------------------------------------------- /App-Common/Build-App-Common.lua: -------------------------------------------------------------------------------- 1 | project "App-Common" 2 | kind "StaticLib" 3 | language "C++" 4 | cppdialect "C++20" 5 | targetdir "bin/%{cfg.buildcfg}" 6 | staticruntime "off" 7 | 8 | files { "Source/**.h", "Source/**.cpp" } 9 | 10 | includedirs 11 | { 12 | "../Walnut/vendor/imgui", 13 | "../Walnut/vendor/glfw/include", 14 | "../Walnut/vendor/glm", 15 | 16 | "../Walnut/Walnut/Source", 17 | "../Walnut-Networking/Source", 18 | 19 | "%{IncludeDir.VulkanSDK}", 20 | "../Walnut/vendor/spdlog/include", 21 | 22 | "../Walnut-Networking/vendor/GameNetworkingSockets/include" 23 | } 24 | 25 | links 26 | { 27 | "Walnut", 28 | "Walnut-Networking", 29 | } 30 | 31 | targetdir ("../bin/" .. outputdir .. "/%{prj.name}") 32 | objdir ("../bin-int/" .. outputdir .. "/%{prj.name}") 33 | 34 | filter "system:windows" 35 | systemversion "latest" 36 | defines { "WL_PLATFORM_WINDOWS" } 37 | 38 | filter "configurations:Debug" 39 | defines { "WL_DEBUG" } 40 | runtime "Debug" 41 | symbols "On" 42 | 43 | filter "configurations:Release" 44 | defines { "WL_RELEASE" } 45 | runtime "Release" 46 | optimize "On" 47 | symbols "On" 48 | 49 | filter "configurations:Dist" 50 | defines { "WL_DIST" } 51 | runtime "Release" 52 | optimize "On" 53 | symbols "Off" -------------------------------------------------------------------------------- /App-Common/Source/ServerPacket.cpp: -------------------------------------------------------------------------------- 1 | #include "ServerPacket.h" 2 | 3 | std::string_view PacketTypeToString(PacketType type) 4 | { 5 | switch (type) 6 | { 7 | case PacketType::None: return "PacketType::None"; 8 | case PacketType::Message: return "PacketType::Message"; 9 | case PacketType::ClientConnectionRequest: return "PacketType::ClientConnectionRequest"; 10 | case PacketType::ConnectionStatus: return "PacketType::ConnectionStatus"; 11 | case PacketType::ClientList: return "PacketType::ClientList"; 12 | case PacketType::ClientConnect: return "PacketType::ClientConnect"; 13 | case PacketType::ClientUpdate: return "PacketType::ClientUpdate"; 14 | case PacketType::ClientDisconnect: return "PacketType::ClientDisconnect"; 15 | case PacketType::ClientUpdateResponse: return "PacketType::ClientUpdateResponse"; 16 | case PacketType::MessageHistory: return "PacketType::MessageHistory"; 17 | case PacketType::ServerShutdown: return "PacketType::ServerShutdown"; 18 | case PacketType::ClientKick: return "PacketType::ClientKick"; 19 | 20 | default: return "PacketType::"; 21 | } 22 | 23 | return "PacketType::"; 24 | } -------------------------------------------------------------------------------- /App-Common/Source/ServerPacket.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | 6 | /////////////////////////////////////////////////////////////////////////////////////////// 7 | // Common "protocol" for server<->client communication for this example chat application // 8 | /////////////////////////////////////////////////////////////////////////////////////////// 9 | 10 | enum class PacketType : uint16_t 11 | { 12 | // 13 | // Invalid packet 14 | // 15 | None = 0, 16 | 17 | // 18 | // -- Message -- 19 | // 20 | // [Server->Client] 21 | // 1. Username - UTF-8 serialized as per Hazel 22 | // 2. Message - UTF-8 string serialized as per Hazel 23 | // [Client->Server] 24 | // 1. Message - buffer of UTF-8 chars 25 | Message = 1, 26 | 27 | // 28 | // -- ClientConnectionRequest -- 29 | // 30 | // [Client->Server] 31 | // 1. 32-bit int with requested user color (RGB, most significant 8 bits ignored) 32 | // 2. Hazel serialized UTF-8 string with requested username 33 | // [Server->Client] 34 | // boolean response indicating acceptance of requested username 35 | ClientConnectionRequest = 2, 36 | 37 | // 38 | // -- ConnectionStatus -- 39 | // 40 | // [Server->Client] 41 | // idk 42 | ConnectionStatus = 3, 43 | 44 | // 45 | // -- ClientList -- 46 | // 47 | // [Server->Client] 48 | // Contains serialized std::vector (as per Hazel serialization) of ClientInfo structs (color + username) 49 | // This is received by client on connection, and at a set interval (10s default) 50 | ClientList = 4, 51 | 52 | // 53 | // -- ClientConnect -- 54 | // 55 | // [Server->Client] 56 | // Indicates connection of new other client 57 | // 1. Requested user color (32-bit int RGB, most significant 8 bits ignored) 58 | // 2. Requested username (Hazel serialized UTF-8 string) 59 | ClientConnect = 5, 60 | 61 | // 62 | // -- ClientUpdate -- 63 | // 64 | // [Server->Client] 65 | // 1. Existing username (Hazel serialized UTF-8 string) 66 | // 2. New color for user (32-bit int, RGB) 67 | // 3. New username (Hazel serialized UTF-8 string) 68 | // [Client->Server] 69 | // 1. Requested new color (32-bit int, RGB) 70 | // 2. Requested new username (Hazel serialized UTF-8 string) 71 | ClientUpdate = 6, 72 | 73 | // 74 | // -- ClientDisconnect -- 75 | // 76 | // [Server->Client] 77 | // Indicates disconnection of existing other client 78 | // 1. Hazel serialized UTF-8 string with username 79 | // [Client->Server] 80 | // Disconnection request from client 81 | // 1. [No data] 82 | ClientDisconnect = 7, 83 | 84 | // 85 | // -- ClientUpdateResponse -- 86 | // 87 | // [Server->Client] 88 | // 1. Boolean response for requested color 89 | // 2. Boolean response for requested username 90 | ClientUpdateResponse = 8, 91 | 92 | // 93 | // -- MessageHistory -- 94 | // 95 | // [Server->Client] 96 | // Server chat history - big boi 97 | // 1. A vector of ChatMessage in order of send time 98 | MessageHistory = 9, 99 | 100 | // 101 | // -- ServerShutdown -- 102 | // 103 | // [Server->Client] 104 | // Server is shutting down 105 | // 106 | ServerShutdown = 10, 107 | 108 | // 109 | // -- ClientKick -- 110 | // 111 | // [Server->Client] 112 | // User has been kicked from server 113 | // 1. String reason, could be empty string 114 | ClientKick = 11, 115 | }; 116 | 117 | std::string_view PacketTypeToString(PacketType type); 118 | 119 | -------------------------------------------------------------------------------- /App-Common/Source/UserInfo.cpp: -------------------------------------------------------------------------------- 1 | #include "UserInfo.h" 2 | 3 | bool IsValidMessage(std::string& message) 4 | { 5 | if (message.empty()) 6 | return false; 7 | 8 | // Only white-space 9 | if (message.find_first_not_of(" \t\n\v\f\r") == std::string::npos) 10 | return false; 11 | 12 | // Trim if exceeds max message length 13 | if (message.size() > MaxMessageLength) 14 | { 15 | message = message.substr(0, MaxMessageLength); 16 | return true; 17 | } 18 | 19 | return true; 20 | } -------------------------------------------------------------------------------- /App-Common/Source/UserInfo.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "Walnut/Serialization/StreamReader.h" 4 | #include "Walnut/Serialization/StreamWriter.h" 5 | 6 | struct UserInfo 7 | { 8 | uint32_t Color; 9 | std::string Username; 10 | 11 | static void Serialize(Walnut::StreamWriter* serializer, const UserInfo& instance) 12 | { 13 | serializer->WriteRaw(instance.Color); 14 | serializer->WriteString(instance.Username); 15 | } 16 | 17 | static void Deserialize(Walnut::StreamReader* deserializer, UserInfo& instance) 18 | { 19 | deserializer->ReadRaw(instance.Color); 20 | deserializer->ReadString(instance.Username); 21 | } 22 | }; 23 | 24 | struct ChatMessage 25 | { 26 | std::string Username; 27 | std::string Message; 28 | 29 | ChatMessage() = default; 30 | 31 | ChatMessage(const std::string& username, const std::string& message) 32 | : Username(username), Message(message) {} 33 | 34 | static void Serialize(Walnut::StreamWriter* serializer, const ChatMessage& instance) 35 | { 36 | serializer->WriteString(instance.Username); 37 | serializer->WriteString(instance.Message); 38 | } 39 | 40 | static void Deserialize(Walnut::StreamReader* deserializer, ChatMessage& instance) 41 | { 42 | deserializer->ReadString(instance.Username); 43 | deserializer->ReadString(instance.Message); 44 | } 45 | }; 46 | 47 | const int MaxMessageLength = 4096; 48 | bool IsValidMessage(std::string& message); 49 | -------------------------------------------------------------------------------- /App-Server/Build-App-Server-Headless.lua: -------------------------------------------------------------------------------- 1 | project "App-Server-Headless" 2 | kind "ConsoleApp" 3 | language "C++" 4 | cppdialect "C++20" 5 | targetdir "bin/%{cfg.buildcfg}" 6 | staticruntime "off" 7 | 8 | files { "Source/**.h", "Source/**.cpp" } 9 | 10 | includedirs 11 | { 12 | "../App-Common/Source", 13 | 14 | "../Walnut/vendor/glm", 15 | 16 | "../Walnut/Walnut/Source", 17 | "../Walnut/Walnut/Platform/Headless", 18 | 19 | "../Walnut/vendor/spdlog/include", 20 | "../Walnut/vendor/yaml-cpp/include", 21 | 22 | -- Walnut-Networking 23 | "../Walnut/Walnut-Modules/Walnut-Networking/Source", 24 | "../Walnut/Walnut-Modules/Walnut-Networking/vendor/GameNetworkingSockets/include" 25 | 26 | } 27 | 28 | links 29 | { 30 | "App-Common-Headless", 31 | "Walnut-Headless", 32 | "Walnut-Networking", 33 | 34 | "yaml-cpp", 35 | } 36 | 37 | defines 38 | { 39 | "YAML_CPP_STATIC_DEFINE" 40 | } 41 | 42 | targetdir ("../bin/" .. outputdir .. "/%{prj.name}") 43 | objdir ("../bin-int/" .. outputdir .. "/%{prj.name}") 44 | 45 | filter "system:windows" 46 | systemversion "latest" 47 | defines { "WL_PLATFORM_WINDOWS" } 48 | 49 | postbuildcommands 50 | { 51 | '{COPY} "../%{WalnutNetworkingBinDir}/GameNetworkingSockets.dll" "%{cfg.targetdir}"', 52 | '{COPY} "../%{WalnutNetworkingBinDir}/libcrypto-3-x64.dll" "%{cfg.targetdir}"', 53 | '{COPY} "../%{WalnutNetworkingBinDir}/libprotobufd.dll" "%{cfg.targetdir}"', 54 | } 55 | 56 | filter "system:linux" 57 | libdirs { "../Walnut/Walnut-Networking/vendor/GameNetworkingSockets/bin/Linux" } 58 | links { "GameNetworkingSockets" } 59 | 60 | defines { "WL_HEADLESS" } 61 | 62 | filter "configurations:Debug" 63 | defines { "WL_DEBUG" } 64 | runtime "Debug" 65 | symbols "On" 66 | 67 | filter "configurations:Release" 68 | defines { "WL_RELEASE" } 69 | runtime "Release" 70 | optimize "On" 71 | symbols "On" 72 | 73 | filter "configurations:Dist" 74 | defines { "WL_DIST" } 75 | runtime "Release" 76 | optimize "On" 77 | symbols "Off" -------------------------------------------------------------------------------- /App-Server/Build-App-Server.lua: -------------------------------------------------------------------------------- 1 | project "App-Server" 2 | kind "ConsoleApp" 3 | language "C++" 4 | cppdialect "C++20" 5 | targetdir "bin/%{cfg.buildcfg}" 6 | staticruntime "off" 7 | 8 | files { "Source/**.h", "Source/**.cpp" } 9 | 10 | includedirs 11 | { 12 | "../App-Common/Source", 13 | 14 | "../Walnut/vendor/imgui", 15 | "../Walnut/vendor/glfw/include", 16 | "../Walnut/vendor/glm", 17 | 18 | "../Walnut/Walnut/Source", 19 | "../Walnut/Walnut/Platform/GUI", 20 | 21 | "%{IncludeDir.VulkanSDK}", 22 | "../Walnut/vendor/spdlog/include", 23 | "../Walnut/vendor/yaml-cpp/include", 24 | 25 | -- Walnut-Networking 26 | "../Walnut/Walnut-Modules/Walnut-Networking/Source", 27 | "../Walnut/Walnut-Modules/Walnut-Networking/vendor/GameNetworkingSockets/include" 28 | } 29 | 30 | links 31 | { 32 | "App-Common", 33 | 34 | "yaml-cpp", 35 | } 36 | 37 | defines 38 | { 39 | "YAML_CPP_STATIC_DEFINE" 40 | } 41 | 42 | targetdir ("../bin/" .. outputdir .. "/%{prj.name}") 43 | objdir ("../bin-int/" .. outputdir .. "/%{prj.name}") 44 | 45 | filter "system:windows" 46 | systemversion "latest" 47 | defines { "WL_PLATFORM_WINDOWS" } 48 | 49 | postbuildcommands 50 | { 51 | '{COPY} "../%{WalnutNetworkingBinDir}/GameNetworkingSockets.dll" "%{cfg.targetdir}"', 52 | '{COPY} "../%{WalnutNetworkingBinDir}/libcrypto-3-x64.dll" "%{cfg.targetdir}"', 53 | '{COPY} "../%{WalnutNetworkingBinDir}/libprotobufd.dll" "%{cfg.targetdir}"', 54 | } 55 | 56 | filter "configurations:Debug" 57 | defines { "WL_DEBUG" } 58 | runtime "Debug" 59 | symbols "On" 60 | 61 | filter "configurations:Release" 62 | defines { "WL_RELEASE" } 63 | runtime "Release" 64 | optimize "On" 65 | symbols "On" 66 | 67 | filter "configurations:Dist" 68 | kind "WindowedApp" 69 | defines { "WL_DIST" } 70 | runtime "Release" 71 | optimize "On" 72 | symbols "Off" -------------------------------------------------------------------------------- /App-Server/Source/HeadlessConsole.cpp: -------------------------------------------------------------------------------- 1 | #include "HeadlessConsole.h" 2 | 3 | HeadlessConsole::HeadlessConsole(std::string_view title) 4 | : m_Title(title) 5 | { 6 | // NOTE(Yan): to run in background on Linux server you'll need to comment out 7 | // the following line, since we can't std::getline with no terminal 8 | m_InputThread = std::thread([this]() { InputThreadFunc(); }); 9 | } 10 | 11 | HeadlessConsole::~HeadlessConsole() 12 | { 13 | m_InputThreadRunning = false; 14 | if (m_InputThread.joinable()) 15 | m_InputThread.join(); 16 | } 17 | 18 | void HeadlessConsole::ClearLog() 19 | { 20 | m_MessageHistory.clear(); 21 | } 22 | 23 | void HeadlessConsole::SetMessageSendCallback(const MessageSendCallback& callback) 24 | { 25 | m_MessageSendCallback = callback; 26 | } 27 | 28 | void HeadlessConsole::InputThreadFunc() 29 | { 30 | m_InputThreadRunning = true; 31 | while (m_InputThreadRunning) 32 | { 33 | std::string line; 34 | std::getline(std::cin, line); 35 | m_MessageSendCallback(line); 36 | } 37 | 38 | } 39 | -------------------------------------------------------------------------------- /App-Server/Source/HeadlessConsole.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | 9 | #include "spdlog/spdlog.h" 10 | 11 | // 12 | // HeadlessConsole - similar to Walnut::UI::Console but for non-GUI builds 13 | // 14 | class HeadlessConsole 15 | { 16 | public: 17 | using MessageSendCallback = std::function; 18 | public: 19 | HeadlessConsole(std::string_view title = "Walnut Console"); 20 | ~HeadlessConsole(); 21 | 22 | void ClearLog(); 23 | 24 | template 25 | void AddMessage(std::string_view format, Args&&... args) 26 | { 27 | std::string messageString = fmt::vformat(format, fmt::make_format_args(args...)); 28 | std::cout << messageString << std::endl; 29 | m_MessageHistory.push_back(messageString); 30 | } 31 | 32 | template 33 | void AddItalicMessage(std::string_view format, Args&&... args) 34 | { 35 | std::string messageString = fmt::vformat(format, fmt::make_format_args(args...)); 36 | MessageInfo info = messageString; 37 | info.Italic = true; 38 | m_MessageHistory.push_back(info); 39 | std::cout << messageString << std::endl; 40 | } 41 | 42 | template 43 | void AddTaggedMessage(std::string_view tag, std::string_view format, Args&&... args) 44 | { 45 | std::string messageString = fmt::vformat(format, fmt::make_format_args(args...)); 46 | m_MessageHistory.push_back(MessageInfo(std::string(tag), messageString)); 47 | std::cout << '[' << tag << "] " << messageString << std::endl; 48 | } 49 | 50 | template 51 | void AddMessageWithColor(uint32_t color, std::string_view format, Args&&... args) 52 | { 53 | std::string messageString = fmt::vformat(format, fmt::make_format_args(args...)); 54 | m_MessageHistory.push_back(MessageInfo(messageString, color)); 55 | std::cout << messageString << std::endl; 56 | } 57 | 58 | template 59 | void AddItalicMessageWithColor(uint32_t color, std::string_view format, Args&&... args) 60 | { 61 | std::string messageString = fmt::vformat(format, fmt::make_format_args(args...)); 62 | MessageInfo info(messageString, color); 63 | info.Italic = true; 64 | m_MessageHistory.push_back(info); 65 | std::cout << messageString << std::endl; 66 | } 67 | 68 | template 69 | void AddTaggedMessageWithColor(uint32_t color, std::string_view tag, std::string_view format, Args&&... args) 70 | { 71 | std::string messageString = fmt::vformat(format, fmt::make_format_args(args...)); 72 | m_MessageHistory.push_back(MessageInfo(std::string(tag), messageString, color)); 73 | std::cout << '[' << tag << "] " << messageString << std::endl; 74 | } 75 | 76 | void OnUIRender() {} 77 | 78 | void SetMessageSendCallback(const MessageSendCallback& callback); 79 | private: 80 | void InputThreadFunc(); 81 | private: 82 | struct MessageInfo 83 | { 84 | std::string Tag; 85 | std::string Message; 86 | bool Italic = false; 87 | uint32_t Color = 0xffffffff; 88 | 89 | MessageInfo(const std::string& message, uint32_t color = 0xffffffff) 90 | : Message(message), Color(color) {} 91 | 92 | MessageInfo(const std::string& tag, const std::string& message, uint32_t color = 0xffffffff) 93 | : Tag(tag), Message(message), Color(color) {} 94 | }; 95 | 96 | std::string m_Title; 97 | std::vector m_MessageHistory; 98 | 99 | std::thread m_InputThread; 100 | bool m_InputThreadRunning = false; 101 | 102 | MessageSendCallback m_MessageSendCallback; 103 | 104 | }; -------------------------------------------------------------------------------- /App-Server/Source/ServerApp.cpp: -------------------------------------------------------------------------------- 1 | #include "Walnut/Application.h" 2 | #include "Walnut/EntryPoint.h" 3 | 4 | #include "ServerLayer.h" 5 | 6 | #include 7 | 8 | Walnut::Application* Walnut::CreateApplication(int argc, char** argv) 9 | { 10 | Walnut::ApplicationSpecification spec; 11 | spec.Name = "Walnut Chat Server 1.0"; 12 | 13 | Walnut::Application* app = new Walnut::Application(spec); 14 | app->PushLayer(); 15 | #ifndef WL_HEADLESS 16 | app->SetMenubarCallback([app]() 17 | { 18 | if (ImGui::BeginMenu("File")) 19 | { 20 | if (ImGui::MenuItem("Exit")) 21 | { 22 | app->Close(); 23 | } 24 | ImGui::EndMenu(); 25 | } 26 | }); 27 | #endif 28 | return app; 29 | } -------------------------------------------------------------------------------- /App-Server/Source/ServerLayer.cpp: -------------------------------------------------------------------------------- 1 | #include "ServerLayer.h" 2 | 3 | #include "ServerPacket.h" 4 | 5 | #include "Walnut/Core/Assert.h" 6 | #include "Walnut/Serialization/BufferStream.h" 7 | 8 | #include "Walnut/Utils/StringUtils.h" 9 | 10 | #include 11 | 12 | #include 13 | #include 14 | 15 | void ServerLayer::OnAttach() 16 | { 17 | const int Port = 8192; 18 | 19 | m_ScratchBuffer.Allocate(8192); // 8KB for now? probably too small for things like the client list/chat history 20 | 21 | m_Server = std::make_unique(Port); 22 | m_Server->SetClientConnectedCallback([this](const Walnut::ClientInfo& clientInfo) { OnClientConnected(clientInfo); }); 23 | m_Server->SetClientDisconnectedCallback([this](const Walnut::ClientInfo& clientInfo) { OnClientDisconnected(clientInfo); }); 24 | m_Server->SetDataReceivedCallback([this](const Walnut::ClientInfo& clientInfo, const Walnut::Buffer data) { OnDataReceived(clientInfo, data); }); 25 | m_Server->Start(); 26 | 27 | m_MessageHistoryFilePath = "MessageHistory.yaml"; 28 | 29 | m_Console.AddTaggedMessage("Info", "Loading message history..."); 30 | LoadMessageHistoryFromFile(m_MessageHistoryFilePath); 31 | for (const auto& message : m_MessageHistory) 32 | { 33 | m_Console.AddTaggedMessage(message.Username, message.Message); 34 | } 35 | 36 | m_Console.AddTaggedMessage("Info", "Started server on port {}", Port); 37 | 38 | m_Console.SetMessageSendCallback([this](std::string_view message) { SendChatMessage(message); }); 39 | } 40 | 41 | void ServerLayer::OnDetach() 42 | { 43 | m_Server->Stop(); 44 | // wait for server to stop here? 45 | 46 | m_ScratchBuffer.Release(); 47 | } 48 | 49 | void ServerLayer::OnUpdate(float ts) 50 | { 51 | m_ClientListTimer -= ts; 52 | if (m_ClientListTimer < 0) 53 | { 54 | m_ClientListTimer = m_ClientListInterval; 55 | SendClientListToAllClients(); 56 | 57 | // Save chat history every 10s too 58 | SaveMessageHistoryToFile(m_MessageHistoryFilePath); 59 | } 60 | } 61 | 62 | void ServerLayer::OnUIRender() 63 | { 64 | #ifndef WL_HEADLESS 65 | { 66 | ImGui::Begin("Client Info"); 67 | ImGui::Text("Connected clients: %d", m_ConnectedClients.size()); 68 | 69 | static bool selected = false; 70 | for (const auto& [id, name] : m_ConnectedClients) 71 | { 72 | if (name.Username.empty()) 73 | continue; 74 | 75 | ImGui::Selectable(name.Username.c_str(), &selected); 76 | if (ImGui::IsItemHovered()) 77 | { 78 | // Get some more info about client from server 79 | const auto& clientInfo = m_Server->GetConnectedClients().at(id); 80 | 81 | ImGui::BeginTooltip(); 82 | ImGui::SetTooltip(clientInfo.ConnectionDesc.c_str()); 83 | ImGui::EndTooltip(); 84 | } 85 | } 86 | ImGui::End(); 87 | } 88 | 89 | m_Console.OnUIRender(); 90 | 91 | // ImGui::ShowDemoWindow(); 92 | #endif 93 | } 94 | 95 | void ServerLayer::OnClientConnected(const Walnut::ClientInfo& clientInfo) 96 | { 97 | // Client connection is handled in the PacketType::ClientConnectionRequest case 98 | } 99 | 100 | void ServerLayer::OnClientDisconnected(const Walnut::ClientInfo& clientInfo) 101 | { 102 | if (m_ConnectedClients.contains(clientInfo.ID)) 103 | { 104 | SendClientDisconnect(clientInfo); 105 | const auto& userInfo = m_ConnectedClients.at(clientInfo.ID); 106 | m_Console.AddItalicMessage("Client {} disconnected", userInfo.Username); 107 | m_ConnectedClients.erase(clientInfo.ID); 108 | } 109 | else 110 | { 111 | std::cout << "[ERROR] OnClientDisconnected - Could not find client with ID=" << clientInfo.ID << std::endl; 112 | std::cout << " ConnectionDesc=" << clientInfo.ConnectionDesc << std::endl; 113 | } 114 | } 115 | 116 | void ServerLayer::OnDataReceived(const Walnut::ClientInfo& clientInfo, const Walnut::Buffer buffer) 117 | { 118 | Walnut::BufferStreamReader stream(buffer); 119 | 120 | PacketType type; 121 | bool success = stream.ReadRaw(type); 122 | WL_CORE_VERIFY(success); 123 | if (!success) // Why couldn't we read packet type? Probs invalid packet 124 | return; 125 | 126 | switch (type) 127 | { 128 | case PacketType::Message: 129 | { 130 | if (!m_ConnectedClients.contains(clientInfo.ID)) 131 | { 132 | // Reject message data from clients we don't recognize 133 | m_Console.AddMessage("Rejected incoming data from client ID={}", clientInfo.ID); 134 | m_Console.AddMessage(" ConnectionDesc={}", clientInfo.ConnectionDesc); 135 | return; 136 | } 137 | 138 | std::string message; 139 | if (stream.ReadString(message)) 140 | { 141 | if (IsValidMessage(message)) // will trim to 4096 max chars if necessary (as defined in UserInfo.h) 142 | { 143 | // Send to other clients and record 144 | WL_CORE_VERIFY(m_ConnectedClients.contains(clientInfo.ID)); 145 | const auto& client = m_ConnectedClients.at(clientInfo.ID); 146 | 147 | m_MessageHistory.push_back({ client.Username, message }); 148 | m_Console.AddTaggedMessageWithColor(client.Color | 0xff000000, client.Username, message); 149 | SendMessageToAllClients(clientInfo, message); 150 | } 151 | } 152 | break; 153 | } 154 | case PacketType::ClientConnectionRequest: 155 | { 156 | uint32_t requestedColor; 157 | std::string requestedUsername; 158 | stream.ReadRaw(requestedColor); 159 | if (stream.ReadString(requestedUsername)) 160 | { 161 | bool isValidUsername = IsValidUsername(requestedUsername); 162 | SendClientConnectionRequestResponse(clientInfo, isValidUsername); 163 | if (isValidUsername) 164 | { 165 | m_Console.AddMessage("Welcome {} (color {})", requestedUsername, requestedColor); 166 | auto& client = m_ConnectedClients[clientInfo.ID]; 167 | client.Username = requestedUsername; 168 | client.Color = requestedColor; 169 | // connection complete? notify everyone else 170 | SendClientConnect(clientInfo); 171 | 172 | // Send the new client info about other connected clients 173 | SendClientList(clientInfo); 174 | 175 | // Send message history to new client 176 | SendMessageHistory(clientInfo); 177 | } 178 | else 179 | { 180 | m_Console.AddMessage("Client connection rejected with color {} and username {}", requestedColor, requestedUsername); 181 | m_Console.AddMessage("Reason: invalid username"); 182 | } 183 | 184 | } 185 | break; 186 | } 187 | 188 | } 189 | 190 | } 191 | 192 | void ServerLayer::OnMessageReceived(const Walnut::ClientInfo& clientInfo, std::string_view message) 193 | { 194 | 195 | } 196 | 197 | void ServerLayer::OnClientConnectionRequest(const Walnut::ClientInfo& clientInfo, uint32_t userColor, std::string_view username) 198 | { 199 | 200 | } 201 | 202 | void ServerLayer::OnClientUpdate(const Walnut::ClientInfo& clientInfo, uint32_t userColor, std::string_view username) 203 | { 204 | 205 | } 206 | 207 | void ServerLayer::SendClientList(const Walnut::ClientInfo& clientInfo) 208 | { 209 | std::vector clientList(m_ConnectedClients.size()); 210 | uint32_t index = 0; 211 | for (const auto& [clientID, clientInfo] : m_ConnectedClients) 212 | clientList[index++] = clientInfo; 213 | 214 | Walnut::BufferStreamWriter stream(m_ScratchBuffer); 215 | stream.WriteRaw(PacketType::ClientList); 216 | stream.WriteArray(clientList); 217 | 218 | // WL_INFO("Sending client list to all clients"); 219 | m_Server->SendBufferToClient(clientInfo.ID, Walnut::Buffer(m_ScratchBuffer, stream.GetStreamPosition())); 220 | } 221 | 222 | void ServerLayer::SendClientListToAllClients() 223 | { 224 | std::vector clientList(m_ConnectedClients.size()); 225 | uint32_t index = 0; 226 | for (const auto& [clientID, clientInfo] : m_ConnectedClients) 227 | clientList[index++] = clientInfo; 228 | 229 | Walnut::BufferStreamWriter stream(m_ScratchBuffer); 230 | stream.WriteRaw(PacketType::ClientList); 231 | stream.WriteArray(clientList); 232 | 233 | // WL_INFO("Sending client list to all clients"); 234 | m_Server->SendBufferToAllClients(Walnut::Buffer(m_ScratchBuffer, stream.GetStreamPosition())); 235 | } 236 | 237 | void ServerLayer::SendClientConnect(const Walnut::ClientInfo& newClient) 238 | { 239 | WL_VERIFY(m_ConnectedClients.contains(newClient.ID)); 240 | const auto& newClientInfo = m_ConnectedClients.at(newClient.ID); 241 | 242 | Walnut::BufferStreamWriter stream(m_ScratchBuffer); 243 | stream.WriteRaw(PacketType::ClientConnect); 244 | stream.WriteObject(newClientInfo); 245 | 246 | m_Server->SendBufferToAllClients(Walnut::Buffer(m_ScratchBuffer, stream.GetStreamPosition()), newClient.ID); 247 | } 248 | 249 | void ServerLayer::SendClientDisconnect(const Walnut::ClientInfo& clientInfo) 250 | { 251 | const auto& userInfo = m_ConnectedClients.at(clientInfo.ID); 252 | 253 | Walnut::BufferStreamWriter stream(m_ScratchBuffer); 254 | stream.WriteRaw(PacketType::ClientDisconnect); 255 | stream.WriteObject(userInfo); 256 | 257 | m_Server->SendBufferToAllClients(Walnut::Buffer(m_ScratchBuffer, stream.GetStreamPosition()), clientInfo.ID); 258 | } 259 | 260 | void ServerLayer::SendClientConnectionRequestResponse(const Walnut::ClientInfo& clientInfo, bool response) 261 | { 262 | Walnut::BufferStreamWriter stream(m_ScratchBuffer); 263 | stream.WriteRaw(PacketType::ClientConnectionRequest); 264 | stream.WriteRaw(response); 265 | 266 | m_Server->SendBufferToClient(clientInfo.ID, Walnut::Buffer(m_ScratchBuffer, stream.GetStreamPosition())); 267 | } 268 | 269 | void ServerLayer::SendClientUpdateResponse(const Walnut::ClientInfo& clientInfo) 270 | { 271 | 272 | } 273 | 274 | void ServerLayer::SendMessageToAllClients(const Walnut::ClientInfo& fromClient, std::string_view message) 275 | { 276 | Walnut::BufferStreamWriter stream(m_ScratchBuffer); 277 | stream.WriteRaw(PacketType::Message); 278 | stream.WriteString(GetClientUsername(fromClient.ID)); 279 | stream.WriteString(message); 280 | 281 | m_Server->SendBufferToAllClients(Walnut::Buffer(m_ScratchBuffer, stream.GetStreamPosition()), fromClient.ID); 282 | } 283 | 284 | void ServerLayer::SendMessageHistory(const Walnut::ClientInfo& clientInfo) 285 | { 286 | Walnut::BufferStreamWriter stream(m_ScratchBuffer); 287 | stream.WriteRaw(PacketType::MessageHistory); 288 | stream.WriteArray(m_MessageHistory); 289 | 290 | m_Server->SendBufferToClient(clientInfo.ID, Walnut::Buffer(m_ScratchBuffer, stream.GetStreamPosition())); 291 | } 292 | 293 | void ServerLayer::SendServerShutdownToAllClients() 294 | { 295 | Walnut::BufferStreamWriter stream(m_ScratchBuffer); 296 | stream.WriteRaw(PacketType::ServerShutdown); 297 | 298 | m_Server->SendBufferToAllClients(Walnut::Buffer(m_ScratchBuffer, stream.GetStreamPosition())); 299 | } 300 | 301 | void ServerLayer::SendClientKick(const Walnut::ClientInfo& clientInfo, std::string_view reason) 302 | { 303 | Walnut::BufferStreamWriter stream(m_ScratchBuffer); 304 | stream.WriteRaw(PacketType::ClientKick); 305 | stream.WriteString(std::string(reason)); 306 | 307 | m_Server->SendBufferToClient(clientInfo.ID, Walnut::Buffer(m_ScratchBuffer, stream.GetStreamPosition())); 308 | } 309 | 310 | bool ServerLayer::KickUser(std::string_view username, std::string_view reason) 311 | { 312 | for (const auto& [clientID, userInfo] : m_ConnectedClients) 313 | { 314 | if (userInfo.Username == username) 315 | { 316 | Walnut::ClientInfo clientInfo = { clientID, "" }; 317 | SendClientKick(clientInfo, reason); 318 | m_Server->KickClient(clientID); 319 | OnClientDisconnected(clientInfo); 320 | return true; 321 | } 322 | } 323 | 324 | // Could not find user with requested username 325 | return false; 326 | } 327 | 328 | void ServerLayer::Quit() 329 | { 330 | SendServerShutdownToAllClients(); 331 | m_Server->Stop(); 332 | } 333 | 334 | bool ServerLayer::IsValidUsername(const std::string& username) const 335 | { 336 | for (const auto& [id, client] : m_ConnectedClients) 337 | { 338 | if (client.Username == username) 339 | return false; 340 | } 341 | 342 | return true; 343 | } 344 | 345 | const std::string& ServerLayer::GetClientUsername(Walnut::ClientID clientID) const 346 | { 347 | WL_VERIFY(m_ConnectedClients.contains(clientID)); 348 | return m_ConnectedClients.at(clientID).Username; 349 | } 350 | 351 | uint32_t ServerLayer::GetClientColor(Walnut::ClientID clientID) const 352 | { 353 | WL_VERIFY(m_ConnectedClients.contains(clientID)); 354 | return m_ConnectedClients.at(clientID).Color; 355 | } 356 | 357 | void ServerLayer::SendChatMessage(std::string_view message) 358 | { 359 | if (message[0] == '/') 360 | { 361 | // Try to run command instead 362 | OnCommand(message); 363 | return; 364 | } 365 | 366 | Walnut::BufferStreamWriter stream(m_ScratchBuffer); 367 | stream.WriteRaw(PacketType::Message); 368 | stream.WriteString(std::string_view("SERVER")); // Username 369 | stream.WriteString(message); 370 | m_Server->SendBufferToAllClients(stream.GetBuffer()); 371 | 372 | // echo in own console and add to message history 373 | m_Console.AddTaggedMessage("SERVER", message); 374 | m_MessageHistory.push_back({ "SERVER", std::string(message) }); 375 | } 376 | 377 | void ServerLayer::OnCommand(std::string_view command) 378 | { 379 | if (command.size() < 2 || command[0] != '/') 380 | return; 381 | 382 | std::string_view commandStr(&command[1], command.size() - 1); 383 | 384 | auto tokens = Walnut::Utils::SplitString(commandStr, ' '); 385 | if (tokens[0] == "kick") 386 | { 387 | if (tokens.size() == 2 || tokens.size() == 3) 388 | { 389 | std::string_view reason = tokens.size() == 3 ? tokens[2] : ""; 390 | if (KickUser(tokens[1], reason)) 391 | { 392 | m_Console.AddItalicMessage("User {} has been kicked.", tokens[1]); 393 | if (!reason.empty()) 394 | m_Console.AddItalicMessage(" Reason: {}", reason); 395 | } 396 | else 397 | { 398 | m_Console.AddItalicMessage("Could not kick user {}; user not found.", tokens[1]); 399 | } 400 | } 401 | else 402 | { 403 | m_Console.AddItalicMessage("Kick command requires single argument, eg. /kick "); 404 | } 405 | } 406 | } 407 | 408 | void ServerLayer::SaveMessageHistoryToFile(const std::filesystem::path& filepath) 409 | { 410 | YAML::Emitter out; 411 | { 412 | out << YAML::BeginMap; // Root 413 | out << YAML::Key << "MessageHistory" << YAML::Value; 414 | 415 | out << YAML::BeginSeq; 416 | for (const auto& chatMessage : m_MessageHistory) 417 | { 418 | out << YAML::BeginMap; 419 | out << YAML::Key << "User" << YAML::Value << chatMessage.Username; 420 | out << YAML::Key << "Message" << YAML::Value << chatMessage.Message; 421 | out << YAML::EndMap; 422 | } 423 | out << YAML::EndSeq; 424 | out << YAML::EndMap; // Root 425 | } 426 | 427 | std::ofstream fout(filepath); 428 | fout << out.c_str(); 429 | 430 | } 431 | 432 | bool ServerLayer::LoadMessageHistoryFromFile(const std::filesystem::path& filepath) 433 | { 434 | if (!std::filesystem::exists(filepath)) 435 | return false; 436 | 437 | m_MessageHistory.clear(); 438 | 439 | YAML::Node data; 440 | try 441 | { 442 | data = YAML::LoadFile(filepath.string()); 443 | } 444 | catch (YAML::ParserException e) 445 | { 446 | std::cout << "[ERROR] Failed to load message history " << filepath << std::endl << e.what() << std::endl; 447 | return false; 448 | } 449 | 450 | auto rootNode = data["MessageHistory"]; 451 | if (!rootNode) 452 | return false; 453 | 454 | m_MessageHistory.reserve(rootNode.size()); 455 | for (const auto& node : rootNode) 456 | m_MessageHistory.emplace_back(ChatMessage(node["User"].as(), node["Message"].as())); 457 | 458 | return true; 459 | } 460 | -------------------------------------------------------------------------------- /App-Server/Source/ServerLayer.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "Walnut/Layer.h" 4 | #include "Walnut/Networking/Server.h" 5 | 6 | #ifdef WL_HEADLESS 7 | #include "HeadlessConsole.h" 8 | #else 9 | #include "Walnut/UI/Console.h" 10 | #endif 11 | 12 | #include "UserInfo.h" 13 | 14 | #include 15 | 16 | class ServerLayer : public Walnut::Layer 17 | { 18 | public: 19 | virtual void OnAttach() override; 20 | virtual void OnDetach() override; 21 | virtual void OnUpdate(float ts) override; 22 | virtual void OnUIRender() override; 23 | private: 24 | // Server event callbacks 25 | void OnClientConnected(const Walnut::ClientInfo& clientInfo); 26 | void OnClientDisconnected(const Walnut::ClientInfo& clientInfo); 27 | void OnDataReceived(const Walnut::ClientInfo& clientInfo, const Walnut::Buffer buffer); 28 | 29 | //////////////////////////////////////////////////////////////////////////////// 30 | // Handle incoming messages 31 | //////////////////////////////////////////////////////////////////////////////// 32 | void OnMessageReceived(const Walnut::ClientInfo& clientInfo, std::string_view message); 33 | void OnClientConnectionRequest(const Walnut::ClientInfo& clientInfo, uint32_t userColor, std::string_view username); 34 | void OnClientUpdate(const Walnut::ClientInfo& clientInfo, uint32_t userColor, std::string_view username); 35 | 36 | //////////////////////////////////////////////////////////////////////////////// 37 | // Handle outgoing messages 38 | //////////////////////////////////////////////////////////////////////////////// 39 | void SendClientList(const Walnut::ClientInfo& clientInfo); 40 | void SendClientListToAllClients(); 41 | void SendClientConnect(const Walnut::ClientInfo& clientInfo); 42 | void SendClientDisconnect(const Walnut::ClientInfo& clientInfo); 43 | void SendClientConnectionRequestResponse(const Walnut::ClientInfo& clientInfo, bool response); 44 | void SendClientUpdateResponse(const Walnut::ClientInfo& clientInfo); 45 | void SendMessageToAllClients(const Walnut::ClientInfo& fromClient, std::string_view message); 46 | void SendMessageHistory(const Walnut::ClientInfo& clientInfo); 47 | void SendServerShutdownToAllClients(); 48 | void SendClientKick(const Walnut::ClientInfo& clientInfo, std::string_view reason); 49 | //////////////////////////////////////////////////////////////////////////////// 50 | 51 | //////////////////////////////////////////////////////////////////////////////// 52 | // Commands 53 | //////////////////////////////////////////////////////////////////////////////// 54 | bool KickUser(std::string_view username, std::string_view reason = ""); 55 | void Quit(); 56 | //////////////////////////////////////////////////////////////////////////////// 57 | 58 | bool IsValidUsername(const std::string& username) const; 59 | const std::string& GetClientUsername(Walnut::ClientID clientID) const; 60 | uint32_t GetClientColor(Walnut::ClientID clientID) const; 61 | 62 | void SendChatMessage(std::string_view message); 63 | void OnCommand(std::string_view command); 64 | void SaveMessageHistoryToFile(const std::filesystem::path& filepath); 65 | bool LoadMessageHistoryFromFile(const std::filesystem::path& filepath); 66 | private: 67 | std::unique_ptr m_Server; 68 | #ifdef WL_HEADLESS 69 | HeadlessConsole m_Console{ "Server Console" }; 70 | #else 71 | Walnut::UI::Console m_Console{ "Server Console" }; 72 | #endif 73 | std::vector m_MessageHistory; 74 | std::filesystem::path m_MessageHistoryFilePath; 75 | 76 | Walnut::Buffer m_ScratchBuffer; 77 | 78 | std::map m_ConnectedClients; 79 | 80 | // Send client list every ten seconds 81 | const float m_ClientListInterval = 10.0f; 82 | float m_ClientListTimer = m_ClientListInterval; 83 | }; -------------------------------------------------------------------------------- /App-Server/imgui.ini: -------------------------------------------------------------------------------- 1 | [Window][DockSpace Demo] 2 | Size=944,599 3 | Collapsed=0 4 | 5 | [Window][Debug##Default] 6 | Pos=60,60 7 | Size=400,400 8 | Collapsed=0 9 | 10 | [Window][Hello] 11 | Pos=397,518 12 | Size=75,68 13 | Collapsed=1 14 | 15 | [Window][Dear ImGui Demo] 16 | ViewportPos=1048,497 17 | ViewportId=0xE927CF2F 18 | Size=939,797 19 | Collapsed=0 20 | 21 | [Window][Example: Console] 22 | Pos=0,26 23 | Size=1226,874 24 | Collapsed=0 25 | DockId=0xF2944C7F,0 26 | 27 | [Window][Example: Log] 28 | Pos=275,213 29 | Size=500,400 30 | Collapsed=0 31 | 32 | [Window][Example: Simple layout] 33 | Pos=359,198 34 | Size=500,440 35 | Collapsed=0 36 | 37 | [Window][Example: Auto-resizing window] 38 | Pos=345,209 39 | Size=499,324 40 | Collapsed=0 41 | 42 | [Window][Same title as another window##1] 43 | Pos=100,100 44 | Size=470,82 45 | Collapsed=0 46 | 47 | [Window][Same title as another window##2] 48 | Pos=100,200 49 | Size=470,82 50 | Collapsed=0 51 | 52 | [Window][###AnimatedTitle] 53 | Pos=100,300 54 | Size=268,62 55 | Collapsed=0 56 | 57 | [Window][Example: Custom rendering] 58 | Pos=427,162 59 | Size=485,498 60 | Collapsed=0 61 | 62 | [Window][Example: Documents] 63 | ViewportPos=262,357 64 | ViewportId=0x1FD64BEB 65 | Size=754,1386 66 | Collapsed=0 67 | 68 | [Window][Lettuce] 69 | ViewportPos=262,357 70 | ViewportId=0x1FD64BEB 71 | Pos=8,124 72 | Size=738,1254 73 | Collapsed=0 74 | DockId=0xA086D808,0 75 | 76 | [Window][Eggplant] 77 | ViewportPos=262,357 78 | ViewportId=0x1FD64BEB 79 | Pos=8,124 80 | Size=738,1254 81 | Collapsed=0 82 | DockId=0xA086D808,1 83 | 84 | [Window][Carrot] 85 | ViewportPos=262,357 86 | ViewportId=0x1FD64BEB 87 | Pos=8,124 88 | Size=738,1254 89 | Collapsed=0 90 | DockId=0xA086D808,2 91 | 92 | [Window][Example: Property editor] 93 | Pos=113,105 94 | Size=430,450 95 | Collapsed=0 96 | 97 | [Window][Client Info] 98 | Pos=1227,33 99 | Size=372,866 100 | Collapsed=0 101 | DockId=0x00000002,0 102 | 103 | [Window][Server Console] 104 | Pos=1,33 105 | Size=1224,866 106 | Collapsed=0 107 | DockId=0x00000001,0 108 | 109 | [Window][DockSpaceWindow] 110 | Pos=0,0 111 | Size=1600,900 112 | Collapsed=0 113 | 114 | [Table][0xD0F0C6E3,2] 115 | Column 0 Weight=1.0000 116 | Column 1 Weight=1.0000 117 | 118 | [Docking][Data] 119 | DockSpace ID=0xA086D808 Pos=270,481 Size=738,1254 CentralNode=1 Selected=0x15EF3519 120 | DockSpace ID=0xCDC34B94 Window=0x41896AB8 Pos=106,167 Size=1598,866 Split=X 121 | DockNode ID=0x00000001 Parent=0xCDC34B94 SizeRef=1224,866 CentralNode=1 HiddenTabBar=1 Selected=0x0ADFD180 122 | DockNode ID=0x00000002 Parent=0xCDC34B94 SizeRef=372,866 HiddenTabBar=1 Selected=0x6C0DE7D7 123 | DockSpace ID=0xF2944C7F Pos=4065,801 Size=944,567 CentralNode=1 HiddenTabBar=1 124 | 125 | -------------------------------------------------------------------------------- /Build-Headless.lua: -------------------------------------------------------------------------------- 1 | -- premake5.lua 2 | workspace "Walnut-Chat-Headless" 3 | architecture "x64" 4 | configurations { "Debug", "Release", "Dist" } 5 | startproject "App-Server" 6 | 7 | -- Workspace-wide defines 8 | defines 9 | { 10 | "WL_HEADLESS" 11 | } 12 | 13 | -- Workspace-wide build options for MSVC 14 | filter "system:windows" 15 | buildoptions { "/EHsc", "/Zc:preprocessor", "/Zc:__cplusplus" } 16 | 17 | -- Directories 18 | outputdir = "%{cfg.buildcfg}-%{cfg.system}-%{cfg.architecture}" 19 | WalnutNetworkingBinDir = "Walnut/Walnut-Modules/Walnut-Networking/vendor/GameNetworkingSockets/bin/%{cfg.system}/%{cfg.buildcfg}/" 20 | 21 | include "Walnut/Build-Walnut-Headless-External.lua" 22 | 23 | group "App" 24 | include "App-Common/Build-App-Common-Headless.lua" 25 | include "App-Server/Build-App-Server-Headless.lua" 26 | group "" -------------------------------------------------------------------------------- /Build.lua: -------------------------------------------------------------------------------- 1 | -- premake5.lua 2 | workspace "Walnut-Chat" 3 | architecture "x64" 4 | configurations { "Debug", "Release", "Dist" } 5 | startproject "WalnutApp" 6 | 7 | -- Workspace-wide build options for MSVC 8 | filter "system:windows" 9 | buildoptions { "/EHsc", "/Zc:preprocessor", "/Zc:__cplusplus" } 10 | 11 | -- Directories 12 | outputdir = "%{cfg.buildcfg}-%{cfg.system}-%{cfg.architecture}" 13 | WalnutNetworkingBinDir = "Walnut/Walnut-Modules/Walnut-Networking/vendor/GameNetworkingSockets/bin/%{cfg.system}/%{cfg.buildcfg}/" 14 | 15 | include "Walnut/Build-Walnut-External.lua" 16 | 17 | group "App" 18 | include "App-Common/Build-App-Common.lua" 19 | include "App-Client/Build-App-Client.lua" 20 | include "App-Server/Build-App-Server.lua" 21 | group "" -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Studio Cherno 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Walnut Chat 2 | 3 | Walnut Chat is a simple client/server text chat app built with [Walnut](https://github.com/StudioCherno/Walnut) and the [Walnut-Networking](https://github.com/StudioCherno/Walnut-Networking) module. The server currently runs on both Windows (GUI/headless) and Linux (headless only), and the client is Windows only at this stage (Linux support coming soon). 4 | 5 | This was built as a demonstration of networking in C++ for a video on my YouTube channel. **There is no security** so be careful! Definitely don't run this as root on your server/computer and there certainly isn't any message encryption. [Watch the video here.](https://youtu.be/jS9rBienEFQ) 6 | 7 | 8 | ![WalnutExample](https://hazelengine.com/images/WalnutChat.jpg) 9 | _
Walnut Chat Client
_ 10 | 11 | ## Building 12 | ### Windows 13 | Running `scripts/Setup.bat` will generate both `Walnut-Chat.sln` and `Walnut-Chat-Headless.sln` solution files for Visual Studio 2022. The headless variant will only include the server, running in the headless config (no GUI console app), and the `Walnut-Chat` solution can be used to build GUI versions of the client and/or server. 14 | 15 | ### Linux (tested on Ubuntu 22) 16 | Run `scripts/Setup.sh` to generate make files for the headless server project. You can then call `make` in the root directory of the repository to build. -------------------------------------------------------------------------------- /scripts/Setup.bat: -------------------------------------------------------------------------------- 1 | @echo off 2 | 3 | pushd .. 4 | Walnut\vendor\bin\premake\Windows\premake5.exe --file=Build.lua vs2022 5 | Walnut\vendor\bin\premake\Windows\premake5.exe --file=Build-Headless.lua vs2022 6 | popd 7 | pause -------------------------------------------------------------------------------- /scripts/Setup.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | pushd .. 4 | Walnut/vendor/bin/premake/Linux/premake5 --cc=clang --file=Build-Headless.lua gmake2 5 | popd 6 | --------------------------------------------------------------------------------