diff --git a/game/client.c b/game/client.c new file mode 100644 index 0000000..42833be --- /dev/null +++ b/game/client.c @@ -0,0 +1,704 @@ +// client.c - raylib client (C99) +// Features: username prompt, inverted A/D, inverted pitch, recoil, crosshair +// widening, spray, inventory UI toggle on E, pick/drop/use items, renders +// dropped items from server. Build (Linux example): +// gcc client.c -O2 -o client -lraylib -lm -lpthread -ldl -lrt -lX11 + +#include "raylib.h" +#include +#include +#include +#include + +#ifdef _WIN32 +#define WIN32_LEAN_AND_MEAN +#include +#include +#pragma comment(lib, "ws2_32.lib") +typedef int socklen_t; +#define CLOSESOCK closesocket +#else +#include +#include +#include +#include +#include +#define CLOSESOCK close +#endif + +#define PROTOCOL_VERSION 4 +#define SERVER_PORT 27015 +#define MAX_PLAYERS 16 +#define USERNAME_MAX 16 + +#define WEAPON_PISTOL 0 +#define WEAPON_RIFLE 1 +#define WEAPON_COUNT 2 + +#define INV_SLOTS 6 +#define MAX_ITEMS 64 + +// Inventory item types (must match server) +#define ITEM_NONE 0 +#define ITEM_MEDKIT 1 +#define ITEM_ARMORPLATE 2 +#define ITEM_AMMO_PISTOL 3 +#define ITEM_AMMO_RIFLE 4 + +// MsgInput.buttons bits +#define BTN_RELOAD (1u << 0) +#define BTN_WPN1 (1u << 1) +#define BTN_WPN2 (1u << 2) +#define BTN_PICK (1u << 3) +#define BTN_DROP (1u << 4) +#define BTN_USE (1u << 5) + +static void set_nonblocking(int sock) { +#ifdef _WIN32 + u_long mode = 1; + ioctlsocket(sock, FIONBIO, &mode); +#else + int flags = fcntl(sock, F_GETFL, 0); + fcntl(sock, F_SETFL, flags | O_NONBLOCK); +#endif +} + +#pragma pack(push, 1) +typedef enum MsgType : uint8_t { + MSG_HELLO = 1, + MSG_WELCOME = 2, + MSG_INPUT = 3, + MSG_SNAPSHOT = 4, + MSG_SHOOT = 5, + MSG_HIT = 6, + MSG_ITEMS = 7 +} MsgType; + +typedef struct MsgHello { + uint8_t type; + uint32_t protocol; + char username[USERNAME_MAX]; +} MsgHello; + +typedef struct MsgWelcome { + uint8_t type; + uint8_t playerId; + uint32_t serverTick; +} MsgWelcome; + +typedef struct MsgInput { + uint8_t type; + uint8_t playerId; + uint32_t clientTick; + float moveX; + float moveZ; + float yaw; + float pitch; + uint8_t buttons; // BTN_* + uint8_t slotIndex; // 0..INV_SLOTS-1 (for DROP/USE) +} MsgInput; + +typedef struct MsgShoot { + uint8_t type; + uint8_t playerId; + uint32_t clientTick; +} MsgShoot; + +typedef struct PlayerStateNet { + uint8_t id; + uint8_t alive; + int16_t hp; + int16_t armor; // NEW + float x, y, z; + float yaw, pitch; + + uint8_t weapon; + int16_t ammoMag[WEAPON_COUNT]; + int16_t ammoRes[WEAPON_COUNT]; + + uint8_t invType[INV_SLOTS]; + int16_t invQty[INV_SLOTS]; + + char username[USERNAME_MAX]; +} PlayerStateNet; + +typedef struct MsgSnapshot { + uint8_t type; + uint32_t serverTick; + uint8_t count; + PlayerStateNet p[MAX_PLAYERS]; +} MsgSnapshot; + +typedef struct MsgHit { + uint8_t type; + uint8_t shooterId; + uint8_t victimId; + int16_t victimHp; +} MsgHit; + +typedef struct ItemNet { + uint16_t id; + uint8_t type; + int16_t qty; + float x, y, z; +} ItemNet; + +typedef struct MsgItems { + uint8_t type; + uint32_t serverTick; + uint8_t count; + ItemNet items[MAX_ITEMS]; +} MsgItems; +#pragma pack(pop) + +typedef struct RemotePlayer { + int present; + int alive; + int hp; + int armor; + Vector3 pos; + float yaw, pitch; + + uint8_t weapon; + int16_t ammoMag[WEAPON_COUNT]; + int16_t ammoRes[WEAPON_COUNT]; + + uint8_t invType[INV_SLOTS]; + int16_t invQty[INV_SLOTS]; + + char username[USERNAME_MAX]; +} RemotePlayer; + +typedef struct WorldItem { + int present; + uint16_t id; + uint8_t type; + int16_t qty; + Vector3 pos; +} WorldItem; + +static Vector3 v3(float x, float y, float z) { + Vector3 v = {x, y, z}; + return v; +} +static Vector3 v3_add(Vector3 a, Vector3 b) { + return v3(a.x + b.x, a.y + b.y, a.z + b.z); +} +static Vector3 v3_sub(Vector3 a, Vector3 b) { + return v3(a.x - b.x, a.y - b.y, a.z - b.z); +} +static float v3_dot(Vector3 a, Vector3 b) { + return a.x * b.x + a.y * b.y + a.z * b.z; +} +static float v3_len(Vector3 a) { + return sqrtf(a.x * a.x + a.y * a.y + a.z * a.z); +} +static Vector3 v3_scale(Vector3 a, float s) { + return v3(a.x * s, a.y * s, a.z * s); +} +static Vector3 v3_norm(Vector3 a) { + float l = v3_len(a); + if (l <= 1e-6f) + return v3(0, 0, 1); + return v3_scale(a, 1.0f / l); +} + +static const char *ItemName(uint8_t t) { + switch (t) { + case ITEM_MEDKIT: + return "Medkit"; + case ITEM_ARMORPLATE: + return "Armor plate"; + case ITEM_AMMO_PISTOL: + return "Pistol ammo"; + case ITEM_AMMO_RIFLE: + return "Rifle ammo"; + default: + return "-"; + } +} + +// Simple username input (ASCII-ish) +static void ui_username_prompt(char outName[USERNAME_MAX]) { + memset(outName, 0, USERNAME_MAX); + while (!WindowShouldClose()) { + int ch = GetCharPressed(); + while (ch > 0) { + int len = (int)strlen(outName); + if (ch >= 32 && ch <= 126) { + if (len < USERNAME_MAX - 1) { + outName[len] = (char)ch; + outName[len + 1] = '\0'; + } + } + ch = GetCharPressed(); + } + if (IsKeyPressed(KEY_BACKSPACE)) { + int len = (int)strlen(outName); + if (len > 0) + outName[len - 1] = '\0'; + } + if (IsKeyPressed(KEY_ENTER) && strlen(outName) > 0) + return; + + BeginDrawing(); + ClearBackground((Color){20, 24, 32, 255}); + DrawText("Enter username (press ENTER):", 60, 80, 28, RAYWHITE); + DrawRectangle(60, 130, 420, 48, (Color){40, 48, 64, 255}); + DrawRectangleLines(60, 130, 420, 48, (Color){120, 140, 170, 255}); + DrawText(outName[0] ? outName : "_", 72, 142, 24, + (Color){230, 230, 240, 255}); + EndDrawing(); + } +} + +int main(void) { +#ifdef _WIN32 + WSADATA wsa; + WSAStartup(MAKEWORD(2, 2), &wsa); +#endif + + const int sw = 1280, sh = 720; + InitWindow(sw, sh, "Voxel Shooter - Client"); + SetTargetFPS(120); + + char myName[USERNAME_MAX]; + ui_username_prompt(myName); + + DisableCursor(); + + int sock = (int)socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP); + if (sock < 0) { + perror("socket"); + return 1; + } + set_nonblocking(sock); + + struct sockaddr_in srv = {0}; + srv.sin_family = AF_INET; + srv.sin_port = htons(SERVER_PORT); + inet_pton(AF_INET, "127.0.0.1", &srv.sin_addr); + + uint8_t myId = 255; + uint32_t clientTick = 0; + + RemotePlayer rp[MAX_PLAYERS] = {0}; + WorldItem wi[MAX_ITEMS] = {0}; + + // view/camera + Vector3 camPos = v3(0, 1.7f, 6); + float yaw = 0.0f; + float pitch = 0.0f; + + // recoil/crosshair + float recoilYaw = 0.0f, recoilPitch = 0.0f; + float crossSpread = 0.0f; + + // firing + float fireCooldown = 0.0f; + int shotsInBurst = 0; + float burstResetTimer = 0.0f; + + // inventory UI + int invOpen = 0; + int selectedSlot = 0; + + // desired weapon + int desiredWeapon = WEAPON_RIFLE; + + // tuning + const float pistolFireRate = 4.0f; + const float rifleFireRate = 12.0f; + const float recoilReturn = 18.0f; + const float crossReturn = 14.0f; + + const float pistolKickPitch = 0.010f; + const float pistolKickYaw = 0.004f; + const float rifleKickPitch = 0.018f; + const float rifleKickYaw = 0.010f; + + const float pistolCrossKick = 2.0f; + const float rifleCrossKick = 4.0f; + + const float pistolSprayGrow = 0.4f; + const float rifleSprayGrow = 1.2f; + + const float burstResetTime = 0.18f; + + // HELLO + MsgHello hello = {0}; + hello.type = MSG_HELLO; + hello.protocol = PROTOCOL_VERSION; + strncpy(hello.username, myName, USERNAME_MAX - 1); + sendto(sock, (const char *)&hello, (int)sizeof(hello), 0, + (const struct sockaddr *)&srv, sizeof(srv)); + + while (!WindowShouldClose()) { + clientTick++; + float dt = GetFrameTime(); + + // timers + fireCooldown -= dt; + if (fireCooldown < 0.0f) + fireCooldown = 0.0f; + burstResetTimer -= dt; + if (burstResetTimer <= 0.0f) + shotsInBurst = 0; + + // recoil return + { + float k = 1.0f - expf(-recoilReturn * dt); + recoilYaw += (0.0f - recoilYaw) * k; + recoilPitch += (0.0f - recoilPitch) * k; + } + // crosshair return + { + float k = 1.0f - expf(-crossReturn * dt); + crossSpread += (0.0f - crossSpread) * k; + if (crossSpread < 0.01f) + crossSpread = 0.0f; + } + + // receive packets + for (;;) { + uint8_t buf[1400]; + struct sockaddr_in from = {0}; + socklen_t fromLen = sizeof(from); + int n = (int)recvfrom(sock, (char *)buf, (int)sizeof(buf), 0, + (struct sockaddr *)&from, &fromLen); + if (n <= 0) { +#ifdef _WIN32 + if (WSAGetLastError() == WSAEWOULDBLOCK) + break; +#else + if (errno == EWOULDBLOCK || errno == EAGAIN) + break; +#endif + break; + } + + uint8_t type = buf[0]; + if (type == MSG_WELCOME && n >= (int)sizeof(MsgWelcome)) { + MsgWelcome *w = (MsgWelcome *)buf; + myId = w->playerId; + } else if (type == MSG_SNAPSHOT && n >= (int)sizeof(MsgSnapshot)) { + MsgSnapshot *s = (MsgSnapshot *)buf; + for (int i = 0; i < MAX_PLAYERS; i++) + rp[i].present = 0; + + for (int i = 0; i < (int)s->count && i < MAX_PLAYERS; i++) { + PlayerStateNet *ps = &s->p[i]; + if (ps->id >= MAX_PLAYERS) + continue; + + RemotePlayer *p = &rp[ps->id]; + p->present = 1; + p->alive = ps->alive; + p->hp = ps->hp; + p->armor = ps->armor; + p->pos = v3(ps->x, ps->y, ps->z); + p->yaw = ps->yaw; + p->pitch = ps->pitch; + p->weapon = ps->weapon; + + for (int w = 0; w < WEAPON_COUNT; w++) { + p->ammoMag[w] = ps->ammoMag[w]; + p->ammoRes[w] = ps->ammoRes[w]; + } + for (int sidx = 0; sidx < INV_SLOTS; sidx++) { + p->invType[sidx] = ps->invType[sidx]; + p->invQty[sidx] = ps->invQty[sidx]; + } + + memset(p->username, 0, USERNAME_MAX); + strncpy(p->username, ps->username, USERNAME_MAX - 1); + } + + if (myId != 255 && rp[myId].present) { + Vector3 body = rp[myId].pos; + camPos.x = body.x; + camPos.z = body.z + 0.0001f; + desiredWeapon = rp[myId].weapon; + } + } else if (type == MSG_ITEMS && n >= (int)sizeof(MsgItems)) { + MsgItems *m = (MsgItems *)buf; + for (int i = 0; i < MAX_ITEMS; i++) + wi[i].present = 0; + + for (int i = 0; i < (int)m->count && i < MAX_ITEMS; i++) { + wi[i].present = 1; + wi[i].id = m->items[i].id; + wi[i].type = m->items[i].type; + wi[i].qty = m->items[i].qty; + wi[i].pos = v3(m->items[i].x, m->items[i].y, m->items[i].z); + } + } + } + + // toggle inventory UI on E + if (IsKeyPressed(KEY_E)) + invOpen = !invOpen; + + // select slot 1..6 + if (IsKeyPressed(KEY_ONE)) + selectedSlot = 0; + if (IsKeyPressed(KEY_TWO)) + selectedSlot = 1; + if (IsKeyPressed(KEY_THREE)) + selectedSlot = 2; + if (IsKeyPressed(KEY_FOUR)) + selectedSlot = 3; + if (IsKeyPressed(KEY_FIVE)) + selectedSlot = 4; + if (IsKeyPressed(KEY_SIX)) + selectedSlot = 5; + + // mouse look: yaw fixed, pitch inverted + Vector2 md = GetMouseDelta(); + const float sens = 0.0025f; + yaw -= md.x * sens; + pitch -= md.y * sens; + if (pitch < -1.5f) + pitch = -1.5f; + if (pitch > 1.5f) + pitch = 1.5f; + + // apply recoil to view + float viewYaw = yaw + recoilYaw; + float viewPitch = pitch + recoilPitch; + if (viewPitch < -1.5f) + viewPitch = -1.5f; + if (viewPitch > 1.5f) + viewPitch = 1.5f; + + // movement (A/D inverted) + float moveX = 0.0f, moveZ = 0.0f; + if (IsKeyDown(KEY_A)) + moveX += 1.0f; + if (IsKeyDown(KEY_D)) + moveX -= 1.0f; + if (IsKeyDown(KEY_W)) + moveZ += 1.0f; + if (IsKeyDown(KEY_S)) + moveZ -= 1.0f; + + // input buttons + uint8_t buttons = 0; + + // weapon switch (separate from slot select) + if (IsKeyPressed(KEY_Z)) { + desiredWeapon = WEAPON_PISTOL; + buttons |= BTN_WPN1; + } + if (IsKeyPressed(KEY_X)) { + desiredWeapon = WEAPON_RIFLE; + buttons |= BTN_WPN2; + } + + if (IsKeyPressed(KEY_R)) + buttons |= BTN_RELOAD; + if (IsKeyPressed(KEY_F)) + buttons |= BTN_PICK; + if (IsKeyPressed(KEY_G)) + buttons |= BTN_DROP; + if (IsKeyPressed(KEY_C)) + buttons |= BTN_USE; // use armor/medkit depending on selected slot + if (IsKeyPressed(KEY_V)) + buttons |= + BTN_USE; // alternate use key (also triggers, server uses slot type) + + // send INPUT + if (myId != 255) { + MsgInput in = {0}; + in.type = MSG_INPUT; + in.playerId = myId; + in.clientTick = clientTick; + in.moveX = moveX; + in.moveZ = moveZ; + in.yaw = viewYaw; + in.pitch = viewPitch; + in.buttons = buttons; + in.slotIndex = (uint8_t)selectedSlot; + sendto(sock, (const char *)&in, (int)sizeof(in), 0, + (const struct sockaddr *)&srv, sizeof(srv)); + } + + // shoot (hold LMB) + if (myId != 255 && IsMouseButtonDown(MOUSE_BUTTON_LEFT) && + fireCooldown <= 0.0f) { + float rate = + (desiredWeapon == WEAPON_PISTOL) ? pistolFireRate : rifleFireRate; + float interval = 1.0f / rate; + fireCooldown = interval; + + shotsInBurst++; + burstResetTimer = burstResetTime; + + if (desiredWeapon == WEAPON_PISTOL) { + recoilPitch += pistolKickPitch; + recoilYaw += + (((float)GetRandomValue(-1000, 1000)) / 1000.0f) * pistolKickYaw; + crossSpread += pistolCrossKick + shotsInBurst * pistolSprayGrow; + } else { + recoilPitch += rifleKickPitch; + recoilYaw += + (((float)GetRandomValue(-1000, 1000)) / 1000.0f) * rifleKickYaw; + crossSpread += rifleCrossKick + shotsInBurst * rifleSprayGrow; + } + + MsgShoot shmsg = {0}; + shmsg.type = MSG_SHOOT; + shmsg.playerId = myId; + shmsg.clientTick = clientTick; + sendto(sock, (const char *)&shmsg, (int)sizeof(shmsg), 0, + (const struct sockaddr *)&srv, sizeof(srv)); + } + + // camera + Vector3 forward = v3(sinf(viewYaw) * cosf(viewPitch), sinf(viewPitch), + cosf(viewYaw) * cosf(viewPitch)); + + Camera3D cam = {0}; + cam.position = camPos; + cam.target = v3_add(camPos, forward); + cam.up = v3(0, 1, 0); + cam.fovy = 75.0f; + cam.projection = CAMERA_PERSPECTIVE; + + BeginDrawing(); + ClearBackground((Color){20, 24, 32, 255}); + + BeginMode3D(cam); + + // ground + for (int z = -16; z <= 16; z++) { + for (int x = -16; x <= 16; x++) { + DrawCubeV(v3((float)x, 0.0f, (float)z), v3(1, 1, 1), + (Color){50, 60, 75, 255}); + } + } + + // blocks + DrawCubeV(v3(2, 1, 2), v3(1, 1, 1), (Color){120, 90, 60, 255}); + DrawCubeV(v3(2, 2, 2), v3(1, 1, 1), (Color){120, 90, 60, 255}); + DrawCubeV(v3(-3, 1, -1), v3(1, 1, 1), (Color){90, 120, 60, 255}); + DrawCubeV(v3(0, 1, 4), v3(2, 1, 2), (Color){90, 90, 130, 255}); + + // items (dropped/pickups) + for (int i = 0; i < MAX_ITEMS; i++) { + if (!wi[i].present) + continue; + Color ic = (Color){220, 220, 220, 255}; + if (wi[i].type == ITEM_MEDKIT) + ic = (Color){120, 255, 120, 255}; + if (wi[i].type == ITEM_ARMORPLATE) + ic = (Color){120, 180, 255, 255}; + if (wi[i].type == ITEM_AMMO_PISTOL) + ic = (Color){255, 220, 120, 255}; + if (wi[i].type == ITEM_AMMO_RIFLE) + ic = (Color){255, 180, 120, 255}; + DrawCubeV(v3(wi[i].pos.x, wi[i].pos.y + 0.15f, wi[i].pos.z), + v3(0.3f, 0.3f, 0.3f), ic); + } + + // players + for (int i = 0; i < MAX_PLAYERS; i++) { + if (!rp[i].present) + continue; + Vector3 p = rp[i].pos; + Color c = + (i == myId) ? (Color){80, 180, 255, 255} : (Color){255, 80, 80, 255}; + if (!rp[i].alive) + c = (Color){120, 120, 120, 255}; + DrawCapsule(v3(p.x, p.y - 0.5f, p.z), v3(p.x, p.y + 0.5f, p.z), 0.35f, 8, + 8, c); + } + + EndMode3D(); + + // nameplates + for (int i = 0; i < MAX_PLAYERS; i++) { + if (!rp[i].present) + continue; + if (!rp[i].username[0]) + continue; + + Vector3 head = v3(rp[i].pos.x, rp[i].pos.y + 1.2f, rp[i].pos.z); + Vector3 camForward = v3_norm(v3_sub(cam.target, cam.position)); + Vector3 toHead = v3_sub(head, cam.position); + if (v3_dot(camForward, toHead) <= 0.0f) + continue; + + Vector2 s = GetWorldToScreen(head, cam); + if (s.x < -200 || s.x > sw + 200 || s.y < -200 || s.y > sh + 200) + continue; + + int fontSize = 18; + int w = MeasureText(rp[i].username, fontSize); + Color tc = (i == myId) ? (Color){180, 230, 255, 255} : RAYWHITE; + DrawText(rp[i].username, (int)(s.x - w / 2), (int)(s.y - fontSize), + fontSize, tc); + } + + // HUD + if (myId == 255 || !rp[myId].present) { + DrawText("Connecting...", 10, 10, 20, RAYWHITE); + } else { + const char *wname = + (rp[myId].weapon == WEAPON_PISTOL) ? "PISTOL" : "RIFLE"; + DrawText(TextFormat("Name: %s | HP: %d | Armor: %d | Weapon: %s", myName, + rp[myId].hp, rp[myId].armor, wname), + 10, 10, 20, RAYWHITE); + + DrawText(TextFormat("Ammo: P %d/%d | R %d/%d (E inv, F pick, G drop, " + "C/V use, R reload, Z/X switch)", + rp[myId].ammoMag[WEAPON_PISTOL], + rp[myId].ammoRes[WEAPON_PISTOL], + rp[myId].ammoMag[WEAPON_RIFLE], + rp[myId].ammoRes[WEAPON_RIFLE]), + 10, 36, 18, (Color){200, 200, 210, 255}); + + if (invOpen) { + DrawRectangle(20, 70, 420, 190, (Color){10, 10, 10, 180}); + DrawRectangleLines(20, 70, 420, 190, (Color){200, 200, 210, 210}); + DrawText("Inventory (slots 1-6)", 32, 80, 22, RAYWHITE); + + for (int sidx = 0; sidx < INV_SLOTS; sidx++) { + Color sc = (sidx == selectedSlot) ? (Color){80, 180, 255, 255} + : (Color){220, 220, 230, 255}; + const char *nm = ItemName(rp[myId].invType[sidx]); + int qty = rp[myId].invQty[sidx]; + DrawText(TextFormat("[%d] %s x%d", sidx + 1, nm, qty), 32, + 110 + sidx * 22, 18, sc); + } + DrawText("Use: C or V | Drop: G | Pick: F", 32, + 110 + INV_SLOTS * 22 + 6, 18, (Color){210, 210, 220, 255}); + } + } + + DrawFPS(sw - 90, 10); + + // crosshair + { + int cx = sw / 2, cy = sh / 2; + int gap = 6 + (int)crossSpread; + int len = 10, thick = 2; + Color col = (Color){240, 240, 245, 220}; + + DrawRectangle(cx - gap - len, cy - thick / 2, len, thick, col); + DrawRectangle(cx + gap, cy - thick / 2, len, thick, col); + DrawRectangle(cx - thick / 2, cy - gap - len, thick, len, col); + DrawRectangle(cx - thick / 2, cy + gap, thick, len, col); + DrawCircleLines(cx, cy, 3.0f, (Color){240, 240, 245, 160}); + } + + EndDrawing(); + } + + CloseWindow(); + CLOSESOCK(sock); +#ifdef _WIN32 + WSACleanup(); +#endif + return 0; +} diff --git a/game/output.md b/game/output.md new file mode 100644 index 0000000..e69de29 diff --git a/game/server.c b/game/server.c new file mode 100644 index 0000000..9b2e6f7 --- /dev/null +++ b/game/server.c @@ -0,0 +1,971 @@ +// server.c - UDP authoritative server + ODE physics + ODE raycast bullets (C99) +// Adds: true spray, inventory capacity (6 slots), world pickups, drop/pick/use, +// armor+medkits. Build (Linux example): gcc server.c -O2 -o server -lode -lm + +#include +#include +#include +#include +#include + +#include + +#ifdef _WIN32 +#define WIN32_LEAN_AND_MEAN +#include +#include +#pragma comment(lib, "ws2_32.lib") +typedef int socklen_t; +#define CLOSESOCK closesocket +#else +#include +#include +#include +#include +#include +#define CLOSESOCK close +#endif + +#define PROTOCOL_VERSION 4 +#define SERVER_PORT 27015 +#define MAX_PLAYERS 16 +#define USERNAME_MAX 16 + +#define TICK_HZ 60 +#define DT (1.0f / (float)TICK_HZ) + +#define WEAPON_PISTOL 0 +#define WEAPON_RIFLE 1 +#define WEAPON_COUNT 2 + +#define INV_SLOTS 6 +#define MAX_ITEMS 64 + +// inventory item types +#define ITEM_NONE 0 +#define ITEM_MEDKIT 1 +#define ITEM_ARMORPLATE 2 +#define ITEM_AMMO_PISTOL 3 +#define ITEM_AMMO_RIFLE 4 + +// MsgInput.buttons bits +#define BTN_RELOAD (1u << 0) +#define BTN_WPN1 (1u << 1) +#define BTN_WPN2 (1u << 2) +#define BTN_PICK (1u << 3) +#define BTN_DROP (1u << 4) +#define BTN_USE (1u << 5) + +// Collision categories +#define CAT_WORLD (1u << 0) +#define CAT_PLAYER (1u << 1) +#define CAT_ITEM (1u << 2) + +static double now_seconds(void) { +#ifdef _WIN32 + static LARGE_INTEGER freq; + static int init = 0; + LARGE_INTEGER t; + if (!init) { + QueryPerformanceFrequency(&freq); + init = 1; + } + QueryPerformanceCounter(&t); + return (double)t.QuadPart / (double)freq.QuadPart; +#else + struct timespec ts; + clock_gettime(CLOCK_MONOTONIC, &ts); + return (double)ts.tv_sec + (double)ts.tv_nsec / 1e9; +#endif +} + +static void set_nonblocking(int sock) { +#ifdef _WIN32 + u_long mode = 1; + ioctlsocket(sock, FIONBIO, &mode); +#else + int flags = fcntl(sock, F_GETFL, 0); + fcntl(sock, F_SETFL, flags | O_NONBLOCK); +#endif +} + +#pragma pack(push, 1) +typedef enum MsgType : uint8_t { + MSG_HELLO = 1, + MSG_WELCOME = 2, + MSG_INPUT = 3, + MSG_SNAPSHOT = 4, + MSG_SHOOT = 5, + MSG_HIT = 6, + MSG_ITEMS = 7 +} MsgType; + +typedef struct MsgHello { + uint8_t type; + uint32_t protocol; + char username[USERNAME_MAX]; +} MsgHello; + +typedef struct MsgWelcome { + uint8_t type; + uint8_t playerId; + uint32_t serverTick; +} MsgWelcome; + +typedef struct MsgInput { + uint8_t type; + uint8_t playerId; + uint32_t clientTick; + float moveX; + float moveZ; + float yaw; + float pitch; + uint8_t buttons; + uint8_t slotIndex; +} MsgInput; + +typedef struct MsgShoot { + uint8_t type; + uint8_t playerId; + uint32_t clientTick; +} MsgShoot; + +typedef struct PlayerStateNet { + uint8_t id; + uint8_t alive; + int16_t hp; + int16_t armor; + float x, y, z; + float yaw, pitch; + + uint8_t weapon; + int16_t ammoMag[WEAPON_COUNT]; + int16_t ammoRes[WEAPON_COUNT]; + + uint8_t invType[INV_SLOTS]; + int16_t invQty[INV_SLOTS]; + + char username[USERNAME_MAX]; +} PlayerStateNet; + +typedef struct MsgSnapshot { + uint8_t type; + uint32_t serverTick; + uint8_t count; + PlayerStateNet p[MAX_PLAYERS]; +} MsgSnapshot; + +typedef struct MsgHit { + uint8_t type; + uint8_t shooterId; + uint8_t victimId; + int16_t victimHp; +} MsgHit; + +typedef struct ItemNet { + uint16_t id; + uint8_t type; + int16_t qty; + float x, y, z; +} ItemNet; + +typedef struct MsgItems { + uint8_t type; + uint32_t serverTick; + uint8_t count; + ItemNet items[MAX_ITEMS]; +} MsgItems; +#pragma pack(pop) + +typedef struct Item { + int active; + uint16_t id; + uint8_t type; + int qty; + float x, y, z; + dGeomID geom; // static pickup geom (no body) +} Item; + +typedef struct Player { + int inUse; + struct sockaddr_in addr; + double lastHeard; + + char username[USERNAME_MAX]; + + float moveX, moveZ; + float yaw, pitch; + + int alive; + int hp; + int armor; + + int weapon; + int ammoMag[WEAPON_COUNT]; + int ammoRes[WEAPON_COUNT]; + + uint8_t invType[INV_SLOTS]; + int invQty[INV_SLOTS]; + + double lastShotTime; + int shotsInBurst; + uint32_t rng; + + dBodyID body; + dGeomID geom; +} Player; + +static int addr_equal(const struct sockaddr_in *a, + const struct sockaddr_in *b) { + return (a->sin_family == b->sin_family) && (a->sin_port == b->sin_port) && + (a->sin_addr.s_addr == b->sin_addr.s_addr); +} + +static float clampf(float v, float lo, float hi) { + if (v < lo) + return lo; + if (v > hi) + return hi; + return v; +} + +static void sendto_player(int sock, const Player *pl, const void *data, + int len) { + sendto(sock, (const char *)data, len, 0, (const struct sockaddr *)&pl->addr, + sizeof(pl->addr)); +} +static void broadcast(int sock, const Player players[MAX_PLAYERS], + const void *data, int len) { + for (int i = 0; i < MAX_PLAYERS; i++) + if (players[i].inUse) + sendto_player(sock, &players[i], data, len); +} + +// ----- ODE globals ----- +static dWorldID gWorld; +static dSpaceID gSpace; +static dJointGroupID gContactGroup; + +// items +static Item gItems[MAX_ITEMS]; +static uint16_t gNextItemId = 1; + +static void item_destroy(Item *it) { + if (it->geom) { + dGeomDestroy(it->geom); + it->geom = 0; + } + memset(it, 0, sizeof(*it)); +} +static int item_spawn(uint8_t type, int qty, float x, float y, float z) { + for (int i = 0; i < MAX_ITEMS; i++) { + if (!gItems[i].active) { + Item *it = &gItems[i]; + it->active = 1; + it->id = gNextItemId++; + it->type = type; + it->qty = qty; + it->x = x; + it->y = y; + it->z = z; + + it->geom = dCreateSphere(gSpace, 0.30); + dGeomSetPosition(it->geom, x, y + 0.3f, z); + + // Mark as item and avoid physical contacts; also make raycasts ignore it. + dGeomSetCategoryBits(it->geom, CAT_ITEM); + dGeomSetCollideBits(it->geom, 0); + + // Attach "user data" pointing to Item for identification if needed. + dGeomSetData(it->geom, it); + + return i; + } + } + return -1; +} + +static int inv_add(Player *p, uint8_t type, int qty) { + // stacking limits + int maxStack = 99; + if (type == ITEM_MEDKIT) + maxStack = 3; + if (type == ITEM_ARMORPLATE) + maxStack = 5; + + // try stack + for (int i = 0; i < INV_SLOTS; i++) { + if (p->invType[i] == type && p->invQty[i] < maxStack) { + int can = maxStack - p->invQty[i]; + int take = (qty < can) ? qty : can; + p->invQty[i] += take; + qty -= take; + if (qty <= 0) + return 1; + } + } + + // place into empty slots + for (int i = 0; i < INV_SLOTS; i++) { + if (p->invType[i] == ITEM_NONE) { + int take = (qty < maxStack) ? qty : maxStack; + p->invType[i] = type; + p->invQty[i] = take; + qty -= take; + if (qty <= 0) + return 1; + } + } + + // no space + return 0; +} + +static int inv_remove_one(Player *p, int slot) { + if (slot < 0 || slot >= INV_SLOTS) + return 0; + if (p->invType[slot] == ITEM_NONE || p->invQty[slot] <= 0) + return 0; + p->invQty[slot]--; + if (p->invQty[slot] <= 0) { + p->invQty[slot] = 0; + p->invType[slot] = ITEM_NONE; + } + return 1; +} + +static void nearCallback(void *user, dGeomID o1, dGeomID o2) { + (void)user; + + if (dGeomIsSpace(o1) || dGeomIsSpace(o2)) { + dSpaceCollide2(o1, o2, user, &nearCallback); + return; + } + + // Do not create contact joints involving item pickups + unsigned long c1 = dGeomGetCategoryBits(o1); + unsigned long c2 = dGeomGetCategoryBits(o2); + if ((c1 & CAT_ITEM) || (c2 & CAT_ITEM)) + return; + + const int MAXC = 8; + dContact c[MAXC]; + int n = dCollide(o1, o2, MAXC, &c[0].geom, sizeof(dContact)); + if (n <= 0) + return; + + for (int i = 0; i < n; i++) { + c[i].surface.mode = dContactApprox1; + c[i].surface.mu = 1.0; + c[i].surface.bounce = 0.0; + c[i].surface.bounce_vel = 0.0; + + dJointID j = dJointCreateContact(gWorld, gContactGroup, &c[i]); + dBodyID b1 = dGeomGetBody(o1); + dBodyID b2 = dGeomGetBody(o2); + dJointAttach(j, b1, b2); + } +} + +static void ode_init_world(void) { + dInitODE(); + gWorld = dWorldCreate(); + gSpace = dHashSpaceCreate(0); + gContactGroup = dJointGroupCreate(0); + + dWorldSetGravity(gWorld, 0, -9.81, 0); + dWorldSetERP(gWorld, 0.2); + dWorldSetCFM(gWorld, 1e-5); + + dGeomID ground = dCreatePlane(gSpace, 0, 1, 0, 0); + dGeomSetCategoryBits(ground, CAT_WORLD); + dGeomSetCollideBits(ground, CAT_PLAYER); + + const struct { + float x, y, z, sx, sy, sz; + } blocks[] = { + {2, 0.5f, 2, 1, 1, 1}, + {2, 1.5f, 2, 1, 1, 1}, + {-3, 0.5f, -1, 1, 1, 1}, + {0, 0.5f, 4, 2, 1, 2}, + }; + for (int i = 0; i < (int)(sizeof(blocks) / sizeof(blocks[0])); i++) { + dGeomID b = dCreateBox(gSpace, blocks[i].sx, blocks[i].sy, blocks[i].sz); + dGeomSetPosition(b, blocks[i].x, blocks[i].y, blocks[i].z); + dGeomSetCategoryBits(b, CAT_WORLD); + dGeomSetCollideBits(b, CAT_PLAYER); + } + + // spawn some test pickups + item_spawn(ITEM_MEDKIT, 1, 1.0f, 0.0f, 1.0f); + item_spawn(ITEM_MEDKIT, 1, -2.0f, 0.0f, 3.0f); + item_spawn(ITEM_ARMORPLATE, 1, 3.0f, 0.0f, -1.0f); + item_spawn(ITEM_AMMO_RIFLE, 30, 0.0f, 0.0f, 2.0f); + item_spawn(ITEM_AMMO_PISTOL, 12, -1.0f, 0.0f, -2.0f); +} + +static void ode_shutdown_world(void) { + dJointGroupDestroy(gContactGroup); + dSpaceDestroy(gSpace); + dWorldDestroy(gWorld); + dCloseODE(); +} + +static void player_ode_create(Player *p, int spawnIndex) { + const dReal radius = 0.35; + const dReal length = 1.0; + + p->body = dBodyCreate(gWorld); + + dMass m; + dMassSetCapsuleTotal(&m, 80.0, 3, radius, length); + dBodySetMass(p->body, &m); + + dBodySetLinearDamping(p->body, 0.05); + dBodySetAngularDamping(p->body, 0.99); + + dBodySetPosition(p->body, (dReal)(spawnIndex * 2), 2.0, 0.0); + dBodySetAngularVel(p->body, 0, 0, 0); + + p->geom = dCreateCapsule(gSpace, radius, length); + dGeomSetBody(p->geom, p->body); + + dGeomSetCategoryBits(p->geom, CAT_PLAYER); + dGeomSetCollideBits(p->geom, CAT_WORLD | CAT_PLAYER); +} + +static void player_ode_destroy(Player *p) { + if (p->geom) { + dGeomDestroy(p->geom); + p->geom = 0; + } + if (p->body) { + dBodyDestroy(p->body); + p->body = 0; + } +} + +// ---- ODE raycast ---- +typedef struct RaycastCtx { + dGeomID ray; + dGeomID hitGeom; + dContactGeom hit; + int hasHit; +} RaycastCtx; + +static void rayCallback(void *data, dGeomID o1, dGeomID o2) { + RaycastCtx *rc = (RaycastCtx *)data; + dGeomID ray = rc->ray; + dGeomID other = (o1 == ray) ? o2 : o1; + if (other == ray) + return; + + dContact c[4]; + int n = dCollide(ray, other, 4, &c[0].geom, sizeof(dContact)); + for (int i = 0; i < n; i++) { + if (!rc->hasHit || c[i].geom.depth < rc->hit.depth) { + rc->hasHit = 1; + rc->hit = c[i].geom; + rc->hitGeom = other; + } + } +} + +static int raycast_space(float ox, float oy, float oz, float dx, float dy, + float dz, float maxDist, dContactGeom *outHit, + dGeomID *outGeom) { + dGeomID ray = dCreateRay(0, maxDist); + dGeomRaySet(ray, ox, oy, oz, dx, dy, dz); + dGeomSetCategoryBits(ray, 0xFFFFFFFFu); + dGeomSetCollideBits(ray, CAT_WORLD | CAT_PLAYER); + + RaycastCtx rc = {0}; + rc.ray = ray; + dSpaceCollide2(ray, (dGeomID)gSpace, &rc, &rayCallback); + dGeomDestroy(ray); + + if (!rc.hasHit) + return 0; + if (outHit) + *outHit = rc.hit; + if (outGeom) + *outGeom = rc.hitGeom; + return 1; +} + +// ---- spray math helpers ---- +static uint32_t xorshift32(uint32_t *s) { + uint32_t x = *s; + x ^= x << 13; + x ^= x >> 17; + x ^= x << 5; + *s = x; + return x; +} +static float frand_signed(uint32_t *s) { + return ((xorshift32(s) & 0xFFFFFF) / (float)0x7FFFFF) - 1.0f; +} +static void dir_from_yaw_pitch(float yaw, float pitch, float *dx, float *dy, + float *dz) { + *dx = sinf(yaw) * cosf(pitch); + *dy = sinf(pitch); + *dz = cosf(yaw) * cosf(pitch); +} +static void v3_cross(float ax, float ay, float az, float bx, float by, float bz, + float *ox, float *oy, float *oz) { + *ox = ay * bz - az * by; + *oy = az * bx - ax * bz; + *oz = ax * by - ay * bx; +} +static void v3_normf(float *x, float *y, float *z) { + float l = sqrtf((*x) * (*x) + (*y) * (*y) + (*z) * (*z)); + if (l < 1e-6f) { + *x = 0; + *y = 0; + *z = 1; + return; + } + *x /= l; + *y /= l; + *z /= l; +} + +static int find_nearest_item(float x, float y, float z, float maxDist) { + int best = -1; + float bestD2 = maxDist * maxDist; + for (int i = 0; i < MAX_ITEMS; i++) { + if (!gItems[i].active) + continue; + float dx = gItems[i].x - x; + float dy = (gItems[i].y + 0.3f) - y; + float dz = gItems[i].z - z; + float d2 = dx * dx + dy * dy + dz * dz; + if (d2 < bestD2) { + bestD2 = d2; + best = i; + } + } + return best; +} + +int main(void) { +#ifdef _WIN32 + WSADATA wsa; + WSAStartup(MAKEWORD(2, 2), &wsa); +#endif + + ode_init_world(); + + int sock = (int)socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP); + if (sock < 0) { + perror("socket"); + return 1; + } + set_nonblocking(sock); + + struct sockaddr_in srv = {0}; + srv.sin_family = AF_INET; + srv.sin_addr.s_addr = htonl(INADDR_ANY); + srv.sin_port = htons(SERVER_PORT); + + if (bind(sock, (struct sockaddr *)&srv, sizeof(srv)) < 0) { + perror("bind"); + CLOSESOCK(sock); + return 1; + } + + Player players[MAX_PLAYERS] = {0}; + uint32_t serverTick = 0; + uint32_t itemsTickDiv = 0; + + printf("Server listening UDP %d\n", SERVER_PORT); + double nextTick = now_seconds(); + + for (;;) { + // receive + for (;;) { + uint8_t buf[1400]; + struct sockaddr_in from = {0}; + socklen_t fromLen = sizeof(from); + + int n = (int)recvfrom(sock, (char *)buf, (int)sizeof(buf), 0, + (struct sockaddr *)&from, &fromLen); + if (n <= 0) { +#ifdef _WIN32 + if (WSAGetLastError() == WSAEWOULDBLOCK) + break; +#else + if (errno == EWOULDBLOCK || errno == EAGAIN) + break; +#endif + break; + } + + uint8_t type = buf[0]; + + int pid = -1; + for (int i = 0; i < MAX_PLAYERS; i++) { + if (players[i].inUse && addr_equal(&players[i].addr, &from)) { + pid = i; + break; + } + } + + if (type == MSG_HELLO && n >= (int)sizeof(MsgHello)) { + const MsgHello *m = (const MsgHello *)buf; + if (m->protocol != PROTOCOL_VERSION) + continue; + + if (pid < 0) { + for (int i = 0; i < MAX_PLAYERS; i++) + if (!players[i].inUse) { + pid = i; + Player *p = &players[i]; + memset(p, 0, sizeof(*p)); + p->inUse = 1; + p->addr = from; + p->lastHeard = now_seconds(); + strncpy(p->username, m->username, USERNAME_MAX - 1); + + p->alive = 1; + p->hp = 100; + p->armor = 0; + + p->weapon = WEAPON_RIFLE; + p->ammoMag[WEAPON_PISTOL] = 12; + p->ammoRes[WEAPON_PISTOL] = 48; + p->ammoMag[WEAPON_RIFLE] = 30; + p->ammoRes[WEAPON_RIFLE] = 90; + + for (int sidx = 0; sidx < INV_SLOTS; sidx++) { + p->invType[sidx] = ITEM_NONE; + p->invQty[sidx] = 0; + } + + p->lastShotTime = 0.0; + p->shotsInBurst = 0; + p->rng = 0xA341316Cu ^ (uint32_t)(i * 2654435761u); + + player_ode_create(p, i); + printf("Player %d joined as '%s'\n", pid, p->username); + break; + } + } else { + players[pid].lastHeard = now_seconds(); + } + + if (pid >= 0) { + MsgWelcome w = {0}; + w.type = MSG_WELCOME; + w.playerId = (uint8_t)pid; + w.serverTick = serverTick; + sendto(sock, (const char *)&w, (int)sizeof(w), 0, + (struct sockaddr *)&from, sizeof(from)); + } + } else if (type == MSG_INPUT && n >= (int)sizeof(MsgInput)) { + const MsgInput *m = (const MsgInput *)buf; + if (pid >= 0 && m->playerId == (uint8_t)pid) { + Player *p = &players[pid]; + p->lastHeard = now_seconds(); + + p->moveX = clampf(m->moveX, -1.0f, 1.0f); + p->moveZ = clampf(m->moveZ, -1.0f, 1.0f); + p->yaw = m->yaw; + p->pitch = clampf(m->pitch, -1.5f, 1.5f); + + if (m->buttons & BTN_WPN1) + p->weapon = WEAPON_PISTOL; + if (m->buttons & BTN_WPN2) + p->weapon = WEAPON_RIFLE; + + // reload + if (m->buttons & BTN_RELOAD) { + int wpn = p->weapon; + int magSize = (wpn == WEAPON_PISTOL) ? 12 : 30; + int need = magSize - p->ammoMag[wpn]; + if (need > 0 && p->ammoRes[wpn] > 0) { + int take = (p->ammoRes[wpn] < need) ? p->ammoRes[wpn] : need; + p->ammoRes[wpn] -= take; + p->ammoMag[wpn] += take; + } + } + + // pick + if (m->buttons & BTN_PICK) { + const dReal *pos = dBodyGetPosition(p->body); + int itIdx = find_nearest_item((float)pos[0], (float)pos[1] + 0.7f, + (float)pos[2], 1.6f); + if (itIdx >= 0) { + Item *it = &gItems[itIdx]; + int taken = 0; + + if (it->type == ITEM_AMMO_PISTOL) { + p->ammoRes[WEAPON_PISTOL] += it->qty; + taken = 1; + } else if (it->type == ITEM_AMMO_RIFLE) { + p->ammoRes[WEAPON_RIFLE] += it->qty; + taken = 1; + } else { + taken = inv_add(p, it->type, it->qty); + } + + if (taken) + item_destroy(it); + } + } + + // drop selected slot + if (m->buttons & BTN_DROP) { + int slot = (m->slotIndex < INV_SLOTS) ? (int)m->slotIndex : 0; + uint8_t t = p->invType[slot]; + if (t != ITEM_NONE && p->invQty[slot] > 0) { + const dReal *pos = dBodyGetPosition(p->body); + float ox = (float)pos[0]; + float oy = (float)pos[1]; + float oz = (float)pos[2]; + + // drop 1 unit + if (inv_remove_one(p, slot)) { + item_spawn(t, 1, ox, oy, oz); + } + } + } + + // use selected slot (medkit/armor plate) + if (m->buttons & BTN_USE) { + int slot = (m->slotIndex < INV_SLOTS) ? (int)m->slotIndex : 0; + uint8_t t = p->invType[slot]; + if (t == ITEM_MEDKIT && p->hp < 100 && p->invQty[slot] > 0) { + inv_remove_one(p, slot); + p->hp += 25; + if (p->hp > 100) + p->hp = 100; + } else if (t == ITEM_ARMORPLATE && p->armor < 100 && + p->invQty[slot] > 0) { + inv_remove_one(p, slot); + p->armor += 25; + if (p->armor > 100) + p->armor = 100; + } + } + } + } else if (type == MSG_SHOOT && n >= (int)sizeof(MsgShoot)) { + const MsgShoot *m = (const MsgShoot *)buf; + if (pid >= 0 && m->playerId == (uint8_t)pid && players[pid].inUse && + players[pid].alive) { + Player *sh = &players[pid]; + const double now = now_seconds(); + + double fireInterval = + (sh->weapon == WEAPON_PISTOL) ? (1.0 / 4.0) : (1.0 / 12.0); + if (now - sh->lastShotTime < fireInterval) + continue; + if (now - sh->lastShotTime > 0.18) + sh->shotsInBurst = 0; + + int wpn = sh->weapon; + if (sh->ammoMag[wpn] <= 0) + continue; + + sh->ammoMag[wpn]--; + sh->shotsInBurst++; + sh->lastShotTime = now; + + const dReal *pos = dBodyGetPosition(sh->body); + float ox = (float)pos[0]; + float oy = (float)pos[1] + 0.7f; + float oz = (float)pos[2]; + + float fx, fy, fz; + dir_from_yaw_pitch(sh->yaw, sh->pitch, &fx, &fy, &fz); + v3_normf(&fx, &fy, &fz); + + float base = (wpn == WEAPON_PISTOL) ? 0.0035f : 0.0080f; + float grow = (wpn == WEAPON_PISTOL) ? 0.0010f : 0.0025f; + float spread = base + grow * (float)sh->shotsInBurst; + if (spread > 0.08f) + spread = 0.08f; + + float upx = 0, upy = 1, upz = 0; + float rx, ry, rz; + v3_cross(fx, fy, fz, upx, upy, upz, &rx, &ry, &rz); + v3_normf(&rx, &ry, &rz); + float ux, uy, uz; + v3_cross(rx, ry, rz, fx, fy, fz, &ux, &uy, &uz); + v3_normf(&ux, &uy, &uz); + + float ax = frand_signed(&sh->rng) * spread; + float ay = frand_signed(&sh->rng) * spread; + + float dx = fx + rx * ax + ux * ay; + float dy = fy + ry * ax + uy * ay; + float dz = fz + rz * ax + uz * ay; + v3_normf(&dx, &dy, &dz); + + dContactGeom hit; + dGeomID hitGeom = 0; + if (raycast_space(ox, oy, oz, dx, dy, dz, 80.0f, &hit, &hitGeom)) { + int victim = -1; + for (int i = 0; i < MAX_PLAYERS; i++) { + if (!players[i].inUse) + continue; + if (players[i].geom == hitGeom) { + victim = i; + break; + } + } + + if (victim >= 0 && victim != pid && players[victim].alive) { + int dmg = 25; + + // armor absorbs first (simple model) + int absorb = + (players[victim].armor < dmg) ? players[victim].armor : dmg; + players[victim].armor -= absorb; + dmg -= absorb; + + players[victim].hp -= dmg; + if (players[victim].hp <= 0) { + players[victim].hp = 0; + players[victim].alive = 0; + } + + MsgHit ev = {0}; + ev.type = MSG_HIT; + ev.shooterId = (uint8_t)pid; + ev.victimId = (uint8_t)victim; + ev.victimHp = (int16_t)players[victim].hp; + broadcast(sock, players, &ev, (int)sizeof(ev)); + } + } + } + } + } + + // fixed tick + double t = now_seconds(); + if (t < nextTick) { +#ifdef _WIN32 + Sleep(1); +#else + struct timespec ts = {0, 1000000}; + nanosleep(&ts, NULL); +#endif + continue; + } + nextTick += (1.0 / (double)TICK_HZ); + serverTick++; + + // timeouts + movement + const float speed = 6.0f; + for (int i = 0; i < MAX_PLAYERS; i++) { + Player *p = &players[i]; + if (!p->inUse) + continue; + + if (now_seconds() - p->lastHeard > 10.0) { + printf("Player %d timed out\n", i); + player_ode_destroy(p); + memset(p, 0, sizeof(*p)); + continue; + } + + if (!p->alive) { + dBodySetLinearVel(p->body, 0, 0, 0); + continue; + } + + float cy = cosf(p->yaw), sy = sinf(p->yaw); + float fwdx = sy, fwdz = cy; + float rgtx = cy, rgtz = -sy; + + float vx = (fwdx * p->moveZ + rgtx * p->moveX) * speed; + float vz = (fwdz * p->moveZ + rgtz * p->moveX) * speed; + + const dReal *vcur = dBodyGetLinearVel(p->body); + dBodySetLinearVel(p->body, vx, vcur[1], vz); + + dQuaternion q; + dQFromAxisAndAngle(q, 0, 1, 0, p->yaw); + dBodySetQuaternion(p->body, q); + dBodySetAngularVel(p->body, 0, 0, 0); + } + + // step ODE + dJointGroupEmpty(gContactGroup); + dSpaceCollide(gSpace, 0, &nearCallback); + dWorldQuickStep(gWorld, DT); + + // snapshot players + MsgSnapshot snap = {0}; + snap.type = MSG_SNAPSHOT; + snap.serverTick = serverTick; + snap.count = 0; + + for (int i = 0; i < MAX_PLAYERS; i++) { + if (!players[i].inUse) + continue; + + const dReal *pos = dBodyGetPosition(players[i].body); + PlayerStateNet *ps = &snap.p[snap.count++]; + + ps->id = (uint8_t)i; + ps->alive = (uint8_t)players[i].alive; + ps->hp = (int16_t)players[i].hp; + ps->armor = (int16_t)players[i].armor; + + ps->x = (float)pos[0]; + ps->y = (float)pos[1]; + ps->z = (float)pos[2]; + ps->yaw = players[i].yaw; + ps->pitch = players[i].pitch; + + ps->weapon = (uint8_t)players[i].weapon; + for (int w = 0; w < WEAPON_COUNT; w++) { + ps->ammoMag[w] = (int16_t)players[i].ammoMag[w]; + ps->ammoRes[w] = (int16_t)players[i].ammoRes[w]; + } + for (int sidx = 0; sidx < INV_SLOTS; sidx++) { + ps->invType[sidx] = players[i].invType[sidx]; + ps->invQty[sidx] = (int16_t)players[i].invQty[sidx]; + } + + memset(ps->username, 0, USERNAME_MAX); + strncpy(ps->username, players[i].username, USERNAME_MAX - 1); + } + + broadcast(sock, players, &snap, (int)sizeof(MsgSnapshot)); + + // broadcast items at ~10Hz + itemsTickDiv++; + if (itemsTickDiv >= 6) { + itemsTickDiv = 0; + + MsgItems im = {0}; + im.type = MSG_ITEMS; + im.serverTick = serverTick; + im.count = 0; + + for (int i = 0; i < MAX_ITEMS && im.count < MAX_ITEMS; i++) { + if (!gItems[i].active) + continue; + ItemNet *on = &im.items[im.count++]; + on->id = gItems[i].id; + on->type = gItems[i].type; + on->qty = (int16_t)gItems[i].qty; + on->x = gItems[i].x; + on->y = gItems[i].y; + on->z = gItems[i].z; + } + + broadcast(sock, players, &im, (int)sizeof(MsgItems)); + } + } + + CLOSESOCK(sock); + ode_shutdown_world(); +#ifdef _WIN32 + WSACleanup(); +#endif + return 0; +}