diff options
| author | xolatile | 2025-08-04 22:53:42 +0200 |
|---|---|---|
| committer | xolatile | 2025-08-04 22:53:42 +0200 |
| commit | d309df4ce4d8ad0ed995a8e1c4267412a7782021 (patch) | |
| tree | 999ca8d785ecc1681e5eb7538ce2e6a18d244fa5 /src/fpsgame | |
| parent | 29d613d9cb65a0faa7e3f80e75bea0b6d910cb9a (diff) | |
| download | xolatile-badassbug-d309df4ce4d8ad0ed995a8e1c4267412a7782021.tar.xz xolatile-badassbug-d309df4ce4d8ad0ed995a8e1c4267412a7782021.tar.zst | |
Bunch of small changes...
Diffstat (limited to 'src/fpsgame')
| -rw-r--r-- | src/fpsgame/ai.cpp | 2788 | ||||
| -rw-r--r-- | src/fpsgame/ai.h | 500 | ||||
| -rw-r--r-- | src/fpsgame/aiman.h | 300 | ||||
| -rw-r--r-- | src/fpsgame/client.cpp | 4010 | ||||
| -rw-r--r-- | src/fpsgame/entities.cpp | 886 | ||||
| -rw-r--r-- | src/fpsgame/extinfo.h | 274 | ||||
| -rw-r--r-- | src/fpsgame/fps.cpp | 2777 | ||||
| -rw-r--r-- | src/fpsgame/game.h | 1114 | ||||
| -rw-r--r-- | src/fpsgame/render.cpp | 936 | ||||
| -rw-r--r-- | src/fpsgame/scoreboard.cpp | 1148 | ||||
| -rw-r--r-- | src/fpsgame/server.cpp | 7389 | ||||
| -rw-r--r-- | src/fpsgame/waypoint.cpp | 1542 | ||||
| -rw-r--r-- | src/fpsgame/weapon.cpp | 1905 |
13 files changed, 12765 insertions, 12804 deletions
diff --git a/src/fpsgame/ai.cpp b/src/fpsgame/ai.cpp index b8c3daa..60b33ca 100644 --- a/src/fpsgame/ai.cpp +++ b/src/fpsgame/ai.cpp @@ -4,881 +4,881 @@ extern int fog; namespace ai { - using namespace game; - - avoidset obstacles; - int updatemillis = 0, iteration = 0, itermillis = 0, forcegun = -1; - vec aitarget(0, 0, 0); - - VAR(aidebug, 0, 0, 6); - VAR(aiforcegun, -1, -1, NUMGUNS-1); - - ICOMMAND(addbot, "s", (char *s), addmsg(N_ADDBOT, "ri", *s ? clamp(parseint(s), 1, 101) : -1)); - ICOMMAND(delbot, "", (), addmsg(N_DELBOT, "r")); - ICOMMAND(botlimit, "i", (int *n), addmsg(N_BOTLIMIT, "ri", *n)); - ICOMMAND(botbalance, "i", (int *n), addmsg(N_BOTBALANCE, "ri", *n)); - - float viewdist(int x) - { - return x <= 100 ? clamp((SIGHTMIN+(SIGHTMAX-SIGHTMIN))/100.f*float(x), float(SIGHTMIN), float(fog)) : float(fog); - } - - float viewfieldx(int x) - { - return x <= 100 ? clamp((VIEWMIN+(VIEWMAX-VIEWMIN))/100.f*float(x), float(VIEWMIN), float(VIEWMAX)) : float(VIEWMAX); - } - - float viewfieldy(int x) - { - return viewfieldx(x)*3.f/4.f; - } - - bool canmove(fpsent *d) - { - return d->state != CS_DEAD && !intermission; - } - - float weapmindist(int weap) - { - return max(int(guns[weap].exprad), 2); - } - - float weapmaxdist(int weap) - { - return guns[weap].range + 4; - } - - bool weaprange(fpsent *d, int weap, float dist) - { - float mindist = weapmindist(weap), maxdist = weapmaxdist(weap); - return dist >= mindist*mindist && dist <= maxdist*maxdist; - } - - bool targetable(fpsent *d, fpsent *e) - { - if(d == e || !canmove(d)) return false; - return e->state == CS_ALIVE && !isteam(d->team, e->team); - } - - bool getsight(vec &o, float yaw, float pitch, vec &q, vec &v, float mdist, float fovx, float fovy) - { - float dist = o.dist(q); - - if(dist <= mdist) - { - float x = fmod(fabs(asin((q.z-o.z)/dist)/RAD-pitch), 360); - float y = fmod(fabs(-atan2(q.x-o.x, q.y-o.y)/RAD-yaw), 360); - if(min(x, 360-x) <= fovx && min(y, 360-y) <= fovy) return raycubelos(o, q, v); - } - return false; - } - - bool cansee(fpsent *d, vec &x, vec &y, vec &targ) - { - aistate &b = d->ai->getstate(); - if(canmove(d) && b.type != AI_S_WAIT) - return getsight(x, d->yaw, d->pitch, y, targ, d->ai->views[2], d->ai->views[0], d->ai->views[1]); - return false; - } - - bool canshoot(fpsent *d, fpsent *e) - { - if(weaprange(d, d->gunselect, e->o.squaredist(d->o)) && targetable(d, e)) - return d->ammo[d->gunselect] > 0 && lastmillis - d->lastaction >= d->gunwait; - return false; - } - - bool canshoot(fpsent *d) - { - return !d->ai->becareful && d->ammo[d->gunselect] > 0 && lastmillis - d->lastaction >= d->gunwait; - } + using namespace game; + + avoidset obstacles; + int updatemillis = 0, iteration = 0, itermillis = 0, forcegun = -1; + vec aitarget(0, 0, 0); + + VAR(aidebug, 0, 0, 6); + VAR(aiforcegun, -1, -1, NUMGUNS-1); + + ICOMMAND(addbot, "s", (char *s), addmsg(N_ADDBOT, "ri", *s ? clamp(parseint(s), 1, 101) : -1)); + ICOMMAND(delbot, "", (), addmsg(N_DELBOT, "r")); + ICOMMAND(botlimit, "i", (int *n), addmsg(N_BOTLIMIT, "ri", *n)); + ICOMMAND(botbalance, "i", (int *n), addmsg(N_BOTBALANCE, "ri", *n)); + + float viewdist(int x) + { + return x <= 100 ? clamp((SIGHTMIN+(SIGHTMAX-SIGHTMIN))/100.f*float(x), float(SIGHTMIN), float(fog)) : float(fog); + } + + float viewfieldx(int x) + { + return x <= 100 ? clamp((VIEWMIN+(VIEWMAX-VIEWMIN))/100.f*float(x), float(VIEWMIN), float(VIEWMAX)) : float(VIEWMAX); + } + + float viewfieldy(int x) + { + return viewfieldx(x)*3.f/4.f; + } + + bool canmove(fpsent *d) + { + return d->state != CS_DEAD && !intermission; + } + + float weapmindist(int weap) + { + return max(int(guns[weap].exprad), 2); + } + + float weapmaxdist(int weap) + { + return guns[weap].range + 4; + } + + bool weaprange(fpsent *d, int weap, float dist) + { + float mindist = weapmindist(weap), maxdist = weapmaxdist(weap); + return dist >= mindist*mindist && dist <= maxdist*maxdist; + } + + bool targetable(fpsent *d, fpsent *e) + { + if(d == e || !canmove(d)) return false; + return e->state == CS_ALIVE && !isteam(d->team, e->team); + } + + bool getsight(vec &o, float yaw, float pitch, vec &q, vec &v, float mdist, float fovx, float fovy) + { + float dist = o.dist(q); + + if(dist <= mdist) + { + float x = fmod(fabs(asin((q.z-o.z)/dist)/RAD-pitch), 360); + float y = fmod(fabs(-atan2(q.x-o.x, q.y-o.y)/RAD-yaw), 360); + if(min(x, 360-x) <= fovx && min(y, 360-y) <= fovy) return raycubelos(o, q, v); + } + return false; + } + + bool cansee(fpsent *d, vec &x, vec &y, vec &targ) + { + aistate &b = d->ai->getstate(); + if(canmove(d) && b.type != AI_S_WAIT) + return getsight(x, d->yaw, d->pitch, y, targ, d->ai->views[2], d->ai->views[0], d->ai->views[1]); + return false; + } + + bool canshoot(fpsent *d, fpsent *e) + { + if(weaprange(d, d->gunselect, e->o.squaredist(d->o)) && targetable(d, e)) + return d->ammo[d->gunselect] > 0 && lastmillis - d->lastaction >= d->gunwait; + return false; + } + + bool canshoot(fpsent *d) + { + return !d->ai->becareful && d->ammo[d->gunselect] > 0 && lastmillis - d->lastaction >= d->gunwait; + } bool hastarget(fpsent *d, aistate &b, fpsent *e, float yaw, float pitch, float dist) { // add margins of error - if(weaprange(d, d->gunselect, dist) || (d->skill <= 100 && !rnd(d->skill))) - { - if(d->gunselect == GUN_FIST) return true; - float skew = clamp(float(lastmillis-d->ai->enemymillis)/float((d->skill*guns[d->gunselect].attackdelay/200.f)), 0.f, guns[d->gunselect].projspeed ? 0.25f : 1e16f), - offy = yaw-d->yaw, offp = pitch-d->pitch; - if(offy > 180) offy -= 360; - else if(offy < -180) offy += 360; - if(fabs(offy) <= d->ai->views[0]*skew && fabs(offp) <= d->ai->views[1]*skew) return true; - } - return false; - } - - vec getaimpos(fpsent *d, fpsent *e) - { - vec o = e->o; - if(d->gunselect == GUN_RL) o.z += (e->aboveeye*0.2f)-(0.8f*d->eyeheight); - else if(d->gunselect != GUN_GL) o.z += (e->aboveeye-e->eyeheight)*0.5f; - if(d->skill <= 100) - { - if(lastmillis >= d->ai->lastaimrnd) - { - const int aiskew[NUMGUNS] = { 1, 10, 50, 5, 20, 1, 100 }; - #define rndaioffset(r) ((rnd(int(r*aiskew[d->gunselect]*2)+1)-(r*aiskew[d->gunselect]))*(1.f/float(max(d->skill, 1)))) - loopk(3) d->ai->aimrnd[k] = rndaioffset(e->radius); - int dur = (d->skill+10)*10; - d->ai->lastaimrnd = lastmillis+dur+rnd(dur); - } - loopk(3) o[k] += d->ai->aimrnd[k]; - } - return o; - } - - void create(fpsent *d) - { - if(!d->ai) d->ai = new aiinfo; - } - - void destroy(fpsent *d) - { - if(d->ai) DELETEP(d->ai); - } - - void init(fpsent *d, int at, int ocn, int sk, int bn, int pm, const char *name, const char *team) - { - loadwaypoints(); - - fpsent *o = newclient(ocn); - - d->aitype = at; - - bool resetthisguy = false; - if(!d->name[0]) - { - if(aidebug) conoutf(CON_DEBUG, "%s assigned to %s at skill %d", colorname(d, name), o ? colorname(o) : "?", sk); - else conoutf("\f0join:\f7 %s", colorname(d, name)); - resetthisguy = true; - } - else - { - if(d->ownernum != ocn) - { - if(aidebug) conoutf(CON_DEBUG, "%s reassigned to %s", colorname(d, name), o ? colorname(o) : "?"); - resetthisguy = true; - } - if(d->skill != sk && aidebug) conoutf(CON_DEBUG, "%s changed skill to %d", colorname(d, name), sk); - } - - copystring(d->name, name, MAXNAMELEN+1); - copystring(d->team, team, MAXTEAMLEN+1); - d->ownernum = ocn; - d->plag = 0; - d->skill = sk; - d->playermodel = 0; - - if(resetthisguy) removeweapons(d); - if(d->ownernum >= 0 && player1->clientnum == d->ownernum) - { - create(d); - if(d->ai) - { - d->ai->views[0] = viewfieldx(d->skill); - d->ai->views[1] = viewfieldy(d->skill); - d->ai->views[2] = viewdist(d->skill); - } - } - else if(d->ai) destroy(d); - } - - void update() - { - if(intermission) { loopv(players) if(players[i]->ai) players[i]->stopmoving(); } - else // fixed rate logic done out-of-sequence at 1 frame per second for each ai - { - if(totalmillis-updatemillis > 1000) - { - avoid(); - forcegun = multiplayer(false) ? -1 : aiforcegun; - updatemillis = totalmillis; - } - if(!iteration && totalmillis-itermillis > 1000) - { - iteration = 1; - itermillis = totalmillis; - } - int count = 0; - loopv(players) if(players[i]->ai) think(players[i], ++count == iteration ? true : false); - if(++iteration > count) iteration = 0; - } - } - - bool checkothers(vector<int> &targets, fpsent *d, int state, int targtype, int target, bool teams, int *members) - { // checks the states of other ai for a match - targets.setsize(0); - loopv(players) - { - fpsent *e = players[i]; - if(targets.find(e->clientnum) >= 0) continue; - if(teams && d && !isteam(d->team, e->team)) continue; - if(members) (*members)++; - if(e == d || !e->ai || e->state != CS_ALIVE) continue; - aistate &b = e->ai->getstate(); - if(state >= 0 && b.type != state) continue; - if(target >= 0 && b.target != target) continue; - if(targtype >=0 && b.targtype != targtype) continue; - targets.add(e->clientnum); - } - return !targets.empty(); - } - - bool makeroute(fpsent *d, aistate &b, int node, bool changed, int retries) - { - if(!iswaypoint(d->lastnode)) return false; + if(weaprange(d, d->gunselect, dist) || (d->skill <= 100 && !rnd(d->skill))) + { + if(d->gunselect == GUN_FIST) return true; + float skew = clamp(float(lastmillis-d->ai->enemymillis)/float((d->skill*guns[d->gunselect].attackdelay/200.f)), 0.f, guns[d->gunselect].projspeed ? 0.25f : 1e16f), + offy = yaw-d->yaw, offp = pitch-d->pitch; + if(offy > 180) offy -= 360; + else if(offy < -180) offy += 360; + if(fabs(offy) <= d->ai->views[0]*skew && fabs(offp) <= d->ai->views[1]*skew) return true; + } + return false; + } + + vec getaimpos(fpsent *d, fpsent *e) + { + vec o = e->o; + if(d->gunselect == GUN_RL) o.z += (e->aboveeye*0.2f)-(0.8f*d->eyeheight); + else if(d->gunselect != GUN_GL) o.z += (e->aboveeye-e->eyeheight)*0.5f; + if(d->skill <= 100) + { + if(lastmillis >= d->ai->lastaimrnd) + { + const int aiskew[NUMGUNS] = { 1, 10, 50, 5, 20, 1, 100 }; + #define rndaioffset(r) ((rnd(int(r*aiskew[d->gunselect]*2)+1)-(r*aiskew[d->gunselect]))*(1.f/float(max(d->skill, 1)))) + loopk(3) d->ai->aimrnd[k] = rndaioffset(e->radius); + int dur = (d->skill+10)*10; + d->ai->lastaimrnd = lastmillis+dur+rnd(dur); + } + loopk(3) o[k] += d->ai->aimrnd[k]; + } + return o; + } + + void create(fpsent *d) + { + if(!d->ai) d->ai = new aiinfo; + } + + void destroy(fpsent *d) + { + if(d->ai) DELETEP(d->ai); + } + + void init(fpsent *d, int at, int ocn, int sk, int bn, int pm, const char *name, const char *team) + { + loadwaypoints(); + + fpsent *o = newclient(ocn); + + d->aitype = at; + + bool resetthisguy = false; + if(!d->name[0]) + { + if(aidebug) conoutf(CON_DEBUG, "%s assigned to %s at skill %d", colorname(d, name), o ? colorname(o) : "?", sk); + else conoutf("\f0join:\f7 %s", colorname(d, name)); + resetthisguy = true; + } + else + { + if(d->ownernum != ocn) + { + if(aidebug) conoutf(CON_DEBUG, "%s reassigned to %s", colorname(d, name), o ? colorname(o) : "?"); + resetthisguy = true; + } + if(d->skill != sk && aidebug) conoutf(CON_DEBUG, "%s changed skill to %d", colorname(d, name), sk); + } + + copystring(d->name, name, MAXNAMELEN+1); + copystring(d->team, team, MAXTEAMLEN+1); + d->ownernum = ocn; + d->plag = 0; + d->skill = sk; + d->playermodel = 0; + + if(resetthisguy) removeweapons(d); + if(d->ownernum >= 0 && player1->clientnum == d->ownernum) + { + create(d); + if(d->ai) + { + d->ai->views[0] = viewfieldx(d->skill); + d->ai->views[1] = viewfieldy(d->skill); + d->ai->views[2] = viewdist(d->skill); + } + } + else if(d->ai) destroy(d); + } + + void update() + { + if(intermission) { loopv(players) if(players[i]->ai) players[i]->stopmoving(); } + else // fixed rate logic done out-of-sequence at 1 frame per second for each ai + { + if(totalmillis-updatemillis > 1000) + { + avoid(); + forcegun = multiplayer(false) ? -1 : aiforcegun; + updatemillis = totalmillis; + } + if(!iteration && totalmillis-itermillis > 1000) + { + iteration = 1; + itermillis = totalmillis; + } + int count = 0; + loopv(players) if(players[i]->ai) think(players[i], ++count == iteration ? true : false); + if(++iteration > count) iteration = 0; + } + } + + bool checkothers(vector<int> &targets, fpsent *d, int state, int targtype, int target, bool teams, int *members) + { // checks the states of other ai for a match + targets.setsize(0); + loopv(players) + { + fpsent *e = players[i]; + if(targets.find(e->clientnum) >= 0) continue; + if(teams && d && !isteam(d->team, e->team)) continue; + if(members) (*members)++; + if(e == d || !e->ai || e->state != CS_ALIVE) continue; + aistate &b = e->ai->getstate(); + if(state >= 0 && b.type != state) continue; + if(target >= 0 && b.target != target) continue; + if(targtype >=0 && b.targtype != targtype) continue; + targets.add(e->clientnum); + } + return !targets.empty(); + } + + bool makeroute(fpsent *d, aistate &b, int node, bool changed, int retries) + { + if(!iswaypoint(d->lastnode)) return false; if(changed && d->ai->route.length() > 1 && d->ai->route[0] == node) return true; if(route(d, d->lastnode, node, d->ai->route, obstacles, retries)) { b.override = false; return true; } - // retry fails: 0 = first attempt, 1 = try ignoring obstacles, 2 = try ignoring prevnodes too + // retry fails: 0 = first attempt, 1 = try ignoring obstacles, 2 = try ignoring prevnodes too if(retries <= 1) return makeroute(d, b, node, false, retries+1); return false; - } - - bool makeroute(fpsent *d, aistate &b, const vec &pos, bool changed, int retries) - { - int node = closestwaypoint(pos, SIGHTMIN, true); - return makeroute(d, b, node, changed, retries); - } - - bool randomnode(fpsent *d, aistate &b, const vec &pos, float guard, float wander) - { - static vector<int> candidates; - candidates.setsize(0); - findwaypointswithin(pos, guard, wander, candidates); - - while(!candidates.empty()) - { - int w = rnd(candidates.length()), n = candidates.removeunordered(w); - if(n != d->lastnode && !d->ai->hasprevnode(n) && !obstacles.find(n, d) && makeroute(d, b, n)) return true; - } - return false; - } - - bool randomnode(fpsent *d, aistate &b, float guard, float wander) - { - return randomnode(d, b, d->feetpos(), guard, wander); - } - - bool badhealth(fpsent *d) - { - if(d->skill <= 100) return d->health <= (111-d->skill)/4; - return false; - } - - bool enemy(fpsent *d, aistate &b, const vec &pos, float guard = SIGHTMIN, int pursue = 0) - { - fpsent *t = NULL; - vec dp = d->headpos(); - float mindist = guard*guard, bestdist = 1e16f; - loopv(players) - { - fpsent *e = players[i]; - if(e == d || !targetable(d, e)) continue; - vec ep = getaimpos(d, e); - float dist = ep.squaredist(dp); - if(dist < bestdist && (cansee(d, dp, ep) || dist <= mindist)) - { - t = e; - bestdist = dist; - } - } - if(t && violence(d, b, t, pursue)) return true; - return false; - } - - bool patrol(fpsent *d, aistate &b, const vec &pos, float guard, float wander, int walk, bool retry) - { - vec feet = d->feetpos(); - if(walk == 2 || b.override || (walk && feet.squaredist(pos) <= guard*guard) || !makeroute(d, b, pos)) - { // run away and back to keep ourselves busy - if(!b.override && randomnode(d, b, pos, guard, wander)) - { - b.override = true; - return true; - } - else if(d->ai->route.empty()) - { - if(!retry) - { - b.override = false; - return patrol(d, b, pos, guard, wander, walk, true); - } - b.override = false; - return false; - } - } - b.override = false; - return true; - } - - bool defend(fpsent *d, aistate &b, const vec &pos, float guard, float wander, int walk) - { + } + + bool makeroute(fpsent *d, aistate &b, const vec &pos, bool changed, int retries) + { + int node = closestwaypoint(pos, SIGHTMIN, true); + return makeroute(d, b, node, changed, retries); + } + + bool randomnode(fpsent *d, aistate &b, const vec &pos, float guard, float wander) + { + static vector<int> candidates; + candidates.setsize(0); + findwaypointswithin(pos, guard, wander, candidates); + + while(!candidates.empty()) + { + int w = rnd(candidates.length()), n = candidates.removeunordered(w); + if(n != d->lastnode && !d->ai->hasprevnode(n) && !obstacles.find(n, d) && makeroute(d, b, n)) return true; + } + return false; + } + + bool randomnode(fpsent *d, aistate &b, float guard, float wander) + { + return randomnode(d, b, d->feetpos(), guard, wander); + } + + bool badhealth(fpsent *d) + { + if(d->skill <= 100) return d->health <= (111-d->skill)/4; + return false; + } + + bool enemy(fpsent *d, aistate &b, const vec &pos, float guard = SIGHTMIN, int pursue = 0) + { + fpsent *t = NULL; + vec dp = d->headpos(); + float mindist = guard*guard, bestdist = 1e16f; + loopv(players) + { + fpsent *e = players[i]; + if(e == d || !targetable(d, e)) continue; + vec ep = getaimpos(d, e); + float dist = ep.squaredist(dp); + if(dist < bestdist && (cansee(d, dp, ep) || dist <= mindist)) + { + t = e; + bestdist = dist; + } + } + if(t && violence(d, b, t, pursue)) return true; + return false; + } + + bool patrol(fpsent *d, aistate &b, const vec &pos, float guard, float wander, int walk, bool retry) + { + vec feet = d->feetpos(); + if(walk == 2 || b.override || (walk && feet.squaredist(pos) <= guard*guard) || !makeroute(d, b, pos)) + { // run away and back to keep ourselves busy + if(!b.override && randomnode(d, b, pos, guard, wander)) + { + b.override = true; + return true; + } + else if(d->ai->route.empty()) + { + if(!retry) + { + b.override = false; + return patrol(d, b, pos, guard, wander, walk, true); + } + b.override = false; + return false; + } + } + b.override = false; + return true; + } + + bool defend(fpsent *d, aistate &b, const vec &pos, float guard, float wander, int walk) + { bool hasenemy = enemy(d, b, pos, wander, d->gunselect == GUN_FIST ? 1 : 0); if(!walk) { - if(d->feetpos().squaredist(pos) <= guard*guard) - { - b.idle = hasenemy ? 2 : 1; - return true; - } - walk++; - } - return patrol(d, b, pos, guard, wander, walk); - } - - bool violence(fpsent *d, aistate &b, fpsent *e, int pursue) - { - if(e && targetable(d, e)) - { - if(pursue) - { - if((b.targtype != AI_T_AFFINITY || !(pursue%2)) && makeroute(d, b, e->lastnode)) - d->ai->switchstate(b, AI_S_PURSUE, AI_T_PLAYER, e->clientnum); - else if(pursue >= 3) return false; // can't pursue - } - if(d->ai->enemy != e->clientnum) - { - d->ai->enemyseen = d->ai->enemymillis = lastmillis; - d->ai->enemy = e->clientnum; - } - return true; - } - return false; - } - - bool target(fpsent *d, aistate &b, int pursue = 0, bool force = false, float mindist = 0.f) - { - static vector<fpsent *> hastried; hastried.setsize(0); - vec dp = d->headpos(); - while(true) - { - float dist = 1e16f; - fpsent *t = NULL; - loopv(players) - { - fpsent *e = players[i]; - if(e == d || hastried.find(e) >= 0 || !targetable(d, e)) continue; - vec ep = getaimpos(d, e); - float v = ep.squaredist(dp); - if((!t || v < dist) && (mindist <= 0 || v <= mindist) && (force || cansee(d, dp, ep))) - { - t = e; - dist = v; - } - } - if(t) - { - if(violence(d, b, t, pursue)) return true; - hastried.add(t); - } - else break; - } - return false; - } - - int isgoodammo(int gun) { return gun >= GUN_SG && gun <= GUN_GL; } - - bool hasgoodammo(fpsent *d) - { - static const int goodguns[] = { GUN_CG, GUN_RL, GUN_SG, GUN_RIFLE }; - loopi(sizeof(goodguns)/sizeof(goodguns[0])) if(d->hasammo(goodguns[0])) return true; - if(d->ammo[GUN_GL] > 5) return true; - return false; - } - - void assist(fpsent *d, aistate &b, vector<interest> &interests, bool all, bool force) - { - loopv(players) - { - fpsent *e = players[i]; - if(e == d || (!all && e->aitype != AI_NONE) || !isteam(d->team, e->team)) continue; - interest &n = interests.add(); - n.state = AI_S_DEFEND; - n.node = e->lastnode; - n.target = e->clientnum; - n.targtype = AI_T_PLAYER; - n.score = e->o.squaredist(d->o)/(hasgoodammo(d) ? 1e8f : (force ? 1e4f : 1e2f)); - } - } - - static void tryitem(fpsent *d, extentity &e, int id, aistate &b, vector<interest> &interests, bool force = false) - { - float score = 0; - switch(e.type) - { - case I_HEALTH: - if(d->health < min(d->skill, 75)) score = 1e3f; - break; - case I_QUAD: score = 1e3f; break; - case I_BOOST: score = 1e2f; break; - case I_GREENARMOUR: case I_YELLOWARMOUR: - { - int atype = A_GREEN + e.type - I_GREENARMOUR; - if(atype > d->armourtype) score = atype == A_YELLOW ? 1e2f : 1e1f; - else if(d->armour < 50) score = 1e1f; - break; - } - default: - { - if(e.type >= I_SHELLS && e.type <= I_CARTRIDGES && !d->hasmaxammo(e.type)) - { - int gun = e.type - I_SHELLS + GUN_SG; - // go get a weapon upgrade - if(gun == d->ai->weappref) score = 1e8f; - else if(isgoodammo(gun)) score = hasgoodammo(d) ? 1e2f : 1e4f; - } - break; - } - } - if(score != 0) - { - interest &n = interests.add(); - n.state = AI_S_INTEREST; - n.node = closestwaypoint(e.o, SIGHTMIN, true); - n.target = id; - n.targtype = AI_T_ENTITY; - n.score = d->feetpos().squaredist(e.o)/(force ? -1 : score); - } - } - - void items(fpsent *d, aistate &b, vector<interest> &interests, bool force = false) - { - loopv(entities::ents) - { - extentity &e = *(extentity *)entities::ents[i]; - if(!e.spawned() || e.nopickup() || !d->canpickup(e.type)) continue; - tryitem(d, e, i, b, interests, force); - } - } - - static vector<int> targets; - - bool parseinterests(fpsent *d, aistate &b, vector<interest> &interests, bool override, bool ignore) - { - while(!interests.empty()) - { - int q = interests.length()-1; - loopi(interests.length()-1) if(interests[i].score < interests[q].score) q = i; - interest n = interests.removeunordered(q); - bool proceed = true; - if(!ignore) switch(n.state) - { - case AI_S_DEFEND: // don't get into herds - { - int members = 0; - proceed = !checkothers(targets, d, n.state, n.targtype, n.target, true, &members) && members > 1; - break; - } - default: break; - } - if(proceed && makeroute(d, b, n.node)) - { - d->ai->switchstate(b, n.state, n.targtype, n.target); - return true; - } - } - return false; - } - - bool find(fpsent *d, aistate &b, bool override = false) - { - static vector<interest> interests; - interests.setsize(0); - if(!m_noitems) - { - if((!m_noammo && !hasgoodammo(d)) || d->health < min(d->skill - 15, 75)) - items(d, b, interests); - else - { - static vector<int> nearby; - nearby.setsize(0); - findents(I_SHELLS, I_QUAD, false, d->feetpos(), vec(32, 32, 24), nearby); - loopv(nearby) - { - int id = nearby[i]; - extentity &e = *(extentity *)entities::ents[id]; - if(d->canpickup(e.type)) tryitem(d, e, id, b, interests); - } - } - } - if(m_teammode) assist(d, b, interests); - return parseinterests(d, b, interests, override); - } - - bool findassist(fpsent *d, aistate &b, bool override = false) - { - static vector<interest> interests; - interests.setsize(0); - assist(d, b, interests); - while(!interests.empty()) - { - int q = interests.length()-1; - loopi(interests.length()-1) if(interests[i].score < interests[q].score) q = i; - interest n = interests.removeunordered(q); - bool proceed = true; - switch(n.state) - { - case AI_S_DEFEND: // don't get into herds - { - int members = 0; - proceed = !checkothers(targets, d, n.state, n.targtype, n.target, true, &members) && members > 1; - break; - } - default: break; - } - if(proceed && makeroute(d, b, n.node)) - { - d->ai->switchstate(b, n.state, n.targtype, n.target); - return true; - } - } - return false; - } - - void damaged(fpsent *d, fpsent *e) - { - if(d->ai && canmove(d) && targetable(d, e)) // see if this ai is interested in a grudge - { - aistate &b = d->ai->getstate(); - if(violence(d, b, e, d->gunselect == GUN_FIST ? 1 : 0)) return; - } - if(checkothers(targets, d, AI_S_DEFEND, AI_T_PLAYER, d->clientnum, true)) - { - loopv(targets) - { - fpsent *t = getclient(targets[i]); - if(!t->ai || !canmove(t) || !targetable(t, e)) continue; - aistate &c = t->ai->getstate(); - if(violence(t, c, e, d->gunselect == GUN_FIST ? 1 : 0)) return; - } - } - } - - void findorientation(vec &o, float yaw, float pitch, vec &pos) - { - vec dir; - vecfromyawpitch(yaw, pitch, 1, 0, dir); - if(raycubepos(o, dir, pos, 0, RAY_CLIPMAT|RAY_SKIPFIRST) == -1) - pos = dir.mul(2*getworldsize()).add(o); - } - - void setup(fpsent *d) - { - d->ai->clearsetup(); - d->ai->reset(true); - d->ai->lastrun = lastmillis; - if(m_insta) d->ai->weappref = GUN_RIFLE; - else - { - if(forcegun >= 0 && forcegun < NUMGUNS) d->ai->weappref = forcegun; - else if(m_noammo) d->ai->weappref = -1; + if(d->feetpos().squaredist(pos) <= guard*guard) + { + b.idle = hasenemy ? 2 : 1; + return true; + } + walk++; + } + return patrol(d, b, pos, guard, wander, walk); + } + + bool violence(fpsent *d, aistate &b, fpsent *e, int pursue) + { + if(e && targetable(d, e)) + { + if(pursue) + { + if((b.targtype != AI_T_AFFINITY || !(pursue%2)) && makeroute(d, b, e->lastnode)) + d->ai->switchstate(b, AI_S_PURSUE, AI_T_PLAYER, e->clientnum); + else if(pursue >= 3) return false; // can't pursue + } + if(d->ai->enemy != e->clientnum) + { + d->ai->enemyseen = d->ai->enemymillis = lastmillis; + d->ai->enemy = e->clientnum; + } + return true; + } + return false; + } + + bool target(fpsent *d, aistate &b, int pursue = 0, bool force = false, float mindist = 0.f) + { + static vector<fpsent *> hastried; hastried.setsize(0); + vec dp = d->headpos(); + while(true) + { + float dist = 1e16f; + fpsent *t = NULL; + loopv(players) + { + fpsent *e = players[i]; + if(e == d || hastried.find(e) >= 0 || !targetable(d, e)) continue; + vec ep = getaimpos(d, e); + float v = ep.squaredist(dp); + if((!t || v < dist) && (mindist <= 0 || v <= mindist) && (force || cansee(d, dp, ep))) + { + t = e; + dist = v; + } + } + if(t) + { + if(violence(d, b, t, pursue)) return true; + hastried.add(t); + } + else break; + } + return false; + } + + int isgoodammo(int gun) { return gun >= GUN_SG && gun <= GUN_GL; } + + bool hasgoodammo(fpsent *d) + { + static const int goodguns[] = { GUN_CG, GUN_RL, GUN_SG, GUN_RIFLE }; + loopi(sizeof(goodguns)/sizeof(goodguns[0])) if(d->hasammo(goodguns[0])) return true; + if(d->ammo[GUN_GL] > 5) return true; + return false; + } + + void assist(fpsent *d, aistate &b, vector<interest> &interests, bool all, bool force) + { + loopv(players) + { + fpsent *e = players[i]; + if(e == d || (!all && e->aitype != AI_NONE) || !isteam(d->team, e->team)) continue; + interest &n = interests.add(); + n.state = AI_S_DEFEND; + n.node = e->lastnode; + n.target = e->clientnum; + n.targtype = AI_T_PLAYER; + n.score = e->o.squaredist(d->o)/(hasgoodammo(d) ? 1e8f : (force ? 1e4f : 1e2f)); + } + } + + static void tryitem(fpsent *d, extentity &e, int id, aistate &b, vector<interest> &interests, bool force = false) + { + float score = 0; + switch(e.type) + { + case I_HEALTH: + if(d->health < min(d->skill, 75)) score = 1e3f; + break; + case I_QUAD: score = 1e3f; break; + case I_BOOST: score = 1e2f; break; + case I_GREENARMOUR: case I_YELLOWARMOUR: + { + int atype = A_GREEN + e.type - I_GREENARMOUR; + if(atype > d->armourtype) score = atype == A_YELLOW ? 1e2f : 1e1f; + else if(d->armour < 50) score = 1e1f; + break; + } + default: + { + if(e.type >= I_SHELLS && e.type <= I_CARTRIDGES && !d->hasmaxammo(e.type)) + { + int gun = e.type - I_SHELLS + GUN_SG; + // go get a weapon upgrade + if(gun == d->ai->weappref) score = 1e8f; + else if(isgoodammo(gun)) score = hasgoodammo(d) ? 1e2f : 1e4f; + } + break; + } + } + if(score != 0) + { + interest &n = interests.add(); + n.state = AI_S_INTEREST; + n.node = closestwaypoint(e.o, SIGHTMIN, true); + n.target = id; + n.targtype = AI_T_ENTITY; + n.score = d->feetpos().squaredist(e.o)/(force ? -1 : score); + } + } + + void items(fpsent *d, aistate &b, vector<interest> &interests, bool force = false) + { + loopv(entities::ents) + { + extentity &e = *(extentity *)entities::ents[i]; + if(!e.spawned() || e.nopickup() || !d->canpickup(e.type)) continue; + tryitem(d, e, i, b, interests, force); + } + } + + static vector<int> targets; + + bool parseinterests(fpsent *d, aistate &b, vector<interest> &interests, bool override, bool ignore) + { + while(!interests.empty()) + { + int q = interests.length()-1; + loopi(interests.length()-1) if(interests[i].score < interests[q].score) q = i; + interest n = interests.removeunordered(q); + bool proceed = true; + if(!ignore) switch(n.state) + { + case AI_S_DEFEND: // don't get into herds + { + int members = 0; + proceed = !checkothers(targets, d, n.state, n.targtype, n.target, true, &members) && members > 1; + break; + } + default: break; + } + if(proceed && makeroute(d, b, n.node)) + { + d->ai->switchstate(b, n.state, n.targtype, n.target); + return true; + } + } + return false; + } + + bool find(fpsent *d, aistate &b, bool override = false) + { + static vector<interest> interests; + interests.setsize(0); + if(!m_noitems) + { + if((!m_noammo && !hasgoodammo(d)) || d->health < min(d->skill - 15, 75)) + items(d, b, interests); + else + { + static vector<int> nearby; + nearby.setsize(0); + findents(I_SHELLS, I_QUAD, false, d->feetpos(), vec(32, 32, 24), nearby); + loopv(nearby) + { + int id = nearby[i]; + extentity &e = *(extentity *)entities::ents[id]; + if(d->canpickup(e.type)) tryitem(d, e, id, b, interests); + } + } + } + if(m_teammode) assist(d, b, interests); + return parseinterests(d, b, interests, override); + } + + bool findassist(fpsent *d, aistate &b, bool override = false) + { + static vector<interest> interests; + interests.setsize(0); + assist(d, b, interests); + while(!interests.empty()) + { + int q = interests.length()-1; + loopi(interests.length()-1) if(interests[i].score < interests[q].score) q = i; + interest n = interests.removeunordered(q); + bool proceed = true; + switch(n.state) + { + case AI_S_DEFEND: // don't get into herds + { + int members = 0; + proceed = !checkothers(targets, d, n.state, n.targtype, n.target, true, &members) && members > 1; + break; + } + default: break; + } + if(proceed && makeroute(d, b, n.node)) + { + d->ai->switchstate(b, n.state, n.targtype, n.target); + return true; + } + } + return false; + } + + void damaged(fpsent *d, fpsent *e) + { + if(d->ai && canmove(d) && targetable(d, e)) // see if this ai is interested in a grudge + { + aistate &b = d->ai->getstate(); + if(violence(d, b, e, d->gunselect == GUN_FIST ? 1 : 0)) return; + } + if(checkothers(targets, d, AI_S_DEFEND, AI_T_PLAYER, d->clientnum, true)) + { + loopv(targets) + { + fpsent *t = getclient(targets[i]); + if(!t->ai || !canmove(t) || !targetable(t, e)) continue; + aistate &c = t->ai->getstate(); + if(violence(t, c, e, d->gunselect == GUN_FIST ? 1 : 0)) return; + } + } + } + + void findorientation(vec &o, float yaw, float pitch, vec &pos) + { + vec dir; + vecfromyawpitch(yaw, pitch, 1, 0, dir); + if(raycubepos(o, dir, pos, 0, RAY_CLIPMAT|RAY_SKIPFIRST) == -1) + pos = dir.mul(2*getworldsize()).add(o); + } + + void setup(fpsent *d) + { + d->ai->clearsetup(); + d->ai->reset(true); + d->ai->lastrun = lastmillis; + if(m_insta) d->ai->weappref = GUN_RIFLE; + else + { + if(forcegun >= 0 && forcegun < NUMGUNS) d->ai->weappref = forcegun; + else if(m_noammo) d->ai->weappref = -1; else d->ai->weappref = rnd(GUN_GL-GUN_SG+1)+GUN_SG; - } - vec dp = d->headpos(); - findorientation(dp, d->yaw, d->pitch, d->ai->target); - } - - void spawned(fpsent *d) - { - if(d->ai) setup(d); - } - - void killed(fpsent *d, fpsent *e) - { - if(d->ai) d->ai->reset(); - } - - void itemspawned(int ent) - { - if(entities::ents.inrange(ent) && entities::ents[ent]->type >= I_SHELLS && entities::ents[ent]->type <= I_QUAD) - { - loopv(players) if(players[i] && players[i]->ai && players[i]->aitype == AI_BOT && players[i]->canpickup(entities::ents[ent]->type)) - { - fpsent *d = players[i]; - bool wantsitem = false; - switch(entities::ents[ent]->type) - { - case I_BOOST: - - case I_HEALTH: wantsitem = badhealth(d); break; - case I_GREENARMOUR: - - case I_YELLOWARMOUR: - - case I_QUAD: break; - default: - { - itemstat &is = itemstats[entities::ents[ent]->type-I_SHELLS]; - wantsitem = isgoodammo(is.info) && d->ammo[is.info] <= (d->ai->weappref == is.info ? is.add : is.add/2); - break; - } - } - if(wantsitem) - { - aistate &b = d->ai->getstate(); - if(b.targtype == AI_T_AFFINITY) continue; - if(b.type == AI_S_INTEREST && b.targtype == AI_T_ENTITY) - { - if(entities::ents.inrange(b.target)) - { - if(d->o.squaredist(entities::ents[ent]->o) < d->o.squaredist(entities::ents[b.target]->o)) - d->ai->switchstate(b, AI_S_INTEREST, AI_T_ENTITY, ent); - } - continue; - } - d->ai->switchstate(b, AI_S_INTEREST, AI_T_ENTITY, ent); - } - } - } - } - - int dowait(fpsent *d, aistate &b) - { - d->ai->clear(true); // ensure they're clean - if(find(d, b)) return 1; - if(target(d, b, 4, false)) return 1; - if(target(d, b, 4, true)) return 1; - if(randomnode(d, b, SIGHTMIN, 1e16f)) - { - d->ai->switchstate(b, AI_S_INTEREST, AI_T_NODE, d->ai->route[0]); - return 1; - } - return 0; // but don't pop the state - } - - int dodefend(fpsent *d, aistate &b) - { - if(d->state == CS_ALIVE) - { - switch(b.targtype) - { - case AI_T_NODE: - if(iswaypoint(b.target)) return defend(d, b, waypoints[b.target].o) ? 1 : 0; - break; - case AI_T_ENTITY: - if(entities::ents.inrange(b.target)) return defend(d, b, entities::ents[b.target]->o) ? 1 : 0; - break; - case AI_T_PLAYER: - { - fpsent *e = getclient(b.target); - if(e && e->state == CS_ALIVE) return defend(d, b, e->feetpos()) ? 1 : 0; - break; - } - default: break; - } - } - return 0; - } - - int dointerest(fpsent *d, aistate &b) - { - if(d->state != CS_ALIVE) return 0; - switch(b.targtype) - { - case AI_T_NODE: // this is like a wait state without sitting still.. - if(find(d, b)) return 1; - if(target(d, b, 4, true)) return 1; - if(iswaypoint(b.target) && vec(waypoints[b.target].o).sub(d->feetpos()).magnitude() > CLOSEDIST) - return makeroute(d, b, waypoints[b.target].o) ? 1 : 0; - break; - case AI_T_ENTITY: - if(entities::ents.inrange(b.target)) - { - extentity &e = *(extentity *)entities::ents[b.target]; - if(!e.spawned() || e.nopickup() || e.type < I_SHELLS || e.type > I_CARTRIDGES || d->hasmaxammo(e.type)) return 0; - //if(d->feetpos().squaredist(e.o) <= CLOSEDIST*CLOSEDIST) - //{ - // b.idle = 1; - // return true; - //} - return makeroute(d, b, e.o) ? 1 : 0; - } - break; - } - return 0; - } - - int dopursue(fpsent *d, aistate &b) - { - if(d->state == CS_ALIVE) - { - switch(b.targtype) - { - case AI_T_NODE: - { - if(iswaypoint(b.target)) - return defend(d, b, waypoints[b.target].o) ? 1 : 0; - break; - } - - case AI_T_PLAYER: - { - fpsent *e = getclient(b.target); - if(e && e->state == CS_ALIVE) - { - float guard = SIGHTMIN, wander = guns[d->gunselect].range; - if(d->gunselect == GUN_FIST) guard = 0.f; - return patrol(d, b, e->feetpos(), guard, wander) ? 1 : 0; - } - break; - } - default: break; - } - } - return 0; - } - - int closenode(fpsent *d) - { - vec pos = d->feetpos(); - int node1 = -1, node2 = -1; - float mindist1 = CLOSEDIST*CLOSEDIST, mindist2 = CLOSEDIST*CLOSEDIST; - loopv(d->ai->route) if(iswaypoint(d->ai->route[i])) - { - vec epos = waypoints[d->ai->route[i]].o; - float dist = epos.squaredist(pos); - if(dist > FARDIST*FARDIST) continue; - int entid = obstacles.remap(d, d->ai->route[i], epos); - if(entid >= 0) - { - if(entid != i) dist = epos.squaredist(pos); - if(dist < mindist1) { node1 = i; mindist1 = dist; } - } - else if(dist < mindist2) { node2 = i; mindist2 = dist; } - } - return node1 >= 0 ? node1 : node2; - } - - int wpspot(fpsent *d, int n, bool check = false) - { - if(iswaypoint(n)) loopk(2) - { - vec epos = waypoints[n].o; - int entid = obstacles.remap(d, n, epos, k!=0); - if(iswaypoint(entid)) - { - d->ai->spot = epos; - d->ai->targnode = entid; - return !check || d->feetpos().squaredist(epos) > MINWPDIST*MINWPDIST ? 1 : 2; - } - } - return 0; - } - - int randomlink(fpsent *d, int n) - { - if(iswaypoint(n) && waypoints[n].haslinks()) - { - waypoint &w = waypoints[n]; - static vector<int> linkmap; linkmap.setsize(0); - loopi(MAXWAYPOINTLINKS) - { - if(!w.links[i]) break; - if(iswaypoint(w.links[i]) && !d->ai->hasprevnode(w.links[i]) && d->ai->route.find(w.links[i]) < 0) - linkmap.add(w.links[i]); - } - if(!linkmap.empty()) return linkmap[rnd(linkmap.length())]; - } - return -1; - } - - bool anynode(fpsent *d, aistate &b, int len = NUMPREVNODES) - { - if(iswaypoint(d->lastnode)) loopk(2) - { - d->ai->clear(k ? true : false); - int n = randomlink(d, d->lastnode); - if(wpspot(d, n)) - { - d->ai->route.add(n); - d->ai->route.add(d->lastnode); - loopi(len) - { - n = randomlink(d, n); - if(iswaypoint(n)) d->ai->route.insert(0, n); - else break; - } - return true; - } - } - return false; - } - - bool checkroute(fpsent *d, int n) - { - if(d->ai->route.empty() || !d->ai->route.inrange(n)) return false; - int last = d->ai->lastcheck ? lastmillis-d->ai->lastcheck : 0; - if(last < 500 || n < 3) return false; // route length is too short - d->ai->lastcheck = lastmillis; - int w = iswaypoint(d->lastnode) ? d->lastnode : d->ai->route[n], c = min(n-1, NUMPREVNODES); - loopj(c) // check ahead to see if we need to go around something - { - int p = n-j-1, v = d->ai->route[p]; - if(d->ai->hasprevnode(v) || obstacles.find(v, d)) // something is in the way, try to remap around it - { - int m = p-1; - if(m < 3) return false; // route length is too short from this point - loopirev(m) - { - int t = d->ai->route[i]; - if(!d->ai->hasprevnode(t) && !obstacles.find(t, d)) - { - static vector<int> remap; remap.setsize(0); - if(route(d, w, t, remap, obstacles)) - { // kill what we don't want and put the remap in - while(d->ai->route.length() > i) d->ai->route.pop(); - loopvk(remap) d->ai->route.add(remap[k]); - return true; - } - return false; // we failed - } - } - return false; - } - } - return false; - } - - bool hunt(fpsent *d, aistate &b) - { - if(!d->ai->route.empty()) - { - int n = closenode(d); - if(d->ai->route.inrange(n) && checkroute(d, n)) n = closenode(d); - if(d->ai->route.inrange(n)) - { - if(!n) - { - switch(wpspot(d, d->ai->route[n], true)) - { - case 2: d->ai->clear(false); - [[fallthrough]]; - - case 1: return true; // not close enough to pop it yet - default: break; - } - } - else - { - while(d->ai->route.length() > n+1) d->ai->route.pop(); // waka-waka-waka-waka - int m = n-1; // next, please! - if(d->ai->route.inrange(m) && wpspot(d, d->ai->route[m])) return true; - } - } - } - b.override = false; - return anynode(d, b); - } - - void jumpto(fpsent *d, aistate &b, const vec &pos) - { + } + vec dp = d->headpos(); + findorientation(dp, d->yaw, d->pitch, d->ai->target); + } + + void spawned(fpsent *d) + { + if(d->ai) setup(d); + } + + void killed(fpsent *d, fpsent *e) + { + if(d->ai) d->ai->reset(); + } + + void itemspawned(int ent) + { + if(entities::ents.inrange(ent) && entities::ents[ent]->type >= I_SHELLS && entities::ents[ent]->type <= I_QUAD) + { + loopv(players) if(players[i] && players[i]->ai && players[i]->aitype == AI_BOT && players[i]->canpickup(entities::ents[ent]->type)) + { + fpsent *d = players[i]; + bool wantsitem = false; + switch(entities::ents[ent]->type) + { + case I_BOOST: + + case I_HEALTH: wantsitem = badhealth(d); break; + case I_GREENARMOUR: + + case I_YELLOWARMOUR: + + case I_QUAD: break; + default: + { + itemstat &is = itemstats[entities::ents[ent]->type-I_SHELLS]; + wantsitem = isgoodammo(is.info) && d->ammo[is.info] <= (d->ai->weappref == is.info ? is.add : is.add/2); + break; + } + } + if(wantsitem) + { + aistate &b = d->ai->getstate(); + if(b.targtype == AI_T_AFFINITY) continue; + if(b.type == AI_S_INTEREST && b.targtype == AI_T_ENTITY) + { + if(entities::ents.inrange(b.target)) + { + if(d->o.squaredist(entities::ents[ent]->o) < d->o.squaredist(entities::ents[b.target]->o)) + d->ai->switchstate(b, AI_S_INTEREST, AI_T_ENTITY, ent); + } + continue; + } + d->ai->switchstate(b, AI_S_INTEREST, AI_T_ENTITY, ent); + } + } + } + } + + int dowait(fpsent *d, aistate &b) + { + d->ai->clear(true); // ensure they're clean + if(find(d, b)) return 1; + if(target(d, b, 4, false)) return 1; + if(target(d, b, 4, true)) return 1; + if(randomnode(d, b, SIGHTMIN, 1e16f)) + { + d->ai->switchstate(b, AI_S_INTEREST, AI_T_NODE, d->ai->route[0]); + return 1; + } + return 0; // but don't pop the state + } + + int dodefend(fpsent *d, aistate &b) + { + if(d->state == CS_ALIVE) + { + switch(b.targtype) + { + case AI_T_NODE: + if(iswaypoint(b.target)) return defend(d, b, waypoints[b.target].o) ? 1 : 0; + break; + case AI_T_ENTITY: + if(entities::ents.inrange(b.target)) return defend(d, b, entities::ents[b.target]->o) ? 1 : 0; + break; + case AI_T_PLAYER: + { + fpsent *e = getclient(b.target); + if(e && e->state == CS_ALIVE) return defend(d, b, e->feetpos()) ? 1 : 0; + break; + } + default: break; + } + } + return 0; + } + + int dointerest(fpsent *d, aistate &b) + { + if(d->state != CS_ALIVE) return 0; + switch(b.targtype) + { + case AI_T_NODE: // this is like a wait state without sitting still.. + if(find(d, b)) return 1; + if(target(d, b, 4, true)) return 1; + if(iswaypoint(b.target) && vec(waypoints[b.target].o).sub(d->feetpos()).magnitude() > CLOSEDIST) + return makeroute(d, b, waypoints[b.target].o) ? 1 : 0; + break; + case AI_T_ENTITY: + if(entities::ents.inrange(b.target)) + { + extentity &e = *(extentity *)entities::ents[b.target]; + if(!e.spawned() || e.nopickup() || e.type < I_SHELLS || e.type > I_CARTRIDGES || d->hasmaxammo(e.type)) return 0; + //if(d->feetpos().squaredist(e.o) <= CLOSEDIST*CLOSEDIST) + //{ + // b.idle = 1; + // return true; + //} + return makeroute(d, b, e.o) ? 1 : 0; + } + break; + } + return 0; + } + + int dopursue(fpsent *d, aistate &b) + { + if(d->state == CS_ALIVE) + { + switch(b.targtype) + { + case AI_T_NODE: + { + if(iswaypoint(b.target)) + return defend(d, b, waypoints[b.target].o) ? 1 : 0; + break; + } + + case AI_T_PLAYER: + { + fpsent *e = getclient(b.target); + if(e && e->state == CS_ALIVE) + { + float guard = SIGHTMIN, wander = guns[d->gunselect].range; + if(d->gunselect == GUN_FIST) guard = 0.f; + return patrol(d, b, e->feetpos(), guard, wander) ? 1 : 0; + } + break; + } + default: break; + } + } + return 0; + } + + int closenode(fpsent *d) + { + vec pos = d->feetpos(); + int node1 = -1, node2 = -1; + float mindist1 = CLOSEDIST*CLOSEDIST, mindist2 = CLOSEDIST*CLOSEDIST; + loopv(d->ai->route) if(iswaypoint(d->ai->route[i])) + { + vec epos = waypoints[d->ai->route[i]].o; + float dist = epos.squaredist(pos); + if(dist > FARDIST*FARDIST) continue; + int entid = obstacles.remap(d, d->ai->route[i], epos); + if(entid >= 0) + { + if(entid != i) dist = epos.squaredist(pos); + if(dist < mindist1) { node1 = i; mindist1 = dist; } + } + else if(dist < mindist2) { node2 = i; mindist2 = dist; } + } + return node1 >= 0 ? node1 : node2; + } + + int wpspot(fpsent *d, int n, bool check = false) + { + if(iswaypoint(n)) loopk(2) + { + vec epos = waypoints[n].o; + int entid = obstacles.remap(d, n, epos, k!=0); + if(iswaypoint(entid)) + { + d->ai->spot = epos; + d->ai->targnode = entid; + return !check || d->feetpos().squaredist(epos) > MINWPDIST*MINWPDIST ? 1 : 2; + } + } + return 0; + } + + int randomlink(fpsent *d, int n) + { + if(iswaypoint(n) && waypoints[n].haslinks()) + { + waypoint &w = waypoints[n]; + static vector<int> linkmap; linkmap.setsize(0); + loopi(MAXWAYPOINTLINKS) + { + if(!w.links[i]) break; + if(iswaypoint(w.links[i]) && !d->ai->hasprevnode(w.links[i]) && d->ai->route.find(w.links[i]) < 0) + linkmap.add(w.links[i]); + } + if(!linkmap.empty()) return linkmap[rnd(linkmap.length())]; + } + return -1; + } + + bool anynode(fpsent *d, aistate &b, int len = NUMPREVNODES) + { + if(iswaypoint(d->lastnode)) loopk(2) + { + d->ai->clear(k ? true : false); + int n = randomlink(d, d->lastnode); + if(wpspot(d, n)) + { + d->ai->route.add(n); + d->ai->route.add(d->lastnode); + loopi(len) + { + n = randomlink(d, n); + if(iswaypoint(n)) d->ai->route.insert(0, n); + else break; + } + return true; + } + } + return false; + } + + bool checkroute(fpsent *d, int n) + { + if(d->ai->route.empty() || !d->ai->route.inrange(n)) return false; + int last = d->ai->lastcheck ? lastmillis-d->ai->lastcheck : 0; + if(last < 500 || n < 3) return false; // route length is too short + d->ai->lastcheck = lastmillis; + int w = iswaypoint(d->lastnode) ? d->lastnode : d->ai->route[n], c = min(n-1, NUMPREVNODES); + loopj(c) // check ahead to see if we need to go around something + { + int p = n-j-1, v = d->ai->route[p]; + if(d->ai->hasprevnode(v) || obstacles.find(v, d)) // something is in the way, try to remap around it + { + int m = p-1; + if(m < 3) return false; // route length is too short from this point + loopirev(m) + { + int t = d->ai->route[i]; + if(!d->ai->hasprevnode(t) && !obstacles.find(t, d)) + { + static vector<int> remap; remap.setsize(0); + if(route(d, w, t, remap, obstacles)) + { // kill what we don't want and put the remap in + while(d->ai->route.length() > i) d->ai->route.pop(); + loopvk(remap) d->ai->route.add(remap[k]); + return true; + } + return false; // we failed + } + } + return false; + } + } + return false; + } + + bool hunt(fpsent *d, aistate &b) + { + if(!d->ai->route.empty()) + { + int n = closenode(d); + if(d->ai->route.inrange(n) && checkroute(d, n)) n = closenode(d); + if(d->ai->route.inrange(n)) + { + if(!n) + { + switch(wpspot(d, d->ai->route[n], true)) + { + case 2: d->ai->clear(false); + [[fallthrough]]; + + case 1: return true; // not close enough to pop it yet + default: break; + } + } + else + { + while(d->ai->route.length() > n+1) d->ai->route.pop(); // waka-waka-waka-waka + int m = n-1; // next, please! + if(d->ai->route.inrange(m) && wpspot(d, d->ai->route[m])) return true; + } + } + } + b.override = false; + return anynode(d, b); + } + + void jumpto(fpsent *d, aistate &b, const vec &pos) + { vec off = vec(pos).sub(d->feetpos()), dir(off.x, off.y, 0); - bool sequenced = d->ai->blockseq || d->ai->targseq, offground = d->timeinair && !d->inwater, - jump = !offground && lastmillis >= d->ai->jumpseed && (sequenced || off.z >= JUMPMIN || lastmillis >= d->ai->jumprand); + bool sequenced = d->ai->blockseq || d->ai->targseq, offground = d->timeinair && !d->inwater, + jump = !offground && lastmillis >= d->ai->jumpseed && (sequenced || off.z >= JUMPMIN || lastmillis >= d->ai->jumprand); if(jump) { vec old = d->o; @@ -903,551 +903,551 @@ namespace ai seed *= b.idle ? 50 : 25; d->ai->jumprand = lastmillis+seed+rnd(seed); } - } - - void fixfullrange(float &yaw, float &pitch, float &roll, bool full) - { - if(full) - { - while(pitch < -180.0f) pitch += 360.0f; - while(pitch >= 180.0f) pitch -= 360.0f; - while(roll < -180.0f) roll += 360.0f; - while(roll >= 180.0f) roll -= 360.0f; - } - else - { - if(pitch > 89.9f) pitch = 89.9f; - if(pitch < -89.9f) pitch = -89.9f; - if(roll > 89.9f) roll = 89.9f; - if(roll < -89.9f) roll = -89.9f; - } - while(yaw < 0.0f) yaw += 360.0f; - while(yaw >= 360.0f) yaw -= 360.0f; - } - - void fixrange(float &yaw, float &pitch) - { - float r = 0.f; - fixfullrange(yaw, pitch, r, false); - } - - void getyawpitch(const vec &from, const vec &pos, float &yaw, float &pitch) - { - float dist = from.dist(pos); - yaw = -atan2(pos.x-from.x, pos.y-from.y)/RAD; - pitch = asin((pos.z-from.z)/dist)/RAD; - } - - void scaleyawpitch(float &yaw, float &pitch, float targyaw, float targpitch, float frame, float scale) - { - if(yaw < targyaw-180.0f) yaw += 360.0f; - if(yaw > targyaw+180.0f) yaw -= 360.0f; - float offyaw = fabs(targyaw-yaw)*frame, offpitch = fabs(targpitch-pitch)*frame*scale; - if(targyaw > yaw) - { - yaw += offyaw; - if(targyaw < yaw) yaw = targyaw; - } - else if(targyaw < yaw) - { - yaw -= offyaw; - if(targyaw > yaw) yaw = targyaw; - } - if(targpitch > pitch) - { - pitch += offpitch; - if(targpitch < pitch) pitch = targpitch; - } - else if(targpitch < pitch) - { - pitch -= offpitch; - if(targpitch > pitch) pitch = targpitch; - } - fixrange(yaw, pitch); - } - - bool lockon(fpsent *d, fpsent *e, float maxdist) - { - if(d->gunselect == GUN_FIST && !d->blocked && !d->timeinair) - { - vec dir = vec(e->o).sub(d->o); - float xydist = dir.x*dir.x+dir.y*dir.y, zdist = dir.z*dir.z, mdist = maxdist*maxdist, ddist = d->radius*d->radius+e->radius*e->radius; - if(zdist <= ddist && xydist >= ddist+4 && xydist <= mdist+ddist) return true; - } - return false; - } - - int process(fpsent *d, aistate &b) - { - int result = 0, stupify = d->skill <= 10+rnd(15) ? rnd(d->skill*1000) : 0, skmod = 101-d->skill; - float frame = d->skill <= 100 ? float(lastmillis-d->ai->lastrun)/float(max(skmod,1)*10) : 1; - vec dp = d->headpos(); - - bool idle = b.idle == 1 || (stupify && stupify <= skmod); - d->ai->dontmove = false; - if(idle) - { - d->ai->lastaction = d->ai->lasthunt = lastmillis; - d->ai->dontmove = true; - d->ai->spot = vec(0, 0, 0); - } - else if(hunt(d, b)) - { - getyawpitch(dp, vec(d->ai->spot).add(vec(0, 0, d->eyeheight)), d->ai->targyaw, d->ai->targpitch); - d->ai->lasthunt = lastmillis; - } - else - { - idle = d->ai->dontmove = true; - d->ai->spot = vec(0, 0, 0); - } + } + + void fixfullrange(float &yaw, float &pitch, float &roll, bool full) + { + if(full) + { + while(pitch < -180.0f) pitch += 360.0f; + while(pitch >= 180.0f) pitch -= 360.0f; + while(roll < -180.0f) roll += 360.0f; + while(roll >= 180.0f) roll -= 360.0f; + } + else + { + if(pitch > 89.9f) pitch = 89.9f; + if(pitch < -89.9f) pitch = -89.9f; + if(roll > 89.9f) roll = 89.9f; + if(roll < -89.9f) roll = -89.9f; + } + while(yaw < 0.0f) yaw += 360.0f; + while(yaw >= 360.0f) yaw -= 360.0f; + } + + void fixrange(float &yaw, float &pitch) + { + float r = 0.f; + fixfullrange(yaw, pitch, r, false); + } + + void getyawpitch(const vec &from, const vec &pos, float &yaw, float &pitch) + { + float dist = from.dist(pos); + yaw = -atan2(pos.x-from.x, pos.y-from.y)/RAD; + pitch = asin((pos.z-from.z)/dist)/RAD; + } + + void scaleyawpitch(float &yaw, float &pitch, float targyaw, float targpitch, float frame, float scale) + { + if(yaw < targyaw-180.0f) yaw += 360.0f; + if(yaw > targyaw+180.0f) yaw -= 360.0f; + float offyaw = fabs(targyaw-yaw)*frame, offpitch = fabs(targpitch-pitch)*frame*scale; + if(targyaw > yaw) + { + yaw += offyaw; + if(targyaw < yaw) yaw = targyaw; + } + else if(targyaw < yaw) + { + yaw -= offyaw; + if(targyaw > yaw) yaw = targyaw; + } + if(targpitch > pitch) + { + pitch += offpitch; + if(targpitch < pitch) pitch = targpitch; + } + else if(targpitch < pitch) + { + pitch -= offpitch; + if(targpitch > pitch) pitch = targpitch; + } + fixrange(yaw, pitch); + } + + bool lockon(fpsent *d, fpsent *e, float maxdist) + { + if(d->gunselect == GUN_FIST && !d->blocked && !d->timeinair) + { + vec dir = vec(e->o).sub(d->o); + float xydist = dir.x*dir.x+dir.y*dir.y, zdist = dir.z*dir.z, mdist = maxdist*maxdist, ddist = d->radius*d->radius+e->radius*e->radius; + if(zdist <= ddist && xydist >= ddist+4 && xydist <= mdist+ddist) return true; + } + return false; + } + + int process(fpsent *d, aistate &b) + { + int result = 0, stupify = d->skill <= 10+rnd(15) ? rnd(d->skill*1000) : 0, skmod = 101-d->skill; + float frame = d->skill <= 100 ? float(lastmillis-d->ai->lastrun)/float(max(skmod,1)*10) : 1; + vec dp = d->headpos(); + + bool idle = b.idle == 1 || (stupify && stupify <= skmod); + d->ai->dontmove = false; + if(idle) + { + d->ai->lastaction = d->ai->lasthunt = lastmillis; + d->ai->dontmove = true; + d->ai->spot = vec(0, 0, 0); + } + else if(hunt(d, b)) + { + getyawpitch(dp, vec(d->ai->spot).add(vec(0, 0, d->eyeheight)), d->ai->targyaw, d->ai->targpitch); + d->ai->lasthunt = lastmillis; + } + else + { + idle = d->ai->dontmove = true; + d->ai->spot = vec(0, 0, 0); + } if(!d->ai->dontmove) jumpto(d, b, d->ai->spot); - fpsent *e = getclient(d->ai->enemy); - bool enemyok = e && targetable(d, e); - if(!enemyok || d->skill >= 50) - { - fpsent *f = (fpsent *)intersectclosest(dp, d->ai->target, d); - if(f) - { - if(targetable(d, f)) - { - if(!enemyok) violence(d, b, f, d->gunselect == GUN_FIST ? 1 : 0); - enemyok = true; - e = f; - } - else enemyok = false; - } - else if(!enemyok && target(d, b, d->gunselect == GUN_FIST ? 1 : 0, false, SIGHTMIN)) - enemyok = (e = getclient(d->ai->enemy)) != NULL; - } - if(enemyok) - { - vec ep = getaimpos(d, e); - float yaw, pitch; - getyawpitch(dp, ep, yaw, pitch); - fixrange(yaw, pitch); - bool insight = cansee(d, dp, ep), hasseen = d->ai->enemyseen && lastmillis-d->ai->enemyseen <= (d->skill*10)+3000, - quick = d->ai->enemyseen && lastmillis-d->ai->enemyseen <= (d->gunselect == GUN_CG ? 300 : skmod)+30; - if(insight) d->ai->enemyseen = lastmillis; - if(idle || insight || hasseen || quick) - { - float sskew = insight || d->skill > 100 ? 1.5f : (hasseen ? 1.f : 0.5f); - if(insight && lockon(d, e, 16)) - { - d->ai->targyaw = yaw; - d->ai->targpitch = pitch; - if(!idle) frame *= 2; - d->ai->becareful = false; - } - scaleyawpitch(d->yaw, d->pitch, yaw, pitch, frame, sskew); - if(insight || quick) - { - if(canshoot(d, e) && hastarget(d, b, e, yaw, pitch, dp.squaredist(ep))) - { - d->attacking = true; - d->ai->lastaction = lastmillis; - result = 3; - } - else result = 2; - } - else result = 1; - } - else - { - if(!d->ai->enemyseen || lastmillis-d->ai->enemyseen > (d->skill*50)+3000) - { - d->ai->enemy = -1; - d->ai->enemyseen = d->ai->enemymillis = 0; - } - enemyok = false; - result = 0; - } - } - else - { - if(!enemyok) - { - d->ai->enemy = -1; - d->ai->enemyseen = d->ai->enemymillis = 0; - } - enemyok = false; - result = 0; - } - - fixrange(d->ai->targyaw, d->ai->targpitch); - if(!result) scaleyawpitch(d->yaw, d->pitch, d->ai->targyaw, d->ai->targpitch, frame*0.25f, 1.f); - - if(d->ai->becareful && d->physstate == PHYS_FALL) - { - float offyaw, offpitch; - vectoyawpitch(d->vel, offyaw, offpitch); - offyaw -= d->yaw; offpitch -= d->pitch; - if(fabs(offyaw)+fabs(offpitch) >= 135) d->ai->becareful = false; - else if(d->ai->becareful) d->ai->dontmove = true; - } - else d->ai->becareful = false; - - if(d->ai->dontmove) d->move = d->strafe = 0; - else - { // our guys move one way.. but turn another?! :) - const struct aimdir { int move, strafe, offset; } aimdirs[8] = - { - { 1, 0, 0 }, - { 1, -1, 45 }, - { 0, -1, 90 }, - { -1, -1, 135 }, - { -1, 0, 180 }, - { -1, 1, 225 }, - { 0, 1, 270 }, - { 1, 1, 315 } - }; - float yaw = d->ai->targyaw-d->yaw; - while(yaw < 0.0f) yaw += 360.0f; - while(yaw >= 360.0f) yaw -= 360.0f; - int r = clamp(((int)floor((yaw+22.5f)/45.0f))&7, 0, 7); - const aimdir &ad = aimdirs[r]; - d->move = ad.move; - d->strafe = ad.strafe; - } - findorientation(dp, d->yaw, d->pitch, d->ai->target); - return result; - } - - bool hasrange(fpsent *d, fpsent *e, int weap) - { - if(!e) return true; - if(targetable(d, e)) - { - vec ep = getaimpos(d, e); - float dist = ep.squaredist(d->headpos()); - if(weaprange(d, weap, dist)) return true; - } - return false; - } - - bool request(fpsent *d, aistate &b) - { - fpsent *e = getclient(d->ai->enemy); - if(!d->hasammo(d->gunselect) || !hasrange(d, e, d->gunselect) || (d->gunselect != d->ai->weappref && (!isgoodammo(d->gunselect) || d->hasammo(d->ai->weappref)))) - { - static const int gunprefs[] = { GUN_CG, GUN_RL, GUN_SG, GUN_RIFLE, GUN_GL, GUN_PISTOL, GUN_FIST }; - int gun = -1; - if(d->hasammo(d->ai->weappref) && hasrange(d, e, d->ai->weappref)) gun = d->ai->weappref; - else - { - loopi(sizeof(gunprefs)/sizeof(gunprefs[0])) if(d->hasammo(gunprefs[i]) && hasrange(d, e, gunprefs[i])) - { - gun = gunprefs[i]; - break; - } - } - if(gun >= 0 && gun != d->gunselect) gunselect(gun, d); - } - return process(d, b) >= 2; - } + fpsent *e = getclient(d->ai->enemy); + bool enemyok = e && targetable(d, e); + if(!enemyok || d->skill >= 50) + { + fpsent *f = (fpsent *)intersectclosest(dp, d->ai->target, d); + if(f) + { + if(targetable(d, f)) + { + if(!enemyok) violence(d, b, f, d->gunselect == GUN_FIST ? 1 : 0); + enemyok = true; + e = f; + } + else enemyok = false; + } + else if(!enemyok && target(d, b, d->gunselect == GUN_FIST ? 1 : 0, false, SIGHTMIN)) + enemyok = (e = getclient(d->ai->enemy)) != NULL; + } + if(enemyok) + { + vec ep = getaimpos(d, e); + float yaw, pitch; + getyawpitch(dp, ep, yaw, pitch); + fixrange(yaw, pitch); + bool insight = cansee(d, dp, ep), hasseen = d->ai->enemyseen && lastmillis-d->ai->enemyseen <= (d->skill*10)+3000, + quick = d->ai->enemyseen && lastmillis-d->ai->enemyseen <= (d->gunselect == GUN_CG ? 300 : skmod)+30; + if(insight) d->ai->enemyseen = lastmillis; + if(idle || insight || hasseen || quick) + { + float sskew = insight || d->skill > 100 ? 1.5f : (hasseen ? 1.f : 0.5f); + if(insight && lockon(d, e, 16)) + { + d->ai->targyaw = yaw; + d->ai->targpitch = pitch; + if(!idle) frame *= 2; + d->ai->becareful = false; + } + scaleyawpitch(d->yaw, d->pitch, yaw, pitch, frame, sskew); + if(insight || quick) + { + if(canshoot(d, e) && hastarget(d, b, e, yaw, pitch, dp.squaredist(ep))) + { + d->attacking = true; + d->ai->lastaction = lastmillis; + result = 3; + } + else result = 2; + } + else result = 1; + } + else + { + if(!d->ai->enemyseen || lastmillis-d->ai->enemyseen > (d->skill*50)+3000) + { + d->ai->enemy = -1; + d->ai->enemyseen = d->ai->enemymillis = 0; + } + enemyok = false; + result = 0; + } + } + else + { + if(!enemyok) + { + d->ai->enemy = -1; + d->ai->enemyseen = d->ai->enemymillis = 0; + } + enemyok = false; + result = 0; + } + + fixrange(d->ai->targyaw, d->ai->targpitch); + if(!result) scaleyawpitch(d->yaw, d->pitch, d->ai->targyaw, d->ai->targpitch, frame*0.25f, 1.f); + + if(d->ai->becareful && d->physstate == PHYS_FALL) + { + float offyaw, offpitch; + vectoyawpitch(d->vel, offyaw, offpitch); + offyaw -= d->yaw; offpitch -= d->pitch; + if(fabs(offyaw)+fabs(offpitch) >= 135) d->ai->becareful = false; + else if(d->ai->becareful) d->ai->dontmove = true; + } + else d->ai->becareful = false; + + if(d->ai->dontmove) d->move = d->strafe = 0; + else + { // our guys move one way.. but turn another?! :) + const struct aimdir { int move, strafe, offset; } aimdirs[8] = + { + { 1, 0, 0 }, + { 1, -1, 45 }, + { 0, -1, 90 }, + { -1, -1, 135 }, + { -1, 0, 180 }, + { -1, 1, 225 }, + { 0, 1, 270 }, + { 1, 1, 315 } + }; + float yaw = d->ai->targyaw-d->yaw; + while(yaw < 0.0f) yaw += 360.0f; + while(yaw >= 360.0f) yaw -= 360.0f; + int r = clamp(((int)floor((yaw+22.5f)/45.0f))&7, 0, 7); + const aimdir &ad = aimdirs[r]; + d->move = ad.move; + d->strafe = ad.strafe; + } + findorientation(dp, d->yaw, d->pitch, d->ai->target); + return result; + } + + bool hasrange(fpsent *d, fpsent *e, int weap) + { + if(!e) return true; + if(targetable(d, e)) + { + vec ep = getaimpos(d, e); + float dist = ep.squaredist(d->headpos()); + if(weaprange(d, weap, dist)) return true; + } + return false; + } + + bool request(fpsent *d, aistate &b) + { + fpsent *e = getclient(d->ai->enemy); + if(!d->hasammo(d->gunselect) || !hasrange(d, e, d->gunselect) || (d->gunselect != d->ai->weappref && (!isgoodammo(d->gunselect) || d->hasammo(d->ai->weappref)))) + { + static const int gunprefs[] = { GUN_CG, GUN_RL, GUN_SG, GUN_RIFLE, GUN_GL, GUN_PISTOL, GUN_FIST }; + int gun = -1; + if(d->hasammo(d->ai->weappref) && hasrange(d, e, d->ai->weappref)) gun = d->ai->weappref; + else + { + loopi(sizeof(gunprefs)/sizeof(gunprefs[0])) if(d->hasammo(gunprefs[i]) && hasrange(d, e, gunprefs[i])) + { + gun = gunprefs[i]; + break; + } + } + if(gun >= 0 && gun != d->gunselect) gunselect(gun, d); + } + return process(d, b) >= 2; + } void timeouts(fpsent *d, aistate &b) { - if(d->blocked) - { - d->ai->blocktime += lastmillis-d->ai->lastrun; - if(d->ai->blocktime > (d->ai->blockseq+1)*1000) - { - d->ai->blockseq++; - switch(d->ai->blockseq) - { - case 1: case 2: case 3: - if(entities::ents.inrange(d->ai->targnode)) d->ai->addprevnode(d->ai->targnode); - d->ai->clear(false); - break; - case 4: d->ai->reset(true); break; - case 5: d->ai->reset(false); break; - case 6: default: suicide(d); return; break; // this is our last resort.. - } - } - } - else d->ai->blocktime = d->ai->blockseq = 0; - - if(d->ai->targnode == d->ai->targlast) - { - d->ai->targtime += lastmillis-d->ai->lastrun; - if(d->ai->targtime > (d->ai->targseq+1)*1000) - { - d->ai->targseq++; - switch(d->ai->targseq) - { - case 1: case 2: case 3: - if(entities::ents.inrange(d->ai->targnode)) d->ai->addprevnode(d->ai->targnode); - d->ai->clear(false); - break; - case 4: d->ai->reset(true); break; - case 5: d->ai->reset(false); break; - case 6: default: suicide(d); return; break; // this is our last resort.. - } - } - } - else - { - d->ai->targtime = d->ai->targseq = 0; - d->ai->targlast = d->ai->targnode; - } - - if(d->ai->lasthunt) - { - int millis = lastmillis-d->ai->lasthunt; - if(millis <= 1000) { d->ai->tryreset = false; d->ai->huntseq = 0; } - else if(millis > (d->ai->huntseq+1)*1000) - { - d->ai->huntseq++; - switch(d->ai->huntseq) - { - case 1: d->ai->reset(true); break; - case 2: d->ai->reset(false); break; - case 3: default: suicide(d); return; break; // this is our last resort.. - } - } - } - } - - void logic(fpsent *d, aistate &b, bool run) - { - bool allowmove = canmove(d) && b.type != AI_S_WAIT; - if(d->state != CS_ALIVE || !allowmove) d->stopmoving(); - if(d->state == CS_ALIVE) - { - if(allowmove) - { - if(!request(d, b)) target(d, b, d->gunselect == GUN_FIST ? 1 : 0, b.idle ? true : false); - shoot(d, d->ai->target); - } - if(!intermission) - { - if(d->ragdoll) cleanragdoll(d); - moveplayer(d, 10, true); - if(allowmove && !b.idle) timeouts(d, b); - if(d->quadmillis) entities::checkquad(curtime, d); - entities::checkitems(d); - } - } - else if(d->state == CS_DEAD) - { - if(d->ragdoll) moveragdoll(d); - else if(lastmillis-d->lastpain<2000) - { - d->move = d->strafe = 0; - moveplayer(d, 10, false); - } - } - d->attacking = d->jumping = false; - } + if(d->blocked) + { + d->ai->blocktime += lastmillis-d->ai->lastrun; + if(d->ai->blocktime > (d->ai->blockseq+1)*1000) + { + d->ai->blockseq++; + switch(d->ai->blockseq) + { + case 1: case 2: case 3: + if(entities::ents.inrange(d->ai->targnode)) d->ai->addprevnode(d->ai->targnode); + d->ai->clear(false); + break; + case 4: d->ai->reset(true); break; + case 5: d->ai->reset(false); break; + case 6: default: suicide(d); return; break; // this is our last resort.. + } + } + } + else d->ai->blocktime = d->ai->blockseq = 0; + + if(d->ai->targnode == d->ai->targlast) + { + d->ai->targtime += lastmillis-d->ai->lastrun; + if(d->ai->targtime > (d->ai->targseq+1)*1000) + { + d->ai->targseq++; + switch(d->ai->targseq) + { + case 1: case 2: case 3: + if(entities::ents.inrange(d->ai->targnode)) d->ai->addprevnode(d->ai->targnode); + d->ai->clear(false); + break; + case 4: d->ai->reset(true); break; + case 5: d->ai->reset(false); break; + case 6: default: suicide(d); return; break; // this is our last resort.. + } + } + } + else + { + d->ai->targtime = d->ai->targseq = 0; + d->ai->targlast = d->ai->targnode; + } + + if(d->ai->lasthunt) + { + int millis = lastmillis-d->ai->lasthunt; + if(millis <= 1000) { d->ai->tryreset = false; d->ai->huntseq = 0; } + else if(millis > (d->ai->huntseq+1)*1000) + { + d->ai->huntseq++; + switch(d->ai->huntseq) + { + case 1: d->ai->reset(true); break; + case 2: d->ai->reset(false); break; + case 3: default: suicide(d); return; break; // this is our last resort.. + } + } + } + } + + void logic(fpsent *d, aistate &b, bool run) + { + bool allowmove = canmove(d) && b.type != AI_S_WAIT; + if(d->state != CS_ALIVE || !allowmove) d->stopmoving(); + if(d->state == CS_ALIVE) + { + if(allowmove) + { + if(!request(d, b)) target(d, b, d->gunselect == GUN_FIST ? 1 : 0, b.idle ? true : false); + shoot(d, d->ai->target); + } + if(!intermission) + { + if(d->ragdoll) cleanragdoll(d); + moveplayer(d, 10, true); + if(allowmove && !b.idle) timeouts(d, b); + if(d->quadmillis) entities::checkquad(curtime, d); + entities::checkitems(d); + } + } + else if(d->state == CS_DEAD) + { + if(d->ragdoll) moveragdoll(d); + else if(lastmillis-d->lastpain<2000) + { + d->move = d->strafe = 0; + moveplayer(d, 10, false); + } + } + d->attacking = d->jumping = false; + } void avoid() - { - // guess as to the radius of ai and other critters relying on the avoid set for now - float guessradius = player1->radius; - obstacles.clear(); - loopv(players) - { - dynent *d = players[i]; - if(d->state != CS_ALIVE) continue; - obstacles.avoidnear(d, d->o.z + d->aboveeye + 1, d->feetpos(), guessradius + d->radius); - } + { + // guess as to the radius of ai and other critters relying on the avoid set for now + float guessradius = player1->radius; + obstacles.clear(); + loopv(players) + { + dynent *d = players[i]; + if(d->state != CS_ALIVE) continue; + obstacles.avoidnear(d, d->o.z + d->aboveeye + 1, d->feetpos(), guessradius + d->radius); + } extern avoidset wpavoid; obstacles.add(wpavoid); avoidweapons(obstacles, guessradius); - } - - void think(fpsent *d, bool run) - { - // the state stack works like a chain of commands, certain commands simply replace each other - // others spawn new commands to the stack the ai reads the top command from the stack and executes - // it or pops the stack and goes back along the history until it finds a suitable command to execute - bool cleannext = false; - if(d->ai->state.empty()) d->ai->addstate(AI_S_WAIT); - loopvrev(d->ai->state) - { - aistate &c = d->ai->state[i]; - if(cleannext) - { - c.millis = lastmillis; - c.override = false; - cleannext = false; - } - if(d->state == CS_DEAD && d->respawned!=d->lifesequence && lastmillis - d->lastpain >= 500) - { - addmsg(N_TRYSPAWN, "rc", d); - d->respawned = d->lifesequence; - } - else if(d->state == CS_ALIVE && run) - { - int result = 0; - c.idle = 0; - switch(c.type) - { - case AI_S_WAIT: result = dowait(d, c); break; - case AI_S_DEFEND: result = dodefend(d, c); break; - case AI_S_PURSUE: result = dopursue(d, c); break; - case AI_S_INTEREST: result = dointerest(d, c); break; - default: result = 0; break; - } - if(result <= 0) - { - if(c.type != AI_S_WAIT) - { - switch(result) - { - case 0: default: d->ai->removestate(i); cleannext = true; break; - case -1: i = d->ai->state.length()-1; break; - } - continue; // shouldn't interfere - } - } - } - logic(d, c, run); - break; - } - if(d->ai->trywipe) d->ai->wipe(); - d->ai->lastrun = lastmillis; - } - - void drawroute(fpsent *d, float amt = 1.f) - { - int last = -1; - loopvrev(d->ai->route) - { - if(d->ai->route.inrange(last)) - { - int index = d->ai->route[i], prev = d->ai->route[last]; - if(iswaypoint(index) && iswaypoint(prev)) - { - waypoint &e = waypoints[index], &f = waypoints[prev]; - vec fr = f.o, dr = e.o; - fr.z += amt; dr.z += amt; - particle_flare(fr, dr, 1, PART_STREAK, 0xFFFFFF); - } - } - last = i; - } - if(aidebug >= 5) - { - vec pos = d->feetpos(); - if(d->ai->spot != vec(0, 0, 0)) particle_flare(pos, d->ai->spot, 1, PART_LIGHTNING, 0x00FFFF); - if(iswaypoint(d->ai->targnode)) - particle_flare(pos, waypoints[d->ai->targnode].o, 1, PART_LIGHTNING, 0xFF00FF); - if(iswaypoint(d->lastnode)) - particle_flare(pos, waypoints[d->lastnode].o, 1, PART_LIGHTNING, 0xFFFF00); - loopi(NUMPREVNODES) if(iswaypoint(d->ai->prevnodes[i])) - { - particle_flare(pos, waypoints[d->ai->prevnodes[i]].o, 1, PART_LIGHTNING, 0x884400); - pos = waypoints[d->ai->prevnodes[i]].o; - } - } - } - - VAR(showwaypoints, 0, 0, 1); - VAR(showwaypointsradius, 0, 200, 10000); - - const char *stnames[AI_S_MAX] = { - "wait", "defend", "pursue", "interest" - }, *sttypes[AI_T_MAX+1] = { - "none", "node", "player", "affinity", "entity" - }; - void render() - { - if(aidebug > 1) - { - int total = 0, alive = 0; - loopv(players) if(players[i]->ai) total++; - loopv(players) if(players[i]->state == CS_ALIVE && players[i]->ai) - { - fpsent *d = players[i]; - vec pos = d->abovehead(); - pos.z += 3; - alive++; - if(aidebug >= 4) drawroute(d, 4.f*(float(alive)/float(total))); - if(aidebug >= 3) - { - defformatstring(q, "node: %d route: %d (%d)", - d->lastnode, - !d->ai->route.empty() ? d->ai->route[0] : -1, - d->ai->route.length() - ); - particle_textcopy(pos, q, PART_TEXT, 1); - pos.z += 2; - } - bool top = true; - loopvrev(d->ai->state) - { - aistate &b = d->ai->state[i]; - defformatstring(s, "%s%s (%d ms) %s:%d", - top ? "\fg" : "\fy", - stnames[b.type], - lastmillis-b.millis, - sttypes[b.targtype+1], b.target - ); - particle_textcopy(pos, s, PART_TEXT, 1); - pos.z += 2; - if(top) - { - if(aidebug >= 3) top = false; - else break; - } - } - if(aidebug >= 3) - { - if(d->ai->weappref >= 0 && d->ai->weappref < NUMGUNS) - { - particle_textcopy(pos, guns[d->ai->weappref].name, PART_TEXT, 1); - pos.z += 2; - } - fpsent *e = getclient(d->ai->enemy); - if(e) - { - particle_textcopy(pos, colorname(e), PART_TEXT, 1); - pos.z += 2; - } - } - } - if(aidebug >= 4) - { - int cur = 0; - loopv(obstacles.obstacles) - { - const avoidset::obstacle &ob = obstacles.obstacles[i]; - int next = cur + ob.numwaypoints; - for(; cur < next; cur++) - { - int ent = obstacles.waypoints[cur]; - if(iswaypoint(ent)) - regular_particle_splash(PART_EDIT, 2, 40, waypoints[ent].o, 0xFF6600, 1.5f); - } - cur = next; - } - } - } - if(showwaypoints || aidebug >= 6) - { - vector<int> close; - int len = waypoints.length(); - if(showwaypointsradius) - { - findwaypointswithin(camera1->o, 0, showwaypointsradius, close); - len = close.length(); - } - loopi(len) - { - waypoint &w = waypoints[showwaypointsradius ? close[i] : i]; - loopj(MAXWAYPOINTLINKS) - { - int link = w.links[j]; - if(!link) break; - particle_flare(w.o, waypoints[link].o, 1, PART_STREAK, 0x0000FF); - } - } - - } - } + } + + void think(fpsent *d, bool run) + { + // the state stack works like a chain of commands, certain commands simply replace each other + // others spawn new commands to the stack the ai reads the top command from the stack and executes + // it or pops the stack and goes back along the history until it finds a suitable command to execute + bool cleannext = false; + if(d->ai->state.empty()) d->ai->addstate(AI_S_WAIT); + loopvrev(d->ai->state) + { + aistate &c = d->ai->state[i]; + if(cleannext) + { + c.millis = lastmillis; + c.override = false; + cleannext = false; + } + if(d->state == CS_DEAD && d->respawned!=d->lifesequence && lastmillis - d->lastpain >= 500) + { + addmsg(N_TRYSPAWN, "rc", d); + d->respawned = d->lifesequence; + } + else if(d->state == CS_ALIVE && run) + { + int result = 0; + c.idle = 0; + switch(c.type) + { + case AI_S_WAIT: result = dowait(d, c); break; + case AI_S_DEFEND: result = dodefend(d, c); break; + case AI_S_PURSUE: result = dopursue(d, c); break; + case AI_S_INTEREST: result = dointerest(d, c); break; + default: result = 0; break; + } + if(result <= 0) + { + if(c.type != AI_S_WAIT) + { + switch(result) + { + case 0: default: d->ai->removestate(i); cleannext = true; break; + case -1: i = d->ai->state.length()-1; break; + } + continue; // shouldn't interfere + } + } + } + logic(d, c, run); + break; + } + if(d->ai->trywipe) d->ai->wipe(); + d->ai->lastrun = lastmillis; + } + + void drawroute(fpsent *d, float amt = 1.f) + { + int last = -1; + loopvrev(d->ai->route) + { + if(d->ai->route.inrange(last)) + { + int index = d->ai->route[i], prev = d->ai->route[last]; + if(iswaypoint(index) && iswaypoint(prev)) + { + waypoint &e = waypoints[index], &f = waypoints[prev]; + vec fr = f.o, dr = e.o; + fr.z += amt; dr.z += amt; + particle_flare(fr, dr, 1, PART_STREAK, 0xFFFFFF); + } + } + last = i; + } + if(aidebug >= 5) + { + vec pos = d->feetpos(); + if(d->ai->spot != vec(0, 0, 0)) particle_flare(pos, d->ai->spot, 1, PART_LIGHTNING, 0x00FFFF); + if(iswaypoint(d->ai->targnode)) + particle_flare(pos, waypoints[d->ai->targnode].o, 1, PART_LIGHTNING, 0xFF00FF); + if(iswaypoint(d->lastnode)) + particle_flare(pos, waypoints[d->lastnode].o, 1, PART_LIGHTNING, 0xFFFF00); + loopi(NUMPREVNODES) if(iswaypoint(d->ai->prevnodes[i])) + { + particle_flare(pos, waypoints[d->ai->prevnodes[i]].o, 1, PART_LIGHTNING, 0x884400); + pos = waypoints[d->ai->prevnodes[i]].o; + } + } + } + + VAR(showwaypoints, 0, 0, 1); + VAR(showwaypointsradius, 0, 200, 10000); + + const char *stnames[AI_S_MAX] = { + "wait", "defend", "pursue", "interest" + }, *sttypes[AI_T_MAX+1] = { + "none", "node", "player", "affinity", "entity" + }; + void render() + { + if(aidebug > 1) + { + int total = 0, alive = 0; + loopv(players) if(players[i]->ai) total++; + loopv(players) if(players[i]->state == CS_ALIVE && players[i]->ai) + { + fpsent *d = players[i]; + vec pos = d->abovehead(); + pos.z += 3; + alive++; + if(aidebug >= 4) drawroute(d, 4.f*(float(alive)/float(total))); + if(aidebug >= 3) + { + defformatstring(q, "node: %d route: %d (%d)", + d->lastnode, + !d->ai->route.empty() ? d->ai->route[0] : -1, + d->ai->route.length() + ); + particle_textcopy(pos, q, PART_TEXT, 1); + pos.z += 2; + } + bool top = true; + loopvrev(d->ai->state) + { + aistate &b = d->ai->state[i]; + defformatstring(s, "%s%s (%d ms) %s:%d", + top ? "\fg" : "\fy", + stnames[b.type], + lastmillis-b.millis, + sttypes[b.targtype+1], b.target + ); + particle_textcopy(pos, s, PART_TEXT, 1); + pos.z += 2; + if(top) + { + if(aidebug >= 3) top = false; + else break; + } + } + if(aidebug >= 3) + { + if(d->ai->weappref >= 0 && d->ai->weappref < NUMGUNS) + { + particle_textcopy(pos, guns[d->ai->weappref].name, PART_TEXT, 1); + pos.z += 2; + } + fpsent *e = getclient(d->ai->enemy); + if(e) + { + particle_textcopy(pos, colorname(e), PART_TEXT, 1); + pos.z += 2; + } + } + } + if(aidebug >= 4) + { + int cur = 0; + loopv(obstacles.obstacles) + { + const avoidset::obstacle &ob = obstacles.obstacles[i]; + int next = cur + ob.numwaypoints; + for(; cur < next; cur++) + { + int ent = obstacles.waypoints[cur]; + if(iswaypoint(ent)) + regular_particle_splash(PART_EDIT, 2, 40, waypoints[ent].o, 0xFF6600, 1.5f); + } + cur = next; + } + } + } + if(showwaypoints || aidebug >= 6) + { + vector<int> close; + int len = waypoints.length(); + if(showwaypointsradius) + { + findwaypointswithin(camera1->o, 0, showwaypointsradius, close); + len = close.length(); + } + loopi(len) + { + waypoint &w = waypoints[showwaypointsradius ? close[i] : i]; + loopj(MAXWAYPOINTLINKS) + { + int link = w.links[j]; + if(!link) break; + particle_flare(w.o, waypoints[link].o, 1, PART_STREAK, 0x0000FF); + } + } + + } + } } diff --git a/src/fpsgame/ai.h b/src/fpsgame/ai.h index 43efde9..9ea7e41 100644 --- a/src/fpsgame/ai.h +++ b/src/fpsgame/ai.h @@ -7,84 +7,84 @@ enum { AI_NONE = 0, AI_BOT, AI_MAX }; namespace ai { - const int MAXWAYPOINTS = USHRT_MAX - 2; - const int MAXWAYPOINTLINKS = 6; - const int WAYPOINTRADIUS = 16; - - const float MINWPDIST = 4.f; // is on top of - const float CLOSEDIST = 32.f; // is close - const float FARDIST = 128.f; // too far to remap close - const float JUMPMIN = 4.f; // decides to jump - const float JUMPMAX = 32.f; // max jump - const float SIGHTMIN = 64.f; // minimum line of sight - const float SIGHTMAX = 1024.f; // maximum line of sight - const float VIEWMIN = 90.f; // minimum field of view - const float VIEWMAX = 180.f; // maximum field of view - - struct waypoint - { - vec o; - float curscore, estscore; + const int MAXWAYPOINTS = USHRT_MAX - 2; + const int MAXWAYPOINTLINKS = 6; + const int WAYPOINTRADIUS = 16; + + const float MINWPDIST = 4.f; // is on top of + const float CLOSEDIST = 32.f; // is close + const float FARDIST = 128.f; // too far to remap close + const float JUMPMIN = 4.f; // decides to jump + const float JUMPMAX = 32.f; // max jump + const float SIGHTMIN = 64.f; // minimum line of sight + const float SIGHTMAX = 1024.f; // maximum line of sight + const float VIEWMIN = 90.f; // minimum field of view + const float VIEWMAX = 180.f; // maximum field of view + + struct waypoint + { + vec o; + float curscore, estscore; int weight; - ushort route, prev; - ushort links[MAXWAYPOINTLINKS]; + ushort route, prev; + ushort links[MAXWAYPOINTLINKS]; - waypoint() {} - waypoint(const vec &o, int weight = 0) : o(o), weight(weight), route(0) { memset(links, 0, sizeof(links)); } + waypoint() {} + waypoint(const vec &o, int weight = 0) : o(o), weight(weight), route(0) { memset(links, 0, sizeof(links)); } - int score() const { return int(curscore) + int(estscore); } + int score() const { return int(curscore) + int(estscore); } - int find(int wp) + int find(int wp) { loopi(MAXWAYPOINTLINKS) if(links[i] == wp) return i; return -1; } bool haslinks() { return links[0]!=0; } - }; - extern vector<waypoint> waypoints; + }; + extern vector<waypoint> waypoints; - static inline bool iswaypoint(int n) - { - return n > 0 && n < waypoints.length(); - } + static inline bool iswaypoint(int n) + { + return n > 0 && n < waypoints.length(); + } - extern int showwaypoints, dropwaypoints; - extern int closestwaypoint(const vec &pos, float mindist, bool links, fpsent *d = NULL); - extern void findwaypointswithin(const vec &pos, float mindist, float maxdist, vector<int> &results); + extern int showwaypoints, dropwaypoints; + extern int closestwaypoint(const vec &pos, float mindist, bool links, fpsent *d = NULL); + extern void findwaypointswithin(const vec &pos, float mindist, float maxdist, vector<int> &results); extern void inferwaypoints(fpsent *d, const vec &o, const vec &v, float mindist = ai::CLOSEDIST); - struct avoidset - { - struct obstacle - { - void *owner; - int numwaypoints; - float above; - - obstacle(void *owner, float above = -1) : owner(owner), numwaypoints(0), above(above) {} - }; - - vector<obstacle> obstacles; - vector<int> waypoints; - - void clear() - { - obstacles.setsize(0); - waypoints.setsize(0); - } - - void add(void *owner, float above) - { - obstacles.add(obstacle(owner, above)); - } - - void add(void *owner, float above, int wp) - { - if(obstacles.empty() || owner != obstacles.last().owner) add(owner, above); - obstacles.last().numwaypoints++; - waypoints.add(wp); - } + struct avoidset + { + struct obstacle + { + void *owner; + int numwaypoints; + float above; + + obstacle(void *owner, float above = -1) : owner(owner), numwaypoints(0), above(above) {} + }; + + vector<obstacle> obstacles; + vector<int> waypoints; + + void clear() + { + obstacles.setsize(0); + waypoints.setsize(0); + } + + void add(void *owner, float above) + { + obstacles.add(obstacle(owner, above)); + } + + void add(void *owner, float above, int wp) + { + if(obstacles.empty() || owner != obstacles.last().owner) add(owner, above); + obstacles.last().numwaypoints++; + waypoints.add(wp); + } void add(avoidset &avoid) { @@ -97,175 +97,175 @@ namespace ai } } - void avoidnear(void *owner, float above, const vec &pos, float limit); - - #define loopavoid(v, d, body) \ - if(!(v).obstacles.empty()) \ - { \ - int cur = 0; \ - loopv((v).obstacles) \ - { \ - const ai::avoidset::obstacle &ob = (v).obstacles[i]; \ - int next = cur + ob.numwaypoints; \ - if(ob.owner != d) \ - { \ - for(; cur < next; cur++) \ - { \ - int wp = (v).waypoints[cur]; \ - body; \ - } \ - } \ - cur = next; \ - } \ - } - - bool find(int n, fpsent *d) const - { - loopavoid(*this, d, { if(wp == n) return true; }); - return false; - } - - int remap(fpsent *d, int n, vec &pos, bool retry = false); - }; - - extern bool route(fpsent *d, int node, int goal, vector<int> &route, const avoidset &obstacles, int retries = 0); - extern void navigate(); - extern void clearwaypoints(bool full = false); - extern void seedwaypoints(); - extern void loadwaypoints(bool force = false, const char *mname = NULL); - extern void savewaypoints(bool force = false, const char *mname = NULL); - - // ai state information for the owner client - enum - { - AI_S_WAIT = 0, // waiting for next command - AI_S_DEFEND, // defend goal target - AI_S_PURSUE, // pursue goal target - AI_S_INTEREST, // interest in goal entity - AI_S_MAX - }; - - enum - { - AI_T_NODE, - AI_T_PLAYER, - AI_T_AFFINITY, - AI_T_ENTITY, - AI_T_MAX - }; - - struct interest - { - int state, node, target, targtype; - float score; - interest() : state(-1), node(-1), target(-1), targtype(-1), score(0.f) {} - ~interest() {} - }; - - struct aistate - { - int type, millis, targtype, target, idle; - bool override; - - aistate(int m, int t, int r = -1, int v = -1) : type(t), millis(m), targtype(r), target(v) - { - reset(); - } - ~aistate() {} - - void reset() - { - idle = 0; - override = false; - } - }; - - const int NUMPREVNODES = 6; - - struct aiinfo - { - vector<aistate> state; - vector<int> route; - vec target, spot; - int enemy, enemyseen, enemymillis, weappref, prevnodes[NUMPREVNODES], targnode, targlast, targtime, targseq, - lastrun, lasthunt, lastaction, lastcheck, jumpseed, jumprand, blocktime, huntseq, blockseq, lastaimrnd; - float targyaw, targpitch, views[3], aimrnd[3]; - bool dontmove, becareful, tryreset, trywipe; - - aiinfo() - { - clearsetup(); - reset(); - loopk(3) views[k] = 0.f; - } - ~aiinfo() {} + void avoidnear(void *owner, float above, const vec &pos, float limit); + + #define loopavoid(v, d, body) \ + if(!(v).obstacles.empty()) \ + { \ + int cur = 0; \ + loopv((v).obstacles) \ + { \ + const ai::avoidset::obstacle &ob = (v).obstacles[i]; \ + int next = cur + ob.numwaypoints; \ + if(ob.owner != d) \ + { \ + for(; cur < next; cur++) \ + { \ + int wp = (v).waypoints[cur]; \ + body; \ + } \ + } \ + cur = next; \ + } \ + } + + bool find(int n, fpsent *d) const + { + loopavoid(*this, d, { if(wp == n) return true; }); + return false; + } + + int remap(fpsent *d, int n, vec &pos, bool retry = false); + }; + + extern bool route(fpsent *d, int node, int goal, vector<int> &route, const avoidset &obstacles, int retries = 0); + extern void navigate(); + extern void clearwaypoints(bool full = false); + extern void seedwaypoints(); + extern void loadwaypoints(bool force = false, const char *mname = NULL); + extern void savewaypoints(bool force = false, const char *mname = NULL); + + // ai state information for the owner client + enum + { + AI_S_WAIT = 0, // waiting for next command + AI_S_DEFEND, // defend goal target + AI_S_PURSUE, // pursue goal target + AI_S_INTEREST, // interest in goal entity + AI_S_MAX + }; + + enum + { + AI_T_NODE, + AI_T_PLAYER, + AI_T_AFFINITY, + AI_T_ENTITY, + AI_T_MAX + }; + + struct interest + { + int state, node, target, targtype; + float score; + interest() : state(-1), node(-1), target(-1), targtype(-1), score(0.f) {} + ~interest() {} + }; + + struct aistate + { + int type, millis, targtype, target, idle; + bool override; + + aistate(int m, int t, int r = -1, int v = -1) : type(t), millis(m), targtype(r), target(v) + { + reset(); + } + ~aistate() {} + + void reset() + { + idle = 0; + override = false; + } + }; + + const int NUMPREVNODES = 6; + + struct aiinfo + { + vector<aistate> state; + vector<int> route; + vec target, spot; + int enemy, enemyseen, enemymillis, weappref, prevnodes[NUMPREVNODES], targnode, targlast, targtime, targseq, + lastrun, lasthunt, lastaction, lastcheck, jumpseed, jumprand, blocktime, huntseq, blockseq, lastaimrnd; + float targyaw, targpitch, views[3], aimrnd[3]; + bool dontmove, becareful, tryreset, trywipe; + + aiinfo() + { + clearsetup(); + reset(); + loopk(3) views[k] = 0.f; + } + ~aiinfo() {} void clearsetup() { - weappref = GUN_PISTOL; - spot = target = vec(0, 0, 0); - lastaction = lasthunt = lastcheck = enemyseen = enemymillis = blocktime = huntseq = blockseq = targtime = targseq = lastaimrnd = 0; - lastrun = jumpseed = lastmillis; - jumprand = lastmillis+5000; - targnode = targlast = enemy = -1; + weappref = GUN_PISTOL; + spot = target = vec(0, 0, 0); + lastaction = lasthunt = lastcheck = enemyseen = enemymillis = blocktime = huntseq = blockseq = targtime = targseq = lastaimrnd = 0; + lastrun = jumpseed = lastmillis; + jumprand = lastmillis+5000; + targnode = targlast = enemy = -1; } void clear(bool prev = false) { - if(prev) memset(prevnodes, -1, sizeof(prevnodes)); - route.setsize(0); + if(prev) memset(prevnodes, -1, sizeof(prevnodes)); + route.setsize(0); } - void wipe(bool prev = false) - { - clear(prev); - state.setsize(0); - addstate(AI_S_WAIT); - trywipe = false; - } - - void clean(bool tryit = false) - { - if(!tryit) becareful = dontmove = false; - targyaw = rnd(360); - targpitch = 0.f; - tryreset = tryit; - } - - void reset(bool tryit = false) { wipe(); clean(tryit); } - - bool hasprevnode(int n) const - { - loopi(NUMPREVNODES) if(prevnodes[i] == n) return true; - return false; - } - - void addprevnode(int n) - { - if(prevnodes[0] != n) - { - memmove(&prevnodes[1], prevnodes, sizeof(prevnodes) - sizeof(prevnodes[0])); - prevnodes[0] = n; - } - } - - aistate &addstate(int t, int r = -1, int v = -1) - { - return state.add(aistate(lastmillis, t, r, v)); - } - - void removestate(int index = -1) - { - if(index < 0) state.pop(); - else if(state.inrange(index)) state.remove(index); - if(!state.length()) addstate(AI_S_WAIT); - } - - aistate &getstate(int idx = -1) - { - if(state.inrange(idx)) return state[idx]; - return state.last(); - } + void wipe(bool prev = false) + { + clear(prev); + state.setsize(0); + addstate(AI_S_WAIT); + trywipe = false; + } + + void clean(bool tryit = false) + { + if(!tryit) becareful = dontmove = false; + targyaw = rnd(360); + targpitch = 0.f; + tryreset = tryit; + } + + void reset(bool tryit = false) { wipe(); clean(tryit); } + + bool hasprevnode(int n) const + { + loopi(NUMPREVNODES) if(prevnodes[i] == n) return true; + return false; + } + + void addprevnode(int n) + { + if(prevnodes[0] != n) + { + memmove(&prevnodes[1], prevnodes, sizeof(prevnodes) - sizeof(prevnodes[0])); + prevnodes[0] = n; + } + } + + aistate &addstate(int t, int r = -1, int v = -1) + { + return state.add(aistate(lastmillis, t, r, v)); + } + + void removestate(int index = -1) + { + if(index < 0) state.pop(); + else if(state.inrange(index)) state.remove(index); + if(!state.length()) addstate(AI_S_WAIT); + } + + aistate &getstate(int idx = -1) + { + if(state.inrange(idx)) return state[idx]; + return state.last(); + } aistate &switchstate(aistate &b, int t, int r = -1, int v = -1) { @@ -278,40 +278,40 @@ namespace ai } return addstate(t, r, v); } - }; + }; extern avoidset obstacles; - extern vec aitarget; - - extern float viewdist(int x = 101); - extern float viewfieldx(int x = 101); - extern float viewfieldy(int x = 101); - extern bool targetable(fpsent *d, fpsent *e); - extern bool cansee(fpsent *d, vec &x, vec &y, vec &targ = aitarget); - - extern void init(fpsent *d, int at, int on, int sk, int bn, int pm, const char *name, const char *team); - extern void update(); - extern void avoid(); - extern void think(fpsent *d, bool run); - - extern bool badhealth(fpsent *d); - extern bool checkothers(vector<int> &targets, fpsent *d = NULL, int state = -1, int targtype = -1, int target = -1, bool teams = false, int *members = NULL); - extern bool makeroute(fpsent *d, aistate &b, int node, bool changed = true, int retries = 0); - extern bool makeroute(fpsent *d, aistate &b, const vec &pos, bool changed = true, int retries = 0); - extern bool randomnode(fpsent *d, aistate &b, const vec &pos, float guard = SIGHTMIN, float wander = SIGHTMAX); - extern bool randomnode(fpsent *d, aistate &b, float guard = SIGHTMIN, float wander = SIGHTMAX); - extern bool violence(fpsent *d, aistate &b, fpsent *e, int pursue = 0); - extern bool patrol(fpsent *d, aistate &b, const vec &pos, float guard = SIGHTMIN, float wander = SIGHTMAX, int walk = 1, bool retry = false); - extern bool defend(fpsent *d, aistate &b, const vec &pos, float guard = SIGHTMIN, float wander = SIGHTMAX, int walk = 1); - extern void assist(fpsent *d, aistate &b, vector<interest> &interests, bool all = false, bool force = false); - extern bool parseinterests(fpsent *d, aistate &b, vector<interest> &interests, bool override = false, bool ignore = false); + extern vec aitarget; + + extern float viewdist(int x = 101); + extern float viewfieldx(int x = 101); + extern float viewfieldy(int x = 101); + extern bool targetable(fpsent *d, fpsent *e); + extern bool cansee(fpsent *d, vec &x, vec &y, vec &targ = aitarget); + + extern void init(fpsent *d, int at, int on, int sk, int bn, int pm, const char *name, const char *team); + extern void update(); + extern void avoid(); + extern void think(fpsent *d, bool run); + + extern bool badhealth(fpsent *d); + extern bool checkothers(vector<int> &targets, fpsent *d = NULL, int state = -1, int targtype = -1, int target = -1, bool teams = false, int *members = NULL); + extern bool makeroute(fpsent *d, aistate &b, int node, bool changed = true, int retries = 0); + extern bool makeroute(fpsent *d, aistate &b, const vec &pos, bool changed = true, int retries = 0); + extern bool randomnode(fpsent *d, aistate &b, const vec &pos, float guard = SIGHTMIN, float wander = SIGHTMAX); + extern bool randomnode(fpsent *d, aistate &b, float guard = SIGHTMIN, float wander = SIGHTMAX); + extern bool violence(fpsent *d, aistate &b, fpsent *e, int pursue = 0); + extern bool patrol(fpsent *d, aistate &b, const vec &pos, float guard = SIGHTMIN, float wander = SIGHTMAX, int walk = 1, bool retry = false); + extern bool defend(fpsent *d, aistate &b, const vec &pos, float guard = SIGHTMIN, float wander = SIGHTMAX, int walk = 1); + extern void assist(fpsent *d, aistate &b, vector<interest> &interests, bool all = false, bool force = false); + extern bool parseinterests(fpsent *d, aistate &b, vector<interest> &interests, bool override = false, bool ignore = false); extern void spawned(fpsent *d); extern void damaged(fpsent *d, fpsent *e); extern void killed(fpsent *d, fpsent *e); extern void itemspawned(int ent); - extern void render(); + extern void render(); } diff --git a/src/fpsgame/aiman.h b/src/fpsgame/aiman.h index 9c1598c..940f13e 100644 --- a/src/fpsgame/aiman.h +++ b/src/fpsgame/aiman.h @@ -1,146 +1,146 @@ // server-side ai manager namespace aiman { - bool dorefresh = false, botbalance = true; - VARN(serverbotlimit, botlimit, 0, 8, MAXBOTS); - VAR(serverbotbalance, 0, 1, 1); + bool dorefresh = false, botbalance = true; + VARN(serverbotlimit, botlimit, 0, 8, MAXBOTS); + VAR(serverbotbalance, 0, 1, 1); - void calcteams(vector<teamscore> &teams) - { - static const char * const defaults[2] = { "good", "evil" }; - loopv(clients) - { - clientinfo *ci = clients[i]; - if(ci->state.state==CS_SPECTATOR || !ci->team[0]) continue; - teamscore *t = NULL; - loopvj(teams) if(!strcmp(teams[j].team, ci->team)) { t = &teams[j]; break; } - if(t) t->score++; - else teams.add(teamscore(ci->team, 1)); - } - teams.sort(teamscore::compare); - if(teams.length() < int(sizeof(defaults)/sizeof(defaults[0]))) - { - loopi(sizeof(defaults)/sizeof(defaults[0])) if(teams.htfind(defaults[i]) < 0) teams.add(teamscore(defaults[i], 0)); - } - } + void calcteams(vector<teamscore> &teams) + { + static const char * const defaults[2] = { "good", "evil" }; + loopv(clients) + { + clientinfo *ci = clients[i]; + if(ci->state.state==CS_SPECTATOR || !ci->team[0]) continue; + teamscore *t = NULL; + loopvj(teams) if(!strcmp(teams[j].team, ci->team)) { t = &teams[j]; break; } + if(t) t->score++; + else teams.add(teamscore(ci->team, 1)); + } + teams.sort(teamscore::compare); + if(teams.length() < int(sizeof(defaults)/sizeof(defaults[0]))) + { + loopi(sizeof(defaults)/sizeof(defaults[0])) if(teams.htfind(defaults[i]) < 0) teams.add(teamscore(defaults[i], 0)); + } + } - void balanceteams() - { - vector<teamscore> teams; - calcteams(teams); - vector<clientinfo *> reassign; - loopv(bots) if(bots[i]) reassign.add(bots[i]); - while(reassign.length() && teams.length() && teams[0].score > teams.last().score + 1) - { - teamscore &t = teams.last(); - clientinfo *bot = NULL; - loopv(reassign) if(reassign[i] && !strcmp(reassign[i]->team, teams[0].team)) - { - bot = reassign.removeunordered(i); - teams[0].score--; - t.score++; - for(int j = teams.length() - 2; j >= 0; j--) - { - if(teams[j].score >= teams[j+1].score) break; - swap(teams[j], teams[j+1]); - } - break; - } - if(bot) - { - copystring(bot->team, t.team, MAXTEAMLEN+1); - sendf(-1, 1, "riisi", N_SETTEAM, bot->clientnum, bot->team, 0); - } - else teams.remove(0, 1); - } - } + void balanceteams() + { + vector<teamscore> teams; + calcteams(teams); + vector<clientinfo *> reassign; + loopv(bots) if(bots[i]) reassign.add(bots[i]); + while(reassign.length() && teams.length() && teams[0].score > teams.last().score + 1) + { + teamscore &t = teams.last(); + clientinfo *bot = NULL; + loopv(reassign) if(reassign[i] && !strcmp(reassign[i]->team, teams[0].team)) + { + bot = reassign.removeunordered(i); + teams[0].score--; + t.score++; + for(int j = teams.length() - 2; j >= 0; j--) + { + if(teams[j].score >= teams[j+1].score) break; + swap(teams[j], teams[j+1]); + } + break; + } + if(bot) + { + copystring(bot->team, t.team, MAXTEAMLEN+1); + sendf(-1, 1, "riisi", N_SETTEAM, bot->clientnum, bot->team, 0); + } + else teams.remove(0, 1); + } + } - const char *chooseteam() - { - vector<teamscore> teams; - calcteams(teams); - return teams.length() ? teams.last().team : ""; - } + const char *chooseteam() + { + vector<teamscore> teams; + calcteams(teams); + return teams.length() ? teams.last().team : ""; + } - static inline bool validaiclient(clientinfo *ci) - { - return ci->clientnum >= 0 && ci->state.aitype == AI_NONE && (ci->state.state!=CS_SPECTATOR || ci->local || (ci->privilege && !ci->warned)); - } + static inline bool validaiclient(clientinfo *ci) + { + return ci->clientnum >= 0 && ci->state.aitype == AI_NONE && (ci->state.state!=CS_SPECTATOR || ci->local || (ci->privilege && !ci->warned)); + } clientinfo *findaiclient(clientinfo *exclude = NULL) { - clientinfo *least = NULL; + clientinfo *least = NULL; loopv(clients) { clientinfo *ci = clients[i]; if(!validaiclient(ci) || ci==exclude) continue; - if(!least || ci->bots.length() < least->bots.length()) least = ci; + if(!least || ci->bots.length() < least->bots.length()) least = ci; } - return least; + return least; } bool addai(int skill, int limit) { int numai = 0, cn = -1, maxai = limit >= 0 ? min(limit, MAXBOTS) : MAXBOTS; loopv(bots) - { - clientinfo *ci = bots[i]; - if(!ci || ci->ownernum < 0) { if(cn < 0) cn = i; continue; } + { + clientinfo *ci = bots[i]; + if(!ci || ci->ownernum < 0) { if(cn < 0) cn = i; continue; } numai++; } if(numai >= maxai) return false; - if(bots.inrange(cn)) - { - clientinfo *ci = bots[cn]; - if(ci) - { // reuse a slot that was going to removed + if(bots.inrange(cn)) + { + clientinfo *ci = bots[cn]; + if(ci) + { // reuse a slot that was going to removed - clientinfo *owner = findaiclient(); - ci->ownernum = owner ? owner->clientnum : -1; - if(owner) owner->bots.add(ci); - ci->aireinit = 2; - dorefresh = true; - return true; - } - } - else { cn = bots.length(); bots.add(NULL); } - const char *team = m_teammode ? chooseteam() : ""; - if(!bots[cn]) bots[cn] = new clientinfo; - clientinfo *ci = bots[cn]; + clientinfo *owner = findaiclient(); + ci->ownernum = owner ? owner->clientnum : -1; + if(owner) owner->bots.add(ci); + ci->aireinit = 2; + dorefresh = true; + return true; + } + } + else { cn = bots.length(); bots.add(NULL); } + const char *team = m_teammode ? chooseteam() : ""; + if(!bots[cn]) bots[cn] = new clientinfo; + clientinfo *ci = bots[cn]; ci->clientnum = MAXCLIENTS + cn; ci->state.aitype = AI_BOT; - clientinfo *owner = findaiclient(); + clientinfo *owner = findaiclient(); ci->ownernum = owner ? owner->clientnum : -1; - if(owner) owner->bots.add(ci); - ci->state.skill = skill <= 0 ? rnd(50) + 51 : clamp(skill, 1, 101); - clients.add(ci); + if(owner) owner->bots.add(ci); + ci->state.skill = skill <= 0 ? rnd(50) + 51 : clamp(skill, 1, 101); + clients.add(ci); ci->state.lasttimeplayed = lastmillis; copystring(ci->name, "bot", MAXNAMELEN+1); ci->state.state = CS_DEAD; - copystring(ci->team, team, MAXTEAMLEN+1); - ci->playermodel = 0; + copystring(ci->team, team, MAXTEAMLEN+1); + ci->playermodel = 0; ci->aireinit = 2; ci->connected = true; - dorefresh = true; + dorefresh = true; return true; } void deleteai(clientinfo *ci) { - int cn = ci->clientnum - MAXCLIENTS; - if(!bots.inrange(cn)) return; - sendf(-1, 1, "ri2", N_CDIS, ci->clientnum); - clientinfo *owner = (clientinfo *)getclientinfo(ci->ownernum); - if(owner) owner->bots.removeobj(ci); - clients.removeobj(ci); - DELETEP(bots[cn]); + int cn = ci->clientnum - MAXCLIENTS; + if(!bots.inrange(cn)) return; + sendf(-1, 1, "ri2", N_CDIS, ci->clientnum); + clientinfo *owner = (clientinfo *)getclientinfo(ci->ownernum); + if(owner) owner->bots.removeobj(ci); + clients.removeobj(ci); + DELETEP(bots[cn]); dorefresh = true; } bool deleteai() { - loopvrev(bots) if(bots[i] && bots[i]->ownernum >= 0) - { + loopvrev(bots) if(bots[i] && bots[i]->ownernum >= 0) + { deleteai(bots[i]); return true; } @@ -154,22 +154,22 @@ namespace aiman { sendf(-1, 1, "ri6ss", N_INITAI, ci->clientnum, ci->ownernum, ci->state.aitype, ci->state.skill, ci->playermodel, ci->name, ci->team); if(ci->aireinit == 2) - { - ci->reassign(); - if(ci->state.state==CS_ALIVE) sendspawn(ci); - else sendresume(ci); - } + { + ci->reassign(); + if(ci->state.state==CS_ALIVE) sendspawn(ci); + else sendresume(ci); + } ci->aireinit = 0; } } void shiftai(clientinfo *ci, clientinfo *owner = NULL) { - clientinfo *prevowner = (clientinfo *)getclientinfo(ci->ownernum); - if(prevowner) prevowner->bots.removeobj(ci); + clientinfo *prevowner = (clientinfo *)getclientinfo(ci->ownernum); + if(prevowner) prevowner->bots.removeobj(ci); if(!owner) { ci->aireinit = 0; ci->ownernum = -1; } else if(ci->ownernum != owner->clientnum) { ci->aireinit = 2; ci->ownernum = owner->clientnum; owner->bots.add(ci); } - dorefresh = true; + dorefresh = true; } void removeai(clientinfo *ci) @@ -180,13 +180,13 @@ namespace aiman bool reassignai() { - clientinfo *hi = NULL, *lo = NULL; + clientinfo *hi = NULL, *lo = NULL; loopv(clients) { clientinfo *ci = clients[i]; if(!validaiclient(ci)) continue; - if(!lo || ci->bots.length() < lo->bots.length()) lo = ci; - if(!hi || ci->bots.length() > hi->bots.length()) hi = ci; + if(!lo || ci->bots.length() < lo->bots.length()) lo = ci; + if(!hi || ci->bots.length() > hi->bots.length()) hi = ci; } if(hi && lo && hi->bots.length() - lo->bots.length() > 1) { @@ -202,20 +202,20 @@ namespace aiman void checksetup() { - if(m_teammode && botbalance) balanceteams(); + if(m_teammode && botbalance) balanceteams(); loopvrev(bots) if(bots[i]) reinitai(bots[i]); } void clearai() { // clear and remove all ai immediately - loopvrev(bots) if(bots[i]) deleteai(bots[i]); + loopvrev(bots) if(bots[i]) deleteai(bots[i]); } void checkai() { - if(!dorefresh) return; - dorefresh = false; - if(m_botmode && numclients(-1, false, true)) + if(!dorefresh) return; + dorefresh = false; + if(m_botmode && numclients(-1, false, true)) { checksetup(); while(reassignai()); @@ -225,49 +225,49 @@ namespace aiman void reqadd(clientinfo *ci, int skill) { - if(!ci->local && !ci->privilege) return; - if(!addai(skill, !ci->local && ci->privilege < PRIV_ADMIN ? botlimit : -1)) sendf(ci->clientnum, 1, "ris", N_SERVMSG, "failed to create or assign bot"); + if(!ci->local && !ci->privilege) return; + if(!addai(skill, !ci->local && ci->privilege < PRIV_ADMIN ? botlimit : -1)) sendf(ci->clientnum, 1, "ris", N_SERVMSG, "failed to create or assign bot"); } void reqdel(clientinfo *ci) { - if(!ci->local && !ci->privilege) return; - if(!deleteai()) sendf(ci->clientnum, 1, "ris", N_SERVMSG, "failed to remove any bots"); + if(!ci->local && !ci->privilege) return; + if(!deleteai()) sendf(ci->clientnum, 1, "ris", N_SERVMSG, "failed to remove any bots"); } - void setbotlimit(clientinfo *ci, int limit) - { - if(ci && !ci->local && ci->privilege < PRIV_ADMIN) return; - botlimit = clamp(limit, 0, MAXBOTS); - dorefresh = true; - defformatstring(msg, "bot limit is now %d", botlimit); - sendservmsg(msg); - } + void setbotlimit(clientinfo *ci, int limit) + { + if(ci && !ci->local && ci->privilege < PRIV_ADMIN) return; + botlimit = clamp(limit, 0, MAXBOTS); + dorefresh = true; + defformatstring(msg, "bot limit is now %d", botlimit); + sendservmsg(msg); + } - void setbotbalance(clientinfo *ci, bool balance) - { - if(ci && !ci->local && !ci->privilege) return; - botbalance = balance ? 1 : 0; - dorefresh = true; - defformatstring(msg, "bot team balancing is now %s", botbalance ? "enabled" : "disabled"); - sendservmsg(msg); - } + void setbotbalance(clientinfo *ci, bool balance) + { + if(ci && !ci->local && !ci->privilege) return; + botbalance = balance ? 1 : 0; + dorefresh = true; + defformatstring(msg, "bot team balancing is now %s", botbalance ? "enabled" : "disabled"); + sendservmsg(msg); + } - void changemap() - { - dorefresh = true; - loopv(clients) if(clients[i]->local || clients[i]->privilege) return; - if(botbalance != (serverbotbalance != 0)) setbotbalance(NULL, serverbotbalance != 0); - } + void changemap() + { + dorefresh = true; + loopv(clients) if(clients[i]->local || clients[i]->privilege) return; + if(botbalance != (serverbotbalance != 0)) setbotbalance(NULL, serverbotbalance != 0); + } - void addclient(clientinfo *ci) - { - if(ci->state.aitype == AI_NONE) dorefresh = true; - } + void addclient(clientinfo *ci) + { + if(ci->state.aitype == AI_NONE) dorefresh = true; + } - void changeteam(clientinfo *ci) - { - if(ci->state.aitype == AI_NONE) dorefresh = true; - } + void changeteam(clientinfo *ci) + { + if(ci->state.aitype == AI_NONE) dorefresh = true; + } } diff --git a/src/fpsgame/client.cpp b/src/fpsgame/client.cpp index 00a0c7e..7155c00 100644 --- a/src/fpsgame/client.cpp +++ b/src/fpsgame/client.cpp @@ -2,2010 +2,2010 @@ namespace game { - bool senditemstoserver = false, sendcrc = false; // after a map change, since server doesn't have map data - int lastping = 0; - - bool connected = false, remote = false, demoplayback = false, gamepaused = false; - int sessionid = 0, mastermode = MM_OPEN, gamespeed = 100; - string servinfo = "", servauth = "", connectpass = ""; - - VARP(deadpush, 1, 2, 20); - - void switchname(const char *name) - { - filtertext(player1->name, name, false, false, MAXNAMELEN); - if(!player1->name[0]) copystring(player1->name, "Anonymous"); - addmsg(N_SWITCHNAME, "rs", player1->name); - } - void printname() - { - conoutf("your name is: %s", colorname(player1)); - } - ICOMMAND(name, "sN", (char *s, int *numargs), - { - if(*numargs > 0) switchname(s); - else if(!*numargs) printname(); - else result(colorname(player1)); - }); - ICOMMAND(getname, "", (), result(player1->name)); - - void switchteam(const char *team) - { - if(player1->clientnum < 0) filtertext(player1->team, team, false, false, MAXTEAMLEN); - else addmsg(N_SWITCHTEAM, "rs", team); - } - void printteam() - { - conoutf("your team is: %s", player1->team); - } - ICOMMAND(team, "sN", (char *s, int *numargs), - { - if(*numargs > 0) switchteam(s); - else if(!*numargs) printteam(); - else result(player1->team); - }); - ICOMMAND(getteam, "", (), result(player1->team)); - - struct authkey - { - char *name, *key, *desc; - int lastauth; - - authkey(const char *name, const char *key, const char *desc) - : name(newstring(name)), key(newstring(key)), desc(newstring(desc)), - lastauth(0) - { - } - - ~authkey() - { - DELETEA(name); - DELETEA(key); - DELETEA(desc); - } - }; - vector<authkey *> authkeys; - - authkey *findauthkey(const char *desc = "") - { - loopv(authkeys) if(!strcmp(authkeys[i]->desc, desc) && !strcasecmp(authkeys[i]->name, player1->name)) return authkeys[i]; - loopv(authkeys) if(!strcmp(authkeys[i]->desc, desc)) return authkeys[i]; - return NULL; - } - - VARP(autoauth, 0, 1, 1); - - void addauthkey(const char *name, const char *key, const char *desc) - { - loopvrev(authkeys) if(!strcmp(authkeys[i]->desc, desc) && !strcmp(authkeys[i]->name, name)) delete authkeys.remove(i); - if(name[0] && key[0]) authkeys.add(new authkey(name, key, desc)); - } - ICOMMAND(authkey, "sss", (char *name, char *key, char *desc), addauthkey(name, key, desc)); - - bool hasauthkey(const char *name, const char *desc) - { - if(!name[0] && !desc[0]) return authkeys.length() > 0; - loopvrev(authkeys) if(!strcmp(authkeys[i]->desc, desc) && !strcmp(authkeys[i]->name, name)) return true; - return false; - } - - ICOMMAND(hasauthkey, "ss", (char *name, char *desc), intret(hasauthkey(name, desc) ? 1 : 0)); - - void genauthkey(const char *secret) - { - if(!secret[0]) { conoutf(CON_ERROR, "you must specify a secret password"); return; } - vector<char> privkey, pubkey; - genprivkey(secret, privkey, pubkey); - conoutf("private key: %s", privkey.getbuf()); - conoutf("public key: %s", pubkey.getbuf()); - result(privkey.getbuf()); - } - COMMAND(genauthkey, "s"); - - void getpubkey(const char *desc) - { - authkey *k = findauthkey(desc); - if(!k) { if(desc[0]) conoutf(CON_ERROR, "no authkey found: %s", desc); else conoutf(CON_ERROR, "no global authkey found"); return; } - vector<char> pubkey; - if(!calcpubkey(k->key, pubkey)) { conoutf(CON_ERROR, "failed calculating pubkey"); return; } - result(pubkey.getbuf()); - } - COMMAND(getpubkey, "s"); - - void saveauthkeys() - { - stream *f = openfile("auth.cfg", "w"); - if(!f) { conoutf(CON_ERROR, "failed to open auth.cfg for writing"); return; } - loopv(authkeys) - { - authkey *a = authkeys[i]; - f->printf("authkey %s %s %s\n", escapestring(a->name), escapestring(a->key), escapestring(a->desc)); - } - conoutf("saved authkeys to auth.cfg"); - delete f; - } - COMMAND(saveauthkeys, ""); - - void sendmapinfo() - { - if(!connected) return; - sendcrc = true; - if(player1->state!=CS_SPECTATOR || player1->privilege || !remote) senditemstoserver = true; - } - - void writeclientinfo(stream *f) - { - f->printf("name %s\n", escapestring(player1->name)); - } - - bool allowedittoggle() - { - if(editmode) return true; - if(isconnected() && multiplayer(false) && !m_edit) - { - conoutf(CON_ERROR, "editing in multiplayer requires coop edit mode (1)"); - return false; - } - return execidentbool("allowedittoggle", true); - } - - void edittoggled(bool on) - { - addmsg(N_EDITMODE, "ri", on ? 1 : 0); - if(player1->state==CS_DEAD) deathstate(player1, true); - else if(player1->state==CS_EDITING && player1->editstate==CS_DEAD) showscores(false); - disablezoom(); - player1->suicided = player1->respawned = -2; - } - - const char *getclientname(int cn) - { - fpsent *d = getclient(cn); - return d ? d->name : ""; - } - ICOMMAND(getclientname, "i", (int *cn), result(getclientname(*cn))); - - const char *getclientteam(int cn) - { - fpsent *d = getclient(cn); - return d ? d->team : ""; - } - ICOMMAND(getclientteam, "i", (int *cn), result(getclientteam(*cn))); - - const char *getclienticon(int cn) - { - fpsent *d = getclient(cn); - if(!d || d->state==CS_SPECTATOR) return "spectator"; - const playermodelinfo &mdl = getplayermodelinfo(d); - return m_teammode ? (isteam(player1->team, d->team) ? mdl.blueicon : mdl.redicon) : mdl.ffaicon; - } - ICOMMAND(getclienticon, "i", (int *cn), result(getclienticon(*cn))); - - bool ismaster(int cn) - { - fpsent *d = getclient(cn); - return d && d->privilege >= PRIV_MASTER; - } - ICOMMAND(ismaster, "i", (int *cn), intret(ismaster(*cn) ? 1 : 0)); - - bool isauth(int cn) - { - fpsent *d = getclient(cn); - return d && d->privilege >= PRIV_AUTH; - } - ICOMMAND(isauth, "i", (int *cn), intret(isauth(*cn) ? 1 : 0)); - - bool isadmin(int cn) - { - fpsent *d = getclient(cn); - return d && d->privilege >= PRIV_ADMIN; - } - ICOMMAND(isadmin, "i", (int *cn), intret(isadmin(*cn) ? 1 : 0)); - - ICOMMAND(getmastermode, "", (), intret(mastermode)); - ICOMMAND(mastermodename, "i", (int *mm), result(server::mastermodename(*mm, ""))); - - bool isspectator(int cn) - { - fpsent *d = getclient(cn); - return d && d->state==CS_SPECTATOR; - } - ICOMMAND(isspectator, "i", (int *cn), intret(isspectator(*cn) ? 1 : 0)); - - bool isai(int cn, int type) - { - fpsent *d = getclient(cn); - int aitype = type > 0 && type < AI_MAX ? type : AI_BOT; - return d && d->aitype==aitype; - } - ICOMMAND(isai, "ii", (int *cn, int *type), intret(isai(*cn, *type) ? 1 : 0)); - - int parseplayer(const char *arg) - { - char *end; - int n = strtol(arg, &end, 10); - if(*arg && !*end) - { - if(n!=player1->clientnum && !clients.inrange(n)) return -1; - return n; - } - // try case sensitive first - loopv(players) - { - fpsent *o = players[i]; - if(!strcmp(arg, o->name)) return o->clientnum; - } - // nothing found, try case insensitive - loopv(players) - { - fpsent *o = players[i]; - if(!strcasecmp(arg, o->name)) return o->clientnum; - } - return -1; - } - ICOMMAND(getclientnum, "s", (char *name), intret(name[0] ? parseplayer(name) : player1->clientnum)); - - void listclients(bool local, bool bots) - { - vector<char> buf; - string cn; - int numclients = 0; - if(local && connected) - { - formatstring(cn, "%d", player1->clientnum); - buf.put(cn, strlen(cn)); - numclients++; - } - loopv(clients) if(clients[i] && (bots || clients[i]->aitype == AI_NONE)) - { - formatstring(cn, "%d", clients[i]->clientnum); - if(numclients++) buf.add(' '); - buf.put(cn, strlen(cn)); - } - buf.add('\0'); - result(buf.getbuf()); - } - ICOMMAND(listclients, "bb", (int *local, int *bots), listclients(*local>0, *bots!=0)); - - void clearbans() - { - addmsg(N_CLEARBANS, "r"); - } - COMMAND(clearbans, ""); - - void kick(const char *victim, const char *reason) - { - int vn = parseplayer(victim); - if(vn>=0 && vn!=player1->clientnum) addmsg(N_KICK, "ris", vn, reason); - } - COMMAND(kick, "ss"); - - void authkick(const char *desc, const char *victim, const char *reason) - { - authkey *a = findauthkey(desc); - int vn = parseplayer(victim); - if(a && vn>=0 && vn!=player1->clientnum) - { - a->lastauth = lastmillis; - addmsg(N_AUTHKICK, "rssis", a->desc, a->name, vn, reason); - } - } - ICOMMAND(authkick, "ss", (const char *victim, const char *reason), authkick("", victim, reason)); - ICOMMAND(sauthkick, "ss", (const char *victim, const char *reason), if(servauth[0]) authkick(servauth, victim, reason)); - ICOMMAND(dauthkick, "sss", (const char *desc, const char *victim, const char *reason), if(desc[0]) authkick(desc, victim, reason)); - - vector<int> ignores; - - void ignore(int cn) - { - fpsent *d = getclient(cn); - if(!d || d == player1) return; - conoutf("ignoring %s", d->name); - if(ignores.find(cn) < 0) ignores.add(cn); - } - - void unignore(int cn) - { - if(ignores.find(cn) < 0) return; - fpsent *d = getclient(cn); - if(d) conoutf("stopped ignoring %s", d->name); - ignores.removeobj(cn); - } - - bool isignored(int cn) { return ignores.find(cn) >= 0; } - - ICOMMAND(ignore, "s", (char *arg), ignore(parseplayer(arg))); - ICOMMAND(unignore, "s", (char *arg), unignore(parseplayer(arg))); - ICOMMAND(isignored, "s", (char *arg), intret(isignored(parseplayer(arg)) ? 1 : 0)); - - void setteam(const char *arg1, const char *arg2) - { - int i = parseplayer(arg1); - if(i>=0) addmsg(N_SETTEAM, "ris", i, arg2); - } - COMMAND(setteam, "ss"); - - void hashpwd(const char *pwd) - { - if(player1->clientnum<0) return; - string hash; - server::hashpassword(player1->clientnum, sessionid, pwd, hash); - result(hash); - } - COMMAND(hashpwd, "s"); - - void setmaster(const char *arg, const char *who) - { - if(!arg[0]) return; - int val = 1, cn = player1->clientnum; - if(who[0]) - { - cn = parseplayer(who); - if(cn < 0) return; - } - string hash = ""; - if(!arg[1] && isdigit(arg[0])) val = parseint(arg); - else - { - if(cn != player1->clientnum) return; - server::hashpassword(player1->clientnum, sessionid, arg, hash); - } - addmsg(N_SETMASTER, "riis", cn, val, hash); - } - COMMAND(setmaster, "ss"); - ICOMMAND(mastermode, "i", (int *val), addmsg(N_MASTERMODE, "ri", *val)); - - bool tryauth(const char *desc) - { - authkey *a = findauthkey(desc); - if(!a) return false; - a->lastauth = lastmillis; - addmsg(N_AUTHTRY, "rss", a->desc, a->name); - return true; - } - ICOMMAND(auth, "s", (char *desc), tryauth(desc)); - ICOMMAND(sauth, "", (), if(servauth[0]) tryauth(servauth)); - ICOMMAND(dauth, "s", (char *desc), if(desc[0]) tryauth(desc)); - - ICOMMAND(getservauth, "", (), result(servauth)); - - void togglespectator(int val, const char *who) - { - int i = who[0] ? parseplayer(who) : player1->clientnum; - if(i>=0) addmsg(N_SPECTATOR, "rii", i, val); - } - ICOMMAND(spectator, "is", (int *val, char *who), togglespectator(*val, who)); - - ICOMMAND(checkmaps, "", (), addmsg(N_CHECKMAPS, "r")); - - int gamemode = INT_MAX, nextmode = INT_MAX; - string clientmap = ""; - - void changemapserv(const char *name, int mode) // forced map change from the server - { - if(multiplayer(false) && !m_mp(mode)) - { - conoutf(CON_ERROR, "mode %s (%d) not supported in multiplayer", server::modename(gamemode), gamemode); - loopi(NUMGAMEMODES) if(m_mp(STARTGAMEMODE + i)) { mode = STARTGAMEMODE + i; break; } - } - - gamemode = mode; - nextmode = mode; - if(editmode) toggleedit(); - if(m_demo) { entities::resetspawns(); return; } - if((m_edit && !name[0]) || !load_world(name)) - { - emptymap(0, true, name); - senditemstoserver = false; - } - startgame(); - } - - void setmode(int mode) - { - if(multiplayer(false) && !m_mp(mode)) - { - conoutf(CON_ERROR, "mode %s (%d) not supported in multiplayer", server::modename(mode), mode); - intret(0); - return; - } - nextmode = mode; - intret(1); - } - ICOMMAND(mode, "i", (int *val), setmode(*val)); - ICOMMAND(getmode, "", (), intret(gamemode)); - ICOMMAND(timeremaining, "i", (int *formatted), - { - int val = max(maplimit - lastmillis + 999, 0)/1000; - if(*formatted) - { - defformatstring(str, "%d:%02d", val/60, val%60); - result(str); - } - else intret(val); - }); - ICOMMANDS("m_noitems", "i", (int *mode), { int gamemode = *mode; intret(m_noitems); }); - ICOMMANDS("m_noammo", "i", (int *mode), { int gamemode = *mode; intret(m_noammo); }); - ICOMMANDS("m_insta", "i", (int *mode), { int gamemode = *mode; intret(m_insta); }); - ICOMMANDS("m_efficiency", "i", (int *mode), { int gamemode = *mode; intret(m_efficiency); }); - ICOMMANDS("m_teammode", "i", (int *mode), { int gamemode = *mode; intret(m_teammode); }); - ICOMMANDS("m_demo", "i", (int *mode), { int gamemode = *mode; intret(m_demo); }); - ICOMMANDS("m_edit", "i", (int *mode), { int gamemode = *mode; intret(m_edit); }); - ICOMMANDS("m_lobby", "i", (int *mode), { int gamemode = *mode; intret(m_lobby); }); - - void changemap(const char *name, int mode) // request map change, server may ignore - { - if(!remote) - { - server::forcemap(name, mode); - if(!isconnected()) localconnect(); - } - else if(player1->state!=CS_SPECTATOR || player1->privilege) addmsg(N_MAPVOTE, "rsi", name, mode); - } - void changemap(const char *name) - { - changemap(name, m_valid(nextmode) ? nextmode : (remote ? 0 : 1)); - } - ICOMMAND(map, "s", (char *name), changemap(name)); - - void forceintermission() - { - if(!remote && !hasnonlocalclients()) server::startintermission(); - else addmsg(N_FORCEINTERMISSION, "r"); - } - - void forceedit(const char *name) - { - changemap(name, 1); - } - - void newmap(int size) - { - addmsg(N_NEWMAP, "ri", size); - } - - int needclipboard = -1; - - void sendclipboard() - { - uchar *outbuf = NULL; - int inlen = 0, outlen = 0; - if(!packeditinfo(localedit, inlen, outbuf, outlen)) - { - outbuf = NULL; - inlen = outlen = 0; - } - packetbuf p(16 + outlen, ENET_PACKET_FLAG_RELIABLE); - putint(p, N_CLIPBOARD); - putint(p, inlen); - putint(p, outlen); - if(outlen > 0) p.put(outbuf, outlen); - sendclientpacket(p.finalize(), 1); - needclipboard = -1; - } - - void edittrigger(const selinfo &sel, int op, int arg1, int arg2, int arg3, const VSlot *vs) - { - if(m_edit) switch(op) - { - case EDIT_FLIP: - case EDIT_COPY: - case EDIT_PASTE: - case EDIT_DELCUBE: - { - switch(op) - { - case EDIT_COPY: needclipboard = 0; break; - case EDIT_PASTE: - if(needclipboard > 0) - { - c2sinfo(true); - sendclipboard(); - } - break; - } - addmsg(N_EDITF + op, "ri9i4", - sel.o.x, sel.o.y, sel.o.z, sel.s.x, sel.s.y, sel.s.z, sel.grid, sel.orient, - sel.cx, sel.cxs, sel.cy, sel.cys, sel.corner); - break; - } - case EDIT_ROTATE: - { - addmsg(N_EDITF + op, "ri9i5", - sel.o.x, sel.o.y, sel.o.z, sel.s.x, sel.s.y, sel.s.z, sel.grid, sel.orient, - sel.cx, sel.cxs, sel.cy, sel.cys, sel.corner, - arg1); - break; - } - case EDIT_MAT: - case EDIT_FACE: - { - addmsg(N_EDITF + op, "ri9i6", - sel.o.x, sel.o.y, sel.o.z, sel.s.x, sel.s.y, sel.s.z, sel.grid, sel.orient, - sel.cx, sel.cxs, sel.cy, sel.cys, sel.corner, - arg1, arg2); - break; - } - case EDIT_TEX: - { - int tex1 = shouldpacktex(arg1); - if(addmsg(N_EDITF + op, "ri9i6", - sel.o.x, sel.o.y, sel.o.z, sel.s.x, sel.s.y, sel.s.z, sel.grid, sel.orient, - sel.cx, sel.cxs, sel.cy, sel.cys, sel.corner, - tex1 ? tex1 : arg1, arg2)) - { - messages.pad(2); - int offset = messages.length(); - if(tex1) packvslot(messages, arg1); - *(ushort *)&messages[offset-2] = lilswap(ushort(messages.length() - offset)); - } - break; - } - case EDIT_REPLACE: - { - int tex1 = shouldpacktex(arg1), tex2 = shouldpacktex(arg2); - if(addmsg(N_EDITF + op, "ri9i7", - sel.o.x, sel.o.y, sel.o.z, sel.s.x, sel.s.y, sel.s.z, sel.grid, sel.orient, - sel.cx, sel.cxs, sel.cy, sel.cys, sel.corner, - tex1 ? tex1 : arg1, tex2 ? tex2 : arg2, arg3)) - { - messages.pad(2); - int offset = messages.length(); - if(tex1) packvslot(messages, arg1); - if(tex2) packvslot(messages, arg2); - *(ushort *)&messages[offset-2] = lilswap(ushort(messages.length() - offset)); - } - break; - } - case EDIT_REMIP: - { - addmsg(N_EDITF + op, "r"); - break; - } - case EDIT_VSLOT: - { - if(addmsg(N_EDITF + op, "ri9i6", - sel.o.x, sel.o.y, sel.o.z, sel.s.x, sel.s.y, sel.s.z, sel.grid, sel.orient, - sel.cx, sel.cxs, sel.cy, sel.cys, sel.corner, - arg1, arg2)) - { - messages.pad(2); - int offset = messages.length(); - packvslot(messages, vs); - *(ushort *)&messages[offset-2] = lilswap(ushort(messages.length() - offset)); - } - break; - } - case EDIT_UNDO: - case EDIT_REDO: - { - uchar *outbuf = NULL; - int inlen = 0, outlen = 0; - if(packundo(op, inlen, outbuf, outlen)) - { - if(addmsg(N_EDITF + op, "ri2", inlen, outlen)) messages.put(outbuf, outlen); - delete[] outbuf; - } - break; - } - } - } - - void printvar(fpsent *d, ident *id) - { - if(id) switch(id->type) - { - case ID_VAR: - { - int val = *id->storage.i; - string str; - if(val < 0) - formatstring(str, "%d", val); - else if(id->flags&IDF_HEX && id->maxval==0xFFFFFF) - formatstring(str, "0x%.6X (%d, %d, %d)", val, (val>>16)&0xFF, (val>>8)&0xFF, val&0xFF); - else - formatstring(str, id->flags&IDF_HEX ? "0x%X" : "%d", val); - conoutf(CON_INFO, id->index, "%s set map var \"%s\" to %s", colorname(d), id->name, str); - break; - } - case ID_FVAR: - conoutf(CON_INFO, id->index, "%s set map var \"%s\" to %s", colorname(d), id->name, floatstr(*id->storage.f)); - break; - case ID_SVAR: - conoutf(CON_INFO, id->index, "%s set map var \"%s\" to \"%s\"", colorname(d), id->name, *id->storage.s); - break; - } - } - - void vartrigger(ident *id) - { - if(!m_edit) return; - switch(id->type) - { - case ID_VAR: - addmsg(N_EDITVAR, "risi", ID_VAR, id->name, *id->storage.i); - break; - - case ID_FVAR: - addmsg(N_EDITVAR, "risf", ID_FVAR, id->name, *id->storage.f); - break; - - case ID_SVAR: - addmsg(N_EDITVAR, "riss", ID_SVAR, id->name, *id->storage.s); - break; - default: return; - } - printvar(player1, id); - } - - void pausegame(bool val) - { - if(!connected) return; - if(!remote) server::forcepaused(val); - else addmsg(N_PAUSEGAME, "ri", val ? 1 : 0); - } - ICOMMAND(pausegame, "i", (int *val), pausegame(*val > 0)); - ICOMMAND(paused, "iN$", (int *val, int *numargs, ident *id), - { - if(*numargs > 0) pausegame(clampvar(id, *val, 0, 1) > 0); - else if(*numargs < 0) intret(gamepaused ? 1 : 0); - else printvar(id, gamepaused ? 1 : 0); - }); - - bool ispaused() { return gamepaused; } - - bool allowmouselook() { return !gamepaused || !remote || m_edit; } - - void changegamespeed(int val) - { - if(!connected) return; - if(!remote) server::forcegamespeed(val); - else addmsg(N_GAMESPEED, "ri", val); - } - ICOMMAND(gamespeed, "iN$", (int *val, int *numargs, ident *id), - { - if(*numargs > 0) changegamespeed(clampvar(id, *val, 10, 1000)); - else if(*numargs < 0) intret(gamespeed); - else printvar(id, gamespeed); - }); - - int scaletime(int t) { return t*gamespeed; } - - // collect c2s messages conveniently - vector<uchar> messages; - int messagecn = -1, messagereliable = false; - - bool addmsg(int type, const char *fmt, ...) - { - if(!connected) return false; - static uchar buf[MAXTRANS]; - ucharbuf p(buf, sizeof(buf)); - putint(p, type); - int numi = 1, numf = 0, nums = 0, mcn = -1; - bool reliable = false; - if(fmt) - { - va_list args; - va_start(args, fmt); - while(*fmt) switch(*fmt++) - { - case 'r': reliable = true; break; - case 'c': - { - fpsent *d = va_arg(args, fpsent *); - mcn = !d || d == player1 ? -1 : d->clientnum; - break; - } - case 'v': - { - int n = va_arg(args, int); - int *v = va_arg(args, int *); - loopi(n) putint(p, v[i]); - numi += n; - break; - } - - case 'i': - { - int n = isdigit(*fmt) ? *fmt++-'0' : 1; - loopi(n) putint(p, va_arg(args, int)); - numi += n; - break; - } - case 'f': - { - int n = isdigit(*fmt) ? *fmt++-'0' : 1; - loopi(n) putfloat(p, (float)va_arg(args, double)); - numf += n; - break; - } - case 's': sendstring(va_arg(args, const char *), p); nums++; break; - } - va_end(args); - } - int num = nums || numf ? 0 : numi, msgsize = server::msgsizelookup(type); - if(msgsize && num!=msgsize) { fatal("inconsistent msg size for %d (%d != %d)", type, num, msgsize); } - if(reliable) messagereliable = true; - if(mcn != messagecn) - { - static uchar mbuf[16]; - ucharbuf m(mbuf, sizeof(mbuf)); - putint(m, N_FROMAI); - putint(m, mcn); - messages.put(mbuf, m.length()); - messagecn = mcn; - } - messages.put(buf, p.length()); - return true; - } - - void connectattempt(const char *name, const char *password, const ENetAddress &address) - { - copystring(connectpass, password); - } - - void connectfail() - { - memset(connectpass, 0, sizeof(connectpass)); - } - - void gameconnect(bool _remote) - { - remote = _remote; - if(editmode) toggleedit(); - } - - void gamedisconnect(bool cleanup) - { - if(remote) stopfollowing(); - ignores.setsize(0); - connected = remote = false; - player1->clientnum = -1; - sessionid = 0; - mastermode = MM_OPEN; - messages.setsize(0); - messagereliable = false; - messagecn = -1; - player1->respawn(); - player1->lifesequence = 0; - player1->state = CS_ALIVE; - player1->privilege = PRIV_NONE; - sendcrc = senditemstoserver = false; - demoplayback = false; - gamepaused = false; - gamespeed = 100; - clearclients(false); - if(cleanup) - { - nextmode = gamemode = INT_MAX; - clientmap[0] = '\0'; - } - } - - VARP(teamcolorchat, 0, 1, 1); - const char *chatcolorname(fpsent *d) { return teamcolorchat ? teamcolorname(d, NULL) : colorname(d); } - - void toserver(char *text) { conoutf(CON_CHAT, "%s:\f0 %s", chatcolorname(player1), text); addmsg(N_TEXT, "rcs", player1, text); } - COMMANDN(say, toserver, "C"); - - void sayteam(char *text) { conoutf(CON_TEAMCHAT, "\fs\f8[team]\fr %s: \f8%s", chatcolorname(player1), text); addmsg(N_SAYTEAM, "rcs", player1, text); } - COMMAND(sayteam, "C"); - - ICOMMAND(servcmd, "C", (char *cmd), addmsg(N_SERVCMD, "rs", cmd)); - - static void sendposition(fpsent *d, packetbuf &q) - { - putint(q, N_POS); - putuint(q, d->clientnum); - // 3 bits phys state, 1 bit life sequence, 2 bits move, 2 bits strafe - uchar physstate = d->physstate | ((d->lifesequence&1)<<3) | ((d->move&3)<<4) | ((d->strafe&3)<<6); - q.put(physstate); - ivec o = ivec(vec(d->o.x, d->o.y, d->o.z-d->eyeheight).mul(DMF)); - uint vel = min(int(d->vel.magnitude()*DVELF), 0xFFFF), fall = min(int(d->falling.magnitude()*DVELF), 0xFFFF); - // 3 bits position, 1 bit velocity, 3 bits falling, 1 bit material - uint flags = 0; - if(o.x < 0 || o.x > 0xFFFF) flags |= 1<<0; - if(o.y < 0 || o.y > 0xFFFF) flags |= 1<<1; - if(o.z < 0 || o.z > 0xFFFF) flags |= 1<<2; - if(vel > 0xFF) flags |= 1<<3; - if(fall > 0) - { - flags |= 1<<4; - if(fall > 0xFF) flags |= 1<<5; - if(d->falling.x || d->falling.y || d->falling.z > 0) flags |= 1<<6; - } - if((lookupmaterial(d->feetpos())&MATF_CLIP) == MAT_GAMECLIP) flags |= 1<<7; - putuint(q, flags); - loopk(3) - { - q.put(o[k]&0xFF); - q.put((o[k]>>8)&0xFF); - if(o[k] < 0 || o[k] > 0xFFFF) q.put((o[k]>>16)&0xFF); - } - uint dir = (d->yaw < 0 ? 360 + int(d->yaw)%360 : int(d->yaw)%360) + clamp(int(d->pitch+90), 0, 180)*360; - q.put(dir&0xFF); - q.put((dir>>8)&0xFF); - q.put(clamp(int(d->roll+90), 0, 180)); - q.put(vel&0xFF); - if(vel > 0xFF) q.put((vel>>8)&0xFF); - float velyaw, velpitch; - vectoyawpitch(d->vel, velyaw, velpitch); - uint veldir = (velyaw < 0 ? 360 + int(velyaw)%360 : int(velyaw)%360) + clamp(int(velpitch+90), 0, 180)*360; - q.put(veldir&0xFF); - q.put((veldir>>8)&0xFF); - if(fall > 0) - { - q.put(fall&0xFF); - if(fall > 0xFF) q.put((fall>>8)&0xFF); - if(d->falling.x || d->falling.y || d->falling.z > 0) - { - float fallyaw, fallpitch; - vectoyawpitch(d->falling, fallyaw, fallpitch); - uint falldir = (fallyaw < 0 ? 360 + int(fallyaw)%360 : int(fallyaw)%360) + clamp(int(fallpitch+90), 0, 180)*360; - q.put(falldir&0xFF); - q.put((falldir>>8)&0xFF); - } - } - } - - void sendposition(fpsent *d, bool reliable) - { - if(d->state != CS_ALIVE && d->state != CS_EDITING) return; - packetbuf q(100, reliable ? ENET_PACKET_FLAG_RELIABLE : 0); - sendposition(d, q); - sendclientpacket(q.finalize(), 0); - } - - void sendpositions() - { - loopv(players) - { - fpsent *d = players[i]; - if((d == player1 || d->ai) && (d->state == CS_ALIVE || d->state == CS_EDITING)) - { - packetbuf q(100); - sendposition(d, q); - for(int j = i+1; j < players.length(); j++) - { - fpsent *d = players[j]; - if((d == player1 || d->ai) && (d->state == CS_ALIVE || d->state == CS_EDITING)) - sendposition(d, q); - } - sendclientpacket(q.finalize(), 0); - break; - } - } - } - - void sendmessages() - { - packetbuf p(MAXTRANS); - if(sendcrc) - { - p.reliable(); - sendcrc = false; - const char *mname = getclientmap(); - putint(p, N_MAPCRC); - sendstring(mname, p); - putint(p, mname[0] ? getmapcrc() : 0); - } - if(senditemstoserver) - { - if(!m_noitems) p.reliable(); - if(!m_noitems) entities::putitems(p); - senditemstoserver = false; - } - if(messages.length()) - { - p.put(messages.getbuf(), messages.length()); - messages.setsize(0); - if(messagereliable) p.reliable(); - messagereliable = false; - messagecn = -1; - } - if(totalmillis-lastping>250) - { - putint(p, N_PING); - putint(p, totalmillis); - lastping = totalmillis; - } - sendclientpacket(p.finalize(), 1); - } - - void c2sinfo(bool force) // send update to the server - { - static int lastupdate = -1000; - if(totalmillis - lastupdate < 33 && !force) return; // don't update faster than 30fps - lastupdate = totalmillis; - sendpositions(); - sendmessages(); - flushclient(); - } - - void sendintro() - { - packetbuf p(MAXTRANS, ENET_PACKET_FLAG_RELIABLE); - putint(p, N_CONNECT); - sendstring(player1->name, p); - putint(p, player1->playermodel); - string hash = ""; - if(connectpass[0]) - { - server::hashpassword(player1->clientnum, sessionid, connectpass, hash); - memset(connectpass, 0, sizeof(connectpass)); - } - sendstring(hash, p); - authkey *a = servauth[0] && autoauth ? findauthkey(servauth) : NULL; - if(a) - { - a->lastauth = lastmillis; - sendstring(a->desc, p); - sendstring(a->name, p); - } - else - { - sendstring("", p); - sendstring("", p); - } - sendclientpacket(p.finalize(), 1); - } - - void updatepos(fpsent *d) - { - // update the position of other clients in the game in our world - // don't care if he's in the scenery or other players, - // just don't overlap with our client - - const float r = player1->radius+d->radius; - const float dx = player1->o.x-d->o.x; - const float dy = player1->o.y-d->o.y; - const float dz = player1->o.z-d->o.z; - const float rz = player1->aboveeye+d->eyeheight; - const float fx = (float)fabs(dx), fy = (float)fabs(dy), fz = (float)fabs(dz); - if(fx<r && fy<r && fz<rz && player1->state!=CS_SPECTATOR && d->state!=CS_DEAD) - { - if(fx<fy) d->o.y += dy<0 ? r-fy : -(r-fy); // push aside - else d->o.x += dx<0 ? r-fx : -(r-fx); - } - int lagtime = totalmillis-d->lastupdate; - if(lagtime) - { - if(d->state!=CS_SPAWNING && d->lastupdate) d->plag = (d->plag*5+lagtime)/6; - d->lastupdate = totalmillis; - } - } - - void parsepositions(ucharbuf &p) - { - int type; - while(p.remaining()) switch(type = getint(p)) - { - case N_DEMOPACKET: break; - case N_POS: // position of another client - { - int cn = getuint(p), physstate = p.get(), flags = getuint(p); - vec o, vel, falling; - float yaw, pitch, roll; - loopk(3) - { - int n = p.get(); n |= p.get()<<8; if(flags&(1<<k)) { n |= p.get()<<16; if(n&0x800000) n |= ~0U<<24; } - o[k] = n/DMF; - } - int dir = p.get(); dir |= p.get()<<8; - yaw = dir%360; - pitch = clamp(dir/360, 0, 180)-90; - roll = clamp(int(p.get()), 0, 180)-90; - int mag = p.get(); if(flags&(1<<3)) mag |= p.get()<<8; - dir = p.get(); dir |= p.get()<<8; - vecfromyawpitch(dir%360, clamp(dir/360, 0, 180)-90, 1, 0, vel); - vel.mul(mag/DVELF); - if(flags&(1<<4)) - { - mag = p.get(); if(flags&(1<<5)) mag |= p.get()<<8; - if(flags&(1<<6)) - { - dir = p.get(); dir |= p.get()<<8; - vecfromyawpitch(dir%360, clamp(dir/360, 0, 180)-90, 1, 0, falling); - } - else falling = vec(0, 0, -1); - falling.mul(mag/DVELF); - } - else falling = vec(0, 0, 0); - int seqcolor = (physstate>>3)&1; - fpsent *d = getclient(cn); - if(!d || d->lifesequence < 0 || seqcolor!=(d->lifesequence&1) || d->state==CS_DEAD) continue; - float oldyaw = d->yaw, oldpitch = d->pitch, oldroll = d->roll; - d->yaw = yaw; - d->pitch = pitch; - d->roll = roll; - d->move = (physstate>>4)&2 ? -1 : (physstate>>4)&1; - d->strafe = (physstate>>6)&2 ? -1 : (physstate>>6)&1; - vec oldpos(d->o); - d->o = o; - d->o.z += d->eyeheight; - d->vel = vel; - d->falling = falling; - d->physstate = physstate&7; - updatephysstate(d); - updatepos(d); - if(smoothmove && d->smoothmillis>=0 && oldpos.dist(d->o) < smoothdist) - { - d->newpos = d->o; - d->newyaw = d->yaw; - d->newpitch = d->pitch; - d->newroll = d->roll; - d->o = oldpos; - d->yaw = oldyaw; - d->pitch = oldpitch; - d->roll = oldroll; - (d->deltapos = oldpos).sub(d->newpos); - d->deltayaw = oldyaw - d->newyaw; - if(d->deltayaw > 180) d->deltayaw -= 360; - else if(d->deltayaw < -180) d->deltayaw += 360; - d->deltapitch = oldpitch - d->newpitch; - d->deltaroll = oldroll - d->newroll; - d->smoothmillis = lastmillis; - } - else d->smoothmillis = 0; - if(d->state==CS_LAGGED || d->state==CS_SPAWNING) d->state = CS_ALIVE; - break; - } - - case N_TELEPORT: - { - int cn = getint(p), tp = getint(p), td = getint(p); - fpsent *d = getclient(cn); - if(!d || d->lifesequence < 0 || d->state==CS_DEAD) continue; - entities::teleporteffects(d, tp, td, false); - break; - } - - case N_JUMPPAD: - { - int cn = getint(p), jp = getint(p); - fpsent *d = getclient(cn); - if(!d || d->lifesequence < 0 || d->state==CS_DEAD) continue; - entities::jumppadeffects(d, jp, false); - break; - } - - default: - neterr("type"); - return; - } - } - - void parsestate(fpsent *d, ucharbuf &p, bool resume = false) - { - if(!d) { static fpsent dummy; d = &dummy; } - if(resume) - { - if(d==player1) getint(p); - else d->state = getint(p); - d->frags = getint(p); - d->flags = getint(p); - d->deaths = getint(p); - if(d==player1) getint(p); - else d->quadmillis = getint(p); - } - d->lifesequence = getint(p); - d->health = getint(p); - d->maxhealth = getint(p); - d->armour = getint(p); - d->maxarmour = getint(p); - d->armourtype = getint(p); - if(resume && d==player1) - { - getint(p); - loopi(GUN_PISTOL-GUN_SG+1) getint(p); - } - else - { - int gun = getint(p); - d->gunselect = clamp(gun, int(GUN_FIST), int(GUN_PISTOL)); - loopi(GUN_PISTOL-GUN_SG+1) d->ammo[GUN_SG+i] = getint(p); - } - } - - extern int deathscore; - - void parsemessages(int cn, fpsent *d, ucharbuf &p) - { - static char text[MAXTRANS]; - int type; - bool mapchanged = false, demopacket = false; - - while(p.remaining()) switch(type = getint(p)) - { - case N_DEMOPACKET: demopacket = true; break; - - case N_SERVINFO: // welcome messsage from the server - { - int mycn = getint(p), prot = getint(p); - if(prot!=PROTOCOL_VERSION) - { - conoutf(CON_ERROR, "you are using a different game protocol (you: %d, server: %d)", PROTOCOL_VERSION, prot); - disconnect(); - return; - } - sessionid = getint(p); - player1->clientnum = mycn; // we are now connected - if(getint(p) > 0) conoutf("this server is password protected"); - getstring(servinfo, p, sizeof(servinfo)); - getstring(servauth, p, sizeof(servauth)); - sendintro(); - break; - } - - case N_WELCOME: - { - connected = true; - notifywelcome(); - break; - } - - case N_PAUSEGAME: - { - bool val = getint(p) > 0; - int cn = getint(p); - fpsent *a = cn >= 0 ? getclient(cn) : NULL; - if(!demopacket) - { - gamepaused = val; - player1->attacking = false; - } - if(a) conoutf("%s %s the game", colorname(a), val ? "paused" : "resumed"); - else conoutf("game is %s", val ? "paused" : "resumed"); - break; - } - - case N_GAMESPEED: - { - int val = clamp(getint(p), 10, 1000), cn = getint(p); - fpsent *a = cn >= 0 ? getclient(cn) : NULL; - if(!demopacket) gamespeed = val; - if(a) conoutf("%s set gamespeed to %d", colorname(a), val); - else conoutf("gamespeed is %d", val); - break; - } - - case N_CLIENT: - { - int cn = getint(p), len = getuint(p); - ucharbuf q = p.subbuf(len); - parsemessages(cn, getclient(cn), q); - break; - } - - case N_SOUND: - if(!d) return; - playsound(getint(p), &d->o); - break; - - case N_TEXT: - { - if(!d) return; - getstring(text, p); - filtertext(text, text, true, true); - if(isignored(d->clientnum)) break; - if(d->state!=CS_DEAD && d->state!=CS_SPECTATOR) - particle_textcopy(d->abovehead(), text, PART_TEXT, 2000, 0x32FF64, 4.0f, -8); - conoutf(CON_CHAT, "%s:\f0 %s", chatcolorname(d), text); - break; - } - - case N_SAYTEAM: - { - int tcn = getint(p); - fpsent *t = getclient(tcn); - getstring(text, p); - filtertext(text, text, true, true); - if(!t || isignored(t->clientnum)) break; - if(t->state!=CS_DEAD && t->state!=CS_SPECTATOR) - particle_textcopy(t->abovehead(), text, PART_TEXT, 2000, 0x6496FF, 4.0f, -8); - conoutf(CON_TEAMCHAT, "\fs\f8[team]\fr %s: \f8%s", chatcolorname(t), text); - break; - } - - case N_MAPCHANGE: - getstring(text, p); - filtertext(text, text, false); - fixmapname(text); - changemapserv(text, getint(p)); - mapchanged = true; - if(getint(p)) entities::spawnitems(); - else senditemstoserver = false; - break; - - case N_FORCEDEATH: - { - int cn = getint(p); - fpsent *d = cn==player1->clientnum ? player1 : newclient(cn); - if(!d) break; - if(d==player1) - { - if(editmode) toggleedit(); - stopfollowing(); - if(deathscore) showscores(true); - } - else d->resetinterp(); - d->state = CS_DEAD; - break; - } - - case N_ITEMLIST: - { - int n; - while((n = getint(p))>=0 && !p.overread()) - { - if(mapchanged) entities::setspawn(n, true); - getint(p); // type - } - break; - } - - case N_INITCLIENT: // another client either connected or changed name/team - { - int cn = getint(p); - fpsent *d = newclient(cn); - if(!d) - { - getstring(text, p); - getstring(text, p); - getint(p); - break; - } - getstring(text, p); - filtertext(text, text, false, false, MAXNAMELEN); - if(!text[0]) copystring(text, "Anonymous"); - if(d->name[0]) // already connected - { - if(strcmp(d->name, text) && !isignored(d->clientnum)) - conoutf("%s is now known as %s", colorname(d), colorname(d, text)); - } - else // new client - { - conoutf("\f0join:\f7 %s", colorname(d, text)); - if(needclipboard >= 0) needclipboard++; - } - copystring(d->name, text, MAXNAMELEN+1); - getstring(text, p); - filtertext(d->team, text, false, false, MAXTEAMLEN); - d->playermodel = getint(p); - d->playermodel = 0; - break; - } - - case N_SWITCHNAME: - getstring(text, p); - if(d) - { - filtertext(text, text, false, false, MAXNAMELEN); - if(!text[0]) copystring(text, "Anonymous"); - if(strcmp(text, d->name)) - { - if(!isignored(d->clientnum)) conoutf("%s is now known as %s", colorname(d), colorname(d, text)); - copystring(d->name, text, MAXNAMELEN+1); - } - } - break; - - case N_SWITCHMODEL: - break; - - case N_CDIS: - clientdisconnected(getint(p)); - break; - - case N_SPAWN: - { - if(d) - { - if(d->state==CS_DEAD && d->lastpain) saveragdoll(d); - d->respawn(); - } - parsestate(d, p); - if(!d) break; - d->state = CS_SPAWNING; - if(player1->state==CS_SPECTATOR && following==d->clientnum) - lasthit = 0; - break; - } - - case N_SPAWNSTATE: - { - int scn = getint(p); - fpsent *s = getclient(scn); - if(!s) { parsestate(NULL, p); break; } - if(s->state==CS_DEAD && s->lastpain) saveragdoll(s); - if(s==player1) - { - if(editmode) toggleedit(); - stopfollowing(); - } - s->respawn(); - parsestate(s, p); - s->state = CS_ALIVE; - pickgamespawn(s); - if(s == player1) - { - showscores(false); - lasthit = 0; - } - ai::spawned(s); - addmsg(N_SPAWN, "rcii", s, s->lifesequence, s->gunselect); - break; - } - - case N_SHOTFX: - { - int scn = getint(p), gun = getint(p), id = getint(p); - vec from, to; - loopk(3) from[k] = getint(p)/DMF; - loopk(3) to[k] = getint(p)/DMF; - fpsent *s = getclient(scn); - if(!s) break; - if(gun>GUN_FIST && gun<=GUN_PISTOL && s->ammo[gun]) s->ammo[gun]--; - s->gunselect = clamp(gun, (int)GUN_FIST, (int)GUN_PISTOL); - s->gunwait = guns[s->gunselect].attackdelay; - int prevaction = s->lastaction; - s->lastaction = lastmillis; - s->lastattackgun = s->gunselect; - shoteffects(s->gunselect, from, to, s, false, id, prevaction); - break; - } - - case N_EXPLODEFX: - { - int ecn = getint(p), gun = getint(p), id = getint(p); - fpsent *e = getclient(ecn); - if(!e) break; - explodeeffects(gun, e, false, id); - break; - } - case N_DAMAGE: - { - int tcn = getint(p), - acn = getint(p), - damage = getint(p), - armour = getint(p), - health = getint(p); - fpsent *target = getclient(tcn), - *actor = getclient(acn); - if(!target || !actor) break; - target->armour = armour; - target->health = health; - if(target->state == CS_ALIVE && actor != player1) target->lastpain = lastmillis; - damaged(damage, target, actor, false); - break; - } - - case N_HITPUSH: - { - int tcn = getint(p), gun = getint(p), damage = getint(p); - fpsent *target = getclient(tcn); - vec dir; - loopk(3) dir[k] = getint(p)/DNF; - if(target) target->hitpush(damage * (target->health<=0 ? deadpush : 1), dir, NULL, gun); - break; - } - - case N_DIED: - { - int vcn = getint(p), acn = getint(p), frags = getint(p), tfrags = getint(p); - fpsent *victim = getclient(vcn), - *actor = getclient(acn); - if(!actor) break; - actor->frags = frags; - if(m_teammode) setteaminfo(actor->team, tfrags); - extern int hidefrags; - if(actor!=player1 && (!hidefrags)) - { - defformatstring(ds, "%d", actor->frags); - particle_textcopy(actor->abovehead(), ds, PART_TEXT, 2000, 0x32FF64, 4.0f, -8); - } - if(!victim) break; - killed(victim, actor); - break; - } - - case N_TEAMINFO: - for(;;) - { - getstring(text, p); - if(p.overread() || !text[0]) break; - int frags = getint(p); - if(p.overread()) break; - if(m_teammode) setteaminfo(text, frags); - } - break; - - case N_GUNSELECT: - { - if(!d) return; - int gun = getint(p); - d->gunselect = clamp(gun, int(GUN_FIST), int(GUN_PISTOL)); - playsound(S_WEAPLOAD, &d->o); - break; - } - - case N_TAUNT: - { - if(!d) return; - d->lasttaunt = lastmillis; - break; - } - - case N_RESUME: - { - for(;;) - { - int cn = getint(p); - if(p.overread() || cn<0) break; - fpsent *d = (cn == player1->clientnum ? player1 : newclient(cn)); - parsestate(d, p, true); - } - break; - } - - case N_ITEMSPAWN: - { - int i = getint(p); - if(!entities::ents.inrange(i)) break; - entities::setspawn(i, true); - ai::itemspawned(i); - playsound(S_ITEMSPAWN, &entities::ents[i]->o, NULL, 0, 0, 0, -1, 0, 1500); - #if 0 - const char *name = entities::itemname(i); - if(name) particle_text(entities::ents[i]->o, name, PART_TEXT, 2000, 0x32FF64, 4.0f, -8); - #endif - int icon = entities::itemicon(i); - if(icon >= 0) particle_icon(vec(0.0f, 0.0f, 4.0f).add(entities::ents[i]->o), icon%4, icon/4, PART_HUD_ICON, 2000, 0xFFFFFF, 2.0f, -8); - break; - } - - case N_ITEMACC: // server acknowledges that I picked up this item - { - int i = getint(p), cn = getint(p); - if(cn >= 0) - { - fpsent *d = getclient(cn); - entities::pickupeffects(i, d); - } - else entities::setspawn(i, true); - break; - } - - case N_CLIPBOARD: - { - int cn = getint(p), unpacklen = getint(p), packlen = getint(p); - fpsent *d = getclient(cn); - ucharbuf q = p.subbuf(max(packlen, 0)); - if(d) unpackeditinfo(d->edit, q.buf, q.maxlen, unpacklen); - break; - } - case N_UNDO: - case N_REDO: - { - int cn = getint(p), unpacklen = getint(p), packlen = getint(p); - fpsent *d = getclient(cn); - ucharbuf q = p.subbuf(max(packlen, 0)); - if(d) unpackundo(q.buf, q.maxlen, unpacklen); - break; - } - - case N_EDITF: // coop editing messages - case N_EDITT: - case N_EDITM: - case N_FLIP: - case N_COPY: - case N_PASTE: - case N_ROTATE: - case N_REPLACE: - case N_DELCUBE: - case N_EDITVSLOT: - { - if(!d) return; - selinfo sel; - sel.o.x = getint(p); sel.o.y = getint(p); sel.o.z = getint(p); - sel.s.x = getint(p); sel.s.y = getint(p); sel.s.z = getint(p); - sel.grid = getint(p); sel.orient = getint(p); - sel.cx = getint(p); sel.cxs = getint(p); sel.cy = getint(p), sel.cys = getint(p); - sel.corner = getint(p); - switch(type) - { - case N_EDITF: { int dir = getint(p), mode = getint(p); if(sel.validate()) mpeditface(dir, mode, sel, false); break; } - case N_EDITT: - { - int tex = getint(p), - allfaces = getint(p); - if(p.remaining() < 2) return; - int extra = lilswap(*(const ushort *)p.pad(2)); - if(p.remaining() < extra) return; - ucharbuf ebuf = p.subbuf(extra); - if(sel.validate()) mpedittex(tex, allfaces, sel, ebuf); - break; - } - case N_EDITM: { int mat = getint(p), filter = getint(p); if(sel.validate()) mpeditmat(mat, filter, sel, false); break; } - case N_FLIP: if(sel.validate()) mpflip(sel, false); break; - case N_COPY: if(d && sel.validate()) mpcopy(d->edit, sel, false); break; - case N_PASTE: if(d && sel.validate()) mppaste(d->edit, sel, false); break; - case N_ROTATE: { int dir = getint(p); if(sel.validate()) mprotate(dir, sel, false); break; } - case N_REPLACE: - { - int oldtex = getint(p), - newtex = getint(p), - insel = getint(p); - if(p.remaining() < 2) return; - int extra = lilswap(*(const ushort *)p.pad(2)); - if(p.remaining() < extra) return; - ucharbuf ebuf = p.subbuf(extra); - if(sel.validate()) mpreplacetex(oldtex, newtex, insel>0, sel, ebuf); - break; - } - case N_DELCUBE: if(sel.validate()) mpdelcube(sel, false); break; - case N_EDITVSLOT: - { - int delta = getint(p), - allfaces = getint(p); - if(p.remaining() < 2) return; - int extra = lilswap(*(const ushort *)p.pad(2)); - if(p.remaining() < extra) return; - ucharbuf ebuf = p.subbuf(extra); - if(sel.validate()) mpeditvslot(delta, allfaces, sel, ebuf); - break; - } - } - break; - } - case N_REMIP: - { - if(!d) return; - conoutf("%s remipped", colorname(d)); - mpremip(false); - break; - } - case N_EDITENT: // coop edit of ent - { - if(!d) return; - int i = getint(p); - float x = getint(p)/DMF, y = getint(p)/DMF, z = getint(p)/DMF; - int type = getint(p); - int attr1 = getint(p), attr2 = getint(p), attr3 = getint(p), attr4 = getint(p), attr5 = getint(p); - - mpeditent(i, vec(x, y, z), type, attr1, attr2, attr3, attr4, attr5, false); - break; - } - case N_EDITVAR: - { - if(!d) return; - int type = getint(p); - getstring(text, p); - string name; - filtertext(name, text, false); - ident *id = getident(name); - switch(type) - { - case ID_VAR: - { - int val = getint(p); - if(id && id->flags&IDF_OVERRIDE && !(id->flags&IDF_READONLY)) setvar(name, val); - break; - } - case ID_FVAR: - { - float val = getfloat(p); - if(id && id->flags&IDF_OVERRIDE && !(id->flags&IDF_READONLY)) setfvar(name, val); - break; - } - case ID_SVAR: - { - getstring(text, p); - if(id && id->flags&IDF_OVERRIDE && !(id->flags&IDF_READONLY)) setsvar(name, text); - break; - } - } - printvar(d, id); - break; - } - - case N_PONG: - addmsg(N_CLIENTPING, "i", player1->ping = (player1->ping*5+totalmillis-getint(p))/6); - break; - - case N_CLIENTPING: - if(!d) return; - d->ping = getint(p); - break; - - case N_TIMEUP: - timeupdate(getint(p)); - break; - - case N_SERVMSG: - getstring(text, p); - conoutf("%s", text); - break; - - case N_SENDDEMOLIST: - { - int demos = getint(p); - if(demos <= 0) conoutf("no demos available"); - else loopi(demos) - { - getstring(text, p); - if(p.overread()) break; - conoutf("%d. %s", i+1, text); - } - break; - } - - case N_DEMOPLAYBACK: - { - int on = getint(p); - if(on) player1->state = CS_SPECTATOR; - else clearclients(); - demoplayback = on!=0; - player1->clientnum = getint(p); - gamepaused = false; - execident(on ? "demostart" : "demoend"); - break; - } - - case N_CURRENTMASTER: - { - int mm = getint(p), mn; - loopv(players) players[i]->privilege = PRIV_NONE; - while((mn = getint(p))>=0 && !p.overread()) - { - fpsent *m = mn==player1->clientnum ? player1 : newclient(mn); - int priv = getint(p); - if(m) m->privilege = priv; - } - if(mm != mastermode) - { - mastermode = mm; - conoutf("mastermode is %s (%d)", server::mastermodename(mastermode), mastermode); - } - break; - } - - case N_MASTERMODE: - { - mastermode = getint(p); - conoutf("mastermode is %s (%d)", server::mastermodename(mastermode), mastermode); - break; - } - - case N_EDITMODE: - { - int val = getint(p); - if(!d) break; - if(val) - { - d->editstate = d->state; - d->state = CS_EDITING; - } - else - { - d->state = d->editstate; - if(d->state==CS_DEAD) deathstate(d, true); - } - break; - } - - case N_SPECTATOR: - { - int sn = getint(p), val = getint(p); - fpsent *s; - if(sn==player1->clientnum) - { - s = player1; - if(val && remote && !player1->privilege) senditemstoserver = false; - } - else s = newclient(sn); - if(!s) return; - if(val) - { - if(s==player1) - { - if(editmode) toggleedit(); - if(s->state==CS_DEAD) showscores(false); - disablezoom(); - } - s->state = CS_SPECTATOR; - } - else if(s->state==CS_SPECTATOR) - { - if(s==player1) stopfollowing(); - deathstate(s, true); - } - break; - } - - case N_SETTEAM: - { - int wn = getint(p); - getstring(text, p); - int reason = getint(p); - fpsent *w = getclient(wn); - if(!w) return; - filtertext(w->team, text, false, false, MAXTEAMLEN); - static const char * const fmt[2] = { "%s switched to team %s", "%s forced to team %s"}; - if(reason >= 0 && size_t(reason) < sizeof(fmt)/sizeof(fmt[0])) - conoutf(fmt[reason], colorname(w), w->team); - break; - } - - case N_ANNOUNCE: - { - int t = getint(p); - if (t==I_QUAD) { playsound(S_V_QUAD10, NULL, NULL, 0, 0, 0, -1, 0, 3000); conoutf(CON_GAMEINFO, "\f2quad damage will spawn in 10 seconds!"); } - else if(t==I_BOOST) { playsound(S_V_BOOST10, NULL, NULL, 0, 0, 0, -1, 0, 3000); conoutf(CON_GAMEINFO, "\f2health boost will spawn in 10 seconds!"); } - break; - } - - case N_NEWMAP: - { - int size = getint(p); - if(size>=0) emptymap(size, true, NULL); - else enlargemap(true); - if(d && d!=player1) - { - int newsize = 0; - while(1<<newsize < getworldsize()) newsize++; - conoutf(size>=0 ? "%s started a new map of size %d" : "%s enlarged the map to size %d", colorname(d), newsize); - } - break; - } - - case N_REQAUTH: - { - getstring(text, p); - if(autoauth && text[0] && tryauth(text)) conoutf("server requested authkey \"%s\"", text); - break; - } - - case N_AUTHCHAL: - { - getstring(text, p); - authkey *a = findauthkey(text); - uint id = (uint)getint(p); - getstring(text, p); - if(a && a->lastauth && lastmillis - a->lastauth < 60*1000) - { - vector<char> buf; - answerchallenge(a->key, text, buf); - //conoutf(CON_DEBUG, "answering %u, challenge %s with %s", id, text, buf.getbuf()); - packetbuf p(MAXTRANS, ENET_PACKET_FLAG_RELIABLE); - putint(p, N_AUTHANS); - sendstring(a->desc, p); - putint(p, id); - sendstring(buf.getbuf(), p); - sendclientpacket(p.finalize(), 1); - } - break; - } - - case N_INITAI: - { - int bn = getint(p), on = getint(p), at = getint(p), sk = clamp(getint(p), 1, 101), pm = getint(p); - string name, team; - getstring(text, p); - filtertext(name, text, false, false, MAXNAMELEN); - getstring(text, p); - filtertext(team, text, false, false, MAXTEAMLEN); - fpsent *b = newclient(bn); - if(!b) break; - ai::init(b, at, on, sk, bn, pm, name, team); - break; - } - - case N_SERVCMD: - getstring(text, p); - break; - - default: - neterr("type", cn < 0); - return; - } - } - - struct demoreq - { - int tag; - string name; - }; - vector<demoreq> demoreqs; - enum { MAXDEMOREQS = 7 }; - static int lastdemoreq = 0; - - void receivefile(packetbuf &p) - { - int type; - while(p.remaining()) switch(type = getint(p)) - { - case N_DEMOPACKET: return; - case N_SENDDEMO: - { - string fname; - fname[0] = '\0'; - int tag = getint(p); - loopv(demoreqs) if(demoreqs[i].tag == tag) - { - copystring(fname, demoreqs[i].name); - demoreqs.remove(i); - break; - } - if(!fname[0]) - { - time_t t = time(NULL); - size_t len = strftime(fname, sizeof(fname), "%Y-%m-%d_%H.%M.%S", localtime(&t)); - fname[min(len, sizeof(fname)-1)] = '\0'; - } - int len = strlen(fname); - if(len < 4 || strcasecmp(&fname[len-4], ".dmo")) concatstring(fname, ".dmo"); - stream *demo = NULL; - if(const char *buf = server::getdemofile(fname, true)) demo = openrawfile(buf, "wb"); - if(!demo) demo = openrawfile(fname, "wb"); - if(!demo) return; - conoutf("received demo \"%s\"", fname); - ucharbuf b = p.subbuf(p.remaining()); - demo->write(b.buf, b.maxlen); - delete demo; - break; - } - - case N_SENDMAP: - { - if(!m_edit) return; - string oldname; - copystring(oldname, getclientmap()); - defformatstring(mname, "getmap_%d", lastmillis); - defformatstring(fname, "packages/maps/%s.ogz", mname); - stream *map = openrawfile(path(fname), "wb"); - if(!map) return; - conoutf("received map"); - ucharbuf b = p.subbuf(p.remaining()); - map->write(b.buf, b.maxlen); - delete map; - if(load_world(mname, oldname[0] ? oldname : NULL)) - entities::spawnitems(true); - remove(findfile(fname, "rb")); - break; - } - } - } - - void parsepacketclient(int chan, packetbuf &p) // processes any updates from the server - { - if(p.packet->flags&ENET_PACKET_FLAG_UNSEQUENCED) return; - switch(chan) - { - case 0: - parsepositions(p); - break; - - case 1: - parsemessages(-1, NULL, p); - break; - - case 2: - receivefile(p); - break; - } - } - - void getmap() - { - if(!m_edit) { conoutf(CON_ERROR, "\"getmap\" only works in coop edit mode"); return; } - conoutf("getting map..."); - addmsg(N_GETMAP, "r"); - } - COMMAND(getmap, ""); - - void stopdemo() - { - if(remote) - { - if(player1->privilege<PRIV_MASTER) return; - addmsg(N_STOPDEMO, "r"); - } - else server::stopdemo(); - } - COMMAND(stopdemo, ""); - - void recorddemo(int val) - { - if(remote && player1->privilege<PRIV_MASTER) return; - addmsg(N_RECORDDEMO, "ri", val); - } - ICOMMAND(recorddemo, "i", (int *val), recorddemo(*val)); - - void cleardemos(int val) - { - if(remote && player1->privilege<PRIV_MASTER) return; - addmsg(N_CLEARDEMOS, "ri", val); - } - ICOMMAND(cleardemos, "i", (int *val), cleardemos(*val)); - - void getdemo(char *val, char *name) - { - int i = 0; - if(isdigit(val[0]) || name[0]) i = parseint(val); - else name = val; - if(i<=0) conoutf("getting demo..."); - else conoutf("getting demo %d...", i); - ++lastdemoreq; - if(name[0]) - { - if(demoreqs.length() >= MAXDEMOREQS) demoreqs.remove(0); - demoreq &r = demoreqs.add(); - r.tag = lastdemoreq; - copystring(r.name, name); - } - addmsg(N_GETDEMO, "rii", i, lastdemoreq); - } - ICOMMAND(getdemo, "ss", (char *val, char *name), getdemo(val, name)); - - void listdemos() - { - conoutf("listing demos..."); - addmsg(N_LISTDEMOS, "r"); - } - COMMAND(listdemos, ""); - - void sendmap() - { - if(!m_edit || (player1->state==CS_SPECTATOR && remote && !player1->privilege)) { conoutf(CON_ERROR, "\"sendmap\" only works in coop edit mode"); return; } - conoutf("sending map..."); - defformatstring(mname, "sendmap_%d", lastmillis); - save_world(mname, true); - defformatstring(fname, "packages/maps/%s.ogz", mname); - stream *map = openrawfile(path(fname), "rb"); - if(map) - { - stream::offset len = map->size(); - if(len > 4*1024*1024) conoutf(CON_ERROR, "map is too large"); - else if(len <= 0) conoutf(CON_ERROR, "could not read map"); - else - { - sendfile(-1, 2, map); - if(needclipboard >= 0) needclipboard++; - } - delete map; - } - else conoutf(CON_ERROR, "could not read map"); - remove(findfile(fname, "rb")); - } - COMMAND(sendmap, ""); - - void gotoplayer(const char *arg) - { - if(player1->state!=CS_SPECTATOR && player1->state!=CS_EDITING) return; - int i = parseplayer(arg); - if(i>=0) - { - fpsent *d = getclient(i); - if(!d || d==player1) return; - player1->o = d->o; - vec dir; - vecfromyawpitch(player1->yaw, player1->pitch, 1, 0, dir); - player1->o.add(dir.mul(-32)); - player1->resetinterp(); - } - } - COMMANDN(goto, gotoplayer, "s"); - - void gotosel() - { - if(player1->state!=CS_EDITING) return; - player1->o = getselpos(); - vec dir; - vecfromyawpitch(player1->yaw, player1->pitch, 1, 0, dir); - player1->o.add(dir.mul(-32)); - player1->resetinterp(); - } - COMMAND(gotosel, ""); + bool senditemstoserver = false, sendcrc = false; // after a map change, since server doesn't have map data + int lastping = 0; + + bool connected = false, remote = false, demoplayback = false, gamepaused = false; + int sessionid = 0, mastermode = MM_OPEN, gamespeed = 100; + string servinfo = "", servauth = "", connectpass = ""; + + VARP(deadpush, 1, 2, 20); + + void switchname(const char *name) + { + filtertext(player1->name, name, false, false, MAXNAMELEN); + if(!player1->name[0]) copystring(player1->name, "Anonymous"); + addmsg(N_SWITCHNAME, "rs", player1->name); + } + void printname() + { + conoutf("your name is: %s", colorname(player1)); + } + ICOMMAND(name, "sN", (char *s, int *numargs), + { + if(*numargs > 0) switchname(s); + else if(!*numargs) printname(); + else result(colorname(player1)); + }); + ICOMMAND(getname, "", (), result(player1->name)); + + void switchteam(const char *team) + { + if(player1->clientnum < 0) filtertext(player1->team, team, false, false, MAXTEAMLEN); + else addmsg(N_SWITCHTEAM, "rs", team); + } + void printteam() + { + conoutf("your team is: %s", player1->team); + } + ICOMMAND(team, "sN", (char *s, int *numargs), + { + if(*numargs > 0) switchteam(s); + else if(!*numargs) printteam(); + else result(player1->team); + }); + ICOMMAND(getteam, "", (), result(player1->team)); + + struct authkey + { + char *name, *key, *desc; + int lastauth; + + authkey(const char *name, const char *key, const char *desc) + : name(newstring(name)), key(newstring(key)), desc(newstring(desc)), + lastauth(0) + { + } + + ~authkey() + { + DELETEA(name); + DELETEA(key); + DELETEA(desc); + } + }; + vector<authkey *> authkeys; + + authkey *findauthkey(const char *desc = "") + { + loopv(authkeys) if(!strcmp(authkeys[i]->desc, desc) && !strcasecmp(authkeys[i]->name, player1->name)) return authkeys[i]; + loopv(authkeys) if(!strcmp(authkeys[i]->desc, desc)) return authkeys[i]; + return NULL; + } + + VARP(autoauth, 0, 1, 1); + + void addauthkey(const char *name, const char *key, const char *desc) + { + loopvrev(authkeys) if(!strcmp(authkeys[i]->desc, desc) && !strcmp(authkeys[i]->name, name)) delete authkeys.remove(i); + if(name[0] && key[0]) authkeys.add(new authkey(name, key, desc)); + } + ICOMMAND(authkey, "sss", (char *name, char *key, char *desc), addauthkey(name, key, desc)); + + bool hasauthkey(const char *name, const char *desc) + { + if(!name[0] && !desc[0]) return authkeys.length() > 0; + loopvrev(authkeys) if(!strcmp(authkeys[i]->desc, desc) && !strcmp(authkeys[i]->name, name)) return true; + return false; + } + + ICOMMAND(hasauthkey, "ss", (char *name, char *desc), intret(hasauthkey(name, desc) ? 1 : 0)); + + void genauthkey(const char *secret) + { + if(!secret[0]) { conoutf(CON_ERROR, "you must specify a secret password"); return; } + vector<char> privkey, pubkey; + genprivkey(secret, privkey, pubkey); + conoutf("private key: %s", privkey.getbuf()); + conoutf("public key: %s", pubkey.getbuf()); + result(privkey.getbuf()); + } + COMMAND(genauthkey, "s"); + + void getpubkey(const char *desc) + { + authkey *k = findauthkey(desc); + if(!k) { if(desc[0]) conoutf(CON_ERROR, "no authkey found: %s", desc); else conoutf(CON_ERROR, "no global authkey found"); return; } + vector<char> pubkey; + if(!calcpubkey(k->key, pubkey)) { conoutf(CON_ERROR, "failed calculating pubkey"); return; } + result(pubkey.getbuf()); + } + COMMAND(getpubkey, "s"); + + void saveauthkeys() + { + stream *f = openfile("auth.cfg", "w"); + if(!f) { conoutf(CON_ERROR, "failed to open auth.cfg for writing"); return; } + loopv(authkeys) + { + authkey *a = authkeys[i]; + f->printf("authkey %s %s %s\n", escapestring(a->name), escapestring(a->key), escapestring(a->desc)); + } + conoutf("saved authkeys to auth.cfg"); + delete f; + } + COMMAND(saveauthkeys, ""); + + void sendmapinfo() + { + if(!connected) return; + sendcrc = true; + if(player1->state!=CS_SPECTATOR || player1->privilege || !remote) senditemstoserver = true; + } + + void writeclientinfo(stream *f) + { + f->printf("name %s\n", escapestring(player1->name)); + } + + bool allowedittoggle() + { + if(editmode) return true; + if(isconnected() && multiplayer(false) && !m_edit) + { + conoutf(CON_ERROR, "editing in multiplayer requires coop edit mode (1)"); + return false; + } + return execidentbool("allowedittoggle", true); + } + + void edittoggled(bool on) + { + addmsg(N_EDITMODE, "ri", on ? 1 : 0); + if(player1->state==CS_DEAD) deathstate(player1, true); + else if(player1->state==CS_EDITING && player1->editstate==CS_DEAD) showscores(false); + disablezoom(); + player1->suicided = player1->respawned = -2; + } + + const char *getclientname(int cn) + { + fpsent *d = getclient(cn); + return d ? d->name : ""; + } + ICOMMAND(getclientname, "i", (int *cn), result(getclientname(*cn))); + + const char *getclientteam(int cn) + { + fpsent *d = getclient(cn); + return d ? d->team : ""; + } + ICOMMAND(getclientteam, "i", (int *cn), result(getclientteam(*cn))); + + const char *getclienticon(int cn) + { + fpsent *d = getclient(cn); + if(!d || d->state==CS_SPECTATOR) return "spectator"; + const playermodelinfo &mdl = getplayermodelinfo(d); + return m_teammode ? (isteam(player1->team, d->team) ? mdl.blueicon : mdl.redicon) : mdl.ffaicon; + } + ICOMMAND(getclienticon, "i", (int *cn), result(getclienticon(*cn))); + + bool ismaster(int cn) + { + fpsent *d = getclient(cn); + return d && d->privilege >= PRIV_MASTER; + } + ICOMMAND(ismaster, "i", (int *cn), intret(ismaster(*cn) ? 1 : 0)); + + bool isauth(int cn) + { + fpsent *d = getclient(cn); + return d && d->privilege >= PRIV_AUTH; + } + ICOMMAND(isauth, "i", (int *cn), intret(isauth(*cn) ? 1 : 0)); + + bool isadmin(int cn) + { + fpsent *d = getclient(cn); + return d && d->privilege >= PRIV_ADMIN; + } + ICOMMAND(isadmin, "i", (int *cn), intret(isadmin(*cn) ? 1 : 0)); + + ICOMMAND(getmastermode, "", (), intret(mastermode)); + ICOMMAND(mastermodename, "i", (int *mm), result(server::mastermodename(*mm, ""))); + + bool isspectator(int cn) + { + fpsent *d = getclient(cn); + return d && d->state==CS_SPECTATOR; + } + ICOMMAND(isspectator, "i", (int *cn), intret(isspectator(*cn) ? 1 : 0)); + + bool isai(int cn, int type) + { + fpsent *d = getclient(cn); + int aitype = type > 0 && type < AI_MAX ? type : AI_BOT; + return d && d->aitype==aitype; + } + ICOMMAND(isai, "ii", (int *cn, int *type), intret(isai(*cn, *type) ? 1 : 0)); + + int parseplayer(const char *arg) + { + char *end; + int n = strtol(arg, &end, 10); + if(*arg && !*end) + { + if(n!=player1->clientnum && !clients.inrange(n)) return -1; + return n; + } + // try case sensitive first + loopv(players) + { + fpsent *o = players[i]; + if(!strcmp(arg, o->name)) return o->clientnum; + } + // nothing found, try case insensitive + loopv(players) + { + fpsent *o = players[i]; + if(!strcasecmp(arg, o->name)) return o->clientnum; + } + return -1; + } + ICOMMAND(getclientnum, "s", (char *name), intret(name[0] ? parseplayer(name) : player1->clientnum)); + + void listclients(bool local, bool bots) + { + vector<char> buf; + string cn; + int numclients = 0; + if(local && connected) + { + formatstring(cn, "%d", player1->clientnum); + buf.put(cn, strlen(cn)); + numclients++; + } + loopv(clients) if(clients[i] && (bots || clients[i]->aitype == AI_NONE)) + { + formatstring(cn, "%d", clients[i]->clientnum); + if(numclients++) buf.add(' '); + buf.put(cn, strlen(cn)); + } + buf.add('\0'); + result(buf.getbuf()); + } + ICOMMAND(listclients, "bb", (int *local, int *bots), listclients(*local>0, *bots!=0)); + + void clearbans() + { + addmsg(N_CLEARBANS, "r"); + } + COMMAND(clearbans, ""); + + void kick(const char *victim, const char *reason) + { + int vn = parseplayer(victim); + if(vn>=0 && vn!=player1->clientnum) addmsg(N_KICK, "ris", vn, reason); + } + COMMAND(kick, "ss"); + + void authkick(const char *desc, const char *victim, const char *reason) + { + authkey *a = findauthkey(desc); + int vn = parseplayer(victim); + if(a && vn>=0 && vn!=player1->clientnum) + { + a->lastauth = lastmillis; + addmsg(N_AUTHKICK, "rssis", a->desc, a->name, vn, reason); + } + } + ICOMMAND(authkick, "ss", (const char *victim, const char *reason), authkick("", victim, reason)); + ICOMMAND(sauthkick, "ss", (const char *victim, const char *reason), if(servauth[0]) authkick(servauth, victim, reason)); + ICOMMAND(dauthkick, "sss", (const char *desc, const char *victim, const char *reason), if(desc[0]) authkick(desc, victim, reason)); + + vector<int> ignores; + + void ignore(int cn) + { + fpsent *d = getclient(cn); + if(!d || d == player1) return; + conoutf("ignoring %s", d->name); + if(ignores.find(cn) < 0) ignores.add(cn); + } + + void unignore(int cn) + { + if(ignores.find(cn) < 0) return; + fpsent *d = getclient(cn); + if(d) conoutf("stopped ignoring %s", d->name); + ignores.removeobj(cn); + } + + bool isignored(int cn) { return ignores.find(cn) >= 0; } + + ICOMMAND(ignore, "s", (char *arg), ignore(parseplayer(arg))); + ICOMMAND(unignore, "s", (char *arg), unignore(parseplayer(arg))); + ICOMMAND(isignored, "s", (char *arg), intret(isignored(parseplayer(arg)) ? 1 : 0)); + + void setteam(const char *arg1, const char *arg2) + { + int i = parseplayer(arg1); + if(i>=0) addmsg(N_SETTEAM, "ris", i, arg2); + } + COMMAND(setteam, "ss"); + + void hashpwd(const char *pwd) + { + if(player1->clientnum<0) return; + string hash; + server::hashpassword(player1->clientnum, sessionid, pwd, hash); + result(hash); + } + COMMAND(hashpwd, "s"); + + void setmaster(const char *arg, const char *who) + { + if(!arg[0]) return; + int val = 1, cn = player1->clientnum; + if(who[0]) + { + cn = parseplayer(who); + if(cn < 0) return; + } + string hash = ""; + if(!arg[1] && isdigit(arg[0])) val = parseint(arg); + else + { + if(cn != player1->clientnum) return; + server::hashpassword(player1->clientnum, sessionid, arg, hash); + } + addmsg(N_SETMASTER, "riis", cn, val, hash); + } + COMMAND(setmaster, "ss"); + ICOMMAND(mastermode, "i", (int *val), addmsg(N_MASTERMODE, "ri", *val)); + + bool tryauth(const char *desc) + { + authkey *a = findauthkey(desc); + if(!a) return false; + a->lastauth = lastmillis; + addmsg(N_AUTHTRY, "rss", a->desc, a->name); + return true; + } + ICOMMAND(auth, "s", (char *desc), tryauth(desc)); + ICOMMAND(sauth, "", (), if(servauth[0]) tryauth(servauth)); + ICOMMAND(dauth, "s", (char *desc), if(desc[0]) tryauth(desc)); + + ICOMMAND(getservauth, "", (), result(servauth)); + + void togglespectator(int val, const char *who) + { + int i = who[0] ? parseplayer(who) : player1->clientnum; + if(i>=0) addmsg(N_SPECTATOR, "rii", i, val); + } + ICOMMAND(spectator, "is", (int *val, char *who), togglespectator(*val, who)); + + ICOMMAND(checkmaps, "", (), addmsg(N_CHECKMAPS, "r")); + + int gamemode = INT_MAX, nextmode = INT_MAX; + string clientmap = ""; + + void changemapserv(const char *name, int mode) // forced map change from the server + { + if(multiplayer(false) && !m_mp(mode)) + { + conoutf(CON_ERROR, "mode %s (%d) not supported in multiplayer", server::modename(gamemode), gamemode); + loopi(NUMGAMEMODES) if(m_mp(STARTGAMEMODE + i)) { mode = STARTGAMEMODE + i; break; } + } + + gamemode = mode; + nextmode = mode; + if(editmode) toggleedit(); + if(m_demo) { entities::resetspawns(); return; } + if((m_edit && !name[0]) || !load_world(name)) + { + emptymap(0, true, name); + senditemstoserver = false; + } + startgame(); + } + + void setmode(int mode) + { + if(multiplayer(false) && !m_mp(mode)) + { + conoutf(CON_ERROR, "mode %s (%d) not supported in multiplayer", server::modename(mode), mode); + intret(0); + return; + } + nextmode = mode; + intret(1); + } + ICOMMAND(mode, "i", (int *val), setmode(*val)); + ICOMMAND(getmode, "", (), intret(gamemode)); + ICOMMAND(timeremaining, "i", (int *formatted), + { + int val = max(maplimit - lastmillis + 999, 0)/1000; + if(*formatted) + { + defformatstring(str, "%d:%02d", val/60, val%60); + result(str); + } + else intret(val); + }); + ICOMMANDS("m_noitems", "i", (int *mode), { int gamemode = *mode; intret(m_noitems); }); + ICOMMANDS("m_noammo", "i", (int *mode), { int gamemode = *mode; intret(m_noammo); }); + ICOMMANDS("m_insta", "i", (int *mode), { int gamemode = *mode; intret(m_insta); }); + ICOMMANDS("m_efficiency", "i", (int *mode), { int gamemode = *mode; intret(m_efficiency); }); + ICOMMANDS("m_teammode", "i", (int *mode), { int gamemode = *mode; intret(m_teammode); }); + ICOMMANDS("m_demo", "i", (int *mode), { int gamemode = *mode; intret(m_demo); }); + ICOMMANDS("m_edit", "i", (int *mode), { int gamemode = *mode; intret(m_edit); }); + ICOMMANDS("m_lobby", "i", (int *mode), { int gamemode = *mode; intret(m_lobby); }); + + void changemap(const char *name, int mode) // request map change, server may ignore + { + if(!remote) + { + server::forcemap(name, mode); + if(!isconnected()) localconnect(); + } + else if(player1->state!=CS_SPECTATOR || player1->privilege) addmsg(N_MAPVOTE, "rsi", name, mode); + } + void changemap(const char *name) + { + changemap(name, m_valid(nextmode) ? nextmode : (remote ? 0 : 1)); + } + ICOMMAND(map, "s", (char *name), changemap(name)); + + void forceintermission() + { + if(!remote && !hasnonlocalclients()) server::startintermission(); + else addmsg(N_FORCEINTERMISSION, "r"); + } + + void forceedit(const char *name) + { + changemap(name, 1); + } + + void newmap(int size) + { + addmsg(N_NEWMAP, "ri", size); + } + + int needclipboard = -1; + + void sendclipboard() + { + uchar *outbuf = NULL; + int inlen = 0, outlen = 0; + if(!packeditinfo(localedit, inlen, outbuf, outlen)) + { + outbuf = NULL; + inlen = outlen = 0; + } + packetbuf p(16 + outlen, ENET_PACKET_FLAG_RELIABLE); + putint(p, N_CLIPBOARD); + putint(p, inlen); + putint(p, outlen); + if(outlen > 0) p.put(outbuf, outlen); + sendclientpacket(p.finalize(), 1); + needclipboard = -1; + } + + void edittrigger(const selinfo &sel, int op, int arg1, int arg2, int arg3, const VSlot *vs) + { + if(m_edit) switch(op) + { + case EDIT_FLIP: + case EDIT_COPY: + case EDIT_PASTE: + case EDIT_DELCUBE: + { + switch(op) + { + case EDIT_COPY: needclipboard = 0; break; + case EDIT_PASTE: + if(needclipboard > 0) + { + c2sinfo(true); + sendclipboard(); + } + break; + } + addmsg(N_EDITF + op, "ri9i4", + sel.o.x, sel.o.y, sel.o.z, sel.s.x, sel.s.y, sel.s.z, sel.grid, sel.orient, + sel.cx, sel.cxs, sel.cy, sel.cys, sel.corner); + break; + } + case EDIT_ROTATE: + { + addmsg(N_EDITF + op, "ri9i5", + sel.o.x, sel.o.y, sel.o.z, sel.s.x, sel.s.y, sel.s.z, sel.grid, sel.orient, + sel.cx, sel.cxs, sel.cy, sel.cys, sel.corner, + arg1); + break; + } + case EDIT_MAT: + case EDIT_FACE: + { + addmsg(N_EDITF + op, "ri9i6", + sel.o.x, sel.o.y, sel.o.z, sel.s.x, sel.s.y, sel.s.z, sel.grid, sel.orient, + sel.cx, sel.cxs, sel.cy, sel.cys, sel.corner, + arg1, arg2); + break; + } + case EDIT_TEX: + { + int tex1 = shouldpacktex(arg1); + if(addmsg(N_EDITF + op, "ri9i6", + sel.o.x, sel.o.y, sel.o.z, sel.s.x, sel.s.y, sel.s.z, sel.grid, sel.orient, + sel.cx, sel.cxs, sel.cy, sel.cys, sel.corner, + tex1 ? tex1 : arg1, arg2)) + { + messages.pad(2); + int offset = messages.length(); + if(tex1) packvslot(messages, arg1); + *(ushort *)&messages[offset-2] = lilswap(ushort(messages.length() - offset)); + } + break; + } + case EDIT_REPLACE: + { + int tex1 = shouldpacktex(arg1), tex2 = shouldpacktex(arg2); + if(addmsg(N_EDITF + op, "ri9i7", + sel.o.x, sel.o.y, sel.o.z, sel.s.x, sel.s.y, sel.s.z, sel.grid, sel.orient, + sel.cx, sel.cxs, sel.cy, sel.cys, sel.corner, + tex1 ? tex1 : arg1, tex2 ? tex2 : arg2, arg3)) + { + messages.pad(2); + int offset = messages.length(); + if(tex1) packvslot(messages, arg1); + if(tex2) packvslot(messages, arg2); + *(ushort *)&messages[offset-2] = lilswap(ushort(messages.length() - offset)); + } + break; + } + case EDIT_REMIP: + { + addmsg(N_EDITF + op, "r"); + break; + } + case EDIT_VSLOT: + { + if(addmsg(N_EDITF + op, "ri9i6", + sel.o.x, sel.o.y, sel.o.z, sel.s.x, sel.s.y, sel.s.z, sel.grid, sel.orient, + sel.cx, sel.cxs, sel.cy, sel.cys, sel.corner, + arg1, arg2)) + { + messages.pad(2); + int offset = messages.length(); + packvslot(messages, vs); + *(ushort *)&messages[offset-2] = lilswap(ushort(messages.length() - offset)); + } + break; + } + case EDIT_UNDO: + case EDIT_REDO: + { + uchar *outbuf = NULL; + int inlen = 0, outlen = 0; + if(packundo(op, inlen, outbuf, outlen)) + { + if(addmsg(N_EDITF + op, "ri2", inlen, outlen)) messages.put(outbuf, outlen); + delete[] outbuf; + } + break; + } + } + } + + void printvar(fpsent *d, ident *id) + { + if(id) switch(id->type) + { + case ID_VAR: + { + int val = *id->storage.i; + string str; + if(val < 0) + formatstring(str, "%d", val); + else if(id->flags&IDF_HEX && id->maxval==0xFFFFFF) + formatstring(str, "0x%.6X (%d, %d, %d)", val, (val>>16)&0xFF, (val>>8)&0xFF, val&0xFF); + else + formatstring(str, id->flags&IDF_HEX ? "0x%X" : "%d", val); + conoutf(CON_INFO, id->index, "%s set map var \"%s\" to %s", colorname(d), id->name, str); + break; + } + case ID_FVAR: + conoutf(CON_INFO, id->index, "%s set map var \"%s\" to %s", colorname(d), id->name, floatstr(*id->storage.f)); + break; + case ID_SVAR: + conoutf(CON_INFO, id->index, "%s set map var \"%s\" to \"%s\"", colorname(d), id->name, *id->storage.s); + break; + } + } + + void vartrigger(ident *id) + { + if(!m_edit) return; + switch(id->type) + { + case ID_VAR: + addmsg(N_EDITVAR, "risi", ID_VAR, id->name, *id->storage.i); + break; + + case ID_FVAR: + addmsg(N_EDITVAR, "risf", ID_FVAR, id->name, *id->storage.f); + break; + + case ID_SVAR: + addmsg(N_EDITVAR, "riss", ID_SVAR, id->name, *id->storage.s); + break; + default: return; + } + printvar(player1, id); + } + + void pausegame(bool val) + { + if(!connected) return; + if(!remote) server::forcepaused(val); + else addmsg(N_PAUSEGAME, "ri", val ? 1 : 0); + } + ICOMMAND(pausegame, "i", (int *val), pausegame(*val > 0)); + ICOMMAND(paused, "iN$", (int *val, int *numargs, ident *id), + { + if(*numargs > 0) pausegame(clampvar(id, *val, 0, 1) > 0); + else if(*numargs < 0) intret(gamepaused ? 1 : 0); + else printvar(id, gamepaused ? 1 : 0); + }); + + bool ispaused() { return gamepaused; } + + bool allowmouselook() { return !gamepaused || !remote || m_edit; } + + void changegamespeed(int val) + { + if(!connected) return; + if(!remote) server::forcegamespeed(val); + else addmsg(N_GAMESPEED, "ri", val); + } + ICOMMAND(gamespeed, "iN$", (int *val, int *numargs, ident *id), + { + if(*numargs > 0) changegamespeed(clampvar(id, *val, 10, 1000)); + else if(*numargs < 0) intret(gamespeed); + else printvar(id, gamespeed); + }); + + int scaletime(int t) { return t*gamespeed; } + + // collect c2s messages conveniently + vector<uchar> messages; + int messagecn = -1, messagereliable = false; + + bool addmsg(int type, const char *fmt, ...) + { + if(!connected) return false; + static uchar buf[MAXTRANS]; + ucharbuf p(buf, sizeof(buf)); + putint(p, type); + int numi = 1, numf = 0, nums = 0, mcn = -1; + bool reliable = false; + if(fmt) + { + va_list args; + va_start(args, fmt); + while(*fmt) switch(*fmt++) + { + case 'r': reliable = true; break; + case 'c': + { + fpsent *d = va_arg(args, fpsent *); + mcn = !d || d == player1 ? -1 : d->clientnum; + break; + } + case 'v': + { + int n = va_arg(args, int); + int *v = va_arg(args, int *); + loopi(n) putint(p, v[i]); + numi += n; + break; + } + + case 'i': + { + int n = isdigit(*fmt) ? *fmt++-'0' : 1; + loopi(n) putint(p, va_arg(args, int)); + numi += n; + break; + } + case 'f': + { + int n = isdigit(*fmt) ? *fmt++-'0' : 1; + loopi(n) putfloat(p, (float)va_arg(args, double)); + numf += n; + break; + } + case 's': sendstring(va_arg(args, const char *), p); nums++; break; + } + va_end(args); + } + int num = nums || numf ? 0 : numi, msgsize = server::msgsizelookup(type); + if(msgsize && num!=msgsize) { fatal("inconsistent msg size for %d (%d != %d)", type, num, msgsize); } + if(reliable) messagereliable = true; + if(mcn != messagecn) + { + static uchar mbuf[16]; + ucharbuf m(mbuf, sizeof(mbuf)); + putint(m, N_FROMAI); + putint(m, mcn); + messages.put(mbuf, m.length()); + messagecn = mcn; + } + messages.put(buf, p.length()); + return true; + } + + void connectattempt(const char *name, const char *password, const ENetAddress &address) + { + copystring(connectpass, password); + } + + void connectfail() + { + memset(connectpass, 0, sizeof(connectpass)); + } + + void gameconnect(bool _remote) + { + remote = _remote; + if(editmode) toggleedit(); + } + + void gamedisconnect(bool cleanup) + { + if(remote) stopfollowing(); + ignores.setsize(0); + connected = remote = false; + player1->clientnum = -1; + sessionid = 0; + mastermode = MM_OPEN; + messages.setsize(0); + messagereliable = false; + messagecn = -1; + player1->respawn(); + player1->lifesequence = 0; + player1->state = CS_ALIVE; + player1->privilege = PRIV_NONE; + sendcrc = senditemstoserver = false; + demoplayback = false; + gamepaused = false; + gamespeed = 100; + clearclients(false); + if(cleanup) + { + nextmode = gamemode = INT_MAX; + clientmap[0] = '\0'; + } + } + + VARP(teamcolorchat, 0, 1, 1); + const char *chatcolorname(fpsent *d) { return teamcolorchat ? teamcolorname(d, NULL) : colorname(d); } + + void toserver(char *text) { conoutf(CON_CHAT, "%s:\f0 %s", chatcolorname(player1), text); addmsg(N_TEXT, "rcs", player1, text); } + COMMANDN(say, toserver, "C"); + + void sayteam(char *text) { conoutf(CON_TEAMCHAT, "\fs\f8[team]\fr %s: \f8%s", chatcolorname(player1), text); addmsg(N_SAYTEAM, "rcs", player1, text); } + COMMAND(sayteam, "C"); + + ICOMMAND(servcmd, "C", (char *cmd), addmsg(N_SERVCMD, "rs", cmd)); + + static void sendposition(fpsent *d, packetbuf &q) + { + putint(q, N_POS); + putuint(q, d->clientnum); + // 3 bits phys state, 1 bit life sequence, 2 bits move, 2 bits strafe + uchar physstate = d->physstate | ((d->lifesequence&1)<<3) | ((d->move&3)<<4) | ((d->strafe&3)<<6); + q.put(physstate); + ivec o = ivec(vec(d->o.x, d->o.y, d->o.z-d->eyeheight).mul(DMF)); + uint vel = min(int(d->vel.magnitude()*DVELF), 0xFFFF), fall = min(int(d->falling.magnitude()*DVELF), 0xFFFF); + // 3 bits position, 1 bit velocity, 3 bits falling, 1 bit material + uint flags = 0; + if(o.x < 0 || o.x > 0xFFFF) flags |= 1<<0; + if(o.y < 0 || o.y > 0xFFFF) flags |= 1<<1; + if(o.z < 0 || o.z > 0xFFFF) flags |= 1<<2; + if(vel > 0xFF) flags |= 1<<3; + if(fall > 0) + { + flags |= 1<<4; + if(fall > 0xFF) flags |= 1<<5; + if(d->falling.x || d->falling.y || d->falling.z > 0) flags |= 1<<6; + } + if((lookupmaterial(d->feetpos())&MATF_CLIP) == MAT_GAMECLIP) flags |= 1<<7; + putuint(q, flags); + loopk(3) + { + q.put(o[k]&0xFF); + q.put((o[k]>>8)&0xFF); + if(o[k] < 0 || o[k] > 0xFFFF) q.put((o[k]>>16)&0xFF); + } + uint dir = (d->yaw < 0 ? 360 + int(d->yaw)%360 : int(d->yaw)%360) + clamp(int(d->pitch+90), 0, 180)*360; + q.put(dir&0xFF); + q.put((dir>>8)&0xFF); + q.put(clamp(int(d->roll+90), 0, 180)); + q.put(vel&0xFF); + if(vel > 0xFF) q.put((vel>>8)&0xFF); + float velyaw, velpitch; + vectoyawpitch(d->vel, velyaw, velpitch); + uint veldir = (velyaw < 0 ? 360 + int(velyaw)%360 : int(velyaw)%360) + clamp(int(velpitch+90), 0, 180)*360; + q.put(veldir&0xFF); + q.put((veldir>>8)&0xFF); + if(fall > 0) + { + q.put(fall&0xFF); + if(fall > 0xFF) q.put((fall>>8)&0xFF); + if(d->falling.x || d->falling.y || d->falling.z > 0) + { + float fallyaw, fallpitch; + vectoyawpitch(d->falling, fallyaw, fallpitch); + uint falldir = (fallyaw < 0 ? 360 + int(fallyaw)%360 : int(fallyaw)%360) + clamp(int(fallpitch+90), 0, 180)*360; + q.put(falldir&0xFF); + q.put((falldir>>8)&0xFF); + } + } + } + + void sendposition(fpsent *d, bool reliable) + { + if(d->state != CS_ALIVE && d->state != CS_EDITING) return; + packetbuf q(100, reliable ? ENET_PACKET_FLAG_RELIABLE : 0); + sendposition(d, q); + sendclientpacket(q.finalize(), 0); + } + + void sendpositions() + { + loopv(players) + { + fpsent *d = players[i]; + if((d == player1 || d->ai) && (d->state == CS_ALIVE || d->state == CS_EDITING)) + { + packetbuf q(100); + sendposition(d, q); + for(int j = i+1; j < players.length(); j++) + { + fpsent *d = players[j]; + if((d == player1 || d->ai) && (d->state == CS_ALIVE || d->state == CS_EDITING)) + sendposition(d, q); + } + sendclientpacket(q.finalize(), 0); + break; + } + } + } + + void sendmessages() + { + packetbuf p(MAXTRANS); + if(sendcrc) + { + p.reliable(); + sendcrc = false; + const char *mname = getclientmap(); + putint(p, N_MAPCRC); + sendstring(mname, p); + putint(p, mname[0] ? getmapcrc() : 0); + } + if(senditemstoserver) + { + if(!m_noitems) p.reliable(); + if(!m_noitems) entities::putitems(p); + senditemstoserver = false; + } + if(messages.length()) + { + p.put(messages.getbuf(), messages.length()); + messages.setsize(0); + if(messagereliable) p.reliable(); + messagereliable = false; + messagecn = -1; + } + if(totalmillis-lastping>250) + { + putint(p, N_PING); + putint(p, totalmillis); + lastping = totalmillis; + } + sendclientpacket(p.finalize(), 1); + } + + void c2sinfo(bool force) // send update to the server + { + static int lastupdate = -1000; + if(totalmillis - lastupdate < 33 && !force) return; // don't update faster than 30fps + lastupdate = totalmillis; + sendpositions(); + sendmessages(); + flushclient(); + } + + void sendintro() + { + packetbuf p(MAXTRANS, ENET_PACKET_FLAG_RELIABLE); + putint(p, N_CONNECT); + sendstring(player1->name, p); + putint(p, player1->playermodel); + string hash = ""; + if(connectpass[0]) + { + server::hashpassword(player1->clientnum, sessionid, connectpass, hash); + memset(connectpass, 0, sizeof(connectpass)); + } + sendstring(hash, p); + authkey *a = servauth[0] && autoauth ? findauthkey(servauth) : NULL; + if(a) + { + a->lastauth = lastmillis; + sendstring(a->desc, p); + sendstring(a->name, p); + } + else + { + sendstring("", p); + sendstring("", p); + } + sendclientpacket(p.finalize(), 1); + } + + void updatepos(fpsent *d) + { + // update the position of other clients in the game in our world + // don't care if he's in the scenery or other players, + // just don't overlap with our client + + const float r = player1->radius+d->radius; + const float dx = player1->o.x-d->o.x; + const float dy = player1->o.y-d->o.y; + const float dz = player1->o.z-d->o.z; + const float rz = player1->aboveeye+d->eyeheight; + const float fx = (float)fabs(dx), fy = (float)fabs(dy), fz = (float)fabs(dz); + if(fx<r && fy<r && fz<rz && player1->state!=CS_SPECTATOR && d->state!=CS_DEAD) + { + if(fx<fy) d->o.y += dy<0 ? r-fy : -(r-fy); // push aside + else d->o.x += dx<0 ? r-fx : -(r-fx); + } + int lagtime = totalmillis-d->lastupdate; + if(lagtime) + { + if(d->state!=CS_SPAWNING && d->lastupdate) d->plag = (d->plag*5+lagtime)/6; + d->lastupdate = totalmillis; + } + } + + void parsepositions(ucharbuf &p) + { + int type; + while(p.remaining()) switch(type = getint(p)) + { + case N_DEMOPACKET: break; + case N_POS: // position of another client + { + int cn = getuint(p), physstate = p.get(), flags = getuint(p); + vec o, vel, falling; + float yaw, pitch, roll; + loopk(3) + { + int n = p.get(); n |= p.get()<<8; if(flags&(1<<k)) { n |= p.get()<<16; if(n&0x800000) n |= ~0U<<24; } + o[k] = n/DMF; + } + int dir = p.get(); dir |= p.get()<<8; + yaw = dir%360; + pitch = clamp(dir/360, 0, 180)-90; + roll = clamp(int(p.get()), 0, 180)-90; + int mag = p.get(); if(flags&(1<<3)) mag |= p.get()<<8; + dir = p.get(); dir |= p.get()<<8; + vecfromyawpitch(dir%360, clamp(dir/360, 0, 180)-90, 1, 0, vel); + vel.mul(mag/DVELF); + if(flags&(1<<4)) + { + mag = p.get(); if(flags&(1<<5)) mag |= p.get()<<8; + if(flags&(1<<6)) + { + dir = p.get(); dir |= p.get()<<8; + vecfromyawpitch(dir%360, clamp(dir/360, 0, 180)-90, 1, 0, falling); + } + else falling = vec(0, 0, -1); + falling.mul(mag/DVELF); + } + else falling = vec(0, 0, 0); + int seqcolor = (physstate>>3)&1; + fpsent *d = getclient(cn); + if(!d || d->lifesequence < 0 || seqcolor!=(d->lifesequence&1) || d->state==CS_DEAD) continue; + float oldyaw = d->yaw, oldpitch = d->pitch, oldroll = d->roll; + d->yaw = yaw; + d->pitch = pitch; + d->roll = roll; + d->move = (physstate>>4)&2 ? -1 : (physstate>>4)&1; + d->strafe = (physstate>>6)&2 ? -1 : (physstate>>6)&1; + vec oldpos(d->o); + d->o = o; + d->o.z += d->eyeheight; + d->vel = vel; + d->falling = falling; + d->physstate = physstate&7; + updatephysstate(d); + updatepos(d); + if(smoothmove && d->smoothmillis>=0 && oldpos.dist(d->o) < smoothdist) + { + d->newpos = d->o; + d->newyaw = d->yaw; + d->newpitch = d->pitch; + d->newroll = d->roll; + d->o = oldpos; + d->yaw = oldyaw; + d->pitch = oldpitch; + d->roll = oldroll; + (d->deltapos = oldpos).sub(d->newpos); + d->deltayaw = oldyaw - d->newyaw; + if(d->deltayaw > 180) d->deltayaw -= 360; + else if(d->deltayaw < -180) d->deltayaw += 360; + d->deltapitch = oldpitch - d->newpitch; + d->deltaroll = oldroll - d->newroll; + d->smoothmillis = lastmillis; + } + else d->smoothmillis = 0; + if(d->state==CS_LAGGED || d->state==CS_SPAWNING) d->state = CS_ALIVE; + break; + } + + case N_TELEPORT: + { + int cn = getint(p), tp = getint(p), td = getint(p); + fpsent *d = getclient(cn); + if(!d || d->lifesequence < 0 || d->state==CS_DEAD) continue; + entities::teleporteffects(d, tp, td, false); + break; + } + + case N_JUMPPAD: + { + int cn = getint(p), jp = getint(p); + fpsent *d = getclient(cn); + if(!d || d->lifesequence < 0 || d->state==CS_DEAD) continue; + entities::jumppadeffects(d, jp, false); + break; + } + + default: + neterr("type"); + return; + } + } + + void parsestate(fpsent *d, ucharbuf &p, bool resume = false) + { + if(!d) { static fpsent dummy; d = &dummy; } + if(resume) + { + if(d==player1) getint(p); + else d->state = getint(p); + d->frags = getint(p); + d->flags = getint(p); + d->deaths = getint(p); + if(d==player1) getint(p); + else d->quadmillis = getint(p); + } + d->lifesequence = getint(p); + d->health = getint(p); + d->maxhealth = getint(p); + d->armour = getint(p); + d->maxarmour = getint(p); + d->armourtype = getint(p); + if(resume && d==player1) + { + getint(p); + loopi(GUN_PISTOL-GUN_SG+1) getint(p); + } + else + { + int gun = getint(p); + d->gunselect = clamp(gun, int(GUN_FIST), int(GUN_PISTOL)); + loopi(GUN_PISTOL-GUN_SG+1) d->ammo[GUN_SG+i] = getint(p); + } + } + + extern int deathscore; + + void parsemessages(int cn, fpsent *d, ucharbuf &p) + { + static char text[MAXTRANS]; + int type; + bool mapchanged = false, demopacket = false; + + while(p.remaining()) switch(type = getint(p)) + { + case N_DEMOPACKET: demopacket = true; break; + + case N_SERVINFO: // welcome messsage from the server + { + int mycn = getint(p), prot = getint(p); + if(prot!=PROTOCOL_VERSION) + { + conoutf(CON_ERROR, "you are using a different game protocol (you: %d, server: %d)", PROTOCOL_VERSION, prot); + disconnect(); + return; + } + sessionid = getint(p); + player1->clientnum = mycn; // we are now connected + if(getint(p) > 0) conoutf("this server is password protected"); + getstring(servinfo, p, sizeof(servinfo)); + getstring(servauth, p, sizeof(servauth)); + sendintro(); + break; + } + + case N_WELCOME: + { + connected = true; + notifywelcome(); + break; + } + + case N_PAUSEGAME: + { + bool val = getint(p) > 0; + int cn = getint(p); + fpsent *a = cn >= 0 ? getclient(cn) : NULL; + if(!demopacket) + { + gamepaused = val; + player1->attacking = false; + } + if(a) conoutf("%s %s the game", colorname(a), val ? "paused" : "resumed"); + else conoutf("game is %s", val ? "paused" : "resumed"); + break; + } + + case N_GAMESPEED: + { + int val = clamp(getint(p), 10, 1000), cn = getint(p); + fpsent *a = cn >= 0 ? getclient(cn) : NULL; + if(!demopacket) gamespeed = val; + if(a) conoutf("%s set gamespeed to %d", colorname(a), val); + else conoutf("gamespeed is %d", val); + break; + } + + case N_CLIENT: + { + int cn = getint(p), len = getuint(p); + ucharbuf q = p.subbuf(len); + parsemessages(cn, getclient(cn), q); + break; + } + + case N_SOUND: + if(!d) return; + playsound(getint(p), &d->o); + break; + + case N_TEXT: + { + if(!d) return; + getstring(text, p); + filtertext(text, text, true, true); + if(isignored(d->clientnum)) break; + if(d->state!=CS_DEAD && d->state!=CS_SPECTATOR) + particle_textcopy(d->abovehead(), text, PART_TEXT, 2000, 0x32FF64, 4.0f, -8); + conoutf(CON_CHAT, "%s:\f0 %s", chatcolorname(d), text); + break; + } + + case N_SAYTEAM: + { + int tcn = getint(p); + fpsent *t = getclient(tcn); + getstring(text, p); + filtertext(text, text, true, true); + if(!t || isignored(t->clientnum)) break; + if(t->state!=CS_DEAD && t->state!=CS_SPECTATOR) + particle_textcopy(t->abovehead(), text, PART_TEXT, 2000, 0x6496FF, 4.0f, -8); + conoutf(CON_TEAMCHAT, "\fs\f8[team]\fr %s: \f8%s", chatcolorname(t), text); + break; + } + + case N_MAPCHANGE: + getstring(text, p); + filtertext(text, text, false); + fixmapname(text); + changemapserv(text, getint(p)); + mapchanged = true; + if(getint(p)) entities::spawnitems(); + else senditemstoserver = false; + break; + + case N_FORCEDEATH: + { + int cn = getint(p); + fpsent *d = cn==player1->clientnum ? player1 : newclient(cn); + if(!d) break; + if(d==player1) + { + if(editmode) toggleedit(); + stopfollowing(); + if(deathscore) showscores(true); + } + else d->resetinterp(); + d->state = CS_DEAD; + break; + } + + case N_ITEMLIST: + { + int n; + while((n = getint(p))>=0 && !p.overread()) + { + if(mapchanged) entities::setspawn(n, true); + getint(p); // type + } + break; + } + + case N_INITCLIENT: // another client either connected or changed name/team + { + int cn = getint(p); + fpsent *d = newclient(cn); + if(!d) + { + getstring(text, p); + getstring(text, p); + getint(p); + break; + } + getstring(text, p); + filtertext(text, text, false, false, MAXNAMELEN); + if(!text[0]) copystring(text, "Anonymous"); + if(d->name[0]) // already connected + { + if(strcmp(d->name, text) && !isignored(d->clientnum)) + conoutf("%s is now known as %s", colorname(d), colorname(d, text)); + } + else // new client + { + conoutf("\f0join:\f7 %s", colorname(d, text)); + if(needclipboard >= 0) needclipboard++; + } + copystring(d->name, text, MAXNAMELEN+1); + getstring(text, p); + filtertext(d->team, text, false, false, MAXTEAMLEN); + d->playermodel = getint(p); + d->playermodel = 0; + break; + } + + case N_SWITCHNAME: + getstring(text, p); + if(d) + { + filtertext(text, text, false, false, MAXNAMELEN); + if(!text[0]) copystring(text, "Anonymous"); + if(strcmp(text, d->name)) + { + if(!isignored(d->clientnum)) conoutf("%s is now known as %s", colorname(d), colorname(d, text)); + copystring(d->name, text, MAXNAMELEN+1); + } + } + break; + + case N_SWITCHMODEL: + break; + + case N_CDIS: + clientdisconnected(getint(p)); + break; + + case N_SPAWN: + { + if(d) + { + if(d->state==CS_DEAD && d->lastpain) saveragdoll(d); + d->respawn(); + } + parsestate(d, p); + if(!d) break; + d->state = CS_SPAWNING; + if(player1->state==CS_SPECTATOR && following==d->clientnum) + lasthit = 0; + break; + } + + case N_SPAWNSTATE: + { + int scn = getint(p); + fpsent *s = getclient(scn); + if(!s) { parsestate(NULL, p); break; } + if(s->state==CS_DEAD && s->lastpain) saveragdoll(s); + if(s==player1) + { + if(editmode) toggleedit(); + stopfollowing(); + } + s->respawn(); + parsestate(s, p); + s->state = CS_ALIVE; + pickgamespawn(s); + if(s == player1) + { + showscores(false); + lasthit = 0; + } + ai::spawned(s); + addmsg(N_SPAWN, "rcii", s, s->lifesequence, s->gunselect); + break; + } + + case N_SHOTFX: + { + int scn = getint(p), gun = getint(p), id = getint(p); + vec from, to; + loopk(3) from[k] = getint(p)/DMF; + loopk(3) to[k] = getint(p)/DMF; + fpsent *s = getclient(scn); + if(!s) break; + if(gun>GUN_FIST && gun<=GUN_PISTOL && s->ammo[gun]) s->ammo[gun]--; + s->gunselect = clamp(gun, (int)GUN_FIST, (int)GUN_PISTOL); + s->gunwait = guns[s->gunselect].attackdelay; + int prevaction = s->lastaction; + s->lastaction = lastmillis; + s->lastattackgun = s->gunselect; + shoteffects(s->gunselect, from, to, s, false, id, prevaction); + break; + } + + case N_EXPLODEFX: + { + int ecn = getint(p), gun = getint(p), id = getint(p); + fpsent *e = getclient(ecn); + if(!e) break; + explodeeffects(gun, e, false, id); + break; + } + case N_DAMAGE: + { + int tcn = getint(p), + acn = getint(p), + damage = getint(p), + armour = getint(p), + health = getint(p); + fpsent *target = getclient(tcn), + *actor = getclient(acn); + if(!target || !actor) break; + target->armour = armour; + target->health = health; + if(target->state == CS_ALIVE && actor != player1) target->lastpain = lastmillis; + damaged(damage, target, actor, false); + break; + } + + case N_HITPUSH: + { + int tcn = getint(p), gun = getint(p), damage = getint(p); + fpsent *target = getclient(tcn); + vec dir; + loopk(3) dir[k] = getint(p)/DNF; + if(target) target->hitpush(damage * (target->health<=0 ? deadpush : 1), dir, NULL, gun); + break; + } + + case N_DIED: + { + int vcn = getint(p), acn = getint(p), frags = getint(p), tfrags = getint(p); + fpsent *victim = getclient(vcn), + *actor = getclient(acn); + if(!actor) break; + actor->frags = frags; + if(m_teammode) setteaminfo(actor->team, tfrags); + extern int hidefrags; + if(actor!=player1 && (!hidefrags)) + { + defformatstring(ds, "%d", actor->frags); + particle_textcopy(actor->abovehead(), ds, PART_TEXT, 2000, 0x32FF64, 4.0f, -8); + } + if(!victim) break; + killed(victim, actor); + break; + } + + case N_TEAMINFO: + for(;;) + { + getstring(text, p); + if(p.overread() || !text[0]) break; + int frags = getint(p); + if(p.overread()) break; + if(m_teammode) setteaminfo(text, frags); + } + break; + + case N_GUNSELECT: + { + if(!d) return; + int gun = getint(p); + d->gunselect = clamp(gun, int(GUN_FIST), int(GUN_PISTOL)); + playsound(S_WEAPLOAD, &d->o); + break; + } + + case N_TAUNT: + { + if(!d) return; + d->lasttaunt = lastmillis; + break; + } + + case N_RESUME: + { + for(;;) + { + int cn = getint(p); + if(p.overread() || cn<0) break; + fpsent *d = (cn == player1->clientnum ? player1 : newclient(cn)); + parsestate(d, p, true); + } + break; + } + + case N_ITEMSPAWN: + { + int i = getint(p); + if(!entities::ents.inrange(i)) break; + entities::setspawn(i, true); + ai::itemspawned(i); + playsound(S_ITEMSPAWN, &entities::ents[i]->o, NULL, 0, 0, 0, -1, 0, 1500); + #if 0 + const char *name = entities::itemname(i); + if(name) particle_text(entities::ents[i]->o, name, PART_TEXT, 2000, 0x32FF64, 4.0f, -8); + #endif + int icon = entities::itemicon(i); + if(icon >= 0) particle_icon(vec(0.0f, 0.0f, 4.0f).add(entities::ents[i]->o), icon%4, icon/4, PART_HUD_ICON, 2000, 0xFFFFFF, 2.0f, -8); + break; + } + + case N_ITEMACC: // server acknowledges that I picked up this item + { + int i = getint(p), cn = getint(p); + if(cn >= 0) + { + fpsent *d = getclient(cn); + entities::pickupeffects(i, d); + } + else entities::setspawn(i, true); + break; + } + + case N_CLIPBOARD: + { + int cn = getint(p), unpacklen = getint(p), packlen = getint(p); + fpsent *d = getclient(cn); + ucharbuf q = p.subbuf(max(packlen, 0)); + if(d) unpackeditinfo(d->edit, q.buf, q.maxlen, unpacklen); + break; + } + case N_UNDO: + case N_REDO: + { + int cn = getint(p), unpacklen = getint(p), packlen = getint(p); + fpsent *d = getclient(cn); + ucharbuf q = p.subbuf(max(packlen, 0)); + if(d) unpackundo(q.buf, q.maxlen, unpacklen); + break; + } + + case N_EDITF: // coop editing messages + case N_EDITT: + case N_EDITM: + case N_FLIP: + case N_COPY: + case N_PASTE: + case N_ROTATE: + case N_REPLACE: + case N_DELCUBE: + case N_EDITVSLOT: + { + if(!d) return; + selinfo sel; + sel.o.x = getint(p); sel.o.y = getint(p); sel.o.z = getint(p); + sel.s.x = getint(p); sel.s.y = getint(p); sel.s.z = getint(p); + sel.grid = getint(p); sel.orient = getint(p); + sel.cx = getint(p); sel.cxs = getint(p); sel.cy = getint(p), sel.cys = getint(p); + sel.corner = getint(p); + switch(type) + { + case N_EDITF: { int dir = getint(p), mode = getint(p); if(sel.validate()) mpeditface(dir, mode, sel, false); break; } + case N_EDITT: + { + int tex = getint(p), + allfaces = getint(p); + if(p.remaining() < 2) return; + int extra = lilswap(*(const ushort *)p.pad(2)); + if(p.remaining() < extra) return; + ucharbuf ebuf = p.subbuf(extra); + if(sel.validate()) mpedittex(tex, allfaces, sel, ebuf); + break; + } + case N_EDITM: { int mat = getint(p), filter = getint(p); if(sel.validate()) mpeditmat(mat, filter, sel, false); break; } + case N_FLIP: if(sel.validate()) mpflip(sel, false); break; + case N_COPY: if(d && sel.validate()) mpcopy(d->edit, sel, false); break; + case N_PASTE: if(d && sel.validate()) mppaste(d->edit, sel, false); break; + case N_ROTATE: { int dir = getint(p); if(sel.validate()) mprotate(dir, sel, false); break; } + case N_REPLACE: + { + int oldtex = getint(p), + newtex = getint(p), + insel = getint(p); + if(p.remaining() < 2) return; + int extra = lilswap(*(const ushort *)p.pad(2)); + if(p.remaining() < extra) return; + ucharbuf ebuf = p.subbuf(extra); + if(sel.validate()) mpreplacetex(oldtex, newtex, insel>0, sel, ebuf); + break; + } + case N_DELCUBE: if(sel.validate()) mpdelcube(sel, false); break; + case N_EDITVSLOT: + { + int delta = getint(p), + allfaces = getint(p); + if(p.remaining() < 2) return; + int extra = lilswap(*(const ushort *)p.pad(2)); + if(p.remaining() < extra) return; + ucharbuf ebuf = p.subbuf(extra); + if(sel.validate()) mpeditvslot(delta, allfaces, sel, ebuf); + break; + } + } + break; + } + case N_REMIP: + { + if(!d) return; + conoutf("%s remipped", colorname(d)); + mpremip(false); + break; + } + case N_EDITENT: // coop edit of ent + { + if(!d) return; + int i = getint(p); + float x = getint(p)/DMF, y = getint(p)/DMF, z = getint(p)/DMF; + int type = getint(p); + int attr1 = getint(p), attr2 = getint(p), attr3 = getint(p), attr4 = getint(p), attr5 = getint(p); + + mpeditent(i, vec(x, y, z), type, attr1, attr2, attr3, attr4, attr5, false); + break; + } + case N_EDITVAR: + { + if(!d) return; + int type = getint(p); + getstring(text, p); + string name; + filtertext(name, text, false); + ident *id = getident(name); + switch(type) + { + case ID_VAR: + { + int val = getint(p); + if(id && id->flags&IDF_OVERRIDE && !(id->flags&IDF_READONLY)) setvar(name, val); + break; + } + case ID_FVAR: + { + float val = getfloat(p); + if(id && id->flags&IDF_OVERRIDE && !(id->flags&IDF_READONLY)) setfvar(name, val); + break; + } + case ID_SVAR: + { + getstring(text, p); + if(id && id->flags&IDF_OVERRIDE && !(id->flags&IDF_READONLY)) setsvar(name, text); + break; + } + } + printvar(d, id); + break; + } + + case N_PONG: + addmsg(N_CLIENTPING, "i", player1->ping = (player1->ping*5+totalmillis-getint(p))/6); + break; + + case N_CLIENTPING: + if(!d) return; + d->ping = getint(p); + break; + + case N_TIMEUP: + timeupdate(getint(p)); + break; + + case N_SERVMSG: + getstring(text, p); + conoutf("%s", text); + break; + + case N_SENDDEMOLIST: + { + int demos = getint(p); + if(demos <= 0) conoutf("no demos available"); + else loopi(demos) + { + getstring(text, p); + if(p.overread()) break; + conoutf("%d. %s", i+1, text); + } + break; + } + + case N_DEMOPLAYBACK: + { + int on = getint(p); + if(on) player1->state = CS_SPECTATOR; + else clearclients(); + demoplayback = on!=0; + player1->clientnum = getint(p); + gamepaused = false; + execident(on ? "demostart" : "demoend"); + break; + } + + case N_CURRENTMASTER: + { + int mm = getint(p), mn; + loopv(players) players[i]->privilege = PRIV_NONE; + while((mn = getint(p))>=0 && !p.overread()) + { + fpsent *m = mn==player1->clientnum ? player1 : newclient(mn); + int priv = getint(p); + if(m) m->privilege = priv; + } + if(mm != mastermode) + { + mastermode = mm; + conoutf("mastermode is %s (%d)", server::mastermodename(mastermode), mastermode); + } + break; + } + + case N_MASTERMODE: + { + mastermode = getint(p); + conoutf("mastermode is %s (%d)", server::mastermodename(mastermode), mastermode); + break; + } + + case N_EDITMODE: + { + int val = getint(p); + if(!d) break; + if(val) + { + d->editstate = d->state; + d->state = CS_EDITING; + } + else + { + d->state = d->editstate; + if(d->state==CS_DEAD) deathstate(d, true); + } + break; + } + + case N_SPECTATOR: + { + int sn = getint(p), val = getint(p); + fpsent *s; + if(sn==player1->clientnum) + { + s = player1; + if(val && remote && !player1->privilege) senditemstoserver = false; + } + else s = newclient(sn); + if(!s) return; + if(val) + { + if(s==player1) + { + if(editmode) toggleedit(); + if(s->state==CS_DEAD) showscores(false); + disablezoom(); + } + s->state = CS_SPECTATOR; + } + else if(s->state==CS_SPECTATOR) + { + if(s==player1) stopfollowing(); + deathstate(s, true); + } + break; + } + + case N_SETTEAM: + { + int wn = getint(p); + getstring(text, p); + int reason = getint(p); + fpsent *w = getclient(wn); + if(!w) return; + filtertext(w->team, text, false, false, MAXTEAMLEN); + static const char * const fmt[2] = { "%s switched to team %s", "%s forced to team %s"}; + if(reason >= 0 && size_t(reason) < sizeof(fmt)/sizeof(fmt[0])) + conoutf(fmt[reason], colorname(w), w->team); + break; + } + + case N_ANNOUNCE: + { + int t = getint(p); + if (t==I_QUAD) { playsound(S_V_QUAD10, NULL, NULL, 0, 0, 0, -1, 0, 3000); conoutf(CON_GAMEINFO, "\f2quad damage will spawn in 10 seconds!"); } + else if(t==I_BOOST) { playsound(S_V_BOOST10, NULL, NULL, 0, 0, 0, -1, 0, 3000); conoutf(CON_GAMEINFO, "\f2health boost will spawn in 10 seconds!"); } + break; + } + + case N_NEWMAP: + { + int size = getint(p); + if(size>=0) emptymap(size, true, NULL); + else enlargemap(true); + if(d && d!=player1) + { + int newsize = 0; + while(1<<newsize < getworldsize()) newsize++; + conoutf(size>=0 ? "%s started a new map of size %d" : "%s enlarged the map to size %d", colorname(d), newsize); + } + break; + } + + case N_REQAUTH: + { + getstring(text, p); + if(autoauth && text[0] && tryauth(text)) conoutf("server requested authkey \"%s\"", text); + break; + } + + case N_AUTHCHAL: + { + getstring(text, p); + authkey *a = findauthkey(text); + uint id = (uint)getint(p); + getstring(text, p); + if(a && a->lastauth && lastmillis - a->lastauth < 60*1000) + { + vector<char> buf; + answerchallenge(a->key, text, buf); + //conoutf(CON_DEBUG, "answering %u, challenge %s with %s", id, text, buf.getbuf()); + packetbuf p(MAXTRANS, ENET_PACKET_FLAG_RELIABLE); + putint(p, N_AUTHANS); + sendstring(a->desc, p); + putint(p, id); + sendstring(buf.getbuf(), p); + sendclientpacket(p.finalize(), 1); + } + break; + } + + case N_INITAI: + { + int bn = getint(p), on = getint(p), at = getint(p), sk = clamp(getint(p), 1, 101), pm = getint(p); + string name, team; + getstring(text, p); + filtertext(name, text, false, false, MAXNAMELEN); + getstring(text, p); + filtertext(team, text, false, false, MAXTEAMLEN); + fpsent *b = newclient(bn); + if(!b) break; + ai::init(b, at, on, sk, bn, pm, name, team); + break; + } + + case N_SERVCMD: + getstring(text, p); + break; + + default: + neterr("type", cn < 0); + return; + } + } + + struct demoreq + { + int tag; + string name; + }; + vector<demoreq> demoreqs; + enum { MAXDEMOREQS = 7 }; + static int lastdemoreq = 0; + + void receivefile(packetbuf &p) + { + int type; + while(p.remaining()) switch(type = getint(p)) + { + case N_DEMOPACKET: return; + case N_SENDDEMO: + { + string fname; + fname[0] = '\0'; + int tag = getint(p); + loopv(demoreqs) if(demoreqs[i].tag == tag) + { + copystring(fname, demoreqs[i].name); + demoreqs.remove(i); + break; + } + if(!fname[0]) + { + time_t t = time(NULL); + size_t len = strftime(fname, sizeof(fname), "%Y-%m-%d_%H.%M.%S", localtime(&t)); + fname[min(len, sizeof(fname)-1)] = '\0'; + } + int len = strlen(fname); + if(len < 4 || strcasecmp(&fname[len-4], ".dmo")) concatstring(fname, ".dmo"); + stream *demo = NULL; + if(const char *buf = server::getdemofile(fname, true)) demo = openrawfile(buf, "wb"); + if(!demo) demo = openrawfile(fname, "wb"); + if(!demo) return; + conoutf("received demo \"%s\"", fname); + ucharbuf b = p.subbuf(p.remaining()); + demo->write(b.buf, b.maxlen); + delete demo; + break; + } + + case N_SENDMAP: + { + if(!m_edit) return; + string oldname; + copystring(oldname, getclientmap()); + defformatstring(mname, "getmap_%d", lastmillis); + defformatstring(fname, "packages/maps/%s.ogz", mname); + stream *map = openrawfile(path(fname), "wb"); + if(!map) return; + conoutf("received map"); + ucharbuf b = p.subbuf(p.remaining()); + map->write(b.buf, b.maxlen); + delete map; + if(load_world(mname, oldname[0] ? oldname : NULL)) + entities::spawnitems(true); + remove(findfile(fname, "rb")); + break; + } + } + } + + void parsepacketclient(int chan, packetbuf &p) // processes any updates from the server + { + if(p.packet->flags&ENET_PACKET_FLAG_UNSEQUENCED) return; + switch(chan) + { + case 0: + parsepositions(p); + break; + + case 1: + parsemessages(-1, NULL, p); + break; + + case 2: + receivefile(p); + break; + } + } + + void getmap() + { + if(!m_edit) { conoutf(CON_ERROR, "\"getmap\" only works in coop edit mode"); return; } + conoutf("getting map..."); + addmsg(N_GETMAP, "r"); + } + COMMAND(getmap, ""); + + void stopdemo() + { + if(remote) + { + if(player1->privilege<PRIV_MASTER) return; + addmsg(N_STOPDEMO, "r"); + } + else server::stopdemo(); + } + COMMAND(stopdemo, ""); + + void recorddemo(int val) + { + if(remote && player1->privilege<PRIV_MASTER) return; + addmsg(N_RECORDDEMO, "ri", val); + } + ICOMMAND(recorddemo, "i", (int *val), recorddemo(*val)); + + void cleardemos(int val) + { + if(remote && player1->privilege<PRIV_MASTER) return; + addmsg(N_CLEARDEMOS, "ri", val); + } + ICOMMAND(cleardemos, "i", (int *val), cleardemos(*val)); + + void getdemo(char *val, char *name) + { + int i = 0; + if(isdigit(val[0]) || name[0]) i = parseint(val); + else name = val; + if(i<=0) conoutf("getting demo..."); + else conoutf("getting demo %d...", i); + ++lastdemoreq; + if(name[0]) + { + if(demoreqs.length() >= MAXDEMOREQS) demoreqs.remove(0); + demoreq &r = demoreqs.add(); + r.tag = lastdemoreq; + copystring(r.name, name); + } + addmsg(N_GETDEMO, "rii", i, lastdemoreq); + } + ICOMMAND(getdemo, "ss", (char *val, char *name), getdemo(val, name)); + + void listdemos() + { + conoutf("listing demos..."); + addmsg(N_LISTDEMOS, "r"); + } + COMMAND(listdemos, ""); + + void sendmap() + { + if(!m_edit || (player1->state==CS_SPECTATOR && remote && !player1->privilege)) { conoutf(CON_ERROR, "\"sendmap\" only works in coop edit mode"); return; } + conoutf("sending map..."); + defformatstring(mname, "sendmap_%d", lastmillis); + save_world(mname, true); + defformatstring(fname, "packages/maps/%s.ogz", mname); + stream *map = openrawfile(path(fname), "rb"); + if(map) + { + stream::offset len = map->size(); + if(len > 4*1024*1024) conoutf(CON_ERROR, "map is too large"); + else if(len <= 0) conoutf(CON_ERROR, "could not read map"); + else + { + sendfile(-1, 2, map); + if(needclipboard >= 0) needclipboard++; + } + delete map; + } + else conoutf(CON_ERROR, "could not read map"); + remove(findfile(fname, "rb")); + } + COMMAND(sendmap, ""); + + void gotoplayer(const char *arg) + { + if(player1->state!=CS_SPECTATOR && player1->state!=CS_EDITING) return; + int i = parseplayer(arg); + if(i>=0) + { + fpsent *d = getclient(i); + if(!d || d==player1) return; + player1->o = d->o; + vec dir; + vecfromyawpitch(player1->yaw, player1->pitch, 1, 0, dir); + player1->o.add(dir.mul(-32)); + player1->resetinterp(); + } + } + COMMANDN(goto, gotoplayer, "s"); + + void gotosel() + { + if(player1->state!=CS_EDITING) return; + player1->o = getselpos(); + vec dir; + vecfromyawpitch(player1->yaw, player1->pitch, 1, 0, dir); + player1->o.add(dir.mul(-32)); + player1->resetinterp(); + } + COMMAND(gotosel, ""); } diff --git a/src/fpsgame/entities.cpp b/src/fpsgame/entities.cpp index 4676ca8..dc0e175 100644 --- a/src/fpsgame/entities.cpp +++ b/src/fpsgame/entities.cpp @@ -4,456 +4,456 @@ int pwitemspicked[7] = { 0 }; namespace entities { - using namespace game; + using namespace game; - int extraentinfosize() { return 0; } // size in bytes of what the 2 methods below read/write... so it can be skipped by other games + int extraentinfosize() { return 0; } // size in bytes of what the 2 methods below read/write... so it can be skipped by other games - void writeent(entity &e, char *buf) // write any additional data to disk (except for ET_ ents) - { - } + void writeent(entity &e, char *buf) // write any additional data to disk (except for ET_ ents) + { + } - void readent(entity &e, char *buf, int ver) // read from disk, and init - { - if(ver <= 30) switch(e.type) - { - case TELEDEST: - e.attr1 = (int(e.attr1)+180)%360; - break; - } - } + void readent(entity &e, char *buf, int ver) // read from disk, and init + { + if(ver <= 30) switch(e.type) + { + case TELEDEST: + e.attr1 = (int(e.attr1)+180)%360; + break; + } + } #ifndef STANDALONE - vector<extentity *> ents; - - vector<extentity *> &getents() { return ents; } - - bool mayattach(extentity &e) { return false; } - bool attachent(extentity &e, extentity &a) { return false; } - - const char *itemname(int i) - { - int t = ents[i]->type; - if(t<I_SHELLS || t>I_QUAD) return NULL; - return itemstats[t-I_SHELLS].name; - } - - int itemicon(int i) - { - int t = ents[i]->type; - if(t<I_SHELLS || t>I_QUAD) return -1; - return itemstats[t-I_SHELLS].icon; - } - - const char *entmdlname(int type) - { - static const char * const entmdlnames[] = - { - NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, - "ammo/shells", "ammo/bullets", "ammo/rockets", "ammo/rrounds", "ammo/grenades", "ammo/cartridges", - "health", "boost", "tinyhealth", "tinyarmour", "armor/green", "armor/yellow", "quad", "teleporter", - NULL, NULL, - "carrot", - NULL, NULL, - "checkpoint", - NULL, NULL, - NULL, NULL, - NULL - }; - return entmdlnames[type]; - } - - const char *entmodel(const entity &e) - { - if(e.type == TELEPORT) - { - if(e.attr2 > 0) return mapmodelname(e.attr2); - if(e.attr2 < 0) return NULL; - } - return e.type < MAXENTTYPES ? entmdlname(e.type) : NULL; - } - - void preloadentities() - { - loopi(MAXENTTYPES) - { - switch(i) - { - case I_SHELLS: - [[fallthrough]]; - case I_BULLETS: - [[fallthrough]]; - case I_ROCKETS: - [[fallthrough]]; - case I_ROUNDS: - [[fallthrough]]; - case I_GRENADES: - [[fallthrough]]; - case I_CARTRIDGES: - if(m_noammo) continue; - break; - case I_HEALTH: - [[fallthrough]]; - case I_BOOST: - [[fallthrough]]; - case I_TINYHEALTH: - [[fallthrough]]; - case I_TINYARMOUR: - [[fallthrough]]; - case I_GREENARMOUR: - [[fallthrough]]; - case I_YELLOWARMOUR: - [[fallthrough]]; - case I_QUAD: - if(m_noitems) continue; - break; - } - const char *mdl = entmdlname(i); - if(!mdl) continue; - preloadmodel(mdl); - } - loopv(ents) - { - extentity &e = *ents[i]; - switch(e.type) - { - case TELEPORT: - if(e.attr2 > 0) preloadmodel(mapmodelname(e.attr2)); - [[fallthrough]]; - - case JUMPPAD: - if(e.attr4 > 0) preloadmapsound(e.attr4); - break; - } - } - } - - void renderentities() - { - loopv(ents) - { - extentity &e = *ents[i]; - int revs = 10; - switch(e.type) - { - case TELEPORT: - if(e.attr2 < 0) continue; - break; - default: - if(!e.spawned() || e.type < I_SHELLS || e.type > I_QUAD) continue; - } - const char *mdlname = entmodel(e); - if(mdlname) - { - vec p = e.o; - p.z += 1+sinf(lastmillis/100.0+e.o.x+e.o.y)/20; - rendermodel(&e.light, mdlname, ANIM_MAPMODEL|ANIM_LOOP, p, lastmillis/(float)revs, 0, MDL_SHADOW | MDL_CULL_VFC | MDL_CULL_DIST | MDL_CULL_OCCLUDED); - } - } - } - - void addammo(int type, int &v, bool local) - { - itemstat &is = itemstats[type-I_SHELLS]; - v += is.add; - if(v>is.max) v = is.max; - if(local) msgsound(is.sound); - } - - void repammo(fpsent *d, int type, bool local) - { - addammo(type, d->ammo[type-I_SHELLS+GUN_SG], local); - } - - // these two functions are called when the server acknowledges that you really - // picked up the item (in multiplayer someone may grab it before you). - - void pickupeffects(int n, fpsent *d) - { - if(!ents.inrange(n)) return; - extentity *e = ents[n]; - int type = e->type; - if(type<I_SHELLS || type>I_QUAD) return; - e->clearspawned(); - e->clearnopickup(); - if(!d) return; - itemstat &is = itemstats[type-I_SHELLS]; - fpsent *h = followingplayer(player1); - if(d!=h || isthirdperson()) - { - //particle_text(d->abovehead(), is.name, PART_TEXT, 2000, 0xFFC864, 4.0f, -8); - particle_icon(d->abovehead(), is.icon%4, is.icon/4, PART_HUD_ICON_GREY, 2000, 0xFFFFFF, 2.0f, -8); - } - playsound(itemstats[type-I_SHELLS].sound, d!=h ? &d->o : NULL, NULL, 0, 0, 0, -1, 0, 1500); - d->pickup(type); - switch(type) { - case I_TINYARMOUR: pwitemspicked[0]++; break; - case I_GREENARMOUR: pwitemspicked[1]++; break; - case I_YELLOWARMOUR: pwitemspicked[2]++; break; - case I_TINYHEALTH: pwitemspicked[3]++; break; - case I_HEALTH: pwitemspicked[4]++; break; - case I_BOOST: pwitemspicked[5]++; break; - case I_QUAD: pwitemspicked[6]++; break; - } - if(d==h) switch(type) - { - case I_BOOST: - conoutf(CON_GAMEINFO, "\f2you got the health boost!"); - playsound(S_V_BOOST, NULL, NULL, 0, 0, 0, -1, 0, 3000); - break; - - case I_QUAD: - conoutf(CON_GAMEINFO, "\f2you got the quad!"); - playsound(S_V_QUAD, NULL, NULL, 0, 0, 0, -1, 0, 3000); - break; - } - } - - // these functions are called when the client touches the item - - void teleporteffects(fpsent *d, int tp, int td, bool local) - { - if(ents.inrange(tp) && ents[tp]->type == TELEPORT) - { - extentity &e = *ents[tp]; - if(e.attr4 >= 0) - { - int snd = S_TELEPORT, flags = 0; - if(e.attr4 > 0) { snd = e.attr4; flags = SND_MAP; } - fpsent *h = followingplayer(player1); - playsound(snd, d==h ? NULL : &e.o, NULL, flags); - if(d!=h && ents.inrange(td) && ents[td]->type == TELEDEST) playsound(snd, &ents[td]->o, NULL, flags); - } - } - if(local && d->clientnum >= 0) - { - sendposition(d); - packetbuf p(32, ENET_PACKET_FLAG_RELIABLE); - putint(p, N_TELEPORT); - putint(p, d->clientnum); - putint(p, tp); - putint(p, td); - sendclientpacket(p.finalize(), 0); - flushclient(); - } - } - - void jumppadeffects(fpsent *d, int jp, bool local) - { - if(ents.inrange(jp) && ents[jp]->type == JUMPPAD) - { - extentity &e = *ents[jp]; - if(e.attr4 >= 0) - { - int snd = S_JUMPPAD, flags = 0; - if(e.attr4 > 0) { snd = e.attr4; flags = SND_MAP; } - playsound(snd, d == followingplayer(player1) ? NULL : &e.o, NULL, flags); - } - } - if(local && d->clientnum >= 0) - { - sendposition(d); - packetbuf p(16, ENET_PACKET_FLAG_RELIABLE); - putint(p, N_JUMPPAD); - putint(p, d->clientnum); - putint(p, jp); - sendclientpacket(p.finalize(), 0); - flushclient(); - } - } - - void teleport(int n, fpsent *d) // also used by monsters - { - int e = -1, tag = ents[n]->attr1, beenhere = -1; - for(;;) - { - e = findentity(TELEDEST, e+1); - if(e==beenhere || e<0) { conoutf(CON_WARN, "no teleport destination for tag %d", tag); return; } - if(beenhere<0) beenhere = e; - if(ents[e]->attr2==tag) - { - teleporteffects(d, n, e, true); - d->o = ents[e]->o; - d->yaw = ents[e]->attr1; - if(ents[e]->attr3 > 0) - { - vec dir; - vecfromyawpitch(d->yaw, 0, 1, 0, dir); - float speed = d->vel.magnitude2(); - d->vel.x = dir.x*speed; - d->vel.y = dir.y*speed; - } - else d->vel = vec(0, 0, 0); - entinmap(d); - updatedynentcache(d); - ai::inferwaypoints(d, ents[n]->o, ents[e]->o, 16.f); - break; - } - } - } - - void trypickup(int n, fpsent *d) - { - extentity *e = ents[n]; - switch(e->type) - { - default: - if(d->canpickup(e->type)) - { - addmsg(N_ITEMPICKUP, "rci", d, n); - e->setnopickup(); // even if someone else gets it first - } - break; - - case TELEPORT: - { - if(d->lastpickup==e->type && lastmillis-d->lastpickupmillis<500) break; - if(e->attr3 > 0) - { - defformatstring(hookname, "can_teleport_%d", e->attr3); - if(!execidentbool(hookname, true)) break; - } - d->lastpickup = e->type; - d->lastpickupmillis = lastmillis; - teleport(n, d); - break; - } - - case JUMPPAD: - { - if(d->lastpickup==e->type && lastmillis-d->lastpickupmillis<300) break; - d->lastpickup = e->type; - d->lastpickupmillis = lastmillis; - jumppadeffects(d, n, true); - vec v((int)(char)e->attr3*10.0f, (int)(char)e->attr2*10.0f, e->attr1*12.5f); - if(d->ai) d->ai->becareful = true; + vector<extentity *> ents; + + vector<extentity *> &getents() { return ents; } + + bool mayattach(extentity &e) { return false; } + bool attachent(extentity &e, extentity &a) { return false; } + + const char *itemname(int i) + { + int t = ents[i]->type; + if(t<I_SHELLS || t>I_QUAD) return NULL; + return itemstats[t-I_SHELLS].name; + } + + int itemicon(int i) + { + int t = ents[i]->type; + if(t<I_SHELLS || t>I_QUAD) return -1; + return itemstats[t-I_SHELLS].icon; + } + + const char *entmdlname(int type) + { + static const char * const entmdlnames[] = + { + NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, + "ammo/shells", "ammo/bullets", "ammo/rockets", "ammo/rrounds", "ammo/grenades", "ammo/cartridges", + "health", "boost", "tinyhealth", "tinyarmour", "armor/green", "armor/yellow", "quad", "teleporter", + NULL, NULL, + "carrot", + NULL, NULL, + "checkpoint", + NULL, NULL, + NULL, NULL, + NULL + }; + return entmdlnames[type]; + } + + const char *entmodel(const entity &e) + { + if(e.type == TELEPORT) + { + if(e.attr2 > 0) return mapmodelname(e.attr2); + if(e.attr2 < 0) return NULL; + } + return e.type < MAXENTTYPES ? entmdlname(e.type) : NULL; + } + + void preloadentities() + { + loopi(MAXENTTYPES) + { + switch(i) + { + case I_SHELLS: + [[fallthrough]]; + case I_BULLETS: + [[fallthrough]]; + case I_ROCKETS: + [[fallthrough]]; + case I_ROUNDS: + [[fallthrough]]; + case I_GRENADES: + [[fallthrough]]; + case I_CARTRIDGES: + if(m_noammo) continue; + break; + case I_HEALTH: + [[fallthrough]]; + case I_BOOST: + [[fallthrough]]; + case I_TINYHEALTH: + [[fallthrough]]; + case I_TINYARMOUR: + [[fallthrough]]; + case I_GREENARMOUR: + [[fallthrough]]; + case I_YELLOWARMOUR: + [[fallthrough]]; + case I_QUAD: + if(m_noitems) continue; + break; + } + const char *mdl = entmdlname(i); + if(!mdl) continue; + preloadmodel(mdl); + } + loopv(ents) + { + extentity &e = *ents[i]; + switch(e.type) + { + case TELEPORT: + if(e.attr2 > 0) preloadmodel(mapmodelname(e.attr2)); + [[fallthrough]]; + + case JUMPPAD: + if(e.attr4 > 0) preloadmapsound(e.attr4); + break; + } + } + } + + void renderentities() + { + loopv(ents) + { + extentity &e = *ents[i]; + int revs = 10; + switch(e.type) + { + case TELEPORT: + if(e.attr2 < 0) continue; + break; + default: + if(!e.spawned() || e.type < I_SHELLS || e.type > I_QUAD) continue; + } + const char *mdlname = entmodel(e); + if(mdlname) + { + vec p = e.o; + p.z += 1+sinf(lastmillis/100.0+e.o.x+e.o.y)/20; + rendermodel(&e.light, mdlname, ANIM_MAPMODEL|ANIM_LOOP, p, lastmillis/(float)revs, 0, MDL_SHADOW | MDL_CULL_VFC | MDL_CULL_DIST | MDL_CULL_OCCLUDED); + } + } + } + + void addammo(int type, int &v, bool local) + { + itemstat &is = itemstats[type-I_SHELLS]; + v += is.add; + if(v>is.max) v = is.max; + if(local) msgsound(is.sound); + } + + void repammo(fpsent *d, int type, bool local) + { + addammo(type, d->ammo[type-I_SHELLS+GUN_SG], local); + } + + // these two functions are called when the server acknowledges that you really + // picked up the item (in multiplayer someone may grab it before you). + + void pickupeffects(int n, fpsent *d) + { + if(!ents.inrange(n)) return; + extentity *e = ents[n]; + int type = e->type; + if(type<I_SHELLS || type>I_QUAD) return; + e->clearspawned(); + e->clearnopickup(); + if(!d) return; + itemstat &is = itemstats[type-I_SHELLS]; + fpsent *h = followingplayer(player1); + if(d!=h || isthirdperson()) + { + //particle_text(d->abovehead(), is.name, PART_TEXT, 2000, 0xFFC864, 4.0f, -8); + particle_icon(d->abovehead(), is.icon%4, is.icon/4, PART_HUD_ICON_GREY, 2000, 0xFFFFFF, 2.0f, -8); + } + playsound(itemstats[type-I_SHELLS].sound, d!=h ? &d->o : NULL, NULL, 0, 0, 0, -1, 0, 1500); + d->pickup(type); + switch(type) { + case I_TINYARMOUR: pwitemspicked[0]++; break; + case I_GREENARMOUR: pwitemspicked[1]++; break; + case I_YELLOWARMOUR: pwitemspicked[2]++; break; + case I_TINYHEALTH: pwitemspicked[3]++; break; + case I_HEALTH: pwitemspicked[4]++; break; + case I_BOOST: pwitemspicked[5]++; break; + case I_QUAD: pwitemspicked[6]++; break; + } + if(d==h) switch(type) + { + case I_BOOST: + conoutf(CON_GAMEINFO, "\f2you got the health boost!"); + playsound(S_V_BOOST, NULL, NULL, 0, 0, 0, -1, 0, 3000); + break; + + case I_QUAD: + conoutf(CON_GAMEINFO, "\f2you got the quad!"); + playsound(S_V_QUAD, NULL, NULL, 0, 0, 0, -1, 0, 3000); + break; + } + } + + // these functions are called when the client touches the item + + void teleporteffects(fpsent *d, int tp, int td, bool local) + { + if(ents.inrange(tp) && ents[tp]->type == TELEPORT) + { + extentity &e = *ents[tp]; + if(e.attr4 >= 0) + { + int snd = S_TELEPORT, flags = 0; + if(e.attr4 > 0) { snd = e.attr4; flags = SND_MAP; } + fpsent *h = followingplayer(player1); + playsound(snd, d==h ? NULL : &e.o, NULL, flags); + if(d!=h && ents.inrange(td) && ents[td]->type == TELEDEST) playsound(snd, &ents[td]->o, NULL, flags); + } + } + if(local && d->clientnum >= 0) + { + sendposition(d); + packetbuf p(32, ENET_PACKET_FLAG_RELIABLE); + putint(p, N_TELEPORT); + putint(p, d->clientnum); + putint(p, tp); + putint(p, td); + sendclientpacket(p.finalize(), 0); + flushclient(); + } + } + + void jumppadeffects(fpsent *d, int jp, bool local) + { + if(ents.inrange(jp) && ents[jp]->type == JUMPPAD) + { + extentity &e = *ents[jp]; + if(e.attr4 >= 0) + { + int snd = S_JUMPPAD, flags = 0; + if(e.attr4 > 0) { snd = e.attr4; flags = SND_MAP; } + playsound(snd, d == followingplayer(player1) ? NULL : &e.o, NULL, flags); + } + } + if(local && d->clientnum >= 0) + { + sendposition(d); + packetbuf p(16, ENET_PACKET_FLAG_RELIABLE); + putint(p, N_JUMPPAD); + putint(p, d->clientnum); + putint(p, jp); + sendclientpacket(p.finalize(), 0); + flushclient(); + } + } + + void teleport(int n, fpsent *d) // also used by monsters + { + int e = -1, tag = ents[n]->attr1, beenhere = -1; + for(;;) + { + e = findentity(TELEDEST, e+1); + if(e==beenhere || e<0) { conoutf(CON_WARN, "no teleport destination for tag %d", tag); return; } + if(beenhere<0) beenhere = e; + if(ents[e]->attr2==tag) + { + teleporteffects(d, n, e, true); + d->o = ents[e]->o; + d->yaw = ents[e]->attr1; + if(ents[e]->attr3 > 0) + { + vec dir; + vecfromyawpitch(d->yaw, 0, 1, 0, dir); + float speed = d->vel.magnitude2(); + d->vel.x = dir.x*speed; + d->vel.y = dir.y*speed; + } + else d->vel = vec(0, 0, 0); + entinmap(d); + updatedynentcache(d); + ai::inferwaypoints(d, ents[n]->o, ents[e]->o, 16.f); + break; + } + } + } + + void trypickup(int n, fpsent *d) + { + extentity *e = ents[n]; + switch(e->type) + { + default: + if(d->canpickup(e->type)) + { + addmsg(N_ITEMPICKUP, "rci", d, n); + e->setnopickup(); // even if someone else gets it first + } + break; + + case TELEPORT: + { + if(d->lastpickup==e->type && lastmillis-d->lastpickupmillis<500) break; + if(e->attr3 > 0) + { + defformatstring(hookname, "can_teleport_%d", e->attr3); + if(!execidentbool(hookname, true)) break; + } + d->lastpickup = e->type; + d->lastpickupmillis = lastmillis; + teleport(n, d); + break; + } + + case JUMPPAD: + { + if(d->lastpickup==e->type && lastmillis-d->lastpickupmillis<300) break; + d->lastpickup = e->type; + d->lastpickupmillis = lastmillis; + jumppadeffects(d, n, true); + vec v((int)(char)e->attr3*10.0f, (int)(char)e->attr2*10.0f, e->attr1*12.5f); + if(d->ai) d->ai->becareful = true; d->falling = vec(0, 0, 0); d->physstate = PHYS_FALL; - d->timeinair = 1; - d->vel = v; - break; - } - } - } - - void checkitems(fpsent *d) - { - if(d->state!=CS_ALIVE) return; - vec o = d->feetpos(); - loopv(ents) - { - extentity &e = *ents[i]; - if(e.type==NOTUSED) continue; - if((!e.spawned() || e.nopickup()) && e.type!=TELEPORT && e.type!=JUMPPAD) continue; - float dist = e.o.dist(o); - if(dist<(e.type==TELEPORT ? 16 : 12)) trypickup(i, d); - } - } - - void checkquad(int time, fpsent *d) - { - if(d->quadmillis && (d->quadmillis -= time)<=0) - { - d->quadmillis = 0; - fpsent *h = followingplayer(player1); - playsound(S_PUPOUT, d==h ? NULL : &d->o); - if(d==h) conoutf(CON_GAMEINFO, "\f2quad damage is over"); - } - } - - void putitems(packetbuf &p) // puts items in network stream and also spawns them locally - { - putint(p, N_ITEMLIST); - loopv(ents) if(ents[i]->type>=I_SHELLS && ents[i]->type<=I_QUAD && (!m_noammo || ents[i]->type<I_SHELLS || ents[i]->type>I_CARTRIDGES)) - { - putint(p, i); - putint(p, ents[i]->type); - } - putint(p, -1); - } - - void resetspawns() { loopv(ents) { extentity *e = ents[i]; e->clearspawned(); e->clearnopickup(); } } - - void spawnitems(bool force) - { - if(m_noitems) return; - loopv(ents) - { - extentity *e = ents[i]; - if(e->type>=I_SHELLS && e->type<=I_QUAD && (!m_noammo || e->type<I_SHELLS || e->type>I_CARTRIDGES)) - { - e->setspawned(force || !server::delayspawn(e->type)); - e->clearnopickup(); - } - } - } - - void setspawn(int i, bool on) { if(ents.inrange(i)) { extentity *e = ents[i]; e->setspawned(on); e->clearnopickup(); } } - - extentity *newentity() { return new extentity(); } - void deleteentity(extentity *e) { delete e; } - - void clearents() - { - while(ents.length()) deleteentity(ents.pop()); - } - - void fixentity(extentity &e) - { - if(e.type == TELEDEST) e.attr3 = e.attr2; - } - - void entradius(extentity &e, bool color) - { - switch(e.type) - { - case TELEDEST: - { - vec dir; - vecfromyawpitch(e.attr1, 0, 1, 0, dir); - renderentarrow(e, dir, 4); - break; - } - case TELEPORT: - loopv(ents) if(ents[i]->type == TELEDEST && e.attr1==ents[i]->attr2) - { - renderentarrow(e, vec(ents[i]->o).sub(e.o).normalize(), e.o.dist(ents[i]->o)); - break; - } - break; - - case JUMPPAD: - renderentarrow(e, vec((int)(char)e.attr3*10.0f, (int)(char)e.attr2*10.0f, e.attr1*12.5f).normalize(), 4); - break; - - default: break; - } - } - - bool printent(extentity &e, char *buf, int len) - { - return false; - } - - const char *entnameinfo(entity &e) { return ""; } - const char *entname(int i) - { - static const char * const entnames[] = - { - "none?", "light", "mapmodel", "playerstart", "envmap", "particles", "sound", "spotlight", - "shells", "bullets", "rockets", "riflerounds", "grenades", "cartridges", - "health", "healthboost", "tinyhealth", "tinyarmour", "greenarmour", "yellowarmour", "quaddamage", - "teleport", "teledest", - "jumppad", - "", "", "", "", - }; - return i>=0 && size_t(i)<sizeof(entnames)/sizeof(entnames[0]) ? entnames[i] : ""; - } - - void editent(int i, bool local) - { - extentity &e = *ents[i]; - if(local) addmsg(N_EDITENT, "rii3ii5", i, (int)(e.o.x*DMF), (int)(e.o.y*DMF), (int)(e.o.z*DMF), e.type, e.attr1, e.attr2, e.attr3, e.attr4, e.attr5); - } + d->timeinair = 1; + d->vel = v; + break; + } + } + } + + void checkitems(fpsent *d) + { + if(d->state!=CS_ALIVE) return; + vec o = d->feetpos(); + loopv(ents) + { + extentity &e = *ents[i]; + if(e.type==NOTUSED) continue; + if((!e.spawned() || e.nopickup()) && e.type!=TELEPORT && e.type!=JUMPPAD) continue; + float dist = e.o.dist(o); + if(dist<(e.type==TELEPORT ? 16 : 12)) trypickup(i, d); + } + } + + void checkquad(int time, fpsent *d) + { + if(d->quadmillis && (d->quadmillis -= time)<=0) + { + d->quadmillis = 0; + fpsent *h = followingplayer(player1); + playsound(S_PUPOUT, d==h ? NULL : &d->o); + if(d==h) conoutf(CON_GAMEINFO, "\f2quad damage is over"); + } + } + + void putitems(packetbuf &p) // puts items in network stream and also spawns them locally + { + putint(p, N_ITEMLIST); + loopv(ents) if(ents[i]->type>=I_SHELLS && ents[i]->type<=I_QUAD && (!m_noammo || ents[i]->type<I_SHELLS || ents[i]->type>I_CARTRIDGES)) + { + putint(p, i); + putint(p, ents[i]->type); + } + putint(p, -1); + } + + void resetspawns() { loopv(ents) { extentity *e = ents[i]; e->clearspawned(); e->clearnopickup(); } } + + void spawnitems(bool force) + { + if(m_noitems) return; + loopv(ents) + { + extentity *e = ents[i]; + if(e->type>=I_SHELLS && e->type<=I_QUAD && (!m_noammo || e->type<I_SHELLS || e->type>I_CARTRIDGES)) + { + e->setspawned(force || !server::delayspawn(e->type)); + e->clearnopickup(); + } + } + } + + void setspawn(int i, bool on) { if(ents.inrange(i)) { extentity *e = ents[i]; e->setspawned(on); e->clearnopickup(); } } + + extentity *newentity() { return new extentity(); } + void deleteentity(extentity *e) { delete e; } + + void clearents() + { + while(ents.length()) deleteentity(ents.pop()); + } + + void fixentity(extentity &e) + { + if(e.type == TELEDEST) e.attr3 = e.attr2; + } + + void entradius(extentity &e, bool color) + { + switch(e.type) + { + case TELEDEST: + { + vec dir; + vecfromyawpitch(e.attr1, 0, 1, 0, dir); + renderentarrow(e, dir, 4); + break; + } + case TELEPORT: + loopv(ents) if(ents[i]->type == TELEDEST && e.attr1==ents[i]->attr2) + { + renderentarrow(e, vec(ents[i]->o).sub(e.o).normalize(), e.o.dist(ents[i]->o)); + break; + } + break; + + case JUMPPAD: + renderentarrow(e, vec((int)(char)e.attr3*10.0f, (int)(char)e.attr2*10.0f, e.attr1*12.5f).normalize(), 4); + break; + + default: break; + } + } + + bool printent(extentity &e, char *buf, int len) + { + return false; + } + + const char *entnameinfo(entity &e) { return ""; } + const char *entname(int i) + { + static const char * const entnames[] = + { + "none?", "light", "mapmodel", "playerstart", "envmap", "particles", "sound", "spotlight", + "shells", "bullets", "rockets", "riflerounds", "grenades", "cartridges", + "health", "healthboost", "tinyhealth", "tinyarmour", "greenarmour", "yellowarmour", "quaddamage", + "teleport", "teledest", + "jumppad", + "", "", "", "", + }; + return i>=0 && size_t(i)<sizeof(entnames)/sizeof(entnames[0]) ? entnames[i] : ""; + } + + void editent(int i, bool local) + { + extentity &e = *ents[i]; + if(local) addmsg(N_EDITENT, "rii3ii5", i, (int)(e.o.x*DMF), (int)(e.o.y*DMF), (int)(e.o.z*DMF), e.type, e.attr1, e.attr2, e.attr3, e.attr4, e.attr5); + } #endif } diff --git a/src/fpsgame/extinfo.h b/src/fpsgame/extinfo.h index b1f4119..059ee27 100644 --- a/src/fpsgame/extinfo.h +++ b/src/fpsgame/extinfo.h @@ -1,143 +1,143 @@ -#define EXT_ACK -1 -#define EXT_VERSION 105 -#define EXT_NO_ERROR 0 -#define EXT_ERROR 1 -#define EXT_PLAYERSTATS_RESP_IDS -10 -#define EXT_PLAYERSTATS_RESP_STATS -11 -#define EXT_UPTIME 0 -#define EXT_PLAYERSTATS 1 -#define EXT_TEAMSCORE 2 +#define EXT_ACK -1 +#define EXT_VERSION 105 +#define EXT_NO_ERROR 0 +#define EXT_ERROR 1 +#define EXT_PLAYERSTATS_RESP_IDS -10 +#define EXT_PLAYERSTATS_RESP_STATS -11 +#define EXT_UPTIME 0 +#define EXT_PLAYERSTATS 1 +#define EXT_TEAMSCORE 2 /* - Client: - ----- - A: 0 EXT_UPTIME - B: 0 EXT_PLAYERSTATS cn #a client number or -1 for all players# - C: 0 EXT_TEAMSCORE - - Server: - -------- - A: 0 EXT_UPTIME EXT_ACK EXT_VERSION uptime #in seconds# - B: 0 EXT_PLAYERSTATS cn #send by client# EXT_ACK EXT_VERSION 0 or 1 #error, if cn was > -1 and client does not exist# ... - EXT_PLAYERSTATS_RESP_IDS pid(s) #1 packet# - EXT_PLAYERSTATS_RESP_STATS pid playerdata #1 packet for each player# - C: 0 EXT_TEAMSCORE EXT_ACK EXT_VERSION 0 or 1 #error, no teammode# remaining_time gamemode loop(teamdata [numbases bases] or -1) - - Errors: - -------------- - B:C:default: 0 command EXT_ACK EXT_VERSION EXT_ERROR + Client: + ----- + A: 0 EXT_UPTIME + B: 0 EXT_PLAYERSTATS cn #a client number or -1 for all players# + C: 0 EXT_TEAMSCORE + + Server: + -------- + A: 0 EXT_UPTIME EXT_ACK EXT_VERSION uptime #in seconds# + B: 0 EXT_PLAYERSTATS cn #send by client# EXT_ACK EXT_VERSION 0 or 1 #error, if cn was > -1 and client does not exist# ... + EXT_PLAYERSTATS_RESP_IDS pid(s) #1 packet# + EXT_PLAYERSTATS_RESP_STATS pid playerdata #1 packet for each player# + C: 0 EXT_TEAMSCORE EXT_ACK EXT_VERSION 0 or 1 #error, no teammode# remaining_time gamemode loop(teamdata [numbases bases] or -1) + + Errors: + -------------- + B:C:default: 0 command EXT_ACK EXT_VERSION EXT_ERROR */ - VAR(extinfoip, 0, 0, 1); - - void extinfoplayer(ucharbuf &p, clientinfo *ci) - { - ucharbuf q = p; - putint(q, EXT_PLAYERSTATS_RESP_STATS); // send player stats following - putint(q, ci->clientnum); //add player id - putint(q, ci->ping); - sendstring(ci->name, q); - sendstring(ci->team, q); - putint(q, ci->state.frags); - putint(q, ci->state.flags); - putint(q, ci->state.deaths); - putint(q, ci->state.teamkills); - putint(q, ci->state.damage*100/max(ci->state.shotdamage,1)); - putint(q, ci->state.health); - putint(q, ci->state.armour); - putint(q, ci->state.gunselect); - putint(q, ci->privilege); - putint(q, ci->state.state); - uint ip = extinfoip ? getclientip(ci->clientnum) : 0; - q.put((uchar*)&ip, 3); - sendserverinforeply(q); - } - - static inline void extinfoteamscore(ucharbuf &p, const char *team, int score) - { - sendstring(team, p); - putint(p, score); - putint(p,-1); //no bases follow - } - - void extinfoteams(ucharbuf &p) - { - putint(p, m_teammode ? 0 : 1); - putint(p, gamemode); - putint(p, max((gamelimit - gamemillis)/1000, 0)); - if(!m_teammode) return; - - vector<teamscore> scores; - loopv(clients) - { - clientinfo *ci = clients[i]; - if(ci->state.state!=CS_SPECTATOR && ci->team[0] && scores.htfind(ci->team) < 0) - { - teaminfo *ti = teaminfos.access(ci->team); - scores.add(teamscore(ci->team, ti ? ti->frags : 0)); - } - } - loopv(scores) extinfoteamscore(p, scores[i].team, scores[i].score); - } - - void extserverinforeply(ucharbuf &req, ucharbuf &p) - { - int extcmd = getint(req); // extended commands - - //Build a new packet - putint(p, EXT_ACK); //send ack - putint(p, EXT_VERSION); //send version of extended info - - switch(extcmd) - { - case EXT_UPTIME: - { - putint(p, totalsecs); //in seconds - break; - } - - case EXT_PLAYERSTATS: - { - int cn = getint(req); //a special player, -1 for all - - clientinfo *ci = NULL; - if(cn >= 0) - { - loopv(clients) if(clients[i]->clientnum == cn) { ci = clients[i]; break; } - if(!ci) - { - putint(p, EXT_ERROR); //client requested by id was not found - sendserverinforeply(p); - return; - } - } - - putint(p, EXT_NO_ERROR); //so far no error can happen anymore - - ucharbuf q = p; //remember buffer position - putint(q, EXT_PLAYERSTATS_RESP_IDS); //send player ids following - if(ci) putint(q, ci->clientnum); - else loopv(clients) putint(q, clients[i]->clientnum); - sendserverinforeply(q); - - if(ci) extinfoplayer(p, ci); - else loopv(clients) extinfoplayer(p, clients[i]); - return; - } - - case EXT_TEAMSCORE: - { - extinfoteams(p); - break; - } - - default: - { - putint(p, EXT_ERROR); - break; - } - } - sendserverinforeply(p); - } + VAR(extinfoip, 0, 0, 1); + + void extinfoplayer(ucharbuf &p, clientinfo *ci) + { + ucharbuf q = p; + putint(q, EXT_PLAYERSTATS_RESP_STATS); // send player stats following + putint(q, ci->clientnum); //add player id + putint(q, ci->ping); + sendstring(ci->name, q); + sendstring(ci->team, q); + putint(q, ci->state.frags); + putint(q, ci->state.flags); + putint(q, ci->state.deaths); + putint(q, ci->state.teamkills); + putint(q, ci->state.damage*100/max(ci->state.shotdamage,1)); + putint(q, ci->state.health); + putint(q, ci->state.armour); + putint(q, ci->state.gunselect); + putint(q, ci->privilege); + putint(q, ci->state.state); + uint ip = extinfoip ? getclientip(ci->clientnum) : 0; + q.put((uchar*)&ip, 3); + sendserverinforeply(q); + } + + static inline void extinfoteamscore(ucharbuf &p, const char *team, int score) + { + sendstring(team, p); + putint(p, score); + putint(p,-1); //no bases follow + } + + void extinfoteams(ucharbuf &p) + { + putint(p, m_teammode ? 0 : 1); + putint(p, gamemode); + putint(p, max((gamelimit - gamemillis)/1000, 0)); + if(!m_teammode) return; + + vector<teamscore> scores; + loopv(clients) + { + clientinfo *ci = clients[i]; + if(ci->state.state!=CS_SPECTATOR && ci->team[0] && scores.htfind(ci->team) < 0) + { + teaminfo *ti = teaminfos.access(ci->team); + scores.add(teamscore(ci->team, ti ? ti->frags : 0)); + } + } + loopv(scores) extinfoteamscore(p, scores[i].team, scores[i].score); + } + + void extserverinforeply(ucharbuf &req, ucharbuf &p) + { + int extcmd = getint(req); // extended commands + + //Build a new packet + putint(p, EXT_ACK); //send ack + putint(p, EXT_VERSION); //send version of extended info + + switch(extcmd) + { + case EXT_UPTIME: + { + putint(p, totalsecs); //in seconds + break; + } + + case EXT_PLAYERSTATS: + { + int cn = getint(req); //a special player, -1 for all + + clientinfo *ci = NULL; + if(cn >= 0) + { + loopv(clients) if(clients[i]->clientnum == cn) { ci = clients[i]; break; } + if(!ci) + { + putint(p, EXT_ERROR); //client requested by id was not found + sendserverinforeply(p); + return; + } + } + + putint(p, EXT_NO_ERROR); //so far no error can happen anymore + + ucharbuf q = p; //remember buffer position + putint(q, EXT_PLAYERSTATS_RESP_IDS); //send player ids following + if(ci) putint(q, ci->clientnum); + else loopv(clients) putint(q, clients[i]->clientnum); + sendserverinforeply(q); + + if(ci) extinfoplayer(p, ci); + else loopv(clients) extinfoplayer(p, clients[i]); + return; + } + + case EXT_TEAMSCORE: + { + extinfoteams(p); + break; + } + + default: + { + putint(p, EXT_ERROR); + break; + } + } + sendserverinforeply(p); + } diff --git a/src/fpsgame/fps.cpp b/src/fpsgame/fps.cpp index 7b066ca..fde58e8 100644 --- a/src/fpsgame/fps.cpp +++ b/src/fpsgame/fps.cpp @@ -2,1401 +2,1400 @@ namespace game { - bool intermission = false; - int maptime = 0, maprealtime = 0, maplimit = -1; - int respawnent = -1; - int lasthit = 0, lastspawnattempt = 0; - - int following = -1, followdir = 0; - - fpsent *player1 = NULL; // our client - vector<fpsent *> players; // other clients - int savedammo[NUMGUNS]; - - bool clientoption(const char *arg) { return false; } - - void taunt() - { - if(player1->state!=CS_ALIVE || player1->physstate<PHYS_SLOPE) return; - if(lastmillis-player1->lasttaunt<1000) return; - player1->lasttaunt = lastmillis; - addmsg(N_TAUNT, "rc", player1); - } - COMMAND(taunt, ""); - - ICOMMAND(getfollow, "", (), - { - fpsent *f = followingplayer(); - intret(f ? f->clientnum : -1); - }); + bool intermission = false; + int maptime = 0, maprealtime = 0, maplimit = -1; + int respawnent = -1; + int lasthit = 0, lastspawnattempt = 0; + + int following = -1, followdir = 0; + + fpsent *player1 = NULL; // our client + vector<fpsent *> players; // other clients + int savedammo[NUMGUNS]; + + bool clientoption(const char *arg) { return false; } + + void taunt() + { + if(player1->state!=CS_ALIVE || player1->physstate<PHYS_SLOPE) return; + if(lastmillis-player1->lasttaunt<1000) return; + player1->lasttaunt = lastmillis; + addmsg(N_TAUNT, "rc", player1); + } + COMMAND(taunt, ""); + + ICOMMAND(getfollow, "", (), + { + fpsent *f = followingplayer(); + intret(f ? f->clientnum : -1); + }); void follow(char *arg) - { - if(arg[0] ? player1->state==CS_SPECTATOR : following>=0) - { - following = arg[0] ? parseplayer(arg) : -1; - if(following==player1->clientnum) following = -1; - followdir = 0; - conoutf("follow %s", following>=0 ? "on" : "off"); - } - } - COMMAND(follow, "s"); - - void nextfollow(int dir) - { - if(player1->state!=CS_SPECTATOR || clients.empty()) - { - stopfollowing(); - return; - } - int cur = following >= 0 ? following : (dir < 0 ? clients.length() - 1 : 0); - loopv(clients) - { - cur = (cur + dir + clients.length()) % clients.length(); - if(clients[cur] && clients[cur]->state!=CS_SPECTATOR) - { - if(following<0) conoutf("follow on"); - following = cur; - followdir = dir; - return; - } - } - stopfollowing(); - } - ICOMMAND(nextfollow, "i", (int *dir), nextfollow(*dir < 0 ? -1 : 1)); - - - const char *getclientmap() { return clientmap; } - - void resetgamestate() - { - clearprojectiles(); - clearbouncers(); - } - - fpsent *spawnstate(fpsent *d) // reset player state not persistent accross spawns - { - d->respawn(); - d->spawnstate(gamemode); - return d; - } - - void respawnself() - { - if(ispaused()) return; - if(m_mp(gamemode)) - { - int seq = (player1->lifesequence<<16)|((lastmillis/1000)&0xFFFF); - if(player1->respawned!=seq) { addmsg(N_TRYSPAWN, "rc", player1); player1->respawned = seq; } - } - else - { - spawnplayer(player1); - showscores(false); - lasthit = 0; - } - } - - fpsent *pointatplayer() - { - loopv(players) if(players[i] != player1 && intersect(players[i], player1->o, worldpos)) return players[i]; - return NULL; - } - - void stopfollowing() - { - if(following<0) return; - following = -1; - followdir = 0; - conoutf("follow off"); - } - - fpsent *followingplayer(fpsent *fallback) - { - if(player1->state!=CS_SPECTATOR || following<0) return fallback; - fpsent *target = getclient(following); - if(target && target->state!=CS_SPECTATOR) return target; - return fallback; - } - - fpsent *hudplayer() - { - if(thirdperson && allowthirdperson()) return player1; - return followingplayer(player1); - } - - void setupcamera() - { - fpsent *target = followingplayer(); - if(target) - { - player1->yaw = target->yaw; - player1->pitch = target->state==CS_DEAD ? 0 : target->pitch; - player1->o = target->o; - player1->resetinterp(); - } - } - - bool allowthirdperson(bool msg) - { - return player1->state==CS_SPECTATOR || player1->state==CS_EDITING || m_edit || !multiplayer(msg); - } - ICOMMAND(allowthirdperson, "b", (int *msg), intret(allowthirdperson(*msg!=0) ? 1 : 0)); - - bool detachcamera() - { - fpsent *d = hudplayer(); - return d->state==CS_DEAD; - } - - bool collidecamera() - { - switch(player1->state) - { - case CS_EDITING: return false; - case CS_SPECTATOR: return followingplayer()!=NULL; - } - return true; - } - - VARP(smoothmove, 0, 75, 100); - VARP(smoothdist, 0, 32, 64); - - void predictplayer(fpsent *d, bool move) - { - d->o = d->newpos; - d->yaw = d->newyaw; - d->pitch = d->newpitch; - d->roll = d->newroll; - if(move) - { - moveplayer(d, 1, false); - d->newpos = d->o; - } - float k = 1.0f - float(lastmillis - d->smoothmillis)/smoothmove; - if(k>0) - { - d->o.add(vec(d->deltapos).mul(k)); - d->yaw += d->deltayaw*k; - if(d->yaw<0) d->yaw += 360; - else if(d->yaw>=360) d->yaw -= 360; - d->pitch += d->deltapitch*k; - d->roll += d->deltaroll*k; - } - } - - void otherplayers(int curtime) - { - loopv(players) - { - fpsent *d = players[i]; - if(d == player1 || d->ai) continue; - - if(d->state==CS_DEAD && d->ragdoll) moveragdoll(d); - else if(!intermission) - { - if(lastmillis - d->lastaction >= d->gunwait) d->gunwait = 0; - if(d->quadmillis) entities::checkquad(curtime, d); - } - - const int lagtime = totalmillis-d->lastupdate; - if(!lagtime || intermission) continue; - else if(lagtime>1000 && d->state==CS_ALIVE) - { - d->state = CS_LAGGED; - continue; - } - if(d->state==CS_ALIVE || d->state==CS_EDITING) - { - if(smoothmove && d->smoothmillis>0) predictplayer(d, true); - else moveplayer(d, 1, false); - } - else if(d->state==CS_DEAD && !d->ragdoll && lastmillis-d->lastpain<2000) moveplayer(d, 1, true); - } - } - - void checkslowmo() - { - static int lastslowmohealth = 0; - server::forcegamespeed(intermission ? 100 : clamp(player1->health, 25, 200)); - if(player1->health<player1->maxhealth && lastmillis-max(maptime, lastslowmohealth)>player1->health*player1->health/2) - { - lastslowmohealth = lastmillis; - player1->health++; - } - } - - void updateworld() // main game update loop - { - if(!maptime) { maptime = lastmillis; maprealtime = totalmillis; return; } - if(!curtime) { gets2c(); if(player1->clientnum>=0) c2sinfo(); return; } - - physicsframe(); - ai::navigate(); - if(player1->state != CS_DEAD && !intermission) - { - if(player1->quadmillis) entities::checkquad(curtime, player1); - } - updateweapons(curtime); - otherplayers(curtime); - ai::update(); - moveragdolls(); - gets2c(); - if(connected) - { - if(player1->state == CS_DEAD) - { - if(player1->ragdoll) moveragdoll(player1); - else if(lastmillis-player1->lastpain<2000) - { - player1->move = player1->strafe = 0; - moveplayer(player1, 10, true); - } - } - else if(!intermission) - { - if(player1->ragdoll) cleanragdoll(player1); - moveplayer(player1, 10, true); - swayhudgun(curtime); - entities::checkitems(player1); - } - } - if(player1->clientnum>=0) c2sinfo(); // do this last, to reduce the effective frame lag - } - - float proximityscore(float x, float lower, float upper) - { - if(x <= lower) return 1.0f; - if(x >= upper) return 0.0f; - float a = x - lower, b = x - upper; - return (b * b) / (a * a + b * b); - } - - static inline float harmonicmean(float a, float b) { return a + b > 0 ? 2 * a * b / (a + b) : 0.0f; } - - // avoid spawning near other players - float ratespawn(dynent *d, const extentity &e) - { - fpsent *p = (fpsent *)d; - vec loc = vec(e.o).addz(p->eyeheight); - float maxrange = !m_noitems ? 400.0f : 110.0f; - float minplayerdist = maxrange; - loopv(players) - { - const fpsent *o = players[i]; - if(o == p) - { - if(m_noitems || (o->state != CS_ALIVE && lastmillis - o->lastpain > 3000)) continue; - } - else if(o->state != CS_ALIVE || isteam(o->team, p->team)) continue; - - vec dir = vec(o->o).sub(loc); - float dist = dir.squaredlen(); - if(dist >= minplayerdist*minplayerdist) continue; - dist = sqrtf(dist); - dir.mul(1/dist); - - // scale actual distance if not in line of sight - if(raycube(loc, dir, dist) < dist) dist *= 1.5f; - minplayerdist = min(minplayerdist, dist); - } - return 1.0f - proximityscore(minplayerdist, 80.0f, maxrange); - } - - void pickgamespawn(fpsent *d) - { - int ent = d == player1 && respawnent >= 0 ? respawnent : -1; - findplayerspawn(d, ent, 0); - } - - void spawnplayer(fpsent *d) // place at random spawn - { - pickgamespawn(d); - spawnstate(d); - if(d==player1) - { - if(editmode) d->state = CS_EDITING; - else if(d->state != CS_SPECTATOR) d->state = CS_ALIVE; - } - else d->state = CS_ALIVE; - } - - VARP(spawnwait, 0, 0, 1000); - - void respawn() - { - if(player1->state==CS_DEAD) - { - player1->attacking = false; - respawnself(); - } - } - COMMAND(respawn, ""); - - // inputs - - VARP(attackspawn, 0, 1, 1); - - void doattack(bool on) - { - if(!connected || intermission) return; - if((player1->attacking = on) && attackspawn) respawn(); - } - - VARP(jumpspawn, 0, 1, 1); - - bool canjump() - { - if(!connected || intermission) return false; - if(jumpspawn) respawn(); - return player1->state!=CS_DEAD; - } - - bool allowmove(physent *d) - { - if(d->type!=ENT_PLAYER) return true; - return !((fpsent *)d)->lasttaunt || lastmillis-((fpsent *)d)->lasttaunt>=1000; - } - - VARP(hitsound, 0, 0, 1); - - void damaged(int damage, fpsent *d, fpsent *actor, bool local) - { - if((d->state!=CS_ALIVE && d->state != CS_LAGGED && d->state != CS_SPAWNING) || intermission) return; - - if(local) damage = d->dodamage(damage); - else if(actor==player1) return; - - fpsent *h = hudplayer(); - if(h!=player1 && actor==h && d!=actor) - { - if(hitsound && lasthit != lastmillis) playsound(S_HIT); - lasthit = lastmillis; - } - if(d==h) - { - damageblend(damage); - damagecompass(damage, actor->o); - } - damageeffect(damage, d, d!=h); - - ai::damaged(d, actor); - - if(d->health<=0) { if(local) killed(d, actor); } - else if(d==h) playsound(S_PAIN6); - else playsound(S_PAIN1+rnd(5), &d->o); - } - - VARP(deathscore, 0, 1, 1); - - void deathstate(fpsent *d, bool restore) - { - d->state = CS_DEAD; - d->lastpain = lastmillis; - if(!restore) d->deaths++; - - if(d==player1) - { - if(deathscore) showscores(true); - disablezoom(); - if(!restore) loopi(NUMGUNS) savedammo[i] = player1->ammo[i]; - d->attacking = false; - d->roll = 0; - playsound(S_DIE1+rnd(2)); - } - else - { - d->move = d->strafe = 0; - d->resetinterp(); - d->smoothmillis = 0; - playsound(S_DIE1+rnd(2), &d->o); - } - } - - VARP(teamcolorfrags, 0, 1, 1); - - /// xolatile: HUD frag messages + { + if(arg[0] ? player1->state==CS_SPECTATOR : following>=0) + { + following = arg[0] ? parseplayer(arg) : -1; + if(following==player1->clientnum) following = -1; + followdir = 0; + conoutf("follow %s", following>=0 ? "on" : "off"); + } + } + COMMAND(follow, "s"); + + void nextfollow(int dir) + { + if(player1->state!=CS_SPECTATOR || clients.empty()) + { + stopfollowing(); + return; + } + int cur = following >= 0 ? following : (dir < 0 ? clients.length() - 1 : 0); + loopv(clients) + { + cur = (cur + dir + clients.length()) % clients.length(); + if(clients[cur] && clients[cur]->state!=CS_SPECTATOR) + { + if(following<0) conoutf("follow on"); + following = cur; + followdir = dir; + return; + } + } + stopfollowing(); + } + ICOMMAND(nextfollow, "i", (int *dir), nextfollow(*dir < 0 ? -1 : 1)); + + + const char *getclientmap() { return clientmap; } + + void resetgamestate() + { + clearprojectiles(); + clearbouncers(); + } + + fpsent *spawnstate(fpsent *d) // reset player state not persistent accross spawns + { + d->respawn(); + d->spawnstate(gamemode); + return d; + } + + void respawnself() + { + if(ispaused()) return; + if(m_mp(gamemode)) + { + int seq = (player1->lifesequence<<16)|((lastmillis/1000)&0xFFFF); + if(player1->respawned!=seq) { addmsg(N_TRYSPAWN, "rc", player1); player1->respawned = seq; } + } + else + { + spawnplayer(player1); + showscores(false); + lasthit = 0; + } + } + + fpsent *pointatplayer() + { + loopv(players) if(players[i] != player1 && intersect(players[i], player1->o, worldpos)) return players[i]; + return NULL; + } + + void stopfollowing() + { + if(following<0) return; + following = -1; + followdir = 0; + conoutf("follow off"); + } + + fpsent *followingplayer(fpsent *fallback) + { + if(player1->state!=CS_SPECTATOR || following<0) return fallback; + fpsent *target = getclient(following); + if(target && target->state!=CS_SPECTATOR) return target; + return fallback; + } + + fpsent *hudplayer() + { + if(thirdperson && allowthirdperson()) return player1; + return followingplayer(player1); + } + + void setupcamera() + { + fpsent *target = followingplayer(); + if(target) + { + player1->yaw = target->yaw; + player1->pitch = target->state==CS_DEAD ? 0 : target->pitch; + player1->o = target->o; + player1->resetinterp(); + } + } + + bool allowthirdperson(bool msg) + { + return player1->state==CS_SPECTATOR || player1->state==CS_EDITING || m_edit || !multiplayer(msg); + } + ICOMMAND(allowthirdperson, "b", (int *msg), intret(allowthirdperson(*msg!=0) ? 1 : 0)); + + bool detachcamera() + { + fpsent *d = hudplayer(); + return d->state==CS_DEAD; + } + + bool collidecamera() + { + switch(player1->state) + { + case CS_EDITING: return false; + case CS_SPECTATOR: return followingplayer()!=NULL; + } + return true; + } + + VARP(smoothmove, 0, 75, 100); + VARP(smoothdist, 0, 32, 64); + + void predictplayer(fpsent *d, bool move) + { + d->o = d->newpos; + d->yaw = d->newyaw; + d->pitch = d->newpitch; + d->roll = d->newroll; + if(move) + { + moveplayer(d, 1, false); + d->newpos = d->o; + } + float k = 1.0f - float(lastmillis - d->smoothmillis)/smoothmove; + if(k>0) + { + d->o.add(vec(d->deltapos).mul(k)); + d->yaw += d->deltayaw*k; + if(d->yaw<0) d->yaw += 360; + else if(d->yaw>=360) d->yaw -= 360; + d->pitch += d->deltapitch*k; + d->roll += d->deltaroll*k; + } + } + + void otherplayers(int curtime) + { + loopv(players) + { + fpsent *d = players[i]; + if(d == player1 || d->ai) continue; + + if(d->state==CS_DEAD && d->ragdoll) moveragdoll(d); + else if(!intermission) + { + if(lastmillis - d->lastaction >= d->gunwait) d->gunwait = 0; + if(d->quadmillis) entities::checkquad(curtime, d); + } + + const int lagtime = totalmillis-d->lastupdate; + if(!lagtime || intermission) continue; + else if(lagtime>1000 && d->state==CS_ALIVE) + { + d->state = CS_LAGGED; + continue; + } + if(d->state==CS_ALIVE || d->state==CS_EDITING) + { + if(smoothmove && d->smoothmillis>0) predictplayer(d, true); + else moveplayer(d, 1, false); + } + else if(d->state==CS_DEAD && !d->ragdoll && lastmillis-d->lastpain<2000) moveplayer(d, 1, true); + } + } + + void checkslowmo() + { + static int lastslowmohealth = 0; + server::forcegamespeed(intermission ? 100 : clamp(player1->health, 25, 200)); + if(player1->health<player1->maxhealth && lastmillis-max(maptime, lastslowmohealth)>player1->health*player1->health/2) + { + lastslowmohealth = lastmillis; + player1->health++; + } + } + + void updateworld() // main game update loop + { + if(!maptime) { maptime = lastmillis; maprealtime = totalmillis; return; } + if(!curtime) { gets2c(); if(player1->clientnum>=0) c2sinfo(); return; } + + physicsframe(); + ai::navigate(); + if(player1->state != CS_DEAD && !intermission) + { + if(player1->quadmillis) entities::checkquad(curtime, player1); + } + updateweapons(curtime); + otherplayers(curtime); + ai::update(); + moveragdolls(); + gets2c(); + if(connected) + { + if(player1->state == CS_DEAD) + { + if(player1->ragdoll) moveragdoll(player1); + else if(lastmillis-player1->lastpain<2000) + { + player1->move = player1->strafe = 0; + moveplayer(player1, 10, true); + } + } + else if(!intermission) + { + if(player1->ragdoll) cleanragdoll(player1); + moveplayer(player1, 10, true); + swayhudgun(curtime); + entities::checkitems(player1); + } + } + if(player1->clientnum>=0) c2sinfo(); // do this last, to reduce the effective frame lag + } + + float proximityscore(float x, float lower, float upper) + { + if(x <= lower) return 1.0f; + if(x >= upper) return 0.0f; + float a = x - lower, b = x - upper; + return (b * b) / (a * a + b * b); + } + + static inline float harmonicmean(float a, float b) { return a + b > 0 ? 2 * a * b / (a + b) : 0.0f; } + + // avoid spawning near other players + float ratespawn(dynent *d, const extentity &e) + { + fpsent *p = (fpsent *)d; + vec loc = vec(e.o).addz(p->eyeheight); + float maxrange = !m_noitems ? 400.0f : 110.0f; + float minplayerdist = maxrange; + loopv(players) + { + const fpsent *o = players[i]; + if(o == p) + { + if(m_noitems || (o->state != CS_ALIVE && lastmillis - o->lastpain > 3000)) continue; + } + else if(o->state != CS_ALIVE || isteam(o->team, p->team)) continue; + + vec dir = vec(o->o).sub(loc); + float dist = dir.squaredlen(); + if(dist >= minplayerdist*minplayerdist) continue; + dist = sqrtf(dist); + dir.mul(1/dist); + + // scale actual distance if not in line of sight + if(raycube(loc, dir, dist) < dist) dist *= 1.5f; + minplayerdist = min(minplayerdist, dist); + } + return 1.0f - proximityscore(minplayerdist, 80.0f, maxrange); + } + + void pickgamespawn(fpsent *d) + { + int ent = d == player1 && respawnent >= 0 ? respawnent : -1; + findplayerspawn(d, ent, 0); + } + + void spawnplayer(fpsent *d) // place at random spawn + { + pickgamespawn(d); + spawnstate(d); + if(d==player1) + { + if(editmode) d->state = CS_EDITING; + else if(d->state != CS_SPECTATOR) d->state = CS_ALIVE; + } + else d->state = CS_ALIVE; + } + + VARP(spawnwait, 0, 0, 1000); + + void respawn() + { + if(player1->state==CS_DEAD) + { + player1->attacking = false; + respawnself(); + } + } + COMMAND(respawn, ""); + + // inputs + + VARP(attackspawn, 0, 1, 1); + + void doattack(bool on) + { + if(!connected || intermission) return; + if((player1->attacking = on) && attackspawn) respawn(); + } + + VARP(jumpspawn, 0, 1, 1); + + bool canjump() + { + if(!connected || intermission) return false; + if(jumpspawn) respawn(); + return player1->state!=CS_DEAD; + } + + bool allowmove(physent *d) + { + if(d->type!=ENT_PLAYER) return true; + return !((fpsent *)d)->lasttaunt || lastmillis-((fpsent *)d)->lasttaunt>=1000; + } + + VARP(hitsound, 0, 0, 1); + + void damaged(int damage, fpsent *d, fpsent *actor, bool local) + { + if((d->state!=CS_ALIVE && d->state != CS_LAGGED && d->state != CS_SPAWNING) || intermission) return; + + if(local) damage = d->dodamage(damage); + else if(actor==player1) return; + + fpsent *h = hudplayer(); + if(h!=player1 && actor==h && d!=actor) + { + if(hitsound && lasthit != lastmillis) playsound(S_HIT); + lasthit = lastmillis; + } + if(d==h) + { + damagecompass(damage, actor->o); + } + damageeffect(damage, d, d!=h); + + ai::damaged(d, actor); + + if(d->health<=0) { if(local) killed(d, actor); } + else if(d==h) playsound(S_PAIN6); + else playsound(S_PAIN1+rnd(5), &d->o); + } + + VARP(deathscore, 0, 1, 1); + + void deathstate(fpsent *d, bool restore) + { + d->state = CS_DEAD; + d->lastpain = lastmillis; + if(!restore) d->deaths++; + + if(d==player1) + { + if(deathscore) showscores(true); + disablezoom(); + if(!restore) loopi(NUMGUNS) savedammo[i] = player1->ammo[i]; + d->attacking = false; + d->roll = 0; + playsound(S_DIE1+rnd(2)); + } + else + { + d->move = d->strafe = 0; + d->resetinterp(); + d->smoothmillis = 0; + playsound(S_DIE1+rnd(2), &d->o); + } + } + + VARP(teamcolorfrags, 0, 1, 1); + + /// xolatile: HUD frag messages #define fragmessageduration (2000) - string hudfragger, hudfragged; - int hudfraggun, hudfragmillis; - - void sethudfragdata(char *fragger, char *fragged, int gunid) - { - copystring(hudfragger, fragger ? fragger : ""); - copystring(hudfragged, fragged); - hudfraggun = gunid; - hudfragmillis = lastmillis; - } - - void killed(fpsent *d, fpsent *actor) - { - if(d->state==CS_EDITING) - { - d->editstate = CS_DEAD; - d->deaths++; - if(d!=player1) d->resetinterp(); - return; - } - else if((d->state!=CS_ALIVE && d->state != CS_LAGGED && d->state != CS_SPAWNING) || intermission) return; - - fpsent *h = followingplayer(player1); - int contype = d==h || actor==h ? CON_FRAG_SELF : CON_FRAG_OTHER; - const char *dname = "", *aname = ""; - if(m_teammode && teamcolorfrags) - { - dname = teamcolorname(d, "you"); - aname = teamcolorname(actor, "you"); - } - else - { - dname = colorname(d, NULL, "", "", "you"); - aname = colorname(actor, NULL, "", "", "you"); - } - if(actor->type==ENT_AI) - conoutf(contype, "\f2%s got killed by %s!", dname, aname); - else if(d==actor || actor->type==ENT_INANIMATE) - conoutf(contype, "\f2%s suicided%s", dname, d==player1 ? "!" : ""); - else if(isteam(d->team, actor->team)) - { - contype |= CON_TEAMKILL; - if(actor==player1) conoutf(contype, "\f6%s fragged a teammate (%s)", aname, dname); - else if(d==player1) conoutf(contype, "\f6%s got fragged by a teammate (%s)", dname, aname); - else conoutf(contype, "\f2%s fragged a teammate (%s)", aname, dname); - } - else - { - if(d==player1) - { - conoutf(contype, "\f2%s got fragged by %s", dname, aname); - sethudfragdata(actor->name, d->name, actor->gunselect); - } - else - { - conoutf(contype, "\f2%s fragged %s", aname, dname); - sethudfragdata(actor->name, d->name, actor->gunselect); - } - } - deathstate(d); - ai::killed(d, actor); - } - - void timeupdate(int secs) - { - server::timeupdate(secs); - if(secs > 0) - { - maplimit = lastmillis + secs*1000; - } - else - { - intermission = true; - player1->attacking = false; - conoutf(CON_GAMEINFO, "\f2intermission:"); - conoutf(CON_GAMEINFO, "\f2game has ended!"); - conoutf(CON_GAMEINFO, "\f2player frags: %d, deaths: %d", player1->frags, player1->deaths); - int accuracy = (player1->totaldamage*100)/max(player1->totalshots, 1); - conoutf(CON_GAMEINFO, "\f2player total damage dealt: %d, damage wasted: %d, accuracy(%%): %d", player1->totaldamage, player1->totalshots-player1->totaldamage, accuracy); - - showscores(true); - disablezoom(); - - execident("intermission"); - } - } - - ICOMMAND(getfrags, "", (), intret(player1->frags)); - ICOMMAND(getflags, "", (), intret(player1->flags)); - ICOMMAND(getdeaths, "", (), intret(player1->deaths)); - ICOMMAND(getaccuracy, "", (), intret((player1->totaldamage*100)/max(player1->totalshots, 1))); - ICOMMAND(gettotaldamage, "", (), intret(player1->totaldamage)); - ICOMMAND(gettotalshots, "", (), intret(player1->totalshots)); - - vector<fpsent *> clients; - - fpsent *newclient(int cn) // ensure valid entity - { - if(cn < 0 || cn > max(0xFF, MAXCLIENTS + MAXBOTS)) - { - neterr("clientnum", false); - return NULL; - } - - if(cn == player1->clientnum) return player1; - - while(cn >= clients.length()) clients.add(NULL); - if(!clients[cn]) - { - fpsent *d = new fpsent; - d->clientnum = cn; - clients[cn] = d; - players.add(d); - } - return clients[cn]; - } - - fpsent *getclient(int cn) // ensure valid entity - { - if(cn == player1->clientnum) return player1; - return clients.inrange(cn) ? clients[cn] : NULL; - } - - void clientdisconnected(int cn, bool notify) - { - if(!clients.inrange(cn)) return; - if(following==cn) - { - if(followdir) nextfollow(followdir); - else stopfollowing(); - } - unignore(cn); - fpsent *d = clients[cn]; - if(!d) return; - if(notify && d->name[0]) conoutf("\f4leave:\f7 %s", colorname(d)); - removeweapons(d); - removetrackedparticles(d); - removetrackeddynlights(d); - players.removeobj(d); - DELETEP(clients[cn]); - cleardynentcache(); - } - - void clearclients(bool notify) - { - loopv(clients) if(clients[i]) clientdisconnected(i, notify); - } - - void initclient() - { - player1 = spawnstate(new fpsent); - filtertext(player1->name, "Anonymous", false, false, MAXNAMELEN); - players.add(player1); - } - - VARP(showmodeinfo, 0, 1, 1); - - void startgame() - { - clearprojectiles(); - clearbouncers(); - clearragdolls(); - - clearteaminfo(); - - // reset perma-state - loopv(players) - { - fpsent *d = players[i]; - d->frags = d->flags = 0; - d->deaths = 0; - d->totaldamage = 0; - d->totalshots = 0; - d->maxhealth = 100; - d->maxarmour = 50; - d->lifesequence = -1; - d->respawned = d->suicided = -2; - } - - intermission = false; - maptime = maprealtime = 0; - maplimit = -1; - - conoutf(CON_GAMEINFO, "\f2game mode is %s", server::modename(gamemode)); - - const char *info = m_valid(gamemode) ? gamemodes[gamemode - STARTGAMEMODE].info : NULL; - if(showmodeinfo && info) conoutf(CON_GAMEINFO, "\f0%s", info); - - showscores(false); - disablezoom(); - lasthit = 0; - - execident("mapstart"); - } - - void loadingmap(const char *name) - { - execident("playsong"); - } - - void startmap(const char *name) // called just after a map load - { - pwreset(); - ai::savewaypoints(); - ai::clearwaypoints(true); - - respawnent = -1; // so we don't respawn at an old spot - if(!m_mp(gamemode)) spawnplayer(player1); - else findplayerspawn(player1, -1); - entities::resetspawns(); - copystring(clientmap, name ? name : ""); - - sendmapinfo(); - } - - const char *getmapinfo() - { - return showmodeinfo && m_valid(gamemode) ? gamemodes[gamemode - STARTGAMEMODE].info : NULL; - } - - const char *getscreenshotinfo() - { - return server::modename(gamemode, NULL); - } - - void physicstrigger(physent *d, bool local, int floorlevel, int waterlevel, int material) - { - if(d->type==ENT_INANIMATE) return; - if (waterlevel>0) { if(material!=MAT_LAVA) playsound(S_SPLASH1, d==player1 ? NULL : &d->o); } - else if(waterlevel<0) playsound(material==MAT_LAVA ? S_BURN : S_SPLASH2, d==player1 ? NULL : &d->o); - if (floorlevel>0) { if(d==player1 || d->type!=ENT_PLAYER || ((fpsent *)d)->ai) msgsound(S_JUMP, d); } - else if(floorlevel<0) { if(d==player1 || d->type!=ENT_PLAYER || ((fpsent *)d)->ai) msgsound(S_LAND, d); } - } - - void dynentcollide(physent *d, physent *o, const vec &dir) - { - return; - } - - void msgsound(int n, physent *d) - { - if(!d || d==player1) - { - addmsg(N_SOUND, "ci", d, n); - playsound(n); - } - else - { - if(d->type==ENT_PLAYER && ((fpsent *)d)->ai) - addmsg(N_SOUND, "ci", d, n); - playsound(n, &d->o); - } - } - - int numdynents() { return players.length(); } - - dynent *iterdynents(int i) - { - if(i<players.length()) return players[i]; - i -= players.length(); - return NULL; - } - - bool duplicatename(fpsent *d, const char *name = NULL, const char *alt = NULL) - { - if(!name) name = d->name; - if(alt && d != player1 && !strcmp(name, alt)) return true; - loopv(players) if(d!=players[i] && !strcmp(name, players[i]->name)) return true; - return false; - } - - static string cname[3]; - static int cidx = 0; - - const char *colorname(fpsent *d, const char *name, const char *prefix, const char *suffix, const char *alt) - { - if(!name) name = alt && d == player1 ? alt : d->name; - bool dup = !name[0] || duplicatename(d, name, alt) || d->aitype != AI_NONE; - if(dup || prefix[0] || suffix[0]) - { - cidx = (cidx+1)%3; - if(dup) formatstring(cname[cidx], d->aitype == AI_NONE ? "%s%s \fs\f5(%d)\fr%s" : "%s%s \fs\f5[%d]\fr%s", prefix, name, d->clientnum, suffix); - else formatstring(cname[cidx], "%s%s%s", prefix, name, suffix); - return cname[cidx]; - } - return name; - } - - VARP(teamcolortext, 0, 1, 1); - - const char *teamcolorname(fpsent *d, const char *alt) - { - if(!teamcolortext || !m_teammode || d->state==CS_SPECTATOR) return colorname(d, NULL, "", "", alt); - return colorname(d, NULL, isteam(d->team, player1->team) ? "\fs\f1" : "\fs\f3", "\fr", alt); - } - - const char *teamcolor(const char *name, bool sameteam, const char *alt) - { - if(!teamcolortext || !m_teammode) return sameteam || !alt ? name : alt; - cidx = (cidx+1)%3; - formatstring(cname[cidx], sameteam ? "\fs\f1%s\fr" : "\fs\f3%s\fr", sameteam || !alt ? name : alt); - return cname[cidx]; - } - - const char *teamcolor(const char *name, const char *team, const char *alt) - { - return teamcolor(name, team && isteam(team, player1->team), alt); - } - - VARP(teamsounds, 0, 1, 1); - - void teamsound(bool sameteam, int n, const vec *loc) - { - playsound(n, loc, NULL, teamsounds ? (m_teammode && sameteam ? SND_USE_ALT : SND_NO_ALT) : 0); - } - - void teamsound(fpsent *d, int n, const vec *loc) - { - teamsound(isteam(d->team, player1->team), n, loc); - } - - void suicide(physent *d) - { - if(d==player1 || (d->type==ENT_PLAYER && ((fpsent *)d)->ai)) - { - if(d->state!=CS_ALIVE) return; - fpsent *pl = (fpsent *)d; - if(!m_mp(gamemode)) killed(pl, pl); - else - { - int seq = (pl->lifesequence<<16)|((lastmillis/1000)&0xFFFF); - if(pl->suicided!=seq) { addmsg(N_SUICIDE, "rc", pl); pl->suicided = seq; } - } - } - } - ICOMMAND(suicide, "", (), suicide(player1)); - - void drawicon(int icon, float x, float y, float sz) - { - settexture("packages/hud/items.png"); - float tsz = 0.25f, tx = tsz*(icon%4), ty = tsz*(icon/4); - gle::defvertex(2); - gle::deftexcoord0(); - gle::begin(GL_TRIANGLE_STRIP); - gle::attribf(x, y); gle::attribf(tx, ty); - gle::attribf(x+sz, y); gle::attribf(tx+tsz, ty); - gle::attribf(x, y+sz); gle::attribf(tx, ty+tsz); - gle::attribf(x+sz, y+sz); gle::attribf(tx+tsz, ty+tsz); - gle::end(); - } - - float abovegameplayhud(int w, int h) - { - switch(hudplayer()->state) - { - case CS_EDITING: - case CS_SPECTATOR: - return 1; - default: - return 1650.0f/1800.0f; - } - } - - int ammohudup[3] = { GUN_CG, GUN_RL, GUN_GL }, - ammohuddown[3] = { GUN_RIFLE, GUN_SG, GUN_PISTOL }, - ammohudcycle[7] = { -1, -1, -1, -1, -1, -1, -1 }; - - ICOMMAND(ammohudup, "V", (tagval *args, int numargs), - { - loopi(3) ammohudup[i] = i < numargs ? getweapon(args[i].getstr()) : -1; - }); - - ICOMMAND(ammohuddown, "V", (tagval *args, int numargs), - { - loopi(3) ammohuddown[i] = i < numargs ? getweapon(args[i].getstr()) : -1; - }); - - ICOMMAND(ammohudcycle, "V", (tagval *args, int numargs), - { - loopi(7) ammohudcycle[i] = i < numargs ? getweapon(args[i].getstr()) : -1; - }); - - VARP(ammohud, 0, 1, 1); - - void drawammohud(fpsent *d) - { - float x = HICON_X + 2*HICON_STEP, y = HICON_Y, sz = HICON_SIZE; - pushhudmatrix(); - hudmatrix.scale(1/3.2f, 1/3.2f, 1); - flushhudmatrix(); - float xup = (x+sz)*3.2f, yup = y*3.2f + 0.1f*sz; - loopi(3) - { - int gun = ammohudup[i]; - if(gun < GUN_FIST || gun > GUN_PISTOL || gun == d->gunselect || !d->ammo[gun]) continue; - drawicon(HICON_FIST+gun, xup, yup, sz); - yup += sz; - } - float xdown = x*3.2f - sz, ydown = (y+sz)*3.2f - 0.1f*sz; - loopi(3) - { - int gun = ammohuddown[3-i-1]; - if(gun < GUN_FIST || gun > GUN_PISTOL || gun == d->gunselect || !d->ammo[gun]) continue; - ydown -= sz; - drawicon(HICON_FIST+gun, xdown, ydown, sz); - } - int offset = 0, num = 0; - loopi(7) - { - int gun = ammohudcycle[i]; - if(gun < GUN_FIST || gun > GUN_PISTOL) continue; - if(gun == d->gunselect) offset = i + 1; - else if(d->ammo[gun]) num++; - } - float xcycle = (x+sz/2)*3.2f + 0.5f*num*sz, ycycle = y*3.2f-sz; - loopi(7) - { - int gun = ammohudcycle[(i + offset)%7]; - if(gun < GUN_FIST || gun > GUN_PISTOL || gun == d->gunselect || !d->ammo[gun]) continue; - xcycle -= sz; - drawicon(HICON_FIST+gun, xcycle, ycycle, sz); - } - pophudmatrix(); - } - - void hudquad(float x, float y, float w, float h, float r = 1, float g = 1, float b = 1, float tx = 0, float ty = 0, float tw = 1, float th = 1) - { - gle::defvertex(2); - gle::deftexcoord0(); - gle::colorf(r, g, b); - gle::begin(GL_TRIANGLE_STRIP); - gle::attribf(x, y); gle::attribf(tx, ty); - gle::attribf(x+w, y); gle::attribf(tx + tw, ty); - gle::attribf(x, y+h); gle::attribf(tx, ty + th); - gle::attribf(x+w, y+h); gle::attribf(tx + tw, ty + th); - gle::end(); - } - - VARP(healthcolors, 0, 1, 1); - - VARP(hudhealth, 0, 1, 1); - FVARP(hudhealthx, 0, 0, 1); - FVARP(hudhealthy, 0, 1, 1); - FVARP(hudhealthscale, 0.1, 1.0, 1.0); - - void drawhudhealth(fpsent *d, int w, int h) - { - pushhudmatrix(); - hudmatrix.scale(hudhealthscale, hudhealthscale, 1); - flushhudmatrix(); - - bvec healthcolor = bvec::hexcolor(healthcolors && !m_insta ? - (d->state==CS_DEAD ? 0x808080 : - (d->health<=25 ? 0xc02020 : - (d->health<=50 ? 0xc08020 : - (d->health<=75 ? 0xc0c040 : - (d->health<=100 ? 0xFFFFFF : 0x4080c0))))) : 0xFFFFFF); - const float proportion = (w/4.0f)/600.0f; - const float healthbarw = 600*proportion; - const float healthbarh = 113*proportion; - vec2 offset = vec2(hudhealthx, hudhealthy).mul(vec2(w-healthbarw, h-healthbarh).div(hudhealthscale)); - settexture("packages/hud/health_bar_base.png"); - float hp = (float)d->health/d->maxhealth; - hudquad(offset.x, offset.y, hp*healthbarw, healthbarh, healthcolor.r, healthcolor.g, healthcolor.b, 0, 0, hp, 1); - settexture("packages/hud/health_bar_over.png"); - hudquad(offset.x, offset.y, healthbarw, healthbarh); - if (d->quadmillis) { - settexture("packages/hud/health_bar_quad.png"); - hudquad(offset.x, offset.y, healthbarw, healthbarh); - } - defformatstring(health, "%d", d->state==CS_DEAD ? 0 : d->health); - float tw=0, th=0; text_boundsf(health, tw, th); - draw_text(health, offset.x+(125*proportion-tw)/2, offset.y+(healthbarh-th)/2, healthcolor.r, healthcolor.g, healthcolor.b); - - pophudmatrix(); - } - - VARP(hudmaxhealth, 0, 1, 1); - FVARP(hudmaxhealthx, 0, 0.207, 1); - FVARP(hudmaxhealthy, 0, 0.97, 1); - FVARP(hudmaxhealthscale, 0.1, 1.0, 1.0); - - void drawhudmaxhealth(fpsent *d, int w, int h) - { - pushhudmatrix(); - hudmatrix.scale(hudmaxhealthscale, hudmaxhealthscale, 1); - flushhudmatrix(); - - const float proportion = (w/15.0f)/160.0f; - const float healthboostw = 160*proportion; - const float healthboosth = 78*proportion; - float hb = (float)d->maxhealth/100.0f-1.0f; - vec2 offset = vec2(hudmaxhealthx, hudmaxhealthy).mul(vec2(w-healthboostw, h-healthboosth).div(hudhealthscale)); - settexture("packages/hud/health_boost_base.png"); - hudquad(offset.x, offset.y, hb*healthboostw, healthboosth, 0.3f, 0.6f, 0.9f, 0, 0, hb, 1); - settexture("packages/hud/health_boost_over.png"); - hudquad(offset.x, offset.y, healthboostw, healthboosth); - if (d->quadmillis) { - settexture("packages/hud/health_boost_quad.png"); - hudquad(offset.x, offset.y, healthboostw, healthboosth); - } - - pophudmatrix(); - } - - VARP(armourcolors, 0, 1, 1); - - VARP(hudarmour, 0, 1, 1); - FVARP(hudarmourx, 0, 1, 1); - FVARP(hudarmoury, 0, 1, 1); - FVARP(hudarmourscale, 0.1, 1.0, 1.0); - - void drawhudarmour(fpsent *d, int w, int h) - { - pushhudmatrix(); - hudmatrix.scale(hudarmourscale, hudarmourscale, 1); - flushhudmatrix(); - - bvec armourcolor = bvec::hexcolor(d->armourtype == A_BLUE ? 0x83ade5 : (d->armourtype == A_GREEN ? 0x77f29e : (d->armourtype == A_YELLOW ? 0xf5f19b : 0xffffff))); - const float proportion = (w/4.0f)/600.0f; - const float armourbarw = 600*proportion; - const float armourbarh = 113*proportion; - vec2 offset = vec2(hudarmourx, hudarmoury).mul(vec2(w-armourbarw, h-armourbarh).div(hudarmourscale)); - settexture("packages/hud/armour_bar_base.png"); - float ap = (float)d->armour/d->maxarmour; - hudquad(offset.x, offset.y, ap*armourbarw, armourbarh, armourcolor.r, armourcolor.g, armourcolor.b, 0, 0, ap, 1); - settexture("packages/hud/armour_bar_over.png"); - hudquad(offset.x, offset.y, armourbarw, armourbarh); - if (d->quadmillis) { - settexture("packages/hud/armour_bar_quad.png"); - hudquad(offset.x, offset.y, armourbarw, armourbarh); - } - defformatstring(armour, "%d", d->state==CS_DEAD ? 0 : d->armour); - float tw=0, th=0; text_boundsf(armour, tw, th); - draw_text(armour, offset.x+(2*(600-63)*proportion-tw)/2, offset.y+(armourbarh-th)/2, armourcolor.r, armourcolor.g, armourcolor.b); - - pophudmatrix(); - } - - void drawhudicons(fpsent *d, int w, int h) - { - glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); - glEnable(GL_BLEND); - - if(hudhealth) drawhudhealth(d, w, h); - if(hudmaxhealth) drawhudmaxhealth(d, w, h); - if(hudarmour) drawhudarmour(d, w, h); - - if(d->state!=CS_DEAD) { - drawicon(HICON_FIST+d->gunselect, HICON_X + 2*HICON_STEP, HICON_Y); - if(ammohud) drawammohud(d); - } - } - - VARP(gameclock, 0, 0, 1); - FVARP(gameclockscale, 1e-3f, 0.75f, 1e3f); - HVARP(gameclockcolour, 0, 0xFFFFFF, 0xFFFFFF); - VARP(gameclockalpha, 0, 255, 255); - HVARP(gameclocklowcolour, 0, 0xFFC040, 0xFFFFFF); - VARP(gameclockalign, -1, 0, 1); - FVARP(gameclockx, 0, 0.50f, 1); - FVARP(gameclocky, 0, 0.03f, 1); - - void drawgameclock(int w, int h) - { - int secs = max(maplimit-lastmillis + 999, 0)/1000, mins = secs/60; - secs %= 60; - - defformatstring(buf, "%d:%02d", mins, secs); - int tw = 0, th = 0; - text_bounds(buf, tw, th); - - vec2 offset = vec2(gameclockx, gameclocky).mul(vec2(w, h).div(gameclockscale)); - if(gameclockalign == 1) offset.x -= tw; - else if(gameclockalign == 0) offset.x -= tw/2.0f; - offset.y -= th/2.0f; - - pushhudmatrix(); - hudmatrix.scale(gameclockscale, gameclockscale, 1); - flushhudmatrix(); - - int color = mins < 1 ? gameclocklowcolour : gameclockcolour; - draw_text(buf, int(offset.x), int(offset.y), (color>>16)&0xFF, (color>>8)&0xFF, color&0xFF, gameclockalpha); - - pophudmatrix(); - } - - extern int hudscore; - extern void drawhudscore(int w, int h); - - VARP(ammobar, 0, 0, 1); - VARP(ammobaralign, -1, 0, 1); - VARP(ammobarhorizontal, 0, 0, 1); - VARP(ammobarflip, 0, 0, 1); - VARP(ammobarhideempty, 0, 1, 1); - VARP(ammobarsep, 0, 20, 500); - VARP(ammobarcountsep, 0, 20, 500); - FVARP(ammobarcountscale, 0.5, 1.5, 2); - FVARP(ammobarx, 0, 0.025f, 1.0f); - FVARP(ammobary, 0, 0.5f, 1.0f); - FVARP(ammobarscale, 0.1f, 0.5f, 1.0f); - - void drawammobarcounter(const vec2 ¢er, const fpsent *p, int gun) - { - vec2 icondrawpos = vec2(center).sub(HICON_SIZE / 2); - int alpha = p->ammo[gun] ? 0xFF : 0x7F; - gle::color(bvec(0xFF, 0xFF, 0xFF), alpha); - drawicon(HICON_FIST + gun, icondrawpos.x, icondrawpos.y); - - int fw, fh; text_bounds("000", fw, fh); - float labeloffset = HICON_SIZE / 2.0f + ammobarcountsep + ammobarcountscale * (ammobarhorizontal ? fh : fw) / 2.0f; - vec2 offsetdir = (ammobarhorizontal ? vec2(0, 1) : vec2(1, 0)).mul(ammobarflip ? -1 : 1); - vec2 labelorigin = vec2(offsetdir).mul(labeloffset).add(center); - - pushhudmatrix(); - hudmatrix.translate(labelorigin.x, labelorigin.y, 0); - hudmatrix.scale(ammobarcountscale, ammobarcountscale, 1); - flushhudmatrix(); - - defformatstring(label, "%d", p->ammo[gun]); - int tw, th; text_bounds(label, tw, th); - vec2 textdrawpos = vec2(-tw, -th).div(2); - float ammoratio = (float)p->ammo[gun] / itemstats[gun-GUN_SG].add; - bvec color = bvec::hexcolor(p->ammo[gun] == 0 || ammoratio >= 1.0f ? 0xFFFFFF : (ammoratio >= 0.5f ? 0xFFC040 : 0xFF0000)); - draw_text(label, textdrawpos.x, textdrawpos.y, color.r, color.g, color.b, alpha); - - pophudmatrix(); - } - - static inline bool ammobargunvisible(const fpsent *d, int gun) - { - return d->ammo[gun] > 0 || d->gunselect == gun; - } - - void drawammobar(int w, int h, fpsent *p) - { - if(m_insta) return; - - int NUMPLAYERGUNS = GUN_PISTOL - GUN_SG + 1; - int numvisibleguns = NUMPLAYERGUNS; - if(ammobarhideempty) loopi(NUMPLAYERGUNS) if(!ammobargunvisible(p, GUN_SG + i)) numvisibleguns--; - - vec2 origin = vec2(ammobarx, ammobary).mul(vec2(w, h).div(ammobarscale)); - vec2 offsetdir = ammobarhorizontal ? vec2(1, 0) : vec2(0, 1); - float stepsize = HICON_SIZE + ammobarsep; - float initialoffset = (ammobaralign - 1) * (numvisibleguns - 1) * stepsize / 2; - - pushhudmatrix(); - hudmatrix.scale(ammobarscale, ammobarscale, 1); - flushhudmatrix(); - - int numskippedguns = 0; - loopi(NUMPLAYERGUNS) if(ammobargunvisible(p, GUN_SG + i) || !ammobarhideempty) - { - float offset = initialoffset + (i - numskippedguns) * stepsize; - vec2 drawpos = vec2(offsetdir).mul(offset).add(origin); - drawammobarcounter(drawpos, p, GUN_SG + i); - } - else numskippedguns++; - - pophudmatrix(); - } - - VARP(speedometer, 0, 1, 1); - FVARP(speedometerx, 0.0, 0.5, 1.0); - FVARP(speedometery, 0.0, 0.6, 1.0); - FVARP(speedometerscale, 0.1, 0.5, 1.0); - VARP(speedometercolor, 0, 1, 1); - FVARP(speedometeralpha, 0.0, 0.5, 1.0); - - void drawspeedometer(fpsent *d, int w, int h) - { - int speedforreal = (int) (sqrtf(d->vel.squaredlen()) + 1.0f); - speedforreal = (speedforreal == 1) ? 0 : speedforreal; - vec colour = vec(255, 255, 255); - float realw = 0; - float realh = 0; - if (speedometercolor) { - if (speedforreal==0) colour = vec(60, 60, 60); - else if (speedforreal>0 && speedforreal<=60) colour = vec(240, 30, 30); - else if (speedforreal>60 && speedforreal<=120) colour = vec(180, 90, 60); - else if (speedforreal>120 && speedforreal<=180) colour = vec(180, 180, 30); - else if (speedforreal>180 && speedforreal<=240) colour = vec(90, 180, 60); - else colour = vec(30, 240, 30); - } - - defformatstring(speedstring, "%d", speedforreal); - text_boundsf(speedstring, realw, realh); - vec2 offset = vec2(speedometerx, speedometery).mul(vec2(w, h).div(speedometerscale)); - offset.x -= realw/2.0f; - offset.y -= realh/2.0f; - - pushhudmatrix(); - hudmatrix.scale(speedometerscale, speedometerscale, 1); - flushhudmatrix(); - - const int speedow = 220; - const int speedoh = 101; - settexture("packages/hud/speedometer.png"); - hudquad(offset.x+realw/2.0f-speedow/2, offset.y+realh/2.0f-speedoh/2, speedow, speedoh); - - draw_text(speedstring, int(offset.x), int(offset.y), colour.x, colour.y, colour.z, (int)(speedometeralpha*255.0f)); - - pophudmatrix(); - } - - void gameplayhud(int w, int h) - { - pushhudmatrix(); - hudmatrix.scale(h/1800.0f, h/1800.0f, 1); - flushhudmatrix(); - - if(player1->state==CS_SPECTATOR) - { - int pw, ph, tw, th, fw, fh; - text_bounds(" ", pw, ph); - text_bounds("SPECTATOR", tw, th); - th = max(th, ph); - fpsent *f = followingplayer(); - text_bounds(f ? colorname(f) : " ", fw, fh); - fh = max(fh, ph); - draw_text("SPECTATOR", w*1800/h - tw - pw, 1650 - th - fh); - if(f) - { - int color = statuscolor(f, 0xFFFFFF); - draw_text(colorname(f), w*1800/h - fw - pw, 1650 - fh, (color>>16)&0xFF, (color>>8)&0xFF, color&0xFF); - } - } - - fpsent *d = hudplayer(); - - pophudmatrix(); - - if(d->state!=CS_EDITING && d->state!=CS_SPECTATOR) drawhudicons(d, w, h); - - if(d->state!=CS_EDITING && d->state!=CS_SPECTATOR && d->state!=CS_DEAD) - { - if(ammobar) drawammobar(w, h, d); - if(speedometer) drawspeedometer(d, w, h); - } - - - if(!m_edit) - { - if(gameclock) drawgameclock(w, h); - if(hudscore) drawhudscore(w, h); - } - - /// Frag message. - - if((hudfragmillis+fragmessageduration > lastmillis) && (lastmillis>fragmessageduration)) - { - vec2 center = vec2(0.5*w, 0.2*h); - float width = 0, height = 0; - pushhudmatrix(); - hudmatrix.scale(0.8, 0.8, 1); - flushhudmatrix(); - draw_textf("%s", center.x, center.y, hudfragger); - text_boundsf(hudfragger, width, height); - drawicon(HICON_FIST+hudfraggun, center.x+width, center.y, height); - draw_textf("%s", center.x+width+height, center.y, hudfragged); - pophudmatrix(); - } - } - - int clipconsole(int w, int h) - { - return 0; - } - - VARP(teamcrosshair, 0, 1, 1); - VARP(hitcrosshair, 0, 425, 1000); - - const char *defaultcrosshair(int index) - { - switch(index) - { - case 2: return "data/hit.png"; - case 1: return "data/teammate.png"; - default: return "data/crosshair.png"; - } - } - - int selectcrosshair(vec &color) - { - fpsent *d = hudplayer(); - if(d->state==CS_SPECTATOR || d->state==CS_DEAD) return -1; - - if(d->state!=CS_ALIVE) return 0; - - int crosshair = 0; - if(lasthit && lastmillis - lasthit < hitcrosshair) crosshair = 2; - else if(teamcrosshair) - { - dynent *o = intersectclosest(d->o, worldpos, d); - if(o && o->type==ENT_PLAYER && isteam(((fpsent *)o)->team, d->team)) - { - crosshair = 1; - color = vec(0, 0, 1); - } - } - - if(crosshair!=1 && !editmode && !m_insta) - { - if(d->health<=25) color = vec(1, 0, 0); - else if(d->health<=50) color = vec(1, 0.5f, 0); - } - if(d->gunwait) color.mul(0.5f); - return crosshair; - } - - void lighteffects(dynent *e, vec &color, vec &dir) - { + string hudfragger, hudfragged; + int hudfraggun, hudfragmillis; + + void sethudfragdata(char *fragger, char *fragged, int gunid) + { + copystring(hudfragger, fragger ? fragger : ""); + copystring(hudfragged, fragged); + hudfraggun = gunid; + hudfragmillis = lastmillis; + } + + void killed(fpsent *d, fpsent *actor) + { + if(d->state==CS_EDITING) + { + d->editstate = CS_DEAD; + d->deaths++; + if(d!=player1) d->resetinterp(); + return; + } + else if((d->state!=CS_ALIVE && d->state != CS_LAGGED && d->state != CS_SPAWNING) || intermission) return; + + fpsent *h = followingplayer(player1); + int contype = d==h || actor==h ? CON_FRAG_SELF : CON_FRAG_OTHER; + const char *dname = "", *aname = ""; + if(m_teammode && teamcolorfrags) + { + dname = teamcolorname(d, "you"); + aname = teamcolorname(actor, "you"); + } + else + { + dname = colorname(d, NULL, "", "", "you"); + aname = colorname(actor, NULL, "", "", "you"); + } + if(actor->type==ENT_AI) + conoutf(contype, "\f2%s got killed by %s!", dname, aname); + else if(d==actor || actor->type==ENT_INANIMATE) + conoutf(contype, "\f2%s suicided%s", dname, d==player1 ? "!" : ""); + else if(isteam(d->team, actor->team)) + { + contype |= CON_TEAMKILL; + if(actor==player1) conoutf(contype, "\f6%s fragged a teammate (%s)", aname, dname); + else if(d==player1) conoutf(contype, "\f6%s got fragged by a teammate (%s)", dname, aname); + else conoutf(contype, "\f2%s fragged a teammate (%s)", aname, dname); + } + else + { + if(d==player1) + { + conoutf(contype, "\f2%s got fragged by %s", dname, aname); + sethudfragdata(actor->name, d->name, actor->gunselect); + } + else + { + conoutf(contype, "\f2%s fragged %s", aname, dname); + sethudfragdata(actor->name, d->name, actor->gunselect); + } + } + deathstate(d); + ai::killed(d, actor); + } + + void timeupdate(int secs) + { + server::timeupdate(secs); + if(secs > 0) + { + maplimit = lastmillis + secs*1000; + } + else + { + intermission = true; + player1->attacking = false; + conoutf(CON_GAMEINFO, "\f2intermission:"); + conoutf(CON_GAMEINFO, "\f2game has ended!"); + conoutf(CON_GAMEINFO, "\f2player frags: %d, deaths: %d", player1->frags, player1->deaths); + int accuracy = (player1->totaldamage*100)/max(player1->totalshots, 1); + conoutf(CON_GAMEINFO, "\f2player total damage dealt: %d, damage wasted: %d, accuracy(%%): %d", player1->totaldamage, player1->totalshots-player1->totaldamage, accuracy); + + showscores(true); + disablezoom(); + + execident("intermission"); + } + } + + ICOMMAND(getfrags, "", (), intret(player1->frags)); + ICOMMAND(getflags, "", (), intret(player1->flags)); + ICOMMAND(getdeaths, "", (), intret(player1->deaths)); + ICOMMAND(getaccuracy, "", (), intret((player1->totaldamage*100)/max(player1->totalshots, 1))); + ICOMMAND(gettotaldamage, "", (), intret(player1->totaldamage)); + ICOMMAND(gettotalshots, "", (), intret(player1->totalshots)); + + vector<fpsent *> clients; + + fpsent *newclient(int cn) // ensure valid entity + { + if(cn < 0 || cn > max(0xFF, MAXCLIENTS + MAXBOTS)) + { + neterr("clientnum", false); + return NULL; + } + + if(cn == player1->clientnum) return player1; + + while(cn >= clients.length()) clients.add(NULL); + if(!clients[cn]) + { + fpsent *d = new fpsent; + d->clientnum = cn; + clients[cn] = d; + players.add(d); + } + return clients[cn]; + } + + fpsent *getclient(int cn) // ensure valid entity + { + if(cn == player1->clientnum) return player1; + return clients.inrange(cn) ? clients[cn] : NULL; + } + + void clientdisconnected(int cn, bool notify) + { + if(!clients.inrange(cn)) return; + if(following==cn) + { + if(followdir) nextfollow(followdir); + else stopfollowing(); + } + unignore(cn); + fpsent *d = clients[cn]; + if(!d) return; + if(notify && d->name[0]) conoutf("\f4leave:\f7 %s", colorname(d)); + removeweapons(d); + removetrackedparticles(d); + removetrackeddynlights(d); + players.removeobj(d); + DELETEP(clients[cn]); + cleardynentcache(); + } + + void clearclients(bool notify) + { + loopv(clients) if(clients[i]) clientdisconnected(i, notify); + } + + void initclient() + { + player1 = spawnstate(new fpsent); + filtertext(player1->name, "Anonymous", false, false, MAXNAMELEN); + players.add(player1); + } + + VARP(showmodeinfo, 0, 1, 1); + + void startgame() + { + clearprojectiles(); + clearbouncers(); + clearragdolls(); + + clearteaminfo(); + + // reset perma-state + loopv(players) + { + fpsent *d = players[i]; + d->frags = d->flags = 0; + d->deaths = 0; + d->totaldamage = 0; + d->totalshots = 0; + d->maxhealth = 100; + d->maxarmour = 50; + d->lifesequence = -1; + d->respawned = d->suicided = -2; + } + + intermission = false; + maptime = maprealtime = 0; + maplimit = -1; + + conoutf(CON_GAMEINFO, "\f2game mode is %s", server::modename(gamemode)); + + const char *info = m_valid(gamemode) ? gamemodes[gamemode - STARTGAMEMODE].info : NULL; + if(showmodeinfo && info) conoutf(CON_GAMEINFO, "\f0%s", info); + + showscores(false); + disablezoom(); + lasthit = 0; + + execident("mapstart"); + } + + void loadingmap(const char *name) + { + execident("playsong"); + } + + void startmap(const char *name) // called just after a map load + { + pwreset(); + ai::savewaypoints(); + ai::clearwaypoints(true); + + respawnent = -1; // so we don't respawn at an old spot + if(!m_mp(gamemode)) spawnplayer(player1); + else findplayerspawn(player1, -1); + entities::resetspawns(); + copystring(clientmap, name ? name : ""); + + sendmapinfo(); + } + + const char *getmapinfo() + { + return showmodeinfo && m_valid(gamemode) ? gamemodes[gamemode - STARTGAMEMODE].info : NULL; + } + + const char *getscreenshotinfo() + { + return server::modename(gamemode, NULL); + } + + void physicstrigger(physent *d, bool local, int floorlevel, int waterlevel, int material) + { + if(d->type==ENT_INANIMATE) return; + if (waterlevel>0) { if(material!=MAT_LAVA) playsound(S_SPLASH1, d==player1 ? NULL : &d->o); } + else if(waterlevel<0) playsound(material==MAT_LAVA ? S_BURN : S_SPLASH2, d==player1 ? NULL : &d->o); + if (floorlevel>0) { if(d==player1 || d->type!=ENT_PLAYER || ((fpsent *)d)->ai) msgsound(S_JUMP, d); } + else if(floorlevel<0) { if(d==player1 || d->type!=ENT_PLAYER || ((fpsent *)d)->ai) msgsound(S_LAND, d); } + } + + void dynentcollide(physent *d, physent *o, const vec &dir) + { + return; + } + + void msgsound(int n, physent *d) + { + if(!d || d==player1) + { + addmsg(N_SOUND, "ci", d, n); + playsound(n); + } + else + { + if(d->type==ENT_PLAYER && ((fpsent *)d)->ai) + addmsg(N_SOUND, "ci", d, n); + playsound(n, &d->o); + } + } + + int numdynents() { return players.length(); } + + dynent *iterdynents(int i) + { + if(i<players.length()) return players[i]; + i -= players.length(); + return NULL; + } + + bool duplicatename(fpsent *d, const char *name = NULL, const char *alt = NULL) + { + if(!name) name = d->name; + if(alt && d != player1 && !strcmp(name, alt)) return true; + loopv(players) if(d!=players[i] && !strcmp(name, players[i]->name)) return true; + return false; + } + + static string cname[3]; + static int cidx = 0; + + const char *colorname(fpsent *d, const char *name, const char *prefix, const char *suffix, const char *alt) + { + if(!name) name = alt && d == player1 ? alt : d->name; + bool dup = !name[0] || duplicatename(d, name, alt) || d->aitype != AI_NONE; + if(dup || prefix[0] || suffix[0]) + { + cidx = (cidx+1)%3; + if(dup) formatstring(cname[cidx], d->aitype == AI_NONE ? "%s%s \fs\f5(%d)\fr%s" : "%s%s \fs\f5[%d]\fr%s", prefix, name, d->clientnum, suffix); + else formatstring(cname[cidx], "%s%s%s", prefix, name, suffix); + return cname[cidx]; + } + return name; + } + + VARP(teamcolortext, 0, 1, 1); + + const char *teamcolorname(fpsent *d, const char *alt) + { + if(!teamcolortext || !m_teammode || d->state==CS_SPECTATOR) return colorname(d, NULL, "", "", alt); + return colorname(d, NULL, isteam(d->team, player1->team) ? "\fs\f1" : "\fs\f3", "\fr", alt); + } + + const char *teamcolor(const char *name, bool sameteam, const char *alt) + { + if(!teamcolortext || !m_teammode) return sameteam || !alt ? name : alt; + cidx = (cidx+1)%3; + formatstring(cname[cidx], sameteam ? "\fs\f1%s\fr" : "\fs\f3%s\fr", sameteam || !alt ? name : alt); + return cname[cidx]; + } + + const char *teamcolor(const char *name, const char *team, const char *alt) + { + return teamcolor(name, team && isteam(team, player1->team), alt); + } + + VARP(teamsounds, 0, 1, 1); + + void teamsound(bool sameteam, int n, const vec *loc) + { + playsound(n, loc, NULL, teamsounds ? (m_teammode && sameteam ? SND_USE_ALT : SND_NO_ALT) : 0); + } + + void teamsound(fpsent *d, int n, const vec *loc) + { + teamsound(isteam(d->team, player1->team), n, loc); + } + + void suicide(physent *d) + { + if(d==player1 || (d->type==ENT_PLAYER && ((fpsent *)d)->ai)) + { + if(d->state!=CS_ALIVE) return; + fpsent *pl = (fpsent *)d; + if(!m_mp(gamemode)) killed(pl, pl); + else + { + int seq = (pl->lifesequence<<16)|((lastmillis/1000)&0xFFFF); + if(pl->suicided!=seq) { addmsg(N_SUICIDE, "rc", pl); pl->suicided = seq; } + } + } + } + ICOMMAND(suicide, "", (), suicide(player1)); + + void drawicon(int icon, float x, float y, float sz) + { + settexture("packages/hud/items.png"); + float tsz = 0.25f, tx = tsz*(icon%4), ty = tsz*(icon/4); + gle::defvertex(2); + gle::deftexcoord0(); + gle::begin(GL_TRIANGLE_STRIP); + gle::attribf(x, y); gle::attribf(tx, ty); + gle::attribf(x+sz, y); gle::attribf(tx+tsz, ty); + gle::attribf(x, y+sz); gle::attribf(tx, ty+tsz); + gle::attribf(x+sz, y+sz); gle::attribf(tx+tsz, ty+tsz); + gle::end(); + } + + float abovegameplayhud(int w, int h) + { + switch(hudplayer()->state) + { + case CS_EDITING: + case CS_SPECTATOR: + return 1; + default: + return 1650.0f/1800.0f; + } + } + + int ammohudup[3] = { GUN_CG, GUN_RL, GUN_GL }, + ammohuddown[3] = { GUN_RIFLE, GUN_SG, GUN_PISTOL }, + ammohudcycle[7] = { -1, -1, -1, -1, -1, -1, -1 }; + + ICOMMAND(ammohudup, "V", (tagval *args, int numargs), + { + loopi(3) ammohudup[i] = i < numargs ? getweapon(args[i].getstr()) : -1; + }); + + ICOMMAND(ammohuddown, "V", (tagval *args, int numargs), + { + loopi(3) ammohuddown[i] = i < numargs ? getweapon(args[i].getstr()) : -1; + }); + + ICOMMAND(ammohudcycle, "V", (tagval *args, int numargs), + { + loopi(7) ammohudcycle[i] = i < numargs ? getweapon(args[i].getstr()) : -1; + }); + + VARP(ammohud, 0, 1, 1); + + void drawammohud(fpsent *d) + { + float x = HICON_X + 2*HICON_STEP, y = HICON_Y, sz = HICON_SIZE; + pushhudmatrix(); + hudmatrix.scale(1/3.2f, 1/3.2f, 1); + flushhudmatrix(); + float xup = (x+sz)*3.2f, yup = y*3.2f + 0.1f*sz; + loopi(3) + { + int gun = ammohudup[i]; + if(gun < GUN_FIST || gun > GUN_PISTOL || gun == d->gunselect || !d->ammo[gun]) continue; + drawicon(HICON_FIST+gun, xup, yup, sz); + yup += sz; + } + float xdown = x*3.2f - sz, ydown = (y+sz)*3.2f - 0.1f*sz; + loopi(3) + { + int gun = ammohuddown[3-i-1]; + if(gun < GUN_FIST || gun > GUN_PISTOL || gun == d->gunselect || !d->ammo[gun]) continue; + ydown -= sz; + drawicon(HICON_FIST+gun, xdown, ydown, sz); + } + int offset = 0, num = 0; + loopi(7) + { + int gun = ammohudcycle[i]; + if(gun < GUN_FIST || gun > GUN_PISTOL) continue; + if(gun == d->gunselect) offset = i + 1; + else if(d->ammo[gun]) num++; + } + float xcycle = (x+sz/2)*3.2f + 0.5f*num*sz, ycycle = y*3.2f-sz; + loopi(7) + { + int gun = ammohudcycle[(i + offset)%7]; + if(gun < GUN_FIST || gun > GUN_PISTOL || gun == d->gunselect || !d->ammo[gun]) continue; + xcycle -= sz; + drawicon(HICON_FIST+gun, xcycle, ycycle, sz); + } + pophudmatrix(); + } + + void hudquad(float x, float y, float w, float h, float r = 1, float g = 1, float b = 1, float tx = 0, float ty = 0, float tw = 1, float th = 1) + { + gle::defvertex(2); + gle::deftexcoord0(); + gle::colorf(r, g, b); + gle::begin(GL_TRIANGLE_STRIP); + gle::attribf(x, y); gle::attribf(tx, ty); + gle::attribf(x+w, y); gle::attribf(tx + tw, ty); + gle::attribf(x, y+h); gle::attribf(tx, ty + th); + gle::attribf(x+w, y+h); gle::attribf(tx + tw, ty + th); + gle::end(); + } + + VARP(healthcolors, 0, 1, 1); + + VARP(hudhealth, 0, 1, 1); + FVARP(hudhealthx, 0, 0, 1); + FVARP(hudhealthy, 0, 1, 1); + FVARP(hudhealthscale, 0.1, 1.0, 1.0); + + void drawhudhealth(fpsent *d, int w, int h) + { + pushhudmatrix(); + hudmatrix.scale(hudhealthscale, hudhealthscale, 1); + flushhudmatrix(); + + bvec healthcolor = bvec::hexcolor(healthcolors && !m_insta ? + (d->state==CS_DEAD ? 0x808080 : + (d->health<=25 ? 0xc02020 : + (d->health<=50 ? 0xc08020 : + (d->health<=75 ? 0xc0c040 : + (d->health<=100 ? 0xFFFFFF : 0x4080c0))))) : 0xFFFFFF); + const float proportion = (w/4.0f)/600.0f; + const float healthbarw = 600*proportion; + const float healthbarh = 113*proportion; + vec2 offset = vec2(hudhealthx, hudhealthy).mul(vec2(w-healthbarw, h-healthbarh).div(hudhealthscale)); + settexture("packages/hud/health_bar_base.png"); + float hp = (float)d->health/d->maxhealth; + hudquad(offset.x, offset.y, hp*healthbarw, healthbarh, healthcolor.r, healthcolor.g, healthcolor.b, 0, 0, hp, 1); + settexture("packages/hud/health_bar_over.png"); + hudquad(offset.x, offset.y, healthbarw, healthbarh); + if (d->quadmillis) { + settexture("packages/hud/health_bar_quad.png"); + hudquad(offset.x, offset.y, healthbarw, healthbarh); + } + defformatstring(health, "%d", d->state==CS_DEAD ? 0 : d->health); + float tw=0, th=0; text_boundsf(health, tw, th); + draw_text(health, offset.x+(125*proportion-tw)/2, offset.y+(healthbarh-th)/2, healthcolor.r, healthcolor.g, healthcolor.b); + + pophudmatrix(); + } + + VARP(hudmaxhealth, 0, 1, 1); + FVARP(hudmaxhealthx, 0, 0.207, 1); + FVARP(hudmaxhealthy, 0, 0.97, 1); + FVARP(hudmaxhealthscale, 0.1, 1.0, 1.0); + + void drawhudmaxhealth(fpsent *d, int w, int h) + { + pushhudmatrix(); + hudmatrix.scale(hudmaxhealthscale, hudmaxhealthscale, 1); + flushhudmatrix(); + + const float proportion = (w/15.0f)/160.0f; + const float healthboostw = 160*proportion; + const float healthboosth = 78*proportion; + float hb = (float)d->maxhealth/100.0f-1.0f; + vec2 offset = vec2(hudmaxhealthx, hudmaxhealthy).mul(vec2(w-healthboostw, h-healthboosth).div(hudhealthscale)); + settexture("packages/hud/health_boost_base.png"); + hudquad(offset.x, offset.y, hb*healthboostw, healthboosth, 0.3f, 0.6f, 0.9f, 0, 0, hb, 1); + settexture("packages/hud/health_boost_over.png"); + hudquad(offset.x, offset.y, healthboostw, healthboosth); + if (d->quadmillis) { + settexture("packages/hud/health_boost_quad.png"); + hudquad(offset.x, offset.y, healthboostw, healthboosth); + } + + pophudmatrix(); + } + + VARP(armourcolors, 0, 1, 1); + + VARP(hudarmour, 0, 1, 1); + FVARP(hudarmourx, 0, 1, 1); + FVARP(hudarmoury, 0, 1, 1); + FVARP(hudarmourscale, 0.1, 1.0, 1.0); + + void drawhudarmour(fpsent *d, int w, int h) + { + pushhudmatrix(); + hudmatrix.scale(hudarmourscale, hudarmourscale, 1); + flushhudmatrix(); + + bvec armourcolor = bvec::hexcolor(d->armourtype == A_BLUE ? 0x83ade5 : (d->armourtype == A_GREEN ? 0x77f29e : (d->armourtype == A_YELLOW ? 0xf5f19b : 0xffffff))); + const float proportion = (w/4.0f)/600.0f; + const float armourbarw = 600*proportion; + const float armourbarh = 113*proportion; + vec2 offset = vec2(hudarmourx, hudarmoury).mul(vec2(w-armourbarw, h-armourbarh).div(hudarmourscale)); + settexture("packages/hud/armour_bar_base.png"); + float ap = (float)d->armour/d->maxarmour; + hudquad(offset.x, offset.y, ap*armourbarw, armourbarh, armourcolor.r, armourcolor.g, armourcolor.b, 0, 0, ap, 1); + settexture("packages/hud/armour_bar_over.png"); + hudquad(offset.x, offset.y, armourbarw, armourbarh); + if (d->quadmillis) { + settexture("packages/hud/armour_bar_quad.png"); + hudquad(offset.x, offset.y, armourbarw, armourbarh); + } + defformatstring(armour, "%d", d->state==CS_DEAD ? 0 : d->armour); + float tw=0, th=0; text_boundsf(armour, tw, th); + draw_text(armour, offset.x+(2*(600-63)*proportion-tw)/2, offset.y+(armourbarh-th)/2, armourcolor.r, armourcolor.g, armourcolor.b); + + pophudmatrix(); + } + + void drawhudicons(fpsent *d, int w, int h) + { + glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); + glEnable(GL_BLEND); + + if(hudhealth) drawhudhealth(d, w, h); + if(hudmaxhealth) drawhudmaxhealth(d, w, h); + if(hudarmour) drawhudarmour(d, w, h); + + if(d->state!=CS_DEAD) { + drawicon(HICON_FIST+d->gunselect, HICON_X + 2*HICON_STEP, HICON_Y); + if(ammohud) drawammohud(d); + } + } + + VARP(gameclock, 0, 0, 1); + FVARP(gameclockscale, 1e-3f, 0.75f, 1e3f); + HVARP(gameclockcolour, 0, 0xFFFFFF, 0xFFFFFF); + VARP(gameclockalpha, 0, 255, 255); + HVARP(gameclocklowcolour, 0, 0xFFC040, 0xFFFFFF); + VARP(gameclockalign, -1, 0, 1); + FVARP(gameclockx, 0, 0.50f, 1); + FVARP(gameclocky, 0, 0.03f, 1); + + void drawgameclock(int w, int h) + { + int secs = max(maplimit-lastmillis + 999, 0)/1000, mins = secs/60; + secs %= 60; + + defformatstring(buf, "%d:%02d", mins, secs); + int tw = 0, th = 0; + text_bounds(buf, tw, th); + + vec2 offset = vec2(gameclockx, gameclocky).mul(vec2(w, h).div(gameclockscale)); + if(gameclockalign == 1) offset.x -= tw; + else if(gameclockalign == 0) offset.x -= tw/2.0f; + offset.y -= th/2.0f; + + pushhudmatrix(); + hudmatrix.scale(gameclockscale, gameclockscale, 1); + flushhudmatrix(); + + int color = mins < 1 ? gameclocklowcolour : gameclockcolour; + draw_text(buf, int(offset.x), int(offset.y), (color>>16)&0xFF, (color>>8)&0xFF, color&0xFF, gameclockalpha); + + pophudmatrix(); + } + + extern int hudscore; + extern void drawhudscore(int w, int h); + + VARP(ammobar, 0, 0, 1); + VARP(ammobaralign, -1, 0, 1); + VARP(ammobarhorizontal, 0, 0, 1); + VARP(ammobarflip, 0, 0, 1); + VARP(ammobarhideempty, 0, 1, 1); + VARP(ammobarsep, 0, 20, 500); + VARP(ammobarcountsep, 0, 20, 500); + FVARP(ammobarcountscale, 0.5, 1.5, 2); + FVARP(ammobarx, 0, 0.025f, 1.0f); + FVARP(ammobary, 0, 0.5f, 1.0f); + FVARP(ammobarscale, 0.1f, 0.5f, 1.0f); + + void drawammobarcounter(const vec2 ¢er, const fpsent *p, int gun) + { + vec2 icondrawpos = vec2(center).sub(HICON_SIZE / 2); + int alpha = p->ammo[gun] ? 0xFF : 0x7F; + gle::color(bvec(0xFF, 0xFF, 0xFF), alpha); + drawicon(HICON_FIST + gun, icondrawpos.x, icondrawpos.y); + + int fw, fh; text_bounds("000", fw, fh); + float labeloffset = HICON_SIZE / 2.0f + ammobarcountsep + ammobarcountscale * (ammobarhorizontal ? fh : fw) / 2.0f; + vec2 offsetdir = (ammobarhorizontal ? vec2(0, 1) : vec2(1, 0)).mul(ammobarflip ? -1 : 1); + vec2 labelorigin = vec2(offsetdir).mul(labeloffset).add(center); + + pushhudmatrix(); + hudmatrix.translate(labelorigin.x, labelorigin.y, 0); + hudmatrix.scale(ammobarcountscale, ammobarcountscale, 1); + flushhudmatrix(); + + defformatstring(label, "%d", p->ammo[gun]); + int tw, th; text_bounds(label, tw, th); + vec2 textdrawpos = vec2(-tw, -th).div(2); + float ammoratio = (float)p->ammo[gun] / itemstats[gun-GUN_SG].add; + bvec color = bvec::hexcolor(p->ammo[gun] == 0 || ammoratio >= 1.0f ? 0xFFFFFF : (ammoratio >= 0.5f ? 0xFFC040 : 0xFF0000)); + draw_text(label, textdrawpos.x, textdrawpos.y, color.r, color.g, color.b, alpha); + + pophudmatrix(); + } + + static inline bool ammobargunvisible(const fpsent *d, int gun) + { + return d->ammo[gun] > 0 || d->gunselect == gun; + } + + void drawammobar(int w, int h, fpsent *p) + { + if(m_insta) return; + + int NUMPLAYERGUNS = GUN_PISTOL - GUN_SG + 1; + int numvisibleguns = NUMPLAYERGUNS; + if(ammobarhideempty) loopi(NUMPLAYERGUNS) if(!ammobargunvisible(p, GUN_SG + i)) numvisibleguns--; + + vec2 origin = vec2(ammobarx, ammobary).mul(vec2(w, h).div(ammobarscale)); + vec2 offsetdir = ammobarhorizontal ? vec2(1, 0) : vec2(0, 1); + float stepsize = HICON_SIZE + ammobarsep; + float initialoffset = (ammobaralign - 1) * (numvisibleguns - 1) * stepsize / 2; + + pushhudmatrix(); + hudmatrix.scale(ammobarscale, ammobarscale, 1); + flushhudmatrix(); + + int numskippedguns = 0; + loopi(NUMPLAYERGUNS) if(ammobargunvisible(p, GUN_SG + i) || !ammobarhideempty) + { + float offset = initialoffset + (i - numskippedguns) * stepsize; + vec2 drawpos = vec2(offsetdir).mul(offset).add(origin); + drawammobarcounter(drawpos, p, GUN_SG + i); + } + else numskippedguns++; + + pophudmatrix(); + } + + VARP(speedometer, 0, 1, 1); + FVARP(speedometerx, 0.0, 0.5, 1.0); + FVARP(speedometery, 0.0, 0.6, 1.0); + FVARP(speedometerscale, 0.1, 0.5, 1.0); + VARP(speedometercolor, 0, 1, 1); + FVARP(speedometeralpha, 0.0, 0.5, 1.0); + + void drawspeedometer(fpsent *d, int w, int h) + { + int speedforreal = (int) (sqrtf(d->vel.squaredlen()) + 1.0f); + speedforreal = (speedforreal == 1) ? 0 : speedforreal; + vec colour = vec(255, 255, 255); + float realw = 0; + float realh = 0; + if (speedometercolor) { + if (speedforreal==0) colour = vec(60, 60, 60); + else if (speedforreal>0 && speedforreal<=60) colour = vec(240, 30, 30); + else if (speedforreal>60 && speedforreal<=120) colour = vec(180, 90, 60); + else if (speedforreal>120 && speedforreal<=180) colour = vec(180, 180, 30); + else if (speedforreal>180 && speedforreal<=240) colour = vec(90, 180, 60); + else colour = vec(30, 240, 30); + } + + defformatstring(speedstring, "%d", speedforreal); + text_boundsf(speedstring, realw, realh); + vec2 offset = vec2(speedometerx, speedometery).mul(vec2(w, h).div(speedometerscale)); + offset.x -= realw/2.0f; + offset.y -= realh/2.0f; + + pushhudmatrix(); + hudmatrix.scale(speedometerscale, speedometerscale, 1); + flushhudmatrix(); + + const int speedow = 220; + const int speedoh = 101; + settexture("packages/hud/speedometer.png"); + hudquad(offset.x+realw/2.0f-speedow/2, offset.y+realh/2.0f-speedoh/2, speedow, speedoh); + + draw_text(speedstring, int(offset.x), int(offset.y), colour.x, colour.y, colour.z, (int)(speedometeralpha*255.0f)); + + pophudmatrix(); + } + + void gameplayhud(int w, int h) + { + pushhudmatrix(); + hudmatrix.scale(h/1800.0f, h/1800.0f, 1); + flushhudmatrix(); + + if(player1->state==CS_SPECTATOR) + { + int pw, ph, tw, th, fw, fh; + text_bounds(" ", pw, ph); + text_bounds("SPECTATOR", tw, th); + th = max(th, ph); + fpsent *f = followingplayer(); + text_bounds(f ? colorname(f) : " ", fw, fh); + fh = max(fh, ph); + draw_text("SPECTATOR", w*1800/h - tw - pw, 1650 - th - fh); + if(f) + { + int color = statuscolor(f, 0xFFFFFF); + draw_text(colorname(f), w*1800/h - fw - pw, 1650 - fh, (color>>16)&0xFF, (color>>8)&0xFF, color&0xFF); + } + } + + fpsent *d = hudplayer(); + + pophudmatrix(); + + if(d->state!=CS_EDITING && d->state!=CS_SPECTATOR) drawhudicons(d, w, h); + + if(d->state!=CS_EDITING && d->state!=CS_SPECTATOR && d->state!=CS_DEAD) + { + if(ammobar) drawammobar(w, h, d); + if(speedometer) drawspeedometer(d, w, h); + } + + + if(!m_edit) + { + if(gameclock) drawgameclock(w, h); + if(hudscore) drawhudscore(w, h); + } + + /// Frag message. + + if((hudfragmillis+fragmessageduration > lastmillis) && (lastmillis>fragmessageduration)) + { + vec2 center = vec2(0.5*w, 0.2*h); + float width = 0, height = 0; + pushhudmatrix(); + hudmatrix.scale(0.8, 0.8, 1); + flushhudmatrix(); + draw_textf("%s", center.x, center.y, hudfragger); + text_boundsf(hudfragger, width, height); + drawicon(HICON_FIST+hudfraggun, center.x+width, center.y, height); + draw_textf("%s", center.x+width+height, center.y, hudfragged); + pophudmatrix(); + } + } + + int clipconsole(int w, int h) + { + return 0; + } + + VARP(teamcrosshair, 0, 1, 1); + VARP(hitcrosshair, 0, 425, 1000); + + const char *defaultcrosshair(int index) + { + switch(index) + { + case 2: return "data/hit.png"; + case 1: return "data/teammate.png"; + default: return "data/crosshair.png"; + } + } + + int selectcrosshair(vec &color) + { + fpsent *d = hudplayer(); + if(d->state==CS_SPECTATOR || d->state==CS_DEAD) return -1; + + if(d->state!=CS_ALIVE) return 0; + + int crosshair = 0; + if(lasthit && lastmillis - lasthit < hitcrosshair) crosshair = 2; + else if(teamcrosshair) + { + dynent *o = intersectclosest(d->o, worldpos, d); + if(o && o->type==ENT_PLAYER && isteam(((fpsent *)o)->team, d->team)) + { + crosshair = 1; + color = vec(0, 0, 1); + } + } + + if(crosshair!=1 && !editmode && !m_insta) + { + if(d->health<=25) color = vec(1, 0, 0); + else if(d->health<=50) color = vec(1, 0.5f, 0); + } + if(d->gunwait) color.mul(0.5f); + return crosshair; + } + + void lighteffects(dynent *e, vec &color, vec &dir) + { #if 0 - fpsent *d = (fpsent *)e; - if(d->state!=CS_DEAD && d->quadmillis) - { - float t = 0.5f + 0.5f*sinf(2*M_PI*lastmillis/1000.0f); - color.y = color.y*(1-t) + t; - } + fpsent *d = (fpsent *)e; + if(d->state!=CS_DEAD && d->quadmillis) + { + float t = 0.5f + 0.5f*sinf(2*M_PI*lastmillis/1000.0f); + color.y = color.y*(1-t) + t; + } #endif - } - - int maxsoundradius(int n) - { - switch(n) - { - case S_JUMP: - case S_LAND: - case S_WEAPLOAD: - case S_ITEMAMMO: - case S_ITEMHEALTH: - case S_ITEMARMOUR: - case S_ITEMPUP: - case S_ITEMSPAWN: - case S_NOAMMO: - case S_PUPOUT: - return 340; - default: - return 500; - } - } - - bool serverinfostartcolumn(g3d_gui *g, int i) - { - static const char * const names[] = { "ping ", "players ", "mode ", "map ", "time ", "master ", "host ", "port ", "description " }; - static const float struts[] = { 7, 7, 12.5f, 14, 7, 8, 14, 7, 24.5f }; - if(size_t(i) >= sizeof(names)/sizeof(names[0])) return false; - g->pushlist(); - g->text(names[i], 0xFFFF80, !i ? " " : NULL); - if(struts[i]) g->strut(struts[i]); - g->mergehits(true); - return true; - } - - void serverinfoendcolumn(g3d_gui *g, int i) - { - g->mergehits(false); - g->column(i); - g->poplist(); - } - - const char *mastermodecolor(int n, const char *unknown) - { - return (n>=MM_START && size_t(n-MM_START)<sizeof(mastermodecolors)/sizeof(mastermodecolors[0])) ? mastermodecolors[n-MM_START] : unknown; - } - - const char *mastermodeicon(int n, const char *unknown) - { - return (n>=MM_START && size_t(n-MM_START)<sizeof(mastermodeicons)/sizeof(mastermodeicons[0])) ? mastermodeicons[n-MM_START] : unknown; - } - - bool serverinfoentry(g3d_gui *g, int i, const char *name, int port, const char *sdesc, const char *map, int ping, const vector<int> &attr, int np) - { - if(ping < 0 || attr.empty() || attr[0]!=PROTOCOL_VERSION) - { - switch(i) - { - case 0: - if(g->button(" ", 0xFFFFDD, "serverunk")&G3D_UP) return true; - break; - - case 1: - case 2: - case 3: - case 4: - case 5: - if(g->button(" ", 0xFFFFDD)&G3D_UP) return true; - break; - - case 6: - if(g->buttonf("%s ", 0xFFFFDD, NULL, name)&G3D_UP) return true; - break; - - case 7: - if(g->buttonf("%d ", 0xFFFFDD, NULL, port)&G3D_UP) return true; - break; - - case 8: - if(ping < 0) - { - if(g->button(sdesc, 0xFFFFDD)&G3D_UP) return true; - } - else if(g->buttonf("[%s protocol] ", 0xFFFFDD, NULL, attr.empty() ? "unknown" : (attr[0] < PROTOCOL_VERSION ? "older" : "newer"))&G3D_UP) return true; - break; - } - return false; - } - - switch(i) - { - case 0: - { - const char *icon = attr.inrange(3) && np >= attr[3] ? "serverfull" : (attr.inrange(4) ? mastermodeicon(attr[4], "serverunk") : "serverunk"); - if(g->buttonf("%d ", 0xFFFFDD, icon, ping)&G3D_UP) return true; - break; - } - - case 1: - if(attr.length()>=4) - { - if(g->buttonf(np >= attr[3] ? "\f3%d/%d " : "%d/%d ", 0xFFFFDD, NULL, np, attr[3])&G3D_UP) return true; - } - else if(g->buttonf("%d ", 0xFFFFDD, NULL, np)&G3D_UP) return true; - break; - - case 2: - if(g->buttonf("%s ", 0xFFFFDD, NULL, attr.length()>=2 ? server::modename(attr[1], "") : "")&G3D_UP) return true; - break; - - case 3: - if(g->buttonf("%.25s ", 0xFFFFDD, NULL, map)&G3D_UP) return true; - break; - - case 4: - if(attr.length()>=3 && attr[2] > 0) - { - int secs = clamp(attr[2], 0, 59*60+59), - mins = secs/60; - secs %= 60; - if(g->buttonf("%d:%02d ", 0xFFFFDD, NULL, mins, secs)&G3D_UP) return true; - } - else if(g->buttonf(" ", 0xFFFFDD)&G3D_UP) return true; - break; - case 5: - if(g->buttonf("%s%s ", 0xFFFFDD, NULL, attr.length()>=5 ? mastermodecolor(attr[4], "") : "", attr.length()>=5 ? server::mastermodename(attr[4], "") : "")&G3D_UP) return true; - break; - - case 6: - if(g->buttonf("%s ", 0xFFFFDD, NULL, name)&G3D_UP) return true; - break; - - case 7: - if(g->buttonf("%d ", 0xFFFFDD, NULL, port)&G3D_UP) return true; - break; - - case 8: - if(g->buttonf("%.25s", 0xFFFFDD, NULL, sdesc)&G3D_UP) return true; - break; - } - return false; - } - - // any data written into this vector will get saved with the map data. Must take care to do own versioning, and endianess if applicable. Will not get called when loading maps from other games, so provide defaults. - void writegamedata(vector<char> &extras) {} - void readgamedata(vector<char> &extras) {} - - const char *savedconfig() { return "config.cfg"; } - const char *restoreconfig() { return "restore.cfg"; } - const char *defaultconfig() { return "data/defaults.cfg"; } - const char *autoexec() { return "autoexec.cfg"; } - const char *savedservers() { return "servers.cfg"; } - - void loadconfigs() - { - execident("playsong"); - - execfile("auth.cfg", false); - } + } + + int maxsoundradius(int n) + { + switch(n) + { + case S_JUMP: + case S_LAND: + case S_WEAPLOAD: + case S_ITEMAMMO: + case S_ITEMHEALTH: + case S_ITEMARMOUR: + case S_ITEMPUP: + case S_ITEMSPAWN: + case S_NOAMMO: + case S_PUPOUT: + return 340; + default: + return 500; + } + } + + bool serverinfostartcolumn(g3d_gui *g, int i) + { + static const char * const names[] = { "ping ", "players ", "mode ", "map ", "time ", "master ", "host ", "port ", "description " }; + static const float struts[] = { 7, 7, 12.5f, 14, 7, 8, 14, 7, 24.5f }; + if(size_t(i) >= sizeof(names)/sizeof(names[0])) return false; + g->pushlist(); + g->text(names[i], 0xFFFF80, !i ? " " : NULL); + if(struts[i]) g->strut(struts[i]); + g->mergehits(true); + return true; + } + + void serverinfoendcolumn(g3d_gui *g, int i) + { + g->mergehits(false); + g->column(i); + g->poplist(); + } + + const char *mastermodecolor(int n, const char *unknown) + { + return (n>=MM_START && size_t(n-MM_START)<sizeof(mastermodecolors)/sizeof(mastermodecolors[0])) ? mastermodecolors[n-MM_START] : unknown; + } + + const char *mastermodeicon(int n, const char *unknown) + { + return (n>=MM_START && size_t(n-MM_START)<sizeof(mastermodeicons)/sizeof(mastermodeicons[0])) ? mastermodeicons[n-MM_START] : unknown; + } + + bool serverinfoentry(g3d_gui *g, int i, const char *name, int port, const char *sdesc, const char *map, int ping, const vector<int> &attr, int np) + { + if(ping < 0 || attr.empty() || attr[0]!=PROTOCOL_VERSION) + { + switch(i) + { + case 0: + if(g->button(" ", 0xFFFFDD, "serverunk")&G3D_UP) return true; + break; + + case 1: + case 2: + case 3: + case 4: + case 5: + if(g->button(" ", 0xFFFFDD)&G3D_UP) return true; + break; + + case 6: + if(g->buttonf("%s ", 0xFFFFDD, NULL, name)&G3D_UP) return true; + break; + + case 7: + if(g->buttonf("%d ", 0xFFFFDD, NULL, port)&G3D_UP) return true; + break; + + case 8: + if(ping < 0) + { + if(g->button(sdesc, 0xFFFFDD)&G3D_UP) return true; + } + else if(g->buttonf("[%s protocol] ", 0xFFFFDD, NULL, attr.empty() ? "unknown" : (attr[0] < PROTOCOL_VERSION ? "older" : "newer"))&G3D_UP) return true; + break; + } + return false; + } + + switch(i) + { + case 0: + { + const char *icon = attr.inrange(3) && np >= attr[3] ? "serverfull" : (attr.inrange(4) ? mastermodeicon(attr[4], "serverunk") : "serverunk"); + if(g->buttonf("%d ", 0xFFFFDD, icon, ping)&G3D_UP) return true; + break; + } + + case 1: + if(attr.length()>=4) + { + if(g->buttonf(np >= attr[3] ? "\f3%d/%d " : "%d/%d ", 0xFFFFDD, NULL, np, attr[3])&G3D_UP) return true; + } + else if(g->buttonf("%d ", 0xFFFFDD, NULL, np)&G3D_UP) return true; + break; + + case 2: + if(g->buttonf("%s ", 0xFFFFDD, NULL, attr.length()>=2 ? server::modename(attr[1], "") : "")&G3D_UP) return true; + break; + + case 3: + if(g->buttonf("%.25s ", 0xFFFFDD, NULL, map)&G3D_UP) return true; + break; + + case 4: + if(attr.length()>=3 && attr[2] > 0) + { + int secs = clamp(attr[2], 0, 59*60+59), + mins = secs/60; + secs %= 60; + if(g->buttonf("%d:%02d ", 0xFFFFDD, NULL, mins, secs)&G3D_UP) return true; + } + else if(g->buttonf(" ", 0xFFFFDD)&G3D_UP) return true; + break; + case 5: + if(g->buttonf("%s%s ", 0xFFFFDD, NULL, attr.length()>=5 ? mastermodecolor(attr[4], "") : "", attr.length()>=5 ? server::mastermodename(attr[4], "") : "")&G3D_UP) return true; + break; + + case 6: + if(g->buttonf("%s ", 0xFFFFDD, NULL, name)&G3D_UP) return true; + break; + + case 7: + if(g->buttonf("%d ", 0xFFFFDD, NULL, port)&G3D_UP) return true; + break; + + case 8: + if(g->buttonf("%.25s", 0xFFFFDD, NULL, sdesc)&G3D_UP) return true; + break; + } + return false; + } + + // any data written into this vector will get saved with the map data. Must take care to do own versioning, and endianess if applicable. Will not get called when loading maps from other games, so provide defaults. + void writegamedata(vector<char> &extras) {} + void readgamedata(vector<char> &extras) {} + + const char *savedconfig() { return "config.cfg"; } + const char *restoreconfig() { return "restore.cfg"; } + const char *defaultconfig() { return "data/defaults.cfg"; } + const char *autoexec() { return "autoexec.cfg"; } + const char *savedservers() { return "servers.cfg"; } + + void loadconfigs() + { + execident("playsong"); + + execfile("auth.cfg", false); + } } diff --git a/src/fpsgame/game.h b/src/fpsgame/game.h index d99bf70..0495502 100644 --- a/src/fpsgame/game.h +++ b/src/fpsgame/game.h @@ -7,123 +7,123 @@ enum { - CON_CHAT = 1<<8, - CON_TEAMCHAT = 1<<9, - CON_GAMEINFO = 1<<10, - CON_FRAG_SELF = 1<<11, - CON_FRAG_OTHER = 1<<12, - CON_TEAMKILL = 1<<13 + CON_CHAT = 1<<8, + CON_TEAMCHAT = 1<<9, + CON_GAMEINFO = 1<<10, + CON_FRAG_SELF = 1<<11, + CON_FRAG_OTHER = 1<<12, + CON_TEAMKILL = 1<<13 }; // network quantization scale -#define DMF 16.0f // for world locations -#define DNF 100.0f // for normalized vectors -#define DVELF 1.0f // for playerspeed based velocity vectors +#define DMF 16.0f // for world locations +#define DNF 100.0f // for normalized vectors +#define DVELF 1.0f // for playerspeed based velocity vectors -enum // static entity types +enum // static entity types { - NOTUSED = ET_EMPTY, // entity slot not in use in map - LIGHT = ET_LIGHT, // lightsource, attr1 = radius, attr2 = intensity - MAPMODEL = ET_MAPMODEL, // attr1 = angle, attr2 = idx - PLAYERSTART, // attr1 = angle, attr2 = team - ENVMAP = ET_ENVMAP, // attr1 = radius - PARTICLES = ET_PARTICLES, - MAPSOUND = ET_SOUND, - SPOTLIGHT = ET_SPOTLIGHT, - I_SHELLS, I_BULLETS, I_ROCKETS, I_ROUNDS, I_GRENADES, I_CARTRIDGES, - I_HEALTH, I_BOOST, - /**/I_TINYHEALTH, I_TINYARMOUR, - I_GREENARMOUR, I_YELLOWARMOUR, - I_QUAD, - TELEPORT, // attr1 = idx, attr2 = model, attr3 = tag - TELEDEST, // attr1 = angle, attr2 = idx - JUMPPAD, // attr1 = zpush, attr2 = ypush, attr3 = xpush - MAXENTTYPES + NOTUSED = ET_EMPTY, // entity slot not in use in map + LIGHT = ET_LIGHT, // lightsource, attr1 = radius, attr2 = intensity + MAPMODEL = ET_MAPMODEL, // attr1 = angle, attr2 = idx + PLAYERSTART, // attr1 = angle, attr2 = team + ENVMAP = ET_ENVMAP, // attr1 = radius + PARTICLES = ET_PARTICLES, + MAPSOUND = ET_SOUND, + SPOTLIGHT = ET_SPOTLIGHT, + I_SHELLS, I_BULLETS, I_ROCKETS, I_ROUNDS, I_GRENADES, I_CARTRIDGES, + I_HEALTH, I_BOOST, + /**/I_TINYHEALTH, I_TINYARMOUR, + I_GREENARMOUR, I_YELLOWARMOUR, + I_QUAD, + TELEPORT, // attr1 = idx, attr2 = model, attr3 = tag + TELEDEST, // attr1 = angle, attr2 = idx + JUMPPAD, // attr1 = zpush, attr2 = ypush, attr3 = xpush + MAXENTTYPES }; enum { GUN_FIST = 0, GUN_SG, GUN_CG, GUN_RL, GUN_RIFLE, GUN_GL, GUN_PISTOL, NUMGUNS }; -enum { A_BLUE, A_GREEN, A_YELLOW }; // armour types... take 20/40/60 % off +enum { A_BLUE, A_GREEN, A_YELLOW }; // armour types... take 20/40/60 % off enum { - M_TEAM = 1<<0, - M_NOITEMS = 1<<1, - M_NOAMMO = 1<<2, - M_INSTA = 1<<3, - M_EFFICIENCY = 1<<4, - M_EDIT = 1<<12, - M_DEMO = 1<<13, - M_LOCAL = 1<<14, - M_LOBBY = 1<<15 + M_TEAM = 1<<0, + M_NOITEMS = 1<<1, + M_NOAMMO = 1<<2, + M_INSTA = 1<<3, + M_EFFICIENCY = 1<<4, + M_EDIT = 1<<12, + M_DEMO = 1<<13, + M_LOCAL = 1<<14, + M_LOBBY = 1<<15 }; static struct gamemodeinfo { - const char *name; - int flags; - const char *info; + const char *name; + int flags; + const char *info; } gamemodes[] = { - { "demo", M_DEMO | M_LOCAL, NULL}, - { "ffa", M_LOBBY, "Free For All: Collect items for ammo. Frag everyone to score points." }, - { "coop edit", M_EDIT, "Cooperative Editing: Edit maps with multiple players simultaneously." }, - { "teamplay", M_TEAM, "Teamplay: Collect items for ammo. Frag \fs\f3the enemy team\fr to score points for \fs\f1your team\fr." }, - { "instagib", M_NOITEMS | M_INSTA, "Instagib: You spawn with full rifle ammo and die instantly from one shot. There are no items. Frag everyone to score points." }, - { "insta team", M_NOITEMS | M_INSTA | M_TEAM, "Instagib Team: You spawn with full rifle ammo and die instantly from one shot. There are no items. Frag \fs\f3the enemy team\fr to score points for \fs\f1your team\fr." }, - { "efficiency", M_NOITEMS | M_EFFICIENCY, "Efficiency: You spawn with all weapons and armour. There are no items. Frag everyone to score points." }, - { "effic team", M_NOITEMS | M_EFFICIENCY | M_TEAM, "Efficiency Team: You spawn with all weapons and armour. There are no items. Frag \fs\f3the enemy team\fr to score points for \fs\f1your team\fr." }, + { "demo", M_DEMO | M_LOCAL, NULL}, + { "ffa", M_LOBBY, "Free For All: Collect items for ammo. Frag everyone to score points." }, + { "coop edit", M_EDIT, "Cooperative Editing: Edit maps with multiple players simultaneously." }, + { "teamplay", M_TEAM, "Teamplay: Collect items for ammo. Frag \fs\f3the enemy team\fr to score points for \fs\f1your team\fr." }, + { "instagib", M_NOITEMS | M_INSTA, "Instagib: You spawn with full rifle ammo and die instantly from one shot. There are no items. Frag everyone to score points." }, + { "insta team", M_NOITEMS | M_INSTA | M_TEAM, "Instagib Team: You spawn with full rifle ammo and die instantly from one shot. There are no items. Frag \fs\f3the enemy team\fr to score points for \fs\f1your team\fr." }, + { "efficiency", M_NOITEMS | M_EFFICIENCY, "Efficiency: You spawn with all weapons and armour. There are no items. Frag everyone to score points." }, + { "effic team", M_NOITEMS | M_EFFICIENCY | M_TEAM, "Efficiency Team: You spawn with all weapons and armour. There are no items. Frag \fs\f3the enemy team\fr to score points for \fs\f1your team\fr." }, }; #define STARTGAMEMODE (-1) #define NUMGAMEMODES ((int)(sizeof(gamemodes)/sizeof(gamemodes[0]))) -#define m_valid(mode) ((mode) >= STARTGAMEMODE && (mode) < STARTGAMEMODE + NUMGAMEMODES) -#define m_check(mode, flag) (m_valid(mode) && gamemodes[(mode) - STARTGAMEMODE].flags&(flag)) +#define m_valid(mode) ((mode) >= STARTGAMEMODE && (mode) < STARTGAMEMODE + NUMGAMEMODES) +#define m_check(mode, flag) (m_valid(mode) && gamemodes[(mode) - STARTGAMEMODE].flags&(flag)) #define m_checknot(mode, flag) (m_valid(mode) && !(gamemodes[(mode) - STARTGAMEMODE].flags&(flag))) #define m_checkall(mode, flag) (m_valid(mode) && (gamemodes[(mode) - STARTGAMEMODE].flags&(flag)) == (flag)) #define m_checkonly(mode, flag, exclude) (m_valid(mode) && (gamemodes[(mode) - STARTGAMEMODE].flags&((flag)|(exclude))) == (flag)) -#define m_noitems (m_check(gamemode, M_NOITEMS)) -#define m_noammo (m_check(gamemode, M_NOAMMO|M_NOITEMS)) -#define m_insta (m_check(gamemode, M_INSTA)) +#define m_noitems (m_check(gamemode, M_NOITEMS)) +#define m_noammo (m_check(gamemode, M_NOAMMO|M_NOITEMS)) +#define m_insta (m_check(gamemode, M_INSTA)) #define m_efficiency (m_check(gamemode, M_EFFICIENCY)) -#define m_teammode (m_check(gamemode, M_TEAM)) -#define isteam(a,b) (m_teammode && strcmp(a, b)==0) +#define m_teammode (m_check(gamemode, M_TEAM)) +#define isteam(a,b) (m_teammode && strcmp(a, b)==0) -#define m_demo (m_check(gamemode, M_DEMO)) -#define m_edit (m_check(gamemode, M_EDIT)) -#define m_lobby (m_check(gamemode, M_LOBBY)) -#define m_timed (m_checknot(gamemode, M_DEMO|M_EDIT|M_LOCAL)) -#define m_botmode (m_checknot(gamemode, M_DEMO|M_LOCAL)) -#define m_mp(mode) (m_checknot(mode, M_LOCAL)) +#define m_demo (m_check(gamemode, M_DEMO)) +#define m_edit (m_check(gamemode, M_EDIT)) +#define m_lobby (m_check(gamemode, M_LOBBY)) +#define m_timed (m_checknot(gamemode, M_DEMO|M_EDIT|M_LOCAL)) +#define m_botmode (m_checknot(gamemode, M_DEMO|M_LOCAL)) +#define m_mp(mode) (m_checknot(mode, M_LOCAL)) enum { MM_AUTH = -1, MM_OPEN = 0, MM_VETO, MM_LOCKED, MM_PRIVATE, MM_PASSWORD, MM_START = MM_AUTH }; -static const char * const mastermodenames[] = { "auth", "open", "veto", "locked", "private", "password" }; -static const char * const mastermodecolors[] = { "", "\f0", "\f2", "\f2", "\f3", "\f3" }; +static const char * const mastermodenames[] = { "auth", "open", "veto", "locked", "private", "password" }; +static const char * const mastermodecolors[] = { "", "\f0", "\f2", "\f2", "\f3", "\f3" }; static const char * const mastermodeicons[] = { "server", "server", "serverlock", "serverlock", "serverpriv", "serverpriv" }; // hardcoded sounds, defined in sounds.cfg enum { - S_JUMP = 0, S_LAND, S_RIFLE, S_PUNCH1, S_SG, S_CG, - S_RLFIRE, S_RLHIT, S_WEAPLOAD, S_ITEMAMMO, S_ITEMHEALTH, - S_ITEMARMOUR, S_ITEMPUP, S_ITEMSPAWN, S_TELEPORT, S_NOAMMO, S_PUPOUT, - S_PAIN1, S_PAIN2, S_PAIN3, S_PAIN4, S_PAIN5, S_PAIN6, - S_DIE1, S_DIE2, - S_FLAUNCH, S_FEXPLODE, - S_SPLASH1, S_SPLASH2, - S_JUMPPAD, S_PISTOL, - - S_V_FIGHT, - S_V_BOOST, S_V_BOOST10, - S_V_QUAD, S_V_QUAD10, - - S_BURN, - S_CHAINSAW_ATTACK, - S_CHAINSAW_IDLE, - - S_HIT + S_JUMP = 0, S_LAND, S_RIFLE, S_PUNCH1, S_SG, S_CG, + S_RLFIRE, S_RLHIT, S_WEAPLOAD, S_ITEMAMMO, S_ITEMHEALTH, + S_ITEMARMOUR, S_ITEMPUP, S_ITEMSPAWN, S_TELEPORT, S_NOAMMO, S_PUPOUT, + S_PAIN1, S_PAIN2, S_PAIN3, S_PAIN4, S_PAIN5, S_PAIN6, + S_DIE1, S_DIE2, + S_FLAUNCH, S_FEXPLODE, + S_SPLASH1, S_SPLASH2, + S_JUMPPAD, S_PISTOL, + + S_V_FIGHT, + S_V_BOOST, S_V_BOOST10, + S_V_QUAD, S_V_QUAD10, + + S_BURN, + S_CHAINSAW_ATTACK, + S_CHAINSAW_IDLE, + + S_HIT }; // network messages codes, c2s, c2c, s2c @@ -132,72 +132,72 @@ enum { PRIV_NONE = 0, PRIV_MASTER, PRIV_AUTH, PRIV_ADMIN }; enum { - N_CONNECT = 0, N_SERVINFO, N_WELCOME, N_INITCLIENT, N_POS, N_TEXT, N_SOUND, N_CDIS, - N_SHOOT, N_EXPLODE, N_SUICIDE, - N_DIED, N_DAMAGE, N_HITPUSH, N_SHOTFX, N_EXPLODEFX, - N_TRYSPAWN, N_SPAWNSTATE, N_SPAWN, N_FORCEDEATH, - N_GUNSELECT, N_TAUNT, - N_MAPCHANGE, N_MAPVOTE, N_TEAMINFO, N_ITEMSPAWN, N_ITEMPICKUP, N_ITEMACC, N_TELEPORT, N_JUMPPAD, - N_PING, N_PONG, N_CLIENTPING, - N_TIMEUP, N_FORCEINTERMISSION, - N_SERVMSG, N_ITEMLIST, N_RESUME, - N_EDITMODE, N_EDITENT, N_EDITF, N_EDITT, N_EDITM, N_FLIP, N_COPY, N_PASTE, N_ROTATE, N_REPLACE, N_DELCUBE, N_REMIP, N_EDITVSLOT, N_UNDO, N_REDO, N_NEWMAP, N_GETMAP, N_SENDMAP, N_CLIPBOARD, N_EDITVAR, - N_MASTERMODE, N_KICK, N_CLEARBANS, N_CURRENTMASTER, N_SPECTATOR, N_SETMASTER, N_SETTEAM, N_ANNOUNCE, - N_LISTDEMOS, N_SENDDEMOLIST, N_GETDEMO, N_SENDDEMO, - N_DEMOPLAYBACK, N_RECORDDEMO, N_STOPDEMO, N_CLEARDEMOS, - N_SAYTEAM, - N_CLIENT, - N_AUTHTRY, N_AUTHKICK, N_AUTHCHAL, N_AUTHANS, N_REQAUTH, - N_PAUSEGAME, N_GAMESPEED, - N_ADDBOT, N_DELBOT, N_INITAI, N_FROMAI, N_BOTLIMIT, N_BOTBALANCE, - N_MAPCRC, N_CHECKMAPS, - N_SWITCHNAME, N_SWITCHMODEL, N_SWITCHTEAM, - N_INITTOKENS, N_TAKETOKEN, N_EXPIRETOKENS, N_DROPTOKENS, N_DEPOSITTOKENS, N_STEALTOKENS, - N_SERVCMD, - N_DEMOPACKET, - NUMMSG + N_CONNECT = 0, N_SERVINFO, N_WELCOME, N_INITCLIENT, N_POS, N_TEXT, N_SOUND, N_CDIS, + N_SHOOT, N_EXPLODE, N_SUICIDE, + N_DIED, N_DAMAGE, N_HITPUSH, N_SHOTFX, N_EXPLODEFX, + N_TRYSPAWN, N_SPAWNSTATE, N_SPAWN, N_FORCEDEATH, + N_GUNSELECT, N_TAUNT, + N_MAPCHANGE, N_MAPVOTE, N_TEAMINFO, N_ITEMSPAWN, N_ITEMPICKUP, N_ITEMACC, N_TELEPORT, N_JUMPPAD, + N_PING, N_PONG, N_CLIENTPING, + N_TIMEUP, N_FORCEINTERMISSION, + N_SERVMSG, N_ITEMLIST, N_RESUME, + N_EDITMODE, N_EDITENT, N_EDITF, N_EDITT, N_EDITM, N_FLIP, N_COPY, N_PASTE, N_ROTATE, N_REPLACE, N_DELCUBE, N_REMIP, N_EDITVSLOT, N_UNDO, N_REDO, N_NEWMAP, N_GETMAP, N_SENDMAP, N_CLIPBOARD, N_EDITVAR, + N_MASTERMODE, N_KICK, N_CLEARBANS, N_CURRENTMASTER, N_SPECTATOR, N_SETMASTER, N_SETTEAM, N_ANNOUNCE, + N_LISTDEMOS, N_SENDDEMOLIST, N_GETDEMO, N_SENDDEMO, + N_DEMOPLAYBACK, N_RECORDDEMO, N_STOPDEMO, N_CLEARDEMOS, + N_SAYTEAM, + N_CLIENT, + N_AUTHTRY, N_AUTHKICK, N_AUTHCHAL, N_AUTHANS, N_REQAUTH, + N_PAUSEGAME, N_GAMESPEED, + N_ADDBOT, N_DELBOT, N_INITAI, N_FROMAI, N_BOTLIMIT, N_BOTBALANCE, + N_MAPCRC, N_CHECKMAPS, + N_SWITCHNAME, N_SWITCHMODEL, N_SWITCHTEAM, + N_INITTOKENS, N_TAKETOKEN, N_EXPIRETOKENS, N_DROPTOKENS, N_DEPOSITTOKENS, N_STEALTOKENS, + N_SERVCMD, + N_DEMOPACKET, + NUMMSG }; -static const int msgsizes[] = // size inclusive message token, 0 for variable or not-checked sizes +static const int msgsizes[] = // size inclusive message token, 0 for variable or not-checked sizes { - N_CONNECT, 0, N_SERVINFO, 0, N_WELCOME, 1, N_INITCLIENT, 0, N_POS, 0, N_TEXT, 0, N_SOUND, 2, N_CDIS, 2, - N_SHOOT, 0, N_EXPLODE, 0, N_SUICIDE, 1, - N_DIED, 5, N_DAMAGE, 6, N_HITPUSH, 7, N_SHOTFX, 10, N_EXPLODEFX, 4, - N_TRYSPAWN, 1, N_SPAWNSTATE, 14, N_SPAWN, 3, N_FORCEDEATH, 2, - N_GUNSELECT, 2, N_TAUNT, 1, - N_MAPCHANGE, 0, N_MAPVOTE, 0, N_TEAMINFO, 0, N_ITEMSPAWN, 2, N_ITEMPICKUP, 2, N_ITEMACC, 3, - N_PING, 2, N_PONG, 2, N_CLIENTPING, 2, - N_TIMEUP, 2, N_FORCEINTERMISSION, 1, - N_SERVMSG, 0, N_ITEMLIST, 0, N_RESUME, 0, - N_EDITMODE, 2, N_EDITENT, 11, N_EDITF, 16, N_EDITT, 16, N_EDITM, 16, N_FLIP, 14, N_COPY, 14, N_PASTE, 14, N_ROTATE, 15, N_REPLACE, 17, N_DELCUBE, 14, N_REMIP, 1, N_EDITVSLOT, 16, N_UNDO, 0, N_REDO, 0, N_NEWMAP, 2, N_GETMAP, 1, N_SENDMAP, 0, N_EDITVAR, 0, - N_MASTERMODE, 2, N_KICK, 0, N_CLEARBANS, 1, N_CURRENTMASTER, 0, N_SPECTATOR, 3, N_SETMASTER, 0, N_SETTEAM, 0, N_ANNOUNCE, 2, - N_LISTDEMOS, 1, N_SENDDEMOLIST, 0, N_GETDEMO, 3, N_SENDDEMO, 0, - N_DEMOPLAYBACK, 3, N_RECORDDEMO, 2, N_STOPDEMO, 1, N_CLEARDEMOS, 2, - N_SAYTEAM, 0, - N_CLIENT, 0, - N_AUTHTRY, 0, N_AUTHKICK, 0, N_AUTHCHAL, 0, N_AUTHANS, 0, N_REQAUTH, 0, - N_PAUSEGAME, 0, N_GAMESPEED, 0, - N_ADDBOT, 2, N_DELBOT, 1, N_INITAI, 0, N_FROMAI, 2, N_BOTLIMIT, 2, N_BOTBALANCE, 2, - N_MAPCRC, 0, N_CHECKMAPS, 1, - N_SWITCHNAME, 0, N_SWITCHMODEL, 2, N_SWITCHTEAM, 0, - N_INITTOKENS, 0, N_TAKETOKEN, 2, N_EXPIRETOKENS, 0, N_DROPTOKENS, 0, N_DEPOSITTOKENS, 2, N_STEALTOKENS, 0, - N_SERVCMD, 0, - N_DEMOPACKET, 0, - -1 + N_CONNECT, 0, N_SERVINFO, 0, N_WELCOME, 1, N_INITCLIENT, 0, N_POS, 0, N_TEXT, 0, N_SOUND, 2, N_CDIS, 2, + N_SHOOT, 0, N_EXPLODE, 0, N_SUICIDE, 1, + N_DIED, 5, N_DAMAGE, 6, N_HITPUSH, 7, N_SHOTFX, 10, N_EXPLODEFX, 4, + N_TRYSPAWN, 1, N_SPAWNSTATE, 14, N_SPAWN, 3, N_FORCEDEATH, 2, + N_GUNSELECT, 2, N_TAUNT, 1, + N_MAPCHANGE, 0, N_MAPVOTE, 0, N_TEAMINFO, 0, N_ITEMSPAWN, 2, N_ITEMPICKUP, 2, N_ITEMACC, 3, + N_PING, 2, N_PONG, 2, N_CLIENTPING, 2, + N_TIMEUP, 2, N_FORCEINTERMISSION, 1, + N_SERVMSG, 0, N_ITEMLIST, 0, N_RESUME, 0, + N_EDITMODE, 2, N_EDITENT, 11, N_EDITF, 16, N_EDITT, 16, N_EDITM, 16, N_FLIP, 14, N_COPY, 14, N_PASTE, 14, N_ROTATE, 15, N_REPLACE, 17, N_DELCUBE, 14, N_REMIP, 1, N_EDITVSLOT, 16, N_UNDO, 0, N_REDO, 0, N_NEWMAP, 2, N_GETMAP, 1, N_SENDMAP, 0, N_EDITVAR, 0, + N_MASTERMODE, 2, N_KICK, 0, N_CLEARBANS, 1, N_CURRENTMASTER, 0, N_SPECTATOR, 3, N_SETMASTER, 0, N_SETTEAM, 0, N_ANNOUNCE, 2, + N_LISTDEMOS, 1, N_SENDDEMOLIST, 0, N_GETDEMO, 3, N_SENDDEMO, 0, + N_DEMOPLAYBACK, 3, N_RECORDDEMO, 2, N_STOPDEMO, 1, N_CLEARDEMOS, 2, + N_SAYTEAM, 0, + N_CLIENT, 0, + N_AUTHTRY, 0, N_AUTHKICK, 0, N_AUTHCHAL, 0, N_AUTHANS, 0, N_REQAUTH, 0, + N_PAUSEGAME, 0, N_GAMESPEED, 0, + N_ADDBOT, 2, N_DELBOT, 1, N_INITAI, 0, N_FROMAI, 2, N_BOTLIMIT, 2, N_BOTBALANCE, 2, + N_MAPCRC, 0, N_CHECKMAPS, 1, + N_SWITCHNAME, 0, N_SWITCHMODEL, 2, N_SWITCHTEAM, 0, + N_INITTOKENS, 0, N_TAKETOKEN, 2, N_EXPIRETOKENS, 0, N_DROPTOKENS, 0, N_DEPOSITTOKENS, 2, N_STEALTOKENS, 0, + N_SERVCMD, 0, + N_DEMOPACKET, 0, + -1 }; #define SAUERBRATEN_LANINFO_PORT 28784 #define SAUERBRATEN_SERVER_PORT 28785 #define SAUERBRATEN_SERVINFO_PORT 28786 #define SAUERBRATEN_MASTER_PORT 28787 -#define PROTOCOL_VERSION 260 // bump when protocol changes -#define DEMO_VERSION 1 // bump when demo format changes +#define PROTOCOL_VERSION 260 // bump when protocol changes +#define DEMO_VERSION 1 // bump when demo format changes #define DEMO_MAGIC "SAUERBRATEN_DEMO" struct demoheader { - char magic[16]; - int version, protocol; + char magic[16]; + int version, protocol; }; #define MAXNAMELEN 15 @@ -205,50 +205,50 @@ struct demoheader enum { - HICON_BLUE_ARMOUR = 0, - HICON_GREEN_ARMOUR, - HICON_YELLOW_ARMOUR, - - HICON_HEALTH, - - HICON_FIST, - HICON_SG, - HICON_CG, - HICON_RL, - HICON_RIFLE, - HICON_GL, - HICON_PISTOL, - - HICON_QUAD, - - HICON_TOKEN, - - HICON_X = 20, - HICON_Y = 1650, - HICON_TEXTY = 1644, - HICON_STEP = 490, - HICON_SIZE = 120, - HICON_SPACE = 40 + HICON_BLUE_ARMOUR = 0, + HICON_GREEN_ARMOUR, + HICON_YELLOW_ARMOUR, + + HICON_HEALTH, + + HICON_FIST, + HICON_SG, + HICON_CG, + HICON_RL, + HICON_RIFLE, + HICON_GL, + HICON_PISTOL, + + HICON_QUAD, + + HICON_TOKEN, + + HICON_X = 20, + HICON_Y = 1650, + HICON_TEXTY = 1644, + HICON_STEP = 490, + HICON_SIZE = 120, + HICON_SPACE = 40 }; static struct itemstat { - int add, max, sound; - const char *name; - int icon, info; + int add, max, sound; + const char *name; + int icon, info; } itemstats[] = { - {10, 30, S_ITEMAMMO, "SG", HICON_SG, GUN_SG}, - {20, 60, S_ITEMAMMO, "CG", HICON_CG, GUN_CG}, - {5, 15, S_ITEMAMMO, "RL", HICON_RL, GUN_RL}, - {5, 15, S_ITEMAMMO, "RI", HICON_RIFLE, GUN_RIFLE}, - {10, 30, S_ITEMAMMO, "GL", HICON_GL, GUN_GL}, - {30, 120, S_ITEMAMMO, "PI", HICON_PISTOL, GUN_PISTOL}, - {25, 100, S_ITEMHEALTH, "H", HICON_HEALTH, -1}, - {100, 200, S_ITEMHEALTH, "MH", HICON_HEALTH, 50}, - {5, 100, S_ITEMHEALTH, "TH", HICON_HEALTH, -1}, - {5, 50, S_ITEMARMOUR, "TA", HICON_BLUE_ARMOUR, A_BLUE}, - {50, 100, S_ITEMARMOUR, "GA", HICON_GREEN_ARMOUR, A_GREEN}, - {100, 200, S_ITEMARMOUR, "YA", HICON_YELLOW_ARMOUR, A_YELLOW}, - {20000, 30000, S_ITEMPUP, "Q", HICON_QUAD, -1}, + {10, 30, S_ITEMAMMO, "SG", HICON_SG, GUN_SG}, + {20, 60, S_ITEMAMMO, "CG", HICON_CG, GUN_CG}, + {5, 15, S_ITEMAMMO, "RL", HICON_RL, GUN_RL}, + {5, 15, S_ITEMAMMO, "RI", HICON_RIFLE, GUN_RIFLE}, + {10, 30, S_ITEMAMMO, "GL", HICON_GL, GUN_GL}, + {30, 120, S_ITEMAMMO, "PI", HICON_PISTOL, GUN_PISTOL}, + {25, 100, S_ITEMHEALTH, "H", HICON_HEALTH, -1}, + {100, 200, S_ITEMHEALTH, "MH", HICON_HEALTH, 50}, + {5, 100, S_ITEMHEALTH, "TH", HICON_HEALTH, -1}, + {5, 50, S_ITEMARMOUR, "TA", HICON_BLUE_ARMOUR, A_BLUE}, + {50, 100, S_ITEMARMOUR, "GA", HICON_GREEN_ARMOUR, A_GREEN}, + {100, 200, S_ITEMARMOUR, "YA", HICON_YELLOW_ARMOUR, A_YELLOW}, + {20000, 30000, S_ITEMPUP, "Q", HICON_QUAD, -1}, }; #define MAXRAYS 12 @@ -257,18 +257,18 @@ static struct itemstat { #define EXP_DISTSCALE 1.5f static const struct guninfo { - int sound, attackdelay, damage, spread, projspeed, kickamount, range, rays, hitpush, exprad, ttl; - const char *name, *file; - short part; + int sound, attackdelay, damage, spread, projspeed, kickamount, range, rays, hitpush, exprad, ttl; + const char *name, *file; + short part; } guns[NUMGUNS] = { - // delay| dmg| spr| spd| kck| rng| ray| pus| exp| - { S_PUNCH1, 100, 30, 0, 0, 0, 30, 1, 80, 0, 0, "fist", "fist", 0 }, - { S_SG, 1000, 20, 280, 0, 20, 1024, MAXRAYS, 100, 0, 0, "shotgun", "shotg", 0 }, - { S_CG, 100, 20, 70, 0, 10, 1024, 1, 80, 0, 0, "chaingun", "chaing", 0 }, - { S_RLFIRE, 800, 120, 0, 270, 10, 1024, 1, 240, 40, 0, "rocketlauncher", "rocket", 0 }, - { S_RIFLE, 1200, 120, 0, 0, 30, 2048, 1, 120, 0, 0, "rifle", "rifle", 0 }, - { S_FLAUNCH, 600, 90, 0, 300, 20, 1024, 1, 160, 45, 1500, "grenadelauncher", "gl", 0 }, - { S_PISTOL, 400, 60, 110, 0, 10, 1024, 1, 80, 0, 0, "pistol", "pistol", 0 }, + // delay| dmg| spr| spd| kck| rng| ray| pus| exp| + { S_PUNCH1, 100, 30, 0, 0, 0, 30, 1, 80, 0, 0, "fist", "fist", 0 }, + { S_SG, 1000, 20, 280, 0, 20, 1024, MAXRAYS, 100, 0, 0, "shotgun", "shotg", 0 }, + { S_CG, 100, 20, 70, 0, 10, 1024, 1, 80, 0, 0, "chaingun", "chaing", 0 }, + { S_RLFIRE, 800, 120, 0, 270, 10, 1024, 1, 240, 40, 0, "rocketlauncher", "rocket", 0 }, + { S_RIFLE, 1200, 120, 0, 0, 30, 2048, 1, 120, 0, 0, "rifle", "rifle", 0 }, + { S_FLAUNCH, 600, 90, 0, 300, 20, 1024, 1, 160, 45, 1500, "grenadelauncher", "gl", 0 }, + { S_PISTOL, 400, 60, 110, 0, 10, 1024, 1, 80, 0, 0, "pistol", "pistol", 0 }, }; /// Rough accuracy code, client-side only. @@ -290,243 +290,243 @@ extern void pwreset(void); // inherited by fpsent and server clients struct fpsstate { - int health, maxhealth; - int armour, maxarmour, armourtype; - int quadmillis; - int gunselect, gunwait; - int ammo[NUMGUNS]; - int aitype, skill; - - fpsstate() : maxhealth(100), maxarmour(50), aitype(AI_NONE), skill(0) {} - - void baseammo(int gun, int k = 2, int scale = 1) - { - ammo[gun] = (itemstats[gun-GUN_SG].add*k)/scale; - } - - void addammo(int gun, int k = 1, int scale = 1) - { - itemstat &is = itemstats[gun-GUN_SG]; - ammo[gun] = min(ammo[gun] + (is.add*k)/scale, is.max); - } - - bool hasmaxammo(int type) - { - const itemstat &is = itemstats[type-I_SHELLS]; - return ammo[type-I_SHELLS+GUN_SG]>=is.max; - } - - bool canpickup(int type) - { - if(type<I_SHELLS || type>I_QUAD) return false; - itemstat &is = itemstats[type-I_SHELLS]; - switch(type) - { - case I_BOOST: return maxhealth<is.max || health<maxhealth; - case I_TINYHEALTH: return health<maxhealth; - case I_HEALTH: return health<maxhealth; - - case I_TINYARMOUR: - [[fallthrough]]; - case I_GREENARMOUR: - [[fallthrough]]; - case I_YELLOWARMOUR: return maxarmour<is.max || armour<maxarmour; - - case I_QUAD: return quadmillis<is.max; - default: return ammo[is.info]<is.max; - } - } - - void pickup(int type) - { - if(type<I_SHELLS || type>I_QUAD) return; - itemstat &is = itemstats[type-I_SHELLS]; - switch(type) - { - case I_TINYHEALTH: - health = min(health+is.add, maxhealth); - break; - case I_BOOST: - maxhealth = min(maxhealth+is.info, is.max); - [[fallthrough]]; - case I_HEALTH: // boost also adds to health - health = min(health+is.add, maxhealth); - break; - case I_TINYARMOUR: - [[fallthrough]]; - case I_GREENARMOUR: - [[fallthrough]]; - case I_YELLOWARMOUR: - maxarmour = max(maxarmour, is.max); - armour = min(armour+is.add, maxarmour); - armourtype = is.info; - break; - case I_QUAD: - quadmillis = min(quadmillis+is.add, is.max); - break; - default: - ammo[is.info] = min(ammo[is.info]+is.add, is.max); - break; - } - } - - void respawn() - { - maxhealth = 100; - health = maxhealth; - maxarmour = 50; - armour = 0; - armourtype = A_BLUE; - quadmillis = 0; - gunselect = GUN_PISTOL; - gunwait = 0; - loopi(NUMGUNS) ammo[i] = 0; - ammo[GUN_FIST] = 1; - } - - void spawnstate(int gamemode) - { - if(m_demo) - { - gunselect = GUN_FIST; - } - else if(m_insta) - { - armour = 0; - health = 1; - gunselect = GUN_RIFLE; - ammo[GUN_RIFLE] = 100; - } - else if(m_efficiency) - { - armourtype = A_GREEN; - armour = 100; - loopi(NUMGUNS-1) baseammo(i+1); - gunselect = GUN_CG; - ammo[GUN_CG] /= 2; - } - else - { - armourtype = A_BLUE; - armour = 25; - ammo[GUN_PISTOL] = 40; - } - } - - // just subtract damage here, can set death, etc. later in code calling this - int dodamage(int damage) - { - int ad = (damage*(armourtype+1)*30)/100; // let armour absorb when possible - if(ad>armour) ad = armour; - armour -= ad; - damage -= ad; - health -= damage; - return damage; - } - - int hasammo(int gun, int exclude = -1) - { - return gun >= 0 && gun <= NUMGUNS && gun != exclude && ammo[gun] > 0; - } + int health, maxhealth; + int armour, maxarmour, armourtype; + int quadmillis; + int gunselect, gunwait; + int ammo[NUMGUNS]; + int aitype, skill; + + fpsstate() : maxhealth(100), maxarmour(50), aitype(AI_NONE), skill(0) {} + + void baseammo(int gun, int k = 2, int scale = 1) + { + ammo[gun] = (itemstats[gun-GUN_SG].add*k)/scale; + } + + void addammo(int gun, int k = 1, int scale = 1) + { + itemstat &is = itemstats[gun-GUN_SG]; + ammo[gun] = min(ammo[gun] + (is.add*k)/scale, is.max); + } + + bool hasmaxammo(int type) + { + const itemstat &is = itemstats[type-I_SHELLS]; + return ammo[type-I_SHELLS+GUN_SG]>=is.max; + } + + bool canpickup(int type) + { + if(type<I_SHELLS || type>I_QUAD) return false; + itemstat &is = itemstats[type-I_SHELLS]; + switch(type) + { + case I_BOOST: return maxhealth<is.max || health<maxhealth; + case I_TINYHEALTH: return health<maxhealth; + case I_HEALTH: return health<maxhealth; + + case I_TINYARMOUR: + [[fallthrough]]; + case I_GREENARMOUR: + [[fallthrough]]; + case I_YELLOWARMOUR: return maxarmour<is.max || armour<maxarmour; + + case I_QUAD: return quadmillis<is.max; + default: return ammo[is.info]<is.max; + } + } + + void pickup(int type) + { + if(type<I_SHELLS || type>I_QUAD) return; + itemstat &is = itemstats[type-I_SHELLS]; + switch(type) + { + case I_TINYHEALTH: + health = min(health+is.add, maxhealth); + break; + case I_BOOST: + maxhealth = min(maxhealth+is.info, is.max); + [[fallthrough]]; + case I_HEALTH: // boost also adds to health + health = min(health+is.add, maxhealth); + break; + case I_TINYARMOUR: + [[fallthrough]]; + case I_GREENARMOUR: + [[fallthrough]]; + case I_YELLOWARMOUR: + maxarmour = max(maxarmour, is.max); + armour = min(armour+is.add, maxarmour); + armourtype = is.info; + break; + case I_QUAD: + quadmillis = min(quadmillis+is.add, is.max); + break; + default: + ammo[is.info] = min(ammo[is.info]+is.add, is.max); + break; + } + } + + void respawn() + { + maxhealth = 100; + health = maxhealth; + maxarmour = 50; + armour = 0; + armourtype = A_BLUE; + quadmillis = 0; + gunselect = GUN_PISTOL; + gunwait = 0; + loopi(NUMGUNS) ammo[i] = 0; + ammo[GUN_FIST] = 1; + } + + void spawnstate(int gamemode) + { + if(m_demo) + { + gunselect = GUN_FIST; + } + else if(m_insta) + { + armour = 0; + health = 1; + gunselect = GUN_RIFLE; + ammo[GUN_RIFLE] = 100; + } + else if(m_efficiency) + { + armourtype = A_GREEN; + armour = 100; + loopi(NUMGUNS-1) baseammo(i+1); + gunselect = GUN_CG; + ammo[GUN_CG] /= 2; + } + else + { + armourtype = A_BLUE; + armour = 25; + ammo[GUN_PISTOL] = 40; + } + } + + // just subtract damage here, can set death, etc. later in code calling this + int dodamage(int damage) + { + int ad = (damage*(armourtype+1)*30)/100; // let armour absorb when possible + if(ad>armour) ad = armour; + armour -= ad; + damage -= ad; + health -= damage; + return damage; + } + + int hasammo(int gun, int exclude = -1) + { + return gun >= 0 && gun <= NUMGUNS && gun != exclude && ammo[gun] > 0; + } }; extern int screenw, screenh; struct fpsent : dynent, fpsstate { - int weight; // affects the effectiveness of hitpush - int clientnum, privilege, lastupdate, plag, ping; - int lifesequence; // sequence id for each respawn, used in damage test - int respawned, suicided; - int lastpain; - int lastaction, lastattackgun; - bool attacking; - int attacksound, attackchan, idlesound, idlechan; - int lasttaunt; - int lastpickup, lastpickupmillis, lastbase, lastrepammo, flagpickup, tokens; - vec lastcollect; - int frags, flags, deaths, totaldamage, totalshots; - editinfo *edit; - float deltayaw, deltapitch, deltaroll, newyaw, newpitch, newroll; - int smoothmillis; - - string name, team, info; - int playermodel; - ai::aiinfo *ai; - int ownernum, lastnode; - - vec muzzle; - - fpsent() : weight(100), clientnum(-1), privilege(PRIV_NONE), lastupdate(0), plag(0), ping(0), lifesequence(0), respawned(-1), suicided(-1), lastpain(0), attacksound(-1), attackchan(-1), idlesound(-1), idlechan(-1), frags(0), flags(0), deaths(0), totaldamage(0), totalshots(0), edit(NULL), smoothmillis(-1), playermodel(-1), ai(NULL), ownernum(-1), muzzle(-1, -1, -1) - { - name[0] = team[0] = info[0] = 0; - respawn(); - } - ~fpsent() - { - freeeditinfo(edit); - if(attackchan >= 0) stopsound(attacksound, attackchan); - if(idlechan >= 0) stopsound(idlesound, idlechan); - if(ai) delete ai; - } - - void hitpush(int damage, const vec &dir, fpsent *actor, int gun) - { - vec push(dir); - push.mul((actor==this && guns[gun].exprad ? EXP_SELFPUSH : 1.0f)*guns[gun].hitpush*damage/weight); - vel.add(push); - } - - void stopattacksound() - { - if(attackchan >= 0) stopsound(attacksound, attackchan, 250); - attacksound = attackchan = -1; - } - - void stopidlesound() - { - if(idlechan >= 0) stopsound(idlesound, idlechan, 100); - idlesound = idlechan = -1; - } - - void respawn() - { - dynent::reset(); - fpsstate::respawn(); - respawned = suicided = -1; - lastaction = 0; - lastattackgun = gunselect; - attacking = false; - lasttaunt = 0; - lastpickup = -1; - lastpickupmillis = 0; - lastbase = lastrepammo = -1; - flagpickup = 0; - tokens = 0; - lastcollect = vec(-1e10f, -1e10f, -1e10f); - stopattacksound(); - lastnode = -1; - } - - int respawnwait(int secs, int delay = 0) - { - return max(0, secs - (::lastmillis - lastpain - delay)/1000); - } + int weight; // affects the effectiveness of hitpush + int clientnum, privilege, lastupdate, plag, ping; + int lifesequence; // sequence id for each respawn, used in damage test + int respawned, suicided; + int lastpain; + int lastaction, lastattackgun; + bool attacking; + int attacksound, attackchan, idlesound, idlechan; + int lasttaunt; + int lastpickup, lastpickupmillis, lastbase, lastrepammo, flagpickup, tokens; + vec lastcollect; + int frags, flags, deaths, totaldamage, totalshots; + editinfo *edit; + float deltayaw, deltapitch, deltaroll, newyaw, newpitch, newroll; + int smoothmillis; + + string name, team, info; + int playermodel; + ai::aiinfo *ai; + int ownernum, lastnode; + + vec muzzle; + + fpsent() : weight(100), clientnum(-1), privilege(PRIV_NONE), lastupdate(0), plag(0), ping(0), lifesequence(0), respawned(-1), suicided(-1), lastpain(0), attacksound(-1), attackchan(-1), idlesound(-1), idlechan(-1), frags(0), flags(0), deaths(0), totaldamage(0), totalshots(0), edit(NULL), smoothmillis(-1), playermodel(-1), ai(NULL), ownernum(-1), muzzle(-1, -1, -1) + { + name[0] = team[0] = info[0] = 0; + respawn(); + } + ~fpsent() + { + freeeditinfo(edit); + if(attackchan >= 0) stopsound(attacksound, attackchan); + if(idlechan >= 0) stopsound(idlesound, idlechan); + if(ai) delete ai; + } + + void hitpush(int damage, const vec &dir, fpsent *actor, int gun) + { + vec push(dir); + push.mul((actor==this && guns[gun].exprad ? EXP_SELFPUSH : 1.0f)*guns[gun].hitpush*damage/weight); + vel.add(push); + } + + void stopattacksound() + { + if(attackchan >= 0) stopsound(attacksound, attackchan, 250); + attacksound = attackchan = -1; + } + + void stopidlesound() + { + if(idlechan >= 0) stopsound(idlesound, idlechan, 100); + idlesound = idlechan = -1; + } + + void respawn() + { + dynent::reset(); + fpsstate::respawn(); + respawned = suicided = -1; + lastaction = 0; + lastattackgun = gunselect; + attacking = false; + lasttaunt = 0; + lastpickup = -1; + lastpickupmillis = 0; + lastbase = lastrepammo = -1; + flagpickup = 0; + tokens = 0; + lastcollect = vec(-1e10f, -1e10f, -1e10f); + stopattacksound(); + lastnode = -1; + } + + int respawnwait(int secs, int delay = 0) + { + return max(0, secs - (::lastmillis - lastpain - delay)/1000); + } }; struct teamscore { - const char *team; - int score; - teamscore() {} - teamscore(const char *s, int n) : team(s), score(n) {} - - static bool compare(const teamscore &x, const teamscore &y) - { - if(x.score > y.score) return true; - if(x.score < y.score) return false; - return strcmp(x.team, y.team) < 0; - } + const char *team; + int score; + teamscore() {} + teamscore(const char *s, int n) : team(s), score(n) {} + + static bool compare(const teamscore &x, const teamscore &y) + { + if(x.score > y.score) return true; + if(x.score < y.score) return false; + return strcmp(x.team, y.team) < 0; + } }; static inline uint hthash(const teamscore &t) { return hthash(t.team); } @@ -536,8 +536,8 @@ static inline bool htcmp(const char *key, const teamscore &t) { return htcmp(key struct teaminfo { - char team[MAXTEAMLEN+1]; - int frags; + char team[MAXTEAMLEN+1]; + int frags; }; static inline uint hthash(const teaminfo &t) { return hthash(t.team); } @@ -545,157 +545,157 @@ static inline bool htcmp(const char *team, const teaminfo &t) { return !strcmp(t namespace entities { - extern vector<extentity *> ents; - - extern const char *entmdlname(int type); - extern const char *itemname(int i); - extern int itemicon(int i); - - extern void preloadentities(); - extern void renderentities(); - extern void checkitems(fpsent *d); - extern void checkquad(int time, fpsent *d); - extern void resetspawns(); - extern void spawnitems(bool force = false); - extern void putitems(packetbuf &p); - extern void setspawn(int i, bool on); - extern void teleport(int n, fpsent *d); - extern void pickupeffects(int n, fpsent *d); - extern void teleporteffects(fpsent *d, int tp, int td, bool local = true); - extern void jumppadeffects(fpsent *d, int jp, bool local = true); - - extern void repammo(fpsent *d, int type, bool local = true); + extern vector<extentity *> ents; + + extern const char *entmdlname(int type); + extern const char *itemname(int i); + extern int itemicon(int i); + + extern void preloadentities(); + extern void renderentities(); + extern void checkitems(fpsent *d); + extern void checkquad(int time, fpsent *d); + extern void resetspawns(); + extern void spawnitems(bool force = false); + extern void putitems(packetbuf &p); + extern void setspawn(int i, bool on); + extern void teleport(int n, fpsent *d); + extern void pickupeffects(int n, fpsent *d); + extern void teleporteffects(fpsent *d, int tp, int td, bool local = true); + extern void jumppadeffects(fpsent *d, int jp, bool local = true); + + extern void repammo(fpsent *d, int type, bool local = true); } namespace game { - // fps - extern int gamemode, nextmode; - extern string clientmap; - extern bool intermission; - extern int maptime, maprealtime, maplimit; - extern fpsent *player1; - extern vector<fpsent *> players, clients; - extern int lastspawnattempt; - extern int lasthit; - extern int respawnent; - extern int following; - extern int smoothmove, smoothdist; - - extern bool clientoption(const char *arg); - extern fpsent *getclient(int cn); - extern fpsent *newclient(int cn); - extern const char *colorname(fpsent *d, const char *name = NULL, const char *prefix = "", const char *suffix = "", const char *alt = NULL); - extern const char *teamcolorname(fpsent *d, const char *alt = "you"); - extern const char *teamcolor(const char *name, bool sameteam, const char *alt = NULL); - extern const char *teamcolor(const char *name, const char *team, const char *alt = NULL); - extern void teamsound(bool sameteam, int n, const vec *loc = NULL); - extern void teamsound(fpsent *d, int n, const vec *loc = NULL); - extern fpsent *pointatplayer(); - extern fpsent *hudplayer(); - extern fpsent *followingplayer(fpsent *fallback = NULL); - extern void stopfollowing(); - extern void clientdisconnected(int cn, bool notify = true); - extern void clearclients(bool notify = true); - extern void startgame(); - extern float proximityscore(float x, float lower, float upper); - extern void pickgamespawn(fpsent *d); - extern void spawnplayer(fpsent *d); - extern void deathstate(fpsent *d, bool restore = false); - extern void damaged(int damage, fpsent *d, fpsent *actor, bool local = true); - extern void killed(fpsent *d, fpsent *actor); - extern void timeupdate(int timeremain); - extern void msgsound(int n, physent *d = NULL); - extern void drawicon(int icon, float x, float y, float sz = 120); - const char *mastermodecolor(int n, const char *unknown); - const char *mastermodeicon(int n, const char *unknown); - - // client - extern bool connected, remote, demoplayback; - extern string servinfo; - extern vector<uchar> messages; - - extern int parseplayer(const char *arg); - extern void ignore(int cn); - extern void unignore(int cn); - extern bool isignored(int cn); - extern bool addmsg(int type, const char *fmt = NULL, ...); - extern void switchname(const char *name); - extern void switchteam(const char *name); - extern void sendmapinfo(); - extern void stopdemo(); - extern void changemap(const char *name, int mode); - extern void forceintermission(); - extern void c2sinfo(bool force = false); - extern void sendposition(fpsent *d, bool reliable = false); - - // weapon - extern int getweapon(const char *name); - extern void shoot(fpsent *d, const vec &targ); - extern void shoteffects(int gun, const vec &from, const vec &to, fpsent *d, bool local, int id, int prevaction); - extern void explode(bool local, fpsent *owner, const vec &v, dynent *safe, int dam, int gun); - extern void explodeeffects(int gun, fpsent *d, bool local, int id = 0); - extern void damageeffect(int damage, fpsent *d, bool thirdperson = true); - extern float intersectdist; - extern bool intersect(dynent *d, const vec &from, const vec &to, float &dist = intersectdist); - extern dynent *intersectclosest(const vec &from, const vec &to, fpsent *at, float &dist = intersectdist); - extern void clearbouncers(); - extern void updatebouncers(int curtime); - extern void removebouncers(fpsent *owner); - extern void renderbouncers(); - extern void clearprojectiles(); - extern void updateprojectiles(int curtime); - extern void removeprojectiles(fpsent *owner); - extern void renderprojectiles(); - extern void preloadbouncers(); - extern void removeweapons(fpsent *owner); - extern void updateweapons(int curtime); - extern void gunselect(int gun, fpsent *d); - extern void weaponswitch(fpsent *d); - extern void avoidweapons(ai::avoidset &obstacles, float radius); - - // scoreboard - extern void showscores(bool on); - extern void getbestplayers(vector<fpsent *> &best); - extern void getbestteams(vector<const char *> &best); - extern void clearteaminfo(); - extern void setteaminfo(const char *team, int frags); - extern int statuscolor(fpsent *d, int color); - - // render - struct playermodelinfo - { - const char *ffa, *blueteam, *redteam, *hudguns, - *vwep, *quad, *armour[3], - *ffaicon, *blueicon, *redicon; - bool ragdoll; - }; - - extern int playermodel, teamskins, testteam; - - extern void saveragdoll(fpsent *d); - extern void clearragdolls(); - extern void moveragdolls(); - extern const playermodelinfo &getplayermodelinfo(fpsent *d); - extern void swayhudgun(int curtime); - extern vec hudgunorigin(int gun, const vec &from, const vec &to, fpsent *d); + // fps + extern int gamemode, nextmode; + extern string clientmap; + extern bool intermission; + extern int maptime, maprealtime, maplimit; + extern fpsent *player1; + extern vector<fpsent *> players, clients; + extern int lastspawnattempt; + extern int lasthit; + extern int respawnent; + extern int following; + extern int smoothmove, smoothdist; + + extern bool clientoption(const char *arg); + extern fpsent *getclient(int cn); + extern fpsent *newclient(int cn); + extern const char *colorname(fpsent *d, const char *name = NULL, const char *prefix = "", const char *suffix = "", const char *alt = NULL); + extern const char *teamcolorname(fpsent *d, const char *alt = "you"); + extern const char *teamcolor(const char *name, bool sameteam, const char *alt = NULL); + extern const char *teamcolor(const char *name, const char *team, const char *alt = NULL); + extern void teamsound(bool sameteam, int n, const vec *loc = NULL); + extern void teamsound(fpsent *d, int n, const vec *loc = NULL); + extern fpsent *pointatplayer(); + extern fpsent *hudplayer(); + extern fpsent *followingplayer(fpsent *fallback = NULL); + extern void stopfollowing(); + extern void clientdisconnected(int cn, bool notify = true); + extern void clearclients(bool notify = true); + extern void startgame(); + extern float proximityscore(float x, float lower, float upper); + extern void pickgamespawn(fpsent *d); + extern void spawnplayer(fpsent *d); + extern void deathstate(fpsent *d, bool restore = false); + extern void damaged(int damage, fpsent *d, fpsent *actor, bool local = true); + extern void killed(fpsent *d, fpsent *actor); + extern void timeupdate(int timeremain); + extern void msgsound(int n, physent *d = NULL); + extern void drawicon(int icon, float x, float y, float sz = 120); + const char *mastermodecolor(int n, const char *unknown); + const char *mastermodeicon(int n, const char *unknown); + + // client + extern bool connected, remote, demoplayback; + extern string servinfo; + extern vector<uchar> messages; + + extern int parseplayer(const char *arg); + extern void ignore(int cn); + extern void unignore(int cn); + extern bool isignored(int cn); + extern bool addmsg(int type, const char *fmt = NULL, ...); + extern void switchname(const char *name); + extern void switchteam(const char *name); + extern void sendmapinfo(); + extern void stopdemo(); + extern void changemap(const char *name, int mode); + extern void forceintermission(); + extern void c2sinfo(bool force = false); + extern void sendposition(fpsent *d, bool reliable = false); + + // weapon + extern int getweapon(const char *name); + extern void shoot(fpsent *d, const vec &targ); + extern void shoteffects(int gun, const vec &from, const vec &to, fpsent *d, bool local, int id, int prevaction); + extern void explode(bool local, fpsent *owner, const vec &v, dynent *safe, int dam, int gun); + extern void explodeeffects(int gun, fpsent *d, bool local, int id = 0); + extern void damageeffect(int damage, fpsent *d, bool thirdperson = true); + extern float intersectdist; + extern bool intersect(dynent *d, const vec &from, const vec &to, float &dist = intersectdist); + extern dynent *intersectclosest(const vec &from, const vec &to, fpsent *at, float &dist = intersectdist); + extern void clearbouncers(); + extern void updatebouncers(int curtime); + extern void removebouncers(fpsent *owner); + extern void renderbouncers(); + extern void clearprojectiles(); + extern void updateprojectiles(int curtime); + extern void removeprojectiles(fpsent *owner); + extern void renderprojectiles(); + extern void preloadbouncers(); + extern void removeweapons(fpsent *owner); + extern void updateweapons(int curtime); + extern void gunselect(int gun, fpsent *d); + extern void weaponswitch(fpsent *d); + extern void avoidweapons(ai::avoidset &obstacles, float radius); + + // scoreboard + extern void showscores(bool on); + extern void getbestplayers(vector<fpsent *> &best); + extern void getbestteams(vector<const char *> &best); + extern void clearteaminfo(); + extern void setteaminfo(const char *team, int frags); + extern int statuscolor(fpsent *d, int color); + + // render + struct playermodelinfo + { + const char *ffa, *blueteam, *redteam, *hudguns, + *vwep, *quad, *armour[3], + *ffaicon, *blueicon, *redicon; + bool ragdoll; + }; + + extern int playermodel, teamskins, testteam; + + extern void saveragdoll(fpsent *d); + extern void clearragdolls(); + extern void moveragdolls(); + extern const playermodelinfo &getplayermodelinfo(fpsent *d); + extern void swayhudgun(int curtime); + extern vec hudgunorigin(int gun, const vec &from, const vec &to, fpsent *d); } namespace server { - extern const char *modename(int n, const char *unknown = "unknown"); - extern const char *mastermodename(int n, const char *unknown = "unknown"); - extern void startintermission(); - extern void stopdemo(); - extern void timeupdate(int secs); - extern const char *getdemofile(const char *file, bool init); - extern void forcemap(const char *map, int mode); - extern void forcepaused(bool paused); - extern void forcegamespeed(int speed); - extern void hashpassword(int cn, int sessionid, const char *pwd, char *result, int maxlen = MAXSTRLEN); - extern int msgsizelookup(int msg); - extern bool serveroption(const char *arg); - extern bool delayspawn(int type); + extern const char *modename(int n, const char *unknown = "unknown"); + extern const char *mastermodename(int n, const char *unknown = "unknown"); + extern void startintermission(); + extern void stopdemo(); + extern void timeupdate(int secs); + extern const char *getdemofile(const char *file, bool init); + extern void forcemap(const char *map, int mode); + extern void forcepaused(bool paused); + extern void forcegamespeed(int speed); + extern void hashpassword(int cn, int sessionid, const char *pwd, char *result, int maxlen = MAXSTRLEN); + extern int msgsizelookup(int msg); + extern bool serveroption(const char *arg); + extern bool delayspawn(int type); } #endif diff --git a/src/fpsgame/render.cpp b/src/fpsgame/render.cpp index b79d73a..f35daec 100644 --- a/src/fpsgame/render.cpp +++ b/src/fpsgame/render.cpp @@ -5,494 +5,460 @@ extern float gatherspawninfos(dynent *d, int tag, vector<spawninfo> &spawninfos) namespace game { - vector<fpsent *> bestplayers; - vector<const char *> bestteams; - - VARP(ragdoll, 0, 1, 1); - VARP(ragdollmillis, 0, 10000, 300000); - VARP(ragdollfade, 0, 1000, 300000); - VARP(playermodel, 0, 0, 0); - VARP(hidedead, 0, 0, 2); - - vector<fpsent *> ragdolls; - - void saveragdoll(fpsent *d) - { - if(!d->ragdoll || !ragdollmillis || (!ragdollfade && lastmillis > d->lastpain + ragdollmillis)) return; - fpsent *r = new fpsent(*d); - r->lastupdate = ragdollfade && lastmillis > d->lastpain + max(ragdollmillis - ragdollfade, 0) ? lastmillis - max(ragdollmillis - ragdollfade, 0) : d->lastpain; - r->edit = NULL; - r->ai = NULL; - r->attackchan = r->idlechan = -1; - if(d==player1) r->playermodel = playermodel; - ragdolls.add(r); - d->ragdoll = NULL; - } - - void clearragdolls() - { - ragdolls.deletecontents(); - } - - void moveragdolls() - { - loopv(ragdolls) - { - fpsent *d = ragdolls[i]; - if(lastmillis > d->lastupdate + ragdollmillis) - { - delete ragdolls.remove(i--); - continue; - } - moveragdoll(d); - } - } - - static const playermodelinfo playermodels[1] = - { - { "mrfixit", "mrfixit/blue", "mrfixit/red", "mrfixit/hudguns", NULL, "mrfixit/horns", { "mrfixit/armor/blue", "mrfixit/armor/green", "mrfixit/armor/yellow" }, "mrfixit", "mrfixit_blue", "mrfixit_red", true }, - }; - - const playermodelinfo *getplayermodelinfo(int n) - { - (void) n; - return &playermodels[0]; - } - - const playermodelinfo &getplayermodelinfo(fpsent *d) - { - const playermodelinfo *mdl = getplayermodelinfo(0); - if(!mdl) mdl = getplayermodelinfo(playermodel); - return *mdl; - } - - void preloadplayermodel() - { - const playermodelinfo *mdl = getplayermodelinfo(0); - if(m_teammode) - { - preloadmodel(mdl->blueteam); - preloadmodel(mdl->redteam); - } - else preloadmodel(mdl->ffa); - if(mdl->vwep) preloadmodel(mdl->vwep); - if(mdl->quad) preloadmodel(mdl->quad); - loopj(3) if(mdl->armour[j]) preloadmodel(mdl->armour[j]); - } - - VAR(testquad, 0, 0, 1); - VAR(testarmour, 0, 0, 1); - VAR(testteam, 0, 0, 3); - - void renderplayer(fpsent *d, const playermodelinfo &mdl, int team, float fade, bool mainpass) - { - int lastaction = d->lastaction, hold = mdl.vwep || d->gunselect==GUN_PISTOL ? 0 : (ANIM_HOLD1+d->gunselect)|ANIM_LOOP, attack = ANIM_ATTACK1+d->gunselect, delay = mdl.vwep ? 300 : guns[d->gunselect].attackdelay+50; - if(intermission && d->state!=CS_DEAD) - { - lastaction = 0; - hold = attack = ANIM_LOSE|ANIM_LOOP; - delay = 0; - if(m_teammode ? bestteams.htfind(d->team)>=0 : bestplayers.find(d)>=0) hold = attack = ANIM_WIN|ANIM_LOOP; - } - else if(d->state==CS_ALIVE && d->lasttaunt && lastmillis-d->lasttaunt<1000 && lastmillis-d->lastaction>delay) - { - lastaction = d->lasttaunt; - hold = attack = ANIM_TAUNT; - delay = 1000; - } - modelattach a[5]; - static const char * const vweps[] = {"vwep/fist", "vwep/shotg", "vwep/chaing", "vwep/rocket", "vwep/rifle", "vwep/gl", "vwep/pistol"}; - int ai = 0; - if((!mdl.vwep || d->gunselect!=GUN_FIST) && d->gunselect<=GUN_PISTOL) - { - int vanim = ANIM_VWEP_IDLE|ANIM_LOOP, vtime = 0; - if(lastaction && d->lastattackgun==d->gunselect && lastmillis < lastaction + delay) - { - vanim = ANIM_VWEP_SHOOT; - vtime = lastaction; - } - a[ai++] = modelattach("tag_weapon", mdl.vwep ? mdl.vwep : vweps[d->gunselect], vanim, vtime); - } - if(d->state==CS_ALIVE) - { - if((testquad || d->quadmillis) && mdl.quad) - a[ai++] = modelattach("tag_powerup", mdl.quad, ANIM_POWERUP|ANIM_LOOP, 0); - if(testarmour || d->armour) - { - int type = clamp(d->armourtype, (int)A_BLUE, (int)A_YELLOW); - if(mdl.armour[type]) - a[ai++] = modelattach("tag_shield", mdl.armour[type], ANIM_SHIELD|ANIM_LOOP, 0); - } - } - if(mainpass) - { - d->muzzle = vec(-1, -1, -1); - a[ai++] = modelattach("tag_muzzle", &d->muzzle); - } - const char *mdlname = mdl.ffa; - switch(testteam ? testteam-1 : team) - { - case 1: mdlname = mdl.blueteam; break; - case 2: mdlname = mdl.redteam; break; - } - renderclient(d, mdlname, a[0].tag ? a : NULL, hold, attack, delay, lastaction, intermission && d->state!=CS_DEAD ? 0 : d->lastpain, fade, ragdoll && mdl.ragdoll); - } - - VARP(teamskins, 0, 0, 1); + vector<fpsent *> bestplayers; + vector<const char *> bestteams; + + VARP(ragdoll, 0, 1, 1); + VARP(ragdollmillis, 0, 10000, 300000); + VARP(ragdollfade, 0, 1000, 300000); + VARP(playermodel, 0, 0, 0); + VARP(hidedead, 0, 0, 2); + + vector<fpsent *> ragdolls; + + void saveragdoll(fpsent *d) + { + if(!d->ragdoll || !ragdollmillis || (!ragdollfade && lastmillis > d->lastpain + ragdollmillis)) return; + fpsent *r = new fpsent(*d); + r->lastupdate = ragdollfade && lastmillis > d->lastpain + max(ragdollmillis - ragdollfade, 0) ? lastmillis - max(ragdollmillis - ragdollfade, 0) : d->lastpain; + r->edit = NULL; + r->ai = NULL; + r->attackchan = r->idlechan = -1; + if(d==player1) r->playermodel = playermodel; + ragdolls.add(r); + d->ragdoll = NULL; + } + + void clearragdolls() + { + ragdolls.deletecontents(); + } + + void moveragdolls() + { + loopv(ragdolls) + { + fpsent *d = ragdolls[i]; + if(lastmillis > d->lastupdate + ragdollmillis) + { + delete ragdolls.remove(i--); + continue; + } + moveragdoll(d); + } + } + + static const playermodelinfo playermodels[1] = + { + { "mrfixit", "mrfixit/blue", "mrfixit/red", "mrfixit/hudguns", NULL, "mrfixit/horns", { "mrfixit/armor/blue", "mrfixit/armor/green", "mrfixit/armor/yellow" }, "mrfixit", "mrfixit_blue", "mrfixit_red", true }, + }; + + const playermodelinfo *getplayermodelinfo(int n) + { + (void) n; + return &playermodels[0]; + } + + const playermodelinfo &getplayermodelinfo(fpsent *d) + { + const playermodelinfo *mdl = getplayermodelinfo(0); + if(!mdl) mdl = getplayermodelinfo(playermodel); + return *mdl; + } + + void preloadplayermodel() + { + const playermodelinfo *mdl = getplayermodelinfo(0); + if(m_teammode) + { + preloadmodel(mdl->blueteam); + preloadmodel(mdl->redteam); + } + else preloadmodel(mdl->ffa); + if(mdl->vwep) preloadmodel(mdl->vwep); + if(mdl->quad) preloadmodel(mdl->quad); + loopj(3) if(mdl->armour[j]) preloadmodel(mdl->armour[j]); + } + + VAR(testquad, 0, 0, 1); + VAR(testarmour, 0, 0, 1); + VAR(testteam, 0, 0, 3); + + void renderplayer(fpsent *d, const playermodelinfo &mdl, int team, float fade, bool mainpass) + { + int lastaction = d->lastaction, hold = mdl.vwep || d->gunselect==GUN_PISTOL ? 0 : (ANIM_HOLD1+d->gunselect)|ANIM_LOOP, attack = ANIM_ATTACK1+d->gunselect, delay = mdl.vwep ? 300 : guns[d->gunselect].attackdelay+50; + if(intermission && d->state!=CS_DEAD) + { + lastaction = 0; + hold = attack = ANIM_LOSE|ANIM_LOOP; + delay = 0; + if(m_teammode ? bestteams.htfind(d->team)>=0 : bestplayers.find(d)>=0) hold = attack = ANIM_WIN|ANIM_LOOP; + } + else if(d->state==CS_ALIVE && d->lasttaunt && lastmillis-d->lasttaunt<1000 && lastmillis-d->lastaction>delay) + { + lastaction = d->lasttaunt; + hold = attack = ANIM_TAUNT; + delay = 1000; + } + modelattach a[5]; + static const char * const vweps[] = {"vwep/fist", "vwep/shotg", "vwep/chaing", "vwep/rocket", "vwep/rifle", "vwep/gl", "vwep/pistol"}; + int ai = 0; + if((!mdl.vwep || d->gunselect!=GUN_FIST) && d->gunselect<=GUN_PISTOL) + { + int vanim = ANIM_VWEP_IDLE|ANIM_LOOP, vtime = 0; + if(lastaction && d->lastattackgun==d->gunselect && lastmillis < lastaction + delay) + { + vanim = ANIM_VWEP_SHOOT; + vtime = lastaction; + } + a[ai++] = modelattach("tag_weapon", mdl.vwep ? mdl.vwep : vweps[d->gunselect], vanim, vtime); + } + if(d->state==CS_ALIVE) + { + if((testquad || d->quadmillis) && mdl.quad) + a[ai++] = modelattach("tag_powerup", mdl.quad, ANIM_POWERUP|ANIM_LOOP, 0); + if(testarmour || d->armour) + { + int type = clamp(d->armourtype, (int)A_BLUE, (int)A_YELLOW); + if(mdl.armour[type]) + a[ai++] = modelattach("tag_shield", mdl.armour[type], ANIM_SHIELD|ANIM_LOOP, 0); + } + } + if(mainpass) + { + d->muzzle = vec(-1, -1, -1); + a[ai++] = modelattach("tag_muzzle", &d->muzzle); + } + const char *mdlname = mdl.ffa; + switch(testteam ? testteam-1 : team) + { + case 1: mdlname = mdl.blueteam; break; + case 2: mdlname = mdl.redteam; break; + } + renderclient(d, mdlname, a[0].tag ? a : NULL, hold, attack, delay, lastaction, intermission && d->state!=CS_DEAD ? 0 : d->lastpain, fade, ragdoll && mdl.ragdoll); + } + + VARP(teamskins, 0, 0, 1); + + VARP(statusicons, 0, 1, 1); + + void renderstatusicons(fpsent *d, int team, float yoffset)///TODO + { + vec p = d->abovehead().madd(camup, yoffset); + int icons = 0; + const itemstat &boost = itemstats[I_BOOST-I_SHELLS]; + if(statusicons && (d->state==CS_ALIVE || d->state==CS_LAGGED)) + { + if(d->quadmillis) icons++; + if(d->maxhealth>100) icons += (min(d->maxhealth, boost.max) - 100 + boost.info-1) / boost.info; + if(d->armour>0 && d->armourtype>=A_GREEN && !m_noitems) icons++; + } + if(icons) concatstring(d->info, " "); + particle_text(p, d->info, PART_TEXT, 1, team ? (team==1 ? 0x6496FF : 0xFF4B19) : 0x1EC850, 2.0f, 0, icons); + if(icons) + { + float tw, th; + text_boundsf(d->info, tw, th); + float offset = (tw - icons*th)/2; + if(d->armour>0 && d->armourtype>=A_GREEN && !m_noitems) + { + int icon = itemstats[(d->armourtype==A_YELLOW ? I_YELLOWARMOUR : I_GREENARMOUR)-I_SHELLS].icon; + particle_texticon(p, icon%4, icon/4, offset, PART_TEXT_ICON, 1, 0xFFFFFF, 2.0f); + offset += th; + } + for(int i = 100; i < min(d->maxhealth, boost.max); i += boost.info) + { + particle_texticon(p, boost.icon%4, boost.icon/4, offset, PART_TEXT_ICON, 1, 0xFFFFFF, 2.0f); + offset += th; + } + if(d->quadmillis) + { + int icon = itemstats[I_QUAD-I_SHELLS].icon; + particle_texticon(p, icon%4, icon/4, offset, PART_TEXT_ICON, 1, 0xFFFFFF, 2.0f); + offset += th; + } + } + } + + VARP(statusbars, 0, 1, 2); + FVARP(statusbarscale, 0, 1, 2); + + float renderstatusbars(fpsent *d, int team)///TODO + { + if(!statusbars || m_insta || (player1->state==CS_SPECTATOR ? statusbars <= 1 : team != 1) || (d->state!=CS_ALIVE && d->state!=CS_LAGGED)) return 0; + vec p = d->abovehead().msub(camdir, 50/80.0f).msub(camup, 2.0f); + float offset = 0; + float scale = statusbarscale; + if(d->armour > 0) + { + int limit = d->armourtype==A_YELLOW ? 200 : (d->armourtype==A_GREEN ? 100 : 50); + int color = d->armourtype==A_YELLOW ? 0xFFC040 : (d->armourtype==A_GREEN ? 0x008C00 : 0x0B5899); + float size = scale*sqrtf(max(d->armour, limit)/100.0f); + float fill = float(d->armour)/limit; + offset += size; + particle_meter(vec(p).madd(camup, offset), fill, PART_METER, 1, color, 0, size); + } + int color = d->health<=25 ? 0xFF0000 : (d->health<=50 ? 0xFF8000 : (d->health<=100 ? 0x40FF80 : 0x40C0FF)); + float size = scale*sqrtf(max(d->health, d->maxhealth)/100.0f); + float fill = float(d->health)/d->maxhealth; + offset += size; + particle_meter(vec(p).madd(camup, offset), fill, PART_METER, 1, color, 0, size); + return offset; + } + + void rendergame(bool mainpass) + { + if(mainpass) ai::render(); + + if(intermission) + { + bestteams.shrink(0); + bestplayers.shrink(0); + if(m_teammode) getbestteams(bestteams); + else getbestplayers(bestplayers); + } + + startmodelbatches(); + + fpsent *exclude = isthirdperson() ? NULL : followingplayer(); + loopv(players) + { + fpsent *d = players[i]; + if(d == player1 || d->state==CS_SPECTATOR || d->state==CS_SPAWNING || d->lifesequence < 0 || d == exclude || (d->state==CS_DEAD && hidedead)) continue; + int team = 0; + if(teamskins || m_teammode) team = isteam(player1->team, d->team) ? 1 : 2; + renderplayer(d, getplayermodelinfo(d), team, 1, mainpass); + + vec dir = vec(d->o).sub(camera1->o); + float dist = dir.magnitude(); + dir.div(dist); + if(d->state!=CS_EDITING && raycube(camera1->o, dir, dist, 0) < dist) + { + d->info[0] = '\0'; + continue; + } + + copystring(d->info, colorname(d)); + if(d->state!=CS_DEAD) + { + float offset = renderstatusbars(d, team); + renderstatusicons(d, team, offset); + } + } + loopv(ragdolls) + { + fpsent *d = ragdolls[i]; + int team = 0; + if(teamskins || m_teammode) team = isteam(player1->team, d->team) ? 1 : 2; + float fade = 1.0f; + if(ragdollmillis && ragdollfade) + fade -= clamp(float(lastmillis - (d->lastupdate + max(ragdollmillis - ragdollfade, 0)))/min(ragdollmillis, ragdollfade), 0.0f, 1.0f); + renderplayer(d, getplayermodelinfo(d), team, fade, mainpass); + } + if(isthirdperson() && !followingplayer() && (player1->state!=CS_DEAD || hidedead != 1)) renderplayer(player1, getplayermodelinfo(player1), teamskins || m_teammode ? 1 : 0, 1, mainpass); + entities::renderentities(); + renderbouncers(); + renderprojectiles(); + + endmodelbatches(); + } + + VARP(hudgun, 0, 1, 1); + VARP(hudgunsway, 0, 1, 1); + VARP(teamhudguns, 0, 1, 1); + VARP(chainsawhudgun, 0, 1, 1); + VAR(testhudgun, 0, 0, 1); + + FVAR(swaystep, 1, 35.0f, 100); + FVAR(swayside, 0, 0.04f, 1); + FVAR(swayup, -1, 0.05f, 1); + + float swayfade = 0, swayspeed = 0, swaydist = 0; + vec swaydir(0, 0, 0); + + void swayhudgun(int curtime) + { + fpsent *d = hudplayer(); + if(d->state != CS_SPECTATOR) + { + if(d->physstate >= PHYS_SLOPE) + { + swayspeed = min(sqrtf(d->vel.x*d->vel.x + d->vel.y*d->vel.y), d->maxspeed); + swaydist += swayspeed*curtime/1000.0f; + swaydist = fmod(swaydist, 2*swaystep); + swayfade = 1; + } + else if(swayfade > 0) + { + swaydist += swayspeed*swayfade*curtime/1000.0f; + swaydist = fmod(swaydist, 2*swaystep); + swayfade -= 0.5f*(curtime*d->maxspeed)/(swaystep*1000.0f); + } + + float k = pow(0.7f, curtime/10.0f); + swaydir.mul(k); + vec vel(d->vel); + vel.add(d->falling); + swaydir.add(vec(vel).mul((1-k)/(15*max(vel.magnitude(), d->maxspeed)))); + } + } + + struct hudent : dynent + { + hudent() { type = ENT_CAMERA; } + } guninterp; + + SVARP(hudgunsdir, ""); + + void drawhudmodel(fpsent *d, int anim, float speed = 0, int base = 0) + { + if(d->gunselect>GUN_PISTOL) return; + + vec sway; + vecfromyawpitch(d->yaw, 0, 0, 1, sway); + float steps = swaydist/swaystep*M_PI; + sway.mul(swayside*cosf(steps)); + sway.z = swayup*(fabs(sinf(steps)) - 1); + sway.add(swaydir).add(d->o); + if(!hudgunsway) sway = d->o; #if 0 - // for testing spawns - - float hsv2rgb(float h, float s, float v, int n) - { - float k = fmod(n + h / 60.0f, 6.0f); - return v - v * s * max(min(min(k, 4.0f - k), 1.0f), 0.0f); - } - - vec hsv2rgb(float h, float s, float v) - { - return vec(hsv2rgb(h, s, v, 5), hsv2rgb(h, s, v, 3), hsv2rgb(h, s, v, 1)); - } - - void renderspawn(const vec &o, int rating, float probability) - { - defformatstring(score, "%d", rating); - defformatstring(percentage, "(%.2f%%)", probability * 100); - bvec colorvec = bvec::fromcolor(hsv2rgb(rating * 1.2f, 0.8, 1)); - int color = (colorvec.r << 16) + (colorvec.g << 8) + colorvec.b; - particle_textcopy(vec(o).addz(5), score, PART_TEXT, 1, color, 5.0f); - particle_textcopy(vec(o).addz(1), percentage, PART_TEXT, 1, color, 4.0f); - } - - void renderspawns() - { - vector<spawninfo> spawninfos; - float ratingsum = gatherspawninfos(player1, 0, spawninfos); - loopv(spawninfos) renderspawn(spawninfos[i].e->o, spawninfos[i].weight * 100, spawninfos[i].weight / ratingsum); - } - - VAR(dbgspawns, 0, 0, 1); + if(player1->state!=CS_DEAD && player1->quadmillis) + { + float t = 0.5f + 0.5f*sinf(2*M_PI*lastmillis/1000.0f); + color.y = color.y*(1-t) + t; + } #endif - - VARP(statusicons, 0, 1, 1); - - void renderstatusicons(fpsent *d, int team, float yoffset)///TODO - { - vec p = d->abovehead().madd(camup, yoffset); - int icons = 0; - const itemstat &boost = itemstats[I_BOOST-I_SHELLS]; - if(statusicons && (d->state==CS_ALIVE || d->state==CS_LAGGED)) - { - if(d->quadmillis) icons++; - if(d->maxhealth>100) icons += (min(d->maxhealth, boost.max) - 100 + boost.info-1) / boost.info; - if(d->armour>0 && d->armourtype>=A_GREEN && !m_noitems) icons++; - } - if(icons) concatstring(d->info, " "); - particle_text(p, d->info, PART_TEXT, 1, team ? (team==1 ? 0x6496FF : 0xFF4B19) : 0x1EC850, 2.0f, 0, icons); - if(icons) - { - float tw, th; - text_boundsf(d->info, tw, th); - float offset = (tw - icons*th)/2; - if(d->armour>0 && d->armourtype>=A_GREEN && !m_noitems) - { - int icon = itemstats[(d->armourtype==A_YELLOW ? I_YELLOWARMOUR : I_GREENARMOUR)-I_SHELLS].icon; - particle_texticon(p, icon%4, icon/4, offset, PART_TEXT_ICON, 1, 0xFFFFFF, 2.0f); - offset += th; - } - for(int i = 100; i < min(d->maxhealth, boost.max); i += boost.info) - { - particle_texticon(p, boost.icon%4, boost.icon/4, offset, PART_TEXT_ICON, 1, 0xFFFFFF, 2.0f); - offset += th; - } - if(d->quadmillis) - { - int icon = itemstats[I_QUAD-I_SHELLS].icon; - particle_texticon(p, icon%4, icon/4, offset, PART_TEXT_ICON, 1, 0xFFFFFF, 2.0f); - offset += th; - } - } - } - - VARP(statusbars, 0, 1, 2); - FVARP(statusbarscale, 0, 1, 2); - - float renderstatusbars(fpsent *d, int team)///TODO - { - if(!statusbars || m_insta || (player1->state==CS_SPECTATOR ? statusbars <= 1 : team != 1) || (d->state!=CS_ALIVE && d->state!=CS_LAGGED)) return 0; - vec p = d->abovehead().msub(camdir, 50/80.0f).msub(camup, 2.0f); - float offset = 0; - float scale = statusbarscale; - if(d->armour > 0) - { - int limit = d->armourtype==A_YELLOW ? 200 : (d->armourtype==A_GREEN ? 100 : 50); - int color = d->armourtype==A_YELLOW ? 0xFFC040 : (d->armourtype==A_GREEN ? 0x008C00 : 0x0B5899); - float size = scale*sqrtf(max(d->armour, limit)/100.0f); - float fill = float(d->armour)/limit; - offset += size; - particle_meter(vec(p).madd(camup, offset), fill, PART_METER, 1, color, 0, size); - } - int color = d->health<=25 ? 0xFF0000 : (d->health<=50 ? 0xFF8000 : (d->health<=100 ? 0x40FF80 : 0x40C0FF)); - float size = scale*sqrtf(max(d->health, d->maxhealth)/100.0f); - float fill = float(d->health)/d->maxhealth; - offset += size; - particle_meter(vec(p).madd(camup, offset), fill, PART_METER, 1, color, 0, size); - return offset; - } - - void rendergame(bool mainpass) - { - if(mainpass) ai::render(); - - if(intermission) - { - bestteams.shrink(0); - bestplayers.shrink(0); - if(m_teammode) getbestteams(bestteams); - else getbestplayers(bestplayers); - } - - startmodelbatches(); - - fpsent *exclude = isthirdperson() ? NULL : followingplayer(); - loopv(players) - { - fpsent *d = players[i]; - if(d == player1 || d->state==CS_SPECTATOR || d->state==CS_SPAWNING || d->lifesequence < 0 || d == exclude || (d->state==CS_DEAD && hidedead)) continue; - int team = 0; - if(teamskins || m_teammode) team = isteam(player1->team, d->team) ? 1 : 2; - renderplayer(d, getplayermodelinfo(d), team, 1, mainpass); - - vec dir = vec(d->o).sub(camera1->o); - float dist = dir.magnitude(); - dir.div(dist); - if(d->state!=CS_EDITING && raycube(camera1->o, dir, dist, 0) < dist) - { - d->info[0] = '\0'; - continue; - } - - copystring(d->info, colorname(d)); - if(d->state!=CS_DEAD) - { - float offset = renderstatusbars(d, team); - renderstatusicons(d, team, offset); - } - } - loopv(ragdolls) - { - fpsent *d = ragdolls[i]; - int team = 0; - if(teamskins || m_teammode) team = isteam(player1->team, d->team) ? 1 : 2; - float fade = 1.0f; - if(ragdollmillis && ragdollfade) - fade -= clamp(float(lastmillis - (d->lastupdate + max(ragdollmillis - ragdollfade, 0)))/min(ragdollmillis, ragdollfade), 0.0f, 1.0f); - renderplayer(d, getplayermodelinfo(d), team, fade, mainpass); - } - if(isthirdperson() && !followingplayer() && (player1->state!=CS_DEAD || hidedead != 1)) renderplayer(player1, getplayermodelinfo(player1), teamskins || m_teammode ? 1 : 0, 1, mainpass); - entities::renderentities(); - renderbouncers(); - renderprojectiles(); - - endmodelbatches(); - } - - VARP(hudgun, 0, 1, 1); - VARP(hudgunsway, 0, 1, 1); - VARP(teamhudguns, 0, 1, 1); - VARP(chainsawhudgun, 0, 1, 1); - VAR(testhudgun, 0, 0, 1); - - FVAR(swaystep, 1, 35.0f, 100); - FVAR(swayside, 0, 0.04f, 1); - FVAR(swayup, -1, 0.05f, 1); - - float swayfade = 0, swayspeed = 0, swaydist = 0; - vec swaydir(0, 0, 0); - - void swayhudgun(int curtime) - { - fpsent *d = hudplayer(); - if(d->state != CS_SPECTATOR) - { - if(d->physstate >= PHYS_SLOPE) - { - swayspeed = min(sqrtf(d->vel.x*d->vel.x + d->vel.y*d->vel.y), d->maxspeed); - swaydist += swayspeed*curtime/1000.0f; - swaydist = fmod(swaydist, 2*swaystep); - swayfade = 1; - } - else if(swayfade > 0) - { - swaydist += swayspeed*swayfade*curtime/1000.0f; - swaydist = fmod(swaydist, 2*swaystep); - swayfade -= 0.5f*(curtime*d->maxspeed)/(swaystep*1000.0f); - } - - float k = pow(0.7f, curtime/10.0f); - swaydir.mul(k); - vec vel(d->vel); - vel.add(d->falling); - swaydir.add(vec(vel).mul((1-k)/(15*max(vel.magnitude(), d->maxspeed)))); - } - } - - struct hudent : dynent - { - hudent() { type = ENT_CAMERA; } - } guninterp; - - SVARP(hudgunsdir, ""); - - void drawhudmodel(fpsent *d, int anim, float speed = 0, int base = 0) - { - if(d->gunselect>GUN_PISTOL) return; - - vec sway; - vecfromyawpitch(d->yaw, 0, 0, 1, sway); - float steps = swaydist/swaystep*M_PI; - sway.mul(swayside*cosf(steps)); - sway.z = swayup*(fabs(sinf(steps)) - 1); - sway.add(swaydir).add(d->o); - if(!hudgunsway) sway = d->o; - -#if 0 - if(player1->state!=CS_DEAD && player1->quadmillis) - { - float t = 0.5f + 0.5f*sinf(2*M_PI*lastmillis/1000.0f); - color.y = color.y*(1-t) + t; - } -#endif - const playermodelinfo &mdl = getplayermodelinfo(d); - defformatstring(gunname, "%s/%s", hudgunsdir[0] ? hudgunsdir : mdl.hudguns, guns[d->gunselect].file); - if((m_teammode || teamskins) && teamhudguns) - concatstring(gunname, d==player1 || isteam(d->team, player1->team) ? "/blue" : "/red"); - else if(testteam > 1) - concatstring(gunname, testteam==2 ? "/blue" : "/red"); - modelattach a[2]; - d->muzzle = vec(-1, -1, -1); - a[0] = modelattach("tag_muzzle", &d->muzzle); - dynent *interp = NULL; - if(d->gunselect==GUN_FIST && chainsawhudgun) - { - anim |= ANIM_LOOP; - base = 0; - interp = &guninterp; - } - rendermodel(NULL, gunname, anim, sway, testhudgun ? 0 : d->yaw+90, testhudgun ? 0 : d->pitch, MDL_LIGHT|MDL_HUD, interp, a, base, (int)ceil(speed)); - if(d->muzzle.x >= 0) d->muzzle = calcavatarpos(d->muzzle, 12); - } - - void drawhudgun() - { - fpsent *d = hudplayer(); - if(d->state==CS_SPECTATOR || d->state==CS_EDITING || !hudgun || editmode) - { - d->muzzle = player1->muzzle = vec(-1, -1, -1); - return; - } - - int rtime = guns[d->gunselect].attackdelay; - if(d->lastaction && d->lastattackgun==d->gunselect && lastmillis-d->lastaction<rtime) - { - drawhudmodel(d, ANIM_GUN_SHOOT|ANIM_SETSPEED, rtime/17.0f, d->lastaction); - } - else - { - drawhudmodel(d, ANIM_GUN_IDLE|ANIM_LOOP); - } - } - - void renderavatar() - { - drawhudgun(); - } - - void renderplayerpreview(int model, int team, int weap) - { - static fpsent *previewent = NULL; - if(!previewent) - { - previewent = new fpsent; - previewent->light.color = vec(1, 1, 1); - previewent->light.dir = vec(0, -1, 2).normalize(); - loopi(GUN_PISTOL-GUN_FIST) previewent->ammo[GUN_FIST+1+i] = 1; - } - float height = previewent->eyeheight + previewent->aboveeye, - zrad = height/2; - vec2 xyrad = vec2(previewent->xradius, previewent->yradius).max(height/4); - previewent->o = calcmodelpreviewpos(vec(xyrad, zrad), previewent->yaw).addz(previewent->eyeheight - zrad); - previewent->gunselect = clamp(weap, int(GUN_FIST), int(GUN_PISTOL)); - previewent->light.millis = -1; - const playermodelinfo *mdlinfo = getplayermodelinfo(model); - if(!mdlinfo) return; - renderplayer(previewent, *mdlinfo, team >= 0 && team <= 2 ? team : 0, 1, false); - } - - vec hudgunorigin(int gun, const vec &from, const vec &to, fpsent *d) - { - if(d->muzzle.x >= 0) return d->muzzle; - vec offset(from); - if(d!=hudplayer() || isthirdperson()) - { - vec front, right; - vecfromyawpitch(d->yaw, d->pitch, 1, 0, front); - offset.add(front.mul(d->radius)); - if(d->type!=ENT_AI) - { - offset.z += (d->aboveeye + d->eyeheight)*0.75f - d->eyeheight; - vecfromyawpitch(d->yaw, 0, 0, -1, right); - offset.add(right.mul(0.5f*d->radius)); - offset.add(front); - } - return offset; - } - offset.add(vec(to).sub(from).normalize().mul(2)); - if(hudgun) - { - offset.sub(vec(camup).mul(1.0f)); - offset.add(vec(camright).mul(0.8f)); - } - else offset.sub(vec(camup).mul(0.8f)); - return offset; - } - - void preloadweapons() - { - const playermodelinfo &mdl = getplayermodelinfo(player1); - loopi(NUMGUNS) - { - const char *file = guns[i].file; - if(!file) continue; - string fname; - if((m_teammode || teamskins) && teamhudguns) - { - formatstring(fname, "%s/%s/blue", hudgunsdir[0] ? hudgunsdir : mdl.hudguns, file); - preloadmodel(fname); - } - else - { - formatstring(fname, "%s/%s", hudgunsdir[0] ? hudgunsdir : mdl.hudguns, file); - preloadmodel(fname); - } - formatstring(fname, "vwep/%s", file); - preloadmodel(fname); - } - } - - void preloadsounds() - { - for(int i = S_JUMP; i <= S_HIT; i++) preloadsound(i); - } - - void preload() - { - if(hudgun) preloadweapons(); - preloadbouncers(); - preloadplayermodel(); - preloadsounds(); - entities::preloadentities(); - } + const playermodelinfo &mdl = getplayermodelinfo(d); + defformatstring(gunname, "%s/%s", hudgunsdir[0] ? hudgunsdir : mdl.hudguns, guns[d->gunselect].file); + if((m_teammode || teamskins) && teamhudguns) + concatstring(gunname, d==player1 || isteam(d->team, player1->team) ? "/blue" : "/red"); + else if(testteam > 1) + concatstring(gunname, testteam==2 ? "/blue" : "/red"); + modelattach a[2]; + d->muzzle = vec(-1, -1, -1); + a[0] = modelattach("tag_muzzle", &d->muzzle); + dynent *interp = NULL; + if(d->gunselect==GUN_FIST && chainsawhudgun) + { + anim |= ANIM_LOOP; + base = 0; + interp = &guninterp; + } + rendermodel(NULL, gunname, anim, sway, testhudgun ? 0 : d->yaw+90, testhudgun ? 0 : d->pitch, MDL_LIGHT|MDL_HUD, interp, a, base, (int)ceil(speed)); + if(d->muzzle.x >= 0) d->muzzle = calcavatarpos(d->muzzle, 12); + } + + void drawhudgun() + { + fpsent *d = hudplayer(); + if(d->state==CS_SPECTATOR || d->state==CS_EDITING || !hudgun || editmode) + { + d->muzzle = player1->muzzle = vec(-1, -1, -1); + return; + } + + int rtime = guns[d->gunselect].attackdelay; + if(d->lastaction && d->lastattackgun==d->gunselect && lastmillis-d->lastaction<rtime) + { + drawhudmodel(d, ANIM_GUN_SHOOT|ANIM_SETSPEED, rtime/17.0f, d->lastaction); + } + else + { + drawhudmodel(d, ANIM_GUN_IDLE|ANIM_LOOP); + } + } + + void renderavatar() + { + drawhudgun(); + } + + void renderplayerpreview(int model, int team, int weap) + { + static fpsent *previewent = NULL; + if(!previewent) + { + previewent = new fpsent; + previewent->light.color = vec(1, 1, 1); + previewent->light.dir = vec(0, -1, 2).normalize(); + loopi(GUN_PISTOL-GUN_FIST) previewent->ammo[GUN_FIST+1+i] = 1; + } + float height = previewent->eyeheight + previewent->aboveeye, + zrad = height/2; + vec2 xyrad = vec2(previewent->xradius, previewent->yradius).max(height/4); + previewent->o = calcmodelpreviewpos(vec(xyrad, zrad), previewent->yaw).addz(previewent->eyeheight - zrad); + previewent->gunselect = clamp(weap, int(GUN_FIST), int(GUN_PISTOL)); + previewent->light.millis = -1; + const playermodelinfo *mdlinfo = getplayermodelinfo(model); + if(!mdlinfo) return; + renderplayer(previewent, *mdlinfo, team >= 0 && team <= 2 ? team : 0, 1, false); + } + + vec hudgunorigin(int gun, const vec &from, const vec &to, fpsent *d) + { + if(d->muzzle.x >= 0) return d->muzzle; + vec offset(from); + if(d!=hudplayer() || isthirdperson()) + { + vec front, right; + vecfromyawpitch(d->yaw, d->pitch, 1, 0, front); + offset.add(front.mul(d->radius)); + if(d->type!=ENT_AI) + { + offset.z += (d->aboveeye + d->eyeheight)*0.75f - d->eyeheight; + vecfromyawpitch(d->yaw, 0, 0, -1, right); + offset.add(right.mul(0.5f*d->radius)); + offset.add(front); + } + return offset; + } + offset.add(vec(to).sub(from).normalize().mul(2)); + if(hudgun) + { + offset.sub(vec(camup).mul(1.0f)); + offset.add(vec(camright).mul(0.8f)); + } + else offset.sub(vec(camup).mul(0.8f)); + return offset; + } + + void preloadweapons() + { + const playermodelinfo &mdl = getplayermodelinfo(player1); + loopi(NUMGUNS) + { + const char *file = guns[i].file; + if(!file) continue; + string fname; + if((m_teammode || teamskins) && teamhudguns) + { + formatstring(fname, "%s/%s/blue", hudgunsdir[0] ? hudgunsdir : mdl.hudguns, file); + preloadmodel(fname); + } + else + { + formatstring(fname, "%s/%s", hudgunsdir[0] ? hudgunsdir : mdl.hudguns, file); + preloadmodel(fname); + } + formatstring(fname, "vwep/%s", file); + preloadmodel(fname); + } + } + + void preloadsounds() + { + for(int i = S_JUMP; i <= S_HIT; i++) preloadsound(i); + } + + void preload() + { + if(hudgun) preloadweapons(); + preloadbouncers(); + preloadplayermodel(); + preloadsounds(); + entities::preloadentities(); + } } diff --git a/src/fpsgame/scoreboard.cpp b/src/fpsgame/scoreboard.cpp index 5e0ae17..96f8868 100644 --- a/src/fpsgame/scoreboard.cpp +++ b/src/fpsgame/scoreboard.cpp @@ -3,579 +3,579 @@ namespace game { - VARP(scoreboard2d, 0, 1, 1); - VARP(showservinfo, 0, 1, 1); - VARP(showclientnum, 0, 1, 1); - VARP(showpj, 0, 0, 1); - VARP(showping, 0, 1, 2); - VARP(showspectators, 0, 1, 1); - VARP(showspectatorping, 0, 1, 1); - VARP(highlightscore, 0, 1, 1); - VARP(showconnecting, 0, 0, 1); - VARP(hidefrags, 0, 0, 1); - VARP(showdeaths, 0, 1, 1); - VARP(showdamagedealt, 0, 1, 1); - - static hashset<teaminfo> teaminfos; - - void clearteaminfo() - { - teaminfos.clear(); - } - - void setteaminfo(const char *team, int frags) - { - teaminfo *t = teaminfos.access(team); - if(!t) { t = &teaminfos[team]; copystring(t->team, team, sizeof(t->team)); } - t->frags = frags; - } - - static inline bool playersort(const fpsent *a, const fpsent *b) - { - if(a->state==CS_SPECTATOR) - { - if(b->state==CS_SPECTATOR) return strcmp(a->name, b->name) < 0; - else return false; - } - else if(b->state==CS_SPECTATOR) return true; - if(a->frags > b->frags) return true; - if(a->frags < b->frags) return false; - return strcmp(a->name, b->name) < 0; - } - - void getbestplayers(vector<fpsent *> &best) - { - loopv(players) - { - fpsent *o = players[i]; - if(o->state!=CS_SPECTATOR) best.add(o); - } - best.sort(playersort); - while(best.length() > 1 && best.last()->frags < best[0]->frags) best.drop(); - } - - void getbestteams(vector<const char *> &best) - { - if(!hidefrags) - { - vector<teamscore> teamscores; - teamscores.sort(teamscore::compare); - while(teamscores.length() > 1 && teamscores.last().score < teamscores[0].score) teamscores.drop(); - loopv(teamscores) best.add(teamscores[i].team); - } - else - { - int bestfrags = INT_MIN; - enumerate(teaminfos, teaminfo, t, bestfrags = max(bestfrags, t.frags)); - if(bestfrags <= 0) loopv(players) - { - fpsent *o = players[i]; - if(o->state!=CS_SPECTATOR && !teaminfos.access(o->team) && best.htfind(o->team) < 0) { bestfrags = 0; best.add(o->team); } - } - enumerate(teaminfos, teaminfo, t, if(t.frags >= bestfrags) best.add(t.team)); - } - } - - struct scoregroup : teamscore - { - vector<fpsent *> players; - }; - static vector<scoregroup *> groups; - static vector<fpsent *> spectators; - - static inline bool scoregroupcmp(const scoregroup *x, const scoregroup *y) - { - if(!x->team) - { - if(y->team) return false; - } - else if(!y->team) return true; - if(x->score > y->score) return true; - if(x->score < y->score) return false; - if(x->players.length() > y->players.length()) return true; - if(x->players.length() < y->players.length()) return false; - return x->team && y->team && strcmp(x->team, y->team) < 0; - } - - static int groupplayers() - { - int numgroups = 0; - spectators.setsize(0); - loopv(players) - { - fpsent *o = players[i]; - if(!showconnecting && !o->name[0]) continue; - if(o->state==CS_SPECTATOR) { spectators.add(o); continue; } - const char *team = m_teammode && o->team[0] ? o->team : NULL; - bool found = false; - loopj(numgroups) - { - scoregroup &g = *groups[j]; - if(team!=g.team && (!team || !g.team || strcmp(team, g.team))) continue; - g.players.add(o); - found = true; - } - if(found) continue; - if(numgroups>=groups.length()) groups.add(new scoregroup); - scoregroup &g = *groups[numgroups++]; - g.team = team; - if(!team) g.score = 0; - else { teaminfo *ti = teaminfos.access(team); g.score = ti ? ti->frags : 0; } - g.players.setsize(0); - g.players.add(o); - } - loopi(numgroups) groups[i]->players.sort(playersort); - spectators.sort(playersort); - groups.sort(scoregroupcmp, 0, numgroups); - return numgroups; - } - - int statuscolor(fpsent *d, int color) - { - if(d->privilege) - { - color = d->privilege>=PRIV_ADMIN ? 0xFF8000 : (d->privilege>=PRIV_AUTH ? 0xC040C0 : 0x40FF80); - if(d->state==CS_DEAD) color = (color>>1)&0x7F7F7F; - } - else if(d->state==CS_DEAD) color = 0x606060; - return color; - } - - void renderscoreboard(g3d_gui &g, bool firstpass) - { - const ENetAddress *address = connectedpeer(); - if(showservinfo && address) - { - string hostname; - if(enet_address_get_host_ip(address, hostname, sizeof(hostname)) >= 0) - { - if(servinfo[0]) g.titlef("%.25s", 0xFFFF80, NULL, servinfo); - else g.titlef("%s:%d", 0xFFFF80, NULL, hostname, address->port); - } - } - - g.pushlist(); - g.spring(); - g.text(server::modename(gamemode), 0xFFFF80); - g.separator(); - const char *mname = getclientmap(); - g.text(mname[0] ? mname : "[new map]", 0xFFFF80); - extern int gamespeed; - if(gamespeed != 100) { g.separator(); g.textf("%d.%02dx", 0xFFFF80, NULL, gamespeed/100, gamespeed%100); } - if(m_timed && mname[0] && (maplimit >= 0 || intermission)) - { - g.separator(); - if(intermission) g.text("intermission", 0xFFFF80); - else - { - int secs = max(maplimit-lastmillis+999, 0)/1000, mins = secs/60; - secs %= 60; - g.pushlist(); - g.strut(mins >= 10 ? 4.5f : 3.5f); - g.textf("%d:%02d", 0xFFFF80, NULL, mins, secs); - g.poplist(); - } - } - if(ispaused()) { g.separator(); g.text("paused", 0xFFFF80); } - g.spring(); - g.poplist(); - - g.separator(); - - int numgroups = groupplayers(); - loopk(numgroups) - { - if((k%2)==0) g.pushlist(); // horizontal - - scoregroup &sg = *groups[k]; - int bgcolor = sg.team && m_teammode ? (isteam(player1->team, sg.team) ? 0x3030C0 : 0xC03030) : 0, - fgcolor = 0xFFFF80; - - g.pushlist(); // vertical - g.pushlist(); // horizontal - - #define loopscoregroup(o, b) \ - loopv(sg.players) \ - { \ - fpsent *o = sg.players[i]; \ - b; \ - } - - g.pushlist(); - if(sg.team && m_teammode) - { - g.pushlist(); - g.background(bgcolor, numgroups>1 ? 3 : 5); - g.strut(1); - g.poplist(); - } - g.text("", 0, " "); - loopscoregroup(o, - { - if(o==player1 && highlightscore && (multiplayer(false) || demoplayback || players.length() > 1)) - { - g.pushlist(); - g.background(0x808080, numgroups>1 ? 3 : 5); - } - const playermodelinfo &mdl = getplayermodelinfo(o); - const char *icon = sg.team && m_teammode ? (isteam(player1->team, sg.team) ? mdl.blueicon : mdl.redicon) : mdl.ffaicon; - g.text("", 0, icon); - if(o==player1 && highlightscore && (multiplayer(false) || demoplayback || players.length() > 1)) g.poplist(); - }); - g.poplist(); - - if(sg.team && m_teammode) - { - g.pushlist(); // vertical - - if(sg.score>=10000) g.textf("%s: WIN", fgcolor, NULL, sg.team); - else g.textf("%s: %d", fgcolor, NULL, sg.team, sg.score); - - g.pushlist(); // horizontal - } - - if(!hidefrags) - { - g.pushlist(); - g.strut(6); - g.text("frags", fgcolor); - loopscoregroup(o, g.textf("%d", 0xFFFFDD, NULL, o->frags)); - g.poplist(); - } - - if(showdeaths) - { - g.pushlist(); - g.strut(6); - g.text("deaths", fgcolor); - loopscoregroup(o, g.textf("%d", 0xFFFFDD, NULL, o->deaths)); - g.poplist(); - } - - if(showdamagedealt) - { - g.pushlist(); - g.strut(7); - g.text("damage", fgcolor); - loopscoregroup(o, g.textf("%d", 0xFFFFDD, NULL, o->totaldamage)); - g.poplist(); - } - - g.pushlist(); - g.text("name", fgcolor); - g.strut(12); - loopscoregroup(o, { g.textf("%s ", statuscolor(o, 0xFFFFDD), NULL, colorname(o)); }); - g.poplist(); - - if(multiplayer(false) || demoplayback) - { - if(showpj || showping) g.space(1); - - if(showpj && showping <= 1) - { - g.pushlist(); - g.strut(6); - g.text("pj", fgcolor); - loopscoregroup(o, - { - if(o->state==CS_LAGGED) g.text("LAG", 0xFFFFDD); - else g.textf("%d", 0xFFFFDD, NULL, o->plag); - }); - g.poplist(); - } - - if(showping > 1) - { - g.pushlist(); - g.strut(6); - - g.pushlist(); - g.text("ping", fgcolor); - g.space(1); - g.spring(); - g.text("pj", fgcolor); - g.poplist(); - - loopscoregroup(o, - { - fpsent *p = o->ownernum >= 0 ? getclient(o->ownernum) : o; - if(!p) p = o; - g.pushlist(); - if(p->state==CS_LAGGED) g.text("LAG", 0xFFFFDD); - else - { - g.textf("%d", 0xFFFFDD, NULL, p->ping); - g.space(1); - g.spring(); - g.textf("%d", 0xFFFFDD, NULL, o->plag); - } - g.poplist(); - - }); - g.poplist(); - } - else if(showping) - { - g.pushlist(); - g.text("ping", fgcolor); - g.strut(6); - loopscoregroup(o, - { - fpsent *p = o->ownernum >= 0 ? getclient(o->ownernum) : o; - if(!p) p = o; - if(!showpj && p->state==CS_LAGGED) g.text("LAG", 0xFFFFDD); - else g.textf("%d", 0xFFFFDD, NULL, p->ping); - }); - g.poplist(); - } - } - - if(showclientnum || player1->privilege>=PRIV_MASTER) - { - g.space(1); - g.pushlist(); - g.text("cn", fgcolor); - loopscoregroup(o, g.textf("%d", 0xFFFFDD, NULL, o->clientnum)); - g.poplist(); - } - - if(sg.team && m_teammode) - { - g.poplist(); // horizontal - g.poplist(); // vertical - } - - g.poplist(); // horizontal - g.poplist(); // vertical - - if(k+1<numgroups && (k+1)%2) g.space(2); - else g.poplist(); // horizontal - } - - if(showspectators && spectators.length()) - { - if(showclientnum || player1->privilege>=PRIV_MASTER) - { - g.pushlist(); - - g.pushlist(); - g.text("spectator", 0xFFFF80, " "); - g.strut(12); - loopv(spectators) - { - fpsent *o = spectators[i]; - if(o==player1 && highlightscore) - { - g.pushlist(); - g.background(0x808080, 3); - } - g.text(colorname(o), statuscolor(o, 0xFFFFDD), "spectator"); - if(o==player1 && highlightscore) g.poplist(); - } - g.poplist(); - - if((multiplayer(false) || demoplayback) && showspectatorping) - { - g.space(1); - g.pushlist(); - g.text("ping", 0xFFFF80); - g.strut(6); - loopv(spectators) - { - fpsent *o = spectators[i]; - fpsent *p = o->ownernum >= 0 ? getclient(o->ownernum) : o; - if(!p) p = o; - if(p->state==CS_LAGGED) g.text("LAG", 0xFFFFDD); - else g.textf("%d", 0xFFFFDD, NULL, p->ping); - } - g.poplist(); - } - - g.space(1); - g.pushlist(); - g.text("cn", 0xFFFF80); - loopv(spectators) g.textf("%d", 0xFFFFDD, NULL, spectators[i]->clientnum); - g.poplist(); - - g.poplist(); - } - else - { - g.textf("%d spectator%s", 0xFFFF80, " ", spectators.length(), spectators.length()!=1 ? "s" : ""); - loopv(spectators) - { - if((i%3)==0) - { - g.pushlist(); - g.text("", 0xFFFFDD, "spectator"); - } - fpsent *o = spectators[i]; - if(o==player1 && highlightscore) - { - g.pushlist(); - g.background(0x808080); - } - g.text(colorname(o), statuscolor(o, 0xFFFFDD)); - if(o==player1 && highlightscore) g.poplist(); - if(i+1<spectators.length() && (i+1)%3) g.space(1); - else g.poplist(); - } - } - } - - /// PW - g.separator(); - g.pushlist(); - g.textf(" %d%% ", 0x787878, "chainsaw.png", pwaccuracy[0]); - g.textf(" %d%% ", 0xfba6a6, "shotgun.png", pwaccuracy[1]); - g.textf(" %d%% ", 0x7bc77a, "chaingun.png", pwaccuracy[2]); - g.textf(" %d%% ", 0xefd7a6, "rocket_launcher.png", pwaccuracy[3]); - g.textf(" %d%% ", 0x8f91e7, "rifle.png", pwaccuracy[4]); - g.textf(" %d%% ", 0x9ee5e5, "grenade_launcher.png", pwaccuracy[5]); - g.textf(" %d%% ", 0xc3c3c3, "pistol.png", pwaccuracy[6]); - g.poplist(); - g.separator(); - g.pushlist(); - g.textf(" x %d ", 0xffffff, "blue_armour.png", pwitemspicked[0]); - g.textf(" x %d ", 0xffffff, "green_armour.png", pwitemspicked[1]); - g.textf(" x %d ", 0xffffff, "yellow_armour.png", pwitemspicked[2]); - g.textf(" x %d ", 0xffffff, "tiny_health.png", pwitemspicked[3]); - g.textf(" x %d ", 0xffffff, "health.png", pwitemspicked[4]); - g.textf(" x %d ", 0xffffff, "health_boost.png", pwitemspicked[5]); - g.textf(" x %d ", 0xffffff, "quad_damage.png", pwitemspicked[6]); - g.poplist(); - } - - struct scoreboardgui : g3d_callback - { - bool showing; - vec menupos; - int menustart; - - scoreboardgui() : showing(false) {} - - void show(bool on) - { - if(!showing && on) - { - menupos = menuinfrontofplayer(); - menustart = starttime(); - } - showing = on; - } - - void gui(g3d_gui &g, bool firstpass) - { - g.start(menustart, 0.03f, NULL, false); - renderscoreboard(g, firstpass); - g.end(); - } - - void render() - { - if(showing) g3d_addgui(this, menupos, (scoreboard2d ? GUI_FORCE_2D : GUI_2D | GUI_FOLLOW) | GUI_BOTTOM); - } - - } scoreboard; - - void g3d_gamemenus() - { - scoreboard.render(); - } - - VARFN(scoreboard, showscoreboard, 0, 0, 1, scoreboard.show(showscoreboard!=0)); - - void showscores(bool on) - { - showscoreboard = on ? 1 : 0; - scoreboard.show(on); - } - ICOMMAND(showscores, "D", (int *down), showscores(*down!=0)); - - VARP(hudscore, 0, 0, 1); - FVARP(hudscorescale, 1e-3f, 1.0f, 1e3f); - VARP(hudscorealign, -1, 0, 1); - FVARP(hudscorex, 0, 0.50f, 1); - FVARP(hudscorey, 0, 0.03f, 1); - HVARP(hudscoreplayercolour, 0, 0x60A0FF, 0xFFFFFF); - HVARP(hudscoreenemycolour, 0, 0xFF4040, 0xFFFFFF); - VARP(hudscorealpha, 0, 255, 255); - VARP(hudscoresep, 0, 200, 1000); - - void drawhudscore(int w, int h) - { - int numgroups = groupplayers(); - if(!numgroups) return; - - scoregroup *g = groups[0]; - int score = INT_MIN, score2 = INT_MIN; - bool best = false; - if(m_teammode) - { - score = g->score; - best = isteam(player1->team, g->team); - if(numgroups > 1) - { - if(best) score2 = groups[1]->score; - else for(int i = 1; i < groups.length(); ++i) if(isteam(player1->team, groups[i]->team)) { score2 = groups[i]->score; break; } - if(score2 == INT_MIN) - { - fpsent *p = followingplayer(player1); - if(p->state==CS_SPECTATOR) score2 = groups[1]->score; - } - } - } - else - { - fpsent *p = followingplayer(player1); - score = g->players[0]->frags; - best = p == g->players[0]; - if(g->players.length() > 1) - { - if(best || p->state==CS_SPECTATOR) score2 = g->players[1]->frags; - else score2 = p->frags; - } - } - if(score == score2 && !best) best = true; - - score = clamp(score, -999, 9999); - defformatstring(buf, "%d", score); - int tw = 0, th = 0; - text_bounds(buf, tw, th); - - string buf2; - int tw2 = 0, th2 = 0; - if(score2 > INT_MIN) - { - score2 = clamp(score2, -999, 9999); - formatstring(buf2, "%d", score2); - text_bounds(buf2, tw2, th2); - } - - int fw = 0, fh = 0; - text_bounds("00", fw, fh); - fw = max(fw, max(tw, tw2)); - - vec2 offset = vec2(hudscorex, hudscorey).mul(vec2(w, h).div(hudscorescale)); - if(hudscorealign == 1) offset.x -= 2*fw + hudscoresep; - else if(hudscorealign == 0) offset.x -= (2*fw + hudscoresep) / 2.0f; - vec2 offset2 = offset; - offset.x += (fw-tw)/2.0f; - offset.y -= th/2.0f; - offset2.x += fw + hudscoresep + (fw-tw2)/2.0f; - offset2.y -= th2/2.0f; - - pushhudmatrix(); - hudmatrix.scale(hudscorescale, hudscorescale, 1); - flushhudmatrix(); - - int color = hudscoreplayercolour, color2 = hudscoreenemycolour; - if(!best) swap(color, color2); - - draw_text(buf, int(offset.x), int(offset.y), (color>>16)&0xFF, (color>>8)&0xFF, color&0xFF, hudscorealpha); - if(score2 > INT_MIN) draw_text(buf2, int(offset2.x), int(offset2.y), (color2>>16)&0xFF, (color2>>8)&0xFF, color2&0xFF, hudscorealpha); - - pophudmatrix(); - } + VARP(scoreboard2d, 0, 1, 1); + VARP(showservinfo, 0, 1, 1); + VARP(showclientnum, 0, 1, 1); + VARP(showpj, 0, 0, 1); + VARP(showping, 0, 1, 2); + VARP(showspectators, 0, 1, 1); + VARP(showspectatorping, 0, 1, 1); + VARP(highlightscore, 0, 1, 1); + VARP(showconnecting, 0, 0, 1); + VARP(hidefrags, 0, 0, 1); + VARP(showdeaths, 0, 1, 1); + VARP(showdamagedealt, 0, 1, 1); + + static hashset<teaminfo> teaminfos; + + void clearteaminfo() + { + teaminfos.clear(); + } + + void setteaminfo(const char *team, int frags) + { + teaminfo *t = teaminfos.access(team); + if(!t) { t = &teaminfos[team]; copystring(t->team, team, sizeof(t->team)); } + t->frags = frags; + } + + static inline bool playersort(const fpsent *a, const fpsent *b) + { + if(a->state==CS_SPECTATOR) + { + if(b->state==CS_SPECTATOR) return strcmp(a->name, b->name) < 0; + else return false; + } + else if(b->state==CS_SPECTATOR) return true; + if(a->frags > b->frags) return true; + if(a->frags < b->frags) return false; + return strcmp(a->name, b->name) < 0; + } + + void getbestplayers(vector<fpsent *> &best) + { + loopv(players) + { + fpsent *o = players[i]; + if(o->state!=CS_SPECTATOR) best.add(o); + } + best.sort(playersort); + while(best.length() > 1 && best.last()->frags < best[0]->frags) best.drop(); + } + + void getbestteams(vector<const char *> &best) + { + if(!hidefrags) + { + vector<teamscore> teamscores; + teamscores.sort(teamscore::compare); + while(teamscores.length() > 1 && teamscores.last().score < teamscores[0].score) teamscores.drop(); + loopv(teamscores) best.add(teamscores[i].team); + } + else + { + int bestfrags = INT_MIN; + enumerate(teaminfos, teaminfo, t, bestfrags = max(bestfrags, t.frags)); + if(bestfrags <= 0) loopv(players) + { + fpsent *o = players[i]; + if(o->state!=CS_SPECTATOR && !teaminfos.access(o->team) && best.htfind(o->team) < 0) { bestfrags = 0; best.add(o->team); } + } + enumerate(teaminfos, teaminfo, t, if(t.frags >= bestfrags) best.add(t.team)); + } + } + + struct scoregroup : teamscore + { + vector<fpsent *> players; + }; + static vector<scoregroup *> groups; + static vector<fpsent *> spectators; + + static inline bool scoregroupcmp(const scoregroup *x, const scoregroup *y) + { + if(!x->team) + { + if(y->team) return false; + } + else if(!y->team) return true; + if(x->score > y->score) return true; + if(x->score < y->score) return false; + if(x->players.length() > y->players.length()) return true; + if(x->players.length() < y->players.length()) return false; + return x->team && y->team && strcmp(x->team, y->team) < 0; + } + + static int groupplayers() + { + int numgroups = 0; + spectators.setsize(0); + loopv(players) + { + fpsent *o = players[i]; + if(!showconnecting && !o->name[0]) continue; + if(o->state==CS_SPECTATOR) { spectators.add(o); continue; } + const char *team = m_teammode && o->team[0] ? o->team : NULL; + bool found = false; + loopj(numgroups) + { + scoregroup &g = *groups[j]; + if(team!=g.team && (!team || !g.team || strcmp(team, g.team))) continue; + g.players.add(o); + found = true; + } + if(found) continue; + if(numgroups>=groups.length()) groups.add(new scoregroup); + scoregroup &g = *groups[numgroups++]; + g.team = team; + if(!team) g.score = 0; + else { teaminfo *ti = teaminfos.access(team); g.score = ti ? ti->frags : 0; } + g.players.setsize(0); + g.players.add(o); + } + loopi(numgroups) groups[i]->players.sort(playersort); + spectators.sort(playersort); + groups.sort(scoregroupcmp, 0, numgroups); + return numgroups; + } + + int statuscolor(fpsent *d, int color) + { + if(d->privilege) + { + color = d->privilege>=PRIV_ADMIN ? 0xFF8000 : (d->privilege>=PRIV_AUTH ? 0xC040C0 : 0x40FF80); + if(d->state==CS_DEAD) color = (color>>1)&0x7F7F7F; + } + else if(d->state==CS_DEAD) color = 0x606060; + return color; + } + + void renderscoreboard(g3d_gui &g, bool firstpass) + { + const ENetAddress *address = connectedpeer(); + if(showservinfo && address) + { + string hostname; + if(enet_address_get_host_ip(address, hostname, sizeof(hostname)) >= 0) + { + if(servinfo[0]) g.titlef("%.25s", 0xFFFF80, NULL, servinfo); + else g.titlef("%s:%d", 0xFFFF80, NULL, hostname, address->port); + } + } + + g.pushlist(); + g.spring(); + g.text(server::modename(gamemode), 0xFFFF80); + g.separator(); + const char *mname = getclientmap(); + g.text(mname[0] ? mname : "[new map]", 0xFFFF80); + extern int gamespeed; + if(gamespeed != 100) { g.separator(); g.textf("%d.%02dx", 0xFFFF80, NULL, gamespeed/100, gamespeed%100); } + if(m_timed && mname[0] && (maplimit >= 0 || intermission)) + { + g.separator(); + if(intermission) g.text("intermission", 0xFFFF80); + else + { + int secs = max(maplimit-lastmillis+999, 0)/1000, mins = secs/60; + secs %= 60; + g.pushlist(); + g.strut(mins >= 10 ? 4.5f : 3.5f); + g.textf("%d:%02d", 0xFFFF80, NULL, mins, secs); + g.poplist(); + } + } + if(ispaused()) { g.separator(); g.text("paused", 0xFFFF80); } + g.spring(); + g.poplist(); + + g.separator(); + + int numgroups = groupplayers(); + loopk(numgroups) + { + if((k%2)==0) g.pushlist(); // horizontal + + scoregroup &sg = *groups[k]; + int bgcolor = sg.team && m_teammode ? (isteam(player1->team, sg.team) ? 0x3030C0 : 0xC03030) : 0, + fgcolor = 0xFFFF80; + + g.pushlist(); // vertical + g.pushlist(); // horizontal + + #define loopscoregroup(o, b) \ + loopv(sg.players) \ + { \ + fpsent *o = sg.players[i]; \ + b; \ + } + + g.pushlist(); + if(sg.team && m_teammode) + { + g.pushlist(); + g.background(bgcolor, numgroups>1 ? 3 : 5); + g.strut(1); + g.poplist(); + } + g.text("", 0, " "); + loopscoregroup(o, + { + if(o==player1 && highlightscore && (multiplayer(false) || demoplayback || players.length() > 1)) + { + g.pushlist(); + g.background(0x808080, numgroups>1 ? 3 : 5); + } + const playermodelinfo &mdl = getplayermodelinfo(o); + const char *icon = sg.team && m_teammode ? (isteam(player1->team, sg.team) ? mdl.blueicon : mdl.redicon) : mdl.ffaicon; + g.text("", 0, icon); + if(o==player1 && highlightscore && (multiplayer(false) || demoplayback || players.length() > 1)) g.poplist(); + }); + g.poplist(); + + if(sg.team && m_teammode) + { + g.pushlist(); // vertical + + if(sg.score>=10000) g.textf("%s: WIN", fgcolor, NULL, sg.team); + else g.textf("%s: %d", fgcolor, NULL, sg.team, sg.score); + + g.pushlist(); // horizontal + } + + if(!hidefrags) + { + g.pushlist(); + g.strut(6); + g.text("frags", fgcolor); + loopscoregroup(o, g.textf("%d", 0xFFFFDD, NULL, o->frags)); + g.poplist(); + } + + if(showdeaths) + { + g.pushlist(); + g.strut(6); + g.text("deaths", fgcolor); + loopscoregroup(o, g.textf("%d", 0xFFFFDD, NULL, o->deaths)); + g.poplist(); + } + + if(showdamagedealt) + { + g.pushlist(); + g.strut(7); + g.text("damage", fgcolor); + loopscoregroup(o, g.textf("%d", 0xFFFFDD, NULL, o->totaldamage)); + g.poplist(); + } + + g.pushlist(); + g.text("name", fgcolor); + g.strut(12); + loopscoregroup(o, { g.textf("%s ", statuscolor(o, 0xFFFFDD), NULL, colorname(o)); }); + g.poplist(); + + if(multiplayer(false) || demoplayback) + { + if(showpj || showping) g.space(1); + + if(showpj && showping <= 1) + { + g.pushlist(); + g.strut(6); + g.text("pj", fgcolor); + loopscoregroup(o, + { + if(o->state==CS_LAGGED) g.text("LAG", 0xFFFFDD); + else g.textf("%d", 0xFFFFDD, NULL, o->plag); + }); + g.poplist(); + } + + if(showping > 1) + { + g.pushlist(); + g.strut(6); + + g.pushlist(); + g.text("ping", fgcolor); + g.space(1); + g.spring(); + g.text("pj", fgcolor); + g.poplist(); + + loopscoregroup(o, + { + fpsent *p = o->ownernum >= 0 ? getclient(o->ownernum) : o; + if(!p) p = o; + g.pushlist(); + if(p->state==CS_LAGGED) g.text("LAG", 0xFFFFDD); + else + { + g.textf("%d", 0xFFFFDD, NULL, p->ping); + g.space(1); + g.spring(); + g.textf("%d", 0xFFFFDD, NULL, o->plag); + } + g.poplist(); + + }); + g.poplist(); + } + else if(showping) + { + g.pushlist(); + g.text("ping", fgcolor); + g.strut(6); + loopscoregroup(o, + { + fpsent *p = o->ownernum >= 0 ? getclient(o->ownernum) : o; + if(!p) p = o; + if(!showpj && p->state==CS_LAGGED) g.text("LAG", 0xFFFFDD); + else g.textf("%d", 0xFFFFDD, NULL, p->ping); + }); + g.poplist(); + } + } + + if(showclientnum || player1->privilege>=PRIV_MASTER) + { + g.space(1); + g.pushlist(); + g.text("cn", fgcolor); + loopscoregroup(o, g.textf("%d", 0xFFFFDD, NULL, o->clientnum)); + g.poplist(); + } + + if(sg.team && m_teammode) + { + g.poplist(); // horizontal + g.poplist(); // vertical + } + + g.poplist(); // horizontal + g.poplist(); // vertical + + if(k+1<numgroups && (k+1)%2) g.space(2); + else g.poplist(); // horizontal + } + + if(showspectators && spectators.length()) + { + if(showclientnum || player1->privilege>=PRIV_MASTER) + { + g.pushlist(); + + g.pushlist(); + g.text("spectator", 0xFFFF80, " "); + g.strut(12); + loopv(spectators) + { + fpsent *o = spectators[i]; + if(o==player1 && highlightscore) + { + g.pushlist(); + g.background(0x808080, 3); + } + g.text(colorname(o), statuscolor(o, 0xFFFFDD), "spectator"); + if(o==player1 && highlightscore) g.poplist(); + } + g.poplist(); + + if((multiplayer(false) || demoplayback) && showspectatorping) + { + g.space(1); + g.pushlist(); + g.text("ping", 0xFFFF80); + g.strut(6); + loopv(spectators) + { + fpsent *o = spectators[i]; + fpsent *p = o->ownernum >= 0 ? getclient(o->ownernum) : o; + if(!p) p = o; + if(p->state==CS_LAGGED) g.text("LAG", 0xFFFFDD); + else g.textf("%d", 0xFFFFDD, NULL, p->ping); + } + g.poplist(); + } + + g.space(1); + g.pushlist(); + g.text("cn", 0xFFFF80); + loopv(spectators) g.textf("%d", 0xFFFFDD, NULL, spectators[i]->clientnum); + g.poplist(); + + g.poplist(); + } + else + { + g.textf("%d spectator%s", 0xFFFF80, " ", spectators.length(), spectators.length()!=1 ? "s" : ""); + loopv(spectators) + { + if((i%3)==0) + { + g.pushlist(); + g.text("", 0xFFFFDD, "spectator"); + } + fpsent *o = spectators[i]; + if(o==player1 && highlightscore) + { + g.pushlist(); + g.background(0x808080); + } + g.text(colorname(o), statuscolor(o, 0xFFFFDD)); + if(o==player1 && highlightscore) g.poplist(); + if(i+1<spectators.length() && (i+1)%3) g.space(1); + else g.poplist(); + } + } + } + + /// PW + g.separator(); + g.pushlist(); + g.textf(" %d%% ", 0x787878, "chainsaw.png", pwaccuracy[0]); + g.textf(" %d%% ", 0xfba6a6, "shotgun.png", pwaccuracy[1]); + g.textf(" %d%% ", 0x7bc77a, "chaingun.png", pwaccuracy[2]); + g.textf(" %d%% ", 0xefd7a6, "rocket_launcher.png", pwaccuracy[3]); + g.textf(" %d%% ", 0x8f91e7, "rifle.png", pwaccuracy[4]); + g.textf(" %d%% ", 0x9ee5e5, "grenade_launcher.png", pwaccuracy[5]); + g.textf(" %d%% ", 0xc3c3c3, "pistol.png", pwaccuracy[6]); + g.poplist(); + g.separator(); + g.pushlist(); + g.textf(" x %d ", 0xffffff, "blue_armour.png", pwitemspicked[0]); + g.textf(" x %d ", 0xffffff, "green_armour.png", pwitemspicked[1]); + g.textf(" x %d ", 0xffffff, "yellow_armour.png", pwitemspicked[2]); + g.textf(" x %d ", 0xffffff, "tiny_health.png", pwitemspicked[3]); + g.textf(" x %d ", 0xffffff, "health.png", pwitemspicked[4]); + g.textf(" x %d ", 0xffffff, "health_boost.png", pwitemspicked[5]); + g.textf(" x %d ", 0xffffff, "quad_damage.png", pwitemspicked[6]); + g.poplist(); + } + + struct scoreboardgui : g3d_callback + { + bool showing; + vec menupos; + int menustart; + + scoreboardgui() : showing(false) {} + + void show(bool on) + { + if(!showing && on) + { + menupos = menuinfrontofplayer(); + menustart = starttime(); + } + showing = on; + } + + void gui(g3d_gui &g, bool firstpass) + { + g.start(menustart, 0.03f, NULL, false); + renderscoreboard(g, firstpass); + g.end(); + } + + void render() + { + if(showing) g3d_addgui(this, menupos, (scoreboard2d ? GUI_FORCE_2D : GUI_2D | GUI_FOLLOW) | GUI_BOTTOM); + } + + } scoreboard; + + void g3d_gamemenus() + { + scoreboard.render(); + } + + VARFN(scoreboard, showscoreboard, 0, 0, 1, scoreboard.show(showscoreboard!=0)); + + void showscores(bool on) + { + showscoreboard = on ? 1 : 0; + scoreboard.show(on); + } + ICOMMAND(showscores, "D", (int *down), showscores(*down!=0)); + + VARP(hudscore, 0, 0, 1); + FVARP(hudscorescale, 1e-3f, 1.0f, 1e3f); + VARP(hudscorealign, -1, 0, 1); + FVARP(hudscorex, 0, 0.50f, 1); + FVARP(hudscorey, 0, 0.03f, 1); + HVARP(hudscoreplayercolour, 0, 0x60A0FF, 0xFFFFFF); + HVARP(hudscoreenemycolour, 0, 0xFF4040, 0xFFFFFF); + VARP(hudscorealpha, 0, 255, 255); + VARP(hudscoresep, 0, 200, 1000); + + void drawhudscore(int w, int h) + { + int numgroups = groupplayers(); + if(!numgroups) return; + + scoregroup *g = groups[0]; + int score = INT_MIN, score2 = INT_MIN; + bool best = false; + if(m_teammode) + { + score = g->score; + best = isteam(player1->team, g->team); + if(numgroups > 1) + { + if(best) score2 = groups[1]->score; + else for(int i = 1; i < groups.length(); ++i) if(isteam(player1->team, groups[i]->team)) { score2 = groups[i]->score; break; } + if(score2 == INT_MIN) + { + fpsent *p = followingplayer(player1); + if(p->state==CS_SPECTATOR) score2 = groups[1]->score; + } + } + } + else + { + fpsent *p = followingplayer(player1); + score = g->players[0]->frags; + best = p == g->players[0]; + if(g->players.length() > 1) + { + if(best || p->state==CS_SPECTATOR) score2 = g->players[1]->frags; + else score2 = p->frags; + } + } + if(score == score2 && !best) best = true; + + score = clamp(score, -999, 9999); + defformatstring(buf, "%d", score); + int tw = 0, th = 0; + text_bounds(buf, tw, th); + + string buf2; + int tw2 = 0, th2 = 0; + if(score2 > INT_MIN) + { + score2 = clamp(score2, -999, 9999); + formatstring(buf2, "%d", score2); + text_bounds(buf2, tw2, th2); + } + + int fw = 0, fh = 0; + text_bounds("00", fw, fh); + fw = max(fw, max(tw, tw2)); + + vec2 offset = vec2(hudscorex, hudscorey).mul(vec2(w, h).div(hudscorescale)); + if(hudscorealign == 1) offset.x -= 2*fw + hudscoresep; + else if(hudscorealign == 0) offset.x -= (2*fw + hudscoresep) / 2.0f; + vec2 offset2 = offset; + offset.x += (fw-tw)/2.0f; + offset.y -= th/2.0f; + offset2.x += fw + hudscoresep + (fw-tw2)/2.0f; + offset2.y -= th2/2.0f; + + pushhudmatrix(); + hudmatrix.scale(hudscorescale, hudscorescale, 1); + flushhudmatrix(); + + int color = hudscoreplayercolour, color2 = hudscoreenemycolour; + if(!best) swap(color, color2); + + draw_text(buf, int(offset.x), int(offset.y), (color>>16)&0xFF, (color>>8)&0xFF, color&0xFF, hudscorealpha); + if(score2 > INT_MIN) draw_text(buf2, int(offset2.x), int(offset2.y), (color2>>16)&0xFF, (color2>>8)&0xFF, color2&0xFF, hudscorealpha); + + pophudmatrix(); + } } diff --git a/src/fpsgame/server.cpp b/src/fpsgame/server.cpp index a3f0481..ec781c6 100644 --- a/src/fpsgame/server.cpp +++ b/src/fpsgame/server.cpp @@ -2,17 +2,17 @@ namespace game { - void parseoptions(vector<const char *> &args) - { - loopv(args) + void parseoptions(vector<const char *> &args) + { + loopv(args) #ifndef STANDALONE - if(!game::clientoption(args[i])) + if(!game::clientoption(args[i])) #endif - if(!server::serveroption(args[i])) - conoutf(CON_ERROR, "unknown command-line option: %s", args[i]); - } + if(!server::serveroption(args[i])) + conoutf(CON_ERROR, "unknown command-line option: %s", args[i]); + } - const char *gameident() { return "fps"; } + const char *gameident() { return "fps"; } } VAR(regenbluearmour, 0, 1, 1); @@ -21,603 +21,603 @@ extern ENetAddress masteraddress; namespace server { - struct server_entity // server side version of "entity" type - { - int type; - int spawntime; - bool spawned; - }; - - static const int DEATHMILLIS = 300; - - struct clientinfo; - - struct gameevent - { - virtual ~gameevent() {} - - virtual bool flush(clientinfo *ci, int fmillis); - virtual void process(clientinfo *ci) {} - - virtual bool keepable() const { return false; } - }; - - struct timedevent : gameevent - { - int millis; - - bool flush(clientinfo *ci, int fmillis); - }; - - struct hitinfo - { - int target; - int lifesequence; - int rays; - float dist; - vec dir; - }; - - struct shotevent : timedevent - { - int id, gun; - vec from, to; - vector<hitinfo> hits; - - void process(clientinfo *ci); - }; - - struct explodeevent : timedevent - { - int id, gun; - vector<hitinfo> hits; - - bool keepable() const { return true; } - - void process(clientinfo *ci); - }; - - struct suicideevent : gameevent - { - void process(clientinfo *ci); - }; - - struct pickupevent : gameevent - { - int ent; - - void process(clientinfo *ci); - }; - - template <int N> - struct projectilestate - { - int projs[N]; - int numprojs; - - projectilestate() : numprojs(0) {} - - void reset() { numprojs = 0; } - - void add(int val) - { - if(numprojs>=N) numprojs = 0; - projs[numprojs++] = val; - } - - bool remove(int val) - { - loopi(numprojs) if(projs[i]==val) - { - projs[i] = projs[--numprojs]; - return true; - } - return false; - } - }; - - struct gamestate : fpsstate - { - vec o; - int state, editstate; - int lastdeath, deadflush, lastspawn, lifesequence; - int lastshot; - projectilestate<8> rockets, grenades; - int frags, flags, deaths, teamkills, shotdamage, damage, tokens; - int lasttimeplayed, timeplayed; - float effectiveness; - - gamestate() : state(CS_DEAD), editstate(CS_DEAD), lifesequence(0) {} - - bool isalive(int gamemillis) - { - return state==CS_ALIVE || (state==CS_DEAD && gamemillis - lastdeath <= DEATHMILLIS); - } - - bool waitexpired(int gamemillis) - { - return gamemillis - lastshot >= gunwait; - } - - void reset() - { - if(state!=CS_SPECTATOR) state = editstate = CS_DEAD; - maxhealth = 100; - maxarmour = 50; - rockets.reset(); - grenades.reset(); - - timeplayed = 0; - effectiveness = 0; - frags = flags = deaths = teamkills = shotdamage = damage = tokens = 0; - - lastdeath = 0; - - respawn(); - } - - void respawn() - { - fpsstate::respawn(); - o = vec(-1e10f, -1e10f, -1e10f); - deadflush = 0; - lastspawn = -1; - lastshot = 0; - tokens = 0; - } - - void reassign() - { - respawn(); - rockets.reset(); - grenades.reset(); - } - }; - - struct savedscore - { - uint ip; - string name; - int frags, flags, deaths, teamkills, shotdamage, damage; - int timeplayed; - float effectiveness; - - void save(gamestate &gs) - { - frags = gs.frags; - flags = gs.flags; - deaths = gs.deaths; - teamkills = gs.teamkills; - shotdamage = gs.shotdamage; - damage = gs.damage; - timeplayed = gs.timeplayed; - effectiveness = gs.effectiveness; - } - - void restore(gamestate &gs) - { - gs.frags = frags; - gs.flags = flags; - gs.deaths = deaths; - gs.teamkills = teamkills; - gs.shotdamage = shotdamage; - gs.damage = damage; - gs.timeplayed = timeplayed; - gs.effectiveness = effectiveness; - } - }; - - extern int gamemillis, nextexceeded; - - struct clientinfo - { - int clientnum, ownernum, connectmillis, sessionid, overflow; - string name, team, mapvote; - int playermodel; - int modevote; - int privilege; - bool connected, local, timesync; - int gameoffset, lastevent, pushed, exceeded; - gamestate state; - vector<gameevent *> events; - vector<uchar> position, messages; - uchar *wsdata; - int wslen; - vector<clientinfo *> bots; - int ping, aireinit; - string clientmap; - int mapcrc; - bool warned, gameclip; - ENetPacket *getdemo, *getmap, *clipboard; - int lastclipboard, needclipboard; - int connectauth; - uint authreq; - string authname, authdesc; - void *authchallenge; - int authkickvictim; - char *authkickreason; - - clientinfo() : getdemo(NULL), getmap(NULL), clipboard(NULL), authchallenge(NULL), authkickreason(NULL) { reset(); } - ~clientinfo() { events.deletecontents(); cleanclipboard(); cleanauth(); } - - void addevent(gameevent *e) - { - if(state.state==CS_SPECTATOR || events.length()>100) delete e; - else events.add(e); - } - - enum - { - PUSHMILLIS = 3000 - }; - - int calcpushrange() - { - ENetPeer *peer = getclientpeer(ownernum); - return PUSHMILLIS + ((peer) ? (int) (peer->roundTripTime + peer->roundTripTimeVariance) : (int) ENET_PEER_DEFAULT_ROUND_TRIP_TIME); - } - - bool checkpushed(int millis, int range) - { - return millis >= pushed - range && millis <= pushed + range; - } - - void scheduleexceeded() - { - if(state.state!=CS_ALIVE || !exceeded) return; - int range = calcpushrange(); - if(!nextexceeded || exceeded + range < nextexceeded) nextexceeded = exceeded + range; - } - - void setexceeded() - { - if(state.state==CS_ALIVE && !exceeded && !checkpushed(gamemillis, calcpushrange())) exceeded = gamemillis; - scheduleexceeded(); - } - - void setpushed() - { - pushed = max(pushed, gamemillis); - if(exceeded && checkpushed(exceeded, calcpushrange())) exceeded = 0; - } - - bool checkexceeded() - { - return state.state==CS_ALIVE && exceeded && gamemillis > exceeded + calcpushrange(); - } - - void mapchange() - { - mapvote[0] = 0; - modevote = INT_MAX; - state.reset(); - events.deletecontents(); - overflow = 0; - timesync = false; - lastevent = 0; - exceeded = 0; - pushed = 0; - clientmap[0] = '\0'; - mapcrc = 0; - warned = false; - gameclip = false; - } - - void reassign() - { - state.reassign(); - events.deletecontents(); - timesync = false; - lastevent = 0; - } - - void cleanclipboard(bool fullclean = true) - { - if(clipboard) { if(--clipboard->referenceCount <= 0) enet_packet_destroy(clipboard); clipboard = NULL; } - if(fullclean) lastclipboard = 0; - } - - void cleanauthkick() - { - authkickvictim = -1; - DELETEA(authkickreason); - } - - void cleanauth(bool full = true) - { - authreq = 0; - if(authchallenge) { freechallenge(authchallenge); authchallenge = NULL; } - if(full) cleanauthkick(); - } - - void reset() - { - name[0] = team[0] = 0; - playermodel = -1; - privilege = PRIV_NONE; - connected = local = false; - connectauth = 0; - position.setsize(0); - messages.setsize(0); - ping = 0; - aireinit = 0; - needclipboard = 0; - cleanclipboard(); - cleanauth(); - mapchange(); - } - - int geteventmillis(int servmillis, int clientmillis) - { - if(!timesync || (events.empty() && state.waitexpired(servmillis))) - { - timesync = true; - gameoffset = servmillis - clientmillis; - return servmillis; - } - else return gameoffset + clientmillis; - } - }; - - struct ban - { - int time, expire; - uint ip; - }; - - namespace aiman - { - extern void removeai(clientinfo *ci); - extern void clearai(); - extern void checkai(); - extern void reqadd(clientinfo *ci, int skill); - extern void reqdel(clientinfo *ci); - extern void setbotlimit(clientinfo *ci, int limit); - extern void setbotbalance(clientinfo *ci, bool balance); - extern void changemap(); - extern void addclient(clientinfo *ci); - extern void changeteam(clientinfo *ci); - } - - #define MM_MODE 0xF - #define MM_AUTOAPPROVE 0x1000 - #define MM_PRIVSERV (MM_MODE | MM_AUTOAPPROVE) - #define MM_PUBSERV ((1<<MM_OPEN) | (1<<MM_VETO)) - #define MM_COOPSERV (MM_AUTOAPPROVE | MM_PUBSERV | (1<<MM_LOCKED)) - - bool notgotitems = true; // true when map has changed and waiting for clients to send item - int gamemode = 0; - int gamemillis = 0, gamelimit = 0, nextexceeded = 0, gamespeed = 100; - bool gamepaused = false, shouldstep = true; - - string smapname = ""; - int interm = 0; - enet_uint32 lastsend = 0; - int mastermode = MM_OPEN, mastermask = MM_PRIVSERV; - stream *mapdata = NULL; - - vector<uint> allowedips; - vector<ban> bannedips; - - void addban(uint ip, int expire) - { - allowedips.removeobj(ip); - ban b; - b.time = totalmillis; - b.expire = totalmillis + expire; - b.ip = ip; - loopv(bannedips) if(bannedips[i].expire - b.expire > 0) { bannedips.insert(i, b); return; } - bannedips.add(b); - } - - vector<clientinfo *> connects, clients, bots; - - void kickclients(uint ip, clientinfo *actor = NULL, int priv = PRIV_NONE) - { - loopvrev(clients) - { - clientinfo &c = *clients[i]; - if(c.state.aitype != AI_NONE || c.privilege >= PRIV_ADMIN || c.local) continue; - if(actor && ((c.privilege > priv && !actor->local) || c.clientnum == actor->clientnum)) continue; - //~if(getclientip(c.clientnum) == ip) disconnect_client(c.clientnum, DISC_KICK); - } - } - - struct maprotation - { - static int exclude; - int modes; - string map; - - int calcmodemask() const { return modes&(1<<NUMGAMEMODES) ? modes & ~exclude : modes; } - bool hasmode(int mode, int offset = STARTGAMEMODE) const { return (calcmodemask() & (1 << (mode-offset))) != 0; } - - int findmode(int mode) const - { - if(!hasmode(mode)) loopi(NUMGAMEMODES) if(hasmode(i, 0)) return i+STARTGAMEMODE; - return mode; - } - - bool match(int reqmode, const char *reqmap) const - { - return hasmode(reqmode) && (!map[0] || !reqmap[0] || !strcmp(map, reqmap)); - } - - bool includes(const maprotation &rot) const - { - return rot.modes == modes ? rot.map[0] && !map[0] : (rot.modes & modes) == rot.modes; - } - }; - int maprotation::exclude = 0; - vector<maprotation> maprotations; - int curmaprotation = 0; - - VAR(lockmaprotation, 0, 0, 2); - - void maprotationreset() - { - maprotations.setsize(0); - curmaprotation = 0; - maprotation::exclude = 0; - } - - void nextmaprotation() - { - curmaprotation++; - if(maprotations.inrange(curmaprotation) && maprotations[curmaprotation].modes) return; - do curmaprotation--; - while(maprotations.inrange(curmaprotation) && maprotations[curmaprotation].modes); - curmaprotation++; - } - - int findmaprotation(int mode, const char *map) - { - for(int i = max(curmaprotation, 0); i < maprotations.length(); i++) - { - maprotation &rot = maprotations[i]; - if(!rot.modes) break; - if(rot.match(mode, map)) return i; - } - int start; - for(start = max(curmaprotation, 0) - 1; start >= 0; start--) if(!maprotations[start].modes) break; - start++; - for(int i = start; i < curmaprotation; i++) - { - maprotation &rot = maprotations[i]; - if(!rot.modes) break; - if(rot.match(mode, map)) return i; - } - int best = -1; - loopv(maprotations) - { - maprotation &rot = maprotations[i]; - if(rot.match(mode, map) && (best < 0 || maprotations[best].includes(rot))) best = i; - } - return best; - } - - bool searchmodename(const char *haystack, const char *needle) - { - if(!needle[0]) return true; - do - { - if(needle[0] != '.') - { - haystack = strchr(haystack, needle[0]); - if(!haystack) break; - haystack++; - } - const char *h = haystack, *n = needle+1; - for(; *h && *n; h++) - { - if(*h == *n) n++; - else if(*h != ' ') break; - } - if(!*n) return true; - if(*n == '.') return !*h; - } while(needle[0] != '.'); - return false; - } - - int genmodemask(vector<char *> &modes) - { - int modemask = 0; - loopv(modes) - { - const char *mode = modes[i]; - int op = mode[0]; - switch(mode[0]) - { - case '*': - modemask |= 1<<NUMGAMEMODES; - loopk(NUMGAMEMODES) if(m_checknot(k+STARTGAMEMODE, M_DEMO|M_EDIT|M_LOCAL)) modemask |= 1<<k; - continue; - case '!': - mode++; - if(mode[0] != '?') break; - [[fallthrough]]; - case '?': - mode++; - loopk(NUMGAMEMODES) if(searchmodename(gamemodes[k].name, mode)) - { - if(op == '!') modemask &= ~(1<<k); - else modemask |= 1<<k; - } - continue; - } - int modenum = INT_MAX; - if(isdigit(mode[0])) modenum = atoi(mode); - else loopk(NUMGAMEMODES) if(searchmodename(gamemodes[k].name, mode)) { modenum = k+STARTGAMEMODE; break; } - if(!m_valid(modenum)) continue; - switch(op) - { - case '!': modemask &= ~(1 << (modenum - STARTGAMEMODE)); break; - default: modemask |= 1 << (modenum - STARTGAMEMODE); break; - } - } - return modemask; - } - - bool addmaprotation(int modemask, const char *map) - { - if(!map[0]) loopk(NUMGAMEMODES) if(modemask&(1<<k) && !m_check(k+STARTGAMEMODE, M_EDIT)) modemask &= ~(1<<k); - if(!modemask) return false; - if(!(modemask&(1<<NUMGAMEMODES))) maprotation::exclude |= modemask; - maprotation &rot = maprotations.add(); - rot.modes = modemask; - copystring(rot.map, map); - return true; - } - - void addmaprotations(tagval *args, int numargs) - { - vector<char *> modes, maps; - for(int i = 0; i + 1 < numargs; i += 2) - { - explodelist(args[i].getstr(), modes); - explodelist(args[i+1].getstr(), maps); - int modemask = genmodemask(modes); - if(maps.length()) loopvj(maps) addmaprotation(modemask, maps[j]); - else addmaprotation(modemask, ""); - modes.deletearrays(); - maps.deletearrays(); - } - if(maprotations.length() && maprotations.last().modes) - { - maprotation &rot = maprotations.add(); - rot.modes = 0; - rot.map[0] = '\0'; - } - } - - COMMAND(maprotationreset, ""); - COMMANDN(maprotation, addmaprotations, "ss2V"); - - struct demofile - { - string info; - uchar *data; - int len; - }; - - vector<demofile> demos; - - bool demonextmatch = false; - stream *demotmp = NULL, *demorecord = NULL, *demoplayback = NULL; - int nextplayback = 0; - - VAR(maxdemos, 0, 5, 25); - VAR(maxdemosize, 0, 16, 31); - VAR(restrictdemos, 0, 1, 1); - VARF(autorecorddemo, 0, 0, 1, demonextmatch = autorecorddemo!=0); - - VAR(restrictpausegame, 0, 1, 1); - VAR(restrictgamespeed, 0, 1, 1); - - SVAR(serverdesc, ""); - SVAR(serverpass, ""); - SVAR(adminpass, ""); - VARF(publicserver, 0, 0, 2, { + struct server_entity // server side version of "entity" type + { + int type; + int spawntime; + bool spawned; + }; + + static const int DEATHMILLIS = 300; + + struct clientinfo; + + struct gameevent + { + virtual ~gameevent() {} + + virtual bool flush(clientinfo *ci, int fmillis); + virtual void process(clientinfo *ci) {} + + virtual bool keepable() const { return false; } + }; + + struct timedevent : gameevent + { + int millis; + + bool flush(clientinfo *ci, int fmillis); + }; + + struct hitinfo + { + int target; + int lifesequence; + int rays; + float dist; + vec dir; + }; + + struct shotevent : timedevent + { + int id, gun; + vec from, to; + vector<hitinfo> hits; + + void process(clientinfo *ci); + }; + + struct explodeevent : timedevent + { + int id, gun; + vector<hitinfo> hits; + + bool keepable() const { return true; } + + void process(clientinfo *ci); + }; + + struct suicideevent : gameevent + { + void process(clientinfo *ci); + }; + + struct pickupevent : gameevent + { + int ent; + + void process(clientinfo *ci); + }; + + template <int N> + struct projectilestate + { + int projs[N]; + int numprojs; + + projectilestate() : numprojs(0) {} + + void reset() { numprojs = 0; } + + void add(int val) + { + if(numprojs>=N) numprojs = 0; + projs[numprojs++] = val; + } + + bool remove(int val) + { + loopi(numprojs) if(projs[i]==val) + { + projs[i] = projs[--numprojs]; + return true; + } + return false; + } + }; + + struct gamestate : fpsstate + { + vec o; + int state, editstate; + int lastdeath, deadflush, lastspawn, lifesequence; + int lastshot; + projectilestate<8> rockets, grenades; + int frags, flags, deaths, teamkills, shotdamage, damage, tokens; + int lasttimeplayed, timeplayed; + float effectiveness; + + gamestate() : state(CS_DEAD), editstate(CS_DEAD), lifesequence(0) {} + + bool isalive(int gamemillis) + { + return state==CS_ALIVE || (state==CS_DEAD && gamemillis - lastdeath <= DEATHMILLIS); + } + + bool waitexpired(int gamemillis) + { + return gamemillis - lastshot >= gunwait; + } + + void reset() + { + if(state!=CS_SPECTATOR) state = editstate = CS_DEAD; + maxhealth = 100; + maxarmour = 50; + rockets.reset(); + grenades.reset(); + + timeplayed = 0; + effectiveness = 0; + frags = flags = deaths = teamkills = shotdamage = damage = tokens = 0; + + lastdeath = 0; + + respawn(); + } + + void respawn() + { + fpsstate::respawn(); + o = vec(-1e10f, -1e10f, -1e10f); + deadflush = 0; + lastspawn = -1; + lastshot = 0; + tokens = 0; + } + + void reassign() + { + respawn(); + rockets.reset(); + grenades.reset(); + } + }; + + struct savedscore + { + uint ip; + string name; + int frags, flags, deaths, teamkills, shotdamage, damage; + int timeplayed; + float effectiveness; + + void save(gamestate &gs) + { + frags = gs.frags; + flags = gs.flags; + deaths = gs.deaths; + teamkills = gs.teamkills; + shotdamage = gs.shotdamage; + damage = gs.damage; + timeplayed = gs.timeplayed; + effectiveness = gs.effectiveness; + } + + void restore(gamestate &gs) + { + gs.frags = frags; + gs.flags = flags; + gs.deaths = deaths; + gs.teamkills = teamkills; + gs.shotdamage = shotdamage; + gs.damage = damage; + gs.timeplayed = timeplayed; + gs.effectiveness = effectiveness; + } + }; + + extern int gamemillis, nextexceeded; + + struct clientinfo + { + int clientnum, ownernum, connectmillis, sessionid, overflow; + string name, team, mapvote; + int playermodel; + int modevote; + int privilege; + bool connected, local, timesync; + int gameoffset, lastevent, pushed, exceeded; + gamestate state; + vector<gameevent *> events; + vector<uchar> position, messages; + uchar *wsdata; + int wslen; + vector<clientinfo *> bots; + int ping, aireinit; + string clientmap; + int mapcrc; + bool warned, gameclip; + ENetPacket *getdemo, *getmap, *clipboard; + int lastclipboard, needclipboard; + int connectauth; + uint authreq; + string authname, authdesc; + void *authchallenge; + int authkickvictim; + char *authkickreason; + + clientinfo() : getdemo(NULL), getmap(NULL), clipboard(NULL), authchallenge(NULL), authkickreason(NULL) { reset(); } + ~clientinfo() { events.deletecontents(); cleanclipboard(); cleanauth(); } + + void addevent(gameevent *e) + { + if(state.state==CS_SPECTATOR || events.length()>100) delete e; + else events.add(e); + } + + enum + { + PUSHMILLIS = 3000 + }; + + int calcpushrange() + { + ENetPeer *peer = getclientpeer(ownernum); + return PUSHMILLIS + ((peer) ? (int) (peer->roundTripTime + peer->roundTripTimeVariance) : (int) ENET_PEER_DEFAULT_ROUND_TRIP_TIME); + } + + bool checkpushed(int millis, int range) + { + return millis >= pushed - range && millis <= pushed + range; + } + + void scheduleexceeded() + { + if(state.state!=CS_ALIVE || !exceeded) return; + int range = calcpushrange(); + if(!nextexceeded || exceeded + range < nextexceeded) nextexceeded = exceeded + range; + } + + void setexceeded() + { + if(state.state==CS_ALIVE && !exceeded && !checkpushed(gamemillis, calcpushrange())) exceeded = gamemillis; + scheduleexceeded(); + } + + void setpushed() + { + pushed = max(pushed, gamemillis); + if(exceeded && checkpushed(exceeded, calcpushrange())) exceeded = 0; + } + + bool checkexceeded() + { + return state.state==CS_ALIVE && exceeded && gamemillis > exceeded + calcpushrange(); + } + + void mapchange() + { + mapvote[0] = 0; + modevote = INT_MAX; + state.reset(); + events.deletecontents(); + overflow = 0; + timesync = false; + lastevent = 0; + exceeded = 0; + pushed = 0; + clientmap[0] = '\0'; + mapcrc = 0; + warned = false; + gameclip = false; + } + + void reassign() + { + state.reassign(); + events.deletecontents(); + timesync = false; + lastevent = 0; + } + + void cleanclipboard(bool fullclean = true) + { + if(clipboard) { if(--clipboard->referenceCount <= 0) enet_packet_destroy(clipboard); clipboard = NULL; } + if(fullclean) lastclipboard = 0; + } + + void cleanauthkick() + { + authkickvictim = -1; + DELETEA(authkickreason); + } + + void cleanauth(bool full = true) + { + authreq = 0; + if(authchallenge) { freechallenge(authchallenge); authchallenge = NULL; } + if(full) cleanauthkick(); + } + + void reset() + { + name[0] = team[0] = 0; + playermodel = -1; + privilege = PRIV_NONE; + connected = local = false; + connectauth = 0; + position.setsize(0); + messages.setsize(0); + ping = 0; + aireinit = 0; + needclipboard = 0; + cleanclipboard(); + cleanauth(); + mapchange(); + } + + int geteventmillis(int servmillis, int clientmillis) + { + if(!timesync || (events.empty() && state.waitexpired(servmillis))) + { + timesync = true; + gameoffset = servmillis - clientmillis; + return servmillis; + } + else return gameoffset + clientmillis; + } + }; + + struct ban + { + int time, expire; + uint ip; + }; + + namespace aiman + { + extern void removeai(clientinfo *ci); + extern void clearai(); + extern void checkai(); + extern void reqadd(clientinfo *ci, int skill); + extern void reqdel(clientinfo *ci); + extern void setbotlimit(clientinfo *ci, int limit); + extern void setbotbalance(clientinfo *ci, bool balance); + extern void changemap(); + extern void addclient(clientinfo *ci); + extern void changeteam(clientinfo *ci); + } + + #define MM_MODE 0xF + #define MM_AUTOAPPROVE 0x1000 + #define MM_PRIVSERV (MM_MODE | MM_AUTOAPPROVE) + #define MM_PUBSERV ((1<<MM_OPEN) | (1<<MM_VETO)) + #define MM_COOPSERV (MM_AUTOAPPROVE | MM_PUBSERV | (1<<MM_LOCKED)) + + bool notgotitems = true; // true when map has changed and waiting for clients to send item + int gamemode = 0; + int gamemillis = 0, gamelimit = 0, nextexceeded = 0, gamespeed = 100; + bool gamepaused = false, shouldstep = true; + + string smapname = ""; + int interm = 0; + enet_uint32 lastsend = 0; + int mastermode = MM_OPEN, mastermask = MM_PRIVSERV; + stream *mapdata = NULL; + + vector<uint> allowedips; + vector<ban> bannedips; + + void addban(uint ip, int expire) + { + allowedips.removeobj(ip); + ban b; + b.time = totalmillis; + b.expire = totalmillis + expire; + b.ip = ip; + loopv(bannedips) if(bannedips[i].expire - b.expire > 0) { bannedips.insert(i, b); return; } + bannedips.add(b); + } + + vector<clientinfo *> connects, clients, bots; + + void kickclients(uint ip, clientinfo *actor = NULL, int priv = PRIV_NONE) + { + loopvrev(clients) + { + clientinfo &c = *clients[i]; + if(c.state.aitype != AI_NONE || c.privilege >= PRIV_ADMIN || c.local) continue; + if(actor && ((c.privilege > priv && !actor->local) || c.clientnum == actor->clientnum)) continue; + //~if(getclientip(c.clientnum) == ip) disconnect_client(c.clientnum, DISC_KICK); + } + } + + struct maprotation + { + static int exclude; + int modes; + string map; + + int calcmodemask() const { return modes&(1<<NUMGAMEMODES) ? modes & ~exclude : modes; } + bool hasmode(int mode, int offset = STARTGAMEMODE) const { return (calcmodemask() & (1 << (mode-offset))) != 0; } + + int findmode(int mode) const + { + if(!hasmode(mode)) loopi(NUMGAMEMODES) if(hasmode(i, 0)) return i+STARTGAMEMODE; + return mode; + } + + bool match(int reqmode, const char *reqmap) const + { + return hasmode(reqmode) && (!map[0] || !reqmap[0] || !strcmp(map, reqmap)); + } + + bool includes(const maprotation &rot) const + { + return rot.modes == modes ? rot.map[0] && !map[0] : (rot.modes & modes) == rot.modes; + } + }; + int maprotation::exclude = 0; + vector<maprotation> maprotations; + int curmaprotation = 0; + + VAR(lockmaprotation, 0, 0, 2); + + void maprotationreset() + { + maprotations.setsize(0); + curmaprotation = 0; + maprotation::exclude = 0; + } + + void nextmaprotation() + { + curmaprotation++; + if(maprotations.inrange(curmaprotation) && maprotations[curmaprotation].modes) return; + do curmaprotation--; + while(maprotations.inrange(curmaprotation) && maprotations[curmaprotation].modes); + curmaprotation++; + } + + int findmaprotation(int mode, const char *map) + { + for(int i = max(curmaprotation, 0); i < maprotations.length(); i++) + { + maprotation &rot = maprotations[i]; + if(!rot.modes) break; + if(rot.match(mode, map)) return i; + } + int start; + for(start = max(curmaprotation, 0) - 1; start >= 0; start--) if(!maprotations[start].modes) break; + start++; + for(int i = start; i < curmaprotation; i++) + { + maprotation &rot = maprotations[i]; + if(!rot.modes) break; + if(rot.match(mode, map)) return i; + } + int best = -1; + loopv(maprotations) + { + maprotation &rot = maprotations[i]; + if(rot.match(mode, map) && (best < 0 || maprotations[best].includes(rot))) best = i; + } + return best; + } + + bool searchmodename(const char *haystack, const char *needle) + { + if(!needle[0]) return true; + do + { + if(needle[0] != '.') + { + haystack = strchr(haystack, needle[0]); + if(!haystack) break; + haystack++; + } + const char *h = haystack, *n = needle+1; + for(; *h && *n; h++) + { + if(*h == *n) n++; + else if(*h != ' ') break; + } + if(!*n) return true; + if(*n == '.') return !*h; + } while(needle[0] != '.'); + return false; + } + + int genmodemask(vector<char *> &modes) + { + int modemask = 0; + loopv(modes) + { + const char *mode = modes[i]; + int op = mode[0]; + switch(mode[0]) + { + case '*': + modemask |= 1<<NUMGAMEMODES; + loopk(NUMGAMEMODES) if(m_checknot(k+STARTGAMEMODE, M_DEMO|M_EDIT|M_LOCAL)) modemask |= 1<<k; + continue; + case '!': + mode++; + if(mode[0] != '?') break; + [[fallthrough]]; + case '?': + mode++; + loopk(NUMGAMEMODES) if(searchmodename(gamemodes[k].name, mode)) + { + if(op == '!') modemask &= ~(1<<k); + else modemask |= 1<<k; + } + continue; + } + int modenum = INT_MAX; + if(isdigit(mode[0])) modenum = atoi(mode); + else loopk(NUMGAMEMODES) if(searchmodename(gamemodes[k].name, mode)) { modenum = k+STARTGAMEMODE; break; } + if(!m_valid(modenum)) continue; + switch(op) + { + case '!': modemask &= ~(1 << (modenum - STARTGAMEMODE)); break; + default: modemask |= 1 << (modenum - STARTGAMEMODE); break; + } + } + return modemask; + } + + bool addmaprotation(int modemask, const char *map) + { + if(!map[0]) loopk(NUMGAMEMODES) if(modemask&(1<<k) && !m_check(k+STARTGAMEMODE, M_EDIT)) modemask &= ~(1<<k); + if(!modemask) return false; + if(!(modemask&(1<<NUMGAMEMODES))) maprotation::exclude |= modemask; + maprotation &rot = maprotations.add(); + rot.modes = modemask; + copystring(rot.map, map); + return true; + } + + void addmaprotations(tagval *args, int numargs) + { + vector<char *> modes, maps; + for(int i = 0; i + 1 < numargs; i += 2) + { + explodelist(args[i].getstr(), modes); + explodelist(args[i+1].getstr(), maps); + int modemask = genmodemask(modes); + if(maps.length()) loopvj(maps) addmaprotation(modemask, maps[j]); + else addmaprotation(modemask, ""); + modes.deletearrays(); + maps.deletearrays(); + } + if(maprotations.length() && maprotations.last().modes) + { + maprotation &rot = maprotations.add(); + rot.modes = 0; + rot.map[0] = '\0'; + } + } + + COMMAND(maprotationreset, ""); + COMMANDN(maprotation, addmaprotations, "ss2V"); + + struct demofile + { + string info; + uchar *data; + int len; + }; + + vector<demofile> demos; + + bool demonextmatch = false; + stream *demotmp = NULL, *demorecord = NULL, *demoplayback = NULL; + int nextplayback = 0; + + VAR(maxdemos, 0, 5, 25); + VAR(maxdemosize, 0, 16, 31); + VAR(restrictdemos, 0, 1, 1); + VARF(autorecorddemo, 0, 0, 1, demonextmatch = autorecorddemo!=0); + + VAR(restrictpausegame, 0, 1, 1); + VAR(restrictgamespeed, 0, 1, 1); + + SVAR(serverdesc, ""); + SVAR(serverpass, ""); + SVAR(adminpass, ""); + VARF(publicserver, 0, 0, 2, { switch(publicserver) { case 0: default: mastermask = MM_PRIVSERV; break; @@ -625,3096 +625,3093 @@ namespace server case 2: mastermask = MM_COOPSERV; break; } }); - SVAR(servermotd, ""); - - struct teamkillkick - { - int modes, limit, ban; - - bool match(int mode) const - { - return (modes&(1<<(mode-STARTGAMEMODE)))!=0; - } - - bool includes(const teamkillkick &tk) const - { - return tk.modes != modes && (tk.modes & modes) == tk.modes; - } - }; - vector<teamkillkick> teamkillkicks; - - void teamkillkickreset() - { - teamkillkicks.setsize(0); - } - - void addteamkillkick(char *modestr, int *limit, int *ban) - { - vector<char *> modes; - explodelist(modestr, modes); - teamkillkick &kick = teamkillkicks.add(); - kick.modes = genmodemask(modes); - kick.limit = *limit; - kick.ban = *ban > 0 ? *ban*60000 : (*ban < 0 ? 0 : 30*60000); - modes.deletearrays(); - } - - COMMAND(teamkillkickreset, ""); - COMMANDN(teamkillkick, addteamkillkick, "sii"); - - struct teamkillinfo - { - uint ip; - int teamkills; - }; - vector<teamkillinfo> teamkills; - bool shouldcheckteamkills = false; - - void addteamkill(clientinfo *actor, clientinfo *victim, int n) - { - if(!m_timed || actor->state.aitype != AI_NONE || actor->local || actor->privilege || (victim && victim->state.aitype != AI_NONE)) return; - shouldcheckteamkills = true; - uint ip = getclientip(actor->clientnum); - loopv(teamkills) if(teamkills[i].ip == ip) - { - teamkills[i].teamkills += n; - return; - } - teamkillinfo &tk = teamkills.add(); - tk.ip = ip; - tk.teamkills = n; - } - - void checkteamkills() - { - teamkillkick *kick = NULL; - if(m_timed) loopv(teamkillkicks) if(teamkillkicks[i].match(gamemode) && (!kick || kick->includes(teamkillkicks[i]))) - kick = &teamkillkicks[i]; - if(kick) loopvrev(teamkills) - { - teamkillinfo &tk = teamkills[i]; - if(tk.teamkills >= kick->limit) - { - if(kick->ban > 0) addban(tk.ip, kick->ban); - kickclients(tk.ip); - teamkills.removeunordered(i); - } - } - shouldcheckteamkills = false; - } - - void *newclientinfo() { return new clientinfo; } - void deleteclientinfo(void *ci) { delete (clientinfo *)ci; } - - clientinfo *getinfo(int n) - { - if(n < MAXCLIENTS) return (clientinfo *)getclientinfo(n); - n -= MAXCLIENTS; - return bots.inrange(n) ? bots[n] : NULL; - } - - uint mcrc = 0; - vector<entity> ments; - vector<server_entity> sents; - vector<savedscore> scores; - - int msgsizelookup(int msg) - { - static int sizetable[NUMMSG] = { -1 }; - if(sizetable[0] < 0) - { - memset(sizetable, -1, sizeof(sizetable)); - for(const int *p = msgsizes; *p >= 0; p += 2) sizetable[p[0]] = p[1]; - } - return msg >= 0 && msg < NUMMSG ? sizetable[msg] : -1; - } - - const char *modename(int n, const char *unknown) - { - if(m_valid(n)) return gamemodes[n - STARTGAMEMODE].name; - return unknown; - } - - const char *mastermodename(int n, const char *unknown) - { - return (n>=MM_START && size_t(n-MM_START)<sizeof(mastermodenames)/sizeof(mastermodenames[0])) ? mastermodenames[n-MM_START] : unknown; - } - - const char *privname(int type) - { - switch(type) - { - case PRIV_ADMIN: return "admin"; - case PRIV_AUTH: return "auth"; - case PRIV_MASTER: return "master"; - default: return "unknown"; - } - } - - void sendservmsg(const char *s) { sendf(-1, 1, "ris", N_SERVMSG, s); } - void sendservmsgf(const char *fmt, ...) - { - defvformatstring(s, fmt, fmt); - sendf(-1, 1, "ris", N_SERVMSG, s); - } - - void resetitems() - { - mcrc = 0; - ments.setsize(0); - sents.setsize(0); - //cps.reset(); - } - - bool serveroption(const char *arg) - { - if(arg[0]=='-') switch(arg[1]) - { - case 'n': setsvar("serverdesc", &arg[2]); return true; - case 'y': setsvar("serverpass", &arg[2]); return true; - case 'p': setsvar("adminpass", &arg[2]); return true; - case 'o': setvar("publicserver", atoi(&arg[2])); return true; - } - return false; - } - - void serverinit() - { - smapname[0] = '\0'; - resetitems(); - } - - int numclients(int exclude = -1, bool nospec = true, bool noai = true, bool priv = false) - { - int n = 0; - loopv(clients) - { - clientinfo *ci = clients[i]; - if(ci->clientnum!=exclude && (!nospec || ci->state.state!=CS_SPECTATOR || (priv && (ci->privilege || ci->local))) && (!noai || ci->state.aitype == AI_NONE)) n++; - } - return n; - } - - bool duplicatename(clientinfo *ci, const char *name) - { - if(!name) name = ci->name; - loopv(clients) if(clients[i]!=ci && !strcmp(name, clients[i]->name)) return true; - return false; - } - - const char *colorname(clientinfo *ci, const char *name = NULL) - { - if(!name) name = ci->name; - if(name[0] && !duplicatename(ci, name) && ci->state.aitype == AI_NONE) return name; - static string cname[3]; - static int cidx = 0; - cidx = (cidx+1)%3; - formatstring(cname[cidx], ci->state.aitype == AI_NONE ? "%s \fs\f5(%d)\fr" : "%s \fs\f5[%d]\fr", name, ci->clientnum); - return cname[cidx]; - } - - bool canspawnitem(int type) { return !m_noitems && (type>=I_SHELLS && type<=I_QUAD && (!m_noammo || type<I_SHELLS || type>I_CARTRIDGES)); } - - int spawntime(int type) - { - int np = numclients(-1, true, false); - np = np<3 ? 4 : (np>4 ? 2 : 3); // spawn times are dependent on number of players - int sec = 0; - switch(type) - { - case I_SHELLS: - case I_BULLETS: - case I_ROCKETS: - case I_ROUNDS: - case I_GRENADES: - case I_CARTRIDGES: sec = np*4; break; - case I_HEALTH: sec = np*5; break; - case I_TINYHEALTH: sec = np*5; break; - case I_TINYARMOUR: sec = np*5; break; - case I_GREENARMOUR: sec = 20; break; - case I_YELLOWARMOUR: sec = 30; break; - case I_BOOST: sec = 60; break; - case I_QUAD: sec = 70; break; - } - return sec*1000; - } - - bool delayspawn(int type) - { - switch(type) - { - case I_GREENARMOUR: - case I_YELLOWARMOUR: - case I_BOOST: - case I_QUAD: - return true; - default: - return false; - } - } - - bool pickup(int i, int sender) // server side item pickup, acknowledge first client that gets it - { - if((m_timed && gamemillis>=gamelimit) || !sents.inrange(i) || !sents[i].spawned) return false; - clientinfo *ci = getinfo(sender); - if(!ci) return false; - if(!ci->local && !ci->state.canpickup(sents[i].type)) - { - sendf(sender, 1, "ri3", N_ITEMACC, i, -1); - return false; - } - sents[i].spawned = false; - sents[i].spawntime = spawntime(sents[i].type); - sendf(-1, 1, "ri3", N_ITEMACC, i, sender); - ci->state.pickup(sents[i].type); - return true; - } - - static hashset<teaminfo> teaminfos; - - void clearteaminfo() - { - teaminfos.clear(); - } - - bool teamhasplayers(const char *team) { loopv(clients) if(!strcmp(clients[i]->team, team)) return true; return false; } - - bool pruneteaminfo() - { - int oldteams = teaminfos.numelems; - enumerate(teaminfos, teaminfo, old, - if(!old.frags && !teamhasplayers(old.team)) teaminfos.remove(old.team); - ); - return teaminfos.numelems < oldteams; - } - - teaminfo *addteaminfo(const char *team) - { - teaminfo *t = teaminfos.access(team); - if(!t) - { - if(teaminfos.numelems >= MAXTEAMS && !pruneteaminfo()) return NULL; - t = &teaminfos[team]; - copystring(t->team, team, sizeof(t->team)); - t->frags = 0; - } - return t; - } - - clientinfo *choosebestclient(float &bestrank) - { - clientinfo *best = NULL; - bestrank = -1; - loopv(clients) - { - clientinfo *ci = clients[i]; - if(ci->state.timeplayed<0) continue; - float rank = ci->state.state!=CS_SPECTATOR ? ci->state.effectiveness/max(ci->state.timeplayed, 1) : -1; - if(!best || rank > bestrank) { best = ci; bestrank = rank; } - } - return best; - } - - VAR(persistteams, 0, 0, 1); - - void autoteam() - { - static const char * const teamnames[2] = {"good", "evil"}; - vector<clientinfo *> team[2]; - float teamrank[2] = {0, 0}; - for(int round = 0, remaining = clients.length(); remaining>=0; round++) - { - int first = round&1, second = (round+1)&1, selected = 0; - while(teamrank[first] <= teamrank[second]) - { - float rank; - clientinfo *ci = choosebestclient(rank); - if(!ci) break; - if(selected && rank<=0) break; - ci->state.timeplayed = -1; - team[first].add(ci); - if(rank>0) teamrank[first] += rank; - selected++; - if(rank<=0) break; - } - if(!selected) break; - remaining -= selected; - } - loopi(sizeof(team)/sizeof(team[0])) - { - addteaminfo(teamnames[i]); - loopvj(team[i]) - { - clientinfo *ci = team[i][j]; - if(!strcmp(ci->team, teamnames[i])) continue; - if(persistteams && ci->team[0]) - { - addteaminfo(ci->team); - continue; - } - copystring(ci->team, teamnames[i], MAXTEAMLEN+1); - sendf(-1, 1, "riisi", N_SETTEAM, ci->clientnum, teamnames[i], -1); - } - } - } - - struct teamrank - { - const char *name; - float rank; - int clients; - - teamrank(const char *name) : name(name), rank(0), clients(0) {} - }; - - const char *chooseworstteam(const char *suggest = NULL, clientinfo *exclude = NULL) - { - teamrank teamranks[2] = { teamrank("good"), teamrank("evil") }; - const int numteams = sizeof(teamranks)/sizeof(teamranks[0]); - loopv(clients) - { - clientinfo *ci = clients[i]; - if(ci==exclude || ci->state.aitype!=AI_NONE || ci->state.state==CS_SPECTATOR || !ci->team[0]) continue; - ci->state.timeplayed += lastmillis - ci->state.lasttimeplayed; - ci->state.lasttimeplayed = lastmillis; - - loopj(numteams) if(!strcmp(ci->team, teamranks[j].name)) - { - teamrank &ts = teamranks[j]; - ts.rank += ci->state.effectiveness/max(ci->state.timeplayed, 1); - ts.clients++; - break; - } - } - teamrank *worst = &teamranks[numteams-1]; - loopi(numteams-1) - { - teamrank &ts = teamranks[i]; - if(ts.rank < worst->rank || (ts.rank == worst->rank && ts.clients < worst->clients)) worst = &ts; - } - return worst->name; - } - - void prunedemos(int extra = 0) - { - int n = clamp(demos.length() + extra - maxdemos, 0, demos.length()); - if(n <= 0) return; - loopi(n) delete[] demos[i].data; - demos.remove(0, n); - } - - void adddemo() - { - if(!demotmp) return; - int len = (int)min(demotmp->size(), stream::offset((maxdemosize<<20) + 0x10000)); - demofile &d = demos.add(); - time_t t = time(NULL); - char *timestr = ctime(&t), *trim = timestr + strlen(timestr); - while(trim>timestr && iscubespace(*--trim)) *trim = '\0'; - formatstring(d.info, "%s: %s, %s, %.2f%s", timestr, modename(gamemode), smapname, len > 1024*1024 ? len/(1024*1024.f) : len/1024.0f, len > 1024*1024 ? "MB" : "kB"); - sendservmsgf("demo \"%s\" recorded", d.info); - d.data = new uchar[len]; - d.len = len; - demotmp->seek(0, SEEK_SET); - demotmp->read(d.data, len); - DELETEP(demotmp); - } - - void enddemorecord() - { - if(!demorecord) return; - - DELETEP(demorecord); - - if(!demotmp) return; - if(!maxdemos || !maxdemosize) { DELETEP(demotmp); return; } - - prunedemos(1); - adddemo(); - } - - void writedemo(int chan, void *data, int len) - { - if(!demorecord) return; - int stamp[3] = { gamemillis, chan, len }; - lilswap(stamp, 3); - demorecord->write(stamp, sizeof(stamp)); - demorecord->write(data, len); - if(demorecord->rawtell() >= (maxdemosize<<20)) enddemorecord(); - } - - void recordpacket(int chan, void *data, int len) - { - writedemo(chan, data, len); - } - - int welcomepacket(packetbuf &p, clientinfo *ci); - void sendwelcome(clientinfo *ci); - - void setupdemorecord() - { - if(!m_mp(gamemode) || m_edit) return; - - demotmp = opentempfile("demorecord", "w+b"); - if(!demotmp) return; - - stream *f = opengzfile(NULL, "wb", demotmp); - if(!f) { DELETEP(demotmp); return; } - - sendservmsg("recording demo"); - - demorecord = f; - - demoheader hdr; - memcpy(hdr.magic, DEMO_MAGIC, sizeof(hdr.magic)); - hdr.version = DEMO_VERSION; - hdr.protocol = PROTOCOL_VERSION; - lilswap(&hdr.version, 2); - demorecord->write(&hdr, sizeof(demoheader)); - - packetbuf p(MAXTRANS, ENET_PACKET_FLAG_RELIABLE); - welcomepacket(p, NULL); - writedemo(1, p.buf, p.len); - } - - void listdemos(int cn) - { - packetbuf p(MAXTRANS, ENET_PACKET_FLAG_RELIABLE); - putint(p, N_SENDDEMOLIST); - putint(p, demos.length()); - loopv(demos) sendstring(demos[i].info, p); - sendpacket(cn, 1, p.finalize()); - } - - void cleardemos(int n) - { - if(!n) - { - loopv(demos) delete[] demos[i].data; - demos.shrink(0); - sendservmsg("cleared all demos"); - } - else if(demos.inrange(n-1)) - { - delete[] demos[n-1].data; - demos.remove(n-1); - sendservmsgf("cleared demo %d", n); - } - } - - static void freegetmap(ENetPacket *packet) - { - loopv(clients) - { - clientinfo *ci = clients[i]; - if(ci->getmap == packet) ci->getmap = NULL; - } - } - - static void freegetdemo(ENetPacket *packet) - { - loopv(clients) - { - clientinfo *ci = clients[i]; - if(ci->getdemo == packet) ci->getdemo = NULL; - } - } - - void senddemo(clientinfo *ci, int num, int tag) - { - if(ci->getdemo) return; - if(!num) num = demos.length(); - if(!demos.inrange(num-1)) return; - demofile &d = demos[num-1]; - if((ci->getdemo = sendf(ci->clientnum, 2, "riim", N_SENDDEMO, tag, d.len, d.data))) - ci->getdemo->freeCallback = freegetdemo; - } - - void enddemoplayback() - { - if(!demoplayback) return; - DELETEP(demoplayback); - - loopv(clients) sendf(clients[i]->clientnum, 1, "ri3", N_DEMOPLAYBACK, 0, clients[i]->clientnum); - - sendservmsg("demo playback finished"); - - loopv(clients) sendwelcome(clients[i]); - } - - SVARP(demodir, "demo"); - - const char *getdemofile(const char *file, bool init) - { - if(!demodir[0]) return NULL; - static string buf; - copystring(buf, demodir); - int dirlen = strlen(buf); - if(buf[dirlen] != '/' && buf[dirlen] != '\\' && dirlen+1 < (int)sizeof(buf)) { buf[dirlen++] = '/'; buf[dirlen] = '\0'; } - if(init) - { - const char *dir = findfile(buf, "w"); - if(!fileexists(dir, "w")) createdir(dir); - } - concatstring(buf, file); - return buf; - } - - void setupdemoplayback() - { - if(demoplayback) return; - demoheader hdr; - string msg; - msg[0] = '\0'; - string file; - copystring(file, smapname); - int len = strlen(file); - if(len < 4 || strcasecmp(&file[len-4], ".dmo")) concatstring(file, ".dmo"); - if(const char *buf = getdemofile(file, false)) demoplayback = opengzfile(buf, "rb"); - if(!demoplayback) demoplayback = opengzfile(file, "rb"); - if(!demoplayback) formatstring(msg, "could not read demo \"%s\"", file); - else if(demoplayback->read(&hdr, sizeof(demoheader))!=sizeof(demoheader) || memcmp(hdr.magic, DEMO_MAGIC, sizeof(hdr.magic))) - formatstring(msg, "\"%s\" is not a demo file", file); - else - { - lilswap(&hdr.version, 2); - if(hdr.version!=DEMO_VERSION) formatstring(msg, "demo \"%s\" requires an %s version of Cube 2: Sauerbraten", file, hdr.version<DEMO_VERSION ? "older" : "newer"); - else if(hdr.protocol!=PROTOCOL_VERSION) formatstring(msg, "demo \"%s\" requires an %s version of Cube 2: Sauerbraten", file, hdr.protocol<PROTOCOL_VERSION ? "older" : "newer"); - } - if(msg[0]) - { - DELETEP(demoplayback); - sendservmsg(msg); - return; - } - - sendservmsgf("playing demo \"%s\"", file); - - sendf(-1, 1, "ri3", N_DEMOPLAYBACK, 1, -1); - - if(demoplayback->read(&nextplayback, sizeof(nextplayback))!=sizeof(nextplayback)) - { - enddemoplayback(); - return; - } - lilswap(&nextplayback, 1); - } - - void readdemo() - { - if(!demoplayback) return; - while(gamemillis>=nextplayback) - { - int chan, len; - if(demoplayback->read(&chan, sizeof(chan))!=sizeof(chan) || - demoplayback->read(&len, sizeof(len))!=sizeof(len)) - { - enddemoplayback(); - return; - } - lilswap(&chan, 1); - lilswap(&len, 1); - ENetPacket *packet = enet_packet_create(NULL, len+1, 0); - if(!packet || demoplayback->read(packet->data+1, len)!=size_t(len)) - { - if(packet) enet_packet_destroy(packet); - enddemoplayback(); - return; - } - packet->data[0] = N_DEMOPACKET; - sendpacket(-1, chan, packet); - if(!packet->referenceCount) enet_packet_destroy(packet); - if(!demoplayback) break; - if(demoplayback->read(&nextplayback, sizeof(nextplayback))!=sizeof(nextplayback)) - { - enddemoplayback(); - return; - } - lilswap(&nextplayback, 1); - } - } - - void timeupdate(int secs) - { - if(!demoplayback) return; - if(secs <= 0) interm = -1; - else gamelimit = max(gamelimit, nextplayback + secs*1000); - } - - void seekdemo(char *t) - { - if(!demoplayback) return; - bool rev = *t == '-'; - if(rev) t++; - int mins = strtoul(t, &t, 10), secs = 0, millis = 0; - if(*t == ':') secs = strtoul(t+1, &t, 10); - else { secs = mins; mins = 0; } - if(*t == '.') millis = strtoul(t+1, &t, 10); - int offset = max(millis + (mins*60 + secs)*1000, 0), prevmillis = gamemillis; - if(rev) while(gamelimit - offset > gamemillis) - { - gamemillis = gamelimit - offset; - readdemo(); - } - else if(offset > gamemillis) - { - gamemillis = offset; - readdemo(); - } - if(gamemillis > prevmillis) - { - if(!interm) sendf(-1, 1, "ri2", N_TIMEUP, max((gamelimit - gamemillis)/1000, 1)); -#ifndef STANDALONE - cleardamagescreen(); -#endif - } - } - - ICOMMAND(seekdemo, "sN$", (char *t, int *numargs, ident *id), - { - if(*numargs > 0) seekdemo(t); - else - { - int secs = gamemillis/1000; - defformatstring(str, "%d:%02d.%03d", secs/60, secs%60, gamemillis%1000); - if(*numargs < 0) result(str); - else printsvar(id, str); - } - }); - - void stopdemo() - { - if(m_demo) enddemoplayback(); - else enddemorecord(); - } - - void pausegame(bool val, clientinfo *ci = NULL) - { - if(gamepaused==val) return; - gamepaused = val; - sendf(-1, 1, "riii", N_PAUSEGAME, gamepaused ? 1 : 0, ci ? ci->clientnum : -1); - } - - void checkpausegame() - { - if(!gamepaused) return; - int admins = 0; - loopv(clients) if(clients[i]->privilege >= (restrictpausegame ? PRIV_ADMIN : PRIV_MASTER) || clients[i]->local) admins++; - if(!admins) pausegame(false); - } - - void forcepaused(bool paused) - { - pausegame(paused); - } - - bool ispaused() { return gamepaused; } - - void changegamespeed(int val, clientinfo *ci = NULL) - { - val = clamp(val, 10, 1000); - if(gamespeed==val) return; - gamespeed = val; - sendf(-1, 1, "riii", N_GAMESPEED, gamespeed, ci ? ci->clientnum : -1); - } - - void forcegamespeed(int speed) - { - changegamespeed(speed); - } - - int scaletime(int t) { return t*gamespeed; } - - SVAR(serverauth, ""); - - struct userkey - { - char *name; - char *desc; - - userkey() : name(NULL), desc(NULL) {} - userkey(char *name, char *desc) : name(name), desc(desc) {} - }; - - static inline uint hthash(const userkey &k) { return ::hthash(k.name); } - static inline bool htcmp(const userkey &x, const userkey &y) { return !strcmp(x.name, y.name) && !strcmp(x.desc, y.desc); } - - struct userinfo : userkey - { - void *pubkey; - int privilege; - - userinfo() : pubkey(NULL), privilege(PRIV_NONE) {} - ~userinfo() { delete[] name; delete[] desc; if(pubkey) freepubkey(pubkey); } - }; - hashset<userinfo> users; - - void adduser(char *name, char *desc, char *pubkey, char *priv) - { - userkey key(name, desc); - userinfo &u = users[key]; - if(u.pubkey) { freepubkey(u.pubkey); u.pubkey = NULL; } - if(!u.name) u.name = newstring(name); - if(!u.desc) u.desc = newstring(desc); - u.pubkey = parsepubkey(pubkey); - switch(priv[0]) - { - case 'a': case 'A': u.privilege = PRIV_ADMIN; break; - case 'm': case 'M': default: u.privilege = PRIV_AUTH; break; - case 'n': case 'N': u.privilege = PRIV_NONE; break; - } - } - COMMAND(adduser, "ssss"); - - void clearusers() - { - users.clear(); - } - COMMAND(clearusers, ""); - - void hashpassword(int cn, int sessionid, const char *pwd, char *result, int maxlen) - { - char buf[2*sizeof(string)]; - formatstring(buf, "%d %d ", cn, sessionid); - concatstring(buf, pwd, sizeof(buf)); - if(!hashstring(buf, result, maxlen)) *result = '\0'; - } - - bool checkpassword(clientinfo *ci, const char *wanted, const char *given) - { - string hash; - hashpassword(ci->clientnum, ci->sessionid, wanted, hash, sizeof(hash)); - return !strcmp(hash, given); - } - - void revokemaster(clientinfo *ci) - { - ci->privilege = PRIV_NONE; - if(ci->state.state==CS_SPECTATOR && !ci->local) aiman::removeai(ci); - } - - extern void connected(clientinfo *ci); - - bool setmaster(clientinfo *ci, bool val, const char *pass = "", const char *authname = NULL, const char *authdesc = NULL, int authpriv = PRIV_MASTER, bool force = false, bool trial = false) - { - if(authname && !val) return false; - const char *name = ""; - if(val) - { - bool haspass = adminpass[0] && checkpassword(ci, adminpass, pass); - int wantpriv = ci->local || haspass ? PRIV_ADMIN : authpriv; - if(wantpriv <= ci->privilege) return true; - else if(wantpriv <= PRIV_MASTER && !force) - { - if(ci->state.state==CS_SPECTATOR) - { - sendf(ci->clientnum, 1, "ris", N_SERVMSG, "Spectators may not claim master."); - return false; - } - loopv(clients) if(ci!=clients[i] && clients[i]->privilege) - { - sendf(ci->clientnum, 1, "ris", N_SERVMSG, "Master is already claimed."); - return false; - } - if(!authname && !(mastermask&MM_AUTOAPPROVE) && !ci->privilege && !ci->local) - { - sendf(ci->clientnum, 1, "ris", N_SERVMSG, "This server requires you to use the \"/auth\" command to claim master."); - return false; - } - } - if(trial) return true; - ci->privilege = wantpriv; - name = privname(ci->privilege); - } - else - { - if(!ci->privilege) return false; - if(trial) return true; - name = privname(ci->privilege); - revokemaster(ci); - } - bool hasmaster = false; - loopv(clients) if(clients[i]->local || clients[i]->privilege >= PRIV_MASTER) hasmaster = true; - if(!hasmaster) - { - mastermode = MM_OPEN; - allowedips.shrink(0); - } - string msg; - if(val && authname) - { - if(authdesc && authdesc[0]) formatstring(msg, "%s claimed %s as '\fs\f5%s\fr' [\fs\f0%s\fr]", colorname(ci), name, authname, authdesc); - else formatstring(msg, "%s claimed %s as '\fs\f5%s\fr'", colorname(ci), name, authname); - } - else formatstring(msg, "%s %s %s", colorname(ci), val ? "claimed" : "relinquished", name); - packetbuf p(MAXTRANS, ENET_PACKET_FLAG_RELIABLE); - putint(p, N_SERVMSG); - sendstring(msg, p); - putint(p, N_CURRENTMASTER); - putint(p, mastermode); - loopv(clients) if(clients[i]->privilege >= PRIV_MASTER) - { - putint(p, clients[i]->clientnum); - putint(p, clients[i]->privilege); - } - putint(p, -1); - sendpacket(-1, 1, p.finalize()); - checkpausegame(); - return true; - } - - bool trykick(clientinfo *ci, int victim, const char *reason = NULL, const char *authname = NULL, const char *authdesc = NULL, int authpriv = PRIV_NONE, bool trial = false) - { - int priv = ci->privilege; - if(authname) - { - if(priv >= authpriv || ci->local) authname = authdesc = NULL; - else priv = authpriv; - } - if((priv || ci->local) && ci->clientnum!=victim) - { - clientinfo *vinfo = (clientinfo *)getclientinfo(victim); - if(vinfo && vinfo->connected && (priv >= vinfo->privilege || ci->local) && vinfo->privilege < PRIV_ADMIN && !vinfo->local) - { - if(trial) return true; - string kicker; - if(authname) - { - if(authdesc && authdesc[0]) formatstring(kicker, "%s as '\fs\f5%s\fr' [\fs\f0%s\fr]", colorname(ci), authname, authdesc); - else formatstring(kicker, "%s as '\fs\f5%s\fr'", colorname(ci), authname); - } - else copystring(kicker, colorname(ci)); - if(reason && reason[0]) sendservmsgf("%s kicked %s because: %s", kicker, colorname(vinfo), reason); - else sendservmsgf("%s kicked %s", kicker, colorname(vinfo)); - uint ip = getclientip(victim); - addban(ip, 4*60*60000); - kickclients(ip, ci, priv); - } - } - return false; - } - - savedscore *findscore(clientinfo *ci, bool insert) - { - uint ip = getclientip(ci->clientnum); - if(!ip && !ci->local) return 0; - if(!insert) - { - loopv(clients) - { - clientinfo *oi = clients[i]; - if(oi->clientnum != ci->clientnum && getclientip(oi->clientnum) == ip && !strcmp(oi->name, ci->name)) - { - oi->state.timeplayed += lastmillis - oi->state.lasttimeplayed; - oi->state.lasttimeplayed = lastmillis; - static savedscore curscore; - curscore.save(oi->state); - return &curscore; - } - } - } - loopv(scores) - { - savedscore &sc = scores[i]; - if(sc.ip == ip && !strcmp(sc.name, ci->name)) return ≻ - } - if(!insert) return 0; - savedscore &sc = scores.add(); - sc.ip = ip; - copystring(sc.name, ci->name); - return ≻ - } - - void savescore(clientinfo *ci) - { - savedscore *sc = findscore(ci, true); - if(sc) sc->save(ci->state); - } - - static struct msgfilter - { - uchar msgmask[NUMMSG]; - - msgfilter(int msg, ...) - { - memset(msgmask, 0, sizeof(msgmask)); - va_list msgs; - va_start(msgs, msg); - for(uchar val = 1; msg < NUMMSG; msg = va_arg(msgs, int)) - { - if(msg < 0) val = uchar(-msg); - else msgmask[msg] = val; - } - va_end(msgs); - } - - uchar operator[](int msg) const { return msg >= 0 && msg < NUMMSG ? msgmask[msg] : 0; } - } msgfilter(-1, N_CONNECT, N_SERVINFO, N_INITCLIENT, N_WELCOME, N_MAPCHANGE, N_SERVMSG, N_DAMAGE, N_HITPUSH, N_SHOTFX, N_EXPLODEFX, N_DIED, - N_SPAWNSTATE, N_FORCEDEATH, N_TEAMINFO, N_ITEMACC, N_ITEMSPAWN, N_TIMEUP, N_CDIS, N_CURRENTMASTER, N_PONG, N_RESUME, - N_ANNOUNCE, N_SENDDEMOLIST, N_SENDDEMO, N_DEMOPLAYBACK, N_SENDMAP, - N_CLIENT, N_AUTHCHAL, N_INITAI, N_EXPIRETOKENS, N_DROPTOKENS, N_STEALTOKENS, N_DEMOPACKET, -2, N_REMIP, - N_NEWMAP, N_GETMAP, N_SENDMAP, N_CLIPBOARD, -3, N_EDITENT, N_EDITF, N_EDITT, N_EDITM, N_FLIP, N_COPY, N_PASTE, N_ROTATE, N_REPLACE, - N_DELCUBE, N_EDITVAR, N_EDITVSLOT, N_UNDO, N_REDO, -4, N_POS, NUMMSG), - connectfilter(-1, N_CONNECT, -2, N_AUTHANS, -3, N_PING, NUMMSG); - - int checktype(int type, clientinfo *ci) - { - if(ci) - { - if(!ci->connected) switch(connectfilter[type]) - { - // allow only before authconnect - case 1: return !ci->connectauth ? type : -1; - // allow only during authconnect - case 2: return ci->connectauth ? type : -1; - // always allow - case 3: return type; - // never allow - default: return -1; - } - if(ci->local) return type; - } - switch(msgfilter[type]) - { - // server-only messages - case 1: return ci ? -1 : type; - // only allowed in coop-edit - case 2: if(m_edit) break; return -1; - // only allowed in coop-edit, no overflow check - case 3: return m_edit ? type : -1; - // no overflow check - case 4: return type; - } - if(ci && ++ci->overflow >= 200) return -2; - return type; - } - - struct worldstate - { - int uses, len; - uchar *data; - - worldstate() : uses(0), len(0), data(NULL) {} - - void setup(int n) { len = n; data = new uchar[n]; } - void cleanup() { DELETEA(data); len = 0; } - bool contains(const uchar *p) const { return p >= data && p < &data[len]; } - }; - vector<worldstate> worldstates; - bool reliablemessages = false; - - void cleanworldstate(ENetPacket *packet) - { - loopv(worldstates) - { - worldstate &ws = worldstates[i]; - if(!ws.contains(packet->data)) continue; - ws.uses--; - if(ws.uses <= 0) - { - ws.cleanup(); - worldstates.removeunordered(i); - } - break; - } - } - - void flushclientposition(clientinfo &ci) - { - if(ci.position.empty() || (!hasnonlocalclients() && !demorecord)) return; - packetbuf p(ci.position.length(), 0); - p.put(ci.position.getbuf(), ci.position.length()); - ci.position.setsize(0); - sendpacket(-1, 0, p.finalize(), ci.ownernum); - } - - static void sendpositions(worldstate &ws, ucharbuf &wsbuf) - { - if(wsbuf.empty()) return; - int wslen = wsbuf.length(); - recordpacket(0, wsbuf.buf, wslen); - wsbuf.put(wsbuf.buf, wslen); - loopv(clients) - { - clientinfo &ci = *clients[i]; - if(ci.state.aitype != AI_NONE) continue; - uchar *data = wsbuf.buf; - int size = wslen; - if(ci.wsdata >= wsbuf.buf) { data = ci.wsdata + ci.wslen; size -= ci.wslen; } - if(size <= 0) continue; - ENetPacket *packet = enet_packet_create(data, size, ENET_PACKET_FLAG_NO_ALLOCATE); - sendpacket(ci.clientnum, 0, packet); - if(packet->referenceCount) { ws.uses++; packet->freeCallback = cleanworldstate; } - else enet_packet_destroy(packet); - } - wsbuf.offset(wsbuf.length()); - } - - static inline void addposition(worldstate &ws, ucharbuf &wsbuf, int mtu, clientinfo &bi, clientinfo &ci) - { - if(bi.position.empty()) return; - if(wsbuf.length() + bi.position.length() > mtu) sendpositions(ws, wsbuf); - int offset = wsbuf.length(); - wsbuf.put(bi.position.getbuf(), bi.position.length()); - bi.position.setsize(0); - int len = wsbuf.length() - offset; - if(ci.wsdata < wsbuf.buf) { ci.wsdata = &wsbuf.buf[offset]; ci.wslen = len; } - else ci.wslen += len; - } - - static void sendmessages(worldstate &ws, ucharbuf &wsbuf) - { - if(wsbuf.empty()) return; - int wslen = wsbuf.length(); - recordpacket(1, wsbuf.buf, wslen); - wsbuf.put(wsbuf.buf, wslen); - loopv(clients) - { - clientinfo &ci = *clients[i]; - if(ci.state.aitype != AI_NONE) continue; - uchar *data = wsbuf.buf; - int size = wslen; - if(ci.wsdata >= wsbuf.buf) { data = ci.wsdata + ci.wslen; size -= ci.wslen; } - if(size <= 0) continue; - ENetPacket *packet = enet_packet_create(data, size, (reliablemessages ? ENET_PACKET_FLAG_RELIABLE : 0) | ENET_PACKET_FLAG_NO_ALLOCATE); - sendpacket(ci.clientnum, 1, packet); - if(packet->referenceCount) { ws.uses++; packet->freeCallback = cleanworldstate; } - else enet_packet_destroy(packet); - } - wsbuf.offset(wsbuf.length()); - } - - static inline void addmessages(worldstate &ws, ucharbuf &wsbuf, int mtu, clientinfo &bi, clientinfo &ci) - { - if(bi.messages.empty()) return; - if(wsbuf.length() + 10 + bi.messages.length() > mtu) sendmessages(ws, wsbuf); - int offset = wsbuf.length(); - putint(wsbuf, N_CLIENT); - putint(wsbuf, bi.clientnum); - putuint(wsbuf, bi.messages.length()); - wsbuf.put(bi.messages.getbuf(), bi.messages.length()); - bi.messages.setsize(0); - int len = wsbuf.length() - offset; - if(ci.wsdata < wsbuf.buf) { ci.wsdata = &wsbuf.buf[offset]; ci.wslen = len; } - else ci.wslen += len; - } - - bool buildworldstate() - { - int wsmax = 0; - loopv(clients) - { - clientinfo &ci = *clients[i]; - ci.overflow = 0; - ci.wsdata = NULL; - wsmax += ci.position.length(); - if(ci.messages.length()) wsmax += 10 + ci.messages.length(); - } - if(wsmax <= 0) - { - reliablemessages = false; - return false; - } - worldstate &ws = worldstates.add(); - ws.setup(2*wsmax); - int mtu = getservermtu() - 100; - if(mtu <= 0) mtu = ws.len; - ucharbuf wsbuf(ws.data, ws.len); - loopv(clients) - { - clientinfo &ci = *clients[i]; - if(ci.state.aitype != AI_NONE) continue; - addposition(ws, wsbuf, mtu, ci, ci); - loopvj(ci.bots) addposition(ws, wsbuf, mtu, *ci.bots[j], ci); - } - sendpositions(ws, wsbuf); - loopv(clients) - { - clientinfo &ci = *clients[i]; - if(ci.state.aitype != AI_NONE) continue; - addmessages(ws, wsbuf, mtu, ci, ci); - loopvj(ci.bots) addmessages(ws, wsbuf, mtu, *ci.bots[j], ci); - } - sendmessages(ws, wsbuf); - reliablemessages = false; - if(ws.uses) return true; - ws.cleanup(); - worldstates.drop(); - return false; - } - - bool sendpackets(bool force) - { - if(clients.empty() || (!hasnonlocalclients() && !demorecord)) return false; - enet_uint32 curtime = enet_time_get()-lastsend; - if(curtime<33 && !force) return false; - bool flush = buildworldstate(); - lastsend += curtime - (curtime%33); - return flush; - } - - template<class T> - void sendstate(gamestate &gs, T &p) - { - putint(p, gs.lifesequence); - putint(p, gs.health); - putint(p, gs.maxhealth); - putint(p, gs.armour); - putint(p, gs.maxarmour); - putint(p, gs.armourtype); - putint(p, gs.gunselect); - loopi(GUN_PISTOL-GUN_SG+1) putint(p, gs.ammo[GUN_SG+i]); - } - - void spawnstate(clientinfo *ci) - { - gamestate &gs = ci->state; - gs.spawnstate(gamemode); - gs.lifesequence = (gs.lifesequence + 1)&0x7F; - } - - void sendspawn(clientinfo *ci) - { - gamestate &gs = ci->state; - spawnstate(ci); - sendf(ci->ownernum, 1, "rii8v", N_SPAWNSTATE, ci->clientnum, gs.lifesequence, - gs.health, gs.maxhealth, - gs.armour, gs.maxarmour, gs.armourtype, - gs.gunselect, GUN_PISTOL-GUN_SG+1, &gs.ammo[GUN_SG]); - gs.lastspawn = gamemillis; - } - - void sendwelcome(clientinfo *ci) - { - packetbuf p(MAXTRANS, ENET_PACKET_FLAG_RELIABLE); - int chan = welcomepacket(p, ci); - sendpacket(ci->clientnum, chan, p.finalize()); - } - - void putinitclient(clientinfo *ci, packetbuf &p) - { - if(ci->state.aitype != AI_NONE) - { - putint(p, N_INITAI); - putint(p, ci->clientnum); - putint(p, ci->ownernum); - putint(p, ci->state.aitype); - putint(p, ci->state.skill); - putint(p, ci->playermodel); - sendstring(ci->name, p); - sendstring(ci->team, p); - } - else - { - putint(p, N_INITCLIENT); - putint(p, ci->clientnum); - sendstring(ci->name, p); - sendstring(ci->team, p); - putint(p, ci->playermodel); - } - } - - void welcomeinitclient(packetbuf &p, int exclude = -1) - { - loopv(clients) - { - clientinfo *ci = clients[i]; - if(!ci->connected || ci->clientnum == exclude) continue; - - putinitclient(ci, p); - } - } - - bool hasmap(clientinfo *ci) - { - return (m_edit && (clients.length() > 0 || ci->local)) || - (smapname[0] && (!m_timed || gamemillis < gamelimit || (ci->state.state==CS_SPECTATOR && !ci->privilege && !ci->local) || numclients(ci->clientnum, true, true, true))); - } - - int welcomepacket(packetbuf &p, clientinfo *ci) - { - putint(p, N_WELCOME); - putint(p, N_MAPCHANGE); - sendstring(smapname, p); - putint(p, gamemode); - putint(p, notgotitems ? 1 : 0); - if(!ci || (m_timed && smapname[0])) - { - putint(p, N_TIMEUP); - putint(p, gamemillis < gamelimit && !interm ? max((gamelimit - gamemillis)/1000, 1) : 0); - } - if(!notgotitems) - { - putint(p, N_ITEMLIST); - loopv(sents) if(sents[i].spawned) - { - putint(p, i); - putint(p, sents[i].type); - } - putint(p, -1); - } - bool hasmaster = false; - if(mastermode != MM_OPEN) - { - putint(p, N_CURRENTMASTER); - putint(p, mastermode); - hasmaster = true; - } - loopv(clients) if(clients[i]->privilege >= PRIV_MASTER) - { - if(!hasmaster) - { - putint(p, N_CURRENTMASTER); - putint(p, mastermode); - hasmaster = true; - } - putint(p, clients[i]->clientnum); - putint(p, clients[i]->privilege); - } - if(hasmaster) putint(p, -1); - if(gamepaused) - { - putint(p, N_PAUSEGAME); - putint(p, 1); - putint(p, -1); - } - if(gamespeed != 100) - { - putint(p, N_GAMESPEED); - putint(p, gamespeed); - putint(p, -1); - } - if(m_teammode) - { - putint(p, N_TEAMINFO); - enumerate(teaminfos, teaminfo, t, - if(t.frags) { sendstring(t.team, p); putint(p, t.frags); } - ); - sendstring("", p); - } - if(ci) - { - putint(p, N_SETTEAM); - putint(p, ci->clientnum); - sendstring(ci->team, p); - putint(p, -1); - } - if(ci && (m_demo || m_mp(gamemode)) && ci->state.state!=CS_SPECTATOR) - { - gamestate &gs = ci->state; - spawnstate(ci); - putint(p, N_SPAWNSTATE); - putint(p, ci->clientnum); - sendstate(gs, p); - gs.lastspawn = gamemillis; - } - if(ci && ci->state.state==CS_SPECTATOR) - { - putint(p, N_SPECTATOR); - putint(p, ci->clientnum); - putint(p, 1); - sendf(-1, 1, "ri3x", N_SPECTATOR, ci->clientnum, 1, ci->clientnum); - } - if(!ci || clients.length()>1) - { - putint(p, N_RESUME); - loopv(clients) - { - clientinfo *oi = clients[i]; - if(ci && oi->clientnum==ci->clientnum) continue; - putint(p, oi->clientnum); - putint(p, oi->state.state); - putint(p, oi->state.frags); - putint(p, oi->state.flags); - putint(p, oi->state.deaths); - putint(p, oi->state.quadmillis); - sendstate(oi->state, p); - } - putint(p, -1); - welcomeinitclient(p, ci ? ci->clientnum : -1); - } - return 1; - } - - bool restorescore(clientinfo *ci) - { - //if(ci->local) return false; - savedscore *sc = findscore(ci, false); - if(sc) - { - sc->restore(ci->state); - return true; - } - return false; - } - - void sendresume(clientinfo *ci) - { - gamestate &gs = ci->state; - sendf(-1, 1, "ri3i5i6vi", N_RESUME, ci->clientnum, gs.state, - gs.frags, gs.flags, gs.deaths, gs.quadmillis, - gs.lifesequence, - gs.health, gs.maxhealth, - gs.armour, gs.maxarmour, gs.armourtype, - gs.gunselect, GUN_PISTOL-GUN_SG+1, &gs.ammo[GUN_SG], -1); - } - - void sendinitclient(clientinfo *ci) - { - packetbuf p(MAXTRANS, ENET_PACKET_FLAG_RELIABLE); - putinitclient(ci, p); - sendpacket(-1, 1, p.finalize(), ci->clientnum); - } - - void loaditems() - { - resetitems(); - notgotitems = true; - if(m_edit || !loadents(smapname, ments, &mcrc)) - return; - loopv(ments) if(canspawnitem(ments[i].type)) - { - server_entity se = { NOTUSED, 0, false }; - while(sents.length()<=i) sents.add(se); - sents[i].type = ments[i].type; - if(m_mp(gamemode) && delayspawn(sents[i].type)) sents[i].spawntime = spawntime(sents[i].type); - else sents[i].spawned = true; - } - notgotitems = false; - } - - void changemap(const char *s, int mode) - { - stopdemo(); - pausegame(false); - changegamespeed(100); - aiman::clearai(); - - gamemode = mode; - gamemillis = 0; - gamelimit = 10*60000; - interm = 0; - nextexceeded = 0; - copystring(smapname, s); - loaditems(); - scores.shrink(0); - shouldcheckteamkills = false; - teamkills.shrink(0); - loopv(clients) - { - clientinfo *ci = clients[i]; - ci->state.timeplayed += lastmillis - ci->state.lasttimeplayed; - } - - if(!m_mp(gamemode)) kicknonlocalclients(DISC_LOCAL); - - sendf(-1, 1, "risii", N_MAPCHANGE, smapname, gamemode, 1); - - clearteaminfo(); - if(m_teammode) autoteam(); - - if(m_timed && smapname[0]) sendf(-1, 1, "ri2", N_TIMEUP, gamemillis < gamelimit && !interm ? max((gamelimit - gamemillis)/1000, 1) : 0); - loopv(clients) - { - clientinfo *ci = clients[i]; - ci->mapchange(); - ci->state.lasttimeplayed = lastmillis; - if(m_mp(gamemode) && ci->state.state!=CS_SPECTATOR) sendspawn(ci); - } - - aiman::changemap(); - - if(m_demo) - { - if(clients.length()) setupdemoplayback(); - } - else - { - if(demonextmatch) setupdemorecord(); - demonextmatch = autorecorddemo!=0; - } - } - - void rotatemap(bool next) - { - if(!maprotations.inrange(curmaprotation)) - { - changemap("", 1); - return; - } - if(next) - { - curmaprotation = findmaprotation(gamemode, smapname); - if(curmaprotation >= 0) nextmaprotation(); - else curmaprotation = smapname[0] ? max(findmaprotation(gamemode, ""), 0) : 0; - } - maprotation &rot = maprotations[curmaprotation]; - changemap(rot.map, rot.findmode(gamemode)); - } - - struct votecount - { - char *map; - int mode, count; - votecount() {} - votecount(char *s, int n) : map(s), mode(n), count(0) {} - }; - - void checkvotes(bool force = false) - { - vector<votecount> votes; - int maxvotes = 0; - loopv(clients) - { - clientinfo *oi = clients[i]; - if(oi->state.state==CS_SPECTATOR && !oi->privilege && !oi->local) continue; - if(oi->state.aitype!=AI_NONE) continue; - maxvotes++; - if(!m_valid(oi->modevote)) continue; - votecount *vc = NULL; - loopvj(votes) if(!strcmp(oi->mapvote, votes[j].map) && oi->modevote==votes[j].mode) - { - vc = &votes[j]; - break; - } - if(!vc) vc = &votes.add(votecount(oi->mapvote, oi->modevote)); - vc->count++; - } - votecount *best = NULL; - loopv(votes) if(!best || votes[i].count > best->count || (votes[i].count == best->count && rnd(2))) best = &votes[i]; - if(force || (best && best->count > maxvotes/2)) - { - sendpackets(true); - if(demorecord) enddemorecord(); - if(best && (best->count > (force ? 1 : maxvotes/2))) - { - sendservmsg(force ? "vote passed by default" : "vote passed by majority"); - changemap(best->map, best->mode); - } - else rotatemap(true); - } - } - - void forcemap(const char *map, int mode) - { - stopdemo(); - if(!map[0] && !m_check(mode, M_EDIT)) - { - int idx = findmaprotation(mode, smapname); - if(idx < 0 && smapname[0]) idx = findmaprotation(mode, ""); - if(idx < 0) return; - map = maprotations[idx].map; - } - if(hasnonlocalclients()) sendservmsgf("local player forced %s on map %s", modename(mode), map[0] ? map : "[new map]"); - changemap(map, mode); - } - - void vote(const char *map, int reqmode, int sender) - { - clientinfo *ci = getinfo(sender); - if(!ci || (ci->state.state==CS_SPECTATOR && !ci->privilege && !ci->local) || (!ci->local && !m_mp(reqmode))) return; - if(!m_valid(reqmode)) return; - if(!map[0] && !m_check(reqmode, M_EDIT)) - { - int idx = findmaprotation(reqmode, smapname); - if(idx < 0 && smapname[0]) idx = findmaprotation(reqmode, ""); - if(idx < 0) return; - map = maprotations[idx].map; - } - if(lockmaprotation && !ci->local && ci->privilege < (lockmaprotation > 1 ? PRIV_ADMIN : PRIV_MASTER) && findmaprotation(reqmode, map) < 0) - { - sendf(sender, 1, "ris", N_SERVMSG, "This server has locked the map rotation."); - return; - } - copystring(ci->mapvote, map); - ci->modevote = reqmode; - if(ci->local || (ci->privilege && mastermode>=MM_VETO)) - { - sendpackets(true); - if(demorecord) enddemorecord(); - if(!ci->local || hasnonlocalclients()) - sendservmsgf("%s forced %s on map %s", colorname(ci), modename(ci->modevote), ci->mapvote[0] ? ci->mapvote : "[new map]"); - changemap(ci->mapvote, ci->modevote); - } - else - { - sendservmsgf("%s suggests %s on map %s (select map to vote)", colorname(ci), modename(reqmode), map[0] ? map : "[new map]"); - checkvotes(); - } - } - - VAR(overtime, 0, 0, 1); - - bool checkovertime() - { - if(!m_timed || !overtime) return false; - const char* topteam = NULL; - int topscore = INT_MIN; - bool tied = false; - if(m_teammode) - { - vector<teamscore> scores; - loopv(clients) - { - clientinfo *ci = clients[i]; - if(ci->state.state==CS_SPECTATOR || !ci->team[0]) continue; - int score = 0; - if(teaminfo *ti = teaminfos.access(ci->team)) score = ti->frags; - if(!topteam || score > topscore) { topteam = ci->team; topscore = score; tied = false; } - else if(score == topscore && strcmp(ci->team, topteam)) tied = true; - } - } - else - { - loopv(clients) - { - clientinfo *ci = clients[i]; - if(ci->state.state==CS_SPECTATOR) continue; - int score = ci->state.frags; - if(score > topscore) { topscore = score; tied = false; } - else if(score == topscore) tied = true; - } - } - if(!tied) return false; - sendservmsg("the game is tied with overtime"); - gamelimit = max(gamemillis, gamelimit) + 2*60000; - sendf(-1, 1, "ri2", N_TIMEUP, max((gamelimit - gamemillis)/1000, 1)); - return true; - } - - void checkintermission(bool force = false) - { - if(gamemillis >= gamelimit && !interm && (force || !checkovertime())) - { - sendf(-1, 1, "ri2", N_TIMEUP, 0); - changegamespeed(100); - interm = gamemillis + 10000; - } - } - - void startintermission() { gamelimit = min(gamelimit, gamemillis); checkintermission(true); } - - void dodamage(clientinfo *target, clientinfo *actor, int damage, int gun, const vec &hitpush = vec(0, 0, 0)) - { - gamestate &ts = target->state; - ts.dodamage(damage); - if(target!=actor && !isteam(target->team, actor->team)) actor->state.damage += damage; - sendf(-1, 1, "ri6", N_DAMAGE, target->clientnum, actor->clientnum, damage, ts.armour, ts.health); - if(target==actor) target->setpushed(); - else if(!hitpush.iszero()) - { - ivec v(vec(hitpush).rescale(DNF)); - sendf(ts.health<=0 ? -1 : target->ownernum, 1, "ri7", N_HITPUSH, target->clientnum, gun, damage, v.x, v.y, v.z); - target->setpushed(); - } - if(ts.health<=0) - { - target->state.deaths++; - int fragvalue = (target==actor || isteam(target->team, actor->team) ? -1 : 1); - actor->state.frags += fragvalue; - if(fragvalue>0) - { - int friends = 0, enemies = 0; // note: friends also includes the fragger - if(m_teammode) loopv(clients) if(strcmp(clients[i]->team, actor->team)) enemies++; else friends++; - else { friends = 1; enemies = clients.length()-1; } - actor->state.effectiveness += fragvalue*friends/float(max(enemies, 1)); - } - teaminfo *t = m_teammode ? teaminfos.access(actor->team) : NULL; - if(t) t->frags += fragvalue; - sendf(-1, 1, "ri5", N_DIED, target->clientnum, actor->clientnum, actor->state.frags, t ? t->frags : 0); - target->position.setsize(0); - ts.state = CS_DEAD; - ts.lastdeath = gamemillis; - if(actor!=target && isteam(actor->team, target->team)) - { - actor->state.teamkills++; - addteamkill(actor, target, 1); - } - ts.deadflush = ts.lastdeath + DEATHMILLIS; - // don't issue respawn yet until DEATHMILLIS has elapsed - // ts.respawn(); - } - } - - void suicide(clientinfo *ci) - { - gamestate &gs = ci->state; - if(gs.state!=CS_ALIVE) return; - int fragvalue = -1; - ci->state.frags += fragvalue; - ci->state.deaths++; - teaminfo *t = m_teammode ? teaminfos.access(ci->team) : NULL; - if(t) t->frags += fragvalue; - sendf(-1, 1, "ri5", N_DIED, ci->clientnum, ci->clientnum, gs.frags, t ? t->frags : 0); - ci->position.setsize(0); - gs.state = CS_DEAD; - gs.lastdeath = gamemillis; - gs.respawn(); - } - - void suicideevent::process(clientinfo *ci) - { - suicide(ci); - } - - void explodeevent::process(clientinfo *ci) - { - gamestate &gs = ci->state; - switch(gun) - { - case GUN_RL: - if(!gs.rockets.remove(id)) return; - break; - - case GUN_GL: - if(!gs.grenades.remove(id)) return; - break; - - default: - return; - } - sendf(-1, 1, "ri4x", N_EXPLODEFX, ci->clientnum, gun, id, ci->ownernum); - loopv(hits) - { - hitinfo &h = hits[i]; - clientinfo *target = getinfo(h.target); - if(!target || target->state.state!=CS_ALIVE || h.lifesequence!=target->state.lifesequence || h.dist<0 || h.dist>guns[gun].exprad) continue; - - bool dup = false; - loopj(i) if(hits[j].target==h.target) { dup = true; break; } - if(dup) continue; - - int damage = guns[gun].damage; - if(gs.quadmillis) damage *= 4; - damage = int(damage*(1-h.dist/EXP_DISTSCALE/guns[gun].exprad)); - if(target==ci) damage /= EXP_SELFDAMDIV; - dodamage(target, ci, damage, gun, h.dir); - } - } - - void shotevent::process(clientinfo *ci) - { - gamestate &gs = ci->state; - int wait = millis - gs.lastshot; - if(!gs.isalive(gamemillis) || - wait<gs.gunwait || - gun<GUN_FIST || gun>GUN_PISTOL || - gs.ammo[gun]<=0 || (guns[gun].range && from.dist(to) > guns[gun].range + 1)) - return; - if(gun!=GUN_FIST) gs.ammo[gun]--; - gs.lastshot = millis; - gs.gunwait = guns[gun].attackdelay; - sendf(-1, 1, "rii9x", N_SHOTFX, ci->clientnum, gun, id, - int(from.x*DMF), int(from.y*DMF), int(from.z*DMF), - int(to.x*DMF), int(to.y*DMF), int(to.z*DMF), - ci->ownernum); - gs.shotdamage += guns[gun].damage*(gs.quadmillis ? 4 : 1)*guns[gun].rays; - switch(gun) - { - case GUN_RL: gs.rockets.add(id); break; - case GUN_GL: gs.grenades.add(id); break; - default: - { - int totalrays = 0, maxrays = guns[gun].rays; - loopv(hits) - { - hitinfo &h = hits[i]; - clientinfo *target = getinfo(h.target); - if(!target || target->state.state!=CS_ALIVE || h.lifesequence!=target->state.lifesequence || h.rays<1 || h.dist > guns[gun].range + 1) continue; - - totalrays += h.rays; - if(totalrays>maxrays) continue; - int damage = h.rays*guns[gun].damage; - if(gs.quadmillis) damage *= 4; - dodamage(target, ci, damage, gun, h.dir); - } - break; - } - } - } - - void pickupevent::process(clientinfo *ci) - { - gamestate &gs = ci->state; - if(m_mp(gamemode) && !gs.isalive(gamemillis)) return; - pickup(ent, ci->clientnum); - } - - bool gameevent::flush(clientinfo *ci, int fmillis) - { - process(ci); - return true; - } - - bool timedevent::flush(clientinfo *ci, int fmillis) - { - if(millis > fmillis) return false; - else if(millis >= ci->lastevent) - { - ci->lastevent = millis; - process(ci); - } - return true; - } - - void clearevent(clientinfo *ci) - { - delete ci->events.remove(0); - } - - void flushevents(clientinfo *ci, int millis) - { - while(ci->events.length()) - { - gameevent *ev = ci->events[0]; - if(ev->flush(ci, millis)) clearevent(ci); - else break; - } - } - - void processevents() - { - loopv(clients) - { - clientinfo *ci = clients[i]; - if(curtime>0 && ci->state.quadmillis) ci->state.quadmillis = max(ci->state.quadmillis-curtime, 0); - flushevents(ci, gamemillis); - } - } - - void cleartimedevents(clientinfo *ci) - { - int keep = 0; - loopv(ci->events) - { - if(ci->events[i]->keepable()) - { - if(keep < i) - { - for(int j = keep; j < i; j++) delete ci->events[j]; - ci->events.remove(keep, i - keep); - i = keep; - } - keep = i+1; - continue; - } - } - while(ci->events.length() > keep) delete ci->events.pop(); - ci->timesync = false; - } - - void serverupdate() - { - if(shouldstep && !gamepaused) - { - gamemillis += curtime; - - if(m_demo) readdemo(); - else if(!m_timed || gamemillis < gamelimit) - { - processevents(); - if(curtime) - { - loopv(sents) if(sents[i].spawntime) // spawn entities when timer reached - { - int oldtime = sents[i].spawntime; - sents[i].spawntime -= curtime; - if(sents[i].spawntime<=0) - { - sents[i].spawntime = 0; - sents[i].spawned = true; - sendf(-1, 1, "ri2", N_ITEMSPAWN, i); - } - else if(sents[i].spawntime<=10000 && oldtime>10000 && (sents[i].type==I_QUAD || sents[i].type==I_BOOST)) - { - sendf(-1, 1, "ri2", N_ANNOUNCE, sents[i].type); - } - } - } - aiman::checkai(); - } - } - - while(bannedips.length() && bannedips[0].expire-totalmillis <= 0) bannedips.remove(0); - //~loopv(connects) if(totalmillis-connects[i]->connectmillis>15000) disconnect_client(connects[i]->clientnum, DISC_TIMEOUT); - - if(nextexceeded && gamemillis > nextexceeded && (!m_timed || gamemillis < gamelimit)) - { - nextexceeded = 0; - loopvrev(clients) - { - clientinfo &c = *clients[i]; - if(c.state.aitype != AI_NONE) continue; - //~if(c.checkexceeded()) disconnect_client(c.clientnum, DISC_MSGERR); - else c.scheduleexceeded(); - } - } - - if(shouldcheckteamkills) checkteamkills(); - - if(shouldstep && !gamepaused) - { - if(m_timed && smapname[0] && gamemillis-curtime>0) checkintermission(); - if(interm > 0 && gamemillis>interm) - { - if(demorecord) enddemorecord(); - interm = -1; - checkvotes(true); - } - } - - shouldstep = clients.length() > 0; - } - - void forcespectator(clientinfo *ci) - { - if(ci->state.state==CS_ALIVE) suicide(ci); - ci->state.state = CS_SPECTATOR; - ci->state.timeplayed += lastmillis - ci->state.lasttimeplayed; - if(!ci->local && (!ci->privilege || ci->warned)) aiman::removeai(ci); - sendf(-1, 1, "ri3", N_SPECTATOR, ci->clientnum, 1); - } - - struct crcinfo - { - int crc, matches; - - crcinfo() {} - crcinfo(int crc, int matches) : crc(crc), matches(matches) {} - - static bool compare(const crcinfo &x, const crcinfo &y) { return x.matches > y.matches; } - }; - - VAR(modifiedmapspectator, 0, 1, 2); - - void checkmaps(int req = -1) - { - if(m_edit || !smapname[0]) return; - vector<crcinfo> crcs; - int total = 0, unsent = 0, invalid = 0; - if(mcrc) crcs.add(crcinfo(mcrc, clients.length() + 1)); - loopv(clients) - { - clientinfo *ci = clients[i]; - if(ci->state.state==CS_SPECTATOR || ci->state.aitype != AI_NONE) continue; - total++; - if(!ci->clientmap[0]) - { - if(ci->mapcrc < 0) invalid++; - else if(!ci->mapcrc) unsent++; - } - else - { - crcinfo *match = NULL; - loopvj(crcs) if(crcs[j].crc == ci->mapcrc) { match = &crcs[j]; break; } - if(!match) crcs.add(crcinfo(ci->mapcrc, 1)); - else match->matches++; - } - } - if(!mcrc && total - unsent < min(total, 4)) return; - crcs.sort(crcinfo::compare); - string msg; - loopv(clients) - { - clientinfo *ci = clients[i]; - if(ci->state.state==CS_SPECTATOR || ci->state.aitype != AI_NONE || ci->clientmap[0] || ci->mapcrc >= 0 || (req < 0 && ci->warned)) continue; - formatstring(msg, "%s has modified map \"%s\"", colorname(ci), smapname); - sendf(req, 1, "ris", N_SERVMSG, msg); - if(req < 0) ci->warned = true; - } - if(crcs.length() >= 2) loopv(crcs) - { - crcinfo &info = crcs[i]; - if(i || info.matches <= crcs[i+1].matches) loopvj(clients) - { - clientinfo *ci = clients[j]; - if(ci->state.state==CS_SPECTATOR || ci->state.aitype != AI_NONE || !ci->clientmap[0] || ci->mapcrc != info.crc || (req < 0 && ci->warned)) continue; - formatstring(msg, "%s has modified map \"%s\"", colorname(ci), smapname); - sendf(req, 1, "ris", N_SERVMSG, msg); - if(req < 0) ci->warned = true; - } - } - if(req < 0 && modifiedmapspectator && (mcrc || modifiedmapspectator > 1)) loopv(clients) - { - clientinfo *ci = clients[i]; - if(!ci->local && ci->warned && ci->state.state != CS_SPECTATOR) forcespectator(ci); - } - } - - bool shouldspectate(clientinfo *ci) - { - return !ci->local && ci->warned && modifiedmapspectator && (mcrc || modifiedmapspectator > 1); - } - - void unspectate(clientinfo *ci) - { - if(shouldspectate(ci)) return; - ci->state.state = CS_DEAD; - ci->state.respawn(); - ci->state.lasttimeplayed = lastmillis; - aiman::addclient(ci); - sendf(-1, 1, "ri3", N_SPECTATOR, ci->clientnum, 0); - if(ci->clientmap[0] || ci->mapcrc) checkmaps(); - if(!hasmap(ci)) rotatemap(true); - } - - void sendservinfo(clientinfo *ci) - { - sendf(ci->clientnum, 1, "ri5ss", N_SERVINFO, ci->clientnum, PROTOCOL_VERSION, ci->sessionid, serverpass[0] ? 1 : 0, serverdesc, serverauth); - } - - void noclients() - { - bannedips.shrink(0); - aiman::clearai(); - } - - void localconnect(int n) - { - clientinfo *ci = getinfo(n); - ci->clientnum = ci->ownernum = n; - ci->connectmillis = totalmillis; - ci->sessionid = (rnd(0x1000000)*((totalmillis%10000)+1))&0xFFFFFF; - ci->local = true; - - connects.add(ci); - sendservinfo(ci); - } - - void localdisconnect(int n) - { - if(m_demo) enddemoplayback(); - clientdisconnect(n); - } - - int clientconnect(int n) - { - clientinfo *ci = getinfo(n); - ci->clientnum = ci->ownernum = n; - ci->connectmillis = totalmillis; - ci->sessionid = (rnd(0x1000000)*((totalmillis%10000)+1))&0xFFFFFF; - - connects.add(ci); - if(!m_mp(gamemode)) return DISC_LOCAL; - sendservinfo(ci); - return DISC_NONE; - } - - void clientdisconnect(int n) - { - clientinfo *ci = getinfo(n); - loopv(clients) if(clients[i]->authkickvictim == ci->clientnum) clients[i]->cleanauth(); - if(ci->connected) - { - if(ci->privilege) setmaster(ci, false); - ci->state.timeplayed += lastmillis - ci->state.lasttimeplayed; - savescore(ci); - sendf(-1, 1, "ri2", N_CDIS, n); - clients.removeobj(ci); - aiman::removeai(ci); - if(!numclients(-1, false, true)) noclients(); // bans clear when server empties - if(ci->local) checkpausegame(); - } - else connects.removeobj(ci); - } - - int reserveclients() { return 3; } - - extern void verifybans(); - - struct banlist - { - vector<ipmask> bans; - - void clear() { bans.shrink(0); } - - bool check(uint ip) - { - loopv(bans) if(bans[i].check(ip)) return true; - return false; - } - - void add(const char *ipname) - { - ipmask ban; - ban.parse(ipname); - bans.add(ban); - - verifybans(); - } - } ipbans, gbans; - - bool checkbans(uint ip) - { - loopv(bannedips) if(bannedips[i].ip==ip) return true; - return ipbans.check(ip) || gbans.check(ip); - } - - void verifybans() - { - loopvrev(clients) - { - clientinfo *ci = clients[i]; - if(ci->state.aitype != AI_NONE || ci->local || ci->privilege >= PRIV_ADMIN) continue; - //~if(checkbans(getclientip(ci->clientnum))) disconnect_client(ci->clientnum, DISC_IPBAN); - } - } - - ICOMMAND(clearipbans, "", (), ipbans.clear()); - ICOMMAND(ipban, "s", (const char *ipname), ipbans.add(ipname)); - - int allowconnect(clientinfo *ci, const char *pwd = "") - { - if(ci->local) return DISC_NONE; - if(!m_mp(gamemode)) return DISC_LOCAL; - if(serverpass[0]) - { - if(!checkpassword(ci, serverpass, pwd)) return DISC_PASSWORD; - return DISC_NONE; - } - if(adminpass[0] && checkpassword(ci, adminpass, pwd)) return DISC_NONE; - if(numclients(-1, false, true)>=maxclients) return DISC_MAXCLIENTS; - uint ip = getclientip(ci->clientnum); - if(checkbans(ip)) return DISC_IPBAN; - if(mastermode>=MM_PRIVATE && allowedips.find(ip)<0) return DISC_PRIVATE; - return DISC_NONE; - } - - bool allowbroadcast(int n) - { - clientinfo *ci = getinfo(n); - return ci && ci->connected; - } - - clientinfo *findauth(uint id) - { - loopv(clients) if(clients[i]->authreq == id) return clients[i]; - return NULL; - } - - - void authfailed(clientinfo *ci) - { - if(!ci) return; - ci->cleanauth(); - //~if(ci->connectauth) disconnect_client(ci->clientnum, ci->connectauth); - } - - void authfailed(uint id) - { - authfailed(findauth(id)); - } - - void authsucceeded(uint id) - { - clientinfo *ci = findauth(id); - if(!ci) return; - ci->cleanauth(ci->connectauth!=0); - if(ci->connectauth) connected(ci); - if(ci->authkickvictim >= 0) - { - if(setmaster(ci, true, "", ci->authname, NULL, PRIV_AUTH, false, true)) - trykick(ci, ci->authkickvictim, ci->authkickreason, ci->authname, NULL, PRIV_AUTH); - ci->cleanauthkick(); - } - else setmaster(ci, true, "", ci->authname, NULL, PRIV_AUTH); - } - - void authchallenged(uint id, const char *val, const char *desc = "") - { - clientinfo *ci = findauth(id); - if(!ci) return; - sendf(ci->clientnum, 1, "risis", N_AUTHCHAL, desc, id, val); - } - - uint nextauthreq = 0; - - bool tryauth(clientinfo *ci, const char *user, const char *desc) - { - ci->cleanauth(); - if(!nextauthreq) nextauthreq = 1; - ci->authreq = nextauthreq++; - filtertext(ci->authname, user, false, false, 100); - copystring(ci->authdesc, desc); - if(ci->authdesc[0]) - { - userinfo *u = users.access(userkey(ci->authname, ci->authdesc)); - if(u) - { - uint seed[3] = { ::hthash(serverauth) + detrnd(size_t(ci) + size_t(user) + size_t(desc), 0x10000), uint(totalmillis), randomMT() }; - vector<char> buf; - ci->authchallenge = genchallenge(u->pubkey, seed, sizeof(seed), buf); - sendf(ci->clientnum, 1, "risis", N_AUTHCHAL, desc, ci->authreq, buf.getbuf()); - } - else ci->cleanauth(); - } - else if(!requestmasterf("reqauth %u %s\n", ci->authreq, ci->authname)) - { - ci->cleanauth(); - sendf(ci->clientnum, 1, "ris", N_SERVMSG, "not connected to authentication server"); - } - if(ci->authreq) return true; - //~if(ci->connectauth) disconnect_client(ci->clientnum, ci->connectauth); - return false; - } - - bool answerchallenge(clientinfo *ci, uint id, char *val, const char *desc) - { - if(ci->authreq != id || strcmp(ci->authdesc, desc)) - { - ci->cleanauth(); - return !ci->connectauth; - } - for(char *s = val; *s; s++) - { - if(!isxdigit(*s)) { *s = '\0'; break; } - } - if(desc[0]) - { - if(ci->authchallenge && checkchallenge(val, ci->authchallenge)) - { - userinfo *u = users.access(userkey(ci->authname, ci->authdesc)); - if(u) - { - if(ci->connectauth) connected(ci); - if(ci->authkickvictim >= 0) - { - if(setmaster(ci, true, "", ci->authname, ci->authdesc, u->privilege, false, true)) - trykick(ci, ci->authkickvictim, ci->authkickreason, ci->authname, ci->authdesc, u->privilege); - } - else setmaster(ci, true, "", ci->authname, ci->authdesc, u->privilege); - } - } - ci->cleanauth(); - } - else if(!requestmasterf("confauth %u %s\n", id, val)) - { - ci->cleanauth(); - sendf(ci->clientnum, 1, "ris", N_SERVMSG, "not connected to authentication server"); - } - return ci->authreq || !ci->connectauth; - } - - void masterconnected() - { - } - - void masterdisconnected() - { - loopvrev(clients) - { - clientinfo *ci = clients[i]; - if(ci->authreq) authfailed(ci); - } - } - - void processmasterinput(const char *cmd, int cmdlen, const char *args) - { - uint id; - string val; - if(sscanf(cmd, "failauth %u", &id) == 1) - authfailed(id); - else if(sscanf(cmd, "succauth %u", &id) == 1) - authsucceeded(id); - else if(sscanf(cmd, "chalauth %u %255s", &id, val) == 2) - authchallenged(id, val); - else if(matchstring(cmd, cmdlen, "cleargbans")) - gbans.clear(); - else if(sscanf(cmd, "addgban %100s", val) == 1) - gbans.add(val); - } - - void receivefile(int sender, uchar *data, int len) - { - if(!m_edit || len <= 0 || len > 4*1024*1024) return; - clientinfo *ci = getinfo(sender); - if(ci->state.state==CS_SPECTATOR && !ci->privilege && !ci->local) return; - if(mapdata) DELETEP(mapdata); - mapdata = opentempfile("mapdata", "w+b"); - if(!mapdata) { sendf(sender, 1, "ris", N_SERVMSG, "failed to open temporary file for map"); return; } - mapdata->write(data, len); - sendservmsgf("[%s sent a map to server, \"/getmap\" to receive it]", colorname(ci)); - } - - void sendclipboard(clientinfo *ci) - { - if(!ci->lastclipboard || !ci->clipboard) return; - bool flushed = false; - loopv(clients) - { - clientinfo &e = *clients[i]; - if(e.clientnum != ci->clientnum && e.needclipboard - ci->lastclipboard >= 0) - { - if(!flushed) { flushserver(true); flushed = true; } - sendpacket(e.clientnum, 1, ci->clipboard); - } - } - } - - void connected(clientinfo *ci) - { - if(m_demo) enddemoplayback(); - - if(!hasmap(ci)) rotatemap(false); - - shouldstep = true; - - connects.removeobj(ci); - clients.add(ci); - - ci->connectauth = 0; - ci->connected = true; - ci->needclipboard = totalmillis ? totalmillis : 1; - if(mastermode>=MM_LOCKED) ci->state.state = CS_SPECTATOR; - ci->state.lasttimeplayed = lastmillis; - - const char *worst = m_teammode ? chooseworstteam(NULL, ci) : NULL; - copystring(ci->team, worst ? worst : "good", MAXTEAMLEN+1); - - sendwelcome(ci); - if(restorescore(ci)) sendresume(ci); - sendinitclient(ci); - - aiman::addclient(ci); - - if(m_demo) setupdemoplayback(); - - if(servermotd[0]) sendf(ci->clientnum, 1, "ris", N_SERVMSG, servermotd); - } - - void parsepacket(int sender, int chan, packetbuf &p) // has to parse exactly each byte of the packet - { - if(sender<0 || p.packet->flags&ENET_PACKET_FLAG_UNSEQUENCED || chan > 2) return; - char text[MAXTRANS]; - int type; - clientinfo *ci = sender>=0 ? getinfo(sender) : NULL, *cq = ci, *cm = ci; - if(ci && !ci->connected) - { - if(chan==0) return; - //~else if(chan!=1) { disconnect_client(sender, DISC_MSGERR); return; } - else while(p.length() < p.maxlen) switch(checktype(getint(p), ci)) - { - case N_CONNECT: - { - getstring(text, p); - filtertext(text, text, false, false, MAXNAMELEN); - if(!text[0]) copystring(text, "Anonymous"); - copystring(ci->name, text, MAXNAMELEN+1); - ci->playermodel = 0; - - string password, authdesc, authname; - getstring(password, p, sizeof(password)); - getstring(authdesc, p, sizeof(authdesc)); - getstring(authname, p, sizeof(authname)); - int disc = allowconnect(ci, password); - if(disc) - { - if(disc == DISC_LOCAL || !serverauth[0] || strcmp(serverauth, authdesc) || !tryauth(ci, authname, authdesc)) - { - //~disconnect_client(sender, disc); - return; - } - ci->connectauth = disc; - } - else connected(ci); - break; - } - - case N_AUTHANS: - { - string desc, ans; - getstring(desc, p, sizeof(desc)); - uint id = (uint)getint(p); - getstring(ans, p, sizeof(ans)); - if(!answerchallenge(ci, id, ans, desc)) - { - //~disconnect_client(sender, ci->connectauth); - return; - } - break; - } - - case N_PING: - getint(p); - break; - - default: - //~disconnect_client(sender, DISC_MSGERR); - return; - } - return; - } - else if(chan==2) - { - receivefile(sender, p.buf, p.maxlen); - return; - } - - if(p.packet->flags&ENET_PACKET_FLAG_RELIABLE) reliablemessages = true; - #define QUEUE_AI clientinfo *cm = cq; - #define QUEUE_MSG { if(cm && (!cm->local || demorecord || hasnonlocalclients())) while(curmsg<p.length()) cm->messages.add(p.buf[curmsg++]); } - #define QUEUE_BUF(body) { \ - if(cm && (!cm->local || demorecord || hasnonlocalclients())) \ - { \ - curmsg = p.length(); \ - { body; } \ - } \ - } - #define QUEUE_INT(n) QUEUE_BUF(putint(cm->messages, n)) - #define QUEUE_UINT(n) QUEUE_BUF(putuint(cm->messages, n)) - #define QUEUE_STR(text) QUEUE_BUF(sendstring(text, cm->messages)) - int curmsg; - while((curmsg = p.length()) < p.maxlen) switch(type = checktype(getint(p), ci)) - { - case N_POS: - { - int pcn = getuint(p); - p.get(); - uint flags = getuint(p); - clientinfo *cp = getinfo(pcn); - if(cp && pcn != sender && cp->ownernum != sender) cp = NULL; - vec pos; - loopk(3) - { - int n = p.get(); n |= p.get()<<8; if(flags&(1<<k)) { n |= p.get()<<16; if(n&0x800000) n |= ~0U<<24; } - pos[k] = n/DMF; - } - loopk(3) p.get(); - int mag = p.get(); if(flags&(1<<3)) mag |= p.get()<<8; - int dir = p.get(); dir |= p.get()<<8; - vec vel = vec((dir%360)*RAD, (clamp(dir/360, 0, 180)-90)*RAD).mul(mag/DVELF); - if(flags&(1<<4)) - { - p.get(); if(flags&(1<<5)) p.get(); - if(flags&(1<<6)) loopk(2) p.get(); - } - if(cp) - { - if((!ci->local || demorecord || hasnonlocalclients()) && (cp->state.state==CS_ALIVE || cp->state.state==CS_EDITING)) - { - if(!ci->local && !m_edit && max(vel.magnitude2(), (float)fabs(vel.z)) >= 180) - cp->setexceeded(); - cp->position.setsize(0); - while(curmsg<p.length()) cp->position.add(p.buf[curmsg++]); - } - cp->state.o = pos; - cp->gameclip = (flags&0x80)!=0; - } - break; - } - - case N_TELEPORT: - { - int pcn = getint(p), teleport = getint(p), teledest = getint(p); - clientinfo *cp = getinfo(pcn); - if(cp && pcn != sender && cp->ownernum != sender) cp = NULL; - if(cp && (!ci->local || demorecord || hasnonlocalclients()) && (cp->state.state==CS_ALIVE || cp->state.state==CS_EDITING)) - { - flushclientposition(*cp); - sendf(-1, 0, "ri4x", N_TELEPORT, pcn, teleport, teledest, cp->ownernum); - } - break; - } - - case N_JUMPPAD: - { - int pcn = getint(p), jumppad = getint(p); - clientinfo *cp = getinfo(pcn); - if(cp && pcn != sender && cp->ownernum != sender) cp = NULL; - if(cp && (!ci->local || demorecord || hasnonlocalclients()) && (cp->state.state==CS_ALIVE || cp->state.state==CS_EDITING)) - { - cp->setpushed(); - flushclientposition(*cp); - sendf(-1, 0, "ri3x", N_JUMPPAD, pcn, jumppad, cp->ownernum); - } - break; - } - - case N_FROMAI: - { - int qcn = getint(p); - if(qcn < 0) cq = ci; - else - { - cq = getinfo(qcn); - if(cq && qcn != sender && cq->ownernum != sender) cq = NULL; - } - break; - } - - case N_EDITMODE: - { - int val = getint(p); - if(!ci->local && !m_edit) break; - if(val ? ci->state.state!=CS_ALIVE && ci->state.state!=CS_DEAD : ci->state.state!=CS_EDITING) break; - if(val) - { - ci->state.editstate = ci->state.state; - ci->state.state = CS_EDITING; - ci->events.setsize(0); - ci->state.rockets.reset(); - ci->state.grenades.reset(); - } - else ci->state.state = ci->state.editstate; - QUEUE_MSG; - break; - } - - case N_MAPCRC: - { - getstring(text, p); - int crc = getint(p); - if(!ci) break; - if(strcmp(text, smapname)) - { - if(ci->clientmap[0]) - { - ci->clientmap[0] = '\0'; - ci->mapcrc = 0; - } - else if(ci->mapcrc > 0) ci->mapcrc = 0; - break; - } - copystring(ci->clientmap, text); - ci->mapcrc = text[0] ? crc : 1; - checkmaps(); - if(cq && cq != ci && cq->ownernum != ci->clientnum) cq = NULL; - break; - } - - case N_CHECKMAPS: - checkmaps(sender); - break; - - case N_TRYSPAWN: - if(!ci || !cq || cq->state.state!=CS_DEAD || cq->state.lastspawn>=0) break; - if(!ci->clientmap[0] && !ci->mapcrc) - { - ci->mapcrc = -1; - checkmaps(); - if(ci == cq) { if(ci->state.state != CS_DEAD) break; } - else if(cq->ownernum != ci->clientnum) { cq = NULL; break; } - } - if(cq->state.deadflush) - { - flushevents(cq, cq->state.deadflush); - cq->state.respawn(); - } - cleartimedevents(cq); - sendspawn(cq); - break; - - case N_GUNSELECT: - { - int gunselect = getint(p); - if(!cq || cq->state.state!=CS_ALIVE) break; - cq->state.gunselect = gunselect >= GUN_FIST && gunselect <= GUN_PISTOL ? gunselect : GUN_FIST; - QUEUE_AI; - QUEUE_MSG; - break; - } - - case N_SPAWN: - { - int ls = getint(p), gunselect = getint(p); - if(!cq || (cq->state.state!=CS_ALIVE && cq->state.state!=CS_DEAD && cq->state.state!=CS_EDITING) || ls!=cq->state.lifesequence || cq->state.lastspawn<0) break; - cq->state.lastspawn = -1; - cq->state.state = CS_ALIVE; - cq->state.gunselect = gunselect >= GUN_FIST && gunselect <= GUN_PISTOL ? gunselect : GUN_FIST; - cq->exceeded = 0; - QUEUE_AI; - QUEUE_BUF({ - putint(cm->messages, N_SPAWN); - sendstate(cq->state, cm->messages); - }); - break; - } - - case N_SUICIDE: - { - if(cq) cq->addevent(new suicideevent); - break; - } - - case N_SHOOT: - { - shotevent *shot = new shotevent; - shot->id = getint(p); - shot->millis = cq ? cq->geteventmillis(gamemillis, shot->id) : 0; - shot->gun = getint(p); - loopk(3) shot->from[k] = getint(p)/DMF; - loopk(3) shot->to[k] = getint(p)/DMF; - int hits = getint(p); - loopk(hits) - { - if(p.overread()) break; - hitinfo &hit = shot->hits.add(); - hit.target = getint(p); - hit.lifesequence = getint(p); - hit.dist = getint(p)/DMF; - hit.rays = getint(p); - loopk(3) hit.dir[k] = getint(p)/DNF; - } - if(cq) - { - cq->addevent(shot); - cq->setpushed(); - } - else delete shot; - break; - } - - case N_EXPLODE: - { - explodeevent *exp = new explodeevent; - int cmillis = getint(p); - exp->millis = cq ? cq->geteventmillis(gamemillis, cmillis) : 0; - exp->gun = getint(p); - exp->id = getint(p); - int hits = getint(p); - loopk(hits) - { - if(p.overread()) break; - hitinfo &hit = exp->hits.add(); - hit.target = getint(p); - hit.lifesequence = getint(p); - hit.dist = getint(p)/DMF; - hit.rays = getint(p); - loopk(3) hit.dir[k] = getint(p)/DNF; - } - if(cq) cq->addevent(exp); - else delete exp; - break; - } - - case N_ITEMPICKUP: - { - int n = getint(p); - if(!cq) break; - pickupevent *pickup = new pickupevent; - pickup->ent = n; - cq->addevent(pickup); - break; - } - - case N_TEXT: - { - QUEUE_AI; - QUEUE_MSG; - getstring(text, p); - filtertext(text, text, true, true); - QUEUE_STR(text); - if(isdedicatedserver() && cq) logoutf("%s: %s", colorname(cq), text); - break; - } - - case N_SAYTEAM: - { - getstring(text, p); - if(!ci || !cq || (ci->state.state==CS_SPECTATOR && !ci->local && !ci->privilege) || !m_teammode || !cq->team[0]) break; - filtertext(text, text, true, true); - loopv(clients) - { - clientinfo *t = clients[i]; - if(t==cq || t->state.state==CS_SPECTATOR || t->state.aitype != AI_NONE || strcmp(cq->team, t->team)) continue; - sendf(t->clientnum, 1, "riis", N_SAYTEAM, cq->clientnum, text); - } - if(isdedicatedserver() && cq) logoutf("%s <%s>: %s", colorname(cq), cq->team, text); - break; - } - - case N_SWITCHNAME: - { - QUEUE_MSG; - getstring(text, p); - filtertext(ci->name, text, false, false, MAXNAMELEN); - if(!ci->name[0]) copystring(ci->name, "Anonymous"); - QUEUE_STR(ci->name); - break; - } - - case N_SWITCHMODEL: - { - ci->playermodel = 0; - QUEUE_MSG; - break; - } - - case N_SWITCHTEAM: - { - getstring(text, p); - filtertext(text, text, false, false, MAXTEAMLEN); - if(m_teammode && text[0] && strcmp(ci->team, text) && addteaminfo(text)) - { - if(ci->state.state==CS_ALIVE) suicide(ci); - copystring(ci->team, text); - aiman::changeteam(ci); - sendf(-1, 1, "riisi", N_SETTEAM, sender, ci->team, ci->state.state==CS_SPECTATOR ? -1 : 0); - } - break; - } - - case N_MAPVOTE: - { - getstring(text, p); - filtertext(text, text, false); - fixmapname(text); - int reqmode = getint(p); - vote(text, reqmode, sender); - break; - } - - case N_ITEMLIST: - { - if((ci->state.state==CS_SPECTATOR && !ci->privilege && !ci->local) || !notgotitems || strcmp(ci->clientmap, smapname)) { while(getint(p)>=0 && !p.overread()) getint(p); break; } - int n; - while((n = getint(p))>=0 && n<MAXENTS && !p.overread()) - { - server_entity se = { NOTUSED, 0, false }; - while(sents.length()<=n) sents.add(se); - sents[n].type = getint(p); - if(canspawnitem(sents[n].type)) - { - if(m_mp(gamemode) && delayspawn(sents[n].type)) sents[n].spawntime = spawntime(sents[n].type); - else sents[n].spawned = true; - } - } - notgotitems = false; - break; - } - - case N_EDITENT: - { - int i = getint(p); - loopk(3) getint(p); - int type = getint(p); - loopk(5) getint(p); - if(!ci || ci->state.state==CS_SPECTATOR) break; - QUEUE_MSG; - bool canspawn = canspawnitem(type); - if(i<MAXENTS && (sents.inrange(i) || canspawnitem(type))) - { - server_entity se = { NOTUSED, 0, false }; - while(sents.length()<=i) sents.add(se); - sents[i].type = type; - if(canspawn ? !sents[i].spawned : (sents[i].spawned || sents[i].spawntime)) - { - sents[i].spawntime = canspawn ? 1 : 0; - sents[i].spawned = false; - } - } - break; - } - - case N_EDITVAR: - { - int type = getint(p); - getstring(text, p); - switch(type) - { - case ID_VAR: getint(p); break; - case ID_FVAR: getfloat(p); break; - case ID_SVAR: getstring(text, p); - } - if(ci && ci->state.state!=CS_SPECTATOR) QUEUE_MSG; - break; - } - - case N_PING: - sendf(sender, 1, "i2", N_PONG, getint(p)); - break; - - case N_CLIENTPING: - { - int ping = getint(p); - if(ci) - { - ci->ping = ping; - loopv(ci->bots) ci->bots[i]->ping = ping; - } - QUEUE_MSG; - break; - } - - case N_MASTERMODE: - { - int mm = getint(p); - if((ci->privilege || ci->local) && mm>=MM_OPEN && mm<=MM_PRIVATE) - { - if((ci->privilege>=PRIV_ADMIN || ci->local) || (mastermask&(1<<mm))) - { - mastermode = mm; - allowedips.shrink(0); - if(mm>=MM_PRIVATE) - { - loopv(clients) allowedips.add(getclientip(clients[i]->clientnum)); - } - sendf(-1, 1, "rii", N_MASTERMODE, mastermode); - //sendservmsgf("mastermode is now %s (%d)", mastermodename(mastermode), mastermode); - } - else - { - defformatstring(s, "mastermode %d is disabled on this server", mm); - sendf(sender, 1, "ris", N_SERVMSG, s); - } - } - break; - } - - case N_CLEARBANS: - { - if(ci->privilege || ci->local) - { - bannedips.shrink(0); - sendservmsg("cleared all bans"); - } - break; - } - - case N_KICK: - { - int victim = getint(p); - getstring(text, p); - filtertext(text, text); - trykick(ci, victim, text); - break; - } - - case N_SPECTATOR: - { - int spectator = getint(p), val = getint(p); - if(!ci->privilege && !ci->local && (spectator!=sender || (ci->state.state==CS_SPECTATOR && mastermode>=MM_LOCKED))) break; - clientinfo *spinfo = (clientinfo *)getclientinfo(spectator); // no bots - if(!spinfo || !spinfo->connected || (spinfo->state.state==CS_SPECTATOR ? val : !val)) break; - - if(spinfo->state.state!=CS_SPECTATOR && val) forcespectator(spinfo); - else if(spinfo->state.state==CS_SPECTATOR && !val) unspectate(spinfo); - - if(cq && cq != ci && cq->ownernum != ci->clientnum) cq = NULL; - break; - } - - case N_SETTEAM: - { - int who = getint(p); - getstring(text, p); - filtertext(text, text, false, false, MAXTEAMLEN); - if(!ci->privilege && !ci->local) break; - clientinfo *wi = getinfo(who); - if(!m_teammode || !text[0] || !wi || !wi->connected || !strcmp(wi->team, text)) break; - if(addteaminfo(text)) - { - if(wi->state.state==CS_ALIVE) suicide(wi); - copystring(wi->team, text, MAXTEAMLEN+1); - } - aiman::changeteam(wi); - sendf(-1, 1, "riisi", N_SETTEAM, who, wi->team, 1); - break; - } - - case N_FORCEINTERMISSION: - if(ci->local && !hasnonlocalclients()) startintermission(); - break; - - case N_RECORDDEMO: - { - int val = getint(p); - if(ci->privilege < (restrictdemos ? PRIV_ADMIN : PRIV_MASTER) && !ci->local) break; - if(!maxdemos || !maxdemosize) - { - sendf(ci->clientnum, 1, "ris", N_SERVMSG, "the server has disabled demo recording"); - break; - } - demonextmatch = val!=0; - sendservmsgf("demo recording is %s for next match", demonextmatch ? "enabled" : "disabled"); - break; - } - - case N_STOPDEMO: - { - if(ci->privilege < (restrictdemos ? PRIV_ADMIN : PRIV_MASTER) && !ci->local) break; - stopdemo(); - break; - } - - case N_CLEARDEMOS: - { - int demo = getint(p); - if(ci->privilege < (restrictdemos ? PRIV_ADMIN : PRIV_MASTER) && !ci->local) break; - cleardemos(demo); - break; - } - - case N_LISTDEMOS: - if(!ci->privilege && !ci->local && ci->state.state==CS_SPECTATOR) break; - listdemos(sender); - break; - - case N_GETDEMO: - { - int n = getint(p), tag = getint(p); - if(!ci->privilege && !ci->local && ci->state.state==CS_SPECTATOR) break; - senddemo(ci, n, tag); - break; - } - - case N_GETMAP: - if(!mapdata) sendf(sender, 1, "ris", N_SERVMSG, "no map to send"); - else if(ci->getmap) sendf(sender, 1, "ris", N_SERVMSG, "already sending map"); - else - { - sendservmsgf("[%s is getting the map]", colorname(ci)); - if((ci->getmap = sendfile(sender, 2, mapdata, "ri", N_SENDMAP))) - ci->getmap->freeCallback = freegetmap; - ci->needclipboard = totalmillis ? totalmillis : 1; - } - break; - - case N_NEWMAP: - { - int size = getint(p); - if(!ci->privilege && !ci->local && ci->state.state==CS_SPECTATOR) break; - if(size>=0) - { - smapname[0] = '\0'; - resetitems(); - notgotitems = false; - } - QUEUE_MSG; - break; - } - - case N_SETMASTER: - { - int mn = getint(p), val = getint(p); - getstring(text, p); - if(mn != ci->clientnum) - { - if(!ci->privilege && !ci->local) break; - clientinfo *minfo = (clientinfo *)getclientinfo(mn); - if(!minfo || !minfo->connected || (!ci->local && minfo->privilege >= ci->privilege) || (val && minfo->privilege)) break; - setmaster(minfo, val!=0, "", NULL, NULL, PRIV_MASTER, true); - } - else setmaster(ci, val!=0, text); - // don't broadcast the master password - break; - } - - case N_ADDBOT: - { - aiman::reqadd(ci, getint(p)); - break; - } - - case N_DELBOT: - { - aiman::reqdel(ci); - break; - } - - case N_BOTLIMIT: - { - int limit = getint(p); - if(ci) aiman::setbotlimit(ci, limit); - break; - } - - case N_BOTBALANCE: - { - int balance = getint(p); - if(ci) aiman::setbotbalance(ci, balance!=0); - break; - } - - case N_AUTHTRY: - { - string desc, name; - getstring(desc, p, sizeof(desc)); - getstring(name, p, sizeof(name)); - tryauth(ci, name, desc); - break; - } - - case N_AUTHKICK: - { - string desc, name; - getstring(desc, p, sizeof(desc)); - getstring(name, p, sizeof(name)); - int victim = getint(p); - getstring(text, p); - filtertext(text, text); - int authpriv = PRIV_AUTH; - if(desc[0]) - { - userinfo *u = users.access(userkey(name, desc)); - if(u) authpriv = u->privilege; else break; - } - if(ci->local || ci->privilege >= authpriv) trykick(ci, victim, text); - else if(trykick(ci, victim, text, name, desc, authpriv, true) && tryauth(ci, name, desc)) - { - ci->authkickvictim = victim; - ci->authkickreason = newstring(text); - } - break; - } - - case N_AUTHANS: - { - string desc, ans; - getstring(desc, p, sizeof(desc)); - uint id = (uint)getint(p); - getstring(ans, p, sizeof(ans)); - answerchallenge(ci, id, ans, desc); - break; - } - - case N_PAUSEGAME: - { - int val = getint(p); - if(ci->privilege < (restrictpausegame ? PRIV_ADMIN : PRIV_MASTER) && !ci->local) break; - pausegame(val > 0, ci); - break; - } - - case N_GAMESPEED: - { - int val = getint(p); - if(ci->privilege < (restrictgamespeed ? PRIV_ADMIN : PRIV_MASTER) && !ci->local) break; - changegamespeed(val, ci); - break; - } - - case N_COPY: - ci->cleanclipboard(); - ci->lastclipboard = totalmillis ? totalmillis : 1; - goto genericmsg; - - case N_PASTE: - if(ci->state.state!=CS_SPECTATOR) sendclipboard(ci); - goto genericmsg; - - case N_CLIPBOARD: - { - int unpacklen = getint(p), packlen = getint(p); - ci->cleanclipboard(false); - if(ci->state.state==CS_SPECTATOR) - { - if(packlen > 0) p.subbuf(packlen); - break; - } - if(packlen <= 0 || packlen > (1<<16) || unpacklen <= 0) - { - if(packlen > 0) p.subbuf(packlen); - packlen = unpacklen = 0; - } - packetbuf q(32 + packlen, ENET_PACKET_FLAG_RELIABLE); - putint(q, N_CLIPBOARD); - putint(q, ci->clientnum); - putint(q, unpacklen); - putint(q, packlen); - if(packlen > 0) p.get(q.subbuf(packlen).buf, packlen); - ci->clipboard = q.finalize(); - ci->clipboard->referenceCount++; - break; - } - - case N_EDITT: - case N_REPLACE: - case N_EDITVSLOT: - { - int size = server::msgsizelookup(type); - //~if(size<=0) { disconnect_client(sender, DISC_MSGERR); return; } - loopi(size-1) getint(p); - //~if(p.remaining() < 2) { disconnect_client(sender, DISC_MSGERR); return; } - int extra = lilswap(*(const ushort *)p.pad(2)); - //~if(p.remaining() < extra) { disconnect_client(sender, DISC_MSGERR); return; } - p.pad(extra); - if(ci && ci->state.state!=CS_SPECTATOR) QUEUE_MSG; - break; - } - - case N_UNDO: - case N_REDO: - { - int unpacklen = getint(p), packlen = getint(p); - if(!ci || ci->state.state==CS_SPECTATOR || packlen <= 0 || packlen > (1<<16) || unpacklen <= 0) - { - if(packlen > 0) p.subbuf(packlen); - break; - } - //~if(p.remaining() < packlen) { disconnect_client(sender, DISC_MSGERR); return; } - packetbuf q(32 + packlen, ENET_PACKET_FLAG_RELIABLE); - putint(q, type); - putint(q, ci->clientnum); - putint(q, unpacklen); - putint(q, packlen); - if(packlen > 0) p.get(q.subbuf(packlen).buf, packlen); - sendpacket(-1, 1, q.finalize(), ci->clientnum); - break; - } - - case N_SERVCMD: - getstring(text, p); - break; - - - case -1: - //~disconnect_client(sender, DISC_MSGERR); - return; - - case -2: - //~disconnect_client(sender, DISC_OVERFLOW); - return; - - default: genericmsg: - { - int size = server::msgsizelookup(type); - //~if(size<=0) { disconnect_client(sender, DISC_MSGERR); return; } - loopi(size-1) getint(p); - if(ci) switch(msgfilter[type]) - { - case 2: case 3: if(ci->state.state != CS_SPECTATOR) QUEUE_MSG; break; - default: if(cq && (ci != cq || ci->state.state!=CS_SPECTATOR)) { QUEUE_AI; QUEUE_MSG; } break; - } - break; - } - } - } - - int laninfoport() { return SAUERBRATEN_LANINFO_PORT; } - int serverinfoport(int servport) { return servport < 0 ? SAUERBRATEN_SERVINFO_PORT : servport+1; } - int serverport(int infoport) { return infoport < 0 ? SAUERBRATEN_SERVER_PORT : infoport-1; } - const char *defaultmaster() { return "master.sauerbraten.org"; } - int masterport() { return SAUERBRATEN_MASTER_PORT; } - int numchannels() { return 3; } - - #include "extinfo.h" - - void serverinforeply(ucharbuf &req, ucharbuf &p) - { - if(req.remaining() && !getint(req)) - { - extserverinforeply(req, p); - return; - } - - putint(p, numclients(-1, false, true)); - putint(p, gamepaused || gamespeed != 100 ? 7 : 5); // number of attrs following - putint(p, PROTOCOL_VERSION); // generic attributes, passed back below - putint(p, gamemode); - putint(p, m_timed ? max((gamelimit - gamemillis)/1000, 0) : 0); - putint(p, maxclients); - putint(p, serverpass[0] ? MM_PASSWORD : (!m_mp(gamemode) ? MM_PRIVATE : (mastermode || mastermask&MM_AUTOAPPROVE ? mastermode : MM_AUTH))); - if(gamepaused || gamespeed != 100) - { - putint(p, gamepaused ? 1 : 0); - putint(p, gamespeed); - } - sendstring(smapname, p); - sendstring(serverdesc, p); - sendserverinforeply(p); - } - - #include "aiman.h" + SVAR(servermotd, ""); + + struct teamkillkick + { + int modes, limit, ban; + + bool match(int mode) const + { + return (modes&(1<<(mode-STARTGAMEMODE)))!=0; + } + + bool includes(const teamkillkick &tk) const + { + return tk.modes != modes && (tk.modes & modes) == tk.modes; + } + }; + vector<teamkillkick> teamkillkicks; + + void teamkillkickreset() + { + teamkillkicks.setsize(0); + } + + void addteamkillkick(char *modestr, int *limit, int *ban) + { + vector<char *> modes; + explodelist(modestr, modes); + teamkillkick &kick = teamkillkicks.add(); + kick.modes = genmodemask(modes); + kick.limit = *limit; + kick.ban = *ban > 0 ? *ban*60000 : (*ban < 0 ? 0 : 30*60000); + modes.deletearrays(); + } + + COMMAND(teamkillkickreset, ""); + COMMANDN(teamkillkick, addteamkillkick, "sii"); + + struct teamkillinfo + { + uint ip; + int teamkills; + }; + vector<teamkillinfo> teamkills; + bool shouldcheckteamkills = false; + + void addteamkill(clientinfo *actor, clientinfo *victim, int n) + { + if(!m_timed || actor->state.aitype != AI_NONE || actor->local || actor->privilege || (victim && victim->state.aitype != AI_NONE)) return; + shouldcheckteamkills = true; + uint ip = getclientip(actor->clientnum); + loopv(teamkills) if(teamkills[i].ip == ip) + { + teamkills[i].teamkills += n; + return; + } + teamkillinfo &tk = teamkills.add(); + tk.ip = ip; + tk.teamkills = n; + } + + void checkteamkills() + { + teamkillkick *kick = NULL; + if(m_timed) loopv(teamkillkicks) if(teamkillkicks[i].match(gamemode) && (!kick || kick->includes(teamkillkicks[i]))) + kick = &teamkillkicks[i]; + if(kick) loopvrev(teamkills) + { + teamkillinfo &tk = teamkills[i]; + if(tk.teamkills >= kick->limit) + { + if(kick->ban > 0) addban(tk.ip, kick->ban); + kickclients(tk.ip); + teamkills.removeunordered(i); + } + } + shouldcheckteamkills = false; + } + + void *newclientinfo() { return new clientinfo; } + void deleteclientinfo(void *ci) { delete (clientinfo *)ci; } + + clientinfo *getinfo(int n) + { + if(n < MAXCLIENTS) return (clientinfo *)getclientinfo(n); + n -= MAXCLIENTS; + return bots.inrange(n) ? bots[n] : NULL; + } + + uint mcrc = 0; + vector<entity> ments; + vector<server_entity> sents; + vector<savedscore> scores; + + int msgsizelookup(int msg) + { + static int sizetable[NUMMSG] = { -1 }; + if(sizetable[0] < 0) + { + memset(sizetable, -1, sizeof(sizetable)); + for(const int *p = msgsizes; *p >= 0; p += 2) sizetable[p[0]] = p[1]; + } + return msg >= 0 && msg < NUMMSG ? sizetable[msg] : -1; + } + + const char *modename(int n, const char *unknown) + { + if(m_valid(n)) return gamemodes[n - STARTGAMEMODE].name; + return unknown; + } + + const char *mastermodename(int n, const char *unknown) + { + return (n>=MM_START && size_t(n-MM_START)<sizeof(mastermodenames)/sizeof(mastermodenames[0])) ? mastermodenames[n-MM_START] : unknown; + } + + const char *privname(int type) + { + switch(type) + { + case PRIV_ADMIN: return "admin"; + case PRIV_AUTH: return "auth"; + case PRIV_MASTER: return "master"; + default: return "unknown"; + } + } + + void sendservmsg(const char *s) { sendf(-1, 1, "ris", N_SERVMSG, s); } + void sendservmsgf(const char *fmt, ...) + { + defvformatstring(s, fmt, fmt); + sendf(-1, 1, "ris", N_SERVMSG, s); + } + + void resetitems() + { + mcrc = 0; + ments.setsize(0); + sents.setsize(0); + //cps.reset(); + } + + bool serveroption(const char *arg) + { + if(arg[0]=='-') switch(arg[1]) + { + case 'n': setsvar("serverdesc", &arg[2]); return true; + case 'y': setsvar("serverpass", &arg[2]); return true; + case 'p': setsvar("adminpass", &arg[2]); return true; + case 'o': setvar("publicserver", atoi(&arg[2])); return true; + } + return false; + } + + void serverinit() + { + smapname[0] = '\0'; + resetitems(); + } + + int numclients(int exclude = -1, bool nospec = true, bool noai = true, bool priv = false) + { + int n = 0; + loopv(clients) + { + clientinfo *ci = clients[i]; + if(ci->clientnum!=exclude && (!nospec || ci->state.state!=CS_SPECTATOR || (priv && (ci->privilege || ci->local))) && (!noai || ci->state.aitype == AI_NONE)) n++; + } + return n; + } + + bool duplicatename(clientinfo *ci, const char *name) + { + if(!name) name = ci->name; + loopv(clients) if(clients[i]!=ci && !strcmp(name, clients[i]->name)) return true; + return false; + } + + const char *colorname(clientinfo *ci, const char *name = NULL) + { + if(!name) name = ci->name; + if(name[0] && !duplicatename(ci, name) && ci->state.aitype == AI_NONE) return name; + static string cname[3]; + static int cidx = 0; + cidx = (cidx+1)%3; + formatstring(cname[cidx], ci->state.aitype == AI_NONE ? "%s \fs\f5(%d)\fr" : "%s \fs\f5[%d]\fr", name, ci->clientnum); + return cname[cidx]; + } + + bool canspawnitem(int type) { return !m_noitems && (type>=I_SHELLS && type<=I_QUAD && (!m_noammo || type<I_SHELLS || type>I_CARTRIDGES)); } + + int spawntime(int type) + { + int np = numclients(-1, true, false); + np = np<3 ? 4 : (np>4 ? 2 : 3); // spawn times are dependent on number of players + int sec = 0; + switch(type) + { + case I_SHELLS: + case I_BULLETS: + case I_ROCKETS: + case I_ROUNDS: + case I_GRENADES: + case I_CARTRIDGES: sec = np*4; break; + case I_HEALTH: sec = np*5; break; + case I_TINYHEALTH: sec = np*5; break; + case I_TINYARMOUR: sec = np*5; break; + case I_GREENARMOUR: sec = 20; break; + case I_YELLOWARMOUR: sec = 30; break; + case I_BOOST: sec = 60; break; + case I_QUAD: sec = 70; break; + } + return sec*1000; + } + + bool delayspawn(int type) + { + switch(type) + { + case I_GREENARMOUR: + case I_YELLOWARMOUR: + case I_BOOST: + case I_QUAD: + return true; + default: + return false; + } + } + + bool pickup(int i, int sender) // server side item pickup, acknowledge first client that gets it + { + if((m_timed && gamemillis>=gamelimit) || !sents.inrange(i) || !sents[i].spawned) return false; + clientinfo *ci = getinfo(sender); + if(!ci) return false; + if(!ci->local && !ci->state.canpickup(sents[i].type)) + { + sendf(sender, 1, "ri3", N_ITEMACC, i, -1); + return false; + } + sents[i].spawned = false; + sents[i].spawntime = spawntime(sents[i].type); + sendf(-1, 1, "ri3", N_ITEMACC, i, sender); + ci->state.pickup(sents[i].type); + return true; + } + + static hashset<teaminfo> teaminfos; + + void clearteaminfo() + { + teaminfos.clear(); + } + + bool teamhasplayers(const char *team) { loopv(clients) if(!strcmp(clients[i]->team, team)) return true; return false; } + + bool pruneteaminfo() + { + int oldteams = teaminfos.numelems; + enumerate(teaminfos, teaminfo, old, + if(!old.frags && !teamhasplayers(old.team)) teaminfos.remove(old.team); + ); + return teaminfos.numelems < oldteams; + } + + teaminfo *addteaminfo(const char *team) + { + teaminfo *t = teaminfos.access(team); + if(!t) + { + if(teaminfos.numelems >= MAXTEAMS && !pruneteaminfo()) return NULL; + t = &teaminfos[team]; + copystring(t->team, team, sizeof(t->team)); + t->frags = 0; + } + return t; + } + + clientinfo *choosebestclient(float &bestrank) + { + clientinfo *best = NULL; + bestrank = -1; + loopv(clients) + { + clientinfo *ci = clients[i]; + if(ci->state.timeplayed<0) continue; + float rank = ci->state.state!=CS_SPECTATOR ? ci->state.effectiveness/max(ci->state.timeplayed, 1) : -1; + if(!best || rank > bestrank) { best = ci; bestrank = rank; } + } + return best; + } + + VAR(persistteams, 0, 0, 1); + + void autoteam() + { + static const char * const teamnames[2] = {"good", "evil"}; + vector<clientinfo *> team[2]; + float teamrank[2] = {0, 0}; + for(int round = 0, remaining = clients.length(); remaining>=0; round++) + { + int first = round&1, second = (round+1)&1, selected = 0; + while(teamrank[first] <= teamrank[second]) + { + float rank; + clientinfo *ci = choosebestclient(rank); + if(!ci) break; + if(selected && rank<=0) break; + ci->state.timeplayed = -1; + team[first].add(ci); + if(rank>0) teamrank[first] += rank; + selected++; + if(rank<=0) break; + } + if(!selected) break; + remaining -= selected; + } + loopi(sizeof(team)/sizeof(team[0])) + { + addteaminfo(teamnames[i]); + loopvj(team[i]) + { + clientinfo *ci = team[i][j]; + if(!strcmp(ci->team, teamnames[i])) continue; + if(persistteams && ci->team[0]) + { + addteaminfo(ci->team); + continue; + } + copystring(ci->team, teamnames[i], MAXTEAMLEN+1); + sendf(-1, 1, "riisi", N_SETTEAM, ci->clientnum, teamnames[i], -1); + } + } + } + + struct teamrank + { + const char *name; + float rank; + int clients; + + teamrank(const char *name) : name(name), rank(0), clients(0) {} + }; + + const char *chooseworstteam(const char *suggest = NULL, clientinfo *exclude = NULL) + { + teamrank teamranks[2] = { teamrank("good"), teamrank("evil") }; + const int numteams = sizeof(teamranks)/sizeof(teamranks[0]); + loopv(clients) + { + clientinfo *ci = clients[i]; + if(ci==exclude || ci->state.aitype!=AI_NONE || ci->state.state==CS_SPECTATOR || !ci->team[0]) continue; + ci->state.timeplayed += lastmillis - ci->state.lasttimeplayed; + ci->state.lasttimeplayed = lastmillis; + + loopj(numteams) if(!strcmp(ci->team, teamranks[j].name)) + { + teamrank &ts = teamranks[j]; + ts.rank += ci->state.effectiveness/max(ci->state.timeplayed, 1); + ts.clients++; + break; + } + } + teamrank *worst = &teamranks[numteams-1]; + loopi(numteams-1) + { + teamrank &ts = teamranks[i]; + if(ts.rank < worst->rank || (ts.rank == worst->rank && ts.clients < worst->clients)) worst = &ts; + } + return worst->name; + } + + void prunedemos(int extra = 0) + { + int n = clamp(demos.length() + extra - maxdemos, 0, demos.length()); + if(n <= 0) return; + loopi(n) delete[] demos[i].data; + demos.remove(0, n); + } + + void adddemo() + { + if(!demotmp) return; + int len = (int)min(demotmp->size(), stream::offset((maxdemosize<<20) + 0x10000)); + demofile &d = demos.add(); + time_t t = time(NULL); + char *timestr = ctime(&t), *trim = timestr + strlen(timestr); + while(trim>timestr && iscubespace(*--trim)) *trim = '\0'; + formatstring(d.info, "%s: %s, %s, %.2f%s", timestr, modename(gamemode), smapname, len > 1024*1024 ? len/(1024*1024.f) : len/1024.0f, len > 1024*1024 ? "MB" : "kB"); + sendservmsgf("demo \"%s\" recorded", d.info); + d.data = new uchar[len]; + d.len = len; + demotmp->seek(0, SEEK_SET); + demotmp->read(d.data, len); + DELETEP(demotmp); + } + + void enddemorecord() + { + if(!demorecord) return; + + DELETEP(demorecord); + + if(!demotmp) return; + if(!maxdemos || !maxdemosize) { DELETEP(demotmp); return; } + + prunedemos(1); + adddemo(); + } + + void writedemo(int chan, void *data, int len) + { + if(!demorecord) return; + int stamp[3] = { gamemillis, chan, len }; + lilswap(stamp, 3); + demorecord->write(stamp, sizeof(stamp)); + demorecord->write(data, len); + if(demorecord->rawtell() >= (maxdemosize<<20)) enddemorecord(); + } + + void recordpacket(int chan, void *data, int len) + { + writedemo(chan, data, len); + } + + int welcomepacket(packetbuf &p, clientinfo *ci); + void sendwelcome(clientinfo *ci); + + void setupdemorecord() + { + if(!m_mp(gamemode) || m_edit) return; + + demotmp = opentempfile("demorecord", "w+b"); + if(!demotmp) return; + + stream *f = opengzfile(NULL, "wb", demotmp); + if(!f) { DELETEP(demotmp); return; } + + sendservmsg("recording demo"); + + demorecord = f; + + demoheader hdr; + memcpy(hdr.magic, DEMO_MAGIC, sizeof(hdr.magic)); + hdr.version = DEMO_VERSION; + hdr.protocol = PROTOCOL_VERSION; + lilswap(&hdr.version, 2); + demorecord->write(&hdr, sizeof(demoheader)); + + packetbuf p(MAXTRANS, ENET_PACKET_FLAG_RELIABLE); + welcomepacket(p, NULL); + writedemo(1, p.buf, p.len); + } + + void listdemos(int cn) + { + packetbuf p(MAXTRANS, ENET_PACKET_FLAG_RELIABLE); + putint(p, N_SENDDEMOLIST); + putint(p, demos.length()); + loopv(demos) sendstring(demos[i].info, p); + sendpacket(cn, 1, p.finalize()); + } + + void cleardemos(int n) + { + if(!n) + { + loopv(demos) delete[] demos[i].data; + demos.shrink(0); + sendservmsg("cleared all demos"); + } + else if(demos.inrange(n-1)) + { + delete[] demos[n-1].data; + demos.remove(n-1); + sendservmsgf("cleared demo %d", n); + } + } + + static void freegetmap(ENetPacket *packet) + { + loopv(clients) + { + clientinfo *ci = clients[i]; + if(ci->getmap == packet) ci->getmap = NULL; + } + } + + static void freegetdemo(ENetPacket *packet) + { + loopv(clients) + { + clientinfo *ci = clients[i]; + if(ci->getdemo == packet) ci->getdemo = NULL; + } + } + + void senddemo(clientinfo *ci, int num, int tag) + { + if(ci->getdemo) return; + if(!num) num = demos.length(); + if(!demos.inrange(num-1)) return; + demofile &d = demos[num-1]; + if((ci->getdemo = sendf(ci->clientnum, 2, "riim", N_SENDDEMO, tag, d.len, d.data))) + ci->getdemo->freeCallback = freegetdemo; + } + + void enddemoplayback() + { + if(!demoplayback) return; + DELETEP(demoplayback); + + loopv(clients) sendf(clients[i]->clientnum, 1, "ri3", N_DEMOPLAYBACK, 0, clients[i]->clientnum); + + sendservmsg("demo playback finished"); + + loopv(clients) sendwelcome(clients[i]); + } + + SVARP(demodir, "demo"); + + const char *getdemofile(const char *file, bool init) + { + if(!demodir[0]) return NULL; + static string buf; + copystring(buf, demodir); + int dirlen = strlen(buf); + if(buf[dirlen] != '/' && buf[dirlen] != '\\' && dirlen+1 < (int)sizeof(buf)) { buf[dirlen++] = '/'; buf[dirlen] = '\0'; } + if(init) + { + const char *dir = findfile(buf, "w"); + if(!fileexists(dir, "w")) createdir(dir); + } + concatstring(buf, file); + return buf; + } + + void setupdemoplayback() + { + if(demoplayback) return; + demoheader hdr; + string msg; + msg[0] = '\0'; + string file; + copystring(file, smapname); + int len = strlen(file); + if(len < 4 || strcasecmp(&file[len-4], ".dmo")) concatstring(file, ".dmo"); + if(const char *buf = getdemofile(file, false)) demoplayback = opengzfile(buf, "rb"); + if(!demoplayback) demoplayback = opengzfile(file, "rb"); + if(!demoplayback) formatstring(msg, "could not read demo \"%s\"", file); + else if(demoplayback->read(&hdr, sizeof(demoheader))!=sizeof(demoheader) || memcmp(hdr.magic, DEMO_MAGIC, sizeof(hdr.magic))) + formatstring(msg, "\"%s\" is not a demo file", file); + else + { + lilswap(&hdr.version, 2); + if(hdr.version!=DEMO_VERSION) formatstring(msg, "demo \"%s\" requires an %s version of Cube 2: Sauerbraten", file, hdr.version<DEMO_VERSION ? "older" : "newer"); + else if(hdr.protocol!=PROTOCOL_VERSION) formatstring(msg, "demo \"%s\" requires an %s version of Cube 2: Sauerbraten", file, hdr.protocol<PROTOCOL_VERSION ? "older" : "newer"); + } + if(msg[0]) + { + DELETEP(demoplayback); + sendservmsg(msg); + return; + } + + sendservmsgf("playing demo \"%s\"", file); + + sendf(-1, 1, "ri3", N_DEMOPLAYBACK, 1, -1); + + if(demoplayback->read(&nextplayback, sizeof(nextplayback))!=sizeof(nextplayback)) + { + enddemoplayback(); + return; + } + lilswap(&nextplayback, 1); + } + + void readdemo() + { + if(!demoplayback) return; + while(gamemillis>=nextplayback) + { + int chan, len; + if(demoplayback->read(&chan, sizeof(chan))!=sizeof(chan) || + demoplayback->read(&len, sizeof(len))!=sizeof(len)) + { + enddemoplayback(); + return; + } + lilswap(&chan, 1); + lilswap(&len, 1); + ENetPacket *packet = enet_packet_create(NULL, len+1, 0); + if(!packet || demoplayback->read(packet->data+1, len)!=size_t(len)) + { + if(packet) enet_packet_destroy(packet); + enddemoplayback(); + return; + } + packet->data[0] = N_DEMOPACKET; + sendpacket(-1, chan, packet); + if(!packet->referenceCount) enet_packet_destroy(packet); + if(!demoplayback) break; + if(demoplayback->read(&nextplayback, sizeof(nextplayback))!=sizeof(nextplayback)) + { + enddemoplayback(); + return; + } + lilswap(&nextplayback, 1); + } + } + + void timeupdate(int secs) + { + if(!demoplayback) return; + if(secs <= 0) interm = -1; + else gamelimit = max(gamelimit, nextplayback + secs*1000); + } + + void seekdemo(char *t) + { + if(!demoplayback) return; + bool rev = *t == '-'; + if(rev) t++; + int mins = strtoul(t, &t, 10), secs = 0, millis = 0; + if(*t == ':') secs = strtoul(t+1, &t, 10); + else { secs = mins; mins = 0; } + if(*t == '.') millis = strtoul(t+1, &t, 10); + int offset = max(millis + (mins*60 + secs)*1000, 0), prevmillis = gamemillis; + if(rev) while(gamelimit - offset > gamemillis) + { + gamemillis = gamelimit - offset; + readdemo(); + } + else if(offset > gamemillis) + { + gamemillis = offset; + readdemo(); + } + if(gamemillis > prevmillis) + { + if(!interm) sendf(-1, 1, "ri2", N_TIMEUP, max((gamelimit - gamemillis)/1000, 1)); + } + } + + ICOMMAND(seekdemo, "sN$", (char *t, int *numargs, ident *id), + { + if(*numargs > 0) seekdemo(t); + else + { + int secs = gamemillis/1000; + defformatstring(str, "%d:%02d.%03d", secs/60, secs%60, gamemillis%1000); + if(*numargs < 0) result(str); + else printsvar(id, str); + } + }); + + void stopdemo() + { + if(m_demo) enddemoplayback(); + else enddemorecord(); + } + + void pausegame(bool val, clientinfo *ci = NULL) + { + if(gamepaused==val) return; + gamepaused = val; + sendf(-1, 1, "riii", N_PAUSEGAME, gamepaused ? 1 : 0, ci ? ci->clientnum : -1); + } + + void checkpausegame() + { + if(!gamepaused) return; + int admins = 0; + loopv(clients) if(clients[i]->privilege >= (restrictpausegame ? PRIV_ADMIN : PRIV_MASTER) || clients[i]->local) admins++; + if(!admins) pausegame(false); + } + + void forcepaused(bool paused) + { + pausegame(paused); + } + + bool ispaused() { return gamepaused; } + + void changegamespeed(int val, clientinfo *ci = NULL) + { + val = clamp(val, 10, 1000); + if(gamespeed==val) return; + gamespeed = val; + sendf(-1, 1, "riii", N_GAMESPEED, gamespeed, ci ? ci->clientnum : -1); + } + + void forcegamespeed(int speed) + { + changegamespeed(speed); + } + + int scaletime(int t) { return t*gamespeed; } + + SVAR(serverauth, ""); + + struct userkey + { + char *name; + char *desc; + + userkey() : name(NULL), desc(NULL) {} + userkey(char *name, char *desc) : name(name), desc(desc) {} + }; + + static inline uint hthash(const userkey &k) { return ::hthash(k.name); } + static inline bool htcmp(const userkey &x, const userkey &y) { return !strcmp(x.name, y.name) && !strcmp(x.desc, y.desc); } + + struct userinfo : userkey + { + void *pubkey; + int privilege; + + userinfo() : pubkey(NULL), privilege(PRIV_NONE) {} + ~userinfo() { delete[] name; delete[] desc; if(pubkey) freepubkey(pubkey); } + }; + hashset<userinfo> users; + + void adduser(char *name, char *desc, char *pubkey, char *priv) + { + userkey key(name, desc); + userinfo &u = users[key]; + if(u.pubkey) { freepubkey(u.pubkey); u.pubkey = NULL; } + if(!u.name) u.name = newstring(name); + if(!u.desc) u.desc = newstring(desc); + u.pubkey = parsepubkey(pubkey); + switch(priv[0]) + { + case 'a': case 'A': u.privilege = PRIV_ADMIN; break; + case 'm': case 'M': default: u.privilege = PRIV_AUTH; break; + case 'n': case 'N': u.privilege = PRIV_NONE; break; + } + } + COMMAND(adduser, "ssss"); + + void clearusers() + { + users.clear(); + } + COMMAND(clearusers, ""); + + void hashpassword(int cn, int sessionid, const char *pwd, char *result, int maxlen) + { + char buf[2*sizeof(string)]; + formatstring(buf, "%d %d ", cn, sessionid); + concatstring(buf, pwd, sizeof(buf)); + if(!hashstring(buf, result, maxlen)) *result = '\0'; + } + + bool checkpassword(clientinfo *ci, const char *wanted, const char *given) + { + string hash; + hashpassword(ci->clientnum, ci->sessionid, wanted, hash, sizeof(hash)); + return !strcmp(hash, given); + } + + void revokemaster(clientinfo *ci) + { + ci->privilege = PRIV_NONE; + if(ci->state.state==CS_SPECTATOR && !ci->local) aiman::removeai(ci); + } + + extern void connected(clientinfo *ci); + + bool setmaster(clientinfo *ci, bool val, const char *pass = "", const char *authname = NULL, const char *authdesc = NULL, int authpriv = PRIV_MASTER, bool force = false, bool trial = false) + { + if(authname && !val) return false; + const char *name = ""; + if(val) + { + bool haspass = adminpass[0] && checkpassword(ci, adminpass, pass); + int wantpriv = ci->local || haspass ? PRIV_ADMIN : authpriv; + if(wantpriv <= ci->privilege) return true; + else if(wantpriv <= PRIV_MASTER && !force) + { + if(ci->state.state==CS_SPECTATOR) + { + sendf(ci->clientnum, 1, "ris", N_SERVMSG, "Spectators may not claim master."); + return false; + } + loopv(clients) if(ci!=clients[i] && clients[i]->privilege) + { + sendf(ci->clientnum, 1, "ris", N_SERVMSG, "Master is already claimed."); + return false; + } + if(!authname && !(mastermask&MM_AUTOAPPROVE) && !ci->privilege && !ci->local) + { + sendf(ci->clientnum, 1, "ris", N_SERVMSG, "This server requires you to use the \"/auth\" command to claim master."); + return false; + } + } + if(trial) return true; + ci->privilege = wantpriv; + name = privname(ci->privilege); + } + else + { + if(!ci->privilege) return false; + if(trial) return true; + name = privname(ci->privilege); + revokemaster(ci); + } + bool hasmaster = false; + loopv(clients) if(clients[i]->local || clients[i]->privilege >= PRIV_MASTER) hasmaster = true; + if(!hasmaster) + { + mastermode = MM_OPEN; + allowedips.shrink(0); + } + string msg; + if(val && authname) + { + if(authdesc && authdesc[0]) formatstring(msg, "%s claimed %s as '\fs\f5%s\fr' [\fs\f0%s\fr]", colorname(ci), name, authname, authdesc); + else formatstring(msg, "%s claimed %s as '\fs\f5%s\fr'", colorname(ci), name, authname); + } + else formatstring(msg, "%s %s %s", colorname(ci), val ? "claimed" : "relinquished", name); + packetbuf p(MAXTRANS, ENET_PACKET_FLAG_RELIABLE); + putint(p, N_SERVMSG); + sendstring(msg, p); + putint(p, N_CURRENTMASTER); + putint(p, mastermode); + loopv(clients) if(clients[i]->privilege >= PRIV_MASTER) + { + putint(p, clients[i]->clientnum); + putint(p, clients[i]->privilege); + } + putint(p, -1); + sendpacket(-1, 1, p.finalize()); + checkpausegame(); + return true; + } + + bool trykick(clientinfo *ci, int victim, const char *reason = NULL, const char *authname = NULL, const char *authdesc = NULL, int authpriv = PRIV_NONE, bool trial = false) + { + int priv = ci->privilege; + if(authname) + { + if(priv >= authpriv || ci->local) authname = authdesc = NULL; + else priv = authpriv; + } + if((priv || ci->local) && ci->clientnum!=victim) + { + clientinfo *vinfo = (clientinfo *)getclientinfo(victim); + if(vinfo && vinfo->connected && (priv >= vinfo->privilege || ci->local) && vinfo->privilege < PRIV_ADMIN && !vinfo->local) + { + if(trial) return true; + string kicker; + if(authname) + { + if(authdesc && authdesc[0]) formatstring(kicker, "%s as '\fs\f5%s\fr' [\fs\f0%s\fr]", colorname(ci), authname, authdesc); + else formatstring(kicker, "%s as '\fs\f5%s\fr'", colorname(ci), authname); + } + else copystring(kicker, colorname(ci)); + if(reason && reason[0]) sendservmsgf("%s kicked %s because: %s", kicker, colorname(vinfo), reason); + else sendservmsgf("%s kicked %s", kicker, colorname(vinfo)); + uint ip = getclientip(victim); + addban(ip, 4*60*60000); + kickclients(ip, ci, priv); + } + } + return false; + } + + savedscore *findscore(clientinfo *ci, bool insert) + { + uint ip = getclientip(ci->clientnum); + if(!ip && !ci->local) return 0; + if(!insert) + { + loopv(clients) + { + clientinfo *oi = clients[i]; + if(oi->clientnum != ci->clientnum && getclientip(oi->clientnum) == ip && !strcmp(oi->name, ci->name)) + { + oi->state.timeplayed += lastmillis - oi->state.lasttimeplayed; + oi->state.lasttimeplayed = lastmillis; + static savedscore curscore; + curscore.save(oi->state); + return &curscore; + } + } + } + loopv(scores) + { + savedscore &sc = scores[i]; + if(sc.ip == ip && !strcmp(sc.name, ci->name)) return ≻ + } + if(!insert) return 0; + savedscore &sc = scores.add(); + sc.ip = ip; + copystring(sc.name, ci->name); + return ≻ + } + + void savescore(clientinfo *ci) + { + savedscore *sc = findscore(ci, true); + if(sc) sc->save(ci->state); + } + + static struct msgfilter + { + uchar msgmask[NUMMSG]; + + msgfilter(int msg, ...) + { + memset(msgmask, 0, sizeof(msgmask)); + va_list msgs; + va_start(msgs, msg); + for(uchar val = 1; msg < NUMMSG; msg = va_arg(msgs, int)) + { + if(msg < 0) val = uchar(-msg); + else msgmask[msg] = val; + } + va_end(msgs); + } + + uchar operator[](int msg) const { return msg >= 0 && msg < NUMMSG ? msgmask[msg] : 0; } + } msgfilter(-1, N_CONNECT, N_SERVINFO, N_INITCLIENT, N_WELCOME, N_MAPCHANGE, N_SERVMSG, N_DAMAGE, N_HITPUSH, N_SHOTFX, N_EXPLODEFX, N_DIED, + N_SPAWNSTATE, N_FORCEDEATH, N_TEAMINFO, N_ITEMACC, N_ITEMSPAWN, N_TIMEUP, N_CDIS, N_CURRENTMASTER, N_PONG, N_RESUME, + N_ANNOUNCE, N_SENDDEMOLIST, N_SENDDEMO, N_DEMOPLAYBACK, N_SENDMAP, + N_CLIENT, N_AUTHCHAL, N_INITAI, N_EXPIRETOKENS, N_DROPTOKENS, N_STEALTOKENS, N_DEMOPACKET, -2, N_REMIP, + N_NEWMAP, N_GETMAP, N_SENDMAP, N_CLIPBOARD, -3, N_EDITENT, N_EDITF, N_EDITT, N_EDITM, N_FLIP, N_COPY, N_PASTE, N_ROTATE, N_REPLACE, + N_DELCUBE, N_EDITVAR, N_EDITVSLOT, N_UNDO, N_REDO, -4, N_POS, NUMMSG), + connectfilter(-1, N_CONNECT, -2, N_AUTHANS, -3, N_PING, NUMMSG); + + int checktype(int type, clientinfo *ci) + { + if(ci) + { + if(!ci->connected) switch(connectfilter[type]) + { + // allow only before authconnect + case 1: return !ci->connectauth ? type : -1; + // allow only during authconnect + case 2: return ci->connectauth ? type : -1; + // always allow + case 3: return type; + // never allow + default: return -1; + } + if(ci->local) return type; + } + switch(msgfilter[type]) + { + // server-only messages + case 1: return ci ? -1 : type; + // only allowed in coop-edit + case 2: if(m_edit) break; return -1; + // only allowed in coop-edit, no overflow check + case 3: return m_edit ? type : -1; + // no overflow check + case 4: return type; + } + if(ci && ++ci->overflow >= 200) return -2; + return type; + } + + struct worldstate + { + int uses, len; + uchar *data; + + worldstate() : uses(0), len(0), data(NULL) {} + + void setup(int n) { len = n; data = new uchar[n]; } + void cleanup() { DELETEA(data); len = 0; } + bool contains(const uchar *p) const { return p >= data && p < &data[len]; } + }; + vector<worldstate> worldstates; + bool reliablemessages = false; + + void cleanworldstate(ENetPacket *packet) + { + loopv(worldstates) + { + worldstate &ws = worldstates[i]; + if(!ws.contains(packet->data)) continue; + ws.uses--; + if(ws.uses <= 0) + { + ws.cleanup(); + worldstates.removeunordered(i); + } + break; + } + } + + void flushclientposition(clientinfo &ci) + { + if(ci.position.empty() || (!hasnonlocalclients() && !demorecord)) return; + packetbuf p(ci.position.length(), 0); + p.put(ci.position.getbuf(), ci.position.length()); + ci.position.setsize(0); + sendpacket(-1, 0, p.finalize(), ci.ownernum); + } + + static void sendpositions(worldstate &ws, ucharbuf &wsbuf) + { + if(wsbuf.empty()) return; + int wslen = wsbuf.length(); + recordpacket(0, wsbuf.buf, wslen); + wsbuf.put(wsbuf.buf, wslen); + loopv(clients) + { + clientinfo &ci = *clients[i]; + if(ci.state.aitype != AI_NONE) continue; + uchar *data = wsbuf.buf; + int size = wslen; + if(ci.wsdata >= wsbuf.buf) { data = ci.wsdata + ci.wslen; size -= ci.wslen; } + if(size <= 0) continue; + ENetPacket *packet = enet_packet_create(data, size, ENET_PACKET_FLAG_NO_ALLOCATE); + sendpacket(ci.clientnum, 0, packet); + if(packet->referenceCount) { ws.uses++; packet->freeCallback = cleanworldstate; } + else enet_packet_destroy(packet); + } + wsbuf.offset(wsbuf.length()); + } + + static inline void addposition(worldstate &ws, ucharbuf &wsbuf, int mtu, clientinfo &bi, clientinfo &ci) + { + if(bi.position.empty()) return; + if(wsbuf.length() + bi.position.length() > mtu) sendpositions(ws, wsbuf); + int offset = wsbuf.length(); + wsbuf.put(bi.position.getbuf(), bi.position.length()); + bi.position.setsize(0); + int len = wsbuf.length() - offset; + if(ci.wsdata < wsbuf.buf) { ci.wsdata = &wsbuf.buf[offset]; ci.wslen = len; } + else ci.wslen += len; + } + + static void sendmessages(worldstate &ws, ucharbuf &wsbuf) + { + if(wsbuf.empty()) return; + int wslen = wsbuf.length(); + recordpacket(1, wsbuf.buf, wslen); + wsbuf.put(wsbuf.buf, wslen); + loopv(clients) + { + clientinfo &ci = *clients[i]; + if(ci.state.aitype != AI_NONE) continue; + uchar *data = wsbuf.buf; + int size = wslen; + if(ci.wsdata >= wsbuf.buf) { data = ci.wsdata + ci.wslen; size -= ci.wslen; } + if(size <= 0) continue; + ENetPacket *packet = enet_packet_create(data, size, (reliablemessages ? ENET_PACKET_FLAG_RELIABLE : 0) | ENET_PACKET_FLAG_NO_ALLOCATE); + sendpacket(ci.clientnum, 1, packet); + if(packet->referenceCount) { ws.uses++; packet->freeCallback = cleanworldstate; } + else enet_packet_destroy(packet); + } + wsbuf.offset(wsbuf.length()); + } + + static inline void addmessages(worldstate &ws, ucharbuf &wsbuf, int mtu, clientinfo &bi, clientinfo &ci) + { + if(bi.messages.empty()) return; + if(wsbuf.length() + 10 + bi.messages.length() > mtu) sendmessages(ws, wsbuf); + int offset = wsbuf.length(); + putint(wsbuf, N_CLIENT); + putint(wsbuf, bi.clientnum); + putuint(wsbuf, bi.messages.length()); + wsbuf.put(bi.messages.getbuf(), bi.messages.length()); + bi.messages.setsize(0); + int len = wsbuf.length() - offset; + if(ci.wsdata < wsbuf.buf) { ci.wsdata = &wsbuf.buf[offset]; ci.wslen = len; } + else ci.wslen += len; + } + + bool buildworldstate() + { + int wsmax = 0; + loopv(clients) + { + clientinfo &ci = *clients[i]; + ci.overflow = 0; + ci.wsdata = NULL; + wsmax += ci.position.length(); + if(ci.messages.length()) wsmax += 10 + ci.messages.length(); + } + if(wsmax <= 0) + { + reliablemessages = false; + return false; + } + worldstate &ws = worldstates.add(); + ws.setup(2*wsmax); + int mtu = getservermtu() - 100; + if(mtu <= 0) mtu = ws.len; + ucharbuf wsbuf(ws.data, ws.len); + loopv(clients) + { + clientinfo &ci = *clients[i]; + if(ci.state.aitype != AI_NONE) continue; + addposition(ws, wsbuf, mtu, ci, ci); + loopvj(ci.bots) addposition(ws, wsbuf, mtu, *ci.bots[j], ci); + } + sendpositions(ws, wsbuf); + loopv(clients) + { + clientinfo &ci = *clients[i]; + if(ci.state.aitype != AI_NONE) continue; + addmessages(ws, wsbuf, mtu, ci, ci); + loopvj(ci.bots) addmessages(ws, wsbuf, mtu, *ci.bots[j], ci); + } + sendmessages(ws, wsbuf); + reliablemessages = false; + if(ws.uses) return true; + ws.cleanup(); + worldstates.drop(); + return false; + } + + bool sendpackets(bool force) + { + if(clients.empty() || (!hasnonlocalclients() && !demorecord)) return false; + enet_uint32 curtime = enet_time_get()-lastsend; + if(curtime<33 && !force) return false; + bool flush = buildworldstate(); + lastsend += curtime - (curtime%33); + return flush; + } + + template<class T> + void sendstate(gamestate &gs, T &p) + { + putint(p, gs.lifesequence); + putint(p, gs.health); + putint(p, gs.maxhealth); + putint(p, gs.armour); + putint(p, gs.maxarmour); + putint(p, gs.armourtype); + putint(p, gs.gunselect); + loopi(GUN_PISTOL-GUN_SG+1) putint(p, gs.ammo[GUN_SG+i]); + } + + void spawnstate(clientinfo *ci) + { + gamestate &gs = ci->state; + gs.spawnstate(gamemode); + gs.lifesequence = (gs.lifesequence + 1)&0x7F; + } + + void sendspawn(clientinfo *ci) + { + gamestate &gs = ci->state; + spawnstate(ci); + sendf(ci->ownernum, 1, "rii8v", N_SPAWNSTATE, ci->clientnum, gs.lifesequence, + gs.health, gs.maxhealth, + gs.armour, gs.maxarmour, gs.armourtype, + gs.gunselect, GUN_PISTOL-GUN_SG+1, &gs.ammo[GUN_SG]); + gs.lastspawn = gamemillis; + } + + void sendwelcome(clientinfo *ci) + { + packetbuf p(MAXTRANS, ENET_PACKET_FLAG_RELIABLE); + int chan = welcomepacket(p, ci); + sendpacket(ci->clientnum, chan, p.finalize()); + } + + void putinitclient(clientinfo *ci, packetbuf &p) + { + if(ci->state.aitype != AI_NONE) + { + putint(p, N_INITAI); + putint(p, ci->clientnum); + putint(p, ci->ownernum); + putint(p, ci->state.aitype); + putint(p, ci->state.skill); + putint(p, ci->playermodel); + sendstring(ci->name, p); + sendstring(ci->team, p); + } + else + { + putint(p, N_INITCLIENT); + putint(p, ci->clientnum); + sendstring(ci->name, p); + sendstring(ci->team, p); + putint(p, ci->playermodel); + } + } + + void welcomeinitclient(packetbuf &p, int exclude = -1) + { + loopv(clients) + { + clientinfo *ci = clients[i]; + if(!ci->connected || ci->clientnum == exclude) continue; + + putinitclient(ci, p); + } + } + + bool hasmap(clientinfo *ci) + { + return (m_edit && (clients.length() > 0 || ci->local)) || + (smapname[0] && (!m_timed || gamemillis < gamelimit || (ci->state.state==CS_SPECTATOR && !ci->privilege && !ci->local) || numclients(ci->clientnum, true, true, true))); + } + + int welcomepacket(packetbuf &p, clientinfo *ci) + { + putint(p, N_WELCOME); + putint(p, N_MAPCHANGE); + sendstring(smapname, p); + putint(p, gamemode); + putint(p, notgotitems ? 1 : 0); + if(!ci || (m_timed && smapname[0])) + { + putint(p, N_TIMEUP); + putint(p, gamemillis < gamelimit && !interm ? max((gamelimit - gamemillis)/1000, 1) : 0); + } + if(!notgotitems) + { + putint(p, N_ITEMLIST); + loopv(sents) if(sents[i].spawned) + { + putint(p, i); + putint(p, sents[i].type); + } + putint(p, -1); + } + bool hasmaster = false; + if(mastermode != MM_OPEN) + { + putint(p, N_CURRENTMASTER); + putint(p, mastermode); + hasmaster = true; + } + loopv(clients) if(clients[i]->privilege >= PRIV_MASTER) + { + if(!hasmaster) + { + putint(p, N_CURRENTMASTER); + putint(p, mastermode); + hasmaster = true; + } + putint(p, clients[i]->clientnum); + putint(p, clients[i]->privilege); + } + if(hasmaster) putint(p, -1); + if(gamepaused) + { + putint(p, N_PAUSEGAME); + putint(p, 1); + putint(p, -1); + } + if(gamespeed != 100) + { + putint(p, N_GAMESPEED); + putint(p, gamespeed); + putint(p, -1); + } + if(m_teammode) + { + putint(p, N_TEAMINFO); + enumerate(teaminfos, teaminfo, t, + if(t.frags) { sendstring(t.team, p); putint(p, t.frags); } + ); + sendstring("", p); + } + if(ci) + { + putint(p, N_SETTEAM); + putint(p, ci->clientnum); + sendstring(ci->team, p); + putint(p, -1); + } + if(ci && (m_demo || m_mp(gamemode)) && ci->state.state!=CS_SPECTATOR) + { + gamestate &gs = ci->state; + spawnstate(ci); + putint(p, N_SPAWNSTATE); + putint(p, ci->clientnum); + sendstate(gs, p); + gs.lastspawn = gamemillis; + } + if(ci && ci->state.state==CS_SPECTATOR) + { + putint(p, N_SPECTATOR); + putint(p, ci->clientnum); + putint(p, 1); + sendf(-1, 1, "ri3x", N_SPECTATOR, ci->clientnum, 1, ci->clientnum); + } + if(!ci || clients.length()>1) + { + putint(p, N_RESUME); + loopv(clients) + { + clientinfo *oi = clients[i]; + if(ci && oi->clientnum==ci->clientnum) continue; + putint(p, oi->clientnum); + putint(p, oi->state.state); + putint(p, oi->state.frags); + putint(p, oi->state.flags); + putint(p, oi->state.deaths); + putint(p, oi->state.quadmillis); + sendstate(oi->state, p); + } + putint(p, -1); + welcomeinitclient(p, ci ? ci->clientnum : -1); + } + return 1; + } + + bool restorescore(clientinfo *ci) + { + //if(ci->local) return false; + savedscore *sc = findscore(ci, false); + if(sc) + { + sc->restore(ci->state); + return true; + } + return false; + } + + void sendresume(clientinfo *ci) + { + gamestate &gs = ci->state; + sendf(-1, 1, "ri3i5i6vi", N_RESUME, ci->clientnum, gs.state, + gs.frags, gs.flags, gs.deaths, gs.quadmillis, + gs.lifesequence, + gs.health, gs.maxhealth, + gs.armour, gs.maxarmour, gs.armourtype, + gs.gunselect, GUN_PISTOL-GUN_SG+1, &gs.ammo[GUN_SG], -1); + } + + void sendinitclient(clientinfo *ci) + { + packetbuf p(MAXTRANS, ENET_PACKET_FLAG_RELIABLE); + putinitclient(ci, p); + sendpacket(-1, 1, p.finalize(), ci->clientnum); + } + + void loaditems() + { + resetitems(); + notgotitems = true; + if(m_edit || !loadents(smapname, ments, &mcrc)) + return; + loopv(ments) if(canspawnitem(ments[i].type)) + { + server_entity se = { NOTUSED, 0, false }; + while(sents.length()<=i) sents.add(se); + sents[i].type = ments[i].type; + if(m_mp(gamemode) && delayspawn(sents[i].type)) sents[i].spawntime = spawntime(sents[i].type); + else sents[i].spawned = true; + } + notgotitems = false; + } + + void changemap(const char *s, int mode) + { + stopdemo(); + pausegame(false); + changegamespeed(100); + aiman::clearai(); + + gamemode = mode; + gamemillis = 0; + gamelimit = 10*60000; + interm = 0; + nextexceeded = 0; + copystring(smapname, s); + loaditems(); + scores.shrink(0); + shouldcheckteamkills = false; + teamkills.shrink(0); + loopv(clients) + { + clientinfo *ci = clients[i]; + ci->state.timeplayed += lastmillis - ci->state.lasttimeplayed; + } + + if(!m_mp(gamemode)) kicknonlocalclients(DISC_LOCAL); + + sendf(-1, 1, "risii", N_MAPCHANGE, smapname, gamemode, 1); + + clearteaminfo(); + if(m_teammode) autoteam(); + + if(m_timed && smapname[0]) sendf(-1, 1, "ri2", N_TIMEUP, gamemillis < gamelimit && !interm ? max((gamelimit - gamemillis)/1000, 1) : 0); + loopv(clients) + { + clientinfo *ci = clients[i]; + ci->mapchange(); + ci->state.lasttimeplayed = lastmillis; + if(m_mp(gamemode) && ci->state.state!=CS_SPECTATOR) sendspawn(ci); + } + + aiman::changemap(); + + if(m_demo) + { + if(clients.length()) setupdemoplayback(); + } + else + { + if(demonextmatch) setupdemorecord(); + demonextmatch = autorecorddemo!=0; + } + } + + void rotatemap(bool next) + { + if(!maprotations.inrange(curmaprotation)) + { + changemap("", 1); + return; + } + if(next) + { + curmaprotation = findmaprotation(gamemode, smapname); + if(curmaprotation >= 0) nextmaprotation(); + else curmaprotation = smapname[0] ? max(findmaprotation(gamemode, ""), 0) : 0; + } + maprotation &rot = maprotations[curmaprotation]; + changemap(rot.map, rot.findmode(gamemode)); + } + + struct votecount + { + char *map; + int mode, count; + votecount() {} + votecount(char *s, int n) : map(s), mode(n), count(0) {} + }; + + void checkvotes(bool force = false) + { + vector<votecount> votes; + int maxvotes = 0; + loopv(clients) + { + clientinfo *oi = clients[i]; + if(oi->state.state==CS_SPECTATOR && !oi->privilege && !oi->local) continue; + if(oi->state.aitype!=AI_NONE) continue; + maxvotes++; + if(!m_valid(oi->modevote)) continue; + votecount *vc = NULL; + loopvj(votes) if(!strcmp(oi->mapvote, votes[j].map) && oi->modevote==votes[j].mode) + { + vc = &votes[j]; + break; + } + if(!vc) vc = &votes.add(votecount(oi->mapvote, oi->modevote)); + vc->count++; + } + votecount *best = NULL; + loopv(votes) if(!best || votes[i].count > best->count || (votes[i].count == best->count && rnd(2))) best = &votes[i]; + if(force || (best && best->count > maxvotes/2)) + { + sendpackets(true); + if(demorecord) enddemorecord(); + if(best && (best->count > (force ? 1 : maxvotes/2))) + { + sendservmsg(force ? "vote passed by default" : "vote passed by majority"); + changemap(best->map, best->mode); + } + else rotatemap(true); + } + } + + void forcemap(const char *map, int mode) + { + stopdemo(); + if(!map[0] && !m_check(mode, M_EDIT)) + { + int idx = findmaprotation(mode, smapname); + if(idx < 0 && smapname[0]) idx = findmaprotation(mode, ""); + if(idx < 0) return; + map = maprotations[idx].map; + } + if(hasnonlocalclients()) sendservmsgf("local player forced %s on map %s", modename(mode), map[0] ? map : "[new map]"); + changemap(map, mode); + } + + void vote(const char *map, int reqmode, int sender) + { + clientinfo *ci = getinfo(sender); + if(!ci || (ci->state.state==CS_SPECTATOR && !ci->privilege && !ci->local) || (!ci->local && !m_mp(reqmode))) return; + if(!m_valid(reqmode)) return; + if(!map[0] && !m_check(reqmode, M_EDIT)) + { + int idx = findmaprotation(reqmode, smapname); + if(idx < 0 && smapname[0]) idx = findmaprotation(reqmode, ""); + if(idx < 0) return; + map = maprotations[idx].map; + } + if(lockmaprotation && !ci->local && ci->privilege < (lockmaprotation > 1 ? PRIV_ADMIN : PRIV_MASTER) && findmaprotation(reqmode, map) < 0) + { + sendf(sender, 1, "ris", N_SERVMSG, "This server has locked the map rotation."); + return; + } + copystring(ci->mapvote, map); + ci->modevote = reqmode; + if(ci->local || (ci->privilege && mastermode>=MM_VETO)) + { + sendpackets(true); + if(demorecord) enddemorecord(); + if(!ci->local || hasnonlocalclients()) + sendservmsgf("%s forced %s on map %s", colorname(ci), modename(ci->modevote), ci->mapvote[0] ? ci->mapvote : "[new map]"); + changemap(ci->mapvote, ci->modevote); + } + else + { + sendservmsgf("%s suggests %s on map %s (select map to vote)", colorname(ci), modename(reqmode), map[0] ? map : "[new map]"); + checkvotes(); + } + } + + VAR(overtime, 0, 0, 1); + + bool checkovertime() + { + if(!m_timed || !overtime) return false; + const char* topteam = NULL; + int topscore = INT_MIN; + bool tied = false; + if(m_teammode) + { + vector<teamscore> scores; + loopv(clients) + { + clientinfo *ci = clients[i]; + if(ci->state.state==CS_SPECTATOR || !ci->team[0]) continue; + int score = 0; + if(teaminfo *ti = teaminfos.access(ci->team)) score = ti->frags; + if(!topteam || score > topscore) { topteam = ci->team; topscore = score; tied = false; } + else if(score == topscore && strcmp(ci->team, topteam)) tied = true; + } + } + else + { + loopv(clients) + { + clientinfo *ci = clients[i]; + if(ci->state.state==CS_SPECTATOR) continue; + int score = ci->state.frags; + if(score > topscore) { topscore = score; tied = false; } + else if(score == topscore) tied = true; + } + } + if(!tied) return false; + sendservmsg("the game is tied with overtime"); + gamelimit = max(gamemillis, gamelimit) + 2*60000; + sendf(-1, 1, "ri2", N_TIMEUP, max((gamelimit - gamemillis)/1000, 1)); + return true; + } + + void checkintermission(bool force = false) + { + if(gamemillis >= gamelimit && !interm && (force || !checkovertime())) + { + sendf(-1, 1, "ri2", N_TIMEUP, 0); + changegamespeed(100); + interm = gamemillis + 10000; + } + } + + void startintermission() { gamelimit = min(gamelimit, gamemillis); checkintermission(true); } + + void dodamage(clientinfo *target, clientinfo *actor, int damage, int gun, const vec &hitpush = vec(0, 0, 0)) + { + gamestate &ts = target->state; + ts.dodamage(damage); + if(target!=actor && !isteam(target->team, actor->team)) actor->state.damage += damage; + sendf(-1, 1, "ri6", N_DAMAGE, target->clientnum, actor->clientnum, damage, ts.armour, ts.health); + if(target==actor) target->setpushed(); + else if(!hitpush.iszero()) + { + ivec v(vec(hitpush).rescale(DNF)); + sendf(ts.health<=0 ? -1 : target->ownernum, 1, "ri7", N_HITPUSH, target->clientnum, gun, damage, v.x, v.y, v.z); + target->setpushed(); + } + if(ts.health<=0) + { + target->state.deaths++; + int fragvalue = (target==actor || isteam(target->team, actor->team) ? -1 : 1); + actor->state.frags += fragvalue; + if(fragvalue>0) + { + int friends = 0, enemies = 0; // note: friends also includes the fragger + if(m_teammode) loopv(clients) if(strcmp(clients[i]->team, actor->team)) enemies++; else friends++; + else { friends = 1; enemies = clients.length()-1; } + actor->state.effectiveness += fragvalue*friends/float(max(enemies, 1)); + } + teaminfo *t = m_teammode ? teaminfos.access(actor->team) : NULL; + if(t) t->frags += fragvalue; + sendf(-1, 1, "ri5", N_DIED, target->clientnum, actor->clientnum, actor->state.frags, t ? t->frags : 0); + target->position.setsize(0); + ts.state = CS_DEAD; + ts.lastdeath = gamemillis; + if(actor!=target && isteam(actor->team, target->team)) + { + actor->state.teamkills++; + addteamkill(actor, target, 1); + } + ts.deadflush = ts.lastdeath + DEATHMILLIS; + // don't issue respawn yet until DEATHMILLIS has elapsed + // ts.respawn(); + } + } + + void suicide(clientinfo *ci) + { + gamestate &gs = ci->state; + if(gs.state!=CS_ALIVE) return; + int fragvalue = -1; + ci->state.frags += fragvalue; + ci->state.deaths++; + teaminfo *t = m_teammode ? teaminfos.access(ci->team) : NULL; + if(t) t->frags += fragvalue; + sendf(-1, 1, "ri5", N_DIED, ci->clientnum, ci->clientnum, gs.frags, t ? t->frags : 0); + ci->position.setsize(0); + gs.state = CS_DEAD; + gs.lastdeath = gamemillis; + gs.respawn(); + } + + void suicideevent::process(clientinfo *ci) + { + suicide(ci); + } + + void explodeevent::process(clientinfo *ci) + { + gamestate &gs = ci->state; + switch(gun) + { + case GUN_RL: + if(!gs.rockets.remove(id)) return; + break; + + case GUN_GL: + if(!gs.grenades.remove(id)) return; + break; + + default: + return; + } + sendf(-1, 1, "ri4x", N_EXPLODEFX, ci->clientnum, gun, id, ci->ownernum); + loopv(hits) + { + hitinfo &h = hits[i]; + clientinfo *target = getinfo(h.target); + if(!target || target->state.state!=CS_ALIVE || h.lifesequence!=target->state.lifesequence || h.dist<0 || h.dist>guns[gun].exprad) continue; + + bool dup = false; + loopj(i) if(hits[j].target==h.target) { dup = true; break; } + if(dup) continue; + + int damage = guns[gun].damage; + if(gs.quadmillis) damage *= 4; + damage = int(damage*(1-h.dist/EXP_DISTSCALE/guns[gun].exprad)); + if(target==ci) damage /= EXP_SELFDAMDIV; + dodamage(target, ci, damage, gun, h.dir); + } + } + + void shotevent::process(clientinfo *ci) + { + gamestate &gs = ci->state; + int wait = millis - gs.lastshot; + if(!gs.isalive(gamemillis) || + wait<gs.gunwait || + gun<GUN_FIST || gun>GUN_PISTOL || + gs.ammo[gun]<=0 || (guns[gun].range && from.dist(to) > guns[gun].range + 1)) + return; + if(gun!=GUN_FIST) gs.ammo[gun]--; + gs.lastshot = millis; + gs.gunwait = guns[gun].attackdelay; + sendf(-1, 1, "rii9x", N_SHOTFX, ci->clientnum, gun, id, + int(from.x*DMF), int(from.y*DMF), int(from.z*DMF), + int(to.x*DMF), int(to.y*DMF), int(to.z*DMF), + ci->ownernum); + gs.shotdamage += guns[gun].damage*(gs.quadmillis ? 4 : 1)*guns[gun].rays; + switch(gun) + { + case GUN_RL: gs.rockets.add(id); break; + case GUN_GL: gs.grenades.add(id); break; + default: + { + int totalrays = 0, maxrays = guns[gun].rays; + loopv(hits) + { + hitinfo &h = hits[i]; + clientinfo *target = getinfo(h.target); + if(!target || target->state.state!=CS_ALIVE || h.lifesequence!=target->state.lifesequence || h.rays<1 || h.dist > guns[gun].range + 1) continue; + + totalrays += h.rays; + if(totalrays>maxrays) continue; + int damage = h.rays*guns[gun].damage; + if(gs.quadmillis) damage *= 4; + dodamage(target, ci, damage, gun, h.dir); + } + break; + } + } + } + + void pickupevent::process(clientinfo *ci) + { + gamestate &gs = ci->state; + if(m_mp(gamemode) && !gs.isalive(gamemillis)) return; + pickup(ent, ci->clientnum); + } + + bool gameevent::flush(clientinfo *ci, int fmillis) + { + process(ci); + return true; + } + + bool timedevent::flush(clientinfo *ci, int fmillis) + { + if(millis > fmillis) return false; + else if(millis >= ci->lastevent) + { + ci->lastevent = millis; + process(ci); + } + return true; + } + + void clearevent(clientinfo *ci) + { + delete ci->events.remove(0); + } + + void flushevents(clientinfo *ci, int millis) + { + while(ci->events.length()) + { + gameevent *ev = ci->events[0]; + if(ev->flush(ci, millis)) clearevent(ci); + else break; + } + } + + void processevents() + { + loopv(clients) + { + clientinfo *ci = clients[i]; + if(curtime>0 && ci->state.quadmillis) ci->state.quadmillis = max(ci->state.quadmillis-curtime, 0); + flushevents(ci, gamemillis); + } + } + + void cleartimedevents(clientinfo *ci) + { + int keep = 0; + loopv(ci->events) + { + if(ci->events[i]->keepable()) + { + if(keep < i) + { + for(int j = keep; j < i; j++) delete ci->events[j]; + ci->events.remove(keep, i - keep); + i = keep; + } + keep = i+1; + continue; + } + } + while(ci->events.length() > keep) delete ci->events.pop(); + ci->timesync = false; + } + + void serverupdate() + { + if(shouldstep && !gamepaused) + { + gamemillis += curtime; + + if(m_demo) readdemo(); + else if(!m_timed || gamemillis < gamelimit) + { + processevents(); + if(curtime) + { + loopv(sents) if(sents[i].spawntime) // spawn entities when timer reached + { + int oldtime = sents[i].spawntime; + sents[i].spawntime -= curtime; + if(sents[i].spawntime<=0) + { + sents[i].spawntime = 0; + sents[i].spawned = true; + sendf(-1, 1, "ri2", N_ITEMSPAWN, i); + } + else if(sents[i].spawntime<=10000 && oldtime>10000 && (sents[i].type==I_QUAD || sents[i].type==I_BOOST)) + { + sendf(-1, 1, "ri2", N_ANNOUNCE, sents[i].type); + } + } + } + aiman::checkai(); + } + } + + while(bannedips.length() && bannedips[0].expire-totalmillis <= 0) bannedips.remove(0); + //~loopv(connects) if(totalmillis-connects[i]->connectmillis>15000) disconnect_client(connects[i]->clientnum, DISC_TIMEOUT); + + if(nextexceeded && gamemillis > nextexceeded && (!m_timed || gamemillis < gamelimit)) + { + nextexceeded = 0; + loopvrev(clients) + { + clientinfo &c = *clients[i]; + if(c.state.aitype != AI_NONE) continue; + //~if(c.checkexceeded()) disconnect_client(c.clientnum, DISC_MSGERR); + else c.scheduleexceeded(); + } + } + + if(shouldcheckteamkills) checkteamkills(); + + if(shouldstep && !gamepaused) + { + if(m_timed && smapname[0] && gamemillis-curtime>0) checkintermission(); + if(interm > 0 && gamemillis>interm) + { + if(demorecord) enddemorecord(); + interm = -1; + checkvotes(true); + } + } + + shouldstep = clients.length() > 0; + } + + void forcespectator(clientinfo *ci) + { + if(ci->state.state==CS_ALIVE) suicide(ci); + ci->state.state = CS_SPECTATOR; + ci->state.timeplayed += lastmillis - ci->state.lasttimeplayed; + if(!ci->local && (!ci->privilege || ci->warned)) aiman::removeai(ci); + sendf(-1, 1, "ri3", N_SPECTATOR, ci->clientnum, 1); + } + + struct crcinfo + { + int crc, matches; + + crcinfo() {} + crcinfo(int crc, int matches) : crc(crc), matches(matches) {} + + static bool compare(const crcinfo &x, const crcinfo &y) { return x.matches > y.matches; } + }; + + VAR(modifiedmapspectator, 0, 1, 2); + + void checkmaps(int req = -1) + { + if(m_edit || !smapname[0]) return; + vector<crcinfo> crcs; + int total = 0, unsent = 0, invalid = 0; + if(mcrc) crcs.add(crcinfo(mcrc, clients.length() + 1)); + loopv(clients) + { + clientinfo *ci = clients[i]; + if(ci->state.state==CS_SPECTATOR || ci->state.aitype != AI_NONE) continue; + total++; + if(!ci->clientmap[0]) + { + if(ci->mapcrc < 0) invalid++; + else if(!ci->mapcrc) unsent++; + } + else + { + crcinfo *match = NULL; + loopvj(crcs) if(crcs[j].crc == ci->mapcrc) { match = &crcs[j]; break; } + if(!match) crcs.add(crcinfo(ci->mapcrc, 1)); + else match->matches++; + } + } + if(!mcrc && total - unsent < min(total, 4)) return; + crcs.sort(crcinfo::compare); + string msg; + loopv(clients) + { + clientinfo *ci = clients[i]; + if(ci->state.state==CS_SPECTATOR || ci->state.aitype != AI_NONE || ci->clientmap[0] || ci->mapcrc >= 0 || (req < 0 && ci->warned)) continue; + formatstring(msg, "%s has modified map \"%s\"", colorname(ci), smapname); + sendf(req, 1, "ris", N_SERVMSG, msg); + if(req < 0) ci->warned = true; + } + if(crcs.length() >= 2) loopv(crcs) + { + crcinfo &info = crcs[i]; + if(i || info.matches <= crcs[i+1].matches) loopvj(clients) + { + clientinfo *ci = clients[j]; + if(ci->state.state==CS_SPECTATOR || ci->state.aitype != AI_NONE || !ci->clientmap[0] || ci->mapcrc != info.crc || (req < 0 && ci->warned)) continue; + formatstring(msg, "%s has modified map \"%s\"", colorname(ci), smapname); + sendf(req, 1, "ris", N_SERVMSG, msg); + if(req < 0) ci->warned = true; + } + } + if(req < 0 && modifiedmapspectator && (mcrc || modifiedmapspectator > 1)) loopv(clients) + { + clientinfo *ci = clients[i]; + if(!ci->local && ci->warned && ci->state.state != CS_SPECTATOR) forcespectator(ci); + } + } + + bool shouldspectate(clientinfo *ci) + { + return !ci->local && ci->warned && modifiedmapspectator && (mcrc || modifiedmapspectator > 1); + } + + void unspectate(clientinfo *ci) + { + if(shouldspectate(ci)) return; + ci->state.state = CS_DEAD; + ci->state.respawn(); + ci->state.lasttimeplayed = lastmillis; + aiman::addclient(ci); + sendf(-1, 1, "ri3", N_SPECTATOR, ci->clientnum, 0); + if(ci->clientmap[0] || ci->mapcrc) checkmaps(); + if(!hasmap(ci)) rotatemap(true); + } + + void sendservinfo(clientinfo *ci) + { + sendf(ci->clientnum, 1, "ri5ss", N_SERVINFO, ci->clientnum, PROTOCOL_VERSION, ci->sessionid, serverpass[0] ? 1 : 0, serverdesc, serverauth); + } + + void noclients() + { + bannedips.shrink(0); + aiman::clearai(); + } + + void localconnect(int n) + { + clientinfo *ci = getinfo(n); + ci->clientnum = ci->ownernum = n; + ci->connectmillis = totalmillis; + ci->sessionid = (rnd(0x1000000)*((totalmillis%10000)+1))&0xFFFFFF; + ci->local = true; + + connects.add(ci); + sendservinfo(ci); + } + + void localdisconnect(int n) + { + if(m_demo) enddemoplayback(); + clientdisconnect(n); + } + + int clientconnect(int n) + { + clientinfo *ci = getinfo(n); + ci->clientnum = ci->ownernum = n; + ci->connectmillis = totalmillis; + ci->sessionid = (rnd(0x1000000)*((totalmillis%10000)+1))&0xFFFFFF; + + connects.add(ci); + if(!m_mp(gamemode)) return DISC_LOCAL; + sendservinfo(ci); + return DISC_NONE; + } + + void clientdisconnect(int n) + { + clientinfo *ci = getinfo(n); + loopv(clients) if(clients[i]->authkickvictim == ci->clientnum) clients[i]->cleanauth(); + if(ci->connected) + { + if(ci->privilege) setmaster(ci, false); + ci->state.timeplayed += lastmillis - ci->state.lasttimeplayed; + savescore(ci); + sendf(-1, 1, "ri2", N_CDIS, n); + clients.removeobj(ci); + aiman::removeai(ci); + if(!numclients(-1, false, true)) noclients(); // bans clear when server empties + if(ci->local) checkpausegame(); + } + else connects.removeobj(ci); + } + + int reserveclients() { return 3; } + + extern void verifybans(); + + struct banlist + { + vector<ipmask> bans; + + void clear() { bans.shrink(0); } + + bool check(uint ip) + { + loopv(bans) if(bans[i].check(ip)) return true; + return false; + } + + void add(const char *ipname) + { + ipmask ban; + ban.parse(ipname); + bans.add(ban); + + verifybans(); + } + } ipbans, gbans; + + bool checkbans(uint ip) + { + loopv(bannedips) if(bannedips[i].ip==ip) return true; + return ipbans.check(ip) || gbans.check(ip); + } + + void verifybans() + { + loopvrev(clients) + { + clientinfo *ci = clients[i]; + if(ci->state.aitype != AI_NONE || ci->local || ci->privilege >= PRIV_ADMIN) continue; + //~if(checkbans(getclientip(ci->clientnum))) disconnect_client(ci->clientnum, DISC_IPBAN); + } + } + + ICOMMAND(clearipbans, "", (), ipbans.clear()); + ICOMMAND(ipban, "s", (const char *ipname), ipbans.add(ipname)); + + int allowconnect(clientinfo *ci, const char *pwd = "") + { + if(ci->local) return DISC_NONE; + if(!m_mp(gamemode)) return DISC_LOCAL; + if(serverpass[0]) + { + if(!checkpassword(ci, serverpass, pwd)) return DISC_PASSWORD; + return DISC_NONE; + } + if(adminpass[0] && checkpassword(ci, adminpass, pwd)) return DISC_NONE; + if(numclients(-1, false, true)>=maxclients) return DISC_MAXCLIENTS; + uint ip = getclientip(ci->clientnum); + if(checkbans(ip)) return DISC_IPBAN; + if(mastermode>=MM_PRIVATE && allowedips.find(ip)<0) return DISC_PRIVATE; + return DISC_NONE; + } + + bool allowbroadcast(int n) + { + clientinfo *ci = getinfo(n); + return ci && ci->connected; + } + + clientinfo *findauth(uint id) + { + loopv(clients) if(clients[i]->authreq == id) return clients[i]; + return NULL; + } + + + void authfailed(clientinfo *ci) + { + if(!ci) return; + ci->cleanauth(); + //~if(ci->connectauth) disconnect_client(ci->clientnum, ci->connectauth); + } + + void authfailed(uint id) + { + authfailed(findauth(id)); + } + + void authsucceeded(uint id) + { + clientinfo *ci = findauth(id); + if(!ci) return; + ci->cleanauth(ci->connectauth!=0); + if(ci->connectauth) connected(ci); + if(ci->authkickvictim >= 0) + { + if(setmaster(ci, true, "", ci->authname, NULL, PRIV_AUTH, false, true)) + trykick(ci, ci->authkickvictim, ci->authkickreason, ci->authname, NULL, PRIV_AUTH); + ci->cleanauthkick(); + } + else setmaster(ci, true, "", ci->authname, NULL, PRIV_AUTH); + } + + void authchallenged(uint id, const char *val, const char *desc = "") + { + clientinfo *ci = findauth(id); + if(!ci) return; + sendf(ci->clientnum, 1, "risis", N_AUTHCHAL, desc, id, val); + } + + uint nextauthreq = 0; + + bool tryauth(clientinfo *ci, const char *user, const char *desc) + { + ci->cleanauth(); + if(!nextauthreq) nextauthreq = 1; + ci->authreq = nextauthreq++; + filtertext(ci->authname, user, false, false, 100); + copystring(ci->authdesc, desc); + if(ci->authdesc[0]) + { + userinfo *u = users.access(userkey(ci->authname, ci->authdesc)); + if(u) + { + uint seed[3] = { ::hthash(serverauth) + detrnd(size_t(ci) + size_t(user) + size_t(desc), 0x10000), uint(totalmillis), randomMT() }; + vector<char> buf; + ci->authchallenge = genchallenge(u->pubkey, seed, sizeof(seed), buf); + sendf(ci->clientnum, 1, "risis", N_AUTHCHAL, desc, ci->authreq, buf.getbuf()); + } + else ci->cleanauth(); + } + else if(!requestmasterf("reqauth %u %s\n", ci->authreq, ci->authname)) + { + ci->cleanauth(); + sendf(ci->clientnum, 1, "ris", N_SERVMSG, "not connected to authentication server"); + } + if(ci->authreq) return true; + //~if(ci->connectauth) disconnect_client(ci->clientnum, ci->connectauth); + return false; + } + + bool answerchallenge(clientinfo *ci, uint id, char *val, const char *desc) + { + if(ci->authreq != id || strcmp(ci->authdesc, desc)) + { + ci->cleanauth(); + return !ci->connectauth; + } + for(char *s = val; *s; s++) + { + if(!isxdigit(*s)) { *s = '\0'; break; } + } + if(desc[0]) + { + if(ci->authchallenge && checkchallenge(val, ci->authchallenge)) + { + userinfo *u = users.access(userkey(ci->authname, ci->authdesc)); + if(u) + { + if(ci->connectauth) connected(ci); + if(ci->authkickvictim >= 0) + { + if(setmaster(ci, true, "", ci->authname, ci->authdesc, u->privilege, false, true)) + trykick(ci, ci->authkickvictim, ci->authkickreason, ci->authname, ci->authdesc, u->privilege); + } + else setmaster(ci, true, "", ci->authname, ci->authdesc, u->privilege); + } + } + ci->cleanauth(); + } + else if(!requestmasterf("confauth %u %s\n", id, val)) + { + ci->cleanauth(); + sendf(ci->clientnum, 1, "ris", N_SERVMSG, "not connected to authentication server"); + } + return ci->authreq || !ci->connectauth; + } + + void masterconnected() + { + } + + void masterdisconnected() + { + loopvrev(clients) + { + clientinfo *ci = clients[i]; + if(ci->authreq) authfailed(ci); + } + } + + void processmasterinput(const char *cmd, int cmdlen, const char *args) + { + uint id; + string val; + if(sscanf(cmd, "failauth %u", &id) == 1) + authfailed(id); + else if(sscanf(cmd, "succauth %u", &id) == 1) + authsucceeded(id); + else if(sscanf(cmd, "chalauth %u %255s", &id, val) == 2) + authchallenged(id, val); + else if(matchstring(cmd, cmdlen, "cleargbans")) + gbans.clear(); + else if(sscanf(cmd, "addgban %100s", val) == 1) + gbans.add(val); + } + + void receivefile(int sender, uchar *data, int len) + { + if(!m_edit || len <= 0 || len > 4*1024*1024) return; + clientinfo *ci = getinfo(sender); + if(ci->state.state==CS_SPECTATOR && !ci->privilege && !ci->local) return; + if(mapdata) DELETEP(mapdata); + mapdata = opentempfile("mapdata", "w+b"); + if(!mapdata) { sendf(sender, 1, "ris", N_SERVMSG, "failed to open temporary file for map"); return; } + mapdata->write(data, len); + sendservmsgf("[%s sent a map to server, \"/getmap\" to receive it]", colorname(ci)); + } + + void sendclipboard(clientinfo *ci) + { + if(!ci->lastclipboard || !ci->clipboard) return; + bool flushed = false; + loopv(clients) + { + clientinfo &e = *clients[i]; + if(e.clientnum != ci->clientnum && e.needclipboard - ci->lastclipboard >= 0) + { + if(!flushed) { flushserver(true); flushed = true; } + sendpacket(e.clientnum, 1, ci->clipboard); + } + } + } + + void connected(clientinfo *ci) + { + if(m_demo) enddemoplayback(); + + if(!hasmap(ci)) rotatemap(false); + + shouldstep = true; + + connects.removeobj(ci); + clients.add(ci); + + ci->connectauth = 0; + ci->connected = true; + ci->needclipboard = totalmillis ? totalmillis : 1; + if(mastermode>=MM_LOCKED) ci->state.state = CS_SPECTATOR; + ci->state.lasttimeplayed = lastmillis; + + const char *worst = m_teammode ? chooseworstteam(NULL, ci) : NULL; + copystring(ci->team, worst ? worst : "good", MAXTEAMLEN+1); + + sendwelcome(ci); + if(restorescore(ci)) sendresume(ci); + sendinitclient(ci); + + aiman::addclient(ci); + + if(m_demo) setupdemoplayback(); + + if(servermotd[0]) sendf(ci->clientnum, 1, "ris", N_SERVMSG, servermotd); + } + + void parsepacket(int sender, int chan, packetbuf &p) // has to parse exactly each byte of the packet + { + if(sender<0 || p.packet->flags&ENET_PACKET_FLAG_UNSEQUENCED || chan > 2) return; + char text[MAXTRANS]; + int type; + clientinfo *ci = sender>=0 ? getinfo(sender) : NULL, *cq = ci, *cm = ci; + if(ci && !ci->connected) + { + if(chan==0) return; + //~else if(chan!=1) { disconnect_client(sender, DISC_MSGERR); return; } + else while(p.length() < p.maxlen) switch(checktype(getint(p), ci)) + { + case N_CONNECT: + { + getstring(text, p); + filtertext(text, text, false, false, MAXNAMELEN); + if(!text[0]) copystring(text, "Anonymous"); + copystring(ci->name, text, MAXNAMELEN+1); + ci->playermodel = 0; + + string password, authdesc, authname; + getstring(password, p, sizeof(password)); + getstring(authdesc, p, sizeof(authdesc)); + getstring(authname, p, sizeof(authname)); + int disc = allowconnect(ci, password); + if(disc) + { + if(disc == DISC_LOCAL || !serverauth[0] || strcmp(serverauth, authdesc) || !tryauth(ci, authname, authdesc)) + { + //~disconnect_client(sender, disc); + return; + } + ci->connectauth = disc; + } + else connected(ci); + break; + } + + case N_AUTHANS: + { + string desc, ans; + getstring(desc, p, sizeof(desc)); + uint id = (uint)getint(p); + getstring(ans, p, sizeof(ans)); + if(!answerchallenge(ci, id, ans, desc)) + { + //~disconnect_client(sender, ci->connectauth); + return; + } + break; + } + + case N_PING: + getint(p); + break; + + default: + //~disconnect_client(sender, DISC_MSGERR); + return; + } + return; + } + else if(chan==2) + { + receivefile(sender, p.buf, p.maxlen); + return; + } + + if(p.packet->flags&ENET_PACKET_FLAG_RELIABLE) reliablemessages = true; + #define QUEUE_AI clientinfo *cm = cq; + #define QUEUE_MSG { if(cm && (!cm->local || demorecord || hasnonlocalclients())) while(curmsg<p.length()) cm->messages.add(p.buf[curmsg++]); } + #define QUEUE_BUF(body) { \ + if(cm && (!cm->local || demorecord || hasnonlocalclients())) \ + { \ + curmsg = p.length(); \ + { body; } \ + } \ + } + #define QUEUE_INT(n) QUEUE_BUF(putint(cm->messages, n)) + #define QUEUE_UINT(n) QUEUE_BUF(putuint(cm->messages, n)) + #define QUEUE_STR(text) QUEUE_BUF(sendstring(text, cm->messages)) + int curmsg; + while((curmsg = p.length()) < p.maxlen) switch(type = checktype(getint(p), ci)) + { + case N_POS: + { + int pcn = getuint(p); + p.get(); + uint flags = getuint(p); + clientinfo *cp = getinfo(pcn); + if(cp && pcn != sender && cp->ownernum != sender) cp = NULL; + vec pos; + loopk(3) + { + int n = p.get(); n |= p.get()<<8; if(flags&(1<<k)) { n |= p.get()<<16; if(n&0x800000) n |= ~0U<<24; } + pos[k] = n/DMF; + } + loopk(3) p.get(); + int mag = p.get(); if(flags&(1<<3)) mag |= p.get()<<8; + int dir = p.get(); dir |= p.get()<<8; + vec vel = vec((dir%360)*RAD, (clamp(dir/360, 0, 180)-90)*RAD).mul(mag/DVELF); + if(flags&(1<<4)) + { + p.get(); if(flags&(1<<5)) p.get(); + if(flags&(1<<6)) loopk(2) p.get(); + } + if(cp) + { + if((!ci->local || demorecord || hasnonlocalclients()) && (cp->state.state==CS_ALIVE || cp->state.state==CS_EDITING)) + { + if(!ci->local && !m_edit && max(vel.magnitude2(), (float)fabs(vel.z)) >= 180) + cp->setexceeded(); + cp->position.setsize(0); + while(curmsg<p.length()) cp->position.add(p.buf[curmsg++]); + } + cp->state.o = pos; + cp->gameclip = (flags&0x80)!=0; + } + break; + } + + case N_TELEPORT: + { + int pcn = getint(p), teleport = getint(p), teledest = getint(p); + clientinfo *cp = getinfo(pcn); + if(cp && pcn != sender && cp->ownernum != sender) cp = NULL; + if(cp && (!ci->local || demorecord || hasnonlocalclients()) && (cp->state.state==CS_ALIVE || cp->state.state==CS_EDITING)) + { + flushclientposition(*cp); + sendf(-1, 0, "ri4x", N_TELEPORT, pcn, teleport, teledest, cp->ownernum); + } + break; + } + + case N_JUMPPAD: + { + int pcn = getint(p), jumppad = getint(p); + clientinfo *cp = getinfo(pcn); + if(cp && pcn != sender && cp->ownernum != sender) cp = NULL; + if(cp && (!ci->local || demorecord || hasnonlocalclients()) && (cp->state.state==CS_ALIVE || cp->state.state==CS_EDITING)) + { + cp->setpushed(); + flushclientposition(*cp); + sendf(-1, 0, "ri3x", N_JUMPPAD, pcn, jumppad, cp->ownernum); + } + break; + } + + case N_FROMAI: + { + int qcn = getint(p); + if(qcn < 0) cq = ci; + else + { + cq = getinfo(qcn); + if(cq && qcn != sender && cq->ownernum != sender) cq = NULL; + } + break; + } + + case N_EDITMODE: + { + int val = getint(p); + if(!ci->local && !m_edit) break; + if(val ? ci->state.state!=CS_ALIVE && ci->state.state!=CS_DEAD : ci->state.state!=CS_EDITING) break; + if(val) + { + ci->state.editstate = ci->state.state; + ci->state.state = CS_EDITING; + ci->events.setsize(0); + ci->state.rockets.reset(); + ci->state.grenades.reset(); + } + else ci->state.state = ci->state.editstate; + QUEUE_MSG; + break; + } + + case N_MAPCRC: + { + getstring(text, p); + int crc = getint(p); + if(!ci) break; + if(strcmp(text, smapname)) + { + if(ci->clientmap[0]) + { + ci->clientmap[0] = '\0'; + ci->mapcrc = 0; + } + else if(ci->mapcrc > 0) ci->mapcrc = 0; + break; + } + copystring(ci->clientmap, text); + ci->mapcrc = text[0] ? crc : 1; + checkmaps(); + if(cq && cq != ci && cq->ownernum != ci->clientnum) cq = NULL; + break; + } + + case N_CHECKMAPS: + checkmaps(sender); + break; + + case N_TRYSPAWN: + if(!ci || !cq || cq->state.state!=CS_DEAD || cq->state.lastspawn>=0) break; + if(!ci->clientmap[0] && !ci->mapcrc) + { + ci->mapcrc = -1; + checkmaps(); + if(ci == cq) { if(ci->state.state != CS_DEAD) break; } + else if(cq->ownernum != ci->clientnum) { cq = NULL; break; } + } + if(cq->state.deadflush) + { + flushevents(cq, cq->state.deadflush); + cq->state.respawn(); + } + cleartimedevents(cq); + sendspawn(cq); + break; + + case N_GUNSELECT: + { + int gunselect = getint(p); + if(!cq || cq->state.state!=CS_ALIVE) break; + cq->state.gunselect = gunselect >= GUN_FIST && gunselect <= GUN_PISTOL ? gunselect : GUN_FIST; + QUEUE_AI; + QUEUE_MSG; + break; + } + + case N_SPAWN: + { + int ls = getint(p), gunselect = getint(p); + if(!cq || (cq->state.state!=CS_ALIVE && cq->state.state!=CS_DEAD && cq->state.state!=CS_EDITING) || ls!=cq->state.lifesequence || cq->state.lastspawn<0) break; + cq->state.lastspawn = -1; + cq->state.state = CS_ALIVE; + cq->state.gunselect = gunselect >= GUN_FIST && gunselect <= GUN_PISTOL ? gunselect : GUN_FIST; + cq->exceeded = 0; + QUEUE_AI; + QUEUE_BUF({ + putint(cm->messages, N_SPAWN); + sendstate(cq->state, cm->messages); + }); + break; + } + + case N_SUICIDE: + { + if(cq) cq->addevent(new suicideevent); + break; + } + + case N_SHOOT: + { + shotevent *shot = new shotevent; + shot->id = getint(p); + shot->millis = cq ? cq->geteventmillis(gamemillis, shot->id) : 0; + shot->gun = getint(p); + loopk(3) shot->from[k] = getint(p)/DMF; + loopk(3) shot->to[k] = getint(p)/DMF; + int hits = getint(p); + loopk(hits) + { + if(p.overread()) break; + hitinfo &hit = shot->hits.add(); + hit.target = getint(p); + hit.lifesequence = getint(p); + hit.dist = getint(p)/DMF; + hit.rays = getint(p); + loopk(3) hit.dir[k] = getint(p)/DNF; + } + if(cq) + { + cq->addevent(shot); + cq->setpushed(); + } + else delete shot; + break; + } + + case N_EXPLODE: + { + explodeevent *exp = new explodeevent; + int cmillis = getint(p); + exp->millis = cq ? cq->geteventmillis(gamemillis, cmillis) : 0; + exp->gun = getint(p); + exp->id = getint(p); + int hits = getint(p); + loopk(hits) + { + if(p.overread()) break; + hitinfo &hit = exp->hits.add(); + hit.target = getint(p); + hit.lifesequence = getint(p); + hit.dist = getint(p)/DMF; + hit.rays = getint(p); + loopk(3) hit.dir[k] = getint(p)/DNF; + } + if(cq) cq->addevent(exp); + else delete exp; + break; + } + + case N_ITEMPICKUP: + { + int n = getint(p); + if(!cq) break; + pickupevent *pickup = new pickupevent; + pickup->ent = n; + cq->addevent(pickup); + break; + } + + case N_TEXT: + { + QUEUE_AI; + QUEUE_MSG; + getstring(text, p); + filtertext(text, text, true, true); + QUEUE_STR(text); + if(isdedicatedserver() && cq) logoutf("%s: %s", colorname(cq), text); + break; + } + + case N_SAYTEAM: + { + getstring(text, p); + if(!ci || !cq || (ci->state.state==CS_SPECTATOR && !ci->local && !ci->privilege) || !m_teammode || !cq->team[0]) break; + filtertext(text, text, true, true); + loopv(clients) + { + clientinfo *t = clients[i]; + if(t==cq || t->state.state==CS_SPECTATOR || t->state.aitype != AI_NONE || strcmp(cq->team, t->team)) continue; + sendf(t->clientnum, 1, "riis", N_SAYTEAM, cq->clientnum, text); + } + if(isdedicatedserver() && cq) logoutf("%s <%s>: %s", colorname(cq), cq->team, text); + break; + } + + case N_SWITCHNAME: + { + QUEUE_MSG; + getstring(text, p); + filtertext(ci->name, text, false, false, MAXNAMELEN); + if(!ci->name[0]) copystring(ci->name, "Anonymous"); + QUEUE_STR(ci->name); + break; + } + + case N_SWITCHMODEL: + { + ci->playermodel = 0; + QUEUE_MSG; + break; + } + + case N_SWITCHTEAM: + { + getstring(text, p); + filtertext(text, text, false, false, MAXTEAMLEN); + if(m_teammode && text[0] && strcmp(ci->team, text) && addteaminfo(text)) + { + if(ci->state.state==CS_ALIVE) suicide(ci); + copystring(ci->team, text); + aiman::changeteam(ci); + sendf(-1, 1, "riisi", N_SETTEAM, sender, ci->team, ci->state.state==CS_SPECTATOR ? -1 : 0); + } + break; + } + + case N_MAPVOTE: + { + getstring(text, p); + filtertext(text, text, false); + fixmapname(text); + int reqmode = getint(p); + vote(text, reqmode, sender); + break; + } + + case N_ITEMLIST: + { + if((ci->state.state==CS_SPECTATOR && !ci->privilege && !ci->local) || !notgotitems || strcmp(ci->clientmap, smapname)) { while(getint(p)>=0 && !p.overread()) getint(p); break; } + int n; + while((n = getint(p))>=0 && n<MAXENTS && !p.overread()) + { + server_entity se = { NOTUSED, 0, false }; + while(sents.length()<=n) sents.add(se); + sents[n].type = getint(p); + if(canspawnitem(sents[n].type)) + { + if(m_mp(gamemode) && delayspawn(sents[n].type)) sents[n].spawntime = spawntime(sents[n].type); + else sents[n].spawned = true; + } + } + notgotitems = false; + break; + } + + case N_EDITENT: + { + int i = getint(p); + loopk(3) getint(p); + int type = getint(p); + loopk(5) getint(p); + if(!ci || ci->state.state==CS_SPECTATOR) break; + QUEUE_MSG; + bool canspawn = canspawnitem(type); + if(i<MAXENTS && (sents.inrange(i) || canspawnitem(type))) + { + server_entity se = { NOTUSED, 0, false }; + while(sents.length()<=i) sents.add(se); + sents[i].type = type; + if(canspawn ? !sents[i].spawned : (sents[i].spawned || sents[i].spawntime)) + { + sents[i].spawntime = canspawn ? 1 : 0; + sents[i].spawned = false; + } + } + break; + } + + case N_EDITVAR: + { + int type = getint(p); + getstring(text, p); + switch(type) + { + case ID_VAR: getint(p); break; + case ID_FVAR: getfloat(p); break; + case ID_SVAR: getstring(text, p); + } + if(ci && ci->state.state!=CS_SPECTATOR) QUEUE_MSG; + break; + } + + case N_PING: + sendf(sender, 1, "i2", N_PONG, getint(p)); + break; + + case N_CLIENTPING: + { + int ping = getint(p); + if(ci) + { + ci->ping = ping; + loopv(ci->bots) ci->bots[i]->ping = ping; + } + QUEUE_MSG; + break; + } + + case N_MASTERMODE: + { + int mm = getint(p); + if((ci->privilege || ci->local) && mm>=MM_OPEN && mm<=MM_PRIVATE) + { + if((ci->privilege>=PRIV_ADMIN || ci->local) || (mastermask&(1<<mm))) + { + mastermode = mm; + allowedips.shrink(0); + if(mm>=MM_PRIVATE) + { + loopv(clients) allowedips.add(getclientip(clients[i]->clientnum)); + } + sendf(-1, 1, "rii", N_MASTERMODE, mastermode); + //sendservmsgf("mastermode is now %s (%d)", mastermodename(mastermode), mastermode); + } + else + { + defformatstring(s, "mastermode %d is disabled on this server", mm); + sendf(sender, 1, "ris", N_SERVMSG, s); + } + } + break; + } + + case N_CLEARBANS: + { + if(ci->privilege || ci->local) + { + bannedips.shrink(0); + sendservmsg("cleared all bans"); + } + break; + } + + case N_KICK: + { + int victim = getint(p); + getstring(text, p); + filtertext(text, text); + trykick(ci, victim, text); + break; + } + + case N_SPECTATOR: + { + int spectator = getint(p), val = getint(p); + if(!ci->privilege && !ci->local && (spectator!=sender || (ci->state.state==CS_SPECTATOR && mastermode>=MM_LOCKED))) break; + clientinfo *spinfo = (clientinfo *)getclientinfo(spectator); // no bots + if(!spinfo || !spinfo->connected || (spinfo->state.state==CS_SPECTATOR ? val : !val)) break; + + if(spinfo->state.state!=CS_SPECTATOR && val) forcespectator(spinfo); + else if(spinfo->state.state==CS_SPECTATOR && !val) unspectate(spinfo); + + if(cq && cq != ci && cq->ownernum != ci->clientnum) cq = NULL; + break; + } + + case N_SETTEAM: + { + int who = getint(p); + getstring(text, p); + filtertext(text, text, false, false, MAXTEAMLEN); + if(!ci->privilege && !ci->local) break; + clientinfo *wi = getinfo(who); + if(!m_teammode || !text[0] || !wi || !wi->connected || !strcmp(wi->team, text)) break; + if(addteaminfo(text)) + { + if(wi->state.state==CS_ALIVE) suicide(wi); + copystring(wi->team, text, MAXTEAMLEN+1); + } + aiman::changeteam(wi); + sendf(-1, 1, "riisi", N_SETTEAM, who, wi->team, 1); + break; + } + + case N_FORCEINTERMISSION: + if(ci->local && !hasnonlocalclients()) startintermission(); + break; + + case N_RECORDDEMO: + { + int val = getint(p); + if(ci->privilege < (restrictdemos ? PRIV_ADMIN : PRIV_MASTER) && !ci->local) break; + if(!maxdemos || !maxdemosize) + { + sendf(ci->clientnum, 1, "ris", N_SERVMSG, "the server has disabled demo recording"); + break; + } + demonextmatch = val!=0; + sendservmsgf("demo recording is %s for next match", demonextmatch ? "enabled" : "disabled"); + break; + } + + case N_STOPDEMO: + { + if(ci->privilege < (restrictdemos ? PRIV_ADMIN : PRIV_MASTER) && !ci->local) break; + stopdemo(); + break; + } + + case N_CLEARDEMOS: + { + int demo = getint(p); + if(ci->privilege < (restrictdemos ? PRIV_ADMIN : PRIV_MASTER) && !ci->local) break; + cleardemos(demo); + break; + } + + case N_LISTDEMOS: + if(!ci->privilege && !ci->local && ci->state.state==CS_SPECTATOR) break; + listdemos(sender); + break; + + case N_GETDEMO: + { + int n = getint(p), tag = getint(p); + if(!ci->privilege && !ci->local && ci->state.state==CS_SPECTATOR) break; + senddemo(ci, n, tag); + break; + } + + case N_GETMAP: + if(!mapdata) sendf(sender, 1, "ris", N_SERVMSG, "no map to send"); + else if(ci->getmap) sendf(sender, 1, "ris", N_SERVMSG, "already sending map"); + else + { + sendservmsgf("[%s is getting the map]", colorname(ci)); + if((ci->getmap = sendfile(sender, 2, mapdata, "ri", N_SENDMAP))) + ci->getmap->freeCallback = freegetmap; + ci->needclipboard = totalmillis ? totalmillis : 1; + } + break; + + case N_NEWMAP: + { + int size = getint(p); + if(!ci->privilege && !ci->local && ci->state.state==CS_SPECTATOR) break; + if(size>=0) + { + smapname[0] = '\0'; + resetitems(); + notgotitems = false; + } + QUEUE_MSG; + break; + } + + case N_SETMASTER: + { + int mn = getint(p), val = getint(p); + getstring(text, p); + if(mn != ci->clientnum) + { + if(!ci->privilege && !ci->local) break; + clientinfo *minfo = (clientinfo *)getclientinfo(mn); + if(!minfo || !minfo->connected || (!ci->local && minfo->privilege >= ci->privilege) || (val && minfo->privilege)) break; + setmaster(minfo, val!=0, "", NULL, NULL, PRIV_MASTER, true); + } + else setmaster(ci, val!=0, text); + // don't broadcast the master password + break; + } + + case N_ADDBOT: + { + aiman::reqadd(ci, getint(p)); + break; + } + + case N_DELBOT: + { + aiman::reqdel(ci); + break; + } + + case N_BOTLIMIT: + { + int limit = getint(p); + if(ci) aiman::setbotlimit(ci, limit); + break; + } + + case N_BOTBALANCE: + { + int balance = getint(p); + if(ci) aiman::setbotbalance(ci, balance!=0); + break; + } + + case N_AUTHTRY: + { + string desc, name; + getstring(desc, p, sizeof(desc)); + getstring(name, p, sizeof(name)); + tryauth(ci, name, desc); + break; + } + + case N_AUTHKICK: + { + string desc, name; + getstring(desc, p, sizeof(desc)); + getstring(name, p, sizeof(name)); + int victim = getint(p); + getstring(text, p); + filtertext(text, text); + int authpriv = PRIV_AUTH; + if(desc[0]) + { + userinfo *u = users.access(userkey(name, desc)); + if(u) authpriv = u->privilege; else break; + } + if(ci->local || ci->privilege >= authpriv) trykick(ci, victim, text); + else if(trykick(ci, victim, text, name, desc, authpriv, true) && tryauth(ci, name, desc)) + { + ci->authkickvictim = victim; + ci->authkickreason = newstring(text); + } + break; + } + + case N_AUTHANS: + { + string desc, ans; + getstring(desc, p, sizeof(desc)); + uint id = (uint)getint(p); + getstring(ans, p, sizeof(ans)); + answerchallenge(ci, id, ans, desc); + break; + } + + case N_PAUSEGAME: + { + int val = getint(p); + if(ci->privilege < (restrictpausegame ? PRIV_ADMIN : PRIV_MASTER) && !ci->local) break; + pausegame(val > 0, ci); + break; + } + + case N_GAMESPEED: + { + int val = getint(p); + if(ci->privilege < (restrictgamespeed ? PRIV_ADMIN : PRIV_MASTER) && !ci->local) break; + changegamespeed(val, ci); + break; + } + + case N_COPY: + ci->cleanclipboard(); + ci->lastclipboard = totalmillis ? totalmillis : 1; + goto genericmsg; + + case N_PASTE: + if(ci->state.state!=CS_SPECTATOR) sendclipboard(ci); + goto genericmsg; + + case N_CLIPBOARD: + { + int unpacklen = getint(p), packlen = getint(p); + ci->cleanclipboard(false); + if(ci->state.state==CS_SPECTATOR) + { + if(packlen > 0) p.subbuf(packlen); + break; + } + if(packlen <= 0 || packlen > (1<<16) || unpacklen <= 0) + { + if(packlen > 0) p.subbuf(packlen); + packlen = unpacklen = 0; + } + packetbuf q(32 + packlen, ENET_PACKET_FLAG_RELIABLE); + putint(q, N_CLIPBOARD); + putint(q, ci->clientnum); + putint(q, unpacklen); + putint(q, packlen); + if(packlen > 0) p.get(q.subbuf(packlen).buf, packlen); + ci->clipboard = q.finalize(); + ci->clipboard->referenceCount++; + break; + } + + case N_EDITT: + case N_REPLACE: + case N_EDITVSLOT: + { + int size = server::msgsizelookup(type); + //~if(size<=0) { disconnect_client(sender, DISC_MSGERR); return; } + loopi(size-1) getint(p); + //~if(p.remaining() < 2) { disconnect_client(sender, DISC_MSGERR); return; } + int extra = lilswap(*(const ushort *)p.pad(2)); + //~if(p.remaining() < extra) { disconnect_client(sender, DISC_MSGERR); return; } + p.pad(extra); + if(ci && ci->state.state!=CS_SPECTATOR) QUEUE_MSG; + break; + } + + case N_UNDO: + case N_REDO: + { + int unpacklen = getint(p), packlen = getint(p); + if(!ci || ci->state.state==CS_SPECTATOR || packlen <= 0 || packlen > (1<<16) || unpacklen <= 0) + { + if(packlen > 0) p.subbuf(packlen); + break; + } + //~if(p.remaining() < packlen) { disconnect_client(sender, DISC_MSGERR); return; } + packetbuf q(32 + packlen, ENET_PACKET_FLAG_RELIABLE); + putint(q, type); + putint(q, ci->clientnum); + putint(q, unpacklen); + putint(q, packlen); + if(packlen > 0) p.get(q.subbuf(packlen).buf, packlen); + sendpacket(-1, 1, q.finalize(), ci->clientnum); + break; + } + + case N_SERVCMD: + getstring(text, p); + break; + + + case -1: + //~disconnect_client(sender, DISC_MSGERR); + return; + + case -2: + //~disconnect_client(sender, DISC_OVERFLOW); + return; + + default: genericmsg: + { + int size = server::msgsizelookup(type); + //~if(size<=0) { disconnect_client(sender, DISC_MSGERR); return; } + loopi(size-1) getint(p); + if(ci) switch(msgfilter[type]) + { + case 2: case 3: if(ci->state.state != CS_SPECTATOR) QUEUE_MSG; break; + default: if(cq && (ci != cq || ci->state.state!=CS_SPECTATOR)) { QUEUE_AI; QUEUE_MSG; } break; + } + break; + } + } + } + + int laninfoport() { return SAUERBRATEN_LANINFO_PORT; } + int serverinfoport(int servport) { return servport < 0 ? SAUERBRATEN_SERVINFO_PORT : servport+1; } + int serverport(int infoport) { return infoport < 0 ? SAUERBRATEN_SERVER_PORT : infoport-1; } + const char *defaultmaster() { return "master.sauerbraten.org"; } + int masterport() { return SAUERBRATEN_MASTER_PORT; } + int numchannels() { return 3; } + + #include "extinfo.h" + + void serverinforeply(ucharbuf &req, ucharbuf &p) + { + if(req.remaining() && !getint(req)) + { + extserverinforeply(req, p); + return; + } + + putint(p, numclients(-1, false, true)); + putint(p, gamepaused || gamespeed != 100 ? 7 : 5); // number of attrs following + putint(p, PROTOCOL_VERSION); // generic attributes, passed back below + putint(p, gamemode); + putint(p, m_timed ? max((gamelimit - gamemillis)/1000, 0) : 0); + putint(p, maxclients); + putint(p, serverpass[0] ? MM_PASSWORD : (!m_mp(gamemode) ? MM_PRIVATE : (mastermode || mastermask&MM_AUTOAPPROVE ? mastermode : MM_AUTH))); + if(gamepaused || gamespeed != 100) + { + putint(p, gamepaused ? 1 : 0); + putint(p, gamespeed); + } + sendstring(smapname, p); + sendstring(serverdesc, p); + sendserverinforeply(p); + } + + #include "aiman.h" } diff --git a/src/fpsgame/waypoint.cpp b/src/fpsgame/waypoint.cpp index b59b388..c6025c5 100644 --- a/src/fpsgame/waypoint.cpp +++ b/src/fpsgame/waypoint.cpp @@ -4,443 +4,443 @@ extern selinfo sel; namespace ai { - using namespace game; - - vector<waypoint> waypoints; - - bool clipped(const vec &o) - { - int material = lookupmaterial(o), clipmat = material&MATF_CLIP; - return clipmat == MAT_CLIP || material&MAT_DEATH || (material&MATF_VOLUME) == MAT_LAVA; - } - - int getweight(const vec &o) - { - vec pos = o; pos.z += ai::JUMPMIN; - if(!insideworld(vec(pos.x, pos.y, min(pos.z, getworldsize() - 1e-3f)))) return -2; - float dist = raycube(pos, vec(0, 0, -1), 0, RAY_CLIPMAT); - int posmat = lookupmaterial(pos), weight = 1; - if(isliquid(posmat&MATF_VOLUME)) weight *= 5; - if(dist >= 0) - { - weight = int(dist/ai::JUMPMIN); - pos.z -= clamp(dist-8.0f, 0.0f, pos.z); - int trgmat = lookupmaterial(pos); - if(trgmat&MAT_DEATH || (trgmat&MATF_VOLUME) == MAT_LAVA) weight *= 10; - else if(isliquid(trgmat&MATF_VOLUME)) weight *= 2; - } - return weight; - } - - enum - { - WPCACHE_STATIC = 0, - WPCACHE_DYNAMIC, - NUMWPCACHES - }; - - struct wpcache - { - struct node - { - float split[2]; - uint child[2]; - - int axis() const { return child[0]>>30; } - int childindex(int which) const { return child[which]&0x3FFFFFFF; } - bool isleaf(int which) const { return (child[1]&(1<<(30+which)))!=0; } - }; - - vector<node> nodes; - int firstwp, lastwp; - vec bbmin, bbmax; - - wpcache() { clear(); } - - void clear() - { - nodes.setsize(0); - firstwp = lastwp = -1; - bbmin = vec(1e16f, 1e16f, 1e16f); - bbmax = vec(-1e16f, -1e16f, -1e16f); - } - - void build(int first = 0, int last = -1) - { - if(last < 0) last = waypoints.length(); - vector<int> indices; - for(int i = first; i < last; i++) - { - waypoint &w = waypoints[i]; - indices.add(i); - if(firstwp < 0) firstwp = i; - float radius = WAYPOINTRADIUS; - bbmin.min(vec(w.o).sub(radius)); - bbmax.max(vec(w.o).add(radius)); - } - if(first < last) lastwp = max(lastwp, last-1); - if(indices.length()) - { - nodes.reserve(indices.length()); - build(indices.getbuf(), indices.length(), bbmin, bbmax); - } - } - - void build(int *indices, int numindices, const vec &vmin, const vec &vmax) - { - int axis = 2; - loopk(2) if(vmax[k] - vmin[k] > vmax[axis] - vmin[axis]) axis = k; - - vec leftmin(1e16f, 1e16f, 1e16f), leftmax(-1e16f, -1e16f, -1e16f), rightmin(1e16f, 1e16f, 1e16f), rightmax(-1e16f, -1e16f, -1e16f); - float split = 0.5f*(vmax[axis] + vmin[axis]), splitleft = -1e16f, splitright = 1e16f; - int left, right; - for(left = 0, right = numindices; left < right;) - { - waypoint &w = waypoints[indices[left]]; - float radius = WAYPOINTRADIUS; - if(max(split - (w.o[axis]-radius), 0.0f) > max((w.o[axis]+radius) - split, 0.0f)) - { - ++left; - splitleft = max(splitleft, w.o[axis]+radius); - leftmin.min(vec(w.o).sub(radius)); - leftmax.max(vec(w.o).add(radius)); - } - else - { - --right; - swap(indices[left], indices[right]); - splitright = min(splitright, w.o[axis]-radius); - rightmin.min(vec(w.o).sub(radius)); - rightmax.max(vec(w.o).add(radius)); - } - } - - if(!left || right==numindices) - { - leftmin = rightmin = vec(1e16f, 1e16f, 1e16f); - leftmax = rightmax = vec(-1e16f, -1e16f, -1e16f); - left = right = numindices/2; - splitleft = -1e16f; - splitright = 1e16f; - loopi(numindices) - { - waypoint &w = waypoints[indices[i]]; - float radius = WAYPOINTRADIUS; - if(i < left) - { - splitleft = max(splitleft, w.o[axis]+radius); - leftmin.min(vec(w.o).sub(radius)); - leftmax.max(vec(w.o).add(radius)); - } - else - { - splitright = min(splitright, w.o[axis]-radius); - rightmin.min(vec(w.o).sub(radius)); - rightmax.max(vec(w.o).add(radius)); - } - } - } - - int offset = nodes.length(); - node &curnode = nodes.add(); - curnode.split[0] = splitleft; - curnode.split[1] = splitright; - - if(left<=1) curnode.child[0] = (axis<<30) | (left>0 ? indices[0] : 0x3FFFFFFF); - else - { - curnode.child[0] = (axis<<30) | (nodes.length()-offset); - if(left) build(indices, left, leftmin, leftmax); - } - - if(numindices-right<=1) curnode.child[1] = (1<<31) | (left<=1 ? 1<<30 : 0) | (numindices-right>0 ? indices[right] : 0x3FFFFFFF); - else - { - curnode.child[1] = (left<=1 ? 1<<30 : 0) | (nodes.length()-offset); - if(numindices-right) build(&indices[right], numindices-right, rightmin, rightmax); - } - } - } wpcaches[NUMWPCACHES]; - - static int invalidatedwpcaches = 0, clearedwpcaches = (1<<NUMWPCACHES)-1, numinvalidatewpcaches = 0, lastwpcache = 0; - - static inline void invalidatewpcache(int wp) - { - if(++numinvalidatewpcaches >= 1000) { numinvalidatewpcaches = 0; invalidatedwpcaches = (1<<NUMWPCACHES)-1; } - else - { - loopi(WPCACHE_DYNAMIC) if(wp >= wpcaches[i].firstwp && wp <= wpcaches[i].lastwp) { invalidatedwpcaches |= 1<<i; return; } - invalidatedwpcaches |= 1<<WPCACHE_DYNAMIC; - } - } - - void clearwpcache(bool full = true) - { - loopi(NUMWPCACHES) if(full || invalidatedwpcaches&(1<<i)) { wpcaches[i].clear(); clearedwpcaches |= 1<<i; } - if(full || invalidatedwpcaches == (1<<NUMWPCACHES)-1) - { - numinvalidatewpcaches = 0; - lastwpcache = 0; - } - invalidatedwpcaches = 0; - } - ICOMMAND(clearwpcache, "", (), clearwpcache()); - - avoidset wpavoid; - - void buildwpcache() - { - loopi(NUMWPCACHES) if(wpcaches[i].firstwp < 0) - wpcaches[i].build(i > 0 ? wpcaches[i-1].lastwp+1 : 1, i+1 >= NUMWPCACHES || wpcaches[i+1].firstwp < 0 ? -1 : wpcaches[i+1].firstwp); - clearedwpcaches = 0; - lastwpcache = waypoints.length(); - - wpavoid.clear(); + using namespace game; + + vector<waypoint> waypoints; + + bool clipped(const vec &o) + { + int material = lookupmaterial(o), clipmat = material&MATF_CLIP; + return clipmat == MAT_CLIP || material&MAT_DEATH || (material&MATF_VOLUME) == MAT_LAVA; + } + + int getweight(const vec &o) + { + vec pos = o; pos.z += ai::JUMPMIN; + if(!insideworld(vec(pos.x, pos.y, min(pos.z, getworldsize() - 1e-3f)))) return -2; + float dist = raycube(pos, vec(0, 0, -1), 0, RAY_CLIPMAT); + int posmat = lookupmaterial(pos), weight = 1; + if(isliquid(posmat&MATF_VOLUME)) weight *= 5; + if(dist >= 0) + { + weight = int(dist/ai::JUMPMIN); + pos.z -= clamp(dist-8.0f, 0.0f, pos.z); + int trgmat = lookupmaterial(pos); + if(trgmat&MAT_DEATH || (trgmat&MATF_VOLUME) == MAT_LAVA) weight *= 10; + else if(isliquid(trgmat&MATF_VOLUME)) weight *= 2; + } + return weight; + } + + enum + { + WPCACHE_STATIC = 0, + WPCACHE_DYNAMIC, + NUMWPCACHES + }; + + struct wpcache + { + struct node + { + float split[2]; + uint child[2]; + + int axis() const { return child[0]>>30; } + int childindex(int which) const { return child[which]&0x3FFFFFFF; } + bool isleaf(int which) const { return (child[1]&(1<<(30+which)))!=0; } + }; + + vector<node> nodes; + int firstwp, lastwp; + vec bbmin, bbmax; + + wpcache() { clear(); } + + void clear() + { + nodes.setsize(0); + firstwp = lastwp = -1; + bbmin = vec(1e16f, 1e16f, 1e16f); + bbmax = vec(-1e16f, -1e16f, -1e16f); + } + + void build(int first = 0, int last = -1) + { + if(last < 0) last = waypoints.length(); + vector<int> indices; + for(int i = first; i < last; i++) + { + waypoint &w = waypoints[i]; + indices.add(i); + if(firstwp < 0) firstwp = i; + float radius = WAYPOINTRADIUS; + bbmin.min(vec(w.o).sub(radius)); + bbmax.max(vec(w.o).add(radius)); + } + if(first < last) lastwp = max(lastwp, last-1); + if(indices.length()) + { + nodes.reserve(indices.length()); + build(indices.getbuf(), indices.length(), bbmin, bbmax); + } + } + + void build(int *indices, int numindices, const vec &vmin, const vec &vmax) + { + int axis = 2; + loopk(2) if(vmax[k] - vmin[k] > vmax[axis] - vmin[axis]) axis = k; + + vec leftmin(1e16f, 1e16f, 1e16f), leftmax(-1e16f, -1e16f, -1e16f), rightmin(1e16f, 1e16f, 1e16f), rightmax(-1e16f, -1e16f, -1e16f); + float split = 0.5f*(vmax[axis] + vmin[axis]), splitleft = -1e16f, splitright = 1e16f; + int left, right; + for(left = 0, right = numindices; left < right;) + { + waypoint &w = waypoints[indices[left]]; + float radius = WAYPOINTRADIUS; + if(max(split - (w.o[axis]-radius), 0.0f) > max((w.o[axis]+radius) - split, 0.0f)) + { + ++left; + splitleft = max(splitleft, w.o[axis]+radius); + leftmin.min(vec(w.o).sub(radius)); + leftmax.max(vec(w.o).add(radius)); + } + else + { + --right; + swap(indices[left], indices[right]); + splitright = min(splitright, w.o[axis]-radius); + rightmin.min(vec(w.o).sub(radius)); + rightmax.max(vec(w.o).add(radius)); + } + } + + if(!left || right==numindices) + { + leftmin = rightmin = vec(1e16f, 1e16f, 1e16f); + leftmax = rightmax = vec(-1e16f, -1e16f, -1e16f); + left = right = numindices/2; + splitleft = -1e16f; + splitright = 1e16f; + loopi(numindices) + { + waypoint &w = waypoints[indices[i]]; + float radius = WAYPOINTRADIUS; + if(i < left) + { + splitleft = max(splitleft, w.o[axis]+radius); + leftmin.min(vec(w.o).sub(radius)); + leftmax.max(vec(w.o).add(radius)); + } + else + { + splitright = min(splitright, w.o[axis]-radius); + rightmin.min(vec(w.o).sub(radius)); + rightmax.max(vec(w.o).add(radius)); + } + } + } + + int offset = nodes.length(); + node &curnode = nodes.add(); + curnode.split[0] = splitleft; + curnode.split[1] = splitright; + + if(left<=1) curnode.child[0] = (axis<<30) | (left>0 ? indices[0] : 0x3FFFFFFF); + else + { + curnode.child[0] = (axis<<30) | (nodes.length()-offset); + if(left) build(indices, left, leftmin, leftmax); + } + + if(numindices-right<=1) curnode.child[1] = (1<<31) | (left<=1 ? 1<<30 : 0) | (numindices-right>0 ? indices[right] : 0x3FFFFFFF); + else + { + curnode.child[1] = (left<=1 ? 1<<30 : 0) | (nodes.length()-offset); + if(numindices-right) build(&indices[right], numindices-right, rightmin, rightmax); + } + } + } wpcaches[NUMWPCACHES]; + + static int invalidatedwpcaches = 0, clearedwpcaches = (1<<NUMWPCACHES)-1, numinvalidatewpcaches = 0, lastwpcache = 0; + + static inline void invalidatewpcache(int wp) + { + if(++numinvalidatewpcaches >= 1000) { numinvalidatewpcaches = 0; invalidatedwpcaches = (1<<NUMWPCACHES)-1; } + else + { + loopi(WPCACHE_DYNAMIC) if(wp >= wpcaches[i].firstwp && wp <= wpcaches[i].lastwp) { invalidatedwpcaches |= 1<<i; return; } + invalidatedwpcaches |= 1<<WPCACHE_DYNAMIC; + } + } + + void clearwpcache(bool full = true) + { + loopi(NUMWPCACHES) if(full || invalidatedwpcaches&(1<<i)) { wpcaches[i].clear(); clearedwpcaches |= 1<<i; } + if(full || invalidatedwpcaches == (1<<NUMWPCACHES)-1) + { + numinvalidatewpcaches = 0; + lastwpcache = 0; + } + invalidatedwpcaches = 0; + } + ICOMMAND(clearwpcache, "", (), clearwpcache()); + + avoidset wpavoid; + + void buildwpcache() + { + loopi(NUMWPCACHES) if(wpcaches[i].firstwp < 0) + wpcaches[i].build(i > 0 ? wpcaches[i-1].lastwp+1 : 1, i+1 >= NUMWPCACHES || wpcaches[i+1].firstwp < 0 ? -1 : wpcaches[i+1].firstwp); + clearedwpcaches = 0; + lastwpcache = waypoints.length(); + + wpavoid.clear(); loopv(waypoints) if(waypoints[i].weight < 0) wpavoid.avoidnear(NULL, waypoints[i].o.z + WAYPOINTRADIUS, waypoints[i].o, WAYPOINTRADIUS); - } - - struct wpcachestack - { - wpcache::node *node; - float tmin, tmax; - }; - - vector<wpcache::node *> wpcachestack; - - int closestwaypoint(const vec &pos, float mindist, bool links, fpsent *d) - { - if(waypoints.empty()) return -1; - if(clearedwpcaches) buildwpcache(); - - #define CHECKCLOSEST(index) do { \ - int n = (index); \ - if(n < waypoints.length()) \ - { \ - const waypoint &w = waypoints[n]; \ - if(!links || w.links[0]) \ - { \ - float dist = w.o.squaredist(pos); \ - if(dist < mindist*mindist) { closest = n; mindist = sqrtf(dist); } \ - } \ - } \ - } while(0) - int closest = -1; - wpcache::node *curnode; - loop(which, NUMWPCACHES) if(wpcaches[which].firstwp >= 0) for(curnode = &wpcaches[which].nodes[0], wpcachestack.setsize(0);;) - { - int axis = curnode->axis(); - float dist1 = pos[axis] - curnode->split[0], dist2 = curnode->split[1] - pos[axis]; - if(dist1 >= mindist) - { - if(dist2 < mindist) - { - if(!curnode->isleaf(1)) { curnode += curnode->childindex(1); continue; } - CHECKCLOSEST(curnode->childindex(1)); - } - } - else if(curnode->isleaf(0)) - { - CHECKCLOSEST(curnode->childindex(0)); - if(dist2 < mindist) - { - if(!curnode->isleaf(1)) { curnode += curnode->childindex(1); continue; } - CHECKCLOSEST(curnode->childindex(1)); - } - } - else - { - if(dist2 < mindist) - { - if(!curnode->isleaf(1)) wpcachestack.add(curnode + curnode->childindex(1)); - else CHECKCLOSEST(curnode->childindex(1)); - } - curnode += curnode->childindex(0); - continue; - } - if(wpcachestack.empty()) break; - curnode = wpcachestack.pop(); - } - for(int i = lastwpcache; i < waypoints.length(); i++) { CHECKCLOSEST(i); } - return closest; - } - - void findwaypointswithin(const vec &pos, float mindist, float maxdist, vector<int> &results) - { - if(waypoints.empty()) return; - if(clearedwpcaches) buildwpcache(); - - float mindist2 = mindist*mindist, maxdist2 = maxdist*maxdist; - #define CHECKWITHIN(index) do { \ - int n = (index); \ - if(n < waypoints.length()) \ - { \ - const waypoint &w = waypoints[n]; \ - float dist = w.o.squaredist(pos); \ - if(dist > mindist2 && dist < maxdist2) results.add(n); \ - } \ - } while(0) - wpcache::node *curnode; - loop(which, NUMWPCACHES) if(wpcaches[which].firstwp >= 0) for(curnode = &wpcaches[which].nodes[0], wpcachestack.setsize(0);;) - { - int axis = curnode->axis(); - float dist1 = pos[axis] - curnode->split[0], dist2 = curnode->split[1] - pos[axis]; - if(dist1 >= maxdist) - { - if(dist2 < maxdist) - { - if(!curnode->isleaf(1)) { curnode += curnode->childindex(1); continue; } - CHECKWITHIN(curnode->childindex(1)); - } - } - else if(curnode->isleaf(0)) - { - CHECKWITHIN(curnode->childindex(0)); - if(dist2 < maxdist) - { - if(!curnode->isleaf(1)) { curnode += curnode->childindex(1); continue; } - CHECKWITHIN(curnode->childindex(1)); - } - } - else - { - if(dist2 < maxdist) - { - if(!curnode->isleaf(1)) wpcachestack.add(curnode + curnode->childindex(1)); - else CHECKWITHIN(curnode->childindex(1)); - } - curnode += curnode->childindex(0); - continue; - } - if(wpcachestack.empty()) break; - curnode = wpcachestack.pop(); - } - for(int i = lastwpcache; i < waypoints.length(); i++) { CHECKWITHIN(i); } - } - - void avoidset::avoidnear(void *owner, float above, const vec &pos, float limit) - { - if(ai::waypoints.empty()) return; - if(clearedwpcaches) buildwpcache(); - - float limit2 = limit*limit; - #define CHECKNEAR(index) do { \ - int n = (index); \ - if(n < ai::waypoints.length()) \ - { \ - const waypoint &w = ai::waypoints[n]; \ - if(w.o.squaredist(pos) < limit2) add(owner, above, n); \ - } \ - } while(0) - wpcache::node *curnode; - loop(which, NUMWPCACHES) if(wpcaches[which].firstwp >= 0) for(curnode = &wpcaches[which].nodes[0], wpcachestack.setsize(0);;) - { - int axis = curnode->axis(); - float dist1 = pos[axis] - curnode->split[0], dist2 = curnode->split[1] - pos[axis]; - if(dist1 >= limit) - { - if(dist2 < limit) - { - if(!curnode->isleaf(1)) { curnode += curnode->childindex(1); continue; } - CHECKNEAR(curnode->childindex(1)); - } - } - else if(curnode->isleaf(0)) - { - CHECKNEAR(curnode->childindex(0)); - if(dist2 < limit) - { - if(!curnode->isleaf(1)) { curnode += curnode->childindex(1); continue; } - CHECKNEAR(curnode->childindex(1)); - } - } - else - { - if(dist2 < limit) - { - if(!curnode->isleaf(1)) wpcachestack.add(curnode + curnode->childindex(1)); - else CHECKNEAR(curnode->childindex(1)); - } - curnode += curnode->childindex(0); - continue; - } - if(wpcachestack.empty()) break; - curnode = wpcachestack.pop(); - } - for(int i = lastwpcache; i < waypoints.length(); i++) { CHECKNEAR(i); } - } - - int avoidset::remap(fpsent *d, int n, vec &pos, bool retry) - { - if(!obstacles.empty()) - { - int cur = 0; - loopv(obstacles) - { - obstacle &ob = obstacles[i]; - int next = cur + ob.numwaypoints; - if(ob.owner != d) - { - for(; cur < next; cur++) if(waypoints[cur] == n) - { - if(ob.above < 0) return retry ? n : -1; - vec above(pos.x, pos.y, ob.above); - if(above.z-d->o.z >= ai::JUMPMAX) - return retry ? n : -1; // too much scotty - int node = closestwaypoint(above, ai::SIGHTMIN, true, d); - if(ai::iswaypoint(node) && node != n) - { // try to reroute above their head? - if(!find(node, d)) - { - pos = ai::waypoints[node].o; - return node; - } - else return retry ? n : -1; - } - else - { - vec old = d->o; - d->o = vec(above).add(vec(0, 0, d->eyeheight)); - bool col = collide(d, vec(0, 0, 1)); - d->o = old; - if(!col) - { - pos = above; - return n; - } - else return retry ? n : -1; - } - } - } - cur = next; - } - } - return n; - } - - static inline float heapscore(waypoint *q) { return q->score(); } - - bool route(fpsent *d, int node, int goal, vector<int> &route, const avoidset &obstacles, int retries) - { - if(waypoints.empty() || !iswaypoint(node) || !iswaypoint(goal) || goal == node || !waypoints[node].links[0]) - return false; - - static ushort routeid = 1; - static vector<waypoint *> queue; - - if(!routeid) - { - loopv(waypoints) waypoints[i].route = 0; - routeid = 1; - } - - if(d) - { - if(retries <= 1 && d->ai) loopi(ai::NUMPREVNODES) if(d->ai->prevnodes[i] != node && iswaypoint(d->ai->prevnodes[i])) - { - waypoints[d->ai->prevnodes[i]].route = routeid; - waypoints[d->ai->prevnodes[i]].curscore = -1; - waypoints[d->ai->prevnodes[i]].estscore = 0; - } + } + + struct wpcachestack + { + wpcache::node *node; + float tmin, tmax; + }; + + vector<wpcache::node *> wpcachestack; + + int closestwaypoint(const vec &pos, float mindist, bool links, fpsent *d) + { + if(waypoints.empty()) return -1; + if(clearedwpcaches) buildwpcache(); + + #define CHECKCLOSEST(index) do { \ + int n = (index); \ + if(n < waypoints.length()) \ + { \ + const waypoint &w = waypoints[n]; \ + if(!links || w.links[0]) \ + { \ + float dist = w.o.squaredist(pos); \ + if(dist < mindist*mindist) { closest = n; mindist = sqrtf(dist); } \ + } \ + } \ + } while(0) + int closest = -1; + wpcache::node *curnode; + loop(which, NUMWPCACHES) if(wpcaches[which].firstwp >= 0) for(curnode = &wpcaches[which].nodes[0], wpcachestack.setsize(0);;) + { + int axis = curnode->axis(); + float dist1 = pos[axis] - curnode->split[0], dist2 = curnode->split[1] - pos[axis]; + if(dist1 >= mindist) + { + if(dist2 < mindist) + { + if(!curnode->isleaf(1)) { curnode += curnode->childindex(1); continue; } + CHECKCLOSEST(curnode->childindex(1)); + } + } + else if(curnode->isleaf(0)) + { + CHECKCLOSEST(curnode->childindex(0)); + if(dist2 < mindist) + { + if(!curnode->isleaf(1)) { curnode += curnode->childindex(1); continue; } + CHECKCLOSEST(curnode->childindex(1)); + } + } + else + { + if(dist2 < mindist) + { + if(!curnode->isleaf(1)) wpcachestack.add(curnode + curnode->childindex(1)); + else CHECKCLOSEST(curnode->childindex(1)); + } + curnode += curnode->childindex(0); + continue; + } + if(wpcachestack.empty()) break; + curnode = wpcachestack.pop(); + } + for(int i = lastwpcache; i < waypoints.length(); i++) { CHECKCLOSEST(i); } + return closest; + } + + void findwaypointswithin(const vec &pos, float mindist, float maxdist, vector<int> &results) + { + if(waypoints.empty()) return; + if(clearedwpcaches) buildwpcache(); + + float mindist2 = mindist*mindist, maxdist2 = maxdist*maxdist; + #define CHECKWITHIN(index) do { \ + int n = (index); \ + if(n < waypoints.length()) \ + { \ + const waypoint &w = waypoints[n]; \ + float dist = w.o.squaredist(pos); \ + if(dist > mindist2 && dist < maxdist2) results.add(n); \ + } \ + } while(0) + wpcache::node *curnode; + loop(which, NUMWPCACHES) if(wpcaches[which].firstwp >= 0) for(curnode = &wpcaches[which].nodes[0], wpcachestack.setsize(0);;) + { + int axis = curnode->axis(); + float dist1 = pos[axis] - curnode->split[0], dist2 = curnode->split[1] - pos[axis]; + if(dist1 >= maxdist) + { + if(dist2 < maxdist) + { + if(!curnode->isleaf(1)) { curnode += curnode->childindex(1); continue; } + CHECKWITHIN(curnode->childindex(1)); + } + } + else if(curnode->isleaf(0)) + { + CHECKWITHIN(curnode->childindex(0)); + if(dist2 < maxdist) + { + if(!curnode->isleaf(1)) { curnode += curnode->childindex(1); continue; } + CHECKWITHIN(curnode->childindex(1)); + } + } + else + { + if(dist2 < maxdist) + { + if(!curnode->isleaf(1)) wpcachestack.add(curnode + curnode->childindex(1)); + else CHECKWITHIN(curnode->childindex(1)); + } + curnode += curnode->childindex(0); + continue; + } + if(wpcachestack.empty()) break; + curnode = wpcachestack.pop(); + } + for(int i = lastwpcache; i < waypoints.length(); i++) { CHECKWITHIN(i); } + } + + void avoidset::avoidnear(void *owner, float above, const vec &pos, float limit) + { + if(ai::waypoints.empty()) return; + if(clearedwpcaches) buildwpcache(); + + float limit2 = limit*limit; + #define CHECKNEAR(index) do { \ + int n = (index); \ + if(n < ai::waypoints.length()) \ + { \ + const waypoint &w = ai::waypoints[n]; \ + if(w.o.squaredist(pos) < limit2) add(owner, above, n); \ + } \ + } while(0) + wpcache::node *curnode; + loop(which, NUMWPCACHES) if(wpcaches[which].firstwp >= 0) for(curnode = &wpcaches[which].nodes[0], wpcachestack.setsize(0);;) + { + int axis = curnode->axis(); + float dist1 = pos[axis] - curnode->split[0], dist2 = curnode->split[1] - pos[axis]; + if(dist1 >= limit) + { + if(dist2 < limit) + { + if(!curnode->isleaf(1)) { curnode += curnode->childindex(1); continue; } + CHECKNEAR(curnode->childindex(1)); + } + } + else if(curnode->isleaf(0)) + { + CHECKNEAR(curnode->childindex(0)); + if(dist2 < limit) + { + if(!curnode->isleaf(1)) { curnode += curnode->childindex(1); continue; } + CHECKNEAR(curnode->childindex(1)); + } + } + else + { + if(dist2 < limit) + { + if(!curnode->isleaf(1)) wpcachestack.add(curnode + curnode->childindex(1)); + else CHECKNEAR(curnode->childindex(1)); + } + curnode += curnode->childindex(0); + continue; + } + if(wpcachestack.empty()) break; + curnode = wpcachestack.pop(); + } + for(int i = lastwpcache; i < waypoints.length(); i++) { CHECKNEAR(i); } + } + + int avoidset::remap(fpsent *d, int n, vec &pos, bool retry) + { + if(!obstacles.empty()) + { + int cur = 0; + loopv(obstacles) + { + obstacle &ob = obstacles[i]; + int next = cur + ob.numwaypoints; + if(ob.owner != d) + { + for(; cur < next; cur++) if(waypoints[cur] == n) + { + if(ob.above < 0) return retry ? n : -1; + vec above(pos.x, pos.y, ob.above); + if(above.z-d->o.z >= ai::JUMPMAX) + return retry ? n : -1; // too much scotty + int node = closestwaypoint(above, ai::SIGHTMIN, true, d); + if(ai::iswaypoint(node) && node != n) + { // try to reroute above their head? + if(!find(node, d)) + { + pos = ai::waypoints[node].o; + return node; + } + else return retry ? n : -1; + } + else + { + vec old = d->o; + d->o = vec(above).add(vec(0, 0, d->eyeheight)); + bool col = collide(d, vec(0, 0, 1)); + d->o = old; + if(!col) + { + pos = above; + return n; + } + else return retry ? n : -1; + } + } + } + cur = next; + } + } + return n; + } + + static inline float heapscore(waypoint *q) { return q->score(); } + + bool route(fpsent *d, int node, int goal, vector<int> &route, const avoidset &obstacles, int retries) + { + if(waypoints.empty() || !iswaypoint(node) || !iswaypoint(goal) || goal == node || !waypoints[node].links[0]) + return false; + + static ushort routeid = 1; + static vector<waypoint *> queue; + + if(!routeid) + { + loopv(waypoints) waypoints[i].route = 0; + routeid = 1; + } + + if(d) + { + if(retries <= 1 && d->ai) loopi(ai::NUMPREVNODES) if(d->ai->prevnodes[i] != node && iswaypoint(d->ai->prevnodes[i])) + { + waypoints[d->ai->prevnodes[i]].route = routeid; + waypoints[d->ai->prevnodes[i]].curscore = -1; + waypoints[d->ai->prevnodes[i]].estscore = 0; + } if(retries <= 0) { loopavoid(obstacles, d, @@ -453,99 +453,99 @@ namespace ai } }); } - } - - waypoints[node].route = routeid; - waypoints[node].curscore = waypoints[node].estscore = 0; - waypoints[node].prev = 0; - queue.setsize(0); - queue.add(&waypoints[node]); - route.setsize(0); - - int lowest = -1; - while(!queue.empty()) - { - waypoint &m = *queue.removeheap(); - float prevscore = m.curscore; - m.curscore = -1; - loopi(MAXWAYPOINTLINKS) - { - int link = m.links[i]; - if(!link) break; - if(iswaypoint(link) && (link == node || link == goal || waypoints[link].links[0])) - { - waypoint &n = waypoints[link]; - int weight = max(n.weight, 1); - float curscore = prevscore + n.o.dist(m.o)*weight; - if(n.route == routeid && curscore >= n.curscore) continue; - n.curscore = curscore; - n.prev = ushort(&m - &waypoints[0]); - if(n.route != routeid) - { - n.estscore = n.o.dist(waypoints[goal].o)*weight; - if(n.estscore <= WAYPOINTRADIUS*4 && (lowest < 0 || n.estscore <= waypoints[lowest].estscore)) - lowest = link; - n.route = routeid; - if(link == goal) goto foundgoal; - queue.addheap(&n); - } - else loopvj(queue) if(queue[j] == &n) { queue.upheap(j); break; } - } - } - } - foundgoal: - - routeid++; - - if(lowest >= 0) // otherwise nothing got there - { - for(waypoint *m = &waypoints[lowest]; m > &waypoints[0]; m = &waypoints[m->prev]) - route.add(m - &waypoints[0]); // just keep it stored backward - } - - return !route.empty(); - } - - VARF(dropwaypoints, 0, 0, 1, { player1->lastnode = -1; }); - - int addwaypoint(const vec &o, int weight = -1) - { - if(waypoints.length() > MAXWAYPOINTS) return -1; - int n = waypoints.length(); - waypoints.add(waypoint(o, weight >= 0 ? weight : getweight(o))); - invalidatewpcache(n); - return n; - } - - void linkwaypoint(waypoint &a, int n) - { - loopi(MAXWAYPOINTLINKS) - { - if(a.links[i] == n) return; - if(!a.links[i]) { a.links[i] = n; return; } - } - a.links[rnd(MAXWAYPOINTLINKS)] = n; - } - - string loadedwaypoints = ""; - - static inline bool shouldnavigate() - { - if(dropwaypoints) return true; - loopvrev(players) if(players[i]->aitype != AI_NONE) return true; - return false; - } - - static inline bool shoulddrop(fpsent *d) - { - return !d->ai && (dropwaypoints || !loadedwaypoints[0]); - } - - void inferwaypoints(fpsent *d, const vec &o, const vec &v, float mindist) - { - if(!shouldnavigate()) return; - if(shoulddrop(d)) - { + } + + waypoints[node].route = routeid; + waypoints[node].curscore = waypoints[node].estscore = 0; + waypoints[node].prev = 0; + queue.setsize(0); + queue.add(&waypoints[node]); + route.setsize(0); + + int lowest = -1; + while(!queue.empty()) + { + waypoint &m = *queue.removeheap(); + float prevscore = m.curscore; + m.curscore = -1; + loopi(MAXWAYPOINTLINKS) + { + int link = m.links[i]; + if(!link) break; + if(iswaypoint(link) && (link == node || link == goal || waypoints[link].links[0])) + { + waypoint &n = waypoints[link]; + int weight = max(n.weight, 1); + float curscore = prevscore + n.o.dist(m.o)*weight; + if(n.route == routeid && curscore >= n.curscore) continue; + n.curscore = curscore; + n.prev = ushort(&m - &waypoints[0]); + if(n.route != routeid) + { + n.estscore = n.o.dist(waypoints[goal].o)*weight; + if(n.estscore <= WAYPOINTRADIUS*4 && (lowest < 0 || n.estscore <= waypoints[lowest].estscore)) + lowest = link; + n.route = routeid; + if(link == goal) goto foundgoal; + queue.addheap(&n); + } + else loopvj(queue) if(queue[j] == &n) { queue.upheap(j); break; } + } + } + } + foundgoal: + + routeid++; + + if(lowest >= 0) // otherwise nothing got there + { + for(waypoint *m = &waypoints[lowest]; m > &waypoints[0]; m = &waypoints[m->prev]) + route.add(m - &waypoints[0]); // just keep it stored backward + } + + return !route.empty(); + } + + VARF(dropwaypoints, 0, 0, 1, { player1->lastnode = -1; }); + + int addwaypoint(const vec &o, int weight = -1) + { + if(waypoints.length() > MAXWAYPOINTS) return -1; + int n = waypoints.length(); + waypoints.add(waypoint(o, weight >= 0 ? weight : getweight(o))); + invalidatewpcache(n); + return n; + } + + void linkwaypoint(waypoint &a, int n) + { + loopi(MAXWAYPOINTLINKS) + { + if(a.links[i] == n) return; + if(!a.links[i]) { a.links[i] = n; return; } + } + a.links[rnd(MAXWAYPOINTLINKS)] = n; + } + + string loadedwaypoints = ""; + + static inline bool shouldnavigate() + { + if(dropwaypoints) return true; + loopvrev(players) if(players[i]->aitype != AI_NONE) return true; + return false; + } + + static inline bool shoulddrop(fpsent *d) + { + return !d->ai && (dropwaypoints || !loadedwaypoints[0]); + } + + void inferwaypoints(fpsent *d, const vec &o, const vec &v, float mindist) + { + if(!shouldnavigate()) return; + if(shoulddrop(d)) + { if(waypoints.empty()) seedwaypoints(); int from = closestwaypoint(o, mindist, false), to = closestwaypoint(v, mindist, false); if(!iswaypoint(from)) from = addwaypoint(o); @@ -560,249 +560,249 @@ namespace ai } } else d->lastnode = closestwaypoint(v, WAYPOINTRADIUS*2, false, d); - } - - void navigate(fpsent *d) - { - vec v(d->feetpos()); - if(d->state != CS_ALIVE) { d->lastnode = -1; return; } - bool dropping = shoulddrop(d); - int mat = lookupmaterial(v); - if((mat&MATF_CLIP) == MAT_CLIP || (mat&MATF_VOLUME) == MAT_LAVA || mat&MAT_DEATH) dropping = false; - float dist = dropping ? WAYPOINTRADIUS : (d->ai ? WAYPOINTRADIUS : SIGHTMIN); - int curnode = closestwaypoint(v, dist, false, d), prevnode = d->lastnode; - if(!iswaypoint(curnode) && dropping) - { + } + + void navigate(fpsent *d) + { + vec v(d->feetpos()); + if(d->state != CS_ALIVE) { d->lastnode = -1; return; } + bool dropping = shoulddrop(d); + int mat = lookupmaterial(v); + if((mat&MATF_CLIP) == MAT_CLIP || (mat&MATF_VOLUME) == MAT_LAVA || mat&MAT_DEATH) dropping = false; + float dist = dropping ? WAYPOINTRADIUS : (d->ai ? WAYPOINTRADIUS : SIGHTMIN); + int curnode = closestwaypoint(v, dist, false, d), prevnode = d->lastnode; + if(!iswaypoint(curnode) && dropping) + { if(waypoints.empty()) seedwaypoints(); - curnode = addwaypoint(v); - } - if(iswaypoint(curnode)) - { - if(dropping && d->lastnode != curnode && iswaypoint(d->lastnode)) - { - linkwaypoint(waypoints[d->lastnode], curnode); - if(!d->timeinair) linkwaypoint(waypoints[curnode], d->lastnode); - } - d->lastnode = curnode; - if(d->ai && iswaypoint(prevnode) && d->lastnode != prevnode) d->ai->addprevnode(prevnode); - } - else if(!iswaypoint(d->lastnode) || waypoints[d->lastnode].o.squaredist(v) > SIGHTMIN*SIGHTMIN) + curnode = addwaypoint(v); + } + if(iswaypoint(curnode)) + { + if(dropping && d->lastnode != curnode && iswaypoint(d->lastnode)) + { + linkwaypoint(waypoints[d->lastnode], curnode); + if(!d->timeinair) linkwaypoint(waypoints[curnode], d->lastnode); + } + d->lastnode = curnode; + if(d->ai && iswaypoint(prevnode) && d->lastnode != prevnode) d->ai->addprevnode(prevnode); + } + else if(!iswaypoint(d->lastnode) || waypoints[d->lastnode].o.squaredist(v) > SIGHTMIN*SIGHTMIN) d->lastnode = closestwaypoint(v, SIGHTMAX, false, d); - } - - void navigate() - { - if(shouldnavigate()) loopv(players) ai::navigate(players[i]); - if(invalidatedwpcaches) clearwpcache(false); - } - - void clearwaypoints(bool full) - { - waypoints.setsize(0); - clearwpcache(); - if(full) - { - loadedwaypoints[0] = '\0'; - dropwaypoints = 0; - } - } - ICOMMAND(clearwaypoints, "", (), clearwaypoints()); - - void seedwaypoints() - { - if(waypoints.empty()) addwaypoint(vec(0, 0, 0)); - loopv(entities::ents) - { - extentity &e = *entities::ents[i]; - switch(e.type) - { - case PLAYERSTART: case TELEPORT: case JUMPPAD: - addwaypoint(e.o); - break; - default: - if(e.type >= I_SHELLS && e.type <= I_QUAD) addwaypoint(e.o); - break; - } - } - } - - void remapwaypoints() - { - vector<ushort> remap; - int total = 0; - loopv(waypoints) remap.add(waypoints[i].links[1] == 0xFFFF ? 0 : total++); - total = 0; - loopvj(waypoints) - { - if(waypoints[j].links[1] == 0xFFFF) continue; - waypoint &w = waypoints[total]; - if(j != total) w = waypoints[j]; - int k = 0; - loopi(MAXWAYPOINTLINKS) - { - int link = w.links[i]; - if(!link) break; - if((w.links[k] = remap[link])) k++; - } - if(k < MAXWAYPOINTLINKS) w.links[k] = 0; - total++; - } - waypoints.setsize(total); - } - - bool cleanwaypoints() - { - int cleared = 0; - for(int i = 1; i < waypoints.length(); i++) - { - waypoint &w = waypoints[i]; - if(clipped(w.o)) - { - w.links[0] = 0; - w.links[1] = 0xFFFF; - cleared++; - } - } - if(cleared) - { - player1->lastnode = -1; - loopv(players) if(players[i]) players[i]->lastnode = -1; - remapwaypoints(); - clearwpcache(); - return true; - } - return false; - } - - bool getwaypointfile(const char *mname, char *wptname) - { - if(!mname || !*mname) mname = getclientmap(); - if(!*mname) return false; - - string pakname, mapname, cfgname; - getmapfilenames(mname, NULL, pakname, mapname, cfgname); - nformatstring(wptname, MAXSTRLEN, "packages/%s.wpt", mapname); - path(wptname); - return true; - } - - void loadwaypoints(bool force, const char *mname) - { - string wptname; - if(!getwaypointfile(mname, wptname)) return; - if(!force && (waypoints.length() || !strcmp(loadedwaypoints, wptname))) return; - - stream *f = opengzfile(wptname, "rb"); - if(!f) return; - char magic[4]; - if(f->read(magic, 4) < 4 || memcmp(magic, "OWPT", 4)) { delete f; return; } - - copystring(loadedwaypoints, wptname); - - waypoints.setsize(0); - waypoints.add(vec(0, 0, 0)); - ushort numwp = f->getlil<ushort>(); - loopi(numwp) - { - if(f->end()) break; - vec o; - o.x = f->getlil<float>(); - o.y = f->getlil<float>(); - o.z = f->getlil<float>(); - waypoint &w = waypoints.add(waypoint(o, getweight(o))); - int numlinks = f->getchar(), k = 0; - loopi(numlinks) - { - if((w.links[k] = f->getlil<ushort>())) - { - if(++k >= MAXWAYPOINTLINKS) break; - } - } - } - - delete f; - conoutf("loaded %d waypoints from %s", numwp, wptname); - - if(!cleanwaypoints()) clearwpcache(); - } - ICOMMAND(loadwaypoints, "s", (char *mname), loadwaypoints(true, mname)); - - void savewaypoints(bool force, const char *mname) - { - if((!dropwaypoints && !force) || waypoints.empty()) return; - - string wptname; - if(!getwaypointfile(mname, wptname)) return; - - stream *f = opengzfile(wptname, "wb"); - if(!f) return; - f->write("OWPT", 4); - f->putlil<ushort>(waypoints.length()-1); - for(int i = 1; i < waypoints.length(); i++) - { - waypoint &w = waypoints[i]; - f->putlil<float>(w.o.x); - f->putlil<float>(w.o.y); - f->putlil<float>(w.o.z); - int numlinks = 0; - loopj(MAXWAYPOINTLINKS) { if(!w.links[j]) break; numlinks++; } - f->putchar(numlinks); - loopj(numlinks) f->putlil<ushort>(w.links[j]); - } - - delete f; - conoutf("saved %d waypoints to %s", waypoints.length()-1, wptname); - } - - ICOMMAND(savewaypoints, "s", (char *mname), savewaypoints(true, mname)); - - void delselwaypoints() - { - if(noedit(true)) return; - vec o = vec(sel.o).sub(0.1f), s = vec(sel.s).mul(sel.grid).add(o).add(0.1f); - int cleared = 0; - for(int i = 1; i < waypoints.length(); i++) - { - waypoint &w = waypoints[i]; - if(w.o.x >= o.x && w.o.x <= s.x && w.o.y >= o.y && w.o.y <= s.y && w.o.z >= o.z && w.o.z <= s.z) - { - w.links[0] = 0; - w.links[1] = 0xFFFF; - cleared++; - } - } - if(cleared) - { - player1->lastnode = -1; - remapwaypoints(); - clearwpcache(); - } - } - COMMAND(delselwaypoints, ""); - - void movewaypoints(const vec &d) - { - if(noedit(true)) return; - int worldsize = getworldsize(); - if(d.x < -worldsize || d.x > worldsize || d.y < -worldsize || d.y > worldsize || d.z < -worldsize || d.z > worldsize) - { - clearwaypoints(); - return; - } - int cleared = 0; - for(int i = 1; i < waypoints.length(); i++) - { - waypoint &w = waypoints[i]; - w.o.add(d); - if(!insideworld(w.o)) { w.links[0] = 0; w.links[1] = 0xFFFF; cleared++; } - } - if(cleared) - { - player1->lastnode = -1; - remapwaypoints(); - } - clearwpcache(); - } - ICOMMAND(movewaypoints, "iii", (int *dx, int *dy, int *dz), movewaypoints(vec(*dx, *dy, *dz))); + } + + void navigate() + { + if(shouldnavigate()) loopv(players) ai::navigate(players[i]); + if(invalidatedwpcaches) clearwpcache(false); + } + + void clearwaypoints(bool full) + { + waypoints.setsize(0); + clearwpcache(); + if(full) + { + loadedwaypoints[0] = '\0'; + dropwaypoints = 0; + } + } + ICOMMAND(clearwaypoints, "", (), clearwaypoints()); + + void seedwaypoints() + { + if(waypoints.empty()) addwaypoint(vec(0, 0, 0)); + loopv(entities::ents) + { + extentity &e = *entities::ents[i]; + switch(e.type) + { + case PLAYERSTART: case TELEPORT: case JUMPPAD: + addwaypoint(e.o); + break; + default: + if(e.type >= I_SHELLS && e.type <= I_QUAD) addwaypoint(e.o); + break; + } + } + } + + void remapwaypoints() + { + vector<ushort> remap; + int total = 0; + loopv(waypoints) remap.add(waypoints[i].links[1] == 0xFFFF ? 0 : total++); + total = 0; + loopvj(waypoints) + { + if(waypoints[j].links[1] == 0xFFFF) continue; + waypoint &w = waypoints[total]; + if(j != total) w = waypoints[j]; + int k = 0; + loopi(MAXWAYPOINTLINKS) + { + int link = w.links[i]; + if(!link) break; + if((w.links[k] = remap[link])) k++; + } + if(k < MAXWAYPOINTLINKS) w.links[k] = 0; + total++; + } + waypoints.setsize(total); + } + + bool cleanwaypoints() + { + int cleared = 0; + for(int i = 1; i < waypoints.length(); i++) + { + waypoint &w = waypoints[i]; + if(clipped(w.o)) + { + w.links[0] = 0; + w.links[1] = 0xFFFF; + cleared++; + } + } + if(cleared) + { + player1->lastnode = -1; + loopv(players) if(players[i]) players[i]->lastnode = -1; + remapwaypoints(); + clearwpcache(); + return true; + } + return false; + } + + bool getwaypointfile(const char *mname, char *wptname) + { + if(!mname || !*mname) mname = getclientmap(); + if(!*mname) return false; + + string pakname, mapname, cfgname; + getmapfilenames(mname, NULL, pakname, mapname, cfgname); + nformatstring(wptname, MAXSTRLEN, "packages/%s.wpt", mapname); + path(wptname); + return true; + } + + void loadwaypoints(bool force, const char *mname) + { + string wptname; + if(!getwaypointfile(mname, wptname)) return; + if(!force && (waypoints.length() || !strcmp(loadedwaypoints, wptname))) return; + + stream *f = opengzfile(wptname, "rb"); + if(!f) return; + char magic[4]; + if(f->read(magic, 4) < 4 || memcmp(magic, "OWPT", 4)) { delete f; return; } + + copystring(loadedwaypoints, wptname); + + waypoints.setsize(0); + waypoints.add(vec(0, 0, 0)); + ushort numwp = f->getlil<ushort>(); + loopi(numwp) + { + if(f->end()) break; + vec o; + o.x = f->getlil<float>(); + o.y = f->getlil<float>(); + o.z = f->getlil<float>(); + waypoint &w = waypoints.add(waypoint(o, getweight(o))); + int numlinks = f->getchar(), k = 0; + loopi(numlinks) + { + if((w.links[k] = f->getlil<ushort>())) + { + if(++k >= MAXWAYPOINTLINKS) break; + } + } + } + + delete f; + conoutf("loaded %d waypoints from %s", numwp, wptname); + + if(!cleanwaypoints()) clearwpcache(); + } + ICOMMAND(loadwaypoints, "s", (char *mname), loadwaypoints(true, mname)); + + void savewaypoints(bool force, const char *mname) + { + if((!dropwaypoints && !force) || waypoints.empty()) return; + + string wptname; + if(!getwaypointfile(mname, wptname)) return; + + stream *f = opengzfile(wptname, "wb"); + if(!f) return; + f->write("OWPT", 4); + f->putlil<ushort>(waypoints.length()-1); + for(int i = 1; i < waypoints.length(); i++) + { + waypoint &w = waypoints[i]; + f->putlil<float>(w.o.x); + f->putlil<float>(w.o.y); + f->putlil<float>(w.o.z); + int numlinks = 0; + loopj(MAXWAYPOINTLINKS) { if(!w.links[j]) break; numlinks++; } + f->putchar(numlinks); + loopj(numlinks) f->putlil<ushort>(w.links[j]); + } + + delete f; + conoutf("saved %d waypoints to %s", waypoints.length()-1, wptname); + } + + ICOMMAND(savewaypoints, "s", (char *mname), savewaypoints(true, mname)); + + void delselwaypoints() + { + if(noedit(true)) return; + vec o = vec(sel.o).sub(0.1f), s = vec(sel.s).mul(sel.grid).add(o).add(0.1f); + int cleared = 0; + for(int i = 1; i < waypoints.length(); i++) + { + waypoint &w = waypoints[i]; + if(w.o.x >= o.x && w.o.x <= s.x && w.o.y >= o.y && w.o.y <= s.y && w.o.z >= o.z && w.o.z <= s.z) + { + w.links[0] = 0; + w.links[1] = 0xFFFF; + cleared++; + } + } + if(cleared) + { + player1->lastnode = -1; + remapwaypoints(); + clearwpcache(); + } + } + COMMAND(delselwaypoints, ""); + + void movewaypoints(const vec &d) + { + if(noedit(true)) return; + int worldsize = getworldsize(); + if(d.x < -worldsize || d.x > worldsize || d.y < -worldsize || d.y > worldsize || d.z < -worldsize || d.z > worldsize) + { + clearwaypoints(); + return; + } + int cleared = 0; + for(int i = 1; i < waypoints.length(); i++) + { + waypoint &w = waypoints[i]; + w.o.add(d); + if(!insideworld(w.o)) { w.links[0] = 0; w.links[1] = 0xFFFF; cleared++; } + } + if(cleared) + { + player1->lastnode = -1; + remapwaypoints(); + } + clearwpcache(); + } + ICOMMAND(movewaypoints, "iii", (int *dx, int *dy, int *dz), movewaypoints(vec(*dx, *dy, *dz))); } diff --git a/src/fpsgame/weapon.cpp b/src/fpsgame/weapon.cpp index d911aba..1bdb302 100644 --- a/src/fpsgame/weapon.cpp +++ b/src/fpsgame/weapon.cpp @@ -3,975 +3,974 @@ namespace game { - static const int OFFSETMILLIS = 500; - vec rays[MAXRAYS]; - - struct hitmsg - { - int target, lifesequence, info1, info2; - ivec dir; - }; - vector<hitmsg> hits; - - ICOMMAND(getweapon, "", (), intret(player1->gunselect)); - - void gunselect(int gun, fpsent *d) - { - if(gun!=d->gunselect) - { - addmsg(N_GUNSELECT, "rci", d, gun); - playsound(S_WEAPLOAD, d == player1 ? NULL : &d->o); - } - d->gunselect = gun; - } - - void nextweapon(int dir, bool force = false) - { - if(player1->state!=CS_ALIVE) return; - dir = (dir < 0 ? NUMGUNS-1 : 1); - int gun = player1->gunselect; - loopi(NUMGUNS) - { - gun = (gun + dir)%NUMGUNS; - if(force || player1->ammo[gun]) break; - } - if(gun != player1->gunselect) gunselect(gun, player1); - else playsound(S_NOAMMO); - } - ICOMMAND(nextweapon, "ii", (int *dir, int *force), nextweapon(*dir, *force!=0)); - - int getweapon(const char *name) - { - const char *abbrevs[] = { "FI", "SG", "CG", "RL", "RI", "GL", "PI" }; - if(isdigit(name[0])) return parseint(name); - else loopi(sizeof(abbrevs)/sizeof(abbrevs[0])) if(!strcasecmp(abbrevs[i], name)) return i; - return -1; - } - - void setweapon(const char *name, bool force = false) - { - int gun = getweapon(name); - if(player1->state!=CS_ALIVE || gun<GUN_FIST || gun>GUN_PISTOL) return; - if(force || player1->ammo[gun]) gunselect(gun, player1); - else playsound(S_NOAMMO); - } - ICOMMAND(setweapon, "si", (char *name, int *force), setweapon(name, *force!=0)); - - void cycleweapon(int numguns, int *guns, bool force = false) - { - if(numguns<=0 || player1->state!=CS_ALIVE) return; - int offset = 0; - loopi(numguns) if(guns[i] == player1->gunselect) { offset = i+1; break; } - loopi(numguns) - { - int gun = guns[(i+offset)%numguns]; - if(gun>=0 && gun<NUMGUNS && (force || player1->ammo[gun])) - { - gunselect(gun, player1); - return; - } - } - playsound(S_NOAMMO); - } - ICOMMAND(cycleweapon, "V", (tagval *args, int numargs), - { - int numguns = min(numargs, 7); - int guns[7]; - loopi(numguns) guns[i] = getweapon(args[i].getstr()); - cycleweapon(numguns, guns); - }); - - void weaponswitch(fpsent *d) - { - if(d->state!=CS_ALIVE) return; - int s = d->gunselect; - if (s!=GUN_CG && d->ammo[GUN_CG]) s = GUN_CG; - else if(s!=GUN_RL && d->ammo[GUN_RL]) s = GUN_RL; - else if(s!=GUN_SG && d->ammo[GUN_SG]) s = GUN_SG; - else if(s!=GUN_RIFLE && d->ammo[GUN_RIFLE]) s = GUN_RIFLE; - else if(s!=GUN_GL && d->ammo[GUN_GL]) s = GUN_GL; - else if(s!=GUN_PISTOL && d->ammo[GUN_PISTOL]) s = GUN_PISTOL; - else s = GUN_FIST; - - gunselect(s, d); - } - - ICOMMAND(weapon, "V", (tagval *args, int numargs), - { - if(player1->state!=CS_ALIVE) return; - loopi(7) - { - const char *name = i < numargs ? args[i].getstr() : ""; - if(name[0]) - { - int gun = getweapon(name); - if(gun >= GUN_FIST && gun <= GUN_PISTOL && gun != player1->gunselect && player1->ammo[gun]) { gunselect(gun, player1); return; } - } else { weaponswitch(player1); return; } - } - playsound(S_NOAMMO); - }); - - void offsetray(const vec &from, const vec &to, int spread, float range, vec &dest) - { - vec offset; - do offset = vec(rndscale(1), rndscale(1), rndscale(1)).sub(0.5f); - while(offset.squaredlen() > 0.5f*0.5f); - offset.mul((to.dist(from)/1024)*spread); - offset.z /= 2; - dest = vec(offset).add(to); - if(dest != from) - { - vec dir = vec(dest).sub(from).normalize(); - raycubepos(from, dir, dest, range, RAY_CLIPMAT|RAY_ALPHAPOLY); - } - } - - void createrays(int gun, const vec &from, const vec &to) // create random spread of rays - { - loopi(guns[gun].rays) offsetray(from, to, guns[gun].spread, guns[gun].range, rays[i]); - } - - enum { BNC_GRENADE }; - - struct bouncer : physent - { - int lifetime, bounces; - float lastyaw, roll; - bool local; - fpsent *owner; - int bouncetype, variant; - vec offset; - int offsetmillis; - float offsetheight; - int id; - entitylight light; - - bouncer() : bounces(0), roll(0), variant(0) - { - type = ENT_BOUNCE; - } - - vec offsetpos() - { - vec pos(o); - if(offsetmillis > 0) - { - pos.add(vec(offset).mul(offsetmillis/float(OFFSETMILLIS))); - if(offsetheight >= 0) pos.z = max(pos.z, o.z - max(offsetheight - eyeheight, 0.0f)); - } - return pos; - } - - void limitoffset() - { - if(bouncetype == BNC_GRENADE && offsetmillis > 0 && offset.z < 0) - offsetheight = raycube(vec(o.x + offset.x, o.y + offset.y, o.z), vec(0, 0, -1), -offset.z); - else offsetheight = -1; - } - }; - - vector<bouncer *> bouncers; - - vec hudgunorigin(int gun, const vec &from, const vec &to, fpsent *d); - - void newbouncer(const vec &from, const vec &to, bool local, int id, fpsent *owner, int type, int lifetime, int speed, entitylight *light = NULL) - { - bouncer &bnc = *bouncers.add(new bouncer); - bnc.o = from; - bnc.radius = bnc.xradius = bnc.yradius = 1.5f; - bnc.eyeheight = bnc.radius; - bnc.aboveeye = bnc.radius; - bnc.lifetime = lifetime; - bnc.local = local; - bnc.owner = owner; - bnc.bouncetype = type; - bnc.id = local ? lastmillis : id; - if(light) bnc.light = *light; - - bnc.collidetype = COLLIDE_ELLIPSE_PRECISE; - - vec dir(to); - dir.sub(from).safenormalize(); - bnc.vel = dir; - bnc.vel.mul(speed); - - avoidcollision(&bnc, dir, owner, 0.1f); - - if(type==BNC_GRENADE) - { - bnc.offset = hudgunorigin(GUN_GL, from, to, owner); - if(owner==followingplayer(player1) && !isthirdperson()) bnc.offset.sub(owner->o).rescale(16).add(owner->o); - } - else bnc.offset = from; - bnc.offset.sub(bnc.o); - bnc.offsetmillis = OFFSETMILLIS; - bnc.limitoffset(); - - bnc.resetinterp(); - } - - void bounced(physent *d, const vec &surface) - { - if(d->type != ENT_BOUNCE) return; - bouncer *b = (bouncer *)d; - if(b->bounces >= 2) return; - b->bounces++; - adddecal(DECAL_BLOOD, vec(b->o).sub(vec(surface).mul(b->radius)), surface, 2.96f/b->bounces, bvec(0x60, 0xFF, 0xFF), rnd(4)); - } - - void updatebouncers(int time) - { - loopv(bouncers) - { - bouncer &bnc = *bouncers[i]; - if(bnc.bouncetype==BNC_GRENADE && bnc.vel.magnitude() > 50.0f) - { - vec pos = bnc.offsetpos(); - regular_particle_splash(PART_SMOKE, 1, 150, pos, 0x404040, 2.4f, 50, -20); - } - vec old(bnc.o); - bool stopped = false; - if(bnc.bouncetype==BNC_GRENADE) stopped = bounce(&bnc, 0.6f, 0.5f, 0.8f) || (bnc.lifetime -= time)<0; - - if(stopped) - { - if(bnc.bouncetype==BNC_GRENADE) - { - int qdam = guns[GUN_GL].damage*(bnc.owner->quadmillis ? 4 : 1); - hits.setsize(0); - explode(bnc.local, bnc.owner, bnc.o, NULL, qdam, GUN_GL); - adddecal(DECAL_SCORCH, bnc.o, vec(0, 0, 1), guns[GUN_GL].exprad/2); - if(bnc.local) - addmsg(N_EXPLODE, "rci3iv", bnc.owner, lastmillis-maptime, GUN_GL, bnc.id-maptime, - hits.length(), hits.length()*sizeof(hitmsg)/sizeof(int), hits.getbuf()); - } - delete bouncers.remove(i--); - } - else - { - bnc.roll += old.sub(bnc.o).magnitude()/(4*RAD); - bnc.offsetmillis = max(bnc.offsetmillis-time, 0); - bnc.limitoffset(); - } - } - } - - void removebouncers(fpsent *owner) - { - loopv(bouncers) if(bouncers[i]->owner==owner) { delete bouncers[i]; bouncers.remove(i--); } - } - - void clearbouncers() { bouncers.deletecontents(); } - - struct projectile - { - vec dir, o, to, offset; - float speed; - fpsent *owner; - int gun; - bool local; - int offsetmillis; - int id; - entitylight light; - }; - vector<projectile> projs; - - void clearprojectiles() { projs.shrink(0); } - - void newprojectile(const vec &from, const vec &to, float speed, bool local, int id, fpsent *owner, int gun) - { - projectile &p = projs.add(); - p.dir = vec(to).sub(from).safenormalize(); - p.o = from; - p.to = to; - p.offset = hudgunorigin(gun, from, to, owner); - p.offset.sub(from); - p.speed = speed; - p.local = local; - p.owner = owner; - p.gun = gun; - p.offsetmillis = OFFSETMILLIS; - p.id = local ? lastmillis : id; - } - - void removeprojectiles(fpsent *owner) - { - // can't use loopv here due to strange GCC optimizer bug - int len = projs.length(); - loopi(len) if(projs[i].owner==owner) { projs.remove(i--); len--; } - } - - VARP(blood, 0, 1, 1); - - void damageeffect(int damage, fpsent *d, bool thirdperson) - { - vec p = d->o; - p.z += 0.6f*(d->eyeheight + d->aboveeye) - d->eyeheight; - if(blood) particle_splash(PART_BLOOD, damage/10, 1000, p, 0x60FFFF, 2.96f); - if(thirdperson) - { - defformatstring(ds, "%d", damage); - particle_textcopy(d->abovehead(), ds, PART_TEXT, 2000, 0xFF4B19, 4.0f, -8); - } - } - - void spawnbouncer(const vec &p, const vec &vel, fpsent *d, int type, entitylight *light = NULL) - { - vec to(rnd(100)-50, rnd(100)-50, rnd(100)-50); - if(to.iszero()) to.z += 1; - to.normalize(); - to.add(p); - newbouncer(p, to, true, 0, d, type, rnd(1000)+1000, rnd(100)+20, light); - } - - void hit(int damage, dynent *d, fpsent *at, const vec &vel, int gun, float info1, int info2 = 1) - { - if(at==player1 && d!=at) - { - extern int hitsound; - if(hitsound && lasthit != lastmillis) playsound(S_HIT); - lasthit = lastmillis; - } - - fpsent *f = (fpsent *)d; - - f->lastpain = lastmillis; - if(at->type==ENT_PLAYER && !isteam(at->team, f->team)) at->totaldamage += damage; - - if(f->type==ENT_AI || !m_mp(gamemode) || f==at) f->hitpush(damage, vel, at, gun); - - pwhit(gun, damage); - - if(!m_mp(gamemode)) damaged(damage, f, at); - else - { - hitmsg &h = hits.add(); - h.target = f->clientnum; - h.lifesequence = f->lifesequence; - h.info1 = int(info1*DMF); - h.info2 = info2; - h.dir = f==at ? ivec(0, 0, 0) : ivec(vec(vel).mul(DNF)); - if(at==player1) - { - damageeffect(damage, f); - if(f==player1) - { - damageblend(damage); - damagecompass(damage, at ? at->o : f->o); - playsound(S_PAIN6); - } - else playsound(S_PAIN1+rnd(5), &f->o); - } - } - } - - void hitpush(int damage, dynent *d, fpsent *at, vec &from, vec &to, int gun, int rays) - { - hit(damage, d, at, vec(to).sub(from).safenormalize(), gun, from.dist(to), rays); - } - - float projdist(dynent *o, vec &dir, const vec &v) - { - vec middle = o->o; - middle.z += (o->aboveeye-o->eyeheight)/2; - float dist = middle.dist(v, dir); - dir.div(dist); - if(dist<0) dist = 0; - return dist; - } - - void radialeffect(dynent *o, const vec &v, int qdam, fpsent *at, int gun) - { - if(o->state!=CS_ALIVE) return; - vec dir; - float dist = projdist(o, dir, v); - if(dist<guns[gun].exprad) - { - int damage = (int)(qdam*(1-dist/EXP_DISTSCALE/guns[gun].exprad)); - if(o==at) damage /= EXP_SELFDAMDIV; - hit(damage, o, at, dir, gun, dist); - } - } - - FVARP(explodebright, 0, 1, 1); - - void explode(bool local, fpsent *owner, const vec &v, dynent *safe, int damage, int gun) - { - particle_splash(PART_SPARK, 200, 300, v, 0xB49B4B, 0.24f); - playsound(gun!=GUN_GL ? S_RLHIT : S_FEXPLODE, &v); - int color = gun!=GUN_GL ? 0xFF8080 : 0x80FFFF; - if((gun==GUN_RL || gun==GUN_GL) && explodebright < 1) color = vec::hexcolor(color).mul(explodebright).tohexcolor(); - particle_fireball(v, guns[gun].exprad, gun!=GUN_GL ? PART_EXPLOSION : PART_EXPLOSION_BLUE, gun!=GUN_GL ? -1 : int((guns[gun].exprad-4.0f)*15), color, 4.0f); - if(gun==GUN_RL) adddynlight(v, 1.15f*guns[gun].exprad, vec(2, 1.5f, 1), 700, 100, 0, guns[gun].exprad/2, vec(1, 0.75f, 0.5f)); - else if(gun==GUN_GL) adddynlight(v, 1.15f*guns[gun].exprad, vec(0.5f, 1.5f, 2), 600, 100, 0, 8, vec(0.25f, 1, 1)); - else adddynlight(v, 1.15f*guns[gun].exprad, vec(2, 1.5f, 1), 700, 100); - if(!local) return; - int numdyn = numdynents(); - loopi(numdyn) - { - dynent *o = iterdynents(i); - if(o->o.reject(v, o->radius + guns[gun].exprad) || o==safe) continue; - radialeffect(o, v, damage, owner, gun); - } - } - - void projsplash(projectile &p, vec &v, dynent *safe, int damage) - { - if(guns[p.gun].part) - { - particle_splash(PART_SPARK, 100, 200, v, 0xB49B4B, 0.24f); - playsound(S_FEXPLODE, &v); - // no push? - } - else - { - explode(p.local, p.owner, v, safe, damage, GUN_RL); - adddecal(DECAL_SCORCH, v, vec(p.dir).neg(), guns[p.gun].exprad/2); - } - } - - void explodeeffects(int gun, fpsent *d, bool local, int id) - { - if(local) return; - switch(gun) - { - case GUN_RL: - loopv(projs) - { - projectile &p = projs[i]; - if(p.gun == gun && p.owner == d && p.id == id && !p.local) - { - vec pos(p.o); - pos.add(vec(p.offset).mul(p.offsetmillis/float(OFFSETMILLIS))); - explode(p.local, p.owner, pos, NULL, 0, GUN_RL); - adddecal(DECAL_SCORCH, pos, vec(p.dir).neg(), guns[gun].exprad/2); - projs.remove(i); - break; - } - } - break; - case GUN_GL: - loopv(bouncers) - { - bouncer &b = *bouncers[i]; - if(b.bouncetype == BNC_GRENADE && b.owner == d && b.id == id && !b.local) - { - vec pos = b.offsetpos(); - explode(b.local, b.owner, pos, NULL, 0, GUN_GL); - adddecal(DECAL_SCORCH, pos, vec(0, 0, 1), guns[gun].exprad/2); - delete bouncers.remove(i); - break; - } - } - break; - default: - break; - } - } - - bool projdamage(dynent *o, projectile &p, vec &v, int qdam) - { - if(o->state!=CS_ALIVE) return false; - if(!intersect(o, p.o, v)) return false; - projsplash(p, v, o, qdam); - vec dir; - projdist(o, dir, v); - hit(qdam, o, p.owner, dir, p.gun, 0); - return true; - } - - void updateprojectiles(int time) - { - loopv(projs) - { - projectile &p = projs[i]; - p.offsetmillis = max(p.offsetmillis-time, 0); - int qdam = guns[p.gun].damage*(p.owner->quadmillis ? 4 : 1); - vec dv; - float dist = p.to.dist(p.o, dv); - dv.mul(time/max(dist*1000/p.speed, float(time))); - vec v = vec(p.o).add(dv); - bool exploded = false; - hits.setsize(0); - if(p.local) - { - vec halfdv = vec(dv).mul(0.5f), bo = vec(p.o).add(halfdv); - float br = max(fabs(halfdv.x), fabs(halfdv.y)) + 1; - loopj(numdynents()) - { - dynent *o = iterdynents(j); - if(p.owner==o || o->o.reject(bo, o->radius + br)) continue; - if(projdamage(o, p, v, qdam)) { exploded = true; break; } - } - } - if(!exploded) - { - if(dist<4) - { - if(p.o!=p.to) // if original target was moving, reevaluate endpoint - { - if(raycubepos(p.o, p.dir, p.to, 0, RAY_CLIPMAT|RAY_ALPHAPOLY)>=4) continue; - } - projsplash(p, v, NULL, qdam); - exploded = true; - } - else - { - vec pos(v); - pos.add(vec(p.offset).mul(p.offsetmillis/float(OFFSETMILLIS))); - regular_particle_splash(PART_SMOKE, 2, 300, pos, 0x404040, 2.4f, 50, -20); - } - } - if(exploded) - { - if(p.local) - addmsg(N_EXPLODE, "rci3iv", p.owner, lastmillis-maptime, p.gun, p.id-maptime, - hits.length(), hits.length()*sizeof(hitmsg)/sizeof(int), hits.getbuf()); - projs.remove(i--); - } - else p.o = v; - } - } - - extern int chainsawhudgun; - - VARP(muzzleflash, 0, 1, 1); - VARP(muzzlelight, 0, 1, 1); - - void shoteffects(int gun, const vec &from, const vec &to, fpsent *d, bool local, int id, int prevaction) // create visual effect from a shot - { - int sound = guns[gun].sound, pspeed = 25; - switch(gun) - { - case GUN_FIST: - if(d->type==ENT_PLAYER && chainsawhudgun) sound = S_CHAINSAW_ATTACK; - break; - - case GUN_SG: - { - if(!local) createrays(gun, from, to); - if(muzzleflash && d->muzzle.x >= 0) - particle_flare(d->muzzle, d->muzzle, 200, PART_MUZZLE_FLASH3, 0xFFFFFF, 2.75f, d); - loopi(guns[gun].rays) - { - if(d->quadmillis) - particle_trail(PART_FLAME, 400, hudgunorigin(gun, from, rays[i], d), rays[i], 0x802010, 0.6f, 36); - particle_splash(PART_SPARK, 20, 250, rays[i], 0xB49B4B, 0.24f); - particle_flare(hudgunorigin(gun, from, rays[i], d), rays[i], 300, PART_STREAK, 0xFFC864, 0.28f); - if(!local) adddecal(DECAL_BULLET, rays[i], vec(from).sub(rays[i]).safenormalize(), 2.0f); - } - if(muzzlelight) adddynlight(hudgunorigin(gun, d->o, to, d), 30, vec(0.5f, 0.375f, 0.25f), 100, 100, DL_FLASH, 0, vec(0, 0, 0), d); - break; - } - - case GUN_CG: - case GUN_PISTOL: - { - particle_splash(PART_SPARK, 200, 250, to, 0xB49B4B, 0.24f); - if(d->quadmillis) - particle_trail(PART_FLAME, 400, hudgunorigin(gun, from, to, d), to, 0x802010, 0.6f, 36); - particle_flare(hudgunorigin(gun, from, to, d), to, 600, PART_STREAK, 0xFFC864, 0.28f); - if(muzzleflash && d->muzzle.x >= 0) - particle_flare(d->muzzle, d->muzzle, gun==GUN_CG ? 100 : 200, PART_MUZZLE_FLASH1, 0xFFFFFF, gun==GUN_CG ? 2.25f : 1.25f, d); - if(!local) adddecal(DECAL_BULLET, to, vec(from).sub(to).safenormalize(), 2.0f); - if(muzzlelight) adddynlight(hudgunorigin(gun, d->o, to, d), gun==GUN_CG ? 30 : 15, vec(0.5f, 0.375f, 0.25f), gun==GUN_CG ? 50 : 100, gun==GUN_CG ? 50 : 100, DL_FLASH, 0, vec(0, 0, 0), d); - break; - } - - case GUN_RL: - if(d->quadmillis) - particle_trail(PART_FLAME, 400, hudgunorigin(gun, from, to, d), to, 0x802010, 0.6f, 36); - if(muzzleflash && d->muzzle.x >= 0) - particle_flare(d->muzzle, d->muzzle, 250, PART_MUZZLE_FLASH2, 0xFFFFFF, 3.0f, d); - pspeed = guns[gun].projspeed; - newprojectile(from, to, (float)pspeed, local, id, d, gun); - break; - - case GUN_GL: - { - float dist = from.dist(to); - vec up = to; - up.z += dist/8; - if(muzzleflash && d->muzzle.x >= 0) - particle_flare(d->muzzle, d->muzzle, 200, PART_MUZZLE_FLASH2, 0xFFFFFF, 1.5f, d); - if(muzzlelight) adddynlight(hudgunorigin(gun, d->o, to, d), 20, vec(0.5f, 0.375f, 0.25f), 100, 100, DL_FLASH, 0, vec(0, 0, 0), d); - newbouncer(from, up, local, id, d, BNC_GRENADE, guns[gun].ttl, guns[gun].projspeed); - break; - } - - case GUN_RIFLE: - particle_splash(PART_SPARK, 200, 250, to, 0xB49B4B, 0.24f); - if(d->quadmillis) - particle_trail(PART_FLAME, 400, hudgunorigin(gun, from, to, d), to, 0x802010, 0.6f, 36); - particle_trail(PART_SMOKE, 500, hudgunorigin(gun, from, to, d), to, 0x404040, 0.6f, 18); - if(muzzleflash && d->muzzle.x >= 0) - particle_flare(d->muzzle, d->muzzle, 150, PART_MUZZLE_FLASH3, 0xFFFFFF, 1.25f, d); - if(!local) adddecal(DECAL_BULLET, to, vec(from).sub(to).safenormalize(), 3.0f); - if(muzzlelight) adddynlight(hudgunorigin(gun, d->o, to, d), 25, vec(0.5f, 0.375f, 0.25f), 75, 75, DL_FLASH, 0, vec(0, 0, 0), d); - break; - } - - bool looped = false; - if(d->attacksound >= 0 && d->attacksound != sound) d->stopattacksound(); - if(d->idlesound >= 0) d->stopidlesound(); - fpsent *h = followingplayer(player1); - switch(sound) - { - case S_CHAINSAW_ATTACK: - if(d->attacksound >= 0) looped = true; - d->attacksound = sound; - d->attackchan = playsound(sound, d==h ? NULL : &d->o, NULL, 0, -1, 100, d->attackchan); - break; - default: - playsound(sound, d==h ? NULL : &d->o); - break; - } - if(d->quadmillis && lastmillis-prevaction>200 && !looped) playsound(S_ITEMPUP, d==h ? NULL : &d->o); - } - - void particletrack(physent *owner, vec &o, vec &d) - { - if(owner->type!=ENT_PLAYER && owner->type!=ENT_AI) return; - fpsent *pl = (fpsent *)owner; - if(pl->muzzle.x < 0 || pl->lastattackgun != pl->gunselect) return; - float dist = o.dist(d); - o = pl->muzzle; - if(dist <= 0) d = o; - else - { - vecfromyawpitch(owner->yaw, owner->pitch, 1, 0, d); - float newdist = raycube(owner->o, d, dist, RAY_CLIPMAT|RAY_ALPHAPOLY); - d.mul(min(newdist, dist)).add(owner->o); - } - } - - void dynlighttrack(physent *owner, vec &o, vec &hud) - { - if(owner->type!=ENT_PLAYER && owner->type!=ENT_AI) return; - fpsent *pl = (fpsent *)owner; - if(pl->muzzle.x < 0 || pl->lastattackgun != pl->gunselect) return; - o = pl->muzzle; - hud = owner == followingplayer(player1) ? vec(pl->o).add(vec(0, 0, 2)) : pl->muzzle; - } - - float intersectdist = 1e16f; - - bool intersect(dynent *d, const vec &from, const vec &to, float &dist) // if lineseg hits entity bounding box - { - vec bottom(d->o), top(d->o); - bottom.z -= d->eyeheight; - top.z += d->aboveeye; - return linecylinderintersect(from, to, bottom, top, d->radius, dist); - } - - dynent *intersectclosest(const vec &from, const vec &to, fpsent *at, float &bestdist) - { - dynent *best = NULL; - bestdist = 1e16f; - loopi(numdynents()) - { - dynent *o = iterdynents(i); - if(o==at || o->state!=CS_ALIVE) continue; - float dist; - if(!intersect(o, from, to, dist)) continue; - if(dist<bestdist) - { - best = o; - bestdist = dist; - } - } - return best; - } - - void shorten(vec &from, vec &target, float dist) - { - target.sub(from).mul(min(1.0f, dist)).add(from); - } - - void raydamage(vec &from, vec &to, fpsent *d) - { - int qdam = guns[d->gunselect].damage; - if(d->quadmillis) qdam *= 4; - dynent *o; - float dist; - if(guns[d->gunselect].rays > 1) - { - dynent *hits[MAXRAYS]; - int maxrays = guns[d->gunselect].rays; - loopi(maxrays) - { - if((hits[i] = intersectclosest(from, rays[i], d, dist))) shorten(from, rays[i], dist); - else adddecal(DECAL_BULLET, rays[i], vec(from).sub(rays[i]).safenormalize(), 2.0f); - } - loopi(maxrays) if(hits[i]) - { - o = hits[i]; - hits[i] = NULL; - int numhits = 1; - for(int j = i+1; j < maxrays; j++) if(hits[j] == o) - { - hits[j] = NULL; - numhits++; - } - hitpush(numhits*qdam, o, d, from, to, d->gunselect, numhits); - } - } - else if((o = intersectclosest(from, to, d, dist))) - { - shorten(from, to, dist); - hitpush(qdam, o, d, from, to, d->gunselect, 1); - } - else if(d->gunselect!=GUN_FIST) adddecal(DECAL_BULLET, to, vec(from).sub(to).safenormalize(), d->gunselect==GUN_RIFLE ? 3.0f : 2.0f); - } - - void shoot(fpsent *d, const vec &targ) - { - int prevaction = d->lastaction, attacktime = lastmillis-prevaction; - if(attacktime<d->gunwait) return; - d->gunwait = 0; - if((d==player1 || d->ai) && !d->attacking) return; - d->lastaction = lastmillis; - d->lastattackgun = d->gunselect; - if(!d->ammo[d->gunselect]) - { - if(d==player1) - { - msgsound(S_NOAMMO, d); - d->gunwait = 600; - d->lastattackgun = -1; - weaponswitch(d); - } - return; - } - if(d->gunselect) d->ammo[d->gunselect]--; - - pwshot(d->gunselect); /// PW - - vec from = d->o, to = targ, dir = vec(to).sub(from).safenormalize(); - float dist = to.dist(from); - vec kickback = vec(dir).mul(guns[d->gunselect].kickamount*-2.5f); - d->vel.add(kickback); - float shorten = 0; - if(guns[d->gunselect].range && dist > guns[d->gunselect].range) - shorten = guns[d->gunselect].range; - float barrier = raycube(d->o, dir, dist, RAY_CLIPMAT|RAY_ALPHAPOLY); - if(barrier > 0 && barrier < dist && (!shorten || barrier < shorten)) - shorten = barrier; - if(shorten) to = vec(dir).mul(shorten).add(from); - - if(guns[d->gunselect].rays > 1) createrays(d->gunselect, from, to); - else if(guns[d->gunselect].spread) offsetray(from, to, guns[d->gunselect].spread, guns[d->gunselect].range, to); - - hits.setsize(0); - - if(!guns[d->gunselect].projspeed) raydamage(from, to, d); - - shoteffects(d->gunselect, from, to, d, true, 0, prevaction); - - if(d==player1 || d->ai) - { - addmsg(N_SHOOT, "rci2i6iv", d, lastmillis-maptime, d->gunselect, - (int)(from.x*DMF), (int)(from.y*DMF), (int)(from.z*DMF), - (int)(to.x*DMF), (int)(to.y*DMF), (int)(to.z*DMF), - hits.length(), hits.length()*sizeof(hitmsg)/sizeof(int), hits.getbuf()); - } - - d->gunwait = guns[d->gunselect].attackdelay; - if(d->gunselect == GUN_PISTOL && d->ai) d->gunwait += int(d->gunwait*(((101-d->skill)+rnd(111-d->skill))/100.f)); - d->totalshots += guns[d->gunselect].damage*(d->quadmillis ? 4 : 1)*guns[d->gunselect].rays; - } - - void adddynlights() - { - loopv(projs) - { - projectile &p = projs[i]; - if(p.gun!=GUN_RL) continue; - vec pos(p.o); - pos.add(vec(p.offset).mul(p.offsetmillis/float(OFFSETMILLIS))); - adddynlight(pos, 20, vec(1, 0.75f, 0.5f)); - } - loopv(bouncers) - { - bouncer &bnc = *bouncers[i]; - if(bnc.bouncetype!=BNC_GRENADE) continue; - vec pos = bnc.offsetpos(); - adddynlight(pos, 8, vec(0.25f, 1, 1)); - } - } - - static const char * const projnames[2] = { "projectiles/grenade", "projectiles/rocket" }; - - void preloadbouncers() { - loopi(sizeof(projnames)/sizeof(projnames[0])) preloadmodel(projnames[i]); - } - - void renderbouncers() - { - float yaw, pitch; - loopv(bouncers) - { - bouncer &bnc = *bouncers[i]; - vec pos = bnc.offsetpos(); - vec vel(bnc.vel); - if(vel.magnitude() <= 25.0f) yaw = bnc.lastyaw; - else - { - vectoyawpitch(vel, yaw, pitch); - yaw += 90; - bnc.lastyaw = yaw; - } - pitch = -bnc.roll; - if(bnc.bouncetype==BNC_GRENADE) - rendermodel(&bnc.light, "projectiles/grenade", ANIM_MAPMODEL|ANIM_LOOP, pos, yaw, pitch, MDL_CULL_VFC|MDL_CULL_OCCLUDED|MDL_LIGHT|MDL_LIGHT_FAST|MDL_DYNSHADOW); - else - { - const char *mdl = NULL; - int cull = MDL_CULL_VFC|MDL_CULL_DIST|MDL_CULL_OCCLUDED; - float fade = 1; - if(bnc.lifetime < 250) fade = bnc.lifetime/250.0f; - rendermodel(&bnc.light, mdl, ANIM_MAPMODEL|ANIM_LOOP, pos, yaw, pitch, cull, NULL, NULL, 0, 0, fade); - } - } - } - - void renderprojectiles() - { - float yaw, pitch; - loopv(projs) - { - projectile &p = projs[i]; - if(p.gun!=GUN_RL) continue; - float dist = min(p.o.dist(p.to)/32.0f, 1.0f); - vec pos = vec(p.o).add(vec(p.offset).mul(dist*p.offsetmillis/float(OFFSETMILLIS))), - v = dist < 1e-6f ? p.dir : vec(p.to).sub(pos).normalize(); - // the amount of distance in front of the smoke trail needs to change if the model does - vectoyawpitch(v, yaw, pitch); - yaw += 90; - v.mul(3); - v.add(pos); - rendermodel(&p.light, "projectiles/rocket", ANIM_MAPMODEL|ANIM_LOOP, v, yaw, pitch, MDL_CULL_VFC|MDL_CULL_OCCLUDED|MDL_LIGHT|MDL_LIGHT_FAST); - } - } - - void checkattacksound(fpsent *d, bool local) - { - int gun = -1; - switch(d->attacksound) - { - case S_CHAINSAW_ATTACK: - if(chainsawhudgun) gun = GUN_FIST; - break; - default: - return; - } - if(gun >= 0 && gun < NUMGUNS && - d->clientnum >= 0 && d->state == CS_ALIVE && - d->lastattackgun == gun && lastmillis - d->lastaction < guns[gun].attackdelay + 50) - { - d->attackchan = playsound(d->attacksound, local ? NULL : &d->o, NULL, 0, -1, -1, d->attackchan); - if(d->attackchan < 0) d->attacksound = -1; - } - else d->stopattacksound(); - } - - void checkidlesound(fpsent *d, bool local) - { - int sound = -1, radius = 0; - if(d->clientnum >= 0 && d->state == CS_ALIVE) switch(d->gunselect) - { - case GUN_FIST: - if(chainsawhudgun && d->attacksound < 0) - { - sound = S_CHAINSAW_IDLE; - radius = 50; - } - break; - } - if(d->idlesound != sound) - { - if(d->idlesound >= 0) d->stopidlesound(); - if(sound >= 0) - { - d->idlechan = playsound(sound, local ? NULL : &d->o, NULL, 0, -1, 100, d->idlechan, radius); - if(d->idlechan >= 0) d->idlesound = sound; - } - } - else if(sound >= 0) - { - d->idlechan = playsound(sound, local ? NULL : &d->o, NULL, 0, -1, -1, d->idlechan, radius); - if(d->idlechan < 0) d->idlesound = -1; - } - } - - void removeweapons(fpsent *d) - { - removebouncers(d); - removeprojectiles(d); - } - - void updateweapons(int curtime) - { - updateprojectiles(curtime); - pwcalcaccuracy(); - if(player1->clientnum>=0 && player1->state==CS_ALIVE) shoot(player1, worldpos); // only shoot when connected to server - updatebouncers(curtime); // need to do this after the player shoots so grenades don't end up inside player's BB next frame - fpsent *following = followingplayer(); - if(!following) following = player1; - loopv(players) - { - fpsent *d = players[i]; - checkattacksound(d, d==following); - checkidlesound(d, d==following); - } - } - - void avoidweapons(ai::avoidset &obstacles, float radius) - { - loopv(projs) - { - projectile &p = projs[i]; - obstacles.avoidnear(NULL, p.o.z + guns[p.gun].exprad + 1, p.o, radius + guns[p.gun].exprad); - } - loopv(bouncers) - { - bouncer &bnc = *bouncers[i]; - if(bnc.bouncetype != BNC_GRENADE) continue; - obstacles.avoidnear(NULL, bnc.o.z + guns[GUN_GL].exprad + 1, bnc.o, radius + guns[GUN_GL].exprad); - } - } + static const int OFFSETMILLIS = 500; + vec rays[MAXRAYS]; + + struct hitmsg + { + int target, lifesequence, info1, info2; + ivec dir; + }; + vector<hitmsg> hits; + + ICOMMAND(getweapon, "", (), intret(player1->gunselect)); + + void gunselect(int gun, fpsent *d) + { + if(gun!=d->gunselect) + { + addmsg(N_GUNSELECT, "rci", d, gun); + playsound(S_WEAPLOAD, d == player1 ? NULL : &d->o); + } + d->gunselect = gun; + } + + void nextweapon(int dir, bool force = false) + { + if(player1->state!=CS_ALIVE) return; + dir = (dir < 0 ? NUMGUNS-1 : 1); + int gun = player1->gunselect; + loopi(NUMGUNS) + { + gun = (gun + dir)%NUMGUNS; + if(force || player1->ammo[gun]) break; + } + if(gun != player1->gunselect) gunselect(gun, player1); + else playsound(S_NOAMMO); + } + ICOMMAND(nextweapon, "ii", (int *dir, int *force), nextweapon(*dir, *force!=0)); + + int getweapon(const char *name) + { + const char *abbrevs[] = { "FI", "SG", "CG", "RL", "RI", "GL", "PI" }; + if(isdigit(name[0])) return parseint(name); + else loopi(sizeof(abbrevs)/sizeof(abbrevs[0])) if(!strcasecmp(abbrevs[i], name)) return i; + return -1; + } + + void setweapon(const char *name, bool force = false) + { + int gun = getweapon(name); + if(player1->state!=CS_ALIVE || gun<GUN_FIST || gun>GUN_PISTOL) return; + if(force || player1->ammo[gun]) gunselect(gun, player1); + else playsound(S_NOAMMO); + } + ICOMMAND(setweapon, "si", (char *name, int *force), setweapon(name, *force!=0)); + + void cycleweapon(int numguns, int *guns, bool force = false) + { + if(numguns<=0 || player1->state!=CS_ALIVE) return; + int offset = 0; + loopi(numguns) if(guns[i] == player1->gunselect) { offset = i+1; break; } + loopi(numguns) + { + int gun = guns[(i+offset)%numguns]; + if(gun>=0 && gun<NUMGUNS && (force || player1->ammo[gun])) + { + gunselect(gun, player1); + return; + } + } + playsound(S_NOAMMO); + } + ICOMMAND(cycleweapon, "V", (tagval *args, int numargs), + { + int numguns = min(numargs, 7); + int guns[7]; + loopi(numguns) guns[i] = getweapon(args[i].getstr()); + cycleweapon(numguns, guns); + }); + + void weaponswitch(fpsent *d) + { + if(d->state!=CS_ALIVE) return; + int s = d->gunselect; + if (s!=GUN_CG && d->ammo[GUN_CG]) s = GUN_CG; + else if(s!=GUN_RL && d->ammo[GUN_RL]) s = GUN_RL; + else if(s!=GUN_SG && d->ammo[GUN_SG]) s = GUN_SG; + else if(s!=GUN_RIFLE && d->ammo[GUN_RIFLE]) s = GUN_RIFLE; + else if(s!=GUN_GL && d->ammo[GUN_GL]) s = GUN_GL; + else if(s!=GUN_PISTOL && d->ammo[GUN_PISTOL]) s = GUN_PISTOL; + else s = GUN_FIST; + + gunselect(s, d); + } + + ICOMMAND(weapon, "V", (tagval *args, int numargs), + { + if(player1->state!=CS_ALIVE) return; + loopi(7) + { + const char *name = i < numargs ? args[i].getstr() : ""; + if(name[0]) + { + int gun = getweapon(name); + if(gun >= GUN_FIST && gun <= GUN_PISTOL && gun != player1->gunselect && player1->ammo[gun]) { gunselect(gun, player1); return; } + } else { weaponswitch(player1); return; } + } + playsound(S_NOAMMO); + }); + + void offsetray(const vec &from, const vec &to, int spread, float range, vec &dest) + { + vec offset; + do offset = vec(rndscale(1), rndscale(1), rndscale(1)).sub(0.5f); + while(offset.squaredlen() > 0.5f*0.5f); + offset.mul((to.dist(from)/1024)*spread); + offset.z /= 2; + dest = vec(offset).add(to); + if(dest != from) + { + vec dir = vec(dest).sub(from).normalize(); + raycubepos(from, dir, dest, range, RAY_CLIPMAT|RAY_ALPHAPOLY); + } + } + + void createrays(int gun, const vec &from, const vec &to) // create random spread of rays + { + loopi(guns[gun].rays) offsetray(from, to, guns[gun].spread, guns[gun].range, rays[i]); + } + + enum { BNC_GRENADE }; + + struct bouncer : physent + { + int lifetime, bounces; + float lastyaw, roll; + bool local; + fpsent *owner; + int bouncetype, variant; + vec offset; + int offsetmillis; + float offsetheight; + int id; + entitylight light; + + bouncer() : bounces(0), roll(0), variant(0) + { + type = ENT_BOUNCE; + } + + vec offsetpos() + { + vec pos(o); + if(offsetmillis > 0) + { + pos.add(vec(offset).mul(offsetmillis/float(OFFSETMILLIS))); + if(offsetheight >= 0) pos.z = max(pos.z, o.z - max(offsetheight - eyeheight, 0.0f)); + } + return pos; + } + + void limitoffset() + { + if(bouncetype == BNC_GRENADE && offsetmillis > 0 && offset.z < 0) + offsetheight = raycube(vec(o.x + offset.x, o.y + offset.y, o.z), vec(0, 0, -1), -offset.z); + else offsetheight = -1; + } + }; + + vector<bouncer *> bouncers; + + vec hudgunorigin(int gun, const vec &from, const vec &to, fpsent *d); + + void newbouncer(const vec &from, const vec &to, bool local, int id, fpsent *owner, int type, int lifetime, int speed, entitylight *light = NULL) + { + bouncer &bnc = *bouncers.add(new bouncer); + bnc.o = from; + bnc.radius = bnc.xradius = bnc.yradius = 1.5f; + bnc.eyeheight = bnc.radius; + bnc.aboveeye = bnc.radius; + bnc.lifetime = lifetime; + bnc.local = local; + bnc.owner = owner; + bnc.bouncetype = type; + bnc.id = local ? lastmillis : id; + if(light) bnc.light = *light; + + bnc.collidetype = COLLIDE_ELLIPSE_PRECISE; + + vec dir(to); + dir.sub(from).safenormalize(); + bnc.vel = dir; + bnc.vel.mul(speed); + + avoidcollision(&bnc, dir, owner, 0.1f); + + if(type==BNC_GRENADE) + { + bnc.offset = hudgunorigin(GUN_GL, from, to, owner); + if(owner==followingplayer(player1) && !isthirdperson()) bnc.offset.sub(owner->o).rescale(16).add(owner->o); + } + else bnc.offset = from; + bnc.offset.sub(bnc.o); + bnc.offsetmillis = OFFSETMILLIS; + bnc.limitoffset(); + + bnc.resetinterp(); + } + + void bounced(physent *d, const vec &surface) + { + if(d->type != ENT_BOUNCE) return; + bouncer *b = (bouncer *)d; + if(b->bounces >= 2) return; + b->bounces++; + adddecal(DECAL_BLOOD, vec(b->o).sub(vec(surface).mul(b->radius)), surface, 2.96f/b->bounces, bvec(0x60, 0xFF, 0xFF), rnd(4)); + } + + void updatebouncers(int time) + { + loopv(bouncers) + { + bouncer &bnc = *bouncers[i]; + if(bnc.bouncetype==BNC_GRENADE && bnc.vel.magnitude() > 50.0f) + { + vec pos = bnc.offsetpos(); + regular_particle_splash(PART_SMOKE, 1, 150, pos, 0x404040, 2.4f, 50, -20); + } + vec old(bnc.o); + bool stopped = false; + if(bnc.bouncetype==BNC_GRENADE) stopped = bounce(&bnc, 0.6f, 0.5f, 0.8f) || (bnc.lifetime -= time)<0; + + if(stopped) + { + if(bnc.bouncetype==BNC_GRENADE) + { + int qdam = guns[GUN_GL].damage*(bnc.owner->quadmillis ? 4 : 1); + hits.setsize(0); + explode(bnc.local, bnc.owner, bnc.o, NULL, qdam, GUN_GL); + adddecal(DECAL_SCORCH, bnc.o, vec(0, 0, 1), guns[GUN_GL].exprad/2); + if(bnc.local) + addmsg(N_EXPLODE, "rci3iv", bnc.owner, lastmillis-maptime, GUN_GL, bnc.id-maptime, + hits.length(), hits.length()*sizeof(hitmsg)/sizeof(int), hits.getbuf()); + } + delete bouncers.remove(i--); + } + else + { + bnc.roll += old.sub(bnc.o).magnitude()/(4*RAD); + bnc.offsetmillis = max(bnc.offsetmillis-time, 0); + bnc.limitoffset(); + } + } + } + + void removebouncers(fpsent *owner) + { + loopv(bouncers) if(bouncers[i]->owner==owner) { delete bouncers[i]; bouncers.remove(i--); } + } + + void clearbouncers() { bouncers.deletecontents(); } + + struct projectile + { + vec dir, o, to, offset; + float speed; + fpsent *owner; + int gun; + bool local; + int offsetmillis; + int id; + entitylight light; + }; + vector<projectile> projs; + + void clearprojectiles() { projs.shrink(0); } + + void newprojectile(const vec &from, const vec &to, float speed, bool local, int id, fpsent *owner, int gun) + { + projectile &p = projs.add(); + p.dir = vec(to).sub(from).safenormalize(); + p.o = from; + p.to = to; + p.offset = hudgunorigin(gun, from, to, owner); + p.offset.sub(from); + p.speed = speed; + p.local = local; + p.owner = owner; + p.gun = gun; + p.offsetmillis = OFFSETMILLIS; + p.id = local ? lastmillis : id; + } + + void removeprojectiles(fpsent *owner) + { + // can't use loopv here due to strange GCC optimizer bug + int len = projs.length(); + loopi(len) if(projs[i].owner==owner) { projs.remove(i--); len--; } + } + + VARP(blood, 0, 1, 1); + + void damageeffect(int damage, fpsent *d, bool thirdperson) + { + vec p = d->o; + p.z += 0.6f*(d->eyeheight + d->aboveeye) - d->eyeheight; + if(blood) particle_splash(PART_BLOOD, damage/10, 1000, p, 0x60FFFF, 2.96f); + if(thirdperson) + { + defformatstring(ds, "%d", damage); + particle_textcopy(d->abovehead(), ds, PART_TEXT, 2000, 0xFF4B19, 4.0f, -8); + } + } + + void spawnbouncer(const vec &p, const vec &vel, fpsent *d, int type, entitylight *light = NULL) + { + vec to(rnd(100)-50, rnd(100)-50, rnd(100)-50); + if(to.iszero()) to.z += 1; + to.normalize(); + to.add(p); + newbouncer(p, to, true, 0, d, type, rnd(1000)+1000, rnd(100)+20, light); + } + + void hit(int damage, dynent *d, fpsent *at, const vec &vel, int gun, float info1, int info2 = 1) + { + if(at==player1 && d!=at) + { + extern int hitsound; + if(hitsound && lasthit != lastmillis) playsound(S_HIT); + lasthit = lastmillis; + } + + fpsent *f = (fpsent *)d; + + f->lastpain = lastmillis; + if(at->type==ENT_PLAYER && !isteam(at->team, f->team)) at->totaldamage += damage; + + if(f->type==ENT_AI || !m_mp(gamemode) || f==at) f->hitpush(damage, vel, at, gun); + + pwhit(gun, damage); + + if(!m_mp(gamemode)) damaged(damage, f, at); + else + { + hitmsg &h = hits.add(); + h.target = f->clientnum; + h.lifesequence = f->lifesequence; + h.info1 = int(info1*DMF); + h.info2 = info2; + h.dir = f==at ? ivec(0, 0, 0) : ivec(vec(vel).mul(DNF)); + if(at==player1) + { + damageeffect(damage, f); + if(f==player1) + { + damagecompass(damage, at ? at->o : f->o); + playsound(S_PAIN6); + } + else playsound(S_PAIN1+rnd(5), &f->o); + } + } + } + + void hitpush(int damage, dynent *d, fpsent *at, vec &from, vec &to, int gun, int rays) + { + hit(damage, d, at, vec(to).sub(from).safenormalize(), gun, from.dist(to), rays); + } + + float projdist(dynent *o, vec &dir, const vec &v) + { + vec middle = o->o; + middle.z += (o->aboveeye-o->eyeheight)/2; + float dist = middle.dist(v, dir); + dir.div(dist); + if(dist<0) dist = 0; + return dist; + } + + void radialeffect(dynent *o, const vec &v, int qdam, fpsent *at, int gun) + { + if(o->state!=CS_ALIVE) return; + vec dir; + float dist = projdist(o, dir, v); + if(dist<guns[gun].exprad) + { + int damage = (int)(qdam*(1-dist/EXP_DISTSCALE/guns[gun].exprad)); + if(o==at) damage /= EXP_SELFDAMDIV; + hit(damage, o, at, dir, gun, dist); + } + } + + FVARP(explodebright, 0, 1, 1); + + void explode(bool local, fpsent *owner, const vec &v, dynent *safe, int damage, int gun) + { + particle_splash(PART_SPARK, 200, 300, v, 0xB49B4B, 0.24f); + playsound(gun!=GUN_GL ? S_RLHIT : S_FEXPLODE, &v); + int color = gun!=GUN_GL ? 0xFF8080 : 0x80FFFF; + if((gun==GUN_RL || gun==GUN_GL) && explodebright < 1) color = vec::hexcolor(color).mul(explodebright).tohexcolor(); + particle_fireball(v, guns[gun].exprad, gun!=GUN_GL ? PART_EXPLOSION : PART_EXPLOSION_BLUE, gun!=GUN_GL ? -1 : int((guns[gun].exprad-4.0f)*15), color, 4.0f); + if(gun==GUN_RL) adddynlight(v, 1.15f*guns[gun].exprad, vec(2, 1.5f, 1), 700, 100, 0, guns[gun].exprad/2, vec(1, 0.75f, 0.5f)); + else if(gun==GUN_GL) adddynlight(v, 1.15f*guns[gun].exprad, vec(0.5f, 1.5f, 2), 600, 100, 0, 8, vec(0.25f, 1, 1)); + else adddynlight(v, 1.15f*guns[gun].exprad, vec(2, 1.5f, 1), 700, 100); + if(!local) return; + int numdyn = numdynents(); + loopi(numdyn) + { + dynent *o = iterdynents(i); + if(o->o.reject(v, o->radius + guns[gun].exprad) || o==safe) continue; + radialeffect(o, v, damage, owner, gun); + } + } + + void projsplash(projectile &p, vec &v, dynent *safe, int damage) + { + if(guns[p.gun].part) + { + particle_splash(PART_SPARK, 100, 200, v, 0xB49B4B, 0.24f); + playsound(S_FEXPLODE, &v); + // no push? + } + else + { + explode(p.local, p.owner, v, safe, damage, GUN_RL); + adddecal(DECAL_SCORCH, v, vec(p.dir).neg(), guns[p.gun].exprad/2); + } + } + + void explodeeffects(int gun, fpsent *d, bool local, int id) + { + if(local) return; + switch(gun) + { + case GUN_RL: + loopv(projs) + { + projectile &p = projs[i]; + if(p.gun == gun && p.owner == d && p.id == id && !p.local) + { + vec pos(p.o); + pos.add(vec(p.offset).mul(p.offsetmillis/float(OFFSETMILLIS))); + explode(p.local, p.owner, pos, NULL, 0, GUN_RL); + adddecal(DECAL_SCORCH, pos, vec(p.dir).neg(), guns[gun].exprad/2); + projs.remove(i); + break; + } + } + break; + case GUN_GL: + loopv(bouncers) + { + bouncer &b = *bouncers[i]; + if(b.bouncetype == BNC_GRENADE && b.owner == d && b.id == id && !b.local) + { + vec pos = b.offsetpos(); + explode(b.local, b.owner, pos, NULL, 0, GUN_GL); + adddecal(DECAL_SCORCH, pos, vec(0, 0, 1), guns[gun].exprad/2); + delete bouncers.remove(i); + break; + } + } + break; + default: + break; + } + } + + bool projdamage(dynent *o, projectile &p, vec &v, int qdam) + { + if(o->state!=CS_ALIVE) return false; + if(!intersect(o, p.o, v)) return false; + projsplash(p, v, o, qdam); + vec dir; + projdist(o, dir, v); + hit(qdam, o, p.owner, dir, p.gun, 0); + return true; + } + + void updateprojectiles(int time) + { + loopv(projs) + { + projectile &p = projs[i]; + p.offsetmillis = max(p.offsetmillis-time, 0); + int qdam = guns[p.gun].damage*(p.owner->quadmillis ? 4 : 1); + vec dv; + float dist = p.to.dist(p.o, dv); + dv.mul(time/max(dist*1000/p.speed, float(time))); + vec v = vec(p.o).add(dv); + bool exploded = false; + hits.setsize(0); + if(p.local) + { + vec halfdv = vec(dv).mul(0.5f), bo = vec(p.o).add(halfdv); + float br = max(fabs(halfdv.x), fabs(halfdv.y)) + 1; + loopj(numdynents()) + { + dynent *o = iterdynents(j); + if(p.owner==o || o->o.reject(bo, o->radius + br)) continue; + if(projdamage(o, p, v, qdam)) { exploded = true; break; } + } + } + if(!exploded) + { + if(dist<4) + { + if(p.o!=p.to) // if original target was moving, reevaluate endpoint + { + if(raycubepos(p.o, p.dir, p.to, 0, RAY_CLIPMAT|RAY_ALPHAPOLY)>=4) continue; + } + projsplash(p, v, NULL, qdam); + exploded = true; + } + else + { + vec pos(v); + pos.add(vec(p.offset).mul(p.offsetmillis/float(OFFSETMILLIS))); + regular_particle_splash(PART_SMOKE, 2, 300, pos, 0x404040, 2.4f, 50, -20); + } + } + if(exploded) + { + if(p.local) + addmsg(N_EXPLODE, "rci3iv", p.owner, lastmillis-maptime, p.gun, p.id-maptime, + hits.length(), hits.length()*sizeof(hitmsg)/sizeof(int), hits.getbuf()); + projs.remove(i--); + } + else p.o = v; + } + } + + extern int chainsawhudgun; + + VARP(muzzleflash, 0, 1, 1); + VARP(muzzlelight, 0, 1, 1); + + void shoteffects(int gun, const vec &from, const vec &to, fpsent *d, bool local, int id, int prevaction) // create visual effect from a shot + { + int sound = guns[gun].sound, pspeed = 25; + switch(gun) + { + case GUN_FIST: + if(d->type==ENT_PLAYER && chainsawhudgun) sound = S_CHAINSAW_ATTACK; + break; + + case GUN_SG: + { + if(!local) createrays(gun, from, to); + if(muzzleflash && d->muzzle.x >= 0) + particle_flare(d->muzzle, d->muzzle, 200, PART_MUZZLE_FLASH3, 0xFFFFFF, 2.75f, d); + loopi(guns[gun].rays) + { + if(d->quadmillis) + particle_trail(PART_FLAME, 400, hudgunorigin(gun, from, rays[i], d), rays[i], 0x802010, 0.6f, 36); + particle_splash(PART_SPARK, 20, 250, rays[i], 0xB49B4B, 0.24f); + particle_flare(hudgunorigin(gun, from, rays[i], d), rays[i], 300, PART_STREAK, 0xFFC864, 0.28f); + if(!local) adddecal(DECAL_BULLET, rays[i], vec(from).sub(rays[i]).safenormalize(), 2.0f); + } + if(muzzlelight) adddynlight(hudgunorigin(gun, d->o, to, d), 30, vec(0.5f, 0.375f, 0.25f), 100, 100, DL_FLASH, 0, vec(0, 0, 0), d); + break; + } + + case GUN_CG: + case GUN_PISTOL: + { + particle_splash(PART_SPARK, 200, 250, to, 0xB49B4B, 0.24f); + if(d->quadmillis) + particle_trail(PART_FLAME, 400, hudgunorigin(gun, from, to, d), to, 0x802010, 0.6f, 36); + particle_flare(hudgunorigin(gun, from, to, d), to, 600, PART_STREAK, 0xFFC864, 0.28f); + if(muzzleflash && d->muzzle.x >= 0) + particle_flare(d->muzzle, d->muzzle, gun==GUN_CG ? 100 : 200, PART_MUZZLE_FLASH1, 0xFFFFFF, gun==GUN_CG ? 2.25f : 1.25f, d); + if(!local) adddecal(DECAL_BULLET, to, vec(from).sub(to).safenormalize(), 2.0f); + if(muzzlelight) adddynlight(hudgunorigin(gun, d->o, to, d), gun==GUN_CG ? 30 : 15, vec(0.5f, 0.375f, 0.25f), gun==GUN_CG ? 50 : 100, gun==GUN_CG ? 50 : 100, DL_FLASH, 0, vec(0, 0, 0), d); + break; + } + + case GUN_RL: + if(d->quadmillis) + particle_trail(PART_FLAME, 400, hudgunorigin(gun, from, to, d), to, 0x802010, 0.6f, 36); + if(muzzleflash && d->muzzle.x >= 0) + particle_flare(d->muzzle, d->muzzle, 250, PART_MUZZLE_FLASH2, 0xFFFFFF, 3.0f, d); + pspeed = guns[gun].projspeed; + newprojectile(from, to, (float)pspeed, local, id, d, gun); + break; + + case GUN_GL: + { + float dist = from.dist(to); + vec up = to; + up.z += dist/8; + if(muzzleflash && d->muzzle.x >= 0) + particle_flare(d->muzzle, d->muzzle, 200, PART_MUZZLE_FLASH2, 0xFFFFFF, 1.5f, d); + if(muzzlelight) adddynlight(hudgunorigin(gun, d->o, to, d), 20, vec(0.5f, 0.375f, 0.25f), 100, 100, DL_FLASH, 0, vec(0, 0, 0), d); + newbouncer(from, up, local, id, d, BNC_GRENADE, guns[gun].ttl, guns[gun].projspeed); + break; + } + + case GUN_RIFLE: + particle_splash(PART_SPARK, 200, 250, to, 0xB49B4B, 0.24f); + if(d->quadmillis) + particle_trail(PART_FLAME, 400, hudgunorigin(gun, from, to, d), to, 0x802010, 0.6f, 36); + particle_trail(PART_SMOKE, 500, hudgunorigin(gun, from, to, d), to, 0x404040, 0.6f, 18); + if(muzzleflash && d->muzzle.x >= 0) + particle_flare(d->muzzle, d->muzzle, 150, PART_MUZZLE_FLASH3, 0xFFFFFF, 1.25f, d); + if(!local) adddecal(DECAL_BULLET, to, vec(from).sub(to).safenormalize(), 3.0f); + if(muzzlelight) adddynlight(hudgunorigin(gun, d->o, to, d), 25, vec(0.5f, 0.375f, 0.25f), 75, 75, DL_FLASH, 0, vec(0, 0, 0), d); + break; + } + + bool looped = false; + if(d->attacksound >= 0 && d->attacksound != sound) d->stopattacksound(); + if(d->idlesound >= 0) d->stopidlesound(); + fpsent *h = followingplayer(player1); + switch(sound) + { + case S_CHAINSAW_ATTACK: + if(d->attacksound >= 0) looped = true; + d->attacksound = sound; + d->attackchan = playsound(sound, d==h ? NULL : &d->o, NULL, 0, -1, 100, d->attackchan); + break; + default: + playsound(sound, d==h ? NULL : &d->o); + break; + } + if(d->quadmillis && lastmillis-prevaction>200 && !looped) playsound(S_ITEMPUP, d==h ? NULL : &d->o); + } + + void particletrack(physent *owner, vec &o, vec &d) + { + if(owner->type!=ENT_PLAYER && owner->type!=ENT_AI) return; + fpsent *pl = (fpsent *)owner; + if(pl->muzzle.x < 0 || pl->lastattackgun != pl->gunselect) return; + float dist = o.dist(d); + o = pl->muzzle; + if(dist <= 0) d = o; + else + { + vecfromyawpitch(owner->yaw, owner->pitch, 1, 0, d); + float newdist = raycube(owner->o, d, dist, RAY_CLIPMAT|RAY_ALPHAPOLY); + d.mul(min(newdist, dist)).add(owner->o); + } + } + + void dynlighttrack(physent *owner, vec &o, vec &hud) + { + if(owner->type!=ENT_PLAYER && owner->type!=ENT_AI) return; + fpsent *pl = (fpsent *)owner; + if(pl->muzzle.x < 0 || pl->lastattackgun != pl->gunselect) return; + o = pl->muzzle; + hud = owner == followingplayer(player1) ? vec(pl->o).add(vec(0, 0, 2)) : pl->muzzle; + } + + float intersectdist = 1e16f; + + bool intersect(dynent *d, const vec &from, const vec &to, float &dist) // if lineseg hits entity bounding box + { + vec bottom(d->o), top(d->o); + bottom.z -= d->eyeheight; + top.z += d->aboveeye; + return linecylinderintersect(from, to, bottom, top, d->radius, dist); + } + + dynent *intersectclosest(const vec &from, const vec &to, fpsent *at, float &bestdist) + { + dynent *best = NULL; + bestdist = 1e16f; + loopi(numdynents()) + { + dynent *o = iterdynents(i); + if(o==at || o->state!=CS_ALIVE) continue; + float dist; + if(!intersect(o, from, to, dist)) continue; + if(dist<bestdist) + { + best = o; + bestdist = dist; + } + } + return best; + } + + void shorten(vec &from, vec &target, float dist) + { + target.sub(from).mul(min(1.0f, dist)).add(from); + } + + void raydamage(vec &from, vec &to, fpsent *d) + { + int qdam = guns[d->gunselect].damage; + if(d->quadmillis) qdam *= 4; + dynent *o; + float dist; + if(guns[d->gunselect].rays > 1) + { + dynent *hits[MAXRAYS]; + int maxrays = guns[d->gunselect].rays; + loopi(maxrays) + { + if((hits[i] = intersectclosest(from, rays[i], d, dist))) shorten(from, rays[i], dist); + else adddecal(DECAL_BULLET, rays[i], vec(from).sub(rays[i]).safenormalize(), 2.0f); + } + loopi(maxrays) if(hits[i]) + { + o = hits[i]; + hits[i] = NULL; + int numhits = 1; + for(int j = i+1; j < maxrays; j++) if(hits[j] == o) + { + hits[j] = NULL; + numhits++; + } + hitpush(numhits*qdam, o, d, from, to, d->gunselect, numhits); + } + } + else if((o = intersectclosest(from, to, d, dist))) + { + shorten(from, to, dist); + hitpush(qdam, o, d, from, to, d->gunselect, 1); + } + else if(d->gunselect!=GUN_FIST) adddecal(DECAL_BULLET, to, vec(from).sub(to).safenormalize(), d->gunselect==GUN_RIFLE ? 3.0f : 2.0f); + } + + void shoot(fpsent *d, const vec &targ) + { + int prevaction = d->lastaction, attacktime = lastmillis-prevaction; + if(attacktime<d->gunwait) return; + d->gunwait = 0; + if((d==player1 || d->ai) && !d->attacking) return; + d->lastaction = lastmillis; + d->lastattackgun = d->gunselect; + if(!d->ammo[d->gunselect]) + { + if(d==player1) + { + msgsound(S_NOAMMO, d); + d->gunwait = 600; + d->lastattackgun = -1; + weaponswitch(d); + } + return; + } + if(d->gunselect) d->ammo[d->gunselect]--; + + pwshot(d->gunselect); /// PW + + vec from = d->o, to = targ, dir = vec(to).sub(from).safenormalize(); + float dist = to.dist(from); + vec kickback = vec(dir).mul(guns[d->gunselect].kickamount*-2.5f); + d->vel.add(kickback); + float shorten = 0; + if(guns[d->gunselect].range && dist > guns[d->gunselect].range) + shorten = guns[d->gunselect].range; + float barrier = raycube(d->o, dir, dist, RAY_CLIPMAT|RAY_ALPHAPOLY); + if(barrier > 0 && barrier < dist && (!shorten || barrier < shorten)) + shorten = barrier; + if(shorten) to = vec(dir).mul(shorten).add(from); + + if(guns[d->gunselect].rays > 1) createrays(d->gunselect, from, to); + else if(guns[d->gunselect].spread) offsetray(from, to, guns[d->gunselect].spread, guns[d->gunselect].range, to); + + hits.setsize(0); + + if(!guns[d->gunselect].projspeed) raydamage(from, to, d); + + shoteffects(d->gunselect, from, to, d, true, 0, prevaction); + + if(d==player1 || d->ai) + { + addmsg(N_SHOOT, "rci2i6iv", d, lastmillis-maptime, d->gunselect, + (int)(from.x*DMF), (int)(from.y*DMF), (int)(from.z*DMF), + (int)(to.x*DMF), (int)(to.y*DMF), (int)(to.z*DMF), + hits.length(), hits.length()*sizeof(hitmsg)/sizeof(int), hits.getbuf()); + } + + d->gunwait = guns[d->gunselect].attackdelay; + if(d->gunselect == GUN_PISTOL && d->ai) d->gunwait += int(d->gunwait*(((101-d->skill)+rnd(111-d->skill))/100.f)); + d->totalshots += guns[d->gunselect].damage*(d->quadmillis ? 4 : 1)*guns[d->gunselect].rays; + } + + void adddynlights() + { + loopv(projs) + { + projectile &p = projs[i]; + if(p.gun!=GUN_RL) continue; + vec pos(p.o); + pos.add(vec(p.offset).mul(p.offsetmillis/float(OFFSETMILLIS))); + adddynlight(pos, 20, vec(1, 0.75f, 0.5f)); + } + loopv(bouncers) + { + bouncer &bnc = *bouncers[i]; + if(bnc.bouncetype!=BNC_GRENADE) continue; + vec pos = bnc.offsetpos(); + adddynlight(pos, 8, vec(0.25f, 1, 1)); + } + } + + static const char * const projnames[2] = { "projectiles/grenade", "projectiles/rocket" }; + + void preloadbouncers() { + loopi(sizeof(projnames)/sizeof(projnames[0])) preloadmodel(projnames[i]); + } + + void renderbouncers() + { + float yaw, pitch; + loopv(bouncers) + { + bouncer &bnc = *bouncers[i]; + vec pos = bnc.offsetpos(); + vec vel(bnc.vel); + if(vel.magnitude() <= 25.0f) yaw = bnc.lastyaw; + else + { + vectoyawpitch(vel, yaw, pitch); + yaw += 90; + bnc.lastyaw = yaw; + } + pitch = -bnc.roll; + if(bnc.bouncetype==BNC_GRENADE) + rendermodel(&bnc.light, "projectiles/grenade", ANIM_MAPMODEL|ANIM_LOOP, pos, yaw, pitch, MDL_CULL_VFC|MDL_CULL_OCCLUDED|MDL_LIGHT|MDL_LIGHT_FAST|MDL_DYNSHADOW); + else + { + const char *mdl = NULL; + int cull = MDL_CULL_VFC|MDL_CULL_DIST|MDL_CULL_OCCLUDED; + float fade = 1; + if(bnc.lifetime < 250) fade = bnc.lifetime/250.0f; + rendermodel(&bnc.light, mdl, ANIM_MAPMODEL|ANIM_LOOP, pos, yaw, pitch, cull, NULL, NULL, 0, 0, fade); + } + } + } + + void renderprojectiles() + { + float yaw, pitch; + loopv(projs) + { + projectile &p = projs[i]; + if(p.gun!=GUN_RL) continue; + float dist = min(p.o.dist(p.to)/32.0f, 1.0f); + vec pos = vec(p.o).add(vec(p.offset).mul(dist*p.offsetmillis/float(OFFSETMILLIS))), + v = dist < 1e-6f ? p.dir : vec(p.to).sub(pos).normalize(); + // the amount of distance in front of the smoke trail needs to change if the model does + vectoyawpitch(v, yaw, pitch); + yaw += 90; + v.mul(3); + v.add(pos); + rendermodel(&p.light, "projectiles/rocket", ANIM_MAPMODEL|ANIM_LOOP, v, yaw, pitch, MDL_CULL_VFC|MDL_CULL_OCCLUDED|MDL_LIGHT|MDL_LIGHT_FAST); + } + } + + void checkattacksound(fpsent *d, bool local) + { + int gun = -1; + switch(d->attacksound) + { + case S_CHAINSAW_ATTACK: + if(chainsawhudgun) gun = GUN_FIST; + break; + default: + return; + } + if(gun >= 0 && gun < NUMGUNS && + d->clientnum >= 0 && d->state == CS_ALIVE && + d->lastattackgun == gun && lastmillis - d->lastaction < guns[gun].attackdelay + 50) + { + d->attackchan = playsound(d->attacksound, local ? NULL : &d->o, NULL, 0, -1, -1, d->attackchan); + if(d->attackchan < 0) d->attacksound = -1; + } + else d->stopattacksound(); + } + + void checkidlesound(fpsent *d, bool local) + { + int sound = -1, radius = 0; + if(d->clientnum >= 0 && d->state == CS_ALIVE) switch(d->gunselect) + { + case GUN_FIST: + if(chainsawhudgun && d->attacksound < 0) + { + sound = S_CHAINSAW_IDLE; + radius = 50; + } + break; + } + if(d->idlesound != sound) + { + if(d->idlesound >= 0) d->stopidlesound(); + if(sound >= 0) + { + d->idlechan = playsound(sound, local ? NULL : &d->o, NULL, 0, -1, 100, d->idlechan, radius); + if(d->idlechan >= 0) d->idlesound = sound; + } + } + else if(sound >= 0) + { + d->idlechan = playsound(sound, local ? NULL : &d->o, NULL, 0, -1, -1, d->idlechan, radius); + if(d->idlechan < 0) d->idlesound = -1; + } + } + + void removeweapons(fpsent *d) + { + removebouncers(d); + removeprojectiles(d); + } + + void updateweapons(int curtime) + { + updateprojectiles(curtime); + pwcalcaccuracy(); + if(player1->clientnum>=0 && player1->state==CS_ALIVE) shoot(player1, worldpos); // only shoot when connected to server + updatebouncers(curtime); // need to do this after the player shoots so grenades don't end up inside player's BB next frame + fpsent *following = followingplayer(); + if(!following) following = player1; + loopv(players) + { + fpsent *d = players[i]; + checkattacksound(d, d==following); + checkidlesound(d, d==following); + } + } + + void avoidweapons(ai::avoidset &obstacles, float radius) + { + loopv(projs) + { + projectile &p = projs[i]; + obstacles.avoidnear(NULL, p.o.z + guns[p.gun].exprad + 1, p.o, radius + guns[p.gun].exprad); + } + loopv(bouncers) + { + bouncer &bnc = *bouncers[i]; + if(bnc.bouncetype != BNC_GRENADE) continue; + obstacles.avoidnear(NULL, bnc.o.z + guns[GUN_GL].exprad + 1, bnc.o, radius + guns[GUN_GL].exprad); + } + } }; /// Rough accuracy code, client-side only. int pwshotsfired [NUMGUNS] = { 0 }; -int pwshotshit [NUMGUNS] = { 0 }; +int pwshotshit [NUMGUNS] = { 0 }; int pwdamagedealt [NUMGUNS] = { 0 }; -int pwaccuracy [NUMGUNS] = { 0 }; +int pwaccuracy [NUMGUNS] = { 0 }; int pwavgaccuracy = 0; void pwshot(int gun) { - pwshotsfired[gun]++; + pwshotsfired[gun]++; } void pwhit(int gun, int damage) { - pwshotshit[gun]++; - pwdamagedealt[gun] += damage; + pwshotshit[gun]++; + pwdamagedealt[gun] += damage; } void pwcalcaccuracy(void) { - pwavgaccuracy = 0; - loopi(NUMGUNS) - if(pwshotsfired[i]) - pwaccuracy[i] = (pwdamagedealt[i] * 100) / (guns[i].damage * guns[i].rays * pwshotsfired[i]); - else - pwaccuracy[i] = 0; - loopi(NUMGUNS) pwavgaccuracy += pwaccuracy[i]; - pwavgaccuracy /= NUMGUNS; + pwavgaccuracy = 0; + loopi(NUMGUNS) + if(pwshotsfired[i]) + pwaccuracy[i] = (pwdamagedealt[i] * 100) / (guns[i].damage * guns[i].rays * pwshotsfired[i]); + else + pwaccuracy[i] = 0; + loopi(NUMGUNS) pwavgaccuracy += pwaccuracy[i]; + pwavgaccuracy /= NUMGUNS; } void pwreset(void) { - loopi(NUMGUNS) pwshotsfired[i] = pwshotshit[i] = pwdamagedealt[i] = pwaccuracy[i] = 0; - loopi(7) pwitemspicked[i] = 0; - pwavgaccuracy = 0; + loopi(NUMGUNS) pwshotsfired[i] = pwshotshit[i] = pwdamagedealt[i] = pwaccuracy[i] = 0; + loopi(7) pwitemspicked[i] = 0; + pwavgaccuracy = 0; } |
