diff options
Diffstat (limited to 'server/server.c')
| -rw-r--r-- | server/server.c | 504 |
1 files changed, 504 insertions, 0 deletions
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)); + } +} |