971 lines
25 KiB
C
971 lines
25 KiB
C
|
|
// 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;
|
||
|
|
}
|