diff options
| author | Devyn Challman <devyn@challman.org> | 2026-04-18 17:08:21 -0700 |
|---|---|---|
| committer | Devyn Challman <devyn@challman.org> | 2026-04-18 17:08:21 -0700 |
| commit | 71ac9c0d75d405a61c4985e644d1d5e24debff6d (patch) | |
| tree | eb37f0f34618d154d48252d3a178941a69d33e4a | |
| parent | 025f9e632153c095cb24505c4163722d823a87ad (diff) | |
improve server client sync and move npc to server side
| -rw-r--r-- | Makefile | 33 | ||||
| -rw-r--r-- | client/client.c | 257 | ||||
| -rw-r--r-- | client/client.h | 81 | ||||
| -rw-r--r-- | client/main.c | 843 | ||||
| -rw-r--r-- | client/net/net.c | 26 | ||||
| -rw-r--r-- | client/net/net.h | 4 | ||||
| -rw-r--r-- | server/main.c | 72 | ||||
| -rw-r--r-- | server/server.c | 504 | ||||
| -rw-r--r-- | server/server.h | 84 | ||||
| -rw-r--r-- | shared/net_compat.h | 16 | ||||
| -rw-r--r-- | shared/queue.c | 29 | ||||
| -rw-r--r-- | shared/queue.h | 20 | ||||
| -rw-r--r-- | shared/shared.h | 121 |
13 files changed, 1994 insertions, 96 deletions
@@ -1,24 +1,28 @@ # Detect OS UNAME_S := $(shell uname -s) -CFLAGS_COMMON = -MMD -MP -Wall -Wextra -I./shared +CFLAGS_COMMON = -MMD -MP -Wall -Wextra -I./shared $(shell pkg-config --cflags raylib) # Compiler ifeq ($(UNAME_S),Darwin) CC = gcc-15 CFLAGS = $(CFLAGS_COMMON) -pthread - LDFLAGS = -pthread + LDFLAGS = -pthread $(shell pkg-config --libs raylib) else ifeq ($(UNAME_S),Linux) CC = gcc CFLAGS = $(CFLAGS_COMMON) -pthread - LDFLAGS = -pthread + LDFLAGS = -pthread $(shell pkg-config --libs raylib) else # Windows (MSYS2 MinGW) CC = gcc CFLAGS = $(CFLAGS_COMMON) - LDFLAGS = -lws2_32 + LDFLAGS = -lws2_32 $(shell pkg-config --libs raylib) endif +SHARED_BUILD = build/shared +SHARED_SRC = $(shell find shared -name "*.c") +SHARED_OBJ = $(SHARED_SRC:shared/%.c=$(SHARED_BUILD)/%.o) + # Directories SERVER_BUILD = build/server SERVER_TARGET = bin/server @@ -31,8 +35,8 @@ CLIENT_SRC = $(shell find client -name "*.c") CLIENT_OBJ = $(CLIENT_SRC:client/%.c=$(CLIENT_BUILD)/%.o) # Raylib via pkg-config (works on macOS, Linux, MSYS2) -CLIENT_CFLAGS = $(CFLAGS) $(shell pkg-config --cflags raylib) -CLIENT_LDFLAGS = $(LDFLAGS) $(shell pkg-config --libs raylib) +CLIENT_CFLAGS = $(CFLAGS) +CLIENT_LDFLAGS = $(LDFLAGS) # Windows needs extra libs sometimes ifeq ($(OS),Windows_NT) @@ -40,8 +44,9 @@ ifeq ($(OS),Windows_NT) endif # Dependency files --include $(SERVER_OBJ:.o=.d) --include $(CLIENT_OBJ:.o=.d) +-include $(wildcard $(SHARED_BUILD)/*.d) +-include $(wildcard $(SERVER_BUILD)/*.d) +-include $(wildcard $(CLIENT_BUILD)/*.d) .PHONY: all clean server client @@ -54,12 +59,16 @@ client: $(CLIENT_TARGET) bin: mkdir -p bin +$(SHARED_BUILD)/%.o: shared/%.c + mkdir -p $(dir $@) + $(CC) $(CFLAGS) -c $< -o $@ + # Link targets -$(SERVER_TARGET): $(SERVER_OBJ) | bin - $(CC) $(SERVER_OBJ) -o $(SERVER_TARGET) $(LDFLAGS) +$(SERVER_TARGET): $(SERVER_OBJ) $(SHARED_OBJ) | bin + $(CC) $(SERVER_OBJ) $(SHARED_OBJ) -o $(SERVER_TARGET) $(LDFLAGS) -$(CLIENT_TARGET): $(CLIENT_OBJ) | bin - $(CC) $(CLIENT_OBJ) -o $(CLIENT_TARGET) $(CLIENT_LDFLAGS) +$(CLIENT_TARGET): $(CLIENT_OBJ) $(SHARED_OBJ) | bin + $(CC) $(CLIENT_OBJ) $(SHARED_OBJ) -o $(CLIENT_TARGET) $(CLIENT_LDFLAGS) # Compile rules $(SERVER_BUILD)/%.o: server/%.c diff --git a/client/client.c b/client/client.c new file mode 100644 index 0000000..13810f8 --- /dev/null +++ b/client/client.c @@ -0,0 +1,257 @@ +#include "queue.h" +#include "raylib.h" +#include "raymath.h" +#include "shared.h" +#include <stdio.h> +#include <stdlib.h> +#include <time.h> +#include "client.h" + +PacketQueue incomingQueue; +ClientPacket latestPacket; +pthread_mutex_t packetLock = PTHREAD_MUTEX_INITIALIZER; +double lastSendTime = 0; +RemotePlayer remotePlayers[MAX_PLAYERS]; // add here +ClientNPC clientNPCs[MAX_VIEW_NPCS]; + +#define PING_SAMPLES 20 +double pingSamples[PING_SAMPLES] = {0}; +int pingSampleIndex = 0; +double pingMs = 0; + +void SendPing() { + PingPacket ping = { + .type = PACKET_PING, + .sentTime = GetWallTime() + }; + sendto(networkFd, &ping, sizeof(ping), 0, + (struct sockaddr *)&server, sizeof(server)); +} + +float GetTerrainHeight(float x, float z) { + Vector3 rayOrigin = { x, 99999.0f, z }; + Ray ray = { rayOrigin, (Vector3){0, -1, 0} }; + RayCollision hit = GetRayCollisionMesh(ray, terrainMesh, terrainTransform); + if (hit.hit) return hit.point.y; + return 0.0f; // fallback if miss +} + +void PrintAddress(struct sockaddr_in *addr) { + char ip[INET_ADDRSTRLEN]; + + inet_ntop(AF_INET, &addr->sin_addr, ip, sizeof(ip)); + + printf("IP: %s, Port: %d\n", ip, ntohs(addr->sin_port)); +} + +Vector3 ToRaylib(Vec3 v) { + return (Vector3){ v.x, v.y, v.z }; +} + +Vec3 FromRaylib(Vector3 v) { + return (Vec3){ v.x, v.y, v.z }; +} + +void NetworkInit() { + net_init(); + + networkFd = socket(AF_INET, SOCK_DGRAM, 0); + + if (networkFd == -1) + err(EXIT_FAILURE, "socket failed"); + + server.sin_family = AF_INET; + server.sin_port = htons(8080); + inet_pton(AF_INET, "10.148.10.109", &server.sin_addr); +} + +void NetworkShutdown() { + close(networkFd); + net_cleanup(); +} + +void *RecvThread(void *arg) { + uint8_t buffer[PACKET_MAX_SIZE]; + struct sockaddr_in from; + socklen_t fromLen = sizeof(from); + + while (1) { + int size = recvfrom(networkFd, (char *)buffer, sizeof(buffer), 0, + (struct sockaddr *)&from, &fromLen); + + if (size <= 0) continue; + + PacketType type = *(PacketType *)buffer; + + // handle pong immediately + if (type == PACKET_PONG) { + PingPacket *pong = (PingPacket *)buffer; + double sample = (GetWallTime() - pong->sentTime) * 1000.0; + +// update rolling average + pingSamples[pingSampleIndex % PING_SAMPLES] = sample; + pingSampleIndex++; + double total = 0; + int count = pingSampleIndex < PING_SAMPLES ? pingSampleIndex : PING_SAMPLES; + for (int i = 0; i < count; i++) total += pingSamples[i]; + pingMs = total / count; + continue; + } + + QueuePush(&incomingQueue, buffer, size); + } + return NULL; +} + +// send thread - fires every 50ms +void *SendThread(void *arg) { + double accumulator = 0; + double previousTime = GetWallTime(); + + while (1) { + double currentTime = GetWallTime(); + double elapsed = currentTime - previousTime; + previousTime = currentTime; + accumulator += elapsed; + + while (accumulator >= NETWORK_RATE) { + pthread_mutex_lock(&packetLock); + lastSendTime = GetWallTime(); + sendto(networkFd, (const char *)&latestPacket, sizeof(latestPacket), 0, + (struct sockaddr *)&server, sizeof(server)); + + pthread_mutex_unlock(&packetLock); + accumulator -= NETWORK_RATE; + } + + sleep_us(1000); + } + return NULL; +} + +// main thread processes incoming queue each frame +void ProcessIncoming() { + uint8_t buffer[PACKET_MAX_SIZE]; + int size; + while (QueuePop(&incomingQueue, buffer, &size)) { + PacketType type = *(PacketType *)buffer; + switch (type) { + case PACKET_SERVER_SNAPSHOT: + HandleSnapshot((ServerSnapshot *)buffer); + break; + case PACKET_PLAYER_JOINED: + HandlePlayerJoined((PlayerJoinedPacket *)buffer); + break; + case PACKET_PLAYER_LEFT: + HandlePlayerLeft((PlayerJoinedPacket *)buffer); + break; + case PACKET_HANDSHAKE_RESPONSE: + HandleHandshakeResponse((HandshakeResponsePacket *)buffer); + break; + } + } +} + + +void HandleSnapshot(ServerSnapshot *snapshot) { + for (int i = 0; i < snapshot->playerCount; i++) { + int id = snapshot->players[i].id; + if (id == player.id) { + ReconcilePlayer(&player, ToRaylib(snapshot->players[i].position)); + } else { + remotePlayers[id].active = true; + remotePlayers[id].serverPosition = ToRaylib(snapshot->players[i].position); + remotePlayers[id].serverYaw = snapshot->players[i].yaw; + remotePlayers[id].serverVelocityY = snapshot->players[i].velocity.y; + remotePlayers[id].serverOnGround = snapshot->players[i].onGround; // add this + } + } + for (int i = 0; i < snapshot->npcCount; i++) { + clientNPCs[i].id = snapshot->npcs[i].id; + clientNPCs[i].active = true; + clientNPCs[i].serverPosition = ToRaylib(snapshot->npcs[i].position); + clientNPCs[i].serverRotation = snapshot->npcs[i].rotation; + clientNPCs[i].health = snapshot->npcs[i].health; + clientNPCs[i].state = snapshot->npcs[i].state; + clientNPCs[i].model = snapshot->npcs[i].model; + clientNPCs[i].scale = (Vector3){snapshot->npcs[i].scale, snapshot->npcs[i].scale, snapshot->npcs[i].scale}; + } +} + +void HandlePlayerJoined(PlayerJoinedPacket *packet) { + int id = packet->playerId; + remotePlayers[id].active = true; + remotePlayers[id].id = id; + + // set initial position so they don't lerp from 0,0,0 + remotePlayers[id].currentPosition = ToRaylib(packet->position); + remotePlayers[id].serverPosition = ToRaylib(packet->position); + remotePlayers[id].serverYaw = 0; + remotePlayers[id].currentYaw = 0; + + printf("remote character model %d\n", packet->characterClass); + + remotePlayers[id].characterClass = packet->characterClass; + + // load correct model based on their class + switch (packet->characterClass) { + case 0: remotePlayers[id].model = LoadModel("assets/dungeon/Models/GLB format/character-orc-f.glb"); break; + case 1: + remotePlayers[id].model = LoadModel("assets/dungeon/Models/GLB format/character-human.glb"); + break; + case 2: remotePlayers[id].model = LoadModel("assets/arena/Models/GLB format/character-soldier.glb"); break; + } + + TraceLog(LOG_INFO, "Player %d joined", id); +} + +void HandlePlayerLeft(PlayerJoinedPacket *packet) { + int id = packet->playerId; + remotePlayers[id].active = false; + + // unload their model to free memory + /* UnloadModel(remotePlayers[id].model); */ + + TraceLog(LOG_INFO, "Player %d left", id); +} + +void HandleHandshakeResponse(HandshakeResponsePacket *packet) { + player.id = packet->playerId; + latestPacket.playerId = packet->playerId; + printf("assigned player id: %d\n", player.id); + scene = 1; + DisableCursor(); +} + +void ReconcilePlayer(Player *p, Vector3 serverPosition) { + p->position.x = Lerp(p->position.x, serverPosition.x, 0.3f); + p->position.z = Lerp(p->position.z, serverPosition.z, 0.3f); +} + +void InitRemotePlayers() { + for (int i = 0; i < MAX_PLAYERS; i++) { + remotePlayers[i].active = false; + remotePlayers[i].currentPosition = (Vector3){600, GetTerrainHeight(600,-1200), -1200}; + remotePlayers[i].serverPosition = (Vector3){600, GetTerrainHeight(600,-1200), -1200}; + remotePlayers[i].currentYaw = 0; + remotePlayers[i].serverYaw = 0; + } +} + +// client.c +void UpdateRemotePlayer(RemotePlayer *p, float dt) { + p->currentPosition.x = Lerp(p->currentPosition.x, p->serverPosition.x, 25.0f * dt); + p->currentPosition.z = Lerp(p->currentPosition.z, p->serverPosition.z, 25.0f * dt); + + if (p->serverOnGround) { + // on ground - snap Y to terrain + float localY = GetTerrainHeight(p->currentPosition.x, p->currentPosition.z); + if (localY != 0.0f) + p->currentPosition.y = localY; + } else { + // in air - lerp Y toward server position for smooth arc + p->currentPosition.y = Lerp(p->currentPosition.y, p->serverPosition.y, 15.0f * dt); + } + + p->currentYaw = Lerp(p->currentYaw, p->serverYaw, 25.0f * dt); +} diff --git a/client/client.h b/client/client.h new file mode 100644 index 0000000..81446ea --- /dev/null +++ b/client/client.h @@ -0,0 +1,81 @@ +#pragma once + +#include "raylib.h" +#include "shared.h" +#include "queue.h" +#include "net_compat.h" + +static int networkFd = -1; +static struct sockaddr_in server; + +extern PacketQueue incomingQueue; +extern ClientPacket latestPacket; +extern pthread_mutex_t packetLock; +extern double lastSendTime; +extern Mesh terrainMesh; +extern Matrix terrainTransform; +extern double pingMs; + +extern int scene; + +// your own player - full prediction +typedef struct { + int id; + Vector3 position; + Vector3 velocity; + float yaw; + float pitch; + float height; + float speed; + int sprint; + bool onGround; +} Player; + +// other players - no prediction, just interpolate +typedef struct { + int id; + bool active; + Vector3 currentPosition; // what we render + Vector3 serverPosition; // latest from server + float currentYaw; + float serverYaw; + float serverVelocityY; // add this + bool serverOnGround; + int animationState; + Model model; + int characterClass; +} RemotePlayer; + +// npcs - no AI, just interpolate +typedef struct { + int id; + bool active; + Vector3 currentPosition; // what we render + Vector3 serverPosition; // latest from server + float currentRotation; + float serverRotation; + int state; + int health; + int model; + Vector3 scale; +} ClientNPC; + +extern Player player; +extern RemotePlayer remotePlayers[MAX_PLAYERS]; +extern ClientNPC clientNPCs[MAX_VIEW_NPCS]; + +void NetworkInit(); +void NetworkShutdown(); +void SendPlayerUpdate(ClientPacket*); +void *RecvThread(void *arg); +void *SendThread(void *arg); +void ProcessIncoming(); +void ReconcilePlayer(Player *p, Vector3 serverPosition); +void HandleSnapshot(ServerSnapshot *snapshot); +void HandlePlayerJoined(PlayerJoinedPacket *packet); +void HandlePlayerLeft(PlayerJoinedPacket *packet); +void HandleHandshakeResponse(HandshakeResponsePacket *packet); +void InitRemotePlayers(); +float GetTerrainHeight(float x, float z); +void UpdateRemotePlayer(RemotePlayer *p, float dt); +void SendPing(); diff --git a/client/main.c b/client/main.c index c0ed27e..dd079b0 100644 --- a/client/main.c +++ b/client/main.c @@ -1,63 +1,844 @@ #include "raylib.h" +#include "raymath.h" +#include "rlgl.h" +#include <math.h> #include <stdio.h> +#include <time.h> +#include "shared.h" +#include "client.h" #include "net/net.h" +#define FPS_SAMPLES 120 +double fpsSamples[FPS_SAMPLES] = {0}; +int fpsSampleIndex = 0; +double avgFps = 0; + +#define HUD_FPS_X 10 +#define HUD_PING_X 120 +#define HUD_COORDS_X 300 + +typedef struct { + Vector3 position; + Vector3 spawnPosition; // remember where they started + Vector3 target; // where they're walking to + float rotation; + float scale; + float moveTimer; // how long to walk + float waitTimer; // how long to wait + bool isMoving; +} NPC; + +#define MAX_PARTICLES 15 + +typedef struct { + Vector3 position; + Vector3 velocity; + float life; + float size; +} FireParticle; + +FireParticle particles[MAX_PARTICLES]; + +Vector3 firePos = {0.0f, -0.10f, 1.5f}; + +int selectedChar = -1; + +void InitParticles() { + for (int i = 0; i < MAX_PARTICLES; i++) { + particles[i].life = 0; + } +} + +void UpdateParticles(float dt) { + for (int i = 0; i < MAX_PARTICLES; i++) { + + if (particles[i].life <= 0) { + // Respawn at fire base + particles[i].position = (Vector3){ + firePos.x + ((float)GetRandomValue(-10,10)/100.0f), + firePos.y + 0.1f, + firePos.z + ((float)GetRandomValue(-10,10)/100.0f) + }; + + particles[i].velocity = (Vector3){ + ((float)GetRandomValue(-5,5)/100.0f), // slight sideways drift + 0.6f + (float)GetRandomValue(0,40)/100.0f, + ((float)GetRandomValue(-5,5)/100.0f) + }; + + particles[i].life = 0.6f + (float)GetRandomValue(0,40)/100.0f; + particles[i].size = 0.15f + (float)GetRandomValue(0,1)/100.0f; + } + + // Movement + particles[i].position.x += particles[i].velocity.x * dt; + particles[i].position.y += particles[i].velocity.y * dt; + particles[i].position.z += particles[i].velocity.z * dt; + + // subtle swirl (makes flame feel alive) + particles[i].position.x += sinf(GetTime()*5 + i) * 0.002f; + particles[i].position.z += cosf(GetTime()*5 + i) * 0.002f; + + // Fade + particles[i].life -= dt; + + // Clamp height (keeps fire tight) + if (particles[i].position.y > firePos.y + 0.5f) { + particles[i].life = 0; + } + } +} + +void DrawParticles() { + for (int i = 0; i < MAX_PARTICLES; i++) { + if (particles[i].life > 0) { + + float t = particles[i].life; + + // Color gradient (yellow → orange → red) + Color color; + if (t > 0.5f) { + color = (Color){255, 200, 50, (unsigned char)(255 * t)}; + } else if (t > 0.25f) { + color = (Color){255, 120, 20, (unsigned char)(255 * t)}; + } else { + color = (Color){200, 40, 10, (unsigned char)(255 * t)}; + } + + float size = particles[i].size * t; + + DrawCubeV(particles[i].position, + (Vector3){size, size, size}, + color); + } + } +} + +int BuildInputMask() { + int inputs = 0; + if (IsKeyDown(KEY_W)) inputs |= INPUT_FORWARD; + if (IsKeyDown(KEY_S)) inputs |= INPUT_BACK; + if (IsKeyDown(KEY_A)) inputs |= INPUT_LEFT; + if (IsKeyDown(KEY_D)) inputs |= INPUT_RIGHT; + if (IsKeyDown(KEY_SPACE)) inputs |= INPUT_JUMP; + if (IsKeyDown(KEY_LEFT_SHIFT)) inputs |= INPUT_SPRINT; + return inputs; +} + +void UpdateNPC(NPC *npc, Mesh terrainMesh, Matrix terrainTransform, float dt) { + if (npc->isMoving) { + // walk toward target + Vector3 dir = Vector3Subtract(npc->target, npc->position); + dir.y = 0; + float dist = Vector3Length(dir); + + if (dist > 1.0f) { + dir = Vector3Normalize(dir); + npc->position.x += dir.x * 20.0f * dt; // npc walk speed + npc->position.z += dir.z * 20.0f * dt; + + // snap to terrain height + npc->position.y = GetTerrainHeight( + npc->position.x, npc->position.z); + + // face direction of travel + npc->rotation = atan2f(dir.x, dir.z) * RAD2DEG; + } + + npc->moveTimer -= dt; + if (npc->moveTimer <= 0.0f || dist <= 1.0f) { + // stop walking, start waiting + npc->isMoving = false; + npc->waitTimer = (float)GetRandomValue(200, 500) / 100.0f; // wait 2-5 sec + } + + } else { + // waiting + npc->waitTimer -= dt; + if (npc->waitTimer <= 0.0f) { + // pick a new random target near spawn + float range = 100.0f; // how far they wander from spawn + float tx = npc->spawnPosition.x + (float)GetRandomValue(-range, range); + float tz = npc->spawnPosition.z + (float)GetRandomValue(-range, range); + npc->target = (Vector3){tx, 0, tz}; + npc->isMoving = true; + npc->moveTimer = (float)GetRandomValue(300, 800) / 100.0f; // walk 3-8 sec + } + } +} + +void UpdatePlayer(Player *p, Camera3D *cam, Mesh terrainMesh, Matrix terrainTransform, float dt) { + + Vector2 mouseDelta = GetMouseDelta(); + p->yaw -= mouseDelta.x * 0.003f; + p->pitch -= mouseDelta.y * 0.003f; + p->pitch = Clamp(p->pitch, -1.4f, 1.4f); // prevent flipping + + // --- Movement direction (relative to yaw) --- + Vector3 forward = { + sinf(p->yaw), + 0, + cosf(p->yaw) + }; + Vector3 right = { + cosf(p->yaw), + 0, + -sinf(p->yaw) + }; + + + // --- Jump --- + if (IsKeyPressed(KEY_SPACE) && p->onGround) { + p->velocity.y = 500.0f; + p->onGround = false; + } + + Vector3 moveDir = {0}; + if (IsKeyDown(KEY_W)) moveDir = Vector3Add(moveDir, forward); + if (IsKeyDown(KEY_S)) moveDir = Vector3Subtract(moveDir, forward); + if (IsKeyDown(KEY_D)) moveDir = Vector3Subtract(moveDir, right); + if (IsKeyDown(KEY_A)) moveDir = Vector3Add(moveDir, right); + + if (IsKeyDown(KEY_LEFT_SHIFT)) { + if (p->onGround) { + p->sprint = 5; + } else { + p->sprint = 10; + } + } else { + p->sprint = 1; + } + + // normalize so diagonals aren't faster + if (Vector3Length(moveDir) > 0) + moveDir = Vector3Normalize(moveDir); + + p->position.x += moveDir.x * p->speed * p-> sprint * dt; + p->position.z += moveDir.z * p->speed * p-> sprint * dt; + + // --- Gravity --- + if (!p->onGround) p->velocity.y -= 300.0f * dt; + p->position.y += p->velocity.y * dt; + + // --- Terrain Collision (downward ray) --- + Vector3 rayOrigin = { p->position.x, p->position.y + 2000.0f, p->position.z }; + Ray downRay = { rayOrigin, (Vector3){0, -1, 0} }; + RayCollision hit = GetRayCollisionMesh(downRay, terrainMesh, terrainTransform); + + if (hit.hit) { + float groundY = hit.point.y; + + if (p->position.y <= groundY + 0.5f) { + // snap to ground + p->position.y = groundY; + p->velocity.y = 0; + p->onGround = true; + } else { + p->onGround = false; + } + } else { + p->onGround = false; + } + + + // --- Attach Camera to Player Head --- + Vector3 headPos = { p->position.x, p->position.y + p->height, p->position.z }; + cam->position = headPos; + + // camera looks in yaw+pitch direction + cam->target = (Vector3){ + headPos.x + sinf(p->yaw) * cosf(p->pitch), + headPos.y + sinf(p->pitch), + headPos.z + cosf(p->yaw) * cosf(p->pitch) + }; +} + +Player player = {.position = (Vector3){600, 3000.0f, -1200}, + .velocity = {0}, + .yaw = 0.0f, + .pitch = 0.0f, + .height = 5.0f, + .speed = 30.0f, + .sprint = 1.0f, + .onGround = false}; + +Mesh terrainMesh; +Matrix terrainTransform; +int scene = 0; + int main() { - const int screenWidth = 800; - const int screenHeight = 450; + SetConfigFlags(FLAG_VSYNC_HINT); + InitWindow(0, 0, "Pants - MMO"); + SetTargetFPS(0); + SetRandomSeed(1234567890); + Model terrain = LoadModel("assets/terrain.glb"); - InitWindow(screenWidth, screenHeight, "Pants - MMO"); + if (terrain.meshCount == 0) { + TraceLog(LOG_ERROR, "Terrain failed to load!"); + } - SetTargetFPS(60); +// Bake scale into vertices so rendering and collision match + float scaleXZ = 10.0f; + float scaleY = 10.0f; + for (int i = 0; i < terrain.meshes[0].vertexCount; i++) { + terrain.meshes[0].vertices[i * 3 + 0] *= scaleXZ; + terrain.meshes[0].vertices[i * 3 + 1] *= scaleY; + terrain.meshes[0].vertices[i * 3 + 2] *= scaleXZ; + } + UpdateMeshBuffer(terrain.meshes[0], 0, terrain.meshes[0].vertices, + terrain.meshes[0].vertexCount * 3 * sizeof(float), 0); - char* message = "Welcome to Pants a game by Matthew & Devyn Challman"; - int fontSize = 25; + terrain.transform = MatrixIdentity(); + terrainMesh = terrain.meshes[0]; // first mesh in the model + terrainTransform = terrain.transform; // world transform of the model + + InitRemotePlayers(); + + NetworkInit(); + QueueInit(&incomingQueue); + + pthread_t recvThread, sendThread; + int r1 = pthread_create(&recvThread, NULL, RecvThread, NULL); + int r2 = pthread_create(&sendThread, NULL, SendThread, NULL); + + int fontSize = 25; + char* message = "Welcome to Pants a game by Matthew & Devyn Challman"; int messageWidth = MeasureText(message, fontSize); - int messagePosX = (screenWidth - messageWidth) / 2; - int messagePosY = (screenHeight - fontSize) / 2; + int messagePosX = (GetScreenWidth() - messageWidth) / 2; + int messagePosY = (GetScreenHeight() - fontSize) / 2 - 200; + + char* subMessage = "Select a character to connect"; + int subMessageWidth = MeasureText(subMessage, fontSize); + int subMessagePosX = (GetScreenWidth() - subMessageWidth) / 2; + int subMessagePosY = (GetScreenHeight() - fontSize) / 2 - 150; int buttonW = 150; int buttonH = 50; - int buttonPosX = (screenWidth - buttonW) / 2; - int buttonPosY = (screenHeight - buttonH) / 2 + 100; + int buttonPosX = (GetScreenWidth() - buttonW) / 2; + int buttonPosY = (GetScreenHeight() - buttonH) / 2 + 100; char *connect = "Connect"; int connectWidth = MeasureText(connect, fontSize); - int connectPosX = (screenWidth - connectWidth) / 2; - int connectPosY = (screenHeight - fontSize) / 2 + 100; + int connectPosX = (GetScreenWidth() - connectWidth) / 2; + int connectPosY = (GetScreenHeight() - fontSize) / 2 + 100; + + Camera3D camera = {0}; + camera.up = (Vector3){0, 1, 0}; + camera.fovy = 70.0f; + camera.projection = CAMERA_PERSPECTIVE; + + Camera3D startCamera = {0}; + + + startCamera.position = (Vector3){ 0.0f, 1.5f, -1.0f }; + startCamera.target = (Vector3){ 0.0f, -0.5f, 10.0f }; // look toward your model + startCamera.up = (Vector3){ 0.0f, 1.0f, 0.0f }; + startCamera.fovy = 70.0f; + startCamera.projection = CAMERA_PERSPECTIVE; + + Model npcModels[47]; + char *paths[47]; + + paths[0] = "assets/characters/Models/GLB format/character-a.glb"; + paths[1] = "assets/characters/Models/GLB format/character-b.glb"; + paths[2] = "assets/characters/Models/GLB format/character-c.glb"; + paths[3] = "assets/characters/Models/GLB format/character-d.glb"; + paths[4] = "assets/characters/Models/GLB format/character-e.glb"; + paths[5] = "assets/characters/Models/GLB format/character-f.glb"; + paths[6] = "assets/characters/Models/GLB format/character-g.glb"; + paths[7] = "assets/characters/Models/GLB format/character-h.glb"; + paths[8] = "assets/characters/Models/GLB format/character-i.glb"; + paths[9] = "assets/characters/Models/GLB format/character-j.glb"; + paths[10] = "assets/characters/Models/GLB format/character-k.glb"; + paths[11] = "assets/characters/Models/GLB format/character-l.glb"; + paths[12] = "assets/characters/Models/GLB format/character-m.glb"; + paths[13] = "assets/characters/Models/GLB format/character-n.glb"; + paths[14] = "assets/characters/Models/GLB format/character-o.glb"; + paths[15] = "assets/characters/Models/GLB format/character-p.glb"; + paths[16] = "assets/characters/Models/GLB format/character-q.glb"; + paths[17] = "assets/characters/Models/GLB format/character-r.glb"; + paths[18] = "assets/graveyard/Models/GLB format/character-ghost.glb"; + paths[19] = "assets/graveyard/Models/GLB format/character-skeleton.glb"; + paths[20] = "assets/graveyard/Models/GLB format/character-zombie.glb"; + paths[21] = "assets/graveyard/Models/GLB format/character-keeper.glb"; + paths[22] = "assets/graveyard/Models/GLB format/character-vampire.glb"; + paths[23] = "assets/pets/Models/GLB format/animal-beaver.glb"; + paths[24] = "assets/pets/Models/GLB format/animal-caterpillar.glb"; + paths[25] = "assets/pets/Models/GLB format/animal-deer.glb"; + paths[26] = "assets/pets/Models/GLB format/animal-fox.glb"; + paths[27] = "assets/pets/Models/GLB format/animal-lion.glb"; + paths[28] = "assets/pets/Models/GLB format/animal-penguin.glb"; + paths[29] = "assets/pets/Models/GLB format/animal-bee.glb"; + paths[30] = "assets/pets/Models/GLB format/animal-chick.glb"; + paths[31] = "assets/pets/Models/GLB format/animal-dog.glb"; + paths[32] = "assets/pets/Models/GLB format/animal-giraffe.glb"; + paths[33] = "assets/pets/Models/GLB format/animal-monkey.glb"; + paths[34] = "assets/pets/Models/GLB format/animal-pig.glb"; + paths[35] = "assets/pets/Models/GLB format/animal-bunny.glb"; + paths[36] = "assets/pets/Models/GLB format/animal-cow.glb"; + paths[37] = "assets/pets/Models/GLB format/animal-elephant.glb"; + paths[38] = "assets/pets/Models/GLB format/animal-hog.glb"; + paths[39] = "assets/pets/Models/GLB format/animal-panda.glb"; + paths[40] = "assets/pets/Models/GLB format/animal-polar.glb"; + paths[41] = "assets/pets/Models/GLB format/animal-cat.glb"; + paths[42] = "assets/pets/Models/GLB format/animal-crab.glb"; + paths[43] = "assets/pets/Models/GLB format/animal-fish.glb"; + paths[44] = "assets/pets/Models/GLB format/animal-koala.glb"; + paths[45] = "assets/pets/Models/GLB format/animal-parrot.glb"; + paths[46] = "assets/pets/Models/GLB format/animal-tiger.glb"; + + for (int i = 0; i < 47; ++i) { + npcModels[i] = LoadModel(paths[i]); + } + + Vector3 npcPositions[1000]; + + float npcRotations[1000]; + + for (int i = 0; i < 1000; ++i) { + float npcX = (float)GetRandomValue(-500, 500); + float npcZ = (float)GetRandomValue(-500, 500); + float npcY = GetTerrainHeight(npcX, npcZ); + npcPositions[i] = (Vector3){ npcX, npcY, npcZ }; + npcRotations[i] = (float)GetRandomValue(0, 360); // random degrees + } + + Vector3 petPositions[1000]; + + float petRotations[1000]; + + for (int i = 0; i < 1000; ++i) { + float npcX = (float)GetRandomValue(-500, 500); + float npcZ = (float)GetRandomValue(-500, 500); + float npcY = GetTerrainHeight(npcX, npcZ); + petPositions[i] = (Vector3){ npcX, npcY, npcZ }; + petRotations[i] = (float)GetRandomValue(0, 360); // random degrees + } + + + Model treeModels[5]; + char *treePaths[5]; + + treePaths[0] = "assets/world/Models/GLB format/tree-crooked.glb"; + treePaths[1] = "assets/world/Models/GLB format/tree-high-crooked.glb"; + treePaths[2] = "assets/world/Models/GLB format/tree-high-round.glb"; + treePaths[3] = "assets/world/Models/GLB format/tree-high.glb"; + treePaths[4] = "assets/world/Models/GLB format/tree.glb"; + + Vector3 treePositions[1000]; + + float treeRotations[1000]; + + float treeScales[1000]; + + for (int i = 0; i < 1000; ++i) { + float npcX = (float)GetRandomValue(-500, 500); + float npcZ = (float)GetRandomValue(-500, 500); + float npcY = GetTerrainHeight(npcX, npcZ); + treePositions[i] = (Vector3){ npcX, npcY - 1, npcZ }; + treeRotations[i] = (float)GetRandomValue(0, 360); // random degrees + treeScales[i] = (float)GetRandomValue(10, 400) / 10.0f; // 0.8 to 1.5 + } + + + for (int i = 0; i < 5; ++i) { + treeModels[i] = LoadModel(treePaths[i]); + } + + Model rockModels[3]; + char *rockPaths[3]; + + rockPaths[0] = "assets/world/Models/GLB format/rock-large.glb"; + rockPaths[1] = "assets/world/Models/GLB format/rock-small.glb"; + rockPaths[2] = "assets/world/Models/GLB format/rock-wide.glb"; + + Vector3 rockPositions[100]; + + float rockRotations[100]; + + float rockScales[100]; + + for (int i = 0; i < 100; ++i) { + float npcX = (float)GetRandomValue(-500, 500); + float npcZ = (float)GetRandomValue(-500, 500); + float npcY = GetTerrainHeight(npcX, npcZ); + rockPositions[i] = (Vector3){ npcX, npcY - 5, npcZ }; + rockRotations[i] = (float)GetRandomValue(0, 360); // random degrees + rockScales[i] = (float)GetRandomValue(10, 200) / 10.0f; // 0.8 to 1.5 + } + + + for (int i = 0; i < 3; ++i) { + rockModels[i] = LoadModel(rockPaths[i]); + } + + Model houseModels[2]; + char *housePaths[2]; + + housePaths[0] = "assets/house.glb"; + housePaths[1] = "assets/house2.glb"; + + Vector3 housePositions[20]; + + float houseRotations[20]; + + float houseScales[20]; + + for (int i = 0; i < 20; ++i) { + float npcX = (float)GetRandomValue(600, 1200); + float npcZ = (float)GetRandomValue(-1300, -2000); + float npcY = GetTerrainHeight(npcX, npcZ); + housePositions[i] = (Vector3){ npcX, npcY - 1, npcZ }; + houseRotations[i] = (float)GetRandomValue(0, 360); // random degrees + houseScales[i] = (float)GetRandomValue(100, 200) / 10.0f; // 0.8 to 1.5 + } + + for (int i = 0; i < 2; ++i) { + houseModels[i] = LoadModel(housePaths[i]); + } + + Model playerModel = + LoadModel("assets/dungeon/Models/GLB format/character-human.glb"); + + int animCount; + ModelAnimation *anims = LoadModelAnimations( + "assets/arena/Models/GLB format/character-soldier.glb", &animCount); + + Model orcModel = + LoadModel("assets/dungeon/Models/GLB format/character-orc-f.glb"); + + Model campFireModel = + LoadModel("assets/survival/Models/GLB format/campfire-pit.glb"); + + Model soldierModel = + LoadModel("assets/arena/Models/GLB format/character-soldier.glb"); + + if (animCount > 0) { + printf("This GLB has %d animation(s)\n", animCount); + } else { + printf("No animations found\n"); + } + + for (int i = 0; i < animCount; i++) { + printf("Animation %d: %s, frames: %d\n", i, anims[i].name, anims[i].frameCount); + } + +// Let's assume idle is animation index 1 + int orcIdleAnim = 7; + int playerIdleAnim = 7; + int soldierIdleAnim = 7; + float orcFrame = 0.0f; + float playerFrame = 0.0f; + float soldierFrame = 0.0f; while (!WindowShouldClose()) { + if (scene == 0) { + float dt = GetFrameTime(); + + float animSpeed = 24.0f; // frames per second of the idle animation + orcFrame += animSpeed * dt; + playerFrame += animSpeed * dt; + soldierFrame += animSpeed * dt; + + Ray ray = GetMouseRay(GetMousePosition(), startCamera); + + BoundingBox playerBox = GetModelBoundingBox(playerModel); + playerBox.min = Vector3Add(playerBox.min, (Vector3){0.0, 0, 3}); + playerBox.max = Vector3Add(playerBox.max, (Vector3){0.0, 0, 3}); + + RayCollision hitPlayer = GetRayCollisionBox(ray, playerBox); + + BoundingBox soldierBox = GetModelBoundingBox(soldierModel); + soldierBox.min = Vector3Add(soldierBox.min, (Vector3){-1.25, 0, 2.5}); + soldierBox.max = Vector3Add(soldierBox.max, (Vector3){-1.25, 0, 2.5}); - bool connecting = false; - if (IsMouseButtonPressed(MOUSE_LEFT_BUTTON)) { - struct Vector2 mousePos = GetMousePosition(); + RayCollision hitSoldier = GetRayCollisionBox(ray, soldierBox); - if (mousePos.x >= buttonPosX && mousePos.x <= buttonPosX + buttonW && - mousePos.y >= buttonPosY && mousePos.y <= buttonPosY + buttonH) { + BoundingBox orcBox = GetModelBoundingBox(orcModel); + orcBox.min = Vector3Add(orcBox.min, (Vector3){1.25, 0, 2.5}); + orcBox.max = Vector3Add(orcBox.max, (Vector3){1.25, 0, 2.5}); - connecting = true; + RayCollision hitOrc = GetRayCollisionBox(ray, orcBox); + + int hitIndex = -1; + + if (hitOrc.hit) hitIndex = 0; + if (hitPlayer.hit) hitIndex = 1; + if (hitSoldier.hit) hitIndex = 2; + + if (hitOrc.hit || hitPlayer.hit || hitSoldier.hit) { + SetMouseCursor(MOUSE_CURSOR_POINTING_HAND); + } else { + SetMouseCursor(MOUSE_CURSOR_DEFAULT); } - } - BeginDrawing(); - ClearBackground(RAYWHITE); - DrawText(message, messagePosX, messagePosY, fontSize, LIGHTGRAY); + if (hitIndex > -1 && IsMouseButtonPressed(MOUSE_BUTTON_LEFT)) { + pthread_mutex_lock(&packetLock); + latestPacket.inputs = BuildInputMask(); + latestPacket.yaw = player.yaw; + latestPacket.type = PACKET_HANDSHAKE; + latestPacket.playerId = player.id; + latestPacket.characterClass = hitIndex; + pthread_mutex_unlock(&packetLock); + if (hitIndex == 0) player.height = 7.0f; + } + + if (hitOrc.hit) { + orcIdleAnim = 19; + } else { + orcIdleAnim = 7; + } + + if (hitPlayer.hit) { + playerIdleAnim = 19; + } else { + playerIdleAnim = 7; + } + + if (hitSoldier.hit) { + soldierIdleAnim = 19; + } else { + soldierIdleAnim = 7; + } + + // Loop the animation + + if (orcFrame >= anims[orcIdleAnim].frameCount) orcFrame = 0.0f; + if (playerFrame >= anims[playerIdleAnim].frameCount) playerFrame = 0.0f; + if (soldierFrame >= anims[soldierIdleAnim].frameCount) soldierFrame = 0.0f; + + UpdateModelAnimation(playerModel, anims[playerIdleAnim], playerFrame); + UpdateModelAnimation(orcModel, anims[orcIdleAnim], orcFrame); + UpdateModelAnimation(soldierModel, anims[soldierIdleAnim], soldierFrame); + UpdateParticles(dt); + + BeginDrawing(); + ClearBackground(RAYWHITE); + + BeginMode3D(startCamera); + + DrawGrid(20, 1.0f); + + DrawModelEx(orcModel, (Vector3){1.25, 0, 2.5}, (Vector3){0, 1, 0}, 200, + (Vector3){1.5, 1.5, 1.5}, WHITE); + + if (hitOrc.hit) { + BeginBlendMode(BLEND_ADDITIVE); + DrawCylinder((Vector3){1.25f, 0.0f, 2.5f}, + 0.6f, 0.6f, 4.0f, 32, + (Color){255, 120, 30, 30}); + DrawCylinder((Vector3){1.25f, 0.0f, 2.5f}, 0.4f, 0.4f, 4.0f, 32, + (Color){255, 160, 60, 80}); + EndBlendMode(); + } + + DrawModelEx(playerModel, (Vector3){0.0, 0, 3}, (Vector3){0, 1, 0}, 180, + (Vector3){1, 1, 1}, WHITE); + + if (hitPlayer.hit) { + BeginBlendMode(BLEND_ADDITIVE); + DrawCylinder((Vector3){0.0f, 0.0f, 3.0f}, + 0.5f, 0.5f, 4.0f, 32, + (Color){255, 120, 30, 30}); + DrawCylinder((Vector3){0.0f, 0.0f, 3.0f}, 0.3f, 0.3f, 4.0f, 32, + (Color){255, 160, 60, 80}); + EndBlendMode(); + } + + + DrawModelEx(soldierModel, (Vector3){-1.15, 0, 2.5}, (Vector3){0, 1, 0}, + 160, (Vector3){1, 1, 1}, WHITE); + + if (hitSoldier.hit) { + BeginBlendMode(BLEND_ADDITIVE); + DrawCylinder((Vector3){-1.15f, 0.0f, 2.5f}, + 0.5f, 0.5f, 4.0f, 32, + (Color){255, 120, 30, 30}); + DrawCylinder((Vector3){-1.15f, 0.0f, 2.5f}, 0.3f, 0.3f, 4.0f, 32, + (Color){255, 160, 60, 80}); + EndBlendMode(); + } + + DrawModelEx(campFireModel, (Vector3){0.0, -0.10, 1.5}, + (Vector3){0, 1, 0}, + 180, (Vector3){1.5, 1.5, 1.5}, WHITE); + + DrawParticles(); + + EndMode3D(); + + DrawText(message, messagePosX, messagePosY, fontSize, BLACK); + DrawText(subMessage, subMessagePosX, subMessagePosY, fontSize, BLACK); + + EndDrawing(); + + ProcessIncoming(); + } else { + float dt = GetFrameTime(); + static double lastPingTime = 0; + fpsSamples[fpsSampleIndex % FPS_SAMPLES] = 1.0 / dt; + fpsSampleIndex++; - if(connecting) { - DrawRectangle(buttonPosX, buttonPosY, buttonW, buttonH, RED); - } else { - DrawRectangle(buttonPosX, buttonPosY, buttonW, buttonH, GREEN); + double total = 0; + int count = fpsSampleIndex < FPS_SAMPLES ? fpsSampleIndex : FPS_SAMPLES; + for (int i = 0; i < count; i++) total += fpsSamples[i]; + avgFps = total / count; + if (GetWallTime() - lastPingTime > 1.0) { + SendPing(); + lastPingTime = GetWallTime(); + } + + + pthread_mutex_lock(&packetLock); + latestPacket.inputs = BuildInputMask(); + latestPacket.yaw = player.yaw; + latestPacket.type = PACKET_CLIENT_INPUT; + latestPacket.playerId = player.id; + pthread_mutex_unlock(&packetLock); + + UpdatePlayer(&player, &camera, terrainMesh, terrainTransform, dt); + + for (int i = 0; i < MAX_PLAYERS; i++) { + if (!remotePlayers[i].active) continue; + if (i == player.id) + continue; // skip yourself + UpdateRemotePlayer(&remotePlayers[i], dt); + } + + bool connecting = false; + if (IsMouseButtonPressed(MOUSE_LEFT_BUTTON)) { + struct Vector2 mousePos = GetMousePosition(); + + if (mousePos.x >= buttonPosX && mousePos.x <= buttonPosX + buttonW && + mousePos.y >= buttonPosY && mousePos.y <= buttonPosY + buttonH) { + + connecting = true; } + } + + BeginDrawing(); + ClearBackground(RAYWHITE); + + BeginMode3D(camera); + + DrawModel(terrain, (Vector3){0, 0, 0}, 1.0f, WHITE); - DrawText(connect, connectPosX, connectPosY, fontSize, BLUE); + DrawGrid(20, 1.0f); - DrawFPS(0,0); - EndDrawing(); + for (int i = 0; i < MAX_PLAYERS; i++) { + if (!remotePlayers[i].active) continue; + if (i == player.id) continue; - if (connecting) - ServerConnect(); + float dist = + Vector3Distance(player.position, remotePlayers[i].currentPosition); + + Vector3 scale = {10.0f, 10.0f, 10.0f}; + + if (remotePlayers[i].characterClass == 0) { + scale.x = 15.0f; + scale.y = 15.0f; + scale.z = 15.0; + } + + // beam disappears when close + if (dist > 50.0f) { + Vector3 pos = remotePlayers[i].currentPosition; + Vector3 beamBottom = { pos.x, pos.y, pos.z }; + Vector3 beamTop = { pos.x, pos.y + 2000, pos.z }; + DrawCylinderEx((Vector3){ pos.x, pos.y+13, pos.z }, beamTop, 4.0f, 4.0f, 8, (Color){255, 255, 255, 220}); + DrawCylinderEx(beamBottom, beamTop, 8.0f, 8.0f, 8, (Color){255, 255, 0, 80}); + } + + DrawModelEx(remotePlayers[i].model, remotePlayers[i].currentPosition, + (Vector3){0, 1, 0}, + remotePlayers[i].currentYaw * RAD2DEG, + scale, WHITE); + + } + + for (int i = 0; i < MAX_VIEW_NPCS; ++i) { + DrawModelEx( + npcModels[clientNPCs[i].model], + clientNPCs[i].serverPosition, + (Vector3){0, 1, 0}, // rotate around Y axis (vertical) + clientNPCs[i].serverRotation, // random angle in degrees + clientNPCs[i].scale, + WHITE + ); + } + + for (int i = 0; i < 1000; ++i) { + DrawModelEx( + treeModels[i % 5], + treePositions[i], + (Vector3){0, 1, 0}, // rotate around Y axis (vertical) + treeRotations[i], // random angle in degrees + (Vector3){treeScales[i], treeScales[i], treeScales[i]}, // scale + WHITE + ); + } + + for (int i = 0; i < 100; ++i) { + DrawModelEx( + rockModels[i % 3], + rockPositions[i], + (Vector3){0, 1, 0}, // rotate around Y axis (vertical) + rockRotations[i], // random angle in degrees + (Vector3){rockScales[i], rockScales[i], rockScales[i]}, // scale + WHITE + ); + } + + for (int i = 0; i < 20; ++i) { + DrawModelEx( + houseModels[i % 2], + housePositions[i], + (Vector3){0, 1, 0}, // rotate around Y axis (vertical) + houseRotations[i], // random angle in degrees + (Vector3){houseScales[i], houseScales[i], houseScales[i]}, // scale + WHITE + ); + } + + EndMode3D(); + +// fixed x positions - never move regardless of content + + +int y = 10; +int padY = 5; +int totalW = 700; +int totalH = fontSize + padY * 2; + +// background +DrawRectangle(0, y - padY, totalW, totalH, (Color){0, 0, 0, 140}); + +// fps - always at same x +char fpsBuf[32]; +snprintf(fpsBuf, sizeof(fpsBuf), "FPS: %3.0f", avgFps); +Color fpsColor = avgFps > 50 ? GREEN : avgFps > 30 ? YELLOW : RED; +DrawText(fpsBuf, HUD_FPS_X, y, fontSize, fpsColor); + +// ping - always at same x +char pingBuf[32]; +snprintf(pingBuf, sizeof(pingBuf), "PING: %5.1f ms", pingMs); +Color pingColor = pingMs < 50 ? GREEN : pingMs < 100 ? YELLOW : RED; +DrawText(pingBuf, HUD_PING_X, y, fontSize, pingColor); + +// coords - always at same x +char coordsBuf[64]; +snprintf(coordsBuf, sizeof(coordsBuf), "X:%8.1f Y:%8.1f Z:%8.1f", + player.position.x, player.position.y, player.position.z); +DrawText(coordsBuf, HUD_COORDS_X, y, fontSize, WHITE); + + EndDrawing(); + + ProcessIncoming(); + } } + NetworkShutdown(); + CloseWindow(); return 0; } diff --git a/client/net/net.c b/client/net/net.c index 7f6332e..e69de29 100644 --- a/client/net/net.c +++ b/client/net/net.c @@ -1,26 +0,0 @@ -#include <stdio.h> -#include <stdlib.h> -#include <time.h> -#include "net.h" -#include "net_compat.h" - -void ServerConnect() { - net_init(); - - int fd = socket(AF_INET, SOCK_DGRAM, 0); - - if (fd == -1) - err(EXIT_FAILURE, "socket failed"); - - char buff[64]; - - struct sockaddr_in server; - server.sin_family = AF_INET; - server.sin_port = htons(8080); - server.sin_addr.s_addr = htonl(INADDR_LOOPBACK); - - sendto(fd, buff, sizeof(buff), 0, (struct sockaddr *)&server, sizeof(server)); - - close(fd); - net_cleanup(); -} diff --git a/client/net/net.h b/client/net/net.h index 7947419..b28b04f 100644 --- a/client/net/net.h +++ b/client/net/net.h @@ -1 +1,3 @@ -void ServerConnect(); + + + diff --git a/server/main.c b/server/main.c index 6a5e5ba..03c0573 100644 --- a/server/main.c +++ b/server/main.c @@ -1,44 +1,66 @@ +#include "raylib.h" +#include "raymath.h" #include <stdio.h> #include <stdlib.h> #include <time.h> +#include "server.h" +#include "queue.h" +#include "shared.h" #include "net_compat.h" +ClientConnection connections[MAX_PLAYERS]; +PacketQueue incomingQueue; +PacketQueue outgoingQueue; +Mesh terrainMesh; +Matrix terrainTransform; +ServerPlayer players[MAX_PLAYERS]; +ServerNPC npcs[MAX_NPCS]; +int playerCount = 0; +int npcCount = 0; + int main() { - net_init(); + /* set_nonblocking(fd); */ + QueueInit(&incomingQueue); + QueueInit(&outgoingQueue); - int fd = socket(AF_INET, SOCK_DGRAM, 0); +InitWindow(1, 1, "server"); // tiny hidden window +SetWindowState(FLAG_WINDOW_HIDDEN); - if (fd == -1) - err(EXIT_FAILURE, "socket failed"); - struct sockaddr_in address; - address.sin_family = AF_INET; - address.sin_port = htons(8080); - address.sin_addr.s_addr = htonl(INADDR_LOOPBACK); + Model terrain = LoadModel("assets/terrain.glb"); - if (bind(fd, (struct sockaddr *)&address, sizeof(address)) == -1) { - close(fd); - err(EXIT_FAILURE, "bind failed"); - } +if (terrain.meshCount == 0) { + TraceLog(LOG_ERROR, "Terrain failed to load!"); +} - set_nonblocking(fd); +// Bake scale into vertices so rendering and collision match + float scaleXZ = 10.0f; + float scaleY = 10.0f; + for (int i = 0; i < terrain.meshes[0].vertexCount; i++) { + terrain.meshes[0].vertices[i * 3 + 0] *= scaleXZ; + terrain.meshes[0].vertices[i * 3 + 1] *= scaleY; + terrain.meshes[0].vertices[i * 3 + 2] *= scaleXZ; + } + UpdateMeshBuffer(terrain.meshes[0], 0, terrain.meshes[0].vertices, + terrain.meshes[0].vertexCount * 3 * sizeof(float), 0); - time_t startTime = time(NULL); + terrain.transform = MatrixIdentity(); + terrainMesh = terrain.meshes[0]; + terrainTransform = terrain.transform; - while (time(NULL) < startTime + 60) { - struct sockaddr connection; - socklen_t connectionLen = sizeof(connection); - char buff[64]; + CloseWindow(); - int size = recvfrom(fd, buff, sizeof(buff), 0, - (struct sockaddr *)&connection, &connectionLen); + NetworkInit(); - if (size != -1) - printf("message recievd\n"); - } + // start threads + pthread_t recvThread, sendThread, gameThread; + pthread_create(&recvThread, NULL, RecvThread, NULL); + pthread_create(&sendThread, NULL, SendThread, NULL); + pthread_create(&gameThread, NULL, GameThread, NULL); - close(fd); - net_cleanup(); + // wait forever + pthread_join(gameThread, NULL); + NetworkShutdown(); return 0; } diff --git a/server/server.c b/server/server.c new file mode 100644 index 0000000..860755b --- /dev/null +++ b/server/server.c @@ -0,0 +1,504 @@ +#include "raylib.h" +#include "raymath.h" +#include <time.h> +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#include "shared.h" +#include "server.h" +#include "queue.h" + +void PrintAddress(struct sockaddr_in *addr) { + char ip[INET_ADDRSTRLEN]; + + inet_ntop(AF_INET, &addr->sin_addr, ip, sizeof(ip)); + + printf("IP: %s, Port: %d\n", ip, ntohs(addr->sin_port)); +} + +Vector3 ToRaylib(Vec3 v) { + return (Vector3){ v.x, v.y, v.z }; +} + +Vec3 FromRaylib(Vector3 v) { + return (Vec3){ v.x, v.y, v.z }; +} + +void NetworkInit() { + net_init(); + + networkFd = socket(AF_INET, SOCK_DGRAM, 0); + + if (networkFd == -1) + err(EXIT_FAILURE, "socket failed"); + + struct sockaddr_in address; + address.sin_family = AF_INET; + address.sin_port = htons(8080); + inet_pton(AF_INET, "10.148.10.109", &address.sin_addr); + + if (bind(networkFd, (struct sockaddr *)&address, sizeof(address)) == -1) { + close(networkFd); + err(EXIT_FAILURE, "bind failed"); + } +} + +void NetworkShutdown() { + close(networkFd); + net_cleanup(); +} + +void *RecvThread(void *arg) { + uint8_t buffer[PACKET_MAX_SIZE]; + struct sockaddr_in from; + socklen_t fromLen = sizeof(from); + + while (1) { + int size = recvfrom(networkFd, (char *)buffer, sizeof(buffer), 0, + (struct sockaddr *)&from, &fromLen); + + if (size <= 0) continue; + + PacketType type = *(PacketType *)buffer; + + // handle ping immediately without queuing + if (type == PACKET_PING) { + PingPacket pong = { + .type = PACKET_PONG, + .sentTime = ((PingPacket *)buffer)->sentTime + }; + sendto(networkFd, (const char*)&pong, sizeof(pong), 0, + (struct sockaddr *)&from, sizeof(from)); + continue; // don't queue + } + + + // figure out which player this came from + int playerId = FindPlayerByAddress(&from); + if (playerId == -1) { + // new connection - handle handshake + HandleNewConnection(&from, buffer, size); + } else { + // existing player - queue their packet + QueuePush(&incomingQueue, buffer, size); + } + } + return NULL; +} + +void *SendThread(void *arg) { + double previousTime = GetWallTime(); + double accumulator = 0; + + while (1) { + double currentTime = GetWallTime(); + double elapsed = currentTime - previousTime; + previousTime = currentTime; + accumulator += elapsed; + + while (accumulator >= NETWORK_RATE) { + // send snapshot to each connected client + for (int i = 0; i < MAX_PLAYERS; i++) { + if (!connections[i].connected) continue; + + // build custom snapshot for this player (nearby entities only) + ServerSnapshot snapshot = BuildSnapshotForPlayer(i); + snapshot.type = PACKET_SERVER_SNAPSHOT; + + sendto(networkFd, (const char *)&snapshot, sizeof(snapshot), 0, + (struct sockaddr *)&connections[i].address, + sizeof(connections[i].address)); + + } + accumulator -= NETWORK_RATE; + } + + sleep_us(1000); + /* // small sleep to avoid burning CPU */ + /* double sleepTime = NETWORK_RATE - */ + /* (GetWallTime() - previousTime); */ + /* if (sleepTime > 0) */ + /* sleep_us((useconds_t)(sleepTime * 1000000)); */ + } + return NULL; +} + +void *GameThread(void *arg) { + double previousTime = GetWallTime(); + double accumulator = 0; + + for (int i = 0; i < 2000; i++) { + float x = (float)GetRandomValue(-500, 500); + float z = (float)GetRandomValue(-500, 500); + float y = GetTerrainHeight(x, z); + + npcs[i].active = true; + npcs[i].model = i % 47; + npcs[i].position = (Vec3){x, y, z}; + npcs[i].spawnPosition = (Vec3){x, y, z}; + npcs[i].target = (Vec3){x, y, z}; + npcs[i].rotation = (float)GetRandomValue(0, 360); + npcs[i].scale = (float)GetRandomValue(8, 15) / 10.0f; + npcs[i].isMoving = false; + npcs[i].waitTimer = (float)GetRandomValue(0, 300) / 100.0f; // 0-3 sec initial wait + npcs[i].moveTimer = 0.0f; + } + + while (1) { + double currentTime = GetWallTime(); + double elapsed = currentTime - previousTime; + previousTime = currentTime; + accumulator += elapsed; + + while (accumulator >= TICK_RATE) { + ProcessIncoming(); + + for (int i = 0; i < MAX_NPCS; i++) { + if (npcs[i].active) + UpdateServerNPC(&npcs[i], TICK_RATE); + } + + CheckHeartbeats(); + accumulator -= TICK_RATE; + } + + sleep_us(1000); + } + return NULL; +} + +void ProcessIncoming() { + uint8_t buffer[PACKET_MAX_SIZE]; + int size; + + while (QueuePop(&incomingQueue, buffer, &size)) { + PacketType type = *(PacketType *)buffer; + + switch (type) { + case PACKET_CLIENT_INPUT: + HandleClientInput((ClientPacket *)buffer); + break; + /* case PACKET_HANDSHAKE: */ + /* HandleHandshake((HandshakePacket *)buffer); */ + /* break; */ + } + } +} + +void HandleClientInput(ClientPacket *packet) { + int id = packet->playerId; + if (!players[id].connected) return; + + // update last seen time + connections[id].lastHeartbeat = GetWallTime(); + + // simulate player movement with their inputs + UpdateServerPlayer(&players[id], packet->inputs, packet->yaw, (float)TICK_RATE, + terrainMesh, terrainTransform); +} + +int CompareNPCDist(const void *a, const void *b) { + float da = ((NPCDist*)a)->dist; + float db = ((NPCDist*)b)->dist; + return (da > db) - (da < db); +} + +ServerSnapshot BuildSnapshotForPlayer(int playerId) { + ServerSnapshot snapshot = {0}; + snapshot.type = PACKET_SERVER_SNAPSHOT; + Vec3 playerPos = players[playerId].position; + + // always add current player first - server is authoritative for their position + snapshot.players[0].id = players[playerId].id; + snapshot.players[0].position = players[playerId].position; + snapshot.players[0].yaw = players[playerId].yaw; + snapshot.players[0].velocity.x = players[playerId].velocity.x; + snapshot.players[0].velocity.y = players[playerId].velocity.y; + snapshot.players[0].velocity.z = players[playerId].velocity.z; + snapshot.players[0].onGround = players[playerId].onGround; + snapshot.playerCount = 1; + + + // nearby OTHER players + for (int i = 0; i < MAX_PLAYERS; i++) { + if (i == playerId) continue; // skip self, already added + if (!players[i].connected) continue; + if (snapshot.playerCount >= 100) break; + + float dist = Vec3Distance(players[i].position, playerPos); + if (dist < VISIBILITY_RANGE) { + snapshot.players[snapshot.playerCount].id = players[i].id; + snapshot.players[snapshot.playerCount].position = players[i].position; + snapshot.players[snapshot.playerCount].yaw = players[i].yaw; + snapshot.players[snapshot.playerCount].velocity.x = players[i].velocity.x; + snapshot.players[snapshot.playerCount].velocity.y = players[i].velocity.y; + snapshot.players[snapshot.playerCount].velocity.z = players[i].velocity.z; + snapshot.players[snapshot.playerCount].onGround = players[i].onGround; + snapshot.playerCount++; + } + } + + // nearby npcs + NPCDist visible[MAX_NPCS]; + int visibleCount = 0; + float cosFov = cosf((180.0f * 0.5f) * DEG2RAD); // 180° FOV + + Vector3 forward = { + sinf(players[playerId].yaw), + 0.0f, + cosf(players[playerId].yaw) + }; + + for (int i = 0; i < MAX_NPCS; i++) { + if (!npcs[i].active) continue; + + Vector3 toNPCVec = + Vector3Subtract(ToRaylib(npcs[i].position), ToRaylib(playerPos)); + + float dist = Vector3Length(toNPCVec); + if (dist > VISIBILITY_RANGE) + continue; + + Vector3 toNPC = Vector3Scale(toNPCVec, 1.0f / dist); // normalize + + float dot = Vector3DotProduct(forward, toNPC); + + // Only keep NPCs in front of player + if (dot >= cosFov) { + visible[visibleCount].index = i; + visible[visibleCount].dist = dist; + visibleCount++; + } + } + + qsort(visible, visibleCount, sizeof(NPCDist), CompareNPCDist); + + snapshot.npcCount = 0; + + for (int i = 0; i < visibleCount && i < MAX_VIEW_NPCS; i++) { + int idx = visible[i].index; + + snapshot.npcs[snapshot.npcCount].id = npcs[idx].id; + snapshot.npcs[snapshot.npcCount].position = npcs[idx].position; + snapshot.npcs[snapshot.npcCount].rotation = npcs[idx].rotation; + snapshot.npcs[snapshot.npcCount].health = npcs[idx].health; + snapshot.npcs[snapshot.npcCount].state = npcs[idx].state; + snapshot.npcs[snapshot.npcCount].scale = npcs[idx].scale; + snapshot.npcs[snapshot.npcCount].model = npcs[idx].model; + snapshot.npcCount++; + } + + return snapshot; +} + +void CheckHeartbeats() { + double now = GetWallTime(); + + for (int i = 0; i < MAX_PLAYERS; i++) { + if (!connections[i].connected) continue; + + if (now - connections[i].lastHeartbeat > TIMEOUT) { + // player timed out + connections[i].connected = false; + players[i].connected = false; + + // tell all other clients they left + PlayerJoinedPacket packet = { + .type = PACKET_PLAYER_LEFT, + .playerId = i, + }; + /* BroadcastToAllClients(&packet); */ + + TraceLog(LOG_INFO, "Player %d timed out", i); + } + } +} + +void UpdateServerPlayer(ServerPlayer *p, int inputs, float yaw, float dt, + Mesh terrainMesh, Matrix terrainTransform) { + p->yaw = yaw; + Vector3 forward = { sinf(p->yaw), 0, cosf(p->yaw) }; + Vector3 right = { cosf(p->yaw), 0, -sinf(p->yaw) }; + + if (inputs & INPUT_JUMP && p->onGround) { + p->velocity.y = 500.0f; + p->onGround = false; + } + + Vector3 moveDir = {0}; + if (inputs & INPUT_FORWARD) moveDir = Vector3Add(moveDir, forward); + if (inputs & INPUT_BACK) moveDir = Vector3Subtract(moveDir, forward); + if (inputs & INPUT_RIGHT) moveDir = Vector3Subtract(moveDir, right); + if (inputs & INPUT_LEFT) moveDir = Vector3Add(moveDir, right); + + if (Vector3Length(moveDir) > 0) + moveDir = Vector3Normalize(moveDir); + + if (inputs & INPUT_SPRINT) { + p->sprint = p->onGround ? 5 : 10; // match client exactly + } else { + p->sprint = 1; + } + p->position.x += moveDir.x * p->speed * p->sprint * dt; + p->position.z += moveDir.z * p->speed * p->sprint * dt; + + if (!p->onGround) p->velocity.y -= 300.0f * dt; + p->position.y += p->velocity.y * dt; + + // terrain collision + Vector3 rayOrigin = { p->position.x, p->position.y + 2000.0f, p->position.z }; + Ray downRay = { rayOrigin, (Vector3){0, -1, 0} }; + RayCollision hit = GetRayCollisionMesh(downRay, terrainMesh, terrainTransform); + if (hit.hit && p->position.y <= hit.point.y + 0.5f) { + p->position.y = hit.point.y; + p->velocity.y = 0; + p->onGround = true; + } else { + p->onGround = false; + } +} + +float GetTerrainHeight(float x, float z) { + Vector3 rayOrigin = { x, 99999.0f, z }; + Ray ray = { rayOrigin, (Vector3){0, -1, 0} }; + RayCollision hit = GetRayCollisionMesh(ray, terrainMesh, terrainTransform); + if (hit.hit) return hit.point.y; + return 0.0f; // fallback if miss +} + +void UpdateServerNPC(ServerNPC *npc, float dt) { + if (npc->isMoving) { + // walk toward target + Vector3 dir = Vector3Subtract(ToRaylib(npc->target), ToRaylib(npc->position)); + dir.y = 0; + float dist = Vector3Length(dir); + + if (dist > 1.0f) { + dir = Vector3Normalize(dir); + npc->position.x += dir.x * 20.0f * dt; // npc walk speed + npc->position.z += dir.z * 20.0f * dt; + + // snap to terrain height + npc->position.y = GetTerrainHeight( + npc->position.x, npc->position.z); + + // face direction of travel + npc->rotation = atan2f(dir.x, dir.z) * RAD2DEG; + } + + npc->moveTimer -= dt; + if (npc->moveTimer <= 0.0f || dist <= 1.0f) { + // stop walking, start waiting + npc->isMoving = false; + npc->waitTimer = (float)GetRandomValue(200, 500) / 100.0f; // wait 2-5 sec + } + + } else { + // waiting + npc->waitTimer -= dt; + if (npc->waitTimer <= 0.0f) { + // pick a new random target near spawn + float range = 100.0f; // how far they wander from spawn + float tx = npc->spawnPosition.x + (float)GetRandomValue(-range, range); + float tz = npc->spawnPosition.z + (float)GetRandomValue(-range, range); + npc->target = (Vec3){tx, 0, tz}; + npc->isMoving = true; + npc->moveTimer = (float)GetRandomValue(300, 800) / 100.0f; // walk 3-8 sec + } + } +} + +int FindPlayerByAddress(struct sockaddr_in *addr) { + for (int i = 0; i < MAX_PLAYERS; i++) { + if (!connections[i].connected) continue; + + if (connections[i].address.sin_addr.s_addr == addr->sin_addr.s_addr && + connections[i].address.sin_port == addr->sin_port) { + return i; + } + } + return -1; // not found +} + +void HandleNewConnection(struct sockaddr_in *addr, uint8_t *buffer, int size) { + PacketType type = *(PacketType *)buffer; + if (type != PACKET_HANDSHAKE) + return; + + ClientPacket *handshake = (ClientPacket *)buffer; + + // find empty slot + int slot = -1; + for (int i = 0; i < MAX_PLAYERS; i++) { + if (!connections[i].connected) { slot = i; break; } + } + if (slot == -1) { printf("Server full\n"); return; } + + // register connection + connections[slot].connected = true; + connections[slot].address = *addr; + connections[slot].lastHeartbeat = GetWallTime(); + + // init player + players[slot].id = slot; + players[slot].connected = true; + players[slot].speed = 30.0f; + players[slot].sprint = 1; + players[slot].onGround = false; + players[slot].yaw = 0; + players[slot].velocity = (Vec3){0, 0, 0}; + players[slot].characterClass = handshake->characterClass; + /* strncpy(players[slot].name, handshake->name, 32); */ + + printf("character class %d", players[slot].characterClass); + + float spawnY = GetTerrainHeight(600, -1200); + players[slot].position = (Vec3){600, spawnY, -1200}; + printf("Player %d connected: %s\n", slot, players[slot].name); + + // 1. send handshake response to new player + HandshakeResponsePacket response = { + .type = PACKET_HANDSHAKE_RESPONSE, + .playerId = slot, + .spawnX = players[slot].position.x, + .spawnY = players[slot].position.y, + .spawnZ = players[slot].position.z, + }; + sendto(networkFd, (const char *)&response, sizeof(response), 0, + (struct sockaddr *)addr, sizeof(*addr)); + + // 2. tell new player about ALL already connected players + for (int i = 0; i < MAX_PLAYERS; i++) { + if (i == slot) continue; + if (!players[i].connected) continue; + + PlayerJoinedPacket existing = { + .type = PACKET_PLAYER_JOINED, + .playerId = players[i].id, + .position = players[i].position, + .characterClass = players[i].characterClass + }; + strncpy(existing.name, players[i].name, 32); + + sendto(networkFd, (const char *)&existing, sizeof(existing), 0, + (struct sockaddr *)addr, sizeof(*addr)); // send to new player (addr not from) + printf("told new player %d about existing player %d\n", slot, i); + } + + // 3. tell ALL existing players about the new player + PlayerJoinedPacket joinPacket = { + .type = PACKET_PLAYER_JOINED, + .playerId = slot, + .position = players[slot].position, + .characterClass = players[slot].characterClass + }; + strncpy(joinPacket.name, players[slot].name, 32); + + for (int i = 0; i < MAX_PLAYERS; i++) { + if (i == slot) continue; + if (!connections[i].connected) continue; + sendto(networkFd, (const char *) &joinPacket, sizeof(joinPacket), 0, + (struct sockaddr *)&connections[i].address, + sizeof(connections[i].address)); + } +} diff --git a/server/server.h b/server/server.h new file mode 100644 index 0000000..fecc446 --- /dev/null +++ b/server/server.h @@ -0,0 +1,84 @@ +#pragma once + +#include "raylib.h" +#include <stdbool.h> +#include "shared.h" +#include "queue.h" +#include "net_compat.h" + +#define TIMEOUT 10.0 +#define VISIBILITY_RANGE 20000.0f + +static int networkFd = -1; +static struct sockaddr_in server; + +// server owns full NPC state including AI + +typedef struct { + int index; + float dist; +} NPCDist; + +typedef struct { + int id; + Vec3 position; + Vec3 spawnPosition; + Vec3 target; + float rotation; + float scale; + float moveTimer; + float waitTimer; + bool isMoving; + bool active; + int health; + int state; + int model; +} ServerNPC; + +// server tracks each connected player +typedef struct { + int id; + bool connected; + char name[32]; + Vec3 position; + Vec3 velocity; + float yaw; + bool onGround; + float speed; + int sprint; + double lastHeartbeat; + int characterClass; // 0=warrior, 1=mage etc +} ServerPlayer; + +typedef struct { + int playerId; + struct sockaddr_in address; // their IP and port + bool connected; + double lastHeartbeat; // detect disconnects +} ClientConnection; + +extern ClientConnection connections[MAX_PLAYERS]; +extern PacketQueue incomingQueue; +extern PacketQueue outgoingQueue; +extern Mesh terrainMesh; +extern Matrix terrainTransform; +extern ServerPlayer players[MAX_PLAYERS]; +extern ServerNPC npcs[MAX_NPCS]; +extern int playerCount; +extern int npcCount; + +void NetworkInit(); +void NetworkShutdown(); +void *RecvThread(void *arg); +void *SendThread(void *arg); +void *GameThread(void *arg); +void ProcessIncoming(); +void HandleClientInput(ClientPacket *packet); +ServerSnapshot BuildSnapshotForPlayer(int playerId); +void CheckHeartbeats(); +void UpdateServerPlayer(ServerPlayer *p, int inputs, float yaw, float dt, + Mesh terrainMesh, Matrix terrainTransform); +float GetTerrainHeight(float x, float z); +void UpdateServerNPC(ServerNPC *npc, float dt); +int FindPlayerByAddress(struct sockaddr_in *addr); +void HandleNewConnection(struct sockaddr_in *addr, uint8_t *buffer, int size); diff --git a/shared/net_compat.h b/shared/net_compat.h index 5e63e04..65fa46d 100644 --- a/shared/net_compat.h +++ b/shared/net_compat.h @@ -2,6 +2,9 @@ #pragma once #ifdef _WIN32 +#define WIN32_LEAN_AND_MEAN +#define NOGDI +#define NOUSER #include <winsock2.h> #include <ws2tcpip.h> #pragma comment(lib, "ws2_32.lib") @@ -41,13 +44,20 @@ ioctlsocket(s, FIONBIO, &mode); } + #include <windows.h> + static inline void sleep_us(int microseconds) { + Sleep(microseconds / 1000); // Windows uses milliseconds + } + #else #include <sys/socket.h> #include <netinet/in.h> #include <unistd.h> #include <err.h> #include <errno.h> - #include <fcntl.h> +#include <fcntl.h> +#include <arpa/inet.h> + static inline void net_init(void) {} // no-op on POSIX static inline void net_cleanup(void) {} // no-op on POSIX @@ -55,6 +65,10 @@ fcntl(s, F_SETFL, fcntl(s, F_GETFL) | O_NONBLOCK); } + static inline void sleep_us(int microseconds) { + usleep(microseconds); + } + static inline int net_errno(void) { return errno; } #define NET_EAGAIN EAGAIN #define NET_EINTR EINTR diff --git a/shared/queue.c b/shared/queue.c new file mode 100644 index 0000000..86e743f --- /dev/null +++ b/shared/queue.c @@ -0,0 +1,29 @@ +#include "queue.h" +#include <string.h> + +void QueueInit(PacketQueue *q) { + q->head = 0; + q->tail = 0; + pthread_mutex_init(&q->lock, NULL); +} + +void QueuePush(PacketQueue *q, void *data, int size) { + pthread_mutex_lock(&q->lock); + memcpy(q->data[q->tail], data, size); + q->size[q->tail] = size; + q->tail = (q->tail + 1) % QUEUE_SIZE; + pthread_mutex_unlock(&q->lock); +} + +bool QueuePop(PacketQueue *q, void *data, int *size) { + pthread_mutex_lock(&q->lock); + if (q->head == q->tail) { + pthread_mutex_unlock(&q->lock); + return false; + } + memcpy(data, q->data[q->head], q->size[q->head]); + *size = q->size[q->head]; + q->head = (q->head + 1) % QUEUE_SIZE; + pthread_mutex_unlock(&q->lock); + return true; +} diff --git a/shared/queue.h b/shared/queue.h new file mode 100644 index 0000000..8a6655c --- /dev/null +++ b/shared/queue.h @@ -0,0 +1,20 @@ +#pragma once + +#include <pthread.h> +#include <stdint.h> +#include <stdbool.h> + +#define QUEUE_SIZE 256 +#define PACKET_MAX_SIZE 2048 + +typedef struct { + uint8_t data[QUEUE_SIZE][PACKET_MAX_SIZE]; + int size[QUEUE_SIZE]; + int head; + int tail; + pthread_mutex_t lock; +} PacketQueue; + +void QueueInit(PacketQueue *q); +void QueuePush(PacketQueue *q, void *data, int size); +bool QueuePop(PacketQueue *q, void *data, int *size); diff --git a/shared/shared.h b/shared/shared.h index e69de29..172438f 100644 --- a/shared/shared.h +++ b/shared/shared.h @@ -0,0 +1,121 @@ +#pragma once +#include <time.h> +#include <math.h> +#include <stdbool.h> + +// inputs bitmask +#define INPUT_FORWARD (1 << 0) +#define INPUT_BACK (1 << 1) +#define INPUT_LEFT (1 << 2) +#define INPUT_RIGHT (1 << 3) +#define INPUT_JUMP (1 << 4) +#define INPUT_SPRINT (1 << 5) + +#define HZ 20.0 +#define NETWORK_RATE (1.0 / HZ) +#define TICK_RATE (1.0 / HZ) +#define MAX_PLAYERS 16 +#define MAX_VIEW_NPCS 32 +#define MAX_NPCS 2000 + +typedef struct { + float x; + float y; + float z; +} Vec3; + +typedef enum { + PACKET_CLIENT_INPUT, + PACKET_SERVER_SNAPSHOT, + PACKET_PLAYER_JOINED, + PACKET_PLAYER_LEFT, + PACKET_HANDSHAKE, + PACKET_HANDSHAKE_RESPONSE, + PACKET_WORLD_EVENT, + PACKET_PING, + PACKET_PONG +} PacketType; + +typedef struct { + PacketType type; + double sentTime; +} PingPacket; + +// client → server +typedef struct { + PacketType type; + int playerId; + int inputs; + float yaw; + float dt; + int characterClass; // 0=warrior, 1=mage etc +} ClientPacket; + +typedef struct { + PacketType type; + int playerId; + char name[32]; + int characterClass; // 0=warrior, 1=mage etc + Vec3 position; +} PlayerJoinedPacket; + +// server → client (full world snapshot) +typedef struct { + PacketType type; + + int playerCount; + struct { + int id; + Vec3 position; + Vec3 velocity; // add this + float yaw; + int animationState; + bool onGround; + } players[MAX_PLAYERS]; + + int npcCount; + struct { + int id; + Vec3 position; + float rotation; + int state; // IDLE, WALKING, COMBAT + int health; + int scale; + int model; + } npcs[MAX_VIEW_NPCS]; + + int eventCount; + struct { + int objectId; + int eventType; // OBJECT_REMOVED, DOOR_OPENED etc + Vec3 position; + } events[8]; + +} ServerSnapshot; + +typedef struct { + PacketType type; + char name[32]; + int characterClass; // 0=warrior, 1=mage etc +} HandshakePacket; + +typedef struct { + PacketType type; + int playerId; + float spawnX; + float spawnY; + float spawnZ; +} HandshakeResponsePacket; + +static inline float Vec3Distance(Vec3 a, Vec3 b) { + float dx = a.x - b.x; + float dy = a.y - b.y; + float dz = a.z - b.z; + return sqrtf(dx*dx + dy*dy + dz*dz); +} + +static inline double GetWallTime() { + struct timespec ts; + clock_gettime(CLOCK_MONOTONIC, &ts); + return ts.tv_sec + ts.tv_nsec / 1e9; +} |