Compare commits

...

2 commits

Author SHA1 Message Date
c880f9dff0 gam 2025-12-18 03:29:37 +05:30
7f6a704dac gam 2025-12-18 03:29:26 +05:30
3 changed files with 1675 additions and 0 deletions

704
game/client.c Normal file
View file

@ -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 <math.h>
#include <stdint.h>
#include <stdio.h>
#include <string.h>
#ifdef _WIN32
#define WIN32_LEAN_AND_MEAN
#include <winsock2.h>
#include <ws2tcpip.h>
#pragma comment(lib, "ws2_32.lib")
typedef int socklen_t;
#define CLOSESOCK closesocket
#else
#include <arpa/inet.h>
#include <errno.h>
#include <fcntl.h>
#include <sys/socket.h>
#include <unistd.h>
#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;
}

0
game/output.md Normal file
View file

971
game/server.c Normal file
View file

@ -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 <math.h>
#include <stdint.h>
#include <stdio.h>
#include <string.h>
#include <time.h>
#include <ode/ode.h>
#ifdef _WIN32
#define WIN32_LEAN_AND_MEAN
#include <winsock2.h>
#include <ws2tcpip.h>
#pragma comment(lib, "ws2_32.lib")
typedef int socklen_t;
#define CLOSESOCK closesocket
#else
#include <arpa/inet.h>
#include <errno.h>
#include <fcntl.h>
#include <sys/socket.h>
#include <unistd.h>
#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;
}