Implement basic multiplayer foundation - v2.0.0

This commit is contained in:
Michael Howard 2026-04-23 17:05:28 -05:00
parent a96e983ef8
commit acf6d84d7c
2 changed files with 248 additions and 2 deletions

View File

@ -11,6 +11,7 @@
#include <filesystem> #include <filesystem>
#include <fstream> #include <fstream>
#include "rlgl.h" #include "rlgl.h"
#include "network.h"
#define CHUNK_SIZE 32 #define CHUNK_SIZE 32
#ifndef PI #ifndef PI
@ -85,7 +86,7 @@ static std::string currentWorldName = "";
static std::string playerName = "Player"; static std::string playerName = "Player";
static bool serverMode = false; 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 // Forward Declarations
bool IsExposedOptimized(int lx, int ly, int lz, Chunk* chunk, Chunk* nxM, Chunk* nxP, Chunk* nzM, Chunk* nzP); 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(); void LoadConfig();
std::string GetRemoteVersion(); std::string GetRemoteVersion();
struct RemotePlayer {
Socket sock;
std::string name;
Vector3 position;
float yaw;
};
static std::vector<RemotePlayer> remotePlayers;
// ---- Inventory System ---- // ---- Inventory System ----
struct InventorySlot { struct InventorySlot {
int blockType = AIR; int blockType = AIR;
@ -752,7 +761,16 @@ int main(void)
vfile.close(); 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<Socket> clientSockets;
InitNetworking();
float gameTime = 75.0f; // Start at 6:00 AM float gameTime = 75.0f; // Start at 6:00 AM
float breakProgress = 0.0f; float breakProgress = 0.0f;
int lastHitX = -1, lastHitY = -1, lastHitZ = -1; int lastHitX = -1, lastHitY = -1, lastHitZ = -1;
@ -895,6 +913,34 @@ int main(void)
if (IsMusicStreamPlaying(titleMusic)) StopMusicStream(titleMusic); 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 // Handle title music loop fading
float fadeTime = 2.0f; // 2 seconds fade float fadeTime = 2.0f; // 2 seconds fade
float timePlayed = GetMusicTimePlayed(titleMusic); float timePlayed = GetMusicTimePlayed(titleMusic);
@ -1280,6 +1326,87 @@ int main(void)
currentState = MAIN_MENU; currentState = MAIN_MENU;
updateReady = false; 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) { } else if (currentState != GAMEPLAY) {
BeginMode2D(camera); BeginMode2D(camera);
// Draw the texture, scaling it to fit the current window size exactly // Draw the texture, scaling it to fit the current window size exactly
@ -1453,6 +1580,19 @@ int main(void)
} }
worldChunks.clear(); 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; currentState = GAMEPLAY;
DisableCursor(); DisableCursor();
inventoryOpen = false; inventoryOpen = false;
@ -1609,6 +1749,17 @@ int main(void)
DrawRectangleRec(serverCheck, serverMode ? GREEN : DARKGRAY); DrawRectangleRec(serverCheck, serverMode ? GREEN : DARKGRAY);
DrawRectangleLinesEx(serverCheck, 2.0f, isServerHovered ? WHITE : GRAY); DrawRectangleLinesEx(serverCheck, 2.0f, isServerHovered ? WHITE : GRAY);
DrawTextEx(customFont, "Server Mode (Experimental)", (Vector2){ serverCheck.x + 35, serverCheck.y }, 18, 1.0f, WHITE); 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)) { if (isServerHovered && IsMouseButtonReleased(MOUSE_LEFT_BUTTON)) {
serverMode = !serverMode; serverMode = !serverMode;
SaveConfig(); SaveConfig();
@ -1746,6 +1897,19 @@ int main(void)
snprintf(worldName, sizeof(worldName), "%s", currentWorldName.c_str()); snprintf(worldName, sizeof(worldName), "%s", currentWorldName.c_str());
worldNameLen = strlen(worldName); 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; currentState = GAMEPLAY;
DisableCursor(); // Hide and lock cursor for first-person control DisableCursor(); // Hide and lock cursor for first-person control
@ -1820,6 +1984,14 @@ int main(void)
DrawSphere(sunPos, 5.0f, YELLOW); DrawSphere(sunPos, 5.0f, YELLOW);
DrawSphere(moonPos, 4.0f, LIGHTGRAY); 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 playerCX = (int)floorf(camera3D.position.x / CHUNK_SIZE);
int playerCZ = (int)floorf(camera3D.position.z / CHUNK_SIZE); int playerCZ = (int)floorf(camera3D.position.z / CHUNK_SIZE);

74
src/network.h Normal file
View File

@ -0,0 +1,74 @@
#ifndef NETWORK_H
#define NETWORK_H
#include <string>
#include <vector>
#ifdef _WIN32
#include <winsock2.h>
#include <ws2tcpip.h>
#pragma comment(lib, "ws2_32.lib")
typedef SOCKET Socket;
#define INVALID_SOCKET_VAL INVALID_SOCKET
#define SOCKET_ERROR_VAL SOCKET_ERROR
#else
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <fcntl.h>
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