diff options
Diffstat (limited to 'src/game')
35 files changed, 54393 insertions, 0 deletions
diff --git a/src/game/bg_lib.c b/src/game/bg_lib.c new file mode 100644 index 0000000..69bca48 --- /dev/null +++ b/src/game/bg_lib.c @@ -0,0 +1,2045 @@ +/* +=========================================================================== +Copyright (C) 1999-2005 Id Software, Inc. +Copyright (C) 2000-2006 Tim Angus + +This file is part of Tremulous. + +Tremulous is free software; you can redistribute it +and/or modify it under the terms of the GNU General Public License as +published by the Free Software Foundation; either version 2 of the License, +or (at your option) any later version. + +Tremulous is distributed in the hope that it will be +useful, but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with Tremulous; if not, write to the Free Software +Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA +=========================================================================== +*/ + +// bg_lib.c -- standard C library replacement routines used by code +// compiled for the virtual machine + + +#include "../qcommon/q_shared.h" + +/*- + * Copyright (c) 1992, 1993 + * The Regents of the University of California. All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * 3. All advertising materials mentioning features or use of this software + * must display the following acknowledgement: + * This product includes software developed by the University of + * California, Berkeley and its contributors. + * 4. Neither the name of the University nor the names of its contributors + * may be used to endorse or promote products derived from this software + * without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ``AS IS'' AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS + * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) + * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT + * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY + * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF + * SUCH DAMAGE. + */ + +#include "bg_lib.h" + +#if defined(LIBC_SCCS) && !defined(lint) +#if 0 +static char sccsid[] = "@(#)qsort.c 8.1 (Berkeley) 6/4/93"; +#endif +static const char rcsid[] = + "$Id: bg_lib.c 965 2007-08-09 13:54:12Z msk $"; +#endif /* LIBC_SCCS and not lint */ + +// bk001127 - needed for DLL's +#if !defined( Q3_VM ) +typedef int cmp_t(const void *, const void *); +#endif + +static char* med3(char *, char *, char *, cmp_t *); +static void swapfunc(char *, char *, int, int); + +#ifndef min +#define min(a, b) (a) < (b) ? a : b +#endif + +/* + * Qsort routine from Bentley & McIlroy's "Engineering a Sort Function". + */ +#define swapcode(TYPE, parmi, parmj, n) { \ + long i = (n) / sizeof (TYPE); \ + register TYPE *pi = (TYPE *) (parmi); \ + register TYPE *pj = (TYPE *) (parmj); \ + do { \ + register TYPE t = *pi; \ + *pi++ = *pj; \ + *pj++ = t; \ + } while (--i > 0); \ +} + +#define SWAPINIT(a, es) swaptype = ((char *)a - (char *)0) % sizeof(long) || \ + es % sizeof(long) ? 2 : es == sizeof(long)? 0 : 1; + +static void +swapfunc(a, b, n, swaptype) + char *a, *b; + int n, swaptype; +{ + if(swaptype <= 1) + swapcode(long, a, b, n) + else + swapcode(char, a, b, n) +} + +#define swap(a, b) \ + if (swaptype == 0) { \ + long t = *(long *)(a); \ + *(long *)(a) = *(long *)(b); \ + *(long *)(b) = t; \ + } else \ + swapfunc(a, b, es, swaptype) + +#define vecswap(a, b, n) if ((n) > 0) swapfunc(a, b, n, swaptype) + +static char * +med3(a, b, c, cmp) + char *a, *b, *c; + cmp_t *cmp; +{ + return cmp(a, b) < 0 ? + (cmp(b, c) < 0 ? b : (cmp(a, c) < 0 ? c : a )) + :(cmp(b, c) > 0 ? b : (cmp(a, c) < 0 ? a : c )); +} + +void +qsort(a, n, es, cmp) + void *a; + size_t n, es; + cmp_t *cmp; +{ + char *pa, *pb, *pc, *pd, *pl, *pm, *pn; + int d, r, swaptype, swap_cnt; + +loop: SWAPINIT(a, es); + swap_cnt = 0; + if (n < 7) { + for (pm = (char *)a + es; pm < (char *)a + n * es; pm += es) + for (pl = pm; pl > (char *)a && cmp(pl - es, pl) > 0; + pl -= es) + swap(pl, pl - es); + return; + } + pm = (char *)a + (n / 2) * es; + if (n > 7) { + pl = a; + pn = (char *)a + (n - 1) * es; + if (n > 40) { + d = (n / 8) * es; + pl = med3(pl, pl + d, pl + 2 * d, cmp); + pm = med3(pm - d, pm, pm + d, cmp); + pn = med3(pn - 2 * d, pn - d, pn, cmp); + } + pm = med3(pl, pm, pn, cmp); + } + swap(a, pm); + pa = pb = (char *)a + es; + + pc = pd = (char *)a + (n - 1) * es; + for (;;) { + while (pb <= pc && (r = cmp(pb, a)) <= 0) { + if (r == 0) { + swap_cnt = 1; + swap(pa, pb); + pa += es; + } + pb += es; + } + while (pb <= pc && (r = cmp(pc, a)) >= 0) { + if (r == 0) { + swap_cnt = 1; + swap(pc, pd); + pd -= es; + } + pc -= es; + } + if (pb > pc) + break; + swap(pb, pc); + swap_cnt = 1; + pb += es; + pc -= es; + } + if (swap_cnt == 0) { /* Switch to insertion sort */ + for (pm = (char *)a + es; pm < (char *)a + n * es; pm += es) + for (pl = pm; pl > (char *)a && cmp(pl - es, pl) > 0; + pl -= es) + swap(pl, pl - es); + return; + } + + pn = (char *)a + n * es; + r = min(pa - (char *)a, pb - pa); + vecswap(a, pb - r, r); + r = min(pd - pc, pn - pd - es); + vecswap(pb, pn - r, r); + if ((r = pb - pa) > es) + qsort(a, r / es, es, cmp); + if ((r = pd - pc) > es) { + /* Iterate rather than recurse to save stack space */ + a = pn - r; + n = r / es; + goto loop; + } +/* qsort(pn - r, r / es, es, cmp);*/ +} + +//================================================================================== + + +// this file is excluded from release builds because of intrinsics + +// bk001211 - gcc errors on compiling strcpy: parse error before `__extension__' +#if defined ( Q3_VM ) + +size_t strlen( const char *string ) +{ + const char *s; + + s = string; + while( *s ) + s++; + + return s - string; +} + + +char *strcat( char *strDestination, const char *strSource ) +{ + char *s; + + s = strDestination; + while( *s ) + s++; + + while( *strSource ) + *s++ = *strSource++; + + *s = 0; + return strDestination; +} + +char *strcpy( char *strDestination, const char *strSource ) +{ + char *s; + + s = strDestination; + + while( *strSource ) + *s++ = *strSource++; + + *s = 0; + return strDestination; +} + + +int strcmp( const char *string1, const char *string2 ) +{ + while( *string1 == *string2 && *string1 && *string2 ) + { + string1++; + string2++; + } + + return *string1 - *string2; +} + +//TA: +char *strrchr( const char *string, int c ) +{ + int i, length = strlen( string ); + char *p; + + for( i = length - 1; i >= 0; i-- ) + { + p = (char *)&string[ i ]; + + if( *p == c ) + return (char *)p; + } + + return (char *)0; +} + +char *strchr( const char *string, int c ) +{ + while( *string ) + { + if( *string == c ) + return ( char * )string; + + string++; + } + return (char *)0; +} + +char *strstr( const char *string, const char *strCharSet ) +{ + while( *string ) + { + int i; + + for( i = 0; strCharSet[ i ]; i++ ) + { + if( string[ i ] != strCharSet[ i ] ) + break; + } + + if( !strCharSet[ i ] ) + return (char *)string; + + string++; + } + return (char *)0; +} + +#endif // bk001211 + +#if defined ( Q3_VM ) + +int tolower( int c ) +{ + if( c >= 'A' && c <= 'Z' ) + c += 'a' - 'A'; + + return c; +} + + +int toupper( int c ) +{ + if( c >= 'a' && c <= 'z' ) + c += 'A' - 'a'; + + return c; +} + +#endif + +void *memmove( void *dest, const void *src, size_t count ) +{ + int i; + + if( dest > src ) + { + for( i = count - 1; i >= 0; i-- ) + ( (char *)dest )[ i ] = ( (char *)src )[ i ]; + } + else + { + for( i = 0; i < count; i++ ) + ( (char *)dest )[ i ] = ( (char *)src )[ i ]; + } + + return dest; +} + + +#if 0 + +double floor( double x ) { + return (int)(x + 0x40000000) - 0x40000000; +} + +void *memset( void *dest, int c, size_t count ) { + while ( count-- ) { + ((char *)dest)[count] = c; + } + return dest; +} + +void *memcpy( void *dest, const void *src, size_t count ) { + while ( count-- ) { + ((char *)dest)[count] = ((char *)src)[count]; + } + return dest; +} + +char *strncpy( char *strDest, const char *strSource, size_t count ) { + char *s; + + s = strDest; + while ( *strSource && count ) { + *s++ = *strSource++; + count--; + } + while ( count-- ) { + *s++ = 0; + } + return strDest; +} + +double sqrt( double x ) { + float y; + float delta; + float maxError; + + if ( x <= 0 ) { + return 0; + } + + // initial guess + y = x / 2; + + // refine + maxError = x * 0.001; + + do { + delta = ( y * y ) - x; + y -= delta / ( 2 * y ); + } while ( delta > maxError || delta < -maxError ); + + return y; +} + + +float sintable[1024] = { +0.000000,0.001534,0.003068,0.004602,0.006136,0.007670,0.009204,0.010738, +0.012272,0.013805,0.015339,0.016873,0.018407,0.019940,0.021474,0.023008, +0.024541,0.026075,0.027608,0.029142,0.030675,0.032208,0.033741,0.035274, +0.036807,0.038340,0.039873,0.041406,0.042938,0.044471,0.046003,0.047535, +0.049068,0.050600,0.052132,0.053664,0.055195,0.056727,0.058258,0.059790, +0.061321,0.062852,0.064383,0.065913,0.067444,0.068974,0.070505,0.072035, +0.073565,0.075094,0.076624,0.078153,0.079682,0.081211,0.082740,0.084269, +0.085797,0.087326,0.088854,0.090381,0.091909,0.093436,0.094963,0.096490, +0.098017,0.099544,0.101070,0.102596,0.104122,0.105647,0.107172,0.108697, +0.110222,0.111747,0.113271,0.114795,0.116319,0.117842,0.119365,0.120888, +0.122411,0.123933,0.125455,0.126977,0.128498,0.130019,0.131540,0.133061, +0.134581,0.136101,0.137620,0.139139,0.140658,0.142177,0.143695,0.145213, +0.146730,0.148248,0.149765,0.151281,0.152797,0.154313,0.155828,0.157343, +0.158858,0.160372,0.161886,0.163400,0.164913,0.166426,0.167938,0.169450, +0.170962,0.172473,0.173984,0.175494,0.177004,0.178514,0.180023,0.181532, +0.183040,0.184548,0.186055,0.187562,0.189069,0.190575,0.192080,0.193586, +0.195090,0.196595,0.198098,0.199602,0.201105,0.202607,0.204109,0.205610, +0.207111,0.208612,0.210112,0.211611,0.213110,0.214609,0.216107,0.217604, +0.219101,0.220598,0.222094,0.223589,0.225084,0.226578,0.228072,0.229565, +0.231058,0.232550,0.234042,0.235533,0.237024,0.238514,0.240003,0.241492, +0.242980,0.244468,0.245955,0.247442,0.248928,0.250413,0.251898,0.253382, +0.254866,0.256349,0.257831,0.259313,0.260794,0.262275,0.263755,0.265234, +0.266713,0.268191,0.269668,0.271145,0.272621,0.274097,0.275572,0.277046, +0.278520,0.279993,0.281465,0.282937,0.284408,0.285878,0.287347,0.288816, +0.290285,0.291752,0.293219,0.294685,0.296151,0.297616,0.299080,0.300543, +0.302006,0.303468,0.304929,0.306390,0.307850,0.309309,0.310767,0.312225, +0.313682,0.315138,0.316593,0.318048,0.319502,0.320955,0.322408,0.323859, +0.325310,0.326760,0.328210,0.329658,0.331106,0.332553,0.334000,0.335445, +0.336890,0.338334,0.339777,0.341219,0.342661,0.344101,0.345541,0.346980, +0.348419,0.349856,0.351293,0.352729,0.354164,0.355598,0.357031,0.358463, +0.359895,0.361326,0.362756,0.364185,0.365613,0.367040,0.368467,0.369892, +0.371317,0.372741,0.374164,0.375586,0.377007,0.378428,0.379847,0.381266, +0.382683,0.384100,0.385516,0.386931,0.388345,0.389758,0.391170,0.392582, +0.393992,0.395401,0.396810,0.398218,0.399624,0.401030,0.402435,0.403838, +0.405241,0.406643,0.408044,0.409444,0.410843,0.412241,0.413638,0.415034, +0.416430,0.417824,0.419217,0.420609,0.422000,0.423390,0.424780,0.426168, +0.427555,0.428941,0.430326,0.431711,0.433094,0.434476,0.435857,0.437237, +0.438616,0.439994,0.441371,0.442747,0.444122,0.445496,0.446869,0.448241, +0.449611,0.450981,0.452350,0.453717,0.455084,0.456449,0.457813,0.459177, +0.460539,0.461900,0.463260,0.464619,0.465976,0.467333,0.468689,0.470043, +0.471397,0.472749,0.474100,0.475450,0.476799,0.478147,0.479494,0.480839, +0.482184,0.483527,0.484869,0.486210,0.487550,0.488889,0.490226,0.491563, +0.492898,0.494232,0.495565,0.496897,0.498228,0.499557,0.500885,0.502212, +0.503538,0.504863,0.506187,0.507509,0.508830,0.510150,0.511469,0.512786, +0.514103,0.515418,0.516732,0.518045,0.519356,0.520666,0.521975,0.523283, +0.524590,0.525895,0.527199,0.528502,0.529804,0.531104,0.532403,0.533701, +0.534998,0.536293,0.537587,0.538880,0.540171,0.541462,0.542751,0.544039, +0.545325,0.546610,0.547894,0.549177,0.550458,0.551738,0.553017,0.554294, +0.555570,0.556845,0.558119,0.559391,0.560662,0.561931,0.563199,0.564466, +0.565732,0.566996,0.568259,0.569521,0.570781,0.572040,0.573297,0.574553, +0.575808,0.577062,0.578314,0.579565,0.580814,0.582062,0.583309,0.584554, +0.585798,0.587040,0.588282,0.589521,0.590760,0.591997,0.593232,0.594466, +0.595699,0.596931,0.598161,0.599389,0.600616,0.601842,0.603067,0.604290, +0.605511,0.606731,0.607950,0.609167,0.610383,0.611597,0.612810,0.614022, +0.615232,0.616440,0.617647,0.618853,0.620057,0.621260,0.622461,0.623661, +0.624859,0.626056,0.627252,0.628446,0.629638,0.630829,0.632019,0.633207, +0.634393,0.635578,0.636762,0.637944,0.639124,0.640303,0.641481,0.642657, +0.643832,0.645005,0.646176,0.647346,0.648514,0.649681,0.650847,0.652011, +0.653173,0.654334,0.655493,0.656651,0.657807,0.658961,0.660114,0.661266, +0.662416,0.663564,0.664711,0.665856,0.667000,0.668142,0.669283,0.670422, +0.671559,0.672695,0.673829,0.674962,0.676093,0.677222,0.678350,0.679476, +0.680601,0.681724,0.682846,0.683965,0.685084,0.686200,0.687315,0.688429, +0.689541,0.690651,0.691759,0.692866,0.693971,0.695075,0.696177,0.697278, +0.698376,0.699473,0.700569,0.701663,0.702755,0.703845,0.704934,0.706021, +0.707107,0.708191,0.709273,0.710353,0.711432,0.712509,0.713585,0.714659, +0.715731,0.716801,0.717870,0.718937,0.720003,0.721066,0.722128,0.723188, +0.724247,0.725304,0.726359,0.727413,0.728464,0.729514,0.730563,0.731609, +0.732654,0.733697,0.734739,0.735779,0.736817,0.737853,0.738887,0.739920, +0.740951,0.741980,0.743008,0.744034,0.745058,0.746080,0.747101,0.748119, +0.749136,0.750152,0.751165,0.752177,0.753187,0.754195,0.755201,0.756206, +0.757209,0.758210,0.759209,0.760207,0.761202,0.762196,0.763188,0.764179, +0.765167,0.766154,0.767139,0.768122,0.769103,0.770083,0.771061,0.772036, +0.773010,0.773983,0.774953,0.775922,0.776888,0.777853,0.778817,0.779778, +0.780737,0.781695,0.782651,0.783605,0.784557,0.785507,0.786455,0.787402, +0.788346,0.789289,0.790230,0.791169,0.792107,0.793042,0.793975,0.794907, +0.795837,0.796765,0.797691,0.798615,0.799537,0.800458,0.801376,0.802293, +0.803208,0.804120,0.805031,0.805940,0.806848,0.807753,0.808656,0.809558, +0.810457,0.811355,0.812251,0.813144,0.814036,0.814926,0.815814,0.816701, +0.817585,0.818467,0.819348,0.820226,0.821103,0.821977,0.822850,0.823721, +0.824589,0.825456,0.826321,0.827184,0.828045,0.828904,0.829761,0.830616, +0.831470,0.832321,0.833170,0.834018,0.834863,0.835706,0.836548,0.837387, +0.838225,0.839060,0.839894,0.840725,0.841555,0.842383,0.843208,0.844032, +0.844854,0.845673,0.846491,0.847307,0.848120,0.848932,0.849742,0.850549, +0.851355,0.852159,0.852961,0.853760,0.854558,0.855354,0.856147,0.856939, +0.857729,0.858516,0.859302,0.860085,0.860867,0.861646,0.862424,0.863199, +0.863973,0.864744,0.865514,0.866281,0.867046,0.867809,0.868571,0.869330, +0.870087,0.870842,0.871595,0.872346,0.873095,0.873842,0.874587,0.875329, +0.876070,0.876809,0.877545,0.878280,0.879012,0.879743,0.880471,0.881197, +0.881921,0.882643,0.883363,0.884081,0.884797,0.885511,0.886223,0.886932, +0.887640,0.888345,0.889048,0.889750,0.890449,0.891146,0.891841,0.892534, +0.893224,0.893913,0.894599,0.895284,0.895966,0.896646,0.897325,0.898001, +0.898674,0.899346,0.900016,0.900683,0.901349,0.902012,0.902673,0.903332, +0.903989,0.904644,0.905297,0.905947,0.906596,0.907242,0.907886,0.908528, +0.909168,0.909806,0.910441,0.911075,0.911706,0.912335,0.912962,0.913587, +0.914210,0.914830,0.915449,0.916065,0.916679,0.917291,0.917901,0.918508, +0.919114,0.919717,0.920318,0.920917,0.921514,0.922109,0.922701,0.923291, +0.923880,0.924465,0.925049,0.925631,0.926210,0.926787,0.927363,0.927935, +0.928506,0.929075,0.929641,0.930205,0.930767,0.931327,0.931884,0.932440, +0.932993,0.933544,0.934093,0.934639,0.935184,0.935726,0.936266,0.936803, +0.937339,0.937872,0.938404,0.938932,0.939459,0.939984,0.940506,0.941026, +0.941544,0.942060,0.942573,0.943084,0.943593,0.944100,0.944605,0.945107, +0.945607,0.946105,0.946601,0.947094,0.947586,0.948075,0.948561,0.949046, +0.949528,0.950008,0.950486,0.950962,0.951435,0.951906,0.952375,0.952842, +0.953306,0.953768,0.954228,0.954686,0.955141,0.955594,0.956045,0.956494, +0.956940,0.957385,0.957826,0.958266,0.958703,0.959139,0.959572,0.960002, +0.960431,0.960857,0.961280,0.961702,0.962121,0.962538,0.962953,0.963366, +0.963776,0.964184,0.964590,0.964993,0.965394,0.965793,0.966190,0.966584, +0.966976,0.967366,0.967754,0.968139,0.968522,0.968903,0.969281,0.969657, +0.970031,0.970403,0.970772,0.971139,0.971504,0.971866,0.972226,0.972584, +0.972940,0.973293,0.973644,0.973993,0.974339,0.974684,0.975025,0.975365, +0.975702,0.976037,0.976370,0.976700,0.977028,0.977354,0.977677,0.977999, +0.978317,0.978634,0.978948,0.979260,0.979570,0.979877,0.980182,0.980485, +0.980785,0.981083,0.981379,0.981673,0.981964,0.982253,0.982539,0.982824, +0.983105,0.983385,0.983662,0.983937,0.984210,0.984480,0.984749,0.985014, +0.985278,0.985539,0.985798,0.986054,0.986308,0.986560,0.986809,0.987057, +0.987301,0.987544,0.987784,0.988022,0.988258,0.988491,0.988722,0.988950, +0.989177,0.989400,0.989622,0.989841,0.990058,0.990273,0.990485,0.990695, +0.990903,0.991108,0.991311,0.991511,0.991710,0.991906,0.992099,0.992291, +0.992480,0.992666,0.992850,0.993032,0.993212,0.993389,0.993564,0.993737, +0.993907,0.994075,0.994240,0.994404,0.994565,0.994723,0.994879,0.995033, +0.995185,0.995334,0.995481,0.995625,0.995767,0.995907,0.996045,0.996180, +0.996313,0.996443,0.996571,0.996697,0.996820,0.996941,0.997060,0.997176, +0.997290,0.997402,0.997511,0.997618,0.997723,0.997825,0.997925,0.998023, +0.998118,0.998211,0.998302,0.998390,0.998476,0.998559,0.998640,0.998719, +0.998795,0.998870,0.998941,0.999011,0.999078,0.999142,0.999205,0.999265, +0.999322,0.999378,0.999431,0.999481,0.999529,0.999575,0.999619,0.999660, +0.999699,0.999735,0.999769,0.999801,0.999831,0.999858,0.999882,0.999905, +0.999925,0.999942,0.999958,0.999971,0.999981,0.999989,0.999995,0.999999 +}; + +double sin( double x ) { + int index; + int quad; + + index = 1024 * x / (M_PI * 0.5); + quad = ( index >> 10 ) & 3; + index &= 1023; + switch ( quad ) { + case 0: + return sintable[index]; + case 1: + return sintable[1023-index]; + case 2: + return -sintable[index]; + case 3: + return -sintable[1023-index]; + } + return 0; +} + + +double cos( double x ) { + int index; + int quad; + + index = 1024 * x / (M_PI * 0.5); + quad = ( index >> 10 ) & 3; + index &= 1023; + switch ( quad ) { + case 3: + return sintable[index]; + case 0: + return sintable[1023-index]; + case 1: + return -sintable[index]; + case 2: + return -sintable[1023-index]; + } + return 0; +} + + +/* +void create_acostable( void ) { + int i; + FILE *fp; + float a; + + fp = fopen("c:\\acostable.txt", "w"); + fprintf(fp, "float acostable[] = {"); + for (i = 0; i < 1024; i++) { + if (!(i & 7)) + fprintf(fp, "\n"); + a = acos( (float) -1 + i / 512 ); + fprintf(fp, "%1.8f,", a); + } + fprintf(fp, "\n}\n"); + fclose(fp); +} +*/ + + +float acostable[] = { +3.14159265,3.07908248,3.05317551,3.03328655,3.01651113,3.00172442,2.98834964,2.97604422, +2.96458497,2.95381690,2.94362719,2.93393068,2.92466119,2.91576615,2.90720289,2.89893629, +2.89093699,2.88318015,2.87564455,2.86831188,2.86116621,2.85419358,2.84738169,2.84071962, +2.83419760,2.82780691,2.82153967,2.81538876,2.80934770,2.80341062,2.79757211,2.79182724, +2.78617145,2.78060056,2.77511069,2.76969824,2.76435988,2.75909250,2.75389319,2.74875926, +2.74368816,2.73867752,2.73372510,2.72882880,2.72398665,2.71919677,2.71445741,2.70976688, +2.70512362,2.70052613,2.69597298,2.69146283,2.68699438,2.68256642,2.67817778,2.67382735, +2.66951407,2.66523692,2.66099493,2.65678719,2.65261279,2.64847088,2.64436066,2.64028133, +2.63623214,2.63221238,2.62822133,2.62425835,2.62032277,2.61641398,2.61253138,2.60867440, +2.60484248,2.60103507,2.59725167,2.59349176,2.58975488,2.58604053,2.58234828,2.57867769, +2.57502832,2.57139977,2.56779164,2.56420354,2.56063509,2.55708594,2.55355572,2.55004409, +2.54655073,2.54307530,2.53961750,2.53617701,2.53275354,2.52934680,2.52595650,2.52258238, +2.51922417,2.51588159,2.51255441,2.50924238,2.50594525,2.50266278,2.49939476,2.49614096, +2.49290115,2.48967513,2.48646269,2.48326362,2.48007773,2.47690482,2.47374472,2.47059722, +2.46746215,2.46433933,2.46122860,2.45812977,2.45504269,2.45196720,2.44890314,2.44585034, +2.44280867,2.43977797,2.43675809,2.43374890,2.43075025,2.42776201,2.42478404,2.42181622, +2.41885841,2.41591048,2.41297232,2.41004380,2.40712480,2.40421521,2.40131491,2.39842379, +2.39554173,2.39266863,2.38980439,2.38694889,2.38410204,2.38126374,2.37843388,2.37561237, +2.37279910,2.36999400,2.36719697,2.36440790,2.36162673,2.35885335,2.35608768,2.35332964, +2.35057914,2.34783610,2.34510044,2.34237208,2.33965094,2.33693695,2.33423003,2.33153010, +2.32883709,2.32615093,2.32347155,2.32079888,2.31813284,2.31547337,2.31282041,2.31017388, +2.30753373,2.30489988,2.30227228,2.29965086,2.29703556,2.29442632,2.29182309,2.28922580, +2.28663439,2.28404881,2.28146900,2.27889490,2.27632647,2.27376364,2.27120637,2.26865460, +2.26610827,2.26356735,2.26103177,2.25850149,2.25597646,2.25345663,2.25094195,2.24843238, +2.24592786,2.24342836,2.24093382,2.23844420,2.23595946,2.23347956,2.23100444,2.22853408, +2.22606842,2.22360742,2.22115104,2.21869925,2.21625199,2.21380924,2.21137096,2.20893709, +2.20650761,2.20408248,2.20166166,2.19924511,2.19683280,2.19442469,2.19202074,2.18962092, +2.18722520,2.18483354,2.18244590,2.18006225,2.17768257,2.17530680,2.17293493,2.17056692, +2.16820274,2.16584236,2.16348574,2.16113285,2.15878367,2.15643816,2.15409630,2.15175805, +2.14942338,2.14709226,2.14476468,2.14244059,2.14011997,2.13780279,2.13548903,2.13317865, +2.13087163,2.12856795,2.12626757,2.12397047,2.12167662,2.11938600,2.11709859,2.11481435, +2.11253326,2.11025530,2.10798044,2.10570867,2.10343994,2.10117424,2.09891156,2.09665185, +2.09439510,2.09214129,2.08989040,2.08764239,2.08539725,2.08315496,2.08091550,2.07867884, +2.07644495,2.07421383,2.07198545,2.06975978,2.06753681,2.06531651,2.06309887,2.06088387, +2.05867147,2.05646168,2.05425445,2.05204979,2.04984765,2.04764804,2.04545092,2.04325628, +2.04106409,2.03887435,2.03668703,2.03450211,2.03231957,2.03013941,2.02796159,2.02578610, +2.02361292,2.02144204,2.01927344,2.01710710,2.01494300,2.01278113,2.01062146,2.00846399, +2.00630870,2.00415556,2.00200457,1.99985570,1.99770895,1.99556429,1.99342171,1.99128119, +1.98914271,1.98700627,1.98487185,1.98273942,1.98060898,1.97848051,1.97635399,1.97422942, +1.97210676,1.96998602,1.96786718,1.96575021,1.96363511,1.96152187,1.95941046,1.95730088, +1.95519310,1.95308712,1.95098292,1.94888050,1.94677982,1.94468089,1.94258368,1.94048818, +1.93839439,1.93630228,1.93421185,1.93212308,1.93003595,1.92795046,1.92586659,1.92378433, +1.92170367,1.91962459,1.91754708,1.91547113,1.91339673,1.91132385,1.90925250,1.90718266, +1.90511432,1.90304746,1.90098208,1.89891815,1.89685568,1.89479464,1.89273503,1.89067683, +1.88862003,1.88656463,1.88451060,1.88245794,1.88040664,1.87835668,1.87630806,1.87426076, +1.87221477,1.87017008,1.86812668,1.86608457,1.86404371,1.86200412,1.85996577,1.85792866, +1.85589277,1.85385809,1.85182462,1.84979234,1.84776125,1.84573132,1.84370256,1.84167495, +1.83964848,1.83762314,1.83559892,1.83357582,1.83155381,1.82953289,1.82751305,1.82549429, +1.82347658,1.82145993,1.81944431,1.81742973,1.81541617,1.81340362,1.81139207,1.80938151, +1.80737194,1.80536334,1.80335570,1.80134902,1.79934328,1.79733848,1.79533460,1.79333164, +1.79132959,1.78932843,1.78732817,1.78532878,1.78333027,1.78133261,1.77933581,1.77733985, +1.77534473,1.77335043,1.77135695,1.76936428,1.76737240,1.76538132,1.76339101,1.76140148, +1.75941271,1.75742470,1.75543743,1.75345090,1.75146510,1.74948002,1.74749565,1.74551198, +1.74352900,1.74154672,1.73956511,1.73758417,1.73560389,1.73362426,1.73164527,1.72966692, +1.72768920,1.72571209,1.72373560,1.72175971,1.71978441,1.71780969,1.71583556,1.71386199, +1.71188899,1.70991653,1.70794462,1.70597325,1.70400241,1.70203209,1.70006228,1.69809297, +1.69612416,1.69415584,1.69218799,1.69022062,1.68825372,1.68628727,1.68432127,1.68235571, +1.68039058,1.67842588,1.67646160,1.67449772,1.67253424,1.67057116,1.66860847,1.66664615, +1.66468420,1.66272262,1.66076139,1.65880050,1.65683996,1.65487975,1.65291986,1.65096028, +1.64900102,1.64704205,1.64508338,1.64312500,1.64116689,1.63920905,1.63725148,1.63529416, +1.63333709,1.63138026,1.62942366,1.62746728,1.62551112,1.62355517,1.62159943,1.61964388, +1.61768851,1.61573332,1.61377831,1.61182346,1.60986877,1.60791422,1.60595982,1.60400556, +1.60205142,1.60009739,1.59814349,1.59618968,1.59423597,1.59228235,1.59032882,1.58837536, +1.58642196,1.58446863,1.58251535,1.58056211,1.57860891,1.57665574,1.57470259,1.57274945, +1.57079633,1.56884320,1.56689007,1.56493692,1.56298375,1.56103055,1.55907731,1.55712403, +1.55517069,1.55321730,1.55126383,1.54931030,1.54735668,1.54540297,1.54344917,1.54149526, +1.53954124,1.53758710,1.53563283,1.53367843,1.53172389,1.52976919,1.52781434,1.52585933, +1.52390414,1.52194878,1.51999323,1.51803748,1.51608153,1.51412537,1.51216900,1.51021240, +1.50825556,1.50629849,1.50434117,1.50238360,1.50042576,1.49846765,1.49650927,1.49455060, +1.49259163,1.49063237,1.48867280,1.48671291,1.48475270,1.48279215,1.48083127,1.47887004, +1.47690845,1.47494650,1.47298419,1.47102149,1.46905841,1.46709493,1.46513106,1.46316677, +1.46120207,1.45923694,1.45727138,1.45530538,1.45333893,1.45137203,1.44940466,1.44743682, +1.44546850,1.44349969,1.44153038,1.43956057,1.43759024,1.43561940,1.43364803,1.43167612, +1.42970367,1.42773066,1.42575709,1.42378296,1.42180825,1.41983295,1.41785705,1.41588056, +1.41390346,1.41192573,1.40994738,1.40796840,1.40598877,1.40400849,1.40202755,1.40004594, +1.39806365,1.39608068,1.39409701,1.39211264,1.39012756,1.38814175,1.38615522,1.38416795, +1.38217994,1.38019117,1.37820164,1.37621134,1.37422025,1.37222837,1.37023570,1.36824222, +1.36624792,1.36425280,1.36225684,1.36026004,1.35826239,1.35626387,1.35426449,1.35226422, +1.35026307,1.34826101,1.34625805,1.34425418,1.34224937,1.34024364,1.33823695,1.33622932, +1.33422072,1.33221114,1.33020059,1.32818904,1.32617649,1.32416292,1.32214834,1.32013273, +1.31811607,1.31609837,1.31407960,1.31205976,1.31003885,1.30801684,1.30599373,1.30396951, +1.30194417,1.29991770,1.29789009,1.29586133,1.29383141,1.29180031,1.28976803,1.28773456, +1.28569989,1.28366400,1.28162688,1.27958854,1.27754894,1.27550809,1.27346597,1.27142257, +1.26937788,1.26733189,1.26528459,1.26323597,1.26118602,1.25913471,1.25708205,1.25502803, +1.25297262,1.25091583,1.24885763,1.24679802,1.24473698,1.24267450,1.24061058,1.23854519, +1.23647833,1.23440999,1.23234015,1.23026880,1.22819593,1.22612152,1.22404557,1.22196806, +1.21988898,1.21780832,1.21572606,1.21364219,1.21155670,1.20946958,1.20738080,1.20529037, +1.20319826,1.20110447,1.19900898,1.19691177,1.19481283,1.19271216,1.19060973,1.18850553, +1.18639955,1.18429178,1.18218219,1.18007079,1.17795754,1.17584244,1.17372548,1.17160663, +1.16948589,1.16736324,1.16523866,1.16311215,1.16098368,1.15885323,1.15672081,1.15458638, +1.15244994,1.15031147,1.14817095,1.14602836,1.14388370,1.14173695,1.13958808,1.13743709, +1.13528396,1.13312866,1.13097119,1.12881153,1.12664966,1.12448556,1.12231921,1.12015061, +1.11797973,1.11580656,1.11363107,1.11145325,1.10927308,1.10709055,1.10490563,1.10271831, +1.10052856,1.09833638,1.09614174,1.09394462,1.09174500,1.08954287,1.08733820,1.08513098, +1.08292118,1.08070879,1.07849378,1.07627614,1.07405585,1.07183287,1.06960721,1.06737882, +1.06514770,1.06291382,1.06067715,1.05843769,1.05619540,1.05395026,1.05170226,1.04945136, +1.04719755,1.04494080,1.04268110,1.04041841,1.03815271,1.03588399,1.03361221,1.03133735, +1.02905939,1.02677830,1.02449407,1.02220665,1.01991603,1.01762219,1.01532509,1.01302471, +1.01072102,1.00841400,1.00610363,1.00378986,1.00147268,0.99915206,0.99682798,0.99450039, +0.99216928,0.98983461,0.98749636,0.98515449,0.98280898,0.98045980,0.97810691,0.97575030, +0.97338991,0.97102573,0.96865772,0.96628585,0.96391009,0.96153040,0.95914675,0.95675912, +0.95436745,0.95197173,0.94957191,0.94716796,0.94475985,0.94234754,0.93993099,0.93751017, +0.93508504,0.93265556,0.93022170,0.92778341,0.92534066,0.92289341,0.92044161,0.91798524, +0.91552424,0.91305858,0.91058821,0.90811309,0.90563319,0.90314845,0.90065884,0.89816430, +0.89566479,0.89316028,0.89065070,0.88813602,0.88561619,0.88309116,0.88056088,0.87802531, +0.87548438,0.87293806,0.87038629,0.86782901,0.86526619,0.86269775,0.86012366,0.85754385, +0.85495827,0.85236686,0.84976956,0.84716633,0.84455709,0.84194179,0.83932037,0.83669277, +0.83405893,0.83141877,0.82877225,0.82611928,0.82345981,0.82079378,0.81812110,0.81544172, +0.81275556,0.81006255,0.80736262,0.80465570,0.80194171,0.79922057,0.79649221,0.79375655, +0.79101352,0.78826302,0.78550497,0.78273931,0.77996593,0.77718475,0.77439569,0.77159865, +0.76879355,0.76598029,0.76315878,0.76032891,0.75749061,0.75464376,0.75178826,0.74892402, +0.74605092,0.74316887,0.74027775,0.73737744,0.73446785,0.73154885,0.72862033,0.72568217, +0.72273425,0.71977644,0.71680861,0.71383064,0.71084240,0.70784376,0.70483456,0.70181469, +0.69878398,0.69574231,0.69268952,0.68962545,0.68654996,0.68346288,0.68036406,0.67725332, +0.67413051,0.67099544,0.66784794,0.66468783,0.66151492,0.65832903,0.65512997,0.65191753, +0.64869151,0.64545170,0.64219789,0.63892987,0.63564741,0.63235028,0.62903824,0.62571106, +0.62236849,0.61901027,0.61563615,0.61224585,0.60883911,0.60541564,0.60197515,0.59851735, +0.59504192,0.59154856,0.58803694,0.58450672,0.58095756,0.57738911,0.57380101,0.57019288, +0.56656433,0.56291496,0.55924437,0.55555212,0.55183778,0.54810089,0.54434099,0.54055758, +0.53675018,0.53291825,0.52906127,0.52517867,0.52126988,0.51733431,0.51337132,0.50938028, +0.50536051,0.50131132,0.49723200,0.49312177,0.48897987,0.48480547,0.48059772,0.47635573, +0.47207859,0.46776530,0.46341487,0.45902623,0.45459827,0.45012983,0.44561967,0.44106652, +0.43646903,0.43182577,0.42713525,0.42239588,0.41760600,0.41276385,0.40786755,0.40291513, +0.39790449,0.39283339,0.38769946,0.38250016,0.37723277,0.37189441,0.36648196,0.36099209, +0.35542120,0.34976542,0.34402054,0.33818204,0.33224495,0.32620390,0.32005298,0.31378574, +0.30739505,0.30087304,0.29421096,0.28739907,0.28042645,0.27328078,0.26594810,0.25841250, +0.25065566,0.24265636,0.23438976,0.22582651,0.21693146,0.20766198,0.19796546,0.18777575, +0.17700769,0.16554844,0.15324301,0.13986823,0.12508152,0.10830610,0.08841715,0.06251018, +}; + +double acos( double x ) { + int index; + + if (x < -1) + x = -1; + if (x > 1) + x = 1; + index = (float) (1.0 + x) * 511.9; + return acostable[index]; +} + + +double atan2( double y, double x ) { + float base; + float temp; + float dir; + float test; + int i; + + if ( x < 0 ) { + if ( y >= 0 ) { + // quad 1 + base = M_PI / 2; + temp = x; + x = y; + y = -temp; + } else { + // quad 2 + base = M_PI; + x = -x; + y = -y; + } + } else { + if ( y < 0 ) { + // quad 3 + base = 3 * M_PI / 2; + temp = x; + x = -y; + y = temp; + } + } + + if ( y > x ) { + base += M_PI/2; + temp = x; + x = y; + y = temp; + dir = -1; + } else { + dir = 1; + } + + // calcualte angle in octant 0 + if ( x == 0 ) { + return base; + } + y /= x; + + for ( i = 0 ; i < 512 ; i++ ) { + test = sintable[i] / sintable[1023-i]; + if ( test > y ) { + break; + } + } + + return base + dir * i * ( M_PI/2048); +} + + +#endif + +#ifdef Q3_VM +/* +=============== +rint +=============== +*/ +double rint( double v ) +{ + if( v >= 0.5f ) + return ceil( v ); + else + return floor( v ); +} + +// bk001127 - guarded this tan replacement +// ld: undefined versioned symbol name tan@@GLIBC_2.0 +double tan( double x ) +{ + return sin( x ) / cos( x ); +} + +/* + * ==================================================== + * Copyright (C) 1993 by Sun Microsystems, Inc. All rights reserved. + * + * Developed at SunPro, a Sun Microsystems, Inc. business. + * Permission to use, copy, modify, and distribute this + * software is freely granted, provided that this notice + * is preserved. + * ==================================================== + */ + +typedef union +{ + float value; + unsigned int word; +} ieee_float_shape_type; + +/* Get a 32 bit int from a float. */ + +#define GET_FLOAT_WORD(i,d) \ +do { \ + ieee_float_shape_type gf_u; \ + gf_u.value = (d); \ + (i) = gf_u.word; \ +} while (0) + +/* Set a float from a 32 bit int. */ + +#define SET_FLOAT_WORD(d,i) \ +do { \ + ieee_float_shape_type sf_u; \ + sf_u.word = (i); \ + (d) = sf_u.value; \ +} while (0) + +/* A union which permits us to convert between a float and a 32 bit + int. */ + +//acos +static const float +pi = 3.1415925026e+00, /* 0x40490fda */ +pio2_hi = 1.5707962513e+00, /* 0x3fc90fda */ +pio2_lo = 7.5497894159e-08, /* 0x33a22168 */ +pS0 = 1.6666667163e-01, /* 0x3e2aaaab */ +pS1 = -3.2556581497e-01, /* 0xbea6b090 */ +pS2 = 2.0121252537e-01, /* 0x3e4e0aa8 */ +pS3 = -4.0055535734e-02, /* 0xbd241146 */ +pS4 = 7.9153501429e-04, /* 0x3a4f7f04 */ +pS5 = 3.4793309169e-05, /* 0x3811ef08 */ +qS1 = -2.4033949375e+00, /* 0xc019d139 */ +qS2 = 2.0209457874e+00, /* 0x4001572d */ +qS3 = -6.8828397989e-01, /* 0xbf303361 */ +qS4 = 7.7038154006e-02; /* 0x3d9dc62e */ + +/* +================== +acos +================== +*/ +double acos( double x ) +{ + float z, subp, p, q, r, w, s, c, df; + int hx, ix; + + GET_FLOAT_WORD( hx, x ); + ix = hx & 0x7fffffff; + + if( ix == 0x3f800000 ) + { // |x|==1 + if( hx > 0 ) + return 0.0; // acos(1) = 0 + else + return pi + (float)2.0 * pio2_lo; // acos(-1)= pi + } + else if( ix > 0x3f800000 ) + { // |x| >= 1 + return (x-x)/(x-x); // acos(|x|>1) is NaN + } + + if( ix < 0x3f000000 ) + { // |x| < 0.5 + if( ix <= 0x23000000 ) + return pio2_hi + pio2_lo;//if|x|<2**-57 + + z = x * x; + subp = pS3 + z * ( pS4 + z * pS5 ); + // chop up expression to keep mac register based stack happy + p = z * ( pS0 + z * ( pS1 + z * ( pS2 + z * subp ) ) ); + q = 1.0 + z * ( qS1 + z * ( qS2 + z * ( qS3 + z * qS4 ) ) ); + r = p / q; + return pio2_hi - ( x - ( pio2_lo - x * r ) ); + } + else if( hx < 0 ) + { // x < -0.5 + z = ( 1.0 + x ) * (float)0.5; + subp = pS3 + z * ( pS4 + z * pS5 ); + // chop up expression to keep mac register based stack happy + p = z * ( pS0 + z * ( pS1 + z * ( pS2 + z * subp ) ) ); + q = 1.0 + z * ( qS1 + z * ( qS2 + z * ( qS3 + z * qS4 ) ) ); + s = sqrt( z ); + r = p / q; + w = r * s - pio2_lo; + return pi - (float)2.0 * ( s + w ); + } + else + { // x > 0.5 + int idf; + z = ( 1.0 - x ) * (float)0.5; + s = sqrt( z ); + df = s; + GET_FLOAT_WORD( idf, df ); + SET_FLOAT_WORD( df, idf & 0xfffff000 ); + c = ( z - df * df ) / ( s + df ); + subp = pS3 + z * ( pS4 + z * pS5 ); + // chop up expression to keep mac register based stack happy + p = z * ( pS0 + z * ( pS1 + z * ( pS2 + z * subp ) ) ); + q = 1.0 + z * ( qS1 + z * ( qS2 + z * ( qS3 + z * qS4 ) ) ); + r = p / q; + w = r * s + c; + return (double)( 2.0 * ( df + w ) ); + } +} + +//pow +static const float +bp[ ] = { 1.0, 1.5, }, +dp_h[ ] = { 0.0, 5.84960938e-01, }, /* 0x3f15c000 */ +dp_l[ ] = { 0.0, 1.56322085e-06, }, /* 0x35d1cfdc */ +huge = 1.0e+30, +tiny = 1.0e-30, +zero = 0.0, +one = 1.0, +two = 2.0, +two24 = 16777216.0, /* 0x4b800000 */ +two25 = 3.355443200e+07, /* 0x4c000000 */ +twom25 = 2.9802322388e-08, /* 0x33000000 */ + /* poly coefs for (3/2)*(log(x)-2s-2/3*s**3 */ +L1 = 6.0000002384e-01, /* 0x3f19999a */ +L2 = 4.2857143283e-01, /* 0x3edb6db7 */ +L3 = 3.3333334327e-01, /* 0x3eaaaaab */ +L4 = 2.7272811532e-01, /* 0x3e8ba305 */ +L5 = 2.3066075146e-01, /* 0x3e6c3255 */ +L6 = 2.0697501302e-01, /* 0x3e53f142 */ +P1 = 1.6666667163e-01, /* 0x3e2aaaab */ +P2 = -2.7777778450e-03, /* 0xbb360b61 */ +P3 = 6.6137559770e-05, /* 0x388ab355 */ +P4 = -1.6533901999e-06, /* 0xb5ddea0e */ +P5 = 4.1381369442e-08, /* 0x3331bb4c */ +lg2 = 6.9314718246e-01, /* 0x3f317218 */ +lg2_h = 6.93145752e-01, /* 0x3f317200 */ +lg2_l = 1.42860654e-06, /* 0x35bfbe8c */ +ovt = 4.2995665694e-08, /* -(128-log2(ovfl+.5ulp)) */ +cp = 9.6179670095e-01, /* 0x3f76384f =2/(3ln2) */ +cp_h = 9.6179199219e-01, /* 0x3f763800 =head of cp */ +cp_l = 4.7017383622e-06, /* 0x369dc3a0 =tail of cp_h */ +ivln2 = 1.4426950216e+00, /* 0x3fb8aa3b =1/ln2 */ +ivln2_h = 1.4426879883e+00, /* 0x3fb8aa00 =16b 1/ln2*/ +ivln2_l = 7.0526075433e-06; /* 0x36eca570 =1/ln2 tail*/ + +/* +================== +copysignf +================== +*/ +static float copysignf( float x, float y ) +{ + unsigned int ix, iy; + + GET_FLOAT_WORD( ix, x ); + GET_FLOAT_WORD( iy, y ); + SET_FLOAT_WORD( x, ( ix & 0x7fffffff ) | ( iy & 0x80000000 ) ); + return x; +} + +/* +================== +__scalbnf +================== +*/ +static float __scalbnf( float x, int n ) +{ + int k, ix; + + GET_FLOAT_WORD( ix, x ); + + k = ( ix & 0x7f800000 ) >> 23; /* extract exponent */ + + if( k == 0 ) + { /* 0 or subnormal x */ + if( ( ix & 0x7fffffff ) == 0 ) + return x; /* +-0 */ + + x *= two25; + GET_FLOAT_WORD( ix, x ); + k = ( ( ix & 0x7f800000 ) >> 23 ) - 25; + } + if( k == 0xff ) + return x+x; /* NaN or Inf */ + + k = k + n; + + if( n > 50000 || k > 0xfe ) + return huge * copysignf( huge, x ); /* overflow */ + if ( n < -50000 ) + return tiny * copysignf( tiny, x ); /*underflow*/ + if( k > 0 ) /* normal result */ + { + SET_FLOAT_WORD( x, ( ix & 0x807fffff ) | ( k << 23 ) ); + return x; + } + if( k <= -25 ) + return tiny * copysignf( tiny, x ); /*underflow*/ + + k += 25; /* subnormal result */ + SET_FLOAT_WORD( x, ( ix & 0x807fffff ) | ( k << 23 ) ); + return x * twom25; +} + +/* +================== +pow +================== +*/ +float pow( float x, float y ) +{ + float z, ax, z_h, z_l, p_h, p_l; + float y1, subt1, t1, t2, subr, r, s, t, u, v, w; + int i, j, k, yisint, n; + int hx, hy, ix, iy, is; + + /*TA: for some reason the Q3 VM goes apeshit when x = 1.0 + and y > 1.0. Curiously this doesn't happen with gcc + hence this hack*/ + if( x == 1.0 ) + return x; + + GET_FLOAT_WORD( hx, x ); + GET_FLOAT_WORD( hy, y ); + ix = hx & 0x7fffffff; + iy = hy & 0x7fffffff; + + /* y==zero: x**0 = 1 */ + if( iy == 0 ) + return one; + + /* +-NaN return x+y */ + if( ix > 0x7f800000 || iy > 0x7f800000 ) + return x + y; + + /* determine if y is an odd int when x < 0 + * yisint = 0 ... y is not an integer + * yisint = 1 ... y is an odd int + * yisint = 2 ... y is an even int + */ + yisint = 0; + if( hx < 0 ) + { + if( iy >= 0x4b800000 ) + yisint = 2; /* even integer y */ + else if( iy >= 0x3f800000 ) + { + k = ( iy >> 23 ) - 0x7f; /* exponent */ + j = iy >> ( 23 - k ); + if( ( j << ( 23 - k ) ) == iy ) + yisint = 2 - ( j & 1 ); + } + } + + /* special value of y */ + if( iy == 0x7f800000 ) + { /* y is +-inf */ + if( ix == 0x3f800000 ) + return y - y; /* inf**+-1 is NaN */ + else if( ix > 0x3f800000 )/* (|x|>1)**+-inf = inf,0 */ + return ( hy >= 0 ) ? y : zero; + else /* (|x|<1)**-,+inf = inf,0 */ + return ( hy < 0 ) ? -y : zero; + } + + if( iy == 0x3f800000 ) + { /* y is +-1 */ + if( hy < 0 ) + return one / x; + else + return x; + } + + if( hy == 0x40000000 ) + return x * x; /* y is 2 */ + + if( hy == 0x3f000000 ) + { /* y is 0.5 */ + if( hx >= 0 ) /* x >= +0 */ + return sqrt( x ); + } + + ax = fabs( x ); + + /* special value of x */ + if( ix == 0x7f800000 || ix == 0 || ix == 0x3f800000 ) + { + z = ax; /*x is +-0,+-inf,+-1*/ + if( hy < 0 ) + z = one / z; /* z = (1/|x|) */ + if( hx < 0 ) + { + if( ( ( ix - 0x3f800000 ) | yisint ) == 0 ) + z = ( z - z ) / ( z - z ); /* (-1)**non-int is NaN */ + else if( yisint == 1 ) + z = -z; /* (x<0)**odd = -(|x|**odd) */ + } + + return z; + } + + /* (x<0)**(non-int) is NaN */ + if( ( ( ( (unsigned int)hx >> 31 ) - 1 ) | yisint ) == 0 ) + return ( x - x ) / ( x - x ); + + /* |y| is huge */ + if( iy > 0x4d000000 ) + { /* if |y| > 2**27 */ + /* over/underflow if x is not close to one */ + if( ix < 0x3f7ffff8 ) + return ( hy < 0 ) ? huge * huge : tiny * tiny; + + if( ix > 0x3f800007 ) + return ( hy > 0 ) ? huge * huge : tiny * tiny; + /* now |1-x| is tiny <= 2**-20, suffice to compute + log(x) by x-x^2/2+x^3/3-x^4/4 */ + t = x - 1; /* t has 20 trailing zeros */ + w = ( t * t ) * ( (float)0.5 - t * ( (float)0.333333333333 - t * (float)0.25 ) ); + u = ivln2_h * t; /* ivln2_h has 16 sig. bits */ + v = t * ivln2_l - w * ivln2; + t1 = u + v; + GET_FLOAT_WORD( is, t1 ); + SET_FLOAT_WORD( t1, is & 0xfffff000 ); + t2 = v - ( t1 - u ); + } + else + { + float s2, s_h, s_l, t_h, t_l; + n = 0; + /* take care subnormal number */ + if( ix < 0x00800000 ) + { + ax *= two24; + n -= 24; + GET_FLOAT_WORD( ix, ax ); + } + + n += ( ( ix ) >> 23 ) - 0x7f; + j = ix & 0x007fffff; + + /* determine interval */ + ix = j | 0x3f800000; /* normalize ix */ + if( j <= 0x1cc471 ) + k = 0; /* |x|<sqrt(3/2) */ + else if( j < 0x5db3d7 ) + k = 1; /* |x|<sqrt(3) */ + else + { + k = 0; + n += 1; + ix -= 0x00800000; + } + SET_FLOAT_WORD( ax, ix ); + + /* compute s = s_h+s_l = (x-1)/(x+1) or (x-1.5)/(x+1.5) */ + u = ax - bp[ k ]; /* bp[0]=1.0, bp[1]=1.5 */ + v = one / ( ax + bp[ k ] ); + s = u * v; + s_h = s; + GET_FLOAT_WORD( is, s_h ); + SET_FLOAT_WORD( s_h, is & 0xfffff000 ); + /* t_h=ax+bp[k] High */ + SET_FLOAT_WORD( t_h, ( ( ix >> 1 ) | 0x20000000 ) + 0x0040000 + ( k << 21 ) ); + t_l = ax - ( t_h - bp[ k ] ); + s_l = v * ( ( u - s_h * t_h ) - s_h * t_l ); + /* compute log(ax) */ + s2 = s * s; + subr = L3 + s2 * ( L4 + s2 * ( L5 + s2 * L6 ) ); + // chop up expression to keep mac register based stack happy + r = s2 * s2 * ( L1 + s2 * ( L2 + s2 * subr ) ); + r += s_l * ( s_h + s ); + s2 = s_h * s_h; + t_h = (float)3.0 + s2 + r; + GET_FLOAT_WORD( is, t_h ); + SET_FLOAT_WORD( t_h, is & 0xfffff000 ); + t_l = r - ( ( t_h - (float)3.0 ) - s2 ); + /* u+v = s*(1+...) */ + u = s_h * t_h; + v = s_l * t_h + t_l * s; + /* 2/(3log2)*(s+...) */ + p_h = u + v; + GET_FLOAT_WORD( is, p_h ); + SET_FLOAT_WORD( p_h, is & 0xfffff000 ); + p_l = v - ( p_h - u ); + z_h = cp_h * p_h; /* cp_h+cp_l = 2/(3*log2) */ + z_l = cp_l * p_h + p_l * cp + dp_l[ k ]; + /* log2(ax) = (s+..)*2/(3*log2) = n + dp_h + z_h + z_l */ + t = (float)n; + t1 = ( ( ( z_h + z_l ) + dp_h[ k ] ) + t ); + GET_FLOAT_WORD( is, t1 ); + SET_FLOAT_WORD( t1, is & 0xfffff000 ); + t2 = z_l - ( ( ( t1 - t ) - dp_h[ k ] ) - z_h ); + } + + s = one; /* s (sign of result -ve**odd) = -1 else = 1 */ + if( ( ( ( (unsigned int)hx >> 31 ) - 1 ) | ( yisint - 1 ) ) == 0 ) + s = -one; /* (-ve)**(odd int) */ + + /* split up y into y1+y2 and compute (y1+y2)*(t1+t2) */ + GET_FLOAT_WORD( is, y ); + SET_FLOAT_WORD( y1, is & 0xfffff000 ); + p_l = ( y - y1 ) * t1 + y * t2; + p_h = y1 * t1; + z = p_l + p_h; + GET_FLOAT_WORD( j, z ); + + if( j > 0x43000000 ) /* if z > 128 */ + return s * huge * huge; /* overflow */ + else if( j == 0x43000000 ) + { /* if z == 128 */ + if( p_l + ovt > z - p_h ) + return s * huge * huge; /* overflow */ + } + else if( ( j & 0x7fffffff ) > 0x43160000 ) /* z <= -150 */ + return s * tiny * tiny; /* underflow */ + else if( (unsigned int)j == 0xc3160000 ) + { /* z == -150 */ + if( p_l <= z - p_h ) + return s * tiny * tiny; /* underflow */ + } + + /* + * compute 2**(p_h+p_l) + */ + i = j & 0x7fffffff; + k = ( i >> 23 ) - 0x7f; + n = 0; + + if( i > 0x3f000000 ) + { /* if |z| > 0.5, set n = [z+0.5] */ + n = j + ( 0x00800000 >> ( k + 1 ) ); + k = ( ( n & 0x7fffffff ) >> 23 ) - 0x7f; /* new k for n */ + SET_FLOAT_WORD( t, n & ~( 0x007fffff >> k ) ); + n = ( ( n & 0x007fffff ) | 0x00800000 ) >> ( 23 - k ); + + if( j < 0 ) + n = -n; + + p_h -= t; + } + + t = p_l + p_h; + GET_FLOAT_WORD( is, t ); + SET_FLOAT_WORD( t, is & 0xfffff000 ); + u = t * lg2_h; + v = ( p_l - ( t - p_h ) ) * lg2 + t * lg2_l; + z = u + v; + w = v - ( z - u ); + t = z * z; + subt1 = P3 + t * ( P4 + t * P5 ); + // chop up expression to keep mac register based stack happy + t1 = z - t * ( P1 + t * ( P2 + t * subt1 ) ); + r = ( z * t1 ) / ( t1 - two ) - ( w + z * w ); + z = one - ( r - z ); + GET_FLOAT_WORD( j, z ); + j += (n << 23 ); + + if( ( j >> 23 ) <= 0 ) + z = __scalbnf( z, n ); /* subnormal output */ + else + SET_FLOAT_WORD( z, j ); + + return s * z; +} + +#endif + + + +static int randSeed = 0; + +void srand( unsigned seed ) +{ + randSeed = seed; +} + +int rand( void ) +{ + randSeed = ( 69069 * randSeed + 1 ); + return randSeed & 0x7fff; +} + +double atof( const char *string ) +{ + float sign; + float value; + int c; + + // skip whitespace + while( *string <= ' ' ) + { + if( !*string ) + return 0; + + string++; + } + + // check sign + switch( *string ) + { + case '+': + string++; + sign = 1; + break; + + case '-': + string++; + sign = -1; + break; + + default: + sign = 1; + break; + } + + // read digits + value = 0; + c = string[ 0 ]; + + if( c != '.' ) + { + do + { + c = *string++; + if( c < '0' || c > '9' ) + break; + + c -= '0'; + value = value * 10 + c; + } while( 1 ); + } + else + string++; + + // check for decimal point + if( c == '.' ) + { + double fraction; + + fraction = 0.1; + do + { + c = *string++; + if( c < '0' || c > '9' ) + break; + + c -= '0'; + value += c * fraction; + fraction *= 0.1; + } while( 1 ); + + } + + // not handling 10e10 notation... + + return value * sign; +} + +double _atof( const char **stringPtr ) +{ + const char *string; + float sign; + float value; + int c = '0'; // bk001211 - uninitialized use possible + + string = *stringPtr; + + // skip whitespace + while( *string <= ' ' ) + { + if( !*string ) + { + *stringPtr = string; + return 0; + } + + string++; + } + + // check sign + switch( *string ) + { + case '+': + string++; + sign = 1; + break; + + case '-': + string++; + sign = -1; + break; + + default: + sign = 1; + break; + } + + // read digits + value = 0; + if( string[ 0 ] != '.' ) + { + do + { + c = *string++; + if( c < '0' || c > '9' ) + break; + + c -= '0'; + value = value * 10 + c; + } while( 1 ); + } + + // check for decimal point + if( c == '.' ) + { + double fraction; + + fraction = 0.1; + do + { + c = *string++; + if( c < '0' || c > '9' ) + break; + + c -= '0'; + value += c * fraction; + fraction *= 0.1; + } while( 1 ); + + } + + // not handling 10e10 notation... + *stringPtr = string; + + return value * sign; +} + + +#if defined ( Q3_VM ) + +int atoi( const char *string ) +{ + int sign; + int value; + int c; + + // skip whitespace + while( *string <= ' ' ) + { + if( !*string ) + return 0; + + string++; + } + + // check sign + switch( *string ) + { + case '+': + string++; + sign = 1; + break; + + case '-': + string++; + sign = -1; + break; + + default: + sign = 1; + break; + } + + // read digits + value = 0; + do + { + c = *string++; + if( c < '0' || c > '9' ) + break; + + c -= '0'; + value = value * 10 + c; + } while( 1 ); + + // not handling 10e10 notation... + + return value * sign; +} + + +int _atoi( const char **stringPtr ) +{ + int sign; + int value; + int c; + const char *string; + + string = *stringPtr; + + // skip whitespace + while( *string <= ' ' ) + { + if( !*string ) + return 0; + + string++; + } + + // check sign + switch( *string ) + { + case '+': + string++; + sign = 1; + break; + + case '-': + string++; + sign = -1; + break; + + default: + sign = 1; + break; + } + + // read digits + value = 0; + do + { + c = *string++; + if( c < '0' || c > '9' ) + break; + + c -= '0'; + value = value * 10 + c; + } while( 1 ); + + // not handling 10e10 notation... + + *stringPtr = string; + + return value * sign; +} + +int abs( int n ) +{ + return n < 0 ? -n : n; +} + +double fabs( double x ) +{ + return x < 0 ? -x : x; +} + +unsigned int _hextoi( const char **stringPtr ) +{ + unsigned int value; + int c; + int i; + const char *string; + + string = *stringPtr; + + // skip whitespace + while( *string <= ' ' ) + { + if( !*string ) + return 0; + + string++; + } + + value = 0; + i = 0; + while( i++ < 8 && ( c = *string++ ) ) + { + if ( c >= '0' && c <= '9' ) + { + value = value * 16 + c - '0'; + continue; + } + else if ( c >= 'a' && c <= 'f' ) + { + value = value * 16 + 10 + c - 'a'; + continue; + } + else if ( c >= 'A' && c <= 'F' ) + { + value = value * 16 + 10 + c - 'A'; + continue; + } + else + break; + } + *stringPtr = string; + return value; +} + +//========================================================= + + +#define ALT 0x00000001 /* alternate form */ +#define HEX 0x00000002 /* hexadecimal */ +#define LADJUST 0x00000004 /* left adjustment */ +#define LONGDBL 0x00000008 /* long double */ +#define LONGINT 0x00000010 /* long integer */ +#define QUADINT 0x00000020 /* quad integer */ +#define SHORTINT 0x00000040 /* short integer */ +#define ZEROPAD 0x00000080 /* zero (as opposed to blank) pad */ +#define FPT 0x00000100 /* floating point number */ +#define UNSIGNED 0x00000200 /* unsigned integer */ + +#define to_digit(c) ((c) - '0') +#define is_digit(c) ((unsigned)to_digit(c) <= 9) +#define to_char(n) ((n) + '0') + +void AddInt( char **buf_p, int val, int width, int flags ) +{ + char text[ 32 ]; + int digits; + char *buf; + + digits = 0; + + if( flags & UNSIGNED ) + val = (unsigned) val; + + if( flags & HEX ) + { + char c; + int n = 0; + + while( n < 32 ) + { + c = "0123456789abcdef"[ ( val >> n ) & 0xF ]; + n += 4; + if( c == '0' && !digits ) + continue; + text[ digits++ ] = c; + } + text[ digits ] = '\0'; + } + else + { + int signedVal = val; + + if( val < 0 ) + val = -val; + do + { + text[ digits++ ] = '0' + val % 10; + val /= 10; + } while( val ); + + if( signedVal < 0 ) + text[ digits++ ] = '-'; + } + + buf = *buf_p; + + if( !( flags & LADJUST ) ) + { + while( digits < width ) + { + *buf++ = ( flags & ZEROPAD ) ? '0' : ' '; + width--; + } + } + + while( digits-- ) + { + *buf++ = text[ digits ]; + width--; + } + + if( flags & LADJUST ) + { + while( width-- > 0 ) + *buf++ = ( flags & ZEROPAD ) ? '0' : ' '; + } + + *buf_p = buf; +} + +void AddFloat( char **buf_p, float fval, int width, int prec ) +{ + char text[ 32 ]; + int digits; + float signedVal; + char *buf; + int val; + + // get the sign + signedVal = fval; + if( fval < 0 ) + fval = -fval; + + // write the float number + digits = 0; + val = (int)fval; + + do + { + text[ digits++ ] = '0' + val % 10; + val /= 10; + } while( val ); + + if( signedVal < 0 ) + text[digits++] = '-'; + + buf = *buf_p; + + while( digits < width ) + { + *buf++ = ' '; + width--; + } + + while( digits-- ) + *buf++ = text[ digits ]; + + *buf_p = buf; + + if( prec < 0 ) + prec = 6; + + // write the fraction + digits = 0; + + while( digits < prec ) + { + fval -= (int)fval; + fval *= 10.0; + val = (int)fval; + text[ digits++ ] = '0' + val % 10; + } + + if( digits > 0 ) + { + buf = *buf_p; + *buf++ = '.'; + for( prec = 0; prec < digits; prec++ ) + *buf++ = text[ prec ]; + + *buf_p = buf; + } +} + +void AddVec3_t( char **buf_p, vec3_t v, int width, int prec ) +{ + char *buf; + + buf = *buf_p; + + *buf++ = '['; + + AddFloat( &buf, v[ 0 ], width, prec ); + buf += width; + *buf++ = ' '; + + AddFloat( &buf, v[ 1 ], width, prec ); + buf += width; + *buf++ = ' '; + + AddFloat( &buf, v[ 2 ], width, prec ); + buf += width; + *buf++ = ']'; + + *buf_p = buf; +} + +void AddString( char **buf_p, char *string, int width, int prec ) +{ + int size; + char *buf; + + buf = *buf_p; + + if( string == NULL ) + { + string = "(null)"; + prec = -1; + } + + if( prec >= 0 ) + { + for( size = 0; size < prec; size++ ) + { + if( string[ size ] == '\0' ) + break; + } + } + else + size = strlen( string ); + + width -= size; + + while( size-- ) + *buf++ = *string++; + + while( width-- > 0 ) + *buf++ = ' '; + + *buf_p = buf; +} + +/* +vsprintf + +I'm not going to support a bunch of the more arcane stuff in here +just to keep it simpler. For example, the '*' and '$' are not +currently supported. I've tried to make it so that it will just +parse and ignore formats we don't support. +*/ +int vsprintf( char *buffer, const char *fmt, va_list argptr ) +{ + int *arg; + char *buf_p; + char ch; + int flags; + int width; + int prec; + int n; + char sign; + + buf_p = buffer; + arg = (int *)argptr; + + while( qtrue ) + { + // run through the format string until we hit a '%' or '\0' + for( ch = *fmt; ( ch = *fmt ) != '\0' && ch != '%'; fmt++ ) + *buf_p++ = ch; + + if( ch == '\0' ) + goto done; + + // skip over the '%' + fmt++; + + // reset formatting state + flags = 0; + width = 0; + prec = -1; + sign = '\0'; + +rflag: + ch = *fmt++; +reswitch: + switch( ch ) + { + case '-': + flags |= LADJUST; + goto rflag; + + case '.': + n = 0; + while( is_digit( ( ch = *fmt++ ) ) ) + n = 10 * n + ( ch - '0' ); + + prec = n < 0 ? -1 : n; + goto reswitch; + + case '0': + flags |= ZEROPAD; + goto rflag; + + case '1': + case '2': + case '3': + case '4': + case '5': + case '6': + case '7': + case '8': + case '9': + n = 0; + do + { + n = 10 * n + ( ch - '0' ); + ch = *fmt++; + } while( is_digit( ch ) ); + + width = n; + goto reswitch; + + case 'c': + *buf_p++ = (char)*arg; + arg++; + break; + + case 'u': + flags |= UNSIGNED; + case 'd': + case 'i': + AddInt( &buf_p, *arg, width, flags ); + arg++; + break; + + case 'f': + AddFloat( &buf_p, *(double *)arg, width, prec ); +#ifdef Q3_VM + arg += 1; // everything is 32 bit in my compiler +#else + arg += 2; +#endif + break; + + case 's': + AddString( &buf_p, (char *)*arg, width, prec ); + arg++; + break; + + case 'v': + AddVec3_t( &buf_p, (vec_t *)*arg, width, prec ); + arg++; + break; + + case 'x': + flags |= HEX; + AddInt( &buf_p, *arg, width, prec ); + arg++; + break; + + case '%': + *buf_p++ = ch; + break; + + default: + *buf_p++ = (char)*arg; + arg++; + break; + } + } + +done: + *buf_p = 0; + return buf_p - buffer; +} + + +/* this is really crappy */ +// FIXME: count is still inaccurate in some cases. +int sscanf( const char *buffer, const char *fmt, ... ) +{ + int cmd; + int **arg; + int count; + + arg = (int **)&fmt + 1; + count = 0; + + while( *fmt ) + { + if( fmt[ 0 ] != '%' ) + { + fmt++; + continue; + } + + if( !buffer[ 0 ] ) break; + + cmd = fmt[ 1 ]; + fmt += 2; + + switch( cmd ) + { + case 'i': + case 'd': + case 'u': + **arg = _atoi( &buffer ); + ++count; + break; + case 'f': + *(float *)*arg = _atof( &buffer ); + ++count; + break; + case 'x': + **arg = _hextoi( &buffer ); + ++count; + break; + } + + arg++; + } + + return count; +} + +#endif diff --git a/src/game/bg_lib.h b/src/game/bg_lib.h new file mode 100644 index 0000000..962a625 --- /dev/null +++ b/src/game/bg_lib.h @@ -0,0 +1,120 @@ +/* +=========================================================================== +Copyright (C) 1999-2005 Id Software, Inc. +Copyright (C) 2000-2006 Tim Angus + +This file is part of Tremulous. + +Tremulous is free software; you can redistribute it +and/or modify it under the terms of the GNU General Public License as +published by the Free Software Foundation; either version 2 of the License, +or (at your option) any later version. + +Tremulous is distributed in the hope that it will be +useful, but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with Tremulous; if not, write to the Free Software +Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA +=========================================================================== +*/ + +// bg_lib.h -- standard C library replacement routines used by code +// compiled for the virtual machine + +// This file is NOT included on native builds +#ifndef BG_LIB_H +#define BG_LIB_H + +#ifndef NULL +#define NULL ((void *)0) +#endif + +typedef int size_t; + +typedef char * va_list; +#define _INTSIZEOF(n) ( (sizeof(n) + sizeof(int) - 1) & ~(sizeof(int) - 1) ) +#define va_start(ap,v) ( ap = (va_list)&v + _INTSIZEOF(v) ) +#define va_arg(ap,t) ( *(t *)((ap += _INTSIZEOF(t)) - _INTSIZEOF(t)) ) +#define va_end(ap) ( ap = (va_list)0 ) + +#define CHAR_BIT 8 /* number of bits in a char */ +#define SCHAR_MIN (-128) /* minimum signed char value */ +#define SCHAR_MAX 127 /* maximum signed char value */ +#define UCHAR_MAX 0xff /* maximum unsigned char value */ + +#define SHRT_MIN (-32768) /* minimum (signed) short value */ +#define SHRT_MAX 32767 /* maximum (signed) short value */ +#define USHRT_MAX 0xffff /* maximum unsigned short value */ +#define INT_MIN (-2147483647 - 1) /* minimum (signed) int value */ +#define INT_MAX 2147483647 /* maximum (signed) int value */ +#define UINT_MAX 0xffffffff /* maximum unsigned int value */ +#define LONG_MIN (-2147483647L - 1) /* minimum (signed) long value */ +#define LONG_MAX 2147483647L /* maximum (signed) long value */ +#define ULONG_MAX 0xffffffffUL /* maximum unsigned long value */ + +#define isalnum(c) (isalpha(c) || isdigit(c)) +#define isalpha(c) (isupper(c) || islower(c)) +#define isascii(c) ((c) > 0 && (c) <= 0x7f) +#define iscntrl(c) (((c) >= 0) && (((c) <= 0x1f) || ((c) == 0x7f))) +#define isdigit(c) ((c) >= '0' && (c) <= '9') +#define isgraph(c) ((c) != ' ' && isprint(c)) +#define islower(c) ((c) >= 'a' && (c) <= 'z') +#define isprint(c) ((c) >= ' ' && (c) <= '~') +#define ispunct(c) (((c) > ' ' && (c) <= '~') && !isalnum(c)) +#define isspace(c) ((c) == ' ' || (c) == '\f' || (c) == '\n' || (c) == '\r' || \ + (c) == '\t' || (c) == '\v') +#define isupper(c) ((c) >= 'A' && (c) <= 'Z') +#define isxdigit(c) (isxupper(c) || isxlower(c)) +#define isxlower(c) (isdigit(c) || (c >= 'a' && c <= 'f')) +#define isxupper(c) (isdigit(c) || (c >= 'A' && c <= 'F')) + +// Misc functions +typedef int cmp_t( const void *, const void * ); +void qsort( void *a, size_t n, size_t es, cmp_t *cmp ); +void srand( unsigned seed ); +int rand( void ); + +// String functions +size_t strlen( const char *string ); +char *strcat( char *strDestination, const char *strSource ); +char *strcpy( char *strDestination, const char *strSource ); +int strcmp( const char *string1, const char *string2 ); +char *strchr( const char *string, int c ); +char *strrchr( const char *string, int c ); +char *strstr( const char *string, const char *strCharSet ); +char *strncpy( char *strDest, const char *strSource, size_t count ); +int tolower( int c ); +int toupper( int c ); + +double atof( const char *string ); +double _atof( const char **stringPtr ); +int atoi( const char *string ); +int _atoi( const char **stringPtr ); + + +int vsprintf( char *buffer, const char *fmt, va_list argptr ); +int sscanf( const char *buffer, const char *fmt, ... ); + +// Memory functions +void *memmove( void *dest, const void *src, size_t count ); +void *memset( void *dest, int c, size_t count ); +void *memcpy( void *dest, const void *src, size_t count ); + +// Math functions +double ceil( double x ); +double floor( double x ); +double sqrt( double x ); +double sin( double x ); +double cos( double x ); +double atan2( double y, double x ); +double tan( double x ); +int abs( int n ); +double fabs( double x ); +double acos( double x ); +float pow( float x, float y ); +double rint( double v ); + +#endif // BG_LIB_H diff --git a/src/game/bg_local.h b/src/game/bg_local.h new file mode 100644 index 0000000..354214c --- /dev/null +++ b/src/game/bg_local.h @@ -0,0 +1,88 @@ +/* +=========================================================================== +Copyright (C) 1999-2005 Id Software, Inc. +Copyright (C) 2000-2006 Tim Angus + +This file is part of Tremulous. + +Tremulous is free software; you can redistribute it +and/or modify it under the terms of the GNU General Public License as +published by the Free Software Foundation; either version 2 of the License, +or (at your option) any later version. + +Tremulous is distributed in the hope that it will be +useful, but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with Tremulous; if not, write to the Free Software +Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA +=========================================================================== +*/ + +// bg_local.h -- local definitions for the bg (both games) files + +#define MIN_WALK_NORMAL 0.7f // can't walk on very steep slopes + +#define STEPSIZE 18 + +#define TIMER_LAND 130 +#define TIMER_GESTURE (34*66+50) +#define TIMER_ATTACK 500 //nonsegmented models + +#define OVERCLIP 1.001f + +#define FALLING_THRESHOLD -900.0f //what vertical speed to start falling sound at + + +// all of the locals will be zeroed before each +// pmove, just to make damn sure we don't have +// any differences when running on client or server +typedef struct +{ + vec3_t forward, right, up; + float frametime; + + int msec; + + qboolean walking; + qboolean groundPlane; + qboolean ladder; + trace_t groundTrace; + + float impactSpeed; + + vec3_t previous_origin; + vec3_t previous_velocity; + int previous_waterlevel; +} pml_t; + +extern pmove_t *pm; +extern pml_t pml; + +// movement parameters +extern float pm_stopspeed; +extern float pm_duckScale; +extern float pm_swimScale; +extern float pm_wadeScale; + +extern float pm_accelerate; +extern float pm_airaccelerate; +extern float pm_wateraccelerate; +extern float pm_flyaccelerate; + +extern float pm_friction; +extern float pm_waterfriction; +extern float pm_flightfriction; + +extern int c_pmove; + +void PM_ClipVelocity( vec3_t in, vec3_t normal, vec3_t out, float overbounce ); +void PM_AddTouchEnt( int entityNum ); +void PM_AddEvent( int newEvent ); + +qboolean PM_SlideMove( qboolean gravity ); +void PM_StepEvent( vec3_t from, vec3_t to, vec3_t normal ); +qboolean PM_StepSlideMove( qboolean gravity, qboolean predictive ); +qboolean PM_PredictStepMove( void ); diff --git a/src/game/bg_misc.c b/src/game/bg_misc.c new file mode 100644 index 0000000..79a8e2a --- /dev/null +++ b/src/game/bg_misc.c @@ -0,0 +1,5733 @@ +/* +=========================================================================== +Copyright (C) 1999-2005 Id Software, Inc. +Copyright (C) 2000-2006 Tim Angus + +This file is part of Tremulous. + +Tremulous is free software; you can redistribute it +and/or modify it under the terms of the GNU General Public License as +published by the Free Software Foundation; either version 2 of the License, +or (at your option) any later version. + +Tremulous is distributed in the hope that it will be +useful, but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with Tremulous; if not, write to the Free Software +Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA +=========================================================================== +*/ + +// bg_misc.c -- both games misc functions, all completely stateless + +#include "../qcommon/q_shared.h" +#include "bg_public.h" + +int trap_FS_FOpenFile( const char *qpath, fileHandle_t *f, fsMode_t mode ); +void trap_FS_Read( void *buffer, int len, fileHandle_t f ); +void trap_FS_Write( const void *buffer, int len, fileHandle_t f ); +void trap_FS_FCloseFile( fileHandle_t f ); +void trap_FS_Seek( fileHandle_t f, long offset, fsOrigin_t origin ); // fsOrigin_t + +buildableAttributes_t bg_buildableList[ ] = +{ + { + BA_A_SPAWN, //int buildNum; + "eggpod", //char *buildName; + "Egg", //char *humanName; + "team_alien_spawn", //char *entityName; + { "models/buildables/eggpod/eggpod.md3", 0, 0, 0 }, + 1.0f, //float modelScale; + { -15, -15, -15 }, //vec3_t mins; + { 15, 15, 15 }, //vec3_t maxs; + 0.0f, //float zOffset; + TR_GRAVITY, //trType_t traj; + 0.0, //float bounce; + ASPAWN_BP, //int buildPoints; + ( 1 << S1 )|( 1 << S2 )|( 1 << S3 ), //int stages + ASPAWN_HEALTH, //int health; + ASPAWN_REGEN, //int regenRate; + ASPAWN_SPLASHDAMAGE, //int splashDamage; + ASPAWN_SPLASHRADIUS, //int splashRadius; + MOD_ASPAWN, //int meansOfDeath; + BIT_ALIENS, //int team; + ( 1 << WP_ABUILD )|( 1 << WP_ABUILD2 ), //weapon_t buildWeapon; + BANIM_IDLE1, //int idleAnim; + 100, //int nextthink; + ASPAWN_BT, //int buildTime; + qfalse, //qboolean usable; + 0, //int turretRange; + 0, //int turretFireSpeed; + WP_NONE, //weapon_t turretProjType; + 0.5f, //float minNormal; + qtrue, //qboolean invertNormal; + qfalse, //qboolean creepTest; + ASPAWN_CREEPSIZE, //int creepSize; + qfalse, //qboolean dccTest; + qfalse, //qboolean transparentTest; + qfalse, //qboolean reactorTest; + qfalse, //qboolean replacable; + }, + { + BA_A_BARRICADE, //int buildNum; + "barricade", //char *buildName; + "Barricade", //char *humanName; + "team_alien_barricade",//char *entityName; + { "models/buildables/barricade/barricade.md3", 0, 0, 0 }, + 1.0f, //float modelScale; + { -35, -35, -15 }, //vec3_t mins; + { 35, 35, 60 }, //vec3_t maxs; + 0.0f, //float zOffset; + TR_GRAVITY, //trType_t traj; + 0.0, //float bounce; + BARRICADE_BP, //int buildPoints; + ( 1 << S1 )|( 1 << S2 )|( 1 << S3 ), //int stages + BARRICADE_HEALTH, //int health; + BARRICADE_REGEN, //int regenRate; + BARRICADE_SPLASHDAMAGE,//int splashDamage; + BARRICADE_SPLASHRADIUS,//int splashRadius; + MOD_ASPAWN, //int meansOfDeath; + BIT_ALIENS, //int team; + ( 1 << WP_ABUILD )|( 1 << WP_ABUILD2 ), //weapon_t buildWeapon; + BANIM_IDLE1, //int idleAnim; + 100, //int nextthink; + BARRICADE_BT, //int buildTime; + qfalse, //qboolean usable; + 0, //int turretRange; + 0, //int turretFireSpeed; + WP_NONE, //weapon_t turretProjType; + 0.707f, //float minNormal; + qfalse, //qboolean invertNormal; + qtrue, //qboolean creepTest; + BARRICADE_CREEPSIZE, //int creepSize; + qfalse, //qboolean dccTest; + qfalse, //qboolean transparentTest; + qfalse, //qboolean reactorTest; + qfalse, //qboolean replaceable; + }, + { + BA_A_BOOSTER, //int buildNum; + "booster", //char *buildName; + "Booster", //char *humanName; + "team_alien_booster", //char *entityName; + { "models/buildables/booster/booster.md3", 0, 0, 0 }, + 1.0f, //float modelScale; + { -26, -26, -9 }, //vec3_t mins; + { 26, 26, 9 }, //vec3_t maxs; + 0.0f, //float zOffset; + TR_GRAVITY, //trType_t traj; + 0.0, //float bounce; + BOOSTER_BP, //int buildPoints; + ( 1 << S2 )|( 1 << S3 ), //int stages + BOOSTER_HEALTH, //int health; + BOOSTER_REGEN, //int regenRate; + BOOSTER_SPLASHDAMAGE, //int splashDamage; + BOOSTER_SPLASHRADIUS, //int splashRadius; + MOD_ASPAWN, //int meansOfDeath; + BIT_ALIENS, //int team; + ( 1 << WP_ABUILD )|( 1 << WP_ABUILD2 ), //weapon_t buildWeapon; + BANIM_IDLE1, //int idleAnim; + 100, //int nextthink; + BOOSTER_BT, //int buildTime; + qfalse, //qboolean usable; + 0, //int turretRange; + 0, //int turretFireSpeed; + WP_NONE, //weapon_t turretProjType; + 0.707f, //float minNormal; + qfalse, //qboolean invertNormal; + qtrue, //qboolean creepTest; + BOOSTER_CREEPSIZE, //int creepSize; + qfalse, //qboolean dccTest; + qtrue, //qboolean transparentTest; + qfalse, //qboolean reactorTest; + qtrue, //qboolean replacable; + }, + { + BA_A_ACIDTUBE, //int buildNum; + "acid_tube", //char *buildName; + "Acid Tube", //char *humanName; + "team_alien_acid_tube",//char *entityName; + { "models/buildables/acid_tube/acid_tube.md3", 0, 0, 0 }, + 1.0f, //float modelScale; + { -25, -25, -25 }, //vec3_t mins; + { 25, 25, 25 }, //vec3_t maxs; + -15.0f, //float zOffset; + TR_GRAVITY, //trType_t traj; + 0.0, //float bounce; + ACIDTUBE_BP, //int buildPoints; + ( 1 << S1 )|( 1 << S2 )|( 1 << S3 ), //int stages + ACIDTUBE_HEALTH, //int health; + ACIDTUBE_REGEN, //int regenRate; + ACIDTUBE_SPLASHDAMAGE, //int splashDamage; + ACIDTUBE_SPLASHRADIUS, //int splashRadius; + MOD_ATUBE, //int meansOfDeath; + BIT_ALIENS, //int team; + ( 1 << WP_ABUILD )|( 1 << WP_ABUILD2 ), //weapon_t buildWeapon; + BANIM_IDLE1, //int idleAnim; + 200, //int nextthink; + ACIDTUBE_BT, //int buildTime; + qfalse, //qboolean usable; + 0, //int turretRange; + 0, //int turretFireSpeed; + WP_NONE, //weapon_t turretProjType; + 0.0f, //float minNormal; + qtrue, //qboolean invertNormal; + qtrue, //qboolean creepTest; + ACIDTUBE_CREEPSIZE, //int creepSize; + qfalse, //qboolean dccTest; + qfalse, //qboolean transparentTest; + qfalse, //qboolean reactorTest; + qfalse, //qboolean replacable; + }, + { + BA_A_HIVE, //int buildNum; + "hive", //char *buildName; + "Hive", //char *humanName; + "team_alien_hive", //char *entityName; + { "models/buildables/acid_tube/acid_tube.md3", 0, 0, 0 }, + 1.0f, //float modelScale; + { -35, -35, -25 }, //vec3_t mins; + { 35, 35, 25 }, //vec3_t maxs; + -15.0f, //float zOffset; + TR_GRAVITY, //trType_t traj; + 0.0, //float bounce; + HIVE_BP, //int buildPoints; + ( 1 << S3 ), //int stages + HIVE_HEALTH, //int health; + HIVE_REGEN, //int regenRate; + HIVE_SPLASHDAMAGE, //int splashDamage; + HIVE_SPLASHRADIUS, //int splashRadius; + MOD_ASPAWN, //int meansOfDeath; + BIT_ALIENS, //int team; + ( 1 << WP_ABUILD )|( 1 << WP_ABUILD2 ), //weapon_t buildWeapon; + BANIM_IDLE1, //int idleAnim; + 500, //int nextthink; + HIVE_BT, //int buildTime; + qfalse, //qboolean usable; + 0, //int turretRange; + 0, //int turretFireSpeed; + WP_HIVE, //weapon_t turretProjType; + 0.0f, //float minNormal; + qtrue, //qboolean invertNormal; + qtrue, //qboolean creepTest; + HIVE_CREEPSIZE, //int creepSize; + qfalse, //qboolean dccTest; + qfalse, //qboolean transparentTest; + qfalse, //qboolean reactorTest; + qfalse, //qboolean replacable; + }, + { + BA_A_TRAPPER, //int buildNum; + "trapper", //char *buildName; + "Trapper", //char *humanName; + "team_alien_trapper", //char *entityName; + { "models/buildables/trapper/trapper.md3", 0, 0, 0 }, + 1.0f, //float modelScale; + { -15, -15, -15 }, //vec3_t mins; + { 15, 15, 15 }, //vec3_t maxs; + 0.0f, //float zOffset; + TR_GRAVITY, //trType_t traj; + 0.0, //float bounce; + TRAPPER_BP, //int buildPoints; + ( 1 << S2 )|( 1 << S3 ), //int stages //NEEDS ADV BUILDER SO S2 AND UP + TRAPPER_HEALTH, //int health; + TRAPPER_REGEN, //int regenRate; + TRAPPER_SPLASHDAMAGE, //int splashDamage; + TRAPPER_SPLASHRADIUS, //int splashRadius; + MOD_ASPAWN, //int meansOfDeath; + BIT_ALIENS, //int team; + ( 1 << WP_ABUILD )|( 1 << WP_ABUILD2 ), //weapon_t buildWeapon; + BANIM_IDLE1, //int idleAnim; + 100, //int nextthink; + TRAPPER_BT, //int buildTime; + qfalse, //qboolean usable; + TRAPPER_RANGE, //int turretRange; + TRAPPER_REPEAT, //int turretFireSpeed; + WP_LOCKBLOB_LAUNCHER, //weapon_t turretProjType; + 0.0f, //float minNormal; + qtrue, //qboolean invertNormal; + qtrue, //qboolean creepTest; + TRAPPER_CREEPSIZE, //int creepSize; + qfalse, //qboolean dccTest; + qtrue, //qboolean transparentTest; + qfalse, //qboolean reactorTest; + qfalse, //qboolean replacable; + }, + { + BA_A_OVERMIND, //int buildNum; + "overmind", //char *buildName; + "Overmind", //char *humanName; + "team_alien_overmind", //char *entityName; + { "models/buildables/overmind/overmind.md3", 0, 0, 0 }, + 1.0f, //float modelScale; + { -45, -45, -15 }, //vec3_t mins; + { 45, 45, 95 }, //vec3_t maxs; + 0.0f, //float zOffset; + TR_GRAVITY, //trType_t traj; + 0.0, //float bounce; + OVERMIND_BP, //int buildPoints; + ( 1 << S1 )|( 1 << S2 )|( 1 << S3 ), //int stages + OVERMIND_HEALTH, //int health; + OVERMIND_REGEN, //int regenRate; + OVERMIND_SPLASHDAMAGE, //int splashDamage; + OVERMIND_SPLASHRADIUS, //int splashRadius; + MOD_ASPAWN, //int meansOfDeath; + BIT_ALIENS, //int team; + ( 1 << WP_ABUILD )|( 1 << WP_ABUILD2 ), //weapon_t buildWeapon; + BANIM_IDLE1, //int idleAnim; + OVERMIND_ATTACK_REPEAT,//int nextthink; + OVERMIND_BT, //int buildTime; + qfalse, //qboolean usable; + 0, //int turretRange; + 0, //int turretFireSpeed; + WP_NONE, //weapon_t turretProjType; + 0.95f, //float minNormal; + qfalse, //qboolean invertNormal; + qfalse, //qboolean creepTest; + OVERMIND_CREEPSIZE, //int creepSize; + qfalse, //qboolean dccTest; + qfalse, //qboolean transparentTest; + qtrue, //qboolean reactorTest; + qtrue, //qboolean replacable; + }, + { + BA_A_HOVEL, //int buildNum; + "hovel", //char *buildName; + "Hovel", //char *humanName; + "team_alien_hovel", //char *entityName; + { "models/buildables/hovel/hovel.md3", 0, 0, 0 }, + 1.0f, //float modelScale; + { -50, -50, -20 }, //vec3_t mins; + { 50, 50, 20 }, //vec3_t maxs; + 0.0f, //float zOffset; + TR_GRAVITY, //trType_t traj; + 0.0, //float bounce; + HOVEL_BP, //int buildPoints; + ( 1 << S3 ), //int stages + HOVEL_HEALTH, //int health; + HOVEL_REGEN, //int regenRate; + HOVEL_SPLASHDAMAGE, //int splashDamage; + HOVEL_SPLASHRADIUS, //int splashRadius; + MOD_ASPAWN, //int meansOfDeath; + BIT_ALIENS, //int team; + ( 1 << WP_ABUILD )|( 1 << WP_ABUILD2 ), //weapon_t buildWeapon; + BANIM_IDLE1, //int idleAnim; + 150, //int nextthink; + HOVEL_BT, //int buildTime; + qtrue, //qboolean usable; + 0, //int turretRange; + 0, //int turretFireSpeed; + WP_NONE, //weapon_t turretProjType; + 0.95f, //float minNormal; + qfalse, //qboolean invertNormal; + qtrue, //qboolean creepTest; + HOVEL_CREEPSIZE, //int creepSize; + qfalse, //qboolean dccTest; + qfalse, //qboolean transparentTest; + qtrue, //qboolean reactorTest; + qfalse, //qboolean replacable; + }, + { + BA_H_SPAWN, //int buildNum; + "telenode", //char *buildName; + "Telenode", //char *humanName; + "team_human_spawn", //char *entityName; + { "models/buildables/telenode/telenode.md3", 0, 0, 0 }, + 1.0f, //float modelScale; + { -40, -40, -4 }, //vec3_t mins; + { 40, 40, 4 }, //vec3_t maxs; + 0.0f, //float zOffset; + TR_GRAVITY, //trType_t traj; + 0.0, //float bounce; + HSPAWN_BP, //int buildPoints; + ( 1 << S1 )|( 1 << S2 )|( 1 << S3 ), //int stages + HSPAWN_HEALTH, //int health; + 0, //int regenRate; + HSPAWN_SPLASHDAMAGE, //int splashDamage; + HSPAWN_SPLASHRADIUS, //int splashRadius; + MOD_HSPAWN, //int meansOfDeath; + BIT_HUMANS, //int team; + ( 1 << WP_HBUILD )|( 1 << WP_HBUILD2 ), //weapon_t buildWeapon; + BANIM_IDLE1, //int idleAnim; + 100, //int nextthink; + HSPAWN_BT, //int buildTime; + qfalse, //qboolean usable; + 0, //int turretRange; + 0, //int turretFireSpeed; + WP_NONE, //weapon_t turretProjType; + 0.95f, //float minNormal; + qfalse, //qboolean invertNormal; + qfalse, //qboolean creepTest; + 0, //int creepSize; + qfalse, //qboolean dccTest; + qtrue, //qboolean transparentTest; + qfalse, //qboolean reactorTest; + qfalse, //qboolean replacable; + }, + { + BA_H_MEDISTAT, //int buildNum; + "medistat", //char *buildName; + "Medistation", //char *humanName; + "team_human_medistat", //char *entityName; + { "models/buildables/medistat/medistat.md3", 0, 0, 0 }, + 1.0f, //float modelScale; + { -35, -35, -7 }, //vec3_t mins; + { 35, 35, 7 }, //vec3_t maxs; + 0.0f, //float zOffset; + TR_GRAVITY, //trType_t traj; + 0.0, //float bounce; + MEDISTAT_BP, //int buildPoints; + ( 1 << S1 )|( 1 << S2 )|( 1 << S3 ), //int stages + MEDISTAT_HEALTH, //int health; + 0, //int regenRate; + MEDISTAT_SPLASHDAMAGE, //int splashDamage; + MEDISTAT_SPLASHRADIUS, //int splashRadius; + MOD_HSPAWN, //int meansOfDeath; + BIT_HUMANS, //int team; + ( 1 << WP_HBUILD )|( 1 << WP_HBUILD2 ), //weapon_t buildWeapon; + BANIM_IDLE1, //int idleAnim; + 100, //int nextthink; + MEDISTAT_BT, //int buildTime; + qfalse, //qboolean usable; + 0, //int turretRange; + 0, //int turretFireSpeed; + WP_NONE, //weapon_t turretProjType; + 0.95f, //float minNormal; + qfalse, //qboolean invertNormal; + qfalse, //qboolean creepTest; + 0, //int creepSize; + qfalse, //qboolean dccTest; + qtrue, //qboolean transparentTest; + qfalse, //qboolean reactorTest; + qtrue, //qboolean replacable; + }, + { + BA_H_MGTURRET, //int buildNum; + "mgturret", //char *buildName; + "Machinegun Turret", //char *humanName; + "team_human_mgturret", //char *entityName; + { "models/buildables/mgturret/turret_base.md3", + "models/buildables/mgturret/turret_barrel.md3", + "models/buildables/mgturret/turret_top.md3", 0 }, + 1.0f, //float modelScale; + { -25, -25, -20 }, //vec3_t mins; + { 25, 25, 20 }, //vec3_t maxs; + 0.0f, //float zOffset; + TR_GRAVITY, //trType_t traj; + 0.0, //float bounce; + MGTURRET_BP, //int buildPoints; + ( 1 << S1 )|( 1 << S2 )|( 1 << S3 ), //int stages + MGTURRET_HEALTH, //int health; + 0, //int regenRate; + MGTURRET_SPLASHDAMAGE, //int splashDamage; + MGTURRET_SPLASHRADIUS, //int splashRadius; + MOD_HSPAWN, //int meansOfDeath; + BIT_HUMANS, //int team; + ( 1 << WP_HBUILD )|( 1 << WP_HBUILD2 ), //weapon_t buildWeapon; + BANIM_IDLE1, //int idleAnim; + 50, //int nextthink; + MGTURRET_BT, //int buildTime; + qfalse, //qboolean usable; + MGTURRET_RANGE, //int turretRange; + MGTURRET_REPEAT, //int turretFireSpeed; + WP_MGTURRET, //weapon_t turretProjType; + 0.95f, //float minNormal; + qfalse, //qboolean invertNormal; + qfalse, //qboolean creepTest; + 0, //int creepSize; + qfalse, //qboolean dccTest; + qtrue, //qboolean transparentTest; + qfalse, //qboolean reactorTest; + qfalse, //qboolean replacable; + }, + { + BA_H_TESLAGEN, //int buildNum; + "tesla", //char *buildName; + "Tesla Generator", //char *humanName; + "team_human_tesla", //char *entityName; + { "models/buildables/tesla/tesla.md3", 0, 0, 0 }, + 1.0f, //float modelScale; + { -22, -22, -40 }, //vec3_t mins; + { 22, 22, 40 }, //vec3_t maxs; + 0.0f, //float zOffset; + TR_GRAVITY, //trType_t traj; + 0.0, //float bounce; + TESLAGEN_BP, //int buildPoints; + ( 1 << S3 ), //int stages + TESLAGEN_HEALTH, //int health; + 0, //int regenRate; + TESLAGEN_SPLASHDAMAGE, //int splashDamage; + TESLAGEN_SPLASHRADIUS, //int splashRadius; + MOD_HSPAWN, //int meansOfDeath; + BIT_HUMANS, //int team; + ( 1 << WP_HBUILD2 ), //weapon_t buildWeapon; + BANIM_IDLE1, //int idleAnim; + 150, //int nextthink; + TESLAGEN_BT, //int buildTime; + qfalse, //qboolean usable; + TESLAGEN_RANGE, //int turretRange; + TESLAGEN_REPEAT, //int turretFireSpeed; + WP_TESLAGEN, //weapon_t turretProjType; + 0.95f, //float minNormal; + qfalse, //qboolean invertNormal; + qfalse, //qboolean creepTest; + 0, //int creepSize; + qtrue, //qboolean dccTest; + qtrue, //qboolean transparentTest; + qfalse, //qboolean reactorTest; + qfalse, //qboolean replacable; + }, + { + BA_H_DCC, //int buildNum; + "dcc", //char *buildName; + "Defence Computer", //char *humanName; + "team_human_dcc", //char *entityName; + { "models/buildables/dcc/dcc.md3", 0, 0, 0 }, + 1.0f, //float modelScale; + { -35, -35, -13 }, //vec3_t mins; + { 35, 35, 47 }, //vec3_t maxs; + 0.0f, //float zOffset; + TR_GRAVITY, //trType_t traj; + 0.0, //float bounce; + DC_BP, //int buildPoints; + ( 1 << S2 )|( 1 << S3 ), //int stages + DC_HEALTH, //int health; + 0, //int regenRate; + DC_SPLASHDAMAGE, //int splashDamage; + DC_SPLASHRADIUS, //int splashRadius; + MOD_HSPAWN, //int meansOfDeath; + BIT_HUMANS, //int team; + ( 1 << WP_HBUILD2 ), //weapon_t buildWeapon; + BANIM_IDLE1, //int idleAnim; + 100, //int nextthink; + DC_BT, //int buildTime; + qfalse, //qboolean usable; + 0, //int turretRange; + 0, //int turretFireSpeed; + WP_NONE, //weapon_t turretProjType; + 0.95f, //float minNormal; + qfalse, //qboolean invertNormal; + qfalse, //qboolean creepTest; + 0, //int creepSize; + qfalse, //qboolean dccTest; + qfalse, //qboolean transparentTest; + qfalse, //qboolean reactorTest; + qtrue, //qboolean replacable; + }, + { + BA_H_ARMOURY, //int buildNum; + "arm", //char *buildName; + "Armoury", //char *humanName; + "team_human_armoury", //char *entityName; + { "models/buildables/arm/arm.md3", 0, 0, 0 }, + 1.0f, //float modelScale; + { -40, -40, -13 }, //vec3_t mins; + { 40, 40, 50 }, //vec3_t maxs; + 0.0f, //float zOffset; + TR_GRAVITY, //trType_t traj; + 0.0, //float bounce; + ARMOURY_BP, //int buildPoints; + ( 1 << S1 )|( 1 << S2 )|( 1 << S3 ), //int stages + ARMOURY_HEALTH, //int health; + 0, //int regenRate; + ARMOURY_SPLASHDAMAGE, //int splashDamage; + ARMOURY_SPLASHRADIUS, //int splashRadius; + MOD_HSPAWN, //int meansOfDeath; + BIT_HUMANS, //int team; + ( 1 << WP_HBUILD )|( 1 << WP_HBUILD2 ), //weapon_t buildWeapon; + BANIM_IDLE1, //int idleAnim; + 100, //int nextthink; + ARMOURY_BT, //int buildTime; + qtrue, //qboolean usable; + 0, //int turretRange; + 0, //int turretFireSpeed; + WP_NONE, //weapon_t turretProjType; + 0.95f, //float minNormal; + qfalse, //qboolean invertNormal; + qfalse, //qboolean creepTest; + 0, //int creepSize; + qfalse, //qboolean dccTest; + qfalse, //qboolean transparentTest; + qfalse, //qboolean reactorTest; + qtrue, //qboolean replacable; + }, + { + BA_H_REACTOR, //int buildNum; + "reactor", //char *buildName; + "Reactor", //char *humanName; + "team_human_reactor", //char *entityName; + { "models/buildables/reactor/reactor.md3", 0, 0, 0 }, + 1.0f, //float modelScale; + { -50, -50, -15 }, //vec3_t mins; + { 50, 50, 95 }, //vec3_t maxs; + 0.0f, //float zOffset; + TR_GRAVITY, //trType_t traj; + 0.0, //float bounce; + REACTOR_BP, //int buildPoints; + ( 1 << S1 )|( 1 << S2 )|( 1 << S3 ), //int stages + REACTOR_HEALTH, //int health; + 0, //int regenRate; + REACTOR_SPLASHDAMAGE, //int splashDamage; + REACTOR_SPLASHRADIUS, //int splashRadius; + MOD_HSPAWN, //int meansOfDeath; + BIT_HUMANS, //int team; + ( 1 << WP_HBUILD )|( 1 << WP_HBUILD2 ), //weapon_t buildWeapon; + BANIM_IDLE1, //int idleAnim; + REACTOR_ATTACK_REPEAT, //int nextthink; + REACTOR_BT, //int buildTime; + qtrue, //qboolean usable; + 0, //int turretRange; + 0, //int turretFireSpeed; + WP_NONE, //weapon_t turretProjType; + 0.95f, //float minNormal; + qfalse, //qboolean invertNormal; + qfalse, //qboolean creepTest; + 0, //int creepSize; + qfalse, //qboolean dccTest; + qfalse, //qboolean transparentTest; + qtrue, //qboolean reactorTest; + qtrue, //qboolean replacable; + }, + { + BA_H_REPEATER, //int buildNum; + "repeater", //char *buildName; + "Repeater", //char *humanName; + "team_human_repeater", //char *entityName; + { "models/buildables/repeater/repeater.md3", 0, 0, 0 }, + 1.0f, //float modelScale; + { -15, -15, -15 }, //vec3_t mins; + { 15, 15, 25 }, //vec3_t maxs; + 0.0f, //float zOffset; + TR_GRAVITY, //trType_t traj; + 0.0, //float bounce; + REPEATER_BP, //int buildPoints; + ( 1 << S2 )|( 1 << S3 ), //int stages + REPEATER_HEALTH, //int health; + 0, //int regenRate; + REPEATER_SPLASHDAMAGE, //int splashDamage; + REPEATER_SPLASHRADIUS, //int splashRadius; + MOD_HSPAWN, //int meansOfDeath; + BIT_HUMANS, //int team; + ( 1 << WP_HBUILD )|( 1 << WP_HBUILD2 ), //weapon_t buildWeapon; + BANIM_IDLE1, //int idleAnim; + 100, //int nextthink; + REPEATER_BT, //int buildTime; + qtrue, //qboolean usable; + 0, //int turretRange; + 0, //int turretFireSpeed; + WP_NONE, //weapon_t turretProjType; + 0.95f, //float minNormal; + qfalse, //qboolean invertNormal; + qfalse, //qboolean creepTest; + 0, //int creepSize; + qfalse, //qboolean dccTest; + qfalse, //qboolean transparentTest; + qfalse, //qboolean reactorTest; + qtrue, //qboolean replacable; + } +}; + +int bg_numBuildables = sizeof( bg_buildableList ) / sizeof( bg_buildableList[ 0 ] ); + +//separate from bg_buildableList to work around char struct init bug +buildableAttributeOverrides_t bg_buildableOverrideList[ BA_NUM_BUILDABLES ]; + +/* +============== +BG_FindBuildNumForName +============== +*/ +int BG_FindBuildNumForName( char *name ) +{ + int i; + + for( i = 0; i < bg_numBuildables; i++ ) + { + if( !Q_stricmp( bg_buildableList[ i ].buildName, name ) ) + return bg_buildableList[ i ].buildNum; + } + + //wimp out + return BA_NONE; +} + +/* +============== +BG_FindBuildNumForEntityName +============== +*/ +int BG_FindBuildNumForEntityName( char *name ) +{ + int i; + + for( i = 0; i < bg_numBuildables; i++ ) + { + if( !Q_stricmp( bg_buildableList[ i ].entityName, name ) ) + return bg_buildableList[ i ].buildNum; + } + + //wimp out + return BA_NONE; +} + +/* +============== +BG_FindNameForBuildNum +============== +*/ +char *BG_FindNameForBuildable( int bclass ) +{ + int i; + + for( i = 0; i < bg_numBuildables; i++ ) + { + if( bg_buildableList[ i ].buildNum == bclass ) + return bg_buildableList[ i ].buildName; + } + + //wimp out + return 0; +} + +/* +============== +BG_FindHumanNameForBuildNum +============== +*/ +char *BG_FindHumanNameForBuildable( int bclass ) +{ + int i; + + for( i = 0; i < bg_numBuildables; i++ ) + { + if( bg_buildableList[ i ].buildNum == bclass ) + return bg_buildableList[ i ].humanName; + } + + //wimp out + return 0; +} + +/* +============== +BG_FindEntityNameForBuildNum +============== +*/ +char *BG_FindEntityNameForBuildable( int bclass ) +{ + int i; + + for( i = 0; i < bg_numBuildables; i++ ) + { + if( bg_buildableList[ i ].buildNum == bclass ) + return bg_buildableList[ i ].entityName; + } + + //wimp out + return 0; +} + +/* +============== +BG_FindModelsForBuildNum +============== +*/ +char *BG_FindModelsForBuildable( int bclass, int modelNum ) +{ + int i; + + if( bg_buildableOverrideList[ bclass ].models[ modelNum ][ 0 ] != 0 ) + return bg_buildableOverrideList[ bclass ].models[ modelNum ]; + + for( i = 0; i < bg_numBuildables; i++ ) + { + if( bg_buildableList[ i ].buildNum == bclass ) + return bg_buildableList[ i ].models[ modelNum ]; + } + + //wimp out + return 0; +} + +/* +============== +BG_FindModelScaleForBuildable +============== +*/ +float BG_FindModelScaleForBuildable( int bclass ) +{ + int i; + + if( bg_buildableOverrideList[ bclass ].modelScale != 0.0f ) + return bg_buildableOverrideList[ bclass ].modelScale; + + for( i = 0; i < bg_numBuildables; i++ ) + { + if( bg_buildableList[ i ].buildNum == bclass ) + return bg_buildableList[ i ].modelScale; + } + + Com_Printf( S_COLOR_YELLOW "WARNING: fallthrough in BG_FindModelScaleForBuildable( %d )\n", bclass ); + return 1.0f; +} + +/* +============== +BG_FindBBoxForBuildable +============== +*/ +void BG_FindBBoxForBuildable( int bclass, vec3_t mins, vec3_t maxs ) +{ + int i; + + for( i = 0; i < bg_numBuildables; i++ ) + { + if( bg_buildableList[ i ].buildNum == bclass ) + { + if( mins != NULL ) + { + VectorCopy( bg_buildableList[ i ].mins, mins ); + + if( VectorLength( bg_buildableOverrideList[ bclass ].mins ) ) + VectorCopy( bg_buildableOverrideList[ bclass ].mins, mins ); + } + + if( maxs != NULL ) + { + VectorCopy( bg_buildableList[ i ].maxs, maxs ); + + if( VectorLength( bg_buildableOverrideList[ bclass ].maxs ) ) + VectorCopy( bg_buildableOverrideList[ bclass ].maxs, maxs ); + } + + return; + } + } + + if( mins != NULL ) + VectorCopy( bg_buildableList[ 0 ].mins, mins ); + + if( maxs != NULL ) + VectorCopy( bg_buildableList[ 0 ].maxs, maxs ); +} + +/* +============== +BG_FindZOffsetForBuildable +============== +*/ +float BG_FindZOffsetForBuildable( int bclass ) +{ + int i; + + if( bg_buildableOverrideList[ bclass ].zOffset != 0.0f ) + return bg_buildableOverrideList[ bclass ].zOffset; + + for( i = 0; i < bg_numBuildables; i++ ) + { + if( bg_buildableList[ i ].buildNum == bclass ) + { + return bg_buildableList[ i ].zOffset; + } + } + + return 0.0f; +} + +/* +============== +BG_FindTrajectoryForBuildable +============== +*/ +trType_t BG_FindTrajectoryForBuildable( int bclass ) +{ + int i; + + for( i = 0; i < bg_numBuildables; i++ ) + { + if( bg_buildableList[ i ].buildNum == bclass ) + { + return bg_buildableList[ i ].traj; + } + } + + return TR_GRAVITY; +} + +/* +============== +BG_FindBounceForBuildable +============== +*/ +float BG_FindBounceForBuildable( int bclass ) +{ + int i; + + for( i = 0; i < bg_numBuildables; i++ ) + { + if( bg_buildableList[ i ].buildNum == bclass ) + { + return bg_buildableList[ i ].bounce; + } + } + + return 0.0; +} + +/* +============== +BG_FindBuildPointsForBuildable +============== +*/ +int BG_FindBuildPointsForBuildable( int bclass ) +{ + int i; + + for( i = 0; i < bg_numBuildables; i++ ) + { + if( bg_buildableList[ i ].buildNum == bclass ) + { + return bg_buildableList[ i ].buildPoints; + } + } + + return 1000; +} + +/* +============== +BG_FindStagesForBuildable +============== +*/ +qboolean BG_FindStagesForBuildable( int bclass, stage_t stage ) +{ + int i; + + for( i = 0; i < bg_numBuildables; i++ ) + { + if( bg_buildableList[ i ].buildNum == bclass ) + { + if( bg_buildableList[ i ].stages & ( 1 << stage ) ) + return qtrue; + else + return qfalse; + } + } + + return qfalse; +} + +/* +============== +BG_FindHealthForBuildable +============== +*/ +int BG_FindHealthForBuildable( int bclass ) +{ + int i; + + for( i = 0; i < bg_numBuildables; i++ ) + { + if( bg_buildableList[ i ].buildNum == bclass ) + { + return bg_buildableList[ i ].health; + } + } + + return 1000; +} + +/* +============== +BG_FindRegenRateForBuildable +============== +*/ +int BG_FindRegenRateForBuildable( int bclass ) +{ + int i; + + for( i = 0; i < bg_numBuildables; i++ ) + { + if( bg_buildableList[ i ].buildNum == bclass ) + { + return bg_buildableList[ i ].regenRate; + } + } + + return 0; +} + +/* +============== +BG_FindSplashDamageForBuildable +============== +*/ +int BG_FindSplashDamageForBuildable( int bclass ) +{ + int i; + + for( i = 0; i < bg_numBuildables; i++ ) + { + if( bg_buildableList[ i ].buildNum == bclass ) + { + return bg_buildableList[ i ].splashDamage; + } + } + + return 50; +} + +/* +============== +BG_FindSplashRadiusForBuildable +============== +*/ +int BG_FindSplashRadiusForBuildable( int bclass ) +{ + int i; + + for( i = 0; i < bg_numBuildables; i++ ) + { + if( bg_buildableList[ i ].buildNum == bclass ) + { + return bg_buildableList[ i ].splashRadius; + } + } + + return 200; +} + +/* +============== +BG_FindMODForBuildable +============== +*/ +int BG_FindMODForBuildable( int bclass ) +{ + int i; + + for( i = 0; i < bg_numBuildables; i++ ) + { + if( bg_buildableList[ i ].buildNum == bclass ) + { + return bg_buildableList[ i ].meansOfDeath; + } + } + + return MOD_UNKNOWN; +} + +/* +============== +BG_FindTeamForBuildable +============== +*/ +int BG_FindTeamForBuildable( int bclass ) +{ + int i; + + for( i = 0; i < bg_numBuildables; i++ ) + { + if( bg_buildableList[ i ].buildNum == bclass ) + { + return bg_buildableList[ i ].team; + } + } + + return BIT_NONE; +} + +/* +============== +BG_FindBuildWeaponForBuildable +============== +*/ +weapon_t BG_FindBuildWeaponForBuildable( int bclass ) +{ + int i; + + for( i = 0; i < bg_numBuildables; i++ ) + { + if( bg_buildableList[ i ].buildNum == bclass ) + { + return bg_buildableList[ i ].buildWeapon; + } + } + + return BA_NONE; +} + +/* +============== +BG_FindAnimForBuildable +============== +*/ +int BG_FindAnimForBuildable( int bclass ) +{ + int i; + + for( i = 0; i < bg_numBuildables; i++ ) + { + if( bg_buildableList[ i ].buildNum == bclass ) + { + return bg_buildableList[ i ].idleAnim; + } + } + + return BANIM_IDLE1; +} + +/* +============== +BG_FindNextThinkForBuildable +============== +*/ +int BG_FindNextThinkForBuildable( int bclass ) +{ + int i; + + for( i = 0; i < bg_numBuildables; i++ ) + { + if( bg_buildableList[ i ].buildNum == bclass ) + { + return bg_buildableList[ i ].nextthink; + } + } + + return 100; +} + +/* +============== +BG_FindBuildTimeForBuildable +============== +*/ +int BG_FindBuildTimeForBuildable( int bclass ) +{ + int i; + + for( i = 0; i < bg_numBuildables; i++ ) + { + if( bg_buildableList[ i ].buildNum == bclass ) + { + return bg_buildableList[ i ].buildTime; + } + } + + return 10000; +} + +/* +============== +BG_FindUsableForBuildable +============== +*/ +qboolean BG_FindUsableForBuildable( int bclass ) +{ + int i; + + for( i = 0; i < bg_numBuildables; i++ ) + { + if( bg_buildableList[ i ].buildNum == bclass ) + { + return bg_buildableList[ i ].usable; + } + } + + return qfalse; +} + +/* +============== +BG_FindFireSpeedForBuildable +============== +*/ +int BG_FindFireSpeedForBuildable( int bclass ) +{ + int i; + + for( i = 0; i < bg_numBuildables; i++ ) + { + if( bg_buildableList[ i ].buildNum == bclass ) + { + return bg_buildableList[ i ].turretFireSpeed; + } + } + + return 1000; +} + +/* +============== +BG_FindRangeForBuildable +============== +*/ +int BG_FindRangeForBuildable( int bclass ) +{ + int i; + + for( i = 0; i < bg_numBuildables; i++ ) + { + if( bg_buildableList[ i ].buildNum == bclass ) + { + return bg_buildableList[ i ].turretRange; + } + } + + return 1000; +} + +/* +============== +BG_FindProjTypeForBuildable +============== +*/ +weapon_t BG_FindProjTypeForBuildable( int bclass ) +{ + int i; + + for( i = 0; i < bg_numBuildables; i++ ) + { + if( bg_buildableList[ i ].buildNum == bclass ) + { + return bg_buildableList[ i ].turretProjType; + } + } + + return WP_NONE; +} + +/* +============== +BG_FindMinNormalForBuildable +============== +*/ +float BG_FindMinNormalForBuildable( int bclass ) +{ + int i; + + for( i = 0; i < bg_numBuildables; i++ ) + { + if( bg_buildableList[ i ].buildNum == bclass ) + { + return bg_buildableList[ i ].minNormal; + } + } + + return 0.707f; +} + +/* +============== +BG_FindInvertNormalForBuildable +============== +*/ +qboolean BG_FindInvertNormalForBuildable( int bclass ) +{ + int i; + + for( i = 0; i < bg_numBuildables; i++ ) + { + if( bg_buildableList[ i ].buildNum == bclass ) + { + return bg_buildableList[ i ].invertNormal; + } + } + + return qfalse; +} + +/* +============== +BG_FindCreepTestForBuildable +============== +*/ +int BG_FindCreepTestForBuildable( int bclass ) +{ + int i; + + for( i = 0; i < bg_numBuildables; i++ ) + { + if( bg_buildableList[ i ].buildNum == bclass ) + { + return bg_buildableList[ i ].creepTest; + } + } + + return qfalse; +} + +/* +============== +BG_FindCreepSizeForBuildable +============== +*/ +int BG_FindCreepSizeForBuildable( int bclass ) +{ + int i; + + for( i = 0; i < bg_numBuildables; i++ ) + { + if( bg_buildableList[ i ].buildNum == bclass ) + { + return bg_buildableList[ i ].creepSize; + } + } + + return CREEP_BASESIZE; +} + +/* +============== +BG_FindDCCTestForBuildable +============== +*/ +int BG_FindDCCTestForBuildable( int bclass ) +{ + int i; + + for( i = 0; i < bg_numBuildables; i++ ) + { + if( bg_buildableList[ i ].buildNum == bclass ) + { + return bg_buildableList[ i ].dccTest; + } + } + + return qfalse; +} + +/* +============== +BG_FindUniqueTestForBuildable +============== +*/ +int BG_FindUniqueTestForBuildable( int bclass ) +{ + int i; + + for( i = 0; i < bg_numBuildables; i++ ) + { + if( bg_buildableList[ i ].buildNum == bclass ) + { + return bg_buildableList[ i ].reactorTest; + } + } + + return qfalse; +} + +/* +============== +BG_FindReplaceableTestForBuildable +============== +*/ +qboolean BG_FindReplaceableTestForBuildable( int bclass ) +{ + int i; + + for( i = 0; i < bg_numBuildables; i++ ) + { + if( bg_buildableList[ i ].buildNum == bclass ) + { + return bg_buildableList[ i ].replaceable; + } + } + return qfalse; +} + +/* +============== +BG_FindOverrideForBuildable +============== +*/ +static buildableAttributeOverrides_t *BG_FindOverrideForBuildable( int bclass ) +{ + return &bg_buildableOverrideList[ bclass ]; +} + +/* +============== +BG_FindTransparentTestForBuildable +============== +*/ +qboolean BG_FindTransparentTestForBuildable( int bclass ) +{ + int i; + + for( i = 0; i < bg_numBuildables; i++ ) + { + if( bg_buildableList[ i ].buildNum == bclass ) + { + return bg_buildableList[ i ].transparentTest; + } + } + return qfalse; +} + +/* +====================== +BG_ParseBuildableFile + +Parses a configuration file describing a builable +====================== +*/ +static qboolean BG_ParseBuildableFile( const char *filename, buildableAttributeOverrides_t *bao ) +{ + char *text_p; + int i; + int len; + char *token; + char text[ 20000 ]; + fileHandle_t f; + float scale; + + + // load the file + len = trap_FS_FOpenFile( filename, &f, FS_READ ); + if( len < 0 ) + return qfalse; + + if( len == 0 || len >= sizeof( text ) - 1 ) + { + trap_FS_FCloseFile( f ); + Com_Printf( S_COLOR_RED "ERROR: Buildable file %s is %s\n", filename, + len == 0 ? "empty" : "too long" ); + return qfalse; + } + + trap_FS_Read( text, len, f ); + text[ len ] = 0; + trap_FS_FCloseFile( f ); + + // parse the text + text_p = text; + + // read optional parameters + while( 1 ) + { + token = COM_Parse( &text_p ); + + if( !token ) + break; + + if( !Q_stricmp( token, "" ) ) + break; + + if( !Q_stricmp( token, "model" ) ) + { + int index = 0; + + token = COM_Parse( &text_p ); + if( !token ) + break; + + index = atoi( token ); + + if( index < 0 ) + index = 0; + else if( index > 3 ) + index = 3; + + token = COM_Parse( &text_p ); + if( !token ) + break; + + Q_strncpyz( bao->models[ index ], token, sizeof( bao->models[ 0 ] ) ); + + continue; + } + else if( !Q_stricmp( token, "modelScale" ) ) + { + token = COM_Parse( &text_p ); + if( !token ) + break; + + scale = atof( token ); + + if( scale < 0.0f ) + scale = 0.0f; + + bao->modelScale = scale; + + continue; + } + else if( !Q_stricmp( token, "mins" ) ) + { + for( i = 0; i <= 2; i++ ) + { + token = COM_Parse( &text_p ); + if( !token ) + break; + + bao->mins[ i ] = atof( token ); + } + + continue; + } + else if( !Q_stricmp( token, "maxs" ) ) + { + for( i = 0; i <= 2; i++ ) + { + token = COM_Parse( &text_p ); + if( !token ) + break; + + bao->maxs[ i ] = atof( token ); + } + + continue; + } + else if( !Q_stricmp( token, "zOffset" ) ) + { + float offset; + + token = COM_Parse( &text_p ); + if( !token ) + break; + + offset = atof( token ); + + bao->zOffset = offset; + + continue; + } + + + Com_Printf( S_COLOR_RED "ERROR: unknown token '%s'\n", token ); + return qfalse; + } + + return qtrue; +} + +/* +=============== +BG_InitBuildableOverrides + +Set any overrides specfied by file +=============== +*/ +void BG_InitBuildableOverrides( void ) +{ + int i; + buildableAttributeOverrides_t *bao; + + for( i = BA_NONE + 1; i < BA_NUM_BUILDABLES; i++ ) + { + bao = BG_FindOverrideForBuildable( i ); + + BG_ParseBuildableFile( va( "overrides/buildables/%s.cfg", BG_FindNameForBuildable( i ) ), bao ); + } +} + +//////////////////////////////////////////////////////////////////////////////// + +classAttributes_t bg_classList[ ] = +{ + { + PCL_NONE, //int classnum; + "spectator", //char *className; + "Spectator", //char *humanName; + "", //char *modelname; + 1.0f, //float modelScale; + "", //char *skinname; + 1.0f, //float shadowScale; + "", //char *hudname; + ( 1 << S1 )|( 1 << S2 )|( 1 << S3 ), //int stages + { -15, -15, -15 }, //vec3_t mins; + { 15, 15, 15 }, //vec3_t maxs; + { 15, 15, 15 }, //vec3_t crouchmaxs; + { -15, -15, -15 }, //vec3_t deadmins; + { 15, 15, 15 }, //vec3_t deadmaxs; + 0.0f, //float zOffset + 0, 0, //int viewheight, crouchviewheight; + 0, //int health; + 0.0f, //float fallDamage; + 0, //int regenRate; + 0, //int abilities; + WP_NONE, //weapon_t startWeapon + 0.0f, //float buildDist; + 90, //int fov; + 0.000f, //float bob; + 1.0f, //float bobCycle; + 0, //int steptime; + 600, //float speed; + 10.0f, //float acceleration; + 1.0f, //float airAcceleration; + 6.0f, //float friction; + 100.0f, //float stopSpeed; + 270.0f, //float jumpMagnitude; + 1.0f, //float knockbackScale; + { PCL_NONE, PCL_NONE, PCL_NONE }, //int children[ 3 ]; + 0, //int cost; + 0 //int value; + }, + { + PCL_ALIEN_BUILDER0, //int classnum; + "builder", //char *className; + "Builder", //char *humanName; + "builder", //char *modelname; + 1.0f, //float modelScale; + "default", //char *skinname; + 1.0f, //float shadowScale; + "alien_builder_hud", //char *hudname; + ( 1 << S1 )|( 1 << S2 )|( 1 << S3 ), //int stages + { -15, -15, -20 }, //vec3_t mins; + { 15, 15, 20 }, //vec3_t maxs; + { 15, 15, 20 }, //vec3_t crouchmaxs; + { -15, -15, -4 }, //vec3_t deadmins; + { 15, 15, 4 }, //vec3_t deadmaxs; + 0.0f, //float zOffset + 0, 0, //int viewheight, crouchviewheight; + ABUILDER_HEALTH, //int health; + 0.2f, //float fallDamage; + ABUILDER_REGEN, //int regenRate; + SCA_TAKESFALLDAMAGE|SCA_FOVWARPS|SCA_ALIENSENSE,//int abilities; + WP_ABUILD, //weapon_t startWeapon + 95.0f, //float buildDist; + 80, //int fov; + 0.001f, //float bob; + 2.0f, //float bobCycle; + 150, //int steptime; + ABUILDER_SPEED, //float speed; + 10.0f, //float acceleration; + 1.0f, //float airAcceleration; + 6.0f, //float friction; + 100.0f, //float stopSpeed; + 195.0f, //float jumpMagnitude; + 1.0f, //float knockbackScale; + { PCL_ALIEN_BUILDER0_UPG, PCL_ALIEN_LEVEL0, PCL_NONE }, //int children[ 3 ]; + ABUILDER_COST, //int cost; + ABUILDER_VALUE //int value; + }, + { + PCL_ALIEN_BUILDER0_UPG, //int classnum; + "builderupg", //char *classname; + "Advanced Builder", //char *humanname; + "builder", //char *modelname; + 1.0f, //float modelScale; + "advanced", //char *skinname; + 1.0f, //float shadowScale; + "alien_builder_hud", //char *hudname; + ( 1 << S2 )|( 1 << S3 ), //int stages + { -20, -20, -20 }, //vec3_t mins; + { 20, 20, 20 }, //vec3_t maxs; + { 20, 20, 20 }, //vec3_t crouchmaxs; + { -20, -20, -4 }, //vec3_t deadmins; + { 20, 20, 4 }, //vec3_t deadmaxs; + 0.0f, //float zOffset + 0, 0, //int viewheight, crouchviewheight; + ABUILDER_UPG_HEALTH, //int health; + 0.0f, //float fallDamage; + ABUILDER_UPG_REGEN, //int regenRate; + SCA_FOVWARPS|SCA_WALLCLIMBER|SCA_ALIENSENSE, //int abilities; + WP_ABUILD2, //weapon_t startWeapon + 105.0f, //float buildDist; + 110, //int fov; + 0.001f, //float bob; + 2.0f, //float bobCycle; + 100, //int steptime; + ABUILDER_UPG_SPEED, //float speed; + 10.0f, //float acceleration; + 1.0f, //float airAcceleration; + 6.0f, //float friction; + 100.0f, //float stopSpeed; + 270.0f, //float jumpMagnitude; + 1.0f, //float knockbackScale; + { PCL_ALIEN_LEVEL0, PCL_NONE, PCL_NONE }, //int children[ 3 ]; + ABUILDER_UPG_COST, //int cost; + ABUILDER_UPG_VALUE //int value; + }, + { + PCL_ALIEN_LEVEL0, //int classnum; + "level0", //char *classname; + "Soldier", //char *humanname; + "jumper", //char *modelname; + 0.2f, //float modelScale; + "default", //char *skinname; + 0.3f, //float shadowScale; + "alien_general_hud", //char *hudname; + ( 1 << S1 )|( 1 << S2 )|( 1 << S3 ), //int stages + { -15, -15, -15 }, //vec3_t mins; + { 15, 15, 15 }, //vec3_t maxs; + { 15, 15, 15 }, //vec3_t crouchmaxs; + { -15, -15, -4 }, //vec3_t deadmins; + { 15, 15, 4 }, //vec3_t deadmaxs; + -8.0f, //float zOffset + 0, 0, //int viewheight, crouchviewheight; + LEVEL0_HEALTH, //int health; + 0.0f, //float fallDamage; + LEVEL0_REGEN, //int regenRate; + SCA_WALLCLIMBER|SCA_NOWEAPONDRIFT| + SCA_FOVWARPS|SCA_ALIENSENSE, //int abilities; + WP_ALEVEL0, //weapon_t startWeapon + 0.0f, //float buildDist; + 140, //int fov; + 0.0f, //float bob; + 2.5f, //float bobCycle; + 25, //int steptime; + LEVEL0_SPEED, //float speed; + 10.0f, //float acceleration; + 1.0f, //float airAcceleration; + 6.0f, //float friction; + 400.0f, //float stopSpeed; + 250.0f, //float jumpMagnitude; + 2.0f, //float knockbackScale; + { PCL_ALIEN_LEVEL1, PCL_NONE, PCL_NONE }, //int children[ 3 ]; + LEVEL0_COST, //int cost; + LEVEL0_VALUE //int value; + }, + { + PCL_ALIEN_LEVEL1, //int classnum; + "level1", //char *classname; + "Hydra", //char *humanname; + "spitter", //char *modelname; + 0.6f, //float modelScale; + "default", //char *skinname; + 1.0f, //float shadowScale; + "alien_general_hud", //char *hudname; + ( 1 << S1 )|( 1 << S2 )|( 1 << S3 ), //int stages + { -18, -18, -18 }, //vec3_t mins; + { 18, 18, 18 }, //vec3_t maxs; + { 18, 18, 18 }, //vec3_t crouchmaxs; + { -18, -18, -4 }, //vec3_t deadmins; + { 18, 18, 4 }, //vec3_t deadmaxs; + 0.0f, //float zOffset + 0, 0, //int viewheight, crouchviewheight; + LEVEL1_HEALTH, //int health; + 0.0f, //float fallDamage; + LEVEL1_REGEN, //int regenRate; + SCA_NOWEAPONDRIFT| + SCA_FOVWARPS|SCA_WALLCLIMBER|SCA_ALIENSENSE, //int abilities; + WP_ALEVEL1, //weapon_t startWeapon + 0.0f, //float buildDist; + 120, //int fov; + 0.001f, //float bob; + 1.8f, //float bobCycle; + 60, //int steptime; + LEVEL1_SPEED, //float speed; + 10.0f, //float acceleration; + 1.0f, //float airAcceleration; + 6.0f, //float friction; + 300.0f, //float stopSpeed; + 270.0f, //float jumpMagnitude; + 1.2f, //float knockbackScale; + { PCL_ALIEN_LEVEL2, PCL_ALIEN_LEVEL1_UPG, PCL_NONE }, //int children[ 3 ]; + LEVEL1_COST, //int cost; + LEVEL1_VALUE //int value; + }, + { + PCL_ALIEN_LEVEL1_UPG, //int classnum; + "level1upg", //char *classname; + "Hydra Upgrade", //char *humanname; + "spitter", //char *modelname; + 0.7f, //float modelScale; + "blue", //char *skinname; + 1.0f, //float shadowScale; + "alien_general_hud", //char *hudname; + ( 1 << S2 )|( 1 << S3 ), //int stages + { -20, -20, -20 }, //vec3_t mins; + { 20, 20, 20 }, //vec3_t maxs; + { 20, 20, 20 }, //vec3_t crouchmaxs; + { -20, -20, -4 }, //vec3_t deadmins; + { 20, 20, 4 }, //vec3_t deadmaxs; + 0.0f, //float zOffset + 0, 0, //int viewheight, crouchviewheight; + LEVEL1_UPG_HEALTH, //int health; + 0.0f, //float fallDamage; + LEVEL1_UPG_REGEN, //int regenRate; + SCA_NOWEAPONDRIFT|SCA_FOVWARPS| + SCA_WALLCLIMBER|SCA_ALIENSENSE, //int abilities; + WP_ALEVEL1_UPG, //weapon_t startWeapon + 0.0f, //float buildDist; + 120, //int fov; + 0.001f, //float bob; + 1.8f, //float bobCycle; + 60, //int steptime; + LEVEL1_UPG_SPEED, //float speed; + 10.0f, //float acceleration; + 1.0f, //float airAcceleration; + 6.0f, //float friction; + 300.0f, //float stopSpeed; + 270.0f, //float jumpMagnitude; + 1.1f, //float knockbackScale; + { PCL_ALIEN_LEVEL2, PCL_NONE, PCL_NONE }, //int children[ 3 ]; + LEVEL1_UPG_COST, //int cost; + LEVEL1_UPG_VALUE //int value; + }, + { + PCL_ALIEN_LEVEL2, //int classnum; + "level2", //char *classname; + "Chimera", //char *humanname; + "tarantula", //char *modelname; + 0.75f, //float modelScale; + "default", //char *skinname; + 1.0f, //float shadowScale; + "alien_general_hud", //char *hudname; + ( 1 << S1 )|( 1 << S2 )|( 1 << S3 ), //int stages + { -22, -22, -22 }, //vec3_t mins; + { 22, 22, 22 }, //vec3_t maxs; + { 22, 22, 22 }, //vec3_t crouchmaxs; + { -22, -22, -4 }, //vec3_t deadmins; + { 22, 22, 4 }, //vec3_t deadmaxs; + 0.0f, //float zOffset + 10, 10, //int viewheight, crouchviewheight; + LEVEL2_HEALTH, //int health; + 0.0f, //float fallDamage; + LEVEL2_REGEN, //int regenRate; + SCA_NOWEAPONDRIFT|SCA_WALLJUMPER| + SCA_FOVWARPS|SCA_ALIENSENSE, //int abilities; + WP_ALEVEL2, //weapon_t startWeapon + 0.0f, //float buildDist; + 90, //int fov; + 0.001f, //float bob; + 1.5f, //float bobCycle; + 80, //int steptime; + LEVEL2_SPEED, //float speed; + 10.0f, //float acceleration; + 2.0f, //float airAcceleration; + 6.0f, //float friction; + 100.0f, //float stopSpeed; + 400.0f, //float jumpMagnitude; + 0.8f, //float knockbackScale; + { PCL_ALIEN_LEVEL3, PCL_ALIEN_LEVEL2_UPG, PCL_NONE }, //int children[ 3 ]; + LEVEL2_COST, //int cost; + LEVEL2_VALUE //int value; + }, + { + PCL_ALIEN_LEVEL2_UPG, //int classnum; + "level2upg", //char *classname; + "Chimera Upgrade", //char *humanname; + "tarantula", //char *modelname; + 0.9f, //float modelScale; + "red", //char *skinname; + 1.0f, //float shadowScale; + "alien_general_hud", //char *hudname; + ( 1 << S2 )|( 1 << S3 ), //int stages + { -24, -24, -24 }, //vec3_t mins; + { 24, 24, 24 }, //vec3_t maxs; + { 24, 24, 24 }, //vec3_t crouchmaxs; + { -24, -24, -4 }, //vec3_t deadmins; + { 24, 24, 4 }, //vec3_t deadmaxs; + 0.0f, //float zOffset + 12, 12, //int viewheight, crouchviewheight; + LEVEL2_UPG_HEALTH, //int health; + 0.0f, //float fallDamage; + LEVEL2_UPG_REGEN, //int regenRate; + SCA_NOWEAPONDRIFT|SCA_WALLJUMPER| + SCA_FOVWARPS|SCA_ALIENSENSE, //int abilities; + WP_ALEVEL2_UPG, //weapon_t startWeapon + 0.0f, //float buildDist; + 90, //int fov; + 0.001f, //float bob; + 1.5f, //float bobCycle; + 80, //int steptime; + LEVEL2_UPG_SPEED, //float speed; + 10.0f, //float acceleration; + 2.0f, //float airAcceleration; + 6.0f, //float friction; + 100.0f, //float stopSpeed; + 400.0f, //float jumpMagnitude; + 0.7f, //float knockbackScale; + { PCL_ALIEN_LEVEL3, PCL_NONE, PCL_NONE }, //int children[ 3 ]; + LEVEL2_UPG_COST, //int cost; + LEVEL2_UPG_VALUE //int value; + }, + { + PCL_ALIEN_LEVEL3, //int classnum; + "level3", //char *classname; + "Dragoon", //char *humanname; + "prowl", //char *modelname; + 1.0f, //float modelScale; + "default", //char *skinname; + 1.0f, //float shadowScale; + "alien_general_hud", //char *hudname; + ( 1 << S1 )|( 1 << S2 )|( 1 << S3 ), //int stages + { -32, -32, -21 }, //vec3_t mins; + { 32, 32, 21 }, //vec3_t maxs; + { 32, 32, 21 }, //vec3_t crouchmaxs; + { -32, -32, -4 }, //vec3_t deadmins; + { 32, 32, 4 }, //vec3_t deadmaxs; + 0.0f, //float zOffset + 24, 24, //int viewheight, crouchviewheight; + LEVEL3_HEALTH, //int health; + 0.0f, //float fallDamage; + LEVEL3_REGEN, //int regenRate; + SCA_NOWEAPONDRIFT| + SCA_FOVWARPS|SCA_ALIENSENSE, //int abilities; + WP_ALEVEL3, //weapon_t startWeapon + 0.0f, //float buildDist; + 110, //int fov; + 0.0005f, //float bob; + 1.3f, //float bobCycle; + 90, //int steptime; + LEVEL3_SPEED, //float speed; + 10.0f, //float acceleration; + 1.0f, //float airAcceleration; + 6.0f, //float friction; + 200.0f, //float stopSpeed; + 270.0f, //float jumpMagnitude; + 0.5f, //float knockbackScale; + { PCL_ALIEN_LEVEL4, PCL_ALIEN_LEVEL3_UPG, PCL_NONE }, //int children[ 3 ]; + LEVEL3_COST, //int cost; + LEVEL3_VALUE //int value; + }, + { + PCL_ALIEN_LEVEL3_UPG, //int classnum; + "level3upg", //char *classname; + "Dragoon Upgrade", //char *humanname; + "prowl", //char *modelname; + 1.0f, //float modelScale; + "default", //char *skinname; + 1.0f, //float shadowScale; + "alien_general_hud", //char *hudname; + ( 1 << S3 ), //int stages + { -32, -32, -21 }, //vec3_t mins; + { 32, 32, 21 }, //vec3_t maxs; + { 32, 32, 21 }, //vec3_t crouchmaxs; + { -32, -32, -4 }, //vec3_t deadmins; + { 32, 32, 4 }, //vec3_t deadmaxs; + 0.0f, //float zOffset + 27, 27, //int viewheight, crouchviewheight; + LEVEL3_UPG_HEALTH, //int health; + 0.0f, //float fallDamage; + LEVEL3_UPG_REGEN, //int regenRate; + SCA_NOWEAPONDRIFT| + SCA_FOVWARPS|SCA_ALIENSENSE, //int abilities; + WP_ALEVEL3_UPG, //weapon_t startWeapon + 0.0f, //float buildDist; + 110, //int fov; + 0.0005f, //float bob; + 1.3f, //float bobCycle; + 90, //int steptime; + LEVEL3_UPG_SPEED, //float speed; + 10.0f, //float acceleration; + 1.0f, //float airAcceleration; + 6.0f, //float friction; + 200.0f, //float stopSpeed; + 270.0f, //float jumpMagnitude; + 0.4f, //float knockbackScale; + { PCL_ALIEN_LEVEL4, PCL_NONE, PCL_NONE }, //int children[ 3 ]; + LEVEL3_UPG_COST, //int cost; + LEVEL3_UPG_VALUE //int value; + }, + { + PCL_ALIEN_LEVEL4, //int classnum; + "level4", //char *classname; + "Big Mofo", //char *humanname; + "mofo", //char *modelname; + 1.0f, //float modelScale; + "default", //char *skinname; + 2.0f, //float shadowScale; + "alien_general_hud", //char *hudname; + ( 1 << S3 ), //int stages + { -30, -30, -20 }, //vec3_t mins; + { 30, 30, 20 }, //vec3_t maxs; + { 30, 30, 20 }, //vec3_t crouchmaxs; + { -15, -15, -4 }, //vec3_t deadmins; + { 15, 15, 4 }, //vec3_t deadmaxs; + 0.0f, //float zOffset + 35, 35, //int viewheight, crouchviewheight; + LEVEL4_HEALTH, //int health; + 0.0f, //float fallDamage; + LEVEL4_REGEN, //int regenRate; + SCA_NOWEAPONDRIFT| + SCA_FOVWARPS|SCA_ALIENSENSE, //int abilities; + WP_ALEVEL4, //weapon_t startWeapon + 0.0f, //float buildDist; + 90, //int fov; + 0.001f, //float bob; + 1.1f, //float bobCycle; + 100, //int steptime; + LEVEL4_SPEED, //float speed; + 10.0f, //float acceleration; + 1.0f, //float airAcceleration; + 6.0f, //float friction; + 100.0f, //float stopSpeed; + 170.0f, //float jumpMagnitude; + 0.1f, //float knockbackScale; + { PCL_NONE, PCL_NONE, PCL_NONE }, //int children[ 3 ]; + LEVEL4_COST, //int cost; + LEVEL4_VALUE //int value; + }, + { + PCL_HUMAN, //int classnum; + "human_base", //char *classname; + "Human", //char *humanname; + "sarge", //char *modelname; + 1.0f, //float modelScale; + "default", //char *skinname; + 1.0f, //float shadowScale; + "human_hud", //char *hudname; + ( 1 << S1 )|( 1 << S2 )|( 1 << S3 ), //int stages + { -15, -15, -24 }, //vec3_t mins; + { 15, 15, 32 }, //vec3_t maxs; + { 15, 15, 16 }, //vec3_t crouchmaxs; + { -15, -15, -4 }, //vec3_t deadmins; + { 15, 15, 4 }, //vec3_t deadmaxs; + 0.0f, //float zOffset + 26, 12, //int viewheight, crouchviewheight; + 100, //int health; + 1.0f, //float fallDamage; + 0, //int regenRate; + SCA_TAKESFALLDAMAGE| + SCA_CANUSELADDERS, //int abilities; + WP_NONE, //special-cased in g_client.c //weapon_t startWeapon + 110.0f, //float buildDist; + 90, //int fov; + 0.002f, //float bob; + 1.0f, //float bobCycle; + 100, //int steptime; + 1.0f, //float speed; + 10.0f, //float acceleration; + 1.0f, //float airAcceleration; + 6.0f, //float friction; + 100.0f, //float stopSpeed; + 220.0f, //float jumpMagnitude; + 1.0f, //float knockbackScale; + { PCL_NONE, PCL_NONE, PCL_NONE }, //int children[ 3 ]; + 0, //int cost; + 0 //int value; + }, + { + PCL_HUMAN_BSUIT, //int classnum; + "human_bsuit", //char *classname; + "bsuit", //char *humanname; + "keel", //char *modelname; + 1.0f, //float modelScale; + "default", //char *skinname; + 1.0f, //float shadowScale; + "human_hud", //char *hudname; + ( 1 << S3 ), //int stages + { -15, -15, -38 }, //vec3_t mins; + { 15, 15, 38 }, //vec3_t maxs; + { 15, 15, 38 }, //vec3_t crouchmaxs; + { -15, -15, -4 }, //vec3_t deadmins; + { 15, 15, 4 }, //vec3_t deadmaxs; + -16.0f, //float zOffset + 35, 35, //int viewheight, crouchviewheight; + 100, //int health; + 1.0f, //float fallDamage; + 0, //int regenRate; + SCA_TAKESFALLDAMAGE| + SCA_CANUSELADDERS, //int abilities; + WP_NONE, //special-cased in g_client.c //weapon_t startWeapon + 110.0f, //float buildDist; + 90, //int fov; + 0.002f, //float bob; + 1.0f, //float bobCycle; + 100, //int steptime; + 1.0f, //float speed; + 10.0f, //float acceleration; + 1.0f, //float airAcceleration; + 6.0f, //float friction; + 100.0f, //float stopSpeed; + 270.0f, //float jumpMagnitude; + 1.0f, //float knockbackScale; + { PCL_NONE, PCL_NONE, PCL_NONE }, //int children[ 3 ]; + 0, //int cost; + 0 //int value; + }, +}; + +int bg_numPclasses = sizeof( bg_classList ) / sizeof( bg_classList[ 0 ] ); + +//separate from bg_classList to work around char struct init bug +classAttributeOverrides_t bg_classOverrideList[ PCL_NUM_CLASSES ]; + +/* +============== +BG_FindClassNumForName +============== +*/ +int BG_FindClassNumForName( char *name ) +{ + int i; + + for( i = 0; i < bg_numPclasses; i++ ) + { + if( !Q_stricmp( bg_classList[ i ].className, name ) ) + return bg_classList[ i ].classNum; + } + + //wimp out + return PCL_NONE; +} + +/* +============== +BG_FindNameForClassNum +============== +*/ +char *BG_FindNameForClassNum( int pclass ) +{ + int i; + + for( i = 0; i < bg_numPclasses; i++ ) + { + if( bg_classList[ i ].classNum == pclass ) + return bg_classList[ i ].className; + } + + Com_Printf( S_COLOR_YELLOW "WARNING: fallthrough in BG_FindNameForClassNum\n" ); + //wimp out + return 0; +} + +/* +============== +BG_FindHumanNameForClassNum +============== +*/ +char *BG_FindHumanNameForClassNum( int pclass ) +{ + int i; + + if( bg_classOverrideList[ pclass ].humanName[ 0 ] != 0 ) + return bg_classOverrideList[ pclass ].humanName; + + for( i = 0; i < bg_numPclasses; i++ ) + { + if( bg_classList[ i ].classNum == pclass ) + return bg_classList[ i ].humanName; + } + + Com_Printf( S_COLOR_YELLOW "WARNING: fallthrough in BG_FindHumanNameForClassNum\n" ); + //wimp out + return 0; +} + +/* +============== +BG_FindModelNameForClass +============== +*/ +char *BG_FindModelNameForClass( int pclass ) +{ + int i; + + if( bg_classOverrideList[ pclass ].modelName[ 0 ] != 0 ) + return bg_classOverrideList[ pclass ].modelName; + + for( i = 0; i < bg_numPclasses; i++ ) + { + if( bg_classList[ i ].classNum == pclass ) + return bg_classList[ i ].modelName; + } + + Com_Printf( S_COLOR_YELLOW "WARNING: fallthrough in BG_FindModelNameForClass\n" ); + //note: must return a valid modelName! + return bg_classList[ 0 ].modelName; +} + +/* +============== +BG_FindModelScaleForClass +============== +*/ +float BG_FindModelScaleForClass( int pclass ) +{ + int i; + + if( bg_classOverrideList[ pclass ].modelScale != 0.0f ) + return bg_classOverrideList[ pclass ].modelScale; + + for( i = 0; i < bg_numPclasses; i++ ) + { + if( bg_classList[ i ].classNum == pclass ) + { + return bg_classList[ i ].modelScale; + } + } + + Com_Printf( S_COLOR_YELLOW "WARNING: fallthrough in BG_FindModelScaleForClass( %d )\n", pclass ); + return 1.0f; +} + +/* +============== +BG_FindSkinNameForClass +============== +*/ +char *BG_FindSkinNameForClass( int pclass ) +{ + int i; + + if( bg_classOverrideList[ pclass ].skinName[ 0 ] != 0 ) + return bg_classOverrideList[ pclass ].skinName; + + for( i = 0; i < bg_numPclasses; i++ ) + { + if( bg_classList[ i ].classNum == pclass ) + return bg_classList[ i ].skinName; + } + + Com_Printf( S_COLOR_YELLOW "WARNING: fallthrough in BG_FindSkinNameForClass\n" ); + //note: must return a valid modelName! + return bg_classList[ 0 ].skinName; +} + +/* +============== +BG_FindShadowScaleForClass +============== +*/ +float BG_FindShadowScaleForClass( int pclass ) +{ + int i; + + if( bg_classOverrideList[ pclass ].shadowScale != 0.0f ) + return bg_classOverrideList[ pclass ].shadowScale; + + for( i = 0; i < bg_numPclasses; i++ ) + { + if( bg_classList[ i ].classNum == pclass ) + { + return bg_classList[ i ].shadowScale; + } + } + + Com_Printf( S_COLOR_YELLOW "WARNING: fallthrough in BG_FindShadowScaleForClass( %d )\n", pclass ); + return 1.0f; +} + +/* +============== +BG_FindHudNameForClass +============== +*/ +char *BG_FindHudNameForClass( int pclass ) +{ + int i; + + if( bg_classOverrideList[ pclass ].hudName[ 0 ] != 0 ) + return bg_classOverrideList[ pclass ].hudName; + + for( i = 0; i < bg_numPclasses; i++ ) + { + if( bg_classList[ i ].classNum == pclass ) + return bg_classList[ i ].hudName; + } + + Com_Printf( S_COLOR_YELLOW "WARNING: fallthrough in BG_FindHudNameForClass\n" ); + //note: must return a valid hudName! + return bg_classList[ 0 ].hudName; +} + +/* +============== +BG_FindStagesForClass +============== +*/ +qboolean BG_FindStagesForClass( int pclass, stage_t stage ) +{ + int i; + + for( i = 0; i < bg_numPclasses; i++ ) + { + if( bg_classList[ i ].classNum == pclass ) + { + if( bg_classList[ i ].stages & ( 1 << stage ) ) + return qtrue; + else + return qfalse; + } + } + + Com_Printf( S_COLOR_YELLOW "WARNING: fallthrough in BG_FindStagesForClass\n" ); + return qfalse; +} + +/* +============== +BG_FindBBoxForClass +============== +*/ +void BG_FindBBoxForClass( int pclass, vec3_t mins, vec3_t maxs, vec3_t cmaxs, vec3_t dmins, vec3_t dmaxs ) +{ + int i; + + for( i = 0; i < bg_numPclasses; i++ ) + { + if( bg_classList[ i ].classNum == pclass ) + { + if( mins != NULL ) + { + VectorCopy( bg_classList[ i ].mins, mins ); + + if( VectorLength( bg_classOverrideList[ pclass ].mins ) ) + VectorCopy( bg_classOverrideList[ pclass ].mins, mins ); + } + + if( maxs != NULL ) + { + VectorCopy( bg_classList[ i ].maxs, maxs ); + + if( VectorLength( bg_classOverrideList[ pclass ].maxs ) ) + VectorCopy( bg_classOverrideList[ pclass ].maxs, maxs ); + } + + if( cmaxs != NULL ) + { + VectorCopy( bg_classList[ i ].crouchMaxs, cmaxs ); + + if( VectorLength( bg_classOverrideList[ pclass ].crouchMaxs ) ) + VectorCopy( bg_classOverrideList[ pclass ].crouchMaxs, cmaxs ); + } + + if( dmins != NULL ) + { + VectorCopy( bg_classList[ i ].deadMins, dmins ); + + if( VectorLength( bg_classOverrideList[ pclass ].deadMins ) ) + VectorCopy( bg_classOverrideList[ pclass ].deadMins, dmins ); + } + + if( dmaxs != NULL ) + { + VectorCopy( bg_classList[ i ].deadMaxs, dmaxs ); + + if( VectorLength( bg_classOverrideList[ pclass ].deadMaxs ) ) + VectorCopy( bg_classOverrideList[ pclass ].deadMaxs, dmaxs ); + } + + return; + } + } + + if( mins != NULL ) + VectorCopy( bg_classList[ 0 ].mins, mins ); + + if( maxs != NULL ) + VectorCopy( bg_classList[ 0 ].maxs, maxs ); + + if( cmaxs != NULL ) + VectorCopy( bg_classList[ 0 ].crouchMaxs, cmaxs ); + + if( dmins != NULL ) + VectorCopy( bg_classList[ 0 ].deadMins, dmins ); + + if( dmaxs != NULL ) + VectorCopy( bg_classList[ 0 ].deadMaxs, dmaxs ); +} + +/* +============== +BG_FindZOffsetForClass +============== +*/ +float BG_FindZOffsetForClass( int pclass ) +{ + int i; + + if( bg_classOverrideList[ pclass ].zOffset != 0.0f ) + return bg_classOverrideList[ pclass ].zOffset; + + for( i = 0; i < bg_numPclasses; i++ ) + { + if( bg_classList[ i ].classNum == pclass ) + { + return bg_classList[ i ].zOffset; + } + } + + Com_Printf( S_COLOR_YELLOW "WARNING: fallthrough in BG_FindZOffsetForClass\n" ); + return 0.0f; +} + +/* +============== +BG_FindViewheightForClass +============== +*/ +void BG_FindViewheightForClass( int pclass, int *viewheight, int *cViewheight ) +{ + int i; + int vh = 0; + int cvh = 0; + + for( i = 0; i < bg_numPclasses; i++ ) + { + if( bg_classList[ i ].classNum == pclass ) + { + vh = bg_classList[ i ].viewheight; + cvh = bg_classList[ i ].crouchViewheight; + break; + } + } + + if( bg_classOverrideList[ pclass ].viewheight != 0 ) + vh = bg_classOverrideList[ pclass ].viewheight; + if( bg_classOverrideList[ pclass ].crouchViewheight != 0 ) + cvh = bg_classOverrideList[ pclass ].crouchViewheight; + + + if( vh == 0 ) + vh = bg_classList[ 0 ].viewheight; + if( cvh == 0 ) + cvh = bg_classList[ 0 ].crouchViewheight; + + if( viewheight != NULL ) + *viewheight = vh; + if( cViewheight != NULL ) + *cViewheight = cvh; +} + +/* +============== +BG_FindHealthForClass +============== +*/ +int BG_FindHealthForClass( int pclass ) +{ + int i; + + for( i = 0; i < bg_numPclasses; i++ ) + { + if( bg_classList[ i ].classNum == pclass ) + { + return bg_classList[ i ].health; + } + } + + Com_Printf( S_COLOR_YELLOW "WARNING: fallthrough in BG_FindHealthForClass\n" ); + return 100; +} + +/* +============== +BG_FindFallDamageForClass +============== +*/ +float BG_FindFallDamageForClass( int pclass ) +{ + int i; + + for( i = 0; i < bg_numPclasses; i++ ) + { + if( bg_classList[ i ].classNum == pclass ) + { + return bg_classList[ i ].fallDamage; + } + } + + Com_Printf( S_COLOR_YELLOW "WARNING: fallthrough in BG_FindFallDamageForClass\n" ); + return 100; +} + +/* +============== +BG_FindRegenRateForClass +============== +*/ +int BG_FindRegenRateForClass( int pclass ) +{ + int i; + + for( i = 0; i < bg_numPclasses; i++ ) + { + if( bg_classList[ i ].classNum == pclass ) + { + return bg_classList[ i ].regenRate; + } + } + + Com_Printf( S_COLOR_YELLOW "WARNING: fallthrough in BG_FindRegenRateForClass\n" ); + return 0; +} + +/* +============== +BG_FindFovForClass +============== +*/ +int BG_FindFovForClass( int pclass ) +{ + int i; + + for( i = 0; i < bg_numPclasses; i++ ) + { + if( bg_classList[ i ].classNum == pclass ) + { + return bg_classList[ i ].fov; + } + } + + Com_Printf( S_COLOR_YELLOW "WARNING: fallthrough in BG_FindFovForClass\n" ); + return 90; +} + +/* +============== +BG_FindBobForClass +============== +*/ +float BG_FindBobForClass( int pclass ) +{ + int i; + + for( i = 0; i < bg_numPclasses; i++ ) + { + if( bg_classList[ i ].classNum == pclass ) + { + return bg_classList[ i ].bob; + } + } + + Com_Printf( S_COLOR_YELLOW "WARNING: fallthrough in BG_FindBobForClass\n" ); + return 0.002; +} + +/* +============== +BG_FindBobCycleForClass +============== +*/ +float BG_FindBobCycleForClass( int pclass ) +{ + int i; + + for( i = 0; i < bg_numPclasses; i++ ) + { + if( bg_classList[ i ].classNum == pclass ) + { + return bg_classList[ i ].bobCycle; + } + } + + Com_Printf( S_COLOR_YELLOW "WARNING: fallthrough in BG_FindBobCycleForClass\n" ); + return 1.0f; +} + +/* +============== +BG_FindSpeedForClass +============== +*/ +float BG_FindSpeedForClass( int pclass ) +{ + int i; + + for( i = 0; i < bg_numPclasses; i++ ) + { + if( bg_classList[ i ].classNum == pclass ) + { + return bg_classList[ i ].speed; + } + } + + Com_Printf( S_COLOR_YELLOW "WARNING: fallthrough in BG_FindSpeedForClass\n" ); + return 1.0f; +} + +/* +============== +BG_FindAccelerationForClass +============== +*/ +float BG_FindAccelerationForClass( int pclass ) +{ + int i; + + for( i = 0; i < bg_numPclasses; i++ ) + { + if( bg_classList[ i ].classNum == pclass ) + { + return bg_classList[ i ].acceleration; + } + } + + Com_Printf( S_COLOR_YELLOW "WARNING: fallthrough in BG_FindAccelerationForClass\n" ); + return 10.0f; +} + +/* +============== +BG_FindAirAccelerationForClass +============== +*/ +float BG_FindAirAccelerationForClass( int pclass ) +{ + int i; + + for( i = 0; i < bg_numPclasses; i++ ) + { + if( bg_classList[ i ].classNum == pclass ) + { + return bg_classList[ i ].airAcceleration; + } + } + + Com_Printf( S_COLOR_YELLOW "WARNING: fallthrough in BG_FindAirAccelerationForClass\n" ); + return 1.0f; +} + +/* +============== +BG_FindFrictionForClass +============== +*/ +float BG_FindFrictionForClass( int pclass ) +{ + int i; + + for( i = 0; i < bg_numPclasses; i++ ) + { + if( bg_classList[ i ].classNum == pclass ) + { + return bg_classList[ i ].friction; + } + } + + Com_Printf( S_COLOR_YELLOW "WARNING: fallthrough in BG_FindFrictionForClass\n" ); + return 6.0f; +} + +/* +============== +BG_FindStopSpeedForClass +============== +*/ +float BG_FindStopSpeedForClass( int pclass ) +{ + int i; + + for( i = 0; i < bg_numPclasses; i++ ) + { + if( bg_classList[ i ].classNum == pclass ) + { + return bg_classList[ i ].stopSpeed; + } + } + + Com_Printf( S_COLOR_YELLOW "WARNING: fallthrough in BG_FindStopSpeedForClass\n" ); + return 100.0f; +} + +/* +============== +BG_FindJumpMagnitudeForClass +============== +*/ +float BG_FindJumpMagnitudeForClass( int pclass ) +{ + int i; + + for( i = 0; i < bg_numPclasses; i++ ) + { + if( bg_classList[ i ].classNum == pclass ) + { + return bg_classList[ i ].jumpMagnitude; + } + } + + Com_Printf( S_COLOR_YELLOW "WARNING: fallthrough in BG_FindJumpMagnitudeForClass\n" ); + return 270.0f; +} + +/* +============== +BG_FindKnockbackScaleForClass +============== +*/ +float BG_FindKnockbackScaleForClass( int pclass ) +{ + int i; + + for( i = 0; i < bg_numPclasses; i++ ) + { + if( bg_classList[ i ].classNum == pclass ) + { + return bg_classList[ i ].knockbackScale; + } + } + + Com_Printf( S_COLOR_YELLOW "WARNING: fallthrough in BG_FindKnockbackScaleForClass\n" ); + return 1.0f; +} + +/* +============== +BG_FindSteptimeForClass +============== +*/ +int BG_FindSteptimeForClass( int pclass ) +{ + int i; + + for( i = 0; i < bg_numPclasses; i++ ) + { + if( bg_classList[ i ].classNum == pclass ) + { + return bg_classList[ i ].steptime; + } + } + + Com_Printf( S_COLOR_YELLOW "WARNING: fallthrough in BG_FindSteptimeForClass\n" ); + return 200; +} + +/* +============== +BG_ClassHasAbility +============== +*/ +qboolean BG_ClassHasAbility( int pclass, int ability ) +{ + int i; + + for( i = 0; i < bg_numPclasses; i++ ) + { + if( bg_classList[ i ].classNum == pclass ) + { + return ( bg_classList[ i ].abilities & ability ); + } + } + + return qfalse; +} + +/* +============== +BG_FindStartWeaponForClass +============== +*/ +weapon_t BG_FindStartWeaponForClass( int pclass ) +{ + int i; + + for( i = 0; i < bg_numPclasses; i++ ) + { + if( bg_classList[ i ].classNum == pclass ) + { + return bg_classList[ i ].startWeapon; + } + } + + Com_Printf( S_COLOR_YELLOW "WARNING: fallthrough in BG_FindStartWeaponForClass\n" ); + return WP_NONE; +} + +/* +============== +BG_FindBuildDistForClass +============== +*/ +float BG_FindBuildDistForClass( int pclass ) +{ + int i; + + for( i = 0; i < bg_numPclasses; i++ ) + { + if( bg_classList[ i ].classNum == pclass ) + { + return bg_classList[ i ].buildDist; + } + } + + Com_Printf( S_COLOR_YELLOW "WARNING: fallthrough in BG_FindBuildDistForClass\n" ); + return 0.0f; +} + +/* +============== +BG_ClassCanEvolveFromTo +============== +*/ +int BG_ClassCanEvolveFromTo( int fclass, int tclass, int credits, int num ) +{ + int i, j, cost; + + cost = BG_FindCostOfClass( tclass ); + + //base case + if( credits < cost ) + return -1; + + if( fclass == PCL_NONE || tclass == PCL_NONE ) + return -1; + + for( i = 0; i < bg_numPclasses; i++ ) + { + if( bg_classList[ i ].classNum == fclass ) + { + for( j = 0; j < 3; j++ ) + if( bg_classList[ i ].children[ j ] == tclass ) + return num + cost; + + for( j = 0; j < 3; j++ ) + { + int sub; + + cost = BG_FindCostOfClass( bg_classList[ i ].children[ j ] ); + sub = BG_ClassCanEvolveFromTo( bg_classList[ i ].children[ j ], + tclass, credits - cost, num + cost ); + if( sub >= 0 ) + return sub; + } + + return -1; //may as well return by this point + } + } + + return -1; +} + +/* +============== +BG_FindValueOfClass +============== +*/ +int BG_FindValueOfClass( int pclass ) +{ + int i; + + for( i = 0; i < bg_numPclasses; i++ ) + { + if( bg_classList[ i ].classNum == pclass ) + { + return bg_classList[ i ].value; + } + } + + Com_Printf( S_COLOR_YELLOW "WARNING: fallthrough in BG_FindValueOfClass\n" ); + return 0; +} + +/* +============== +BG_FindCostOfClass +============== +*/ +int BG_FindCostOfClass( int pclass ) +{ + int i; + + for( i = 0; i < bg_numPclasses; i++ ) + { + if( bg_classList[ i ].classNum == pclass ) + { + return bg_classList[ i ].cost; + } + } + + Com_Printf( S_COLOR_YELLOW "WARNING: fallthrough in BG_FindCostOfClass\n" ); + return 0; +} + +/* +============== +BG_FindOverrideForClass +============== +*/ +static classAttributeOverrides_t *BG_FindOverrideForClass( int pclass ) +{ + return &bg_classOverrideList[ pclass ]; +} + +/* +====================== +BG_ParseClassFile + +Parses a configuration file describing a class +====================== +*/ +static qboolean BG_ParseClassFile( const char *filename, classAttributeOverrides_t *cao ) +{ + char *text_p; + int i; + int len; + char *token; + char text[ 20000 ]; + fileHandle_t f; + float scale = 0.0f; + + + // load the file + len = trap_FS_FOpenFile( filename, &f, FS_READ ); + if( len < 0 ) + return qfalse; + + if( len == 0 || len >= sizeof( text ) - 1 ) + { + trap_FS_FCloseFile( f ); + Com_Printf( S_COLOR_RED "ERROR: Class file %s is %s\n", filename, + len == 0 ? "empty" : "too long" ); + return qfalse; + } + + trap_FS_Read( text, len, f ); + text[ len ] = 0; + trap_FS_FCloseFile( f ); + + // parse the text + text_p = text; + + // read optional parameters + while( 1 ) + { + token = COM_Parse( &text_p ); + + if( !token ) + break; + + if( !Q_stricmp( token, "" ) ) + break; + + if( !Q_stricmp( token, "model" ) ) + { + token = COM_Parse( &text_p ); + if( !token ) + break; + + Q_strncpyz( cao->modelName, token, sizeof( cao->modelName ) ); + + continue; + } + else if( !Q_stricmp( token, "skin" ) ) + { + token = COM_Parse( &text_p ); + if( !token ) + break; + + Q_strncpyz( cao->skinName, token, sizeof( cao->skinName ) ); + + continue; + } + else if( !Q_stricmp( token, "hud" ) ) + { + token = COM_Parse( &text_p ); + if( !token ) + break; + + Q_strncpyz( cao->hudName, token, sizeof( cao->hudName ) ); + + continue; + } + else if( !Q_stricmp( token, "modelScale" ) ) + { + token = COM_Parse( &text_p ); + if( !token ) + break; + + scale = atof( token ); + + if( scale < 0.0f ) + scale = 0.0f; + + cao->modelScale = scale; + + continue; + } + else if( !Q_stricmp( token, "shadowScale" ) ) + { + token = COM_Parse( &text_p ); + if( !token ) + break; + + scale = atof( token ); + + if( scale < 0.0f ) + scale = 0.0f; + + cao->shadowScale = scale; + + continue; + } + else if( !Q_stricmp( token, "mins" ) ) + { + for( i = 0; i <= 2; i++ ) + { + token = COM_Parse( &text_p ); + if( !token ) + break; + + cao->mins[ i ] = atof( token ); + } + + continue; + } + else if( !Q_stricmp( token, "maxs" ) ) + { + for( i = 0; i <= 2; i++ ) + { + token = COM_Parse( &text_p ); + if( !token ) + break; + + cao->maxs[ i ] = atof( token ); + } + + continue; + } + else if( !Q_stricmp( token, "deadMins" ) ) + { + for( i = 0; i <= 2; i++ ) + { + token = COM_Parse( &text_p ); + if( !token ) + break; + + cao->deadMins[ i ] = atof( token ); + } + + continue; + } + else if( !Q_stricmp( token, "deadMaxs" ) ) + { + for( i = 0; i <= 2; i++ ) + { + token = COM_Parse( &text_p ); + if( !token ) + break; + + cao->deadMaxs[ i ] = atof( token ); + } + + continue; + } + else if( !Q_stricmp( token, "crouchMaxs" ) ) + { + for( i = 0; i <= 2; i++ ) + { + token = COM_Parse( &text_p ); + if( !token ) + break; + + cao->crouchMaxs[ i ] = atof( token ); + } + + continue; + } + else if( !Q_stricmp( token, "viewheight" ) ) + { + token = COM_Parse( &text_p ); + cao->viewheight = atoi( token ); + continue; + } + else if( !Q_stricmp( token, "crouchViewheight" ) ) + { + token = COM_Parse( &text_p ); + cao->crouchViewheight = atoi( token ); + continue; + } + else if( !Q_stricmp( token, "zOffset" ) ) + { + float offset; + + token = COM_Parse( &text_p ); + if( !token ) + break; + + offset = atof( token ); + + cao->zOffset = offset; + + continue; + } + else if( !Q_stricmp( token, "name" ) ) + { + token = COM_Parse( &text_p ); + if( !token ) + break; + + Q_strncpyz( cao->humanName, token, sizeof( cao->humanName ) ); + + continue; + } + + + Com_Printf( S_COLOR_RED "ERROR: unknown token '%s'\n", token ); + return qfalse; + } + + return qtrue; +} + +/* +=============== +BG_InitClassOverrides + +Set any overrides specfied by file +=============== +*/ +void BG_InitClassOverrides( void ) +{ + int i; + classAttributeOverrides_t *cao; + + for( i = PCL_NONE + 1; i < PCL_NUM_CLASSES; i++ ) + { + cao = BG_FindOverrideForClass( i ); + + BG_ParseClassFile( va( "overrides/classes/%s.cfg", BG_FindNameForClassNum( i ) ), cao ); + } +} + +//////////////////////////////////////////////////////////////////////////////// + +weaponAttributes_t bg_weapons[ ] = +{ + { + WP_BLASTER, //int weaponNum; + 0, //int price; + ( 1 << S1 )|( 1 << S2 )|( 1 << S3 ), //int stages + 0, //int slots; + "blaster", //char *weaponName; + "Blaster", //char *weaponHumanName; + 0, //int maxAmmo; + 0, //int maxClips; + qtrue, //int infiniteAmmo; + qfalse, //int usesEnergy; + BLASTER_REPEAT, //int repeatRate1; + 0, //int repeatRate2; + 0, //int repeatRate3; + 0, //int reloadTime; + BLASTER_K_SCALE, //float knockbackScale; + qfalse, //qboolean hasAltMode; + qfalse, //qboolean hasThirdMode; + qfalse, //qboolean canZoom; + 90.0f, //float zoomFov; + qfalse, //qboolean purchasable; + qtrue, //qboolean longRanged; + 0, //int buildDelay; + WUT_HUMANS //WUTeam_t team; + }, + { + WP_MACHINEGUN, //int weaponNum; + RIFLE_PRICE, //int price; + ( 1 << S1 )|( 1 << S2 )|( 1 << S3 ), //int stages + SLOT_WEAPON, //int slots; + "rifle", //char *weaponName; + "Rifle", //char *weaponHumanName; + RIFLE_CLIPSIZE, //int maxAmmo; + RIFLE_MAXCLIPS, //int maxClips; + qfalse, //int infiniteAmmo; + qfalse, //int usesEnergy; + RIFLE_REPEAT, //int repeatRate1; + 0, //int repeatRate2; + 0, //int repeatRate3; + RIFLE_RELOAD, //int reloadTime; + RIFLE_K_SCALE, //float knockbackScale; + qfalse, //qboolean hasAltMode; + qfalse, //qboolean hasThirdMode; + qfalse, //qboolean canZoom; + 90.0f, //float zoomFov; + qtrue, //qboolean purchasable; + qtrue, //qboolean longRanged; + 0, //int buildDelay; + WUT_HUMANS //WUTeam_t team; + }, + { + WP_SHOTGUN, //int weaponNum; + SHOTGUN_PRICE, //int price; + ( 1 << S1 )|( 1 << S2 )|( 1 << S3 ), //int stages + SLOT_WEAPON, //int slots; + "shotgun", //char *weaponName; + "Shotgun", //char *weaponHumanName; + SHOTGUN_SHELLS, //int maxAmmo; + SHOTGUN_MAXCLIPS, //int maxClips; + qfalse, //int infiniteAmmo; + qfalse, //int usesEnergy; + SHOTGUN_REPEAT, //int repeatRate1; + 0, //int repeatRate2; + 0, //int repeatRate3; + SHOTGUN_RELOAD, //int reloadTime; + SHOTGUN_K_SCALE, //float knockbackScale; + qfalse, //qboolean hasAltMode; + qfalse, //qboolean hasThirdMode; + qfalse, //qboolean canZoom; + 90.0f, //float zoomFov; + qtrue, //qboolean purchasable; + qtrue, //qboolean longRanged; + 0, //int buildDelay; + WUT_HUMANS //WUTeam_t team; + }, + { + WP_FLAMER, //int weaponNum; + FLAMER_PRICE, //int price; + ( 1 << S2 )|( 1 << S3 ), //int stages + SLOT_WEAPON, //int slots; + "flamer", //char *weaponName; + "Flame Thrower", //char *weaponHumanName; + FLAMER_GAS, //int maxAmmo; + 0, //int maxClips; + qfalse, //int infiniteAmmo; + qfalse, //int usesEnergy; + FLAMER_REPEAT, //int repeatRate1; + 0, //int repeatRate2; + 0, //int repeatRate3; + 0, //int reloadTime; + FLAMER_K_SCALE, //float knockbackScale; + qfalse, //qboolean hasAltMode; + qfalse, //qboolean hasThirdMode; + qfalse, //qboolean canZoom; + 90.0f, //float zoomFov; + qtrue, //qboolean purchasable; + qtrue, //qboolean longRanged; + 0, //int buildDelay; + WUT_HUMANS //WUTeam_t team; + }, + { + WP_CHAINGUN, //int weaponNum; + CHAINGUN_PRICE, //int price; + ( 1 << S1 )|( 1 << S2 )|( 1 << S3 ), //int stages + SLOT_WEAPON, //int slots; + "chaingun", //char *weaponName; + "Chaingun", //char *weaponHumanName; + CHAINGUN_BULLETS, //int maxAmmo; + 0, //int maxClips; + qfalse, //int infiniteAmmo; + qfalse, //int usesEnergy; + CHAINGUN_REPEAT, //int repeatRate1; + 0, //int repeatRate2; + 0, //int repeatRate3; + 0, //int reloadTime; + CHAINGUN_K_SCALE, //float knockbackScale; + qfalse, //qboolean hasAltMode; + qfalse, //qboolean hasThirdMode; + qfalse, //qboolean canZoom; + 90.0f, //float zoomFov; + qtrue, //qboolean purchasable; + qtrue, //qboolean longRanged; + 0, //int buildDelay; + WUT_HUMANS //WUTeam_t team; + }, + { + WP_MASS_DRIVER, //int weaponNum; + MDRIVER_PRICE, //int price; + ( 1 << S1 )|( 1 << S2 )|( 1 << S3 ), //int stages + SLOT_WEAPON, //int slots; + "mdriver", //char *weaponName; + "Mass Driver", //char *weaponHumanName; + MDRIVER_CLIPSIZE, //int maxAmmo; + MDRIVER_MAXCLIPS, //int maxClips; + qfalse, //int infiniteAmmo; + qtrue, //int usesEnergy; + MDRIVER_REPEAT, //int repeatRate1; + 0, //int repeatRate2; + 0, //int repeatRate3; + MDRIVER_RELOAD, //int reloadTime; + MDRIVER_K_SCALE, //float knockbackScale; + qfalse, //qboolean hasAltMode; + qfalse, //qboolean hasThirdMode; + qtrue, //qboolean canZoom; + 20.0f, //float zoomFov; + qtrue, //qboolean purchasable; + qtrue, //qboolean longRanged; + 0, //int buildDelay; + WUT_HUMANS //WUTeam_t team; + }, + { + WP_PULSE_RIFLE, //int weaponNum; + PRIFLE_PRICE, //int price; + ( 1 << S2 )|( 1 << S3 ), //int stages + SLOT_WEAPON, //int slots; + "prifle", //char *weaponName; + "Pulse Rifle", //char *weaponHumanName; + PRIFLE_CLIPS, //int maxAmmo; + PRIFLE_MAXCLIPS, //int maxClips; + qfalse, //int infiniteAmmo; + qtrue, //int usesEnergy; + PRIFLE_REPEAT, //int repeatRate1; + 0, //int repeatRate2; + 0, //int repeatRate3; + PRIFLE_RELOAD, //int reloadTime; + PRIFLE_K_SCALE, //float knockbackScale; + qfalse, //qboolean hasAltMode; + qfalse, //qboolean hasThirdMode; + qfalse, //qboolean canZoom; + 90.0f, //float zoomFov; + qtrue, //qboolean purchasable; + qtrue, //qboolean longRanged; + 0, //int buildDelay; + WUT_HUMANS //WUTeam_t team; + }, + { + WP_LUCIFER_CANNON, //int weaponNum; + LCANNON_PRICE, //int price; + ( 1 << S3 ), //int stages + SLOT_WEAPON, //int slots; + "lcannon", //char *weaponName; + "Lucifer Cannon", //char *weaponHumanName; + LCANNON_AMMO, //int maxAmmo; + 0, //int maxClips; + qfalse, //int infiniteAmmo; + qtrue, //int usesEnergy; + LCANNON_REPEAT, //int repeatRate1; + LCANNON_CHARGEREPEAT, //int repeatRate2; + 0, //int repeatRate3; + LCANNON_RELOAD, //int reloadTime; + LCANNON_K_SCALE, //float knockbackScale; + qtrue, //qboolean hasAltMode; + qfalse, //qboolean hasThirdMode; + qfalse, //qboolean canZoom; + 90.0f, //float zoomFov; + qtrue, //qboolean purchasable; + qtrue, //qboolean longRanged; + 0, //int buildDelay; + WUT_HUMANS //WUTeam_t team; + }, + { + WP_LAS_GUN, //int weaponNum; + LASGUN_PRICE, //int price; + ( 1 << S1 )|( 1 << S2 )|( 1 << S3 ), //int stages + SLOT_WEAPON, //int slots; + "lgun", //char *weaponName; + "Las Gun", //char *weaponHumanName; + LASGUN_AMMO, //int maxAmmo; + 0, //int maxClips; + qfalse, //int infiniteAmmo; + qtrue, //int usesEnergy; + LASGUN_REPEAT, //int repeatRate1; + 0, //int repeatRate2; + 0, //int repeatRate3; + LASGUN_RELOAD, //int reloadTime; + LASGUN_K_SCALE, //float knockbackScale; + qfalse, //qboolean hasAltMode; + qfalse, //qboolean hasThirdMode; + qfalse, //qboolean canZoom; + 90.0f, //float zoomFov; + qtrue, //qboolean purchasable; + qtrue, //qboolean longRanged; + 0, //int buildDelay; + WUT_HUMANS //WUTeam_t team; + }, + { + WP_PAIN_SAW, //int weaponNum; + PAINSAW_PRICE, //int price; + ( 1 << S1 )|( 1 << S2 )|( 1 << S3 ), //int stages + SLOT_WEAPON, //int slots; + "psaw", //char *weaponName; + "Pain Saw", //char *weaponHumanName; + 0, //int maxAmmo; + 0, //int maxClips; + qtrue, //int infiniteAmmo; + qfalse, //int usesEnergy; + PAINSAW_REPEAT, //int repeatRate1; + 0, //int repeatRate2; + 0, //int repeatRate3; + 0, //int reloadTime; + PAINSAW_K_SCALE, //float knockbackScale; + qfalse, //qboolean hasAltMode; + qfalse, //qboolean hasThirdMode; + qfalse, //qboolean canZoom; + 90.0f, //float zoomFov; + qtrue, //qboolean purchasable; + qfalse, //qboolean longRanged; + 0, //int buildDelay; + WUT_HUMANS //WUTeam_t team; + }, + { + WP_GRENADE, //int weaponNum; + GRENADE_PRICE, //int price; + ( 1 << S2 )|( 1 << S3 ), //int stages + SLOT_NONE, //int slots; + "grenade", //char *weaponName; + "Grenade", //char *weaponHumanName; + 1, //int maxAmmo; + 0, //int maxClips; + qfalse, //int infiniteAmmo; + qfalse, //int usesEnergy; + GRENADE_REPEAT, //int repeatRate1; + 0, //int repeatRate2; + 0, //int repeatRate3; + 0, //int reloadTime; + GRENADE_K_SCALE, //float knockbackScale; + qfalse, //qboolean hasAltMode; + qfalse, //qboolean hasThirdMode; + qfalse, //qboolean canZoom; + 90.0f, //float zoomFov; + qfalse, //qboolean purchasable; + qfalse, //qboolean longRanged; + 0, //int buildDelay; + WUT_HUMANS //WUTeam_t team; + }, + { + WP_HBUILD, //int weaponNum; + HBUILD_PRICE, //int price; + ( 1 << S1 )|( 1 << S2 )|( 1 << S3 ), //int stages + SLOT_WEAPON, //int slots; + "ckit", //char *weaponName; + "Construction Kit", //char *weaponHumanName; + 0, //int maxAmmo; + 0, //int maxClips; + qtrue, //int infiniteAmmo; + qfalse, //int usesEnergy; + HBUILD_REPEAT, //int repeatRate1; + HBUILD_REPEAT, //int repeatRate2; + 0, //int repeatRate3; + 0, //int reloadTime; + 0.0f, //float knockbackScale; + qtrue, //qboolean hasAltMode; + qfalse, //qboolean hasThirdMode; + qfalse, //qboolean canZoom; + 90.0f, //float zoomFov; + qtrue, //qboolean purchasable; + qfalse, //qboolean longRanged; + HBUILD_DELAY, //int buildDelay; + WUT_HUMANS //WUTeam_t team; + }, + { + WP_HBUILD2, //int weaponNum; + HBUILD2_PRICE, //int price; + ( 1 << S2 )|( 1 << S3 ), //int stages + SLOT_WEAPON, //int slots; + "ackit", //char *weaponName; + "Adv Construction Kit",//char *weaponHumanName; + 0, //int maxAmmo; + 0, //int maxClips; + qtrue, //int infiniteAmmo; + qfalse, //int usesEnergy; + HBUILD2_REPEAT, //int repeatRate1; + HBUILD2_REPEAT, //int repeatRate2; + 0, //int repeatRate3; + 0, //int reloadTime; + 0.0f, //float knockbackScale; + qtrue, //qboolean hasAltMode; + qfalse, //qboolean hasThirdMode; + qfalse, //qboolean canZoom; + 90.0f, //float zoomFov; + qtrue, //qboolean purchasable; + qfalse, //qboolean longRanged; + HBUILD2_DELAY, //int buildDelay; + WUT_HUMANS //WUTeam_t team; + }, + { + WP_ABUILD, //int weaponNum; + 0, //int price; + ( 1 << S1 )|( 1 << S2 )|( 1 << S3 ), //int stages + SLOT_WEAPON, //int slots; + "abuild", //char *weaponName; + "Alien build weapon", //char *weaponHumanName; + 0, //int maxAmmo; + 0, //int maxClips; + qtrue, //int infiniteAmmo; + qfalse, //int usesEnergy; + ABUILDER_BUILD_REPEAT,//int repeatRate1; + ABUILDER_BUILD_REPEAT,//int repeatRate2; + 0, //int repeatRate3; + 0, //int reloadTime; + 0.0f, //float knockbackScale; + qtrue, //qboolean hasAltMode; + qfalse, //qboolean hasThirdMode; + qfalse, //qboolean canZoom; + 90.0f, //float zoomFov; + qtrue, //qboolean purchasable; + qfalse, //qboolean longRanged; + ABUILDER_BASE_DELAY, //int buildDelay; + WUT_ALIENS //WUTeam_t team; + }, + { + WP_ABUILD2, //int weaponNum; + 0, //int price; + ( 1 << S1 )|( 1 << S2 )|( 1 << S3 ), //int stages + SLOT_WEAPON, //int slots; + "abuildupg", //char *weaponName; + "Alien build weapon2",//char *weaponHumanName; + 0, //int maxAmmo; + 0, //int maxClips; + qtrue, //int infiniteAmmo; + qfalse, //int usesEnergy; + ABUILDER_BUILD_REPEAT,//int repeatRate1; + ABUILDER_CLAW_REPEAT, //int repeatRate2; + ABUILDER_BLOB_REPEAT, //int repeatRate3; + 0, //int reloadTime; + ABUILDER_CLAW_K_SCALE,//float knockbackScale; + qtrue, //qboolean hasAltMode; + qtrue, //qboolean hasThirdMode; + qfalse, //qboolean canZoom; + 90.0f, //float zoomFov; + qtrue, //qboolean purchasable; + qfalse, //qboolean longRanged; + ABUILDER_ADV_DELAY, //int buildDelay; + WUT_ALIENS //WUTeam_t team; + }, + { + WP_ALEVEL0, //int weaponNum; + 0, //int price; + ( 1 << S1 )|( 1 << S2 )|( 1 << S3 ), //int stages + SLOT_WEAPON, //int slots; + "level0", //char *weaponName; + "Bite", //char *weaponHumanName; + 0, //int maxAmmo; + 0, //int maxClips; + qtrue, //int infiniteAmmo; + qfalse, //int usesEnergy; + LEVEL0_BITE_REPEAT, //int repeatRate1; + 0, //int repeatRate2; + 0, //int repeatRate3; + 0, //int reloadTime; + LEVEL0_BITE_K_SCALE, //float knockbackScale; + qfalse, //qboolean hasAltMode; + qfalse, //qboolean hasThirdMode; + qfalse, //qboolean canZoom; + 90.0f, //float zoomFov; + qfalse, //qboolean purchasable; + qfalse, //qboolean longRanged; + 0, //int buildDelay; + WUT_ALIENS //WUTeam_t team; + }, + { + WP_ALEVEL1, //int weaponNum; + 0, //int price; + ( 1 << S1 )|( 1 << S2 )|( 1 << S3 ), //int stages + SLOT_WEAPON, //int slots; + "level1", //char *weaponName; + "Claws", //char *weaponHumanName; + 0, //int maxAmmo; + 0, //int maxClips; + qtrue, //int infiniteAmmo; + qfalse, //int usesEnergy; + LEVEL1_CLAW_REPEAT, //int repeatRate1; + 0, //int repeatRate2; + 0, //int repeatRate3; + 0, //int reloadTime; + LEVEL1_CLAW_K_SCALE, //float knockbackScale; + qfalse, //qboolean hasAltMode; + qfalse, //qboolean hasThirdMode; + qfalse, //qboolean canZoom; + 90.0f, //float zoomFov; + qfalse, //qboolean purchasable; + qfalse, //qboolean longRanged; + 0, //int buildDelay; + WUT_ALIENS //WUTeam_t team; + }, + { + WP_ALEVEL1_UPG, //int weaponNum; + 0, //int price; + ( 1 << S1 )|( 1 << S2 )|( 1 << S3 ), //int stages + SLOT_WEAPON, //int slots; + "level1upg", //char *weaponName; + "Claws Upgrade", //char *weaponHumanName; + 0, //int maxAmmo; + 0, //int maxClips; + qtrue, //int infiniteAmmo; + qfalse, //int usesEnergy; + LEVEL1_CLAW_U_REPEAT, //int repeatRate1; + LEVEL1_PCLOUD_REPEAT, //int repeatRate2; + 0, //int repeatRate3; + 0, //int reloadTime; + LEVEL1_CLAW_U_K_SCALE,//float knockbackScale; + qtrue, //qboolean hasAltMode; + qfalse, //qboolean hasThirdMode; + qfalse, //qboolean canZoom; + 90.0f, //float zoomFov; + qfalse, //qboolean purchasable; + qtrue, //qboolean longRanged; + 0, //int buildDelay; + WUT_ALIENS //WUTeam_t team; + }, + { + WP_ALEVEL2, //int weaponNum; + 0, //int price; + ( 1 << S1 )|( 1 << S2 )|( 1 << S3 ), //int stages + SLOT_WEAPON, //int slots; + "level2", //char *weaponName; + "Bite", //char *weaponHumanName; + 0, //int maxAmmo; + 0, //int maxClips; + qtrue, //int infiniteAmmo; + qfalse, //int usesEnergy; + LEVEL2_CLAW_REPEAT, //int repeatRate1; + 0, //int repeatRate2; + 0, //int repeatRate3; + 0, //int reloadTime; + LEVEL2_CLAW_K_SCALE, //float knockbackScale; + qfalse, //qboolean hasAltMode; + qfalse, //qboolean hasThirdMode; + qfalse, //qboolean canZoom; + 90.0f, //float zoomFov; + qfalse, //qboolean purchasable; + qfalse, //qboolean longRanged; + 0, //int buildDelay; + WUT_ALIENS //WUTeam_t team; + }, + { + WP_ALEVEL2_UPG, //int weaponNum; + 0, //int price; + ( 1 << S1 )|( 1 << S2 )|( 1 << S3 ), //int stages + SLOT_WEAPON, //int slots; + "level2upg", //char *weaponName; + "Zap", //char *weaponHumanName; + 0, //int maxAmmo; + 0, //int maxClips; + qtrue, //int infiniteAmmo; + qfalse, //int usesEnergy; + LEVEL2_CLAW_U_REPEAT, //int repeatRate1; + LEVEL2_AREAZAP_REPEAT,//int repeatRate2; + 0, //int repeatRate3; + 0, //int reloadTime; + LEVEL2_CLAW_U_K_SCALE,//float knockbackScale; + qtrue, //qboolean hasAltMode; + qfalse, //qboolean hasThirdMode; + qfalse, //qboolean canZoom; + 90.0f, //float zoomFov; + qfalse, //qboolean purchasable; + qfalse, //qboolean longRanged; + 0, //int buildDelay; + WUT_ALIENS //WUTeam_t team; + }, + { + WP_ALEVEL3, //int weaponNum; + 0, //int price; + ( 1 << S1 )|( 1 << S2 )|( 1 << S3 ), //int stages + SLOT_WEAPON, //int slots; + "level3", //char *weaponName; + "Pounce", //char *weaponHumanName; + 0, //int maxAmmo; + 0, //int maxClips; + qtrue, //int infiniteAmmo; + qfalse, //int usesEnergy; + LEVEL3_CLAW_REPEAT, //int repeatRate1; + 0, //int repeatRate2; + 0, //int repeatRate3; + 0, //int reloadTime; + LEVEL3_CLAW_K_SCALE, //float knockbackScale; + qfalse, //qboolean hasAltMode; + qfalse, //qboolean hasThirdMode; + qfalse, //qboolean canZoom; + 90.0f, //float zoomFov; + qfalse, //qboolean purchasable; + qfalse, //qboolean longRanged; + 0, //int buildDelay; + WUT_ALIENS //WUTeam_t team; + }, + { + WP_ALEVEL3_UPG, //int weaponNum; + 0, //int price; + ( 1 << S1 )|( 1 << S2 )|( 1 << S3 ), //int stages + SLOT_WEAPON, //int slots; + "level3upg", //char *weaponName; + "Pounce (upgrade)", //char *weaponHumanName; + 3, //int maxAmmo; + 0, //int maxClips; + qtrue, //int infiniteAmmo; + qfalse, //int usesEnergy; + LEVEL3_CLAW_U_REPEAT, //int repeatRate1; + 0, //int repeatRate2; + LEVEL3_BOUNCEBALL_REPEAT,//int repeatRate3; + 0, //int reloadTime; + LEVEL3_CLAW_U_K_SCALE,//float knockbackScale; + qfalse, //qboolean hasAltMode; + qtrue, //qboolean hasThirdMode; + qfalse, //qboolean canZoom; + 90.0f, //float zoomFov; + qfalse, //qboolean purchasable; + qtrue, //qboolean longRanged; + 0, //int buildDelay; + WUT_ALIENS //WUTeam_t team; + }, + { + WP_ALEVEL4, //int weaponNum; + 0, //int price; + ( 1 << S1 )|( 1 << S2 )|( 1 << S3 ), //int stages + SLOT_WEAPON, //int slots; + "level4", //char *weaponName; + "Charge", //char *weaponHumanName; + 0, //int maxAmmo; + 0, //int maxClips; + qtrue, //int infiniteAmmo; + qfalse, //int usesEnergy; + LEVEL4_CLAW_REPEAT, //int repeatRate1; + 0, //int repeatRate2; + 0, //int repeatRate3; + 0, //int reloadTime; + LEVEL4_CLAW_K_SCALE, //float knockbackScale; + qfalse, //qboolean hasAltMode; + qfalse, //qboolean hasThirdMode; + qfalse, //qboolean canZoom; + 90.0f, //float zoomFov; + qfalse, //qboolean purchasable; + qfalse, //qboolean longRanged; + 0, //int buildDelay; + WUT_ALIENS //WUTeam_t team; + }, + { + WP_LOCKBLOB_LAUNCHER, //int weaponNum; + 0, //int price; + ( 1 << S1 )|( 1 << S2 )|( 1 << S3 ), //int stages + SLOT_WEAPON, //int slots; + "lockblob", //char *weaponName; + "Lock Blob", //char *weaponHumanName; + 0, //int maxAmmo; + 0, //int maxClips; + qtrue, //int infiniteAmmo; + qfalse, //int usesEnergy; + 500, //int repeatRate1; + 500, //int repeatRate2; + 500, //int repeatRate3; + 0, //int reloadTime; + LOCKBLOB_K_SCALE, //float knockbackScale; + qfalse, //qboolean hasAltMode; + qfalse, //qboolean hasThirdMode; + qfalse, //qboolean canZoom; + 90.0f, //float zoomFov; + qfalse, //qboolean purchasable; + qfalse, //qboolean longRanged; + 0, //int buildDelay; + WUT_ALIENS //WUTeam_t team; + }, + { + WP_HIVE, //int weaponNum; + 0, //int price; + ( 1 << S1 )|( 1 << S2 )|( 1 << S3 ), //int stages + SLOT_WEAPON, //int slots; + "hive", //char *weaponName; + "Hive", //char *weaponHumanName; + 0, //int maxAmmo; + 0, //int maxClips; + qtrue, //int infiniteAmmo; + qfalse, //int usesEnergy; + 500, //int repeatRate1; + 500, //int repeatRate2; + 500, //int repeatRate3; + 0, //int reloadTime; + HIVE_K_SCALE, //float knockbackScale; + qfalse, //qboolean hasAltMode; + qfalse, //qboolean hasThirdMode; + qfalse, //qboolean canZoom; + 90.0f, //float zoomFov; + qfalse, //qboolean purchasable; + qfalse, //qboolean longRanged; + 0, //int buildDelay; + WUT_ALIENS //WUTeam_t team; + }, + { + WP_MGTURRET, //int weaponNum; + 0, //int price; + ( 1 << S1 )|( 1 << S2 )|( 1 << S3 ), //int stages + SLOT_WEAPON, //int slots; + "mgturret", //char *weaponName; + "Machinegun Turret", //char *weaponHumanName; + 0, //int maxAmmo; + 0, //int maxClips; + qtrue, //int infiniteAmmo; + qfalse, //int usesEnergy; + 0, //int repeatRate1; + 0, //int repeatRate2; + 0, //int repeatRate3; + 0, //int reloadTime; + MGTURRET_K_SCALE, //float knockbackScale; + qfalse, //qboolean hasAltMode; + qfalse, //qboolean hasThirdMode; + qfalse, //qboolean canZoom; + 90.0f, //float zoomFov; + qfalse, //qboolean purchasable; + qfalse, //qboolean longRanged; + 0, //int buildDelay; + WUT_HUMANS //WUTeam_t team; + }, + { + WP_TESLAGEN, //int weaponNum; + 0, //int price; + ( 1 << S1 )|( 1 << S2 )|( 1 << S3 ), //int stages + SLOT_WEAPON, //int slots; + "teslagen", //char *weaponName; + "Tesla Generator", //char *weaponHumanName; + 0, //int maxAmmo; + 0, //int maxClips; + qtrue, //int infiniteAmmo; + qtrue, //int usesEnergy; + 500, //int repeatRate1; + 500, //int repeatRate2; + 500, //int repeatRate3; + 0, //int reloadTime; + TESLAGEN_K_SCALE, //float knockbackScale; + qfalse, //qboolean hasAltMode; + qfalse, //qboolean hasThirdMode; + qfalse, //qboolean canZoom; + 90.0f, //float zoomFov; + qfalse, //qboolean purchasable; + qfalse, //qboolean longRanged; + 0, //int buildDelay; + WUT_HUMANS //WUTeam_t team; + } +}; + +int bg_numWeapons = sizeof( bg_weapons ) / sizeof( bg_weapons[ 0 ] ); + +/* +============== +BG_FindPriceForWeapon +============== +*/ +int BG_FindPriceForWeapon( int weapon ) +{ + int i; + + for( i = 0; i < bg_numWeapons; i++ ) + { + if( bg_weapons[ i ].weaponNum == weapon ) + { + return bg_weapons[ i ].price; + } + } + + return 100; +} + +/* +============== +BG_FindStagesForWeapon +============== +*/ +qboolean BG_FindStagesForWeapon( int weapon, stage_t stage ) +{ + int i; + + for( i = 0; i < bg_numWeapons; i++ ) + { + if( bg_weapons[ i ].weaponNum == weapon ) + { + if( bg_weapons[ i ].stages & ( 1 << stage ) ) + return qtrue; + else + return qfalse; + } + } + + return qfalse; +} + +/* +============== +BG_FindSlotsForWeapon +============== +*/ +int BG_FindSlotsForWeapon( int weapon ) +{ + int i; + + for( i = 0; i < bg_numWeapons; i++ ) + { + if( bg_weapons[ i ].weaponNum == weapon ) + { + return bg_weapons[ i ].slots; + } + } + + return SLOT_WEAPON; +} + +/* +============== +BG_FindNameForWeapon +============== +*/ +char *BG_FindNameForWeapon( int weapon ) +{ + int i; + + for( i = 0; i < bg_numWeapons; i++ ) + { + if( bg_weapons[ i ].weaponNum == weapon ) + return bg_weapons[ i ].weaponName; + } + + //wimp out + return 0; +} + +/* +============== +BG_FindWeaponNumForName +============== +*/ +int BG_FindWeaponNumForName( char *name ) +{ + int i; + + for( i = 0; i < bg_numWeapons; i++ ) + { + if( !Q_stricmp( bg_weapons[ i ].weaponName, name ) ) + return bg_weapons[ i ].weaponNum; + } + + //wimp out + return WP_NONE; +} + +/* +============== +BG_FindHumanNameForWeapon +============== +*/ +char *BG_FindHumanNameForWeapon( int weapon ) +{ + int i; + + for( i = 0; i < bg_numWeapons; i++ ) + { + if( bg_weapons[ i ].weaponNum == weapon ) + return bg_weapons[ i ].weaponHumanName; + } + + //wimp out + return 0; +} + +/* +============== +BG_FindAmmoForWeapon +============== +*/ +void BG_FindAmmoForWeapon( int weapon, int *maxAmmo, int *maxClips ) +{ + int i; + + for( i = 0; i < bg_numWeapons; i++ ) + { + if( bg_weapons[ i ].weaponNum == weapon ) + { + if( maxAmmo != NULL ) + *maxAmmo = bg_weapons[ i ].maxAmmo; + if( maxClips != NULL ) + *maxClips = bg_weapons[ i ].maxClips; + + //no need to keep going + break; + } + } +} + +/* +============== +BG_FindInfinteAmmoForWeapon +============== +*/ +qboolean BG_FindInfinteAmmoForWeapon( int weapon ) +{ + int i; + + for( i = 0; i < bg_numWeapons; i++ ) + { + if( bg_weapons[ i ].weaponNum == weapon ) + { + return bg_weapons[ i ].infiniteAmmo; + } + } + + return qfalse; +} + +/* +============== +BG_FindUsesEnergyForWeapon +============== +*/ +qboolean BG_FindUsesEnergyForWeapon( int weapon ) +{ + int i; + + for( i = 0; i < bg_numWeapons; i++ ) + { + if( bg_weapons[ i ].weaponNum == weapon ) + { + return bg_weapons[ i ].usesEnergy; + } + } + + return qfalse; +} + +/* +============== +BG_FindRepeatRate1ForWeapon +============== +*/ +int BG_FindRepeatRate1ForWeapon( int weapon ) +{ + int i; + + for( i = 0; i < bg_numWeapons; i++ ) + { + if( bg_weapons[ i ].weaponNum == weapon ) + return bg_weapons[ i ].repeatRate1; + } + + return 1000; +} + +/* +============== +BG_FindRepeatRate2ForWeapon +============== +*/ +int BG_FindRepeatRate2ForWeapon( int weapon ) +{ + int i; + + for( i = 0; i < bg_numWeapons; i++ ) + { + if( bg_weapons[ i ].weaponNum == weapon ) + return bg_weapons[ i ].repeatRate2; + } + + return 1000; +} + +/* +============== +BG_FindRepeatRate3ForWeapon +============== +*/ +int BG_FindRepeatRate3ForWeapon( int weapon ) +{ + int i; + + for( i = 0; i < bg_numWeapons; i++ ) + { + if( bg_weapons[ i ].weaponNum == weapon ) + return bg_weapons[ i ].repeatRate3; + } + + return 1000; +} + +/* +============== +BG_FindReloadTimeForWeapon +============== +*/ +int BG_FindReloadTimeForWeapon( int weapon ) +{ + int i; + + for( i = 0; i < bg_numWeapons; i++ ) + { + if( bg_weapons[ i ].weaponNum == weapon ) + { + return bg_weapons[ i ].reloadTime; + } + } + + return 1000; +} + +/* +============== +BG_FindKnockbackScaleForWeapon +============== +*/ +float BG_FindKnockbackScaleForWeapon( int weapon ) +{ + int i; + + for( i = 0; i < bg_numWeapons; i++ ) + { + if( bg_weapons[ i ].weaponNum == weapon ) + { + return bg_weapons[ i ].knockbackScale; + } + } + + return 1.0f; +} + +/* +============== +BG_WeaponHasAltMode +============== +*/ +qboolean BG_WeaponHasAltMode( int weapon ) +{ + int i; + + for( i = 0; i < bg_numWeapons; i++ ) + { + if( bg_weapons[ i ].weaponNum == weapon ) + { + return bg_weapons[ i ].hasAltMode; + } + } + + return qfalse; +} + +/* +============== +BG_WeaponHasThirdMode +============== +*/ +qboolean BG_WeaponHasThirdMode( int weapon ) +{ + int i; + + for( i = 0; i < bg_numWeapons; i++ ) + { + if( bg_weapons[ i ].weaponNum == weapon ) + { + return bg_weapons[ i ].hasThirdMode; + } + } + + return qfalse; +} + +/* +============== +BG_WeaponCanZoom +============== +*/ +qboolean BG_WeaponCanZoom( int weapon ) +{ + int i; + + for( i = 0; i < bg_numWeapons; i++ ) + { + if( bg_weapons[ i ].weaponNum == weapon ) + { + return bg_weapons[ i ].canZoom; + } + } + + return qfalse; +} + +/* +============== +BG_FindZoomFovForWeapon +============== +*/ +float BG_FindZoomFovForWeapon( int weapon ) +{ + int i; + + for( i = 0; i < bg_numWeapons; i++ ) + { + if( bg_weapons[ i ].weaponNum == weapon ) + { + return bg_weapons[ i ].zoomFov; + } + } + + return qfalse; +} + +/* +============== +BG_FindPurchasableForWeapon +============== +*/ +qboolean BG_FindPurchasableForWeapon( int weapon ) +{ + int i; + + for( i = 0; i < bg_numWeapons; i++ ) + { + if( bg_weapons[ i ].weaponNum == weapon ) + { + return bg_weapons[ i ].purchasable; + } + } + + return qfalse; +} + +/* +============== +BG_FindLongRangeForWeapon +============== +*/ +qboolean BG_FindLongRangedForWeapon( int weapon ) +{ + int i; + + for( i = 0; i < bg_numWeapons; i++ ) + { + if( bg_weapons[ i ].weaponNum == weapon ) + { + return bg_weapons[ i ].longRanged; + } + } + + return qfalse; +} + +/* +============== +BG_FindBuildDelayForWeapon +============== +*/ +int BG_FindBuildDelayForWeapon( int weapon ) +{ + int i; + + for( i = 0; i < bg_numWeapons; i++ ) + { + if( bg_weapons[ i ].weaponNum == weapon ) + { + return bg_weapons[ i ].buildDelay; + } + } + + return 0; +} + +/* +============== +BG_FindTeamForWeapon +============== +*/ +WUTeam_t BG_FindTeamForWeapon( int weapon ) +{ + int i; + + for( i = 0; i < bg_numWeapons; i++ ) + { + if( bg_weapons[ i ].weaponNum == weapon ) + { + return bg_weapons[ i ].team; + } + } + + return WUT_NONE; +} + +//////////////////////////////////////////////////////////////////////////////// + +upgradeAttributes_t bg_upgrades[ ] = +{ + { + UP_LIGHTARMOUR, //int upgradeNum; + LIGHTARMOUR_PRICE, //int price; + ( 1 << S1 )|( 1 << S2 )|( 1 << S3 ), //int stages + SLOT_TORSO|SLOT_ARMS|SLOT_LEGS, //int slots; + "larmour", //char *upgradeName; + "Light Armour", //char *upgradeHumanName; + "icons/iconu_larmour", + qtrue, //qboolean purchasable + qfalse, //qboolean usable + WUT_HUMANS //WUTeam_t team; + }, + { + UP_HELMET, //int upgradeNum; + HELMET_PRICE, //int price; + ( 1 << S2 )|( 1 << S3 ), //int stages + SLOT_HEAD, //int slots; + "helmet", //char *upgradeName; + "Helmet", //char *upgradeHumanName; + "icons/iconu_helmet", + qtrue, //qboolean purchasable + qfalse, //qboolean usable + WUT_HUMANS //WUTeam_t team; + }, + { + UP_MEDKIT, //int upgradeNum; + MEDKIT_PRICE, //int price; + ( 1 << S1 )|( 1 << S2 )|( 1 << S3 ), //int stages + SLOT_NONE, //int slots; + "medkit", //char *upgradeName; + "Medkit", //char *upgradeHumanName; + "icons/iconu_atoxin", + qfalse, //qboolean purchasable + qtrue, //qboolean usable + WUT_HUMANS //WUTeam_t team; + }, + { + UP_BATTPACK, //int upgradeNum; + BATTPACK_PRICE, //int price; + ( 1 << S1 )|( 1 << S2 )|( 1 << S3 ), //int stages + SLOT_BACKPACK, //int slots; + "battpack", //char *upgradeName; + "Battery Pack", //char *upgradeHumanName; + "icons/iconu_battpack", + qtrue, //qboolean purchasable + qfalse, //qboolean usable + WUT_HUMANS //WUTeam_t team; + }, + { + UP_JETPACK, //int upgradeNum; + JETPACK_PRICE, //int price; + ( 1 << S2 )|( 1 << S3 ), //int stages + SLOT_BACKPACK, //int slots; + "jetpack", //char *upgradeName; + "Jet Pack", //char *upgradeHumanName; + "icons/iconu_jetpack", + qtrue, //qboolean purchasable + qtrue, //qboolean usable + WUT_HUMANS //WUTeam_t team; + }, + { + UP_BATTLESUIT, //int upgradeNum; + BSUIT_PRICE, //int price; + ( 1 << S3 ), //int stages + SLOT_HEAD|SLOT_TORSO|SLOT_ARMS|SLOT_LEGS|SLOT_BACKPACK, //int slots; + "bsuit", //char *upgradeName; + "Battlesuit", //char *upgradeHumanName; + "icons/iconu_bsuit", + qtrue, //qboolean purchasable + qfalse, //qboolean usable + WUT_HUMANS //WUTeam_t team; + }, + { + UP_GRENADE, //int upgradeNum; + GRENADE_PRICE, //int price; + ( 1 << S2 )|( 1 << S3 ),//int stages + SLOT_NONE, //int slots; + "gren", //char *upgradeName; + "Grenade", //char *upgradeHumanName; + 0, + qtrue, //qboolean purchasable + qtrue, //qboolean usable + WUT_HUMANS //WUTeam_t team; + }, + { + UP_AMMO, //int upgradeNum; + 0, //int price; + ( 1 << S1 )|( 1 << S2 )|( 1 << S3 ), //int stages + SLOT_NONE, //int slots; + "ammo", //char *upgradeName; + "Ammunition", //char *upgradeHumanName; + 0, + qtrue, //qboolean purchasable + qfalse, //qboolean usable + WUT_HUMANS //WUTeam_t team; + } +}; + +int bg_numUpgrades = sizeof( bg_upgrades ) / sizeof( bg_upgrades[ 0 ] ); + +/* +============== +BG_FindPriceForUpgrade +============== +*/ +int BG_FindPriceForUpgrade( int upgrade ) +{ + int i; + + for( i = 0; i < bg_numUpgrades; i++ ) + { + if( bg_upgrades[ i ].upgradeNum == upgrade ) + { + return bg_upgrades[ i ].price; + } + } + + return 100; +} + +/* +============== +BG_FindStagesForUpgrade +============== +*/ +qboolean BG_FindStagesForUpgrade( int upgrade, stage_t stage ) +{ + int i; + + for( i = 0; i < bg_numUpgrades; i++ ) + { + if( bg_upgrades[ i ].upgradeNum == upgrade ) + { + if( bg_upgrades[ i ].stages & ( 1 << stage ) ) + return qtrue; + else + return qfalse; + } + } + + return qfalse; +} + +/* +============== +BG_FindSlotsForUpgrade +============== +*/ +int BG_FindSlotsForUpgrade( int upgrade ) +{ + int i; + + for( i = 0; i < bg_numUpgrades; i++ ) + { + if( bg_upgrades[ i ].upgradeNum == upgrade ) + { + return bg_upgrades[ i ].slots; + } + } + + return SLOT_NONE; +} + +/* +============== +BG_FindNameForUpgrade +============== +*/ +char *BG_FindNameForUpgrade( int upgrade ) +{ + int i; + + for( i = 0; i < bg_numUpgrades; i++ ) + { + if( bg_upgrades[ i ].upgradeNum == upgrade ) + return bg_upgrades[ i ].upgradeName; + } + + //wimp out + return 0; +} + +/* +============== +BG_FindUpgradeNumForName +============== +*/ +int BG_FindUpgradeNumForName( char *name ) +{ + int i; + + for( i = 0; i < bg_numUpgrades; i++ ) + { + if( !Q_stricmp( bg_upgrades[ i ].upgradeName, name ) ) + return bg_upgrades[ i ].upgradeNum; + } + + //wimp out + return UP_NONE; +} + +/* +============== +BG_FindHumanNameForUpgrade +============== +*/ +char *BG_FindHumanNameForUpgrade( int upgrade ) +{ + int i; + + for( i = 0; i < bg_numUpgrades; i++ ) + { + if( bg_upgrades[ i ].upgradeNum == upgrade ) + return bg_upgrades[ i ].upgradeHumanName; + } + + //wimp out + return 0; +} + +/* +============== +BG_FindIconForUpgrade +============== +*/ +char *BG_FindIconForUpgrade( int upgrade ) +{ + int i; + + for( i = 0; i < bg_numUpgrades; i++ ) + { + if( bg_upgrades[ i ].upgradeNum == upgrade ) + return bg_upgrades[ i ].icon; + } + + //wimp out + return 0; +} + +/* +============== +BG_FindPurchasableForUpgrade +============== +*/ +qboolean BG_FindPurchasableForUpgrade( int upgrade ) +{ + int i; + + for( i = 0; i < bg_numUpgrades; i++ ) + { + if( bg_upgrades[ i ].upgradeNum == upgrade ) + return bg_upgrades[ i ].purchasable; + } + + return qfalse; +} + +/* +============== +BG_FindUsableForUpgrade +============== +*/ +qboolean BG_FindUsableForUpgrade( int upgrade ) +{ + int i; + + for( i = 0; i < bg_numUpgrades; i++ ) + { + if( bg_upgrades[ i ].upgradeNum == upgrade ) + return bg_upgrades[ i ].usable; + } + + return qfalse; +} + +/* +============== +BG_FindTeamForUpgrade +============== +*/ +WUTeam_t BG_FindTeamForUpgrade( int upgrade ) +{ + int i; + + for( i = 0; i < bg_numUpgrades; i++ ) + { + if( bg_upgrades[ i ].upgradeNum == upgrade ) + { + return bg_upgrades[ i ].team; + } + } + + return WUT_NONE; +} + +//////////////////////////////////////////////////////////////////////////////// + +/* +================ +BG_EvaluateTrajectory + +================ +*/ +void BG_EvaluateTrajectory( const trajectory_t *tr, int atTime, vec3_t result ) +{ + float deltaTime; + float phase; + + switch( tr->trType ) + { + case TR_STATIONARY: + case TR_INTERPOLATE: + VectorCopy( tr->trBase, result ); + break; + + case TR_LINEAR: + deltaTime = ( atTime - tr->trTime ) * 0.001; // milliseconds to seconds + VectorMA( tr->trBase, deltaTime, tr->trDelta, result ); + break; + + case TR_SINE: + deltaTime = ( atTime - tr->trTime ) / (float)tr->trDuration; + phase = sin( deltaTime * M_PI * 2 ); + VectorMA( tr->trBase, phase, tr->trDelta, result ); + break; + + case TR_LINEAR_STOP: + if( atTime > tr->trTime + tr->trDuration ) + atTime = tr->trTime + tr->trDuration; + + deltaTime = ( atTime - tr->trTime ) * 0.001; // milliseconds to seconds + if( deltaTime < 0 ) + deltaTime = 0; + + VectorMA( tr->trBase, deltaTime, tr->trDelta, result ); + break; + + case TR_GRAVITY: + deltaTime = ( atTime - tr->trTime ) * 0.001; // milliseconds to seconds + VectorMA( tr->trBase, deltaTime, tr->trDelta, result ); + result[ 2 ] -= 0.5 * DEFAULT_GRAVITY * deltaTime * deltaTime; // FIXME: local gravity... + break; + + case TR_BUOYANCY: + deltaTime = ( atTime - tr->trTime ) * 0.001; // milliseconds to seconds + VectorMA( tr->trBase, deltaTime, tr->trDelta, result ); + result[ 2 ] += 0.5 * DEFAULT_GRAVITY * deltaTime * deltaTime; // FIXME: local gravity... + break; + + default: + Com_Error( ERR_DROP, "BG_EvaluateTrajectory: unknown trType: %i", tr->trTime ); + break; + } +} + +/* +================ +BG_EvaluateTrajectoryDelta + +For determining velocity at a given time +================ +*/ +void BG_EvaluateTrajectoryDelta( const trajectory_t *tr, int atTime, vec3_t result ) +{ + float deltaTime; + float phase; + + switch( tr->trType ) + { + case TR_STATIONARY: + case TR_INTERPOLATE: + VectorClear( result ); + break; + + case TR_LINEAR: + VectorCopy( tr->trDelta, result ); + break; + + case TR_SINE: + deltaTime = ( atTime - tr->trTime ) / (float)tr->trDuration; + phase = cos( deltaTime * M_PI * 2 ); // derivative of sin = cos + phase *= 0.5; + VectorScale( tr->trDelta, phase, result ); + break; + + case TR_LINEAR_STOP: + if( atTime > tr->trTime + tr->trDuration ) + { + VectorClear( result ); + return; + } + VectorCopy( tr->trDelta, result ); + break; + + case TR_GRAVITY: + deltaTime = ( atTime - tr->trTime ) * 0.001; // milliseconds to seconds + VectorCopy( tr->trDelta, result ); + result[ 2 ] -= DEFAULT_GRAVITY * deltaTime; // FIXME: local gravity... + break; + + case TR_BUOYANCY: + deltaTime = ( atTime - tr->trTime ) * 0.001; // milliseconds to seconds + VectorCopy( tr->trDelta, result ); + result[ 2 ] += DEFAULT_GRAVITY * deltaTime; // FIXME: local gravity... + break; + + default: + Com_Error( ERR_DROP, "BG_EvaluateTrajectoryDelta: unknown trType: %i", tr->trTime ); + break; + } +} + +char *eventnames[ ] = +{ + "EV_NONE", + + "EV_FOOTSTEP", + "EV_FOOTSTEP_METAL", + "EV_FOOTSTEP_SQUELCH", + "EV_FOOTSPLASH", + "EV_FOOTWADE", + "EV_SWIM", + + "EV_STEP_4", + "EV_STEP_8", + "EV_STEP_12", + "EV_STEP_16", + + "EV_STEPDN_4", + "EV_STEPDN_8", + "EV_STEPDN_12", + "EV_STEPDN_16", + + "EV_FALL_SHORT", + "EV_FALL_MEDIUM", + "EV_FALL_FAR", + "EV_FALLING", + + "EV_JUMP", + "EV_WATER_TOUCH", // foot touches + "EV_WATER_LEAVE", // foot leaves + "EV_WATER_UNDER", // head touches + "EV_WATER_CLEAR", // head leaves + + "EV_NOAMMO", + "EV_CHANGE_WEAPON", + "EV_FIRE_WEAPON", + "EV_FIRE_WEAPON2", + "EV_FIRE_WEAPON3", + + "EV_PLAYER_RESPAWN", //TA: for fovwarp effects + "EV_PLAYER_TELEPORT_IN", + "EV_PLAYER_TELEPORT_OUT", + + "EV_GRENADE_BOUNCE", // eventParm will be the soundindex + + "EV_GENERAL_SOUND", + "EV_GLOBAL_SOUND", // no attenuation + + "EV_BULLET_HIT_FLESH", + "EV_BULLET_HIT_WALL", + + "EV_SHOTGUN", + + "EV_MISSILE_HIT", + "EV_MISSILE_MISS", + "EV_MISSILE_MISS_METAL", + "EV_TESLATRAIL", + "EV_BULLET", // otherEntity is the shooter + + "EV_LEV1_GRAB", + "EV_LEV4_CHARGE_PREPARE", + "EV_LEV4_CHARGE_START", + + "EV_PAIN", + "EV_DEATH1", + "EV_DEATH2", + "EV_DEATH3", + "EV_OBITUARY", + + "EV_GIB_PLAYER", // gib a previously living player + + "EV_BUILD_CONSTRUCT", //TA + "EV_BUILD_DESTROY", //TA + "EV_BUILD_DELAY", //TA: can't build yet + "EV_BUILD_REPAIR", //TA: repairing buildable + "EV_BUILD_REPAIRED", //TA: buildable has full health + "EV_HUMAN_BUILDABLE_EXPLOSION", + "EV_ALIEN_BUILDABLE_EXPLOSION", + "EV_ALIEN_ACIDTUBE", + + "EV_MEDKIT_USED", + + "EV_ALIEN_EVOLVE", + "EV_ALIEN_EVOLVE_FAILED", + + "EV_DEBUG_LINE", + "EV_STOPLOOPINGSOUND", + "EV_TAUNT", + + "EV_OVERMIND_ATTACK", //TA: overmind under attack + "EV_OVERMIND_DYING", //TA: overmind close to death + "EV_OVERMIND_SPAWNS", //TA: overmind needs spawns + + "EV_DCC_ATTACK", //TA: dcc under attack + + "EV_RPTUSE_SOUND" //TA: trigger a sound +}; + +/* +=============== +BG_AddPredictableEventToPlayerstate + +Handles the sequence numbers +=============== +*/ + +void trap_Cvar_VariableStringBuffer( const char *var_name, char *buffer, int bufsize ); + +void BG_AddPredictableEventToPlayerstate( int newEvent, int eventParm, playerState_t *ps ) +{ +#ifdef _DEBUG + { + char buf[ 256 ]; + trap_Cvar_VariableStringBuffer( "showevents", buf, sizeof( buf ) ); + + if( atof( buf ) != 0 ) + { +#ifdef QAGAME + Com_Printf( " game event svt %5d -> %5d: num = %20s parm %d\n", + ps->pmove_framecount/*ps->commandTime*/, ps->eventSequence, eventnames[ newEvent ], eventParm); +#else + Com_Printf( "Cgame event svt %5d -> %5d: num = %20s parm %d\n", + ps->pmove_framecount/*ps->commandTime*/, ps->eventSequence, eventnames[ newEvent ], eventParm); +#endif + } + } +#endif + ps->events[ ps->eventSequence & ( MAX_PS_EVENTS - 1 ) ] = newEvent; + ps->eventParms[ ps->eventSequence & ( MAX_PS_EVENTS - 1 ) ] = eventParm; + ps->eventSequence++; +} + + +/* +======================== +BG_PlayerStateToEntityState + +This is done after each set of usercmd_t on the server, +and after local prediction on the client +======================== +*/ +void BG_PlayerStateToEntityState( playerState_t *ps, entityState_t *s, qboolean snap ) +{ + int i; + + if( ps->pm_type == PM_INTERMISSION || ps->pm_type == PM_SPECTATOR || ps->pm_type == PM_FREEZE ) + s->eType = ET_INVISIBLE; + else if( ps->persistant[ PERS_TEAM ] == TEAM_SPECTATOR ) + s->eType = ET_INVISIBLE; + else + s->eType = ET_PLAYER; + + s->number = ps->clientNum; + + s->pos.trType = TR_INTERPOLATE; + VectorCopy( ps->origin, s->pos.trBase ); + + if( snap ) + SnapVector( s->pos.trBase ); + + //set the trDelta for flag direction + VectorCopy( ps->velocity, s->pos.trDelta ); + + s->apos.trType = TR_INTERPOLATE; + VectorCopy( ps->viewangles, s->apos.trBase ); + + if( snap ) + SnapVector( s->apos.trBase ); + + //TA: i need for other things :) + //s->angles2[YAW] = ps->movementDir; + s->time2 = ps->movementDir; + s->legsAnim = ps->legsAnim; + s->torsoAnim = ps->torsoAnim; + s->clientNum = ps->clientNum; // ET_PLAYER looks here instead of at number + // so corpses can also reference the proper config + s->eFlags = ps->eFlags; + if( ps->stats[STAT_HEALTH] <= 0 ) + s->eFlags |= EF_DEAD; + else + s->eFlags &= ~EF_DEAD; + + if( ps->stats[ STAT_STATE ] & SS_BLOBLOCKED ) + s->eFlags |= EF_BLOBLOCKED; + else + s->eFlags &= ~EF_BLOBLOCKED; + + if( ps->externalEvent ) + { + s->event = ps->externalEvent; + s->eventParm = ps->externalEventParm; + } + else if( ps->entityEventSequence < ps->eventSequence ) + { + int seq; + + if( ps->entityEventSequence < ps->eventSequence - MAX_PS_EVENTS ) + ps->entityEventSequence = ps->eventSequence - MAX_PS_EVENTS; + + seq = ps->entityEventSequence & ( MAX_PS_EVENTS - 1 ); + s->event = ps->events[ seq ] | ( ( ps->entityEventSequence & 3 ) << 8 ); + s->eventParm = ps->eventParms[ seq ]; + ps->entityEventSequence++; + } + + s->weapon = ps->weapon; + s->groundEntityNum = ps->groundEntityNum; + + //store items held and active items in modelindex and modelindex2 + s->modelindex = 0; + s->modelindex2 = 0; + for( i = UP_NONE + 1; i < UP_NUM_UPGRADES; i++ ) + { + if( BG_InventoryContainsUpgrade( i, ps->stats ) ) + { + s->modelindex |= 1 << i; + + if( BG_UpgradeIsActive( i, ps->stats ) ) + s->modelindex2 |= 1 << i; + } + } + + //TA: use powerups field to store team/class info: + s->powerups = ps->stats[ STAT_PTEAM ] | ( ps->stats[ STAT_PCLASS ] << 8 ); + + //TA: have to get the surfNormal thru somehow... + VectorCopy( ps->grapplePoint, s->angles2 ); + if( ps->stats[ STAT_STATE ] & SS_WALLCLIMBINGCEILING ) + s->eFlags |= EF_WALLCLIMBCEILING; + + s->loopSound = ps->loopSound; + s->generic1 = ps->generic1; + + if( s->generic1 <= WPM_NONE || s->generic1 >= WPM_NUM_WEAPONMODES ) + s->generic1 = WPM_PRIMARY; + + s->otherEntityNum = ps->otherEntityNum; +} + + +/* +======================== +BG_PlayerStateToEntityStateExtraPolate + +This is done after each set of usercmd_t on the server, +and after local prediction on the client +======================== +*/ +void BG_PlayerStateToEntityStateExtraPolate( playerState_t *ps, entityState_t *s, int time, qboolean snap ) +{ + int i; + + if( ps->pm_type == PM_INTERMISSION || ps->pm_type == PM_SPECTATOR || ps->pm_type == PM_FREEZE ) + s->eType = ET_INVISIBLE; + else if( ps->persistant[ PERS_TEAM ] == TEAM_SPECTATOR ) + s->eType = ET_INVISIBLE; + else + s->eType = ET_PLAYER; + + s->number = ps->clientNum; + + s->pos.trType = TR_LINEAR_STOP; + VectorCopy( ps->origin, s->pos.trBase ); + + if( snap ) + SnapVector( s->pos.trBase ); + + // set the trDelta for flag direction and linear prediction + VectorCopy( ps->velocity, s->pos.trDelta ); + // set the time for linear prediction + s->pos.trTime = time; + // set maximum extra polation time + s->pos.trDuration = 50; // 1000 / sv_fps (default = 20) + + s->apos.trType = TR_INTERPOLATE; + VectorCopy( ps->viewangles, s->apos.trBase ); + if( snap ) + SnapVector( s->apos.trBase ); + + //TA: i need for other things :) + //s->angles2[YAW] = ps->movementDir; + s->time2 = ps->movementDir; + s->legsAnim = ps->legsAnim; + s->torsoAnim = ps->torsoAnim; + s->clientNum = ps->clientNum; // ET_PLAYER looks here instead of at number + // so corpses can also reference the proper config + s->eFlags = ps->eFlags; + + if( ps->stats[STAT_HEALTH] <= 0 ) + s->eFlags |= EF_DEAD; + else + s->eFlags &= ~EF_DEAD; + + if( ps->stats[ STAT_STATE ] & SS_BLOBLOCKED ) + s->eFlags |= EF_BLOBLOCKED; + else + s->eFlags &= ~EF_BLOBLOCKED; + + if( ps->externalEvent ) + { + s->event = ps->externalEvent; + s->eventParm = ps->externalEventParm; + } + else if( ps->entityEventSequence < ps->eventSequence ) + { + int seq; + + if( ps->entityEventSequence < ps->eventSequence - MAX_PS_EVENTS ) + ps->entityEventSequence = ps->eventSequence - MAX_PS_EVENTS; + + seq = ps->entityEventSequence & ( MAX_PS_EVENTS - 1 ); + s->event = ps->events[ seq ] | ( ( ps->entityEventSequence & 3 ) << 8 ); + s->eventParm = ps->eventParms[ seq ]; + ps->entityEventSequence++; + } + + s->weapon = ps->weapon; + s->groundEntityNum = ps->groundEntityNum; + + //store items held and active items in modelindex and modelindex2 + s->modelindex = 0; + s->modelindex2 = 0; + + for( i = UP_NONE + 1; i < UP_NUM_UPGRADES; i++ ) + { + if( BG_InventoryContainsUpgrade( i, ps->stats ) ) + { + s->modelindex |= 1 << i; + + if( BG_UpgradeIsActive( i, ps->stats ) ) + s->modelindex2 |= 1 << i; + } + } + + //TA: use powerups field to store team/class info: + s->powerups = ps->stats[ STAT_PTEAM ] | ( ps->stats[ STAT_PCLASS ] << 8 ); + + //TA: have to get the surfNormal thru somehow... + VectorCopy( ps->grapplePoint, s->angles2 ); + if( ps->stats[ STAT_STATE ] & SS_WALLCLIMBINGCEILING ) + s->eFlags |= EF_WALLCLIMBCEILING; + + s->loopSound = ps->loopSound; + s->generic1 = ps->generic1; + + if( s->generic1 <= WPM_NONE || s->generic1 >= WPM_NUM_WEAPONMODES ) + s->generic1 = WPM_PRIMARY; + + s->otherEntityNum = ps->otherEntityNum; +} + +/* +======================== +BG_UnpackAmmoArray + +Extract the ammo quantity from the array +======================== +*/ +void BG_UnpackAmmoArray( int weapon, int psAmmo[ ], int psAmmo2[ ], int *ammo, int *clips ) +{ + int ammoarray[ 32 ]; + int i; + + for( i = 0; i <= 15; i++ ) + ammoarray[ i ] = psAmmo[ i ]; + + for( i = 16; i <= 31; i++ ) + ammoarray[ i ] = psAmmo2[ i - 16 ]; + + if( ammo != NULL ) + *ammo = ammoarray[ weapon ] & 0x0FFF; + + if( clips != NULL ) + *clips = ( ammoarray[ weapon ] >> 12 ) & 0x0F; +} + +/* +======================== +BG_PackAmmoArray + +Pack the ammo quantity into the array +======================== +*/ +void BG_PackAmmoArray( int weapon, int psAmmo[ ], int psAmmo2[ ], int ammo, int clips ) +{ + int weaponvalue; + + weaponvalue = ammo | ( clips << 12 ); + + if( weapon <= 15 ) + psAmmo[ weapon ] = weaponvalue; + else if( weapon >= 16 ) + psAmmo2[ weapon - 16 ] = weaponvalue; +} + +/* +======================== +BG_WeaponIsFull + +Check if a weapon has full ammo +======================== +*/ +qboolean BG_WeaponIsFull( weapon_t weapon, int stats[ ], int psAmmo[ ], int psAmmo2[ ] ) +{ + int maxAmmo, maxClips; + int ammo, clips; + + BG_FindAmmoForWeapon( weapon, &maxAmmo, &maxClips ); + BG_UnpackAmmoArray( weapon, psAmmo, psAmmo2, &ammo, &clips ); + + if( BG_InventoryContainsUpgrade( UP_BATTPACK, stats ) ) + maxAmmo = (int)( (float)maxAmmo * BATTPACK_MODIFIER ); + + return ( maxAmmo == ammo ) && ( maxClips == clips ); +} + +/* +======================== +BG_AddWeaponToInventory + +Give a player a weapon +======================== +*/ +void BG_AddWeaponToInventory( int weapon, int stats[ ] ) +{ + int weaponList; + + weaponList = ( stats[ STAT_WEAPONS ] & 0x0000FFFF ) | ( ( stats[ STAT_WEAPONS2 ] << 16 ) & 0xFFFF0000 ); + + weaponList |= ( 1 << weapon ); + + stats[ STAT_WEAPONS ] = weaponList & 0x0000FFFF; + stats[ STAT_WEAPONS2 ] = ( weaponList & 0xFFFF0000 ) >> 16; + + if( stats[ STAT_SLOTS ] & BG_FindSlotsForWeapon( weapon ) ) + Com_Printf( S_COLOR_YELLOW "WARNING: Held items conflict with weapon %d\n", weapon ); + + stats[ STAT_SLOTS ] |= BG_FindSlotsForWeapon( weapon ); +} + +/* +======================== +BG_RemoveWeaponToInventory + +Take a weapon from a player +======================== +*/ +void BG_RemoveWeaponFromInventory( int weapon, int stats[ ] ) +{ + int weaponList; + + weaponList = ( stats[ STAT_WEAPONS ] & 0x0000FFFF ) | ( ( stats[ STAT_WEAPONS2 ] << 16 ) & 0xFFFF0000 ); + + weaponList &= ~( 1 << weapon ); + + stats[ STAT_WEAPONS ] = weaponList & 0x0000FFFF; + stats[ STAT_WEAPONS2 ] = ( weaponList & 0xFFFF0000 ) >> 16; + + stats[ STAT_SLOTS ] &= ~BG_FindSlotsForWeapon( weapon ); +} + +/* +======================== +BG_InventoryContainsWeapon + +Does the player hold a weapon? +======================== +*/ +qboolean BG_InventoryContainsWeapon( int weapon, int stats[ ] ) +{ + int weaponList; + + weaponList = ( stats[ STAT_WEAPONS ] & 0x0000FFFF ) | ( ( stats[ STAT_WEAPONS2 ] << 16 ) & 0xFFFF0000 ); + + return( weaponList & ( 1 << weapon ) ); +} + +/* +======================== +BG_AddUpgradeToInventory + +Give the player an upgrade +======================== +*/ +void BG_AddUpgradeToInventory( int item, int stats[ ] ) +{ + stats[ STAT_ITEMS ] |= ( 1 << item ); + + if( stats[ STAT_SLOTS ] & BG_FindSlotsForUpgrade( item ) ) + Com_Printf( S_COLOR_YELLOW "WARNING: Held items conflict with upgrade %d\n", item ); + + stats[ STAT_SLOTS ] |= BG_FindSlotsForUpgrade( item ); +} + +/* +======================== +BG_RemoveUpgradeFromInventory + +Take an upgrade from the player +======================== +*/ +void BG_RemoveUpgradeFromInventory( int item, int stats[ ] ) +{ + stats[ STAT_ITEMS ] &= ~( 1 << item ); + + stats[ STAT_SLOTS ] &= ~BG_FindSlotsForUpgrade( item ); +} + +/* +======================== +BG_InventoryContainsUpgrade + +Does the player hold an upgrade? +======================== +*/ +qboolean BG_InventoryContainsUpgrade( int item, int stats[ ] ) +{ + return( stats[ STAT_ITEMS ] & ( 1 << item ) ); +} + +/* +======================== +BG_ActivateUpgrade + +Activates an upgrade +======================== +*/ +void BG_ActivateUpgrade( int item, int stats[ ] ) +{ + stats[ STAT_ACTIVEITEMS ] |= ( 1 << item ); +} + +/* +======================== +BG_DeactivateUpgrade + +Deactivates an upgrade +======================== +*/ +void BG_DeactivateUpgrade( int item, int stats[ ] ) +{ + stats[ STAT_ACTIVEITEMS ] &= ~( 1 << item ); +} + +/* +======================== +BG_UpgradeIsActive + +Is this upgrade active? +======================== +*/ +qboolean BG_UpgradeIsActive( int item, int stats[ ] ) +{ + return( stats[ STAT_ACTIVEITEMS ] & ( 1 << item ) ); +} + +/* +=============== +BG_RotateAxis + +Shared axis rotation function +=============== +*/ +qboolean BG_RotateAxis( vec3_t surfNormal, vec3_t inAxis[ 3 ], + vec3_t outAxis[ 3 ], qboolean inverse, qboolean ceiling ) +{ + vec3_t refNormal = { 0.0f, 0.0f, 1.0f }; + vec3_t ceilingNormal = { 0.0f, 0.0f, -1.0f }; + vec3_t localNormal, xNormal; + float rotAngle; + + //the grapplePoint being a surfNormal rotation Normal hack... see above :) + if( ceiling ) + { + VectorCopy( ceilingNormal, localNormal ); + VectorCopy( surfNormal, xNormal ); + } + else + { + //cross the reference normal and the surface normal to get the rotation axis + VectorCopy( surfNormal, localNormal ); + CrossProduct( localNormal, refNormal, xNormal ); + VectorNormalize( xNormal ); + } + + //can't rotate with no rotation vector + if( VectorLength( xNormal ) != 0.0f ) + { + rotAngle = RAD2DEG( acos( DotProduct( localNormal, refNormal ) ) ); + + if( inverse ) + rotAngle = -rotAngle; + + AngleNormalize180( rotAngle ); + + //hmmm could get away with only one rotation and some clever stuff later... but i'm lazy + RotatePointAroundVector( outAxis[ 0 ], xNormal, inAxis[ 0 ], -rotAngle ); + RotatePointAroundVector( outAxis[ 1 ], xNormal, inAxis[ 1 ], -rotAngle ); + RotatePointAroundVector( outAxis[ 2 ], xNormal, inAxis[ 2 ], -rotAngle ); + } + else + return qfalse; + + return qtrue; +} + +/* +=============== +BG_PositionBuildableRelativeToPlayer + +Find a place to build a buildable +=============== +*/ +void BG_PositionBuildableRelativeToPlayer( const playerState_t *ps, + const vec3_t mins, const vec3_t maxs, + void (*trace)( trace_t *, const vec3_t, const vec3_t, + const vec3_t, const vec3_t, int, int ), + vec3_t outOrigin, vec3_t outAngles, trace_t *tr ) +{ + vec3_t forward, entityOrigin, targetOrigin; + vec3_t angles, playerOrigin, playerNormal; + float buildDist; + + if( ps->stats[ STAT_STATE ] & SS_WALLCLIMBING ) + { + if( ps->stats[ STAT_STATE ] & SS_WALLCLIMBINGCEILING ) + VectorSet( playerNormal, 0.0f, 0.0f, -1.0f ); + else + VectorCopy( ps->grapplePoint, playerNormal ); + } + else + VectorSet( playerNormal, 0.0f, 0.0f, 1.0f ); + + VectorCopy( ps->viewangles, angles ); + VectorCopy( ps->origin, playerOrigin ); + buildDist = BG_FindBuildDistForClass( ps->stats[ STAT_PCLASS ] ); + + AngleVectors( angles, forward, NULL, NULL ); + ProjectPointOnPlane( forward, forward, playerNormal ); + VectorNormalize( forward ); + + VectorMA( playerOrigin, buildDist, forward, entityOrigin ); + + VectorCopy( entityOrigin, targetOrigin ); + + //so buildings can be placed facing slopes + VectorMA( entityOrigin, 32, playerNormal, entityOrigin ); + + //so buildings drop to floor + VectorMA( targetOrigin, -128, playerNormal, targetOrigin ); + + (*trace)( tr, entityOrigin, mins, maxs, targetOrigin, ps->clientNum, MASK_PLAYERSOLID ); + VectorCopy( tr->endpos, entityOrigin ); + VectorMA( entityOrigin, 0.1f, playerNormal, outOrigin ); + vectoangles( forward, outAngles ); +} + +/* +=============== +BG_GetValueOfEquipment + +Returns the equipment value of some human player's gear +=============== +*/ + int BG_GetValueOfEquipment( playerState_t *ps ) { + int i, worth = 0; + + for( i = UP_NONE + 1; i < UP_NUM_UPGRADES; i++ ) + { + if( BG_InventoryContainsUpgrade( i, ps->stats ) ) + worth += BG_FindPriceForUpgrade( i ); + } + + for( i = WP_NONE + 1; i < WP_NUM_WEAPONS; i++ ) + { + if( BG_InventoryContainsWeapon( i, ps->stats ) ) + worth += BG_FindPriceForWeapon( i ); + } + + return worth; + } +/* +=============== +BG_GetValueOfHuman + +Returns the kills value of some human player +=============== +*/ +int BG_GetValueOfHuman( playerState_t *ps ) +{ + float portion = BG_GetValueOfEquipment( ps ) / (float)HUMAN_MAXED; + + + if( portion < 0.01f ) + portion = 0.01f; + else if( portion > 1.0f ) + portion = 1.0f; + + return ceil( ALIEN_MAX_SINGLE_KILLS * portion ); +} + +/* +=============== +atof_neg + +atof with an allowance for negative values +=============== +*/ +float atof_neg( char *token, qboolean allowNegative ) +{ + float value; + + value = atof( token ); + + if( !allowNegative && value < 0.0f ) + value = 1.0f; + + return value; +} + +/* +=============== +atoi_neg + +atoi with an allowance for negative values +=============== +*/ +int atoi_neg( char *token, qboolean allowNegative ) +{ + int value; + + value = atoi( token ); + + if( !allowNegative && value < 0 ) + value = 1; + + return value; +} + +/* +=============== +BG_ParseCSVEquipmentList +=============== +*/ +void BG_ParseCSVEquipmentList( const char *string, weapon_t *weapons, int weaponsSize, + upgrade_t *upgrades, int upgradesSize ) +{ + char buffer[ MAX_STRING_CHARS ]; + int i = 0, j = 0; + char *p, *q; + qboolean EOS = qfalse; + + Q_strncpyz( buffer, string, MAX_STRING_CHARS ); + + p = q = buffer; + + while( *p != '\0' ) + { + //skip to first , or EOS + while( *p != ',' && *p != '\0' ) + p++; + + if( *p == '\0' ) + EOS = qtrue; + + *p = '\0'; + + //strip leading whitespace + while( *q == ' ' ) + q++; + + if( weaponsSize ) + weapons[ i ] = BG_FindWeaponNumForName( q ); + + if( upgradesSize ) + upgrades[ j ] = BG_FindUpgradeNumForName( q ); + + if( weaponsSize && weapons[ i ] == WP_NONE && + upgradesSize && upgrades[ j ] == UP_NONE ) + Com_Printf( S_COLOR_YELLOW "WARNING: unknown equipment %s\n", q ); + else if( weaponsSize && weapons[ i ] != WP_NONE ) + i++; + else if( upgradesSize && upgrades[ j ] != UP_NONE ) + j++; + + if( !EOS ) + { + p++; + q = p; + } + else + break; + + if( i == ( weaponsSize - 1 ) || j == ( upgradesSize - 1 ) ) + break; + } + + if( weaponsSize ) + weapons[ i ] = WP_NONE; + + if( upgradesSize ) + upgrades[ j ] = UP_NONE; +} + +/* +=============== +BG_ParseCSVClassList +=============== +*/ +void BG_ParseCSVClassList( const char *string, pClass_t *classes, int classesSize ) +{ + char buffer[ MAX_STRING_CHARS ]; + int i = 0; + char *p, *q; + qboolean EOS = qfalse; + + Q_strncpyz( buffer, string, MAX_STRING_CHARS ); + + p = q = buffer; + + while( *p != '\0' ) + { + //skip to first , or EOS + while( *p != ',' && *p != '\0' ) + p++; + + if( *p == '\0' ) + EOS = qtrue; + + *p = '\0'; + + //strip leading whitespace + while( *q == ' ' ) + q++; + + classes[ i ] = BG_FindClassNumForName( q ); + + if( classes[ i ] == PCL_NONE ) + Com_Printf( S_COLOR_YELLOW "WARNING: unknown class %s\n", q ); + else + i++; + + if( !EOS ) + { + p++; + q = p; + } + else + break; + } + + classes[ i ] = PCL_NONE; +} + +/* +=============== +BG_ParseCSVBuildableList +=============== +*/ +void BG_ParseCSVBuildableList( const char *string, buildable_t *buildables, int buildablesSize ) +{ + char buffer[ MAX_STRING_CHARS ]; + int i = 0; + char *p, *q; + qboolean EOS = qfalse; + + Q_strncpyz( buffer, string, MAX_STRING_CHARS ); + + p = q = buffer; + + while( *p != '\0' ) + { + //skip to first , or EOS + while( *p != ',' && *p != '\0' ) + p++; + + if( *p == '\0' ) + EOS = qtrue; + + *p = '\0'; + + //strip leading whitespace + while( *q == ' ' ) + q++; + + buildables[ i ] = BG_FindBuildNumForName( q ); + + if( buildables[ i ] == BA_NONE ) + Com_Printf( S_COLOR_YELLOW "WARNING: unknown buildable %s\n", q ); + else + i++; + + if( !EOS ) + { + p++; + q = p; + } + else + break; + } + + buildables[ i ] = BA_NONE; +} + +/* +============ +BG_UpgradeClassAvailable +============ +*/ +qboolean BG_UpgradeClassAvailable( playerState_t *ps ) +{ + int i; + char buffer[ MAX_STRING_CHARS ]; + stage_t currentStage; + + trap_Cvar_VariableStringBuffer( "g_alienStage", buffer, MAX_STRING_CHARS ); + currentStage = atoi( buffer ); + + for( i = PCL_NONE + 1; i < PCL_NUM_CLASSES; i++ ) + { + if( BG_ClassCanEvolveFromTo( ps->stats[ STAT_PCLASS ], i, + ps->persistant[ PERS_CREDIT ], 0 ) >= 0 && + BG_FindStagesForClass( i, currentStage ) && + BG_ClassIsAllowed( i ) ) + { + return qtrue; + } + } + + return qfalse; +} + +typedef struct gameElements_s +{ + buildable_t buildables[ BA_NUM_BUILDABLES ]; + pClass_t classes[ PCL_NUM_CLASSES ]; + weapon_t weapons[ WP_NUM_WEAPONS ]; + upgrade_t upgrades[ UP_NUM_UPGRADES ]; +} gameElements_t; + +static gameElements_t bg_disabledGameElements; + +/* +============ +BG_InitAllowedGameElements +============ +*/ +void BG_InitAllowedGameElements( void ) +{ + char cvar[ MAX_CVAR_VALUE_STRING ]; + + trap_Cvar_VariableStringBuffer( "g_disabledEquipment", + cvar, MAX_CVAR_VALUE_STRING ); + + BG_ParseCSVEquipmentList( cvar, + bg_disabledGameElements.weapons, WP_NUM_WEAPONS, + bg_disabledGameElements.upgrades, UP_NUM_UPGRADES ); + + trap_Cvar_VariableStringBuffer( "g_disabledClasses", + cvar, MAX_CVAR_VALUE_STRING ); + + BG_ParseCSVClassList( cvar, + bg_disabledGameElements.classes, PCL_NUM_CLASSES ); + + trap_Cvar_VariableStringBuffer( "g_disabledBuildables", + cvar, MAX_CVAR_VALUE_STRING ); + + BG_ParseCSVBuildableList( cvar, + bg_disabledGameElements.buildables, BA_NUM_BUILDABLES ); +} + +/* +============ +BG_WeaponIsAllowed +============ +*/ +qboolean BG_WeaponIsAllowed( weapon_t weapon ) +{ + int i; + + for( i = 0; i < WP_NUM_WEAPONS && + bg_disabledGameElements.weapons[ i ] != WP_NONE; i++ ) + { + if( bg_disabledGameElements.weapons[ i ] == weapon ) + return qfalse; + } + + return qtrue; +} + +/* +============ +BG_UpgradeIsAllowed +============ +*/ +qboolean BG_UpgradeIsAllowed( upgrade_t upgrade ) +{ + int i; + + for( i = 0; i < UP_NUM_UPGRADES && + bg_disabledGameElements.upgrades[ i ] != UP_NONE; i++ ) + { + if( bg_disabledGameElements.upgrades[ i ] == upgrade ) + return qfalse; + } + + return qtrue; +} + +/* +============ +BG_ClassIsAllowed +============ +*/ +qboolean BG_ClassIsAllowed( pClass_t class ) +{ + int i; + + for( i = 0; i < PCL_NUM_CLASSES && + bg_disabledGameElements.classes[ i ] != PCL_NONE; i++ ) + { + if( bg_disabledGameElements.classes[ i ] == class ) + return qfalse; + } + + return qtrue; +} + +/* +============ +BG_BuildableIsAllowed +============ +*/ +qboolean BG_BuildableIsAllowed( buildable_t buildable ) +{ + int i; + + for( i = 0; i < BA_NUM_BUILDABLES && + bg_disabledGameElements.buildables[ i ] != BA_NONE; i++ ) + { + if( bg_disabledGameElements.buildables[ i ] == buildable ) + return qfalse; + } + + return qtrue; +} + +/* +============ +BG_ClientListTest +============ +*/ +qboolean BG_ClientListTest( clientList_t *list, int clientNum ) +{ + if( clientNum < 0 || clientNum >= MAX_CLIENTS || !list ) + return qfalse; + if( clientNum < 32 ) + return ( ( list->lo & ( 1 << clientNum ) ) != 0 ); + else + return ( ( list->hi & ( 1 << ( clientNum - 32 ) ) ) != 0 ); +} + +/* +============ +BG_ClientListAdd +============ +*/ +void BG_ClientListAdd( clientList_t *list, int clientNum ) +{ + if( clientNum < 0 || clientNum >= MAX_CLIENTS || !list ) + return; + if( clientNum < 32 ) + list->lo |= ( 1 << clientNum ); + else + list->hi |= ( 1 << ( clientNum - 32 ) ); +} + +/* +============ +BG_ClientListRemove +============ +*/ +void BG_ClientListRemove( clientList_t *list, int clientNum ) +{ + if( clientNum < 0 || clientNum >= MAX_CLIENTS || !list ) + return; + if( clientNum < 32 ) + list->lo &= ~( 1 << clientNum ); + else + list->hi &= ~( 1 << ( clientNum - 32 ) ); +} + +/* +============ +BG_ClientListString +============ +*/ +char *BG_ClientListString( clientList_t *list ) +{ + static char s[ 17 ]; + + s[ 0 ] = '\0'; + if( !list ) + return s; + Com_sprintf( s, sizeof( s ), "%08x%08x", list->hi, list->lo ); + return s; +} + +/* +============ +BG_ClientListParse +============ +*/ +void BG_ClientListParse( clientList_t *list, const char *s ) +{ + if( !list ) + return; + list->lo = 0; + list->hi = 0; + if( !s ) + return; + if( strlen( s ) != 16 ) + return; + sscanf( s, "%x%x", &list->hi, &list->lo ); +} + + diff --git a/src/game/bg_pmove.c b/src/game/bg_pmove.c new file mode 100644 index 0000000..b0e5c96 --- /dev/null +++ b/src/game/bg_pmove.c @@ -0,0 +1,3531 @@ +/* +=========================================================================== +Copyright (C) 1999-2005 Id Software, Inc. +Copyright (C) 2000-2006 Tim Angus + +This file is part of Tremulous. + +Tremulous is free software; you can redistribute it +and/or modify it under the terms of the GNU General Public License as +published by the Free Software Foundation; either version 2 of the License, +or (at your option) any later version. + +Tremulous is distributed in the hope that it will be +useful, but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with Tremulous; if not, write to the Free Software +Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA +=========================================================================== +*/ + +// bg_pmove.c -- both games player movement code +// takes a playerstate and a usercmd as input and returns a modifed playerstate + +#include "../qcommon/q_shared.h" +#include "bg_public.h" +#include "bg_local.h" + +pmove_t *pm; +pml_t pml; + +// movement parameters +float pm_stopspeed = 100.0f; +float pm_duckScale = 0.25f; +float pm_swimScale = 0.50f; +float pm_wadeScale = 0.70f; + +float pm_accelerate = 10.0f; +float pm_airaccelerate = 1.0f; +float pm_wateraccelerate = 4.0f; +float pm_flyaccelerate = 4.0f; + +float pm_friction = 6.0f; +float pm_waterfriction = 1.0f; +float pm_flightfriction = 6.0f; +float pm_spectatorfriction = 5.0f; + +int c_pmove = 0; + +/* +=============== +PM_AddEvent + +=============== +*/ +void PM_AddEvent( int newEvent ) +{ + BG_AddPredictableEventToPlayerstate( newEvent, 0, pm->ps ); +} + +/* +=============== +PM_AddTouchEnt +=============== +*/ +void PM_AddTouchEnt( int entityNum ) +{ + int i; + + if( entityNum == ENTITYNUM_WORLD ) + return; + + if( pm->numtouch == MAXTOUCH ) + return; + + // see if it is already added + for( i = 0 ; i < pm->numtouch ; i++ ) + { + if( pm->touchents[ i ] == entityNum ) + return; + } + + // add it + pm->touchents[ pm->numtouch ] = entityNum; + pm->numtouch++; +} + +/* +=================== +PM_StartTorsoAnim +=================== +*/ +static void PM_StartTorsoAnim( int anim ) +{ + if( pm->ps->pm_type >= PM_DEAD ) + return; + + pm->ps->torsoAnim = ( ( pm->ps->torsoAnim & ANIM_TOGGLEBIT ) ^ ANIM_TOGGLEBIT ) + | anim; +} + +/* +=================== +PM_StartLegsAnim +=================== +*/ +static void PM_StartLegsAnim( int anim ) +{ + if( pm->ps->pm_type >= PM_DEAD ) + return; + + //legsTimer is clamped too tightly for nonsegmented models + if( !( pm->ps->persistant[ PERS_STATE ] & PS_NONSEGMODEL ) ) + { + if( pm->ps->legsTimer > 0 ) + return; // a high priority animation is running + } + else + { + if( pm->ps->torsoTimer > 0 ) + return; // a high priority animation is running + } + + pm->ps->legsAnim = ( ( pm->ps->legsAnim & ANIM_TOGGLEBIT ) ^ ANIM_TOGGLEBIT ) + | anim; +} + +/* +=================== +PM_ContinueLegsAnim +=================== +*/ +static void PM_ContinueLegsAnim( int anim ) +{ + if( ( pm->ps->legsAnim & ~ANIM_TOGGLEBIT ) == anim ) + return; + + //legsTimer is clamped too tightly for nonsegmented models + if( !( pm->ps->persistant[ PERS_STATE ] & PS_NONSEGMODEL ) ) + { + if( pm->ps->legsTimer > 0 ) + return; // a high priority animation is running + } + else + { + if( pm->ps->torsoTimer > 0 ) + return; // a high priority animation is running + } + + PM_StartLegsAnim( anim ); +} + +/* +=================== +PM_ContinueTorsoAnim +=================== +*/ +static void PM_ContinueTorsoAnim( int anim ) +{ + if( ( pm->ps->torsoAnim & ~ANIM_TOGGLEBIT ) == anim ) + return; + + if( pm->ps->torsoTimer > 0 ) + return; // a high priority animation is running + + PM_StartTorsoAnim( anim ); +} + +/* +=================== +PM_ForceLegsAnim +=================== +*/ +static void PM_ForceLegsAnim( int anim ) +{ + //legsTimer is clamped too tightly for nonsegmented models + if( !( pm->ps->persistant[ PERS_STATE ] & PS_NONSEGMODEL ) ) + pm->ps->legsTimer = 0; + else + pm->ps->torsoTimer = 0; + + PM_StartLegsAnim( anim ); +} + + +/* +================== +PM_ClipVelocity + +Slide off of the impacting surface +================== +*/ +void PM_ClipVelocity( vec3_t in, vec3_t normal, vec3_t out, float overbounce ) +{ + float backoff; + float change; + int i; + + backoff = DotProduct( in, normal ); + + //Com_Printf( "%1.0f ", backoff ); + + if( backoff < 0 ) + backoff *= overbounce; + else + backoff /= overbounce; + + for( i = 0; i < 3; i++ ) + { + change = normal[ i ] * backoff; + //Com_Printf( "%1.0f ", change ); + out[ i ] = in[ i ] - change; + } + + //Com_Printf( " " ); +} + + +/* +================== +PM_Friction + +Handles both ground friction and water friction +================== +*/ +static void PM_Friction( void ) +{ + vec3_t vec; + float *vel; + float speed, newspeed, control; + float drop; + + vel = pm->ps->velocity; + + //TA: make sure vertical velocity is NOT set to zero when wall climbing + VectorCopy( vel, vec ); + if( pml.walking && !( pm->ps->stats[ STAT_STATE ] & SS_WALLCLIMBING ) ) + vec[ 2 ] = 0; // ignore slope movement + + speed = VectorLength( vec ); + + if( speed < 1 ) + { + vel[ 0 ] = 0; + vel[ 1 ] = 0; // allow sinking underwater + // FIXME: still have z friction underwater? + return; + } + + drop = 0; + + // apply ground friction + if( pm->waterlevel <= 1 ) + { + if( ( pml.walking || pml.ladder ) && !( pml.groundTrace.surfaceFlags & SURF_SLICK ) ) + { + // if getting knocked back, no friction + if( !( pm->ps->pm_flags & PMF_TIME_KNOCKBACK ) ) + { + float stopSpeed = BG_FindStopSpeedForClass( pm->ps->stats[ STAT_PCLASS ] ); + + control = speed < stopSpeed ? stopSpeed : speed; + drop += control * BG_FindFrictionForClass( pm->ps->stats[ STAT_PCLASS ] ) * pml.frametime; + } + } + } + + // apply water friction even if just wading + if( pm->waterlevel ) + drop += speed * pm_waterfriction * pm->waterlevel * pml.frametime; + + // apply flying friction + if( pm->ps->pm_type == PM_JETPACK ) + drop += speed * pm_flightfriction * pml.frametime; + + if( pm->ps->pm_type == PM_SPECTATOR ) + drop += speed * pm_spectatorfriction * pml.frametime; + + // scale the velocity + newspeed = speed - drop; + if( newspeed < 0 ) + newspeed = 0; + + newspeed /= speed; + + vel[ 0 ] = vel[ 0 ] * newspeed; + vel[ 1 ] = vel[ 1 ] * newspeed; + vel[ 2 ] = vel[ 2 ] * newspeed; +} + + +/* +============== +PM_Accelerate + +Handles user intended acceleration +============== +*/ +static void PM_Accelerate( vec3_t wishdir, float wishspeed, float accel ) +{ +#if 1 + // q2 style + int i; + float addspeed, accelspeed, currentspeed; + + currentspeed = DotProduct( pm->ps->velocity, wishdir ); + addspeed = wishspeed - currentspeed; + if( addspeed <= 0 ) + return; + + accelspeed = accel * pml.frametime * wishspeed; + if( accelspeed > addspeed ) + accelspeed = addspeed; + + for( i = 0; i < 3; i++ ) + pm->ps->velocity[ i ] += accelspeed * wishdir[ i ]; +#else + // proper way (avoids strafe jump maxspeed bug), but feels bad + vec3_t wishVelocity; + vec3_t pushDir; + float pushLen; + float canPush; + + VectorScale( wishdir, wishspeed, wishVelocity ); + VectorSubtract( wishVelocity, pm->ps->velocity, pushDir ); + pushLen = VectorNormalize( pushDir ); + + canPush = accel * pml.frametime * wishspeed; + if( canPush > pushLen ) + canPush = pushLen; + + VectorMA( pm->ps->velocity, canPush, pushDir, pm->ps->velocity ); +#endif +} + + + +/* +============ +PM_CmdScale + +Returns the scale factor to apply to cmd movements +This allows the clients to use axial -127 to 127 values for all directions +without getting a sqrt(2) distortion in speed. +============ +*/ +static float PM_CmdScale( usercmd_t *cmd ) +{ + int max; + float total; + float scale; + float modifier = 1.0f; + + if( pm->ps->stats[ STAT_PTEAM ] == PTE_HUMANS && pm->ps->pm_type == PM_NORMAL ) + { + if( pm->ps->stats[ STAT_STATE ] & SS_SPEEDBOOST ) + modifier *= HUMAN_SPRINT_MODIFIER; + else + modifier *= HUMAN_JOG_MODIFIER; + + if( cmd->forwardmove < 0 ) + { + //can't run backwards + modifier *= HUMAN_BACK_MODIFIER; + } + else if( cmd->rightmove ) + { + //can't move that fast sideways + modifier *= HUMAN_SIDE_MODIFIER; + } + + //must have +ve stamina to jump + if( pm->ps->stats[ STAT_STAMINA ] < 0 ) + cmd->upmove = 0; + + //slow down once stamina depletes + if( pm->ps->stats[ STAT_STAMINA ] <= -500 ) + modifier *= (float)( pm->ps->stats[ STAT_STAMINA ] + 1000 ) / 500.0f; + + if( pm->ps->stats[ STAT_STATE ] & SS_CREEPSLOWED ) + { + if( BG_InventoryContainsUpgrade( UP_LIGHTARMOUR, pm->ps->stats ) || + BG_InventoryContainsUpgrade( UP_BATTLESUIT, pm->ps->stats ) ) + modifier *= CREEP_ARMOUR_MODIFIER; + else + modifier *= CREEP_MODIFIER; + } + } + + if( pm->ps->weapon == WP_ALEVEL4 && pm->ps->pm_flags & PMF_CHARGE ) + modifier *= ( 1.0f + ( pm->ps->stats[ STAT_MISC ] / (float)LEVEL4_CHARGE_TIME ) * + ( LEVEL4_CHARGE_SPEED - 1.0f ) ); + + //slow player if charging up for a pounce + if( ( pm->ps->weapon == WP_ALEVEL3 || pm->ps->weapon == WP_ALEVEL3_UPG ) && + cmd->buttons & BUTTON_ATTACK2 ) + modifier *= LEVEL3_POUNCE_SPEED_MOD; + + //slow the player if slow locked + if( pm->ps->stats[ STAT_STATE ] & SS_SLOWLOCKED ) + modifier *= ABUILDER_BLOB_SPEED_MOD; + + if( pm->ps->pm_type == PM_GRABBED ) + modifier = 0.0f; + + if( pm->ps->pm_type != PM_SPECTATOR && pm->ps->pm_type != PM_NOCLIP ) + { + if( BG_FindJumpMagnitudeForClass( pm->ps->stats[ STAT_PCLASS ] ) == 0.0f ) + cmd->upmove = 0; + + //prevent speed distortions for non ducking classes + if( !( pm->ps->pm_flags & PMF_DUCKED ) && pm->ps->pm_type != PM_JETPACK && cmd->upmove < 0 ) + cmd->upmove = 0; + } + + max = abs( cmd->forwardmove ); + if( abs( cmd->rightmove ) > max ) + max = abs( cmd->rightmove ); + + if( abs( cmd->upmove ) > max ) + max = abs( cmd->upmove ); + + if( !max ) + return 0; + + total = sqrt( cmd->forwardmove * cmd->forwardmove + + cmd->rightmove * cmd->rightmove + cmd->upmove * cmd->upmove ); + + scale = (float)pm->ps->speed * max / ( 127.0 * total ) * modifier; + + return scale; +} + + +/* +================ +PM_SetMovementDir + +Determine the rotation of the legs reletive +to the facing dir +================ +*/ +static void PM_SetMovementDir( void ) +{ + if( pm->cmd.forwardmove || pm->cmd.rightmove ) + { + if( pm->cmd.rightmove == 0 && pm->cmd.forwardmove > 0 ) + pm->ps->movementDir = 0; + else if( pm->cmd.rightmove < 0 && pm->cmd.forwardmove > 0 ) + pm->ps->movementDir = 1; + else if( pm->cmd.rightmove < 0 && pm->cmd.forwardmove == 0 ) + pm->ps->movementDir = 2; + else if( pm->cmd.rightmove < 0 && pm->cmd.forwardmove < 0 ) + pm->ps->movementDir = 3; + else if( pm->cmd.rightmove == 0 && pm->cmd.forwardmove < 0 ) + pm->ps->movementDir = 4; + else if( pm->cmd.rightmove > 0 && pm->cmd.forwardmove < 0 ) + pm->ps->movementDir = 5; + else if( pm->cmd.rightmove > 0 && pm->cmd.forwardmove == 0 ) + pm->ps->movementDir = 6; + else if( pm->cmd.rightmove > 0 && pm->cmd.forwardmove > 0 ) + pm->ps->movementDir = 7; + } + else + { + // if they aren't actively going directly sideways, + // change the animation to the diagonal so they + // don't stop too crooked + if( pm->ps->movementDir == 2 ) + pm->ps->movementDir = 1; + else if( pm->ps->movementDir == 6 ) + pm->ps->movementDir = 7; + } +} + + +/* +============= +PM_CheckCharge +============= +*/ +static void PM_CheckCharge( void ) +{ + if( pm->ps->weapon != WP_ALEVEL4 ) + return; + + if( pm->cmd.buttons & BUTTON_ATTACK2 && + !( pm->ps->stats[ STAT_STATE ] & SS_CHARGING ) ) + { + pm->ps->pm_flags &= ~PMF_CHARGE; + return; + } + + if( pm->ps->stats[ STAT_MISC ] > 0 ) + pm->ps->pm_flags |= PMF_CHARGE; + else + pm->ps->pm_flags &= ~PMF_CHARGE; +} + +/* +============= +PM_CheckPounce +============= +*/ +static qboolean PM_CheckPounce( void ) +{ + if( pm->ps->weapon != WP_ALEVEL3 && + pm->ps->weapon != WP_ALEVEL3_UPG ) + return qfalse; + + // we were pouncing, but we've landed + if( pm->ps->groundEntityNum != ENTITYNUM_NONE + && ( pm->ps->pm_flags & PMF_CHARGE ) ) + { + pm->ps->weaponTime += LEVEL3_POUNCE_TIME; + pm->ps->pm_flags &= ~PMF_CHARGE; + } + + // we're building up for a pounce + if( pm->cmd.buttons & BUTTON_ATTACK2 ) + return qfalse; + + // already a pounce in progress + if( pm->ps->pm_flags & PMF_CHARGE ) + return qfalse; + + if( pm->ps->stats[ STAT_MISC ] == 0 ) + return qfalse; + + pml.groundPlane = qfalse; // jumping away + pml.walking = qfalse; + + pm->ps->pm_flags |= PMF_CHARGE; + + pm->ps->groundEntityNum = ENTITYNUM_NONE; + + VectorMA( pm->ps->velocity, pm->ps->stats[ STAT_MISC ], pml.forward, pm->ps->velocity ); + + PM_AddEvent( EV_JUMP ); + + if( pm->cmd.forwardmove >= 0 ) + { + if( !( pm->ps->persistant[ PERS_STATE ] & PS_NONSEGMODEL ) ) + PM_ForceLegsAnim( LEGS_JUMP ); + else + PM_ForceLegsAnim( NSPA_JUMP ); + + pm->ps->pm_flags &= ~PMF_BACKWARDS_JUMP; + } + else + { + if( !( pm->ps->persistant[ PERS_STATE ] & PS_NONSEGMODEL ) ) + PM_ForceLegsAnim( LEGS_JUMPB ); + else + PM_ForceLegsAnim( NSPA_JUMPBACK ); + + pm->ps->pm_flags |= PMF_BACKWARDS_JUMP; + } + + pm->pmext->pouncePayload = pm->ps->stats[ STAT_MISC ]; + pm->ps->stats[ STAT_MISC ] = 0; + + return qtrue; +} + +/* +============= +PM_CheckWallJump +============= +*/ +static qboolean PM_CheckWallJump( void ) +{ + vec3_t dir, forward, right; + vec3_t refNormal = { 0.0f, 0.0f, 1.0f }; + float normalFraction = 1.5f; + float cmdFraction = 1.0f; + float upFraction = 1.5f; + + if( pm->ps->pm_flags & PMF_RESPAWNED ) + return qfalse; // don't allow jump until all buttons are up + + if( pm->cmd.upmove < 10 ) + // not holding jump + return qfalse; + + if( pm->ps->pm_flags & PMF_TIME_WALLJUMP ) + return qfalse; + + // must wait for jump to be released + if( pm->ps->pm_flags & PMF_JUMP_HELD && + pm->ps->grapplePoint[ 2 ] == 1.0f ) + { + // clear upmove so cmdscale doesn't lower running speed + pm->cmd.upmove = 0; + return qfalse; + } + + pm->ps->pm_flags |= PMF_TIME_WALLJUMP; + pm->ps->pm_time = 200; + + pml.groundPlane = qfalse; // jumping away + pml.walking = qfalse; + pm->ps->pm_flags |= PMF_JUMP_HELD; + + pm->ps->groundEntityNum = ENTITYNUM_NONE; + + ProjectPointOnPlane( forward, pml.forward, pm->ps->grapplePoint ); + ProjectPointOnPlane( right, pml.right, pm->ps->grapplePoint ); + + VectorScale( pm->ps->grapplePoint, normalFraction, dir ); + + if( pm->cmd.forwardmove > 0 ) + VectorMA( dir, cmdFraction, forward, dir ); + else if( pm->cmd.forwardmove < 0 ) + VectorMA( dir, -cmdFraction, forward, dir ); + + if( pm->cmd.rightmove > 0 ) + VectorMA( dir, cmdFraction, right, dir ); + else if( pm->cmd.rightmove < 0 ) + VectorMA( dir, -cmdFraction, right, dir ); + + VectorMA( dir, upFraction, refNormal, dir ); + VectorNormalize( dir ); + + VectorMA( pm->ps->velocity, BG_FindJumpMagnitudeForClass( pm->ps->stats[ STAT_PCLASS ] ), + dir, pm->ps->velocity ); + + //for a long run of wall jumps the velocity can get pretty large, this caps it + if( VectorLength( pm->ps->velocity ) > LEVEL2_WALLJUMP_MAXSPEED ) + { + VectorNormalize( pm->ps->velocity ); + VectorScale( pm->ps->velocity, LEVEL2_WALLJUMP_MAXSPEED, pm->ps->velocity ); + } + + PM_AddEvent( EV_JUMP ); + + if( pm->cmd.forwardmove >= 0 ) + { + if( !( pm->ps->persistant[ PERS_STATE ] & PS_NONSEGMODEL ) ) + PM_ForceLegsAnim( LEGS_JUMP ); + else + PM_ForceLegsAnim( NSPA_JUMP ); + + pm->ps->pm_flags &= ~PMF_BACKWARDS_JUMP; + } + else + { + if( !( pm->ps->persistant[ PERS_STATE ] & PS_NONSEGMODEL ) ) + PM_ForceLegsAnim( LEGS_JUMPB ); + else + PM_ForceLegsAnim( NSPA_JUMPBACK ); + + pm->ps->pm_flags |= PMF_BACKWARDS_JUMP; + } + + return qtrue; +} + +/* +============= +PM_CheckJump +============= +*/ +static qboolean PM_CheckJump( void ) +{ + if( BG_FindJumpMagnitudeForClass( pm->ps->stats[ STAT_PCLASS ] ) == 0.0f ) + return qfalse; + + if( BG_ClassHasAbility( pm->ps->stats[ STAT_PCLASS ], SCA_WALLJUMPER ) ) + return PM_CheckWallJump( ); + + //can't jump and pounce at the same time + if( ( pm->ps->weapon == WP_ALEVEL3 || + pm->ps->weapon == WP_ALEVEL3_UPG ) && + pm->ps->stats[ STAT_MISC ] > 0 ) + return qfalse; + + //can't jump and charge at the same time + if( ( pm->ps->weapon == WP_ALEVEL4 ) && + pm->ps->stats[ STAT_MISC ] > 0 ) + return qfalse; + + if( ( pm->ps->stats[ STAT_PTEAM ] == PTE_HUMANS ) && + ( pm->ps->stats[ STAT_STAMINA ] < 0 ) ) + return qfalse; + + if( pm->ps->pm_flags & PMF_RESPAWNED ) + return qfalse; // don't allow jump until all buttons are up + + if( pm->cmd.upmove < 10 ) + // not holding jump + return qfalse; + + //can't jump whilst grabbed + if( pm->ps->pm_type == PM_GRABBED ) + { + pm->cmd.upmove = 0; + return qfalse; + } + + // must wait for jump to be released + if( pm->ps->pm_flags & PMF_JUMP_HELD ) + { + // clear upmove so cmdscale doesn't lower running speed + pm->cmd.upmove = 0; + return qfalse; + } + + pml.groundPlane = qfalse; // jumping away + pml.walking = qfalse; + pm->ps->pm_flags |= PMF_JUMP_HELD; + + //TA: take some stamina off + if( pm->ps->stats[ STAT_PTEAM ] == PTE_HUMANS ) + pm->ps->stats[ STAT_STAMINA ] -= 500; + + pm->ps->groundEntityNum = ENTITYNUM_NONE; + + //TA: jump away from wall + if( pm->ps->stats[ STAT_STATE ] & SS_WALLCLIMBING ) + { + vec3_t normal = { 0, 0, -1 }; + + if( !( pm->ps->stats[ STAT_STATE ] & SS_WALLCLIMBINGCEILING ) ) + VectorCopy( pm->ps->grapplePoint, normal ); + + VectorMA( pm->ps->velocity, BG_FindJumpMagnitudeForClass( pm->ps->stats[ STAT_PCLASS ] ), + normal, pm->ps->velocity ); + } + else + pm->ps->velocity[ 2 ] = BG_FindJumpMagnitudeForClass( pm->ps->stats[ STAT_PCLASS ] ); + + PM_AddEvent( EV_JUMP ); + + if( pm->cmd.forwardmove >= 0 ) + { + if( !( pm->ps->persistant[ PERS_STATE ] & PS_NONSEGMODEL ) ) + PM_ForceLegsAnim( LEGS_JUMP ); + else + PM_ForceLegsAnim( NSPA_JUMP ); + + pm->ps->pm_flags &= ~PMF_BACKWARDS_JUMP; + } + else + { + if( !( pm->ps->persistant[ PERS_STATE ] & PS_NONSEGMODEL ) ) + PM_ForceLegsAnim( LEGS_JUMPB ); + else + PM_ForceLegsAnim( NSPA_JUMPBACK ); + + pm->ps->pm_flags |= PMF_BACKWARDS_JUMP; + } + + return qtrue; +} + +/* +============= +PM_CheckWaterJump +============= +*/ +static qboolean PM_CheckWaterJump( void ) +{ + vec3_t spot; + int cont; + vec3_t flatforward; + + if( pm->ps->pm_time ) + return qfalse; + + // check for water jump + if( pm->waterlevel != 2 ) + return qfalse; + + flatforward[ 0 ] = pml.forward[ 0 ]; + flatforward[ 1 ] = pml.forward[ 1 ]; + flatforward[ 2 ] = 0; + VectorNormalize( flatforward ); + + VectorMA( pm->ps->origin, 30, flatforward, spot ); + spot[ 2 ] += 4; + cont = pm->pointcontents( spot, pm->ps->clientNum ); + + if( !( cont & CONTENTS_SOLID ) ) + return qfalse; + + spot[ 2 ] += 16; + cont = pm->pointcontents( spot, pm->ps->clientNum ); + + if( cont ) + return qfalse; + + // jump out of water + VectorScale( pml.forward, 200, pm->ps->velocity ); + pm->ps->velocity[ 2 ] = 350; + + pm->ps->pm_flags |= PMF_TIME_WATERJUMP; + pm->ps->pm_time = 2000; + + return qtrue; +} + +//============================================================================ + + +/* +=================== +PM_WaterJumpMove + +Flying out of the water +=================== +*/ +static void PM_WaterJumpMove( void ) +{ + // waterjump has no control, but falls + + PM_StepSlideMove( qtrue, qfalse ); + + pm->ps->velocity[ 2 ] -= pm->ps->gravity * pml.frametime; + if( pm->ps->velocity[ 2 ] < 0 ) + { + // cancel as soon as we are falling down again + pm->ps->pm_flags &= ~PMF_ALL_TIMES; + pm->ps->pm_time = 0; + } +} + +/* +=================== +PM_WaterMove + +=================== +*/ +static void PM_WaterMove( void ) +{ + int i; + vec3_t wishvel; + float wishspeed; + vec3_t wishdir; + float scale; + float vel; + + if( PM_CheckWaterJump( ) ) + { + PM_WaterJumpMove(); + return; + } +#if 0 + // jump = head for surface + if ( pm->cmd.upmove >= 10 ) { + if (pm->ps->velocity[2] > -300) { + if ( pm->watertype == CONTENTS_WATER ) { + pm->ps->velocity[2] = 100; + } else if (pm->watertype == CONTENTS_SLIME) { + pm->ps->velocity[2] = 80; + } else { + pm->ps->velocity[2] = 50; + } + } + } +#endif + PM_Friction( ); + + scale = PM_CmdScale( &pm->cmd ); + // + // user intentions + // + if( !scale ) + { + wishvel[ 0 ] = 0; + wishvel[ 1 ] = 0; + wishvel[ 2 ] = -60; // sink towards bottom + } + else + { + for( i = 0; i < 3; i++ ) + wishvel[ i ] = scale * pml.forward[ i ] * pm->cmd.forwardmove + scale * pml.right[ i ] * pm->cmd.rightmove; + + wishvel[ 2 ] += scale * pm->cmd.upmove; + } + + VectorCopy( wishvel, wishdir ); + wishspeed = VectorNormalize( wishdir ); + + if( wishspeed > pm->ps->speed * pm_swimScale ) + wishspeed = pm->ps->speed * pm_swimScale; + + PM_Accelerate( wishdir, wishspeed, pm_wateraccelerate ); + + // make sure we can go up slopes easily under water + if( pml.groundPlane && DotProduct( pm->ps->velocity, pml.groundTrace.plane.normal ) < 0 ) + { + vel = VectorLength( pm->ps->velocity ); + // slide along the ground plane + PM_ClipVelocity( pm->ps->velocity, pml.groundTrace.plane.normal, + pm->ps->velocity, OVERCLIP ); + + VectorNormalize( pm->ps->velocity ); + VectorScale( pm->ps->velocity, vel, pm->ps->velocity ); + } + + PM_SlideMove( qfalse ); +} + +/* +=================== +PM_JetPackMove + +Only with the jetpack +=================== +*/ +static void PM_JetPackMove( void ) +{ + int i; + vec3_t wishvel; + float wishspeed; + vec3_t wishdir; + float scale; + + //normal slowdown + PM_Friction( ); + + scale = PM_CmdScale( &pm->cmd ); + + // user intentions + for( i = 0; i < 2; i++ ) + wishvel[ i ] = scale * pml.forward[ i ] * pm->cmd.forwardmove + scale * pml.right[ i ] * pm->cmd.rightmove; + + if( pm->cmd.upmove > 0.0f ) + wishvel[ 2 ] = JETPACK_FLOAT_SPEED; + else if( pm->cmd.upmove < 0.0f ) + wishvel[ 2 ] = -JETPACK_SINK_SPEED; + else + wishvel[ 2 ] = 0.0f; + + VectorCopy( wishvel, wishdir ); + wishspeed = VectorNormalize( wishdir ); + + PM_Accelerate( wishdir, wishspeed, pm_flyaccelerate ); + + PM_StepSlideMove( qfalse, qfalse ); + + if( !( pm->ps->persistant[ PERS_STATE ] & PS_NONSEGMODEL ) ) + PM_ContinueLegsAnim( LEGS_LAND ); + else + PM_ContinueLegsAnim( NSPA_LAND ); +} + + + + +/* +=================== +PM_FlyMove + +Only with the flight powerup +=================== +*/ +static void PM_FlyMove( void ) +{ + int i; + vec3_t wishvel; + float wishspeed; + vec3_t wishdir; + float scale; + + // normal slowdown + PM_Friction( ); + + scale = PM_CmdScale( &pm->cmd ); + // + // user intentions + // + if( !scale ) + { + wishvel[ 0 ] = 0; + wishvel[ 1 ] = 0; + wishvel[ 2 ] = 0; + } + else + { + for( i = 0; i < 3; i++ ) + wishvel[ i ] = scale * pml.forward[ i ] * pm->cmd.forwardmove + scale * pml.right[ i ] * pm->cmd.rightmove; + + wishvel[ 2 ] += scale * pm->cmd.upmove; + } + + VectorCopy( wishvel, wishdir ); + wishspeed = VectorNormalize( wishdir ); + + PM_Accelerate( wishdir, wishspeed, pm_flyaccelerate ); + + PM_StepSlideMove( qfalse, qfalse ); +} + + +/* +=================== +PM_AirMove + +=================== +*/ +static void PM_AirMove( void ) +{ + int i; + vec3_t wishvel; + float fmove, smove; + vec3_t wishdir; + float wishspeed; + float scale; + usercmd_t cmd; + + PM_Friction( ); + + fmove = pm->cmd.forwardmove; + smove = pm->cmd.rightmove; + + cmd = pm->cmd; + scale = PM_CmdScale( &cmd ); + + // set the movementDir so clients can rotate the legs for strafing + PM_SetMovementDir( ); + + // project moves down to flat plane + pml.forward[ 2 ] = 0; + pml.right[ 2 ] = 0; + VectorNormalize( pml.forward ); + VectorNormalize( pml.right ); + + for( i = 0; i < 2; i++ ) + wishvel[ i ] = pml.forward[ i ] * fmove + pml.right[ i ] * smove; + + wishvel[ 2 ] = 0; + + VectorCopy( wishvel, wishdir ); + wishspeed = VectorNormalize( wishdir ); + wishspeed *= scale; + + // not on ground, so little effect on velocity + PM_Accelerate( wishdir, wishspeed, + BG_FindAirAccelerationForClass( pm->ps->stats[ STAT_PCLASS ] ) ); + + // we may have a ground plane that is very steep, even + // though we don't have a groundentity + // slide along the steep plane + if( pml.groundPlane ) + PM_ClipVelocity( pm->ps->velocity, pml.groundTrace.plane.normal, + pm->ps->velocity, OVERCLIP ); + + PM_StepSlideMove( qtrue, qfalse ); +} + +/* +=================== +PM_ClimbMove + +=================== +*/ +static void PM_ClimbMove( void ) +{ + int i; + vec3_t wishvel; + float fmove, smove; + vec3_t wishdir; + float wishspeed; + float scale; + usercmd_t cmd; + float accelerate; + float vel; + + if( pm->waterlevel > 2 && DotProduct( pml.forward, pml.groundTrace.plane.normal ) > 0 ) + { + // begin swimming + PM_WaterMove( ); + return; + } + + + if( PM_CheckJump( ) || PM_CheckPounce( ) ) + { + // jumped away + if( pm->waterlevel > 1 ) + PM_WaterMove( ); + else + PM_AirMove( ); + + return; + } + + PM_Friction( ); + + fmove = pm->cmd.forwardmove; + smove = pm->cmd.rightmove; + + cmd = pm->cmd; + scale = PM_CmdScale( &cmd ); + + // set the movementDir so clients can rotate the legs for strafing + PM_SetMovementDir( ); + + // project the forward and right directions onto the ground plane + PM_ClipVelocity( pml.forward, pml.groundTrace.plane.normal, pml.forward, OVERCLIP ); + PM_ClipVelocity( pml.right, pml.groundTrace.plane.normal, pml.right, OVERCLIP ); + // + VectorNormalize( pml.forward ); + VectorNormalize( pml.right ); + + for( i = 0; i < 3; i++ ) + wishvel[ i ] = pml.forward[ i ] * fmove + pml.right[ i ] * smove; + + // when going up or down slopes the wish velocity should Not be zero +// wishvel[2] = 0; + + VectorCopy( wishvel, wishdir ); + wishspeed = VectorNormalize( wishdir ); + wishspeed *= scale; + + // clamp the speed lower if ducking + if( pm->ps->pm_flags & PMF_DUCKED ) + { + if( wishspeed > pm->ps->speed * pm_duckScale ) + wishspeed = pm->ps->speed * pm_duckScale; + } + + // clamp the speed lower if wading or walking on the bottom + if( pm->waterlevel ) + { + float waterScale; + + waterScale = pm->waterlevel / 3.0; + waterScale = 1.0 - ( 1.0 - pm_swimScale ) * waterScale; + if( wishspeed > pm->ps->speed * waterScale ) + wishspeed = pm->ps->speed * waterScale; + } + + // when a player gets hit, they temporarily lose + // full control, which allows them to be moved a bit + if( ( pml.groundTrace.surfaceFlags & SURF_SLICK ) || pm->ps->pm_flags & PMF_TIME_KNOCKBACK ) + accelerate = BG_FindAirAccelerationForClass( pm->ps->stats[ STAT_PCLASS ] ); + else + accelerate = BG_FindAccelerationForClass( pm->ps->stats[ STAT_PCLASS ] ); + + PM_Accelerate( wishdir, wishspeed, accelerate ); + + if( ( pml.groundTrace.surfaceFlags & SURF_SLICK ) || pm->ps->pm_flags & PMF_TIME_KNOCKBACK ) + pm->ps->velocity[ 2 ] -= pm->ps->gravity * pml.frametime; + + vel = VectorLength( pm->ps->velocity ); + + // slide along the ground plane + PM_ClipVelocity( pm->ps->velocity, pml.groundTrace.plane.normal, + pm->ps->velocity, OVERCLIP ); + + // don't decrease velocity when going up or down a slope + VectorNormalize( pm->ps->velocity ); + VectorScale( pm->ps->velocity, vel, pm->ps->velocity ); + + // don't do anything if standing still + if( !pm->ps->velocity[ 0 ] && !pm->ps->velocity[ 1 ] && !pm->ps->velocity[ 2 ] ) + return; + + PM_StepSlideMove( qfalse, qfalse ); +} + + +/* +=================== +PM_WalkMove + +=================== +*/ +static void PM_WalkMove( void ) +{ + int i; + vec3_t wishvel; + float fmove, smove; + vec3_t wishdir; + float wishspeed; + float scale; + usercmd_t cmd; + float accelerate; + + if( pm->waterlevel > 2 && DotProduct( pml.forward, pml.groundTrace.plane.normal ) > 0 ) + { + // begin swimming + PM_WaterMove( ); + return; + } + + + if( PM_CheckJump( ) || PM_CheckPounce( ) ) + { + // jumped away + if( pm->waterlevel > 1 ) + PM_WaterMove( ); + else + PM_AirMove( ); + + return; + } + + //charging + PM_CheckCharge( ); + + PM_Friction( ); + + fmove = pm->cmd.forwardmove; + smove = pm->cmd.rightmove; + + cmd = pm->cmd; + scale = PM_CmdScale( &cmd ); + + // set the movementDir so clients can rotate the legs for strafing + PM_SetMovementDir( ); + + // project moves down to flat plane + pml.forward[ 2 ] = 0; + pml.right[ 2 ] = 0; + + // project the forward and right directions onto the ground plane + PM_ClipVelocity( pml.forward, pml.groundTrace.plane.normal, pml.forward, OVERCLIP ); + PM_ClipVelocity( pml.right, pml.groundTrace.plane.normal, pml.right, OVERCLIP ); + // + VectorNormalize( pml.forward ); + VectorNormalize( pml.right ); + + for( i = 0; i < 3; i++ ) + wishvel[ i ] = pml.forward[ i ] * fmove + pml.right[ i ] * smove; + + // when going up or down slopes the wish velocity should Not be zero +// wishvel[2] = 0; + + VectorCopy( wishvel, wishdir ); + wishspeed = VectorNormalize( wishdir ); + wishspeed *= scale; + + // clamp the speed lower if ducking + if( pm->ps->pm_flags & PMF_DUCKED ) + { + if( wishspeed > pm->ps->speed * pm_duckScale ) + wishspeed = pm->ps->speed * pm_duckScale; + } + + // clamp the speed lower if wading or walking on the bottom + if( pm->waterlevel ) + { + float waterScale; + + waterScale = pm->waterlevel / 3.0; + waterScale = 1.0 - ( 1.0 - pm_swimScale ) * waterScale; + if( wishspeed > pm->ps->speed * waterScale ) + wishspeed = pm->ps->speed * waterScale; + } + + // when a player gets hit, they temporarily lose + // full control, which allows them to be moved a bit + if( ( pml.groundTrace.surfaceFlags & SURF_SLICK ) || pm->ps->pm_flags & PMF_TIME_KNOCKBACK ) + accelerate = BG_FindAirAccelerationForClass( pm->ps->stats[ STAT_PCLASS ] ); + else + accelerate = BG_FindAccelerationForClass( pm->ps->stats[ STAT_PCLASS ] ); + + PM_Accelerate( wishdir, wishspeed, accelerate ); + + //Com_Printf("velocity = %1.1f %1.1f %1.1f\n", pm->ps->velocity[0], pm->ps->velocity[1], pm->ps->velocity[2]); + //Com_Printf("velocity1 = %1.1f\n", VectorLength(pm->ps->velocity)); + + if( ( pml.groundTrace.surfaceFlags & SURF_SLICK ) || pm->ps->pm_flags & PMF_TIME_KNOCKBACK ) + pm->ps->velocity[ 2 ] -= pm->ps->gravity * pml.frametime; + else + { + // don't reset the z velocity for slopes +// pm->ps->velocity[2] = 0; + } + + // slide along the ground plane + PM_ClipVelocity( pm->ps->velocity, pml.groundTrace.plane.normal, + pm->ps->velocity, OVERCLIP ); + + // don't do anything if standing still + if( !pm->ps->velocity[ 0 ] && !pm->ps->velocity[ 1 ] ) + return; + + PM_StepSlideMove( qfalse, qfalse ); + + //Com_Printf("velocity2 = %1.1f\n", VectorLength(pm->ps->velocity)); + +} + + +/* +=================== +PM_LadderMove + +Basically a rip of PM_WaterMove with a few changes +=================== +*/ +static void PM_LadderMove( void ) +{ + int i; + vec3_t wishvel; + float wishspeed; + vec3_t wishdir; + float scale; + float vel; + + PM_Friction( ); + + scale = PM_CmdScale( &pm->cmd ); + + for( i = 0; i < 3; i++ ) + wishvel[ i ] = scale * pml.forward[ i ] * pm->cmd.forwardmove + scale * pml.right[ i ] * pm->cmd.rightmove; + + wishvel[ 2 ] += scale * pm->cmd.upmove; + + VectorCopy( wishvel, wishdir ); + wishspeed = VectorNormalize( wishdir ); + + if( wishspeed > pm->ps->speed * pm_swimScale ) + wishspeed = pm->ps->speed * pm_swimScale; + + PM_Accelerate( wishdir, wishspeed, pm_accelerate ); + + //slanty ladders + if( pml.groundPlane && DotProduct( pm->ps->velocity, pml.groundTrace.plane.normal ) < 0.0f ) + { + vel = VectorLength( pm->ps->velocity ); + + // slide along the ground plane + PM_ClipVelocity( pm->ps->velocity, pml.groundTrace.plane.normal, + pm->ps->velocity, OVERCLIP ); + + VectorNormalize( pm->ps->velocity ); + VectorScale( pm->ps->velocity, vel, pm->ps->velocity ); + } + + PM_SlideMove( qfalse ); +} + + +/* +============= +PM_CheckLadder + +Check to see if the player is on a ladder or not +============= +*/ +static void PM_CheckLadder( void ) +{ + vec3_t forward, end; + trace_t trace; + + //test if class can use ladders + if( !BG_ClassHasAbility( pm->ps->stats[ STAT_PCLASS ], SCA_CANUSELADDERS ) ) + { + pml.ladder = qfalse; + return; + } + + VectorCopy( pml.forward, forward ); + forward[ 2 ] = 0.0f; + + VectorMA( pm->ps->origin, 1.0f, forward, end ); + + pm->trace( &trace, pm->ps->origin, pm->mins, pm->maxs, end, pm->ps->clientNum, MASK_PLAYERSOLID ); + + if( ( trace.fraction < 1.0f ) && ( trace.surfaceFlags & SURF_LADDER ) ) + pml.ladder = qtrue; + else + pml.ladder = qfalse; +} + + +/* +============== +PM_DeadMove +============== +*/ +static void PM_DeadMove( void ) +{ + float forward; + + if( !pml.walking ) + return; + + // extra friction + + forward = VectorLength( pm->ps->velocity ); + forward -= 20; + + if( forward <= 0 ) + VectorClear( pm->ps->velocity ); + else + { + VectorNormalize( pm->ps->velocity ); + VectorScale( pm->ps->velocity, forward, pm->ps->velocity ); + } +} + + +/* +=============== +PM_NoclipMove +=============== +*/ +static void PM_NoclipMove( void ) +{ + float speed, drop, friction, control, newspeed; + int i; + vec3_t wishvel; + float fmove, smove; + vec3_t wishdir; + float wishspeed; + float scale; + + pm->ps->viewheight = DEFAULT_VIEWHEIGHT; + + // friction + + speed = VectorLength( pm->ps->velocity ); + + if( speed < 1 ) + { + VectorCopy( vec3_origin, pm->ps->velocity ); + } + else + { + drop = 0; + + friction = pm_friction * 1.5; // extra friction + control = speed < pm_stopspeed ? pm_stopspeed : speed; + drop += control * friction * pml.frametime; + + // scale the velocity + newspeed = speed - drop; + + if( newspeed < 0 ) + newspeed = 0; + + newspeed /= speed; + + VectorScale( pm->ps->velocity, newspeed, pm->ps->velocity ); + } + + // accelerate + scale = PM_CmdScale( &pm->cmd ); + + fmove = pm->cmd.forwardmove; + smove = pm->cmd.rightmove; + + for( i = 0; i < 3; i++ ) + wishvel[ i ] = pml.forward[ i ] * fmove + pml.right[ i ] * smove; + + wishvel[ 2 ] += pm->cmd.upmove; + + VectorCopy( wishvel, wishdir ); + wishspeed = VectorNormalize( wishdir ); + wishspeed *= scale; + + PM_Accelerate( wishdir, wishspeed, pm_accelerate ); + + // move + VectorMA( pm->ps->origin, pml.frametime, pm->ps->velocity, pm->ps->origin ); +} + +//============================================================================ + +/* +================ +PM_FootstepForSurface + +Returns an event number apropriate for the groundsurface +================ +*/ +static int PM_FootstepForSurface( void ) +{ + //TA: + if( pm->ps->stats[ STAT_STATE ] & SS_CREEPSLOWED ) + return EV_FOOTSTEP_SQUELCH; + + if( pml.groundTrace.surfaceFlags & SURF_NOSTEPS ) + return 0; + + if( pml.groundTrace.surfaceFlags & SURF_METALSTEPS ) + return EV_FOOTSTEP_METAL; + + return EV_FOOTSTEP; +} + + +/* +================= +PM_CrashLand + +Check for hard landings that generate sound events +================= +*/ +static void PM_CrashLand( void ) +{ + float delta; + float dist; + float vel, acc; + float t; + float a, b, c, den; + + // decide which landing animation to use + if( pm->ps->pm_flags & PMF_BACKWARDS_JUMP ) + { + if( !( pm->ps->persistant[ PERS_STATE ] & PS_NONSEGMODEL ) ) + PM_ForceLegsAnim( LEGS_LANDB ); + else + PM_ForceLegsAnim( NSPA_LANDBACK ); + } + else + { + if( !( pm->ps->persistant[ PERS_STATE ] & PS_NONSEGMODEL ) ) + PM_ForceLegsAnim( LEGS_LAND ); + else + PM_ForceLegsAnim( NSPA_LAND ); + } + + if( !( pm->ps->persistant[ PERS_STATE ] & PS_NONSEGMODEL ) ) + pm->ps->legsTimer = TIMER_LAND; + else + pm->ps->torsoTimer = TIMER_LAND; + + // calculate the exact velocity on landing + dist = pm->ps->origin[ 2 ] - pml.previous_origin[ 2 ]; + vel = pml.previous_velocity[ 2 ]; + acc = -pm->ps->gravity; + + a = acc / 2; + b = vel; + c = -dist; + + den = b * b - 4 * a * c; + if( den < 0 ) + return; + + t = (-b - sqrt( den ) ) / ( 2 * a ); + + delta = vel + t * acc; + delta = delta*delta * 0.0001; + + // ducking while falling doubles damage + if( pm->ps->pm_flags & PMF_DUCKED ) + delta *= 2; + + // never take falling damage if completely underwater + if( pm->waterlevel == 3 ) + return; + + // reduce falling damage if there is standing water + if( pm->waterlevel == 2 ) + delta *= 0.25; + + if( pm->waterlevel == 1 ) + delta *= 0.5; + + if( delta < 1 ) + return; + + // create a local entity event to play the sound + + // SURF_NODAMAGE is used for bounce pads where you don't ever + // want to take damage or play a crunch sound + if( !( pml.groundTrace.surfaceFlags & SURF_NODAMAGE ) ) + { + pm->ps->stats[ STAT_FALLDIST ] = delta; + + if( delta > AVG_FALL_DISTANCE ) + { + PM_AddEvent( EV_FALL_FAR ); + } + else if( delta > MIN_FALL_DISTANCE ) + { + // this is a pain grunt, so don't play it if dead + if( pm->ps->stats[STAT_HEALTH] > 0 ) + PM_AddEvent( EV_FALL_MEDIUM ); + } + else + { + if( delta > 7 ) + PM_AddEvent( EV_FALL_SHORT ); + else + PM_AddEvent( PM_FootstepForSurface( ) ); + } + } + + // start footstep cycle over + pm->ps->bobCycle = 0; +} + + +/* +============= +PM_CorrectAllSolid +============= +*/ +static int PM_CorrectAllSolid( trace_t *trace ) +{ + int i, j, k; + vec3_t point; + + if( pm->debugLevel ) + Com_Printf("%i:allsolid\n", c_pmove); + + // jitter around + for( i = -1; i <= 1; i++ ) + { + for( j = -1; j <= 1; j++ ) + { + for( k = -1; k <= 1; k++ ) + { + VectorCopy( pm->ps->origin, point ); + point[ 0 ] += (float)i; + point[ 1 ] += (float)j; + point[ 2 ] += (float)k; + pm->trace( trace, point, pm->mins, pm->maxs, point, pm->ps->clientNum, pm->tracemask ); + + if( !trace->allsolid ) + { + point[ 0 ] = pm->ps->origin[ 0 ]; + point[ 1 ] = pm->ps->origin[ 1 ]; + point[ 2 ] = pm->ps->origin[ 2 ] - 0.25; + + pm->trace( trace, pm->ps->origin, pm->mins, pm->maxs, point, pm->ps->clientNum, pm->tracemask ); + pml.groundTrace = *trace; + return qtrue; + } + } + } + } + + pm->ps->groundEntityNum = ENTITYNUM_NONE; + pml.groundPlane = qfalse; + pml.walking = qfalse; + + return qfalse; +} + + +/* +============= +PM_GroundTraceMissed + +The ground trace didn't hit a surface, so we are in freefall +============= +*/ +static void PM_GroundTraceMissed( void ) +{ + trace_t trace; + vec3_t point; + + if( pm->ps->groundEntityNum != ENTITYNUM_NONE ) + { + // we just transitioned into freefall + if( pm->debugLevel ) + Com_Printf( "%i:lift\n", c_pmove ); + + // if they aren't in a jumping animation and the ground is a ways away, force into it + // if we didn't do the trace, the player would be backflipping down staircases + VectorCopy( pm->ps->origin, point ); + point[ 2 ] -= 64.0f; + + pm->trace( &trace, pm->ps->origin, NULL, NULL, point, pm->ps->clientNum, pm->tracemask ); + if( trace.fraction == 1.0f ) + { + if( pm->cmd.forwardmove >= 0 ) + { + if( !( pm->ps->persistant[ PERS_STATE ] & PS_NONSEGMODEL ) ) + PM_ForceLegsAnim( LEGS_JUMP ); + else + PM_ForceLegsAnim( NSPA_JUMP ); + + pm->ps->pm_flags &= ~PMF_BACKWARDS_JUMP; + } + else + { + if( !( pm->ps->persistant[ PERS_STATE ] & PS_NONSEGMODEL ) ) + PM_ForceLegsAnim( LEGS_JUMPB ); + else + PM_ForceLegsAnim( NSPA_JUMPBACK ); + + pm->ps->pm_flags |= PMF_BACKWARDS_JUMP; + } + } + } + + if( BG_ClassHasAbility( pm->ps->stats[ STAT_PCLASS ], SCA_TAKESFALLDAMAGE ) ) + { + if( pm->ps->velocity[ 2 ] < FALLING_THRESHOLD && pml.previous_velocity[ 2 ] >= FALLING_THRESHOLD ) + PM_AddEvent( EV_FALLING ); + } + + pm->ps->groundEntityNum = ENTITYNUM_NONE; + pml.groundPlane = qfalse; + pml.walking = qfalse; +} + + +/* +============= +PM_GroundClimbTrace +============= +*/ +static void PM_GroundClimbTrace( void ) +{ + vec3_t surfNormal, movedir, lookdir, point; + vec3_t refNormal = { 0.0f, 0.0f, 1.0f }; + vec3_t ceilingNormal = { 0.0f, 0.0f, -1.0f }; + vec3_t toAngles, surfAngles; + trace_t trace; + int i; + + //used for delta correction + vec3_t traceCROSSsurf, traceCROSSref, surfCROSSref; + float traceDOTsurf, traceDOTref, surfDOTref, rTtDOTrTsTt; + float traceANGsurf, traceANGref, surfANGref; + vec3_t horizontal = { 1.0f, 0.0f, 0.0f }; //arbituary vector perpendicular to refNormal + vec3_t refTOtrace, refTOsurfTOtrace, tempVec; + int rTtANGrTsTt; + float ldDOTtCs, d; + vec3_t abc; + + //TA: If we're on the ceiling then grapplePoint is a rotation normal.. otherwise its a surface normal. + // would have been nice if Carmack had left a few random variables in the ps struct for mod makers + if( pm->ps->stats[ STAT_STATE ] & SS_WALLCLIMBINGCEILING ) + VectorCopy( ceilingNormal, surfNormal ); + else + VectorCopy( pm->ps->grapplePoint, surfNormal ); + + //construct a vector which reflects the direction the player is looking wrt the surface normal + ProjectPointOnPlane( movedir, pml.forward, surfNormal ); + VectorNormalize( movedir ); + + VectorCopy( movedir, lookdir ); + + if( pm->cmd.forwardmove < 0 ) + VectorNegate( movedir, movedir ); + + //allow strafe transitions + if( pm->cmd.rightmove ) + { + VectorCopy( pml.right, movedir ); + + if( pm->cmd.rightmove < 0 ) + VectorNegate( movedir, movedir ); + } + + for( i = 0; i <= 4; i++ ) + { + switch ( i ) + { + case 0: + //we are going to step this frame so skip the transition test + if( PM_PredictStepMove( ) ) + continue; + + //trace into direction we are moving + VectorMA( pm->ps->origin, 0.25f, movedir, point ); + pm->trace( &trace, pm->ps->origin, pm->mins, pm->maxs, point, pm->ps->clientNum, pm->tracemask ); + break; + + case 1: + //trace straight down anto "ground" surface + VectorMA( pm->ps->origin, -0.25f, surfNormal, point ); + pm->trace( &trace, pm->ps->origin, pm->mins, pm->maxs, point, pm->ps->clientNum, pm->tracemask ); + break; + + case 2: + if( pml.groundPlane != qfalse && PM_PredictStepMove( ) ) + { + //step down + VectorMA( pm->ps->origin, -STEPSIZE, surfNormal, point ); + pm->trace( &trace, pm->ps->origin, pm->mins, pm->maxs, point, pm->ps->clientNum, pm->tracemask ); + } + else + continue; + break; + + case 3: + //trace "underneath" BBOX so we can traverse angles > 180deg + if( pml.groundPlane != qfalse ) + { + VectorMA( pm->ps->origin, -16.0f, surfNormal, point ); + VectorMA( point, -16.0f, movedir, point ); + pm->trace( &trace, pm->ps->origin, pm->mins, pm->maxs, point, pm->ps->clientNum, pm->tracemask ); + } + else + continue; + break; + + case 4: + //fall back so we don't have to modify PM_GroundTrace too much + VectorCopy( pm->ps->origin, point ); + point[ 2 ] = pm->ps->origin[ 2 ] - 0.25f; + pm->trace( &trace, pm->ps->origin, pm->mins, pm->maxs, point, pm->ps->clientNum, pm->tracemask ); + break; + } + + //if we hit something + if( trace.fraction < 1.0f && !( trace.surfaceFlags & ( SURF_SKY | SURF_SLICK ) ) && + !( trace.entityNum != ENTITYNUM_WORLD && i != 4 ) ) + { + if( i == 2 || i == 3 ) + { + if( i == 2 ) + PM_StepEvent( pm->ps->origin, trace.endpos, surfNormal ); + + VectorCopy( trace.endpos, pm->ps->origin ); + } + + //calculate a bunch of stuff... + CrossProduct( trace.plane.normal, surfNormal, traceCROSSsurf ); + VectorNormalize( traceCROSSsurf ); + + CrossProduct( trace.plane.normal, refNormal, traceCROSSref ); + VectorNormalize( traceCROSSref ); + + CrossProduct( surfNormal, refNormal, surfCROSSref ); + VectorNormalize( surfCROSSref ); + + //calculate angle between surf and trace + traceDOTsurf = DotProduct( trace.plane.normal, surfNormal ); + traceANGsurf = RAD2DEG( acos( traceDOTsurf ) ); + + if( traceANGsurf > 180.0f ) + traceANGsurf -= 180.0f; + + //calculate angle between trace and ref + traceDOTref = DotProduct( trace.plane.normal, refNormal ); + traceANGref = RAD2DEG( acos( traceDOTref ) ); + + if( traceANGref > 180.0f ) + traceANGref -= 180.0f; + + //calculate angle between surf and ref + surfDOTref = DotProduct( surfNormal, refNormal ); + surfANGref = RAD2DEG( acos( surfDOTref ) ); + + if( surfANGref > 180.0f ) + surfANGref -= 180.0f; + + //if the trace result and old surface normal are different then we must have transided to a new + //surface... do some stuff... + if( !VectorCompare( trace.plane.normal, surfNormal ) ) + { + //if the trace result or the old vector is not the floor or ceiling correct the YAW angle + if( !VectorCompare( trace.plane.normal, refNormal ) && !VectorCompare( surfNormal, refNormal ) && + !VectorCompare( trace.plane.normal, ceilingNormal ) && !VectorCompare( surfNormal, ceilingNormal ) ) + { + //behold the evil mindfuck from hell + //it has fucked mind like nothing has fucked mind before + + //calculate reference rotated through to trace plane + RotatePointAroundVector( refTOtrace, traceCROSSref, horizontal, -traceANGref ); + + //calculate reference rotated through to surf plane then to trace plane + RotatePointAroundVector( tempVec, surfCROSSref, horizontal, -surfANGref ); + RotatePointAroundVector( refTOsurfTOtrace, traceCROSSsurf, tempVec, -traceANGsurf ); + + //calculate angle between refTOtrace and refTOsurfTOtrace + rTtDOTrTsTt = DotProduct( refTOtrace, refTOsurfTOtrace ); + rTtANGrTsTt = ANGLE2SHORT( RAD2DEG( acos( rTtDOTrTsTt ) ) ); + + if( rTtANGrTsTt > 32768 ) + rTtANGrTsTt -= 32768; + + CrossProduct( refTOtrace, refTOsurfTOtrace, tempVec ); + VectorNormalize( tempVec ); + if( DotProduct( trace.plane.normal, tempVec ) > 0.0f ) + rTtANGrTsTt = -rTtANGrTsTt; + + //phew! - correct the angle + pm->ps->delta_angles[ YAW ] -= rTtANGrTsTt; + } + + //construct a plane dividing the surf and trace normals + CrossProduct( traceCROSSsurf, surfNormal, abc ); + VectorNormalize( abc ); + d = DotProduct( abc, pm->ps->origin ); + + //construct a point representing where the player is looking + VectorAdd( pm->ps->origin, lookdir, point ); + + //check whether point is on one side of the plane, if so invert the correction angle + if( ( abc[ 0 ] * point[ 0 ] + abc[ 1 ] * point[ 1 ] + abc[ 2 ] * point[ 2 ] - d ) > 0 ) + traceANGsurf = -traceANGsurf; + + //find the . product of the lookdir and traceCROSSsurf + if( ( ldDOTtCs = DotProduct( lookdir, traceCROSSsurf ) ) < 0.0f ) + { + VectorInverse( traceCROSSsurf ); + ldDOTtCs = DotProduct( lookdir, traceCROSSsurf ); + } + + //set the correction angle + traceANGsurf *= 1.0f - ldDOTtCs; + + if( !( pm->ps->persistant[ PERS_STATE ] & PS_WALLCLIMBINGFOLLOW ) ) + { + //correct the angle + pm->ps->delta_angles[ PITCH ] -= ANGLE2SHORT( traceANGsurf ); + } + + //transition from wall to ceiling + //normal for subsequent viewangle rotations + if( VectorCompare( trace.plane.normal, ceilingNormal ) ) + { + CrossProduct( surfNormal, trace.plane.normal, pm->ps->grapplePoint ); + VectorNormalize( pm->ps->grapplePoint ); + pm->ps->stats[ STAT_STATE ] |= SS_WALLCLIMBINGCEILING; + } + + //transition from ceiling to wall + //we need to do some different angle correction here cos GPISROTVEC + if( VectorCompare( surfNormal, ceilingNormal ) ) + { + vectoangles( trace.plane.normal, toAngles ); + vectoangles( pm->ps->grapplePoint, surfAngles ); + + pm->ps->delta_angles[ 1 ] -= ANGLE2SHORT( ( ( surfAngles[ 1 ] - toAngles[ 1 ] ) * 2 ) - 180.0f ); + } + } + + pml.groundTrace = trace; + + //so everything knows where we're wallclimbing (ie client side) + pm->ps->eFlags |= EF_WALLCLIMB; + + //if we're not stuck to the ceiling then set grapplePoint to be a surface normal + if( !VectorCompare( trace.plane.normal, ceilingNormal ) ) + { + //so we know what surface we're stuck to + VectorCopy( trace.plane.normal, pm->ps->grapplePoint ); + pm->ps->stats[ STAT_STATE ] &= ~SS_WALLCLIMBINGCEILING; + } + + //IMPORTANT: break out of the for loop if we've hit something + break; + } + else if( trace.allsolid ) + { + // do something corrective if the trace starts in a solid... + if( !PM_CorrectAllSolid( &trace ) ) + return; + } + } + + if( trace.fraction >= 1.0f ) + { + // if the trace didn't hit anything, we are in free fall + PM_GroundTraceMissed( ); + pml.groundPlane = qfalse; + pml.walking = qfalse; + pm->ps->eFlags &= ~EF_WALLCLIMB; + + //just transided from ceiling to floor... apply delta correction + if( pm->ps->stats[ STAT_STATE ] & SS_WALLCLIMBINGCEILING ) + { + vec3_t forward, rotated, angles; + + AngleVectors( pm->ps->viewangles, forward, NULL, NULL ); + + RotatePointAroundVector( rotated, pm->ps->grapplePoint, forward, 180.0f ); + vectoangles( rotated, angles ); + + pm->ps->delta_angles[ YAW ] -= ANGLE2SHORT( angles[ YAW ] - pm->ps->viewangles[ YAW ] ); + } + + pm->ps->stats[ STAT_STATE ] &= ~SS_WALLCLIMBINGCEILING; + + //we get very bizarre effects if we don't do this :0 + VectorCopy( refNormal, pm->ps->grapplePoint ); + return; + } + + pml.groundPlane = qtrue; + pml.walking = qtrue; + + // hitting solid ground will end a waterjump + if( pm->ps->pm_flags & PMF_TIME_WATERJUMP ) + { + pm->ps->pm_flags &= ~(PMF_TIME_WATERJUMP | PMF_TIME_LAND); + pm->ps->pm_time = 0; + } + + pm->ps->groundEntityNum = trace.entityNum; + + // don't reset the z velocity for slopes +// pm->ps->velocity[2] = 0; + + PM_AddTouchEnt( trace.entityNum ); +} + + +/* +============= +PM_GroundTrace +============= +*/ +static void PM_GroundTrace( void ) +{ + vec3_t point; + vec3_t movedir; + vec3_t refNormal = { 0.0f, 0.0f, 1.0f }; + trace_t trace; + + if( BG_ClassHasAbility( pm->ps->stats[ STAT_PCLASS ], SCA_WALLCLIMBER ) ) + { + if( pm->ps->persistant[ PERS_STATE ] & PS_WALLCLIMBINGTOGGLE ) + { + //toggle wall climbing if holding crouch + if( pm->cmd.upmove < 0 && !( pm->ps->pm_flags & PMF_CROUCH_HELD ) ) + { + if( !( pm->ps->stats[ STAT_STATE ] & SS_WALLCLIMBING ) ) + pm->ps->stats[ STAT_STATE ] |= SS_WALLCLIMBING; + else if( pm->ps->stats[ STAT_STATE ] & SS_WALLCLIMBING ) + pm->ps->stats[ STAT_STATE ] &= ~SS_WALLCLIMBING; + + pm->ps->pm_flags |= PMF_CROUCH_HELD; + } + else if( pm->cmd.upmove >= 0 ) + pm->ps->pm_flags &= ~PMF_CROUCH_HELD; + } + else + { + if( pm->cmd.upmove < 0 ) + pm->ps->stats[ STAT_STATE ] |= SS_WALLCLIMBING; + else if( pm->cmd.upmove >= 0 ) + pm->ps->stats[ STAT_STATE ] &= ~SS_WALLCLIMBING; + } + + if( pm->ps->pm_type == PM_DEAD ) + pm->ps->stats[ STAT_STATE ] &= ~SS_WALLCLIMBING; + + if( pm->ps->stats[ STAT_STATE ] & SS_WALLCLIMBING ) + { + PM_GroundClimbTrace( ); + return; + } + + //just transided from ceiling to floor... apply delta correction + if( pm->ps->stats[ STAT_STATE ] & SS_WALLCLIMBINGCEILING ) + { + vec3_t forward, rotated, angles; + + AngleVectors( pm->ps->viewangles, forward, NULL, NULL ); + + RotatePointAroundVector( rotated, pm->ps->grapplePoint, forward, 180.0f ); + vectoangles( rotated, angles ); + + pm->ps->delta_angles[ YAW ] -= ANGLE2SHORT( angles[ YAW ] - pm->ps->viewangles[ YAW ] ); + } + } + + pm->ps->stats[ STAT_STATE ] &= ~SS_WALLCLIMBING; + pm->ps->stats[ STAT_STATE ] &= ~SS_WALLCLIMBINGCEILING; + pm->ps->eFlags &= ~EF_WALLCLIMB; + + point[ 0 ] = pm->ps->origin[ 0 ]; + point[ 1 ] = pm->ps->origin[ 1 ]; + point[ 2 ] = pm->ps->origin[ 2 ] - 0.25f; + + pm->trace( &trace, pm->ps->origin, pm->mins, pm->maxs, point, pm->ps->clientNum, pm->tracemask ); + + pml.groundTrace = trace; + + // do something corrective if the trace starts in a solid... + if( trace.allsolid ) + if( !PM_CorrectAllSolid( &trace ) ) + return; + + //make sure that the surfNormal is reset to the ground + VectorCopy( refNormal, pm->ps->grapplePoint ); + + // if the trace didn't hit anything, we are in free fall + if( trace.fraction == 1.0f ) + { + qboolean steppedDown = qfalse; + + // try to step down + if( pml.groundPlane != qfalse && PM_PredictStepMove( ) ) + { + //step down + point[ 0 ] = pm->ps->origin[ 0 ]; + point[ 1 ] = pm->ps->origin[ 1 ]; + point[ 2 ] = pm->ps->origin[ 2 ] - STEPSIZE; + pm->trace( &trace, pm->ps->origin, pm->mins, pm->maxs, point, pm->ps->clientNum, pm->tracemask ); + + //if we hit something + if( trace.fraction < 1.0f ) + { + PM_StepEvent( pm->ps->origin, trace.endpos, refNormal ); + VectorCopy( trace.endpos, pm->ps->origin ); + steppedDown = qtrue; + } + } + + if( !steppedDown ) + { + PM_GroundTraceMissed( ); + pml.groundPlane = qfalse; + pml.walking = qfalse; + + if( BG_ClassHasAbility( pm->ps->stats[ STAT_PCLASS ], SCA_WALLJUMPER ) ) + { + ProjectPointOnPlane( movedir, pml.forward, refNormal ); + VectorNormalize( movedir ); + + if( pm->cmd.forwardmove < 0 ) + VectorNegate( movedir, movedir ); + + //allow strafe transitions + if( pm->cmd.rightmove ) + { + VectorCopy( pml.right, movedir ); + + if( pm->cmd.rightmove < 0 ) + VectorNegate( movedir, movedir ); + } + + //trace into direction we are moving + VectorMA( pm->ps->origin, 0.25f, movedir, point ); + pm->trace( &trace, pm->ps->origin, pm->mins, pm->maxs, point, pm->ps->clientNum, pm->tracemask ); + + if( trace.fraction < 1.0f && !( trace.surfaceFlags & ( SURF_SKY | SURF_SLICK ) ) && + ( trace.entityNum == ENTITYNUM_WORLD ) ) + { + if( !VectorCompare( trace.plane.normal, pm->ps->grapplePoint ) ) + { + VectorCopy( trace.plane.normal, pm->ps->grapplePoint ); + PM_CheckWallJump( ); + } + } + } + + return; + } + } + + // check if getting thrown off the ground + if( pm->ps->velocity[ 2 ] > 0.0f && DotProduct( pm->ps->velocity, trace.plane.normal ) > 10.0f ) + { + if( pm->debugLevel ) + Com_Printf( "%i:kickoff\n", c_pmove ); + + // go into jump animation + if( pm->cmd.forwardmove >= 0 ) + { + if( !( pm->ps->persistant[ PERS_STATE ] & PS_NONSEGMODEL ) ) + PM_ForceLegsAnim( LEGS_JUMP ); + else + PM_ForceLegsAnim( NSPA_JUMP ); + + pm->ps->pm_flags &= ~PMF_BACKWARDS_JUMP; + } + else + { + if( !( pm->ps->persistant[ PERS_STATE ] & PS_NONSEGMODEL ) ) + PM_ForceLegsAnim( LEGS_JUMPB ); + else + PM_ForceLegsAnim( NSPA_JUMPBACK ); + + pm->ps->pm_flags |= PMF_BACKWARDS_JUMP; + } + + pm->ps->groundEntityNum = ENTITYNUM_NONE; + pml.groundPlane = qfalse; + pml.walking = qfalse; + return; + } + + // slopes that are too steep will not be considered onground + if( trace.plane.normal[ 2 ] < MIN_WALK_NORMAL ) + { + if( pm->debugLevel ) + Com_Printf( "%i:steep\n", c_pmove ); + + // FIXME: if they can't slide down the slope, let them + // walk (sharp crevices) + pm->ps->groundEntityNum = ENTITYNUM_NONE; + pml.groundPlane = qtrue; + pml.walking = qfalse; + return; + } + + pml.groundPlane = qtrue; + pml.walking = qtrue; + + // hitting solid ground will end a waterjump + if( pm->ps->pm_flags & PMF_TIME_WATERJUMP ) + { + pm->ps->pm_flags &= ~( PMF_TIME_WATERJUMP | PMF_TIME_LAND ); + pm->ps->pm_time = 0; + } + + if( pm->ps->groundEntityNum == ENTITYNUM_NONE ) + { + // just hit the ground + if( pm->debugLevel ) + Com_Printf( "%i:Land\n", c_pmove ); + + if( BG_ClassHasAbility( pm->ps->stats[ STAT_PCLASS ], SCA_TAKESFALLDAMAGE ) ) + PM_CrashLand( ); + + // don't do landing time if we were just going down a slope + if( pml.previous_velocity[ 2 ] < -200 ) + { + // don't allow another jump for a little while + pm->ps->pm_flags |= PMF_TIME_LAND; + pm->ps->pm_time = 250; + } + } + + pm->ps->groundEntityNum = trace.entityNum; + + // don't reset the z velocity for slopes +// pm->ps->velocity[2] = 0; + + PM_AddTouchEnt( trace.entityNum ); +} + + +/* +============= +PM_SetWaterLevel FIXME: avoid this twice? certainly if not moving +============= +*/ +static void PM_SetWaterLevel( void ) +{ + vec3_t point; + int cont; + int sample1; + int sample2; + + // + // get waterlevel, accounting for ducking + // + pm->waterlevel = 0; + pm->watertype = 0; + + point[ 0 ] = pm->ps->origin[ 0 ]; + point[ 1 ] = pm->ps->origin[ 1 ]; + point[ 2 ] = pm->ps->origin[ 2 ] + MINS_Z + 1; + cont = pm->pointcontents( point, pm->ps->clientNum ); + + if( cont & MASK_WATER ) + { + sample2 = pm->ps->viewheight - MINS_Z; + sample1 = sample2 / 2; + + pm->watertype = cont; + pm->waterlevel = 1; + point[ 2 ] = pm->ps->origin[ 2 ] + MINS_Z + sample1; + cont = pm->pointcontents( point, pm->ps->clientNum ); + + if( cont & MASK_WATER ) + { + pm->waterlevel = 2; + point[ 2 ] = pm->ps->origin[ 2 ] + MINS_Z + sample2; + cont = pm->pointcontents( point, pm->ps->clientNum ); + + if( cont & MASK_WATER ) + pm->waterlevel = 3; + } + } +} + + + +/* +============== +PM_CheckDuck + +Sets mins, maxs, and pm->ps->viewheight +============== +*/ +static void PM_CheckDuck (void) +{ + trace_t trace; + vec3_t PCmins, PCmaxs, PCcmaxs; + int PCvh, PCcvh; + + BG_FindBBoxForClass( pm->ps->stats[ STAT_PCLASS ], PCmins, PCmaxs, PCcmaxs, NULL, NULL ); + BG_FindViewheightForClass( pm->ps->stats[ STAT_PCLASS ], &PCvh, &PCcvh ); + + //TA: iD bug? you can still crouch when you're a spectator + if( pm->ps->persistant[ PERS_TEAM ] == TEAM_SPECTATOR ) + PCcvh = PCvh; + + pm->mins[ 0 ] = PCmins[ 0 ]; + pm->mins[ 1 ] = PCmins[ 1 ]; + + pm->maxs[ 0 ] = PCmaxs[ 0 ]; + pm->maxs[ 1 ] = PCmaxs[ 1 ]; + + pm->mins[ 2 ] = PCmins[ 2 ]; + + if( pm->ps->pm_type == PM_DEAD ) + { + pm->maxs[ 2 ] = -8; + pm->ps->viewheight = DEAD_VIEWHEIGHT; + return; + } + + //TA: If the standing and crouching viewheights are the same the class can't crouch + if( ( pm->cmd.upmove < 0 ) && ( PCvh != PCcvh ) && + pm->ps->pm_type != PM_JETPACK && + !BG_InventoryContainsUpgrade( UP_BATTLESUIT, pm->ps->stats ) ) + { + // duck + pm->ps->pm_flags |= PMF_DUCKED; + } + else + { + // stand up if possible + if( pm->ps->pm_flags & PMF_DUCKED ) + { + // try to stand up + pm->maxs[ 2 ] = PCmaxs[ 2 ]; + pm->trace( &trace, pm->ps->origin, pm->mins, pm->maxs, pm->ps->origin, pm->ps->clientNum, pm->tracemask ); + if( !trace.allsolid ) + pm->ps->pm_flags &= ~PMF_DUCKED; + } + } + + if( pm->ps->pm_flags & PMF_DUCKED ) + { + pm->maxs[ 2 ] = PCcmaxs[ 2 ]; + pm->ps->viewheight = PCcvh; + } + else + { + pm->maxs[ 2 ] = PCmaxs[ 2 ]; + pm->ps->viewheight = PCvh; + } +} + + + +//=================================================================== + + +/* +=============== +PM_Footsteps +=============== +*/ +static void PM_Footsteps( void ) +{ + float bobmove; + int old; + qboolean footstep; + + // + // calculate speed and cycle to be used for + // all cyclic walking effects + // + if( BG_ClassHasAbility( pm->ps->stats[ STAT_PCLASS ], SCA_WALLCLIMBER ) && ( pml.groundPlane ) ) + { + //TA: FIXME: yes yes i know this is wrong + pm->xyspeed = sqrt( pm->ps->velocity[ 0 ] * pm->ps->velocity[ 0 ] + + pm->ps->velocity[ 1 ] * pm->ps->velocity[ 1 ] + + pm->ps->velocity[ 2 ] * pm->ps->velocity[ 2 ] ); + } + else + pm->xyspeed = sqrt( pm->ps->velocity[ 0 ] * pm->ps->velocity[ 0 ] + + pm->ps->velocity[ 1 ] * pm->ps->velocity[ 1 ] ); + + if( pm->ps->groundEntityNum == ENTITYNUM_NONE ) + { + // airborne leaves position in cycle intact, but doesn't advance + if( pm->waterlevel > 1 ) + { + if( !( pm->ps->persistant[ PERS_STATE ] & PS_NONSEGMODEL ) ) + PM_ContinueLegsAnim( LEGS_SWIM ); + else + PM_ContinueLegsAnim( NSPA_SWIM ); + } + + return; + } + + // if not trying to move + if( !pm->cmd.forwardmove && !pm->cmd.rightmove ) + { + if( pm->xyspeed < 5 ) + { + pm->ps->bobCycle = 0; // start at beginning of cycle again + if( pm->ps->pm_flags & PMF_DUCKED ) + { + if( !( pm->ps->persistant[ PERS_STATE ] & PS_NONSEGMODEL ) ) + PM_ContinueLegsAnim( LEGS_IDLECR ); + else + PM_ContinueLegsAnim( NSPA_STAND ); + } + else + { + if( !( pm->ps->persistant[ PERS_STATE ] & PS_NONSEGMODEL ) ) + PM_ContinueLegsAnim( LEGS_IDLE ); + else + PM_ContinueLegsAnim( NSPA_STAND ); + } + } + return; + } + + + footstep = qfalse; + + if( pm->ps->pm_flags & PMF_DUCKED ) + { + bobmove = 0.5; // ducked characters bob much faster + + if( pm->ps->pm_flags & PMF_BACKWARDS_RUN ) + { + if( !( pm->ps->persistant[ PERS_STATE ] & PS_NONSEGMODEL ) ) + PM_ContinueLegsAnim( LEGS_BACKCR ); + else + { + if( pm->cmd.rightmove > 0 && !pm->cmd.forwardmove ) + PM_ContinueLegsAnim( NSPA_WALKRIGHT ); + else if( pm->cmd.rightmove < 0 && !pm->cmd.forwardmove ) + PM_ContinueLegsAnim( NSPA_WALKLEFT ); + else + PM_ContinueLegsAnim( NSPA_WALKBACK ); + } + } + else + { + if( !( pm->ps->persistant[ PERS_STATE ] & PS_NONSEGMODEL ) ) + PM_ContinueLegsAnim( LEGS_WALKCR ); + else + { + if( pm->cmd.rightmove > 0 && !pm->cmd.forwardmove ) + PM_ContinueLegsAnim( NSPA_WALKRIGHT ); + else if( pm->cmd.rightmove < 0 && !pm->cmd.forwardmove ) + PM_ContinueLegsAnim( NSPA_WALKLEFT ); + else + PM_ContinueLegsAnim( NSPA_WALK ); + } + } + + // ducked characters never play footsteps + } + else + { + if( !( pm->cmd.buttons & BUTTON_WALKING ) ) + { + bobmove = 0.4f; // faster speeds bob faster + + if( pm->ps->weapon == WP_ALEVEL4 && pm->ps->pm_flags & PMF_CHARGE ) + PM_ContinueLegsAnim( NSPA_CHARGE ); + else if( pm->ps->pm_flags & PMF_BACKWARDS_RUN ) + { + if( !( pm->ps->persistant[ PERS_STATE ] & PS_NONSEGMODEL ) ) + PM_ContinueLegsAnim( LEGS_BACK ); + else + { + if( pm->cmd.rightmove > 0 && !pm->cmd.forwardmove ) + PM_ContinueLegsAnim( NSPA_RUNRIGHT ); + else if( pm->cmd.rightmove < 0 && !pm->cmd.forwardmove ) + PM_ContinueLegsAnim( NSPA_RUNLEFT ); + else + PM_ContinueLegsAnim( NSPA_RUNBACK ); + } + } + else + { + if( !( pm->ps->persistant[ PERS_STATE ] & PS_NONSEGMODEL ) ) + PM_ContinueLegsAnim( LEGS_RUN ); + else + { + if( pm->cmd.rightmove > 0 && !pm->cmd.forwardmove ) + PM_ContinueLegsAnim( NSPA_RUNRIGHT ); + else if( pm->cmd.rightmove < 0 && !pm->cmd.forwardmove ) + PM_ContinueLegsAnim( NSPA_RUNLEFT ); + else + PM_ContinueLegsAnim( NSPA_RUN ); + } + } + + footstep = qtrue; + } + else + { + bobmove = 0.3f; // walking bobs slow + if( pm->ps->pm_flags & PMF_BACKWARDS_RUN ) + { + if( !( pm->ps->persistant[ PERS_STATE ] & PS_NONSEGMODEL ) ) + PM_ContinueLegsAnim( LEGS_BACKWALK ); + else + { + if( pm->cmd.rightmove > 0 && !pm->cmd.forwardmove ) + PM_ContinueLegsAnim( NSPA_WALKRIGHT ); + else if( pm->cmd.rightmove < 0 && !pm->cmd.forwardmove ) + PM_ContinueLegsAnim( NSPA_WALKLEFT ); + else + PM_ContinueLegsAnim( NSPA_WALKBACK ); + } + } + else + { + if( !( pm->ps->persistant[ PERS_STATE ] & PS_NONSEGMODEL ) ) + PM_ContinueLegsAnim( LEGS_WALK ); + else + { + if( pm->cmd.rightmove > 0 && !pm->cmd.forwardmove ) + PM_ContinueLegsAnim( NSPA_WALKRIGHT ); + else if( pm->cmd.rightmove < 0 && !pm->cmd.forwardmove ) + PM_ContinueLegsAnim( NSPA_WALKLEFT ); + else + PM_ContinueLegsAnim( NSPA_WALK ); + } + } + } + } + + bobmove *= BG_FindBobCycleForClass( pm->ps->stats[ STAT_PCLASS ] ); + + if( pm->ps->stats[ STAT_STATE ] & SS_SPEEDBOOST ) + bobmove *= HUMAN_SPRINT_MODIFIER; + + // check for footstep / splash sounds + old = pm->ps->bobCycle; + pm->ps->bobCycle = (int)( old + bobmove * pml.msec ) & 255; + + // if we just crossed a cycle boundary, play an apropriate footstep event + if( ( ( old + 64 ) ^ ( pm->ps->bobCycle + 64 ) ) & 128 ) + { + if( pm->waterlevel == 0 ) + { + // on ground will only play sounds if running + if( footstep && !pm->noFootsteps ) + PM_AddEvent( PM_FootstepForSurface( ) ); + } + else if( pm->waterlevel == 1 ) + { + // splashing + PM_AddEvent( EV_FOOTSPLASH ); + } + else if( pm->waterlevel == 2 ) + { + // wading / swimming at surface + PM_AddEvent( EV_SWIM ); + } + else if( pm->waterlevel == 3 ) + { + // no sound when completely underwater + } + } +} + +/* +============== +PM_WaterEvents + +Generate sound events for entering and leaving water +============== +*/ +static void PM_WaterEvents( void ) +{ + // FIXME? + // + // if just entered a water volume, play a sound + // + if( !pml.previous_waterlevel && pm->waterlevel ) + PM_AddEvent( EV_WATER_TOUCH ); + + // + // if just completely exited a water volume, play a sound + // + if( pml.previous_waterlevel && !pm->waterlevel ) + PM_AddEvent( EV_WATER_LEAVE ); + + // + // check for head just going under water + // + if( pml.previous_waterlevel != 3 && pm->waterlevel == 3 ) + PM_AddEvent( EV_WATER_UNDER ); + + // + // check for head just coming out of water + // + if( pml.previous_waterlevel == 3 && pm->waterlevel != 3 ) + PM_AddEvent( EV_WATER_CLEAR ); +} + + +/* +=============== +PM_BeginWeaponChange +=============== +*/ +static void PM_BeginWeaponChange( int weapon ) +{ + if( weapon <= WP_NONE || weapon >= WP_NUM_WEAPONS ) + return; + + if( !BG_InventoryContainsWeapon( weapon, pm->ps->stats ) ) + return; + + if( pm->ps->weaponstate == WEAPON_DROPPING ) + return; + + //special case to prevent storing a charged up lcannon + if( pm->ps->weapon == WP_LUCIFER_CANNON ) + pm->ps->stats[ STAT_MISC ] = 0; + + + // force this here to prevent flamer effect from continuing, among other issues + pm->ps->generic1 = WPM_NOTFIRING; + + PM_AddEvent( EV_CHANGE_WEAPON ); + pm->ps->weaponstate = WEAPON_DROPPING; + pm->ps->weaponTime += 200; + pm->ps->persistant[ PERS_NEWWEAPON ] = weapon; + + //reset build weapon + pm->ps->stats[ STAT_BUILDABLE ] = BA_NONE; + + if( !( pm->ps->persistant[ PERS_STATE ] & PS_NONSEGMODEL ) ) + PM_StartTorsoAnim( TORSO_DROP ); +} + + +/* +=============== +PM_FinishWeaponChange +=============== +*/ +static void PM_FinishWeaponChange( void ) +{ + int weapon; + + weapon = pm->ps->persistant[ PERS_NEWWEAPON ]; + if( weapon < WP_NONE || weapon >= WP_NUM_WEAPONS ) + weapon = WP_NONE; + + if( !BG_InventoryContainsWeapon( weapon, pm->ps->stats ) ) + weapon = WP_NONE; + + pm->ps->weapon = weapon; + pm->ps->weaponstate = WEAPON_RAISING; + pm->ps->weaponTime += 250; + + if( !( pm->ps->persistant[ PERS_STATE ] & PS_NONSEGMODEL ) ) + PM_StartTorsoAnim( TORSO_RAISE ); +} + + +/* +============== +PM_TorsoAnimation + +============== +*/ +static void PM_TorsoAnimation( void ) +{ + if( pm->ps->persistant[ PERS_STATE ] & PS_NONSEGMODEL ) + return; + + if( pm->ps->weaponstate == WEAPON_READY ) + { + if( pm->ps->weapon == WP_BLASTER ) + PM_ContinueTorsoAnim( TORSO_STAND2 ); + else + PM_ContinueTorsoAnim( TORSO_STAND ); + } +} + + +/* +============== +PM_Weapon + +Generates weapon events and modifes the weapon counter +============== +*/ +static void PM_Weapon( void ) +{ + int addTime = 200; //default addTime - should never be used + int ammo, clips, maxClips; + qboolean attack1 = qfalse; + qboolean attack2 = qfalse; + qboolean attack3 = qfalse; + + // don't allow attack until all buttons are up + if( pm->ps->pm_flags & PMF_RESPAWNED ) + return; + + // ignore if spectator + if( pm->ps->persistant[ PERS_TEAM ] == TEAM_SPECTATOR ) + return; + + if( pm->ps->stats[ STAT_STATE ] & SS_INFESTING ) + return; + + if( pm->ps->stats[ STAT_STATE ] & SS_HOVELING ) + return; + + // check for dead player + if( pm->ps->stats[ STAT_HEALTH ] <= 0 ) + { + pm->ps->weapon = WP_NONE; + return; + } + + + // no bite during pounce + if( ( pm->ps->weapon == WP_ALEVEL3 || pm->ps->weapon == WP_ALEVEL3_UPG ) + && ( pm->cmd.buttons & BUTTON_ATTACK ) + && ( pm->ps->pm_flags & PMF_CHARGE ) ) + { + return; + } + + if( pm->ps->weaponTime > 0 ) + pm->ps->weaponTime -= pml.msec; + + // check for weapon change + // can't change if weapon is firing, but can change + // again if lowering or raising + if( pm->ps->weaponTime <= 0 || pm->ps->weaponstate != WEAPON_FIRING ) + { + //TA: must press use to switch weapons + if( pm->cmd.buttons & BUTTON_USE_HOLDABLE ) + { + if( !( pm->ps->pm_flags & PMF_USE_ITEM_HELD ) ) + { + if( pm->cmd.weapon <= 32 ) + { + //if trying to select a weapon, select it + if( pm->ps->weapon != pm->cmd.weapon ) + PM_BeginWeaponChange( pm->cmd.weapon ); + } + else if( pm->cmd.weapon > 32 ) + { + //if trying to toggle an upgrade, toggle it + if( BG_InventoryContainsUpgrade( pm->cmd.weapon - 32, pm->ps->stats ) ) //sanity check + { + if( BG_UpgradeIsActive( pm->cmd.weapon - 32, pm->ps->stats ) ) + BG_DeactivateUpgrade( pm->cmd.weapon - 32, pm->ps->stats ); + else + BG_ActivateUpgrade( pm->cmd.weapon - 32, pm->ps->stats ); + } + } + pm->ps->pm_flags |= PMF_USE_ITEM_HELD; + } + } + else + pm->ps->pm_flags &= ~PMF_USE_ITEM_HELD; + + //something external thinks a weapon change is necessary + if( pm->ps->pm_flags & PMF_WEAPON_SWITCH ) + { + pm->ps->pm_flags &= ~PMF_WEAPON_SWITCH; + PM_BeginWeaponChange( pm->ps->persistant[ PERS_NEWWEAPON ] ); + } + } + + if( pm->ps->weaponTime > 0 ) + return; + + // change weapon if time + if( pm->ps->weaponstate == WEAPON_DROPPING ) + { + PM_FinishWeaponChange( ); + return; + } + + if( pm->ps->weaponstate == WEAPON_RAISING ) + { + pm->ps->weaponstate = WEAPON_READY; + + if( !( pm->ps->persistant[ PERS_STATE ] & PS_NONSEGMODEL ) ) + { + if( pm->ps->weapon == WP_BLASTER ) + PM_ContinueTorsoAnim( TORSO_STAND2 ); + else + PM_ContinueTorsoAnim( TORSO_STAND ); + } + + return; + } + + // start the animation even if out of ammo + + BG_UnpackAmmoArray( pm->ps->weapon, pm->ps->ammo, pm->ps->powerups, &ammo, &clips ); + BG_FindAmmoForWeapon( pm->ps->weapon, NULL, &maxClips ); + + // check for out of ammo + if( !ammo && !clips && !BG_FindInfinteAmmoForWeapon( pm->ps->weapon ) ) + { + PM_AddEvent( EV_NOAMMO ); + pm->ps->weaponTime += 200; + + if( pm->ps->weaponstate == WEAPON_FIRING ) + pm->ps->weaponstate = WEAPON_READY; + + return; + } + + //done reloading so give em some ammo + if( pm->ps->weaponstate == WEAPON_RELOADING ) + { + if( maxClips > 0 ) + { + clips--; + BG_FindAmmoForWeapon( pm->ps->weapon, &ammo, NULL ); + } + + if( BG_FindUsesEnergyForWeapon( pm->ps->weapon ) && + BG_InventoryContainsUpgrade( UP_BATTPACK, pm->ps->stats ) ) + ammo = (int)( (float)ammo * BATTPACK_MODIFIER ); + + BG_PackAmmoArray( pm->ps->weapon, pm->ps->ammo, pm->ps->powerups, ammo, clips ); + + //allow some time for the weapon to be raised + pm->ps->weaponstate = WEAPON_RAISING; + PM_StartTorsoAnim( TORSO_RAISE ); + pm->ps->weaponTime += 250; + return; + } + + // check for end of clip + if( ( !ammo || pm->ps->pm_flags & PMF_WEAPON_RELOAD ) && clips ) + { + pm->ps->pm_flags &= ~PMF_WEAPON_RELOAD; + + pm->ps->weaponstate = WEAPON_RELOADING; + + //drop the weapon + PM_StartTorsoAnim( TORSO_DROP ); + + addTime = BG_FindReloadTimeForWeapon( pm->ps->weapon ); + + pm->ps->weaponTime += addTime; + return; + } + + //check if non-auto primary/secondary attacks are permited + switch( pm->ps->weapon ) + { + case WP_ALEVEL0: + //venom is only autohit + attack1 = attack2 = attack3 = qfalse; + + if( !pm->autoWeaponHit[ pm->ps->weapon ] ) + { + pm->ps->weaponTime = 0; + pm->ps->weaponstate = WEAPON_READY; + return; + } + break; + + case WP_ALEVEL3: + case WP_ALEVEL3_UPG: + //pouncing has primary secondary AND autohit procedures + attack1 = pm->cmd.buttons & BUTTON_ATTACK; + attack2 = pm->cmd.buttons & BUTTON_ATTACK2; + attack3 = pm->cmd.buttons & BUTTON_USE_HOLDABLE; + + if( !pm->autoWeaponHit[ pm->ps->weapon ] && !attack1 && !attack2 && !attack3 ) + { + pm->ps->weaponTime = 0; + pm->ps->weaponstate = WEAPON_READY; + return; + } + break; + + case WP_LUCIFER_CANNON: + attack1 = pm->cmd.buttons & BUTTON_ATTACK; + attack2 = pm->cmd.buttons & BUTTON_ATTACK2; + attack3 = pm->cmd.buttons & BUTTON_USE_HOLDABLE; + + if( ( attack1 || pm->ps->stats[ STAT_MISC ] == 0 ) && !attack2 && !attack3 ) + { + if( pm->ps->stats[ STAT_MISC ] < LCANNON_TOTAL_CHARGE ) + { + pm->ps->weaponTime = 0; + pm->ps->weaponstate = WEAPON_READY; + return; + } + else + attack1 = !attack1; + } + + //erp this looks confusing + if( pm->ps->stats[ STAT_MISC ] > LCANNON_MIN_CHARGE ) + attack1 = !attack1; + else if( pm->ps->stats[ STAT_MISC ] > 0 ) + { + pm->ps->stats[ STAT_MISC ] = 0; + pm->ps->weaponTime = 0; + pm->ps->weaponstate = WEAPON_READY; + return; + } + break; + + case WP_MASS_DRIVER: + attack1 = pm->cmd.buttons & BUTTON_ATTACK; + // attack2 is handled on the client for zooming (cg_view.c) + + if( !attack1 ) + { + pm->ps->weaponTime = 0; + pm->ps->weaponstate = WEAPON_READY; + return; + } + break; + + default: + //by default primary and secondary attacks are allowed + attack1 = pm->cmd.buttons & BUTTON_ATTACK; + attack2 = pm->cmd.buttons & BUTTON_ATTACK2; + attack3 = pm->cmd.buttons & BUTTON_USE_HOLDABLE; + + if( !attack1 && !attack2 && !attack3 ) + { + pm->ps->weaponTime = 0; + pm->ps->weaponstate = WEAPON_READY; + return; + } + break; + } + + if ( pm->ps->weapon == WP_LUCIFER_CANNON && pm->ps->stats[ STAT_MISC ] > 0 && attack3 ) + { + attack1 = qtrue; + attack3 = qfalse; + } + + //TA: fire events for non auto weapons + if( attack3 ) + { + if( BG_WeaponHasThirdMode( pm->ps->weapon ) ) + { + //hacky special case for slowblob + if( pm->ps->weapon == WP_ALEVEL3_UPG && !ammo ) + { + PM_AddEvent( EV_NOAMMO ); + pm->ps->weaponTime += 200; + return; + } + + pm->ps->generic1 = WPM_TERTIARY; + PM_AddEvent( EV_FIRE_WEAPON3 ); + addTime = BG_FindRepeatRate3ForWeapon( pm->ps->weapon ); + } + else + { + pm->ps->weaponTime = 0; + pm->ps->weaponstate = WEAPON_READY; + pm->ps->generic1 = WPM_NOTFIRING; + return; + } + } + else if( attack2 ) + { + if( BG_WeaponHasAltMode( pm->ps->weapon ) ) + { + pm->ps->generic1 = WPM_SECONDARY; + PM_AddEvent( EV_FIRE_WEAPON2 ); + addTime = BG_FindRepeatRate2ForWeapon( pm->ps->weapon ); + } + else + { + pm->ps->weaponTime = 0; + pm->ps->weaponstate = WEAPON_READY; + pm->ps->generic1 = WPM_NOTFIRING; + return; + } + } + else if( attack1 ) + { + pm->ps->generic1 = WPM_PRIMARY; + PM_AddEvent( EV_FIRE_WEAPON ); + addTime = BG_FindRepeatRate1ForWeapon( pm->ps->weapon ); + } + + //TA: fire events for autohit weapons + if( pm->autoWeaponHit[ pm->ps->weapon ] ) + { + switch( pm->ps->weapon ) + { + case WP_ALEVEL0: + pm->ps->generic1 = WPM_PRIMARY; + PM_AddEvent( EV_FIRE_WEAPON ); + addTime = BG_FindRepeatRate1ForWeapon( pm->ps->weapon ); + break; + + case WP_ALEVEL3: + case WP_ALEVEL3_UPG: + pm->ps->generic1 = WPM_SECONDARY; + PM_AddEvent( EV_FIRE_WEAPON2 ); + addTime = BG_FindRepeatRate2ForWeapon( pm->ps->weapon ); + break; + + default: + break; + } + } + + if( !( pm->ps->persistant[ PERS_STATE ] & PS_NONSEGMODEL ) ) + { + //FIXME: this should be an option in the client weapon.cfg + switch( pm->ps->weapon ) + { + case WP_FLAMER: + if( pm->ps->weaponstate == WEAPON_READY ) + { + PM_StartTorsoAnim( TORSO_ATTACK ); + } + break; + + case WP_BLASTER: + PM_StartTorsoAnim( TORSO_ATTACK2 ); + break; + + default: + PM_StartTorsoAnim( TORSO_ATTACK ); + break; + } + } + else + { + if( pm->ps->weapon == WP_ALEVEL4 ) + { + //hack to get random attack animations + //FIXME: does pm->ps->weaponTime cycle enough? + int num = abs( pm->ps->weaponTime ) % 3; + + if( num == 0 ) + PM_ForceLegsAnim( NSPA_ATTACK1 ); + else if( num == 1 ) + PM_ForceLegsAnim( NSPA_ATTACK2 ); + else if( num == 2 ) + PM_ForceLegsAnim( NSPA_ATTACK3 ); + } + else + { + if( attack1 ) + PM_ForceLegsAnim( NSPA_ATTACK1 ); + else if( attack2 ) + PM_ForceLegsAnim( NSPA_ATTACK2 ); + else if( attack3 ) + PM_ForceLegsAnim( NSPA_ATTACK3 ); + } + + pm->ps->torsoTimer = TIMER_ATTACK; + } + + pm->ps->weaponstate = WEAPON_FIRING; + + // take an ammo away if not infinite + if( !BG_FindInfinteAmmoForWeapon( pm->ps->weapon ) ) + { + //special case for lCanon + if( pm->ps->weapon == WP_LUCIFER_CANNON && attack1 && !attack2 ) + { + ammo -= (int)( ceil( ( (float)pm->ps->stats[ STAT_MISC ] / (float)LCANNON_TOTAL_CHARGE ) * 10.0f ) ); + + //stay on the safe side + if( ammo < 0 ) + ammo = 0; + } + else + ammo--; + + BG_PackAmmoArray( pm->ps->weapon, pm->ps->ammo, pm->ps->powerups, ammo, clips ); + } + else if( pm->ps->weapon == WP_ALEVEL3_UPG && attack3 ) + { + //special case for slowblob + ammo--; + BG_PackAmmoArray( pm->ps->weapon, pm->ps->ammo, pm->ps->powerups, ammo, clips ); + } + + //FIXME: predicted angles miss a problem?? + if( pm->ps->weapon == WP_CHAINGUN ) + { + if( pm->ps->pm_flags & PMF_DUCKED || + BG_InventoryContainsUpgrade( UP_BATTLESUIT, pm->ps->stats ) ) + { + pm->ps->delta_angles[ PITCH ] -= ANGLE2SHORT( ( ( random() * 0.5 ) - 0.125 ) * ( 30 / (float)addTime ) ); + pm->ps->delta_angles[ YAW ] -= ANGLE2SHORT( ( ( random() * 0.5 ) - 0.25 ) * ( 30.0 / (float)addTime ) ); + } + else + { + pm->ps->delta_angles[ PITCH ] -= ANGLE2SHORT( ( ( random() * 8 ) - 2 ) * ( 30.0 / (float)addTime ) ); + pm->ps->delta_angles[ YAW ] -= ANGLE2SHORT( ( ( random() * 8 ) - 4 ) * ( 30.0 / (float)addTime ) ); + } + } + + pm->ps->weaponTime += addTime; +} + +/* +================ +PM_Animate +================ +*/ +static void PM_Animate( void ) +{ + if( pm->cmd.buttons & BUTTON_GESTURE ) + { + if( !( pm->ps->persistant[ PERS_STATE ] & PS_NONSEGMODEL ) ) + { + if( pm->ps->torsoTimer == 0 ) + { + PM_StartTorsoAnim( TORSO_GESTURE ); + pm->ps->torsoTimer = TIMER_GESTURE; + + PM_AddEvent( EV_TAUNT ); + } + } + else + { + if( pm->ps->torsoTimer == 0 ) + { + PM_ForceLegsAnim( NSPA_GESTURE ); + pm->ps->torsoTimer = TIMER_GESTURE; + + PM_AddEvent( EV_TAUNT ); + } + } + } +} + + +/* +================ +PM_DropTimers +================ +*/ +static void PM_DropTimers( void ) +{ + // drop misc timing counter + if( pm->ps->pm_time ) + { + if( pml.msec >= pm->ps->pm_time ) + { + pm->ps->pm_flags &= ~PMF_ALL_TIMES; + pm->ps->pm_time = 0; + } + else + pm->ps->pm_time -= pml.msec; + } + + // drop animation counter + if( pm->ps->legsTimer > 0 ) + { + pm->ps->legsTimer -= pml.msec; + + if( pm->ps->legsTimer < 0 ) + pm->ps->legsTimer = 0; + } + + if( pm->ps->torsoTimer > 0 ) + { + pm->ps->torsoTimer -= pml.msec; + + if( pm->ps->torsoTimer < 0 ) + pm->ps->torsoTimer = 0; + } +} + + +/* +================ +PM_UpdateViewAngles + +This can be used as another entry point when only the viewangles +are being updated instead of a full move +================ +*/ +void PM_UpdateViewAngles( playerState_t *ps, const usercmd_t *cmd ) +{ + short temp[ 3 ]; + int i; + vec3_t axis[ 3 ], rotaxis[ 3 ]; + vec3_t tempang; + + if( ps->pm_type == PM_INTERMISSION || ps->pm_type == PM_SPINTERMISSION ) + return; // no view changes at all + + if( ps->pm_type != PM_SPECTATOR && ps->stats[ STAT_HEALTH ] <= 0 ) + return; // no view changes at all + + // circularly clamp the angles with deltas + for( i = 0; i < 3; i++ ) + { + temp[ i ] = cmd->angles[ i ] + ps->delta_angles[ i ]; + + if( i == PITCH ) + { + // don't let the player look up or down more than 90 degrees + if( temp[ i ] > 16000 ) + { + ps->delta_angles[ i ] = 16000 - cmd->angles[ i ]; + temp[ i ] = 16000; + } + else if( temp[ i ] < -16000 ) + { + ps->delta_angles[ i ] = -16000 - cmd->angles[ i ]; + temp[ i ] = -16000; + } + } + tempang[ i ] = SHORT2ANGLE( temp[ i ] ); + } + + //convert viewangles -> axis + AnglesToAxis( tempang, axis ); + + if( !( ps->stats[ STAT_STATE ] & SS_WALLCLIMBING ) || + !BG_RotateAxis( ps->grapplePoint, axis, rotaxis, qfalse, + ps->stats[ STAT_STATE ] & SS_WALLCLIMBINGCEILING ) ) + AxisCopy( axis, rotaxis ); + + //convert the new axis back to angles + AxisToAngles( rotaxis, tempang ); + + //force angles to -180 <= x <= 180 + for( i = 0; i < 3; i++ ) + { + while( tempang[ i ] > 180.0f ) + tempang[ i ] -= 360.0f; + + while( tempang[ i ] < 180.0f ) + tempang[ i ] += 360.0f; + } + + //actually set the viewangles + for( i = 0; i < 3; i++ ) + ps->viewangles[ i ] = tempang[ i ]; + + //pull the view into the lock point + if( ps->pm_type == PM_GRABBED && !BG_InventoryContainsUpgrade( UP_BATTLESUIT, ps->stats ) ) + { + vec3_t dir, angles; + + ByteToDir( ps->stats[ STAT_VIEWLOCK ], dir ); + vectoangles( dir, angles ); + + for( i = 0; i < 3; i++ ) + { + float diff = AngleSubtract( ps->viewangles[ i ], angles[ i ] ); + + while( diff > 180.0f ) + diff -= 360.0f; + while( diff < -180.0f ) + diff += 360.0f; + + if( diff < -90.0f ) + ps->delta_angles[ i ] += ANGLE2SHORT( fabs( diff ) - 90.0f ); + else if( diff > 90.0f ) + ps->delta_angles[ i ] -= ANGLE2SHORT( fabs( diff ) - 90.0f ); + + if( diff < 0.0f ) + ps->delta_angles[ i ] += ANGLE2SHORT( fabs( diff ) * 0.05f ); + else if( diff > 0.0f ) + ps->delta_angles[ i ] -= ANGLE2SHORT( fabs( diff ) * 0.05f ); + } + } +} + + +/* +================ +PmoveSingle + +================ +*/ +void trap_SnapVector( float *v ); + +void PmoveSingle( pmove_t *pmove ) +{ + int ammo, clips; + + pm = pmove; + + BG_UnpackAmmoArray( pm->ps->weapon, pm->ps->ammo, pm->ps->powerups, &ammo, &clips ); + + // this counter lets us debug movement problems with a journal + // by setting a conditional breakpoint fot the previous frame + c_pmove++; + + // clear results + pm->numtouch = 0; + pm->watertype = 0; + pm->waterlevel = 0; + + if( pm->ps->stats[ STAT_HEALTH ] <= 0 ) + pm->tracemask &= ~CONTENTS_BODY; // corpses can fly through bodies + + // make sure walking button is clear if they are running, to avoid + // proxy no-footsteps cheats + if( abs( pm->cmd.forwardmove ) > 64 || abs( pm->cmd.rightmove ) > 64 ) + pm->cmd.buttons &= ~BUTTON_WALKING; + + // set the talk balloon flag + if( pm->cmd.buttons & BUTTON_TALK ) + pm->ps->eFlags |= EF_TALK; + else + pm->ps->eFlags &= ~EF_TALK; + + // set the firing flag for continuous beam weapons + if( !(pm->ps->pm_flags & PMF_RESPAWNED) && pm->ps->pm_type != PM_INTERMISSION && + ( pm->cmd.buttons & BUTTON_ATTACK ) && + ( ( ammo > 0 || clips > 0 ) || BG_FindInfinteAmmoForWeapon( pm->ps->weapon ) ) ) + pm->ps->eFlags |= EF_FIRING; + else + pm->ps->eFlags &= ~EF_FIRING; + + // set the firing flag for continuous beam weapons + if( !(pm->ps->pm_flags & PMF_RESPAWNED) && pm->ps->pm_type != PM_INTERMISSION && + ( pm->cmd.buttons & BUTTON_ATTACK2 ) && + ( ( ammo > 0 || clips > 0 ) || BG_FindInfinteAmmoForWeapon( pm->ps->weapon ) ) ) + pm->ps->eFlags |= EF_FIRING2; + else + pm->ps->eFlags &= ~EF_FIRING2; + + // set the firing flag for continuous beam weapons + if( !(pm->ps->pm_flags & PMF_RESPAWNED) && pm->ps->pm_type != PM_INTERMISSION && + ( pm->cmd.buttons & BUTTON_USE_HOLDABLE ) && + ( ( ammo > 0 || clips > 0 ) || BG_FindInfinteAmmoForWeapon( pm->ps->weapon ) ) ) + pm->ps->eFlags |= EF_FIRING3; + else + pm->ps->eFlags &= ~EF_FIRING3; + + + // clear the respawned flag if attack and use are cleared + if( pm->ps->stats[STAT_HEALTH] > 0 && + !( pm->cmd.buttons & ( BUTTON_ATTACK | BUTTON_USE_HOLDABLE ) ) ) + pm->ps->pm_flags &= ~PMF_RESPAWNED; + + // if talk button is down, dissallow all other input + // this is to prevent any possible intercept proxy from + // adding fake talk balloons + if( pmove->cmd.buttons & BUTTON_TALK ) + { + pmove->cmd.buttons = BUTTON_TALK; + pmove->cmd.forwardmove = 0; + pmove->cmd.rightmove = 0; + pmove->cmd.upmove = 0; + } + + // clear all pmove local vars + memset( &pml, 0, sizeof( pml ) ); + + // determine the time + pml.msec = pmove->cmd.serverTime - pm->ps->commandTime; + + if( pml.msec < 1 ) + pml.msec = 1; + else if( pml.msec > 200 ) + pml.msec = 200; + + pm->ps->commandTime = pmove->cmd.serverTime; + + // save old org in case we get stuck + VectorCopy( pm->ps->origin, pml.previous_origin ); + + // save old velocity for crashlanding + VectorCopy( pm->ps->velocity, pml.previous_velocity ); + + pml.frametime = pml.msec * 0.001; + + AngleVectors( pm->ps->viewangles, pml.forward, pml.right, pml.up ); + + if( pm->cmd.upmove < 10 ) + { + // not holding jump + pm->ps->pm_flags &= ~PMF_JUMP_HELD; + } + + // decide if backpedaling animations should be used + if( pm->cmd.forwardmove < 0 ) + pm->ps->pm_flags |= PMF_BACKWARDS_RUN; + else if( pm->cmd.forwardmove > 0 || ( pm->cmd.forwardmove == 0 && pm->cmd.rightmove ) ) + pm->ps->pm_flags &= ~PMF_BACKWARDS_RUN; + + if( pm->ps->pm_type >= PM_DEAD ) + { + pm->cmd.forwardmove = 0; + pm->cmd.rightmove = 0; + pm->cmd.upmove = 0; + } + + if( pm->ps->pm_type == PM_SPECTATOR ) + { + // update the viewangles + PM_UpdateViewAngles( pm->ps, &pm->cmd ); + PM_CheckDuck( ); + PM_FlyMove( ); + PM_DropTimers( ); + return; + } + + if( pm->ps->pm_type == PM_NOCLIP ) + { + PM_UpdateViewAngles( pm->ps, &pm->cmd ); + PM_NoclipMove( ); + PM_DropTimers( ); + return; + } + + if( pm->ps->pm_type == PM_FREEZE) + return; // no movement at all + + if( pm->ps->pm_type == PM_INTERMISSION || pm->ps->pm_type == PM_SPINTERMISSION ) + return; // no movement at all + + // set watertype, and waterlevel + PM_SetWaterLevel( ); + pml.previous_waterlevel = pmove->waterlevel; + + // set mins, maxs, and viewheight + PM_CheckDuck( ); + + PM_CheckLadder( ); + + // set groundentity + PM_GroundTrace( ); + + // update the viewangles + PM_UpdateViewAngles( pm->ps, &pm->cmd ); + + if( pm->ps->pm_type == PM_DEAD || pm->ps->pm_type == PM_GRABBED ) + PM_DeadMove( ); + + PM_DropTimers( ); + + if( pm->ps->pm_type == PM_JETPACK ) + PM_JetPackMove( ); + else if( pm->ps->pm_flags & PMF_TIME_WATERJUMP ) + PM_WaterJumpMove( ); + else if( pm->waterlevel > 1 ) + PM_WaterMove( ); + else if( pml.ladder ) + PM_LadderMove( ); + else if( pml.walking ) + { + if( BG_ClassHasAbility( pm->ps->stats[ STAT_PCLASS ], SCA_WALLCLIMBER ) && + ( pm->ps->stats[ STAT_STATE ] & SS_WALLCLIMBING ) ) + PM_ClimbMove( ); //TA: walking on any surface + else + PM_WalkMove( ); // walking on ground + } + else + PM_AirMove( ); + + PM_Animate( ); + + // set groundentity, watertype, and waterlevel + PM_GroundTrace( ); + //TA: must update after every GroundTrace() - yet more clock cycles down the drain :( (14 vec rotations/frame) + // update the viewangles + PM_UpdateViewAngles( pm->ps, &pm->cmd ); + + PM_SetWaterLevel( ); + + // weapons + PM_Weapon( ); + + // torso animation + PM_TorsoAnimation( ); + + // footstep events / legs animations + PM_Footsteps( ); + + // entering / leaving water splashes + PM_WaterEvents( ); + + // snap some parts of playerstate to save network bandwidth + trap_SnapVector( pm->ps->velocity ); +} + + +/* +================ +Pmove + +Can be called by either the server or the client +================ +*/ +void Pmove( pmove_t *pmove ) +{ + int finalTime; + + finalTime = pmove->cmd.serverTime; + + if( finalTime < pmove->ps->commandTime ) + return; // should not happen + + if( finalTime > pmove->ps->commandTime + 1000 ) + pmove->ps->commandTime = finalTime - 1000; + + pmove->ps->pmove_framecount = ( pmove->ps->pmove_framecount + 1 ) & ( ( 1 << PS_PMOVEFRAMECOUNTBITS ) - 1 ); + + // chop the move up if it is too long, to prevent framerate + // dependent behavior + while( pmove->ps->commandTime != finalTime ) + { + int msec; + + msec = finalTime - pmove->ps->commandTime; + + if( pmove->pmove_fixed ) + { + if( msec > pmove->pmove_msec ) + msec = pmove->pmove_msec; + } + else + { + if( msec > 66 ) + msec = 66; + } + + + pmove->cmd.serverTime = pmove->ps->commandTime + msec; + PmoveSingle( pmove ); + + if( pmove->ps->pm_flags & PMF_JUMP_HELD ) + pmove->cmd.upmove = 20; + } +} diff --git a/src/game/bg_public.h b/src/game/bg_public.h new file mode 100644 index 0000000..6642d91 --- /dev/null +++ b/src/game/bg_public.h @@ -0,0 +1,1332 @@ +/* +=========================================================================== +Copyright (C) 1999-2005 Id Software, Inc. +Copyright (C) 2000-2006 Tim Angus + +This file is part of Tremulous. + +Tremulous is free software; you can redistribute it +and/or modify it under the terms of the GNU General Public License as +published by the Free Software Foundation; either version 2 of the License, +or (at your option) any later version. + +Tremulous is distributed in the hope that it will be +useful, but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with Tremulous; if not, write to the Free Software +Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA +=========================================================================== +*/ + +// bg_public.h -- definitions shared by both the server game and client game modules + +//tremulous balance header +#include "tremulous.h" + +// because games can change separately from the main system version, we need a +// second version that must match between game and cgame +#define GAME_VERSION "base" + +#define DEFAULT_GRAVITY 800 + +#define VOTE_TIME 30000 // 30 seconds before vote times out + +#define MINS_Z -24 +#define DEFAULT_VIEWHEIGHT 26 +#define CROUCH_VIEWHEIGHT 12 +#define DEAD_VIEWHEIGHT -14 //TA: watch for mins[ 2 ] less than this causing + +// +// config strings are a general means of communicating variable length strings +// from the server to all connected clients. +// + +// CS_SERVERINFO and CS_SYSTEMINFO are defined in q_shared.h +#define CS_MUSIC 2 +#define CS_MESSAGE 3 // from the map worldspawn's message field +#define CS_MOTD 4 // g_motd string for server message of the day +#define CS_WARMUP 5 // server time when the match will be restarted +// 6 UNUSED +// 7 UNUSED +#define CS_VOTE_TIME 8 +#define CS_VOTE_STRING 9 +#define CS_VOTE_YES 10 +#define CS_VOTE_NO 11 + +#define CS_TEAMVOTE_TIME 12 +#define CS_TEAMVOTE_STRING 14 +#define CS_TEAMVOTE_YES 16 +#define CS_TEAMVOTE_NO 18 + +#define CS_GAME_VERSION 20 +#define CS_LEVEL_START_TIME 21 // so the timer only shows the current level +#define CS_INTERMISSION 22 // when 1, fraglimit/timelimit has been hit and intermission will start in a second or two +#define CS_WINNER 23 // string indicating round winner +#define CS_SHADERSTATE 24 +#define CS_BOTINFO 25 +#define CS_CLIENTS_READY 26 //TA: following suggestion in STAT_ enum STAT_CLIENTS_READY becomes a configstring + +//TA: extra stuff: +#define CS_BUILDPOINTS 28 +#define CS_STAGES 29 +#define CS_SPAWNS 30 + +#define CS_MODELS 33 +#define CS_SOUNDS (CS_MODELS+MAX_MODELS) +#define CS_SHADERS (CS_SOUNDS+MAX_SOUNDS) +#define CS_PARTICLE_SYSTEMS (CS_SHADERS+MAX_GAME_SHADERS) +#define CS_PLAYERS (CS_PARTICLE_SYSTEMS+MAX_GAME_PARTICLE_SYSTEMS) +#define CS_PRECACHES (CS_PLAYERS+MAX_CLIENTS) +#define CS_LOCATIONS (CS_PRECACHES+MAX_CLIENTS) + +#define CS_MAX (CS_LOCATIONS+MAX_LOCATIONS) + +#if (CS_MAX) > MAX_CONFIGSTRINGS +#error overflow: (CS_MAX) > MAX_CONFIGSTRINGS +#endif + +typedef enum +{ + GENDER_MALE, + GENDER_FEMALE, + GENDER_NEUTER +} gender_t; + +/* +=================================================================================== + +PMOVE MODULE + +The pmove code takes a player_state_t and a usercmd_t and generates a new player_state_t +and some other output data. Used for local prediction on the client game and true +movement on the server game. +=================================================================================== +*/ + +typedef enum +{ + PM_NORMAL, // can accelerate and turn + PM_NOCLIP, // noclip movement + PM_SPECTATOR, // still run into walls + PM_JETPACK, // jetpack physics + PM_GRABBED, // like dead, but for when the player is still live + PM_DEAD, // no acceleration or turning, but free falling + PM_FREEZE, // stuck in place with no control + PM_INTERMISSION, // no movement or status bar + PM_SPINTERMISSION // no movement or status bar +} pmtype_t; + +typedef enum +{ + WEAPON_READY, + WEAPON_RAISING, + WEAPON_DROPPING, + WEAPON_FIRING, + WEAPON_RELOADING +} weaponstate_t; + +// pmove->pm_flags +#define PMF_DUCKED 1 +#define PMF_JUMP_HELD 2 +#define PMF_CROUCH_HELD 4 +#define PMF_BACKWARDS_JUMP 8 // go into backwards land +#define PMF_BACKWARDS_RUN 16 // coast down to backwards run +#define PMF_TIME_LAND 32 // pm_time is time before rejump +#define PMF_TIME_KNOCKBACK 64 // pm_time is an air-accelerate only time +#define PMF_TIME_WATERJUMP 256 // pm_time is waterjump +#define PMF_RESPAWNED 512 // clear after attack and jump buttons come up +#define PMF_USE_ITEM_HELD 1024 +#define PMF_WEAPON_RELOAD 2048 //TA: force a weapon switch +#define PMF_FOLLOW 4096 // spectate following another player +#define PMF_QUEUED 8192 //TA: player is queued +#define PMF_TIME_WALLJUMP 16384 //TA: for limiting wall jumping +#define PMF_CHARGE 32768 //TA: keep track of pouncing +#define PMF_WEAPON_SWITCH 65536 //TA: force a weapon switch + + +#define PMF_ALL_TIMES (PMF_TIME_WATERJUMP|PMF_TIME_LAND|PMF_TIME_KNOCKBACK|PMF_TIME_WALLJUMP) + +typedef struct +{ + int pouncePayload; +} pmoveExt_t; + +#define MAXTOUCH 32 +typedef struct +{ + // state (in / out) + playerState_t *ps; + pmoveExt_t *pmext; + // command (in) + usercmd_t cmd; + int tracemask; // collide against these types of surfaces + int debugLevel; // if set, diagnostic output will be printed + qboolean noFootsteps; // if the game is setup for no footsteps by the server + qboolean autoWeaponHit[ 32 ]; //FIXME: TA: remind myself later this might be a problem + + int framecount; + + // results (out) + int numtouch; + int touchents[ MAXTOUCH ]; + + vec3_t mins, maxs; // bounding box size + + int watertype; + int waterlevel; + + float xyspeed; + + // for fixed msec Pmove + int pmove_fixed; + int pmove_msec; + + // callbacks to test the world + // these will be different functions during game and cgame + /*void (*trace)( trace_t *results, const vec3_t start, vec3_t mins, vec3_t maxs, const vec3_t end, int passEntityNum, int contentMask );*/ + void (*trace)( trace_t *results, const vec3_t start, const vec3_t mins, const vec3_t maxs, + const vec3_t end, int passEntityNum, int contentMask ); + + + int (*pointcontents)( const vec3_t point, int passEntityNum ); +} pmove_t; + +// if a full pmove isn't done on the client, you can just update the angles +void PM_UpdateViewAngles( playerState_t *ps, const usercmd_t *cmd ); +void Pmove( pmove_t *pmove ); + +//=================================================================================== + + +// player_state->stats[] indexes +typedef enum +{ + STAT_HEALTH, + STAT_ITEMS, + STAT_SLOTS, //TA: tracks the amount of stuff human players are carrying + STAT_ACTIVEITEMS, + STAT_WEAPONS, // 16 bit fields + STAT_WEAPONS2, //TA: another 16 bits to push the max weapon count up + STAT_MAX_HEALTH, // health / armor limit, changable by handicap + STAT_PCLASS, //TA: player class (for aliens AND humans) + STAT_PTEAM, //TA: player team + STAT_STAMINA, //TA: stamina (human only) + STAT_STATE, //TA: client states e.g. wall climbing + STAT_MISC, //TA: for uh...misc stuff + STAT_BUILDABLE, //TA: which ghost model to display for building + STAT_BOOSTTIME, //TA: time left for boost (alien only) + STAT_FALLDIST, //TA: the distance the player fell + STAT_VIEWLOCK //TA: direction to lock the view in +} statIndex_t; + +#define SCA_WALLCLIMBER 0x00000001 +#define SCA_TAKESFALLDAMAGE 0x00000002 +#define SCA_CANZOOM 0x00000004 +#define SCA_NOWEAPONDRIFT 0x00000008 +#define SCA_FOVWARPS 0x00000010 +#define SCA_ALIENSENSE 0x00000020 +#define SCA_CANUSELADDERS 0x00000040 +#define SCA_WALLJUMPER 0x00000080 + +#define SS_WALLCLIMBING 0x00000001 +#define SS_WALLCLIMBINGCEILING 0x00000002 +#define SS_CREEPSLOWED 0x00000004 +#define SS_SPEEDBOOST 0x00000008 +#define SS_INFESTING 0x00000010 +#define SS_GRABBED 0x00000020 +#define SS_BLOBLOCKED 0x00000040 +#define SS_POISONED 0x00000080 +#define SS_HOVELING 0x00000100 +#define SS_BOOSTED 0x00000200 +#define SS_SLOWLOCKED 0x00000400 +#define SS_POISONCLOUDED 0x00000800 +#define SS_MEDKIT_ACTIVE 0x00001000 +#define SS_CHARGING 0x00002000 + +#define SB_VALID_TOGGLEBIT 0x00004000 + +#define MAX_STAMINA 1000 + +// player_state->persistant[] indexes +// these fields are the only part of player_state that isn't +// cleared on respawn +typedef enum +{ + PERS_SCORE, // !!! MUST NOT CHANGE, SERVER AND GAME BOTH REFERENCE !!! + PERS_HITS, // total points damage inflicted so damage beeps can sound on change + PERS_UNUSED, // used to be PERS_RANK, no longer used + PERS_TEAM, + PERS_SPAWN_COUNT, // incremented every respawn + PERS_ATTACKER, // clientnum of last damage inflicter + PERS_KILLED, // count of the number of times you died + + //TA: + PERS_STATE, + PERS_CREDIT, // human credit + PERS_BANK, // human credit in the bank + PERS_QUEUEPOS, // position in the spawn queue + PERS_NEWWEAPON // weapon to switch to +} persEnum_t; + +#define PS_WALLCLIMBINGFOLLOW 0x00000001 +#define PS_WALLCLIMBINGTOGGLE 0x00000002 +#define PS_NONSEGMODEL 0x00000004 + +// entityState_t->eFlags +#define EF_DEAD 0x00000001 // don't draw a foe marker over players with EF_DEAD +#define EF_TELEPORT_BIT 0x00000002 // toggled every time the origin abruptly changes +#define EF_PLAYER_EVENT 0x00000004 +#define EF_BOUNCE 0x00000008 // for missiles +#define EF_BOUNCE_HALF 0x00000010 // for missiles +#define EF_NO_BOUNCE_SOUND 0x00000020 // for missiles +#define EF_WALLCLIMB 0x00000040 // TA: wall walking +#define EF_WALLCLIMBCEILING 0x00000080 // TA: wall walking ceiling hack +#define EF_NODRAW 0x00000100 // may have an event, but no model (unspawned items) +#define EF_FIRING 0x00000200 // for lightning gun +#define EF_FIRING2 0x00000400 // alt fire +#define EF_FIRING3 0x00000800 // third fire +#define EF_MOVER_STOP 0x00001000 // will push otherwise +#define EF_TALK 0x00002000 // draw a talk balloon +#define EF_CONNECTION 0x00004000 // draw a connection trouble sprite +#define EF_VOTED 0x00008000 // already cast a vote +#define EF_TEAMVOTED 0x00010000 // already cast a vote +#define EF_BLOBLOCKED 0x00020000 // TA: caught by a trapper +#define EF_REAL_LIGHT 0x00040000 // TA: light sprites according to ambient light +#define EF_DBUILDER 0x00080000 // designated builder protection + +typedef enum +{ + PW_NONE, + + PW_QUAD, + PW_BATTLESUIT, + PW_HASTE, + PW_INVIS, + PW_REGEN, + PW_FLIGHT, + + PW_REDFLAG, + PW_BLUEFLAG, + PW_BALL, + + PW_NUM_POWERUPS +} powerup_t; + +typedef enum +{ + HI_NONE, + + HI_TELEPORTER, + HI_MEDKIT, + + HI_NUM_HOLDABLE +} holdable_t; + +typedef enum +{ + WPM_NONE, + + WPM_PRIMARY, + WPM_SECONDARY, + WPM_TERTIARY, + + WPM_NOTFIRING, + + WPM_NUM_WEAPONMODES +} weaponMode_t; + +typedef enum +{ + WP_NONE, + + WP_ALEVEL0, + WP_ALEVEL1, + WP_ALEVEL1_UPG, + WP_ALEVEL2, + WP_ALEVEL2_UPG, + WP_ALEVEL3, + WP_ALEVEL3_UPG, + WP_ALEVEL4, + + WP_BLASTER, + WP_MACHINEGUN, + WP_PAIN_SAW, + WP_SHOTGUN, + WP_LAS_GUN, + WP_MASS_DRIVER, + WP_CHAINGUN, + WP_PULSE_RIFLE, + WP_FLAMER, + WP_LUCIFER_CANNON, + WP_GRENADE, + + WP_LOCKBLOB_LAUNCHER, + WP_HIVE, + WP_TESLAGEN, + WP_MGTURRET, + + //build weapons must remain in a block + WP_ABUILD, + WP_ABUILD2, + WP_HBUILD2, + WP_HBUILD, + //ok? + + WP_NUM_WEAPONS +} weapon_t; + +typedef enum +{ + UP_NONE, + + UP_LIGHTARMOUR, + UP_HELMET, + UP_MEDKIT, + UP_BATTPACK, + UP_JETPACK, + UP_BATTLESUIT, + UP_GRENADE, + + UP_AMMO, + + UP_NUM_UPGRADES +} upgrade_t; + +typedef enum +{ + WUT_NONE, + + WUT_ALIENS, + WUT_HUMANS, + + WUT_NUM_TEAMS +} WUTeam_t; + +//TA: bitmasks for upgrade slots +#define SLOT_NONE 0x00000000 +#define SLOT_HEAD 0x00000001 +#define SLOT_TORSO 0x00000002 +#define SLOT_ARMS 0x00000004 +#define SLOT_LEGS 0x00000008 +#define SLOT_BACKPACK 0x00000010 +#define SLOT_WEAPON 0x00000020 +#define SLOT_SIDEARM 0x00000040 + +typedef enum +{ + BA_NONE, + + BA_A_SPAWN, + BA_A_OVERMIND, + + BA_A_BARRICADE, + BA_A_ACIDTUBE, + BA_A_TRAPPER, + BA_A_BOOSTER, + BA_A_HIVE, + + BA_A_HOVEL, + + BA_H_SPAWN, + + BA_H_MGTURRET, + BA_H_TESLAGEN, + + BA_H_ARMOURY, + BA_H_DCC, + BA_H_MEDISTAT, + + BA_H_REACTOR, + BA_H_REPEATER, + + BA_NUM_BUILDABLES +} buildable_t; + +typedef enum +{ + BIT_NONE, + + BIT_ALIENS, + BIT_HUMANS, + + BIT_NUM_TEAMS +} buildableTeam_t; + +#define B_HEALTH_BITS 5 +#define B_HEALTH_MASK ((1<<B_HEALTH_BITS)-1) + +#define B_DCCED_TOGGLEBIT 0x00000000 +#define B_SPAWNED_TOGGLEBIT 0x00000020 +#define B_POWERED_TOGGLEBIT 0x00000040 +#define B_MARKED_TOGGLEBIT 0x00000080 + + +// reward sounds (stored in ps->persistant[PERS_PLAYEREVENTS]) +#define PLAYEREVENT_DENIEDREWARD 0x0001 +#define PLAYEREVENT_GAUNTLETREWARD 0x0002 +#define PLAYEREVENT_HOLYSHIT 0x0004 + +// entityState_t->event values +// entity events are for effects that take place reletive +// to an existing entities origin. Very network efficient. + +// two bits at the top of the entityState->event field +// will be incremented with each change in the event so +// that an identical event started twice in a row can +// be distinguished. And off the value with ~EV_EVENT_BITS +// to retrieve the actual event number +#define EV_EVENT_BIT1 0x00000100 +#define EV_EVENT_BIT2 0x00000200 +#define EV_EVENT_BITS (EV_EVENT_BIT1|EV_EVENT_BIT2) + +#define EVENT_VALID_MSEC 300 + +typedef enum +{ + EV_NONE, + + EV_FOOTSTEP, + EV_FOOTSTEP_METAL, + EV_FOOTSTEP_SQUELCH, + EV_FOOTSPLASH, + EV_FOOTWADE, + EV_SWIM, + + EV_STEP_4, + EV_STEP_8, + EV_STEP_12, + EV_STEP_16, + + EV_STEPDN_4, + EV_STEPDN_8, + EV_STEPDN_12, + EV_STEPDN_16, + + EV_FALL_SHORT, + EV_FALL_MEDIUM, + EV_FALL_FAR, + EV_FALLING, + + EV_JUMP, + EV_WATER_TOUCH, // foot touches + EV_WATER_LEAVE, // foot leaves + EV_WATER_UNDER, // head touches + EV_WATER_CLEAR, // head leaves + + EV_NOAMMO, + EV_CHANGE_WEAPON, + EV_FIRE_WEAPON, + EV_FIRE_WEAPON2, + EV_FIRE_WEAPON3, + + EV_PLAYER_RESPAWN, //TA: for fovwarp effects + EV_PLAYER_TELEPORT_IN, + EV_PLAYER_TELEPORT_OUT, + + EV_GRENADE_BOUNCE, // eventParm will be the soundindex + + EV_GENERAL_SOUND, + EV_GLOBAL_SOUND, // no attenuation + + EV_BULLET_HIT_FLESH, + EV_BULLET_HIT_WALL, + + EV_SHOTGUN, + + EV_MISSILE_HIT, + EV_MISSILE_MISS, + EV_MISSILE_MISS_METAL, + EV_TESLATRAIL, + EV_BULLET, // otherEntity is the shooter + + EV_LEV1_GRAB, + EV_LEV4_CHARGE_PREPARE, + EV_LEV4_CHARGE_START, + + EV_PAIN, + EV_DEATH1, + EV_DEATH2, + EV_DEATH3, + EV_OBITUARY, + + EV_GIB_PLAYER, // gib a previously living player + + EV_BUILD_CONSTRUCT, //TA + EV_BUILD_DESTROY, //TA + EV_BUILD_DELAY, //TA: can't build yet + EV_BUILD_REPAIR, //TA: repairing buildable + EV_BUILD_REPAIRED, //TA: buildable has full health + EV_HUMAN_BUILDABLE_EXPLOSION, + EV_ALIEN_BUILDABLE_EXPLOSION, + EV_ALIEN_ACIDTUBE, + + EV_MEDKIT_USED, + + EV_ALIEN_EVOLVE, + EV_ALIEN_EVOLVE_FAILED, + + EV_DEBUG_LINE, + EV_STOPLOOPINGSOUND, + EV_TAUNT, + + EV_OVERMIND_ATTACK, //TA: overmind under attack + EV_OVERMIND_DYING, //TA: overmind close to death + EV_OVERMIND_SPAWNS, //TA: overmind needs spawns + + EV_DCC_ATTACK, //TA: dcc under attack + + EV_RPTUSE_SOUND //TA: trigger a sound +} entity_event_t; + +typedef enum +{ + MN_TEAM, + MN_A_TEAMFULL, + MN_H_TEAMFULL, + + //alien stuff + MN_A_CLASS, + MN_A_BUILD, + MN_A_INFEST, + MN_A_HOVEL_OCCUPIED, + MN_A_HOVEL_BLOCKED, + MN_A_NOEROOM, + MN_A_TOOCLOSE, + MN_A_NOOVMND_EVOLVE, + + //alien build + MN_A_SPWNWARN, + MN_A_OVERMIND, + MN_A_NOASSERT, + MN_A_NOCREEP, + MN_A_NOOVMND, + MN_A_NOROOM, + MN_A_NORMAL, + MN_A_HOVEL, + MN_A_HOVEL_EXIT, + + //human stuff + MN_H_SPAWN, + MN_H_BUILD, + MN_H_ARMOURY, + MN_H_NOSLOTS, + MN_H_NOFUNDS, + MN_H_ITEMHELD, + + //human build + MN_H_REPEATER, + MN_H_NOPOWER, + MN_H_NOTPOWERED, + MN_H_NODCC, + MN_H_REACTOR, + MN_H_NOROOM, + MN_H_NORMAL, + MN_H_TNODEWARN, + MN_H_RPTWARN, + MN_H_RPTWARN2, + + //not used + MN_A_TEAMCHANGEBUILDTIMER, + MN_H_TEAMCHANGEBUILDTIMER, + + MN_A_EVOLVEBUILDTIMER, + + MN_H_NOENERGYAMMOHERE, + MN_H_NOARMOURYHERE, + MN_H_NOROOMBSUITON, + MN_H_NOROOMBSUITOFF, + MN_H_ARMOURYBUILDTIMER +} dynMenu_t; + +// animations +typedef enum +{ + BOTH_DEATH1, + BOTH_DEAD1, + BOTH_DEATH2, + BOTH_DEAD2, + BOTH_DEATH3, + BOTH_DEAD3, + + TORSO_GESTURE, + + TORSO_ATTACK, + TORSO_ATTACK2, + + TORSO_DROP, + TORSO_RAISE, + + TORSO_STAND, + TORSO_STAND2, + + LEGS_WALKCR, + LEGS_WALK, + LEGS_RUN, + LEGS_BACK, + LEGS_SWIM, + + LEGS_JUMP, + LEGS_LAND, + + LEGS_JUMPB, + LEGS_LANDB, + + LEGS_IDLE, + LEGS_IDLECR, + + LEGS_TURN, + + TORSO_GETFLAG, + TORSO_GUARDBASE, + TORSO_PATROL, + TORSO_FOLLOWME, + TORSO_AFFIRMATIVE, + TORSO_NEGATIVE, + + MAX_PLAYER_ANIMATIONS, + + LEGS_BACKCR, + LEGS_BACKWALK, + FLAG_RUN, + FLAG_STAND, + FLAG_STAND2RUN, + + MAX_PLAYER_TOTALANIMATIONS +} playerAnimNumber_t; + +// nonsegmented animations +typedef enum +{ + NSPA_STAND, + + NSPA_GESTURE, + + NSPA_WALK, + NSPA_RUN, + NSPA_RUNBACK, + NSPA_CHARGE, + + NSPA_RUNLEFT, + NSPA_WALKLEFT, + NSPA_RUNRIGHT, + NSPA_WALKRIGHT, + + NSPA_SWIM, + + NSPA_JUMP, + NSPA_LAND, + NSPA_JUMPBACK, + NSPA_LANDBACK, + + NSPA_TURN, + + NSPA_ATTACK1, + NSPA_ATTACK2, + NSPA_ATTACK3, + + NSPA_PAIN1, + NSPA_PAIN2, + + NSPA_DEATH1, + NSPA_DEAD1, + NSPA_DEATH2, + NSPA_DEAD2, + NSPA_DEATH3, + NSPA_DEAD3, + + MAX_NONSEG_PLAYER_ANIMATIONS, + + NSPA_WALKBACK, + + MAX_NONSEG_PLAYER_TOTALANIMATIONS +} nonSegPlayerAnimNumber_t; + +//TA: for buildable animations +typedef enum +{ + BANIM_NONE, + + BANIM_CONSTRUCT1, + BANIM_CONSTRUCT2, + + BANIM_IDLE1, + BANIM_IDLE2, + BANIM_IDLE3, + + BANIM_ATTACK1, + BANIM_ATTACK2, + + BANIM_SPAWN1, + BANIM_SPAWN2, + + BANIM_PAIN1, + BANIM_PAIN2, + + BANIM_DESTROY1, + BANIM_DESTROY2, + BANIM_DESTROYED, + + MAX_BUILDABLE_ANIMATIONS +} buildableAnimNumber_t; + +typedef struct animation_s +{ + int firstFrame; + int numFrames; + int loopFrames; // 0 to numFrames + int frameLerp; // msec between frames + int initialLerp; // msec to get to first frame + int reversed; // true if animation is reversed + int flipflop; // true if animation should flipflop back to base +} animation_t; + + +// flip the togglebit every time an animation +// changes so a restart of the same anim can be detected +#define ANIM_TOGGLEBIT 0x80 +#define ANIM_FORCEBIT 0x40 + + +typedef enum +{ + TEAM_FREE, + TEAM_SPECTATOR, + + TEAM_NUM_TEAMS +} team_t; + +// Time between location updates +#define TEAM_LOCATION_UPDATE_TIME 1000 + +// How many players on the overlay +#define TEAM_MAXOVERLAY 32 + +//TA: player classes +typedef enum +{ + PCL_NONE, + + //builder classes + PCL_ALIEN_BUILDER0, + PCL_ALIEN_BUILDER0_UPG, + + //offensive classes + PCL_ALIEN_LEVEL0, + PCL_ALIEN_LEVEL1, + PCL_ALIEN_LEVEL1_UPG, + PCL_ALIEN_LEVEL2, + PCL_ALIEN_LEVEL2_UPG, + PCL_ALIEN_LEVEL3, + PCL_ALIEN_LEVEL3_UPG, + PCL_ALIEN_LEVEL4, + + //human class + PCL_HUMAN, + PCL_HUMAN_BSUIT, + + PCL_NUM_CLASSES +} pClass_t; + + +//TA: player teams +typedef enum +{ + PTE_NONE, + PTE_ALIENS, + PTE_HUMANS, + + PTE_NUM_TEAMS +} pTeam_t; + + +// means of death +typedef enum +{ + MOD_UNKNOWN, + MOD_SHOTGUN, + MOD_BLASTER, + MOD_PAINSAW, + MOD_MACHINEGUN, + MOD_CHAINGUN, + MOD_PRIFLE, + MOD_MDRIVER, + MOD_LASGUN, + MOD_LCANNON, + MOD_LCANNON_SPLASH, + MOD_FLAMER, + MOD_FLAMER_SPLASH, + MOD_GRENADE, + MOD_WATER, + MOD_SLIME, + MOD_LAVA, + MOD_CRUSH, + MOD_TELEFRAG, + MOD_FALLING, + MOD_SUICIDE, + MOD_TARGET_LASER, + MOD_TRIGGER_HURT, + + MOD_ABUILDER_CLAW, + MOD_LEVEL0_BITE, + MOD_LEVEL1_CLAW, + MOD_LEVEL1_PCLOUD, + MOD_LEVEL3_CLAW, + MOD_LEVEL3_POUNCE, + MOD_LEVEL3_BOUNCEBALL, + MOD_LEVEL2_CLAW, + MOD_LEVEL2_ZAP, + MOD_LEVEL4_CLAW, + MOD_LEVEL4_CHARGE, + + MOD_SLOWBLOB, + MOD_POISON, + MOD_SWARM, + + MOD_HSPAWN, + MOD_TESLAGEN, + MOD_MGTURRET, + MOD_REACTOR, + + MOD_ASPAWN, + MOD_ATUBE, + MOD_OVERMIND, + MOD_SLAP +} meansOfDeath_t; + + +//--------------------------------------------------------- + +//TA: player class record +typedef struct +{ + int classNum; + + char *className; + char *humanName; + + char *modelName; + float modelScale; + char *skinName; + float shadowScale; + + char *hudName; + + int stages; + + vec3_t mins; + vec3_t maxs; + vec3_t crouchMaxs; + vec3_t deadMins; + vec3_t deadMaxs; + float zOffset; + + int viewheight; + int crouchViewheight; + + int health; + float fallDamage; + int regenRate; + + int abilities; + + weapon_t startWeapon; + + float buildDist; + + int fov; + float bob; + float bobCycle; + int steptime; + + float speed; + float acceleration; + float airAcceleration; + float friction; + float stopSpeed; + float jumpMagnitude; + float knockbackScale; + + int children[ 3 ]; + int cost; + int value; +} classAttributes_t; + +typedef struct +{ + char modelName[ MAX_QPATH ]; + float modelScale; + char skinName[ MAX_QPATH ]; + float shadowScale; + char hudName[ MAX_QPATH ]; + char humanName[ MAX_STRING_CHARS ]; + + vec3_t mins; + vec3_t maxs; + vec3_t crouchMaxs; + vec3_t deadMins; + vec3_t deadMaxs; + int viewheight; + int crouchViewheight; + float zOffset; +} classAttributeOverrides_t; + +//stages +typedef enum +{ + S1, + S2, + S3 +} stage_t; + +#define MAX_BUILDABLE_MODELS 4 + +//TA: buildable item record +typedef struct +{ + int buildNum; + + char *buildName; + char *humanName; + char *entityName; + + char *models[ MAX_BUILDABLE_MODELS ]; + float modelScale; + + vec3_t mins; + vec3_t maxs; + float zOffset; + + trType_t traj; + float bounce; + + int buildPoints; + int stages; + + int health; + int regenRate; + + int splashDamage; + int splashRadius; + + int meansOfDeath; + + int team; + weapon_t buildWeapon; + + int idleAnim; + + int nextthink; + int buildTime; + qboolean usable; + + int turretRange; + int turretFireSpeed; + weapon_t turretProjType; + + float minNormal; + qboolean invertNormal; + + qboolean creepTest; + int creepSize; + + qboolean dccTest; + qboolean transparentTest; + qboolean reactorTest; + qboolean replaceable; +} buildableAttributes_t; + +typedef struct +{ + char models[ MAX_BUILDABLE_MODELS ][ MAX_QPATH ]; + + float modelScale; + vec3_t mins; + vec3_t maxs; + float zOffset; +} buildableAttributeOverrides_t; + +//TA: weapon record +typedef struct +{ + int weaponNum; + + int price; + int stages; + + int slots; + + char *weaponName; + char *weaponHumanName; + + int maxAmmo; + int maxClips; + qboolean infiniteAmmo; + qboolean usesEnergy; + + int repeatRate1; + int repeatRate2; + int repeatRate3; + int reloadTime; + float knockbackScale; + + qboolean hasAltMode; + qboolean hasThirdMode; + + qboolean canZoom; + float zoomFov; + + qboolean purchasable; + qboolean longRanged; + + int buildDelay; + + WUTeam_t team; +} weaponAttributes_t; + +//TA: upgrade record +typedef struct +{ + int upgradeNum; + + int price; + int stages; + + int slots; + + char *upgradeName; + char *upgradeHumanName; + + char *icon; + + qboolean purchasable; + qboolean usable; + + WUTeam_t team; +} upgradeAttributes_t; + + +//TA: +void BG_UnpackAmmoArray( int weapon, int psAmmo[ ], int psAmmo2[ ], int *ammo, int *clips ); +void BG_PackAmmoArray( int weapon, int psAmmo[ ], int psAmmo2[ ], int ammo, int clips ); +qboolean BG_WeaponIsFull( weapon_t weapon, int stats[ ], int psAmmo[ ], int psAmmo2[ ] ); +void BG_AddWeaponToInventory( int weapon, int stats[ ] ); +void BG_RemoveWeaponFromInventory( int weapon, int stats[ ] ); +qboolean BG_InventoryContainsWeapon( int weapon, int stats[ ] ); +void BG_AddUpgradeToInventory( int item, int stats[ ] ); +void BG_RemoveUpgradeFromInventory( int item, int stats[ ] ); +qboolean BG_InventoryContainsUpgrade( int item, int stats[ ] ); +void BG_ActivateUpgrade( int item, int stats[ ] ); +void BG_DeactivateUpgrade( int item, int stats[ ] ); +qboolean BG_UpgradeIsActive( int item, int stats[ ] ); +qboolean BG_RotateAxis( vec3_t surfNormal, vec3_t inAxis[ 3 ], + vec3_t outAxis[ 3 ], qboolean inverse, qboolean ceiling ); +void BG_PositionBuildableRelativeToPlayer( const playerState_t *ps, + const vec3_t mins, const vec3_t maxs, + void (*trace)( trace_t *, const vec3_t, const vec3_t, + const vec3_t, const vec3_t, int, int ), + vec3_t outOrigin, vec3_t outAngles, trace_t *tr ); +int BG_GetValueOfHuman( playerState_t *ps ); +int BG_GetValueOfEquipment( playerState_t *ps ); + +int BG_FindBuildNumForName( char *name ); +int BG_FindBuildNumForEntityName( char *name ); +char *BG_FindNameForBuildable( int bclass ); +char *BG_FindHumanNameForBuildable( int bclass ); +char *BG_FindEntityNameForBuildable( int bclass ); +char *BG_FindModelsForBuildable( int bclass, int modelNum ); +float BG_FindModelScaleForBuildable( int bclass ); +void BG_FindBBoxForBuildable( int bclass, vec3_t mins, vec3_t maxs ); +float BG_FindZOffsetForBuildable( int pclass ); +int BG_FindHealthForBuildable( int bclass ); +int BG_FindRegenRateForBuildable( int bclass ); +trType_t BG_FindTrajectoryForBuildable( int bclass ); +float BG_FindBounceForBuildable( int bclass ); +int BG_FindBuildPointsForBuildable( int bclass ); +qboolean BG_FindStagesForBuildable( int bclass, stage_t stage ); +int BG_FindSplashDamageForBuildable( int bclass ); +int BG_FindSplashRadiusForBuildable( int bclass ); +int BG_FindMODForBuildable( int bclass ); +int BG_FindTeamForBuildable( int bclass ); +weapon_t BG_FindBuildWeaponForBuildable( int bclass ); +int BG_FindAnimForBuildable( int bclass ); +int BG_FindNextThinkForBuildable( int bclass ); +int BG_FindBuildTimeForBuildable( int bclass ); +qboolean BG_FindUsableForBuildable( int bclass ); +int BG_FindRangeForBuildable( int bclass ); +int BG_FindFireSpeedForBuildable( int bclass ); +weapon_t BG_FindProjTypeForBuildable( int bclass ); +float BG_FindMinNormalForBuildable( int bclass ); +qboolean BG_FindInvertNormalForBuildable( int bclass ); +int BG_FindCreepTestForBuildable( int bclass ); +int BG_FindCreepSizeForBuildable( int bclass ); +int BG_FindDCCTestForBuildable( int bclass ); +int BG_FindUniqueTestForBuildable( int bclass ); +qboolean BG_FindReplaceableTestForBuildable( int bclass ); +qboolean BG_FindTransparentTestForBuildable( int bclass ); +void BG_InitBuildableOverrides( void ); + +int BG_FindClassNumForName( char *name ); +char *BG_FindNameForClassNum( int pclass ); +char *BG_FindHumanNameForClassNum( int pclass ); +char *BG_FindModelNameForClass( int pclass ); +float BG_FindModelScaleForClass( int pclass ); +char *BG_FindSkinNameForClass( int pclass ); +float BG_FindShadowScaleForClass( int pclass ); +char *BG_FindHudNameForClass( int pclass ); +qboolean BG_FindStagesForClass( int pclass, stage_t stage ); +void BG_FindBBoxForClass( int pclass, vec3_t mins, vec3_t maxs, vec3_t cmaxs, vec3_t dmins, vec3_t dmaxs ); +float BG_FindZOffsetForClass( int pclass ); +void BG_FindViewheightForClass( int pclass, int *viewheight, int *cViewheight ); +int BG_FindHealthForClass( int pclass ); +float BG_FindFallDamageForClass( int pclass ); +int BG_FindRegenRateForClass( int pclass ); +int BG_FindFovForClass( int pclass ); +float BG_FindBobForClass( int pclass ); +float BG_FindBobCycleForClass( int pclass ); +float BG_FindSpeedForClass( int pclass ); +float BG_FindAccelerationForClass( int pclass ); +float BG_FindAirAccelerationForClass( int pclass ); +float BG_FindFrictionForClass( int pclass ); +float BG_FindStopSpeedForClass( int pclass ); +float BG_FindJumpMagnitudeForClass( int pclass ); +float BG_FindKnockbackScaleForClass( int pclass ); +int BG_FindSteptimeForClass( int pclass ); +qboolean BG_ClassHasAbility( int pclass, int ability ); +weapon_t BG_FindStartWeaponForClass( int pclass ); +float BG_FindBuildDistForClass( int pclass ); +int BG_ClassCanEvolveFromTo( int fclass, int tclass, int credits, int num ); +int BG_FindCostOfClass( int pclass ); +int BG_FindValueOfClass( int pclass ); +void BG_InitClassOverrides( void ); + +int BG_FindPriceForWeapon( int weapon ); +qboolean BG_FindStagesForWeapon( int weapon, stage_t stage ); +int BG_FindSlotsForWeapon( int weapon ); +char *BG_FindNameForWeapon( int weapon ); +int BG_FindWeaponNumForName( char *name ); +char *BG_FindHumanNameForWeapon( int weapon ); +char *BG_FindModelsForWeapon( int weapon, int modelNum ); +char *BG_FindIconForWeapon( int weapon ); +char *BG_FindCrosshairForWeapon( int weapon ); +int BG_FindCrosshairSizeForWeapon( int weapon ); +void BG_FindAmmoForWeapon( int weapon, int *maxAmmo, int *maxClips ); +qboolean BG_FindInfinteAmmoForWeapon( int weapon ); +qboolean BG_FindUsesEnergyForWeapon( int weapon ); +int BG_FindRepeatRate1ForWeapon( int weapon ); +int BG_FindRepeatRate2ForWeapon( int weapon ); +int BG_FindRepeatRate3ForWeapon( int weapon ); +int BG_FindReloadTimeForWeapon( int weapon ); +float BG_FindKnockbackScaleForWeapon( int weapon ); +qboolean BG_WeaponHasAltMode( int weapon ); +qboolean BG_WeaponHasThirdMode( int weapon ); +qboolean BG_WeaponCanZoom( int weapon ); +float BG_FindZoomFovForWeapon( int weapon ); +qboolean BG_FindPurchasableForWeapon( int weapon ); +qboolean BG_FindLongRangedForWeapon( int weapon ); +int BG_FindBuildDelayForWeapon( int weapon ); +WUTeam_t BG_FindTeamForWeapon( int weapon ); + +int BG_FindPriceForUpgrade( int upgrade ); +qboolean BG_FindStagesForUpgrade( int upgrade, stage_t stage ); +int BG_FindSlotsForUpgrade( int upgrade ); +char *BG_FindNameForUpgrade( int upgrade ); +int BG_FindUpgradeNumForName( char *name ); +char *BG_FindHumanNameForUpgrade( int upgrade ); +char *BG_FindIconForUpgrade( int upgrade ); +qboolean BG_FindPurchasableForUpgrade( int upgrade ); +qboolean BG_FindUsableForUpgrade( int upgrade ); +WUTeam_t BG_FindTeamForUpgrade( int upgrade ); + +// content masks +#define MASK_ALL (-1) +#define MASK_SOLID (CONTENTS_SOLID) +#define MASK_PLAYERSOLID (CONTENTS_SOLID|CONTENTS_PLAYERCLIP|CONTENTS_BODY) +#define MASK_DEADSOLID (CONTENTS_SOLID|CONTENTS_PLAYERCLIP) +#define MASK_WATER (CONTENTS_WATER|CONTENTS_LAVA|CONTENTS_SLIME) +#define MASK_OPAQUE (CONTENTS_SOLID|CONTENTS_SLIME|CONTENTS_LAVA) +#define MASK_SHOT (CONTENTS_SOLID|CONTENTS_BODY) + + +// +// entityState_t->eType +// +typedef enum +{ + ET_GENERAL, + ET_PLAYER, + ET_ITEM, + + ET_BUILDABLE, //TA: buildable type + + ET_MISSILE, + ET_MOVER, + ET_BEAM, + ET_PORTAL, + ET_SPEAKER, + ET_PUSH_TRIGGER, + ET_TELEPORT_TRIGGER, + ET_INVISIBLE, + ET_GRAPPLE, // grapple hooked on wall + + ET_CORPSE, + ET_PARTICLE_SYSTEM, + ET_ANIMMAPOBJ, + ET_MODELDOOR, + ET_LIGHTFLARE, + ET_LEV2_ZAP_CHAIN, + + ET_EVENTS // any of the EV_* events can be added freestanding + // by setting eType to ET_EVENTS + eventNum + // this avoids having to set eFlags and eventNum +} entityType_t; + +void BG_EvaluateTrajectory( const trajectory_t *tr, int atTime, vec3_t result ); +void BG_EvaluateTrajectoryDelta( const trajectory_t *tr, int atTime, vec3_t result ); + +void BG_AddPredictableEventToPlayerstate( int newEvent, int eventParm, playerState_t *ps ); + +void BG_PlayerStateToEntityState( playerState_t *ps, entityState_t *s, qboolean snap ); +void BG_PlayerStateToEntityStateExtraPolate( playerState_t *ps, entityState_t *s, int time, qboolean snap ); + +qboolean BG_PlayerTouchesItem( playerState_t *ps, entityState_t *item, int atTime ); + +#define ARENAS_PER_TIER 4 +#define MAX_ARENAS 1024 +#define MAX_ARENAS_TEXT 8192 + +#define MAX_BOTS 1024 +#define MAX_BOTS_TEXT 8192 + +float atof_neg( char *token, qboolean allowNegative ); +int atoi_neg( char *token, qboolean allowNegative ); + +void BG_ParseCSVEquipmentList( const char *string, weapon_t *weapons, int weaponsSize, + upgrade_t *upgrades, int upgradesSize ); +void BG_ParseCSVClassList( const char *string, pClass_t *classes, int classesSize ); +void BG_ParseCSVBuildableList( const char *string, buildable_t *buildables, int buildablesSize ); +void BG_InitAllowedGameElements( void ); +qboolean BG_WeaponIsAllowed( weapon_t weapon ); +qboolean BG_UpgradeIsAllowed( upgrade_t upgrade ); +qboolean BG_ClassIsAllowed( pClass_t class ); +qboolean BG_BuildableIsAllowed( buildable_t buildable ); +qboolean BG_UpgradeClassAvailable( playerState_t *ps ); + +typedef struct +{ + unsigned int hi; + unsigned int lo; +} clientList_t; +qboolean BG_ClientListTest( clientList_t *list, int clientNum ); +void BG_ClientListAdd( clientList_t *list, int clientNum ); +void BG_ClientListRemove( clientList_t *list, int clientNum ); +char *BG_ClientListString( clientList_t *list ); +void BG_ClientListParse( clientList_t *list, const char *s ); + +// Friendly Fire Flags +#define FFF_HUMANS 1 +#define FFF_ALIENS 2 +#define FFF_BUILDABLES 4 + diff --git a/src/game/bg_slidemove.c b/src/game/bg_slidemove.c new file mode 100644 index 0000000..aa32a6a --- /dev/null +++ b/src/game/bg_slidemove.c @@ -0,0 +1,416 @@ +/* +=========================================================================== +Copyright (C) 1999-2005 Id Software, Inc. +Copyright (C) 2000-2006 Tim Angus + +This file is part of Tremulous. + +Tremulous is free software; you can redistribute it +and/or modify it under the terms of the GNU General Public License as +published by the Free Software Foundation; either version 2 of the License, +or (at your option) any later version. + +Tremulous is distributed in the hope that it will be +useful, but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with Tremulous; if not, write to the Free Software +Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA +=========================================================================== +*/ + +// bg_slidemove.c -- part of bg_pmove functionality + +#include "../qcommon/q_shared.h" +#include "bg_public.h" +#include "bg_local.h" + +/* + +input: origin, velocity, bounds, groundPlane, trace function + +output: origin, velocity, impacts, stairup boolean + +*/ + +/* +================== +PM_SlideMove + +Returns qtrue if the velocity was clipped in some way +================== +*/ +#define MAX_CLIP_PLANES 5 +qboolean PM_SlideMove( qboolean gravity ) +{ + int bumpcount, numbumps; + vec3_t dir; + float d; + int numplanes; + vec3_t planes[MAX_CLIP_PLANES]; + vec3_t primal_velocity; + vec3_t clipVelocity; + int i, j, k; + trace_t trace; + vec3_t end; + float time_left; + float into; + vec3_t endVelocity; + vec3_t endClipVelocity; + + numbumps = 4; + + VectorCopy( pm->ps->velocity, primal_velocity ); + + if( gravity ) + { + VectorCopy( pm->ps->velocity, endVelocity ); + endVelocity[ 2 ] -= pm->ps->gravity * pml.frametime; + pm->ps->velocity[ 2 ] = ( pm->ps->velocity[ 2 ] + endVelocity[ 2 ] ) * 0.5; + primal_velocity[ 2 ] = endVelocity[ 2 ]; + + if( pml.groundPlane ) + { + // slide along the ground plane + PM_ClipVelocity( pm->ps->velocity, pml.groundTrace.plane.normal, + pm->ps->velocity, OVERCLIP ); + } + } + + time_left = pml.frametime; + + // never turn against the ground plane + if( pml.groundPlane ) + { + numplanes = 1; + VectorCopy( pml.groundTrace.plane.normal, planes[ 0 ] ); + } + else + numplanes = 0; + + // never turn against original velocity + VectorNormalize2( pm->ps->velocity, planes[ numplanes ] ); + numplanes++; + + for( bumpcount = 0; bumpcount < numbumps; bumpcount++ ) + { + // calculate position we are trying to move to + VectorMA( pm->ps->origin, time_left, pm->ps->velocity, end ); + + // see if we can make it there + pm->trace( &trace, pm->ps->origin, pm->mins, pm->maxs, end, pm->ps->clientNum, pm->tracemask ); + + if( trace.allsolid ) + { + // entity is completely trapped in another solid + pm->ps->velocity[ 2 ] = 0; // don't build up falling damage, but allow sideways acceleration + return qtrue; + } + + if( trace.fraction > 0 ) + { + // actually covered some distance + VectorCopy( trace.endpos, pm->ps->origin ); + } + + if( trace.fraction == 1 ) + break; // moved the entire distance + + // save entity for contact + PM_AddTouchEnt( trace.entityNum ); + + time_left -= time_left * trace.fraction; + + if( numplanes >= MAX_CLIP_PLANES ) + { + // this shouldn't really happen + VectorClear( pm->ps->velocity ); + return qtrue; + } + + // + // if this is the same plane we hit before, nudge velocity + // out along it, which fixes some epsilon issues with + // non-axial planes + // + for( i = 0 ; i < numplanes ; i++ ) + { + if( DotProduct( trace.plane.normal, planes[i] ) > 0.99 ) + { + VectorAdd( trace.plane.normal, pm->ps->velocity, pm->ps->velocity ); + break; + } + } + + if( i < numplanes ) + continue; + + VectorCopy( trace.plane.normal, planes[ numplanes ] ); + numplanes++; + + // + // modify velocity so it parallels all of the clip planes + // + + // find a plane that it enters + for( i = 0; i < numplanes; i++ ) + { + into = DotProduct( pm->ps->velocity, planes[ i ] ); + if( into >= 0.1 ) + continue; // move doesn't interact with the plane + + // see how hard we are hitting things + if( -into > pml.impactSpeed ) + pml.impactSpeed = -into; + + // slide along the plane + PM_ClipVelocity( pm->ps->velocity, planes[ i ], clipVelocity, OVERCLIP ); + + // slide along the plane + PM_ClipVelocity( endVelocity, planes[ i ], endClipVelocity, OVERCLIP ); + + // see if there is a second plane that the new move enters + for( j = 0; j < numplanes; j++ ) + { + if( j == i ) + continue; + + if( DotProduct( clipVelocity, planes[ j ] ) >= 0.1 ) + continue; // move doesn't interact with the plane + + // try clipping the move to the plane + PM_ClipVelocity( clipVelocity, planes[ j ], clipVelocity, OVERCLIP ); + PM_ClipVelocity( endClipVelocity, planes[ j ], endClipVelocity, OVERCLIP ); + + // see if it goes back into the first clip plane + if( DotProduct( clipVelocity, planes[ i ] ) >= 0 ) + continue; + + // slide the original velocity along the crease + CrossProduct( planes[ i ], planes[ j ], dir ); + VectorNormalize( dir ); + d = DotProduct( dir, pm->ps->velocity ); + VectorScale( dir, d, clipVelocity ); + + CrossProduct( planes[ i ], planes[ j ], dir); + VectorNormalize( dir ); + d = DotProduct( dir, endVelocity ); + VectorScale( dir, d, endClipVelocity ); + + // see if there is a third plane the the new move enters + for( k = 0; k < numplanes; k++ ) + { + if( k == i || k == j ) + continue; + + if( DotProduct( clipVelocity, planes[ k ] ) >= 0.1 ) + continue; // move doesn't interact with the plane + + // stop dead at a tripple plane interaction + VectorClear( pm->ps->velocity ); + return qtrue; + } + } + + // if we have fixed all interactions, try another move + VectorCopy( clipVelocity, pm->ps->velocity ); + VectorCopy( endClipVelocity, endVelocity ); + break; + } + } + + if( gravity ) + VectorCopy( endVelocity, pm->ps->velocity ); + + // don't change velocity if in a timer (FIXME: is this correct?) + if( pm->ps->pm_time ) + VectorCopy( primal_velocity, pm->ps->velocity ); + + return ( bumpcount != 0 ); +} + +/* +================== +PM_StepEvent +================== +*/ +void PM_StepEvent( vec3_t from, vec3_t to, vec3_t normal ) +{ + float size; + vec3_t delta, dNormal; + + VectorSubtract( from, to, delta ); + VectorCopy( delta, dNormal ); + VectorNormalize( dNormal ); + + size = DotProduct( normal, dNormal ) * VectorLength( delta ); + + if( size > 0.0f ) + { + if( size > 2.0f ) + { + if( size < 7.0f ) + PM_AddEvent( EV_STEPDN_4 ); + else if( size < 11.0f ) + PM_AddEvent( EV_STEPDN_8 ); + else if( size < 15.0f ) + PM_AddEvent( EV_STEPDN_12 ); + else + PM_AddEvent( EV_STEPDN_16 ); + } + } + else + { + size = fabs( size ); + + if( size > 2.0f ) + { + if( size < 7.0f ) + PM_AddEvent( EV_STEP_4 ); + else if( size < 11.0f ) + PM_AddEvent( EV_STEP_8 ); + else if( size < 15.0f ) + PM_AddEvent( EV_STEP_12 ); + else + PM_AddEvent( EV_STEP_16 ); + } + } + + if( pm->debugLevel ) + Com_Printf( "%i:stepped\n", c_pmove ); +} + +/* +================== +PM_StepSlideMove +================== +*/ +qboolean PM_StepSlideMove( qboolean gravity, qboolean predictive ) +{ + vec3_t start_o, start_v; + vec3_t down_o, down_v; + trace_t trace; + vec3_t normal; + vec3_t step_v, step_vNormal; + vec3_t up, down; + float stepSize; + qboolean stepped = qfalse; + + if( pm->ps->stats[ STAT_STATE ] & SS_WALLCLIMBING ) + { + if( pm->ps->stats[ STAT_STATE ] & SS_WALLCLIMBINGCEILING ) + VectorSet( normal, 0.0f, 0.0f, -1.0f ); + else + VectorCopy( pm->ps->grapplePoint, normal ); + } + else + VectorSet( normal, 0.0f, 0.0f, 1.0f ); + + VectorCopy( pm->ps->origin, start_o ); + VectorCopy( pm->ps->velocity, start_v ); + + if( PM_SlideMove( gravity ) == 0 ) + { + VectorCopy( start_o, down ); + VectorMA( down, -STEPSIZE, normal, down ); + pm->trace( &trace, start_o, pm->mins, pm->maxs, down, pm->ps->clientNum, pm->tracemask ); + + //we can step down + if( trace.fraction > 0.01f && trace.fraction < 1.0f && + !trace.allsolid && pml.groundPlane != qfalse ) + { + if( pm->debugLevel ) + Com_Printf( "%d: step down\n", c_pmove ); + + stepped = qtrue; + } + } + else + { + VectorCopy( start_o, down ); + VectorMA( down, -STEPSIZE, normal, down ); + pm->trace( &trace, start_o, pm->mins, pm->maxs, down, pm->ps->clientNum, pm->tracemask ); + // never step up when you still have up velocity + if( DotProduct( trace.plane.normal, pm->ps->velocity ) > 0.0f && + ( trace.fraction == 1.0f || DotProduct( trace.plane.normal, normal ) < 0.7f ) ) + { + return stepped; + } + + VectorCopy( pm->ps->origin, down_o ); + VectorCopy( pm->ps->velocity, down_v ); + + VectorCopy( start_o, up ); + VectorMA( up, STEPSIZE, normal, up ); + + // test the player position if they were a stepheight higher + pm->trace( &trace, start_o, pm->mins, pm->maxs, up, pm->ps->clientNum, pm->tracemask ); + if( trace.allsolid ) + { + if( pm->debugLevel ) + Com_Printf( "%i:bend can't step\n", c_pmove ); + + return stepped; // can't step up + } + + VectorSubtract( trace.endpos, start_o, step_v ); + VectorCopy( step_v, step_vNormal ); + VectorNormalize( step_vNormal ); + + stepSize = DotProduct( normal, step_vNormal ) * VectorLength( step_v ); + // try slidemove from this position + VectorCopy( trace.endpos, pm->ps->origin ); + VectorCopy( start_v, pm->ps->velocity ); + + if( PM_SlideMove( gravity ) == 0 ) + { + if( pm->debugLevel ) + Com_Printf( "%d: step up\n", c_pmove ); + + stepped = qtrue; + } + + // push down the final amount + VectorCopy( pm->ps->origin, down ); + VectorMA( down, -stepSize, normal, down ); + pm->trace( &trace, pm->ps->origin, pm->mins, pm->maxs, down, pm->ps->clientNum, pm->tracemask ); + + if( !trace.allsolid ) + VectorCopy( trace.endpos, pm->ps->origin ); + + if( trace.fraction < 1.0f ) + PM_ClipVelocity( pm->ps->velocity, trace.plane.normal, pm->ps->velocity, OVERCLIP ); + } + + if( !predictive && stepped ) + PM_StepEvent( start_o, pm->ps->origin, normal ); + + return stepped; +} + +/* +================== +PM_PredictStepMove +================== +*/ +qboolean PM_PredictStepMove( void ) +{ + vec3_t velocity, origin; + float impactSpeed; + qboolean stepped = qfalse; + + VectorCopy( pm->ps->velocity, velocity ); + VectorCopy( pm->ps->origin, origin ); + impactSpeed = pml.impactSpeed; + + if( PM_StepSlideMove( qfalse, qtrue ) ) + stepped = qtrue; + + VectorCopy( velocity, pm->ps->velocity ); + VectorCopy( origin, pm->ps->origin ); + pml.impactSpeed = impactSpeed; + + return stepped; +} diff --git a/src/game/g_active.c b/src/game/g_active.c new file mode 100644 index 0000000..57f58fe --- /dev/null +++ b/src/game/g_active.c @@ -0,0 +1,2032 @@ +/* +=========================================================================== +Copyright (C) 1999-2005 Id Software, Inc. +Copyright (C) 2000-2006 Tim Angus + +This file is part of Tremulous. + +Tremulous is free software; you can redistribute it +and/or modify it under the terms of the GNU General Public License as +published by the Free Software Foundation; either version 2 of the License, +or (at your option) any later version. + +Tremulous is distributed in the hope that it will be +useful, but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with Tremulous; if not, write to the Free Software +Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA +=========================================================================== +*/ + +#include "g_local.h" + +/* +=============== +G_DamageFeedback + +Called just before a snapshot is sent to the given player. +Totals up all damage and generates both the player_state_t +damage values to that client for pain blends and kicks, and +global pain sound events for all clients. +=============== +*/ +void P_DamageFeedback( gentity_t *player ) +{ + gclient_t *client; + float count; + vec3_t angles; + + client = player->client; + if( client->ps.pm_type == PM_DEAD ) + return; + + // total points of damage shot at the player this frame + count = client->damage_blood + client->damage_armor; + if( count == 0 ) + return; // didn't take any damage + + if( count > 255 ) + count = 255; + + // send the information to the client + + // world damage (falling, slime, etc) uses a special code + // to make the blend blob centered instead of positional + if( client->damage_fromWorld ) + { + client->ps.damagePitch = 255; + client->ps.damageYaw = 255; + + client->damage_fromWorld = qfalse; + } + else + { + vectoangles( client->damage_from, angles ); + client->ps.damagePitch = angles[ PITCH ] / 360.0 * 256; + client->ps.damageYaw = angles[ YAW ] / 360.0 * 256; + } + + // play an apropriate pain sound + if( ( level.time > player->pain_debounce_time ) && !( player->flags & FL_GODMODE ) ) + { + player->pain_debounce_time = level.time + 700; + G_AddEvent( player, EV_PAIN, player->health > 255 ? 255 : player->health ); + client->ps.damageEvent++; + } + + + client->ps.damageCount = count; + + // + // clear totals + // + client->damage_blood = 0; + client->damage_armor = 0; + client->damage_knockback = 0; +} + + + +/* +============= +P_WorldEffects + +Check for lava / slime contents and drowning +============= +*/ +void P_WorldEffects( gentity_t *ent ) +{ + int waterlevel; + + if( ent->client->noclip ) + { + ent->client->airOutTime = level.time + 12000; // don't need air + return; + } + + waterlevel = ent->waterlevel; + + // + // check for drowning + // + if( waterlevel == 3 ) + { + // if out of air, start drowning + if( ent->client->airOutTime < level.time) + { + // drown! + ent->client->airOutTime += 1000; + if( ent->health > 0 ) + { + // take more damage the longer underwater + ent->damage += 2; + if( ent->damage > 15 ) + ent->damage = 15; + + // play a gurp sound instead of a normal pain sound + if( ent->health <= ent->damage ) + G_Sound( ent, CHAN_VOICE, G_SoundIndex( "*drown.wav" ) ); + else if( rand( ) & 1 ) + G_Sound( ent, CHAN_VOICE, G_SoundIndex( "sound/player/gurp1.wav" ) ); + else + G_Sound( ent, CHAN_VOICE, G_SoundIndex( "sound/player/gurp2.wav" ) ); + + // don't play a normal pain sound + ent->pain_debounce_time = level.time + 200; + + G_Damage( ent, NULL, NULL, NULL, NULL, + ent->damage, DAMAGE_NO_ARMOR, MOD_WATER ); + } + } + } + else + { + ent->client->airOutTime = level.time + 12000; + ent->damage = 2; + } + + // + // check for sizzle damage (move to pmove?) + // + if( waterlevel && + ( ent->watertype & ( CONTENTS_LAVA | CONTENTS_SLIME ) ) ) + { + if( ent->health > 0 && + ent->pain_debounce_time <= level.time ) + { + if( ent->watertype & CONTENTS_LAVA ) + { + G_Damage( ent, NULL, NULL, NULL, NULL, + 30 * waterlevel, 0, MOD_LAVA ); + } + + if( ent->watertype & CONTENTS_SLIME ) + { + G_Damage( ent, NULL, NULL, NULL, NULL, + 10 * waterlevel, 0, MOD_SLIME ); + } + } + } +} + + + +/* +=============== +G_SetClientSound +=============== +*/ +void G_SetClientSound( gentity_t *ent ) +{ + if( ent->waterlevel && ( ent->watertype & ( CONTENTS_LAVA | CONTENTS_SLIME ) ) ) + ent->client->ps.loopSound = level.snd_fry; + else + ent->client->ps.loopSound = 0; +} + + + +//============================================================== + +static void G_ClientShove( gentity_t *ent, gentity_t *victim ) +{ + vec3_t dir, push; + int entMass = 200, vicMass = 200; + + // shoving enemies changes gameplay too much + if( !OnSameTeam( ent, victim ) ) + return; + + if ( ( victim->client->ps.weapon >= WP_ABUILD ) && + ( victim->client->ps.weapon <= WP_HBUILD ) && + ( victim->client->ps.stats[ STAT_BUILDABLE ] != BA_NONE ) ) + return; + + // alien mass is directly related to their health points + // human mass is 200, double for bsuit + if( ent->client->pers.teamSelection == PTE_ALIENS ) + { + entMass = BG_FindHealthForClass( ent->client->pers.classSelection ); + } + else if( ent->client->pers.teamSelection == PTE_HUMANS ) + { + if( BG_InventoryContainsUpgrade( UP_BATTLESUIT, ent->client->ps.stats ) ) + entMass *= 2; + } + else + return; + + if( victim->client->pers.teamSelection == PTE_ALIENS ) + { + vicMass = BG_FindHealthForClass( victim->client->pers.classSelection ); + } + else if( BG_InventoryContainsUpgrade( UP_BATTLESUIT, + victim->client->ps.stats ) ) + { + vicMass *= 2; + } + + if( vicMass <= 0 || entMass <= 0 ) + return; + + VectorSubtract( victim->r.currentOrigin, ent->r.currentOrigin, dir ); + VectorNormalizeFast( dir ); + + // don't break the dretch elevator + if( abs( dir[ 2 ] ) > abs( dir[ 0 ] ) && abs( dir[ 2 ] ) > abs( dir[ 1 ] ) ) + return; + + VectorScale( dir, + ( g_shove.value * ( ( float )entMass / ( float )vicMass ) ), push ); + VectorAdd( victim->client->ps.velocity, push, + victim->client->ps.velocity ); + +} + +/* +============== +ClientImpacts +============== +*/ +void ClientImpacts( gentity_t *ent, pmove_t *pm ) +{ + int i, j; + trace_t trace; + gentity_t *other; + + memset( &trace, 0, sizeof( trace ) ); + + for( i = 0; i < pm->numtouch; i++ ) + { + for( j = 0; j < i; j++ ) + { + if( pm->touchents[ j ] == pm->touchents[ i ] ) + break; + } + + if( j != i ) + continue; // duplicated + + other = &g_entities[ pm->touchents[ i ] ]; + + // see G_UnlaggedDetectCollisions(), this is the inverse of that. + // if our movement is blocked by another player's real position, + // don't use the unlagged position for them because they are + // blocking or server-side Pmove() from reaching it + if( other->client && other->client->unlaggedCalc.used ) + other->client->unlaggedCalc.used = qfalse; + + //charge attack + if( ent->client->ps.weapon == WP_ALEVEL4 && + ent->client->ps.stats[ STAT_MISC ] > 0 && + ent->client->charging ) + ChargeAttack( ent, other ); + + if( ent->client && other->client ) + G_ClientShove( ent, other ); + + if( !other->touch ) + continue; + + other->touch( other, ent, &trace ); + } +} + +/* +============ +G_TouchTriggers + +Find all trigger entities that ent's current position touches. +Spectators will only interact with teleporters. +============ +*/ +void G_TouchTriggers( gentity_t *ent ) +{ + int i, num; + int touch[MAX_GENTITIES]; + gentity_t *hit; + trace_t trace; + vec3_t mins, maxs; + vec3_t pmins, pmaxs; + static vec3_t range = { 10, 10, 10 }; + + if( !ent->client ) + return; + + // dead clients don't activate triggers! + if( ent->client->ps.stats[ STAT_HEALTH ] <= 0 ) + return; + + BG_FindBBoxForClass( ent->client->ps.stats[ STAT_PCLASS ], + pmins, pmaxs, NULL, NULL, NULL ); + + VectorAdd( ent->client->ps.origin, pmins, mins ); + VectorAdd( ent->client->ps.origin, pmaxs, maxs ); + + VectorSubtract( mins, range, mins ); + VectorAdd( maxs, range, maxs ); + + num = trap_EntitiesInBox( mins, maxs, touch, MAX_GENTITIES ); + + // can't use ent->absmin, because that has a one unit pad + VectorAdd( ent->client->ps.origin, ent->r.mins, mins ); + VectorAdd( ent->client->ps.origin, ent->r.maxs, maxs ); + + for( i = 0; i < num; i++ ) + { + hit = &g_entities[ touch[ i ] ]; + + if( !hit->touch && !ent->touch ) + continue; + + if( !( hit->r.contents & CONTENTS_TRIGGER ) ) + continue; + + // ignore most entities if a spectator + if( ( ent->client->sess.sessionTeam == TEAM_SPECTATOR ) || + ( ent->client->ps.stats[ STAT_STATE ] & SS_INFESTING ) || + ( ent->client->ps.stats[ STAT_STATE ] & SS_HOVELING ) ) + { + if( hit->s.eType != ET_TELEPORT_TRIGGER && + // this is ugly but adding a new ET_? type will + // most likely cause network incompatibilities + hit->touch != Touch_DoorTrigger ) + { + //check for manually triggered doors + manualTriggerSpectator( hit, ent ); + continue; + } + } + + if( !trap_EntityContact( mins, maxs, hit ) ) + continue; + + memset( &trace, 0, sizeof( trace ) ); + + if( hit->touch ) + hit->touch( hit, ent, &trace ); + } +} + +/* +================= +SpectatorThink +================= +*/ +void SpectatorThink( gentity_t *ent, usercmd_t *ucmd ) +{ + pmove_t pm; + gclient_t *client; + qboolean attack1, attack3; + qboolean doPmove = qtrue; + + client = ent->client; + + client->oldbuttons = client->buttons; + client->buttons = ucmd->buttons; + + attack1 = ( ( client->buttons & BUTTON_ATTACK ) && + !( client->oldbuttons & BUTTON_ATTACK ) ); + attack3 = ( ( client->buttons & BUTTON_USE_HOLDABLE ) && + !( client->oldbuttons & BUTTON_USE_HOLDABLE ) ); + + if( level.mapRotationVoteTime ) + { + if( attack1 ) + { + G_IntermissionMapVoteCommand( ent, qtrue, qfalse ); + attack1 = qfalse; + } + if( ( client->buttons & BUTTON_ATTACK2 ) && !( client->oldbuttons & BUTTON_ATTACK2 ) ) + G_IntermissionMapVoteCommand( ent, qfalse, qfalse ); + } + + if( client->sess.spectatorState == SPECTATOR_LOCKED || client->sess.spectatorState == SPECTATOR_FOLLOW ) + client->ps.pm_type = PM_FREEZE; + else + client->ps.pm_type = PM_SPECTATOR; + + if ( client->sess.spectatorState == SPECTATOR_FOLLOW ) + { + gclient_t *cl; + if ( client->sess.spectatorClient >= 0 ) + { + cl = &level.clients[ client->sess.spectatorClient ]; + if ( cl->sess.sessionTeam != TEAM_SPECTATOR ) + doPmove = qfalse; + } + } + + if (doPmove) + { + client->ps.speed = BG_FindSpeedForClass( client->ps.stats[ STAT_PCLASS ] ); + + // in case the client entered the queue while following a teammate + if( ( client->pers.teamSelection == PTE_ALIENS && + G_SearchSpawnQueue( &level.alienSpawnQueue, ent-g_entities ) ) || + ( client->pers.teamSelection == PTE_HUMANS && + G_SearchSpawnQueue( &level.humanSpawnQueue, ent-g_entities ) ) ) + { + client->ps.pm_flags |= PMF_QUEUED; + } + + + client->ps.stats[ STAT_STAMINA ] = 0; + client->ps.stats[ STAT_MISC ] = 0; + client->ps.stats[ STAT_BUILDABLE ] = 0; + client->ps.stats[ STAT_PCLASS ] = PCL_NONE; + client->ps.weapon = WP_NONE; + + // set up for pmove + memset( &pm, 0, sizeof( pm ) ); + pm.ps = &client->ps; + pm.cmd = *ucmd; + pm.tracemask = MASK_PLAYERSOLID & ~CONTENTS_BODY; // spectators can fly through bodies + pm.trace = trap_Trace; + pm.pointcontents = trap_PointContents; + + // perform a pmove + Pmove( &pm ); + + // save results of pmove + VectorCopy( client->ps.origin, ent->s.origin ); + + G_TouchTriggers( ent ); + trap_UnlinkEntity( ent ); + + if( ( attack1 ) && ( client->ps.pm_flags & PMF_QUEUED ) ) + { + if( client->ps.stats[ STAT_PTEAM ] == PTE_ALIENS ) + G_RemoveFromSpawnQueue( &level.alienSpawnQueue, client->ps.clientNum ); + else if( client->ps.stats[ STAT_PTEAM ] == PTE_HUMANS ) + G_RemoveFromSpawnQueue( &level.humanSpawnQueue, client->ps.clientNum ); + + client->pers.classSelection = PCL_NONE; + client->ps.stats[ STAT_PCLASS ] = PCL_NONE; + } + + if( attack1 && client->pers.classSelection == PCL_NONE ) + { + if( client->pers.teamSelection == PTE_NONE ) + G_TriggerMenu( client->ps.clientNum, MN_TEAM ); + else if( client->pers.teamSelection == PTE_ALIENS ) + G_TriggerMenu( client->ps.clientNum, MN_A_CLASS ); + else if( client->pers.teamSelection == PTE_HUMANS ) + G_TriggerMenu( client->ps.clientNum, MN_H_SPAWN ); + } + + //set the queue position for the client side + if( client->ps.pm_flags & PMF_QUEUED ) + { + if( client->ps.stats[ STAT_PTEAM ] == PTE_ALIENS ) + { + client->ps.persistant[ PERS_QUEUEPOS ] = + G_GetPosInSpawnQueue( &level.alienSpawnQueue, client->ps.clientNum ); + } + else if( client->ps.stats[ STAT_PTEAM ] == PTE_HUMANS ) + { + client->ps.persistant[ PERS_QUEUEPOS ] = + G_GetPosInSpawnQueue( &level.humanSpawnQueue, client->ps.clientNum ); + } + } + } + + else if( attack1 && ent->client->sess.spectatorState == SPECTATOR_FOLLOW ) + { + G_StopFollowing( ent ); + client->pers.classSelection = PCL_NONE; + if( client->pers.teamSelection == PTE_NONE ) + G_TriggerMenu( ent-g_entities, MN_TEAM ); + else if( client->pers.teamSelection == PTE_ALIENS ) + G_TriggerMenu( ent-g_entities, MN_A_CLASS ); + else if( client->pers.teamSelection == PTE_HUMANS ) + G_TriggerMenu( ent-g_entities, MN_H_SPAWN ); + } + + if( attack3 ) + { + G_ToggleFollow( ent ); + } +} + + + +/* +================= +ClientInactivityTimer + +Returns qfalse if the client is dropped +================= +*/ +qboolean ClientInactivityTimer( gclient_t *client ) +{ + if( ! g_inactivity.integer ) + { + // give everyone some time, so if the operator sets g_inactivity during + // gameplay, everyone isn't kicked + client->inactivityTime = level.time + 60 * 1000; + client->inactivityWarning = qfalse; + } + else if( client->pers.cmd.forwardmove || + client->pers.cmd.rightmove || + client->pers.cmd.upmove || + ( client->pers.cmd.buttons & BUTTON_ATTACK ) ) + { + client->inactivityTime = level.time + g_inactivity.integer * 1000; + client->inactivityWarning = qfalse; + } + else if( !client->pers.localClient ) + { + if( level.time > client->inactivityTime ) + { + trap_DropClient( client - level.clients, "Dropped due to inactivity" ); + return qfalse; + } + + if( level.time > client->inactivityTime - 10000 && !client->inactivityWarning ) + { + client->inactivityWarning = qtrue; + trap_SendServerCommand( client - level.clients, "cp \"Ten seconds until inactivity drop!\n\"" ); + } + } + + return qtrue; +} + +/* +================== +ClientTimerActions + +Actions that happen once a second +================== +*/ +void ClientTimerActions( gentity_t *ent, int msec ) +{ + gclient_t *client; + usercmd_t *ucmd; + int aForward, aRight; + qboolean walking = qfalse, stopped = qfalse, + crouched = qfalse, jumping = qfalse, + strafing = qfalse; + + ucmd = &ent->client->pers.cmd; + + aForward = abs( ucmd->forwardmove ); + aRight = abs( ucmd->rightmove ); + + client = ent->client; + client->time100 += msec; + client->time1000 += msec; + client->time10000 += msec; + + if( aForward == 0 && aRight == 0 ) + stopped = qtrue; + else if( aForward <= 64 && aRight <= 64 ) + walking = qtrue; + + if( aRight > 0 ) + strafing = qtrue; + + if( ucmd->upmove > 0 ) + jumping = qtrue; + else if( ent->client->ps.pm_flags & PMF_DUCKED ) + crouched = qtrue; + + while ( client->time100 >= 100 ) + { + client->time100 -= 100; + + //if not trying to run then not trying to sprint + if( walking || stopped ) + client->ps.stats[ STAT_STATE ] &= ~SS_SPEEDBOOST; + + if( BG_InventoryContainsUpgrade( UP_JETPACK, client->ps.stats ) && BG_UpgradeIsActive( UP_JETPACK, client->ps.stats ) ) + client->ps.stats[ STAT_STATE ] &= ~SS_SPEEDBOOST; + + if( ( client->ps.stats[ STAT_STATE ] & SS_SPEEDBOOST ) && !crouched ) + { + //subtract stamina + if( BG_InventoryContainsUpgrade( UP_LIGHTARMOUR, client->ps.stats ) ) + client->ps.stats[ STAT_STAMINA ] -= STAMINA_LARMOUR_TAKE; + else + client->ps.stats[ STAT_STAMINA ] -= STAMINA_SPRINT_TAKE; + + if( client->ps.stats[ STAT_STAMINA ] < -MAX_STAMINA ) + client->ps.stats[ STAT_STAMINA ] = -MAX_STAMINA; + } + + if( walking || crouched ) + { + //restore stamina + client->ps.stats[ STAT_STAMINA ] += STAMINA_WALK_RESTORE; + + if( client->ps.stats[ STAT_STAMINA ] > MAX_STAMINA ) + client->ps.stats[ STAT_STAMINA ] = MAX_STAMINA; + } + else if( stopped ) + { + //restore stamina faster + client->ps.stats[ STAT_STAMINA ] += STAMINA_STOP_RESTORE; + + if( client->ps.stats[ STAT_STAMINA ] > MAX_STAMINA ) + client->ps.stats[ STAT_STAMINA ] = MAX_STAMINA; + } + + //client is charging up for a pounce + if( client->ps.weapon == WP_ALEVEL3 || client->ps.weapon == WP_ALEVEL3_UPG ) + { + int pounceSpeed = 0; + + if( client->ps.weapon == WP_ALEVEL3 ) + pounceSpeed = LEVEL3_POUNCE_SPEED; + else if( client->ps.weapon == WP_ALEVEL3_UPG ) + pounceSpeed = LEVEL3_POUNCE_UPG_SPEED; + + if( client->ps.stats[ STAT_MISC ] < pounceSpeed && ucmd->buttons & BUTTON_ATTACK2 ) + client->ps.stats[ STAT_MISC ] += ( 100.0f / (float)LEVEL3_POUNCE_CHARGE_TIME ) * pounceSpeed; + + if( !( ucmd->buttons & BUTTON_ATTACK2 ) ) + { + if( client->pmext.pouncePayload > 0 ) + client->allowedToPounce = qtrue; + } + + if( client->ps.stats[ STAT_MISC ] > pounceSpeed ) + client->ps.stats[ STAT_MISC ] = pounceSpeed; + } + + //client is charging up for a... charge + if( client->ps.weapon == WP_ALEVEL4 ) + { + if( client->ps.stats[ STAT_MISC ] < LEVEL4_CHARGE_TIME && ucmd->buttons & BUTTON_ATTACK2 && + !client->charging ) + { + client->charging = qfalse; //should already be off, just making sure + client->ps.stats[ STAT_STATE ] &= ~SS_CHARGING; + + if( ucmd->forwardmove > 0 ) + { + //trigger charge sound...is quite annoying + //if( client->ps.stats[ STAT_MISC ] <= 0 ) + // G_AddEvent( ent, EV_LEV4_CHARGE_PREPARE, 0 ); + + client->ps.stats[ STAT_MISC ] += (int)( 100 * (float)LEVEL4_CHARGE_CHARGE_RATIO ); + + if( client->ps.stats[ STAT_MISC ] > LEVEL4_CHARGE_TIME ) + client->ps.stats[ STAT_MISC ] = LEVEL4_CHARGE_TIME; + } + else + client->ps.stats[ STAT_MISC ] = 0; + } + + if( !( ucmd->buttons & BUTTON_ATTACK2 ) || client->charging || + client->ps.stats[ STAT_MISC ] == LEVEL4_CHARGE_TIME ) + { + if( client->ps.stats[ STAT_MISC ] > LEVEL4_MIN_CHARGE_TIME ) + { + client->ps.stats[ STAT_MISC ] -= 100; + + if( client->charging == qfalse ) + G_AddEvent( ent, EV_LEV4_CHARGE_START, 0 ); + + client->charging = qtrue; + client->ps.stats[ STAT_STATE ] |= SS_CHARGING; + + //if the charger has stopped moving take a chunk of charge away + if( VectorLength( client->ps.velocity ) < 64.0f || aRight ) + client->ps.stats[ STAT_MISC ] = client->ps.stats[ STAT_MISC ] / 2; + + //can't charge backwards + if( ucmd->forwardmove < 0 ) + client->ps.stats[ STAT_MISC ] = 0; + } + else + client->ps.stats[ STAT_MISC ] = 0; + + + if( client->ps.stats[ STAT_MISC ] <= 0 ) + { + client->ps.stats[ STAT_MISC ] = 0; + client->charging = qfalse; + client->ps.stats[ STAT_STATE ] &= ~SS_CHARGING; + } + } + } + + //client is charging up an lcannon + if( client->ps.weapon == WP_LUCIFER_CANNON ) + { + int ammo; + + BG_UnpackAmmoArray( WP_LUCIFER_CANNON, client->ps.ammo, client->ps.powerups, &ammo, NULL ); + + if( client->ps.stats[ STAT_MISC ] < LCANNON_TOTAL_CHARGE && ucmd->buttons & BUTTON_ATTACK ) + client->ps.stats[ STAT_MISC ] += ( 100.0f / LCANNON_CHARGE_TIME ) * LCANNON_TOTAL_CHARGE; + + if( client->ps.stats[ STAT_MISC ] > LCANNON_TOTAL_CHARGE ) + client->ps.stats[ STAT_MISC ] = LCANNON_TOTAL_CHARGE; + + if( client->ps.stats[ STAT_MISC ] > ( ammo * LCANNON_TOTAL_CHARGE ) / 10 ) + client->ps.stats[ STAT_MISC ] = ammo * LCANNON_TOTAL_CHARGE / 10; + } + + switch( client->ps.weapon ) + { + case WP_ABUILD: + case WP_ABUILD2: + case WP_HBUILD: + case WP_HBUILD2: + //set validity bit on buildable + if( ( client->ps.stats[ STAT_BUILDABLE ] & ~SB_VALID_TOGGLEBIT ) > BA_NONE ) + { + int dist = BG_FindBuildDistForClass( ent->client->ps.stats[ STAT_PCLASS ] ); + vec3_t dummy; + + if( G_CanBuild( ent, client->ps.stats[ STAT_BUILDABLE ] & ~SB_VALID_TOGGLEBIT, + dist, dummy ) == IBE_NONE ) + client->ps.stats[ STAT_BUILDABLE ] |= SB_VALID_TOGGLEBIT; + else + client->ps.stats[ STAT_BUILDABLE ] &= ~SB_VALID_TOGGLEBIT; + } + + case WP_BLASTER: + //update build timer + if( client->ps.stats[ STAT_MISC ] > 0 ) + client->ps.stats[ STAT_MISC ] -= 100; + + if( client->ps.stats[ STAT_MISC ] < 0 ) + client->ps.stats[ STAT_MISC ] = 0; + break; + + default: + break; + } + + if( client->ps.stats[ STAT_STATE ] & SS_MEDKIT_ACTIVE ) + { + int remainingStartupTime = MEDKIT_STARTUP_TIME - ( level.time - client->lastMedKitTime ); + + if( remainingStartupTime < 0 ) + { + if( ent->health < ent->client->ps.stats[ STAT_MAX_HEALTH ] && + ent->client->medKitHealthToRestore && + ent->client->ps.pm_type != PM_DEAD ) + { + ent->client->medKitHealthToRestore--; + ent->health++; + } + else + ent->client->ps.stats[ STAT_STATE ] &= ~SS_MEDKIT_ACTIVE; + } + else + { + if( ent->health < ent->client->ps.stats[ STAT_MAX_HEALTH ] && + ent->client->medKitHealthToRestore && + ent->client->ps.pm_type != PM_DEAD ) + { + //partial increase + if( level.time > client->medKitIncrementTime ) + { + ent->client->medKitHealthToRestore--; + ent->health++; + + client->medKitIncrementTime = level.time + + ( remainingStartupTime / MEDKIT_STARTUP_SPEED ); + } + } + else + ent->client->ps.stats[ STAT_STATE ] &= ~SS_MEDKIT_ACTIVE; + } + } + } + + while( client->time1000 >= 1000 ) + { + client->time1000 -= 1000; + + //client is poison clouded + if( client->ps.stats[ STAT_STATE ] & SS_POISONCLOUDED ) + G_Damage( ent, client->lastPoisonCloudedClient, client->lastPoisonCloudedClient, NULL, NULL, + LEVEL1_PCLOUD_DMG, 0, MOD_LEVEL1_PCLOUD ); + + //client is poisoned + if( client->ps.stats[ STAT_STATE ] & SS_POISONED ) + { + int damage = ALIEN_POISON_DMG; + + if( BG_InventoryContainsUpgrade( UP_BATTLESUIT, client->ps.stats ) ) + damage -= BSUIT_POISON_PROTECTION; + if( BG_InventoryContainsUpgrade( UP_HELMET, client->ps.stats ) ) + damage -= HELMET_POISON_PROTECTION; + if( BG_InventoryContainsUpgrade( UP_LIGHTARMOUR, client->ps.stats ) ) + damage -= LIGHTARMOUR_POISON_PROTECTION; + + G_Damage( ent, client->lastPoisonClient, client->lastPoisonClient, NULL, + 0, damage, 0, MOD_POISON ); + } + + //replenish alien health + if( client->ps.stats[ STAT_PTEAM ] == PTE_ALIENS && + level.surrenderTeam != PTE_ALIENS ) + { + int entityList[ MAX_GENTITIES ]; + vec3_t range = { LEVEL4_REGEN_RANGE, LEVEL4_REGEN_RANGE, LEVEL4_REGEN_RANGE }; + vec3_t mins, maxs; + int i, num; + gentity_t *boostEntity; + float modifier = 1.0f; + + VectorAdd( client->ps.origin, range, maxs ); + VectorSubtract( client->ps.origin, range, mins ); + + num = trap_EntitiesInBox( mins, maxs, entityList, MAX_GENTITIES ); + for( i = 0; i < num; i++ ) + { + boostEntity = &g_entities[ entityList[ i ] ]; + + if( boostEntity->client && boostEntity->client->ps.stats[ STAT_PTEAM ] == PTE_ALIENS && + boostEntity->client->ps.stats[ STAT_PCLASS ] == PCL_ALIEN_LEVEL4 ) + { + modifier = LEVEL4_REGEN_MOD; + break; + } + else if( boostEntity->s.eType == ET_BUILDABLE && + boostEntity->s.modelindex == BA_A_BOOSTER && + boostEntity->spawned && boostEntity->health > 0 ) + { + modifier = BOOSTER_REGEN_MOD; + break; + } + } + + if( ent->health > 0 && ent->health < client->ps.stats[ STAT_MAX_HEALTH ] && + !level.paused && + ( ent->lastDamageTime + ALIEN_REGEN_DAMAGE_TIME ) < level.time ) + { + ent->health += BG_FindRegenRateForClass( client->ps.stats[ STAT_PCLASS ] ) * modifier; + + // if completely healed, cancel retribution + if( ent->health >= client->ps.stats[ STAT_MAX_HEALTH ] ) + { + for( i = 0; i < MAX_CLIENTS; i++ ) + ent->client->tkcredits[ i ] = 0; + } + } + + if( ent->health > client->ps.stats[ STAT_MAX_HEALTH ] ) + ent->health = client->ps.stats[ STAT_MAX_HEALTH ]; + } + + + if( ent->client->ps.stats[ STAT_HEALTH ] > 0 && ent->client->ps.stats[ STAT_PTEAM ] == PTE_ALIENS ) + { + ent->client->pers.statscounters.timealive++; + level.alienStatsCounters.timealive++; + if( G_BuildableRange( ent->client->ps.origin, 900, BA_A_OVERMIND ) ) + { + ent->client->pers.statscounters.timeinbase++; + level.alienStatsCounters.timeinbase++; + } + if( BG_ClassHasAbility( ent->client->ps.stats[ STAT_PCLASS ], SCA_WALLCLIMBER ) ) + { + ent->client->pers.statscounters.dretchbasytime++; + level.alienStatsCounters.dretchbasytime++; + if( ent->client->ps.stats[ STAT_STATE ] & SS_WALLCLIMBING || ent->client->ps.stats[ STAT_STATE ] & SS_WALLCLIMBINGCEILING) + { + ent->client->pers.statscounters.jetpackusewallwalkusetime++; + level.alienStatsCounters.jetpackusewallwalkusetime++; + } + } + } + else if( ent->client->ps.stats[ STAT_HEALTH ] > 0 && ent->client->ps.stats[ STAT_PTEAM ] == PTE_HUMANS ) + { + ent->client->pers.statscounters.timealive++; + level.humanStatsCounters.timealive++; + if( G_BuildableRange( ent->client->ps.origin, 900, BA_H_REACTOR ) ) + { + ent->client->pers.statscounters.timeinbase++; + level.humanStatsCounters.timeinbase++; + } + if( BG_InventoryContainsUpgrade( UP_JETPACK, client->ps.stats ) ) + { + if( client->ps.pm_type == PM_JETPACK ) + { + ent->client->pers.statscounters.jetpackusewallwalkusetime++; + level.humanStatsCounters.jetpackusewallwalkusetime++; + } + } + } + + // turn off life support when a team admits defeat + if( client->ps.stats[ STAT_PTEAM ] == PTE_ALIENS && + level.surrenderTeam == PTE_ALIENS ) + { + G_Damage( ent, NULL, NULL, NULL, NULL, + BG_FindRegenRateForClass( client->ps.stats[ STAT_PCLASS ] ), + DAMAGE_NO_ARMOR, MOD_SUICIDE ); + } + else if( client->ps.stats[ STAT_PTEAM ] == PTE_HUMANS && + level.surrenderTeam == PTE_HUMANS ) + { + G_Damage( ent, NULL, NULL, NULL, NULL, 5, DAMAGE_NO_ARMOR, MOD_SUICIDE ); + } + + //my new jetpack code + if( mod_jetpackFuel.value >= 10.0f ) { + //if we have jetpack and its on + if( BG_InventoryContainsUpgrade( UP_JETPACK, client->ps.stats ) && BG_UpgradeIsActive( UP_JETPACK, client->ps.stats ) ) { + //check if fuels 0 if so deactivate it if not give a 10 second fuel low warning and take JETPACK_USE_RATE from fuel + if( client->jetpackfuel <= 0.0f ) { + BG_DeactivateUpgrade( UP_JETPACK, client->ps.stats ); + } else if( client->jetpackfuel < 10.0f && client->jetpackfuel > 0.0f) { + client->jetpackfuel = client->jetpackfuel - mod_jetpackConsume.value; + trap_SendServerCommand( client - level.clients, "cp \"^3Fuel ^1Low!!!!!^7\nLand now.\"" ); + } else { + client->jetpackfuel = client->jetpackfuel - mod_jetpackConsume.value; + } + + //if jetpack isnt active regenerate fuel and give a message when its full + } else if( BG_InventoryContainsUpgrade( UP_JETPACK, client->ps.stats ) && !BG_UpgradeIsActive( UP_JETPACK, client->ps.stats ) ) { + if( client->jetpackfuel > ( mod_jetpackFuel.value - 10.0f ) && client->jetpackfuel <= mod_jetpackFuel.value ) { + client->jetpackfuel = client->jetpackfuel + mod_jetpackRegen.value; + trap_SendServerCommand( client - level.clients, "cp \"^3Fuel Status: ^2Full!^7\n\"" ); + } else if( client->jetpackfuel < mod_jetpackFuel.value ) { + //regenerate some fuel + client->jetpackfuel = client->jetpackfuel + mod_jetpackRegen.value; + } + } + } + } + + while( client->time10000 >= 10000 ) + { + client->time10000 -= 10000; + + if( client->ps.weapon == WP_ALEVEL3_UPG ) + { + int ammo, maxAmmo; + + BG_FindAmmoForWeapon( WP_ALEVEL3_UPG, &maxAmmo, NULL ); + BG_UnpackAmmoArray( WP_ALEVEL3_UPG, client->ps.ammo, client->ps.powerups, &ammo, NULL ); + + if( ammo < maxAmmo ) + { + ammo++; + BG_PackAmmoArray( WP_ALEVEL3_UPG, client->ps.ammo, client->ps.powerups, ammo, 0 ); + } + } + } +} + +/* +==================== +ClientIntermissionThink +==================== +*/ +void ClientIntermissionThink( gclient_t *client ) +{ + client->ps.eFlags &= ~EF_TALK; + client->ps.eFlags &= ~EF_FIRING; + client->ps.eFlags &= ~EF_FIRING2; + + // the level will exit when everyone wants to or after timeouts + + // swap and latch button actions + client->oldbuttons = client->buttons; + client->buttons = client->pers.cmd.buttons; + if( client->buttons & ( BUTTON_ATTACK | BUTTON_USE_HOLDABLE ) & ( client->oldbuttons ^ client->buttons ) ) + client->readyToExit = 1; +} + + +/* +================ +ClientEvents + +Events will be passed on to the clients for presentation, +but any server game effects are handled here +================ +*/ +void ClientEvents( gentity_t *ent, int oldEventSequence ) +{ + int i; + int event; + gclient_t *client; + int damage; + vec3_t dir; + vec3_t point, mins; + float fallDistance; + pClass_t class; + + client = ent->client; + class = client->ps.stats[ STAT_PCLASS ]; + + if( oldEventSequence < client->ps.eventSequence - MAX_PS_EVENTS ) + oldEventSequence = client->ps.eventSequence - MAX_PS_EVENTS; + + for( i = oldEventSequence; i < client->ps.eventSequence; i++ ) + { + event = client->ps.events[ i & ( MAX_PS_EVENTS - 1 ) ]; + + switch( event ) + { + case EV_FALL_MEDIUM: + case EV_FALL_FAR: + if( ent->s.eType != ET_PLAYER ) + break; // not in the player model + + fallDistance = ( (float)client->ps.stats[ STAT_FALLDIST ] - MIN_FALL_DISTANCE ) / + ( MAX_FALL_DISTANCE - MIN_FALL_DISTANCE ); + + if( fallDistance < 0.0f ) + fallDistance = 0.0f; + else if( fallDistance > 1.0f ) + fallDistance = 1.0f; + + damage = (int)( (float)BG_FindHealthForClass( class ) * + BG_FindFallDamageForClass( class ) * fallDistance ); + + VectorSet( dir, 0, 0, 1 ); + BG_FindBBoxForClass( class, mins, NULL, NULL, NULL, NULL ); + mins[ 0 ] = mins[ 1 ] = 0.0f; + VectorAdd( client->ps.origin, mins, point ); + + ent->pain_debounce_time = level.time + 200; // no normal pain sound + G_Damage( ent, NULL, NULL, dir, point, damage, DAMAGE_NO_LOCDAMAGE, MOD_FALLING ); + break; + + case EV_FIRE_WEAPON: + FireWeapon( ent ); + break; + + case EV_FIRE_WEAPON2: + FireWeapon2( ent ); + break; + + case EV_FIRE_WEAPON3: + FireWeapon3( ent ); + break; + + case EV_NOAMMO: + break; + + default: + break; + } + } +} + + +/* +============== +SendPendingPredictableEvents +============== +*/ +void SendPendingPredictableEvents( playerState_t *ps ) +{ + gentity_t *t; + int event, seq; + int extEvent, number; + + // if there are still events pending + if( ps->entityEventSequence < ps->eventSequence ) + { + // create a temporary entity for this event which is sent to everyone + // except the client who generated the event + seq = ps->entityEventSequence & ( MAX_PS_EVENTS - 1 ); + event = ps->events[ seq ] | ( ( ps->entityEventSequence & 3 ) << 8 ); + // set external event to zero before calling BG_PlayerStateToEntityState + extEvent = ps->externalEvent; + ps->externalEvent = 0; + // create temporary entity for event + t = G_TempEntity( ps->origin, event ); + number = t->s.number; + BG_PlayerStateToEntityState( ps, &t->s, qtrue ); + t->s.number = number; + t->s.eType = ET_EVENTS + event; + t->s.eFlags |= EF_PLAYER_EVENT; + t->s.otherEntityNum = ps->clientNum; + // send to everyone except the client who generated the event + t->r.svFlags |= SVF_NOTSINGLECLIENT; + t->r.singleClient = ps->clientNum; + // set back external event + ps->externalEvent = extEvent; + } +} + +/* +============== + G_UnlaggedStore + + Called on every server frame. Stores position data for the client at that + into client->unlaggedHist[] and the time into level.unlaggedTimes[]. + This data is used by G_UnlaggedCalc() +============== +*/ +void G_UnlaggedStore( void ) +{ + int i = 0; + gentity_t *ent; + unlagged_t *save; + + if( !g_unlagged.integer ) + return; + level.unlaggedIndex++; + if( level.unlaggedIndex >= MAX_UNLAGGED_MARKERS ) + level.unlaggedIndex = 0; + + level.unlaggedTimes[ level.unlaggedIndex ] = level.time; + + for( i = 0; i < level.maxclients; i++ ) + { + ent = &g_entities[ i ]; + save = &ent->client->unlaggedHist[ level.unlaggedIndex ]; + save->used = qfalse; + if( !ent->r.linked || !( ent->r.contents & CONTENTS_BODY ) ) + continue; + if( ent->client->pers.connected != CON_CONNECTED ) + continue; + VectorCopy( ent->r.mins, save->mins ); + VectorCopy( ent->r.maxs, save->maxs ); + VectorCopy( ent->s.pos.trBase, save->origin ); + save->used = qtrue; + } +} + +/* +============== + G_UnlaggedClear + + Mark all unlaggedHist[] markers for this client invalid. Useful for + preventing teleporting and death. +============== +*/ +void G_UnlaggedClear( gentity_t *ent ) +{ + int i; + + for( i = 0; i < MAX_UNLAGGED_MARKERS; i++ ) + ent->client->unlaggedHist[ i ].used = qfalse; +} + +/* +============== + G_UnlaggedCalc + + Loops through all active clients and calculates their predicted position + for time then stores it in client->unlaggedCalc +============== +*/ +void G_UnlaggedCalc( int time, gentity_t *rewindEnt ) +{ + int i = 0; + gentity_t *ent; + int startIndex = level.unlaggedIndex; + int stopIndex = -1; + int frameMsec = 0; + float lerp = 0.5f; + + if( !g_unlagged.integer ) + return; + + // clear any calculated values from a previous run + for( i = 0; i < level.maxclients; i++ ) + { + ent = &g_entities[ i ]; + ent->client->unlaggedCalc.used = qfalse; + } + + for( i = 0; i < MAX_UNLAGGED_MARKERS; i++ ) + { + if( level.unlaggedTimes[ startIndex ] <= time ) + break; + stopIndex = startIndex; + if( --startIndex < 0 ) + startIndex = MAX_UNLAGGED_MARKERS - 1; + } + if( i == MAX_UNLAGGED_MARKERS ) + { + // if we searched all markers and the oldest one still isn't old enough + // just use the oldest marker with no lerping + lerp = 0.0f; + } + + // client is on the current frame, no need for unlagged + if( stopIndex == -1 ) + return; + + // lerp between two markers + frameMsec = level.unlaggedTimes[ stopIndex ] - + level.unlaggedTimes[ startIndex ]; + if( frameMsec > 0 ) + { + lerp = ( float )( time - level.unlaggedTimes[ startIndex ] ) + / ( float )frameMsec; + } + + for( i = 0; i < level.maxclients; i++ ) + { + ent = &g_entities[ i ]; + if( ent == rewindEnt ) + continue; + if( !ent->r.linked || !( ent->r.contents & CONTENTS_BODY ) ) + continue; + if( ent->client->pers.connected != CON_CONNECTED ) + continue; + if( !ent->client->unlaggedHist[ startIndex ].used ) + continue; + if( !ent->client->unlaggedHist[ stopIndex ].used ) + continue; + + // between two unlagged markers + VectorLerp( lerp, ent->client->unlaggedHist[ startIndex ].mins, + ent->client->unlaggedHist[ stopIndex ].mins, + ent->client->unlaggedCalc.mins ); + VectorLerp( lerp, ent->client->unlaggedHist[ startIndex ].maxs, + ent->client->unlaggedHist[ stopIndex ].maxs, + ent->client->unlaggedCalc.maxs ); + VectorLerp( lerp, ent->client->unlaggedHist[ startIndex ].origin, + ent->client->unlaggedHist[ stopIndex ].origin, + ent->client->unlaggedCalc.origin ); + + ent->client->unlaggedCalc.used = qtrue; + } +} + +/* +============== + G_UnlaggedOff + + Reverses the changes made to all active clients by G_UnlaggedOn() +============== +*/ +void G_UnlaggedOff( void ) +{ + int i = 0; + gentity_t *ent; + + if( !g_unlagged.integer ) + return; + + for( i = 0; i < level.maxclients; i++ ) + { + ent = &g_entities[ i ]; + if( !ent->client->unlaggedBackup.used ) + continue; + VectorCopy( ent->client->unlaggedBackup.mins, ent->r.mins ); + VectorCopy( ent->client->unlaggedBackup.maxs, ent->r.maxs ); + VectorCopy( ent->client->unlaggedBackup.origin, ent->r.currentOrigin ); + ent->client->unlaggedBackup.used = qfalse; + trap_LinkEntity( ent ); + } +} + +/* +============== + G_UnlaggedOn + + Called after G_UnlaggedCalc() to apply the calculated values to all active + clients. Once finished tracing, G_UnlaggedOff() must be called to restore + the clients' position data + + As an optimization, all clients that have an unlagged position that is + not touchable at "range" from "muzzle" will be ignored. This is required + to prevent a huge amount of trap_LinkEntity() calls per user cmd. +============== +*/ + +void G_UnlaggedOn( gentity_t *attacker, vec3_t muzzle, float range ) +{ + int i = 0; + gentity_t *ent; + unlagged_t *calc; + + if( !g_unlagged.integer ) + return; + + if( !attacker->client->pers.useUnlagged ) + return; + + for( i = 0; i < level.maxclients; i++ ) + { + ent = &g_entities[ i ]; + calc = &ent->client->unlaggedCalc; + + if( !calc->used ) + continue; + if( ent->client->unlaggedBackup.used ) + continue; + if( !ent->r.linked || !( ent->r.contents & CONTENTS_BODY ) ) + continue; + if( VectorCompare( ent->r.currentOrigin, calc->origin ) ) + continue; + if( muzzle ) + { + float r1 = Distance( calc->origin, calc->maxs ); + float r2 = Distance( calc->origin, calc->mins ); + float maxRadius = ( r1 > r2 ) ? r1 : r2; + + if( Distance( muzzle, calc->origin ) > range + maxRadius ) + continue; + } + + // create a backup of the real positions + VectorCopy( ent->r.mins, ent->client->unlaggedBackup.mins ); + VectorCopy( ent->r.maxs, ent->client->unlaggedBackup.maxs ); + VectorCopy( ent->r.currentOrigin, ent->client->unlaggedBackup.origin ); + ent->client->unlaggedBackup.used = qtrue; + + // move the client to the calculated unlagged position + VectorCopy( calc->mins, ent->r.mins ); + VectorCopy( calc->maxs, ent->r.maxs ); + VectorCopy( calc->origin, ent->r.currentOrigin ); + trap_LinkEntity( ent ); + } +} +/* +============== + G_UnlaggedDetectCollisions + + cgame prediction will predict a client's own position all the way up to + the current time, but only updates other player's positions up to the + postition sent in the most recent snapshot. + + This allows player X to essentially "move through" the position of player Y + when player X's cmd is processed with Pmove() on the server. This is because + player Y was clipping player X's Pmove() on his client, but when the same + cmd is processed with Pmove on the server it is not clipped. + + Long story short (too late): don't use unlagged positions for players who + were blocking this player X's client-side Pmove(). This makes the assumption + that if player X's movement was blocked in the client he's going to still + be up against player Y when the Pmove() is run on the server with the + same cmd. + + NOTE: this must be called after Pmove() and G_UnlaggedCalc() +============== +*/ +static void G_UnlaggedDetectCollisions( gentity_t *ent ) +{ + unlagged_t *calc; + trace_t tr; + float r1, r2; + float range; + + if( !g_unlagged.integer ) + return; + if( !ent->client->pers.useUnlagged ) + return; + + calc = &ent->client->unlaggedCalc; + + // if the client isn't moving, this is not necessary + if( VectorCompare( ent->client->oldOrigin, ent->client->ps.origin ) ) + return; + + range = Distance( ent->client->oldOrigin, ent->client->ps.origin ); + + // increase the range by the player's largest possible radius since it's + // the players bounding box that collides, not their origin + r1 = Distance( calc->origin, calc->mins ); + r2 = Distance( calc->origin, calc->maxs ); + range += ( r1 > r2 ) ? r1 : r2; + + G_UnlaggedOn( ent, ent->client->oldOrigin, range ); + + trap_Trace(&tr, ent->client->oldOrigin, ent->r.mins, ent->r.maxs, + ent->client->ps.origin, ent->s.number, MASK_PLAYERSOLID ); + if( tr.entityNum >= 0 && tr.entityNum < MAX_CLIENTS ) + g_entities[ tr.entityNum ].client->unlaggedCalc.used = qfalse; + + G_UnlaggedOff( ); +} + +/* +============== +ClientThink + +This will be called once for each client frame, which will +usually be a couple times for each server frame on fast clients. + +If "g_synchronousClients 1" is set, this will be called exactly +once for each server frame, which makes for smooth demo recording. +============== +*/ +void ClientThink_real( gentity_t *ent ) +{ + gclient_t *client; + pmove_t pm; + int oldEventSequence; + int msec; + usercmd_t *ucmd; + int real_pm_type; + + client = ent->client; + + // don't think if the client is not yet connected (and thus not yet spawned in) + if( client->pers.connected != CON_CONNECTED ) + return; + + // mark the time, so the connection sprite can be removed + ucmd = &ent->client->pers.cmd; + + if( client->pers.paused ) + ucmd->forwardmove = ucmd->rightmove = ucmd->upmove = ucmd->buttons = 0; + + // sanity check the command time to prevent speedup cheating + if( ucmd->serverTime > level.time + 200 ) + { + ucmd->serverTime = level.time + 200; +// G_Printf("serverTime <<<<<\n" ); + } + + if( ucmd->serverTime < level.time - 1000 ) + { + ucmd->serverTime = level.time - 1000; +// G_Printf("serverTime >>>>>\n" ); + } + + // ucmd->serverTime is a client predicted value, but it works for making a + // replacement for client->ps.ping when in SPECTATOR_FOLLOW + client->pers.ping = level.time - ucmd->serverTime; + + // account for the one frame of delay on client side + client->pers.ping -= level.time - level.previousTime; + + // account for the time that's elapsed since the last ClientEndFrame() + client->pers.ping += trap_Milliseconds( ) - level.frameMsec; + + if( client->pers.ping < 0 ) + client->pers.ping = 0; + + msec = ucmd->serverTime - client->ps.commandTime; + // following others may result in bad times, but we still want + // to check for follow toggles + if( msec < 1 && client->sess.spectatorState != SPECTATOR_FOLLOW ) + return; + + if( msec > 200 ) + msec = 200; + + client->unlaggedTime = ucmd->serverTime; + + if( pmove_msec.integer < 8 ) + trap_Cvar_Set( "pmove_msec", "8" ); + else if( pmove_msec.integer > 33 ) + trap_Cvar_Set( "pmove_msec", "33" ); + + if( pmove_fixed.integer || client->pers.pmoveFixed ) + { + ucmd->serverTime = ( ( ucmd->serverTime + pmove_msec.integer - 1 ) / pmove_msec.integer ) * pmove_msec.integer; + //if (ucmd->serverTime - client->ps.commandTime <= 0) + // return; + } + + // + // check for exiting intermission + // + if( level.intermissiontime ) + { + if( level.mapRotationVoteTime ) + { + SpectatorThink( ent, ucmd ); + return; + } + + ClientIntermissionThink( client ); + return; + } + + if( client->pers.teamSelection != PTE_NONE && client->pers.joinedATeam ) + G_UpdatePTRConnection( client ); + + // spectators don't do much + if( client->sess.sessionTeam == TEAM_SPECTATOR ) + { + if( client->sess.spectatorState == SPECTATOR_SCOREBOARD ) + return; + + SpectatorThink( ent, ucmd ); + return; + } + + // check for inactivity timer, but never drop the local client of a non-dedicated server + if( !ClientInactivityTimer( client ) ) + return; + + // calculate where ent is currently seeing all the other active clients + G_UnlaggedCalc( ent->client->unlaggedTime, ent ); + + if( client->noclip ) + client->ps.pm_type = PM_NOCLIP; + else if( client->ps.stats[ STAT_HEALTH ] <= 0 ) + client->ps.pm_type = PM_DEAD; + else if( client->ps.stats[ STAT_STATE ] & SS_INFESTING || + client->ps.stats[ STAT_STATE ] & SS_HOVELING ) + client->ps.pm_type = PM_FREEZE; + else if( client->ps.stats[ STAT_STATE ] & SS_BLOBLOCKED || + client->ps.stats[ STAT_STATE ] & SS_GRABBED ) + client->ps.pm_type = PM_GRABBED; + else if( BG_InventoryContainsUpgrade( UP_JETPACK, client->ps.stats ) && BG_UpgradeIsActive( UP_JETPACK, client->ps.stats ) ) + client->ps.pm_type = PM_JETPACK; + else + client->ps.pm_type = PM_NORMAL; + + // paused + real_pm_type = client->ps.pm_type; + if ( level.paused ) client->ps.pm_type = PM_SPECTATOR; + + if( client->ps.stats[ STAT_STATE ] & SS_GRABBED && + client->grabExpiryTime < level.time ) + client->ps.stats[ STAT_STATE ] &= ~SS_GRABBED; + + if( client->ps.stats[ STAT_STATE ] & SS_BLOBLOCKED && + client->lastLockTime + LOCKBLOB_LOCKTIME < level.time ) + client->ps.stats[ STAT_STATE ] &= ~SS_BLOBLOCKED; + + if( client->ps.stats[ STAT_STATE ] & SS_SLOWLOCKED && + client->lastSlowTime + ABUILDER_BLOB_TIME < level.time ) + client->ps.stats[ STAT_STATE ] &= ~SS_SLOWLOCKED; + + client->ps.stats[ STAT_BOOSTTIME ] = level.time - client->lastBoostedTime; + + if( client->ps.stats[ STAT_STATE ] & SS_BOOSTED && + client->lastBoostedTime + BOOST_TIME < level.time ) + client->ps.stats[ STAT_STATE ] &= ~SS_BOOSTED; + + if( client->ps.stats[ STAT_STATE ] & SS_POISONCLOUDED && + client->lastPoisonCloudedTime + LEVEL1_PCLOUD_TIME < level.time ) + client->ps.stats[ STAT_STATE ] &= ~SS_POISONCLOUDED; + + if( client->ps.stats[ STAT_STATE ] & SS_POISONED && + client->lastPoisonTime + ALIEN_POISON_TIME < level.time ) + client->ps.stats[ STAT_STATE ] &= ~SS_POISONED; + + client->ps.gravity = g_gravity.value; + + if( BG_InventoryContainsUpgrade( UP_MEDKIT, client->ps.stats ) && + BG_UpgradeIsActive( UP_MEDKIT, client->ps.stats ) ) + { + //if currently using a medkit or have no need for a medkit now + if( client->ps.stats[ STAT_STATE ] & SS_MEDKIT_ACTIVE || + ( client->ps.stats[ STAT_HEALTH ] == client->ps.stats[ STAT_MAX_HEALTH ] && + !( client->ps.stats[ STAT_STATE ] & SS_POISONED ) ) ) + { + BG_DeactivateUpgrade( UP_MEDKIT, client->ps.stats ); + } + else if( client->ps.stats[ STAT_HEALTH ] > 0 && !level.paused ) + { + //remove anti toxin + BG_DeactivateUpgrade( UP_MEDKIT, client->ps.stats ); + BG_RemoveUpgradeFromInventory( UP_MEDKIT, client->ps.stats ); + + client->ps.stats[ STAT_STATE ] &= ~SS_POISONED; + client->poisonImmunityTime = level.time + MEDKIT_POISON_IMMUNITY_TIME; + + client->ps.stats[ STAT_STATE ] |= SS_MEDKIT_ACTIVE; + client->lastMedKitTime = level.time; + client->medKitHealthToRestore = + client->ps.stats[ STAT_MAX_HEALTH ] - client->ps.stats[ STAT_HEALTH ]; + client->medKitIncrementTime = level.time + + ( MEDKIT_STARTUP_TIME / MEDKIT_STARTUP_SPEED ); + + G_AddEvent( ent, EV_MEDKIT_USED, 0 ); + } + } + + if( BG_InventoryContainsUpgrade( UP_GRENADE, client->ps.stats ) && + BG_UpgradeIsActive( UP_GRENADE, client->ps.stats ) ) + { + int lastWeapon = ent->s.weapon; + + //remove grenade + BG_DeactivateUpgrade( UP_GRENADE, client->ps.stats ); + BG_RemoveUpgradeFromInventory( UP_GRENADE, client->ps.stats ); + + //M-M-M-M-MONSTER HACK + ent->s.weapon = WP_GRENADE; + FireWeapon( ent ); + ent->s.weapon = lastWeapon; + } + + // set speed + client->ps.speed = g_speed.value * BG_FindSpeedForClass( client->ps.stats[ STAT_PCLASS ] ); + + if( client->pers.paused ) + client->ps.speed = 0; + + if( client->lastCreepSlowTime + CREEP_TIMEOUT < level.time ) + client->ps.stats[ STAT_STATE ] &= ~SS_CREEPSLOWED; + + //randomly disable the jet pack if damaged + if( BG_InventoryContainsUpgrade( UP_JETPACK, client->ps.stats ) && + BG_UpgradeIsActive( UP_JETPACK, client->ps.stats ) ) + { + if( ent->lastDamageTime + JETPACK_DISABLE_TIME > level.time ) + { + if( random( ) > JETPACK_DISABLE_CHANCE ) + client->ps.pm_type = PM_NORMAL; + } + + //switch jetpack off if no reactor + if( !level.reactorPresent ) + BG_DeactivateUpgrade( UP_JETPACK, client->ps.stats ); + } + + // set up for pmove + oldEventSequence = client->ps.eventSequence; + + memset( &pm, 0, sizeof( pm ) ); + + if( !( ucmd->buttons & BUTTON_TALK ) && !( client->ps.pm_flags & PMF_RESPAWNED ) ) + { + switch( client->ps.weapon ) + { + case WP_ALEVEL0: + if( client->ps.weaponTime <= 0 ) + pm.autoWeaponHit[ client->ps.weapon ] = CheckVenomAttack( ent ); + break; + + case WP_ALEVEL1: + case WP_ALEVEL1_UPG: + CheckGrabAttack( ent ); + break; + + case WP_ALEVEL3: + case WP_ALEVEL3_UPG: + if( client->ps.weaponTime <= 0 ) + pm.autoWeaponHit[ client->ps.weapon ] = CheckPounceAttack( ent ); + break; + + default: + break; + } + } + + if( ent->flags & FL_FORCE_GESTURE ) + { + ent->flags &= ~FL_FORCE_GESTURE; + ent->client->pers.cmd.buttons |= BUTTON_GESTURE; + } + + pm.ps = &client->ps; + pm.pmext = &client->pmext; + pm.cmd = *ucmd; + + if( pm.ps->pm_type == PM_DEAD ) + pm.tracemask = MASK_PLAYERSOLID; // & ~CONTENTS_BODY; + + if( pm.ps->stats[ STAT_STATE ] & SS_INFESTING || + pm.ps->stats[ STAT_STATE ] & SS_HOVELING ) + pm.tracemask = MASK_PLAYERSOLID & ~CONTENTS_BODY; + else + pm.tracemask = MASK_PLAYERSOLID; + + pm.trace = trap_Trace; + pm.pointcontents = trap_PointContents; + pm.debugLevel = g_debugMove.integer; + pm.noFootsteps = 0; + + pm.pmove_fixed = pmove_fixed.integer | client->pers.pmoveFixed; + pm.pmove_msec = pmove_msec.integer; + + VectorCopy( client->ps.origin, client->oldOrigin ); + + // moved from after Pmove -- potentially the cause of + // future triggering bugs + if( !ent->client->noclip ) + G_TouchTriggers( ent ); + + Pmove( &pm ); + + G_UnlaggedDetectCollisions( ent ); + + // save results of pmove + if( ent->client->ps.eventSequence != oldEventSequence ) + ent->eventTime = level.time; + + if ( level.paused ) client->ps.pm_type = real_pm_type; + + if( g_smoothClients.integer ) + BG_PlayerStateToEntityStateExtraPolate( &ent->client->ps, &ent->s, ent->client->ps.commandTime, qtrue ); + else + BG_PlayerStateToEntityState( &ent->client->ps, &ent->s, qtrue ); + + SendPendingPredictableEvents( &ent->client->ps ); + + if( !( ent->client->ps.eFlags & EF_FIRING ) ) + client->fireHeld = qfalse; // for grapple + if( !( ent->client->ps.eFlags & EF_FIRING2 ) ) + client->fire2Held = qfalse; + + // use the snapped origin for linking so it matches client predicted versions + VectorCopy( ent->s.pos.trBase, ent->r.currentOrigin ); + + VectorCopy( pm.mins, ent->r.mins ); + VectorCopy( pm.maxs, ent->r.maxs ); + + ent->waterlevel = pm.waterlevel; + ent->watertype = pm.watertype; + + // touch other objects + ClientImpacts( ent, &pm ); + + // execute client events + ClientEvents( ent, oldEventSequence ); + + // link entity now, after any personal teleporters have been used + trap_LinkEntity( ent ); + + // NOTE: now copy the exact origin over otherwise clients can be snapped into solid + VectorCopy( ent->client->ps.origin, ent->r.currentOrigin ); + VectorCopy( ent->client->ps.origin, ent->s.origin ); + + // save results of triggers and client events + if( ent->client->ps.eventSequence != oldEventSequence ) + ent->eventTime = level.time; + + // Don't think anymore if dead + if( client->ps.stats[ STAT_HEALTH ] <= 0 ) + return; + + // swap and latch button actions + client->oldbuttons = client->buttons; + client->buttons = ucmd->buttons; + client->latched_buttons |= client->buttons & ~client->oldbuttons; + + if( ( client->buttons & BUTTON_GETFLAG ) && !( client->oldbuttons & BUTTON_GETFLAG ) && + client->ps.stats[ STAT_HEALTH ] > 0 ) + { + trace_t trace; + vec3_t view, point; + gentity_t *traceEnt; + + if( client->ps.stats[ STAT_STATE ] & SS_HOVELING ) + { + gentity_t *hovel = client->hovel; + + //only let the player out if there is room + if( !AHovel_Blocked( hovel, ent, qtrue ) ) + { + //prevent lerping + client->ps.eFlags ^= EF_TELEPORT_BIT; + client->ps.eFlags &= ~EF_NODRAW; + G_UnlaggedClear( ent ); + + //client leaves hovel + client->ps.stats[ STAT_STATE ] &= ~SS_HOVELING; + + //hovel is empty + G_SetBuildableAnim( hovel, BANIM_ATTACK2, qfalse ); + hovel->active = qfalse; + } + else + { + //exit is blocked + G_TriggerMenu( ent->client->ps.clientNum, MN_A_HOVEL_BLOCKED ); + } + } + else + { +#define USE_OBJECT_RANGE 64 + + int entityList[ MAX_GENTITIES ]; + vec3_t range = { USE_OBJECT_RANGE, USE_OBJECT_RANGE, USE_OBJECT_RANGE }; + vec3_t mins, maxs; + int i, num; + + //TA: look for object infront of player + AngleVectors( client->ps.viewangles, view, NULL, NULL ); + VectorMA( client->ps.origin, USE_OBJECT_RANGE, view, point ); + trap_Trace( &trace, client->ps.origin, NULL, NULL, point, ent->s.number, MASK_SHOT ); + + traceEnt = &g_entities[ trace.entityNum ]; + + if( traceEnt && traceEnt->biteam == client->ps.stats[ STAT_PTEAM ] && traceEnt->use ) + traceEnt->use( traceEnt, ent, ent ); //other and activator are the same in this context + else + { + //no entity in front of player - do a small area search + + VectorAdd( client->ps.origin, range, maxs ); + VectorSubtract( client->ps.origin, range, mins ); + + num = trap_EntitiesInBox( mins, maxs, entityList, MAX_GENTITIES ); + for( i = 0; i < num; i++ ) + { + traceEnt = &g_entities[ entityList[ i ] ]; + + if( traceEnt && traceEnt->biteam == client->ps.stats[ STAT_PTEAM ] && traceEnt->use ) + { + traceEnt->use( traceEnt, ent, ent ); //other and activator are the same in this context + break; + } + } + + if( i == num && client->ps.stats[ STAT_PTEAM ] == PTE_ALIENS ) + { + if( BG_UpgradeClassAvailable( &client->ps ) ) + { + //no nearby objects and alien - show class menu + G_TriggerMenu( ent->client->ps.clientNum, MN_A_INFEST ); + } + else + { + //flash frags + G_AddEvent( ent, EV_ALIEN_EVOLVE_FAILED, 0 ); + } + } + } + } + } + + if( level.framenum > client->retriggerArmouryMenu && client->retriggerArmouryMenu ) + { + G_TriggerMenu( client->ps.clientNum, MN_H_ARMOURY ); + + client->retriggerArmouryMenu = 0; + } + + // Give clients some credit periodically + if( ent->client->lastKillTime + FREEKILL_PERIOD < level.time ) + { + if( !g_suddenDeath.integer ) { + if( ent->client->ps.stats[ STAT_PTEAM ] == PTE_ALIENS ) + G_AddCreditToClient( ent->client, FREEKILL_ALIEN, qtrue ); + if( ent->client->ps.stats[ STAT_PTEAM ] == PTE_HUMANS ) + G_AddCreditToClient( ent->client, FREEKILL_HUMAN, qtrue ); + } + ent->client->lastKillTime = level.time; + } + + // perform once-a-second actions + ClientTimerActions( ent, msec ); + + if( ent->suicideTime > 0 && ent->suicideTime < level.time ) + { + ent->flags &= ~FL_GODMODE; + ent->client->ps.stats[ STAT_HEALTH ] = ent->health = 0; + player_die( ent, ent, ent, 100000, MOD_SUICIDE ); + + ent->suicideTime = 0; + } +} + +/* +================== +ClientThink + +A new command has arrived from the client +================== +*/ +void ClientThink( int clientNum ) +{ + gentity_t *ent; + + ent = g_entities + clientNum; + trap_GetUsercmd( clientNum, &ent->client->pers.cmd ); + + // mark the time we got info, so we can display the + // phone jack if they don't get any for a while + ent->client->lastCmdTime = level.time; + + if( !g_synchronousClients.integer ) + ClientThink_real( ent ); +} + + +void G_RunClient( gentity_t *ent ) +{ + if( !g_synchronousClients.integer ) + return; + + ent->client->pers.cmd.serverTime = level.time; + ClientThink_real( ent ); +} + + +/* +================== +SpectatorClientEndFrame + +================== +*/ +void SpectatorClientEndFrame( gentity_t *ent ) +{ + gclient_t *cl; + int clientNum, flags; + int score, ping; + vec3_t spawn_origin, spawn_angles; + + // if we are doing a chase cam or a remote view, grab the latest info + if( ent->client->sess.spectatorState == SPECTATOR_FOLLOW ) + { + clientNum = ent->client->sess.spectatorClient; + + if( clientNum >= 0 ) + { + cl = &level.clients[ clientNum ]; + + if( cl->pers.connected == CON_CONNECTED ) + { + + if( cl -> sess.spectatorState != SPECTATOR_FOLLOW ) + { + flags = ( cl->ps.eFlags & ~( EF_VOTED | EF_TEAMVOTED ) ) | + ( ent->client->ps.eFlags & ( EF_VOTED | EF_TEAMVOTED ) ); + score = ent->client->ps.persistant[ PERS_SCORE ]; + ping = ent->client->ps.ping; + ent->client->ps = cl->ps; + ent->client->ps.persistant[ PERS_SCORE ] = score; + ent->client->ps.ping = ping; + ent->client->ps.eFlags = flags; + ent->client->ps.pm_flags |= PMF_FOLLOW; + ent->client->ps.pm_flags &= ~PMF_QUEUED; + } + else //we are stickyspec-spectating someone who is spectating someone else + { + ent->client->ps.clientNum = (g_entities + clientNum)->s.number; + ent->client->ps.commandTime = cl->ps.commandTime; + ent->client->ps.weapon = 0; + ent->client->ps.pm_flags |= PMF_FOLLOW; + ent->client->ps.stats[ STAT_PCLASS ] = PCL_NONE; + + if( cl->pers.teamSelection == PTE_ALIENS ) + G_SelectAlienLockSpawnPoint( spawn_origin, spawn_angles ); + else if( cl->pers.teamSelection == PTE_HUMANS ) + G_SelectHumanLockSpawnPoint( spawn_origin, spawn_angles ); + + G_SetOrigin( ent, spawn_origin ); + VectorCopy( spawn_origin, ent->client->ps.origin ); + G_SetClientViewAngle( ent, spawn_angles ); + } + } + } + } +} + +/* +============== +ClientEndFrame + +Called at the end of each server frame for each connected client +A fast client will have multiple ClientThink for each ClientEdFrame, +while a slow client may have multiple ClientEndFrame between ClientThink. +============== +*/ +void ClientEndFrame( gentity_t *ent ) +{ + clientPersistant_t *pers; + + if( ent->client->sess.sessionTeam == TEAM_SPECTATOR ) + { + SpectatorClientEndFrame( ent ); + return; + } + + pers = &ent->client->pers; + + // save a copy of certain playerState values in case of SPECTATOR_FOLLOW + pers->score = ent->client->ps.persistant[ PERS_SCORE ]; + pers->credit = ent->client->ps.persistant[ PERS_CREDIT ]; + + // + // If the end of unit layout is displayed, don't give + // the player any normal movement attributes + // + if( level.intermissiontime ) + return; + + // burn from lava, etc + P_WorldEffects( ent ); + + // apply all the damage taken this frame + P_DamageFeedback( ent ); + + // add the EF_CONNECTION flag if we haven't gotten commands recently + if( level.time - ent->client->lastCmdTime > 1000 ) + ent->s.eFlags |= EF_CONNECTION; + else + ent->s.eFlags &= ~EF_CONNECTION; + + ent->client->ps.stats[ STAT_HEALTH ] = ent->health; // FIXME: get rid of ent->health... + + // respawn if dead + if( ent->client->ps.stats[ STAT_HEALTH ] <= 0 && level.time >= ent->client->respawnTime ) + respawn( ent ); + + G_SetClientSound( ent ); + + // set the latest infor + if( g_smoothClients.integer ) + BG_PlayerStateToEntityStateExtraPolate( &ent->client->ps, &ent->s, ent->client->ps.commandTime, qtrue ); + else + BG_PlayerStateToEntityState( &ent->client->ps, &ent->s, qtrue ); + + SendPendingPredictableEvents( &ent->client->ps ); +} + + diff --git a/src/game/g_admin.c b/src/game/g_admin.c new file mode 100644 index 0000000..f9c5f84 --- /dev/null +++ b/src/game/g_admin.c @@ -0,0 +1,7256 @@ +/* +=========================================================================== +Copyright (C) 2004-2006 Tony J. White + +This file is part of Tremulous. + +This shrubbot implementation is the original work of Tony J. White. + +Contains contributions from Wesley van Beelen, Chris Bajumpaa, Josh Menke, +and Travis Maurer. + +The functionality of this code mimics the behaviour of the currently +inactive project shrubet (http://www.etstats.com/shrubet/index.php?ver=2) +by Ryan Mannion. However, shrubet was a closed-source project and +none of it's code has been copied, only it's functionality. + +Tremulous is free software; you can redistribute it +and/or modify it under the terms of the GNU General Public License as +published by the Free Software Foundation; either version 2 of the License, +or (at your option) any later version. + +Tremulous is distributed in the hope that it will be +useful, but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with Tremulous; if not, write to the Free Software +Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA +=========================================================================== +*/ + +#include "g_local.h" + +// big ugly global buffer for use with buffered printing of long outputs +static char g_bfb[ 32000 ]; + +// note: list ordered alphabetically +g_admin_cmd_t g_admin_cmds[ ] = + { + {"adjustban", G_admin_adjustban, "ban", + "change the duration or reason of a ban. time is specified as numbers " + "followed by units 'w' (weeks), 'd' (days), 'h' (hours) or 'm' (minutes)," + " or seconds if no units are specified. if the duration is" + " preceded by a + or -, the ban duration will be extended or shortened by" + " the specified amount", + "[^3ban#^7] (^5duration^7) (^5reason^7)" + }, + + {"adminlog", G_admin_adminlog, "adminlog", + "list recent admin activity", + "(^5start id#|name|!command|-skip#^7) (^5search skip#^7)" + }, + + {"admintest", G_admin_admintest, "admintest", + "display your current admin level", + "" + }, + + {"allowbuild", G_admin_denybuild, "denybuild", + "restore a player's ability to build", + "[^3name|slot#^7]" + }, + + {"allowweapon", G_admin_denyweapon, "denyweapon", + "restore a player's ability to use a weapon or class", + "[^3name|slot#^7] [^3class|weapon|all^7]" + }, + + {"allready", G_admin_allready, "allready", + "makes everyone ready in intermission", + "" + }, + + {"ban", G_admin_ban, "ban", + "ban a player by IP and GUID with an optional expiration time and reason." + " time is specified as numbers followed by units 'w' (weeks), 'd' " + "(days), 'h' (hours) or 'm' (minutes), or seconds if no units are " + "specified", + "[^3name|slot#|IP^7] (^5time^7) (^5reason^7)" + }, + + {"buildlog", G_admin_buildlog, "buildlog", + "display a list of recent builds and deconstructs, optionally specifying" + " a team", + "(^5xnum^7) (^5#skip^7) (^5-name|num^7) (^5a|h^7)" + "\n ^3Example:^7 '!buildlog #10 h' skips 10 events, then shows the previous 10 events affecting human buildables" + }, + + {"cancelvote", G_admin_cancelvote, "cancelvote", + "cancel a vote taking place", + "" + }, + + {"cp", G_admin_cp, "cp", + "display a CP message to users, optionally specifying team(s) to send to", + "(-AHS) [^3message^7]" + }, + + {"demo", G_admin_demo, "demo", + "turn admin chat off for the caller so it does not appear in demos. " + "this is a toggle use !demo again to turn warnings back on", + "" + }, + + {"denybuild", G_admin_denybuild, "denybuild", + "take away a player's ability to build", + "[^3name|slot#^7]" + }, + + {"designate", G_admin_designate, "designate", + "give the player designated builder privileges", + "[^3name|slot#^7]" + }, + + {"devmap", G_admin_devmap, "devmap", + "load a map with cheats (and optionally force layout)", + "[^3mapname^7] (^5layout^7)" + }, + + {"denyweapon", G_admin_denyweapon, "denyweapon", + "take away a player's ability to use a weapon or class", + "[^3name|slot#^7] [^3class|weapon^7]" + }, + + {"drop", G_admin_drop, "drop", + "kick a client from the server without log", + "[^3name|slot#^7] [^3message^7]" + }, + + {"flag", G_admin_flag, "flag", + "add an admin flag to a player, prefix flag with '-' to disallow the flag. " + "console can use this command on admin levels by prefacing a '*' to the admin level value.", + "[^3name|slot#|admin#|*adminlevel^7] (^5+^7|^5-^7)[^3flag^7]" + }, + + {"flaglist", G_admin_flaglist, "flag", + "list all flags understood by this server", + "" + }, + + {"help", G_admin_help, "help", + "display commands available to you or help on a specific command", + "(^5command^7)" + }, + + {"info", G_admin_info, "info", + "display the contents of server info files", + "(^5subject^7)" + }, + + {"invisible", G_admin_invisible, "invisible", + "hides a player so they cannot be seen in playerlists", + "" + }, + + {"kick", G_admin_kick, "kick", + "kick a player with an optional reason", + "[^3name|slot#^7] (^5reason^7)" + }, + + {"L0", G_admin_L0, "l0", + "Sets a level 1 to level 0", + "[^3name|slot#^7]" + }, + + {"L1", G_admin_L1, "l1", + "Sets a level 0 to level 1", + "[^3name|slot#^7]" + }, + + {"layoutsave", G_admin_layoutsave, "layoutsave", + "save a map layout", + "[^3mapname^7]" + }, + + {"listadmins", G_admin_listadmins, "listadmins", + "display a list of all server admins and their levels", + "(^5name|start admin#^7) (^5minimum level to display^7)" + }, + + {"listlayouts", G_admin_listlayouts, "listlayouts", + "display a list of all available layouts for a map", + "(^5mapname^7)" + }, + + {"listplayers", G_admin_listplayers, "listplayers", + "display a list of players, their client numbers and their levels", + "" + }, + + {"listmaps", G_admin_listmaps, "listmaps", + "display a list of available maps on the server", + "(^5map name^7)" + }, + + {"lock", G_admin_lock, "lock", + "lock a team to prevent anyone from joining it", + "[^3a|h^7]" + }, + + {"map", G_admin_map, "map", + "load a map (and optionally force layout)", + "[^3mapname^7] (^5layout^7)" + }, + + {"maplog", G_admin_maplog, "maplog", + "show recently played maps", + "" + }, + + {"mute", G_admin_mute, "mute", + "mute a player", + "[^3name|slot#^7] (Duration)" + }, + + {"namelog", G_admin_namelog, "namelog", + "display a list of names used by recently connected players", + "(^5name^7)" + }, + + {"nextmap", G_admin_nextmap, "nextmap", + "go to the next map in the cycle", + "" + }, + + {"nobuild", G_admin_nobuild, "nobuild", + "set nobuild markers to prevent players from building in an area", + "(^5area^7) (^5height^7)" + }, + + {"passvote", G_admin_passvote, "passvote", + "pass a vote currently taking place", + "" + }, + + {"pause", G_admin_pause, "pause", + "prevent a player from interacting with the game." + " * will pause all players, using no argument will pause game clock", + "(^5name|slot|*^7)" + }, + + + {"putteam", G_admin_putteam, "putteam", + "move a player to a specified team", + "[^3name|slot#^7] [^3h|a|s^7] (^3duration^7)" + }, + + {"readconfig", G_admin_readconfig, "readconfig", + "reloads the admin config file and refreshes permission flags", + "" + }, + + {"register", G_admin_register, "register", + "Registers your name to protect it from being used by others or updates your admin name to your current name.", + "" + }, + + {"rename", G_admin_rename, "rename", + "rename a player", + "[^3name|slot#^7] [^3new name^7]" + }, + + {"restart", G_admin_restart, "restart", + "restart the current map (optionally using named layout or keeping/switching teams)", + "(^5layout^7) (^5keepteams|switchteams|keepteamslock|switchteamslock^7)" + }, + + {"revert", G_admin_revert, "revert", + "revert one or more buildlog events, optionally of only one team", + "(^5xnum^7) (^5#ID^7) (^5-name|num^7) (^5a|h^7)" + "\n ^3Example:^7 '!revert x5 h' reverts the last 5 events affecting human buildables" + }, + + {"rotation", G_admin_listrotation, "rotation", + "display a list of maps that are in the active map rotation", + "" + }, + + {"seen", G_admin_seen, "seen", + "find the last time a player was on the server", + "[^3name|admin#^7]" + }, + + {"setlevel", G_admin_setlevel, "setlevel", + "sets the admin level of a player", + "[^3name|slot#|admin#^7] [^3level^7]" + }, + + {"showbans", G_admin_showbans, "showbans", + "display a (partial) list of active bans", + "(^5start at ban#^7) (^5name|IP|'-subnet'^7)" + }, + + {"slap", G_admin_slap, "slap", + "Do damage to a player, and send them flying", + "[^3name|slot^7] (damage)" + }, + + {"spec999", G_admin_spec999, "spec999", + "move 999 pingers to the spectator team", + "" + }, + + {"specme", G_admin_putmespec, "specme", + "moves you to the spectators", + "" + }, + + {"subnetban", G_admin_subnetban, "subnetban", + "Add or change a subnet mask on a ban", + "[^3ban#^7] [^5CIDR mask^7]" + "\n ^3Example:^7 '!subnetban 10 16' changes ban #10 to be a ban on XXX.XXX.*.*" + "\n ^3Example:^7 '!subnetban 10 24' changes ban #10 to be a ban on XXX.XXX.XXX.*" + "\n ^3Example:^7 '!subnetban 10 32' changes ban #10 to be a regular (non-subnet) ban" + "\n ^1WARNING:^7 Use of this command may make your admin.dat incompatible with other game.qvms" + }, + + {"suspendban", G_admin_suspendban, "ban", + "suspend a ban for a length of time. time is specified as numbers " + "followed by units 'w' (weeks), 'd' (days), 'h' (hours) or 'm' (minutes)," + " or seconds if no units are specified", + "[^5ban #^7] [^5length^7]" + }, + + {"time", G_admin_time, "time", + "show the current local server time", + "" + }, + + {"unban", G_admin_unban, "ban", + "unbans a player specified by the slot as seen in showbans", + "[^3ban#^7]" + }, + + {"undesignate", G_admin_designate, "designate", + "revoke designated builder privileges", + "[^3name|slot#^7]" + }, + + {"unflag", G_admin_flag, "flag", + "clears an admin flag from a player. " + "console can use this command on admin levels by prefacing a '*' to the admin level value.", + "[^3name|slot#|admin#|*adminlevel^7] (^5+^7|^5-^7)[^3flag^7]" + }, + + {"unlock", G_admin_unlock, "lock", + "unlock a locked team", + "[^3a|h^7]" + }, + + {"unmute", G_admin_mute, "mute", + "unmute a muted player", + "[^3name|slot#^7]" + }, + + {"unpause", G_admin_pause, "pause", + "allow a player to interact with the game." + " * will unpause all players, using no argument will unpause game clock", + "(^5name|slot|*^7)" + }, + + { + "warn", G_admin_warn, "warn", + "Warn a player to cease or face admin intervention", + "[^3name|slot#^7] [reason]" + } + }; + +static int adminNumCmds = sizeof( g_admin_cmds ) / sizeof( g_admin_cmds[ 0 ] ); + +static int admin_level_maxname = 0; +g_admin_level_t *g_admin_levels[ MAX_ADMIN_LEVELS ]; +g_admin_admin_t *g_admin_admins[ MAX_ADMIN_ADMINS ]; +g_admin_ban_t *g_admin_bans[ MAX_ADMIN_BANS ]; +g_admin_command_t *g_admin_commands[ MAX_ADMIN_COMMANDS ]; +g_admin_namelog_t *g_admin_namelog[ MAX_ADMIN_NAMELOGS ]; + +// match a certain flag within these flags +// return state of whether flag was found or not, +// set *perm to indicate whether found flag was + or - +static qboolean admin_permission( char *flags, const char *flag, qboolean *perm ) +{ + char *token, *token_p = flags; + qboolean all_found = qfalse; + qboolean base_perm = qfalse; + + while( *( token = COM_Parse( &token_p ) ) ) + { + *perm = qtrue; + if( *token == '-' || *token == '+' ) + *perm = *token++ == '+'; + if( !strcmp( token, flag ) ) + return qtrue; + if( !strcmp( token, ADMF_ALLFLAGS ) ) + { + all_found = qtrue; + base_perm = *perm; + } + } + + if( all_found && flag[ 0 ] != '.' ) + { + *perm = base_perm; + return qtrue; + } + + return qfalse; +} + +static int admin_adminlog_index = 0; +g_admin_adminlog_t *g_admin_adminlog[ MAX_ADMIN_ADMINLOGS ]; + +// This function should only be used directly when the client is connecting and thus has no GUID. +// Else, use G_admin_permission() +qboolean G_admin_permission_guid( char *guid, const char* flag ) +{ + int i; + int l = 0; + qboolean perm = qfalse; + + // Does the admin specifically have this flag granted/denied to them, + // irrespective of their admin level? + for( i = 0; i < MAX_ADMIN_ADMINS && g_admin_admins[ i ]; i++ ) + { + if( !Q_stricmp( guid, g_admin_admins[ i ]->guid ) ) + { + if( admin_permission( g_admin_admins[ i ]->flags, flag, &perm ) ) + return perm; + l = g_admin_admins[ i ]->level; + break; + } + } + + // If not, is this flag granted/denied for their admin level? + for( i = 0; i < MAX_ADMIN_LEVELS && g_admin_levels[ i ]; i++ ) + { + if( g_admin_levels[ i ]->level == l ) + return admin_permission( g_admin_levels[ i ]->flags, flag, &perm ) && + perm; + } + return qfalse; +} + + +qboolean G_admin_permission( gentity_t *ent, const char *flag ) +{ + if(!ent) return qtrue; //console always wins + + return G_admin_permission_guid(ent->client->pers.guid, flag); +} + +qboolean G_admin_name_check( gentity_t *ent, char *name, char *err, int len ) +{ + int i; + gclient_t *client; + char testName[ MAX_NAME_LENGTH ] = {""}; + char name2[ MAX_NAME_LENGTH ] = {""}; + int alphaCount = 0; + + G_SanitiseString( name, name2, sizeof( name2) ); + + if( !Q_stricmp( name2, "UnnamedPlayer" ) ) + return qtrue; + + if( !Q_stricmp( name2, "console" ) ) + { + Q_strncpyz( err, va( "The name '%s^7' is invalid here", name2 ), + len ); + return qfalse; + } + + for( i = 0; i < level.maxclients; i++ ) + { + client = &level.clients[ i ]; + if( client->pers.connected != CON_CONNECTING + && client->pers.connected != CON_CONNECTED ) + { + continue; + } + + // can rename ones self to the same name using different colors + if( i == ( ent - g_entities ) ) + continue; + + G_SanitiseString( client->pers.netname, testName, sizeof( testName) ); + if( !Q_stricmp( name2, testName ) ) + { + Q_strncpyz( err, va( "The name '%s^7' is already in use", name ), + len ); + return qfalse; + } + } + + if( Q_isdigit( name2[ 0 ] ) || name2[ 0 ] == '-' ) + { + Q_strncpyz( err, "Names cannot begin with a number or with a dash. Please choose another.", len ); + return qfalse; + } + + for( i = 0; name2[ i ] !='\0'; i++) + { + if( Q_isalpha( name2[ i ] ) ) + alphaCount++; + + if( name2[ i ] == ' ' ) + { + if( name2[ i + 1 ] == '-' ) + { + Q_strncpyz( err, "Names cannot contain a - preceded by a space. Please choose another.", len ); + return qfalse; + } + } + } + + if( alphaCount == 0 ) + { + Q_strncpyz( err, va( "The name '%s^7' does not include at least one letter. Please choose another.", name ), len ); + return qfalse; + } + + if( !g_adminNameProtect.string[ 0 ] ) + return qtrue; + + for( i = 0; i < MAX_ADMIN_ADMINS && g_admin_admins[ i ]; i++ ) + { + if( g_admin_admins[ i ]->level < 1 ) + continue; + G_SanitiseString( g_admin_admins[ i ]->name, testName, sizeof( testName) ); + if( !Q_stricmp( name2, testName ) && + Q_stricmp( ent->client->pers.guid, g_admin_admins[ i ]->guid ) ) + { + Q_strncpyz( err, va( "The name '%s^7' belongs to an admin. " + "Please choose another.", name ), len ); + return qfalse; + } + } + return qtrue; +} + +static qboolean admin_higher_guid( char *admin_guid, char *victim_guid ) +{ + int i; + int alevel = 0; + int alevel2 = 0; + + for( i = 0; i < MAX_ADMIN_ADMINS && g_admin_admins[ i ]; i++ ) + { + if( !Q_stricmp( admin_guid, g_admin_admins[ i ]->guid ) ) + { + alevel = g_admin_admins[ i ]->level; + + // Novelty Levels should be equivelant to level 1 + if( alevel > 9 ) + alevel = 1; + + break; + } + } + for( i = 0; i < MAX_ADMIN_ADMINS && g_admin_admins[ i ]; i++ ) + { + if( !Q_stricmp( victim_guid, g_admin_admins[ i ]->guid ) ) + { + alevel2 = g_admin_admins[ i ]->level; + + // Novelty Levels should be equivelant to level 1 + if( alevel2 > 9 ) + alevel2 = 1; + + if( alevel < alevel2 ) + return qfalse; + + if( strstr( g_admin_admins[ i ]->flags, va( "%s", ADMF_IMMUTABLE ) ) ) + return qfalse; + } + } + return qtrue; +} + +static qboolean admin_higher( gentity_t *admin, gentity_t *victim ) +{ + + // console always wins + if( !admin ) + return qtrue; + // just in case + if( !victim ) + return qtrue; + + return admin_higher_guid( admin->client->pers.guid, + victim->client->pers.guid ); +} + +static void admin_writeconfig_string( char *s, fileHandle_t f ) +{ + char buf[ MAX_STRING_CHARS ]; + + buf[ 0 ] = '\0'; + if( s[ 0 ] ) + { + //Q_strcat(buf, sizeof(buf), s); + Q_strncpyz( buf, s, sizeof( buf ) ); + trap_FS_Write( buf, strlen( buf ), f ); + } + trap_FS_Write( "\n", 1, f ); +} + +static void admin_writeconfig_int( int v, fileHandle_t f ) +{ + char buf[ 32 ]; + + Com_sprintf( buf, sizeof(buf), "%d", v ); + if( buf[ 0 ] ) + trap_FS_Write( buf, strlen( buf ), f ); + trap_FS_Write( "\n", 1, f ); +} + +static void admin_writeconfig( void ) +{ + fileHandle_t f; + int len, i; + int t, expiretime; + char levels[ MAX_STRING_CHARS ] = {""}; + + if( !g_admin.string[ 0 ] ) + { + G_Printf( S_COLOR_YELLOW "WARNING: g_admin is not set. " + " configuration will not be saved to a file.\n" ); + return; + } + t = trap_RealTime( NULL ); + len = trap_FS_FOpenFile( g_admin.string, &f, FS_WRITE ); + if( len < 0 ) + { + G_Printf( "admin_writeconfig: could not open g_admin file \"%s\"\n", + g_admin.string ); + return; + } + for( i = 0; i < MAX_ADMIN_LEVELS && g_admin_levels[ i ]; i++ ) + { + trap_FS_Write( "[level]\n", 8, f ); + trap_FS_Write( "level = ", 10, f ); + admin_writeconfig_int( g_admin_levels[ i ]->level, f ); + trap_FS_Write( "name = ", 10, f ); + admin_writeconfig_string( g_admin_levels[ i ]->name, f ); + trap_FS_Write( "flags = ", 10, f ); + admin_writeconfig_string( g_admin_levels[ i ]->flags, f ); + trap_FS_Write( "\n", 1, f ); + } + for( i = 0; i < MAX_ADMIN_ADMINS && g_admin_admins[ i ]; i++ ) + { + // don't write level 0 users + if( g_admin_admins[ i ]->level < 1 ) + continue; + + //if set dont write admins that havent been seen in a while + expiretime = G_admin_parse_time( g_adminExpireTime.string ); + if( expiretime > 0 ) { + //only expire level 1 people + if( t - expiretime > g_admin_admins[ i ]->seen && g_admin_admins[ i ]->level == 1 ) { + G_Printf("Admin %s has been expired.\n", g_admin_admins[ i ]->name ); + continue; + } + } + + trap_FS_Write( "[admin]\n", 8, f ); + trap_FS_Write( "name = ", 10, f ); + admin_writeconfig_string( g_admin_admins[ i ]->name, f ); + trap_FS_Write( "guid = ", 10, f ); + admin_writeconfig_string( g_admin_admins[ i ]->guid, f ); + trap_FS_Write( "level = ", 10, f ); + admin_writeconfig_int( g_admin_admins[ i ]->level, f ); + trap_FS_Write( "flags = ", 10, f ); + admin_writeconfig_string( g_admin_admins[ i ]->flags, f ); + trap_FS_Write( "seen = ", 10, f ); + admin_writeconfig_int( g_admin_admins[ i ]->seen, f ); + trap_FS_Write( "\n", 1, f ); + } + for( i = 0; i < MAX_ADMIN_BANS && g_admin_bans[ i ]; i++ ) + { + // don't write expired bans + // if expires is 0, then it's a perm ban + if( g_admin_bans[ i ]->expires != 0 && + ( g_admin_bans[ i ]->expires - t ) < 1 ) + continue; + + trap_FS_Write( "[ban]\n", 6, f ); + trap_FS_Write( "name = ", 10, f ); + admin_writeconfig_string( g_admin_bans[ i ]->name, f ); + trap_FS_Write( "guid = ", 10, f ); + admin_writeconfig_string( g_admin_bans[ i ]->guid, f ); + trap_FS_Write( "ip = ", 10, f ); + admin_writeconfig_string( g_admin_bans[ i ]->ip, f ); + trap_FS_Write( "reason = ", 10, f ); + admin_writeconfig_string( g_admin_bans[ i ]->reason, f ); + trap_FS_Write( "made = ", 10, f ); + admin_writeconfig_string( g_admin_bans[ i ]->made, f ); + trap_FS_Write( "expires = ", 10, f ); + admin_writeconfig_int( g_admin_bans[ i ]->expires, f ); + if( g_admin_bans[ i ]->suspend > t ) { + trap_FS_Write( "suspend = ", 10, f ); + admin_writeconfig_int( g_admin_bans[ i ]->suspend, f ); + } + trap_FS_Write( "banner = ", 10, f ); + admin_writeconfig_string( g_admin_bans[ i ]->banner, f ); + trap_FS_Write( "blevel = ", 10, f ); + admin_writeconfig_int( g_admin_bans[ i ]->bannerlevel, f ); + trap_FS_Write( "\n", 1, f ); + } + for( i = 0; i < MAX_ADMIN_COMMANDS && g_admin_commands[ i ]; i++ ) + { + levels[ 0 ] = '\0'; + trap_FS_Write( "[command]\n", 10, f ); + trap_FS_Write( "command = ", 10, f ); + admin_writeconfig_string( g_admin_commands[ i ]->command, f ); + trap_FS_Write( "exec = ", 10, f ); + admin_writeconfig_string( g_admin_commands[ i ]->exec, f ); + trap_FS_Write( "desc = ", 10, f ); + admin_writeconfig_string( g_admin_commands[ i ]->desc, f ); + trap_FS_Write( "flag = ", 10, f ); + admin_writeconfig_string( g_admin_commands[ i ]->flag, f ); + trap_FS_Write( "\n", 1, f ); + } + trap_FS_FCloseFile( f ); +} + +static void admin_readconfig_string( char **cnf, char *s, int size ) +{ + char * t; + + //COM_MatchToken(cnf, "="); + t = COM_ParseExt( cnf, qfalse ); + if( !strcmp( t, "=" ) ) + { + t = COM_ParseExt( cnf, qfalse ); + } + else + { + G_Printf( "readconfig: warning missing = before " + "\"%s\" on line %d\n", + t, + COM_GetCurrentParseLine() ); + } + s[ 0 ] = '\0'; + while( t[ 0 ] ) + { + if( ( s[ 0 ] == '\0' && strlen( t ) <= size ) + || ( strlen( t ) + strlen( s ) < size ) ) + { + + Q_strcat( s, size, t ); + Q_strcat( s, size, " " ); + } + t = COM_ParseExt( cnf, qfalse ); + } + // trim the trailing space + if( strlen( s ) > 0 && s[ strlen( s ) - 1 ] == ' ' ) + s[ strlen( s ) - 1 ] = '\0'; +} + +static void admin_readconfig_int( char **cnf, int *v ) +{ + char * t; + + //COM_MatchToken(cnf, "="); + t = COM_ParseExt( cnf, qfalse ); + if( !strcmp( t, "=" ) ) + { + t = COM_ParseExt( cnf, qfalse ); + } + else + { + G_Printf( "readconfig: warning missing = before " + "\"%s\" on line %d\n", + t, + COM_GetCurrentParseLine() ); + } + *v = atoi( t ); +} + +// if we can't parse any levels from readconfig, set up default +// ones to make new installs easier for admins +static void admin_default_levels( void ) +{ + g_admin_level_t * l; + int i; + + for( i = 0; i < MAX_ADMIN_LEVELS && g_admin_levels[ i ]; i++ ) + { + G_Free( g_admin_levels[ i ] ); + g_admin_levels[ i ] = NULL; + } + for( i = 0; i <= 5; i++ ) + { + l = G_Alloc( sizeof( g_admin_level_t ) ); + l->level = i; + *l->name = '\0'; + *l->flags = '\0'; + g_admin_levels[ i ] = l; + } + + Q_strncpyz( g_admin_levels[ 0 ]->name, "^4Unknown Player", + sizeof( l->name ) ); + Q_strncpyz( g_admin_levels[ 0 ]->flags, + "listplayers admintest help specme time", + sizeof( l->flags ) ); + + Q_strncpyz( g_admin_levels[ 1 ]->name, "^5Server Regular", + sizeof( l->name ) ); + Q_strncpyz( g_admin_levels[ 1 ]->flags, + "listplayers admintest help specme time", + sizeof( l->flags ) ); + + Q_strncpyz( g_admin_levels[ 2 ]->name, "^6Team Manager", + sizeof( l->name ) ); + Q_strncpyz( g_admin_levels[ 2 ]->flags, + "listplayers admintest help specme time putteam spec999 warn denybuild", + sizeof( l->flags ) ); + + Q_strncpyz( g_admin_levels[ 3 ]->name, "^2Junior Admin", + sizeof( l->name ) ); + Q_strncpyz( g_admin_levels[ 3 ]->flags, + "listplayers admintest help specme time putteam spec999 kick mute warn " + "denybuild ADMINCHAT SEESFULLLISTPLAYERS", + sizeof( l->flags ) ); + + Q_strncpyz( g_admin_levels[ 4 ]->name, "^3Senior Admin", + sizeof( l->name ) ); + Q_strncpyz( g_admin_levels[ 4 ]->flags, + "listplayers admintest help specme time putteam spec999 kick mute showbans " + "ban namelog warn denybuild ADMINCHAT SEESFULLLISTPLAYERS", + sizeof( l->flags ) ); + + Q_strncpyz( g_admin_levels[ 5 ]->name, "^1Server Operator", + sizeof( l->name ) ); + Q_strncpyz( g_admin_levels[ 5 ]->flags, + "ALLFLAGS -INCOGNITO -IMMUTABLE -DBUILDER -BANIMMUNITY", + sizeof( l->flags ) ); +} + +// return a level for a player entity. +int G_admin_level( gentity_t *ent ) +{ + int i; + qboolean found = qfalse; + + if( !ent ) + { + return MAX_ADMIN_LEVELS; + } + + for( i = 0; i < MAX_ADMIN_ADMINS && g_admin_admins[ i ]; i++ ) + { + if( !Q_stricmp( g_admin_admins[ i ]->guid, ent->client->pers.guid ) ) + { + + found = qtrue; + break; + } + } + + if( found ) + { + return g_admin_admins[ i ]->level; + } + + return 0; +} + +// set a player's adminname +void G_admin_set_adminname( gentity_t *ent ) +{ + int i; + qboolean found = qfalse; + + if( !ent ) + { + return; + } + + for( i = 0; i < MAX_ADMIN_ADMINS && g_admin_admins[ i ]; i++ ) + { + if( !Q_stricmp( g_admin_admins[ i ]->guid, ent->client->pers.guid ) ) + { + found = qtrue; + break; + } + } + + if( found ) + { + Q_strncpyz( ent->client->pers.adminName, g_admin_admins[ i ]->name, sizeof( ent->client->pers.adminName ) ); + } + else + { + Q_strncpyz( ent->client->pers.adminName, "", sizeof( ent->client->pers.adminName ) ); + } +} + +// Get an admin's registered name +const char *G_admin_get_adminname( gentity_t *ent ) +{ + int i; + + if( !ent ) + return "console"; + + for( i = 0; i < MAX_ADMIN_ADMINS && g_admin_admins[ i ]; i++ ) + { + if( !Q_stricmp( g_admin_admins[ i ]->guid, ent->client->pers.guid ) ) + return g_admin_admins[ i ]->name; + } + + return ent->client->pers.netname; +} + +// Get an admin's name to print as the user of various commands, +// obeying admin stealth settings when necessary +char* G_admin_adminPrintName( gentity_t *ent ) +{ + char *out; + + if( !ent->client->pers.adminLevel ) + { + out = ""; + return out; + } + + if( G_admin_permission( ent, ADMF_ADMINSTEALTH ) ) + { + out = ent->client->pers.adminName; + } + else + { + out = ent->client->pers.netname; + } + + + return out; +} + +static void admin_log( gentity_t *admin, char *cmd, int skiparg ) +{ + fileHandle_t f; + int len, i, j; + char string[ MAX_STRING_CHARS ], decoloured[ MAX_STRING_CHARS ]; + int min, tens, sec; + g_admin_admin_t *a; + g_admin_level_t *l; + char flags[ MAX_ADMIN_FLAGS * 2 ]; + gentity_t *victim = NULL; + int pids[ MAX_CLIENTS ]; + char name[ MAX_NAME_LENGTH ]; + + if( !g_adminLog.string[ 0 ] ) + return ; + + + len = trap_FS_FOpenFile( g_adminLog.string, &f, FS_APPEND ); + if( len < 0 ) + { + G_Printf( "admin_log: error could not open %s\n", g_adminLog.string ); + return ; + } + + sec = (level.time - level.startTime) / 1000; + min = sec / 60; + sec -= min * 60; + tens = sec / 10; + sec -= tens * 10; + + *flags = '\0'; + if( admin ) + { + for( i = 0; i < MAX_ADMIN_ADMINS && g_admin_admins[ i ]; i++ ) + { + if( !Q_stricmp( g_admin_admins[ i ]->guid , admin->client->pers.guid ) ) + { + + a = g_admin_admins[ i ]; + Q_strncpyz( flags, a->flags, sizeof( flags ) ); + for( j = 0; j < MAX_ADMIN_LEVELS && g_admin_levels[ j ]; j++ ) + { + if( g_admin_levels[ j ]->level == a->level ) + { + l = g_admin_levels[ j ]; + Q_strcat( flags, sizeof( flags ), l->flags ); + break; + } + } + break; + } + } + } + + if( G_SayArgc() > 1 + skiparg ) + { + G_SayArgv( 1 + skiparg, name, sizeof( name ) ); + if( G_ClientNumbersFromString( name, pids ) == 1 ) + { + victim = &g_entities[ pids[ 0 ] ]; + } + } + + if( victim && Q_stricmp( cmd, "attempted" ) ) + { + Com_sprintf( string, sizeof( string ), + "%3i:%i%i: %i: %s: %s (%s): %s: %s: %s: %s: \"%s\"\n", + min, + tens, + sec, + ( admin ) ? admin->s.clientNum : -1, + ( admin ) ? admin->client->pers.guid + : "XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX", + ( admin ) ? admin->client->pers.netname : "console", + ( admin ) ? admin->client->pers.adminName : "console", + flags, + cmd, + victim->client->pers.guid, + victim->client->pers.netname, + G_SayConcatArgs( 2 + skiparg ) ); + } + else + { + Com_sprintf( string, sizeof( string ), + "%3i:%i%i: %i: %s: %s (%s): %s: %s: \"%s\"\n", + min, + tens, + sec, + ( admin ) ? admin->s.clientNum : -1, + ( admin ) ? admin->client->pers.guid + : "XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX", + ( admin ) ? admin->client->pers.netname : "console", + ( admin ) ? admin->client->pers.adminName : "console", + flags, + cmd, + G_SayConcatArgs( 1 + skiparg ) ); + } + + if( g_decolourLogfiles.integer ) + { + G_DecolorString( string, decoloured ); + trap_FS_Write( decoloured, strlen( decoloured ), f ); + } + else + { + trap_FS_Write( string, strlen( string ), f ); + } + trap_FS_FCloseFile( f ); + + if ( !Q_stricmp( cmd, "attempted" ) ) + { + Com_sprintf( string, sizeof( string ), + "%s^7 (%i) %s: %s", + ( admin ) ? admin->client->pers.netname : "console", + ( admin ) ? admin->s.clientNum : -1, + cmd, + G_SayConcatArgs( 1 + skiparg ) ); + G_AdminsPrintf("%s\n",string); + } + + G_LogPrintf("Admin Command: %s^7 (%s): %s %s\n",( admin ) ? admin->client->pers.netname : "console", ( admin ) ? admin->client->pers.adminName : "console", cmd, G_SayConcatArgs( 1 + skiparg )); +} + +static int admin_listadmins( gentity_t *ent, int start, char *search, int minlevel ) +{ + int drawn = 0; + char guid_stub[9]; + char name[ MAX_NAME_LENGTH ] = {""}; + char name2[ MAX_NAME_LENGTH ] = {""}; + char lname[ MAX_NAME_LENGTH ] = {""}; + char lname_fmt[ 5 ]; + int i,j; + gentity_t *vic; + int l = 0; + qboolean dup = qfalse; + + ADMBP_begin(); + + // print out all connected players regardless of level if name searching + for( i = 0; i < level.maxclients && search[ 0 ]; i++ ) + { + vic = &g_entities[ i ]; + + if( vic->client && vic->client->pers.connected != CON_CONNECTED ) + continue; + + l = vic->client->pers.adminLevel; + + G_SanitiseString( vic->client->pers.netname, name, sizeof( name ) ); + if( !strstr( name, search ) ) + continue; + + for( j = 0; j < 8; j++ ) + guid_stub[ j ] = vic->client->pers.guid[ j + 24 ]; + guid_stub[ j ] = '\0'; + + lname[ 0 ] = '\0'; + Q_strncpyz( lname_fmt, "%s", sizeof( lname_fmt ) ); + for( j = 0; j < MAX_ADMIN_LEVELS && g_admin_levels[ j ]; j++ ) + { + if( g_admin_levels[ j ]->level == l ) + { + G_DecolorString( g_admin_levels[ j ]->name, lname ); + Com_sprintf( lname_fmt, sizeof( lname_fmt ), "%%%is", + ( admin_level_maxname + strlen( g_admin_levels[ j ]->name ) + - strlen( lname ) ) ); + Com_sprintf( lname, sizeof( lname ), lname_fmt, + g_admin_levels[ j ]->name ); + break; + } + } + ADMBP( va( "%4i %4i %s^7 (*%s) %s^7\n", + i, + l, + lname, + guid_stub, + vic->client->pers.netname ) ); + drawn++; + } + + for( i = 0; i < MAX_ADMIN_ADMINS && g_admin_admins[ i ] + && drawn < MAX_ADMIN_LISTITEMS; i++ ) + if( g_admin_admins[ i ]->level >= minlevel ) + { + + if( start ) + { + start--; + continue; + } + + if( search[ 0 ] ) + { + G_SanitiseString( g_admin_admins[ i ]->name, name, sizeof( name ) ); + if( !strstr( name, search ) ) + continue; + + // verify we don't have the same guid/name pair in connected players + // since we don't want to draw the same player twice + dup = qfalse; + for( j = 0; j < level.maxclients; j++ ) + { + vic = &g_entities[ j ]; + if( !vic->client || vic->client->pers.connected != CON_CONNECTED ) + continue; + G_SanitiseString( vic->client->pers.netname, name2, sizeof( name2) ); + if( !Q_stricmp( vic->client->pers.guid, g_admin_admins[ i ]->guid ) + && strstr( name2, search ) ) + { + dup = qtrue; + break; + } + } + if( dup ) + continue; + } + for( j = 0; j < 8; j++ ) + guid_stub[ j ] = g_admin_admins[ i ]->guid[ j + 24 ]; + guid_stub[ j ] = '\0'; + + lname[ 0 ] = '\0'; + Q_strncpyz( lname_fmt, "%s", sizeof( lname_fmt ) ); + for( j = 0; j < MAX_ADMIN_LEVELS && g_admin_levels[ j ]; j++ ) + { + if( g_admin_levels[ j ]->level == g_admin_admins[ i ]->level ) + { + G_DecolorString( g_admin_levels[ j ]->name, lname ); + Com_sprintf( lname_fmt, sizeof( lname_fmt ), "%%%is", + ( admin_level_maxname + strlen( g_admin_levels[ j ]->name ) + - strlen( lname ) ) ); + Com_sprintf( lname, sizeof( lname ), lname_fmt, + g_admin_levels[ j ]->name ); + break; + } + } + ADMBP( va( "%4i %4i %s^7 (*%s) %s^7\n", + ( i + MAX_CLIENTS ), + g_admin_admins[ i ]->level, + lname, + guid_stub, + g_admin_admins[ i ]->name ) ); + drawn++; + } + ADMBP_end(); + return drawn; +} + +void G_admin_duration( int secs, char *duration, int dursize ) +{ + + if( secs > ( 60 * 60 * 24 * 365 * 50 ) || secs < 0 ) + Q_strncpyz( duration, "PERMANENT", dursize ); + else if( secs >= ( 60 * 60 * 24 * 365 ) ) + Com_sprintf( duration, dursize, "%1.1f years", + ( secs / ( 60 * 60 * 24 * 365.0f ) ) ); + else if( secs >= ( 60 * 60 * 24 * 90 ) ) + Com_sprintf( duration, dursize, "%1.1f weeks", + ( secs / ( 60 * 60 * 24 * 7.0f ) ) ); + else if( secs >= ( 60 * 60 * 24 ) ) + Com_sprintf( duration, dursize, "%1.1f days", + ( secs / ( 60 * 60 * 24.0f ) ) ); + else if( secs >= ( 60 * 60 ) ) + Com_sprintf( duration, dursize, "%1.1f hours", + ( secs / ( 60 * 60.0f ) ) ); + else if( secs >= 60 ) + Com_sprintf( duration, dursize, "%1.1f minutes", + ( secs / 60.0f ) ); + else + Com_sprintf( duration, dursize, "%i seconds", secs ); +} + +qboolean G_admin_ban_check( char *userinfo, char *reason, int rlen ) +{ + static char lastConnectIP[ 16 ] = {""}; + static int lastConnectTime = 0; + char guid[ 33 ]; + char ip[ 16 ]; + char *value; + int i; + unsigned int userIP = 0, intIP = 0, tempIP; + int IP[5], k, mask, ipscanfcount; + int t; + char notice[51]; + qboolean ignoreIP = qfalse; + + trap_Cvar_VariableStringBuffer( "g_banNotice", notice, sizeof( notice ) ); + + *reason = '\0'; + + if( !*userinfo ) + return qfalse; + + value = Info_ValueForKey( userinfo, "ip" ); + Q_strncpyz( ip, value, sizeof( ip ) ); + // strip port + value = strchr( ip, ':' ); + if ( value ) + *value = '\0'; + + if( !*ip ) + return qfalse; + + value = Info_ValueForKey( userinfo, "cl_guid" ); + Q_strncpyz( guid, value, sizeof( guid ) ); + + t = trap_RealTime( NULL ); + memset( IP, 0, sizeof( IP )); + sscanf(ip, "%i.%i.%i.%i", &IP[4], &IP[3], &IP[2], &IP[1]); + for(k = 4; k >= 1; k--) + { + if(!IP[k]) continue; + userIP |= IP[k] << 8*(k-1); + } + ignoreIP = G_admin_permission_guid( guid , ADMF_BAN_IMMUNITY ); + for( i = 0; i < MAX_ADMIN_BANS && g_admin_bans[ i ]; i++ ) + { + // 0 is for perm ban + if( g_admin_bans[ i ]->expires != 0 && + ( g_admin_bans[ i ]->expires - t ) < 1 ) + continue; + //if suspend time is over continue + if( g_admin_bans[ i ]->suspend >= t ) + continue; + + if( !ignoreIP ) + { + tempIP = userIP; + intIP = 0; + mask = -1; + + memset( IP, 0, sizeof( IP )); + ipscanfcount = sscanf(g_admin_bans[ i ]->ip, "%d.%d.%d.%d/%d", &IP[4], &IP[3], &IP[2], &IP[1], &IP[0]); + + if( ipscanfcount == 4 ) + mask = -1; + else if( ipscanfcount == 5 ) + mask = IP[0]; + else if( ipscanfcount > 0 && ipscanfcount < 4 ) + mask = 8 * ipscanfcount; + else + continue; + + for(k = 4; k >= 1; k--) + { + if(!IP[k]) continue; + intIP |= IP[k] << 8*(k-1); + } + + if(mask > 0 && mask <= 32) + { + tempIP &= ~((1 << (32-mask)) - 1); + intIP &= ~((1 << (32-mask)) - 1); + } + + if( intIP == tempIP || mask == 0 ) + { + char duration[ 32 ]; + G_admin_duration( ( g_admin_bans[ i ]->expires - t ), + duration, sizeof( duration ) ); + + // flood protected + if( t - lastConnectTime >= 300 || + Q_stricmp( lastConnectIP, ip ) ) + { + lastConnectTime = t; + Q_strncpyz( lastConnectIP, ip, sizeof( lastConnectIP ) ); + + G_WarningsPrintf( + "ban", + "Banned player %s^7 (%s^7) tried to connect (ban #%i on %s by %s^7 expires %s reason: %s^7 )\n", + Info_ValueForKey( userinfo, "name" ), + g_admin_bans[ i ]->name, + i+1, + ip, + g_admin_bans[ i ]->banner, + duration, + g_admin_bans[ i ]->reason ); + } + + Com_sprintf( + reason, + rlen, + "You have been banned by %s^7 reason: %s^7 expires: %s %s", + g_admin_bans[ i ]->banner, + g_admin_bans[ i ]->reason, + duration, + notice + ); + G_LogPrintf("Banned player tried to connect from IP %s\n", ip); + return qtrue; + } + } + if( *guid && !Q_stricmp( g_admin_bans[ i ]->guid, guid ) ) + { + char duration[ 32 ]; + G_admin_duration( ( g_admin_bans[ i ]->expires - t ), + duration, sizeof( duration ) ); + Com_sprintf( + reason, + rlen, + "You have been banned by %s^7 reason: %s^7 expires: %s", + g_admin_bans[ i ]->banner, + g_admin_bans[ i ]->reason, + duration + ); + G_Printf("Banned player tried to connect with GUID %s\n", guid); + return qtrue; + } + } + if ( *guid ) + { + int count = 0; + qboolean valid = qtrue; + + while( guid[ count ] != '\0' && valid ) + { + if( (guid[ count ] < '0' || guid[ count ] > '9') && + (guid[ count ] < 'A' || guid[ count ] > 'F') ) + { + valid = qfalse; + } + count++; + } + if( !valid || count != 32 ) + { + Com_sprintf( reason, rlen, "Invalid client data" ); + G_Printf("Player with invalid GUID [%s] connect from IP %s\n", guid, ip); + return qtrue; + } + } + return qfalse; +} + +qboolean G_admin_cmd_check( gentity_t *ent, qboolean say ) +{ + int i; + char command[ MAX_ADMIN_CMD_LEN ]; + char *cmd; + int skip = 0; + + command[ 0 ] = '\0'; + G_SayArgv( 0, command, sizeof( command ) ); + if( !Q_stricmp( command, "say" ) || + ( G_admin_permission( ent, ADMF_TEAMCHAT_CMD ) && + ( !Q_stricmp( command, "say_team" ) ) ) ) + { + skip = 1; + G_SayArgv( 1, command, sizeof( command ) ); + } + if( !command[ 0 ] ) + return qfalse; + + if( command[ 0 ] == '!' ) + { + cmd = &command[ 1 ]; + } + else + { + return qfalse; + } + + // Flood limit. If they're talking too fast, determine that and return. + if( g_floodMinTime.integer ) + if ( G_Flood_Limited( ent ) ) + { + trap_SendServerCommand( ent-g_entities, "print \"Your chat is flood-limited; wait before chatting again\n\"" ); + return qtrue; + } + + for( i = 0; i < MAX_ADMIN_COMMANDS && g_admin_commands[ i ]; i++ ) + { + if( Q_stricmp( cmd, g_admin_commands[ i ]->command ) ) + continue; + + if( G_admin_permission( ent, g_admin_commands[ i ]->flag ) ) + { + trap_SendConsoleCommand( EXEC_APPEND, g_admin_commands[ i ]->exec ); + admin_log( ent, cmd, skip ); + G_admin_adminlog_log( ent, cmd, NULL, skip, qtrue ); + } + else + { + ADMP( va( "^3!%s: ^7permission denied\n", g_admin_commands[ i ]->command ) ); + admin_log( ent, "attempted", skip - 1 ); + G_admin_adminlog_log( ent, cmd, NULL, skip, qfalse ); + } + return qtrue; + } + + for( i = 0; i < adminNumCmds; i++ ) + { + if( Q_stricmp( cmd, g_admin_cmds[ i ].keyword ) ) + continue; + + if( G_admin_permission( ent, g_admin_cmds[ i ].flag ) ) + { + g_admin_cmds[ i ].handler( ent, skip ); + admin_log( ent, cmd, skip ); + G_admin_adminlog_log( ent, cmd, NULL, skip, qtrue ); + } + else + { + ADMP( va( "^3!%s: ^7permission denied\n", g_admin_cmds[ i ].keyword ) ); + admin_log( ent, "attempted", skip - 1 ); + G_admin_adminlog_log( ent, cmd, NULL, skip, qfalse ); + } + return qtrue; + } + return qfalse; +} + +void G_admin_namelog_cleanup( ) +{ + int i; + + for( i = 0; i < MAX_ADMIN_NAMELOGS && g_admin_namelog[ i ]; i++ ) + { + G_Free( g_admin_namelog[ i ] ); + g_admin_namelog[ i ] = NULL; + } +} + +void G_admin_namelog_update( gclient_t *client, qboolean disconnect ) +{ + int i, j; + g_admin_namelog_t *namelog; + char n1[ MAX_NAME_LENGTH ]; + char n2[ MAX_NAME_LENGTH ]; + int clientNum = ( client - level.clients ); + + if ( client->sess.invisible == qfalse ) + { + G_admin_seen_update( client->pers.guid ); + } + + G_SanitiseString( client->pers.netname, n1, sizeof( n1 ) ); + for( i = 0; i < MAX_ADMIN_NAMELOGS && g_admin_namelog[ i ]; i++ ) + { + if( disconnect && g_admin_namelog[ i ]->slot != clientNum ) + continue; + + if( !disconnect && !( g_admin_namelog[ i ]->slot == clientNum || + g_admin_namelog[ i ]->slot == -1 ) ) + { + continue; + } + + if( !Q_stricmp( client->pers.ip, g_admin_namelog[ i ]->ip ) + && !Q_stricmp( client->pers.guid, g_admin_namelog[ i ]->guid ) ) + { + for( j = 0; j < MAX_ADMIN_NAMELOG_NAMES + && g_admin_namelog[ i ]->name[ j ][ 0 ]; j++ ) + { + G_SanitiseString( g_admin_namelog[ i ]->name[ j ], n2, sizeof( n2 ) ); + if( !Q_stricmp( n1, n2 ) ) + break; + } + if( j == MAX_ADMIN_NAMELOG_NAMES ) + j = MAX_ADMIN_NAMELOG_NAMES - 1; + Q_strncpyz( g_admin_namelog[ i ]->name[ j ], client->pers.netname, + sizeof( g_admin_namelog[ i ]->name[ j ] ) ); + g_admin_namelog[ i ]->slot = ( disconnect ) ? -1 : clientNum; + + // if this player is connecting, they are no longer banned + if( !disconnect ) + g_admin_namelog[ i ]->banned = qfalse; + + //check other things like if user was denybuild or muted or denyweapon and restore them + if( !disconnect ) + { + if( g_admin_namelog[ i ]->muted ) + { + client->pers.muted = qtrue; + client->pers.muteExpires = g_admin_namelog[ i ]->muteExpires; + G_AdminsPrintf( "^7%s^7's mute has been restored\n", client->pers.netname ); + g_admin_namelog[ i ]->muted = qfalse; + } + if( g_admin_namelog[ i ]->denyBuild ) + { + client->pers.denyBuild = qtrue; + G_AdminsPrintf( "^7%s^7's Denybuild has been restored\n", client->pers.netname ); + g_admin_namelog[ i ]->denyBuild = qfalse; + } + if( g_admin_namelog[ i ]->denyHumanWeapons > 0 || g_admin_namelog[ i ]->denyAlienClasses > 0 ) + { + if( g_admin_namelog[ i ]->denyHumanWeapons > 0 ) + client->pers.denyHumanWeapons = g_admin_namelog[ i ]->denyHumanWeapons; + if( g_admin_namelog[ i ]->denyAlienClasses > 0 ) + client->pers.denyAlienClasses = g_admin_namelog[ i ]->denyAlienClasses; + + G_AdminsPrintf( "^7%s^7's Denyweapon has been restored\n", client->pers.netname ); + g_admin_namelog[ i ]->denyHumanWeapons = 0; + g_admin_namelog[ i ]->denyAlienClasses = 0; + } + if( g_admin_namelog[ i ]->specExpires > 0 ) + { + client->pers.specExpires = g_admin_namelog[ i ]->specExpires; + G_AdminsPrintf( "^7%s^7's Putteam spectator has been restored\n", client->pers.netname ); + g_admin_namelog[ i ]->specExpires = 0; + } + if( g_admin_namelog[ i ]->voteCount > 0 ) + { + client->pers.voteCount = g_admin_namelog[ i ]->voteCount; + g_admin_namelog[ i ]->voteCount = 0; + } + } + else + { + //for mute + if( G_IsMuted( client ) ) + { + g_admin_namelog[ i ]->muted = qtrue; + g_admin_namelog[ i ]->muteExpires = client->pers.muteExpires; + } + //denybuild + if( client->pers.denyBuild ) + { + g_admin_namelog[ i ]->denyBuild = qtrue; + } + //denyweapon humans + if( client->pers.denyHumanWeapons > 0 ) + { + g_admin_namelog[ i ]->denyHumanWeapons = client->pers.denyHumanWeapons; + } + //denyweapon aliens + if( client->pers.denyAlienClasses > 0 ) + { + g_admin_namelog[ i ]->denyAlienClasses = client->pers.denyAlienClasses; + } + //putteam spec + if( client->pers.specExpires > 0 ) + { + g_admin_namelog[ i ]->specExpires = client->pers.specExpires; + } + if( client->pers.voteCount > 0 ) + { + g_admin_namelog[ i ]->voteCount = client->pers.voteCount; + } + } + + return; + } + } + if( i >= MAX_ADMIN_NAMELOGS ) + { + G_Printf( "G_admin_namelog_update: warning, g_admin_namelogs overflow\n" ); + return; + } + namelog = G_Alloc( sizeof( g_admin_namelog_t ) ); + memset( namelog, 0, sizeof( namelog ) ); + for( j = 0; j < MAX_ADMIN_NAMELOG_NAMES ; j++ ) + namelog->name[ j ][ 0 ] = '\0'; + Q_strncpyz( namelog->ip, client->pers.ip, sizeof( namelog->ip ) ); + Q_strncpyz( namelog->guid, client->pers.guid, sizeof( namelog->guid ) ); + Q_strncpyz( namelog->name[ 0 ], client->pers.netname, + sizeof( namelog->name[ 0 ] ) ); + namelog->slot = ( disconnect ) ? -1 : clientNum; + g_admin_namelog[ i ] = namelog; +} + +qboolean G_admin_readconfig( gentity_t *ent, int skiparg ) +{ + g_admin_level_t * l = NULL; + g_admin_admin_t *a = NULL; + g_admin_ban_t *b = NULL; + g_admin_command_t *c = NULL; + int lc = 0, ac = 0, bc = 0, cc = 0; + fileHandle_t f; + int len; + char *cnf, *cnf2; + char *t; + qboolean level_open, admin_open, ban_open, command_open; + int i; + + G_admin_cleanup(); + + if( !g_admin.string[ 0 ] ) + { + ADMP( "^3!readconfig: g_admin is not set, not loading configuration " + "from a file\n" ); + admin_default_levels(); + return qfalse; + } + + len = trap_FS_FOpenFile( g_admin.string, &f, FS_READ ) ; + if( len < 0 ) + { + ADMP( va( "^3!readconfig: ^7could not open admin config file %s\n", + g_admin.string ) ); + admin_default_levels(); + return qfalse; + } + cnf = G_Alloc( len + 1 ); + cnf2 = cnf; + trap_FS_Read( cnf, len, f ); + *( cnf + len ) = '\0'; + trap_FS_FCloseFile( f ); + + t = COM_Parse( &cnf ); + level_open = admin_open = ban_open = command_open = qfalse; + while( *t ) + { + if( !Q_stricmp( t, "[level]" ) || + !Q_stricmp( t, "[admin]" ) || + !Q_stricmp( t, "[ban]" ) || + !Q_stricmp( t, "[command]" ) ) + { + + if( level_open ) + g_admin_levels[ lc++ ] = l; + else if( admin_open ) + g_admin_admins[ ac++ ] = a; + else if( ban_open ) + g_admin_bans[ bc++ ] = b; + else if( command_open ) + g_admin_commands[ cc++ ] = c; + level_open = admin_open = + ban_open = command_open = qfalse; + } + + if( level_open ) + { + if( !Q_stricmp( t, "level" ) ) + { + admin_readconfig_int( &cnf, &l->level ); + } + else if( !Q_stricmp( t, "name" ) ) + { + admin_readconfig_string( &cnf, l->name, sizeof( l->name ) ); + } + else if( !Q_stricmp( t, "flags" ) ) + { + admin_readconfig_string( &cnf, l->flags, sizeof( l->flags ) ); + } + else + { + ADMP( va( "^3!readconfig: ^7[level] parse error near %s on line %d\n", + t, + COM_GetCurrentParseLine() ) ); + } + } + else if( admin_open ) + { + if( !Q_stricmp( t, "name" ) ) + { + admin_readconfig_string( &cnf, a->name, sizeof( a->name ) ); + } + else if( !Q_stricmp( t, "guid" ) ) + { + admin_readconfig_string( &cnf, a->guid, sizeof( a->guid ) ); + } + else if( !Q_stricmp( t, "level" ) ) + { + admin_readconfig_int( &cnf, &a->level ); + } + else if( !Q_stricmp( t, "flags" ) ) + { + admin_readconfig_string( &cnf, a->flags, sizeof( a->flags ) ); + } + else if( !Q_stricmp( t, "seen" ) ) + { + admin_readconfig_int( &cnf, &a->seen ); + } + else + { + ADMP( va( "^3!readconfig: ^7[admin] parse error near %s on line %d\n", + t, + COM_GetCurrentParseLine() ) ); + } + + } + else if( ban_open ) + { + if( !Q_stricmp( t, "name" ) ) + { + admin_readconfig_string( &cnf, b->name, sizeof( b->name ) ); + } + else if( !Q_stricmp( t, "guid" ) ) + { + admin_readconfig_string( &cnf, b->guid, sizeof( b->guid ) ); + } + else if( !Q_stricmp( t, "ip" ) ) + { + admin_readconfig_string( &cnf, b->ip, sizeof( b->ip ) ); + } + else if( !Q_stricmp( t, "reason" ) ) + { + admin_readconfig_string( &cnf, b->reason, sizeof( b->reason ) ); + } + else if( !Q_stricmp( t, "made" ) ) + { + admin_readconfig_string( &cnf, b->made, sizeof( b->made ) ); + } + else if( !Q_stricmp( t, "expires" ) ) + { + admin_readconfig_int( &cnf, &b->expires ); + } + else if( !Q_stricmp( t, "suspend" ) ) + { + admin_readconfig_int( &cnf, &b->suspend ); + } + else if( !Q_stricmp( t, "banner" ) ) + { + admin_readconfig_string( &cnf, b->banner, sizeof( b->banner ) ); + } + else if( !Q_stricmp( t, "blevel" ) ) + { + admin_readconfig_int( &cnf, &b->bannerlevel ); + } + else + { + ADMP( va( "^3!readconfig: ^7[ban] parse error near %s on line %d\n", + t, + COM_GetCurrentParseLine() ) ); + } + } + else if( command_open ) + { + if( !Q_stricmp( t, "command" ) ) + { + admin_readconfig_string( &cnf, c->command, sizeof( c->command ) ); + } + else if( !Q_stricmp( t, "exec" ) ) + { + admin_readconfig_string( &cnf, c->exec, sizeof( c->exec ) ); + } + else if( !Q_stricmp( t, "desc" ) ) + { + admin_readconfig_string( &cnf, c->desc, sizeof( c->desc ) ); + } + else if( !Q_stricmp( t, "flag" ) ) + { + admin_readconfig_string( &cnf, c->flag, sizeof( c->flag ) ); + } + else + { + ADMP( va( "^3!readconfig: ^7[command] parse error near %s on line %d\n", + t, + COM_GetCurrentParseLine() ) ); + } + } + + if( !Q_stricmp( t, "[level]" ) ) + { + if( lc >= MAX_ADMIN_LEVELS ) + return qfalse; + l = G_Alloc( sizeof( g_admin_level_t ) ); + l->level = 0; + *l->name = '\0'; + *l->flags = '\0'; + level_open = qtrue; + } + else if( !Q_stricmp( t, "[admin]" ) ) + { + if( ac >= MAX_ADMIN_ADMINS ) + return qfalse; + a = G_Alloc( sizeof( g_admin_admin_t ) ); + *a->name = '\0'; + *a->guid = '\0'; + a->level = 0; + *a->flags = '\0'; + a->seen = 0; + admin_open = qtrue; + } + else if( !Q_stricmp( t, "[ban]" ) ) + { + if( bc >= MAX_ADMIN_BANS ) + return qfalse; + b = G_Alloc( sizeof( g_admin_ban_t ) ); + *b->name = '\0'; + *b->guid = '\0'; + *b->ip = '\0'; + *b->made = '\0'; + b->expires = 0; + b->suspend = 0; + *b->reason = '\0'; + b->bannerlevel = 0; + ban_open = qtrue; + } + else if( !Q_stricmp( t, "[command]" ) ) + { + if( cc >= MAX_ADMIN_COMMANDS ) + return qfalse; + c = G_Alloc( sizeof( g_admin_command_t ) ); + *c->command = '\0'; + *c->exec = '\0'; + *c->desc = '\0'; + *c->flag = '\0'; + command_open = qtrue; + } + t = COM_Parse( &cnf ); + } + if( level_open ) + { + + g_admin_levels[ lc++ ] = l; + } + if( admin_open ) + g_admin_admins[ ac++ ] = a; + if( ban_open ) + g_admin_bans[ bc++ ] = b; + if( command_open ) + g_admin_commands[ cc++ ] = c; + G_Free( cnf2 ); + ADMP( va( "^3!readconfig: ^7loaded %d levels, %d admins, %d bans, %d commands\n", + lc, ac, bc, cc ) ); + if( lc == 0 ) + admin_default_levels(); + else + { + char n[ MAX_NAME_LENGTH ] = {""}; + + // max printable name length for formatting + for( i = 0; i < MAX_ADMIN_LEVELS && g_admin_levels[ i ]; i++ ) + { + G_DecolorString( l->name, n ); + if( strlen( n ) > admin_level_maxname ) + admin_level_maxname = strlen( n ); + } + } + // reset adminLevel + for( i = 0; i < level.maxclients; i++ ) + if( level.clients[ i ].pers.connected != CON_DISCONNECTED ) + level.clients[ i ].pers.adminLevel = G_admin_level( &g_entities[ i ] ); + return qtrue; +} + +qboolean G_admin_time( gentity_t *ent, int skiparg ) +{ + qtime_t qt; + int t; + + t = trap_RealTime( &qt ); + ADMP( va( "^3!time: ^7local time is %02i:%02i:%02i\n", + qt.tm_hour, qt.tm_min, qt.tm_sec ) ); + + return qtrue; +} + +static int G_admin_find_slot( gentity_t *ent, char *namearg, const char *command ) +{ + char name[ MAX_NAME_LENGTH ]; + char testname[ MAX_NAME_LENGTH ]; + char *guid = NULL; + int matches = 0; + int id = -1; + int i; + qboolean numeric = qtrue; + + G_SanitiseString( namearg, name, sizeof( name ) ); + for( i = 0; i < sizeof( name ) && name[ i ] ; i++ ) + { + if( !isdigit( name[ i ] ) ) + { + numeric = qfalse; + break; + } + } + if( numeric ) + { + id = atoi( name ); + + if( id >= 0 && id < level.maxclients ) + { + gentity_t *vic; + + vic = &g_entities[ id ]; + if( vic && vic->client && vic->client->pers.connected != CON_DISCONNECTED ) + return id; + + ADMP( va( "^3!%s:^7 no one connected by that slot number\n", command ) ); + return -1; + } + if( id >= MAX_CLIENTS && id < MAX_CLIENTS + MAX_ADMIN_ADMINS && + g_admin_admins[ id - MAX_CLIENTS ] ) + { + return id; + } + + ADMP( va( "^3!%s:^7 no match for slot or admin number %d\n", command, id ) ); + return -1; + } + + for( i = 0; i < MAX_ADMIN_ADMINS && g_admin_admins[ i ] && matches < 2; i++ ) + { + G_SanitiseString( g_admin_admins[ i ]->name, testname, sizeof( testname ) ); + if( strstr( testname, name ) ) + { + id = i + MAX_CLIENTS; + guid = g_admin_admins[ i ]->guid; + matches++; + } + } + for( i = 0; i < level.maxclients && matches < 2; i++ ) + { + if( level.clients[ i ].pers.connected == CON_DISCONNECTED ) + continue; + + if( matches && guid && !Q_stricmp( level.clients[ i ].pers.guid, guid ) ) + continue; + + G_SanitiseString( level.clients[ i ].pers.netname, testname, sizeof( testname ) ); + if( strstr( testname, name ) ) + { + id = i; + matches++; + } + } + + if( matches == 0 ) + { + ADMP( va( "^3!%s:^7 no match, use !listplayers or !listadmins to " + "find an appropriate number to use instead of name.\n", command ) ); + return -1; + } + + if( matches == 1 ) + return id; + + ADMP( va( "^3!%s:^7 multiple matches, use the admin number instead:\n", command ) ); + admin_listadmins( ent, 0, name, 0 ); + + return -1; +} + +static int G_admin_find_admin_slot( gentity_t *ent, char *namearg, char *command, char *nick, int nick_len ) +{ + gentity_t *vic; + char *guid; + int id; + int i; + + if ( nick ) + nick[ 0 ] = '\0'; + + id = G_admin_find_slot( ent, namearg, command ); + if( id < 0 ) + return -1; + + if( id < MAX_CLIENTS ) + { + vic = &g_entities[ id ]; + guid = vic->client->pers.guid; + for( i = 0; i < MAX_ADMIN_ADMINS && g_admin_admins[ i ]; i++ ) + { + if( !Q_stricmp( guid, g_admin_admins[ i ]->guid ) ) + { + id = i + MAX_CLIENTS; + if( nick ) + Q_strncpyz( nick, vic->client->pers.netname, nick_len ); + break; + } + } + if( id < MAX_CLIENTS ) + { + ADMP( va( "^3!%s:^7 player is not !registered\n", command ) ); + return -1; + } + } + + id -= MAX_CLIENTS; + if( nick && !nick[ 0 ] ) + Q_strncpyz( nick, g_admin_admins[ id ]->name, nick_len ); + + return id; +} + +qboolean G_admin_setlevel( gentity_t *ent, int skiparg ) +{ + char lstr[ 11 ]; // 10 is max strlen() for 32-bit int + char adminname[ MAX_NAME_LENGTH ] = {""}; + char testname[ MAX_NAME_LENGTH ] = {""}; + char guid[ 33 ]; + int l, i; + gentity_t *vic = NULL; + qboolean updated = qfalse; + g_admin_admin_t *a; + qboolean found = qfalse; + int id = -1; + + if( G_SayArgc() < 3 + skiparg ) + { + ADMP( "^3!setlevel: ^7usage: !setlevel [name|slot#] [level]\n" ); + return qfalse; + } + G_SayArgv( 1 + skiparg, testname, sizeof( testname ) ); + G_SayArgv( 2 + skiparg, lstr, sizeof( lstr ) ); + l = atoi( lstr ); + + if( ent && l > ent->client->pers.adminLevel ) + { + ADMP( "^3!setlevel: ^7you may not use !setlevel to set a level higher " + "than your current level\n" ); + return qfalse; + } + + // if admin is activated for the first time on a running server, we need + // to ensure at least the default levels get created + if( !ent && !g_admin_levels[ 0 ] ) + G_admin_readconfig(NULL, 0); + + for( i = 0; i < MAX_ADMIN_LEVELS && g_admin_levels[ i ]; i++ ) + { + if( g_admin_levels[ i ]->level == l ) + { + found = qtrue; + break; + } + } + if( !found ) + { + ADMP( "^3!setlevel: ^7level is not defined\n" ); + return qfalse; + } + + id = G_admin_find_slot( ent, testname, "setlevel" ); + if( id >=0 && id < MAX_CLIENTS ) + { + vic = &g_entities[ id ]; + Q_strncpyz( adminname, vic->client->pers.netname, sizeof( adminname ) ); + Q_strncpyz( guid, vic->client->pers.guid, sizeof( guid ) ); + } + else if( id >= MAX_CLIENTS && id < MAX_CLIENTS + MAX_ADMIN_ADMINS && + g_admin_admins[ id - MAX_CLIENTS ] ) + { + Q_strncpyz( adminname, g_admin_admins[ id - MAX_CLIENTS ]->name, + sizeof( adminname ) ); + Q_strncpyz( guid, g_admin_admins[ id - MAX_CLIENTS ]->guid, + sizeof( guid ) ); + for( i = 0; i < level.maxclients; i++ ) + { + if( level.clients[ i ].pers.connected == CON_DISCONNECTED ) + continue; + if( !Q_stricmp( level.clients[ i ].pers.guid, guid ) ) + { + vic = &g_entities[ i ]; + Q_strncpyz( adminname, vic->client->pers.netname, sizeof( adminname ) ); + } + } + } + else + { + return qfalse; + } + + if( !Q_stricmp( guid, "XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX" ) ) + { + ADMP( va( "^3!setlevel: ^7%s does not have a valid GUID\n", adminname ) ); + return qfalse; + } + if( ent && !admin_higher_guid( ent->client->pers.guid, guid ) ) + { + ADMP( "^3!setlevel: ^7sorry, but your intended victim has a higher" + " admin level than you\n" ); + return qfalse; + } + + for( i = 0; i < MAX_ADMIN_ADMINS && g_admin_admins[ i ];i++ ) + { + if( !Q_stricmp( g_admin_admins[ i ]->guid, guid ) ) + { + g_admin_admins[ i ]->level = l; + Q_strncpyz( g_admin_admins[ i ]->name, adminname, + sizeof( g_admin_admins[ i ]->name ) ); + updated = qtrue; + } + } + if( !updated ) + { + if( i == MAX_ADMIN_ADMINS ) + { + ADMP( "^3!setlevel: ^7too many admins\n" ); + return qfalse; + } + a = G_Alloc( sizeof( g_admin_admin_t ) ); + a->level = l; + Q_strncpyz( a->name, adminname, sizeof( a->name ) ); + Q_strncpyz( a->guid, guid, sizeof( a->guid ) ); + *a->flags = '\0'; + g_admin_admins[ i ] = a; + } + + AP( va( + "print \"^3!setlevel: ^7%s^7 was given level %d admin rights by %s\n\"", + adminname, l, ( ent ) ? G_admin_adminPrintName( ent ) : "console" ) ); + if( vic ) + { + vic->client->pers.adminLevel = l; + G_admin_set_adminname( vic ); + } + + if( !g_admin.string[ 0 ] ) + ADMP( "^3!setlevel: ^7WARNING g_admin not set, not saving admin record " + "to a file\n" ); + else + admin_writeconfig(); + return qtrue; +} + +static int SortFlags( const void *pa, const void *pb ) +{ + char *a = (char *)pa; + char *b = (char *)pb; + + if( *a == '-' || *a == '+' ) + a++; + if( *b == '-' || *b == '+' ) + b++; + return strcmp(a, b); +} + +#define MAX_USER_FLAGS 200 +const char *G_admin_user_flag( char *oldflags, char *flag, qboolean add, qboolean clear, + char *newflags, int size ) +{ + char *token, *token_p; + char *key; + char head_flags[ MAX_USER_FLAGS ][ MAX_ADMIN_FLAG_LEN ]; + char tail_flags[ MAX_USER_FLAGS ][ MAX_ADMIN_FLAG_LEN ]; + char allflag[ MAX_ADMIN_FLAG_LEN ]; + char newflag[ MAX_ADMIN_FLAG_LEN ]; + int head_count = 0; + int tail_count = 0; + qboolean flagset = qfalse; + int i; + + if( !flag[ 0 ] ) + { + return "invalid admin flag"; + } + + allflag[ 0 ] = '\0'; + token_p = oldflags; + while( *( token = COM_Parse( &token_p ) ) ) + { + key = token; + if( *key == '-' || *key == '+' ) + key++; + + if( !strcmp( key, flag ) ) + { + if( flagset ) + continue; + flagset = qtrue; + if( clear ) + { + // clearing ALLFLAGS will result in clearing any following flags + if( !strcmp( key, ADMF_ALLFLAGS ) ) + break; + else + continue; + } + Com_sprintf( newflag, sizeof( newflag ), "%s%s", + ( add ) ? "+" : "-", key ); + } + else + { + Q_strncpyz( newflag, token, sizeof( newflag ) ); + } + + if( !strcmp( key, ADMF_ALLFLAGS ) ) + { + if( !allflag[ 0 ] ) + Q_strncpyz( allflag, newflag, sizeof( allflag ) ); + continue; + } + + if( !allflag[ 0 ] ) + { + if( head_count < MAX_USER_FLAGS ) + { + Q_strncpyz( head_flags[ head_count ], newflag, + sizeof( head_flags[ head_count ] ) ); + head_count++; + } + } + else + { + if( tail_count < MAX_USER_FLAGS ) + { + Q_strncpyz( tail_flags[ tail_count ], newflag, + sizeof( tail_flags[ tail_count ] ) ); + tail_count++; + } + } + } + + if( !flagset && !clear ) + { + if( !strcmp( flag, ADMF_ALLFLAGS ) ) + { + Com_sprintf( allflag, sizeof( allflag ), "%s%s", + ( add ) ? "" : "-", ADMF_ALLFLAGS ); + } + else if( !allflag[ 0 ] ) + { + if( head_count < MAX_USER_FLAGS ) + { + Com_sprintf( head_flags[ head_count ], sizeof( head_flags[ head_count ] ), + "%s%s", ( add ) ? "" : "-", flag ); + head_count++; + } + } + else + { + if( tail_count < MAX_USER_FLAGS ) + { + Com_sprintf( tail_flags[ tail_count ], sizeof( tail_flags[ tail_count ] ), + "%s%s", ( add ) ? "+" : "-", flag ); + tail_count++; + } + } + } + + qsort( head_flags, head_count, sizeof( head_flags[ 0 ] ), SortFlags ); + qsort( tail_flags, tail_count, sizeof( tail_flags[ 0 ] ), SortFlags ); + + // rebuild flags + newflags[ 0 ] = '\0'; + for( i = 0; i < head_count; i++ ) + { + Q_strcat( newflags, size, + va( "%s%s", ( i ) ? " ": "", head_flags[ i ] ) ); + } + if( allflag[ 0 ] ) + { + Q_strcat( newflags, size, + va( "%s%s", ( newflags[ 0 ] ) ? " ": "", allflag ) ); + + for( i = 0; i < tail_count; i++ ) + { + Q_strcat( newflags, size, + va( " %s", tail_flags[ i ] ) ); + } + } + + return NULL; +} + +typedef struct { + char *flag; + char *description; +} AdminFlagListEntry_t; +static AdminFlagListEntry_t adminFlagList[] = +{ + { ADMF_ACTIVITY, "inactivity rules do not apply" }, + { ADMF_ADMINCHAT, "can see and use admin chat" }, + { ADMF_ALLFLAGS, "has all flags and can use any command" }, + { ADMF_BAN_IMMUNITY, "immune from IP bans" }, + { ADMF_CAN_PERM_BAN, "can permanently ban players" }, + { ADMF_DBUILDER, "permanent designated builder" }, + { ADMF_FORCETEAMCHANGE, "team balance rules do not apply" }, + { ADMF_INCOGNITO, "does not show as admin in !listplayers" }, + { ADMF_IMMUNITY, "cannot be vote kicked or muted" }, + { ADMF_IMMUTABLE, "admin commands cannot be used on them" }, + { ADMF_NOCENSORFLOOD, "no flood protection" }, + { ADMF_NO_VOTE_LIMIT, "vote limitations do not apply" }, + { ADMF_SEESFULLLISTPLAYERS, "sees all info in !listplayers" }, + { ADMF_SPEC_ALLCHAT, "can see team chat as spectator" }, + { ADMF_ADMINSTEALTH, "uses admin stealth" }, + { ADMF_TEAMCHANGEFREE, "keeps credits on team switch" }, + { ADMF_TEAMCHAT_CMD, "can run commands from team chat" }, + { ADMF_UNACCOUNTABLE, "does not need to specify reason for kick/ban" }, + { ADMF_NO_CHAT, "can not talk" }, + { ADMF_NO_VOTE, "can not call votes" } +}; +static int adminNumFlags= sizeof( adminFlagList ) / sizeof( adminFlagList[ 0 ] ); + +#define MAX_LISTCOMMANDS 128 +qboolean G_admin_flaglist( gentity_t *ent, int skiparg ) +{ + qboolean shown[ MAX_LISTCOMMANDS ]; + int i, j; + int count = 0; + + ADMBP_begin(); + + ADMBP( "^3Ability flags:\n" ); + + for( i = 0; i < adminNumFlags; i++ ) + { + ADMBP( va( " %s%-20s ^7%s\n", + ( adminFlagList[ i ].flag[ 0 ] != '.' ) ? "^5" : "^1", + adminFlagList[ i ].flag, + adminFlagList[ i ].description ) ); + } + + ADMBP( "^3Command flags:\n" ); + + memset( shown, 0, sizeof( shown ) ); + for( i = 0; i < adminNumCmds; i++ ) + { + if( i < MAX_LISTCOMMANDS && shown[ i ] ) + continue; + ADMBP( va( " ^5%-20s^7", g_admin_cmds[ i ].flag ) ); + for( j = i; j < adminNumCmds; j++ ) + { + if( !strcmp( g_admin_cmds[ j ].flag, g_admin_cmds[ i ].flag ) ) + { + ADMBP( va( " %s", g_admin_cmds[ j ].keyword ) ); + if( j < MAX_LISTCOMMANDS ) + shown[ j ] = qtrue; + } + } + ADMBP( "\n" ); + count++; + } + + ADMBP( va( "^3!flaglist: ^7listed %d abilities and %d command flags\n", + adminNumFlags, count ) ); + + ADMBP_end(); + + return qtrue; +} + +qboolean G_admin_flag( gentity_t *ent, int skiparg ) +{ + char command[ MAX_ADMIN_CMD_LEN ], *cmd; + char name[ MAX_NAME_LENGTH ]; + char flagbuf[ MAX_ADMIN_FLAG_LEN ]; + char *flag; + int id; + char adminname[ MAX_NAME_LENGTH ] = {""}; + const char *result; + qboolean add = qtrue; + qboolean clear = qfalse; + int admin_level = -1; + int i, level; + + G_SayArgv( skiparg, command, sizeof( command ) ); + cmd = command; + if( *cmd == '!' ) + cmd++; + + if( G_SayArgc() < 2 + skiparg ) + { + ADMP( va( "^3!%s: ^7usage: !%s slot# flag\n", cmd, cmd ) ); + return qfalse; + } + + G_SayArgv( 1 + skiparg, name, sizeof( name ) ); + if( name[ 0 ] == '*' ) + { + if( ent ) + { + ADMP( va( "^3!%s: only console can change admin level flags\n", cmd ) ); + return qfalse; + } + id = atoi( name + 1 ); + for( i = 0; i < MAX_ADMIN_LEVELS && g_admin_levels[ i ]; i++ ) + { + if( g_admin_levels[ i ]->level == id ) + { + admin_level = i; + break; + } + } + if( admin_level < 0 ) + { + ADMP( va( "^3!%s: admin level %d does not exist\n", cmd, id ) ); + return qfalse; + } + Com_sprintf( adminname, sizeof( adminname ), "admin level %d", id ); + } + else + { + id = G_admin_find_admin_slot( ent, name, cmd, adminname, sizeof( adminname ) ); + if( id < 0 ) + return qfalse; + + if( ent && !admin_higher_guid( ent->client->pers.guid, g_admin_admins[ id ]->guid ) ) + { + ADMP( va( "^3%s:^7 your intended victim has a higher admin level than you\n", cmd ) ); + return qfalse; + } + } + + if( G_SayArgc() < 3 + skiparg ) + { + flag = ""; + level = 0; + if( admin_level < 0 ) + { + for( i = 0; i < MAX_ADMIN_LEVELS && g_admin_levels[ i ]; i++ ) + { + if( g_admin_admins[ id ]->level == g_admin_levels[ i ]->level ) + { + flag = g_admin_levels[ i ]->flags; + level = g_admin_admins[ id ]->level; + break; + } + } + ADMP( va( "^3%s:^7 flags for %s^7 are '^3%s^7'\n", + cmd, adminname, g_admin_admins[ id ]->flags) ); + } + else + { + flag = g_admin_levels[ admin_level ]->flags; + level = g_admin_levels[ admin_level ]->level; + } + ADMP( va( "^3%s:^7 level %d flags are '%s'\n", + cmd, level, flag ) ); + + return qtrue; + } + + G_SayArgv( 2 + skiparg, flagbuf, sizeof( flagbuf ) ); + flag = flagbuf; + if( flag[ 0 ] == '-' || flag[ 0 ] == '+' ) + { + add = ( flag[ 0 ] == '+' ); + flag++; + } + if( ent && !Q_stricmp( ent->client->pers.guid, g_admin_admins[ id ]->guid ) ) + { + ADMP( va( "^3%s:^7 you may not change your own flags (use rcon)\n", cmd ) ); + return qfalse; + } + if( flag[ 0 ] != '.' && !G_admin_permission( ent, flag ) ) + { + ADMP( va( "^3%s:^7 you can only change flags that you also have\n", cmd ) ); + return qfalse; + } + + if( !Q_stricmp( cmd, "unflag" ) ) + { + clear = qtrue; + } + + if( admin_level < 0 ) + { + result = G_admin_user_flag( g_admin_admins[ id ]->flags, flag, add, clear, + g_admin_admins[ id ]->flags, sizeof( g_admin_admins[ id ]->flags ) ); + } + else + { + result = G_admin_user_flag( g_admin_levels[ admin_level ]->flags, flag, add, clear, + g_admin_levels[ admin_level ]->flags, + sizeof( g_admin_levels[ admin_level ]->flags ) ); + } + if( result ) + { + ADMP( va( "^3!flag: ^7an error occured setting flag '^3%s^7', %s\n", + flag, result ) ); + return qfalse; + } + + if( !Q_stricmp( cmd, "flag" ) ) + { + G_AdminsPrintf( "^3!%s: ^7%s^7 was %s admin flag '%s' by %s\n", + cmd, adminname, + ( add ) ? "given" : "denied", + flag, + ( ent ) ? ent->client->pers.netname : "console" ); + } + else + { + G_AdminsPrintf( "^3!%s: ^7admin flag '%s' for %s^7 cleared by %s\n", + cmd, flag, adminname, + ( ent ) ? ent->client->pers.netname : "console" ); + } + + if( !g_admin.string[ 0 ] ) + ADMP( va( "^3!%s: ^7WARNING g_admin not set, not saving admin record " + "to a file\n", cmd ) ); + else + admin_writeconfig(); + + return qtrue; +} + +int G_admin_parse_time( const char *time ) +{ + int seconds = 0, num = 0; + int i; + for( i = 0; time[ i ]; i++ ) + { + if( isdigit( time[ i ] ) ) + { + num = num * 10 + time[ i ] - '0'; + continue; + } + if( i == 0 || !isdigit( time[ i - 1 ] ) ) + return -1; + switch( time[ i ] ) + { + case 'w': num *= 7; + case 'd': num *= 24; + case 'h': num *= 60; + case 'm': num *= 60; + case 's': break; + default: return -1; + } + seconds += num; + num = 0; + } + if( num ) + seconds += num; + // overflow + if( seconds < 0 ) + seconds = 0; + return seconds; +} + +static qboolean admin_create_ban( gentity_t *ent, + char *netname, + char *guid, + char *ip, + int seconds, + char *reason ) +{ + g_admin_ban_t *b = NULL; + qtime_t qt; + int t; + int i; + + t = trap_RealTime( &qt ); + b = G_Alloc( sizeof( g_admin_ban_t ) ); + + if( !b ) + return qfalse; + + Q_strncpyz( b->name, netname, sizeof( b->name ) ); + Q_strncpyz( b->guid, guid, sizeof( b->guid ) ); + Q_strncpyz( b->ip, ip, sizeof( b->ip ) ); + b->suspend = 0; + + //strftime( b->made, sizeof( b->made ), "%m/%d/%y %H:%M:%S", lt ); + Q_strncpyz( b->made, va( "%02i/%02i/%02i %02i:%02i:%02i", + (qt.tm_mon + 1), qt.tm_mday, (qt.tm_year - 100), + qt.tm_hour, qt.tm_min, qt.tm_sec ), + sizeof( b->made ) ); + + Q_strncpyz( b->banner, G_admin_get_adminname( ent ), sizeof( b->banner ) ); + + if( ent ) + b->bannerlevel = G_admin_level( ent ); + else + b->bannerlevel = 0; + + if( !seconds ) + b->expires = 0; + else + b->expires = t + seconds; + if( !*reason ) + Q_strncpyz( b->reason, "banned by admin", sizeof( b->reason ) ); + else + Q_strncpyz( b->reason, reason, sizeof( b->reason ) ); + for( i = 0; i < MAX_ADMIN_BANS && g_admin_bans[ i ]; i++ ) + ; + if( i == MAX_ADMIN_BANS ) + { + ADMP( "^3!ban: ^7too many bans\n" ); + G_Free( b ); + return qfalse; + } + g_admin_bans[ i ] = b; + return qtrue; +} + +qboolean G_admin_kick( gentity_t *ent, int skiparg ) +{ + int pids[ MAX_CLIENTS ]; + char name[ MAX_NAME_LENGTH ], *reason, err[ MAX_STRING_CHARS ]; + int minargc; + gentity_t *vic; + char notice[51]; + + trap_Cvar_VariableStringBuffer( "g_banNotice", notice, sizeof( notice ) ); + + minargc = 3 + skiparg; + if( G_admin_permission( ent, ADMF_UNACCOUNTABLE ) ) + minargc = 2 + skiparg; + + if( G_SayArgc() < minargc ) + { + ADMP( "^3!kick: ^7usage: !kick [name] [reason]\n" ); + return qfalse; + } + G_SayArgv( 1 + skiparg, name, sizeof( name ) ); + reason = G_SayConcatArgs( 2 + skiparg ); + if( G_ClientNumbersFromString( name, pids ) != 1 ) + { + G_MatchOnePlayer( pids, err, sizeof( err ) ); + ADMP( va( "^3!kick: ^7%s\n", err ) ); + return qfalse; + } + if( !admin_higher( ent, &g_entities[ pids[ 0 ] ] ) ) + { + ADMP( "^3!kick: ^7sorry, but your intended victim has a higher admin" + " level than you\n" ); + return qfalse; + } + vic = &g_entities[ pids[ 0 ] ]; + admin_create_ban( ent, + vic->client->pers.netname, + vic->client->pers.guid, + vic->client->pers.ip, G_admin_parse_time( g_adminTempBan.string ), + ( *reason ) ? reason : "kicked by admin" ); + if( g_admin.string[ 0 ] ) + admin_writeconfig(); + + trap_SendServerCommand( pids[ 0 ], + va( "disconnect \"You have been kicked.\n%s^7\nreason:\n%s\n%s\"", + ( ent ) ? va( "admin:\n%s", G_admin_adminPrintName( ent ) ) : "admin\nconsole", + ( *reason ) ? reason : "kicked by admin", notice ) ); + + trap_DropClient( pids[ 0 ], va( "kicked%s^7, reason: %s", + ( ent ) ? va( " by %s", G_admin_adminPrintName( ent ) ) : " by console", + ( *reason ) ? reason : "kicked by admin" ) ); + + return qtrue; +} + +qboolean G_admin_ban( gentity_t *ent, int skiparg ) +{ + int seconds; + char search[ MAX_NAME_LENGTH ]; + char secs[ 7 ]; + char *reason; + int minargc; + char duration[ 32 ]; + int logmatch = -1, logmatches = 0; + int i, j; + qboolean exactmatch = qfalse; + char n2[ MAX_NAME_LENGTH ]; + char s2[ MAX_NAME_LENGTH ]; + char guid_stub[ 9 ]; + char notice[51]; + + trap_Cvar_VariableStringBuffer( "g_banNotice", notice, sizeof( notice ) ); + + if( G_admin_permission( ent, ADMF_CAN_PERM_BAN ) && + G_admin_permission( ent, ADMF_UNACCOUNTABLE ) ) + { + minargc = 2 + skiparg; + } + else if( ( G_admin_permission( ent, ADMF_CAN_PERM_BAN ) || g_adminMaxBan.integer ) || + G_admin_permission( ent, ADMF_UNACCOUNTABLE ) ) + { + minargc = 3 + skiparg; + } + else + { + minargc = 4 + skiparg; + } + if( G_SayArgc() < minargc ) + { + ADMP( "^3!ban: ^7usage: !ban [name|slot|ip] [time] [reason]\n" ); + return qfalse; + } + G_SayArgv( 1 + skiparg, search, sizeof( search ) ); + G_SanitiseString( search, s2, sizeof( s2 ) ); + G_SayArgv( 2 + skiparg, secs, sizeof( secs ) ); + + seconds = G_admin_parse_time( secs ); + if( seconds <= 0 ) + { + if( g_adminMaxBan.integer && !G_admin_permission( ent, ADMF_CAN_PERM_BAN ) ) + { + ADMP( va( "^3!ban: ^7using your admin level's maximum ban length of %s\n", + g_adminMaxBan.string ) ); + seconds = G_admin_parse_time( g_adminMaxBan.string ); + } + else if( G_admin_permission( ent, ADMF_CAN_PERM_BAN ) ) + { + seconds = 0; + } + else + { + ADMP( "^3!ban: ^7ban time must be positive\n" ); + return qfalse; + } + reason = G_SayConcatArgs( 2 + skiparg ); + } + else + { + reason = G_SayConcatArgs( 3 + skiparg ); + + if( g_adminMaxBan.integer && + seconds > G_admin_parse_time( g_adminMaxBan.string ) && + !G_admin_permission( ent, ADMF_CAN_PERM_BAN ) ) + { + seconds = G_admin_parse_time( g_adminMaxBan.string ); + ADMP( va( "^3!ban: ^7ban length limited to %s for your admin level\n", + g_adminMaxBan.string ) ); + } + } + + for( i = 0; i < MAX_ADMIN_NAMELOGS && g_admin_namelog[ i ]; i++ ) + { + // skip players in the namelog who have already been banned + if( g_admin_namelog[ i ]->banned ) + continue; + + // skip disconnected players when banning on slot number + if( g_admin_namelog[ i ]->slot == -1 ) + continue; + + if( !Q_stricmp( va( "%d", g_admin_namelog[ i ]->slot ), s2 ) ) + { + logmatches = 1; + logmatch = i; + exactmatch = qtrue; + break; + } + } + + for( i = 0; + !exactmatch && i < MAX_ADMIN_NAMELOGS && g_admin_namelog[ i ]; + i++ ) + { + // skip players in the namelog who have already been banned + if( g_admin_namelog[ i ]->banned ) + continue; + + if( !Q_stricmp( g_admin_namelog[ i ]->ip, s2 ) ) + { + logmatches = 1; + logmatch = i; + exactmatch = qtrue; + break; + } + for( j = 0; j < MAX_ADMIN_NAMELOG_NAMES + && g_admin_namelog[ i ]->name[ j ][ 0 ]; j++ ) + { + G_SanitiseString(g_admin_namelog[ i ]->name[ j ], n2, sizeof( n2 ) ); + if( strstr( n2, s2 ) ) + { + if( logmatch != i ) + logmatches++; + logmatch = i; + } + } + } + + if( !logmatches ) + { + ADMP( "^3!ban: ^7no player found by that name, IP, or slot number\n" ); + return qfalse; + } + else if( logmatches > 1 ) + { + ADMBP_begin(); + ADMBP( "^3!ban: ^7multiple recent clients match name, use IP or slot#:\n" ); + for( i = 0; i < MAX_ADMIN_NAMELOGS && g_admin_namelog[ i ]; i++ ) + { + for( j = 0; j < 8; j++ ) + guid_stub[ j ] = g_admin_namelog[ i ]->guid[ j + 24 ]; + guid_stub[ j ] = '\0'; + for( j = 0; j < MAX_ADMIN_NAMELOG_NAMES + && g_admin_namelog[ i ]->name[ j ][ 0 ]; j++ ) + { + G_SanitiseString(g_admin_namelog[ i ]->name[ j ], n2, sizeof( n2 ) ); + if( strstr( n2, s2 ) ) + { + if( g_admin_namelog[ i ]->slot > -1 ) + ADMBP( "^3" ); + ADMBP( va( "%-2s (*%s) %15s ^7'%s^7'\n", + (g_admin_namelog[ i ]->slot > -1) ? + va( "%d", g_admin_namelog[ i ]->slot ) : "-", + guid_stub, + g_admin_namelog[ i ]->ip, + g_admin_namelog[ i ]->name[ j ] ) ); + } + } + } + ADMBP_end(); + return qfalse; + } + + G_admin_duration( ( seconds ) ? seconds : -1, + duration, sizeof( duration ) ); + + if( ent && !admin_higher_guid( ent->client->pers.guid, + g_admin_namelog[ logmatch ]->guid ) ) + { + + ADMP( "^3!ban: ^7sorry, but your intended victim has a higher admin" + " level than you\n" ); + return qfalse; + } + + admin_create_ban( ent, + g_admin_namelog[ logmatch ]->name[ 0 ], + g_admin_namelog[ logmatch ]->guid, + g_admin_namelog[ logmatch ]->ip, + seconds, reason ); + + g_admin_namelog[ logmatch ]->banned = qtrue; + + if( !g_admin.string[ 0 ] ) + ADMP( "^3!ban: ^7WARNING g_admin not set, not saving ban to a file\n" ); + else + admin_writeconfig(); + + if( g_admin_namelog[ logmatch ]->slot == -1 ) + { + // client is already disconnected so stop here + AP( va( "print \"^3!ban:^7 %s^7 has been banned by %s^7 " + "duration: %s, reason: %s\n\"", + g_admin_namelog[ logmatch ]->name[ 0 ], + ( ent ) ? G_admin_adminPrintName( ent ) : "console", + duration, + ( *reason ) ? reason : "banned by admin" ) ); + return qtrue; + } + + trap_SendServerCommand( g_admin_namelog[ logmatch ]->slot, + va( "disconnect \"You have been banned.\n" + "admin:\n%s^7\nduration:\n%s\nreason:\n%s\n%s\"", + ( ent ) ? G_admin_adminPrintName( ent ) : "console", + duration, + ( *reason ) ? reason : "banned by admin", notice ) ); + + trap_DropClient( g_admin_namelog[ logmatch ]->slot, + va( "banned by %s^7, duration: %s, reason: %s", + ( ent ) ? G_admin_adminPrintName( ent ) : "console", + duration, + ( *reason ) ? reason : "banned by admin" ) ); + return qtrue; +} + +qboolean G_admin_adjustban( gentity_t *ent, int skiparg ) +{ + int bnum; + int length; + int expires; + int time = trap_RealTime( NULL ); + char duration[ 32 ] = {""}; + char *reason; + char bs[ 5 ]; + char secs[ MAX_TOKEN_CHARS ]; + char mode = '\0'; + + if( G_SayArgc() < 3 + skiparg ) + { + ADMP( "^3!adjustban: ^7usage: !adjustban [ban#] [time] [reason]\n" ); + return qfalse; + } + G_SayArgv( 1 + skiparg, bs, sizeof( bs ) ); + bnum = atoi( bs ); + if( bnum < 1 || bnum > MAX_ADMIN_BANS || !g_admin_bans[ bnum - 1] ) + { + ADMP( "^3!adjustban: ^7invalid ban#\n" ); + return qfalse; + } + if( g_admin_bans[ bnum - 1 ]->expires == 0 && + !G_admin_permission( ent, ADMF_CAN_PERM_BAN ) ) + { + ADMP( "^3!adjustban: ^7you cannot modify permanent bans\n" ); + return qfalse; + } + if( g_admin_bans[ bnum - 1 ]->bannerlevel > G_admin_level( ent ) ) + { + ADMP( "^3!adjustban: ^7you cannot modify Bans made by admins higher than you\n" ); + return qfalse; + } + if( g_adminMaxBan.integer && + !G_admin_permission( ent, ADMF_CAN_PERM_BAN ) && + g_admin_bans[ bnum - 1 ]->expires - time > G_admin_parse_time( g_adminMaxBan.string ) ) + { + ADMP( va( "^3!adjustban: ^7your admin level cannot modify bans longer than %s\n", + g_adminMaxBan.string ) ); + return qfalse; + } + G_SayArgv( 2 + skiparg, secs, sizeof( secs ) ); + if( secs[ 0 ] == '+' || secs[ 0 ] == '-' ) + mode = secs[ 0 ]; + length = G_admin_parse_time( &secs[ mode ? 1 : 0 ] ); + if( length < 0 ) + skiparg--; + else + { + if( length ) + { + if( g_admin_bans[ bnum - 1 ]->expires == 0 && mode ) + { + ADMP( "^3!adjustban: ^7new duration must be explicit\n" ); + return qfalse; + } + if( mode == '+' ) + expires = g_admin_bans[ bnum - 1 ]->expires + length; + else if( mode == '-' ) + expires = g_admin_bans[ bnum - 1 ]->expires - length; + else + expires = time + length; + if( expires <= time ) + { + ADMP( "^3!adjustban: ^7ban time must be positive\n" ); + return qfalse; + } + if( g_adminMaxBan.integer && + !G_admin_permission( ent, ADMF_CAN_PERM_BAN ) && + expires - time > G_admin_parse_time( g_adminMaxBan.string ) ) + { + ADMP( va( "^3!adjustban: ^7ban length is limited to %s for your admin level\n", + g_adminMaxBan.string ) ); + length = G_admin_parse_time( g_adminMaxBan.string ); + expires = time + length; + } + } + else if( G_admin_permission( ent, ADMF_CAN_PERM_BAN ) ) + expires = 0; + else + { + ADMP( "^3!adjustban: ^7ban time must be positive\n" ); + return qfalse; + } + + g_admin_bans[ bnum - 1 ]->expires = expires; + G_admin_duration( ( expires ) ? expires - time : -1, + duration, sizeof( duration ) ); + } + reason = G_SayConcatArgs( 3 + skiparg ); + if( *reason ) + Q_strncpyz( g_admin_bans[ bnum - 1 ]->reason, reason, + sizeof( g_admin_bans[ bnum - 1 ]->reason ) ); + AP( va( "print \"^3!adjustban: ^7ban #%d for %s^7 has been updated by %s^7 " + "%s%s%s%s%s\n\"", + bnum, + g_admin_bans[ bnum - 1 ]->name, + ( ent ) ? G_admin_adminPrintName( ent ) : "console", + ( length >= 0 ) ? "duration: " : "", + duration, + ( length >= 0 && *reason ) ? ", " : "", + ( *reason ) ? "reason: " : "", + reason ) ); + if( ent ) { + Q_strncpyz( g_admin_bans[ bnum - 1 ]->banner, G_admin_get_adminname( ent ), + sizeof( g_admin_bans[ bnum - 1 ]->banner ) ); + g_admin_bans[ bnum - 1 ]->bannerlevel = G_admin_level( ent ); + } + + if( g_admin.string[ 0 ] ) + admin_writeconfig(); + return qtrue; +} + + +qboolean G_admin_subnetban( gentity_t *ent, int skiparg ) +{ + int bnum; + int mask; + unsigned int IPRlow = 0, IPRhigh = 0; + char cIPRlow[ 32 ], cIPRhigh[ 32 ]; + char bs[ 5 ]; + char strmask[ 5 ]; + char exl[2]; + int k, IP[5]; + + if( G_SayArgc() < 3 + skiparg ) + { + ADMP( "^3!subnetban: ^7usage: !subnetban [ban#] [mask]\n" ); + return qfalse; + } + G_SayArgv( 1 + skiparg, bs, sizeof( bs ) ); + bnum = atoi( bs ); + if( bnum < 1 || bnum > MAX_ADMIN_BANS || !g_admin_bans[ bnum - 1] ) + { + ADMP( "^3!subnetban: ^7invalid ban#\n" ); + return qfalse; + } + if( g_admin_bans[ bnum - 1 ]->bannerlevel > G_admin_level( ent ) ) + { + ADMP( "^3!subnetban: ^7you cannot subnetban Bans on bans made by admins higher than you\n" ); + return qfalse; + } + + G_SayArgv( 2 + skiparg, strmask, sizeof( strmask ) ); + mask = atoi( strmask ); + + if( mask >= 0 && mask <= 32) + { + G_SayArgv( 3 + skiparg, exl, sizeof( exl ) ); + if( mask >= 0 && mask < 16 ) + { + if( ent ) + { + ADMP( "^3!subnetban: ^7Only console may ban such a large network. Regular admins may only ban >=16.\n" ); + return qfalse; + } + if( strcmp(exl, "!") ) + { + ADMP( "^3!subnetban: ^1WARNING:^7 you are about to ban a large network, use !subnetban [ban] [mask] ! to force^7\n" ); + return qfalse; + } + } + + memset( IP, 0, sizeof( IP ) ); + sscanf(g_admin_bans[ bnum - 1 ]->ip, "%d.%d.%d.%d/%d", &IP[4], &IP[3], &IP[2], &IP[1], &IP[0]); + for(k = 4; k >= 1; k--) + { + if( IP[k] ) + IPRlow |= IP[k] << 8*(k-1); + } + IPRhigh = IPRlow; + if( mask == 32 ) + { + Q_strncpyz( + g_admin_bans[ bnum - 1 ]->ip, + va("%i.%i.%i.%i", IP[4], IP[3], IP[2], IP[1]), + sizeof( g_admin_bans[ bnum - 1 ]->ip ) + ); + } + else + { + Q_strncpyz( + g_admin_bans[ bnum - 1 ]->ip, + va("%i.%i.%i.%i/%i", IP[4], IP[3], IP[2], IP[1], mask ), + sizeof( g_admin_bans[ bnum - 1 ]->ip ) + ); + IPRlow &= ~((1 << (32-mask)) - 1); + IPRhigh |= ((1 << (32-mask)) - 1); + } + } + else + { + ADMP( "^3!subnetban: ^7mask is out of range, please use 0-32 inclusive\n" ); + return qfalse; + } + if( mask > 0 ) + { + Q_strncpyz( + cIPRlow, + va("%u.%u.%u.%u", (IPRlow & (255 << 24)) >> 24, (IPRlow & (255 << 16)) >> 16, (IPRlow & (255 << 8)) >> 8, IPRlow & 255), + sizeof( cIPRlow ) + ); + Q_strncpyz( + cIPRhigh, + va("%u.%u.%u.%u", (IPRhigh & (255 << 24)) >> 24, (IPRhigh & (255 << 16)) >> 16, (IPRhigh & (255 << 8)) >> 8, IPRhigh & 255), + sizeof( cIPRhigh ) + ); + } + else + { + Q_strncpyz( cIPRlow, "0.0.0.0", sizeof( cIPRlow ) ); + Q_strncpyz( cIPRhigh, "255.255.255.255", sizeof( cIPRhigh ) ); + + } + + AP( va( "print \"^3!subnetban: ^7ban #%d for %s^7 has been updated by %s^7 " + "%s (%s - %s)\n\"", + bnum, + g_admin_bans[ bnum - 1 ]->name, + ( ent ) ? G_admin_adminPrintName( ent ) : "console", + g_admin_bans[ bnum - 1 ]->ip, + cIPRlow, + cIPRhigh) ); + if( ent ) + Q_strncpyz( g_admin_bans[ bnum - 1 ]->banner, G_admin_get_adminname( ent ), + sizeof( g_admin_bans[ bnum - 1 ]->banner ) ); + if( g_admin.string[ 0 ] ) + admin_writeconfig(); + return qtrue; +} + +qboolean G_admin_suspendban( gentity_t *ent, int skiparg ) +{ + int bnum; + int length; + int timenow = 0; + int expires = 0; + char *arg; + char bs[ 5 ]; + char duration[ 32 ]; + qtime_t qt; + + if( G_SayArgc() < 3 + skiparg ) + { + ADMP( "^3!suspendban: ^7usage: !suspendban [ban #] [length]\n" ); + return qfalse; + } + G_SayArgv( 1 + skiparg, bs, sizeof( bs ) ); + bnum = atoi( bs ); + if( bnum < 1 || !g_admin_bans[ bnum - 1] ) + { + ADMP( "^3!suspendban: ^7invalid ban #\n" ); + return qfalse; + } + if( g_admin_bans[ bnum - 1 ]->bannerlevel > G_admin_level( ent ) ) + { + ADMP( "^3!suspendban: ^7you cannot suspend Bans made by admins higher than you\n" ); + return qfalse; + } + + arg = G_SayConcatArgs( 2 + skiparg ); + timenow = trap_RealTime( &qt ); + length = G_admin_parse_time( arg ); + + if( length < 0 ) + { + ADMP( "^3!suspendban: ^7invalid length\n" ); + return qfalse; + } + if( length > MAX_ADMIN_BANSUSPEND_DAYS * 24 * 60 * 60 ) + { + length = MAX_ADMIN_BANSUSPEND_DAYS * 24 * 60 * 60; + ADMP( va( "^3!suspendban: ^7maximum ban suspension is %d days\n", + MAX_ADMIN_BANSUSPEND_DAYS ) ); + } else if( g_admin_bans[ bnum - 1 ]->expires > 0 && length + timenow > g_admin_bans[ bnum - 1 ]->expires ) { + length = g_admin_bans[ bnum - 1 ]->expires - timenow; + G_admin_duration( length , duration, sizeof( duration ) ); + ADMP( va( "^3!suspendban: ^7Suspension Duration trimmed to Ban duration: %s\n", + duration ) ); + } + + if ( length > 0 ) + { + expires = timenow + length; + } + if( g_admin_bans[ bnum - 1 ]->suspend == expires ) + { + ADMP( "^3!suspendban: ^7no change\n" ); + return qfalse; + } + + g_admin_bans[ bnum - 1 ]->suspend = expires; + if ( length > 0 ) + { + G_admin_duration( length , duration, sizeof( duration ) ); + AP( va( "print \"^3!suspendban: ^7ban #%d suspended for %s\n\"", + bnum, duration ) ); + } + else + { + AP( va( "print \"^3!suspendban: ^7ban #%d suspension removed\n\"", + bnum ) ); + } + + if( !g_admin.string[ 0 ] ) + ADMP( "^3!adjustban: ^7WARNING g_admin not set, not saving ban to a file\n" ); + else + admin_writeconfig(); + return qtrue; +} + +qboolean G_admin_unban( gentity_t *ent, int skiparg ) +{ + int bnum; + char bs[ 5 ]; + int t; + + t = trap_RealTime( NULL ); + if( G_SayArgc() < 2 + skiparg ) + { + ADMP( "^3!unban: ^7usage: !unban [ban#]\n" ); + return qfalse; + } + G_SayArgv( 1 + skiparg, bs, sizeof( bs ) ); + bnum = atoi( bs ); + if( bnum < 1 || bnum > MAX_ADMIN_BANS || !g_admin_bans[ bnum - 1 ] ) + { + ADMP( "^3!unban: ^7invalid ban#\n" ); + return qfalse; + } + if( g_admin_bans[ bnum - 1 ]->expires == 0 && + !G_admin_permission( ent, ADMF_CAN_PERM_BAN ) ) + { + ADMP( "^3!unban: ^7you cannot remove permanent bans\n" ); + return qfalse; + } + if( g_admin_bans[ bnum - 1 ]->bannerlevel > G_admin_level( ent ) ) + { + ADMP( "^3!unban: ^7you cannot Remove Bans made by admins higher than you\n" ); + return qfalse; + } + if( g_adminMaxBan.integer && + !G_admin_permission( ent, ADMF_CAN_PERM_BAN ) && + g_admin_bans[ bnum - 1 ]->expires - t > G_admin_parse_time( g_adminMaxBan.string ) ) + { + ADMP( va( "^3!unban: ^7your admin level cannot remove bans longer than %s\n", + g_adminMaxBan.string ) ); + return qfalse; + } + g_admin_bans[ bnum -1 ]->expires = t; + AP( va( "print \"^3!unban: ^7ban #%d for %s^7 has been removed by %s\n\"", + bnum, + g_admin_bans[ bnum - 1 ]->name, + ( ent ) ? G_admin_adminPrintName( ent ) : "console" ) ); + if( g_admin.string[ 0 ] ) + admin_writeconfig(); + return qtrue; +} + +qboolean G_admin_putteam( gentity_t *ent, int skiparg ) +{ + int pids[ MAX_CLIENTS ]; + char name[ MAX_NAME_LENGTH ], team[ 7 ], err[ MAX_STRING_CHARS ]; + gentity_t *vic; + pTeam_t teamnum = PTE_NONE; + char teamdesc[ 32 ] = {"spectators"}; + char secs[ 7 ]; + int seconds = 0; + qboolean useDuration = qfalse; + + G_SayArgv( 1 + skiparg, name, sizeof( name ) ); + G_SayArgv( 2 + skiparg, team, sizeof( team ) ); + if( G_SayArgc() < 3 + skiparg ) + { + ADMP( "^3!putteam: ^7usage: !putteam [name] [h|a|s] (duration)\n" ); + return qfalse; + } + + if( G_ClientNumbersFromString( name, pids ) != 1 ) + { + G_MatchOnePlayer( pids, err, sizeof( err ) ); + ADMP( va( "^3!putteam: ^7%s\n", err ) ); + return qfalse; + } + if( !admin_higher( ent, &g_entities[ pids[ 0 ] ] ) ) + { + ADMP( "^3!putteam: ^7sorry, but your intended victim has a higher " + " admin level than you\n" ); + return qfalse; + } + vic = &g_entities[ pids[ 0 ] ]; + + if ( vic->client->sess.invisible == qtrue ) + { + ADMP( "^3!putteam: ^7invisible players cannot join a team\n" ); + return qfalse; + } + + switch( team[ 0 ] ) + { + case 'a': + teamnum = PTE_ALIENS; + Q_strncpyz( teamdesc, "aliens", sizeof( teamdesc ) ); + break; + case 'h': + teamnum = PTE_HUMANS; + Q_strncpyz( teamdesc, "humans", sizeof( teamdesc ) ); + break; + case 's': + teamnum = PTE_NONE; + break; + default: + ADMP( va( "^3!putteam: ^7unknown team %c\n", team[ 0 ] ) ); + return qfalse; + } + //duration code + if( G_SayArgc() > 3 + skiparg ) { + //can only lock players in spectator + if ( teamnum != PTE_NONE ) + { + ADMP( "^3!putteam: ^7You can only lock a player into the spectators team\n" ); + return qfalse; + } + G_SayArgv( 3 + skiparg, secs, sizeof( secs ) ); + seconds = G_admin_parse_time( secs ); + useDuration = qtrue; + } + + if( vic->client->pers.teamSelection == teamnum && teamnum != PTE_NONE ) + { + ADMP( va( "^3!putteam: ^7%s ^7is already on the %s team\n", vic->client->pers.netname, teamdesc ) ); + return qfalse; + } + + if( useDuration == qtrue && seconds > 0 ) { + vic->client->pers.specExpires = level.time + ( seconds * 1000 ); + } + G_ChangeTeam( vic, teamnum ); + + AP( va( "print \"^3!putteam: ^7%s^7 put %s^7 on to the %s team%s\n\"", + ( ent ) ? G_admin_adminPrintName( ent ) : "console", + vic->client->pers.netname, teamdesc, + ( seconds ) ? va( " for %i seconds", seconds ) : "" ) ); + return qtrue; +} + +qboolean G_admin_seen(gentity_t *ent, int skiparg ) +{ + char name[ MAX_NAME_LENGTH ]; + char search[ MAX_NAME_LENGTH ]; + char sduration[ 32 ]; + qboolean numeric = qtrue; + int i, j; + int id = -1; + int count = 0; + int t; + qtime_t qt; + gentity_t *vic; + qboolean ison; + + if( G_SayArgc() < 2 + skiparg ) + { + ADMP( "^3!seen: ^7usage: !seen [name|admin#]\n" ); + return qfalse; + } + G_SayArgv( 1 + skiparg, name, sizeof( name ) ); + G_SanitiseString( name, search, sizeof( search ) ); + for( i = 0; i < sizeof( search ) && search[ i ] ; i++ ) + { + if( search[ i ] < '0' || search[ i ] > '9' ) + { + numeric = qfalse; + break; + } + } + + if( numeric ) + { + id = atoi( name ); + search[ 0 ] = '\0'; + } + + ADMBP_begin(); + t = trap_RealTime( &qt ); + + for( i = 0; i < level.maxclients && count < 10; i ++ ) + { + vic = &g_entities[ i ]; + + if( !vic->client || vic->client->pers.connected != CON_CONNECTED ) + continue; + + G_SanitiseString( vic->client->pers.netname, name, sizeof( name ) ); + + if( i == id || (search[ 0 ] && strstr( name, search ) ) ) + { + if ( vic->client->sess.invisible == qfalse ) + { + ADMBP( va( "^3%4d ^7%s^7 is currently playing\n", i, vic->client->pers.netname ) ); + count++; + } + } + } + for( i = 0; i < MAX_ADMIN_ADMINS && g_admin_admins[ i ] && count < 10; i++ ) + { + G_SanitiseString( g_admin_admins[ i ]->name, name, sizeof( name ) ); + if( i + MAX_CLIENTS == id || (search[ 0 ] && strstr( name, search ) ) ) + { + ison = qfalse; + for( j = 0; j < level.maxclients; j++ ) + { + vic = &g_entities[ j ]; + if( !vic->client || vic->client->pers.connected != CON_CONNECTED ) + continue; + G_SanitiseString( vic->client->pers.netname, name, sizeof( name ) ); + if( !Q_stricmp( vic->client->pers.guid, g_admin_admins[ i ]->guid ) + && strstr( name, search ) ) + { + if ( vic->client->sess.invisible == qfalse ) + { + ison = qtrue; + break; + } + } + } + + if( ison ) + { + if( id == -1 ) + continue; + ADMBP( va( "^3%4d ^7%s^7 is currently playing\n", + i + MAX_CLIENTS, g_admin_admins[ i ]->name ) ); + } + else + { + G_admin_duration( t - g_admin_admins[ i ]->seen, + sduration, sizeof( sduration ) ); + ADMBP( va( "%4d %s^7 last seen %s%s\n", + i + MAX_CLIENTS, g_admin_admins[ i ]->name , + ( g_admin_admins[ i ]->seen ) ? sduration : "", + ( g_admin_admins[ i ]->seen ) ? " ago" : "time is unknown" ) ); + } + count++; + } + } + + if( search[ 0 ] ) + ADMBP( va( "^3!seen:^7 found %d player%s matching '%s'\n", + count, (count == 1) ? "" : "s", search ) ); + else if ( !count ) + ADMBP( "^3!seen:^7 no one connectd by that slot number\n" ); + + ADMBP_end(); + return qtrue; +} + +void G_admin_seen_update( char *guid ) +{ + int i; + + for( i = 0; i < MAX_ADMIN_ADMINS && g_admin_admins[ i ] ; i++ ) + { + if( !Q_stricmp( g_admin_admins[ i ]->guid, guid ) ) + { + qtime_t qt; + + g_admin_admins[ i ]->seen = trap_RealTime( &qt ); + return; + } + } +} + +void G_admin_adminlog_cleanup( void ) +{ + int i; + + for( i = 0; i < MAX_ADMIN_ADMINLOGS && g_admin_adminlog[ i ]; i++ ) + { + G_Free( g_admin_adminlog[ i ] ); + g_admin_adminlog[ i ] = NULL; + } + + admin_adminlog_index = 0; +} + +void G_admin_adminlog_log( gentity_t *ent, char *command, char *args, int skiparg, qboolean success ) +{ + g_admin_adminlog_t *adminlog; + int previous; + int count = 1; + int i; + + if( !command ) + return; + + if( !Q_stricmp( command, "adminlog" ) || + !Q_stricmp( command, "admintest" ) || + !Q_stricmp( command, "help" ) || + !Q_stricmp( command, "info" ) || + !Q_stricmp( command, "listadmins" ) || + !Q_stricmp( command, "listplayers" ) || + !Q_stricmp( command, "namelog" ) || + !Q_stricmp( command, "showbans" ) || + !Q_stricmp( command, "seen" ) || + !Q_stricmp( command, "time" ) ) + return; + + previous = admin_adminlog_index - 1; + if( previous < 0 ) + previous = MAX_ADMIN_ADMINLOGS - 1; + + if( g_admin_adminlog[ previous ] ) + count = g_admin_adminlog[ previous ]->id + 1; + + if( g_admin_adminlog[ admin_adminlog_index ] ) + adminlog = g_admin_adminlog[ admin_adminlog_index ]; + else + adminlog = G_Alloc( sizeof( g_admin_adminlog_t ) ); + + memset( adminlog, 0, sizeof( adminlog ) ); + adminlog->id = count; + adminlog->time = level.time - level.startTime; + adminlog->success = success; + Q_strncpyz( adminlog->command, command, sizeof( adminlog->command ) ); + + if ( args ) + Q_strncpyz( adminlog->args, args, sizeof( adminlog->args ) ); + else + Q_strncpyz( adminlog->args, G_SayConcatArgs( 1 + skiparg ), sizeof( adminlog->args ) ); + + if( ent ) + { + qboolean found = qfalse; + // real admin name + for( i = 0; i < MAX_ADMIN_ADMINS && g_admin_admins[ i ]; i++ ) + { + if( !Q_stricmp( g_admin_admins[ i ]->guid, ent->client->pers.guid ) ) + { + Q_strncpyz( adminlog->name, g_admin_admins[ i ]->name, sizeof( adminlog->name ) ); + found = qtrue; + break; + } + } + if( !found ) + Q_strncpyz( adminlog->name, ent->client->pers.netname, sizeof( adminlog->name ) ); + + adminlog->level = ent->client->pers.adminLevel; + } + else + { + Q_strncpyz( adminlog->name, "console", sizeof( adminlog->name ) ); + adminlog->level = 10000; + } + + g_admin_adminlog[ admin_adminlog_index ] = adminlog; + admin_adminlog_index++; + if( admin_adminlog_index >= MAX_ADMIN_ADMINLOGS ) + admin_adminlog_index = 0; +} + +qboolean G_admin_adminlog( gentity_t *ent, int skiparg ) +{ + g_admin_adminlog_t *results[ 10 ]; + int result_index = 0; + char *search_cmd = NULL; + char *search_name = NULL; + int index; + int skip = 0; + int skipped = 0; + int checked = 0; + char n1[ MAX_NAME_LENGTH ]; + char fmt_name[ 16 ]; + char argbuf[ 32 ]; + int name_length = 12; + int max_id = 0; + int i; + qboolean match; + + memset( results, 0, sizeof( results ) ); + + index = admin_adminlog_index; + for( i = 0; i < 10; i++ ) + { + int prev; + + prev = index - 1; + if( prev < 0 ) + prev = MAX_ADMIN_ADMINLOGS - 1; + if( !g_admin_adminlog[ prev ] ) + break; + if( g_admin_adminlog[ prev ]->id > max_id ) + max_id = g_admin_adminlog[ prev ]->id; + index = prev; + } + + if( G_SayArgc() > 1 + skiparg ) + { + G_SayArgv( 1 + skiparg, argbuf, sizeof( argbuf ) ); + if( ( *argbuf >= '0' && *argbuf <= '9' ) || *argbuf == '-' ) + { + int id; + + id = atoi( argbuf ); + if( id < 0 ) + id += ( max_id - 9 ); + else if( id <= max_id - MAX_ADMIN_ADMINLOGS ) + id = max_id - MAX_ADMIN_ADMINLOGS + 1; + + if( id + 9 >= max_id ) + id = max_id - 9; + if( id < 1 ) + id = 1; + for( i = 0; i < MAX_ADMIN_ADMINLOGS; i++ ) + { + if( g_admin_adminlog[ i ]->id == id ) + { + index = i; + break; + } + } + } + else if ( *argbuf == '!' ) + { + search_cmd = argbuf + 1; + } + else + { + search_name = argbuf; + } + + if( G_SayArgc() > 2 + skiparg && ( search_cmd || search_name ) ) + { + char skipbuf[ 4 ]; + G_SayArgv( 2 + skiparg, skipbuf, sizeof( skipbuf ) ); + skip = atoi( skipbuf ); + } + } + + if( search_cmd || search_name ) + { + g_admin_adminlog_t *result_swap[ 10 ]; + + memset( result_swap, 0, sizeof( result_swap ) ); + + index = admin_adminlog_index - 1; + if( index < 0 ) + index = MAX_ADMIN_ADMINLOGS - 1; + + while( g_admin_adminlog[ index ] && + checked < MAX_ADMIN_ADMINLOGS && + result_index < 10 ) + { + match = qfalse; + if( search_cmd ) + { + if( !Q_stricmp( search_cmd, g_admin_adminlog[ index ]->command ) ) + match = qtrue; + } + if( search_name ) + { + G_SanitiseString( g_admin_adminlog[ index ]->name, n1, sizeof( n1 ) ); + if( strstr( n1, search_name ) ) + match = qtrue; + } + + if( match && skip > 0 ) + { + match = qfalse; + skip--; + skipped++; + } + if( match ) + { + result_swap[ result_index ] = g_admin_adminlog[ index ]; + result_index++; + } + + checked++; + index--; + if( index < 0 ) + index = MAX_ADMIN_ADMINLOGS - 1; + } + // search runs backwards, turn it around + for( i = 0; i < result_index; i++ ) + results[ i ] = result_swap[ result_index - i - 1 ]; + } + else + { + while( g_admin_adminlog[ index ] && result_index < 10 ) + { + results[ result_index ] = g_admin_adminlog[ index ]; + result_index++; + index++; + if( index >= MAX_ADMIN_ADMINLOGS ) + index = 0; + } + } + + for( i = 0; results[ i ] && i < 10; i++ ) + { + int l; + + G_DecolorString( results[ i ]->name, n1 ); + l = strlen( n1 ); + if( l > name_length ) + name_length = l; + } + ADMBP_begin( ); + for( i = 0; results[ i ] && i < 10; i++ ) + { + char levelbuf[ 8 ]; + int t; + + t = results[ i ]->time / 1000; + G_DecolorString( results[ i ]->name, n1 ); + Com_sprintf( fmt_name, sizeof( fmt_name ), "%%%ds", + ( name_length + strlen( results[ i ]->name ) - strlen( n1 ) ) ); + Com_sprintf( n1, sizeof( n1 ), fmt_name, results[ i ]->name ); + Com_sprintf( levelbuf, sizeof( levelbuf ), "%2d", results[ i ]->level ); + ADMBP( va( "%s%3d %3d:%02d %2s ^7%s^7 %s!%s ^7%s^7\n", + ( results[ i ]->success ) ? "^7" : "^1", + results[ i ]->id, + t / 60, t % 60, + ( results[ i ]->level ) < 10000 ? levelbuf : " -", + n1, + ( results[ i ]->success ) ? "^3" : "^1", + results[ i ]->command, + results[ i ]->args ) ); + } + if( search_cmd || search_name ) + { + ADMBP( va( "^3!adminlog:^7 Showing %d matches for '%s^7'.", + result_index, + argbuf ) ); + if( checked < MAX_ADMIN_ADMINLOGS && g_admin_adminlog[ checked ] ) + ADMBP( va( " run '!adminlog %s^7 %d' to see more", + argbuf, + skipped + result_index ) ); + ADMBP( "\n" ); + } + else if ( results[ 0 ] ) + { + ADMBP( va( "^3!adminlog:^7 Showing %d - %d of %d.\n", + results[ 0 ]->id, + results[ result_index - 1 ]->id, + max_id ) ); + } + else + { + ADMBP( "^3!adminlog:^7 log is empty.\n" ); + } + ADMBP_end( ); + + return qtrue; +} + +qboolean G_admin_map( gentity_t *ent, int skiparg ) +{ + char map[ MAX_QPATH ]; + char layout[ MAX_QPATH ] = { "" }; + + if( G_SayArgc( ) < 2 + skiparg ) + { + ADMP( "^3!map: ^7usage: !map [map] (layout)\n" ); + return qfalse; + } + + G_SayArgv( skiparg + 1, map, sizeof( map ) ); + + if( !trap_FS_FOpenFile( va( "maps/%s.bsp", map ), NULL, FS_READ ) ) + { + ADMP( va( "^3!map: ^7invalid map name '%s'\n", map ) ); + return qfalse; + } + + if( G_SayArgc( ) > 2 + skiparg ) + { + G_SayArgv( skiparg + 2, layout, sizeof( layout ) ); + if( !Q_stricmp( layout, "*BUILTIN*" ) || + trap_FS_FOpenFile( va( "layouts/%s/%s.dat", map, layout ), + NULL, FS_READ ) > 0 ) + { + trap_Cvar_Set( "g_layouts", layout ); + } + else + { + ADMP( va( "^3!map: ^7invalid layout name '%s'\n", layout ) ); + return qfalse; + } + } + + trap_SendConsoleCommand( EXEC_APPEND, va( "map %s", map ) ); + level.restarted = qtrue; + AP( va( "print \"^3!map: ^7map '%s' started by %s^7 %s\n\"", map, + ( ent ) ? G_admin_adminPrintName( ent ) : "console", + ( layout[ 0 ] ) ? va( "(forcing layout '%s')", layout ) : "" ) ); + G_admin_maplog_result( "M" ); + return qtrue; +} + +qboolean G_admin_devmap( gentity_t *ent, int skiparg ) +{ + char map[ MAX_QPATH ]; + char layout[ MAX_QPATH ] = { "" }; + + if( G_SayArgc( ) < 2 + skiparg ) + { + ADMP( "^3!devmap: ^7usage: !devmap [map] (layout)\n" ); + return qfalse; + } + + G_SayArgv( skiparg + 1, map, sizeof( map ) ); + + if( !trap_FS_FOpenFile( va( "maps/%s.bsp", map ), NULL, FS_READ ) ) + { + ADMP( va( "^3!devmap: ^7invalid map name '%s'\n", map ) ); + return qfalse; + } + + if( G_SayArgc( ) > 2 + skiparg ) + { + G_SayArgv( skiparg + 2, layout, sizeof( layout ) ); + if( !Q_stricmp( layout, "*BUILTIN*" ) || + trap_FS_FOpenFile( va( "layouts/%s/%s.dat", map, layout ), + NULL, FS_READ ) > 0 ) + { + trap_Cvar_Set( "g_layouts", layout ); + } + else + { + ADMP( va( "^3!devmap: ^7invalid layout name '%s'\n", layout ) ); + return qfalse; + } + } + + trap_SendConsoleCommand( EXEC_APPEND, va( "devmap %s", map ) ); + level.restarted = qtrue; + AP( va( "print \"^3!devmap: ^7map '%s' started by %s^7 with cheats %s\n\"", map, + ( ent ) ? G_admin_adminPrintName( ent ) : "console", + ( layout[ 0 ] ) ? va( "(forcing layout '%s')", layout ) : "" ) ); + G_admin_maplog_result( "D" ); + return qtrue; +} + +void G_admin_maplog_update( void ) +{ + char map[ 64 ]; + char maplog[ MAX_CVAR_VALUE_STRING ]; + char *ptr; + int count = 0; + + trap_Cvar_VariableStringBuffer( "mapname", map, sizeof( map ) ); + + Q_strncpyz( maplog, g_adminMapLog.string, sizeof( maplog ) ); + ptr = maplog; + while( *ptr && count < MAX_ADMIN_MAPLOG_LENGTH ) + { + while( *ptr != ' ' && *ptr != '\0' ) ptr++; + + count++; + if( count >= MAX_ADMIN_MAPLOG_LENGTH ) + { + *ptr = '\0'; + } + + if( *ptr == ' ' ) ptr++; + } + + trap_Cvar_Set( "g_adminMapLog", va( "%s%s%s", + map, + ( maplog[0] != '\0' ) ? " " : "", + maplog ) ); +} + +void G_admin_maplog_result( char *flag ) +{ + static int lastTime = 0; + char maplog[ MAX_CVAR_VALUE_STRING ]; + int t; + + if( !flag ) + return; + + // avoid race when called in same frame + if( level.time == lastTime ) + return; + + lastTime = level.time; + + if( g_adminMapLog.string[ 0 ] && + g_adminMapLog.string[ 1 ] == ';' ) + { + // only one result allowed + return; + } + + if ( level.surrenderTeam != PTE_NONE ) + { + if( flag[ 0 ] == 'a' ) + { + if( level.surrenderTeam == PTE_HUMANS ) + flag = "A"; + } + else if( flag[ 0 ] == 'h' ) + { + if( level.surrenderTeam == PTE_ALIENS ) + flag = "H"; + } + } + + t = ( level.time - level.startTime ) / 1000; + Q_strncpyz( maplog, g_adminMapLog.string, sizeof( maplog ) ); + trap_Cvar_Set( "g_adminMapLog", va( "%1s;%03d:%02d;%s", + flag, + t / 60, t % 60, + maplog ) ); +} + + +qboolean G_admin_maplog( gentity_t *ent, int skiparg ) +{ + char maplog[ MAX_CVAR_VALUE_STRING ]; + char *ptr; + int count = 0; + + Q_strncpyz( maplog, g_adminMapLog.string, sizeof( maplog ) ); + + ADMBP_begin( ); + ptr = maplog; + while( *ptr != '\0' && count < MAX_ADMIN_MAPLOG_LENGTH + 1 ) + { + char *end; + const char *result = NULL; + char *clock = NULL; + char *colon; + + end = ptr; + while( *end != ' ' && *end != '\0' ) end++; + if( *end == ' ' ) + { + *end = '\0'; + end++; + } + + if( ptr[ 0 ] && ptr[ 1 ] == ';' ) + { + switch( ptr[ 0 ] ) + { + case 't': + result = "^7tie"; + break; + case 'a': + result = "^1Alien win"; + break; + case 'A': + result = "^1Alien win ^7/ Humans admitted defeat"; + break; + case 'h': + result = "^4Human win"; + break; + case 'H': + result = "^4Human win ^7/ Aliens admitted defeat"; + break; + case 'd': + result = "^5draw vote"; + break; + case 'N': + result = "^6admin loaded next map"; + break; + case 'r': + result = "^2restart vote"; + break; + case 'R': + result = "^6admin restarted map"; + break; + case 'm': + result = "^2map vote"; + break; + case 'M': + result = "^6admin changed map"; + break; + case 'D': + result = "^6admin loaded devmap"; + break; + default: + result = ""; + break; + } + ptr += 2; + colon = strchr( ptr, ';' ); + if ( colon ) + { + clock = ptr; + ptr = colon + 1; + *colon = '\0'; + + // right justification with -6%s doesnt work.. + if( clock[ 0 ] == '0' && clock[ 1 ] != ':' ) + { + if( clock[ 1 ] == '0' && clock[ 2 ] != ':' ) + clock[ 1 ] = ' '; + clock[ 0 ] = ' '; + } + } + } + else if( count == 0 ) + { + result = "^3current map"; + clock = " -:--"; + } + + ADMBP( va( "%s%20s %-6s %s^7\n", + ( count == 0 ) ? "^3" : "^7", + ptr, + ( clock ) ? clock : "", + ( result ) ? result : "" ) ); + + ptr = end; + count++; + } + ADMBP_end( ); + + return qtrue; +} + +qboolean G_admin_layoutsave( gentity_t *ent, int skiparg ) +{ + char layout[ MAX_QPATH ]; + + if( G_SayArgc( ) < 2 + skiparg ) + { + ADMP( "^3!layoutsave: ^7usage: !layoutsave [layout]\n" ); + return qfalse; + } + + G_SayArgv( skiparg + 1, layout, sizeof( layout ) ); + + trap_SendConsoleCommand( EXEC_APPEND, va( "layoutsave %s", layout ) ); + AP( va( "print \"^3!layoutsave: ^7layout saved as '%s' by %s\n\"", layout, + ( ent ) ? G_admin_adminPrintName( ent ) : "console" ) ); + return qtrue; +} + +qboolean G_admin_demo( gentity_t *ent, int skiparg ) +{ + if( !ent ) + { + ADMP( "!demo: console can not use demo.\n" ); + return qfalse; + } + + ent->client->pers.ignoreAdminWarnings = !( ent->client->pers.ignoreAdminWarnings ); + + ADMP( va( "^3!demo: ^7your visibility of admin chat is now %s\n", + ( ent->client->pers.ignoreAdminWarnings ) ? "^1disabled" : "^2enabled" ) ); + + return qtrue; +} + +qboolean G_admin_mute( gentity_t *ent, int skiparg ) +{ + int pids[ MAX_CLIENTS ]; + char name[ MAX_NAME_LENGTH ], err[ MAX_STRING_CHARS ]; + char command[ MAX_ADMIN_CMD_LEN ], *cmd; + gentity_t *vic; + char secs[ 7 ]; + qboolean usageDuration = qfalse; + int seconds = 0; + + G_SayArgv( skiparg, command, sizeof( command ) ); + cmd = command; + + if( cmd && *cmd == '!' ) + cmd++; + + if( G_SayArgc() < 2 + skiparg ) + { + ADMP( va( "^3!%s: ^7usage: !%s [name|slot#] (duration)\n", cmd, cmd ) ); + return qfalse; + } + + G_SayArgv( 1 + skiparg, name, sizeof( name ) ); + + if( G_ClientNumbersFromString( name, pids ) != 1 ) + { + G_MatchOnePlayer( pids, err, sizeof( err ) ); + ADMP( va( "^3!%s: ^7%s\n", cmd, err ) ); + return qfalse; + } + + if( !admin_higher( ent, &g_entities[ pids[ 0 ] ] ) ) + { + ADMP( va( "^3!%s: ^7sorry, but your intended victim has a higher admin" + " level than you\n", cmd ) ); + return qfalse; + } + + vic = &g_entities[ pids[ 0 ] ]; + if( !Q_stricmp( cmd, "unmute" ) ) + { + if( vic->client->pers.muted == qfalse ) + { + ADMP( "^3!unmute: ^7player is not currently muted\n" ); + return qtrue; + } + + vic->client->pers.muteExpires = 0; + vic->client->pers.muted = qfalse; + + CPx( pids[ 0 ], "cp \"^1You have been unmuted\"" ); + AP( va( "print \"^3!unmute: ^7%s^7 has been unmuted by %s\n\"", + vic->client->pers.netname, + ( ent ) ? G_admin_adminPrintName( ent ) : "console" ) ); + } else { + // Duration + if( G_SayArgc() > 2 + skiparg ) + { + G_SayArgv( 2 + skiparg, secs, sizeof( secs ) ); + seconds = G_admin_parse_time( secs ); + vic->client->pers.muteExpires = level.time + ( seconds * 1000 ); + } + + vic->client->pers.muted = qtrue; + + CPx( pids[ 0 ], "cp \"^1You've been muted\"" ); + AP( va( "print \"^3!mute: ^7%s^7 has been muted by ^7%s%s\n\"", + vic->client->pers.netname, + ( ent ) ? G_admin_adminPrintName( ent ) : "console", + ( seconds ) ? va( " ^7for %i seconds", seconds ) : "" ) ); + } + + return qtrue; +} + +qboolean G_admin_cp( gentity_t *ent, int skiparg ) +{ + int minargc; + char *s; + + minargc = 2 + skiparg; + + if( G_SayArgc() < minargc ) + { + ADMP( "^3!cp: ^7usage: !cp [message]\n" ); + return qfalse; + } + + s = G_SayConcatArgs( 1 + skiparg ); + G_CP(ent); + return qtrue; +} + +qboolean G_admin_denybuild( gentity_t *ent, int skiparg ) +{ + int pids[ MAX_CLIENTS ]; + char name[ MAX_NAME_LENGTH ], err[ MAX_STRING_CHARS ]; + char command[ MAX_ADMIN_CMD_LEN ], *cmd; + gentity_t *vic; + + G_SayArgv( skiparg, command, sizeof( command ) ); + cmd = command; + if( cmd && *cmd == '!' ) + cmd++; + if( G_SayArgc() < 2 + skiparg ) + { + ADMP( va( "^3!%s: ^7usage: !%s [name|slot#]\n", cmd, cmd ) ); + return qfalse; + } + G_SayArgv( 1 + skiparg, name, sizeof( name ) ); + if( G_ClientNumbersFromString( name, pids ) != 1 ) + { + G_MatchOnePlayer( pids, err, sizeof( err ) ); + ADMP( va( "^3!%s: ^7%s\n", cmd, err ) ); + return qfalse; + } + if( !admin_higher( ent, &g_entities[ pids[ 0 ] ] ) ) + { + ADMP( va( "^3!%s: ^7sorry, but your intended victim has a higher admin" + " level than you\n", cmd ) ); + return qfalse; + } + vic = &g_entities[ pids[ 0 ] ]; + if( vic->client->pers.denyBuild ) + { + if( !Q_stricmp( cmd, "denybuild" ) ) + { + ADMP( "^3!denybuild: ^7player already has no building rights\n" ); + return qtrue; + } + vic->client->pers.denyBuild = qfalse; + CPx( pids[ 0 ], "cp \"^1You've regained your building rights\"" ); + AP( va( + "print \"^3!allowbuild: ^7building rights for ^7%s^7 restored by %s\n\"", + vic->client->pers.netname, + ( ent ) ? G_admin_adminPrintName( ent ) : "console" ) ); + } + else + { + if( !Q_stricmp( cmd, "allowbuild" ) ) + { + ADMP( "^3!allowbuild: ^7player already has building rights\n" ); + return qtrue; + } + vic->client->pers.denyBuild = qtrue; + vic->client->ps.stats[ STAT_BUILDABLE ] = BA_NONE; + if( vic->client->ps.stats[ STAT_PCLASS ]== PCL_ALIEN_BUILDER0 || vic->client->ps.stats[ STAT_PCLASS ] == PCL_ALIEN_BUILDER0_UPG ) + { + vic->suicideTime = level.time + 1000; + } + CPx( pids[ 0 ], "cp \"^1You've lost your building rights\"" ); + AP( va( + "print \"^3!denybuild: ^7building rights for ^7%s^7 revoked by ^7%s\n\"", + vic->client->pers.netname, + ( ent ) ? G_admin_adminPrintName( ent ) : "console" ) ); + } + return qtrue; +} + +qboolean G_admin_denyweapon( gentity_t *ent, int skiparg ) +{ + int pids[ MAX_CLIENTS ]; + char name[ MAX_NAME_LENGTH ], err[ MAX_STRING_CHARS ]; + char command[ MAX_ADMIN_CMD_LEN ], *cmd; + char buffer[ 32 ]; + int weapon = WP_NONE; + int class = PCL_NONE; + char *realname; + gentity_t *vic; + int flag; + qboolean all = qfalse; + + G_SayArgv( skiparg, command, sizeof( command ) ); + cmd = command; + if( cmd && *cmd == '!' ) + cmd++; + if( G_SayArgc() < 3 + skiparg ) + { + ADMP( va( "^3!%s: ^7usage: !%s [name|slot#] [class|weapon]\n", cmd, cmd ) ); + return qfalse; + } + G_SayArgv( 1 + skiparg, name, sizeof( name ) ); + if( G_ClientNumbersFromString( name, pids ) != 1 ) + { + G_MatchOnePlayer( pids, err, sizeof( err ) ); + ADMP( va( "^3!%s: ^7%s\n", cmd, err ) ); + return qfalse; + } + if( !admin_higher( ent, &g_entities[ pids[ 0 ] ] ) ) + { + ADMP( va( "^3!%s: ^7sorry, but your intended victim has a higher admin" + " level than you\n", cmd ) ); + return qfalse; + } + vic = &g_entities[ pids[ 0 ] ]; + + if( vic == ent && + !Q_stricmp( cmd, "denyweapon" ) ) + { + ADMP( va( "^3!%s: ^7sorry, you cannot %s yourself\n", cmd, cmd ) ); + return qfalse; + } + + G_SayArgv( 2 + skiparg, buffer, sizeof( buffer ) ); + + if( !Q_stricmp( buffer, "all" ) && + !Q_stricmp( cmd, "allowweapon" ) ) + { + if( vic->client->pers.denyHumanWeapons || + vic->client->pers.denyAlienClasses ) + { + vic->client->pers.denyHumanWeapons = 0; + vic->client->pers.denyAlienClasses = 0; + + CPx( pids[ 0 ], "cp \"^1You've regained all weapon and class rights\"" ); + AP( va( "print \"^3!%s: ^7all weapon and class rights for ^7%s^7 restored by %s\n\"", + cmd, + vic->client->pers.netname, + ( ent ) ? ent->client->pers.netname : "console" ) ); + } + else + { + ADMP( va( "^3!%s: ^7player already has all rights\n", cmd ) ); + } + return qtrue; + } + + if( !Q_stricmp( buffer, "all" ) && + !Q_stricmp( cmd, "denyweapon" ) ) + { + all = qtrue; + weapon = WP_NONE; + class = PCL_NONE; + + if( vic->client->pers.denyHumanWeapons == 0xFFFFFF && + vic->client->pers.denyAlienClasses == 0xFFFFFF ) + { + ADMP( va( "^3!%s: ^7player already has no weapon or class rights\n", cmd ) ); + return qtrue; + } + + if( vic->client->pers.teamSelection == PTE_HUMANS ) + { + weapon = vic->client->ps.weapon; + if( weapon < WP_PAIN_SAW || weapon > WP_GRENADE ) + weapon = WP_NONE; + } + if( vic->client->pers.teamSelection == PTE_ALIENS ) + { + class = vic->client->pers.classSelection; + if( class < PCL_ALIEN_LEVEL1 || class > PCL_ALIEN_LEVEL4 ) + class = PCL_NONE; + } + + vic->client->pers.denyHumanWeapons = 0xFFFFFF; + vic->client->pers.denyAlienClasses = 0xFFFFFF; + } + else + { + weapon = BG_FindWeaponNumForName( buffer ); + if( weapon < WP_PAIN_SAW || weapon > WP_GRENADE ) + class = BG_FindClassNumForName( buffer ); + if( ( weapon < WP_PAIN_SAW || weapon > WP_GRENADE ) && + ( class < PCL_ALIEN_LEVEL1 || class > PCL_ALIEN_LEVEL4 ) ) + { + { + ADMP( va( "^3!%s: ^7unknown weapon or class\n", cmd ) ); + return qfalse; + } + } + } + + if( class == PCL_NONE ) + { + realname = BG_FindHumanNameForWeapon( weapon ); + flag = 1 << (weapon - WP_BLASTER); + if( !Q_stricmp( cmd, "denyweapon" ) ) + { + if( ( vic->client->pers.denyHumanWeapons & flag ) && !all ) + { + ADMP( va( "^3!%s: ^7player already has no %s rights\n", cmd, realname ) ); + return qtrue; + } + vic->client->pers.denyHumanWeapons |= flag; + if( vic->client->pers.teamSelection == PTE_HUMANS ) + { + if( ( weapon == WP_GRENADE || all ) && + BG_InventoryContainsUpgrade( UP_GRENADE, vic->client->ps.stats ) ) + { + BG_RemoveUpgradeFromInventory( UP_GRENADE, vic->client->ps.stats ); + G_AddCreditToClient( vic->client, (short)BG_FindPriceForUpgrade( UP_GRENADE ), qfalse ); + } + if( BG_InventoryContainsWeapon( weapon, vic->client->ps.stats ) ) + { + int maxAmmo, maxClips; + + BG_RemoveWeaponFromInventory( weapon, vic->client->ps.stats ); + G_AddCreditToClient( vic->client, (short)BG_FindPriceForWeapon( weapon ), qfalse ); + + BG_AddWeaponToInventory( WP_MACHINEGUN, vic->client->ps.stats ); + BG_FindAmmoForWeapon( WP_MACHINEGUN, &maxAmmo, &maxClips ); + BG_PackAmmoArray( WP_MACHINEGUN, vic->client->ps.ammo, vic->client->ps.powerups, + maxAmmo, maxClips ); + G_ForceWeaponChange( vic, WP_MACHINEGUN ); + vic->client->ps.stats[ STAT_MISC ] = 0; + ClientUserinfoChanged( pids[ 0 ], qfalse ); + } + } + } + else + { + if( !( vic->client->pers.denyHumanWeapons & flag ) ) + { + ADMP( va( "^3!%s: ^7player already has %s rights\n", cmd, realname ) ); + return qtrue; + } + vic->client->pers.denyHumanWeapons &= ~flag; + } + } + else + { + realname = BG_FindHumanNameForClassNum( class ); + flag = 1 << class; + if( !Q_stricmp( cmd, "denyweapon" ) ) + { + if( ( vic->client->pers.denyAlienClasses & flag ) && !all ) + { + ADMP( va( "^3!%s: ^7player already has no %s rights\n", cmd, realname ) ); + return qtrue; + } + vic->client->pers.denyAlienClasses |= flag; + if( vic->client->pers.teamSelection == PTE_ALIENS && + vic->client->pers.classSelection == class ) + { + vec3_t infestOrigin; + short cost; + + G_RoomForClassChange( vic, PCL_ALIEN_LEVEL0, infestOrigin ); + + vic->client->pers.evolveHealthFraction = (float)vic->client->ps.stats[ STAT_HEALTH ] / + (float)BG_FindHealthForClass( class ); + if( vic->client->pers.evolveHealthFraction < 0.0f ) + vic->client->pers.evolveHealthFraction = 0.0f; + else if( vic->client->pers.evolveHealthFraction > 1.0f ) + vic->client->pers.evolveHealthFraction = 1.0f; + + vic->client->pers.classSelection = PCL_ALIEN_LEVEL0; + cost = BG_ClassCanEvolveFromTo( PCL_ALIEN_LEVEL0, class, 9, 0 ); + if( cost < 0 ) cost = 0; + G_AddCreditToClient( vic->client, cost, qfalse ); + ClientUserinfoChanged( pids[ 0 ], qfalse ); + VectorCopy( infestOrigin, vic->s.pos.trBase ); + ClientSpawn( vic, vic, vic->s.pos.trBase, vic->s.apos.trBase ); + } + } + else + { + if( !( vic->client->pers.denyAlienClasses & flag ) ) + { + ADMP( va( "^3!%s: ^7player already has %s rights\n", cmd, realname ) ); + return qtrue; + } + vic->client->pers.denyAlienClasses &= ~flag; + } + } + + if( all ) + realname = "weapon and class"; + + CPx( pids[ 0 ], va( "cp \"^1You've %s your %s rights\"", + ( !Q_stricmp( cmd, "denyweapon" ) ) ? "lost" : "regained", + realname ) ); + AP( va( + "print \"^3!%s: ^7%s rights for ^7%s^7 %s by %s\n\"", + cmd, realname, + vic->client->pers.netname, + ( !Q_stricmp( cmd, "denyweapon" ) ) ? "revoked" : "restored", + ( ent ) ? ent->client->pers.netname : "console" ) ); + + return qtrue; +} + +qboolean G_admin_listadmins( gentity_t *ent, int skiparg ) +{ + int i, found = 0; + char search[ MAX_NAME_LENGTH ] = {""}; + char s[ MAX_NAME_LENGTH ] = {""}; + int start = 0; + qboolean numeric = qtrue; + int drawn = 0; + int minlevel = 1; + + if( G_SayArgc() == 3 + skiparg ) + { + G_SayArgv( 2 + skiparg, s, sizeof( s ) ); + if( s[ 0 ] >= '0' && s[ 0] <= '9' ) + { + minlevel = atoi( s ); + if( minlevel < 1 ) + minlevel = 1; + } + } + + for( i = 0; i < MAX_ADMIN_ADMINS && g_admin_admins[ i ]; i++ ) + { + if( g_admin_admins[ i ]->level >= minlevel ) + found++; + } + if( !found ) + { + if( minlevel > 1 ) + { + ADMP( va( "^3!listadmins: ^7no admins level %i or greater found\n", minlevel ) ); + } + else + { + ADMP( "^3!listadmins: ^7no admins defined\n" ); + } + return qfalse; + } + + if( G_SayArgc() >= 2 + skiparg ) + { + G_SayArgv( 1 + skiparg, s, sizeof( s ) ); + for( i = 0; i < sizeof( s ) && s[ i ]; i++ ) + { + if( s[ i ] >= '0' && s[ i ] <= '9' ) + continue; + numeric = qfalse; + } + if( numeric ) + { + start = atoi( s ); + if( start > 0 ) + start -= 1; + else if( start < 0 ) + start = found + start; + } + else + G_SanitiseString( s, search, sizeof( search ) ); + } + + if( start >= found || start < 0 ) + start = 0; + + drawn = admin_listadmins( ent, start, search, minlevel ); + + if( search[ 0 ] ) + { + if( drawn <= 20 ) + { + ADMP( va( "^3!listadmins:^7 found %d admins level %i or greater matching '%s^7'\n", + drawn, minlevel, search ) ); + } + else + { + ADMP( va( "^3!listadmins:^7 found >20 admins level %i or greater matching '%s^7. Try a more narrow search.'\n", + minlevel, search ) ); + } + } + else + { + ADMBP_begin(); + ADMBP( va( "^3!listadmins:^7 showing admins level %i or greater %d - %d of %d. ", + minlevel, + ( found ) ? ( start + 1 ) : 0, + ( ( start + MAX_ADMIN_LISTITEMS ) > found ) ? + found : ( start + MAX_ADMIN_LISTITEMS ), + found ) ); + if( ( start + MAX_ADMIN_LISTITEMS ) < found ) + { + if( minlevel > 1) + { + ADMBP( va( "run '!listadmins %d %d' to see more", + ( start + MAX_ADMIN_LISTITEMS + 1 ), minlevel ) ); + } + else + { + ADMBP( va( "run '!listadmins %d' to see more", + ( start + MAX_ADMIN_LISTITEMS + 1 ) ) ); + } + } + ADMBP( "\n" ); + ADMBP_end(); + } + return qtrue; +} + +qboolean G_admin_listlayouts( gentity_t *ent, int skiparg ) +{ + char list[ MAX_CVAR_VALUE_STRING ]; + char map[ MAX_QPATH ]; + int count = 0; + char *s; + char layout[ MAX_QPATH ] = { "" }; + int i = 0; + + if( G_SayArgc( ) == 2 + skiparg ) + G_SayArgv( 1 +skiparg, map, sizeof( map ) ); + else + trap_Cvar_VariableStringBuffer( "mapname", map, sizeof( map ) ); + + count = G_LayoutList( map, list, sizeof( list ) ); + ADMBP_begin( ); + ADMBP( va( "^3!listlayouts:^7 %d layouts found for '%s':\n", count, map ) ); + s = &list[ 0 ]; + while( *s ) + { + if( *s == ' ' ) + { + ADMBP( va ( " %s\n", layout ) ); + layout[ 0 ] = '\0'; + i = 0; + } + else if( i < sizeof( layout ) - 2 ) + { + layout[ i++ ] = *s; + layout[ i ] = '\0'; + } + s++; + } + if( layout[ 0 ] ) + ADMBP( va ( " %s\n", layout ) ); + ADMBP_end( ); + return qtrue; +} + +qboolean G_admin_listplayers( gentity_t *ent, int skiparg ) +{ + int i, j; + int invisiblePlayers = 0; + gclient_t *p; + char c[ 3 ], t[ 2 ]; // color and team letter + char n[ MAX_NAME_LENGTH ] = {""}; + char n2[ MAX_NAME_LENGTH ] = {""}; + char n3[ MAX_NAME_LENGTH ] = {""}; + char lname[ MAX_NAME_LENGTH ]; + char lname2[ MAX_NAME_LENGTH ]; + char guid_stub[ 9 ]; + char muted[ 2 ], denied[ 2 ], dbuilder[ 2 ], misc[ 2 ]; + int l; + char lname_fmt[ 5 ]; + + for( i = 0; i < level.maxclients; i++ ) { + p = &level.clients[ i ]; + if ( p->sess.invisible == qtrue ) + invisiblePlayers++; + } + + ADMBP_begin(); + ADMBP( va( "^3!listplayers^7: %i players connected:\n", + level.numConnectedClients - invisiblePlayers ) ); + for( i = 0; i < level.maxclients; i++ ) + { + p = &level.clients[ i ]; + + // Ignore invisible players + if ( p->sess.invisible == qtrue ) + continue; + + Q_strncpyz( t, "S", sizeof( t ) ); + Q_strncpyz( c, S_COLOR_YELLOW, sizeof( c ) ); + if( p->pers.teamSelection == PTE_HUMANS ) + { + Q_strncpyz( t, "H", sizeof( t ) ); + Q_strncpyz( c, S_COLOR_BLUE, sizeof( c ) ); + } + else if( p->pers.teamSelection == PTE_ALIENS ) + { + Q_strncpyz( t, "A", sizeof( t ) ); + Q_strncpyz( c, S_COLOR_RED, sizeof( c ) ); + } + + if( p->pers.connected == CON_CONNECTING ) + { + Q_strncpyz( t, "C", sizeof( t ) ); + Q_strncpyz( c, S_COLOR_CYAN, sizeof( c ) ); + } + else if( p->pers.connected != CON_CONNECTED ) + { + continue; + } + + for( j = 0; j < 8; j++ ) + guid_stub[ j ] = p->pers.guid[ j + 24 ]; + guid_stub[ j ] = '\0'; + + muted[ 0 ] = '\0'; + if( p->pers.muted ) + { + Q_strncpyz( muted, "M", sizeof( muted ) ); + } + denied[ 0 ] = '\0'; + if( p->pers.denyBuild ) + { + Q_strncpyz( denied, "B", sizeof( denied ) ); + } + if( p->pers.denyHumanWeapons || p->pers.denyAlienClasses ) + { + Q_strncpyz( denied, "W", sizeof( denied ) ); + } + + dbuilder[ 0 ] = '\0'; + if( p->pers.designatedBuilder ) + { + if( !G_admin_permission( &g_entities[ i ], ADMF_INCOGNITO ) && + G_admin_permission( &g_entities[ i ], ADMF_DBUILDER ) && + G_admin_permission(ent, ADMF_SEESFULLLISTPLAYERS ) ) + { + Q_strncpyz( dbuilder, "P", sizeof( dbuilder ) ); + } + else + { + Q_strncpyz( dbuilder, "D", sizeof( dbuilder ) ); + } + } + + misc[ 0 ] = '\0'; + if( G_admin_permission( &g_entities[ i ], ADMF_BAN_IMMUNITY ) ) + { + // use Misc slot for Immunity player status + Q_strncpyz( misc, "I", sizeof( misc ) ); + } else if( p->pers.paused ) { + // use Misc slot for paused player status + Q_strncpyz( misc, "P", sizeof( misc ) ); + } + + l = 0; + G_SanitiseString( p->pers.netname, n2, sizeof( n2 ) ); + n[ 0 ] = '\0'; + for( j = 0; j < MAX_ADMIN_ADMINS && g_admin_admins[ j ]; j++ ) + { + if( !Q_stricmp( g_admin_admins[ j ]->guid, p->pers.guid ) ) + { + + // don't gather aka or level info if the admin is incognito + if( ent && G_admin_permission( &g_entities[ i ], ADMF_INCOGNITO ) ) + { + break; + } + l = g_admin_admins[ j ]->level; + G_SanitiseString( g_admin_admins[ j ]->name, n3, sizeof( n3 ) ); + if( Q_stricmp( n2, n3 ) ) + { + Q_strncpyz( n, g_admin_admins[ j ]->name, sizeof( n ) ); + } + break; + } + } + lname[ 0 ] = '\0'; + Q_strncpyz( lname_fmt, "%s", sizeof( lname_fmt ) ); + for( j = 0; j < MAX_ADMIN_LEVELS && g_admin_levels[ j ]; j++ ) + { + if( g_admin_levels[ j ]->level == l ) + { + Q_strncpyz( lname, g_admin_levels[ j ]->name, sizeof( lname ) ); + if( *lname ) + { + G_DecolorString( lname, lname2 ); + Com_sprintf( lname_fmt, sizeof( lname_fmt ), "%%%is", + ( admin_level_maxname + strlen( lname ) - strlen( lname2 ) ) ); + Com_sprintf( lname2, sizeof( lname2 ), lname_fmt, lname ); + } + break; + } + + } + + if( G_admin_permission(ent, ADMF_SEESFULLLISTPLAYERS ) ) { + + ADMBP( va( "%2i %s%s^7 %-2i %s^7 (*%s) ^1%1s%1s%1s%1s^7 %s^7 %s%s^7%s\n", + i, + c, + t, + l, + ( *lname ) ? lname2 : "", + guid_stub, + muted, + dbuilder, + denied, + misc, + p->pers.netname, + ( *n ) ? "(a.k.a. " : "", + n, + ( *n ) ? ")" : "" + ) ); + } + else + { + ADMBP( va( "%2i %s%s^7 ^1%1s%1s%1s^7 %s^7\n", + i, + c, + t, + muted, + dbuilder, + denied, + p->pers.netname + ) ); + } + } + ADMBP_end(); + return qtrue; +} + +#define MAX_LISTMAPS_MAPS 256 + +static int SortMaps(const void *a, const void *b) +{ + return strcmp(*(char **)a, *(char **)b); +} + +qboolean G_admin_listmaps( gentity_t *ent, int skiparg ) +{ + char fileList[ 4096 ] = {""}; + char *fileSort[ MAX_LISTMAPS_MAPS ]; + char search[ 16 ] = {""}; + int numFiles; + int i; + int fileLen = 0; + int count = 0; + char *filePtr; + int rows; + + if( G_SayArgc( ) > 1 + skiparg ) + { + G_SayArgv( skiparg + 1, search, sizeof( search ) ); + } + + numFiles = trap_FS_GetFileList( "maps/", ".bsp", + fileList, sizeof( fileList ) ); + filePtr = fileList; + for( i = 0; i < numFiles && count < MAX_LISTMAPS_MAPS; i++, filePtr += fileLen + 1 ) + { + fileLen = strlen( filePtr ); + if (fileLen < 5) + continue; + + filePtr[ fileLen - 4 ] = '\0'; + + if( search[ 0 ] && !strstr( filePtr, search ) ) + continue; + + fileSort[ count ] = filePtr; + count++; + } + + qsort(fileSort, count, sizeof(fileSort[ 0 ]), SortMaps); + + rows = count / 3; + if ( rows * 3 < count ) rows++; + + ADMBP_begin(); + for( i = 0; i < rows; i++ ) + { + ADMBP( va( "^7%20s %20s %20s\n", + fileSort[ i ], + ( rows + i < count ) ? fileSort[ rows + i ] : "", + ( rows * 2 + i < count ) ? fileSort[ rows * 2 + i ] : "" ) ); + } + if ( search[ 0 ] ) + ADMBP( va( "^3!listmaps: ^7found %d maps matching '%s^7'.\n", count, search ) ); + else + ADMBP( va( "^3!listmaps: ^7listing %d maps.\n", count ) ); + + ADMBP_end(); + + return qtrue; +} + +qboolean G_admin_listrotation( gentity_t *ent, int skiparg ) +{ + int i, j, statusColor; + char mapnames[ MAX_STRING_CHARS ]; + char *status = '\0'; + + extern mapRotations_t mapRotations; + + // Check for an active map rotation + if ( !G_MapRotationActive() || + g_currentMapRotation.integer == NOT_ROTATING ) + { + trap_SendServerCommand( ent-g_entities, "print \"^3!rotation: ^7There is no active map rotation on this server\n\"" ); + return qfalse; + } + + // Locate the active map rotation and output its contents + for( i = 0; i < mapRotations.numRotations; i++ ) + { + if ( i == g_currentMapRotation.integer ) + { + int currentMap = G_GetCurrentMap( i ); + + ADMBP_begin(); + ADMBP( va( "^3!rotation: ^7%s\n", mapRotations.rotations[ i ].name ) ); + + for( j = 0; j < mapRotations.rotations[ i ].numMaps; j++ ) + { + Q_strncpyz( mapnames, mapRotations.rotations[ i ].maps[ j ].name, sizeof( mapnames ) ); + + if( !Q_stricmp( mapRotations.rotations[ i ].maps[ j ].name, "*VOTE*" ) ) + { + char slotMap[ 64 ]; + int lineLen = 0; + int k; + + trap_Cvar_VariableStringBuffer( "mapname", slotMap, sizeof( slotMap ) ); + mapnames[ 0 ] = '\0'; + for( k = 0; k < mapRotations.rotations[ i ].maps[ j ].numConditions; k++ ) + { + char *thisMap; + int mc = 7; + + if( mapRotations.rotations[ i ].maps[ j ].conditions[ k ].lhs != MCV_VOTE ) + continue; + + thisMap = mapRotations.rotations[ i ].maps[ j ].conditions[ k ].dest; + lineLen += strlen( thisMap ) + 1; + + if( currentMap == j && !Q_stricmp( thisMap, slotMap ) ) + mc = 3; + Q_strcat( mapnames, sizeof( mapnames ), va( "^7%s%s^%i%s", + ( k ) ? ", " : "", + ( lineLen > 50 ) ? "\n " : "", + mc, thisMap ) ); + if( lineLen > 50 ) + lineLen = strlen( thisMap ) + 2; + else + lineLen++; + } + + if( currentMap == j ) + { + statusColor = 3; + status = "current slot"; + } + else if( !k ) + { + statusColor = 1; + status = "empty vote"; + } + else + { + statusColor = 7; + status = "vote"; + } + } else if( !Q_stricmp( mapRotations.rotations[ i ].maps[ j ].name, "*RANDOM*" ) ) + { + char slotMap[ 64 ]; + int lineLen = 0; + int k; + + trap_Cvar_VariableStringBuffer( "mapname", slotMap, sizeof( slotMap ) ); + mapnames[ 0 ] = '\0'; + for( k = 0; k < mapRotations.rotations[ i ].maps[ j ].numConditions; k++ ) + { + char *thisMap; + int mc = 7; + + if( mapRotations.rotations[ i ].maps[ j ].conditions[ k ].lhs != MCV_SELECTEDRANDOM ) + continue; + + thisMap = mapRotations.rotations[ i ].maps[ j ].conditions[ k ].dest; + lineLen += strlen( thisMap ) + 1; + + if( currentMap == j && !Q_stricmp( thisMap, slotMap ) ) + mc = 3; + Q_strcat( mapnames, sizeof( mapnames ), va( "^7%s%s^%i%s", + ( k ) ? ", " : "", + ( lineLen > 50 ) ? "\n " : "", + mc, thisMap ) ); + if( lineLen > 50 ) + lineLen = strlen( thisMap ) + 2; + else + lineLen++; + } + + if( currentMap == j ) + { + statusColor = 3; + status = "current slot"; + } + else if( !k ) + { + statusColor = 1; + status = "empty Random"; + } + else + { + statusColor = 7; + status = "Random"; + } + } + else if ( currentMap == j ) + { + statusColor = 3; + status = "current slot"; + } + else if ( !G_MapExists( mapRotations.rotations[ i ].maps[ j ].name ) ) + { + statusColor = 1; + status = "missing"; + } + else + { + statusColor = 7; + status = ""; + } + ADMBP( va( " ^%i%-12s %3i %s\n", statusColor, status, j + 1, mapnames ) ); + } + + ADMBP_end(); + + // No maps were found in the active map rotation + if ( mapRotations.rotations[ i ].numMaps < 1 ) + { + trap_SendServerCommand( ent-g_entities, "print \" - ^7Empty!\n\"" ); + return qfalse; + } + } + } + + if( g_nextMap.string[0] ) + { + ADMP( va ("^5 Next map overriden by g_nextMap to: %s\n", g_nextMap.string ) ); + } + + return qtrue; +} + + +qboolean G_admin_showbans( gentity_t *ent, int skiparg ) +{ + int i, found = 0; + int t; + char duration[ 32 ]; + char sduration[ 32 ]; + char suspended[ 64 ] = { "" }; + char name_fmt[ 32 ] = { "%s" }; + char banner_fmt[ 32 ] = { "%s" }; + int max_name = 1, max_banner = 1; + int secs; + int start = 0; + char filter[ MAX_NAME_LENGTH ] = {""}; + char date[ 11 ]; + char *made; + int j; + char n1[ MAX_NAME_LENGTH * 2 ] = {""}; + char n2[ MAX_NAME_LENGTH * 2 ] = {""}; + int bannerslevel = 0; + qboolean numeric = qtrue; + char *ip_match = NULL; + int ip_match_len = 0; + char name_match[ MAX_NAME_LENGTH ] = {""}; + int show_count = 0; + qboolean subnetfilter = qfalse; + + t = trap_RealTime( NULL ); + + for( i = 0; i < MAX_ADMIN_BANS && g_admin_bans[ i ]; i++ ) + { + if( g_admin_bans[ i ]->expires != 0 + && ( g_admin_bans[ i ]->expires - t ) < 1 ) + { + continue; + } + found++; + } + + if( G_SayArgc() >= 2 + skiparg ) + { + G_SayArgv( 1 + skiparg, filter, sizeof( filter ) ); + if( G_SayArgc() >= 3 + skiparg ) + { + start = atoi( filter ); + G_SayArgv( 2 + skiparg, filter, sizeof( filter ) ); + } + for( i = 0; i < sizeof( filter ) && filter[ i ] ; i++ ) + { + if( ( filter[ i ] < '0' || filter[ i ] > '9' ) + && filter[ i ] != '.' && filter[ i ] != '-' ) + { + numeric = qfalse; + break; + } + } + + if (!numeric) + { + if( filter[ 0 ] != '-' ) + { + G_SanitiseString( filter, name_match, sizeof( name_match) ); + + } + else + { + if( !Q_strncmp( filter, "-sub", 4 ) ) + { + subnetfilter = qtrue; + } + else + { + ADMP( va( "^3!showbans: ^7invalid argument %s\n", filter ) ); + return qfalse; + } + } + } + else if( strchr( filter, '.' ) != NULL ) + { + ip_match = filter; + ip_match_len = strlen(ip_match); + } + else + { + start = atoi( filter ); + filter[0] = '\0'; + } + // showbans 1 means start with ban 0 + if( start > 0 ) + start -= 1; + else if( start < 0 ) + start = found + start; + } + + if( start >= MAX_ADMIN_BANS || start < 0 ) + start = 0; + + for( i = start; i < MAX_ADMIN_BANS && g_admin_bans[ i ] + && show_count < MAX_ADMIN_SHOWBANS; i++ ) + { + qboolean match = qfalse; + + if (!numeric) + { + if( !subnetfilter ) + { + G_SanitiseString( g_admin_bans[ i ]->name, n1, sizeof( n1 ) ); + if (strstr( n1, name_match) ) + match = qtrue; + } + else + { + int mask = -1; + int dummy; + int scanflen = 0; + scanflen = sscanf( g_admin_bans[ i ]->ip, "%d.%d.%d.%d/%d", &dummy, &dummy, &dummy, &dummy, &mask ); + if( scanflen == 5 && mask < 32 ) + { + match = qtrue; + } + } + } + + if ( ( match ) || !ip_match + || Q_strncmp( ip_match, g_admin_bans[ i ]->ip, ip_match_len) == 0 ) + { + G_DecolorString( g_admin_bans[ i ]->name, n1 ); + G_DecolorString( g_admin_bans[ i ]->banner, n2 ); + if( strlen( n1 ) > max_name ) + { + max_name = strlen( n1 ); + } + if( strlen( n2 ) > max_banner ) + max_banner = strlen( n2 ); + + show_count++; + } + } + + if( start >= found ) + { + ADMP( va( "^3!showbans: ^7there are %d active bans\n", found ) ); + return qfalse; + } + ADMBP_begin(); + show_count = 0; + for( i = start; i < MAX_ADMIN_BANS && g_admin_bans[ i ] + && show_count < MAX_ADMIN_SHOWBANS; i++ ) + { + if (!numeric) + { + if( !subnetfilter ) + { + G_SanitiseString( g_admin_bans[ i ]->name, n1, sizeof( n1 ) ); + if ( strstr ( n1, name_match ) == NULL ) + continue; + } + else + { + int mask = -1; + int dummy; + int scanflen = 0; + scanflen = sscanf( g_admin_bans[ i ]->ip, "%d.%d.%d.%d/%d", &dummy, &dummy, &dummy, &dummy, &mask ); + if( scanflen != 5 || mask >= 32 ) + { + continue; + } + } + } + else if( ip_match != NULL + && Q_strncmp( ip_match, g_admin_bans[ i ]->ip, ip_match_len ) != 0) + continue; + + // only print out the the date part of made + date[ 0 ] = '\0'; + made = g_admin_bans[ i ]->made; + for( j = 0; made && *made; j++ ) + { + if( ( j + 1 ) >= sizeof( date ) ) + break; + if( *made == ' ' ) + break; + date[ j ] = *made; + date[ j + 1 ] = '\0'; + made++; + } + + if( g_admin_bans[ i ]->expires != 0 + && ( g_admin_bans[ i ]->expires - t ) < 1 ) + { + Com_sprintf( duration, sizeof( duration ), "^1*EXPIRED*^7" ); + } else { + secs = ( g_admin_bans[ i ]->expires - t ); + G_admin_duration( secs, duration, sizeof( duration ) ); + } + + suspended[ 0 ] = '\0'; + if( g_admin_bans[ i ]->suspend > t ) + { + G_admin_duration( g_admin_bans[ i ]->suspend - t, sduration, sizeof( sduration ) ); + Com_sprintf( suspended, sizeof( suspended ), "^3*SUSPENDED*^7 for %s^7", + sduration ); + } + + G_DecolorString( g_admin_bans[ i ]->name, n1 ); + Com_sprintf( name_fmt, sizeof( name_fmt ), "%%%is", + ( max_name + strlen( g_admin_bans[ i ]->name ) - strlen( n1 ) ) ); + Com_sprintf( n1, sizeof( n1 ), name_fmt, g_admin_bans[ i ]->name ); + + G_DecolorString( g_admin_bans[ i ]->banner, n2 ); + Com_sprintf( banner_fmt, sizeof( banner_fmt ), "%%%is", + ( max_banner + strlen( g_admin_bans[ i ]->banner ) - strlen( n2 ) ) ); + Com_sprintf( n2, sizeof( n2 ), banner_fmt, g_admin_bans[ i ]->banner ); + bannerslevel = g_admin_bans[ i ]->bannerlevel; + + ADMBP( va( "%4i %s^7 %-15s %-8s %-10s\n | %-15s^7 Level:%2i\n | %s\n \\__ %s\n", + ( i + 1 ), + n1, + g_admin_bans[ i ]->ip, + date, + duration, + n2, + bannerslevel, + suspended, + g_admin_bans[ i ]->reason ) ); + + show_count++; + } + + if (!numeric || ip_match) + { + char matchmethod[50]; + if( numeric ) + Com_sprintf( matchmethod, sizeof(matchmethod), "IP" ); + else if( !subnetfilter ) + Com_sprintf( matchmethod, sizeof(matchmethod), "name" ); + else + Com_sprintf( matchmethod, sizeof(matchmethod), "ip range size" ); + + + ADMBP( va( "^3!showbans:^7 found %d matching bans by %s. ", + show_count, + matchmethod ) ); + } + else + { + ADMBP( va( "^3!showbans:^7 showing bans %d - %d of %d. ", + ( found ) ? ( start + 1 ) : 0, + ( ( start + MAX_ADMIN_SHOWBANS ) > found ) ? + found : ( start + MAX_ADMIN_SHOWBANS ), + found ) ); + } + + if( ( start + MAX_ADMIN_SHOWBANS ) < found ) + { + ADMBP( va( "run !showbans %d %s to see more", + ( start + MAX_ADMIN_SHOWBANS + 1 ), + (filter[0]) ? filter : "" ) ); + } + ADMBP( "\n" ); + ADMBP_end(); + return qtrue; +} + +qboolean G_admin_help( gentity_t *ent, int skiparg ) +{ + int i; + int count = 0; + int commandsPerLine = 6; + + if( G_SayArgc() < 2 + skiparg ) + { + int j = 0; + int count = 0; + + ADMBP_begin(); + for( i = 0; i < adminNumCmds; i++ ) + { + if( G_admin_permission( ent, g_admin_cmds[ i ].flag ) ) + { + ADMBP( va( "^3!%-12s", g_admin_cmds[ i ].keyword ) ); + j++; + count++; + } + // show 6 commands per line + if( j == 6 ) + { + ADMBP( "\n" ); + j = 0; + } + } + for( i = 0; i < MAX_ADMIN_COMMANDS && g_admin_commands[ i ]; i++ ) + { + if( !G_admin_permission( ent, g_admin_commands[ i ]->flag ) ) + continue; + ADMBP( va( "^3!%-12s", g_admin_commands[ i ]->command ) ); + j++; + count++; + // show 6 commands per line + if( j == 6 ) + { + ADMBP( "\n" ); + j = 0; + } + } + + if( count ) + ADMBP( "\n" ); + ADMBP( va( "^3!help: ^7%i available commands\n", count ) ); + ADMBP( "run !help [^3command^7] for help with a specific command.\n" ); + ADMBP( "The following non-standard /commands may also be available to you: \n" ); + count = 1; + + if( ent && g_AllStats.integer ) { + if( count > commandsPerLine && ( count % commandsPerLine ) == 1 ) ADMBP( "\n" ); + ADMBP( va( "^5/%-12s", "allstats" ) ); + count++; + } + if ( ent ) { + if( count > commandsPerLine && ( count % commandsPerLine ) == 1 ) ADMBP( "\n" ); + ADMBP( va( "^5/%-12s", "builder" ) ); + count++; + } + if( ent && g_allowVote.integer && G_admin_permission( ent, ADMF_NO_VOTE ) ) { + if( count > commandsPerLine && ( count % commandsPerLine ) == 1 ) ADMBP( "\n" ); + ADMBP( va( "^5/%-12s", "callvote" ) ); + count++; + } + if( ent && g_allowVote.integer && G_admin_permission( ent, ADMF_NO_VOTE ) && ( ent->client->ps.stats[ STAT_PTEAM ] == PTE_ALIENS || ent->client->ps.stats[ STAT_PTEAM ] == PTE_HUMANS ) ) { + if( count > commandsPerLine && ( count % commandsPerLine ) == 1 ) ADMBP( "\n" ); + ADMBP( va( "^5/%-12s", "callteamvote" ) ); + count++; + } + if( ent && g_allowShare.integer && ( ent->client->ps.stats[ STAT_PTEAM ] == PTE_ALIENS || ent->client->ps.stats[ STAT_PTEAM ] == PTE_HUMANS ) ) { + if( count > commandsPerLine && ( count % commandsPerLine ) == 1 ) ADMBP( "\n" ); + ADMBP( va( "^5/%-12s", "donate" ) ); + count++; + } + if( g_privateMessages.integer ) { + if( count > commandsPerLine && ( count % commandsPerLine ) == 1 ) ADMBP( "\n" ); + ADMBP( va( "^5/%-12s", "m" ) ); + count++; + } + if( ent && g_markDeconstruct.integer == 2 && ( ent->client->ps.stats[ STAT_PTEAM ] == PTE_ALIENS || ent->client->ps.stats[ STAT_PTEAM ] == PTE_HUMANS ) ) { + if( count > commandsPerLine && ( count % commandsPerLine ) == 1 ) ADMBP( "\n" ); + ADMBP( va( "^5/%-12s", "mark" ) ); + count++; + } + if( ent && g_actionPrefix.string[0] ) { + if( count > commandsPerLine && ( count % commandsPerLine ) == 1 ) ADMBP( "\n" ); + ADMBP( va( "^5/%-12s", "me" ) ); + count++; + } + if( ent && g_actionPrefix.string[0] ) { + if( count > commandsPerLine && ( count % commandsPerLine ) == 1 ) ADMBP( "\n" ); + ADMBP( va( "^5/%-12s", "me_team" ) ); + count++; + } + if( ent && g_actionPrefix.string[0] && ( ent->client->ps.stats[ STAT_PTEAM ] == PTE_ALIENS || ent->client->ps.stats[ STAT_PTEAM ] == PTE_HUMANS ) ) { + if( count > commandsPerLine && ( count % commandsPerLine ) == 1 ) ADMBP( "\n" ); + ADMBP( va( "^5/%-12s", "mt" ) ); + count++; + } + if( ent && g_myStats.integer && ( ent->client->ps.stats[ STAT_PTEAM ] == PTE_ALIENS || ent->client->ps.stats[ STAT_PTEAM ] == PTE_HUMANS ) ) { + if( count > commandsPerLine && ( count % commandsPerLine ) == 1 ) ADMBP( "\n" ); + ADMBP( va( "^5/%-12s", "mystats" ) ); + count++; + } + if( ent->client->pers.designatedBuilder && ( ent->client->ps.stats[ STAT_PTEAM ] == PTE_ALIENS || ent->client->ps.stats[ STAT_PTEAM ] == PTE_HUMANS ) ) { + if( count > commandsPerLine && ( count % commandsPerLine ) == 1 ) ADMBP( "\n" ); + ADMBP( va( "^5/%-12s", "protect" ) ); + count++; + } + if( ent->client->pers.designatedBuilder && ( ent->client->ps.stats[ STAT_PTEAM ] == PTE_ALIENS || ent->client->ps.stats[ STAT_PTEAM ] == PTE_HUMANS ) ) { + if( count > commandsPerLine && ( count % commandsPerLine ) == 1 ) ADMBP( "\n" ); + ADMBP( va( "^5/%-12s", "resign" ) ); + count++; + } + if( g_publicSayadmins.integer || G_admin_permission( ent, ADMF_ADMINCHAT ) ) { + if( count > commandsPerLine && ( count % commandsPerLine ) == 1 ) ADMBP( "\n" ); + ADMBP( va( "^5/%-12s", "say_admins" ) ); + count++; + } + if( ent && ( ent->client->ps.stats[ STAT_PTEAM ] == PTE_ALIENS || ent->client->ps.stats[ STAT_PTEAM ] == PTE_HUMANS ) ) { + if( count > commandsPerLine && ( count % commandsPerLine ) == 1 ) ADMBP( "\n" ); + ADMBP( va( "^5/%-12s", "say_area" ) ); + count++; + } + if( ent && g_allowShare.integer && ( ent->client->ps.stats[ STAT_PTEAM ] == PTE_ALIENS || ent->client->ps.stats[ STAT_PTEAM ] == PTE_HUMANS ) ) { + if( count > commandsPerLine && ( count % commandsPerLine ) == 1 ) ADMBP( "\n" ); + ADMBP( va( "^5/%-12s", "share" ) ); + count++; + } + if( ent && g_teamStatus.integer && ( ent->client->ps.stats[ STAT_PTEAM ] == PTE_ALIENS || ent->client->ps.stats[ STAT_PTEAM ] == PTE_HUMANS ) ) { + if( count > commandsPerLine && ( count % commandsPerLine ) == 1 ) ADMBP( "\n" ); + ADMBP( va( "^5/%-12s", "teamstatus" ) ); + count++; + } + ADMBP( "\n" ); + ADMBP_end(); + + return qtrue; + } + else + { + //!help param + char param[ MAX_ADMIN_CMD_LEN ]; + char *cmd; + + G_SayArgv( 1 + skiparg, param, sizeof( param ) ); + cmd = ( param[0] == '!' ) ? ¶m[1] : ¶m[0]; + ADMBP_begin(); + for( i = 0; i < adminNumCmds; i++ ) + { + if( !Q_stricmp( cmd, g_admin_cmds[ i ].keyword ) ) + { + if( !G_admin_permission( ent, g_admin_cmds[ i ].flag ) ) + { + ADMBP( va( "^3!help: ^7you have no permission to use '%s'\n", + g_admin_cmds[ i ].keyword ) ); + ADMBP_end(); + return qfalse; + } + ADMBP( va( "^3!help: ^7help for '!%s':\n", + g_admin_cmds[ i ].keyword ) ); + ADMBP( va( " ^3Function: ^7%s\n", g_admin_cmds[ i ].function ) ); + ADMBP( va( " ^3Syntax: ^7!%s %s\n", g_admin_cmds[ i ].keyword, + g_admin_cmds[ i ].syntax ) ); + ADMBP( va( " ^3Flag: ^7'%s'\n", g_admin_cmds[ i ].flag ) ); + ADMBP_end(); + return qtrue; + } + } + for( i = 0; i < MAX_ADMIN_COMMANDS && g_admin_commands[ i ]; i++ ) + { + if( !Q_stricmp( cmd, g_admin_commands[ i ]->command ) ) + { + if( !G_admin_permission( ent, g_admin_commands[ i ]->flag ) ) + { + ADMBP( va( "^3!help: ^7you have no permission to use '%s'\n", + g_admin_commands[ i ]->command ) ); + ADMBP_end(); + return qfalse; + } + ADMBP( va( "^3!help: ^7help for '%s':\n", + g_admin_commands[ i ]->command ) ); + ADMBP( va( " ^3Description: ^7%s\n", g_admin_commands[ i ]->desc ) ); + ADMBP( va( " ^3Syntax: ^7!%s\n", g_admin_commands[ i ]->command ) ); + ADMBP_end(); + return qtrue; + } + } + ADMBP( va( "^3!help: ^7no help found for '%s'\n", cmd ) ); + ADMBP_end(); + return qfalse; + } +} + +qboolean G_admin_admintest( gentity_t *ent, int skiparg ) +{ + int i, l = 0; + qboolean found = qfalse; + qboolean lname = qfalse; + + if( !ent ) + { + ADMP( "^3!admintest: ^7you are on the console.\n" ); + return qtrue; + } + for( i = 0; i < MAX_ADMIN_ADMINS && g_admin_admins[ i ]; i++ ) + { + if( !Q_stricmp( g_admin_admins[ i ]->guid, ent->client->pers.guid ) ) + { + found = qtrue; + break; + } + } + + if( found ) + { + l = g_admin_admins[ i ]->level; + for( i = 0; i < MAX_ADMIN_LEVELS && g_admin_levels[ i ]; i++ ) + { + if( g_admin_levels[ i ]->level != l ) + continue; + if( *g_admin_levels[ i ]->name ) + { + lname = qtrue; + break; + } + } + } + AP( va( "print \"^3!admintest: ^7%s^7 is a level %d admin %s%s^7%s\n\"", + ent->client->pers.netname, + l, + ( lname ) ? "(" : "", + ( lname ) ? g_admin_levels[ i ]->name : "", + ( lname ) ? ")" : "" ) ); + return qtrue; +} + +qboolean G_admin_allready( gentity_t *ent, int skiparg ) +{ + int i = 0; + gclient_t *cl; + + if( !level.intermissiontime ) + { + ADMP( "^3!allready: ^7this command is only valid during intermission\n" ); + return qfalse; + } + + for( i = 0; i < g_maxclients.integer; i++ ) + { + cl = level.clients + i; + if( cl->pers.connected != CON_CONNECTED ) + continue; + + if( cl->pers.teamSelection == PTE_NONE ) + continue; + + cl->readyToExit = 1; + } + AP( va( "print \"^3!allready:^7 %s^7 says everyone is READY now\n\"", + ( ent ) ? G_admin_adminPrintName( ent ) : "console" ) ); + return qtrue; +} + +qboolean G_admin_cancelvote( gentity_t *ent, int skiparg ) +{ + + if(!level.voteTime && !level.teamVoteTime[ 0 ] && !level.teamVoteTime[ 1 ] ) + { + ADMP( "^3!cancelvote^7: no vote in progress\n" ); + return qfalse; + } + level.voteNo = level.numConnectedClients; + level.voteYes = 0; + CheckVote( ); + level.teamVoteNo[ 0 ] = level.numConnectedClients; + level.teamVoteYes[ 0 ] = 0; + CheckTeamVote( PTE_HUMANS ); + level.teamVoteNo[ 1 ] = level.numConnectedClients; + level.teamVoteYes[ 1 ] = 0; + CheckTeamVote( PTE_ALIENS ); + AP( va( "print \"^3!cancelvote: ^7%s^7 decided that everyone voted No\n\"", + ( ent ) ? G_admin_adminPrintName( ent ) : "console" ) ); + return qtrue; +} + +qboolean G_admin_passvote( gentity_t *ent, int skiparg ) +{ + if(!level.voteTime && !level.teamVoteTime[ 0 ] && !level.teamVoteTime[ 1 ] ) + { + ADMP( "^3!passvote^7: no vote in progress\n" ); + return qfalse; + } + level.voteYes = level.numConnectedClients; + level.voteNo = 0; + CheckVote( ); + level.teamVoteYes[ 0 ] = level.numConnectedClients; + level.teamVoteNo[ 0 ] = 0; + CheckTeamVote( PTE_HUMANS ); + level.teamVoteYes[ 1 ] = level.numConnectedClients; + level.teamVoteNo[ 1 ] = 0; + CheckTeamVote( PTE_ALIENS ); + AP( va( "print \"^3!passvote: ^7%s^7 decided that everyone voted Yes\n\"", + ( ent ) ? G_admin_adminPrintName( ent ) : "console" ) ); + return qtrue; +} + +static qboolean G_admin_global_pause( gentity_t *ent, int skiparg ) +{ + if( level.paused ) + { + level.pauseTime = level.time; + ADMP( "^3!pause: ^7Game is already paused, unpause timer reset\n" ); + return qfalse; + } + + level.paused = qtrue; + level.pauseTime = level.time; + + level.pause_speed = g_speed.value; + level.pause_gravity = g_gravity.value; + level.pause_knockback = g_knockback.value; + level.pause_ff = g_friendlyFire.integer; + level.pause_ffb = g_friendlyBuildableFire.integer; + + g_speed.value = 0; + g_gravity.value = 0; + g_knockback.value = 0; + g_friendlyFire.integer = 0; + g_friendlyBuildableFire.integer = 0; + + CP( "cp \"^1Game is PAUSED\"" ); + AP( va( "print \"^3!pause: ^7The game has been paused by %s\n\"", + ( ent ) ? G_admin_adminPrintName( ent ) : "console" ) ); + + return qtrue; +} + +static qboolean G_admin_global_unpause( gentity_t *ent, int skiparg ) +{ + if( !level.paused ) + { + ADMP( "^3!unpause: ^7Game is not paused\n" ); + return qfalse; + } + + level.paused = qfalse; + + g_speed.value = level.pause_speed; + g_gravity.value = level.pause_gravity; + g_knockback.value = level.pause_knockback; + g_friendlyFire.integer = level.pause_ff; + g_friendlyBuildableFire.integer = level.pause_ffb; + + CP( "cp \"^2Game is RESUMED\"" ); + AP( va( "print \"^3!unpause: ^7The game has been resumed by %s\n\"", + ( ent ) ? G_admin_adminPrintName( ent ) : "console" ) ); + + return qtrue; +} + +qboolean G_admin_pause( gentity_t *ent, int skiparg ) +{ + int pids[ MAX_CLIENTS ]; + char name[ MAX_NAME_LENGTH ], err[ MAX_STRING_CHARS ]; + char command[ MAX_ADMIN_CMD_LEN ], *cmd; + int i; + int count = 0; + gentity_t *vic; + + G_SayArgv( skiparg, command, sizeof( command ) ); + cmd = command; + if( cmd && *cmd == '!' ) + cmd++; + + if( G_SayArgc() < 2 + skiparg ) + { + // global pause + if( !Q_stricmp( cmd, "pause" ) ) + return G_admin_global_pause( ent, skiparg ); + else + return G_admin_global_unpause( ent, skiparg ); + } + if( G_SayArgc() != 2 + skiparg ) + { + ADMP( va( "^3!%s: ^7usage: !%s (^5name|slot|*^7)\n", cmd, cmd ) ); + return qfalse; + } + + G_SayArgv( 1 + skiparg, name, sizeof( name ) ); + if( !Q_stricmp( name, "*" ) ) + { + for( i = 0; i < MAX_CLIENTS; i++ ) + { + vic = &g_entities[ i ]; + if( vic && vic->client && + vic->client->pers.connected == CON_CONNECTED ) + { + pids[ count ] = i; + count++; + } + } + } + else + { + if( G_ClientNumbersFromString( name, pids ) != 1 ) + { + G_MatchOnePlayer( pids, err, sizeof( err ) ); + ADMP( va( "^3!%s: ^7%s\n", cmd, err ) ); + return qfalse; + } + count = 1; + } + + for( i = 0; i < count; i++ ) + { + vic = &g_entities[ pids[ i ] ]; + if ( !vic || !vic->client ) continue; + if( !admin_higher( ent, vic ) ) + { + if( count == 1 ) + ADMP( va( "^3!%s: ^7sorry, but your intended victim has a higher admin" + " level than you\n", cmd ) ); + continue; + } + if( vic->client->pers.paused ) + { + if( !Q_stricmp( cmd, "pause" ) ) + { + if( count == 1 ) + ADMP( "^3!pause: ^7player is already paused\n" ); + continue; + } + vic->client->pers.paused = qfalse; + CPx( pids[ i ], "cp \"^2You've been unpaused\"" ); + if( count == 1 ) + AP( va( "print \"^3!unpause: ^7%s^7 unpaused by %s\n\"", + vic->client->pers.netname, + ( ent ) ? ent->client->pers.netname : "console" ) ); + } + else + { + if( !Q_stricmp( cmd, "unpause" ) ) + { + if( count == 1 ) + ADMP( "^3!unpause: ^7player is already unpaused\n" ); + continue; + } + if( count == 1 && ent == vic) + { + ADMP( "^3!pause: ^7you can not pause yourself\n" ); + continue; + } + vic->client->pers.paused = qtrue; + CPx( pids[ i ], va( "cp \"^1You've been paused by ^7%s\"", + ( ent ) ? ent->client->pers.netname : "console" ) ); + if( count == 1 ) + AP( va( "print \"^3!pause: ^7%s^7 paused by %s\n\"", + vic->client->pers.netname, + ( ent ) ? ent->client->pers.netname : "console" ) ); + } + ClientUserinfoChanged( pids[ i ], qfalse ); + } + return qtrue; +} + +qboolean G_admin_spec999( gentity_t *ent, int skiparg ) +{ + int i; + gentity_t *vic; + + for( i = 0; i < level.maxclients; i++ ) + { + vic = &g_entities[ i ]; + if( !vic->client ) + continue; + if( vic->client->pers.connected != CON_CONNECTED ) + continue; + if( vic->client->pers.teamSelection == PTE_NONE ) + continue; + if( vic->client->ps.ping == 999 ) + { + G_ChangeTeam( vic, PTE_NONE ); + AP( va( "print \"^3!spec999: ^7%s^7 moved ^7%s^7 to spectators\n\"", + ( ent ) ? G_admin_adminPrintName( ent ) : "console", + vic->client->pers.netname ) ); + } + } + return qtrue; +} + +qboolean G_admin_register(gentity_t *ent, int skiparg ){ + int level = 0; + + if( !ent ) return qtrue; + + level = G_admin_level(ent); + + if( level == 0 ) + level = 1; + + if( !Q_stricmp( ent->client->pers.guid, "XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX" ) ) + { + ADMP( va( "^3!register: ^7 You cannot register for name protection until you update your client. Please replace your client executable with the one at http://trem.tjw.org/backport/ and reconnect. Updating your client will also allow you to have faster map downloads.\n" ) ); + return qfalse; + } + + if( g_newbieNumbering.integer + && g_newbieNamePrefix.string[ 0 ] + && !Q_stricmpn ( ent->client->pers.netname, g_newbieNamePrefix.string, strlen(g_newbieNamePrefix.string ) ) ) + { + ADMP( va( "^3!register: ^7 You cannot register names that begin with '%s^7'.\n", + g_newbieNamePrefix.string ) ); + return qfalse; + } + + trap_SendConsoleCommand( EXEC_APPEND,va( "!setlevel %d %d;",ent - g_entities, level) ); + + AP( va( "print \"^3!register: ^7%s^7 is now a protected nickname.\n\"", ent->client->pers.netname) ); + + return qtrue; +} + +qboolean G_admin_rename( gentity_t *ent, int skiparg ) +{ + int pids[ MAX_CLIENTS ]; + char name[ MAX_NAME_LENGTH ]; + char newname[ MAX_NAME_LENGTH ]; + char oldname[ MAX_NAME_LENGTH ]; + char err[ MAX_STRING_CHARS ]; + char userinfo[ MAX_INFO_STRING ]; + char *s; + gentity_t *victim = NULL; + + if( G_SayArgc() < 3 + skiparg ) + { + ADMP( "^3!rename: ^7usage: !rename [name] [newname]\n" ); + return qfalse; + } + G_SayArgv( 1 + skiparg, name, sizeof( name ) ); + s = G_SayConcatArgs( 2 + skiparg ); + Q_strncpyz( newname, s, sizeof( newname ) ); + if( G_ClientNumbersFromString( name, pids ) != 1 ) + { + G_MatchOnePlayer( pids, err, sizeof( err ) ); + ADMP( va( "^3!rename: ^7%s\n", err ) ); + return qfalse; + } + victim = &g_entities[ pids[ 0 ] ] ; + if( !admin_higher( ent, victim ) ) + { + ADMP( "^3!rename: ^7sorry, but your intended victim has a higher admin" + " level than you\n" ); + return qfalse; + } + if( !G_admin_name_check( victim, newname, err, sizeof( err ) ) ) + { + G_admin_print( ent, va( "^3!rename: Invalid name: ^7%s\n", err ) ); + return qfalse; + } + level.clients[ pids[ 0 ] ].pers.nameChanges--; + level.clients[ pids[ 0 ] ].pers.nameChangeTime = 0; + trap_GetUserinfo( pids[ 0 ], userinfo, sizeof( userinfo ) ); + s = Info_ValueForKey( userinfo, "name" ); + Q_strncpyz( oldname, s, sizeof( oldname ) ); + Info_SetValueForKey( userinfo, "name", newname ); + trap_SetUserinfo( pids[ 0 ], userinfo ); + ClientUserinfoChanged( pids[ 0 ], qtrue ); + if( strcmp( oldname, level.clients[ pids[ 0 ] ].pers.netname ) ) + AP( va( "print \"^3!rename: ^7%s^7 has been renamed to %s^7 by %s\n\"", + oldname, + level.clients[ pids[ 0 ] ].pers.netname, + ( ent ) ? G_admin_adminPrintName( ent ) : "console" ) ); + return qtrue; +} + +qboolean G_admin_restart( gentity_t *ent, int skiparg ) +{ + char layout[ MAX_CVAR_VALUE_STRING ] = { "" }; + char teampref[ MAX_CVAR_VALUE_STRING ] = { "" }; + int i; + gclient_t *cl; + + if( G_SayArgc( ) > 1 + skiparg ) + { + char map[ MAX_QPATH ]; + + trap_Cvar_VariableStringBuffer( "mapname", map, sizeof( map ) ); + G_SayArgv( skiparg + 1, layout, sizeof( layout ) ); + + if( Q_stricmp( layout, "keepteams" ) && Q_stricmp( layout, "keepteamslock" ) && Q_stricmp( layout, "switchteams" ) && Q_stricmp( layout, "switchteamslock" ) ) + { + if( !Q_stricmp( layout, "*BUILTIN*" ) || + trap_FS_FOpenFile( va( "layouts/%s/%s.dat", map, layout ), + NULL, FS_READ ) > 0 ) + { + trap_Cvar_Set( "g_layouts", layout ); + } + else + { + ADMP( va( "^3!restart: ^7layout '%s' does not exist\n", layout ) ); + return qfalse; + } + } + else + { + strcpy(layout,""); + G_SayArgv( skiparg + 1, teampref, sizeof( teampref ) ); + } + } + + if( G_SayArgc( ) > 2 + skiparg ) + { + G_SayArgv( skiparg + 2, teampref, sizeof( teampref ) ); + } + + + if( !Q_stricmp( teampref, "keepteams" ) || !Q_stricmp( teampref, "keepteamslock" ) ) + { + for( i = 0; i < g_maxclients.integer; i++ ) + { + cl = level.clients + i; + if( cl->pers.connected != CON_CONNECTED ) + continue; + + if( cl->pers.teamSelection == PTE_NONE ) + continue; + + cl->sess.restartTeam = cl->pers.teamSelection; + } + } + else if(!Q_stricmp( teampref, "switchteams" ) || !Q_stricmp( teampref, "switchteamslock" )) + { + for( i = 0; i < g_maxclients.integer; i++ ) + { + cl = level.clients + i; + if( cl->pers.connected != CON_CONNECTED ) + continue; + + if( cl->pers.teamSelection == PTE_NONE ) + continue; + + if( cl->pers.teamSelection == PTE_ALIENS ) + cl->sess.restartTeam = PTE_HUMANS; + else if(cl->pers.teamSelection == PTE_HUMANS ) + cl->sess.restartTeam = PTE_ALIENS; + } + } + + if( !Q_stricmp( teampref, "switchteamslock" ) || !Q_stricmp( teampref, "keepteamslock" ) ) + { + trap_Cvar_Set( "g_lockTeamsAtStart", "1" ); + } + + trap_SendConsoleCommand( EXEC_APPEND, "map_restart" ); + + if(teampref[ 0 ]) + strcpy(teampref,va( "^7(with teams option: '%s^7')", teampref )); + + G_admin_maplog_result( "R" ); + + AP( va( "print \"^3!restart: ^7map restarted by %s %s %s\n\"", + ( ent ) ? G_admin_adminPrintName( ent ) : "console", + ( layout[ 0 ] ) ? va( "^7(forcing layout '%s^7')", layout ) : "", + teampref ) ); + return qtrue; +} + +qboolean G_admin_nobuild( gentity_t *ent, int skiparg ) +{ + char buffer[ MAX_STRING_CHARS ]; + int i, tmp; + + if( G_SayArgc() < 2 + skiparg ) + { + ADMP( "^3!nobuild: ^7usage: !nobuild (^5enable / disable / log / remove / save^7)\n" ); + return qfalse; + } + + G_SayArgv( 1 + skiparg, buffer, sizeof( buffer ) ); + + if( !Q_stricmp( buffer, "enable" ) || !Q_stricmp( buffer, "Enable" ) ) + { + if( G_SayArgc() < 4 + skiparg ) + { + ADMP( "^3!nobuild: ^7usage: !nobuild enable (^5area^7) (^5height^7)\n" ); + return qfalse; + } + + if( !level.noBuilding ) + { + + level.noBuilding = qtrue; + + // Grab and set the area + G_SayArgv( 2 + skiparg, buffer, sizeof( buffer ) ); + tmp = atoi( buffer ); + level.nbArea = tmp; + + // Grab and set the height + G_SayArgv( 3 + skiparg, buffer, sizeof( buffer ) ); + tmp = atoi( buffer ); + level.nbHeight = tmp; + + ADMP( "^3!nobuild: ^7nobuild is now enabled, please place a buildable to spawn a marker\n" ); + return qtrue; + } + else + { + ADMP( "^3!nobuild: ^7nobuild is already enabled. type !nobuild disable to disable nobuild mode.\n" ); + return qfalse; + } + } + + if( !Q_stricmp( buffer, "disable" ) || !Q_stricmp( buffer, "Disable" ) ) + { + if( level.noBuilding ) + { + level.noBuilding = qfalse; + level.nbArea = 0.0f; + level.nbHeight = 0.0f; + ADMP( "^3!nobuild: ^7nobuild is now disabled\n" ); + return qtrue; + } + else + { + ADMP( "^3!nobuild: ^7nobuild is disabled. type !nobuild enable (^5area^7) (^5height^7) to enable nobuild mode.\n" ); + return qfalse; + } + } + + if( !Q_stricmp( buffer, "log" ) || !Q_stricmp( buffer, "Log" ) ) + { + ADMBP_begin(); + + tmp = 0; + for( i = 0; i < MAX_GENTITIES; i++ ) + { + if( level.nbMarkers[ i ].Marker == NULL ) + continue; + + // This will display a start at 1, and not 0. This is to stop confusion by server ops + ADMBP( va( "^7#%i at origin (^5%f %f %f^7)^7\n", i + 1, level.nbMarkers[ i ].Origin[0], level.nbMarkers[ i ].Origin[1], level.nbMarkers[ i ].Origin[2] ) ); + tmp++; + } + ADMBP( va( "^3!nobuild:^7 displaying %i marker(s)\n", tmp ) ); + + ADMBP_end(); + return qtrue; + } + + if( !Q_stricmp( buffer, "remove" ) || !Q_stricmp( buffer, "Remove" ) ) + { + if( G_SayArgc() < 3 + skiparg ) + { + ADMP( "^3!nobuild: ^7usage: !nobuild remove (^5marker #^7)\n" ); + return qfalse; + } + + G_SayArgv( 2 + skiparg, buffer, sizeof( buffer ) ); + tmp = atoi( buffer ) - 1; + // ^ that was to work with the display number... + + if( level.nbMarkers[ tmp ].Marker == NULL ) + { + ADMP( "^3!nobuild: ^7that is not a valid marker number\n" ); + return qfalse; + } + // Donno why im doing this, but lets clear this... + level.nbMarkers[ tmp ].Marker->noBuild.isNB = qfalse; + level.nbMarkers[ tmp ].Marker->noBuild.Area = 0.0f; + level.nbMarkers[ tmp ].Marker->noBuild.Height = 0.0f; + + // Free the entitiy and null it out... + G_FreeEntity( level.nbMarkers[ tmp ].Marker ); + level.nbMarkers[ tmp ].Marker = NULL; + + // That is to work with the display number... + ADMP( va( "^3!nobuild:^7 marker %i has been removed\n", tmp + 1 ) ); + return qtrue; + } + + if( !Q_stricmp( buffer, "save" ) || !Q_stricmp( buffer, "Save" ) ) + { + int i, tmp; + + G_NobuildSave( ); + + tmp = 0; + for( i = 0; i < MAX_GENTITIES; i++ ) + { + if( level.nbMarkers[ i ].Marker == NULL ) + continue; + + tmp++; + } + ADMP( va( "^3!nobuild:^7 %i nobuild markers have been saved\n", tmp ) ); + return qtrue; + } + return qfalse; +} + +qboolean G_admin_nextmap( gentity_t *ent, int skiparg ) +{ + AP( va( "print \"^3!nextmap: ^7%s^7 decided to load the next map\n\"", + ( ent ) ? G_admin_adminPrintName( ent ) : "console" ) ); + level.lastWin = PTE_NONE; + trap_SetConfigstring( CS_WINNER, "Evacuation" ); + LogExit( va( "nextmap was run by %s", + ( ent ) ? G_admin_adminPrintName( ent ) : "console" ) ); + G_admin_maplog_result( "N" ); + return qtrue; +} + +qboolean G_admin_namelog( gentity_t *ent, int skiparg ) +{ + int i, j; + char search[ MAX_NAME_LENGTH ] = {""}; + char s2[ MAX_NAME_LENGTH ] = {""}; + char n2[ MAX_NAME_LENGTH ] = {""}; + char guid_stub[ 9 ]; + qboolean found = qfalse; + int printed = 0; + + if( G_SayArgc() > 1 + skiparg ) + { + G_SayArgv( 1 + skiparg, search, sizeof( search ) ); + G_SanitiseString( search, s2, sizeof( s2 ) ); + } + ADMBP_begin(); + for( i = 0; i < MAX_ADMIN_NAMELOGS && g_admin_namelog[ i ]; i++ ) + { + if( search[0] ) + { + found = qfalse; + for( j = 0; j < MAX_ADMIN_NAMELOG_NAMES && + g_admin_namelog[ i ]->name[ j ][ 0 ]; j++ ) + { + G_SanitiseString( g_admin_namelog[ i ]->name[ j ], n2, sizeof( n2 ) ); + if( strstr( n2, s2 ) ) + { + found = qtrue; + break; + } + } + if( !found ) + continue; + } + printed++; + for( j = 0; j < 8; j++ ) + guid_stub[ j ] = g_admin_namelog[ i ]->guid[ j + 24 ]; + guid_stub[ j ] = '\0'; + if( g_admin_namelog[ i ]->slot > -1 ) + ADMBP( "^3" ); + ADMBP( va( "%-2s (*%s) %15s^7", + (g_admin_namelog[ i ]->slot > -1 ) ? + va( "%d", g_admin_namelog[ i ]->slot ) : "-", + guid_stub, g_admin_namelog[ i ]->ip ) ); + for( j = 0; j < MAX_ADMIN_NAMELOG_NAMES && + g_admin_namelog[ i ]->name[ j ][ 0 ]; j++ ) + { + ADMBP( va( " '%s^7'", g_admin_namelog[ i ]->name[ j ] ) ); + } + ADMBP( "\n" ); + } + ADMBP( va( "^3!namelog:^7 %d recent clients found\n", printed ) ); + ADMBP_end(); + return qtrue; +} + +qboolean G_admin_lock( gentity_t *ent, int skiparg ) +{ + char teamName[2] = {""}; + pTeam_t team; + + if( G_SayArgc() < 2 + skiparg ) + { + ADMP( "^3!lock: ^7usage: !lock [a|h]\n" ); + return qfalse; + } + G_SayArgv( 1 + skiparg, teamName, sizeof( teamName ) ); + if( teamName[ 0 ] == 'a' || teamName[ 0 ] == 'A' ) + team = PTE_ALIENS; + else if( teamName[ 0 ] == 'h' || teamName[ 0 ] == 'H' ) + team = PTE_HUMANS; + else + { + ADMP( va( "^3!lock: ^7invalid team\"%c\"\n", teamName[0] ) ); + return qfalse; + } + + if( team == PTE_ALIENS ) + { + if( level.alienTeamLocked ) + { + ADMP( "^3!lock: ^7Alien team is already locked\n" ); + return qfalse; + } + else + level.alienTeamLocked = qtrue; + } + else if( team == PTE_HUMANS ) { + if( level.humanTeamLocked ) + { + ADMP( "^3!lock: ^7Human team is already locked\n" ); + return qfalse; + } + else + level.humanTeamLocked = qtrue; + } + + AP( va( "print \"^3!lock: ^7%s team has been locked by %s\n\"", + ( team == PTE_ALIENS ) ? "Alien" : "Human", + ( ent ) ? G_admin_adminPrintName( ent ) : "console" ) ); + return qtrue; +} + +qboolean G_admin_unlock( gentity_t *ent, int skiparg ) +{ + char teamName[2] = {""}; + pTeam_t team; + + if( G_SayArgc() < 2 + skiparg ) + { + ADMP( "^3!unlock: ^7usage: !unlock [a|h]\n" ); + return qfalse; + } + G_SayArgv( 1 + skiparg, teamName, sizeof( teamName ) ); + if( teamName[ 0 ] == 'a' || teamName[ 0 ] == 'A' ) + team = PTE_ALIENS; + else if( teamName[ 0 ] == 'h' || teamName[ 0 ] == 'H' ) + team = PTE_HUMANS; + else + { + ADMP( va( "^3!unlock: ^7invalid team\"%c\"\n", teamName[0] ) ); + return qfalse; + } + + if( team == PTE_ALIENS ) + { + if( !level.alienTeamLocked ) + { + ADMP( "^3!unlock: ^7Alien team is not currently locked\n" ); + return qfalse; + } + else + level.alienTeamLocked = qfalse; + } + else if( team == PTE_HUMANS ) { + if( !level.humanTeamLocked ) + { + ADMP( "^3!unlock: ^7Human team is not currently locked\n" ); + return qfalse; + } + else + level.humanTeamLocked = qfalse; + } + + AP( va( "print \"^3!unlock: ^7%s team has been unlocked by %s\n\"", + ( team == PTE_ALIENS ) ? "Alien" : "Human", + ( ent ) ? G_admin_adminPrintName( ent ) : "console" ) ); + return qtrue; +} + +qboolean G_admin_designate( gentity_t *ent, int skiparg ) +{ + int pids[ MAX_CLIENTS ]; + char name[ MAX_NAME_LENGTH ], err[ MAX_STRING_CHARS ]; + char command[ MAX_ADMIN_CMD_LEN ], *cmd; + gentity_t *vic; + + if( G_SayArgc() < 2 + skiparg ) + { + ADMP( "^3!designate: ^7usage: designate [name|slot#]\n" ); + return qfalse; + } + G_SayArgv( skiparg, command, sizeof( command ) ); + cmd = command; + if( cmd && *cmd == '!' ) + cmd++; + G_SayArgv( 1 + skiparg, name, sizeof( name ) ); + if( G_ClientNumbersFromString( name, pids ) != 1 ) + { + G_MatchOnePlayer( pids, err, sizeof( err ) ); + ADMP( va( "^3!designate: ^7%s\n", err ) ); + return qfalse; + } + if( !admin_higher( ent, &g_entities[ pids[ 0 ] ] ) && + !Q_stricmp( cmd, "undesignate" ) ) + { + ADMP( "^3!designate: ^7sorry, but your intended victim has a higher admin" + " level than you\n" ); + return qfalse; + } + vic = &g_entities[ pids[ 0 ] ]; + if( vic->client->pers.designatedBuilder == qtrue ) + { + if( !Q_stricmp( cmd, "designate" ) ) + { + ADMP( "^3!designate: ^7player is already designated builder\n" ); + return qtrue; + } + vic->client->pers.designatedBuilder = qfalse; + CPx( pids[ 0 ], "cp \"^1Your designation has been revoked\"" ); + AP( va( + "print \"^3!designate: ^7%s^7's designation has been revoked by %s\n\"", + vic->client->pers.netname, + ( ent ) ? G_admin_adminPrintName( ent ) : "console" ) ); + G_CheckDBProtection( ); + } + else + { + if( !Q_stricmp( cmd, "undesignate" ) ) + { + ADMP( "^3!undesignate: ^7player is not currently designated builder\n" ); + return qtrue; + } + vic->client->pers.designatedBuilder = qtrue; + CPx( pids[ 0 ], "cp \"^1You've been designated\"" ); + AP( va( "print \"^3!designate: ^7%s^7 has been designated by ^7%s\n\"", + vic->client->pers.netname, + ( ent ) ? G_admin_adminPrintName( ent ) : "console" ) ); + } + return qtrue; +} + + //!Warn by Gate (Daniel Evans) +qboolean G_admin_warn( gentity_t *ent, int skiparg ) +{//mostly copy and paste with the proper lines altered from !mute and !kick + int pids[ MAX_CLIENTS ]; + char name[ MAX_NAME_LENGTH ], *reason, err[ MAX_STRING_CHARS ]; + int minargc; + gentity_t *vic; + + minargc = 3 + skiparg; + if( G_admin_permission( ent, ADMF_UNACCOUNTABLE ) ) + minargc = 2 + skiparg; + + if( G_SayArgc() < minargc ) + { + ADMP( "^3!warn: ^7usage: warn [name] [reason]\n" ); + return qfalse; + } + G_SayArgv( 1 + skiparg, name, sizeof( name ) ); + reason = G_SayConcatArgs( 2 + skiparg ); + if( G_ClientNumbersFromString( name, pids ) != 1 ) + { + G_MatchOnePlayer( pids, err, sizeof( err ) ); + ADMP( va( "^3!warn: ^7%s\n", err ) ); + return qfalse; + } + if( !admin_higher( ent, &g_entities[ pids[ 0 ] ] ) ) + { + ADMP( "^3!warn: ^7sorry, but your intended victim has a higher admin" + " level than you.\n" ); + return qfalse; + } + + vic = &g_entities[ pids[ 0 ] ]; + //next line is the onscreen warning + CPx( pids[ 0 ],va("cp \"^1You have been warned by an administrator.\n ^3Cease immediately or face admin action!\n^1 %s%s\"",(*reason)? "REASON: " : "" ,(*reason)? reason : "") ); + AP( va( "print \"^3!warn: ^7%s^7 has been warned to cease and desist: %s by %s \n\"", + vic->client->pers.netname, (*reason) ? reason : "his current activity", + ( ent ) ? G_admin_adminPrintName( ent ) : "console" ) );//console announcement + return qtrue; +} + +qboolean G_admin_putmespec( gentity_t *ent, int skiparg ) +{ + if( !ent ) + { + ADMP( "!specme: sorry, but console isn't allowed on the spectators team\n"); + return qfalse; + } + + if( level.paused ) + { + ADMP("!specme: disabled when game is paused\n"); + return qfalse; + } + + if(ent->client->pers.teamSelection == PTE_NONE) + return qfalse; + + //guard against build timer exploit + if( ent->client->pers.teamSelection != PTE_NONE && ent->client->sess.sessionTeam != TEAM_SPECTATOR && + ( ent->client->ps.stats[ STAT_PCLASS ] == PCL_ALIEN_BUILDER0 || + ent->client->ps.stats[ STAT_PCLASS ] == PCL_ALIEN_BUILDER0_UPG || + BG_InventoryContainsWeapon( WP_HBUILD, ent->client->ps.stats ) || + BG_InventoryContainsWeapon( WP_HBUILD2, ent->client->ps.stats ) ) && + ent->client->ps.stats[ STAT_MISC ] > 0 ) + { + ADMP("!specme: You cannot leave your team until the build timer expires"); + return qfalse; + } + + G_ChangeTeam( ent, PTE_NONE ); + AP( va("print \"^3!specme: ^7%s^7 decided to join the spectators\n\"", ent->client->pers.netname ) ); + return qtrue; +} + +qboolean G_admin_slap( gentity_t *ent, int skiparg ) +{ + int pids[ MAX_CLIENTS ]; + char name[ MAX_NAME_LENGTH ], err[ MAX_STRING_CHARS ]; + gentity_t *vic; + vec3_t dir; + + if( level.intermissiontime ) return qfalse; + + if( G_SayArgc() < 2 + skiparg ) + { + ADMP( "^3!slap: ^7usage: !slap [name|slot#]\n" ); + return qfalse; + } + + G_SayArgv( 1 + skiparg, name, sizeof( name ) ); + if( G_ClientNumbersFromString( name, pids ) != 1 ) + { + G_MatchOnePlayer( pids, err, sizeof( err ) ); + ADMP( va( "^3!slap: ^7%s\n", err ) ); + return qfalse; + } + + vic = &g_entities[ pids[ 0 ] ]; + if( !vic ) + { + ADMP( "^3!slap: ^7bad target\n" ); + return qfalse; + } + if( vic == ent ) + { + ADMP( "^3!slap: ^7sorry, you cannot slap yourself\n" ); + return qfalse; + } + if( !admin_higher( ent, vic ) ) + { + ADMP( "^3!slap: ^7sorry, but your intended victim has a higher admin" + " level than you\n" ); + return qfalse; + } + if( vic->client->pers.teamSelection == PTE_NONE || + vic->client->pers.classSelection == PCL_NONE ) + { + ADMP( "^3!slap: ^7can't slap spectators\n" ); + return qfalse; + } + + // knockback in a random direction + dir[0] = crandom(); + dir[1] = crandom(); + dir[2] = random(); + G_Knockback( vic, dir, g_slapKnockback.integer ); + + trap_SendServerCommand( vic-g_entities, + va( "cp \"%s^7 is not amused\n\"", + ( ent ) ? G_admin_adminPrintName( ent ) : "console" ) ); + + if( g_slapDamage.integer > 0 ) + { + int damage; + + if( G_SayArgc() > 2 + skiparg ) + { + char dmg_str[ MAX_STRING_CHARS ]; + G_SayArgv( 2 + skiparg, dmg_str, sizeof( dmg_str ) ); + damage = atoi(dmg_str); + if( damage < 0 ) damage = 0; + } + else + { + if( g_slapDamage.integer > 100 ) g_slapDamage.integer = 100; + damage = BG_FindHealthForClass( vic->client->ps.stats[ STAT_PCLASS ] ) * + g_slapDamage.integer / 100; + if( damage < 1 ) damage = 1; + } + + vic->health -= damage; + vic->client->ps.stats[ STAT_HEALTH ] = vic->health; + vic->lastDamageTime = level.time; + if( vic->health <= 0 ) + { + vic->flags |= FL_NO_KNOCKBACK; + vic->enemy = &g_entities[ pids[ 0 ] ]; + vic->die( vic, ent, ent, damage, MOD_SLAP ); + } + else if( vic->pain ) + { + vic->pain( vic, &g_entities[ pids[ 0 ] ], damage ); + } + } + return qtrue; +} + +qboolean G_admin_drop( gentity_t *ent, int skiparg ) +{ + int pids[ MAX_CLIENTS ]; + char name[ MAX_NAME_LENGTH ], err[ MAX_STRING_CHARS ]; + + if( G_SayArgc() < 2 + skiparg ) + { + ADMP( "^3!drop: ^7usage: !drop [name|slot#] [message]\n" ); + return qfalse; + } + + G_SayArgv( 1 + skiparg, name, sizeof( name ) ); + if( G_ClientNumbersFromString( name, pids ) != 1 ) + { + G_MatchOnePlayer( pids, err, sizeof( err ) ); + ADMP( va( "^3!drop: ^7%s\n", err ) ); + return qfalse; + } + + if( !admin_higher( ent, &g_entities[ pids[ 0 ] ] ) ) + { + ADMP( "^3!drop: ^7sorry, but your intended victim has a higher admin" + " level than you\n" ); + return qfalse; + } + + // victim's message + if( G_SayArgc() > 2 + skiparg ) + trap_SendServerCommand( pids[ 0 ], + va( "disconnect \"You have been dropped.\n%s^7\n\"", + G_SayConcatArgs( 2 + skiparg ) ) ); + else + trap_SendServerCommand( pids[ 0 ], va( "disconnect" ) ); + + // server message + trap_DropClient( pids[ 0 ], va( "disconnected" ) ); + + return qtrue; +} + +qboolean G_admin_buildlog( gentity_t *ent, int skiparg ) +{ +#define LOG_DISPLAY_LENGTH 10 + buildHistory_t *ptr; + gentity_t *builder = NULL; + int skip = 0, start = 0, lastID = -1, firstID = -1, i, len, matchlen = 0; + pTeam_t team = PTE_NONE; + char message[ MAX_STRING_CHARS ], *teamchar; + char *name, *action, *buildablename, markstring[ MAX_STRING_CHARS ]; + if( !g_buildLogMaxLength.integer ) + { + ADMP( "^3!buildlog: ^7build logging is disabled" ); + return qfalse; + } + if( G_SayArgc( ) >= 2 + skiparg ) + { + for( i = 1; i + skiparg < G_SayArgc( ); i++ ) + { + char argbuf[ 64 ], err[ MAX_STRING_CHARS ]; + int x = 0, pids[ MAX_CLIENTS ]; + G_SayArgv( i + skiparg, argbuf, sizeof argbuf ); + switch( argbuf[ 0 ]) + { + case 'x': + x = 1; + default: + skip = atoi( argbuf + x ); + start = 0; + break; + case '#': + start = atoi( argbuf + 1 ); + skip = 0; + break; + case '-': + if(G_ClientNumbersFromString(argbuf + 1, pids) != 1) + { + G_MatchOnePlayer(pids, err, sizeof(err)); + ADMP(va("^3!revert: ^7%s\n", err)); + return qfalse; + } + builder = g_entities + *pids; + break; + case 'A': + case 'a': + team = PTE_ALIENS; + break; + case 'H': + case 'h': + team = PTE_HUMANS; + break; + } + } + } + // !buildlog can be abused, so let everyone know when it is used + AP( va( "print \"^3!buildlog: ^7%s^7 requested a log of recent building" + " activity\n\"", ( ent ) ? G_admin_adminPrintName( ent ) : "console" ) ); + len = G_CountBuildLog( ); // also clips the log if too long + if( !len ) + { + ADMP( "^3!buildlog: ^7no build log found\n" ); + return qfalse; + } + if( start ) + { + // set skip based on start + for( ptr = level.buildHistory; ptr && ptr->ID != start; + ptr = ptr->next, skip++ ); + if( !ptr ) + { + ADMP( "^3!buildlog: ^7log ID not found\n" ); + skip = 0; + } + } + // ensure skip is a useful value + if( skip > len - LOG_DISPLAY_LENGTH ) + skip = len - LOG_DISPLAY_LENGTH; + *message = '\0'; + // skip to start entry + for( ptr = level.buildHistory, i = len; ptr && i > len - skip; + ptr = ptr->next ) + { + // these checks could perhaps be done more efficiently but they are cheap + // in processor time so I'm not worrying + if( team != PTE_NONE && team != BG_FindTeamForBuildable( ptr->buildable ) ) + continue; + if( builder && builder != ptr->ent ) + continue; + matchlen++; + i--; + } + for( ; i + LOG_DISPLAY_LENGTH > len - skip && i > 0; i--, ptr = ptr->next ) + { + if( !ptr ) + break; // run out of log + *markstring = '\0'; // reinit markstring + // check team + if( ( team != PTE_NONE && team != BG_FindTeamForBuildable( ptr->buildable ) ) + || ( builder && builder != ptr->ent ) ) + { + skip++; // loop an extra time because we skipped one + continue; + } + if( lastID < 0 ) + lastID = ptr->ID; + firstID = ptr->ID; + matchlen++; + // set name to the ent's current name or last recorded name + if( ptr->ent ) + { + if( ptr->ent->client ) + name = ptr->ent->client->pers.netname; + else + name = "<world>"; // any non-client action + } + else + name = ptr->name; + switch( ptr->fate ) + { + case BF_BUILT: + action = "^2built^7 a"; + break; + case BF_DECONNED: + action = "^3DECONSTRUCTED^7 a"; + break; + case BF_DESTROYED: + action = "destroyed a"; + break; + case BF_TEAMKILLED: + action = "^1TEAMKILLED^7 a"; + break; + default: + action = "\0"; // erm + break; + } + // handle buildables removed by markdecon + if( ptr->marked ) + { + buildHistory_t *mark; + int j, markdecon[ BA_NUM_BUILDABLES ], and = 2; + char bnames[ 32 ], *article; + mark = ptr; + // count the number of buildables + memset( markdecon, 0, sizeof( markdecon ) ); + while( ( mark = mark->marked ) ) + markdecon[ mark->buildable ]++; + // reverse order makes grammar easier + for( j = BA_NUM_BUILDABLES; j >= 0; j-- ) + { + buildablename = BG_FindHumanNameForBuildable( j ); + // plural is easy + if( markdecon[ j ] > 1 ) + Com_sprintf( bnames, 32, "%d %ss", markdecon[ j ], buildablename ); + // use an appropriate article + else if( markdecon[ j ] == 1 ) + { + if( BG_FindUniqueTestForBuildable( j ) ) + article = "the"; // if only one + else if( strchr( "aeiouAEIOU", *buildablename ) ) + article = "an"; // if first char is vowel + else + article = "a"; + Com_sprintf( bnames, 32, "%s %s", article, buildablename ); + } + else + continue; // none of this buildable + // C grammar: x, y, and z + // the integer and is 2 initially, the test means it is used on the + // second sprintf only, the reverse order makes this second to last + // the comma is only printed if there is already some markstring i.e. + // not the first time ( which would put it on the end of the string ) + Com_sprintf( markstring, sizeof( markstring ), "%s%s %s%s", bnames, + ( *markstring ) ? "," : "", ( and-- == 1 ) ? "and " : "", markstring ); + } + } + buildablename = BG_FindHumanNameForBuildable( ptr->buildable ); + switch( BG_FindTeamForBuildable( ptr->buildable ) ) + { + case PTE_ALIENS: + teamchar = "^1A"; + break; + case PTE_HUMANS: + teamchar = "^4H"; + break; + default: + teamchar = " "; // space so it lines up neatly + break; + } + // prepend the information to the string as we go back in buildhistory + // so the earliest events are at the top + Com_sprintf( message, MAX_STRING_CHARS, "%3d %s^7 %s^7 %s%s %s%s%s\n%s", + ptr->ID, teamchar, name, action, + ( strchr( "aeiouAEIOU", buildablename[ 0 ] ) ) ? "n" : "", + buildablename, ( markstring[ 0 ] ) ? ", removing " : "", + markstring, message ); + } + for( ; ptr; ptr = ptr->next ) + { + if( builder && builder != ptr->ent ) + continue; + if( team != PTE_NONE && team != BG_FindTeamForBuildable( ptr->buildable ) ) + continue; + matchlen++; + } + if( matchlen ) + ADMP( va( "%s^3!buildlog: showing log entries %d - %d of %d\n", message, + firstID, lastID, matchlen ) ); + else + ADMP( "^3!buildlog: ^7no log entries match those criteria\n" ); + return qtrue; +} + +qboolean G_admin_revert( gentity_t *ent, int skiparg ) +{ + int i = 0, j = 0, repeat = 1, ID = 0, len, matchlen=0; + pTeam_t team = PTE_NONE; + qboolean force = qfalse, reached = qfalse; + gentity_t *builder = NULL, *targ; + buildHistory_t *ptr, *tmp, *mark, *prev; + vec3_t dist; + char argbuf[ 64 ], *name, *bname, *action, *article; + len = G_CountBuildLog( ); + if( !len ) + { + ADMP( "^3!revert: ^7no build log found\n" ); + return qfalse; + } + if( G_SayArgc( ) < 2 + skiparg ) + { + ADMP( "^3!revert: ^7usage: !revert (^5xnum^7) (^5#ID^7) (^5-name|num^7) (^5a|h^7)\n" ); + return qfalse; + } + for( i = 1; i + skiparg < G_SayArgc( ); i++ ) + { + char arg[ 64 ], err[ MAX_STRING_CHARS ]; + int pids[ MAX_CLIENTS ]; + G_SayArgv( i + skiparg, arg, sizeof arg ); + switch( arg[ 0 ]) + { + case 'x': + repeat = atoi( arg + 1 ); + break; + case '#': + ID = atoi( arg + 1 ); + break; + case '-': + if(G_ClientNumbersFromString(arg + 1, pids) != 1) + { + G_MatchOnePlayer(pids, err, sizeof err); + ADMP(va("^3!revert: ^7%s\n", err)); + return qfalse; + } + builder = g_entities + *pids; + break; + case 'A': + case 'a': + team = PTE_ALIENS; + break; + case 'H': + case 'h': + team = PTE_HUMANS; + break; + case '!': + force = qtrue; + break; + default: + ADMP( "^3!revert: ^7usage: !revert (^5xnum^7) (^5#ID^7) (^5-name|num^7) (^5a|h^7)\n" ); + return qfalse; + } + } + if( repeat > 25 ) + { + ADMP( "^3!revert: ^7to avoid flooding, can only revert 25 builds at a time\n" ); + repeat = 25; + } + for( i = 0, ptr = prev = level.buildHistory; repeat > 0; repeat--, j = 0 ) + { + if( !ptr ) + break; // run out of bhist + if( !reached && ID ) + { + if( ptr->ID == ID ) + reached = qtrue; + else + { + prev = ptr; + ptr = ptr->next; + repeat++; + continue; + } + } + if( ( team != PTE_NONE && + team != BG_FindTeamForBuildable( ptr->buildable ) ) || + ( builder && builder != ptr->ent )) + { + // team doesn't match, so skip this ptr and reset prev + prev = ptr; + ptr = ptr->next; + // we don't want to count this one so counteract the decrement by the for + repeat++; + continue; + } + // get the ent's current or last recorded name + if( ptr->ent ) + { + if( ptr->ent->client ) + name = ptr->ent->client->pers.netname; + else + name = "<world>"; // non-client actions + } + else + name = ptr->name; + bname = BG_FindHumanNameForBuildable( ptr->buildable ); + action = ""; + switch( ptr->fate ) + { + case BF_BUILT: + action = "^2build^7"; + for( j = MAX_CLIENTS, targ = g_entities + j; + j < level.num_entities; j++, targ++ ) + { + // easy checks first to save time + if( targ->s.eType != ET_BUILDABLE ) + continue; + if( targ->s.modelindex != ptr->buildable ) + continue; + VectorSubtract( targ->s.pos.trBase, ptr->origin, dist ); +#define FIND_BUILDABLE_TOLERANCE 5 + if( VectorLength( dist ) > FIND_BUILDABLE_TOLERANCE ) + continue; // number is somewhat arbitrary, watch for false pos/neg + // if we didn't continue then it's this one, unlink it but we can't + // free it yet, because the markdecon buildables might not place + trap_UnlinkEntity( targ ); + break; + } + // if there are marked buildables to replace, and we aren't overriding + // space check, check they can fit before acting + if( ptr->marked && !force ) + { + for( mark = ptr->marked; mark; mark = mark->marked ) + if( !G_RevertCanFit( mark ) ) + { + trap_LinkEntity( targ ); // put it back, we failed + // scariest sprintf ever: + Com_sprintf( argbuf, sizeof argbuf, "%s%s%s%s%s%s%s!", + ( repeat > 1 ) ? "x" : "", ( repeat > 1 ) ? va( "%d ", repeat ) : "", + ( ID ) ? "#" : "", ( ID ) ? va( "%d ", ptr->ID ) : "", + ( builder ) ? "-" : "", ( builder ) ? va( "%d ", builder - g_entities ) : "", + ( team == PTE_ALIENS ) ? "a " : ( team == PTE_HUMANS ) ? "h " : "" ); + ADMP( va( "^3!revert: ^7revert aborted: reverting this %s would conflict with " + "another buildable, use ^3!revert %s ^7to override\n", action, argbuf ) ); + return qfalse; + } + } + // Prevent teleport glitch when reverting an occupied hovel + if( targ->s.modelindex == BA_A_HOVEL && + targ->active ) + { + gentity_t *builder = targ->builder; + vec3_t newOrigin; + vec3_t newAngles; + + VectorCopy( targ->s.angles, newAngles ); + newAngles[ ROLL ] = 0; + + VectorCopy( targ->s.origin, newOrigin ); + VectorMA( newOrigin, 1.0f, targ->s.origin2, newOrigin ); + + //prevent lerping + builder->client->ps.eFlags ^= EF_TELEPORT_BIT; + builder->client->ps.eFlags &= ~EF_NODRAW; + G_UnlaggedClear( builder ); + + G_SetOrigin( builder, newOrigin ); + VectorCopy( newOrigin, builder->client->ps.origin ); + G_SetClientViewAngle( builder, newAngles ); + + //client leaves hovel + builder->client->ps.stats[ STAT_STATE ] &= ~SS_HOVELING; + } + + // if we haven't returned yet then we're good to go, free it + G_FreeEntity( targ ); + // put the marked buildables back and mark them again + if( ptr->marked ) // there may be a more efficient way of doing this + { + for( mark = ptr->marked; mark; mark = mark->marked ) + G_SpawnRevertedBuildable( mark, qtrue ); + } + break; + case BF_DECONNED: + if( !action[ 0 ] ) action = "^3deconstruction^7"; + case BF_TEAMKILLED: + if( !action[ 0 ] ) action ="^1TEAMKILL^7"; + case BF_DESTROYED: + if( !action[ 0 ] ) action = "destruction"; + // if we're not overriding and the replacement can't fit, as before + if( !force && !G_RevertCanFit( ptr ) ) + { + Com_sprintf( argbuf, sizeof argbuf, "%s%s%s%s%s%s%s!", + ( repeat > 1 ) ? "x" : "", ( repeat > 1 ) ? va( "%d ", repeat ) : "", + ( ID ) ? "#" : "", ( ID ) ? va( "%d ", ptr->ID ) : "", + ( builder ) ? "-" : "", ( builder ) ? va( "%d ", builder - g_entities ) : "", + ( team == PTE_ALIENS ) ? "a " : ( team == PTE_HUMANS ) ? "h " : "" ); + ADMP( va( "^3!revert: ^7revert aborted: reverting this %s would " + "conflict with another buildable, use ^3!revert %s ^7to override\n", + action, argbuf ) ); + return qfalse; + } + // else replace it but don't mark it ( it might have been marked before + // but it isn't that important ) + G_SpawnRevertedBuildable( ptr, qfalse ); + break; + default: + // if this happens something has gone wrong + ADMP( "^3!revert: ^7incomplete or corrupted build log entry\n" ); + /* quarantine and dispose of the log, it's dangerous + trap_Cvar_Set( "g_buildLogMaxLength", "0" ); + G_CountBuildLog( ); + */ + return qfalse; + } + if( j == level.num_entities ) + { + ADMP( va( "^3!revert: ^7could not find logged buildable #%d\n", ptr->ID )); + prev = ptr; + ptr = ptr->next; + continue; + } + // this is similar to the buildlog stuff + if( BG_FindUniqueTestForBuildable( ptr->buildable ) ) + article = "the"; + else if( strchr( "aeiouAEIOU", *bname ) ) + article = "an"; + else + article = "a"; + AP( va( "print \"%s^7 reverted %s^7'%s %s of %s %s\n\"", + ( ent ) ? G_admin_adminPrintName( ent ) : "console", + name, strchr( "Ss", name[ strlen( name ) - 1 ] ) ? "" : "s", + action, article, bname ) ); + matchlen++; + // remove the reverted entry + // ptr moves on, prev just readjusts ->next unless it is about to be + // freed, in which case it is forced to move on too + tmp = ptr; + if( ptr == level.buildHistory ) + prev = level.buildHistory = ptr = ptr->next; + else + prev->next = ptr = ptr->next; + G_Free( tmp ); + } + if( ID && !reached ) + { + ADMP( "^3!revert: ^7no buildlog entry with that ID\n" ); + return qfalse; + } + + if( !matchlen ) + { + ADMP( "^3!revert: ^7no log entries match those criteria\n" ); + return qfalse; + } + else + { + ADMP( va( "^3!revert: ^7reverted %d buildlog events\n", matchlen ) ); + } + + return qtrue; +} + +void G_Unescape( char *input, char *output, int len ); +qboolean G_StringReplaceCvars( char *input, char *output, int len ); + +qboolean G_admin_info( gentity_t *ent, int skiparg ) +{ + fileHandle_t infoFile; + int length; + char filename[ MAX_OSPATH ], message[ MAX_STRING_CHARS ]; + if( G_SayArgc() == 2 + skiparg ) + G_SayArgv( 1 + skiparg, filename, sizeof( filename ) ); + else if( G_SayArgc() == 1 + skiparg ) + Q_strncpyz( filename, "default", sizeof( filename ) ); + else + { + ADMP( "^3!info: ^7usage: ^3!info ^7(^5subject^7)\n" ); + return qfalse; + } + Com_sprintf( filename, sizeof( filename ), "info/info-%s.txt", filename ); + length = trap_FS_FOpenFile( filename, &infoFile, FS_READ ); + if( length <= 0 || !infoFile ) + { + trap_FS_FCloseFile( infoFile ); + ADMP( "^3!info: ^7no relevant information is available\n" ); + return qfalse; + } + else + { + int i; + char *cr; + trap_FS_Read( message, sizeof( message ), infoFile ); + if( length < sizeof( message ) ) + message[ length ] = '\0'; + else + message[ sizeof( message ) - 1 ] = '\0'; + trap_FS_FCloseFile( infoFile ); + // strip carriage returns for windows platforms + while( ( cr = strchr( message, '\r' ) ) ) + memmove( cr, cr + 1, strlen( cr + 1 ) + 1 ); +#define MAX_INFO_PARSE_LOOPS 100 + for( i = 0; i < MAX_INFO_PARSE_LOOPS && + G_StringReplaceCvars( message, message, sizeof( message ) ); i++ ); + G_Unescape( message, message, sizeof( message ) ); + if( i == MAX_INFO_PARSE_LOOPS ) + G_Printf( S_COLOR_YELLOW "WARNING: %s exceeds MAX_INFO_PARSE_LOOPS\n", filename ); + ADMP( va( "%s\n", message ) ); + return qtrue; + } +} + +void G_Unescape( char *input, char *output, int len ) +{ + // \n -> newline, \%c -> %c + // output is terminated at output[len - 1] + // it's OK for input to equal output, because our position in input is always + // equal or greater than our position in output + // however, if output is later in the same string as input, a crash is pretty + // much inevitable + int i, j; + for( i = j = 0; input[i] && j + 1 < len; i++, j++ ) + { + if( input[i] == '\\' ) + { + if( !input[++i] ) + { + output[j] = '\0'; + return; + } + else if( input[i] == 'n' ) + output[j] = '\n'; + else + output[j] = input[i]; + } + else + output[j] = input[i]; + } + output[j] = '\0'; +} + +qboolean G_StringReplaceCvars( char *input, char *output, int len ) +{ + int i, outNum = 0; + char cvarName[ 64 ], cvarValue[ MAX_CVAR_VALUE_STRING ]; + char *outputBuffer; + qboolean doneAnything = qfalse; + if( len <= 0 ) + return qfalse; + // use our own internal buffer in case output == input + outputBuffer = G_Alloc( len ); + len -= 1; // fit in a terminator + while( *input && outNum < len ) + { + if( *input == '\\' && input[1] && outNum < len - 1 ) + { + outputBuffer[ outNum++ ] = *input++; + outputBuffer[ outNum++ ] = *input++; + } + else if( *input == '$' ) + { + doneAnything = qtrue; + input++; + if( *input == '{' ) + input++; + for( i = 0; *input && ( isalnum( *input ) || *input == '_' ) && + i < 63; i++ ) + cvarName[ i ] = *input++; + cvarName[ i ] = '\0'; + if( *input == '}' ) + input++; + trap_Cvar_VariableStringBuffer( cvarName, cvarValue, sizeof( cvarValue ) ); + if( cvarValue[ 0 ] ) + { + for( i = 0; cvarValue[ i ] && outNum < len; i++ ) + outputBuffer[ outNum++ ] = cvarValue[ i ]; + } + } + else + outputBuffer[ outNum++ ] = *input++; + } + outputBuffer[ outNum ] = '\0'; + Q_strncpyz( output, outputBuffer, len ); + G_Free( outputBuffer ); + return doneAnything; +} + +/* +================ + G_admin_print + + This function facilitates the ADMP define. ADMP() is similar to CP except + that it prints the message to the server console if ent is not defined. +================ +*/ +void G_admin_print( gentity_t *ent, char *m ) +{ + if( ent ) + trap_SendServerCommand( ent - level.gentities, va( "print \"%s\"", m ) ); + else + { + char m2[ MAX_STRING_CHARS ]; + if( !trap_Cvar_VariableIntegerValue( "com_ansiColor" ) ) + { + G_DecolorString( m, m2 ); + G_Printf( m2 ); + } + else + G_Printf( m ); + } +} + +void G_admin_buffer_begin() +{ + g_bfb[ 0 ] = '\0'; +} + +void G_admin_buffer_end( gentity_t *ent ) +{ + ADMP( g_bfb ); +} + +void G_admin_buffer_print( gentity_t *ent, char *m ) +{ + // 1022 - strlen("print 64 \"\"") - 1 + if( strlen( m ) + strlen( g_bfb ) >= 1009 ) + { + ADMP( g_bfb ); + g_bfb[ 0 ] = '\0'; + } + Q_strcat( g_bfb, sizeof( g_bfb ), m ); +} + + +void G_admin_cleanup() +{ + int i = 0; + + for( i = 0; i < MAX_ADMIN_LEVELS && g_admin_levels[ i ]; i++ ) + { + G_Free( g_admin_levels[ i ] ); + g_admin_levels[ i ] = NULL; + } + for( i = 0; i < MAX_ADMIN_ADMINS && g_admin_admins[ i ]; i++ ) + { + G_Free( g_admin_admins[ i ] ); + g_admin_admins[ i ] = NULL; + } + for( i = 0; i < MAX_ADMIN_BANS && g_admin_bans[ i ]; i++ ) + { + G_Free( g_admin_bans[ i ] ); + g_admin_bans[ i ] = NULL; + } + for( i = 0; i < MAX_ADMIN_COMMANDS && g_admin_commands[ i ]; i++ ) + { + G_Free( g_admin_commands[ i ] ); + g_admin_commands[ i ] = NULL; + } +} + +qboolean G_admin_L0(gentity_t *ent, int skiparg ){ + int pids[ MAX_CLIENTS ]; + char name[ MAX_NAME_LENGTH ] = {""}; + char testname[ MAX_NAME_LENGTH ] = {""}; + char err[ MAX_STRING_CHARS ]; + qboolean numeric = qtrue; + int i; + int id = -1; + gentity_t *vic; + + if( G_SayArgc() < 2 + skiparg ) + { + ADMP( "^3!L0: ^7usage: !L0 [name|slot#|admin#]\n" ); + return qfalse; + } + G_SayArgv( 1 + skiparg, testname, sizeof( testname ) ); + G_SanitiseString( testname, name, sizeof( name ) ); + for( i = 0; i < sizeof( name ) && name[ i ] ; i++ ) + { + if( name[ i ] < '0' || name[ i ] > '9' ) + { + numeric = qfalse; + break; + } + } + + if( numeric ) + { + id = atoi( name ); + } + else + { + if( G_ClientNumbersFromString( name, pids ) != 1 ) + { + G_MatchOnePlayer( pids, err, sizeof( err ) ); + ADMP( va( "^3!L0: ^7%s\n", err ) ); + return qfalse; + } + id = pids[ 0 ]; + } + + if (id >= 0 && id < level.maxclients) + { + vic = &g_entities[ id ]; + if( !vic || !(vic->client) || vic->client->pers.connected != CON_CONNECTED ) + { + ADMP( "^3!L0:^7 no one connected by that slot number\n" ); + return qfalse; + } + + if( G_admin_level( vic ) != 1 ) + { + ADMP( "^3!L0:^7 intended victim is not level 1\n" ); + return qfalse; + } + } + else if (id >= MAX_CLIENTS && id < MAX_CLIENTS + MAX_ADMIN_ADMINS + && g_admin_admins[ id - MAX_CLIENTS ] ) + { + if( g_admin_admins[ id - MAX_CLIENTS ]->level != 1 ) + { + ADMP( "^3!L0:^7 intended victim is not level 1\n" ); + return qfalse; + } + } + else + { + ADMP( "^3!L0:^7 no match. use !listplayers or !listadmins to " + "find an appropriate number to use instead of name.\n" ); + return qfalse; + } + + trap_SendConsoleCommand( EXEC_APPEND, va( "!setlevel %d 0;", id ) ); + + return qtrue; +} + +qboolean G_admin_L1(gentity_t *ent, int skiparg ){ + int pids[ MAX_CLIENTS ]; + char name[ MAX_NAME_LENGTH ], *reason, err[ MAX_STRING_CHARS ]; + int minargc; + + minargc = 2 + skiparg; + + if( G_SayArgc() < minargc ) + { + ADMP( "^3!L1: ^7usage: !L1 [name]\n" ); + return qfalse; + } + G_SayArgv( 1 + skiparg, name, sizeof( name ) ); + reason = G_SayConcatArgs( 2 + skiparg ); + if( G_ClientNumbersFromString( name, pids ) != 1 ) + { + G_MatchOnePlayer( pids, err, sizeof( err ) ); + ADMP( va( "^3!L1: ^7%s\n", err ) ); + return qfalse; + } + if( G_admin_level(&g_entities[ pids[ 0 ] ] )>0 ) + { + ADMP( "^3!L1: ^7Sorry, but that person is already higher than level 0.\n" ); + return qfalse; + } + + trap_SendConsoleCommand( EXEC_APPEND,va( "!setlevel %d 1;", pids[ 0 ] ) ); + return qtrue; +} + +qboolean G_admin_invisible( gentity_t *ent, int skiparg ) +{ + if( !ent ) + { + ADMP( "!invisible: console can not become invisible.\n" ); + return qfalse; + } + + if ( ent->client->sess.invisible != qtrue ) + { + // Make the player invisible + G_ChangeTeam( ent, PTE_NONE ); + ent->client->sess.invisible = qtrue; + ClientUserinfoChanged( ent->client->pers.connection->clientNum, qfalse ); + G_admin_namelog_update( ent->client, qtrue ); + trap_SendServerCommand( -1, va( "print \"%s" S_COLOR_WHITE " disconnected\n\"", ent->client->pers.netname ) ); + } + else + { + // Make the player visible + ent->client->sess.invisible = qfalse; + ClientUserinfoChanged( ent->client->pers.connection->clientNum, qfalse ); + G_admin_namelog_update( ent->client, qfalse ); + trap_SendServerCommand( -1, va( "print \"%s" S_COLOR_WHITE " connected\n\"", ent->client->pers.netname ) ); + trap_SendServerCommand( -1, va( "print \"%s" S_COLOR_WHITE " entered the game\n\"", ent->client->pers.netname ) ); + } + return qtrue; +} + diff --git a/src/game/g_admin.h b/src/game/g_admin.h new file mode 100644 index 0000000..33103ff --- /dev/null +++ b/src/game/g_admin.h @@ -0,0 +1,269 @@ +/* +=========================================================================== +Copyright (C) 2004-2006 Tony J. White + +This file is part of Tremulous. + +Tremulous is free software; you can redistribute it +and/or modify it under the terms of the GNU General Public License as +published by the Free Software Foundation; either version 2 of the License, +or (at your option) any later version. + +Tremulous is distributed in the hope that it will be +useful, but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with Tremulous; if not, write to the Free Software +Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA +=========================================================================== +*/ + +#ifndef _G_ADMIN_H +#define _G_ADMIN_H + +#define AP(x) trap_SendServerCommand(-1, x) +#define CP(x) trap_SendServerCommand(ent-g_entities, x) +#define CPx(x, y) trap_SendServerCommand(x, y) +#define ADMP(x) G_admin_print(ent, x) +#define ADMBP(x) G_admin_buffer_print(ent, x) +#define ADMBP_begin() G_admin_buffer_begin() +#define ADMBP_end() G_admin_buffer_end(ent) + +#define MAX_ADMIN_LEVELS 32 +#define MAX_ADMIN_ADMINS 1024 +#define MAX_ADMIN_BANS 1024 +#define MAX_ADMIN_NAMELOGS 128 +#define MAX_ADMIN_NAMELOG_NAMES 5 +#define MAX_ADMIN_ADMINLOGS 128 +#define MAX_ADMIN_ADMINLOG_ARGS 50 +#define MAX_ADMIN_FLAG_LEN 20 +#define MAX_ADMIN_FLAGS 1024 +#define MAX_ADMIN_COMMANDS 64 +#define MAX_ADMIN_CMD_LEN 20 +#define MAX_ADMIN_BAN_REASON 50 +#define MAX_ADMIN_BANSUSPEND_DAYS 14 + +/* + * IMMUNITY - cannot be vote kicked, vote muted + * NOCENSORFLOOD - cannot be censored or flood protected + * TEAMCHANGEFREE - never loses credits for changing teams + * SPECALLCHAT - can see team chat as a spectator + * FORCETEAMCHANGE - can switch teams any time, regardless of balance + * UNACCOUNTABLE - does not need to specify a reason for a kick/ban + * NOVOTELIMIT - can call a vote at any time (regardless of a vote being + * disabled or voting limitations) + * CANPERMBAN - does not need to specify a duration for a ban + * TEAMCHATCMD - can run commands from team chat + * ACTIVITY - inactivity rules do not apply to them + * IMMUTABLE - admin commands cannot be used on them + * INCOGNITO - does not show up as an admin in !listplayers + * ADMINCHAT - receives and can send /a admin messages + * SEESFULLLISTPLAYERS - sees all information in !listplayers + * DBUILDER - permanent designated builder + * STEALTH - uses admin stealth + * SPECIAL - allows some special permissions (unlimited votes etc) + * SPECIALNAME - allows black text in name + * .NOCHAT - mutes a player on connect + * .NOVOTE - disallows voting by a player + * ALLFLAGS - all flags (including command flags) apply to this player + */ + + +#define ADMF_IMMUNITY "IMMUNITY" +#define ADMF_NOCENSORFLOOD "NOCENSORFLOOD" +#define ADMF_TEAMCHANGEFREE "TEAMCHANGEFREE" +#define ADMF_SPEC_ALLCHAT "SPECALLCHAT" +#define ADMF_FORCETEAMCHANGE "FORCETEAMCHANGE" +#define ADMF_UNACCOUNTABLE "UNACCOUNTABLE" +#define ADMF_NO_VOTE_LIMIT "NOVOTELIMIT" +#define ADMF_CAN_PERM_BAN "CANPERMBAN" +#define ADMF_TEAMCHAT_CMD "TEAMCHATCMD" +#define ADMF_ACTIVITY "ACTIVITY" + +#define ADMF_IMMUTABLE "IMMUTABLE" +#define ADMF_INCOGNITO "INCOGNITO" +#define ADMF_ADMINCHAT "ADMINCHAT" +#define ADMF_SEESFULLLISTPLAYERS "SEESFULLLISTPLAYERS" +#define ADMF_DBUILDER "DBUILDER" +#define ADMF_ADMINSTEALTH "STEALTH" +#define ADMF_ALLFLAGS "ALLFLAGS" + +#define ADMF_BAN_IMMUNITY "BANIMMUNITY" + +#define ADMF_SPECIAL "SPECIAL" +#define ADMF_SPECIALNAME "SPECIALNAME" + +#define ADMF_NO_CHAT ".NOCHAT" +#define ADMF_NO_VOTE ".NOVOTE" + +#define MAX_ADMIN_LISTITEMS 20 +#define MAX_ADMIN_SHOWBANS 10 +#define MAX_ADMIN_MAPLOG_LENGTH 5 + +// important note: QVM does not seem to allow a single char to be a +// member of a struct at init time. flag has been converted to char* +typedef struct +{ + char *keyword; + qboolean ( * handler ) ( gentity_t *ent, int skiparg ); + char *flag; + char *function; // used for !help + char *syntax; // used for !help +} +g_admin_cmd_t; + +typedef struct g_admin_level +{ + int level; + char name[ MAX_NAME_LENGTH ]; + char flags[ MAX_ADMIN_FLAGS ]; +} +g_admin_level_t; + +typedef struct g_admin_admin +{ + char guid[ 33 ]; + char name[ MAX_NAME_LENGTH ]; + int level; + char flags[ MAX_ADMIN_FLAGS ]; + int seen; +} +g_admin_admin_t; + +typedef struct g_admin_ban +{ + char name[ MAX_NAME_LENGTH ]; + char guid[ 33 ]; + char ip[ 20 ]; + char reason[ MAX_ADMIN_BAN_REASON ]; + char made[ 18 ]; // big enough for strftime() %c + int expires; + int suspend; + char banner[ MAX_NAME_LENGTH ]; + int bannerlevel; +} +g_admin_ban_t; + +typedef struct g_admin_command +{ + char command[ MAX_ADMIN_CMD_LEN ]; + char exec[ MAX_QPATH ]; + char desc[ 50 ]; + char flag[ MAX_ADMIN_FLAG_LEN ]; +} +g_admin_command_t; + +typedef struct g_admin_namelog +{ + char name[ MAX_ADMIN_NAMELOG_NAMES ][MAX_NAME_LENGTH ]; + char ip[ 16 ]; + char guid[ 33 ]; + int slot; + qboolean banned; + qboolean muted; + int muteExpires; + qboolean denyBuild; + int denyHumanWeapons; + int denyAlienClasses; + int specExpires; + int voteCount; +} +g_admin_namelog_t; + +typedef struct g_admin_adminlog +{ + char name[ MAX_NAME_LENGTH ]; + char command[ MAX_ADMIN_CMD_LEN ]; + char args[ MAX_ADMIN_ADMINLOG_ARGS ]; + int id; + int time; + int level; + qboolean success; +} +g_admin_adminlog_t; + +qboolean G_admin_ban_check( char *userinfo, char *reason, int rlen ); +qboolean G_admin_cmd_check( gentity_t *ent, qboolean say ); +qboolean G_admin_readconfig( gentity_t *ent, int skiparg ); +qboolean G_admin_permission( gentity_t *ent, const char *flag ); +qboolean G_admin_name_check( gentity_t *ent, char *name, char *err, int len ); +void G_admin_namelog_update( gclient_t *ent, qboolean disconnect ); +void G_admin_maplog_result( char *flag ); +int G_admin_level( gentity_t *ent ); +void G_admin_set_adminname( gentity_t *ent ); +char* G_admin_adminPrintName( gentity_t *ent ); + +qboolean G_admin_seen(gentity_t *ent, int skiparg ); +void G_admin_seen_update( char *guid ); + +// ! command functions +qboolean G_admin_time( gentity_t *ent, int skiparg ); +qboolean G_admin_setlevel( gentity_t *ent, int skiparg ); +qboolean G_admin_flaglist( gentity_t *ent, int skiparg ); +qboolean G_admin_flag( gentity_t *ent, int skiparg ); +qboolean G_admin_kick( gentity_t *ent, int skiparg ); +qboolean G_admin_adjustban( gentity_t *ent, int skiparg ); +qboolean G_admin_subnetban( gentity_t *ent, int skiparg ); +qboolean G_admin_suspendban( gentity_t *ent, int skiparg ); +qboolean G_admin_ban( gentity_t *ent, int skiparg ); +qboolean G_admin_unban( gentity_t *ent, int skiparg ); +qboolean G_admin_putteam( gentity_t *ent, int skiparg ); +qboolean G_admin_adminlog( gentity_t *ent, int skiparg ); +void G_admin_adminlog_cleanup( void ); +void G_admin_adminlog_log( gentity_t *ent, char *command, char *args, int skiparg, qboolean success ); +qboolean G_admin_listadmins( gentity_t *ent, int skiparg ); +qboolean G_admin_listlayouts( gentity_t *ent, int skiparg ); +qboolean G_admin_listplayers( gentity_t *ent, int skiparg ); +qboolean G_admin_listmaps( gentity_t *ent, int skiparg ); +qboolean G_admin_listrotation( gentity_t *ent, int skiparg ); +qboolean G_admin_map( gentity_t *ent, int skiparg ); +qboolean G_admin_devmap( gentity_t *ent, int skiparg ); +void G_admin_maplog_update( void ); +qboolean G_admin_maplog( gentity_t *ent, int skiparg ); +qboolean G_admin_layoutsave( gentity_t *ent, int skiparg ); +qboolean G_admin_demo( gentity_t *ent, int skiparg ); +qboolean G_admin_mute( gentity_t *ent, int skiparg ); +qboolean G_admin_denybuild( gentity_t *ent, int skiparg ); +qboolean G_admin_denyweapon( gentity_t *ent, int skiparg ); +qboolean G_admin_showbans( gentity_t *ent, int skiparg ); +qboolean G_admin_help( gentity_t *ent, int skiparg ); +qboolean G_admin_admintest( gentity_t *ent, int skiparg ); +qboolean G_admin_allready( gentity_t *ent, int skiparg ); +qboolean G_admin_cancelvote( gentity_t *ent, int skiparg ); +qboolean G_admin_passvote( gentity_t *ent, int skiparg ); +qboolean G_admin_spec999( gentity_t *ent, int skiparg ); +qboolean G_admin_register( gentity_t *ent, int skiparg ); +qboolean G_admin_rename( gentity_t *ent, int skiparg ); +qboolean G_admin_restart( gentity_t *ent, int skiparg ); +qboolean G_admin_nobuild( gentity_t *ent, int skiparg ); +qboolean G_admin_nextmap( gentity_t *ent, int skiparg ); +qboolean G_admin_namelog( gentity_t *ent, int skiparg ); +qboolean G_admin_lock( gentity_t *ent, int skiparg ); +qboolean G_admin_unlock( gentity_t *ent, int skiparg ); +qboolean G_admin_info( gentity_t *ent, int skiparg ); +qboolean G_admin_buildlog( gentity_t *ent, int skiparg ); +qboolean G_admin_revert( gentity_t *ent, int skiparg ); +qboolean G_admin_pause( gentity_t *ent, int skiparg ); +qboolean G_admin_L0( gentity_t *ent, int skiparg ); +qboolean G_admin_L1( gentity_t *ent, int skiparg ); +qboolean G_admin_putmespec( gentity_t *ent, int skiparg ); +qboolean G_admin_warn( gentity_t *ent, int skiparg ); +qboolean G_admin_designate( gentity_t *ent, int skiparg ); +qboolean G_admin_cp( gentity_t *ent, int skiparg ); + +qboolean G_admin_slap( gentity_t *ent, int skiparg ); +qboolean G_admin_drop( gentity_t *ent, int skiparg ); +qboolean G_admin_invisible( gentity_t *ent, int skiparg ); + +void G_admin_print( gentity_t *ent, char *m ); +void G_admin_buffer_print( gentity_t *ent, char *m ); +void G_admin_buffer_begin( void ); +void G_admin_buffer_end( gentity_t *ent ); + +void G_admin_duration( int secs, char *duration, int dursize ); +void G_admin_cleanup( void ); +void G_admin_namelog_cleanup( void ); + +#endif /* ifndef _G_ADMIN_H */ diff --git a/src/game/g_buildable.c b/src/game/g_buildable.c new file mode 100644 index 0000000..3290ccc --- /dev/null +++ b/src/game/g_buildable.c @@ -0,0 +1,4710 @@ +/* +=========================================================================== +Copyright (C) 1999-2005 Id Software, Inc. +Copyright (C) 2000-2006 Tim Angus + +This file is part of Tremulous. + +Tremulous is free software; you can redistribute it +and/or modify it under the terms of the GNU General Public License as +published by the Free Software Foundation; either version 2 of the License, +or (at your option) any later version. + +Tremulous is distributed in the hope that it will be +useful, but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with Tremulous; if not, write to the Free Software +Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA +=========================================================================== +*/ + +#include "g_local.h" + +// from g_combat.c +extern char *modNames[ ]; + +/* +================ +G_SetBuildableAnim + +Triggers an animation client side +================ +*/ +void G_SetBuildableAnim( gentity_t *ent, buildableAnimNumber_t anim, qboolean force ) +{ + int localAnim = anim; + + if( force ) + localAnim |= ANIM_FORCEBIT; + + // don't toggle the togglebit more than once per frame + if( ent->animTime != level.time ) + { + localAnim |= ( ( ent->s.legsAnim & ANIM_TOGGLEBIT ) ^ ANIM_TOGGLEBIT ); + ent->animTime = level.time; + } + else + localAnim |= ent->s.legsAnim & ANIM_TOGGLEBIT; + + ent->s.legsAnim = localAnim; +} + +/* +================ +G_SetIdleBuildableAnim + +Set the animation to use whilst no other animations are running +================ +*/ +void G_SetIdleBuildableAnim( gentity_t *ent, buildableAnimNumber_t anim ) +{ + ent->s.torsoAnim = anim; +} + +/* +=============== +G_CheckSpawnPoint + +Check if a spawn at a specified point is valid +=============== +*/ +gentity_t *G_CheckSpawnPoint( int spawnNum, vec3_t origin, vec3_t normal, + buildable_t spawn, vec3_t spawnOrigin ) +{ + float displacement; + vec3_t mins, maxs; + vec3_t cmins, cmaxs; + vec3_t localOrigin; + trace_t tr; + + BG_FindBBoxForBuildable( spawn, mins, maxs ); + + if( spawn == BA_A_SPAWN ) + { + VectorSet( cmins, -MAX_ALIEN_BBOX, -MAX_ALIEN_BBOX, -MAX_ALIEN_BBOX ); + VectorSet( cmaxs, MAX_ALIEN_BBOX, MAX_ALIEN_BBOX, MAX_ALIEN_BBOX ); + + displacement = ( maxs[ 2 ] + MAX_ALIEN_BBOX ) * M_ROOT3; + VectorMA( origin, displacement, normal, localOrigin ); + + trap_Trace( &tr, origin, NULL, NULL, localOrigin, spawnNum, MASK_SHOT ); + + if( tr.entityNum != ENTITYNUM_NONE ) + return &g_entities[ tr.entityNum ]; + + trap_Trace( &tr, localOrigin, cmins, cmaxs, localOrigin, -1, MASK_PLAYERSOLID ); + + if( tr.entityNum == ENTITYNUM_NONE ) + { + if( spawnOrigin != NULL ) + VectorCopy( localOrigin, spawnOrigin ); + + return NULL; + } + else + return &g_entities[ tr.entityNum ]; + } + else if( spawn == BA_H_SPAWN ) + { + BG_FindBBoxForClass( PCL_HUMAN, cmins, cmaxs, NULL, NULL, NULL ); + + VectorCopy( origin, localOrigin ); + localOrigin[ 2 ] += maxs[ 2 ] + fabs( cmins[ 2 ] ) + 1.0f; + + trap_Trace( &tr, origin, NULL, NULL, localOrigin, spawnNum, MASK_SHOT ); + + if( tr.entityNum != ENTITYNUM_NONE ) + return &g_entities[ tr.entityNum ]; + + trap_Trace( &tr, localOrigin, cmins, cmaxs, localOrigin, -1, MASK_PLAYERSOLID ); + + if( tr.entityNum == ENTITYNUM_NONE ) + { + if( spawnOrigin != NULL ) + VectorCopy( localOrigin, spawnOrigin ); + + return NULL; + } + else + return &g_entities[ tr.entityNum ]; + } + + return NULL; +} + +/* +================ +G_NumberOfDependants + +Return number of entities that depend on this one +================ +*/ +static int G_NumberOfDependants( gentity_t *self ) +{ + int i, n = 0; + gentity_t *ent; + + for ( i = 1, ent = g_entities + i; i < level.num_entities; i++, ent++ ) + { + if( ent->s.eType != ET_BUILDABLE ) + continue; + + if( ent->parentNode == self ) + n++; + } + + return n; +} + +#define POWER_REFRESH_TIME 2000 + +/* +================ +G_FindPower + +attempt to find power for self, return qtrue if successful +================ +*/ +static qboolean G_FindPower( gentity_t *self ) +{ + int i; + gentity_t *ent; + gentity_t *closestPower = NULL; + int distance = 0; + int minDistance = 10000; + vec3_t temp_v; + + if( self->biteam != BIT_HUMANS ) + return qfalse; + + //reactor is always powered + if( self->s.modelindex == BA_H_REACTOR ) + return qtrue; + + //if this already has power then stop now + if( self->parentNode && self->parentNode->powered ) + return qtrue; + + //reset parent + self->parentNode = NULL; + + //iterate through entities + for ( i = 1, ent = g_entities + i; i < level.num_entities; i++, ent++ ) + { + if( ent->s.eType != ET_BUILDABLE ) + continue; + + //if entity is a power item calculate the distance to it + if( ( ent->s.modelindex == BA_H_REACTOR || ent->s.modelindex == BA_H_REPEATER ) && + ent->spawned ) + { + VectorSubtract( self->s.origin, ent->s.origin, temp_v ); + distance = VectorLength( temp_v ); + + if( distance < minDistance && ent->powered && + ( ( ent->s.modelindex == BA_H_REACTOR && + distance <= REACTOR_BASESIZE ) || + ( ent->s.modelindex == BA_H_REPEATER && + distance <= REPEATER_BASESIZE ) ) ) { + + closestPower = ent; + minDistance = distance; + } + } + } + + //if there were no power items nearby give up + if( closestPower ) { + self->parentNode = closestPower; + return qtrue; + } + else + return qfalse; +} + +/* +================ +G_IsPowered + +Simple wrapper to G_FindPower to check if a location has power +================ +*/ +qboolean G_IsPowered( vec3_t origin ) +{ + gentity_t dummy; + + dummy.parentNode = NULL; + dummy.biteam = BIT_HUMANS; + dummy.s.modelindex = BA_NONE; + VectorCopy( origin, dummy.s.origin ); + + return G_FindPower( &dummy ); +} + +/* +================ +G_FindDCC + +attempt to find a controlling DCC for self, return qtrue if successful +================ +*/ +static qboolean G_FindDCC( gentity_t *self ) +{ + int i; + gentity_t *ent; + gentity_t *closestDCC = NULL; + int distance = 0; + int minDistance = 10000; + vec3_t temp_v; + qboolean foundDCC = qfalse; + + if( self->biteam != BIT_HUMANS ) + return qfalse; + + //if this already has dcc then stop now + if( self->dccNode && self->dccNode->powered ) + return qtrue; + + //reset parent + self->dccNode = NULL; + + //iterate through entities + for( i = 1, ent = g_entities + i; i < level.num_entities; i++, ent++ ) + { + if( ent->s.eType != ET_BUILDABLE ) + continue; + + //if entity is a dcc calculate the distance to it + if( ent->s.modelindex == BA_H_DCC && ent->spawned ) + { + VectorSubtract( self->s.origin, ent->s.origin, temp_v ); + distance = VectorLength( temp_v ); + if( ( !foundDCC || distance < minDistance ) && ent->powered ) + { + closestDCC = ent; + minDistance = distance; + foundDCC = qtrue; + } + } + } + + //if there was no nearby DCC give up + if( !foundDCC ) + return qfalse; + + self->dccNode = closestDCC; + + return qtrue; +} + +/* +================ +G_IsDCCBuilt + +simple wrapper to G_FindDCC to check for a dcc +================ +*/ +qboolean G_IsDCCBuilt( void ) +{ + gentity_t dummy; + + memset( &dummy, 0, sizeof( gentity_t ) ); + + dummy.dccNode = NULL; + dummy.biteam = BIT_HUMANS; + + return G_FindDCC( &dummy ); +} + +/* +================ +G_FindOvermind + +Attempt to find an overmind for self +================ +*/ +static qboolean G_FindOvermind( gentity_t *self ) +{ + int i; + gentity_t *ent; + + if( self->biteam != BIT_ALIENS ) + return qfalse; + + //if this already has overmind then stop now + if( self->overmindNode && self->overmindNode->health > 0 ) + return qtrue; + + //reset parent + self->overmindNode = NULL; + + //iterate through entities + for( i = 1, ent = g_entities + i; i < level.num_entities; i++, ent++ ) + { + if( ent->s.eType != ET_BUILDABLE ) + continue; + + //if entity is an overmind calculate the distance to it + if( ent->s.modelindex == BA_A_OVERMIND && ent->spawned && ent->health > 0 ) + { + self->overmindNode = ent; + return qtrue; + } + } + + return qfalse; +} + +/* +================ +G_IsOvermindBuilt + +Simple wrapper to G_FindOvermind to check if a location has an overmind +================ +*/ +qboolean G_IsOvermindBuilt( void ) +{ + gentity_t dummy; + + memset( &dummy, 0, sizeof( gentity_t ) ); + + dummy.overmindNode = NULL; + dummy.biteam = BIT_ALIENS; + + return G_FindOvermind( &dummy ); +} + +/* +================ +G_FindCreep + +attempt to find creep for self, return qtrue if successful +================ +*/ +static qboolean G_FindCreep( gentity_t *self ) +{ + int i; + gentity_t *ent; + gentity_t *closestSpawn = NULL; + int distance = 0; + int minDistance = 10000; + vec3_t temp_v; + + //don't check for creep if flying through the air + if( self->s.groundEntityNum == -1 ) + return qtrue; + + //if self does not have a parentNode or it's parentNode is invalid find a new one + if( ( self->parentNode == NULL ) || !self->parentNode->inuse ) + { + for ( i = 1, ent = g_entities + i; i < level.num_entities; i++, ent++ ) + { + if( ent->s.eType != ET_BUILDABLE ) + continue; + + if( ( ent->s.modelindex == BA_A_SPAWN || ent->s.modelindex == BA_A_OVERMIND ) && + ent->spawned ) + { + VectorSubtract( self->s.origin, ent->s.origin, temp_v ); + distance = VectorLength( temp_v ); + if( distance < minDistance ) + { + closestSpawn = ent; + minDistance = distance; + } + } + } + + if( minDistance <= CREEP_BASESIZE ) + { + self->parentNode = closestSpawn; + return qtrue; + } + else + return qfalse; + } + + //if we haven't returned by now then we must already have a valid parent + return qtrue; +} + +/* +================ +G_IsCreepHere + +simple wrapper to G_FindCreep to check if a location has creep +================ +*/ +static qboolean G_IsCreepHere( vec3_t origin ) +{ + gentity_t dummy; + + memset( &dummy, 0, sizeof( gentity_t ) ); + + dummy.parentNode = NULL; + dummy.s.modelindex = BA_NONE; + VectorCopy( origin, dummy.s.origin ); + + return G_FindCreep( &dummy ); +} + +/* +================ +G_CreepSlow + +Set any nearby humans' SS_CREEPSLOWED flag +================ +*/ +static void G_CreepSlow( gentity_t *self ) +{ + int entityList[ MAX_GENTITIES ]; + vec3_t range; + vec3_t mins, maxs; + int i, num; + gentity_t *enemy; + buildable_t buildable = self->s.modelindex; + float creepSize = (float)BG_FindCreepSizeForBuildable( buildable ); + + VectorSet( range, creepSize, creepSize, creepSize ); + + VectorAdd( self->s.origin, range, maxs ); + VectorSubtract( self->s.origin, range, mins ); + + //find humans + num = trap_EntitiesInBox( mins, maxs, entityList, MAX_GENTITIES ); + for( i = 0; i < num; i++ ) + { + enemy = &g_entities[ entityList[ i ] ]; + + if( enemy->flags & FL_NOTARGET ) + continue; + + if( enemy->client && enemy->client->ps.stats[ STAT_PTEAM ] == PTE_HUMANS && + enemy->client->ps.groundEntityNum != ENTITYNUM_NONE && + G_Visible( self, enemy ) ) + { + enemy->client->ps.stats[ STAT_STATE ] |= SS_CREEPSLOWED; + enemy->client->lastCreepSlowTime = level.time; + } + } +} + +/* +================ +nullDieFunction + +hack to prevent compilers complaining about function pointer -> NULL conversion +================ +*/ +static void nullDieFunction( gentity_t *self, gentity_t *inflictor, gentity_t *attacker, int damage, int mod ) +{ +} + +/* +================ +freeBuildable +================ +*/ +static void freeBuildable( gentity_t *self ) +{ + G_FreeEntity( self ); +} + + +//================================================================================== + + + +/* +================ +A_CreepRecede + +Called when an alien spawn dies +================ +*/ +void A_CreepRecede( gentity_t *self ) +{ + //if the creep just died begin the recession + if( !( self->s.eFlags & EF_DEAD ) ) + { + self->s.eFlags |= EF_DEAD; + G_AddEvent( self, EV_BUILD_DESTROY, 0 ); + + if( self->spawned ) + self->s.time = -level.time; + else + self->s.time = -( level.time - + (int)( (float)CREEP_SCALEDOWN_TIME * + ( 1.0f - ( (float)( level.time - self->buildTime ) / + (float)BG_FindBuildTimeForBuildable( self->s.modelindex ) ) ) ) ); + } + + //creep is still receeding + if( ( self->timestamp + 10000 ) > level.time ) + self->nextthink = level.time + 500; + else //creep has died + G_FreeEntity( self ); +} + + + + +//================================================================================== + + + + +/* +================ +ASpawn_Melt + +Called when an alien spawn dies +================ +*/ +void ASpawn_Melt( gentity_t *self ) +{ + G_SelectiveRadiusDamage( self->s.pos.trBase, self, self->splashDamage, + self->splashRadius, self, self->splashMethodOfDeath, PTE_ALIENS ); + + //start creep recession + if( !( self->s.eFlags & EF_DEAD ) ) + { + self->s.eFlags |= EF_DEAD; + G_AddEvent( self, EV_BUILD_DESTROY, 0 ); + + if( self->spawned ) + self->s.time = -level.time; + else + self->s.time = -( level.time - + (int)( (float)CREEP_SCALEDOWN_TIME * + ( 1.0f - ( (float)( level.time - self->buildTime ) / + (float)BG_FindBuildTimeForBuildable( self->s.modelindex ) ) ) ) ); + } + + //not dead yet + if( ( self->timestamp + 10000 ) > level.time ) + self->nextthink = level.time + 500; + else //dead now + G_FreeEntity( self ); +} + +/* +================ +ASpawn_Blast + +Called when an alien spawn dies +================ +*/ +void ASpawn_Blast( gentity_t *self ) +{ + vec3_t dir; + + VectorCopy( self->s.origin2, dir ); + + //do a bit of radius damage + G_SelectiveRadiusDamage( self->s.pos.trBase, self, self->splashDamage, + self->splashRadius, self, self->splashMethodOfDeath, PTE_ALIENS ); + + //pretty events and item cleanup + self->s.eFlags |= EF_NODRAW; //don't draw the model once it's destroyed + G_AddEvent( self, EV_ALIEN_BUILDABLE_EXPLOSION, DirToByte( dir ) ); + self->timestamp = level.time; + self->think = ASpawn_Melt; + self->nextthink = level.time + 500; //wait .5 seconds before damaging others + + self->r.contents = 0; //stop collisions... + trap_LinkEntity( self ); //...requires a relink +} + +/* +================ +ASpawn_Die + +Called when an alien spawn dies +================ +*/ +void ASpawn_Die( gentity_t *self, gentity_t *inflictor, gentity_t *attacker, int damage, int mod ) +{ + buildHistory_t *new; + new = G_Alloc( sizeof( buildHistory_t ) ); + new->ID = ( ++level.lastBuildID > 1000 ) ? ( level.lastBuildID = 1 ) : level.lastBuildID; + new->ent = ( attacker && attacker->client ) ? attacker : NULL; + if( new->ent ) + new->name[ 0 ] = 0; + else + Q_strncpyz( new->name, "<world>", 8 ); + new->buildable = self->s.modelindex; + VectorCopy( self->s.pos.trBase, new->origin ); + VectorCopy( self->s.angles, new->angles ); + VectorCopy( self->s.origin2, new->origin2 ); + VectorCopy( self->s.angles2, new->angles2 ); + new->fate = ( attacker && attacker->client && attacker->client->ps.stats[ STAT_PTEAM ] == PTE_ALIENS ) ? BF_TEAMKILLED : BF_DESTROYED; + new->next = NULL; + G_LogBuild( new ); + + G_SetBuildableAnim( self, BANIM_DESTROY1, qtrue ); + G_SetIdleBuildableAnim( self, BANIM_DESTROYED ); + + self->die = nullDieFunction; + self->think = ASpawn_Blast; + + if( self->spawned ) + self->nextthink = level.time + 5000; + else + self->nextthink = level.time; //blast immediately + + self->s.eFlags &= ~EF_FIRING; //prevent any firing effects + + if( attacker && attacker->client ) + { + if( attacker->client->ps.stats[ STAT_PTEAM ] == PTE_HUMANS ) + { + if( self->s.modelindex == BA_A_OVERMIND ) + G_AddCreditToClient( attacker->client, OVERMIND_VALUE, qtrue ); + else if( self->s.modelindex == BA_A_SPAWN ) + G_AddCreditToClient( attacker->client, ASPAWN_VALUE, qtrue ); + } + else + { + G_TeamCommand( PTE_ALIENS, + va( "print \"%s ^3DESTROYED^7 by teammate %s^7\n\"", + BG_FindHumanNameForBuildable( self->s.modelindex ), + attacker->client->pers.netname ) ); + G_LogOnlyPrintf("%s ^3DESTROYED^7 by teammate %s^7\n", + BG_FindHumanNameForBuildable( self->s.modelindex ), + attacker->client->pers.netname ); + } + G_LogPrintf( "Decon: %i %i %i: %s^7 destroyed %s by %s\n", + attacker->client->ps.clientNum, self->s.modelindex, mod, + attacker->client->pers.netname, + BG_FindNameForBuildable( self->s.modelindex ), + modNames[ mod ] ); + } +} + +/* +================ +ASpawn_Think + +think function for Alien Spawn +================ +*/ +void ASpawn_Think( gentity_t *self ) +{ + gentity_t *ent; + + if( self->spawned ) + { + //only suicide if at rest + if( self->s.groundEntityNum ) + { + if( ( ent = G_CheckSpawnPoint( self->s.number, self->s.origin, + self->s.origin2, BA_A_SPAWN, NULL ) ) != NULL ) + { + // If the thing blocking the spawn is a buildable, kill it. + // If it's part of the map, kill self. + if( ent->s.eType == ET_BUILDABLE ) + { + G_Damage( ent, NULL, NULL, NULL, NULL, 10000, 0, MOD_SUICIDE ); + G_SetBuildableAnim( self, BANIM_SPAWN1, qtrue ); + } + else if( ent->s.number == ENTITYNUM_WORLD || ent->s.eType == ET_MOVER ) + { + G_Damage( self, NULL, NULL, NULL, NULL, 10000, 0, MOD_SUICIDE ); + return; + } + else if( g_antiSpawnBlock.integer && ent->client && + ent->client->ps.stats[ STAT_PTEAM ] == PTE_ALIENS ) + { + //spawnblock protection + if( self->spawnBlockTime && level.time - self->spawnBlockTime > 10000 ) + { + //five seconds of countermeasures and we're still blocked + //time for something more drastic + G_Damage( ent, NULL, NULL, NULL, NULL, 10000, 0, MOD_TRIGGER_HURT ); + self->spawnBlockTime += 2000; + //inappropriate MOD but prints an apt obituary + } + else if( self->spawnBlockTime && level.time - self->spawnBlockTime > 5000 ) + //five seconds of blocked by client and... + { + //random direction + vec3_t velocity; + velocity[0] = crandom() * g_antiSpawnBlock.integer; + velocity[1] = crandom() * g_antiSpawnBlock.integer; + velocity[2] = g_antiSpawnBlock.integer; + + VectorAdd( ent->client->ps.velocity, velocity, ent->client->ps.velocity ); + trap_SendServerCommand( ent-g_entities, "cp \"Don't spawn block!\"" ); + } + else if( !self->spawnBlockTime ) + self->spawnBlockTime = level.time; + } + if( ent->s.eType == ET_CORPSE ) + G_FreeEntity( ent ); //quietly remove + } + else + self->spawnBlockTime = 0; + } + } + + G_CreepSlow( self ); + + self->nextthink = level.time + BG_FindNextThinkForBuildable( self->s.modelindex ); +} + +/* +================ +ASpawn_Pain + +pain function for Alien Spawn +================ +*/ +void ASpawn_Pain( gentity_t *self, gentity_t *attacker, int damage ) +{ + G_SetBuildableAnim( self, BANIM_PAIN1, qfalse ); +} + + + + + +//================================================================================== + + + + + +#define OVERMIND_ATTACK_PERIOD 10000 +#define OVERMIND_DYING_PERIOD 5000 +#define OVERMIND_SPAWNS_PERIOD 30000 + +/* +================ +AOvermind_Think + +Think function for Alien Overmind +================ +*/ +void AOvermind_Think( gentity_t *self ) +{ + int entityList[ MAX_GENTITIES ]; + vec3_t range = { OVERMIND_ATTACK_RANGE, OVERMIND_ATTACK_RANGE, OVERMIND_ATTACK_RANGE }; + vec3_t mins, maxs; + int i, num; + gentity_t *enemy; + + VectorAdd( self->s.origin, range, maxs ); + VectorSubtract( self->s.origin, range, mins ); + + if( self->spawned && ( self->health > 0 ) ) + { + //do some damage + num = trap_EntitiesInBox( mins, maxs, entityList, MAX_GENTITIES ); + for( i = 0; i < num; i++ ) + { + enemy = &g_entities[ entityList[ i ] ]; + + if( enemy->flags & FL_NOTARGET ) + continue; + + if( enemy->client && enemy->client->ps.stats[ STAT_PTEAM ] == PTE_HUMANS ) + { + self->timestamp = level.time; + G_SelectiveRadiusDamage( self->s.pos.trBase, self, self->splashDamage, + self->splashRadius, self, MOD_OVERMIND, PTE_ALIENS ); + G_SetBuildableAnim( self, BANIM_ATTACK1, qfalse ); + } + } + + // just in case an egg finishes building after we tell overmind to stfu + if( level.numAlienSpawns > 0 ) + level.overmindMuted = qfalse; + + //low on spawns + if( !level.overmindMuted && level.numAlienSpawns <= 0 && + level.time > self->overmindSpawnsTimer ) + { + qboolean haveBuilder = qfalse; + gentity_t *builder; + + self->overmindSpawnsTimer = level.time + OVERMIND_SPAWNS_PERIOD; + G_BroadcastEvent( EV_OVERMIND_SPAWNS, 0 ); + + for( i = 0; i < level.numConnectedClients; i++ ) + { + builder = &g_entities[ level.sortedClients[ i ] ]; + if( builder->health > 0 && + ( builder->client->pers.classSelection == PCL_ALIEN_BUILDER0 || + builder->client->pers.classSelection == PCL_ALIEN_BUILDER0_UPG ) ) + { + haveBuilder = qtrue; + break; + } + } + // aliens now know they have no eggs, but they're screwed, so stfu + if( !haveBuilder || G_TimeTilSuddenDeath( ) <= 0 ) + level.overmindMuted = qtrue; + } + + //overmind dying + if( self->health < ( OVERMIND_HEALTH / 10.0f ) && level.time > self->overmindDyingTimer ) + { + self->overmindDyingTimer = level.time + OVERMIND_DYING_PERIOD; + G_BroadcastEvent( EV_OVERMIND_DYING, 0 ); + } + + //overmind under attack + if( self->health < self->lastHealth && level.time > self->overmindAttackTimer ) + { + self->overmindAttackTimer = level.time + OVERMIND_ATTACK_PERIOD; + G_BroadcastEvent( EV_OVERMIND_ATTACK, 0 ); + } + + self->lastHealth = self->health; + } + else + self->overmindSpawnsTimer = level.time + OVERMIND_SPAWNS_PERIOD; + + G_CreepSlow( self ); + + self->nextthink = level.time + BG_FindNextThinkForBuildable( self->s.modelindex ); +} + + + + + + +//================================================================================== + + + + + +/* +================ +ABarricade_Pain + +pain function for Alien Spawn +================ +*/ +void ABarricade_Pain( gentity_t *self, gentity_t *attacker, int damage ) +{ + if( rand( ) % 2 ) + G_SetBuildableAnim( self, BANIM_PAIN1, qfalse ); + else + G_SetBuildableAnim( self, BANIM_PAIN2, qfalse ); +} + +/* +================ +ABarricade_Blast + +Called when an alien spawn dies +================ +*/ +void ABarricade_Blast( gentity_t *self ) +{ + vec3_t dir; + + VectorCopy( self->s.origin2, dir ); + + //do a bit of radius damage + G_SelectiveRadiusDamage( self->s.pos.trBase, self, self->splashDamage, + self->splashRadius, self, self->splashMethodOfDeath, PTE_ALIENS ); + + //pretty events and item cleanup + self->s.eFlags |= EF_NODRAW; //don't draw the model once its destroyed + G_AddEvent( self, EV_ALIEN_BUILDABLE_EXPLOSION, DirToByte( dir ) ); + self->timestamp = level.time; + self->think = A_CreepRecede; + self->nextthink = level.time + 500; //wait .5 seconds before damaging others + + self->r.contents = 0; //stop collisions... + trap_LinkEntity( self ); //...requires a relink +} + +/* +================ +ABarricade_Die + +Called when an alien spawn dies +================ +*/ +void ABarricade_Die( gentity_t *self, gentity_t *inflictor, gentity_t *attacker, int damage, int mod ) +{ + buildHistory_t *new; + new = G_Alloc( sizeof( buildHistory_t ) ); + new->ID = ( ++level.lastBuildID > 1000 ) ? ( level.lastBuildID = 1 ) : level.lastBuildID; + new->ent = ( attacker && attacker->client ) ? attacker : NULL; + if( new->ent ) + new->name[ 0 ] = 0; + else + Q_strncpyz( new->name, "<world>", 8 ); + new->buildable = self->s.modelindex; + VectorCopy( self->s.pos.trBase, new->origin ); + VectorCopy( self->s.angles, new->angles ); + VectorCopy( self->s.origin2, new->origin2 ); + VectorCopy( self->s.angles2, new->angles2 ); + new->fate = ( attacker && attacker->client && attacker->client->ps.stats[ STAT_PTEAM ] == PTE_ALIENS ) ? BF_TEAMKILLED : BF_DESTROYED; + new->next = NULL; + G_LogBuild( new ); + + G_SetBuildableAnim( self, BANIM_DESTROY1, qtrue ); + G_SetIdleBuildableAnim( self, BANIM_DESTROYED ); + + self->die = nullDieFunction; + self->think = ABarricade_Blast; + self->s.eFlags &= ~EF_FIRING; //prevent any firing effects + + if( self->spawned ) + self->nextthink = level.time + 5000; + else + self->nextthink = level.time; //blast immediately + + if( attacker && attacker->client ) + { + if( attacker->client->ps.stats[ STAT_PTEAM ] == PTE_ALIENS ) + { + G_TeamCommand( PTE_ALIENS, + va( "print \"%s ^3DESTROYED^7 by teammate %s^7\n\"", + BG_FindHumanNameForBuildable( self->s.modelindex ), + attacker->client->pers.netname ) ); + G_LogOnlyPrintf("%s ^3DESTROYED^7 by teammate %s^7\n", + BG_FindHumanNameForBuildable( self->s.modelindex ), + attacker->client->pers.netname ); + } + G_LogPrintf( "Decon: %i %i %i: %s^7 destroyed %s by %s\n", + attacker->client->ps.clientNum, self->s.modelindex, mod, + attacker->client->pers.netname, + BG_FindNameForBuildable( self->s.modelindex ), + modNames[ mod ] ); + } +} + +/* +================ +ABarricade_Think + +Think function for Alien Barricade +================ +*/ +void ABarricade_Think( gentity_t *self ) +{ + + self->powered = G_IsOvermindBuilt( ); + + //if there is no creep nearby die + if( !G_FindCreep( self ) ) + { + G_Damage( self, NULL, NULL, NULL, NULL, 10000, 0, MOD_SUICIDE ); + return; + } + + G_CreepSlow( self ); + + self->nextthink = level.time + BG_FindNextThinkForBuildable( self->s.modelindex ); +} + + + + +//================================================================================== + + + + +void AAcidTube_Think( gentity_t *self ); + +/* +================ +AAcidTube_Damage + +Damage function for Alien Acid Tube +================ +*/ +void AAcidTube_Damage( gentity_t *self ) +{ + if( self->spawned ) + { + if( !( self->s.eFlags & EF_FIRING ) ) + { + self->s.eFlags |= EF_FIRING; + G_AddEvent( self, EV_ALIEN_ACIDTUBE, DirToByte( self->s.origin2 ) ); + } + + if( ( self->timestamp + ACIDTUBE_REPEAT ) > level.time ) + self->think = AAcidTube_Damage; + else + { + self->think = AAcidTube_Think; + self->s.eFlags &= ~EF_FIRING; + } + + //do some damage + G_SelectiveRadiusDamage( self->s.pos.trBase, self, self->splashDamage, + self->splashRadius, self, self->splashMethodOfDeath, PTE_ALIENS ); + } + + G_CreepSlow( self ); + + self->nextthink = level.time + BG_FindNextThinkForBuildable( self->s.modelindex ); +} + +/* +================ +AAcidTube_Think + +Think function for Alien Acid Tube +================ +*/ +void AAcidTube_Think( gentity_t *self ) +{ + int entityList[ MAX_GENTITIES ]; + vec3_t range = { ACIDTUBE_RANGE, ACIDTUBE_RANGE, ACIDTUBE_RANGE }; + vec3_t mins, maxs; + int i, num; + gentity_t *enemy; + + self->powered = G_IsOvermindBuilt( ); + + VectorAdd( self->s.origin, range, maxs ); + VectorSubtract( self->s.origin, range, mins ); + + //if there is no creep nearby die + if( !G_FindCreep( self ) ) + { + G_Damage( self, NULL, NULL, NULL, NULL, 10000, 0, MOD_SUICIDE ); + return; + } + + if( self->spawned && G_FindOvermind( self ) ) + { + //do some damage + num = trap_EntitiesInBox( mins, maxs, entityList, MAX_GENTITIES ); + for( i = 0; i < num; i++ ) + { + enemy = &g_entities[ entityList[ i ] ]; + + if( enemy->flags & FL_NOTARGET ) + continue; + + if( !G_Visible( self, enemy ) ) + continue; + + if( enemy->client && enemy->client->ps.stats[ STAT_PTEAM ] == PTE_HUMANS ) + { + if( level.paused || enemy->client->pers.paused ) + continue; + self->timestamp = level.time; + self->think = AAcidTube_Damage; + self->nextthink = level.time + 100; + G_SetBuildableAnim( self, BANIM_ATTACK1, qfalse ); + return; + } + } + } + + G_CreepSlow( self ); + + self->nextthink = level.time + BG_FindNextThinkForBuildable( self->s.modelindex ); +} + + + + +//================================================================================== + + + + +/* +================ +AHive_Think + +Think function for Alien Hive +================ +*/ +void AHive_Think( gentity_t *self ) +{ + int entityList[ MAX_GENTITIES ]; + vec3_t range = { ACIDTUBE_RANGE, ACIDTUBE_RANGE, ACIDTUBE_RANGE }; + vec3_t mins, maxs; + int i, num; + gentity_t *enemy; + vec3_t dirToTarget; + + self->powered = G_IsOvermindBuilt( ); + + self->nextthink = level.time + BG_FindNextThinkForBuildable( self->s.modelindex ); + + VectorAdd( self->s.origin, range, maxs ); + VectorSubtract( self->s.origin, range, mins ); + + //if there is no creep nearby die + if( !G_FindCreep( self ) ) + { + G_Damage( self, NULL, NULL, NULL, NULL, 10000, 0, MOD_SUICIDE ); + return; + } + + if( self->timestamp < level.time ) + self->active = qfalse; //nothing has returned in HIVE_REPEAT seconds, forget about it + + if( self->spawned && !self->active && G_FindOvermind( self ) ) + { + //do some damage + num = trap_EntitiesInBox( mins, maxs, entityList, MAX_GENTITIES ); + for( i = 0; i < num; i++ ) + { + enemy = &g_entities[ entityList[ i ] ]; + + if( enemy->flags & FL_NOTARGET ) + continue; + + if( enemy->health <= 0 ) + continue; + + if( !G_Visible( self, enemy ) ) + continue; + + if( enemy->client && enemy->client->ps.stats[ STAT_PTEAM ] == PTE_HUMANS ) + { + if( level.paused || enemy->client->pers.paused ) + continue; + self->active = qtrue; + self->target_ent = enemy; + self->timestamp = level.time + HIVE_REPEAT; + + VectorSubtract( enemy->s.pos.trBase, self->s.pos.trBase, dirToTarget ); + VectorNormalize( dirToTarget ); + vectoangles( dirToTarget, self->turretAim ); + + //fire at target + FireWeapon( self ); + G_SetBuildableAnim( self, BANIM_ATTACK1, qfalse ); + return; + } + } + } + + G_CreepSlow( self ); +} + + + + +//================================================================================== + + + + +#define HOVEL_TRACE_DEPTH 128.0f + +/* +================ +AHovel_Blocked + +Is this hovel entrance blocked? +================ +*/ +qboolean AHovel_Blocked( gentity_t *hovel, gentity_t *player, qboolean provideExit ) +{ + vec3_t forward, normal, origin, start, end, angles, hovelMaxs; + vec3_t mins, maxs; + float displacement; + trace_t tr; + + BG_FindBBoxForBuildable( BA_A_HOVEL, NULL, hovelMaxs ); + BG_FindBBoxForClass( player->client->ps.stats[ STAT_PCLASS ], + mins, maxs, NULL, NULL, NULL ); + + VectorCopy( hovel->s.origin2, normal ); + AngleVectors( hovel->s.angles, forward, NULL, NULL ); + VectorInverse( forward ); + + displacement = VectorMaxComponent( maxs ) + + VectorMaxComponent( hovelMaxs ) + 1.0f; + + VectorMA( hovel->s.origin, displacement, forward, origin ); + + VectorCopy( hovel->s.origin, start ); + VectorCopy( origin, end ); + + // see if there's something between the hovel and its exit + // (eg built right up against a wall) + trap_Trace( &tr, start, NULL, NULL, end, player->s.number, MASK_PLAYERSOLID ); + if( tr.fraction < 1.0f ) + return qtrue; + + vectoangles( forward, angles ); + + VectorMA( origin, HOVEL_TRACE_DEPTH, normal, start ); + + //compute a place up in the air to start the real trace + trap_Trace( &tr, origin, mins, maxs, start, player->s.number, MASK_PLAYERSOLID ); + + VectorMA( origin, ( HOVEL_TRACE_DEPTH * tr.fraction ) - 1.0f, normal, start ); + VectorMA( origin, -HOVEL_TRACE_DEPTH, normal, end ); + + trap_Trace( &tr, start, mins, maxs, end, player->s.number, MASK_PLAYERSOLID ); + + VectorCopy( tr.endpos, origin ); + + trap_Trace( &tr, origin, mins, maxs, origin, player->s.number, MASK_PLAYERSOLID ); + + if( provideExit ) + { + G_SetOrigin( player, origin ); + VectorCopy( origin, player->client->ps.origin ); + // nudge + VectorMA( normal, 200.0f, forward, player->client->ps.velocity ); + G_SetClientViewAngle( player, angles ); + } + + if( tr.fraction < 1.0f ) + return qtrue; + else + return qfalse; +} + +/* +================ +APropHovel_Blocked + +Wrapper to test a hovel placement for validity +================ +*/ +static qboolean APropHovel_Blocked( vec3_t origin, vec3_t angles, vec3_t normal, + gentity_t *player ) +{ + gentity_t hovel; + + VectorCopy( origin, hovel.s.origin ); + VectorCopy( angles, hovel.s.angles ); + VectorCopy( normal, hovel.s.origin2 ); + + return AHovel_Blocked( &hovel, player, qfalse ); +} + +/* +================ +AHovel_Use + +Called when an alien uses a hovel +================ +*/ +void AHovel_Use( gentity_t *self, gentity_t *other, gentity_t *activator ) +{ + vec3_t hovelOrigin, hovelAngles, inverseNormal; + + if( self->spawned && G_FindOvermind( self ) ) + { + if( self->active ) + { + //this hovel is in use + G_TriggerMenu( activator->client->ps.clientNum, MN_A_HOVEL_OCCUPIED ); + } + else if( ( ( activator->client->ps.stats[ STAT_PCLASS ] == PCL_ALIEN_BUILDER0 ) || + ( activator->client->ps.stats[ STAT_PCLASS ] == PCL_ALIEN_BUILDER0_UPG ) ) && + activator->health > 0 && self->health > 0 ) + { + if( AHovel_Blocked( self, activator, qfalse ) ) + { + //you can get in, but you can't get out + G_TriggerMenu( activator->client->ps.clientNum, MN_A_HOVEL_BLOCKED ); + return; + } + + self->active = qtrue; + G_SetBuildableAnim( self, BANIM_ATTACK1, qfalse ); + + //prevent lerping + activator->client->ps.eFlags ^= EF_TELEPORT_BIT; + activator->client->ps.eFlags |= EF_NODRAW; + G_UnlaggedClear( activator ); + + activator->client->ps.stats[ STAT_STATE ] |= SS_HOVELING; + activator->client->hovel = self; + self->builder = activator; + + // Cancel pending suicides + activator->suicideTime = 0; + + VectorCopy( self->s.pos.trBase, hovelOrigin ); + VectorMA( hovelOrigin, 128.0f, self->s.origin2, hovelOrigin ); + + VectorCopy( self->s.origin2, inverseNormal ); + VectorInverse( inverseNormal ); + vectoangles( inverseNormal, hovelAngles ); + + VectorCopy( activator->s.pos.trBase, activator->client->hovelOrigin ); + + G_SetOrigin( activator, hovelOrigin ); + VectorCopy( hovelOrigin, activator->client->ps.origin ); + G_SetClientViewAngle( activator, hovelAngles ); + } + } +} + + +/* +================ +AHovel_Think + +Think for alien hovel +================ +*/ +void AHovel_Think( gentity_t *self ) +{ + self->powered = G_IsOvermindBuilt( ); + if( self->spawned ) + { + if( self->active ) + G_SetIdleBuildableAnim( self, BANIM_IDLE2 ); + else + G_SetIdleBuildableAnim( self, BANIM_IDLE1 ); + } + + G_CreepSlow( self ); + + self->nextthink = level.time + 200; +} + +/* +================ +AHovel_Die + +Die for alien hovel +================ +*/ +void AHovel_Die( gentity_t *self, gentity_t *inflictor, gentity_t *attacker, int damage, int mod ) +{ + vec3_t dir; + + buildHistory_t *new; + new = G_Alloc( sizeof( buildHistory_t ) ); + new->ID = ( ++level.lastBuildID > 1000 ) ? ( level.lastBuildID = 1 ) : level.lastBuildID; + new->ent = ( attacker && attacker->client ) ? attacker : NULL; + if( new->ent ) + new->name[ 0 ] = 0; + else + Q_strncpyz( new->name, "<world>", 8 ); + new->buildable = self->s.modelindex; + VectorCopy( self->s.pos.trBase, new->origin ); + VectorCopy( self->s.angles, new->angles ); + VectorCopy( self->s.origin2, new->origin2 ); + VectorCopy( self->s.angles2, new->angles2 ); + new->fate = ( attacker && attacker->client && attacker->client->ps.stats[ STAT_PTEAM ] == PTE_ALIENS ) ? BF_TEAMKILLED : BF_DESTROYED; + new->next = NULL; + G_LogBuild( new ); + + VectorCopy( self->s.origin2, dir ); + + //do a bit of radius damage + G_SelectiveRadiusDamage( self->s.pos.trBase, self, self->splashDamage, + self->splashRadius, self, self->splashMethodOfDeath, PTE_ALIENS ); + + //pretty events and item cleanup + self->s.eFlags |= EF_NODRAW; //don't draw the model once its destroyed + G_AddEvent( self, EV_ALIEN_BUILDABLE_EXPLOSION, DirToByte( dir ) ); + self->s.eFlags &= ~EF_FIRING; //prevent any firing effects + self->timestamp = level.time; + self->think = ASpawn_Melt; + self->nextthink = level.time + 500; //wait .5 seconds before damaging others + self->die = nullDieFunction; + + //if the hovel is occupied free the occupant + if( self->active ) + { + gentity_t *builder = self->builder; + vec3_t newOrigin; + vec3_t newAngles; + + VectorCopy( self->s.angles, newAngles ); + newAngles[ ROLL ] = 0; + + VectorCopy( self->s.origin, newOrigin ); + VectorMA( newOrigin, 1.0f, self->s.origin2, newOrigin ); + + //prevent lerping + builder->client->ps.eFlags ^= EF_TELEPORT_BIT; + builder->client->ps.eFlags &= ~EF_NODRAW; + G_UnlaggedClear( builder ); + + G_SetOrigin( builder, newOrigin ); + VectorCopy( newOrigin, builder->client->ps.origin ); + G_SetClientViewAngle( builder, newAngles ); + + //client leaves hovel + builder->client->ps.stats[ STAT_STATE ] &= ~SS_HOVELING; + } + + self->r.contents = 0; //stop collisions... + trap_LinkEntity( self ); //...requires a relink + + if( attacker && attacker->client ) + { + if( attacker->client->ps.stats[ STAT_PTEAM ] == PTE_ALIENS ) + { + G_TeamCommand( PTE_ALIENS, + va( "print \"%s ^3DESTROYED^7 by teammate %s^7\n\"", + BG_FindHumanNameForBuildable( self->s.modelindex ), + attacker->client->pers.netname ) ); + G_LogOnlyPrintf("%s ^3DESTROYED^7 by teammate %s^7\n", + BG_FindHumanNameForBuildable( self->s.modelindex ), + attacker->client->pers.netname ); + } + G_LogPrintf( "Decon: %i %i %i: %s^7 destroyed %s by %s\n", + attacker->client->ps.clientNum, self->s.modelindex, mod, + attacker->client->pers.netname, + BG_FindNameForBuildable( self->s.modelindex ), + modNames[ mod ] ); + } +} + + + + + +//================================================================================== + + + + +/* +================ +ABooster_Touch + +Called when an alien touches a booster +================ +*/ +void ABooster_Touch( gentity_t *self, gentity_t *other, trace_t *trace ) +{ + gclient_t *client = other->client; + + if( other->flags & FL_NOTARGET ) + return; // notarget cancels even beneficial effects? + + if( !self->spawned || self->health <= 0 ) + return; + + if( !G_FindOvermind( self ) ) + return; + + if( !client ) + return; + + if( client && client->ps.stats[ STAT_PTEAM ] == PTE_HUMANS ) + return; + + //only allow boostage once every 30 seconds + if( client->lastBoostedTime + BOOSTER_INTERVAL > level.time ) + return; + + if( !( client->ps.stats[ STAT_STATE ] & SS_BOOSTED ) ) + { + client->ps.stats[ STAT_STATE ] |= SS_BOOSTED; + client->lastBoostedTime = level.time; + } +} + + + + +//================================================================================== + +#define TRAPPER_ACCURACY 10 // lower is better + +/* +================ +ATrapper_FireOnEnemy + +Used by ATrapper_Think to fire at enemy +================ +*/ +void ATrapper_FireOnEnemy( gentity_t *self, int firespeed, float range ) +{ + gentity_t *enemy = self->enemy; + vec3_t dirToTarget; + vec3_t halfAcceleration, thirdJerk; + float distanceToTarget = BG_FindRangeForBuildable( self->s.modelindex ); + int lowMsec = 0; + int highMsec = (int)( ( + ( ( distanceToTarget * LOCKBLOB_SPEED ) + + ( distanceToTarget * BG_FindSpeedForClass( enemy->client->ps.stats[ STAT_PCLASS ] ) ) ) / + ( LOCKBLOB_SPEED * LOCKBLOB_SPEED ) ) * 1000.0f ); + + VectorScale( enemy->acceleration, 1.0f / 2.0f, halfAcceleration ); + VectorScale( enemy->jerk, 1.0f / 3.0f, thirdJerk ); + + // highMsec and lowMsec can only move toward + // one another, so the loop must terminate + while( highMsec - lowMsec > TRAPPER_ACCURACY ) + { + int partitionMsec = ( highMsec + lowMsec ) / 2; + float time = (float)partitionMsec / 1000.0f; + float projectileDistance = LOCKBLOB_SPEED * time; + + VectorMA( enemy->s.pos.trBase, time, enemy->s.pos.trDelta, dirToTarget ); + VectorMA( dirToTarget, time * time, halfAcceleration, dirToTarget ); + VectorMA( dirToTarget, time * time * time, thirdJerk, dirToTarget ); + VectorSubtract( dirToTarget, self->s.pos.trBase, dirToTarget ); + distanceToTarget = VectorLength( dirToTarget ); + + if( projectileDistance < distanceToTarget ) + lowMsec = partitionMsec; + else if( projectileDistance > distanceToTarget ) + highMsec = partitionMsec; + else if( projectileDistance == distanceToTarget ) + break; // unlikely to happen + } + + VectorNormalize( dirToTarget ); + vectoangles( dirToTarget, self->turretAim ); + + //fire at target + FireWeapon( self ); + G_SetBuildableAnim( self, BANIM_ATTACK1, qfalse ); + self->count = level.time + firespeed; +} + +/* +================ +ATrapper_CheckTarget + +Used by ATrapper_Think to check enemies for validity +================ +*/ +qboolean ATrapper_CheckTarget( gentity_t *self, gentity_t *target, int range ) +{ + vec3_t distance; + trace_t trace; + + if( !target ) // Do we have a target? + return qfalse; + if( !target->inuse ) // Does the target still exist? + return qfalse; + if( target == self ) // is the target us? + return qfalse; + if( !target->client ) // is the target a bot or player? + return qfalse; + if( target->flags & FL_NOTARGET ) // is the target cheating? + return qfalse; + if( target->client->ps.stats[ STAT_PTEAM ] == PTE_ALIENS ) // one of us? + return qfalse; + if( target->client->sess.sessionTeam == TEAM_SPECTATOR ) // is the target alive? + return qfalse; + if( target->health <= 0 ) // is the target still alive? + return qfalse; + if( target->client->ps.stats[ STAT_STATE ] & SS_BLOBLOCKED ) // locked? + return qfalse; + + VectorSubtract( target->r.currentOrigin, self->r.currentOrigin, distance ); + if( VectorLength( distance ) > range ) // is the target within range? + return qfalse; + + //only allow a narrow field of "vision" + VectorNormalize( distance ); //is now direction of target + if( DotProduct( distance, self->s.origin2 ) < LOCKBLOB_DOT ) + return qfalse; + + trap_Trace( &trace, self->s.pos.trBase, NULL, NULL, target->s.pos.trBase, self->s.number, MASK_SHOT ); + if ( trace.contents & CONTENTS_SOLID ) // can we see the target? + return qfalse; + + return qtrue; +} + +/* +================ +ATrapper_FindEnemy + +Used by ATrapper_Think to locate enemy gentities +================ +*/ +void ATrapper_FindEnemy( gentity_t *ent, int range ) +{ + gentity_t *target; + + //iterate through entities + for( target = g_entities; target < &g_entities[ level.num_entities ]; target++ ) + { + //if target is not valid keep searching + if( !ATrapper_CheckTarget( ent, target, range ) ) + continue; + + //we found a target + ent->enemy = target; + return; + } + + //couldn't find a target + ent->enemy = NULL; +} + +/* +================ +ATrapper_Think + +think function for Alien Defense +================ +*/ +void ATrapper_Think( gentity_t *self ) +{ + int range = BG_FindRangeForBuildable( self->s.modelindex ); + int firespeed = BG_FindFireSpeedForBuildable( self->s.modelindex ); + + self->powered = G_IsOvermindBuilt( ); + + G_CreepSlow( self ); + + self->nextthink = level.time + BG_FindNextThinkForBuildable( self->s.modelindex ); + + //if there is no creep nearby die + if( !G_FindCreep( self ) ) + { + G_Damage( self, NULL, NULL, NULL, NULL, 10000, 0, MOD_SUICIDE ); + return; + } + + if( self->spawned && G_FindOvermind( self ) ) + { + //if the current target is not valid find a new one + if( !ATrapper_CheckTarget( self, self->enemy, range ) ) + ATrapper_FindEnemy( self, range ); + + //if a new target cannot be found don't do anything + if( !self->enemy ) + return; + + //if we are pointing at our target and we can fire shoot it + if( self->count < level.time ) + ATrapper_FireOnEnemy( self, firespeed, range ); + } +} + + + +//================================================================================== + + + +/* +================ +HRepeater_Think + +Think for human power repeater +================ +*/ +void HRepeater_Think( gentity_t *self ) +{ + int i; + qboolean reactor = qfalse; + gentity_t *ent; + + if( self->spawned ) + { + //iterate through entities + for ( i = 1, ent = g_entities + i; i < level.num_entities; i++, ent++ ) + { + if( ent->s.eType != ET_BUILDABLE ) + continue; + + if( ent->s.modelindex == BA_H_REACTOR && ent->spawned ) + reactor = qtrue; + } + } + + if( G_NumberOfDependants( self ) == 0 ) + { + //if no dependants for x seconds then disappear + if( self->count < 0 ) + self->count = level.time; + else if( self->count > 0 && ( ( level.time - self->count ) > REPEATER_INACTIVE_TIME ) ) + G_Damage( self, NULL, NULL, NULL, NULL, 10000, 0, MOD_SUICIDE ); + } + else + self->count = -1; + + self->powered = reactor; + + self->nextthink = level.time + POWER_REFRESH_TIME; +} + +/* +================ +HRepeater_Use + +Use for human power repeater +================ +*/ +void HRepeater_Use( gentity_t *self, gentity_t *other, gentity_t *activator ) +{ + if( self->health <= 0 ) + return; + + if( !self->spawned ) + return; + + if( other ) + G_GiveClientMaxAmmo( other, qtrue ); +} + + +#define DCC_ATTACK_PERIOD 10000 + +/* +================ +HReactor_Think + +Think function for Human Reactor +================ +*/ +void HReactor_Think( gentity_t *self ) +{ + int entityList[ MAX_GENTITIES ]; + vec3_t range = { REACTOR_ATTACK_RANGE, REACTOR_ATTACK_RANGE, REACTOR_ATTACK_RANGE }; + vec3_t mins, maxs; + int i, num; + gentity_t *enemy, *tent; + + VectorAdd( self->s.origin, range, maxs ); + VectorSubtract( self->s.origin, range, mins ); + + if( self->spawned && ( self->health > 0 ) ) + { + //do some damage + num = trap_EntitiesInBox( mins, maxs, entityList, MAX_GENTITIES ); + for( i = 0; i < num; i++ ) + { + enemy = &g_entities[ entityList[ i ] ]; + + if( enemy->flags & FL_NOTARGET ) + continue; + + if( enemy->client && enemy->client->ps.stats[ STAT_PTEAM ] == PTE_ALIENS ) + { + if( level.paused || enemy->client->pers.paused ) + continue; + self->timestamp = level.time; + G_SelectiveRadiusDamage( self->s.pos.trBase, self, REACTOR_ATTACK_DAMAGE, + REACTOR_ATTACK_RANGE, self, MOD_REACTOR, PTE_HUMANS ); + + tent = G_TempEntity( enemy->s.pos.trBase, EV_TESLATRAIL ); + + VectorCopy( self->s.pos.trBase, tent->s.origin2 ); + + tent->s.generic1 = self->s.number; //src + tent->s.clientNum = enemy->s.number; //dest + } + } + + //reactor under attack + if( self->health < self->lastHealth && + level.time > level.humanBaseAttackTimer && G_IsDCCBuilt( ) ) + { + level.humanBaseAttackTimer = level.time + DCC_ATTACK_PERIOD; + G_BroadcastEvent( EV_DCC_ATTACK, 0 ); + } + + self->lastHealth = self->health; + } + + self->nextthink = level.time + BG_FindNextThinkForBuildable( self->s.modelindex ); +} + +//================================================================================== + + + +/* +================ +HArmoury_Activate + +Called when a human activates an Armoury +================ +*/ +void HArmoury_Activate( gentity_t *self, gentity_t *other, gentity_t *activator ) +{ + if( self->spawned ) + { + //only humans can activate this + if( activator->client->ps.stats[ STAT_PTEAM ] != PTE_HUMANS ) + return; + + //if this is powered then call the armoury menu + if( self->powered ) + G_TriggerMenu( activator->client->ps.clientNum, MN_H_ARMOURY ); + else + G_TriggerMenu( activator->client->ps.clientNum, MN_H_NOTPOWERED ); + } +} + +/* +================ +HArmoury_Think + +Think for armoury +================ +*/ +void HArmoury_Think( gentity_t *self ) +{ + //make sure we have power + self->nextthink = level.time + POWER_REFRESH_TIME; + + self->powered = G_FindPower( self ); +} + + + + +//================================================================================== + + + + + +/* +================ +HDCC_Think + +Think for dcc +================ +*/ +void HDCC_Think( gentity_t *self ) +{ + //make sure we have power + self->nextthink = level.time + POWER_REFRESH_TIME; + + self->powered = G_FindPower( self ); +} + + + + +//================================================================================== + +/* +================ +HMedistat_Think + +think function for Human Medistation +================ +*/ +void HMedistat_Think( gentity_t *self ) +{ + int entityList[ MAX_GENTITIES ]; + vec3_t mins, maxs; + int i, num; + gentity_t *player; + qboolean occupied = qfalse; + + self->nextthink = level.time + BG_FindNextThinkForBuildable( self->s.modelindex ); + + //make sure we have power + if( !( self->powered = G_FindPower( self ) ) ) + { + if( self->active ) + { + G_SetBuildableAnim( self, BANIM_CONSTRUCT2, qtrue ); + G_SetIdleBuildableAnim( self, BANIM_IDLE1 ); + self->active = qfalse; + self->enemy = NULL; + } + + self->nextthink = level.time + POWER_REFRESH_TIME; + return; + } + + if( self->spawned ) + { + VectorAdd( self->s.origin, self->r.maxs, maxs ); + VectorAdd( self->s.origin, self->r.mins, mins ); + + mins[ 2 ] += fabs( self->r.mins[ 2 ] ) + self->r.maxs[ 2 ]; + maxs[ 2 ] += 60; //player height + + //if active use the healing idle + if( self->active ) + G_SetIdleBuildableAnim( self, BANIM_IDLE2 ); + + //check if a previous occupier is still here + num = trap_EntitiesInBox( mins, maxs, entityList, MAX_GENTITIES ); + for( i = 0; i < num; i++ ) + { + player = &g_entities[ entityList[ i ] ]; + + if( player->client && player->client->ps.stats[ STAT_PTEAM ] == PTE_HUMANS ) + { + if( player->health < player->client->ps.stats[ STAT_MAX_HEALTH ] && + player->client->ps.pm_type != PM_DEAD && + self->enemy == player ) + occupied = qtrue; + } + } + + if( !occupied ) + { + self->enemy = NULL; + + //look for something to heal + for( i = 0; i < num; i++ ) + { + player = &g_entities[ entityList[ i ] ]; + + if( player->flags & FL_NOTARGET ) + continue; // notarget cancels even beneficial effects? + + if( player->client && player->client->ps.stats[ STAT_PTEAM ] == PTE_HUMANS ) + { + if( player->health < player->client->ps.stats[ STAT_MAX_HEALTH ] && + player->client->ps.pm_type != PM_DEAD ) + { + self->enemy = player; + + //start the heal anim + if( !self->active ) + { + G_SetBuildableAnim( self, BANIM_ATTACK1, qfalse ); + self->active = qtrue; + } + } + else if( !BG_InventoryContainsUpgrade( UP_MEDKIT, player->client->ps.stats ) ) + BG_AddUpgradeToInventory( UP_MEDKIT, player->client->ps.stats ); + } + } + } + + //nothing left to heal so go back to idling + if( !self->enemy && self->active ) + { + G_SetBuildableAnim( self, BANIM_CONSTRUCT2, qtrue ); + G_SetIdleBuildableAnim( self, BANIM_IDLE1 ); + + self->active = qfalse; + } + else if( self->enemy ) //heal! + { + if( self->enemy->client && self->enemy->client->ps.stats[ STAT_STATE ] & SS_POISONED ) + self->enemy->client->ps.stats[ STAT_STATE ] &= ~SS_POISONED; + + if( self->enemy->client && self->enemy->client->ps.stats[ STAT_STATE ] & SS_MEDKIT_ACTIVE ) + self->enemy->client->ps.stats[ STAT_STATE ] &= ~SS_MEDKIT_ACTIVE; + + self->enemy->health++; + + //if they're completely healed, give them a medkit + if( self->enemy->health >= self->enemy->client->ps.stats[ STAT_MAX_HEALTH ] && + !BG_InventoryContainsUpgrade( UP_MEDKIT, self->enemy->client->ps.stats ) ) + BG_AddUpgradeToInventory( UP_MEDKIT, self->enemy->client->ps.stats ); + + // if completely healed, cancel retribution + if( self->enemy->health >= self->enemy->client->ps.stats[ STAT_MAX_HEALTH ] ) + { + for( i = 0; i < MAX_CLIENTS; i++ ) + self->enemy->client->tkcredits[ i ] = 0; + } + } + } +} + + + + +//================================================================================== + + + + +/* +================ +HMGTurret_TrackEnemy + +Used by HMGTurret_Think to track enemy location +================ +*/ +qboolean HMGTurret_TrackEnemy( gentity_t *self ) +{ + vec3_t dirToTarget, dttAdjusted, angleToTarget, angularDiff, xNormal; + vec3_t refNormal = { 0.0f, 0.0f, 1.0f }; + float temp, rotAngle; + float accuracyTolerance, angularSpeed; + + if( self->lev1Grabbed ) + { + //can't turn fast if grabbed + accuracyTolerance = MGTURRET_GRAB_ACCURACYTOLERANCE; + angularSpeed = MGTURRET_GRAB_ANGULARSPEED; + } + else if( self->dcced ) + { + accuracyTolerance = MGTURRET_DCC_ACCURACYTOLERANCE; + angularSpeed = MGTURRET_DCC_ANGULARSPEED; + } + else + { + accuracyTolerance = MGTURRET_ACCURACYTOLERANCE; + angularSpeed = MGTURRET_ANGULARSPEED; + } + + VectorSubtract( self->enemy->s.pos.trBase, self->s.pos.trBase, dirToTarget ); + + VectorNormalize( dirToTarget ); + + CrossProduct( self->s.origin2, refNormal, xNormal ); + VectorNormalize( xNormal ); + rotAngle = RAD2DEG( acos( DotProduct( self->s.origin2, refNormal ) ) ); + RotatePointAroundVector( dttAdjusted, xNormal, dirToTarget, rotAngle ); + + vectoangles( dttAdjusted, angleToTarget ); + + angularDiff[ PITCH ] = AngleSubtract( self->s.angles2[ PITCH ], angleToTarget[ PITCH ] ); + angularDiff[ YAW ] = AngleSubtract( self->s.angles2[ YAW ], angleToTarget[ YAW ] ); + + //if not pointing at our target then move accordingly + if( angularDiff[ PITCH ] < (-accuracyTolerance) ) + self->s.angles2[ PITCH ] += angularSpeed; + else if( angularDiff[ PITCH ] > accuracyTolerance ) + self->s.angles2[ PITCH ] -= angularSpeed; + else + self->s.angles2[ PITCH ] = angleToTarget[ PITCH ]; + + //disallow vertical movement past a certain limit + temp = fabs( self->s.angles2[ PITCH ] ); + if( temp > 180 ) + temp -= 360; + + if( temp < -MGTURRET_VERTICALCAP ) + self->s.angles2[ PITCH ] = (-360) + MGTURRET_VERTICALCAP; + + //if not pointing at our target then move accordingly + if( angularDiff[ YAW ] < (-accuracyTolerance) ) + self->s.angles2[ YAW ] += angularSpeed; + else if( angularDiff[ YAW ] > accuracyTolerance ) + self->s.angles2[ YAW ] -= angularSpeed; + else + self->s.angles2[ YAW ] = angleToTarget[ YAW ]; + + AngleVectors( self->s.angles2, dttAdjusted, NULL, NULL ); + RotatePointAroundVector( dirToTarget, xNormal, dttAdjusted, -rotAngle ); + vectoangles( dirToTarget, self->turretAim ); + + //if pointing at our target return true + if( abs( angleToTarget[ YAW ] - self->s.angles2[ YAW ] ) <= accuracyTolerance && + abs( angleToTarget[ PITCH ] - self->s.angles2[ PITCH ] ) <= accuracyTolerance ) + return qtrue; + + return qfalse; +} + + +/* +================ +HMGTurret_CheckTarget + +Used by HMGTurret_Think to check enemies for validity +================ +*/ +qboolean HMGTurret_CheckTarget( gentity_t *self, gentity_t *target, qboolean ignorePainted ) +{ + trace_t trace; + gentity_t *traceEnt; + + if( !target ) + return qfalse; + + if( target->flags & FL_NOTARGET ) + return qfalse; + + if( !target->client ) + return qfalse; + + if( level.paused || target->client->pers.paused ) + return qfalse; + + if( target->client->ps.stats[ STAT_STATE ] & SS_HOVELING ) + return qfalse; + + if( target->health <= 0 ) + return qfalse; + + if( Distance( self->s.origin, target->s.pos.trBase ) > MGTURRET_RANGE ) + return qfalse; + + //some turret has already selected this target + if( self->dcced && target->targeted && target->targeted->powered && !ignorePainted ) + return qfalse; + + trap_Trace( &trace, self->s.pos.trBase, NULL, NULL, target->s.pos.trBase, self->s.number, MASK_SHOT ); + + traceEnt = &g_entities[ trace.entityNum ]; + + if( !traceEnt->client ) + return qfalse; + + if( traceEnt->client && traceEnt->client->ps.stats[ STAT_PTEAM ] != PTE_ALIENS ) + return qfalse; + + return qtrue; +} + + +/* +================ +HMGTurret_FindEnemy + +Used by HMGTurret_Think to locate enemy gentities +================ +*/ +void HMGTurret_FindEnemy( gentity_t *self ) +{ + int entityList[ MAX_GENTITIES ]; + vec3_t range; + vec3_t mins, maxs; + int i, num; + gentity_t *target; + + VectorSet( range, MGTURRET_RANGE, MGTURRET_RANGE, MGTURRET_RANGE ); + VectorAdd( self->s.origin, range, maxs ); + VectorSubtract( self->s.origin, range, mins ); + + //find aliens + num = trap_EntitiesInBox( mins, maxs, entityList, MAX_GENTITIES ); + for( i = 0; i < num; i++ ) + { + target = &g_entities[ entityList[ i ] ]; + + if( target->client && target->client->ps.stats[ STAT_PTEAM ] == PTE_ALIENS ) + { + //if target is not valid keep searching + if( !HMGTurret_CheckTarget( self, target, qfalse ) ) + continue; + + //we found a target + self->enemy = target; + return; + } + } + + if( self->dcced ) + { + //check again, this time ignoring painted targets + for( i = 0; i < num; i++ ) + { + target = &g_entities[ entityList[ i ] ]; + + if( target->client && target->client->ps.stats[ STAT_PTEAM ] == PTE_ALIENS ) + { + //if target is not valid keep searching + if( !HMGTurret_CheckTarget( self, target, qtrue ) ) + continue; + + //we found a target + self->enemy = target; + return; + } + } + } + + //couldn't find a target + self->enemy = NULL; +} + +#define MGTURRET_DROOPSCALE 0.5f +#define TURRET_REST_TIME 5000 +#define TURRET_REST_SPEED 3.0f +#define TURRET_REST_TOLERANCE 4.0f + +/* +================ +HMGTurret_Think + +Think function for MG turret +================ +*/ +void HMGTurret_Think( gentity_t *self ) +{ + int firespeed = BG_FindFireSpeedForBuildable( self->s.modelindex ); + + self->nextthink = level.time + BG_FindNextThinkForBuildable( self->s.modelindex ); + + //used for client side muzzle flashes + self->s.eFlags &= ~EF_FIRING; + + //if not powered don't do anything and check again for power next think + if( !( self->powered = G_FindPower( self ) ) ) + { + if( self->spawned ) + { + // unpowered turret barrel falls to bottom of range + float droop; + + droop = AngleNormalize180( self->s.angles2[ PITCH ] ); + if( droop < MGTURRET_VERTICALCAP ) + { + droop += MGTURRET_DROOPSCALE; + if( droop > MGTURRET_VERTICALCAP ) + droop = MGTURRET_VERTICALCAP; + self->s.angles2[ PITCH ] = droop; + return; + } + } + + self->nextthink = level.time + POWER_REFRESH_TIME; + return; + } + + if( self->spawned ) + { + //find a dcc for self + self->dcced = G_FindDCC( self ); + + //if the current target is not valid find a new one + if( !HMGTurret_CheckTarget( self, self->enemy, qfalse ) ) + { + if( self->enemy ) + self->enemy->targeted = NULL; + + HMGTurret_FindEnemy( self ); + } + + //if a new target cannot be found don't do anything + if( !self->enemy ) + return; + + self->enemy->targeted = self; + + //if we are pointing at our target and we can fire shoot it + if( HMGTurret_TrackEnemy( self ) && ( self->count < level.time ) ) + { + //fire at target + FireWeapon( self ); + + self->s.eFlags |= EF_FIRING; + G_AddEvent( self, EV_FIRE_WEAPON, 0 ); + G_SetBuildableAnim( self, BANIM_ATTACK1, qfalse ); + + self->count = level.time + firespeed; + } + } +} + + + + +//================================================================================== + + + + +/* +================ +HTeslaGen_Think + +Think function for Tesla Generator +================ +*/ +void HTeslaGen_Think( gentity_t *self ) +{ + int entityList[ MAX_GENTITIES ]; + vec3_t range; + vec3_t mins, maxs; + vec3_t dir; + int i, num; + gentity_t *enemy; + + self->nextthink = level.time + BG_FindNextThinkForBuildable( self->s.modelindex ); + + //if not powered don't do anything and check again for power next think + if( !( self->powered = G_FindPower( self ) ) || !( self->dcced = G_FindDCC( self ) ) ) + { + self->s.eFlags &= ~EF_FIRING; + self->nextthink = level.time + POWER_REFRESH_TIME; + return; + } + + if( self->spawned && self->count < level.time ) + { + //used to mark client side effects + self->s.eFlags &= ~EF_FIRING; + + VectorSet( range, TESLAGEN_RANGE, TESLAGEN_RANGE, TESLAGEN_RANGE ); + VectorAdd( self->s.origin, range, maxs ); + VectorSubtract( self->s.origin, range, mins ); + + //find aliens + num = trap_EntitiesInBox( mins, maxs, entityList, MAX_GENTITIES ); + for( i = 0; i < num; i++ ) + { + enemy = &g_entities[ entityList[ i ] ]; + + if( enemy->flags & FL_NOTARGET ) + continue; + + if( enemy->client && enemy->client->ps.stats[ STAT_PTEAM ] == PTE_ALIENS && + enemy->health > 0 && + !level.paused && !enemy->client->pers.paused && + Distance( enemy->s.pos.trBase, self->s.pos.trBase ) <= TESLAGEN_RANGE ) + { + VectorSubtract( enemy->s.pos.trBase, self->s.pos.trBase, dir ); + VectorNormalize( dir ); + vectoangles( dir, self->turretAim ); + + //fire at target + FireWeapon( self ); + } + } + + if( self->s.eFlags & EF_FIRING ) + { + G_AddEvent( self, EV_FIRE_WEAPON, 0 ); + + //doesn't really need an anim + //G_SetBuildableAnim( self, BANIM_ATTACK1, qfalse ); + + self->count = level.time + TESLAGEN_REPEAT; + } + } +} + + + + +//================================================================================== + + + + +/* +================ +HSpawn_Disappear + +Called when a human spawn is destroyed before it is spawned +think function +================ +*/ +void HSpawn_Disappear( gentity_t *self ) +{ + vec3_t dir; + + // we don't have a valid direction, so just point straight up + dir[ 0 ] = dir[ 1 ] = 0; + dir[ 2 ] = 1; + + self->s.eFlags |= EF_NODRAW; //don't draw the model once its destroyed + self->timestamp = level.time; + + self->think = freeBuildable; + self->nextthink = level.time + 100; + + self->r.contents = 0; //stop collisions... + trap_LinkEntity( self ); //...requires a relink +} + + +/* +================ +HSpawn_blast + +Called when a human spawn explodes +think function +================ +*/ +void HSpawn_Blast( gentity_t *self ) +{ + vec3_t dir; + + // we don't have a valid direction, so just point straight up + dir[ 0 ] = dir[ 1 ] = 0; + dir[ 2 ] = 1; + + self->s.eFlags |= EF_NODRAW; //don't draw the model once its destroyed + G_AddEvent( self, EV_HUMAN_BUILDABLE_EXPLOSION, DirToByte( dir ) ); + self->timestamp = level.time; + + //do some radius damage + G_RadiusDamage( self->s.pos.trBase, self, self->splashDamage, + self->splashRadius, self, self->splashMethodOfDeath ); + + self->think = freeBuildable; + self->nextthink = level.time + 100; + + self->r.contents = 0; //stop collisions... + trap_LinkEntity( self ); //...requires a relink +} + + +/* +================ +HSpawn_die + +Called when a human spawn dies +================ +*/ +void HSpawn_Die( gentity_t *self, gentity_t *inflictor, gentity_t *attacker, int damage, int mod ) +{ + buildHistory_t *new; + new = G_Alloc( sizeof( buildHistory_t ) ); + new->ID = ( ++level.lastBuildID > 1000 ) ? ( level.lastBuildID = 1 ) : level.lastBuildID; + new->ent = ( attacker && attacker->client ) ? attacker : NULL; + if( new->ent ) + new->name[ 0 ] = 0; + else + Q_strncpyz( new->name, "<world>", 8 ); + new->buildable = self->s.modelindex; + VectorCopy( self->s.pos.trBase, new->origin ); + VectorCopy( self->s.angles, new->angles ); + VectorCopy( self->s.origin2, new->origin2 ); + VectorCopy( self->s.angles2, new->angles2 ); + new->fate = ( attacker && attacker->client && attacker->client->ps.stats[ STAT_PTEAM ] == PTE_HUMANS ) ? BF_TEAMKILLED : BF_DESTROYED; + new->next = NULL; + G_LogBuild( new ); + + //pretty events and cleanup + G_SetBuildableAnim( self, BANIM_DESTROY1, qtrue ); + G_SetIdleBuildableAnim( self, BANIM_DESTROYED ); + + self->die = nullDieFunction; + self->powered = qfalse; //free up power + //prevent any firing effects and cancel structure protection + self->s.eFlags &= ~( EF_FIRING | EF_DBUILDER ); + + if( self->spawned ) + { + self->think = HSpawn_Blast; + self->nextthink = level.time + HUMAN_DETONATION_DELAY; + } + else + { + self->think = HSpawn_Disappear; + self->nextthink = level.time; //blast immediately + } + + if( attacker && attacker->client ) + { + if( attacker->client->ps.stats[ STAT_PTEAM ] == PTE_ALIENS ) + { + if( self->s.modelindex == BA_H_REACTOR ) + G_AddCreditToClient( attacker->client, REACTOR_VALUE, qtrue ); + else if( self->s.modelindex == BA_H_SPAWN ) + G_AddCreditToClient( attacker->client, HSPAWN_VALUE, qtrue ); + } + else + { + G_TeamCommand( PTE_HUMANS, + va( "print \"%s ^3DESTROYED^7 by teammate %s^7\n\"", + BG_FindHumanNameForBuildable( self->s.modelindex ), + attacker->client->pers.netname ) ); + G_LogOnlyPrintf("%s ^3DESTROYED^7 by teammate %s^7\n", + BG_FindHumanNameForBuildable( self->s.modelindex ), + attacker->client->pers.netname ); + } + G_LogPrintf( "Decon: %i %i %i: %s^7 destroyed %s by %s\n", + attacker->client->ps.clientNum, self->s.modelindex, mod, + attacker->client->pers.netname, + BG_FindNameForBuildable( self->s.modelindex ), + modNames[ mod ] ); + } +} + +/* +================ +HSpawn_Think + +Think for human spawn +================ +*/ +void HSpawn_Think( gentity_t *self ) +{ + gentity_t *ent; + + // spawns work without power + self->powered = qtrue; + + if( self->spawned ) + { + //only suicide if at rest + if( self->s.groundEntityNum ) + { + if( ( ent = G_CheckSpawnPoint( self->s.number, self->s.origin, + self->s.origin2, BA_H_SPAWN, NULL ) ) != NULL ) + { + // If the thing blocking the spawn is a buildable, kill it. + // If it's part of the map, kill self. + if( ent->s.eType == ET_BUILDABLE ) + { + G_Damage( ent, NULL, NULL, NULL, NULL, 10000, 0, MOD_SUICIDE ); + G_SetBuildableAnim( self, BANIM_SPAWN1, qtrue ); + } + else if( ent->s.number == ENTITYNUM_WORLD || ent->s.eType == ET_MOVER ) + { + G_Damage( self, NULL, NULL, NULL, NULL, 10000, 0, MOD_SUICIDE ); + return; + } + else if( g_antiSpawnBlock.integer && ent->client && + ent->client->ps.stats[ STAT_PTEAM ] == PTE_HUMANS ) + { + //spawnblock protection + if( self->spawnBlockTime && level.time - self->spawnBlockTime > 10000 ) + { + //five seconds of countermeasures and we're still blocked + //time for something more drastic + G_Damage( ent, NULL, NULL, NULL, NULL, 10000, 0, MOD_TRIGGER_HURT ); + self->spawnBlockTime += 2000; + //inappropriate MOD but prints an apt obituary + } + else if( self->spawnBlockTime && level.time - self->spawnBlockTime > 5000 ) + //five seconds of blocked by client and... + { + //random direction + vec3_t velocity; + velocity[0] = crandom() * g_antiSpawnBlock.integer; + velocity[1] = crandom() * g_antiSpawnBlock.integer; + velocity[2] = g_antiSpawnBlock.integer; + + VectorAdd( ent->client->ps.velocity, velocity, ent->client->ps.velocity ); + trap_SendServerCommand( ent-g_entities, "cp \"Don't spawn block!\"" ); + } + else if( !self->spawnBlockTime ) + self->spawnBlockTime = level.time; + } + + if( ent->s.eType == ET_CORPSE ) + G_FreeEntity( ent ); //quietly remove + } + else + self->spawnBlockTime = 0; + } + + //spawn under attack + if( self->health < self->lastHealth && + level.time > level.humanBaseAttackTimer && G_IsDCCBuilt( ) ) + { + level.humanBaseAttackTimer = level.time + DCC_ATTACK_PERIOD; + G_BroadcastEvent( EV_DCC_ATTACK, 0 ); + } + + self->lastHealth = self->health; + } + + self->nextthink = level.time + BG_FindNextThinkForBuildable( self->s.modelindex ); +} + + + + +//================================================================================== + + +/* +============ +G_BuildableTouchTriggers + +Find all trigger entities that a buildable touches. +============ +*/ +void G_BuildableTouchTriggers( gentity_t *ent ) +{ + int i, num; + int touch[ MAX_GENTITIES ]; + gentity_t *hit; + trace_t trace; + vec3_t mins, maxs; + vec3_t bmins, bmaxs; + static vec3_t range = { 10, 10, 10 }; + + // dead buildables don't activate triggers! + if( ent->health <= 0 ) + return; + + BG_FindBBoxForBuildable( ent->s.modelindex, bmins, bmaxs ); + + VectorAdd( ent->s.origin, bmins, mins ); + VectorAdd( ent->s.origin, bmaxs, maxs ); + + VectorSubtract( mins, range, mins ); + VectorAdd( maxs, range, maxs ); + + num = trap_EntitiesInBox( mins, maxs, touch, MAX_GENTITIES ); + + VectorAdd( ent->s.origin, bmins, mins ); + VectorAdd( ent->s.origin, bmaxs, maxs ); + + for( i = 0; i < num; i++ ) + { + hit = &g_entities[ touch[ i ] ]; + + if( !hit->touch ) + continue; + + if( !( hit->r.contents & CONTENTS_TRIGGER ) ) + continue; + + //ignore buildables not yet spawned + if( !ent->spawned ) + continue; + + if( !trap_EntityContact( mins, maxs, hit ) ) + continue; + + memset( &trace, 0, sizeof( trace ) ); + + if( hit->touch ) + hit->touch( hit, ent, &trace ); + } +} + + +/* +=============== +G_BuildableThink + +General think function for buildables +=============== +*/ +void G_BuildableThink( gentity_t *ent, int msec ) +{ + int bHealth = BG_FindHealthForBuildable( ent->s.modelindex ); + int bRegen = BG_FindRegenRateForBuildable( ent->s.modelindex ); + int bTime = BG_FindBuildTimeForBuildable( ent->s.modelindex ); + + //pack health, power and dcc + + //toggle spawned flag for buildables + if( !ent->spawned && ent->health > 0 ) + { + if( ent->buildTime + bTime < level.time ) + ent->spawned = qtrue; + } + + ent->s.generic1 = (int)( ( (float)ent->health / (float)bHealth ) * B_HEALTH_MASK ); + + if( ent->s.generic1 < 0 ) + ent->s.generic1 = 0; + + if( ent->powered ) + ent->s.generic1 |= B_POWERED_TOGGLEBIT; + + if( ent->dcced ) + ent->s.generic1 |= B_DCCED_TOGGLEBIT; + + if( ent->spawned ) + ent->s.generic1 |= B_SPAWNED_TOGGLEBIT; + + if( ent->deconstruct ) + ent->s.generic1 |= B_MARKED_TOGGLEBIT; + + ent->time1000 += msec; + + if( ent->time1000 >= 1000 ) + { + ent->time1000 -= 1000; + + if( !ent->spawned && ent->health > 0 ) + ent->health += (int)( ceil( (float)bHealth / (float)( bTime * 0.001 ) ) ); + else if( ent->biteam == BIT_ALIENS && ent->health > 0 && ent->health < bHealth && + bRegen && ( ent->lastDamageTime + ALIEN_REGEN_DAMAGE_TIME ) < level.time ) + ent->health += bRegen; + + if( ent->health > bHealth ) + ent->health = bHealth; + } + + if( ent->lev1Grabbed && ent->lev1GrabTime + LEVEL1_GRAB_TIME < level.time ) + ent->lev1Grabbed = qfalse; + + if( ent->clientSpawnTime > 0 ) + ent->clientSpawnTime -= msec; + + if( ent->clientSpawnTime < 0 ) + ent->clientSpawnTime = 0; + + //check if this buildable is touching any triggers + G_BuildableTouchTriggers( ent ); + + //fall back on normal physics routines + G_Physics( ent, msec ); +} + + +/* +=============== +G_BuildableRange + +Check whether a point is within some range of a type of buildable +=============== +*/ +qboolean G_BuildableRange( vec3_t origin, float r, buildable_t buildable ) +{ + int entityList[ MAX_GENTITIES ]; + vec3_t range; + vec3_t mins, maxs; + int i, num; + gentity_t *ent; + + VectorSet( range, r, r, r ); + VectorAdd( origin, range, maxs ); + VectorSubtract( origin, range, mins ); + + num = trap_EntitiesInBox( mins, maxs, entityList, MAX_GENTITIES ); + for( i = 0; i < num; i++ ) + { + ent = &g_entities[ entityList[ i ] ]; + + if( ent->s.eType != ET_BUILDABLE ) + continue; + + if( ent->biteam == BIT_HUMANS && !ent->powered ) + continue; + + if( ent->s.modelindex == buildable && ent->spawned ) + return qtrue; + } + + return qfalse; +} + +static qboolean G_BoundsIntersect(const vec3_t mins, const vec3_t maxs, + const vec3_t mins2, const vec3_t maxs2) +{ + if ( maxs[0] < mins2[0] || + maxs[1] < mins2[1] || + maxs[2] < mins2[2] || + mins[0] > maxs2[0] || + mins[1] > maxs2[1] || + mins[2] > maxs2[2]) + { + return qfalse; + } + + return qtrue; +} + +/* +=============== +G_BuildablesIntersect + +Test if two buildables intersect each other +=============== +*/ +static qboolean G_BuildablesIntersect( buildable_t a, vec3_t originA, + buildable_t b, vec3_t originB ) +{ + vec3_t minsA, maxsA; + vec3_t minsB, maxsB; + + BG_FindBBoxForBuildable( a, minsA, maxsA ); + VectorAdd( minsA, originA, minsA ); + VectorAdd( maxsA, originA, maxsA ); + + BG_FindBBoxForBuildable( b, minsB, maxsB ); + VectorAdd( minsB, originB, minsB ); + VectorAdd( maxsB, originB, maxsB ); + + return G_BoundsIntersect( minsA, maxsA, minsB, maxsB ); +} + +/* +=============== +G_CompareBuildablesForRemoval + +qsort comparison function for a buildable removal list +=============== +*/ +static buildable_t cmpBuildable; +static vec3_t cmpOrigin; +static int G_CompareBuildablesForRemoval( const void *a, const void *b ) +{ + int precedence[ ] = + { + BA_NONE, + + BA_A_BARRICADE, + BA_A_ACIDTUBE, + BA_A_TRAPPER, + BA_A_HIVE, + BA_A_BOOSTER, + BA_A_HOVEL, + BA_A_SPAWN, + BA_A_OVERMIND, + + BA_H_MGTURRET, + BA_H_TESLAGEN, + BA_H_DCC, + BA_H_MEDISTAT, + BA_H_ARMOURY, + BA_H_SPAWN, + BA_H_REPEATER, + BA_H_REACTOR + }; + + gentity_t *buildableA, *buildableB; + int i; + int aPrecedence = 0, bPrecedence = 0; + qboolean aMatches = qfalse, bMatches = qfalse; + + buildableA = *(gentity_t **)a; + buildableB = *(gentity_t **)b; + + + // Prefer the one that collides with the thing we're building + aMatches = G_BuildablesIntersect( cmpBuildable, cmpOrigin, + buildableA->s.modelindex, buildableA->s.origin ); + bMatches = G_BuildablesIntersect( cmpBuildable, cmpOrigin, + buildableB->s.modelindex, buildableB->s.origin ); + if( aMatches && !bMatches ) + return -1; + if( !aMatches && bMatches ) + return 1; + + // If one matches the thing we're building, prefer it + aMatches = ( buildableA->s.modelindex == cmpBuildable ); + bMatches = ( buildableB->s.modelindex == cmpBuildable ); + if( aMatches && !bMatches ) + return -1; + if( !aMatches && bMatches ) + return 1; + + // If they're the same type then pick the one marked earliest + if( buildableA->s.modelindex == buildableB->s.modelindex ) + return buildableA->deconstructTime - buildableB->deconstructTime; + + for( i = 0; i < sizeof( precedence ) / sizeof( precedence[ 0 ] ); i++ ) + { + if( buildableA->s.modelindex == precedence[ i ] ) + aPrecedence = i; + + if( buildableB->s.modelindex == precedence[ i ] ) + bPrecedence = i; + } + + return aPrecedence - bPrecedence; +} + +/* +=============== +G_FreeMarkedBuildables + +Free up build points for a team by deconstructing marked buildables +=============== +*/ +void G_FreeMarkedBuildables( void ) +{ + int i; + gentity_t *ent; + buildHistory_t *new, *last; + last = level.buildHistory; + + if( !g_markDeconstruct.integer ) + return; // Not enabled, can't deconstruct anything + + for( i = 0; i < level.numBuildablesForRemoval; i++ ) + { + ent = level.markedBuildables[ i ]; + + new = G_Alloc( sizeof( buildHistory_t ) ); + new->ID = -1; + new->ent = NULL; + Q_strncpyz( new->name, "<markdecon>", 12 ); + new->buildable = ent->s.modelindex; + VectorCopy( ent->s.pos.trBase, new->origin ); + VectorCopy( ent->s.angles, new->angles ); + VectorCopy( ent->s.origin2, new->origin2 ); + VectorCopy( ent->s.angles2, new->angles2 ); + new->fate = BF_DECONNED; + new->next = NULL; + new->marked = NULL; + + last = last->marked = new; + + G_FreeEntity( ent ); + } +} + +/* +=============== +G_SufficientBPAvailable + +Determine if enough build points can be released for the buildable +and list the buildables that much be destroyed if this is the case +=============== +*/ +static itemBuildError_t G_SufficientBPAvailable( buildable_t buildable, + vec3_t origin ) +{ + int i; + int numBuildables = 0; + int pointsYielded = 0; + gentity_t *ent; + qboolean unique = BG_FindUniqueTestForBuildable( buildable ); + int remainingBP, remainingSpawns; + int team; + int buildPoints, buildpointsneeded; + qboolean collision = qfalse; + int collisionCount = 0; + qboolean repeaterInRange = qfalse; + int repeaterInRangeCount = 0; + itemBuildError_t bpError; + buildable_t spawn; + buildable_t core; + int spawnCount = 0; + + level.numBuildablesForRemoval = 0; + + buildPoints = buildpointsneeded = BG_FindBuildPointsForBuildable( buildable ); + team = BG_FindTeamForBuildable( buildable ); + if( team == BIT_ALIENS ) + { + remainingBP = level.alienBuildPoints; + remainingSpawns = level.numAlienSpawns; + bpError = IBE_NOASSERT; + spawn = BA_A_SPAWN; + core = BA_A_OVERMIND; + } + else if( team == BIT_HUMANS ) + { + remainingBP = level.humanBuildPoints; + remainingSpawns = level.numHumanSpawns; + bpError = IBE_NOPOWER; + spawn = BA_H_SPAWN; + core = BA_H_REACTOR; + } + else + return IBE_NONE; + + // Simple non-marking case + if( !g_markDeconstruct.integer ) + { + if( remainingBP - buildPoints < 0 ) + return bpError; + + // Check for buildable<->buildable collisions + for( i = MAX_CLIENTS, ent = g_entities + i; i < level.num_entities; i++, ent++ ) + { + if( ent->s.eType != ET_BUILDABLE ) + continue; + + if( G_BuildablesIntersect( buildable, origin, ent->s.modelindex, ent->s.origin ) ) + return IBE_NOROOM; + } + + return IBE_NONE; + } + + buildpointsneeded -= remainingBP; + + // Set buildPoints to the number extra that are required + if( !g_markDeconstructMode.integer ) + buildPoints -= remainingBP; + + // Build a list of buildable entities + for( i = MAX_CLIENTS, ent = g_entities + i; i < level.num_entities; i++, ent++ ) + { + if( ent->s.eType != ET_BUILDABLE ) + continue; + + collision = G_BuildablesIntersect( buildable, origin, ent->s.modelindex, ent->s.origin ); + + if( collision ) + collisionCount++; + + // Check if this is a repeater and it's in range + if( buildable == BA_H_REPEATER && + buildable == ent->s.modelindex && + Distance( ent->s.origin, origin ) < REPEATER_BASESIZE ) + { + repeaterInRange = qtrue; + repeaterInRangeCount++; + } + else + repeaterInRange = qfalse; + + if( !ent->inuse ) + continue; + + if( ent->health <= 0 ) + continue; + + if( ent->biteam != team ) + continue; + + // Don't allow destruction of hovel with granger inside + if( ent->s.modelindex == BA_A_HOVEL && ent->active ) + { + if( buildable == BA_A_HOVEL ) + return IBE_HOVEL; + else + continue; + } + + // Explicitly disallow replacement of the core buildable with anything + // other than the core buildable + if( ent->s.modelindex == core && buildable != core ) + continue; + + if( ent->deconstruct ) + { + level.markedBuildables[ numBuildables++ ] = ent; + + if( collision || repeaterInRange ) + { + if( collision ) + collisionCount--; + + if( repeaterInRange ) + repeaterInRangeCount--; + + pointsYielded += BG_FindBuildPointsForBuildable( ent->s.modelindex ); + level.numBuildablesForRemoval++; + } + else if( unique && ent->s.modelindex == buildable ) + { + // If it's a unique buildable, it must be replaced by the same type + pointsYielded += BG_FindBuildPointsForBuildable( ent->s.modelindex ); + level.numBuildablesForRemoval++; + } + } + } + + // We still need build points, but have no candidates for removal + if( buildpointsneeded > 0 && numBuildables == 0 ) + return bpError; + + // Collided with something we can't remove + if( collisionCount > 0 ) + return IBE_NOROOM; + + // There are one or more repeaters we can't remove + if( repeaterInRangeCount > 0 ) + return IBE_RPTWARN2; + + // Sort the list + cmpBuildable = buildable; + VectorCopy( origin, cmpOrigin ); + qsort( level.markedBuildables, numBuildables, sizeof( level.markedBuildables[ 0 ] ), + G_CompareBuildablesForRemoval ); + + // Determine if there are enough markees to yield the required BP + for( ; pointsYielded < buildPoints && level.numBuildablesForRemoval < numBuildables; + level.numBuildablesForRemoval++ ) + { + ent = level.markedBuildables[ level.numBuildablesForRemoval ]; + pointsYielded += BG_FindBuildPointsForBuildable( ent->s.modelindex ); + } + + // Make sure we're not removing the last spawn + for( i = 0; i < level.numBuildablesForRemoval; i++ ) + { + if( level.markedBuildables[ i ]->s.modelindex == spawn ) + spawnCount++; + } + if( !g_cheats.integer && remainingSpawns > 0 && ( remainingSpawns - spawnCount ) < 1 ) + return IBE_NORMAL; + + // Not enough points yielded + if( pointsYielded < buildpointsneeded ) + return bpError; + + return IBE_NONE; +} + +/* +================ +G_SetBuildableLinkState + +Links or unlinks all the buildable entities +================ +*/ +static void G_SetBuildableLinkState( qboolean link ) +{ + int i; + gentity_t *ent; + + for ( i = 1, ent = g_entities + i; i < level.num_entities; i++, ent++ ) + { + if( ent->s.eType != ET_BUILDABLE ) + continue; + + if( link ) + trap_LinkEntity( ent ); + else + trap_UnlinkEntity( ent ); + } +} + +static void G_SetBuildableMarkedLinkState( qboolean link ) +{ + int i; + gentity_t *ent; + + for( i = 0; i < level.numBuildablesForRemoval; i++ ) + { + ent = level.markedBuildables[ i ]; + if( link ) + trap_LinkEntity( ent ); + else + trap_UnlinkEntity( ent ); + } +} + +/* +================ +G_CanBuild + +Checks to see if a buildable can be built +================ +*/ +itemBuildError_t G_CanBuild( gentity_t *ent, buildable_t buildable, int distance, vec3_t origin ) +{ + vec3_t angles; + vec3_t entity_origin, normal; + vec3_t mins, maxs, nbmins, nbmaxs; + vec3_t nbVect; + trace_t tr1, tr2, tr3; + int i; + itemBuildError_t reason = IBE_NONE; + gentity_t *tempent; + float minNormal; + qboolean invert; + int contents; + playerState_t *ps = &ent->client->ps; + int buildPoints; + gentity_t *tmp; + itemBuildError_t tempReason; + + // Stop all buildables from interacting with traces + G_SetBuildableLinkState( qfalse ); + + BG_FindBBoxForBuildable( buildable, mins, maxs ); + + BG_PositionBuildableRelativeToPlayer( ps, mins, maxs, trap_Trace, entity_origin, angles, &tr1 ); + + trap_Trace( &tr2, entity_origin, mins, maxs, entity_origin, ent->s.number, MASK_PLAYERSOLID ); + trap_Trace( &tr3, ps->origin, NULL, NULL, entity_origin, ent->s.number, MASK_PLAYERSOLID ); + + VectorCopy( entity_origin, origin ); + + VectorCopy( tr1.plane.normal, normal ); + minNormal = BG_FindMinNormalForBuildable( buildable ); + invert = BG_FindInvertNormalForBuildable( buildable ); + + //can we build at this angle? + if( !( normal[ 2 ] >= minNormal || ( invert && normal[ 2 ] <= -minNormal ) ) ) + reason = IBE_NORMAL; + + if( tr1.entityNum != ENTITYNUM_WORLD ) + reason = IBE_NORMAL; + + contents = trap_PointContents( entity_origin, -1 ); + buildPoints = BG_FindBuildPointsForBuildable( buildable ); + + //check if we are near a nobuild marker, if so, can't build here... + for( i = 0; i < MAX_GENTITIES; i++ ) + { + tmp = &g_entities[ i ]; + + if( !tmp->noBuild.isNB ) + continue; + + nbVect[0] = tmp->noBuild.Area; + nbVect[1] = tmp->noBuild.Area; + nbVect[2] = tmp->noBuild.Height; + + VectorSubtract( origin, nbVect, nbmins ); + VectorAdd( origin, nbVect, nbmaxs ); + + if( trap_EntityContact( nbmins, nbmaxs, tmp ) ) + reason = IBE_PERMISSION; + + } + if( ent->client->ps.stats[ STAT_PTEAM ] == PTE_ALIENS ) + { + //alien criteria + + if( buildable == BA_A_HOVEL ) + { + vec3_t builderMins, builderMaxs; + + //this assumes the adv builder is the biggest thing that'll use the hovel + BG_FindBBoxForClass( PCL_ALIEN_BUILDER0_UPG, builderMins, builderMaxs, NULL, NULL, NULL ); + + if( APropHovel_Blocked( origin, angles, normal, ent ) ) + reason = IBE_HOVELEXIT; + } + + //check there is creep near by for building on + if( BG_FindCreepTestForBuildable( buildable ) ) + { + if( !G_IsCreepHere( entity_origin ) ) + reason = IBE_NOCREEP; + } + + //check permission to build here + if( tr1.surfaceFlags & SURF_NOALIENBUILD || tr1.surfaceFlags & SURF_NOBUILD || + contents & CONTENTS_NOALIENBUILD || contents & CONTENTS_NOBUILD ) + reason = IBE_PERMISSION; + + //look for an Overmind + for ( i = 1, tempent = g_entities + i; i < level.num_entities; i++, tempent++ ) + { + if( tempent->s.eType != ET_BUILDABLE ) + continue; + if( tempent->s.modelindex == BA_A_OVERMIND && tempent->spawned && + tempent->health > 0 ) + break; + } + + //if none found... + if( i >= level.num_entities && buildable != BA_A_OVERMIND ) + reason = IBE_NOOVERMIND; + + //can we only have one of these? + if( BG_FindUniqueTestForBuildable( buildable ) ) + { + for ( i = 1, tempent = g_entities + i; i < level.num_entities; i++, tempent++ ) + { + if( tempent->s.eType != ET_BUILDABLE ) + continue; + + if( tempent->s.modelindex == buildable && !tempent->deconstruct ) + { + switch( buildable ) + { + case BA_A_OVERMIND: + reason = IBE_OVERMIND; + break; + + case BA_A_HOVEL: + reason = IBE_HOVEL; + break; + + default: + Com_Error( ERR_FATAL, "No reason for denying build of %d\n", buildable ); + break; + } + + break; + } + } + } + } + else if( ent->client->ps.stats[ STAT_PTEAM ] == PTE_HUMANS ) + { + //human criteria + if( !G_IsPowered( entity_origin ) ) + { + //tell player to build a repeater to provide power + if( buildable != BA_H_REACTOR && buildable != BA_H_REPEATER ) + reason = IBE_REPEATER; + } + + //this buildable requires a DCC + if( BG_FindDCCTestForBuildable( buildable ) && !G_IsDCCBuilt( ) ) + reason = IBE_NODCC; + + //check that there is a parent reactor when building a repeater + if( buildable == BA_H_REPEATER ) + { + for ( i = 1, tempent = g_entities + i; i < level.num_entities; i++, tempent++ ) + { + if( tempent->s.eType != ET_BUILDABLE ) + continue; + + if( tempent->s.modelindex == BA_H_REACTOR ) + break; + } + + if( i >= level.num_entities ) + { + //no reactor present + + //check for other nearby repeaters + for ( i = 1, tempent = g_entities + i; i < level.num_entities; i++, tempent++ ) + { + if( tempent->s.eType != ET_BUILDABLE ) + continue; + + if( tempent->s.modelindex == BA_H_REPEATER && + Distance( tempent->s.origin, entity_origin ) < REPEATER_BASESIZE ) + { + reason = IBE_RPTWARN2; + break; + } + } + + if( reason == IBE_NONE ) + reason = IBE_RPTWARN; + } + else if( G_IsPowered( entity_origin ) ) + reason = IBE_RPTWARN2; + } + + //check permission to build here + if( tr1.surfaceFlags & SURF_NOHUMANBUILD || tr1.surfaceFlags & SURF_NOBUILD || + contents & CONTENTS_NOHUMANBUILD || contents & CONTENTS_NOBUILD ) + reason = IBE_PERMISSION; + + //can we only build one of these? + if( BG_FindUniqueTestForBuildable( buildable ) ) + { + for ( i = 1, tempent = g_entities + i; i < level.num_entities; i++, tempent++ ) + { + if( tempent->s.eType != ET_BUILDABLE ) + continue; + + if( tempent->s.modelindex == BA_H_REACTOR && !tempent->deconstruct ) + { + reason = IBE_REACTOR; + break; + } + } + } + } + + if( ( tempReason = G_SufficientBPAvailable( buildable, origin ) ) != IBE_NONE ) + reason = tempReason; + + // Relink buildables + G_SetBuildableLinkState( qtrue ); + + //check there is enough room to spawn from (presuming this is a spawn) + if( reason == IBE_NONE ) + { + G_SetBuildableMarkedLinkState( qfalse ); + if( G_CheckSpawnPoint( ENTITYNUM_NONE, origin, normal, buildable, NULL ) != NULL ) + reason = IBE_NORMAL; + G_SetBuildableMarkedLinkState( qtrue ); + } + + //this item does not fit here + if( reason == IBE_NONE && ( tr2.fraction < 1.0 || tr3.fraction < 1.0 ) ) + return IBE_NOROOM; + + if( reason != IBE_NONE ) + level.numBuildablesForRemoval = 0; + + return reason; +} + +/* +============== +G_BuildingExists +============== +*/ +qboolean G_BuildingExists( int bclass ) +{ + int i; + gentity_t *tempent; + //look for an Armoury + for (i = 1, tempent = g_entities + i; i < level.num_entities; i++, tempent++ ) + { + if( tempent->s.eType != ET_BUILDABLE ) + continue; + if( tempent->s.modelindex == bclass && tempent->health > 0 ) + { + return qtrue; + } + } + return qfalse; +} + + +/* +================ +G_Build + +Spawns a buildable +================ +*/ +static gentity_t *G_Build( gentity_t *builder, buildable_t buildable, vec3_t origin, vec3_t angles ) +{ + gentity_t *built; + buildHistory_t *new; + vec3_t normal; + + // initialise the buildhistory so other functions can use it + if( builder && builder->client ) + { + new = G_Alloc( sizeof( buildHistory_t ) ); + G_LogBuild( new ); + } + + // Free existing buildables + G_FreeMarkedBuildables( ); + + //spawn the buildable + built = G_Spawn(); + + built->s.eType = ET_BUILDABLE; + + built->classname = BG_FindEntityNameForBuildable( buildable ); + + built->s.modelindex = buildable; //so we can tell what this is on the client side + built->biteam = built->s.modelindex2 = BG_FindTeamForBuildable( buildable ); + + BG_FindBBoxForBuildable( buildable, built->r.mins, built->r.maxs ); + + // detect the buildable's normal vector + if( !builder->client ) + { + // initial base layout created by server + + if( builder->s.origin2[ 0 ] + || builder->s.origin2[ 1 ] + || builder->s.origin2[ 2 ] ) + { + VectorCopy( builder->s.origin2, normal ); + } + else if( BG_FindTrajectoryForBuildable( buildable ) == TR_BUOYANCY ) + VectorSet( normal, 0.0f, 0.0f, -1.0f ); + else + VectorSet( normal, 0.0f, 0.0f, 1.0f ); + } + else + { + // in-game building by a player + + if( builder->client->ps.stats[ STAT_STATE ] & SS_WALLCLIMBING ) + { + if( builder->client->ps.stats[ STAT_STATE ] & SS_WALLCLIMBINGCEILING ) + VectorSet( normal, 0.0f, 0.0f, -1.0f ); + else + VectorCopy( builder->client->ps.grapplePoint, normal ); + } + else + VectorSet( normal, 0.0f, 0.0f, 1.0f ); + } + + // when building the initial layout, spawn the entity slightly off its + // target surface so that it can be "dropped" onto it + if( !builder->client ) + VectorMA( origin, 1.0f, normal, origin ); + + built->health = 1; + + built->splashDamage = BG_FindSplashDamageForBuildable( buildable ); + built->splashRadius = BG_FindSplashRadiusForBuildable( buildable ); + built->splashMethodOfDeath = BG_FindMODForBuildable( buildable ); + + built->nextthink = BG_FindNextThinkForBuildable( buildable ); + + built->takedamage = qtrue; + built->spawned = qfalse; + built->buildTime = built->s.time = level.time; + built->spawnBlockTime = 0; + + // build instantly in cheat mode + if( builder->client && g_cheats.integer ) + { + built->health = BG_FindHealthForBuildable( buildable ); + built->buildTime = built->s.time = + level.time - BG_FindBuildTimeForBuildable( buildable ); + } + + //things that vary for each buildable that aren't in the dbase + switch( buildable ) + { + case BA_A_SPAWN: + built->die = ASpawn_Die; + built->think = ASpawn_Think; + built->pain = ASpawn_Pain; + break; + + case BA_A_BARRICADE: + built->die = ABarricade_Die; + built->think = ABarricade_Think; + built->pain = ABarricade_Pain; + break; + + case BA_A_BOOSTER: + built->die = ABarricade_Die; + built->think = ABarricade_Think; + built->pain = ABarricade_Pain; + built->touch = ABooster_Touch; + break; + + case BA_A_ACIDTUBE: + built->die = ABarricade_Die; + built->think = AAcidTube_Think; + built->pain = ASpawn_Pain; + break; + + case BA_A_HIVE: + built->die = ABarricade_Die; + built->think = AHive_Think; + built->pain = ASpawn_Pain; + break; + + case BA_A_TRAPPER: + built->die = ABarricade_Die; + built->think = ATrapper_Think; + built->pain = ASpawn_Pain; + break; + + case BA_A_OVERMIND: + built->die = ASpawn_Die; + built->think = AOvermind_Think; + built->pain = ASpawn_Pain; + break; + + case BA_A_HOVEL: + built->die = AHovel_Die; + built->use = AHovel_Use; + built->think = AHovel_Think; + built->pain = ASpawn_Pain; + break; + + case BA_H_SPAWN: + built->die = HSpawn_Die; + built->think = HSpawn_Think; + break; + + case BA_H_MGTURRET: + built->die = HSpawn_Die; + built->think = HMGTurret_Think; + break; + + case BA_H_TESLAGEN: + built->die = HSpawn_Die; + built->think = HTeslaGen_Think; + break; + + case BA_H_ARMOURY: + built->think = HArmoury_Think; + built->die = HSpawn_Die; + built->use = HArmoury_Activate; + break; + + case BA_H_DCC: + built->think = HDCC_Think; + built->die = HSpawn_Die; + break; + + case BA_H_MEDISTAT: + built->think = HMedistat_Think; + built->die = HSpawn_Die; + break; + + case BA_H_REACTOR: + built->think = HReactor_Think; + built->die = HSpawn_Die; + built->use = HRepeater_Use; + built->powered = built->active = qtrue; + break; + + case BA_H_REPEATER: + built->think = HRepeater_Think; + built->die = HSpawn_Die; + built->use = HRepeater_Use; + built->count = -1; + break; + + default: + //erk + break; + } + + built->s.number = built - g_entities; + built->r.contents = CONTENTS_BODY; + built->clipmask = MASK_PLAYERSOLID; + built->enemy = NULL; + built->s.weapon = BG_FindProjTypeForBuildable( buildable ); + + if( builder->client ) + { + built->builtBy = builder->client->ps.clientNum; + + if( builder->client->pers.designatedBuilder ) + { + built->s.eFlags |= EF_DBUILDER; // designated builder protection + } + } + else + built->builtBy = -1; + + G_SetOrigin( built, origin ); + + // gently nudge the buildable onto the surface :) + VectorScale( normal, -50.0f, built->s.pos.trDelta ); + + // set turret angles + VectorCopy( builder->s.angles2, built->s.angles2 ); + + VectorCopy( angles, built->s.angles ); + built->s.angles[ PITCH ] = 0.0f; + built->s.angles2[ YAW ] = angles[ YAW ]; + built->s.pos.trType = BG_FindTrajectoryForBuildable( buildable ); + built->s.pos.trTime = level.time; + built->physicsBounce = BG_FindBounceForBuildable( buildable ); + built->s.groundEntityNum = -1; + + built->s.generic1 = (int)( ( (float)built->health / + (float)BG_FindHealthForBuildable( buildable ) ) * B_HEALTH_MASK ); + + if( built->s.generic1 < 0 ) + built->s.generic1 = 0; + + if( BG_FindTeamForBuildable( built->s.modelindex ) == PTE_ALIENS ) + { + built->powered = qtrue; + built->s.generic1 |= B_POWERED_TOGGLEBIT; + } + else if( ( built->powered = G_FindPower( built ) ) ) + built->s.generic1 |= B_POWERED_TOGGLEBIT; + + if( ( built->dcced = G_FindDCC( built ) ) ) + built->s.generic1 |= B_DCCED_TOGGLEBIT; + + built->s.generic1 &= ~B_SPAWNED_TOGGLEBIT; + + VectorCopy( normal, built->s.origin2 ); + + G_AddEvent( built, EV_BUILD_CONSTRUCT, 0 ); + + G_SetIdleBuildableAnim( built, BG_FindAnimForBuildable( buildable ) ); + + if( built->builtBy >= 0 ) + G_SetBuildableAnim( built, BANIM_CONSTRUCT1, qtrue ); + + trap_LinkEntity( built ); + + + if( builder->client ) + { + builder->client->pers.statscounters.structsbuilt++; + if( builder->client->pers.teamSelection == PTE_ALIENS ) + { + level.alienStatsCounters.structsbuilt++; + } + else if( builder->client->pers.teamSelection == PTE_HUMANS ) + { + level.humanStatsCounters.structsbuilt++; + } + } + + if( builder->client ) { + G_TeamCommand( builder->client->pers.teamSelection, + va( "print \"%s is ^2being built^7 by %s^7\n\"", + BG_FindHumanNameForBuildable( built->s.modelindex ), + builder->client->pers.netname ) ); + G_LogPrintf("Build: %i %i 0: %s^7 is ^2building^7 %s\n", + builder->client->ps.clientNum, + built->s.modelindex, + builder->client->pers.netname, + BG_FindNameForBuildable( built->s.modelindex ) ); + } + + // ok we're all done building, so what we log here should be the final values + if( builder && builder->client ) // log ingame building only + { + new = level.buildHistory; + new->ID = ( ++level.lastBuildID > 1000 ) ? ( level.lastBuildID = 1 ) : level.lastBuildID; + new->ent = builder; + new->name[ 0 ] = 0; + new->buildable = buildable; + VectorCopy( built->s.pos.trBase, new->origin ); + VectorCopy( built->s.angles, new->angles ); + VectorCopy( built->s.origin2, new->origin2 ); + VectorCopy( built->s.angles2, new->angles2 ); + new->fate = BF_BUILT; + } + + if( builder && builder->client ) + built->bdnumb = new->ID; + else + built->bdnumb = -1; + + return built; +} + +static void G_SpawnMarker( vec3_t origin ) +{ + gentity_t *nb; + int i; + + // Make the marker... + nb = G_Spawn( ); + nb->s.modelindex = 0; //Coder humor is win + VectorCopy( origin, nb->s.pos.trBase ); + VectorCopy( origin, nb->r.currentOrigin ); + nb->noBuild.isNB = qtrue; + nb->noBuild.Area = level.nbArea; + nb->noBuild.Height = level.nbHeight; + trap_LinkEntity( nb ); + + // Log markers made... + for( i = 0; i < MAX_GENTITIES; i++ ) + { + if( level.nbMarkers[ i ].Marker != NULL ) + continue; + + level.nbMarkers[ i ].Marker = nb; + VectorCopy( origin, level.nbMarkers[ i ].Origin ); + SnapVector( level.nbMarkers[ i ].Origin ); + break; + } + + // End nobuild mode... + level.noBuilding = qfalse; + level.nbArea = 0.0f; + level.nbHeight = 0.0f; +} + +/* +================= +G_BuildIfValid +================= +*/ +qboolean G_BuildIfValid( gentity_t *ent, buildable_t buildable ) +{ + float dist; + vec3_t origin; + + dist = BG_FindBuildDistForClass( ent->client->ps.stats[ STAT_PCLASS ] ); + + switch( G_CanBuild( ent, buildable, dist, origin ) ) + { + case IBE_NONE: + if( level.noBuilding ) + { + vec3_t mins; + BG_FindBBoxForBuildable( buildable, mins, NULL ); + origin[2] += mins[2]; + + G_SpawnMarker( origin ); + return qtrue; + } + G_Build( ent, buildable, origin, ent->s.apos.trBase ); + return qtrue; + + case IBE_NOASSERT: + G_TriggerMenu( ent->client->ps.clientNum, MN_A_NOASSERT ); + return qfalse; + + case IBE_NOOVERMIND: + G_TriggerMenu( ent->client->ps.clientNum, MN_A_NOOVMND ); + return qfalse; + + case IBE_NOCREEP: + G_TriggerMenu( ent->client->ps.clientNum, MN_A_NOCREEP ); + return qfalse; + + case IBE_OVERMIND: + G_TriggerMenu( ent->client->ps.clientNum, MN_A_OVERMIND ); + return qfalse; + + case IBE_HOVEL: + G_TriggerMenu( ent->client->ps.clientNum, MN_A_HOVEL ); + return qfalse; + + case IBE_HOVELEXIT: + G_TriggerMenu( ent->client->ps.clientNum, MN_A_HOVEL_EXIT ); + return qfalse; + + case IBE_NORMAL: + if( ent->client->ps.stats[ STAT_PTEAM ] == PTE_HUMANS ) + G_TriggerMenu( ent->client->ps.clientNum, MN_H_NORMAL ); + else + G_TriggerMenu( ent->client->ps.clientNum, MN_A_NORMAL ); + return qfalse; + + case IBE_PERMISSION: + if( ent->client->ps.stats[ STAT_PTEAM ] == PTE_HUMANS ) + G_TriggerMenu( ent->client->ps.clientNum, MN_H_NORMAL ); + else + G_TriggerMenu( ent->client->ps.clientNum, MN_A_NORMAL ); + return qfalse; + + case IBE_REACTOR: + G_TriggerMenu( ent->client->ps.clientNum, MN_H_REACTOR ); + return qfalse; + + case IBE_REPEATER: + G_TriggerMenu( ent->client->ps.clientNum, MN_H_REPEATER ); + return qfalse; + + case IBE_NOROOM: + if( ent->client->ps.stats[ STAT_PTEAM ] == PTE_HUMANS ) + G_TriggerMenu( ent->client->ps.clientNum, MN_H_NOROOM ); + else + G_TriggerMenu( ent->client->ps.clientNum, MN_A_NOROOM ); + return qfalse; + + case IBE_NOPOWER: + G_TriggerMenu( ent->client->ps.clientNum, MN_H_NOPOWER ); + return qfalse; + + case IBE_NODCC: + G_TriggerMenu( ent->client->ps.clientNum, MN_H_NODCC ); + return qfalse; + + case IBE_SPWNWARN: + if( level.noBuilding ) + { + vec3_t mins; + BG_FindBBoxForBuildable( buildable, mins, NULL ); + origin[2] += mins[2]; + G_SpawnMarker( origin ); + return qtrue; + } + G_TriggerMenu( ent->client->ps.clientNum, MN_A_SPWNWARN ); + G_Build( ent, buildable, origin, ent->s.apos.trBase ); + return qtrue; + + case IBE_TNODEWARN: + if( level.noBuilding ) + { + vec3_t mins; + BG_FindBBoxForBuildable( buildable, mins, NULL ); + origin[2] += mins[2]; + G_SpawnMarker( origin ); + return qtrue; + } + G_TriggerMenu( ent->client->ps.clientNum, MN_H_TNODEWARN ); + G_Build( ent, buildable, origin, ent->s.apos.trBase ); + return qtrue; + + case IBE_RPTWARN: + if( level.noBuilding ) + { + vec3_t mins; + BG_FindBBoxForBuildable( buildable, mins, NULL ); + origin[2] += mins[2]; + G_SpawnMarker( origin ); + return qtrue; + } + G_TriggerMenu( ent->client->ps.clientNum, MN_H_RPTWARN ); + G_Build( ent, buildable, origin, ent->s.apos.trBase ); + return qtrue; + + case IBE_RPTWARN2: + G_TriggerMenu( ent->client->ps.clientNum, MN_H_RPTWARN2 ); + return qfalse; + + default: + break; + } + + return qfalse; +} + +/* +================ +G_FinishSpawningBuildable + +Traces down to find where an item should rest, instead of letting them +free fall from their spawn points +================ +*/ +static void G_FinishSpawningBuildable( gentity_t *ent ) +{ + trace_t tr; + vec3_t dest; + gentity_t *built; + buildable_t buildable = ent->s.modelindex; + + built = G_Build( ent, buildable, ent->s.pos.trBase, ent->s.angles ); + G_FreeEntity( ent ); + + built->takedamage = qtrue; + built->spawned = qtrue; //map entities are already spawned + built->health = BG_FindHealthForBuildable( buildable ); + built->s.generic1 |= B_SPAWNED_TOGGLEBIT; + + // drop towards normal surface + VectorScale( built->s.origin2, -4096.0f, dest ); + VectorAdd( dest, built->s.origin, dest ); + + trap_Trace( &tr, built->s.origin, built->r.mins, built->r.maxs, dest, built->s.number, built->clipmask ); + + if( tr.startsolid ) + { + G_Printf( S_COLOR_YELLOW "G_FinishSpawningBuildable: %s startsolid at %s\n", + built->classname, vtos( built->s.origin ) ); + G_FreeEntity( built ); + return; + } + + //point items in the correct direction + VectorCopy( tr.plane.normal, built->s.origin2 ); + + // allow to ride movers + built->s.groundEntityNum = tr.entityNum; + + G_SetOrigin( built, tr.endpos ); + + trap_LinkEntity( built ); +} + +/* +============ +G_SpawnBuildable + +Sets the clipping size and plants the object on the floor. + +Items can't be immediately dropped to floor, because they might +be on an entity that hasn't spawned yet. +============ +*/ +void G_SpawnBuildable( gentity_t *ent, buildable_t buildable ) +{ + ent->s.modelindex = buildable; + + // some movers spawn on the second frame, so delay item + // spawns until the third frame so they can ride trains + ent->nextthink = level.time + FRAMETIME * 2; + ent->think = G_FinishSpawningBuildable; +} + + /* + ============ + G_CheckDBProtection + + Count how many designated builders are in both teams and + if none found in some team, cancel protection for all + structures of that team + ============ + */ + + void G_CheckDBProtection( void ) + { + int alienDBs = 0, humanDBs = 0, i; + gentity_t *ent; + + // count designated builders + for( i = 0, ent = g_entities + i; i < level.maxclients; i++, ent++) + { + if( !ent->client || ( ent->client->pers.connected != CON_CONNECTED ) ) + continue; + + if( ent->client->pers.designatedBuilder) + { + if( ent->client->pers.teamSelection == PTE_HUMANS ) + { + humanDBs++; + } + else if( ent->client->pers.teamSelection == PTE_ALIENS ) + { + alienDBs++; + } + } + } + + // both teams have designate builders, we're done + if( alienDBs > 0 && humanDBs > 0 ) + return; + + // cancel protection if needed + for( i = 1, ent = g_entities + i; i < level.num_entities; i++, ent++) + { + if( ent->s.eType != ET_BUILDABLE) + continue; + + if( ( !alienDBs && ent->biteam == BIT_ALIENS ) || + ( !humanDBs && ent->biteam == BIT_HUMANS ) ) + { + ent->s.eFlags &= ~EF_DBUILDER; + } + } + } + +/* +============ +G_LayoutSave + +============ +*/ +void G_LayoutSave( char *name ) +{ + char map[ MAX_QPATH ]; + char fileName[ MAX_OSPATH ]; + fileHandle_t f; + int len; + int i; + gentity_t *ent; + char *s; + + trap_Cvar_VariableStringBuffer( "mapname", map, sizeof( map ) ); + if( !map[ 0 ] ) + { + G_Printf( "LayoutSave( ): no map is loaded\n" ); + return; + } + Com_sprintf( fileName, sizeof( fileName ), "layouts/%s/%s.dat", map, name ); + + len = trap_FS_FOpenFile( fileName, &f, FS_WRITE ); + if( len < 0 ) + { + G_Printf( "layoutsave: could not open %s\n", fileName ); + return; + } + + G_Printf("layoutsave: saving layout to %s\n", fileName ); + + for( i = MAX_CLIENTS; i < level.num_entities; i++ ) + { + ent = &level.gentities[ i ]; + if( ent->s.eType != ET_BUILDABLE ) + continue; + + s = va( "%i %f %f %f %f %f %f %f %f %f %f %f %f\n", + ent->s.modelindex, + ent->s.pos.trBase[ 0 ], + ent->s.pos.trBase[ 1 ], + ent->s.pos.trBase[ 2 ], + ent->s.angles[ 0 ], + ent->s.angles[ 1 ], + ent->s.angles[ 2 ], + ent->s.origin2[ 0 ], + ent->s.origin2[ 1 ], + ent->s.origin2[ 2 ], + ent->s.angles2[ 0 ], + ent->s.angles2[ 1 ], + ent->s.angles2[ 2 ] ); + trap_FS_Write( s, strlen( s ), f ); + } + trap_FS_FCloseFile( f ); +} + +int G_LayoutList( const char *map, char *list, int len ) +{ + // up to 128 single character layout names could fit in layouts + char fileList[ ( MAX_CVAR_VALUE_STRING / 2 ) * 5 ] = {""}; + char layouts[ MAX_CVAR_VALUE_STRING ] = {""}; + int numFiles, i, fileLen = 0, listLen; + int count = 0; + char *filePtr; + + Q_strcat( layouts, sizeof( layouts ), "*BUILTIN* " ); + numFiles = trap_FS_GetFileList( va( "layouts/%s", map ), ".dat", + fileList, sizeof( fileList ) ); + filePtr = fileList; + for( i = 0; i < numFiles; i++, filePtr += fileLen + 1 ) + { + fileLen = strlen( filePtr ); + listLen = strlen( layouts ); + if( fileLen < 5 ) + continue; + + // list is full, stop trying to add to it + if( ( listLen + fileLen ) >= sizeof( layouts ) ) + break; + + Q_strcat( layouts, sizeof( layouts ), filePtr ); + listLen = strlen( layouts ); + + // strip extension and add space delimiter + layouts[ listLen - 4 ] = ' '; + layouts[ listLen - 3 ] = '\0'; + count++; + } + if( count != numFiles ) + { + G_Printf( S_COLOR_YELLOW "WARNING: layout list was truncated to %d " + "layouts, but %d layout files exist in layouts/%s/.\n", + count, numFiles, map ); + } + Q_strncpyz( list, layouts, len ); + return count + 1; +} + +/* +============ +G_LayoutSelect + +set level.layout based on g_layouts or g_layoutAuto +============ +*/ +void G_LayoutSelect( void ) +{ + char fileName[ MAX_OSPATH ]; + char layouts[ MAX_CVAR_VALUE_STRING ]; + char layouts2[ MAX_CVAR_VALUE_STRING ]; + char *l; + char map[ MAX_QPATH ]; + char *s; + int cnt = 0; + int layoutNum; + + Q_strncpyz( layouts, g_layouts.string, sizeof( layouts ) ); + trap_Cvar_VariableStringBuffer( "mapname", map, sizeof( map ) ); + + // one time use cvar + trap_Cvar_Set( "g_layouts", "" ); + + // pick an included layout at random if no list has been provided + if( !layouts[ 0 ] && g_layoutAuto.integer ) + { + G_LayoutList( map, layouts, sizeof( layouts ) ); + } + + if( !layouts[ 0 ] ) + return; + + Q_strncpyz( layouts2, layouts, sizeof( layouts2 ) ); + l = &layouts2[ 0 ]; + layouts[ 0 ] = '\0'; + s = COM_ParseExt( &l, qfalse ); + while( *s ) + { + if( !Q_stricmp( s, "*BUILTIN*" ) ) + { + Q_strcat( layouts, sizeof( layouts ), s ); + Q_strcat( layouts, sizeof( layouts ), " " ); + cnt++; + s = COM_ParseExt( &l, qfalse ); + continue; + } + + Com_sprintf( fileName, sizeof( fileName ), "layouts/%s/%s.dat", map, s ); + if( trap_FS_FOpenFile( fileName, NULL, FS_READ ) > 0 ) + { + Q_strcat( layouts, sizeof( layouts ), s ); + Q_strcat( layouts, sizeof( layouts ), " " ); + cnt++; + } + else + G_Printf( S_COLOR_YELLOW "WARNING: layout \"%s\" does not exist\n", s ); + s = COM_ParseExt( &l, qfalse ); + } + if( !cnt ) + { + G_Printf( S_COLOR_RED "ERROR: none of the specified layouts could be " + "found, using map default\n" ); + return; + } + layoutNum = ( rand( ) % cnt ) + 1; + cnt = 0; + + Q_strncpyz( layouts2, layouts, sizeof( layouts2 ) ); + l = &layouts2[ 0 ]; + s = COM_ParseExt( &l, qfalse ); + while( *s ) + { + Q_strncpyz( level.layout, s, sizeof( level.layout ) ); + cnt++; + if( cnt >= layoutNum ) + break; + s = COM_ParseExt( &l, qfalse ); + } + G_Printf("using layout \"%s\" from list ( %s)\n", level.layout, layouts ); +} + +static void G_LayoutBuildItem( buildable_t buildable, vec3_t origin, + vec3_t angles, vec3_t origin2, vec3_t angles2 ) +{ + gentity_t *builder; + + builder = G_Spawn( ); + builder->client = 0; + VectorCopy( origin, builder->s.pos.trBase ); + VectorCopy( angles, builder->s.angles ); + VectorCopy( origin2, builder->s.origin2 ); + VectorCopy( angles2, builder->s.angles2 ); + G_SpawnBuildable( builder, buildable ); +} + +/* +============ +G_InstantBuild + +This function is extremely similar to the few functions that place a +buildable on map load. It exists because G_LayoutBuildItem takes a couple +of frames to finish spawning it, so it's not truly instant +Do not call this function immediately after the map loads - that's what +G_LayoutBuildItem is for. +============ +*/ +gentity_t *G_InstantBuild( buildable_t buildable, vec3_t origin, vec3_t angles, vec3_t origin2, vec3_t angles2 ) +{ + gentity_t *builder, *built; + trace_t tr; + vec3_t dest; + + builder = G_Spawn( ); + builder->client = 0; + VectorCopy( origin, builder->s.pos.trBase ); + VectorCopy( angles, builder->s.angles ); + VectorCopy( origin2, builder->s.origin2 ); + VectorCopy( angles2, builder->s.angles2 ); +//old method didn't quite work out +//builder->s.modelindex = buildable; +//G_FinishSpawningBuildable( builder ); + + built = G_Build( builder, buildable, builder->s.pos.trBase, builder->s.angles ); + G_FreeEntity( builder ); + + built->takedamage = qtrue; + built->spawned = qtrue; //map entities are already spawned + built->health = BG_FindHealthForBuildable( buildable ); + built->s.generic1 |= B_SPAWNED_TOGGLEBIT; + + // drop towards normal surface + VectorScale( built->s.origin2, -4096.0f, dest ); + VectorAdd( dest, built->s.origin, dest ); + + trap_Trace( &tr, built->s.origin, built->r.mins, built->r.maxs, dest, built->s.number, built->clipmask ); + if( tr.startsolid ) + { + G_Printf( S_COLOR_YELLOW "G_FinishSpawningBuildable: %s startsolid at %s\n", + built->classname, vtos( built->s.origin ) ); + G_FreeEntity( built ); + return NULL; + } + + //point items in the correct direction + VectorCopy( tr.plane.normal, built->s.origin2 ); + + // allow to ride movers + built->s.groundEntityNum = tr.entityNum; + + G_SetOrigin( built, tr.endpos ); + + trap_LinkEntity( built ); + return built; +} + +/* +============ +G_SpawnRevertedBuildable + +Given a buildhistory, try to replace the lost buildable +============ +*/ +void G_SpawnRevertedBuildable( buildHistory_t *bh, qboolean mark ) +{ + vec3_t mins, maxs; + int i, j, blockCount, blockers[ MAX_GENTITIES ]; + gentity_t *targ, *built, *toRecontent[ MAX_GENTITIES ]; + + BG_FindBBoxForBuildable( bh->buildable, mins, maxs ); + VectorAdd( bh->origin, mins, mins ); + VectorAdd( bh->origin, maxs, maxs ); + blockCount = trap_EntitiesInBox( mins, maxs, blockers, MAX_GENTITIES ); + for( i = j = 0; i < blockCount; i++ ) + { + targ = g_entities + blockers[ i ]; + if( targ->s.eType == ET_BUILDABLE ) + G_FreeEntity( targ ); + else if( targ->s.eType == ET_PLAYER ) + { + targ->r.contents = 0; // make it intangible + toRecontent[ j++ ] = targ; // and remember it + } + } + level.numBuildablesForRemoval = 0; + built = G_InstantBuild( bh->buildable, bh->origin, bh->angles, bh->origin2, bh->angles2 ); + if( built ) + { + built->r.contents = 0; + built->think = G_CommitRevertedBuildable; + built->nextthink = level.time; + built->deconstruct = mark; + } + for( i = 0; i < j; i++ ) + toRecontent[ i ]->r.contents = CONTENTS_BODY; +} + +/* +============ +G_CommitRevertedBuildable + +Check if there's anyone occupying me, and if not, become solid and operate as +normal. Else, try to get rid of them. +============ +*/ +void G_CommitRevertedBuildable( gentity_t *ent ) +{ + gentity_t *targ; + int i, n, occupants[ MAX_GENTITIES ]; + vec3_t mins, maxs; + int victims = 0; + + VectorAdd( ent->s.origin, ent->r.mins, mins ); + VectorAdd( ent->s.origin, ent->r.maxs, maxs ); + trap_UnlinkEntity( ent ); + n = trap_EntitiesInBox( mins, maxs, occupants, MAX_GENTITIES ); + trap_LinkEntity( ent ); + + for( i = 0; i < n; i++ ) + { + vec3_t gtfo; + targ = g_entities + occupants[ i ]; + if( targ->client ) + { + VectorSet( gtfo, crandom() * 150, crandom() * 150, random() * 150 ); + VectorAdd( targ->client->ps.velocity, gtfo, targ->client->ps.velocity ); + victims++; + } + } + if( !victims ) + { // we're in the clear! + ent->r.contents = MASK_PLAYERSOLID; + trap_LinkEntity( ent ); // relink + // oh dear, manual think set + switch( ent->s.modelindex ) + { + case BA_A_SPAWN: + ent->think = ASpawn_Think; + break; + case BA_A_BARRICADE: + case BA_A_BOOSTER: + ent->think = ABarricade_Think; + break; + case BA_A_ACIDTUBE: + ent->think = AAcidTube_Think; + break; + case BA_A_HIVE: + ent->think = AHive_Think; + break; + case BA_A_TRAPPER: + ent->think = ATrapper_Think; + break; + case BA_A_OVERMIND: + ent->think = AOvermind_Think; + break; + case BA_A_HOVEL: + ent->think = AHovel_Think; + break; + case BA_H_SPAWN: + ent->think = HSpawn_Think; + break; + case BA_H_MGTURRET: + ent->think = HMGTurret_Think; + break; + case BA_H_TESLAGEN: + ent->think = HTeslaGen_Think; + break; + case BA_H_ARMOURY: + ent->think = HArmoury_Think; + break; + case BA_H_DCC: + ent->think = HDCC_Think; + break; + case BA_H_MEDISTAT: + ent->think = HMedistat_Think; + break; + case BA_H_REACTOR: + ent->think = HReactor_Think; + break; + case BA_H_REPEATER: + ent->think = HRepeater_Think; + break; + } + ent->nextthink = level.time + BG_FindNextThinkForBuildable( ent->s.modelindex ); + // oh if only everything was that simple + return; + } +#define REVERT_THINK_INTERVAL 50 + ent->nextthink = level.time + REVERT_THINK_INTERVAL; +} + +/* +============ +G_RevertCanFit + +take a bhist and make sure you're not overwriting anything by placing it +============ +*/ +qboolean G_RevertCanFit( buildHistory_t *bh ) +{ + int i, num, blockers[ MAX_GENTITIES ]; + vec3_t mins, maxs; + gentity_t *targ; + vec3_t dist; + + BG_FindBBoxForBuildable( bh->buildable, mins, maxs ); + VectorAdd( bh->origin, mins, mins ); + VectorAdd( bh->origin, maxs, maxs ); + num = trap_EntitiesInBox( mins, maxs, blockers, MAX_GENTITIES ); + for( i = 0; i < num; i++ ) + { + targ = g_entities + blockers[ i ]; + if( targ->s.eType == ET_BUILDABLE ) + { + VectorSubtract( bh->origin, targ->s.pos.trBase, dist ); + if( targ->s.modelindex == bh->buildable && VectorLength( dist ) < 10 && targ->health <= 0 ) + continue; // it's the same buildable, hasn't blown up yet + else + return qfalse; // can't get rid of this one + } + else + continue; + } + return qtrue; +} + +/* +============ +G_LayoutLoad + +load the layout .dat file indicated by level.layout and spawn buildables +as if a builder was creating them +============ +*/ +void G_LayoutLoad( void ) +{ + fileHandle_t f; + int len; + char *layout; + char map[ MAX_QPATH ]; + int buildable = BA_NONE; + vec3_t origin = { 0.0f, 0.0f, 0.0f }; + vec3_t angles = { 0.0f, 0.0f, 0.0f }; + vec3_t origin2 = { 0.0f, 0.0f, 0.0f }; + vec3_t angles2 = { 0.0f, 0.0f, 0.0f }; + char line[ MAX_STRING_CHARS ]; + int i = 0; + + if( !level.layout[ 0 ] || !Q_stricmp( level.layout, "*BUILTIN*" ) ) + return; + + trap_Cvar_VariableStringBuffer( "mapname", map, sizeof( map ) ); + len = trap_FS_FOpenFile( va( "layouts/%s/%s.dat", map, level.layout ), + &f, FS_READ ); + if( len < 0 ) + { + G_Printf( "ERROR: layout %s could not be opened\n", level.layout ); + return; + } + layout = G_Alloc( len + 1 ); + trap_FS_Read( layout, len, f ); + *( layout + len ) = '\0'; + trap_FS_FCloseFile( f ); + while( *layout ) + { + if( i >= sizeof( line ) - 1 ) + { + G_Printf( S_COLOR_RED "ERROR: line overflow in %s before \"%s\"\n", + va( "layouts/%s/%s.dat", map, level.layout ), line ); + return; + } + line[ i++ ] = *layout; + line[ i ] = '\0'; + if( *layout == '\n' ) + { + i = 0; + sscanf( line, "%d %f %f %f %f %f %f %f %f %f %f %f %f\n", + &buildable, + &origin[ 0 ], &origin[ 1 ], &origin[ 2 ], + &angles[ 0 ], &angles[ 1 ], &angles[ 2 ], + &origin2[ 0 ], &origin2[ 1 ], &origin2[ 2 ], + &angles2[ 0 ], &angles2[ 1 ], &angles2[ 2 ] ); + + if( buildable > BA_NONE && buildable < BA_NUM_BUILDABLES ) + G_LayoutBuildItem( buildable, origin, angles, origin2, angles2 ); + else + G_Printf( S_COLOR_YELLOW "WARNING: bad buildable number (%d) in " + " layout. skipping\n", buildable ); + } + layout++; + } +} + +void G_BaseSelfDestruct( pTeam_t team ) +{ + int i; + gentity_t *ent; + + for( i = MAX_CLIENTS; i < level.num_entities; i++ ) + { + ent = &level.gentities[ i ]; + if( ent->health <= 0 ) + continue; + if( ent->s.eType != ET_BUILDABLE ) + continue; + if( team == PTE_HUMANS && ent->biteam != BIT_HUMANS ) + continue; + if( team == PTE_ALIENS && ent->biteam != BIT_ALIENS ) + continue; + G_Damage( ent, NULL, NULL, NULL, NULL, 10000, 0, MOD_SUICIDE ); + } +} + + int G_LogBuild( buildHistory_t *new ) + { + new->next = level.buildHistory; + level.buildHistory = new; + return G_CountBuildLog(); + } + + int G_CountBuildLog( void ) + { + buildHistory_t *ptr, *mark; + int i = 0, overflow; + for( ptr = level.buildHistory; ptr; ptr = ptr->next, i++ ); + if( i > g_buildLogMaxLength.integer ) + { + for( overflow = i - g_buildLogMaxLength.integer; overflow > 0; overflow-- ) + { + ptr = level.buildHistory; + while( ptr->next ) + { + if( ptr->next->next ) + ptr = ptr->next; + else + { + while( ( mark = ptr->next ) ) + { + ptr->next = ptr->next->marked; + G_Free( mark ); + } + } + } + } + return g_buildLogMaxLength.integer; + } + return i; + } + + char *G_FindBuildLogName( int id ) + { + buildHistory_t *ptr; + + for( ptr = level.buildHistory; ptr && ptr->ID != id; ptr = ptr->next ); + if( ptr ) + { + if( ptr->ent ) + { + if( ptr->ent->client ) + return ptr->ent->client->pers.netname; + } + else if( ptr->name[ 0 ] ) + { + return ptr->name; + } + } + + return "<buildlog entry expired>"; + } + +/* +============ +G_NobuildLoad + +load the nobuild markers that were previously saved (if there are any). +============ +*/ +void G_NobuildLoad( void ) +{ + fileHandle_t f; + int len; + char *nobuild; + char map[ MAX_QPATH ]; + vec3_t origin = { 0.0f, 0.0f, 0.0f }; + char line[ MAX_STRING_CHARS ]; + int i = 0; + int i2; + gentity_t *nb; + float area; + float height; + + trap_Cvar_VariableStringBuffer( "mapname", map, sizeof( map ) ); + len = trap_FS_FOpenFile( va( "nobuild/%s.dat", map ), + &f, FS_READ ); + if( len < 0 ) + { + // This isn't needed since nobuild is pretty much optional... + //G_Printf( "ERROR: nobuild for %s could not be opened\n", map ); + return; + } + nobuild = G_Alloc( len + 1 ); + trap_FS_Read( nobuild, len, f ); + *( nobuild + len ) = '\0'; + trap_FS_FCloseFile( f ); + while( *nobuild ) + { + if( i >= sizeof( line ) - 1 ) + { + return; + } + + line[ i++ ] = *nobuild; + line[ i ] = '\0'; + if( *nobuild == '\n' ) + { + i = 0; + sscanf( line, "%f %f %f %f %f\n", + &origin[ 0 ], &origin[ 1 ], &origin[ 2 ], &area, &height ); + + // Make the marker... + nb = G_Spawn( ); + nb->s.modelindex = 0; + VectorCopy( origin, nb->s.pos.trBase ); + VectorCopy( origin, nb->r.currentOrigin ); + nb->noBuild.isNB = qtrue; + nb->noBuild.Area = area; + nb->noBuild.Height = height; + trap_LinkEntity( nb ); + + // Log markers made... + for( i = 0; i < MAX_GENTITIES; i++ ) + { + if( level.nbMarkers[ i ].Marker != NULL ) + continue; + + level.nbMarkers[ i ].Marker = nb; + VectorCopy( origin, level.nbMarkers[ i ].Origin ); + SnapVector( level.nbMarkers[ i ].Origin ); + break; + } + + } + nobuild++; + } +} + +/* +============ +G_NobuildSave +Save all currently placed nobuild markers into the "nobuild" folder +============ +*/ +void G_NobuildSave( void ) +{ + char map[ MAX_QPATH ]; + char fileName[ MAX_OSPATH ]; + fileHandle_t f; + int len; + int i; + gentity_t *ent; + char *s; + + trap_Cvar_VariableStringBuffer( "mapname", map, sizeof( map ) ); + if( !map[ 0 ] ) + { + G_Printf( "NobuildSave( ): no map is loaded\n" ); + return; + } + Com_sprintf( fileName, sizeof( fileName ), "nobuild/%s.dat", map ); + + len = trap_FS_FOpenFile( fileName, &f, FS_WRITE ); + if( len < 0 ) + { + G_Printf( "nobuildsave: could not open %s\n", fileName ); + return; + } + + G_Printf("nobuildsave: saving nobuild to %s\n", fileName ); + + for( i = 0; i < MAX_GENTITIES; i++ ) + { + ent = &level.gentities[ i ]; + if( ent->noBuild.isNB != qtrue ) + continue; + + s = va( "%f %f %f %f %f\n", + ent->r.currentOrigin[ 0 ], + ent->r.currentOrigin[ 1 ], + ent->r.currentOrigin[ 2 ], + ent->noBuild.Area, + ent->noBuild.Height ); + trap_FS_Write( s, strlen( s ), f ); + } + trap_FS_FCloseFile( f ); +} diff --git a/src/game/g_client.c b/src/game/g_client.c new file mode 100644 index 0000000..3d274ee --- /dev/null +++ b/src/game/g_client.c @@ -0,0 +1,2039 @@ +/* +=========================================================================== +Copyright (C) 1999-2005 Id Software, Inc. +Copyright (C) 2000-2006 Tim Angus + +This file is part of Tremulous. + +Tremulous is free software; you can redistribute it +and/or modify it under the terms of the GNU General Public License as +published by the Free Software Foundation; either version 2 of the License, +or (at your option) any later version. + +Tremulous is distributed in the hope that it will be +useful, but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with Tremulous; if not, write to the Free Software +Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA +=========================================================================== +*/ + +#include "g_local.h" + +// g_client.c -- client functions that don't happen every frame + +static vec3_t playerMins = {-15, -15, -24}; +static vec3_t playerMaxs = {15, 15, 32}; + +/*QUAKED info_player_deathmatch (1 0 1) (-16 -16 -24) (16 16 32) initial +potential spawning position for deathmatch games. +The first time a player enters the game, they will be at an 'initial' spot. +Targets will be fired when someone spawns in on them. +"nobots" will prevent bots from using this spot. +"nohumans" will prevent non-bots from using this spot. +*/ +void SP_info_player_deathmatch( gentity_t *ent ) +{ + int i; + + G_SpawnInt( "nobots", "0", &i); + + if( i ) + ent->flags |= FL_NO_BOTS; + + G_SpawnInt( "nohumans", "0", &i ); + if( i ) + ent->flags |= FL_NO_HUMANS; +} + +/*QUAKED info_player_start (1 0 0) (-16 -16 -24) (16 16 32) +equivelant to info_player_deathmatch +*/ +void SP_info_player_start( gentity_t *ent ) +{ + ent->classname = "info_player_deathmatch"; + SP_info_player_deathmatch( ent ); +} + +/*QUAKED info_player_intermission (1 0 1) (-16 -16 -24) (16 16 32) +The intermission will be viewed from this point. Target an info_notnull for the view direction. +*/ +void SP_info_player_intermission( gentity_t *ent ) +{ +} + +/*QUAKED info_alien_intermission (1 0 1) (-16 -16 -24) (16 16 32) +The intermission will be viewed from this point. Target an info_notnull for the view direction. +*/ +void SP_info_alien_intermission( gentity_t *ent ) +{ +} + +/*QUAKED info_human_intermission (1 0 1) (-16 -16 -24) (16 16 32) +The intermission will be viewed from this point. Target an info_notnull for the view direction. +*/ +void SP_info_human_intermission( gentity_t *ent ) +{ +} + +/* +=============== +G_OverflowCredits +=============== +*/ +void G_OverflowCredits( gclient_t *doner, int credits ) +{ + int i; + int maxCredits; + int clientNum; + + if( !g_creditOverflow.integer ) + return; + + if( doner->ps.stats[ STAT_PTEAM ] == PTE_ALIENS ) + { + maxCredits = ALIEN_MAX_KILLS; + clientNum = level.lastCreditedAlien; + } + else if( doner->ps.stats[ STAT_PTEAM ] == PTE_HUMANS ) + { + maxCredits = HUMAN_MAX_CREDITS; + clientNum = level.lastCreditedHuman; + } + else + { + return; + } + + if( g_creditOverflow.integer == 1 ) + { + // distribute to everyone on team + gentity_t *vic; + + i = 0; + while( credits > 0 && i < level.maxclients ) + { + i++; + clientNum++; + if( clientNum >= level.maxclients ) + clientNum = 0; + + vic = &g_entities[ clientNum ]; + if( vic->client->ps.stats[ STAT_PTEAM ] != doner->ps.stats[ STAT_PTEAM ] || + vic->client->ps.persistant[ PERS_CREDIT ] >= maxCredits ) + continue; + + if( vic->client->ps.stats[ STAT_PTEAM ] == PTE_ALIENS ) + level.lastCreditedAlien = clientNum; + else + level.lastCreditedHuman = clientNum; + + if( vic->client->ps.persistant[ PERS_CREDIT ] + credits > maxCredits ) + { + credits -= maxCredits - vic->client->ps.persistant[ PERS_CREDIT ]; + vic->client->ps.persistant[ PERS_CREDIT ] = maxCredits; + } + else + { + vic->client->ps.persistant[ PERS_CREDIT ] += credits; + return; + } + } + } + else if( g_creditOverflow.integer == 2 ) + { + // distribute by team rank + gclient_t *cl; + + for( i = 0; i < level.numPlayingClients && credits > 0; i++ ) + { + // get the client list sorted by rank + cl = &level.clients[ level.sortedClients[ i ] ]; + if( cl->ps.stats[ STAT_PTEAM ] != doner->ps.stats[ STAT_PTEAM ] || + cl->ps.persistant[ PERS_CREDIT ] >= maxCredits ) + continue; + + if( cl->ps.persistant[ PERS_CREDIT ] + credits > maxCredits ) + { + credits -= maxCredits - cl->ps.persistant[ PERS_CREDIT ]; + cl->ps.persistant[ PERS_CREDIT ] = maxCredits; + } + else + { + cl->ps.persistant[ PERS_CREDIT ] += credits; + return; + } + } + } +} + +/* +=============== +G_AddCreditToClient +=============== +*/ +void G_AddCreditToClient( gclient_t *client, short credit, qboolean cap ) +{ + if( !client ) + return; + + //if we're already at the max and trying to add credit then stop + if( cap ) + { + if( client->pers.teamSelection == PTE_ALIENS ) + { + if( client->pers.credit >= ALIEN_MAX_KILLS && + credit > 0 ) + { + G_OverflowCredits( client, credit ); + return; + } + } + else if( client->pers.teamSelection == PTE_HUMANS ) + { + if( client->pers.credit >= HUMAN_MAX_CREDITS && + credit > 0 ) + { + G_OverflowCredits( client, credit ); + return; + } + } + } + + client->pers.credit += credit; + + if( cap ) + { + if( client->pers.teamSelection == PTE_ALIENS ) + { + if( client->pers.credit > ALIEN_MAX_KILLS ) + { + G_OverflowCredits( client, client->ps.persistant[ PERS_CREDIT ] - ALIEN_MAX_KILLS ); + client->pers.credit = ALIEN_MAX_KILLS; + } + } + else if( client->pers.teamSelection == PTE_HUMANS ) + { + if( client->pers.credit > HUMAN_MAX_CREDITS ) + { + G_OverflowCredits( client, client->ps.persistant[ PERS_CREDIT ] - HUMAN_MAX_CREDITS ); + client->pers.credit = HUMAN_MAX_CREDITS; + } + } + } + + if( client->pers.credit < 0 ) + client->pers.credit = 0; + + // keep PERS_CREDIT in sync if not following + if( client->sess.spectatorState != SPECTATOR_FOLLOW ) + client->ps.persistant[ PERS_CREDIT ] = client->pers.credit; +} + + +/* +======================================================================= + + G_SelectSpawnPoint + +======================================================================= +*/ + +/* +================ +SpotWouldTelefrag + +================ +*/ +qboolean SpotWouldTelefrag( gentity_t *spot ) +{ + int i, num; + int touch[ MAX_GENTITIES ]; + gentity_t *hit; + vec3_t mins, maxs; + + VectorAdd( spot->s.origin, playerMins, mins ); + VectorAdd( spot->s.origin, playerMaxs, maxs ); + num = trap_EntitiesInBox( mins, maxs, touch, MAX_GENTITIES ); + + for( i = 0; i < num; i++ ) + { + hit = &g_entities[ touch[ i ] ]; + //if ( hit->client && hit->client->ps.stats[STAT_HEALTH] > 0 ) { + if( hit->client ) + return qtrue; + } + + return qfalse; +} + +/* +================ +G_SelectNearestDeathmatchSpawnPoint + +Find the spot that we DON'T want to use +================ +*/ +#define MAX_SPAWN_POINTS 128 +gentity_t *G_SelectNearestDeathmatchSpawnPoint( vec3_t from ) +{ + gentity_t *spot; + vec3_t delta; + float dist, nearestDist; + gentity_t *nearestSpot; + + nearestDist = 999999; + nearestSpot = NULL; + spot = NULL; + + while( (spot = G_Find( spot, FOFS( classname ), "info_player_deathmatch" ) ) != NULL ) + { + VectorSubtract( spot->s.origin, from, delta ); + dist = VectorLength( delta ); + + if( dist < nearestDist ) + { + nearestDist = dist; + nearestSpot = spot; + } + } + + return nearestSpot; +} + + +/* +================ +G_SelectRandomDeathmatchSpawnPoint + +go to a random point that doesn't telefrag +================ +*/ +#define MAX_SPAWN_POINTS 128 +gentity_t *G_SelectRandomDeathmatchSpawnPoint( void ) +{ + gentity_t *spot; + int count; + int selection; + gentity_t *spots[ MAX_SPAWN_POINTS ]; + + count = 0; + spot = NULL; + + while( ( spot = G_Find( spot, FOFS( classname ), "info_player_deathmatch" ) ) != NULL ) + { + if( SpotWouldTelefrag( spot ) ) + continue; + + spots[ count ] = spot; + count++; + } + + if( !count ) // no spots that won't telefrag + return G_Find( NULL, FOFS( classname ), "info_player_deathmatch" ); + + selection = rand( ) % count; + return spots[ selection ]; +} + + +/* +=========== +G_SelectRandomFurthestSpawnPoint + +Chooses a player start, deathmatch start, etc +============ +*/ +gentity_t *G_SelectRandomFurthestSpawnPoint ( vec3_t avoidPoint, vec3_t origin, vec3_t angles ) +{ + gentity_t *spot; + vec3_t delta; + float dist; + float list_dist[ 64 ]; + gentity_t *list_spot[ 64 ]; + int numSpots, rnd, i, j; + + numSpots = 0; + spot = NULL; + + while( ( spot = G_Find( spot, FOFS( classname ), "info_player_deathmatch" ) ) != NULL ) + { + if( SpotWouldTelefrag( spot ) ) + continue; + + VectorSubtract( spot->s.origin, avoidPoint, delta ); + dist = VectorLength( delta ); + + for( i = 0; i < numSpots; i++ ) + { + if( dist > list_dist[ i ] ) + { + if( numSpots >= 64 ) + numSpots = 64 - 1; + + for( j = numSpots; j > i; j-- ) + { + list_dist[ j ] = list_dist[ j - 1 ]; + list_spot[ j ] = list_spot[ j - 1 ]; + } + + list_dist[ i ] = dist; + list_spot[ i ] = spot; + numSpots++; + + if( numSpots > 64 ) + numSpots = 64; + + break; + } + } + + if( i >= numSpots && numSpots < 64 ) + { + list_dist[ numSpots ] = dist; + list_spot[ numSpots ] = spot; + numSpots++; + } + } + + if( !numSpots ) + { + spot = G_Find( NULL, FOFS( classname ), "info_player_deathmatch" ); + + if( !spot ) + G_Error( "Couldn't find a spawn point" ); + + VectorCopy( spot->s.origin, origin ); + origin[ 2 ] += 9; + VectorCopy( spot->s.angles, angles ); + return spot; + } + + // select a random spot from the spawn points furthest away + rnd = random( ) * ( numSpots / 2 ); + + VectorCopy( list_spot[ rnd ]->s.origin, origin ); + origin[ 2 ] += 9; + VectorCopy( list_spot[ rnd ]->s.angles, angles ); + + return list_spot[ rnd ]; +} + + +/* +================ +G_SelectAlienSpawnPoint + +go to a random point that doesn't telefrag +================ +*/ +gentity_t *G_SelectAlienSpawnPoint( vec3_t preference ) +{ + gentity_t *spot; + int count; + gentity_t *spots[ MAX_SPAWN_POINTS ]; + + if( level.numAlienSpawns <= 0 ) + return NULL; + + count = 0; + spot = NULL; + + while( ( spot = G_Find( spot, FOFS( classname ), + BG_FindEntityNameForBuildable( BA_A_SPAWN ) ) ) != NULL ) + { + if( !spot->spawned ) + continue; + + if( spot->health <= 0 ) + continue; + + if( !spot->s.groundEntityNum ) + continue; + + if( spot->clientSpawnTime > 0 ) + continue; + + if( G_CheckSpawnPoint( spot->s.number, spot->s.origin, + spot->s.origin2, BA_A_SPAWN, NULL ) != NULL ) + continue; + + spots[ count ] = spot; + count++; + } + + if( !count ) + return NULL; + + return G_ClosestEnt( preference, spots, count ); +} + + +/* +================ +G_SelectHumanSpawnPoint + +go to a random point that doesn't telefrag +================ +*/ +gentity_t *G_SelectHumanSpawnPoint( vec3_t preference ) +{ + gentity_t *spot; + int count; + gentity_t *spots[ MAX_SPAWN_POINTS ]; + + if( level.numHumanSpawns <= 0 ) + return NULL; + + count = 0; + spot = NULL; + + while( ( spot = G_Find( spot, FOFS( classname ), + BG_FindEntityNameForBuildable( BA_H_SPAWN ) ) ) != NULL ) + { + if( !spot->spawned ) + continue; + + if( spot->health <= 0 ) + continue; + + if( !spot->s.groundEntityNum ) + continue; + + if( spot->clientSpawnTime > 0 ) + continue; + + if( G_CheckSpawnPoint( spot->s.number, spot->s.origin, + spot->s.origin2, BA_H_SPAWN, NULL ) != NULL ) + continue; + + spots[ count ] = spot; + count++; + } + + if( !count ) + return NULL; + + return G_ClosestEnt( preference, spots, count ); +} + + +/* +=========== +G_SelectSpawnPoint + +Chooses a player start, deathmatch start, etc +============ +*/ +gentity_t *G_SelectSpawnPoint( vec3_t avoidPoint, vec3_t origin, vec3_t angles ) +{ + return G_SelectRandomFurthestSpawnPoint( avoidPoint, origin, angles ); +} + + +/* +=========== +G_SelectTremulousSpawnPoint + +Chooses a player start, deathmatch start, etc +============ +*/ +gentity_t *G_SelectTremulousSpawnPoint( pTeam_t team, vec3_t preference, vec3_t origin, vec3_t angles ) +{ + gentity_t *spot = NULL; + + if( team == PTE_ALIENS ) + spot = G_SelectAlienSpawnPoint( preference ); + else if( team == PTE_HUMANS ) + spot = G_SelectHumanSpawnPoint( preference ); + + //no available spots + if( !spot ) + return NULL; + + if( team == PTE_ALIENS ) + G_CheckSpawnPoint( spot->s.number, spot->s.origin, spot->s.origin2, BA_A_SPAWN, origin ); + else if( team == PTE_HUMANS ) + G_CheckSpawnPoint( spot->s.number, spot->s.origin, spot->s.origin2, BA_H_SPAWN, origin ); + + VectorCopy( spot->s.angles, angles ); + angles[ ROLL ] = 0; + + return spot; + +} + + +/* +=========== +G_SelectInitialSpawnPoint + +Try to find a spawn point marked 'initial', otherwise +use normal spawn selection. +============ +*/ +gentity_t *G_SelectInitialSpawnPoint( vec3_t origin, vec3_t angles ) +{ + gentity_t *spot; + + spot = NULL; + while( ( spot = G_Find( spot, FOFS( classname ), "info_player_deathmatch" ) ) != NULL ) + { + if( spot->spawnflags & 1 ) + break; + } + + if( !spot || SpotWouldTelefrag( spot ) ) + { + return G_SelectSpawnPoint( vec3_origin, origin, angles ); + } + + VectorCopy( spot->s.origin, origin ); + origin[ 2 ] += 9; + VectorCopy( spot->s.angles, angles ); + + return spot; +} + +/* +=========== +G_SelectSpectatorSpawnPoint + +============ +*/ +gentity_t *G_SelectSpectatorSpawnPoint( vec3_t origin, vec3_t angles ) +{ + FindIntermissionPoint( ); + + VectorCopy( level.intermission_origin, origin ); + VectorCopy( level.intermission_angle, angles ); + + return NULL; +} + + +/* +=========== +G_SelectAlienLockSpawnPoint + +Try to find a spawn point for alien intermission otherwise +use normal intermission spawn. +============ +*/ +gentity_t *G_SelectAlienLockSpawnPoint( vec3_t origin, vec3_t angles ) +{ + gentity_t *spot; + + spot = NULL; + spot = G_Find( spot, FOFS( classname ), "info_alien_intermission" ); + + if( !spot ) + return G_SelectSpectatorSpawnPoint( origin, angles ); + + VectorCopy( spot->s.origin, origin ); + VectorCopy( spot->s.angles, angles ); + + return spot; +} + + +/* +=========== +G_SelectHumanLockSpawnPoint + +Try to find a spawn point for human intermission otherwise +use normal intermission spawn. +============ +*/ +gentity_t *G_SelectHumanLockSpawnPoint( vec3_t origin, vec3_t angles ) +{ + gentity_t *spot; + + spot = NULL; + spot = G_Find( spot, FOFS( classname ), "info_human_intermission" ); + + if( !spot ) + return G_SelectSpectatorSpawnPoint( origin, angles ); + + VectorCopy( spot->s.origin, origin ); + VectorCopy( spot->s.angles, angles ); + + return spot; +} + + +/* +======================================================================= + +BODYQUE + +======================================================================= +*/ + + +/* +============= +BodySink + +After sitting around for five seconds, fall into the ground and dissapear +============= +*/ +void BodySink( gentity_t *ent ) +{ + //run on first BodySink call + if( !ent->active ) + { + ent->active = qtrue; + + //sinking bodies can't be infested + ent->killedBy = ent->s.powerups = MAX_CLIENTS; + ent->timestamp = level.time; + } + + if( level.time - ent->timestamp > 6500 ) + { + G_FreeEntity( ent ); + return; + } + + ent->nextthink = level.time + 100; + ent->s.pos.trBase[ 2 ] -= 1; +} + + +/* +============= +BodyFree + +After sitting around for a while the body becomes a freebie +============= +*/ +void BodyFree( gentity_t *ent ) +{ + ent->killedBy = -1; + + //if not claimed in the next minute destroy + ent->think = BodySink; + ent->nextthink = level.time + 60000; +} + + +/* +============= +SpawnCorpse + +A player is respawning, so make an entity that looks +just like the existing corpse to leave behind. +============= +*/ +void SpawnCorpse( gentity_t *ent ) +{ + gentity_t *body; + int contents; + vec3_t origin, dest; + trace_t tr; + float vDiff; + + // prevent crashing everyone with bad corpsenum bug + if( ent->client->pers.connected != CON_CONNECTED ) + return; + + VectorCopy( ent->r.currentOrigin, origin ); + + trap_UnlinkEntity( ent ); + + // if client is in a nodrop area, don't leave the body + contents = trap_PointContents( origin, -1 ); + if( contents & CONTENTS_NODROP ) + return; + + body = G_Spawn( ); + + VectorCopy( ent->s.apos.trBase, body->s.angles ); + body->s.eFlags = EF_DEAD; + body->s.eType = ET_CORPSE; + body->s.number = body - g_entities; + body->timestamp = level.time; + body->s.event = 0; + body->r.contents = CONTENTS_CORPSE; + body->s.clientNum = ent->client->ps.stats[ STAT_PCLASS ]; + body->nonSegModel = ent->client->ps.persistant[ PERS_STATE ] & PS_NONSEGMODEL; + + if( ent->client->ps.stats[ STAT_PTEAM ] == PTE_HUMANS ) + body->classname = "humanCorpse"; + else + body->classname = "alienCorpse"; + + body->s.powerups = MAX_CLIENTS; + + body->think = BodySink; + body->nextthink = level.time + 20000; + + body->s.legsAnim = ent->s.legsAnim; + + if( !body->nonSegModel ) + { + switch( body->s.legsAnim & ~ANIM_TOGGLEBIT ) + { + case BOTH_DEATH1: + case BOTH_DEAD1: + body->s.torsoAnim = body->s.legsAnim = BOTH_DEAD1; + break; + case BOTH_DEATH2: + case BOTH_DEAD2: + body->s.torsoAnim = body->s.legsAnim = BOTH_DEAD2; + break; + case BOTH_DEATH3: + case BOTH_DEAD3: + default: + body->s.torsoAnim = body->s.legsAnim = BOTH_DEAD3; + break; + } + } + else + { + switch( body->s.legsAnim & ~ANIM_TOGGLEBIT ) + { + case NSPA_DEATH1: + case NSPA_DEAD1: + body->s.legsAnim = NSPA_DEAD1; + break; + case NSPA_DEATH2: + case NSPA_DEAD2: + body->s.legsAnim = NSPA_DEAD2; + break; + case NSPA_DEATH3: + case NSPA_DEAD3: + default: + body->s.legsAnim = NSPA_DEAD3; + break; + } + } + + body->takedamage = qfalse; + + body->health = ent->health = ent->client->ps.stats[ STAT_HEALTH ]; + ent->health = 0; + + //change body dimensions + BG_FindBBoxForClass( ent->client->ps.stats[ STAT_PCLASS ], NULL, NULL, NULL, body->r.mins, body->r.maxs ); + vDiff = body->r.mins[ 2 ] - ent->r.mins[ 2 ]; + + //drop down to match the *model* origins of ent and body + VectorSet( dest, origin[ 0 ], origin[ 1 ], origin[ 2 ] - vDiff ); + trap_Trace( &tr, origin, body->r.mins, body->r.maxs, dest, body->s.number, body->clipmask ); + VectorCopy( tr.endpos, origin ); + + G_SetOrigin( body, origin ); + VectorCopy( origin, body->s.origin ); + body->s.pos.trType = TR_GRAVITY; + body->s.pos.trTime = level.time; + VectorCopy( ent->client->ps.velocity, body->s.pos.trDelta ); + + VectorCopy ( body->s.pos.trBase, body->r.currentOrigin ); + trap_LinkEntity( body ); +} + +//====================================================================== + + +/* +================== +G_SetClientViewAngle + +================== +*/ +void G_SetClientViewAngle( gentity_t *ent, vec3_t angle ) +{ + int i; + + // set the delta angle + for( i = 0; i < 3; i++ ) + { + int cmdAngle; + + cmdAngle = ANGLE2SHORT( angle[ i ] ); + ent->client->ps.delta_angles[ i ] = cmdAngle - ent->client->pers.cmd.angles[ i ]; + } + + VectorCopy( angle, ent->s.angles ); + VectorCopy( ent->s.angles, ent->client->ps.viewangles ); +} + +/* +================ +respawn +================ +*/ +void respawn( gentity_t *ent ) +{ + SpawnCorpse( ent ); + + //TA: Clients can't respawn - they must go thru the class cmd + ent->client->pers.classSelection = PCL_NONE; + ClientSpawn( ent, NULL, NULL, NULL ); +} + +/* +================ +TeamCount + +Returns number of players on a team +================ +*/ +team_t TeamCount( int ignoreClientNum, int team ) +{ + int i; + int count = 0; + + for( i = 0 ; i < level.maxclients ; i++ ) + { + if( i == ignoreClientNum ) + continue; + + if( level.clients[ i ].pers.connected == CON_DISCONNECTED ) + continue; + + if( level.clients[ i ].sess.sessionTeam == team ) + count++; + } + + return count; +} + + +/* +=========== +ClientCleanName +============ +*/ +static void ClientCleanName( const char *in, char *out, int outSize, qboolean special ) +{ + int len, colorlessLen; + char ch; + char *p; + int spaces; + qboolean invalid = qfalse; + + //save room for trailing null byte + outSize--; + + len = 0; + colorlessLen = 0; + p = out; + *p = 0; + spaces = 0; + + while( 1 ) + { + ch = *in++; + if( !ch ) + break; + + // don't allow leading spaces + if( !*p && ch == ' ' ) + continue; + + // don't allow nonprinting characters or (dead) console keys + if( ch < ' ' || ch > '}' || ch == '`' ) + continue; + + // check colors + if( Q_IsColorString( in - 1 ) ) + { + // make sure room in dest for both chars + if( len > outSize - 2 ) + break; + + *out++ = ch; + len += 2; + + // solo trailing carat is not a color prefix + if( !*in ) { + *out++ = COLOR_WHITE; + break; + } + + // don't allow black in a name, unless if special + if( ColorIndex( *in ) == 0 && !special ) + *out++ = COLOR_WHITE; + else + *out++ = *in; + + in++; + continue; + } + + // don't allow too many consecutive spaces + if( ch == ' ' ) + { + spaces++; + if( spaces > 3 ) + continue; + } + else + spaces = 0; + + if( len > outSize - 1 ) + break; + + *out++ = ch; + colorlessLen++; + len++; + } + + *out = 0; + + // don't allow names beginning with "[skipnotify]" because it messes up /ignore-related code + if( !Q_strncmp( p, "[skipnotify]", 12 ) ) + invalid = qtrue; + + // don't allow comment-beginning strings because it messes up various parsers + if( strstr( p, "//" ) || strstr( p, "/*" ) ) + invalid = qtrue; + + // don't allow empty names + if( *p == 0 || colorlessLen == 0 ) + invalid = qtrue; + + // if something made the name bad, put them back to UnnamedPlayer + if( invalid ) + Q_strncpyz( p, "UnnamedPlayer", outSize ); +} + + +/* +=================== +G_NextNewbieName + +Generate a unique, known-good name for an UnnamedPlayer +=================== +*/ +char *G_NextNewbieName( gentity_t *ent ) +{ + char newname[ MAX_NAME_LENGTH ]; + char namePrefix[ MAX_NAME_LENGTH - 4 ]; + char err[ MAX_STRING_CHARS ]; + + if( g_newbieNamePrefix.string[ 0 ] ) + Q_strncpyz( namePrefix, g_newbieNamePrefix.string , sizeof( namePrefix ) ); + else + strcpy( namePrefix, "Newbie#" ); + + while( level.numNewbies < 10000 ) + { + strcpy( newname, va( "%s%i", namePrefix, level.numNewbies ) ); + if ( G_admin_name_check( ent, newname, err, sizeof( err ) ) ) + { + return va( "%s", newname ); + } + level.numNewbies++; // Only increments if the last requested name was used. + } + return "UnnamedPlayer"; +} + + +/* +====================== +G_NonSegModel + +Reads an animation.cfg to check for nonsegmentation +====================== +*/ +static qboolean G_NonSegModel( const char *filename ) +{ + char *text_p; + int len; + char *token; + char text[ 20000 ]; + fileHandle_t f; + + // load the file + len = trap_FS_FOpenFile( filename, &f, FS_READ ); + if( !f ) + { + G_Printf( "File not found: %s\n", filename ); + return qfalse; + } + + if( len < 0 ) + return qfalse; + + if( len == 0 || len >= sizeof( text ) - 1 ) + { + trap_FS_FCloseFile( f ); + G_Printf( "File %s is %s\n", filename, len == 0 ? "empty" : "too long" ); + return qfalse; + } + + trap_FS_Read( text, len, f ); + text[ len ] = 0; + trap_FS_FCloseFile( f ); + + // parse the text + text_p = text; + + // read optional parameters + while( 1 ) + { + token = COM_Parse( &text_p ); + + //EOF + if( !token[ 0 ] ) + break; + + if( !Q_stricmp( token, "nonsegmented" ) ) + return qtrue; + } + + return qfalse; +} + +/* +=========== +ClientUserInfoChanged + +Called from ClientConnect when the player first connects and +directly by the server system when the player updates a userinfo variable. + +The game can override any of the settings and call trap_SetUserinfo +if desired. +============ +*/ +void ClientUserinfoChanged( int clientNum, qboolean forceName ) +{ + gentity_t *ent; + int teamTask, teamLeader, health; + char *s; + char model[ MAX_QPATH ]; + char buffer[ MAX_QPATH ]; + char filename[ MAX_QPATH ]; + char oldname[ MAX_NAME_LENGTH ]; + char newname[ MAX_NAME_LENGTH ]; + char err[ MAX_STRING_CHARS ]; + qboolean revertName = qfalse; + qboolean showRenameMsg = qtrue; + gclient_t *client; + char c1[ MAX_INFO_STRING ]; + char c2[ MAX_INFO_STRING ]; + char userinfo[ MAX_INFO_STRING ]; + team_t team; + + ent = g_entities + clientNum; + client = ent->client; + + trap_GetUserinfo( clientNum, userinfo, sizeof( userinfo ) ); + + // check for malformed or illegal info strings + if( !Info_Validate(userinfo) ) + { + trap_SendServerCommand( ent - g_entities, + "disconnect \"illegal or malformed userinfo\n\"" ); + trap_DropClient( ent - g_entities, + "dropped: illegal or malformed userinfo"); + } + + + // check for local client + s = Info_ValueForKey( userinfo, "ip" ); + + if( !strcmp( s, "localhost" ) ) + client->pers.localClient = qtrue; + + // check the item prediction + s = Info_ValueForKey( userinfo, "cg_predictItems" ); + + if( !atoi( s ) ) + client->pers.predictItemPickup = qfalse; + else + client->pers.predictItemPickup = qtrue; + + // set name + Q_strncpyz( oldname, client->pers.netname, sizeof( oldname ) ); + s = Info_ValueForKey( userinfo, "name" ); + + if ( !G_admin_permission( ent, ADMF_SPECIALNAME ) ) + ClientCleanName( s, newname, sizeof( newname ), qfalse ); + else + ClientCleanName( s, newname, sizeof( newname ), qtrue ); + + if( strcmp( oldname, newname ) ) + { + if( !strlen( oldname ) && client->pers.connected != CON_CONNECTED ) + showRenameMsg = qfalse; + + // in case we need to revert and there's no oldname + if ( !G_admin_permission( ent, ADMF_SPECIALNAME ) ) + ClientCleanName( va( "%s", client->pers.netname ), oldname, sizeof( oldname ), qfalse ); + else + ClientCleanName( va( "%s", client->pers.netname ), oldname, sizeof( oldname ), qtrue ); + + if( g_newbieNumbering.integer ) + { + if( !strcmp( newname, "UnnamedPlayer" ) ) + Q_strncpyz( newname, G_NextNewbieName( ent ), sizeof( newname ) ); + if( !strcmp( oldname, "UnnamedPlayer" ) ) + Q_strncpyz( oldname, G_NextNewbieName( ent ), sizeof( oldname ) ); + } + + + if( !forceName ) + { + if( G_IsMuted( client ) ) + { + trap_SendServerCommand( ent - g_entities, + "print \"You cannot change your name while you are muted\n\"" ); + revertName = qtrue; + } + else if( client->pers.nameChangeTime && + ( level.time - client->pers.nameChangeTime ) + <= ( g_minNameChangePeriod.value * 1000 ) ) + { + trap_SendServerCommand( ent - g_entities, va( + "print \"Name change spam protection (g_minNameChangePeriod = %d)\n\"", + g_minNameChangePeriod.integer ) ); + revertName = qtrue; + } + else if( g_maxNameChanges.integer > 0 + && client->pers.nameChanges >= g_maxNameChanges.integer + && !G_admin_permission( ent, ADMF_SPECIAL ) ) + { + trap_SendServerCommand( ent - g_entities, va( + "print \"Maximum name changes reached (g_maxNameChanges = %d)\n\"", + g_maxNameChanges.integer ) ); + revertName = qtrue; + } + } + + if( !G_admin_name_check( ent, newname, err, sizeof( err ) ) ) + { + trap_SendServerCommand( ent - g_entities, va( "print \"%s\n\"", err ) ); + revertName = qtrue; + } + + if( revertName ) + { + Q_strncpyz( client->pers.netname, oldname, + sizeof( client->pers.netname ) ); + Info_SetValueForKey( userinfo, "name", oldname ); + trap_SetUserinfo( clientNum, userinfo ); + } + else + { + Q_strncpyz( client->pers.netname, newname, + sizeof( client->pers.netname ) ); + Info_SetValueForKey( userinfo, "name", newname ); + trap_SetUserinfo( clientNum, userinfo ); + if( client->pers.connected == CON_CONNECTED ) + { + client->pers.nameChangeTime = level.time; + client->pers.nameChanges++; + } + } + } + + if( client->sess.sessionTeam == TEAM_SPECTATOR ) + { + if( client->sess.spectatorState == SPECTATOR_SCOREBOARD ) + Q_strncpyz( client->pers.netname, "scoreboard", sizeof( client->pers.netname ) ); + } + + if( client->pers.connected >= CON_CONNECTING && showRenameMsg ) + { + if( strcmp( oldname, client->pers.netname ) ) + { + //dont show if players invisible + if( client->sess.invisible != qtrue ) + trap_SendServerCommand( -1, va( "print \"%s" S_COLOR_WHITE + " renamed to %s^7\n\"", oldname, client->pers.netname ) ); + if( g_decolourLogfiles.integer) + { + char decoloured[ MAX_STRING_CHARS ] = ""; + if( g_decolourLogfiles.integer == 1 ) + { + Com_sprintf( decoloured, sizeof(decoloured), " (\"%s^7\" -> \"%s^7\")", oldname, client->pers.netname ); + G_DecolorString( decoloured, decoloured ); + G_LogPrintfColoured( "ClientRename: %i [%s] (%s) \"%s^7\" -> \"%s^7\"%s\n", clientNum, + client->pers.ip, client->pers.guid, oldname, client->pers.netname, decoloured ); + } + else + { + G_LogPrintf( "ClientRename: %i [%s] (%s) \"%s^7\" -> \"%s^7\"%s\n", clientNum, + client->pers.ip, client->pers.guid, oldname, client->pers.netname, decoloured ); + } + + } + else + { + G_LogPrintf( "ClientRename: %i [%s] (%s) \"%s^7\" -> \"%s^7\"\n", clientNum, + client->pers.ip, client->pers.guid, oldname, client->pers.netname ); + } + G_admin_namelog_update( client, qfalse ); + } + } + + // set max health + health = atoi( Info_ValueForKey( userinfo, "handicap" ) ); + client->pers.maxHealth = health; + + if( client->pers.maxHealth < 1 || client->pers.maxHealth > 100 ) + client->pers.maxHealth = 100; + + //hack to force a client update if the config string does not change between spawning + if( client->pers.classSelection == PCL_NONE ) + client->pers.maxHealth = 0; + + // set model + if( client->ps.stats[ STAT_PCLASS ] == PCL_HUMAN && BG_InventoryContainsUpgrade( UP_BATTLESUIT, client->ps.stats ) ) + { + Com_sprintf( buffer, MAX_QPATH, "%s/%s", BG_FindModelNameForClass( PCL_HUMAN_BSUIT ), + BG_FindSkinNameForClass( PCL_HUMAN_BSUIT ) ); + } + else if( client->pers.classSelection == PCL_NONE ) + { + //This looks hacky and frankly it is. The clientInfo string needs to hold different + //model details to that of the spawning class or the info change will not be + //registered and an axis appears instead of the player model. There is zero chance + //the player can spawn with the battlesuit, hence this choice. + Com_sprintf( buffer, MAX_QPATH, "%s/%s", BG_FindModelNameForClass( PCL_HUMAN_BSUIT ), + BG_FindSkinNameForClass( PCL_HUMAN_BSUIT ) ); + } + else + { + Com_sprintf( buffer, MAX_QPATH, "%s/%s", BG_FindModelNameForClass( client->pers.classSelection ), + BG_FindSkinNameForClass( client->pers.classSelection ) ); + } + Q_strncpyz( model, buffer, sizeof( model ) ); + + //don't bother setting model type if spectating + if( client->pers.classSelection != PCL_NONE ) + { + //model segmentation + Com_sprintf( filename, sizeof( filename ), "models/players/%s/animation.cfg", + BG_FindModelNameForClass( client->pers.classSelection ) ); + + if( G_NonSegModel( filename ) ) + client->ps.persistant[ PERS_STATE ] |= PS_NONSEGMODEL; + else + client->ps.persistant[ PERS_STATE ] &= ~PS_NONSEGMODEL; + } + + // wallwalk follow + s = Info_ValueForKey( userinfo, "cg_wwFollow" ); + + if( atoi( s ) ) + client->ps.persistant[ PERS_STATE ] |= PS_WALLCLIMBINGFOLLOW; + else + client->ps.persistant[ PERS_STATE ] &= ~PS_WALLCLIMBINGFOLLOW; + + // wallwalk toggle + s = Info_ValueForKey( userinfo, "cg_wwToggle" ); + + if( atoi( s ) ) + client->ps.persistant[ PERS_STATE ] |= PS_WALLCLIMBINGTOGGLE; + else + client->ps.persistant[ PERS_STATE ] &= ~PS_WALLCLIMBINGTOGGLE; + + // teamInfo + s = Info_ValueForKey( userinfo, "teamoverlay" ); + + if( ! *s || atoi( s ) != 0 ) + client->pers.teamInfo = qtrue; + else + client->pers.teamInfo = qfalse; + + s = Info_ValueForKey( userinfo, "cg_unlagged" ); + if( !s[0] || atoi( s ) != 0 ) + client->pers.useUnlagged = qtrue; + else + client->pers.useUnlagged = qfalse; + + // team task (0 = none, 1 = offence, 2 = defence) + teamTask = atoi( Info_ValueForKey( userinfo, "teamtask" ) ); + // team Leader (1 = leader, 0 is normal player) + teamLeader = client->sess.teamLeader; + + // colors + strcpy( c1, Info_ValueForKey( userinfo, "color1" ) ); + strcpy( c2, Info_ValueForKey( userinfo, "color2" ) ); + + team = client->pers.teamSelection; + + // send over a subset of the userinfo keys so other clients can + // print scoreboards, display models, and play custom sounds + if ( client->sess.invisible != qtrue ) + { + Com_sprintf( userinfo, sizeof( userinfo ), + "n\\%s\\t\\%i\\model\\%s\\hmodel\\%s\\c1\\%s\\c2\\%s\\" + "hc\\%i\\w\\%i\\l\\%i\\tt\\%d\\" + "tl\\%d\\ig\\%16s", + client->pers.netname, team, model, model, c1, c2, + client->pers.maxHealth, client->sess.wins, client->sess.losses, teamTask, + teamLeader, BG_ClientListString( &client->sess.ignoreList ) ); + + trap_SetConfigstring( CS_PLAYERS + clientNum, userinfo ); + } else { + trap_SetConfigstring( CS_PLAYERS + clientNum, "" ); + } + /*G_LogPrintf( "ClientUserinfoChanged: %i %s\n", clientNum, userinfo );*/ +} + + +/* +=========== +ClientConnect + +Called when a player begins connecting to the server. +Called again for every map change or tournement restart. + +The session information will be valid after exit. + +Return NULL if the client should be allowed, otherwise return +a string with the reason for denial. + +Otherwise, the client will be sent the current gamestate +and will eventually get to ClientBegin. + +firstTime will be qtrue the very first time a client connects +to the server machine, but qfalse on map changes and tournement +restarts. +============ +*/ +char *ClientConnect( int clientNum, qboolean firstTime ) +{ + char *value; + gclient_t *client; + char userinfo[ MAX_INFO_STRING ]; + gentity_t *ent; + char guid[ 33 ]; + char ip[ 16 ] = {""}; + char reason[ MAX_STRING_CHARS ] = {""}; + int i; + + ent = &g_entities[ clientNum ]; + + trap_GetUserinfo( clientNum, userinfo, sizeof( userinfo ) ); + + value = Info_ValueForKey( userinfo, "cl_guid" ); + Q_strncpyz( guid, value, sizeof( guid ) ); + + // check for admin ban + if( G_admin_ban_check( userinfo, reason, sizeof( reason ) ) ) + { + return va( "%s", reason ); + } + + // IP filtering + // https://zerowing.idsoftware.com/bugzilla/show_bug.cgi?id=500 + // recommanding PB based IP / GUID banning, the builtin system is pretty limited + // check to see if they are on the banned IP list + value = Info_ValueForKey( userinfo, "ip" ); + i = 0; + while( *value && i < sizeof( ip ) - 2 ) + { + if( *value != '.' && ( *value < '0' || *value > '9' ) ) + break; + ip[ i++ ] = *value; + value++; + } + ip[ i ] = '\0'; + if( G_FilterPacket( value ) ) + return "You are banned from this server."; + + if( ip[ 0 ] == 0 ||strlen( ip ) < 7 ) + { + G_AdminsPrintf( "Connect from client with invalid IP: '%s' NAME: '%s^7'\n", + ip, Info_ValueForKey( userinfo, "name" ) ); + return "Invalid client data"; + } + + // check for a password + value = Info_ValueForKey( userinfo, "password" ); + + if( g_password.string[ 0 ] && Q_stricmp( g_password.string, "none" ) && + strcmp( g_password.string, value ) != 0 ) + return "Invalid password"; + + // they can connect + ent->client = level.clients + clientNum; + client = ent->client; + + memset( client, 0, sizeof(*client) ); + + // add guid to session so we don't have to keep parsing userinfo everywhere + if( !guid[ 0 ] ) + { + Q_strncpyz( client->pers.guid, "XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX", + sizeof( client->pers.guid ) ); + } + else + { + Q_strncpyz( client->pers.guid, guid, sizeof( client->pers.guid ) ); + } + + Q_strncpyz( client->pers.ip, ip, sizeof( client->pers.ip ) ); + client->pers.adminLevel = G_admin_level( ent ); + + // do autoghost now so that there won't be any name conflicts later on + if ( g_autoGhost.integer && client->pers.guid[ 0 ] != 'X' ) + { + for ( i = 0; i < MAX_CLIENTS; i++ ) + { + if ( i != ent - g_entities && g_entities[i].client && g_entities[i].client->pers.connected != CON_DISCONNECTED && !Q_stricmp( g_entities[i].client->pers.guid, client->pers.guid ) ) + { + trap_SendServerCommand( i, "disconnect \"You may not be connected to this server multiple times\"" ); + trap_DropClient( i, "disconnected" ); + } + } + } + + client->pers.connected = CON_CONNECTING; + + // read or initialize the session data + if( firstTime || level.newSession ) + G_InitSessionData( client, userinfo ); + + G_ReadSessionData( client ); + + if( firstTime ) + client->pers.firstConnect = qtrue; + else + client->pers.firstConnect = qfalse; + + // get and distribute relevent paramters + ClientUserinfoChanged( clientNum, qfalse ); + + G_admin_set_adminname( ent ); + + if( g_decolourLogfiles.integer ) + { + char decoloured[ MAX_STRING_CHARS ] = ""; + if( g_decolourLogfiles.integer == 1 ) + { + Com_sprintf( decoloured, sizeof(decoloured), " (\"%s^7\")", client->pers.netname ); + G_DecolorString( decoloured, decoloured ); + G_LogPrintfColoured( "ClientConnect: %i [%s] (%s) \"%s^7\"%s\n", clientNum, + client->pers.ip, client->pers.guid, client->pers.netname, decoloured ); + } + else + { + G_LogPrintf( "ClientConnect: %i [%s] (%s) \"%s^7\"%s\n", clientNum, + client->pers.ip, client->pers.guid, client->pers.netname, decoloured ); + } + } + else + { + G_LogPrintf( "ClientConnect: %i [%s] (%s) \"%s^7\"\n", clientNum, + client->pers.ip, client->pers.guid, client->pers.netname ); + } + + if( client->pers.adminLevel ) + { + G_LogPrintf( "ClientAuth: %i [%s] \"%s^7\" authenticated to admin level %i using GUID %s (^7%s)\n", clientNum, client->pers.ip, client->pers.netname, client->pers.adminLevel, client->pers.guid, client->pers.adminName ); + } + + // don't do the "xxx connected" messages if they were caried over from previous level + if( client->sess.invisible != qtrue ) + { + if( firstTime ) + trap_SendServerCommand( -1, va( "print \"%s" S_COLOR_WHITE " connected\n\"", client->pers.netname ) ); + + // count current clients and rank for scoreboard + CalculateRanks( ); + G_admin_namelog_update( client, qfalse ); + } + + + // if this is after !restart keepteams or !restart switchteams, apply said selection + if ( client->sess.restartTeam != PTE_NONE ) { + G_ChangeTeam( ent, client->sess.restartTeam ); + client->sess.restartTeam = PTE_NONE; + } + + + return NULL; +} + +/* +=========== +ClientBegin + +called when a client has finished connecting, and is ready +to be placed into the level. This will happen every level load, +and on transition between teams, but doesn't happen on respawns +============ +*/ +void ClientBegin( int clientNum ) +{ + gentity_t *ent; + gclient_t *client; + int flags; + + ent = g_entities + clientNum; + + client = level.clients + clientNum; + + if( ent->r.linked ) + trap_UnlinkEntity( ent ); + + G_InitGentity( ent ); + ent->touch = 0; + ent->pain = 0; + ent->client = client; + + client->pers.connected = CON_CONNECTED; + client->pers.enterTime = level.time; + client->pers.teamState.state = TEAM_BEGIN; + client->pers.classSelection = PCL_NONE; + + // save eflags around this, because changing teams will + // cause this to happen with a valid entity, and we + // want to make sure the teleport bit is set right + // so the viewpoint doesn't interpolate through the + // world to the new position + flags = client->ps.eFlags; + memset( &client->ps, 0, sizeof( client->ps ) ); + memset( &client->pmext, 0, sizeof( client->pmext ) ); + client->ps.eFlags = flags; + + // locate ent at a spawn point + + ClientSpawn( ent, NULL, NULL, NULL ); + + // Ignore invisible players for this section: + if ( client->sess.invisible != qtrue ) + { + trap_SendServerCommand( -1, va( "print \"%s" S_COLOR_WHITE " entered the game\n\"", client->pers.netname ) ); + + // auto mute flag + if( G_admin_permission( ent, ADMF_NO_CHAT ) ) + client->pers.muted = qtrue; + + // name can change between ClientConnect() and ClientBegin() + G_admin_namelog_update( client, qfalse ); + + // request the clients PTR code + trap_SendServerCommand( ent - g_entities, "ptrcrequest" ); + } + G_LogPrintf( "ClientBegin: %i\n", clientNum ); + + if( g_clientUpgradeNotice.integer ) + { + if( !Q_stricmp( ent->client->pers.guid, "XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX" ) ) + { + trap_SendServerCommand( client->ps.clientNum, va( "print \"^3Your client is out of date. Updating your client will allow you to " + "become an admin on servers and download maps much more quickly. Please replace your client executable with a newer client. \n\"" ) ); + + trap_SendServerCommand( client->ps.clientNum, va("print \"^3Some available clients: \n" + "^2TremFusion^7- ^3http://www.tremfusion.net/download/^7\n" + "^2FSM-Trem^7 - ^3http://code.google.com/p/fsm-trem/^7\n" + "^2MGClient^7 - ^3http://releases.mercenariesguild.net/client/^7\n\"" ) ); + } + } + + // count current clients and rank for scoreboard + CalculateRanks( ); +} + +/* +=========== +ClientSpawn + +Called every time a client is placed fresh in the world: +after the first ClientBegin, and after each respawn +Initializes all non-persistant parts of playerState +============ +*/ +void ClientSpawn( gentity_t *ent, gentity_t *spawn, vec3_t origin, vec3_t angles ) +{ + int index; + vec3_t spawn_origin, spawn_angles; + gclient_t *client; + int i; + clientPersistant_t saved; + clientSession_t savedSess; + int persistant[ MAX_PERSISTANT ]; + gentity_t *spawnPoint = NULL; + int flags; + int savedPing; + int teamLocal; + int eventSequence; + char userinfo[ MAX_INFO_STRING ]; + vec3_t up = { 0.0f, 0.0f, 1.0f }; + int maxAmmo, maxClips; + weapon_t weapon; + + + index = ent - g_entities; + client = ent->client; + + teamLocal = client->pers.teamSelection; + + //TA: only start client if chosen a class and joined a team + if( client->pers.classSelection == PCL_NONE && teamLocal == PTE_NONE ) + { + client->sess.sessionTeam = TEAM_SPECTATOR; + client->sess.spectatorState = SPECTATOR_FREE; + } + else if( client->pers.classSelection == PCL_NONE ) + { + client->sess.sessionTeam = TEAM_SPECTATOR; + client->sess.spectatorState = SPECTATOR_LOCKED; + } + + //if client is dead and following teammate, stop following before spawning + if(ent->client->sess.spectatorClient!=-1) + { + ent->client->sess.spectatorClient = -1; + ent->client->sess.spectatorState = SPECTATOR_FREE; + } + + if( origin != NULL ) + VectorCopy( origin, spawn_origin ); + + if( angles != NULL ) + VectorCopy( angles, spawn_angles ); + + // find a spawn point + // do it before setting health back up, so farthest + // ranging doesn't count this client + if( client->sess.sessionTeam == TEAM_SPECTATOR ) + { + if( teamLocal == PTE_NONE ) + spawnPoint = G_SelectSpectatorSpawnPoint( spawn_origin, spawn_angles ); + else if( teamLocal == PTE_ALIENS ) + spawnPoint = G_SelectAlienLockSpawnPoint( spawn_origin, spawn_angles ); + else if( teamLocal == PTE_HUMANS ) + spawnPoint = G_SelectHumanLockSpawnPoint( spawn_origin, spawn_angles ); + } + else + { + if( spawn == NULL ) + { + G_Error( "ClientSpawn: spawn is NULL\n" ); + return; + } + + spawnPoint = spawn; + + if( ent != spawn ) + { + //start spawn animation on spawnPoint + G_SetBuildableAnim( spawnPoint, BANIM_SPAWN1, qtrue ); + + if( spawnPoint->biteam == PTE_ALIENS ) + spawnPoint->clientSpawnTime = ALIEN_SPAWN_REPEAT_TIME; + else if( spawnPoint->biteam == PTE_HUMANS ) + spawnPoint->clientSpawnTime = HUMAN_SPAWN_REPEAT_TIME; + } + } + client->pers.teamState.state = TEAM_ACTIVE; + + // toggle the teleport bit so the client knows to not lerp + flags = ent->client->ps.eFlags & ( EF_TELEPORT_BIT | EF_VOTED | EF_TEAMVOTED ); + flags ^= EF_TELEPORT_BIT; + G_UnlaggedClear( ent ); + + // clear everything but the persistant data + + saved = client->pers; + savedSess = client->sess; + savedPing = client->ps.ping; + + for( i = 0; i < MAX_PERSISTANT; i++ ) + persistant[ i ] = client->ps.persistant[ i ]; + + eventSequence = client->ps.eventSequence; + memset( client, 0, sizeof( *client ) ); + + client->pers = saved; + client->sess = savedSess; + client->ps.ping = savedPing; + client->lastkilled_client = -1; + + for( i = 0; i < MAX_PERSISTANT; i++ ) + client->ps.persistant[ i ] = persistant[ i ]; + + client->ps.eventSequence = eventSequence; + + // increment the spawncount so the client will detect the respawn + client->ps.persistant[ PERS_SPAWN_COUNT ]++; + client->ps.persistant[ PERS_TEAM ] = client->sess.sessionTeam; + + // restore really persistant things + client->ps.persistant[ PERS_SCORE ] = client->pers.score; + client->ps.persistant[ PERS_CREDIT ] = client->pers.credit; + + client->airOutTime = level.time + 12000; + + trap_GetUserinfo( index, userinfo, sizeof( userinfo ) ); + client->ps.eFlags = flags; + + //Com_Printf( "ent->client->pers->pclass = %i\n", ent->client->pers.classSelection ); + + ent->s.groundEntityNum = ENTITYNUM_NONE; + ent->client = &level.clients[ index ]; + ent->takedamage = qtrue; + ent->inuse = qtrue; + ent->classname = "player"; + ent->r.contents = CONTENTS_BODY; + ent->clipmask = MASK_PLAYERSOLID; + ent->die = player_die; + ent->waterlevel = 0; + ent->watertype = 0; + ent->flags = 0; + + //TA: calculate each client's acceleration + ent->evaluateAcceleration = qtrue; + + client->ps.stats[ STAT_WEAPONS ] = 0; + client->ps.stats[ STAT_WEAPONS2 ] = 0; + client->ps.stats[ STAT_SLOTS ] = 0; + + client->ps.eFlags = flags; + client->ps.clientNum = index; + + BG_FindBBoxForClass( ent->client->pers.classSelection, ent->r.mins, ent->r.maxs, NULL, NULL, NULL ); + + if( client->sess.sessionTeam != TEAM_SPECTATOR ) + client->pers.maxHealth = client->ps.stats[ STAT_MAX_HEALTH ] = + BG_FindHealthForClass( ent->client->pers.classSelection ); + else + client->pers.maxHealth = client->ps.stats[ STAT_MAX_HEALTH ] = 100; + + // clear entity values + if( ent->client->pers.classSelection == PCL_HUMAN ) + { + BG_AddWeaponToInventory( WP_BLASTER, client->ps.stats ); + BG_AddUpgradeToInventory( UP_MEDKIT, client->ps.stats ); + weapon = client->pers.humanItemSelection; + } + else if( client->sess.sessionTeam != TEAM_SPECTATOR ) + weapon = BG_FindStartWeaponForClass( ent->client->pers.classSelection ); + else + weapon = WP_NONE; + + BG_FindAmmoForWeapon( weapon, &maxAmmo, &maxClips ); + BG_AddWeaponToInventory( weapon, client->ps.stats ); + BG_PackAmmoArray( weapon, client->ps.ammo, client->ps.powerups, maxAmmo, maxClips ); + + ent->client->ps.stats[ STAT_PCLASS ] = ent->client->pers.classSelection; + ent->client->ps.stats[ STAT_PTEAM ] = ent->client->pers.teamSelection; + + ent->client->ps.stats[ STAT_BUILDABLE ] = BA_NONE; + ent->client->ps.stats[ STAT_STATE ] = 0; + VectorSet( ent->client->ps.grapplePoint, 0.0f, 0.0f, 1.0f ); + + // health will count down towards max_health + ent->health = client->ps.stats[ STAT_HEALTH ] = client->ps.stats[ STAT_MAX_HEALTH ]; //* 1.25; + + //if evolving scale health + if( ent == spawn ) + { + ent->health *= ent->client->pers.evolveHealthFraction; + client->ps.stats[ STAT_HEALTH ] *= ent->client->pers.evolveHealthFraction; + } + + //clear the credits array + for( i = 0; i < MAX_CLIENTS; i++ ) + ent->credits[ i ] = 0; + + client->ps.stats[ STAT_STAMINA ] = MAX_STAMINA; + + if( mod_jetpackFuel.value >= 10.0f ) { + client->jetpackfuel = mod_jetpackFuel.value; + } + + G_SetOrigin( ent, spawn_origin ); + VectorCopy( spawn_origin, client->ps.origin ); + +#define UP_VEL 150.0f +#define F_VEL 50.0f + + //give aliens some spawn velocity + if( client->sess.sessionTeam != TEAM_SPECTATOR && + client->ps.stats[ STAT_PTEAM ] == PTE_ALIENS ) + { + if( ent == spawn ) + { + //evolution particle system + G_AddPredictableEvent( ent, EV_ALIEN_EVOLVE, DirToByte( up ) ); + } + else + { + spawn_angles[ YAW ] += 180.0f; + AngleNormalize360( spawn_angles[ YAW ] ); + + if( spawnPoint->s.origin2[ 2 ] > 0.0f ) + { + vec3_t forward, dir; + + AngleVectors( spawn_angles, forward, NULL, NULL ); + VectorScale( forward, F_VEL, forward ); + VectorAdd( spawnPoint->s.origin2, forward, dir ); + VectorNormalize( dir ); + + VectorScale( dir, UP_VEL, client->ps.velocity ); + } + + G_AddPredictableEvent( ent, EV_PLAYER_RESPAWN, 0 ); + } + } + else if( client->sess.sessionTeam != TEAM_SPECTATOR && + client->ps.stats[ STAT_PTEAM ] == PTE_HUMANS ) + { + spawn_angles[ YAW ] += 180.0f; + AngleNormalize360( spawn_angles[ YAW ] ); + } + + // the respawned flag will be cleared after the attack and jump keys come up + client->ps.pm_flags |= PMF_RESPAWNED; + + trap_GetUsercmd( client - level.clients, &ent->client->pers.cmd ); + G_SetClientViewAngle( ent, spawn_angles ); + + if( !( client->sess.sessionTeam == TEAM_SPECTATOR ) ) + { + /*G_KillBox( ent );*/ //blame this if a newly spawned client gets stuck in another + trap_LinkEntity( ent ); + + // force the base weapon up + client->ps.weapon = WP_NONE; + client->ps.weaponstate = WEAPON_READY; + } + + // don't allow full run speed for a bit + client->ps.pm_flags |= PMF_TIME_KNOCKBACK; + client->ps.pm_time = 100; + + client->respawnTime = level.time; + client->lastKillTime = level.time; + + client->inactivityTime = level.time + g_inactivity.integer * 1000; + client->latched_buttons = 0; + + // set default animations + client->ps.torsoAnim = TORSO_STAND; + client->ps.legsAnim = LEGS_IDLE; + + if( level.intermissiontime ) + MoveClientToIntermission( ent ); + else + { + // fire the targets of the spawn point + if( !spawn ) + G_UseTargets( spawnPoint, ent ); + + // select the highest weapon number available, after any + // spawn given items have fired + client->ps.weapon = 1; + + for( i = WP_NUM_WEAPONS - 1; i > 0 ; i-- ) + { + if( BG_InventoryContainsWeapon( i, client->ps.stats ) ) + { + client->ps.weapon = i; + break; + } + } + } + + // run a client frame to drop exactly to the floor, + // initialize animations and other things + client->ps.commandTime = level.time - 100; + ent->client->pers.cmd.serverTime = level.time; + ClientThink( ent-g_entities ); + + // positively link the client, even if the command times are weird + if( client->sess.sessionTeam != TEAM_SPECTATOR ) + { + BG_PlayerStateToEntityState( &client->ps, &ent->s, qtrue ); + VectorCopy( ent->client->ps.origin, ent->r.currentOrigin ); + trap_LinkEntity( ent ); + } + + //TA: must do this here so the number of active clients is calculated + CalculateRanks( ); + + // run the presend to set anything else + ClientEndFrame( ent ); + + // clear entity state values + BG_PlayerStateToEntityState( &client->ps, &ent->s, qtrue ); +} + + +/* +=========== +ClientDisconnect + +Called when a player drops from the server. +Will not be called between levels. + +This should NOT be called directly by any game logic, +call trap_DropClient(), which will call this and do +server system housekeeping. +============ +*/ +void ClientDisconnect( int clientNum ) +{ + gentity_t *ent; + gentity_t *tent; + int i; + buildHistory_t *ptr; + + ent = g_entities + clientNum; + + if( !ent->client ) + return; + + // look through the bhist and readjust it if the referenced ent has left + for( ptr = level.buildHistory; ptr; ptr = ptr->next ) + { + if( ptr->ent == ent ) + { + ptr->ent = NULL; + Q_strncpyz( ptr->name, ent->client->pers.netname, MAX_NETNAME ); + } + } + + if ( ent->client->sess.invisible != qtrue ) + G_admin_namelog_update( ent->client, qtrue ); + G_LeaveTeam( ent ); + + // stop any following clients + for( i = 0; i < level.maxclients; i++ ) + { + // remove any /ignore settings for this clientNum + BG_ClientListRemove( &level.clients[ i ].sess.ignoreList, clientNum ); + } + + // send effect if they were completely connected + if( ent->client->pers.connected == CON_CONNECTED && + ent->client->sess.sessionTeam != TEAM_SPECTATOR ) + { + tent = G_TempEntity( ent->client->ps.origin, EV_PLAYER_TELEPORT_OUT ); + tent->s.clientNum = ent->s.clientNum; + } + + if( ent->client->pers.connection ) + ent->client->pers.connection->clientNum = -1; + + G_LogPrintf( "ClientDisconnect: %i [%s] (%s) \"%s\"\n", clientNum, + ent->client->pers.ip, ent->client->pers.guid, ent->client->pers.netname ); + + trap_UnlinkEntity( ent ); + ent->s.modelindex = 0; + ent->inuse = qfalse; + ent->classname = "disconnected"; + ent->client->pers.connected = CON_DISCONNECTED; + ent->client->ps.persistant[ PERS_TEAM ] = TEAM_FREE; + ent->client->sess.sessionTeam = TEAM_FREE; + + trap_SetConfigstring( CS_PLAYERS + clientNum, ""); + + CalculateRanks( ); +} diff --git a/src/game/g_cmds.c b/src/game/g_cmds.c new file mode 100644 index 0000000..3857ef1 --- /dev/null +++ b/src/game/g_cmds.c @@ -0,0 +1,5709 @@ +/* +=========================================================================== +Copyright (C) 1999-2005 Id Software, Inc. +Copyright (C) 2000-2006 Tim Angus + +This file is part of Tremulous. + +Tremulous is free software; you can redistribute it +and/or modify it under the terms of the GNU General Public License as +published by the Free Software Foundation; either version 2 of the License, +or (at your option) any later version. + +Tremulous is distributed in the hope that it will be +useful, but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with Tremulous; if not, write to the Free Software +Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA +=========================================================================== +*/ + +#include "g_local.h" + +/* +================== +G_SanitiseString + +Remove case and control characters from a player name +================== +*/ +void G_SanitiseString( char *in, char *out, int len ) +{ + qboolean skip = qtrue; + int spaces = 0; + + while( *in && len > 0 ) + { + // strip leading white space + if( *in == ' ' ) + { + if( skip ) + { + in++; + continue; + } + spaces++; + } + else + { + spaces = 0; + skip = qfalse; + } + + if( Q_IsColorString( in ) ) + { + in += 2; // skip color code + continue; + } + + if( *in < 32 ) + { + in++; + continue; + } + + *out++ = tolower( *in++ ); + len--; + } + out -= spaces; + *out = 0; +} + +/* +================== +G_ClientNumberFromString + +Returns a player number for either a number or name string +Returns -1 if invalid +================== +*/ +int G_ClientNumberFromString( gentity_t *to, char *s ) +{ + gclient_t *cl; + int idnum; + char s2[ MAX_STRING_CHARS ]; + char n2[ MAX_STRING_CHARS ]; + + // numeric values are just slot numbers + if( s[ 0 ] >= '0' && s[ 0 ] <= '9' ) + { + idnum = atoi( s ); + + if( idnum < 0 || idnum >= level.maxclients ) + return -1; + + cl = &level.clients[ idnum ]; + + if( cl->pers.connected == CON_DISCONNECTED ) + return -1; + + return idnum; + } + + // check for a name match + G_SanitiseString( s, s2, sizeof( s2 ) ); + + for( idnum = 0, cl = level.clients; idnum < level.maxclients; idnum++, cl++ ) + { + if( cl->pers.connected == CON_DISCONNECTED ) + continue; + + G_SanitiseString( cl->pers.netname, n2, sizeof( n2 ) ); + + if( !strcmp( n2, s2 ) ) + return idnum; + } + + return -1; +} + + +/* +================== +G_MatchOnePlayer + +This is a companion function to G_ClientNumbersFromString() + +returns qtrue if the int array plist only has one client id, false otherwise +In the case of false, err will be populated with an error message. +================== +*/ +qboolean G_MatchOnePlayer( int *plist, char *err, int len ) +{ + gclient_t *cl; + int *p; + char line[ MAX_NAME_LENGTH + 10 ] = {""}; + + err[ 0 ] = '\0'; + if( plist[ 0 ] == -1 ) + { + Q_strcat( err, len, "no connected player by that name or slot #" ); + return qfalse; + } + if( plist[ 1 ] != -1 ) + { + Q_strcat( err, len, "more than one player name matches. " + "be more specific or use the slot #:\n" ); + for( p = plist; *p != -1; p++ ) + { + cl = &level.clients[ *p ]; + if( cl->pers.connected == CON_CONNECTED ) + { + Com_sprintf( line, sizeof( line ), "%2i - %s^7\n", + *p, cl->pers.netname ); + if( strlen( err ) + strlen( line ) > len ) + break; + Q_strcat( err, len, line ); + } + } + return qfalse; + } + return qtrue; +} + +/* +================== +G_ClientNumbersFromString + +Sets plist to an array of integers that represent client numbers that have +names that are a partial match for s. + +Returns number of matching clientids up to MAX_CLIENTS. +================== +*/ +int G_ClientNumbersFromString( char *s, int *plist) +{ + gclient_t *p; + int i, found = 0; + char n2[ MAX_NAME_LENGTH ] = {""}; + char s2[ MAX_NAME_LENGTH ] = {""}; + int max = MAX_CLIENTS; + + // if a number is provided, it might be a slot # + for( i = 0; s[ i ] && isdigit( s[ i ] ); i++ ); + if( !s[ i ] ) + { + i = atoi( s ); + if( i >= 0 && i < level.maxclients ) + { + p = &level.clients[ i ]; + if( p->pers.connected != CON_DISCONNECTED ) + { + *plist = i; + return 1; + } + } + // we must assume that if only a number is provided, it is a clientNum + *plist = -1; + return 0; + } + + // now look for name matches + G_SanitiseString( s, s2, sizeof( s2 ) ); + if( strlen( s2 ) < 1 ) + return 0; + for( i = 0; i < level.maxclients && found <= max; i++ ) + { + p = &level.clients[ i ]; + if( p->pers.connected == CON_DISCONNECTED ) + { + continue; + } + G_SanitiseString( p->pers.netname, n2, sizeof( n2 ) ); + if( strstr( n2, s2 ) ) + { + *plist++ = i; + found++; + } + } + *plist = -1; + return found; +} + +/* +================== +ScoreboardMessage + +================== +*/ +void ScoreboardMessage( gentity_t *ent ) +{ + char entry[ 1024 ]; + char string[ 1400 ]; + int stringlength; + int i, j; + gclient_t *cl; + int numSorted; + weapon_t weapon = WP_NONE; + upgrade_t upgrade = UP_NONE; + + // send the latest information on all clients + string[ 0 ] = 0; + stringlength = 0; + + numSorted = level.numConnectedClients; + + for( i = 0; i < numSorted; i++ ) + { + int ping; + + cl = &level.clients[ level.sortedClients[ i ] ]; + + if( cl->pers.connected == CON_CONNECTING ) + ping = -1; + else if( cl->sess.spectatorState == SPECTATOR_FOLLOW ) + ping = cl->pers.ping < 999 ? cl->pers.ping : 999; + else + ping = cl->ps.ping < 999 ? cl->ps.ping : 999; + + //If (loop) client is a spectator, they have nothing, so indicate such. + //Only send the client requesting the scoreboard the weapon/upgrades information for members of their team. If they are not on a team, send it all. + if( cl->sess.sessionTeam != TEAM_SPECTATOR && + (ent->client->pers.teamSelection == PTE_NONE || cl->pers.teamSelection == ent->client->pers.teamSelection ) ) + { + weapon = cl->ps.weapon; + + if( BG_InventoryContainsUpgrade( UP_BATTLESUIT, cl->ps.stats ) ) + upgrade = UP_BATTLESUIT; + else if( BG_InventoryContainsUpgrade( UP_JETPACK, cl->ps.stats ) ) + upgrade = UP_JETPACK; + else if( BG_InventoryContainsUpgrade( UP_BATTPACK, cl->ps.stats ) ) + upgrade = UP_BATTPACK; + else if( BG_InventoryContainsUpgrade( UP_HELMET, cl->ps.stats ) ) + upgrade = UP_HELMET; + else if( BG_InventoryContainsUpgrade( UP_LIGHTARMOUR, cl->ps.stats ) ) + upgrade = UP_LIGHTARMOUR; + else + upgrade = UP_NONE; + } + else + { + weapon = WP_NONE; + upgrade = UP_NONE; + } + + Com_sprintf( entry, sizeof( entry ), + " %d %d %d %d %d %d", level.sortedClients[ i ], cl->pers.score, ping, + ( level.time - cl->pers.enterTime ) / 60000, weapon, upgrade ); + + j = strlen( entry ); + + if( stringlength + j > 1024 ) + break; + + strcpy( string + stringlength, entry ); + stringlength += j; + } + + trap_SendServerCommand( ent-g_entities, va( "scores %i %i %i%s", i, + level.alienKills, level.humanKills, string ) ); +} + + +/* +================== +ConcatArgs +================== +*/ +char *ConcatArgs( int start ) +{ + int i, c, tlen; + static char line[ MAX_STRING_CHARS ]; + int len; + char arg[ MAX_STRING_CHARS ]; + + len = 0; + c = trap_Argc( ); + + for( i = start; i < c; i++ ) + { + trap_Argv( i, arg, sizeof( arg ) ); + tlen = strlen( arg ); + + if( len + tlen >= MAX_STRING_CHARS - 1 ) + break; + + memcpy( line + len, arg, tlen ); + len += tlen; + + if( len == MAX_STRING_CHARS - 1 ) + break; + + if( i != c - 1 ) + { + line[ len ] = ' '; + len++; + } + } + + line[ len ] = 0; + + return line; +} + +/* +================== +G_Flood_Limited + +Determine whether a user is flood limited, and adjust their flood demerits +================== +*/ + +qboolean G_Flood_Limited( gentity_t *ent ) +{ + int millisSinceLastCommand; + int maximumDemerits; + + // This shouldn't be called if g_floodMinTime isn't set, but handle it anyway. + if( !g_floodMinTime.integer ) + return qfalse; + + // Do not limit admins with no censor/flood flag + if( G_admin_permission( ent, ADMF_NOCENSORFLOOD ) ) + return qfalse; + + millisSinceLastCommand = level.time - ent->client->pers.lastFloodTime; + if( millisSinceLastCommand < g_floodMinTime.integer ) + ent->client->pers.floodDemerits += ( g_floodMinTime.integer - millisSinceLastCommand ); + else + { + ent->client->pers.floodDemerits -= ( millisSinceLastCommand - g_floodMinTime.integer ); + if( ent->client->pers.floodDemerits < 0 ) + ent->client->pers.floodDemerits = 0; + } + + ent->client->pers.lastFloodTime = level.time; + + // If g_floodMaxDemerits == 0, then we go against g_floodMinTime^2. + + if( !g_floodMaxDemerits.integer ) + maximumDemerits = g_floodMinTime.integer * g_floodMinTime.integer / 1000; + else + maximumDemerits = g_floodMaxDemerits.integer; + + if( ent->client->pers.floodDemerits > maximumDemerits ) + return qtrue; + + return qfalse; +} + +/* +================== +Cmd_Give_f + +Give items to a client +================== +*/ +void Cmd_Give_f( gentity_t *ent ) +{ + char *name; + qboolean give_all = qfalse; + + name = ConcatArgs( 1 ); + if( Q_stricmp( name, "all" ) == 0 ) + give_all = qtrue; + + if( give_all || Q_stricmp( name, "health" ) == 0 ) + { + if(!g_devmapNoGod.integer) + { + ent->health = ent->client->ps.stats[ STAT_MAX_HEALTH ]; + BG_AddUpgradeToInventory( UP_MEDKIT, ent->client->ps.stats ); + } + } + + if( give_all || Q_stricmpn( name, "funds", 5 ) == 0 ) + { + int credits = give_all ? HUMAN_MAX_CREDITS : atoi( name + 6 ); + G_AddCreditToClient( ent->client, credits, qtrue ); + } + + if( give_all || Q_stricmp( name, "stamina" ) == 0 ) + ent->client->ps.stats[ STAT_STAMINA ] = MAX_STAMINA; + + if( Q_stricmp( name, "poison" ) == 0 ) + { + ent->client->ps.stats[ STAT_STATE ] |= SS_BOOSTED; + ent->client->lastBoostedTime = level.time; + } + + if( give_all || Q_stricmp( name, "ammo" ) == 0 ) + { + int maxAmmo, maxClips; + gclient_t *client = ent->client; + + if( client->ps.weapon != WP_ALEVEL3_UPG && + BG_FindInfinteAmmoForWeapon( client->ps.weapon ) ) + return; + + BG_FindAmmoForWeapon( client->ps.weapon, &maxAmmo, &maxClips ); + + if( BG_FindUsesEnergyForWeapon( client->ps.weapon ) && + BG_InventoryContainsUpgrade( UP_BATTPACK, client->ps.stats ) ) + maxAmmo = (int)( (float)maxAmmo * BATTPACK_MODIFIER ); + + BG_PackAmmoArray( client->ps.weapon, client->ps.ammo, client->ps.powerups, maxAmmo, maxClips ); + } +} + + +/* +================== +Cmd_God_f + +Sets client to godmode + +argv(0) god +================== +*/ +void Cmd_God_f( gentity_t *ent ) +{ + char *msg; + + if( !g_devmapNoGod.integer ) + { + ent->flags ^= FL_GODMODE; + + if( !( ent->flags & FL_GODMODE ) ) + msg = "godmode OFF\n"; + else + msg = "godmode ON\n"; + } + else + { + msg = "Godmode has been disabled.\n"; + } + + trap_SendServerCommand( ent - g_entities, va( "print \"%s\"", msg ) ); +} + + +/* +================== +Cmd_Notarget_f + +Sets client to notarget + +argv(0) notarget +================== +*/ +void Cmd_Notarget_f( gentity_t *ent ) +{ + char *msg; + + if( !g_devmapNoGod.integer ) + { + ent->flags ^= FL_NOTARGET; + + if( !( ent->flags & FL_NOTARGET ) ) + msg = "notarget OFF\n"; + else + msg = "notarget ON\n"; + } + else + { + msg = "Godmode has been disabled.\n"; + } + + trap_SendServerCommand( ent - g_entities, va( "print \"%s\"", msg ) ); +} + + +/* +================== +Cmd_Noclip_f + +argv(0) noclip +================== +*/ +void Cmd_Noclip_f( gentity_t *ent ) +{ + char *msg; + + if( !g_devmapNoGod.integer ) + { + if( ent->client->noclip ) + msg = "noclip OFF\n"; + else + msg = "noclip ON\n"; + + ent->client->noclip = !ent->client->noclip; + } + else + { + msg = "Godmode has been disabled.\n"; + } + + trap_SendServerCommand( ent - g_entities, va( "print \"%s\"", msg ) ); +} + + +/* +================== +Cmd_LevelShot_f + +This is just to help generate the level pictures +for the menus. It goes to the intermission immediately +and sends over a command to the client to resize the view, +hide the scoreboard, and take a special screenshot +================== +*/ +void Cmd_LevelShot_f( gentity_t *ent ) +{ + BeginIntermission( ); + trap_SendServerCommand( ent - g_entities, "clientLevelShot" ); +} + +/* +================= +Cmd_Kill_f +================= +*/ +void Cmd_Kill_f( gentity_t *ent ) +{ + if( ent->client->ps.stats[ STAT_STATE ] & SS_INFESTING ) + return; + + if( ent->client->ps.stats[ STAT_STATE ] & SS_HOVELING ) + { + trap_SendServerCommand( ent-g_entities, "print \"Leave the hovel first (use your destroy key)\n\"" ); + return; + } + + if( g_cheats.integer ) + { + ent->flags &= ~FL_GODMODE; + ent->client->ps.stats[ STAT_HEALTH ] = ent->health = 0; + player_die( ent, ent, ent, 100000, MOD_SUICIDE ); + } + else + { + if( ent->suicideTime == 0 ) + { + trap_SendServerCommand( ent-g_entities, "print \"You will suicide in 20 seconds\n\"" ); + ent->suicideTime = level.time + 20000; + } + else if( ent->suicideTime > level.time ) + { + trap_SendServerCommand( ent-g_entities, "print \"Suicide canceled\n\"" ); + ent->suicideTime = 0; + } + } +} + +/* +================== +G_LeaveTeam +================== +*/ +void G_LeaveTeam( gentity_t *self ) +{ + pTeam_t team = self->client->pers.teamSelection; + gentity_t *ent; + int i; + + if( team == PTE_ALIENS ) + G_RemoveFromSpawnQueue( &level.alienSpawnQueue, self->client->ps.clientNum ); + else if( team == PTE_HUMANS ) + G_RemoveFromSpawnQueue( &level.humanSpawnQueue, self->client->ps.clientNum ); + else + { + if( self->client->sess.spectatorState == SPECTATOR_FOLLOW ) + { + G_StopFollowing( self ); + } + return; + } + + // Cancel pending suicides + self->suicideTime = 0; + + // stop any following clients + G_StopFromFollowing( self ); + + for( i = 0; i < level.num_entities; i++ ) + { + ent = &g_entities[ i ]; + if( !ent->inuse ) + continue; + + // clean up projectiles + if( ent->s.eType == ET_MISSILE && ent->r.ownerNum == self->s.number ) + G_FreeEntity( ent ); + if( ent->client && ent->client->pers.connected == CON_CONNECTED ) + { + // cure poison + if( ent->client->ps.stats[ STAT_STATE ] & SS_POISONCLOUDED && + ent->client->lastPoisonCloudedClient == self ) + ent->client->ps.stats[ STAT_STATE ] &= ~SS_POISONCLOUDED; + if( ent->client->ps.stats[ STAT_STATE ] & SS_POISONED && + ent->client->lastPoisonClient == self ) + ent->client->ps.stats[ STAT_STATE ] &= ~SS_POISONED; + } + } +} + +/* +================= +G_ChangeTeam +================= +*/ +void G_ChangeTeam( gentity_t *ent, pTeam_t newTeam ) +{ + pTeam_t oldTeam = ent->client->pers.teamSelection; + qboolean isFixingImbalance=qfalse; + + if( oldTeam == newTeam ) + return; + + G_LeaveTeam( ent ); + ent->client->pers.teamSelection = newTeam; + + // G_LeaveTeam() calls G_StopFollowing() which sets spec mode to free. + // Undo that in this case, or else people can freespec while in the spawn queue on their new team + if( newTeam != PTE_NONE ) + { + ent->client->sess.spectatorState = SPECTATOR_LOCKED; + } + + + if ( ( level.numAlienClients - level.numHumanClients > 2 && oldTeam==PTE_ALIENS && newTeam == PTE_HUMANS && level.numHumanSpawns>0 ) || + ( level.numHumanClients - level.numAlienClients > 2 && oldTeam==PTE_HUMANS && newTeam == PTE_ALIENS && level.numAlienSpawns>0 ) ) + { + isFixingImbalance=qtrue; + } + + // under certain circumstances, clients can keep their kills and credits + // when switching teams + if( G_admin_permission( ent, ADMF_TEAMCHANGEFREE ) || + ( g_teamImbalanceWarnings.integer && isFixingImbalance ) || + ( ( oldTeam == PTE_HUMANS || oldTeam == PTE_ALIENS ) + && ( level.time - ent->client->pers.teamChangeTime ) > 60000 ) ) + { + if( oldTeam == PTE_ALIENS ) + ent->client->pers.credit *= (float)FREEKILL_HUMAN / FREEKILL_ALIEN; + else if( newTeam == PTE_ALIENS ) + ent->client->pers.credit *= (float)FREEKILL_ALIEN / FREEKILL_HUMAN; + } + else + { + ent->client->pers.credit = 0; + ent->client->pers.score = 0; + } + + ent->client->ps.persistant[ PERS_KILLED ] = 0; + ent->client->pers.statscounters.kills = 0; + ent->client->pers.statscounters.structskilled = 0; + ent->client->pers.statscounters.assists = 0; + ent->client->pers.statscounters.repairspoisons = 0; + ent->client->pers.statscounters.headshots = 0; + ent->client->pers.statscounters.hits = 0; + ent->client->pers.statscounters.hitslocational = 0; + ent->client->pers.statscounters.deaths = 0; + ent->client->pers.statscounters.feeds = 0; + ent->client->pers.statscounters.suicides = 0; + ent->client->pers.statscounters.teamkills = 0; + ent->client->pers.statscounters.dmgdone = 0; + ent->client->pers.statscounters.structdmgdone = 0; + ent->client->pers.statscounters.ffdmgdone = 0; + ent->client->pers.statscounters.structsbuilt = 0; + ent->client->pers.statscounters.timealive = 0; + ent->client->pers.statscounters.timeinbase = 0; + ent->client->pers.statscounters.dretchbasytime = 0; + ent->client->pers.statscounters.jetpackusewallwalkusetime = 0; + + if( G_admin_permission( ent, ADMF_DBUILDER ) ) + { + if( !ent->client->pers.designatedBuilder ) + { + ent->client->pers.designatedBuilder = qtrue; + trap_SendServerCommand( ent-g_entities, + "print \"Your designation has been restored\n\"" ); + } + } + else if( ent->client->pers.designatedBuilder ) + { + ent->client->pers.designatedBuilder = qfalse; + trap_SendServerCommand( ent-g_entities, + "print \"You have lost designation due to teamchange\n\"" ); + } + + ent->client->pers.classSelection = PCL_NONE; + ClientSpawn( ent, NULL, NULL, NULL ); + + ent->client->pers.joinedATeam = qtrue; + ent->client->pers.teamChangeTime = level.time; + + //update ClientInfo + ClientUserinfoChanged( ent->client->ps.clientNum, qfalse ); + G_CheckDBProtection( ); +} + +/* +================= +Cmd_Team_f +================= +*/ +void Cmd_Team_f( gentity_t *ent ) +{ + pTeam_t team; + pTeam_t oldteam = ent->client->pers.teamSelection; + char s[ MAX_TOKEN_CHARS ]; + char buf[ MAX_STRING_CHARS ]; + qboolean force = G_admin_permission(ent, ADMF_FORCETEAMCHANGE); + int aliens = level.numAlienClients; + int humans = level.numHumanClients; + + // stop team join spam + if( level.time - ent->client->pers.teamChangeTime < 1000 ) + return; + // Prevent invisible players from joining a team + if ( ent->client->sess.invisible == qtrue ) + { + trap_SendServerCommand( ent-g_entities, + va( "print \"You cannot join a team while invisible\n\"" ) ); + return; + } + + if( oldteam == PTE_ALIENS ) + aliens--; + else if( oldteam == PTE_HUMANS ) + humans--; + + // do warm up + if( g_doWarmup.integer && g_warmupMode.integer == 1 && + level.time - level.startTime < g_warmup.integer * 1000 ) + { + trap_SendServerCommand( ent - g_entities, va( "print \"team: you can't join" + " a team during warm up (%d seconds remaining)\n\"", + g_warmup.integer - ( level.time - level.startTime ) / 1000 ) ); + return; + } + + trap_Argv( 1, s, sizeof( s ) ); + + if( !strlen( s ) ) + { + trap_SendServerCommand( ent-g_entities, va("print \"team: %i\n\"", + oldteam ) ); + return; + } + + if( Q_stricmpn( s, "spec", 4 ) ){ + if(G_admin_level(ent)<g_minLevelToJoinTeam.integer){ + trap_SendServerCommand( ent-g_entities,"print \"Sorry, but your admin level is only permitted to spectate.\n\"" ); + return; + } + } + + if( !Q_stricmpn( s, "spec", 4 ) ) + team = PTE_NONE; + else if( !force && ent->client->pers.teamSelection == PTE_NONE && + g_maxGameClients.integer && level.numPlayingClients >= + g_maxGameClients.integer ) + { + trap_SendServerCommand( ent - g_entities, va( "print \"The maximum number " + "of playing clients has been reached (g_maxGameClients = %i)\n\"", + g_maxGameClients.integer ) ); + return; + } + else if ( ent->client->pers.specExpires > level.time ) + { + trap_SendServerCommand( ent-g_entities, va( "print \"You can't join a team yet. Expires in %d seconds.\n\"", + ( ent->client->pers.specExpires - level.time ) / 1000 ) ); + return; + } + else if( !Q_stricmpn( s, "alien", 5 ) ) + { + if( g_forceAutoSelect.integer && !G_admin_permission(ent, ADMF_FORCETEAMCHANGE) ) + { + trap_SendServerCommand( ent-g_entities, "print \"You can only join teams using autoselect\n\"" ); + return; + } + + if( level.alienTeamLocked && !force ) + { + trap_SendServerCommand( ent-g_entities, + va( "print \"Alien team has been ^1LOCKED\n\"" ) ); + return; + } + else if( level.humanTeamLocked ) + { + // if only one team has been locked, let people join the other + // regardless of balance + force = qtrue; + } + + if( !force && g_teamForceBalance.integer && aliens > humans ) + { + G_TriggerMenu( ent - g_entities, MN_A_TEAMFULL ); + return; + } + + + team = PTE_ALIENS; + } + else if( !Q_stricmpn( s, "human", 5 ) ) + { + if( g_forceAutoSelect.integer && !G_admin_permission(ent, ADMF_FORCETEAMCHANGE) ) + { + trap_SendServerCommand( ent-g_entities, "print \"You can only join teams using autoselect\n\"" ); + return; + } + + if( level.humanTeamLocked && !force ) + { + trap_SendServerCommand( ent-g_entities, + va( "print \"Human team has been ^1LOCKED\n\"" ) ); + return; + } + else if( level.alienTeamLocked ) + { + // if only one team has been locked, let people join the other + // regardless of balance + force = qtrue; + } + + if( !force && g_teamForceBalance.integer && humans > aliens ) + { + G_TriggerMenu( ent - g_entities, MN_H_TEAMFULL ); + return; + } + + team = PTE_HUMANS; + } + else if( !Q_stricmp( s, "auto" ) ) + { + if( level.humanTeamLocked && level.alienTeamLocked ) + team = PTE_NONE; + else if( humans > aliens ) + team = PTE_ALIENS; + else if( humans < aliens ) + team = PTE_HUMANS; + else + team = PTE_ALIENS + ( rand( ) % 2 ); + + if( team == PTE_ALIENS && level.alienTeamLocked ) + team = PTE_HUMANS; + else if( team == PTE_HUMANS && level.humanTeamLocked ) + team = PTE_ALIENS; + } + else + { + trap_SendServerCommand( ent-g_entities, va( "print \"Unknown team: %s\n\"", s ) ); + return; + } + + // stop team join spam + if( oldteam == team ) + return; + + //guard against build timer exploit + if( oldteam != PTE_NONE && ent->client->sess.sessionTeam != TEAM_SPECTATOR && + ( ent->client->ps.stats[ STAT_PCLASS ] == PCL_ALIEN_BUILDER0 || + ent->client->ps.stats[ STAT_PCLASS ] == PCL_ALIEN_BUILDER0_UPG || + BG_InventoryContainsWeapon( WP_HBUILD, ent->client->ps.stats ) || + BG_InventoryContainsWeapon( WP_HBUILD2, ent->client->ps.stats ) ) && + ent->client->ps.stats[ STAT_MISC ] > 0 ) + { + trap_SendServerCommand( ent-g_entities, + va( "print \"You cannot change teams until build timer expires\n\"" ) ); + return; + } + + if (team != PTE_NONE) + { + char namebuff[32]; + + Q_strncpyz (namebuff, ent->client->pers.netname, sizeof(namebuff)); + Q_CleanStr (namebuff); + + if (!namebuff[0] || !Q_stricmp (namebuff, "UnnamedPlayer")) + { + trap_SendServerCommand( ent-g_entities, va( "print \"Please set your player name before joining a team. Press ESC and use the Options / Game menu or use /name in the console\n\"") ); + return; + } + } + + + G_ChangeTeam( ent, team ); + + + + if( team == PTE_ALIENS ) { + if ( oldteam == PTE_HUMANS ) + Com_sprintf( buf, sizeof( buf ), "%s^7 abandoned humans and joined the aliens.", ent->client->pers.netname ); + else + Com_sprintf( buf, sizeof( buf ), "%s^7 joined the aliens.", ent->client->pers.netname ); + } + else if( team == PTE_HUMANS ) { + if ( oldteam == PTE_ALIENS ) + Com_sprintf( buf, sizeof( buf ), "%s^7 abandoned the aliens and joined the humans.", ent->client->pers.netname ); + else + Com_sprintf( buf, sizeof( buf ), "%s^7 joined the humans.", ent->client->pers.netname ); + } + else if( team == PTE_NONE ) { + if ( oldteam == PTE_HUMANS ) + Com_sprintf( buf, sizeof( buf ), "%s^7 left the humans.", ent->client->pers.netname ); + else + Com_sprintf( buf, sizeof( buf ), "%s^7 left the aliens.", ent->client->pers.netname ); + } + trap_SendServerCommand( -1, va( "print \"%s\n\"", buf ) ); + G_LogOnlyPrintf("ClientTeam: %s\n",buf); +} + + +/* +================== +G_Say +================== +*/ +static void G_SayTo( gentity_t *ent, gentity_t *other, int mode, int color, const char *name, const char *message, const char *prefix ) +{ + qboolean ignore = qfalse; + qboolean specAllChat = qfalse; + + if( !other ) + return; + + if( !other->inuse ) + return; + + if( !other->client ) + return; + + if( other->client->pers.connected != CON_CONNECTED ) + return; + + if( ( mode == SAY_TEAM || mode == SAY_ACTION_T ) && !OnSameTeam( ent, other ) ) + { + if( other->client->pers.teamSelection != PTE_NONE ) + return; + + specAllChat = G_admin_permission( other, ADMF_SPEC_ALLCHAT ); + if( !specAllChat ) + return; + + // specs with ADMF_SPEC_ALLCHAT flag can see team chat + } + + if( mode == SAY_ADMINS && + (!G_admin_permission( other, ADMF_ADMINCHAT ) || other->client->pers.ignoreAdminWarnings ) ) + return; + + if( BG_ClientListTest( &other->client->sess.ignoreList, ent-g_entities ) ) + ignore = qtrue; + + if ( ignore && g_fullIgnore.integer ) + return; + + trap_SendServerCommand( other-g_entities, va( "%s \"%s%s%s%c%c%s\"", + ( mode == SAY_TEAM || mode == SAY_ACTION_T ) ? "tchat" : "chat", + ( ignore ) ? "[skipnotify]" : "", + ( specAllChat ) ? prefix : "", + name, Q_COLOR_ESCAPE, color, message ) ); +} + +#define EC "\x19" + +void G_Say( gentity_t *ent, gentity_t *target, int mode, const char *chatText ) +{ + int j; + gentity_t *other; + int color; + const char *prefix; + char name[ 64 ]; + // don't let text be too long for malicious reasons + char text[ MAX_SAY_TEXT ]; + char location[ 64 ]; + + // Bail if the text is blank. + if( ! chatText[0] ) + return; + + // Invisible players cannot use chat + if( ent->client->sess.invisible == qtrue ) + { + if( !G_admin_cmd_check( ent, qtrue ) ) + trap_SendServerCommand( ent-g_entities, "print \"You cannot chat while invisible\n\"" ); + return; + } + + // Spam limit: If they said this message recently, ignore it. + if( g_spamTime.integer ) + if ( ( level.time - ent->client->pers.lastMessageTime ) < ( g_spamTime.integer * 1000 ) && + !Q_stricmp( ent->client->pers.lastMessage, chatText) && + !G_admin_permission( ent, ADMF_NOCENSORFLOOD ) && + ent->client->pers.floodDemerits <= g_floodMaxDemerits.integer ) + { + trap_SendServerCommand( ent-g_entities, "print \"Your message has been ignored to prevent spam\n\"" ); + return; + } + else + { + ent->client->pers.lastMessageTime = level.time; + + Q_strncpyz( ent->client->pers.lastMessage, chatText, + sizeof( ent->client->pers.lastMessage ) ); + } + + // Flood limit. If they're talking too fast, determine that and return. + if( g_floodMinTime.integer ) + if ( G_Flood_Limited( ent ) ) + { + trap_SendServerCommand( ent-g_entities, "print \"Your chat is flood-limited; wait before chatting again\n\"" ); + return; + } + + if (g_chatTeamPrefix.integer && ent && ent->client ) + { + switch( ent->client->pers.teamSelection) + { + default: + case PTE_NONE: + prefix = "[^3S^7] "; + break; + + case PTE_ALIENS: + prefix = "[^1A^7] "; + break; + + case PTE_HUMANS: + prefix = "[^4H^7] "; + } + } + else + prefix = ""; + + switch( mode ) + { + default: + case SAY_ALL: + G_LogPrintf( "say: %s^7: %s^7\n", ent->client->pers.netname, chatText ); + Com_sprintf( name, sizeof( name ), "%s%s%c%c"EC": ", prefix, + ent->client->pers.netname, Q_COLOR_ESCAPE, COLOR_WHITE ); + color = COLOR_GREEN; + break; + + case SAY_TEAM: + G_LogPrintf( "sayteam: %s%s^7: %s^7\n", prefix, ent->client->pers.netname, chatText ); + if( Team_GetLocationMsg( ent, location, sizeof( location ) ) ) + Com_sprintf( name, sizeof( name ), EC"(%s%c%c"EC") (%s)"EC": ", + ent->client->pers.netname, Q_COLOR_ESCAPE, COLOR_WHITE, location ); + else + Com_sprintf( name, sizeof( name ), EC"(%s%c%c"EC")"EC": ", + ent->client->pers.netname, Q_COLOR_ESCAPE, COLOR_WHITE ); + + if( ent->client->pers.teamSelection == PTE_NONE ) + color = COLOR_YELLOW; + else + color = COLOR_CYAN; + break; + + case SAY_TELL: + if( target && OnSameTeam( target, ent ) && + Team_GetLocationMsg( ent, location, sizeof( location ) ) ) + Com_sprintf( name, sizeof( name ), EC"[%s%c%c"EC"] (%s)"EC": ", + ent->client->pers.netname, Q_COLOR_ESCAPE, COLOR_WHITE, location ); + else + Com_sprintf( name, sizeof( name ), EC"[%s%c%c"EC"]"EC": ", + ent->client->pers.netname, Q_COLOR_ESCAPE, COLOR_WHITE ); + color = COLOR_MAGENTA; + break; + + case SAY_ACTION: + G_LogPrintf( "action: %s^7: %s^7\n", ent->client->pers.netname, chatText ); + Com_sprintf( name, sizeof( name ), "^2%s^7%s%s%c%c"EC" ", g_actionPrefix.string, prefix, + ent->client->pers.netname, Q_COLOR_ESCAPE, COLOR_WHITE ); + color = COLOR_WHITE; + break; + + case SAY_ACTION_T: + G_LogPrintf( "actionteam: %s%s^7: %s^7\n", prefix, ent->client->pers.netname, chatText ); + if( Team_GetLocationMsg( ent, location, sizeof( location ) ) ) + Com_sprintf( name, sizeof( name ), EC"^5%s^7%s%c%c"EC"(%s)"EC" ", g_actionPrefix.string, + ent->client->pers.netname, Q_COLOR_ESCAPE, COLOR_WHITE, location ); + else + Com_sprintf( name, sizeof( name ), EC"^5%s^7%s%c%c"EC""EC" ", g_actionPrefix.string, + ent->client->pers.netname, Q_COLOR_ESCAPE, COLOR_WHITE ); + color = COLOR_WHITE; + break; + + case SAY_ADMINS: + if( G_admin_permission( ent, ADMF_ADMINCHAT ) ) //Differentiate between inter-admin chatter and user-admin alerts + { + G_LogPrintf( "say_admins: [ADMIN]%s^7: %s^7\n", ( ent ) ? ent->client->pers.netname : "console", chatText ); + Com_sprintf( name, sizeof( name ), "%s[ADMIN]%s%c%c"EC": ", prefix, + ( ent ) ? ent->client->pers.netname : "console", Q_COLOR_ESCAPE, COLOR_WHITE ); + color = COLOR_MAGENTA; + } + else + { + G_LogPrintf( "say_admins: [PLAYER]%s^7: %s^7\n", ent->client->pers.netname, chatText ); + Com_sprintf( name, sizeof( name ), "%s[PLAYER]%s%c%c"EC": ", prefix, + ent->client->pers.netname, Q_COLOR_ESCAPE, COLOR_WHITE ); + color = COLOR_MAGENTA; + } + break; + } + + if( mode!=SAY_TEAM && ent && ent->client && ent->client->pers.teamSelection == PTE_NONE && G_admin_level(ent)<g_minLevelToSpecMM1.integer ) + { + trap_SendServerCommand( ent-g_entities,va( "print \"Sorry, but your admin level may only use teamchat while spectating.\n\"") ); + return; + } + + Com_sprintf( text, sizeof( text ), "%s^7", chatText ); + + if( ent && ent->client && g_aimbotAdvertBan.integer && ( Q_stricmp( text, "^1N^7ullify for ^1T^7remulous [beta] | Get it at CheatersUtopia.com^7" ) == 0 ) ) + { + trap_SendConsoleCommand( 0, + va( "!ban %s %s %s\n", + ent->client->pers.ip, + ( g_aimbotAdvertBanTime.string && Q_stricmp( g_aimbotAdvertBanTime.string, "0" ) == 1 ) ? g_aimbotAdvertBanTime.string : "" , + g_aimbotAdvertBanReason.string ) ); + Q_strncpyz( text, "^7has been caught hacking and will be dealt with.", sizeof( text ) ); + } + + if( target ) + { + G_SayTo( ent, target, mode, color, name, text, prefix ); + return; + } + + + + // Ugly hax: if adminsayfilter is off, do the SAY first to prevent text from going out of order + if( !g_adminSayFilter.integer ) + { + // send it to all the apropriate clients + for( j = 0; j < level.maxclients; j++ ) + { + other = &g_entities[ j ]; + G_SayTo( ent, other, mode, color, name, text, prefix ); + } + } + + if( g_adminParseSay.integer && ( mode== SAY_ALL || mode == SAY_TEAM ) ) + { + if( G_admin_cmd_check ( ent, qtrue ) && g_adminSayFilter.integer ) + { + return; + } + } + + // if it's on, do it here, where it won't happen if it was an admin command + if( g_adminSayFilter.integer ) + { + // send it to all the apropriate clients + for( j = 0; j < level.maxclients; j++ ) + { + other = &g_entities[ j ]; + G_SayTo( ent, other, mode, color, name, text, prefix ); + } + } + + +} + +static void Cmd_SayArea_f( gentity_t *ent ) +{ + int entityList[ MAX_GENTITIES ]; + int num, i; + int color = COLOR_BLUE; + const char *prefix; + vec3_t range = { HELMET_RANGE, HELMET_RANGE, HELMET_RANGE }; + vec3_t mins, maxs; + char *msg = ConcatArgs( 1 ); + char name[ 64 ]; + + if( g_floodMinTime.integer ) + if ( G_Flood_Limited( ent ) ) + { + trap_SendServerCommand( ent-g_entities, "print \"Your chat is flood-limited; wait before chatting again\n\"" ); + return; + } + + if (g_chatTeamPrefix.integer) + { + switch( ent->client->pers.teamSelection) + { + default: + case PTE_NONE: + prefix = "[^3S^7] "; + break; + + case PTE_ALIENS: + prefix = "[^1A^7] "; + break; + + case PTE_HUMANS: + prefix = "[^4H^7] "; + } + } + else + prefix = ""; + + G_LogPrintf( "sayarea: %s%s^7: %s\n", prefix, ent->client->pers.netname, msg ); + Com_sprintf( name, sizeof( name ), EC"<%s%c%c"EC"> ", + ent->client->pers.netname, Q_COLOR_ESCAPE, COLOR_WHITE ); + + VectorAdd( ent->s.origin, range, maxs ); + VectorSubtract( ent->s.origin, range, mins ); + + num = trap_EntitiesInBox( mins, maxs, entityList, MAX_GENTITIES ); + for( i = 0; i < num; i++ ) + G_SayTo( ent, &g_entities[ entityList[ i ] ], SAY_TEAM, color, name, msg, prefix ); + + //Send to ADMF_SPEC_ALLCHAT candidates + for( i = 0; i < level.maxclients; i++ ) + { + if( (&g_entities[ i ])->client->pers.teamSelection == PTE_NONE && + G_admin_permission( &g_entities[ i ], ADMF_SPEC_ALLCHAT ) ) + { + G_SayTo( ent, &g_entities[ i ], SAY_TEAM, color, name, msg, prefix ); + } + } +} + + +/* +================== +Cmd_Say_f +================== +*/ +static void Cmd_Say_f( gentity_t *ent ) +{ + char *p; + char *args; + int mode = SAY_ALL; + int skipargs = 0; + + args = G_SayConcatArgs( 0 ); + if( Q_stricmpn( args, "say_team ", 9 ) == 0 ) + mode = SAY_TEAM; + if( Q_stricmpn( args, "say_admins ", 11 ) == 0 || Q_stricmpn( args, "a ", 2 ) == 0) + mode = SAY_ADMINS; + + // support parsing /m out of say text since some people have a hard + // time figuring out what the console is. + if( !Q_stricmpn( args, "say /m ", 7 ) || + !Q_stricmpn( args, "say_team /m ", 12 ) || + !Q_stricmpn( args, "say /mt ", 8 ) || + !Q_stricmpn( args, "say_team /mt ", 13 ) ) + { + G_PrivateMessage( ent ); + return; + } + + + if( !Q_stricmpn( args, "say /a ", 7) || + !Q_stricmpn( args, "say_team /a ", 12) || + !Q_stricmpn( args, "say /say_admins ", 16) || + !Q_stricmpn( args, "say_team /say_admins ", 21) ) + { + mode = SAY_ADMINS; + skipargs=1; + } + + if( mode == SAY_ADMINS) + if(!G_admin_permission( ent, ADMF_ADMINCHAT ) ) + { + if( !g_publicSayadmins.integer ) + { + ADMP( "Sorry, but public use of say_admins has been disabled.\n" ); + return; + } + else + { + ADMP( "Your message has been sent to any available admins and to the server logs.\n" ); + } + } + + + if(!Q_stricmpn( args, "say /me ", 8 ) ) + { + if( g_actionPrefix.string[0] ) + { + mode = SAY_ACTION; + skipargs=1; + } else return; + } + else if(!Q_stricmpn( args, "say_team /me ", 13 ) ) + { + if( g_actionPrefix.string[0] ) + { + mode = SAY_ACTION_T; + skipargs=1; + } else return; + } + else if( !Q_stricmpn( args, "me ", 3 ) ) + { + if( g_actionPrefix.string[0] ) + { + mode = SAY_ACTION; + } else return; + } + else if( !Q_stricmpn( args, "me_team ", 8 ) ) + { + if( g_actionPrefix.string[0] ) + { + mode = SAY_ACTION_T; + } else return; + } + + + if( g_allowShare.integer ) + { + args = G_SayConcatArgs(0); + if( !Q_stricmpn( args, "say /share", 10 ) || + !Q_stricmpn( args, "say_team /share", 15 ) ) + { + Cmd_Share_f( ent ); + return; + } + if( !Q_stricmpn( args, "say /donate", 11 ) || + !Q_stricmpn( args, "say_team /donate", 16 ) ) + { + Cmd_Donate_f( ent ); + return; + } + } + + + if( trap_Argc( ) < 2 ) + return; + + p = G_SayConcatArgs( 1 + skipargs ); + + G_Say( ent, NULL, mode, p ); +} + +/* +================== +Cmd_Tell_f +================== +*/ +static void Cmd_Tell_f( gentity_t *ent ) +{ + int targetNum; + gentity_t *target; + char *p; + char arg[MAX_TOKEN_CHARS]; + + if( trap_Argc( ) < 2 ) + return; + + trap_Argv( 1, arg, sizeof( arg ) ); + targetNum = atoi( arg ); + + if( targetNum < 0 || targetNum >= level.maxclients ) + return; + + target = &g_entities[ targetNum ]; + if( !target || !target->inuse || !target->client ) + return; + + p = ConcatArgs( 2 ); + + G_LogPrintf( "tell: %s to %s: %s\n", ent->client->pers.netname, target->client->pers.netname, p ); + G_Say( ent, target, SAY_TELL, p ); + // don't tell to the player self if it was already directed to this player + // also don't send the chat back to a bot + if( ent != target ) + G_Say( ent, ent, SAY_TELL, p ); +} + +/* +================== +Cmd_Where_f +================== +*/ +void Cmd_Where_f( gentity_t *ent ) +{ + trap_SendServerCommand( ent-g_entities, va( "print \"%s\n\"", vtos( ent->s.origin ) ) ); +} + + +static qboolean map_is_votable( const char *map ) +{ + char maps[ MAX_CVAR_VALUE_STRING ]; + char *token, *token_p; + + if( !g_votableMaps.string[ 0 ] ) + return qtrue; + + Q_strncpyz( maps, g_votableMaps.string, sizeof( maps ) ); + token_p = maps; + while( *( token = COM_Parse( &token_p ) ) ) + { + if( !Q_stricmp( token, map ) ) + return qtrue; + } + + return qfalse; +} + +/* +================== +Cmd_CallVote_f +================== +*/ +void Cmd_CallVote_f( gentity_t *ent ) +{ + int i; + char arg1[ MAX_STRING_TOKENS ]; + char arg2[ MAX_STRING_TOKENS ]; + int clientNum = -1; + char name[ MAX_NETNAME ]; + char *arg1plus; + char *arg2plus; + char nullstring[] = ""; + char message[ MAX_STRING_CHARS ]; + char targetname[ MAX_NAME_LENGTH] = ""; + char reason[ MAX_STRING_CHARS ] = ""; + char *ptr = NULL; + + arg1plus = G_SayConcatArgs( 1 ); + arg2plus = G_SayConcatArgs( 2 ); + + // Invisible players cannot call votes + if( ent->client->sess.invisible == qtrue ) + { + trap_SendServerCommand( ent-g_entities, "print \"You cannot call votes while invisible\n\"" ); + return; + } + + if( !g_allowVote.integer ) + { + trap_SendServerCommand( ent-g_entities, "print \"Voting not allowed here\n\"" ); + return; + } + + // Flood limit. If they're talking too fast, determine that and return. + if( g_floodMinTime.integer ) + if ( G_Flood_Limited( ent ) ) + { + trap_SendServerCommand( ent-g_entities, "print \"Your /callvote attempt is flood-limited; wait before chatting again\n\"" ); + return; + } + + //see if they can vote + if( G_admin_permission( ent, ADMF_NO_VOTE ) ) + { + trap_SendServerCommand( ent-g_entities, "print \"You have no voting rights\n\"" ); + return; + } + + if( g_voteMinTime.integer + && ent->client->pers.firstConnect + && level.time - ent->client->pers.enterTime < g_voteMinTime.integer * 1000 + && !G_admin_permission( ent, ADMF_NO_VOTE_LIMIT ) + && (level.numPlayingClients > 0 && level.numConnectedClients>1) ) + { + trap_SendServerCommand( ent-g_entities, va( + "print \"You must wait %d seconds after connecting before calling a vote\n\"", + g_voteMinTime.integer ) ); + return; + } + + if( level.voteTime ) + { + trap_SendServerCommand( ent-g_entities, "print \"A vote is already in progress\n\"" ); + return; + } + + if( g_voteLimit.integer > 0 + && ent->client->pers.voteCount >= g_voteLimit.integer + && !G_admin_permission( ent, ADMF_NO_VOTE_LIMIT ) ) + { + trap_SendServerCommand( ent-g_entities, va( + "print \"You have already called the maximum number of votes (%d)\n\"", + g_voteLimit.integer ) ); + return; + } + + if( G_IsMuted( ent->client ) ) + { + trap_SendServerCommand( ent - g_entities, + "print \"You are muted and cannot call votes\n\"" ); + return; + } + + // make sure it is a valid command to vote on + trap_Argv( 1, arg1, sizeof( arg1 ) ); + trap_Argv( 2, arg2, sizeof( arg2 ) ); + + if( strchr( arg1plus, ';' ) ) + { + trap_SendServerCommand( ent-g_entities, "print \"Invalid vote string\n\"" ); + return; + } + + // if there is still a vote to be executed + if( level.voteExecuteTime ) + { + if( !Q_stricmp( level.voteString, "map_restart" ) ) + { + G_admin_maplog_result( "r" ); + } + else if( !Q_stricmpn( level.voteString, "map", 3 ) ) + { + G_admin_maplog_result( "m" ); + } + + level.voteExecuteTime = 0; + trap_SendConsoleCommand( EXEC_APPEND, va( "%s\n", level.voteString ) ); + } + + level.votePassThreshold=50; + + ptr = strstr(arg1plus, " -"); + if( ptr ) + { + *ptr = '\0'; + ptr+=2; + + if( *ptr == 'r' || *ptr=='R' ) + { + ptr++; + while( *ptr == ' ' ) + ptr++; + strcpy(reason, ptr); + } + else + { + trap_SendServerCommand( ent-g_entities, "print \"callvote: Warning: invalid argument specified \n\"" ); + } + } + + // detect clientNum for partial name match votes + if( !Q_stricmp( arg1, "kick" ) || + !Q_stricmp( arg1, "spec" ) || + !Q_stricmp( arg1, "mute" ) || + !Q_stricmp( arg1, "unmute" ) ) + { + int clientNums[ MAX_CLIENTS ] = { -1 }; + int numMatches=0; + char err[ MAX_STRING_CHARS ] = ""; + + Q_strncpyz(targetname, arg2plus, sizeof(targetname)); + ptr = strstr(targetname, " -"); + if( ptr ) + *ptr = '\0'; + + if( g_requireVoteReasons.integer && !G_admin_permission( ent, ADMF_UNACCOUNTABLE ) && !Q_stricmp( arg1, "kick" ) && reason[ 0 ]=='\0' ) + { + trap_SendServerCommand( ent-g_entities, "print \"callvote: You must specify a reason. Use /callvote kick [player] -r [reason] \n\"" ); + return; + } + + if( !targetname[ 0 ] ) + { + trap_SendServerCommand( ent-g_entities, + "print \"callvote: no target\n\"" ); + return; + } + + numMatches = G_ClientNumbersFromString( targetname, clientNums ); + if( numMatches == 1 ) + { + // there was only one partial name match + clientNum = clientNums[ 0 ]; + } + else + { + // look for an exact name match (sets clientNum to -1 if it fails) + clientNum = G_ClientNumberFromString( ent, targetname ); + } + + if( clientNum==-1 && numMatches > 1 ) + { + G_MatchOnePlayer( clientNums, err, sizeof( err ) ); + ADMP( va( "^3callvote: ^7%s\n", err ) ); + return; + } + + if( clientNum != -1 && + level.clients[ clientNum ].pers.connected != CON_CONNECTED ) + { + clientNum = -1; + } + + if( clientNum != -1 ) + { + Q_strncpyz( name, level.clients[ clientNum ].pers.netname, + sizeof( name ) ); + Q_CleanStr( name ); + if ( G_admin_permission ( &g_entities[ clientNum ], ADMF_IMMUNITY ) ) + { + char reasonprint[ MAX_STRING_CHARS ] = ""; + + if( reason[ 0 ] != '\0' ) + Com_sprintf(reasonprint, sizeof(reasonprint), "With reason: %s", reason); + + Com_sprintf( message, sizeof( message ), "%s^7 attempted /callvote %s %s on immune admin %s^7 %s^7", + ent->client->pers.netname, arg1, targetname, g_entities[ clientNum ].client->pers.netname, reasonprint ); + } + } + else + { + trap_SendServerCommand( ent-g_entities, + "print \"callvote: invalid player\n\"" ); + return; + } + } + + if( !Q_stricmp( arg1, "kick" ) ) + { + if( G_admin_permission( &g_entities[ clientNum ], ADMF_IMMUNITY ) ) + { + trap_SendServerCommand( ent-g_entities, + "print \"callvote: admin is immune from vote kick\n\"" ); + G_AdminsPrintf("%s\n",message); + G_admin_adminlog_log( ent, "vote", NULL, 0, qfalse ); + return; + } + + // use ip in case this player disconnects before the vote ends + Com_sprintf( level.voteString, sizeof( level.voteString ), + "!ban %s \"%s\" vote kick", level.clients[ clientNum ].pers.ip, + g_adminTempBan.string ); + if ( reason[0]!='\0' ) + Q_strcat( level.voteString, sizeof( level.voteDisplayString ), va( "(%s^7)", reason ) ); + Com_sprintf( level.voteDisplayString, sizeof( level.voteDisplayString ), + "Kick player \'%s\'", name ); + } + else if( !Q_stricmp( arg1, "spec" ) ) + { + if( G_admin_permission( &g_entities[ clientNum ], ADMF_IMMUNITY ) ) + { + trap_SendServerCommand( ent-g_entities, "print \"callvote: admin is immune from vote spec\n\"" ); + return; + } + Com_sprintf( level.voteString, sizeof( level.voteString ), "!putteam %i s %s", clientNum, g_adminTempSpec.string ); + Com_sprintf( level.voteDisplayString, sizeof( level.voteDisplayString ), "Spec player \'%s\'", name ); + + } + else if( !Q_stricmp( arg1, "mute" ) ) + { + if( G_IsMuted( &level.clients[ clientNum ] ) ) + { + trap_SendServerCommand( ent-g_entities, + "print \"callvote: player is already muted\n\"" ); + return; + } + + if( G_admin_permission( &g_entities[ clientNum ], ADMF_IMMUNITY ) ) + { + trap_SendServerCommand( ent-g_entities, + "print \"callvote: admin is immune from vote mute\n\"" ); + G_AdminsPrintf("%s\n",message); + G_admin_adminlog_log( ent, "vote", NULL, 0, qfalse ); + return; + } + Com_sprintf( level.voteString, sizeof( level.voteString ), + "!mute %i %s", clientNum, g_adminTempMute.string ); + Com_sprintf( level.voteDisplayString, sizeof( level.voteDisplayString ), + "Mute player \'%s\'", name ); + } + else if( !Q_stricmp( arg1, "unmute" ) ) + { + if( !G_IsMuted( &level.clients[ clientNum ] ) ) + { + trap_SendServerCommand( ent-g_entities, + "print \"callvote: player is not currently muted\n\"" ); + return; + } + Com_sprintf( level.voteString, sizeof( level.voteString ), + "!unmute %i", clientNum ); + Com_sprintf( level.voteDisplayString, sizeof( level.voteDisplayString ), + "Un-Mute player \'%s\'", name ); + } + else if( !Q_stricmp( arg1, "map_restart" ) ) + { + if( g_mapvoteMaxTime.integer + && (( level.time - level.startTime ) >= g_mapvoteMaxTime.integer * 1000 ) + && !G_admin_permission( ent, ADMF_NO_VOTE_LIMIT ) + && (level.numPlayingClients > 0 && level.numConnectedClients>1) ) + { + trap_SendServerCommand( ent-g_entities, va( + "print \"You cannot call for a restart after %d seconds\n\"", + g_mapvoteMaxTime.integer ) ); + G_admin_adminlog_log( ent, "vote", NULL, 0, qfalse ); + return; + } + Com_sprintf( level.voteString, sizeof( level.voteString ), "%s", arg1 ); + Com_sprintf( level.voteDisplayString, + sizeof( level.voteDisplayString ), "Restart current map" ); + level.votePassThreshold = g_mapVotesPercent.integer; + } + else if( !Q_stricmp( arg1, "map" ) ) + { + if( g_mapvoteMaxTime.integer + && (( level.time - level.startTime ) >= g_mapvoteMaxTime.integer * 1000 ) + && !G_admin_permission( ent, ADMF_NO_VOTE_LIMIT ) + && (level.numPlayingClients > 0 && level.numConnectedClients>1) ) + { + trap_SendServerCommand( ent-g_entities, va( + "print \"You cannot call for a mapchange after %d seconds\n\"", + g_mapvoteMaxTime.integer ) ); + G_admin_adminlog_log( ent, "vote", NULL, 0, qfalse ); + return; + } + + if( !G_MapExists( arg2 ) ) + { + trap_SendServerCommand( ent - g_entities, va( "print \"callvote: " + "'maps/%s.bsp' could not be found on the server\n\"", arg2 ) ); + return; + } + + if( !G_admin_permission( ent, ADMF_NO_VOTE_LIMIT ) && !map_is_votable( arg2 ) ) + { + trap_SendServerCommand( ent - g_entities, va( "print \"callvote: " + "Only admins may call a vote for map: %s\n\"", arg2 ) ); + return; + } + + Com_sprintf( level.voteString, sizeof( level.voteString ), "%s %s", arg1, arg2 ); + Com_sprintf( level.voteDisplayString, + sizeof( level.voteDisplayString ), "Change to map '%s'", arg2 ); + level.votePassThreshold = g_mapVotesPercent.integer; + } + else if( !Q_stricmp( arg1, "nextmap" ) ) + { + if( G_MapExists( g_nextMap.string ) ) + { + trap_SendServerCommand( ent - g_entities, va( "print \"callvote: " + "the next map is already set to '%s^7'\n\"", g_nextMap.string ) ); + return; + } + + if( !arg2[ 0 ] ) + { + trap_SendServerCommand( ent-g_entities, + "print \"callvote: you must specify a map\n\"" ); + return; + } + + if( !G_MapExists( arg2 ) ) + { + trap_SendServerCommand( ent - g_entities, va( "print \"callvote: " + "'maps/%s^7.bsp' could not be found on the server\n\"", arg2 ) ); + return; + } + + if( !G_admin_permission( ent, ADMF_NO_VOTE_LIMIT ) && !map_is_votable( arg2 ) ) + { + trap_SendServerCommand( ent - g_entities, va( "print \"callvote: " + "Only admins may call a vote for map: %s\n\"", arg2 ) ); + return; + } + + Com_sprintf( level.voteString, sizeof( level.voteString ), + "set g_nextMap %s", arg2 ); + Com_sprintf( level.voteDisplayString, + sizeof( level.voteDisplayString ), "Set the next map to '%s^7'", arg2 ); + level.votePassThreshold = g_mapVotesPercent.integer; + } + else if( !Q_stricmp( arg1, "draw" ) ) + { + Com_sprintf( level.voteString, sizeof( level.voteString ), "evacuation" ); + Com_sprintf( level.voteDisplayString, sizeof( level.voteDisplayString ), + "End match in a draw" ); + level.votePassThreshold = g_mapVotesPercent.integer; + } + else if( !Q_stricmp( arg1, "poll" ) ) + { + if( arg2plus[ 0 ] == '\0' ) + { + trap_SendServerCommand( ent-g_entities, "print \"callvote: You forgot to specify what people should vote on.\n\"" ); + return; + } + Com_sprintf( level.voteString, sizeof( level.voteString ), nullstring); + Com_sprintf( level.voteDisplayString, + sizeof( level.voteDisplayString ), "[Poll] \'%s\'", arg2plus ); + } + else if( !Q_stricmp( arg1, "sudden_death" ) || + !Q_stricmp( arg1, "suddendeath" ) ) + { + if(!g_suddenDeathVotePercent.integer) + { + trap_SendServerCommand( ent-g_entities, "print \"Sudden Death votes have been disabled\n\"" ); + return; + } + else if( g_suddenDeath.integer ) + { + trap_SendServerCommand( ent - g_entities, va( "print \"callvote: Sudden Death has already begun\n\"") ); + return; + } + else if( G_TimeTilSuddenDeath() <= g_suddenDeathVoteDelay.integer * 1000 ) + { + trap_SendServerCommand( ent - g_entities, va( "print \"callvote: Sudden Death is already immenent\n\"") ); + return; + } + else + { + level.votePassThreshold = g_suddenDeathVotePercent.integer; + Com_sprintf( level.voteString, sizeof( level.voteString ), "suddendeath" ); + Com_sprintf( level.voteDisplayString, + sizeof( level.voteDisplayString ), "Begin sudden death" ); + + if( g_suddenDeathVoteDelay.integer ) + Q_strcat( level.voteDisplayString, sizeof( level.voteDisplayString ), va( " in %d seconds", g_suddenDeathVoteDelay.integer ) ); + + } + } + else if( !Q_stricmp( arg1, "extend" ) ) + { + if( !g_extendVotesPercent.integer ) + { + trap_SendServerCommand( ent-g_entities, "print \"Extend votes have been disabled\n\"" ); + return; + } + if( g_extendVotesCount.integer + && level.extend_vote_count >= g_extendVotesCount.integer ) + { + trap_SendServerCommand( ent-g_entities, + va( "print \"callvote: Maximum number of %d extend votes has been reached\n\"", + g_extendVotesCount.integer ) ); + return; + } + if( !g_timelimit.integer ) { + trap_SendServerCommand( ent-g_entities, + "print \"This match has no timelimit so extend votes wont work\n\"" ); + return; + } + if( level.time - level.startTime < + ( g_timelimit.integer - g_extendVotesTime.integer / 2 ) * 60000 ) + { + trap_SendServerCommand( ent-g_entities, + va( "print \"callvote: Extend votes only allowed with less than %d minutes remaining\n\"", + g_extendVotesTime.integer / 2 ) ); + return; + } + level.extend_vote_count++; + level.votePassThreshold = g_extendVotesPercent.integer; + Com_sprintf( level.voteString, sizeof( level.voteString ), + "timelimit %i", g_timelimit.integer + g_extendVotesTime.integer ); + Com_sprintf( level.voteDisplayString, sizeof( level.voteDisplayString ), + "Extend the timelimit by %d minutes", g_extendVotesTime.integer ); + } + else + { + qboolean match = qfalse; + char customVoteKeys[ MAX_STRING_CHARS ]; + + customVoteKeys[ 0 ] = '\0'; + if( g_customVotePercent.integer ) + { + char text[ MAX_STRING_CHARS ]; + char *votekey, *votemsg, *votecmd, *voteperc; + int votePValue; + + text[ 0 ] = '\0'; + for( i = 0; i < CUSTOM_VOTE_COUNT; i++ ) + { + switch( i ) + { + case 0: + Q_strncpyz( text, g_customVote1.string, sizeof( text ) ); + break; + case 1: + Q_strncpyz( text, g_customVote2.string, sizeof( text ) ); + break; + case 2: + Q_strncpyz( text, g_customVote3.string, sizeof( text ) ); + break; + case 3: + Q_strncpyz( text, g_customVote4.string, sizeof( text ) ); + break; + case 4: + Q_strncpyz( text, g_customVote5.string, sizeof( text ) ); + break; + case 5: + Q_strncpyz( text, g_customVote6.string, sizeof( text ) ); + break; + case 6: + Q_strncpyz( text, g_customVote7.string, sizeof( text ) ); + break; + case 7: + Q_strncpyz( text, g_customVote8.string, sizeof( text ) ); + break; + } + if ( text[ 0 ] == '\0' ) + continue; + + // custom vote cvar format: "callvote_name,Vote message string,vote success command[,percent]" + votekey = text; + votemsg = strchr( votekey, ',' ); + if( !votemsg || *votemsg != ',' ) + continue; + *votemsg = '\0'; + votemsg++; + Q_strcat( customVoteKeys, sizeof( customVoteKeys ), + va( "%s%s", ( customVoteKeys[ 0 ] == '\0' ) ? "" : ", ", votekey ) ); + votecmd = strchr( votemsg, ',' ); + if( !votecmd || *votecmd != ',' ) + continue; + *votecmd = '\0'; + votecmd++; + + voteperc = strchr( votecmd, ',' ); + if( !voteperc || *voteperc != ',' ) + votePValue = g_customVotePercent.integer; + else + { + *voteperc = '\0'; + voteperc++; + votePValue = atoi( voteperc ); + if( !votePValue ) + votePValue = g_customVotePercent.integer; + } + + if( Q_stricmp( arg1, votekey ) != 0 ) + continue; + if( votemsg[ 0 ] == '\0' || votecmd[ 0 ] == '\0' ) + continue; + + level.votePassThreshold = votePValue; + Com_sprintf( level.voteString, sizeof( level.voteString ), "%s", votecmd ); + Com_sprintf( level.voteDisplayString, sizeof( level.voteDisplayString ), "%s", votemsg ); + match = qtrue; + break; + } + } + + if( !match ) + { + trap_SendServerCommand( ent-g_entities, "print \"Invalid vote string\n\"" ); + trap_SendServerCommand( ent-g_entities, "print \"Valid vote commands are: " + "map, map_restart, draw, extend, nextmap, kick, spec, mute, unmute, poll, and sudden_death\n" ); + if( customVoteKeys[ 0 ] != '\0' ) + trap_SendServerCommand( ent-g_entities, + va( "print \"Additional custom vote commands: %s\n\"", customVoteKeys ) ); + return; + } + } + + if( level.votePassThreshold!=50 ) + { + Q_strcat( level.voteDisplayString, sizeof( level.voteDisplayString ), va( "^7 (Needs > %d percent)", level.votePassThreshold ) ); + } + + if ( reason[0]!='\0' ) + Q_strcat( level.voteDisplayString, sizeof( level.voteDisplayString ), va( " Reason: '%s^7'", reason ) ); + + G_admin_adminlog_log( ent, "vote", NULL, 0, qtrue ); + + trap_SendServerCommand( -1, va( "print \"%s" S_COLOR_WHITE + " called a vote: %s" S_COLOR_WHITE "\n\"", ent->client->pers.netname, level.voteDisplayString ) ); + + G_LogPrintf("Vote: %s^7 called a vote: %s^7\n", ent->client->pers.netname, level.voteDisplayString ); + + Q_strcat( level.voteDisplayString, sizeof( level.voteDisplayString ), va( " Called by: '%s^7'", ent->client->pers.netname ) ); + + ent->client->pers.voteCount++; + + // start the voting, the caller autoamtically votes yes + level.voteTime = level.time; + level.voteNo = 0; + + for( i = 0 ; i < level.maxclients ; i++ ) + level.clients[i].ps.eFlags &= ~EF_VOTED; + + if( !Q_stricmp( arg1, "poll" ) ) + { + level.voteYes = 0; + } + else + { + level.voteYes = 1; + ent->client->ps.eFlags |= EF_VOTED; + } + + trap_SetConfigstring( CS_VOTE_TIME, va( "%i", level.voteTime ) ); + trap_SetConfigstring( CS_VOTE_STRING, level.voteDisplayString ); + trap_SetConfigstring( CS_VOTE_YES, va( "%i", level.voteYes ) ); + trap_SetConfigstring( CS_VOTE_NO, va( "%i", level.voteNo ) ); +} + + +/* +================== +Cmd_Vote_f +================== +*/ +void Cmd_Vote_f( gentity_t *ent ) +{ + char msg[ 64 ]; + + if ( level.intermissiontime || level.paused ) + { + if( level.mapRotationVoteTime ) + { + trap_Argv( 1, msg, sizeof( msg ) ); + if( msg[ 0 ] == 'y' || msg[ 1 ] == 'Y' || msg[ 1 ] == '1' ) + G_IntermissionMapVoteCommand( ent, qfalse, qtrue ); + } + + return; + } + + if( !level.voteTime ) + { + if( ent->client->pers.teamSelection != PTE_NONE ) + { + // If there is a teamvote going on but no global vote, forward this vote on as a teamvote + // (ugly hack for 1.1 cgames + noobs who can't figure out how to use any command that isn't bound by default) + int cs_offset = 0; + if( ent->client->pers.teamSelection == PTE_ALIENS ) + cs_offset = 1; + + if( level.teamVoteTime[ cs_offset ] ) + { + if( !(ent->client->ps.eFlags & EF_TEAMVOTED ) ) + { + Cmd_TeamVote_f(ent); + return; + } + } + } + trap_SendServerCommand( ent-g_entities, "print \"No vote in progress\n\"" ); + return; + } + + if( ent->client->ps.eFlags & EF_VOTED ) + { + trap_SendServerCommand( ent-g_entities, "print \"Vote already cast\n\"" ); + return; + } + + trap_SendServerCommand( ent-g_entities, "print \"Vote cast\n\"" ); + + ent->client->ps.eFlags |= EF_VOTED; + + trap_Argv( 1, msg, sizeof( msg ) ); + + if( msg[ 0 ] == 'y' || msg[ 1 ] == 'Y' || msg[ 1 ] == '1' ) + { + level.voteYes++; + trap_SetConfigstring( CS_VOTE_YES, va( "%i", level.voteYes ) ); + } + else + { + level.voteNo++; + trap_SetConfigstring( CS_VOTE_NO, va( "%i", level.voteNo ) ); + } + + // a majority will be determined in G_CheckVote, which will also account + // for players entering or leaving +} + +/* +================== +Cmd_CallTeamVote_f +================== +*/ +void Cmd_CallTeamVote_f( gentity_t *ent ) +{ + int i, team, cs_offset = 0; + char arg1[ MAX_STRING_TOKENS ]; + char arg2[ MAX_STRING_TOKENS ]; + int clientNum = -1; + char name[ MAX_NETNAME ]; + char nullstring[] = ""; + char message[ MAX_STRING_CHARS ]; + char targetname[ MAX_NAME_LENGTH] = ""; + char reason[ MAX_STRING_CHARS ] = ""; + char *arg1plus; + char *arg2plus; + char *ptr = NULL; + int numVoters = 0; + + arg1plus = G_SayConcatArgs( 1 ); + arg2plus = G_SayConcatArgs( 2 ); + + team = ent->client->pers.teamSelection; + + if( team == PTE_ALIENS ) + cs_offset = 1; + + if(team==PTE_ALIENS) + numVoters = level.numAlienClients; + else if(team==PTE_HUMANS) + numVoters = level.numHumanClients; + + if( !g_allowVote.integer ) + { + trap_SendServerCommand( ent-g_entities, "print \"Voting not allowed here\n\"" ); + return; + } + + if( level.teamVoteTime[ cs_offset ] ) + { + trap_SendServerCommand( ent-g_entities, "print \"A team vote is already in progress\n\"" ); + return; + } + + //see if they can vote + if( G_admin_permission( ent, ADMF_NO_VOTE ) ) + { + trap_SendServerCommand( ent-g_entities, "print \"You have no voting rights\n\"" ); + return; + } + + if( g_voteLimit.integer > 0 + && ent->client->pers.voteCount >= g_voteLimit.integer + && !G_admin_permission( ent, ADMF_NO_VOTE_LIMIT ) ) + { + trap_SendServerCommand( ent-g_entities, va( + "print \"You have already called the maximum number of votes (%d)\n\"", + g_voteLimit.integer ) ); + return; + } + + if( G_IsMuted( ent->client ) ) + { + trap_SendServerCommand( ent - g_entities, + "print \"You are muted and cannot call teamvotes\n\"" ); + return; + } + + if( g_voteMinTime.integer + && ent->client->pers.firstConnect + && level.time - ent->client->pers.enterTime < g_voteMinTime.integer * 1000 + && !G_admin_permission( ent, ADMF_NO_VOTE_LIMIT ) + && (level.numPlayingClients > 0 && level.numConnectedClients>1) ) + { + trap_SendServerCommand( ent-g_entities, va( + "print \"You must wait %d seconds after connecting before calling a vote\n\"", + g_voteMinTime.integer ) ); + return; + } + + // make sure it is a valid command to vote on + trap_Argv( 1, arg1, sizeof( arg1 ) ); + trap_Argv( 2, arg2, sizeof( arg2 ) ); + + if( strchr( arg1plus, ';' ) ) + { + trap_SendServerCommand( ent-g_entities, "print \"Invalid team vote string\n\"" ); + return; + } + + ptr = strstr(arg1plus, " -"); + if( ptr ) + { + *ptr = '\0'; + ptr+=2; + + if( *ptr == 'r' || *ptr=='R' ) + { + ptr++; + while( *ptr == ' ' ) + ptr++; + strcpy(reason, ptr); + } + else + { + trap_SendServerCommand( ent-g_entities, "print \"callteamvote: Warning: invalid argument specified \n\"" ); + } + } + + // detect clientNum for partial name match votes + if( !Q_stricmp( arg1, "kick" ) || + !Q_stricmp( arg1, "denybuild" ) || + !Q_stricmp( arg1, "allowbuild" ) || + !Q_stricmp( arg1, "designate" ) || + !Q_stricmp( arg1, "undesignate" ) ) + { + int clientNums[ MAX_CLIENTS ] = { -1 }; + int numMatches=0; + char err[ MAX_STRING_CHARS ]; + + Q_strncpyz(targetname, arg2plus, sizeof(targetname)); + ptr = strstr(targetname, " -"); + if( ptr ) + *ptr = '\0'; + + if( g_requireVoteReasons.integer && !G_admin_permission( ent, ADMF_UNACCOUNTABLE ) && !Q_stricmp( arg1, "kick" ) && reason[ 0 ]=='\0' ) + { + trap_SendServerCommand( ent-g_entities, "print \"callvote: You must specify a reason. Use /callteamvote kick [player] -r [reason] \n\"" ); + return; + } + + + if( !arg2[ 0 ] ) + { + trap_SendServerCommand( ent-g_entities, + "print \"callteamvote: no target\n\"" ); + return; + } + + numMatches = G_ClientNumbersFromString( targetname, clientNums ); + if( numMatches == 1 ) + { + // there was only one partial name match + clientNum = clientNums[ 0 ]; + } + else + { + // look for an exact name match (sets clientNum to -1 if it fails) + clientNum = G_ClientNumberFromString( ent, targetname ); + } + + if( clientNum==-1 && numMatches > 1 ) + { + G_MatchOnePlayer( clientNums, err, sizeof( err ) ); + ADMP( va( "^3callteamvote: ^7%s\n", err ) ); + return; + } + + // make sure this player is on the same team + if( clientNum != -1 && level.clients[ clientNum ].pers.teamSelection != + team ) + { + clientNum = -1; + } + + if( clientNum != -1 && + level.clients[ clientNum ].pers.connected == CON_DISCONNECTED ) + { + clientNum = -1; + } + + if( clientNum != -1 ) + { + Q_strncpyz( name, level.clients[ clientNum ].pers.netname, + sizeof( name ) ); + Q_CleanStr( name ); + if( G_admin_permission( &g_entities[ clientNum ], ADMF_IMMUNITY ) ) + { + char reasonprint[ MAX_STRING_CHARS ] = ""; + if( reason[ 0 ] != '\0' ) + Com_sprintf(reasonprint, sizeof(reasonprint), "With reason: %s", reason); + + Com_sprintf( message, sizeof( message ), "%s^7 attempted /callteamvote %s %s on immune admin %s^7 %s^7", + ent->client->pers.netname, arg1, arg2, g_entities[ clientNum ].client->pers.netname, reasonprint ); + } + } + else + { + trap_SendServerCommand( ent-g_entities, + "print \"callteamvote: invalid player\n\"" ); + return; + } + } + + if( !Q_stricmp( arg1, "kick" ) ) + { + if( G_admin_permission( &g_entities[ clientNum ], ADMF_IMMUNITY ) ) + { + trap_SendServerCommand( ent-g_entities, + "print \"callteamvote: admin is immune from vote kick\n\"" ); + G_AdminsPrintf("%s\n",message); + G_admin_adminlog_log( ent, "teamvote", NULL, 0, qfalse ); + return; + } + + + // use ip in case this player disconnects before the vote ends + Com_sprintf( level.teamVoteString[ cs_offset ], + sizeof( level.teamVoteString[ cs_offset ] ), + "!ban %s \"%s\" team vote kick", level.clients[ clientNum ].pers.ip, + g_adminTempBan.string ); + Com_sprintf( level.teamVoteDisplayString[ cs_offset ], + sizeof( level.teamVoteDisplayString[ cs_offset ] ), + "Kick player '%s'", name ); + } + else if( !Q_stricmp( arg1, "denybuild" ) ) + { + if( level.clients[ clientNum ].pers.denyBuild ) + { + trap_SendServerCommand( ent-g_entities, + "print \"callteamvote: player already lost building rights\n\"" ); + return; + } + + if( G_admin_permission( &g_entities[ clientNum ], ADMF_IMMUNITY ) ) + { + trap_SendServerCommand( ent-g_entities, + "print \"callteamvote: admin is immune from denybuild\n\"" ); + G_AdminsPrintf("%s\n",message); + G_admin_adminlog_log( ent, "teamvote", NULL, 0, qfalse ); + return; + } + + Com_sprintf( level.teamVoteString[ cs_offset ], + sizeof( level.teamVoteString[ cs_offset ] ), "!denybuild %i", clientNum ); + Com_sprintf( level.teamVoteDisplayString[ cs_offset ], + sizeof( level.teamVoteDisplayString[ cs_offset ] ), + "Take away building rights from '%s'", name ); + } + else if( !Q_stricmp( arg1, "allowbuild" ) ) + { + if( !level.clients[ clientNum ].pers.denyBuild ) + { + trap_SendServerCommand( ent-g_entities, + "print \"callteamvote: player already has building rights\n\"" ); + return; + } + + Com_sprintf( level.teamVoteString[ cs_offset ], + sizeof( level.teamVoteString[ cs_offset ] ), "!allowbuild %i", clientNum ); + Com_sprintf( level.teamVoteDisplayString[ cs_offset ], + sizeof( level.teamVoteDisplayString[ cs_offset ] ), + "Allow '%s' to build", name ); + } + else if( !Q_stricmp( arg1, "designate" ) ) + { + if( !g_designateVotes.integer ) + { + trap_SendServerCommand( ent-g_entities, + "print \"callteamvote: Designate votes have been disabled.\n\"" ); + return; + } + + if( level.clients[ clientNum ].pers.designatedBuilder ) + { + trap_SendServerCommand( ent-g_entities, + "print \"callteamvote: player is already a designated builder\n\"" ); + return; + } + Com_sprintf( level.teamVoteString[ cs_offset ], + sizeof( level.teamVoteString[ cs_offset ] ), "!designate %i", clientNum ); + Com_sprintf( level.teamVoteDisplayString[ cs_offset ], + sizeof( level.teamVoteDisplayString[ cs_offset ] ), + "Make '%s' a designated builder", name ); + } + else if( !Q_stricmp( arg1, "undesignate" ) ) + { + + if( !g_designateVotes.integer ) + { + trap_SendServerCommand( ent-g_entities, + "print \"callteamvote: Designate votes have been disabled.\n\"" ); + return; + } + + if( !level.clients[ clientNum ].pers.designatedBuilder ) + { + trap_SendServerCommand( ent-g_entities, + "print \"callteamvote: player is not currently a designated builder\n\"" ); + return; + } + Com_sprintf( level.teamVoteString[ cs_offset ], + sizeof( level.teamVoteString[ cs_offset ] ), "!undesignate %i", clientNum ); + Com_sprintf( level.teamVoteDisplayString[ cs_offset ], + sizeof( level.teamVoteDisplayString[ cs_offset ] ), + "Remove designated builder status from '%s'", name ); + } + else if( !Q_stricmp( arg1, "admitdefeat" ) ) + { + if( numVoters <=1 ) + { + trap_SendServerCommand( ent-g_entities, + "print \"callteamvote: You cannot admitdefeat by yourself. Use /callvote draw.\n\"" ); + return; + } + + Com_sprintf( level.teamVoteString[ cs_offset ], + sizeof( level.teamVoteString[ cs_offset ] ), "admitdefeat %i", team ); + Com_sprintf( level.teamVoteDisplayString[ cs_offset ], + sizeof( level.teamVoteDisplayString[ cs_offset ] ), + "Admit Defeat" ); + } + else if( !Q_stricmp( arg1, "poll" ) ) + { + if( arg2plus[ 0 ] == '\0' ) + { + trap_SendServerCommand( ent-g_entities, "print \"callteamvote: You forgot to specify what people should vote on.\n\"" ); + return; + } + Com_sprintf( level.teamVoteString[ cs_offset ], sizeof( level.teamVoteString[ cs_offset ] ), nullstring ); + Com_sprintf( level.teamVoteDisplayString[ cs_offset ], + sizeof( level.voteDisplayString ), "[Poll] \'%s\'", arg2plus ); + } + else + { + trap_SendServerCommand( ent-g_entities, "print \"Invalid vote string\n\"" ); + trap_SendServerCommand( ent-g_entities, + "print \"Valid team vote commands are: " + "kick, denybuild, allowbuild, poll, designate, undesignate, and admitdefeat\n\"" ); + return; + } + ent->client->pers.voteCount++; + + G_admin_adminlog_log( ent, "teamvote", arg1, 0, qtrue ); + + if ( reason[0]!='\0' ) + Q_strcat( level.teamVoteDisplayString[ cs_offset ], sizeof( level.teamVoteDisplayString[ cs_offset ] ), va( " Reason: '%s'^7", reason ) ); + + for( i = 0 ; i < level.maxclients ; i++ ) + { + if( level.clients[ i ].pers.connected == CON_DISCONNECTED ) + continue; + + if( level.clients[ i ].ps.stats[ STAT_PTEAM ] == team ) + { + trap_SendServerCommand( i, va("print \"%s " S_COLOR_WHITE + "called a team vote: %s^7 \n\"", ent->client->pers.netname, level.teamVoteDisplayString[ cs_offset ] ) ); + } + else if( G_admin_permission( &g_entities[ i ], ADMF_ADMINCHAT ) && + ( ( !Q_stricmp( arg1, "kick" ) || !Q_stricmp( arg1, "denybuild" ) ) || + level.clients[ i ].pers.teamSelection == PTE_NONE ) ) + { + trap_SendServerCommand( i, va("print \"^6[Admins]^7 %s " S_COLOR_WHITE + "called a team vote: %s^7 \n\"", ent->client->pers.netname, level.teamVoteDisplayString[ cs_offset ] ) ); + } + } + + if(team==PTE_ALIENS) + G_LogPrintf("Teamvote: %s^7 called a teamvote (aliens): %s^7\n", ent->client->pers.netname, level.teamVoteDisplayString[ cs_offset ] ); + else if(team==PTE_HUMANS) + G_LogPrintf("Teamvote: %s^7 called a teamvote (humans): %s^7\n", ent->client->pers.netname, level.teamVoteDisplayString[ cs_offset ] ); + + Q_strcat( level.teamVoteDisplayString[ cs_offset ], sizeof( level.teamVoteDisplayString[ cs_offset ] ), va( " Called by: '%s^7'", ent->client->pers.netname ) ); + + // start the voting, the caller autoamtically votes yes + level.teamVoteTime[ cs_offset ] = level.time; + level.teamVoteNo[ cs_offset ] = 0; + + for( i = 0 ; i < level.maxclients ; i++ ) + { + if( level.clients[ i ].ps.stats[ STAT_PTEAM ] == team ) + level.clients[ i ].ps.eFlags &= ~EF_TEAMVOTED; + } + + if( !Q_stricmp( arg1, "poll" ) ) + { + level.teamVoteYes[ cs_offset ] = 0; + } + else + { + level.teamVoteYes[ cs_offset ] = 1; + ent->client->ps.eFlags |= EF_TEAMVOTED; + } + + trap_SetConfigstring( CS_TEAMVOTE_TIME + cs_offset, va( "%i", level.teamVoteTime[ cs_offset ] ) ); + trap_SetConfigstring( CS_TEAMVOTE_STRING + cs_offset, level.teamVoteDisplayString[ cs_offset ] ); + trap_SetConfigstring( CS_TEAMVOTE_YES + cs_offset, va( "%i", level.teamVoteYes[ cs_offset ] ) ); + trap_SetConfigstring( CS_TEAMVOTE_NO + cs_offset, va( "%i", level.teamVoteNo[ cs_offset ] ) ); +} + + +/* +================== +Cmd_TeamVote_f +================== +*/ +void Cmd_TeamVote_f( gentity_t *ent ) +{ + int cs_offset = 0; + char msg[ 64 ]; + + if( ent->client->pers.teamSelection == PTE_ALIENS ) + cs_offset = 1; + + if( !level.teamVoteTime[ cs_offset ] ) + { + trap_SendServerCommand( ent-g_entities, "print \"No team vote in progress\n\"" ); + return; + } + + if( ent->client->ps.eFlags & EF_TEAMVOTED ) + { + trap_SendServerCommand( ent-g_entities, "print \"Team vote already cast\n\"" ); + return; + } + + trap_SendServerCommand( ent-g_entities, "print \"Team vote cast\n\"" ); + + ent->client->ps.eFlags |= EF_TEAMVOTED; + + trap_Argv( 1, msg, sizeof( msg ) ); + + if( msg[ 0 ] == 'y' || msg[ 1 ] == 'Y' || msg[ 1 ] == '1' ) + { + level.teamVoteYes[ cs_offset ]++; + trap_SetConfigstring( CS_TEAMVOTE_YES + cs_offset, va( "%i", level.teamVoteYes[ cs_offset ] ) ); + } + else + { + level.teamVoteNo[ cs_offset ]++; + trap_SetConfigstring( CS_TEAMVOTE_NO + cs_offset, va( "%i", level.teamVoteNo[ cs_offset ] ) ); + } + + // a majority will be determined in TeamCheckVote, which will also account + // for players entering or leaving +} + + +/* +================= +Cmd_SetViewpos_f +================= +*/ +void Cmd_SetViewpos_f( gentity_t *ent ) +{ + vec3_t origin, angles; + char buffer[ MAX_TOKEN_CHARS ]; + int i; + + if( trap_Argc( ) != 5 ) + { + trap_SendServerCommand( ent-g_entities, va( "print \"usage: setviewpos x y z yaw\n\"" ) ); + return; + } + + VectorClear( angles ); + + for( i = 0 ; i < 3 ; i++ ) + { + trap_Argv( i + 1, buffer, sizeof( buffer ) ); + origin[ i ] = atof( buffer ); + } + + trap_Argv( 4, buffer, sizeof( buffer ) ); + angles[ YAW ] = atof( buffer ); + + TeleportPlayer( ent, origin, angles ); +} + +#define AS_OVER_RT3 ((ALIENSENSE_RANGE*0.5f)/M_ROOT3) + +qboolean G_RoomForClassChange( gentity_t *ent, pClass_t class, vec3_t newOrigin ) +{ + vec3_t fromMins, fromMaxs; + vec3_t toMins, toMaxs; + vec3_t temp; + trace_t tr; + float nudgeHeight; + float maxHorizGrowth; + pClass_t oldClass = ent->client->ps.stats[ STAT_PCLASS ]; + + BG_FindBBoxForClass( oldClass, fromMins, fromMaxs, NULL, NULL, NULL ); + BG_FindBBoxForClass( class, toMins, toMaxs, NULL, NULL, NULL ); + + VectorCopy( ent->s.origin, newOrigin ); + + // find max x/y diff + maxHorizGrowth = toMaxs[ 0 ] - fromMaxs[ 0 ]; + if( toMaxs[ 1 ] - fromMaxs[ 1 ] > maxHorizGrowth ) + maxHorizGrowth = toMaxs[ 1 ] - fromMaxs[ 1 ]; + if( toMins[ 0 ] - fromMins[ 0 ] > -maxHorizGrowth ) + maxHorizGrowth = -( toMins[ 0 ] - fromMins[ 0 ] ); + if( toMins[ 1 ] - fromMins[ 1 ] > -maxHorizGrowth ) + maxHorizGrowth = -( toMins[ 1 ] - fromMins[ 1 ] ); + + if( maxHorizGrowth > 0.0f ) + { + // test by moving the player up the max required on a 60 degree slope + nudgeHeight = maxHorizGrowth * 2.0f; + } + else + { + // player is shrinking, so there's no need to nudge them upwards + nudgeHeight = 0.0f; + } + + // find what the new origin would be on a level surface + newOrigin[ 2 ] += fabs( toMins[ 2 ] ) - fabs( fromMins[ 2 ] ); + + //compute a place up in the air to start the real trace + VectorCopy( newOrigin, temp ); + temp[ 2 ] += nudgeHeight; + trap_Trace( &tr, newOrigin, toMins, toMaxs, temp, ent->s.number, MASK_PLAYERSOLID ); + + //trace down to the ground so that we can evolve on slopes + VectorCopy( newOrigin, temp ); + temp[ 2 ] += ( nudgeHeight * tr.fraction ); + trap_Trace( &tr, temp, toMins, toMaxs, newOrigin, ent->s.number, MASK_PLAYERSOLID ); + VectorCopy( tr.endpos, newOrigin ); + + //make REALLY sure + trap_Trace( &tr, newOrigin, toMins, toMaxs, newOrigin, + ent->s.number, MASK_PLAYERSOLID ); + + //check there is room to evolve + if( !tr.startsolid && tr.fraction == 1.0f ) + return qtrue; + else + return qfalse; +} + +/* +================= +Cmd_Class_f +================= +*/ +void Cmd_Class_f( gentity_t *ent ) +{ + char s[ MAX_TOKEN_CHARS ]; + int clientNum; + int i; + vec3_t infestOrigin; + pClass_t currentClass = ent->client->pers.classSelection; + pClass_t newClass; + int numLevels; + int entityList[ MAX_GENTITIES ]; + vec3_t range = { AS_OVER_RT3, AS_OVER_RT3, AS_OVER_RT3 }; + vec3_t mins, maxs; + int num; + gentity_t *other; + qboolean humanNear = qfalse; + + + clientNum = ent->client - level.clients; + trap_Argv( 1, s, sizeof( s ) ); + newClass = BG_FindClassNumForName( s ); + + if( ent->client->sess.sessionTeam == TEAM_SPECTATOR ) + { + if( ent->client->sess.spectatorState == SPECTATOR_FOLLOW ) + G_StopFollowing( ent ); + + if( ent->client->pers.teamSelection == PTE_ALIENS ) + { + if( newClass != PCL_ALIEN_BUILDER0 && + newClass != PCL_ALIEN_BUILDER0_UPG && + newClass != PCL_ALIEN_LEVEL0 ) + { + trap_SendServerCommand( ent-g_entities, + va( "print \"You cannot spawn with class %s\n\"", s ) ); + return; + } + + if( !BG_ClassIsAllowed( newClass ) ) + { + trap_SendServerCommand( ent-g_entities, + va( "print \"Class %s is not allowed\n\"", s ) ); + return; + } + + if( !BG_FindStagesForClass( newClass, g_alienStage.integer ) ) + { + trap_SendServerCommand( ent-g_entities, + va( "print \"Class %s not allowed at stage %d\n\"", + s, g_alienStage.integer ) ); + return; + } + + if( ent->client->pers.denyBuild && ( newClass==PCL_ALIEN_BUILDER0 || newClass==PCL_ALIEN_BUILDER0_UPG ) ) + { + trap_SendServerCommand( ent-g_entities, "print \"Your building rights have been revoked\n\"" ); + return; + } + + // spawn from an egg + if( G_PushSpawnQueue( &level.alienSpawnQueue, clientNum ) ) + { + ent->client->pers.classSelection = newClass; + ent->client->ps.stats[ STAT_PCLASS ] = newClass; + } + } + else if( ent->client->pers.teamSelection == PTE_HUMANS ) + { + //set the item to spawn with + if( !Q_stricmp( s, BG_FindNameForWeapon( WP_MACHINEGUN ) ) && + BG_WeaponIsAllowed( WP_MACHINEGUN ) ) + { + ent->client->pers.humanItemSelection = WP_MACHINEGUN; + } + else if( !Q_stricmp( s, BG_FindNameForWeapon( WP_HBUILD ) ) && + BG_WeaponIsAllowed( WP_HBUILD ) ) + { + ent->client->pers.humanItemSelection = WP_HBUILD; + } + else if( !Q_stricmp( s, BG_FindNameForWeapon( WP_HBUILD2 ) ) && + BG_WeaponIsAllowed( WP_HBUILD2 ) && + BG_FindStagesForWeapon( WP_HBUILD2, g_humanStage.integer ) ) + { + ent->client->pers.humanItemSelection = WP_HBUILD2; + } + else + { + trap_SendServerCommand( ent-g_entities, + "print \"Unknown starting item\n\"" ); + return; + } + // spawn from a telenode + G_LogOnlyPrintf("ClientTeamClass: %i human %s\n", clientNum, s); + if( G_PushSpawnQueue( &level.humanSpawnQueue, clientNum ) ) + { + ent->client->pers.classSelection = PCL_HUMAN; + ent->client->ps.stats[ STAT_PCLASS ] = PCL_HUMAN; + } + } + return; + } + + if( ent->health <= 0 ) + return; + + if( ent->client->pers.teamSelection == PTE_ALIENS && + !( ent->client->ps.stats[ STAT_STATE ] & SS_INFESTING ) && + !( ent->client->ps.stats[ STAT_STATE ] & SS_HOVELING ) ) + { + if( newClass == PCL_NONE ) + { + trap_SendServerCommand( ent-g_entities, "print \"Unknown class\n\"" ); + return; + } + + //if we are not currently spectating, we are attempting evolution + if( ent->client->pers.classSelection != PCL_NONE ) + { + if( ( ent->client->ps.stats[ STAT_STATE ] & SS_WALLCLIMBING ) || + ( ent->client->ps.stats[ STAT_STATE ] & SS_WALLCLIMBINGCEILING ) ) + { + trap_SendServerCommand( ent-g_entities, + "print \"You cannot evolve while wallwalking\n\"" ); + return; + } + + //check there are no humans nearby + VectorAdd( ent->client->ps.origin, range, maxs ); + VectorSubtract( ent->client->ps.origin, range, mins ); + + num = trap_EntitiesInBox( mins, maxs, entityList, MAX_GENTITIES ); + for( i = 0; i < num; i++ ) + { + other = &g_entities[ entityList[ i ] ]; + + if( ( other->client && other->client->ps.stats[ STAT_PTEAM ] == PTE_HUMANS ) || + ( other->s.eType == ET_BUILDABLE && other->biteam == BIT_HUMANS ) ) + { + humanNear = qtrue; + } + //If its the OM, then ignore all humans. + if(other->s.eType == ET_BUILDABLE && other->s.modelindex == BA_A_OVERMIND) + { + humanNear = qfalse; + break; + } + } + + if(humanNear == qtrue) { + G_TriggerMenu( clientNum, MN_A_TOOCLOSE ); + return; + } + + if( !level.overmindPresent ) + { + G_TriggerMenu( clientNum, MN_A_NOOVMND_EVOLVE ); + return; + } + + // denyweapons + if( newClass >= PCL_ALIEN_LEVEL1 && newClass <= PCL_ALIEN_LEVEL4 && + ent->client->pers.denyAlienClasses & ( 1 << newClass ) ) + { + trap_SendServerCommand( ent-g_entities, va( "print \"You are denied from using this class\n\"" ) ); + return; + } + + //guard against selling the HBUILD weapons exploit + if( ent->client->sess.sessionTeam != TEAM_SPECTATOR && + ( currentClass == PCL_ALIEN_BUILDER0 || + currentClass == PCL_ALIEN_BUILDER0_UPG ) && + ent->client->ps.stats[ STAT_MISC ] > 0 ) + { + trap_SendServerCommand( ent-g_entities, + va( "print \"You cannot evolve until build timer expires\n\"" ) ); + return; + } + + numLevels = BG_ClassCanEvolveFromTo( currentClass, + newClass, + (short)ent->client->ps.persistant[ PERS_CREDIT ], 0 ); + + if( G_RoomForClassChange( ent, newClass, infestOrigin ) ) + { + //...check we can evolve to that class + if( numLevels >= 0 && + BG_FindStagesForClass( newClass, g_alienStage.integer ) && + BG_ClassIsAllowed( newClass ) ) + { + G_LogOnlyPrintf("ClientTeamClass: %i alien %s\n", clientNum, s); + + ent->client->pers.evolveHealthFraction = (float)ent->client->ps.stats[ STAT_HEALTH ] / + (float)BG_FindHealthForClass( currentClass ); + + if( ent->client->pers.evolveHealthFraction < 0.0f ) + ent->client->pers.evolveHealthFraction = 0.0f; + else if( ent->client->pers.evolveHealthFraction > 1.0f ) + ent->client->pers.evolveHealthFraction = 1.0f; + + //remove credit + G_AddCreditToClient( ent->client, -(short)numLevels, qtrue ); + ent->client->pers.classSelection = newClass; + ClientUserinfoChanged( clientNum, qfalse ); + VectorCopy( infestOrigin, ent->s.pos.trBase ); + ClientSpawn( ent, ent, ent->s.pos.trBase, ent->s.apos.trBase ); + return; + } + else + { + trap_SendServerCommand( ent-g_entities, + "print \"You cannot evolve from your current class\n\"" ); + return; + } + } + else + { + G_TriggerMenu( clientNum, MN_A_NOEROOM ); + return; + } + } + else if( ent->client->pers.teamSelection == PTE_HUMANS ) + { + //humans cannot use this command whilst alive + if( ent->client->pers.classSelection != PCL_NONE ) + { + trap_SendServerCommand( ent-g_entities, va( "print \"You must be dead to use the class command\n\"" ) ); + return; + } + + ent->client->pers.classSelection = + ent->client->ps.stats[ STAT_PCLASS ] = PCL_HUMAN; + + //set the item to spawn with + if( !Q_stricmp( s, BG_FindNameForWeapon( WP_MACHINEGUN ) ) && BG_WeaponIsAllowed( WP_MACHINEGUN ) ) + ent->client->pers.humanItemSelection = WP_MACHINEGUN; + else if( !Q_stricmp( s, BG_FindNameForWeapon( WP_HBUILD ) ) && BG_WeaponIsAllowed( WP_HBUILD ) ) + ent->client->pers.humanItemSelection = WP_HBUILD; + else if( !Q_stricmp( s, BG_FindNameForWeapon( WP_HBUILD2 ) ) && BG_WeaponIsAllowed( WP_HBUILD2 ) && + BG_FindStagesForWeapon( WP_HBUILD2, g_humanStage.integer ) ) + ent->client->pers.humanItemSelection = WP_HBUILD2; + else + { + ent->client->pers.classSelection = PCL_NONE; + trap_SendServerCommand( ent-g_entities, va( "print \"Unknown starting item\n\"" ) ); + return; + } + + G_LogOnlyPrintf("ClientTeamClass: %i human %s\n", clientNum, s); + + G_PushSpawnQueue( &level.humanSpawnQueue, clientNum ); + } + } +} + +/* +================= +DBCommand + +Send command to all designated builders of selected team +================= +*/ +void DBCommand( pTeam_t team, const char *text ) +{ + int i; + gentity_t *ent; + + for( i = 0, ent = g_entities + i; i < level.maxclients; i++, ent++ ) + { + if( !ent->client || ( ent->client->pers.connected != CON_CONNECTED ) || + ( ent->client->pers.teamSelection != team ) || + !ent->client->pers.designatedBuilder ) + continue; + + trap_SendServerCommand( i, text ); + } +} + +/* +================= +Cmd_Destroy_f +================= +*/ +void Cmd_Destroy_f( gentity_t *ent ) +{ + vec3_t forward, end; + trace_t tr; + gentity_t *traceEnt; + char cmd[ 12 ]; + qboolean deconstruct = qtrue; + + if( ent->client->pers.denyBuild ) + { + trap_SendServerCommand( ent-g_entities, + "print \"Your building rights have been revoked\n\"" ); + return; + } + + trap_Argv( 0, cmd, sizeof( cmd ) ); + if( Q_stricmp( cmd, "destroy" ) == 0 ) + deconstruct = qfalse; + + if( ent->client->ps.stats[ STAT_STATE ] & SS_HOVELING ) + { + if( ( ent->client->hovel->s.eFlags & EF_DBUILDER ) && + !ent->client->pers.designatedBuilder ) + { + trap_SendServerCommand( ent-g_entities, + "print \"This structure is protected by designated builder\n\"" ); + DBCommand( ent->client->pers.teamSelection, + va( "print \"%s^3 has attempted to decon a protected structure!\n\"", + ent->client->pers.netname ) ); + return; + } + G_Damage( ent->client->hovel, ent, ent, forward, ent->s.origin, + 10000, 0, MOD_SUICIDE ); + } + + if( !( ent->client->ps.stats[ STAT_STATE ] & SS_INFESTING ) ) + { + AngleVectors( ent->client->ps.viewangles, forward, NULL, NULL ); + VectorMA( ent->client->ps.origin, 100, forward, end ); + + trap_Trace( &tr, ent->client->ps.origin, NULL, NULL, end, ent->s.number, MASK_PLAYERSOLID ); + traceEnt = &g_entities[ tr.entityNum ]; + + if( tr.fraction < 1.0f && + ( traceEnt->s.eType == ET_BUILDABLE ) && + ( traceEnt->biteam == ent->client->pers.teamSelection ) && + ( ( ent->client->ps.weapon >= WP_ABUILD ) && + ( ent->client->ps.weapon <= WP_HBUILD ) ) ) + { + // Cancel deconstruction + if( g_markDeconstruct.integer == 1 && traceEnt->deconstruct ) + { + traceEnt->deconstruct = qfalse; + return; + } + if( ( traceEnt->s.eFlags & EF_DBUILDER ) && + !ent->client->pers.designatedBuilder ) + { + trap_SendServerCommand( ent-g_entities, + "print \"This structure is protected by designated builder\n\"" ); + DBCommand( ent->client->pers.teamSelection, + va( "print \"%s^3 has attempted to decon a protected structure!\n\"", + ent->client->pers.netname ) ); + return; + } + + // Check the minimum level to deconstruct + if ( G_admin_level( ent ) < g_minDeconLevel.integer && !ent->client->pers.designatedBuilder ) + { + trap_SendServerCommand( ent-g_entities, + "print \"You do not have deconstructuction rights.\n\"" ); + return; + } + + // Prevent destruction of the last spawn + if( g_markDeconstruct.integer != 1 && !g_cheats.integer ) + { + if( ent->client->pers.teamSelection == PTE_ALIENS && + traceEnt->s.modelindex == BA_A_SPAWN ) + { + if( level.numAlienSpawns <= 1 ) + return; + } + else if( ent->client->pers.teamSelection == PTE_HUMANS && + traceEnt->s.modelindex == BA_H_SPAWN ) + { + if( level.numHumanSpawns <= 1 ) + return; + } + } + + // Don't allow destruction of hovel with granger inside + if( traceEnt->s.modelindex == BA_A_HOVEL && traceEnt->active ) + return; + + // Don't allow destruction of buildables that cannot be rebuilt + if(g_suddenDeath.integer && traceEnt->health > 0 && + ( ( g_suddenDeathMode.integer == SDMODE_SELECTIVE && + !BG_FindReplaceableTestForBuildable( traceEnt->s.modelindex ) ) || + ( g_suddenDeathMode.integer == SDMODE_BP && + BG_FindBuildPointsForBuildable( traceEnt->s.modelindex ) ) || + g_suddenDeathMode.integer == SDMODE_NO_BUILD || + g_suddenDeathMode.integer == SDMODE_NO_DECON ) ) + { + trap_SendServerCommand( ent-g_entities, + "print \"During Sudden Death you can only decon buildings that " + "can be rebuilt\n\"" ); + return; + } + + if( ent->client->ps.stats[ STAT_MISC ] > 0 ) + { + G_AddEvent( ent, EV_BUILD_DELAY, ent->client->ps.clientNum ); + return; + } + + if( traceEnt->health > 0 || g_deconDead.integer ) + { + if( g_markDeconstruct.integer == 1 ) + { + traceEnt->deconstruct = qtrue; // Mark buildable for deconstruction + traceEnt->deconstructTime = level.time; + } + else + { + if( traceEnt->health > 0 ) + { + buildHistory_t *new; + + new = G_Alloc( sizeof( buildHistory_t ) ); + new->ID = ( ++level.lastBuildID > 1000 ) + ? ( level.lastBuildID = 1 ) : level.lastBuildID; + new->ent = ent; + new->name[ 0 ] = 0; + new->buildable = traceEnt->s.modelindex; + VectorCopy( traceEnt->s.pos.trBase, new->origin ); + VectorCopy( traceEnt->s.angles, new->angles ); + VectorCopy( traceEnt->s.origin2, new->origin2 ); + VectorCopy( traceEnt->s.angles2, new->angles2 ); + new->fate = BF_DECONNED; + new->next = NULL; + new->marked = NULL; + G_LogBuild( new ); + + G_TeamCommand( ent->client->pers.teamSelection, + va( "print \"%s ^3DECONSTRUCTED^7 by %s^7\n\"", + BG_FindHumanNameForBuildable( traceEnt->s.modelindex ), + ent->client->pers.netname ) ); + + G_LogPrintf( "Decon: %i %i 0: %s^7 deconstructed %s\n", + ent->client->ps.clientNum, + traceEnt->s.modelindex, + ent->client->pers.netname, + BG_FindNameForBuildable( traceEnt->s.modelindex ) ); + } + + if( !deconstruct ) + G_Damage( traceEnt, ent, ent, forward, tr.endpos, 10000, 0, MOD_SUICIDE ); + else + G_FreeEntity( traceEnt ); + + if( !g_cheats.integer ) + ent->client->ps.stats[ STAT_MISC ] += + BG_FindBuildDelayForWeapon( ent->s.weapon ) >> 2; + } + } + } + } +} + +void Cmd_Mark_f( gentity_t *ent ) +{ + vec3_t forward, end; + trace_t tr; + gentity_t *traceEnt; + + if( g_markDeconstruct.integer != 2 ) + { + trap_SendServerCommand( ent-g_entities, + "print \"Mark is disabled\n\"" ); + return; + } + + if( ent->client->pers.denyBuild ) + { + trap_SendServerCommand( ent-g_entities, + "print \"Your building rights have been revoked\n\"" ); + return; + } + + // Check the minimum level to deconstruct + if ( G_admin_level( ent ) < g_minDeconLevel.integer && !ent->client->pers.designatedBuilder && g_minDeconAffectsMark.integer > 0 ) + { + trap_SendServerCommand( ent-g_entities, + "print \"You do not have deconstructuction rights.\n\"" ); + return; + } + + // can't mark when in hovel + if( ent->client->ps.stats[ STAT_STATE ] & SS_HOVELING ) + return; + + if( !( ent->client->ps.stats[ STAT_STATE ] & SS_INFESTING ) ) + { + AngleVectors( ent->client->ps.viewangles, forward, NULL, NULL ); + VectorMA( ent->client->ps.origin, 100, forward, end ); + + trap_Trace( &tr, ent->client->ps.origin, NULL, NULL, end, ent->s.number, MASK_PLAYERSOLID ); + traceEnt = &g_entities[ tr.entityNum ]; + + if( tr.fraction < 1.0f && + ( traceEnt->s.eType == ET_BUILDABLE ) && + ( traceEnt->biteam == ent->client->pers.teamSelection ) && + ( ( ent->client->ps.weapon >= WP_ABUILD ) && + ( ent->client->ps.weapon <= WP_HBUILD ) ) ) + { + if( ( traceEnt->s.eFlags & EF_DBUILDER ) && + !ent->client->pers.designatedBuilder ) + { + trap_SendServerCommand( ent-g_entities, + "print \"this structure is protected by a designated builder\n\"" ); + return; + } + + // Cancel deconstruction + if( traceEnt->deconstruct ) + { + traceEnt->deconstruct = qfalse; + + trap_SendServerCommand( ent-g_entities, + va( "print \"%s no longer marked for deconstruction\n\"", + BG_FindHumanNameForBuildable( traceEnt->s.modelindex ) ) ); + return; + } + + // Don't allow marking of buildables that cannot be rebuilt + if(g_suddenDeath.integer && traceEnt->health > 0 && + ( ( g_suddenDeathMode.integer == SDMODE_SELECTIVE && + !BG_FindReplaceableTestForBuildable( traceEnt->s.modelindex ) ) || + ( g_suddenDeathMode.integer == SDMODE_BP && + BG_FindBuildPointsForBuildable( traceEnt->s.modelindex ) ) || + g_suddenDeathMode.integer == SDMODE_NO_BUILD || + g_suddenDeathMode.integer == SDMODE_NO_DECON ) ) + { + trap_SendServerCommand( ent-g_entities, + "print \"During Sudden Death you can only mark buildings that " + "can be rebuilt\n\"" ); + return; + } + + if( traceEnt->health > 0 ) + { + traceEnt->deconstruct = qtrue; // Mark buildable for deconstruction + traceEnt->deconstructTime = level.time; + + trap_SendServerCommand( ent-g_entities, + va( "print \"%s marked for deconstruction\n\"", + BG_FindHumanNameForBuildable( traceEnt->s.modelindex ) ) ); + } + } + } +} + + +/* +================= +Cmd_ActivateItem_f + +Activate an item +================= +*/ +void Cmd_ActivateItem_f( gentity_t *ent ) +{ + char s[ MAX_TOKEN_CHARS ]; + int upgrade, weapon; + + trap_Argv( 1, s, sizeof( s ) ); + upgrade = BG_FindUpgradeNumForName( s ); + weapon = BG_FindWeaponNumForName( s ); + + if( upgrade != UP_NONE && BG_InventoryContainsUpgrade( upgrade, ent->client->ps.stats ) ) + BG_ActivateUpgrade( upgrade, ent->client->ps.stats ); + else if( weapon != WP_NONE && BG_InventoryContainsWeapon( weapon, ent->client->ps.stats ) ) + G_ForceWeaponChange( ent, weapon ); + else + trap_SendServerCommand( ent-g_entities, va( "print \"You don't have the %s\n\"", s ) ); +} + + +/* +================= +Cmd_DeActivateItem_f + +Deactivate an item +================= +*/ +void Cmd_DeActivateItem_f( gentity_t *ent ) +{ + char s[ MAX_TOKEN_CHARS ]; + int upgrade; + + trap_Argv( 1, s, sizeof( s ) ); + upgrade = BG_FindUpgradeNumForName( s ); + + if( BG_InventoryContainsUpgrade( upgrade, ent->client->ps.stats ) ) + BG_DeactivateUpgrade( upgrade, ent->client->ps.stats ); + else + trap_SendServerCommand( ent-g_entities, va( "print \"You don't have the %s\n\"", s ) ); +} + + +/* +================= +Cmd_ToggleItem_f +================= +*/ +void Cmd_ToggleItem_f( gentity_t *ent ) +{ + char s[ MAX_TOKEN_CHARS ]; + int upgrade, weapon, i; + + trap_Argv( 1, s, sizeof( s ) ); + upgrade = BG_FindUpgradeNumForName( s ); + weapon = BG_FindWeaponNumForName( s ); + + if( weapon != WP_NONE ) + { + //special case to allow switching between + //the blaster and the primary weapon + + if( ent->client->ps.weapon != WP_BLASTER ) + weapon = WP_BLASTER; + else + { + //find a held weapon which isn't the blaster + for( i = WP_NONE + 1; i < WP_NUM_WEAPONS; i++ ) + { + if( i == WP_BLASTER ) + continue; + + if( BG_InventoryContainsWeapon( i, ent->client->ps.stats ) ) + { + weapon = i; + break; + } + } + + if( i == WP_NUM_WEAPONS ) + weapon = WP_BLASTER; + } + + G_ForceWeaponChange( ent, weapon ); + } + else if( BG_InventoryContainsUpgrade( upgrade, ent->client->ps.stats ) ) + { + if( BG_UpgradeIsActive( upgrade, ent->client->ps.stats ) ) + BG_DeactivateUpgrade( upgrade, ent->client->ps.stats ); + else + BG_ActivateUpgrade( upgrade, ent->client->ps.stats ); + } + else + trap_SendServerCommand( ent-g_entities, va( "print \"You don't have the %s\n\"", s ) ); +} + +/* +================= +Cmd_Buy_f +================= +*/ +void Cmd_Buy_f( gentity_t *ent ) +{ + char s[ MAX_TOKEN_CHARS ]; + int i; + int weapon, upgrade, numItems = 0; + int maxAmmo, maxClips; + qboolean buyingEnergyAmmo = qfalse; + qboolean hasEnergyWeapon = qfalse; + + for( i = UP_NONE; i < UP_NUM_UPGRADES; i++ ) + { + if( BG_InventoryContainsUpgrade( i, ent->client->ps.stats ) ) + numItems++; + } + + for( i = WP_NONE; i < WP_NUM_WEAPONS; i++ ) + { + if( BG_InventoryContainsWeapon( i, ent->client->ps.stats ) ) + { + if( BG_FindUsesEnergyForWeapon( i ) ) + hasEnergyWeapon = qtrue; + numItems++; + } + } + + trap_Argv( 1, s, sizeof( s ) ); + + weapon = BG_FindWeaponNumForName( s ); + upgrade = BG_FindUpgradeNumForName( s ); + + //special case to keep norf happy + if( weapon == WP_NONE && upgrade == UP_AMMO ) + { + buyingEnergyAmmo = hasEnergyWeapon; + } + + if( buyingEnergyAmmo ) + { + //no armoury nearby + if( !G_BuildableRange( ent->client->ps.origin, 100, BA_H_REACTOR ) && + !G_BuildableRange( ent->client->ps.origin, 100, BA_H_REPEATER ) && + !G_BuildableRange( ent->client->ps.origin, 100, BA_H_ARMOURY ) ) + { + trap_SendServerCommand( ent-g_entities, va( + "print \"You must be near a reactor, repeater or armoury\n\"" ) ); + return; + } + } + else + { + //no armoury nearby + if( !G_BuildableRange( ent->client->ps.origin, 100, BA_H_ARMOURY ) ) + { + trap_SendServerCommand( ent-g_entities, va( "print \"You must be near a powered armoury\n\"" ) ); + return; + } + } + + if( weapon != WP_NONE ) + { + //already got this? + if( BG_InventoryContainsWeapon( weapon, ent->client->ps.stats ) ) + { + G_TriggerMenu( ent->client->ps.clientNum, MN_H_ITEMHELD ); + return; + } + + // denyweapons + if( weapon >= WP_PAIN_SAW && weapon <= WP_GRENADE && + ent->client->pers.denyHumanWeapons & ( 1 << (weapon - WP_BLASTER) ) ) + { + trap_SendServerCommand( ent-g_entities, va( "print \"You are denied from buying this weapon\n\"" ) ); + return; + } + + //can afford this? + if( BG_FindPriceForWeapon( weapon ) > (short)ent->client->ps.persistant[ PERS_CREDIT ] ) + { + G_TriggerMenu( ent->client->ps.clientNum, MN_H_NOFUNDS ); + return; + } + + //have space to carry this? + if( BG_FindSlotsForWeapon( weapon ) & ent->client->ps.stats[ STAT_SLOTS ] ) + { + G_TriggerMenu( ent->client->ps.clientNum, MN_H_NOSLOTS ); + return; + } + + if( BG_FindTeamForWeapon( weapon ) != WUT_HUMANS ) + { + //shouldn't need a fancy dialog + trap_SendServerCommand( ent-g_entities, va( "print \"You can't buy alien items\n\"" ) ); + return; + } + + //are we /allowed/ to buy this? + if( !BG_FindPurchasableForWeapon( weapon ) ) + { + trap_SendServerCommand( ent-g_entities, va( "print \"You can't buy this item\n\"" ) ); + return; + } + + //are we /allowed/ to buy this? + if( !BG_FindStagesForWeapon( weapon, g_humanStage.integer ) || !BG_WeaponIsAllowed( weapon ) ) + { + trap_SendServerCommand( ent-g_entities, va( "print \"You can't buy this item\n\"" ) ); + return; + } + + //add to inventory + BG_AddWeaponToInventory( weapon, ent->client->ps.stats ); + BG_FindAmmoForWeapon( weapon, &maxAmmo, &maxClips ); + + if( BG_FindUsesEnergyForWeapon( weapon ) && + BG_InventoryContainsUpgrade( UP_BATTPACK, ent->client->ps.stats ) ) + maxAmmo = (int)( (float)maxAmmo * BATTPACK_MODIFIER ); + + BG_PackAmmoArray( weapon, ent->client->ps.ammo, ent->client->ps.powerups, + maxAmmo, maxClips ); + + G_ForceWeaponChange( ent, weapon ); + + //set build delay/pounce etc to 0 + ent->client->ps.stats[ STAT_MISC ] = 0; + + //subtract from funds + G_AddCreditToClient( ent->client, -(short)BG_FindPriceForWeapon( weapon ), qfalse ); + } + else if( upgrade != UP_NONE ) + { + //already got this? + if( BG_InventoryContainsUpgrade( upgrade, ent->client->ps.stats ) ) + { + G_TriggerMenu( ent->client->ps.clientNum, MN_H_ITEMHELD ); + return; + } + + // denyweapons + if( upgrade == UP_GRENADE && + ent->client->pers.denyHumanWeapons & ( 1 << (WP_GRENADE - WP_BLASTER) ) ) + { + trap_SendServerCommand( ent-g_entities, va( "print \"You are denied from buying this upgrade\n\"" ) ); + return; + } + + //can afford this? + if( BG_FindPriceForUpgrade( upgrade ) > (short)ent->client->ps.persistant[ PERS_CREDIT ] ) + { + G_TriggerMenu( ent->client->ps.clientNum, MN_H_NOFUNDS ); + return; + } + + //have space to carry this? + if( BG_FindSlotsForUpgrade( upgrade ) & ent->client->ps.stats[ STAT_SLOTS ] ) + { + G_TriggerMenu( ent->client->ps.clientNum, MN_H_NOSLOTS ); + return; + } + + if( BG_FindTeamForUpgrade( upgrade ) != WUT_HUMANS ) + { + //shouldn't need a fancy dialog + trap_SendServerCommand( ent-g_entities, va( "print \"You can't buy alien items\n\"" ) ); + return; + } + + //are we /allowed/ to buy this? + if( !BG_FindPurchasableForUpgrade( upgrade ) ) + { + trap_SendServerCommand( ent-g_entities, va( "print \"You can't buy this item\n\"" ) ); + return; + } + + //are we /allowed/ to buy this? + if( !BG_FindStagesForUpgrade( upgrade, g_humanStage.integer ) || !BG_UpgradeIsAllowed( upgrade ) ) + { + trap_SendServerCommand( ent-g_entities, va( "print \"You can't buy this item\n\"" ) ); + return; + } + + if( upgrade == UP_BATTLESUIT && ent->client->ps.pm_flags & PMF_DUCKED ) + { + trap_SendServerCommand( ent-g_entities, + va( "print \"You can't buy this item while crouching\n\"" ) ); + return; + } + + if( upgrade == UP_AMMO ) + G_GiveClientMaxAmmo( ent, buyingEnergyAmmo ); + else + { + //add to inventory + BG_AddUpgradeToInventory( upgrade, ent->client->ps.stats ); + } + + if( upgrade == UP_BATTPACK ) + G_GiveClientMaxAmmo( ent, qtrue ); + + //subtract from funds + G_AddCreditToClient( ent->client, -(short)BG_FindPriceForUpgrade( upgrade ), qfalse ); + } + else + { + trap_SendServerCommand( ent-g_entities, va( "print \"Unknown item\n\"" ) ); + } + + if( trap_Argc( ) >= 2 ) + { + trap_Argv( 2, s, sizeof( s ) ); + + //retrigger the armoury menu + if( !Q_stricmp( s, "retrigger" ) ) + ent->client->retriggerArmouryMenu = level.framenum + RAM_FRAMES; + } + if( ent->client->pers.paused ) + { + trap_SendServerCommand( ent-g_entities, + "print \"You may not deconstruct while paused\n\"" ); + return; + } + + //update ClientInfo + ClientUserinfoChanged( ent->client->ps.clientNum, qfalse ); +} + + +/* +================= +Cmd_Sell_f +================= +*/ +void Cmd_Sell_f( gentity_t *ent ) +{ + char s[ MAX_TOKEN_CHARS ]; + int i; + int weapon, upgrade; + + trap_Argv( 1, s, sizeof( s ) ); + + //no armoury nearby + if( !G_BuildableRange( ent->client->ps.origin, 100, BA_H_ARMOURY ) ) + { + trap_SendServerCommand( ent-g_entities, va( "print \"You must be near a powered armoury\n\"" ) ); + return; + } + + weapon = BG_FindWeaponNumForName( s ); + upgrade = BG_FindUpgradeNumForName( s ); + + if( weapon != WP_NONE ) + { + //are we /allowed/ to sell this? + if( !BG_FindPurchasableForWeapon( weapon ) ) + { + trap_SendServerCommand( ent-g_entities, va( "print \"You can't sell this weapon\n\"" ) ); + return; + } + + //remove weapon if carried + if( BG_InventoryContainsWeapon( weapon, ent->client->ps.stats ) ) + { + //guard against selling the HBUILD weapons exploit + if( ( weapon == WP_HBUILD || weapon == WP_HBUILD2 ) && + ent->client->ps.stats[ STAT_MISC ] > 0 ) + { + trap_SendServerCommand( ent-g_entities, va( "print \"Cannot sell until build timer expires\n\"" ) ); + return; + } + + BG_RemoveWeaponFromInventory( weapon, ent->client->ps.stats ); + + //add to funds + G_AddCreditToClient( ent->client, (short)BG_FindPriceForWeapon( weapon ), qfalse ); + } + + //if we have this weapon selected, force a new selection + if( weapon == ent->client->ps.weapon ) + G_ForceWeaponChange( ent, WP_NONE ); + } + else if( upgrade != UP_NONE ) + { + //are we /allowed/ to sell this? + if( !BG_FindPurchasableForUpgrade( upgrade ) ) + { + trap_SendServerCommand( ent-g_entities, va( "print \"You can't sell this item\n\"" ) ); + return; + } + //remove upgrade if carried + if( BG_InventoryContainsUpgrade( upgrade, ent->client->ps.stats ) ) + { + BG_RemoveUpgradeFromInventory( upgrade, ent->client->ps.stats ); + + if( upgrade == UP_BATTPACK ) + G_GiveClientMaxAmmo( ent, qtrue ); + + //add to funds + G_AddCreditToClient( ent->client, (short)BG_FindPriceForUpgrade( upgrade ), qfalse ); + } + } + else if( !Q_stricmp( s, "weapons" ) ) + { + for( i = WP_NONE + 1; i < WP_NUM_WEAPONS; i++ ) + { + //guard against selling the HBUILD weapons exploit + if( ( i == WP_HBUILD || i == WP_HBUILD2 ) && + ent->client->ps.stats[ STAT_MISC ] > 0 ) + { + trap_SendServerCommand( ent-g_entities, va( "print \"Cannot sell until build timer expires\n\"" ) ); + continue; + } + + if( BG_InventoryContainsWeapon( i, ent->client->ps.stats ) && + BG_FindPurchasableForWeapon( i ) ) + { + BG_RemoveWeaponFromInventory( i, ent->client->ps.stats ); + + //add to funds + G_AddCreditToClient( ent->client, (short)BG_FindPriceForWeapon( i ), qfalse ); + } + + //if we have this weapon selected, force a new selection + if( i == ent->client->ps.weapon ) + G_ForceWeaponChange( ent, WP_NONE ); + } + } + else if( !Q_stricmp( s, "upgrades" ) ) + { + for( i = UP_NONE + 1; i < UP_NUM_UPGRADES; i++ ) + { + //remove upgrade if carried + if( BG_InventoryContainsUpgrade( i, ent->client->ps.stats ) && + BG_FindPurchasableForUpgrade( i ) ) + { + BG_RemoveUpgradeFromInventory( i, ent->client->ps.stats ); + + if( i == UP_BATTPACK ) + { + int j; + + //remove energy + for( j = WP_NONE; j < WP_NUM_WEAPONS; j++ ) + { + if( BG_InventoryContainsWeapon( j, ent->client->ps.stats ) && + BG_FindUsesEnergyForWeapon( j ) && + !BG_FindInfinteAmmoForWeapon( j ) ) + { + BG_PackAmmoArray( j, ent->client->ps.ammo, ent->client->ps.powerups, 0, 0 ); + } + } + } + + //add to funds + G_AddCreditToClient( ent->client, (short)BG_FindPriceForUpgrade( i ), qfalse ); + } + } + } + else + trap_SendServerCommand( ent-g_entities, va( "print \"Unknown item\n\"" ) ); + + if( trap_Argc( ) >= 2 ) + { + trap_Argv( 2, s, sizeof( s ) ); + + //retrigger the armoury menu + if( !Q_stricmp( s, "retrigger" ) ) + ent->client->retriggerArmouryMenu = level.framenum + RAM_FRAMES; + } + + //update ClientInfo + ClientUserinfoChanged( ent->client->ps.clientNum, qfalse ); +} + + +/* +================= +Cmd_Build_f +================= +*/ +void Cmd_Build_f( gentity_t *ent ) +{ + char s[ MAX_TOKEN_CHARS ]; + buildable_t buildable; + float dist; + vec3_t origin; + pTeam_t team; + + if( ent->client->pers.denyBuild ) + { + trap_SendServerCommand( ent-g_entities, + "print \"Your building rights have been revoked\n\"" ); + return; + } + if( ent->client->pers.paused ) + { + trap_SendServerCommand( ent-g_entities, + "print \"You may not mark while paused\n\"" ); + return; + } + + trap_Argv( 1, s, sizeof( s ) ); + + buildable = BG_FindBuildNumForName( s ); + + + if( g_suddenDeath.integer) + { + if( g_suddenDeathMode.integer == SDMODE_SELECTIVE ) + { + if( !BG_FindReplaceableTestForBuildable( buildable ) ) + { + trap_SendServerCommand( ent-g_entities, + "print \"This building type cannot be rebuilt during Sudden Death\n\"" ); + return; + } + if( G_BuildingExists( buildable ) ) + { + trap_SendServerCommand( ent-g_entities, + "print \"You can only rebuild one of each type of rebuildable building during Sudden Death.\n\"" ); + return; + } + } + else if( g_suddenDeathMode.integer == SDMODE_NO_BUILD ) + { + trap_SendServerCommand( ent-g_entities, + "print \"Building is not allowed during Sudden Death\n\"" ); + return; + } + } + + team = ent->client->ps.stats[ STAT_PTEAM ]; + + if( buildable != BA_NONE && + ( ( 1 << ent->client->ps.weapon ) & BG_FindBuildWeaponForBuildable( buildable ) ) && + !( ent->client->ps.stats[ STAT_STATE ] & SS_INFESTING ) && + !( ent->client->ps.stats[ STAT_STATE ] & SS_HOVELING ) && + BG_BuildableIsAllowed( buildable ) && + ( ( team == PTE_ALIENS && BG_FindStagesForBuildable( buildable, g_alienStage.integer ) ) || + ( team == PTE_HUMANS && BG_FindStagesForBuildable( buildable, g_humanStage.integer ) ) ) ) + { + dist = BG_FindBuildDistForClass( ent->client->ps.stats[ STAT_PCLASS ] ); + + //these are the errors displayed when the builder first selects something to use + switch( G_CanBuild( ent, buildable, dist, origin ) ) + { + case IBE_NONE: + case IBE_TNODEWARN: + case IBE_RPTWARN: + case IBE_RPTWARN2: + case IBE_SPWNWARN: + case IBE_NOROOM: + case IBE_NORMAL: + case IBE_HOVELEXIT: + ent->client->ps.stats[ STAT_BUILDABLE ] = ( buildable | SB_VALID_TOGGLEBIT ); + break; + + case IBE_NOASSERT: + G_TriggerMenu( ent->client->ps.clientNum, MN_A_NOASSERT ); + break; + + case IBE_NOOVERMIND: + G_TriggerMenu( ent->client->ps.clientNum, MN_A_NOOVMND ); + break; + + case IBE_OVERMIND: + G_TriggerMenu( ent->client->ps.clientNum, MN_A_OVERMIND ); + break; + + case IBE_REACTOR: + G_TriggerMenu( ent->client->ps.clientNum, MN_H_REACTOR ); + break; + + case IBE_REPEATER: + G_TriggerMenu( ent->client->ps.clientNum, MN_H_REPEATER ); + break; + + case IBE_NOPOWER: + G_TriggerMenu( ent->client->ps.clientNum, MN_H_NOPOWER ); + break; + + case IBE_NOCREEP: + G_TriggerMenu( ent->client->ps.clientNum, MN_A_NOCREEP ); + break; + + case IBE_NODCC: + G_TriggerMenu( ent->client->ps.clientNum, MN_H_NODCC ); + break; + + default: + break; + } + } + else + trap_SendServerCommand( ent-g_entities, va( "print \"Cannot build this item\n\"" ) ); +} + + +/* +================= +Cmd_Boost_f +================= +*/ +void Cmd_Boost_f( gentity_t *ent ) +{ + if( BG_InventoryContainsUpgrade( UP_JETPACK, ent->client->ps.stats ) && + BG_UpgradeIsActive( UP_JETPACK, ent->client->ps.stats ) ) + return; + + if( ent->client->pers.cmd.buttons & BUTTON_WALKING ) + return; + + if( ( ent->client->ps.stats[ STAT_PTEAM ] == PTE_HUMANS ) && + ( ent->client->ps.stats[ STAT_STAMINA ] > 0 ) ) + ent->client->ps.stats[ STAT_STATE ] |= SS_SPEEDBOOST; +} + +/* +================= +Cmd_Protect_f +================= +*/ +void Cmd_Protect_f( gentity_t *ent ) +{ + vec3_t forward, end; + trace_t tr; + gentity_t *traceEnt; + + if( !ent->client->pers.designatedBuilder ) + { + trap_SendServerCommand( ent-g_entities, "print \"Only designated" + " builders can toggle structure protection.\n\"" ); + return; + } + + AngleVectors( ent->client->ps.viewangles, forward, NULL, NULL ); + VectorMA( ent->client->ps.origin, 100, forward, end ); + + trap_Trace( &tr, ent->client->ps.origin, NULL, NULL, end, ent->s.number, + MASK_PLAYERSOLID ); + traceEnt = &g_entities[ tr.entityNum ]; + + if( tr.fraction < 1.0f && ( traceEnt->s.eType == ET_BUILDABLE ) && + ( traceEnt->biteam == ent->client->pers.teamSelection ) ) + { + if( traceEnt->s.eFlags & EF_DBUILDER ) + { + trap_SendServerCommand( ent-g_entities, + "print \"Structure protection removed\n\"" ); + traceEnt->s.eFlags &= ~EF_DBUILDER; + } + else + { + trap_SendServerCommand( ent-g_entities, + "print \"Structure protection applied\n\"" ); + traceEnt->s.eFlags |= EF_DBUILDER; + + // adding protection turns off deconstruction mark + traceEnt->deconstruct = qfalse; + } + } +} + + /* + ================= + Cmd_Resign_f + ================= + */ + void Cmd_Resign_f( gentity_t *ent ) + { + if( !ent->client->pers.designatedBuilder ) + { + trap_SendServerCommand( ent-g_entities, + "print \"You are not a designated builder\n\"" ); + return; + } + + ent->client->pers.designatedBuilder = qfalse; + trap_SendServerCommand( -1, va( + "print \"%s" S_COLOR_WHITE " has resigned\n\"", + ent->client->pers.netname ) ); + G_CheckDBProtection( ); + } + + + +/* +================= +Cmd_Reload_f +================= +*/ +void Cmd_Reload_f( gentity_t *ent ) +{ + if( ( ent->client->ps.weapon >= WP_ABUILD ) && + ( ent->client->ps.weapon <= WP_HBUILD ) ) + { + if( ent->client->pers.designatedBuilder ) + Cmd_Protect_f( ent ); + else + Cmd_Mark_f( ent ); + } + else if( ent->client->ps.weaponstate != WEAPON_RELOADING ) + ent->client->ps.pm_flags |= PMF_WEAPON_RELOAD; +} + + +/* +================= +Cmd_MyStats_f +================= +*/ +void Cmd_MyStats_f( gentity_t *ent ) +{ + + if(!ent) return; + + + if( !level.intermissiontime && ent->client->pers.statscounters.timeLastViewed && (level.time - ent->client->pers.statscounters.timeLastViewed) <60000 ) + { + ADMP( "You may only check your stats once per minute and during intermission.\n"); + return; + } + + if( !g_myStats.integer ) + { + ADMP( "myStats has been disabled\n"); + return; + } + + ADMP( G_statsString( &ent->client->pers.statscounters, &ent->client->pers.teamSelection ) ); + ent->client->pers.statscounters.timeLastViewed = level.time; + + return; +} + +char *G_statsString( statsCounters_t *sc, pTeam_t *pt ) +{ + char *s; + + int percentNearBase=0; + int percentJetpackWallwalk=0; + int percentHeadshots=0; + double avgTimeAlive=0; + int avgTimeAliveMins = 0; + int avgTimeAliveSecs = 0; + + if( sc->timealive ) + percentNearBase = (int)(100 * (float) sc->timeinbase / ((float) (sc->timealive ) ) ); + + if( sc->timealive && sc->deaths ) + { + avgTimeAlive = sc->timealive / sc->deaths; + } + + avgTimeAliveMins = (int) (avgTimeAlive / 60.0f); + avgTimeAliveSecs = (int) (avgTimeAlive - (60.0f * avgTimeAliveMins)); + + if( *pt == PTE_ALIENS ) + { + if( sc->dretchbasytime > 0 ) + percentJetpackWallwalk = (int)(100 * (float) sc->jetpackusewallwalkusetime / ((float) ( sc->dretchbasytime) ) ); + + if( sc->hitslocational ) + percentHeadshots = (int)(100 * (float) sc->headshots / ((float) (sc->hitslocational) ) ); + + s = va( "^3Kills:^7 %3i ^3StructKills:^7 %3i ^3Assists:^7 %3i^7 ^3Poisons:^7 %3i ^3Headshots:^7 %3i (%3i)\n^3Deaths:^7 %3i ^3Feeds:^7 %3i ^3Suicides:^7 %3i ^3TKs:^7 %3i ^3Avg Lifespan:^7 %4d:%02d\n^3Damage to:^7 ^3Enemies:^7 %5i ^3Structs:^7 %5i ^3Friendlies:^7 %3i \n^3Structs Built:^7 %3i ^3Time Near Base:^7 %3i ^3Time wallwalking:^7 %3i\n", + sc->kills, + sc->structskilled, + sc->assists, + sc->repairspoisons, + sc->headshots, + percentHeadshots, + sc->deaths, + sc->feeds, + sc->suicides, + sc->teamkills, + avgTimeAliveMins, + avgTimeAliveSecs, + sc->dmgdone, + sc->structdmgdone, + sc->ffdmgdone, + sc->structsbuilt, + percentNearBase, + percentJetpackWallwalk + ); + } + else if( *pt == PTE_HUMANS ) + { + if( sc->timealive ) + percentJetpackWallwalk = (int)(100 * (float) sc->jetpackusewallwalkusetime / ((float) ( sc->timealive ) ) ); + s = va( "^3Kills:^7 %3i ^3StructKills:^7 %3i ^3Assists:^7 %3i \n^3Deaths:^7 %3i ^3Feeds:^7 %3i ^3Suicides:^7 %3i ^3TKs:^7 %3i ^3Avg Lifespan:^7 %4d:%02d\n^3Damage to:^7 ^3Enemies:^7 %5i ^3Structs:^7 %5i ^3Friendlies:^7 %3i \n^3Structs Built:^7 %3i ^3Repairs:^7 %4i ^3Time Near Base:^7 %3i ^3Time Jetpacking:^7 %3i\n", + sc->kills, + sc->structskilled, + sc->assists, + sc->deaths, + sc->feeds, + sc->suicides, + sc->teamkills, + avgTimeAliveMins, + avgTimeAliveSecs, + sc->dmgdone, + sc->structdmgdone, + sc->ffdmgdone, + sc->structsbuilt, + sc->repairspoisons, + percentNearBase, + percentJetpackWallwalk + ); + } + else s="No stats available\n"; + + return s; +} + + /* + ================= + Cmd_AllStats_f + ================= + */ + void Cmd_AllStats_f( gentity_t *ent ) + { + int i; + int NextViewTime; + int NumResults = 0; + int Teamcolor = 3; + gentity_t *tmpent; + + //check if ent exists + if(!ent) return; + + NextViewTime = ent->client->pers.statscounters.AllstatstimeLastViewed + g_AllStatsTime.integer * 1000; + //check if you can use the cmd at this time + if( !level.intermissiontime && level.time < NextViewTime) + { + ADMP( va("You may only check your stats every %i Seconds and during intermission. Next valid time is %d:%02d\n",( g_AllStatsTime.integer ) ? ( g_AllStatsTime.integer ) : 60, ( NextViewTime / 60000 ), ( NextViewTime / 1000 ) % 60 ) ); + return; + } + //see if allstats is enabled + if( !g_AllStats.integer ) + { + ADMP( "AllStats has been disabled\n"); + return; + } + ADMP("^3K^2=^7Kills ^3A^2=^7Assists ^3SK^2=^7StructKills\n^3D^2=^7Deaths ^3F^2=^7Feeds ^3S^2=^7Suicides ^3TK^2=^7Teamkills\n^3DD^2=^7Damage done ^3TDD^2=^7Team Damage done\n^3SB^2=^7Structs Built\n\n" ); + //display a header describing the data + ADMP( "^3 #| K A SK| D F S TK| DD TDD| SB| Name\n" ); + + //loop through the clients that are connected + for( i = 0; i < level.numConnectedClients; i++ ) + { + //assign a tempent 4 the hell of it + tmpent = &g_entities[ level.sortedClients[ i ] ]; + + //check for what mode we are working in and display relevent data + if( g_AllStats.integer == 1 ) + { + //check if client is connected and on same team + if( tmpent->client && tmpent->client->pers.connected == CON_CONNECTED && tmpent->client->pers.teamSelection == ent->client->pers.teamSelection && tmpent->client->pers.teamSelection != PTE_NONE ) + { + NumResults++; + if( tmpent->client->pers.teamSelection == PTE_ALIENS ) Teamcolor = 1; + if( tmpent->client->pers.teamSelection == PTE_HUMANS ) Teamcolor = 5; + ADMP( va( "^%i%2i^3|^%i%3i %3i %3i^3|^%i%3i %3i %3i %3i^3|^%i%5i %5i^3|^%i%3i^3|^7 %s\n", + Teamcolor, + NumResults, + Teamcolor, + ( tmpent->client->pers.statscounters.kills ) ? tmpent->client->pers.statscounters.kills : 0, + ( tmpent->client->pers.statscounters.assists ) ? tmpent->client->pers.statscounters.assists : 0, + ( tmpent->client->pers.statscounters.structskilled ) ? tmpent->client->pers.statscounters.structskilled : 0, + Teamcolor, + ( tmpent->client->pers.statscounters.deaths ) ? tmpent->client->pers.statscounters.deaths : 0, + ( tmpent->client->pers.statscounters.feeds ) ? tmpent->client->pers.statscounters.feeds : 0, + ( tmpent->client->pers.statscounters.suicides ) ? tmpent->client->pers.statscounters.suicides : 0, + ( tmpent->client->pers.statscounters.teamkills ) ? tmpent->client->pers.statscounters.teamkills : 0, + Teamcolor, + ( tmpent->client->pers.statscounters.dmgdone ) ? tmpent->client->pers.statscounters.dmgdone : 0, + ( tmpent->client->pers.statscounters.ffdmgdone ) ? tmpent->client->pers.statscounters.ffdmgdone : 0, + Teamcolor, + ( tmpent->client->pers.statscounters.structsbuilt ) ? tmpent->client->pers.statscounters.structsbuilt : 0, + ( tmpent->client->pers.netname ) ? tmpent->client->pers.netname : "Unknown" ) ); + } + } + else if( g_AllStats.integer == 2 ) + { + //check if client is connected and has some stats or atleast is on a team + if( tmpent->client && tmpent->client->pers.connected == CON_CONNECTED && ( tmpent->client->pers.teamSelection != PTE_NONE ) ) + { + NumResults++; + if( tmpent->client->pers.teamSelection == PTE_ALIENS ) Teamcolor = 1; + if( tmpent->client->pers.teamSelection == PTE_HUMANS ) Teamcolor = 5; + ADMP( va( "^%i%2i^3|^%i%3i %3i %3i^3|^%i%3i %3i %3i %3i^3|^%i%5i %5i^3|^%i%3i^3|^7 %s\n", + Teamcolor, + NumResults, + Teamcolor, + ( tmpent->client->pers.statscounters.kills ) ? tmpent->client->pers.statscounters.kills : 0, + ( tmpent->client->pers.statscounters.assists ) ? tmpent->client->pers.statscounters.assists : 0, + ( tmpent->client->pers.statscounters.structskilled ) ? tmpent->client->pers.statscounters.structskilled : 0, + Teamcolor, + ( tmpent->client->pers.statscounters.deaths ) ? tmpent->client->pers.statscounters.deaths : 0, + ( tmpent->client->pers.statscounters.feeds ) ? tmpent->client->pers.statscounters.feeds : 0, + ( tmpent->client->pers.statscounters.suicides ) ? tmpent->client->pers.statscounters.suicides : 0, + ( tmpent->client->pers.statscounters.teamkills ) ? tmpent->client->pers.statscounters.teamkills : 0, + Teamcolor, + ( tmpent->client->pers.statscounters.dmgdone ) ? tmpent->client->pers.statscounters.dmgdone : 0, + ( tmpent->client->pers.statscounters.ffdmgdone ) ? tmpent->client->pers.statscounters.ffdmgdone : 0, + Teamcolor, + ( tmpent->client->pers.statscounters.structsbuilt ) ? tmpent->client->pers.statscounters.structsbuilt : 0, + ( tmpent->client->pers.netname ) ? tmpent->client->pers.netname : "Unknown" ) ); + } + } + } + if( NumResults == 0 ) { + ADMP( " ^3EMPTY!\n" ); + } else { + ADMP( va( "^7 %i Players found!\n", NumResults ) ); + } + //update time last viewed + + ent->client->pers.statscounters.AllstatstimeLastViewed = level.time; + return; +} + +/* +================= +Cmd_TeamStatus_f +================= +*/ +void Cmd_TeamStatus_f( gentity_t *ent ) +{ + char multiple[ 12 ]; + int builders = 0; + int arm = 0, mediboost = 0; + int omrccount = 0, omrchealth = 0; + qboolean omrcbuild = qfalse; + gentity_t *tmp; + int i; + + if( !g_teamStatus.integer ) + { + trap_SendServerCommand( ent - g_entities, + "print \"teamstatus is disabled.\n\"" ); + return; + } + + if( G_IsMuted( ent->client ) ) + { + trap_SendServerCommand( ent - g_entities, + "print \"You are muted and cannot use message commands.\n\"" ); + return; + } + + if( ent->client->pers.lastTeamStatus && + ( level.time - ent->client->pers.lastTeamStatus) < g_teamStatus.integer * 1000 ) + { + ADMP( va("You may only check your team's status once every %i seconds.\n", + g_teamStatus.integer )); + return; + } + + ent->client->pers.lastTeamStatus = level.time; + + tmp = &g_entities[ 0 ]; + for ( i = 0; i < level.num_entities; i++, tmp++ ) + { + if( i < MAX_CLIENTS ) + { + if( tmp->client && + tmp->client->pers.connected == CON_CONNECTED && + tmp->client->pers.teamSelection == ent->client->pers.teamSelection && + tmp->health > 0 && + ( tmp->client->ps.stats[ STAT_PCLASS ] == PCL_ALIEN_BUILDER0 || + tmp->client->ps.stats[ STAT_PCLASS ] == PCL_ALIEN_BUILDER0_UPG || + BG_InventoryContainsWeapon( WP_HBUILD, tmp->client->ps.stats ) || + BG_InventoryContainsWeapon( WP_HBUILD2, tmp->client->ps.stats ) ) ) + builders++; + continue; + } + + if( tmp->s.eType == ET_BUILDABLE ) + { + if( tmp->biteam != ent->client->pers.teamSelection || + tmp->health <= 0 ) + continue; + + switch( tmp->s.modelindex ) + { + case BA_H_REACTOR: + case BA_A_OVERMIND: + omrccount++; + if( tmp->health > omrchealth ) + omrchealth = tmp->health; + if( !omrcbuild ) + omrcbuild = tmp->spawned; + break; + case BA_H_ARMOURY: + arm++; + break; + case BA_H_MEDISTAT: + case BA_A_BOOSTER: + mediboost++; + break; + default: + break; + } + } + } + + if( omrccount > 1 ) + Com_sprintf( multiple, sizeof( multiple ), "^7[x%d]", omrccount ); + else + multiple[ 0 ] = '\0'; + + if( ent->client->pers.teamSelection == PTE_ALIENS ) + { + G_Say( ent, NULL, SAY_TEAM, + va( "^3OM: %s(%d)%s ^3Spawns: ^5%d ^3Builders: ^5%d ^3Boosters: ^5%d^7" , + ( !omrccount ) ? "^1Down" : ( omrcbuild ) ? "^2Up" : "^5Building", + omrchealth * 100 / OVERMIND_HEALTH, + multiple, + level.numAlienSpawns, + builders, + mediboost ) ); + } + else + { + G_Say( ent, NULL, SAY_TEAM, + va( "^3RC: %s(%d)%s ^3Spawns: ^5%d ^3Builders: ^5%d ^3Armouries: ^5%d ^3Medistations: ^5%d^7" , + ( !omrccount ) ? "^1Down" : ( omrcbuild ) ? "^2Up" : "^5Building", + omrchealth * 100 / REACTOR_HEALTH, + multiple, + level.numHumanSpawns, + builders, + arm, mediboost ) ); + } +} + +/* +================= +G_StopFromFollowing + +stops any other clients from following this one +called when a player leaves a team or dies +================= +*/ +void G_StopFromFollowing( gentity_t *ent ) +{ + int i; + + for( i = 0; i < level.maxclients; i++ ) + { + if( level.clients[ i ].sess.spectatorState == SPECTATOR_FOLLOW && + level.clients[ i ].sess.spectatorClient == ent-g_entities ) + { + if( !G_FollowNewClient( &g_entities[ i ], 1 ) ) + G_StopFollowing( &g_entities[ i ] ); + } + } +} + +/* +================= +G_StopFollowing + +If the client being followed leaves the game, or you just want to drop +to free floating spectator mode +================= +*/ +void G_StopFollowing( gentity_t *ent ) +{ + ent->client->ps.persistant[ PERS_TEAM ] = TEAM_SPECTATOR; + ent->client->sess.sessionTeam = TEAM_SPECTATOR; + ent->client->ps.stats[ STAT_PTEAM ] = ent->client->pers.teamSelection; + + if( ent->client->pers.teamSelection == PTE_NONE ) + { + ent->client->sess.spectatorState = SPECTATOR_FREE; + ent->client->ps.pm_type = PM_SPECTATOR; + ent->client->ps.stats[ STAT_HEALTH ] = 100; // hacky server-side fix to prevent cgame from viewlocking a freespec + } + else + { + vec3_t spawn_origin, spawn_angles; + + ent->client->sess.spectatorState = SPECTATOR_LOCKED; + if( ent->client->pers.teamSelection == PTE_ALIENS ) + G_SelectAlienLockSpawnPoint( spawn_origin, spawn_angles ); + else if( ent->client->pers.teamSelection == PTE_HUMANS ) + G_SelectHumanLockSpawnPoint( spawn_origin, spawn_angles ); + G_SetOrigin( ent, spawn_origin ); + VectorCopy( spawn_origin, ent->client->ps.origin ); + G_SetClientViewAngle( ent, spawn_angles ); + } + ent->client->sess.spectatorClient = -1; + ent->client->ps.pm_flags &= ~PMF_FOLLOW; + + // Prevent spawning with bsuit in rare case + if( BG_InventoryContainsUpgrade( UP_BATTLESUIT, ent->client->ps.stats ) ) + BG_RemoveUpgradeFromInventory( UP_BATTLESUIT, ent->client->ps.stats ); + + ent->client->ps.stats[ STAT_STATE ] &= ~SS_WALLCLIMBING; + ent->client->ps.stats[ STAT_STATE ] &= ~SS_WALLCLIMBINGCEILING; + ent->client->ps.eFlags &= ~EF_WALLCLIMB; + ent->client->ps.viewangles[ PITCH ] = 0.0f; + + ent->client->ps.clientNum = ent - g_entities; + + CalculateRanks( ); +} + +/* +================= +G_FollowNewClient + +This was a really nice, elegant function. Then I fucked it up. +================= +*/ +qboolean G_FollowNewClient( gentity_t *ent, int dir ) +{ + int clientnum = ent->client->sess.spectatorClient; + int original = clientnum; + qboolean selectAny = qfalse; + + if( dir > 1 ) + dir = 1; + else if( dir < -1 ) + dir = -1; + else if( dir == 0 ) + return qtrue; + + if( ent->client->sess.sessionTeam != TEAM_SPECTATOR ) + return qfalse; + + // select any if no target exists + if( clientnum < 0 || clientnum >= level.maxclients ) + { + clientnum = original = 0; + selectAny = qtrue; + } + + do + { + clientnum += dir; + + if( clientnum >= level.maxclients ) + clientnum = 0; + + if( clientnum < 0 ) + clientnum = level.maxclients - 1; + + // avoid selecting existing follow target + if( clientnum == original && !selectAny ) + continue; //effectively break; + + // can't follow self + if( &level.clients[ clientnum ] == ent->client ) + continue; + + // can only follow connected clients + if( level.clients[ clientnum ].pers.connected != CON_CONNECTED ) + continue; + + // can't follow another spectator + if( level.clients[ clientnum ].pers.teamSelection == PTE_NONE ) + continue; + + // can only follow teammates when dead and on a team + if( ent->client->pers.teamSelection != PTE_NONE && + ( level.clients[ clientnum ].pers.teamSelection != + ent->client->pers.teamSelection ) ) + continue; + + // cannot follow a teammate who is following you + if( level.clients[ clientnum ].sess.spectatorState == SPECTATOR_FOLLOW && + ( level.clients[ clientnum ].sess.spectatorClient == ent->s.number ) ) + continue; + + // this is good, we can use it + ent->client->sess.spectatorClient = clientnum; + ent->client->sess.spectatorState = SPECTATOR_FOLLOW; + return qtrue; + + } while( clientnum != original ); + + return qfalse; +} + +/* +================= +G_ToggleFollow +================= +*/ +void G_ToggleFollow( gentity_t *ent ) +{ + if( level.mapRotationVoteTime ) + { + G_IntermissionMapVoteCommand( ent, qfalse, qtrue ); + return; + } + + if( ent->client->sess.spectatorState == SPECTATOR_FOLLOW ) + G_StopFollowing( ent ); + else + G_FollowNewClient( ent, 1 ); +} + +/* +================= +Cmd_Follow_f +================= +*/ +void Cmd_Follow_f( gentity_t *ent ) +{ + int i; + int pids[ MAX_CLIENTS ]; + char arg[ MAX_TOKEN_CHARS ]; + + if( ent->client->sess.sessionTeam != TEAM_SPECTATOR ) + { + trap_SendServerCommand( ent - g_entities, "print \"follow: You cannot follow unless you are dead or on the spectators.\n\"" ); + return; + } + if( ent->client->pers.paused ) + { + trap_SendServerCommand( ent-g_entities, + "print \"You may not build while paused\n\"" ); + return; + } + + if( trap_Argc( ) != 2 ) + { + G_ToggleFollow( ent ); + } + else + { + trap_Argv( 1, arg, sizeof( arg ) ); + if( G_ClientNumbersFromString( arg, pids ) == 1 ) + { + i = pids[ 0 ]; + } + else + { + i = G_ClientNumberFromString( ent, arg ); + + if( i == -1 ) + { + trap_SendServerCommand( ent - g_entities, + "print \"follow: invalid player\n\"" ); + return; + } + } + + // can't follow self + if( &level.clients[ i ] == ent->client ) + { + trap_SendServerCommand( ent - g_entities, "print \"follow: You cannot follow yourself.\n\"" ); + return; + } + + // can't follow another spectator + if( level.clients[ i ].pers.teamSelection == PTE_NONE) + { + trap_SendServerCommand( ent - g_entities, "print \"follow: You cannot follow another spectator.\n\"" ); + return; + } + + // can only follow teammates when dead and on a team + if( ent->client->pers.teamSelection != PTE_NONE && + ( level.clients[ i ].pers.teamSelection != + ent->client->pers.teamSelection ) ) + { + trap_SendServerCommand( ent - g_entities, "print \"follow: You can only follow teammates, and only when you are dead.\n\"" ); + return; + } + + ent->client->sess.spectatorState = SPECTATOR_FOLLOW; + ent->client->sess.spectatorClient = i; + } +} + +/* +================= +Cmd_FollowCycle_f +================= +*/ +void Cmd_FollowCycle_f( gentity_t *ent ) +{ + char args[ 11 ]; + int dir = 1; + + trap_Argv( 0, args, sizeof( args ) ); + if( Q_stricmp( args, "followprev" ) == 0 ) + dir = -1; + + // won't work unless spectating + if( ent->client->sess.sessionTeam != TEAM_SPECTATOR ) + return; + if( ent->client->sess.spectatorState == SPECTATOR_NOT ) + return; + G_FollowNewClient( ent, dir ); +} + +/* +================= +Cmd_PTRCVerify_f + +Check a PTR code is valid +================= +*/ +void Cmd_PTRCVerify_f( gentity_t *ent ) +{ + connectionRecord_t *connection; + char s[ MAX_TOKEN_CHARS ] = { 0 }; + int code; + + if( ent->client->pers.connection ) + return; + + trap_Argv( 1, s, sizeof( s ) ); + + if( !strlen( s ) ) + return; + + code = atoi( s ); + + connection = G_FindConnectionForCode( code ); + if( connection && connection->clientNum == -1 ) + { + // valid code + if( connection->clientTeam != PTE_NONE ) + trap_SendServerCommand( ent->client->ps.clientNum, "ptrcconfirm" ); + + // restore mapping + ent->client->pers.connection = connection; + connection->clientNum = ent->client->ps.clientNum; + } + else + { + // invalid code -- generate a new one + connection = G_GenerateNewConnection( ent->client ); + + if( connection ) + { + trap_SendServerCommand( ent->client->ps.clientNum, + va( "ptrcissue %d", connection->ptrCode ) ); + } + } +} + +/* +================= +Cmd_PTRCRestore_f + +Restore against a PTR code +================= +*/ +void Cmd_PTRCRestore_f( gentity_t *ent ) +{ + char s[ MAX_TOKEN_CHARS ] = { 0 }; + int code; + connectionRecord_t *connection; + + if( ent->client->pers.joinedATeam ) + { + trap_SendServerCommand( ent - g_entities, + "print \"You cannot use a PTR code after joining a team\n\"" ); + return; + } + + trap_Argv( 1, s, sizeof( s ) ); + + if( !strlen( s ) ) + return; + + code = atoi( s ); + + connection = ent->client->pers.connection; + if( connection && connection->ptrCode == code ) + { + // Set the correct team + if( !( ent->client->pers.specExpires > level.time ) ) + { + // Check if the alien team is full + if( connection->clientTeam == PTE_ALIENS && + !G_admin_permission(ent, ADMF_FORCETEAMCHANGE) && + g_teamForceBalance.integer && + level.numAlienClients > level.numHumanClients ) + { + G_TriggerMenu( ent - g_entities, MN_A_TEAMFULL ); + } + // Check if the human team is full + else if( connection->clientTeam == PTE_HUMANS && + !G_admin_permission(ent, ADMF_FORCETEAMCHANGE) && + g_teamForceBalance.integer && + level.numHumanClients > level.numAlienClients ) + { + G_TriggerMenu( ent - g_entities, MN_H_TEAMFULL ); + } + else + { + G_ChangeTeam( ent, connection->clientTeam ); + } + } + + // set the correct credit etc. + ent->client->ps.persistant[ PERS_CREDIT ] = 0; + G_AddCreditToClient( ent->client, connection->clientCredit, qtrue ); + ent->client->pers.score = connection->clientScore; + ent->client->pers.enterTime = connection->clientEnterTime; + } + else + { + trap_SendServerCommand( ent - g_entities, + va( "print \"\"%d\" is not a valid PTR code\n\"", code ) ); + } +} + +static void Cmd_Ignore_f( gentity_t *ent ) +{ + int pids[ MAX_CLIENTS ]; + char name[ MAX_NAME_LENGTH ]; + char cmd[ 9 ]; + int matches = 0; + int i; + qboolean ignore = qfalse; + + trap_Argv( 0, cmd, sizeof( cmd ) ); + if( Q_stricmp( cmd, "ignore" ) == 0 ) + ignore = qtrue; + + if( trap_Argc() < 2 ) + { + trap_SendServerCommand( ent-g_entities, va( "print \"[skipnotify]" + "%s: usage \\%s [clientNum | partial name match]\n\"", cmd, cmd ) ); + return; + } + + Q_strncpyz( name, ConcatArgs( 1 ), sizeof( name ) ); + matches = G_ClientNumbersFromString( name, pids ); + if( matches < 1 ) + { + trap_SendServerCommand( ent-g_entities, va( "print \"[skipnotify]" + "%s: no clients match the name '%s'\n\"", cmd, name ) ); + return; + } + + for( i = 0; i < matches; i++ ) + { + if( ignore ) + { + if( !BG_ClientListTest( &ent->client->sess.ignoreList, pids[ i ] ) ) + { + BG_ClientListAdd( &ent->client->sess.ignoreList, pids[ i ] ); + ClientUserinfoChanged( ent->client->ps.clientNum, qfalse ); + trap_SendServerCommand( ent-g_entities, va( "print \"[skipnotify]" + "ignore: added %s^7 to your ignore list\n\"", + level.clients[ pids[ i ] ].pers.netname ) ); + } + else + { + trap_SendServerCommand( ent-g_entities, va( "print \"[skipnotify]" + "ignore: %s^7 is already on your ignore list\n\"", + level.clients[ pids[ i ] ].pers.netname ) ); + } + } + else + { + if( BG_ClientListTest( &ent->client->sess.ignoreList, pids[ i ] ) ) + { + BG_ClientListRemove( &ent->client->sess.ignoreList, pids[ i ] ); + ClientUserinfoChanged( ent->client->ps.clientNum, qfalse ); + trap_SendServerCommand( ent-g_entities, va( "print \"[skipnotify]" + "unignore: removed %s^7 from your ignore list\n\"", + level.clients[ pids[ i ] ].pers.netname ) ); + } + else + { + trap_SendServerCommand( ent-g_entities, va( "print \"[skipnotify]" + "unignore: %s^7 is not on your ignore list\n\"", + level.clients[ pids[ i ] ].pers.netname ) ); + } + } + } +} + + /* + ================= + Cmd_Share_f + ================= + */ + void Cmd_Share_f( gentity_t *ent ) + { + int i, clientNum = 0, creds = 0, skipargs = 0; + int clientNums[ MAX_CLIENTS ] = { -1 }; + char cmd[ 12 ]; + char arg1[ MAX_STRING_TOKENS ]; + char arg2[ MAX_STRING_TOKENS ]; + pTeam_t team; + + if( !ent || !ent->client || ( ent->client->pers.teamSelection == PTE_NONE ) ) + { + return; + } + + if( !g_allowShare.integer ) + { + trap_SendServerCommand( ent-g_entities, "print \"Share has been disabled.\n\"" ); + return; + } + + if( g_floodMinTime.integer ) + if ( G_Flood_Limited( ent ) ) + { + trap_SendServerCommand( ent-g_entities, "print \"Your chat is flood-limited; wait before chatting again\n\"" ); + return; + } + + team = ent->client->pers.teamSelection; + + G_SayArgv( 0, cmd, sizeof( cmd ) ); + if( !Q_stricmp( cmd, "say" ) || !Q_stricmp( cmd, "say_team" ) ) + { + skipargs = 1; + G_SayArgv( 1, cmd, sizeof( cmd ) ); + } + + // target player name is in arg1 + G_SayArgv( 1+skipargs, arg1, sizeof( arg1 ) ); + // amount to be shared is in arg2 + G_SayArgv( 2+skipargs, arg2, sizeof( arg2 ) ); + + if( arg1[0] && !strchr( arg1, ';' ) && Q_stricmp( arg1, "target_in_aim" ) ) + { + //check arg1 is a number + for( i = 0; arg1[ i ]; i++ ) + { + if( arg1[ i ] < '0' || arg1[ i ] > '9' ) + { + clientNum = -1; + break; + } + } + + if( clientNum >= 0 ) + { + clientNum = atoi( arg1 ); + } + else if( G_ClientNumbersFromString( arg1, clientNums ) == 1 ) + { + // there was one partial name match + clientNum = clientNums[ 0 ]; + } + else + { + // look for an exact name match before bailing out + clientNum = G_ClientNumberFromString( ent, arg1 ); + if( clientNum == -1 ) + { + trap_SendServerCommand( ent-g_entities, + "print \"share: invalid player name specified.\n\"" ); + return; + } + } + } + else // arg1 not set + { + vec3_t forward, end; + trace_t tr; + gentity_t *traceEnt; + + // trace a teammate + AngleVectors( ent->client->ps.viewangles, forward, NULL, NULL ); + VectorMA( ent->client->ps.origin, 8192 * 16, forward, end ); + + trap_Trace( &tr, ent->client->ps.origin, NULL, NULL, end, ent->s.number, MASK_PLAYERSOLID ); + traceEnt = &g_entities[ tr.entityNum ]; + + if( tr.fraction < 1.0f && traceEnt->client && + ( traceEnt->client->pers.teamSelection == team ) ) + { + clientNum = traceEnt - g_entities; + } + else + { + trap_SendServerCommand( ent-g_entities, + va( "print \"share: aim at a teammate to share %s.\n\"", + ( team == PTE_HUMANS ) ? "credits" : "evolvepoints" ) ); + return; + } + } + + // verify target player team + if( ( clientNum < 0 ) || ( clientNum >= level.maxclients ) || + ( level.clients[ clientNum ].pers.teamSelection != team ) ) + { + trap_SendServerCommand( ent-g_entities, + "print \"share: not a valid player of your team.\n\"" ); + return; + } + + if( !arg2[0] || strchr( arg2, ';' ) ) + { + // default credit count + if( team == PTE_HUMANS ) + { + creds = FREEKILL_HUMAN; + } + else if( team == PTE_ALIENS ) + { + creds = FREEKILL_ALIEN; + } + } + else + { + //check arg2 is a number + for( i = 0; arg2[ i ]; i++ ) + { + if( arg2[ i ] < '0' || arg2[ i ] > '9' ) + { + trap_SendServerCommand( ent-g_entities, + "print \"usage: share [name|slot#] [amount]\n\"" ); + return; + } + } + + // credit count from parameter + creds = atoi( arg2 ); + } + + // player specified "0" to transfer + if( creds <= 0 ) + { + trap_SendServerCommand( ent-g_entities, + "print \"Ooh, you are a generous one, indeed!\n\"" ); + return; + } + + // transfer only credits the player really has + if( creds > ent->client->pers.credit ) + { + creds = ent->client->pers.credit; + } + + // player has no credits + if( creds <= 0 ) + { + trap_SendServerCommand( ent-g_entities, + "print \"Earn some first, lazy gal!\n\"" ); + return; + } + + // allow transfers only up to the credit/evo limit + if( ( team == PTE_HUMANS ) && + ( creds > HUMAN_MAX_CREDITS - level.clients[ clientNum ].pers.credit ) ) + { + creds = HUMAN_MAX_CREDITS - level.clients[ clientNum ].pers.credit; + } + else if( ( team == PTE_ALIENS ) && + ( creds > ALIEN_MAX_KILLS - level.clients[ clientNum ].pers.credit ) ) + { + creds = ALIEN_MAX_KILLS - level.clients[ clientNum ].pers.credit; + } + + // target cannot take any more credits + if( creds <= 0 ) + { + trap_SendServerCommand( ent-g_entities, + va( "print \"share: player cannot receive any more %s.\n\"", + ( team == PTE_HUMANS ) ? "credits" : "evolvepoints" ) ); + return; + } + + // transfer credits + G_AddCreditToClient( ent->client, -creds, qfalse ); + trap_SendServerCommand( ent-g_entities, + va( "print \"share: transferred %d %s to %s^7.\n\"", creds, + ( team == PTE_HUMANS ) ? "credits" : "evolvepoints", + level.clients[ clientNum ].pers.netname ) ); + G_AddCreditToClient( &(level.clients[ clientNum ]), creds, qtrue ); + trap_SendServerCommand( clientNum, + va( "print \"You have received %d %s from %s^7.\n\"", creds, + ( team == PTE_HUMANS ) ? "credits" : "evolvepoints", + ent->client->pers.netname ) ); + + G_LogPrintf( "Share: %i %i %i %d: %s^7 transferred %d%s to %s^7\n", + ent->client->ps.clientNum, + clientNum, + team, + creds, + ent->client->pers.netname, + creds, + ( team == PTE_HUMANS ) ? "c" : "e", + level.clients[ clientNum ].pers.netname ); + } + + /* + ================= + Cmd_Donate_f + + Alms for the poor + ================= + */ + void Cmd_Donate_f( gentity_t *ent ) { + char s[ MAX_TOKEN_CHARS ] = "", *type = "evo(s)"; + int i, value, divisor, portion, new_credits, total=0, + max = ALIEN_MAX_KILLS, *amounts, *totals; + qboolean donated = qtrue; + + if( !ent->client ) return; + + if( !g_allowShare.integer ) + { + trap_SendServerCommand( ent-g_entities, "print \"Donate has been disabled.\n\"" ); + return; + } + + if( g_floodMinTime.integer ) + if ( G_Flood_Limited( ent ) ) + { + trap_SendServerCommand( ent-g_entities, "print \"Your chat is flood-limited; wait before chatting again\n\"" ); + return; + } + + if( ent->client->pers.teamSelection == PTE_ALIENS ) + divisor = level.numAlienClients-1; + else if( ent->client->pers.teamSelection == PTE_HUMANS ) { + divisor = level.numHumanClients-1; + max = HUMAN_MAX_CREDITS; + type = "credit(s)"; + } else { + trap_SendServerCommand( ent-g_entities, + va( "print \"donate: spectators cannot be so gracious\n\"" ) ); + return; + } + + if( divisor < 1 ) { + trap_SendServerCommand( ent-g_entities, + "print \"donate: get yourself some teammates first\n\"" ); + return; + } + + trap_Argv( 1, s, sizeof( s ) ); + value = atoi(s); + if( value <= 0 ) { + trap_SendServerCommand( ent-g_entities, + "print \"donate: very funny\n\"" ); + return; + } + if( value > ent->client->pers.credit) + value = ent->client->pers.credit; + + // allocate memory for distribution amounts + amounts = G_Alloc( level.maxclients * sizeof( int ) ); + totals = G_Alloc( level.maxclients * sizeof( int ) ); + for( i = 0; i < level.maxclients; i++ ) { + amounts[ i ] = 0; + totals[ i ] = 0; + } + + // determine donation amounts for each client + total = value; + while( donated && value ) { + donated = qfalse; + portion = value / divisor; + if( portion < 1 ) portion = 1; + for( i = 0; i < level.maxclients; i++ ) + if( level.clients[ i ].pers.connected == CON_CONNECTED && + ent->client != level.clients + i && + level.clients[ i ].pers.teamSelection == + ent->client->pers.teamSelection ) { + new_credits = level.clients[ i ].pers.credit + portion; + amounts[ i ] = portion; + totals[ i ] += portion; + if( new_credits > max ) { + amounts[ i ] -= new_credits - max; + totals[ i ] -= new_credits - max; + new_credits = max; + } + if( amounts[ i ] ) { + G_AddCreditToClient( &(level.clients[ i ]), amounts[ i ], qtrue ); + donated = qtrue; + value -= amounts[ i ]; + if( value < portion ) break; + } + } + } + + // transfer funds + G_AddCreditToClient( ent->client, value - total, qtrue ); + for( i = 0; i < level.maxclients; i++ ) + if( totals[ i ] ) { + trap_SendServerCommand( i, + va( "print \"%s^7 donated %d %s to you, don't forget to say 'thank you'!\n\"", + ent->client->pers.netname, totals[ i ], type ) ); + } + + G_Free( amounts ); + G_Free( totals ); + + trap_SendServerCommand( ent-g_entities, + va( "print \"Donated %d %s to the cause.\n\"", + total-value, type ) ); + } + +commands_t cmds[ ] = { + // normal commands + { "team", 0, Cmd_Team_f }, + { "vote", CMD_INTERMISSION, Cmd_Vote_f }, + { "ignore", 0, Cmd_Ignore_f }, + { "unignore", 0, Cmd_Ignore_f }, + + // communication commands + { "tell", CMD_MESSAGE, Cmd_Tell_f }, + { "callvote", CMD_MESSAGE, Cmd_CallVote_f }, + { "callteamvote", CMD_MESSAGE|CMD_TEAM, Cmd_CallTeamVote_f }, + { "say_area", CMD_MESSAGE|CMD_TEAM, Cmd_SayArea_f }, + // can be used even during intermission + { "say", CMD_MESSAGE|CMD_INTERMISSION, Cmd_Say_f }, + { "say_team", CMD_MESSAGE|CMD_INTERMISSION, Cmd_Say_f }, + { "say_admins", CMD_MESSAGE|CMD_INTERMISSION, Cmd_Say_f }, + { "a", CMD_MESSAGE|CMD_INTERMISSION, Cmd_Say_f }, + { "m", CMD_MESSAGE|CMD_INTERMISSION, G_PrivateMessage }, + { "mt", CMD_MESSAGE|CMD_INTERMISSION, G_PrivateMessage }, + { "me", CMD_MESSAGE|CMD_INTERMISSION, Cmd_Say_f }, + { "me_team", CMD_MESSAGE|CMD_INTERMISSION, Cmd_Say_f }, + + { "score", CMD_INTERMISSION, ScoreboardMessage }, + { "mystats", CMD_TEAM|CMD_INTERMISSION, Cmd_MyStats_f }, + { "allstats", 0|CMD_INTERMISSION, Cmd_AllStats_f }, + { "teamstatus", CMD_TEAM, Cmd_TeamStatus_f }, + + // cheats + { "give", CMD_CHEAT|CMD_TEAM|CMD_LIVING, Cmd_Give_f }, + { "god", CMD_CHEAT|CMD_TEAM|CMD_LIVING, Cmd_God_f }, + { "notarget", CMD_CHEAT|CMD_TEAM|CMD_LIVING, Cmd_Notarget_f }, + { "noclip", CMD_CHEAT|CMD_TEAM|CMD_LIVING, Cmd_Noclip_f }, + { "levelshot", CMD_CHEAT, Cmd_LevelShot_f }, + { "setviewpos", CMD_CHEAT, Cmd_SetViewpos_f }, + { "destroy", CMD_CHEAT|CMD_TEAM|CMD_LIVING, Cmd_Destroy_f }, + + { "kill", CMD_TEAM|CMD_LIVING, Cmd_Kill_f }, + + // game commands + { "ptrcverify", CMD_NOTEAM, Cmd_PTRCVerify_f }, + { "ptrcrestore", CMD_NOTEAM, Cmd_PTRCRestore_f }, + + { "follow", 0, Cmd_Follow_f }, + { "follownext", 0, Cmd_FollowCycle_f }, + { "followprev", 0, Cmd_FollowCycle_f }, + + { "where", CMD_TEAM, Cmd_Where_f }, + { "teamvote", CMD_TEAM, Cmd_TeamVote_f }, + { "class", CMD_TEAM, Cmd_Class_f }, + + { "build", CMD_TEAM|CMD_LIVING, Cmd_Build_f }, + { "deconstruct", CMD_TEAM|CMD_LIVING, Cmd_Destroy_f }, + { "mark", CMD_TEAM|CMD_LIVING, Cmd_Mark_f }, + + { "buy", CMD_HUMAN|CMD_LIVING, Cmd_Buy_f }, + { "sell", CMD_HUMAN|CMD_LIVING, Cmd_Sell_f }, + { "itemact", CMD_HUMAN|CMD_LIVING, Cmd_ActivateItem_f }, + { "itemdeact", CMD_HUMAN|CMD_LIVING, Cmd_DeActivateItem_f }, + { "itemtoggle", CMD_HUMAN|CMD_LIVING, Cmd_ToggleItem_f }, + { "reload", CMD_TEAM|CMD_LIVING, Cmd_Reload_f }, + { "boost", 0, Cmd_Boost_f }, + { "share", CMD_TEAM, Cmd_Share_f }, + { "donate", CMD_TEAM, Cmd_Donate_f }, + { "protect", CMD_TEAM|CMD_LIVING, Cmd_Protect_f }, + { "resign", CMD_TEAM, Cmd_Resign_f }, + { "builder", 0, Cmd_Builder_f } +}; +static int numCmds = sizeof( cmds ) / sizeof( cmds[ 0 ] ); + +/* +================= +ClientCommand +================= +*/ +void ClientCommand( int clientNum ) +{ + gentity_t *ent; + char cmd[ MAX_TOKEN_CHARS ]; + int i; + + ent = g_entities + clientNum; + if( !ent->client ) + return; // not fully in game yet + + trap_Argv( 0, cmd, sizeof( cmd ) ); + + for( i = 0; i < numCmds; i++ ) + { + if( Q_stricmp( cmd, cmds[ i ].cmdName ) == 0 ) + break; + } + + if( i == numCmds ) + { + if( !G_admin_cmd_check( ent, qfalse ) ) + trap_SendServerCommand( clientNum, + va( "print \"Unknown command %s\n\"", cmd ) ); + return; + } + + // do tests here to reduce the amount of repeated code + + if( !( cmds[ i ].cmdFlags & CMD_INTERMISSION ) && ( level.intermissiontime || level.paused ) ) + return; + + if( cmds[ i ].cmdFlags & CMD_CHEAT && !g_cheats.integer ) + { + trap_SendServerCommand( clientNum, + "print \"Cheats are not enabled on this server\n\"" ); + return; + } + + if( cmds[ i ].cmdFlags & CMD_MESSAGE && G_IsMuted( ent->client ) ) + { + trap_SendServerCommand( clientNum, + "print \"You are muted and cannot use message commands.\n\"" ); + return; + } + + if( cmds[ i ].cmdFlags & CMD_TEAM && + ent->client->pers.teamSelection == PTE_NONE ) + { + trap_SendServerCommand( clientNum, "print \"Join a team first\n\"" ); + return; + } + + if( cmds[ i ].cmdFlags & CMD_NOTEAM && + ent->client->pers.teamSelection != PTE_NONE ) + { + trap_SendServerCommand( clientNum, + "print \"Cannot use this command when on a team\n\"" ); + return; + } + + if( cmds[ i ].cmdFlags & CMD_ALIEN && + ent->client->pers.teamSelection != PTE_ALIENS ) + { + trap_SendServerCommand( clientNum, + "print \"Must be alien to use this command\n\"" ); + return; + } + + if( cmds[ i ].cmdFlags & CMD_HUMAN && + ent->client->pers.teamSelection != PTE_HUMANS ) + { + trap_SendServerCommand( clientNum, + "print \"Must be human to use this command\n\"" ); + return; + } + + if( cmds[ i ].cmdFlags & CMD_LIVING && + ( ent->client->ps.stats[ STAT_HEALTH ] <= 0 || + ent->client->sess.sessionTeam == TEAM_SPECTATOR ) ) + { + trap_SendServerCommand( clientNum, + "print \"Must be living to use this command\n\"" ); + return; + } + + cmds[ i ].cmdHandler( ent ); +} + +int G_SayArgc( void ) +{ + int c = 0; + char *s; + + s = ConcatArgs( 0 ); + while( 1 ) + { + while( *s == ' ' ) + s++; + if( !*s ) + break; + c++; + while( *s && *s != ' ' ) + s++; + } + return c; +} + +qboolean G_SayArgv( int n, char *buffer, int bufferLength ) +{ + int bc = 0; + int c = 0; + char *s; + + if( bufferLength < 1 ) + return qfalse; + if( n < 0 ) + return qfalse; + s = ConcatArgs( 0 ); + while( c < n ) + { + while( *s == ' ' ) + s++; + if( !*s ) + break; + c++; + while( *s && *s != ' ' ) + s++; + } + if( c < n ) + return qfalse; + while( *s == ' ' ) + s++; + if( !*s ) + return qfalse; + //memccpy( buffer, s, ' ', bufferLength ); + while( bc < bufferLength - 1 && *s && *s != ' ' ) + buffer[ bc++ ] = *s++; + buffer[ bc ] = 0; + return qtrue; +} + +char *G_SayConcatArgs( int start ) +{ + char *s; + int c = 0; + + s = ConcatArgs( 0 ); + while( c < start ) + { + while( *s == ' ' ) + s++; + if( !*s ) + break; + c++; + while( *s && *s != ' ' ) + s++; + } + while( *s == ' ' ) + s++; + return s; +} + +void G_DecolorString( char *in, char *out ) +{ + while( *in ) { + if( *in == 27 || *in == '^' ) { + in++; + if( *in ) + in++; + continue; + } + *out++ = *in++; + } + *out = '\0'; +} + +void G_ParseEscapedString( char *buffer ) +{ + int i = 0; + int j = 0; + + while( buffer[i] ) + { + if(!buffer[i]) break; + + if(buffer[i] == '\\') + { + if(buffer[i + 1] == '\\') + buffer[j] = buffer[++i]; + else if(buffer[i + 1] == 'n') + { + buffer[j] = '\n'; + i++; + } + else + buffer[j] = buffer[i]; + } + else + buffer[j] = buffer[i]; + + i++; + j++; + } + buffer[j] = 0; +} + +void G_WordWrap( char *buffer, int maxwidth ) +{ + char out[ MAX_STRING_CHARS ]; + int i = 0; + int j = 0; + int k; + int linecount = 0; + int currentcolor = 7; + + while ( buffer[ j ]!='\0' ) + { + if( i == ( MAX_STRING_CHARS - 1 ) ) + break; + + //If it's the start of a new line, copy over the color code, + //but not if we already did it, or if the text at the start of the next line is also a color code + if( linecount == 0 && i>2 && out[ i-2 ] != Q_COLOR_ESCAPE && out[ i-1 ] != Q_COLOR_ESCAPE ) + { + out[ i ] = Q_COLOR_ESCAPE; + out[ i + 1 ] = '0' + currentcolor; + i+=2; + continue; + } + + if( linecount < maxwidth ) + { + out[ i ] = buffer[ j ]; + if( out[ i ] == '\n' ) + { + linecount = 0; + } + else if( Q_IsColorString( &buffer[j] ) ) + { + currentcolor = buffer[j+1] - '0'; + } + else + linecount++; + + //If we're at a space and getting close to a line break, look ahead and make sure that there isn't already a \n or a closer space coming. If not, break here. + if( out[ i ] == ' ' && linecount >= (maxwidth - 10 ) ) + { + qboolean foundbreak = qfalse; + for( k = i+1; k < maxwidth; k++ ) + { + if( !buffer[ k ] ) + continue; + if( buffer[ k ] == '\n' || buffer[ k ] == ' ' ) + foundbreak = qtrue; + } + if( !foundbreak ) + { + out [ i ] = '\n'; + linecount = 0; + } + } + + i++; + j++; + } + else + { + out[ i ] = '\n'; + i++; + linecount = 0; + } + } + out[ i ] = '\0'; + + + strcpy( buffer, out ); +} + +void G_PrivateMessage( gentity_t *ent ) +{ + int pids[ MAX_CLIENTS ]; + int ignoreids[ MAX_CLIENTS ]; + char name[ MAX_NAME_LENGTH ]; + char cmd[ 12 ]; + char str[ MAX_STRING_CHARS ]; + char *msg; + char color; + int pcount, matches, ignored = 0; + int i; + int skipargs = 0; + qboolean teamonly = qfalse; + gentity_t *tmpent; + + if( !g_privateMessages.integer && ent ) + { + ADMP( "Sorry, but private messages have been disabled\n" ); + return; + } + + if( g_floodMinTime.integer ) + if ( G_Flood_Limited( ent ) ) + { + trap_SendServerCommand( ent-g_entities, "print \"Your chat is flood-limited; wait before chatting again\n\"" ); + return; + } + + G_SayArgv( 0, cmd, sizeof( cmd ) ); + if( !Q_stricmp( cmd, "say" ) || !Q_stricmp( cmd, "say_team" ) ) + { + skipargs = 1; + G_SayArgv( 1, cmd, sizeof( cmd ) ); + } + if( G_SayArgc( ) < 3+skipargs ) + { + ADMP( va( "usage: %s [name|slot#] [message]\n", cmd ) ); + return; + } + + if( !Q_stricmp( cmd, "mt" ) || !Q_stricmp( cmd, "/mt" ) ) + teamonly = qtrue; + + G_SayArgv( 1+skipargs, name, sizeof( name ) ); + msg = G_SayConcatArgs( 2+skipargs ); + pcount = G_ClientNumbersFromString( name, pids ); + + if( ent ) + { + int count = 0; + + for( i=0; i < pcount; i++ ) + { + tmpent = &g_entities[ pids[ i ] ]; + + if( teamonly && !OnSameTeam( ent, tmpent ) ) + continue; + + // Ignore sending to invisible players + if( tmpent->client->sess.invisible == qtrue && !G_admin_permission( ent, "invisible" ) ) + continue; + + // Ignore sending to non-invisible-capable players while invisible + if( ent->client->sess.invisible == qtrue && !G_admin_permission( tmpent, "invisible" ) ) + continue; + + if( BG_ClientListTest( &tmpent->client->sess.ignoreList, + ent-g_entities ) ) + { + ignoreids[ ignored++ ] = pids[ i ]; + continue; + } + + pids[ count ] = pids[ i ]; + count++; + } + matches = count; + } + else + { + matches = pcount; + } + + color = teamonly ? COLOR_CYAN : COLOR_YELLOW; + + if( !Q_stricmp( name, "console" ) ) + { + ADMP( va( "^%cPrivate message: ^7%s\n", color, msg ) ); + ADMP( va( "^%csent to Console.\n", color ) ); + + G_LogPrintf( "privmsg: %s^7: Console: ^6%s^7\n", + ( ent ) ? ent->client->pers.netname : "Console", msg ); + + return; + } + + Q_strncpyz( str, + va( "^%csent to %i player%s: ^7", color, matches, + ( matches == 1 ) ? "" : "s" ), + sizeof( str ) ); + + for( i=0; i < matches; i++ ) + { + tmpent = &g_entities[ pids[ i ] ]; + + if( i > 0 ) + Q_strcat( str, sizeof( str ), "^7, " ); + Q_strcat( str, sizeof( str ), tmpent->client->pers.netname ); + trap_SendServerCommand( pids[ i ], va( + "chat \"%s^%c -> ^7%s^7: (%d recipients): ^%c%s^7\" %i", + ( ent ) ? ent->client->pers.netname : "console", + color, + name, + matches, + color, + msg, + ent ? ent-g_entities : -1 ) ); + + trap_SendServerCommand( pids[ i ], va( + "cp \"^%cprivate message from ^7%s^7\"", color, + ( ent ) ? ent->client->pers.netname : "console" ) ); + } + + if( !matches ) + ADMP( va( "^3No player matching ^7\'%s^7\' ^3to send message to.\n", + name ) ); + else + { + if( ent ) + ADMP( va( "^%cPrivate message: ^7%s\n", color, msg ) ); + + ADMP( va( "%s\n", str ) ); + + G_LogPrintf( "%s: %s^7: %s^7: %s\n", + ( teamonly ) ? "tprivmsg" : "privmsg", + ( ent ) ? ent->client->pers.netname : "console", + name, msg ); + } + + if( ignored ) + { + Q_strncpyz( str, va( "^%cignored by %i player%s: ^7", color, ignored, + ( ignored == 1 ) ? "" : "s" ), sizeof( str ) ); + for( i=0; i < ignored; i++ ) + { + tmpent = &g_entities[ ignoreids[ i ] ]; + if( i > 0 ) + Q_strcat( str, sizeof( str ), "^7, " ); + Q_strcat( str, sizeof( str ), tmpent->client->pers.netname ); + } + ADMP( va( "%s\n", str ) ); + } +} + + /* + ================= + Cmd_Builder_f + ================= + */ + void Cmd_Builder_f( gentity_t *ent ) + { + vec3_t forward, right, up; + vec3_t start, end; + trace_t tr; + gentity_t *traceEnt; + char bdnumbchr[21]; + + AngleVectors( ent->client->ps.viewangles, forward, right, up ); + if( ent->client->pers.teamSelection != PTE_NONE ) + CalcMuzzlePoint( ent, forward, right, up, start ); + else + VectorCopy( ent->client->ps.origin, start ); + VectorMA( start, 1000, forward, end ); + + trap_Trace( &tr, start, NULL, NULL, end, ent->s.number, MASK_PLAYERSOLID ); + traceEnt = &g_entities[ tr.entityNum ]; + + Com_sprintf( bdnumbchr, sizeof(bdnumbchr), "%i", traceEnt->bdnumb ); + + if( tr.fraction < 1.0f && ( traceEnt->s.eType == ET_BUILDABLE ) ) + { + if( G_admin_permission( ent, "buildlog" ) ) { + trap_SendServerCommand( ent-g_entities, va( + "print \"^5/builder:^7 ^3Building:^7 %s ^3Built By:^7 %s^7 ^3Buildlog Number:^7 %s^7\n\"", + BG_FindHumanNameForBuildable( traceEnt->s.modelindex ), + (traceEnt->bdnumb != -1) ? G_FindBuildLogName( traceEnt->bdnumb ) : "<world>", + (traceEnt->bdnumb != -1) ? bdnumbchr : "none" ) ); + } + else + { + trap_SendServerCommand( ent-g_entities, va( + "print \"^5/builder:^7 ^3Building:^7 %s ^3Built By:^7 %s^7\n\"", + BG_FindHumanNameForBuildable( traceEnt->s.modelindex ), + (traceEnt->bdnumb != -1) ? G_FindBuildLogName( traceEnt->bdnumb ) : "<world>" ) ); + } + } + else + { + trap_SendServerCommand( ent-g_entities, "print \"^5/builder:^7 No structure found in your crosshair. Please face a structure and try again.\n\"" ); + } + } + +void G_CP( gentity_t *ent ) +{ + int i; + char buffer[MAX_STRING_CHARS]; + char prefixes[MAX_STRING_CHARS] = ""; + char wrappedtext[ MAX_STRING_CHARS ] = ""; + char *ptr; + char *text; + qboolean sendAliens = qtrue; + qboolean sendHumans = qtrue; + qboolean sendSpecs = qtrue; + Q_strncpyz( buffer, ConcatArgs( 1 ), sizeof( buffer ) ); + G_ParseEscapedString( buffer ); + + if( strstr( buffer, "!cp" ) ) + { + ptr = buffer; + while( *ptr != '!' ) + ptr++; + ptr+=4; + + Q_strncpyz( buffer, ptr, sizeof(buffer) ); + } + + text = buffer; + + ptr = buffer; + while( *ptr == ' ' ) + ptr++; + if( *ptr == '-' ) + { + sendAliens = qfalse; + sendHumans = qfalse; + sendSpecs = qfalse; + Q_strcat( prefixes, sizeof( prefixes ), " " ); + ptr++; + + while( *ptr && *ptr != ' ' ) + { + if( !sendAliens && ( *ptr == 'a' || *ptr == 'A' ) ) + { + sendAliens = qtrue; + Q_strcat( prefixes, sizeof( prefixes ), "[^1A^7]" ); + } + if( !sendHumans && ( *ptr == 'h' || *ptr == 'H' ) ) + { + sendHumans = qtrue; + Q_strcat( prefixes, sizeof( prefixes ), "[^4H^7]" ); + } + if( !sendSpecs && ( *ptr == 's' || *ptr == 'S' ) ) + { + sendSpecs = qtrue; + Q_strcat( prefixes, sizeof( prefixes ), "[^3S^7]" ); + } + ptr++; + } + if( *ptr ) text = ptr+1; + else text = ptr; + } + + strcpy( wrappedtext, text ); + + if( strlen( text ) == 0 ) return; + + G_WordWrap( wrappedtext, 50 ); + + for( i = 0; i < level.maxclients; i++ ) + { + if( level.clients[ i ].pers.connected == CON_DISCONNECTED ) + continue; + + if( ( !sendAliens && level.clients[ i ].pers.teamSelection == PTE_ALIENS ) || + ( !sendHumans && level.clients[ i ].pers.teamSelection == PTE_HUMANS ) || + ( !sendSpecs && level.clients[ i ].pers.teamSelection == PTE_NONE ) ) + { + if( G_admin_permission( &g_entities[ i ], ADMF_ADMINCHAT ) ) + { + trap_SendServerCommand( i, va("print \"^6[Admins]^7 CP to other team%s: %s \n\"", prefixes, text ) ); + } + continue; + } + + trap_SendServerCommand( i, va( "cp \"%s\"", wrappedtext ) ); + trap_SendServerCommand( i, va( "print \"%s^7 CP%s: %s\n\"", ( ent ? G_admin_adminPrintName( ent ) : "console" ), prefixes, text ) ); + } + + G_Printf( "cp: %s\n", ConcatArgs( 1 ) ); +} + +/* +================= +G_IsMuted + +Check if a player is muted +================= +*/ +qboolean G_IsMuted( gclient_t *client ) +{ + qboolean muteState = qfalse; + + //check if mute has expired + if( client->pers.muteExpires ) { + if( client->pers.muteExpires < level.time ) + { + client->pers.muted = qfalse; + client->pers.muteExpires = 0; + } + } + + if( client->pers.muted ) + muteState = qtrue; + + return muteState; +} + +/* +================== +G_TeamKill_Repent + +Determine whether a players team kill activity is high +================== +*/ + +qboolean G_TeamKill_Repent( gentity_t *ent ) +{ + int millisSinceLastTeamKill; + + // Doesn't work if g_teamKillThreshold isn't set + if( !g_teamKillThreshold.integer || + g_teamKillThreshold.integer == 0 ) + return qfalse; + + // Doesn't work when game is paused + if( level.paused ) + return qfalse; + + millisSinceLastTeamKill = level.time - ent->client->pers.lastTeamKillTime; + if( millisSinceLastTeamKill < 30000 ) + ent->client->pers.teamKillDemerits++; + else + { + ent->client->pers.teamKillDemerits--; + if( ent->client->pers.teamKillDemerits < 0 ) + ent->client->pers.teamKillDemerits = 0; + } + + ent->client->pers.lastTeamKillTime = level.time; + + if ( ent->client->pers.teamKillDemerits >= ( g_teamKillThreshold.integer + 2 ) ) + trap_SendConsoleCommand( 0, va( "!ban %s 30m team killing\n", ent->client->pers.ip ) ); + else if ( ent->client->pers.teamKillDemerits == ( g_teamKillThreshold.integer + 1 ) ) + trap_SendConsoleCommand( 0, va( "!warn %i team killing\n", ent->client->ps.clientNum ) ); + else if ( ent->client->pers.teamKillDemerits == g_teamKillThreshold.integer ) + G_AdminsPrintf( "Team killer %s^7 has team killed ^6%i^7 times.\n", + ent->client->pers.netname, + ent->client->pers.statscounters.teamkills ); + + return qfalse; +} diff --git a/src/game/g_combat.c b/src/game/g_combat.c new file mode 100644 index 0000000..0cc079a --- /dev/null +++ b/src/game/g_combat.c @@ -0,0 +1,1761 @@ +/* +=========================================================================== +Copyright (C) 1999-2005 Id Software, Inc. +Copyright (C) 2000-2006 Tim Angus + +This file is part of Tremulous. + +Tremulous is free software; you can redistribute it +and/or modify it under the terms of the GNU General Public License as +published by the Free Software Foundation; either version 2 of the License, +or (at your option) any later version. + +Tremulous is distributed in the hope that it will be +useful, but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with Tremulous; if not, write to the Free Software +Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA +=========================================================================== +*/ + +#include "g_local.h" + +damageRegion_t g_damageRegions[ PCL_NUM_CLASSES ][ MAX_LOCDAMAGE_REGIONS ]; +int g_numDamageRegions[ PCL_NUM_CLASSES ]; + +armourRegion_t g_armourRegions[ UP_NUM_UPGRADES ][ MAX_ARMOUR_REGIONS ]; +int g_numArmourRegions[ UP_NUM_UPGRADES ]; + +/* +============ +AddScore + +Adds score to both the client and his team +============ +*/ +void AddScore( gentity_t *ent, int score ) +{ + if( !ent->client ) + return; + + ent->client->ps.persistant[ PERS_SCORE ] += score; + CalculateRanks( ); +} + +/* +================== +LookAtKiller +================== +*/ +void LookAtKiller( gentity_t *self, gentity_t *inflictor, gentity_t *attacker ) +{ + vec3_t dir; + + if ( attacker && attacker != self ) + VectorSubtract( attacker->s.pos.trBase, self->s.pos.trBase, dir ); + else if( inflictor && inflictor != self ) + VectorSubtract( inflictor->s.pos.trBase, self->s.pos.trBase, dir ); + else + { + self->client->ps.stats[ STAT_VIEWLOCK ] = self->s.angles[ YAW ]; + return; + } + + self->client->ps.stats[ STAT_VIEWLOCK ] = vectoyaw( dir ); +} + +// these are just for logging, the client prints its own messages +char *modNames[ ] = +{ + "MOD_UNKNOWN", + "MOD_SHOTGUN", + "MOD_BLASTER", + "MOD_PAINSAW", + "MOD_MACHINEGUN", + "MOD_CHAINGUN", + "MOD_PRIFLE", + "MOD_MDRIVER", + "MOD_LASGUN", + "MOD_LCANNON", + "MOD_LCANNON_SPLASH", + "MOD_FLAMER", + "MOD_FLAMER_SPLASH", + "MOD_GRENADE", + "MOD_WATER", + "MOD_SLIME", + "MOD_LAVA", + "MOD_CRUSH", + "MOD_TELEFRAG", + "MOD_FALLING", + "MOD_SUICIDE", + "MOD_TARGET_LASER", + "MOD_TRIGGER_HURT", + + "MOD_ABUILDER_CLAW", + "MOD_LEVEL0_BITE", + "MOD_LEVEL1_CLAW", + "MOD_LEVEL1_PCLOUD", + "MOD_LEVEL3_CLAW", + "MOD_LEVEL3_POUNCE", + "MOD_LEVEL3_BOUNCEBALL", + "MOD_LEVEL2_CLAW", + "MOD_LEVEL2_ZAP", + "MOD_LEVEL4_CLAW", + "MOD_LEVEL4_CHARGE", + + "MOD_SLOWBLOB", + "MOD_POISON", + "MOD_SWARM", + + "MOD_HSPAWN", + "MOD_TESLAGEN", + "MOD_MGTURRET", + "MOD_REACTOR", + + "MOD_ASPAWN", + "MOD_ATUBE", + "MOD_OVERMIND", + "MOD_SLAP" +}; + +/* +================== +player_die +================== +*/ +void player_die( gentity_t *self, gentity_t *inflictor, gentity_t *attacker, int damage, int meansOfDeath ) +{ + gentity_t *ent; + int anim; + int killer; + int i, j; + char *killerName, *obit; + float totalTK = 0; + float totalDamage = 0.0f; + float percentDamage = 0.0f; + gentity_t *player; + qboolean tk = qfalse; + + + if( self->client->ps.pm_type == PM_DEAD ) + return; + + + if( level.intermissiontime ) + return; + + self->client->ps.pm_type = PM_DEAD; + self->suicideTime = 0; + + if( attacker ) + { + killer = attacker->s.number; + + if( attacker->client ) + { + killerName = attacker->client->pers.netname; + tk = ( attacker != self && attacker->client->ps.stats[ STAT_PTEAM ] + == self->client->ps.stats[ STAT_PTEAM ] ); + + if( attacker != self && attacker->client->ps.stats[ STAT_PTEAM ] == self->client->ps.stats[ STAT_PTEAM ] ) + { + attacker->client->pers.statscounters.teamkills++; + if( attacker->client->pers.teamSelection == PTE_ALIENS ) + { + level.alienStatsCounters.teamkills++; + } + else if( attacker->client->pers.teamSelection == PTE_HUMANS ) + { + level.humanStatsCounters.teamkills++; + } + } + + } + else + killerName = "<non-client>"; + } + else + { + killer = ENTITYNUM_WORLD; + killerName = "<world>"; + } + + if( killer < 0 || killer >= MAX_CLIENTS ) + { + killer = ENTITYNUM_WORLD; + killerName = "<world>"; + } + + if( meansOfDeath < 0 || meansOfDeath >= sizeof( modNames ) / sizeof( modNames[0] ) ) + obit = "<bad obituary>"; + else + obit = modNames[ meansOfDeath ]; + + G_LogPrintf("Kill: %i %i %i: %s^7 killed %s^7 by %s\n", + killer, self->s.number, meansOfDeath, killerName, + self->client->pers.netname, obit ); + + //TA: close any menus the client has open + G_CloseMenus( self->client->ps.clientNum ); + + //TA: deactivate all upgrades + for( i = UP_NONE + 1; i < UP_NUM_UPGRADES; i++ ) + BG_DeactivateUpgrade( i, self->client->ps.stats ); + + if( meansOfDeath == MOD_SLAP ) + { + trap_SendServerCommand( -1, + va( "print \"%s^7 felt %s^7's authority\n\"", + self->client->pers.netname, killerName ) ); + goto finish_dying; + } + + // broadcast the death event to everyone + if( !tk ) + { + ent = G_TempEntity( self->r.currentOrigin, EV_OBITUARY ); + ent->s.eventParm = meansOfDeath; + ent->s.otherEntityNum = self->s.number; + ent->s.otherEntityNum2 = killer; + ent->r.svFlags = SVF_BROADCAST; // send to everyone + } + else if( attacker && attacker->client ) + { + // tjw: obviously this is a hack and belongs in the client, but + // this works as a temporary fix. + trap_SendServerCommand( -1, + va( "print \"%s^7 was killed by ^1TEAMMATE^7 %s^7 (Did %d damage to %d max)\n\"", + self->client->pers.netname, attacker->client->pers.netname, self->client->tkcredits[ attacker->s.number ], self->client->ps.stats[ STAT_MAX_HEALTH ] ) ); + trap_SendServerCommand( attacker - g_entities, + va( "cp \"You killed ^1TEAMMATE^7 %s\"", self->client->pers.netname ) ); + G_LogOnlyPrintf("%s^7 was killed by ^1TEAMMATE^7 %s^7 (Did %d damage to %d max)\n", + self->client->pers.netname, attacker->client->pers.netname, self->client->tkcredits[ attacker->s.number ], self->client->ps.stats[ STAT_MAX_HEALTH ] ); + G_TeamKill_Repent( attacker ); + } + + self->enemy = attacker; + + self->client->ps.persistant[ PERS_KILLED ]++; + self->client->pers.statscounters.deaths++; + if( self->client->pers.teamSelection == PTE_ALIENS ) + { + level.alienStatsCounters.deaths++; + } + else if( self->client->pers.teamSelection == PTE_HUMANS ) + { + level.humanStatsCounters.deaths++; + } + + if( attacker && attacker->client ) + { + attacker->client->lastkilled_client = self->s.number; + + if( g_killerHP.integer || + ( g_devmapKillerHP.integer && g_cheats.integer ) ) + { + trap_SendServerCommand( self-g_entities, + va( "print \"Your killer, %s^7, had %3i HP.\n\"", + killerName, attacker->health ) ); + } + + if( attacker == self || OnSameTeam( self, attacker ) ) + { + AddScore( attacker, -1 ); + + // Normal teamkill penalty + if( !g_retribution.integer ) + { + if( attacker->client->ps.stats[ STAT_PTEAM ] == PTE_ALIENS ) + G_AddCreditToClient( attacker->client, -FREEKILL_ALIEN, qtrue ); + else if( attacker->client->ps.stats[ STAT_PTEAM ] == PTE_HUMANS ) + G_AddCreditToClient( attacker->client, -FREEKILL_HUMAN, qtrue ); + } + } + else + { + AddScore( attacker, 1 ); + + attacker->client->lastKillTime = level.time; + attacker->client->pers.statscounters.kills++; + if( attacker->client->pers.teamSelection == PTE_ALIENS ) + { + level.alienStatsCounters.kills++; + } + else if( attacker->client->pers.teamSelection == PTE_HUMANS ) + { + level.humanStatsCounters.kills++; + } + } + + if( attacker == self ) + { + attacker->client->pers.statscounters.suicides++; + if( attacker->client->pers.teamSelection == PTE_ALIENS ) + { + level.alienStatsCounters.suicides++; + } + else if( attacker->client->pers.teamSelection == PTE_HUMANS ) + { + level.humanStatsCounters.suicides++; + } + } + } + else if( attacker->s.eType != ET_BUILDABLE ) + AddScore( self, -1 ); + + //total up all the damage done by every client + for( i = 0; i < MAX_CLIENTS; i++ ) + { + totalDamage += (float)self->credits[ i ]; + totalTK += (float)self->client->tkcredits[ i ]; + } + // punish players for damaging teammates + if ( g_retribution.integer && totalTK ) + { + int totalPrice; + int max = HUMAN_MAX_CREDITS; + + if ( self->client->ps.stats[ STAT_PTEAM ] == PTE_ALIENS ) + { + totalPrice = BG_ClassCanEvolveFromTo( PCL_ALIEN_LEVEL0, self->client->ps.stats[ STAT_PCLASS ], ALIEN_MAX_KILLS, 0 ); + max = ALIEN_MAX_KILLS; + } + else + { + totalPrice = BG_GetValueOfEquipment( &self->client->ps ); + } + + if ( self->client->ps.persistant[ PERS_CREDIT ] + totalPrice > max ) + totalPrice = max - self->client->ps.persistant[ PERS_CREDIT ]; + + if ( totalPrice > 0 ) + { + totalTK += totalDamage; + if( totalTK < self->client->ps.stats[ STAT_MAX_HEALTH ] ) + totalTK = self->client->ps.stats[ STAT_MAX_HEALTH ]; + + if ( self->client->ps.stats[ STAT_PTEAM ] == PTE_HUMANS ) + { + for ( i = 0; i < MAX_CLIENTS; i++ ) + { + int price; + // no retribution if self damage or enemy damage or building damage or no damage from this client + if ( i == self - g_entities || !g_entities[ i ].client || + !OnSameTeam( &g_entities[ i ], self ) || + !self->client->tkcredits[ i ] ) + continue; + + // calculate retribution price (rounded up) + price = ( totalPrice * self->client->tkcredits[ i ] ) / totalTK + 0.5f; + self->client->tkcredits[ i ] = 0; + + // check for enough credits + if ( g_entities[ i ].client->ps.persistant[ PERS_CREDIT ] < price ) + price = g_entities[ i ].client->ps.persistant[ PERS_CREDIT ]; + if ( price ) + { + G_AddCreditToClient( self->client, price, qtrue ); + G_AddCreditToClient( g_entities[ i ].client, -price, qtrue ); + + trap_SendServerCommand( self->client->ps.clientNum, + va( "print \"Received ^3%d credits ^7from %s ^7in retribution.\n\"", + price, g_entities[ i ].client->pers.netname ) ); + trap_SendServerCommand( g_entities[ i ].client->ps.clientNum, + va( "print \"Transfered ^3%d credits ^7to %s ^7in retribution.\n\"", + price, self->client->pers.netname ) ); + } + } + } + else + { + int toPay[ MAX_CLIENTS ] = { 0 }; + int frags = totalPrice; + int damageForEvo = totalTK / totalPrice; + for ( i = 0; i < MAX_CLIENTS; i++ ) + { + // no retribution if self damage or enemy damage or building damage or no damage from this client + if ( i == self - g_entities || !g_entities[ i ].client || + !OnSameTeam( &g_entities[ i ], self ) || + !self->client->tkcredits[ i ] ) + continue; + + // find out how many full evos this client needs to pay + toPay[ i ] = ( totalPrice * self->client->tkcredits[ i ] ) / totalTK; + if ( toPay[ i ] > g_entities[ i ].client->ps.persistant[ PERS_CREDIT ] ) + toPay[ i ] = g_entities[ i ].client->ps.persistant[ PERS_CREDIT ]; + frags -= toPay[ i ]; + self->client->tkcredits[ i ] -= damageForEvo * toPay[ i ]; + } + + // if we have not met the evo count, continue stealing evos + while ( 1 ) + { + int maximum = 0; + int topClient = 0; + for ( i = 0; i < MAX_CLIENTS; i++ ) + { + if ( self->client->tkcredits[ i ] > maximum && g_entities[ i ].client->ps.persistant[ PERS_CREDIT ] ) + { + maximum = self->client->tkcredits[ i ]; + topClient = i; + } + } + if ( !maximum ) + break; + toPay[ topClient ]++; + self->client->tkcredits[ topClient ] = 0; + frags--; + if ( !frags ) + break; + } + + // now move the evos around + for ( i = 0; i < MAX_CLIENTS; i++ ) + { + if ( !toPay[ i ] ) + continue; + + G_AddCreditToClient( self->client, toPay[ i ], qtrue ); + G_AddCreditToClient( g_entities[ i ].client, -toPay[ i ], qtrue ); + + trap_SendServerCommand( self->client->ps.clientNum, + va( "print \"Received ^3%d ^7evos from %s ^7in retribution.\n\"", + toPay[ i ], g_entities[ i ].client->pers.netname ) ); + trap_SendServerCommand( g_entities[ i ].client->ps.clientNum, + va( "print \"Transfered ^3%d ^7evos to %s ^7in retribution.\n\"", + toPay[ i ], self->client->pers.netname ) ); + } + } + } + } + + // if players did more than DAMAGE_FRACTION_FOR_KILL increment the stage counters + if( !OnSameTeam( self, attacker ) && totalDamage >= ( self->client->ps.stats[ STAT_MAX_HEALTH ] * DAMAGE_FRACTION_FOR_KILL ) ) + { + if( self->client->ps.stats[ STAT_PTEAM ] == PTE_HUMANS ) + { + trap_Cvar_Set( "g_alienKills", va( "%d", g_alienKills.integer + 1 ) ); + if( g_alienStage.integer < 2 ) + { + self->client->pers.statscounters.feeds++; + level.humanStatsCounters.feeds++; + } + } + else if( self->client->ps.stats[ STAT_PTEAM ] == PTE_ALIENS ) + { + trap_Cvar_Set( "g_humanKills", va( "%d", g_humanKills.integer + 1 ) ); + if( g_humanStage.integer < 2 ) + { + self->client->pers.statscounters.feeds++; + level.alienStatsCounters.feeds++; + } + } + } + + if( totalDamage > 0.0f ) + { + if( self->client->ps.stats[ STAT_PTEAM ] == PTE_ALIENS ) + { + //nice simple happy bouncy human land + float classValue = BG_FindValueOfClass( self->client->ps.stats[ STAT_PCLASS ] ); + + for( i = 0; i < MAX_CLIENTS; i++ ) + { + player = g_entities + i; + + if( !player->client ) + continue; + + if( player->client->ps.stats[ STAT_PTEAM ] != PTE_HUMANS ) + continue; + + if( !self->credits[ i ] ) + continue; + + percentDamage = (float)self->credits[ i ] / totalDamage; + if( percentDamage > 0 && percentDamage < 1) + { + player->client->pers.statscounters.assists++; + level.humanStatsCounters.assists++; + } + + //add credit + G_AddCreditToClient( player->client, + (int)( classValue * percentDamage ), qtrue ); + } + } + else if( self->client->ps.stats[ STAT_PTEAM ] == PTE_HUMANS ) + { + //horribly complex nasty alien land + float humanValue = BG_GetValueOfHuman( &self->client->ps ); + int frags; + int unclaimedFrags = (int)humanValue; + + for( i = 0; i < MAX_CLIENTS; i++ ) + { + player = g_entities + i; + + if( !player->client ) + continue; + + if( player->client->ps.stats[ STAT_PTEAM ] != PTE_ALIENS ) + continue; + + //this client did no damage + if( !self->credits[ i ] ) + continue; + + //nothing left to claim + if( !unclaimedFrags ) + break; + + percentDamage = (float)self->credits[ i ] / totalDamage; + if( percentDamage > 0 && percentDamage < 1) + { + player->client->pers.statscounters.assists++; + level.alienStatsCounters.assists++; + } + + frags = (int)floor( humanValue * percentDamage); + + if( frags > 0 ) + { + //add kills + G_AddCreditToClient( player->client, frags, qtrue ); + + //can't revist this account later + self->credits[ i ] = 0; + + //reduce frags left to be claimed + unclaimedFrags -= frags; + } + } + + //there are frags still to be claimed + if( unclaimedFrags ) + { + //the clients remaining at this point do not + //have enough credit to claim even one frag + //so simply give the top <unclaimedFrags> clients + //a frag each + + for( i = 0; i < unclaimedFrags; i++ ) + { + int maximum = 0; + int topClient = 0; + + for( j = 0; j < MAX_CLIENTS; j++ ) + { + //this client did no damage + if( !self->credits[ j ] ) + continue; + + if( self->credits[ j ] > maximum ) + { + maximum = self->credits[ j ]; + topClient = j; + } + } + + if( maximum > 0 ) + { + player = g_entities + topClient; + + //add kills + G_AddCreditToClient( player->client, 1, qtrue ); + + //can't revist this account again + self->credits[ topClient ] = 0; + } + } + } + } + } + + ScoreboardMessage( self ); // show scores + + // send updated scores to any clients that are following this one, + // or they would get stale scoreboards + for( i = 0 ; i < level.maxclients ; i++ ) + { + gclient_t *client; + + client = &level.clients[ i ]; + if( client->pers.connected != CON_CONNECTED ) + continue; + + if( client->sess.sessionTeam != TEAM_SPECTATOR ) + continue; + + if( client->sess.spectatorClient == self->s.number ) + ScoreboardMessage( g_entities + i ); + } + +finish_dying: // from MOD_SLAP + + VectorCopy( self->s.origin, self->client->pers.lastDeathLocation ); + + self->takedamage = qfalse; // can still be gibbed + + self->s.weapon = WP_NONE; + self->r.contents = CONTENTS_CORPSE; + + self->s.angles[ PITCH ] = 0; + self->s.angles[ ROLL ] = 0; + self->s.angles[ YAW ] = self->s.apos.trBase[ YAW ]; + LookAtKiller( self, inflictor, attacker ); + + VectorCopy( self->s.angles, self->client->ps.viewangles ); + + self->s.loopSound = 0; + + self->r.maxs[ 2 ] = -8; + + // don't allow respawn until the death anim is done + // g_forcerespawn may force spawning at some later time + self->client->respawnTime = level.time + 1700; + + // remove powerups + memset( self->client->ps.powerups, 0, sizeof( self->client->ps.powerups ) ); + + { + // normal death + static int i; + + if( !( self->client->ps.persistant[ PERS_STATE ] & PS_NONSEGMODEL ) ) + { + switch( i ) + { + case 0: + anim = BOTH_DEATH1; + break; + case 1: + anim = BOTH_DEATH2; + break; + case 2: + default: + anim = BOTH_DEATH3; + break; + } + } + else + { + switch( i ) + { + case 0: + anim = NSPA_DEATH1; + break; + case 1: + anim = NSPA_DEATH2; + break; + case 2: + default: + anim = NSPA_DEATH3; + break; + } + } + + self->client->ps.legsAnim = + ( ( self->client->ps.legsAnim & ANIM_TOGGLEBIT ) ^ ANIM_TOGGLEBIT ) | anim; + + if( !( self->client->ps.persistant[ PERS_STATE ] & PS_NONSEGMODEL ) ) + { + self->client->ps.torsoAnim = + ( ( self->client->ps.torsoAnim & ANIM_TOGGLEBIT ) ^ ANIM_TOGGLEBIT ) | anim; + } + + // use own entityid if killed by non-client to prevent uint8_t overflow + G_AddEvent( self, EV_DEATH1 + i, + ( killer < MAX_CLIENTS ) ? killer : self - g_entities ); + + // globally cycle through the different death animations + i = ( i + 1 ) % 3; + } + + trap_LinkEntity( self ); +} + + +////////TA: locdamage + +/* +=============== +G_ParseArmourScript +=============== +*/ +void G_ParseArmourScript( char *buf, int upgrade ) +{ + char *token; + int count; + + count = 0; + + while( 1 ) + { + token = COM_Parse( &buf ); + + if( !token[0] ) + break; + + if( strcmp( token, "{" ) ) + { + G_Printf( "Missing { in armour file\n" ); + break; + } + + if( count == MAX_ARMOUR_REGIONS ) + { + G_Printf( "Max armour regions exceeded in locdamage file\n" ); + break; + } + + //default + g_armourRegions[ upgrade ][ count ].minHeight = 0.0; + g_armourRegions[ upgrade ][ count ].maxHeight = 1.0; + g_armourRegions[ upgrade ][ count ].minAngle = 0; + g_armourRegions[ upgrade ][ count ].maxAngle = 360; + g_armourRegions[ upgrade ][ count ].modifier = 1.0; + g_armourRegions[ upgrade ][ count ].crouch = qfalse; + + while( 1 ) + { + token = COM_ParseExt( &buf, qtrue ); + + if( !token[0] ) + { + G_Printf( "Unexpected end of armour file\n" ); + break; + } + + if( !Q_stricmp( token, "}" ) ) + { + break; + } + else if( !strcmp( token, "minHeight" ) ) + { + token = COM_ParseExt( &buf, qfalse ); + + if ( !token[0] ) + strcpy( token, "0" ); + + g_armourRegions[ upgrade ][ count ].minHeight = atof( token ); + } + else if( !strcmp( token, "maxHeight" ) ) + { + token = COM_ParseExt( &buf, qfalse ); + + if ( !token[0] ) + strcpy( token, "100" ); + + g_armourRegions[ upgrade ][ count ].maxHeight = atof( token ); + } + else if( !strcmp( token, "minAngle" ) ) + { + token = COM_ParseExt( &buf, qfalse ); + + if ( !token[0] ) + strcpy( token, "0" ); + + g_armourRegions[ upgrade ][ count ].minAngle = atoi( token ); + } + else if( !strcmp( token, "maxAngle" ) ) + { + token = COM_ParseExt( &buf, qfalse ); + + if ( !token[0] ) + strcpy( token, "360" ); + + g_armourRegions[ upgrade ][ count ].maxAngle = atoi( token ); + } + else if( !strcmp( token, "modifier" ) ) + { + token = COM_ParseExt( &buf, qfalse ); + + if ( !token[0] ) + strcpy( token, "1.0" ); + + g_armourRegions[ upgrade ][ count ].modifier = atof( token ); + } + else if( !strcmp( token, "crouch" ) ) + { + g_armourRegions[ upgrade ][ count ].crouch = qtrue; + } + } + + g_numArmourRegions[ upgrade ]++; + count++; + } +} + + +/* +=============== +G_ParseDmgScript +=============== +*/ +void G_ParseDmgScript( char *buf, int class ) +{ + char *token; + int count; + + count = 0; + + while( 1 ) + { + token = COM_Parse( &buf ); + + if( !token[0] ) + break; + + if( strcmp( token, "{" ) ) + { + G_Printf( "Missing { in locdamage file\n" ); + break; + } + + if( count == MAX_LOCDAMAGE_REGIONS ) + { + G_Printf( "Max damage regions exceeded in locdamage file\n" ); + break; + } + + //default + g_damageRegions[ class ][ count ].minHeight = 0.0; + g_damageRegions[ class ][ count ].maxHeight = 1.0; + g_damageRegions[ class ][ count ].minAngle = 0; + g_damageRegions[ class ][ count ].maxAngle = 360; + g_damageRegions[ class ][ count ].modifier = 1.0; + g_damageRegions[ class ][ count ].crouch = qfalse; + + while( 1 ) + { + token = COM_ParseExt( &buf, qtrue ); + + if( !token[0] ) + { + G_Printf( "Unexpected end of locdamage file\n" ); + break; + } + + if( !Q_stricmp( token, "}" ) ) + { + break; + } + else if( !strcmp( token, "minHeight" ) ) + { + token = COM_ParseExt( &buf, qfalse ); + + if ( !token[0] ) + strcpy( token, "0" ); + + g_damageRegions[ class ][ count ].minHeight = atof( token ); + } + else if( !strcmp( token, "maxHeight" ) ) + { + token = COM_ParseExt( &buf, qfalse ); + + if ( !token[0] ) + strcpy( token, "100" ); + + g_damageRegions[ class ][ count ].maxHeight = atof( token ); + } + else if( !strcmp( token, "minAngle" ) ) + { + token = COM_ParseExt( &buf, qfalse ); + + if ( !token[0] ) + strcpy( token, "0" ); + + g_damageRegions[ class ][ count ].minAngle = atoi( token ); + } + else if( !strcmp( token, "maxAngle" ) ) + { + token = COM_ParseExt( &buf, qfalse ); + + if ( !token[0] ) + strcpy( token, "360" ); + + g_damageRegions[ class ][ count ].maxAngle = atoi( token ); + } + else if( !strcmp( token, "modifier" ) ) + { + token = COM_ParseExt( &buf, qfalse ); + + if ( !token[0] ) + strcpy( token, "1.0" ); + + g_damageRegions[ class ][ count ].modifier = atof( token ); + } + else if( !strcmp( token, "crouch" ) ) + { + g_damageRegions[ class ][ count ].crouch = qtrue; + } + } + + g_numDamageRegions[ class ]++; + count++; + } +} + + +/* +============ +G_CalcDamageModifier +============ +*/ +static float G_CalcDamageModifier( vec3_t point, gentity_t *targ, gentity_t *attacker, int class, int dflags ) +{ + vec3_t targOrigin; + vec3_t bulletPath; + vec3_t bulletAngle; + vec3_t pMINUSfloor, floor, normal; + + float clientHeight, hitRelative, hitRatio; + int bulletRotation, clientRotation, hitRotation; + float modifier = 1.0f; + int i, j; + + if( point == NULL ) + return 1.0f; + + if( g_unlagged.integer && targ->client && targ->client->unlaggedCalc.used ) + VectorCopy( targ->client->unlaggedCalc.origin, targOrigin ); + else + VectorCopy( targ->r.currentOrigin, targOrigin ); + + clientHeight = targ->r.maxs[ 2 ] - targ->r.mins[ 2 ]; + + if( targ->client->ps.stats[ STAT_STATE ] & SS_WALLCLIMBING ) + VectorCopy( targ->client->ps.grapplePoint, normal ); + else + VectorSet( normal, 0, 0, 1 ); + + VectorMA( targOrigin, targ->r.mins[ 2 ], normal, floor ); + VectorSubtract( point, floor, pMINUSfloor ); + + hitRelative = DotProduct( normal, pMINUSfloor ) / VectorLength( normal ); + + if( hitRelative < 0.0f ) + hitRelative = 0.0f; + + if( hitRelative > clientHeight ) + hitRelative = clientHeight; + + hitRatio = hitRelative / clientHeight; + + VectorSubtract( targOrigin, point, bulletPath ); + vectoangles( bulletPath, bulletAngle ); + + clientRotation = targ->client->ps.viewangles[ YAW ]; + bulletRotation = bulletAngle[ YAW ]; + + hitRotation = abs( clientRotation - bulletRotation ); + + hitRotation = hitRotation % 360; // Keep it in the 0-359 range + + if( dflags & DAMAGE_NO_LOCDAMAGE ) + { + for( i = UP_NONE + 1; i < UP_NUM_UPGRADES; i++ ) + { + float totalModifier = 0.0f; + float averageModifier = 1.0f; + + //average all of this upgrade's armour regions together + if( BG_InventoryContainsUpgrade( i, targ->client->ps.stats ) ) + { + for( j = 0; j < g_numArmourRegions[ i ]; j++ ) + totalModifier += g_armourRegions[ i ][ j ].modifier; + + if( g_numArmourRegions[ i ] ) + averageModifier = totalModifier / g_numArmourRegions[ i ]; + else + averageModifier = 1.0f; + } + + modifier *= averageModifier; + } + } + else + { + if( attacker && attacker->client ) + { + attacker->client->pers.statscounters.hitslocational++; + level.alienStatsCounters.hitslocational++; + } + for( i = 0; i < g_numDamageRegions[ class ]; i++ ) + { + qboolean rotationBound; + + if( g_damageRegions[ class ][ i ].minAngle > + g_damageRegions[ class ][ i ].maxAngle ) + { + rotationBound = ( hitRotation >= g_damageRegions[ class ][ i ].minAngle && + hitRotation <= 360 ) || ( hitRotation >= 0 && + hitRotation <= g_damageRegions[ class ][ i ].maxAngle ); + } + else + { + rotationBound = ( hitRotation >= g_damageRegions[ class ][ i ].minAngle && + hitRotation <= g_damageRegions[ class ][ i ].maxAngle ); + } + + if( rotationBound && + hitRatio >= g_damageRegions[ class ][ i ].minHeight && + hitRatio <= g_damageRegions[ class ][ i ].maxHeight && + ( g_damageRegions[ class ][ i ].crouch == + ( targ->client->ps.pm_flags & PMF_DUCKED ) ) ) + modifier *= g_damageRegions[ class ][ i ].modifier; + } + + if( attacker && attacker->client && modifier == 2 ) + { + attacker->client->pers.statscounters.headshots++; + level.alienStatsCounters.headshots++; + } + + for( i = UP_NONE + 1; i < UP_NUM_UPGRADES; i++ ) + { + if( BG_InventoryContainsUpgrade( i, targ->client->ps.stats ) ) + { + for( j = 0; j < g_numArmourRegions[ i ]; j++ ) + { + qboolean rotationBound; + + if( g_armourRegions[ i ][ j ].minAngle > + g_armourRegions[ i ][ j ].maxAngle ) + { + rotationBound = ( hitRotation >= g_armourRegions[ i ][ j ].minAngle && + hitRotation <= 360 ) || ( hitRotation >= 0 && + hitRotation <= g_armourRegions[ i ][ j ].maxAngle ); + } + else + { + rotationBound = ( hitRotation >= g_armourRegions[ i ][ j ].minAngle && + hitRotation <= g_armourRegions[ i ][ j ].maxAngle ); + } + + if( rotationBound && + hitRatio >= g_armourRegions[ i ][ j ].minHeight && + hitRatio <= g_armourRegions[ i ][ j ].maxHeight && + ( g_armourRegions[ i ][ j ].crouch == + ( targ->client->ps.pm_flags & PMF_DUCKED ) ) ) + modifier *= g_armourRegions[ i ][ j ].modifier; + } + } + } + } + + return modifier; +} + + +/* +============ +G_InitDamageLocations +============ +*/ +void G_InitDamageLocations( void ) +{ + char *modelName; + char filename[ MAX_QPATH ]; + int i; + int len; + fileHandle_t fileHandle; + char buffer[ MAX_LOCDAMAGE_TEXT ]; + + for( i = PCL_NONE + 1; i < PCL_NUM_CLASSES; i++ ) + { + modelName = BG_FindModelNameForClass( i ); + Com_sprintf( filename, sizeof( filename ), "models/players/%s/locdamage.cfg", modelName ); + + len = trap_FS_FOpenFile( filename, &fileHandle, FS_READ ); + if ( !fileHandle ) + { + G_Printf( va( S_COLOR_RED "file not found: %s\n", filename ) ); + continue; + } + + if( len >= MAX_LOCDAMAGE_TEXT ) + { + G_Printf( va( S_COLOR_RED "file too large: %s is %i, max allowed is %i", filename, len, MAX_LOCDAMAGE_TEXT ) ); + trap_FS_FCloseFile( fileHandle ); + continue; + } + + trap_FS_Read( buffer, len, fileHandle ); + buffer[len] = 0; + trap_FS_FCloseFile( fileHandle ); + + G_ParseDmgScript( buffer, i ); + } + + for( i = UP_NONE + 1; i < UP_NUM_UPGRADES; i++ ) + { + modelName = BG_FindNameForUpgrade( i ); + Com_sprintf( filename, sizeof( filename ), "armour/%s.armour", modelName ); + + len = trap_FS_FOpenFile( filename, &fileHandle, FS_READ ); + + //no file - no parsage + if ( !fileHandle ) + continue; + + if( len >= MAX_LOCDAMAGE_TEXT ) + { + G_Printf( va( S_COLOR_RED "file too large: %s is %i, max allowed is %i", filename, len, MAX_LOCDAMAGE_TEXT ) ); + trap_FS_FCloseFile( fileHandle ); + continue; + } + + trap_FS_Read( buffer, len, fileHandle ); + buffer[len] = 0; + trap_FS_FCloseFile( fileHandle ); + + G_ParseArmourScript( buffer, i ); + } +} + +////////TA: locdamage + + +/* +============ +T_Damage + +targ entity that is being damaged +inflictor entity that is causing the damage +attacker entity that caused the inflictor to damage targ + example: targ=monster, inflictor=rocket, attacker=player + +dir direction of the attack for knockback +point point at which the damage is being inflicted, used for headshots +damage amount of damage being inflicted +knockback force to be applied against targ as a result of the damage + +inflictor, attacker, dir, and point can be NULL for environmental effects + +dflags these flags are used to control how T_Damage works + DAMAGE_RADIUS damage was indirect (from a nearby explosion) + DAMAGE_NO_ARMOR armor does not protect from this damage + DAMAGE_NO_KNOCKBACK do not affect velocity, just view angles + DAMAGE_NO_PROTECTION kills godmode, armor, everything +============ +*/ + +//TA: team is the team that is immune to this damage +void G_SelectiveDamage( gentity_t *targ, gentity_t *inflictor, gentity_t *attacker, + vec3_t dir, vec3_t point, int damage, int dflags, int mod, int team ) +{ + if( targ->client && ( team != targ->client->ps.stats[ STAT_PTEAM ] ) ) + G_Damage( targ, inflictor, attacker, dir, point, damage, dflags, mod ); +} + +void G_Damage( gentity_t *targ, gentity_t *inflictor, gentity_t *attacker, + vec3_t dir, vec3_t point, int damage, int dflags, int mod ) +{ + gclient_t *client; + int take; + int save; + int asave = 0; + int knockback; + float damagemodifier=0.0; + int takeNoOverkill; + + if( !targ->takedamage ) + return; + + // the intermission has allready been qualified for, so don't + // allow any extra scoring + if( level.intermissionQueued ) + return; + + if( !inflictor ) + inflictor = &g_entities[ ENTITYNUM_WORLD ]; + + if( !attacker ) + attacker = &g_entities[ ENTITYNUM_WORLD ]; + + if( attacker->client && attacker->client->pers.paused ) + return; + + // shootable doors / buttons don't actually have any health + if( targ->s.eType == ET_MOVER ) + { + if( targ->use && ( targ->moverState == MOVER_POS1 || + targ->moverState == ROTATOR_POS1 ) ) + targ->use( targ, inflictor, attacker ); + + return; + } + + client = targ->client; + + if( client ) + { + if( client->noclip && !g_devmapNoGod.integer) + return; + if( client->pers.paused ) + return; + } + + if( !dir ) + dflags |= DAMAGE_NO_KNOCKBACK; + else + VectorNormalize( dir ); + + knockback = damage; + + if( inflictor->s.weapon != WP_NONE ) + { + knockback = (int)( (float)knockback * + BG_FindKnockbackScaleForWeapon( inflictor->s.weapon ) ); + } + + if( targ->client ) + { + knockback = (int)( (float)knockback * + BG_FindKnockbackScaleForClass( targ->client->ps.stats[ STAT_PCLASS ] ) ); + } + + if( knockback > 200 ) + knockback = 200; + + if( targ->flags & FL_NO_KNOCKBACK ) + knockback = 0; + + if( dflags & DAMAGE_NO_KNOCKBACK ) + knockback = 0; + + // figure momentum add, even if the damage won't be taken + if( knockback && targ->client ) + { + vec3_t kvel; + float mass; + + mass = 200; + + VectorScale( dir, g_knockback.value * (float)knockback / mass, kvel ); + VectorAdd( targ->client->ps.velocity, kvel, targ->client->ps.velocity ); + + // set the timer so that the other client can't cancel + // out the movement immediately + if( !targ->client->ps.pm_time ) + { + int t; + + t = knockback * 2; + if( t < 50 ) + t = 50; + + if( t > 200 ) + t = 200; + + targ->client->ps.pm_time = t; + targ->client->ps.pm_flags |= PMF_TIME_KNOCKBACK; + } + } + + // check for completely getting out of the damage + if( !( dflags & DAMAGE_NO_PROTECTION ) ) + { + + // if TF_NO_FRIENDLY_FIRE is set, don't do damage to the target + // if the attacker was on the same team + if( targ != attacker && OnSameTeam( targ, attacker ) ) + { + if( g_dretchPunt.integer && + targ->client->ps.stats[ STAT_PCLASS ] == PCL_ALIEN_LEVEL0 ) + { + vec3_t dir, push; + + VectorSubtract( targ->r.currentOrigin, attacker->r.currentOrigin, dir ); + VectorNormalizeFast( dir ); + VectorScale( dir, ( damage * 10.0f ), push ); + push[2] = 64.0f; + VectorAdd( targ->client->ps.velocity, push, targ->client->ps.velocity ); + return; + } + else if(mod == MOD_LEVEL4_CHARGE || mod == MOD_LEVEL3_POUNCE ) + { // don't do friendly fire on movement attacks + if( g_friendlyFireMovementAttacks.value <= 0 || ( g_friendlyFire.value<=0 && g_friendlyFireAliens.value<=0 ) ) + return; + else if( g_friendlyFireMovementAttacks.value > 0 && g_friendlyFireMovementAttacks.value < 1 ) + damage =(int)(0.5 + g_friendlyFireMovementAttacks.value * (float) damage); + } + else if( g_friendlyFire.value <=0) + { + if( targ->client->ps.stats[ STAT_PTEAM ] == PTE_HUMANS ) + { + if(g_friendlyFireHumans.value<=0) + return; + else if( g_friendlyFireHumans.value > 0 && g_friendlyFireHumans.value < 1 ) + damage =(int)(0.5 + g_friendlyFireHumans.value * (float) damage); + } + if( targ->client->ps.stats[ STAT_PTEAM ] == PTE_ALIENS ) + { + if(g_friendlyFireAliens.value==0) + return; + else if( g_friendlyFireAliens.value > 0 && g_friendlyFireAliens.value < 1 ) + damage =(int)(0.5 + g_friendlyFireAliens.value * (float) damage); + } + } + else if( g_friendlyFire.value > 0 && g_friendlyFire.value < 1 ) + { + damage =(int)(0.5 + g_friendlyFire.value * (float) damage); + } + } + + // If target is buildable on the same team as the attacking client + if( targ->s.eType == ET_BUILDABLE && attacker->client && + targ->biteam == attacker->client->pers.teamSelection ) + { + if(mod == MOD_LEVEL4_CHARGE || mod == MOD_LEVEL3_POUNCE ) + { + if(g_friendlyFireMovementAttacks.value <= 0) + return; + else if(g_friendlyFireMovementAttacks.value > 0 && g_friendlyFireMovementAttacks.value < 1) + damage =(int)(0.5 + g_friendlyFireMovementAttacks.value * (float) damage); + } + if( g_friendlyBuildableFire.value <= 0 ) + { + return; + } + else if( g_friendlyBuildableFire.value > 0 && g_friendlyBuildableFire.value < 1 ) + { + damage =(int)(0.5 + g_friendlyBuildableFire.value * (float) damage); + } + } + + // check for godmode + if ( targ->flags & FL_GODMODE && !g_devmapNoGod.integer) + return; + + if( level.paused ) + return; + + if(targ->s.eType == ET_BUILDABLE && g_cheats.integer && g_devmapNoStructDmg.integer) + return; + } + + // add to the attacker's hit counter + if( attacker->client && targ != attacker && targ->health > 0 + && targ->s.eType != ET_MISSILE + && targ->s.eType != ET_GENERAL ) + { + if( OnSameTeam( targ, attacker ) ) + attacker->client->ps.persistant[ PERS_HITS ]--; + else + attacker->client->ps.persistant[ PERS_HITS ]++; + } + + take = damage; + save = 0; + + // add to the damage inflicted on a player this frame + // the total will be turned into screen blends and view angle kicks + // at the end of the frame + if( client ) + { + if( attacker ) + client->ps.persistant[ PERS_ATTACKER ] = attacker->s.number; + else + client->ps.persistant[ PERS_ATTACKER ] = ENTITYNUM_WORLD; + + client->damage_armor += asave; + client->damage_blood += take; + client->damage_knockback += knockback; + + if( dir ) + { + VectorCopy ( dir, client->damage_from ); + client->damage_fromWorld = qfalse; + } + else + { + VectorCopy ( targ->r.currentOrigin, client->damage_from ); + client->damage_fromWorld = qtrue; + } + + // set the last client who damaged the target + targ->client->lasthurt_client = attacker->s.number; + targ->client->lasthurt_mod = mod; + + damagemodifier = G_CalcDamageModifier( point, targ, attacker, client->ps.stats[ STAT_PCLASS ], dflags ); + take = (int)( (float)take * damagemodifier ); + + //if boosted poison every attack + if( attacker->client && attacker->client->ps.stats[ STAT_STATE ] & SS_BOOSTED ) + { + if( targ->client->ps.stats[ STAT_PTEAM ] == PTE_HUMANS && + !( targ->client->ps.stats[ STAT_STATE ] & SS_POISONED ) && + mod != MOD_LEVEL2_ZAP && + targ->client->poisonImmunityTime < level.time ) + { + targ->client->ps.stats[ STAT_STATE ] |= SS_POISONED; + targ->client->lastPoisonTime = level.time; + targ->client->lastPoisonClient = attacker; + attacker->client->pers.statscounters.repairspoisons++; + level.alienStatsCounters.repairspoisons++; + } + } + } + + if( take < 1 ) + take = 1; + + if( g_debugDamage.integer ) + { + G_Printf( "%i: client:%i health:%i damage:%i armor:%i\n", level.time, targ->s.number, + targ->health, take, asave ); + } + + takeNoOverkill = take; + if( takeNoOverkill > targ->health ) + { + if(targ->health > 0) + takeNoOverkill = targ->health; + else + takeNoOverkill = 0; + } + + if( take ) + { + //Increment some stats counters + if( attacker && attacker->client ) + { + if( targ->biteam == attacker->client->pers.teamSelection || OnSameTeam( targ, attacker ) ) + { + attacker->client->pers.statscounters.ffdmgdone += takeNoOverkill; + if( attacker->client->pers.teamSelection == PTE_ALIENS ) + { + level.alienStatsCounters.ffdmgdone+=takeNoOverkill; + } + else if( attacker->client->pers.teamSelection == PTE_HUMANS ) + { + level.humanStatsCounters.ffdmgdone+=takeNoOverkill; + } + } + else if( targ->s.eType == ET_BUILDABLE ) + { + attacker->client->pers.statscounters.structdmgdone += takeNoOverkill; + + if( attacker->client->pers.teamSelection == PTE_ALIENS ) + { + level.alienStatsCounters.structdmgdone+=takeNoOverkill; + } + else if( attacker->client->pers.teamSelection == PTE_HUMANS ) + { + level.humanStatsCounters.structdmgdone+=takeNoOverkill; + } + + if( targ->health > 0 && ( targ->health - take ) <=0 ) + { + attacker->client->pers.statscounters.structskilled++; + if( attacker->client->pers.teamSelection == PTE_ALIENS ) + { + level.alienStatsCounters.structskilled++; + } + else if( attacker->client->pers.teamSelection == PTE_HUMANS ) + { + level.humanStatsCounters.structskilled++; + } + } + } + else if( targ->client ) + { + attacker->client->pers.statscounters.dmgdone +=takeNoOverkill; + attacker->client->pers.statscounters.hits++; + if( attacker->client->pers.teamSelection == PTE_ALIENS ) + { + level.alienStatsCounters.dmgdone+=takeNoOverkill; + } + else if( attacker->client->pers.teamSelection == PTE_HUMANS ) + { + level.humanStatsCounters.dmgdone+=takeNoOverkill; + } + } + } + + + //Do the damage + targ->health = targ->health - take; + + if( targ->client ) + targ->client->ps.stats[ STAT_HEALTH ] = targ->health; + + targ->lastDamageTime = level.time; + + //TA: add to the attackers "account" on the target + if( targ->client && attacker->client ) + { + if( attacker != targ && !OnSameTeam( targ, attacker ) ) + targ->credits[ attacker->client->ps.clientNum ] += take; + else if( attacker != targ && OnSameTeam( targ, attacker ) ) + targ->client->tkcredits[ attacker->client->ps.clientNum ] += takeNoOverkill; + } + + if( targ->health <= 0 ) + { + if( client ) + targ->flags |= FL_NO_KNOCKBACK; + + if( targ->health < -999 ) + targ->health = -999; + + targ->enemy = attacker; + targ->die( targ, inflictor, attacker, take, mod ); + return; + } + else if( targ->pain ) + targ->pain( targ, attacker, take ); + } +} + + +/* +============ +CanDamage + +Returns qtrue if the inflictor can directly damage the target. Used for +explosions and melee attacks. +============ +*/ +qboolean CanDamage( gentity_t *targ, vec3_t origin ) +{ + vec3_t dest; + trace_t tr; + vec3_t midpoint; + + // use the midpoint of the bounds instead of the origin, because + // bmodels may have their origin is 0,0,0 + VectorAdd( targ->r.absmin, targ->r.absmax, midpoint ); + VectorScale( midpoint, 0.5, midpoint ); + + VectorCopy( midpoint, dest ); + trap_Trace( &tr, origin, vec3_origin, vec3_origin, dest, ENTITYNUM_NONE, MASK_SOLID ); + if( tr.fraction == 1.0 || tr.entityNum == targ->s.number ) + return qtrue; + + // this should probably check in the plane of projection, + // rather than in world coordinate, and also include Z + VectorCopy( midpoint, dest ); + dest[ 0 ] += 15.0; + dest[ 1 ] += 15.0; + trap_Trace( &tr, origin, vec3_origin, vec3_origin, dest, ENTITYNUM_NONE, MASK_SOLID ); + if( tr.fraction == 1.0 ) + return qtrue; + + VectorCopy( midpoint, dest ); + dest[ 0 ] += 15.0; + dest[ 1 ] -= 15.0; + trap_Trace( &tr, origin, vec3_origin, vec3_origin, dest, ENTITYNUM_NONE, MASK_SOLID ); + if( tr.fraction == 1.0 ) + return qtrue; + + VectorCopy( midpoint, dest ); + dest[ 0 ] -= 15.0; + dest[ 1 ] += 15.0; + trap_Trace( &tr, origin, vec3_origin, vec3_origin, dest, ENTITYNUM_NONE, MASK_SOLID ); + if( tr.fraction == 1.0 ) + return qtrue; + + VectorCopy( midpoint, dest ); + dest[ 0 ] -= 15.0; + dest[ 1 ] -= 15.0; + trap_Trace( &tr, origin, vec3_origin, vec3_origin, dest, ENTITYNUM_NONE, MASK_SOLID ); + if( tr.fraction == 1.0 ) + return qtrue; + + return qfalse; +} + + +//TA: +/* +============ +G_SelectiveRadiusDamage +============ +*/ +qboolean G_SelectiveRadiusDamage( vec3_t origin, gentity_t *attacker, float damage, + float radius, gentity_t *ignore, int mod, int team ) +{ + float points, dist; + gentity_t *ent; + int entityList[ MAX_GENTITIES ]; + int numListedEntities; + vec3_t mins, maxs; + vec3_t v; + vec3_t dir; + int i, e; + qboolean hitClient = qfalse; + + if( radius < 1 ) + radius = 1; + + for( i = 0; i < 3; i++ ) + { + mins[ i ] = origin[ i ] - radius; + maxs[ i ] = origin[ i ] + radius; + } + + numListedEntities = trap_EntitiesInBox( mins, maxs, entityList, MAX_GENTITIES ); + + for( e = 0; e < numListedEntities; e++ ) + { + ent = &g_entities[ entityList[ e ] ]; + + if( ent == ignore ) + continue; + + if( !ent->takedamage ) + continue; + + // find the distance from the edge of the bounding box + for( i = 0 ; i < 3 ; i++ ) + { + if( origin[ i ] < ent->r.absmin[ i ] ) + v[ i ] = ent->r.absmin[ i ] - origin[ i ]; + else if( origin[ i ] > ent->r.absmax[ i ] ) + v[ i ] = origin[ i ] - ent->r.absmax[ i ]; + else + v[ i ] = 0; + } + + dist = VectorLength( v ); + if( dist >= radius ) + continue; + + points = damage * ( 1.0 - dist / radius ); + + if( CanDamage( ent, origin ) ) + { + VectorSubtract( ent->r.currentOrigin, origin, dir ); + // push the center of mass higher than the origin so players + // get knocked into the air more + dir[ 2 ] += 24; + G_SelectiveDamage( ent, NULL, attacker, dir, origin, + (int)points, DAMAGE_RADIUS|DAMAGE_NO_LOCDAMAGE, mod, team ); + } + } + + return hitClient; +} + + +/* +============ +G_RadiusDamage +============ +*/ +qboolean G_RadiusDamage( vec3_t origin, gentity_t *attacker, float damage, + float radius, gentity_t *ignore, int mod ) +{ + float points, dist; + gentity_t *ent; + int entityList[ MAX_GENTITIES ]; + int numListedEntities; + vec3_t mins, maxs; + vec3_t v; + vec3_t dir; + int i, e; + qboolean hitClient = qfalse; + + if( radius < 1 ) + radius = 1; + + for( i = 0; i < 3; i++ ) + { + mins[ i ] = origin[ i ] - radius; + maxs[ i ] = origin[ i ] + radius; + } + + numListedEntities = trap_EntitiesInBox( mins, maxs, entityList, MAX_GENTITIES ); + + for( e = 0; e < numListedEntities; e++ ) + { + ent = &g_entities[ entityList[ e ] ]; + + if( ent == ignore ) + continue; + + if( !ent->takedamage ) + continue; + + // find the distance from the edge of the bounding box + for( i = 0; i < 3; i++ ) + { + if( origin[ i ] < ent->r.absmin[ i ] ) + v[ i ] = ent->r.absmin[ i ] - origin[ i ]; + else if( origin[ i ] > ent->r.absmax[ i ] ) + v[ i ] = origin[ i ] - ent->r.absmax[ i ]; + else + v[ i ] = 0; + } + + dist = VectorLength( v ); + if( dist >= radius ) + continue; + + points = damage * ( 1.0 - dist / radius ); + + if( CanDamage( ent, origin ) ) + { + VectorSubtract( ent->r.currentOrigin, origin, dir ); + // push the center of mass higher than the origin so players + // get knocked into the air more + dir[ 2 ] += 24; + G_Damage( ent, NULL, attacker, dir, origin, + (int)points, DAMAGE_RADIUS|DAMAGE_NO_LOCDAMAGE, mod ); + } + } + + return hitClient; +} + +/* +============ +G_Knockback +============ +*/ +void G_Knockback( gentity_t *targ, vec3_t dir, int knockback ) +{ + if( knockback && targ->client ) + { + vec3_t kvel; + float mass; + + mass = 200; + + // Halve knockback for bsuits + if( targ->client && + targ->client->ps.stats[ STAT_PTEAM ] == PTE_HUMANS && + BG_InventoryContainsUpgrade( UP_BATTLESUIT, targ->client->ps.stats ) ) + mass += 400; + + // Halve knockback for crouching players + if(targ->client->ps.pm_flags&PMF_DUCKED) knockback /= 2; + + VectorScale( dir, g_knockback.value * (float)knockback / mass, kvel ); + VectorAdd( targ->client->ps.velocity, kvel, targ->client->ps.velocity ); + + // set the timer so that the other client can't cancel + // out the movement immediately + if( !targ->client->ps.pm_time ) + { + int t; + + t = knockback * 2; + if( t < 50 ) + t = 50; + + if( t > 200 ) + t = 200; + targ->client->ps.pm_time = t; + targ->client->ps.pm_flags |= PMF_TIME_KNOCKBACK; + } + } +} + diff --git a/src/game/g_local.h b/src/game/g_local.h new file mode 100644 index 0000000..8c65998 --- /dev/null +++ b/src/game/g_local.h @@ -0,0 +1,1533 @@ +/* +=========================================================================== +Copyright (C) 1999-2005 Id Software, Inc. +Copyright (C) 2000-2006 Tim Angus + +This file is part of Tremulous. + +Tremulous is free software; you can redistribute it +and/or modify it under the terms of the GNU General Public License as +published by the Free Software Foundation; either version 2 of the License, +or (at your option) any later version. + +Tremulous is distributed in the hope that it will be +useful, but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with Tremulous; if not, write to the Free Software +Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA +=========================================================================== +*/ + +// g_local.h -- local definitions for game module + +#include "../qcommon/q_shared.h" +#include "bg_public.h" +#include "g_public.h" + +typedef struct gentity_s gentity_t; +typedef struct gclient_s gclient_t; + +#include "g_admin.h" + +//================================================================== + +#define INFINITE 1000000 + +#define FRAMETIME 100 // msec +#define CARNAGE_REWARD_TIME 3000 +#define REWARD_SPRITE_TIME 2000 + +#define INTERMISSION_DELAY_TIME 1000 +#define SP_INTERMISSION_DELAY_TIME 5000 + +// gentity->flags +#define FL_GODMODE 0x00000010 +#define FL_NOTARGET 0x00000020 +#define FL_TEAMSLAVE 0x00000400 // not the first on the team +#define FL_NO_KNOCKBACK 0x00000800 +#define FL_DROPPED_ITEM 0x00001000 +#define FL_NO_BOTS 0x00002000 // spawn point not for bot use +#define FL_NO_HUMANS 0x00004000 // spawn point just for bots +#define FL_FORCE_GESTURE 0x00008000 // spawn point just for bots + +typedef struct +{ + qboolean isNB; + float Area; + float Height; +} noBuild_t; + +typedef struct +{ + gentity_t *Marker; + vec3_t Origin; +} nbMarkers_t; + +// movers are things like doors, plats, buttons, etc +typedef enum +{ + MOVER_POS1, + MOVER_POS2, + MOVER_1TO2, + MOVER_2TO1, + + ROTATOR_POS1, + ROTATOR_POS2, + ROTATOR_1TO2, + ROTATOR_2TO1, + + MODEL_POS1, + MODEL_POS2, + MODEL_1TO2, + MODEL_2TO1 +} moverState_t; + +#define SP_PODIUM_MODEL "models/mapobjects/podium/podium4.md3" + +//============================================================================ + +struct gentity_s +{ + entityState_t s; // communicated by server to clients + entityShared_t r; // shared by both the server system and game + + // DO NOT MODIFY ANYTHING ABOVE THIS, THE SERVER + // EXPECTS THE FIELDS IN THAT ORDER! + //================================ + + struct gclient_s *client; // NULL if not a client + + qboolean inuse; + + char *classname; // set in QuakeEd + int spawnflags; // set in QuakeEd + + qboolean neverFree; // if true, FreeEntity will only unlink + // bodyque uses this + + int flags; // FL_* variables + + char *model; + char *model2; + int freetime; // level.time when the object was freed + + int eventTime; // events will be cleared EVENT_VALID_MSEC after set + qboolean freeAfterEvent; + qboolean unlinkAfterEvent; + + qboolean physicsObject; // if true, it can be pushed by movers and fall off edges + // all game items are physicsObjects, + float physicsBounce; // 1.0 = continuous bounce, 0.0 = no bounce + int clipmask; // brushes with this content value will be collided against + // when moving. items and corpses do not collide against + // players, for instance + + // movers + moverState_t moverState; + int soundPos1; + int sound1to2; + int sound2to1; + int soundPos2; + int soundLoop; + gentity_t *parent; + gentity_t *nextTrain; + gentity_t *prevTrain; + vec3_t pos1, pos2; + float rotatorAngle; + gentity_t *clipBrush; // clipping brush for model doors + + char *message; + + int timestamp; // body queue sinking, etc + + float angle; // set in editor, -1 = up, -2 = down + char *target; + char *targetname; + char *team; + char *targetShaderName; + char *targetShaderNewName; + gentity_t *target_ent; + + float speed; + float lastSpeed; // used by trains that have been restarted + vec3_t movedir; + + // acceleration evaluation + qboolean evaluateAcceleration; + vec3_t oldVelocity; + vec3_t acceleration; + vec3_t oldAccel; + vec3_t jerk; + + int nextthink; + void (*think)( gentity_t *self ); + void (*reached)( gentity_t *self ); // movers call this when hitting endpoint + void (*blocked)( gentity_t *self, gentity_t *other ); + void (*touch)( gentity_t *self, gentity_t *other, trace_t *trace ); + void (*use)( gentity_t *self, gentity_t *other, gentity_t *activator ); + void (*pain)( gentity_t *self, gentity_t *attacker, int damage ); + void (*die)( gentity_t *self, gentity_t *inflictor, gentity_t *attacker, int damage, int mod ); + + int pain_debounce_time; + int fly_sound_debounce_time; // wind tunnel + int last_move_time; + + int health; + int lastHealth; // currently only used for overmind + + qboolean takedamage; + + int damage; + int splashDamage; // quad will increase this without increasing radius + int splashRadius; + int methodOfDeath; + int splashMethodOfDeath; + int chargeRepeat; + + int count; + + gentity_t *chain; + gentity_t *enemy; + gentity_t *activator; + gentity_t *teamchain; // next entity in team + gentity_t *teammaster; // master of the team + + int watertype; + int waterlevel; + + int noise_index; + + // timing variables + float wait; + float random; + + pTeam_t stageTeam; + stage_t stageStage; + + int biteam; // buildable item team + gentity_t *parentNode; // for creep and defence/spawn dependencies + qboolean active; // for power repeater, but could be useful elsewhere + qboolean powered; // for human buildables + int builtBy; // clientNum of person that built this + gentity_t *dccNode; // controlling dcc + gentity_t *overmindNode; // controlling overmind + qboolean dcced; // controlled by a dcc or not? + qboolean spawned; // whether or not this buildable has finished spawning + int buildTime; // when this buildable was built + int animTime; // last animation change + int time1000; // timer evaluated every second + qboolean deconstruct; // deconstruct if no BP left + int deconstructTime; // time at which structure marked + int overmindAttackTimer; + int overmindDyingTimer; + int overmindSpawnsTimer; + int nextPhysicsTime; // buildables don't need to check what they're sitting on + // every single frame.. so only do it periodically + int clientSpawnTime; // the time until this spawn can spawn a client + qboolean lev1Grabbed; // for turrets interacting with lev1s + int lev1GrabTime; // for turrets interacting with lev1s + int spawnBlockTime; + + int credits[ MAX_CLIENTS ]; // human credits for each client + qboolean creditsHash[ MAX_CLIENTS ]; // track who has claimed credit + int killedBy; // clientNum of killer + + gentity_t *targeted; // true if the player is currently a valid target of a turret + vec3_t turretAim; // aim vector for turrets + + vec4_t animation; // animated map objects + + gentity_t *builder; // occupant of this hovel + + qboolean nonSegModel; // this entity uses a nonsegmented player model + + buildable_t bTriggers[ BA_NUM_BUILDABLES ]; // which buildables are triggers + pClass_t cTriggers[ PCL_NUM_CLASSES ]; // which classes are triggers + weapon_t wTriggers[ WP_NUM_WEAPONS ]; // which weapons are triggers + upgrade_t uTriggers[ UP_NUM_UPGRADES ]; // which upgrades are triggers + + int triggerGravity; // gravity for this trigger + + int suicideTime; // when the client will suicide + + int lastDamageTime; + + int bdnumb; // buildlog entry ID + + // For nobuild! + noBuild_t noBuild; +}; + +typedef enum +{ + CON_DISCONNECTED, + CON_CONNECTING, + CON_CONNECTED +} clientConnected_t; + +typedef enum +{ + SPECTATOR_NOT, + SPECTATOR_FREE, + SPECTATOR_LOCKED, + SPECTATOR_FOLLOW, + SPECTATOR_SCOREBOARD +} spectatorState_t; + +typedef enum +{ + TEAM_BEGIN, // Beginning a team game, spawn at base + TEAM_ACTIVE // Now actively playing +} playerTeamStateState_t; + +typedef struct +{ + playerTeamStateState_t state; + + int location; + + int captures; + int basedefense; + int carrierdefense; + int flagrecovery; + int fragcarrier; + int assists; + + float lasthurtcarrier; + float lastreturnedflag; + float flagsince; + float lastfraggedcarrier; +} playerTeamState_t; + +// the auto following clients don't follow a specific client +// number, but instead follow the first two active players +#define FOLLOW_ACTIVE1 -1 +#define FOLLOW_ACTIVE2 -2 + +// client data that stays across multiple levels or tournament restarts +// this is achieved by writing all the data to cvar strings at game shutdown +// time and reading them back at connection time. Anything added here +// MUST be dealt with in G_InitSessionData() / G_ReadSessionData() / G_WriteSessionData() +typedef struct +{ + team_t sessionTeam; + pTeam_t restartTeam; //for !restart keepteams and !restart switchteams + int spectatorTime; // for determining next-in-line to play + spectatorState_t spectatorState; + int spectatorClient; // for chasecam and follow mode + int wins, losses; // tournament stats + qboolean invisible; // for being invisible on the server - ghosts! + qboolean teamLeader; // true when this client is a team leader + clientList_t ignoreList; +} clientSession_t; + +#define MAX_NETNAME 36 + +// data to store details of clients that have abnormally disconnected +typedef struct connectionRecord_s +{ + int clientNum; + pTeam_t clientTeam; + int clientCredit; + int clientScore; + int clientEnterTime; + + int ptrCode; +} connectionRecord_t; + +typedef struct +{ + short kills; + short deaths; + short feeds; + short suicides; + short assists; + int dmgdone; + int ffdmgdone; + int structdmgdone; + short structsbuilt; + short repairspoisons; + short structskilled; + int timealive; + int timeinbase; + short headshots; + int hits; + int hitslocational; + short teamkills; + int dretchbasytime; + int jetpackusewallwalkusetime; + int timeLastViewed; + int AllstatstimeLastViewed; +} statsCounters_t; + +typedef struct +{ + int kills; + int deaths; + int feeds; + int suicides; + int assists; + long dmgdone; + long ffdmgdone; + long structdmgdone; + int structsbuilt; + int repairspoisons; + int structskilled; + long timealive; + long timeinbase; + int headshots; + long hits; + long hitslocational; + int teamkills; + long dretchbasytime; + long jetpackusewallwalkusetime; + long timeLastViewed; +} statsCounters_level; + +// client data that stays across multiple respawns, but is cleared +// on each level change or team change at ClientBegin() +typedef struct +{ + clientConnected_t connected; + usercmd_t cmd; // we would lose angles if not persistant + qboolean localClient; // true if "ip" info key is "localhost" + qboolean initialSpawn; // the first spawn should be at a cool location + qboolean predictItemPickup; // based on cg_predictItems userinfo + qboolean pmoveFixed; // + char netname[ MAX_NETNAME ]; + int maxHealth; // for handicapping + int enterTime; // level.time the client entered the game + playerTeamState_t teamState; // status in teamplay games + int voteCount; // to prevent people from constantly calling votes + qboolean teamInfo; // send team overlay updates? + + pClass_t classSelection; // player class (copied to ent->client->ps.stats[ STAT_PCLASS ] once spawned) + float evolveHealthFraction; + weapon_t humanItemSelection; // humans have a starting item + pTeam_t teamSelection; // player team (copied to ps.stats[ STAT_PTEAM ]) + + int teamChangeTime; // level.time of last team change + qboolean joinedATeam; // used to tell when a PTR code is valid + connectionRecord_t *connection; + + int nameChangeTime; + int nameChanges; + + // used to save playerState_t values while in SPECTATOR_FOLLOW mode + int score; + int credit; + int ping; + + int lastTeamStatus; + + int lastFloodTime; // level.time of last flood-limited command + int floodDemerits; // number of flood demerits accumulated + + char lastMessage[ MAX_SAY_TEXT ]; // last message said by this player + int lastMessageTime; // level.time of last message said by this player + + int lastTeamKillTime; // level.time of last team kill + int teamKillDemerits; // number of team kill demerits accumulated + + vec3_t lastDeathLocation; + char guid[ 33 ]; + char ip[ 16 ]; + qboolean paused; + qboolean muted; + int muteExpires; // level.time at which a player is unmuted + qboolean ignoreAdminWarnings; + qboolean denyBuild; + int specExpires; // level.time at which a player can join a team again after being forced into spectator + int denyHumanWeapons; + int denyAlienClasses; + int adminLevel; + char adminName[ MAX_NETNAME ]; + qboolean designatedBuilder; + qboolean firstConnect; // This is the first map since connect + qboolean useUnlagged; + statsCounters_t statscounters; +} clientPersistant_t; + +#define MAX_UNLAGGED_MARKERS 10 +typedef struct unlagged_s { + vec3_t origin; + vec3_t mins; + vec3_t maxs; + qboolean used; +} unlagged_t; + +// this structure is cleared on each ClientSpawn(), +// except for 'client->pers' and 'client->sess' +struct gclient_s +{ + // ps MUST be the first element, because the server expects it + playerState_t ps; // communicated by server to clients + + // exported into pmove, but not communicated to clients + pmoveExt_t pmext; + + // the rest of the structure is private to game + clientPersistant_t pers; + clientSession_t sess; + + qboolean readyToExit; // wishes to leave the intermission + + qboolean noclip; + + int lastCmdTime; // level.time of last usercmd_t, for EF_CONNECTION + // we can't just use pers.lastCommand.time, because + // of the g_sycronousclients case + int buttons; + int oldbuttons; + int latched_buttons; + + vec3_t oldOrigin; + + // sum up damage over an entire frame, so + // shotgun blasts give a single big kick + int damage_armor; // damage absorbed by armor + int damage_blood; // damage taken out of health + int damage_knockback; // impact damage + vec3_t damage_from; // origin for vector calculation + qboolean damage_fromWorld; // if true, don't use the damage_from vector + + // + int lastkilled_client;// last client that this client killed + int lasthurt_client; // last client that damaged this client + int lasthurt_mod; // type of damage the client did + + // timers + int respawnTime; // can respawn when time > this + int inactivityTime; // kick players when time > this + qboolean inactivityWarning;// qtrue if the five seoond warning has been given + int rewardTime; // clear the EF_AWARD_IMPRESSIVE, etc when time > this + + int airOutTime; + + int lastKillTime; // for multiple kill rewards + + qboolean fireHeld; // used for hook + qboolean fire2Held; // used for alt fire + gentity_t *hook; // grapple hook if out + + int switchTeamTime; // time the player switched teams + + // timeResidual is used to handle events that happen every second + // like health / armor countdowns and regeneration + // two timers, one every 100 msecs, another every sec + int time100; + int time1000; + int time10000; + + char *areabits; + + gentity_t *hovel; + + int lastPoisonTime; + int poisonImmunityTime; + gentity_t *lastPoisonClient; + int lastPoisonCloudedTime; + gentity_t *lastPoisonCloudedClient; + int grabExpiryTime; + int lastLockTime; + int lastSlowTime; + int lastBoostedTime; + int lastMedKitTime; + int medKitHealthToRestore; + int medKitIncrementTime; + int lastCreepSlowTime; // time until creep can be removed + + qboolean allowedToPounce; + + qboolean charging; + + float jetpackfuel; + + vec3_t hovelOrigin; // player origin before entering hovel + + int lastFlameBall; // s.number of the last flame ball fired + +#define RAM_FRAMES 1 // number of frames to wait before retriggering + int retriggerArmouryMenu; // frame number to retrigger the armoury menu + + unlagged_t unlaggedHist[ MAX_UNLAGGED_MARKERS ]; + unlagged_t unlaggedBackup; + unlagged_t unlaggedCalc; + int unlaggedTime; + + int tkcredits[ MAX_CLIENTS ]; + +}; + + +typedef struct spawnQueue_s +{ + int clients[ MAX_CLIENTS ]; + + int front, back; +} spawnQueue_t; + +#define QUEUE_PLUS1(x) (((x)+1)%MAX_CLIENTS) +#define QUEUE_MINUS1(x) (((x)+MAX_CLIENTS-1)%MAX_CLIENTS) + +void G_InitSpawnQueue( spawnQueue_t *sq ); +int G_GetSpawnQueueLength( spawnQueue_t *sq ); +int G_PopSpawnQueue( spawnQueue_t *sq ); +int G_PeekSpawnQueue( spawnQueue_t *sq ); +qboolean G_SearchSpawnQueue( spawnQueue_t *sq, int clientNum ); +qboolean G_PushSpawnQueue( spawnQueue_t *sq, int clientNum ); +qboolean G_RemoveFromSpawnQueue( spawnQueue_t *sq, int clientNum ); +int G_GetPosInSpawnQueue( spawnQueue_t *sq, int clientNum ); + + +#define MAX_LOCDAMAGE_TEXT 8192 +#define MAX_LOCDAMAGE_REGIONS 16 + +// store locational damage regions +typedef struct damageRegion_s +{ + float minHeight, maxHeight; + int minAngle, maxAngle; + + float modifier; + + qboolean crouch; +} damageRegion_t; + +#define MAX_ARMOUR_TEXT 8192 +#define MAX_ARMOUR_REGIONS 16 + +// store locational armour regions +typedef struct armourRegion_s +{ + float minHeight, maxHeight; + int minAngle, maxAngle; + + float modifier; + + qboolean crouch; +} armourRegion_t; + +//status of the warning of certain events +typedef enum +{ + TW_NOT = 0, + TW_IMMINENT, + TW_PASSED +} timeWarning_t; + +typedef enum +{ + BF_BUILT, + BF_DECONNED, + BF_DESTROYED, + BF_TEAMKILLED +} buildableFate_t; + +// record all changes to the buildable layout - build, decon, destroy - and +// enough information to revert that change +typedef struct buildHistory_s buildHistory_t; +struct buildHistory_s +{ + int ID; // persistent ID to aid in specific reverting + gentity_t *ent; // who, NULL if they've disconnected (or aren't an ent) + char name[ MAX_NETNAME ]; // who, saves name if ent is NULL + int buildable; // what + vec3_t origin; // where + vec3_t angles; // which way round + vec3_t origin2; // I don't know what the hell these are, but layoutsave saves + vec3_t angles2; // them so I will do the same + buildableFate_t fate; // was it built, destroyed or deconned + buildHistory_t *next; // next oldest change + buildHistory_t *marked; // linked list of markdecon buildings taken +}; + +// +// this structure is cleared as each map is entered +// +#define MAX_SPAWN_VARS 64 +#define MAX_SPAWN_VARS_CHARS 4096 + +typedef struct +{ + struct gclient_s *clients; // [maxclients] + + struct gentity_s *gentities; + int gentitySize; + int num_entities; // current number, <= MAX_GENTITIES + + fileHandle_t logFile; + + // store latched cvars here that we want to get at often + int maxclients; + + int framenum; + int time; // in msec + int previousTime; // so movers can back up when blocked + int frameMsec; // trap_Milliseconds() at end frame + + int startTime; // level.time the map was started + + int teamScores[ TEAM_NUM_TEAMS ]; + int lastTeamLocationTime; // last time of client team location update + + qboolean newSession; // don't use any old session data, because + // we changed gametype + + qboolean restarted; // waiting for a map_restart to fire + + int numConnectedClients; + int numNonSpectatorClients; // includes connecting clients + int numPlayingClients; // connected, non-spectators + int sortedClients[MAX_CLIENTS]; // sorted by score + + int numNewbies; // number of UnnamedPlayers who have been renamed this round. + + int snd_fry; // sound index for standing in lava + + // voting state + char voteString[MAX_STRING_CHARS]; + char voteDisplayString[MAX_STRING_CHARS]; + int votePassThreshold; + int voteTime; // level.time vote was called + int voteExecuteTime; // time the vote is executed + int voteYes; + int voteNo; + int numVotingClients; // set by CalculateRanks + + // team voting state + char teamVoteString[ 2 ][ MAX_STRING_CHARS ]; + char teamVoteDisplayString[ 2 ][ MAX_STRING_CHARS ]; + int teamVoteTime[ 2 ]; // level.time vote was called + int teamVoteYes[ 2 ]; + int teamVoteNo[ 2 ]; + int numteamVotingClients[ 2 ]; // set by CalculateRanks + + // spawn variables + qboolean spawning; // the G_Spawn*() functions are valid + int numSpawnVars; + char *spawnVars[ MAX_SPAWN_VARS ][ 2 ]; // key / value pairs + int numSpawnVarChars; + char spawnVarChars[ MAX_SPAWN_VARS_CHARS ]; + + // intermission state + int intermissionQueued; // intermission was qualified, but + // wait INTERMISSION_DELAY_TIME before + // actually going there so the last + // frag can be watched. Disable future + // kills during this delay + int intermissiontime; // time the intermission was started + char *changemap; + qboolean readyToExit; // at least one client wants to exit + int exitTime; + vec3_t intermission_origin; // also used for spectator spawns + vec3_t intermission_angle; + + qboolean locationLinked; // target_locations get linked + gentity_t *locationHead; // head of the location list + + int numAlienSpawns; + int numHumanSpawns; + + int numAlienClients; + int numHumanClients; + + float averageNumAlienClients; + int numAlienSamples; + float averageNumHumanClients; + int numHumanSamples; + + int numLiveAlienClients; + int numLiveHumanClients; + + int alienBuildPoints; + int humanBuildPoints; + int humanBuildPointsPowered; + + gentity_t *markedBuildables[ MAX_GENTITIES ]; + int numBuildablesForRemoval; + + int alienKills; + int humanKills; + + qboolean reactorPresent; + qboolean overmindPresent; + qboolean overmindMuted; + + int humanBaseAttackTimer; + + pTeam_t lastWin; + + int suddenDeathABuildPoints; + int suddenDeathHBuildPoints; + qboolean suddenDeath; + int suddenDeathBeginTime; + timeWarning_t suddenDeathWarning; + timeWarning_t timelimitWarning; + int extend_vote_count; + + spawnQueue_t alienSpawnQueue; + spawnQueue_t humanSpawnQueue; + + int alienStage2Time; + int alienStage3Time; + int humanStage2Time; + int humanStage3Time; + + qboolean uncondAlienWin; + qboolean uncondHumanWin; + qboolean alienTeamLocked; + qboolean humanTeamLocked; + qboolean paused; + int pauseTime; + float pause_speed; + float pause_gravity; + float pause_knockback; + int pause_ff; + int pause_ffb; + + int lastCreditedAlien; + int lastCreditedHuman; + + int unlaggedIndex; + int unlaggedTimes[ MAX_UNLAGGED_MARKERS ]; + + char layout[ MAX_QPATH ]; + + pTeam_t surrenderTeam; + buildHistory_t *buildHistory; + int lastBuildID; + int lastTeamUnbalancedTime; + int numTeamWarnings; + int lastMsgTime; + int mapRotationVoteTime; + + statsCounters_level alienStatsCounters; + statsCounters_level humanStatsCounters; + + qboolean noBuilding; + float nbArea; + float nbHeight; + + nbMarkers_t nbMarkers[ MAX_GENTITIES ]; +} level_locals_t; + +#define CMD_CHEAT 0x01 +#define CMD_MESSAGE 0x02 // sends message to others (skip when muted) +#define CMD_TEAM 0x04 // must be on a team +#define CMD_NOTEAM 0x08 // must not be on a team +#define CMD_ALIEN 0x10 +#define CMD_HUMAN 0x20 +#define CMD_LIVING 0x40 +#define CMD_INTERMISSION 0x80 // valid during intermission + +typedef struct +{ + char *cmdName; + int cmdFlags; + void ( *cmdHandler )( gentity_t *ent ); +} commands_t; + +// +// g_spawn.c +// +qboolean G_SpawnString( const char *key, const char *defaultString, char **out ); +// spawn string returns a temporary reference, you must CopyString() if you want to keep it +qboolean G_SpawnFloat( const char *key, const char *defaultString, float *out ); +qboolean G_SpawnInt( const char *key, const char *defaultString, int *out ); +qboolean G_SpawnVector( const char *key, const char *defaultString, float *out ); +void G_SpawnEntitiesFromString( void ); +char *G_NewString( const char *string ); + +// +// g_cmds.c +// +void Cmd_Score_f( gentity_t *ent ); +qboolean G_RoomForClassChange( gentity_t *ent, pClass_t class, vec3_t newOrigin ); +void G_StopFromFollowing( gentity_t *ent ); +void G_StopFollowing( gentity_t *ent ); +qboolean G_FollowNewClient( gentity_t *ent, int dir ); +void G_ToggleFollow( gentity_t *ent ); +qboolean G_MatchOnePlayer( int *plist, char *err, int len ); +int G_ClientNumbersFromString( char *s, int *plist ); +void G_Say( gentity_t *ent, gentity_t *target, int mode, const char *chatText ); +int G_SayArgc( void ); +qboolean G_SayArgv( int n, char *buffer, int bufferLength ); +char *G_SayConcatArgs( int start ); +void G_DecolorString( char *in, char *out ); +void G_ParseEscapedString( char *buffer ); +void G_LeaveTeam( gentity_t *self ); +void G_ChangeTeam( gentity_t *ent, pTeam_t newTeam ); +void G_SanitiseString( char *in, char *out, int len ); +void G_PrivateMessage( gentity_t *ent ); +char *G_statsString( statsCounters_t *sc, pTeam_t *pt ); +void Cmd_Share_f( gentity_t *ent ); +void Cmd_Donate_f( gentity_t *ent ); +void Cmd_TeamVote_f( gentity_t *ent ); +void Cmd_Builder_f( gentity_t *ent ); +void G_WordWrap( char *buffer, int maxwidth ); +void G_CP( gentity_t *ent ); +qboolean G_IsMuted( gclient_t *ent ); +qboolean G_TeamKill_Repent( gentity_t *ent ); + +// +// g_physics.c +// +void G_Physics( gentity_t *ent, int msec ); + +// +// g_buildable.c +// + +#define MAX_ALIEN_BBOX 25 + +typedef enum +{ + IBE_NONE, + + IBE_NOOVERMIND, + IBE_OVERMIND, + IBE_NOASSERT, + IBE_SPWNWARN, + IBE_NOCREEP, + IBE_HOVEL, + IBE_HOVELEXIT, + + IBE_REACTOR, + IBE_REPEATER, + IBE_TNODEWARN, + IBE_RPTWARN, + IBE_RPTWARN2, + IBE_NOPOWER, + IBE_NODCC, + + IBE_NORMAL, + IBE_NOROOM, + IBE_PERMISSION, + + IBE_MAXERRORS +} itemBuildError_t; + +qboolean AHovel_Blocked( gentity_t *hovel, gentity_t *player, qboolean provideExit ); +gentity_t *G_CheckSpawnPoint( int spawnNum, vec3_t origin, vec3_t normal, + buildable_t spawn, vec3_t spawnOrigin ); + +qboolean G_IsPowered( vec3_t origin ); +qboolean G_IsDCCBuilt( void ); +qboolean G_IsOvermindBuilt( void ); + +void G_BuildableThink( gentity_t *ent, int msec ); +qboolean G_BuildableRange( vec3_t origin, float r, buildable_t buildable ); +itemBuildError_t G_CanBuild( gentity_t *ent, buildable_t buildable, int distance, vec3_t origin ); +qboolean G_BuildingExists( int bclass ) ; +qboolean G_BuildIfValid( gentity_t *ent, buildable_t buildable ); +void G_SetBuildableAnim( gentity_t *ent, buildableAnimNumber_t anim, qboolean force ); +void G_SetIdleBuildableAnim( gentity_t *ent, buildableAnimNumber_t anim ); +void G_SpawnBuildable(gentity_t *ent, buildable_t buildable); +void FinishSpawningBuildable( gentity_t *ent ); +void G_CheckDBProtection( void ); +void G_LayoutSave( char *name ); +int G_LayoutList( const char *map, char *list, int len ); +void G_LayoutSelect( void ); +void G_LayoutLoad( void ); +void G_BaseSelfDestruct( pTeam_t team ); +gentity_t *G_InstantBuild( buildable_t buildable, vec3_t origin, vec3_t angles, vec3_t origin2, vec3_t angles2 ); +void G_SpawnRevertedBuildable( buildHistory_t *bh, qboolean mark ); +void G_CommitRevertedBuildable( gentity_t *ent ); +qboolean G_RevertCanFit( buildHistory_t *bh ); +int G_LogBuild( buildHistory_t *new ); +int G_CountBuildLog( void ); +char *G_FindBuildLogName( int id ); +void G_NobuildSave( void ); +void G_NobuildLoad( void ); + +// +// g_utils.c +// +int G_ParticleSystemIndex( char *name ); +int G_ShaderIndex( char *name ); +int G_ModelIndex( char *name ); +int G_SoundIndex( char *name ); +void G_TeamCommand( pTeam_t team, char *cmd ); +void G_KillBox (gentity_t *ent); +gentity_t *G_Find (gentity_t *from, int fieldofs, const char *match); +gentity_t *G_PickTarget (char *targetname); +void G_UseTargets (gentity_t *ent, gentity_t *activator); +void G_SetMovedir ( vec3_t angles, vec3_t movedir); + +void G_InitGentity( gentity_t *e ); +gentity_t *G_Spawn( void ); +gentity_t *G_TempEntity( vec3_t origin, int event ); +void G_Sound( gentity_t *ent, int channel, int soundIndex ); +void G_FreeEntity( gentity_t *e ); +qboolean G_EntitiesFree( void ); + +void G_TouchTriggers( gentity_t *ent ); +void G_TouchSolids( gentity_t *ent ); + +float *tv( float x, float y, float z ); +char *vtos( const vec3_t v ); + +float vectoyaw( const vec3_t vec ); + +void G_AddPredictableEvent( gentity_t *ent, int event, int eventParm ); +void G_AddEvent( gentity_t *ent, int event, int eventParm ); +void G_BroadcastEvent( int event, int eventParm ); +void G_SetOrigin( gentity_t *ent, vec3_t origin ); +void AddRemap(const char *oldShader, const char *newShader, float timeOffset); +const char *BuildShaderStateConfig( void ); + + +qboolean G_ClientIsLagging( gclient_t *client ); + +void G_TriggerMenu( int clientNum, dynMenu_t menu ); +void G_CloseMenus( int clientNum ); + +qboolean G_Visible( gentity_t *ent1, gentity_t *ent2 ); +gentity_t *G_ClosestEnt( vec3_t origin, gentity_t **entities, int numEntities ); + +// +// g_combat.c +// +qboolean CanDamage( gentity_t *targ, vec3_t origin ); +void G_Damage( gentity_t *targ, gentity_t *inflictor, gentity_t *attacker, + vec3_t dir, vec3_t point, int damage, int dflags, int mod ); +void G_SelectiveDamage( gentity_t *targ, gentity_t *inflictor, gentity_t *attacker, vec3_t dir, + vec3_t point, int damage, int dflags, int mod, int team ); +qboolean G_RadiusDamage( vec3_t origin, gentity_t *attacker, float damage, float radius, + gentity_t *ignore, int mod ); +qboolean G_SelectiveRadiusDamage( vec3_t origin, gentity_t *attacker, float damage, float radius, + gentity_t *ignore, int mod, int team ); +void G_Knockback( gentity_t *targ, vec3_t dir, int knockback ); +void body_die( gentity_t *self, gentity_t *inflictor, gentity_t *attacker, int damage, int meansOfDeath ); +void AddScore( gentity_t *ent, int score ); + +void G_InitDamageLocations( void ); + +// damage flags +#define DAMAGE_RADIUS 0x00000001 // damage was indirect +#define DAMAGE_NO_ARMOR 0x00000002 // armour does not protect from this damage +#define DAMAGE_NO_KNOCKBACK 0x00000004 // do not affect velocity, just view angles +#define DAMAGE_NO_PROTECTION 0x00000008 // armor, shields, invulnerability, and godmode have no effect +#define DAMAGE_NO_LOCDAMAGE 0x00000010 // do not apply locational damage + +// +// g_missile.c +// +void G_RunMissile( gentity_t *ent ); + +gentity_t *fire_flamer( gentity_t *self, vec3_t start, vec3_t aimdir ); +gentity_t *fire_blaster( gentity_t *self, vec3_t start, vec3_t dir ); +gentity_t *fire_pulseRifle( gentity_t *self, vec3_t start, vec3_t dir ); +gentity_t *fire_luciferCannon( gentity_t *self, vec3_t start, vec3_t dir, int damage, int radius ); +gentity_t *fire_lockblob( gentity_t *self, vec3_t start, vec3_t dir ); +gentity_t *fire_paraLockBlob( gentity_t *self, vec3_t start, vec3_t dir ); +gentity_t *fire_slowBlob( gentity_t *self, vec3_t start, vec3_t dir ); +gentity_t *fire_bounceBall( gentity_t *self, vec3_t start, vec3_t dir ); +gentity_t *fire_hive( gentity_t *self, vec3_t start, vec3_t dir ); +gentity_t *launch_grenade( gentity_t *self, vec3_t start, vec3_t dir ); + + +// +// g_mover.c +// +void G_RunMover( gentity_t *ent ); +void Touch_DoorTrigger( gentity_t *ent, gentity_t *other, trace_t *trace ); +void manualTriggerSpectator( gentity_t *trigger, gentity_t *player ); + +// +// g_trigger.c +// +void trigger_teleporter_touch( gentity_t *self, gentity_t *other, trace_t *trace ); +void G_Checktrigger_stages( pTeam_t team, stage_t stage ); + + +// +// g_misc.c +// +void TeleportPlayer( gentity_t *player, vec3_t origin, vec3_t angles ); +void ShineTorch( gentity_t *self ); + +// +// g_weapon.c +// + +#define MAX_ZAP_TARGETS LEVEL2_AREAZAP_MAX_TARGETS + +typedef struct zap_s +{ + qboolean used; + + gentity_t *creator; + gentity_t *targets[ MAX_ZAP_TARGETS ]; + int numTargets; + + int timeToLive; + int damageUsed; + + gentity_t *effectChannel; +} zap_t; + +void G_ForceWeaponChange( gentity_t *ent, weapon_t weapon ); +void G_GiveClientMaxAmmo( gentity_t *ent, qboolean buyingEnergyAmmo ); +void CalcMuzzlePoint( gentity_t *ent, vec3_t forward, vec3_t right, vec3_t up, vec3_t muzzlePoint ); +void SnapVectorTowards( vec3_t v, vec3_t to ); +qboolean CheckVenomAttack( gentity_t *ent ); +void CheckGrabAttack( gentity_t *ent ); +qboolean CheckPounceAttack( gentity_t *ent ); +void ChargeAttack( gentity_t *ent, gentity_t *victim ); +void G_UpdateZaps( int msec ); + + +// +// g_client.c +// +void G_AddCreditToClient( gclient_t *client, short credit, qboolean cap ); +team_t TeamCount( int ignoreClientNum, int team ); +void G_SetClientViewAngle( gentity_t *ent, vec3_t angle ); +gentity_t *G_SelectTremulousSpawnPoint( pTeam_t team, vec3_t preference, vec3_t origin, vec3_t angles ); +gentity_t *G_SelectSpawnPoint( vec3_t avoidPoint, vec3_t origin, vec3_t angles ); +gentity_t *G_SelectAlienLockSpawnPoint( vec3_t origin, vec3_t angles ); +gentity_t *G_SelectHumanLockSpawnPoint( vec3_t origin, vec3_t angles ); +void SpawnCorpse( gentity_t *ent ); +void respawn( gentity_t *ent ); +void BeginIntermission( void ); +void ClientSpawn( gentity_t *ent, gentity_t *spawn, vec3_t origin, vec3_t angles ); +void player_die( gentity_t *self, gentity_t *inflictor, gentity_t *attacker, int damage, int mod ); +qboolean SpotWouldTelefrag( gentity_t *spot ); +char *G_NextNewbieName( gentity_t *ent ); + +// +// g_svcmds.c +// +qboolean ConsoleCommand( void ); +void G_ProcessIPBans( void ); +qboolean G_FilterPacket( char *from ); + +// +// g_weapon.c +// +void FireWeapon( gentity_t *ent ); +void FireWeapon2( gentity_t *ent ); +void FireWeapon3( gentity_t *ent ); + +// +// g_cmds.c +// + +// +// g_main.c +// +void ScoreboardMessage( gentity_t *client ); +void MoveClientToIntermission( gentity_t *client ); +void G_MapConfigs( const char *mapname ); +void CalculateRanks( void ); +void FindIntermissionPoint( void ); +void G_RunThink( gentity_t *ent ); +void QDECL G_LogPrintf( const char *fmt, ... ); +void QDECL G_LogPrintfColoured( const char *fmt, ... ); +void QDECL G_LogOnlyPrintf( const char *fmt, ... ); +void QDECL G_AdminsPrintf( const char *fmt, ... ); +void QDECL G_WarningsPrintf( char *flag, const char *fmt, ... ); +void QDECL G_LogOnlyPrintf( const char *fmt, ... ); +void SendScoreboardMessageToAllClients( void ); +void QDECL G_Printf( const char *fmt, ... ); +void QDECL G_Error( const char *fmt, ... ); +void CheckVote( void ); +void CheckTeamVote( int teamnum ); +void LogExit( const char *string ); +int G_TimeTilSuddenDeath( void ); +void CheckMsgTimer( void ); +qboolean G_Flood_Limited( gentity_t *ent ); + +// +// g_client.c +// +char *ClientConnect( int clientNum, qboolean firstTime ); +void ClientUserinfoChanged( int clientNum, qboolean forceName ); +void ClientDisconnect( int clientNum ); +void ClientBegin( int clientNum ); +void ClientCommand( int clientNum ); + +// +// g_active.c +// +void G_UnlaggedStore( void ); +void G_UnlaggedClear( gentity_t *ent ); +void G_UnlaggedCalc( int time, gentity_t *skipEnt ); +void G_UnlaggedOn( gentity_t *attacker, vec3_t muzzle, float range ); +void G_UnlaggedOff( void ); +void ClientThink( int clientNum ); +void ClientEndFrame( gentity_t *ent ); +void G_RunClient( gentity_t *ent ); + +// +// g_team.c +// +qboolean OnSameTeam( gentity_t *ent1, gentity_t *ent2 ); +gentity_t *Team_GetLocation( gentity_t *ent ); +qboolean Team_GetLocationMsg( gentity_t *ent, char *loc, int loclen ); +void TeamplayInfoMessage( gentity_t *ent ); +void CheckTeamStatus( void ); + +// +// g_mem.c +// +void *G_Alloc( int size ); +void G_InitMemory( void ); +void G_Free( void *ptr ); +void G_DefragmentMemory( void ); +void Svcmd_GameMem_f( void ); + +// +// g_session.c +// +void G_ReadSessionData( gclient_t *client ); +void G_InitSessionData( gclient_t *client, char *userinfo ); +void G_WriteSessionData( void ); + +// +// g_maprotation.c +// +#define MAX_MAP_ROTATIONS 16 +#define MAX_MAP_ROTATION_MAPS 64 +#define MAX_MAP_COMMANDS 16 +#define MAX_MAP_ROTATION_CONDS 8 + +#define NOT_ROTATING -1 + +typedef enum +{ + MCV_ERR, + MCV_RANDOM, + MCV_NUMCLIENTS, + MCV_LASTWIN, + MCV_VOTE, + MCV_SELECTEDRANDOM +} mapConditionVariable_t; + +typedef enum +{ + MCO_LT, + MCO_EQ, + MCO_GT +} mapConditionOperator_t; + +typedef enum +{ + MCT_ERR, + MCT_MAP, + MCT_ROTATION +} mapConditionType_t; + +typedef struct mapRotationCondition_s +{ + char dest[ MAX_QPATH ]; + + qboolean unconditional; + + mapConditionVariable_t lhs; + mapConditionOperator_t op; + + int numClients; + pTeam_t lastWin; +} mapRotationCondition_t; + +typedef struct mapRotationEntry_s +{ + char name[ MAX_QPATH ]; + + char postCmds[ MAX_MAP_COMMANDS ][ MAX_STRING_CHARS ]; + char layouts[ MAX_CVAR_VALUE_STRING ]; + int numCmds; + + mapRotationCondition_t conditions[ MAX_MAP_ROTATION_CONDS ]; + int numConditions; +} mapRotationEntry_t; + +typedef struct mapRotation_s +{ + char name[ MAX_QPATH ]; + + mapRotationEntry_t maps[ MAX_MAP_ROTATION_MAPS ]; + int numMaps; + int currentMap; +} mapRotation_t; + +typedef struct mapRotations_s +{ + mapRotation_t rotations[ MAX_MAP_ROTATIONS ]; + int numRotations; +} mapRotations_t; + +void G_PrintRotations( void ); +qboolean G_AdvanceMapRotation( void ); +qboolean G_StartMapRotation( char *name, qboolean changeMap ); +void G_StopMapRotation( void ); +qboolean G_MapRotationActive( void ); +void G_InitMapRotations( void ); +qboolean G_MapExists( char *name ); +int G_GetCurrentMap( int rotation ); + +qboolean G_CheckMapRotationVote( void ); +qboolean G_IntermissionMapVoteWinner( void ); +void G_IntermissionMapVoteMessage( gentity_t *ent ); +void G_IntermissionMapVoteMessageAll( void ); +void G_IntermissionMapVoteCommand( gentity_t *ent, qboolean next, qboolean choose ); +static qboolean G_GetRandomMap( char *name, int size, int rotation, int map ); + +// +// g_ptr.c +// +void G_UpdatePTRConnection( gclient_t *client ); +connectionRecord_t *G_GenerateNewConnection( gclient_t *client ); +void G_ResetPTRConnections( void ); +connectionRecord_t *G_FindConnectionForCode( int code ); + + +//some maxs +#define MAX_FILEPATH 144 + +extern level_locals_t level; +extern gentity_t g_entities[ MAX_GENTITIES ]; + +#define FOFS(x) ((int)&(((gentity_t *)0)->x)) + +extern vmCvar_t g_dedicated; +extern vmCvar_t g_cheats; +extern vmCvar_t g_maxclients; // allow this many total, including spectators +extern vmCvar_t g_maxGameClients; // allow this many active +extern vmCvar_t g_restarted; +extern vmCvar_t g_lockTeamsAtStart; +extern vmCvar_t g_minCommandPeriod; +extern vmCvar_t g_minNameChangePeriod; +extern vmCvar_t g_maxNameChanges; +extern vmCvar_t g_newbieNumbering; +extern vmCvar_t g_newbieNamePrefix; + +extern vmCvar_t g_timelimit; +extern vmCvar_t g_suddenDeathTime; +extern vmCvar_t g_suddenDeath; +extern vmCvar_t g_suddenDeathMode; +extern vmCvar_t g_friendlyFire; +extern vmCvar_t g_friendlyFireHumans; +extern vmCvar_t g_friendlyFireAliens; +extern vmCvar_t g_retribution; +extern vmCvar_t g_friendlyFireMovementAttacks; +extern vmCvar_t g_friendlyBuildableFire; +extern vmCvar_t g_password; +extern vmCvar_t g_needpass; +extern vmCvar_t g_gravity; +extern vmCvar_t g_speed; +extern vmCvar_t g_knockback; +extern vmCvar_t g_quadfactor; +extern vmCvar_t g_inactivity; +extern vmCvar_t g_debugMove; +extern vmCvar_t g_debugAlloc; +extern vmCvar_t g_debugDamage; +extern vmCvar_t g_weaponRespawn; +extern vmCvar_t g_weaponTeamRespawn; +extern vmCvar_t g_synchronousClients; +extern vmCvar_t g_motd; +extern vmCvar_t g_warmup; +extern vmCvar_t g_warmupMode; +extern vmCvar_t g_doWarmup; +extern vmCvar_t g_blood; +extern vmCvar_t g_allowVote; +extern vmCvar_t g_requireVoteReasons; +extern vmCvar_t g_voteLimit; +extern vmCvar_t g_suddenDeathVotePercent; +extern vmCvar_t g_suddenDeathVoteDelay; +extern vmCvar_t g_extendVotesPercent; +extern vmCvar_t g_extendVotesTime; +extern vmCvar_t g_extendVotesCount; +extern vmCvar_t g_kickVotesPercent; +extern vmCvar_t g_customVote1; +extern vmCvar_t g_customVote2; +extern vmCvar_t g_customVote3; +extern vmCvar_t g_customVote4; +extern vmCvar_t g_customVote5; +extern vmCvar_t g_customVote6; +extern vmCvar_t g_customVote7; +extern vmCvar_t g_customVote8; +#define CUSTOM_VOTE_COUNT 8 +extern vmCvar_t g_customVotePercent; +extern vmCvar_t g_mapVotesPercent; +extern vmCvar_t g_mapRotationVote; +extern vmCvar_t g_extendVotesPercent; +extern vmCvar_t g_extendVotesTime; +extern vmCvar_t g_extendVotesCount; +extern vmCvar_t g_readyPercent; +extern vmCvar_t g_designateVotes; +extern vmCvar_t g_teamAutoJoin; +extern vmCvar_t g_teamForceBalance; +extern vmCvar_t g_banIPs; +extern vmCvar_t g_filterBan; +extern vmCvar_t g_smoothClients; +extern vmCvar_t g_clientUpgradeNotice; +extern vmCvar_t pmove_fixed; +extern vmCvar_t pmove_msec; +extern vmCvar_t g_rankings; +extern vmCvar_t g_allowShare; +extern vmCvar_t g_creditOverflow; +extern vmCvar_t g_enableDust; +extern vmCvar_t g_enableBreath; +extern vmCvar_t g_singlePlayer; + +extern vmCvar_t g_humanBuildPoints; +extern vmCvar_t g_alienBuildPoints; +extern vmCvar_t g_humanStage; +extern vmCvar_t g_humanKills; +extern vmCvar_t g_humanMaxStage; +extern vmCvar_t g_humanStage2Threshold; +extern vmCvar_t g_humanStage3Threshold; +extern vmCvar_t g_alienStage; +extern vmCvar_t g_alienKills; +extern vmCvar_t g_alienMaxStage; +extern vmCvar_t g_alienStage2Threshold; +extern vmCvar_t g_alienStage3Threshold; +extern vmCvar_t g_teamImbalanceWarnings; + +extern vmCvar_t g_unlagged; + +extern vmCvar_t g_disabledEquipment; +extern vmCvar_t g_disabledClasses; +extern vmCvar_t g_disabledBuildables; + +extern vmCvar_t g_markDeconstruct; +extern vmCvar_t g_markDeconstructMode; +extern vmCvar_t g_deconDead; + +extern vmCvar_t g_debugMapRotation; +extern vmCvar_t g_currentMapRotation; +extern vmCvar_t g_currentMap; +extern vmCvar_t g_nextMap; +extern vmCvar_t g_initialMapRotation; +extern vmCvar_t g_chatTeamPrefix; +extern vmCvar_t g_actionPrefix; +extern vmCvar_t g_floodMaxDemerits; +extern vmCvar_t g_floodMinTime; +extern vmCvar_t g_spamTime; + +extern vmCvar_t g_shove; + +extern vmCvar_t g_mapConfigs; + +extern vmCvar_t g_layouts; +extern vmCvar_t g_layoutAuto; + +extern vmCvar_t g_admin; +extern vmCvar_t g_adminLog; +extern vmCvar_t g_adminParseSay; +extern vmCvar_t g_adminSayFilter; +extern vmCvar_t g_adminNameProtect; +extern vmCvar_t g_adminTempMute; +extern vmCvar_t g_adminTempBan; +extern vmCvar_t g_adminMaxBan; +extern vmCvar_t g_adminTempSpec; +extern vmCvar_t g_adminMapLog; +extern vmCvar_t g_minLevelToJoinTeam; +extern vmCvar_t g_minDeconLevel; +extern vmCvar_t g_minDeconAffectsMark; +extern vmCvar_t g_forceAutoSelect; +extern vmCvar_t g_minLevelToSpecMM1; +extern vmCvar_t g_banNotice; + +extern vmCvar_t g_devmapKillerHP; +extern vmCvar_t g_killerHP; + +extern vmCvar_t g_privateMessages; +extern vmCvar_t g_fullIgnore; +extern vmCvar_t g_decolourLogfiles; +extern vmCvar_t g_publicSayadmins; +extern vmCvar_t g_myStats; +extern vmCvar_t g_teamStatus; +extern vmCvar_t g_antiSpawnBlock; + +extern vmCvar_t g_dretchPunt; + +extern vmCvar_t g_devmapNoGod; +extern vmCvar_t g_devmapNoStructDmg; + +extern vmCvar_t g_slapKnockback; +extern vmCvar_t g_slapDamage; + +extern vmCvar_t g_voteMinTime; +extern vmCvar_t g_mapvoteMaxTime; +extern vmCvar_t g_votableMaps; + +extern vmCvar_t g_msg; +extern vmCvar_t g_msgTime; +extern vmCvar_t g_welcomeMsg; +extern vmCvar_t g_welcomeMsgTime; + +extern vmCvar_t g_buildLogMaxLength; + +extern vmCvar_t g_AllStats; +extern vmCvar_t g_AllStatsTime; + +extern vmCvar_t mod_jetpackFuel; +extern vmCvar_t mod_jetpackConsume; +extern vmCvar_t mod_jetpackRegen; + +extern vmCvar_t g_adminExpireTime; + +extern vmCvar_t g_autoGhost; + +extern vmCvar_t g_teamKillThreshold; + +extern vmCvar_t g_aimbotAdvertBan; +extern vmCvar_t g_aimbotAdvertBanTime; +extern vmCvar_t g_aimbotAdvertBanReason; + +void trap_Printf( const char *fmt ); +void trap_Error( const char *fmt ); +int trap_Milliseconds( void ); +int trap_RealTime( qtime_t *qtime ); +int trap_Argc( void ); +void trap_Argv( int n, char *buffer, int bufferLength ); +void trap_Args( char *buffer, int bufferLength ); +int trap_FS_FOpenFile( const char *qpath, fileHandle_t *f, fsMode_t mode ); +void trap_FS_Read( void *buffer, int len, fileHandle_t f ); +void trap_FS_Write( const void *buffer, int len, fileHandle_t f ); +void trap_FS_FCloseFile( fileHandle_t f ); +int trap_FS_GetFileList( const char *path, const char *extension, char *listbuf, int bufsize ); +int trap_FS_Seek( fileHandle_t f, long offset, int origin ); // fsOrigin_t +void trap_SendConsoleCommand( int exec_when, const char *text ); +void trap_Cvar_Register( vmCvar_t *cvar, const char *var_name, const char *value, int flags ); +void trap_Cvar_Update( vmCvar_t *cvar ); +void trap_Cvar_Set( const char *var_name, const char *value ); +int trap_Cvar_VariableIntegerValue( const char *var_name ); +float trap_Cvar_VariableValue( const char *var_name ); +void trap_Cvar_VariableStringBuffer( const char *var_name, char *buffer, int bufsize ); +void trap_LocateGameData( gentity_t *gEnts, int numGEntities, int sizeofGEntity_t, + playerState_t *gameClients, int sizeofGameClient ); +void trap_DropClient( int clientNum, const char *reason ); +void trap_SendServerCommand( int clientNum, const char *text ); +void trap_SetConfigstring( int num, const char *string ); +void trap_GetConfigstring( int num, char *buffer, int bufferSize ); +void trap_GetUserinfo( int num, char *buffer, int bufferSize ); +void trap_SetUserinfo( int num, const char *buffer ); +void trap_GetServerinfo( char *buffer, int bufferSize ); +void trap_SetBrushModel( gentity_t *ent, const char *name ); +void trap_Trace( trace_t *results, const vec3_t start, const vec3_t mins, const vec3_t maxs, + const vec3_t end, int passEntityNum, int contentmask ); +int trap_PointContents( const vec3_t point, int passEntityNum ); +qboolean trap_InPVS( const vec3_t p1, const vec3_t p2 ); +qboolean trap_InPVSIgnorePortals( const vec3_t p1, const vec3_t p2 ); +void trap_AdjustAreaPortalState( gentity_t *ent, qboolean open ); +qboolean trap_AreasConnected( int area1, int area2 ); +void trap_LinkEntity( gentity_t *ent ); +void trap_UnlinkEntity( gentity_t *ent ); +int trap_EntitiesInBox( const vec3_t mins, const vec3_t maxs, int *entityList, int maxcount ); +qboolean trap_EntityContact( const vec3_t mins, const vec3_t maxs, const gentity_t *ent ); +int trap_BotAllocateClient( void ); +void trap_BotFreeClient( int clientNum ); +void trap_GetUsercmd( int clientNum, usercmd_t *cmd ); +qboolean trap_GetEntityToken( char *buffer, int bufferSize ); + +void trap_SnapVector( float *v ); +void trap_SendGameStat( const char *data ); diff --git a/src/game/g_main.c b/src/game/g_main.c new file mode 100644 index 0000000..b946238 --- /dev/null +++ b/src/game/g_main.c @@ -0,0 +1,3001 @@ +/* +=========================================================================== +Copyright (C) 1999-2005 Id Software, Inc. +Copyright (C) 2000-2006 Tim Angus + +This file is part of Tremulous. + +Tremulous is free software; you can redistribute it +and/or modify it under the terms of the GNU General Public License as +published by the Free Software Foundation; either version 2 of the License, +or (at your option) any later version. + +Tremulous is distributed in the hope that it will be +useful, but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with Tremulous; if not, write to the Free Software +Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA +=========================================================================== +*/ + +#include "g_local.h" + +#define QVM_NAME "Slackers QVM" " (Lakitu7 5.5)" +#define QVM_VERSIONNUM "1.1+" + +level_locals_t level; + +typedef struct +{ + vmCvar_t *vmCvar; + char *cvarName; + char *defaultString; + int cvarFlags; + int modificationCount; // for tracking changes + qboolean trackChange; // track this variable, and announce if changed + qboolean teamShader; // track and if changed, update shader state +} cvarTable_t; + +gentity_t g_entities[ MAX_GENTITIES ]; +gclient_t g_clients[ MAX_CLIENTS ]; + +vmCvar_t g_fraglimit; +vmCvar_t g_timelimit; +vmCvar_t g_suddenDeathTime; +vmCvar_t g_suddenDeath; +vmCvar_t g_suddenDeathMode; +vmCvar_t g_capturelimit; +vmCvar_t g_friendlyFire; +vmCvar_t g_friendlyFireAliens; +vmCvar_t g_friendlyFireHumans; +vmCvar_t g_friendlyFireMovementAttacks; +vmCvar_t g_retribution; +vmCvar_t g_friendlyBuildableFire; +vmCvar_t g_password; +vmCvar_t g_needpass; +vmCvar_t g_maxclients; +vmCvar_t g_maxGameClients; +vmCvar_t g_dedicated; +vmCvar_t g_speed; +vmCvar_t g_gravity; +vmCvar_t g_cheats; +vmCvar_t g_knockback; +vmCvar_t g_quadfactor; +vmCvar_t g_inactivity; +vmCvar_t g_debugMove; +vmCvar_t g_debugDamage; +vmCvar_t g_debugAlloc; +vmCvar_t g_weaponRespawn; +vmCvar_t g_weaponTeamRespawn; +vmCvar_t g_motd; +vmCvar_t g_synchronousClients; +vmCvar_t g_warmup; +vmCvar_t g_warmupMode; +vmCvar_t g_doWarmup; +vmCvar_t g_restarted; +vmCvar_t g_lockTeamsAtStart; +vmCvar_t g_logFile; +vmCvar_t g_logFileSync; +vmCvar_t g_blood; +vmCvar_t g_podiumDist; +vmCvar_t g_podiumDrop; +vmCvar_t g_allowVote; +vmCvar_t g_requireVoteReasons; +vmCvar_t g_voteLimit; +vmCvar_t g_suddenDeathVotePercent; +vmCvar_t g_suddenDeathVoteDelay; +vmCvar_t g_extendVotesPercent; +vmCvar_t g_extendVotesTime; +vmCvar_t g_extendVotesCount; +vmCvar_t g_kickVotesPercent; +vmCvar_t g_customVote1; +vmCvar_t g_customVote2; +vmCvar_t g_customVote3; +vmCvar_t g_customVote4; +vmCvar_t g_customVote5; +vmCvar_t g_customVote6; +vmCvar_t g_customVote7; +vmCvar_t g_customVote8; +vmCvar_t g_customVotePercent; +vmCvar_t g_mapVotesPercent; +vmCvar_t g_extendVotesPercent; +vmCvar_t g_extendVotesTime; +vmCvar_t g_extendVotesCount; +vmCvar_t g_mapRotationVote; +vmCvar_t g_readyPercent; +vmCvar_t g_designateVotes; +vmCvar_t g_teamAutoJoin; +vmCvar_t g_teamForceBalance; +vmCvar_t g_banIPs; +vmCvar_t g_filterBan; +vmCvar_t g_smoothClients; +vmCvar_t g_clientUpgradeNotice; +vmCvar_t pmove_fixed; +vmCvar_t pmove_msec; +vmCvar_t g_rankings; +vmCvar_t g_listEntity; +vmCvar_t g_minCommandPeriod; +vmCvar_t g_minNameChangePeriod; +vmCvar_t g_maxNameChanges; +vmCvar_t g_newbieNumbering; +vmCvar_t g_newbieNamePrefix; + +vmCvar_t g_humanBuildPoints; +vmCvar_t g_alienBuildPoints; +vmCvar_t g_humanStage; +vmCvar_t g_humanKills; +vmCvar_t g_humanMaxStage; +vmCvar_t g_humanStage2Threshold; +vmCvar_t g_humanStage3Threshold; +vmCvar_t g_alienStage; +vmCvar_t g_alienKills; +vmCvar_t g_alienMaxStage; +vmCvar_t g_alienStage2Threshold; +vmCvar_t g_alienStage3Threshold; +vmCvar_t g_teamImbalanceWarnings; + +vmCvar_t g_unlagged; + +vmCvar_t g_disabledEquipment; +vmCvar_t g_disabledClasses; +vmCvar_t g_disabledBuildables; + +vmCvar_t g_markDeconstruct; +vmCvar_t g_markDeconstructMode; +vmCvar_t g_deconDead; + +vmCvar_t g_debugMapRotation; +vmCvar_t g_currentMapRotation; +vmCvar_t g_currentMap; +vmCvar_t g_nextMap; +vmCvar_t g_initialMapRotation; + +vmCvar_t g_shove; + +vmCvar_t g_mapConfigs; +vmCvar_t g_chatTeamPrefix; +vmCvar_t g_actionPrefix; +vmCvar_t g_floodMaxDemerits; +vmCvar_t g_floodMinTime; +vmCvar_t g_spamTime; + +vmCvar_t g_layouts; +vmCvar_t g_layoutAuto; + +vmCvar_t g_admin; +vmCvar_t g_adminLog; +vmCvar_t g_adminParseSay; +vmCvar_t g_adminSayFilter; +vmCvar_t g_adminNameProtect; +vmCvar_t g_adminTempMute; +vmCvar_t g_adminTempBan; +vmCvar_t g_adminMaxBan; +vmCvar_t g_adminTempSpec; +vmCvar_t g_adminMapLog; +vmCvar_t g_minLevelToJoinTeam; +vmCvar_t g_minDeconLevel; +vmCvar_t g_minDeconAffectsMark; +vmCvar_t g_forceAutoSelect; + +vmCvar_t g_privateMessages; +vmCvar_t g_fullIgnore; +vmCvar_t g_decolourLogfiles; +vmCvar_t g_minLevelToSpecMM1; +vmCvar_t g_publicSayadmins; +vmCvar_t g_myStats; +vmCvar_t g_AllStats; +vmCvar_t g_AllStatsTime; +vmCvar_t g_teamStatus; +vmCvar_t g_antiSpawnBlock; +vmCvar_t g_banNotice; + +vmCvar_t g_devmapKillerHP; +vmCvar_t g_killerHP; + +vmCvar_t g_buildLogMaxLength; + +vmCvar_t g_tag; + +vmCvar_t g_dretchPunt; + +vmCvar_t g_allowShare; +vmCvar_t g_creditOverflow; + +vmCvar_t g_devmapNoGod; +vmCvar_t g_devmapNoStructDmg; + +vmCvar_t g_slapKnockback; +vmCvar_t g_slapDamage; + +vmCvar_t g_voteMinTime; +vmCvar_t g_mapvoteMaxTime; +vmCvar_t g_votableMaps; + +vmCvar_t g_msg; +vmCvar_t g_msgTime; +vmCvar_t g_welcomeMsg; +vmCvar_t g_welcomeMsgTime; + + +vmCvar_t mod_jetpackFuel; +vmCvar_t mod_jetpackConsume; +vmCvar_t mod_jetpackRegen; + +vmCvar_t g_adminExpireTime; + +vmCvar_t g_autoGhost; + +vmCvar_t g_teamKillThreshold; + +vmCvar_t g_aimbotAdvertBan; +vmCvar_t g_aimbotAdvertBanTime; +vmCvar_t g_aimbotAdvertBanReason; + +static cvarTable_t gameCvarTable[ ] = +{ + // don't override the cheat state set by the system + { &g_cheats, "sv_cheats", "", 0, 0, qfalse }, + + // noset vars + { NULL, "gamename", GAME_VERSION , CVAR_SERVERINFO | CVAR_ROM, 0, qfalse }, + { NULL, "gamedate", __DATE__ , CVAR_ROM, 0, qfalse }, + { &g_restarted, "g_restarted", "0", CVAR_ROM, 0, qfalse }, + { &g_lockTeamsAtStart, "g_lockTeamsAtStart", "0", CVAR_ROM, 0, qfalse }, + { NULL, "sv_mapname", "", CVAR_SERVERINFO | CVAR_ROM, 0, qfalse }, + { NULL, "P", "", CVAR_SERVERINFO | CVAR_ROM, 0, qfalse }, + { NULL, "ff", "0", CVAR_SERVERINFO | CVAR_ROM, 0, qfalse }, + { NULL, "qvm_version", QVM_NAME " " QVM_VERSIONNUM " (" __DATE__ ", " __TIME__ ")", CVAR_SERVERINFO | CVAR_ROM, 0, qfalse }, + + // latched vars + + { &g_maxclients, "sv_maxclients", "8", CVAR_SERVERINFO | CVAR_LATCH | CVAR_ARCHIVE, 0, qfalse }, + { &g_maxGameClients, "g_maxGameClients", "0", CVAR_SERVERINFO | CVAR_ARCHIVE | CVAR_NORESTART, 0, qtrue }, + + // change anytime vars + { &g_timelimit, "timelimit", "45", CVAR_SERVERINFO | CVAR_ARCHIVE | CVAR_NORESTART, 0, qtrue }, + { &g_suddenDeathTime, "g_suddenDeathTime", "30", CVAR_SERVERINFO | CVAR_ARCHIVE | CVAR_NORESTART, 0, qtrue }, + { &g_suddenDeathMode, "g_suddenDeathMode", "0", CVAR_SERVERINFO | CVAR_ARCHIVE | CVAR_NORESTART, 0, qtrue }, + { &g_suddenDeath, "g_suddenDeath", "0", CVAR_SERVERINFO | CVAR_NORESTART, 0, qtrue }, + + { &g_synchronousClients, "g_synchronousClients", "0", CVAR_SYSTEMINFO, 0, qfalse }, + + { &g_friendlyFire, "g_friendlyFire", "0", CVAR_ARCHIVE | CVAR_SERVERINFO, 0, qtrue }, + { &g_friendlyFireAliens, "g_friendlyFireAliens", "0", CVAR_ARCHIVE, 0, qtrue }, + { &g_friendlyFireHumans, "g_friendlyFireHumans", "0", CVAR_ARCHIVE, 0, qtrue }, + { &g_retribution, "g_retribution", "0", CVAR_ARCHIVE, 0, qtrue }, + { &g_friendlyBuildableFire, "g_friendlyBuildableFire", "0", CVAR_ARCHIVE | CVAR_SERVERINFO, 0, qtrue }, + { &g_friendlyFireMovementAttacks, "g_friendlyFireMovementAttacks", "1", CVAR_ARCHIVE, 0, qtrue }, + { &g_devmapNoGod, "g_devmapNoGod", "0", CVAR_ARCHIVE, 0, qtrue }, + { &g_devmapNoStructDmg, "g_devmapNoStructDmg", "0", CVAR_ARCHIVE, 0, qtrue }, + + { &g_slapKnockback, "g_slapKnockback", "200", CVAR_ARCHIVE, 0, qfalse}, + { &g_slapDamage, "g_slapDamage", "0", CVAR_ARCHIVE, 0, qfalse}, + + { &g_teamAutoJoin, "g_teamAutoJoin", "0", CVAR_ARCHIVE }, + { &g_teamForceBalance, "g_teamForceBalance", "1", CVAR_ARCHIVE }, + + { &g_warmup, "g_warmup", "10", CVAR_ARCHIVE, 0, qtrue }, + { &g_warmupMode, "g_warmupMode", "1", CVAR_ARCHIVE, 0, qtrue }, + { &g_doWarmup, "g_doWarmup", "1", CVAR_ARCHIVE, 0, qtrue }, + { &g_logFile, "g_logFile", "games.log", CVAR_ARCHIVE, 0, qfalse }, + { &g_logFileSync, "g_logFileSync", "0", CVAR_ARCHIVE, 0, qfalse }, + + { &g_password, "g_password", "", CVAR_USERINFO, 0, qfalse }, + + { &g_banIPs, "g_banIPs", "", CVAR_ARCHIVE, 0, qfalse }, + { &g_filterBan, "g_filterBan", "1", CVAR_ARCHIVE, 0, qfalse }, + + { &g_needpass, "g_needpass", "0", CVAR_SERVERINFO | CVAR_ROM, 0, qfalse }, + + { &g_autoGhost, "g_autoGhost", "1", CVAR_SERVERINFO, 0, qfalse }, + + { &g_dedicated, "dedicated", "0", 0, 0, qfalse }, + + { &g_speed, "g_speed", "320", CVAR_SERVERINFO, 0, qtrue }, + { &g_gravity, "g_gravity", "800", CVAR_SERVERINFO, 0, qtrue }, + { &g_knockback, "g_knockback", "1000", CVAR_SERVERINFO, 0, qtrue }, + { &g_quadfactor, "g_quadfactor", "3", 0, 0, qtrue }, + { &g_weaponRespawn, "g_weaponrespawn", "5", 0, 0, qtrue }, + { &g_weaponTeamRespawn, "g_weaponTeamRespawn", "30", 0, 0, qtrue }, + { &g_inactivity, "g_inactivity", "0", 0, 0, qtrue }, + { &g_debugMove, "g_debugMove", "0", 0, 0, qfalse }, + { &g_debugDamage, "g_debugDamage", "0", 0, 0, qfalse }, + { &g_debugAlloc, "g_debugAlloc", "0", 0, 0, qfalse }, + { &g_motd, "g_motd", "", 0, 0, qfalse }, + { &g_blood, "com_blood", "1", 0, 0, qfalse }, + + { &g_podiumDist, "g_podiumDist", "80", 0, 0, qfalse }, + { &g_podiumDrop, "g_podiumDrop", "70", 0, 0, qfalse }, + + { &g_allowVote, "g_allowVote", "1", CVAR_ARCHIVE, 0, qfalse }, + { &g_requireVoteReasons, "g_requireVoteReasons", "0", CVAR_ARCHIVE, 0, qfalse }, + { &g_voteLimit, "g_voteLimit", "5", CVAR_ARCHIVE, 0, qfalse }, + { &g_voteMinTime, "g_voteMinTime", "120", CVAR_ARCHIVE, 0, qfalse }, + { &g_mapvoteMaxTime, "g_mapvoteMaxTime", "240", CVAR_ARCHIVE, 0, qfalse }, + { &g_votableMaps, "g_votableMaps", "", CVAR_ARCHIVE, 0, qtrue }, + { &g_suddenDeathVotePercent, "g_suddenDeathVotePercent", "74", CVAR_ARCHIVE, 0, qfalse }, + { &g_suddenDeathVoteDelay, "g_suddenDeathVoteDelay", "180", CVAR_ARCHIVE, 0, qfalse }, + { &g_customVote1, "g_customVote1", "", CVAR_ARCHIVE, 0, qfalse }, + { &g_customVote2, "g_customVote2", "", CVAR_ARCHIVE, 0, qfalse }, + { &g_customVote3, "g_customVote3", "", CVAR_ARCHIVE, 0, qfalse }, + { &g_customVote4, "g_customVote4", "", CVAR_ARCHIVE, 0, qfalse }, + { &g_customVote5, "g_customVote5", "", CVAR_ARCHIVE, 0, qfalse }, + { &g_customVote6, "g_customVote6", "", CVAR_ARCHIVE, 0, qfalse }, + { &g_customVote7, "g_customVote7", "", CVAR_ARCHIVE, 0, qfalse }, + { &g_customVote8, "g_customVote8", "", CVAR_ARCHIVE, 0, qfalse }, + { &g_customVotePercent, "g_customVotePercent", "50", CVAR_ARCHIVE, 0, qfalse }, + { &g_mapVotesPercent, "g_mapVotesPercent", "50", CVAR_ARCHIVE, 0, qfalse }, + { &g_extendVotesPercent, "g_extendVotesPercent", "74", CVAR_ARCHIVE, 0, qfalse }, + { &g_extendVotesTime, "g_extendVotesTime", "10", CVAR_ARCHIVE, 0, qfalse }, + { &g_extendVotesCount, "g_extendVotesCount", "2", CVAR_ARCHIVE, 0, qfalse }, + { &g_mapRotationVote, "g_mapRotationVote", "15", CVAR_ARCHIVE, 0, qfalse }, + { &g_readyPercent, "g_readyPercent", "0", CVAR_ARCHIVE, 0, qfalse }, + { &g_designateVotes, "g_designateVotes", "0", CVAR_ARCHIVE, 0, qfalse }, + + { &g_listEntity, "g_listEntity", "0", 0, 0, qfalse }, + { &g_minCommandPeriod, "g_minCommandPeriod", "500", 0, 0, qfalse}, + { &g_minNameChangePeriod, "g_minNameChangePeriod", "5", 0, 0, qfalse}, + { &g_maxNameChanges, "g_maxNameChanges", "5", 0, 0, qfalse}, + { &g_newbieNumbering, "g_newbieNumbering", "0", CVAR_ARCHIVE, 0, qfalse}, + { &g_newbieNamePrefix, "g_newbieNamePrefix", "Newbie#", CVAR_ARCHIVE, 0, qfalse}, + + { &g_smoothClients, "g_smoothClients", "1", 0, 0, qfalse}, + { &g_clientUpgradeNotice, "g_clientUpgradeNotice", "1", 0, 0, qfalse}, + { &pmove_fixed, "pmove_fixed", "0", CVAR_SYSTEMINFO, 0, qfalse}, + { &pmove_msec, "pmove_msec", "8", CVAR_SYSTEMINFO, 0, qfalse}, + + { &g_humanBuildPoints, "g_humanBuildPoints", DEFAULT_HUMAN_BUILDPOINTS, CVAR_SERVERINFO, 0, qfalse }, + { &g_alienBuildPoints, "g_alienBuildPoints", DEFAULT_ALIEN_BUILDPOINTS, CVAR_SERVERINFO, 0, qfalse }, + { &g_humanStage, "g_humanStage", "0", 0, 0, qfalse }, + { &g_humanKills, "g_humanKills", "0", 0, 0, qfalse }, + { &g_humanMaxStage, "g_humanMaxStage", DEFAULT_HUMAN_MAX_STAGE, 0, 0, qfalse }, + { &g_humanStage2Threshold, "g_humanStage2Threshold", DEFAULT_HUMAN_STAGE2_THRESH, 0, 0, qfalse }, + { &g_humanStage3Threshold, "g_humanStage3Threshold", DEFAULT_HUMAN_STAGE3_THRESH, 0, 0, qfalse }, + { &g_alienStage, "g_alienStage", "0", 0, 0, qfalse }, + { &g_alienKills, "g_alienKills", "0", 0, 0, qfalse }, + { &g_alienMaxStage, "g_alienMaxStage", DEFAULT_ALIEN_MAX_STAGE, 0, 0, qfalse }, + { &g_alienStage2Threshold, "g_alienStage2Threshold", DEFAULT_ALIEN_STAGE2_THRESH, 0, 0, qfalse }, + { &g_alienStage3Threshold, "g_alienStage3Threshold", DEFAULT_ALIEN_STAGE3_THRESH, 0, 0, qfalse }, + + { &g_teamImbalanceWarnings, "g_teamImbalanceWarnings", "30", CVAR_ARCHIVE, 0, qfalse }, + + { &g_unlagged, "g_unlagged", "1", CVAR_SERVERINFO | CVAR_ARCHIVE, 0, qtrue }, + + { &g_disabledEquipment, "g_disabledEquipment", "", CVAR_ROM, 0, qfalse }, + { &g_disabledClasses, "g_disabledClasses", "", CVAR_ROM, 0, qfalse }, + { &g_disabledBuildables, "g_disabledBuildables", "", CVAR_ROM, 0, qfalse }, + + { &g_chatTeamPrefix, "g_chatTeamPrefix", "1", CVAR_ARCHIVE }, + { &g_actionPrefix, "g_actionPrefix", "* ", CVAR_ARCHIVE, 0, qfalse }, + { &g_floodMaxDemerits, "g_floodMaxDemerits", "5000", CVAR_ARCHIVE, 0, qfalse }, + { &g_floodMinTime, "g_floodMinTime", "2000", CVAR_ARCHIVE, 0, qfalse }, + { &g_spamTime, "g_spamTime", "2", CVAR_ARCHIVE, 0, qfalse }, + + { &g_markDeconstruct, "g_markDeconstruct", "0", CVAR_ARCHIVE, 0, qtrue }, + { &g_markDeconstructMode, "g_markDeconstructMode", "0", CVAR_ARCHIVE, 0, qfalse }, + { &g_deconDead, "g_deconDead", "0", CVAR_ARCHIVE, 0, qtrue }, + + { &g_debugMapRotation, "g_debugMapRotation", "0", 0, 0, qfalse }, + { &g_currentMapRotation, "g_currentMapRotation", "-1", 0, 0, qfalse }, // -1 = NOT_ROTATING + { &g_currentMap, "g_currentMap", "0", 0, 0, qfalse }, + { &g_nextMap, "g_nextMap", "", 0 , 0, qtrue }, + { &g_initialMapRotation, "g_initialMapRotation", "", CVAR_ARCHIVE, 0, qfalse }, + { &g_shove, "g_shove", "15", CVAR_ARCHIVE, 0, qfalse }, + { &g_mapConfigs, "g_mapConfigs", "", CVAR_ARCHIVE, 0, qfalse }, + { NULL, "g_mapConfigsLoaded", "0", CVAR_ROM, 0, qfalse }, + + { &g_layouts, "g_layouts", "", CVAR_LATCH, 0, qfalse }, + { &g_layoutAuto, "g_layoutAuto", "1", CVAR_ARCHIVE, 0, qfalse }, + + { &g_admin, "g_admin", "admin.dat", CVAR_ARCHIVE, 0, qfalse }, + { &g_adminLog, "g_adminLog", "admin.log", CVAR_ARCHIVE, 0, qfalse }, + { &g_adminParseSay, "g_adminParseSay", "1", CVAR_ARCHIVE, 0, qfalse }, + { &g_adminSayFilter, "g_adminSayFilter", "0", CVAR_ARCHIVE, 0, qfalse }, + { &g_adminNameProtect, "g_adminNameProtect", "1", CVAR_ARCHIVE, 0, qfalse }, + { &g_adminTempMute, "g_adminTempMute", "5m", CVAR_ARCHIVE, 0, qfalse }, + { &g_adminTempBan, "g_adminTempBan", "2m", CVAR_ARCHIVE, 0, qfalse }, + { &g_adminMaxBan, "g_adminMaxBan", "2w", CVAR_ARCHIVE, 0, qfalse }, + { &g_adminTempSpec, "g_adminTempSpec", "2m", CVAR_ARCHIVE, 0, qfalse }, + { &g_adminMapLog, "g_adminMapLog", "", CVAR_ROM, 0, qfalse }, + { &g_minLevelToJoinTeam, "g_minLevelToJoinTeam", "0", CVAR_ARCHIVE, 0, qfalse }, + { &g_minDeconLevel, "g_minDeconLevel", "0", CVAR_ARCHIVE, 0, qfalse}, + { &g_minDeconAffectsMark, "g_minDeconAffectsMark", "0", CVAR_ARCHIVE, 0, qfalse}, + { &g_forceAutoSelect, "g_forceAutoSelect", "0", CVAR_ARCHIVE, 0, qtrue }, + { &g_adminExpireTime, "g_adminExpireTime", "0", CVAR_ARCHIVE, 0, qfalse }, + + { &g_privateMessages, "g_privateMessages", "1", CVAR_ARCHIVE, 0, qfalse }, + { &g_fullIgnore, "g_fullIgnore", "1", CVAR_ARCHIVE, 0, qtrue }, + { &g_decolourLogfiles, "g_decolourLogfiles", "0", CVAR_ARCHIVE, 0, qfalse }, + { &g_buildLogMaxLength, "g_buildLogMaxLength", "50", CVAR_ARCHIVE, 0, qfalse }, + { &g_myStats, "g_myStats", "1", CVAR_ARCHIVE, 0, qtrue }, + { &g_AllStats, "g_AllStats", "0", CVAR_ARCHIVE, 0, qtrue }, + { &g_AllStatsTime, "g_AllStatsTime", "60", CVAR_ARCHIVE, 0, qfalse }, + { &g_teamStatus, "g_teamStatus", "0", CVAR_ARCHIVE, 0, qtrue }, + { &g_publicSayadmins, "g_publicSayadmins", "1", CVAR_ARCHIVE, 0, qfalse }, + { &g_minLevelToSpecMM1, "g_minLevelToSpecMM1", "0", CVAR_ARCHIVE, 0, qfalse }, + { &g_antiSpawnBlock, "g_antiSpawnBlock", "0", CVAR_ARCHIVE, 0, qfalse }, + + { &g_devmapKillerHP, "g_devmapKillerHP", "0", CVAR_ARCHIVE, 0, qtrue }, + { &g_killerHP, "g_killerHP", "0", CVAR_ARCHIVE, 0, qtrue }, + + { &g_tag, "g_tag", "main", CVAR_INIT, 0, qfalse }, + + { &g_dretchPunt, "g_dretchPunt", "1", CVAR_ARCHIVE, 0, qfalse }, + + { &g_msg, "g_msg", "", CVAR_ARCHIVE, 0, qfalse }, + { &g_msgTime, "g_msgTime", "0", CVAR_ARCHIVE, 0, qfalse }, + { &g_welcomeMsg, "g_welcomeMsg", "", CVAR_ARCHIVE, 0, qfalse }, + { &g_welcomeMsgTime, "g_welcomeMsgTime", "0", CVAR_ARCHIVE, 0, qfalse }, + + { &g_rankings, "g_rankings", "0", 0, 0, qfalse }, + { &g_allowShare, "g_allowShare", "0", CVAR_ARCHIVE | CVAR_SERVERINFO, 0, qfalse}, + { &g_creditOverflow, "g_creditOverflow", "0", CVAR_ARCHIVE | CVAR_SERVERINFO, 0, qfalse}, + { &g_banNotice, "g_banNotice", "", CVAR_ARCHIVE, 0, qfalse }, + + { &mod_jetpackFuel, "mod_jetpackFuel", "0", CVAR_ARCHIVE, 0, qtrue }, + { &mod_jetpackConsume, "mod_jetpackConsume", "2", CVAR_ARCHIVE, 0, qfalse }, + { &mod_jetpackRegen, "mod_jetpackRegen", "3", CVAR_ARCHIVE, 0, qfalse }, + + { &g_teamKillThreshold, "g_teamKillThreshold", "0", CVAR_ARCHIVE, 0, qfalse }, + + { &g_aimbotAdvertBan, "g_aimbotAdvertBan", "0", CVAR_ARCHIVE, 0, qfalse }, + { &g_aimbotAdvertBanTime, "g_aimbotAdvertBanTime", "0", CVAR_ARCHIVE, 0, qfalse }, + { &g_aimbotAdvertBanReason, "g_aimbotAdvertBanReason", "AUTOBAN: AIMBOT", CVAR_ARCHIVE, 0, qfalse } +}; + +static int gameCvarTableSize = sizeof( gameCvarTable ) / sizeof( gameCvarTable[ 0 ] ); + + +void G_InitGame( int levelTime, int randomSeed, int restart ); +void G_RunFrame( int levelTime ); +void G_ShutdownGame( int restart ); +void CheckExitRules( void ); + +void G_CountSpawns( void ); +void G_CalculateBuildPoints( void ); + +/* +================ +vmMain + +This is the only way control passes into the module. +This must be the very first function compiled into the .q3vm file +================ +*/ +intptr_t vmMain( int command, int arg0, int arg1, int arg2, int arg3, int arg4, + int arg5, int arg6, int arg7, int arg8, int arg9, + int arg10, int arg11 ) +{ + switch( command ) + { + case GAME_INIT: + G_InitGame( arg0, arg1, arg2 ); + return 0; + + case GAME_SHUTDOWN: + G_ShutdownGame( arg0 ); + return 0; + + case GAME_CLIENT_CONNECT: + return (intptr_t)ClientConnect( arg0, arg1 ); + + case GAME_CLIENT_THINK: + ClientThink( arg0 ); + return 0; + + case GAME_CLIENT_USERINFO_CHANGED: + ClientUserinfoChanged( arg0, qfalse ); + return 0; + + case GAME_CLIENT_DISCONNECT: + ClientDisconnect( arg0 ); + return 0; + + case GAME_CLIENT_BEGIN: + ClientBegin( arg0 ); + return 0; + + case GAME_CLIENT_COMMAND: + ClientCommand( arg0 ); + return 0; + + case GAME_RUN_FRAME: + G_RunFrame( arg0 ); + return 0; + + case GAME_CONSOLE_COMMAND: + return ConsoleCommand( ); + } + + return -1; +} + + +void QDECL G_Printf( const char *fmt, ... ) +{ + va_list argptr; + char text[ 1024 ]; + + va_start( argptr, fmt ); + vsprintf( text, fmt, argptr ); + va_end( argptr ); + + trap_Printf( text ); +} + +void QDECL G_Error( const char *fmt, ... ) +{ + va_list argptr; + char text[ 1024 ]; + + va_start( argptr, fmt ); + vsprintf( text, fmt, argptr ); + va_end( argptr ); + + trap_Error( text ); +} + +/* +================ +G_FindTeams + +Chain together all entities with a matching team field. +Entity teams are used for item groups and multi-entity mover groups. + +All but the first will have the FL_TEAMSLAVE flag set and teammaster field set +All but the last will have the teamchain field set to the next one +================ +*/ +void G_FindTeams( void ) +{ + gentity_t *e, *e2; + int i, j; + int c, c2; + + c = 0; + c2 = 0; + + for( i = 1, e = g_entities+i; i < level.num_entities; i++, e++ ) + { + if( !e->inuse ) + continue; + + if( !e->team ) + continue; + + if( e->flags & FL_TEAMSLAVE ) + continue; + + e->teammaster = e; + c++; + c2++; + + for( j = i + 1, e2 = e + 1; j < level.num_entities; j++, e2++ ) + { + if( !e2->inuse ) + continue; + + if( !e2->team ) + continue; + + if( e2->flags & FL_TEAMSLAVE ) + continue; + + if( !strcmp( e->team, e2->team ) ) + { + c2++; + e2->teamchain = e->teamchain; + e->teamchain = e2; + e2->teammaster = e; + e2->flags |= FL_TEAMSLAVE; + + // make sure that targets only point at the master + if( e2->targetname ) + { + e->targetname = e2->targetname; + e2->targetname = NULL; + } + } + } + } + + G_Printf( "%i teams with %i entities\n", c, c2 ); +} + +void G_RemapTeamShaders( void ) +{ +} + + +/* +================= +G_RegisterCvars +================= +*/ +void G_RegisterCvars( void ) +{ + int i; + cvarTable_t *cv; + qboolean remapped = qfalse; + + for( i = 0, cv = gameCvarTable; i < gameCvarTableSize; i++, cv++ ) + { + trap_Cvar_Register( cv->vmCvar, cv->cvarName, + cv->defaultString, cv->cvarFlags ); + + if( cv->vmCvar ) + cv->modificationCount = cv->vmCvar->modificationCount; + + if( cv->teamShader ) + remapped = qtrue; + } + + if( remapped ) + G_RemapTeamShaders( ); +} + +/* +================= +G_UpdateCvars +================= +*/ +void G_UpdateCvars( void ) +{ + int i; + cvarTable_t *cv; + qboolean remapped = qfalse; + + for( i = 0, cv = gameCvarTable; i < gameCvarTableSize; i++, cv++ ) + { + if( cv->vmCvar ) + { + trap_Cvar_Update( cv->vmCvar ); + + if( cv->modificationCount != cv->vmCvar->modificationCount ) + { + cv->modificationCount = cv->vmCvar->modificationCount; + + if( cv->trackChange ) + { + trap_SendServerCommand( -1, va( "print \"Server: %s changed to %s\n\"", + cv->cvarName, cv->vmCvar->string ) ); + // update serverinfo in case this cvar is passed to clients indirectly + CalculateRanks( ); + } + + if( cv->teamShader ) + remapped = qtrue; + } + } + } + + if( remapped ) + G_RemapTeamShaders( ); +} + +/* +================= +G_MapConfigs +================= +*/ +void G_MapConfigs( const char *mapname ) +{ + + if( !g_mapConfigs.string[0] ) + return; + + if( trap_Cvar_VariableIntegerValue( "g_mapConfigsLoaded" ) ) + return; + + trap_SendConsoleCommand( EXEC_APPEND, + va( "exec \"%s/default.cfg\"\n", g_mapConfigs.string ) ); + + trap_SendConsoleCommand( EXEC_APPEND, + va( "exec \"%s/%s.cfg\"\n", g_mapConfigs.string, mapname ) ); + + trap_Cvar_Set( "g_mapConfigsLoaded", "1" ); +} + +/* +============ +G_InitGame + +============ +*/ +void G_InitGame( int levelTime, int randomSeed, int restart ) +{ + int i; + + srand( randomSeed ); + + G_RegisterCvars( ); + + G_Printf( "------- Game Initialization -------\n" ); + G_Printf( "gamename: %s\n", GAME_VERSION ); + G_Printf( "gamedate: %s\n", __DATE__ ); + + G_ProcessIPBans( ); + + G_InitMemory( ); + + // set some level globals + memset( &level, 0, sizeof( level ) ); + level.time = levelTime; + level.startTime = levelTime; + level.alienStage2Time = level.alienStage3Time = + level.humanStage2Time = level.humanStage3Time = level.startTime; + + level.snd_fry = G_SoundIndex( "sound/misc/fry.wav" ); // FIXME standing in lava / slime + + trap_Cvar_Set( "qvm_version", + QVM_NAME " " QVM_VERSIONNUM " (" __DATE__ ", " __TIME__ ")" ); + + if( g_logFile.string[ 0 ] ) + { + if( g_logFileSync.integer ) + trap_FS_FOpenFile( g_logFile.string, &level.logFile, FS_APPEND_SYNC ); + else + trap_FS_FOpenFile( g_logFile.string, &level.logFile, FS_APPEND ); + + if( !level.logFile ) + G_Printf( "WARNING: Couldn't open logfile: %s\n", g_logFile.string ); + else + { + char serverinfo[ MAX_INFO_STRING ]; + qtime_t qt; + int t; + + + trap_GetServerinfo( serverinfo, sizeof( serverinfo ) ); + + G_LogPrintf( "------------------------------------------------------------\n" ); + G_LogPrintf( "InitGame: %s\n", serverinfo ); + + t = trap_RealTime( &qt ); + G_LogPrintf("RealTime: %04i/%02i/%02i %02i:%02i:%02i\n", + qt.tm_year+1900, qt.tm_mon+1, qt.tm_mday, + qt.tm_hour, qt.tm_min, qt.tm_sec ); + + } + } + else + G_Printf( "Not logging to disk\n" ); + + { + char map[ MAX_CVAR_VALUE_STRING ] = {""}; + + trap_Cvar_VariableStringBuffer( "mapname", map, sizeof( map ) ); + G_MapConfigs( map ); + } + + // we're done with g_mapConfigs, so reset this for the next map + trap_Cvar_Set( "g_mapConfigsLoaded", "0" ); + + if ( g_admin.string[ 0 ] ) { + G_admin_readconfig( NULL, 0 ); + } + + // initialize all entities for this game + memset( g_entities, 0, MAX_GENTITIES * sizeof( g_entities[ 0 ] ) ); + level.gentities = g_entities; + + // initialize all clients for this game + level.maxclients = g_maxclients.integer; + memset( g_clients, 0, MAX_CLIENTS * sizeof( g_clients[ 0 ] ) ); + level.clients = g_clients; + + // set client fields on player ents + for( i = 0; i < level.maxclients; i++ ) + g_entities[ i ].client = level.clients + i; + + // always leave room for the max number of clients, + // even if they aren't all used, so numbers inside that + // range are NEVER anything but clients + level.num_entities = MAX_CLIENTS; + + // let the server system know where the entites are + trap_LocateGameData( level.gentities, level.num_entities, sizeof( gentity_t ), + &level.clients[ 0 ].ps, sizeof( level.clients[ 0 ] ) ); + + trap_SetConfigstring( CS_INTERMISSION, "0" ); + + // update maplog + G_admin_maplog_update( ); + + // test to see if a custom buildable layout will be loaded + G_LayoutSelect( ); + + // parse the key/value pairs and spawn gentities + G_SpawnEntitiesFromString( ); + + // load up a custom building layout if there is one + G_LayoutLoad( ); + + // load any nobuild markers that have been saved + G_NobuildLoad( ); + + // the map might disable some things + BG_InitAllowedGameElements( ); + + // general initialization + G_FindTeams( ); + + //TA: + BG_InitClassOverrides( ); + BG_InitBuildableOverrides( ); + G_InitDamageLocations( ); + G_InitMapRotations( ); + G_InitSpawnQueue( &level.alienSpawnQueue ); + G_InitSpawnQueue( &level.humanSpawnQueue ); + + if( g_debugMapRotation.integer ) + G_PrintRotations( ); + + //reset stages + trap_Cvar_Set( "g_alienStage", va( "%d", S1 ) ); + trap_Cvar_Set( "g_humanStage", va( "%d", S1 ) ); + trap_Cvar_Set( "g_alienKills", 0 ); + trap_Cvar_Set( "g_humanKills", 0 ); + trap_Cvar_Set( "g_suddenDeath", 0 ); + level.suddenDeathBeginTime = g_suddenDeathTime.integer * 60000; + + G_Printf( "-----------------------------------\n" ); + + G_RemapTeamShaders( ); + + //TA: so the server counts the spawns without a client attached + G_CountSpawns( ); + + G_ResetPTRConnections( ); + + if(g_lockTeamsAtStart.integer) + { + level.alienTeamLocked=qtrue; + level.humanTeamLocked=qtrue; + trap_Cvar_Set( "g_lockTeamsAtStart", "0" ); + } +} + +/* +================== +G_ClearVotes + +remove all currently active votes +================== +*/ +static void G_ClearVotes( void ) +{ + level.voteTime = 0; + trap_SetConfigstring( CS_VOTE_TIME, "" ); + trap_SetConfigstring( CS_VOTE_STRING, "" ); + level.teamVoteTime[ 0 ] = 0; + trap_SetConfigstring( CS_TEAMVOTE_TIME, "" ); + trap_SetConfigstring( CS_TEAMVOTE_STRING, "" ); + level.teamVoteTime[ 1 ] = 0; + trap_SetConfigstring( CS_TEAMVOTE_TIME + 1, "" ); + trap_SetConfigstring( CS_TEAMVOTE_STRING + 1, "" ); +} + +/* +================= +G_ShutdownGame +================= +*/ +void G_ShutdownGame( int restart ) +{ + // in case of a map_restart + G_ClearVotes( ); + + G_Printf( "==== ShutdownGame ====\n" ); + + if( level.logFile ) + { + G_LogPrintf( "ShutdownGame:\n" ); + G_LogPrintf( "------------------------------------------------------------\n" ); + trap_FS_FCloseFile( level.logFile ); + } + + // write admin.dat for !seen data + admin_writeconfig(); + + // write all the client session data so we can get it back + G_WriteSessionData( ); + + G_admin_cleanup( ); + G_admin_namelog_cleanup( ); + G_admin_adminlog_cleanup( ); + + level.restarted = qfalse; + level.surrenderTeam = PTE_NONE; + trap_SetConfigstring( CS_WINNER, "" ); +} + + + +//=================================================================== + +void QDECL Com_Error( int level, const char *error, ... ) +{ + va_list argptr; + char text[ 1024 ]; + + va_start( argptr, error ); + vsprintf( text, error, argptr ); + va_end( argptr ); + + G_Error( "%s", text ); +} + +void QDECL Com_Printf( const char *msg, ... ) +{ + va_list argptr; + char text[ 1024 ]; + + va_start( argptr, msg ); + vsprintf( text, msg, argptr ); + va_end( argptr ); + + G_Printf( "%s", text ); +} + +/* +======================================================================== + +PLAYER COUNTING / SCORE SORTING + +======================================================================== +*/ + + +/* +============= +SortRanks + +============= +*/ +int QDECL SortRanks( const void *a, const void *b ) +{ + gclient_t *ca, *cb; + + ca = &level.clients[ *(int *)a ]; + cb = &level.clients[ *(int *)b ]; + + // then sort by score + if( ca->pers.score > cb->pers.score ) + return -1; + else if( ca->pers.score < cb->pers.score ) + return 1; + else + return 0; +} + +/* +============ +G_InitSpawnQueue + +Initialise a spawn queue +============ +*/ +void G_InitSpawnQueue( spawnQueue_t *sq ) +{ + int i; + + sq->back = sq->front = 0; + sq->back = QUEUE_MINUS1( sq->back ); + + //0 is a valid clientNum, so use something else + for( i = 0; i < MAX_CLIENTS; i++ ) + sq->clients[ i ] = -1; +} + +/* +============ +G_GetSpawnQueueLength + +Return tha length of a spawn queue +============ +*/ +int G_GetSpawnQueueLength( spawnQueue_t *sq ) +{ + int length = sq->back - sq->front + 1; + + while( length < 0 ) + length += MAX_CLIENTS; + + while( length >= MAX_CLIENTS ) + length -= MAX_CLIENTS; + + return length; +} + +/* +============ +G_PopSpawnQueue + +Remove from front element from a spawn queue +============ +*/ +int G_PopSpawnQueue( spawnQueue_t *sq ) +{ + int clientNum = sq->clients[ sq->front ]; + + if( G_GetSpawnQueueLength( sq ) > 0 ) + { + sq->clients[ sq->front ] = -1; + sq->front = QUEUE_PLUS1( sq->front ); + g_entities[ clientNum ].client->ps.pm_flags &= ~PMF_QUEUED; + + return clientNum; + } + else + return -1; +} + +/* +============ +G_PeekSpawnQueue + +Look at front element from a spawn queue +============ +*/ +int G_PeekSpawnQueue( spawnQueue_t *sq ) +{ + return sq->clients[ sq->front ]; +} + +/* +============ +G_SearchSpawnQueue + +Look to see if clientNum is already in the spawnQueue +============ +*/ +qboolean G_SearchSpawnQueue( spawnQueue_t *sq, int clientNum ) +{ + int i; + + for( i = 0; i < MAX_CLIENTS; i++ ) + if( sq->clients[ i ] == clientNum ) + return qtrue; + return qfalse; +} + +/* +============ +G_PushSpawnQueue + +Add an element to the back of the spawn queue +============ +*/ +qboolean G_PushSpawnQueue( spawnQueue_t *sq, int clientNum ) +{ + // don't add the same client more than once + if( G_SearchSpawnQueue( sq, clientNum ) ) + return qfalse; + + sq->back = QUEUE_PLUS1( sq->back ); + sq->clients[ sq->back ] = clientNum; + + g_entities[ clientNum ].client->ps.pm_flags |= PMF_QUEUED; + return qtrue; +} + +/* +============ +G_RemoveFromSpawnQueue + +remove a specific client from a spawn queue +============ +*/ +qboolean G_RemoveFromSpawnQueue( spawnQueue_t *sq, int clientNum ) +{ + int i = sq->front; + + if( G_GetSpawnQueueLength( sq ) ) + { + do + { + if( sq->clients[ i ] == clientNum ) + { + //and this kids is why it would have + //been better to use an LL for internal + //representation + do + { + sq->clients[ i ] = sq->clients[ QUEUE_PLUS1( i ) ]; + + i = QUEUE_PLUS1( i ); + } while( i != QUEUE_PLUS1( sq->back ) ); + + sq->back = QUEUE_MINUS1( sq->back ); + g_entities[ clientNum ].client->ps.pm_flags &= ~PMF_QUEUED; + + return qtrue; + } + + i = QUEUE_PLUS1( i ); + } while( i != QUEUE_PLUS1( sq->back ) ); + } + + return qfalse; +} + +/* +============ +G_GetPosInSpawnQueue + +Get the position of a client in a spawn queue +============ +*/ +int G_GetPosInSpawnQueue( spawnQueue_t *sq, int clientNum ) +{ + int i = sq->front; + + if( G_GetSpawnQueueLength( sq ) ) + { + do + { + if( sq->clients[ i ] == clientNum ) + { + if( i < sq->front ) + return i + MAX_CLIENTS - sq->front; + else + return i - sq->front; + } + + i = QUEUE_PLUS1( i ); + } while( i != QUEUE_PLUS1( sq->back ) ); + } + + return -1; +} + +/* +============ +G_PrintSpawnQueue + +Print the contents of a spawn queue +============ +*/ +void G_PrintSpawnQueue( spawnQueue_t *sq ) +{ + int i = sq->front; + int length = G_GetSpawnQueueLength( sq ); + + G_Printf( "l:%d f:%d b:%d :", length, sq->front, sq->back ); + + if( length > 0 ) + { + do + { + if( sq->clients[ i ] == -1 ) + G_Printf( "*:" ); + else + G_Printf( "%d:", sq->clients[ i ] ); + + i = QUEUE_PLUS1( i ); + } while( i != QUEUE_PLUS1( sq->back ) ); + } + + G_Printf( "\n" ); +} + +/* +============ +G_SpawnClients + +Spawn queued clients +============ +*/ +void G_SpawnClients( pTeam_t team ) +{ + int clientNum; + gentity_t *ent, *spawn; + vec3_t spawn_origin, spawn_angles; + spawnQueue_t *sq = NULL; + int numSpawns = 0; + if( g_doWarmup.integer && ( g_warmupMode.integer==1 || g_warmupMode.integer == 2 ) && + level.time - level.startTime < g_warmup.integer * 1000 ) + { + return; + } + if( team == PTE_ALIENS ) + { + sq = &level.alienSpawnQueue; + numSpawns = level.numAlienSpawns; + } + else if( team == PTE_HUMANS ) + { + sq = &level.humanSpawnQueue; + numSpawns = level.numHumanSpawns; + } + + if( G_GetSpawnQueueLength( sq ) > 0 && numSpawns > 0 ) + { + clientNum = G_PeekSpawnQueue( sq ); + ent = &g_entities[ clientNum ]; + + if( ( spawn = G_SelectTremulousSpawnPoint( team, + ent->client->pers.lastDeathLocation, + spawn_origin, spawn_angles ) ) ) + { + clientNum = G_PopSpawnQueue( sq ); + + if( clientNum < 0 ) + return; + + ent = &g_entities[ clientNum ]; + + ent->client->sess.sessionTeam = TEAM_FREE; + ClientUserinfoChanged( clientNum, qfalse ); + ClientSpawn( ent, spawn, spawn_origin, spawn_angles ); + } + } +} + +/* +============ +G_CountSpawns + +Counts the number of spawns for each team +============ +*/ +void G_CountSpawns( void ) +{ + int i; + gentity_t *ent; + + level.numAlienSpawns = 0; + level.numHumanSpawns = 0; + + for( i = 1, ent = g_entities + i ; i < level.num_entities ; i++, ent++ ) + { + if( !ent->inuse ) + continue; + + if( ent->s.modelindex == BA_A_SPAWN && ent->health > 0 ) + level.numAlienSpawns++; + + if( ent->s.modelindex == BA_H_SPAWN && ent->health > 0 ) + level.numHumanSpawns++; + } + + //let the client know how many spawns there are + trap_SetConfigstring( CS_SPAWNS, va( "%d %d", + level.numAlienSpawns, level.numHumanSpawns ) ); +} + +/* +============ +G_TimeTilSuddenDeath +============ +*/ +int G_TimeTilSuddenDeath( void ) +{ + if( (!g_suddenDeathTime.integer && level.suddenDeathBeginTime==0 ) || level.suddenDeathBeginTime<0 ) + return 999999999; // Always some time away + + return ( ( level.suddenDeathBeginTime ) - ( level.time - level.startTime ) ); +} + + +#define PLAYER_COUNT_MOD 5.0f + +/* +============ +G_CalculateBuildPoints + +Recalculate the quantity of building points available to the teams +============ +*/ +void G_CalculateBuildPoints( void ) +{ + int i; + buildable_t buildable; + gentity_t *ent; + int localHTP = g_humanBuildPoints.integer, + localATP = g_alienBuildPoints.integer; + + // g_suddenDeath sets what state we want it to be. + // level.suddenDeath says whether we've calculated BPs at the 'start' of SD or not + + // reset if SD was on, but now it's off + if(!g_suddenDeath.integer && level.suddenDeath) + { + level.suddenDeath=qfalse; + level.suddenDeathWarning=0; + level.suddenDeathBeginTime = -1; + if((level.time - level.startTime) < (g_suddenDeathTime.integer * 60000 ) ) + level.suddenDeathBeginTime = g_suddenDeathTime.integer * 60000; + else + level.suddenDeathBeginTime = -1; + } + + if(!level.suddenDeath) + { + if(g_suddenDeath.integer || G_TimeTilSuddenDeath( ) <= 0 ) //Conditions to enter SD + { + //begin sudden death + if( level.suddenDeathWarning < TW_PASSED ) + { + trap_SendServerCommand( -1, "cp \"Sudden Death!\"" ); + G_LogPrintf("Beginning Sudden Death (Mode %d)\n",g_suddenDeathMode.integer); + localHTP = 0; + localATP = 0; + + if( g_suddenDeathMode.integer == SDMODE_SELECTIVE ) + { + for( i = 1, ent = g_entities + i; i < level.num_entities; i++, ent++ ) + { + if( ent->s.eType != ET_BUILDABLE ) + continue; + + if( BG_FindReplaceableTestForBuildable( ent->s.modelindex ) ) + { + int t = BG_FindTeamForBuildable( ent->s.modelindex ); + + if( t == BIT_HUMANS ) + localHTP += BG_FindBuildPointsForBuildable( ent->s.modelindex ); + else if( t == BIT_ALIENS ) + localATP += BG_FindBuildPointsForBuildable( ent->s.modelindex ); + } + } + } + level.suddenDeathHBuildPoints = localHTP; + level.suddenDeathABuildPoints = localATP; + level.suddenDeathBeginTime = level.time; + level.suddenDeath=qtrue; + trap_Cvar_Set( "g_suddenDeath", "1" ); + + level.suddenDeathWarning = TW_PASSED; + } + } + else + { + //warn about sudden death + if( ( G_TimeTilSuddenDeath( ) <= 60000 ) && + ( level.suddenDeathWarning < TW_IMMINENT ) ) + { + trap_SendServerCommand( -1, va("cp \"Sudden Death in %d seconds!\"", + (int)(G_TimeTilSuddenDeath() / 1000 ) ) ); + level.suddenDeathWarning = TW_IMMINENT; + } + } + } + + //set BP at each cycle + if( g_suddenDeath.integer ) + { + localHTP = level.suddenDeathHBuildPoints; + localATP = level.suddenDeathABuildPoints; + } + else + { + localHTP = g_humanBuildPoints.integer; + localATP = g_alienBuildPoints.integer; + } + + level.humanBuildPoints = level.humanBuildPointsPowered = localHTP; + level.alienBuildPoints = localATP; + + level.reactorPresent = qfalse; + level.overmindPresent = qfalse; + + for( i = 1, ent = g_entities + i ; i < level.num_entities ; i++, ent++ ) + { + if( !ent->inuse ) + continue; + + if( ent->s.eType != ET_BUILDABLE ) + continue; + + buildable = ent->s.modelindex; + + if( buildable != BA_NONE ) + { + if( buildable == BA_H_REACTOR && ent->spawned && ent->health > 0 ) + level.reactorPresent = qtrue; + + if( buildable == BA_A_OVERMIND && ent->spawned && ent->health > 0 ) + level.overmindPresent = qtrue; + + if( !g_suddenDeath.integer || BG_FindReplaceableTestForBuildable( buildable ) ) + { + if( BG_FindTeamForBuildable( buildable ) == BIT_HUMANS ) + { + level.humanBuildPoints -= BG_FindBuildPointsForBuildable( buildable ); + if( ent->powered ) + level.humanBuildPointsPowered -= BG_FindBuildPointsForBuildable( buildable ); + } + else + { + level.alienBuildPoints -= BG_FindBuildPointsForBuildable( buildable ); + } + } + } + } + + if( level.humanBuildPoints < 0 ) + { + localHTP -= level.humanBuildPoints; + level.humanBuildPointsPowered -= level.humanBuildPoints; + level.humanBuildPoints = 0; + } + + if( level.alienBuildPoints < 0 ) + { + localATP -= level.alienBuildPoints; + level.alienBuildPoints = 0; + } + + trap_SetConfigstring( CS_BUILDPOINTS, va( "%d %d %d %d %d", + level.alienBuildPoints, localATP, + level.humanBuildPoints, localHTP, + level.humanBuildPointsPowered ) ); + + //may as well pump the stages here too + { + float alienPlayerCountMod = level.averageNumAlienClients / PLAYER_COUNT_MOD; + float humanPlayerCountMod = level.averageNumHumanClients / PLAYER_COUNT_MOD; + int alienNextStageThreshold, humanNextStageThreshold; + + if( alienPlayerCountMod < 0.1f ) + alienPlayerCountMod = 0.1f; + + if( humanPlayerCountMod < 0.1f ) + humanPlayerCountMod = 0.1f; + + if( g_alienStage.integer == S1 && g_alienMaxStage.integer > S1 ) + alienNextStageThreshold = (int)( ceil( (float)g_alienStage2Threshold.integer * alienPlayerCountMod ) ); + else if( g_alienStage.integer == S2 && g_alienMaxStage.integer > S2 ) + alienNextStageThreshold = (int)( ceil( (float)g_alienStage3Threshold.integer * alienPlayerCountMod ) ); + else + alienNextStageThreshold = -1; + + if( g_humanStage.integer == S1 && g_humanMaxStage.integer > S1 ) + humanNextStageThreshold = (int)( ceil( (float)g_humanStage2Threshold.integer * humanPlayerCountMod ) ); + else if( g_humanStage.integer == S2 && g_humanMaxStage.integer > S2 ) + humanNextStageThreshold = (int)( ceil( (float)g_humanStage3Threshold.integer * humanPlayerCountMod ) ); + else + humanNextStageThreshold = -1; + + trap_SetConfigstring( CS_STAGES, va( "%d %d %d %d %d %d", + g_alienStage.integer, g_humanStage.integer, + g_alienKills.integer, g_humanKills.integer, + alienNextStageThreshold, humanNextStageThreshold ) ); + } +} + +/* +============ +G_CalculateStages +============ +*/ +void G_CalculateStages( void ) +{ + float alienPlayerCountMod = level.averageNumAlienClients / PLAYER_COUNT_MOD; + float humanPlayerCountMod = level.averageNumHumanClients / PLAYER_COUNT_MOD; + static int lastAlienStageModCount = 1; + static int lastHumanStageModCount = 1; + + if( alienPlayerCountMod < 0.1f ) + alienPlayerCountMod = 0.1f; + + if( humanPlayerCountMod < 0.1f ) + humanPlayerCountMod = 0.1f; + + if( g_alienKills.integer >= + (int)( ceil( (float)g_alienStage2Threshold.integer * alienPlayerCountMod ) ) && + g_alienStage.integer == S1 && g_alienMaxStage.integer > S1 ) + { + trap_Cvar_Set( "g_alienStage", va( "%d", S2 ) ); + level.alienStage2Time = level.time; + lastAlienStageModCount = g_alienStage.modificationCount; + G_LogPrintf("Stage: A 2: Aliens reached Stage 2\n"); + } + + if( g_alienKills.integer >= + (int)( ceil( (float)g_alienStage3Threshold.integer * alienPlayerCountMod ) ) && + g_alienStage.integer == S2 && g_alienMaxStage.integer > S2 ) + { + trap_Cvar_Set( "g_alienStage", va( "%d", S3 ) ); + level.alienStage3Time = level.time; + lastAlienStageModCount = g_alienStage.modificationCount; + G_LogPrintf("Stage: A 3: Aliens reached Stage 3\n"); + } + + if( g_humanKills.integer >= + (int)( ceil( (float)g_humanStage2Threshold.integer * humanPlayerCountMod ) ) && + g_humanStage.integer == S1 && g_humanMaxStage.integer > S1 ) + { + trap_Cvar_Set( "g_humanStage", va( "%d", S2 ) ); + level.humanStage2Time = level.time; + lastHumanStageModCount = g_humanStage.modificationCount; + G_LogPrintf("Stage: H 2: Humans reached Stage 2\n"); + } + + if( g_humanKills.integer >= + (int)( ceil( (float)g_humanStage3Threshold.integer * humanPlayerCountMod ) ) && + g_humanStage.integer == S2 && g_humanMaxStage.integer > S2 ) + { + trap_Cvar_Set( "g_humanStage", va( "%d", S3 ) ); + level.humanStage3Time = level.time; + G_LogPrintf("Stage: H 3: Humans reached Stage 3\n"); + lastHumanStageModCount = g_humanStage.modificationCount; + } + + if( g_alienStage.modificationCount > lastAlienStageModCount ) + { + G_Checktrigger_stages( PTE_ALIENS, g_alienStage.integer ); + if( g_alienStage.integer == S2 ) + level.alienStage2Time = level.time; + else if( g_alienStage.integer == S3 ) + level.alienStage3Time = level.time; + + lastAlienStageModCount = g_alienStage.modificationCount; + } + + if( g_humanStage.modificationCount > lastHumanStageModCount ) + { + G_Checktrigger_stages( PTE_HUMANS, g_humanStage.integer ); + + if( g_humanStage.integer == S2 ) + level.humanStage2Time = level.time; + else if( g_humanStage.integer == S3 ) + level.humanStage3Time = level.time; + + lastHumanStageModCount = g_humanStage.modificationCount; + } +} + +/* +============ +CalculateAvgPlayers + +Calculates the average number of players playing this game +============ +*/ +void G_CalculateAvgPlayers( void ) +{ + //there are no clients or only spectators connected, so + //reset the number of samples in order to avoid the situation + //where the average tends to 0 + if( !level.numAlienClients ) + { + level.numAlienSamples = 0; + trap_Cvar_Set( "g_alienKills", "0" ); + } + + if( !level.numHumanClients ) + { + level.numHumanSamples = 0; + trap_Cvar_Set( "g_humanKills", "0" ); + } + + //calculate average number of clients for stats + level.averageNumAlienClients = + ( ( level.averageNumAlienClients * level.numAlienSamples ) + + level.numAlienClients ) / + (float)( level.numAlienSamples + 1 ); + level.numAlienSamples++; + + level.averageNumHumanClients = + ( ( level.averageNumHumanClients * level.numHumanSamples ) + + level.numHumanClients ) / + (float)( level.numHumanSamples + 1 ); + level.numHumanSamples++; +} + +/* +============ +CalculateRanks + +Recalculates the score ranks of all players +This will be called on every client connect, begin, disconnect, death, +and team change. +============ +*/ +void CalculateRanks( void ) +{ + int i; + char P[ MAX_CLIENTS + 1 ] = {""}; + int ff = 0; + + level.numConnectedClients = 0; + level.numNonSpectatorClients = 0; + level.numPlayingClients = 0; + level.numVotingClients = 0; // don't count bots + level.numAlienClients = 0; + level.numHumanClients = 0; + level.numLiveAlienClients = 0; + level.numLiveHumanClients = 0; + + for( i = 0; i < level.maxclients; i++ ) + { + P[ i ] = '-'; + if ( level.clients[ i ].pers.connected != CON_DISCONNECTED ) + { + level.sortedClients[ level.numConnectedClients ] = i; + level.numConnectedClients++; + P[ i ] = (char)'0' + level.clients[ i ].pers.teamSelection; + + if( level.clients[ i ].pers.connected != CON_CONNECTED ) + continue; + + level.numVotingClients++; + if( level.clients[ i ].pers.teamSelection != PTE_NONE ) + { + level.numPlayingClients++; + if( level.clients[ i ].sess.sessionTeam != TEAM_SPECTATOR ) + level.numNonSpectatorClients++; + + if( level.clients[ i ].pers.teamSelection == PTE_ALIENS ) + { + level.numAlienClients++; + if( level.clients[ i ].sess.sessionTeam != TEAM_SPECTATOR ) + level.numLiveAlienClients++; + } + else if( level.clients[ i ].pers.teamSelection == PTE_HUMANS ) + { + level.numHumanClients++; + if( level.clients[ i ].sess.sessionTeam != TEAM_SPECTATOR ) + level.numLiveHumanClients++; + } + } + } + } + level.numteamVotingClients[ 0 ] = level.numHumanClients; + level.numteamVotingClients[ 1 ] = level.numAlienClients; + P[ i ] = '\0'; + trap_Cvar_Set( "P", P ); + + if( g_friendlyFire.value>0 ) + ff |= ( FFF_HUMANS | FFF_ALIENS ); + if( g_friendlyFireHumans.value>0 ) + ff |= FFF_HUMANS; + if( g_friendlyFireAliens.value>0 ) + ff |= FFF_ALIENS; + if( g_friendlyBuildableFire.value>0 ) + ff |= FFF_BUILDABLES; + trap_Cvar_Set( "ff", va( "%i", ff ) ); + + qsort( level.sortedClients, level.numConnectedClients, + sizeof( level.sortedClients[ 0 ] ), SortRanks ); + + // see if it is time to end the level + CheckExitRules( ); + + // if we are at the intermission, send the new info to everyone + if( level.intermissiontime && !level.mapRotationVoteTime ) + SendScoreboardMessageToAllClients( ); +} + + +/* +======================================================================== + +MAP CHANGING + +======================================================================== +*/ + +/* +======================== +SendScoreboardMessageToAllClients + +Do this at BeginIntermission time and whenever ranks are recalculated +due to enters/exits/forced team changes +======================== +*/ +void SendScoreboardMessageToAllClients( void ) +{ + int i; + + for( i = 0; i < level.maxclients; i++ ) + { + if( level.clients[ i ].pers.connected == CON_CONNECTED ) + ScoreboardMessage( g_entities + i ); + } +} + +/* +======================== +MoveClientToIntermission + +When the intermission starts, this will be called for all players. +If a new client connects, this will be called after the spawn function. +======================== +*/ +void MoveClientToIntermission( gentity_t *ent ) +{ + // take out of follow mode if needed + if( ent->client->sess.spectatorState == SPECTATOR_FOLLOW ) + G_StopFollowing( ent ); + + // move to the spot + VectorCopy( level.intermission_origin, ent->s.origin ); + VectorCopy( level.intermission_origin, ent->client->ps.origin ); + VectorCopy( level.intermission_angle, ent->client->ps.viewangles ); + ent->client->ps.pm_type = PM_INTERMISSION; + + // clean up powerup info + memset( ent->client->ps.powerups, 0, sizeof( ent->client->ps.powerups ) ); + + ent->client->ps.eFlags = 0; + ent->s.eFlags = 0; + ent->s.eType = ET_GENERAL; + ent->s.modelindex = 0; + ent->s.loopSound = 0; + ent->s.event = 0; + ent->r.contents = 0; +} + +/* +================== +FindIntermissionPoint + +This is also used for spectator spawns +================== +*/ +void FindIntermissionPoint( void ) +{ + gentity_t *ent, *target; + vec3_t dir; + + // find the intermission spot + ent = G_Find( NULL, FOFS( classname ), "info_player_intermission" ); + + if( !ent ) + { // the map creator forgot to put in an intermission point... + G_SelectSpawnPoint( vec3_origin, level.intermission_origin, level.intermission_angle ); + } + else + { + VectorCopy( ent->s.origin, level.intermission_origin ); + VectorCopy( ent->s.angles, level.intermission_angle ); + // if it has a target, look towards it + if( ent->target ) + { + target = G_PickTarget( ent->target ); + + if( target ) + { + VectorSubtract( target->s.origin, level.intermission_origin, dir ); + vectoangles( dir, level.intermission_angle ); + } + } + } + +} + +/* +================== +BeginIntermission +================== +*/ +void BeginIntermission( void ) +{ + int i; + gentity_t *client; + + if( level.intermissiontime ) + return; // already active + + level.numTeamWarnings = 99; + + level.intermissiontime = level.time; + + G_ClearVotes( ); + + FindIntermissionPoint( ); + + // move all clients to the intermission point + for( i = 0; i < level.maxclients; i++ ) + { + client = g_entities + i; + + if( !client->inuse ) + continue; + + // respawn if dead + if( client->health <= 0 ) + respawn(client); + + MoveClientToIntermission( client ); + } + + // send the current scoring to all clients + SendScoreboardMessageToAllClients( ); + + if( g_nextMap.string[ 0 ] ) + { + trap_SendServerCommand( -1, + va( "print \"next map has been set to %s^7%s\n\"", + g_nextMap.string, + ( G_CheckMapRotationVote() ) ? ", voting will be skipped" : "" ) ); + } +} + +void BeginMapRotationVote( void ) +{ + gentity_t *ent; + int length; + int i; + + if( level.mapRotationVoteTime ) + return; + + length = g_mapRotationVote.integer; + if( length > 60 ) + length = 60; + level.mapRotationVoteTime = level.time + ( length * 1000 ); + + for( i = 0; i < level.maxclients; i++ ) + { + ent = g_entities + i; + + if( !ent->inuse ) + continue; + + ent->client->ps.pm_type = PM_SPECTATOR; + ent->client->sess.sessionTeam = TEAM_SPECTATOR; + ent->client->sess.spectatorState = SPECTATOR_LOCKED; + } +} + +/* +============= +ExitLevel + +When the intermission has been exited, the server is either moved +to a new map based on the map rotation or the current map restarted +============= +*/ +void ExitLevel( void ) +{ + int i; + gclient_t *cl; + buildHistory_t *tmp, *mark; + + if( level.mapRotationVoteTime ) + { + if( level.time < level.mapRotationVoteTime && + !G_IntermissionMapVoteWinner( ) ) + return; + } + else if( g_mapRotationVote.integer > 0 && + G_CheckMapRotationVote() && + !g_nextMap.string[ 0 ] ) + { + BeginMapRotationVote( ); + return; + } + + while( ( tmp = level.buildHistory ) ) + { + level.buildHistory = level.buildHistory->next; + while( ( mark = tmp ) ) + { + tmp = tmp->marked; + G_Free( mark ); + } + } + + if ( G_MapExists( g_nextMap.string ) ) + trap_SendConsoleCommand( EXEC_APPEND, va("!map %s\n", g_nextMap.string ) ); + else if( G_MapRotationActive( ) ) + G_AdvanceMapRotation( ); + else + trap_SendConsoleCommand( EXEC_APPEND, "map_restart\n" ); + + trap_Cvar_Set( "g_nextMap", "" ); + + level.restarted = qtrue; + level.changemap = NULL; + level.intermissiontime = 0; + + // reset all the scores so we don't enter the intermission again + for( i = 0; i < g_maxclients.integer; i++ ) + { + cl = level.clients + i; + if( cl->pers.connected != CON_CONNECTED ) + continue; + + cl->ps.persistant[ PERS_SCORE ] = 0; + } + + // we need to do this here before chaning to CON_CONNECTING + G_WriteSessionData( ); + + // change all client states to connecting, so the early players into the + // next level will know the others aren't done reconnecting + for( i = 0; i < g_maxclients.integer; i++ ) + { + if( level.clients[ i ].pers.connected == CON_CONNECTED ) + level.clients[ i ].pers.connected = CON_CONNECTING; + } + +} +/* +================= +G_AdminsPrintf + +Print to all active admins, and the logfile with a time stamp if it is open, and to the console +================= +*/ +void QDECL G_AdminsPrintf( const char *fmt, ... ) +{ + va_list argptr; + char string[ 1024 ]; + gentity_t *tempent; + int j; + + va_start( argptr, fmt ); + vsprintf( string, fmt,argptr ); + va_end( argptr ); + + for( j = 0; j < level.maxclients; j++ ) + { + tempent = &g_entities[ j ]; + if( G_admin_permission( tempent, ADMF_ADMINCHAT ) && + !tempent->client->pers.ignoreAdminWarnings ) + { + trap_SendServerCommand(tempent-g_entities,va( "print \"^6[Admins]^7 %s\"", string) ); + } + } + + G_LogPrintf("%s",string); + +} +/* +================= +G_WarningsPrintf + +Print to everyone with a certain flag, and the logfile with a time stamp if it is open, and to the console +(just a copy of the G_AdminsPrintf with flag suport) +================= +*/ +void QDECL G_WarningsPrintf( char *flag, const char *fmt, ... ) +{ + va_list argptr; + char string[ 1024 ]; + gentity_t *tempent; + int j; + + va_start( argptr, fmt ); + vsprintf( string, fmt,argptr ); + va_end( argptr ); + + for( j = 0; j < level.maxclients; j++ ) + { + tempent = &g_entities[ j ]; + if( G_admin_permission( tempent, flag ) ) + { + trap_SendServerCommand(tempent-g_entities,va( "print \"^6[Warnings]^7 %s\"", string) ); + } + } + + G_LogPrintf("%s",string); + +} +/* +================= +G_LogPrintf + +Print to the logfile with a time stamp if it is open +================= +*/ +void QDECL G_LogPrintf( const char *fmt, ... ) +{ + va_list argptr; + char string[ 1024 ], decoloured[ 1024 ]; + int min, tens, sec; + + sec = ( level.time - level.startTime ) / 1000; + + min = sec / 60; + sec -= min * 60; + tens = sec / 10; + sec -= tens * 10; + + Com_sprintf( string, sizeof( string ), "%3i:%i%i ", min, tens, sec ); + + va_start( argptr, fmt ); + vsprintf( string +7 , fmt,argptr ); + va_end( argptr ); + + if( g_dedicated.integer ) + G_Printf( "%s", string + 7 ); + + if( !level.logFile ) + return; + + if( g_decolourLogfiles.integer ) + { + G_DecolorString( string, decoloured ); + trap_FS_Write( decoloured, strlen( decoloured ), level.logFile ); + } + else + { + trap_FS_Write( string, strlen( string ), level.logFile ); + } +} + +/* +================= +G_LogPrintfColoured + +Bypasses g_decolourLogfiles for events that need colors in the logs +================= +*/ +void QDECL G_LogPrintfColoured( const char *fmt, ... ) +{ + va_list argptr; + char string[ 1024 ]; + int min, tens, sec; + + sec = (level.time - level.startTime) / 1000; + + min = sec / 60; + sec -= min * 60; + tens = sec / 10; + sec -= tens * 10; + + Com_sprintf( string, sizeof( string ), "%3i:%i%i ", min, tens, sec ); + + va_start( argptr, fmt ); + vsprintf( string +7 , fmt,argptr ); + va_end( argptr ); + + if( g_dedicated.integer ) + G_Printf( "%s", string + 7 ); + + if( !level.logFile ) + return; + + trap_FS_Write( string, strlen( string ), level.logFile ); +} + +/* +================= +G_LogOnlyPrintf + +Print to the logfile only (not console) with a time stamp if it is open +================= +*/ +void QDECL G_LogOnlyPrintf( const char *fmt, ... ) +{ + va_list argptr; + char string[ 1024 ], decoloured[ 1024 ]; + int min, tens, sec; + + sec = (level.time - level.startTime) / 1000; + + min = sec / 60; + sec -= min * 60; + tens = sec / 10; + sec -= tens * 10; + + Com_sprintf( string, sizeof( string ), "%3i:%i%i ", min, tens, sec ); + + va_start( argptr, fmt ); + vsprintf( string +7 , fmt,argptr ); + va_end( argptr ); + + if( !level.logFile ) + return; + + if( g_decolourLogfiles.integer ) + { + G_DecolorString( string, decoloured ); + trap_FS_Write( decoloured, strlen( decoloured ), level.logFile ); + } + else + { + trap_FS_Write( string, strlen( string ), level.logFile ); + } +} + +/* +================= +G_SendGameStat +================= +*/ +void G_SendGameStat( pTeam_t team ) +{ + char map[ MAX_STRING_CHARS ]; + char teamChar; + char data[ BIG_INFO_STRING ]; + char entry[ MAX_STRING_CHARS ]; + int i, dataLength, entryLength; + gclient_t *cl; + + trap_Cvar_VariableStringBuffer( "mapname", map, sizeof( map ) ); + + switch( team ) + { + case PTE_ALIENS: teamChar = 'A'; break; + case PTE_HUMANS: teamChar = 'H'; break; + case PTE_NONE: teamChar = 'L'; break; + default: return; + } + + Com_sprintf( data, BIG_INFO_STRING, + "%s %s T:%c A:%f H:%f M:%s D:%d SD:%d AS:%d AS2T:%d AS3T:%d HS:%d HS2T:%d HS3T:%d CL:%d", + Q3_VERSION, + g_tag.string, + teamChar, + level.averageNumAlienClients, + level.averageNumHumanClients, + map, + level.time - level.startTime, + G_TimeTilSuddenDeath( ), + g_alienStage.integer, + level.alienStage2Time - level.startTime, + level.alienStage3Time - level.startTime, + g_humanStage.integer, + level.humanStage2Time - level.startTime, + level.humanStage3Time - level.startTime, + level.numConnectedClients ); + + dataLength = strlen( data ); + + for( i = 0; i < level.numConnectedClients; i++ ) + { + int ping; + + cl = &level.clients[ level.sortedClients[ i ] ]; + + // Ignore invisible players + if ( cl->sess.invisible == qtrue ) + continue; + + if( cl->pers.connected == CON_CONNECTING ) + ping = -1; + else + ping = cl->ps.ping < 999 ? cl->ps.ping : 999; + + switch( cl->ps.stats[ STAT_PTEAM ] ) + { + case PTE_ALIENS: teamChar = 'A'; break; + case PTE_HUMANS: teamChar = 'H'; break; + case PTE_NONE: teamChar = 'S'; break; + default: return; + } + + Com_sprintf( entry, MAX_STRING_CHARS, + " \"%s\" %c %d %d %d", + cl->pers.netname, + teamChar, + cl->ps.persistant[ PERS_SCORE ], + ping, + ( level.time - cl->pers.enterTime ) / 60000 ); + + entryLength = strlen( entry ); + + if( dataLength + entryLength >= BIG_INFO_STRING ) + break; + + strcpy( data + dataLength, entry ); + dataLength += entryLength; + } + + trap_SendGameStat( data ); +} + +/* +================ +LogExit + +Append information about this game to the log file +================ +*/ +void LogExit( const char *string ) +{ + int i, numSorted; + gclient_t *cl; + gentity_t *ent; + + G_LogPrintf( "Exit: %s\n", string ); + + level.intermissionQueued = level.time; + + // this will keep the clients from playing any voice sounds + // that will get cut off when the queued intermission starts + trap_SetConfigstring( CS_INTERMISSION, "1" ); + + // don't send more than 32 scores (FIXME?) + numSorted = level.numConnectedClients; + if( numSorted > 32 ) + numSorted = 32; + + for( i = 0; i < numSorted; i++ ) + { + int ping; + + cl = &level.clients[ level.sortedClients[ i ] ]; + + if( cl->ps.stats[ STAT_PTEAM ] == PTE_NONE ) + continue; + + if( cl->pers.connected == CON_CONNECTING ) + continue; + + ping = cl->ps.ping < 999 ? cl->ps.ping : 999; + + G_LogPrintf( "score: %i ping: %i client: %i %s\n", + cl->ps.persistant[ PERS_SCORE ], ping, level.sortedClients[ i ], + cl->pers.netname ); + + } + + for( i = 1, ent = g_entities + i ; i < level.num_entities ; i++, ent++ ) + { + if( !ent->inuse ) + continue; + + if( !Q_stricmp( ent->classname, "trigger_win" ) ) + { + if( level.lastWin == ent->stageTeam ) + ent->use( ent, ent, ent ); + } + } + + G_SendGameStat( level.lastWin ); +} + + +/* +================= +CheckIntermissionExit + +The level will stay at the intermission for a minimum of 5 seconds +If all players wish to continue, the level will then exit. +If one or more players have not acknowledged the continue, the game will +wait 10 seconds before going on. +================= +*/ +void CheckIntermissionExit( void ) +{ + int ready, notReady, numPlayers; + int i; + gclient_t *cl; + int readyMask; + + //if no clients are connected, just exit + if( !level.numConnectedClients ) + { + ExitLevel( ); + return; + } + + // map vote started + if( level.mapRotationVoteTime ) + { + ExitLevel( ); + return; + } + + // see which players are ready + ready = 0; + notReady = 0; + readyMask = 0; + numPlayers = 0; + for( i = 0; i < g_maxclients.integer; i++ ) + { + cl = level.clients + i; + if( cl->pers.connected != CON_CONNECTED ) + continue; + + if( cl->ps.stats[ STAT_PTEAM ] == PTE_NONE ) + continue; + + if( cl->readyToExit ) + { + ready++; + if( i < 16 ) + readyMask |= 1 << i; + } + else + notReady++; + + numPlayers++; + } + + trap_SetConfigstring( CS_CLIENTS_READY, va( "%d", readyMask ) ); + + // never exit in less than five seconds + if( level.time < level.intermissiontime + 5000 ) + return; + + // never let intermission go on for over 1 minute + if( level.time > level.intermissiontime + 60000 ) + { + ExitLevel( ); + return; + } + + // if nobody wants to go, clear timer + if( !ready && numPlayers ) + { + level.readyToExit = qfalse; + return; + } + + // if everyone wants to go, go now + if( !notReady ) + { + ExitLevel( ); + return; + } + + // if only a percent is needed to ready, check for it + if( g_readyPercent.integer && numPlayers && + ready * 100 / numPlayers >= g_readyPercent.integer ) + { + ExitLevel( ); + return; + } + + // the first person to ready starts the thirty second timeout + if( !level.readyToExit ) + { + level.readyToExit = qtrue; + level.exitTime = level.time; + } + + // if we have waited thirty seconds since at least one player + // wanted to exit, go ahead + if( level.time < level.exitTime + 30000 ) + return; + + ExitLevel( ); +} + +/* +============= +ScoreIsTied +============= +*/ +qboolean ScoreIsTied( void ) +{ + int a, b; + + if( level.numPlayingClients < 2 ) + return qfalse; + + a = level.clients[ level.sortedClients[ 0 ] ].ps.persistant[ PERS_SCORE ]; + b = level.clients[ level.sortedClients[ 1 ] ].ps.persistant[ PERS_SCORE ]; + + return a == b; +} + +/* +================= +CheckExitRules + +There will be a delay between the time the exit is qualified for +and the time everyone is moved to the intermission spot, so you +can see the last frag. +================= +*/ +void CheckExitRules( void ) +{ + // if at the intermission, wait for all non-bots to + // signal ready, then go to next level + if( level.intermissiontime ) + { + CheckIntermissionExit( ); + return; + } + + if( level.intermissionQueued ) + { + if( level.time - level.intermissionQueued >= INTERMISSION_DELAY_TIME ) + { + level.intermissionQueued = 0; + BeginIntermission( ); + } + + return; + } + + if( g_timelimit.integer ) + { + if( level.time - level.startTime >= g_timelimit.integer * 60000 ) + { + level.lastWin = PTE_NONE; + trap_SendServerCommand( -1, "print \"Timelimit hit\n\"" ); + trap_SetConfigstring( CS_WINNER, "Stalemate" ); + LogExit( "Timelimit hit." ); + G_admin_maplog_result( "t" ); + return; + } + else if( level.time - level.startTime >= ( g_timelimit.integer - 5 ) * 60000 && + level.timelimitWarning < TW_IMMINENT ) + { + trap_SendServerCommand( -1, "cp \"5 minutes remaining!\"" ); + level.timelimitWarning = TW_IMMINENT; + } + else if( level.time - level.startTime >= ( g_timelimit.integer - 1 ) * 60000 && + level.timelimitWarning < TW_PASSED ) + { + trap_SendServerCommand( -1, "cp \"1 minute remaining!\"" ); + level.timelimitWarning = TW_PASSED; + } + } + + if( level.uncondHumanWin || + ( ( level.time > level.startTime + 1000 ) && + ( level.numAlienSpawns == 0 ) && + ( level.numLiveAlienClients == 0 ) ) ) + { + //humans win + level.lastWin = PTE_HUMANS; + trap_SendServerCommand( -1, "print \"Humans win\n\""); + trap_SetConfigstring( CS_WINNER, "Humans Win" ); + LogExit( "Humans win." ); + G_admin_maplog_result( "h" ); + } + else if( level.uncondAlienWin || + ( ( level.time > level.startTime + 1000 ) && + ( level.numHumanSpawns == 0 ) && + ( level.numLiveHumanClients == 0 ) ) ) + { + //aliens win + level.lastWin = PTE_ALIENS; + trap_SendServerCommand( -1, "print \"Aliens win\n\""); + trap_SetConfigstring( CS_WINNER, "Aliens Win" ); + LogExit( "Aliens win." ); + G_admin_maplog_result( "a" ); + } +} + + + +/* +======================================================================== + +FUNCTIONS CALLED EVERY FRAME + +======================================================================== +*/ + + +/* +================== +CheckVote +================== +*/ +void CheckVote( void ) +{ + int votePassThreshold=level.votePassThreshold; + int voteYesPercent; + + if( level.voteExecuteTime && level.voteExecuteTime < level.time ) + { + level.voteExecuteTime = 0; + + if( !Q_stricmp( level.voteString, "map_restart" ) ) + { + G_admin_maplog_result( "r" ); + } + else if( !Q_stricmpn( level.voteString, "map", 3 ) ) + { + G_admin_maplog_result( "m" ); + } + + + if( !Q_stricmp( level.voteString, "suddendeath" ) ) + { + level.suddenDeathBeginTime = level.time + ( 1000 * g_suddenDeathVoteDelay.integer ) - level.startTime; + + level.voteString[0] = '\0'; + + if( g_suddenDeathVoteDelay.integer ) + trap_SendServerCommand( -1, va("cp \"Sudden Death will begin in %d seconds\n\"", g_suddenDeathVoteDelay.integer ) ); + } + + if( level.voteString[0] ) + trap_SendConsoleCommand( EXEC_APPEND, va( "%s\n", level.voteString ) ); + + if( !Q_stricmp( level.voteString, "map_restart" ) || + !Q_stricmpn( level.voteString, "map", 3 ) ) + { + level.restarted = qtrue; + } + } + + if( !level.voteTime ) + return; + + if( level.voteYes + level.voteNo > 0 ) + voteYesPercent = (int)( 100 * ( level.voteYes ) / ( level.voteYes + level.voteNo ) ); + else + voteYesPercent = 0; + + if( ( level.time - level.voteTime >= VOTE_TIME ) || + ( level.voteYes + level.voteNo == level.numConnectedClients ) ) + { + if( voteYesPercent> votePassThreshold || level.voteNo == 0 ) + { + // execute the command, then remove the vote + trap_SendServerCommand( -1, va("print \"Vote passed (%d - %d)\n\"", + level.voteYes, level.voteNo ) ); + G_LogPrintf( "Vote: Vote passed (%d-%d)\n", level.voteYes, level.voteNo ); + level.voteExecuteTime = level.time + 3000; + } + else + { + // same behavior as a timeout + trap_SendServerCommand( -1, va("print \"Vote failed (%d - %d)\n\"", + level.voteYes, level.voteNo ) ); + G_LogPrintf( "Vote: Vote failed (%d - %d)\n", level.voteYes, level.voteNo ); + } + } + else + { + if( level.voteYes > (int)( (double) level.numConnectedClients * + ( (double) votePassThreshold/100.0 ) ) ) + { + // execute the command, then remove the vote + trap_SendServerCommand( -1, va("print \"Vote passed (%d - %d)\n\"", + level.voteYes, level.voteNo ) ); + G_LogPrintf( "Vote: Vote passed (%d - %d)\n", level.voteYes, level.voteNo ); + level.voteExecuteTime = level.time + 3000; + } + else if( level.voteNo > (int)( (double) level.numConnectedClients * + ( (double) ( 100.0-votePassThreshold )/ 100.0 ) ) ) + { + // same behavior as a timeout + trap_SendServerCommand( -1, va("print \"Vote failed (%d - %d)\n\"", + level.voteYes, level.voteNo ) ); + G_LogPrintf("Vote failed\n"); + } + else + { + // still waiting for a majority + return; + } + } + + level.voteTime = 0; + trap_SetConfigstring( CS_VOTE_TIME, "" ); + trap_SetConfigstring( CS_VOTE_STRING, "" ); +} + + +/* +================== +CheckTeamVote +================== +*/ +void CheckTeamVote( int team ) +{ + int cs_offset; + + if ( team == PTE_HUMANS ) + cs_offset = 0; + else if ( team == PTE_ALIENS ) + cs_offset = 1; + else + return; + + if( !level.teamVoteTime[ cs_offset ] ) + return; + + if( level.time - level.teamVoteTime[ cs_offset ] >= VOTE_TIME ) + { + if( level.teamVoteYes[ cs_offset ] > level.teamVoteNo[ cs_offset ] && level.teamVoteYes[ cs_offset ] >= 2 ) + { + // execute the command, then remove the vote + trap_SendServerCommand( -1, va("print \"Team vote passed (%d - %d)\n\"", level.teamVoteYes[ cs_offset ], level.teamVoteNo[ cs_offset ] ) ); + trap_SendConsoleCommand( EXEC_APPEND, va( "%s\n", level.teamVoteString[ cs_offset ] ) ); + } + else + { + trap_SendServerCommand( -1, va("print \"Team vote failed (%d - %d)\n\"", level.teamVoteYes[ cs_offset ], level.teamVoteNo[ cs_offset ] ) ); + G_LogPrintf( "Teamvote: Team vote failed (%d - %d)\n", level.teamVoteYes[ cs_offset ], level.teamVoteNo[ cs_offset ] ); + } + } + else + { + if( level.teamVoteYes[ cs_offset ] > level.numteamVotingClients[ cs_offset ] / 2 ) + { + // execute the command, then remove the vote + trap_SendServerCommand( -1, va("print \"Team vote passed (%d - %d)\n\"", level.teamVoteYes[ cs_offset ], level.teamVoteNo[ cs_offset ] ) ); + G_LogPrintf( "Teamvote: Team vote passed (%d - %d)\n", level.teamVoteYes[ cs_offset ], level.teamVoteNo[ cs_offset ] ); + // + trap_SendConsoleCommand( EXEC_APPEND, va( "%s\n", level.teamVoteString[ cs_offset ] ) ); + } + else if( level.teamVoteNo[ cs_offset ] >= level.numteamVotingClients[ cs_offset ] / 2 ) + { + // same behavior as a timeout + trap_SendServerCommand( -1, va("print \"Team vote failed (%d - %d)\n\"", level.teamVoteYes[ cs_offset ], level.teamVoteNo[ cs_offset ] ) ); + G_LogPrintf( "Teamvote: Team vote failed (%d - %d)\n", level.teamVoteYes[ cs_offset ], level.teamVoteNo[ cs_offset ] ); + } + else + { + // still waiting for a majority + return; + } + } + + level.teamVoteTime[ cs_offset ] = 0; + trap_SetConfigstring( CS_TEAMVOTE_TIME + cs_offset, "" ); + trap_SetConfigstring( CS_TEAMVOTE_STRING + cs_offset, "" ); +} + +/* +================== +CheckMsgTimer +================== +*/ +void CheckMsgTimer( void ) +{ + static int LastTime = 0; + + if( level.time - LastTime < 1000 ) + return; + + LastTime = level.time; + + if( level.mapRotationVoteTime ) + { + G_IntermissionMapVoteMessageAll( ); + return; + } + + if( g_welcomeMsgTime.integer && g_welcomeMsg.string[ 0 ] ) + { + char buffer[ MAX_STRING_CHARS ]; + int wt; + int i; + + buffer[ 0 ] = '\0'; + wt = g_welcomeMsgTime.integer * 1000; + for( i = 0; i < level.maxclients; i++ ) + { + if( level.clients[ i ].pers.connected != CON_CONNECTED ) + continue; + + if( level.time - level.clients[ i ].pers.enterTime < wt ) + { + if( buffer[ 0 ] == '\0' ) + { + Q_strncpyz( buffer, g_welcomeMsg.string, sizeof( buffer ) ); + G_ParseEscapedString( buffer ); + } + trap_SendServerCommand( i, va( "cp \"%s\"", buffer ) ); + } + } + } + + if( !g_msgTime.integer ) + return; + + if( level.time - level.lastMsgTime < abs( g_msgTime.integer ) * 60000 ) + return; + + // negative settings only print once per map + if( ( level.lastMsgTime ) && g_msgTime.integer < 0 ) + return; + + level.lastMsgTime = level.time; + + if( g_msg.string[0] ) + { + char buffer[ MAX_STRING_CHARS ]; + + Q_strncpyz( buffer, g_msg.string, sizeof( buffer ) ); + G_ParseEscapedString( buffer ); + trap_SendServerCommand( -1, va( "cp \"%s\"", buffer ) ); + trap_SendServerCommand( -1, va( "print \"%s\n\"", buffer ) ); + } +} + +/* +================== +CheckCountdown +================== +*/ +void CheckCountdown( void ) +{ + static int lastmsg = 0; + int timeleft = g_warmup.integer - ( level.time - level.startTime ) / 1000; + + if( !g_doWarmup.integer || timeleft < 0 ) + return; + + if( level.time - lastmsg < 1000 ) + return; + + lastmsg = level.time; + if( timeleft > 0 ) + trap_SendServerCommand( -1, va( "cp \"^1Warmup Time:^7\n^%i----- ^7%i ^%i-----\"", timeleft % 7, timeleft, timeleft % 7 ) ); + else if( timeleft == 0 ) + trap_SendServerCommand( -1, "cp \"^2----- GO! -----^7\"" ); +} + + +/* +================== +CheckCvars +================== +*/ +void CheckCvars( void ) +{ + static int lastPasswordModCount = -1; + static int lastMarkDeconModCount = -1; + static int lastSDTimeModCount = -1; + + if( g_password.modificationCount != lastPasswordModCount ) + { + lastPasswordModCount = g_password.modificationCount; + + if( *g_password.string && Q_stricmp( g_password.string, "none" ) ) + trap_Cvar_Set( "g_needpass", "1" ); + else + trap_Cvar_Set( "g_needpass", "0" ); + } + + // Unmark any structures for deconstruction when + // the server setting is changed + if( g_markDeconstruct.modificationCount != lastMarkDeconModCount ) + { + int i; + gentity_t *ent; + + lastMarkDeconModCount = g_markDeconstruct.modificationCount; + + for( i = 1, ent = g_entities + i ; i < level.num_entities ; i++, ent++ ) + { + if( !ent->inuse ) + continue; + + if( ent->s.eType != ET_BUILDABLE ) + continue; + + ent->deconstruct = qfalse; + } + } + + if( g_suddenDeathTime.modificationCount != lastSDTimeModCount ) + { + lastSDTimeModCount = g_suddenDeathTime.modificationCount; + level.suddenDeathBeginTime = g_suddenDeathTime.integer * 60000; + } + + level.frameMsec = trap_Milliseconds( ); +} + +/* +============= +G_RunThink + +Runs thinking code for this frame if necessary +============= +*/ +void G_RunThink( gentity_t *ent ) +{ + float thinktime; + + thinktime = ent->nextthink; + if( thinktime <= 0 ) + return; + + if( thinktime > level.time ) + return; + + ent->nextthink = 0; + if( !ent->think ) + G_Error( "NULL ent->think" ); + + ent->think( ent ); +} + +/* +============= +G_EvaluateAcceleration + +Calculates the acceleration for an entity +============= +*/ +void G_EvaluateAcceleration( gentity_t *ent, int msec ) +{ + vec3_t deltaVelocity; + vec3_t deltaAccel; + + VectorSubtract( ent->s.pos.trDelta, ent->oldVelocity, deltaVelocity ); + VectorScale( deltaVelocity, 1.0f / (float)msec, ent->acceleration ); + + VectorSubtract( ent->acceleration, ent->oldAccel, deltaAccel ); + VectorScale( deltaAccel, 1.0f / (float)msec, ent->jerk ); + + VectorCopy( ent->s.pos.trDelta, ent->oldVelocity ); + VectorCopy( ent->acceleration, ent->oldAccel ); +} + +/* +================ +G_RunFrame + +Advances the non-player objects in the world +================ +*/ +void G_RunFrame( int levelTime ) +{ + int i; + gentity_t *ent; + int msec; + int start, end; + + // if we are waiting for the level to restart, do nothing + if( level.restarted ) + return; + + if( level.paused ) + { + if( ( levelTime % 6000 ) == 0) + trap_SendServerCommand( -1, "cp \"^3Game is paused.\"" ); + + level.startTime += levelTime - level.time; + trap_SetConfigstring( CS_LEVEL_START_TIME, va( "%i", level.startTime ) ); + + if( levelTime - level.pauseTime > 3 * 60000 ) + { + trap_SendConsoleCommand( EXEC_APPEND, "!unpause" ); + } + } + + CheckMsgTimer( ); + CheckCountdown( ); + + level.framenum++; + level.previousTime = level.time; + level.time = levelTime; + msec = level.time - level.previousTime; + + //TA: seed the rng + srand( level.framenum ); + + // get any cvar changes + G_UpdateCvars( ); + + // + // go through all allocated objects + // + start = trap_Milliseconds( ); + ent = &g_entities[ 0 ]; + + for( i = 0; i < level.num_entities; i++, ent++ ) + { + if( !ent->inuse ) + continue; + + // clear events that are too old + if( level.time - ent->eventTime > EVENT_VALID_MSEC ) + { + if( ent->s.event ) + { + ent->s.event = 0; // &= EV_EVENT_BITS; + if ( ent->client ) + { + ent->client->ps.externalEvent = 0; + //ent->client->ps.events[0] = 0; + //ent->client->ps.events[1] = 0; + } + } + + if( ent->freeAfterEvent ) + { + // tempEntities or dropped items completely go away after their event + G_FreeEntity( ent ); + continue; + } + else if( ent->unlinkAfterEvent ) + { + // items that will respawn will hide themselves after their pickup event + ent->unlinkAfterEvent = qfalse; + trap_UnlinkEntity( ent ); + } + } + + // temporary entities don't think + if( ent->freeAfterEvent ) + continue; + + //TA: calculate the acceleration of this entity + if( ent->evaluateAcceleration ) + G_EvaluateAcceleration( ent, msec ); + + if( !ent->r.linked && ent->neverFree ) + continue; + + if( ent->s.eType == ET_MISSILE ) + { + G_RunMissile( ent ); + continue; + } + + if( ent->s.eType == ET_BUILDABLE ) + { + G_BuildableThink( ent, msec ); + continue; + } + + if( ent->s.eType == ET_CORPSE || ent->physicsObject ) + { + G_Physics( ent, msec ); + continue; + } + + if( ent->s.eType == ET_MOVER ) + { + G_RunMover( ent ); + continue; + } + + if( i < MAX_CLIENTS ) + { + G_RunClient( ent ); + continue; + } + + G_RunThink( ent ); + } + end = trap_Milliseconds(); + + start = trap_Milliseconds(); + + // perform final fixups on the players + ent = &g_entities[ 0 ]; + + for( i = 0; i < level.maxclients; i++, ent++ ) + { + if( ent->inuse ) + ClientEndFrame( ent ); + } + + // save position information for all active clients + G_UnlaggedStore( ); + + end = trap_Milliseconds(); + + //TA: + G_CountSpawns( ); + G_CalculateBuildPoints( ); + G_CalculateStages( ); + G_SpawnClients( PTE_ALIENS ); + G_SpawnClients( PTE_HUMANS ); + G_CalculateAvgPlayers( ); + G_UpdateZaps( msec ); + + // see if it is time to end the level + CheckExitRules( ); + + // update to team status? + CheckTeamStatus( ); + + // cancel vote if timed out + CheckVote( ); + + // check team votes + CheckTeamVote( PTE_HUMANS ); + CheckTeamVote( PTE_ALIENS ); + + // for tracking changes + CheckCvars( ); + + if( g_listEntity.integer ) + { + for( i = 0; i < MAX_GENTITIES; i++ ) + G_Printf( "%4i: %s\n", i, g_entities[ i ].classname ); + + trap_Cvar_Set( "g_listEntity", "0" ); + } +} + diff --git a/src/game/g_maprotation.c b/src/game/g_maprotation.c new file mode 100644 index 0000000..60fd696 --- /dev/null +++ b/src/game/g_maprotation.c @@ -0,0 +1,1316 @@ +/* +=========================================================================== +Copyright (C) 1999-2005 Id Software, Inc. +Copyright (C) 2000-2006 Tim Angus + +This file is part of Tremulous. + +Tremulous is free software; you can redistribute it +and/or modify it under the terms of the GNU General Public License as +published by the Free Software Foundation; either version 2 of the License, +or (at your option) any later version. + +Tremulous is distributed in the hope that it will be +useful, but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with Tremulous; if not, write to the Free Software +Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA +=========================================================================== +*/ + +// g_maprotation.c -- the map rotation system + +#include "g_local.h" + +mapRotations_t mapRotations; + + +static qboolean G_GetVotedMap( char *name, int size, int rotation, int map ); + +/* +=============== +G_MapExists + +Check if a map exists +=============== +*/ +qboolean G_MapExists( char *name ) +{ + return trap_FS_FOpenFile( va( "maps/%s.bsp", name ), NULL, FS_READ ); +} + +/* +=============== +G_RotationExists + +Check if a rotation exists +=============== +*/ +static qboolean G_RotationExists( char *name ) +{ + int i; + + for( i = 0; i < mapRotations.numRotations; i++ ) + { + if( Q_strncmp( mapRotations.rotations[ i ].name, name, MAX_QPATH ) == 0 ) + return qtrue; + } + + return qfalse; +} + +/* +=============== +G_ParseCommandSection + +Parse a map rotation command section +=============== +*/ +static qboolean G_ParseMapCommandSection( mapRotationEntry_t *mre, char **text_p ) +{ + char *token; + + // read optional parameters + while( 1 ) + { + token = COM_Parse( text_p ); + + if( !token ) + break; + + if( !Q_stricmp( token, "" ) ) + return qfalse; + + if( !Q_stricmp( token, "}" ) ) + return qtrue; //reached the end of this command section + + if( !Q_stricmp( token, "layouts" ) ) + { + token = COM_ParseExt( text_p, qfalse ); + mre->layouts[ 0 ] = '\0'; + while( token && token[ 0 ] != 0 ) + { + Q_strcat( mre->layouts, sizeof( mre->layouts ), token ); + Q_strcat( mre->layouts, sizeof( mre->layouts ), " " ); + token = COM_ParseExt( text_p, qfalse ); + } + continue; + } + + Q_strncpyz( mre->postCmds[ mre->numCmds ], token, sizeof( mre->postCmds[ 0 ] ) ); + Q_strcat( mre->postCmds[ mre->numCmds ], sizeof( mre->postCmds[ 0 ] ), " " ); + + token = COM_ParseExt( text_p, qfalse ); + + while( token && token[ 0 ] != 0 ) + { + Q_strcat( mre->postCmds[ mre->numCmds ], sizeof( mre->postCmds[ 0 ] ), token ); + Q_strcat( mre->postCmds[ mre->numCmds ], sizeof( mre->postCmds[ 0 ] ), " " ); + token = COM_ParseExt( text_p, qfalse ); + } + + if( mre->numCmds == MAX_MAP_COMMANDS ) + { + G_Printf( S_COLOR_RED "ERROR: maximum number of map commands (%d) reached\n", + MAX_MAP_COMMANDS ); + return qfalse; + } + else + mre->numCmds++; + } + + return qfalse; +} + +/* +=============== +G_ParseMapRotation + +Parse a map rotation section +=============== +*/ +static qboolean G_ParseMapRotation( mapRotation_t *mr, char **text_p ) +{ + char *token; + qboolean mnSet = qfalse; + mapRotationEntry_t *mre = NULL; + mapRotationCondition_t *mrc; + + // read optional parameters + while( 1 ) + { + token = COM_Parse( text_p ); + + if( !token ) + break; + + if( !Q_stricmp( token, "" ) ) + return qfalse; + + if( !Q_stricmp( token, "{" ) ) + { + if( !mnSet ) + { + G_Printf( S_COLOR_RED "ERROR: map settings section with no name\n" ); + return qfalse; + } + + if( !G_ParseMapCommandSection( mre, text_p ) ) + { + G_Printf( S_COLOR_RED "ERROR: failed to parse map command section\n" ); + return qfalse; + } + + mnSet = qfalse; + continue; + } + else if( !Q_stricmp( token, "goto" ) ) + { + token = COM_Parse( text_p ); + + if( !token ) + break; + + mrc = &mre->conditions[ mre->numConditions ]; + mrc->unconditional = qtrue; + Q_strncpyz( mrc->dest, token, sizeof( mrc->dest ) ); + + if( mre->numConditions == MAX_MAP_ROTATION_CONDS ) + { + G_Printf( S_COLOR_RED "ERROR: maximum number of conditions for one map (%d) reached\n", + MAX_MAP_ROTATION_CONDS ); + return qfalse; + } + else + mre->numConditions++; + + continue; + } + else if( !Q_stricmp( token, "if" ) ) + { + token = COM_Parse( text_p ); + + if( !token ) + break; + + mrc = &mre->conditions[ mre->numConditions ]; + + if( !Q_stricmp( token, "numClients" ) ) + { + mrc->lhs = MCV_NUMCLIENTS; + + token = COM_Parse( text_p ); + + if( !token ) + break; + + if( !Q_stricmp( token, "<" ) ) + mrc->op = MCO_LT; + else if( !Q_stricmp( token, ">" ) ) + mrc->op = MCO_GT; + else if( !Q_stricmp( token, "=" ) ) + mrc->op = MCO_EQ; + else + { + G_Printf( S_COLOR_RED "ERROR: invalid operator in expression: %s\n", token ); + return qfalse; + } + + token = COM_Parse( text_p ); + + if( !token ) + break; + + mrc->numClients = atoi( token ); + } + else if( !Q_stricmp( token, "lastWin" ) ) + { + mrc->lhs = MCV_LASTWIN; + + token = COM_Parse( text_p ); + + if( !token ) + break; + + if( !Q_stricmp( token, "aliens" ) ) + mrc->lastWin = PTE_ALIENS; + else if( !Q_stricmp( token, "humans" ) ) + mrc->lastWin = PTE_HUMANS; + else + { + G_Printf( S_COLOR_RED "ERROR: invalid right hand side in expression: %s\n", token ); + return qfalse; + } + } + else if( !Q_stricmp( token, "random" ) ) + mrc->lhs = MCV_RANDOM; + else + { + G_Printf( S_COLOR_RED "ERROR: invalid left hand side in expression: %s\n", token ); + return qfalse; + } + + token = COM_Parse( text_p ); + + if( !token ) + break; + + mrc->unconditional = qfalse; + Q_strncpyz( mrc->dest, token, sizeof( mrc->dest ) ); + + if( mre->numConditions == MAX_MAP_ROTATION_CONDS ) + { + G_Printf( S_COLOR_RED "ERROR: maximum number of conditions for one map (%d) reached\n", + MAX_MAP_ROTATION_CONDS ); + return qfalse; + } + else + mre->numConditions++; + + continue; + } + else if( !Q_stricmp( token, "*VOTE*" ) ) + { + if( mr->numMaps == MAX_MAP_ROTATION_MAPS ) + { + G_Printf( S_COLOR_RED "ERROR: maximum number of maps in one rotation (%d) reached\n", + MAX_MAP_ROTATION_MAPS ); + return qfalse; + } + mre = &mr->maps[ mr->numMaps ]; + Q_strncpyz( mre->name, token, sizeof( mre->name ) ); + + token = COM_Parse( text_p ); + + if( !Q_stricmp( token, "{" ) ) + { + while( 1 ) + { + token = COM_Parse( text_p ); + + if( !token ) + break; + + if( !Q_stricmp( token, "}" ) ) + { + break; + } + else + { + if( mre->numConditions < MAX_MAP_ROTATION_CONDS ) + { + mrc = &mre->conditions[ mre->numConditions ]; + mrc->lhs = MCV_VOTE; + mrc->unconditional = qfalse; + Q_strncpyz( mrc->dest, token, sizeof( mrc->dest ) ); + + mre->numConditions++; + } + else + { + G_Printf( S_COLOR_YELLOW "WARNING: maximum number of maps for one vote (%d) reached\n", + MAX_MAP_ROTATION_CONDS ); + } + } + } + if( !mre->numConditions ) + { + G_Printf( S_COLOR_YELLOW "WARNING: no maps in *VOTE* section\n" ); + } + else + { + mr->numMaps++; + mnSet = qtrue; + } + } + else + { + G_Printf( S_COLOR_RED "ERROR: *VOTE* with no section\n" ); + return qfalse; + } + + continue; + } + else if( !Q_stricmp( token, "*RANDOM*" ) ) + { + if( mr->numMaps == MAX_MAP_ROTATION_MAPS ) + { + G_Printf( S_COLOR_RED "ERROR: maximum number of maps in one rotation (%d) reached\n", + MAX_MAP_ROTATION_MAPS ); + return qfalse; + } + mre = &mr->maps[ mr->numMaps ]; + Q_strncpyz( mre->name, token, sizeof( mre->name ) ); + + token = COM_Parse( text_p ); + + if( !Q_stricmp( token, "{" ) ) + { + while( 1 ) + { + token = COM_Parse( text_p ); + + if( !token ) + break; + + if( !Q_stricmp( token, "}" ) ) + { + break; + } + else + { + if( mre->numConditions < MAX_MAP_ROTATION_CONDS ) + { + mrc = &mre->conditions[ mre->numConditions ]; + mrc->lhs = MCV_SELECTEDRANDOM; + mrc->unconditional = qfalse; + Q_strncpyz( mrc->dest, token, sizeof( mrc->dest ) ); + + mre->numConditions++; + } + else + { + G_Printf( S_COLOR_YELLOW "WARNING: maximum number of maps for one Random Slot (%d) reached\n", + MAX_MAP_ROTATION_CONDS ); + } + } + } + if( !mre->numConditions ) + { + G_Printf( S_COLOR_YELLOW "WARNING: no maps in *RANDOM* section\n" ); + } + else + { + mr->numMaps++; + mnSet = qtrue; + } + } + else + { + G_Printf( S_COLOR_RED "ERROR: *RANDOM* with no section\n" ); + return qfalse; + } + + continue; + } + else if( !Q_stricmp( token, "}" ) ) + return qtrue; //reached the end of this map rotation + + mre = &mr->maps[ mr->numMaps ]; + + if( mr->numMaps == MAX_MAP_ROTATION_MAPS ) + { + G_Printf( S_COLOR_RED "ERROR: maximum number of maps in one rotation (%d) reached\n", + MAX_MAP_ROTATION_MAPS ); + return qfalse; + } + else + mr->numMaps++; + + Q_strncpyz( mre->name, token, sizeof( mre->name ) ); + mnSet = qtrue; + } + + return qfalse; +} + +/* +=============== +G_ParseMapRotationFile + +Load the map rotations from a map rotation file +=============== +*/ +static qboolean G_ParseMapRotationFile( const char *fileName ) +{ + char *text_p; + int i, j, k; + int len; + char *token; + char text[ 20000 ]; + char mrName[ MAX_QPATH ]; + qboolean mrNameSet = qfalse; + fileHandle_t f; + + // load the file + len = trap_FS_FOpenFile( fileName, &f, FS_READ ); + if( len < 0 ) + return qfalse; + + if( len == 0 || len >= sizeof( text ) - 1 ) + { + trap_FS_FCloseFile( f ); + G_Printf( S_COLOR_RED "ERROR: map rotation file %s is %s\n", fileName, + len == 0 ? "empty" : "too long" ); + return qfalse; + } + + trap_FS_Read( text, len, f ); + text[ len ] = 0; + trap_FS_FCloseFile( f ); + + // parse the text + text_p = text; + + // read optional parameters + while( 1 ) + { + token = COM_Parse( &text_p ); + + if( !token ) + break; + + if( !Q_stricmp( token, "" ) ) + break; + + if( !Q_stricmp( token, "{" ) ) + { + if( mrNameSet ) + { + //check for name space clashes + for( i = 0; i < mapRotations.numRotations; i++ ) + { + if( !Q_stricmp( mapRotations.rotations[ i ].name, mrName ) ) + { + G_Printf( S_COLOR_RED "ERROR: a map rotation is already named %s\n", mrName ); + return qfalse; + } + } + + Q_strncpyz( mapRotations.rotations[ mapRotations.numRotations ].name, mrName, MAX_QPATH ); + + if( !G_ParseMapRotation( &mapRotations.rotations[ mapRotations.numRotations ], &text_p ) ) + { + G_Printf( S_COLOR_RED "ERROR: %s: failed to parse map rotation %s\n", fileName, mrName ); + return qfalse; + } + + //start parsing map rotations again + mrNameSet = qfalse; + + if( mapRotations.numRotations == MAX_MAP_ROTATIONS ) + { + G_Printf( S_COLOR_RED "ERROR: maximum number of map rotations (%d) reached\n", + MAX_MAP_ROTATIONS ); + return qfalse; + } + else + mapRotations.numRotations++; + + continue; + } + else + { + G_Printf( S_COLOR_RED "ERROR: unamed map rotation\n" ); + return qfalse; + } + } + + if( !mrNameSet ) + { + Q_strncpyz( mrName, token, sizeof( mrName ) ); + mrNameSet = qtrue; + } + else + { + G_Printf( S_COLOR_RED "ERROR: map rotation already named\n" ); + return qfalse; + } + } + + for( i = 0; i < mapRotations.numRotations; i++ ) + { + for( j = 0; j < mapRotations.rotations[ i ].numMaps; j++ ) + { + if( Q_stricmp( mapRotations.rotations[ i ].maps[ j ].name, "*VOTE*") != 0 && + Q_stricmp( mapRotations.rotations[ i ].maps[ j ].name, "*RANDOM*") != 0 && + !G_MapExists( mapRotations.rotations[ i ].maps[ j ].name ) ) + { + G_Printf( S_COLOR_RED "ERROR: map \"%s\" doesn't exist\n", + mapRotations.rotations[ i ].maps[ j ].name ); + return qfalse; + } + + for( k = 0; k < mapRotations.rotations[ i ].maps[ j ].numConditions; k++ ) + { + if( !G_MapExists( mapRotations.rotations[ i ].maps[ j ].conditions[ k ].dest ) && + !G_RotationExists( mapRotations.rotations[ i ].maps[ j ].conditions[ k ].dest ) ) + { + G_Printf( S_COLOR_RED "ERROR: conditional destination \"%s\" doesn't exist\n", + mapRotations.rotations[ i ].maps[ j ].conditions[ k ].dest ); + return qfalse; + } + + } + + } + } + + return qtrue; +} + +/* +=============== +G_PrintRotations + +Print the parsed map rotations +=============== +*/ +void G_PrintRotations( void ) +{ + int i, j, k; + + G_Printf( "Map rotations as parsed:\n\n" ); + + for( i = 0; i < mapRotations.numRotations; i++ ) + { + G_Printf( "rotation: %s\n{\n", mapRotations.rotations[ i ].name ); + + for( j = 0; j < mapRotations.rotations[ i ].numMaps; j++ ) + { + G_Printf( " map: %s\n {\n", mapRotations.rotations[ i ].maps[ j ].name ); + + for( k = 0; k < mapRotations.rotations[ i ].maps[ j ].numCmds; k++ ) + { + G_Printf( " command: %s\n", + mapRotations.rotations[ i ].maps[ j ].postCmds[ k ] ); + } + + G_Printf( " }\n" ); + + for( k = 0; k < mapRotations.rotations[ i ].maps[ j ].numConditions; k++ ) + { + G_Printf( " conditional: %s\n", + mapRotations.rotations[ i ].maps[ j ].conditions[ k ].dest ); + } + + } + + G_Printf( "}\n" ); + } + + G_Printf( "Total memory used: %d bytes\n", sizeof( mapRotations ) ); +} + +/* +=============== +G_GetCurrentMapArray + +Fill a static array with the current map of each rotation +=============== +*/ +static int *G_GetCurrentMapArray( void ) +{ + static int currentMap[ MAX_MAP_ROTATIONS ]; + int i = 0; + char text[ MAX_MAP_ROTATIONS * 2 ]; + char *text_p, *token; + + Q_strncpyz( text, g_currentMap.string, sizeof( text ) ); + + text_p = text; + + while( 1 ) + { + token = COM_Parse( &text_p ); + + if( !token ) + break; + + if( !Q_stricmp( token, "" ) ) + break; + + currentMap[ i++ ] = atoi( token ); + } + + return currentMap; +} + +/* +=============== +G_SetCurrentMap + +Set the current map in some rotation +=============== +*/ +static void G_SetCurrentMap( int currentMap, int rotation ) +{ + char text[ MAX_MAP_ROTATIONS * 2 ] = { 0 }; + int *p = G_GetCurrentMapArray( ); + int i; + + p[ rotation ] = currentMap; + + for( i = 0; i < mapRotations.numRotations; i++ ) + Q_strcat( text, sizeof( text ), va( "%d ", p[ i ] ) ); + + trap_Cvar_Set( "g_currentMap", text ); + trap_Cvar_Update( &g_currentMap ); +} + +/* +=============== +G_GetCurrentMap + +Return the current map in some rotation +=============== +*/ +int G_GetCurrentMap( int rotation ) +{ + int *p = G_GetCurrentMapArray( ); + + return p[ rotation ]; +} + +/* +=============== +G_IssueMapChange + +Send commands to the server to actually change the map +=============== +*/ +static void G_IssueMapChange( int rotation ) +{ + int i; + int map = G_GetCurrentMap( rotation ); + char cmd[ MAX_TOKEN_CHARS ]; + char newmap[ MAX_CVAR_VALUE_STRING ]; + + Q_strncpyz( newmap, mapRotations.rotations[rotation].maps[map].name, sizeof( newmap )); + + if (!Q_stricmp( newmap, "*VOTE*") ) + { + fileHandle_t f; + + G_GetVotedMap( newmap, sizeof( newmap ), rotation, map ); + if( trap_FS_FOpenFile( va("maps/%s.bsp", newmap), &f, FS_READ ) > 0 ) + { + trap_FS_FCloseFile( f ); + } + else + { + G_AdvanceMapRotation(); + return; + } + } + else if(!Q_stricmp( newmap, "*RANDOM*") ) + { + fileHandle_t f; + + G_GetRandomMap( newmap, sizeof( newmap ), rotation, map ); + if( trap_FS_FOpenFile( va("maps/%s.bsp", newmap), &f, FS_READ ) > 0 ) + { + trap_FS_FCloseFile( f ); + } + else + { + G_AdvanceMapRotation(); + return; + } + } + + // allow a manually defined g_layouts setting to override the maprotation + if( !g_layouts.string[ 0 ] && + mapRotations.rotations[ rotation ].maps[ map ].layouts[ 0 ] ) + { + trap_Cvar_Set( "g_layouts", + mapRotations.rotations[ rotation ].maps[ map ].layouts ); + } + + trap_SendConsoleCommand( EXEC_APPEND, va( "map %s\n", + newmap ) ); + + // load up map defaults if g_mapConfigs is set + G_MapConfigs( newmap ); + + for( i = 0; i < mapRotations.rotations[ rotation ].maps[ map ].numCmds; i++ ) + { + Q_strncpyz( cmd, mapRotations.rotations[ rotation ].maps[ map ].postCmds[ i ], + sizeof( cmd ) ); + Q_strcat( cmd, sizeof( cmd ), "\n" ); + trap_SendConsoleCommand( EXEC_APPEND, cmd ); + } +} + +/* +=============== +G_ResolveConditionDestination + +Resolve the destination of some condition +=============== +*/ +static mapConditionType_t G_ResolveConditionDestination( int *n, char *name ) +{ + int i; + + //search the current rotation first... + for( i = 0; i < mapRotations.rotations[ g_currentMapRotation.integer ].numMaps; i++ ) + { + if( !Q_stricmp( mapRotations.rotations[ g_currentMapRotation.integer ].maps[ i ].name, name ) ) + { + *n = i; + return MCT_MAP; + } + } + + //...then search the rotation names + for( i = 0; i < mapRotations.numRotations; i++ ) + { + if( !Q_stricmp( mapRotations.rotations[ i ].name, name ) ) + { + *n = i; + return MCT_ROTATION; + } + } + + return MCT_ERR; +} + +/* +=============== +G_EvaluateMapCondition + +Evaluate a map condition +=============== +*/ +static qboolean G_EvaluateMapCondition( mapRotationCondition_t *mrc ) +{ + switch( mrc->lhs ) + { + case MCV_RANDOM: + return rand( ) & 1; + break; + + case MCV_NUMCLIENTS: + switch( mrc->op ) + { + case MCO_LT: + return level.numConnectedClients < mrc->numClients; + break; + + case MCO_GT: + return level.numConnectedClients > mrc->numClients; + break; + + case MCO_EQ: + return level.numConnectedClients == mrc->numClients; + break; + } + break; + + case MCV_LASTWIN: + return level.lastWin == mrc->lastWin; + break; + + case MCV_VOTE: + // ignore vote for conditions; + break; + case MCV_SELECTEDRANDOM: + // ignore vote for conditions; + break; + + default: + case MCV_ERR: + G_Printf( S_COLOR_RED "ERROR: malformed map switch condition\n" ); + break; + } + + return qfalse; +} + +/* +=============== +G_AdvanceMapRotation + +Increment the current map rotation +=============== +*/ +qboolean G_AdvanceMapRotation( void ) +{ + mapRotation_t *mr; + mapRotationEntry_t *mre; + mapRotationCondition_t *mrc; + int currentRotation, currentMap, nextMap; + int i, n; + mapConditionType_t mct; + + if( ( currentRotation = g_currentMapRotation.integer ) == NOT_ROTATING ) + return qfalse; + + currentMap = G_GetCurrentMap( currentRotation ); + + mr = &mapRotations.rotations[ currentRotation ]; + mre = &mr->maps[ currentMap ]; + nextMap = ( currentMap + 1 ) % mr->numMaps; + + for( i = 0; i < mre->numConditions; i++ ) + { + mrc = &mre->conditions[ i ]; + + if( mrc->unconditional || G_EvaluateMapCondition( mrc ) ) + { + mct = G_ResolveConditionDestination( &n, mrc->dest ); + + switch( mct ) + { + case MCT_MAP: + nextMap = n; + break; + + case MCT_ROTATION: + //need to increment the current map before changing the rotation + //or you get infinite loops with some conditionals + G_SetCurrentMap( nextMap, currentRotation ); + G_StartMapRotation( mrc->dest, qtrue ); + return qtrue; + break; + + default: + case MCT_ERR: + G_Printf( S_COLOR_YELLOW "WARNING: map switch destination could not be resolved: %s\n", + mrc->dest ); + break; + } + } + } + + G_SetCurrentMap( nextMap, currentRotation ); + G_IssueMapChange( currentRotation ); + + return qtrue; +} + +/* +=============== +G_StartMapRotation + +Switch to a new map rotation +=============== +*/ +qboolean G_StartMapRotation( char *name, qboolean changeMap ) +{ + int i; + + for( i = 0; i < mapRotations.numRotations; i++ ) + { + if( !Q_stricmp( mapRotations.rotations[ i ].name, name ) ) + { + trap_Cvar_Set( "g_currentMapRotation", va( "%d", i ) ); + trap_Cvar_Update( &g_currentMapRotation ); + + if( changeMap ) + G_IssueMapChange( i ); + break; + } + } + + if( i == mapRotations.numRotations ) + return qfalse; + else + return qtrue; +} + +/* +=============== +G_StopMapRotation + +Stop the current map rotation +=============== +*/ +void G_StopMapRotation( void ) +{ + trap_Cvar_Set( "g_currentMapRotation", va( "%d", NOT_ROTATING ) ); + trap_Cvar_Update( &g_currentMapRotation ); +} + +/* +=============== +G_MapRotationActive + +Test if any map rotation is currently active +=============== +*/ +qboolean G_MapRotationActive( void ) +{ + return ( g_currentMapRotation.integer != NOT_ROTATING ); +} + +/* +=============== +G_InitMapRotations + +Load and intialise the map rotations +=============== +*/ +void G_InitMapRotations( void ) +{ + const char *fileName = "maprotation.cfg"; + + //load the file if it exists + if( trap_FS_FOpenFile( fileName, NULL, FS_READ ) ) + { + if( !G_ParseMapRotationFile( fileName ) ) + G_Printf( S_COLOR_RED "ERROR: failed to parse %s file\n", fileName ); + } + else + G_Printf( "%s file not found.\n", fileName ); + + if( g_currentMapRotation.integer == NOT_ROTATING ) + { + if( g_initialMapRotation.string[ 0 ] != 0 ) + { + G_StartMapRotation( g_initialMapRotation.string, qfalse ); + + trap_Cvar_Set( "g_initialMapRotation", "" ); + trap_Cvar_Update( &g_initialMapRotation ); + } + } +} + +static char rotationVoteList[ MAX_MAP_ROTATION_CONDS ][ MAX_QPATH ]; +static int rotationVoteLen = 0; + +static int rotationVoteClientPosition[ MAX_CLIENTS ]; +static int rotationVoteClientSelection[ MAX_CLIENTS ]; + +/* +=============== +G_CheckMapRotationVote +=============== +*/ +qboolean G_CheckMapRotationVote( void ) +{ + mapRotation_t *mr; + mapRotationEntry_t *mre; + mapRotationCondition_t *mrc; + int currentRotation, currentMap, nextMap; + int i; + + rotationVoteLen = 0; + + if( g_mapRotationVote.integer < 1 ) + return qfalse; + + if( ( currentRotation = g_currentMapRotation.integer ) == NOT_ROTATING ) + return qfalse; + + currentMap = G_GetCurrentMap( currentRotation ); + + mr = &mapRotations.rotations[ currentRotation ]; + nextMap = ( currentMap + 1 ) % mr->numMaps; + mre = &mr->maps[ nextMap ]; + + for( i = 0; i < mre->numConditions; i++ ) + { + mrc = &mre->conditions[ i ]; + + if( mrc->lhs == MCV_VOTE ) + { + Q_strncpyz( rotationVoteList[ rotationVoteLen ], mrc->dest, + sizeof( rotationVoteList[ rotationVoteLen ] ) ); + rotationVoteLen++; + if( rotationVoteLen >= MAX_MAP_ROTATION_CONDS ) + break; + } + } + + if( !rotationVoteLen ) + return qfalse; + + for( i = 0; i < MAX_CLIENTS; i++ ) + { + rotationVoteClientPosition[ i ] = 0; + rotationVoteClientSelection[ i ] = -1; + } + + return qtrue; +} + +typedef struct { + int votes; + int map; +} MapVoteResultsSort_t; + +static int SortMapVoteResults( const void *av, const void *bv ) +{ + const MapVoteResultsSort_t *a = av; + const MapVoteResultsSort_t *b = bv; + + if( a->votes > b->votes ) + return -1; + if( a->votes < b->votes ) + return 1; + + if( a->map > b->map ) + return 1; + if( a->map < b->map ) + return -1; + + return 0; +} + +static int G_GetMapVoteWinner( int *winvotes, int *totalvotes, int *resultorder ) +{ + MapVoteResultsSort_t results[ MAX_MAP_ROTATION_CONDS ]; + int tv = 0; + int i, n; + + for( i = 0; i < MAX_MAP_ROTATION_CONDS; i++ ) + { + results[ i ].votes = 0; + results[ i ].map = i; + } + for( i = 0; i < MAX_CLIENTS; i++ ) + { + n = rotationVoteClientSelection[ i ]; + if( n >=0 && n < MAX_MAP_ROTATION_CONDS ) + { + results[ n ].votes += 1; + tv++; + } + } + + qsort ( results, MAX_MAP_ROTATION_CONDS, sizeof( results[ 0 ] ), SortMapVoteResults ); + + if( winvotes != NULL ) + *winvotes = results[ 0 ].votes; + if( totalvotes != NULL ) + *totalvotes = tv; + + if( resultorder != NULL ) + { + for( i = 0; i < MAX_MAP_ROTATION_CONDS; i++ ) + resultorder[ results[ i ].map ] = i; + } + + return results[ 0 ].map; +} + +qboolean G_IntermissionMapVoteWinner( void ) +{ + int winner, winvotes, totalvotes; + int nonvotes; + + winner = G_GetMapVoteWinner( &winvotes, &totalvotes, NULL ); + if( winvotes * 2 > level.numConnectedClients ) + return qtrue; + nonvotes = level.numConnectedClients - totalvotes; + if( nonvotes < 0 ) + nonvotes = 0; + if( winvotes > nonvotes + ( totalvotes - winvotes ) ) + return qtrue; + + return qfalse; +} + +static qboolean G_GetVotedMap( char *name, int size, int rotation, int map ) +{ + mapRotation_t *mr; + mapRotationEntry_t *mre; + mapRotationCondition_t *mrc; + int i, n; + int winner; + qboolean found = qfalse; + + if( !rotationVoteLen ) + return qfalse; + + winner = G_GetMapVoteWinner( NULL, NULL, NULL ); + + mr = &mapRotations.rotations[ rotation ]; + mre = &mr->maps[ map ]; + + n = 0; + for( i = 0; i < mre->numConditions && n < rotationVoteLen; i++ ) + { + mrc = &mre->conditions[ i ]; + + if( mrc->lhs == MCV_VOTE ) + { + if( n == winner ) + { + Q_strncpyz( name, mrc->dest, size ); + found = qtrue; + break; + } + n++; + } + } + + rotationVoteLen = 0; + + return found; +} + +static void G_IntermissionMapVoteMessageReal( gentity_t *ent, int winner, int winvotes, int totalvotes, int *ranklist ) +{ + int clientNum; + char string[ MAX_STRING_CHARS ]; + char entry[ MAX_STRING_CHARS ]; + int ourlist[ MAX_MAP_ROTATION_CONDS ]; + int len = 0; + int index, selection; + int i; + char *color; + char *rank; + + clientNum = ent-g_entities; + + index = rotationVoteClientSelection[ clientNum ]; + selection = rotationVoteClientPosition[ clientNum ]; + + if( winner < 0 || winner >= MAX_MAP_ROTATION_CONDS || ranklist == NULL ) + { + ranklist = &ourlist[0]; + winner = G_GetMapVoteWinner( &winvotes, &totalvotes, ranklist ); + } + + Q_strncpyz( string, "^7Attack = down ^0/^7 Repair = up ^0/^7 F1 = vote\n\n" + "^2Map Vote Menu\n" + "^7+------------------+\n", sizeof( string ) ); + for( i = 0; i < rotationVoteLen; i++ ) + { + if( !G_MapExists( rotationVoteList[ i ] ) ) + continue; + + if( i == selection ) + color = "^5"; + else if( i == index ) + color = "^1"; + else + color = "^7"; + + switch( ranklist[ i ] ) + { + case 0: + rank = "^7---"; + break; + case 1: + rank = "^7--"; + break; + case 2: + rank = "^7-"; + break; + default: + rank = ""; + break; + } + + Com_sprintf( entry, sizeof( entry ), "^7%s%s%s%s %s %s%s^7%s\n", + ( i == index ) ? "^1>>>" : "", + ( i == selection ) ? "^7(" : " ", + rank, + color, + rotationVoteList[ i ], + rank, + ( i == selection ) ? "^7)" : " ", + ( i == index ) ? "^1<<<" : "" ); + + Q_strcat( string, sizeof( string ), entry ); + len += strlen( entry ); + } + + Com_sprintf( entry, sizeof( entry ), + "\n^7+----------------+\nleader: ^3%s^7 with %d vote%s\nvoters: %d\ntime left: %d", + rotationVoteList[ winner ], + winvotes, + ( winvotes == 1 ) ? "" : "s", + totalvotes, + ( level.mapRotationVoteTime - level.time ) / 1000 ); + Q_strcat( string, sizeof( string ), entry ); + + trap_SendServerCommand( ent-g_entities, va( "cp \"%s\"\n", string ) ); +} + +void G_IntermissionMapVoteMessageAll( void ) +{ + int ranklist[ MAX_MAP_ROTATION_CONDS ]; + int winner; + int winvotes, totalvotes; + int i; + + winner = G_GetMapVoteWinner( &winvotes, &totalvotes, &ranklist[ 0 ] ); + for( i = 0; i < level.maxclients; i++ ) + { + if( level.clients[ i ].pers.connected == CON_CONNECTED ) + G_IntermissionMapVoteMessageReal( g_entities + i, winner, winvotes, totalvotes, ranklist ); + } +} + +void G_IntermissionMapVoteMessage( gentity_t *ent ) +{ + G_IntermissionMapVoteMessageReal( ent, -1, 0, 0, NULL ); +} + +void G_IntermissionMapVoteCommand( gentity_t *ent, qboolean next, qboolean choose ) +{ + int clientNum; + int n; + + clientNum = ent-g_entities; + + if( choose ) + { + rotationVoteClientSelection[ clientNum ] = rotationVoteClientPosition[ clientNum ]; + } + else + { + n = rotationVoteClientPosition[ clientNum ]; + if( next ) + n++; + else + n--; + + if( n >= rotationVoteLen ) + n = rotationVoteLen - 1; + if( n < 0 ) + n = 0; + + rotationVoteClientPosition[ clientNum ] = n; + } + + G_IntermissionMapVoteMessage( ent ); +} + +static qboolean G_GetRandomMap( char *name, int size, int rotation, int map ) +{ + mapRotation_t *mr; + mapRotationEntry_t *mre; + mapRotationCondition_t *mrc; + int i, nummaps; + int randompick = 0; + int maplist[ 32 ]; + + mr = &mapRotations.rotations[ rotation ]; + mre = &mr->maps[ map ]; + + nummaps = 0; + //count the number of map votes + for( i = 0; i < mre->numConditions; i++ ) + { + mrc = &mre->conditions[ i ]; + + if( mrc->lhs == MCV_SELECTEDRANDOM ) + { + //map doesnt exist + if( !G_MapExists( mrc->dest ) ) { + continue; + } + maplist[ nummaps ] = i; + nummaps++; + } + } + + if( nummaps == 0 ) { + return qfalse; + } + + randompick = (int)( random() * nummaps ); + + Q_strncpyz( name, mre->conditions[ maplist[ randompick ] ].dest, size ); + + return qtrue; +} diff --git a/src/game/g_mem.c b/src/game/g_mem.c new file mode 100644 index 0000000..6935194 --- /dev/null +++ b/src/game/g_mem.c @@ -0,0 +1,216 @@ +/* +=========================================================================== +Copyright (C) 1999-2005 Id Software, Inc. +Copyright (C) 2000-2006 Tim Angus + +This file is part of Tremulous. + +Tremulous is free software; you can redistribute it +and/or modify it under the terms of the GNU General Public License as +published by the Free Software Foundation; either version 2 of the License, +or (at your option) any later version. + +Tremulous is distributed in the hope that it will be +useful, but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with Tremulous; if not, write to the Free Software +Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA +=========================================================================== +*/ + +#include "g_local.h" + +#define POOLSIZE ( 1024 * 1024 ) +#define FREEMEMCOOKIE ((int)0xDEADBE3F) // Any unlikely to be used value +#define ROUNDBITS 31 // Round to 32 bytes + +struct freememnode +{ + // Size of ROUNDBITS + int cookie, size; // Size includes node (obviously) + struct freememnode *prev, *next; +}; + +static char memoryPool[POOLSIZE]; +static struct freememnode *freehead; +static int freemem; + +void *G_Alloc( int size ) +{ + // Find a free block and allocate. + // Does two passes, attempts to fill same-sized free slot first. + + struct freememnode *fmn, *prev, *next, *smallest; + int allocsize, smallestsize; + char *endptr; + int *ptr; + + allocsize = ( size + sizeof(int) + ROUNDBITS ) & ~ROUNDBITS; // Round to 32-byte boundary + ptr = NULL; + + smallest = NULL; + smallestsize = POOLSIZE + 1; // Guaranteed not to miss any slots :) + for( fmn = freehead; fmn; fmn = fmn->next ) + { + if( fmn->cookie != FREEMEMCOOKIE ) + G_Error( "G_Alloc: Memory corruption detected!\n" ); + + if( fmn->size >= allocsize ) + { + // We've got a block + if( fmn->size == allocsize ) + { + // Same size, just remove + + prev = fmn->prev; + next = fmn->next; + if( prev ) + prev->next = next; // Point previous node to next + if( next ) + next->prev = prev; // Point next node to previous + if( fmn == freehead ) + freehead = next; // Set head pointer to next + ptr = (int *) fmn; + break; // Stop the loop, this is fine + } + else + { + // Keep track of the smallest free slot + if( fmn->size < smallestsize ) + { + smallest = fmn; + smallestsize = fmn->size; + } + } + } + } + + if( !ptr && smallest ) + { + // We found a slot big enough + smallest->size -= allocsize; + endptr = (char *) smallest + smallest->size; + ptr = (int *) endptr; + } + + if( ptr ) + { + freemem -= allocsize; + if( g_debugAlloc.integer ) + G_Printf( "G_Alloc of %i bytes (%i left)\n", allocsize, freemem ); + memset( ptr, 0, allocsize ); + *ptr++ = allocsize; // Store a copy of size for deallocation + return( (void *) ptr ); + } + + G_Error( "G_Alloc: failed on allocation of %i bytes\n", size ); + return( NULL ); +} + +void G_Free( void *ptr ) +{ + // Release allocated memory, add it to the free list. + + struct freememnode *fmn; + char *freeend; + int *freeptr; + + freeptr = ptr; + freeptr--; + + freemem += *freeptr; + if( g_debugAlloc.integer ) + G_Printf( "G_Free of %i bytes (%i left)\n", *freeptr, freemem ); + + for( fmn = freehead; fmn; fmn = fmn->next ) + { + freeend = ((char *) fmn) + fmn->size; + if( freeend == (char *) freeptr ) + { + // Released block can be merged to an existing node + + fmn->size += *freeptr; // Add size of node. + return; + } + } + // No merging, add to head of list + + fmn = (struct freememnode *) freeptr; + fmn->size = *freeptr; // Set this first to avoid corrupting *freeptr + fmn->cookie = FREEMEMCOOKIE; + fmn->prev = NULL; + fmn->next = freehead; + freehead->prev = fmn; + freehead = fmn; +} + +void G_InitMemory( void ) +{ + // Set up the initial node + + freehead = (struct freememnode *)memoryPool; + freehead->cookie = FREEMEMCOOKIE; + freehead->size = POOLSIZE; + freehead->next = NULL; + freehead->prev = NULL; + freemem = sizeof( memoryPool ); +} + +void G_DefragmentMemory( void ) +{ + // If there's a frenzy of deallocation and we want to + // allocate something big, this is useful. Otherwise... + // not much use. + + struct freememnode *startfmn, *endfmn, *fmn; + + for( startfmn = freehead; startfmn; ) + { + endfmn = (struct freememnode *)(((char *) startfmn) + startfmn->size); + for( fmn = freehead; fmn; ) + { + if( fmn->cookie != FREEMEMCOOKIE ) + G_Error( "G_DefragmentMemory: Memory corruption detected!\n" ); + + if( fmn == endfmn ) + { + // We can add fmn onto startfmn. + + if( fmn->prev ) + fmn->prev->next = fmn->next; + if( fmn->next ) + { + if( !(fmn->next->prev = fmn->prev) ) + freehead = fmn->next; // We're removing the head node + } + startfmn->size += fmn->size; + memset( fmn, 0, sizeof(struct freememnode) ); // A redundant call, really. + + startfmn = freehead; + endfmn = fmn = NULL; // Break out of current loop + } + else + fmn = fmn->next; + } + + if( endfmn ) + startfmn = startfmn->next; // endfmn acts as a 'restart' flag here + } +} + +void Svcmd_GameMem_f( void ) +{ + // Give a breakdown of memory + + struct freememnode *fmn; + + G_Printf( "Game memory status: %i out of %i bytes allocated\n", POOLSIZE - freemem, POOLSIZE ); + + for( fmn = freehead; fmn; fmn = fmn->next ) + G_Printf( " %dd: %d bytes free.\n", fmn, fmn->size ); + G_Printf( "Status complete.\n" ); +} + diff --git a/src/game/g_misc.c b/src/game/g_misc.c new file mode 100644 index 0000000..b0d6077 --- /dev/null +++ b/src/game/g_misc.c @@ -0,0 +1,436 @@ +/* +=========================================================================== +Copyright (C) 1999-2005 Id Software, Inc. +Copyright (C) 2000-2006 Tim Angus + +This file is part of Tremulous. + +Tremulous is free software; you can redistribute it +and/or modify it under the terms of the GNU General Public License as +published by the Free Software Foundation; either version 2 of the License, +or (at your option) any later version. + +Tremulous is distributed in the hope that it will be +useful, but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with Tremulous; if not, write to the Free Software +Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA +=========================================================================== +*/ + +#include "g_local.h" + + +/*QUAKED func_group (0 0 0) ? +Used to group brushes together just for editor convenience. They are turned into normal brushes by the utilities. +*/ + + +/*QUAKED info_null (0 0.5 0) (-4 -4 -4) (4 4 4) +Used as a positional target for calculations in the utilities (spotlights, etc), but removed during gameplay. +*/ +void SP_info_null( gentity_t *self ) +{ + G_FreeEntity( self ); +} + + +/*QUAKED info_notnull (0 0.5 0) (-4 -4 -4) (4 4 4) +Used as a positional target for in-game calculation, like jumppad targets. +target_position does the same thing +*/ +void SP_info_notnull( gentity_t *self ) +{ + G_SetOrigin( self, self->s.origin ); +} + + +/*QUAKED light (0 1 0) (-8 -8 -8) (8 8 8) linear +Non-displayed light. +"light" overrides the default 300 intensity. +Linear checbox gives linear falloff instead of inverse square +Lights pointed at a target will be spotlights. +"radius" overrides the default 64 unit radius of a spotlight at the target point. +*/ +void SP_light( gentity_t *self ) +{ + G_FreeEntity( self ); +} + + + +/* +================================================================================= + +TELEPORTERS + +================================================================================= +*/ + +void TeleportPlayer( gentity_t *player, vec3_t origin, vec3_t angles ) +{ + // unlink to make sure it can't possibly interfere with G_KillBox + trap_UnlinkEntity( player ); + + VectorCopy( origin, player->client->ps.origin ); + player->client->ps.origin[ 2 ] += 1; + + // spit the player out + AngleVectors( angles, player->client->ps.velocity, NULL, NULL ); + VectorScale( player->client->ps.velocity, 400, player->client->ps.velocity ); + player->client->ps.pm_time = 160; // hold time + player->client->ps.pm_flags |= PMF_TIME_KNOCKBACK; + + // toggle the teleport bit so the client knows to not lerp + player->client->ps.eFlags ^= EF_TELEPORT_BIT; + G_UnlaggedClear( player ); + + // set angles + G_SetClientViewAngle( player, angles ); + + // kill anything at the destination + if( player->client->sess.sessionTeam != TEAM_SPECTATOR ) + G_KillBox( player ); + + // save results of pmove + BG_PlayerStateToEntityState( &player->client->ps, &player->s, qtrue ); + + // use the precise origin for linking + VectorCopy( player->client->ps.origin, player->r.currentOrigin ); + + if( player->client->sess.sessionTeam != TEAM_SPECTATOR ) + trap_LinkEntity (player); +} + + +/*QUAKED misc_teleporter_dest (1 0 0) (-32 -32 -24) (32 32 -16) +Point teleporters at these. +Now that we don't have teleport destination pads, this is just +an info_notnull +*/ +void SP_misc_teleporter_dest( gentity_t *ent ) +{ +} + + +//=========================================================== + +/*QUAKED misc_model (1 0 0) (-16 -16 -16) (16 16 16) +"model" arbitrary .md3 file to display +*/ +void SP_misc_model( gentity_t *ent ) +{ +#if 0 + ent->s.modelindex = G_ModelIndex( ent->model ); + VectorSet (ent->mins, -16, -16, -16); + VectorSet (ent->maxs, 16, 16, 16); + trap_LinkEntity (ent); + + G_SetOrigin( ent, ent->s.origin ); + VectorCopy( ent->s.angles, ent->s.apos.trBase ); +#else + G_FreeEntity( ent ); +#endif +} + +//=========================================================== + +void locateCamera( gentity_t *ent ) +{ + vec3_t dir; + gentity_t *target; + gentity_t *owner; + + owner = G_PickTarget( ent->target ); + if( !owner ) + { + G_Printf( "Couldn't find target for misc_portal_surface\n" ); + G_FreeEntity( ent ); + return; + } + ent->r.ownerNum = owner->s.number; + + // frame holds the rotate speed + if( owner->spawnflags & 1 ) + ent->s.frame = 25; + else if( owner->spawnflags & 2 ) + ent->s.frame = 75; + + // swing camera ? + if( owner->spawnflags & 4 ) + { + // set to 0 for no rotation at all + ent->s.powerups = 0; + } + else + ent->s.powerups = 1; + + // clientNum holds the rotate offset + ent->s.clientNum = owner->s.clientNum; + + VectorCopy( owner->s.origin, ent->s.origin2 ); + + // see if the portal_camera has a target + target = G_PickTarget( owner->target ); + if( target ) + { + VectorSubtract( target->s.origin, owner->s.origin, dir ); + VectorNormalize( dir ); + } + else + G_SetMovedir( owner->s.angles, dir ); + + ent->s.eventParm = DirToByte( dir ); +} + +/*QUAKED misc_portal_surface (0 0 1) (-8 -8 -8) (8 8 8) +The portal surface nearest this entity will show a view from the targeted misc_portal_camera, or a mirror view if untargeted. +This must be within 64 world units of the surface! +*/ +void SP_misc_portal_surface( gentity_t *ent ) +{ + VectorClear( ent->r.mins ); + VectorClear( ent->r.maxs ); + trap_LinkEntity( ent ); + + ent->r.svFlags = SVF_PORTAL; + ent->s.eType = ET_PORTAL; + + if( !ent->target ) + { + VectorCopy( ent->s.origin, ent->s.origin2 ); + } + else + { + ent->think = locateCamera; + ent->nextthink = level.time + 100; + } +} + +/*QUAKED misc_portal_camera (0 0 1) (-8 -8 -8) (8 8 8) slowrotate fastrotate noswing + +The target for a misc_portal_director. You can set either angles or target another entity to determine the direction of view. +"roll" an angle modifier to orient the camera around the target vector; +*/ +void SP_misc_portal_camera( gentity_t *ent ) +{ + float roll; + + VectorClear( ent->r.mins ); + VectorClear( ent->r.maxs ); + trap_LinkEntity( ent ); + + G_SpawnFloat( "roll", "0", &roll ); + + ent->s.clientNum = roll / 360.0f * 256; +} + +/* +====================================================================== + + NEAT EFFECTS AND STUFF FOR TREMULOUS + +====================================================================== +*/ + +void SP_toggle_particle_system( gentity_t *self ) +{ + //toggle EF_NODRAW + self->s.eFlags ^= EF_NODRAW; + + self->nextthink = 0; +} + +/* +=============== +SP_use_particle_system + +Use function for particle_system +=============== +*/ +void SP_use_particle_system( gentity_t *self, gentity_t *other, gentity_t *activator ) +{ + SP_toggle_particle_system( self ); + + if( self->wait > 0.0f ) + { + self->think = SP_toggle_particle_system; + self->nextthink = level.time + (int)( self->wait * 1000 ); + } +} + +/* +=============== +SP_spawn_particle_system + +Spawn function for particle system +=============== +*/ +void SP_misc_particle_system( gentity_t *self ) +{ + char *s; + + G_SetOrigin( self, self->s.origin ); + + G_SpawnString( "psName", "", &s ); + G_SpawnFloat( "wait", "0", &self->wait ); + + //add the particle system to the client precache list + self->s.modelindex = G_ParticleSystemIndex( s ); + + if( self->spawnflags & 1 ) + self->s.eFlags |= EF_NODRAW; + + self->use = SP_use_particle_system; + self->s.eType = ET_PARTICLE_SYSTEM; + trap_LinkEntity( self ); +} + +/* +=============== +SP_use_anim_model + +Use function for anim model +=============== +*/ +void SP_use_anim_model( gentity_t *self, gentity_t *other, gentity_t *activator ) +{ + if( self->spawnflags & 1 ) + { + //if spawnflag 1 is set + //toggle EF_NODRAW + if( self->s.eFlags & EF_NODRAW ) + self->s.eFlags &= ~EF_NODRAW; + else + self->s.eFlags |= EF_NODRAW; + } + else + { + //if the animation loops then toggle the animation + //toggle EF_MOVER_STOP + if( self->s.eFlags & EF_MOVER_STOP ) + self->s.eFlags &= ~EF_MOVER_STOP; + else + self->s.eFlags |= EF_MOVER_STOP; + } +} + +/* +=============== +SP_misc_anim_model + +Spawn function for anim model +=============== +*/ +void SP_misc_anim_model( gentity_t *self ) +{ + self->s.powerups = (int)self->animation[ 0 ]; + self->s.weapon = (int)self->animation[ 1 ]; + self->s.torsoAnim = (int)self->animation[ 2 ]; + self->s.legsAnim = (int)self->animation[ 3 ]; + + self->s.angles2[ 0 ] = self->pos2[ 0 ]; + + //add the model to the client precache list + self->s.modelindex = G_ModelIndex( self->model ); + + self->use = SP_use_anim_model; + + self->s.eType = ET_ANIMMAPOBJ; + + // spawn with animation stopped + if( self->spawnflags & 2 ) + self->s.eFlags |= EF_MOVER_STOP; + + trap_LinkEntity( self ); +} + +/* +=============== +SP_use_light_flare + +Use function for light flare +=============== +*/ +void SP_use_light_flare( gentity_t *self, gentity_t *other, gentity_t *activator ) +{ + self->s.eFlags ^= EF_NODRAW; +} + +/* +=============== +findEmptySpot + +Finds an empty spot radius units from origin +============== +*/ +static void findEmptySpot( vec3_t origin, float radius, vec3_t spot ) +{ + int i, j, k; + vec3_t delta, test, total; + trace_t tr; + + VectorClear( total ); + + //54(!) traces to test for empty spots + for( i = -1; i <= 1; i++ ) + { + for( j = -1; j <= 1; j++ ) + { + for( k = -1; k <= 1; k++ ) + { + VectorSet( delta, ( i * radius ), + ( j * radius ), + ( k * radius ) ); + + VectorAdd( origin, delta, test ); + + trap_Trace( &tr, test, NULL, NULL, test, -1, MASK_SOLID ); + + if( !tr.allsolid ) + { + trap_Trace( &tr, test, NULL, NULL, origin, -1, MASK_SOLID ); + VectorScale( delta, tr.fraction, delta ); + VectorAdd( total, delta, total ); + } + } + } + } + + VectorNormalize( total ); + VectorScale( total, radius, total ); + VectorAdd( origin, total, spot ); +} + +/* +=============== +SP_misc_light_flare + +Spawn function for light flare +=============== +*/ +void SP_misc_light_flare( gentity_t *self ) +{ + self->s.eType = ET_LIGHTFLARE; + self->s.modelindex = G_ShaderIndex( self->targetShaderName ); + VectorCopy( self->pos2, self->s.origin2 ); + + //try to find a spot near to the flare which is empty. This + //is used to facilitate visibility testing + findEmptySpot( self->s.origin, 8.0f, self->s.angles2 ); + + self->use = SP_use_light_flare; + + G_SpawnFloat( "speed", "200", &self->speed ); + self->s.time = self->speed; + + G_SpawnInt( "mindist", "0", &self->s.generic1 ); + + if( self->spawnflags & 1 ) + self->s.eFlags |= EF_NODRAW; + + trap_LinkEntity( self ); +} diff --git a/src/game/g_missile.c b/src/game/g_missile.c new file mode 100644 index 0000000..6317636 --- /dev/null +++ b/src/game/g_missile.c @@ -0,0 +1,810 @@ +/* +=========================================================================== +Copyright (C) 1999-2005 Id Software, Inc. +Copyright (C) 2000-2006 Tim Angus + +This file is part of Tremulous. + +Tremulous is free software; you can redistribute it +and/or modify it under the terms of the GNU General Public License as +published by the Free Software Foundation; either version 2 of the License, +or (at your option) any later version. + +Tremulous is distributed in the hope that it will be +useful, but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with Tremulous; if not, write to the Free Software +Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA +=========================================================================== +*/ + +#include "g_local.h" + +#define MISSILE_PRESTEP_TIME 50 + +/* +================ +G_BounceMissile + +================ +*/ +void G_BounceMissile( gentity_t *ent, trace_t *trace ) +{ + vec3_t velocity; + float dot; + int hitTime; + + // reflect the velocity on the trace plane + hitTime = level.previousTime + ( level.time - level.previousTime ) * trace->fraction; + BG_EvaluateTrajectoryDelta( &ent->s.pos, hitTime, velocity ); + dot = DotProduct( velocity, trace->plane.normal ); + VectorMA( velocity, -2 * dot, trace->plane.normal, ent->s.pos.trDelta ); + + if( ent->s.eFlags & EF_BOUNCE_HALF ) + { + VectorScale( ent->s.pos.trDelta, 0.65, ent->s.pos.trDelta ); + // check for stop + if( trace->plane.normal[ 2 ] > 0.2 && VectorLength( ent->s.pos.trDelta ) < 40 ) + { + G_SetOrigin( ent, trace->endpos ); + return; + } + } + + VectorAdd( ent->r.currentOrigin, trace->plane.normal, ent->r.currentOrigin ); + VectorCopy( ent->r.currentOrigin, ent->s.pos.trBase ); + ent->s.pos.trTime = level.time; +} + + +/* +================ +G_ExplodeMissile + +Explode a missile without an impact +================ +*/ +void G_ExplodeMissile( gentity_t *ent ) +{ + vec3_t dir; + vec3_t origin; + + BG_EvaluateTrajectory( &ent->s.pos, level.time, origin ); + SnapVector( origin ); + G_SetOrigin( ent, origin ); + + // we don't have a valid direction, so just point straight up + dir[ 0 ] = dir[ 1 ] = 0; + dir[ 2 ] = 1; + + ent->s.eType = ET_GENERAL; + + //TA: tired... can't be fucked... hack + if( ent->s.weapon != WP_LOCKBLOB_LAUNCHER && + ent->s.weapon != WP_FLAMER ) + G_AddEvent( ent, EV_MISSILE_MISS, DirToByte( dir ) ); + + ent->freeAfterEvent = qtrue; + + // splash damage + if( ent->splashDamage ) + G_RadiusDamage( ent->r.currentOrigin, ent->parent, ent->splashDamage, + ent->splashRadius, ent, ent->splashMethodOfDeath ); + + trap_LinkEntity( ent ); +} + +void AHive_ReturnToHive( gentity_t *self ); + +/* +================ +G_MissileImpact + +================ +*/ +void G_MissileImpact( gentity_t *ent, trace_t *trace ) +{ + gentity_t *other, *attacker; + qboolean returnAfterDamage = qfalse; + vec3_t dir; + + other = &g_entities[ trace->entityNum ]; + attacker = &g_entities[ ent->r.ownerNum ]; + + // check for bounce + if( !other->takedamage && + ( ent->s.eFlags & ( EF_BOUNCE | EF_BOUNCE_HALF ) ) ) + { + G_BounceMissile( ent, trace ); + + //only play a sound if requested + if( !( ent->s.eFlags & EF_NO_BOUNCE_SOUND ) ) + G_AddEvent( ent, EV_GRENADE_BOUNCE, 0 ); + + return; + } + + if( !strcmp( ent->classname, "grenade" ) ) + { + //grenade doesn't explode on impact + G_BounceMissile( ent, trace ); + + //only play a sound if requested + if( !( ent->s.eFlags & EF_NO_BOUNCE_SOUND ) ) + G_AddEvent( ent, EV_GRENADE_BOUNCE, 0 ); + + return; + } + else if( !strcmp( ent->classname, "lockblob" ) ) + { + if( other->client && other->client->ps.stats[ STAT_PTEAM ] == PTE_HUMANS ) + { + other->client->ps.stats[ STAT_STATE ] |= SS_BLOBLOCKED; + other->client->lastLockTime = level.time; + AngleVectors( other->client->ps.viewangles, dir, NULL, NULL ); + other->client->ps.stats[ STAT_VIEWLOCK ] = DirToByte( dir ); + } + } + else if( !strcmp( ent->classname, "slowblob" ) ) + { + if( other->client && other->client->ps.stats[ STAT_PTEAM ] == PTE_HUMANS ) + { + other->client->ps.stats[ STAT_STATE ] |= SS_SLOWLOCKED; + other->client->lastSlowTime = level.time; + AngleVectors( other->client->ps.viewangles, dir, NULL, NULL ); + other->client->ps.stats[ STAT_VIEWLOCK ] = DirToByte( dir ); + } + } + else if( !strcmp( ent->classname, "hive" ) ) + { + if( other->s.eType == ET_BUILDABLE && other->s.modelindex == BA_A_HIVE ) + { + if( !ent->parent ) + G_Printf( S_COLOR_YELLOW "WARNING: hive entity has no parent in G_MissileImpact\n" ); + else + ent->parent->active = qfalse; + + G_FreeEntity( ent ); + return; + } + else + { + //prevent collision with the client when returning + ent->r.ownerNum = other->s.number; + + ent->think = AHive_ReturnToHive; + ent->nextthink = level.time + FRAMETIME; + + //only damage humans + if( other->client && other->client->ps.stats[ STAT_PTEAM ] == PTE_HUMANS ) + returnAfterDamage = qtrue; + else + return; + } + } + + // impact damage + if( other->takedamage ) + { + // FIXME: wrong damage direction? + if( ent->damage ) + { + vec3_t velocity; + + BG_EvaluateTrajectoryDelta( &ent->s.pos, level.time, velocity ); + if( VectorLength( velocity ) == 0 ) + velocity[ 2 ] = 1; // stepped on a grenade + + G_Damage( other, ent, attacker, velocity, ent->s.origin, ent->damage, + 0, ent->methodOfDeath ); + } + } + + if( returnAfterDamage ) + return; + + // is it cheaper in bandwidth to just remove this ent and create a new + // one, rather than changing the missile into the explosion? + + if( other->takedamage && other->client ) + { + G_AddEvent( ent, EV_MISSILE_HIT, DirToByte( trace->plane.normal ) ); + ent->s.otherEntityNum = other->s.number; + } + else if( trace->surfaceFlags & SURF_METALSTEPS ) + G_AddEvent( ent, EV_MISSILE_MISS_METAL, DirToByte( trace->plane.normal ) ); + else + G_AddEvent( ent, EV_MISSILE_MISS, DirToByte( trace->plane.normal ) ); + + ent->freeAfterEvent = qtrue; + + // change over to a normal entity right at the point of impact + ent->s.eType = ET_GENERAL; + + SnapVectorTowards( trace->endpos, ent->s.pos.trBase ); // save net bandwidth + + G_SetOrigin( ent, trace->endpos ); + + // splash damage (doesn't apply to person directly hit) + if( ent->splashDamage ) + G_RadiusDamage( trace->endpos, ent->parent, ent->splashDamage, ent->splashRadius, + other, ent->splashMethodOfDeath ); + + trap_LinkEntity( ent ); +} + + +/* +================ +G_RunMissile + +================ +*/ +void G_RunMissile( gentity_t *ent ) +{ + vec3_t origin; + trace_t tr; + int passent; + + // get current position + BG_EvaluateTrajectory( &ent->s.pos, level.time, origin ); + + // ignore interactions with the missile owner + passent = ent->r.ownerNum; + + // trace a line from the previous position to the current position + trap_Trace( &tr, ent->r.currentOrigin, ent->r.mins, ent->r.maxs, origin, passent, ent->clipmask ); + + if( tr.startsolid || tr.allsolid ) + { + // make sure the tr.entityNum is set to the entity we're stuck in + trap_Trace( &tr, ent->r.currentOrigin, ent->r.mins, ent->r.maxs, ent->r.currentOrigin, passent, ent->clipmask ); + tr.fraction = 0; + } + else + VectorCopy( tr.endpos, ent->r.currentOrigin ); + + ent->r.contents = CONTENTS_SOLID; //trick trap_LinkEntity into... + trap_LinkEntity( ent ); + ent->r.contents = 0; //...encoding bbox information + + if( tr.fraction != 1 ) + { + // never explode or bounce on sky + if( tr.surfaceFlags & SURF_NOIMPACT ) + { + // If grapple, reset owner + if( ent->parent && ent->parent->client && ent->parent->client->hook == ent ) + ent->parent->client->hook = NULL; + + G_FreeEntity( ent ); + return; + } + + G_MissileImpact( ent, &tr ); + if( ent->s.eType != ET_MISSILE ) + return; // exploded + } + + // check think function after bouncing + G_RunThink( ent ); +} + + +//============================================================================= + +/* +================= +fire_flamer + +================= +*/ +gentity_t *fire_flamer( gentity_t *self, vec3_t start, vec3_t dir ) +{ + gentity_t *bolt; + vec3_t pvel; + + VectorNormalize (dir); + + bolt = G_Spawn(); + bolt->classname = "flame"; + bolt->nextthink = level.time + FLAMER_LIFETIME; + bolt->think = G_ExplodeMissile; + bolt->s.eType = ET_MISSILE; + bolt->r.svFlags = SVF_USE_CURRENT_ORIGIN; + bolt->s.weapon = WP_FLAMER; + bolt->s.generic1 = self->s.generic1; //weaponMode + bolt->r.ownerNum = self->s.number; + bolt->parent = self; + bolt->damage = FLAMER_DMG; + bolt->splashDamage = FLAMER_DMG; + bolt->splashRadius = FLAMER_RADIUS; + bolt->methodOfDeath = MOD_FLAMER; + bolt->splashMethodOfDeath = MOD_FLAMER_SPLASH; + bolt->clipmask = MASK_SHOT; + bolt->target_ent = NULL; + bolt->r.mins[ 0 ] = bolt->r.mins[ 1 ] = bolt->r.mins[ 2 ] = -15.0f; + bolt->r.maxs[ 0 ] = bolt->r.maxs[ 1 ] = bolt->r.maxs[ 2 ] = 15.0f; + + bolt->s.pos.trType = TR_LINEAR; + bolt->s.pos.trTime = level.time - MISSILE_PRESTEP_TIME; // move a bit on the very first frame + VectorCopy( start, bolt->s.pos.trBase ); + VectorScale( self->client->ps.velocity, FLAMER_LAG, pvel ); + VectorMA( pvel, FLAMER_SPEED, dir, bolt->s.pos.trDelta ); + SnapVector( bolt->s.pos.trDelta ); // save net bandwidth + + VectorCopy( start, bolt->r.currentOrigin ); + + return bolt; +} + +//============================================================================= + +/* +================= +fire_blaster + +================= +*/ +gentity_t *fire_blaster( gentity_t *self, vec3_t start, vec3_t dir ) +{ + gentity_t *bolt; + + VectorNormalize (dir); + + bolt = G_Spawn(); + bolt->classname = "blaster"; + bolt->nextthink = level.time + 10000; + bolt->think = G_ExplodeMissile; + bolt->s.eType = ET_MISSILE; + bolt->r.svFlags = SVF_USE_CURRENT_ORIGIN; + bolt->s.weapon = WP_BLASTER; + bolt->s.generic1 = self->s.generic1; //weaponMode + bolt->r.ownerNum = self->s.number; + bolt->parent = self; + bolt->damage = BLASTER_DMG; + bolt->splashDamage = 0; + bolt->splashRadius = 0; + bolt->methodOfDeath = MOD_BLASTER; + bolt->splashMethodOfDeath = MOD_BLASTER; + bolt->clipmask = MASK_SHOT; + bolt->target_ent = NULL; + + bolt->s.pos.trType = TR_LINEAR; + bolt->s.pos.trTime = level.time - MISSILE_PRESTEP_TIME; // move a bit on the very first frame + VectorCopy( start, bolt->s.pos.trBase ); + VectorScale( dir, BLASTER_SPEED, bolt->s.pos.trDelta ); + SnapVector( bolt->s.pos.trDelta ); // save net bandwidth + + VectorCopy( start, bolt->r.currentOrigin ); + + return bolt; +} + +//============================================================================= + +/* +================= +fire_pulseRifle + +================= +*/ +gentity_t *fire_pulseRifle( gentity_t *self, vec3_t start, vec3_t dir ) +{ + gentity_t *bolt; + + VectorNormalize (dir); + + bolt = G_Spawn(); + bolt->classname = "pulse"; + bolt->nextthink = level.time + 10000; + bolt->think = G_ExplodeMissile; + bolt->s.eType = ET_MISSILE; + bolt->r.svFlags = SVF_USE_CURRENT_ORIGIN; + bolt->s.weapon = WP_PULSE_RIFLE; + bolt->s.generic1 = self->s.generic1; //weaponMode + bolt->r.ownerNum = self->s.number; + bolt->parent = self; + bolt->damage = PRIFLE_DMG; + bolt->splashDamage = 0; + bolt->splashRadius = 0; + bolt->methodOfDeath = MOD_PRIFLE; + bolt->splashMethodOfDeath = MOD_PRIFLE; + bolt->clipmask = MASK_SHOT; + bolt->target_ent = NULL; + + bolt->s.pos.trType = TR_LINEAR; + bolt->s.pos.trTime = level.time - MISSILE_PRESTEP_TIME; // move a bit on the very first frame + VectorCopy( start, bolt->s.pos.trBase ); + VectorScale( dir, PRIFLE_SPEED, bolt->s.pos.trDelta ); + SnapVector( bolt->s.pos.trDelta ); // save net bandwidth + + VectorCopy( start, bolt->r.currentOrigin ); + + return bolt; +} + +//============================================================================= + +/* +================= +fire_luciferCannon + +================= +*/ +gentity_t *fire_luciferCannon( gentity_t *self, vec3_t start, vec3_t dir, int damage, int radius ) +{ + gentity_t *bolt; + int localDamage = (int)( ceil( ( (float)damage / + (float)LCANNON_TOTAL_CHARGE ) * (float)LCANNON_DAMAGE ) ); + + VectorNormalize( dir ); + + bolt = G_Spawn( ); + bolt->classname = "lcannon"; + + if( damage == LCANNON_TOTAL_CHARGE ) + bolt->nextthink = level.time; + else + bolt->nextthink = level.time + 10000; + + bolt->think = G_ExplodeMissile; + bolt->s.eType = ET_MISSILE; + bolt->r.svFlags = SVF_USE_CURRENT_ORIGIN; + bolt->s.weapon = WP_LUCIFER_CANNON; + bolt->s.generic1 = self->s.generic1; //weaponMode + bolt->r.ownerNum = self->s.number; + bolt->parent = self; + bolt->damage = localDamage; + bolt->splashDamage = localDamage / 2; + bolt->splashRadius = radius; + bolt->methodOfDeath = MOD_LCANNON; + bolt->splashMethodOfDeath = MOD_LCANNON_SPLASH; + bolt->clipmask = MASK_SHOT; + bolt->target_ent = NULL; + + bolt->s.pos.trType = TR_LINEAR; + bolt->s.pos.trTime = level.time - MISSILE_PRESTEP_TIME; // move a bit on the very first frame + VectorCopy( start, bolt->s.pos.trBase ); + VectorScale( dir, LCANNON_SPEED, bolt->s.pos.trDelta ); + SnapVector( bolt->s.pos.trDelta ); // save net bandwidth + + VectorCopy( start, bolt->r.currentOrigin ); + + return bolt; +} + +/* +================= +launch_grenade + +================= +*/ +gentity_t *launch_grenade( gentity_t *self, vec3_t start, vec3_t dir ) +{ + gentity_t *bolt; + + VectorNormalize( dir ); + + bolt = G_Spawn( ); + bolt->classname = "grenade"; + bolt->nextthink = level.time + 5000; + bolt->think = G_ExplodeMissile; + bolt->s.eType = ET_MISSILE; + bolt->r.svFlags = SVF_USE_CURRENT_ORIGIN; + bolt->s.weapon = WP_GRENADE; + bolt->s.eFlags = EF_BOUNCE_HALF; + bolt->s.generic1 = WPM_PRIMARY; //weaponMode + bolt->r.ownerNum = self->s.number; + bolt->parent = self; + bolt->damage = GRENADE_DAMAGE; + bolt->splashDamage = GRENADE_DAMAGE; + bolt->splashRadius = GRENADE_RANGE; + bolt->methodOfDeath = MOD_GRENADE; + bolt->splashMethodOfDeath = MOD_GRENADE; + bolt->clipmask = MASK_SHOT; + bolt->target_ent = NULL; + bolt->r.mins[ 0 ] = bolt->r.mins[ 1 ] = bolt->r.mins[ 2 ] = -3.0f; + bolt->r.maxs[ 0 ] = bolt->r.maxs[ 1 ] = bolt->r.maxs[ 2 ] = 3.0f; + bolt->s.time = level.time; + + bolt->s.pos.trType = TR_GRAVITY; + bolt->s.pos.trTime = level.time - MISSILE_PRESTEP_TIME; // move a bit on the very first frame + VectorCopy( start, bolt->s.pos.trBase ); + VectorScale( dir, GRENADE_SPEED, bolt->s.pos.trDelta ); + SnapVector( bolt->s.pos.trDelta ); // save net bandwidth + + VectorCopy( start, bolt->r.currentOrigin ); + + return bolt; +} +//============================================================================= + +/* +================ +AHive_ReturnToHive + +Adjust the trajectory to point towards the hive +================ +*/ +void AHive_ReturnToHive( gentity_t *self ) +{ + vec3_t dir; + trace_t tr; + + if( !self->parent ) + { + G_Printf( S_COLOR_YELLOW "WARNING: AHive_ReturnToHive called with no self->parent\n" ); + return; + } + + trap_UnlinkEntity( self->parent ); + trap_Trace( &tr, self->r.currentOrigin, self->r.mins, self->r.maxs, + self->parent->r.currentOrigin, self->r.ownerNum, self->clipmask ); + trap_LinkEntity( self->parent ); + + if( tr.fraction < 1.0f ) + { + //if can't see hive then disperse + VectorCopy( self->r.currentOrigin, self->s.pos.trBase ); + self->s.pos.trType = TR_STATIONARY; + self->s.pos.trTime = level.time; + + self->think = G_ExplodeMissile; + self->nextthink = level.time + 2000; + self->parent->active = qfalse; //allow the parent to start again + } + else + { + VectorSubtract( self->parent->r.currentOrigin, self->r.currentOrigin, dir ); + VectorNormalize( dir ); + + //change direction towards the hive + VectorScale( dir, HIVE_SPEED, self->s.pos.trDelta ); + SnapVector( self->s.pos.trDelta ); // save net bandwidth + VectorCopy( self->r.currentOrigin, self->s.pos.trBase ); + self->s.pos.trTime = level.time; + + self->think = G_ExplodeMissile; + self->nextthink = level.time + 15000; + } +} + +/* +================ +AHive_SearchAndDestroy + +Adjust the trajectory to point towards the target +================ +*/ +void AHive_SearchAndDestroy( gentity_t *self ) +{ + vec3_t dir; + trace_t tr; + + trap_Trace( &tr, self->r.currentOrigin, self->r.mins, self->r.maxs, + self->target_ent->r.currentOrigin, self->r.ownerNum, self->clipmask ); + + //if there is no LOS or the parent hive is too far away or the target is dead or notargeting, return + if( tr.entityNum == ENTITYNUM_WORLD || + Distance( self->r.currentOrigin, self->parent->r.currentOrigin ) > ( HIVE_RANGE * 5 ) || + self->target_ent->health <= 0 || self->target_ent->flags & FL_NOTARGET ) + { + self->r.ownerNum = ENTITYNUM_WORLD; + + self->think = AHive_ReturnToHive; + self->nextthink = level.time + FRAMETIME; + } + else + { + VectorSubtract( self->target_ent->r.currentOrigin, self->r.currentOrigin, dir ); + VectorNormalize( dir ); + + //change direction towards the player + VectorScale( dir, HIVE_SPEED, self->s.pos.trDelta ); + SnapVector( self->s.pos.trDelta ); // save net bandwidth + VectorCopy( self->r.currentOrigin, self->s.pos.trBase ); + self->s.pos.trTime = level.time; + + self->nextthink = level.time + HIVE_DIR_CHANGE_PERIOD; + } +} + +/* +================= +fire_hive +================= +*/ +gentity_t *fire_hive( gentity_t *self, vec3_t start, vec3_t dir ) +{ + gentity_t *bolt; + + VectorNormalize ( dir ); + + bolt = G_Spawn( ); + bolt->classname = "hive"; + bolt->nextthink = level.time + HIVE_DIR_CHANGE_PERIOD; + bolt->think = AHive_SearchAndDestroy; + bolt->s.eType = ET_MISSILE; + bolt->s.eFlags |= EF_BOUNCE|EF_NO_BOUNCE_SOUND; + bolt->r.svFlags = SVF_USE_CURRENT_ORIGIN; + bolt->s.weapon = WP_HIVE; + bolt->s.generic1 = WPM_PRIMARY; //weaponMode + bolt->r.ownerNum = self->s.number; + bolt->parent = self; + bolt->damage = HIVE_DMG; + bolt->splashDamage = 0; + bolt->splashRadius = 0; + bolt->methodOfDeath = MOD_SWARM; + bolt->clipmask = MASK_SHOT; + bolt->target_ent = self->target_ent; + + bolt->s.pos.trType = TR_LINEAR; + bolt->s.pos.trTime = level.time - MISSILE_PRESTEP_TIME; // move a bit on the very first frame + VectorCopy( start, bolt->s.pos.trBase ); + VectorScale( dir, HIVE_SPEED, bolt->s.pos.trDelta ); + SnapVector( bolt->s.pos.trDelta ); // save net bandwidth + VectorCopy( start, bolt->r.currentOrigin ); + + return bolt; +} + +//============================================================================= + +/* +================= +fire_lockblob +================= +*/ +gentity_t *fire_lockblob( gentity_t *self, vec3_t start, vec3_t dir ) +{ + gentity_t *bolt; + + VectorNormalize ( dir ); + + bolt = G_Spawn( ); + bolt->classname = "lockblob"; + bolt->nextthink = level.time + 15000; + bolt->think = G_ExplodeMissile; + bolt->s.eType = ET_MISSILE; + bolt->r.svFlags = SVF_USE_CURRENT_ORIGIN; + bolt->s.weapon = WP_LOCKBLOB_LAUNCHER; + bolt->s.generic1 = WPM_PRIMARY; //weaponMode + bolt->r.ownerNum = self->s.number; + bolt->parent = self; + bolt->damage = 0; + bolt->splashDamage = 0; + bolt->splashRadius = 0; + bolt->methodOfDeath = MOD_UNKNOWN; //doesn't do damage so will never kill + bolt->clipmask = MASK_SHOT; + bolt->target_ent = NULL; + + bolt->s.pos.trType = TR_LINEAR; + bolt->s.pos.trTime = level.time - MISSILE_PRESTEP_TIME; // move a bit on the very first frame + VectorCopy( start, bolt->s.pos.trBase ); + VectorScale( dir, 500, bolt->s.pos.trDelta ); + SnapVector( bolt->s.pos.trDelta ); // save net bandwidth + VectorCopy( start, bolt->r.currentOrigin ); + + return bolt; +} + +/* +================= +fire_slowBlob +================= +*/ +gentity_t *fire_slowBlob( gentity_t *self, vec3_t start, vec3_t dir ) +{ + gentity_t *bolt; + + VectorNormalize ( dir ); + + bolt = G_Spawn( ); + bolt->classname = "slowblob"; + bolt->nextthink = level.time + 15000; + bolt->think = G_ExplodeMissile; + bolt->s.eType = ET_MISSILE; + bolt->r.svFlags = SVF_USE_CURRENT_ORIGIN; + bolt->s.weapon = WP_ABUILD2; + bolt->s.generic1 = self->s.generic1; //weaponMode + bolt->r.ownerNum = self->s.number; + bolt->parent = self; + bolt->damage = ABUILDER_BLOB_DMG; + bolt->splashDamage = 0; + bolt->splashRadius = 0; + bolt->methodOfDeath = MOD_SLOWBLOB; + bolt->splashMethodOfDeath = MOD_SLOWBLOB; + bolt->clipmask = MASK_SHOT; + bolt->target_ent = NULL; + + bolt->s.pos.trType = TR_GRAVITY; + bolt->s.pos.trTime = level.time - MISSILE_PRESTEP_TIME; // move a bit on the very first frame + VectorCopy( start, bolt->s.pos.trBase ); + VectorScale( dir, ABUILDER_BLOB_SPEED, bolt->s.pos.trDelta ); + SnapVector( bolt->s.pos.trDelta ); // save net bandwidth + VectorCopy( start, bolt->r.currentOrigin ); + + return bolt; +} + +/* +================= +fire_paraLockBlob +================= +*/ +gentity_t *fire_paraLockBlob( gentity_t *self, vec3_t start, vec3_t dir ) +{ + gentity_t *bolt; + + VectorNormalize ( dir ); + + bolt = G_Spawn( ); + bolt->classname = "lockblob"; + bolt->nextthink = level.time + 15000; + bolt->think = G_ExplodeMissile; + bolt->s.eType = ET_MISSILE; + bolt->r.svFlags = SVF_USE_CURRENT_ORIGIN; + bolt->s.weapon = WP_LOCKBLOB_LAUNCHER; + bolt->s.generic1 = self->s.generic1; //weaponMode + bolt->r.ownerNum = self->s.number; + bolt->parent = self; + bolt->damage = 0; + bolt->splashDamage = 0; + bolt->splashRadius = 0; + bolt->clipmask = MASK_SHOT; + bolt->target_ent = NULL; + + bolt->s.pos.trType = TR_GRAVITY; + bolt->s.pos.trTime = level.time - MISSILE_PRESTEP_TIME; // move a bit on the very first frame + VectorCopy( start, bolt->s.pos.trBase ); + VectorScale( dir, LOCKBLOB_SPEED, bolt->s.pos.trDelta ); + SnapVector( bolt->s.pos.trDelta ); // save net bandwidth + VectorCopy( start, bolt->r.currentOrigin ); + + return bolt; +} + +/* +================= +fire_bounceBall +================= +*/ +gentity_t *fire_bounceBall( gentity_t *self, vec3_t start, vec3_t dir ) +{ + gentity_t *bolt; + + VectorNormalize ( dir ); + + bolt = G_Spawn( ); + bolt->classname = "bounceball"; + bolt->nextthink = level.time + 3000; + bolt->think = G_ExplodeMissile; + bolt->s.eType = ET_MISSILE; + bolt->r.svFlags = SVF_USE_CURRENT_ORIGIN; + bolt->s.weapon = WP_ALEVEL3_UPG; + bolt->s.generic1 = self->s.generic1; //weaponMode + bolt->r.ownerNum = self->s.number; + bolt->parent = self; + bolt->damage = LEVEL3_BOUNCEBALL_DMG; + bolt->splashDamage = 0; + bolt->splashRadius = 0; + bolt->methodOfDeath = MOD_LEVEL3_BOUNCEBALL; + bolt->splashMethodOfDeath = MOD_LEVEL3_BOUNCEBALL; + bolt->clipmask = MASK_SHOT; + bolt->target_ent = NULL; + + bolt->s.pos.trType = TR_GRAVITY; + bolt->s.pos.trTime = level.time - MISSILE_PRESTEP_TIME; // move a bit on the very first frame + VectorCopy( start, bolt->s.pos.trBase ); + VectorScale( dir, LEVEL3_BOUNCEBALL_SPEED, bolt->s.pos.trDelta ); + SnapVector( bolt->s.pos.trDelta ); // save net bandwidth + VectorCopy( start, bolt->r.currentOrigin ); + /*bolt->s.eFlags |= EF_BOUNCE;*/ + + return bolt; +} + diff --git a/src/game/g_mover.c b/src/game/g_mover.c new file mode 100644 index 0000000..268a25d --- /dev/null +++ b/src/game/g_mover.c @@ -0,0 +1,2479 @@ +/* +=========================================================================== +Copyright (C) 1999-2005 Id Software, Inc. +Copyright (C) 2000-2006 Tim Angus + +This file is part of Tremulous. + +Tremulous is free software; you can redistribute it +and/or modify it under the terms of the GNU General Public License as +published by the Free Software Foundation; either version 2 of the License, +or (at your option) any later version. + +Tremulous is distributed in the hope that it will be +useful, but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with Tremulous; if not, write to the Free Software +Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA +=========================================================================== +*/ + +#include "g_local.h" + + + +/* +=============================================================================== + +PUSHMOVE + +=============================================================================== +*/ + +void MatchTeam( gentity_t *teamLeader, int moverState, int time ); + +typedef struct +{ + gentity_t *ent; + vec3_t origin; + vec3_t angles; + float deltayaw; +} pushed_t; + +pushed_t pushed[ MAX_GENTITIES ], *pushed_p; + + +/* +============ +G_TestEntityPosition + +============ +*/ +gentity_t *G_TestEntityPosition( gentity_t *ent ) +{ + trace_t tr; + int mask; + + if( ent->clipmask ) + mask = ent->clipmask; + else + mask = MASK_SOLID; + + if( ent->client ) + trap_Trace( &tr, ent->client->ps.origin, ent->r.mins, ent->r.maxs, ent->client->ps.origin, ent->s.number, mask ); + else + trap_Trace( &tr, ent->s.pos.trBase, ent->r.mins, ent->r.maxs, ent->s.pos.trBase, ent->s.number, mask ); + + if( tr.startsolid ) + return &g_entities[ tr.entityNum ]; + + return NULL; +} + +/* +================ +G_CreateRotationMatrix +================ +*/ +void G_CreateRotationMatrix( vec3_t angles, vec3_t matrix[ 3 ] ) +{ + AngleVectors( angles, matrix[ 0 ], matrix[ 1 ], matrix[ 2 ] ); + VectorInverse( matrix[ 1 ] ); +} + +/* +================ +G_TransposeMatrix +================ +*/ +void G_TransposeMatrix( vec3_t matrix[ 3 ], vec3_t transpose[ 3 ] ) +{ + int i, j; + + for( i = 0; i < 3; i++ ) + { + for( j = 0; j < 3; j++ ) + { + transpose[ i ][ j ] = matrix[ j ][ i ]; + } + } +} + +/* +================ +G_RotatePoint +================ +*/ +void G_RotatePoint( vec3_t point, vec3_t matrix[ 3 ] ) +{ + vec3_t tvec; + + VectorCopy( point, tvec ); + point[ 0 ] = DotProduct( matrix[ 0 ], tvec ); + point[ 1 ] = DotProduct( matrix[ 1 ], tvec ); + point[ 2 ] = DotProduct( matrix[ 2 ], tvec ); +} + +/* +================== +G_TryPushingEntity + +Returns qfalse if the move is blocked +================== +*/ +qboolean G_TryPushingEntity( gentity_t *check, gentity_t *pusher, vec3_t move, vec3_t amove ) +{ + vec3_t matrix[ 3 ], transpose[ 3 ]; + vec3_t org, org2, move2; + gentity_t *block; + + // EF_MOVER_STOP will just stop when contacting another entity + // instead of pushing it, but entities can still ride on top of it + if( ( pusher->s.eFlags & EF_MOVER_STOP ) && + check->s.groundEntityNum != pusher->s.number ) + return qfalse; + + //don't try to move buildables unless standing on a mover + if( check->s.eType == ET_BUILDABLE && + check->s.groundEntityNum != pusher->s.number ) + return qfalse; + + // save off the old position + if( pushed_p > &pushed[ MAX_GENTITIES ] ) + G_Error( "pushed_p > &pushed[MAX_GENTITIES]" ); + + pushed_p->ent = check; + VectorCopy( check->s.pos.trBase, pushed_p->origin ); + VectorCopy( check->s.apos.trBase, pushed_p->angles ); + + if( check->client ) + { + pushed_p->deltayaw = check->client->ps.delta_angles[ YAW ]; + VectorCopy( check->client->ps.origin, pushed_p->origin ); + } + pushed_p++; + + // try moving the contacted entity + // figure movement due to the pusher's amove + G_CreateRotationMatrix( amove, transpose ); + G_TransposeMatrix( transpose, matrix ); + + if( check->client ) + VectorSubtract( check->client->ps.origin, pusher->r.currentOrigin, org ); + else + VectorSubtract( check->s.pos.trBase, pusher->r.currentOrigin, org ); + + VectorCopy( org, org2 ); + G_RotatePoint( org2, matrix ); + VectorSubtract( org2, org, move2 ); + // add movement + VectorAdd( check->s.pos.trBase, move, check->s.pos.trBase ); + VectorAdd( check->s.pos.trBase, move2, check->s.pos.trBase ); + + if( check->client ) + { + VectorAdd( check->client->ps.origin, move, check->client->ps.origin ); + VectorAdd( check->client->ps.origin, move2, check->client->ps.origin ); + // make sure the client's view rotates when on a rotating mover + check->client->ps.delta_angles[ YAW ] += ANGLE2SHORT( amove[ YAW ] ); + } + + // may have pushed them off an edge + if( check->s.groundEntityNum != pusher->s.number ) + check->s.groundEntityNum = -1; + + block = G_TestEntityPosition( check ); + + if( !block ) + { + // pushed ok + if( check->client ) + VectorCopy( check->client->ps.origin, check->r.currentOrigin ); + else + VectorCopy( check->s.pos.trBase, check->r.currentOrigin ); + + trap_LinkEntity( check ); + return qtrue; + } + + // if it is ok to leave in the old position, do it + // this is only relevent for riding entities, not pushed + // Sliding trapdoors can cause this. + VectorCopy( ( pushed_p - 1 )->origin, check->s.pos.trBase ); + + if( check->client ) + VectorCopy( ( pushed_p - 1 )->origin, check->client->ps.origin ); + + VectorCopy( ( pushed_p - 1 )->angles, check->s.apos.trBase ); + block = G_TestEntityPosition( check ); + + if( !block ) + { + check->s.groundEntityNum = -1; + pushed_p--; + return qtrue; + } + + // blocked + return qfalse; +} + + +/* +============ +G_MoverPush + +Objects need to be moved back on a failed push, +otherwise riders would continue to slide. +If qfalse is returned, *obstacle will be the blocking entity +============ +*/ +qboolean G_MoverPush( gentity_t *pusher, vec3_t move, vec3_t amove, gentity_t **obstacle ) +{ + int i, e; + gentity_t *check; + vec3_t mins, maxs; + pushed_t *p; + int entityList[ MAX_GENTITIES ]; + int listedEntities; + vec3_t totalMins, totalMaxs; + + *obstacle = NULL; + + + // mins/maxs are the bounds at the destination + // totalMins / totalMaxs are the bounds for the entire move + if( pusher->r.currentAngles[ 0 ] || pusher->r.currentAngles[ 1 ] || pusher->r.currentAngles[ 2 ] + || amove[ 0 ] || amove[ 1 ] || amove[ 2 ] ) + { + float radius; + + radius = RadiusFromBounds( pusher->r.mins, pusher->r.maxs ); + + for( i = 0 ; i < 3 ; i++ ) + { + mins[ i ] = pusher->r.currentOrigin[ i ] + move[ i ] - radius; + maxs[ i ] = pusher->r.currentOrigin[ i ] + move[ i ] + radius; + totalMins[ i ] = mins[ i ] - move[ i ]; + totalMaxs[ i ] = maxs[ i ] - move[ i ]; + } + } + else + { + for( i = 0; i < 3; i++ ) + { + mins[ i ] = pusher->r.absmin[ i ] + move[ i ]; + maxs[ i ] = pusher->r.absmax[ i ] + move[ i ]; + } + + VectorCopy( pusher->r.absmin, totalMins ); + VectorCopy( pusher->r.absmax, totalMaxs ); + for( i = 0; i < 3; i++ ) + { + if( move[ i ] > 0 ) + totalMaxs[ i ] += move[ i ]; + else + totalMins[ i ] += move[ i ]; + } + } + + // unlink the pusher so we don't get it in the entityList + trap_UnlinkEntity( pusher ); + + listedEntities = trap_EntitiesInBox( totalMins, totalMaxs, entityList, MAX_GENTITIES ); + + // move the pusher to it's final position + VectorAdd( pusher->r.currentOrigin, move, pusher->r.currentOrigin ); + VectorAdd( pusher->r.currentAngles, amove, pusher->r.currentAngles ); + trap_LinkEntity( pusher ); + + // see if any solid entities are inside the final position + for( e = 0 ; e < listedEntities ; e++ ) + { + check = &g_entities[ entityList[ e ] ]; + + // only push items and players + if( check->s.eType != ET_ITEM && check->s.eType != ET_BUILDABLE && + check->s.eType != ET_CORPSE && check->s.eType != ET_PLAYER && + !check->physicsObject ) + continue; + + // if the entity is standing on the pusher, it will definitely be moved + if( check->s.groundEntityNum != pusher->s.number ) + { + // see if the ent needs to be tested + if( check->r.absmin[ 0 ] >= maxs[ 0 ] + || check->r.absmin[ 1 ] >= maxs[ 1 ] + || check->r.absmin[ 2 ] >= maxs[ 2 ] + || check->r.absmax[ 0 ] <= mins[ 0 ] + || check->r.absmax[ 1 ] <= mins[ 1 ] + || check->r.absmax[ 2 ] <= mins[ 2 ] ) + continue; + + // see if the ent's bbox is inside the pusher's final position + // this does allow a fast moving object to pass through a thin entity... + if( !G_TestEntityPosition( check ) ) + continue; + } + + // the entity needs to be pushed + if( G_TryPushingEntity( check, pusher, move, amove ) ) + continue; + + // the move was blocked an entity + + // bobbing entities are instant-kill and never get blocked + if( pusher->s.pos.trType == TR_SINE || pusher->s.apos.trType == TR_SINE ) + { + G_Damage( check, pusher, pusher, NULL, NULL, 99999, 0, MOD_CRUSH ); + continue; + } + + + // save off the obstacle so we can call the block function (crush, etc) + *obstacle = check; + + // move back any entities we already moved + // go backwards, so if the same entity was pushed + // twice, it goes back to the original position + for( p = pushed_p - 1; p >= pushed; p-- ) + { + VectorCopy( p->origin, p->ent->s.pos.trBase ); + VectorCopy( p->angles, p->ent->s.apos.trBase ); + + if( p->ent->client ) + { + p->ent->client->ps.delta_angles[ YAW ] = p->deltayaw; + VectorCopy( p->origin, p->ent->client->ps.origin ); + } + + trap_LinkEntity( p->ent ); + } + + return qfalse; + } + + return qtrue; +} + + +/* +================= +G_MoverTeam +================= +*/ +void G_MoverTeam( gentity_t *ent ) +{ + vec3_t move, amove; + gentity_t *part, *obstacle; + vec3_t origin, angles; + + obstacle = NULL; + + // make sure all team slaves can move before commiting + // any moves or calling any think functions + // if the move is blocked, all moved objects will be backed out + pushed_p = pushed; + for( part = ent; part; part = part->teamchain ) + { + // get current position + BG_EvaluateTrajectory( &part->s.pos, level.time, origin ); + BG_EvaluateTrajectory( &part->s.apos, level.time, angles ); + VectorSubtract( origin, part->r.currentOrigin, move ); + VectorSubtract( angles, part->r.currentAngles, amove ); + if( !G_MoverPush( part, move, amove, &obstacle ) ) + break; // move was blocked + } + + if( part ) + { + // go back to the previous position + for( part = ent; part; part = part->teamchain ) + { + part->s.pos.trTime += level.time - level.previousTime; + part->s.apos.trTime += level.time - level.previousTime; + BG_EvaluateTrajectory( &part->s.pos, level.time, part->r.currentOrigin ); + BG_EvaluateTrajectory( &part->s.apos, level.time, part->r.currentAngles ); + trap_LinkEntity( part ); + } + + // if the pusher has a "blocked" function, call it + if( ent->blocked ) + ent->blocked( ent, obstacle ); + + return; + } + + // the move succeeded + for( part = ent; part; part = part->teamchain ) + { + // call the reached function if time is at or past end point + if( part->s.pos.trType == TR_LINEAR_STOP ) + { + if( level.time >= part->s.pos.trTime + part->s.pos.trDuration ) + { + if( part->reached ) + part->reached( part ); + } + } + if ( part->s.apos.trType == TR_LINEAR_STOP ) { + if ( level.time >= part->s.apos.trTime + part->s.apos.trDuration ) { + if ( part->reached ) { + part->reached( part ); + } + } + } + } +} + +/* +================ +G_RunMover + +================ +*/ +void G_RunMover( gentity_t *ent ) +{ + // if not a team captain, don't do anything, because + // the captain will handle everything + if( ent->flags & FL_TEAMSLAVE ) + return; + + // if stationary at one of the positions, don't move anything + if( ( ent->s.pos.trType != TR_STATIONARY || ent->s.apos.trType != TR_STATIONARY ) && + ent->moverState < MODEL_POS1 ) //yuck yuck hack + G_MoverTeam( ent ); + + // check think function + G_RunThink( ent ); +} + +/* +============================================================================ + +GENERAL MOVERS + +Doors, plats, and buttons are all binary (two position) movers +Pos1 is "at rest", pos2 is "activated" +============================================================================ +*/ + +/* +=============== +SetMoverState +=============== +*/ +void SetMoverState( gentity_t *ent, moverState_t moverState, int time ) +{ + vec3_t delta; + float f; + + ent->moverState = moverState; + + ent->s.pos.trTime = time; + ent->s.apos.trTime = time; + + switch( moverState ) + { + case MOVER_POS1: + VectorCopy( ent->pos1, ent->s.pos.trBase ); + ent->s.pos.trType = TR_STATIONARY; + break; + + case MOVER_POS2: + VectorCopy( ent->pos2, ent->s.pos.trBase ); + ent->s.pos.trType = TR_STATIONARY; + break; + + case MOVER_1TO2: + VectorCopy( ent->pos1, ent->s.pos.trBase ); + VectorSubtract( ent->pos2, ent->pos1, delta ); + f = 1000.0 / ent->s.pos.trDuration; + VectorScale( delta, f, ent->s.pos.trDelta ); + ent->s.pos.trType = TR_LINEAR_STOP; + break; + + case MOVER_2TO1: + VectorCopy( ent->pos2, ent->s.pos.trBase ); + VectorSubtract( ent->pos1, ent->pos2, delta ); + f = 1000.0 / ent->s.pos.trDuration; + VectorScale( delta, f, ent->s.pos.trDelta ); + ent->s.pos.trType = TR_LINEAR_STOP; + break; + + case ROTATOR_POS1: + VectorCopy( ent->pos1, ent->s.apos.trBase ); + ent->s.apos.trType = TR_STATIONARY; + break; + + case ROTATOR_POS2: + VectorCopy( ent->pos2, ent->s.apos.trBase ); + ent->s.apos.trType = TR_STATIONARY; + break; + + case ROTATOR_1TO2: + VectorCopy( ent->pos1, ent->s.apos.trBase ); + VectorSubtract( ent->pos2, ent->pos1, delta ); + f = 1000.0 / ent->s.apos.trDuration; + VectorScale( delta, f, ent->s.apos.trDelta ); + ent->s.apos.trType = TR_LINEAR_STOP; + break; + + case ROTATOR_2TO1: + VectorCopy( ent->pos2, ent->s.apos.trBase ); + VectorSubtract( ent->pos1, ent->pos2, delta ); + f = 1000.0 / ent->s.apos.trDuration; + VectorScale( delta, f, ent->s.apos.trDelta ); + ent->s.apos.trType = TR_LINEAR_STOP; + break; + + case MODEL_POS1: + break; + + case MODEL_POS2: + break; + + default: + break; + } + + if( moverState >= MOVER_POS1 && moverState <= MOVER_2TO1 ) + BG_EvaluateTrajectory( &ent->s.pos, level.time, ent->r.currentOrigin ); + + if( moverState >= ROTATOR_POS1 && moverState <= ROTATOR_2TO1 ) + BG_EvaluateTrajectory( &ent->s.apos, level.time, ent->r.currentAngles ); + + trap_LinkEntity( ent ); +} + +/* +================ +MatchTeam + +All entities in a mover team will move from pos1 to pos2 +in the same amount of time +================ +*/ +void MatchTeam( gentity_t *teamLeader, int moverState, int time ) +{ + gentity_t *slave; + + for( slave = teamLeader; slave; slave = slave->teamchain ) + SetMoverState( slave, moverState, time ); +} + + + +/* +================ +ReturnToPos1 +================ +*/ +void ReturnToPos1( gentity_t *ent ) +{ + MatchTeam( ent, MOVER_2TO1, level.time ); + + // looping sound + ent->s.loopSound = ent->soundLoop; + + // starting sound + if( ent->sound2to1 ) + G_AddEvent( ent, EV_GENERAL_SOUND, ent->sound2to1 ); +} + + +/* +================ +ReturnToApos1 +================ +*/ +void ReturnToApos1( gentity_t *ent ) +{ + MatchTeam( ent, ROTATOR_2TO1, level.time ); + + // looping sound + ent->s.loopSound = ent->soundLoop; + + // starting sound + if( ent->sound2to1 ) + G_AddEvent( ent, EV_GENERAL_SOUND, ent->sound2to1 ); +} + + +/* +================ +Think_ClosedModelDoor +================ +*/ +void Think_ClosedModelDoor( gentity_t *ent ) +{ + // play sound + if( ent->soundPos1 ) + G_AddEvent( ent, EV_GENERAL_SOUND, ent->soundPos1 ); + + // close areaportals + if( ent->teammaster == ent || !ent->teammaster ) + trap_AdjustAreaPortalState( ent, qfalse ); + + ent->moverState = MODEL_POS1; +} + + +/* +================ +Think_CloseModelDoor +================ +*/ +void Think_CloseModelDoor( gentity_t *ent ) +{ + int entityList[ MAX_GENTITIES ]; + int numEntities, i; + gentity_t *clipBrush = ent->clipBrush; + gentity_t *check; + qboolean canClose = qtrue; + + numEntities = trap_EntitiesInBox( clipBrush->r.absmin, clipBrush->r.absmax, entityList, MAX_GENTITIES ); + + //set brush solid + trap_LinkEntity( ent->clipBrush ); + + //see if any solid entities are inside the door + for( i = 0; i < numEntities; i++ ) + { + check = &g_entities[ entityList[ i ] ]; + + //only test items and players + if( check->s.eType != ET_ITEM && check->s.eType != ET_BUILDABLE && + check->s.eType != ET_CORPSE && check->s.eType != ET_PLAYER && + !check->physicsObject ) + continue; + + //test is this entity collides with this door + if( G_TestEntityPosition( check ) ) + canClose = qfalse; + } + + //something is blocking this door + if( !canClose ) + { + //set brush non-solid + trap_UnlinkEntity( ent->clipBrush ); + + ent->nextthink = level.time + ent->wait; + return; + } + + //toggle door state + ent->s.legsAnim = qfalse; + + // play sound + if( ent->sound2to1 ) + G_AddEvent( ent, EV_GENERAL_SOUND, ent->sound2to1 ); + + ent->moverState = MODEL_2TO1; + + ent->think = Think_ClosedModelDoor; + ent->nextthink = level.time + ent->speed; +} + + +/* +================ +Think_OpenModelDoor +================ +*/ +void Think_OpenModelDoor( gentity_t *ent ) +{ + //set brush non-solid + trap_UnlinkEntity( ent->clipBrush ); + + // looping sound + ent->s.loopSound = ent->soundLoop; + + // starting sound + if( ent->soundPos2 ) + G_AddEvent( ent, EV_GENERAL_SOUND, ent->soundPos2 ); + + ent->moverState = MODEL_POS2; + + // return to pos1 after a delay + ent->think = Think_CloseModelDoor; + ent->nextthink = level.time + ent->wait; + + // fire targets + if( !ent->activator ) + ent->activator = ent; + + G_UseTargets( ent, ent->activator ); +} + + +/* +================ +Reached_BinaryMover +================ +*/ +void Reached_BinaryMover( gentity_t *ent ) +{ + // stop the looping sound + ent->s.loopSound = ent->soundLoop; + + if( ent->moverState == MOVER_1TO2 ) + { + // reached pos2 + SetMoverState( ent, MOVER_POS2, level.time ); + + // play sound + if( ent->soundPos2 ) + G_AddEvent( ent, EV_GENERAL_SOUND, ent->soundPos2 ); + + // return to pos1 after a delay + ent->think = ReturnToPos1; + ent->nextthink = level.time + ent->wait; + + // fire targets + if( !ent->activator ) + ent->activator = ent; + + G_UseTargets( ent, ent->activator ); + } + else if( ent->moverState == MOVER_2TO1 ) + { + // reached pos1 + SetMoverState( ent, MOVER_POS1, level.time ); + + // play sound + if( ent->soundPos1 ) + G_AddEvent( ent, EV_GENERAL_SOUND, ent->soundPos1 ); + + // close areaportals + if( ent->teammaster == ent || !ent->teammaster ) + trap_AdjustAreaPortalState( ent, qfalse ); + } + else if( ent->moverState == ROTATOR_1TO2 ) + { + // reached pos2 + SetMoverState( ent, ROTATOR_POS2, level.time ); + + // play sound + if( ent->soundPos2 ) + G_AddEvent( ent, EV_GENERAL_SOUND, ent->soundPos2 ); + + // return to apos1 after a delay + ent->think = ReturnToApos1; + ent->nextthink = level.time + ent->wait; + + // fire targets + if( !ent->activator ) + ent->activator = ent; + + G_UseTargets( ent, ent->activator ); + } + else if( ent->moverState == ROTATOR_2TO1 ) + { + // reached pos1 + SetMoverState( ent, ROTATOR_POS1, level.time ); + + // play sound + if( ent->soundPos1 ) + G_AddEvent( ent, EV_GENERAL_SOUND, ent->soundPos1 ); + + // close areaportals + if( ent->teammaster == ent || !ent->teammaster ) + trap_AdjustAreaPortalState( ent, qfalse ); + } + else + G_Error( "Reached_BinaryMover: bad moverState" ); +} + + +/* +================ +Use_BinaryMover +================ +*/ +void Use_BinaryMover( gentity_t *ent, gentity_t *other, gentity_t *activator ) +{ + int total; + int partial; + + // if this is a non-client-usable door return + if( ent->targetname && other && other->client ) + return; + + // only the master should be used + if( ent->flags & FL_TEAMSLAVE ) + { + Use_BinaryMover( ent->teammaster, other, activator ); + return; + } + + ent->activator = activator; + + if( ent->moverState == MOVER_POS1 ) + { + // start moving 50 msec later, becase if this was player + // triggered, level.time hasn't been advanced yet + MatchTeam( ent, MOVER_1TO2, level.time + 50 ); + + // starting sound + if( ent->sound1to2 ) + G_AddEvent( ent, EV_GENERAL_SOUND, ent->sound1to2 ); + + // looping sound + ent->s.loopSound = ent->soundLoop; + + // open areaportal + if( ent->teammaster == ent || !ent->teammaster ) + trap_AdjustAreaPortalState( ent, qtrue ); + } + else if( ent->moverState == MOVER_POS2 ) + { + // if all the way up, just delay before coming down + ent->nextthink = level.time + ent->wait; + } + else if( ent->moverState == MOVER_2TO1 ) + { + // only partway down before reversing + total = ent->s.pos.trDuration; + partial = level.time - ent->s.pos.trTime; + + if( partial > total ) + partial = total; + + MatchTeam( ent, MOVER_1TO2, level.time - ( total - partial ) ); + + if( ent->sound1to2 ) + G_AddEvent( ent, EV_GENERAL_SOUND, ent->sound1to2 ); + } + else if( ent->moverState == MOVER_1TO2 ) + { + // only partway up before reversing + total = ent->s.pos.trDuration; + partial = level.time - ent->s.pos.trTime; + + if( partial > total ) + partial = total; + + MatchTeam( ent, MOVER_2TO1, level.time - ( total - partial ) ); + + if( ent->sound2to1 ) + G_AddEvent( ent, EV_GENERAL_SOUND, ent->sound2to1 ); + } + else if( ent->moverState == ROTATOR_POS1 ) + { + // start moving 50 msec later, becase if this was player + // triggered, level.time hasn't been advanced yet + MatchTeam( ent, ROTATOR_1TO2, level.time + 50 ); + + // starting sound + if( ent->sound1to2 ) + G_AddEvent( ent, EV_GENERAL_SOUND, ent->sound1to2 ); + + // looping sound + ent->s.loopSound = ent->soundLoop; + + // open areaportal + if( ent->teammaster == ent || !ent->teammaster ) + trap_AdjustAreaPortalState( ent, qtrue ); + } + else if( ent->moverState == ROTATOR_POS2 ) + { + // if all the way up, just delay before coming down + ent->nextthink = level.time + ent->wait; + } + else if( ent->moverState == ROTATOR_2TO1 ) + { + // only partway down before reversing + total = ent->s.apos.trDuration; + partial = level.time - ent->s.apos.trTime; + + if( partial > total ) + partial = total; + + MatchTeam( ent, ROTATOR_1TO2, level.time - ( total - partial ) ); + + if( ent->sound1to2 ) + G_AddEvent( ent, EV_GENERAL_SOUND, ent->sound1to2 ); + } + else if( ent->moverState == ROTATOR_1TO2 ) + { + // only partway up before reversing + total = ent->s.apos.trDuration; + partial = level.time - ent->s.apos.trTime; + + if( partial > total ) + partial = total; + + MatchTeam( ent, ROTATOR_2TO1, level.time - ( total - partial ) ); + + if( ent->sound2to1 ) + G_AddEvent( ent, EV_GENERAL_SOUND, ent->sound2to1 ); + } + else if( ent->moverState == MODEL_POS1 ) + { + //toggle door state + ent->s.legsAnim = qtrue; + + ent->think = Think_OpenModelDoor; + ent->nextthink = level.time + ent->speed; + + // starting sound + if( ent->sound1to2 ) + G_AddEvent( ent, EV_GENERAL_SOUND, ent->sound1to2 ); + + // looping sound + ent->s.loopSound = ent->soundLoop; + + // open areaportal + if( ent->teammaster == ent || !ent->teammaster ) + trap_AdjustAreaPortalState( ent, qtrue ); + + ent->moverState = MODEL_1TO2; + } + else if( ent->moverState == MODEL_POS2 ) + { + // if all the way up, just delay before coming down + ent->nextthink = level.time + ent->wait; + } +} + + + +/* +================ +InitMover + +"pos1", "pos2", and "speed" should be set before calling, +so the movement delta can be calculated +================ +*/ +void InitMover( gentity_t *ent ) +{ + vec3_t move; + float distance; + float light; + vec3_t color; + qboolean lightSet, colorSet; + char *sound; + + // if the "model2" key is set, use a seperate model + // for drawing, but clip against the brushes + if( ent->model2 ) + ent->s.modelindex2 = G_ModelIndex( ent->model2 ); + + // if the "loopsound" key is set, use a constant looping sound when moving + if( G_SpawnString( "noise", "100", &sound ) ) + ent->s.loopSound = G_SoundIndex( sound ); + + // if the "color" or "light" keys are set, setup constantLight + lightSet = G_SpawnFloat( "light", "100", &light ); + colorSet = G_SpawnVector( "color", "1 1 1", color ); + + if( lightSet || colorSet ) + { + int r, g, b, i; + + r = color[ 0 ] * 255; + if( r > 255 ) + r = 255; + + g = color[ 1 ] * 255; + if( g > 255 ) + g = 255; + + b = color[ 2 ] * 255; + if( b > 255 ) + b = 255; + + i = light / 4; + if( i > 255 ) + i = 255; + + ent->s.constantLight = r | ( g << 8 ) | ( b << 16 ) | ( i << 24 ); + } + + + ent->use = Use_BinaryMover; + ent->reached = Reached_BinaryMover; + + ent->moverState = MOVER_POS1; + ent->r.svFlags = SVF_USE_CURRENT_ORIGIN; + ent->s.eType = ET_MOVER; + VectorCopy( ent->pos1, ent->r.currentOrigin ); + trap_LinkEntity( ent ); + + ent->s.pos.trType = TR_STATIONARY; + VectorCopy( ent->pos1, ent->s.pos.trBase ); + + // calculate time to reach second position from speed + VectorSubtract( ent->pos2, ent->pos1, move ); + distance = VectorLength( move ); + if( !ent->speed ) + ent->speed = 100; + + VectorScale( move, ent->speed, ent->s.pos.trDelta ); + ent->s.pos.trDuration = distance * 1000 / ent->speed; + + if( ent->s.pos.trDuration <= 0 ) + ent->s.pos.trDuration = 1; +} + + +/* +================ +InitRotator + +"pos1", "pos2", and "speed" should be set before calling, +so the movement delta can be calculated +================ +*/ +void InitRotator( gentity_t *ent ) +{ + vec3_t move; + float angle; + float light; + vec3_t color; + qboolean lightSet, colorSet; + char *sound; + + // if the "model2" key is set, use a seperate model + // for drawing, but clip against the brushes + if( ent->model2 ) + ent->s.modelindex2 = G_ModelIndex( ent->model2 ); + + // if the "loopsound" key is set, use a constant looping sound when moving + if( G_SpawnString( "noise", "100", &sound ) ) + ent->s.loopSound = G_SoundIndex( sound ); + + // if the "color" or "light" keys are set, setup constantLight + lightSet = G_SpawnFloat( "light", "100", &light ); + colorSet = G_SpawnVector( "color", "1 1 1", color ); + + if( lightSet || colorSet ) + { + int r, g, b, i; + + r = color[ 0 ] * 255; + + if( r > 255 ) + r = 255; + + g = color[ 1 ] * 255; + + if( g > 255 ) + g = 255; + + b = color[ 2 ] * 255; + + if( b > 255 ) + b = 255; + + i = light / 4; + + if( i > 255 ) + i = 255; + + ent->s.constantLight = r | ( g << 8 ) | ( b << 16 ) | ( i << 24 ); + } + + + ent->use = Use_BinaryMover; + ent->reached = Reached_BinaryMover; + + ent->moverState = ROTATOR_POS1; + ent->r.svFlags = SVF_USE_CURRENT_ORIGIN; + ent->s.eType = ET_MOVER; + VectorCopy( ent->pos1, ent->r.currentAngles ); + trap_LinkEntity( ent ); + + ent->s.apos.trType = TR_STATIONARY; + VectorCopy( ent->pos1, ent->s.apos.trBase ); + + // calculate time to reach second position from speed + VectorSubtract( ent->pos2, ent->pos1, move ); + angle = VectorLength( move ); + + if( !ent->speed ) + ent->speed = 120; + + VectorScale( move, ent->speed, ent->s.apos.trDelta ); + ent->s.apos.trDuration = angle * 1000 / ent->speed; + + if( ent->s.apos.trDuration <= 0 ) + ent->s.apos.trDuration = 1; +} + + +/* +=============================================================================== + +DOOR + +A use can be triggered either by a touch function, by being shot, or by being +targeted by another entity. + +=============================================================================== +*/ + +/* +================ +Blocked_Door +================ +*/ +void Blocked_Door( gentity_t *ent, gentity_t *other ) +{ + // remove anything other than a client or buildable + if( !other->client && other->s.eType != ET_BUILDABLE ) + { + G_FreeEntity( other ); + return; + } + + if( ent->damage ) + G_Damage( other, ent, ent, NULL, NULL, ent->damage, 0, MOD_CRUSH ); + + if( ent->spawnflags & 4 ) + return; // crushers don't reverse + + // reverse direction + Use_BinaryMover( ent, ent, other ); +} + +/* +================ +Touch_DoorTriggerSpectator +================ +*/ +static void Touch_DoorTriggerSpectator( gentity_t *ent, gentity_t *other, trace_t *trace ) +{ + int i, axis; + vec3_t origin, dir, angles; + + axis = ent->count; + VectorClear( dir ); + + if( fabs( other->s.origin[ axis ] - ent->r.absmax[ axis ] ) < + fabs( other->s.origin[ axis ] - ent->r.absmin[ axis ] ) ) + { + origin[ axis ] = ent->r.absmin[ axis ] - 20; + dir[ axis ] = -1; + } + else + { + origin[ axis ] = ent->r.absmax[ axis ] + 20; + dir[ axis ] = 1; + } + + for( i = 0; i < 3; i++ ) + { + if( i == axis ) + continue; + + origin[ i ] = ( ent->r.absmin[ i ] + ent->r.absmax[ i ] ) * 0.5; + } + + vectoangles( dir, angles ); + TeleportPlayer( other, origin, angles ); +} + + +/* +================ +manualDoorTriggerSpectator + +This effectively creates a temporary door auto trigger so manually +triggers doors can be skipped by spectators +================ +*/ +static void manualDoorTriggerSpectator( gentity_t *door, gentity_t *player ) +{ + gentity_t *other; + gentity_t triggerHull; + int best, i; + vec3_t mins, maxs; + + //don't skip a door that is already open + if( door->moverState == MOVER_1TO2 || + door->moverState == MOVER_POS2 || + door->moverState == ROTATOR_1TO2 || + door->moverState == ROTATOR_POS2 || + door->moverState == MODEL_1TO2 || + door->moverState == MODEL_POS2 ) + return; + + // find the bounds of everything on the team + VectorCopy( door->r.absmin, mins ); + VectorCopy( door->r.absmax, maxs ); + + for( other = door->teamchain; other; other = other->teamchain ) + { + AddPointToBounds( other->r.absmin, mins, maxs ); + AddPointToBounds( other->r.absmax, mins, maxs ); + } + + // find the thinnest axis, which will be the one we expand + best = 0; + for( i = 1; i < 3; i++ ) + { + if( maxs[ i ] - mins[ i ] < maxs[ best ] - mins[ best ] ) + best = i; + } + + maxs[ best ] += 60; + mins[ best ] -= 60; + + VectorCopy( mins, triggerHull.r.absmin ); + VectorCopy( maxs, triggerHull.r.absmax ); + triggerHull.count = best; + + Touch_DoorTriggerSpectator( &triggerHull, player, NULL ); +} + +/* +================ +manualTriggerSpectator + +Trip to skip the closest door targetted by trigger +================ +*/ +void manualTriggerSpectator( gentity_t *trigger, gentity_t *player ) +{ + gentity_t *t = NULL; + gentity_t *targets[ MAX_GENTITIES ]; + int i = 0, j; + float minDistance = (float)INFINITE; + + //restrict this hack to trigger_multiple only for now + if( strcmp( trigger->classname, "trigger_multiple" ) ) + return; + + if( !trigger->target ) + return; + + //create a list of door entities this trigger targets + while( ( t = G_Find( t, FOFS( targetname ), trigger->target ) ) != NULL ) + { + if( !strcmp( t->classname, "func_door" ) ) + targets[ i++ ] = t; + else if( t == trigger ) + G_Printf( "WARNING: Entity used itself.\n" ); + + if( !trigger->inuse ) + { + G_Printf( "triggerity was removed while using targets\n" ); + return; + } + } + + //if more than 0 targets + if( i > 0 ) + { + gentity_t *closest = NULL; + + //pick the closest door + for( j = 0; j < i; j++ ) + { + float d = Distance( player->r.currentOrigin, targets[ j ]->r.currentOrigin ); + + if( d < minDistance ) + { + minDistance = d; + closest = targets[ j ]; + } + } + + //try and skip the door + manualDoorTriggerSpectator( closest, player ); + } +} + + +/* +================ +Touch_DoorTrigger +================ +*/ +void Touch_DoorTrigger( gentity_t *ent, gentity_t *other, trace_t *trace ) +{ + //buildables don't trigger movers + if( other->s.eType == ET_BUILDABLE ) + return; + + if( other->client && other->client->sess.sessionTeam == TEAM_SPECTATOR ) + { + // if the door is not open and not opening + if( ent->parent->moverState != MOVER_1TO2 && + ent->parent->moverState != MOVER_POS2 && + ent->parent->moverState != ROTATOR_1TO2 && + ent->parent->moverState != ROTATOR_POS2 ) + Touch_DoorTriggerSpectator( ent, other, trace ); + } + else if( ent->parent->moverState != MOVER_1TO2 && + ent->parent->moverState != ROTATOR_1TO2 && + ent->parent->moverState != ROTATOR_2TO1 ) + { + Use_BinaryMover( ent->parent, ent, other ); + } +} + + +/* +====================== +Think_SpawnNewDoorTrigger + +All of the parts of a door have been spawned, so create +a trigger that encloses all of them +====================== +*/ +void Think_SpawnNewDoorTrigger( gentity_t *ent ) +{ + gentity_t *other; + vec3_t mins, maxs; + int i, best; + + //TA: disable shootable doors + // set all of the slaves as shootable + //for( other = ent; other; other = other->teamchain ) + // other->takedamage = qtrue; + + // find the bounds of everything on the team + VectorCopy( ent->r.absmin, mins ); + VectorCopy( ent->r.absmax, maxs ); + + for( other = ent->teamchain; other; other=other->teamchain ) + { + AddPointToBounds( other->r.absmin, mins, maxs ); + AddPointToBounds( other->r.absmax, mins, maxs ); + } + + // find the thinnest axis, which will be the one we expand + best = 0; + for( i = 1; i < 3; i++ ) + { + if( maxs[ i ] - mins[ i ] < maxs[ best ] - mins[ best ] ) + best = i; + } + + maxs[ best ] += 60; + mins[ best ] -= 60; + + // create a trigger with this size + other = G_Spawn( ); + other->classname = "door_trigger"; + VectorCopy( mins, other->r.mins ); + VectorCopy( maxs, other->r.maxs ); + other->parent = ent; + other->r.contents = CONTENTS_TRIGGER; + other->touch = Touch_DoorTrigger; + // remember the thinnest axis + other->count = best; + trap_LinkEntity( other ); + + if( ent->moverState < MODEL_POS1 ) + MatchTeam( ent, ent->moverState, level.time ); +} + +void Think_MatchTeam( gentity_t *ent ) +{ + MatchTeam( ent, ent->moverState, level.time ); +} + + +/*QUAKED func_door (0 .5 .8) ? START_OPEN x CRUSHER +TOGGLE wait in both the start and end states for a trigger event. +START_OPEN the door to moves to its destination when spawned, and operate in reverse. It is used to temporarily or permanently close off an area when triggered (not useful for touch or takedamage doors). +NOMONSTER monsters will not trigger this door + +"model2" .md3 model to also draw +"angle" determines the opening direction +"targetname" if set, no touch field will be spawned and a remote button or trigger field activates the door. +"speed" movement speed (100 default) +"wait" wait before returning (3 default, -1 = never return) +"lip" lip remaining at end of move (8 default) +"dmg" damage to inflict when blocked (2 default) +"color" constantLight color +"light" constantLight radius +"health" if set, the door must be shot open +*/ +void SP_func_door( gentity_t *ent ) +{ + vec3_t abs_movedir; + float distance; + vec3_t size; + float lip; + char *s; + + G_SpawnString( "sound2to1", "sound/movers/doors/dr1_strt.wav", &s ); + ent->sound2to1 = G_SoundIndex( s ); + G_SpawnString( "sound1to2", "sound/movers/doors/dr1_strt.wav", &s ); + ent->sound1to2 = G_SoundIndex( s ); + + G_SpawnString( "soundPos2", "sound/movers/doors/dr1_end.wav", &s ); + ent->soundPos2 = G_SoundIndex( s ); + G_SpawnString( "soundPos1", "sound/movers/doors/dr1_end.wav", &s ); + ent->soundPos1 = G_SoundIndex( s ); + + ent->blocked = Blocked_Door; + + // default speed of 400 + if( !ent->speed ) + ent->speed = 400; + + // default wait of 2 seconds + if( !ent->wait ) + ent->wait = 2; + + ent->wait *= 1000; + + // default lip of 8 units + G_SpawnFloat( "lip", "8", &lip ); + + // default damage of 2 points + G_SpawnInt( "dmg", "2", &ent->damage ); + + // first position at start + VectorCopy( ent->s.origin, ent->pos1 ); + + // calculate second position + trap_SetBrushModel( ent, ent->model ); + G_SetMovedir( ent->s.angles, ent->movedir ); + abs_movedir[ 0 ] = fabs( ent->movedir[ 0 ] ); + abs_movedir[ 1 ] = fabs( ent->movedir[ 1 ] ); + abs_movedir[ 2 ] = fabs( ent->movedir[ 2 ] ); + VectorSubtract( ent->r.maxs, ent->r.mins, size ); + distance = DotProduct( abs_movedir, size ) - lip; + VectorMA( ent->pos1, distance, ent->movedir, ent->pos2 ); + + // if "start_open", reverse position 1 and 2 + if( ent->spawnflags & 1 ) + { + vec3_t temp; + + VectorCopy( ent->pos2, temp ); + VectorCopy( ent->s.origin, ent->pos2 ); + VectorCopy( temp, ent->pos1 ); + } + + InitMover( ent ); + + ent->nextthink = level.time + FRAMETIME; + + if( !( ent->flags & FL_TEAMSLAVE ) ) + { + int health; + + G_SpawnInt( "health", "0", &health ); + if( health ) + ent->takedamage = qtrue; + + if( ent->targetname || health ) + { + // non touch/shoot doors + ent->think = Think_MatchTeam; + } + else + ent->think = Think_SpawnNewDoorTrigger; + } +} + +/*QUAKED func_door_rotating (0 .5 .8) START_OPEN CRUSHER REVERSE TOGGLE X_AXIS Y_AXIS + * This is the rotating door... just as the name suggests it's a door that rotates + * START_OPEN the door to moves to its destination when spawned, and operate in reverse. + * REVERSE if you want the door to open in the other direction, use this switch. + * TOGGLE wait in both the start and end states for a trigger event. + * X_AXIS open on the X-axis instead of the Z-axis + * Y_AXIS open on the Y-axis instead of the Z-axis + * + * You need to have an origin brush as part of this entity. The center of that brush will be + * the point around which it is rotated. It will rotate around the Z axis by default. You can + * check either the X_AXIS or Y_AXIS box to change that. + * + * "model2" .md3 model to also draw + * "distance" how many degrees the door will open + * "speed" how fast the door will open (degrees/second) + * "color" constantLight color + * "light" constantLight radius + * */ + +void SP_func_door_rotating( gentity_t *ent ) +{ + char *s; + + G_SpawnString( "sound2to1", "sound/movers/doors/dr1_strt.wav", &s ); + ent->sound2to1 = G_SoundIndex( s ); + G_SpawnString( "sound1to2", "sound/movers/doors/dr1_strt.wav", &s ); + ent->sound1to2 = G_SoundIndex( s ); + + G_SpawnString( "soundPos2", "sound/movers/doors/dr1_end.wav", &s ); + ent->soundPos2 = G_SoundIndex( s ); + G_SpawnString( "soundPos1", "sound/movers/doors/dr1_end.wav", &s ); + ent->soundPos1 = G_SoundIndex( s ); + + ent->blocked = Blocked_Door; + + //default speed of 120 + if( !ent->speed ) + ent->speed = 120; + + // if speed is negative, positize it and add reverse flag + if( ent->speed < 0 ) + { + ent->speed *= -1; + ent->spawnflags |= 8; + } + + // default of 2 seconds + if( !ent->wait ) + ent->wait = 2; + + ent->wait *= 1000; + + // set the axis of rotation + VectorClear( ent->movedir ); + VectorClear( ent->s.angles ); + + if( ent->spawnflags & 32 ) + ent->movedir[ 2 ] = 1.0; + else if( ent->spawnflags & 64 ) + ent->movedir[ 0 ] = 1.0; + else + ent->movedir[ 1 ] = 1.0; + + // reverse direction if necessary + if( ent->spawnflags & 8 ) + VectorNegate ( ent->movedir, ent->movedir ); + + // default distance of 90 degrees. This is something the mapper should not + // leave out, so we'll tell him if he does. + if( !ent->rotatorAngle ) + { + G_Printf( "%s at %s with no rotatorAngle set.\n", + ent->classname, vtos( ent->s.origin ) ); + + ent->rotatorAngle = 90.0; + } + + VectorCopy( ent->s.angles, ent->pos1 ); + trap_SetBrushModel( ent, ent->model ); + VectorMA( ent->pos1, ent->rotatorAngle, ent->movedir, ent->pos2 ); + + // if "start_open", reverse position 1 and 2 + if( ent->spawnflags & 1 ) + { + vec3_t temp; + + VectorCopy( ent->pos2, temp ); + VectorCopy( ent->s.angles, ent->pos2 ); + VectorCopy( temp, ent->pos1 ); + VectorNegate( ent->movedir, ent->movedir ); + } + + // set origin + VectorCopy( ent->s.origin, ent->s.pos.trBase ); + VectorCopy( ent->s.pos.trBase, ent->r.currentOrigin ); + + InitRotator( ent ); + + ent->nextthink = level.time + FRAMETIME; + + if( !( ent->flags & FL_TEAMSLAVE ) ) + { + int health; + + G_SpawnInt( "health", "0", &health ); + + if( health ) + ent->takedamage = qtrue; + + if( ent->targetname || health ) + { + // non touch/shoot doors + ent->think = Think_MatchTeam; + } + else + ent->think = Think_SpawnNewDoorTrigger; + } +} + +/*QUAKED func_door_model (0 .5 .8) ? START_OPEN +TOGGLE wait in both the start and end states for a trigger event. +START_OPEN the door to moves to its destination when spawned, and operate in reverse. It is used to temporarily or permanently close off an area when triggered (not useful for touch or takedamage doors). +NOMONSTER monsters will not trigger this door + +"model2" .md3 model to also draw +"targetname" if set, no touch field will be spawned and a remote button or trigger field activates the door. +"speed" movement speed (100 default) +"wait" wait before returning (3 default, -1 = never return) +"color" constantLight color +"light" constantLight radius +"health" if set, the door must be shot open +*/ +void SP_func_door_model( gentity_t *ent ) +{ + char *s; + float light; + vec3_t color; + qboolean lightSet, colorSet; + char *sound; + gentity_t *clipBrush; + + G_SpawnString( "sound2to1", "sound/movers/doors/dr1_strt.wav", &s ); + ent->sound2to1 = G_SoundIndex( s ); + G_SpawnString( "sound1to2", "sound/movers/doors/dr1_strt.wav", &s ); + ent->sound1to2 = G_SoundIndex( s ); + + G_SpawnString( "soundPos2", "sound/movers/doors/dr1_end.wav", &s ); + ent->soundPos2 = G_SoundIndex( s ); + G_SpawnString( "soundPos1", "sound/movers/doors/dr1_end.wav", &s ); + ent->soundPos1 = G_SoundIndex( s ); + + //default speed of 100ms + if( !ent->speed ) + ent->speed = 200; + + //default wait of 2 seconds + if( ent->wait <= 0 ) + ent->wait = 2; + + ent->wait *= 1000; + + //brush model + clipBrush = ent->clipBrush = G_Spawn( ); + clipBrush->model = ent->model; + trap_SetBrushModel( clipBrush, clipBrush->model ); + clipBrush->s.eType = ET_INVISIBLE; + trap_LinkEntity( clipBrush ); + + //copy the bounds back from the clipBrush so the + //triggers can be made + VectorCopy( clipBrush->r.absmin, ent->r.absmin ); + VectorCopy( clipBrush->r.absmax, ent->r.absmax ); + VectorCopy( clipBrush->r.mins, ent->r.mins ); + VectorCopy( clipBrush->r.maxs, ent->r.maxs ); + + G_SpawnVector( "modelOrigin", "0 0 0", ent->s.origin ); + + G_SpawnVector( "scale", "1 1 1", ent->s.origin2 ); + + // if the "model2" key is set, use a seperate model + // for drawing, but clip against the brushes + if( !ent->model2 ) + G_Printf( S_COLOR_YELLOW "WARNING: func_door_model %d spawned with no model2 key\n", ent->s.number ); + else + ent->s.modelindex = G_ModelIndex( ent->model2 ); + + // if the "loopsound" key is set, use a constant looping sound when moving + if( G_SpawnString( "noise", "100", &sound ) ) + ent->s.loopSound = G_SoundIndex( sound ); + + // if the "color" or "light" keys are set, setup constantLight + lightSet = G_SpawnFloat( "light", "100", &light ); + colorSet = G_SpawnVector( "color", "1 1 1", color ); + + if( lightSet || colorSet ) + { + int r, g, b, i; + + r = color[ 0 ] * 255; + if( r > 255 ) + r = 255; + + g = color[ 1 ] * 255; + if( g > 255 ) + g = 255; + + b = color[ 2 ] * 255; + if( b > 255 ) + b = 255; + + i = light / 4; + if( i > 255 ) + i = 255; + + ent->s.constantLight = r | ( g << 8 ) | ( b << 16 ) | ( i << 24 ); + } + + ent->use = Use_BinaryMover; + + ent->moverState = MODEL_POS1; + ent->s.eType = ET_MODELDOOR; + VectorCopy( ent->s.origin, ent->s.pos.trBase ); + ent->s.pos.trType = TR_STATIONARY; + ent->s.pos.trTime = 0; + ent->s.pos.trDuration = 0; + VectorClear( ent->s.pos.trDelta ); + VectorCopy( ent->s.angles, ent->s.apos.trBase ); + ent->s.apos.trType = TR_STATIONARY; + ent->s.apos.trTime = 0; + ent->s.apos.trDuration = 0; + VectorClear( ent->s.apos.trDelta ); + + ent->s.powerups = (int)ent->animation[ 0 ]; //first frame + ent->s.weapon = abs( (int)ent->animation[ 1 ] ); //number of frames + + //must be at least one frame -- mapper has forgotten animation key + if( ent->s.weapon == 0 ) + ent->s.weapon = 1; + + ent->s.torsoAnim = ent->s.weapon * ( 1000.0f / ent->speed ); //framerate + + trap_LinkEntity( ent ); + + if( !( ent->flags & FL_TEAMSLAVE ) ) + { + int health; + + G_SpawnInt( "health", "0", &health ); + if( health ) + ent->takedamage = qtrue; + + if( !( ent->targetname || health ) ) + { + ent->nextthink = level.time + FRAMETIME; + ent->think = Think_SpawnNewDoorTrigger; + } + } +} + +/* +=============================================================================== + +PLAT + +=============================================================================== +*/ + +/* +============== +Touch_Plat + +Don't allow decent if a living player is on it +=============== +*/ +void Touch_Plat( gentity_t *ent, gentity_t *other, trace_t *trace ) +{ + // DONT_WAIT + if( ent->spawnflags & 1 ) + return; + + if( !other->client || other->client->ps.stats[ STAT_HEALTH ] <= 0 ) + return; + + // delay return-to-pos1 by one second + if( ent->moverState == MOVER_POS2 ) + ent->nextthink = level.time + 1000; +} + +/* +============== +Touch_PlatCenterTrigger + +If the plat is at the bottom position, start it going up +=============== +*/ +void Touch_PlatCenterTrigger( gentity_t *ent, gentity_t *other, trace_t *trace ) +{ + if( !other->client ) + return; + + if( ent->parent->moverState == MOVER_POS1 ) + Use_BinaryMover( ent->parent, ent, other ); +} + + +/* +================ +SpawnPlatTrigger + +Spawn a trigger in the middle of the plat's low position +Elevator cars require that the trigger extend through the entire low position, +not just sit on top of it. +================ +*/ +void SpawnPlatTrigger( gentity_t *ent ) +{ + gentity_t *trigger; + vec3_t tmin, tmax; + + // the middle trigger will be a thin trigger just + // above the starting position + trigger = G_Spawn( ); + trigger->classname = "plat_trigger"; + trigger->touch = Touch_PlatCenterTrigger; + trigger->r.contents = CONTENTS_TRIGGER; + trigger->parent = ent; + + tmin[ 0 ] = ent->pos1[ 0 ] + ent->r.mins[ 0 ] + 33; + tmin[ 1 ] = ent->pos1[ 1 ] + ent->r.mins[ 1 ] + 33; + tmin[ 2 ] = ent->pos1[ 2 ] + ent->r.mins[ 2 ]; + + tmax[ 0 ] = ent->pos1[ 0 ] + ent->r.maxs[ 0 ] - 33; + tmax[ 1 ] = ent->pos1[ 1 ] + ent->r.maxs[ 1 ] - 33; + tmax[ 2 ] = ent->pos1[ 2 ] + ent->r.maxs[ 2 ] + 8; + + if( tmax[ 0 ] <= tmin[ 0 ] ) + { + tmin[ 0 ] = ent->pos1[ 0 ] + ( ent->r.mins[ 0 ] + ent->r.maxs[ 0 ] ) * 0.5; + tmax[ 0 ] = tmin[ 0 ] + 1; + } + + if( tmax[ 1 ] <= tmin[ 1 ] ) + { + tmin[ 1 ] = ent->pos1[ 1 ] + ( ent->r.mins[ 1 ] + ent->r.maxs[ 1 ] ) * 0.5; + tmax[ 1 ] = tmin[ 1 ] + 1; + } + + VectorCopy( tmin, trigger->r.mins ); + VectorCopy( tmax, trigger->r.maxs ); + + trap_LinkEntity( trigger ); +} + + +/*QUAKED func_plat (0 .5 .8) ? +Plats are always drawn in the extended position so they will light correctly. + +"lip" default 8, protrusion above rest position +"height" total height of movement, defaults to model height +"speed" overrides default 200. +"dmg" overrides default 2 +"model2" .md3 model to also draw +"color" constantLight color +"light" constantLight radius +*/ +void SP_func_plat( gentity_t *ent ) +{ + float lip, height; + char *s; + + G_SpawnString( "sound2to1", "sound/movers/plats/pt1_strt.wav", &s ); + ent->sound2to1 = G_SoundIndex( s ); + G_SpawnString( "sound1to2", "sound/movers/plats/pt1_strt.wav", &s ); + ent->sound1to2 = G_SoundIndex( s ); + + G_SpawnString( "soundPos2", "sound/movers/plats/pt1_end.wav", &s ); + ent->soundPos2 = G_SoundIndex( s ); + G_SpawnString( "soundPos1", "sound/movers/plats/pt1_end.wav", &s ); + ent->soundPos1 = G_SoundIndex( s ); + + VectorClear( ent->s.angles ); + + G_SpawnFloat( "speed", "200", &ent->speed ); + G_SpawnInt( "dmg", "2", &ent->damage ); + G_SpawnFloat( "wait", "1", &ent->wait ); + G_SpawnFloat( "lip", "8", &lip ); + + ent->wait = 1000; + + // create second position + trap_SetBrushModel( ent, ent->model ); + + if( !G_SpawnFloat( "height", "0", &height ) ) + height = ( ent->r.maxs[ 2 ] - ent->r.mins[ 2 ] ) - lip; + + // pos1 is the rest (bottom) position, pos2 is the top + VectorCopy( ent->s.origin, ent->pos2 ); + VectorCopy( ent->pos2, ent->pos1 ); + ent->pos1[ 2 ] -= height; + + InitMover( ent ); + + // touch function keeps the plat from returning while + // a live player is standing on it + ent->touch = Touch_Plat; + + ent->blocked = Blocked_Door; + + ent->parent = ent; // so it can be treated as a door + + // spawn the trigger if one hasn't been custom made + if( !ent->targetname ) + SpawnPlatTrigger( ent ); +} + + +/* +=============================================================================== + +BUTTON + +=============================================================================== +*/ + +/* +============== +Touch_Button + +=============== +*/ +void Touch_Button( gentity_t *ent, gentity_t *other, trace_t *trace ) +{ + if( !other->client ) + return; + + if( ent->moverState == MOVER_POS1 ) + Use_BinaryMover( ent, other, other ); +} + + +/*QUAKED func_button (0 .5 .8) ? +When a button is touched, it moves some distance in the direction of it's angle, triggers all of it's targets, waits some time, then returns to it's original position where it can be triggered again. + +"model2" .md3 model to also draw +"angle" determines the opening direction +"target" all entities with a matching targetname will be used +"speed" override the default 40 speed +"wait" override the default 1 second wait (-1 = never return) +"lip" override the default 4 pixel lip remaining at end of move +"health" if set, the button must be killed instead of touched +"color" constantLight color +"light" constantLight radius +*/ +void SP_func_button( gentity_t *ent ) +{ + vec3_t abs_movedir; + float distance; + vec3_t size; + float lip; + char *s; + + G_SpawnString( "sound1to2", "sound/movers/switches/button1.wav", &s ); + ent->sound1to2 = G_SoundIndex( s ); + + if( !ent->speed ) + ent->speed = 40; + + if( !ent->wait ) + ent->wait = 1; + + ent->wait *= 1000; + + // first position + VectorCopy( ent->s.origin, ent->pos1 ); + + // calculate second position + trap_SetBrushModel( ent, ent->model ); + + G_SpawnFloat( "lip", "4", &lip ); + + G_SetMovedir( ent->s.angles, ent->movedir ); + abs_movedir[ 0 ] = fabs( ent->movedir[ 0 ] ); + abs_movedir[ 1 ] = fabs( ent->movedir[ 1 ] ); + abs_movedir[ 2 ] = fabs( ent->movedir[ 2 ] ); + VectorSubtract( ent->r.maxs, ent->r.mins, size ); + distance = abs_movedir[ 0 ] * size[ 0 ] + abs_movedir[ 1 ] * size[ 1 ] + abs_movedir[ 2 ] * size[ 2 ] - lip; + VectorMA( ent->pos1, distance, ent->movedir, ent->pos2 ); + + if( ent->health ) + { + // shootable button + ent->takedamage = qtrue; + } + else + { + // touchable button + ent->touch = Touch_Button; + } + + InitMover( ent ); +} + + + +/* +=============================================================================== + +TRAIN + +=============================================================================== +*/ + + +#define TRAIN_START_OFF 1 +#define TRAIN_BLOCK_STOPS 2 + +/* +=============== +Think_BeginMoving + +The wait time at a corner has completed, so start moving again +=============== +*/ +void Think_BeginMoving( gentity_t *ent ) +{ + ent->s.pos.trTime = level.time; + ent->s.pos.trType = TR_LINEAR_STOP; +} + +/* +=============== +Reached_Train +=============== +*/ +void Reached_Train( gentity_t *ent ) +{ + gentity_t *next; + float speed; + vec3_t move; + float length; + + // copy the apropriate values + next = ent->nextTrain; + if( !next || !next->nextTrain ) + return; // just stop + + // fire all other targets + G_UseTargets( next, NULL ); + + // set the new trajectory + ent->nextTrain = next->nextTrain; + VectorCopy( next->s.origin, ent->pos1 ); + VectorCopy( next->nextTrain->s.origin, ent->pos2 ); + + // if the path_corner has a speed, use that + if( next->speed ) + { + speed = next->speed; + } + else + { + // otherwise use the train's speed + speed = ent->speed; + } + + if( speed < 1 ) + speed = 1; + + ent->lastSpeed = speed; + + // calculate duration + VectorSubtract( ent->pos2, ent->pos1, move ); + length = VectorLength( move ); + + ent->s.pos.trDuration = length * 1000 / speed; + + // looping sound + ent->s.loopSound = next->soundLoop; + + // start it going + SetMoverState( ent, MOVER_1TO2, level.time ); + + if( ent->spawnflags & TRAIN_START_OFF ) + { + ent->s.pos.trType = TR_STATIONARY; + return; + } + + // if there is a "wait" value on the target, don't start moving yet + if( next->wait ) + { + ent->nextthink = level.time + next->wait * 1000; + ent->think = Think_BeginMoving; + ent->s.pos.trType = TR_STATIONARY; + } +} + +/* +================ +Start_Train +================ +*/ +void Start_Train( gentity_t *ent, gentity_t *other, gentity_t *activator ) +{ + vec3_t move; + + //recalculate duration as the mover is highly + //unlikely to be right on a path_corner + VectorSubtract( ent->pos2, ent->pos1, move ); + ent->s.pos.trDuration = VectorLength( move ) * 1000 / ent->lastSpeed; + SetMoverState( ent, MOVER_1TO2, level.time ); + + ent->spawnflags &= ~TRAIN_START_OFF; +} + +/* +================ +Stop_Train +================ +*/ +void Stop_Train( gentity_t *ent, gentity_t *other, gentity_t *activator ) +{ + vec3_t origin; + + //get current origin + BG_EvaluateTrajectory( &ent->s.pos, level.time, origin ); + VectorCopy( origin, ent->pos1 ); + SetMoverState( ent, MOVER_POS1, level.time ); + + ent->spawnflags |= TRAIN_START_OFF; +} + +/* +================ +Use_Train +================ +*/ +void Use_Train( gentity_t *ent, gentity_t *other, gentity_t *activator ) +{ + if( ent->spawnflags & TRAIN_START_OFF ) + { + //train is currently not moving so start it + Start_Train( ent, other, activator ); + } + else + { + //train is moving so stop it + Stop_Train( ent, other, activator ); + } +} + +/* +=============== +Think_SetupTrainTargets + +Link all the corners together +=============== +*/ +void Think_SetupTrainTargets( gentity_t *ent ) +{ + gentity_t *path, *next, *start; + + ent->nextTrain = G_Find( NULL, FOFS( targetname ), ent->target ); + + if( !ent->nextTrain ) + { + G_Printf( "func_train at %s with an unfound target\n", + vtos( ent->r.absmin ) ); + return; + } + + start = NULL; + for( path = ent->nextTrain; path != start; path = next ) + { + if( !start ) + start = path; + + if( !path->target ) + { + G_Printf( "Train corner at %s without a target\n", + vtos( path->s.origin ) ); + return; + } + + // find a path_corner among the targets + // there may also be other targets that get fired when the corner + // is reached + next = NULL; + do + { + next = G_Find( next, FOFS( targetname ), path->target ); + + if( !next ) + { + G_Printf( "Train corner at %s without a target path_corner\n", + vtos( path->s.origin ) ); + return; + } + } while( strcmp( next->classname, "path_corner" ) ); + + path->nextTrain = next; + } + + // start the train moving from the first corner + Reached_Train( ent ); +} + + + +/*QUAKED path_corner (.5 .3 0) (-8 -8 -8) (8 8 8) +Train path corners. +Target: next path corner and other targets to fire +"speed" speed to move to the next corner +"wait" seconds to wait before behining move to next corner +*/ +void SP_path_corner( gentity_t *self ) +{ + if( !self->targetname ) + { + G_Printf( "path_corner with no targetname at %s\n", vtos( self->s.origin ) ); + G_FreeEntity( self ); + return; + } + // path corners don't need to be linked in +} + +/* +================ +Blocked_Train +================ +*/ +void Blocked_Train( gentity_t *self, gentity_t *other ) +{ + if( self->spawnflags & TRAIN_BLOCK_STOPS ) + Stop_Train( self, other, other ); + else + { + if( !other->client ) + { + //whatever is blocking the train isn't a client + + //KILL!!1!!! + G_Damage( other, self, self, NULL, NULL, 10000, 0, MOD_CRUSH ); + + //buildables need to be handled differently since even when + //dealth fatal amounts of damage they won't instantly become non-solid + if( other->s.eType == ET_BUILDABLE && other->spawned ) + { + vec3_t dir; + gentity_t *tent; + + if( other->biteam == BIT_ALIENS ) + { + VectorCopy( other->s.origin2, dir ); + tent = G_TempEntity( other->s.origin, EV_ALIEN_BUILDABLE_EXPLOSION ); + tent->s.eventParm = DirToByte( dir ); + } + else if( other->biteam == BIT_HUMANS ) + { + VectorSet( dir, 0.0f, 0.0f, 1.0f ); + tent = G_TempEntity( other->s.origin, EV_HUMAN_BUILDABLE_EXPLOSION ); + tent->s.eventParm = DirToByte( dir ); + } + } + + //if it's still around free it + if( other ) + G_FreeEntity( other ); + + return; + } + + G_Damage( other, self, self, NULL, NULL, 10000, 0, MOD_CRUSH ); + } +} + + +/*QUAKED func_train (0 .5 .8) ? START_ON TOGGLE BLOCK_STOPS +A train is a mover that moves between path_corner target points. +Trains MUST HAVE AN ORIGIN BRUSH. +The train spawns at the first target it is pointing at. +"model2" .md3 model to also draw +"speed" default 100 +"dmg" default 2 +"noise" looping sound to play when the train is in motion +"target" next path corner +"color" constantLight color +"light" constantLight radius +*/ +void SP_func_train( gentity_t *self ) +{ + VectorClear( self->s.angles ); + + if( self->spawnflags & TRAIN_BLOCK_STOPS ) + self->damage = 0; + else if( !self->damage ) + self->damage = 2; + + if( !self->speed ) + self->speed = 100; + + if( !self->target ) + { + G_Printf( "func_train without a target at %s\n", vtos( self->r.absmin ) ); + G_FreeEntity( self ); + return; + } + + trap_SetBrushModel( self, self->model ); + InitMover( self ); + + self->reached = Reached_Train; + self->use = Use_Train; + self->blocked = Blocked_Train; + + // start trains on the second frame, to make sure their targets have had + // a chance to spawn + self->nextthink = level.time + FRAMETIME; + self->think = Think_SetupTrainTargets; +} + +/* +=============================================================================== + +STATIC + +=============================================================================== +*/ + + +/*QUAKED func_static (0 .5 .8) ? +A bmodel that just sits there, doing nothing. Can be used for conditional walls and models. +"model2" .md3 model to also draw +"color" constantLight color +"light" constantLight radius +*/ +void SP_func_static( gentity_t *ent ) +{ + trap_SetBrushModel( ent, ent->model ); + InitMover( ent ); + VectorCopy( ent->s.origin, ent->s.pos.trBase ); + VectorCopy( ent->s.origin, ent->r.currentOrigin ); +} + + +/* +=============================================================================== + +ROTATING + +=============================================================================== +*/ + + +/*QUAKED func_rotating (0 .5 .8) ? START_ON - X_AXIS Y_AXIS +You need to have an origin brush as part of this entity. The center of that brush will be +the point around which it is rotated. It will rotate around the Z axis by default. You can +check either the X_AXIS or Y_AXIS box to change that. + +"model2" .md3 model to also draw +"speed" determines how fast it moves; default value is 100. +"dmg" damage to inflict when blocked (2 default) +"color" constantLight color +"light" constantLight radius +*/ +void SP_func_rotating( gentity_t *ent ) +{ + if( !ent->speed ) + ent->speed = 100; + + // set the axis of rotation + ent->s.apos.trType = TR_LINEAR; + + if( ent->spawnflags & 4 ) + ent->s.apos.trDelta[ 2 ] = ent->speed; + else if( ent->spawnflags & 8 ) + ent->s.apos.trDelta[ 0 ] = ent->speed; + else + ent->s.apos.trDelta[ 1 ] = ent->speed; + + if( !ent->damage ) + ent->damage = 2; + + trap_SetBrushModel( ent, ent->model ); + InitMover( ent ); + + VectorCopy( ent->s.origin, ent->s.pos.trBase ); + VectorCopy( ent->s.pos.trBase, ent->r.currentOrigin ); + VectorCopy( ent->s.apos.trBase, ent->r.currentAngles ); + + trap_LinkEntity( ent ); +} + + +/* +=============================================================================== + +BOBBING + +=============================================================================== +*/ + + +/*QUAKED func_bobbing (0 .5 .8) ? X_AXIS Y_AXIS +Normally bobs on the Z axis +"model2" .md3 model to also draw +"height" amplitude of bob (32 default) +"speed" seconds to complete a bob cycle (4 default) +"phase" the 0.0 to 1.0 offset in the cycle to start at +"dmg" damage to inflict when blocked (2 default) +"color" constantLight color +"light" constantLight radius +*/ +void SP_func_bobbing( gentity_t *ent ) +{ + float height; + float phase; + + G_SpawnFloat( "speed", "4", &ent->speed ); + G_SpawnFloat( "height", "32", &height ); + G_SpawnInt( "dmg", "2", &ent->damage ); + G_SpawnFloat( "phase", "0", &phase ); + + trap_SetBrushModel( ent, ent->model ); + InitMover( ent ); + + VectorCopy( ent->s.origin, ent->s.pos.trBase ); + VectorCopy( ent->s.origin, ent->r.currentOrigin ); + + ent->s.pos.trDuration = ent->speed * 1000; + ent->s.pos.trTime = ent->s.pos.trDuration * phase; + ent->s.pos.trType = TR_SINE; + + // set the axis of bobbing + if( ent->spawnflags & 1 ) + ent->s.pos.trDelta[ 0 ] = height; + else if( ent->spawnflags & 2 ) + ent->s.pos.trDelta[ 1 ] = height; + else + ent->s.pos.trDelta[ 2 ] = height; +} + +/* +=============================================================================== + +PENDULUM + +=============================================================================== +*/ + + +/*QUAKED func_pendulum (0 .5 .8) ? +You need to have an origin brush as part of this entity. +Pendulums always swing north / south on unrotated models. Add an angles field to the model to allow rotation in other directions. +Pendulum frequency is a physical constant based on the length of the beam and gravity. +"model2" .md3 model to also draw +"speed" the number of degrees each way the pendulum swings, (30 default) +"phase" the 0.0 to 1.0 offset in the cycle to start at +"dmg" damage to inflict when blocked (2 default) +"color" constantLight color +"light" constantLight radius +*/ +void SP_func_pendulum( gentity_t *ent ) +{ + float freq; + float length; + float phase; + float speed; + + G_SpawnFloat( "speed", "30", &speed ); + G_SpawnInt( "dmg", "2", &ent->damage ); + G_SpawnFloat( "phase", "0", &phase ); + + trap_SetBrushModel( ent, ent->model ); + + // find pendulum length + length = fabs( ent->r.mins[ 2 ] ); + + if( length < 8 ) + length = 8; + + freq = 1 / ( M_PI * 2 ) * sqrt( g_gravity.value / ( 3 * length ) ); + + ent->s.pos.trDuration = ( 1000 / freq ); + + InitMover( ent ); + + VectorCopy( ent->s.origin, ent->s.pos.trBase ); + VectorCopy( ent->s.origin, ent->r.currentOrigin ); + + VectorCopy( ent->s.angles, ent->s.apos.trBase ); + + ent->s.apos.trDuration = 1000 / freq; + ent->s.apos.trTime = ent->s.apos.trDuration * phase; + ent->s.apos.trType = TR_SINE; + ent->s.apos.trDelta[ 2 ] = speed; +} diff --git a/src/game/g_physics.c b/src/game/g_physics.c new file mode 100644 index 0000000..58c6487 --- /dev/null +++ b/src/game/g_physics.c @@ -0,0 +1,167 @@ +/* +=========================================================================== +Copyright (C) 1999-2005 Id Software, Inc. +Copyright (C) 2000-2006 Tim Angus + +This file is part of Tremulous. + +Tremulous is free software; you can redistribute it +and/or modify it under the terms of the GNU General Public License as +published by the Free Software Foundation; either version 2 of the License, +or (at your option) any later version. + +Tremulous is distributed in the hope that it will be +useful, but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with Tremulous; if not, write to the Free Software +Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA +=========================================================================== +*/ + +#include "g_local.h" + +/* +================ +G_Bounce + +================ +*/ +static void G_Bounce( gentity_t *ent, trace_t *trace ) +{ + vec3_t velocity; + float dot; + int hitTime; + float minNormal; + qboolean invert = qfalse; + + // reflect the velocity on the trace plane + hitTime = level.previousTime + ( level.time - level.previousTime ) * trace->fraction; + BG_EvaluateTrajectoryDelta( &ent->s.pos, hitTime, velocity ); + dot = DotProduct( velocity, trace->plane.normal ); + VectorMA( velocity, -2*dot, trace->plane.normal, ent->s.pos.trDelta ); + + if( ent->s.eType == ET_BUILDABLE ) + { + minNormal = BG_FindMinNormalForBuildable( ent->s.modelindex ); + invert = BG_FindInvertNormalForBuildable( ent->s.modelindex ); + } + else + minNormal = 0.707f; + + // cut the velocity to keep from bouncing forever + if( ( trace->plane.normal[ 2 ] >= minNormal || + ( invert && trace->plane.normal[ 2 ] <= -minNormal ) ) && + trace->entityNum == ENTITYNUM_WORLD ) + VectorScale( ent->s.pos.trDelta, ent->physicsBounce, ent->s.pos.trDelta ); + else + VectorScale( ent->s.pos.trDelta, 0.3f, ent->s.pos.trDelta ); + + if( VectorLength( ent->s.pos.trDelta ) < 10 ) + { + VectorMA( trace->endpos, 0.5f, trace->plane.normal, trace->endpos ); // make sure it is off ground + G_SetOrigin( ent, trace->endpos ); + ent->s.groundEntityNum = trace->entityNum; + VectorCopy( trace->plane.normal, ent->s.origin2 ); + VectorSet( ent->s.pos.trDelta, 0.0f, 0.0f, 0.0f ); + return; + } + + VectorCopy( ent->r.currentOrigin, ent->s.pos.trBase ); + VectorAdd( ent->r.currentOrigin, trace->plane.normal, ent->r.currentOrigin); + ent->s.pos.trTime = level.time; +} + +#define PHYSICS_TIME 200 + +/* +================ +G_Physics + +================ +*/ +void G_Physics( gentity_t *ent, int msec ) +{ + vec3_t origin; + trace_t tr; + int contents; + int mask; + + // if groundentity has been set to -1, it may have been pushed off an edge + if( ent->s.groundEntityNum == -1 ) + { + if( ent->s.eType == ET_BUILDABLE ) + { + if( ent->s.pos.trType != BG_FindTrajectoryForBuildable( ent->s.modelindex ) ) + { + ent->s.pos.trType = BG_FindTrajectoryForBuildable( ent->s.modelindex ); + ent->s.pos.trTime = level.time; + } + } + else if( ent->s.pos.trType != TR_GRAVITY ) + { + ent->s.pos.trType = TR_GRAVITY; + ent->s.pos.trTime = level.time; + } + } + + // trace a line from the previous position to the current position + if( ent->clipmask ) + mask = ent->clipmask; + else + mask = MASK_PLAYERSOLID & ~CONTENTS_BODY;//MASK_SOLID; + + if( ent->s.pos.trType == TR_STATIONARY ) + { + // check think function + G_RunThink( ent ); + + //check floor infrequently + if( ent->nextPhysicsTime < level.time ) + { + VectorCopy( ent->r.currentOrigin, origin ); + + VectorMA( origin, -2.0f, ent->s.origin2, origin ); + + trap_Trace( &tr, ent->r.currentOrigin, ent->r.mins, ent->r.maxs, origin, ent->s.number, mask ); + + if( tr.fraction == 1.0f ) + ent->s.groundEntityNum = -1; + + ent->nextPhysicsTime = level.time + PHYSICS_TIME; + } + + return; + } + + // get current position + BG_EvaluateTrajectory( &ent->s.pos, level.time, origin ); + + trap_Trace( &tr, ent->r.currentOrigin, ent->r.mins, ent->r.maxs, origin, ent->s.number, mask ); + + VectorCopy( tr.endpos, ent->r.currentOrigin ); + + if( tr.startsolid ) + tr.fraction = 0; + + trap_LinkEntity( ent ); // FIXME: avoid this for stationary? + + // check think function + G_RunThink( ent ); + + if( tr.fraction == 1.0f ) + return; + + // if it is in a nodrop volume, remove it + contents = trap_PointContents( ent->r.currentOrigin, -1 ); + if( contents & CONTENTS_NODROP ) + { + G_FreeEntity( ent ); + return; + } + + G_Bounce( ent, &tr ); +} + diff --git a/src/game/g_ptr.c b/src/game/g_ptr.c new file mode 100644 index 0000000..e102183 --- /dev/null +++ b/src/game/g_ptr.c @@ -0,0 +1,143 @@ +/* +=========================================================================== +Copyright (C) 1999-2005 Id Software, Inc. +Copyright (C) 2000-2006 Tim Angus + +This file is part of Tremulous. + +Tremulous is free software; you can redistribute it +and/or modify it under the terms of the GNU General Public License as +published by the Free Software Foundation; either version 2 of the License, +or (at your option) any later version. + +Tremulous is distributed in the hope that it will be +useful, but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with Tremulous; if not, write to the Free Software +Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA +=========================================================================== +*/ + +// g_ptr.c -- post timeout restoration handling + +#include "g_local.h" + +static connectionRecord_t connections[ MAX_CLIENTS ]; + +/* +=============== +G_CheckForUniquePTRC + +Callback to detect ptrc clashes +=============== +*/ +static qboolean G_CheckForUniquePTRC( int code ) +{ + int i; + + if( code == 0 ) + return qfalse; + + for( i = 0; i < MAX_CLIENTS; i++ ) + { + if( connections[ i ].ptrCode == code ) + return qfalse; + } + + return qtrue; +} + +/* +=============== +G_UpdatePTRConnection + +Update the data in a connection record +=============== +*/ +void G_UpdatePTRConnection( gclient_t *client ) +{ + if( client && client->pers.connection ) + { + client->pers.connection->clientTeam = client->pers.teamSelection; + client->pers.connection->clientCredit = client->pers.credit; + client->pers.connection->clientScore = client->pers.score; + } +} + +/* +=============== +G_GenerateNewConnection + +Generates a new connection +=============== +*/ +connectionRecord_t *G_GenerateNewConnection( gclient_t *client ) +{ + int code = 0; + int i; + + // this should be really random + srand( trap_Milliseconds( ) ); + + // there is a very very small possibility that this + // will loop infinitely + do + { + code = rand( ); + } while( !G_CheckForUniquePTRC( code ) ); + + for( i = 0; i < MAX_CLIENTS; i++ ) + { + //found an unused slot + if( !connections[ i ].ptrCode ) + { + connections[ i ].ptrCode = code; + connections[ i ].clientNum = client->ps.clientNum; + client->pers.connection = &connections[ i ]; + G_UpdatePTRConnection( client ); + client->pers.connection->clientEnterTime = client->pers.enterTime; + + return &connections[ i ]; + } + } + + return NULL; +} + +/* +=============== +G_FindConnectionForCode + +Finds a connection for a given code +=============== +*/ +connectionRecord_t *G_FindConnectionForCode( int code ) +{ + int i; + + if( code == 0 ) + return NULL; + + for( i = 0; i < MAX_CLIENTS; i++ ) + { + if( connections[ i ].ptrCode == code ) + return &connections[ i ]; + } + + return NULL; +} + +/* +=============== +G_ResetPTRConnections + +Invalidate any existing codes +=============== +*/ +void G_ResetPTRConnections( void ) +{ + memset( connections, 0, sizeof( connectionRecord_t ) * MAX_CLIENTS ); +} diff --git a/src/game/g_public.h b/src/game/g_public.h new file mode 100644 index 0000000..fc6b7df --- /dev/null +++ b/src/game/g_public.h @@ -0,0 +1,262 @@ +/* +=========================================================================== +Copyright (C) 1999-2005 Id Software, Inc. +Copyright (C) 2000-2006 Tim Angus + +This file is part of Tremulous. + +Tremulous is free software; you can redistribute it +and/or modify it under the terms of the GNU General Public License as +published by the Free Software Foundation; either version 2 of the License, +or (at your option) any later version. + +Tremulous is distributed in the hope that it will be +useful, but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with Tremulous; if not, write to the Free Software +Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA +=========================================================================== +*/ + +// g_public.h -- game module information visible to server + +#define GAME_API_VERSION 8 + +// entity->svFlags +// the server does not know how to interpret most of the values +// in entityStates (level eType), so the game must explicitly flag +// special server behaviors +#define SVF_NOCLIENT 0x00000001 // don't send entity to clients, even if it has effects + +// TTimo +// https://zerowing.idsoftware.com/bugzilla/show_bug.cgi?id=551 +#define SVF_CLIENTMASK 0x00000002 + +#define SVF_BROADCAST 0x00000020 // send to all connected clients +#define SVF_PORTAL 0x00000040 // merge a second pvs at origin2 into snapshots +#define SVF_USE_CURRENT_ORIGIN 0x00000080 // entity->r.currentOrigin instead of entity->s.origin + // for link position (missiles and movers) +#define SVF_SINGLECLIENT 0x00000100 // only send to a single client (entityShared_t->singleClient) +#define SVF_NOSERVERINFO 0x00000200 // don't send CS_SERVERINFO updates to this client + // so that it can be updated for ping tools without + // lagging clients +#define SVF_CAPSULE 0x00000400 // use capsule for collision detection instead of bbox +#define SVF_NOTSINGLECLIENT 0x00000800 // send entity to everyone but one client + // (entityShared_t->singleClient) + +//=============================================================== + + +typedef struct { + entityState_t s; // communicated by server to clients + + qboolean linked; // qfalse if not in any good cluster + int linkcount; + + int svFlags; // SVF_NOCLIENT, SVF_BROADCAST, etc + int singleClient; // only send to this client when SVF_SINGLECLIENT is set + + qboolean bmodel; // if false, assume an explicit mins / maxs bounding box + // only set by trap_SetBrushModel + vec3_t mins, maxs; + int contents; // CONTENTS_TRIGGER, CONTENTS_SOLID, CONTENTS_BODY, etc + // a non-solid entity should set to 0 + + vec3_t absmin, absmax; // derived from mins/maxs and origin + rotation + + // currentOrigin will be used for all collision detection and world linking. + // it will not necessarily be the same as the trajectory evaluation for the current + // time, because each entity must be moved one at a time after time is advanced + // to avoid simultanious collision issues + vec3_t currentOrigin; + vec3_t currentAngles; + + // when a trace call is made and passEntityNum != ENTITYNUM_NONE, + // an ent will be excluded from testing if: + // ent->s.number == passEntityNum (don't interact with self) + // ent->s.ownerNum = passEntityNum (don't interact with your own missiles) + // entity[ent->s.ownerNum].ownerNum = passEntityNum (don't interact with other missiles from owner) + int ownerNum; +} entityShared_t; + + + +// the server looks at a sharedEntity, which is the start of the game's gentity_t structure +typedef struct { + entityState_t s; // communicated by server to clients + entityShared_t r; // shared by both the server system and game +} sharedEntity_t; + + + +//=============================================================== + +// +// system traps provided by the main engine +// +typedef enum { + //============== general Quake services ================== + + G_PRINT, // ( const char *string ); + // print message on the local console + + G_ERROR, // ( const char *string ); + // abort the game + + G_MILLISECONDS, // ( void ); + // get current time for profiling reasons + // this should NOT be used for any game related tasks, + // because it is not journaled + + // console variable interaction + G_CVAR_REGISTER, // ( vmCvar_t *vmCvar, const char *varName, const char *defaultValue, int flags ); + G_CVAR_UPDATE, // ( vmCvar_t *vmCvar ); + G_CVAR_SET, // ( const char *var_name, const char *value ); + G_CVAR_VARIABLE_INTEGER_VALUE, // ( const char *var_name ); + + G_CVAR_VARIABLE_STRING_BUFFER, // ( const char *var_name, char *buffer, int bufsize ); + + G_ARGC, // ( void ); + // ClientCommand and ServerCommand parameter access + + G_ARGV, // ( int n, char *buffer, int bufferLength ); + + G_FS_FOPEN_FILE, // ( const char *qpath, fileHandle_t *file, fsMode_t mode ); + G_FS_READ, // ( void *buffer, int len, fileHandle_t f ); + G_FS_WRITE, // ( const void *buffer, int len, fileHandle_t f ); + G_FS_FCLOSE_FILE, // ( fileHandle_t f ); + + G_SEND_CONSOLE_COMMAND, // ( const char *text ); + // add commands to the console as if they were typed in + // for map changing, etc + + + //=========== server specific functionality ============= + + G_LOCATE_GAME_DATA, // ( gentity_t *gEnts, int numGEntities, int sizeofGEntity_t, + // playerState_t *clients, int sizeofGameClient ); + // the game needs to let the server system know where and how big the gentities + // are, so it can look at them directly without going through an interface + + G_DROP_CLIENT, // ( int clientNum, const char *reason ); + // kick a client off the server with a message + + G_SEND_SERVER_COMMAND, // ( int clientNum, const char *fmt, ... ); + // reliably sends a command string to be interpreted by the given + // client. If clientNum is -1, it will be sent to all clients + + G_SET_CONFIGSTRING, // ( int num, const char *string ); + // config strings hold all the index strings, and various other information + // that is reliably communicated to all clients + // All of the current configstrings are sent to clients when + // they connect, and changes are sent to all connected clients. + // All confgstrings are cleared at each level start. + + G_GET_CONFIGSTRING, // ( int num, char *buffer, int bufferSize ); + + G_GET_USERINFO, // ( int num, char *buffer, int bufferSize ); + // userinfo strings are maintained by the server system, so they + // are persistant across level loads, while all other game visible + // data is completely reset + + G_SET_USERINFO, // ( int num, const char *buffer ); + + G_GET_SERVERINFO, // ( char *buffer, int bufferSize ); + // the serverinfo info string has all the cvars visible to server browsers + + G_SET_BRUSH_MODEL, // ( gentity_t *ent, const char *name ); + // sets mins and maxs based on the brushmodel name + + G_TRACE, // ( trace_t *results, const vec3_t start, const vec3_t mins, const vec3_t maxs, const vec3_t end, int passEntityNum, int contentmask ); + // collision detection against all linked entities + + G_POINT_CONTENTS, // ( const vec3_t point, int passEntityNum ); + // point contents against all linked entities + + G_IN_PVS, // ( const vec3_t p1, const vec3_t p2 ); + + G_IN_PVS_IGNORE_PORTALS, // ( const vec3_t p1, const vec3_t p2 ); + + G_ADJUST_AREA_PORTAL_STATE, // ( gentity_t *ent, qboolean open ); + + G_AREAS_CONNECTED, // ( int area1, int area2 ); + + G_LINKENTITY, // ( gentity_t *ent ); + // an entity will never be sent to a client or used for collision + // if it is not passed to linkentity. If the size, position, or + // solidity changes, it must be relinked. + + G_UNLINKENTITY, // ( gentity_t *ent ); + // call before removing an interactive entity + + G_ENTITIES_IN_BOX, // ( const vec3_t mins, const vec3_t maxs, gentity_t **list, int maxcount ); + // EntitiesInBox will return brush models based on their bounding box, + // so exact determination must still be done with EntityContact + + G_ENTITY_CONTACT, // ( const vec3_t mins, const vec3_t maxs, const gentity_t *ent ); + // perform an exact check against inline brush models of non-square shape + + G_GET_USERCMD, // ( int clientNum, usercmd_t *cmd ) + + G_GET_ENTITY_TOKEN, // qboolean ( char *buffer, int bufferSize ) + // Retrieves the next string token from the entity spawn text, returning + // false when all tokens have been parsed. + // This should only be done at GAME_INIT time. + + G_FS_GETFILELIST, + G_REAL_TIME, + G_SNAPVECTOR, + + G_TRACECAPSULE, // ( trace_t *results, const vec3_t start, const vec3_t mins, const vec3_t maxs, const vec3_t end, int passEntityNum, int contentmask ); + G_ENTITY_CONTACTCAPSULE, // ( const vec3_t mins, const vec3_t maxs, const gentity_t *ent ); + + // 1.32 + G_FS_SEEK, + + G_PARSE_ADD_GLOBAL_DEFINE, + G_PARSE_LOAD_SOURCE, + G_PARSE_FREE_SOURCE, + G_PARSE_READ_TOKEN, + G_PARSE_SOURCE_FILE_AND_LINE, + + G_SEND_GAMESTAT +} gameImport_t; + + +// +// functions exported by the game subsystem +// +typedef enum { + GAME_INIT, // ( int levelTime, int randomSeed, int restart ); + // init and shutdown will be called every single level + // The game should call G_GET_ENTITY_TOKEN to parse through all the + // entity configuration text and spawn gentities. + + GAME_SHUTDOWN, // (void); + + GAME_CLIENT_CONNECT, // ( int clientNum, qboolean firstTime ); + // return NULL if the client is allowed to connect, otherwise return + // a text string with the reason for denial + + GAME_CLIENT_BEGIN, // ( int clientNum ); + + GAME_CLIENT_USERINFO_CHANGED, // ( int clientNum ); + + GAME_CLIENT_DISCONNECT, // ( int clientNum ); + + GAME_CLIENT_COMMAND, // ( int clientNum ); + + GAME_CLIENT_THINK, // ( int clientNum ); + + GAME_RUN_FRAME, // ( int levelTime ); + + GAME_CONSOLE_COMMAND // ( void ); + // ConsoleCommand will be called when a command has been issued + // that is not recognized as a builtin function. + // The game can issue trap_argc() / trap_argv() commands to get the command + // and parameters. Return qfalse if the game doesn't recognize it as a command. +} gameExport_t; + diff --git a/src/game/g_session.c b/src/game/g_session.c new file mode 100644 index 0000000..ef78e8a --- /dev/null +++ b/src/game/g_session.c @@ -0,0 +1,171 @@ +/* +=========================================================================== +Copyright (C) 1999-2005 Id Software, Inc. +Copyright (C) 2000-2006 Tim Angus + +This file is part of Tremulous. + +Tremulous is free software; you can redistribute it +and/or modify it under the terms of the GNU General Public License as +published by the Free Software Foundation; either version 2 of the License, +or (at your option) any later version. + +Tremulous is distributed in the hope that it will be +useful, but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with Tremulous; if not, write to the Free Software +Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA +=========================================================================== +*/ + +#include "g_local.h" + + +/* +======================================================================= + + SESSION DATA + +Session data is the only data that stays persistant across level loads +and tournament restarts. +======================================================================= +*/ + +/* +================ +G_WriteClientSessionData + +Called on game shutdown +================ +*/ +void G_WriteClientSessionData( gclient_t *client ) +{ + const char *s; + const char *var; + + s = va( "%i %i %i %i %i %i %i %i %i %s", + client->sess.sessionTeam, + client->sess.restartTeam, + client->sess.spectatorTime, + client->sess.spectatorState, + client->sess.spectatorClient, + client->sess.wins, + client->sess.losses, + client->sess.teamLeader, + client->sess.invisible, + BG_ClientListString( &client->sess.ignoreList ) + ); + + var = va( "session%i", client - level.clients ); + + trap_Cvar_Set( var, s ); +} + +/* +================ +G_ReadSessionData + +Called on a reconnect +================ +*/ +void G_ReadSessionData( gclient_t *client ) +{ + char s[ MAX_STRING_CHARS ]; + const char *var; + + // bk001205 - format + int teamLeader; + int spectatorState; + int sessionTeam; + int restartTeam; + int invisible; + + var = va( "session%i", client - level.clients ); + trap_Cvar_VariableStringBuffer( var, s, sizeof(s) ); + + // FIXME: should be using BG_ClientListParse() for ignoreList, but + // bg_lib.c's sscanf() currently lacks %s + sscanf( s, "%i %i %i %i %i %i %i %i %i %x%x", + &sessionTeam, + &restartTeam, + &client->sess.spectatorTime, + &spectatorState, + &client->sess.spectatorClient, + &client->sess.wins, + &client->sess.losses, + &teamLeader, + &invisible, + &client->sess.ignoreList.hi, + &client->sess.ignoreList.lo + ); + // bk001205 - format issues + client->sess.sessionTeam = (team_t)sessionTeam; + client->sess.restartTeam = (pTeam_t)restartTeam; + client->sess.spectatorState = (spectatorState_t)spectatorState; + client->sess.teamLeader = (qboolean)teamLeader; + client->sess.invisible = (qboolean)invisible; +} + + +/* +================ +G_InitSessionData + +Called on a first-time connect +================ +*/ +void G_InitSessionData( gclient_t *client, char *userinfo ) +{ + clientSession_t *sess; + const char *value; + + sess = &client->sess; + + // initial team determination + value = Info_ValueForKey( userinfo, "team" ); + if( value[ 0 ] == 's' ) + { + // a willing spectator, not a waiting-in-line + sess->sessionTeam = TEAM_SPECTATOR; + } + else + { + if( g_maxGameClients.integer > 0 && + level.numNonSpectatorClients >= g_maxGameClients.integer ) + sess->sessionTeam = TEAM_SPECTATOR; + else + sess->sessionTeam = TEAM_FREE; + } + + sess->restartTeam = PTE_NONE; + sess->spectatorState = SPECTATOR_FREE; + sess->spectatorTime = level.time; + sess->spectatorClient = -1; + memset( &sess->ignoreList, 0, sizeof( sess->ignoreList ) ); + + G_WriteClientSessionData( client ); +} + + +/* +================== +G_WriteSessionData + +================== +*/ +void G_WriteSessionData( void ) +{ + int i; + + //TA: ? + trap_Cvar_Set( "session", va( "%i", 0 ) ); + + for( i = 0 ; i < level.maxclients ; i++ ) + { + if( level.clients[ i ].pers.connected == CON_CONNECTED ) + G_WriteClientSessionData( &level.clients[ i ] ); + } +} diff --git a/src/game/g_spawn.c b/src/game/g_spawn.c new file mode 100644 index 0000000..028c39f --- /dev/null +++ b/src/game/g_spawn.c @@ -0,0 +1,698 @@ +/* +=========================================================================== +Copyright (C) 1999-2005 Id Software, Inc. +Copyright (C) 2000-2006 Tim Angus + +This file is part of Tremulous. + +Tremulous is free software; you can redistribute it +and/or modify it under the terms of the GNU General Public License as +published by the Free Software Foundation; either version 2 of the License, +or (at your option) any later version. + +Tremulous is distributed in the hope that it will be +useful, but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with Tremulous; if not, write to the Free Software +Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA +=========================================================================== +*/ + +#include "g_local.h" + +qboolean G_SpawnString( const char *key, const char *defaultString, char **out ) +{ + int i; + + if( !level.spawning ) + { + *out = (char *)defaultString; +// G_Error( "G_SpawnString() called while not spawning" ); + } + + for( i = 0; i < level.numSpawnVars; i++ ) + { + if( !Q_stricmp( key, level.spawnVars[ i ][ 0 ] ) ) + { + *out = level.spawnVars[ i ][ 1 ]; + return qtrue; + } + } + + *out = (char *)defaultString; + return qfalse; +} + +qboolean G_SpawnFloat( const char *key, const char *defaultString, float *out ) +{ + char *s; + qboolean present; + + present = G_SpawnString( key, defaultString, &s ); + *out = atof( s ); + return present; +} + +qboolean G_SpawnInt( const char *key, const char *defaultString, int *out ) +{ + char *s; + qboolean present; + + present = G_SpawnString( key, defaultString, &s ); + *out = atoi( s ); + return present; +} + +qboolean G_SpawnVector( const char *key, const char *defaultString, float *out ) +{ + char *s; + qboolean present; + + present = G_SpawnString( key, defaultString, &s ); + sscanf( s, "%f %f %f", &out[ 0 ], &out[ 1 ], &out[ 2 ] ); + return present; +} + +qboolean G_SpawnVector4( const char *key, const char *defaultString, float *out ) +{ + char *s; + qboolean present; + + present = G_SpawnString( key, defaultString, &s ); + sscanf( s, "%f %f %f %f", &out[ 0 ], &out[ 1 ], &out[ 2 ], &out[ 3 ] ); + return present; +} + + + +// +// fields are needed for spawning from the entity string +// +typedef enum +{ + F_INT, + F_FLOAT, + F_LSTRING, // string on disk, pointer in memory, TAG_LEVEL + F_GSTRING, // string on disk, pointer in memory, TAG_GAME + F_VECTOR, + F_VECTOR4, //TA + F_ANGLEHACK, + F_ENTITY, // index on disk, pointer in memory + F_ITEM, // index on disk, pointer in memory + F_CLIENT, // index on disk, pointer in memory + F_IGNORE +} fieldtype_t; + +typedef struct +{ + char *name; + int ofs; + fieldtype_t type; + int flags; +} field_t; + +field_t fields[ ] = +{ + {"classname", FOFS(classname), F_LSTRING}, + {"origin", FOFS(s.origin), F_VECTOR}, + {"model", FOFS(model), F_LSTRING}, + {"model2", FOFS(model2), F_LSTRING}, + {"spawnflags", FOFS(spawnflags), F_INT}, + {"speed", FOFS(speed), F_FLOAT}, + {"target", FOFS(target), F_LSTRING}, + {"targetname", FOFS(targetname), F_LSTRING}, + {"message", FOFS(message), F_LSTRING}, + {"team", FOFS(team), F_LSTRING}, + {"wait", FOFS(wait), F_FLOAT}, + {"random", FOFS(random), F_FLOAT}, + {"count", FOFS(count), F_INT}, + {"health", FOFS(health), F_INT}, + {"light", 0, F_IGNORE}, + {"dmg", FOFS(damage), F_INT}, + {"angles", FOFS(s.angles), F_VECTOR}, + {"angle", FOFS(s.angles), F_ANGLEHACK}, + {"bounce", FOFS(physicsBounce), F_FLOAT}, + {"alpha", FOFS(pos1), F_VECTOR}, + {"radius", FOFS(pos2), F_VECTOR}, + {"acceleration", FOFS(acceleration), F_VECTOR}, + {"animation", FOFS(animation), F_VECTOR4}, + {"rotatorAngle", FOFS(rotatorAngle), F_FLOAT}, + {"targetShaderName", FOFS(targetShaderName), F_LSTRING}, + {"targetShaderNewName", FOFS(targetShaderNewName), F_LSTRING}, + + {NULL} +}; + + +typedef struct +{ + char *name; + void (*spawn)(gentity_t *ent); +} spawn_t; + +void SP_info_player_start( gentity_t *ent ); +void SP_info_player_deathmatch( gentity_t *ent ); +void SP_info_player_intermission( gentity_t *ent ); + +void SP_info_alien_intermission( gentity_t *ent ); +void SP_info_human_intermission( gentity_t *ent ); + +void SP_info_firstplace( gentity_t *ent ); +void SP_info_secondplace( gentity_t *ent ); +void SP_info_thirdplace( gentity_t *ent ); +void SP_info_podium( gentity_t *ent ); + +void SP_func_plat( gentity_t *ent ); +void SP_func_static( gentity_t *ent ); +void SP_func_rotating( gentity_t *ent ); +void SP_func_bobbing( gentity_t *ent ); +void SP_func_pendulum( gentity_t *ent ); +void SP_func_button( gentity_t *ent ); +void SP_func_door( gentity_t *ent ); +void SP_func_door_rotating( gentity_t *ent ); +void SP_func_door_model( gentity_t *ent ); +void SP_func_train( gentity_t *ent ); +void SP_func_timer( gentity_t *self); + +void SP_trigger_always( gentity_t *ent ); +void SP_trigger_multiple( gentity_t *ent ); +void SP_trigger_push( gentity_t *ent ); +void SP_trigger_teleport( gentity_t *ent ); +void SP_trigger_hurt( gentity_t *ent ); +void SP_trigger_stage( gentity_t *ent ); +void SP_trigger_win( gentity_t *ent ); +void SP_trigger_buildable( gentity_t *ent ); +void SP_trigger_class( gentity_t *ent ); +void SP_trigger_equipment( gentity_t *ent ); +void SP_trigger_gravity( gentity_t *ent ); +void SP_trigger_heal( gentity_t *ent ); +void SP_trigger_ammo( gentity_t *ent ); + +void SP_target_delay( gentity_t *ent ); +void SP_target_speaker( gentity_t *ent ); +void SP_target_print( gentity_t *ent ); +void SP_target_character( gentity_t *ent ); +void SP_target_score( gentity_t *ent ); +void SP_target_teleporter( gentity_t *ent ); +void SP_target_relay( gentity_t *ent ); +void SP_target_kill( gentity_t *ent ); +void SP_target_position( gentity_t *ent ); +void SP_target_location( gentity_t *ent ); +void SP_target_push( gentity_t *ent ); +void SP_target_rumble( gentity_t *ent ); +void SP_target_alien_win( gentity_t *ent ); +void SP_target_human_win( gentity_t *ent ); +void SP_target_hurt( gentity_t *ent ); + +void SP_light( gentity_t *self ); +void SP_info_null( gentity_t *self ); +void SP_info_notnull( gentity_t *self ); +void SP_info_camp( gentity_t *self ); +void SP_path_corner( gentity_t *self ); + +void SP_misc_teleporter_dest( gentity_t *self ); +void SP_misc_model( gentity_t *ent ); +void SP_misc_portal_camera( gentity_t *ent ); +void SP_misc_portal_surface( gentity_t *ent ); + +void SP_shooter_rocket( gentity_t *ent ); +void SP_shooter_plasma( gentity_t *ent ); +void SP_shooter_grenade( gentity_t *ent ); + +void SP_misc_particle_system( gentity_t *ent ); +void SP_misc_anim_model( gentity_t *ent ); +void SP_misc_light_flare( gentity_t *ent ); + +spawn_t spawns[ ] = +{ + // info entities don't do anything at all, but provide positional + // information for things controlled by other processes + { "info_player_start", SP_info_player_start }, + { "info_player_deathmatch", SP_info_player_deathmatch }, + { "info_player_intermission", SP_info_player_intermission }, + + //TA: extra bits + { "info_alien_intermission", SP_info_alien_intermission }, + { "info_human_intermission", SP_info_human_intermission }, + + { "info_null", SP_info_null }, + { "info_notnull", SP_info_notnull }, // use target_position instead + + { "func_plat", SP_func_plat }, + { "func_button", SP_func_button }, + { "func_door", SP_func_door }, + { "func_door_rotating", SP_func_door_rotating }, //TA + { "func_door_model", SP_func_door_model }, //TA + { "func_static", SP_func_static }, + { "func_rotating", SP_func_rotating }, + { "func_bobbing", SP_func_bobbing }, + { "func_pendulum", SP_func_pendulum }, + { "func_train", SP_func_train }, + { "func_group", SP_info_null }, + { "func_timer", SP_func_timer }, // rename trigger_timer? + + // Triggers are brush objects that cause an effect when contacted + // by a living player, usually involving firing targets. + // While almost everything could be done with + // a single trigger class and different targets, triggered effects + // could not be client side predicted (push and teleport). + { "trigger_always", SP_trigger_always }, + { "trigger_multiple", SP_trigger_multiple }, + { "trigger_push", SP_trigger_push }, + { "trigger_teleport", SP_trigger_teleport }, + { "trigger_hurt", SP_trigger_hurt }, + { "trigger_stage", SP_trigger_stage }, + { "trigger_win", SP_trigger_win }, + { "trigger_buildable", SP_trigger_buildable }, + { "trigger_class", SP_trigger_class }, + { "trigger_equipment", SP_trigger_equipment }, + { "trigger_gravity", SP_trigger_gravity }, + { "trigger_heal", SP_trigger_heal }, + { "trigger_ammo", SP_trigger_ammo }, + + // targets perform no action by themselves, but must be triggered + // by another entity + { "target_delay", SP_target_delay }, + { "target_speaker", SP_target_speaker }, + { "target_print", SP_target_print }, + { "target_score", SP_target_score }, + { "target_teleporter", SP_target_teleporter }, + { "target_relay", SP_target_relay }, + { "target_kill", SP_target_kill }, + { "target_position", SP_target_position }, + { "target_location", SP_target_location }, + { "target_push", SP_target_push }, + { "target_rumble", SP_target_rumble }, + { "target_alien_win", SP_target_alien_win }, + { "target_human_win", SP_target_human_win }, + { "target_hurt", SP_target_hurt }, + + { "light", SP_light }, + { "path_corner", SP_path_corner }, + + { "misc_teleporter_dest", SP_misc_teleporter_dest }, + { "misc_model", SP_misc_model }, + { "misc_portal_surface", SP_misc_portal_surface }, + { "misc_portal_camera", SP_misc_portal_camera }, + + { "misc_particle_system", SP_misc_particle_system }, + { "misc_anim_model", SP_misc_anim_model }, + { "misc_light_flare", SP_misc_light_flare }, + + { NULL, 0 } +}; + +/* +=============== +G_CallSpawn + +Finds the spawn function for the entity and calls it, +returning qfalse if not found +=============== +*/ +qboolean G_CallSpawn( gentity_t *ent ) +{ + spawn_t *s; + buildable_t buildable; + + if( !ent->classname ) + { + G_Printf( "G_CallSpawn: NULL classname\n" ); + return qfalse; + } + + //check buildable spawn functions + if( ( buildable = BG_FindBuildNumForEntityName( ent->classname ) ) != BA_NONE ) + { + // don't spawn built-in buildings if we are using a custom layout + if( level.layout[ 0 ] && Q_stricmp( level.layout, "*BUILTIN*" ) ) + return qtrue; + + if( buildable == BA_A_SPAWN || buildable == BA_H_SPAWN ) + { + ent->s.angles[ YAW ] += 180.0f; + AngleNormalize360( ent->s.angles[ YAW ] ); + } + + G_SpawnBuildable( ent, buildable ); + return qtrue; + } + + // check normal spawn functions + for( s = spawns; s->name; s++ ) + { + if( !strcmp( s->name, ent->classname ) ) + { + // found it + s->spawn( ent ); + return qtrue; + } + } + + G_Printf( "%s doesn't have a spawn function\n", ent->classname ); + return qfalse; +} + +/* +============= +G_NewString + +Builds a copy of the string, translating \n to real linefeeds +so message texts can be multi-line +============= +*/ +char *G_NewString( const char *string ) +{ + char *newb, *new_p; + int i,l; + + l = strlen( string ) + 1; + + newb = G_Alloc( l ); + + new_p = newb; + + // turn \n into a real linefeed + for( i = 0 ; i < l ; i++ ) + { + if( string[ i ] == '\\' && i < l - 1 ) + { + i++; + if( string[ i ] == 'n' ) + *new_p++ = '\n'; + else + *new_p++ = '\\'; + } + else + *new_p++ = string[ i ]; + } + + return newb; +} + + + + +/* +=============== +G_ParseField + +Takes a key/value pair and sets the binary values +in a gentity +=============== +*/ +void G_ParseField( const char *key, const char *value, gentity_t *ent ) +{ + field_t *f; + byte *b; + float v; + vec3_t vec; + vec4_t vec4; + + for( f = fields; f->name; f++ ) + { + if( !Q_stricmp( f->name, key ) ) + { + // found it + b = (byte *)ent; + + switch( f->type ) + { + case F_LSTRING: + *(char **)( b + f->ofs ) = G_NewString( value ); + break; + + case F_VECTOR: + sscanf( value, "%f %f %f", &vec[ 0 ], &vec[ 1 ], &vec[ 2 ] ); + + ( (float *)( b + f->ofs ) )[ 0 ] = vec[ 0 ]; + ( (float *)( b + f->ofs ) )[ 1 ] = vec[ 1 ]; + ( (float *)( b + f->ofs ) )[ 2 ] = vec[ 2 ]; + break; + + case F_VECTOR4: + sscanf( value, "%f %f %f %f", &vec4[ 0 ], &vec4[ 1 ], &vec4[ 2 ], &vec4[ 3 ] ); + + ( (float *)( b + f->ofs ) )[ 0 ] = vec4[ 0 ]; + ( (float *)( b + f->ofs ) )[ 1 ] = vec4[ 1 ]; + ( (float *)( b + f->ofs ) )[ 2 ] = vec4[ 2 ]; + ( (float *)( b + f->ofs ) )[ 3 ] = vec4[ 3 ]; + break; + + case F_INT: + *(int *)( b + f->ofs ) = atoi( value ); + break; + + case F_FLOAT: + *(float *)( b + f->ofs ) = atof( value ); + break; + + case F_ANGLEHACK: + v = atof( value ); + ( (float *)( b + f->ofs ) )[ 0 ] = 0; + ( (float *)( b + f->ofs ) )[ 1 ] = v; + ( (float *)( b + f->ofs ) )[ 2 ] = 0; + break; + + default: + case F_IGNORE: + break; + } + + return; + } + } +} + + + + +/* +=================== +G_SpawnGEntityFromSpawnVars + +Spawn an entity and fill in all of the level fields from +level.spawnVars[], then call the class specfic spawn function +=================== +*/ +void G_SpawnGEntityFromSpawnVars( void ) +{ + int i; + gentity_t *ent; + + // get the next free entity + ent = G_Spawn( ); + + for( i = 0 ; i < level.numSpawnVars ; i++ ) + G_ParseField( level.spawnVars[ i ][ 0 ], level.spawnVars[ i ][ 1 ], ent ); + + G_SpawnInt( "notq3a", "0", &i ); + + if( i ) + { + G_FreeEntity( ent ); + return; + } + + // move editor origin to pos + VectorCopy( ent->s.origin, ent->s.pos.trBase ); + VectorCopy( ent->s.origin, ent->r.currentOrigin ); + + // if we didn't get a classname, don't bother spawning anything + if( !G_CallSpawn( ent ) ) + G_FreeEntity( ent ); +} + + + +/* +==================== +G_AddSpawnVarToken +==================== +*/ +char *G_AddSpawnVarToken( const char *string ) +{ + int l; + char *dest; + + l = strlen( string ); + if( level.numSpawnVarChars + l + 1 > MAX_SPAWN_VARS_CHARS ) + G_Error( "G_AddSpawnVarToken: MAX_SPAWN_CHARS" ); + + dest = level.spawnVarChars + level.numSpawnVarChars; + memcpy( dest, string, l + 1 ); + + level.numSpawnVarChars += l + 1; + + return dest; +} + +/* +==================== +G_ParseSpawnVars + +Parses a brace bounded set of key / value pairs out of the +level's entity strings into level.spawnVars[] + +This does not actually spawn an entity. +==================== +*/ +qboolean G_ParseSpawnVars( void ) +{ + char keyname[ MAX_TOKEN_CHARS ]; + char com_token[ MAX_TOKEN_CHARS ]; + + level.numSpawnVars = 0; + level.numSpawnVarChars = 0; + + // parse the opening brace + if( !trap_GetEntityToken( com_token, sizeof( com_token ) ) ) + { + // end of spawn string + return qfalse; + } + + if( com_token[ 0 ] != '{' ) + G_Error( "G_ParseSpawnVars: found %s when expecting {", com_token ); + + // go through all the key / value pairs + while( 1 ) + { + // parse key + if( !trap_GetEntityToken( keyname, sizeof( keyname ) ) ) + G_Error( "G_ParseSpawnVars: EOF without closing brace" ); + + if( keyname[0] == '}' ) + break; + + // parse value + if( !trap_GetEntityToken( com_token, sizeof( com_token ) ) ) + G_Error( "G_ParseSpawnVars: EOF without closing brace" ); + + if( com_token[0] == '}' ) + G_Error( "G_ParseSpawnVars: closing brace without data" ); + + if( level.numSpawnVars == MAX_SPAWN_VARS ) + G_Error( "G_ParseSpawnVars: MAX_SPAWN_VARS" ); + + level.spawnVars[ level.numSpawnVars ][ 0 ] = G_AddSpawnVarToken( keyname ); + level.spawnVars[ level.numSpawnVars ][ 1 ] = G_AddSpawnVarToken( com_token ); + level.numSpawnVars++; + } + + return qtrue; +} + + + +/*QUAKED worldspawn (0 0 0) ? + +Every map should have exactly one worldspawn. +"music" music wav file +"gravity" 800 is default gravity +"message" Text to print during connection process +*/ +void SP_worldspawn( void ) +{ + char *s; + + G_SpawnString( "classname", "", &s ); + + if( Q_stricmp( s, "worldspawn" ) ) + G_Error( "SP_worldspawn: The first entity isn't 'worldspawn'" ); + + // make some data visible to connecting client + trap_SetConfigstring( CS_GAME_VERSION, GAME_VERSION ); + + trap_SetConfigstring( CS_LEVEL_START_TIME, va( "%i", level.startTime ) ); + + G_SpawnString( "music", "", &s ); + trap_SetConfigstring( CS_MUSIC, s ); + + G_SpawnString( "message", "", &s ); + trap_SetConfigstring( CS_MESSAGE, s ); // map specific message + + trap_SetConfigstring( CS_MOTD, g_motd.string ); // message of the day + + G_SpawnString( "gravity", "800", &s ); + trap_Cvar_Set( "g_gravity", s ); + + G_SpawnString( "humanBuildPoints", DEFAULT_HUMAN_BUILDPOINTS, &s ); + trap_Cvar_Set( "g_humanBuildPoints", s ); + + G_SpawnString( "humanMaxStage", DEFAULT_HUMAN_MAX_STAGE, &s ); + trap_Cvar_Set( "g_humanMaxStage", s ); + + G_SpawnString( "humanStage2Threshold", DEFAULT_HUMAN_STAGE2_THRESH, &s ); + trap_Cvar_Set( "g_humanStage2Threshold", s ); + + G_SpawnString( "humanStage3Threshold", DEFAULT_HUMAN_STAGE3_THRESH, &s ); + trap_Cvar_Set( "g_humanStage3Threshold", s ); + + G_SpawnString( "alienBuildPoints", DEFAULT_ALIEN_BUILDPOINTS, &s ); + trap_Cvar_Set( "g_alienBuildPoints", s ); + + G_SpawnString( "alienMaxStage", DEFAULT_ALIEN_MAX_STAGE, &s ); + trap_Cvar_Set( "g_alienMaxStage", s ); + + G_SpawnString( "alienStage2Threshold", DEFAULT_ALIEN_STAGE2_THRESH, &s ); + trap_Cvar_Set( "g_alienStage2Threshold", s ); + + G_SpawnString( "alienStage3Threshold", DEFAULT_ALIEN_STAGE3_THRESH, &s ); + trap_Cvar_Set( "g_alienStage3Threshold", s ); + + G_SpawnString( "enableDust", "0", &s ); + trap_Cvar_Set( "g_enableDust", s ); + + G_SpawnString( "enableBreath", "0", &s ); + trap_Cvar_Set( "g_enableBreath", s ); + + G_SpawnString( "disabledEquipment", "", &s ); + trap_Cvar_Set( "g_disabledEquipment", s ); + + G_SpawnString( "disabledClasses", "", &s ); + trap_Cvar_Set( "g_disabledClasses", s ); + + G_SpawnString( "disabledBuildables", "", &s ); + trap_Cvar_Set( "g_disabledBuildables", s ); + + g_entities[ ENTITYNUM_WORLD ].s.number = ENTITYNUM_WORLD; + g_entities[ ENTITYNUM_WORLD ].classname = "worldspawn"; + + if( g_restarted.integer ) + trap_Cvar_Set( "g_restarted", "0" ); + +} + + +/* +============== +G_SpawnEntitiesFromString + +Parses textual entity definitions out of an entstring and spawns gentities. +============== +*/ +void G_SpawnEntitiesFromString( void ) +{ + // allow calls to G_Spawn*() + level.spawning = qtrue; + level.numSpawnVars = 0; + + // the worldspawn is not an actual entity, but it still + // has a "spawn" function to perform any global setup + // needed by a level (setting configstrings or cvars, etc) + if( !G_ParseSpawnVars( ) ) + G_Error( "SpawnEntities: no entities" ); + + SP_worldspawn( ); + + // parse ents + while( G_ParseSpawnVars( ) ) + G_SpawnGEntityFromSpawnVars( ); + + level.spawning = qfalse; // any future calls to G_Spawn*() will be errors +} + diff --git a/src/game/g_svcmds.c b/src/game/g_svcmds.c new file mode 100644 index 0000000..8b7b575 --- /dev/null +++ b/src/game/g_svcmds.c @@ -0,0 +1,746 @@ +/* +=========================================================================== +Copyright (C) 1999-2005 Id Software, Inc. +Copyright (C) 2000-2006 Tim Angus + +This file is part of Tremulous. + +Tremulous is free software; you can redistribute it +and/or modify it under the terms of the GNU General Public License as +published by the Free Software Foundation; either version 2 of the License, +or (at your option) any later version. + +Tremulous is distributed in the hope that it will be +useful, but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with Tremulous; if not, write to the Free Software +Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA +=========================================================================== +*/ + +// this file holds commands that can be executed by the server console, but not remote clients + +#include "g_local.h" + + +/* +============================================================================== + +PACKET FILTERING + + +You can add or remove addresses from the filter list with: + +addip <ip> +removeip <ip> + +The ip address is specified in dot format, and you can use '*' to match any value +so you can specify an entire class C network with "addip 192.246.40.*" + +Removeip will only remove an address specified exactly the same way. You cannot addip a subnet, then removeip a single host. + +listip +Prints the current list of filters. + +g_filterban <0 or 1> + +If 1 (the default), then ip addresses matching the current list will be prohibited from entering the game. This is the default setting. + +If 0, then only addresses matching the list will be allowed. This lets you easily set up a private game, or a game that only allows players from your local network. + +TTimo NOTE: for persistence, bans are stored in g_banIPs cvar MAX_CVAR_VALUE_STRING +The size of the cvar string buffer is limiting the banning to around 20 masks +this could be improved by putting some g_banIPs2 g_banIps3 etc. maybe +still, you should rely on PB for banning instead + +============================================================================== +*/ + +// extern vmCvar_t g_banIPs; +// extern vmCvar_t g_filterBan; + + +typedef struct ipFilter_s +{ + unsigned mask; + unsigned compare; +} ipFilter_t; + +#define MAX_IPFILTERS 1024 + +static ipFilter_t ipFilters[ MAX_IPFILTERS ]; +static int numIPFilters; + +/* +================= +StringToFilter +================= +*/ +static qboolean StringToFilter( char *s, ipFilter_t *f ) +{ + char num[ 128 ]; + int i, j; + byte b[ 4 ]; + byte m[ 4 ]; + + for( i = 0; i < 4; i++ ) + { + b[ i ] = 0; + m[ i ] = 0; + } + + for( i = 0; i < 4; i++ ) + { + if( *s < '0' || *s > '9' ) + { + if( *s == '*' ) // 'match any' + { + //b[ i ] and m[ i ] to 0 + s++; + if ( !*s ) + break; + + s++; + continue; + } + + G_Printf( "Bad filter address: %s\n", s ); + return qfalse; + } + + j = 0; + while( *s >= '0' && *s <= '9' ) + num[ j++ ] = *s++; + + num[ j ] = 0; + b[ i ] = atoi( num ); + + m[ i ] = 255; + + if( !*s ) + break; + + s++; + } + + f->mask = *(unsigned *)m; + f->compare = *(unsigned *)b; + + return qtrue; +} + +/* +================= +UpdateIPBans +================= +*/ +static void UpdateIPBans( void ) +{ + byte b[ 4 ]; + byte m[ 4 ]; + int i, j; + char iplist_final[ MAX_CVAR_VALUE_STRING ]; + char ip[ 64 ]; + + *iplist_final = 0; + + for( i = 0 ; i < numIPFilters ; i++ ) + { + if( ipFilters[ i ].compare == 0xffffffff ) + continue; + + *(unsigned *)b = ipFilters[ i ].compare; + *(unsigned *)m = ipFilters[ i ].mask; + *ip = 0; + + for( j = 0 ; j < 4 ; j++ ) + { + if( m[ j ] != 255 ) + Q_strcat( ip, sizeof( ip ), "*" ); + else + Q_strcat( ip, sizeof( ip ), va( "%i", b[ j ] ) ); + + Q_strcat( ip, sizeof( ip ), ( j < 3 ) ? "." : " " ); + } + + if( strlen( iplist_final ) + strlen( ip ) < MAX_CVAR_VALUE_STRING ) + Q_strcat( iplist_final, sizeof( iplist_final ), ip ); + else + { + Com_Printf( "g_banIPs overflowed at MAX_CVAR_VALUE_STRING\n" ); + break; + } + } + + trap_Cvar_Set( "g_banIPs", iplist_final ); +} + +/* +================= +G_FilterPacket +================= +*/ +qboolean G_FilterPacket( char *from ) +{ + int i; + unsigned in; + byte m[ 4 ]; + char *p; + + i = 0; + p = from; + while( *p && i < 4 ) + { + m[ i ] = 0; + while( *p >= '0' && *p <= '9' ) + { + m[ i ] = m[ i ] * 10 + ( *p - '0' ); + p++; + } + + if( !*p || *p == ':' ) + break; + + i++, p++; + } + + in = *(unsigned *)m; + + for( i = 0; i < numIPFilters; i++ ) + if( ( in & ipFilters[ i ].mask ) == ipFilters[ i ].compare ) + return g_filterBan.integer != 0; + + return g_filterBan.integer == 0; +} + +/* +================= +AddIP +================= +*/ +static void AddIP( char *str ) +{ + int i; + + for( i = 0 ; i < numIPFilters ; i++ ) + if( ipFilters[ i ].compare == 0xffffffff ) + break; // free spot + + if( i == numIPFilters ) + { + if( numIPFilters == MAX_IPFILTERS ) + { + G_Printf( "IP filter list is full\n" ); + return; + } + + numIPFilters++; + } + + if( !StringToFilter( str, &ipFilters[ i ] ) ) + ipFilters[ i ].compare = 0xffffffffu; + + UpdateIPBans( ); +} + +/* +================= +G_ProcessIPBans +================= +*/ +void G_ProcessIPBans( void ) +{ + char *s, *t; + char str[ MAX_CVAR_VALUE_STRING ]; + + Q_strncpyz( str, g_banIPs.string, sizeof( str ) ); + + for( t = s = g_banIPs.string; *t; /* */ ) + { + s = strchr( s, ' ' ); + + if( !s ) + break; + + while( *s == ' ' ) + *s++ = 0; + + if( *t ) + AddIP( t ); + + t = s; + } +} + + +/* +================= +Svcmd_AddIP_f +================= +*/ +void Svcmd_AddIP_f( void ) +{ + char str[ MAX_TOKEN_CHARS ]; + + if( trap_Argc( ) < 2 ) + { + G_Printf( "Usage: addip <ip-mask>\n" ); + return; + } + + trap_Argv( 1, str, sizeof( str ) ); + + AddIP( str ); +} + +/* +================= +Svcmd_RemoveIP_f +================= +*/ +void Svcmd_RemoveIP_f( void ) +{ + ipFilter_t f; + int i; + char str[ MAX_TOKEN_CHARS ]; + + if( trap_Argc( ) < 2 ) + { + G_Printf( "Usage: sv removeip <ip-mask>\n" ); + return; + } + + trap_Argv( 1, str, sizeof( str ) ); + + if( !StringToFilter( str, &f ) ) + return; + + for( i = 0; i < numIPFilters; i++ ) + { + if( ipFilters[ i ].mask == f.mask && + ipFilters[ i ].compare == f.compare) + { + ipFilters[ i ].compare = 0xffffffffu; + G_Printf ( "Removed.\n" ); + + UpdateIPBans( ); + return; + } + } + + G_Printf ( "Didn't find %s.\n", str ); +} + +/* +=================== +Svcmd_EntityList_f +=================== +*/ +void Svcmd_EntityList_f( void ) +{ + int e; + gentity_t *check; + + check = g_entities; + + for( e = 0; e < level.num_entities; e++, check++ ) + { + if( !check->inuse ) + continue; + + G_Printf( "%3i:", e ); + + switch( check->s.eType ) + { + case ET_GENERAL: + G_Printf( "ET_GENERAL " ); + break; + case ET_PLAYER: + G_Printf( "ET_PLAYER " ); + break; + case ET_ITEM: + G_Printf( "ET_ITEM " ); + break; + case ET_BUILDABLE: + G_Printf( "ET_BUILDABLE " ); + break; + case ET_MISSILE: + G_Printf( "ET_MISSILE " ); + break; + case ET_MOVER: + G_Printf( "ET_MOVER " ); + break; + case ET_BEAM: + G_Printf( "ET_BEAM " ); + break; + case ET_PORTAL: + G_Printf( "ET_PORTAL " ); + break; + case ET_SPEAKER: + G_Printf( "ET_SPEAKER " ); + break; + case ET_PUSH_TRIGGER: + G_Printf( "ET_PUSH_TRIGGER " ); + break; + case ET_TELEPORT_TRIGGER: + G_Printf( "ET_TELEPORT_TRIGGER " ); + break; + case ET_INVISIBLE: + G_Printf( "ET_INVISIBLE " ); + break; + case ET_GRAPPLE: + G_Printf( "ET_GRAPPLE " ); + break; + default: + G_Printf( "%3i ", check->s.eType ); + break; + } + + if( check->classname ) + G_Printf( "%s", check->classname ); + + G_Printf( "\n" ); + } +} + +gclient_t *ClientForString( const char *s ) +{ + gclient_t *cl; + int i; + int idnum; + + // numeric values are just slot numbers + if( s[ 0 ] >= '0' && s[ 0 ] <= '9' ) + { + idnum = atoi( s ); + + if( idnum < 0 || idnum >= level.maxclients ) + { + Com_Printf( "Bad client slot: %i\n", idnum ); + return NULL; + } + + cl = &level.clients[ idnum ]; + + if( cl->pers.connected == CON_DISCONNECTED ) + { + G_Printf( "Client %i is not connected\n", idnum ); + return NULL; + } + + return cl; + } + + // check for a name match + for( i = 0; i < level.maxclients; i++ ) + { + cl = &level.clients[ i ]; + if( cl->pers.connected == CON_DISCONNECTED ) + continue; + + if( !Q_stricmp( cl->pers.netname, s ) ) + return cl; + } + + G_Printf( "User %s is not on the server\n", s ); + + return NULL; +} + +/* +=================== +Svcmd_ForceTeam_f + +forceteam <player> <team> +=================== +*/ +void Svcmd_ForceTeam_f( void ) +{ + gclient_t *cl; + char str[ MAX_TOKEN_CHARS ]; + + // find the player + trap_Argv( 1, str, sizeof( str ) ); + cl = ClientForString( str ); + + if( !cl ) + return; + + // set the team + trap_Argv( 2, str, sizeof( str ) ); + /*SetTeam( &g_entities[cl - level.clients], str );*/ + //FIXME: tremulise this +} + +/* +=================== +Svcmd_LayoutSave_f + +layoutsave <name> +=================== +*/ +void Svcmd_LayoutSave_f( void ) +{ + char str[ MAX_QPATH ]; + char str2[ MAX_QPATH - 4 ]; + char *s; + int i = 0; + + if( trap_Argc( ) != 2 ) + { + G_Printf( "usage: layoutsave LAYOUTNAME\n" ); + return; + } + trap_Argv( 1, str, sizeof( str ) ); + + // sanitize name + s = &str[ 0 ]; + while( *s && i < sizeof( str2 ) - 1 ) + { + if( ( *s >= '0' && *s <= '9' ) || + ( *s >= 'a' && *s <= 'z' ) || + ( *s >= 'A' && *s <= 'Z' ) || *s == '-' || *s == '_' ) + { + str2[ i++ ] = *s; + str2[ i ] = '\0'; + } + s++; + } + + if( !str2[ 0 ] ) + { + G_Printf("layoutsave: invalid name \"%s\"\n", str ); + return; + } + + G_LayoutSave( str2 ); +} + +char *ConcatArgs( int start ); + +/* +=================== +Svcmd_LayoutLoad_f + +layoutload [<name> [<name2> [<name3 [...]]]] + +This is just a silly alias for doing: + set g_layouts "name name2 name3" + map_restart +=================== +*/ +void Svcmd_LayoutLoad_f( void ) +{ + char layouts[ MAX_CVAR_VALUE_STRING ]; + char *s; + + s = ConcatArgs( 1 ); + Q_strncpyz( layouts, s, sizeof( layouts ) ); + trap_Cvar_Set( "g_layouts", layouts ); + trap_SendConsoleCommand( EXEC_APPEND, "map_restart\n" ); + level.restarted = qtrue; +} + +static void Svcmd_AdmitDefeat_f( void ) +{ + int team; + char teamNum[ 2 ]; + + if( trap_Argc( ) != 2 ) + { + G_Printf("admitdefeat: must provide a team\n"); + return; + } + trap_Argv( 1, teamNum, sizeof( teamNum ) ); + team = atoi( teamNum ); + if( team == PTE_ALIENS || teamNum[ 0 ] == 'a' ) + { + level.surrenderTeam = PTE_ALIENS; + G_BaseSelfDestruct( PTE_ALIENS ); + G_TeamCommand( PTE_ALIENS, "cp \"Hivemind Link Broken\" 1"); + trap_SendServerCommand( -1, "print \"Alien team has admitted defeat\n\"" ); + } + else if( team == PTE_HUMANS || teamNum[ 0 ] == 'h' ) + { + level.surrenderTeam = PTE_HUMANS; + G_BaseSelfDestruct( PTE_HUMANS ); + G_TeamCommand( PTE_HUMANS, "cp \"Life Support Terminated\" 1"); + trap_SendServerCommand( -1, "print \"Human team has admitted defeat\n\"" ); + } + else + { + G_Printf("admitdefeat: invalid team\n"); + } +} + +/* +================= +ConsoleCommand + +================= +*/ +qboolean ConsoleCommand( void ) +{ + char cmd[ MAX_TOKEN_CHARS ]; + + trap_Argv( 0, cmd, sizeof( cmd ) ); + + if( Q_stricmp( cmd, "entitylist" ) == 0 ) + { + Svcmd_EntityList_f( ); + return qtrue; + } + + if( Q_stricmp( cmd, "forceteam" ) == 0 ) + { + Svcmd_ForceTeam_f( ); + return qtrue; + } + + if( Q_stricmp( cmd, "game_memory" ) == 0 ) + { + Svcmd_GameMem_f( ); + return qtrue; + } + + if( Q_stricmp( cmd, "addip" ) == 0 ) + { + Svcmd_AddIP_f( ); + return qtrue; + } + + if( Q_stricmp( cmd, "removeip" ) == 0 ) + { + Svcmd_RemoveIP_f( ); + return qtrue; + } + + if( Q_stricmp( cmd, "listip" ) == 0 ) + { + trap_SendConsoleCommand( EXEC_NOW, "g_banIPs\n" ); + return qtrue; + } + + if( Q_stricmp( cmd, "mapRotation" ) == 0 ) + { + char *rotationName = ConcatArgs( 1 ); + + if( !G_StartMapRotation( rotationName, qfalse ) ) + G_Printf( "Can't find map rotation %s\n", rotationName ); + + return qtrue; + } + + if( Q_stricmp( cmd, "stopMapRotation" ) == 0 ) + { + G_StopMapRotation( ); + + return qtrue; + } + + if( Q_stricmp( cmd, "advanceMapRotation" ) == 0 ) + { + G_AdvanceMapRotation( ); + + return qtrue; + } + + if( Q_stricmp( cmd, "alienWin" ) == 0 ) + { + int i; + gentity_t *e; + + for( i = 1, e = g_entities + i; i < level.num_entities; i++, e++ ) + { + if( e->s.modelindex == BA_H_SPAWN ) + G_Damage( e, NULL, NULL, NULL, NULL, 10000, 0, MOD_SUICIDE ); + } + + return qtrue; + } + + if( Q_stricmp( cmd, "humanWin" ) == 0 ) + { + int i; + gentity_t *e; + + for( i = 1, e = g_entities + i; i < level.num_entities; i++, e++ ) + { + if( e->s.modelindex == BA_A_SPAWN ) + G_Damage( e, NULL, NULL, NULL, NULL, 10000, 0, MOD_SUICIDE ); + } + + return qtrue; + } + + if( !Q_stricmp( cmd, "layoutsave" ) ) + { + Svcmd_LayoutSave_f( ); + return qtrue; + } + + if( !Q_stricmp( cmd, "layoutload" ) ) + { + Svcmd_LayoutLoad_f( ); + return qtrue; + } + + if( !Q_stricmp( cmd, "admitdefeat" ) ) + { + Svcmd_AdmitDefeat_f( ); + return qtrue; + } + + if( !Q_stricmp( cmd, "evacuation" ) ) + { + trap_SendServerCommand( -1, "print \"Evacuation ordered\n\"" ); + level.lastWin = PTE_NONE; + trap_SetConfigstring( CS_WINNER, "Evacuation" ); + LogExit( "Evacuation." ); + G_admin_maplog_result( "d" ); + return qtrue; + } + + // see if this is a a admin command + if( G_admin_cmd_check( NULL, qfalse ) ) + return qtrue; + + if( g_dedicated.integer ) + { + if( Q_stricmp( cmd, "say" ) == 0 ) + { + trap_SendServerCommand( -1, va( "print \"server: %s\n\"", ConcatArgs( 1 ) ) ); + return qtrue; + } + else if( !Q_stricmp( cmd, "chat" ) ) + { + trap_SendServerCommand( -1, va( "chat \"%s\" -1 0", ConcatArgs( 1 ) ) ); + G_Printf( "chat: %s\n", ConcatArgs( 1 ) ); + return qtrue; + } + else if( !Q_stricmp( cmd, "cp" ) ) + { + G_CP( NULL ); + return qtrue; + } + else if( !Q_stricmp( cmd, "m" ) ) + { + G_PrivateMessage( NULL ); + return qtrue; + } + else if( !Q_stricmp( cmd, "a" ) || !Q_stricmp( cmd, "say_admins" )) + { + G_Say( NULL, NULL, SAY_ADMINS, ConcatArgs( 1 ) ); + return qtrue; + } + + G_Printf( "unknown command: %s\n", cmd ); + return qtrue; + } + + return qfalse; +} + diff --git a/src/game/g_syscalls.asm b/src/game/g_syscalls.asm new file mode 100644 index 0000000..e77017b --- /dev/null +++ b/src/game/g_syscalls.asm @@ -0,0 +1,65 @@ +code + +equ trap_Printf -1 +equ trap_Error -2 +equ trap_Milliseconds -3 +equ trap_Cvar_Register -4 +equ trap_Cvar_Update -5 +equ trap_Cvar_Set -6 +equ trap_Cvar_VariableIntegerValue -7 +equ trap_Cvar_VariableStringBuffer -8 +equ trap_Argc -9 +equ trap_Argv -10 +equ trap_FS_FOpenFile -11 +equ trap_FS_Read -12 +equ trap_FS_Write -13 +equ trap_FS_FCloseFile -14 +equ trap_SendConsoleCommand -15 +equ trap_LocateGameData -16 +equ trap_DropClient -17 +equ trap_SendServerCommand -18 +equ trap_SetConfigstring -19 +equ trap_GetConfigstring -20 +equ trap_GetUserinfo -21 +equ trap_SetUserinfo -22 +equ trap_GetServerinfo -23 +equ trap_SetBrushModel -24 +equ trap_Trace -25 +equ trap_PointContents -26 +equ trap_InPVS -27 +equ trap_InPVSIgnorePortals -28 +equ trap_AdjustAreaPortalState -29 +equ trap_AreasConnected -30 +equ trap_LinkEntity -31 +equ trap_UnlinkEntity -32 +equ trap_EntitiesInBox -33 +equ trap_EntityContact -34 +equ trap_GetUsercmd -35 +equ trap_GetEntityToken -36 +equ trap_FS_GetFileList -37 +equ trap_RealTime -38 +equ trap_SnapVector -39 +equ trap_TraceCapsule -40 +equ trap_EntityContactCapsule -41 +equ trap_FS_Seek -42 + +equ trap_Parse_AddGlobalDefine -43 +equ trap_Parse_LoadSource -44 +equ trap_Parse_FreeSource -45 +equ trap_Parse_ReadToken -46 +equ trap_Parse_SourceFileAndLine -47 + +equ trap_SendGameStat -48 + + +equ memset -101 +equ memcpy -102 +equ strncpy -103 +equ sin -104 +equ cos -105 +equ atan2 -106 +equ sqrt -107 +equ floor -111 +equ ceil -112 +equ testPrintInt -113 +equ testPrintFloat -114 diff --git a/src/game/g_syscalls.c b/src/game/g_syscalls.c new file mode 100644 index 0000000..cb4ceb6 --- /dev/null +++ b/src/game/g_syscalls.c @@ -0,0 +1,284 @@ +/* +=========================================================================== +Copyright (C) 1999-2005 Id Software, Inc. +Copyright (C) 2000-2006 Tim Angus + +This file is part of Tremulous. + +Tremulous is free software; you can redistribute it +and/or modify it under the terms of the GNU General Public License as +published by the Free Software Foundation; either version 2 of the License, +or (at your option) any later version. + +Tremulous is distributed in the hope that it will be +useful, but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with Tremulous; if not, write to the Free Software +Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA +=========================================================================== +*/ + +#include "g_local.h" + +// this file is only included when building a dll +// g_syscalls.asm is included instead when building a qvm + +static intptr_t (QDECL *syscall)( intptr_t arg, ... ) = (intptr_t (QDECL *)( intptr_t, ...))-1; + + +void dllEntry( intptr_t (QDECL *syscallptr)( intptr_t arg,... ) ) +{ + syscall = syscallptr; +} + +int PASSFLOAT( float x ) +{ + float floatTemp; + floatTemp = x; + return *(int *)&floatTemp; +} + +void trap_Printf( const char *fmt ) +{ + syscall( G_PRINT, fmt ); +} + +void trap_Error( const char *fmt ) +{ + syscall( G_ERROR, fmt ); +} + +int trap_Milliseconds( void ) +{ + return syscall( G_MILLISECONDS ); +} +int trap_Argc( void ) +{ + return syscall( G_ARGC ); +} + +void trap_Argv( int n, char *buffer, int bufferLength ) +{ + syscall( G_ARGV, n, buffer, bufferLength ); +} + +int trap_FS_FOpenFile( const char *qpath, fileHandle_t *f, fsMode_t mode ) +{ + return syscall( G_FS_FOPEN_FILE, qpath, f, mode ); +} + +void trap_FS_Read( void *buffer, int len, fileHandle_t f ) +{ + syscall( G_FS_READ, buffer, len, f ); +} + +void trap_FS_Write( const void *buffer, int len, fileHandle_t f ) +{ + syscall( G_FS_WRITE, buffer, len, f ); +} + +void trap_FS_FCloseFile( fileHandle_t f ) +{ + syscall( G_FS_FCLOSE_FILE, f ); +} + +int trap_FS_GetFileList( const char *path, const char *extension, char *listbuf, int bufsize ) +{ + return syscall( G_FS_GETFILELIST, path, extension, listbuf, bufsize ); +} + +void trap_SendConsoleCommand( int exec_when, const char *text ) +{ + syscall( G_SEND_CONSOLE_COMMAND, exec_when, text ); +} + +void trap_Cvar_Register( vmCvar_t *cvar, const char *var_name, const char *value, int flags ) +{ + syscall( G_CVAR_REGISTER, cvar, var_name, value, flags ); +} + +void trap_Cvar_Update( vmCvar_t *cvar ) +{ + syscall( G_CVAR_UPDATE, cvar ); +} + +void trap_Cvar_Set( const char *var_name, const char *value ) +{ + syscall( G_CVAR_SET, var_name, value ); +} + +int trap_Cvar_VariableIntegerValue( const char *var_name ) +{ + return syscall( G_CVAR_VARIABLE_INTEGER_VALUE, var_name ); +} + +void trap_Cvar_VariableStringBuffer( const char *var_name, char *buffer, int bufsize ) +{ + syscall( G_CVAR_VARIABLE_STRING_BUFFER, var_name, buffer, bufsize ); +} + + +void trap_LocateGameData( gentity_t *gEnts, int numGEntities, int sizeofGEntity_t, + playerState_t *clients, int sizeofGClient ) +{ + syscall( G_LOCATE_GAME_DATA, gEnts, numGEntities, sizeofGEntity_t, clients, sizeofGClient ); +} + +void trap_DropClient( int clientNum, const char *reason ) +{ + syscall( G_DROP_CLIENT, clientNum, reason ); +} + +void trap_SendServerCommand( int clientNum, const char *text ) +{ + syscall( G_SEND_SERVER_COMMAND, clientNum, text ); +} + +void trap_SetConfigstring( int num, const char *string ) +{ + syscall( G_SET_CONFIGSTRING, num, string ); +} + +void trap_GetConfigstring( int num, char *buffer, int bufferSize ) +{ + syscall( G_GET_CONFIGSTRING, num, buffer, bufferSize ); +} + +void trap_GetUserinfo( int num, char *buffer, int bufferSize ) +{ + syscall( G_GET_USERINFO, num, buffer, bufferSize ); +} + +void trap_SetUserinfo( int num, const char *buffer ) +{ + syscall( G_SET_USERINFO, num, buffer ); +} + +void trap_GetServerinfo( char *buffer, int bufferSize ) +{ + syscall( G_GET_SERVERINFO, buffer, bufferSize ); +} + +void trap_SetBrushModel( gentity_t *ent, const char *name ) +{ + syscall( G_SET_BRUSH_MODEL, ent, name ); +} + +void trap_Trace( trace_t *results, const vec3_t start, const vec3_t mins, + const vec3_t maxs, const vec3_t end, int passEntityNum, int contentmask ) +{ + syscall( G_TRACE, results, start, mins, maxs, end, passEntityNum, contentmask ); +} + +void trap_TraceCapsule( trace_t *results, const vec3_t start, const vec3_t mins, const vec3_t maxs, const vec3_t end, int passEntityNum, int contentmask ) +{ + syscall( G_TRACECAPSULE, results, start, mins, maxs, end, passEntityNum, contentmask ); +} + +int trap_PointContents( const vec3_t point, int passEntityNum ) +{ + return syscall( G_POINT_CONTENTS, point, passEntityNum ); +} + + +qboolean trap_InPVS( const vec3_t p1, const vec3_t p2 ) +{ + return syscall( G_IN_PVS, p1, p2 ); +} + +qboolean trap_InPVSIgnorePortals( const vec3_t p1, const vec3_t p2 ) +{ + return syscall( G_IN_PVS_IGNORE_PORTALS, p1, p2 ); +} + +void trap_AdjustAreaPortalState( gentity_t *ent, qboolean open ) +{ + syscall( G_ADJUST_AREA_PORTAL_STATE, ent, open ); +} + +qboolean trap_AreasConnected( int area1, int area2 ) +{ + return syscall( G_AREAS_CONNECTED, area1, area2 ); +} + +void trap_LinkEntity( gentity_t *ent ) +{ + syscall( G_LINKENTITY, ent ); +} + +void trap_UnlinkEntity( gentity_t *ent ) +{ + syscall( G_UNLINKENTITY, ent ); +} + + +int trap_EntitiesInBox( const vec3_t mins, const vec3_t maxs, int *list, int maxcount ) +{ + return syscall( G_ENTITIES_IN_BOX, mins, maxs, list, maxcount ); +} + +qboolean trap_EntityContact( const vec3_t mins, const vec3_t maxs, const gentity_t *ent ) +{ + return syscall( G_ENTITY_CONTACT, mins, maxs, ent ); +} + +qboolean trap_EntityContactCapsule( const vec3_t mins, const vec3_t maxs, const gentity_t *ent ) +{ + return syscall( G_ENTITY_CONTACTCAPSULE, mins, maxs, ent ); +} + +void trap_GetUsercmd( int clientNum, usercmd_t *cmd ) +{ + syscall( G_GET_USERCMD, clientNum, cmd ); +} + +qboolean trap_GetEntityToken( char *buffer, int bufferSize ) +{ + return syscall( G_GET_ENTITY_TOKEN, buffer, bufferSize ); +} + +int trap_RealTime( qtime_t *qtime ) +{ + return syscall( G_REAL_TIME, qtime ); +} + +void trap_SnapVector( float *v ) +{ + syscall( G_SNAPVECTOR, v ); + return; +} + +void trap_SendGameStat( const char *data ) +{ + syscall( G_SEND_GAMESTAT, data ); + return; +} + +int trap_Parse_AddGlobalDefine( char *define ) +{ + return syscall( G_PARSE_ADD_GLOBAL_DEFINE, define ); +} + +int trap_Parse_LoadSource( const char *filename ) +{ + return syscall( G_PARSE_LOAD_SOURCE, filename ); +} + +int trap_Parse_FreeSource( int handle ) +{ + return syscall( G_PARSE_FREE_SOURCE, handle ); +} + +int trap_Parse_ReadToken( int handle, pc_token_t *pc_token ) +{ + return syscall( G_PARSE_READ_TOKEN, handle, pc_token ); +} + +int trap_Parse_SourceFileAndLine( int handle, char *filename, int *line ) +{ + return syscall( G_PARSE_SOURCE_FILE_AND_LINE, handle, filename, line ); +} + diff --git a/src/game/g_target.c b/src/game/g_target.c new file mode 100644 index 0000000..467920a --- /dev/null +++ b/src/game/g_target.c @@ -0,0 +1,478 @@ +/* +=========================================================================== +Copyright (C) 1999-2005 Id Software, Inc. +Copyright (C) 2000-2006 Tim Angus + +This file is part of Tremulous. + +Tremulous is free software; you can redistribute it +and/or modify it under the terms of the GNU General Public License as +published by the Free Software Foundation; either version 2 of the License, +or (at your option) any later version. + +Tremulous is distributed in the hope that it will be +useful, but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with Tremulous; if not, write to the Free Software +Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA +=========================================================================== +*/ + +#include "g_local.h" + +//========================================================== + +/*QUAKED target_delay (1 0 0) (-8 -8 -8) (8 8 8) +"wait" seconds to pause before firing targets. +"random" delay variance, total delay = delay +/- random seconds +*/ +void Think_Target_Delay( gentity_t *ent ) +{ + G_UseTargets( ent, ent->activator ); +} + +void Use_Target_Delay( gentity_t *ent, gentity_t *other, gentity_t *activator ) +{ + ent->nextthink = level.time + ( ent->wait + ent->random * crandom( ) ) * 1000; + ent->think = Think_Target_Delay; + ent->activator = activator; +} + +void SP_target_delay( gentity_t *ent ) +{ + // check delay for backwards compatability + if( !G_SpawnFloat( "delay", "0", &ent->wait ) ) + G_SpawnFloat( "wait", "1", &ent->wait ); + + if( !ent->wait ) + ent->wait = 1; + + ent->use = Use_Target_Delay; +} + + +//========================================================== + +/*QUAKED target_score (1 0 0) (-8 -8 -8) (8 8 8) +"count" number of points to add, default 1 + +The activator is given this many points. +*/ +void Use_Target_Score( gentity_t *ent, gentity_t *other, gentity_t *activator ) +{ + if( !activator ) + return; + + AddScore( activator, ent->count ); +} + +void SP_target_score( gentity_t *ent ) +{ + if( !ent->count ) + ent->count = 1; + + ent->use = Use_Target_Score; +} + + +//========================================================== + +/*QUAKED target_print (1 0 0) (-8 -8 -8) (8 8 8) humanteam alienteam private +"message" text to print +If "private", only the activator gets the message. If no checks, all clients get the message. +*/ +void Use_Target_Print( gentity_t *ent, gentity_t *other, gentity_t *activator ) +{ + if( activator && activator->client && ( ent->spawnflags & 4 ) ) + { + trap_SendServerCommand( activator-g_entities, va( "cp \"%s\"", ent->message ) ); + return; + } + + if( ent->spawnflags & 3 ) + { + if( ent->spawnflags & 1 ) + G_TeamCommand( PTE_HUMANS, va( "cp \"%s\"", ent->message ) ); + if( ent->spawnflags & 2 ) + G_TeamCommand( PTE_ALIENS, va( "cp \"%s\"", ent->message ) ); + + return; + } + + trap_SendServerCommand( -1, va("cp \"%s\"", ent->message ) ); +} + +void SP_target_print( gentity_t *ent ) +{ + ent->use = Use_Target_Print; +} + + +//========================================================== + + +/*QUAKED target_speaker (1 0 0) (-8 -8 -8) (8 8 8) looped-on looped-off global activator +"noise" wav file to play + +A global sound will play full volume throughout the level. +Activator sounds will play on the player that activated the target. +Global and activator sounds can't be combined with looping. +Normal sounds play each time the target is used. +Looped sounds will be toggled by use functions. +Multiple identical looping sounds will just increase volume without any speed cost. +"wait" : Seconds between auto triggerings, 0 = don't auto trigger +"random" wait variance, default is 0 +*/ +void Use_Target_Speaker( gentity_t *ent, gentity_t *other, gentity_t *activator ) +{ + if( ent->spawnflags & 3 ) + { // looping sound toggles + if( ent->s.loopSound ) + ent->s.loopSound = 0; // turn it off + else + ent->s.loopSound = ent->noise_index; // start it + } + else + { + // normal sound + if( ent->spawnflags & 8 && activator ) + G_AddEvent( activator, EV_GENERAL_SOUND, ent->noise_index ); + else if( ent->spawnflags & 4 ) + G_AddEvent( ent, EV_GLOBAL_SOUND, ent->noise_index ); + else + G_AddEvent( ent, EV_GENERAL_SOUND, ent->noise_index ); + } +} + +void SP_target_speaker( gentity_t *ent ) +{ + char buffer[ MAX_QPATH ]; + char *s; + + G_SpawnFloat( "wait", "0", &ent->wait ); + G_SpawnFloat( "random", "0", &ent->random ); + + if( !G_SpawnString( "noise", "NOSOUND", &s ) ) + G_Error( "target_speaker without a noise key at %s", vtos( ent->s.origin ) ); + + // force all client reletive sounds to be "activator" speakers that + // play on the entity that activates it + if( s[ 0 ] == '*' ) + ent->spawnflags |= 8; + + if( !strstr( s, ".wav" ) ) + Com_sprintf( buffer, sizeof( buffer ), "%s.wav", s ); + else + Q_strncpyz( buffer, s, sizeof( buffer ) ); + + ent->noise_index = G_SoundIndex( buffer ); + + // a repeating speaker can be done completely client side + ent->s.eType = ET_SPEAKER; + ent->s.eventParm = ent->noise_index; + ent->s.frame = ent->wait * 10; + ent->s.clientNum = ent->random * 10; + + + // check for prestarted looping sound + if( ent->spawnflags & 1 ) + ent->s.loopSound = ent->noise_index; + + ent->use = Use_Target_Speaker; + + if( ent->spawnflags & 4 ) + ent->r.svFlags |= SVF_BROADCAST; + + VectorCopy( ent->s.origin, ent->s.pos.trBase ); + + // must link the entity so we get areas and clusters so + // the server can determine who to send updates to + trap_LinkEntity( ent ); +} + +//========================================================== + +void target_teleporter_use( gentity_t *self, gentity_t *other, gentity_t *activator ) +{ + gentity_t *dest; + + if( !activator || !activator->client ) + return; + + dest = G_PickTarget( self->target ); + + if( !dest ) + { + G_Printf( "Couldn't find teleporter destination\n" ); + return; + } + + TeleportPlayer( activator, dest->s.origin, dest->s.angles ); +} + +/*QUAKED target_teleporter (1 0 0) (-8 -8 -8) (8 8 8) +The activator will be teleported away. +*/ +void SP_target_teleporter( gentity_t *self ) +{ + if( !self->targetname ) + G_Printf( "untargeted %s at %s\n", self->classname, vtos( self->s.origin ) ); + + self->use = target_teleporter_use; +} + +//========================================================== + + +/*QUAKED target_relay (.5 .5 .5) (-8 -8 -8) (8 8 8) RED_ONLY BLUE_ONLY RANDOM +This doesn't perform any actions except fire its targets. +The activator can be forced to be from a certain team. +if RANDOM is checked, only one of the targets will be fired, not all of them +*/ +void target_relay_use( gentity_t *self, gentity_t *other, gentity_t *activator ) +{ + if( ( self->spawnflags & 1 ) && activator && activator->client && + activator->client->ps.stats[ STAT_PTEAM ] != PTE_HUMANS ) + return; + + if( ( self->spawnflags & 2 ) && activator && activator->client && + activator->client->ps.stats[ STAT_PTEAM ] != PTE_ALIENS ) + return; + + if( self->spawnflags & 4 ) + { + gentity_t *ent; + + ent = G_PickTarget( self->target ); + if( ent && ent->use ) + ent->use( ent, self, activator ); + + return; + } + + G_UseTargets( self, activator ); +} + +void SP_target_relay( gentity_t *self ) +{ + self->use = target_relay_use; +} + + +//========================================================== + +/*QUAKED target_kill (.5 .5 .5) (-8 -8 -8) (8 8 8) +Kills the activator. +*/ +void target_kill_use( gentity_t *self, gentity_t *other, gentity_t *activator ) +{ + if( !activator ) + return; + + G_Damage( activator, NULL, NULL, NULL, NULL, 100000, DAMAGE_NO_PROTECTION, MOD_TELEFRAG ); +} + +void SP_target_kill( gentity_t *self ) +{ + self->use = target_kill_use; +} + +/*QUAKED target_position (0 0.5 0) (-4 -4 -4) (4 4 4) +Used as a positional target for in-game calculation, like jumppad targets. +*/ +void SP_target_position( gentity_t *self ) +{ + G_SetOrigin( self, self->s.origin ); +} + +static void target_location_linkup( gentity_t *ent ) +{ + int i; + int n; + + if( level.locationLinked ) + return; + + level.locationLinked = qtrue; + + level.locationHead = NULL; + + trap_SetConfigstring( CS_LOCATIONS, "unknown" ); + + for( i = 0, ent = g_entities, n = 1; i < level.num_entities; i++, ent++) + { + if( ent->classname && !Q_stricmp( ent->classname, "target_location" ) ) + { + // lets overload some variables! + ent->health = n; // use for location marking + trap_SetConfigstring( CS_LOCATIONS + n, ent->message ); + n++; + ent->nextTrain = level.locationHead; + level.locationHead = ent; + } + } + // All linked together now +} + +/*QUAKED target_location (0 0.5 0) (-8 -8 -8) (8 8 8) +Set "message" to the name of this location. +Set "count" to 0-7 for color. +0:white 1:red 2:green 3:yellow 4:blue 5:cyan 6:magenta 7:white + +Closest target_location in sight used for the location, if none +in site, closest in distance +*/ +void SP_target_location( gentity_t *self ) +{ + self->think = target_location_linkup; + self->nextthink = level.time + 200; // Let them all spawn first + + G_SetOrigin( self, self->s.origin ); +} + + +/* +=============== +target_rumble_think +=============== +*/ +void target_rumble_think( gentity_t *self ) +{ + int i; + gentity_t *ent; + + if( self->last_move_time < level.time ) + self->last_move_time = level.time + 0.5; + + for( i = 0, ent = g_entities + i; i < level.num_entities; i++, ent++ ) + { + if( !ent->inuse ) + continue; + + if( !ent->client ) + continue; + + if( ent->client->ps.groundEntityNum == ENTITYNUM_NONE ) + continue; + + ent->client->ps.groundEntityNum = ENTITYNUM_NONE; + ent->client->ps.velocity[ 0 ] += crandom( ) * 150; + ent->client->ps.velocity[ 1 ] += crandom( ) * 150; + ent->client->ps.velocity[ 2 ] = self->speed; + } + + if( level.time < self->timestamp ) + self->nextthink = level.time + FRAMETIME; +} + +/* +=============== +target_rumble_use +=============== +*/ +void target_rumble_use( gentity_t *self, gentity_t *other, gentity_t *activator ) +{ + self->timestamp = level.time + ( self->count * FRAMETIME ); + self->nextthink = level.time + FRAMETIME; + self->activator = activator; + self->last_move_time = 0; +} + +/* +=============== +SP_target_rumble +=============== +*/ +void SP_target_rumble( gentity_t *self ) +{ + if( !self->targetname ) + { + G_Printf( S_COLOR_YELLOW "WARNING: untargeted %s at %s\n", self->classname, + vtos( self->s.origin ) ); + } + + if( !self->count ) + self->count = 10; + + if( !self->speed ) + self->speed = 100; + + self->think = target_rumble_think; + self->use = target_rumble_use; +} + +/* +=============== +target_alien_win_use +=============== +*/ +void target_alien_win_use( gentity_t *self, gentity_t *other, gentity_t *activator ) +{ + level.uncondAlienWin = qtrue; +} + +/* +=============== +SP_target_alien_win +=============== +*/ +void SP_target_alien_win( gentity_t *self ) +{ + self->use = target_alien_win_use; +} + +/* +=============== +target_human_win_use +=============== +*/ +void target_human_win_use( gentity_t *self, gentity_t *other, gentity_t *activator ) +{ + level.uncondHumanWin = qtrue; +} + +/* +=============== +SP_target_human_win +=============== +*/ +void SP_target_human_win( gentity_t *self ) +{ + self->use = target_human_win_use; +} + +/* +=============== +target_hurt_use +=============== +*/ +void target_hurt_use( gentity_t *self, gentity_t *other, gentity_t *activator ) +{ + // hurt the activator + if( !activator || !activator->takedamage ) + return; + + G_Damage( activator, self, self, NULL, NULL, self->damage, 0, MOD_TRIGGER_HURT ); +} + +/* +=============== +SP_target_hurt +=============== +*/ +void SP_target_hurt( gentity_t *self ) +{ + if( !self->targetname ) + { + G_Printf( S_COLOR_YELLOW "WARNING: untargeted %s at %s\n", self->classname, + vtos( self->s.origin ) ); + } + + if( !self->damage ) + self->damage = 5; + + self->use = target_hurt_use; +} diff --git a/src/game/g_team.c b/src/game/g_team.c new file mode 100644 index 0000000..133711b --- /dev/null +++ b/src/game/g_team.c @@ -0,0 +1,276 @@ +/* +=========================================================================== +Copyright (C) 1999-2005 Id Software, Inc. +Copyright (C) 2000-2006 Tim Angus + +This file is part of Tremulous. + +Tremulous is free software; you can redistribute it +and/or modify it under the terms of the GNU General Public License as +published by the Free Software Foundation; either version 2 of the License, +or (at your option) any later version. + +Tremulous is distributed in the hope that it will be +useful, but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with Tremulous; if not, write to the Free Software +Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA +=========================================================================== +*/ + +#include "g_local.h" + +// NULL for everyone +void QDECL PrintMsg( gentity_t *ent, const char *fmt, ... ) +{ + char msg[ 1024 ]; + va_list argptr; + char *p; + + va_start( argptr,fmt ); + + if( vsprintf( msg, fmt, argptr ) > sizeof( msg ) ) + G_Error ( "PrintMsg overrun" ); + + va_end( argptr ); + + // double quotes are bad + while( ( p = strchr( msg, '"' ) ) != NULL ) + *p = '\''; + + trap_SendServerCommand( ( ( ent == NULL ) ? -1 : ent-g_entities ), va( "print \"%s\"", msg ) ); +} + + +/* +============== +OnSameTeam +============== +*/ +qboolean OnSameTeam( gentity_t *ent1, gentity_t *ent2 ) +{ + if( !ent1->client || !ent2->client ) + return qfalse; + + if( ent1->client->pers.teamSelection == ent2->client->pers.teamSelection ) + return qtrue; + + return qfalse; +} + +/* +=========== +Team_GetLocation + +Report a location for the player. Uses placed nearby target_location entities +============ +*/ +gentity_t *Team_GetLocation( gentity_t *ent ) +{ + gentity_t *eloc, *best; + float bestlen, len; + vec3_t origin; + + best = NULL; + bestlen = 3.0f * 8192.0f * 8192.0f; + + VectorCopy( ent->r.currentOrigin, origin ); + + for( eloc = level.locationHead; eloc; eloc = eloc->nextTrain ) + { + len = ( origin[ 0 ] - eloc->r.currentOrigin[ 0 ] ) * ( origin[ 0 ] - eloc->r.currentOrigin[ 0 ] ) + + ( origin[ 1 ] - eloc->r.currentOrigin[ 1 ] ) * ( origin[ 1 ] - eloc->r.currentOrigin[ 1 ] ) + + ( origin[ 2 ] - eloc->r.currentOrigin[ 2 ] ) * ( origin[ 2 ] - eloc->r.currentOrigin[ 2 ] ); + + if( len > bestlen ) + continue; + + if( !trap_InPVS( origin, eloc->r.currentOrigin ) ) + continue; + + bestlen = len; + best = eloc; + } + + return best; +} + + +/* +=========== +Team_GetLocationMsg + +Report a location message for the player. Uses placed nearby target_location entities +============ +*/ +qboolean Team_GetLocationMsg( gentity_t *ent, char *loc, int loclen ) +{ + gentity_t *best; + + best = Team_GetLocation( ent ); + + if( !best ) + return qfalse; + + if( best->count ) + { + if( best->count < 0 ) + best->count = 0; + + if( best->count > 7 ) + best->count = 7; + + Com_sprintf( loc, loclen, "%c%c%s" S_COLOR_WHITE, Q_COLOR_ESCAPE, best->count + '0', best->message ); + } + else + Com_sprintf( loc, loclen, "%s", best->message ); + + return qtrue; +} + + +/*---------------------------------------------------------------------------*/ + +static int QDECL SortClients( const void *a, const void *b ) +{ + return *(int *)a - *(int *)b; +} + + +/* +================== +TeamplayLocationsMessage + +Format: + clientNum location health armor weapon powerups + +================== +*/ +void TeamplayInfoMessage( gentity_t *ent ) +{ + char entry[ 1024 ]; + char string[ 8192 ]; + int stringlength; + int i, j; + gentity_t *player; + int cnt; + int h, a = 0; + int clients[ TEAM_MAXOVERLAY ]; + + if( ! ent->client->pers.teamInfo ) + return; + + // figure out what client should be on the display + // we are limited to 8, but we want to use the top eight players + // but in client order (so they don't keep changing position on the overlay) + for( i = 0, cnt = 0; i < g_maxclients.integer && cnt < TEAM_MAXOVERLAY; i++ ) + { + player = g_entities + level.sortedClients[ i ]; + + if( player->inuse && player->client->sess.sessionTeam == + ent->client->sess.sessionTeam ) + clients[ cnt++ ] = level.sortedClients[ i ]; + } + + // We have the top eight players, sort them by clientNum + qsort( clients, cnt, sizeof( clients[ 0 ] ), SortClients ); + + // send the latest information on all clients + string[ 0 ] = 0; + stringlength = 0; + + for( i = 0, cnt = 0; i < g_maxclients.integer && cnt < TEAM_MAXOVERLAY; i++) + { + player = g_entities + i; + + if( player->inuse && player->client->sess.sessionTeam == + ent->client->sess.sessionTeam ) + { + h = player->client->ps.stats[ STAT_HEALTH ]; + + if( h < 0 ) + h = 0; + + Com_sprintf( entry, sizeof( entry ), + " %i %i %i %i %i %i", +// level.sortedClients[i], player->client->pers.teamState.location, h, a, + i, player->client->pers.teamState.location, h, a, + player->client->ps.weapon, player->s.powerups ); + + j = strlen( entry ); + + if( stringlength + j > sizeof( string ) ) + break; + + strcpy( string + stringlength, entry ); + stringlength += j; + cnt++; + } + } + + trap_SendServerCommand( ent - g_entities, va( "tinfo %i %s", cnt, string ) ); +} + +void CheckTeamStatus( void ) +{ + int i; + gentity_t *loc, *ent; + + if( level.time - level.lastTeamLocationTime > TEAM_LOCATION_UPDATE_TIME ) + { + level.lastTeamLocationTime = level.time; + + for( i = 0; i < g_maxclients.integer; i++ ) + { + ent = g_entities + i; + if( ent->client->pers.connected != CON_CONNECTED ) + continue; + + if( ent->inuse && ( ent->client->ps.stats[ STAT_PTEAM ] == PTE_HUMANS || + ent->client->ps.stats[ STAT_PTEAM ] == PTE_ALIENS ) ) + { + + loc = Team_GetLocation( ent ); + + if( loc ) + ent->client->pers.teamState.location = loc->health; + else + ent->client->pers.teamState.location = 0; + } + } + + for( i = 0; i < g_maxclients.integer; i++ ) + { + ent = g_entities + i; + if( ent->client->pers.connected != CON_CONNECTED ) + continue; + + if( ent->inuse && ( ent->client->ps.stats[ STAT_PTEAM ] == PTE_HUMANS || + ent->client->ps.stats[ STAT_PTEAM ] == PTE_ALIENS ) ) + TeamplayInfoMessage( ent ); + } + } + + //Warn on unbalanced teams + if ( g_teamImbalanceWarnings.integer && !level.intermissiontime && level.time - level.lastTeamUnbalancedTime > ( g_teamImbalanceWarnings.integer * 1000 ) && level.numTeamWarnings<3 ) + { + level.lastTeamUnbalancedTime = level.time; + if (level.numAlienSpawns > 0 && level.numHumanClients - level.numAlienClients > 2) + { + trap_SendServerCommand (-1, "print \"Teams are unbalanced. Humans have more players.\n Humans will keep their points when switching teams.\n\""); + level.numTeamWarnings++; + } + else if (level.numHumanSpawns > 0 && level.numAlienClients - level.numHumanClients > 2) + { + trap_SendServerCommand (-1, "print \"Teams are unbalanced. Aliens have more players.\n Aliens will keep their points when switching teams.\n\""); + level.numTeamWarnings++; + } + else + { + level.numTeamWarnings = 0; + } + } +} diff --git a/src/game/g_trigger.c b/src/game/g_trigger.c new file mode 100644 index 0000000..2b5b25a --- /dev/null +++ b/src/game/g_trigger.c @@ -0,0 +1,1141 @@ +/* +=========================================================================== +Copyright (C) 1999-2005 Id Software, Inc. +Copyright (C) 2000-2006 Tim Angus + +This file is part of Tremulous. + +Tremulous is free software; you can redistribute it +and/or modify it under the terms of the GNU General Public License as +published by the Free Software Foundation; either version 2 of the License, +or (at your option) any later version. + +Tremulous is distributed in the hope that it will be +useful, but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with Tremulous; if not, write to the Free Software +Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA +=========================================================================== +*/ + +#include "g_local.h" + + +void InitTrigger( gentity_t *self ) +{ + if( !VectorCompare( self->s.angles, vec3_origin ) ) + G_SetMovedir( self->s.angles, self->movedir ); + + trap_SetBrushModel( self, self->model ); + self->r.contents = CONTENTS_TRIGGER; // replaces the -1 from trap_SetBrushModel + self->r.svFlags = SVF_NOCLIENT; +} + + +// the wait time has passed, so set back up for another activation +void multi_wait( gentity_t *ent ) +{ + ent->nextthink = 0; +} + + +// the trigger was just activated +// ent->activator should be set to the activator so it can be held through a delay +// so wait for the delay time before firing +void multi_trigger( gentity_t *ent, gentity_t *activator ) +{ + ent->activator = activator; + if( ent->nextthink ) + return; // can't retrigger until the wait is over + + if( activator->client ) + { + if( ( ent->spawnflags & 1 ) && + activator->client->ps.stats[ STAT_PTEAM ] != PTE_HUMANS ) + return; + + if( ( ent->spawnflags & 2 ) && + activator->client->ps.stats[ STAT_PTEAM ] != PTE_ALIENS ) + return; + } + + G_UseTargets( ent, ent->activator ); + + if( ent->wait > 0 ) + { + ent->think = multi_wait; + ent->nextthink = level.time + ( ent->wait + ent->random * crandom( ) ) * 1000; + } + else + { + // we can't just remove (self) here, because this is a touch function + // called while looping through area links... + ent->touch = 0; + ent->nextthink = level.time + FRAMETIME; + ent->think = G_FreeEntity; + } +} + +void Use_Multi( gentity_t *ent, gentity_t *other, gentity_t *activator ) +{ + multi_trigger( ent, activator ); +} + +void Touch_Multi( gentity_t *self, gentity_t *other, trace_t *trace ) +{ + if( !other->client && other->s.eType != ET_BUILDABLE ) + return; + + multi_trigger( self, other ); +} + +/*QUAKED trigger_multiple (.5 .5 .5) ? +"wait" : Seconds between triggerings, 0.5 default, -1 = one time only. +"random" wait variance, default is 0 +Variable sized repeatable trigger. Must be targeted at one or more entities. +so, the basic time between firing is a random time between +(wait - random) and (wait + random) +*/ +void SP_trigger_multiple( gentity_t *ent ) +{ + G_SpawnFloat( "wait", "0.5", &ent->wait ); + G_SpawnFloat( "random", "0", &ent->random ); + + if( ent->random >= ent->wait && ent->wait >= 0 ) + { + ent->random = ent->wait - FRAMETIME; + G_Printf( "trigger_multiple has random >= wait\n" ); + } + + ent->touch = Touch_Multi; + ent->use = Use_Multi; + + InitTrigger( ent ); + trap_LinkEntity( ent ); +} + + + +/* +============================================================================== + +trigger_always + +============================================================================== +*/ + +void trigger_always_think( gentity_t *ent ) +{ + G_UseTargets( ent, ent ); + G_FreeEntity( ent ); +} + +/*QUAKED trigger_always (.5 .5 .5) (-8 -8 -8) (8 8 8) +This trigger will always fire. It is activated by the world. +*/ +void SP_trigger_always( gentity_t *ent ) +{ + // we must have some delay to make sure our use targets are present + ent->nextthink = level.time + 300; + ent->think = trigger_always_think; +} + + +/* +============================================================================== + +trigger_push + +============================================================================== +*/ + +void trigger_push_touch( gentity_t *self, gentity_t *other, trace_t *trace ) +{ + if( !other->client ) + return; +} + + +/* +================= +AimAtTarget + +Calculate origin2 so the target apogee will be hit +================= +*/ +void AimAtTarget( gentity_t *self ) +{ + gentity_t *ent; + vec3_t origin; + float height, gravity, time, forward; + float dist; + + VectorAdd( self->r.absmin, self->r.absmax, origin ); + VectorScale( origin, 0.5, origin ); + + ent = G_PickTarget( self->target ); + + if( !ent ) + { + G_FreeEntity( self ); + return; + } + + height = ent->s.origin[ 2 ] - origin[ 2 ]; + gravity = g_gravity.value; + time = sqrt( height / ( 0.5 * gravity ) ); + + if( !time ) + { + G_FreeEntity( self ); + return; + } + + // set s.origin2 to the push velocity + VectorSubtract( ent->s.origin, origin, self->s.origin2 ); + self->s.origin2[ 2 ] = 0; + dist = VectorNormalize( self->s.origin2 ); + + forward = dist / time; + VectorScale( self->s.origin2, forward, self->s.origin2 ); + + self->s.origin2[ 2 ] = time * gravity; +} + + +/*QUAKED trigger_push (.5 .5 .5) ? +Must point at a target_position, which will be the apex of the leap. +This will be client side predicted, unlike target_push +*/ +void SP_trigger_push( gentity_t *self ) +{ + InitTrigger( self ); + + // unlike other triggers, we need to send this one to the client + self->r.svFlags &= ~SVF_NOCLIENT; + + self->s.eType = ET_PUSH_TRIGGER; + self->touch = trigger_push_touch; + self->think = AimAtTarget; + self->nextthink = level.time + FRAMETIME; + trap_LinkEntity( self ); +} + + +void Use_target_push( gentity_t *self, gentity_t *other, gentity_t *activator ) +{ + if( !activator->client ) + return; + + if( activator->client->ps.pm_type != PM_NORMAL ) + return; + + VectorCopy( self->s.origin2, activator->client->ps.velocity ); + +} + +/*QUAKED target_push (.5 .5 .5) (-8 -8 -8) (8 8 8) +Pushes the activator in the direction.of angle, or towards a target apex. +"speed" defaults to 1000 +*/ +void SP_target_push( gentity_t *self ) +{ + if( !self->speed ) + self->speed = 1000; + + G_SetMovedir( self->s.angles, self->s.origin2 ); + VectorScale( self->s.origin2, self->speed, self->s.origin2 ); + + if( self->target ) + { + VectorCopy( self->s.origin, self->r.absmin ); + VectorCopy( self->s.origin, self->r.absmax ); + self->think = AimAtTarget; + self->nextthink = level.time + FRAMETIME; + } + + self->use = Use_target_push; +} + +/* +============================================================================== + +trigger_teleport + +============================================================================== +*/ + +void trigger_teleporter_touch( gentity_t *self, gentity_t *other, trace_t *trace ) +{ + gentity_t *dest; + + if( self->s.eFlags & EF_NODRAW ) + return; + + if( !other->client ) + return; + + if( other->client->ps.pm_type == PM_DEAD ) + return; + + // Spectators only? + if( ( self->spawnflags & 1 ) && + other->client->sess.sessionTeam != TEAM_SPECTATOR ) + return; + + + dest = G_PickTarget( self->target ); + + if( !dest ) + { + G_Printf( "Couldn't find teleporter destination\n" ); + return; + } + + TeleportPlayer( other, dest->s.origin, dest->s.angles ); +} + +/* +=============== +trigger_teleport_use +=============== +*/ +void trigger_teleporter_use( gentity_t *ent, gentity_t *other, gentity_t *activator ) +{ + ent->s.eFlags ^= EF_NODRAW; +} + + +/*QUAKED trigger_teleport (.5 .5 .5) ? SPECTATOR SPAWN_DISABLED +Allows client side prediction of teleportation events. +Must point at a target_position, which will be the teleport destination. + +If spectator is set, only spectators can use this teleport +Spectator teleporters are not normally placed in the editor, but are created +automatically near doors to allow spectators to move through them +*/ +void SP_trigger_teleport( gentity_t *self ) +{ + InitTrigger( self ); + + // unlike other triggers, we need to send this one to the client + // unless is a spectator trigger + if( self->spawnflags & 1 ) + self->r.svFlags |= SVF_NOCLIENT; + else + self->r.svFlags &= ~SVF_NOCLIENT; + + // SPAWN_DISABLED + if( self->spawnflags & 2 ) + self->s.eFlags |= EF_NODRAW; + + self->s.eType = ET_TELEPORT_TRIGGER; + self->touch = trigger_teleporter_touch; + self->use = trigger_teleporter_use; + + trap_LinkEntity( self ); +} + + +/* +============================================================================== + +trigger_hurt + +============================================================================== +*/ + +/*QUAKED trigger_hurt (.5 .5 .5) ? START_OFF - SILENT NO_PROTECTION SLOW +Any entity that touches this will be hurt. +It does dmg points of damage each server frame +Targeting the trigger will toggle its on / off state. + +SILENT supresses playing the sound +SLOW changes the damage rate to once per second +NO_PROTECTION *nothing* stops the damage + +"dmg" default 5 (whole numbers only) + +*/ +void hurt_use( gentity_t *self, gentity_t *other, gentity_t *activator ) +{ + if( self->r.linked ) + trap_UnlinkEntity( self ); + else + trap_LinkEntity( self ); +} + +void hurt_touch( gentity_t *self, gentity_t *other, trace_t *trace ) +{ + int dflags; + + if( !other->takedamage ) + return; + + if( self->timestamp > level.time ) + return; + + if( self->spawnflags & 16 ) + self->timestamp = level.time + 1000; + else + self->timestamp = level.time + FRAMETIME; + + // play sound + if( !( self->spawnflags & 4 ) ) + G_Sound( other, CHAN_AUTO, self->noise_index ); + + if( self->spawnflags & 8 ) + dflags = DAMAGE_NO_PROTECTION; + else + dflags = 0; + + G_Damage( other, self, self, NULL, NULL, self->damage, dflags, MOD_TRIGGER_HURT ); +} + +void SP_trigger_hurt( gentity_t *self ) +{ + InitTrigger( self ); + + self->noise_index = G_SoundIndex( "sound/misc/electro.wav" ); + self->touch = hurt_touch; + + if( self->damage <= 0 ) + self->damage = 5; + + self->r.contents = CONTENTS_TRIGGER; + + if( self->spawnflags & 2 ) + self->use = hurt_use; + + // link in to the world if starting active + if( !( self->spawnflags & 1 ) ) + trap_LinkEntity( self ); +} + + +/* +============================================================================== + +timer + +============================================================================== +*/ + + +/*QUAKED func_timer (0.3 0.1 0.6) (-8 -8 -8) (8 8 8) START_ON +This should be renamed trigger_timer... +Repeatedly fires its targets. +Can be turned on or off by using. + +"wait" base time between triggering all targets, default is 1 +"random" wait variance, default is 0 +so, the basic time between firing is a random time between +(wait - random) and (wait + random) + +*/ +void func_timer_think( gentity_t *self ) +{ + G_UseTargets( self, self->activator ); + // set time before next firing + self->nextthink = level.time + 1000 * ( self->wait + crandom( ) * self->random ); +} + +void func_timer_use( gentity_t *self, gentity_t *other, gentity_t *activator ) +{ + self->activator = activator; + + // if on, turn it off + if( self->nextthink ) + { + self->nextthink = 0; + return; + } + + // turn it on + func_timer_think( self ); +} + +void SP_func_timer( gentity_t *self ) +{ + G_SpawnFloat( "random", "1", &self->random ); + G_SpawnFloat( "wait", "1", &self->wait ); + + self->use = func_timer_use; + self->think = func_timer_think; + + if( self->random >= self->wait ) + { + self->random = self->wait - FRAMETIME; + G_Printf( "func_timer at %s has random >= wait\n", vtos( self->s.origin ) ); + } + + if( self->spawnflags & 1 ) + { + self->nextthink = level.time + FRAMETIME; + self->activator = self; + } + + self->r.svFlags = SVF_NOCLIENT; +} + + +/* +=============== +G_Checktrigger_stages + +Called when stages change +=============== +*/ +void G_Checktrigger_stages( pTeam_t team, stage_t stage ) +{ + int i; + gentity_t *ent; + + for( i = 1, ent = g_entities + i ; i < level.num_entities ; i++, ent++ ) + { + if( !ent->inuse ) + continue; + + if( !Q_stricmp( ent->classname, "trigger_stage" ) ) + { + if( team == ent->stageTeam && stage == ent->stageStage ) + ent->use( ent, ent, ent ); + } + } +} + + +/* +=============== +trigger_stage_use +=============== +*/ +void trigger_stage_use( gentity_t *self, gentity_t *other, gentity_t *activator ) +{ + G_UseTargets( self, self ); +} + +void SP_trigger_stage( gentity_t *self ) +{ + G_SpawnInt( "team", "0", (int *)&self->stageTeam ); + G_SpawnInt( "stage", "0", (int *)&self->stageStage ); + + self->use = trigger_stage_use; + + self->r.svFlags = SVF_NOCLIENT; +} + + +/* +=============== +trigger_win +=============== +*/ +void trigger_win( gentity_t *self, gentity_t *other, gentity_t *activator ) +{ + G_UseTargets( self, self ); +} + +void SP_trigger_win( gentity_t *self ) +{ + G_SpawnInt( "team", "0", (int *)&self->stageTeam ); + + self->use = trigger_win; + + self->r.svFlags = SVF_NOCLIENT; +} + + +/* +=============== +trigger_buildable_match +=============== +*/ +qboolean trigger_buildable_match( gentity_t *self, gentity_t *activator ) +{ + int i = 0; + + //if there is no buildable list every buildable triggers + if( self->bTriggers[ i ] == BA_NONE ) + return qtrue; + else + { + //otherwise check against the list + for( i = 0; self->bTriggers[ i ] != BA_NONE; i++ ) + { + if( activator->s.modelindex == self->bTriggers[ i ] ) + return qtrue; + } + } + + return qfalse; +} + +/* +=============== +trigger_buildable_trigger +=============== +*/ +void trigger_buildable_trigger( gentity_t *self, gentity_t *activator ) +{ + self->activator = activator; + + if( self->s.eFlags & EF_NODRAW ) + return; + + if( self->nextthink ) + return; // can't retrigger until the wait is over + + if( self->s.eFlags & EF_DEAD ) + { + if( !trigger_buildable_match( self, activator ) ) + G_UseTargets( self, activator ); + } + else + { + if( trigger_buildable_match( self, activator ) ) + G_UseTargets( self, activator ); + } + + if( self->wait > 0 ) + { + self->think = multi_wait; + self->nextthink = level.time + ( self->wait + self->random * crandom( ) ) * 1000; + } + else + { + // we can't just remove (self) here, because this is a touch function + // called while looping through area links... + self->touch = 0; + self->nextthink = level.time + FRAMETIME; + self->think = G_FreeEntity; + } +} + +/* +=============== +trigger_buildable_touch +=============== +*/ +void trigger_buildable_touch( gentity_t *ent, gentity_t *other, trace_t *trace ) +{ + //only triggered by buildables + if( other->s.eType != ET_BUILDABLE ) + return; + + trigger_buildable_trigger( ent, other ); +} + +/* +=============== +trigger_buildable_use +=============== +*/ +void trigger_buildable_use( gentity_t *ent, gentity_t *other, gentity_t *activator ) +{ + ent->s.eFlags ^= EF_NODRAW; +} + +/* +=============== +SP_trigger_buildable +=============== +*/ +void SP_trigger_buildable( gentity_t *self ) +{ + char *buffer; + + G_SpawnFloat( "wait", "0.5", &self->wait ); + G_SpawnFloat( "random", "0", &self->random ); + + if( self->random >= self->wait && self->wait >= 0 ) + { + self->random = self->wait - FRAMETIME; + G_Printf( S_COLOR_YELLOW "WARNING: trigger_buildable has random >= wait\n" ); + } + + G_SpawnString( "buildables", "", &buffer ); + + BG_ParseCSVBuildableList( buffer, self->bTriggers, BA_NUM_BUILDABLES ); + + self->touch = trigger_buildable_touch; + self->use = trigger_buildable_use; + + // SPAWN_DISABLED + if( self->spawnflags & 1 ) + self->s.eFlags |= EF_NODRAW; + + // NEGATE + if( self->spawnflags & 2 ) + self->s.eFlags |= EF_DEAD; + + InitTrigger( self ); + trap_LinkEntity( self ); +} + + +/* +=============== +trigger_class_match +=============== +*/ +qboolean trigger_class_match( gentity_t *self, gentity_t *activator ) +{ + int i = 0; + + //if there is no class list every class triggers (stupid case) + if( self->cTriggers[ i ] == PCL_NONE ) + return qtrue; + else + { + //otherwise check against the list + for( i = 0; self->cTriggers[ i ] != PCL_NONE; i++ ) + { + if( activator->client->ps.stats[ STAT_PCLASS ] == self->cTriggers[ i ] ) + return qtrue; + } + } + + return qfalse; +} + +/* +=============== +trigger_class_trigger +=============== +*/ +void trigger_class_trigger( gentity_t *self, gentity_t *activator ) +{ + //sanity check + if( !activator->client ) + return; + + if( activator->client->ps.stats[ STAT_PTEAM ] != PTE_ALIENS ) + return; + + if( self->s.eFlags & EF_NODRAW ) + return; + + self->activator = activator; + if( self->nextthink ) + return; // can't retrigger until the wait is over + + if( self->s.eFlags & EF_DEAD ) + { + if( !trigger_class_match( self, activator ) ) + G_UseTargets( self, activator ); + } + else + { + if( trigger_class_match( self, activator ) ) + G_UseTargets( self, activator ); + } + + if( self->wait > 0 ) + { + self->think = multi_wait; + self->nextthink = level.time + ( self->wait + self->random * crandom( ) ) * 1000; + } + else + { + // we can't just remove (self) here, because this is a touch function + // called while looping through area links... + self->touch = 0; + self->nextthink = level.time + FRAMETIME; + self->think = G_FreeEntity; + } +} + +/* +=============== +trigger_class_touch +=============== +*/ +void trigger_class_touch( gentity_t *ent, gentity_t *other, trace_t *trace ) +{ + //only triggered by clients + if( !other->client ) + return; + + trigger_class_trigger( ent, other ); +} + +/* +=============== +trigger_class_use +=============== +*/ +void trigger_class_use( gentity_t *ent, gentity_t *other, gentity_t *activator ) +{ + ent->s.eFlags ^= EF_NODRAW; +} + +/* +=============== +SP_trigger_class +=============== +*/ +void SP_trigger_class( gentity_t *self ) +{ + char *buffer; + + G_SpawnFloat( "wait", "0.5", &self->wait ); + G_SpawnFloat( "random", "0", &self->random ); + + if( self->random >= self->wait && self->wait >= 0 ) + { + self->random = self->wait - FRAMETIME; + G_Printf( S_COLOR_YELLOW "WARNING: trigger_class has random >= wait\n" ); + } + + G_SpawnString( "classes", "", &buffer ); + + BG_ParseCSVClassList( buffer, self->cTriggers, PCL_NUM_CLASSES ); + + self->touch = trigger_class_touch; + self->use = trigger_class_use; + + // SPAWN_DISABLED + if( self->spawnflags & 1 ) + self->s.eFlags |= EF_NODRAW; + + // NEGATE + if( self->spawnflags & 2 ) + self->s.eFlags |= EF_DEAD; + + InitTrigger( self ); + trap_LinkEntity( self ); +} + + +/* +=============== +trigger_equipment_match +=============== +*/ +qboolean trigger_equipment_match( gentity_t *self, gentity_t *activator ) +{ + int i = 0; + + //if there is no equipment list all equipment triggers (stupid case) + if( self->wTriggers[ i ] == WP_NONE && self->uTriggers[ i ] == UP_NONE ) + return qtrue; + else + { + //otherwise check against the lists + for( i = 0; self->wTriggers[ i ] != WP_NONE; i++ ) + { + if( BG_InventoryContainsWeapon( self->wTriggers[ i ], activator->client->ps.stats ) ) + return qtrue; + } + + for( i = 0; self->uTriggers[ i ] != UP_NONE; i++ ) + { + if( BG_InventoryContainsUpgrade( self->uTriggers[ i ], activator->client->ps.stats ) ) + return qtrue; + } + } + + return qfalse; +} + +/* +=============== +trigger_equipment_trigger +=============== +*/ +void trigger_equipment_trigger( gentity_t *self, gentity_t *activator ) +{ + //sanity check + if( !activator->client ) + return; + + if( activator->client->ps.stats[ STAT_PTEAM ] != PTE_HUMANS ) + return; + + if( self->s.eFlags & EF_NODRAW ) + return; + + self->activator = activator; + if( self->nextthink ) + return; // can't retrigger until the wait is over + + if( self->s.eFlags & EF_DEAD ) + { + if( !trigger_equipment_match( self, activator ) ) + G_UseTargets( self, activator ); + } + else + { + if( trigger_equipment_match( self, activator ) ) + G_UseTargets( self, activator ); + } + + if( self->wait > 0 ) + { + self->think = multi_wait; + self->nextthink = level.time + ( self->wait + self->random * crandom( ) ) * 1000; + } + else + { + // we can't just remove (self) here, because this is a touch function + // called while looping through area links... + self->touch = 0; + self->nextthink = level.time + FRAMETIME; + self->think = G_FreeEntity; + } +} + +/* +=============== +trigger_equipment_touch +=============== +*/ +void trigger_equipment_touch( gentity_t *ent, gentity_t *other, trace_t *trace ) +{ + //only triggered by clients + if( !other->client ) + return; + + trigger_equipment_trigger( ent, other ); +} + +/* +=============== +trigger_equipment_use +=============== +*/ +void trigger_equipment_use( gentity_t *ent, gentity_t *other, gentity_t *activator ) +{ + ent->s.eFlags ^= EF_NODRAW; +} + +/* +=============== +SP_trigger_equipment +=============== +*/ +void SP_trigger_equipment( gentity_t *self ) +{ + char *buffer; + + G_SpawnFloat( "wait", "0.5", &self->wait ); + G_SpawnFloat( "random", "0", &self->random ); + + if( self->random >= self->wait && self->wait >= 0 ) + { + self->random = self->wait - FRAMETIME; + G_Printf( S_COLOR_YELLOW "WARNING: trigger_equipment has random >= wait\n" ); + } + + G_SpawnString( "equipment", "", &buffer ); + + BG_ParseCSVEquipmentList( buffer, self->wTriggers, WP_NUM_WEAPONS, + self->uTriggers, UP_NUM_UPGRADES ); + + self->touch = trigger_equipment_touch; + self->use = trigger_equipment_use; + + // SPAWN_DISABLED + if( self->spawnflags & 1 ) + self->s.eFlags |= EF_NODRAW; + + // NEGATE + if( self->spawnflags & 2 ) + self->s.eFlags |= EF_DEAD; + + InitTrigger( self ); + trap_LinkEntity( self ); +} + + +/* +=============== +trigger_gravity_touch +=============== +*/ +void trigger_gravity_touch( gentity_t *ent, gentity_t *other, trace_t *trace ) +{ + //only triggered by clients + if( !other->client ) + return; + + other->client->ps.gravity = ent->triggerGravity; +} + +/* +=============== +trigger_gravity_use +=============== +*/ +void trigger_gravity_use( gentity_t *ent, gentity_t *other, gentity_t *activator ) +{ + if( ent->r.linked ) + trap_UnlinkEntity( ent ); + else + trap_LinkEntity( ent ); +} + + +/* +=============== +SP_trigger_gravity +=============== +*/ +void SP_trigger_gravity( gentity_t *self ) +{ + G_SpawnInt( "gravity", "800", &self->triggerGravity ); + + self->touch = trigger_gravity_touch; + self->use = trigger_gravity_use; + + InitTrigger( self ); + trap_LinkEntity( self ); +} + + +/* +=============== +trigger_heal_use +=============== +*/ +void trigger_heal_use( gentity_t *self, gentity_t *other, gentity_t *activator ) +{ + if( self->r.linked ) + trap_UnlinkEntity( self ); + else + trap_LinkEntity( self ); +} + +/* +=============== +trigger_heal_touch +=============== +*/ +void trigger_heal_touch( gentity_t *self, gentity_t *other, trace_t *trace ) +{ + int max; + + if( !other->client ) + return; + + if( self->timestamp > level.time ) + return; + + if( self->spawnflags & 2 ) + self->timestamp = level.time + 1000; + else + self->timestamp = level.time + FRAMETIME; + + max = other->client->ps.stats[ STAT_MAX_HEALTH ]; + + other->health += self->damage; + + if( other->health > max ) + other->health = max; + + other->client->ps.stats[ STAT_HEALTH ] = other->health; +} + +/* +=============== +SP_trigger_heal +=============== +*/ +void SP_trigger_heal( gentity_t *self ) +{ + G_SpawnInt( "heal", "5", &self->damage ); + + if( self->damage <= 0 ) + { + self->damage = 1; + G_Printf( S_COLOR_YELLOW "WARNING: trigger_heal with negative damage key\n" ); + } + + self->touch = trigger_heal_touch; + self->use = trigger_heal_use; + + InitTrigger( self ); + + // link in to the world if starting active + if( !( self->spawnflags & 1 ) ) + trap_LinkEntity( self ); +} + + +/* +=============== +trigger_ammo_touch +=============== +*/ +void trigger_ammo_touch( gentity_t *self, gentity_t *other, trace_t *trace ) +{ + int ammo, clips, maxClips, maxAmmo; + + if( !other->client ) + return; + + if( other->client->ps.stats[ STAT_PTEAM ] != PTE_HUMANS ) + return; + + if( self->timestamp > level.time ) + return; + + if( other->client->ps.weaponstate != WEAPON_READY ) + return; + + if( BG_FindUsesEnergyForWeapon( other->client->ps.weapon ) && self->spawnflags & 2 ) + return; + + if( !BG_FindUsesEnergyForWeapon( other->client->ps.weapon ) && self->spawnflags & 4 ) + return; + + if( self->spawnflags & 1 ) + self->timestamp = level.time + 1000; + else + self->timestamp = level.time + FRAMETIME; + + BG_FindAmmoForWeapon( other->client->ps.weapon, &maxAmmo, &maxClips ); + BG_UnpackAmmoArray( other->client->ps.weapon, other->client->ps.ammo, other->client->ps.powerups, + &ammo, &clips ); + + if( ( ammo + self->damage ) > maxAmmo ) + { + if( clips < maxClips ) + { + clips++; + ammo = 1; + } + else + ammo = maxAmmo; + } + else + ammo += self->damage; + + BG_PackAmmoArray( other->client->ps.weapon, other->client->ps.ammo, other->client->ps.powerups, + ammo, clips ); +} + +/* +=============== +SP_trigger_ammo +=============== +*/ +void SP_trigger_ammo( gentity_t *self ) +{ + G_SpawnInt( "ammo", "1", &self->damage ); + + if( self->damage <= 0 ) + { + self->damage = 1; + G_Printf( S_COLOR_YELLOW "WARNING: trigger_ammo with negative ammo key\n" ); + } + + self->touch = trigger_ammo_touch; + + InitTrigger( self ); + trap_LinkEntity( self ); +} diff --git a/src/game/g_utils.c b/src/game/g_utils.c new file mode 100644 index 0000000..a74df3f --- /dev/null +++ b/src/game/g_utils.c @@ -0,0 +1,849 @@ +/* +=========================================================================== +Copyright (C) 1999-2005 Id Software, Inc. +Copyright (C) 2000-2006 Tim Angus + +This file is part of Tremulous. + +Tremulous is free software; you can redistribute it +and/or modify it under the terms of the GNU General Public License as +published by the Free Software Foundation; either version 2 of the License, +or (at your option) any later version. + +Tremulous is distributed in the hope that it will be +useful, but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with Tremulous; if not, write to the Free Software +Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA +=========================================================================== +*/ + +// g_utils.c -- misc utility functions for game module + +#include "g_local.h" + +typedef struct +{ + char oldShader[ MAX_QPATH ]; + char newShader[ MAX_QPATH ]; + float timeOffset; +} shaderRemap_t; + +#define MAX_SHADER_REMAPS 128 + +int remapCount = 0; +shaderRemap_t remappedShaders[ MAX_SHADER_REMAPS ]; + +void AddRemap( const char *oldShader, const char *newShader, float timeOffset ) +{ + int i; + + for( i = 0; i < remapCount; i++ ) + { + if( Q_stricmp( oldShader, remappedShaders[ i ].oldShader ) == 0 ) + { + // found it, just update this one + strcpy( remappedShaders[ i ].newShader,newShader ); + remappedShaders[ i ].timeOffset = timeOffset; + return; + } + } + + if( remapCount < MAX_SHADER_REMAPS ) + { + strcpy( remappedShaders[ remapCount ].newShader,newShader ); + strcpy( remappedShaders[ remapCount ].oldShader,oldShader ); + remappedShaders[ remapCount ].timeOffset = timeOffset; + remapCount++; + } +} + +const char *BuildShaderStateConfig( void ) +{ + static char buff[ MAX_STRING_CHARS * 4 ]; + char out[ ( MAX_QPATH * 2 ) + 5 ]; + int i; + + memset( buff, 0, MAX_STRING_CHARS ); + + for( i = 0; i < remapCount; i++ ) + { + Com_sprintf( out, ( MAX_QPATH * 2 ) + 5, "%s=%s:%5.2f@", remappedShaders[ i ].oldShader, + remappedShaders[ i ].newShader, remappedShaders[ i ].timeOffset ); + Q_strcat( buff, sizeof( buff ), out ); + } + return buff; +} + + +/* +========================================================================= + +model / sound configstring indexes + +========================================================================= +*/ + +/* +================ +G_FindConfigstringIndex + +================ +*/ +int G_FindConfigstringIndex( char *name, int start, int max, qboolean create ) +{ + int i; + char s[ MAX_STRING_CHARS ]; + + if( !name || !name[ 0 ] ) + return 0; + + for( i = 1; i < max; i++ ) + { + trap_GetConfigstring( start + i, s, sizeof( s ) ); + if( !s[ 0 ] ) + break; + + if( !strcmp( s, name ) ) + return i; + } + + if( !create ) + return 0; + + if( i == max ) + G_Error( "G_FindConfigstringIndex: overflow" ); + + trap_SetConfigstring( start + i, name ); + + return i; +} + +//TA: added ParticleSystemIndex +int G_ParticleSystemIndex( char *name ) +{ + return G_FindConfigstringIndex( name, CS_PARTICLE_SYSTEMS, MAX_GAME_PARTICLE_SYSTEMS, qtrue ); +} + +//TA: added ShaderIndex +int G_ShaderIndex( char *name ) +{ + return G_FindConfigstringIndex( name, CS_SHADERS, MAX_GAME_SHADERS, qtrue ); +} + +int G_ModelIndex( char *name ) +{ + return G_FindConfigstringIndex( name, CS_MODELS, MAX_MODELS, qtrue ); +} + +int G_SoundIndex( char *name ) +{ + return G_FindConfigstringIndex( name, CS_SOUNDS, MAX_SOUNDS, qtrue ); +} + +//===================================================================== + + +/* +================ +G_TeamCommand + +Broadcasts a command to only a specific team +================ +*/ +void G_TeamCommand( pTeam_t team, char *cmd ) +{ + int i; + + for( i = 0 ; i < level.maxclients ; i++ ) + { + if( level.clients[ i ].pers.connected == CON_CONNECTED ) + { + if( level.clients[ i ].pers.teamSelection == team || + ( level.clients[ i ].pers.teamSelection == PTE_NONE && + G_admin_permission( &g_entities[ i ], ADMF_SPEC_ALLCHAT ) ) ) + trap_SendServerCommand( i, cmd ); + } + } +} + + +/* +============= +G_Find + +Searches all active entities for the next one that holds +the matching string at fieldofs (use the FOFS() macro) in the structure. + +Searches beginning at the entity after from, or the beginning if NULL +NULL will be returned if the end of the list is reached. + +============= +*/ +gentity_t *G_Find( gentity_t *from, int fieldofs, const char *match ) +{ + char *s; + + if( !from ) + from = g_entities; + else + from++; + + for( ; from < &g_entities[ level.num_entities ]; from++ ) + { + if( !from->inuse ) + continue; + s = *(char **)( (byte *)from + fieldofs ); + + if( !s ) + continue; + + if( !Q_stricmp( s, match ) ) + return from; + } + + return NULL; +} + + +/* +============= +G_PickTarget + +Selects a random entity from among the targets +============= +*/ +#define MAXCHOICES 32 + +gentity_t *G_PickTarget( char *targetname ) +{ + gentity_t *ent = NULL; + int num_choices = 0; + gentity_t *choice[ MAXCHOICES ]; + + if( !targetname ) + { + G_Printf("G_PickTarget called with NULL targetname\n"); + return NULL; + } + + while( 1 ) + { + ent = G_Find( ent, FOFS( targetname ), targetname ); + + if( !ent ) + break; + + choice[ num_choices++ ] = ent; + + if( num_choices == MAXCHOICES ) + break; + } + + if( !num_choices ) + { + G_Printf( "G_PickTarget: target %s not found\n", targetname ); + return NULL; + } + + return choice[ rand( ) % num_choices ]; +} + + +/* +============================== +G_UseTargets + +"activator" should be set to the entity that initiated the firing. + +Search for (string)targetname in all entities that +match (string)self.target and call their .use function + +============================== +*/ +void G_UseTargets( gentity_t *ent, gentity_t *activator ) +{ + gentity_t *t; + + if( !ent ) + return; + + if( ent->targetShaderName && ent->targetShaderNewName ) + { + float f = level.time * 0.001; + AddRemap( ent->targetShaderName, ent->targetShaderNewName, f ); + trap_SetConfigstring( CS_SHADERSTATE, BuildShaderStateConfig( ) ); + } + + if( !ent->target ) + return; + + t = NULL; + while( ( t = G_Find( t, FOFS( targetname ), ent->target ) ) != NULL ) + { + if( t == ent ) + G_Printf( "WARNING: Entity used itself.\n" ); + else + { + if( t->use ) + t->use( t, ent, activator ); + } + + if( !ent->inuse ) + { + G_Printf( "entity was removed while using targets\n" ); + return; + } + } +} + + +/* +============= +TempVector + +This is just a convenience function +for making temporary vectors for function calls +============= +*/ +float *tv( float x, float y, float z ) +{ + static int index; + static vec3_t vecs[ 8 ]; + float *v; + + // use an array so that multiple tempvectors won't collide + // for a while + v = vecs[ index ]; + index = ( index + 1 ) & 7; + + v[ 0 ] = x; + v[ 1 ] = y; + v[ 2 ] = z; + + return v; +} + + +/* +============= +VectorToString + +This is just a convenience function +for printing vectors +============= +*/ +char *vtos( const vec3_t v ) +{ + static int index; + static char str[ 8 ][ 32 ]; + char *s; + + // use an array so that multiple vtos won't collide + s = str[ index ]; + index = ( index + 1 ) & 7; + + Com_sprintf( s, 32, "(%i %i %i)", (int)v[ 0 ], (int)v[ 1 ], (int)v[ 2 ] ); + + return s; +} + + +/* +=============== +G_SetMovedir + +The editor only specifies a single value for angles (yaw), +but we have special constants to generate an up or down direction. +Angles will be cleared, because it is being used to represent a direction +instead of an orientation. +=============== +*/ +void G_SetMovedir( vec3_t angles, vec3_t movedir ) +{ + static vec3_t VEC_UP = { 0, -1, 0 }; + static vec3_t MOVEDIR_UP = { 0, 0, 1 }; + static vec3_t VEC_DOWN = { 0, -2, 0 }; + static vec3_t MOVEDIR_DOWN = { 0, 0, -1 }; + + if( VectorCompare( angles, VEC_UP ) ) + VectorCopy( MOVEDIR_UP, movedir ); + else if( VectorCompare( angles, VEC_DOWN ) ) + VectorCopy( MOVEDIR_DOWN, movedir ); + else + AngleVectors( angles, movedir, NULL, NULL ); + + VectorClear( angles ); +} + + +float vectoyaw( const vec3_t vec ) +{ + float yaw; + + if( vec[ YAW ] == 0 && vec[ PITCH ] == 0 ) + { + yaw = 0; + } + else + { + if( vec[ PITCH ] ) + yaw = ( atan2( vec[ YAW ], vec[ PITCH ] ) * 180 / M_PI ); + else if( vec[ YAW ] > 0 ) + yaw = 90; + else + yaw = 270; + + if( yaw < 0 ) + yaw += 360; + } + + return yaw; +} + + +void G_InitGentity( gentity_t *e ) +{ + e->inuse = qtrue; + e->classname = "noclass"; + e->s.number = e - g_entities; + e->r.ownerNum = ENTITYNUM_NONE; +} + +/* +================= +G_Spawn + +Either finds a free entity, or allocates a new one. + + The slots from 0 to MAX_CLIENTS-1 are always reserved for clients, and will +never be used by anything else. + +Try to avoid reusing an entity that was recently freed, because it +can cause the client to think the entity morphed into something else +instead of being removed and recreated, which can cause interpolated +angles and bad trails. +================= +*/ +gentity_t *G_Spawn( void ) +{ + int i, force; + gentity_t *e; + + e = NULL; // shut up warning + i = 0; // shut up warning + + for( force = 0; force < 2; force++ ) + { + // if we go through all entities and can't find one to free, + // override the normal minimum times before use + e = &g_entities[ MAX_CLIENTS ]; + + for( i = MAX_CLIENTS; i < level.num_entities; i++, e++ ) + { + if( e->inuse ) + continue; + + // the first couple seconds of server time can involve a lot of + // freeing and allocating, so relax the replacement policy + if( !force && e->freetime > level.startTime + 2000 && level.time - e->freetime < 1000 ) + continue; + + // reuse this slot + G_InitGentity( e ); + return e; + } + + if( i != MAX_GENTITIES ) + break; + } + + if( i == ENTITYNUM_MAX_NORMAL ) + { + for( i = 0; i < MAX_GENTITIES; i++ ) + G_Printf( "%4i: %s\n", i, g_entities[ i ].classname ); + + G_Error( "G_Spawn: no free entities" ); + } + + // open up a new slot + level.num_entities++; + + // let the server system know that there are more entities + trap_LocateGameData( level.gentities, level.num_entities, sizeof( gentity_t ), + &level.clients[ 0 ].ps, sizeof( level.clients[ 0 ] ) ); + + G_InitGentity( e ); + return e; +} + + +/* +================= +G_EntitiesFree +================= +*/ +qboolean G_EntitiesFree( void ) +{ + int i; + gentity_t *e; + + e = &g_entities[ MAX_CLIENTS ]; + + for( i = MAX_CLIENTS; i < level.num_entities; i++, e++ ) + { + if( e->inuse ) + continue; + + // slot available + return qtrue; + } + + return qfalse; +} + + +/* +================= +G_FreeEntity + +Marks the entity as free +================= +*/ +void G_FreeEntity( gentity_t *ent ) +{ + trap_UnlinkEntity( ent ); // unlink from world + + if( ent->neverFree ) + return; + + memset( ent, 0, sizeof( *ent ) ); + ent->classname = "freent"; + ent->freetime = level.time; + ent->inuse = qfalse; +} + +/* +================= +G_TempEntity + +Spawns an event entity that will be auto-removed +The origin will be snapped to save net bandwidth, so care +must be taken if the origin is right on a surface (snap towards start vector first) +================= +*/ +gentity_t *G_TempEntity( vec3_t origin, int event ) +{ + gentity_t *e; + vec3_t snapped; + + e = G_Spawn( ); + e->s.eType = ET_EVENTS + event; + + e->classname = "tempEntity"; + e->eventTime = level.time; + e->freeAfterEvent = qtrue; + + VectorCopy( origin, snapped ); + SnapVector( snapped ); // save network bandwidth + G_SetOrigin( e, snapped ); + + // find cluster for PVS + trap_LinkEntity( e ); + + return e; +} + + + +/* +============================================================================== + +Kill box + +============================================================================== +*/ + +/* +================= +G_KillBox + +Kills all entities that would touch the proposed new positioning +of ent. Ent should be unlinked before calling this! +================= +*/ +void G_KillBox( gentity_t *ent ) +{ + int i, num; + int touch[ MAX_GENTITIES ]; + gentity_t *hit; + vec3_t mins, maxs; + + VectorAdd( ent->client->ps.origin, ent->r.mins, mins ); + VectorAdd( ent->client->ps.origin, ent->r.maxs, maxs ); + num = trap_EntitiesInBox( mins, maxs, touch, MAX_GENTITIES ); + + for( i = 0; i < num; i++ ) + { + hit = &g_entities[ touch[ i ] ]; + + if( !hit->client ) + continue; + + //TA: impossible to telefrag self + if( ent == hit ) + continue; + + // nail it + G_Damage( hit, ent, ent, NULL, NULL, + 100000, DAMAGE_NO_PROTECTION, MOD_TELEFRAG ); + } + +} + +//============================================================================== + +/* +=============== +G_AddPredictableEvent + +Use for non-pmove events that would also be predicted on the +client side: jumppads and item pickups +Adds an event+parm and twiddles the event counter +=============== +*/ +void G_AddPredictableEvent( gentity_t *ent, int event, int eventParm ) +{ + if( !ent->client ) + return; + + BG_AddPredictableEventToPlayerstate( event, eventParm, &ent->client->ps ); +} + + +/* +=============== +G_AddEvent + +Adds an event+parm and twiddles the event counter +=============== +*/ +void G_AddEvent( gentity_t *ent, int event, int eventParm ) +{ + int bits; + + if( !event ) + { + G_Printf( "G_AddEvent: zero event added for entity %i\n", ent->s.number ); + return; + } + + // eventParm is converted to uint8_t (0 - 255) in msg.c + if( eventParm & ~0xFF ) + { + G_Printf( S_COLOR_YELLOW "WARNING: G_AddEvent: event %d " + " eventParm uint8_t overflow (given %d)\n", event, eventParm ); + } + + // clients need to add the event in playerState_t instead of entityState_t + if( ent->client ) + { + bits = ent->client->ps.externalEvent & EV_EVENT_BITS; + bits = ( bits + EV_EVENT_BIT1 ) & EV_EVENT_BITS; + ent->client->ps.externalEvent = event | bits; + ent->client->ps.externalEventParm = eventParm; + ent->client->ps.externalEventTime = level.time; + } + else + { + bits = ent->s.event & EV_EVENT_BITS; + bits = ( bits + EV_EVENT_BIT1 ) & EV_EVENT_BITS; + ent->s.event = event | bits; + ent->s.eventParm = eventParm; + } + + ent->eventTime = level.time; +} + + +/* +=============== +G_BroadcastEvent + +Sends an event to every client +=============== +*/ +void G_BroadcastEvent( int event, int eventParm ) +{ + gentity_t *ent; + + ent = G_TempEntity( vec3_origin, event ); + ent->s.eventParm = eventParm; + ent->r.svFlags = SVF_BROADCAST; // send to everyone +} + + +/* +============= +G_Sound +============= +*/ +void G_Sound( gentity_t *ent, int channel, int soundIndex ) +{ + gentity_t *te; + + te = G_TempEntity( ent->r.currentOrigin, EV_GENERAL_SOUND ); + te->s.eventParm = soundIndex; +} + + +/* +============= +G_ClientIsLagging +============= +*/ +qboolean G_ClientIsLagging( gclient_t *client ) +{ + if( client ) + { + if( client->ps.ping >= 999 ) + return qtrue; + else + return qfalse; + } + + return qfalse; //is a non-existant client lagging? woooo zen +} + +//============================================================================== + + +/* +================ +G_SetOrigin + +Sets the pos trajectory for a fixed position +================ +*/ +void G_SetOrigin( gentity_t *ent, vec3_t origin ) +{ + VectorCopy( origin, ent->s.pos.trBase ); + ent->s.pos.trType = TR_STATIONARY; + ent->s.pos.trTime = 0; + ent->s.pos.trDuration = 0; + VectorClear( ent->s.pos.trDelta ); + + VectorCopy( origin, ent->r.currentOrigin ); + VectorCopy( origin, ent->s.origin ); //TA: if shit breaks - blame this line +} + +//TA: from quakestyle.telefragged.com +// (NOBODY): Code helper function +// +gentity_t *G_FindRadius( gentity_t *from, vec3_t org, float rad ) +{ + vec3_t eorg; + int j; + + if( !from ) + from = g_entities; + else + from++; + + for( ; from < &g_entities[ level.num_entities ]; from++ ) + { + if( !from->inuse ) + continue; + + for( j = 0; j < 3; j++ ) + eorg[ j ] = org[ j ] - ( from->r.currentOrigin[ j ] + ( from->r.mins[ j ] + from->r.maxs[ j ] ) * 0.5 ); + + if( VectorLength( eorg ) > rad ) + continue; + + return from; + } + + return NULL; +} + +/* +=============== +G_Visible + +Test for a LOS between two entities +=============== +*/ +qboolean G_Visible( gentity_t *ent1, gentity_t *ent2 ) +{ + trace_t trace; + + trap_Trace( &trace, ent1->s.pos.trBase, NULL, NULL, ent2->s.pos.trBase, ent1->s.number, MASK_SHOT ); + + if( trace.contents & CONTENTS_SOLID ) + return qfalse; + + return qtrue; +} + +/* +=============== +G_ClosestEnt + +Test a list of entities for the closest to a particular point +=============== +*/ +gentity_t *G_ClosestEnt( vec3_t origin, gentity_t **entities, int numEntities ) +{ + int i; + float nd, d = 1000000.0f; + gentity_t *closestEnt = NULL; + + for( i = 0; i < numEntities; i++ ) + { + gentity_t *ent = entities[ i ]; + + nd = DistanceSquared( origin, ent->s.origin ); + if( i == 0 || nd < d ) + { + d = nd; + closestEnt = ent; + } + } + + return closestEnt; +} + +/* +=============== +G_TriggerMenu + +Trigger a menu on some client +=============== +*/ +void G_TriggerMenu( int clientNum, dynMenu_t menu ) +{ + char buffer[ 32 ]; + + Com_sprintf( buffer, 32, "servermenu %d", menu ); + trap_SendServerCommand( clientNum, buffer ); +} + + +/* +=============== +G_CloseMenus + +Close all open menus on some client +=============== +*/ +void G_CloseMenus( int clientNum ) +{ + char buffer[ 32 ]; + + Com_sprintf( buffer, 32, "serverclosemenus" ); + trap_SendServerCommand( clientNum, buffer ); +} diff --git a/src/game/g_weapon.c b/src/game/g_weapon.c new file mode 100644 index 0000000..a9e04e6 --- /dev/null +++ b/src/game/g_weapon.c @@ -0,0 +1,1655 @@ +/* +=========================================================================== +Copyright (C) 1999-2005 Id Software, Inc. +Copyright (C) 2000-2006 Tim Angus + +This file is part of Tremulous. + +Tremulous is free software; you can redistribute it +and/or modify it under the terms of the GNU General Public License as +published by the Free Software Foundation; either version 2 of the License, +or (at your option) any later version. + +Tremulous is distributed in the hope that it will be +useful, but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with Tremulous; if not, write to the Free Software +Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA +=========================================================================== +*/ + +// g_weapon.c +// perform the server side effects of a weapon firing + +#include "g_local.h" + +static vec3_t forward, right, up; +static vec3_t muzzle; + +/* +================ +G_ForceWeaponChange +================ +*/ +void G_ForceWeaponChange( gentity_t *ent, weapon_t weapon ) +{ + int i; + + if( ent ) + { + ent->client->ps.pm_flags |= PMF_WEAPON_SWITCH; + + if( weapon == WP_NONE + || !BG_InventoryContainsWeapon( weapon, ent->client->ps.stats )) + { + //switch to the first non blaster weapon + for( i = WP_NONE + 1; i < WP_NUM_WEAPONS; i++ ) + { + if( i == WP_BLASTER ) + continue; + + if( BG_InventoryContainsWeapon( i, ent->client->ps.stats ) ) + { + ent->client->ps.persistant[ PERS_NEWWEAPON ] = i; + break; + } + } + + //only got the blaster to switch to + if( i == WP_NUM_WEAPONS ) + ent->client->ps.persistant[ PERS_NEWWEAPON ] = WP_BLASTER; + } + else + ent->client->ps.persistant[ PERS_NEWWEAPON ] = weapon; + + // Lak: The following hack has been moved to PM_BeginWeaponChange, but I'm going to + // redundantly leave it here as well just in case there's a case I'm forgetting + // because I don't want to face the gameplay consequences such an error would have + + // force this here to prevent flamer effect from continuing + ent->client->ps.generic1 = WPM_NOTFIRING; + + ent->client->ps.weapon = ent->client->ps.persistant[ PERS_NEWWEAPON ]; + } +} + +/* +================= +G_GiveClientMaxAmmo +================= +*/ +void G_GiveClientMaxAmmo( gentity_t *ent, qboolean buyingEnergyAmmo ) +{ + int i; + int maxAmmo, maxClips; + qboolean weaponType, restoredAmmo = qfalse; + + for( i = WP_NONE + 1; i < WP_NUM_WEAPONS; i++ ) + { + if( buyingEnergyAmmo ) + weaponType = BG_FindUsesEnergyForWeapon( i ); + else + weaponType = !BG_FindUsesEnergyForWeapon( i ); + + if( BG_InventoryContainsWeapon( i, ent->client->ps.stats ) && + weaponType && !BG_FindInfinteAmmoForWeapon( i ) && + !BG_WeaponIsFull( i, ent->client->ps.stats, + ent->client->ps.ammo, ent->client->ps.powerups ) ) + { + BG_FindAmmoForWeapon( i, &maxAmmo, &maxClips ); + + if( buyingEnergyAmmo ) + { + G_AddEvent( ent, EV_RPTUSE_SOUND, 0 ); + + if( BG_InventoryContainsUpgrade( UP_BATTPACK, ent->client->ps.stats ) ) + maxAmmo = (int)( (float)maxAmmo * BATTPACK_MODIFIER ); + } + + BG_PackAmmoArray( i, ent->client->ps.ammo, ent->client->ps.powerups, + maxAmmo, maxClips ); + + restoredAmmo = qtrue; + } + } + + if( restoredAmmo ) + G_ForceWeaponChange( ent, ent->client->ps.weapon ); +} + +/* +================ +G_BounceProjectile +================ +*/ +void G_BounceProjectile( vec3_t start, vec3_t impact, vec3_t dir, vec3_t endout ) +{ + vec3_t v, newv; + float dot; + + VectorSubtract( impact, start, v ); + dot = DotProduct( v, dir ); + VectorMA( v, -2 * dot, dir, newv ); + + VectorNormalize(newv); + VectorMA(impact, 8192, newv, endout); +} + +/* +================ +G_WideTrace + +Trace a bounding box against entities, but not the world +Also check there is a line of sight between the start and end point +================ +*/ +static void G_WideTrace( trace_t *tr, gentity_t *ent, float range, float width, gentity_t **target ) +{ + vec3_t mins, maxs; + vec3_t end; + + VectorSet( mins, -width, -width, -width ); + VectorSet( maxs, width, width, width ); + + *target = NULL; + + if( !ent->client ) + return; + + // Set aiming directions + AngleVectors( ent->client->ps.viewangles, forward, right, up ); + CalcMuzzlePoint( ent, forward, right, up, muzzle ); + VectorMA( muzzle, range, forward, end ); + + G_UnlaggedOn( ent, muzzle, range ); + + // Trace against entities + trap_Trace( tr, muzzle, mins, maxs, end, ent->s.number, CONTENTS_BODY ); + if( tr->entityNum != ENTITYNUM_NONE ) + { + *target = &g_entities[ tr->entityNum ]; + + // Set range to the trace length plus the width, so that the end of the + // LOS trace is close to the exterior of the target's bounding box + range = Distance( muzzle, tr->endpos ) + width; + VectorMA( muzzle, range, forward, end ); + + // Trace for line of sight against the world + trap_Trace( tr, muzzle, NULL, NULL, end, 0, CONTENTS_SOLID ); + if( tr->fraction < 1.0f ) + *target = NULL; + } + + G_UnlaggedOff( ); +} + + +/* +====================== +SnapVectorTowards + +Round a vector to integers for more efficient network +transmission, but make sure that it rounds towards a given point +rather than blindly truncating. This prevents it from truncating +into a wall. +====================== +*/ +void SnapVectorTowards( vec3_t v, vec3_t to ) +{ + int i; + + for( i = 0 ; i < 3 ; i++ ) + { + if( to[ i ] <= v[ i ] ) + v[ i ] = (int)v[ i ]; + else + v[ i ] = (int)v[ i ] + 1; + } +} + +/* +=============== +meleeAttack +=============== +*/ +void meleeAttack( gentity_t *ent, float range, float width, int damage, meansOfDeath_t mod ) +{ + trace_t tr; + vec3_t end; + gentity_t *tent; + gentity_t *traceEnt; + vec3_t mins, maxs; + + VectorSet( mins, -width, -width, -width ); + VectorSet( maxs, width, width, width ); + + // set aiming directions + AngleVectors( ent->client->ps.viewangles, forward, right, up ); + + CalcMuzzlePoint( ent, forward, right, up, muzzle ); + + VectorMA( muzzle, range, forward, end ); + + G_UnlaggedOn( ent, muzzle, range ); + trap_Trace( &tr, muzzle, mins, maxs, end, ent->s.number, MASK_SHOT ); + G_UnlaggedOff( ); + + if( tr.surfaceFlags & SURF_NOIMPACT ) + return; + + traceEnt = &g_entities[ tr.entityNum ]; + + // send blood impact + if( traceEnt->takedamage && traceEnt->client ) + { + tent = G_TempEntity( tr.endpos, EV_MISSILE_HIT ); + tent->s.otherEntityNum = traceEnt->s.number; + tent->s.eventParm = DirToByte( tr.plane.normal ); + tent->s.weapon = ent->s.weapon; + tent->s.generic1 = ent->s.generic1; //weaponMode + } + + if( traceEnt->takedamage ) + G_Damage( traceEnt, ent, ent, forward, tr.endpos, damage, DAMAGE_NO_KNOCKBACK, mod ); +} + +/* +====================================================================== + +MACHINEGUN + +====================================================================== +*/ + +void bulletFire( gentity_t *ent, float spread, int damage, int mod ) +{ + trace_t tr; + vec3_t end; + float r; + float u; + gentity_t *tent; + gentity_t *traceEnt; + + r = random( ) * M_PI * 2.0f; + u = sin( r ) * crandom( ) * spread * 16; + r = cos( r ) * crandom( ) * spread * 16; + VectorMA( muzzle, 8192 * 16, forward, end ); + VectorMA( end, r, right, end ); + VectorMA( end, u, up, end ); + + // don't use unlagged if this is not a client (e.g. turret) + if( ent->client ) + { + G_UnlaggedOn( ent, muzzle, 8192 * 16 ); + trap_Trace( &tr, muzzle, NULL, NULL, end, ent->s.number, MASK_SHOT ); + G_UnlaggedOff( ); + } + else + trap_Trace( &tr, muzzle, NULL, NULL, end, ent->s.number, MASK_SHOT ); + + if( tr.surfaceFlags & SURF_NOIMPACT ) + return; + + traceEnt = &g_entities[ tr.entityNum ]; + + // snap the endpos to integers, but nudged towards the line + SnapVectorTowards( tr.endpos, muzzle ); + + // send bullet impact + if( traceEnt->takedamage && traceEnt->client ) + { + tent = G_TempEntity( tr.endpos, EV_BULLET_HIT_FLESH ); + tent->s.eventParm = traceEnt->s.number; + } + else + { + tent = G_TempEntity( tr.endpos, EV_BULLET_HIT_WALL ); + tent->s.eventParm = DirToByte( tr.plane.normal ); + } + tent->s.otherEntityNum = ent->s.number; + + if( traceEnt->takedamage ) + { + G_Damage( traceEnt, ent, ent, forward, tr.endpos, + damage, 0, mod ); + } +} + +/* +====================================================================== + +SHOTGUN + +====================================================================== +*/ + +// this should match CG_ShotgunPattern +void ShotgunPattern( vec3_t origin, vec3_t origin2, int seed, gentity_t *ent ) +{ + int i; + float r, u; + vec3_t end; + vec3_t forward, right, up; + trace_t tr; + gentity_t *traceEnt; + + // derive the right and up vectors from the forward vector, because + // the client won't have any other information + VectorNormalize2( origin2, forward ); + PerpendicularVector( right, forward ); + CrossProduct( forward, right, up ); + + // generate the "random" spread pattern + for( i = 0; i < SHOTGUN_PELLETS; i++ ) + { + r = Q_crandom( &seed ) * SHOTGUN_SPREAD * 16; + u = Q_crandom( &seed ) * SHOTGUN_SPREAD * 16; + VectorMA( origin, 8192 * 16, forward, end ); + VectorMA( end, r, right, end ); + VectorMA( end, u, up, end ); + + trap_Trace( &tr, origin, NULL, NULL, end, ent->s.number, MASK_SHOT ); + traceEnt = &g_entities[ tr.entityNum ]; + + // send bullet impact + if( !( tr.surfaceFlags & SURF_NOIMPACT ) ) + { + if( traceEnt->takedamage ) + G_Damage( traceEnt, ent, ent, forward, tr.endpos, SHOTGUN_DMG, 0, MOD_SHOTGUN ); + } + } +} + + +void shotgunFire( gentity_t *ent ) +{ + gentity_t *tent; + + // send shotgun blast + tent = G_TempEntity( muzzle, EV_SHOTGUN ); + VectorScale( forward, 4096, tent->s.origin2 ); + SnapVector( tent->s.origin2 ); + tent->s.eventParm = rand() & 255; // seed for spread pattern + tent->s.otherEntityNum = ent->s.number; + G_UnlaggedOn( ent, muzzle, 8192 * 16 ); + ShotgunPattern( tent->s.pos.trBase, tent->s.origin2, tent->s.eventParm, ent ); + G_UnlaggedOff(); +} + +/* +====================================================================== + +MASS DRIVER + +====================================================================== +*/ + +void massDriverFire( gentity_t *ent ) +{ + trace_t tr; + vec3_t end; + gentity_t *tent; + gentity_t *traceEnt; + + VectorMA( muzzle, 8192 * 16, forward, end ); + + G_UnlaggedOn( ent, muzzle, 8192 * 16 ); + trap_Trace( &tr, muzzle, NULL, NULL, end, ent->s.number, MASK_SHOT ); + G_UnlaggedOff( ); + + if( tr.surfaceFlags & SURF_NOIMPACT ) + return; + + traceEnt = &g_entities[ tr.entityNum ]; + + // snap the endpos to integers, but nudged towards the line + SnapVectorTowards( tr.endpos, muzzle ); + + // send impact + if( traceEnt->takedamage && traceEnt->client ) + { + tent = G_TempEntity( tr.endpos, EV_MISSILE_HIT ); + tent->s.otherEntityNum = traceEnt->s.number; + tent->s.eventParm = DirToByte( tr.plane.normal ); + tent->s.weapon = ent->s.weapon; + tent->s.generic1 = ent->s.generic1; //weaponMode + } + else + { + tent = G_TempEntity( tr.endpos, EV_MISSILE_MISS ); + tent->s.eventParm = DirToByte( tr.plane.normal ); + tent->s.weapon = ent->s.weapon; + tent->s.generic1 = ent->s.generic1; //weaponMode + } + + if( traceEnt->takedamage ) + { + G_Damage( traceEnt, ent, ent, forward, tr.endpos, + MDRIVER_DMG, 0, MOD_MDRIVER ); + } +} + +/* +====================================================================== + +LOCKBLOB + +====================================================================== +*/ + +void lockBlobLauncherFire( gentity_t *ent ) +{ + gentity_t *m; + + m = fire_lockblob( ent, muzzle, forward ); + +// VectorAdd( m->s.pos.trDelta, ent->client->ps.velocity, m->s.pos.trDelta ); // "real" physics +} + +/* +====================================================================== + +HIVE + +====================================================================== +*/ + +void hiveFire( gentity_t *ent ) +{ + gentity_t *m; + + m = fire_hive( ent, muzzle, forward ); + +// VectorAdd( m->s.pos.trDelta, ent->client->ps.velocity, m->s.pos.trDelta ); // "real" physics +} + +/* +====================================================================== + +BLASTER PISTOL + +====================================================================== +*/ + +void blasterFire( gentity_t *ent ) +{ + gentity_t *m; + + m = fire_blaster( ent, muzzle, forward ); + +// VectorAdd( m->s.pos.trDelta, ent->client->ps.velocity, m->s.pos.trDelta ); // "real" physics +} + +/* +====================================================================== + +PULSE RIFLE + +====================================================================== +*/ + +void pulseRifleFire( gentity_t *ent ) +{ + gentity_t *m; + + m = fire_pulseRifle( ent, muzzle, forward ); + +// VectorAdd( m->s.pos.trDelta, ent->client->ps.velocity, m->s.pos.trDelta ); // "real" physics +} + +/* +====================================================================== + +FLAME THROWER + +====================================================================== +*/ + +void flamerFire( gentity_t *ent ) +{ + gentity_t *m; + + m = fire_flamer( ent, muzzle, forward ); +} + +/* +====================================================================== + +GRENADE + +====================================================================== +*/ + +void throwGrenade( gentity_t *ent ) +{ + gentity_t *m; + + m = launch_grenade( ent, muzzle, forward ); +} + +/* +====================================================================== + +LAS GUN + +====================================================================== +*/ + +/* +=============== +lasGunFire +=============== +*/ +void lasGunFire( gentity_t *ent ) +{ + trace_t tr; + vec3_t end; + gentity_t *tent; + gentity_t *traceEnt; + + VectorMA( muzzle, 8192 * 16, forward, end ); + + G_UnlaggedOn( ent, muzzle, 8192 * 16 ); + trap_Trace( &tr, muzzle, NULL, NULL, end, ent->s.number, MASK_SHOT ); + G_UnlaggedOff( ); + + if( tr.surfaceFlags & SURF_NOIMPACT ) + return; + + traceEnt = &g_entities[ tr.entityNum ]; + + // snap the endpos to integers, but nudged towards the line + SnapVectorTowards( tr.endpos, muzzle ); + + // send impact + if( traceEnt->takedamage && traceEnt->client ) + { + tent = G_TempEntity( tr.endpos, EV_MISSILE_HIT ); + tent->s.otherEntityNum = traceEnt->s.number; + tent->s.eventParm = DirToByte( tr.plane.normal ); + tent->s.weapon = ent->s.weapon; + tent->s.generic1 = ent->s.generic1; //weaponMode + } + else + { + tent = G_TempEntity( tr.endpos, EV_MISSILE_MISS ); + tent->s.eventParm = DirToByte( tr.plane.normal ); + tent->s.weapon = ent->s.weapon; + tent->s.generic1 = ent->s.generic1; //weaponMode + } + + if( traceEnt->takedamage ) + G_Damage( traceEnt, ent, ent, forward, tr.endpos, LASGUN_DAMAGE, 0, MOD_LASGUN ); +} + +/* +====================================================================== + +PAIN SAW + +====================================================================== +*/ + +void painSawFire( gentity_t *ent ) +{ + trace_t tr; + vec3_t end; + gentity_t *tent; + gentity_t *traceEnt; + + // set aiming directions + AngleVectors( ent->client->ps.viewangles, forward, right, up ); + + CalcMuzzlePoint( ent, forward, right, up, muzzle ); + + VectorMA( muzzle, PAINSAW_RANGE, forward, end ); + + G_UnlaggedOn( ent, muzzle, PAINSAW_RANGE ); + trap_Trace( &tr, muzzle, NULL, NULL, end, ent->s.number, MASK_SHOT ); + G_UnlaggedOff( ); + + if( tr.surfaceFlags & SURF_NOIMPACT ) + return; + + traceEnt = &g_entities[ tr.entityNum ]; + + // send blood impact + if( traceEnt->takedamage ) + { + vec3_t temp; + + //hack to get the particle system to line up with the weapon + VectorCopy( tr.endpos, temp ); + temp[ 2 ] -= 10.0f; + + if( traceEnt->client ) + { + tent = G_TempEntity( temp, EV_MISSILE_HIT ); + tent->s.otherEntityNum = traceEnt->s.number; + } + else + tent = G_TempEntity( temp, EV_MISSILE_MISS ); + + tent->s.eventParm = DirToByte( tr.plane.normal ); + tent->s.weapon = ent->s.weapon; + tent->s.generic1 = ent->s.generic1; //weaponMode + } + + if( traceEnt->takedamage ) + G_Damage( traceEnt, ent, ent, forward, tr.endpos, PAINSAW_DAMAGE, DAMAGE_NO_KNOCKBACK, MOD_PAINSAW ); +} + +/* +====================================================================== + +LUCIFER CANNON + +====================================================================== +*/ + +/* +=============== +LCChargeFire +=============== +*/ +void LCChargeFire( gentity_t *ent, qboolean secondary ) +{ + gentity_t *m; + + if( secondary ) + { + m = fire_luciferCannon( ent, muzzle, forward, LCANNON_SECONDARY_DAMAGE, + LCANNON_SECONDARY_RADIUS ); + ent->client->ps.weaponTime = LCANNON_REPEAT; + } + else + { + m = fire_luciferCannon( ent, muzzle, forward, ent->client->ps.stats[ STAT_MISC ], LCANNON_RADIUS ); + ent->client->ps.weaponTime = LCANNON_CHARGEREPEAT; + } + + ent->client->ps.stats[ STAT_MISC ] = 0; +} + +/* +====================================================================== + +TESLA GENERATOR + +====================================================================== +*/ + + +void teslaFire( gentity_t *ent ) +{ + trace_t tr; + vec3_t end; + gentity_t *traceEnt, *tent; + + VectorMA( muzzle, TESLAGEN_RANGE, forward, end ); + + trap_Trace( &tr, muzzle, NULL, NULL, end, ent->s.number, MASK_SHOT ); + + if( tr.entityNum == ENTITYNUM_NONE ) + return; + + traceEnt = &g_entities[ tr.entityNum ]; + + if( !traceEnt->client ) + return; + + if( traceEnt->client && traceEnt->client->ps.stats[ STAT_PTEAM ] != PTE_ALIENS ) + return; + + //so the client side knows + ent->s.eFlags |= EF_FIRING; + + if( traceEnt->takedamage ) + { + G_Damage( traceEnt, ent, ent, forward, tr.endpos, + TESLAGEN_DMG, 0, MOD_TESLAGEN ); + } + + // snap the endpos to integers to save net bandwidth, but nudged towards the line + SnapVectorTowards( tr.endpos, muzzle ); + + // send railgun beam effect + tent = G_TempEntity( tr.endpos, EV_TESLATRAIL ); + + VectorCopy( muzzle, tent->s.origin2 ); + + tent->s.generic1 = ent->s.number; //src + tent->s.clientNum = traceEnt->s.number; //dest + + // move origin a bit to come closer to the drawn gun muzzle + VectorMA( tent->s.origin2, 28, up, tent->s.origin2 ); +} + + +/* +====================================================================== + +BUILD GUN + +====================================================================== +*/ + +/* +=============== +cancelBuildFire +=============== +*/ +void cancelBuildFire( gentity_t *ent ) +{ + vec3_t forward, end; + trace_t tr; + gentity_t *traceEnt; + int bHealth; + + if( ent->client->ps.stats[ STAT_BUILDABLE ] != BA_NONE ) + { + ent->client->ps.stats[ STAT_BUILDABLE ] = BA_NONE; + return; + } + + //repair buildable + if( ent->client->ps.stats[ STAT_PTEAM ] == PTE_HUMANS ) + { + AngleVectors( ent->client->ps.viewangles, forward, NULL, NULL ); + VectorMA( ent->client->ps.origin, 100, forward, end ); + + trap_Trace( &tr, ent->client->ps.origin, NULL, NULL, end, ent->s.number, MASK_PLAYERSOLID ); + traceEnt = &g_entities[ tr.entityNum ]; + + if( tr.fraction < 1.0 && + ( traceEnt->s.eType == ET_BUILDABLE ) && + ( traceEnt->biteam == ent->client->ps.stats[ STAT_PTEAM ] ) && + ( ( ent->client->ps.weapon >= WP_HBUILD2 ) && + ( ent->client->ps.weapon <= WP_HBUILD ) ) && + traceEnt->spawned && traceEnt->health > 0 ) + { + if( ent->client->ps.stats[ STAT_MISC ] > 0 ) + { + G_AddEvent( ent, EV_BUILD_DELAY, ent->client->ps.clientNum ); + return; + } + + bHealth = BG_FindHealthForBuildable( traceEnt->s.modelindex ); + + traceEnt->health += HBUILD_HEALRATE; + ent->client->pers.statscounters.repairspoisons++; + level.humanStatsCounters.repairspoisons++; + + if( traceEnt->health > bHealth ) + traceEnt->health = bHealth; + + if( traceEnt->health == bHealth ) + G_AddEvent( ent, EV_BUILD_REPAIRED, 0 ); + else + G_AddEvent( ent, EV_BUILD_REPAIR, 0 ); + } + } + else if( ent->client->ps.weapon == WP_ABUILD2 ) + meleeAttack( ent, ABUILDER_CLAW_RANGE, ABUILDER_CLAW_WIDTH, + ABUILDER_CLAW_DMG, MOD_ABUILDER_CLAW ); //melee attack for alien builder +} + +/* +=============== +buildFire +=============== +*/ +void buildFire( gentity_t *ent, dynMenu_t menu ) +{ + if( ( ent->client->ps.stats[ STAT_BUILDABLE ] & ~SB_VALID_TOGGLEBIT ) > BA_NONE ) + { + if( ent->client->ps.stats[ STAT_MISC ] > 0 ) + { + G_AddEvent( ent, EV_BUILD_DELAY, ent->client->ps.clientNum ); + return; + } + + if( G_BuildIfValid( ent, ent->client->ps.stats[ STAT_BUILDABLE ] & ~SB_VALID_TOGGLEBIT ) ) + { + if( g_cheats.integer ) + { + ent->client->ps.stats[ STAT_MISC ] = 0; + } + else if( ent->client->ps.stats[ STAT_PTEAM ] == PTE_ALIENS && !G_IsOvermindBuilt( ) ) + { + ent->client->ps.stats[ STAT_MISC ] += + BG_FindBuildDelayForWeapon( ent->s.weapon ) * 2; + } + else if( ent->client->ps.stats[ STAT_PTEAM ] == PTE_HUMANS && !G_IsPowered( muzzle ) && + ( ent->client->ps.stats[ STAT_BUILDABLE ] & ~SB_VALID_TOGGLEBIT ) != BA_H_REPEATER ) //hack + { + ent->client->ps.stats[ STAT_MISC ] += + BG_FindBuildDelayForWeapon( ent->s.weapon ) * 2; + } + else + ent->client->ps.stats[ STAT_MISC ] += + BG_FindBuildDelayForWeapon( ent->s.weapon ); + + ent->client->ps.stats[ STAT_BUILDABLE ] = BA_NONE; + + // don't want it bigger than 32k + if( ent->client->ps.stats[ STAT_MISC ] > 30000 ) + ent->client->ps.stats[ STAT_MISC ] = 30000; + } + return; + } + + G_TriggerMenu( ent->client->ps.clientNum, menu ); +} + +void slowBlobFire( gentity_t *ent ) +{ + gentity_t *m; + + m = fire_slowBlob( ent, muzzle, forward ); + +// VectorAdd( m->s.pos.trDelta, ent->client->ps.velocity, m->s.pos.trDelta ); // "real" physics +} + + +/* +====================================================================== + +LEVEL0 + +====================================================================== +*/ + +/* +=============== +CheckVenomAttack +=============== +*/ +qboolean CheckVenomAttack( gentity_t *ent ) +{ + trace_t tr; + vec3_t end; + gentity_t *tent; + gentity_t *traceEnt; + vec3_t mins, maxs; + int damage = LEVEL0_BITE_DMG; + + VectorSet( mins, -LEVEL0_BITE_WIDTH, -LEVEL0_BITE_WIDTH, -LEVEL0_BITE_WIDTH ); + VectorSet( maxs, LEVEL0_BITE_WIDTH, LEVEL0_BITE_WIDTH, LEVEL0_BITE_WIDTH ); + + // set aiming directions + AngleVectors( ent->client->ps.viewangles, forward, right, up ); + + CalcMuzzlePoint( ent, forward, right, up, muzzle ); + + VectorMA( muzzle, LEVEL0_BITE_RANGE, forward, end ); + + G_UnlaggedOn( ent, muzzle, LEVEL0_BITE_RANGE ); + trap_Trace( &tr, muzzle, mins, maxs, end, ent->s.number, MASK_SHOT ); + G_UnlaggedOff( ); + + if( tr.surfaceFlags & SURF_NOIMPACT ) + return qfalse; + + traceEnt = &g_entities[ tr.entityNum ]; + + if( !traceEnt->takedamage ) + return qfalse; + + if( !traceEnt->client && !traceEnt->s.eType == ET_BUILDABLE ) + return qfalse; + + //allow bites to work against defensive buildables only + if( traceEnt->s.eType == ET_BUILDABLE ) + { + if( traceEnt->s.modelindex != BA_H_MGTURRET && + traceEnt->s.modelindex != BA_H_TESLAGEN ) + return qfalse; + + //hackery + damage *= 0.5f; + } + + if( traceEnt->client ) + { + if( traceEnt->client->ps.stats[ STAT_PTEAM ] == PTE_ALIENS ) + return qfalse; + if( traceEnt->client->ps.stats[ STAT_HEALTH ] <= 0 ) + return qfalse; + } + + // send blood impact + if( traceEnt->takedamage && traceEnt->client ) + { + tent = G_TempEntity( tr.endpos, EV_MISSILE_HIT ); + tent->s.otherEntityNum = traceEnt->s.number; + tent->s.eventParm = DirToByte( tr.plane.normal ); + tent->s.weapon = ent->s.weapon; + tent->s.generic1 = ent->s.generic1; //weaponMode + } + + G_Damage( traceEnt, ent, ent, forward, tr.endpos, damage, DAMAGE_NO_KNOCKBACK, MOD_LEVEL0_BITE ); + + return qtrue; +} + +/* +====================================================================== + +LEVEL1 + +====================================================================== +*/ + +/* +=============== +CheckGrabAttack +=============== +*/ +void CheckGrabAttack( gentity_t *ent ) +{ + trace_t tr; + vec3_t end, dir; + gentity_t *traceEnt; + + // set aiming directions + AngleVectors( ent->client->ps.viewangles, forward, right, up ); + + CalcMuzzlePoint( ent, forward, right, up, muzzle ); + + VectorMA( muzzle, LEVEL1_GRAB_RANGE, forward, end ); + + trap_Trace( &tr, muzzle, NULL, NULL, end, ent->s.number, MASK_SHOT ); + if( tr.surfaceFlags & SURF_NOIMPACT ) + return; + + traceEnt = &g_entities[ tr.entityNum ]; + + if( !traceEnt->takedamage ) + return; + + if( traceEnt->client ) + { + if( traceEnt->client->ps.stats[ STAT_PTEAM ] == PTE_ALIENS ) + return; + + if( traceEnt->client->ps.stats[ STAT_HEALTH ] <= 0 ) + return; + + if( !( traceEnt->client->ps.stats[ STAT_STATE ] & SS_GRABBED ) ) + { + AngleVectors( traceEnt->client->ps.viewangles, dir, NULL, NULL ); + traceEnt->client->ps.stats[ STAT_VIEWLOCK ] = DirToByte( dir ); + + //event for client side grab effect + G_AddPredictableEvent( ent, EV_LEV1_GRAB, 0 ); + } + + traceEnt->client->ps.stats[ STAT_STATE ] |= SS_GRABBED; + + if( ent->client->ps.weapon == WP_ALEVEL1 ) + traceEnt->client->grabExpiryTime = level.time + LEVEL1_GRAB_TIME; + else if( ent->client->ps.weapon == WP_ALEVEL1_UPG ) + traceEnt->client->grabExpiryTime = level.time + LEVEL1_GRAB_U_TIME; + } + else if( traceEnt->s.eType == ET_BUILDABLE && + traceEnt->s.modelindex == BA_H_MGTURRET ) + { + if( !traceEnt->lev1Grabbed ) + G_AddPredictableEvent( ent, EV_LEV1_GRAB, 0 ); + + traceEnt->lev1Grabbed = qtrue; + traceEnt->lev1GrabTime = level.time; + } +} + +/* +=============== +poisonCloud +=============== +*/ +void poisonCloud( gentity_t *ent ) +{ + int entityList[ MAX_GENTITIES ]; + vec3_t range = { LEVEL1_PCLOUD_RANGE, LEVEL1_PCLOUD_RANGE, LEVEL1_PCLOUD_RANGE }; + vec3_t mins, maxs; + int i, num; + gentity_t *humanPlayer; + trace_t tr; + + VectorAdd( ent->client->ps.origin, range, maxs ); + VectorSubtract( ent->client->ps.origin, range, mins ); + + G_UnlaggedOn( ent, ent->client->ps.origin, LEVEL1_PCLOUD_RANGE ); + num = trap_EntitiesInBox( mins, maxs, entityList, MAX_GENTITIES ); + for( i = 0; i < num; i++ ) + { + humanPlayer = &g_entities[ entityList[ i ] ]; + + if( humanPlayer->client && humanPlayer->client->ps.stats[ STAT_PTEAM ] == PTE_HUMANS ) + { + if( BG_InventoryContainsUpgrade( UP_LIGHTARMOUR, humanPlayer->client->ps.stats ) ) + continue; + + if( BG_InventoryContainsUpgrade( UP_BATTLESUIT, humanPlayer->client->ps.stats ) ) + continue; + + trap_Trace( &tr, muzzle, NULL, NULL, humanPlayer->s.origin, humanPlayer->s.number, MASK_SHOT ); + + //can't see target from here + if( tr.entityNum == ENTITYNUM_WORLD ) + continue; + + if( !( humanPlayer->client->ps.stats[ STAT_STATE ] & SS_POISONCLOUDED ) ) + { + humanPlayer->client->ps.stats[ STAT_STATE ] |= SS_POISONCLOUDED; + humanPlayer->client->lastPoisonCloudedTime = level.time; + humanPlayer->client->lastPoisonCloudedClient = ent; + trap_SendServerCommand( humanPlayer->client->ps.clientNum, "poisoncloud" ); + } + } + } + G_UnlaggedOff( ); +} + + +/* +====================================================================== + +LEVEL2 + +====================================================================== +*/ + +#define MAX_ZAPS 64 + +static zap_t zaps[ MAX_CLIENTS ]; + +/* +=============== +G_FindNewZapTarget +=============== +*/ +static gentity_t *G_FindNewZapTarget( gentity_t *ent ) +{ + int entityList[ MAX_GENTITIES ]; + vec3_t range = { LEVEL2_AREAZAP_RANGE, LEVEL2_AREAZAP_RANGE, LEVEL2_AREAZAP_RANGE }; + vec3_t mins, maxs; + int i, j, k, num; + gentity_t *enemy; + trace_t tr; + + VectorScale( range, 1.0f / M_ROOT3, range ); + VectorAdd( ent->s.origin, range, maxs ); + VectorSubtract( ent->s.origin, range, mins ); + + num = trap_EntitiesInBox( mins, maxs, entityList, MAX_GENTITIES ); + + for( i = 0; i < num; i++ ) + { + enemy = &g_entities[ entityList[ i ] ]; + + if( ( ( enemy->client && enemy->client->ps.stats[ STAT_PTEAM ] == PTE_HUMANS ) || + ( enemy->s.eType == ET_BUILDABLE && + BG_FindTeamForBuildable( enemy->s.modelindex ) == BIT_HUMANS ) ) && enemy->health > 0 ) + { + qboolean foundOldTarget = qfalse; + + trap_Trace( &tr, muzzle, NULL, NULL, enemy->s.origin, ent->s.number, MASK_SHOT ); + + //can't see target from here + if( tr.entityNum == ENTITYNUM_WORLD ) + continue; + + for( j = 0; j < MAX_ZAPS; j++ ) + { + zap_t *zap = &zaps[ j ]; + + for( k = 0; k < zap->numTargets; k++ ) + { + if( zap->targets[ k ] == enemy ) + { + foundOldTarget = qtrue; + break; + } + } + + if( foundOldTarget ) + break; + } + + // enemy is already targetted + if( foundOldTarget ) + continue; + + return enemy; + } + } + + return NULL; +} + +/* +=============== +G_UpdateZapEffect +=============== +*/ +static void G_UpdateZapEffect( zap_t *zap ) +{ + int j; + gentity_t *effect = zap->effectChannel; + + effect->s.eType = ET_LEV2_ZAP_CHAIN; + effect->classname = "lev2zapchain"; + G_SetOrigin( effect, zap->creator->s.origin ); + effect->s.powerups = zap->creator->s.number; + + effect->s.time = effect->s.time2 = effect->s.constantLight = -1; + + for( j = 0; j < zap->numTargets; j++ ) + { + int number = zap->targets[ j ]->s.number; + + switch( j ) + { + case 0: effect->s.time = number; break; + case 1: effect->s.time2 = number; break; + case 2: effect->s.constantLight = number; break; + default: break; + } + } + + trap_LinkEntity( effect ); +} + +/* +=============== +G_CreateNewZap +=============== +*/ +static void G_CreateNewZap( gentity_t *creator, gentity_t *target ) +{ + int i, j; + zap_t *zap; + + for( i = 0; i < MAX_ZAPS; i++ ) + { + zap = &zaps[ i ]; + + if( !zap->used ) + { + zap->used = qtrue; + + zap->timeToLive = LEVEL2_AREAZAP_TIME; + zap->damageUsed = 0; + + zap->creator = creator; + + zap->targets[ 0 ] = target; + zap->numTargets = 1; + + for( j = 1; j < MAX_ZAP_TARGETS && zap->targets[ j - 1 ]; j++ ) + { + zap->targets[ j ] = G_FindNewZapTarget( zap->targets[ j - 1 ] ); + + if( zap->targets[ j ] ) + zap->numTargets++; + } + + zap->effectChannel = G_Spawn( ); + G_UpdateZapEffect( zap ); + + return; + } + } +} + + +/* +=============== +G_UpdateZaps +=============== +*/ +void G_UpdateZaps( int msec ) +{ + int i, j; + zap_t *zap; + int damage; + + for( i = 0; i < MAX_ZAPS; i++ ) + { + zap = &zaps[ i ]; + + if( zap->used ) + { + //check each target is valid + for( j = 0; j < zap->numTargets; j++ ) + { + gentity_t *source; + gentity_t *target = zap->targets[ j ]; + + if( j == 0 ) + source = zap->creator; + else + source = zap->targets[ j - 1 ]; + + if( target->health <= 0 || !target->inuse || //early out + Distance( source->s.origin, target->s.origin ) > LEVEL2_AREAZAP_RANGE ) + { + target = zap->targets[ j ] = G_FindNewZapTarget( source ); + + //couldn't find a target, so forget about the rest of the chain + if( !target ) + zap->numTargets = j; + } + } + + if( zap->numTargets ) + { + for( j = 0; j < zap->numTargets; j++ ) + { + gentity_t *source; + gentity_t *target = zap->targets[ j ]; + float r = 1.0f / zap->numTargets; + float damageFraction = 2 * r - 2 * j * r * r - r * r; + vec3_t forward; + + if( j == 0 ) + source = zap->creator; + else + source = zap->targets[ j - 1 ]; + + damage = ceil( ( (float)msec / LEVEL2_AREAZAP_TIME ) * + LEVEL2_AREAZAP_DMG * damageFraction ); + + // don't let a high msec value inflate the total damage + if( damage + zap->damageUsed > LEVEL2_AREAZAP_DMG ) + damage = LEVEL2_AREAZAP_DMG - zap->damageUsed; + + VectorSubtract( target->s.origin, source->s.origin, forward ); + VectorNormalize( forward ); + + //do the damage + if( damage ) + { + G_Damage( target, source, zap->creator, forward, target->s.origin, + damage, DAMAGE_NO_KNOCKBACK | DAMAGE_NO_LOCDAMAGE, MOD_LEVEL2_ZAP ); + zap->damageUsed += damage; + } + } + } + + G_UpdateZapEffect( zap ); + + zap->timeToLive -= msec; + + if( zap->timeToLive <= 0 || zap->numTargets == 0 || zap->creator->health <= 0 ) + { + zap->used = qfalse; + G_FreeEntity( zap->effectChannel ); + } + } + } +} + +/* +=============== +areaZapFire +=============== +*/ +void areaZapFire( gentity_t *ent ) +{ + trace_t tr; + vec3_t end; + gentity_t *traceEnt; + vec3_t mins, maxs; + + VectorSet( mins, -LEVEL2_AREAZAP_WIDTH, -LEVEL2_AREAZAP_WIDTH, -LEVEL2_AREAZAP_WIDTH ); + VectorSet( maxs, LEVEL2_AREAZAP_WIDTH, LEVEL2_AREAZAP_WIDTH, LEVEL2_AREAZAP_WIDTH ); + + // set aiming directions + AngleVectors( ent->client->ps.viewangles, forward, right, up ); + + CalcMuzzlePoint( ent, forward, right, up, muzzle ); + + VectorMA( muzzle, LEVEL2_AREAZAP_RANGE, forward, end ); + + G_UnlaggedOn( ent, muzzle, LEVEL2_AREAZAP_RANGE ); + trap_Trace( &tr, muzzle, mins, maxs, end, ent->s.number, MASK_SHOT ); + G_UnlaggedOff( ); + + if( tr.surfaceFlags & SURF_NOIMPACT ) + return; + + traceEnt = &g_entities[ tr.entityNum ]; + + if( ( ( traceEnt->client && traceEnt->client->ps.stats[ STAT_PTEAM ] == PTE_HUMANS ) || + ( traceEnt->s.eType == ET_BUILDABLE && + BG_FindTeamForBuildable( traceEnt->s.modelindex ) == BIT_HUMANS ) ) && traceEnt->health > 0 ) + { + G_CreateNewZap( ent, traceEnt ); + } +} + + +/* +====================================================================== + +LEVEL3 + +====================================================================== +*/ + +/* +=============== +CheckPounceAttack +=============== +*/ +qboolean CheckPounceAttack( gentity_t *ent ) +{ + trace_t tr; + gentity_t *tent; + gentity_t *traceEnt; + int damage; + + if( ent->client->ps.groundEntityNum != ENTITYNUM_NONE ) + { + ent->client->allowedToPounce = qfalse; + ent->client->pmext.pouncePayload = 0; + } + + if( !ent->client->allowedToPounce ) + return qfalse; + + if( ent->client->ps.weaponTime ) + return qfalse; + + G_WideTrace( &tr, ent, LEVEL3_POUNCE_RANGE, LEVEL3_POUNCE_WIDTH, &traceEnt ); + + if( traceEnt == NULL ) + return qfalse; + + // send blood impact + if( traceEnt->takedamage && traceEnt->client ) + { + tent = G_TempEntity( tr.endpos, EV_MISSILE_HIT ); + tent->s.otherEntityNum = traceEnt->s.number; + tent->s.eventParm = DirToByte( tr.plane.normal ); + tent->s.weapon = ent->s.weapon; + tent->s.generic1 = ent->s.generic1; //weaponMode + } + + if( !traceEnt->takedamage ) + return qfalse; + + damage = (int)( ( (float)ent->client->pmext.pouncePayload + / (float)LEVEL3_POUNCE_SPEED ) * LEVEL3_POUNCE_DMG ); + + ent->client->pmext.pouncePayload = 0; + + G_Damage( traceEnt, ent, ent, forward, tr.endpos, damage, + DAMAGE_NO_KNOCKBACK|DAMAGE_NO_LOCDAMAGE, MOD_LEVEL3_POUNCE ); + + ent->client->allowedToPounce = qfalse; + + return qtrue; +} + +void bounceBallFire( gentity_t *ent ) +{ + gentity_t *m; + + m = fire_bounceBall( ent, muzzle, forward ); + +// VectorAdd( m->s.pos.trDelta, ent->client->ps.velocity, m->s.pos.trDelta ); // "real" physics +} + + +/* +====================================================================== + +LEVEL4 + +====================================================================== +*/ + +/* +=============== +ChargeAttack +=============== +*/ +void ChargeAttack( gentity_t *ent, gentity_t *victim ) +{ + gentity_t *tent; + int damage; + vec3_t forward, normal; + + if( level.time < victim->chargeRepeat ) + return; + + victim->chargeRepeat = level.time + LEVEL4_CHARGE_REPEAT; + + VectorSubtract( victim->s.origin, ent->s.origin, forward ); + VectorNormalize( forward ); + VectorNegate( forward, normal ); + + if( victim->client ) + { + tent = G_TempEntity( victim->s.origin, EV_MISSILE_HIT ); + tent->s.otherEntityNum = victim->s.number; + tent->s.eventParm = DirToByte( normal ); + tent->s.weapon = ent->s.weapon; + tent->s.generic1 = ent->s.generic1; //weaponMode + } + + if( !victim->takedamage ) + return; + + damage = (int)( ( (float)ent->client->ps.stats[ STAT_MISC ] / (float)LEVEL4_CHARGE_TIME ) * LEVEL4_CHARGE_DMG ); + + G_Damage( victim, ent, ent, forward, victim->s.origin, damage, 0, MOD_LEVEL4_CHARGE ); +} + +//====================================================================== + +/* +=============== +CalcMuzzlePoint + +set muzzle location relative to pivoting eye +=============== +*/ +void CalcMuzzlePoint( gentity_t *ent, vec3_t forward, vec3_t right, vec3_t up, vec3_t muzzlePoint ) +{ + VectorCopy( ent->s.pos.trBase, muzzlePoint ); + muzzlePoint[ 2 ] += ent->client->ps.viewheight; + VectorMA( muzzlePoint, 1, forward, muzzlePoint ); + VectorMA( muzzlePoint, 1, right, muzzlePoint ); + // snap to integer coordinates for more efficient network bandwidth usage + SnapVector( muzzlePoint ); +} + +/* +=============== +FireWeapon3 +=============== +*/ +void FireWeapon3( gentity_t *ent ) +{ + if( ent->client ) + { + // set aiming directions + AngleVectors( ent->client->ps.viewangles, forward, right, up ); + CalcMuzzlePoint( ent, forward, right, up, muzzle ); + } + else + { + AngleVectors( ent->s.angles2, forward, right, up ); + VectorCopy( ent->s.pos.trBase, muzzle ); + } + + // fire the specific weapon + switch( ent->s.weapon ) + { + case WP_ALEVEL3_UPG: + bounceBallFire( ent ); + break; + + case WP_ABUILD2: + slowBlobFire( ent ); + break; + + default: + break; + } +} + +/* +=============== +FireWeapon2 +=============== +*/ +void FireWeapon2( gentity_t *ent ) +{ + if( ent->client ) + { + // set aiming directions + AngleVectors( ent->client->ps.viewangles, forward, right, up ); + CalcMuzzlePoint( ent, forward, right, up, muzzle ); + } + else + { + AngleVectors( ent->s.angles2, forward, right, up ); + VectorCopy( ent->s.pos.trBase, muzzle ); + } + + // fire the specific weapon + switch( ent->s.weapon ) + { + case WP_ALEVEL1_UPG: + poisonCloud( ent ); + break; + case WP_ALEVEL2_UPG: + areaZapFire( ent ); + break; + + case WP_LUCIFER_CANNON: + LCChargeFire( ent, qtrue ); + break; + + case WP_ABUILD: + case WP_ABUILD2: + case WP_HBUILD: + case WP_HBUILD2: + cancelBuildFire( ent ); + break; + default: + break; + } +} + +/* +=============== +FireWeapon +=============== +*/ +void FireWeapon( gentity_t *ent ) +{ + if( level.paused ) return; + + if( ent->client ) + { + // set aiming directions + AngleVectors( ent->client->ps.viewangles, forward, right, up ); + CalcMuzzlePoint( ent, forward, right, up, muzzle ); + } + else + { + AngleVectors( ent->turretAim, forward, right, up ); + VectorCopy( ent->s.pos.trBase, muzzle ); + } + + // fire the specific weapon + switch( ent->s.weapon ) + { + case WP_ALEVEL1: + case WP_ALEVEL1_UPG: + meleeAttack( ent, LEVEL1_CLAW_RANGE, LEVEL1_CLAW_WIDTH, LEVEL1_CLAW_DMG, MOD_LEVEL1_CLAW ); + break; + case WP_ALEVEL3: + case WP_ALEVEL3_UPG: + meleeAttack( ent, LEVEL3_CLAW_RANGE, LEVEL3_CLAW_WIDTH, LEVEL3_CLAW_DMG, MOD_LEVEL3_CLAW ); + break; + case WP_ALEVEL2: + meleeAttack( ent, LEVEL2_CLAW_RANGE, LEVEL2_CLAW_WIDTH, LEVEL2_CLAW_DMG, MOD_LEVEL2_CLAW ); + break; + case WP_ALEVEL2_UPG: + meleeAttack( ent, LEVEL2_CLAW_RANGE, LEVEL2_CLAW_WIDTH, LEVEL2_CLAW_DMG, MOD_LEVEL2_CLAW ); + break; + case WP_ALEVEL4: + meleeAttack( ent, LEVEL4_CLAW_RANGE, LEVEL4_CLAW_WIDTH, LEVEL4_CLAW_DMG, MOD_LEVEL4_CLAW ); + break; + + case WP_BLASTER: + blasterFire( ent ); + break; + case WP_MACHINEGUN: + bulletFire( ent, RIFLE_SPREAD, RIFLE_DMG, MOD_MACHINEGUN ); + break; + case WP_SHOTGUN: + shotgunFire( ent ); + break; + case WP_CHAINGUN: + bulletFire( ent, CHAINGUN_SPREAD, CHAINGUN_DMG, MOD_CHAINGUN ); + break; + case WP_FLAMER: + flamerFire( ent ); + break; + case WP_PULSE_RIFLE: + pulseRifleFire( ent ); + break; + case WP_MASS_DRIVER: + massDriverFire( ent ); + break; + case WP_LUCIFER_CANNON: + LCChargeFire( ent, qfalse ); + break; + case WP_LAS_GUN: + lasGunFire( ent ); + break; + case WP_PAIN_SAW: + painSawFire( ent ); + break; + case WP_GRENADE: + throwGrenade( ent ); + break; + + case WP_LOCKBLOB_LAUNCHER: + lockBlobLauncherFire( ent ); + break; + case WP_HIVE: + hiveFire( ent ); + break; + case WP_TESLAGEN: + teslaFire( ent ); + break; + case WP_MGTURRET: + bulletFire( ent, MGTURRET_SPREAD, MGTURRET_DMG, MOD_MGTURRET ); + break; + + case WP_ABUILD: + case WP_ABUILD2: + buildFire( ent, MN_A_BUILD ); + break; + case WP_HBUILD: + case WP_HBUILD2: + buildFire( ent, MN_H_BUILD ); + break; + default: + break; + } +} + diff --git a/src/game/tremulous.h b/src/game/tremulous.h new file mode 100644 index 0000000..0d2d1c1 --- /dev/null +++ b/src/game/tremulous.h @@ -0,0 +1,626 @@ +/* +=========================================================================== +Copyright (C) 1999-2005 Id Software, Inc. +Copyright (C) 2000-2006 Tim Angus + +This file is part of Tremulous. + +Tremulous is free software; you can redistribute it +and/or modify it under the terms of the GNU General Public License as +published by the Free Software Foundation; either version 2 of the License, +or (at your option) any later version. + +Tremulous is distributed in the hope that it will be +useful, but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with Tremulous; if not, write to the Free Software +Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA +=========================================================================== +*/ + + +/* + * ALIEN weapons + * + * _REPEAT - time in msec until the weapon can be used again + * _DMG - amount of damage the weapon does + * + * ALIEN_WDMG_MODIFIER - overall damage modifier for coarse tuning + * + */ + +#define ALIEN_WDMG_MODIFIER 1.0f +#define ADM(d) ((int)((float)d*ALIEN_WDMG_MODIFIER)) + +#define ABUILDER_BUILD_REPEAT 500 +#define ABUILDER_CLAW_DMG ADM(20) +#define ABUILDER_CLAW_RANGE 64.0f +#define ABUILDER_CLAW_WIDTH 4.0f +#define ABUILDER_CLAW_REPEAT 1000 +#define ABUILDER_CLAW_K_SCALE 1.0f +#define ABUILDER_BASE_DELAY 17000 +#define ABUILDER_ADV_DELAY 12000 +#define ABUILDER_BLOB_DMG ADM(4) +#define ABUILDER_BLOB_REPEAT 1000 +#define ABUILDER_BLOB_SPEED 800.0f +#define ABUILDER_BLOB_SPEED_MOD 0.5f +#define ABUILDER_BLOB_TIME 5000 + +#define LEVEL0_BITE_DMG ADM(48) +#define LEVEL0_BITE_RANGE 64.0f +#define LEVEL0_BITE_WIDTH 6.0f +#define LEVEL0_BITE_REPEAT 500 +#define LEVEL0_BITE_K_SCALE 1.0f + +#define LEVEL1_CLAW_DMG ADM(32) +#define LEVEL1_CLAW_RANGE 96.0f +#define LEVEL1_CLAW_WIDTH 10.0f +#define LEVEL1_CLAW_REPEAT 600 +#define LEVEL1_CLAW_U_REPEAT 500 +#define LEVEL1_CLAW_K_SCALE 1.0f +#define LEVEL1_CLAW_U_K_SCALE 1.0f +#define LEVEL1_GRAB_RANGE 64.0f +#define LEVEL1_GRAB_TIME 300 +#define LEVEL1_GRAB_U_TIME 450 +#define LEVEL1_PCLOUD_DMG ADM(4) +#define LEVEL1_PCLOUD_RANGE 200.0f +#define LEVEL1_PCLOUD_REPEAT 2000 +#define LEVEL1_PCLOUD_TIME 10000 + +#define LEVEL2_CLAW_DMG ADM(40) +#define LEVEL2_CLAW_RANGE 96.0f +#define LEVEL2_CLAW_WIDTH 12.0f +#define LEVEL2_CLAW_REPEAT 500 +#define LEVEL2_CLAW_K_SCALE 1.0f +#define LEVEL2_CLAW_U_REPEAT 400 +#define LEVEL2_CLAW_U_K_SCALE 1.0f +#define LEVEL2_AREAZAP_DMG ADM(80) +#define LEVEL2_AREAZAP_RANGE 200.0f +#define LEVEL2_AREAZAP_WIDTH 15.0f +#define LEVEL2_AREAZAP_REPEAT 1500 +#define LEVEL2_AREAZAP_TIME 1000 +#define LEVEL2_AREAZAP_MAX_TARGETS 3 +#define LEVEL2_WALLJUMP_MAXSPEED 1000.0f + +#define LEVEL3_CLAW_DMG ADM(80) +#define LEVEL3_CLAW_RANGE 96.0f +#define LEVEL3_CLAW_WIDTH 16.0f +#define LEVEL3_CLAW_REPEAT 700 +#define LEVEL3_CLAW_K_SCALE 1.0f +#define LEVEL3_CLAW_U_REPEAT 600 +#define LEVEL3_CLAW_U_K_SCALE 1.0f +#define LEVEL3_POUNCE_DMG ADM(100) +#define LEVEL3_POUNCE_RANGE 72.0f +#define LEVEL3_POUNCE_WIDTH 16.0f +#define LEVEL3_POUNCE_SPEED 700 +#define LEVEL3_POUNCE_UPG_SPEED 800 +#define LEVEL3_POUNCE_SPEED_MOD 0.75f +#define LEVEL3_POUNCE_CHARGE_TIME 700 +#define LEVEL3_POUNCE_TIME 400 +#define LEVEL3_BOUNCEBALL_DMG ADM(110) +#define LEVEL3_BOUNCEBALL_REPEAT 1000 +#define LEVEL3_BOUNCEBALL_SPEED 1000.0f + +#define LEVEL4_CLAW_DMG ADM(100) +#define LEVEL4_CLAW_RANGE 128.0f +#define LEVEL4_CLAW_WIDTH 20.0f +#define LEVEL4_CLAW_REPEAT 750 +#define LEVEL4_CLAW_K_SCALE 1.0f +#define LEVEL4_REGEN_RANGE 200.0f +#define LEVEL4_REGEN_MOD 2.0f +#define LEVEL4_CHARGE_SPEED 2.0f +#define LEVEL4_CHARGE_TIME 3000 +#define LEVEL4_CHARGE_CHARGE_TIME 1500 +#define LEVEL4_MIN_CHARGE_TIME 750 +#define LEVEL4_CHARGE_CHARGE_RATIO (LEVEL4_CHARGE_TIME/LEVEL4_CHARGE_CHARGE_TIME) +#define LEVEL4_CHARGE_REPEAT 1000 +#define LEVEL4_CHARGE_DMG ADM(110) + + + +/* + * ALIEN classes + * + * _SPEED - fraction of Q3A run speed the class can move + * _REGEN - health per second regained + * + * ALIEN_HLTH_MODIFIER - overall health modifier for coarse tuning + * + */ + +#define ALIEN_HLTH_MODIFIER 1.0f +#define AHM(h) ((int)((float)h*ALIEN_HLTH_MODIFIER)) + +#define ALIEN_VALUE_MODIFIER 1.0f +#define AVM(h) ((int)((float)h*ALIEN_VALUE_MODIFIER)) + +#define ABUILDER_SPEED 0.8f +#define ABUILDER_VALUE AVM(200) +#define ABUILDER_HEALTH AHM(50) +#define ABUILDER_REGEN 2 +#define ABUILDER_COST 0 + +#define ABUILDER_UPG_SPEED 1.0f +#define ABUILDER_UPG_VALUE AVM(250) +#define ABUILDER_UPG_HEALTH AHM(75) +#define ABUILDER_UPG_REGEN 3 +#define ABUILDER_UPG_COST 0 + +#define LEVEL0_SPEED 1.3f +#define LEVEL0_VALUE AVM(175) +#define LEVEL0_HEALTH AHM(25) +#define LEVEL0_REGEN 1 +#define LEVEL0_COST 0 + +#define LEVEL1_SPEED 1.25f +#define LEVEL1_VALUE AVM(225) +#define LEVEL1_HEALTH AHM(75) +#define LEVEL1_REGEN 2 +#define LEVEL1_COST 1 + +#define LEVEL1_UPG_SPEED 1.25f +#define LEVEL1_UPG_VALUE AVM(275) +#define LEVEL1_UPG_HEALTH AHM(100) +#define LEVEL1_UPG_REGEN 3 +#define LEVEL1_UPG_COST 1 + +#define LEVEL2_SPEED 1.2f +#define LEVEL2_VALUE AVM(350) +#define LEVEL2_HEALTH AHM(150) +#define LEVEL2_REGEN 4 +#define LEVEL2_COST 1 + +#define LEVEL2_UPG_SPEED 1.2f +#define LEVEL2_UPG_VALUE AVM(450) +#define LEVEL2_UPG_HEALTH AHM(175) +#define LEVEL2_UPG_REGEN 5 +#define LEVEL2_UPG_COST 1 + +#define LEVEL3_SPEED 1.1f +#define LEVEL3_VALUE AVM(500) +#define LEVEL3_HEALTH AHM(200) +#define LEVEL3_REGEN 6 +#define LEVEL3_COST 1 + +#define LEVEL3_UPG_SPEED 1.1f +#define LEVEL3_UPG_VALUE AVM(600) +#define LEVEL3_UPG_HEALTH AHM(250) +#define LEVEL3_UPG_REGEN 7 +#define LEVEL3_UPG_COST 1 + +#define LEVEL4_SPEED 1.2f +#define LEVEL4_VALUE AVM(800) +#define LEVEL4_HEALTH AHM(400) +#define LEVEL4_REGEN 7 +#define LEVEL4_COST 2 + + + +/* + * ALIEN buildables + * + * _BP - build points required for this buildable + * _BT - build time required for this buildable + * _REGEN - the amount of health per second regained + * _SPLASHDAMGE - the amount of damage caused by this buildable when melting + * _SPLASHRADIUS - the radius around which it does this damage + * + * CREEP_BASESIZE - the maximum distance a buildable can be from an egg/overmind + * ALIEN_BHLTH_MODIFIER - overall health modifier for coarse tuning + * + */ + +#define ALIEN_BHLTH_MODIFIER 1.0f +#define ABHM(h) ((int)((float)h*ALIEN_BHLTH_MODIFIER)) + +#define CREEP_BASESIZE 700 +#define CREEP_TIMEOUT 1000 +#define CREEP_MODIFIER 0.5f +#define CREEP_ARMOUR_MODIFIER 0.75f +#define CREEP_SCALEDOWN_TIME 3000 + +#define ASPAWN_BP 10 +#define ASPAWN_BT 15000 +#define ASPAWN_HEALTH ABHM(250) +#define ASPAWN_REGEN 8 +#define ASPAWN_SPLASHDAMAGE 50 +#define ASPAWN_SPLASHRADIUS 50 +#define ASPAWN_CREEPSIZE 120 +#define ASPAWN_VALUE 150 + +#define BARRICADE_BP 10 +#define BARRICADE_BT 20000 +#define BARRICADE_HEALTH ABHM(200) +#define BARRICADE_REGEN 14 +#define BARRICADE_SPLASHDAMAGE 50 +#define BARRICADE_SPLASHRADIUS 50 +#define BARRICADE_CREEPSIZE 120 + +#define BOOSTER_BP 12 +#define BOOSTER_BT 15000 +#define BOOSTER_HEALTH ABHM(150) +#define BOOSTER_REGEN 8 +#define BOOSTER_SPLASHDAMAGE 50 +#define BOOSTER_SPLASHRADIUS 50 +#define BOOSTER_CREEPSIZE 120 +#define BOOSTER_INTERVAL 30000 //time in msec between uses (per player) +#define BOOSTER_REGEN_MOD 2.0f +#define BOOST_TIME 30000 + +#define ACIDTUBE_BP 8 +#define ACIDTUBE_BT 15000 +#define ACIDTUBE_HEALTH ABHM(125) +#define ACIDTUBE_REGEN 10 +#define ACIDTUBE_SPLASHDAMAGE 6 +#define ACIDTUBE_SPLASHRADIUS 300 +#define ACIDTUBE_CREEPSIZE 120 +#define ACIDTUBE_RANGE 300.0f +#define ACIDTUBE_REPEAT 3000 +#define ACIDTUBE_K_SCALE 1.0f + +#define HIVE_BP 12 +#define HIVE_BT 20000 +#define HIVE_HEALTH ABHM(125) +#define HIVE_REGEN 10 +#define HIVE_SPLASHDAMAGE 30 +#define HIVE_SPLASHRADIUS 200 +#define HIVE_CREEPSIZE 120 +#define HIVE_RANGE 400.0f +#define HIVE_REPEAT 5000 +#define HIVE_K_SCALE 1.0f +#define HIVE_DMG 50 +#define HIVE_SPEED 240.0f +#define HIVE_DIR_CHANGE_PERIOD 500 + +#define TRAPPER_BP 8 +#define TRAPPER_BT 12000 +#define TRAPPER_HEALTH ABHM(50) +#define TRAPPER_REGEN 6 +#define TRAPPER_SPLASHDAMAGE 15 +#define TRAPPER_SPLASHRADIUS 100 +#define TRAPPER_CREEPSIZE 30 +#define TRAPPER_RANGE 400 +#define TRAPPER_REPEAT 1000 +#define TRAPPER_K_SCALE 1.0f +#define LOCKBLOB_SPEED 650.0f +#define LOCKBLOB_LOCKTIME 5000 +#define LOCKBLOB_DOT 0.85f // max angle = acos( LOCKBLOB_DOT ) +#define LOCKBLOB_K_SCALE 1.0f + +#define OVERMIND_BP 0 +#define OVERMIND_BT 30000 +#define OVERMIND_HEALTH ABHM(750) +#define OVERMIND_REGEN 6 +#define OVERMIND_SPLASHDAMAGE 15 +#define OVERMIND_SPLASHRADIUS 300 +#define OVERMIND_CREEPSIZE 120 +#define OVERMIND_ATTACK_RANGE 150.0f +#define OVERMIND_ATTACK_REPEAT 1000 +#define OVERMIND_VALUE 300 + +#define HOVEL_BP 0 +#define HOVEL_BT 15000 +#define HOVEL_HEALTH ABHM(375) +#define HOVEL_REGEN 20 +#define HOVEL_SPLASHDAMAGE 20 +#define HOVEL_SPLASHRADIUS 200 +#define HOVEL_CREEPSIZE 120 + + + +/* + * ALIEN misc + * + * ALIENSENSE_RANGE - the distance alien sense is useful for + * + */ + +#define ALIENSENSE_RANGE 1000.0f + +#define ALIEN_POISON_TIME 5000 +#define ALIEN_POISON_DMG 5 +#define ALIEN_POISON_DIVIDER (1.0f/1.32f) //about 1.0/(time`th root of damage) + +#define ALIEN_SPAWN_REPEAT_TIME 10000 + +#define ALIEN_REGEN_DAMAGE_TIME 2000 //msec since damage that regen starts again + +/* + * HUMAN weapons + * + * _REPEAT - time between firings + * _RELOAD - time needed to reload + * _PRICE - amount in credits weapon costs + * + * HUMAN_WDMG_MODIFIER - overall damage modifier for coarse tuning + * + */ + +#define HUMAN_WDMG_MODIFIER 1.0f +#define HDM(d) ((int)((float)d*HUMAN_WDMG_MODIFIER)) + +#define BLASTER_REPEAT 600 +#define BLASTER_K_SCALE 1.0f +#define BLASTER_SPREAD 200 +#define BLASTER_SPEED 1400 +#define BLASTER_DMG HDM(9) + +#define RIFLE_CLIPSIZE 30 +#define RIFLE_MAXCLIPS 6 +#define RIFLE_REPEAT 90 +#define RIFLE_K_SCALE 1.0f +#define RIFLE_RELOAD 2000 +#define RIFLE_PRICE 0 +#define RIFLE_SPREAD 200 +#define RIFLE_DMG HDM(5) + +#define PAINSAW_PRICE 100 +#define PAINSAW_REPEAT 75 +#define PAINSAW_K_SCALE 1.0f +#define PAINSAW_DAMAGE HDM(15) +#define PAINSAW_RANGE 40.0f + +#define GRENADE_PRICE 200 +#define GRENADE_REPEAT 0 +#define GRENADE_K_SCALE 1.0f +#define GRENADE_DAMAGE HDM(310) +#define GRENADE_RANGE 192.0f +#define GRENADE_SPEED 400.0f + +#define SHOTGUN_PRICE 150 +#define SHOTGUN_SHELLS 8 +#define SHOTGUN_PELLETS 8 //used to sync server and client side +#define SHOTGUN_MAXCLIPS 3 +#define SHOTGUN_REPEAT 1000 +#define SHOTGUN_K_SCALE 1.0f +#define SHOTGUN_RELOAD 2000 +#define SHOTGUN_SPREAD 900 +#define SHOTGUN_DMG HDM(7) + +#define LASGUN_PRICE 250 +#define LASGUN_AMMO 200 +#define LASGUN_REPEAT 200 +#define LASGUN_K_SCALE 1.0f +#define LASGUN_RELOAD 2000 +#define LASGUN_DAMAGE HDM(9) + +#define MDRIVER_PRICE 350 +#define MDRIVER_CLIPSIZE 5 +#define MDRIVER_MAXCLIPS 4 +#define MDRIVER_DMG HDM(38) +#define MDRIVER_REPEAT 1000 +#define MDRIVER_K_SCALE 1.0f +#define MDRIVER_RELOAD 2000 + +#define CHAINGUN_PRICE 400 +#define CHAINGUN_BULLETS 300 +#define CHAINGUN_REPEAT 80 +#define CHAINGUN_K_SCALE 1.0f +#define CHAINGUN_SPREAD 1000 +#define CHAINGUN_DMG HDM(6) + +#define PRIFLE_PRICE 400 +#define PRIFLE_CLIPS 50 +#define PRIFLE_MAXCLIPS 4 +#define PRIFLE_REPEAT 100 +#define PRIFLE_K_SCALE 1.0f +#define PRIFLE_RELOAD 2000 +#define PRIFLE_DMG HDM(9) +#define PRIFLE_SPEED 1000 + +#define FLAMER_PRICE 450 +#define FLAMER_GAS 150 +#define FLAMER_REPEAT 200 +#define FLAMER_K_SCALE 1.0f +#define FLAMER_DMG HDM(20) +#define FLAMER_RADIUS 50 +#define FLAMER_LIFETIME 800.0f +#define FLAMER_SPEED 200.0f +#define FLAMER_LAG 0.65f //the amount of player velocity that is added to the fireball + +#define LCANNON_PRICE 600 +#define LCANNON_AMMO 90 +#define LCANNON_REPEAT 500 +#define LCANNON_K_SCALE 1.0f +#define LCANNON_CHARGEREPEAT 1000 +#define LCANNON_RELOAD 2000 +#define LCANNON_DAMAGE HDM(265) +#define LCANNON_RADIUS 150 +#define LCANNON_SECONDARY_DAMAGE HDM(27) +#define LCANNON_SECONDARY_RADIUS 75 +#define LCANNON_SPEED 350 +#define LCANNON_CHARGE_TIME 2000 +#define LCANNON_TOTAL_CHARGE 255 +#define LCANNON_MIN_CHARGE 50 + +#define HBUILD_PRICE 0 +#define HBUILD_REPEAT 1000 +#define HBUILD_DELAY 17500 +#define HBUILD_HEALRATE 18 + +#define HBUILD2_PRICE 0 +#define HBUILD2_REPEAT 1000 +#define HBUILD2_DELAY 15000 + + + +/* + * HUMAN upgrades + */ + +#define LIGHTARMOUR_PRICE 70 +#define LIGHTARMOUR_POISON_PROTECTION 1 + +#define HELMET_PRICE 90 +#define HELMET_RANGE 1000.0f +#define HELMET_POISON_PROTECTION 2 + +#define MEDKIT_PRICE 0 + +#define BATTPACK_PRICE 100 +#define BATTPACK_MODIFIER 1.5f //modifier for extra energy storage available + +#define JETPACK_PRICE 120 +#define JETPACK_FLOAT_SPEED 128.0f //up movement speed +#define JETPACK_SINK_SPEED 192.0f //down movement speed +#define JETPACK_DISABLE_TIME 1000 //time to disable the jetpack when player damaged +#define JETPACK_DISABLE_CHANCE 0.3f + +#define BSUIT_PRICE 400 +#define BSUIT_POISON_PROTECTION 4 + +#define MGCLIP_PRICE 0 + +#define CGAMMO_PRICE 0 + +#define GAS_PRICE 0 + +#define MEDKIT_POISON_IMMUNITY_TIME 0 +#define MEDKIT_STARTUP_TIME 4000 +#define MEDKIT_STARTUP_SPEED 5 + + +/* + * HUMAN buildables + * + * _BP - build points required for this buildable + * _BT - build time required for this buildable + * _SPLASHDAMGE - the amount of damage caused by this buildable when it blows up + * _SPLASHRADIUS - the radius around which it does this damage + * + * REACTOR_BASESIZE - the maximum distance a buildable can be from an reactor + * REPEATER_BASESIZE - the maximum distance a buildable can be from a repeater + * HUMAN_BHLTH_MODIFIER - overall health modifier for coarse tuning + * + */ + +#define HUMAN_BHLTH_MODIFIER 1.0f +#define HBHM(h) ((int)((float)h*HUMAN_BHLTH_MODIFIER)) + +#define REACTOR_BASESIZE 1000 +#define REPEATER_BASESIZE 500 +#define HUMAN_DETONATION_DELAY 5000 + +#define HSPAWN_BP 10 +#define HSPAWN_BT 10000 +#define HSPAWN_HEALTH HBHM(310) +#define HSPAWN_SPLASHDAMAGE 50 +#define HSPAWN_SPLASHRADIUS 100 +#define HSPAWN_VALUE 1 + +#define MEDISTAT_BP 8 +#define MEDISTAT_BT 10000 +#define MEDISTAT_HEALTH HBHM(190) +#define MEDISTAT_SPLASHDAMAGE 50 +#define MEDISTAT_SPLASHRADIUS 100 + +#define MGTURRET_BP 8 +#define MGTURRET_BT 10000 +#define MGTURRET_HEALTH HBHM(190) +#define MGTURRET_SPLASHDAMAGE 100 +#define MGTURRET_SPLASHRADIUS 100 +#define MGTURRET_ANGULARSPEED 8 //degrees/think ~= 200deg/sec +#define MGTURRET_ACCURACYTOLERANCE MGTURRET_ANGULARSPEED / 1.5f //angular difference for turret to fire +#define MGTURRET_VERTICALCAP 30 // +/- maximum pitch +#define MGTURRET_REPEAT 100 +#define MGTURRET_K_SCALE 1.0f +#define MGTURRET_RANGE 300.0f +#define MGTURRET_SPREAD 200 +#define MGTURRET_DMG HDM(4) +#define MGTURRET_DCC_ANGULARSPEED 10 +#define MGTURRET_DCC_ACCURACYTOLERANCE MGTURRET_DCC_ANGULARSPEED / 1.5f +#define MGTURRET_GRAB_ANGULARSPEED 3 +#define MGTURRET_GRAB_ACCURACYTOLERANCE MGTURRET_GRAB_ANGULARSPEED / 1.5f + +#define TESLAGEN_BP 10 +#define TESLAGEN_BT 15000 +#define TESLAGEN_HEALTH HBHM(220) +#define TESLAGEN_SPLASHDAMAGE 50 +#define TESLAGEN_SPLASHRADIUS 100 +#define TESLAGEN_REPEAT 250 +#define TESLAGEN_K_SCALE 4.0f +#define TESLAGEN_RANGE 250 +#define TESLAGEN_DMG HDM(9) + +#define DC_BP 8 +#define DC_BT 10000 +#define DC_HEALTH HBHM(190) +#define DC_SPLASHDAMAGE 50 +#define DC_SPLASHRADIUS 100 + +#define ARMOURY_BP 10 +#define ARMOURY_BT 10000 +#define ARMOURY_HEALTH HBHM(280) +#define ARMOURY_SPLASHDAMAGE 50 +#define ARMOURY_SPLASHRADIUS 100 + +#define REACTOR_BP 0 +#define REACTOR_BT 20000 +#define REACTOR_HEALTH HBHM(930) +#define REACTOR_SPLASHDAMAGE 200 +#define REACTOR_SPLASHRADIUS 300 +#define REACTOR_ATTACK_RANGE 100.0f +#define REACTOR_ATTACK_REPEAT 1000 +#define REACTOR_ATTACK_DAMAGE 40 +#define REACTOR_VALUE 2 + +#define REPEATER_BP 0 +#define REPEATER_BT 10000 +#define REPEATER_HEALTH HBHM(250) +#define REPEATER_SPLASHDAMAGE 50 +#define REPEATER_SPLASHRADIUS 100 +#define REPEATER_INACTIVE_TIME 90000 + +/* + * HUMAN misc + */ + +#define HUMAN_SPRINT_MODIFIER 1.2f +#define HUMAN_JOG_MODIFIER 1.0f +#define HUMAN_BACK_MODIFIER 0.8f +#define HUMAN_SIDE_MODIFIER 0.9f + +#define STAMINA_STOP_RESTORE 25 +#define STAMINA_WALK_RESTORE 15 +#define STAMINA_SPRINT_TAKE 8 +#define STAMINA_LARMOUR_TAKE 4 + +#define HUMAN_SPAWN_REPEAT_TIME 10000 + +/* + * Misc + */ + +#define MIN_FALL_DISTANCE 30.0f //the fall distance at which fall damage kicks in +#define MAX_FALL_DISTANCE 120.0f //the fall distance at which maximum damage is dealt +#define AVG_FALL_DISTANCE ((MIN_FALL_DISTANCE+MAX_FALL_DISTANCE)/2.0f) + +#define HUMAN_MAXED 900 //a human with a strong selection of weapons/upgrades +#define HUMAN_MAX_CREDITS 2000 +#define ALIEN_MAX_KILLS 9 +#define ALIEN_MAX_SINGLE_KILLS 3 + +#define FREEKILL_PERIOD 120000 //msec +#define FREEKILL_ALIEN 1 +#define FREEKILL_HUMAN LEVEL0_VALUE + +#define DEFAULT_ALIEN_BUILDPOINTS "100" +#define DEFAULT_ALIEN_STAGE2_THRESH "20" +#define DEFAULT_ALIEN_STAGE3_THRESH "40" +#define DEFAULT_ALIEN_MAX_STAGE "2" +#define DEFAULT_HUMAN_BUILDPOINTS "100" +#define DEFAULT_HUMAN_STAGE2_THRESH "20" +#define DEFAULT_HUMAN_STAGE3_THRESH "40" +#define DEFAULT_HUMAN_MAX_STAGE "2" + +#define DAMAGE_FRACTION_FOR_KILL 0.5f //how much damage players (versus structures) need to + //do to increment the stage kill counters + +// g_suddenDeathMode settings +#define SDMODE_BP 0 +#define SDMODE_NO_BUILD 1 +#define SDMODE_SELECTIVE 2 +#define SDMODE_NO_DECON 3 |