summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorDevyn Challman <devyn@challman.org>2026-04-18 17:08:21 -0700
committerDevyn Challman <devyn@challman.org>2026-04-18 17:08:21 -0700
commit71ac9c0d75d405a61c4985e644d1d5e24debff6d (patch)
treeeb37f0f34618d154d48252d3a178941a69d33e4a
parent025f9e632153c095cb24505c4163722d823a87ad (diff)
improve server client sync and move npc to server side
-rw-r--r--Makefile33
-rw-r--r--client/client.c257
-rw-r--r--client/client.h81
-rw-r--r--client/main.c843
-rw-r--r--client/net/net.c26
-rw-r--r--client/net/net.h4
-rw-r--r--server/main.c72
-rw-r--r--server/server.c504
-rw-r--r--server/server.h84
-rw-r--r--shared/net_compat.h16
-rw-r--r--shared/queue.c29
-rw-r--r--shared/queue.h20
-rw-r--r--shared/shared.h121
13 files changed, 1994 insertions, 96 deletions
diff --git a/Makefile b/Makefile
index e3d8520..84b2873 100644
--- a/Makefile
+++ b/Makefile
@@ -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;
+}