From acf6d84d7cbeb22811a994ab2bc0635593201f8a Mon Sep 17 00:00:00 2001 From: Michael Howard Date: Thu, 23 Apr 2026 17:05:28 -0500 Subject: [PATCH] Implement basic multiplayer foundation - v2.0.0 --- src/main.cpp | 176 +++++++++++++++++++++++++++++++++++++++++++++++++- src/network.h | 74 +++++++++++++++++++++ 2 files changed, 248 insertions(+), 2 deletions(-) create mode 100644 src/network.h diff --git a/src/main.cpp b/src/main.cpp index 629dece..08450ee 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -11,6 +11,7 @@ #include #include #include "rlgl.h" +#include "network.h" #define CHUNK_SIZE 32 #ifndef PI @@ -85,7 +86,7 @@ static std::string currentWorldName = ""; static std::string playerName = "Player"; static bool serverMode = false; -enum MenuState { MAIN_MENU, OPTIONS_MENU, CREATE_WORLD_MENU, LOAD_WORLD_MENU, GAMEPLAY, PAUSE_MENU, CRAFTING_GUI, CHECKING_UPDATES, UPDATE_NOTES, UPDATE_FOUND, DOWNLOADING_UPDATE }; +enum MenuState { MAIN_MENU, OPTIONS_MENU, CREATE_WORLD_MENU, LOAD_WORLD_MENU, GAMEPLAY, PAUSE_MENU, CRAFTING_GUI, CHECKING_UPDATES, UPDATE_NOTES, UPDATE_FOUND, DOWNLOADING_UPDATE, CONNECT_MENU }; // Forward Declarations bool IsExposedOptimized(int lx, int ly, int lz, Chunk* chunk, Chunk* nxM, Chunk* nxP, Chunk* nzM, Chunk* nzP); @@ -96,6 +97,14 @@ void SaveConfig(); void LoadConfig(); std::string GetRemoteVersion(); +struct RemotePlayer { + Socket sock; + std::string name; + Vector3 position; + float yaw; +}; +static std::vector remotePlayers; + // ---- Inventory System ---- struct InventorySlot { int blockType = AIR; @@ -752,7 +761,16 @@ int main(void) vfile.close(); } - InventorySlot mouseHeldItem(AIR, 0); + // Networking State + char targetIP[128] = "127.0.0.1"; + char targetPort[16] = "12345"; + int activeNetField = 0; // 0=none, 1=IP, 2=Port + bool isConnecting = false; + Socket clientSocket = INVALID_SOCKET_VAL; + Socket serverSocket = INVALID_SOCKET_VAL; + std::vector clientSockets; + + InitNetworking(); float gameTime = 75.0f; // Start at 6:00 AM float breakProgress = 0.0f; int lastHitX = -1, lastHitY = -1, lastHitZ = -1; @@ -895,6 +913,34 @@ int main(void) if (IsMusicStreamPlaying(titleMusic)) StopMusicStream(titleMusic); } + // --- NETWORK UPDATE --- + if (isConnecting) { + fd_set writefds; + FD_ZERO(&writefds); + FD_SET(clientSocket, &writefds); + struct timeval tv = {0, 0}; + if (select((int)clientSocket + 1, NULL, &writefds, NULL, &tv) > 0) { + isConnecting = false; + currentState = GAMEPLAY; + // Send Handshake + PacketHeader head = { (uint8_t)PACKET_HANDSHAKE, (uint32_t)sizeof(PacketHandshake) }; + PacketHandshake hand; + strncpy(hand.name, playerName.c_str(), 31); + send(clientSocket, (char*)&head, sizeof(head), 0); + send(clientSocket, (char*)&hand, sizeof(hand), 0); + } + } + + if (serverSocket != INVALID_SOCKET_VAL) { + struct sockaddr_in client_addr; + socklen_t client_len = sizeof(client_addr); + Socket newClient = accept(serverSocket, (struct sockaddr*)&client_addr, &client_len); + if (newClient != INVALID_SOCKET_VAL) { + SetNonBlocking(newClient); + clientSockets.push_back(newClient); + } + } + // Handle title music loop fading float fadeTime = 2.0f; // 2 seconds fade float timePlayed = GetMusicTimePlayed(titleMusic); @@ -1280,6 +1326,87 @@ int main(void) currentState = MAIN_MENU; updateReady = false; } + } else if (currentState == CONNECT_MENU) { + DrawRectangle(0, 0, currentWidth, currentHeight, (Color){ 0, 0, 0, 220 }); + int cw = 500, ch = 350; + Rectangle cBox = { (float)currentWidth/2 - cw/2, (float)currentHeight/2 - ch/2, (float)cw, (float)ch }; + DrawRectangleRec(cBox, (Color){ 30, 30, 30, 255 }); + DrawRectangleLinesEx(cBox, 4.0f, DARKGRAY); + + DrawTextEx(customFont, "DIRECT CONNECT", (Vector2){ cBox.x + 110, cBox.y + 30 }, 28, 1.0f, WHITE); + + // IP Address Input + DrawTextEx(customFont, "Server IP Address:", (Vector2){ cBox.x + 50, cBox.y + 90 }, 20, 1.0f, LIGHTGRAY); + Rectangle ipRect = { cBox.x + 50, cBox.y + 120, 400, 40 }; + bool isIPHovered = CheckCollisionPointRec(mousePos, ipRect); + DrawRectangleRec(ipRect, (activeNetField == 1) ? BLACK : (isIPHovered ? (Color){ 50, 50, 50, 255 } : (Color){ 40, 40, 40, 255 })); + DrawRectangleLinesEx(ipRect, 2, (activeNetField == 1) ? GREEN : GRAY); + DrawTextEx(customFont, targetIP, (Vector2){ ipRect.x + 10, ipRect.y + 10 }, 20, 1.0f, WHITE); + if (isIPHovered && IsMouseButtonPressed(MOUSE_LEFT_BUTTON)) activeNetField = 1; + + // Port Input + DrawTextEx(customFont, "Server Port:", (Vector2){ cBox.x + 50, cBox.y + 180 }, 20, 1.0f, LIGHTGRAY); + Rectangle portRect = { cBox.x + 50, cBox.y + 210, 150, 40 }; + bool isPortHovered = CheckCollisionPointRec(mousePos, portRect); + DrawRectangleRec(portRect, (activeNetField == 2) ? BLACK : (isPortHovered ? (Color){ 50, 50, 50, 255 } : (Color){ 40, 40, 40, 255 })); + DrawRectangleLinesEx(portRect, 2, (activeNetField == 2) ? GREEN : GRAY); + DrawTextEx(customFont, targetPort, (Vector2){ portRect.x + 10, portRect.y + 10 }, 20, 1.0f, WHITE); + if (isPortHovered && IsMouseButtonPressed(MOUSE_LEFT_BUTTON)) activeNetField = 2; + + // Input handling for IP/Port + int key = GetCharPressed(); + while (key > 0) { + if (activeNetField == 1 && strlen(targetIP) < 127) { + int len = strlen(targetIP); + targetIP[len] = (char)key; + targetIP[len+1] = '\0'; + } else if (activeNetField == 2 && strlen(targetPort) < 15) { + int len = strlen(targetPort); + targetPort[len] = (char)key; + targetPort[len+1] = '\0'; + } + key = GetCharPressed(); + } + if (IsKeyPressed(KEY_BACKSPACE)) { + if (activeNetField == 1) { + int len = strlen(targetIP); + if (len > 0) targetIP[len-1] = '\0'; + } else if (activeNetField == 2) { + int len = strlen(targetPort); + if (len > 0) targetPort[len-1] = '\0'; + } + } + + // Join Button Logic + Rectangle joinBtn = { cBox.x + 50, cBox.y + 280, 180, 45 }; + bool isJoinHovered = CheckCollisionPointRec(mousePos, joinBtn); + DrawRectangleRec(joinBtn, isJoinHovered ? GREEN : (Color){ 0, 120, 0, 255 }); + DrawTextEx(customFont, "JOIN SERVER", (Vector2){ joinBtn.x + 25, joinBtn.y + 12 }, 20, 1.0f, WHITE); + + if (isJoinHovered && IsMouseButtonReleased(MOUSE_LEFT_BUTTON)) { + clientSocket = socket(AF_INET, SOCK_STREAM, 0); + if (clientSocket != INVALID_SOCKET_VAL) { + SetNonBlocking(clientSocket); + struct sockaddr_in serv_addr; + serv_addr.sin_family = AF_INET; + serv_addr.sin_port = htons(atoi(targetPort)); + inet_pton(AF_INET, targetIP, &serv_addr.sin_addr); + + connect(clientSocket, (struct sockaddr*)&serv_addr, sizeof(serv_addr)); + isConnecting = true; + } + } + + // Cancel Button + Rectangle cancelBtn = { cBox.x + 270, cBox.y + 280, 180, 45 }; + bool isCancelHovered = CheckCollisionPointRec(mousePos, cancelBtn); + DrawRectangleRec(cancelBtn, isCancelHovered ? RED : (Color){ 120, 0, 0, 255 }); + DrawTextEx(customFont, "CANCEL", (Vector2){ cancelBtn.x + 55, cancelBtn.y + 12 }, 20, 1.0f, WHITE); + if (isCancelHovered && IsMouseButtonReleased(MOUSE_LEFT_BUTTON)) { + currentState = MAIN_MENU; + activeNetField = 0; + } + } else if (currentState != GAMEPLAY) { BeginMode2D(camera); // Draw the texture, scaling it to fit the current window size exactly @@ -1453,6 +1580,19 @@ int main(void) } worldChunks.clear(); + if (serverMode) { + serverSocket = socket(AF_INET, SOCK_STREAM, 0); + if (serverSocket != INVALID_SOCKET_VAL) { + SetNonBlocking(serverSocket); + struct sockaddr_in serv_addr; + serv_addr.sin_family = AF_INET; + serv_addr.sin_addr.s_addr = INADDR_ANY; + serv_addr.sin_port = htons(12345); + bind(serverSocket, (struct sockaddr*)&serv_addr, sizeof(serv_addr)); + listen(serverSocket, 5); + } + } + currentState = GAMEPLAY; DisableCursor(); inventoryOpen = false; @@ -1609,6 +1749,17 @@ int main(void) DrawRectangleRec(serverCheck, serverMode ? GREEN : DARKGRAY); DrawRectangleLinesEx(serverCheck, 2.0f, isServerHovered ? WHITE : GRAY); DrawTextEx(customFont, "Server Mode (Experimental)", (Vector2){ serverCheck.x + 35, serverCheck.y }, 18, 1.0f, WHITE); + + // Connect Button + Rectangle connBtn = { (float)currentWidth/2 - 100, (float)currentHeight/2 + 70, 200, 40 }; + bool isConnHovered = CheckCollisionPointRec(mousePos, connBtn); + DrawRectangleRec(connBtn, isConnHovered ? DARKGRAY : BLACK); + DrawRectangleLinesEx(connBtn, 2, GRAY); + DrawTextEx(customFont, "CONNECT", (Vector2){ connBtn.x + 50, connBtn.y + 10 }, 20, 1.0f, WHITE); + if (isConnHovered && IsMouseButtonReleased(MOUSE_LEFT_BUTTON)) { + currentState = CONNECT_MENU; + } + if (isServerHovered && IsMouseButtonReleased(MOUSE_LEFT_BUTTON)) { serverMode = !serverMode; SaveConfig(); @@ -1746,6 +1897,19 @@ int main(void) snprintf(worldName, sizeof(worldName), "%s", currentWorldName.c_str()); worldNameLen = strlen(worldName); + if (serverMode) { + serverSocket = socket(AF_INET, SOCK_STREAM, 0); + if (serverSocket != INVALID_SOCKET_VAL) { + SetNonBlocking(serverSocket); + struct sockaddr_in serv_addr; + serv_addr.sin_family = AF_INET; + serv_addr.sin_addr.s_addr = INADDR_ANY; + serv_addr.sin_port = htons(12345); + bind(serverSocket, (struct sockaddr*)&serv_addr, sizeof(serv_addr)); + listen(serverSocket, 5); + } + } + currentState = GAMEPLAY; DisableCursor(); // Hide and lock cursor for first-person control @@ -1820,6 +1984,14 @@ int main(void) DrawSphere(sunPos, 5.0f, YELLOW); DrawSphere(moonPos, 4.0f, LIGHTGRAY); + // --- DRAW REMOTE PLAYERS --- + for (const auto& rp : remotePlayers) { + DrawCapsule(rp.position, (Vector3){ rp.position.x, rp.position.y + 1.8f, rp.position.z }, 0.4f, 8, 8, BLUE); + // Draw Nameplate + Vector2 namePos = GetWorldToScreen((Vector3){ rp.position.x, rp.position.y + 2.2f, rp.position.z }, camera3D); + DrawTextEx(customFont, rp.name.c_str(), (Vector2){ namePos.x - MeasureTextEx(customFont, rp.name.c_str(), 20, 1.0f).x/2, namePos.y }, 20, 1.0f, WHITE); + } + int playerCX = (int)floorf(camera3D.position.x / CHUNK_SIZE); int playerCZ = (int)floorf(camera3D.position.z / CHUNK_SIZE); diff --git a/src/network.h b/src/network.h new file mode 100644 index 0000000..5baecdc --- /dev/null +++ b/src/network.h @@ -0,0 +1,74 @@ +#ifndef NETWORK_H +#define NETWORK_H + +#include +#include + +#ifdef _WIN32 + #include + #include + #pragma comment(lib, "ws2_32.lib") + typedef SOCKET Socket; + #define INVALID_SOCKET_VAL INVALID_SOCKET + #define SOCKET_ERROR_VAL SOCKET_ERROR +#else + #include + #include + #include + #include + #include + typedef int Socket; + #define INVALID_SOCKET_VAL -1 + #define SOCKET_ERROR_VAL -1 + #define closesocket close +#endif + +enum PacketType { + PACKET_HANDSHAKE = 0, + PACKET_PLAYER_UPDATE = 1, + PACKET_CHUNK_DATA = 2, + PACKET_DISCONNECT = 3 +}; + +struct PacketHeader { + uint8_t type; + uint32_t size; +}; + +struct PacketHandshake { + char name[32]; +}; + +struct PacketPlayerUpdate { + float x, y, z; + float yaw; +}; + +// Cross-platform socket initialization +inline bool InitNetworking() { +#ifdef _WIN32 + WSADATA wsaData; + return WSAStartup(MAKEWORD(2, 2), &wsaData) == 0; +#else + return true; +#endif +} + +inline void ShutdownNetworking() { +#ifdef _WIN32 + WSACleanup(); +#endif +} + +inline bool SetNonBlocking(Socket s) { +#ifdef _WIN32 + unsigned long mode = 1; + return ioctlsocket(s, FIONBIO, &mode) == 0; +#else + int flags = fcntl(s, F_GETFL, 0); + if (flags == -1) return false; + return fcntl(s, F_SETFL, flags | O_NONBLOCK) == 0; +#endif +} + +#endif