diff options
author | Paweł Redman <trem.redman@gmail.com> | 2013-09-06 22:40:51 +0200 |
---|---|---|
committer | Paweł Redman <trem.redman@gmail.com> | 2013-09-06 22:40:51 +0200 |
commit | 5a85e81685300e2299dabfeb25d513b99df471be (patch) | |
tree | 45c3e342a9af062528c6c32b695629a65eede91b /src/game |
Initial commit
Diffstat (limited to 'src/game')
41 files changed, 58934 insertions, 0 deletions
diff --git a/src/game/bg_alloc.c b/src/game/bg_alloc.c new file mode 100644 index 0000000..b145c62 --- /dev/null +++ b/src/game/bg_alloc.c @@ -0,0 +1,241 @@ +/* +=========================================================================== +Copyright (C) 1999-2005 Id Software, Inc. +Copyright (C) 2000-2009 Darklegion Development + +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 "../qcommon/q_shared.h" +#include "bg_public.h" + +#ifdef GAME +# define POOLSIZE ( 1024 * 1024 ) +#else +# define POOLSIZE ( 256 * 1024 ) +#endif + +#define FREEMEMCOOKIE ((int)0xDEADBE3F) // Any unlikely to be used value +#define ROUNDBITS 31 // Round to 32 bytes + +typedef struct freeMemNode_s +{ + // Size of ROUNDBITS + int cookie, size; // Size includes node (obviously) + struct freeMemNode_s *prev, *next; +} freeMemNode_t; + +static char memoryPool[POOLSIZE]; +static freeMemNode_t *freeHead; +static int freeMem; + +void *BG_Alloc( int size ) +{ + // Find a free block and allocate. + // Does two passes, attempts to fill same-sized free slot first. + + freeMemNode_t *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 ) + Com_Error( ERR_DROP, "BG_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; + memset( ptr, 0, allocsize ); + *ptr++ = allocsize; // Store a copy of size for deallocation + return( (void *) ptr ); + } + + Com_Error( ERR_DROP, "BG_Alloc: failed on allocation of %i bytes\n", size ); + return( NULL ); +} + +void BG_Free( void *ptr ) +{ + // Release allocated memory, add it to the free list. + + freeMemNode_t *fmn; + char *freeend; + int *freeptr; + + freeptr = ptr; + freeptr--; + + freeMem += *freeptr; + + 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 = (freeMemNode_t *) 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 BG_InitMemory( void ) +{ + // Set up the initial node + + freeHead = (freeMemNode_t *)memoryPool; + freeHead->cookie = FREEMEMCOOKIE; + freeHead->size = POOLSIZE; + freeHead->next = NULL; + freeHead->prev = NULL; + freeMem = sizeof( memoryPool ); +} + +void BG_DefragmentMemory( void ) +{ + // If there's a frenzy of deallocation and we want to + // allocate something big, this is useful. Otherwise... + // not much use. + + freeMemNode_t *startfmn, *endfmn, *fmn; + + for( startfmn = freeHead; startfmn; ) + { + endfmn = (freeMemNode_t *)(((char *) startfmn) + startfmn->size); + for( fmn = freeHead; fmn; ) + { + if( fmn->cookie != FREEMEMCOOKIE ) + Com_Error( ERR_DROP, "BG_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(freeMemNode_t) ); // 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 BG_MemoryInfo( void ) +{ + // Give a breakdown of memory + + freeMemNode_t *fmn = (freeMemNode_t *)memoryPool; + int size, chunks; + freeMemNode_t *end = (freeMemNode_t *)( memoryPool + POOLSIZE ); + void *p; + + Com_Printf( "%p-%p: %d out of %d bytes allocated\n", + fmn, end, POOLSIZE - freeMem, POOLSIZE ); + + while( fmn < end ) + { + size = chunks = 0; + p = fmn; + while( fmn < end && fmn->cookie == FREEMEMCOOKIE ) + { + size += fmn->size; + chunks++; + fmn = (freeMemNode_t *)( (char *)fmn + fmn->size ); + } + if( size ) + Com_Printf( " %p: %d bytes free (%d chunks)\n", p, size, chunks ); + size = chunks = 0; + p = fmn; + while( fmn < end && fmn->cookie != FREEMEMCOOKIE ) + { + size += *(int *)fmn; + chunks++; + fmn = (freeMemNode_t *)( (size_t)fmn + *(int *)fmn ); + } + if( size ) + Com_Printf( " %p: %d bytes allocated (%d chunks)\n", p, size, chunks ); + } +} diff --git a/src/game/bg_lib.c b/src/game/bg_lib.c new file mode 100644 index 0000000..bdace78 --- /dev/null +++ b/src/game/bg_lib.c @@ -0,0 +1,2816 @@ +/* +=========================================================================== +Copyright (C) 1999-2005 Id Software, Inc. +Copyright (C) 2000-2009 Darklegion Development + +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 + + +#ifdef Q3_VM + +#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 2248 2011-08-08 22:39:07Z cschwarz $"; +#endif /* LIBC_SCCS and not lint */ + +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);*/ +} + +//================================================================================== + + +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; +} + +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; +} + +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; +} + +void *memmove( void *dest, const void *src, size_t count ) +{ + size_t i; + + if( dest > src ) + { + i = count; + while( 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 + +/* +=============== +rint +=============== +*/ +double rint( double v ) +{ + if( v >= 0.5f ) + return ceil( v ); + else + return floor( v ); +} + +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; +} + + + +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; +} + +/* +============== +strtod + +Without an errno variable, this is a fair bit less useful than it is in libc +but it's still a fair bit more capable than atof or _atof +Handles inf[inity], nan (ignoring case), hexadecimals, and decimals +Handles decimal exponents like 10e10 and hex exponents like 0x7f8p20 +10e10 == 10000000000 (power of ten) +0x7f8p20 == 0x7f800000 (decimal power of two) +The variable pointed to by endptr will hold the location of the first character +in the nptr string that was not used in the conversion +============== +*/ +double strtod( const char *nptr, char **endptr ) +{ + double res; + qboolean neg = qfalse; + + // skip whitespace + while( isspace( *nptr ) ) + nptr++; + + // special string parsing + if( Q_stricmpn( nptr, "nan", 3 ) == 0 ) + { + floatint_t nan; + + if( endptr ) + *endptr = (char *)&nptr[3]; + + // nan can be followed by a bracketed number (in hex, octal, + // or decimal) which is then put in the mantissa + // this can be used to generate signalling or quiet NaNs, for + // example (though I doubt it'll ever be used) + // note that nan(0) is infinity! + if( nptr[3] == '(' ) + { + char *end; + int mantissa = strtol( &nptr[4], &end, 0 ); + + if( *end == ')' ) + { + nan.ui = 0x7f800000 | ( mantissa & 0x7fffff ); + + if( endptr ) + *endptr = &end[1]; + return nan.f; + } + } + + nan.ui = 0x7fffffff; + return nan.f; + } + + if( Q_stricmpn( nptr, "inf", 3 ) == 0 ) + { + floatint_t inf; + inf.ui = 0x7f800000; + + if( endptr == NULL ) + return inf.f; + + if( Q_stricmpn( &nptr[3], "inity", 5 ) == 0 ) + *endptr = (char *)&nptr[8]; + else + *endptr = (char *)&nptr[3]; + + return inf.f; + } + + // normal numeric parsing + // sign + if( *nptr == '-' ) + { + nptr++; + neg = qtrue; + } + else if( *nptr == '+' ) + nptr++; + + // hex + if( Q_stricmpn( nptr, "0x", 2 ) == 0 ) + { + // track if we use any digits + const char *s = &nptr[1], *end = s; + nptr += 2; + + for( res = 0;; ) + { + if( isdigit( *nptr ) ) + res = 16 * res + ( *nptr++ - '0' ); + else if( *nptr >= 'A' && *nptr <= 'F' ) + res = 16 * res + 10 + *nptr++ - 'A'; + else if( *nptr >= 'a' && *nptr <= 'f' ) + res = 16 * res + 10 + *nptr++ - 'a'; + else + break; + } + + // if nptr moved, save it + if( end + 1 < nptr ) + end = nptr; + + if( *nptr == '.' ) + { + float place; + nptr++; + + // 1.0 / 16.0 == 0.0625 + // I don't expect the float accuracy to hold out for + // very long but since we need to know the length of + // the string anyway we keep on going regardless + for( place = 0.0625;; place /= 16.0 ) + { + if( isdigit( *nptr ) ) + res += place * ( *nptr++ - '0' ); + else if( *nptr >= 'A' && *nptr <= 'F' ) + res += place * ( 10 + *nptr++ - 'A' ); + else if( *nptr >= 'a' && *nptr <= 'f' ) + res += place * ( 10 + *nptr++ - 'a' ); + else + break; + } + + if( end < nptr ) + end = nptr; + } + + // parse an optional exponent, representing multiplication + // by a power of two + // exponents are only valid if we encountered at least one + // digit already (and have therefore set end to something) + if( end != s && tolower( *nptr ) == 'p' ) + { + int exp; + float res2; + // apparently (confusingly) the exponent should be + // decimal + exp = strtol( &nptr[1], (char **)&end, 10 ); + if( &nptr[1] == end ) + { + // no exponent + if( endptr ) + *endptr = (char *)nptr; + return res; + } + if( exp > 0 ) + { + while( exp-- > 0 ) + { + res2 = res * 2; + // check for infinity + if( res2 <= res ) + break; + res = res2; + } + } + else + { + while( exp++ < 0 ) + { + res2 = res / 2; + // check for underflow + if( res2 >= res ) + break; + res = res2; + } + } + } + if( endptr ) + *endptr = (char *)end; + return res; + } + // decimal + else + { + // track if we find any digits + const char *end = nptr, *p = nptr; + // this is most of the work + for( res = 0; isdigit( *nptr ); + res = 10 * res + *nptr++ - '0' ); + // if nptr moved, we read something + if( end < nptr ) + end = nptr; + if( *nptr == '.' ) + { + // fractional part + float place; + nptr++; + for( place = 0.1; isdigit( *nptr ); place /= 10.0 ) + res += ( *nptr++ - '0' ) * place; + // if nptr moved, we read something + if( end + 1 < nptr ) + end = nptr; + } + // exponent + // meaningless without having already read digits, so check + // we've set end to something + if( p != end && tolower( *nptr ) == 'e' ) + { + int exp; + float res10; + exp = strtol( &nptr[1], (char **)&end, 10 ); + if( &nptr[1] == end ) + { + // no exponent + if( endptr ) + *endptr = (char *)nptr; + return res; + } + if( exp > 0 ) + { + while( exp-- > 0 ) + { + res10 = res * 10; + // check for infinity to save us time + if( res10 <= res ) + break; + res = res10; + } + } + else if( exp < 0 ) + { + while( exp++ < 0 ) + { + res10 = res / 10; + // check for underflow + // (test for 0 would probably be just + // as good) + if( res10 >= res ) + break; + res = res10; + } + } + } + if( endptr ) + *endptr = (char *)end; + return res; + } +} + +double _atof( const char **stringPtr ) +{ + const char *string; + float sign; + float value; + int c = '0'; + + 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; +} + +/* +============== +strtol + +Handles any base from 2 to 36. If base is 0 then it guesses +decimal, hex, or octal based on the format of the number (leading 0 or 0x) +Will not overflow - returns LONG_MIN or LONG_MAX as appropriate +*endptr is set to the location of the first character not used +============== +*/ +long strtol( const char *nptr, char **endptr, int base ) +{ + long res; + qboolean pos = qtrue; + + if( endptr ) + *endptr = (char *)nptr; + + // bases other than 0, 2, 8, 16 are very rarely used, but they're + // not much extra effort to support + if( base < 0 || base == 1 || base > 36 ) + return 0; + + // skip leading whitespace + while( isspace( *nptr ) ) + nptr++; + + // sign + if( *nptr == '-' ) + { + nptr++; + pos = qfalse; + } + else if( *nptr == '+' ) + nptr++; + + // look for base-identifying sequences e.g. 0x for hex, 0 for octal + if( nptr[0] == '0' ) + { + nptr++; + + // 0 is always a valid digit + if( endptr ) + *endptr = (char *)nptr; + + if( *nptr == 'x' || *nptr == 'X' ) + { + if( base != 0 && base != 16 ) + { + // can't be hex, reject x (accept 0) + if( endptr ) + *endptr = (char *)nptr; + return 0; + } + + nptr++; + base = 16; + } + else if( base == 0 ) + base = 8; + } + else if( base == 0 ) + base = 10; + + for( res = 0;; ) + { + int val; + + if( isdigit( *nptr ) ) + val = *nptr - '0'; + else if( islower( *nptr ) ) + val = 10 + *nptr - 'a'; + else if( isupper( *nptr ) ) + val = 10 + *nptr - 'A'; + else + break; + + if( val >= base ) + break; + + // we go negative because LONG_MIN is further from 0 than + // LONG_MAX + if( res < ( LONG_MIN + val ) / base ) + res = LONG_MIN; // overflow + else + res = res * base - val; + + nptr++; + + if( endptr ) + *endptr = (char *)nptr; + } + if( pos ) + { + // can't represent LONG_MIN positive + if( res == LONG_MIN ) + res = LONG_MAX; + else + res = -res; + } + return res; +} + +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; +} + +//========================================================= + +/* + * New implementation by Patrick Powell and others for vsnprintf. + * Supports length checking in strings. + */ + +/* + * Copyright Patrick Powell 1995 + * This code is based on code written by Patrick Powell (papowell@astart.com) + * It may be used for any purpose as long as this notice remains intact + * on all source code distributions + */ + +/************************************************************** + * Original: + * Patrick Powell Tue Apr 11 09:48:21 PDT 1995 + * A bombproof version of doprnt (dopr) included. + * Sigh. This sort of thing is always nasty do deal with. Note that + * the version here does not include floating point... + * + * snprintf() is used instead of sprintf() as it does limit checks + * for string length. This covers a nasty loophole. + * + * The other functions are there to prevent NULL pointers from + * causing nast effects. + * + * More Recently: + * Brandon Long <blong@fiction.net> 9/15/96 for mutt 0.43 + * This was ugly. It is still ugly. I opted out of floating point + * numbers, but the formatter understands just about everything + * from the normal C string format, at least as far as I can tell from + * the Solaris 2.5 printf(3S) man page. + * + * Brandon Long <blong@fiction.net> 10/22/97 for mutt 0.87.1 + * Ok, added some minimal floating point support, which means this + * probably requires libm on most operating systems. Don't yet + * support the exponent (e,E) and sigfig (g,G). Also, fmtint() + * was pretty badly broken, it just wasn't being exercised in ways + * which showed it, so that's been fixed. Also, formated the code + * to mutt conventions, and removed dead code left over from the + * original. Also, there is now a builtin-test, just compile with: + * gcc -DTEST_SNPRINTF -o snprintf snprintf.c -lm + * and run snprintf for results. + * + * Thomas Roessler <roessler@guug.de> 01/27/98 for mutt 0.89i + * The PGP code was using unsigned hexadecimal formats. + * Unfortunately, unsigned formats simply didn't work. + * + * Michael Elkins <me@cs.hmc.edu> 03/05/98 for mutt 0.90.8 + * The original code assumed that both snprintf() and vsnprintf() were + * missing. Some systems only have snprintf() but not vsnprintf(), so + * the code is now broken down under HAVE_SNPRINTF and HAVE_VSNPRINTF. + * + * Andrew Tridgell (tridge@samba.org) Oct 1998 + * fixed handling of %.0f + * added test for HAVE_LONG_DOUBLE + * + * Russ Allbery <rra@stanford.edu> 2000-08-26 + * fixed return value to comply with C99 + * fixed handling of snprintf(NULL, ...) + * + * Hrvoje Niksic <hniksic@arsdigita.com> 2000-11-04 + * include <config.h> instead of "config.h". + * moved TEST_SNPRINTF stuff out of HAVE_SNPRINTF ifdef. + * include <stdio.h> for NULL. + * added support and test cases for long long. + * don't declare argument types to (v)snprintf if stdarg is not used. + * use int instead of short int as 2nd arg to va_arg. + * + **************************************************************/ + +/* BDR 2002-01-13 %e and %g were being ignored. Now do something, + if not necessarily correctly */ + +#if (SIZEOF_LONG_DOUBLE > 0) +/* #ifdef HAVE_LONG_DOUBLE */ +#define LDOUBLE long double +#else +#define LDOUBLE double +#endif + +#if (SIZEOF_LONG_LONG > 0) +/* #ifdef HAVE_LONG_LONG */ +# define LLONG long long +#else +# define LLONG long +#endif + +static int dopr (char *buffer, size_t maxlen, const char *format, + va_list args); +static int fmtstr (char *buffer, size_t *currlen, size_t maxlen, + char *value, int flags, int min, int max); +static int fmtint (char *buffer, size_t *currlen, size_t maxlen, + LLONG value, int base, int min, int max, int flags); +static int fmtfp (char *buffer, size_t *currlen, size_t maxlen, + LDOUBLE fvalue, int min, int max, int flags); +static int dopr_outch (char *buffer, size_t *currlen, size_t maxlen, char c ); + +/* + * dopr(): poor man's version of doprintf + */ + +/* format read states */ +#define DP_S_DEFAULT 0 +#define DP_S_FLAGS 1 +#define DP_S_MIN 2 +#define DP_S_DOT 3 +#define DP_S_MAX 4 +#define DP_S_MOD 5 +#define DP_S_MOD_L 6 +#define DP_S_CONV 7 +#define DP_S_DONE 8 + +/* format flags - Bits */ +#define DP_F_MINUS (1 << 0) +#define DP_F_PLUS (1 << 1) +#define DP_F_SPACE (1 << 2) +#define DP_F_NUM (1 << 3) +#define DP_F_ZERO (1 << 4) +#define DP_F_UP (1 << 5) +#define DP_F_UNSIGNED (1 << 6) + +/* Conversion Flags */ +#define DP_C_SHORT 1 +#define DP_C_LONG 2 +#define DP_C_LLONG 3 +#define DP_C_LDOUBLE 4 + +#define char_to_int(p) (p - '0') + +static int dopr (char *buffer, size_t maxlen, const char *format, va_list args) +{ + char ch; + LLONG value; + LDOUBLE fvalue; + char *strvalue; + int min; + int max; + int state; + int flags; + int cflags; + int total; + size_t currlen; + + state = DP_S_DEFAULT; + currlen = flags = cflags = min = 0; + max = -1; + ch = *format++; + total = 0; + + while (state != DP_S_DONE) + { + if (ch == '\0') + state = DP_S_DONE; + + switch(state) + { + case DP_S_DEFAULT: + if (ch == '%') + state = DP_S_FLAGS; + else + total += dopr_outch (buffer, &currlen, maxlen, ch); + ch = *format++; + break; + case DP_S_FLAGS: + switch (ch) + { + case '-': + flags |= DP_F_MINUS; + ch = *format++; + break; + case '+': + flags |= DP_F_PLUS; + ch = *format++; + break; + case ' ': + flags |= DP_F_SPACE; + ch = *format++; + break; + case '#': + flags |= DP_F_NUM; + ch = *format++; + break; + case '0': + flags |= DP_F_ZERO; + ch = *format++; + break; + default: + state = DP_S_MIN; + break; + } + break; + case DP_S_MIN: + if ('0' <= ch && ch <= '9') + { + min = 10*min + char_to_int (ch); + ch = *format++; + } + else if (ch == '*') + { + min = va_arg (args, int); + ch = *format++; + state = DP_S_DOT; + } + else + state = DP_S_DOT; + break; + case DP_S_DOT: + if (ch == '.') + { + state = DP_S_MAX; + ch = *format++; + } + else + state = DP_S_MOD; + break; + case DP_S_MAX: + if ('0' <= ch && ch <= '9') + { + if (max < 0) + max = 0; + max = 10*max + char_to_int (ch); + ch = *format++; + } + else if (ch == '*') + { + max = va_arg (args, int); + ch = *format++; + state = DP_S_MOD; + } + else + state = DP_S_MOD; + break; + case DP_S_MOD: + switch (ch) + { + case 'h': + cflags = DP_C_SHORT; + ch = *format++; + break; + case 'l': + cflags = DP_C_LONG; + ch = *format++; + break; + case 'L': + cflags = DP_C_LDOUBLE; + ch = *format++; + break; + default: + break; + } + if (cflags != DP_C_LONG) + state = DP_S_CONV; + else + state = DP_S_MOD_L; + break; + case DP_S_MOD_L: + switch (ch) + { + case 'l': + cflags = DP_C_LLONG; + ch = *format++; + break; + default: + break; + } + state = DP_S_CONV; + break; + case DP_S_CONV: + switch (ch) + { + case 'd': + case 'i': + if (cflags == DP_C_SHORT) + value = (short int)va_arg (args, int); + else if (cflags == DP_C_LONG) + value = va_arg (args, long int); + else if (cflags == DP_C_LLONG) + value = va_arg (args, LLONG); + else + value = va_arg (args, int); + total += fmtint (buffer, &currlen, maxlen, value, 10, min, max, flags); + break; + case 'o': + flags |= DP_F_UNSIGNED; + if (cflags == DP_C_SHORT) + // value = (unsigned short int) va_arg (args, unsigned short int); // Thilo: This does not work because the rcc compiler cannot do that cast correctly. + value = va_arg (args, unsigned int) & ( (1 << sizeof(unsigned short int) * 8) - 1); // Using this workaround instead. + else if (cflags == DP_C_LONG) + value = va_arg (args, unsigned long int); + else if (cflags == DP_C_LLONG) + value = va_arg (args, unsigned LLONG); + else + value = va_arg (args, unsigned int); + total += fmtint (buffer, &currlen, maxlen, value, 8, min, max, flags); + break; + case 'u': + flags |= DP_F_UNSIGNED; + if (cflags == DP_C_SHORT) + value = va_arg (args, unsigned int) & ( (1 << sizeof(unsigned short int) * 8) - 1); + else if (cflags == DP_C_LONG) + value = va_arg (args, unsigned long int); + else if (cflags == DP_C_LLONG) + value = va_arg (args, unsigned LLONG); + else + value = va_arg (args, unsigned int); + total += fmtint (buffer, &currlen, maxlen, value, 10, min, max, flags); + break; + case 'X': + flags |= DP_F_UP; + case 'x': + flags |= DP_F_UNSIGNED; + if (cflags == DP_C_SHORT) + value = va_arg (args, unsigned int) & ( (1 << sizeof(unsigned short int) * 8) - 1); + else if (cflags == DP_C_LONG) + value = va_arg (args, unsigned long int); + else if (cflags == DP_C_LLONG) + value = va_arg (args, unsigned LLONG); + else + value = va_arg (args, unsigned int); + total += fmtint (buffer, &currlen, maxlen, value, 16, min, max, flags); + break; + case 'f': + if (cflags == DP_C_LDOUBLE) + fvalue = va_arg (args, LDOUBLE); + else + fvalue = va_arg (args, double); + /* um, floating point? */ + total += fmtfp (buffer, &currlen, maxlen, fvalue, min, max, flags); + break; + case 'E': + flags |= DP_F_UP; + case 'e': + if (cflags == DP_C_LDOUBLE) + fvalue = va_arg (args, LDOUBLE); + else + fvalue = va_arg (args, double); + /* um, floating point? */ + total += fmtfp (buffer, &currlen, maxlen, fvalue, min, max, flags); + break; + case 'G': + flags |= DP_F_UP; + case 'g': + if (cflags == DP_C_LDOUBLE) + fvalue = va_arg (args, LDOUBLE); + else + fvalue = va_arg (args, double); + /* um, floating point? */ + total += fmtfp (buffer, &currlen, maxlen, fvalue, min, max, flags); + break; + case 'c': + total += dopr_outch (buffer, &currlen, maxlen, va_arg (args, int)); + break; + case 's': + strvalue = va_arg (args, char *); + total += fmtstr (buffer, &currlen, maxlen, strvalue, flags, min, max); + break; + case 'p': + strvalue = va_arg (args, void *); + total += fmtint (buffer, &currlen, maxlen, (long) strvalue, 16, min, + max, flags); + break; + case 'n': + if (cflags == DP_C_SHORT) + { + short int *num; + num = va_arg (args, short int *); + *num = currlen; + } + else if (cflags == DP_C_LONG) + { + long int *num; + num = va_arg (args, long int *); + *num = currlen; + } + else if (cflags == DP_C_LLONG) + { + LLONG *num; + num = va_arg (args, LLONG *); + *num = currlen; + } + else + { + int *num; + num = va_arg (args, int *); + *num = currlen; + } + break; + case '%': + total += dopr_outch (buffer, &currlen, maxlen, ch); + break; + case 'w': + /* not supported yet, treat as next char */ + ch = *format++; + break; + default: + /* Unknown, skip */ + break; + } + ch = *format++; + state = DP_S_DEFAULT; + flags = cflags = min = 0; + max = -1; + break; + case DP_S_DONE: + break; + default: + /* hmm? */ + break; /* some picky compilers need this */ + } + } + if (maxlen > 0) + buffer[currlen] = '\0'; + return total; +} + +static int fmtstr (char *buffer, size_t *currlen, size_t maxlen, + char *value, int flags, int min, int max) +{ + int padlen, strln; /* amount to pad */ + int cnt = 0; + int total = 0; + + if (value == 0) + { + value = "<NULL>"; + } + + for (strln = 0; value[strln]; ++strln); /* strlen */ + if (max >= 0 && max < strln) + strln = max; + padlen = min - strln; + if (padlen < 0) + padlen = 0; + if (flags & DP_F_MINUS) + padlen = -padlen; /* Left Justify */ + + while (padlen > 0) + { + total += dopr_outch (buffer, currlen, maxlen, ' '); + --padlen; + } + while (*value && ((max < 0) || (cnt < max))) + { + total += dopr_outch (buffer, currlen, maxlen, *value++); + ++cnt; + } + while (padlen < 0) + { + total += dopr_outch (buffer, currlen, maxlen, ' '); + ++padlen; + } + return total; +} + +/* Have to handle DP_F_NUM (ie 0x and 0 alternates) */ + +static int fmtint (char *buffer, size_t *currlen, size_t maxlen, + LLONG value, int base, int min, int max, int flags) +{ + int signvalue = 0; + unsigned LLONG uvalue; + char convert[24]; + int place = 0; + int spadlen = 0; /* amount to space pad */ + int zpadlen = 0; /* amount to zero pad */ + const char *digits; + int total = 0; + + if (max < 0) + max = 0; + + uvalue = value; + + if(!(flags & DP_F_UNSIGNED)) + { + if( value < 0 ) { + signvalue = '-'; + uvalue = -value; + } + else + if (flags & DP_F_PLUS) /* Do a sign (+/i) */ + signvalue = '+'; + else + if (flags & DP_F_SPACE) + signvalue = ' '; + } + + if (flags & DP_F_UP) + /* Should characters be upper case? */ + digits = "0123456789ABCDEF"; + else + digits = "0123456789abcdef"; + + do { + convert[place++] = digits[uvalue % (unsigned)base]; + uvalue = (uvalue / (unsigned)base ); + } while(uvalue && (place < sizeof (convert))); + if (place == sizeof (convert)) place--; + convert[place] = 0; + + zpadlen = max - place; + spadlen = min - MAX (max, place) - (signvalue ? 1 : 0); + if (zpadlen < 0) zpadlen = 0; + if (spadlen < 0) spadlen = 0; + if (flags & DP_F_ZERO) + { + zpadlen = MAX(zpadlen, spadlen); + spadlen = 0; + } + if (flags & DP_F_MINUS) + spadlen = -spadlen; /* Left Justifty */ + +#ifdef DEBUG_SNPRINTF + dprint (1, (debugfile, "zpad: %d, spad: %d, min: %d, max: %d, place: %d\n", + zpadlen, spadlen, min, max, place)); +#endif + + /* Spaces */ + while (spadlen > 0) + { + total += dopr_outch (buffer, currlen, maxlen, ' '); + --spadlen; + } + + /* Sign */ + if (signvalue) + total += dopr_outch (buffer, currlen, maxlen, signvalue); + + /* Zeros */ + if (zpadlen > 0) + { + while (zpadlen > 0) + { + total += dopr_outch (buffer, currlen, maxlen, '0'); + --zpadlen; + } + } + + /* Digits */ + while (place > 0) + total += dopr_outch (buffer, currlen, maxlen, convert[--place]); + + /* Left Justified spaces */ + while (spadlen < 0) { + total += dopr_outch (buffer, currlen, maxlen, ' '); + ++spadlen; + } + + return total; +} + +static LDOUBLE abs_val (LDOUBLE value) +{ + LDOUBLE result = value; + + if (value < 0) + result = -value; + + return result; +} + +static LDOUBLE pow10 (int exp) +{ + LDOUBLE result = 1; + + while (exp) + { + result *= 10; + exp--; + } + + return result; +} + +static long round (LDOUBLE value) +{ + long intpart; + + intpart = value; + value = value - intpart; + if (value >= 0.5) + intpart++; + + return intpart; +} + +static int fmtfp (char *buffer, size_t *currlen, size_t maxlen, + LDOUBLE fvalue, int min, int max, int flags) +{ + int signvalue = 0; + LDOUBLE ufvalue; + char iconvert[20]; + char fconvert[20]; + int iplace = 0; + int fplace = 0; + int padlen = 0; /* amount to pad */ + int zpadlen = 0; + int caps = 0; + int total = 0; + long intpart; + long fracpart; + + /* + * AIX manpage says the default is 0, but Solaris says the default + * is 6, and sprintf on AIX defaults to 6 + */ + if (max < 0) + max = 6; + + ufvalue = abs_val (fvalue); + + if (fvalue < 0) + signvalue = '-'; + else + if (flags & DP_F_PLUS) /* Do a sign (+/i) */ + signvalue = '+'; + else + if (flags & DP_F_SPACE) + signvalue = ' '; + +#if 0 + if (flags & DP_F_UP) caps = 1; /* Should characters be upper case? */ +#endif + + intpart = ufvalue; + + /* + * Sorry, we only support 9 digits past the decimal because of our + * conversion method + */ + if (max > 9) + max = 9; + + /* We "cheat" by converting the fractional part to integer by + * multiplying by a factor of 10 + */ + fracpart = round ((pow10 (max)) * (ufvalue - intpart)); + + if (fracpart >= pow10 (max)) + { + intpart++; + fracpart -= pow10 (max); + } + +#ifdef DEBUG_SNPRINTF + dprint (1, (debugfile, "fmtfp: %f =? %d.%d\n", fvalue, intpart, fracpart)); +#endif + + /* Convert integer part */ + do { + iconvert[iplace++] = + (caps? "0123456789ABCDEF":"0123456789abcdef")[intpart % 10]; + intpart = (intpart / 10); + } while(intpart && (iplace < 20)); + if (iplace == 20) iplace--; + iconvert[iplace] = 0; + + /* Convert fractional part */ + do { + fconvert[fplace++] = + (caps? "0123456789ABCDEF":"0123456789abcdef")[fracpart % 10]; + fracpart = (fracpart / 10); + } while(fracpart && (fplace < 20)); + if (fplace == 20) fplace--; + fconvert[fplace] = 0; + + /* -1 for decimal point, another -1 if we are printing a sign */ + padlen = min - iplace - max - 1 - ((signvalue) ? 1 : 0); + zpadlen = max - fplace; + if (zpadlen < 0) + zpadlen = 0; + if (padlen < 0) + padlen = 0; + if (flags & DP_F_MINUS) + padlen = -padlen; /* Left Justifty */ + + if ((flags & DP_F_ZERO) && (padlen > 0)) + { + if (signvalue) + { + total += dopr_outch (buffer, currlen, maxlen, signvalue); + --padlen; + signvalue = 0; + } + while (padlen > 0) + { + total += dopr_outch (buffer, currlen, maxlen, '0'); + --padlen; + } + } + while (padlen > 0) + { + total += dopr_outch (buffer, currlen, maxlen, ' '); + --padlen; + } + if (signvalue) + total += dopr_outch (buffer, currlen, maxlen, signvalue); + + while (iplace > 0) + total += dopr_outch (buffer, currlen, maxlen, iconvert[--iplace]); + + /* + * Decimal point. This should probably use locale to find the correct + * char to print out. + */ + if (max > 0) + { + total += dopr_outch (buffer, currlen, maxlen, '.'); + + while (zpadlen-- > 0) + total += dopr_outch (buffer, currlen, maxlen, '0'); + + while (fplace > 0) + total += dopr_outch (buffer, currlen, maxlen, fconvert[--fplace]); + } + + while (padlen < 0) + { + total += dopr_outch (buffer, currlen, maxlen, ' '); + ++padlen; + } + + return total; +} + +static int dopr_outch (char *buffer, size_t *currlen, size_t maxlen, char c) +{ + if (*currlen + 1 < maxlen) + buffer[(*currlen)++] = c; + return 1; +} + +int Q_vsnprintf(char *str, size_t length, const char *fmt, va_list args) +{ + return dopr(str, length, fmt, args); +} + +int Q_snprintf(char *str, size_t length, const char *fmt, ...) +{ + va_list ap; + int retval; + + va_start(ap, fmt); + retval = Q_vsnprintf(str, length, fmt, ap); + va_end(ap); + + return retval; +} + +/* this is really crappy */ +int sscanf( const char *buffer, const char *fmt, ... ) +{ + int cmd; + va_list ap; + int count; + size_t len; + + va_start( ap, fmt ); + count = 0; + + while( *fmt ) + { + if( fmt[ 0 ] != '%' ) + { + fmt++; + continue; + } + + fmt++; + cmd = *fmt; + + if( isdigit( cmd ) ) + { + len = (size_t)_atoi( &fmt ); + cmd = *( fmt - 1 ); + } + else + { + len = MAX_STRING_CHARS - 1; + fmt++; + } + + switch( cmd ) + { + case 'i': + case 'd': + case 'u': + *( va_arg( ap, int * ) ) = _atoi( &buffer ); + break; + case 'f': + *( va_arg( ap, float * ) ) = _atof( &buffer ); + break; + case 'x': + *( va_arg( ap, unsigned int * ) ) = _hextoi( &buffer ); + break; + case 's': + { + char *s = va_arg( ap, char * ); + while( isspace( *buffer ) ) + buffer++; + while( *buffer && !isspace( *buffer) && len-- > 0 ) + *s++ = *buffer++; + *s++ = '\0'; + break; + } + } + } + + va_end( ap ); + return count; +} + +void *bsearch( const void *key, const void *base, size_t nmemb, size_t size, + cmp_t *compar ) +{ + size_t low = 0, high = nmemb, mid; + int comp; + void *ptr; + + while( low < high ) + { + mid = low + (high - low) / 2; + ptr = (void *)((char *)base + ( mid * size )); + comp = compar (key, ptr); + if( comp < 0 ) + high = mid; + else if( comp > 0 ) + low = mid + 1; + else + return ptr; + } + return NULL; +} + +#endif diff --git a/src/game/bg_lib.h b/src/game/bg_lib.h new file mode 100644 index 0000000..3eb6e19 --- /dev/null +++ b/src/game/bg_lib.h @@ -0,0 +1,137 @@ +/* +=========================================================================== +Copyright (C) 1999-2005 Id Software, Inc. +Copyright (C) 2000-2009 Darklegion Development + +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 +#if !defined( BG_LIB_H ) && defined( Q3_VM ) +#define BG_LIB_H + +#ifndef NULL +#define NULL ((void *)0) +#endif + +typedef unsigned 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_MAX 0x7f /* maximum signed char value */ +#define SCHAR_MIN (-SCHAR_MAX - 1)/* minimum signed char value */ +#define UCHAR_MAX 0xff /* maximum unsigned char value */ + +#define SHRT_MAX 0x7fff /* maximum (signed) short value */ +#define SHRT_MIN (-SHRT_MAX - 1) /* minimum (signed) short value */ +#define USHRT_MAX 0xffff /* maximum unsigned short value */ +#define INT_MAX 0x7fffffff /* maximum (signed) int value */ +#define INT_MIN (-INT_MAX - 1) /* minimum (signed) int value */ +#define UINT_MAX 0xffffffff /* maximum unsigned int value */ +#define LONG_MAX 0x7fffffffL /* maximum (signed) long value */ +#define LONG_MIN (-LONG_MAX - 1) /* minimum (signed) long value */ +#define ULONG_MAX 0xffffffffUL /* maximum unsigned long value */ + +typedef signed char int8_t; +typedef unsigned char uint8_t; +typedef signed short int16_t; +typedef unsigned short uint16_t; +typedef signed long int32_t; +typedef unsigned long uint32_t; + +#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 +#define assert( expr )\ + if( !( expr ) )\ + Com_Error( ERR_DROP, "%s:%d: Assertion `%s' failed",\ + __FILE__, __LINE__, #expr ) +typedef int cmp_t( const void *, const void * ); +void qsort( void *a, size_t n, size_t es, cmp_t *cmp ); +#define RAND_MAX 0x7fff +void srand( unsigned seed ); +int rand( void ); +void *bsearch( const void *key, const void *base, size_t nmemb, + size_t size, cmp_t *compar ); + +// 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 ); +double strtod( const char *nptr, char **endptr ); +int atoi( const char *string ); +int _atoi( const char **stringPtr ); +long strtol( const char *nptr, char **endptr, int base ); + +int Q_vsnprintf( char *buffer, size_t length, const char *fmt, va_list argptr ); +int Q_snprintf( char *buffer, size_t length, const char *fmt, ... ) __attribute__ ((format (printf, 3, 4))); + +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..6a3fd72 --- /dev/null +++ b/src/game/bg_local.h @@ -0,0 +1,88 @@ +/* +=========================================================================== +Copyright (C) 1999-2005 Id Software, Inc. +Copyright (C) 2000-2009 Darklegion Development + +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..5940a57 --- /dev/null +++ b/src/game/bg_misc.c @@ -0,0 +1,4491 @@ +/* +=========================================================================== +Copyright (C) 1999-2005 Id Software, Inc. +Copyright (C) 2000-2009 Darklegion Development + +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 +int trap_FS_GetFileList( const char *path, const char *extension, char *listbuf, int bufsize ); + +static const buildableAttributes_t bg_buildableList[ ] = +{ + { + BA_A_SPAWN, //int buildNum; + "eggpod", //char *buildName; + "Egg", //char *humanName; + "The most basic alien structure. It allows aliens to spawn " + "and protect the Overmind. Without any of these, the Overmind " + "is left nearly defenseless and defeat is imminent.", + "team_alien_spawn", //char *entityName; + 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; + TEAM_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 uniqueTest; + ASPAWN_VALUE, //int value; + qfalse //qboolean cuboid; + }, + { + BA_A_OVERMIND, //int buildNum; + "overmind", //char *buildName; + "Overmind", //char *humanName; + "A collective consciousness that controls all the alien structures " + "in its vicinity. It must be protected at all costs, since its " + "death will render alien structures defenseless.", + "team_alien_overmind", //char *entityName; + 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; + TEAM_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 uniqueTest; + OVERMIND_VALUE, //int value; + qfalse //qboolean cuboid; + }, + { + BA_A_BARRICADE, //int buildNum; + "barricade", //char *buildName; + "Barricade", //char *humanName; + "Used to obstruct corridors and doorways, hindering humans from " + "threatening the spawns and Overmind. Barricades will shrink " + "to allow aliens to pass over them, however.", + "team_alien_barricade",//char *entityName; + 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; + TEAM_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 uniqueTest; + BARRICADE_VALUE, //int value; + qfalse //qboolean cuboid; + }, + { + BA_A_ACIDTUBE, //int buildNum; + "acid_tube", //char *buildName; + "Acid Tube", //char *humanName; + "Ejects lethal poisonous acid at an approaching human. These " + "are highly effective when used in conjunction with a trapper " + "to hold the victim in place.", + "team_alien_acid_tube",//char *entityName; + 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_ASPAWN, //int meansOfDeath; + TEAM_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 uniqueTest; + ACIDTUBE_VALUE, //int value; + qfalse //qboolean cuboid; + }, + { + BA_A_TRAPPER, //int buildNum; + "trapper", //char *buildName; + "Trapper", //char *humanName; + "Fires a blob of adhesive spit at any non-alien in its line of " + "sight. This hinders their movement, making them an easy target " + "for other defensive structures or aliens.", + "team_alien_trapper", //char *entityName; + 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; + TEAM_ALIENS, //int team; + ( 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 uniqueTest; + TRAPPER_VALUE, //int value; + qfalse //qboolean cuboid; + }, + { + BA_A_BOOSTER, //int buildNum; + "booster", //char *buildName; + "Booster", //char *humanName; + "Laces the attacks of any alien that touches it with a poison " + "that will gradually deal damage to any humans exposed to it. " + "The booster also increases the rate of health regeneration for " + "any nearby aliens.", + "team_alien_booster", //char *entityName; + 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; + TEAM_ALIENS, //int team; + ( 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 uniqueTest; + BOOSTER_VALUE, //int value; + qfalse //qboolean cuboid; + }, + { + BA_A_HIVE, //int buildNum; + "hive", //char *buildName; + "Hive", //char *humanName; + "Houses millions of tiny insectoid aliens. When a human " + "approaches this structure, the insectoids attack.", + "team_alien_hive", //char *entityName; + 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; + TEAM_ALIENS, //int team; + ( 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 uniqueTest; + HIVE_VALUE, //int value; + qfalse //qboolean cuboid; + }, + { + BA_H_SPAWN, //int buildNum; + "telenode", //char *buildName; + "Telenode", //char *humanName; + "The most basic human structure. It provides a means for humans " + "to enter the battle arena. Without any of these the humans " + "cannot spawn and defeat is imminent.", + "team_human_spawn", //char *entityName; + 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; + TEAM_HUMANS, //int team; + ( 1 << WP_HBUILD ), //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 uniqueTest; + HSPAWN_VALUE, //int value; + qfalse //qboolean cuboid; + }, + { + BA_H_MGTURRET, //int buildNum; + "mgturret", //char *buildName; + "Machinegun Turret", //char *humanName; + "Automated base defense that is effective against large targets " + "but slow to begin firing. Should always be " + "backed up by physical support.", + "team_human_mgturret", //char *entityName; + 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; + TEAM_HUMANS, //int team; + ( 1 << WP_HBUILD ), //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 uniqueTest; + MGTURRET_VALUE, //int value; + qfalse //qboolean cuboid; + }, + { + BA_H_TESLAGEN, //int buildNum; + "tesla", //char *buildName; + "Tesla Generator", //char *humanName; + "A structure equipped with a strong electrical attack that fires " + "instantly and always hits its target. It is effective against smaller " + "aliens and for consolidating basic defense.", + "team_human_tesla", //char *entityName; + 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; + TEAM_HUMANS, //int team; + ( 1 << WP_HBUILD ), //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; + qfalse, //qboolean dccTest; + qtrue, //qboolean transparentTest; + qfalse, //qboolean uniqueTest; + TESLAGEN_VALUE, //int value; + qfalse //qboolean cuboid; + }, + { + BA_H_ARMOURY, //int buildNum; + "arm", //char *buildName; + "Armoury", //char *humanName; + "An essential part of the human base, providing a means " + "to upgrade the basic human equipment. A range of upgrades " + "and weapons are available for sale from the armoury.", + "team_human_armoury", //char *entityName; + 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; + TEAM_HUMANS, //int team; + ( 1 << WP_HBUILD ), //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 uniqueTest; + ARMOURY_VALUE, //int value; + qfalse //qboolean cuboid; + }, + { + BA_H_DCC, //int buildNum; + "dcc", //char *buildName; + "Defence Computer", //char *humanName; + "A structure that enables self-repair functionality in " + "human structures. Each Defence Computer built increases " + "repair rate slightly.", + "team_human_dcc", //char *entityName; + 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; + TEAM_HUMANS, //int team; + ( 1 << WP_HBUILD ), //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 uniqueTest; + DC_VALUE, //int value; + qfalse //qboolean cuboid; + }, + { + BA_H_MEDISTAT, //int buildNum; + "medistat", //char *buildName; + "Medistation", //char *humanName; + "A structure that automatically restores " + "the health and stamina of any human that stands on it. " + "It may only be used by one person at a time. This structure " + "also issues medkits.", + "team_human_medistat", //char *entityName; + 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; + TEAM_HUMANS, //int team; + ( 1 << WP_HBUILD ), //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 uniqueTest; + MEDISTAT_VALUE, //int value; + qfalse //qboolean cuboid; + }, + { + BA_H_REACTOR, //int buildNum; + "reactor", //char *buildName; + "Reactor", //char *humanName; + "All structures except the telenode rely on a reactor to operate. " + "The reactor provides power for all the human structures either " + "directly or via repeaters. Only one reactor can be built at a time.", + "team_human_reactor", //char *entityName; + 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; + TEAM_HUMANS, //int team; + ( 1 << WP_HBUILD ), //weapon_t buildWeapon; + BANIM_IDLE1, //int idleAnim; + REACTOR_ATTACK_DCC_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 uniqueTest; + REACTOR_VALUE, //int value; + qfalse //qboolean cuboid; + }, + { + BA_H_REPEATER, //int buildNum; + "repeater", //char *buildName; + "Repeater", //char *humanName; + "A power distributor that transmits power from the reactor " + "to remote locations, so that bases may be built far " + "from the reactor.", + "team_human_repeater", //char *entityName; + TR_GRAVITY, //trType_t traj; + 0.0, //float bounce; + REPEATER_BP, //int buildPoints; + ( 1 << S1 )|( 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; + TEAM_HUMANS, //int team; + ( 1 << WP_HBUILD ), //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 uniqueTest; + REPEATER_VALUE, //int value; + qfalse //qboolean cuboid; + }, + { + BA_H_CUBOID1, //int buildNum; + "concrete", //char *buildName; + "Concrete", //char *humanName; + "A strong and solid cuboid used either as a " + "protection from alien units or as a bearer for " + "greater structures.", + "team_human_hcuboid1", //char *entityName; + TR_GRAVITY, //trType_t traj; + 0.0, //float bounce; + 0, //int buildPoints; + ( 1 << S1 )|( 1 << S2 )|( 1 << S3 ), //int stages + 0, //int health; + 0, //int regenRate; + 0, //int splashDamage; + 0, //int splashRadius; + MOD_HSPAWN, //int meansOfDeath; + TEAM_HUMANS, //int team; + ( 1 << WP_HBUILD ), //weapon_t buildWeapon; + BANIM_IDLE1, //int idleAnim; + 100, //int nextthink; + 0, //int buildTime; + qfalse, //qboolean usable; + 0, //int turretRange; + 0, //int turretFireSpeed; + WP_NONE, //weapon_t turretProjType; + 1.00f, //float minNormal; + qfalse, //qboolean invertNormal; + qfalse, //qboolean creepTest; + 0, //int creepSize; + qfalse, //qboolean dccTest; + qfalse, //qboolean transparentTest; + qfalse, //qboolean uniqueTest; + 0, //int value; + qtrue //qboolean cuboid; + }, + { + BA_H_CUBOID2, //int buildNum; + "glass", //char *buildName; + "Glass", //char *humanName; + "A cuboid made of a transparent chemical compound. " + "Its durability is low compared to other glass-like " + "materials. However it has shown increased strenght " + "when formed into a thin pane shape.", + "team_human_hcuboid2", //char *entityName; + TR_GRAVITY, //trType_t traj; + 0.0, //float bounce; + 0, //int buildPoints; + ( 1 << S1 )|( 1 << S2 )|( 1 << S3 ), //int stages + 0, //int health; + 0, //int regenRate; + 0, //int splashDamage; + 0, //int splashRadius; + MOD_HSPAWN, //int meansOfDeath; + TEAM_HUMANS, //int team; + ( 1 << WP_HBUILD ), //weapon_t buildWeapon; + BANIM_IDLE1, //int idleAnim; + 100, //int nextthink; + 0, //int buildTime; + qfalse, //qboolean usable; + 0, //int turretRange; + 0, //int turretFireSpeed; + WP_NONE, //weapon_t turretProjType; + 1.00f, //float minNormal; + qfalse, //qboolean invertNormal; + qfalse, //qboolean creepTest; + 0, //int creepSize; + qfalse, //qboolean dccTest; + qfalse, //qboolean transparentTest; + qfalse, //qboolean uniqueTest; + 0, //int value; + qtrue //qboolean cuboid; + }, + { + BA_H_CUBOID3, //int buildNum; + "ladder", //char *buildName; + "Ladder", //char *humanName; + "A cuboid made of a durable metal alloy. " + "Purpose-built hollows allow humans to easily " + "climb on its surface thus making it a good " + "substitute for unreliable standard ladders.", + "team_human_hcuboid3", //char *entityName; + TR_GRAVITY, //trType_t traj; + 0.0, //float bounce; + 0, //int buildPoints; + ( 1 << S1 )|( 1 << S2 )|( 1 << S3 ), //int stages + 0, //int health; + 0, //int regenRate; + 0, //int splashDamage; + 0, //int splashRadius; + MOD_HSPAWN, //int meansOfDeath; + TEAM_HUMANS, //int team; + ( 1 << WP_HBUILD ), //weapon_t buildWeapon; + BANIM_IDLE1, //int idleAnim; + 100, //int nextthink; + 0, //int buildTime; + qfalse, //qboolean usable; + 0, //int turretRange; + 0, //int turretFireSpeed; + WP_NONE, //weapon_t turretProjType; + 1.00f, //float minNormal; + qfalse, //qboolean invertNormal; + qfalse, //qboolean creepTest; + 0, //int creepSize; + qfalse, //qboolean dccTest; + qfalse, //qboolean transparentTest; + qfalse, //qboolean uniqueTest; + 0, //int value; + qtrue //qboolean cuboid; + }, + { + BA_A_CUBOID1, //int buildNum; + "cells", //char *buildName; + "Cells", //char *humanName; + "A solid wall made out of alien cells. " + "Used primarly for protecting bases " + "and outposts.", + "team_alien_acuboid1", //char *entityName; + TR_GRAVITY, //trType_t traj; + 0.0, //float bounce; + 0, //int buildPoints; + ( 1 << S1 )|( 1 << S2 )|( 1 << S3 ), //int stages + 0, //int health; + 0, //int regenRate; + 0, //int splashDamage; + 0, //int splashRadius; + MOD_ASPAWN, //int meansOfDeath; + TEAM_ALIENS, //int team; + ( 1 << WP_ABUILD )|( 1 << WP_ABUILD2 ), //weapon_t buildWeapon; + BANIM_IDLE1, //int idleAnim; + 100, //int nextthink; + 0, //int buildTime; + qfalse, //qboolean usable; + 0, //int turretRange; + 0, //int turretFireSpeed; + WP_NONE, //weapon_t turretProjType; + 0.00f, //float minNormal; + qfalse, //qboolean invertNormal; + qfalse, //qboolean creepTest; + 0, //int creepSize; + qfalse, //qboolean dccTest; + qfalse, //qboolean transparentTest; + qfalse, //qboolean uniqueTest; + 0, //int value; + qtrue //qboolean cuboid; + }, + { + BA_A_CUBOID2, //int buildNum; + "slime", //char *buildName; + "Slime", //char *humanName; + "A rigid chunk of the finest quality " + "mixture of various types of acids and " + "slippery chemical compunds. Dangerous " + "to human units but completely safe for " + "aliens.", + "team_alien_acuboid2", //char *entityName; + TR_GRAVITY, //trType_t traj; + 0.0, //float bounce; + 0, //int buildPoints; + ( 1 << S1 )|( 1 << S2 )|( 1 << S3 ), //int stages + 0, //int health; + 0, //int regenRate; + 0, //int splashDamage; + 0, //int splashRadius; + MOD_ASPAWN, //int meansOfDeath; + TEAM_ALIENS, //int team; + ( 1 << WP_ABUILD )|( 1 << WP_ABUILD2 ), //weapon_t buildWeapon; + BANIM_IDLE1, //int idleAnim; + 100, //int nextthink; + 0, //int buildTime; + qfalse, //qboolean usable; + 0, //int turretRange; + 0, //int turretFireSpeed; + WP_NONE, //weapon_t turretProjType; + 0.00f, //float minNormal; + qfalse, //qboolean invertNormal; + qfalse, //qboolean creepTest; + 0, //int creepSize; + qfalse, //qboolean dccTest; + qfalse, //qboolean transparentTest; + qfalse, //qboolean uniqueTest; + 0, //int value; + qtrue //qboolean cuboid; + } +}; + +int bg_numBuildables = sizeof( bg_buildableList ) / sizeof( bg_buildableList[ 0 ] ); + +static const buildableAttributes_t nullBuildable = { 0 }; + +/* +============== +BG_BuildableByName +============== +*/ +const buildableAttributes_t *BG_BuildableByName( const char *name ) +{ + int i; + + for( i = 0; i < bg_numBuildables; i++ ) + { + if( !Q_stricmp( bg_buildableList[ i ].name, name ) ) + return &bg_buildableList[ i ]; + } + + return &nullBuildable; +} + +/* +============== +BG_BuildableByEntityName +============== +*/ +const buildableAttributes_t *BG_BuildableByEntityName( const char *name ) +{ + int i; + + for( i = 0; i < bg_numBuildables; i++ ) + { + if( !Q_stricmp( bg_buildableList[ i ].entityName, name ) ) + return &bg_buildableList[ i ]; + } + + return &nullBuildable; +} + +/* +============== +BG_Buildable +============== +*/ + +const buildableAttributes_t *BG_Buildable( buildable_t buildable, const vec3_t cuboidSize ) +{ + const buildableAttributes_t *attr; + static buildableAttributes_t cuboidAttr; + const cuboidAttributes_t *cuboid; + float volume; + vec3_t sorted; + + if(buildable>BA_NONE&&buildable<BA_NUM_BUILDABLES) + attr=&bg_buildableList[buildable-1]; + else + return &nullBuildable; + + if(attr->cuboid) + { + volume=cuboidSize[0]*cuboidSize[1]*cuboidSize[2]*5.5306341e-5; // NOTE: cubic quake units -> cubic meters ( 1qu = 0.0381m ) + memcpy(&cuboidAttr,attr,sizeof(buildableAttributes_t)); + cuboid=BG_CuboidAttributes(buildable); + + switch(cuboid->hpt) + { + case CBHPT_PLAIN: + cuboidAttr.health=ceil(volume*cuboid->hppv); + break; + case CBHPT_CUBES: + BG_CuboidSortSize(cuboidSize,sorted); + cuboidAttr.health=sqrt(sorted[2]*sorted[0])*sqrt(sorted[1]*sorted[0])*(float)sorted[0]*5.5306341e-5*cuboid->hppv; + break; + case CBHPT_PANES: + BG_CuboidSortSize(cuboidSize,sorted); + cuboidAttr.health=(float)sorted[2]*(float)sorted[1]*((float)sorted[0]+(float)sorted[2])/2.0f*5.5306341e-5*cuboid->hppv; + break; + } + + if(cuboidAttr.health<1) + cuboidAttr.health=1; + cuboidAttr.splashDamage=0; + cuboidAttr.splashRadius=0; + if(cuboid->regen) + cuboidAttr.regenRate=cuboid->regenrate; + cuboidAttr.buildTime=MAX((float)cuboidAttr.health/(float)cuboid->buildrate*1e3,(float)cuboid->minbt); + cuboidAttr.buildPoints=MAX(ceil((float)cuboidAttr.health/cuboid->hppv*cuboid->bppv),1); + return &cuboidAttr; + } + return attr; +} + +/* +============== +BG_BuildableAllowedInStage +============== +*/ +qboolean BG_BuildableAllowedInStage( buildable_t buildable, + stage_t stage ) +{ + int stages = BG_Buildable( buildable, NULL )->stages; + + if( stages & ( 1 << stage ) ) + return qtrue; + else + return qfalse; +} + +static buildableConfig_t bg_buildableConfigList[ BA_NUM_BUILDABLES ]; + +/* +============== +BG_BuildableConfig +============== +*/ +buildableConfig_t *BG_BuildableConfig( buildable_t buildable ) +{ + return &bg_buildableConfigList[ buildable ]; +} + +/* +============== +BG_BuildableBoundingBox +============== +*/ +void BG_BuildableBoundingBox( buildable_t buildable, + vec3_t mins, vec3_t maxs ) +{ + buildableConfig_t *buildableConfig = BG_BuildableConfig( buildable ); + + if( mins != NULL ) + VectorCopy( buildableConfig->mins, mins ); + + if( maxs != NULL ) + VectorCopy( buildableConfig->maxs, maxs ); +} + +/* +====================== +BG_ParseBuildableFile + +Parses a configuration file describing a buildable +====================== +*/ +static qboolean BG_ParseBuildableFile( const char *filename, buildableConfig_t *bc ) +{ + char *text_p; + int i; + int len; + char *token; + char text[ 20000 ]; + fileHandle_t f; + float scale; + int defined = 0; + enum + { + MODEL = 1 << 0, + MODELSCALE = 1 << 1, + MINS = 1 << 2, + MAXS = 1 << 3, + ZOFFSET = 1 << 4 + }; + + + // load the file + len = trap_FS_FOpenFile( filename, &f, FS_READ ); + if( len < 0 ) + { + Com_Printf( S_COLOR_RED "ERROR: Buildable file %s doesn't exist\n", filename ); + 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( bc->models[ index ], token, sizeof( bc->models[ 0 ] ) ); + + defined |= MODEL; + 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; + + bc->modelScale = scale; + + defined |= MODELSCALE; + continue; + } + else if( !Q_stricmp( token, "mins" ) ) + { + for( i = 0; i <= 2; i++ ) + { + token = COM_Parse( &text_p ); + if( !token ) + break; + + bc->mins[ i ] = atof( token ); + } + + defined |= MINS; + continue; + } + else if( !Q_stricmp( token, "maxs" ) ) + { + for( i = 0; i <= 2; i++ ) + { + token = COM_Parse( &text_p ); + if( !token ) + break; + + bc->maxs[ i ] = atof( token ); + } + + defined |= MAXS; + continue; + } + else if( !Q_stricmp( token, "zOffset" ) ) + { + float offset; + + token = COM_Parse( &text_p ); + if( !token ) + break; + + offset = atof( token ); + + bc->zOffset = offset; + + defined |= ZOFFSET; + continue; + } + + + Com_Printf( S_COLOR_RED "ERROR: unknown token '%s'\n", token ); + return qfalse; + } + + if( !( defined & MODEL ) ) token = "model"; + else if( !( defined & MODELSCALE ) ) token = "modelScale"; + else if( !( defined & MINS ) ) token = "mins"; + else if( !( defined & MAXS ) ) token = "maxs"; + else if( !( defined & ZOFFSET ) ) token = "zOffset"; + else token = ""; + + if( strlen( token ) > 0 ) + { + Com_Printf( S_COLOR_RED "ERROR: %s not defined in %s\n", + token, filename ); + return qfalse; + } + + return qtrue; +} + +/* +=============== +BG_InitBuildableConfigs +=============== +*/ +void BG_InitBuildableConfigs( void ) +{ + int i; + buildableConfig_t *bc; + + for( i = BA_NONE + 1; i < BA_NUM_BUILDABLES; i++ ) + { + bc = BG_BuildableConfig( i ); + Com_Memset( bc, 0, sizeof( buildableConfig_t ) ); + + if(BG_Buildable(i,NULL)->cuboid) + continue; + + BG_ParseBuildableFile( va( "configs/buildables/%s.cfg", + BG_Buildable( i, NULL )->name ), bc ); + } +} + +//////////////////////////////////////////////////////////////////////////////// + +static const classAttributes_t bg_classList[ ] = +{ + { + PCL_NONE, //int classnum; + "spectator", //char *className; + "", + ( 1 << S1 )|( 1 << S2 )|( 1 << S3 ), //int stages + 0, //int health; + 0.0f, //float fallDamage; + 0.0f, //float 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; + "Responsible for building and maintaining all the alien structures. " + "Has a weak melee slash attack.", + ( 1 << S1 )|( 1 << S2 )|( 1 << S3 ), //int stages + ABUILDER_HEALTH, //int health; + 0.2f, //float fallDamage; + ABUILDER_REGEN, //float regenRate; + SCA_TAKESFALLDAMAGE|SCA_FOVWARPS|SCA_ALIENSENSE,//int abilities; + WP_ABUILD, //weapon_t startWeapon + 95.0f, //float buildDist; + 110, //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; + "Similar to the base Granger, except that in addition to " + "being able to build structures it has a spit attack " + "that slows victims and the ability to crawl on walls.", + ( 1 << S2 )|( 1 << S3 ), //int stages + ABUILDER_UPG_HEALTH, //int health; + 0.2f, //float fallDamage; + ABUILDER_UPG_REGEN, //float regenRate; + SCA_TAKESFALLDAMAGE|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; + "Has a lethal reflexive bite and the ability to crawl on " + "walls and ceilings.", + ( 1 << S1 )|( 1 << S2 )|( 1 << S3 ), //int stages + LEVEL0_HEALTH, //int health; + 0.0f, //float fallDamage; + LEVEL0_REGEN, //float regenRate; + SCA_WALLCLIMBER|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; + "A support class able to crawl on walls and ceilings. Its melee " + "attack is most effective when combined with the ability to grab " + "and hold its victims in place. Provides a weak healing aura " + "that accelerates the healing rate of nearby aliens.", + ( 1 << S1 )|( 1 << S2 )|( 1 << S3 ), //int stages + LEVEL1_HEALTH, //int health; + 0.0f, //float fallDamage; + LEVEL1_REGEN, //float regenRate; + 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; + 310.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; + "In addition to the basic Basilisk abilities, the Advanced " + "Basilisk sprays a poisonous gas which disorients any " + "nearby humans. Has a strong healing aura that " + "that accelerates the healing rate of nearby aliens.", + ( 1 << S2 )|( 1 << S3 ), //int stages + LEVEL1_UPG_HEALTH, //int health; + 0.0f, //float fallDamage; + LEVEL1_UPG_REGEN, //float regenRate; + 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; + 310.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; + "Has a melee attack and the ability to jump off walls. This " + "allows the Marauder to gather great speed in enclosed areas.", + ( 1 << S1 )|( 1 << S2 )|( 1 << S3 ), //int stages + LEVEL2_HEALTH, //int health; + 0.0f, //float fallDamage; + LEVEL2_REGEN, //float regenRate; + 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; + 3.0f, //float airAcceleration; + 6.0f, //float friction; + 100.0f, //float stopSpeed; + 380.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; + "The Advanced Marauder has all the abilities of the basic Marauder " + "with the addition of an area effect electric shock attack.", + ( 1 << S2 )|( 1 << S3 ), //int stages + LEVEL2_UPG_HEALTH, //int health; + 0.0f, //float fallDamage; + LEVEL2_UPG_REGEN, //float regenRate; + 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; + 3.0f, //float airAcceleration; + 6.0f, //float friction; + 100.0f, //float stopSpeed; + 380.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; + "Possesses a melee attack and the pounce ability, which may " + "be used as both an attack and a means to reach remote " + "locations inaccessible from the ground.", + ( 1 << S1 )|( 1 << S2 )|( 1 << S3 ), //int stages + LEVEL3_HEALTH, //int health; + 0.0f, //float fallDamage; + LEVEL3_REGEN, //float regenRate; + 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; + "In addition to the basic Dragoon abilities, the Advanced " + "Dragoon has 3 barbs which may be used to attack humans " + "from a distance.", + ( 1 << S2 )|( 1 << S3 ), //int stages + LEVEL3_UPG_HEALTH, //int health; + 0.0f, //float fallDamage; + LEVEL3_UPG_REGEN, //float regenRate; + 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; + "A large alien with a strong melee attack, this class can " + "also charge at enemy humans and structures, inflicting " + "great damage. Any humans or their structures caught under " + "a falling Tyrant will be crushed by its weight.", + ( 1 << S3 ), //int stages + LEVEL4_HEALTH, //int health; + 0.0f, //float fallDamage; + LEVEL4_REGEN, //float regenRate; + 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; + "", + ( 1 << S1 )|( 1 << S2 )|( 1 << S3 ), //int stages + 100, //int health; + 1.0f, //float fallDamage; + 0.0f, //float 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; + ALIEN_CREDITS_PER_KILL //int value; + }, + { + PCL_HUMAN_BSUIT, //int classnum; + "human_bsuit", //char *classname; + "", + ( 1 << S3 ), //int stages + 100, //int health; + 1.0f, //float fallDamage; + 0.0f, //float 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; + ALIEN_CREDITS_PER_KILL //int value; + } +}; + +int bg_numClasses = sizeof( bg_classList ) / sizeof( bg_classList[ 0 ] ); + +static const classAttributes_t nullClass = { 0 }; + +/* +============== +BG_ClassByName +============== +*/ +const classAttributes_t *BG_ClassByName( const char *name ) +{ + int i; + + for( i = 0; i < bg_numClasses; i++ ) + { + if( !Q_stricmp( bg_classList[ i ].name, name ) ) + return &bg_classList[ i ]; + } + + return &nullClass; +} + +/* +============== +BG_Class +============== +*/ +const classAttributes_t *BG_Class( class_t class ) +{ + return ( class >= PCL_NONE && class < PCL_NUM_CLASSES ) ? + &bg_classList[ class ] : &nullClass; +} + +/* +============== +BG_ClassAllowedInStage +============== +*/ +qboolean BG_ClassAllowedInStage( class_t class, + stage_t stage ) +{ + int stages = BG_Class( class )->stages; + + return stages & ( 1 << stage ); +} + +static classConfig_t bg_classConfigList[ PCL_NUM_CLASSES ]; + +/* +============== +BG_ClassConfig +============== +*/ +classConfig_t *BG_ClassConfig( class_t class ) +{ + return &bg_classConfigList[ class ]; +} + +/* +============== +BG_ClassBoundingBox +============== +*/ +void BG_ClassBoundingBox( class_t class, + vec3_t mins, vec3_t maxs, + vec3_t cmaxs, vec3_t dmins, vec3_t dmaxs ) +{ + classConfig_t *classConfig = BG_ClassConfig( class ); + + if( mins != NULL ) + VectorCopy( classConfig->mins, mins ); + + if( maxs != NULL ) + VectorCopy( classConfig->maxs, maxs ); + + if( cmaxs != NULL ) + VectorCopy( classConfig->crouchMaxs, cmaxs ); + + if( dmins != NULL ) + VectorCopy( classConfig->deadMins, dmins ); + + if( dmaxs != NULL ) + VectorCopy( classConfig->deadMaxs, dmaxs ); +} + +/* +============== +BG_ClassHasAbility +============== +*/ +qboolean BG_ClassHasAbility( class_t class, int ability ) +{ + int abilities = BG_Class( class )->abilities; + + return abilities & ability; +} + +/* +============== +BG_ClassCanEvolveFromTo +============== +*/ +int BG_ClassCanEvolveFromTo( class_t fclass, + class_t tclass, + int credits, int stage, + int cost ) +{ + int i, j, best, value; + + if( credits < cost || fclass == PCL_NONE || tclass == PCL_NONE || + fclass == tclass ) + return -1; + + for( i = 0; i < bg_numClasses; i++ ) + { + if( bg_classList[ i ].number != fclass ) + continue; + + best = credits + 1; + for( j = 0; j < 3; j++ ) + { + int thruClass, evolveCost; + + thruClass = bg_classList[ i ].children[ j ]; + if( thruClass == PCL_NONE || !BG_ClassAllowedInStage( thruClass, stage ) || + !BG_ClassIsAllowed( thruClass ) ) + continue; + + evolveCost = BG_Class( thruClass )->cost * ALIEN_CREDITS_PER_KILL; + if( thruClass == tclass ) + value = cost + evolveCost; + else + value = BG_ClassCanEvolveFromTo( thruClass, tclass, credits, stage, + cost + evolveCost ); + + if( value >= 0 && value < best ) + best = value; + } + + return best <= credits ? best : -1; + } + + Com_Printf( S_COLOR_YELLOW "WARNING: fallthrough in BG_ClassCanEvolveFromTo\n" ); + return -1; +} + +/* +============== +BG_AlienCanEvolve +============== +*/ +qboolean BG_AlienCanEvolve( class_t class, int credits, int stage ) +{ + int i, j, tclass; + + for( i = 0; i < bg_numClasses; i++ ) + { + if( bg_classList[ i ].number != class ) + continue; + + for( j = 0; j < 3; j++ ) + { + tclass = bg_classList[ i ].children[ j ]; + if( tclass != PCL_NONE && BG_ClassAllowedInStage( tclass, stage ) && + BG_ClassIsAllowed( tclass ) && + credits >= BG_Class( tclass )->cost * ALIEN_CREDITS_PER_KILL ) + return qtrue; + } + + return qfalse; + } + + Com_Printf( S_COLOR_YELLOW "WARNING: fallthrough in BG_AlienCanEvolve\n" ); + return qfalse; +} + +/* +====================== +BG_ParseClassFile + +Parses a configuration file describing a class +====================== +*/ +static qboolean BG_ParseClassFile( const char *filename, classConfig_t *cc ) +{ + char *text_p; + int i; + int len; + char *token; + char text[ 20000 ]; + fileHandle_t f; + float scale = 0.0f; + int defined = 0; + enum + { + MODEL = 1 << 0, + SKIN = 1 << 1, + HUD = 1 << 2, + MODELSCALE = 1 << 3, + SHADOWSCALE = 1 << 4, + MINS = 1 << 5, + MAXS = 1 << 6, + DEADMINS = 1 << 7, + DEADMAXS = 1 << 8, + CROUCHMAXS = 1 << 9, + VIEWHEIGHT = 1 << 10, + CVIEWHEIGHT = 1 << 11, + ZOFFSET = 1 << 12, + NAME = 1 << 13, + SHOULDEROFFSETS = 1 << 14 + }; + + // 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( cc->modelName, token, sizeof( cc->modelName ) ); + + defined |= MODEL; + continue; + } + else if( !Q_stricmp( token, "skin" ) ) + { + token = COM_Parse( &text_p ); + if( !token ) + break; + + Q_strncpyz( cc->skinName, token, sizeof( cc->skinName ) ); + + defined |= SKIN; + continue; + } + else if( !Q_stricmp( token, "hud" ) ) + { + token = COM_Parse( &text_p ); + if( !token ) + break; + + Q_strncpyz( cc->hudName, token, sizeof( cc->hudName ) ); + + defined |= HUD; + 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; + + cc->modelScale = scale; + + defined |= MODELSCALE; + 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; + + cc->shadowScale = scale; + + defined |= SHADOWSCALE; + continue; + } + else if( !Q_stricmp( token, "mins" ) ) + { + for( i = 0; i <= 2; i++ ) + { + token = COM_Parse( &text_p ); + if( !token ) + break; + + cc->mins[ i ] = atof( token ); + } + + defined |= MINS; + continue; + } + else if( !Q_stricmp( token, "maxs" ) ) + { + for( i = 0; i <= 2; i++ ) + { + token = COM_Parse( &text_p ); + if( !token ) + break; + + cc->maxs[ i ] = atof( token ); + } + + defined |= MAXS; + continue; + } + else if( !Q_stricmp( token, "deadMins" ) ) + { + for( i = 0; i <= 2; i++ ) + { + token = COM_Parse( &text_p ); + if( !token ) + break; + + cc->deadMins[ i ] = atof( token ); + } + + defined |= DEADMINS; + continue; + } + else if( !Q_stricmp( token, "deadMaxs" ) ) + { + for( i = 0; i <= 2; i++ ) + { + token = COM_Parse( &text_p ); + if( !token ) + break; + + cc->deadMaxs[ i ] = atof( token ); + } + + defined |= DEADMAXS; + continue; + } + else if( !Q_stricmp( token, "crouchMaxs" ) ) + { + for( i = 0; i <= 2; i++ ) + { + token = COM_Parse( &text_p ); + if( !token ) + break; + + cc->crouchMaxs[ i ] = atof( token ); + } + + defined |= CROUCHMAXS; + continue; + } + else if( !Q_stricmp( token, "viewheight" ) ) + { + token = COM_Parse( &text_p ); + cc->viewheight = atoi( token ); + defined |= VIEWHEIGHT; + continue; + } + else if( !Q_stricmp( token, "crouchViewheight" ) ) + { + token = COM_Parse( &text_p ); + cc->crouchViewheight = atoi( token ); + defined |= CVIEWHEIGHT; + continue; + } + else if( !Q_stricmp( token, "zOffset" ) ) + { + float offset; + + token = COM_Parse( &text_p ); + if( !token ) + break; + + offset = atof( token ); + + cc->zOffset = offset; + + defined |= ZOFFSET; + continue; + } + else if( !Q_stricmp( token, "name" ) ) + { + token = COM_Parse( &text_p ); + if( !token ) + break; + + Q_strncpyz( cc->humanName, token, sizeof( cc->humanName ) ); + + defined |= NAME; + continue; + } + else if( !Q_stricmp( token, "shoulderOffsets" ) ) + { + for( i = 0; i <= 2; i++ ) + { + token = COM_Parse( &text_p ); + if( !token ) + break; + + cc->shoulderOffsets[ i ] = atof( token ); + } + + defined |= SHOULDEROFFSETS; + continue; + } + + Com_Printf( S_COLOR_RED "ERROR: unknown token '%s'\n", token ); + return qfalse; + } + + if( !( defined & MODEL ) ) token = "model"; + else if( !( defined & SKIN ) ) token = "skin"; + else if( !( defined & HUD ) ) token = "hud"; + else if( !( defined & MODELSCALE ) ) token = "modelScale"; + else if( !( defined & SHADOWSCALE ) ) token = "shadowScale"; + else if( !( defined & MINS ) ) token = "mins"; + else if( !( defined & MAXS ) ) token = "maxs"; + else if( !( defined & DEADMINS ) ) token = "deadMins"; + else if( !( defined & DEADMAXS ) ) token = "deadMaxs"; + else if( !( defined & CROUCHMAXS ) ) token = "crouchMaxs"; + else if( !( defined & VIEWHEIGHT ) ) token = "viewheight"; + else if( !( defined & CVIEWHEIGHT ) ) token = "crouchViewheight"; + else if( !( defined & ZOFFSET ) ) token = "zOffset"; + else if( !( defined & NAME ) ) token = "name"; + else if( !( defined & SHOULDEROFFSETS ) ) token = "shoulderOffsets"; + else token = ""; + + if( strlen( token ) > 0 ) + { + Com_Printf( S_COLOR_RED "ERROR: %s not defined in %s\n", + token, filename ); + return qfalse; + } + + return qtrue; +} + +/* +=============== +BG_InitClassConfigs +=============== +*/ +void BG_InitClassConfigs( void ) +{ + int i; + classConfig_t *cc; + + for( i = PCL_NONE; i < PCL_NUM_CLASSES; i++ ) + { + cc = BG_ClassConfig( i ); + + BG_ParseClassFile( va( "configs/classes/%s.cfg", + BG_Class( i )->name ), cc ); + } +} + +//////////////////////////////////////////////////////////////////////////////// + +static const weaponAttributes_t bg_weapons[ ] = +{ + { + WP_ALEVEL0, //int weaponNum; + 0, //int price; + ( 1 << S1 )|( 1 << S2 )|( 1 << S3 ), //int stages + SLOT_WEAPON, //int slots; + "level0", //char *weaponName; + "Bite", //char *humanName; + "", + 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; + TEAM_ALIENS //team_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 *humanName; + "", + 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; + TEAM_ALIENS //team_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 *humanName; + "", + 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; + TEAM_ALIENS //team_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 *humanName; + "", + 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; + TEAM_ALIENS //team_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 *humanName; + "", + 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; + TEAM_ALIENS //team_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 *humanName; + "", + 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; + TEAM_ALIENS //team_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 *humanName; + "", + 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; + TEAM_ALIENS //team_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 *humanName; + "", + 1, //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; + qtrue, //qboolean hasThirdMode; + qfalse, //qboolean canZoom; + 90.0f, //float zoomFov; + qfalse, //qboolean purchasable; + qfalse, //qboolean longRanged; + TEAM_ALIENS //team_t team; + }, + { + WP_BLASTER, //int weaponNum; + 0, //int price; + ( 1 << S1 )|( 1 << S2 )|( 1 << S3 ), //int stages + 0, //int slots; + "blaster", //char *weaponName; + "Blaster", //char *humanName; + "", + 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; + TEAM_HUMANS //team_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 *humanName; + "Basic weapon. Cased projectile weapon, with a slow clip based " + "reload system.", + 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; + TEAM_HUMANS //team_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 *humanName; + "Similar to a chainsaw, but instead of a chain it has an " + "electric arc capable of dealing a great deal of damage at " + "close range.", + 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; + TEAM_HUMANS //team_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 *humanName; + "Close range weapon that is useful against larger foes. " + "It has a slow repeat rate, but can be devastatingly " + "effective.", + 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; + TEAM_HUMANS //team_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 *humanName; + "Slightly more powerful than the basic rifle, rapidly fires " + "small packets of energy.", + 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; + TEAM_HUMANS //team_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 *humanName; + "A portable particle accelerator which causes minor nuclear " + "reactions at the point of impact. It has a very large " + "payload, but fires slowly.", + 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; + TEAM_HUMANS //team_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 *humanName; + "Belt drive, cased projectile weapon. It has a high repeat " + "rate but a wide firing angle and is therefore relatively " + "inaccurate.", + 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; + TEAM_HUMANS //team_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 *humanName; + "Sprays fire at its target. It is powered by compressed " + "gas. The relatively low rate of fire means this weapon is most " + "effective against static targets.", + 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; + TEAM_HUMANS //team_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 *humanName; + "An energy weapon that fires rapid pulses of concentrated energy.", + 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; + TEAM_HUMANS //team_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 *humanName; + "Blaster technology scaled up to deliver devastating power. " + "Primary fire must be charged before firing. It has a quick " + "secondary attack that does not require charging.", + LCANNON_AMMO, //int maxAmmo; + 0, //int maxClips; + qfalse, //int infiniteAmmo; + qtrue, //int usesEnergy; + LCANNON_REPEAT, //int repeatRate1; + LCANNON_SECONDARY_REPEAT, //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; + TEAM_HUMANS //team_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 *humanName; + "", + 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; + TEAM_HUMANS //team_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 *humanName; + "", + 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; + TEAM_ALIENS //team_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 *humanName; + "", + 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; + TEAM_ALIENS //team_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 *humanName; + "", + 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; + TEAM_HUMANS //team_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 *humanName; + "", + 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; + TEAM_HUMANS //team_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 *humanName; + "", + 0, //int maxAmmo; + 0, //int maxClips; + qtrue, //int infiniteAmmo; + qfalse, //int usesEnergy; + ABUILDER_BUILD_REPEAT,//int repeatRate1; + ABUILDER_CLAW_REPEAT, //int repeatRate2; + 0, //int repeatRate3; + 0, //int reloadTime; + ABUILDER_CLAW_K_SCALE,//float knockbackScale; + qtrue, //qboolean hasAltMode; + qfalse, //qboolean hasThirdMode; + qfalse, //qboolean canZoom; + 90.0f, //float zoomFov; + qtrue, //qboolean purchasable; + qfalse, //qboolean longRanged; + TEAM_ALIENS //team_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 *humanName; + "", + 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; + TEAM_ALIENS //team_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 *humanName; + "Used for building structures. This includes " + "spawns, power and basic defense. More structures become " + "available with new stages.", + 0, //int maxAmmo; + 0, //int maxClips; + qtrue, //int infiniteAmmo; + qfalse, //int usesEnergy; + HBUILD_REPEAT, //int repeatRate1; + 0, //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; + TEAM_HUMANS //team_t team; + } +}; + +int bg_numWeapons = sizeof( bg_weapons ) / sizeof( bg_weapons[ 0 ] ); + +static const weaponAttributes_t nullWeapon = { 0 }; + +/* +============== +BG_WeaponByName +============== +*/ +const weaponAttributes_t *BG_WeaponByName( const char *name ) +{ + int i; + + for( i = 0; i < bg_numWeapons; i++ ) + { + if( !Q_stricmp( bg_weapons[ i ].name, name ) ) + { + return &bg_weapons[ i ]; + } + } + + return &nullWeapon; +} + +/* +============== +BG_Weapon +============== +*/ +const weaponAttributes_t *BG_Weapon( weapon_t weapon ) +{ + return ( weapon > WP_NONE && weapon < WP_NUM_WEAPONS ) ? + &bg_weapons[ weapon - 1 ] : &nullWeapon; +} + +/* +============== +BG_WeaponAllowedInStage +============== +*/ +qboolean BG_WeaponAllowedInStage( weapon_t weapon, stage_t stage ) +{ + int stages = BG_Weapon( weapon )->stages; + + return stages & ( 1 << stage ); +} + +//////////////////////////////////////////////////////////////////////////////// + +static const 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 *humanName; + "Protective armour that helps to defend against light alien melee " + "attacks.", + "icons/iconu_larmour", + qtrue, //qboolean purchasable + qfalse, //qboolean usable + TEAM_HUMANS //team_t team; + }, + { + UP_HELMET_MK1, //int upgradeNum; + HELMET_MK1_PRICE, //int price; + ( 1 << S1 ), //int stages + SLOT_HEAD, //int slots; + "helmet_mk1", //char *upgradeName; + "Helmet Mk1", //char *humanName; + "Headgear that protects your head from injuries.", + "icons/iconu_helmet", + qtrue, //qboolean purchasable + qfalse, //qboolean usable + TEAM_HUMANS //team_t team; + }, + { + UP_HELMET_MK2, //int upgradeNum; + HELMET_MK2_PRICE, //int price; + ( 1 << S2 )|( 1 << S3 ),//int stages + SLOT_HEAD, //int slots; + "helmet_mk2", //char *upgradeName; + "Helmet Mk2", //char *humanName; + "In addition to protecting your head, the helmet provides a " + "scanner indicating the presence of any friendly or hostile " + "lifeforms and structures in your immediate vicinity.", + "icons/iconu_helmet_mk2", + qtrue, //qboolean purchasable + qfalse, //qboolean usable + TEAM_HUMANS //team_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 *humanName; + "", + "icons/iconu_atoxin", + qfalse, //qboolean purchasable + qtrue, //qboolean usable + TEAM_HUMANS //team_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 *humanName; + "Back-mounted battery pack that permits storage of one and a half " + "times the normal energy capacity for energy weapons.", + "icons/iconu_battpack", + qtrue, //qboolean purchasable + qfalse, //qboolean usable + TEAM_HUMANS //team_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 *humanName; + "Back-mounted jet pack that enables the user to fly to remote " + "locations. It is very useful against alien spawns in hard " + "to reach spots.", + "icons/iconu_jetpack", + qtrue, //qboolean purchasable + qtrue, //qboolean usable + TEAM_HUMANS //team_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 *humanName; + "A full body armour that is highly effective at repelling alien attacks. " + "It allows the user to enter hostile situations with a greater degree " + "of confidence.", + "icons/iconu_bsuit", + qtrue, //qboolean purchasable + qfalse, //qboolean usable + TEAM_HUMANS //team_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 *humanName; + "A small incendinary device ideal for damaging tightly packed " + "alien structures. Has a five second timer.", + 0, + qtrue, //qboolean purchasable + qtrue, //qboolean usable + TEAM_HUMANS //team_t team; + }, + { + UP_BIORES, //int upgradeNum; + BIORES_PRICE, //int price; + ( 1 << S2 )|( 1 << S3 ), //int stages + SLOT_NONE, //int slots; + "biores", //char *upgradeName; + "Biores", //char *humanName; + "figure out a description later", + "icons/iconu_biokit", + qtrue, //qboolean purchasable + qfalse, //qboolean usable + TEAM_HUMANS //team_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 *humanName; + "Ammunition for the currently held weapon.", + 0, + qtrue, //qboolean purchasable + qfalse, //qboolean usable + TEAM_HUMANS //team_t team; + } +}; + +int bg_numUpgrades = sizeof( bg_upgrades ) / sizeof( bg_upgrades[ 0 ] ); + +static const upgradeAttributes_t nullUpgrade = { 0 }; + +/* +============== +BG_UpgradeByName +============== +*/ +const upgradeAttributes_t *BG_UpgradeByName( const char *name ) +{ + int i; + + for( i = 0; i < bg_numUpgrades; i++ ) + { + if( !Q_stricmp( bg_upgrades[ i ].name, name ) ) + { + return &bg_upgrades[ i ]; + } + } + + return &nullUpgrade; +} + +/* +============== +BG_Upgrade +============== +*/ +const upgradeAttributes_t *BG_Upgrade( upgrade_t upgrade ) +{ + return ( upgrade > UP_NONE && upgrade < UP_NUM_UPGRADES ) ? + &bg_upgrades[ upgrade - 1 ] : &nullUpgrade; +} + +/* +============== +BG_UpgradeAllowedInStage +============== +*/ +qboolean BG_UpgradeAllowedInStage( upgrade_t upgrade, stage_t stage ) +{ + int stages = BG_Upgrade( upgrade )->stages; + + return stages & ( 1 << stage ); +} + +//////////////////////////////////////////////////////////////////////////////// + +/* +================ +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", // 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_MASS_DRIVER", + + "EV_MISSILE_HIT", + "EV_MISSILE_MISS", + "EV_MISSILE_MISS_METAL", + "EV_TESLATRAIL", + "EV_BULLET", // otherEntity is the shooter + + "EV_LEV1_GRAB", + "EV_LEV4_TRAMPLE_PREPARE", + "EV_LEV4_TRAMPLE_START", + + "EV_PAIN", + "EV_DEATH1", + "EV_DEATH2", + "EV_DEATH3", + "EV_OBITUARY", + + "EV_GIB_PLAYER", // gib a previously living player + + "EV_BUILD_CONSTRUCT", + "EV_BUILD_DESTROY", + "EV_BUILD_DELAY", // can't build yet + "EV_BUILD_REPAIR", // repairing buildable + "EV_BUILD_REPAIRED", // 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", // overmind under attack + "EV_OVERMIND_DYING", // overmind close to death + "EV_OVERMIND_SPAWNS", // overmind needs spawns + + "EV_DCC_ATTACK", // dcc under attack + + "EV_MGTURRET_SPINUP", // trigger a sound + + "EV_RPTUSE_SOUND", // trigger a sound + "EV_LEV2_ZAP" +}; + +/* +=============== +BG_EventName +=============== +*/ +const char *BG_EventName( int num ) +{ + if( num < 0 || num >= sizeof( eventnames ) / sizeof( char * ) ) + return "UNKNOWN"; + + return eventnames[ num ]; +} + +/* +=============== +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 GAME + Com_Printf( " game event svt %5d -> %5d: num = %20s parm %d\n", + ps->pmove_framecount/*ps->commandTime*/, ps->eventSequence, + BG_EventName( newEvent ), eventParm ); +#else + Com_Printf( "Cgame event svt %5d -> %5d: num = %20s parm %d\n", + ps->pmove_framecount/*ps->commandTime*/, ps->eventSequence, + BG_EventName( 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_SPECSTATE ] != SPECTATOR_NOT ) + 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 ); + + s->time2 = ps->movementDir; + s->legsAnim = ps->legsAnim; + s->torsoAnim = ps->torsoAnim; + s->weaponAnim = ps->weaponAnim; + 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; + } + } + + // use misc field to store team/class info: + s->misc = ps->stats[ STAT_TEAM ] | ( ps->stats[ STAT_CLASS ] << 8 ); + + // have to get the surfNormal through somehow... + VectorCopy( ps->grapplePoint, s->angles2 ); + + 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_SPECSTATE ] != SPECTATOR_NOT ) + 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 ); + + s->time2 = ps->movementDir; + s->legsAnim = ps->legsAnim; + s->torsoAnim = ps->torsoAnim; + s->weaponAnim = ps->weaponAnim; + 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; + } + } + + // use misc field to store team/class info: + s->misc = ps->stats[ STAT_TEAM ] | ( ps->stats[ STAT_CLASS ] << 8 ); + + // have to get the surfNormal through somehow... + VectorCopy( ps->grapplePoint, s->angles2 ); + + 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_WeaponMaxAmmo +======================== +*/ +int BG_WeaponMaxAmmo( weapon_t weapon, int *stats) +{ + if( BG_InventoryContainsUpgrade( UP_BATTPACK, stats ) ) + return (int)( (float)BG_Weapon(weapon)->maxAmmo * BATTPACK_MODIFIER ); + else + return BG_Weapon(weapon)->maxAmmo; +} + +/* +======================== +BG_WeaponIsFull + +Check if a weapon has full ammo +======================== +*/ +qboolean BG_WeaponIsFull( weapon_t weapon, int stats[ ], int ammo, int clips ) +{ + int maxAmmo, maxClips; + + maxAmmo = BG_Weapon( weapon )->maxAmmo; + maxClips = BG_Weapon( weapon )->maxClips; + + if( BG_InventoryContainsUpgrade( UP_BATTPACK, stats ) ) + maxAmmo = (int)( (float)maxAmmo * BATTPACK_MODIFIER ); + + return ( maxAmmo == ammo ) && ( maxClips == clips ); +} + +/* +======================== +BG_InventoryContainsWeapon + +Does the player hold a weapon? +======================== +*/ +qboolean BG_InventoryContainsWeapon( int weapon, int stats[ ] ) +{ + // humans always have a blaster + if( stats[ STAT_TEAM ] == TEAM_HUMANS && weapon == WP_BLASTER ) + return qtrue; + + return ( stats[ STAT_WEAPON ] == weapon ); +} + +/* +======================== +BG_SlotsForInventory + +Calculate the slots used by an inventory and warn of conflicts +======================== +*/ +int BG_SlotsForInventory( int stats[ ] ) +{ + int i, slot, slots; + + slots = BG_Weapon( stats[ STAT_WEAPON ] )->slots; + if( stats[ STAT_TEAM ] == TEAM_HUMANS ) + slots |= BG_Weapon( WP_BLASTER )->slots; + + for( i = UP_NONE; i < UP_NUM_UPGRADES; i++ ) + { + if( BG_InventoryContainsUpgrade( i, stats ) ) + { + slot = BG_Upgrade( i )->slots; + + // this check should never be true + if( slots & slot ) + { + Com_Printf( S_COLOR_YELLOW "WARNING: held item %d conflicts with " + "inventory slot %d\n", i, slot ); + } + + slots |= slot; + } + } + + return slots; +} + +/* +======================== +BG_AddUpgradeToInventory + +Give the player an upgrade +======================== +*/ +void BG_AddUpgradeToInventory( int item, int stats[ ] ) +{ + stats[ STAT_ITEMS ] |= ( 1 << item ); +} + +/* +======================== +BG_RemoveUpgradeFromInventory + +Take an upgrade from the player +======================== +*/ +void BG_RemoveUpgradeFromInventory( int item, int stats[ ] ) +{ + stats[ STAT_ITEMS ] &= ~( 1 << 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[ ] ) +{ + if( item != UP_JETPACK || stats[ STAT_FUEL ] > 0) + 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_GetClientNormal + +Get the normal for the surface the client is walking on +=============== +*/ +void BG_GetClientNormal( const playerState_t *ps, vec3_t normal ) +{ + if( ps->stats[ STAT_STATE ] & SS_WALLCLIMBING ) + { + if( ps->eFlags & EF_WALLCLIMBCEILING ) + VectorSet( normal, 0.0f, 0.0f, -1.0f ); + else + VectorCopy( ps->grapplePoint, normal ); + } + else + VectorSet( normal, 0.0f, 0.0f, 1.0f ); +} + +/* +=============== +BG_GetClientViewOrigin + +Get the position of the client's eye, based on the client's position, the surface's normal, and client's view height +=============== +*/ +void BG_GetClientViewOrigin( const playerState_t *ps, vec3_t viewOrigin ) +{ + vec3_t normal; + BG_GetClientNormal( ps, normal ); + VectorMA( ps->origin, ps->viewheight, normal, viewOrigin ); +} + +/* +=============== +BG_PositionBuildableRelativeToPlayer + +Find a place to build a buildable +=============== +*/ + +#define DIAGONAL2(a,b) sqrt((a)*(a)+(b)*(b)) +qboolean BG_PositionBuildableRelativeToPlayer( const playerState_t *ps, qboolean iscuboid, + 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, normal, view, traceA, traceB, faces[6]; + float buildDist, maxDist, normalOffset=0.0f, zOffset, dist, min=10e6; + int i; + vec3_t cuboid; + + BG_GetClientNormal(ps,normal); + AngleVectors(ps->viewangles,forward,NULL,NULL); + buildDist=BG_Class(ps->stats[STAT_CLASS])->buildDist; + if(iscuboid) + { + BG_GetClientViewOrigin(ps,view); + VectorNormalize(forward); + VectorSubtract(maxs,mins,cuboid); + maxDist=buildDist+VectorLength(cuboid); + VectorMA(view,maxDist,forward,traceB); + VectorMA(view,0.0f,forward,traceA); + (*trace)(tr,traceA,NULL,NULL,traceB,ps->clientNum,MASK_PLAYERSOLID); + if(COMPARE_FLOAT_EPSILON(tr->fraction,1.0f)) + return qfalse; + zOffset=-cuboid[2]*0.5f; + if(COMPARE_VECTOR_EPSILON(tr->plane.normal,0.0f,0.0f,1.0f)) + zOffset=0.0f; + else if(COMPARE_VECTOR_EPSILON(tr->plane.normal,0.0f,0.0f,-1.0f)) + zOffset=-cuboid[2]; + else if(COMPARE_VECTOR_EPSILON(tr->plane.normal,1.0f,0.0f,0.0f) || + COMPARE_VECTOR_EPSILON(tr->plane.normal,-1.0f,0.0f,0.0f)) + normalOffset=cuboid[0]*0.5f; + else if(COMPARE_VECTOR_EPSILON(tr->plane.normal,0.0f,1.0f,0.0f) || + COMPARE_VECTOR_EPSILON(tr->plane.normal,0.0f,-1.0f,0.0f)) + normalOffset=cuboid[1]*0.5f; + else + return qfalse; + VectorMA(tr->endpos,normalOffset,tr->plane.normal,outOrigin); + outOrigin[2]+=zOffset; + for(i=0;i<6;i++) + VectorCopy(outOrigin,faces[i]); + faces[0][0]+=cuboid[0]/2.0f; + faces[1][1]+=cuboid[1]/2.0f; + faces[2][0]-=cuboid[0]/2.0f; + faces[3][1]-=cuboid[1]/2.0f; + faces[4][2]+=cuboid[2]; + for(i=0;i<6;i++) + if((dist=Distance(view,faces[i]))<min) + min=dist; + if(min>256) + return qfalse; + return qtrue; + } + vectoangles(forward,outAngles); + ProjectPointOnPlane(forward,forward,normal); + VectorNormalize(forward); + VectorMA(ps->origin,buildDist,forward,traceA); + VectorMA(traceA,-128,normal,traceB); + VectorMA(traceA,32,normal,traceA); + (*trace)(tr,traceA,mins,maxs,traceB,ps->clientNum,MASK_PLAYERSOLID); + VectorCopy(tr->endpos,outOrigin); + return qtrue; +} + +/* +=============== +BG_GetValueOfPlayer + +Returns the credit value of a player +=============== +*/ +int BG_GetValueOfPlayer( playerState_t *ps ) +{ + int i, worth = 0; + + worth = BG_Class( ps->stats[ STAT_CLASS ] )->value; + + // Humans have worth from their equipment as well + if( ps->stats[ STAT_TEAM ] == TEAM_HUMANS ) + { + for( i = UP_NONE + 1; i < UP_NUM_UPGRADES; i++ ) + { + if( BG_InventoryContainsUpgrade( i, ps->stats ) ) + worth += BG_Upgrade( i )->price; + } + + for( i = WP_NONE + 1; i < WP_NUM_WEAPONS; i++ ) + { + if( BG_InventoryContainsWeapon( i, ps->stats ) ) + worth += BG_Weapon( i )->price; + } + } + + return worth; +} + +/* +================= +BG_PlayerCanChangeWeapon +================= +*/ +qboolean BG_PlayerCanChangeWeapon( playerState_t *ps ) +{ + // Do not allow Lucifer Cannon "canceling" via weapon switch + if( ps->weapon == WP_LUCIFER_CANNON && + ps->stats[ STAT_MISC ] > LCANNON_CHARGE_TIME_MIN ) + return qfalse; + + return ps->weaponTime <= 0 || ps->weaponstate != WEAPON_FIRING; +} + +/* +================= +BG_PlayerPoisonCloudTime +================= +*/ +int BG_PlayerPoisonCloudTime( playerState_t *ps ) +{ + int time = LEVEL1_PCLOUD_TIME; + + if( BG_InventoryContainsUpgrade( UP_BATTLESUIT, ps->stats ) ) + time -= BSUIT_PCLOUD_PROTECTION; + if( BG_InventoryContainsUpgrade( UP_HELMET_MK1, ps->stats ) ) + time -= HELMET_MK1_PCLOUD_PROTECTION; + if( BG_InventoryContainsUpgrade( UP_HELMET_MK2, ps->stats ) ) + time -= HELMET_MK2_PCLOUD_PROTECTION; + if( BG_InventoryContainsUpgrade( UP_LIGHTARMOUR, ps->stats ) ) + time -= LIGHTARMOUR_PCLOUD_PROTECTION; + + return time; +} + +/* +================= +BG_GetPlayerWeapon + +Returns the players current weapon or the weapon they are switching to. +Only needs to be used for human weapons. +================= +*/ +weapon_t BG_GetPlayerWeapon( playerState_t *ps ) +{ + if( ps->persistant[ PERS_NEWWEAPON ] ) + return ps->persistant[ PERS_NEWWEAPON ]; + + return ps->weapon; +} + +/* +=============== +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; +} + +#define MAX_NUM_PACKED_ENTITY_NUMS 10 + +/* +=============== +BG_PackEntityNumbers + +Pack entity numbers into an entityState_t +=============== +*/ +void BG_PackEntityNumbers( entityState_t *es, const int *entityNums, int count ) +{ + int i; + + if( count > MAX_NUM_PACKED_ENTITY_NUMS ) + { + count = MAX_NUM_PACKED_ENTITY_NUMS; + Com_Printf( S_COLOR_YELLOW "WARNING: A maximum of %d entity numbers can be " + "packed, but BG_PackEntityNumbers was passed %d entities", + MAX_NUM_PACKED_ENTITY_NUMS, count ); + } + + es->misc = es->time = es->time2 = es->constantLight = 0; + + for( i = 0; i < MAX_NUM_PACKED_ENTITY_NUMS; i++ ) + { + int entityNum; + + if( i < count ) + entityNum = entityNums[ i ]; + else + entityNum = ENTITYNUM_NONE; + + if( entityNum & ~GENTITYNUM_MASK ) + { + Com_Error( ERR_FATAL, "BG_PackEntityNumbers passed an entity number (%d) which " + "exceeds %d bits", entityNum, GENTITYNUM_BITS ); + } + + switch( i ) + { + case 0: es->misc |= entityNum; break; + case 1: es->time |= entityNum; break; + case 2: es->time |= entityNum << GENTITYNUM_BITS; break; + case 3: es->time |= entityNum << (GENTITYNUM_BITS * 2); break; + case 4: es->time2 |= entityNum; break; + case 5: es->time2 |= entityNum << GENTITYNUM_BITS; break; + case 6: es->time2 |= entityNum << (GENTITYNUM_BITS * 2); break; + case 7: es->constantLight |= entityNum; break; + case 8: es->constantLight |= entityNum << GENTITYNUM_BITS; break; + case 9: es->constantLight |= entityNum << (GENTITYNUM_BITS * 2); break; + default: Com_Error( ERR_FATAL, "Entity index %d not handled", i ); break; + } + } +} + +/* +=============== +BG_UnpackEntityNumbers + +Unpack entity numbers from an entityState_t +=============== +*/ +int BG_UnpackEntityNumbers( entityState_t *es, int *entityNums, int count ) +{ + int i; + + if( count > MAX_NUM_PACKED_ENTITY_NUMS ) + count = MAX_NUM_PACKED_ENTITY_NUMS; + + for( i = 0; i < count; i++ ) + { + int *entityNum = &entityNums[ i ]; + + switch( i ) + { + case 0: *entityNum = es->misc; break; + case 1: *entityNum = es->time; break; + case 2: *entityNum = (es->time >> GENTITYNUM_BITS); break; + case 3: *entityNum = (es->time >> (GENTITYNUM_BITS * 2)); break; + case 4: *entityNum = es->time2; break; + case 5: *entityNum = (es->time2 >> GENTITYNUM_BITS); break; + case 6: *entityNum = (es->time2 >> (GENTITYNUM_BITS * 2)); break; + case 7: *entityNum = es->constantLight; break; + case 8: *entityNum = (es->constantLight >> GENTITYNUM_BITS); break; + case 9: *entityNum = (es->constantLight >> (GENTITYNUM_BITS * 2)); break; + default: Com_Error( ERR_FATAL, "Entity index %d not handled", i ); break; + } + + *entityNum &= GENTITYNUM_MASK; + + if( *entityNum == ENTITYNUM_NONE ) + break; + } + + return i; +} + +/* +=============== +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_WeaponByName( q )->number; + + if( upgradesSize ) + upgrades[ j ] = BG_UpgradeByName( q )->number; + + 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, class_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' && i < classesSize - 1 ) + { + //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_ClassByName( q )->number; + + 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' && i < buildablesSize - 1 ) + { + //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_BuildableByName( q )->number; + + 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; +} + +typedef struct gameElements_s +{ + buildable_t buildables[ BA_NUM_BUILDABLES ]; + class_t classes[ PCL_NUM_CLASSES ]; + weapon_t weapons[ WP_NUM_WEAPONS ]; + upgrade_t upgrades[ UP_NUM_UPGRADES ]; + int cuboidMode; +} 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 ); + + trap_Cvar_VariableStringBuffer( "g_cuboidMode", + cvar, MAX_CVAR_VALUE_STRING ); + + bg_disabledGameElements.cuboidMode = atoi_neg(cvar,qfalse); + +} + +/* +============ +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( class_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_PrimaryWeapon +============ +*/ +weapon_t BG_PrimaryWeapon( int stats[ ] ) +{ + int i; + + for( i = WP_NONE; i < WP_NUM_WEAPONS; i++ ) + { + if( BG_Weapon( i )->slots != SLOT_WEAPON ) + continue; + if( BG_InventoryContainsWeapon( i, stats ) ) + return i; + } + + if( BG_InventoryContainsWeapon( WP_BLASTER, stats ) ) + return WP_BLASTER; + + return WP_NONE; +} + +/* +============ +BG_LoadEmoticons +============ +*/ +int BG_LoadEmoticons( emoticon_t *emoticons, int num ) +{ + int numFiles; + char fileList[ MAX_EMOTICONS * ( MAX_EMOTICON_NAME_LEN + 9 ) ] = {""}; + int i; + char *filePtr; + int fileLen; + int count; + + numFiles = trap_FS_GetFileList( "emoticons", "x1.tga", fileList, + sizeof( fileList ) ); + + if( numFiles < 1 ) + return 0; + + filePtr = fileList; + fileLen = 0; + count = 0; + for( i = 0; i < numFiles && count < num; i++, filePtr += fileLen + 1 ) + { + fileLen = strlen( filePtr ); + if( fileLen < 9 || filePtr[ fileLen - 8 ] != '_' || + filePtr[ fileLen - 7 ] < '1' || filePtr[ fileLen - 7 ] > '9' ) + { + Com_Printf( S_COLOR_YELLOW "skipping invalidly named emoticon \"%s\"\n", + filePtr ); + continue; + } + if( fileLen - 8 > MAX_EMOTICON_NAME_LEN ) + { + Com_Printf( S_COLOR_YELLOW "emoticon file name \"%s\" too long (>%d)\n", + filePtr, MAX_EMOTICON_NAME_LEN + 8 ); + continue; + } + if( !trap_FS_FOpenFile( va( "emoticons/%s", filePtr ), NULL, FS_READ ) ) + { + Com_Printf( S_COLOR_YELLOW "could not open \"emoticons/%s\"\n", filePtr ); + continue; + } + + Q_strncpyz( emoticons[ count ].name, filePtr, fileLen - 8 + 1 ); +#ifndef GAME + emoticons[ count ].width = filePtr[ fileLen - 7 ] - '0'; +#endif + count++; + } + + Com_Printf( "Loaded %d of %d emoticons (MAX_EMOTICONS is %d)\n", + count, numFiles, MAX_EMOTICONS ); + return count; +} + +/* +============ +BG_TeamName +============ +*/ +char *BG_TeamName( team_t team ) +{ + if( team == TEAM_NONE ) + return "spectator"; + if( team == TEAM_ALIENS ) + return "alien"; + if( team == TEAM_HUMANS ) + return "human"; + return "<team>"; +} + +int cmdcmp( const void *a, const void *b ) +{ + return Q_stricmp( (const char *)a, ((dummyCmd_t *)b)->name ); +} + +/* +============ +cuboid stuff +============ +*/ + +const cuboidAttributes_t BG_CuboidTypes [] = +{ + { + qtrue, // qboolean genericAnim + "concrete", // const char* file + "[concrete]", // const char* icon + { // cuboidSoundset_t pain { + 3, // int numsounds + }, // } + { // cuboidSoundset_t dstr { + 1, // int numsounds + {0} // float volumes[6] + }, // } + 180, // float hppv + CBHPT_CUBES, // int hpt + 3.0, // float bppv + 40, // int buildrate + 1500, // int minbt + qfalse, // qboolean regen; + 0, // int regenspeed + qfalse, // qboolean zappable + qfalse, // qboolean repairable + qfalse, // qboolean detectable + qfalse, // qboolean needpower + 0 // int surfaceFlags + }, + { + qtrue, // qboolean genericAnim + "glass", // const char* file + "[glass]", // const char* icon + { // cuboidSoundset_t pain { + 3, // int numsounds + }, // } + { // cuboidSoundset_t dstr { + 3, // int numsounds + {0, 0.5, 6} // float volumes[6] + }, // } + 52, // float hppv + CBHPT_PANES, // int hpt + 1.5, // float bppv + 55, // int buildrate + 1200, // int minbt + qfalse, // qboolean regen; + 0, // int regenspeed + qfalse, // qboolean zappable + qfalse, // qboolean repairable + qfalse, // qboolean detectable + qfalse, // qboolean needpower + 0 // int surfaceFlags + }, + { + qtrue, // qboolean genericAnim + "ladder", // const char* file + "[ladder]", // const char* icon + { // cuboidSoundset_t pain { + 2, // int numsounds + }, // } + { // cuboidSoundset_t dstr { + 1, // int numsounds + {0} // float volumes[6] + }, // } + 50, // float hppv + CBHPT_PLAIN, // int hpt + 4.0, // float bppv + 26, // int buildrate + 1800, // int minbt + qfalse, // qboolean regen; + 0, // int regenspeed + qfalse, // qboolean zappable + qtrue, // qboolean repairable + qfalse, // qboolean detectable + qfalse, // qboolean needpower + SURF_LADDER | SURF_METALSTEPS // int surfaceFlags + }, + { + qfalse, // qboolean genericAnim + "organic", // const char* file + "[organic]", // const char* icon + { // cuboidSoundset_t pain { + 3, // int numsounds + }, // } + { // cuboidSoundset_t dstr { + 3, // int numsounds + {0,2.0,4.5} // float volumes[6] + }, // } + 50, // float hppv + CBHPT_PLAIN, // int hpt + 1.2, // float bppv + 83, // int buildrate + 750, // int minbt + qtrue, // qboolean regen; + 45, // int regenspeed + qfalse, // qboolean zappable + qfalse, // qboolean repairable + qfalse, // qboolean detectable + qfalse, // qboolean needpower + SURF_FLESH // int surfaceFlags + }, + { + qfalse, // qboolean genericAnim + "slime", // const char* file + "[slime]", // const char* icon + { // cuboidSoundset_t pain { + 2, // int numsounds + }, // } + { // cuboidSoundset_t dstr { + 2, // int numsounds + {0,2.0} // float volumes[6] + }, // } + 20, // float hppv + CBHPT_PLAIN, // int hpt + 1.5, // float bppv + 45, // int buildrate + 1300, // int minbt + qtrue, // qboolean regen; + 90, // int regenspeed + qfalse, // qboolean zappable + qfalse, // qboolean repairable + qfalse, // qboolean detectable + qfalse, // qboolean needpower + SURF_SLICK // int surfaceFlags + } +}; + +const cuboidAttributes_t *BG_CuboidAttributes( buildable_t buildable ) +{ + if( buildable>=CUBOID_FIRST && buildable<=CUBOID_LAST ) + return &BG_CuboidTypes[ buildable-CUBOID_FIRST ]; + return &BG_CuboidTypes[ 0 ]; +} + +qboolean BG_IsCuboid(buildable_t buildable) +{ + return ( buildable >= CUBOID_FIRST && buildable <= CUBOID_LAST ); +} + +void BG_CuboidBBox(const vec3_t dims, vec3_t mins, vec3_t maxs) +{ + mins[ 0 ] = -0.5 * dims[0]; + mins[ 1 ] = -0.5 * dims[1]; + maxs[ 0 ] = 0.5 * dims[0]; + maxs[ 1 ] = 0.5 * dims[1]; + mins[ 2 ] = 0.0; + maxs[ 2 ] = dims[2]; +} + +/* Since there is no space in entityState_t designed for cuboid-specific data, + * existing fields that are not in use by a cuboid are used: + * + * vec3_t angles - cuboid dimensions (NOTE: not sure if there are no precision losses) + * int constantLight - cuboid health (because generic1 is too small to fit the huge health values) + */ + +int BG_CuboidUnpackHealth(entityState_t *es) +{ + return es->constantLight; +} + +void BG_CuboidPackHealth(entityState_t *es, int health) +{ + es->constantLight = health; +} + +void BG_CuboidPackHealthSafe(buildable_t buildable, entityState_t *es, int health) +{ + if(!BG_Buildable(buildable,NULL)->cuboid) + return; + BG_CuboidPackHealth(es,health); +} + +int BG_CuboidMode(void) +{ + return bg_disabledGameElements.cuboidMode; +} + +// The g_cuboidMode cvar governs whether building cuboids is allowed or not. +// It was introduced to disable cuboids on maps on which balance could be +// heavily harmed by the ability to construct cuboids. + +// g_cuboidMode 0 - allowed +// g_cuboidMode 1 - allowed, except for Stage 1 +// g_cuboidMode 2 - not allowed at all + +qboolean BG_CuboidAllowed(int stage) +{ + int mode = bg_disabledGameElements.cuboidMode; + if(mode==0) + return qtrue; + if(mode==1) + return (stage>S1); + return qfalse; +} + +void BG_CuboidSortSize(const vec3_t a, vec3_t b) +{ + if(a[0]<a[1]) + { + if(a[0]<a[2]) + { + if(a[1]<a[2]) + b[0]=a[0],b[1]=a[1],b[2]=a[2]; + else + b[0]=a[0],b[1]=a[2],b[2]=a[1]; + } + else + b[0]=a[2],b[1]=a[0],b[2]=a[1]; + } + else + { + if(a[1]<a[2]) + { + if(a[0]<a[2]) + b[0]=a[1],b[1]=a[0],b[2]=a[2]; + else + b[0]=a[1],b[1]=a[2],b[2]=a[0]; + } + else + b[0]=a[2],b[1]=a[1],b[2]=a[0]; + } +} + +// TODO: not sure if used anymore, check and remove if neccessary +int BG_SoundForVolume(const cuboidVolsnds_t *set, const vec3_t size) +{ + int i; + float volume; + volume=size[0]*size[1]*size[2]*5.5306341e-5; + for(i=set->numsounds-1;i>=0;i--) + if(set->volumes[i]<volume) + return i; + return 0; //NOTE: can cause wtfs +} diff --git a/src/game/bg_pmove.c b/src/game/bg_pmove.c new file mode 100644 index 0000000..348ff4e --- /dev/null +++ b/src/game/bg_pmove.c @@ -0,0 +1,3878 @@ +/* +=========================================================================== +Copyright (C) 1999-2005 Id Software, Inc. +Copyright (C) 2000-2009 Darklegion Development + +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; + +float pm_airControlAmount = 150.0f; //Equivalence to side strafes value is approx ~165 + +//Side Air Strafes +float cpm_pm_strafeaccelerate = 70; //GoldSRC used 150. vq3: 1 +float cpm_pm_wishspeed = 30; //vq3: 400. I personally set this to 500 on vanilla mode since dretch travels at 448 ups +//You missed this. VQ3 just has "1.0" +float cpm_pm_airstopaccelerate = 2.5; + +//TODO: Marauder doesn't have a double jump since I removed it because it'll stack heavily when wall climbing. +//It doesn't need it though, but may feel akward +float cpm_pm_jump_z = 0.5; //CPM: 100/270 (normal jumpvel is 270, doublejump default 100) = 0.37037 + //However, that value feels too low and nothing much is acomplished since + //tremulous human jump default is only 240 ups, meaning half hight. + +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 +=================== +*/ +void PM_StartTorsoAnim( int anim ) +{ + if( PM_Paralyzed( pm->ps->pm_type ) ) + return; + + pm->ps->torsoAnim = ( ( pm->ps->torsoAnim & ANIM_TOGGLEBIT ) ^ ANIM_TOGGLEBIT ) + | anim; +} + +/* +=================== +PM_StartWeaponAnim +=================== +*/ +static void PM_StartWeaponAnim( int anim ) +{ + if( PM_Paralyzed( pm->ps->pm_type ) ) + return; + + pm->ps->weaponAnim = ( ( pm->ps->weaponAnim & ANIM_TOGGLEBIT ) ^ ANIM_TOGGLEBIT ) + | anim; +} + +/* +=================== +PM_StartLegsAnim +=================== +*/ +static void PM_StartLegsAnim( int anim ) +{ + if( PM_Paralyzed( pm->ps->pm_type ) ) + 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_ContinueWeaponAnim +=================== +*/ +static void PM_ContinueWeaponAnim( int anim ) +{ + if( ( pm->ps->weaponAnim & ~ANIM_TOGGLEBIT ) == anim ) + return; + + PM_StartWeaponAnim( 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; + + // 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_Class( pm->ps->stats[ STAT_CLASS ] )->stopSpeed; + float friction = BG_Class( pm->ps->stats[ STAT_CLASS ] )->friction; + + control = speed < stopSpeed ? stopSpeed : speed; + drop += control * friction * 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_TEAM ] == TEAM_HUMANS && pm->ps->pm_type == PM_NORMAL ) + { + qboolean wasSprinting; + qboolean sprint; + wasSprinting = sprint = pm->ps->stats[ STAT_STATE ] & SS_SPEEDBOOST; + + if( pm->ps->persistant[ PERS_STATE ] & PS_SPRINTTOGGLE ) + { + if( cmd->buttons & BUTTON_SPRINT && + !( pm->ps->pm_flags & PMF_SPRINTHELD ) ) + { + sprint = !sprint; + pm->ps->pm_flags |= PMF_SPRINTHELD; + } + else if( pm->ps->pm_flags & PMF_SPRINTHELD && + !( cmd->buttons & BUTTON_SPRINT ) ) + pm->ps->pm_flags &= ~PMF_SPRINTHELD; + } + else + sprint = cmd->buttons & BUTTON_SPRINT; + + if( sprint ) + pm->ps->stats[ STAT_STATE ] |= SS_SPEEDBOOST; + else if( wasSprinting && !sprint ) + pm->ps->stats[ STAT_STATE ] &= ~SS_SPEEDBOOST; + + // Walk overrides sprint. We keep the state that we want to be sprinting + // (above), but don't apply the modifier, and in g_active we skip taking + // the stamina too. + if( sprint && !( cmd->buttons & BUTTON_WALKING ) ) + 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 have stamina or a jetpack to jump + if( pm->ps->stats[ STAT_STAMINA ] < STAMINA_SLOW_LEVEL + STAMINA_JUMP_TAKE && + ( !BG_InventoryContainsUpgrade( UP_JETPACK, pm->ps->stats ) || pm->ps->stats[ STAT_FUEL ] < JETPACK_FUEL_JUMP ) ) + cmd->upmove = 0; + + //slow down once stamina depletes + if( pm->ps->stats[ STAT_STAMINA ] <= STAMINA_SLOW_LEVEL ) + modifier *= (float)( pm->ps->stats[ STAT_STAMINA ] + STAMINA_MAX ) / (float)(STAMINA_SLOW_LEVEL + STAMINA_MAX); + + 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->eFlags & EF_POISONCLOUDED ) + { + if( BG_InventoryContainsUpgrade( UP_LIGHTARMOUR, pm->ps->stats ) || + BG_InventoryContainsUpgrade( UP_BATTLESUIT, pm->ps->stats ) ) + modifier *= PCLOUD_ARMOUR_MODIFIER; + else + modifier *= PCLOUD_MODIFIER; + } + } + + if( pm->ps->weapon == WP_ALEVEL4 && pm->ps->pm_flags & PMF_CHARGE ) + modifier *= 1.0f + ( pm->ps->stats[ STAT_MISC ] * + ( LEVEL4_TRAMPLE_SPEED - 1.0f ) / + LEVEL4_TRAMPLE_DURATION ); + + //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_Class( pm->ps->stats[ STAT_CLASS ] )->jumpMagnitude == 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 ) +{ + int jumpMagnitude; + + 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->pm_flags &= ~PMF_CHARGE; + pm->ps->weaponTime += LEVEL3_POUNCE_REPEAT; + return qfalse; + } + + // We're building up for a pounce + if( pm->cmd.buttons & BUTTON_ATTACK2 ) + { + pm->ps->pm_flags &= ~PMF_CHARGE; + return qfalse; + } + + // Can't start a pounce + if( ( pm->ps->pm_flags & PMF_CHARGE ) || + pm->ps->stats[ STAT_MISC ] < LEVEL3_POUNCE_TIME_MIN || + pm->ps->groundEntityNum == ENTITYNUM_NONE ) + return qfalse; + + // Give the player forward velocity and simulate a jump + pml.groundPlane = qfalse; + pml.walking = qfalse; + pm->ps->pm_flags |= PMF_CHARGE; + pm->ps->groundEntityNum = ENTITYNUM_NONE; + if( pm->ps->weapon == WP_ALEVEL3 ) + jumpMagnitude = pm->ps->stats[ STAT_MISC ] * + LEVEL3_POUNCE_JUMP_MAG / LEVEL3_POUNCE_TIME; + else + jumpMagnitude = pm->ps->stats[ STAT_MISC ] * + LEVEL3_POUNCE_JUMP_MAG_UPG / LEVEL3_POUNCE_TIME_UPG; + VectorMA( pm->ps->velocity, jumpMagnitude, pml.forward, pm->ps->velocity ); + PM_AddEvent( EV_JUMP ); + + // Play jumping 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->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, movedir, point; + vec3_t refNormal = { 0.0f, 0.0f, 1.0f }; + float normalFraction = 1.5f; + float cmdFraction = 1.0f; + float upFraction = 1.5f; + trace_t trace; + + if( !( BG_Class( pm->ps->stats[ STAT_CLASS ] )->abilities & SCA_WALLJUMPER ) ) + return qfalse; + + 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.plane.normal[ 2 ] < MIN_WALK_NORMAL ) + { + if( !VectorCompare( trace.plane.normal, pm->ps->grapplePoint ) ) + { + VectorCopy( trace.plane.normal, pm->ps->grapplePoint ); + } + } + else + 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; + + 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_Class( pm->ps->stats[ STAT_CLASS ] )->jumpMagnitude, + 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 ) +{ + vec3_t normal; + qboolean jetjump; +//ZdrytchX: Uhh... Okay then... + int jumpvel; + + if( pm->ps->groundEntityNum == ENTITYNUM_NONE ) + return qfalse; + + if( BG_Class( pm->ps->stats[ STAT_CLASS ] )->jumpMagnitude == 0.0f ) + return qfalse; + + //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_TEAM ] == TEAM_HUMANS ) && + ( pm->ps->stats[ STAT_STAMINA ] < STAMINA_SLOW_LEVEL + STAMINA_JUMP_TAKE ) && + ( !BG_InventoryContainsUpgrade( UP_JETPACK, pm->ps->stats ) || pm->ps->stats[ STAT_FUEL ] < JETPACK_FUEL_JUMP ) ) + return qfalse; + + //no bunny hopping off a dodge + if( pm->ps->stats[ STAT_TEAM ] == TEAM_HUMANS && + pm->ps->pm_time ) + 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; + } + + //don't allow walljump for a short while after jumping from the ground + if( BG_ClassHasAbility( pm->ps->stats[ STAT_CLASS ], SCA_WALLJUMPER ) ) + { + 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; + + // true if using the jetpack to jump (doesn't use stamina, only fuel) + jetjump = ( BG_InventoryContainsUpgrade( UP_JETPACK, pm->ps->stats ) && + pm->ps->stats[ STAT_FUEL ] >= JETPACK_FUEL_JUMP ); + + if( pm->ps->stats[ STAT_TEAM ] == TEAM_HUMANS ) + { + if( !jetjump ) + pm->ps->stats[ STAT_STAMINA ] -= STAMINA_JUMP_TAKE; + else + pm->ps->stats[ STAT_FUEL ] -= JETPACK_FUEL_JUMP; + } + +//Alright, let's start the jumping process. + jumpvel = BG_Class( pm->ps->stats[ STAT_CLASS ] )->jumpMagnitude; + + //ZdrytchX: check for double-jump + if(pm->ps->stats[ STAT_STAMINA ] > 0 //derp doesn't have STAMINA_MIN_TO_JUMP :/ + || pm->ps->stats[ STAT_TEAM ] == pm->ps->stats[ STAT_TEAM ] == TEAM_ALIENS + && !BG_ClassHasAbility( pm->ps->stats[ STAT_CLASS ], SCA_WALLJUMPER ) ) + //Trust me. You don't want marauders flying out of the map from 3 wall jumps. + if (cpm_pm_jump_z) { + if (pm->ps->persistant[PERS_JUMPTIME] > 0) { + jumpvel += (cpm_pm_jump_z * BG_Class( pm->ps->stats[ STAT_CLASS ] )->jumpMagnitude); + //Adding requires me to multiply by class vel again + } + pm->ps->persistant[PERS_JUMPTIME] = 400; + } + + pm->ps->groundEntityNum = ENTITYNUM_NONE; + + // jump away from wall + BG_GetClientNormal( pm->ps, normal ); + + if( pm->ps->velocity[ 2 ] < 0 ) + pm->ps->velocity[ 2 ] = 0; + + VectorMA( pm->ps->velocity, jumpvel, + normal, pm->ps->velocity ); + + if( jetjump ) + PM_AddEvent( EV_JETJUMP ); + else + 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_AirControl + +=================== +*/ +static void PM_AirControl(vec3_t wishdir, float wishspeed) +{ + float zspeed, speed, dot, k; + int i; + + if((pm->ps->movementDir && pm->ps->movementDir != 4) || wishspeed == 0.0) + return; // can't control movement if not moving forward or backward + + zspeed = pm->ps->velocity[2]; + pm->ps->velocity[2] = 0; + speed = VectorNormalize(pm->ps->velocity); + + dot = DotProduct(pm->ps->velocity, wishdir); + k = 32; + k *= pm_airControlAmount * BG_Class( pm->ps->stats[ STAT_CLASS ] )->airAcceleration * dot * dot * pml.frametime; + + if(dot > 0) + { + // we can't change direction while slowing down + for(i = 0; i < 2; i++) + pm->ps->velocity[i] = pm->ps->velocity[i] * speed + wishdir[i] * k; + VectorNormalize(pm->ps->velocity); + } + + for(i = 0; i < 2; i++) + pm->ps->velocity[i] *= speed; + + pm->ps->velocity[2] = zspeed; +} + +/* +=================== +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; + + float accel; // CPM + float wishspeed2; // CPM + float velscale;//classes temp.var + + velscale = BG_Class( pm->ps->stats[ STAT_CLASS ] )->airAcceleration; + + PM_CheckWallJump( ); + 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; + + if (DotProduct(pm->ps->velocity, wishdir) < 0 && (pm_airaccelerate * velscale) < cpm_pm_airstopaccelerate )// Stop marauders from climbing walls easily + accel = cpm_pm_airstopaccelerate * velscale; + +//ZdrytchX: Use side strafes for Q1 style turning + if (pm->ps->movementDir == 2 || pm->ps->movementDir == 6 ) //Detect for side strafe buttons + { + wishspeed = cpm_pm_wishspeed * velscale; + accel = cpm_pm_strafeaccelerate * velscale; + PM_Accelerate (wishdir, wishspeed, accel); + } + else //accelerte normally + { + PM_Accelerate( wishdir, wishspeed, velscale ); + } + + // cpma air control + PM_AirControl (wishdir, wishspeed); + + // 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_Class( pm->ps->stats[ STAT_CLASS ] )->airAcceleration; + else + accelerate = BG_Class( pm->ps->stats[ STAT_CLASS ] )->acceleration; + + 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_Class( pm->ps->stats[ STAT_CLASS ] )->airAcceleration; + else + accelerate = BG_Class( pm->ps->stats[ STAT_CLASS ] )->acceleration; + + 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_CLASS ], 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; + + // 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 ) +{ + 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; + + // 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 ) + { + if( PM_Live( pm->ps->pm_type ) ) + PM_AddEvent( EV_FALL_FAR ); + } + else if( delta > MIN_FALL_DISTANCE ) + { + if( PM_Live( pm->ps->pm_type ) ) + 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_CLASS ], 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; + const float eps = 0.000001f; + + //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; + + BG_GetClientNormal( pm->ps, 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 + //mask out CONTENTS_BODY to not hit other players and avoid the camera flipping out when + // wallwalkers touch + VectorMA( pm->ps->origin, -0.25f, surfNormal, point ); + pm->trace( &trace, pm->ps->origin, pm->mins, pm->maxs, point, pm->ps->clientNum, pm->tracemask /*& ~CONTENTS_BODY*/ ); + 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( !VectorCompareEpsilon( trace.plane.normal, surfNormal, eps ) ) + { + //if the trace result or the old vector is not the floor or ceiling correct the YAW angle + if( !VectorCompareEpsilon( trace.plane.normal, refNormal, eps ) && !VectorCompareEpsilon( surfNormal, refNormal, eps ) && + !VectorCompareEpsilon( trace.plane.normal, ceilingNormal, eps ) && !VectorCompareEpsilon( surfNormal, ceilingNormal, eps ) ) + { + //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( VectorCompareEpsilon( trace.plane.normal, ceilingNormal, eps ) ) + { + CrossProduct( surfNormal, trace.plane.normal, pm->ps->grapplePoint ); + VectorNormalize( pm->ps->grapplePoint ); + pm->ps->eFlags |= EF_WALLCLIMBCEILING; + } + + //transition from ceiling to wall + //we need to do some different angle correction here cos GPISROTVEC + if( VectorCompareEpsilon( surfNormal, ceilingNormal, eps ) ) + { + 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( !VectorCompareEpsilon( trace.plane.normal, ceilingNormal, eps ) ) + { + //so we know what surface we're stuck to + VectorCopy( trace.plane.normal, pm->ps->grapplePoint ); + pm->ps->eFlags &= ~EF_WALLCLIMBCEILING; + } + + //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->eFlags & EF_WALLCLIMBCEILING ) + { + 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->eFlags &= ~EF_WALLCLIMBCEILING; + + //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; + 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 refNormal = { 0.0f, 0.0f, 1.0f }; + trace_t trace; + + if( BG_ClassHasAbility( pm->ps->stats[ STAT_CLASS ], 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->eFlags & EF_WALLCLIMBCEILING ) + { + 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->eFlags &= ~( EF_WALLCLIMB | EF_WALLCLIMBCEILING ); + + 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( ) && pm->ps->velocity[ 2 ] > 0.0f ) + { + //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; + + 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; + 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 ); + + // communicate the fall velocity to the server + pm->pmext->fallVelocity = pml.previous_velocity[ 2 ]; + + if( BG_ClassHasAbility( pm->ps->stats[ STAT_CLASS ], SCA_TAKESFALLDAMAGE ) ) + PM_CrashLand( ); + } + + 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_SetViewheight +============== +*/ +static void PM_SetViewheight( void ) +{ + pm->ps->viewheight = ( pm->ps->pm_flags & PMF_DUCKED ) + ? BG_ClassConfig( pm->ps->stats[ STAT_CLASS ] )->crouchViewheight + : BG_ClassConfig( pm->ps->stats[ STAT_CLASS ] )->viewheight; +} + +/* +============== +PM_CheckDuck + +Sets mins and maxs, and calls PM_SetViewheight +============== +*/ +static void PM_CheckDuck (void) +{ + trace_t trace; + vec3_t PCmins, PCmaxs, PCcmaxs; + + BG_ClassBoundingBox( pm->ps->stats[ STAT_CLASS ], PCmins, PCmaxs, PCcmaxs, NULL, NULL ); + + 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 = PCmins[ 2 ] + DEAD_VIEWHEIGHT; + return; + } + + // If the standing and crouching bboxes are the same the class can't crouch + if( ( pm->cmd.upmove < 0 ) && !VectorCompare( PCmaxs, PCcmaxs ) && + pm->ps->pm_type != PM_JETPACK ) + { + // 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 ]; + else + pm->maxs[ 2 ] = PCmaxs[ 2 ]; + + PM_SetViewheight( ); +} + + + +//=================================================================== + + +/* +=============== +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_CLASS ], SCA_WALLCLIMBER ) && ( pml.groundPlane ) ) + { + // 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_Class( pm->ps->stats[ STAT_CLASS ] )->bobCycle; + + 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; + + // cancel a reload + pm->ps->pm_flags &= ~PMF_WEAPON_RELOAD; + if( pm->ps->weaponstate == WEAPON_RELOADING ) + pm->ps->weaponTime = 0; + + //special case to prevent storing a charged up lcannon + if( pm->ps->weapon == WP_LUCIFER_CANNON ) + pm->ps->stats[ STAT_MISC ] = 0; + + 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_StartWeaponAnim( WANIM_DROP ); + } +} + + +/* +=============== +PM_FinishWeaponChange +=============== +*/ +static void PM_FinishWeaponChange( void ) +{ + int weapon; + + PM_AddEvent( EV_CHANGE_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_StartWeaponAnim( WANIM_RAISE ); + } +} + + +/* +============== +PM_TorsoAnimation + +============== +*/ +static void PM_TorsoAnimation( void ) +{ + if( 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 ); + } + + PM_ContinueWeaponAnim( WANIM_IDLE ); + } +} + + +/* +============== +PM_Weapon + +Generates weapon events and modifes the weapon counter +============== +*/ +static void PM_Weapon( void ) +{ + int addTime = 200; //default addTime - should never be used + qboolean attack1 = pm->cmd.buttons & BUTTON_ATTACK; + qboolean attack2 = pm->cmd.buttons & BUTTON_ATTACK2; + qboolean attack3 = pm->cmd.buttons & BUTTON_USE_HOLDABLE; + + // Ignore weapons in some cases + if( pm->ps->persistant[ PERS_SPECSTATE ] != SPECTATOR_NOT ) + return; + + // Check for dead player + if( pm->ps->stats[ STAT_HEALTH ] <= 0 ) + { + pm->ps->weapon = WP_NONE; + return; + } + + // Charging for a pounce or canceling a pounce + if( pm->ps->weapon == WP_ALEVEL3 || pm->ps->weapon == WP_ALEVEL3_UPG ) + { + int max; + + max = pm->ps->weapon == WP_ALEVEL3 ? LEVEL3_POUNCE_TIME : + LEVEL3_POUNCE_TIME_UPG; + if( pm->cmd.buttons & BUTTON_ATTACK2 ) + pm->ps->stats[ STAT_MISC ] += pml.msec; + else + pm->ps->stats[ STAT_MISC ] -= pml.msec; + + if( pm->ps->stats[ STAT_MISC ] > max ) + pm->ps->stats[ STAT_MISC ] = max; + else if( pm->ps->stats[ STAT_MISC ] < 0 ) + pm->ps->stats[ STAT_MISC ] = 0; + } + + // Trample charge mechanics + if( pm->ps->weapon == WP_ALEVEL4 ) + { + // Charging up + if( !( pm->ps->stats[ STAT_STATE ] & SS_CHARGING ) ) + { + // Charge button held + if( pm->ps->stats[ STAT_MISC ] < LEVEL4_TRAMPLE_CHARGE_TRIGGER && + ( pm->cmd.buttons & BUTTON_ATTACK2 ) ) + { + pm->ps->stats[ STAT_STATE ] &= ~SS_CHARGING; + if( pm->cmd.forwardmove > 0 ) + { + int charge = pml.msec; + vec3_t dir,vel; + + AngleVectors( pm->ps->viewangles, dir, NULL, NULL ); + VectorCopy( pm->ps->velocity, vel ); + vel[2] = 0; + dir[2] = 0; + VectorNormalize( vel ); + VectorNormalize( dir ); + + charge *= DotProduct( dir, vel ); + + pm->ps->stats[ STAT_MISC ] += charge; + } + else + pm->ps->stats[ STAT_MISC ] = 0; + } + + // Charge button released + else if( !( pm->ps->stats[ STAT_STATE ] & SS_CHARGING ) ) + { + if( pm->ps->stats[ STAT_MISC ] > LEVEL4_TRAMPLE_CHARGE_MIN ) + { + if( pm->ps->stats[ STAT_MISC ] > LEVEL4_TRAMPLE_CHARGE_MAX ) + pm->ps->stats[ STAT_MISC ] = LEVEL4_TRAMPLE_CHARGE_MAX; + pm->ps->stats[ STAT_MISC ] = pm->ps->stats[ STAT_MISC ] * + LEVEL4_TRAMPLE_DURATION / + LEVEL4_TRAMPLE_CHARGE_MAX; + pm->ps->stats[ STAT_STATE ] |= SS_CHARGING; + PM_AddEvent( EV_LEV4_TRAMPLE_START ); + } + else + pm->ps->stats[ STAT_MISC ] -= pml.msec; + } + } + + // Discharging + else + { + if( pm->ps->stats[ STAT_MISC ] < LEVEL4_TRAMPLE_CHARGE_MIN ) + pm->ps->stats[ STAT_MISC ] = 0; + else + pm->ps->stats[ STAT_MISC ] -= pml.msec; + + // If the charger has stopped moving take a chunk of charge away + if( VectorLength( pm->ps->velocity ) < 64.0f || pm->cmd.rightmove ) + pm->ps->stats[ STAT_MISC ] -= LEVEL4_TRAMPLE_STOP_PENALTY * pml.msec; + } + + // Charge is over + if( pm->ps->stats[ STAT_MISC ] <= 0 || pm->cmd.forwardmove <= 0 ) + { + pm->ps->stats[ STAT_MISC ] = 0; + pm->ps->stats[ STAT_STATE ] &= ~SS_CHARGING; + } + } + + // Charging up a Lucifer Cannon + pm->ps->eFlags &= ~EF_WARN_CHARGE; + if( pm->ps->weapon == WP_LUCIFER_CANNON ) + { + // Charging up + if( !pm->ps->weaponTime && pm->ps->weaponstate != WEAPON_NEEDS_RESET && + ( pm->cmd.buttons & BUTTON_ATTACK ) ) + { + pm->ps->stats[ STAT_MISC ] += pml.msec; + if( pm->ps->stats[ STAT_MISC ] >= LCANNON_CHARGE_TIME_MAX ) + pm->ps->stats[ STAT_MISC ] = LCANNON_CHARGE_TIME_MAX; + if( pm->ps->stats[ STAT_MISC ] > pm->ps->ammo * LCANNON_CHARGE_TIME_MAX / + LCANNON_CHARGE_AMMO ) + pm->ps->stats[ STAT_MISC ] = pm->ps->ammo * LCANNON_CHARGE_TIME_MAX / + LCANNON_CHARGE_AMMO; + } + + // Set overcharging flag so other players can hear the warning beep + if( pm->ps->stats[ STAT_MISC ] > LCANNON_CHARGE_TIME_WARN ) + pm->ps->eFlags |= EF_WARN_CHARGE; + } + + // don't allow attack until all buttons are up + if( pm->ps->pm_flags & PMF_RESPAWNED ) + 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; + + // pump weapon delays (repeat times etc) + if( pm->ps->weaponTime > 0 ) + pm->ps->weaponTime -= pml.msec; + if( pm->ps->weaponTime < 0 ) + pm->ps->weaponTime = 0; + + // no slash during charge + if( pm->ps->stats[ STAT_STATE ] & SS_CHARGING ) + return; + + // check for weapon change + // can't change if weapon is firing, but can change + // again if lowering or raising + if( BG_PlayerCanChangeWeapon( pm->ps ) ) + { + // 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 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; + if( pm->ps->weapon != WP_NONE ) + { + // drop the current weapon + PM_BeginWeaponChange( pm->ps->persistant[ PERS_NEWWEAPON ] ); + } + else + { + // no current weapon, so just raise the new one + PM_FinishWeaponChange( ); + } + } + } + + if( pm->ps->weaponTime > 0 ) + return; + + // change weapon if time + if( pm->ps->weaponstate == WEAPON_DROPPING ) + { + PM_FinishWeaponChange( ); + return; + } + + // Set proper animation + 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 ); + } + + PM_ContinueWeaponAnim( WANIM_IDLE ); + + return; + } + + // check for out of ammo + if( !pm->ps->ammo && !pm->ps->clips && !BG_Weapon( pm->ps->weapon )->infiniteAmmo ) + { + if( attack1 || + ( BG_Weapon( pm->ps->weapon )->hasAltMode && attack2 ) || + ( BG_Weapon( pm->ps->weapon )->hasThirdMode && attack3 ) ) + { + PM_AddEvent( EV_NOAMMO ); + pm->ps->weaponTime += 500; + } + + 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 ) + { + pm->ps->clips--; + pm->ps->ammo = BG_Weapon( pm->ps->weapon )->maxAmmo; + + if( BG_Weapon( pm->ps->weapon )->usesEnergy && + BG_InventoryContainsUpgrade( UP_BATTPACK, pm->ps->stats ) ) + pm->ps->ammo *= BATTPACK_MODIFIER; + + //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( !BG_Weapon( pm->ps->weapon )->infiniteAmmo && + ( pm->ps->ammo <= 0 || ( pm->ps->pm_flags & PMF_WEAPON_RELOAD ) ) && + pm->ps->clips > 0 ) + { + pm->ps->pm_flags &= ~PMF_WEAPON_RELOAD; + pm->ps->weaponstate = WEAPON_RELOADING; + + //drop the weapon + PM_StartTorsoAnim( TORSO_DROP ); + PM_StartWeaponAnim( WANIM_RELOAD ); + + pm->ps->weaponTime += BG_Weapon( pm->ps->weapon )->reloadTime; + return; + } + + //check if non-auto primary/secondary attacks are permited + switch( pm->ps->weapon ) + { + case WP_ALEVEL0: + //venom is only autohit + return; + + case WP_ALEVEL3: + case WP_ALEVEL3_UPG: + //pouncing has primary secondary AND autohit procedures + // pounce is autohit + if( !attack1 && !attack2 && !attack3 ) + return; + break; + + case WP_LUCIFER_CANNON: + attack3 = qfalse; + + // Prevent firing of the Lucifer Cannon after an overcharge + if( pm->ps->weaponstate == WEAPON_NEEDS_RESET ) + { + if( attack1 ) + return; + pm->ps->weaponstate = WEAPON_READY; + } + + // Can't fire secondary while primary is charging + if( attack1 || pm->ps->stats[ STAT_MISC ] > 0 ) + attack2 = qfalse; + + if( ( attack1 || pm->ps->stats[ STAT_MISC ] == 0 ) && !attack2 ) + { + pm->ps->weaponTime = 0; + + // Charging + if( pm->ps->stats[ STAT_MISC ] < LCANNON_CHARGE_TIME_MAX ) + { + pm->ps->weaponstate = WEAPON_READY; + return; + } + + // Overcharge + pm->ps->weaponstate = WEAPON_NEEDS_RESET; + } + + if( pm->ps->stats[ STAT_MISC ] > LCANNON_CHARGE_TIME_MIN ) + { + // Fire primary attack + attack1 = qtrue; + attack2 = qfalse; + } + else if( pm->ps->stats[ STAT_MISC ] > 0 ) + { + // Not enough charge + pm->ps->stats[ STAT_MISC ] = 0; + pm->ps->weaponTime = 0; + pm->ps->weaponstate = WEAPON_READY; + return; + } + else if( !attack2 ) + { + // Idle + pm->ps->weaponTime = 0; + pm->ps->weaponstate = WEAPON_READY; + return; + } + break; + + case WP_MASS_DRIVER: + attack2 = attack3 = qfalse; + // 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: + if( !attack1 && !attack2 && !attack3 ) + { + pm->ps->weaponTime = 0; + pm->ps->weaponstate = WEAPON_READY; + return; + } + break; + } + + // fire events for non auto weapons + if( attack3 ) + { + if( BG_Weapon( pm->ps->weapon )->hasThirdMode ) + { + //hacky special case for slowblob + if( ( pm->ps->weapon == WP_ALEVEL3_UPG || + pm->ps->weapon == WP_ALEVEL4 ) + && !pm->ps->ammo ) + { + pm->ps->weaponTime += 200; + return; + } + + pm->ps->generic1 = WPM_TERTIARY; + PM_AddEvent( EV_FIRE_WEAPON3 ); + addTime = BG_Weapon( pm->ps->weapon )->repeatRate3; + } + else + { + pm->ps->weaponTime = 0; + pm->ps->weaponstate = WEAPON_READY; + pm->ps->generic1 = WPM_NOTFIRING; + return; + } + } + else if( attack2 ) + { + if( BG_Weapon( pm->ps->weapon )->hasAltMode ) + { + pm->ps->generic1 = WPM_SECONDARY; + PM_AddEvent( EV_FIRE_WEAPON2 ); + addTime = BG_Weapon( pm->ps->weapon )->repeatRate2; + } + 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_Weapon( pm->ps->weapon )->repeatRate1; + } + + // 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_Weapon( pm->ps->weapon )->repeatRate1; + break; + + case WP_ALEVEL3: + case WP_ALEVEL3_UPG: + pm->ps->generic1 = WPM_SECONDARY; + PM_AddEvent( EV_FIRE_WEAPON2 ); + addTime = BG_Weapon( pm->ps->weapon )->repeatRate2; + 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 ); + PM_StartWeaponAnim( WANIM_ATTACK1 ); + } + break; + + case WP_BLASTER: + PM_StartTorsoAnim( TORSO_ATTACK2 ); + PM_StartWeaponAnim( WANIM_ATTACK1 ); + break; + + default: + PM_StartTorsoAnim( TORSO_ATTACK ); + PM_StartWeaponAnim( WANIM_ATTACK1 ); + break; + } + } + else + { + int num = rand( ); + + //FIXME: it would be nice to have these hard coded policies in + // weapon.cfg + switch( pm->ps->weapon ) + { + case WP_ALEVEL1_UPG: + case WP_ALEVEL1: + if( attack1 ) + { + num /= RAND_MAX / 6 + 1; + PM_ForceLegsAnim( NSPA_ATTACK1 ); + PM_StartWeaponAnim( WANIM_ATTACK1 + num ); + } + break; + + case WP_ALEVEL2_UPG: + if( attack2 ) + { + PM_ForceLegsAnim( NSPA_ATTACK2 ); + PM_StartWeaponAnim( WANIM_ATTACK7 ); + } + case WP_ALEVEL2: + if( attack1 ) + { + num /= RAND_MAX / 6 + 1; + PM_ForceLegsAnim( NSPA_ATTACK1 ); + PM_StartWeaponAnim( WANIM_ATTACK1 + num ); + } + break; + + case WP_ALEVEL4: + num /= RAND_MAX / 3 + 1; + PM_ForceLegsAnim( NSPA_ATTACK1 + num ); + PM_StartWeaponAnim( WANIM_ATTACK1 + num ); + break; + + default: + if( attack1 ) + { + PM_ForceLegsAnim( NSPA_ATTACK1 ); + PM_StartWeaponAnim( WANIM_ATTACK1 ); + } + else if( attack2 ) + { + PM_ForceLegsAnim( NSPA_ATTACK2 ); + PM_StartWeaponAnim( WANIM_ATTACK2 ); + } + else if( attack3 ) + { + PM_ForceLegsAnim( NSPA_ATTACK3 ); + PM_StartWeaponAnim( WANIM_ATTACK3 ); + } + break; + } + + pm->ps->torsoTimer = TIMER_ATTACK; + } + + if( pm->ps->weaponstate != WEAPON_NEEDS_RESET ) + pm->ps->weaponstate = WEAPON_FIRING; + + // take an ammo away if not infinite + if( !BG_Weapon( pm->ps->weapon )->infiniteAmmo || + ( pm->ps->weapon == WP_ALEVEL3_UPG && attack3 ) || + ( pm->ps->weapon == WP_ALEVEL4 && attack3 )) + { + // Special case for lcannon + if( pm->ps->weapon == WP_LUCIFER_CANNON && attack1 && !attack2 ) + pm->ps->ammo -= ( pm->ps->stats[ STAT_MISC ] * LCANNON_CHARGE_AMMO + + LCANNON_CHARGE_TIME_MAX - 1 ) / LCANNON_CHARGE_TIME_MAX; + else + pm->ps->ammo--; + + // Stay on the safe side + if( pm->ps->ammo < 0 ) + pm->ps->ammo = 0; + } + + //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_Paralyzed( pm->ps->pm_type ) ) + return; + + if( pm->cmd.buttons & BUTTON_GESTURE ) + { + if( pm->ps->tauntTimer > 0 ) + return; + + if( !( pm->ps->persistant[ PERS_STATE ] & PS_NONSEGMODEL ) ) + { + if( pm->ps->torsoTimer == 0 ) + { + PM_StartTorsoAnim( TORSO_GESTURE ); + pm->ps->torsoTimer = TIMER_GESTURE; + pm->ps->tauntTimer = TIMER_GESTURE; + + PM_AddEvent( EV_TAUNT ); + } + } + else + { + if( pm->ps->torsoTimer == 0 ) + { + PM_ForceLegsAnim( NSPA_GESTURE ); + pm->ps->torsoTimer = TIMER_GESTURE; + pm->ps->tauntTimer = 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; + } + + if( pm->ps->tauntTimer > 0 ) + { + pm->ps->tauntTimer -= pml.msec; + + if( pm->ps->tauntTimer < 0 ) + { + pm->ps->tauntTimer = 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 ) + 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++ ) + { + if( i == ROLL ) + { + // Guard against speed hack + temp[ i ] = ps->delta_angles[ i ]; + +#ifdef CGAME + // Assert here so that if cmd->angles[ i ] becomes non-zero + // for a legitimate reason we can tell where and why it's + // being ignored + assert( cmd->angles[ i ] == 0 ); +#endif + + } + else + 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->eFlags & EF_WALLCLIMBCEILING ) ) + 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 ) +{ + pm = pmove; + + // 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; + + // 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 firing flag for continuous beam weapons + if( !(pm->ps->pm_flags & PMF_RESPAWNED) && pm->ps->pm_type != PM_INTERMISSION && + ( pm->cmd.buttons & BUTTON_ATTACK ) && + ( ( pm->ps->ammo > 0 || pm->ps->clips > 0 ) || BG_Weapon( pm->ps->weapon )->infiniteAmmo ) ) + 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 ) && + ( ( pm->ps->ammo > 0 || pm->ps->clips > 0 ) || BG_Weapon( pm->ps->weapon )->infiniteAmmo ) ) + 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 ) && + ( ( pm->ps->ammo > 0 || pm->ps->clips > 0 ) || BG_Weapon( pm->ps->weapon )->infiniteAmmo ) ) + 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; + + if( pmove->cmd.upmove > 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_Paralyzed( pm->ps->pm_type ) ) + { + 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_SetViewheight( ); + PM_Weapon( ); + PM_DropTimers( ); + return; + } + + if( pm->ps->pm_type == PM_FREEZE) + return; // no movement at all + + if( pm->ps->pm_type == PM_INTERMISSION ) + 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( ); + //Incremenet jumptime status + if (pm->ps->persistant[PERS_JUMPTIME] > 0) pm->ps->persistant[PERS_JUMPTIME] -= pml.msec; + + 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_CLASS ], SCA_WALLCLIMBER ) && + ( pm->ps->stats[ STAT_STATE ] & SS_WALLCLIMBING ) ) + PM_ClimbMove( ); // walking on any surface + else + PM_WalkMove( ); // walking on ground + } + else + PM_AirMove( ); + + PM_Animate( ); + + // set groundentity, watertype, and waterlevel + PM_GroundTrace( ); + + // 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..9355ac6 --- /dev/null +++ b/src/game/bg_public.h @@ -0,0 +1,1409 @@ +/* +=========================================================================== +Copyright (C) 1999-2005 Id Software, Inc. +Copyright (C) 2000-2009 Darklegion Development + +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 4 // height from ground + +#define COMPARE_FLOAT_EPSILON(a,b) ((a)>(b)-0.0001f&&(a)<(b)+0.0001f) +#define COMPARE_VECTOR_EPSILON(v,x,y,z) (COMPARE_FLOAT_EPSILON((v)[0],(x))&&COMPARE_FLOAT_EPSILON((v)[1],(y))&&COMPARE_FLOAT_EPSILON((v)[2],(z))) + +// player teams +typedef enum +{ + TEAM_NONE, + TEAM_ALIENS, + TEAM_HUMANS, + + NUM_TEAMS +} team_t; + +// +// 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 +enum +{ + CS_MUSIC = 2, + CS_MESSAGE, // from the map worldspawn's message field + CS_MOTD, // g_motd string for server message of the day + CS_WARMUP, // server time when the match will be restarted + + CS_VOTE_TIME, // Vote stuff each needs NUM_TEAMS slots + CS_VOTE_STRING = CS_VOTE_TIME + NUM_TEAMS, + CS_VOTE_YES = CS_VOTE_STRING + NUM_TEAMS, + CS_VOTE_NO = CS_VOTE_YES + NUM_TEAMS, + CS_VOTE_CALLER = CS_VOTE_NO + NUM_TEAMS, + + CS_GAME_VERSION = CS_VOTE_CALLER + NUM_TEAMS, + CS_LEVEL_START_TIME, // so the timer only shows the current level + CS_INTERMISSION, // when 1, fraglimit/timelimit has been hit and intermission will start in a second or two + CS_WINNER , // string indicating round winner + CS_SHADERSTATE, + CS_BOTINFO, + CS_CLIENTS_READY, + + CS_ALIEN_STAGES, + CS_HUMAN_STAGES, + + CS_MODELS, + CS_SOUNDS = CS_MODELS + MAX_MODELS, + CS_SHADERS = CS_SOUNDS + MAX_SOUNDS, + CS_PARTICLE_SYSTEMS = CS_SHADERS + MAX_GAME_SHADERS, + CS_PLAYERS = CS_PARTICLE_SYSTEMS + MAX_GAME_PARTICLE_SYSTEMS, + CS_LOCATIONS = CS_PLAYERS + MAX_CLIENTS, + + 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 +} pmtype_t; + +// pmtype_t categories +#define PM_Paralyzed( x ) ( (x) == PM_DEAD || (x) == PM_FREEZE ||\ + (x) == PM_INTERMISSION ) +#define PM_Live( x ) ( (x) == PM_NORMAL || (x) == PM_JETPACK ||\ + (x) == PM_GRABBED ) + +typedef enum +{ + WEAPON_READY, + WEAPON_RAISING, + WEAPON_DROPPING, + WEAPON_FIRING, + WEAPON_RELOADING, + WEAPON_NEEDS_RESET, +} weaponstate_t; + +// pmove->pm_flags +#define PMF_DUCKED 0x000001 +#define PMF_JUMP_HELD 0x000002 +#define PMF_CROUCH_HELD 0x000004 +#define PMF_BACKWARDS_JUMP 0x000008 // go into backwards land +#define PMF_BACKWARDS_RUN 0x000010 // coast down to backwards run +#define PMF_TIME_LAND 0x000020 // pm_time is time before rejump +#define PMF_TIME_KNOCKBACK 0x000040 // pm_time is an air-accelerate only time +#define PMF_TIME_WATERJUMP 0x000080 // pm_time is waterjump +#define PMF_RESPAWNED 0x000100 // clear after attack and jump buttons come up +#define PMF_USE_ITEM_HELD 0x000200 +#define PMF_WEAPON_RELOAD 0x000400 // force a weapon switch +#define PMF_FOLLOW 0x000800 // spectate following another player +#define PMF_QUEUED 0x001000 // player is queued +#define PMF_TIME_WALLJUMP 0x002000 // for limiting wall jumping +#define PMF_CHARGE 0x004000 // keep track of pouncing +#define PMF_WEAPON_SWITCH 0x008000 // force a weapon switch +#define PMF_SPRINTHELD 0x010000 + + +#define PMF_ALL_TIMES (PMF_TIME_WATERJUMP|PMF_TIME_LAND|PMF_TIME_KNOCKBACK|PMF_TIME_WALLJUMP) + +typedef struct +{ + int pouncePayload; + float fallVelocity; +} pmoveExt_t; + +#define MAXTOUCH 32 +typedef struct pmove_s +{ + // 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 ]; + + 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_ACTIVEITEMS, + STAT_WEAPON, // current primary weapon + STAT_MAX_HEALTH,// health / armor limit, changable by handicap + STAT_CLASS, // player class (for aliens AND humans) + STAT_TEAM, // player team + STAT_STAMINA, // stamina (human only) + STAT_STATE, // client states e.g. wall climbing + STAT_MISC, // for uh...misc stuff (pounce, trample, lcannon) + STAT_BUILDABLE, // which ghost model to display for building + STAT_FALLDIST, // the distance the player fell + STAT_VIEWLOCK, // direction to lock the view in + STAT_FUEL // jetpacks + // netcode has space for 2 more +} statIndex_t; + +#define SCA_WALLCLIMBER 0x00000001 +#define SCA_TAKESFALLDAMAGE 0x00000002 +#define SCA_CANZOOM 0x00000004 +#define SCA_FOVWARPS 0x00000008 +#define SCA_ALIENSENSE 0x00000010 +#define SCA_CANUSELADDERS 0x00000020 +#define SCA_WALLJUMPER 0x00000040 + +#define SS_WALLCLIMBING 0x00000001 +#define SS_CREEPSLOWED 0x00000002 +#define SS_SPEEDBOOST 0x00000004 +#define SS_GRABBED 0x00000008 +#define SS_BLOBLOCKED 0x00000010 +#define SS_POISONED 0x00000020 +#define SS_BOOSTED 0x00000040 +#define SS_BOOSTEDWARNING 0x00000080 // booster poison is running out +#define SS_SLOWLOCKED 0x00000100 +#define SS_CHARGING 0x00000200 +#define SS_HEALING_ACTIVE 0x00000400 // medistat for humans, creep for aliens +#define SS_HEALING_2X 0x00000800 // medkit or double healing rate +#define SS_HEALING_3X 0x00001000 // triple healing rate + +#define SB_VALID_TOGGLEBIT 0x00002000 + +// 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_SPAWNS, // how many spawns your team has + PERS_SPECSTATE, + PERS_SPAWN_COUNT, // incremented every respawn + PERS_ATTACKER, // clientnum of last damage inflicter + PERS_KILLED, // count of the number of times you died + + PERS_STATE, + PERS_CREDIT, // human credit + PERS_QUEUEPOS, // position in the spawn queue + PERS_NEWWEAPON, // weapon to switch to + PERS_BP, + PERS_MARKEDBP, + //zdrytchx: no space in stats, use persistant. This meanas we risk doing a double jump upon spawning but death animations are 1700 msecs long, so technically it's impossible anyway + PERS_JUMPTIME, + PERS_SPAWNS_IMPLANTED + // netcode has space for 1 more +} persEnum_t; + +#define PS_WALLCLIMBINGFOLLOW 0x00000001 +#define PS_WALLCLIMBINGTOGGLE 0x00000002 +#define PS_NONSEGMODEL 0x00000004 +#define PS_SPRINTTOGGLE 0x00000008 + +// entityState_t->eFlags +// notice that some flags are overlapped, so their meaning depends on context +#define EF_DEAD 0x0001 // don't draw a foe marker over players with EF_DEAD +#define EF_TELEPORT_BIT 0x0002 // toggled every time the origin abruptly changes +#define EF_PLAYER_EVENT 0x0004 // only used for eType > ET_EVENTS + +// for missiles: +#define EF_BOUNCE 0x0008 // for missiles +#define EF_BOUNCE_HALF 0x0010 // for missiles +#define EF_NO_BOUNCE_SOUND 0x0020 // for missiles + +// buildable flags: +#define EF_B_SPAWNED 0x0008 +#define EF_B_POWERED 0x0010 +#define EF_B_MARKED 0x0020 + +#define EF_WARN_CHARGE 0x0020 // Lucifer Cannon is about to overcharge +#define EF_WALLCLIMB 0x0040 // wall walking +#define EF_WALLCLIMBCEILING 0x0080 // wall walking ceiling hack +#define EF_NODRAW 0x0100 // may have an event, but no model (unspawned items) +#define EF_FIRING 0x0200 // for lightning gun +#define EF_FIRING2 0x0400 // alt fire +#define EF_FIRING3 0x0800 // third fire +#define EF_MOVER_STOP 0x1000 // will push otherwise +#define EF_POISONCLOUDED 0x2000 // player hit with basilisk gas +#define EF_CONNECTION 0x4000 // draw a connection trouble sprite +#define EF_BLOBLOCKED 0x8000 // caught by a trapper + +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_FLAMER, + WP_PULSE_RIFLE, + 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_HBUILD, + //ok? + + WP_NUM_WEAPONS +} weapon_t; + +typedef enum +{ + UP_NONE, + + UP_LIGHTARMOUR, + UP_HELMET_MK1, + UP_HELMET_MK2, + UP_MEDKIT, + UP_BATTPACK, + UP_JETPACK, + UP_BATTLESUIT, + UP_GRENADE, + UP_BIORES, + + UP_AMMO, + + UP_NUM_UPGRADES +} upgrade_t; + +// 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_H_SPAWN, + + BA_H_MGTURRET, + BA_H_TESLAGEN, + + BA_H_ARMOURY, + BA_H_DCC, + BA_H_MEDISTAT, + + BA_H_REACTOR, + BA_H_REPEATER, + + //cuboids must stay in a block +#define CUBOID_FIRST BA_H_CUBOID1 + BA_H_CUBOID1, + BA_H_CUBOID2, + BA_H_CUBOID3, + BA_A_CUBOID1, + BA_A_CUBOID2, +#define CUBOID_LAST BA_A_CUBOID2 +#define CUBOID_TYPES (CUBOID_LAST-CUBOID_FIRST+1) + + BA_NUM_BUILDABLES +} buildable_t; + +// 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 + +const char *BG_EventName( int num ); + +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_JETJUMP, + 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, // 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_MASS_DRIVER, + + EV_MISSILE_HIT, + EV_MISSILE_MISS, + EV_MISSILE_MISS_METAL, + EV_TESLATRAIL, + EV_BULLET, // otherEntity is the shooter + + EV_LEV1_GRAB, + EV_LEV4_TRAMPLE_PREPARE, + EV_LEV4_TRAMPLE_START, + + EV_PAIN, + EV_DEATH1, + EV_DEATH2, + EV_DEATH3, + EV_OBITUARY, + + EV_GIB_PLAYER, // gib a previously living player + + EV_BUILD_CONSTRUCT, + EV_BUILD_DESTROY, + EV_BUILD_DELAY, // can't build yet + EV_BUILD_REPAIR, // repairing buildable + EV_BUILD_REPAIRED, // buildable has full health + EV_HUMAN_BUILDABLE_EXPLOSION, + EV_ALIEN_BUILDABLE_EXPLOSION, + EV_CUBOID_EXPLOSION, + EV_ALIEN_ACIDTUBE, + + EV_MEDKIT_USED, + + EV_ALIEN_EVOLVE, + EV_ALIEN_EVOLVE_FAILED, + + EV_DEBUG_LINE, + EV_STOPLOOPINGSOUND, + EV_TAUNT, + + EV_OVERMIND_ATTACK, // overmind under attack + EV_OVERMIND_DYING, // overmind close to death + EV_OVERMIND_SPAWNS, // overmind needs spawns + + EV_DCC_ATTACK, // dcc under attack + + EV_MGTURRET_SPINUP, // turret spinup sound should play + + EV_RPTUSE_SOUND, // trigger a sound + EV_LEV2_ZAP, + + EV_ALIEN_HATCH, // when an alien pops out of a human's chest + EV_ALIEN_HATCH_FAILURE, // when it doesns't work + + EV_JETPACK_DEACTIVATE, + EV_JETPACK_REFUEL + +} entity_event_t; + +typedef enum +{ + MN_NONE, + + MN_TEAM, + MN_A_TEAMFULL, + MN_H_TEAMFULL, + MN_A_TEAMLOCKED, + MN_H_TEAMLOCKED, + MN_PLAYERLIMIT, + MN_WARMUP, + + // cmd stuff + MN_CMD_CHEAT, + MN_CMD_CHEAT_TEAM, + MN_CMD_TEAM, + MN_CMD_SPEC, + MN_CMD_ALIEN, + MN_CMD_HUMAN, + MN_CMD_LIVING, + + //alien stuff + MN_A_CLASS, + MN_A_BUILD, + MN_A_INFEST, + MN_A_NOEROOM, + MN_A_TOOCLOSE, + MN_A_NOOVMND_EVOLVE, + MN_A_EVOLVEBUILDTIMER, + MN_A_CANTEVOLVE, + MN_A_EVOLVEWALLWALK, + MN_A_UNKNOWNCLASS, + MN_A_CLASSNOTSPAWN, + MN_A_CLASSNOTALLOWED, + MN_A_CLASSNOTATSTAGE, + + //shared build + MN_B_NOROOM, + MN_B_NORMAL, + MN_B_CANNOT, + MN_B_LASTSPAWN, + MN_B_SUDDENDEATH, + MN_B_REVOKED, + MN_B_SURRENDER, + MN_B_NOSURF, + MN_B_CUBOID_MODE1, + MN_B_CUBOID_MODE2, + MN_B_TOODENSE, + + //alien build + MN_A_ONEOVERMIND, + MN_A_NOBP, + MN_A_NOCREEP, + MN_A_NOOVMND, + + //human stuff + MN_H_SPAWN, + MN_H_BUILD, + MN_H_ARMOURY, + MN_H_UNKNOWNITEM, + MN_H_NOSLOTS, + MN_H_NOFUNDS, + MN_H_ITEMHELD, + MN_H_NOARMOURYHERE, + MN_H_NOENERGYAMMOHERE, + MN_H_NOROOMBSUITON, + MN_H_NOROOMBSUITOFF, + MN_H_ARMOURYBUILDTIMER, + MN_H_DEADTOCLASS, + MN_H_UNKNOWNSPAWNITEM, + + //human build + MN_H_NOPOWERHERE, + MN_H_NOBP, + MN_H_NOTPOWERED, + MN_H_NODCC, + MN_H_ONEREACTOR, + MN_H_RPTPOWERHERE, +} 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; + +// 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 enum +{ + WANIM_NONE, + + WANIM_IDLE, + + WANIM_DROP, + WANIM_RELOAD, + WANIM_RAISE, + + WANIM_ATTACK1, + WANIM_ATTACK2, + WANIM_ATTACK3, + WANIM_ATTACK4, + WANIM_ATTACK5, + WANIM_ATTACK6, + WANIM_ATTACK7, + WANIM_ATTACK8, + + MAX_WEAPON_ANIMATIONS +} weaponAnimNumber_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 + +// Time between location updates +#define TEAM_LOCATION_UPDATE_TIME 500 + +// 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 +} class_t; + +// spectator state +typedef enum +{ + SPECTATOR_NOT, + SPECTATOR_FREE, + SPECTATOR_LOCKED, + SPECTATOR_FOLLOW, + SPECTATOR_SCOREBOARD +} spectatorState_t; + +// modes of text communication +typedef enum +{ + SAY_ALL, + SAY_TEAM, + SAY_PRIVMSG, + SAY_TPRIVMSG, + SAY_AREA, + SAY_ADMINS, + SAY_ADMINS_PUBLIC, + SAY_RAW +} saymode_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_TRAMPLE, + MOD_LEVEL4_CRUSH, + MOD_LEVEL4_BOMB, + + MOD_SLOWBLOB, + MOD_POISON, + MOD_SWARM, + + MOD_HSPAWN, + MOD_TESLAGEN, + MOD_MGTURRET, + MOD_REACTOR, + + MOD_ASPAWN, + MOD_ATUBE, + MOD_OVERMIND, + MOD_DECONSTRUCT, + MOD_REPLACE, + MOD_NOCREEP, + + MOD_ALIEN_HATCH, + MOD_ALIEN_HATCH_FAILED + +} meansOfDeath_t; + + +//--------------------------------------------------------- + +// player class record +typedef struct +{ + class_t number; + + char *name; + char *info; + + int stages; + + int health; + float fallDamage; + float 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; + vec3_t shoulderOffsets; +} classConfig_t; + +//stages +typedef enum +{ + S1, + S2, + S3 +} stage_t; + +#define MAX_BUILDABLE_MODELS 4 + +// buildable item record +typedef struct +{ + buildable_t number; + + char *name; + char *humanName; + char *info; + char *entityName; + + trType_t traj; + float bounce; + + int buildPoints; + int stages; + + int health; + int regenRate; + + int splashDamage; + int splashRadius; + + int meansOfDeath; + + team_t 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 uniqueTest; + + int value; + + qboolean cuboid; +} buildableAttributes_t; + +typedef struct +{ + char models[ MAX_BUILDABLE_MODELS ][ MAX_QPATH ]; + + float modelScale; + vec3_t mins; + vec3_t maxs; + float zOffset; +} buildableConfig_t; + +// weapon record +typedef struct +{ + weapon_t number; + + int price; + int stages; + + int slots; + + char *name; + char *humanName; + char *info; + + 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; + + team_t team; +} weaponAttributes_t; + +// upgrade record +typedef struct +{ + upgrade_t number; + + int price; + int stages; + + int slots; + + char *name; + char *humanName; + char *info; + + char *icon; + + qboolean purchasable; + qboolean usable; + + team_t team; +} upgradeAttributes_t; + +qboolean BG_WeaponIsFull( weapon_t weapon, int stats[ ], int ammo, int clips ); +int BG_WeaponMaxAmmo( weapon_t weapon, int *stats); +qboolean BG_InventoryContainsWeapon( int weapon, int stats[ ] ); +int BG_SlotsForInventory( 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_GetClientNormal( const playerState_t *ps, vec3_t normal ); +void BG_GetClientViewOrigin( const playerState_t *ps, vec3_t viewOrigin ); +qboolean BG_PositionBuildableRelativeToPlayer( const playerState_t *ps, qboolean cuboid, + 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_GetValueOfPlayer( playerState_t *ps ); +qboolean BG_PlayerCanChangeWeapon( playerState_t *ps ); +int BG_PlayerPoisonCloudTime( playerState_t *ps ); +weapon_t BG_GetPlayerWeapon( playerState_t *ps ); +qboolean BG_HasEnergyWeapon( playerState_t *ps ); + +void BG_PackEntityNumbers( entityState_t *es, const int *entityNums, int count ); +int BG_UnpackEntityNumbers( entityState_t *es, int *entityNums, int count ); + +const buildableAttributes_t *BG_BuildableByName( const char *name ); +const buildableAttributes_t *BG_BuildableByEntityName( const char *name ); +const buildableAttributes_t *BG_Buildable( buildable_t buildable, const vec3_t cuboidSize ); +qboolean BG_BuildableAllowedInStage( buildable_t buildable, + stage_t stage ); + +buildableConfig_t *BG_BuildableConfig( buildable_t buildable ); +void BG_BuildableBoundingBox( buildable_t buildable, + vec3_t mins, vec3_t maxs ); +void BG_InitBuildableConfigs( void ); + +const classAttributes_t *BG_ClassByName( const char *name ); +const classAttributes_t *BG_Class( class_t class ); +qboolean BG_ClassAllowedInStage( class_t class, + stage_t stage ); + +classConfig_t *BG_ClassConfig( class_t class ); + +void BG_ClassBoundingBox( class_t class, vec3_t mins, + vec3_t maxs, vec3_t cmaxs, + vec3_t dmins, vec3_t dmaxs ); +qboolean BG_ClassHasAbility( class_t class, int ability ); +int BG_ClassCanEvolveFromTo( class_t fclass, + class_t tclass, + int credits, int alienStage, int num ); +qboolean BG_AlienCanEvolve( class_t class, int credits, int alienStage ); + +void BG_InitClassConfigs( void ); + +const weaponAttributes_t *BG_WeaponByName( const char *name ); +const weaponAttributes_t *BG_Weapon( weapon_t weapon ); +qboolean BG_WeaponAllowedInStage( weapon_t weapon, + stage_t stage ); + +const upgradeAttributes_t *BG_UpgradeByName( const char *name ); +const upgradeAttributes_t *BG_Upgrade( upgrade_t upgrade ); +qboolean BG_UpgradeAllowedInStage( upgrade_t upgrade, + stage_t stage ); +// 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_DEADSOLID MASK_PLAYERSOLID +#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, // buildable type + + ET_LOCATION, + + 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_Alloc( int size ); +void BG_InitMemory( void ); +void BG_Free( void *ptr ); +void BG_DefragmentMemory( void ); +void BG_MemoryInfo( void ); + +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, class_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( class_t class ); +qboolean BG_BuildableIsAllowed( buildable_t buildable ); +weapon_t BG_PrimaryWeapon( int stats[ ] ); + +// Friendly Fire Flags +#define FFF_HUMANS 1 +#define FFF_ALIENS 2 +#define FFF_BUILDABLES 4 + +// bg_voice.c +#define MAX_VOICES 8 +#define MAX_VOICE_NAME_LEN 16 +#define MAX_VOICE_CMD_LEN 16 +#define VOICE_ENTHUSIASM_DECAY 0.5f // enthusiasm lost per second + +typedef enum +{ + VOICE_CHAN_ALL, + VOICE_CHAN_TEAM , + VOICE_CHAN_LOCAL, + + VOICE_CHAN_NUM_CHANS +} voiceChannel_t; + +typedef struct voiceTrack_s +{ +#ifdef CGAME + sfxHandle_t track; + int duration; +#endif + char *text; + int enthusiasm; + int team; + int class; + int weapon; + struct voiceTrack_s *next; +} voiceTrack_t; + + +typedef struct voiceCmd_s +{ + char cmd[ MAX_VOICE_CMD_LEN ]; + voiceTrack_t *tracks; + struct voiceCmd_s *next; +} voiceCmd_t; + +typedef struct voice_s +{ + char name[ MAX_VOICE_NAME_LEN ]; + voiceCmd_t *cmds; + struct voice_s *next; +} voice_t; + +voice_t *BG_VoiceInit( void ); +void BG_PrintVoices( voice_t *voices, int debugLevel ); + +voice_t *BG_VoiceByName( voice_t *head, char *name ); +voiceCmd_t *BG_VoiceCmdFind( voiceCmd_t *head, char *name, int *cmdNum ); +voiceCmd_t *BG_VoiceCmdByNum( voiceCmd_t *head, int num); +voiceTrack_t *BG_VoiceTrackByNum( voiceTrack_t *head, int num ); +voiceTrack_t *BG_VoiceTrackFind( voiceTrack_t *head, team_t team, + class_t class, weapon_t weapon, + int enthusiasm, int *trackNum ); + +int BG_LoadEmoticons( emoticon_t *emoticons, int num ); + +char *BG_TeamName( team_t team ); + +typedef struct +{ + const char *name; +} dummyCmd_t; +int cmdcmp( const void *a, const void *b ); + + +//cuboid health point calculation types +enum +{ + CBHPT_PLAIN, // hp depends only on volume + CBHPT_CUBES, // more efficient as a cube + CBHPT_PANES // more efficient as a thin pane +}; + +// sound selection based on cuboid's volume +typedef struct +{ + int numsounds; // number of sounds + float volumes[6]; // minimum cuboid's volume at which sound plays (IN CUBIC METRES) +} cuboidVolsnds_t; + +// sound selection based on randomness +typedef struct +{ + int numsounds; +} cuboidRandsnds_t; + +typedef struct +{ + qboolean genericAnim; // use generic animation instead of loading four shaders + const char* file; // cuboid's name in files + const char* icon; // cuboid's [emoticon] + cuboidRandsnds_t pain;// paint sound stuff + cuboidVolsnds_t dstr; // destruction sound stuff + float hppv; // health per volume + int hpt; // health calculation type + float bppv; // build points usage per volume + int buildrate; // build rate (hp per sec) + int minbt; // minimum build time + qboolean regen; // does it regenerate? + int regenrate; // regeneration speed (hp per sec) + qboolean zappable; // can it be zapped by an Adv. Marauder? + qboolean repairable; // can it be repaired by a Construction Kit? + qboolean detectable; // do we want it to be visible on radars? + qboolean needpower; // does it need power? + int surfaceFlags; // surface flags (ladder, slick, etc.) +} cuboidAttributes_t; + +qboolean BG_IsCuboid(buildable_t buildable); +void BG_CuboidBBox(const vec3_t size, vec3_t mins, vec3_t maxs); +const cuboidAttributes_t *BG_CuboidAttributes(buildable_t buildable); +qboolean BG_CuboidAllowed(int stage); +int BG_CuboidMode(void); +int BG_CuboidUnpackHealth(entityState_t *es); +void BG_CuboidPackHealth(entityState_t *es, int health); +void BG_CuboidPackHealthSafe(buildable_t buildable, entityState_t *es, int health); +void BG_CuboidSortSize(const vec3_t a, vec3_t b); +int BG_SoundForVolume(const cuboidVolsnds_t *set, const vec3_t size); diff --git a/src/game/bg_slidemove.c b/src/game/bg_slidemove.c new file mode 100644 index 0000000..bd61401 --- /dev/null +++ b/src/game/bg_slidemove.c @@ -0,0 +1,408 @@ +/* +=========================================================================== +Copyright (C) 1999-2005 Id Software, Inc. +Copyright (C) 2000-2009 Darklegion Development + +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; + + BG_GetClientNormal( pm->ps, normal ); + + 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/bg_voice.c b/src/game/bg_voice.c new file mode 100644 index 0000000..c0764dd --- /dev/null +++ b/src/game/bg_voice.c @@ -0,0 +1,652 @@ +/* +=========================================================================== +Copyright (C) 1999-2005 Id Software, Inc. +Copyright (C) 2000-2009 Darklegion Development +Copyright (C) 2008 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 +=========================================================================== +*/ + +// bg_voice.c -- both games voice functions +#include "../qcommon/q_shared.h" +#include "bg_public.h" +#include "bg_local.h" + +int trap_FS_FOpenFile( const char *qpath, fileHandle_t *f, fsMode_t mode ); +int trap_FS_GetFileList( const char *path, const char *extension, char *listbuf, int bufsize ); +int trap_Parse_LoadSource( const char *filename ); +int trap_Parse_FreeSource( int handle ); +int trap_Parse_ReadToken( int handle, pc_token_t *pc_token ); +int trap_Parse_SourceFileAndLine( int handle, char *filename, int *line ); + +#ifdef CGAME +sfxHandle_t trap_S_RegisterSound( const char *sample, qboolean compressed ); +int trap_S_SoundDuration( sfxHandle_t handle ); +#endif + + +/* +============ +BG_VoiceParseError +============ +*/ +static void BG_VoiceParseError( fileHandle_t handle, char *err ) +{ + int line; + char filename[ MAX_QPATH ]; + + trap_Parse_SourceFileAndLine( handle, filename, &line ); + trap_Parse_FreeSource( handle ); + Com_Error( ERR_FATAL, "%s on line %d of %s\n", err, line, filename ); +} + +/* +============ +BG_VoiceList +============ +*/ +static voice_t *BG_VoiceList( void ) +{ + char fileList[ MAX_VOICES * ( MAX_VOICE_NAME_LEN + 8 ) ] = {""}; + int numFiles, i, fileLen = 0; + int count = 0; + char *filePtr; + voice_t *voices = NULL; + voice_t *top = NULL; + + numFiles = trap_FS_GetFileList( "voice", ".voice", fileList, + sizeof( fileList ) ); + + if( numFiles < 1 ) + return NULL; + + // special case for default.voice. this file is REQUIRED and will + // always be loaded first in the event of overflow of voice definitions + if( !trap_FS_FOpenFile( "voice/default.voice", NULL, FS_READ ) ) + { + Com_Printf( "voice/default.voice missing, voice system disabled." ); + return NULL; + } + + voices = (voice_t*)BG_Alloc( sizeof( voice_t ) ); + Q_strncpyz( voices->name, "default", sizeof( voices->name ) ); + voices->cmds = NULL; + voices->next = NULL; + count = 1; + + top = voices; + + filePtr = fileList; + for( i = 0; i < numFiles; i++, filePtr += fileLen + 1 ) + { + fileLen = strlen( filePtr ); + + // accounted for above + if( !Q_stricmp( filePtr, "default.voice" ) ) + continue; + + if( fileLen > MAX_VOICE_NAME_LEN + 8 ) { + Com_Printf( S_COLOR_YELLOW "WARNING: MAX_VOICE_NAME_LEN is %d. " + "skipping \"%s\", filename too long", MAX_VOICE_NAME_LEN, filePtr ); + continue; + } + + // trap_FS_GetFileList() buffer has overflowed + if( !trap_FS_FOpenFile( va( "voice/%s", filePtr ), NULL, FS_READ ) ) + { + Com_Printf( S_COLOR_YELLOW "WARNING: BG_VoiceList(): detected " + "an invalid .voice file \"%s\" in directory listing. You have " + "probably named one or more .voice files with outrageously long " + "names. gjbs", filePtr ); + break; + } + + if( count >= MAX_VOICES ) + { + Com_Printf( S_COLOR_YELLOW "WARNING: .voice file overflow. " + "%d of %d .voice files loaded. MAX_VOICES is %d", + count, numFiles, MAX_VOICES ); + break; + } + + voices->next = (voice_t*)BG_Alloc( sizeof( voice_t ) ); + voices = voices->next; + + Q_strncpyz( voices->name, filePtr, sizeof( voices->name ) ); + // strip extension + voices->name[ fileLen - 6 ] = '\0'; + voices->cmds = NULL; + voices->next = NULL; + count++; + } + return top; +} + +/* +============ +BG_VoiceParseTrack +============ +*/ +static qboolean BG_VoiceParseTrack( int handle, voiceTrack_t *voiceTrack ) +{ + pc_token_t token; + qboolean found = qfalse; + qboolean foundText = qfalse; + qboolean foundToken = qfalse; + + foundToken = trap_Parse_ReadToken( handle, &token ); + while( foundToken ) + { + if( token.string[ 0 ] == '}' ) + { + if( foundText ) + return qtrue; + else + { + BG_VoiceParseError( handle, "BG_VoiceParseTrack(): " + "missing text attribute for track" ); + } + } + else if( !Q_stricmp( token.string, "team" ) ) + { + foundToken = trap_Parse_ReadToken( handle, &token ); + found = qfalse; + while( foundToken && token.type == TT_NUMBER ) + { + found = qtrue; + if( voiceTrack->team < 0 ) + voiceTrack->team = 0; + voiceTrack->team |= ( 1 << token.intvalue ); + foundToken = trap_Parse_ReadToken( handle, &token ); + } + if( !found ) + { + BG_VoiceParseError( handle, + "BG_VoiceParseTrack(): missing \"team\" value" ); + } + continue; + } + else if( !Q_stricmp( token.string, "class" ) ) + { + foundToken = trap_Parse_ReadToken( handle, &token ); + found = qfalse; + while( foundToken && token.type == TT_NUMBER ) + { + found = qtrue; + if( voiceTrack->class < 0 ) + voiceTrack->class = 0; + voiceTrack->class |= ( 1 << token.intvalue ); + foundToken = trap_Parse_ReadToken( handle, &token ); + } + if( !found ) + { + BG_VoiceParseError( handle, + "BG_VoiceParseTrack(): missing \"class\" value" ); + } + continue; + } + else if( !Q_stricmp( token.string, "weapon" ) ) + { + foundToken = trap_Parse_ReadToken( handle, &token ); + found = qfalse; + while( foundToken && token.type == TT_NUMBER ) + { + found = qtrue; + if( voiceTrack->weapon < 0 ) + voiceTrack->weapon = 0; + voiceTrack->weapon |= ( 1 << token.intvalue ); + foundToken = trap_Parse_ReadToken( handle, &token ); + } + if( !found ) + { + BG_VoiceParseError( handle, + "BG_VoiceParseTrack(): missing \"weapon\" value"); + } + continue; + } + else if( !Q_stricmp( token.string, "text" ) ) + { + if( foundText ) + { + BG_VoiceParseError( handle, "BG_VoiceParseTrack(): " + "duplicate \"text\" definition for track" ); + } + foundToken = trap_Parse_ReadToken( handle, &token ); + if( !foundToken ) + { + BG_VoiceParseError( handle, "BG_VoiceParseTrack(): " + "missing \"text\" value" ); + } + foundText = qtrue; + if( strlen( token.string ) >= MAX_SAY_TEXT ) + { + BG_VoiceParseError( handle, va( "BG_VoiceParseTrack(): " + "\"text\" value " "\"%s\" exceeds MAX_SAY_TEXT length", + token.string ) ); + } + + voiceTrack->text = (char *)BG_Alloc( strlen( token.string ) + 1 ); + Q_strncpyz( voiceTrack->text, token.string, strlen( token.string ) + 1 ); + foundToken = trap_Parse_ReadToken( handle, &token ); + continue; + } + else if( !Q_stricmp( token.string, "enthusiasm" ) ) + { + foundToken = trap_Parse_ReadToken( handle, &token ); + if( token.type == TT_NUMBER ) + { + voiceTrack->enthusiasm = token.intvalue; + } + else + { + BG_VoiceParseError( handle, "BG_VoiceParseTrack(): " + "missing \"enthusiasm\" value" ); + } + foundToken = trap_Parse_ReadToken( handle, &token ); + continue; + } + else + { + BG_VoiceParseError( handle, va( "BG_VoiceParseTrack():" + " unknown token \"%s\"", token.string ) ); + } + } + return qfalse; +} + +/* +============ +BG_VoiceParseCommand +============ +*/ +static voiceTrack_t *BG_VoiceParseCommand( int handle ) +{ + pc_token_t token; + qboolean parsingTrack = qfalse; + voiceTrack_t *voiceTracks = NULL; + voiceTrack_t *top = NULL; + + while( trap_Parse_ReadToken( handle, &token ) ) + { + if( !parsingTrack && token.string[ 0 ] == '}' ) + return top; + + if( parsingTrack ) + { + if( token.string[ 0 ] == '{' ) + { + BG_VoiceParseTrack( handle, voiceTracks ); + parsingTrack = qfalse; + continue; + + } + else + { + BG_VoiceParseError( handle, va( "BG_VoiceParseCommand(): " + "parse error at \"%s\"", token.string ) ); + } + } + + + if( top == NULL ) + { + voiceTracks = BG_Alloc( sizeof( voiceTrack_t ) ); + top = voiceTracks; + } + else + { + voiceTracks->next = BG_Alloc( sizeof( voiceCmd_t ) ); + voiceTracks = voiceTracks->next; + } + + if( !trap_FS_FOpenFile( token.string, NULL, FS_READ ) ) + { + int line; + char filename[ MAX_QPATH ]; + + trap_Parse_SourceFileAndLine( handle, filename, &line ); + Com_Printf( S_COLOR_YELLOW "WARNING: BG_VoiceParseCommand(): " + "track \"%s\" referenced on line %d of %s does not exist\n", + token.string, line, filename ); + } + else + { +#ifdef CGAME + voiceTracks->track = trap_S_RegisterSound( token.string, qfalse ); + voiceTracks->duration = trap_S_SoundDuration( voiceTracks->track ); +#endif + } + + voiceTracks->team = -1; + voiceTracks->class = -1; + voiceTracks->weapon = -1; + voiceTracks->enthusiasm = 0; + voiceTracks->text = NULL; + voiceTracks->next = NULL; + parsingTrack = qtrue; + + } + return NULL; +} + +/* +============ +BG_VoiceParse +============ +*/ +static voiceCmd_t *BG_VoiceParse( char *name ) +{ + voiceCmd_t *voiceCmds = NULL; + voiceCmd_t *top = NULL; + pc_token_t token; + qboolean parsingCmd = qfalse; + int handle; + + handle = trap_Parse_LoadSource( va( "voice/%s.voice", name ) ); + if( !handle ) + return NULL; + + while( trap_Parse_ReadToken( handle, &token ) ) + { + if( parsingCmd ) + { + if( token.string[ 0 ] == '{' ) + { + voiceCmds->tracks = BG_VoiceParseCommand( handle ); + parsingCmd = qfalse; + continue; + } + else + { + int line; + char filename[ MAX_QPATH ]; + + trap_Parse_SourceFileAndLine( handle, filename, &line ); + Com_Error( ERR_FATAL, "BG_VoiceParse(): " + "parse error on line %d of %s\n", line, filename ); + } + } + + if( strlen( token.string ) >= MAX_VOICE_CMD_LEN ) + { + int line; + char filename[ MAX_QPATH ]; + + trap_Parse_SourceFileAndLine( handle, filename, &line ); + Com_Error( ERR_FATAL, "BG_VoiceParse(): " + "command \"%s\" exceeds MAX_VOICE_CMD_LEN (%d) on line %d of %s\n", + token.string, MAX_VOICE_CMD_LEN, line, filename ); + } + + if( top == NULL ) + { + voiceCmds = BG_Alloc( sizeof( voiceCmd_t ) ); + top = voiceCmds; + } + else + { + voiceCmds->next = BG_Alloc( sizeof( voiceCmd_t ) ); + voiceCmds = voiceCmds->next; + } + + Q_strncpyz( voiceCmds->cmd, token.string, sizeof( voiceCmds->cmd ) ); + voiceCmds->next = NULL; + parsingCmd = qtrue; + + } + + trap_Parse_FreeSource( handle ); + + return top; +} + +/* +============ +BG_VoiceInit +============ +*/ +voice_t *BG_VoiceInit( void ) +{ + voice_t *voices; + voice_t *voice; + + voices = BG_VoiceList(); + + voice = voices; + while( voice ) + { + voice->cmds = BG_VoiceParse( voice->name ); + voice = voice->next; + } + + return voices; +} + + +/* +============ +BG_PrintVoices +============ +*/ +void BG_PrintVoices( voice_t *voices, int debugLevel ) +{ + voice_t *voice = voices; + voiceCmd_t *voiceCmd; + voiceTrack_t *voiceTrack; + + int cmdCount; + int trackCount; + + if( voice == NULL ) + { + Com_Printf( "voice list is empty\n" ); + return; + } + + while( voice != NULL ) + { + if( debugLevel > 0 ) + Com_Printf( "voice \"%s\"\n", voice->name ); + voiceCmd = voice->cmds; + cmdCount = 0; + trackCount = 0; + while( voiceCmd != NULL ) + { + if( debugLevel > 0 ) + Com_Printf( " %s\n", voiceCmd->cmd ); + voiceTrack = voiceCmd->tracks; + cmdCount++; + while ( voiceTrack != NULL ) + { + if( debugLevel > 1 ) + Com_Printf( " text -> %s\n", voiceTrack->text ); + if( debugLevel > 2 ) + { + Com_Printf( " team -> %d\n", voiceTrack->team ); + Com_Printf( " class -> %d\n", voiceTrack->class ); + Com_Printf( " weapon -> %d\n", voiceTrack->weapon ); + Com_Printf( " enthusiasm -> %d\n", voiceTrack->enthusiasm ); +#ifdef CGAME + Com_Printf( " duration -> %d\n", voiceTrack->duration ); +#endif + } + if( debugLevel > 1 ) + Com_Printf( "\n" ); + trackCount++; + voiceTrack = voiceTrack->next; + } + voiceCmd = voiceCmd->next; + } + + if( !debugLevel ) + { + Com_Printf( "voice \"%s\": %d commands, %d tracks\n", + voice->name, cmdCount, trackCount ); + } + voice = voice->next; + } +} + +/* +============ +BG_VoiceByName +============ +*/ +voice_t *BG_VoiceByName( voice_t *head, char *name ) +{ + voice_t *v = head; + + while( v ) + { + if( !Q_stricmp( v->name, name ) ) + return v; + v = v->next; + } + return NULL; +} + +/* +============ +BG_VoiceCmdFind +============ +*/ +voiceCmd_t *BG_VoiceCmdFind( voiceCmd_t *head, char *name, int *cmdNum ) +{ + voiceCmd_t *vc = head; + int i = 0; + + while( vc ) + { + i++; + if( !Q_stricmp( vc->cmd, name ) ) + { + *cmdNum = i; + return vc; + } + vc = vc->next; + } + return NULL; +} + +/* +============ +BG_VoiceCmdByNum +============ +*/ +voiceCmd_t *BG_VoiceCmdByNum( voiceCmd_t *head, int num ) +{ + voiceCmd_t *vc = head; + int i = 0; + + while( vc ) + { + i++; + if( i == num ) + return vc; + vc = vc->next; + } + return NULL; +} + +/* +============ +BG_VoiceTrackByNum +============ +*/ +voiceTrack_t *BG_VoiceTrackByNum( voiceTrack_t *head, int num ) +{ + voiceTrack_t *vt = head; + int i = 0; + + while( vt ) + { + i++; + if( i == num ) + return vt; + vt = vt->next; + } + return NULL; +} + +/* +============ +BG_VoiceTrackFind +============ +*/ +voiceTrack_t *BG_VoiceTrackFind( voiceTrack_t *head, team_t team, + class_t class, weapon_t weapon, + int enthusiasm, int *trackNum ) +{ + voiceTrack_t *vt = head; + int highestMatch = 0; + int matchCount = 0; + int selectedMatch = 0; + int i = 0; + int j = 0; + + // find highest enthusiasm without going over + while( vt ) + { + if( ( vt->team >= 0 && !( vt->team & ( 1 << team ) ) ) || + ( vt->class >= 0 && !( vt->class & ( 1 << class ) ) ) || + ( vt->weapon >= 0 && !( vt->weapon & ( 1 << weapon ) ) ) || + vt->enthusiasm > enthusiasm ) + { + vt = vt->next; + continue; + } + + if( vt->enthusiasm > highestMatch ) + { + matchCount = 0; + highestMatch = vt->enthusiasm; + } + if( vt->enthusiasm == highestMatch ) + matchCount++; + vt = vt->next; + } + + if( !matchCount ) + return NULL; + + // return randomly selected match + selectedMatch = rand() / ( RAND_MAX / matchCount + 1 ); + vt = head; + i = 0; + j = 0; + while( vt ) + { + j++; + if( ( vt->team >= 0 && !( vt->team & ( 1 << team ) ) ) || + ( vt->class >= 0 && !( vt->class & ( 1 << class ) ) ) || + ( vt->weapon >= 0 && !( vt->weapon & ( 1 << weapon ) ) ) || + vt->enthusiasm != highestMatch ) + { + vt = vt->next; + continue; + } + if( i == selectedMatch ) + { + *trackNum = j; + return vt; + } + i++; + vt = vt->next; + } + return NULL; +} diff --git a/src/game/g_active.c b/src/game/g_active.c new file mode 100644 index 0000000..a65d9f2 --- /dev/null +++ b/src/game/g_active.c @@ -0,0 +1,1955 @@ +/* +=========================================================================== +Copyright (C) 1999-2005 Id Software, Inc. +Copyright (C) 2000-2009 Darklegion Development + +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" + +/* To create useful and interesting cuboids (like ladders), the ability to specify + * custom surface flags for buildings was needed. This is what the following function + * was written for. It replaces the regular tracing function in PM code (also on the + * client side, grep for CG_PMTraceHack). This function also takes into account + * the modelindex of the entity it found and adjusts the surfaceFlags output + * accordingly. This way custom surface flags for buildings can be done without + * messing around with the PM code or the engine itself. + */ +void G_PMTraceHack(trace_t *results,const vec3_t start,const vec3_t mins,const vec3_t maxs,const vec3_t end,int passEntityNum,int contentmask ) +{ + int modelindex; + trap_Trace(results,start,mins,maxs,end,passEntityNum,contentmask); + if(results->entityNum!=ENTITYNUM_NONE) + { + modelindex=g_entities[results->entityNum].s.modelindex; + if(modelindex>=CUBOID_FIRST&&modelindex<=CUBOID_LAST&&(g_entities[results->entityNum].s.eFlags&EF_B_SPAWNED)) + results->surfaceFlags|=BG_CuboidAttributes(modelindex)->surfaceFlags; + } +} + + +/* +=============== +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( !PM_Live( client->ps.pm_type ) ) + 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( ) < RAND_MAX / 2 + 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; +} + + + +//============================================================== + +/* +============== +ClientShove +============== +*/ +static int GetClientMass( gentity_t *ent ) +{ + int entMass = 100; + + if( ent->client->pers.teamSelection == TEAM_ALIENS ) + entMass = BG_Class( ent->client->pers.classSelection )->health; + else if( ent->client->pers.teamSelection == TEAM_HUMANS ) + { + if( BG_InventoryContainsUpgrade( UP_BATTLESUIT, ent->client->ps.stats ) ) + entMass *= 2; + } + else + return 0; + return entMass; +} + +static void ClientShove( gentity_t *ent, gentity_t *victim ) +{ + vec3_t dir, push; + float force; + int entMass, vicMass; + + // Don't push if the entity is not trying to move + if( !ent->client->pers.cmd.rightmove && !ent->client->pers.cmd.forwardmove && + !ent->client->pers.cmd.upmove ) + return; + + // Cannot push enemy players unless they are walking on the player + if( !OnSameTeam( ent, victim ) && + victim->client->ps.groundEntityNum != ent - g_entities ) + return; + + // Shove force is scaled by relative mass + entMass = GetClientMass( ent ); + vicMass = GetClientMass( victim ); + if( vicMass <= 0 || entMass <= 0 ) + return; + force = g_shove.value * entMass / vicMass; + if( force < 0 ) + force = 0; + if( force > 150 ) + force = 150; + + // Give the victim some shove velocity + VectorSubtract( victim->r.currentOrigin, ent->r.currentOrigin, dir ); + VectorNormalizeFast( dir ); + VectorScale( dir, force, push ); + VectorAdd( victim->client->ps.velocity, push, victim->client->ps.velocity ); + + // Set the pmove timer so that the other client can't cancel + // out the movement immediately + if( !victim->client->ps.pm_time ) + { + int time; + + time = force * 2 + 0.5f; + if( time < 50 ) + time = 50; + if( time > 200 ) + time = 200; + victim->client->ps.pm_time = time; + victim->client->ps.pm_flags |= PMF_TIME_KNOCKBACK; + } +} + +/* +============== +ClientImpacts +============== +*/ +void ClientImpacts( gentity_t *ent, pmove_t *pm ) +{ + int i; + trace_t trace; + gentity_t *other; + + // clear a fake trace struct for touch function + memset( &trace, 0, sizeof( trace ) ); + + for( i = 0; i < pm->numtouch; i++ ) + { + 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; + + // tyrant impact attacks + if( ent->client->ps.weapon == WP_ALEVEL4 ) + { + G_ChargeAttack( ent, other ); + G_CrushAttack( ent, other ); + } + + // shove players + if( ent->client && other->client ) + ClientShove( ent, other ); + + // touch triggers + if( other->touch ) + 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_ClassBoundingBox( ent->client->ps.stats[ STAT_CLASS ], + 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.spectatorState != SPECTATOR_NOT ) + { + 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 ); + } +} + +/* +================= +G_ClientUpdateSpawnQueue + +Send spawn queue data to a client (used to be in SpectatorThink) +================= +*/ +void G_ClientUpdateSpawnQueue( gclient_t *client ) +{ + if( client->ps.stats[ STAT_TEAM ] == TEAM_ALIENS ) + { + client->ps.persistant[ PERS_QUEUEPOS ] = + G_GetPosInSpawnQueue( &level.alienSpawnQueue, client->ps.clientNum ); + client->ps.persistant[ PERS_SPAWNS ] = level.numAlienSpawns; + client->ps.persistant[ PERS_SPAWNS_IMPLANTED ] = level.numAlienImplantedSpawns; + } + else if( client->ps.stats[ STAT_TEAM ] == TEAM_HUMANS ) + { + client->ps.persistant[ PERS_QUEUEPOS ] = + G_GetPosInSpawnQueue( &level.humanSpawnQueue, client->ps.clientNum ); + client->ps.persistant[ PERS_SPAWNS ] = level.numHumanSpawns; + client->ps.persistant[ PERS_SPAWNS_IMPLANTED ] = level.numHumanImplantedSpawns; + } +} + +/* +================= +SpectatorThink +================= +*/ +void SpectatorThink( gentity_t *ent, usercmd_t *ucmd ) +{ + pmove_t pm; + gclient_t *client; + int clientNum; + qboolean attack1, attack3, following, queued; + + 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 ); + + // We are in following mode only if we are following a non-spectating client + following = client->sess.spectatorState == SPECTATOR_FOLLOW; + if( following ) + { + clientNum = client->sess.spectatorClient; + if( clientNum < 0 || clientNum > level.maxclients || + !g_entities[ clientNum ].client || + g_entities[ clientNum ].client->sess.spectatorState != SPECTATOR_NOT ) + following = qfalse; + } + + // Check to see if we are in the spawn queue + if( client->pers.teamSelection == TEAM_ALIENS ) + queued = G_SearchSpawnQueue( &level.alienSpawnQueue, ent - g_entities ); + else if( client->pers.teamSelection == TEAM_HUMANS ) + queued = G_SearchSpawnQueue( &level.humanSpawnQueue, ent - g_entities ); + else + queued = qfalse; + + // Wants to get out of spawn queue + if( attack1 && queued ) + { + if( client->sess.spectatorState == SPECTATOR_FOLLOW ) + G_StopFollowing( ent ); + if( client->ps.stats[ STAT_TEAM ] == TEAM_ALIENS ) + G_RemoveFromSpawnQueue( &level.alienSpawnQueue, client->ps.clientNum ); + else if( client->ps.stats[ STAT_TEAM ] == TEAM_HUMANS ) + G_RemoveFromSpawnQueue( &level.humanSpawnQueue, client->ps.clientNum ); + client->pers.classSelection = PCL_NONE; + client->ps.stats[ STAT_CLASS ] = PCL_NONE; + client->ps.pm_flags &= ~PMF_QUEUED; + queued = qfalse; + } + else if( attack1 ) + { + // Wants to get into spawn queue + if( client->sess.spectatorState == SPECTATOR_FOLLOW ) + G_StopFollowing( ent ); + if( client->pers.teamSelection == TEAM_NONE ) + G_TriggerMenu( client->ps.clientNum, MN_TEAM ); + else if( client->pers.teamSelection == TEAM_ALIENS ) + G_TriggerMenu( client->ps.clientNum, MN_A_CLASS ); + else if( client->pers.teamSelection == TEAM_HUMANS ) + G_TriggerMenu( client->ps.clientNum, MN_H_SPAWN ); + } + + // We are either not following anyone or following a spectator + if( !following ) + { + if( client->sess.spectatorState == SPECTATOR_LOCKED || + client->sess.spectatorState == SPECTATOR_FOLLOW ) + client->ps.pm_type = PM_FREEZE; + else if( client->noclip ) + client->ps.pm_type = PM_NOCLIP; + else + client->ps.pm_type = PM_SPECTATOR; + + if( queued ) + client->ps.pm_flags |= PMF_QUEUED; + + client->ps.speed = client->pers.flySpeed; + client->ps.stats[ STAT_STAMINA ] = 0; + client->ps.stats[ STAT_MISC ] = 0; + client->buildTimer=0; + client->ps.stats[ STAT_BUILDABLE ] = BA_NONE; + client->ps.stats[ STAT_CLASS ] = PCL_NONE; + client->ps.weapon = WP_NONE; + + // Set up for pmove + memset( &pm, 0, sizeof( pm ) ); + pm.ps = &client->ps; + pm.pmext = &client->pmext; + pm.cmd = *ucmd; + pm.tracemask = MASK_DEADSOLID; // spectators can fly through bodies + pm.trace = G_PMTraceHack; + pm.pointcontents = trap_PointContents; + + // Perform a pmove + Pmove( &pm ); + + // Save results of pmove + VectorCopy( client->ps.origin, ent->s.origin ); + + G_ClientUpdateSpawnQueue( ent->client ); + + G_TouchTriggers( ent ); + trap_UnlinkEntity( ent ); + } +} + + + +/* +================= +ClientInactivityTimer + +Returns qfalse if the client is dropped +================= +*/ +qboolean ClientInactivityTimer( gentity_t *ent ) +{ + gclient_t *client = ent->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 && + !G_admin_permission( ent, ADMF_ACTIVITY ) ) + { + trap_DropClient( client - level.clients, "Dropped due to inactivity" ); + return qfalse; + } + + if( level.time > client->inactivityTime - 10000 && + !client->inactivityWarning && + !G_admin_permission( ent, ADMF_ACTIVITY ) ) + { + 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; + int i; + + ucmd = &ent->client->pers.cmd; + + aForward = abs( ucmd->forwardmove ); + aRight = abs( ucmd->rightmove ); + + 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; + + client = ent->client; + client->time100 += msec; + client->time1000 += msec; + client->time10000 += msec; + + while ( client->time100 >= 100 ) + { + weapon_t weapon = BG_GetPlayerWeapon( &client->ps ); + + client->time100 -= 100; + + // Restore or subtract stamina + if( stopped || client->ps.pm_type == PM_JETPACK ) + client->ps.stats[ STAT_STAMINA ] += STAMINA_STOP_RESTORE; + else if( ( client->ps.stats[ STAT_STATE ] & SS_SPEEDBOOST ) && + !( client->buttons & BUTTON_WALKING ) ) // walk overrides sprint + client->ps.stats[ STAT_STAMINA ] -= STAMINA_SPRINT_TAKE; + else if( walking || crouched ) + client->ps.stats[ STAT_STAMINA ] += STAMINA_WALK_RESTORE; + + // Check stamina limits + if( client->ps.stats[ STAT_STAMINA ] > STAMINA_MAX ) + client->ps.stats[ STAT_STAMINA ] = STAMINA_MAX; + else if( client->ps.stats[ STAT_STAMINA ] < -STAMINA_MAX ) + client->ps.stats[ STAT_STAMINA ] = -STAMINA_MAX; + + if( weapon == WP_ABUILD || weapon == WP_ABUILD2 || + BG_InventoryContainsWeapon( WP_HBUILD, client->ps.stats ) ) + { + client->buildTimer-=MIN(100,client->buildTimer); + G_RecalcBuildTimer(client); + } + + switch( weapon ) + { + case WP_ABUILD: + case WP_ABUILD2: + case WP_HBUILD: + + // Set validity bit on buildable + if( ( client->ps.stats[ STAT_BUILDABLE ] & ~SB_VALID_TOGGLEBIT ) > BA_NONE ) + { + int dist = BG_Class( ent->client->ps.stats[ STAT_CLASS ] )->buildDist; + vec3_t dummy, dummy2; + + if( G_CanBuild( ent, client->ps.stats[ STAT_BUILDABLE ] & ~SB_VALID_TOGGLEBIT, + dist, dummy, dummy2, client->cuboidSelection ) == IBE_NONE ) + client->ps.stats[ STAT_BUILDABLE ] |= SB_VALID_TOGGLEBIT; + else + client->ps.stats[ STAT_BUILDABLE ] &= ~SB_VALID_TOGGLEBIT; + + // Let the client know which buildables will be removed by building + for( i = 0; i < MAX_MISC; i++ ) + { + if( i < level.numBuildablesForRemoval ) + client->ps.misc[ i ] = level.markedBuildables[ i ]->s.number; + else + client->ps.misc[ i ] = 0; + } + } + else + { + for( i = 0; i < MAX_MISC; i++ ) + client->ps.misc[ i ] = 0; + } + break; + + default: + break; + } + + if( ent->client->pers.teamSelection == TEAM_HUMANS && + ( client->ps.stats[ STAT_STATE ] & SS_HEALING_2X ) ) + { + 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_HEALING_2X; + } + 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_HEALING_2X; + } + } + + //use fuel + if( BG_InventoryContainsUpgrade( UP_JETPACK, ent->client->ps.stats ) && + BG_UpgradeIsActive( UP_JETPACK, ent->client->ps.stats ) && + ent->client->ps.stats[ STAT_FUEL ] > 0 ) + { + ent->client->ps.stats[ STAT_FUEL ] -= JETPACK_FUEL_USAGE; + if( ent->client->ps.stats[ STAT_FUEL ] <= 0 ) + { + ent->client->ps.stats[ STAT_FUEL ] = 0; + BG_DeactivateUpgrade( UP_JETPACK, client->ps.stats ); + BG_AddPredictableEventToPlayerstate( EV_JETPACK_DEACTIVATE, 0, &client->ps ); + } + } + + //biores + if( BG_InventoryContainsUpgrade( UP_BIORES, ent->client->ps.stats ) && + ent->health < ent->client->ps.stats[ STAT_MAX_HEALTH ] ) + { + float hp, dmod; + + dmod = MIN( level.time - ent->lastDamageTime, 3000 ) / 3000.0f; + hp = (float)ent->health / ent->client->ps.stats[ STAT_MAX_HEALTH ]; + ent->client->bioresHealTimer += (BIORES_EQUATION) * 10 * dmod; + } + + if( ent->client->bioresHealTimer >= 100 ) + { + int delta; + + delta = ent->client->bioresHealTimer / 100; + ent->health = MAX( MIN( ent->health+delta, ent->client->ps.stats[ STAT_MAX_HEALTH ] ), 1 ); + + ent->client->bioresHealTimer %= 100; + } + + } + + while( client->time1000 >= 1000 ) + { + client->time1000 -= 1000; + + //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_MK1, client->ps.stats ) ) + damage -= HELMET_MK1_POISON_PROTECTION; + + if( BG_InventoryContainsUpgrade( UP_HELMET_MK2, client->ps.stats ) ) + damage -= HELMET_MK2_POISON_PROTECTION; + + if( BG_InventoryContainsUpgrade( UP_LIGHTARMOUR, client->ps.stats ) ) + damage -= LIGHTARMOUR_POISON_PROTECTION; + + if( BG_InventoryContainsUpgrade( UP_BIORES, client->ps.stats ) ) + damage *= BIORES_POISON_MODIFIER; + + G_Damage( ent, client->lastPoisonClient, client->lastPoisonClient, NULL, + 0, damage, 0, MOD_POISON ); + } + + // turn off life support when a team admits defeat + if( client->ps.stats[ STAT_TEAM ] == TEAM_ALIENS && + level.surrenderTeam == TEAM_ALIENS ) + { + G_Damage( ent, NULL, NULL, NULL, NULL, + BG_Class( client->ps.stats[ STAT_CLASS ] )->regenRate, + DAMAGE_NO_ARMOR, MOD_SUICIDE ); + } + else if( client->ps.stats[ STAT_TEAM ] == TEAM_HUMANS && + level.surrenderTeam == TEAM_HUMANS ) + { + G_Damage( ent, NULL, NULL, NULL, NULL, 5, DAMAGE_NO_ARMOR, MOD_SUICIDE ); + } + + // lose some voice enthusiasm + if( client->voiceEnthusiasm > 0.0f ) + client->voiceEnthusiasm -= VOICE_ENTHUSIASM_DECAY; + else + client->voiceEnthusiasm = 0.0f; + + client->pers.aliveSeconds++; + if( g_freeFundPeriod.integer > 0 && + client->pers.aliveSeconds % g_freeFundPeriod.integer == 0 ) + { + // Give clients some credit periodically + if( G_TimeTilSuddenDeath( ) > 0 ) + { + if( client->ps.stats[ STAT_TEAM ] == TEAM_ALIENS ) + G_AddCreditToClient( client, FREEKILL_ALIEN, qtrue ); + else if( client->ps.stats[ STAT_TEAM ] == TEAM_HUMANS ) + G_AddCreditToClient( client, FREEKILL_HUMAN, qtrue ); + } + } + + G_ClientUpdateSpawnQueue( ent->client ); + + if( client->isImpregnated && !client->isImplantMature && + level.time >= client->impregnationTime + ALIEN_IMPLANT_MATURING_TIME_MIN && + random() <= ALIEN_IMPLANT_MATURING_CHANCE ) + client->isImplantMature = qtrue; + + } + + while( client->time10000 >= 10000 ) + { + client->time10000 -= 10000; + + if( ent->client->ps.weapon == WP_ABUILD || + ent->client->ps.weapon == WP_ABUILD2 ) + { + AddScore( ent, ALIEN_BUILDER_SCOREINC ); + } + else if( ent->client->ps.weapon == WP_HBUILD ) + { + AddScore( ent, HUMAN_BUILDER_SCOREINC ); + } + + // Give score to basis that healed other aliens + if( ent->client->pers.hasHealed ) + { + if( client->ps.weapon == WP_ALEVEL1 ) + AddScore( ent, LEVEL1_REGEN_SCOREINC ); + else if( client->ps.weapon == WP_ALEVEL1_UPG ) + AddScore( ent, LEVEL1_UPG_REGEN_SCOREINC ); + + ent->client->pers.hasHealed = qfalse; + } + } + + // Regenerate Adv. Dragoon barbs + if( client->ps.weapon == WP_ALEVEL3_UPG ) + { + if( client->ps.ammo < BG_Weapon( WP_ALEVEL3_UPG )->maxAmmo ) + { + if( ent->timestamp + LEVEL3_BOUNCEBALL_REGEN < level.time ) + { + client->ps.ammo++; + ent->timestamp = level.time; + } + } + else + ent->timestamp = level.time; + } +} + +/* +==================== +ClientIntermissionThink +==================== +*/ +void ClientIntermissionThink( gclient_t *client ) +{ + 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; + class_t class; + + client = ent->client; + class = client->ps.stats[ STAT_CLASS ]; + + 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_Class( class )->health * + BG_Class( class )->fallDamage * fallDistance ); + + VectorSet( dir, 0, 0, 1 ); + BG_ClassBoundingBox( 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; + + 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; + + // 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" ); + } + + 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 ) + { + ClientIntermissionThink( client ); + return; + } + + // spectators don't do much + if( client->sess.spectatorState != SPECTATOR_NOT ) + { + if( client->sess.spectatorState == SPECTATOR_SCOREBOARD ) + return; + + SpectatorThink( ent, ucmd ); + return; + } + + G_namelog_update_score( client ); + + // check for inactivity timer, but never drop the local client of a non-dedicated server + if( !ClientInactivityTimer( ent ) ) + 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_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; + + 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; + + // Update boosted state flags + client->ps.stats[ STAT_STATE ] &= ~SS_BOOSTEDWARNING; + if( client->ps.stats[ STAT_STATE ] & SS_BOOSTED ) + { + if( level.time - client->boostedTime >= BOOST_TIME ) + client->ps.stats[ STAT_STATE ] &= ~SS_BOOSTED; + else if( level.time - client->boostedTime >= BOOST_WARN_TIME ) + client->ps.stats[ STAT_STATE ] |= SS_BOOSTEDWARNING; + } + + // Check if poison cloud has worn off + if( ( client->ps.eFlags & EF_POISONCLOUDED ) && + BG_PlayerPoisonCloudTime( &client->ps ) - level.time + + client->lastPoisonCloudedTime <= 0 ) + client->ps.eFlags &= ~EF_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_HEALING_2X || + ( 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 ) + { + //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_HEALING_2X; + 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 ); + } + } + + // Replenish alien health + if( level.surrenderTeam != client->pers.teamSelection && + ent->nextRegenTime >= 0 && ent->nextRegenTime < level.time ) + { + float regenRate = + BG_Class( ent->client->ps.stats[ STAT_CLASS ] )->regenRate; + + if( ent->health <= 0 || ent->nextRegenTime < 0 || regenRate == 0 ) + ent->nextRegenTime = -1; // no regen + else + { + int entityList[ MAX_GENTITIES ]; + int i, num; + int count, interval; + vec3_t range, mins, maxs; + float modifier = 1.0f; + + VectorSet( range, REGEN_BOOST_RANGE, REGEN_BOOST_RANGE, + REGEN_BOOST_RANGE ); + 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++ ) + { + gentity_t *boost = &g_entities[ entityList[ i ] ]; + + if( Distance( client->ps.origin, boost->s.origin ) > REGEN_BOOST_RANGE ) + continue; + + if( modifier < BOOSTER_REGEN_MOD && boost->s.eType == ET_BUILDABLE && + boost->s.modelindex == BA_A_BOOSTER && boost->spawned && + boost->health > 0 && boost->powered ) + { + modifier = BOOSTER_REGEN_MOD; + continue; + } + + if( boost->s.eType == ET_PLAYER && boost->client && + boost->client->pers.teamSelection == + ent->client->pers.teamSelection && boost->health > 0 ) + { + class_t class = boost->client->ps.stats[ STAT_CLASS ]; + qboolean didBoost = qfalse; + + if( class == PCL_ALIEN_LEVEL1 && modifier < LEVEL1_REGEN_MOD ) + { + modifier = LEVEL1_REGEN_MOD; + didBoost = qtrue; + } + else if( class == PCL_ALIEN_LEVEL1_UPG && + modifier < LEVEL1_UPG_REGEN_MOD ) + { + modifier = LEVEL1_UPG_REGEN_MOD; + didBoost = qtrue; + } + + if( didBoost && ent->health < client->ps.stats[ STAT_MAX_HEALTH ] ) + boost->client->pers.hasHealed = qtrue; + } + } + + // Transmit heal rate to the client so it can be displayed on the HUD + client->ps.stats[ STAT_STATE ] |= SS_HEALING_ACTIVE; + client->ps.stats[ STAT_STATE ] &= ~( SS_HEALING_2X | SS_HEALING_3X ); + if( modifier == 1.0f && !G_FindCreep( ent ) ) + { + client->ps.stats[ STAT_STATE ] &= ~SS_HEALING_ACTIVE; + modifier *= ALIEN_REGEN_NOCREEP_MOD; + } + else if( modifier >= 3.0f ) + client->ps.stats[ STAT_STATE ] |= SS_HEALING_3X; + else if( modifier >= 2.0f ) + client->ps.stats[ STAT_STATE ] |= SS_HEALING_2X; + + interval = 1000 / ( regenRate * modifier ); + // if recovery interval is less than frametime, compensate + count = 1 + ( level.time - ent->nextRegenTime ) / interval; + + ent->health += count; + ent->nextRegenTime += count * interval; + + // if at max health, clear damage counters + if( ent->health >= client->ps.stats[ STAT_MAX_HEALTH ] ) + { + ent->health = client->ps.stats[ STAT_MAX_HEALTH ]; + for( i = 0; i < MAX_CLIENTS; i++ ) + ent->credits[ i ] = 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 + if( client->ps.pm_type == PM_NOCLIP ) + client->ps.speed = client->pers.flySpeed; + else + client->ps.speed = g_speed.value * + BG_Class( client->ps.stats[ STAT_CLASS ] )->speed; + + 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( !G_Reactor( ) ) + // BG_DeactivateUpgrade( UP_JETPACK, client->ps.stats ); + } + + // set up for pmove + oldEventSequence = client->ps.eventSequence; + + memset( &pm, 0, sizeof( pm ) ); + + if( ent->flags & FL_FORCE_GESTURE ) + { + ent->flags &= ~FL_FORCE_GESTURE; + ent->client->pers.cmd.buttons |= BUTTON_GESTURE; + } + + // clear fall velocity before every pmove + client->pmext.fallVelocity = 0.0f; + + pm.ps = &client->ps; + pm.pmext = &client->pmext; + pm.cmd = *ucmd; + + if( pm.ps->pm_type == PM_DEAD ) + pm.tracemask = MASK_DEADSOLID; + else + pm.tracemask = MASK_PLAYERSOLID; + + pm.trace = G_PMTraceHack; + 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( g_smoothClients.integer ) + BG_PlayerStateToEntityStateExtraPolate( &ent->client->ps, &ent->s, ent->client->ps.commandTime, qtrue ); + else + BG_PlayerStateToEntityState( &ent->client->ps, &ent->s, qtrue ); + + switch( client->ps.weapon ) + { + case WP_ALEVEL0: + if( !CheckVenomAttack( ent ) ) + { + client->ps.weaponstate = WEAPON_READY; + } + else + { + client->ps.generic1 = WPM_PRIMARY; + G_AddEvent( ent, EV_FIRE_WEAPON, 0 ); + } + break; + + case WP_ALEVEL1: + case WP_ALEVEL1_UPG: + CheckGrabAttack( ent ); + break; + + case WP_ALEVEL3: + case WP_ALEVEL3_UPG: + if( !CheckPounceAttack( ent ) ) + { + client->ps.weaponstate = WEAPON_READY; + } + else + { + client->ps.generic1 = WPM_SECONDARY; + G_AddEvent( ent, EV_FIRE_WEAPON2, 0 ); + } + break; + + case WP_ALEVEL4: + // If not currently in a trample, reset the trample bookkeeping data + if( !( client->ps.pm_flags & PMF_CHARGE ) && client->trampleBuildablesHitPos ) + { + ent->client->trampleBuildablesHitPos = 0; + memset( ent->client->trampleBuildablesHit, 0, sizeof( ent->client->trampleBuildablesHit ) ); + } + break; + + case WP_HBUILD: + CheckCkitRepair( ent ); + break; + + default: + break; + } + + 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_USE_EVOLVE ) && !( client->oldbuttons & BUTTON_USE_EVOLVE ) && + client->ps.stats[ STAT_HEALTH ] > 0 ) + { + trace_t trace; + vec3_t eyes, view, point; + gentity_t *traceEnt; + +#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; + + // look for object infront of player + AngleVectors( client->ps.viewangles, view, NULL, NULL ); + BG_GetClientViewOrigin( &client->ps, eyes ); // !@#CUBOID + VectorMA( eyes, 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->buildableTeam == client->ps.stats[ STAT_TEAM ] && 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->buildableTeam == client->ps.stats[ STAT_TEAM ] && traceEnt->use ) + { + traceEnt->use( traceEnt, ent, ent ); //other and activator are the same in this context + break; + } + } + + if( i == num && client->ps.stats[ STAT_TEAM ] == TEAM_ALIENS ) + { + if( BG_AlienCanEvolve( client->ps.stats[ STAT_CLASS ], + client->pers.credit, + g_alienStage.integer ) ) + { + //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 ); + } + } + } + } + + client->ps.persistant[ PERS_BP ] = G_GetBuildPoints( client->ps.origin, + client->ps.stats[ STAT_TEAM ] ); + client->ps.persistant[ PERS_MARKEDBP ] = G_GetMarkedBuildPoints( client->ps.origin, + client->ps.stats[ STAT_TEAM ] ); + + if( client->ps.persistant[ PERS_BP ] < 0 ) + client->ps.persistant[ PERS_BP ] = 0; + + // 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; + int score, ping; + + // 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 && clientNum < level.maxclients ) + { + cl = &level.clients[ clientNum ]; + if( cl->pers.connected == CON_CONNECTED ) + { + score = ent->client->ps.persistant[ PERS_SCORE ]; + ping = ent->client->ps.ping; + + // Copy + ent->client->ps = cl->ps; + + // Restore + ent->client->ps.persistant[ PERS_SCORE ] = score; + ent->client->ps.ping = ping; + + ent->client->ps.pm_flags |= PMF_FOLLOW; + ent->client->ps.pm_flags &= ~PMF_QUEUED; + } + } + } +} + +/* +============== +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.spectatorState != SPECTATOR_NOT ) + { + SpectatorClientEndFrame( ent ); + return; + } + + pers = &ent->client->pers; + + // + // 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_active.c.orig b/src/game/g_active.c.orig new file mode 100644 index 0000000..cfa6f89 --- /dev/null +++ b/src/game/g_active.c.orig @@ -0,0 +1,1937 @@ +/* +=========================================================================== +Copyright (C) 1999-2005 Id Software, Inc. +Copyright (C) 2000-2009 Darklegion Development + +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" + +/* To create useful and interesting cuboids (like ladders), the ability to specify + * custom surface flags for buildings was needed. This is what the following function + * was written for. It replaces the regular tracing function in PM code (also on the + * client side, grep for CG_PMTraceHack). This function also takes into account + * the modelindex of the entity it found and adjusts the surfaceFlags output + * accordingly. This way custom surface flags for buildings can be done without + * messing around with the PM code or the engine itself. + */ +void G_PMTraceHack(trace_t *results,const vec3_t start,const vec3_t mins,const vec3_t maxs,const vec3_t end,int passEntityNum,int contentmask ) +{ + int modelindex; + trap_Trace(results,start,mins,maxs,end,passEntityNum,contentmask); + if(results->entityNum!=ENTITYNUM_NONE) + { + modelindex=g_entities[results->entityNum].s.modelindex; + if(modelindex>=CUBOID_FIRST&&modelindex<=CUBOID_LAST&&(g_entities[results->entityNum].s.eFlags&EF_B_SPAWNED)) + results->surfaceFlags|=BG_CuboidAttributes(modelindex)->surfaceFlags; + } +} + + +/* +=============== +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( !PM_Live( client->ps.pm_type ) ) + 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( ) < RAND_MAX / 2 + 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; +} + + + +//============================================================== + +/* +============== +ClientShove +============== +*/ +static int GetClientMass( gentity_t *ent ) +{ + int entMass = 100; + + if( ent->client->pers.teamSelection == TEAM_ALIENS ) + entMass = BG_Class( ent->client->pers.classSelection )->health; + else if( ent->client->pers.teamSelection == TEAM_HUMANS ) + { + if( BG_InventoryContainsUpgrade( UP_BATTLESUIT, ent->client->ps.stats ) ) + entMass *= 2; + } + else + return 0; + return entMass; +} + +static void ClientShove( gentity_t *ent, gentity_t *victim ) +{ + vec3_t dir, push; + float force; + int entMass, vicMass; + + // Don't push if the entity is not trying to move + if( !ent->client->pers.cmd.rightmove && !ent->client->pers.cmd.forwardmove && + !ent->client->pers.cmd.upmove ) + return; + + // Cannot push enemy players unless they are walking on the player + if( !OnSameTeam( ent, victim ) && + victim->client->ps.groundEntityNum != ent - g_entities ) + return; + + // Shove force is scaled by relative mass + entMass = GetClientMass( ent ); + vicMass = GetClientMass( victim ); + if( vicMass <= 0 || entMass <= 0 ) + return; + force = g_shove.value * entMass / vicMass; + if( force < 0 ) + force = 0; + if( force > 150 ) + force = 150; + + // Give the victim some shove velocity + VectorSubtract( victim->r.currentOrigin, ent->r.currentOrigin, dir ); + VectorNormalizeFast( dir ); + VectorScale( dir, force, push ); + VectorAdd( victim->client->ps.velocity, push, victim->client->ps.velocity ); + + // Set the pmove timer so that the other client can't cancel + // out the movement immediately + if( !victim->client->ps.pm_time ) + { + int time; + + time = force * 2 + 0.5f; + if( time < 50 ) + time = 50; + if( time > 200 ) + time = 200; + victim->client->ps.pm_time = time; + victim->client->ps.pm_flags |= PMF_TIME_KNOCKBACK; + } +} + +/* +============== +ClientImpacts +============== +*/ +void ClientImpacts( gentity_t *ent, pmove_t *pm ) +{ + int i; + trace_t trace; + gentity_t *other; + + // clear a fake trace struct for touch function + memset( &trace, 0, sizeof( trace ) ); + + for( i = 0; i < pm->numtouch; i++ ) + { + 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; + + // tyrant impact attacks + if( ent->client->ps.weapon == WP_ALEVEL4 ) + { + G_ChargeAttack( ent, other ); + G_CrushAttack( ent, other ); + } + + // shove players + if( ent->client && other->client ) + ClientShove( ent, other ); + + // touch triggers + if( other->touch ) + 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_ClassBoundingBox( ent->client->ps.stats[ STAT_CLASS ], + 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.spectatorState != SPECTATOR_NOT ) + { + 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; + int clientNum; + qboolean attack1, attack3, following, queued; + + 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 ); + + // We are in following mode only if we are following a non-spectating client + following = client->sess.spectatorState == SPECTATOR_FOLLOW; + if( following ) + { + clientNum = client->sess.spectatorClient; + if( clientNum < 0 || clientNum > level.maxclients || + !g_entities[ clientNum ].client || + g_entities[ clientNum ].client->sess.spectatorState != SPECTATOR_NOT ) + following = qfalse; + } + + // Check to see if we are in the spawn queue + if( client->pers.teamSelection == TEAM_ALIENS ) + queued = G_SearchSpawnQueue( &level.alienSpawnQueue, ent - g_entities ); + else if( client->pers.teamSelection == TEAM_HUMANS ) + queued = G_SearchSpawnQueue( &level.humanSpawnQueue, ent - g_entities ); + else + queued = qfalse; + + // Wants to get out of spawn queue + if( attack1 && queued ) + { + if( client->sess.spectatorState == SPECTATOR_FOLLOW ) + G_StopFollowing( ent ); + if( client->ps.stats[ STAT_TEAM ] == TEAM_ALIENS ) + G_RemoveFromSpawnQueue( &level.alienSpawnQueue, client->ps.clientNum ); + else if( client->ps.stats[ STAT_TEAM ] == TEAM_HUMANS ) + G_RemoveFromSpawnQueue( &level.humanSpawnQueue, client->ps.clientNum ); + client->pers.classSelection = PCL_NONE; + client->ps.stats[ STAT_CLASS ] = PCL_NONE; + client->ps.pm_flags &= ~PMF_QUEUED; + queued = qfalse; + } + else if( attack1 ) + { + // Wants to get into spawn queue + if( client->sess.spectatorState == SPECTATOR_FOLLOW ) + G_StopFollowing( ent ); + if( client->pers.teamSelection == TEAM_NONE ) + G_TriggerMenu( client->ps.clientNum, MN_TEAM ); + else if( client->pers.teamSelection == TEAM_ALIENS ) + G_TriggerMenu( client->ps.clientNum, MN_A_CLASS ); + else if( client->pers.teamSelection == TEAM_HUMANS ) + G_TriggerMenu( client->ps.clientNum, MN_H_SPAWN ); + } + + // We are either not following anyone or following a spectator + if( !following ) + { + if( client->sess.spectatorState == SPECTATOR_LOCKED || + client->sess.spectatorState == SPECTATOR_FOLLOW ) + client->ps.pm_type = PM_FREEZE; + else if( client->noclip ) + client->ps.pm_type = PM_NOCLIP; + else + client->ps.pm_type = PM_SPECTATOR; + + if( queued ) + client->ps.pm_flags |= PMF_QUEUED; + + client->ps.speed = client->pers.flySpeed; + client->ps.stats[ STAT_STAMINA ] = 0; + client->ps.stats[ STAT_MISC ] = 0; + client->buildTimer=0; + client->ps.stats[ STAT_BUILDABLE ] = BA_NONE; + client->ps.stats[ STAT_CLASS ] = PCL_NONE; + client->ps.weapon = WP_NONE; + + // Set up for pmove + memset( &pm, 0, sizeof( pm ) ); + pm.ps = &client->ps; + pm.pmext = &client->pmext; + pm.cmd = *ucmd; + pm.tracemask = MASK_DEADSOLID; // spectators can fly through bodies + pm.trace = G_PMTraceHack; + 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 ); + + // Set the queue position and spawn count for the client side + if( client->ps.pm_flags & PMF_QUEUED ) + { + if( client->ps.stats[ STAT_TEAM ] == TEAM_ALIENS ) + { + client->ps.persistant[ PERS_QUEUEPOS ] = + G_GetPosInSpawnQueue( &level.alienSpawnQueue, client->ps.clientNum ); + client->ps.persistant[ PERS_SPAWNS ] = level.numAlienSpawns; + } + else if( client->ps.stats[ STAT_TEAM ] == TEAM_HUMANS ) + { + client->ps.persistant[ PERS_QUEUEPOS ] = + G_GetPosInSpawnQueue( &level.humanSpawnQueue, client->ps.clientNum ); + client->ps.persistant[ PERS_SPAWNS ] = level.numHumanSpawns; + } + } + } +} + + + +/* +================= +ClientInactivityTimer + +Returns qfalse if the client is dropped +================= +*/ +qboolean ClientInactivityTimer( gentity_t *ent ) +{ + gclient_t *client = ent->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 && + !G_admin_permission( ent, ADMF_ACTIVITY ) ) + { + trap_DropClient( client - level.clients, "Dropped due to inactivity" ); + return qfalse; + } + + if( level.time > client->inactivityTime - 10000 && + !client->inactivityWarning && + !G_admin_permission( ent, ADMF_ACTIVITY ) ) + { + 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; + int i; + + ucmd = &ent->client->pers.cmd; + + aForward = abs( ucmd->forwardmove ); + aRight = abs( ucmd->rightmove ); + + 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; + + client = ent->client; + client->time100 += msec; + client->time1000 += msec; + client->time10000 += msec; + + while ( client->time100 >= 100 ) + { + weapon_t weapon = BG_GetPlayerWeapon( &client->ps ); + + client->time100 -= 100; + + // Restore or subtract stamina + if( stopped || client->ps.pm_type == PM_JETPACK ) + client->ps.stats[ STAT_STAMINA ] += STAMINA_STOP_RESTORE; + else if( ( client->ps.stats[ STAT_STATE ] & SS_SPEEDBOOST ) && + !( client->buttons & BUTTON_WALKING ) ) // walk overrides sprint + client->ps.stats[ STAT_STAMINA ] -= STAMINA_SPRINT_TAKE; + else if( walking || crouched ) + client->ps.stats[ STAT_STAMINA ] += STAMINA_WALK_RESTORE; + + // Check stamina limits + if( client->ps.stats[ STAT_STAMINA ] > STAMINA_MAX ) + client->ps.stats[ STAT_STAMINA ] = STAMINA_MAX; + else if( client->ps.stats[ STAT_STAMINA ] < -STAMINA_MAX ) + client->ps.stats[ STAT_STAMINA ] = -STAMINA_MAX; + + if( weapon == WP_ABUILD || weapon == WP_ABUILD2 || + BG_InventoryContainsWeapon( WP_HBUILD, client->ps.stats ) ) + { + client->buildTimer-=MIN(100,client->buildTimer); + G_RecalcBuildTimer(client); + } + + switch( weapon ) + { + case WP_ABUILD: + case WP_ABUILD2: + case WP_HBUILD: + + // Set validity bit on buildable + if( ( client->ps.stats[ STAT_BUILDABLE ] & ~SB_VALID_TOGGLEBIT ) > BA_NONE ) + { + int dist = BG_Class( ent->client->ps.stats[ STAT_CLASS ] )->buildDist; + vec3_t dummy, dummy2; + + if( G_CanBuild( ent, client->ps.stats[ STAT_BUILDABLE ] & ~SB_VALID_TOGGLEBIT, + dist, dummy, dummy2, client->cuboidSelection ) == IBE_NONE ) + client->ps.stats[ STAT_BUILDABLE ] |= SB_VALID_TOGGLEBIT; + else + client->ps.stats[ STAT_BUILDABLE ] &= ~SB_VALID_TOGGLEBIT; + + // Let the client know which buildables will be removed by building + for( i = 0; i < MAX_MISC; i++ ) + { + if( i < level.numBuildablesForRemoval ) + client->ps.misc[ i ] = level.markedBuildables[ i ]->s.number; + else + client->ps.misc[ i ] = 0; + } + } + else + { + for( i = 0; i < MAX_MISC; i++ ) + client->ps.misc[ i ] = 0; + } + break; + + default: + break; + } + + if( ent->client->pers.teamSelection == TEAM_HUMANS && + ( client->ps.stats[ STAT_STATE ] & SS_HEALING_2X ) ) + { + 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_HEALING_2X; + } + 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_HEALING_2X; + } + } + + //use fuel + if( BG_InventoryContainsUpgrade( UP_JETPACK, ent->client->ps.stats ) && + BG_UpgradeIsActive( UP_JETPACK, ent->client->ps.stats ) && + ent->client->ps.stats[ STAT_FUEL ] > 0 ) + { + ent->client->ps.stats[ STAT_FUEL ] -= JETPACK_FUEL_USAGE; + if( ent->client->ps.stats[ STAT_FUEL ] <= 0 ) + { + ent->client->ps.stats[ STAT_FUEL ] = 0; + BG_DeactivateUpgrade( UP_JETPACK, client->ps.stats ); + BG_AddPredictableEventToPlayerstate( EV_JETPACK_DEACTIVATE, 0, &client->ps ); + } + } + + //biores + if( BG_InventoryContainsUpgrade( UP_BIORES, ent->client->ps.stats ) && + ent->health < ent->client->ps.stats[ STAT_MAX_HEALTH ] ) + { + float hp, dmod; + + dmod = MIN( level.time - ent->lastDamageTime, 3000 ) / 3000.0f; + hp = (float)ent->health / ent->client->ps.stats[ STAT_MAX_HEALTH ]; + ent->client->bioresHealTimer += (BIORES_EQUATION) * 10 * dmod; + } + + if( ent->client->bioresHealTimer >= 100 ) + { + int delta; + + delta = ent->client->bioresHealTimer / 100; + ent->health = MAX( MIN( ent->health+delta, ent->client->ps.stats[ STAT_MAX_HEALTH ] ), 1 ); + + ent->client->bioresHealTimer %= 100; + } + + } + + while( client->time1000 >= 1000 ) + { + client->time1000 -= 1000; + + //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_MK1, client->ps.stats ) ) + damage -= HELMET_MK1_POISON_PROTECTION; + + if( BG_InventoryContainsUpgrade( UP_HELMET_MK2, client->ps.stats ) ) + damage -= HELMET_MK2_POISON_PROTECTION; + + if( BG_InventoryContainsUpgrade( UP_LIGHTARMOUR, client->ps.stats ) ) + damage -= LIGHTARMOUR_POISON_PROTECTION; + + if( BG_InventoryContainsUpgrade( UP_BIORES, client->ps.stats ) ) + damage *= BIORES_POISON_MODIFIER; + + G_Damage( ent, client->lastPoisonClient, client->lastPoisonClient, NULL, + 0, damage, 0, MOD_POISON ); + } + + // turn off life support when a team admits defeat + if( client->ps.stats[ STAT_TEAM ] == TEAM_ALIENS && + level.surrenderTeam == TEAM_ALIENS ) + { + G_Damage( ent, NULL, NULL, NULL, NULL, + BG_Class( client->ps.stats[ STAT_CLASS ] )->regenRate, + DAMAGE_NO_ARMOR, MOD_SUICIDE ); + } + else if( client->ps.stats[ STAT_TEAM ] == TEAM_HUMANS && + level.surrenderTeam == TEAM_HUMANS ) + { + G_Damage( ent, NULL, NULL, NULL, NULL, 5, DAMAGE_NO_ARMOR, MOD_SUICIDE ); + } + + // lose some voice enthusiasm + if( client->voiceEnthusiasm > 0.0f ) + client->voiceEnthusiasm -= VOICE_ENTHUSIASM_DECAY; + else + client->voiceEnthusiasm = 0.0f; + + client->pers.aliveSeconds++; + if( g_freeFundPeriod.integer > 0 && + client->pers.aliveSeconds % g_freeFundPeriod.integer == 0 ) + { + // Give clients some credit periodically + if( G_TimeTilSuddenDeath( ) > 0 ) + { + if( client->ps.stats[ STAT_TEAM ] == TEAM_ALIENS ) + G_AddCreditToClient( client, FREEKILL_ALIEN, qtrue ); + else if( client->ps.stats[ STAT_TEAM ] == TEAM_HUMANS ) + G_AddCreditToClient( client, FREEKILL_HUMAN, qtrue ); + } + } + } + + while( client->time10000 >= 10000 ) + { + client->time10000 -= 10000; + + if( ent->client->ps.weapon == WP_ABUILD || + ent->client->ps.weapon == WP_ABUILD2 ) + { + AddScore( ent, ALIEN_BUILDER_SCOREINC ); + } + else if( ent->client->ps.weapon == WP_HBUILD ) + { + AddScore( ent, HUMAN_BUILDER_SCOREINC ); + } + + // Give score to basis that healed other aliens + if( ent->client->pers.hasHealed ) + { + if( client->ps.weapon == WP_ALEVEL1 ) + AddScore( ent, LEVEL1_REGEN_SCOREINC ); + else if( client->ps.weapon == WP_ALEVEL1_UPG ) + AddScore( ent, LEVEL1_UPG_REGEN_SCOREINC ); + + ent->client->pers.hasHealed = qfalse; + } + } + + // Regenerate Adv. Dragoon barbs + if( client->ps.weapon == WP_ALEVEL3_UPG ) + { + if( client->ps.ammo < BG_Weapon( WP_ALEVEL3_UPG )->maxAmmo ) + { + if( ent->timestamp + LEVEL3_BOUNCEBALL_REGEN < level.time ) + { + client->ps.ammo++; + ent->timestamp = level.time; + } + } + else + ent->timestamp = level.time; + } +} + +/* +==================== +ClientIntermissionThink +==================== +*/ +void ClientIntermissionThink( gclient_t *client ) +{ + 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; + class_t class; + + client = ent->client; + class = client->ps.stats[ STAT_CLASS ]; + + 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_Class( class )->health * + BG_Class( class )->fallDamage * fallDistance ); + + VectorSet( dir, 0, 0, 1 ); + BG_ClassBoundingBox( 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; + + 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; + + // 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" ); + } + + 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 ) + { + ClientIntermissionThink( client ); + return; + } + + // spectators don't do much + if( client->sess.spectatorState != SPECTATOR_NOT ) + { + if( client->sess.spectatorState == SPECTATOR_SCOREBOARD ) + return; + + SpectatorThink( ent, ucmd ); + return; + } + + G_namelog_update_score( client ); + + // check for inactivity timer, but never drop the local client of a non-dedicated server + if( !ClientInactivityTimer( ent ) ) + 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_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; + + 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; + + // Update boosted state flags + client->ps.stats[ STAT_STATE ] &= ~SS_BOOSTEDWARNING; + if( client->ps.stats[ STAT_STATE ] & SS_BOOSTED ) + { + if( level.time - client->boostedTime >= BOOST_TIME ) + client->ps.stats[ STAT_STATE ] &= ~SS_BOOSTED; + else if( level.time - client->boostedTime >= BOOST_WARN_TIME ) + client->ps.stats[ STAT_STATE ] |= SS_BOOSTEDWARNING; + } + + // Check if poison cloud has worn off + if( ( client->ps.eFlags & EF_POISONCLOUDED ) && + BG_PlayerPoisonCloudTime( &client->ps ) - level.time + + client->lastPoisonCloudedTime <= 0 ) + client->ps.eFlags &= ~EF_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_HEALING_2X || + ( 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 ) + { + //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_HEALING_2X; + 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 ); + } + } + + // Replenish alien health + if( level.surrenderTeam != client->pers.teamSelection && + ent->nextRegenTime >= 0 && ent->nextRegenTime < level.time ) + { + float regenRate = + BG_Class( ent->client->ps.stats[ STAT_CLASS ] )->regenRate; + + if( ent->health <= 0 || ent->nextRegenTime < 0 || regenRate == 0 ) + ent->nextRegenTime = -1; // no regen + else + { + int entityList[ MAX_GENTITIES ]; + int i, num; + int count, interval; + vec3_t range, mins, maxs; + float modifier = 1.0f; + + VectorSet( range, REGEN_BOOST_RANGE, REGEN_BOOST_RANGE, + REGEN_BOOST_RANGE ); + 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++ ) + { + gentity_t *boost = &g_entities[ entityList[ i ] ]; + + if( Distance( client->ps.origin, boost->s.origin ) > REGEN_BOOST_RANGE ) + continue; + + if( modifier < BOOSTER_REGEN_MOD && boost->s.eType == ET_BUILDABLE && + boost->s.modelindex == BA_A_BOOSTER && boost->spawned && + boost->health > 0 && boost->powered ) + { + modifier = BOOSTER_REGEN_MOD; + continue; + } + + if( boost->s.eType == ET_PLAYER && boost->client && + boost->client->pers.teamSelection == + ent->client->pers.teamSelection && boost->health > 0 ) + { + class_t class = boost->client->ps.stats[ STAT_CLASS ]; + qboolean didBoost = qfalse; + + if( class == PCL_ALIEN_LEVEL1 && modifier < LEVEL1_REGEN_MOD ) + { + modifier = LEVEL1_REGEN_MOD; + didBoost = qtrue; + } + else if( class == PCL_ALIEN_LEVEL1_UPG && + modifier < LEVEL1_UPG_REGEN_MOD ) + { + modifier = LEVEL1_UPG_REGEN_MOD; + didBoost = qtrue; + } + + if( didBoost && ent->health < client->ps.stats[ STAT_MAX_HEALTH ] ) + boost->client->pers.hasHealed = qtrue; + } + } + + // Transmit heal rate to the client so it can be displayed on the HUD + client->ps.stats[ STAT_STATE ] |= SS_HEALING_ACTIVE; + client->ps.stats[ STAT_STATE ] &= ~( SS_HEALING_2X | SS_HEALING_3X ); + if( modifier == 1.0f && !G_FindCreep( ent ) ) + { + client->ps.stats[ STAT_STATE ] &= ~SS_HEALING_ACTIVE; + modifier *= ALIEN_REGEN_NOCREEP_MOD; + } + else if( modifier >= 3.0f ) + client->ps.stats[ STAT_STATE ] |= SS_HEALING_3X; + else if( modifier >= 2.0f ) + client->ps.stats[ STAT_STATE ] |= SS_HEALING_2X; + + interval = 1000 / ( regenRate * modifier ); + // if recovery interval is less than frametime, compensate + count = 1 + ( level.time - ent->nextRegenTime ) / interval; + + ent->health += count; + ent->nextRegenTime += count * interval; + + // if at max health, clear damage counters + if( ent->health >= client->ps.stats[ STAT_MAX_HEALTH ] ) + { + ent->health = client->ps.stats[ STAT_MAX_HEALTH ]; + for( i = 0; i < MAX_CLIENTS; i++ ) + ent->credits[ i ] = 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 + if( client->ps.pm_type == PM_NOCLIP ) + client->ps.speed = client->pers.flySpeed; + else + client->ps.speed = g_speed.value * + BG_Class( client->ps.stats[ STAT_CLASS ] )->speed; + + 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( !G_Reactor( ) ) + // BG_DeactivateUpgrade( UP_JETPACK, client->ps.stats ); + } + + // set up for pmove + oldEventSequence = client->ps.eventSequence; + + memset( &pm, 0, sizeof( pm ) ); + + if( ent->flags & FL_FORCE_GESTURE ) + { + ent->flags &= ~FL_FORCE_GESTURE; + ent->client->pers.cmd.buttons |= BUTTON_GESTURE; + } + + // clear fall velocity before every pmove + client->pmext.fallVelocity = 0.0f; + + pm.ps = &client->ps; + pm.pmext = &client->pmext; + pm.cmd = *ucmd; + + if( pm.ps->pm_type == PM_DEAD ) + pm.tracemask = MASK_DEADSOLID; + else + pm.tracemask = MASK_PLAYERSOLID; + + pm.trace = G_PMTraceHack; + 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( g_smoothClients.integer ) + BG_PlayerStateToEntityStateExtraPolate( &ent->client->ps, &ent->s, ent->client->ps.commandTime, qtrue ); + else + BG_PlayerStateToEntityState( &ent->client->ps, &ent->s, qtrue ); + + switch( client->ps.weapon ) + { + case WP_ALEVEL0: + if( !CheckVenomAttack( ent ) ) + { + client->ps.weaponstate = WEAPON_READY; + } + else + { + client->ps.generic1 = WPM_PRIMARY; + G_AddEvent( ent, EV_FIRE_WEAPON, 0 ); + } + break; + + case WP_ALEVEL1: + case WP_ALEVEL1_UPG: + CheckGrabAttack( ent ); + break; + + case WP_ALEVEL3: + case WP_ALEVEL3_UPG: + if( !CheckPounceAttack( ent ) ) + { + client->ps.weaponstate = WEAPON_READY; + } + else + { + client->ps.generic1 = WPM_SECONDARY; + G_AddEvent( ent, EV_FIRE_WEAPON2, 0 ); + } + break; + + case WP_ALEVEL4: + // If not currently in a trample, reset the trample bookkeeping data + if( !( client->ps.pm_flags & PMF_CHARGE ) && client->trampleBuildablesHitPos ) + { + ent->client->trampleBuildablesHitPos = 0; + memset( ent->client->trampleBuildablesHit, 0, sizeof( ent->client->trampleBuildablesHit ) ); + } + break; + + case WP_HBUILD: + CheckCkitRepair( ent ); + break; + + default: + break; + } + + 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_USE_EVOLVE ) && !( client->oldbuttons & BUTTON_USE_EVOLVE ) && + client->ps.stats[ STAT_HEALTH ] > 0 ) + { + trace_t trace; + vec3_t eyes, view, point; + gentity_t *traceEnt; + +#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; + + // look for object infront of player + AngleVectors( client->ps.viewangles, view, NULL, NULL ); + BG_GetClientViewOrigin( &client->ps, eyes ); // !@#CUBOID + VectorMA( eyes, 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->buildableTeam == client->ps.stats[ STAT_TEAM ] && 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->buildableTeam == client->ps.stats[ STAT_TEAM ] && traceEnt->use ) + { + traceEnt->use( traceEnt, ent, ent ); //other and activator are the same in this context + break; + } + } + + if( i == num && client->ps.stats[ STAT_TEAM ] == TEAM_ALIENS ) + { + if( BG_AlienCanEvolve( client->ps.stats[ STAT_CLASS ], + client->pers.credit, + g_alienStage.integer ) ) + { + //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 ); + } + } + } + } + + client->ps.persistant[ PERS_BP ] = G_GetBuildPoints( client->ps.origin, + client->ps.stats[ STAT_TEAM ] ); + client->ps.persistant[ PERS_MARKEDBP ] = G_GetMarkedBuildPoints( client->ps.origin, + client->ps.stats[ STAT_TEAM ] ); + + if( client->ps.persistant[ PERS_BP ] < 0 ) + client->ps.persistant[ PERS_BP ] = 0; + + // 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; + int score, ping; + + // 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 && clientNum < level.maxclients ) + { + cl = &level.clients[ clientNum ]; + if( cl->pers.connected == CON_CONNECTED ) + { + score = ent->client->ps.persistant[ PERS_SCORE ]; + ping = ent->client->ps.ping; + + // Copy + ent->client->ps = cl->ps; + + // Restore + ent->client->ps.persistant[ PERS_SCORE ] = score; + ent->client->ps.ping = ping; + + ent->client->ps.pm_flags |= PMF_FOLLOW; + ent->client->ps.pm_flags &= ~PMF_QUEUED; + } + } + } +} + +/* +============== +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.spectatorState != SPECTATOR_NOT ) + { + SpectatorClientEndFrame( ent ); + return; + } + + pers = &ent->client->pers; + + // + // 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..e753f19 --- /dev/null +++ b/src/game/g_admin.c @@ -0,0 +1,3277 @@ +/* +=========================================================================== +Copyright (C) 2000-2009 Darklegion Development + +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, qfalse, "ban", + "change the duration or reason of a ban. duration 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] (^5/mask^7) (^5duration^7) (^5reason^7)" + }, + + {"adminhelp", G_admin_adminhelp, qtrue, "adminhelp", + "display admin commands available to you or help on a specific command", + "(^5command^7)" + }, + + {"admintest", G_admin_admintest, qfalse, "admintest", + "display your current admin level", + "" + }, + + {"allowbuild", G_admin_denybuild, qfalse, "denybuild", + "restore a player's ability to build", + "[^3name|slot#^7]" + }, + + {"allready", G_admin_allready, qfalse, "allready", + "makes everyone ready in intermission", + "" + }, + + {"ban", G_admin_ban, qfalse, "ban", + "ban a player by IP and GUID with an optional expiration time and reason." + " duration 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(/mask)^7] (^5duration^7) (^5reason^7)" + }, + + {"builder", G_admin_builder, qtrue, "builder", + "show who built a structure", + "" + }, + + {"buildlog", G_admin_buildlog, qfalse, "buildlog", + "show buildable log", + "(^5name|slot#^7) (^5id^7)" + }, + + {"cancelvote", G_admin_endvote, qfalse, "cancelvote", + "cancel a vote taking place", + "(^5a|h^7)" + }, + + {"changemap", G_admin_changemap, qfalse, "changemap", + "load a map (and optionally force layout)", + "[^3mapname^7] (^5layout^7)" + }, + + {"denybuild", G_admin_denybuild, qfalse, "denybuild", + "take away a player's ability to build", + "[^3name|slot#^7]" + }, + + {"kick", G_admin_kick, qfalse, "kick", + "kick a player with an optional reason", + "[^3name|slot#^7] (^5reason^7)" + }, + + {"listadmins", G_admin_listadmins, qtrue, "listadmins", + "display a list of all server admins and their levels", + "(^5name^7) (^5start admin#^7)" + }, + + {"listlayouts", G_admin_listlayouts, qtrue, "listlayouts", + "display a list of all available layouts for a map", + "(^5mapname^7)" + }, + + {"listplayers", G_admin_listplayers, qtrue, "listplayers", + "display a list of players, their client numbers and their levels", + "" + }, + + {"lock", G_admin_lock, qfalse, "lock", + "lock a team to prevent anyone from joining it", + "[^3a|h^7]" + }, + + {"mute", G_admin_mute, qfalse, "mute", + "mute a player", + "[^3name|slot#^7]" + }, + + {"namelog", G_admin_namelog, qtrue, "namelog", + "display a list of names used by recently connected players", + "(^5name|IP(/mask)^7) (start namelog#)" + }, + + {"nextmap", G_admin_nextmap, qfalse, "nextmap", + "go to the next map in the cycle", + "" + }, + + {"passvote", G_admin_endvote, qfalse, "passvote", + "pass a vote currently taking place", + "(^5a|h^7)" + }, + + {"pause", G_admin_pause, qfalse, "pause", + "Pause (or unpause) the game.", + "" + }, + + {"putteam", G_admin_putteam, qfalse, "putteam", + "move a player to a specified team", + "[^3name|slot#^7] [^3h|a|s^7]" + }, + + {"readconfig", G_admin_readconfig, qfalse, "readconfig", + "reloads the admin config file and refreshes permission flags", + "" + }, + + {"rename", G_admin_rename, qfalse, "rename", + "rename a player", + "[^3name|slot#^7] [^3new name^7]" + }, + + {"restart", G_admin_restart, qfalse, "restart", + "restart the current map (optionally using named layout or keeping/switching teams)", + "(^5layout^7) (^5keepteams|switchteams|keepteamslock|switchteamslock^7)" + }, + + {"revert", G_admin_revert, qfalse, "revert", + "revert buildables to a given time", + "[^3id^7]" + }, + + {"setlevel", G_admin_setlevel, qfalse, "setlevel", + "sets the admin level of a player", + "[^3name|slot#|admin#^7] [^3level^7]" + }, + + {"showbans", G_admin_showbans, qtrue, "showbans", + "display a (partial) list of active bans", + "(^5name|IP(/mask)^7) (^5start at ban#^7)" + }, + + {"spec999", G_admin_spec999, qfalse, "spec999", + "move 999 pingers to the spectator team", + ""}, + + {"time", G_admin_time, qtrue, "time", + "show the current local server time", + ""}, + + {"unban", G_admin_unban, qfalse, "ban", + "unbans a player specified by the slot as seen in showbans", + "[^3ban#^7]" + }, + + {"unlock", G_admin_lock, qfalse, "lock", + "unlock a locked team", + "[^3a|h^7]" + }, + + {"unmute", G_admin_mute, qfalse, "mute", + "unmute a muted player", + "[^3name|slot#^7]" + } + }; + +static size_t adminNumCmds = sizeof( g_admin_cmds ) / sizeof( g_admin_cmds[ 0 ] ); + +static int admin_level_maxname = 0; +g_admin_level_t *g_admin_levels = NULL; +g_admin_admin_t *g_admin_admins = NULL; +g_admin_ban_t *g_admin_bans = NULL; +g_admin_command_t *g_admin_commands = NULL; + +void G_admin_register_cmds( void ) +{ + int i; + + for( i = 0; i < adminNumCmds; i++ ) + trap_AddCommand( g_admin_cmds[ i ].keyword ); +} + +void G_admin_unregister_cmds( void ) +{ + int i; + + for( i = 0; i < adminNumCmds; i++ ) + trap_RemoveCommand( g_admin_cmds[ i ].keyword ); +} + +void G_admin_cmdlist( gentity_t *ent ) +{ + int i; + char out[ MAX_STRING_CHARS ] = ""; + int len, outlen; + + outlen = 0; + + for( i = 0; i < adminNumCmds; i++ ) + { + if( !G_admin_permission( ent, g_admin_cmds[ i ].flag ) ) + continue; + + len = strlen( g_admin_cmds[ i ].keyword ) + 1; + if( len + outlen >= sizeof( out ) - 1 ) + { + trap_SendServerCommand( ent - g_entities, va( "cmds%s\n", out ) ); + outlen = 0; + } + + strcpy( out + outlen, va( " %s", g_admin_cmds[ i ].keyword ) ); + outlen += len; + } + + trap_SendServerCommand( ent - g_entities, va( "cmds%s\n", out ) ); +} + +// match a certain flag within these flags +static qboolean admin_permission( char *flags, const char *flag, qboolean *perm ) +{ + char *token, *token_p = flags; + qboolean allflags = qfalse; + qboolean p = qfalse; + *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 ) ) + { + allflags = qtrue; + p = *perm; + } + } + if( allflags ) + *perm = p; + return allflags; +} + +g_admin_cmd_t *G_admin_cmd( const char *cmd ) +{ + return bsearch( cmd, g_admin_cmds, adminNumCmds, sizeof( g_admin_cmd_t ), + cmdcmp ); +} + +g_admin_level_t *G_admin_level( const int l ) +{ + g_admin_level_t *level; + + for( level = g_admin_levels; level; level = level->next ) + { + if( level->level == l ) + return level; + } + + return NULL; +} + +g_admin_admin_t *G_admin_admin( const char *guid ) +{ + g_admin_admin_t *admin; + + for( admin = g_admin_admins; admin; admin = admin->next ) + { + if( !Q_stricmp( admin->guid, guid ) ) + return admin; + } + + return NULL; +} + +g_admin_command_t *G_admin_command( const char *cmd ) +{ + g_admin_command_t *c; + + for( c = g_admin_commands; c; c = c->next ) + { + if( !Q_stricmp( c->command, cmd ) ) + return c; + } + + return NULL; +} + +qboolean G_admin_permission( gentity_t *ent, const char *flag ) +{ + qboolean perm; + g_admin_admin_t *a; + g_admin_level_t *l; + + // console always wins + if( !ent ) + return qtrue; + + if( ( a = ent->client->pers.admin ) ) + { + if( admin_permission( a->flags, flag, &perm ) ) + return perm; + + l = G_admin_level( a->level ); + } + else + l = G_admin_level( 0 ); + + if( l ) + return admin_permission( l->flags, flag, &perm ) && perm; + + return qfalse; +} + +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 ] = {""}; + g_admin_admin_t *admin; + int alphaCount = 0; + + G_SanitiseString( name, name2, sizeof( name2 ) ); + + if( !strcmp( name2, "unnamedplayer" ) ) + return qtrue; + + if( !strcmp( name2, "console" ) ) + { + if( err && len > 0 ) + Q_strncpyz( err, "The name 'console' is not allowed.", len ); + return qfalse; + } + + G_DecolorString( name, testName, sizeof( testName ) ); + if( isdigit( testName[ 0 ] ) ) + { + if( err && len > 0 ) + Q_strncpyz( err, "Names cannot begin with numbers", len ); + return qfalse; + } + + for( i = 0; testName[ i ]; i++) + { + if( isalpha( testName[ i ] ) ) + alphaCount++; + } + + if( alphaCount == 0 ) + { + if( err && len > 0 ) + Q_strncpyz( err, "Names must contain letters", len ); + return qfalse; + } + + for( i = 0; i < level.maxclients; i++ ) + { + client = &level.clients[ i ]; + if( client->pers.connected == CON_DISCONNECTED ) + 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( !strcmp( name2, testName ) ) + { + if( err && len > 0 ) + Com_sprintf( err, len, "The name '%s^7' is already in use", name ); + return qfalse; + } + } + + for( admin = g_admin_admins; admin; admin = admin->next ) + { + if( admin->level < 1 ) + continue; + G_SanitiseString( admin->name, testName, sizeof( testName ) ); + if( !strcmp( name2, testName ) && ent->client->pers.admin != admin ) + { + if( err && len > 0 ) + Com_sprintf( err, len, "The name '%s^7' belongs to an admin, " + "please use another name", name ); + return qfalse; + } + } + return qtrue; +} + +static qboolean admin_higher_admin( g_admin_admin_t *a, g_admin_admin_t *b ) +{ + qboolean perm; + + if( !b ) + return qtrue; + + if( admin_permission( b->flags, ADMF_IMMUTABLE, &perm ) ) + return !perm; + + return b->level <= ( a ? a->level : 0 ); +} + +static qboolean admin_higher_guid( char *admin_guid, char *victim_guid ) +{ + return admin_higher_admin( G_admin_admin( admin_guid ), + G_admin_admin( victim_guid ) ); +} + +static qboolean admin_higher( gentity_t *admin, gentity_t *victim ) +{ + + // console always wins + if( !admin ) + return qtrue; + + return admin_higher_admin( admin->client->pers.admin, + victim->client->pers.admin ); +} + +static void admin_writeconfig_string( char *s, fileHandle_t f ) +{ + if( s[ 0 ] ) + trap_FS_Write( s, strlen( s ), 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\n", v ); + trap_FS_Write( buf, strlen( buf ), f ); +} + +static void admin_writeconfig( void ) +{ + fileHandle_t f; + int t; + g_admin_admin_t *a; + g_admin_level_t *l; + g_admin_ban_t *b; + g_admin_command_t *c; + + 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 ); + if( trap_FS_FOpenFile( g_admin.string, &f, FS_WRITE ) < 0 ) + { + G_Printf( "admin_writeconfig: could not open g_admin file \"%s\"\n", + g_admin.string ); + return; + } + for( l = g_admin_levels; l; l = l->next ) + { + trap_FS_Write( "[level]\n", 8, f ); + trap_FS_Write( "level = ", 10, f ); + admin_writeconfig_int( l->level, f ); + trap_FS_Write( "name = ", 10, f ); + admin_writeconfig_string( l->name, f ); + trap_FS_Write( "flags = ", 10, f ); + admin_writeconfig_string( l->flags, f ); + trap_FS_Write( "\n", 1, f ); + } + for( a = g_admin_admins; a; a = a->next ) + { + // don't write level 0 users + if( a->level == 0 ) + continue; + + trap_FS_Write( "[admin]\n", 8, f ); + trap_FS_Write( "name = ", 10, f ); + admin_writeconfig_string( a->name, f ); + trap_FS_Write( "guid = ", 10, f ); + admin_writeconfig_string( a->guid, f ); + trap_FS_Write( "level = ", 10, f ); + admin_writeconfig_int( a->level, f ); + trap_FS_Write( "flags = ", 10, f ); + admin_writeconfig_string( a->flags, f ); + trap_FS_Write( "\n", 1, f ); + } + for( b = g_admin_bans; b; b = b->next ) + { + // don't write expired bans + // if expires is 0, then it's a perm ban + if( b->expires != 0 && b->expires <= t ) + continue; + + trap_FS_Write( "[ban]\n", 6, f ); + trap_FS_Write( "name = ", 10, f ); + admin_writeconfig_string( b->name, f ); + trap_FS_Write( "guid = ", 10, f ); + admin_writeconfig_string( b->guid, f ); + trap_FS_Write( "ip = ", 10, f ); + admin_writeconfig_string( b->ip.str, f ); + trap_FS_Write( "reason = ", 10, f ); + admin_writeconfig_string( b->reason, f ); + trap_FS_Write( "made = ", 10, f ); + admin_writeconfig_string( b->made, f ); + trap_FS_Write( "expires = ", 10, f ); + admin_writeconfig_int( b->expires, f ); + trap_FS_Write( "banner = ", 10, f ); + admin_writeconfig_string( b->banner, f ); + trap_FS_Write( "\n", 1, f ); + } + for( c = g_admin_commands; c; c = c->next ) + { + trap_FS_Write( "[command]\n", 10, f ); + trap_FS_Write( "command = ", 10, f ); + admin_writeconfig_string( c->command, f ); + trap_FS_Write( "exec = ", 10, f ); + admin_writeconfig_string( c->exec, f ); + trap_FS_Write( "desc = ", 10, f ); + admin_writeconfig_string( c->desc, f ); + trap_FS_Write( "flag = ", 10, f ); + admin_writeconfig_string( c->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, "="); + s[ 0 ] = '\0'; + t = COM_ParseExt( cnf, qfalse ); + if( strcmp( t, "=" ) ) + { + COM_ParseWarning( "expected '=' before \"%s\"", t ); + Q_strncpyz( s, t, size ); + } + while( 1 ) + { + t = COM_ParseExt( cnf, qfalse ); + if( !*t ) + break; + if( strlen( t ) + strlen( s ) >= size ) + break; + if( *s ) + Q_strcat( s, size, " " ); + Q_strcat( s, size, t ); + } +} + +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 + { + COM_ParseWarning( "expected '=' before \"%s\"", t ); + } + *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 level = 0; + + l = g_admin_levels = BG_Alloc( sizeof( g_admin_level_t ) ); + l->level = level++; + Q_strncpyz( l->name, "^4Unknown Player", sizeof( l->name ) ); + Q_strncpyz( l->flags, + "listplayers admintest adminhelp time", + sizeof( l->flags ) ); + + l = l->next = BG_Alloc( sizeof( g_admin_level_t ) ); + l->level = level++; + Q_strncpyz( l->name, "^5Server Regular", sizeof( l->name ) ); + Q_strncpyz( l->flags, + "listplayers admintest adminhelp time", + sizeof( l->flags ) ); + + l = l->next = BG_Alloc( sizeof( g_admin_level_t ) ); + l->level = level++; + Q_strncpyz( l->name, "^6Team Manager", sizeof( l->name ) ); + Q_strncpyz( l->flags, + "listplayers admintest adminhelp time putteam spec999", + sizeof( l->flags ) ); + + l = l->next = BG_Alloc( sizeof( g_admin_level_t ) ); + l->level = level++; + Q_strncpyz( l->name, "^2Junior Admin", sizeof( l->name ) ); + Q_strncpyz( l->flags, + "listplayers admintest adminhelp time putteam spec999 kick mute ADMINCHAT", + sizeof( l->flags ) ); + + l = l->next = BG_Alloc( sizeof( g_admin_level_t ) ); + l->level = level++; + Q_strncpyz( l->name, "^3Senior Admin", sizeof( l->name ) ); + Q_strncpyz( l->flags, + "listplayers admintest adminhelp time putteam spec999 kick mute showbans ban " + "namelog ADMINCHAT", + sizeof( l->flags ) ); + + l = l->next = BG_Alloc( sizeof( g_admin_level_t ) ); + l->level = level++; + Q_strncpyz( l->name, "^1Server Operator", sizeof( l->name ) ); + Q_strncpyz( l->flags, + "ALLFLAGS -IMMUTABLE -INCOGNITO", + sizeof( l->flags ) ); + admin_level_maxname = 15; +} + +void G_admin_authlog( gentity_t *ent ) +{ + char aflags[ MAX_ADMIN_FLAGS * 2 ]; + g_admin_level_t *level; + int levelNum = 0; + + if( !ent ) + return; + + if( ent->client->pers.admin ) + levelNum = ent->client->pers.admin->level; + + level = G_admin_level( levelNum ); + + Com_sprintf( aflags, sizeof( aflags ), "%s %s", + ent->client->pers.admin->flags, + ( level ) ? level->flags : "" ); + + G_LogPrintf( "AdminAuth: %i \"%s" S_COLOR_WHITE "\" \"%s" S_COLOR_WHITE + "\" [%d] (%s): %s\n", + ent - g_entities, ent->client->pers.netname, + ent->client->pers.admin->name, ent->client->pers.admin->level, + ent->client->pers.guid, aflags ); +} + +static char adminLog[ MAX_STRING_CHARS ]; +static int adminLogLen; +static void admin_log_start( gentity_t *admin, const char *cmd ) +{ + const char *name = admin ? admin->client->pers.netname : "console"; + + adminLogLen = Q_snprintf( adminLog, sizeof( adminLog ), + "%d \"%s" S_COLOR_WHITE "\" \"%s" S_COLOR_WHITE "\" [%d] (%s): %s", + admin ? admin->s.clientNum : -1, + name, + admin && admin->client->pers.admin ? admin->client->pers.admin->name : name, + admin && admin->client->pers.admin ? admin->client->pers.admin->level : 0, + admin ? admin->client->pers.guid : "", + cmd ); +} + +static void admin_log( const char *str ) +{ + if( adminLog[ 0 ] ) + adminLogLen += Q_snprintf( adminLog + adminLogLen, + sizeof( adminLog ) - adminLogLen, ": %s", str ); +} + +static void admin_log_abort( void ) +{ + adminLog[ 0 ] = '\0'; + adminLogLen = 0; +} + +static void admin_log_end( const qboolean ok ) +{ + if( adminLog[ 0 ] ) + G_LogPrintf( "AdminExec: %s: %s\n", ok ? "ok" : "fail", adminLog ); + admin_log_abort( ); +} + +struct llist +{ + struct llist *next; +}; +static int admin_search( gentity_t *ent, + const char *cmd, + const char *noun, + qboolean ( *match )( void *, const void * ), + void ( *out )( void *, char * ), + const void *list, + const void *arg, /* this will be used as char* later */ + int start, + const int offset, + const int limit ) +{ + int i; + int count = 0; + int found = 0; + int total; + int next = 0, end = 0; + char str[ MAX_STRING_CHARS ]; + struct llist *l = (struct llist *)list; + + for( total = 0; l; total++, l = l->next ) ; + if( start < 0 ) + start += total; + else + start -= offset; + if( start < 0 || start > total ) + start = 0; + + ADMBP_begin(); + for( i = 0, l = (struct llist *)list; l; i++, l = l->next ) + { + if( match( l, arg ) ) + { + if( i >= start && ( limit < 1 || count < limit ) ) + { + out( l, str ); + ADMBP( va( "%-3d %s\n", i + offset, str ) ); + count++; + end = i; + } + else if( count == limit ) + { + if( next == 0 ) + next = i; + } + + found++; + } + } + + if( limit > 0 ) + { + ADMBP( va( "^3%s: ^7showing %d of %d %s %d-%d%s%s.", + cmd, count, found, noun, start + offset, end + offset, + *(char *)arg ? " matching " : "", (char *)arg ) ); + if( next ) + ADMBP( va( " use '%s%s%s %d' to see more", cmd, + *(char *)arg ? " " : "", + (char *)arg, + next + offset ) ); + } + ADMBP( "\n" ); + ADMBP_end(); + return next + offset; +} + +static qboolean admin_match( void *admin, const void *match ) +{ + char n1[ MAX_NAME_LENGTH ], n2[ MAX_NAME_LENGTH ]; + G_SanitiseString( (char *)match, n2, sizeof( n2 ) ); + if( !n2[ 0 ] ) + return qtrue; + G_SanitiseString( ( (g_admin_admin_t *)admin )->name, n1, sizeof( n1 ) ); + return strstr( n1, n2 ) ? qtrue : qfalse; +} +static void admin_out( void *admin, char *str ) +{ + g_admin_admin_t *a = (g_admin_admin_t *)admin; + g_admin_level_t *l = G_admin_level( a->level ); + int lncol = 0, i; + for( i = 0; l && l->name[ i ]; i++ ) + { + if( Q_IsColorString( l->name + i ) ) + lncol += 2; + } + Com_sprintf( str, MAX_STRING_CHARS, "%-6d %*s^7 %s", + a->level, admin_level_maxname + lncol - 1, l ? l->name : "(null)", + a->name ); +} +static int admin_listadmins( gentity_t *ent, int start, char *search ) +{ + return admin_search( ent, "listadmins", "admins", admin_match, admin_out, + g_admin_admins, search, start, MAX_CLIENTS, MAX_ADMIN_LISTITEMS ); +} + +#define MAX_DURATION_LENGTH 13 +void G_admin_duration( int secs, char *duration, int dursize ) +{ + // sizeof("12.5 minutes") == 13 + 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 ); +} + +static void G_admin_ban_message( + gentity_t *ent, + g_admin_ban_t *ban, + char *creason, + int clen, + char *areason, + int alen ) +{ + if( creason ) + { + char duration[ MAX_DURATION_LENGTH ]; + G_admin_duration( ban->expires - trap_RealTime( NULL ), duration, + sizeof( duration ) ); + // part of this might get cut off on the connect screen + Com_sprintf( creason, clen, + "You have been banned by %s" S_COLOR_WHITE " duration: %s" + " reason: %s", + ban->banner, + duration, + ban->reason ); + } + + if( areason && ent ) + { + // we just want the ban number + int n = 1; + g_admin_ban_t *b = g_admin_bans; + for( ; b && b != ban; b = b->next, n++ ) + ; + Com_sprintf( areason, alen, + S_COLOR_YELLOW "Banned player %s" S_COLOR_YELLOW + " tried to connect from %s (ban #%d)", + ent->client->pers.netname[ 0 ] ? ent->client->pers.netname : ban->name, + ent->client->pers.ip.str, + n ); + } +} + +static qboolean G_admin_ban_matches( g_admin_ban_t *ban, gentity_t *ent ) +{ + return !Q_stricmp( ban->guid, ent->client->pers.guid ) || + ( !G_admin_permission( ent, ADMF_IMMUNITY ) && + G_AddressCompare( &ban->ip, &ent->client->pers.ip ) ); +} + +static g_admin_ban_t *G_admin_match_ban( gentity_t *ent ) +{ + int t; + g_admin_ban_t *ban; + + t = trap_RealTime( NULL ); + if( ent->client->pers.localClient ) + return NULL; + + for( ban = g_admin_bans; ban; ban = ban->next ) + { + // 0 is for perm ban + if( ban->expires != 0 && ban->expires <= t ) + continue; + + if( G_admin_ban_matches( ban, ent ) ) + return ban; + } + + return NULL; +} + +qboolean G_admin_ban_check( gentity_t *ent, char *reason, int rlen ) +{ + g_admin_ban_t *ban; + char warningMessage[ MAX_STRING_CHARS ]; + + if( ent->client->pers.localClient ) + return qfalse; + + if( ( ban = G_admin_match_ban( ent ) ) ) + { + G_admin_ban_message( ent, ban, reason, rlen, + warningMessage, sizeof( warningMessage ) ); + + // don't spam admins + if( ban->warnCount++ < 5 ) + G_AdminMessage( NULL, warningMessage ); + // and don't fill the console + else if( ban->warnCount < 10 ) + trap_Print( va( "%s%s\n", warningMessage, + ban->warnCount + 1 == 10 ? + S_COLOR_WHITE " - future messages for this ban will be suppressed" : + "" ) ); + return qtrue; + } + + return qfalse; +} + +qboolean G_admin_cmd_check( gentity_t *ent ) +{ + char command[ MAX_ADMIN_CMD_LEN ]; + g_admin_cmd_t *admincmd; + g_admin_command_t *c; + qboolean success; + + command[ 0 ] = '\0'; + trap_Argv( 0, command, sizeof( command ) ); + if( !command[ 0 ] ) + return qfalse; + + Q_strlwr( command ); + admin_log_start( ent, command ); + + if( ( c = G_admin_command( command ) ) ) + { + admin_log( ConcatArgsPrintable( 1 ) ); + if( ( success = G_admin_permission( ent, c->flag ) ) ) + { + if( G_FloodLimited( ent ) ) + return qtrue; + trap_SendConsoleCommand( EXEC_APPEND, c->exec ); + } + else + { + ADMP( va( "^3%s: ^7permission denied\n", c->command ) ); + } + admin_log_end( success ); + return qtrue; + } + + if( ( admincmd = G_admin_cmd( command ) ) ) + { + if( ( success = G_admin_permission( ent, admincmd->flag ) ) ) + { + if( G_FloodLimited( ent ) ) + return qtrue; + if( admincmd->silent ) + admin_log_abort( ); + if( !( success = admincmd->handler( ent ) ) ) + admin_log( ConcatArgsPrintable( 1 ) ); + } + else + { + ADMP( va( "^3%s: ^7permission denied\n", admincmd->keyword ) ); + admin_log( ConcatArgsPrintable( 1 ) ); + } + admin_log_end( success ); + return qtrue; + } + return qfalse; +} + +static void llsort( struct llist **head, int compar( const void *, const void * ) ) +{ + struct llist *a, *b, *t, *l; + int i, c = 1, ns, as, bs; + + if( !*head ) + return; + + do + { + a = *head, l = *head = NULL; + for( ns = 0; a; ns++, a = b ) + { + b = a; + for( i = as = 0; i < c; i++ ) + { + as++; + if( !( b = b->next ) ) + break; + } + for( bs = c; ( b && bs ) || as; l = t ) + { + if( as && ( !bs || !b || compar( a, b ) <= 0 ) ) + t = a, a = a->next, as--; + else + t = b, b = b->next, bs--; + if( l ) + l->next = t; + else + *head = t; + } + } + l->next = NULL; + c *= 2; + } while( ns > 1 ); +} + +static int cmplevel( const void *a, const void *b ) +{ + return ((g_admin_level_t *)b)->level - ((g_admin_level_t *)a)->level; +} + +qboolean G_admin_readconfig( gentity_t *ent ) +{ + 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; + char ip[ 44 ]; + + G_admin_cleanup(); + + if( !g_admin.string[ 0 ] ) + { + ADMP( "^3readconfig: g_admin is not set, not loading configuration " + "from a file\n" ); + return qfalse; + } + + len = trap_FS_FOpenFile( g_admin.string, &f, FS_READ ); + if( len < 0 ) + { + G_Printf( "^3readconfig: ^7could not open admin config file %s\n", + g_admin.string ); + admin_default_levels(); + return qfalse; + } + cnf = BG_Alloc( len + 1 ); + cnf2 = cnf; + trap_FS_Read( cnf, len, f ); + *( cnf + len ) = '\0'; + trap_FS_FCloseFile( f ); + + admin_level_maxname = 0; + + level_open = admin_open = ban_open = command_open = qfalse; + COM_BeginParseSession( g_admin.string ); + while( 1 ) + { + t = COM_Parse( &cnf ); + if( !*t ) + break; + + if( !Q_stricmp( t, "[level]" ) ) + { + if( l ) + l = l->next = BG_Alloc( sizeof( g_admin_level_t ) ); + else + l = g_admin_levels = BG_Alloc( sizeof( g_admin_level_t ) ); + level_open = qtrue; + admin_open = ban_open = command_open = qfalse; + lc++; + } + else if( !Q_stricmp( t, "[admin]" ) ) + { + if( a ) + a = a->next = BG_Alloc( sizeof( g_admin_admin_t ) ); + else + a = g_admin_admins = BG_Alloc( sizeof( g_admin_admin_t ) ); + admin_open = qtrue; + level_open = ban_open = command_open = qfalse; + ac++; + } + else if( !Q_stricmp( t, "[ban]" ) ) + { + if( b ) + b = b->next = BG_Alloc( sizeof( g_admin_ban_t ) ); + else + b = g_admin_bans = BG_Alloc( sizeof( g_admin_ban_t ) ); + ban_open = qtrue; + level_open = admin_open = command_open = qfalse; + bc++; + } + else if( !Q_stricmp( t, "[command]" ) ) + { + if( c ) + c = c->next = BG_Alloc( sizeof( g_admin_command_t ) ); + else + c = g_admin_commands = BG_Alloc( sizeof( g_admin_command_t ) ); + command_open = qtrue; + level_open = admin_open = ban_open = qfalse; + cc++; + } + else 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 ) ); + // max printable name length for formatting + len = Q_PrintStrlen( l->name ); + if( len > admin_level_maxname ) + admin_level_maxname = len; + } + else if( !Q_stricmp( t, "flags" ) ) + { + admin_readconfig_string( &cnf, l->flags, sizeof( l->flags ) ); + } + else + { + COM_ParseError( "[level] unrecognized token \"%s\"", t ); + } + } + 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 + { + COM_ParseError( "[admin] unrecognized token \"%s\"", t ); + } + + } + 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, ip, sizeof( ip ) ); + G_AddressParse( ip, &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, "banner" ) ) + { + admin_readconfig_string( &cnf, b->banner, sizeof( b->banner ) ); + } + else + { + COM_ParseError( "[ban] unrecognized token \"%s\"", t ); + } + } + 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 + { + COM_ParseError( "[command] unrecognized token \"%s\"", t ); + } + } + else + { + COM_ParseError( "unexpected token \"%s\"", t ); + } + } + BG_Free( cnf2 ); + ADMP( va( "^3readconfig: ^7loaded %d levels, %d admins, %d bans, %d commands\n", + lc, ac, bc, cc ) ); + if( lc == 0 ) + admin_default_levels(); + else + { + llsort( (struct llist **)&g_admin_levels, cmplevel ); + llsort( (struct llist **)&g_admin_admins, cmplevel ); + } + + // restore admin mapping + for( i = 0; i < level.maxclients; i++ ) + { + if( level.clients[ i ].pers.connected != CON_DISCONNECTED ) + { + level.clients[ i ].pers.admin = + G_admin_admin( level.clients[ i ].pers.guid ); + if( level.clients[ i ].pers.admin ) + G_admin_authlog( &g_entities[ i ] ); + G_admin_cmdlist( &g_entities[ i ] ); + } + } + + return qtrue; +} + +qboolean G_admin_time( gentity_t *ent ) +{ + qtime_t qt; + + trap_RealTime( &qt ); + ADMP( va( "^3time: ^7local time is %02i:%02i:%02i\n", + qt.tm_hour, qt.tm_min, qt.tm_sec ) ); + return qtrue; +} + +// this should be in one of the headers, but it is only used here for now +namelog_t *G_NamelogFromString( gentity_t *ent, char *s ); + +/* +for consistency, we should be able to target a disconnected player with setlevel +but we can't use namelog and remain consistent, so the solution would be to make +everyone a real level 0 admin so they can be targeted until the next level +but that seems kind of stupid +*/ +qboolean G_admin_setlevel( gentity_t *ent ) +{ + char name[ MAX_NAME_LENGTH ] = {""}; + char lstr[ 12 ]; // 11 is max strlen() for 32-bit (signed) int + char testname[ MAX_NAME_LENGTH ] = {""}; + int i; + gentity_t *vic = NULL; + g_admin_admin_t *a = NULL; + g_admin_level_t *l = NULL; + int na; + + if( trap_Argc() < 3 ) + { + ADMP( "^3setlevel: ^7usage: setlevel [name|slot#] [level]\n" ); + return qfalse; + } + + trap_Argv( 1, testname, sizeof( testname ) ); + trap_Argv( 2, lstr, sizeof( lstr ) ); + + if( !( l = G_admin_level( atoi( lstr ) ) ) ) + { + ADMP( "^3setlevel: ^7level is not defined\n" ); + return qfalse; + } + + if( ent && l->level > + ( ent->client->pers.admin ? ent->client->pers.admin->level : 0 ) ) + { + ADMP( "^3setlevel: ^7you may not use setlevel to set a level higher " + "than your current level\n" ); + return qfalse; + } + + for( na = 0, a = g_admin_admins; a; na++, a = a->next ); + + for( i = 0; testname[ i ] && isdigit( testname[ i ] ); i++ ); + if( !testname[ i ] ) + { + int id = atoi( testname ); + if( id < MAX_CLIENTS ) + { + vic = &g_entities[ id ]; + if( !vic || !vic->client || vic->client->pers.connected == CON_DISCONNECTED ) + { + ADMP( va( "^3setlevel: ^7no player connected in slot %d\n", id ) ); + return qfalse; + } + } + else if( id < na + MAX_CLIENTS ) + for( i = 0, a = g_admin_admins; i < id - MAX_CLIENTS; i++, a = a->next ); + else + { + ADMP( va( "^3setlevel: ^7%s not in range 1-%d\n", + testname, na + MAX_CLIENTS - 1 ) ); + return qfalse; + } + } + else + G_SanitiseString( testname, name, sizeof( name ) ); + + if( vic ) + a = vic->client->pers.admin; + else if( !a ) + { + g_admin_admin_t *wa; + int matches = 0; + + for( wa = g_admin_admins; wa && matches < 2; wa = wa->next ) + { + G_SanitiseString( wa->name, testname, sizeof( testname ) ); + if( strstr( testname, name ) ) + { + a = wa; + matches++; + } + } + + for( i = 0; i < level.maxclients && matches < 2; i++ ) + { + if( level.clients[ i ].pers.connected == CON_DISCONNECTED ) + continue; + + if( matches && level.clients[ i ].pers.admin && + level.clients[ i ].pers.admin == a ) + { + vic = &g_entities[ i ]; + continue; + } + + G_SanitiseString( level.clients[ i ].pers.netname, testname, + sizeof( testname ) ); + if( strstr( testname, name ) ) + { + vic = &g_entities[ i ]; + a = vic->client->pers.admin; + matches++; + } + } + + if( matches == 0 ) + { + ADMP( "^3setlevel:^7 no match. use listplayers or listadmins to " + "find an appropriate number to use instead of name.\n" ); + return qfalse; + } + if( matches > 1 ) + { + ADMP( "^3setlevel:^7 more than one match. Use the admin number " + "instead:\n" ); + admin_listadmins( ent, 0, name ); + return qfalse; + } + } + + if( ent && !admin_higher_admin( ent->client->pers.admin, a ) ) + { + ADMP( "^3setlevel: ^7sorry, but your intended victim has a higher" + " admin level than you\n" ); + return qfalse; + } + + if( !a && vic ) + { + for( a = g_admin_admins; a && a->next; a = a->next ); + if( a ) + a = a->next = BG_Alloc( sizeof( g_admin_admin_t ) ); + else + a = g_admin_admins = BG_Alloc( sizeof( g_admin_admin_t ) ); + vic->client->pers.admin = a; + Q_strncpyz( a->guid, vic->client->pers.guid, sizeof( a->guid ) ); + } + + a->level = l->level; + if( vic ) + Q_strncpyz( a->name, vic->client->pers.netname, sizeof( a->name ) ); + + admin_log( va( "%d (%s) \"%s" S_COLOR_WHITE "\"", a->level, a->guid, + a->name ) ); + + AP( va( + "print \"^3setlevel: ^7%s^7 was given level %d admin rights by %s\n\"", + a->name, a->level, ( ent ) ? ent->client->pers.netname : "console" ) ); + + admin_writeconfig(); + if( vic ) + { + G_admin_authlog( vic ); + G_admin_cmdlist( vic ); + } + return qtrue; +} + +static void admin_create_ban( gentity_t *ent, + char *netname, + char *guid, + addr_t *ip, + int seconds, + char *reason ) +{ + g_admin_ban_t *b = NULL; + qtime_t qt; + int t; + int i; + char *name; + char disconnect[ MAX_STRING_CHARS ]; + + t = trap_RealTime( &qt ); + + for( b = g_admin_bans; b; b = b->next ) + { + if( !b->next ) + break; + } + + if( b ) + { + if( !b->next ) + b = b->next = BG_Alloc( sizeof( g_admin_ban_t ) ); + } + else + b = g_admin_bans = BG_Alloc( sizeof( g_admin_ban_t ) ); + + Q_strncpyz( b->name, netname, sizeof( b->name ) ); + Q_strncpyz( b->guid, guid, sizeof( b->guid ) ); + memcpy( &b->ip, ip, sizeof( b->ip ) ); + + Com_sprintf( b->made, sizeof( b->made ), "%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 ); + + if( ent && ent->client->pers.admin ) + name = ent->client->pers.admin->name; + else if( ent ) + name = ent->client->pers.netname; + else + name = "console"; + + Q_strncpyz( b->banner, name, sizeof( b->banner ) ); + 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 ) ); + + G_admin_ban_message( NULL, b, disconnect, sizeof( disconnect ), NULL, 0 ); + + for( i = 0; i < level.maxclients; i++ ) + { + if( level.clients[ i ].pers.connected == CON_DISCONNECTED ) + continue; + if( G_admin_ban_matches( b, &g_entities[ i ] ) ) + { + trap_SendServerCommand( i, va( "disconnect \"%s\"", disconnect ) ); + + trap_DropClient( i, va( "has been kicked by %s^7. reason: %s", + b->banner, b->reason ) ); + } + } +} + +int G_admin_parse_time( const char *time ) +{ + int seconds = 0, num = 0; + if( !*time ) + return -1; + while( *time ) + { + if( !isdigit( *time ) ) + return -1; + while( isdigit( *time ) ) + num = num * 10 + *time++ - '0'; + + if( !*time ) + break; + switch( *time++ ) + { + 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; + return seconds; +} + +qboolean G_admin_kick( gentity_t *ent ) +{ + int pid; + char name[ MAX_NAME_LENGTH ], *reason, err[ MAX_STRING_CHARS ]; + int minargc; + gentity_t *vic; + + minargc = 3; + if( G_admin_permission( ent, ADMF_UNACCOUNTABLE ) ) + minargc = 2; + + if( trap_Argc() < minargc ) + { + ADMP( "^3kick: ^7usage: kick [name] [reason]\n" ); + return qfalse; + } + trap_Argv( 1, name, sizeof( name ) ); + reason = ConcatArgs( 2 ); + if( ( pid = G_ClientNumberFromString( name, err, sizeof( err ) ) ) == -1 ) + { + ADMP( va( "^3kick: ^7%s", err ) ); + return qfalse; + } + vic = &g_entities[ pid ]; + if( !admin_higher( ent, vic ) ) + { + ADMP( "^3kick: ^7sorry, but your intended victim has a higher admin" + " level than you\n" ); + return qfalse; + } + if( vic->client->pers.localClient ) + { + ADMP( "^3kick: ^7disconnecting the host would end the game\n" ); + return qfalse; + } + admin_log( va( "%d (%s) \"%s" S_COLOR_WHITE "\": \"%s" S_COLOR_WHITE "\"", + pid, + vic->client->pers.guid, + vic->client->pers.netname, + reason ) ); + admin_create_ban( ent, + vic->client->pers.netname, + vic->client->pers.guid, + &vic->client->pers.ip, + MAX( 1, G_admin_parse_time( g_adminTempBan.string ) ), + ( *reason ) ? reason : "kicked by admin" ); + admin_writeconfig(); + + return qtrue; +} + +qboolean G_admin_ban( gentity_t *ent ) +{ + int seconds; + char search[ MAX_NAME_LENGTH ]; + char secs[ MAX_TOKEN_CHARS ]; + char *reason; + char duration[ MAX_DURATION_LENGTH ]; + int i; + addr_t ip; + qboolean ipmatch = qfalse; + namelog_t *match = NULL; + + if( trap_Argc() < 2 ) + { + ADMP( "^3ban: ^7usage: ban [name|slot|IP(/mask)] [duration] [reason]\n" ); + return qfalse; + } + trap_Argv( 1, search, sizeof( search ) ); + trap_Argv( 2, secs, sizeof( secs ) ); + + seconds = G_admin_parse_time( secs ); + if( seconds <= 0 ) + { + seconds = 0; + reason = ConcatArgs( 2 ); + } + else + { + reason = ConcatArgs( 3 ); + } + if( !*reason && !G_admin_permission( ent, ADMF_UNACCOUNTABLE ) ) + { + ADMP( "^3ban: ^7you must specify a reason\n" ); + return qfalse; + } + if( !G_admin_permission( ent, ADMF_CAN_PERM_BAN ) ) + { + int maximum = MAX( 1, G_admin_parse_time( g_adminMaxBan.string ) ); + if( seconds == 0 || seconds > maximum ) + { + ADMP( "^3ban: ^7you may not issue permanent bans\n" ); + seconds = maximum; + } + } + + if( G_AddressParse( search, &ip ) ) + { + int max = ip.type == IPv4 ? 32 : 128; + int min = ent ? max / 2 : 1; + + if( ip.mask < min || ip.mask > max ) + { + ADMP( va( "^3ban: ^7invalid netmask (%d is not one of %d-%d)\n", + ip.mask, min, max ) ); + return qfalse; + } + ipmatch = qtrue; + + for( match = level.namelogs; match; match = match->next ) + { + // skip players in the namelog who have already been banned + if( match->banned ) + continue; + + for( i = 0; i < MAX_NAMELOG_ADDRS && match->ip[ i ].str[ 0 ]; i++ ) + { + if( G_AddressCompare( &ip, &match->ip[ i ] ) ) + break; + } + if( i < MAX_NAMELOG_ADDRS && match->ip[ i ].str[ 0 ] ) + break; + } + + if( !match ) + { + ADMP( "^3ban: ^7no player found by that IP address\n" ); + return qfalse; + } + } + else if( !( match = G_NamelogFromString( ent, search ) ) || match->banned ) + { + ADMP( "^3ban: ^7no match\n" ); + return qfalse; + } + + if( ent && !admin_higher_guid( ent->client->pers.guid, match->guid ) ) + { + + ADMP( "^3ban: ^7sorry, but your intended victim has a higher admin" + " level than you\n" ); + return qfalse; + } + if( match->slot > -1 && level.clients[ match->slot ].pers.localClient ) + { + ADMP( "^3ban: ^7disconnecting the host would end the game\n" ); + return qfalse; + } + + G_admin_duration( ( seconds ) ? seconds : -1, + duration, sizeof( duration ) ); + + AP( va( "print \"^3ban:^7 %s^7 has been banned by %s^7 " + "duration: %s, reason: %s\n\"", + match->name[ match->nameOffset ], + ( ent ) ? ent->client->pers.netname : "console", + duration, + ( *reason ) ? reason : "banned by admin" ) ); + + admin_log( va( "%d (%s) \"%s" S_COLOR_WHITE "\": \"%s" S_COLOR_WHITE "\"", + seconds, match->guid, match->name[ match->nameOffset ], reason ) ); + if( ipmatch ) + { + admin_create_ban( ent, + match->slot == -1 ? + match->name[ match->nameOffset ] : + level.clients[ match->slot ].pers.netname, + match->guid, + &ip, + seconds, reason ); + admin_log( va( "[%s]", ip.str ) ); + } + else + { + // ban all IP addresses used by this player + for( i = 0; i < MAX_NAMELOG_ADDRS && match->ip[ i ].str[ 0 ]; i++ ) + { + admin_create_ban( ent, + match->slot == -1 ? + match->name[ match->nameOffset ] : + level.clients[ match->slot ].pers.netname, + match->guid, + &match->ip[ i ], + seconds, reason ); + admin_log( va( "[%s]", match->ip[ i ].str ) ); + } + } + + match->banned = qtrue; + + if( !g_admin.string[ 0 ] ) + ADMP( "^3ban: ^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 bnum; + int time = trap_RealTime( NULL ); + char bs[ 5 ]; + int i; + g_admin_ban_t *ban, *p; + + if( trap_Argc() < 2 ) + { + ADMP( "^3unban: ^7usage: unban [ban#]\n" ); + return qfalse; + } + trap_Argv( 1, bs, sizeof( bs ) ); + bnum = atoi( bs ); + for( ban = p = g_admin_bans, i = 1; ban && i < bnum; + p = ban, ban = ban->next, i++ ); + if( i != bnum || !ban ) + { + ADMP( "^3unban: ^7invalid ban#\n" ); + return qfalse; + } + if( !G_admin_permission( ent, ADMF_CAN_PERM_BAN ) && + ( ban->expires == 0 || ( ban->expires - time > MAX( 1, + G_admin_parse_time( g_adminMaxBan.string ) ) ) ) ) + { + ADMP( "^3unban: ^7you cannot remove permanent bans\n" ); + return qfalse; + } + admin_log( va( "%d (%s) \"%s" S_COLOR_WHITE "\": \"%s" S_COLOR_WHITE "\": [%s]", + ban->expires ? ban->expires - time : 0, ban->guid, ban->name, ban->reason, + ban->ip.str ) ); + AP( va( "print \"^3unban: ^7ban #%d for %s^7 has been removed by %s\n\"", + bnum, + ban->name, + ( ent ) ? ent->client->pers.netname : "console" ) ); + if( p == ban ) + g_admin_bans = ban->next; + else + p->next = ban->next; + BG_Free( ban ); + admin_writeconfig(); + return qtrue; +} + +qboolean G_admin_adjustban( gentity_t *ent ) +{ + int bnum; + int length, maximum; + int expires; + int time = trap_RealTime( NULL ); + char duration[ MAX_DURATION_LENGTH ] = {""}; + char *reason; + char bs[ 5 ]; + char secs[ MAX_TOKEN_CHARS ]; + char mode = '\0'; + g_admin_ban_t *ban; + int mask = 0; + int i; + int skiparg = 0; + + if( trap_Argc() < 3 ) + { + ADMP( "^3adjustban: ^7usage: adjustban [ban#] [/mask] [duration] [reason]" + "\n" ); + return qfalse; + } + trap_Argv( 1, bs, sizeof( bs ) ); + bnum = atoi( bs ); + for( ban = g_admin_bans, i = 1; ban && i < bnum; ban = ban->next, i++ ); + if( i != bnum || !ban ) + { + ADMP( "^3adjustban: ^7invalid ban#\n" ); + return qfalse; + } + maximum = MAX( 1, G_admin_parse_time( g_adminMaxBan.string ) ); + if( !G_admin_permission( ent, ADMF_CAN_PERM_BAN ) && + ( ban->expires == 0 || ban->expires - time > maximum ) ) + { + ADMP( "^3adjustban: ^7you cannot modify permanent bans\n" ); + return qfalse; + } + trap_Argv( 2, secs, sizeof( secs ) ); + if( secs[ 0 ] == '/' ) + { + int max = ban->ip.type == IPv6 ? 128 : 32; + int min = ent ? max / 2 : 1; + mask = atoi( secs + 1 ); + if( mask < min || mask > max ) + { + ADMP( va( "^3adjustban: ^7invalid netmask (%d is not one of %d-%d)\n", + mask, min, max ) ); + return qfalse; + } + trap_Argv( 3 + 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( ban->expires == 0 && mode ) + { + ADMP( "^3adjustban: ^7new duration must be explicit\n" ); + return qfalse; + } + if( mode == '+' ) + expires = ban->expires + length; + else if( mode == '-' ) + expires = ban->expires - length; + else + expires = time + length; + if( expires <= time ) + { + ADMP( "^3adjustban: ^7ban duration must be positive\n" ); + return qfalse; + } + } + else + length = expires = 0; + if( !G_admin_permission( ent, ADMF_CAN_PERM_BAN ) && + ( length == 0 || length > maximum ) ) + { + ADMP( "^3adjustban: ^7you may not issue permanent bans\n" ); + expires = time + maximum; + } + + ban->expires = expires; + G_admin_duration( ( expires ) ? expires - time : -1, duration, + sizeof( duration ) ); + } + if( mask ) + { + char *p = strchr( ban->ip.str, '/' ); + if( !p ) + p = ban->ip.str + strlen( ban->ip.str ); + if( mask == ( ban->ip.type == IPv6 ? 128 : 32 ) ) + *p = '\0'; + else + Com_sprintf( p, sizeof( ban->ip.str ) - ( p - ban->ip.str ), "/%d", mask ); + ban->ip.mask = mask; + } + reason = ConcatArgs( 3 + skiparg ); + if( *reason ) + Q_strncpyz( ban->reason, reason, sizeof( ban->reason ) ); + admin_log( va( "%d (%s) \"%s" S_COLOR_WHITE "\": \"%s" S_COLOR_WHITE "\": [%s]", + ban->expires ? ban->expires - time : 0, ban->guid, ban->name, ban->reason, + ban->ip.str ) ); + AP( va( "print \"^3adjustban: ^7ban #%d for %s^7 has been updated by %s^7 " + "%s%s%s%s%s%s\n\"", + bnum, + ban->name, + ( ent ) ? ent->client->pers.netname : "console", + ( mask ) ? + va( "netmask: /%d%s", mask, + ( length >= 0 || *reason ) ? ", " : "" ) : "", + ( length >= 0 ) ? "duration: " : "", + duration, + ( length >= 0 && *reason ) ? ", " : "", + ( *reason ) ? "reason: " : "", + reason ) ); + if( ent ) + Q_strncpyz( ban->banner, ent->client->pers.netname, sizeof( ban->banner ) ); + admin_writeconfig(); + return qtrue; +} + +qboolean G_admin_putteam( gentity_t *ent ) +{ + int pid; + char name[ MAX_NAME_LENGTH ], team[ sizeof( "spectators" ) ], + err[ MAX_STRING_CHARS ]; + gentity_t *vic; + team_t teamnum = TEAM_NONE; + + trap_Argv( 1, name, sizeof( name ) ); + trap_Argv( 2, team, sizeof( team ) ); + if( trap_Argc() < 3 ) + { + ADMP( "^3putteam: ^7usage: putteam [name] [h|a|s]\n" ); + return qfalse; + } + + if( ( pid = G_ClientNumberFromString( name, err, sizeof( err ) ) ) == -1 ) + { + ADMP( va( "^3putteam: ^7%s", err ) ); + return qfalse; + } + vic = &g_entities[ pid ]; + if( !admin_higher( ent, vic ) ) + { + ADMP( "^3putteam: ^7sorry, but your intended victim has a higher " + " admin level than you\n" ); + return qfalse; + } + teamnum = G_TeamFromString( team ); + if( teamnum == NUM_TEAMS ) + { + ADMP( va( "^3putteam: ^7unknown team %s\n", team ) ); + return qfalse; + } + if( vic->client->pers.teamSelection == teamnum ) + return qfalse; + admin_log( va( "%d (%s) \"%s" S_COLOR_WHITE "\"", pid, vic->client->pers.guid, + vic->client->pers.netname ) ); + G_ChangeTeam( vic, teamnum ); + + AP( va( "print \"^3putteam: ^7%s^7 put %s^7 on to the %s team\n\"", + ( ent ) ? ent->client->pers.netname : "console", + vic->client->pers.netname, BG_TeamName( teamnum ) ) ); + return qtrue; +} + +qboolean G_admin_changemap( gentity_t *ent ) +{ + char map[ MAX_QPATH ]; + char layout[ MAX_QPATH ] = { "" }; + + if( trap_Argc( ) < 2 ) + { + ADMP( "^3changemap: ^7usage: changemap [map] (layout)\n" ); + return qfalse; + } + + trap_Argv( 1, map, sizeof( map ) ); + + if( !trap_FS_FOpenFile( va( "maps/%s.bsp", map ), NULL, FS_READ ) ) + { + ADMP( va( "^3changemap: ^7invalid map name '%s'\n", map ) ); + return qfalse; + } + + if( trap_Argc( ) > 2 ) + { + trap_Argv( 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( "^3changemap: ^7invalid layout name '%s'\n", layout ) ); + return qfalse; + } + } + admin_log( map ); + admin_log( layout ); + + trap_SendConsoleCommand( EXEC_APPEND, va( "map %s", map ) ); + level.restarted = qtrue; + AP( va( "print \"^3changemap: ^7map '%s' started by %s^7 %s\n\"", map, + ( ent ) ? ent->client->pers.netname : "console", + ( layout[ 0 ] ) ? va( "(forcing layout '%s')", layout ) : "" ) ); + return qtrue; +} + +qboolean G_admin_mute( gentity_t *ent ) +{ + char name[ MAX_NAME_LENGTH ]; + char command[ MAX_ADMIN_CMD_LEN ]; + namelog_t *vic; + + trap_Argv( 0, command, sizeof( command ) ); + if( trap_Argc() < 2 ) + { + ADMP( va( "^3%s: ^7usage: %s [name|slot#]\n", command, command ) ); + return qfalse; + } + trap_Argv( 1, name, sizeof( name ) ); + if( !( vic = G_NamelogFromString( ent, name ) ) ) + { + ADMP( va( "^3%s: ^7no match\n", command ) ); + return qfalse; + } + if( ent && !admin_higher_admin( ent->client->pers.admin, + G_admin_admin( vic->guid ) ) ) + { + ADMP( va( "^3%s: ^7sorry, but your intended victim has a higher admin" + " level than you\n", command ) ); + return qfalse; + } + if( vic->muted ) + { + if( !Q_stricmp( command, "mute" ) ) + { + ADMP( "^3mute: ^7player is already muted\n" ); + return qfalse; + } + vic->muted = qfalse; + if( vic->slot > -1 ) + CPx( vic->slot, "cp \"^1You have been unmuted\"" ); + AP( va( "print \"^3unmute: ^7%s^7 has been unmuted by %s\n\"", + vic->name[ vic->nameOffset ], + ( ent ) ? ent->client->pers.netname : "console" ) ); + } + else + { + if( !Q_stricmp( command, "unmute" ) ) + { + ADMP( "^3unmute: ^7player is not currently muted\n" ); + return qfalse; + } + vic->muted = qtrue; + if( vic->slot > -1 ) + CPx( vic->slot, "cp \"^1You've been muted\"" ); + AP( va( "print \"^3mute: ^7%s^7 has been muted by ^7%s\n\"", + vic->name[ vic->nameOffset ], + ( ent ) ? ent->client->pers.netname : "console" ) ); + } + admin_log( va( "%d (%s) \"%s" S_COLOR_WHITE "\"", vic->slot, vic->guid, + vic->name[ vic->nameOffset ] ) ); + return qtrue; +} + +qboolean G_admin_denybuild( gentity_t *ent ) +{ + char name[ MAX_NAME_LENGTH ]; + char command[ MAX_ADMIN_CMD_LEN ]; + namelog_t *vic; + + trap_Argv( 0, command, sizeof( command ) ); + if( trap_Argc() < 2 ) + { + ADMP( va( "^3%s: ^7usage: %s [name|slot#]\n", command, command ) ); + return qfalse; + } + trap_Argv( 1, name, sizeof( name ) ); + if( !( vic = G_NamelogFromString( ent, name ) ) ) + { + ADMP( va( "^3%s: ^7no match\n", command ) ); + return qfalse; + } + if( ent && !admin_higher_admin( ent->client->pers.admin, + G_admin_admin( vic->guid ) ) ) + { + ADMP( va( "^3%s: ^7sorry, but your intended victim has a higher admin" + " level than you\n", command ) ); + return qfalse; + } + if( vic->denyBuild ) + { + if( !Q_stricmp( command, "denybuild" ) ) + { + ADMP( "^3denybuild: ^7player already has no building rights\n" ); + return qfalse; + } + vic->denyBuild = qfalse; + if( vic->slot > -1 ) + CPx( vic->slot, "cp \"^1You've regained your building rights\"" ); + AP( va( + "print \"^3allowbuild: ^7building rights for ^7%s^7 restored by %s\n\"", + vic->name[ vic->nameOffset ], + ( ent ) ? ent->client->pers.netname : "console" ) ); + } + else + { + if( !Q_stricmp( command, "allowbuild" ) ) + { + ADMP( "^3allowbuild: ^7player already has building rights\n" ); + return qfalse; + } + vic->denyBuild = qtrue; + if( vic->slot > -1 ) + { + level.clients[ vic->slot ].ps.stats[ STAT_BUILDABLE ] = BA_NONE; + CPx( vic->slot, "cp \"^1You've lost your building rights\"" ); + } + AP( va( + "print \"^3denybuild: ^7building rights for ^7%s^7 revoked by ^7%s\n\"", + vic->name[ vic->nameOffset ], + ( ent ) ? ent->client->pers.netname : "console" ) ); + } + admin_log( va( "%d (%s) \"%s" S_COLOR_WHITE "\"", vic->slot, vic->guid, + vic->name[ vic->nameOffset ] ) ); + return qtrue; +} + +qboolean G_admin_listadmins( gentity_t *ent ) +{ + int i; + char search[ MAX_NAME_LENGTH ] = {""}; + char s[ MAX_NAME_LENGTH ] = {""}; + int start = MAX_CLIENTS; + + if( trap_Argc() == 3 ) + { + trap_Argv( 2, s, sizeof( s ) ); + start = atoi( s ); + } + if( trap_Argc() > 1 ) + { + trap_Argv( 1, s, sizeof( s ) ); + i = 0; + if( trap_Argc() == 2 ) + { + i = s[ 0 ] == '-'; + for( ; isdigit( s[ i ] ); i++ ); + } + if( i && !s[ i ] ) + start = atoi( s ); + else + G_SanitiseString( s, search, sizeof( search ) ); + } + + admin_listadmins( ent, start, search ); + return qtrue; +} + +qboolean G_admin_listlayouts( gentity_t *ent ) +{ + char list[ MAX_CVAR_VALUE_STRING ]; + char map[ MAX_QPATH ]; + int count = 0; + char *s; + char layout[ MAX_QPATH ] = { "" }; + int i = 0; + + if( trap_Argc( ) == 2 ) + trap_Argv( 1, map, sizeof( map ) ); + else + trap_Cvar_VariableStringBuffer( "mapname", map, sizeof( map ) ); + + count = G_LayoutList( map, list, sizeof( list ) ); + ADMBP_begin( ); + ADMBP( va( "^3listlayouts:^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 i, j; + gclient_t *p; + char c, t; // color and team letter + char *registeredname; + char lname[ MAX_NAME_LENGTH ]; + char muted, denied; + int colorlen; + char namecleaned[ MAX_NAME_LENGTH ]; + char name2cleaned[ MAX_NAME_LENGTH ]; + g_admin_level_t *l; + g_admin_level_t *d = G_admin_level( 0 ); + qboolean hint; + qboolean canset = G_admin_permission( ent, "setlevel" ); + + ADMBP_begin(); + ADMBP( va( "^3listplayers: ^7%d players connected:\n", + level.numConnectedClients ) ); + for( i = 0; i < level.maxclients; i++ ) + { + p = &level.clients[ i ]; + if( p->pers.connected == CON_DISCONNECTED ) + continue; + if( p->pers.connected == CON_CONNECTING ) + { + t = 'C'; + c = COLOR_YELLOW; + } + else + { + t = toupper( *( BG_TeamName( p->pers.teamSelection ) ) ); + if( p->pers.teamSelection == TEAM_HUMANS ) + c = COLOR_CYAN; + else if( p->pers.teamSelection == TEAM_ALIENS ) + c = COLOR_RED; + else + c = COLOR_WHITE; + } + + muted = p->pers.namelog->muted ? 'M' : ' '; + denied = p->pers.namelog->denyBuild ? 'B' : ' '; + + l = d; + registeredname = NULL; + hint = canset; + if( p->pers.admin ) + { + if( hint ) + hint = admin_higher( ent, &g_entities[ i ] ); + if( hint || !G_admin_permission( &g_entities[ i ], ADMF_INCOGNITO ) ) + { + l = G_admin_level( p->pers.admin->level ); + G_SanitiseString( p->pers.netname, namecleaned, + sizeof( namecleaned ) ); + G_SanitiseString( p->pers.admin->name, + name2cleaned, sizeof( name2cleaned ) ); + if( Q_stricmp( namecleaned, name2cleaned ) ) + registeredname = p->pers.admin->name; + } + } + + if( l ) + Q_strncpyz( lname, l->name, sizeof( lname ) ); + + for( colorlen = j = 0; lname[ j ]; j++ ) + { + if( Q_IsColorString( &lname[ j ] ) ) + colorlen += 2; + } + + ADMBP( va( "%2i ^%c%c^7 %-2i^2%c^7 %*s^7 ^1%c%c^7 %s^7 %s%s%s\n", + i, + c, + t, + l ? l->level : 0, + hint ? '*' : ' ', + admin_level_maxname + colorlen, + lname, + muted, + denied, + p->pers.netname, + ( registeredname ) ? "(a.k.a. " : "", + ( registeredname ) ? registeredname : "", + ( registeredname ) ? S_COLOR_WHITE ")" : "" ) ); + + } + ADMBP_end(); + return qtrue; +} + +static qboolean ban_matchip( void *ban, const void *ip ) +{ + return G_AddressCompare( &((g_admin_ban_t *)ban)->ip, (addr_t *)ip ) || + G_AddressCompare( (addr_t *)ip, &((g_admin_ban_t *)ban)->ip ); +} +static qboolean ban_matchname( void *ban, const void *name ) +{ + char match[ MAX_NAME_LENGTH ]; + + G_SanitiseString( ( (g_admin_ban_t *)ban )->name, match, sizeof( match ) ); + return strstr( match, (const char *)name ) != NULL; +} +static void ban_out( void *ban, char *str ) +{ + int i; + int colorlen1 = 0; + char duration[ MAX_DURATION_LENGTH ]; + char *d_color = S_COLOR_WHITE; + char date[ 11 ]; + g_admin_ban_t *b = ( g_admin_ban_t * )ban; + int t = trap_RealTime( NULL ); + char *made = b->made; + + for( i = 0; b->name[ i ]; i++ ) + { + if( Q_IsColorString( &b->name[ i ] ) ) + colorlen1 += 2; + } + + // only print out the the date part of made + date[ 0 ] = '\0'; + for( i = 0; *made && *made != ' ' && i < sizeof( date ) - 1; i++ ) + date[ i ] = *made++; + date[ i ] = 0; + + if( !b->expires || b->expires - t > 0 ) + G_admin_duration( b->expires ? b->expires - t : - 1, + duration, sizeof( duration ) ); + else + { + Q_strncpyz( duration, "expired", sizeof( duration ) ); + d_color = S_COLOR_CYAN; + } + + Com_sprintf( str, MAX_STRING_CHARS, "%-*s %s%-15s " S_COLOR_WHITE "%-8s %s" + "\n \\__ %s%-*s " S_COLOR_WHITE "%s", + MAX_NAME_LENGTH + colorlen1 - 1, b->name, + ( strchr( b->ip.str, '/' ) ) ? S_COLOR_RED : S_COLOR_WHITE, + b->ip.str, + date, + b->banner, + d_color, + MAX_DURATION_LENGTH - 1, + duration, + b->reason ); +} +qboolean G_admin_showbans( gentity_t *ent ) +{ + int i; + int start = 1; + char filter[ MAX_NAME_LENGTH ] = {""}; + char name_match[ MAX_NAME_LENGTH ] = {""}; + qboolean ipmatch = qfalse; + addr_t ip; + + if( trap_Argc() == 3 ) + { + trap_Argv( 2, filter, sizeof( filter ) ); + start = atoi( filter ); + } + if( trap_Argc() > 1 ) + { + trap_Argv( 1, filter, sizeof( filter ) ); + i = 0; + if( trap_Argc() == 2 ) + for( i = filter[ 0 ] == '-'; isdigit( filter[ i ] ); i++ ); + if( !filter[ i ] ) + start = atoi( filter ); + else if( !( ipmatch = G_AddressParse( filter, &ip ) ) ) + G_SanitiseString( filter, name_match, sizeof( name_match ) ); + } + + admin_search( ent, "showbans", "bans", + ipmatch ? ban_matchip : ban_matchname, + ban_out, g_admin_bans, + ipmatch ? (void * )&ip : (void *)name_match, + start, 1, MAX_ADMIN_SHOWBANS ); + return qtrue; +} + +qboolean G_admin_adminhelp( gentity_t *ent ) +{ + g_admin_command_t *c; + if( trap_Argc() < 2 ) + { + int i; + 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 ) ); + count++; + // show 6 commands per line + if( count % 6 == 0 ) + ADMBP( "\n" ); + } + } + for( c = g_admin_commands; c; c = c->next ) + { + if( !G_admin_permission( ent, c->flag ) ) + continue; + ADMBP( va( "^3%-12s", c->command ) ); + count++; + // show 6 commands per line + if( count % 6 == 0 ) + ADMBP( "\n" ); + } + if( count % 6 ) + ADMBP( "\n" ); + ADMBP( va( "^3adminhelp: ^7%i available commands\n", count ) ); + ADMBP( "run adminhelp [^3command^7] for adminhelp with a specific command.\n" ); + ADMBP_end(); + + return qtrue; + } + else + { + //!adminhelp param + char param[ MAX_ADMIN_CMD_LEN ]; + g_admin_cmd_t *admincmd; + qboolean denied = qfalse; + + trap_Argv( 1, param, sizeof( param ) ); + ADMBP_begin(); + if( ( c = G_admin_command( param ) ) ) + { + if( G_admin_permission( ent, c->flag ) ) + { + ADMBP( va( "^3adminhelp: ^7help for '%s':\n", c->command ) ); + ADMBP( va( " ^3Description: ^7%s\n", c->desc ) ); + ADMBP( va( " ^3Syntax: ^7%s\n", c->command ) ); + ADMBP( va( " ^3Flag: ^7'%s'\n", c->flag ) ); + ADMBP_end( ); + return qtrue; + } + denied = qtrue; + } + if( ( admincmd = G_admin_cmd( param ) ) ) + { + if( G_admin_permission( ent, admincmd->flag ) ) + { + ADMBP( va( "^3adminhelp: ^7help for '%s':\n", admincmd->keyword ) ); + ADMBP( va( " ^3Description: ^7%s\n", admincmd->function ) ); + ADMBP( va( " ^3Syntax: ^7%s %s\n", admincmd->keyword, + admincmd->syntax ) ); + ADMBP( va( " ^3Flag: ^7'%s'\n", admincmd->flag ) ); + ADMBP_end(); + return qtrue; + } + denied = qtrue; + } + ADMBP( va( "^3adminhelp: ^7%s '%s'\n", + denied ? "you do not have permission to use" : "no help found for", + param ) ); + ADMBP_end( ); + return qfalse; + } +} + +qboolean G_admin_admintest( gentity_t *ent ) +{ + g_admin_level_t *l; + + if( !ent ) + { + ADMP( "^3admintest: ^7you are on the console.\n" ); + return qtrue; + } + + l = G_admin_level( ent->client->pers.admin ? ent->client->pers.admin->level : 0 ); + + AP( va( "print \"^3admintest: ^7%s^7 is a level %d admin %s%s^7%s\n\"", + ent->client->pers.netname, + l ? l->level : 0, + l ? "(" : "", + l ? l->name : "", + l ? ")" : "" ) ); + return qtrue; +} + +qboolean G_admin_allready( gentity_t *ent ) +{ + int i = 0; + gclient_t *cl; + + if( !level.intermissiontime ) + { + ADMP( "^3allready: ^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 == TEAM_NONE ) + continue; + + cl->readyToExit = qtrue; + } + AP( va( "print \"^3allready:^7 %s^7 says everyone is READY now\n\"", + ( ent ) ? ent->client->pers.netname : "console" ) ); + return qtrue; +} + +qboolean G_admin_endvote( gentity_t *ent ) +{ + char teamName[ sizeof( "spectators" ) ] = {"s"}; + char command[ MAX_ADMIN_CMD_LEN ]; + team_t team; + qboolean cancel; + char *msg; + + trap_Argv( 0, command, sizeof( command ) ); + cancel = !Q_stricmp( command, "cancelvote" ); + if( trap_Argc() == 2 ) + trap_Argv( 1, teamName, sizeof( teamName ) ); + team = G_TeamFromString( teamName ); + if( team == NUM_TEAMS ) + { + ADMP( va( "^3%s: ^7invalid team '%s'\n", command, teamName ) ); + return qfalse; + } + msg = va( "print \"^3%s: ^7%s^7 decided that everyone voted %s\n\"", + command, ( ent ) ? ent->client->pers.netname : "console", + cancel ? "No" : "Yes" ); + if( !level.voteTime[ team ] ) + { + ADMP( va( "^3%s: ^7no vote in progress\n", command ) ); + return qfalse; + } + admin_log( BG_TeamName( team ) ); + level.voteNo[ team ] = cancel ? level.numVotingClients[ team ] : 0; + level.voteYes[ team ] = cancel ? 0 : level.numVotingClients[ team ]; + G_CheckVote( team ); + if( team == TEAM_NONE ) + AP( msg ); + else + G_TeamCommand( team, msg ); + return qtrue; +} + +qboolean G_admin_spec999( gentity_t *ent ) +{ + 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 == TEAM_NONE ) + continue; + if( vic->client->ps.ping == 999 ) + { + G_ChangeTeam( vic, TEAM_NONE ); + AP( va( "print \"^3spec999: ^7%s^7 moved %s^7 to spectators\n\"", + ( ent ) ? ent->client->pers.netname : "console", + vic->client->pers.netname ) ); + } + } + return qtrue; +} + +qboolean G_admin_rename( gentity_t *ent ) +{ + int pid; + char name[ MAX_NAME_LENGTH ]; + char newname[ MAX_NAME_LENGTH ]; + char err[ MAX_STRING_CHARS ]; + char userinfo[ MAX_INFO_STRING ]; + gentity_t *victim = NULL; + + if( trap_Argc() < 3 ) + { + ADMP( "^3rename: ^7usage: rename [name] [newname]\n" ); + return qfalse; + } + trap_Argv( 1, name, sizeof( name ) ); + Q_strncpyz( newname, ConcatArgs( 2 ), sizeof( newname ) ); + if( ( pid = G_ClientNumberFromString( name, err, sizeof( err ) ) ) == -1 ) + { + ADMP( va( "^3rename: ^7%s", err ) ); + return qfalse; + } + victim = &g_entities[ pid ]; + if( !admin_higher( ent, victim ) ) + { + ADMP( "^3rename: ^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 ) ) ) + { + ADMP( va( "^3rename: ^7%s\n", err ) ); + return qfalse; + } + if( victim->client->pers.connected != CON_CONNECTED ) + { + ADMP( "^3rename: ^7sorry, but your intended victim is still connecting\n" ); + return qfalse; + } + admin_log( va( "%d (%s) \"%s" S_COLOR_WHITE "\"", pid, + victim->client->pers.guid, victim->client->pers.netname ) ); + admin_log( newname ); + trap_GetUserinfo( pid, userinfo, sizeof( userinfo ) ); + AP( va( "print \"^3rename: ^7%s^7 has been renamed to %s^7 by %s\n\"", + victim->client->pers.netname, + newname, + ( ent ) ? ent->client->pers.netname : "console" ) ); + Info_SetValueForKey( userinfo, "name", newname ); + trap_SetUserinfo( pid, userinfo ); + ClientUserinfoChanged( pid, qtrue ); + return qtrue; +} + +qboolean G_admin_restart( gentity_t *ent ) +{ + char layout[ MAX_CVAR_VALUE_STRING ] = { "" }; + char teampref[ MAX_STRING_CHARS ] = { "" }; + int i; + gclient_t *cl; + + if( trap_Argc( ) > 1 ) + { + char map[ MAX_QPATH ]; + + trap_Cvar_VariableStringBuffer( "mapname", map, sizeof( map ) ); + trap_Argv( 1, layout, sizeof( layout ) ); + + // Figure out which argument is which + 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( "^3restart: ^7layout '%s' does not exist\n", layout ) ); + return qfalse; + } + } + else + { + layout[ 0 ] = '\0'; + trap_Argv( 1, teampref, sizeof( teampref ) ); + } + } + + if( trap_Argc( ) > 2 ) + trap_Argv( 2, teampref, sizeof( teampref ) ); + + admin_log( layout ); + admin_log( teampref ); + + if( !Q_stricmpn( teampref, "keepteams", 9 ) ) + { + for( i = 0; i < g_maxclients.integer; i++ ) + { + cl = level.clients + i; + if( cl->pers.connected != CON_CONNECTED ) + continue; + + if( cl->pers.teamSelection == TEAM_NONE ) + continue; + + cl->sess.restartTeam = cl->pers.teamSelection; + } + } + else if( !Q_stricmpn( teampref, "switchteams", 11 ) ) + { + for( i = 0; i < g_maxclients.integer; i++ ) + { + cl = level.clients + i; + + if( cl->pers.connected != CON_CONNECTED ) + continue; + + if( cl->pers.teamSelection == TEAM_HUMANS ) + cl->sess.restartTeam = TEAM_ALIENS; + else if(cl->pers.teamSelection == TEAM_ALIENS ) + cl->sess.restartTeam = TEAM_HUMANS; + } + } + + if( !Q_stricmp( teampref, "switchteamslock" ) || + !Q_stricmp( teampref, "keepteamslock" ) ) + trap_Cvar_Set( "g_lockTeamsAtStart", "1" ); + + trap_SendConsoleCommand( EXEC_APPEND, "map_restart" ); + + AP( va( "print \"^3restart: ^7map restarted by %s %s %s\n\"", + ( ent ) ? ent->client->pers.netname : "console", + ( layout[ 0 ] ) ? va( "^7(forcing layout '%s^7')", layout ) : "", + ( teampref[ 0 ] ) ? va( "^7(with teams option: '%s^7')", teampref ) : "" ) ); + return qtrue; +} + +qboolean G_admin_nextmap( gentity_t *ent ) +{ + AP( va( "print \"^3nextmap: ^7%s^7 decided to load the next map\n\"", + ( ent ) ? ent->client->pers.netname : "console" ) ); + level.lastWin = TEAM_NONE; + trap_SetConfigstring( CS_WINNER, "Evacuation" ); + LogExit( va( "nextmap was run by %s", + ( ent ) ? ent->client->pers.netname : "console" ) ); + return qtrue; +} + +static qboolean namelog_matchip( void *namelog, const void *ip ) +{ + int i; + namelog_t *n = (namelog_t *)namelog; + for( i = 0; i < MAX_NAMELOG_ADDRS && n->ip[ i ].str[ 0 ]; i++ ) + { + if( G_AddressCompare( &n->ip[ i ], (addr_t *)ip ) || + G_AddressCompare( (addr_t *)ip, &n->ip[ i ] ) ) + return qtrue; + } + return qfalse; +} +static qboolean namelog_matchname( void *namelog, const void *name ) +{ + char match[ MAX_NAME_LENGTH ]; + int i; + namelog_t *n = (namelog_t *)namelog; + for( i = 0; i < MAX_NAMELOG_NAMES && n->name[ i ][ 0 ]; i++ ) + { + G_SanitiseString( n->name[ i ], match, sizeof( match ) ); + if( strstr( match, (const char *)name ) ) + return qtrue; + } + return qfalse; +} +static void namelog_out( void *namelog, char *str ) +{ + namelog_t *n = (namelog_t *)namelog; + char *p = str; + int l, l2 = MAX_STRING_CHARS, i; + const char *scolor; + + if( n->slot > -1 ) + { + scolor = S_COLOR_YELLOW; + l = Q_snprintf( p, l2, "%s%-2d", scolor, n->slot ); + p += l; + l2 -= l; + } + else + { + *p++ = '-'; + *p++ = ' '; + *p = '\0'; + l2 -= 2; + scolor = S_COLOR_WHITE; + } + + for( i = 0; i < MAX_NAMELOG_ADDRS && n->ip[ i ].str[ 0 ]; i++ ) + { + l = Q_snprintf( p, l2, " %s", n->ip[ i ].str ); + p += l; + l2 -= l; + } + + for( i = 0; i < MAX_NAMELOG_NAMES && n->name[ i ][ 0 ]; i++ ) + { + l = Q_snprintf( p, l2, " '" S_COLOR_WHITE "%s%s'%s", n->name[ i ], scolor, + i == n->nameOffset ? "*" : "" ); + p += l; + l2 -= l; + } +} +qboolean G_admin_namelog( gentity_t *ent ) +{ + char search[ MAX_NAME_LENGTH ] = {""}; + char s2[ MAX_NAME_LENGTH ] = {""}; + addr_t ip; + qboolean ipmatch = qfalse; + int start = MAX_CLIENTS, i; + + if( trap_Argc() == 3 ) + { + trap_Argv( 2, search, sizeof( search ) ); + start = atoi( search ); + } + if( trap_Argc() > 1 ) + { + trap_Argv( 1, search, sizeof( search ) ); + i = 0; + if( trap_Argc() == 2 ) + for( i = search[ 0 ] == '-'; isdigit( search[ i ] ); i++ ); + if( !search[ i ] ) + start = atoi( search ); + else if( !( ipmatch = G_AddressParse( search, &ip ) ) ) + G_SanitiseString( search, s2, sizeof( s2 ) ); + } + + admin_search( ent, "namelog", "recent players", + ipmatch ? namelog_matchip : namelog_matchname, namelog_out, level.namelogs, + ipmatch ? (void *)&ip : s2, start, MAX_CLIENTS, MAX_ADMIN_LISTITEMS ); + return qtrue; +} + +/* +================== +G_NamelogFromString + +This is similar to G_ClientNumberFromString but for namelog instead +Returns NULL if not exactly 1 match +================== +*/ +namelog_t *G_NamelogFromString( gentity_t *ent, char *s ) +{ + namelog_t *p, *m = NULL; + int i, found = 0; + char n2[ MAX_NAME_LENGTH ] = {""}; + char s2[ MAX_NAME_LENGTH ] = {""}; + + if( !s[ 0 ] ) + return NULL; + + // if a number is provided, it is a clientnum or namelog id + for( i = 0; s[ i ] && isdigit( s[ i ] ); i++ ); + if( !s[ i ] ) + { + i = atoi( s ); + + if( i >= 0 && i < level.maxclients ) + { + if( level.clients[ i ].pers.connected != CON_DISCONNECTED ) + return level.clients[ i ].pers.namelog; + } + else if( i >= MAX_CLIENTS ) + { + for( p = level.namelogs; p; p = p->next ) + { + if( p->id == i ) + break; + } + if( p ) + return p; + } + + return NULL; + } + + // check for a name match + G_SanitiseString( s, s2, sizeof( s2 ) ); + + for( p = level.namelogs; p; p = p->next ) + { + for( i = 0; i < MAX_NAMELOG_NAMES && p->name[ i ][ 0 ]; i++ ) + { + G_SanitiseString( p->name[ i ], n2, sizeof( n2 ) ); + + // if this is an exact match to a current player + if( i == p->nameOffset && p->slot > -1 && !strcmp( s2, n2 ) ) + return p; + + if( strstr( n2, s2 ) ) + m = p; + } + + if( m == p ) + found++; + } + + if( found == 1 ) + return m; + + if( found > 1 ) + admin_search( ent, "namelog", "recent players", namelog_matchname, + namelog_out, level.namelogs, s2, 0, MAX_CLIENTS, -1 ); + + return NULL; +} + +qboolean G_admin_lock( gentity_t *ent ) +{ + char command[ MAX_ADMIN_CMD_LEN ]; + char teamName[ sizeof( "aliens" ) ]; + team_t team; + qboolean lock, fail = qfalse; + + trap_Argv( 0, command, sizeof( command ) ); + if( trap_Argc() < 2 ) + { + ADMP( va( "^3%s: ^7usage: %s [a|h]\n", command, command ) ); + return qfalse; + } + lock = !Q_stricmp( command, "lock" ); + trap_Argv( 1, teamName, sizeof( teamName ) ); + team = G_TeamFromString( teamName ); + + if( team == TEAM_ALIENS ) + { + if( level.alienTeamLocked == lock ) + fail = qtrue; + else + level.alienTeamLocked = lock; + } + else if( team == TEAM_HUMANS ) + { + if( level.humanTeamLocked == lock ) + fail = qtrue; + else + level.humanTeamLocked = lock; + } + else + { + ADMP( va( "^3%s: ^7invalid team: '%s'\n", command, teamName ) ); + return qfalse; + } + + if( fail ) + { + ADMP( va( "^3%s: ^7the %s team is %s locked\n", + command, BG_TeamName( team ), lock ? "already" : "not currently" ) ); + + return qfalse; + } + + admin_log( BG_TeamName( team ) ); + AP( va( "print \"^3%s: ^7the %s team has been %slocked by %s\n\"", + command, BG_TeamName( team ), lock ? "" : "un", + ent ? ent->client->pers.netname : "console" ) ); + + return qtrue; +} + +qboolean G_admin_builder( gentity_t *ent ) +{ + vec3_t forward, right, up; + vec3_t start, end, dist; + trace_t tr; + gentity_t *traceEnt; + buildLog_t *log; + int i; + + if( !ent ) + { + ADMP( "^3builder: ^7console can't aim.\n" ); + return qfalse; + } + + AngleVectors( ent->client->ps.viewangles, forward, right, up ); + if( ent->client->pers.teamSelection != TEAM_NONE && + ent->client->sess.spectatorState == SPECTATOR_NOT ) + 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 ]; + if( tr.fraction < 1.0f && ( traceEnt->s.eType == ET_BUILDABLE ) ) + { + if( !G_admin_permission( ent, "buildlog" ) && + ent->client->pers.teamSelection != TEAM_NONE && + ent->client->pers.teamSelection != traceEnt->buildableTeam ) + { + ADMP( "^3builder: ^7structure not owned by your team\n" ); + return qfalse; + } + + for( i = 0 ; i < level.numBuildLogs; i++ ) + { + log = &level.buildLog[ ( level.buildId - i - 1 ) % MAX_BUILDLOG ]; + if( log->fate != BF_CONSTRUCT || traceEnt->s.modelindex != log->modelindex ) + continue; + + VectorSubtract( traceEnt->s.pos.trBase, log->origin, dist ); + if( VectorLengthSquared( dist ) < 2.0f ) + { + char logid[ 20 ] = {""}; + + if( G_admin_permission( ent, "buildlog" ) ) + Com_sprintf( logid, sizeof( logid ), ", buildlog #%d", + MAX_CLIENTS + level.buildId - i - 1 ); + ADMP( va( "^3builder: ^7%s built by %s^7%s\n", + BG_Buildable( log->modelindex, NULL )->humanName, + log->actor ? + log->actor->name[ log->actor->nameOffset ] : + "<world>", + logid ) ); + break; + } + } + if( i == level.numBuildLogs ) + ADMP( va( "^3builder: ^7%s not in build log, possibly a layout item\n", + BG_Buildable( traceEnt->s.modelindex, NULL )->humanName ) ); + } + else + ADMP( "^3builder: ^7no structure found under crosshair\n" ); + + return qtrue; +} + +qboolean G_admin_pause( gentity_t *ent ) +{ + if( !level.pausedTime ) + { + AP( va( "print \"^3pause: ^7%s^7 paused the game.\n\"", + ( ent ) ? ent->client->pers.netname : "console" ) ); + level.pausedTime = 1; + trap_SendServerCommand( -1, "cp \"The game has been paused. Please wait.\"" ); + } + else + { + // Prevent accidental pause->unpause race conditions by two admins + if( level.pausedTime < 1000 ) + { + ADMP( "^3pause: ^7Unpausing so soon assumed accidental and ignored.\n" ); + return qfalse; + } + + AP( va( "print \"^3pause: ^7%s^7 unpaused the game (Paused for %d sec) \n\"", + ( ent ) ? ent->client->pers.netname : "console", + (int) ( (float) level.pausedTime / 1000.0f ) ) ); + trap_SendServerCommand( -1, "cp \"The game has been unpaused!\"" ); + + level.pausedTime = 0; + } + + return qtrue; +} + +static char *fates[] = +{ + "^2built^7", + "^3deconstructed^7", + "^7replaced^7", + "^3destroyed^7", + "^1TEAMKILLED^7", + "^7unpowered^7", + "removed" +}; +qboolean G_admin_buildlog( gentity_t *ent ) +{ + char search[ MAX_NAME_LENGTH ] = {""}; + char s[ MAX_NAME_LENGTH ] = {""}; + char n[ MAX_NAME_LENGTH ]; + char stamp[ 8 ]; + int id = -1; + int printed = 0; + int time; + int start = MAX_CLIENTS + level.buildId - level.numBuildLogs; + int i = 0, j; + buildLog_t *log; + + if( !level.buildId ) + { + ADMP( "^3buildlog: ^7log is empty\n" ); + return qtrue; + } + + if( trap_Argc() == 3 ) + { + trap_Argv( 2, search, sizeof( search ) ); + start = atoi( search ); + } + if( trap_Argc() > 1 ) + { + trap_Argv( 1, search, sizeof( search ) ); + for( i = search[ 0 ] == '-'; isdigit( search[ i ] ); i++ ); + if( i && !search[ i ] ) + { + id = atoi( search ); + if( trap_Argc() == 2 && ( id < 0 || id >= MAX_CLIENTS ) ) + { + start = id; + id = -1; + } + else if( id < 0 || id >= MAX_CLIENTS || + level.clients[ id ].pers.connected != CON_CONNECTED ) + { + ADMP( "^3buildlog: ^7invalid client id\n" ); + return qfalse; + } + } + else + G_SanitiseString( search, s, sizeof( s ) ); + } + else + start = MAX( -MAX_ADMIN_LISTITEMS, -level.buildId ); + + if( start < 0 ) + start = MAX( level.buildId - level.numBuildLogs, start + level.buildId ); + else + start -= MAX_CLIENTS; + if( start < level.buildId - level.numBuildLogs || start >= level.buildId ) + { + ADMP( "^3buildlog: ^7invalid build ID\n" ); + return qfalse; + } + + if( ent && ent->client->pers.teamSelection != TEAM_NONE ) + trap_SendServerCommand( -1, + va( "print \"^3buildlog: ^7%s^7 requested a log of recent building activity\n\"", + ent->client->pers.netname ) ); + + ADMBP_begin(); + for( i = start; i < level.buildId && printed < MAX_ADMIN_LISTITEMS; i++ ) + { + log = &level.buildLog[ i % MAX_BUILDLOG ]; + if( id >= 0 && id < MAX_CLIENTS ) + { + if( log->actor != level.clients[ id ].pers.namelog ) + continue; + } + else if( s[ 0 ] ) + { + if( !log->actor ) + continue; + for( j = 0; j < MAX_NAMELOG_NAMES && log->actor->name[ j ][ 0 ]; j++ ) + { + G_SanitiseString( log->actor->name[ j ], n, sizeof( n ) ); + if( strstr( n, s ) ) + break; + } + if( j >= MAX_NAMELOG_NAMES || !log->actor->name[ j ][ 0 ] ) + continue; + } + printed++; + time = ( log->time - level.startTime ) / 1000; + Com_sprintf( stamp, sizeof( stamp ), "%3d:%02d", time / 60, time % 60 ); + ADMBP( va( "^2%c^7%-3d %s ^7%s^7 %s%s%s\n", + log->actor && log->fate != BF_REPLACE && log->fate != BF_UNPOWER ? + '*' : ' ', + i + MAX_CLIENTS, + log->actor && ( log->fate == BF_REPLACE || log->fate == BF_UNPOWER ) ? + " \\_" : stamp, + BG_Buildable( log->modelindex, NULL )->humanName, + fates[ log->fate ], + log->actor ? " by " : "", + log->actor ? + log->actor->name[ log->actor->nameOffset ] : + "" ) ); + } + ADMBP( va( "^3buildlog: ^7showing %d build logs %d - %d of %d - %d. %s\n", + printed, start + MAX_CLIENTS, i + MAX_CLIENTS - 1, + level.buildId + MAX_CLIENTS - level.numBuildLogs, + level.buildId + MAX_CLIENTS - 1, + i < level.buildId ? va( "run 'buildlog %s%s%d' to see more", + search, search[ 0 ] ? " " : "", i + MAX_CLIENTS ) : "" ) ); + ADMBP_end(); + return qtrue; +} + +qboolean G_admin_revert( gentity_t *ent ) +{ + char arg[ MAX_TOKEN_CHARS ]; + char time[ MAX_DURATION_LENGTH ]; + int id; + buildLog_t *log; + + if( trap_Argc() != 2 ) + { + ADMP( "^3revert: ^7usage: revert [id]\n" ); + return qfalse; + } + + trap_Argv( 1, arg, sizeof( arg ) ); + id = atoi( arg ) - MAX_CLIENTS; + if( id < level.buildId - level.numBuildLogs || id >= level.buildId ) + { + ADMP( "^3revert: ^7invalid id\n" ); + return qfalse; + } + + log = &level.buildLog[ id % MAX_BUILDLOG ]; + if( !log->actor || log->fate == BF_REPLACE || log->fate == BF_UNPOWER ) + { + // fixme: then why list them with an id # in build log ? - rez + ADMP( "^3revert: ^7you can only revert direct player actions, " + "indicated by ^2* ^7in buildlog\n" ); + return qfalse; + } + + G_admin_duration( ( level.time - log->time ) / 1000, time, + sizeof( time ) ); + admin_log( arg ); + AP( va( "print \"^3revert: ^7%s^7 reverted %d %s over the past %s\n\"", + ent ? ent->client->pers.netname : "console", + level.buildId - id, + ( level.buildId - id ) > 1 ? "changes" : "change", + time ) ); + G_BuildLogRevert( id ); + return qtrue; +} + +/* +================ + 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, sizeof( m2 ) ); + trap_Print( m2 ); + } + else + trap_Print( m ); + } +} + +void G_admin_buffer_begin( void ) +{ + 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( void ) +{ + g_admin_level_t *l; + g_admin_admin_t *a; + g_admin_ban_t *b; + g_admin_command_t *c; + void *n; + + for( l = g_admin_levels; l; l = n ) + { + n = l->next; + BG_Free( l ); + } + g_admin_levels = NULL; + for( a = g_admin_admins; a; a = n ) + { + n = a->next; + BG_Free( a ); + } + g_admin_admins = NULL; + for( b = g_admin_bans; b; b = n ) + { + n = b->next; + BG_Free( b ); + } + g_admin_bans = NULL; + for( c = g_admin_commands; c; c = n ) + { + n = c->next; + BG_Free( c ); + } + g_admin_commands = NULL; + BG_DefragmentMemory( ); +} diff --git a/src/game/g_admin.h b/src/game/g_admin.h new file mode 100644 index 0000000..8024fc8 --- /dev/null +++ b/src/game/g_admin.h @@ -0,0 +1,197 @@ +/* +=========================================================================== +Copyright (C) 2000-2009 Darklegion Development + +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_FLAG_LEN 20 +#define MAX_ADMIN_FLAGS 1024 +#define MAX_ADMIN_CMD_LEN 20 +#define MAX_ADMIN_BAN_REASON 50 + +/* + * IMMUNITY - cannot be vote kicked, vote muted + * NOCENSORFLOOD - cannot be censored or flood protected + * 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 + * 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 + * ALLFLAGS - all flags (including command flags) apply to this player + * ADMINCHAT - receieves and can send /a admin messages + * ALWAYSFRIEND - always marked as a friendly entity during buildgames + */ +#define ADMF_IMMUNITY "IMMUNITY" +#define ADMF_NOCENSORFLOOD "NOCENSORFLOOD" +#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_ACTIVITY "ACTIVITY" + +#define ADMF_IMMUTABLE "IMMUTABLE" +#define ADMF_INCOGNITO "INCOGNITO" +#define ADMF_ALLFLAGS "ALLFLAGS" +#define ADMF_ADMINCHAT "ADMINCHAT" + +#define ADMF_ALWAYSFRIEND "ALWAYSFRIEND" + +#define MAX_ADMIN_LISTITEMS 20 +#define MAX_ADMIN_SHOWBANS 10 + +// 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 ); + qboolean silent; + char *flag; + char *function; // used for !help + char *syntax; // used for !help +} +g_admin_cmd_t; + +typedef struct g_admin_level +{ + struct g_admin_level *next; + int level; + char name[ MAX_NAME_LENGTH ]; + char flags[ MAX_ADMIN_FLAGS ]; +} +g_admin_level_t; + +typedef struct g_admin_admin +{ + struct g_admin_admin *next; + int level; + char guid[ 33 ]; + char name[ MAX_NAME_LENGTH ]; + char flags[ MAX_ADMIN_FLAGS ]; +} +g_admin_admin_t; + +#define ADDRLEN 16 +/* +addr_ts are passed as "arg" to admin_search for IP address matching +admin_search prints (char *)arg, so the stringified address needs to be first +*/ +typedef struct +{ + char str[ 44 ]; + enum + { + IPv4, + IPv6 + } type; + byte addr[ ADDRLEN ]; + int mask; +} addr_t; + +typedef struct g_admin_ban +{ + struct g_admin_ban *next; + char name[ MAX_NAME_LENGTH ]; + char guid[ 33 ]; + addr_t ip; + char reason[ MAX_ADMIN_BAN_REASON ]; + char made[ 18 ]; // big enough for strftime() %c + int expires; + char banner[ MAX_NAME_LENGTH ]; + int warnCount; +} +g_admin_ban_t; + +typedef struct g_admin_command +{ + struct g_admin_command *next; + char command[ MAX_ADMIN_CMD_LEN ]; + char exec[ MAX_QPATH ]; + char desc[ 50 ]; + char flag[ MAX_ADMIN_FLAG_LEN ]; +} +g_admin_command_t; + +void G_admin_register_cmds( void ); +void G_admin_unregister_cmds( void ); +void G_admin_cmdlist( gentity_t *ent ); + +qboolean G_admin_ban_check( gentity_t *ent, char *reason, int rlen ); +qboolean G_admin_cmd_check( gentity_t *ent ); +qboolean G_admin_readconfig( gentity_t *ent ); +qboolean G_admin_permission( gentity_t *ent, const char *flag ); +qboolean G_admin_name_check( gentity_t *ent, char *name, char *err, int len ); +g_admin_admin_t *G_admin_admin( const char *guid ); +void G_admin_authlog( gentity_t *ent ); + +// admin command functions +qboolean G_admin_time( gentity_t *ent ); +qboolean G_admin_setlevel( gentity_t *ent ); +qboolean G_admin_kick( gentity_t *ent ); +qboolean G_admin_adjustban( gentity_t *ent ); +qboolean G_admin_ban( gentity_t *ent ); +qboolean G_admin_unban( gentity_t *ent ); +qboolean G_admin_putteam( gentity_t *ent ); +qboolean G_admin_listadmins( gentity_t *ent ); +qboolean G_admin_listlayouts( gentity_t *ent ); +qboolean G_admin_listplayers( gentity_t *ent ); +qboolean G_admin_changemap( gentity_t *ent ); +qboolean G_admin_mute( gentity_t *ent ); +qboolean G_admin_denybuild( gentity_t *ent ); +qboolean G_admin_showbans( gentity_t *ent ); +qboolean G_admin_adminhelp( gentity_t *ent ); +qboolean G_admin_admintest( gentity_t *ent ); +qboolean G_admin_allready( gentity_t *ent ); +qboolean G_admin_endvote( gentity_t *ent ); +qboolean G_admin_spec999( gentity_t *ent ); +qboolean G_admin_rename( gentity_t *ent ); +qboolean G_admin_restart( gentity_t *ent ); +qboolean G_admin_nextmap( gentity_t *ent ); +qboolean G_admin_namelog( gentity_t *ent ); +qboolean G_admin_lock( gentity_t *ent ); +qboolean G_admin_pause( gentity_t *ent ); +qboolean G_admin_builder( gentity_t *ent ); +qboolean G_admin_buildlog( gentity_t *ent ); +qboolean G_admin_revert( gentity_t *ent ); + +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 ); + +#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..ae3a97d --- /dev/null +++ b/src/game/g_buildable.c @@ -0,0 +1,4548 @@ +/* +=========================================================================== +Copyright (C) 1999-2005 Id Software, Inc. +Copyright (C) 2000-2009 Darklegion Development + +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_SetBuildableAnim + +Triggers an animation client side +================ +*/ +void G_SetBuildableAnim( gentity_t *ent, buildableAnimNumber_t anim, qboolean force ) +{ + int localAnim = anim | ( ent->s.legsAnim & ANIM_TOGGLEBIT ); + + if( force ) + localAnim |= ANIM_FORCEBIT; + + // don't flip the togglebit more than once per frame + if( ent->animTime != level.time ) + { + ent->animTime = level.time; + localAnim ^= 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, const vec3_t origin, + const 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_BuildableBoundingBox( 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 ); + } + else if( spawn == BA_H_SPAWN ) + { + BG_ClassBoundingBox( PCL_HUMAN, cmins, cmaxs, NULL, NULL, NULL ); + + VectorCopy( origin, localOrigin ); + localOrigin[ 2 ] += maxs[ 2 ] + fabs( cmins[ 2 ] ) + 1.0f; + } + else + return NULL; + + 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 ) + return &g_entities[ tr.entityNum ]; + + if( spawnOrigin != NULL ) + VectorCopy( localOrigin, spawnOrigin ); + + return NULL; +} + +#define POWER_REFRESH_TIME 2000 + +/* +================ +G_FindPower + +attempt to find power for self, return qtrue if successful +================ +*/ +qboolean G_FindPower( gentity_t *self, qboolean searchUnspawned ) +{ + int i, j; + gentity_t *ent, *ent2; + gentity_t *closestPower = NULL; + int distance = 0; + int minDistance = REPEATER_BASESIZE + 1; + vec3_t temp_v; + + if( self->buildableTeam != TEAM_HUMANS ) + return qfalse; + + // Reactor is always powered + if( self->s.modelindex == BA_H_REACTOR ) + { + self->parentNode = self; + + return qtrue; + } + + // Handle repeaters + if( self->s.modelindex == BA_H_REPEATER ) + { + self->parentNode = G_Reactor( ); + + return self->parentNode != NULL; + } + + // Iterate through entities + for( i = MAX_CLIENTS, 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 ) && + ( searchUnspawned || ent->spawned ) && ent->powered && ent->health > 0 ) + { + VectorSubtract( self->s.origin, ent->s.origin, temp_v ); + distance = VectorLength( temp_v ); + + // Always prefer a reactor if there is one in range + if( ent->s.modelindex == BA_H_REACTOR && distance <= REACTOR_BASESIZE ) + { + // Only power as much BP as the reactor can hold + if( self->s.modelindex != BA_NONE ) + { + int buildPoints = g_humanBuildPoints.integer; + + // Scan the buildables in the reactor zone + for( j = MAX_CLIENTS, ent2 = g_entities + j; j < level.num_entities; j++, ent2++ ) + { + gentity_t *powerEntity; + + if( ent2->s.eType != ET_BUILDABLE ) + continue; + + if( ent2 == self ) + continue; + + powerEntity = ent2->parentNode; + + if( powerEntity && powerEntity->s.modelindex == BA_H_REACTOR && ( powerEntity == ent ) ) + { + buildPoints -= BG_Buildable( ent2->s.modelindex, ent2->cuboidSize )->buildPoints; + } + } + + buildPoints -= level.humanBuildPointQueue; + + buildPoints -= BG_Buildable( self->s.modelindex, self->cuboidSize )->buildPoints; + + if( buildPoints >= 0 ) + { + self->parentNode = ent; + return qtrue; + } + else + { + // a buildable can still be built if it shares BP from two zones + + // TODO: handle combined power zones here + } + } + + // Dummy buildables don't need to look for zones + else + { + self->parentNode = ent; + return qtrue; + } + } + else if( distance < minDistance ) + { + // It's a repeater, so check that enough BP will be available to power + // the buildable but only if self is a real buildable + + if( self->s.modelindex != BA_NONE ) + { + int buildPoints = g_humanRepeaterBuildPoints.integer; + + // Scan the buildables in the repeater zone + for( j = MAX_CLIENTS, ent2 = g_entities + j; j < level.num_entities; j++, ent2++ ) + { + gentity_t *powerEntity; + + if( ent2->s.eType != ET_BUILDABLE ) + continue; + + if( ent2 == self ) + continue; + + powerEntity = ent2->parentNode; + + if( powerEntity && powerEntity->s.modelindex == BA_H_REPEATER && ( powerEntity == ent ) ) + { + buildPoints -= BG_Buildable( ent2->s.modelindex, ent->cuboidSize )->buildPoints; + } + } + + if( ent->usesBuildPointZone && level.buildPointZones[ ent->buildPointZone ].active ) + buildPoints -= level.buildPointZones[ ent->buildPointZone ].queuedBuildPoints; + + buildPoints -= BG_Buildable( self->s.modelindex, self->cuboidSize )->buildPoints; + + if( buildPoints >= 0 ) + { + closestPower = ent; + minDistance = distance; + } + else + { + // a buildable can still be built if it shares BP from two zones + + // TODO: handle combined power zones here + } + } + else + { + // Dummy buildables don't need to look for zones + closestPower = ent; + minDistance = distance; + } + } + } + } + + self->parentNode = closestPower; + return self->parentNode != NULL; +} + +/* +================ +G_PowerEntityForPoint + +Simple wrapper to G_FindPower to find the entity providing +power for the specified point +================ +*/ +gentity_t *G_PowerEntityForPoint( const vec3_t origin ) +{ + gentity_t dummy; + + dummy.parentNode = NULL; + dummy.buildableTeam = TEAM_HUMANS; + dummy.s.modelindex = BA_NONE; + VectorCopy( origin, dummy.s.origin ); + + if( G_FindPower( &dummy, qfalse ) ) + return dummy.parentNode; + else + return NULL; +} + +/* +================ +G_PowerEntityForEntity + +Simple wrapper to G_FindPower to find the entity providing +power for the specified entity +================ +*/ +gentity_t *G_PowerEntityForEntity( gentity_t *ent ) +{ + if( G_FindPower( ent, qfalse ) ) + return ent->parentNode; + return NULL; +} + +/* +================ +G_IsPowered + +Check if a location has power, returning the entity type +that is providing it +================ +*/ +buildable_t G_IsPowered( vec3_t origin ) +{ + gentity_t *ent = G_PowerEntityForPoint( origin ); + + if( ent ) + return ent->s.modelindex; + else + return BA_NONE; +} + + +/* +================== +G_GetBuildPoints + +Get the number of build points from a position +================== +*/ +int G_GetBuildPoints( const vec3_t pos, team_t team ) +{ + if( G_TimeTilSuddenDeath( ) <= 0 ) + { + return 0; + } + else if( team == TEAM_ALIENS ) + { + return level.alienBuildPoints; + } + else if( team == TEAM_HUMANS ) + { + gentity_t *powerPoint = G_PowerEntityForPoint( pos ); + + if( powerPoint && powerPoint->s.modelindex == BA_H_REACTOR ) + return level.humanBuildPoints; + + if( powerPoint && powerPoint->s.modelindex == BA_H_REPEATER && + powerPoint->usesBuildPointZone && level.buildPointZones[ powerPoint->buildPointZone ].active ) + { + return level.buildPointZones[ powerPoint->buildPointZone ].totalBuildPoints - + level.buildPointZones[ powerPoint->buildPointZone ].queuedBuildPoints; + } + + // Return the BP of the main zone by default + return level.humanBuildPoints; + } + + return 0; +} + +/* +================== +G_GetMarkedBuildPoints + +Get the number of marked build points from a position +================== +*/ +int G_GetMarkedBuildPoints( const vec3_t pos, team_t team ) +{ + gentity_t *ent; + int i; + int sum = 0; + + if( G_TimeTilSuddenDeath( ) <= 0 ) + return 0; + + if( !g_markDeconstruct.integer ) + return 0; + + for( i = MAX_CLIENTS, ent = g_entities + i; i < level.num_entities; i++, ent++ ) + { + if( ent->s.eType != ET_BUILDABLE ) + continue; + + if( team == TEAM_HUMANS && + ent->s.modelindex != BA_H_REACTOR && + ent->s.modelindex != BA_H_REPEATER && + ent->parentNode != G_PowerEntityForPoint( pos ) ) + continue; + + if( !ent->inuse ) + continue; + + if( ent->health <= 0 ) + continue; + + if( ent->buildableTeam != team ) + continue; + + if( ent->deconstruct ) + sum += BG_Buildable( ent->s.modelindex, ent->cuboidSize )->buildPoints; + } + + return sum; +} + +/* +================== +G_InPowerZone + +See if a buildable is inside of another power zone. +Return pointer to provider if so. +It's different from G_FindPower because FindPower for +providers will find themselves. +(This doesn't check if power zones overlap) +================== +*/ +gentity_t *G_InPowerZone( gentity_t *self ) +{ + int i; + gentity_t *ent; + int distance; + vec3_t temp_v; + + for( i = MAX_CLIENTS, ent = g_entities + i; i < level.num_entities; i++, ent++ ) + { + if( ent->s.eType != ET_BUILDABLE ) + continue; + + if( ent == self ) + continue; + + if( !ent->spawned ) + continue; + + if( ent->health <= 0 ) + 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 && ent->powered ) + { + VectorSubtract( self->s.origin, ent->s.origin, temp_v ); + distance = VectorLength( temp_v ); + + if( ent->s.modelindex == BA_H_REACTOR && distance <= REACTOR_BASESIZE ) + return ent; + else if( ent->s.modelindex == BA_H_REPEATER && distance <= REPEATER_BASESIZE ) + return ent; + } + } + + return NULL; +} + +/* +================ +G_FindDCC + +attempt to find a controlling DCC for self, return number found +================ +*/ +int G_FindDCC( gentity_t *self ) +{ + int i; + gentity_t *ent; + int distance = 0; + vec3_t temp_v; + int foundDCC = 0; + + if( self->buildableTeam != TEAM_HUMANS ) + return 0; + + //iterate through entities + for( i = MAX_CLIENTS, 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( distance < DC_RANGE && ent->powered ) + { + foundDCC++; + } + } + } + + return foundDCC; +} + +/* +================ +G_IsDCCBuilt + +See if any powered DCC exists +================ +*/ +qboolean G_IsDCCBuilt( void ) +{ + int i; + gentity_t *ent; + + for( i = MAX_CLIENTS, ent = g_entities + i; i < level.num_entities; i++, ent++ ) + { + if( ent->s.eType != ET_BUILDABLE ) + continue; + + if( ent->s.modelindex != BA_H_DCC ) + continue; + + if( !ent->spawned ) + continue; + + if( ent->health <= 0 ) + continue; + + return qtrue; + } + + return qfalse; +} + +/* +================ +G_Reactor +G_Overmind + +Since there's only one of these and we quite often want to find them, cache the +results, but check them for validity each time + +The code here will break if more than one reactor or overmind is allowed, even +if one of them is dead/unspawned +================ +*/ +static gentity_t *G_FindBuildable( buildable_t buildable ); + +gentity_t *G_Reactor( void ) +{ + static gentity_t *rc; + + // If cache becomes invalid renew it + if( !rc || rc->s.eType != ET_BUILDABLE || rc->s.modelindex != BA_H_REACTOR ) + rc = G_FindBuildable( BA_H_REACTOR ); + + // If we found it and it's alive, return it + if( rc && rc->spawned && rc->health > 0 ) + return rc; + + return NULL; +} + +gentity_t *G_Overmind( void ) +{ + static gentity_t *om; + + // If cache becomes invalid renew it + if( !om || om->s.eType != ET_BUILDABLE || om->s.modelindex != BA_A_OVERMIND ) + om = G_FindBuildable( BA_A_OVERMIND ); + + // If we found it and it's alive, return it + if( om && om->spawned && om->health > 0 ) + return om; + + return NULL; +} + +/* +================ +G_FindCreep + +attempt to find creep for self, return qtrue if successful +================ +*/ +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->client || self->parentNode == NULL || !self->parentNode->inuse || + self->parentNode->health <= 0 ) + { + for( i = MAX_CLIENTS, 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 && ent->health > 0 ) + { + VectorSubtract( self->s.origin, ent->s.origin, temp_v ); + distance = VectorLength( temp_v ); + if( distance < minDistance ) + { + closestSpawn = ent; + minDistance = distance; + } + } + } + + if( minDistance <= CREEP_BASESIZE ) + { + if( !self->client ) + self->parentNode = closestSpawn; + return qtrue; + } + else + return qfalse; + } + + if( self->client ) + 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_Buildable( buildable, self->cuboidSize )->creepSize; + + 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_TEAM ] == TEAM_HUMANS && + enemy->client->ps.groundEntityNum != ENTITYNUM_NONE ) + { + 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 ) +{ +} + +//================================================================================== + + + +/* +================ +AGeneric_CreepRecede + +Called when an alien buildable dies +================ +*/ +void AGeneric_CreepRecede( gentity_t *self ) +{ + //if the creep just died begin the recession + if( !( self->s.eFlags & EF_DEAD ) ) + { + self->s.eFlags |= EF_DEAD; + G_QueueBuildPoints( self ); + + G_RewardAttackers( self ); + + 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_Buildable( self->s.modelindex, self->cuboidSize )->buildTime ) ) ) ); + } + + //creep is still receeding + if( ( self->timestamp + 10000 ) > level.time ) + self->nextthink = level.time + 500; + else //creep has died + G_FreeEntity( self ); +} + +/* +================ +AGeneric_Blast + +Called when an Alien buildable explodes after dead state +================ +*/ +void AGeneric_Blast( gentity_t *self ) +{ + vec3_t dir; + + VectorCopy( self->s.origin2, dir ); + + //do a bit of radius damage + G_SelectiveRadiusDamage( self->s.pos.trBase, g_entities + self->killedBy, self->splashDamage, + self->splashRadius, self, self->splashMethodOfDeath, + TEAM_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 = AGeneric_CreepRecede; + self->nextthink = level.time + 500; + + self->r.contents = 0; //stop collisions... + trap_LinkEntity( self ); //...requires a relink +} + +/* +================ +AGeneric_Die + +Called when an Alien buildable is killed and enters a brief dead state prior to +exploding. +================ +*/ +void AGeneric_Die( gentity_t *self, gentity_t *inflictor, gentity_t *attacker, int damage, int mod ) +{ + G_SetBuildableAnim( self, BANIM_DESTROY1, qtrue ); + G_SetIdleBuildableAnim( self, BANIM_DESTROYED ); + + self->die = nullDieFunction; + self->killedBy = attacker - g_entities; + self->think = AGeneric_Blast; + self->s.eFlags &= ~EF_FIRING; //prevent any firing effects + self->powered = qfalse; + + if( self->spawned ) + self->nextthink = level.time + 5000; + else + self->nextthink = level.time; //blast immediately + + G_LogDestruction( self, attacker, mod ); +} + +/* +================ +AGeneric_CreepCheck + +Tests for creep and kills the buildable if there is none +================ +*/ +void AGeneric_CreepCheck( gentity_t *self ) +{ + gentity_t *spawn; + + spawn = self->parentNode; + if( !G_FindCreep( self ) ) + { + if( spawn ) + G_Damage( self, NULL, g_entities + spawn->killedBy, NULL, NULL, + self->health, 0, MOD_NOCREEP ); + else + G_Damage( self, NULL, NULL, NULL, NULL, self->health, 0, MOD_NOCREEP ); + return; + } + G_CreepSlow( self ); +} + +/* +================ +AGeneric_Think + +A generic think function for Alien buildables +================ +*/ +void AGeneric_Think( gentity_t *self ) +{ + self->powered = G_Overmind( ) != NULL; + self->nextthink = level.time + BG_Buildable( self->s.modelindex, NULL )->nextthink; + AGeneric_CreepCheck( self ); +} + +/* +================ +AGeneric_Pain + +A generic pain function for Alien buildables +================ +*/ +void AGeneric_Pain( gentity_t *self, gentity_t *attacker, int damage ) +{ + if( self->health <= 0 ) + return; + + // Alien buildables only have the first pain animation defined + G_SetBuildableAnim( self, BANIM_PAIN1, qfalse ); +} + + + + +//================================================================================== + +/* +================ +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 ) + { + if( ent->builtBy >= 0 ) // don't queue the bp from this + G_Damage( ent, NULL, g_entities + ent->builtBy, NULL, NULL, 10000, 0, MOD_SUICIDE ); + else + 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; + } + + if( ent->s.eType == ET_CORPSE ) + G_FreeEntity( ent ); //quietly remove + } + } + } + + G_CreepSlow( self ); + + self->nextthink = level.time + BG_Buildable( self->s.modelindex, NULL )->nextthink; +} + + + + + +//================================================================================== + + + + + +#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 ) +{ + vec3_t range = { OVERMIND_ATTACK_RANGE, OVERMIND_ATTACK_RANGE, OVERMIND_ATTACK_RANGE }; + int i; + + if( self->spawned && ( self->health > 0 ) ) + { + //do some damage + if( G_SelectiveRadiusDamage( self->s.pos.trBase, self, self->splashDamage, + self->splashRadius, self, MOD_OVERMIND, TEAM_ALIENS ) ) + { + self->timestamp = level.time; + 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; + + // shut up during intermission + if( level.intermissiontime ) + level.overmindMuted = qtrue; + + //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_Buildable( self->s.modelindex, NULL )->nextthink; +} + + + + + +//================================================================================== + + + + + +/* +================ +ABarricade_Pain + +Barricade pain animation depends on shrunk state +================ +*/ +void ABarricade_Pain( gentity_t *self, gentity_t *attacker, int damage ) +{ + if( self->health <= 0 ) + return; + + if( !self->shrunkTime ) + G_SetBuildableAnim( self, BANIM_PAIN1, qfalse ); + else + G_SetBuildableAnim( self, BANIM_PAIN2, qfalse ); +} + +/* +================ +ABarricade_Shrink + +Set shrink state for a barricade. When unshrinking, checks to make sure there +is enough room. +================ +*/ +void ABarricade_Shrink( gentity_t *self, qboolean shrink ) +{ + if ( !self->spawned || self->health <= 0 ) + shrink = qtrue; + if ( shrink && self->shrunkTime ) + { + int anim; + + // We need to make sure that the animation has been set to shrunk mode + // because we start out shrunk but with the construct animation when built + self->shrunkTime = level.time; + anim = self->s.torsoAnim & ~( ANIM_FORCEBIT | ANIM_TOGGLEBIT ); + if ( self->spawned && self->health > 0 && anim != BANIM_DESTROYED ) + { + G_SetIdleBuildableAnim( self, BANIM_DESTROYED ); + G_SetBuildableAnim( self, BANIM_ATTACK1, qtrue ); + } + return; + } + + if ( !shrink && ( !self->shrunkTime || + level.time < self->shrunkTime + BARRICADE_SHRINKTIMEOUT ) ) + return; + + BG_BuildableBoundingBox( BA_A_BARRICADE, self->r.mins, self->r.maxs ); + + if ( shrink ) + { + self->r.maxs[ 2 ] = (int)( self->r.maxs[ 2 ] * BARRICADE_SHRINKPROP ); + self->shrunkTime = level.time; + + // shrink animation, the destroy animation is used + if ( self->spawned && self->health > 0 ) + { + G_SetBuildableAnim( self, BANIM_ATTACK1, qtrue ); + G_SetIdleBuildableAnim( self, BANIM_DESTROYED ); + } + } + else + { + trace_t tr; + int anim; + + trap_Trace( &tr, self->s.origin, self->r.mins, self->r.maxs, + self->s.origin, self->s.number, MASK_PLAYERSOLID ); + if ( tr.startsolid || tr.fraction < 1.0f ) + { + self->r.maxs[ 2 ] = (int)( self->r.maxs[ 2 ] * BARRICADE_SHRINKPROP ); + return; + } + self->shrunkTime = 0; + + // unshrink animation, IDLE2 has been hijacked for this + anim = self->s.legsAnim & ~( ANIM_FORCEBIT | ANIM_TOGGLEBIT ); + if ( self->spawned && self->health > 0 && + anim != BANIM_CONSTRUCT1 && anim != BANIM_CONSTRUCT2 ) + { + G_SetIdleBuildableAnim( self, BG_Buildable( BA_A_BARRICADE, NULL )->idleAnim ); + G_SetBuildableAnim( self, BANIM_ATTACK2, qtrue ); + } + } + + // a change in size requires a relink + if ( self->spawned ) + trap_LinkEntity( self ); +} + +/* +================ +ABarricade_Die + +Called when an alien barricade dies +================ +*/ +void ABarricade_Die( gentity_t *self, gentity_t *inflictor, gentity_t *attacker, int damage, int mod ) +{ + AGeneric_Die( self, inflictor, attacker, damage, mod ); + ABarricade_Shrink( self, qtrue ); +} + +/* +================ +ABarricade_Think + +Think function for Alien Barricade +================ +*/ +void ABarricade_Think( gentity_t *self ) +{ + AGeneric_Think( self ); + + // Shrink if unpowered + ABarricade_Shrink( self, !self->powered ); +} + +/* +================ +ABarricade_Touch + +Barricades shrink when they are come into contact with an Alien that can +pass through +================ +*/ + +void ABarricade_Touch( gentity_t *self, gentity_t *other, trace_t *trace ) +{ + gclient_t *client = other->client; + int client_z, min_z; + + if( !client || client->pers.teamSelection != TEAM_ALIENS ) + return; + + // Client must be high enough to pass over. Note that STEPSIZE (18) is + // hardcoded here because we don't include bg_local.h! + client_z = other->s.origin[ 2 ] + other->r.mins[ 2 ]; + min_z = self->s.origin[ 2 ] - 18 + + (int)( self->r.maxs[ 2 ] * BARRICADE_SHRINKPROP ); + if( client_z < min_z ) + return; + ABarricade_Shrink( self, qtrue ); +} + +//================================================================================== + + + + +/* +================ +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; + + AGeneric_Think( self ); + + VectorAdd( self->s.origin, range, maxs ); + VectorSubtract( self->s.origin, range, mins ); + + // attack nearby humans + if( self->spawned && self->health > 0 && self->powered ) + { + 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, CONTENTS_SOLID ) ) + continue; + + if( enemy->client && enemy->client->ps.stats[ STAT_TEAM ] == TEAM_HUMANS ) + { + // start the attack animation + if( level.time >= self->timestamp + ACIDTUBE_REPEAT_ANIM ) + { + self->timestamp = level.time; + G_SetBuildableAnim( self, BANIM_ATTACK1, qfalse ); + G_AddEvent( self, EV_ALIEN_ACIDTUBE, DirToByte( self->s.origin2 ) ); + } + + G_SelectiveRadiusDamage( self->s.pos.trBase, self, ACIDTUBE_DAMAGE, + ACIDTUBE_RANGE, self, MOD_ATUBE, TEAM_ALIENS ); + self->nextthink = level.time + ACIDTUBE_REPEAT; + return; + } + } + } +} + + + + +//================================================================================== + +/* +================ +AHive_CheckTarget + +Returns true and fires the hive missile if the target is valid +================ +*/ +static qboolean AHive_CheckTarget( gentity_t *self, gentity_t *enemy ) +{ + trace_t trace; + vec3_t tip_origin, dirToTarget; + + // Check if this is a valid target + if( enemy->health <= 0 || !enemy->client || + enemy->client->ps.stats[ STAT_TEAM ] != TEAM_HUMANS ) + return qfalse; + + if( enemy->flags & FL_NOTARGET ) + return qfalse; + + // Check if the tip of the hive can see the target + VectorMA( self->s.pos.trBase, self->r.maxs[ 2 ], self->s.origin2, + tip_origin ); + if( Distance( tip_origin, enemy->s.origin ) > HIVE_SENSE_RANGE ) + return qfalse; + + trap_Trace( &trace, tip_origin, NULL, NULL, enemy->s.pos.trBase, + self->s.number, MASK_SHOT ); + if( trace.fraction < 1.0f && trace.entityNum != enemy->s.number ) + return qfalse; + + 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 qtrue; +} + +/* +================ +AHive_Think + +Think function for Alien Hive +================ +*/ +void AHive_Think( gentity_t *self ) +{ + int start; + + AGeneric_Think( self ); + + // Hive missile hasn't returned in HIVE_REPEAT seconds, forget about it + if( self->timestamp < level.time ) + self->active = qfalse; + + // Find a target to attack + if( self->spawned && !self->active && self->powered ) + { + int i, num, entityList[ MAX_GENTITIES ]; + vec3_t mins, maxs, + range = { HIVE_SENSE_RANGE, HIVE_SENSE_RANGE, HIVE_SENSE_RANGE }; + + VectorAdd( self->s.origin, range, maxs ); + VectorSubtract( self->s.origin, range, mins ); + + num = trap_EntitiesInBox( mins, maxs, entityList, MAX_GENTITIES ); + + if( num == 0 ) + return; + + start = rand( ) / ( RAND_MAX / num + 1 ); + for( i = start; i < num + start; i++ ) + { + if( AHive_CheckTarget( self, g_entities + entityList[ i % num ] ) ) + return; + } + } +} + +/* +================ +AHive_Pain + +pain function for Alien Hive +================ +*/ +void AHive_Pain( gentity_t *self, gentity_t *attacker, int damage ) +{ + if( self->spawned && self->powered && !self->active ) + AHive_CheckTarget( self, attacker ); + + G_SetBuildableAnim( self, BANIM_PAIN1, qfalse ); +} + + +//================================================================================== + + +/* +================ +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( !self->spawned || !self->powered || self->health <= 0 ) + return; + + if( !client ) + return; + + if( client->ps.stats[ STAT_TEAM ] == TEAM_HUMANS ) + return; + + if( other->flags & FL_NOTARGET ) + return; // notarget cancels even beneficial effects? + + client->ps.stats[ STAT_STATE ] |= SS_BOOSTED; + client->boostedTime = level.time; + + // give the rant bomb + if( client->lastRantBombTime + LEVEL4_BOMB_REGEN <= level.time ) + { + client->lastRantBombTime = level.time; + client->ps.ammo = 1; + } +} + + + + +//================================================================================== + +#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_Buildable( self->s.modelindex, NULL )->turretRange; + int lowMsec = 0; + int highMsec = (int)( ( + ( ( distanceToTarget * LOCKBLOB_SPEED ) + + ( distanceToTarget * BG_Class( enemy->client->ps.stats[ STAT_CLASS ] )->speed ) ) / + ( 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_TEAM ] == TEAM_ALIENS ) // one of us? + return qfalse; + if( target->client->sess.spectatorState != SPECTATOR_NOT ) // 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; + int i; + int start; + + // iterate through entities + // note that if we exist then level.num_entities != 0 + start = rand( ) / ( RAND_MAX / level.num_entities + 1 ); + for( i = start; i < level.num_entities + start; i++ ) + { + target = g_entities + ( i % level.num_entities ); + //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_Buildable( self->s.modelindex, NULL )->turretRange; + int firespeed = BG_Buildable( self->s.modelindex, NULL )->turretFireSpeed; + + AGeneric_Think( self ); + + if( self->spawned && self->powered ) + { + //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 ); + } +} + + + + +//================================================================================== + + + + +/* +================ +G_SuicideIfNoPower + +Destroy human structures that have been unpowered too long +================ +*/ +static qboolean G_SuicideIfNoPower( gentity_t *self ) +{ + if( self->buildableTeam != TEAM_HUMANS ) + return qfalse; + + if( !self->powered ) + { + // if the power hasn't reached this buildable for some time, then destroy the buildable + if( self->count == 0 ) + self->count = level.time; + else if( ( level.time - self->count ) >= HUMAN_BUILDABLE_INACTIVE_TIME ) + { + if( self->parentNode ) + G_Damage( self, NULL, g_entities + self->parentNode->killedBy, + NULL, NULL, self->health, 0, MOD_NOCREEP ); + else + G_Damage( self, NULL, NULL, NULL, NULL, self->health, 0, MOD_NOCREEP ); + return qtrue; + } + } + else + self->count = 0; + + return qfalse; +} + +/* +================ +G_IdlePowerState + +Set buildable idle animation to match power state +================ +*/ +static void G_IdlePowerState( gentity_t *self ) +{ + if( self->powered ) + { + if( self->s.torsoAnim == BANIM_IDLE3 ) + G_SetIdleBuildableAnim( self, BG_Buildable( self->s.modelindex, NULL )->idleAnim ); + } + else + { + if( self->s.torsoAnim != BANIM_IDLE3 ) + G_SetIdleBuildableAnim( self, BANIM_IDLE3 ); + } +} + + + + +//================================================================================== + + + + +/* +================ +HSpawn_Disappear + +Called when a human spawn is destroyed before it is spawned +think function +================ +*/ +void HSpawn_Disappear( gentity_t *self ) +{ + self->s.eFlags |= EF_NODRAW; //don't draw the model once its destroyed + self->timestamp = level.time; + G_QueueBuildPoints( self ); + G_RewardAttackers( self ); + + G_FreeEntity( self ); +} + + +/* +================ +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->timestamp = level.time; + + //do some radius damage + G_RadiusDamage( self->s.pos.trBase, g_entities + self->killedBy, self->splashDamage, + self->splashRadius, self, self->splashMethodOfDeath ); + + // begin freeing build points + G_QueueBuildPoints( self ); + G_RewardAttackers( self ); + // turn into an explosion + self->s.eType = ET_EVENTS + EV_HUMAN_BUILDABLE_EXPLOSION; + self->freeAfterEvent = qtrue; + G_AddEvent( self, EV_HUMAN_BUILDABLE_EXPLOSION, DirToByte( dir ) ); +} + + +/* +================ +HSpawn_die + +Called when a human spawn dies +================ +*/ +void HSpawn_Die( gentity_t *self, gentity_t *inflictor, gentity_t *attacker, int damage, int mod ) +{ + G_SetBuildableAnim( self, BANIM_DESTROY1, qtrue ); + G_SetIdleBuildableAnim( self, BANIM_DESTROYED ); + + self->die = nullDieFunction; + self->killedBy = attacker - g_entities; + self->powered = qfalse; //free up power + self->s.eFlags &= ~EF_FIRING; //prevent any firing effects + + 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 + } + + G_LogDestruction( self, attacker, mod ); +} + +/* +================ +HSpawn_Think + +Think for human spawn +================ +*/ +void HSpawn_Think( gentity_t *self ) +{ + gentity_t *ent; + + // set parentNode + self->powered = G_FindPower( self, qfalse ); + + if( G_SuicideIfNoPower( self ) ) + return; + + 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, self->health, 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, self->health, 0, MOD_SUICIDE ); + return; + } + + if( ent->s.eType == ET_CORPSE ) + G_FreeEntity( ent ); //quietly remove + } + } + } + + self->nextthink = level.time + BG_Buildable( self->s.modelindex, NULL )->nextthink; +} + + + + +//================================================================================== + + + + +/* +================ +HRepeater_Die + +Called when a repeater dies +================ +*/ +static void HRepeater_Die( gentity_t *self, gentity_t *inflictor, gentity_t *attacker, int damage, int mod ) +{ + G_SetBuildableAnim( self, BANIM_DESTROY1, qtrue ); + G_SetIdleBuildableAnim( self, BANIM_DESTROYED ); + + self->die = nullDieFunction; + self->killedBy = attacker - g_entities; + self->powered = qfalse; //free up power + self->s.eFlags &= ~EF_FIRING; //prevent any firing effects + + 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 + } + + G_LogDestruction( self, attacker, mod ); + + if( self->usesBuildPointZone ) + { + buildPointZone_t *zone = &level.buildPointZones[self->buildPointZone]; + + zone->active = qfalse; + self->usesBuildPointZone = qfalse; + } +} + +/* +================ +HRepeater_Think + +Think for human power repeater +================ +*/ +void HRepeater_Think( gentity_t *self ) +{ + int i; + gentity_t *powerEnt; + buildPointZone_t *zone; + + self->powered = G_FindPower( self, qfalse ); + + powerEnt = G_InPowerZone( self ); + if( powerEnt != NULL ) + { + // If the repeater is inside of another power zone then suicide + // Attribute death to whoever built the reactor if that's a human, + // which will ensure that it does not queue the BP + if( powerEnt->builtBy >= 0 ) + G_Damage( self, NULL, g_entities + powerEnt->builtBy, NULL, NULL, self->health, 0, MOD_SUICIDE ); + else + G_Damage( self, NULL, NULL, NULL, NULL, self->health, 0, MOD_SUICIDE ); + return; + } + + G_IdlePowerState( self ); + + // Initialise the zone once the repeater has spawned + if( self->spawned && ( !self->usesBuildPointZone || !level.buildPointZones[ self->buildPointZone ].active ) ) + { + // See if a free zone exists + for( i = 0; i < g_humanRepeaterMaxZones.integer; i++ ) + { + zone = &level.buildPointZones[ i ]; + + if( !zone->active ) + { + // Initialise the BP queue with no BP queued + zone->queuedBuildPoints = 0; + zone->totalBuildPoints = g_humanRepeaterBuildPoints.integer; + zone->nextQueueTime = level.time; + zone->active = qtrue; + + self->buildPointZone = zone - level.buildPointZones; + self->usesBuildPointZone = qtrue; + + break; + } + } + } + + 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 || !self->spawned ) + return; + + if( other && other->client ) + G_GiveClientMaxAmmo( other, qtrue ); +} + +/* +================ +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 dccrange = { REACTOR_ATTACK_DCC_RANGE, + REACTOR_ATTACK_DCC_RANGE, + REACTOR_ATTACK_DCC_RANGE }; + vec3_t mins, maxs; + int i, num; + gentity_t *enemy, *tent; + + if( self->dcc ) + { + VectorAdd( self->s.origin, dccrange, maxs ); + VectorSubtract( self->s.origin, dccrange, mins ); + } + else + { + VectorAdd( self->s.origin, range, maxs ); + VectorSubtract( self->s.origin, range, mins ); + } + + if( self->spawned && ( self->health > 0 ) ) + { + qboolean fired = qfalse; + + // Creates a tesla trail for every target + 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_TEAM ] != TEAM_ALIENS ) + continue; + if( enemy->flags & FL_NOTARGET ) + continue; + + tent = G_TempEntity( enemy->s.pos.trBase, EV_TESLATRAIL ); + tent->s.generic1 = self->s.number; //src + tent->s.clientNum = enemy->s.number; //dest + VectorCopy( self->s.pos.trBase, tent->s.origin2 ); + fired = qtrue; + } + + // Actual damage is done by radius + if( fired ) + { + self->timestamp = level.time; + if( self->dcc ) + G_SelectiveRadiusDamage( self->s.pos.trBase, self, + REACTOR_ATTACK_DCC_DAMAGE, + REACTOR_ATTACK_DCC_RANGE, self, + MOD_REACTOR, TEAM_HUMANS ); + else + G_SelectiveRadiusDamage( self->s.pos.trBase, self, + REACTOR_ATTACK_DAMAGE, + REACTOR_ATTACK_RANGE, self, + MOD_REACTOR, TEAM_HUMANS ); + } + } + + if( self->dcc ) + self->nextthink = level.time + REACTOR_ATTACK_DCC_REPEAT; + else + self->nextthink = level.time + REACTOR_ATTACK_REPEAT; +} + +//================================================================================== + + + +/* +================ +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_TEAM ] != TEAM_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, qfalse ); + + G_SuicideIfNoPower( 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, qfalse ); + + G_SuicideIfNoPower( self ); +} + + + + +//================================================================================== + + + + +/* +================ +HMedistat_Die + +Die function for Human Medistation +================ +*/ +void HMedistat_Die( gentity_t *self, gentity_t *inflictor, + gentity_t *attacker, int damage, int mod ) +{ + //clear target's healing flag + if( self->enemy && self->enemy->client ) + self->enemy->client->ps.stats[ STAT_STATE ] &= ~SS_HEALING_ACTIVE; + + HSpawn_Die( self, inflictor, attacker, damage, mod ); +} + +/* +================ +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_Buildable( self->s.modelindex, NULL )->nextthink; + + self->powered = G_FindPower( self, qfalse ); + if( G_SuicideIfNoPower( self ) ) + return; + G_IdlePowerState( self ); + + //clear target's healing flag + if( self->enemy && self->enemy->client ) + self->enemy->client->ps.stats[ STAT_STATE ] &= ~SS_HEALING_ACTIVE; + + //make sure we have power + if( !self->powered ) + { + if( self->active ) + { + 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->flags & FL_NOTARGET ) + continue; // notarget cancels even beneficial effects? + + //remove poison from everyone, not just the healed player + if( player->client && player->client->ps.stats[ STAT_STATE ] & SS_POISONED ) + player->client->ps.stats[ STAT_STATE ] &= ~SS_POISONED; + + if( self->enemy == player && player->client && + ( (player->client->ps.stats[ STAT_TEAM ] == TEAM_HUMANS ) && // DIFF NOTE: remove this change from diffs ASAP + player->health < player->client->ps.stats[ STAT_MAX_HEALTH ] && + PM_Live( player->client->ps.pm_type ) ) ) + { + occupied = qtrue; + player->client->ps.stats[ STAT_STATE ] |= SS_HEALING_ACTIVE; + } + } + + 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_TEAM ] == TEAM_HUMANS ) + { + if( ( player->health < player->client->ps.stats[ STAT_MAX_HEALTH ] || + player->client->ps.stats[ STAT_STAMINA ] < STAMINA_MAX ) && + PM_Live( player->client->ps.pm_type ) ) + { + self->enemy = player; + + //start the heal anim + if( !self->active ) + { + G_SetBuildableAnim( self, BANIM_ATTACK1, qfalse ); + self->active = qtrue; + player->client->ps.stats[ STAT_STATE ] |= SS_HEALING_ACTIVE; + } + } + 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 && self->enemy->client ) //heal! + { + if( self->enemy->client->ps.stats[ STAT_STAMINA ] < STAMINA_MAX ) + self->enemy->client->ps.stats[ STAT_STAMINA ] += STAMINA_MEDISTAT_RESTORE; + + if( self->enemy->client->ps.stats[ STAT_STAMINA ] > STAMINA_MAX ) + self->enemy->client->ps.stats[ STAT_STAMINA ] = STAMINA_MAX; + + self->enemy->health++; + + //if they're completely healed, give them a medkit + if( self->enemy->health >= self->enemy->client->ps.stats[ STAT_MAX_HEALTH ] ) + { + self->enemy->health = self->enemy->client->ps.stats[ STAT_MAX_HEALTH ]; + if( !BG_InventoryContainsUpgrade( UP_MEDKIT, self->enemy->client->ps.stats ) ) + BG_AddUpgradeToInventory( UP_MEDKIT, self->enemy->client->ps.stats ); + } + } + } +} + + + + +//================================================================================== + + + + +/* +================ +HMGTurret_CheckTarget + +Used by HMGTurret_Think to check enemies for validity +================ +*/ +qboolean HMGTurret_CheckTarget( gentity_t *self, gentity_t *target, + qboolean los_check ) +{ + trace_t tr; + vec3_t dir, end; + + if( !target || target->health <= 0 || !target->client || + target->client->pers.teamSelection != TEAM_ALIENS ) + return qfalse; + + if( target->flags & FL_NOTARGET ) + return qfalse; + + if( !los_check ) + return qtrue; + + // Accept target if we can line-trace to it + VectorSubtract( target->s.pos.trBase, self->s.pos.trBase, dir ); + VectorNormalize( dir ); + VectorMA( self->s.pos.trBase, MGTURRET_RANGE, dir, end ); + trap_Trace( &tr, self->s.pos.trBase, NULL, NULL, end, + self->s.number, MASK_SHOT ); + return tr.entityNum == target - g_entities; +} + + +/* +================ +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; + + 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 ] < 0 && angularDiff[ PITCH ] < (-MGTURRET_ANGULARSPEED) ) + self->s.angles2[ PITCH ] += MGTURRET_ANGULARSPEED; + else if( angularDiff[ PITCH ] > 0 && angularDiff[ PITCH ] > MGTURRET_ANGULARSPEED ) + self->s.angles2[ PITCH ] -= MGTURRET_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 ] < 0 && angularDiff[ YAW ] < ( -MGTURRET_ANGULARSPEED ) ) + self->s.angles2[ YAW ] += MGTURRET_ANGULARSPEED; + else if( angularDiff[ YAW ] > 0 && angularDiff[ YAW ] > MGTURRET_ANGULARSPEED ) + self->s.angles2[ YAW ] -= MGTURRET_ANGULARSPEED; + else + self->s.angles2[ YAW ] = angleToTarget[ YAW ]; + + AngleVectors( self->s.angles2, dttAdjusted, NULL, NULL ); + RotatePointAroundVector( dirToTarget, xNormal, dttAdjusted, -rotAngle ); + vectoangles( dirToTarget, self->turretAim ); + + //fire if target is within accuracy + return ( abs( angularDiff[ YAW ] ) - MGTURRET_ANGULARSPEED <= + MGTURRET_ACCURACY_TO_FIRE ) && + ( abs( angularDiff[ PITCH ] ) - MGTURRET_ANGULARSPEED <= + MGTURRET_ACCURACY_TO_FIRE ); +} + + +/* +================ +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; + int start; + + if( self->enemy ) + self->enemy->targeted = NULL; + + self->enemy = NULL; + + // Look for targets in a box around the turret + VectorSet( range, MGTURRET_RANGE, MGTURRET_RANGE, MGTURRET_RANGE ); + VectorAdd( self->s.origin, range, maxs ); + VectorSubtract( self->s.origin, range, mins ); + num = trap_EntitiesInBox( mins, maxs, entityList, MAX_GENTITIES ); + + if( num == 0 ) + return; + + start = rand( ) / ( RAND_MAX / num + 1 ); + for( i = start; i < num + start ; i++ ) + { + target = &g_entities[ entityList[ i % num ] ]; + if( !HMGTurret_CheckTarget( self, target, qtrue ) ) + continue; + + self->enemy = target; + self->enemy->targeted = self; + return; + } +} + +/* +================ +HMGTurret_State + +Raise or lower MG turret towards desired state +================ +*/ +enum { + MGT_STATE_INACTIVE, + MGT_STATE_DROP, + MGT_STATE_RISE, + MGT_STATE_ACTIVE +}; + +static qboolean HMGTurret_State( gentity_t *self, int state ) +{ + float angle; + + if( self->waterlevel == state ) + return qfalse; + + angle = AngleNormalize180( self->s.angles2[ PITCH ] ); + + if( state == MGT_STATE_INACTIVE ) + { + if( angle < MGTURRET_VERTICALCAP ) + { + if( self->waterlevel != MGT_STATE_DROP ) + { + self->speed = 0.25f; + self->waterlevel = MGT_STATE_DROP; + } + else + self->speed *= 1.25f; + + self->s.angles2[ PITCH ] = + MIN( MGTURRET_VERTICALCAP, angle + self->speed ); + return qtrue; + } + else + self->waterlevel = MGT_STATE_INACTIVE; + } + else if( state == MGT_STATE_ACTIVE ) + { + if( !self->enemy && angle > 0.0f ) + { + self->waterlevel = MGT_STATE_RISE; + self->s.angles2[ PITCH ] = + MAX( 0.0f, angle - MGTURRET_ANGULARSPEED * 0.5f ); + } + else + self->waterlevel = MGT_STATE_ACTIVE; + } + + return qfalse; +} + +/* +================ +HMGTurret_Think + +Think function for MG turret +================ +*/ +void HMGTurret_Think( gentity_t *self ) +{ + self->nextthink = level.time + + BG_Buildable( self->s.modelindex, NULL )->nextthink; + + // Turn off client side muzzle flashes + self->s.eFlags &= ~EF_FIRING; + + self->powered = G_FindPower( self, qfalse ); + if( G_SuicideIfNoPower( self ) ) + return; + G_IdlePowerState( self ); + + // If not powered or spawned don't do anything + if( !self->powered ) + { + // if power loss drop turret + if( self->spawned && + HMGTurret_State( self, MGT_STATE_INACTIVE ) ) + return; + + self->nextthink = level.time + POWER_REFRESH_TIME; + return; + } + if( !self->spawned ) + return; + + // If the current target is not valid find a new enemy + if( !HMGTurret_CheckTarget( self, self->enemy, qtrue ) ) + { + self->active = qfalse; + self->turretSpinupTime = -1; + HMGTurret_FindEnemy( self ); + } + // if newly powered raise turret + HMGTurret_State( self, MGT_STATE_ACTIVE ); + if( !self->enemy ) + return; + + // Track until we can hit the target + if( !HMGTurret_TrackEnemy( self ) ) + { + self->active = qfalse; + self->turretSpinupTime = -1; + return; + } + + // Update spin state + if( !self->active && self->timestamp < level.time ) + { + self->active = qtrue; + + self->turretSpinupTime = level.time + MGTURRET_SPINUP_TIME; + G_AddEvent( self, EV_MGTURRET_SPINUP, 0 ); + } + + // Not firing or haven't spun up yet + if( !self->active || self->turretSpinupTime > level.time ) + return; + + // Fire repeat delay + if( self->timestamp > level.time ) + return; + + FireWeapon( self ); + self->s.eFlags |= EF_FIRING; + self->timestamp = level.time + BG_Buildable( self->s.modelindex, NULL )->turretFireSpeed; + G_AddEvent( self, EV_FIRE_WEAPON, 0 ); + G_SetBuildableAnim( self, BANIM_ATTACK1, qfalse ); +} + + + + +//================================================================================== + + + + +/* +================ +HTeslaGen_Think + +Think function for Tesla Generator +================ +*/ +void HTeslaGen_Think( gentity_t *self ) +{ + self->nextthink = level.time + BG_Buildable( self->s.modelindex, NULL )->nextthink; + + self->powered = G_FindPower( self, qfalse ); + if( G_SuicideIfNoPower( self ) ) + return; + G_IdlePowerState( self ); + + //if not powered don't do anything and check again for power next think + if( !self->powered ) + { + self->s.eFlags &= ~EF_FIRING; + self->nextthink = level.time + POWER_REFRESH_TIME; + return; + } + + if( self->spawned && self->timestamp < level.time ) + { + vec3_t origin, range, mins, maxs; + int entityList[ MAX_GENTITIES ], i, num; + + // Communicates firing state to client + self->s.eFlags &= ~EF_FIRING; + + // Move the muzzle from the entity origin up a bit to fire over turrets + VectorMA( self->s.origin, self->r.maxs[ 2 ], self->s.origin2, origin ); + + VectorSet( range, TESLAGEN_RANGE, TESLAGEN_RANGE, TESLAGEN_RANGE ); + VectorAdd( origin, range, maxs ); + VectorSubtract( origin, range, mins ); + + // Attack nearby Aliens + num = trap_EntitiesInBox( mins, maxs, entityList, MAX_GENTITIES ); + for( i = 0; i < num; i++ ) + { + self->enemy = &g_entities[ entityList[ i ] ]; + + if( self->enemy->flags & FL_NOTARGET ) + continue; + + if( self->enemy->client && self->enemy->health > 0 && + self->enemy->client->ps.stats[ STAT_TEAM ] == TEAM_ALIENS && + Distance( origin, self->enemy->s.pos.trBase ) <= TESLAGEN_RANGE ) + FireWeapon( self ); + } + self->enemy = NULL; + + 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->timestamp = level.time + TESLAGEN_REPEAT; + } + } +} + +//================================================================================== +// CUBOID FUNCTIONS +// generic functions for all types of cuboids +//================================================================================== + +void Cuboid_Think(gentity_t *self) +{ +} + +//Cuboids need a new die function because of the cuboid explosion effects. +void Cuboid_Die(gentity_t *self,gentity_t *inflictor,gentity_t *attacker,int damage,int mod) +{ + vec3_t dir; + qboolean event=qfalse; + const cuboidAttributes_t *cuboid; + + cuboid=BG_CuboidAttributes(self->s.modelindex); + G_SetBuildableAnim(self,BANIM_DESTROY1,qtrue); // just for sound + self->die=nullDieFunction; + self->killedBy=attacker-g_entities; + self->powered=qfalse; + self->s.eFlags&=~EF_FIRING; + G_LogDestruction(self,attacker,mod); + dir[0]=dir[1]=0; + dir[2]=1; + self->timestamp = level.time; + G_QueueBuildPoints(self); + if(mod!=MOD_DECONSTRUCT&&self->spawned) + { + G_RewardAttackers(self); + G_RadiusDamage(self->s.pos.trBase,g_entities+self->killedBy,self->splashDamage,self->splashRadius,self,self->splashMethodOfDeath); + //NOTE: all cuboid info is already packed + self->s.eType=ET_EVENTS+EV_CUBOID_EXPLOSION; + self->freeAfterEvent = qtrue; + G_AddEvent(self,EV_HUMAN_BUILDABLE_EXPLOSION,DirToByte(dir)); + event=qtrue; + self->r.contents=0; + trap_LinkEntity(self); + } + else + { + self->s.eType=0; + G_FreeEntity(self); + } +} + + +//================================================================================== + + + + +/* +============ +G_QueueValue +============ +*/ + +static int G_QueueValue( gentity_t *self ) +{ + int i; + int damageTotal = 0; + int queuePoints; + double queueFraction = 0; + + for( i = 0; i < level.maxclients; i++ ) + { + gentity_t *player = g_entities + i; + + damageTotal += self->credits[ i ]; + + if( self->buildableTeam != player->client->pers.teamSelection ) + queueFraction += (double) self->credits[ i ]; + } + + if( damageTotal > 0 ) + queueFraction = queueFraction / (double) damageTotal; + else // all damage was done by nonclients, so queue everything + queueFraction = 1.0; + + queuePoints = (int) ( queueFraction * (double) BG_Buildable( self->s.modelindex, self->cuboidSize )->buildPoints ); + return queuePoints; +} + +/* +============ +G_QueueBuildPoints +============ +*/ +void G_QueueBuildPoints( gentity_t *self ) +{ + gentity_t *powerEntity; + int queuePoints; + + queuePoints = G_QueueValue( self ); + + if( !queuePoints ) + return; + + switch( self->buildableTeam ) + { + default: + case TEAM_NONE: + return; + + case TEAM_ALIENS: + if( !level.alienBuildPointQueue ) + level.alienNextQueueTime = level.time + g_alienBuildQueueTime.integer; + + level.alienBuildPointQueue += queuePoints; + break; + + case TEAM_HUMANS: + powerEntity = G_PowerEntityForEntity( self ); + + if( powerEntity ) + { + int nqt; + switch( powerEntity->s.modelindex ) + { + case BA_H_REACTOR: + nqt = G_NextQueueTime( level.humanBuildPointQueue, + g_humanBuildPoints.integer, + g_humanBuildQueueTime.integer ); + if( !level.humanBuildPointQueue || + level.time + nqt < level.humanNextQueueTime ) + level.humanNextQueueTime = level.time + nqt; + + level.humanBuildPointQueue += queuePoints; + break; + + case BA_H_REPEATER: + if( powerEntity->usesBuildPointZone && + level.buildPointZones[ powerEntity->buildPointZone ].active ) + { + buildPointZone_t *zone = &level.buildPointZones[ powerEntity->buildPointZone ]; + + nqt = G_NextQueueTime( zone->queuedBuildPoints, + zone->totalBuildPoints, + g_humanRepeaterBuildQueueTime.integer ); + + if( !zone->queuedBuildPoints || + level.time + nqt < zone->nextQueueTime ) + zone->nextQueueTime = level.time + nqt; + + zone->queuedBuildPoints += queuePoints; + } + break; + + default: + break; + } + } + } +} + +/* +============ +G_NextQueueTime +============ +*/ +int G_NextQueueTime( int queuedBP, int totalBP, int queueBaseRate ) +{ + float fractionQueued; + + if( totalBP == 0 ) + return 0; + + fractionQueued = queuedBP / (float)totalBP; + return ( 1.0f - fractionQueued ) * queueBaseRate; +} + +/* +============ +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_BuildableBoundingBox( 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 maxHealth = BG_Buildable( ent->s.modelindex, ent->cuboidSize )->health; + int regenRate = BG_Buildable( ent->s.modelindex, ent->cuboidSize )->regenRate; + int buildTime = BG_Buildable( ent->s.modelindex, ent->cuboidSize )->buildTime; + int buildRate; + + //toggle spawned flag for buildables + if( !ent->spawned && ( ent->healthLeft<=0 || ent->health==maxHealth ) && !level.pausedTime ) + { + ent->spawned = qtrue; + if( ent->s.modelindex == BA_A_OVERMIND ) + { + G_TeamCommand( TEAM_ALIENS, "cp \"The Overmind has awakened!\"" ); + } + } + + // Timer actions + ent->time1000 += msec; + if( ent->time1000 >= 1000 ) + { + ent->time1000 -= 1000; + + if( !ent->spawned ) + { + buildRate=(int)(ceil((float)maxHealth/(float)buildTime*1e3f)); + ent->health=MIN(ent->health+buildRate,maxHealth); + ent->healthLeft=MAX(ent->healthLeft-buildRate,0); + } + else if( ent->health > 0 && ent->health < maxHealth ) + { + if( ent->buildableTeam == TEAM_ALIENS && regenRate && + ( ent->lastDamageTime + ALIEN_REGEN_DAMAGE_TIME ) < level.time ) + { + ent->health += regenRate; + } + else if( ent->buildableTeam == TEAM_HUMANS && ent->dcc && + ( ent->lastDamageTime + HUMAN_REGEN_DAMAGE_TIME ) < level.time ) + { + ent->health += DC_HEALRATE * ent->dcc; + } + } + + if( ent->health >= maxHealth ) + { + int i; + ent->health = maxHealth; + for( i = 0; i < MAX_CLIENTS; i++ ) + ent->credits[ i ] = 0; + } + } + + if( ent->clientSpawnTime > 0 ) + ent->clientSpawnTime -= msec; + + if( ent->clientSpawnTime < 0 ) + ent->clientSpawnTime = 0; + + ent->dcc = ( ent->buildableTeam != TEAM_HUMANS ) ? 0 : G_FindDCC( ent ); + + // Set health + ent->s.generic1 = MIN(MAX(ent->health,0),999); + + // Set health for cuboids (BG_CuboidPackHealthSafe does nothing if not a cuboid) !@#CUBOID + BG_CuboidPackHealthSafe(ent->s.modelindex,&ent->s,ent->health); + + // Set flags + ent->s.eFlags &= ~( EF_B_POWERED | EF_B_SPAWNED | EF_B_MARKED ); + if( ent->powered ) + ent->s.eFlags |= EF_B_POWERED; + + if( ent->spawned ) + ent->s.eFlags |= EF_B_SPAWNED; + + if( ent->deconstruct ) + ent->s.eFlags |= EF_B_MARKED; + + // 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->buildableTeam == TEAM_HUMANS && !ent->powered ) + continue; + + if( ent->s.modelindex == buildable && ent->spawned ) + return qtrue; + } + + return qfalse; +} + +/* +================ +G_FindBuildable + +Finds a buildable of the specified type +================ +*/ +static gentity_t *G_FindBuildable( buildable_t buildable ) +{ + int i; + gentity_t *ent; + + for( i = MAX_CLIENTS, ent = g_entities + i; + i < level.num_entities; i++, ent++ ) + { + if( ent->s.eType != ET_BUILDABLE ) + continue; + + if( ent->s.modelindex == buildable && !( ent->s.eFlags & EF_DEAD ) ) + return ent; + } + + return NULL; +} + +/* +=============== +G_BuildablesIntersect + +Test if two buildables intersect each other +=============== +*/ +static qboolean G_BuildablesIntersect( buildable_t a, vec3_t originA, vec3_t cuboidA, + buildable_t b, vec3_t originB, vec3_t cuboidB ) +{ + vec3_t minsA, maxsA; + vec3_t minsB, maxsB; + + if(BG_Buildable(a,NULL)->cuboid) + BG_CuboidBBox(cuboidA,minsA,maxsA); + else + BG_BuildableBoundingBox( a, minsA, maxsA ); + VectorAdd( minsA, originA, minsA ); + VectorAdd( maxsA, originA, maxsA ); + + if(BG_Buildable(b,NULL)->cuboid) + BG_CuboidBBox(cuboidB,minsB,maxsB); + else + BG_BuildableBoundingBox( b, minsB, maxsB ); + VectorAdd( minsB, originB, minsB ); + VectorAdd( maxsB, originB, maxsB ); + + return BoundsIntersect( minsA, maxsA, minsB, maxsB ); +} + +/* +=============== +G_CompareBuildablesForRemoval + +qsort comparison function for a buildable removal list +=============== +*/ +static buildable_t cmpBuildable; +static vec3_t cmpOrigin; +static vec3_t cmpCuboid; +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_SPAWN, + BA_A_CUBOID1, + 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_CUBOID1, + BA_H_CUBOID2, + 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, cmpCuboid, + buildableA->s.modelindex, buildableA->s.origin, buildableA->cuboidSize ); + bMatches = G_BuildablesIntersect( cmpBuildable, cmpOrigin, cmpCuboid, + buildableB->s.modelindex, buildableB->s.origin, buildableB->cuboidSize ); + if( aMatches && !bMatches ) + return -1; + else if( !aMatches && bMatches ) + return 1; + + // If the only spawn is marked, prefer it last + if( cmpBuildable == BA_A_SPAWN || cmpBuildable == BA_H_SPAWN ) + { + if( ( buildableA->s.modelindex == BA_A_SPAWN && level.numAlienSpawns == 1 ) || + ( buildableA->s.modelindex == BA_H_SPAWN && level.numHumanSpawns == 1 ) ) + return 1; + + if( ( buildableB->s.modelindex == BA_A_SPAWN && level.numAlienSpawns == 1 ) || + ( buildableB->s.modelindex == BA_H_SPAWN && level.numHumanSpawns == 1 ) ) + 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; + else if( !aMatches && bMatches ) + return 1; + + // They're the same type + if( buildableA->s.modelindex == buildableB->s.modelindex ) + { + gentity_t *powerEntity = G_PowerEntityForPoint( cmpOrigin ); + + // Prefer the entity that is providing power for this point + aMatches = ( powerEntity == buildableA ); + bMatches = ( powerEntity == buildableB ); + if( aMatches && !bMatches ) + return -1; + else if( !aMatches && bMatches ) + return 1; + + // Pick the one marked earliest + return buildableA->deconstructTime - buildableB->deconstructTime; + } + + // Resort to preference list + 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_ClearDeconMarks + +Remove decon mark from all buildables +=============== +*/ +void G_ClearDeconMarks( void ) +{ + int i; + gentity_t *ent; + + for( i = MAX_CLIENTS, ent = g_entities + i ; i < level.num_entities ; i++, ent++ ) + { + if( !ent->inuse ) + continue; + + if( ent->s.eType != ET_BUILDABLE ) + continue; + + ent->deconstruct = qfalse; + } +} + +/* +=============== +G_FreeMarkedBuildables + +Free up build points for a team by deconstructing marked buildables +=============== +*/ +void G_FreeMarkedBuildables( gentity_t *deconner, char *readable, int rsize, + char *nums, int nsize ) +{ + int i; + int bNum; + int listItems = 0; + int totalListItems = 0; + gentity_t *ent; + int removalCounts[ BA_NUM_BUILDABLES ] = {0}; + + if( readable && rsize ) + readable[ 0 ] = '\0'; + if( nums && nsize ) + nums[ 0 ] = '\0'; + + if( !g_markDeconstruct.integer ) + return; // Not enabled, can't deconstruct anything + + for( i = 0; i < level.numBuildablesForRemoval; i++ ) + { + ent = level.markedBuildables[ i ]; + bNum = BG_Buildable( ent->s.modelindex, NULL )->number; + + if( removalCounts[ bNum ] == 0 ) + totalListItems++; + + G_Damage( ent, NULL, deconner, NULL, NULL, ent->health, 0, MOD_REPLACE ); + + removalCounts[ bNum ]++; + + if( nums ) + Q_strcat( nums, nsize, va( " %d", ent - g_entities ) ); + + G_FreeEntity( ent ); + } + + if( !readable ) + return; + + for( i = 0; i < BA_NUM_BUILDABLES; i++ ) + { + if( removalCounts[ i ] ) + { + if( listItems ) + { + if( listItems == ( totalListItems - 1 ) ) + Q_strcat( readable, rsize, va( "%s and ", + ( totalListItems > 2 ) ? "," : "" ) ); + else + Q_strcat( readable, rsize, ", " ); + } + Q_strcat( readable, rsize, va( "%s", BG_Buildable( i, NULL )->humanName ) ); + if( removalCounts[ i ] > 1 ) + Q_strcat( readable, rsize, va( " (%dx)", removalCounts[ i ] ) ); + listItems++; + } + } +} + +/* +=============== +G_SufficientBPAvailable + +Determine if enough build points can be released for the buildable +and list the buildables that must be destroyed if this is the case +=============== +*/ +static itemBuildError_t G_SufficientBPAvailable( buildable_t buildable, + vec3_t origin, + vec3_t cuboidSize ) +{ + int i; + int numBuildables = 0; + int numRequired = 0; + int pointsYielded = 0; + gentity_t *ent; + team_t team = BG_Buildable( buildable, NULL )->team; + int buildPoints = BG_Buildable( buildable, cuboidSize )->buildPoints; + int remainingBP, remainingSpawns; + qboolean collision = qfalse; + int collisionCount = 0; + qboolean repeaterInRange = qfalse; + int repeaterInRangeCount = 0; + itemBuildError_t bpError; + buildable_t spawn; + buildable_t core; + int spawnCount = 0; + qboolean changed = qtrue; + + level.numBuildablesForRemoval = 0; + + if( team == TEAM_ALIENS ) + { + remainingBP = G_GetBuildPoints( origin, team ); + remainingSpawns = level.numAlienSpawns; + bpError = IBE_NOALIENBP; + spawn = BA_A_SPAWN; + core = BA_A_OVERMIND; + } + else if( team == TEAM_HUMANS ) + { + if( buildable == BA_H_REACTOR || buildable == BA_H_REPEATER ) + remainingBP = level.humanBuildPoints; + else + remainingBP = G_GetBuildPoints( origin, team ); + + remainingSpawns = level.numHumanSpawns; + bpError = IBE_NOHUMANBP; + spawn = BA_H_SPAWN; + core = BA_H_REACTOR; + } + else + { + Com_Error( ERR_FATAL, "team is %d\n", team ); + 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, cuboidSize, ent->s.modelindex, ent->s.origin, ent->cuboidSize ) ) + return IBE_NOROOM; + + } + + return IBE_NONE; + } + + // Set buildPoints to the number extra that are required + 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, cuboidSize, ent->s.modelindex, ent->s.origin, ent->cuboidSize ); + + if( collision ) + { + // Don't allow replacements at all + if( g_markDeconstruct.integer == 1 ) + return IBE_NOROOM; + + // Only allow replacements of the same type + if( g_markDeconstruct.integer == 2 && ent->s.modelindex != buildable ) + return IBE_NOROOM; + + // Any other setting means anything goes + + 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; + + // Don't allow marked buildables to be replaced in another zone, + // unless the marked buildable isn't in a zone (and thus unpowered) + if( team == TEAM_HUMANS && + buildable != BA_H_REACTOR && + buildable != BA_H_REPEATER && + ent->parentNode != G_PowerEntityForPoint( origin ) ) + continue; + + if( !ent->inuse ) + continue; + + if( ent->health <= 0 ) + continue; + + if( ent->buildableTeam != team ) + continue; + + // Explicitly disallow replacement of the core buildable with anything + // other than the core buildable + if( ent->s.modelindex == core && buildable != core ) + continue; + + // Don't allow a power source to be replaced by a dependant + if( team == TEAM_HUMANS && + G_PowerEntityForPoint( origin ) == ent && + buildable != BA_H_REPEATER && + buildable != core ) + continue; + + // Don't include unpowered buildables + if( !collision && !ent->powered ) + continue; + + if( ent->deconstruct ) + { + level.markedBuildables[ numBuildables++ ] = ent; + + // Buildables that are marked here will always end up at the front of the + // removal list, so just incrementing numBuildablesForRemoval is sufficient + if( collision || repeaterInRange ) + { + // Collided with something, so we definitely have to remove it or + // it's a repeater that intersects the new repeater's power area, + // so it must be removed + + if( collision ) + collisionCount--; + + if( repeaterInRange ) + repeaterInRangeCount--; + + if( ent->powered ) + pointsYielded += BG_Buildable( ent->s.modelindex, ent->cuboidSize )->buildPoints; + level.numBuildablesForRemoval++; + } + else if( BG_Buildable( ent->s.modelindex, NULL )->uniqueTest && + ent->s.modelindex == buildable ) + { + // If it's a unique buildable, it must be replaced by the same type + if( ent->powered ) + pointsYielded += BG_Buildable( ent->s.modelindex, ent->cuboidSize )->buildPoints; + level.numBuildablesForRemoval++; + } + } + } + + numRequired = level.numBuildablesForRemoval; + + // We still need build points, but have no candidates for removal + if( buildPoints > 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_RPTPOWERHERE; + + // Sort the list + cmpBuildable = buildable; + VectorCopy( origin, cmpOrigin ); + VectorCopy( cuboidSize, cmpCuboid ); + 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 ]; + if( ent->powered ) + pointsYielded += BG_Buildable( ent->s.modelindex, ent->cuboidSize )->buildPoints; + } + + // Do another pass to see if we can meet quota with fewer buildables + // than we have now due to mismatches between priority and BP amounts + // by repeatedly testing if we can chop off the first thing that isn't + // required by rules of collision/uniqueness, which are always at the head + while( changed && level.numBuildablesForRemoval > 1 && + level.numBuildablesForRemoval > numRequired ) + { + int pointsUnYielded = 0; + changed = qfalse; + ent = level.markedBuildables[ numRequired ]; + if( ent->powered ) + pointsUnYielded = BG_Buildable( ent->s.modelindex, ent->cuboidSize )->buildPoints; + + if( pointsYielded - pointsUnYielded >= buildPoints ) + { + pointsYielded -= pointsUnYielded; + memmove( &level.markedBuildables[ numRequired ], + &level.markedBuildables[ numRequired + 1 ], + ( level.numBuildablesForRemoval - numRequired ) + * sizeof( gentity_t * ) ); + level.numBuildablesForRemoval--; + changed = qtrue; + } + } + + for( i = 0; i < level.numBuildablesForRemoval; i++ ) + { + if( level.markedBuildables[ i ]->s.modelindex == spawn ) + spawnCount++; + } + + // Make sure we're not removing the last spawn + if( !g_cheats.integer && remainingSpawns > 0 && ( remainingSpawns - spawnCount ) < 1 ) + return IBE_LASTSPAWN; + + // Not enough points yielded + if( pointsYielded < buildPoints ) + return bpError; + else + 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 normal, vec3_t cuboidSize ) +{ + vec3_t angles; + vec3_t entity_origin; + vec3_t mins, maxs; + trace_t tr1, tr2, tr3; + itemBuildError_t reason = IBE_NONE, tempReason; + gentity_t *tempent; + float minNormal; + qboolean invert; + int contents; + playerState_t *ps = &ent->client->ps; + + if( BG_Buildable(buildable,NULL)->cuboid ) + BG_CuboidBBox(cuboidSize,mins,maxs); + else + BG_BuildableBoundingBox( buildable, mins, maxs ); + + if(!BG_PositionBuildableRelativeToPlayer( ps, BG_Buildable(buildable,NULL)->cuboid, mins, maxs, trap_Trace, entity_origin, angles, &tr1 )) + return IBE_NOSURF; + trap_Trace( &tr2, entity_origin, mins, maxs, entity_origin, -1, 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_Buildable( buildable, NULL )->minNormal; + invert = BG_Buildable( buildable, NULL )->invertNormal; + + //can we build at this angle? + if(!BG_Buildable(buildable,NULL)->cuboid) + if( !( normal[ 2 ] >= minNormal || ( invert && normal[ 2 ] <= -minNormal ) ) ) + reason = IBE_NORMAL; + + //the only buildable one can build on is a cuboid + if( tr1.entityNum != ENTITYNUM_WORLD ) + if( g_entities[ tr1.entityNum ].s.eType == ET_BUILDABLE && + !BG_Buildable( g_entities[ tr1.entityNum ].s.modelindex, NULL )->cuboid ) + reason = IBE_NORMAL; + + contents = trap_PointContents( entity_origin, -1 ); + + if( ( tempReason = G_SufficientBPAvailable( buildable, origin, cuboidSize ) ) != IBE_NONE ) + reason = tempReason; + + if( ent->client->ps.stats[ STAT_TEAM ] == TEAM_ALIENS ) + { + //alien criteria + + // Check there is an Overmind + if( buildable != BA_A_OVERMIND ) + { + if( !G_Overmind( ) ) + reason = IBE_NOOVERMIND; + } + + //check there is creep near by for building on + if( BG_Buildable( buildable, NULL )->creepTest ) + { + if( !G_IsCreepHere( entity_origin ) ) + reason = IBE_NOCREEP; + } + + // Check permission to build here + if( tr1.surfaceFlags & SURF_NOALIENBUILD || contents & CONTENTS_NOALIENBUILD ) + reason = IBE_PERMISSION; + } + else if( ent->client->ps.stats[ STAT_TEAM ] == TEAM_HUMANS ) + { + //human criteria + + // Check for power + if( G_IsPowered( entity_origin ) == BA_NONE ) + { + //tell player to build a repeater to provide power + if( buildable != BA_H_REACTOR && buildable != BA_H_REPEATER ) + reason = IBE_NOPOWERHERE; + } + + //this buildable requires a DCC + if( BG_Buildable( buildable, NULL )->dccTest && !G_IsDCCBuilt( ) ) + reason = IBE_NODCC; + + //check that there is a parent reactor when building a repeater + if( buildable == BA_H_REPEATER ) + { + tempent = G_Reactor( ); + + if( tempent == NULL ) // No reactor + reason = IBE_RPTNOREAC; + else if( g_markDeconstruct.integer && G_IsPowered( entity_origin ) == BA_H_REACTOR ) + reason = IBE_RPTPOWERHERE; + else if( !g_markDeconstruct.integer && G_IsPowered( entity_origin ) ) + reason = IBE_RPTPOWERHERE; + } + + // Check permission to build here + if( tr1.surfaceFlags & SURF_NOHUMANBUILD || contents & CONTENTS_NOHUMANBUILD ) + reason = IBE_PERMISSION; + } + + // Check permission to build here + if( tr1.surfaceFlags & SURF_NOBUILD || contents & CONTENTS_NOBUILD ) + reason = IBE_PERMISSION; + + // Can we only have one of these? + if( BG_Buildable( buildable, NULL )->uniqueTest ) + { + tempent = G_FindBuildable( buildable ); + if( tempent && !tempent->deconstruct ) + { + switch( buildable ) + { + case BA_A_OVERMIND: + reason = IBE_ONEOVERMIND; + break; + + case BA_H_REACTOR: + reason = IBE_ONEREACTOR; + break; + + default: + Com_Error( ERR_FATAL, "No reason for denying build of %d\n", buildable ); + break; + } + } + } + + //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.startsolid || !COMPARE_FLOAT_EPSILON(tr3.fraction,1.0f) ) ) + reason = IBE_NOROOM; + if( reason != IBE_NONE ) + level.numBuildablesForRemoval = 0; + + if( g_buildableDensityLimit.integer > 0 ) + { + int i, count, numents, ents[ MAX_GENTITIES ]; + vec3_t rmins, rmaxs; + gentity_t *other; + + for( i = 0; i < 3; i++ ) + rmins[ i ] = origin[ i ] - g_buildableDensityLimitRange.value, + rmaxs[ i ] = origin[ i ] + g_buildableDensityLimitRange.value; + + numents = trap_EntitiesInBox( rmins, rmaxs, ents, MAX_GENTITIES ); + + for( count = 0, i = 0; i < numents ; i++ ) + { + other = &g_entities[ ents[ i ] ]; + + if( other->s.eType != ET_BUILDABLE ) + continue; + + if( other->buildableTeam != ent->client->ps.stats[ STAT_TEAM ] ) + continue; + + if( Distance( origin, other->s.origin ) > g_buildableDensityLimitRange.value ) + continue; + + if( !trap_InPVSIgnorePortals( origin, other->s.origin ) ) + continue; + + if( ++count > g_buildableDensityLimit.integer ) + { + reason = IBE_TOODENSE; + break; + } + } + } + + return reason; +} + +/* +================ +G_CuboidName + +For standard buildables the function returns humanName. +In case of cuboids, a "dynamic" one is generated. +================ +*/ + +const char *G_CuboidName(buildable_t buildable, const vec3_t cuboidSize, qboolean verbose) +{ + static char buffer[100]; + const buildableAttributes_t *battr; + + if((battr=BG_Buildable(buildable,cuboidSize))->cuboid) + { + if(verbose) + Com_sprintf(buffer,sizeof(buffer),"%s %fx%fx%f %ibp",battr->humanName,cuboidSize[0],cuboidSize[1],cuboidSize[2],battr->buildPoints); + else + Com_sprintf(buffer,sizeof(buffer),"%s %s (%i bp)",BG_CuboidAttributes(buildable)->icon,battr->humanName,battr->buildPoints); + return buffer; + } + else + return battr->humanName; +} + +/* +================ +G_Build + +Spawns a buildable +================ +*/ +static gentity_t *G_Build( gentity_t *builder, buildable_t buildable, + const vec3_t origin, const vec3_t normal, const vec3_t angles, + const vec3_t cuboidSize ) +{ + gentity_t *built; + vec3_t localOrigin; + char readable[ MAX_STRING_CHARS ]; + char buildnums[ MAX_STRING_CHARS ]; + buildLog_t *log; + qboolean cuboid; + + cuboid=BG_Buildable(buildable,NULL)->cuboid; + + VectorCopy( origin, localOrigin ); + + if( builder->client ) + log = G_BuildLogNew( builder, BF_CONSTRUCT ); + else + log = NULL; + + // Free existing buildables + G_FreeMarkedBuildables( builder, readable, sizeof( readable ), + buildnums, sizeof( buildnums ) ); + + // Spawn the buildable + built = G_Spawn(); + built->s.eType = ET_BUILDABLE; + built->killedBy = ENTITYNUM_NONE; + built->classname = BG_Buildable( buildable, NULL )->entityName; + built->s.modelindex = buildable; + built->buildableTeam = built->s.modelindex2 = BG_Buildable( buildable, NULL )->team; + + // 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( localOrigin, 1.0f, normal, localOrigin ); + + if(cuboid) + { + BG_CuboidBBox(cuboidSize,built->r.mins,built->r.maxs); + VectorCopy(cuboidSize,&built->cuboidSize); + } + else + BG_BuildableBoundingBox(buildable,built->r.mins,built->r.maxs); + + built->health = 1; + built->healthLeft = BG_Buildable( buildable, cuboidSize )->health-1; + built->s.generic1 = MIN(MAX(built->health,0),999); + built->splashDamage = BG_Buildable( buildable, cuboidSize )->splashDamage; + built->splashRadius = BG_Buildable( buildable, cuboidSize )->splashRadius; + built->splashMethodOfDeath = BG_Buildable( buildable, cuboidSize )->meansOfDeath; + + built->nextthink = BG_Buildable( buildable, NULL )->nextthink; + + built->takedamage = qtrue; + built->spawned = qfalse; + built->buildTime = built->s.time = level.time; + + // build instantly in cheat mode + if( builder->client && (g_cheats.integer || g_instantBuild.integer) ) + { + built->health = BG_Buildable( buildable, cuboidSize )->health; + built->buildTime = built->s.time = + level.time -BG_Buildable( buildable, cuboidSize )->buildTime; + } + + //things that vary for each buildable that aren't in the dbase + switch( buildable ) + { + case BA_A_SPAWN: + built->die = AGeneric_Die; + built->think = ASpawn_Think; + built->pain = AGeneric_Pain; + break; + + case BA_A_BARRICADE: + built->die = ABarricade_Die; + built->think = ABarricade_Think; + built->pain = ABarricade_Pain; + built->touch = ABarricade_Touch; + built->shrunkTime = 0; + ABarricade_Shrink( built, qtrue ); + break; + + case BA_A_BOOSTER: + built->die = AGeneric_Die; + built->think = AGeneric_Think; + built->pain = AGeneric_Pain; + built->touch = ABooster_Touch; + break; + + case BA_A_ACIDTUBE: + built->die = AGeneric_Die; + built->think = AAcidTube_Think; + built->pain = AGeneric_Pain; + break; + + case BA_A_HIVE: + built->die = AGeneric_Die; + built->think = AHive_Think; + built->pain = AHive_Pain; + break; + + case BA_A_TRAPPER: + built->die = AGeneric_Die; + built->think = ATrapper_Think; + built->pain = AGeneric_Pain; + break; + + case BA_A_OVERMIND: + built->die = AGeneric_Die; + built->think = AOvermind_Think; + built->pain = AGeneric_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 = HMedistat_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 = HRepeater_Die; + built->use = HRepeater_Use; + built->count = -1; + break; + + default: + //erk + break; + } + + if(buildable>=CUBOID_FIRST && buildable<=CUBOID_LAST) + { + built->think=Cuboid_Think; + built->die=Cuboid_Die; + } + + built->clipmask = MASK_PLAYERSOLID; + built->r.contents=CONTENTS_BODY; + built->s.number = built - g_entities; + built->enemy = NULL; + built->s.weapon = BG_Buildable( buildable, NULL )->turretProjType; + + if( builder->client ) + built->builtBy = builder->client->ps.clientNum; + else + built->builtBy = -1; + + G_SetOrigin( built, localOrigin ); + + // roughly nudge the buildable onto the surface D:< !@#CUBOID + VectorScale( normal, -512.0f, built->s.pos.trDelta ); + + if(BG_Buildable(buildable, NULL)->cuboid) + VectorCopy(cuboidSize,built->s.angles); + else + { + VectorCopy( angles, built->s.angles ); + built->s.angles[ PITCH ] = 0.0f; + } + + built->s.pos.trType = BG_Buildable( buildable, NULL )->traj; + built->s.pos.trTime = level.time; + built->physicsBounce = BG_Buildable( buildable, NULL )->bounce; + built->s.groundEntityNum = -1; + //turret angles + VectorCopy( builder->s.angles2, built->s.angles2 ); + built->s.angles2[ YAW ] = angles[ YAW ]; + built->s.angles2[ PITCH ] = MGTURRET_VERTICALCAP; + + if( BG_Buildable( buildable, NULL )->team == TEAM_ALIENS ) + { + built->powered = qtrue; + built->s.eFlags |= EF_B_POWERED; + } + else if( ( built->powered = G_FindPower( built, qfalse ) ) ) + built->s.eFlags |= EF_B_POWERED; + + built->s.eFlags &= ~EF_B_SPAWNED; + + VectorCopy( normal, built->s.origin2 ); + + G_AddEvent( built, EV_BUILD_CONSTRUCT, 0 ); + + G_SetIdleBuildableAnim( built, BG_Buildable( buildable, NULL )->idleAnim ); + + if( built->builtBy >= 0 ) + G_SetBuildableAnim( built, BANIM_CONSTRUCT1, qtrue ); + + trap_LinkEntity( built ); + + if( builder && builder->client ) + { + G_TeamCommand( builder->client->ps.stats[ STAT_TEAM ], + va( "print \"%s ^2built^7 by %s%s%s\n\"", + G_CuboidName(built->s.modelindex,cuboidSize,qfalse), + builder->client->pers.netname, + ( readable[ 0 ] ) ? "^7, ^3replacing^7 " : "", + readable ) ); + G_LogPrintf( "Construct: %d %d %s%s: %s" S_COLOR_WHITE " is building " + "%s%s%s\n", + builder - g_entities, + built - g_entities, + BG_Buildable( built->s.modelindex, NULL )->name, + buildnums, + builder->client->pers.netname, + G_CuboidName(built->s.modelindex,built->cuboidSize,qtrue), + readable[ 0 ] ? ", replacing " : "", + readable ); + } + + if( log ) + G_BuildLogSet( log, built ); + + BG_CuboidPackHealthSafe(built->s.modelindex,&built->s,built->health); + + return built; +} + +/* +================= +G_BuildIfValid +================= +*/ +qboolean G_BuildIfValid( gentity_t *ent, buildable_t buildable, vec3_t cuboidSize ) +{ + float dist; + vec3_t origin, normal; + + dist = BG_Class( ent->client->ps.stats[ STAT_CLASS ] )->buildDist; + + switch( G_CanBuild( ent, buildable, dist, origin, normal, cuboidSize ) ) + { + case IBE_NONE: + G_Build( ent, buildable, origin, normal, ent->s.apos.trBase, cuboidSize ); + return qtrue; + + case IBE_NOALIENBP: + G_TriggerMenu( ent->client->ps.clientNum, MN_A_NOBP ); + 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_ONEOVERMIND: + G_TriggerMenu( ent->client->ps.clientNum, MN_A_ONEOVERMIND ); + return qfalse; + + case IBE_NORMAL: + G_TriggerMenu( ent->client->ps.clientNum, MN_B_NORMAL ); + return qfalse; + + case IBE_PERMISSION: + G_TriggerMenu( ent->client->ps.clientNum, MN_B_NORMAL ); + return qfalse; + + case IBE_ONEREACTOR: + G_TriggerMenu( ent->client->ps.clientNum, MN_H_ONEREACTOR ); + return qfalse; + + case IBE_NOPOWERHERE: + G_TriggerMenu( ent->client->ps.clientNum, MN_H_NOPOWERHERE ); + return qfalse; + + case IBE_NOROOM: + G_TriggerMenu( ent->client->ps.clientNum, MN_B_NOROOM ); + return qfalse; + + case IBE_NOHUMANBP: + G_TriggerMenu( ent->client->ps.clientNum, MN_H_NOBP); + return qfalse; + + case IBE_NODCC: + G_TriggerMenu( ent->client->ps.clientNum, MN_H_NODCC ); + return qfalse; + + case IBE_RPTPOWERHERE: + G_TriggerMenu( ent->client->ps.clientNum, MN_H_RPTPOWERHERE ); + return qfalse; + + case IBE_LASTSPAWN: + G_TriggerMenu( ent->client->ps.clientNum, MN_B_LASTSPAWN ); + return qfalse; + + case IBE_NOSURF: + G_TriggerMenu( ent->client->ps.clientNum, MN_B_NOSURF ); + return qfalse; + + case IBE_TOODENSE: + G_TriggerMenu( ent->client->ps.clientNum, MN_B_TOODENSE ); + 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 gentity_t *G_FinishSpawningBuildable( gentity_t *ent, qboolean force ) +{ + trace_t tr; + vec3_t normal, dest; + gentity_t *built; + buildable_t buildable = ent->s.modelindex; + + if( ent->s.origin2[ 0 ] || ent->s.origin2[ 1 ] || ent->s.origin2[ 2 ] ) + VectorCopy( ent->s.origin2, normal ); + else if( BG_Buildable( buildable, NULL )->traj == TR_BUOYANCY ) + VectorSet( normal, 0.0f, 0.0f, -1.0f ); + else + VectorSet( normal, 0.0f, 0.0f, 1.0f ); + + built = G_Build( ent, buildable, ent->s.pos.trBase, normal, ent->s.angles, built->cuboidSize ); + + built->takedamage = qtrue; + built->spawned = qtrue; //map entities are already spawned + built->health = BG_Buildable( buildable, built->cuboidSize )->health; + built->s.eFlags |= EF_B_SPAWNED; + + // 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 && !force ) + { + 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_SpawnBuildableThink + +Complete spawning a buildable using it's placeholder +============ +*/ +static void G_SpawnBuildableThink( gentity_t *ent ) +{ + G_FinishSpawningBuildable( ent, qfalse ); + G_FreeEntity( ent ); +} + +/* +============ +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_SpawnBuildableThink; +} + +/* +============ +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( "%s %f %f %f %f %f %f %f %f %f %f %f %f\n", + BG_Buildable( ent->s.modelindex, NULL )->name, + 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 ); +} + +/* +============ +G_LayoutList +============ +*/ +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'; + while( 1 ) + { + s = COM_ParseExt( &l, qfalse ); + if( !*s ) + break; + + if( !Q_stricmp( s, "*BUILTIN*" ) ) + { + Q_strcat( layouts, sizeof( layouts ), s ); + Q_strcat( layouts, sizeof( layouts ), " " ); + cnt++; + 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 ); + } + if( !cnt ) + { + G_Printf( S_COLOR_RED "ERROR: none of the specified layouts could be " + "found, using map default\n" ); + return; + } + layoutNum = rand( ) / ( RAND_MAX / cnt + 1 ) + 1; + cnt = 0; + + Q_strncpyz( layouts2, layouts, sizeof( layouts2 ) ); + l = &layouts2[ 0 ]; + while( 1 ) + { + s = COM_ParseExt( &l, qfalse ); + if( !*s ) + break; + + Q_strncpyz( level.layout, s, sizeof( level.layout ) ); + cnt++; + if( cnt >= layoutNum ) + break; + } + G_Printf( "using layout \"%s\" from list (%s)\n", level.layout, layouts ); +} + +/* +============ +G_LayoutBuildItem +============ +*/ +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( angles, builder->cuboidSize ); + VectorCopy( origin2, builder->s.origin2 ); + VectorCopy( angles2, builder->s.angles2 ); + G_SpawnBuildable( builder, buildable ); +} + +/* +============ +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, *layoutHead; + char map[ MAX_QPATH ]; + char buildName[ MAX_TOKEN_CHARS ]; + int buildable; + 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; + } + layoutHead = layout = BG_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 ); + break; + } + line[ i++ ] = *layout; + line[ i ] = '\0'; + if( *layout == '\n' ) + { + i = 0; + sscanf( line, "%s %f %f %f %f %f %f %f %f %f %f %f %f\n", + buildName, + &origin[ 0 ], &origin[ 1 ], &origin[ 2 ], + &angles[ 0 ], &angles[ 1 ], &angles[ 2 ], + &origin2[ 0 ], &origin2[ 1 ], &origin2[ 2 ], + &angles2[ 0 ], &angles2[ 1 ], &angles2[ 2 ] ); + + buildable = BG_BuildableByName( buildName )->number; + if( buildable <= BA_NONE || buildable >= BA_NUM_BUILDABLES ) + G_Printf( S_COLOR_YELLOW "WARNING: bad buildable name (%s) in layout." + " skipping\n", buildName ); + else + G_LayoutBuildItem( buildable, origin, angles, origin2, angles2 ); + } + layout++; + } + BG_Free( layoutHead ); +} + +/* +============ +G_BaseSelfDestruct +============ +*/ +void G_BaseSelfDestruct( team_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( ent->buildableTeam != team ) + continue; + G_Damage( ent, NULL, NULL, NULL, NULL, 10000, 0, MOD_SUICIDE ); + } +} + +/* +============ +build log +============ +*/ +buildLog_t *G_BuildLogNew( gentity_t *actor, buildFate_t fate ) +{ + buildLog_t *log = &level.buildLog[ level.buildId++ % MAX_BUILDLOG ]; + + if( level.numBuildLogs < MAX_BUILDLOG ) + level.numBuildLogs++; + log->time = level.time; + log->fate = fate; + log->actor = actor && actor->client ? actor->client->pers.namelog : NULL; + return log; +} + +void G_BuildLogSet( buildLog_t *log, gentity_t *ent ) +{ + log->modelindex = ent->s.modelindex; + log->deconstruct = log->deconstruct; + log->deconstructTime = ent->deconstructTime; + VectorCopy( ent->s.pos.trBase, log->origin ); + VectorCopy( ent->s.angles, log->angles ); + VectorCopy( ent->s.origin2, log->origin2 ); + VectorCopy( ent->s.angles2, log->angles2 ); + log->powerSource = ent->parentNode ? ent->parentNode->s.modelindex : BA_NONE; + log->powerValue = G_QueueValue( ent ); +} + +void G_BuildLogAuto( gentity_t *actor, gentity_t *buildable, buildFate_t fate ) +{ + G_BuildLogSet( G_BuildLogNew( actor, fate ), buildable ); +} + +void G_BuildLogRevertThink( gentity_t *ent ) +{ + gentity_t *built; + vec3_t mins, maxs; + int blockers[ MAX_GENTITIES ]; + int num; + int victims = 0; + int i; + + if( ent->suicideTime > 0 ) + { + BG_BuildableBoundingBox( ent->s.modelindex, mins, maxs ); + VectorAdd( ent->s.pos.trBase, mins, mins ); + VectorAdd( ent->s.pos.trBase, maxs, maxs ); + num = trap_EntitiesInBox( mins, maxs, blockers, MAX_GENTITIES ); + for( i = 0; i < num; i++ ) + { + gentity_t *targ; + vec3_t push; + + targ = g_entities + blockers[ i ]; + if( targ->client ) + { + float val = ( targ->client->ps.eFlags & EF_WALLCLIMB) ? 300.0 : 150.0; + + VectorSet( push, crandom() * val, crandom() * val, random() * val ); + VectorAdd( targ->client->ps.velocity, push, targ->client->ps.velocity ); + victims++; + } + } + + ent->suicideTime--; + + if( victims ) + { + // still a blocker + ent->nextthink = level.time + FRAMETIME; + return; + } + } + + built = G_FinishSpawningBuildable( ent, qtrue ); + if( ( built->deconstruct = ent->deconstruct ) ) + built->deconstructTime = ent->deconstructTime; + built->buildTime = built->s.time = 0; + G_KillBox( built ); + + G_LogPrintf( "revert: restore %d %s\n", + built - g_entities, BG_Buildable( built->s.modelindex, NULL )->name ); + + G_FreeEntity( ent ); +} + +void G_BuildLogRevert( int id ) +{ + buildLog_t *log; + gentity_t *ent; + int i; + vec3_t dist; + + level.numBuildablesForRemoval = 0; + + level.numBuildLogs -= level.buildId - id; + while( level.buildId > id ) + { + log = &level.buildLog[ --level.buildId % MAX_BUILDLOG ]; + if( log->fate == BF_CONSTRUCT ) + { + for( i = MAX_CLIENTS; i < level.num_entities; i++ ) + { + ent = &g_entities[ i ]; + if( ( ( ent->s.eType == ET_BUILDABLE && + ent->health > 0 ) || + ( ent->s.eType == ET_GENERAL && + ent->think == G_BuildLogRevertThink ) ) && + ent->s.modelindex == log->modelindex ) + { + VectorSubtract( ent->s.pos.trBase, log->origin, dist ); + if( VectorLengthSquared( dist ) <= 2.0f ) + { + if( ent->s.eType == ET_BUILDABLE ) + G_LogPrintf( "revert: remove %d %s\n", + ent - g_entities, BG_Buildable( ent->s.modelindex, NULL )->name ); + G_FreeEntity( ent ); + break; + } + } + } + } + else + { + gentity_t *builder = G_Spawn(); + + builder->client = NULL; + VectorCopy( log->origin, builder->s.pos.trBase ); + VectorCopy( log->angles, builder->s.angles ); + VectorCopy( log->origin2, builder->s.origin2 ); + VectorCopy( log->angles2, builder->s.angles2 ); + builder->s.modelindex = log->modelindex; + builder->deconstruct = log->deconstruct; + builder->deconstructTime = log->deconstructTime; + + builder->think = G_BuildLogRevertThink; + builder->nextthink = level.time + FRAMETIME; + + // Number of thinks before giving up and killing players in the way + builder->suicideTime = 30; + + if( log->fate == BF_DESTROY || log->fate == BF_TEAMKILL ) + { + int value = log->powerValue; + + if( BG_Buildable( log->modelindex, NULL )->team == TEAM_ALIENS ) + { + level.alienBuildPointQueue = + MAX( 0, level.alienBuildPointQueue - value ); + } + else + { + if( log->powerSource == BA_H_REACTOR ) + { + level.humanBuildPointQueue = + MAX( 0, level.humanBuildPointQueue - value ); + } + else if( log->powerSource == BA_H_REPEATER ) + { + gentity_t *source; + buildPointZone_t *zone; + + source = G_PowerEntityForPoint( log->origin ); + if( source && source->usesBuildPointZone ) + { + zone = &level.buildPointZones[ source->buildPointZone ]; + zone->queuedBuildPoints = + MAX( 0, zone->queuedBuildPoints - value ); + } + } + } + } + } + } +} + diff --git a/src/game/g_client.c b/src/game/g_client.c new file mode 100644 index 0000000..c8421f0 --- /dev/null +++ b/src/game/g_client.c @@ -0,0 +1,1800 @@ +/* +=========================================================================== +Copyright (C) 1999-2005 Id Software, Inc. +Copyright (C) 2000-2009 Darklegion Development + +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_AddCreditToClient +=============== +*/ +void G_AddCreditToClient( gclient_t *client, short credit, qboolean cap ) +{ + int capAmount; + + if( !client ) + return; + + if( cap && credit > 0 ) + { + capAmount = client->pers.teamSelection == TEAM_ALIENS ? + ALIEN_MAX_CREDITS : HUMAN_MAX_CREDITS; + if( client->pers.credit < capAmount ) + { + client->pers.credit += credit; + if( client->pers.credit > capAmount ) + client->pers.credit = capAmount; + } + } + else + client->pers.credit += credit; + + if( client->pers.credit < 0 ) + client->pers.credit = 0; + + // Copy to ps so the client can access it + 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_SelectRandomFurthestSpawnPoint + +Chooses a player start, deathmatch start, etc +============ +*/ +static 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_SelectSpawnBuildable + +find the nearest buildable of the right type that is +spawned/healthy/unblocked etc. +================ +*/ +static gentity_t *G_SelectSpawnBuildable( vec3_t preference, buildable_t buildable ) +{ + gentity_t *search, *spot; + + search = spot = NULL; + + while( ( search = G_Find( search, FOFS( classname ), + BG_Buildable( buildable, NULL )->entityName ) ) != NULL ) + { + if( !search->spawned ) + continue; + + if( search->health <= 0 ) + continue; + + if( !search->s.groundEntityNum ) + continue; + + if( search->clientSpawnTime > 0 ) + continue; + + if( G_CheckSpawnPoint( search->s.number, search->s.origin, + search->s.origin2, buildable, NULL ) != NULL ) + continue; + + if( !spot || DistanceSquared( preference, search->s.origin ) < + DistanceSquared( preference, spot->s.origin ) ) + spot = search; + } + + return spot; +} + +/* +================ +G_SelectAlienImplantedSpawn + +find the nearest impregnated human for spawning +================ +*/ +static gentity_t *G_SelectAlienImplantedSpawn( vec3_t preference ) +{ + int i; + float r; + gentity_t *search, *spot = NULL; + + for( i = 0; i < level.num_entities; i++ ) + { + search = g_entities + i; + + if( !search->client ) + continue; + + if( search->client->sess.spectatorState != SPECTATOR_NOT ) + continue; + + if( search->client->pers.teamSelection != TEAM_HUMANS ) + continue; + + if( search->client->ps.stats[ STAT_HEALTH ] <= 0 ) + continue; + + if( !search->client->isImpregnated ) + continue; + + if( !search->client->isImplantMature ) + continue; + + if( !spot || DistanceSquared( preference, search->s.origin ) < + DistanceSquared( preference, spot->s.origin ) ) + spot = search; + } + + return spot; +} + +/* +=========== +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( team_t team, vec3_t preference, vec3_t origin, vec3_t angles ) +{ + gentity_t *spot = NULL; + + if( team == TEAM_ALIENS ) + { + if( level.numAlienImplantedSpawns > 0 ) + { + spot = G_SelectAlienImplantedSpawn( preference ); + + if( spot ) + { + //spawn from a human + VectorCopy( spot->s.pos.trBase, origin ); + VectorCopy( spot->s.apos.trBase, angles ); + return spot; + } + } + + + if( level.numAlienSpawns <= 0 ) + return NULL; + + spot = G_SelectSpawnBuildable( preference, BA_A_SPAWN ); + } + else if( team == TEAM_HUMANS ) + { + if( level.numHumanSpawns <= 0 ) + return NULL; + + spot = G_SelectSpawnBuildable( preference, BA_H_SPAWN ); + } + + //no available spots + if( !spot ) + return NULL; + + if( team == TEAM_ALIENS ) + G_CheckSpawnPoint( spot->s.number, spot->s.origin, spot->s.origin2, BA_A_SPAWN, origin ); + else if( team == TEAM_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_SelectSpectatorSpawnPoint + +============ +*/ +static 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 +============= +*/ +static 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.misc = 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; +} + + +/* +============= +SpawnCorpse + +A player is respawning, so make an entity that looks +just like the existing corpse to leave behind. +============= +*/ +static void SpawnCorpse( gentity_t *ent ) +{ + gentity_t *body; + int contents; + vec3_t origin, dest; + trace_t tr; + float vDiff; + + 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_CLASS ]; + body->nonSegModel = ent->client->ps.persistant[ PERS_STATE ] & PS_NONSEGMODEL; + + if( ent->client->ps.stats[ STAT_TEAM ] == TEAM_HUMANS ) + body->classname = "humanCorpse"; + else + body->classname = "alienCorpse"; + + body->s.misc = 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_ClassBoundingBox( ent->client->ps.stats[ STAT_CLASS ], 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 ) +{ + int i; + + SpawnCorpse( ent ); + + // Clients can't respawn - they must go through the class cmd + ent->client->pers.classSelection = PCL_NONE; + ClientSpawn( ent, NULL, NULL, NULL ); + + // stop any following clients that don't have sticky spec on + for( i = 0; i < level.maxclients; i++ ) + { + if( level.clients[ i ].sess.spectatorState == SPECTATOR_FOLLOW && + level.clients[ i ].sess.spectatorClient == ent - g_entities ) + { + if( !( level.clients[ i ].pers.stickySpec ) ) + { + if( !G_FollowNewClient( &g_entities[ i ], 1 ) ) + G_StopFollowing( &g_entities[ i ] ); + } + else + G_FollowLockView( &g_entities[ i ] ); + } + } +} + +static qboolean G_IsEmoticon( const char *s, qboolean *escaped ) +{ + int i, j; + const char *p = s; + char emoticon[ MAX_EMOTICON_NAME_LEN ] = {""}; + qboolean escape = qfalse; + + if( *p != '[' ) + return qfalse; + p++; + if( *p == '[' ) + { + escape = qtrue; + p++; + } + i = 0; + while( *p && i < ( MAX_EMOTICON_NAME_LEN - 1 ) ) + { + if( *p == ']' ) + { + for( j = 0; j < level.emoticonCount; j++ ) + { + if( !Q_stricmp( emoticon, level.emoticons[ j ].name ) ) + { + *escaped = escape; + return qtrue; + } + } + return qfalse; + } + emoticon[ i++ ] = *p; + emoticon[ i ] = '\0'; + p++; + } + return qfalse; +} + +/* +=========== +G_ClientCleanName +============ +*/ +static void G_ClientCleanName( const char *in, char *out, int outSize ) +{ + int len, colorlessLen; + char *p; + int spaces; + qboolean escaped; + qboolean invalid = qfalse; + + //save room for trailing null byte + outSize--; + + len = 0; + colorlessLen = 0; + p = out; + *p = 0; + spaces = 0; + + for( ; *in; in++ ) + { + // don't allow leading spaces + if( colorlessLen == 0 && *in == ' ' ) + continue; + + // don't allow nonprinting characters or (dead) console keys + if( *in < ' ' || *in > '}' || *in == '`' ) + continue; + + // check colors + if( Q_IsColorString( in ) ) + { + in++; + + // make sure room in dest for both chars + if( len > outSize - 2 ) + break; + + *out++ = Q_COLOR_ESCAPE; + + *out++ = *in; + + len += 2; + continue; + } + else if( !g_emoticonsAllowedInNames.integer && G_IsEmoticon( in, &escaped ) ) + { + // make sure room in dest for both chars + if( len > outSize - 2 ) + break; + + *out++ = '['; + *out++ = '['; + len += 2; + if( escaped ) + in++; + continue; + } + + // don't allow too many consecutive spaces + if( *in == ' ' ) + { + spaces++; + if( spaces > 3 ) + continue; + } + else + spaces = 0; + + if( len > outSize - 1 ) + break; + + *out++ = *in; + colorlessLen++; + len++; + } + + *out = 0; + + // don't allow names beginning with "[skipnotify]" because it messes up /ignore-related code + if( !Q_stricmpn( 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_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. +============ +*/ +char *ClientUserinfoChanged( int clientNum, qboolean forceName ) +{ + gentity_t *ent; + 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; + gclient_t *client; + char userinfo[ MAX_INFO_STRING ]; + + 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"); + return "Illegal or malformed userinfo"; + } + // If their userinfo overflowed, tremded is in the process of disconnecting them. + // If we send our own disconnect, it won't work, so just return to prevent crashes later + // in this function. This check must come after the Info_Validate call. + else if( !userinfo[ 0 ] ) + return "Empty (overflowed) userinfo"; + + // stickyspec toggle + s = Info_ValueForKey( userinfo, "cg_stickySpec" ); + client->pers.stickySpec = atoi( s ) != 0; + + // set name + Q_strncpyz( oldname, client->pers.netname, sizeof( oldname ) ); + s = Info_ValueForKey( userinfo, "name" ); + G_ClientCleanName( s, newname, sizeof( newname ) ); + + if( strcmp( oldname, newname ) ) + { + if( !forceName && client->pers.namelog->nameChangeTime && + level.time - client->pers.namelog->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( !forceName && g_maxNameChanges.integer > 0 && + client->pers.namelog->nameChanges >= g_maxNameChanges.integer ) + { + trap_SendServerCommand( ent - g_entities, va( + "print \"Maximum name changes reached (g_maxNameChanges = %d)\n\"", + g_maxNameChanges.integer ) ); + revertName = qtrue; + } + else if( !forceName && client->pers.namelog->muted ) + { + trap_SendServerCommand( ent - g_entities, + "print \"You cannot change your name while you are muted\n\"" ); + revertName = qtrue; + } + else 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 ? oldname : "UnnamedPlayer", + sizeof( client->pers.netname ) ); + Info_SetValueForKey( userinfo, "name", oldname ); + trap_SetUserinfo( clientNum, userinfo ); + } + else + { + G_CensorString( client->pers.netname, newname, + sizeof( client->pers.netname ), ent ); + if( !forceName && client->pers.connected == CON_CONNECTED ) + { + client->pers.namelog->nameChangeTime = level.time; + client->pers.namelog->nameChanges++; + } + if( *oldname ) + { + G_LogPrintf( "ClientRename: %i [%s] (%s) \"%s^7\" -> \"%s^7\" \"%c%s%c^7\"\n", + clientNum, client->pers.ip.str, client->pers.guid, + oldname, client->pers.netname, + DECOLOR_OFF, client->pers.netname, DECOLOR_ON ); + } + } + G_namelog_update_name( client ); + } + + 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_ClassConfig( PCL_HUMAN_BSUIT )->modelName, + BG_ClassConfig( PCL_HUMAN_BSUIT )->skinName ); + } + else + { + Com_sprintf( buffer, MAX_QPATH, "%s/%s", BG_ClassConfig( client->pers.classSelection )->modelName, + BG_ClassConfig( client->pers.classSelection )->skinName ); + + //model segmentation + Com_sprintf( filename, sizeof( filename ), "models/players/%s/animation.cfg", + BG_ClassConfig( client->pers.classSelection )->modelName ); + + if( G_NonSegModel( filename ) ) + client->ps.persistant[ PERS_STATE ] |= PS_NONSEGMODEL; + else + client->ps.persistant[ PERS_STATE ] &= ~PS_NONSEGMODEL; + } + Q_strncpyz( model, buffer, sizeof( model ) ); + + // 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; + + // always sprint + s = Info_ValueForKey( userinfo, "cg_sprintToggle" ); + + if( atoi( s ) ) + client->ps.persistant[ PERS_STATE ] |= PS_SPRINTTOGGLE; + else + client->ps.persistant[ PERS_STATE ] &= ~PS_SPRINTTOGGLE; + + // fly speed + s = Info_ValueForKey( userinfo, "cg_flySpeed" ); + + if( *s ) + client->pers.flySpeed = atoi( s ); + else + client->pers.flySpeed = BG_Class( PCL_NONE )->speed; + + // disable blueprint errors + s = Info_ValueForKey( userinfo, "cg_disableBlueprintErrors" ); + + if( atoi( s ) ) + client->pers.disableBlueprintErrors = qtrue; + else + client->pers.disableBlueprintErrors = qfalse; + + // teamInfo + s = Info_ValueForKey( userinfo, "teamoverlay" ); + + if( 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; + + Q_strncpyz( client->pers.voice, Info_ValueForKey( userinfo, "voice" ), + sizeof( client->pers.voice ) ); + + // send over a subset of the userinfo keys so other clients can + // print scoreboards, display models, and play custom sounds + + Com_sprintf( userinfo, sizeof( userinfo ), + "n\\%s\\t\\%i\\model\\%s\\ig\\%16s\\v\\%s", + client->pers.netname, client->pers.teamSelection, model, + Com_ClientListString( &client->sess.ignoreList ), + client->pers.voice ); + + trap_SetConfigstring( CS_PLAYERS + clientNum, userinfo ); + + /*G_LogPrintf( "ClientUserinfoChanged: %i %s\n", clientNum, userinfo );*/ + + return NULL; +} + + +/* +=========== +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; + char *userInfoError; + gclient_t *client; + char userinfo[ MAX_INFO_STRING ]; + gentity_t *ent; + char reason[ MAX_STRING_CHARS ] = {""}; + int i; + + ent = &g_entities[ clientNum ]; + client = &level.clients[ clientNum ]; + + // ignore if client already connected + if( client->pers.connected != CON_DISCONNECTED ) + return NULL; + + ent->client = client; + memset( client, 0, sizeof( *client ) ); + + trap_GetUserinfo( clientNum, userinfo, sizeof( userinfo ) ); + + value = Info_ValueForKey( userinfo, "cl_guid" ); + Q_strncpyz( client->pers.guid, value, sizeof( client->pers.guid ) ); + + value = Info_ValueForKey( userinfo, "ip" ); + // check for local client + if( !strcmp( value, "localhost" ) ) + client->pers.localClient = qtrue; + G_AddressParse( value, &client->pers.ip ); + + client->pers.admin = G_admin_admin( client->pers.guid ); + + // check for admin ban + if( G_admin_ban_check( ent, reason, sizeof( reason ) ) ) + { + return va( "%s", reason ); + } + + // 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"; + + // add guid to session so we don't have to keep parsing userinfo everywhere + for( i = 0; i < sizeof( client->pers.guid ) - 1 && + isxdigit( client->pers.guid[ i ] ); i++ ); + + if( i < sizeof( client->pers.guid ) - 1 ) + return "Invalid GUID"; + + for( i = 0; i < level.maxclients; i++ ) + { + if( level.clients[ i ].pers.connected == CON_DISCONNECTED ) + continue; + + if( !Q_stricmp( client->pers.guid, level.clients[ i ].pers.guid ) ) + { + if( !G_ClientIsLagging( level.clients + i ) ) + { + trap_SendServerCommand( i, "cp \"Your GUID is not secure\"" ); + return "Duplicate GUID"; + } + trap_DropClient( i, "Ghost" ); + } + } + + client->pers.connected = CON_CONNECTING; + + // read or initialize the session data + if( firstTime || level.newSession ) + G_InitSessionData( client, userinfo ); + + G_ReadSessionData( client ); + + // get and distribute relevent paramters + G_namelog_connect( client ); + userInfoError = ClientUserinfoChanged( clientNum, qfalse ); + if( userInfoError != NULL ) + return userInfoError; + + G_LogPrintf( "ClientConnect: %i [%s] (%s) \"%s^7\" \"%c%s%c^7\"\n", + clientNum, client->pers.ip.str, client->pers.guid, + client->pers.netname, + DECOLOR_OFF, client->pers.netname, DECOLOR_ON ); + + // don't do the "xxx connected" messages if they were caried over from previous level + if( firstTime ) + trap_SendServerCommand( -1, va( "print \"%s" S_COLOR_WHITE " connected\n\"", + client->pers.netname ) ); + + if( client->pers.admin ) + G_admin_authlog( ent ); + + // count current clients and rank for scoreboard + CalculateRanks( ); + + + // if this is after !restart keepteams or !restart switchteams, apply said selection + if ( client->sess.restartTeam != TEAM_NONE ) + { + G_ChangeTeam( ent, client->sess.restartTeam ); + client->sess.restartTeam = TEAM_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; + + // ignore if client already entered the game + if( client->pers.connected != CON_CONNECTING ) + return; + + 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; + + // 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 ); + + trap_SendServerCommand( -1, va( "print \"%s" S_COLOR_WHITE " entered the game\n\"", client->pers.netname ) ); + + G_namelog_restore( client ); + + G_LogPrintf( "ClientBegin: %i\n", clientNum ); + + // count current clients and rank for scoreboard + CalculateRanks( ); + + // send the client a list of commands that can be used + G_ListCommands( ent ); + + // reset cuboidSelection + client->cuboidSelection[ 0 ] = + client->cuboidSelection[ 1 ] = + client->cuboidSelection[ 2 ] = 32; +} + +/* +=========== +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, *event; + int flags; + int savedPing; + int teamLocal; + int eventSequence; + char userinfo[ MAX_INFO_STRING ]; + vec3_t up = { 0.0f, 0.0f, 1.0f }, implant_dir, implant_angles, spawnPoint_velocity; + int maxAmmo, maxClips; + weapon_t weapon; + qboolean fromImplant = qfalse, hatchingFailed = qfalse; + + index = ent - g_entities; + client = ent->client; + + teamLocal = client->pers.teamSelection; + + //if client is dead and following teammate, stop following before spawning + if( client->sess.spectatorClient != -1 ) + { + client->sess.spectatorClient = -1; + client->sess.spectatorState = SPECTATOR_FREE; + } + + // only start client if chosen a class and joined a team + if( client->pers.classSelection == PCL_NONE && teamLocal == TEAM_NONE ) + client->sess.spectatorState = SPECTATOR_FREE; + else if( client->pers.classSelection == PCL_NONE ) + client->sess.spectatorState = SPECTATOR_LOCKED; + + // if client is dead and following teammate, stop following before spawning + if( ent->client->sess.spectatorState == SPECTATOR_FOLLOW ) + G_StopFollowing( ent ); + + 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.spectatorState != SPECTATOR_NOT ) + { + if( teamLocal == TEAM_NONE ) + spawnPoint = G_SelectSpectatorSpawnPoint( spawn_origin, spawn_angles ); + else if( teamLocal == TEAM_ALIENS ) + spawnPoint = G_SelectAlienLockSpawnPoint( spawn_origin, spawn_angles ); + else if( teamLocal == TEAM_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 ) + { + if( !spawnPoint->client ) //might be a human + { + //start spawn animation on spawnPoint + G_SetBuildableAnim( spawnPoint, BANIM_SPAWN1, qtrue ); + + if( spawnPoint->buildableTeam == TEAM_ALIENS ) + spawnPoint->clientSpawnTime = ALIEN_SPAWN_REPEAT_TIME; + else if( spawnPoint->buildableTeam == TEAM_HUMANS ) + spawnPoint->clientSpawnTime = HUMAN_SPAWN_REPEAT_TIME; + } + else + { + qboolean crouch; + int i; + float zoffs = 0.0f; + trace_t tr; + vec3_t neworigin, mins, maxs; + + fromImplant = qtrue; //spawning from a human + + // move the origin a bit on the Z axis so the aliens jumps out of the chest, not knees + // also prevents grangers from getting stuck in ceilings and floors + crouch = spawnPoint->client->ps.pm_flags & PMF_DUCKED; + switch( client->pers.classSelection ) + { + case PCL_ALIEN_BUILDER0: + if( !crouch ) + zoffs = 19.0f; + else + zoffs = -4.0f; + break; + case PCL_ALIEN_BUILDER0_UPG: + if( !crouch ) + zoffs = 16.5f; + else + zoffs = -4.0f; + break; + case PCL_ALIEN_LEVEL0: + if( !crouch ) + zoffs = 15.0f; + else + zoffs = -9.1f; + break; + } + spawn_origin[ 2 ] += zoffs; + + // check if the spot would block + BG_ClassBoundingBox( client->pers.classSelection, mins, maxs, NULL, NULL, NULL ); + trap_Trace( &tr, spawn_origin, mins, maxs, spawn_origin, spawnPoint->s.number, MASK_PLAYERSOLID ); + + // try to unblock the player + if( tr.startsolid ) + { + Com_Printf("DEBUG: player is stuck!\n"); + for( i = 0; i < 16*2; i++ ) + { + float a, r; + + VectorCopy( spawn_origin, neworigin ); + + a = (float)i / 16.0f * 2.0f * M_PI; + #define fmod(a,n) ((a)-(n)*floor((a)/(n))) + r = ( i < 16 ? 5.5f : 11.0f ) * 1.0f / cos( fmod( a+0.25f*M_PI, 0.5f*M_PI ) - 0.25f*M_PI ); + neworigin[ 0 ] += cos( a ) * r; + neworigin[ 1 ] += sin( a ) * r; + + trap_Trace( &tr, neworigin, mins, maxs, neworigin, spawnPoint->s.number, MASK_PLAYERSOLID ); + + if( !tr.startsolid ) + { + Com_Printf("DEBUG: player position fixed at iteration %i\n",i); + VectorCopy( neworigin, spawn_origin ); + break; + } + } + } + + //reward the player that implanted this one + if( spawnPoint->client->impregnatedBy >= 0 ) + { + gentity_t *granger; + + granger = &g_entities[ spawnPoint->client->impregnatedBy ]; + G_AddCreditToClient( granger->client, ALIEN_IMPREGNATION_REWARD, qtrue ); + AddScore( granger, ALIEN_IMPREGNATION_REWARD_SCORE ); + } + + // kill the human, set up angles and velocity for the new alien + if( !BG_InventoryContainsUpgrade( UP_BATTLESUIT, spawnPoint->client->ps.stats ) //humans without battlesuits always die + || spawnPoint->client->ps.stats[ STAT_HEALTH ] < ALIEN_HATCHING_MAX_BATTLESUIT_HEALTH ) //battlesuits survive if high hp + { + //save viewangles and spawn velocity for velocity calculation + VectorCopy( spawnPoint->client->ps.viewangles, implant_angles ); + AngleVectors( implant_angles, implant_dir, NULL, NULL ); + VectorCopy( spawnPoint->client->ps.velocity, spawnPoint_velocity ); + + //fire a nice chest exploding effect + event = G_TempEntity( spawnPoint->s.pos.trBase, EV_ALIEN_HATCH ); + VectorCopy( implant_dir, event->s.angles ); + + //kill the player + G_Damage( spawnPoint, NULL, ent, NULL, NULL, spawnPoint->client->ps.stats[ STAT_HEALTH ], DAMAGE_NO_ARMOR, MOD_ALIEN_HATCH ); + } + else //human survives + { + //clear impregnation so the human won't explode again + spawnPoint->client->isImpregnated = qfalse; + spawnPoint->client->isImplantMature = qfalse; + + //make a sound + event = G_TempEntity( spawnPoint->s.pos.trBase, EV_ALIEN_HATCH_FAILURE ); + + //damage the human + G_Damage( spawnPoint, NULL, ent, NULL, NULL, ALIEN_FAILED_HATCH_DAMAGE, DAMAGE_NO_ARMOR, MOD_ALIEN_HATCH ); + + //kill the newly spawned alien + VectorCopy( spawnPoint->client->ps.viewangles, implant_angles ); + implant_dir[0] = 0.0f; + implant_dir[1] = 0.0f; + implant_dir[2] = 0.0f; + hatchingFailed = qtrue; + } + } + } + } + + // toggle the teleport bit so the client knows to not lerp + flags = ( ent->client->ps.eFlags & EF_TELEPORT_BIT ) ^ 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_SPECSTATE ] = client->sess.spectatorState; + + 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; + + // calculate each client's acceleration + ent->evaluateAcceleration = qtrue; + + client->ps.stats[ STAT_MISC ] = 0; + client->buildTimer = 0; + + client->ps.eFlags = flags; + client->ps.clientNum = index; + + BG_ClassBoundingBox( ent->client->pers.classSelection, ent->r.mins, ent->r.maxs, NULL, NULL, NULL ); + + if( client->sess.spectatorState == SPECTATOR_NOT ) + client->ps.stats[ STAT_MAX_HEALTH ] = + BG_Class( ent->client->pers.classSelection )->health; + else + client->ps.stats[ STAT_MAX_HEALTH ] = 100; + + // clear entity values + if( ent->client->pers.classSelection == PCL_HUMAN ) + { + BG_AddUpgradeToInventory( UP_MEDKIT, client->ps.stats ); + weapon = client->pers.humanItemSelection; + } + else if( client->sess.spectatorState == SPECTATOR_NOT ) + weapon = BG_Class( ent->client->pers.classSelection )->startWeapon; + else + weapon = WP_NONE; + + maxAmmo = BG_Weapon( weapon )->maxAmmo; + maxClips = BG_Weapon( weapon )->maxClips; + client->ps.stats[ STAT_WEAPON ] = weapon; + client->ps.ammo = maxAmmo; + client->ps.clips = maxClips; + + // We just spawned, not changing weapons + client->ps.persistant[ PERS_NEWWEAPON ] = 0; + + ent->client->ps.stats[ STAT_CLASS ] = ent->client->pers.classSelection; + ent->client->ps.stats[ STAT_TEAM ] = 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 ] = STAMINA_MAX; + + //never impregnated after respawning + client->isImpregnated = qfalse; + client->isImplantMature = qfalse; + + 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.spectatorState == SPECTATOR_NOT && + client->ps.stats[ STAT_TEAM ] == TEAM_ALIENS ) + { + if( ent == spawn ) + { + //evolution particle system + G_AddPredictableEvent( ent, EV_ALIEN_EVOLVE, DirToByte( up ) ); + } + else if( !fromImplant ) //regular egg + { + 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 //implanted egg + { + VectorCopy( implant_angles, spawn_angles ); + VectorScale( implant_dir, ALIEN_HATCHING_VELOCITY, client->ps.velocity ); + VectorAdd( client->ps.velocity, spawnPoint_velocity, client->ps.velocity ); + } + } + else if( client->sess.spectatorState == SPECTATOR_NOT && + client->ps.stats[ STAT_TEAM ] == TEAM_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.spectatorState == SPECTATOR_NOT ) + { + trap_LinkEntity( ent ); + + // force the base weapon up + if( client->pers.teamSelection == TEAM_HUMANS ) + G_ForceWeaponChange( ent, weapon ); + + 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; + ent->nextRegenTime = 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; + } + } + } + + client->lastRantBombTime = level.time; + + // 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.spectatorState == SPECTATOR_NOT ) + { + BG_PlayerStateToEntityState( &client->ps, &ent->s, qtrue ); + VectorCopy( ent->client->ps.origin, ent->r.currentOrigin ); + trap_LinkEntity( ent ); + } + + // 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 ); + + // kill him instantly after respawning if hatching failed + if( fromImplant && hatchingFailed ) + { + VectorCopy( spawnPoint->client->ps.velocity, client->ps.velocity ); + client->ps.stats[ STAT_HEALTH ] = ent->health = 0; + player_die( ent, NULL, spawnPoint, 0, MOD_ALIEN_HATCH_FAILED ); + } + +} + + +/* +=========== +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 ) +{ + gclient_t *client; + gentity_t *ent; + gentity_t *tent; + int i; + + ent = g_entities + clientNum; + + if( !ent->client || ent->client->pers.connected == CON_DISCONNECTED ) + return; + + G_LeaveTeam( ent ); + G_namelog_disconnect( ent->client ); + G_Vote( ent, TEAM_NONE, qfalse ); + + // stop any following clients + for( i = 0; i < level.maxclients; i++ ) + { + client = &level.clients[ i ]; + + // remove any /ignore settings for this clientNum + Com_ClientListRemove( &client->sess.ignoreList, clientNum ); + + // clear impregnatedBy for everyone impregnated by this player + if( client->isImpregnated && client->impregnatedBy == clientNum ) + client->impregnatedBy = -2; + } + + // send effect if they were completely connected + if( ent->client->pers.connected == CON_CONNECTED && + ent->client->sess.spectatorState == SPECTATOR_NOT ) + { + tent = G_TempEntity( ent->client->ps.origin, EV_PLAYER_TELEPORT_OUT ); + tent->s.clientNum = ent->s.clientNum; + } + + G_LogPrintf( "ClientDisconnect: %i [%s] (%s) \"%s^7\"\n", clientNum, + ent->client->pers.ip.str, 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->sess.spectatorState = + ent->client->ps.persistant[ PERS_SPECSTATE ] = SPECTATOR_NOT; + + trap_SetConfigstring( CS_PLAYERS + clientNum, ""); + + CalculateRanks( ); +} + +/* +=========== +G_RelayCuboidToSpectators + +Called everytime a player changes his cuboid size. +A server command is issued to everyone spectating him +so that their clients can know the cuboid size as well. +============ +*/ +void G_RelayCuboidToSpectators(gentity_t *self) +{ +} + diff --git a/src/game/g_client.c.orig b/src/game/g_client.c.orig new file mode 100644 index 0000000..735c59d --- /dev/null +++ b/src/game/g_client.c.orig @@ -0,0 +1,1600 @@ +/* +=========================================================================== +Copyright (C) 1999-2005 Id Software, Inc. +Copyright (C) 2000-2009 Darklegion Development + +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_AddCreditToClient +=============== +*/ +void G_AddCreditToClient( gclient_t *client, short credit, qboolean cap ) +{ + int capAmount; + + if( !client ) + return; + + if( cap && credit > 0 ) + { + capAmount = client->pers.teamSelection == TEAM_ALIENS ? + ALIEN_MAX_CREDITS : HUMAN_MAX_CREDITS; + if( client->pers.credit < capAmount ) + { + client->pers.credit += credit; + if( client->pers.credit > capAmount ) + client->pers.credit = capAmount; + } + } + else + client->pers.credit += credit; + + if( client->pers.credit < 0 ) + client->pers.credit = 0; + + // Copy to ps so the client can access it + 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_SelectRandomFurthestSpawnPoint + +Chooses a player start, deathmatch start, etc +============ +*/ +static 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_SelectSpawnBuildable + +find the nearest buildable of the right type that is +spawned/healthy/unblocked etc. +================ +*/ +static gentity_t *G_SelectSpawnBuildable( vec3_t preference, buildable_t buildable ) +{ + gentity_t *search, *spot; + + search = spot = NULL; + + while( ( search = G_Find( search, FOFS( classname ), + BG_Buildable( buildable, NULL )->entityName ) ) != NULL ) + { + if( !search->spawned ) + continue; + + if( search->health <= 0 ) + continue; + + if( !search->s.groundEntityNum ) + continue; + + if( search->clientSpawnTime > 0 ) + continue; + + if( G_CheckSpawnPoint( search->s.number, search->s.origin, + search->s.origin2, buildable, NULL ) != NULL ) + continue; + + if( !spot || DistanceSquared( preference, search->s.origin ) < + DistanceSquared( preference, spot->s.origin ) ) + spot = search; + } + + return spot; +} + +/* +=========== +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( team_t team, vec3_t preference, vec3_t origin, vec3_t angles ) +{ + gentity_t *spot = NULL; + + if( team == TEAM_ALIENS ) + { + if( level.numAlienSpawns <= 0 ) + return NULL; + + spot = G_SelectSpawnBuildable( preference, BA_A_SPAWN ); + } + else if( team == TEAM_HUMANS ) + { + if( level.numHumanSpawns <= 0 ) + return NULL; + + spot = G_SelectSpawnBuildable( preference, BA_H_SPAWN ); + } + + //no available spots + if( !spot ) + return NULL; + + if( team == TEAM_ALIENS ) + G_CheckSpawnPoint( spot->s.number, spot->s.origin, spot->s.origin2, BA_A_SPAWN, origin ); + else if( team == TEAM_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_SelectSpectatorSpawnPoint + +============ +*/ +static 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 +============= +*/ +static 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.misc = 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; +} + + +/* +============= +SpawnCorpse + +A player is respawning, so make an entity that looks +just like the existing corpse to leave behind. +============= +*/ +static void SpawnCorpse( gentity_t *ent ) +{ + gentity_t *body; + int contents; + vec3_t origin, dest; + trace_t tr; + float vDiff; + + 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_CLASS ]; + body->nonSegModel = ent->client->ps.persistant[ PERS_STATE ] & PS_NONSEGMODEL; + + if( ent->client->ps.stats[ STAT_TEAM ] == TEAM_HUMANS ) + body->classname = "humanCorpse"; + else + body->classname = "alienCorpse"; + + body->s.misc = 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_ClassBoundingBox( ent->client->ps.stats[ STAT_CLASS ], 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 ) +{ + int i; + + SpawnCorpse( ent ); + + // Clients can't respawn - they must go through the class cmd + ent->client->pers.classSelection = PCL_NONE; + ClientSpawn( ent, NULL, NULL, NULL ); + + // stop any following clients that don't have sticky spec on + for( i = 0; i < level.maxclients; i++ ) + { + if( level.clients[ i ].sess.spectatorState == SPECTATOR_FOLLOW && + level.clients[ i ].sess.spectatorClient == ent - g_entities ) + { + if( !( level.clients[ i ].pers.stickySpec ) ) + { + if( !G_FollowNewClient( &g_entities[ i ], 1 ) ) + G_StopFollowing( &g_entities[ i ] ); + } + else + G_FollowLockView( &g_entities[ i ] ); + } + } +} + +static qboolean G_IsEmoticon( const char *s, qboolean *escaped ) +{ + int i, j; + const char *p = s; + char emoticon[ MAX_EMOTICON_NAME_LEN ] = {""}; + qboolean escape = qfalse; + + if( *p != '[' ) + return qfalse; + p++; + if( *p == '[' ) + { + escape = qtrue; + p++; + } + i = 0; + while( *p && i < ( MAX_EMOTICON_NAME_LEN - 1 ) ) + { + if( *p == ']' ) + { + for( j = 0; j < level.emoticonCount; j++ ) + { + if( !Q_stricmp( emoticon, level.emoticons[ j ].name ) ) + { + *escaped = escape; + return qtrue; + } + } + return qfalse; + } + emoticon[ i++ ] = *p; + emoticon[ i ] = '\0'; + p++; + } + return qfalse; +} + +/* +=========== +G_ClientCleanName +============ +*/ +static void G_ClientCleanName( const char *in, char *out, int outSize ) +{ + int len, colorlessLen; + char *p; + int spaces; + qboolean escaped; + qboolean invalid = qfalse; + + //save room for trailing null byte + outSize--; + + len = 0; + colorlessLen = 0; + p = out; + *p = 0; + spaces = 0; + + for( ; *in; in++ ) + { + // don't allow leading spaces + if( colorlessLen == 0 && *in == ' ' ) + continue; + + // don't allow nonprinting characters or (dead) console keys + if( *in < ' ' || *in > '}' || *in == '`' ) + continue; + + // check colors + if( Q_IsColorString( in ) ) + { + in++; + + // make sure room in dest for both chars + if( len > outSize - 2 ) + break; + + *out++ = Q_COLOR_ESCAPE; + + *out++ = *in; + + len += 2; + continue; + } + else if( !g_emoticonsAllowedInNames.integer && G_IsEmoticon( in, &escaped ) ) + { + // make sure room in dest for both chars + if( len > outSize - 2 ) + break; + + *out++ = '['; + *out++ = '['; + len += 2; + if( escaped ) + in++; + continue; + } + + // don't allow too many consecutive spaces + if( *in == ' ' ) + { + spaces++; + if( spaces > 3 ) + continue; + } + else + spaces = 0; + + if( len > outSize - 1 ) + break; + + *out++ = *in; + colorlessLen++; + len++; + } + + *out = 0; + + // don't allow names beginning with "[skipnotify]" because it messes up /ignore-related code + if( !Q_stricmpn( 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_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. +============ +*/ +char *ClientUserinfoChanged( int clientNum, qboolean forceName ) +{ + gentity_t *ent; + 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; + gclient_t *client; + char userinfo[ MAX_INFO_STRING ]; + + 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"); + return "Illegal or malformed userinfo"; + } + // If their userinfo overflowed, tremded is in the process of disconnecting them. + // If we send our own disconnect, it won't work, so just return to prevent crashes later + // in this function. This check must come after the Info_Validate call. + else if( !userinfo[ 0 ] ) + return "Empty (overflowed) userinfo"; + + // stickyspec toggle + s = Info_ValueForKey( userinfo, "cg_stickySpec" ); + client->pers.stickySpec = atoi( s ) != 0; + + // set name + Q_strncpyz( oldname, client->pers.netname, sizeof( oldname ) ); + s = Info_ValueForKey( userinfo, "name" ); + G_ClientCleanName( s, newname, sizeof( newname ) ); + + if( strcmp( oldname, newname ) ) + { + if( !forceName && client->pers.namelog->nameChangeTime && + level.time - client->pers.namelog->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( !forceName && g_maxNameChanges.integer > 0 && + client->pers.namelog->nameChanges >= g_maxNameChanges.integer ) + { + trap_SendServerCommand( ent - g_entities, va( + "print \"Maximum name changes reached (g_maxNameChanges = %d)\n\"", + g_maxNameChanges.integer ) ); + revertName = qtrue; + } + else if( !forceName && client->pers.namelog->muted ) + { + trap_SendServerCommand( ent - g_entities, + "print \"You cannot change your name while you are muted\n\"" ); + revertName = qtrue; + } + else 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 ? oldname : "UnnamedPlayer", + sizeof( client->pers.netname ) ); + Info_SetValueForKey( userinfo, "name", oldname ); + trap_SetUserinfo( clientNum, userinfo ); + } + else + { + G_CensorString( client->pers.netname, newname, + sizeof( client->pers.netname ), ent ); + if( !forceName && client->pers.connected == CON_CONNECTED ) + { + client->pers.namelog->nameChangeTime = level.time; + client->pers.namelog->nameChanges++; + } + if( *oldname ) + { + G_LogPrintf( "ClientRename: %i [%s] (%s) \"%s^7\" -> \"%s^7\" \"%c%s%c^7\"\n", + clientNum, client->pers.ip.str, client->pers.guid, + oldname, client->pers.netname, + DECOLOR_OFF, client->pers.netname, DECOLOR_ON ); + } + } + G_namelog_update_name( client ); + } + + 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_ClassConfig( PCL_HUMAN_BSUIT )->modelName, + BG_ClassConfig( PCL_HUMAN_BSUIT )->skinName ); + } + else + { + Com_sprintf( buffer, MAX_QPATH, "%s/%s", BG_ClassConfig( client->pers.classSelection )->modelName, + BG_ClassConfig( client->pers.classSelection )->skinName ); + + //model segmentation + Com_sprintf( filename, sizeof( filename ), "models/players/%s/animation.cfg", + BG_ClassConfig( client->pers.classSelection )->modelName ); + + if( G_NonSegModel( filename ) ) + client->ps.persistant[ PERS_STATE ] |= PS_NONSEGMODEL; + else + client->ps.persistant[ PERS_STATE ] &= ~PS_NONSEGMODEL; + } + Q_strncpyz( model, buffer, sizeof( model ) ); + + // 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; + + // always sprint + s = Info_ValueForKey( userinfo, "cg_sprintToggle" ); + + if( atoi( s ) ) + client->ps.persistant[ PERS_STATE ] |= PS_SPRINTTOGGLE; + else + client->ps.persistant[ PERS_STATE ] &= ~PS_SPRINTTOGGLE; + + // fly speed + s = Info_ValueForKey( userinfo, "cg_flySpeed" ); + + if( *s ) + client->pers.flySpeed = atoi( s ); + else + client->pers.flySpeed = BG_Class( PCL_NONE )->speed; + + // disable blueprint errors + s = Info_ValueForKey( userinfo, "cg_disableBlueprintErrors" ); + + if( atoi( s ) ) + client->pers.disableBlueprintErrors = qtrue; + else + client->pers.disableBlueprintErrors = qfalse; + + // teamInfo + s = Info_ValueForKey( userinfo, "teamoverlay" ); + + if( 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; + + Q_strncpyz( client->pers.voice, Info_ValueForKey( userinfo, "voice" ), + sizeof( client->pers.voice ) ); + + // send over a subset of the userinfo keys so other clients can + // print scoreboards, display models, and play custom sounds + + Com_sprintf( userinfo, sizeof( userinfo ), + "n\\%s\\t\\%i\\model\\%s\\ig\\%16s\\v\\%s", + client->pers.netname, client->pers.teamSelection, model, + Com_ClientListString( &client->sess.ignoreList ), + client->pers.voice ); + + trap_SetConfigstring( CS_PLAYERS + clientNum, userinfo ); + + /*G_LogPrintf( "ClientUserinfoChanged: %i %s\n", clientNum, userinfo );*/ + + return NULL; +} + + +/* +=========== +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; + char *userInfoError; + gclient_t *client; + char userinfo[ MAX_INFO_STRING ]; + gentity_t *ent; + char reason[ MAX_STRING_CHARS ] = {""}; + int i; + + ent = &g_entities[ clientNum ]; + client = &level.clients[ clientNum ]; + + // ignore if client already connected + if( client->pers.connected != CON_DISCONNECTED ) + return NULL; + + ent->client = client; + memset( client, 0, sizeof( *client ) ); + + trap_GetUserinfo( clientNum, userinfo, sizeof( userinfo ) ); + + value = Info_ValueForKey( userinfo, "cl_guid" ); + Q_strncpyz( client->pers.guid, value, sizeof( client->pers.guid ) ); + + value = Info_ValueForKey( userinfo, "ip" ); + // check for local client + if( !strcmp( value, "localhost" ) ) + client->pers.localClient = qtrue; + G_AddressParse( value, &client->pers.ip ); + + client->pers.admin = G_admin_admin( client->pers.guid ); + + // check for admin ban + if( G_admin_ban_check( ent, reason, sizeof( reason ) ) ) + { + return va( "%s", reason ); + } + + // 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"; + + // add guid to session so we don't have to keep parsing userinfo everywhere + for( i = 0; i < sizeof( client->pers.guid ) - 1 && + isxdigit( client->pers.guid[ i ] ); i++ ); + + if( i < sizeof( client->pers.guid ) - 1 ) + return "Invalid GUID"; + + for( i = 0; i < level.maxclients; i++ ) + { + if( level.clients[ i ].pers.connected == CON_DISCONNECTED ) + continue; + + if( !Q_stricmp( client->pers.guid, level.clients[ i ].pers.guid ) ) + { + if( !G_ClientIsLagging( level.clients + i ) ) + { + trap_SendServerCommand( i, "cp \"Your GUID is not secure\"" ); + return "Duplicate GUID"; + } + trap_DropClient( i, "Ghost" ); + } + } + + client->pers.connected = CON_CONNECTING; + + // read or initialize the session data + if( firstTime || level.newSession ) + G_InitSessionData( client, userinfo ); + + G_ReadSessionData( client ); + + // get and distribute relevent paramters + G_namelog_connect( client ); + userInfoError = ClientUserinfoChanged( clientNum, qfalse ); + if( userInfoError != NULL ) + return userInfoError; + + G_LogPrintf( "ClientConnect: %i [%s] (%s) \"%s^7\" \"%c%s%c^7\"\n", + clientNum, client->pers.ip.str, client->pers.guid, + client->pers.netname, + DECOLOR_OFF, client->pers.netname, DECOLOR_ON ); + + // don't do the "xxx connected" messages if they were caried over from previous level + if( firstTime ) + trap_SendServerCommand( -1, va( "print \"%s" S_COLOR_WHITE " connected\n\"", + client->pers.netname ) ); + + if( client->pers.admin ) + G_admin_authlog( ent ); + + // count current clients and rank for scoreboard + CalculateRanks( ); + + + // if this is after !restart keepteams or !restart switchteams, apply said selection + if ( client->sess.restartTeam != TEAM_NONE ) + { + G_ChangeTeam( ent, client->sess.restartTeam ); + client->sess.restartTeam = TEAM_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; + + // ignore if client already entered the game + if( client->pers.connected != CON_CONNECTING ) + return; + + 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; + + // 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 ); + + trap_SendServerCommand( -1, va( "print \"%s" S_COLOR_WHITE " entered the game\n\"", client->pers.netname ) ); + + G_namelog_restore( client ); + + G_LogPrintf( "ClientBegin: %i\n", clientNum ); + + // count current clients and rank for scoreboard + CalculateRanks( ); + + // send the client a list of commands that can be used + G_ListCommands( ent ); + + // reset cuboidSelection + client->cuboidSelection[ 0 ] = + client->cuboidSelection[ 1 ] = + client->cuboidSelection[ 2 ] = 32; +} + +/* +=========== +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; + + //if client is dead and following teammate, stop following before spawning + if( client->sess.spectatorClient != -1 ) + { + client->sess.spectatorClient = -1; + client->sess.spectatorState = SPECTATOR_FREE; + } + + // only start client if chosen a class and joined a team + if( client->pers.classSelection == PCL_NONE && teamLocal == TEAM_NONE ) + client->sess.spectatorState = SPECTATOR_FREE; + else if( client->pers.classSelection == PCL_NONE ) + client->sess.spectatorState = SPECTATOR_LOCKED; + + // if client is dead and following teammate, stop following before spawning + if( ent->client->sess.spectatorState == SPECTATOR_FOLLOW ) + G_StopFollowing( ent ); + + 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.spectatorState != SPECTATOR_NOT ) + { + if( teamLocal == TEAM_NONE ) + spawnPoint = G_SelectSpectatorSpawnPoint( spawn_origin, spawn_angles ); + else if( teamLocal == TEAM_ALIENS ) + spawnPoint = G_SelectAlienLockSpawnPoint( spawn_origin, spawn_angles ); + else if( teamLocal == TEAM_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->buildableTeam == TEAM_ALIENS ) + spawnPoint->clientSpawnTime = ALIEN_SPAWN_REPEAT_TIME; + else if( spawnPoint->buildableTeam == TEAM_HUMANS ) + spawnPoint->clientSpawnTime = HUMAN_SPAWN_REPEAT_TIME; + } + } + + // toggle the teleport bit so the client knows to not lerp + flags = ( ent->client->ps.eFlags & EF_TELEPORT_BIT ) ^ 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_SPECSTATE ] = client->sess.spectatorState; + + 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; + + // calculate each client's acceleration + ent->evaluateAcceleration = qtrue; + + client->ps.stats[ STAT_MISC ] = 0; + client->buildTimer = 0; + + client->ps.eFlags = flags; + client->ps.clientNum = index; + + BG_ClassBoundingBox( ent->client->pers.classSelection, ent->r.mins, ent->r.maxs, NULL, NULL, NULL ); + + if( client->sess.spectatorState == SPECTATOR_NOT ) + client->ps.stats[ STAT_MAX_HEALTH ] = + BG_Class( ent->client->pers.classSelection )->health; + else + client->ps.stats[ STAT_MAX_HEALTH ] = 100; + + // clear entity values + if( ent->client->pers.classSelection == PCL_HUMAN ) + { + BG_AddUpgradeToInventory( UP_MEDKIT, client->ps.stats ); + weapon = client->pers.humanItemSelection; + } + else if( client->sess.spectatorState == SPECTATOR_NOT ) + weapon = BG_Class( ent->client->pers.classSelection )->startWeapon; + else + weapon = WP_NONE; + + maxAmmo = BG_Weapon( weapon )->maxAmmo; + maxClips = BG_Weapon( weapon )->maxClips; + client->ps.stats[ STAT_WEAPON ] = weapon; + client->ps.ammo = maxAmmo; + client->ps.clips = maxClips; + + // We just spawned, not changing weapons + client->ps.persistant[ PERS_NEWWEAPON ] = 0; + + ent->client->ps.stats[ STAT_CLASS ] = ent->client->pers.classSelection; + ent->client->ps.stats[ STAT_TEAM ] = 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 ] = STAMINA_MAX; + + 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.spectatorState == SPECTATOR_NOT && + client->ps.stats[ STAT_TEAM ] == TEAM_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.spectatorState == SPECTATOR_NOT && + client->ps.stats[ STAT_TEAM ] == TEAM_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.spectatorState == SPECTATOR_NOT ) + { + trap_LinkEntity( ent ); + + // force the base weapon up + if( client->pers.teamSelection == TEAM_HUMANS ) + G_ForceWeaponChange( ent, weapon ); + + 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; + ent->nextRegenTime = 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; + } + } + } + + client->lastRantBombTime = level.time; + + // 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.spectatorState == SPECTATOR_NOT ) + { + BG_PlayerStateToEntityState( &client->ps, &ent->s, qtrue ); + VectorCopy( ent->client->ps.origin, ent->r.currentOrigin ); + trap_LinkEntity( ent ); + } + + // 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; + + ent = g_entities + clientNum; + + if( !ent->client || ent->client->pers.connected == CON_DISCONNECTED ) + return; + + G_LeaveTeam( ent ); + G_namelog_disconnect( ent->client ); + G_Vote( ent, TEAM_NONE, qfalse ); + + // stop any following clients + for( i = 0; i < level.maxclients; i++ ) + { + // remove any /ignore settings for this clientNum + Com_ClientListRemove( &level.clients[ i ].sess.ignoreList, clientNum ); + } + + // send effect if they were completely connected + if( ent->client->pers.connected == CON_CONNECTED && + ent->client->sess.spectatorState == SPECTATOR_NOT ) + { + tent = G_TempEntity( ent->client->ps.origin, EV_PLAYER_TELEPORT_OUT ); + tent->s.clientNum = ent->s.clientNum; + } + + G_LogPrintf( "ClientDisconnect: %i [%s] (%s) \"%s^7\"\n", clientNum, + ent->client->pers.ip.str, 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->sess.spectatorState = + ent->client->ps.persistant[ PERS_SPECSTATE ] = SPECTATOR_NOT; + + trap_SetConfigstring( CS_PLAYERS + clientNum, ""); + + CalculateRanks( ); +} + +/* +=========== +G_RelayCuboidToSpectators + +Called everytime a player changes his cuboid size. +A server command is issued to everyone spectating him +so that their clients can know the cuboid size as well. +============ +*/ +void G_RelayCuboidToSpectators(gentity_t *self) +{ +} + diff --git a/src/game/g_cmds.c b/src/game/g_cmds.c new file mode 100644 index 0000000..3406f4e --- /dev/null +++ b/src/game/g_cmds.c @@ -0,0 +1,3609 @@ +/* +=========================================================================== +Copyright (C) 1999-2005 Id Software, Inc. +Copyright (C) 2000-2009 Darklegion Development + +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 color codes and non-alphanumeric characters from a string +================== +*/ +void G_SanitiseString( char *in, char *out, int len ) +{ + len--; + + while( *in && len > 0 ) + { + if( Q_IsColorString( in ) ) + { + in += 2; // skip color code + continue; + } + + if( isalnum( *in ) ) + { + *out++ = tolower( *in ); + len--; + } + in++; + } + *out = 0; +} + +/* +================== +G_ClientNumberFromString + +Returns a player number for either a number or name string +Returns -1 and optionally sets err if invalid or not exactly 1 match +err will have a trailing \n if set +================== +*/ +int G_ClientNumberFromString( char *s, char *err, int len ) +{ + gclient_t *cl; + int i, found = 0, m = -1; + char s2[ MAX_NAME_LENGTH ]; + char n2[ MAX_NAME_LENGTH ]; + char *p = err; + int l, l2 = len; + + if( !s[ 0 ] ) + { + if( p ) + Q_strncpyz( p, "no player name or slot # provided\n", len ); + + return -1; + } + + // numeric values are just slot numbers + for( i = 0; s[ i ] && isdigit( s[ i ] ); i++ ); + if( !s[ i ] ) + { + i = atoi( s ); + + if( i < 0 || i >= level.maxclients ) + return -1; + + cl = &level.clients[ i ]; + + if( cl->pers.connected == CON_DISCONNECTED ) + { + if( p ) + Q_strncpyz( p, "no player connected in that slot #\n", len ); + + return -1; + } + + return i; + } + + G_SanitiseString( s, s2, sizeof( s2 ) ); + if( !s2[ 0 ] ) + { + if( p ) + Q_strncpyz( p, "no player name provided\n", len ); + + return -1; + } + + if( p ) + { + Q_strncpyz( p, "more than one player name matches. " + "be more specific or use the slot #:\n", l2 ); + l = strlen( p ); + p += l; + l2 -= l; + } + + // check for a name match + for( i = 0, cl = level.clients; i < level.maxclients; i++, cl++ ) + { + if( cl->pers.connected == CON_DISCONNECTED ) + continue; + + G_SanitiseString( cl->pers.netname, n2, sizeof( n2 ) ); + + if( !strcmp( n2, s2 ) ) + return i; + + if( strstr( n2, s2 ) ) + { + if( p ) + { + l = Q_snprintf( p, l2, "%-2d - %s^7\n", i, cl->pers.netname ); + p += l; + l2 -= l; + } + + found++; + m = i; + } + } + + if( found == 1 ) + return m; + + if( found == 0 && err ) + Q_strncpyz( err, "no connected player by that name or slot #\n", len ); + + return -1; +} + +/* +================== +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. +================== +*/ +int G_ClientNumbersFromString( char *s, int *plist, int max ) +{ + gclient_t *p; + int i, found = 0; + char *endptr; + char n2[ MAX_NAME_LENGTH ] = {""}; + char s2[ MAX_NAME_LENGTH ] = {""}; + + if( max == 0 ) + return 0; + + if( !s[ 0 ] ) + return 0; + + // if a number is provided, it is a clientnum + i = strtol( s, &endptr, 10 ); + if( *endptr == '\0' ) + { + 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 + return 0; + } + + // now look for name matches + G_SanitiseString( s, s2, sizeof( s2 ) ); + if( !s2[ 0 ] ) + 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++; + } + } + 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 + ping = cl->ps.ping < 999 ? cl->ps.ping : 999; + + if( cl->sess.spectatorState == SPECTATOR_NOT && + ( ent->client->pers.teamSelection == TEAM_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_MK1, cl->ps.stats ) ) + upgrade = UP_HELMET_MK1; + else if( BG_InventoryContainsUpgrade( UP_HELMET_MK2, cl->ps.stats ) ) + upgrade = UP_HELMET_MK2; + 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->ps.persistant[ 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%s", + 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; +} + +/* +================== +ConcatArgsPrintable +Duplicate of concatargs but enquotes things that need to be +Used to log command arguments in a way that preserves user intended tokenizing +================== +*/ +char *ConcatArgsPrintable( int start ) +{ + int i, c, tlen; + static char line[ MAX_STRING_CHARS ]; + int len; + char arg[ MAX_STRING_CHARS + 2 ]; + char *printArg; + + len = 0; + c = trap_Argc( ); + + for( i = start; i < c; i++ ) + { + printArg = arg; + trap_Argv( i, arg, sizeof( arg ) ); + if( strchr( arg, ' ' ) ) + printArg = va( "\"%s\"", arg ); + tlen = strlen( printArg ); + + if( len + tlen >= MAX_STRING_CHARS - 1 ) + break; + + memcpy( line + len, printArg, tlen ); + len += tlen; + + if( len == MAX_STRING_CHARS - 1 ) + break; + + if( i != c - 1 ) + { + line[ len ] = ' '; + len++; + } + } + + line[ len ] = 0; + + return line; +} + + +/* +================== +Cmd_Give_f + +Give items to a client +================== +*/ +void Cmd_Give_f( gentity_t *ent ) +{ + char *name; + qboolean give_all = qfalse; + + if( trap_Argc( ) < 2 ) + { + ADMP( "usage: give [what]\n" ); + ADMP( "usage: valid choices are: all, health, funds [amount], stamina, " + "poison, gas, ammo\n" ); + return; + } + + name = ConcatArgs( 1 ); + if( Q_stricmp( name, "all" ) == 0 ) + give_all = qtrue; + + if( give_all || Q_stricmp( name, "health" ) == 0 ) + { + 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 ) + { + float credits; + + if( give_all || trap_Argc( ) < 3 ) + credits = 30000.0f; + else + { + credits = atof( name + 6 ) * + ( ent->client->pers.teamSelection == + TEAM_ALIENS ? ALIEN_CREDITS_PER_KILL : 1.0f ); + + // clamp credits manually, as G_AddCreditToClient() expects a short int + if( credits > SHRT_MAX ) + credits = 30000.0f; + else if( credits < SHRT_MIN ) + credits = -30000.0f; + } + + G_AddCreditToClient( ent->client, (short)credits, qtrue ); + } + + if( give_all || Q_stricmp( name, "stamina" ) == 0 ) + ent->client->ps.stats[ STAT_STAMINA ] = STAMINA_MAX; + + if( Q_stricmp( name, "poison" ) == 0 ) + { + if( ent->client->pers.teamSelection == TEAM_HUMANS ) + { + ent->client->ps.stats[ STAT_STATE ] |= SS_POISONED; + ent->client->lastPoisonTime = level.time; + ent->client->lastPoisonClient = ent; + } + else + { + ent->client->ps.stats[ STAT_STATE ] |= SS_BOOSTED; + ent->client->boostedTime = level.time; + } + } + + if( Q_stricmp( name, "gas" ) == 0 ) + { + ent->client->ps.eFlags |= EF_POISONCLOUDED; + ent->client->lastPoisonCloudedTime = level.time; + trap_SendServerCommand( ent->client->ps.clientNum, + "poisoncloud" ); + } + + if( give_all || Q_stricmp( name, "ammo" ) == 0 ) + { + gclient_t *client = ent->client; + + if( client->ps.weapon != WP_ALEVEL3_UPG && + BG_Weapon( client->ps.weapon )->infiniteAmmo ) + return; + + client->ps.ammo = BG_Weapon( client->ps.weapon )->maxAmmo; + client->ps.clips = BG_Weapon( client->ps.weapon )->maxClips; + + if( BG_Weapon( client->ps.weapon )->usesEnergy && + BG_InventoryContainsUpgrade( UP_BATTPACK, client->ps.stats ) ) + client->ps.ammo = (int)( (float)client->ps.ammo * BATTPACK_MODIFIER ); + } + + if( give_all || Q_stricmp( name, "fuel" ) == 0 ) + { + if( BG_InventoryContainsUpgrade( UP_JETPACK, ent->client->ps.stats ) ) + ent->client->ps.stats[ STAT_FUEL ] = JETPACK_FUEL_FULL; + } +} + + +/* +================== +Cmd_God_f + +Sets client to godmode + +argv(0) god +================== +*/ +void Cmd_God_f( gentity_t *ent ) +{ + char *msg; + + ent->flags ^= FL_GODMODE; + + if( !( ent->flags & FL_GODMODE ) ) + msg = "godmode OFF\n"; + else + msg = "godmode ON\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; + + ent->flags ^= FL_NOTARGET; + + if( !( ent->flags & FL_NOTARGET ) ) + msg = "notarget OFF\n"; + else + msg = "notarget ON\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( ent->client->noclip ) + msg = "noclip OFF\n"; + else + msg = "noclip ON\n"; + + ent->client->noclip = !ent->client->noclip; + + 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( 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 cancelled\n\"" ); + ent->suicideTime = 0; + } + } +} + +/* +================= +Cmd_Impregnate_f + +Implants an egg in one's stomach (slowblob equivalent) +Used for debugging only (cheat-protected) +================= +*/ +void Cmd_Impregnate_f( gentity_t *ent ) +{ + if( !ent->client->isImpregnated ) + { + ent->client->isImpregnated = qtrue; + ent->client->impregnationTime = level.time; + + trap_SendServerCommand( ent-g_entities, + "print \"You have impregnated yourself with an alien egg\n\"" ); + } +} + +/* +================= +Cmd_Team_f +================= +*/ +void Cmd_Team_f( gentity_t *ent ) +{ + team_t team; + team_t oldteam = ent->client->pers.teamSelection; + char s[ MAX_TOKEN_CHARS ]; + qboolean force = G_admin_permission( ent, ADMF_FORCETEAMCHANGE ); + int aliens = level.numAlienClients; + int humans = level.numHumanClients; + + if( oldteam == TEAM_ALIENS ) + aliens--; + else if( oldteam == TEAM_HUMANS ) + humans--; + + // stop team join spam + if( ent->client->pers.teamChangeTime && + level.time - ent->client->pers.teamChangeTime < 1000 ) + return; + + // stop switching teams for gameplay exploit reasons by enforcing a long + // wait before they can come back + if( !force && !g_cheats.integer && ent->client->pers.aliveSeconds && + level.time - ent->client->pers.teamChangeTime < 30000 ) + { + trap_SendServerCommand( ent-g_entities, + va( "print \"You must wait another %d seconds before changing teams again\n\"", + (int) ( ( 30000 - ( level.time - ent->client->pers.teamChangeTime ) ) / 1000.f ) ) ); + return; + } + + // disallow joining teams during warmup + if( g_doWarmup.integer && ( ( level.warmupTime - level.time ) / 1000 ) > 0 ) + { + G_TriggerMenu( ent - g_entities, MN_WARMUP ); + return; + } + + trap_Argv( 1, s, sizeof( s ) ); + + if( !s[ 0 ] ) + { + trap_SendServerCommand( ent-g_entities, va( "print \"team: %s\n\"", + BG_TeamName( oldteam ) ) ); + return; + } + + if( !Q_stricmp( s, "auto" ) ) + { + if( level.humanTeamLocked && level.alienTeamLocked ) + team = TEAM_NONE; + else if( level.humanTeamLocked || humans > aliens ) + team = TEAM_ALIENS; + + else if( level.alienTeamLocked || aliens > humans ) + team = TEAM_HUMANS; + else + team = TEAM_ALIENS + rand( ) / ( RAND_MAX / 2 + 1 ); + } + else switch( G_TeamFromString( s ) ) + { + case TEAM_NONE: + team = TEAM_NONE; + break; + + case TEAM_ALIENS: + if( level.alienTeamLocked ) + { + G_TriggerMenu( ent - g_entities, MN_A_TEAMLOCKED ); + return; + } + else if( level.humanTeamLocked ) + force = qtrue; + + if( !force && g_teamForceBalance.integer && aliens > humans ) + { + G_TriggerMenu( ent - g_entities, MN_A_TEAMFULL ); + return; + } + + team = TEAM_ALIENS; + break; + + case TEAM_HUMANS: + if( level.humanTeamLocked ) + { + G_TriggerMenu( ent - g_entities, MN_H_TEAMLOCKED ); + return; + } + else if( level.alienTeamLocked ) + force = qtrue; + + if( !force && g_teamForceBalance.integer && humans > aliens ) + { + G_TriggerMenu( ent - g_entities, MN_H_TEAMFULL ); + return; + } + + team = TEAM_HUMANS; + break; + + default: + trap_SendServerCommand( ent-g_entities, + va( "print \"Unknown team: %s\n\"", s ) ); + return; + } + + // stop team join spam + if( oldteam == team ) + return; + + if( team != TEAM_NONE && g_maxGameClients.integer && + level.numPlayingClients >= g_maxGameClients.integer ) + { + G_TriggerMenu( ent - g_entities, MN_PLAYERLIMIT ); + return; + } + + // Apply the change + G_ChangeTeam( ent, team ); +} + +/* +================== +G_CensorString +================== +*/ +static char censors[ 20000 ]; +static int numcensors; + +void G_LoadCensors( void ) +{ + char *text_p, *token; + char text[ 20000 ]; + char *term; + int len; + fileHandle_t f; + + numcensors = 0; + + if( !g_censorship.string[ 0 ] ) + return; + + len = trap_FS_FOpenFile( g_censorship.string, &f, FS_READ ); + if( len < 0 ) + { + Com_Printf( S_COLOR_RED "ERROR: Censors file %s doesn't exist\n", + g_censorship.string ); + return; + } + if( len == 0 || len >= sizeof( text ) - 1 ) + { + trap_FS_FCloseFile( f ); + Com_Printf( S_COLOR_RED "ERROR: Censors file %s is %s\n", + g_censorship.string, len == 0 ? "empty" : "too long" ); + return; + } + trap_FS_Read( text, len, f ); + trap_FS_FCloseFile( f ); + text[ len ] = 0; + + term = censors; + + text_p = text; + while( 1 ) + { + token = COM_Parse( &text_p ); + if( !*token || sizeof( censors ) - ( term - censors ) < 4 ) + break; + Q_strncpyz( term, token, sizeof( censors ) - ( term - censors ) ); + Q_strlwr( term ); + term += strlen( term ) + 1; + if( sizeof( censors ) - ( term - censors ) == 0 ) + break; + token = COM_ParseExt( &text_p, qfalse ); + Q_strncpyz( term, token, sizeof( censors ) - ( term - censors ) ); + term += strlen( term ) + 1; + numcensors++; + } + G_Printf( "Parsed %d string replacements\n", numcensors ); +} + +void G_CensorString( char *out, const char *in, int len, gentity_t *ent ) +{ + const char *s, *m; + int i; + + if( !numcensors || G_admin_permission( ent, ADMF_NOCENSORFLOOD) ) + { + Q_strncpyz( out, in, len ); + return; + } + + len--; + while( *in ) + { + if( Q_IsColorString( in ) ) + { + if( len < 2 ) + break; + *out++ = *in++; + *out++ = *in++; + len -= 2; + continue; + } + if( !isalnum( *in ) ) + { + if( len < 1 ) + break; + *out++ = *in++; + len--; + continue; + } + m = censors; + for( i = 0; i < numcensors; i++, m++ ) + { + s = in; + while( *s && *m ) + { + if( Q_IsColorString( s ) ) + { + s += 2; + continue; + } + if( !isalnum( *s ) ) + { + s++; + continue; + } + if( tolower( *s ) != *m ) + break; + s++; + m++; + } + // match + if( !*m ) + { + in = s; + m++; + while( *m ) + { + if( len < 1 ) + break; + *out++ = *m++; + len--; + } + break; + } + else + { + while( *m ) + m++; + m++; + while( *m ) + m++; + } + } + if( len < 1 ) + break; + // no match + if( i == numcensors ) + { + *out++ = *in++; + len--; + } + } + *out = 0; +} + +/* +================== +G_Say +================== +*/ +static qboolean G_SayTo( gentity_t *ent, gentity_t *other, saymode_t mode, const char *message ) +{ + if( !other ) + return qfalse; + + if( !other->inuse ) + return qfalse; + + if( !other->client ) + return qfalse; + + if( other->client->pers.connected != CON_CONNECTED ) + return qfalse; + + if( ( ent && !OnSameTeam( ent, other ) ) && + ( mode == SAY_TEAM || mode == SAY_AREA || mode == SAY_TPRIVMSG ) ) + { + if( other->client->pers.teamSelection != TEAM_NONE ) + return qfalse; + + // specs with ADMF_SPEC_ALLCHAT flag can see team chat + if( !G_admin_permission( other, ADMF_SPEC_ALLCHAT ) && mode != SAY_TPRIVMSG ) + return qfalse; + } + + trap_SendServerCommand( other-g_entities, va( "chat %d %d \"%s\"", + ent ? ent-g_entities : -1, + mode, + message ) ); + + return qtrue; +} + +void G_Say( gentity_t *ent, saymode_t mode, const char *chatText ) +{ + int j; + gentity_t *other; + // don't let text be too long for malicious reasons + char text[ MAX_SAY_TEXT ]; + + // check if blocked by g_specChat 0 + if( ( !g_specChat.integer ) && ( mode != SAY_TEAM ) && + ( ent ) && ( ent->client->pers.teamSelection == TEAM_NONE ) && + ( !G_admin_permission( ent, ADMF_NOCENSORFLOOD ) ) ) + { + trap_SendServerCommand( ent-g_entities, "print \"say: Global chatting for " + "spectators has been disabled. You may only use team chat.\n\"" ); + mode = SAY_TEAM; + } + + switch( mode ) + { + case SAY_ALL: + G_LogPrintf( "Say: %d \"%s" S_COLOR_WHITE "\": " S_COLOR_GREEN "%s\n", + ( ent ) ? ent - g_entities : -1, + ( ent ) ? ent->client->pers.netname : "console", chatText ); + break; + case SAY_TEAM: + // console say_team is handled in g_svscmds, not here + if( !ent || !ent->client ) + Com_Error( ERR_FATAL, "SAY_TEAM by non-client entity\n" ); + G_LogPrintf( "SayTeam: %d \"%s" S_COLOR_WHITE "\": " S_COLOR_CYAN "%s\n", + ent - g_entities, ent->client->pers.netname, chatText ); + break; + case SAY_RAW: + if( ent ) + Com_Error( ERR_FATAL, "SAY_RAW by client entity\n" ); + G_LogPrintf( "Chat: -1 \"console\": %s\n", chatText ); + default: + break; + } + + G_CensorString( text, chatText, sizeof( text ), ent ); + + // send it to all the apropriate clients + for( j = 0; j < level.maxclients; j++ ) + { + other = &g_entities[ j ]; + G_SayTo( ent, other, mode, text ); + } +} + +/* +================== +Cmd_SayArea_f +================== +*/ +static void Cmd_SayArea_f( gentity_t *ent ) +{ + int entityList[ MAX_GENTITIES ]; + int num, i; + vec3_t range = { 1000.0f, 1000.0f, 1000.0f }; + vec3_t mins, maxs; + char *msg; + + if( trap_Argc( ) < 2 ) + { + ADMP( "usage: say_area [message]\n" ); + return; + } + + msg = ConcatArgs( 1 ); + + for(i = 0; i < 3; i++ ) + range[ i ] = g_sayAreaRange.value; + + G_LogPrintf( "SayArea: %d \"%s" S_COLOR_WHITE "\": " S_COLOR_BLUE "%s\n", + ent - g_entities, ent->client->pers.netname, msg ); + + 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_AREA, msg ); + + //Send to ADMF_SPEC_ALLCHAT candidates + for( i = 0; i < level.maxclients; i++ ) + { + if( g_entities[ i ].client->pers.teamSelection == TEAM_NONE && + G_admin_permission( &g_entities[ i ], ADMF_SPEC_ALLCHAT ) ) + { + G_SayTo( ent, &g_entities[ i ], SAY_AREA, msg ); + } + } +} + + +/* +================== +Cmd_Say_f +================== +*/ +static void Cmd_Say_f( gentity_t *ent ) +{ + char *p; + char cmd[ MAX_TOKEN_CHARS ]; + saymode_t mode = SAY_ALL; + + if( trap_Argc( ) < 2 ) + return; + + trap_Argv( 0, cmd, sizeof( cmd ) ); + if( Q_stricmp( cmd, "say_team" ) == 0 ) + mode = SAY_TEAM; + + p = ConcatArgs( 1 ); + + G_Say( ent, mode, p ); +} + +/* +================== +Cmd_VSay_f +================== +*/ +void Cmd_VSay_f( gentity_t *ent ) +{ + char arg[MAX_TOKEN_CHARS]; + char text[ MAX_TOKEN_CHARS ]; + voiceChannel_t vchan; + voice_t *voice; + voiceCmd_t *cmd; + voiceTrack_t *track; + int cmdNum = 0; + int trackNum = 0; + char voiceName[ MAX_VOICE_NAME_LEN ] = {"default"}; + char voiceCmd[ MAX_VOICE_CMD_LEN ] = {""}; + char vsay[ 12 ] = {""}; + weapon_t weapon; + + if( !ent || !ent->client ) + Com_Error( ERR_FATAL, "Cmd_VSay_f() called by non-client entity\n" ); + + trap_Argv( 0, arg, sizeof( arg ) ); + if( trap_Argc( ) < 2 ) + { + trap_SendServerCommand( ent-g_entities, va( + "print \"usage: %s command [text] \n\"", arg ) ); + return; + } + if( !level.voices ) + { + trap_SendServerCommand( ent-g_entities, va( + "print \"%s: voice system is not installed on this server\n\"", arg ) ); + return; + } + if( !g_voiceChats.integer ) + { + trap_SendServerCommand( ent-g_entities, va( + "print \"%s: voice system administratively disabled on this server\n\"", + arg ) ); + return; + } + if( !Q_stricmp( arg, "vsay" ) ) + vchan = VOICE_CHAN_ALL; + else if( !Q_stricmp( arg, "vsay_team" ) ) + vchan = VOICE_CHAN_TEAM; + else if( !Q_stricmp( arg, "vsay_local" ) ) + vchan = VOICE_CHAN_LOCAL; + else + return; + Q_strncpyz( vsay, arg, sizeof( vsay ) ); + + if( ent->client->pers.voice[ 0 ] ) + Q_strncpyz( voiceName, ent->client->pers.voice, sizeof( voiceName ) ); + voice = BG_VoiceByName( level.voices, voiceName ); + if( !voice ) + { + trap_SendServerCommand( ent-g_entities, va( + "print \"%s: voice '%s' not found\n\"", vsay, voiceName ) ); + return; + } + + trap_Argv( 1, voiceCmd, sizeof( voiceCmd ) ) ; + cmd = BG_VoiceCmdFind( voice->cmds, voiceCmd, &cmdNum ); + if( !cmd ) + { + trap_SendServerCommand( ent-g_entities, va( + "print \"%s: command '%s' not found in voice '%s'\n\"", + vsay, voiceCmd, voiceName ) ); + return; + } + + // filter non-spec humans by their primary weapon as well + weapon = WP_NONE; + if( ent->client->sess.spectatorState == SPECTATOR_NOT ) + { + weapon = BG_PrimaryWeapon( ent->client->ps.stats ); + } + + track = BG_VoiceTrackFind( cmd->tracks, ent->client->pers.teamSelection, + ent->client->pers.classSelection, weapon, (int)ent->client->voiceEnthusiasm, + &trackNum ); + if( !track ) + { + trap_SendServerCommand( ent-g_entities, va( + "print \"%s: no available track for command '%s', team %d, " + "class %d, weapon %d, and enthusiasm %d in voice '%s'\n\"", + vsay, voiceCmd, ent->client->pers.teamSelection, + ent->client->pers.classSelection, weapon, + (int)ent->client->voiceEnthusiasm, voiceName ) ); + return; + } + + if( !Q_stricmp( ent->client->lastVoiceCmd, cmd->cmd ) ) + ent->client->voiceEnthusiasm++; + + Q_strncpyz( ent->client->lastVoiceCmd, cmd->cmd, + sizeof( ent->client->lastVoiceCmd ) ); + + // optional user supplied text + trap_Argv( 2, arg, sizeof( arg ) ); + G_CensorString( text, arg, sizeof( text ), ent ); + + switch( vchan ) + { + case VOICE_CHAN_ALL: + case VOICE_CHAN_LOCAL: + trap_SendServerCommand( -1, va( + "voice %d %d %d %d \"%s\"\n", + ent-g_entities, vchan, cmdNum, trackNum, text ) ); + break; + case VOICE_CHAN_TEAM: + G_TeamCommand( ent->client->pers.teamSelection, va( + "voice %d %d %d %d \"%s\"\n", + ent-g_entities, vchan, cmdNum, trackNum, text ) ); + break; + default: + break; + } +} + +/* +================== +Cmd_Where_f +================== +*/ +void Cmd_Where_f( gentity_t *ent ) +{ + if( !ent->client ) + return; + trap_SendServerCommand( ent - g_entities, + va( "print \"origin: %f %f %f\n\"", + ent->s.origin[ 0 ], ent->s.origin[ 1 ], + ent->s.origin[ 2 ] ) ); +} + +/* +================== +Cmd_CallVote_f +================== +*/ +void Cmd_CallVote_f( gentity_t *ent ) +{ + char cmd[ MAX_TOKEN_CHARS ], + vote[ MAX_TOKEN_CHARS ], + arg[ MAX_TOKEN_CHARS ]; + char name[ MAX_NAME_LENGTH ] = ""; + char caller[ MAX_NAME_LENGTH ] = ""; + char reason[ MAX_TOKEN_CHARS ]; + char *creason; + int clientNum = -1; + int id = -1; + team_t team; + + trap_Argv( 0, cmd, sizeof( cmd ) ); + trap_Argv( 1, vote, sizeof( vote ) ); + trap_Argv( 2, arg, sizeof( arg ) ); + creason = ConcatArgs( 3 ); + G_DecolorString( creason, reason, sizeof( reason ) ); + + if( !Q_stricmp( cmd, "callteamvote" ) ) + team = ent->client->pers.teamSelection; + else + team = TEAM_NONE; + + if( !g_allowVote.integer ) + { + trap_SendServerCommand( ent-g_entities, + va( "print \"%s: voting not allowed here\n\"", cmd ) ); + return; + } + + if( level.voteTime[ team ] ) + { + trap_SendServerCommand( ent-g_entities, + va( "print \"%s: a vote is already in progress\n\"", cmd ) ); + return; + } + + if( level.voteExecuteTime[ team ] ) + G_ExecuteVote( team ); + + level.voteDelay[ team ] = 0; + level.voteThreshold[ team ] = 50; + + if( g_voteLimit.integer > 0 && + ent->client->pers.namelog->voteCount >= g_voteLimit.integer && + !G_admin_permission( ent, ADMF_NO_VOTE_LIMIT ) ) + { + trap_SendServerCommand( ent-g_entities, va( + "print \"%s: you have already called the maximum number of votes (%d)\n\"", + cmd, g_voteLimit.integer ) ); + return; + } + + // kick, mute, unmute, denybuild, allowbuild + if( !Q_stricmp( vote, "kick" ) || + !Q_stricmp( vote, "mute" ) || !Q_stricmp( vote, "unmute" ) || + !Q_stricmp( vote, "denybuild" ) || !Q_stricmp( vote, "allowbuild" ) ) + { + char err[ MAX_STRING_CHARS ]; + + if( !arg[ 0 ] ) + { + trap_SendServerCommand( ent-g_entities, + va( "print \"%s: no target\n\"", cmd ) ); + return; + } + + // with a little extra work only players from the right team are considered + clientNum = G_ClientNumberFromString( arg, err, sizeof( err ) ); + + if( clientNum == -1 ) + { + ADMP( va( "%s: %s", cmd, err ) ); + return; + } + + G_DecolorString( level.clients[ clientNum ].pers.netname, name, sizeof( name ) ); + id = level.clients[ clientNum ].pers.namelog->id; + + if( !Q_stricmp( vote, "kick" ) || !Q_stricmp( vote, "mute" ) || + !Q_stricmp( vote, "denybuild" ) ) + { + if( G_admin_permission( g_entities + clientNum, ADMF_IMMUNITY ) ) + { + trap_SendServerCommand( ent-g_entities, + va( "print \"%s: admin is immune\n\"", cmd ) ); + + G_AdminMessage( NULL, va( S_COLOR_WHITE "%s" S_COLOR_YELLOW " attempted %s %s" + " on immune admin " S_COLOR_WHITE "%s" S_COLOR_YELLOW + " for: %s", + ent->client->pers.netname, cmd, vote, + g_entities[ clientNum ].client->pers.netname, + reason[ 0 ] ? reason : "no reason" ) ); + return; + } + + if( team != TEAM_NONE && + ( ent->client->pers.teamSelection != + level.clients[ clientNum ].pers.teamSelection ) ) + { + trap_SendServerCommand( ent-g_entities, + va( "print \"%s: player is not on your team\n\"", cmd ) ); + return; + } + + if( !reason[ 0 ] && !G_admin_permission( ent, ADMF_UNACCOUNTABLE ) ) + { + trap_SendServerCommand( ent-g_entities, + va( "print \"%s: You must provide a reason\n\"", cmd ) ); + return; + } + } + } + + if( !Q_stricmp( vote, "kick" ) ) + { + if( level.clients[ clientNum ].pers.localClient ) + { + trap_SendServerCommand( ent-g_entities, + va( "print \"%s: admin is immune\n\"", cmd ) ); + return; + } + + Com_sprintf( level.voteString[ team ], sizeof( level.voteString[ team ] ), + "ban %s \"1s%s\" vote kick (%s)", level.clients[ clientNum ].pers.ip.str, + g_adminTempBan.string, reason ); + Com_sprintf( level.voteDisplayString[ team ], + sizeof( level.voteDisplayString[ team ] ), "Kick player '%s'", name ); + if( reason[ 0 ] ) + { + Q_strcat( level.voteDisplayString[ team ], + sizeof( level.voteDisplayString[ team ] ), va( " for '%s'", reason ) ); + } + } + else if( team == TEAM_NONE ) + { + if( !Q_stricmp( vote, "mute" ) ) + { + if( level.clients[ clientNum ].pers.namelog->muted ) + { + trap_SendServerCommand( ent-g_entities, + va( "print \"%s: player is already muted\n\"", cmd ) ); + return; + } + + Com_sprintf( level.voteString[ team ], sizeof( level.voteString[ team ] ), + "mute %d", id ); + Com_sprintf( level.voteDisplayString[ team ], + sizeof( level.voteDisplayString[ team ] ), + "Mute player '%s'", name ); + if( reason[ 0 ] ) + { + Q_strcat( level.voteDisplayString[ team ], + sizeof( level.voteDisplayString[ team ] ), va( " for '%s'", reason ) ); + } + } + else if( !Q_stricmp( vote, "unmute" ) ) + { + if( !level.clients[ clientNum ].pers.namelog->muted ) + { + trap_SendServerCommand( ent-g_entities, + va( "print \"%s: player is not currently muted\n\"", cmd ) ); + return; + } + + Com_sprintf( level.voteString[ team ], sizeof( level.voteString[ team ] ), + "unmute %d", id ); + Com_sprintf( level.voteDisplayString[ team ], + sizeof( level.voteDisplayString[ team ] ), + "Unmute player '%s'", name ); + } + else if( !Q_stricmp( vote, "map_restart" ) ) + { + strcpy( level.voteString[ team ], vote ); + strcpy( level.voteDisplayString[ team ], "Restart current map" ); + // map_restart comes with a default delay + } + else if( !Q_stricmp( vote, "map" ) ) + { + if( !G_MapExists( arg ) ) + { + trap_SendServerCommand( ent-g_entities, + va( "print \"%s: 'maps/%s.bsp' could not be found on the server\n\"", + cmd, arg ) ); + return; + } + + Com_sprintf( level.voteString[ team ], sizeof( level.voteString ), + "%s \"%s\"", vote, arg ); + Com_sprintf( level.voteDisplayString[ team ], + sizeof( level.voteDisplayString[ team ] ), + "Change to map '%s'", arg ); + level.voteDelay[ team ] = 3000; + } + else if( !Q_stricmp( vote, "nextmap" ) ) + { + if( G_MapExists( g_nextMap.string ) ) + { + trap_SendServerCommand( ent-g_entities, + va( "print \"%s: the next map is already set to '%s'\n\"", + cmd, g_nextMap.string ) ); + return; + } + + if( !G_MapExists( arg ) ) + { + trap_SendServerCommand( ent-g_entities, + va( "print \"%s: 'maps/%s.bsp' could not be found on the server\n\"", + cmd, arg ) ); + return; + } + + Com_sprintf( level.voteString[ team ], sizeof( level.voteString[ team ] ), + "set g_nextMap \"%s\"", arg ); + Com_sprintf( level.voteDisplayString[ team ], + sizeof( level.voteDisplayString[ team ] ), + "Set the next map to '%s'", arg ); + } + else if( !Q_stricmp( vote, "draw" ) ) + { + strcpy( level.voteString[ team ], "evacuation" ); + strcpy( level.voteDisplayString[ team ], "End match in a draw" ); + level.voteDelay[ team ] = 3000; + } + else if( !Q_stricmp( vote, "sudden_death" ) ) + { + if(!g_suddenDeathVotePercent.integer) + { + trap_SendServerCommand( ent-g_entities, + "print \"Sudden Death votes have been disabled\n\"" ); + return; + } + if( G_TimeTilSuddenDeath( ) <= 0 ) + { + trap_SendServerCommand( ent - g_entities, + va( "print \"callvote: Sudden Death has already begun\n\"") ); + return; + } + if( level.suddenDeathBeginTime > 0 && + G_TimeTilSuddenDeath() <= g_suddenDeathVoteDelay.integer * 1000 ) + { + trap_SendServerCommand( ent - g_entities, + va( "print \"callvote: Sudden Death is imminent\n\"") ); + return; + } + level.voteThreshold[ team ] = g_suddenDeathVotePercent.integer; + Com_sprintf( level.voteString[ team ], sizeof( level.voteString[ team ] ), + "suddendeath %d", g_suddenDeathVoteDelay.integer ); + Com_sprintf( level.voteDisplayString[ team ], + sizeof( level.voteDisplayString[ team ] ), + "Begin sudden death in %d seconds", + g_suddenDeathVoteDelay.integer ); + } + else + { + trap_SendServerCommand( ent-g_entities, "print \"Invalid vote string\n\"" ); + trap_SendServerCommand( ent-g_entities, "print \"Valid vote commands are: " + "map, nextmap, map_restart, draw, sudden_death, kick, mute and unmute\n" ); + return; + } + } + else if( !Q_stricmp( vote, "denybuild" ) ) + { + if( level.clients[ clientNum ].pers.namelog->denyBuild ) + { + trap_SendServerCommand( ent-g_entities, + va( "print \"%s: player already lost building rights\n\"", cmd ) ); + return; + } + + Com_sprintf( level.voteString[ team ], sizeof( level.voteString[ team ] ), + "denybuild %d", id ); + Com_sprintf( level.voteDisplayString[ team ], + sizeof( level.voteDisplayString[ team ] ), + "Take away building rights from '%s'", name ); + if( reason[ 0 ] ) + { + Q_strcat( level.voteDisplayString[ team ], + sizeof( level.voteDisplayString[ team ] ), va( " for '%s'", reason ) ); + } + } + else if( !Q_stricmp( vote, "allowbuild" ) ) + { + if( !level.clients[ clientNum ].pers.namelog->denyBuild ) + { + trap_SendServerCommand( ent-g_entities, + va( "print \"%s: player already has building rights\n\"", cmd ) ); + return; + } + + Com_sprintf( level.voteString[ team ], sizeof( level.voteString[ team ] ), + "allowbuild %d", id ); + Com_sprintf( level.voteDisplayString[ team ], + sizeof( level.voteDisplayString[ team ] ), + "Allow '%s' to build", name ); + } + else if( !Q_stricmp( vote, "admitdefeat" ) ) + { + Com_sprintf( level.voteString[ team ], sizeof( level.voteString[ team ] ), + "admitdefeat %d", team ); + strcpy( level.voteDisplayString[ team ], "Admit Defeat" ); + level.voteDelay[ team ] = 3000; + } + 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 and admitdefeat\n\"" ); + return; + } + + G_LogPrintf( "%s: %d \"%s" S_COLOR_WHITE "\": %s\n", + team == TEAM_NONE ? "CallVote" : "CallTeamVote", + ent - g_entities, ent->client->pers.netname, level.voteString[ team ] ); + + if( team == TEAM_NONE ) + { + trap_SendServerCommand( -1, va( "print \"%s" S_COLOR_WHITE + " called a vote: %s\n\"", ent->client->pers.netname, + level.voteDisplayString[ team ] ) ); + } + else + { + 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 == TEAM_NONE && + G_admin_permission( &g_entities[ i ], ADMF_SPEC_ALLCHAT ) ) ) + { + trap_SendServerCommand( i, va( "print \"%s" S_COLOR_WHITE + " called a team vote: %s\n\"", ent->client->pers.netname, + level.voteDisplayString[ team ] ) ); + } + else if( G_admin_permission( &g_entities[ i ], ADMF_ADMINCHAT ) ) + { + trap_SendServerCommand( i, va( "chat -1 %d \"" S_COLOR_YELLOW "%s" + S_COLOR_YELLOW " called a team vote (%ss): %s\"", SAY_ADMINS, + ent->client->pers.netname, BG_TeamName( team ), + level.voteDisplayString[ team ] ) ); + } + } + } + } + + G_DecolorString( ent->client->pers.netname, caller, sizeof( caller ) ); + + level.voteTime[ team ] = level.time; + trap_SetConfigstring( CS_VOTE_TIME + team, + va( "%d", level.voteTime[ team ] ) ); + trap_SetConfigstring( CS_VOTE_STRING + team, + level.voteDisplayString[ team ] ); + trap_SetConfigstring( CS_VOTE_CALLER + team, + caller ); + + ent->client->pers.namelog->voteCount++; + ent->client->pers.vote |= 1 << team; + G_Vote( ent, team, qtrue ); +} + +/* +================== +Cmd_Vote_f +================== +*/ +void Cmd_Vote_f( gentity_t *ent ) +{ + char cmd[ MAX_TOKEN_CHARS ], vote[ MAX_TOKEN_CHARS ]; + team_t team = ent->client->pers.teamSelection; + + trap_Argv( 0, cmd, sizeof( cmd ) ); + if( Q_stricmp( cmd, "teamvote" ) ) + team = TEAM_NONE; + + if( !level.voteTime[ team ] ) + { + trap_SendServerCommand( ent-g_entities, + va( "print \"%s: no vote in progress\n\"", cmd ) ); + return; + } + + if( ent->client->pers.voted & ( 1 << team ) ) + { + trap_SendServerCommand( ent-g_entities, + va( "print \"%s: vote already cast\n\"", cmd ) ); + return; + } + + trap_SendServerCommand( ent-g_entities, + va( "print \"%s: vote cast\n\"", cmd ) ); + + trap_Argv( 1, vote, sizeof( vote ) ); + if( vote[ 0 ] == 'y' ) + ent->client->pers.vote |= 1 << team; + else + ent->client->pers.vote &= ~( 1 << team ); + G_Vote( ent, team, qtrue ); +} + + +/* +================= +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, "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) + +static qboolean G_RoomForClassChange( gentity_t *ent, class_t class, + vec3_t newOrigin ) +{ + vec3_t fromMins, fromMaxs; + vec3_t toMins, toMaxs; + vec3_t temp; + trace_t tr; + float nudgeHeight; + float maxHorizGrowth; + class_t oldClass = ent->client->ps.stats[ STAT_CLASS ]; + + BG_ClassBoundingBox( oldClass, fromMins, fromMaxs, NULL, NULL, NULL ); + BG_ClassBoundingBox( 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 ] -= toMins[ 2 ] - 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 + return ( !tr.startsolid && tr.fraction == 1.0f ); +} + +/* +================= +Cmd_Class_f +================= +*/ +void Cmd_Class_f( gentity_t *ent ) +{ + char s[ MAX_TOKEN_CHARS ]; + int clientNum; + int i; + vec3_t infestOrigin; + class_t currentClass = ent->client->pers.classSelection; + class_t newClass; + 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; + int oldBoostTime = -1; + vec3_t oldVel; + + clientNum = ent->client - level.clients; + trap_Argv( 1, s, sizeof( s ) ); + newClass = BG_ClassByName( s )->number; + + if( ent->client->sess.spectatorState != SPECTATOR_NOT ) + { + if( ent->client->sess.spectatorState == SPECTATOR_FOLLOW ) + G_StopFollowing( ent ); + if( ent->client->pers.teamSelection == TEAM_ALIENS ) + { + if( newClass != PCL_ALIEN_BUILDER0 && + newClass != PCL_ALIEN_BUILDER0_UPG && + newClass != PCL_ALIEN_LEVEL0 ) + { + G_TriggerMenuArgs( ent->client->ps.clientNum, MN_A_CLASSNOTSPAWN, newClass ); + return; + } + + if( !BG_ClassIsAllowed( newClass ) ) + { + G_TriggerMenuArgs( ent->client->ps.clientNum, MN_A_CLASSNOTALLOWED, newClass ); + return; + } + + if( !BG_ClassAllowedInStage( newClass, g_alienStage.integer ) ) + { + G_TriggerMenuArgs( ent->client->ps.clientNum, MN_A_CLASSNOTATSTAGE, newClass ); + return; + } + + // spawn from an egg + if( G_PushSpawnQueue( &level.alienSpawnQueue, clientNum ) ) + { + ent->client->pers.classSelection = newClass; + ent->client->ps.stats[ STAT_CLASS ] = newClass; + } + } + else if( ent->client->pers.teamSelection == TEAM_HUMANS ) + { + //set the item to spawn with + if( !Q_stricmp( s, BG_Weapon( WP_MACHINEGUN )->name ) && + BG_WeaponIsAllowed( WP_MACHINEGUN ) ) + { + ent->client->pers.humanItemSelection = WP_MACHINEGUN; + } + else if( !Q_stricmp( s, BG_Weapon( WP_HBUILD )->name ) && + BG_WeaponIsAllowed( WP_HBUILD ) ) + { + ent->client->pers.humanItemSelection = WP_HBUILD; + } + else + { + G_TriggerMenu( ent->client->ps.clientNum, MN_H_UNKNOWNSPAWNITEM ); + return; + } + // spawn from a telenode + if( G_PushSpawnQueue( &level.humanSpawnQueue, clientNum ) ) + { + ent->client->pers.classSelection = PCL_HUMAN; + ent->client->ps.stats[ STAT_CLASS ] = PCL_HUMAN; + } + } + return; + } + + if( ent->health <= 0 ) + return; + + if( ent->client->pers.teamSelection == TEAM_ALIENS ) + { + if( newClass == PCL_NONE ) + { + G_TriggerMenu( ent->client->ps.clientNum, MN_A_UNKNOWNCLASS ); + return; + } + + //if we are not currently spectating, we are attempting evolution + if( ent->client->pers.classSelection != PCL_NONE ) + { + int cost; + + //check that we have an overmind + if( !G_Overmind( ) ) + { + G_TriggerMenu( clientNum, MN_A_NOOVMND_EVOLVE ); + 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_TEAM ] == TEAM_HUMANS ) || + ( other->s.eType == ET_BUILDABLE && other->buildableTeam == TEAM_HUMANS && + other->powered ) ) + { + trace_t tr; + + trap_Trace( &tr, ent->client->ps.origin, NULL, NULL, other->s.origin, ent->client->ps.clientNum, MASK_SOLID ); + + if( tr.fraction > 0.99f ) + { + G_TriggerMenu( clientNum, MN_A_TOOCLOSE ); + return; + } + } + } + + //check that we are not wallwalking + if( ent->client->ps.eFlags & EF_WALLCLIMB ) + { + G_TriggerMenu( clientNum, MN_A_EVOLVEWALLWALK ); + return; + } + + //guard against selling the HBUILD weapons exploit + if( ent->client->sess.spectatorState == SPECTATOR_NOT && + ( currentClass == PCL_ALIEN_BUILDER0 || + currentClass == PCL_ALIEN_BUILDER0_UPG ) && + ent->client->buildTimer ) + { + G_TriggerMenu( ent->client->ps.clientNum, MN_A_EVOLVEBUILDTIMER ); + return; + } + + cost = BG_ClassCanEvolveFromTo( currentClass, newClass, + ent->client->pers.credit, + g_alienStage.integer, 0 ); + + if( G_RoomForClassChange( ent, newClass, infestOrigin ) ) + { + if( cost >= 0 ) + { + + ent->client->pers.evolveHealthFraction = (float)ent->client->ps.stats[ STAT_HEALTH ] / + (float)BG_Class( currentClass )->health; + + 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, -cost, qtrue ); + ent->client->pers.classSelection = newClass; + ClientUserinfoChanged( clientNum, qfalse ); + VectorCopy( infestOrigin, ent->s.pos.trBase ); + VectorCopy( ent->client->ps.velocity, oldVel ); + + if( ent->client->ps.stats[ STAT_STATE ] & SS_BOOSTED ) + oldBoostTime = ent->client->boostedTime; + + ClientSpawn( ent, ent, ent->s.pos.trBase, ent->s.apos.trBase ); + + VectorCopy( oldVel, ent->client->ps.velocity ); + if( oldBoostTime > 0 ) + { + ent->client->boostedTime = oldBoostTime; + ent->client->ps.stats[ STAT_STATE ] |= SS_BOOSTED; + } + } + else + G_TriggerMenuArgs( clientNum, MN_A_CANTEVOLVE, newClass ); + } + else + G_TriggerMenu( clientNum, MN_A_NOEROOM ); + } + } + else if( ent->client->pers.teamSelection == TEAM_HUMANS ) + G_TriggerMenu( clientNum, MN_H_DEADTOCLASS ); +} + + +/* +================= +Cmd_Destroy_f +================= +*/ +void Cmd_Destroy_f( gentity_t *ent ) +{ + vec3_t viewOrigin, forward, end; + trace_t tr; + gentity_t *traceEnt; + char cmd[ 12 ]; + qboolean deconstruct = qtrue; + qboolean lastSpawn = qfalse; + + if( ent->client->pers.namelog->denyBuild ) + { + G_TriggerMenu( ent->client->ps.clientNum, MN_B_REVOKED ); + return; + } + + trap_Argv( 0, cmd, sizeof( cmd ) ); + if( Q_stricmp( cmd, "destroy" ) == 0 ) + deconstruct = qfalse; + + BG_GetClientViewOrigin( &ent->client->ps, viewOrigin ); + AngleVectors( ent->client->ps.viewangles, forward, NULL, NULL ); + VectorMA( viewOrigin, 100, forward, end ); + + trap_Trace( &tr, viewOrigin, NULL, NULL, end, ent->s.number, MASK_PLAYERSOLID ); + traceEnt = &g_entities[ tr.entityNum ]; + + if( tr.fraction < 1.0f && + ( traceEnt->s.eType == ET_BUILDABLE ) && + ( traceEnt->buildableTeam == ent->client->pers.teamSelection ) && + ( ( ent->client->ps.weapon >= WP_ABUILD ) && + ( ent->client->ps.weapon <= WP_HBUILD ) ) ) + { + // Always let the builder prevent the explosion + if( traceEnt->health <= 0 ) + { + G_QueueBuildPoints( traceEnt ); + G_RewardAttackers( traceEnt ); + G_FreeEntity( traceEnt ); + return; + } + + // Cancel deconstruction (unmark) + if( deconstruct && g_markDeconstruct.integer && traceEnt->deconstruct ) + { + traceEnt->deconstruct = qfalse; + return; + } + + // Prevent destruction of the last spawn + if( ent->client->pers.teamSelection == TEAM_ALIENS && + traceEnt->s.modelindex == BA_A_SPAWN ) + { + if( level.numAlienSpawns <= 1 ) + lastSpawn = qtrue; + } + else if( ent->client->pers.teamSelection == TEAM_HUMANS && + traceEnt->s.modelindex == BA_H_SPAWN ) + { + if( level.numHumanSpawns <= 1 ) + lastSpawn = qtrue; + } + + if( lastSpawn && !g_cheats.integer && + !g_markDeconstruct.integer ) + { + G_TriggerMenu( ent->client->ps.clientNum, MN_B_LASTSPAWN ); + return; + } + + // Don't allow destruction of buildables that cannot be rebuilt + if( G_TimeTilSuddenDeath( ) <= 0 ) + { + G_TriggerMenu( ent->client->ps.clientNum, MN_B_SUDDENDEATH ); + return; + } + + if( !g_markDeconstruct.integer || + ( ent->client->pers.teamSelection == TEAM_HUMANS && + !G_FindPower( traceEnt, qtrue ) ) ) + { + if( ent->client->buildTimer ) + { + G_AddEvent( ent, EV_BUILD_DELAY, ent->client->ps.clientNum ); + return; + } + } + + if( traceEnt->health > 0 ) + { + if( !deconstruct ) + { + G_Damage( traceEnt, ent, ent, forward, tr.endpos, + traceEnt->health, 0, MOD_SUICIDE ); + } + else if( g_markDeconstruct.integer && + ( ent->client->pers.teamSelection != TEAM_HUMANS || + G_FindPower( traceEnt , qtrue ) || lastSpawn ) ) + { + traceEnt->deconstruct = qtrue; // Mark buildable for deconstruction + traceEnt->deconstructTime = level.time; + } + else + { + if( !g_cheats.integer && !g_instantBuild.integer ) // add a bit to the build timer + { + ent->client->buildTimer += + BG_Buildable( traceEnt->s.modelindex, traceEnt->cuboidSize )->buildTime / 4; + G_RecalcBuildTimer(ent->client); + } + G_Damage( traceEnt, ent, ent, forward, tr.endpos, + traceEnt->health, 0, MOD_DECONSTRUCT ); + G_FreeEntity( traceEnt ); + } + } + } +} + +/* +================= +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 ) ); + + // "weapon" aliased to whatever weapon you have + if( !Q_stricmp( "weapon", s ) ) + { + if( ent->client->ps.weapon == WP_BLASTER && + BG_PlayerCanChangeWeapon( &ent->client->ps ) ) + G_ForceWeaponChange( ent, WP_NONE ); + return; + } + + upgrade = BG_UpgradeByName( s )->number; + weapon = BG_WeaponByName( s )->number; + + 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 ) ) + { + if( ent->client->ps.weapon != weapon && + BG_PlayerCanChangeWeapon( &ent->client->ps ) ) + 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 ]; + upgrade_t upgrade; + + trap_Argv( 1, s, sizeof( s ) ); + upgrade = BG_UpgradeByName( s )->number; + + if( BG_InventoryContainsUpgrade( upgrade, ent->client->ps.stats ) ) + { + BG_DeactivateUpgrade( upgrade, ent->client->ps.stats ); + if( upgrade == UP_JETPACK ) + BG_AddPredictableEventToPlayerstate( EV_JETPACK_DEACTIVATE, 0, &ent->client->ps ); + } + 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 ]; + weapon_t weapon; + upgrade_t upgrade; + + trap_Argv( 1, s, sizeof( s ) ); + upgrade = BG_UpgradeByName( s )->number; + weapon = BG_WeaponByName( s )->number; + + if( weapon != WP_NONE ) + { + if( !BG_PlayerCanChangeWeapon( &ent->client->ps ) ) + return; + + //special case to allow switching between + //the blaster and the primary weapon + if( ent->client->ps.weapon != WP_BLASTER ) + weapon = WP_BLASTER; + else + weapon = WP_NONE; + + 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 ); + if( upgrade == UP_JETPACK ) + BG_AddPredictableEventToPlayerstate( EV_JETPACK_DEACTIVATE, 0, &ent->client->ps ); + } + 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 ]; + weapon_t weapon; + upgrade_t upgrade; + qboolean energyOnly, sellHelmet = qfalse; + + trap_Argv( 1, s, sizeof( s ) ); + + weapon = BG_WeaponByName( s )->number; + upgrade = BG_UpgradeByName( s )->number; + + if( upgrade == UP_NONE && !Q_stricmp(s, "helmet") ) + { + if( g_humanStage.integer < S2 ) + upgrade = UP_HELMET_MK1; + else + upgrade = UP_HELMET_MK2; + sellHelmet = qtrue; + } + + // Only give energy from reactors or repeaters + if( G_BuildableRange( ent->client->ps.origin, 100, BA_H_ARMOURY ) ) + energyOnly = qfalse; + else if( upgrade == UP_AMMO && + BG_Weapon( ent->client->ps.stats[ STAT_WEAPON ] )->usesEnergy && + ( G_BuildableRange( ent->client->ps.origin, 100, BA_H_REACTOR ) || + G_BuildableRange( ent->client->ps.origin, 100, BA_H_REPEATER ) ) ) + energyOnly = qtrue; + else + { + if( upgrade == UP_AMMO && + BG_Weapon( ent->client->ps.weapon )->usesEnergy ) + G_TriggerMenu( ent->client->ps.clientNum, MN_H_NOENERGYAMMOHERE ); + else + G_TriggerMenu( ent->client->ps.clientNum, MN_H_NOARMOURYHERE ); + 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; + } + + // Only humans can buy stuff + if( BG_Weapon( weapon )->team != TEAM_HUMANS ) + { + trap_SendServerCommand( ent-g_entities, "print \"You can't buy alien items\n\"" ); + return; + } + + //are we /allowed/ to buy this? + if( !BG_Weapon( weapon )->purchasable ) + { + trap_SendServerCommand( ent-g_entities, "print \"You can't buy this item\n\"" ); + return; + } + + //are we /allowed/ to buy this? + if( !BG_WeaponAllowedInStage( weapon, g_humanStage.integer ) || !BG_WeaponIsAllowed( weapon ) ) + { + trap_SendServerCommand( ent-g_entities, "print \"You can't buy this item\n\"" ); + return; + } + + //can afford this? + if( BG_Weapon( weapon )->price > (short)ent->client->pers.credit ) + { + G_TriggerMenu( ent->client->ps.clientNum, MN_H_NOFUNDS ); + return; + } + + //have space to carry this? + if( BG_Weapon( weapon )->slots & BG_SlotsForInventory( ent->client->ps.stats ) ) + { + G_TriggerMenu( ent->client->ps.clientNum, MN_H_NOSLOTS ); + return; + } + + // In some instances, weapons can't be changed + if( !BG_PlayerCanChangeWeapon( &ent->client->ps ) ) + return; + + ent->client->ps.stats[ STAT_WEAPON ] = weapon; + ent->client->ps.ammo = BG_Weapon( weapon )->maxAmmo; + ent->client->ps.clips = BG_Weapon( weapon )->maxClips; + + if( BG_Weapon( weapon )->usesEnergy && + BG_InventoryContainsUpgrade( UP_BATTPACK, ent->client->ps.stats ) ) + ent->client->ps.ammo *= BATTPACK_MODIFIER; + + G_ForceWeaponChange( ent, weapon ); + + //set build delay/pounce etc to 0 + ent->client->ps.stats[ STAT_MISC ] = 0; + ent->client->buildTimer = 0; + + //subtract from funds + G_AddCreditToClient( ent->client, -(short)BG_Weapon( weapon )->price, qfalse ); + } + else if( upgrade != UP_NONE ) + { + //already got this? + if( !sellHelmet && BG_InventoryContainsUpgrade( upgrade, ent->client->ps.stats ) ) + { + G_TriggerMenu( ent->client->ps.clientNum, MN_H_ITEMHELD ); + return; + } + + //can afford this? + if( BG_Upgrade( upgrade )->price > (short)ent->client->pers.credit ) + { + G_TriggerMenu( ent->client->ps.clientNum, MN_H_NOFUNDS ); + return; + } + + //have space to carry this? + if( !sellHelmet && BG_Upgrade( upgrade )->slots & BG_SlotsForInventory( ent->client->ps.stats ) ) + { + G_TriggerMenu( ent->client->ps.clientNum, MN_H_NOSLOTS ); + return; + } + + // Only humans can buy stuff + if( BG_Upgrade( upgrade )->team != TEAM_HUMANS ) + { + trap_SendServerCommand( ent-g_entities, "print \"You can't buy alien items\n\"" ); + return; + } + + //are we /allowed/ to buy this? + if( !BG_Upgrade( upgrade )->purchasable ) + { + trap_SendServerCommand( ent-g_entities, "print \"You can't buy this item\n\"" ); + return; + } + + //are we /allowed/ to buy this? + if( !BG_UpgradeAllowedInStage( upgrade, g_humanStage.integer ) || !BG_UpgradeIsAllowed( upgrade ) ) + { + trap_SendServerCommand( ent-g_entities, "print \"You can't buy this item\n\"" ); + return; + } + + if( upgrade == UP_AMMO ) + { + G_GiveClientMaxAmmo( ent, energyOnly ); + if( !energyOnly && BG_InventoryContainsUpgrade( UP_JETPACK, ent->client->ps.stats ) && + ent->client->ps.stats[ STAT_FUEL ] < JETPACK_FUEL_FULL ) + { + G_AddEvent( ent, EV_JETPACK_REFUEL, 0) ; + ent->client->ps.stats[ STAT_FUEL ] = JETPACK_FUEL_FULL; + } + } + else + { + if( upgrade == UP_BATTLESUIT ) + { + vec3_t newOrigin; + + if( !G_RoomForClassChange( ent, PCL_HUMAN_BSUIT, newOrigin ) ) + { + G_TriggerMenu( ent->client->ps.clientNum, MN_H_NOROOMBSUITON ); + return; + } + VectorCopy( newOrigin, ent->client->ps.origin ); + ent->client->ps.stats[ STAT_CLASS ] = PCL_HUMAN_BSUIT; + ent->client->pers.classSelection = PCL_HUMAN_BSUIT; + ent->client->ps.eFlags ^= EF_TELEPORT_BIT; + } + else if( upgrade == UP_JETPACK ) + ent->client->ps.stats[ STAT_FUEL ] = JETPACK_FUEL_FULL; + + if( sellHelmet ) + { + BG_RemoveUpgradeFromInventory( UP_HELMET_MK1, ent->client->ps.stats ); + BG_RemoveUpgradeFromInventory( UP_HELMET_MK2, ent->client->ps.stats ); + } + + //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_Upgrade( upgrade )->price, qfalse ); + } + else + G_TriggerMenu( ent->client->ps.clientNum, MN_H_UNKNOWNITEM ); + + //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; + weapon_t weapon; + upgrade_t upgrade; + + trap_Argv( 1, s, sizeof( s ) ); + + //no armoury nearby + if( !G_BuildableRange( ent->client->ps.origin, 100, BA_H_ARMOURY ) ) + { + G_TriggerMenu( ent->client->ps.clientNum, MN_H_NOARMOURYHERE ); + return; + } + + if( !Q_stricmpn( s, "weapon", 6 ) ) + weapon = ent->client->ps.stats[ STAT_WEAPON ]; + else + weapon = BG_WeaponByName( s )->number; + + upgrade = BG_UpgradeByName( s )->number; + + if( !upgrade && !Q_stricmp( s, "helmet" ) ) + { + if( BG_InventoryContainsUpgrade( UP_HELMET_MK1, ent->client->ps.stats ) ) + upgrade = UP_HELMET_MK1; + else if( BG_InventoryContainsUpgrade( UP_HELMET_MK2, ent->client->ps.stats ) ) + upgrade = UP_HELMET_MK2; + } + + if( weapon != WP_NONE ) + { + weapon_t selected = BG_GetPlayerWeapon( &ent->client->ps ); + + if( !BG_PlayerCanChangeWeapon( &ent->client->ps ) ) + return; + + //are we /allowed/ to sell this? + if( !BG_Weapon( weapon )->purchasable ) + { + trap_SendServerCommand( ent-g_entities, "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 && ent->client->buildTimer ) + { + G_TriggerMenu( ent->client->ps.clientNum, MN_H_ARMOURYBUILDTIMER ); + return; + } + + ent->client->ps.stats[ STAT_WEAPON ] = WP_NONE; + // Cancel ghost buildables + ent->client->ps.stats[ STAT_BUILDABLE ] = BA_NONE; + + //add to funds + G_AddCreditToClient( ent->client, (short)BG_Weapon( weapon )->price, qfalse ); + } + + //if we have this weapon selected, force a new selection + if( weapon == selected ) + G_ForceWeaponChange( ent, WP_NONE ); + } + else if( upgrade != UP_NONE ) + { + //are we /allowed/ to sell this? + if( !BG_Upgrade( upgrade )->purchasable ) + { + trap_SendServerCommand( ent-g_entities, "print \"You can't sell this item\n\"" ); + return; + } + //remove upgrade if carried + if( BG_InventoryContainsUpgrade( upgrade, ent->client->ps.stats ) ) + { + // shouldn't really need to test for this, but just to be safe + if( upgrade == UP_BATTLESUIT ) + { + vec3_t newOrigin; + + if( !G_RoomForClassChange( ent, PCL_HUMAN, newOrigin ) ) + { + G_TriggerMenu( ent->client->ps.clientNum, MN_H_NOROOMBSUITOFF ); + return; + } + VectorCopy( newOrigin, ent->client->ps.origin ); + ent->client->ps.stats[ STAT_CLASS ] = PCL_HUMAN; + ent->client->pers.classSelection = PCL_HUMAN; + ent->client->ps.eFlags ^= EF_TELEPORT_BIT; + } + + //add to inventory + BG_RemoveUpgradeFromInventory( upgrade, ent->client->ps.stats ); + + if( upgrade == UP_BATTPACK ) + G_GiveClientMaxAmmo( ent, qtrue ); + + //add to funds + G_AddCreditToClient( ent->client, (short)BG_Upgrade( upgrade )->price, qfalse ); + } + } + else if( !Q_stricmp( s, "weapons" ) ) + { + weapon_t selected = BG_GetPlayerWeapon( &ent->client->ps ); + + if( !BG_PlayerCanChangeWeapon( &ent->client->ps ) ) + return; + + for( i = WP_NONE + 1; i < WP_NUM_WEAPONS; i++ ) + { + //guard against selling the HBUILD weapons exploit + if( i == WP_HBUILD && ent->client->buildTimer ) + { + G_TriggerMenu( ent->client->ps.clientNum, MN_H_ARMOURYBUILDTIMER ); + continue; + } + + if( BG_InventoryContainsWeapon( i, ent->client->ps.stats ) && + BG_Weapon( i )->purchasable ) + { + ent->client->ps.stats[ STAT_WEAPON ] = WP_NONE; + + //add to funds + G_AddCreditToClient( ent->client, (short)BG_Weapon( i )->price, qfalse ); + } + + //if we have this weapon selected, force a new selection + if( i == selected ) + 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_Upgrade( i )->purchasable ) + { + + // shouldn't really need to test for this, but just to be safe + if( i == UP_BATTLESUIT ) + { + vec3_t newOrigin; + + if( !G_RoomForClassChange( ent, PCL_HUMAN, newOrigin ) ) + { + G_TriggerMenu( ent->client->ps.clientNum, MN_H_NOROOMBSUITOFF ); + continue; + } + VectorCopy( newOrigin, ent->client->ps.origin ); + ent->client->ps.stats[ STAT_CLASS ] = PCL_HUMAN; + ent->client->pers.classSelection = PCL_HUMAN; + ent->client->ps.eFlags ^= EF_TELEPORT_BIT; + } + + BG_RemoveUpgradeFromInventory( i, ent->client->ps.stats ); + + if( i == UP_BATTPACK ) + G_GiveClientMaxAmmo( ent, qtrue ); + + //add to funds + G_AddCreditToClient( ent->client, (short)BG_Upgrade( i )->price, qfalse ); + } + } + } + else + G_TriggerMenu( ent->client->ps.clientNum, MN_H_UNKNOWNITEM ); + + //update ClientInfo + ClientUserinfoChanged( ent->client->ps.clientNum, qfalse ); +} + +/* +================= +Cmd_CheckCuboidSize + +Check if the specified dimensions are valid. +================= +*/ +qboolean Cmd_CheckCuboidSize(vec3_t dims) +{ + if(g_cuboidSizeLimit.integer) + if(dims[0]>g_cuboidSizeLimit.integer||dims[1]>g_cuboidSizeLimit.integer||dims[2]>g_cuboidSizeLimit.integer) + return qfalse; + if(dims[0]*dims[1]*dims[2]<CUBOID_MINVOLUME) + return qfalse; + if(dims[0]<1||dims[1]<1||dims[2]<1) + return qfalse; + return qtrue; +} + +/* +================= +Cmd_Cb_f + +Sent by cgame in background (not directly by a person). +Update player's cuboid selection (after validation) with data sent over network. +================= +*/ +void Cmd_Cb_f(gentity_t *ent) +{ + char s[MAX_TOKEN_CHARS]; + int echo; + vec3_t dims; + + if(trap_Argc()!=5) + return; + trap_Argv(1,s,sizeof(s)); + echo=atoi(s); + trap_Argv(2,s,sizeof(s)); + dims[0]=atof(s); + trap_Argv(3,s,sizeof(s)); + dims[1]=atof(s); + trap_Argv(4,s,sizeof(s)); + dims[2]=atof(s); + if(Cmd_CheckCuboidSize(dims)) + { + VectorCopy(dims,ent->client->cuboidSelection); + trap_SendServerCommand(ent->client-level.clients,va("cb3 %i\n",echo)); + G_RelayCuboidToSpectators(ent->client); + } + else + { + if(Cmd_CheckCuboidSize(ent->client->cuboidSelection)) + trap_SendServerCommand(ent->client-level.clients,va("cb3 %i %f %f %f\n",echo,ent->client->cuboidSelection[0],ent->client->cuboidSelection[1],ent->client->cuboidSelection[2])); + else + trap_SendServerCommand(ent->client-level.clients,va("cb3 %i %f %f %f\n",echo,g_cuboidSizeLimit.integer,g_cuboidSizeLimit.integer,g_cuboidSizeLimit.integer)); + } +} + + +/* +================= +Cmd_Build_f +================= +*/ +void Cmd_Build_f( gentity_t *ent ) +{ + char s[ MAX_TOKEN_CHARS ]; + buildable_t buildable; + float dist; + vec3_t origin, normal; + team_t team; + char buf[128]; + vec3_t dims; + + if( ent->client->pers.namelog->denyBuild ) + { + G_TriggerMenu( ent->client->ps.clientNum, MN_B_REVOKED ); + return; + } + + if( ent->client->pers.teamSelection == level.surrenderTeam ) + { + G_TriggerMenu( ent->client->ps.clientNum, MN_B_SURRENDER ); + return; + } + + trap_Argv( 1, s, sizeof( s ) ); + + buildable = BG_BuildableByName( s )->number; + + /* To allow players build cuboids with completely arbitrary + * dimensions (the current resizing method doesn't provide + * much precision) the build command was extended with the + * following syntax: + * build [building] [X] [Y] [Z] + * where X, Y and Z are respectively the cuboid's length + * on the X, Y and Z axis. + */ + if( BG_Buildable(buildable,NULL)->cuboid ) + { + if( trap_Argc() >= 5 ) + { + trap_Argv(2,s,sizeof(s)); + dims[0]=MAX(1,atof(s)); + trap_Argv(3,s,sizeof(s)); + dims[1]=MAX(1,atof(s)); + trap_Argv(4,s,sizeof(s)); + dims[2]=MAX(1,atof(s)); + if(!Cmd_CheckCuboidSize(dims)) + { + Com_sprintf(buf,sizeof(buf),"print \"^1error: invalid cuboid size (min volume: %i, max size: %s)\n\"", + CUBOID_MINVOLUME,(g_cuboidSizeLimit.integer?va("%ix%ix%i",g_cuboidSizeLimit.integer,g_cuboidSizeLimit.integer, g_cuboidSizeLimit.integer):"no limit")); + trap_SendServerCommand(ent->client-level.clients,buf); + return; + } + VectorCopy(dims,ent->client->cuboidSelection); + } + // client is building a cuboid for the first time so reset the selection to default + if(!Cmd_CheckCuboidSize(ent->client->cuboidSelection)) + { + ent->client->cuboidSelection[0]=32; + ent->client->cuboidSelection[1]=32; + ent->client->cuboidSelection[2]=32; + trap_SendServerCommand(ent->client-level.clients,"cb2 32 32 32"); + G_RelayCuboidToSpectators(ent->client); + } + + if(!BG_CuboidAllowed((team==TEAM_ALIENS?g_alienStage.integer:g_humanStage.integer))) + { + if(BG_CuboidMode()==1) + G_TriggerMenu(ent->client->ps.clientNum,MN_B_CUBOID_MODE1); + else + G_TriggerMenu(ent->client->ps.clientNum,MN_B_CUBOID_MODE2); + return; + } + } + + if( G_TimeTilSuddenDeath( ) <= 0 ) + { + G_TriggerMenu( ent->client->ps.clientNum, MN_B_SUDDENDEATH ); + return; + } + + team = ent->client->ps.stats[ STAT_TEAM ]; + + if( buildable != BA_NONE && + ( ( 1 << ent->client->ps.weapon ) & BG_Buildable( buildable, NULL )->buildWeapon ) && + BG_BuildableIsAllowed( buildable ) && + ( ( team == TEAM_ALIENS && BG_BuildableAllowedInStage( buildable, g_alienStage.integer ) ) || + ( team == TEAM_HUMANS && BG_BuildableAllowedInStage( buildable, g_humanStage.integer ) ) ) ) + { + dynMenu_t err; + dist = BG_Class( ent->client->ps.stats[ STAT_CLASS ] )->buildDist; + + ent->client->ps.stats[ STAT_BUILDABLE ] = BA_NONE; + + //these are the errors displayed when the builder first selects something to use + switch( G_CanBuild( ent, buildable, dist, origin, normal, ent->client->cuboidSelection ) ) + { + // can place right away, set the blueprint and the valid togglebit + case IBE_NONE: + case IBE_TNODEWARN: + case IBE_RPTNOREAC: + case IBE_RPTPOWERHERE: + case IBE_SPWNWARN: + err = MN_NONE; + // we OR-in the selected builable later + ent->client->ps.stats[ STAT_BUILDABLE ] = SB_VALID_TOGGLEBIT; + break; + + // can't place yet but maybe soon: start with valid togglebit off + case IBE_NORMAL: + case IBE_NOCREEP: + case IBE_NOROOM: + case IBE_NOOVERMIND: + case IBE_NOPOWERHERE: + case IBE_NOSURF: + err = MN_NONE; + break; + + // more serious errors just pop a menu + case IBE_NOALIENBP: + err = MN_A_NOBP; + break; + + case IBE_ONEOVERMIND: + err = MN_A_ONEOVERMIND; + break; + + case IBE_ONEREACTOR: + err = MN_H_ONEREACTOR; + break; + + case IBE_NOHUMANBP: + err = MN_H_NOBP; + break; + + case IBE_NODCC: + err = MN_H_NODCC; + break; + + case IBE_PERMISSION: + err = MN_B_CANNOT; + break; + + case IBE_LASTSPAWN: + err = MN_B_LASTSPAWN; + break; + + case IBE_TOODENSE: + err = MN_B_TOODENSE; + break; + + default: + err = -1; // stop uninitialised warning + break; + } + + if( ( err == MN_A_NOBP || err == MN_H_NOBP ) && BG_Buildable(buildable,NULL)->cuboid ) + { + err = MN_NONE; + ent->client->ps.stats[ STAT_BUILDABLE ] = SB_VALID_TOGGLEBIT; + } + + if( err == MN_NONE || ent->client->pers.disableBlueprintErrors ) + { + trap_SendServerCommand(ent->client-level.clients,va("cb2 %f %f %f\n", + ent->client->cuboidSelection[0], + ent->client->cuboidSelection[1], + ent->client->cuboidSelection[2])); + G_RelayCuboidToSpectators(ent->client); + ent->client->ps.stats[ STAT_BUILDABLE ] |= buildable; + } + else + G_TriggerMenu( ent->client->ps.clientNum, err ); + } + else + G_TriggerMenu( ent->client->ps.clientNum, MN_B_CANNOT ); +} + +/* +================= +Cmd_Reload_f +================= +*/ +void Cmd_Reload_f( gentity_t *ent ) +{ + playerState_t *ps = &ent->client->ps; + int ammo; + + // weapon doesn't ever need reloading + if( BG_Weapon( ps->weapon )->infiniteAmmo ) + return; + + if( ps->clips <= 0 ) + return; + + if( BG_Weapon( ps->weapon )->usesEnergy && + BG_InventoryContainsUpgrade( UP_BATTPACK, ps->stats ) ) + ammo = BG_Weapon( ps->weapon )->maxAmmo * BATTPACK_MODIFIER; + else + ammo = BG_Weapon( ps->weapon )->maxAmmo; + + // don't reload when full + if( ps->ammo >= ammo ) + return; + + // the animation, ammo refilling etc. is handled by PM_Weapon + if( ent->client->ps.weaponstate != WEAPON_RELOADING ) + ent->client->ps.pm_flags |= PMF_WEAPON_RELOAD; +} + +/* +================= +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->client->ps.clientNum ) + { + 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.stats[ STAT_TEAM ] = ent->client->pers.teamSelection; + + if( ent->client->pers.teamSelection == TEAM_NONE ) + { + ent->client->sess.spectatorState = + ent->client->ps.persistant[ PERS_SPECSTATE ] = SPECTATOR_FREE; + } + else + { + vec3_t spawn_origin, spawn_angles; + + ent->client->sess.spectatorState = + ent->client->ps.persistant[ PERS_SPECSTATE ] = SPECTATOR_LOCKED; + + if( ent->client->pers.teamSelection == TEAM_ALIENS ) + G_SelectAlienLockSpawnPoint( spawn_origin, spawn_angles ); + else if( ent->client->pers.teamSelection == TEAM_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; + ent->client->ps.groundEntityNum = ENTITYNUM_NONE; + ent->client->ps.stats[ STAT_STATE ] = 0; + ent->client->ps.stats[ STAT_VIEWLOCK ] = 0; + ent->client->ps.eFlags &= ~( EF_WALLCLIMB | EF_WALLCLIMBCEILING ); + ent->client->ps.viewangles[ PITCH ] = 0.0f; + ent->client->ps.clientNum = ent - g_entities; + ent->client->ps.persistant[ PERS_CREDIT ] = ent->client->pers.credit; + + CalculateRanks( ); +} + +/* +================= +G_FollowLockView + +Client is still following a player, but that player has gone to spectator +mode and cannot be followed for the moment +================= +*/ +void G_FollowLockView( gentity_t *ent ) +{ + vec3_t spawn_origin, spawn_angles; + int clientNum; + + clientNum = ent->client->sess.spectatorClient; + ent->client->sess.spectatorState = + ent->client->ps.persistant[ PERS_SPECSTATE ] = SPECTATOR_FOLLOW; + ent->client->ps.clientNum = clientNum; + ent->client->ps.pm_flags &= ~PMF_FOLLOW; + ent->client->ps.stats[ STAT_TEAM ] = ent->client->pers.teamSelection; + ent->client->ps.stats[ STAT_STATE ] &= ~SS_WALLCLIMBING; + ent->client->ps.stats[ STAT_VIEWLOCK ] = 0; + ent->client->ps.eFlags &= ~( EF_WALLCLIMB | EF_WALLCLIMBCEILING ); + ent->client->ps.eFlags ^= EF_TELEPORT_BIT; + ent->client->ps.viewangles[ PITCH ] = 0.0f; + + // Put the view at the team spectator lock position + if( level.clients[ clientNum ].pers.teamSelection == TEAM_ALIENS ) + G_SelectAlienLockSpawnPoint( spawn_origin, spawn_angles ); + else if( level.clients[ clientNum ].pers.teamSelection == TEAM_HUMANS ) + G_SelectHumanLockSpawnPoint( spawn_origin, spawn_angles ); + + G_SetOrigin( ent, spawn_origin ); + VectorCopy( spawn_origin, ent->client->ps.origin ); + G_SetClientViewAngle( ent, spawn_angles ); +} + +/* +================= +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.spectatorState == SPECTATOR_NOT ) + 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; + + // can't follow self + if( &g_entities[ clientnum ] == ent ) + continue; + + // avoid selecting existing follow target + if( clientnum == original && !selectAny ) + continue; //effectively break; + + // can only follow connected clients + if( level.clients[ clientnum ].pers.connected != CON_CONNECTED ) + continue; + + // can't follow a spectator + if( level.clients[ clientnum ].pers.teamSelection == TEAM_NONE ) + continue; + + // if stickyspec is disabled, can't follow someone in queue either + if( !ent->client->pers.stickySpec && + level.clients[ clientnum ].sess.spectatorState != SPECTATOR_NOT ) + continue; + + // can only follow teammates when dead and on a team + if( ent->client->pers.teamSelection != TEAM_NONE && + ( level.clients[ clientnum ].pers.teamSelection != + ent->client->pers.teamSelection ) ) + continue; + + // this is good, we can use it + ent->client->sess.spectatorClient = clientnum; + ent->client->sess.spectatorState = SPECTATOR_FOLLOW; + + // if this client is in the spawn queue, we need to do something special + if( level.clients[ clientnum ].sess.spectatorState != SPECTATOR_NOT ) + G_FollowLockView( ent ); + + return qtrue; + + } while( clientnum != original ); + + return qfalse; +} + +/* +================= +G_ToggleFollow +================= +*/ +void G_ToggleFollow( gentity_t *ent ) +{ + 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; + char arg[ MAX_NAME_LENGTH ]; + + // won't work unless spectating + if( ent->client->sess.spectatorState == SPECTATOR_NOT ) + return; + + if( trap_Argc( ) != 2 ) + { + G_ToggleFollow( ent ); + } + else + { + char err[ MAX_STRING_CHARS ]; + trap_Argv( 1, arg, sizeof( arg ) ); + + i = G_ClientNumberFromString( arg, err, sizeof( err ) ); + + if( i == -1 ) + { + trap_SendServerCommand( ent - g_entities, + va( "print \"follow: %s\"", err ) ); + return; + } + + // can't follow self + if( &level.clients[ i ] == ent->client ) + return; + + // can't follow another spectator if sticky spec is off + if( !ent->client->pers.stickySpec && + level.clients[ i ].sess.spectatorState != SPECTATOR_NOT ) + return; + + // if not on team spectator, you can only follow teammates + if( ent->client->pers.teamSelection != TEAM_NONE && + ( level.clients[ i ].pers.teamSelection != + ent->client->pers.teamSelection ) ) + 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.spectatorState == SPECTATOR_NOT ) + return; + + G_FollowNewClient( ent, dir ); +} + +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]" + "usage: %s [clientNum | partial name match]\n\"", cmd ) ); + return; + } + + Q_strncpyz( name, ConcatArgs( 1 ), sizeof( name ) ); + matches = G_ClientNumbersFromString( name, pids, MAX_CLIENTS ); + 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( !Com_ClientListContains( &ent->client->sess.ignoreList, pids[ i ] ) ) + { + Com_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( Com_ClientListContains( &ent->client->sess.ignoreList, pids[ i ] ) ) + { + Com_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_ListMaps_f + +List all maps on the server +================= +*/ + +static int SortMaps( const void *a, const void *b ) +{ + return strcmp( *(char **)a, *(char **)b ); +} + +#define MAX_MAPLIST_MAPS 256 +#define MAX_MAPLIST_ROWS 9 +void Cmd_ListMaps_f( gentity_t *ent ) +{ + char search[ 16 ] = {""}; + char fileList[ 4096 ] = {""}; + char *fileSort[ MAX_MAPLIST_MAPS ]; + char *filePtr, *p; + int numFiles; + int fileLen = 0; + int shown = 0; + int count = 0; + int page = 0; + int pages; + int row, rows; + int start, i, j; + + if( trap_Argc( ) > 1 ) + { + trap_Argv( 1, search, sizeof( search ) ); + for( p = search; ( *p ) && isdigit( *p ); p++ ); + if( !( *p ) ) + { + page = atoi( search ); + search[ 0 ] = '\0'; + } + else if( trap_Argc( ) > 2 ) + { + char lp[ 8 ]; + trap_Argv( 2, lp, sizeof( lp ) ); + page = atoi( lp ); + } + + if( page > 0 ) + page--; + else if( page < 0 ) + page = 0; + } + + numFiles = trap_FS_GetFileList( "maps/", ".bsp", + fileList, sizeof( fileList ) ); + filePtr = fileList; + for( i = 0; i < numFiles && count < MAX_MAPLIST_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 + 2 ) / 3; + pages = MAX( 1, ( rows + MAX_MAPLIST_ROWS - 1 ) / MAX_MAPLIST_ROWS ); + if( page >= pages ) + page = pages - 1; + + start = page * MAX_MAPLIST_ROWS * 3; + if( count < start + ( 3 * MAX_MAPLIST_ROWS ) ) + rows = ( count - start + 2 ) / 3; + else + rows = MAX_MAPLIST_ROWS; + + ADMBP_begin( ); + for( row = 0; row < rows; row++ ) + { + for( i = start + row, j = 0; i < count && j < 3; i += rows, j++ ) + { + ADMBP( va( "^7 %-20s", fileSort[ i ] ) ); + shown++; + } + ADMBP( "\n" ); + } + if ( search[ 0 ] ) + ADMBP( va( "^3listmaps: ^7found %d maps matching '%s^7'", count, search ) ); + else + ADMBP( va( "^3listmaps: ^7listing %d of %d maps", shown, count ) ); + if( pages > 1 ) + ADMBP( va( ", page %d of %d", page + 1, pages ) ); + if( page + 1 < pages ) + ADMBP( va( ", use 'listmaps %s%s%d' to see more", + search, ( search[ 0 ] ) ? " ": "", page + 2 ) ); + ADMBP( ".\n" ); + ADMBP_end( ); +} + +/* +================= +Cmd_Test_f +================= +*/ +void Cmd_Test_f( gentity_t *humanPlayer ) +{ +} + +/* +================= +Cmd_Damage_f + +Deals damage to you (for testing), arguments: [damage] [dx] [dy] [dz] +The dx/dy arguments describe the damage point's offset from the entity origin +================= +*/ +void Cmd_Damage_f( gentity_t *ent ) +{ + vec3_t point; + char arg[ 16 ]; + float dx = 0.0f, dy = 0.0f, dz = 100.0f; + int damage = 100; + qboolean nonloc = qtrue; + + if( trap_Argc() > 1 ) + { + trap_Argv( 1, arg, sizeof( arg ) ); + damage = atoi( arg ); + } + if( trap_Argc() > 4 ) + { + trap_Argv( 2, arg, sizeof( arg ) ); + dx = atof( arg ); + trap_Argv( 3, arg, sizeof( arg ) ); + dy = atof( arg ); + trap_Argv( 4, arg, sizeof( arg ) ); + dz = atof( arg ); + nonloc = qfalse; + } + VectorCopy( ent->s.origin, point ); + point[ 0 ] += dx; + point[ 1 ] += dy; + point[ 2 ] += dz; + G_Damage( ent, NULL, NULL, NULL, point, damage, + ( nonloc ? DAMAGE_NO_LOCDAMAGE : 0 ), MOD_TARGET_LASER ); +} + +/* +================== +G_FloodLimited + +Determine whether a user is flood limited, and adjust their flood demerits +Print them a warning message if they are over the limit +Return is time in msec until the user can speak again +================== +*/ +int G_FloodLimited( gentity_t *ent ) +{ + int deltatime, ms; + + if( g_floodMinTime.integer <= 0 ) + return 0; + + // handles !ent + if( G_admin_permission( ent, ADMF_NOCENSORFLOOD ) ) + return 0; + + deltatime = level.time - ent->client->pers.floodTime; + + ent->client->pers.floodDemerits += g_floodMinTime.integer - deltatime; + if( ent->client->pers.floodDemerits < 0 ) + ent->client->pers.floodDemerits = 0; + ent->client->pers.floodTime = level.time; + + ms = ent->client->pers.floodDemerits - g_floodMaxDemerits.integer; + if( ms <= 0 ) + return 0; + trap_SendServerCommand( ent - g_entities, va( "print \"You are flooding: " + "please wait %d second%s before trying again\n", + ( ms + 999 ) / 1000, ( ms > 1000 ) ? "s" : "" ) ); + return ms; +} + +commands_t cmds[ ] = { + { "a", CMD_MESSAGE|CMD_INTERMISSION, Cmd_AdminMessage_f }, + { "build", CMD_TEAM|CMD_LIVING, Cmd_Build_f }, + { "buy", CMD_HUMAN|CMD_LIVING, Cmd_Buy_f }, + { "callteamvote", CMD_MESSAGE|CMD_TEAM, Cmd_CallVote_f }, + { "callvote", CMD_MESSAGE, Cmd_CallVote_f }, + { "cb", 0, Cmd_Cb_f }, //NOTE: it's a command used only by cgame + { "class", CMD_TEAM, Cmd_Class_f }, + { "damage", CMD_CHEAT|CMD_LIVING, Cmd_Damage_f }, + { "deconstruct", CMD_TEAM|CMD_LIVING, Cmd_Destroy_f }, + { "destroy", CMD_CHEAT|CMD_TEAM|CMD_LIVING, Cmd_Destroy_f }, + { "follow", CMD_SPEC, Cmd_Follow_f }, + { "follownext", CMD_SPEC, Cmd_FollowCycle_f }, + { "followprev", CMD_SPEC, Cmd_FollowCycle_f }, + { "give", CMD_CHEAT|CMD_TEAM|CMD_LIVING, Cmd_Give_f }, + { "god", CMD_CHEAT|CMD_TEAM|CMD_LIVING, Cmd_God_f }, + { "ignore", 0, Cmd_Ignore_f }, + { "impregnate", CMD_CHEAT|CMD_TEAM|CMD_LIVING, Cmd_Impregnate_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 }, + { "kill", CMD_TEAM|CMD_LIVING, Cmd_Kill_f }, + { "levelshot", CMD_CHEAT, Cmd_LevelShot_f }, + { "listmaps", CMD_MESSAGE|CMD_INTERMISSION, Cmd_ListMaps_f }, + { "m", CMD_MESSAGE|CMD_INTERMISSION, Cmd_PrivateMessage_f }, + { "mt", CMD_MESSAGE|CMD_INTERMISSION, Cmd_PrivateMessage_f }, + { "noclip", CMD_CHEAT_TEAM, Cmd_Noclip_f }, + { "notarget", CMD_CHEAT|CMD_TEAM|CMD_LIVING, Cmd_Notarget_f }, + { "reload", CMD_HUMAN|CMD_LIVING, Cmd_Reload_f }, + { "say", CMD_MESSAGE|CMD_INTERMISSION, Cmd_Say_f }, + { "say_area", CMD_MESSAGE|CMD_TEAM|CMD_LIVING, Cmd_SayArea_f }, + { "say_team", CMD_MESSAGE|CMD_INTERMISSION, Cmd_Say_f }, + { "score", CMD_INTERMISSION, ScoreboardMessage }, + { "sell", CMD_HUMAN|CMD_LIVING, Cmd_Sell_f }, + { "setviewpos", CMD_CHEAT_TEAM, Cmd_SetViewpos_f }, + { "team", 0, Cmd_Team_f }, + { "teamvote", CMD_TEAM, Cmd_Vote_f }, + { "test", CMD_CHEAT, Cmd_Test_f }, + { "unignore", 0, Cmd_Ignore_f }, + { "vote", 0, Cmd_Vote_f }, + { "vsay", CMD_MESSAGE|CMD_INTERMISSION, Cmd_VSay_f }, + { "vsay_local", CMD_MESSAGE|CMD_INTERMISSION, Cmd_VSay_f }, + { "vsay_team", CMD_MESSAGE|CMD_INTERMISSION, Cmd_VSay_f }, + { "where", 0, Cmd_Where_f } +}; +static size_t numCmds = sizeof( cmds ) / sizeof( cmds[ 0 ] ); + +/* +================= +ClientCommand +================= +*/ +void ClientCommand( int clientNum ) +{ + gentity_t *ent; + char cmd[ MAX_TOKEN_CHARS ]; + commands_t *command; + + ent = g_entities + clientNum; + if( !ent->client || ent->client->pers.connected != CON_CONNECTED ) + return; // not fully in game yet + + trap_Argv( 0, cmd, sizeof( cmd ) ); + + command = bsearch( cmd, cmds, numCmds, sizeof( cmds[ 0 ] ), cmdcmp ); + + if( !command ) + { + if( !G_admin_cmd_check( ent ) ) + trap_SendServerCommand( clientNum, + va( "print \"Unknown command %s\n\"", cmd ) ); + return; + } + + // do tests here to reduce the amount of repeated code + + if( !( command->cmdFlags & CMD_INTERMISSION ) && + ( level.intermissiontime || level.pausedTime ) ) + return; + + if( command->cmdFlags & CMD_CHEAT && !g_cheats.integer ) + { + G_TriggerMenu( clientNum, MN_CMD_CHEAT ); + return; + } + + if( command->cmdFlags & CMD_MESSAGE && ( ent->client->pers.namelog->muted || + G_FloodLimited( ent ) ) ) + return; + + if( command->cmdFlags & CMD_TEAM && + ent->client->pers.teamSelection == TEAM_NONE ) + { + G_TriggerMenu( clientNum, MN_CMD_TEAM ); + return; + } + + if( command->cmdFlags & CMD_CHEAT_TEAM && !g_cheats.integer && + ent->client->pers.teamSelection != TEAM_NONE ) + { + G_TriggerMenu( clientNum, MN_CMD_CHEAT_TEAM ); + return; + } + + if( command->cmdFlags & CMD_SPEC && + ent->client->sess.spectatorState == SPECTATOR_NOT ) + { + G_TriggerMenu( clientNum, MN_CMD_SPEC ); + return; + } + + if( command->cmdFlags & CMD_ALIEN && + ent->client->pers.teamSelection != TEAM_ALIENS ) + { + G_TriggerMenu( clientNum, MN_CMD_ALIEN ); + return; + } + + if( command->cmdFlags & CMD_HUMAN && + ent->client->pers.teamSelection != TEAM_HUMANS ) + { + G_TriggerMenu( clientNum, MN_CMD_HUMAN ); + return; + } + + if( command->cmdFlags & CMD_LIVING && + ( ent->client->ps.stats[ STAT_HEALTH ] <= 0 || + ent->client->sess.spectatorState != SPECTATOR_NOT ) ) + { + G_TriggerMenu( clientNum, MN_CMD_LIVING ); + return; + } + + command->cmdHandler( ent ); +} + +void G_ListCommands( gentity_t *ent ) +{ + int i; + char out[ MAX_STRING_CHARS ] = ""; + int len, outlen; + + outlen = 0; + + for( i = 0; i < numCmds; i++ ) + { + len = strlen( cmds[ i ].cmdName ) + 1; + if( len + outlen >= sizeof( out ) - 1 ) + { + trap_SendServerCommand( ent - g_entities, va( "cmds%s\n", out ) ); + outlen = 0; + } + + strcpy( out + outlen, va( " %s", cmds[ i ].cmdName ) ); + outlen += len; + } + + trap_SendServerCommand( ent - g_entities, va( "cmds%s\n", out ) ); + G_admin_cmdlist( ent ); +} + +void G_DecolorString( char *in, char *out, int len ) +{ + qboolean decolor = qtrue; + + len--; + + while( *in && len > 0 ) { + if( *in == DECOLOR_OFF || *in == DECOLOR_ON ) + { + decolor = ( *in == DECOLOR_ON ); + in++; + continue; + } + if( Q_IsColorString( in ) && decolor ) { + in += 2; + continue; + } + *out++ = *in++; + len--; + } + *out = '\0'; +} + +void G_UnEscapeString( char *in, char *out, int len ) +{ + len--; + + while( *in && len > 0 ) + { + if( *in >= ' ' || *in == '\n' ) + { + *out++ = *in; + len--; + } + in++; + } + *out = '\0'; +} + +void Cmd_PrivateMessage_f( gentity_t *ent ) +{ + int pids[ MAX_CLIENTS ]; + char name[ MAX_NAME_LENGTH ]; + char cmd[ 12 ]; + char text[ MAX_STRING_CHARS ]; + char *msg; + char color; + int i, pcount; + int count = 0; + qboolean teamonly = qfalse; + char recipients[ MAX_STRING_CHARS ] = ""; + + if( !g_privateMessages.integer && ent ) + { + ADMP( "Sorry, but private messages have been disabled\n" ); + return; + } + + trap_Argv( 0, cmd, sizeof( cmd ) ); + if( trap_Argc( ) < 3 ) + { + ADMP( va( "usage: %s [name|slot#] [message]\n", cmd ) ); + return; + } + + if( !Q_stricmp( cmd, "mt" ) ) + teamonly = qtrue; + + trap_Argv( 1, name, sizeof( name ) ); + msg = ConcatArgs( 2 ); + pcount = G_ClientNumbersFromString( name, pids, MAX_CLIENTS ); + + G_CensorString( text, msg, sizeof( text ), ent ); + + // send the message + for( i = 0; i < pcount; i++ ) + { + if( G_SayTo( ent, &g_entities[ pids[ i ] ], + teamonly ? SAY_TPRIVMSG : SAY_PRIVMSG, text ) ) + { + count++; + Q_strcat( recipients, sizeof( recipients ), va( "%s" S_COLOR_WHITE ", ", + level.clients[ pids[ i ] ].pers.netname ) ); + } + } + + // report the results + color = teamonly ? COLOR_CYAN : COLOR_YELLOW; + + if( !count ) + ADMP( va( "^3No player matching ^7\'%s^7\' ^3to send message to.\n", + name ) ); + else + { + ADMP( va( "^%cPrivate message: ^7%s\n", color, text ) ); + // remove trailing ", " + recipients[ strlen( recipients ) - 2 ] = '\0'; + ADMP( va( "^%csent to %i player%s: " S_COLOR_WHITE "%s\n", color, count, + count == 1 ? "" : "s", recipients ) ); + + G_LogPrintf( "%s: %d \"%s" S_COLOR_WHITE "\" \"%s\": ^%c%s\n", + ( teamonly ) ? "TPrivMsg" : "PrivMsg", + ( ent ) ? ent - g_entities : -1, + ( ent ) ? ent->client->pers.netname : "console", + name, color, msg ); + } +} + +/* +================= +Cmd_AdminMessage_f + +Send a message to all active admins +================= +*/ +void Cmd_AdminMessage_f( gentity_t *ent ) +{ + // Check permissions and add the appropriate user [prefix] + if( !G_admin_permission( ent, ADMF_ADMINCHAT ) ) + { + if( !g_publicAdminMessages.integer ) + { + ADMP( "Sorry, but use of /a by non-admins has been disabled.\n" ); + return; + } + else + { + ADMP( "Your message has been sent to any available admins " + "and to the server logs.\n" ); + } + } + + if( trap_Argc( ) < 2 ) + { + ADMP( "usage: a [message]\n" ); + return; + } + + G_AdminMessage( ent, ConcatArgs( 1 ) ); +} + diff --git a/src/game/g_cmds.c.orig b/src/game/g_cmds.c.orig new file mode 100644 index 0000000..5a30f7b --- /dev/null +++ b/src/game/g_cmds.c.orig @@ -0,0 +1,3588 @@ +/* +=========================================================================== +Copyright (C) 1999-2005 Id Software, Inc. +Copyright (C) 2000-2009 Darklegion Development + +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 color codes and non-alphanumeric characters from a string +================== +*/ +void G_SanitiseString( char *in, char *out, int len ) +{ + len--; + + while( *in && len > 0 ) + { + if( Q_IsColorString( in ) ) + { + in += 2; // skip color code + continue; + } + + if( isalnum( *in ) ) + { + *out++ = tolower( *in ); + len--; + } + in++; + } + *out = 0; +} + +/* +================== +G_ClientNumberFromString + +Returns a player number for either a number or name string +Returns -1 and optionally sets err if invalid or not exactly 1 match +err will have a trailing \n if set +================== +*/ +int G_ClientNumberFromString( char *s, char *err, int len ) +{ + gclient_t *cl; + int i, found = 0, m = -1; + char s2[ MAX_NAME_LENGTH ]; + char n2[ MAX_NAME_LENGTH ]; + char *p = err; + int l, l2 = len; + + if( !s[ 0 ] ) + { + if( p ) + Q_strncpyz( p, "no player name or slot # provided\n", len ); + + return -1; + } + + // numeric values are just slot numbers + for( i = 0; s[ i ] && isdigit( s[ i ] ); i++ ); + if( !s[ i ] ) + { + i = atoi( s ); + + if( i < 0 || i >= level.maxclients ) + return -1; + + cl = &level.clients[ i ]; + + if( cl->pers.connected == CON_DISCONNECTED ) + { + if( p ) + Q_strncpyz( p, "no player connected in that slot #\n", len ); + + return -1; + } + + return i; + } + + G_SanitiseString( s, s2, sizeof( s2 ) ); + if( !s2[ 0 ] ) + { + if( p ) + Q_strncpyz( p, "no player name provided\n", len ); + + return -1; + } + + if( p ) + { + Q_strncpyz( p, "more than one player name matches. " + "be more specific or use the slot #:\n", l2 ); + l = strlen( p ); + p += l; + l2 -= l; + } + + // check for a name match + for( i = 0, cl = level.clients; i < level.maxclients; i++, cl++ ) + { + if( cl->pers.connected == CON_DISCONNECTED ) + continue; + + G_SanitiseString( cl->pers.netname, n2, sizeof( n2 ) ); + + if( !strcmp( n2, s2 ) ) + return i; + + if( strstr( n2, s2 ) ) + { + if( p ) + { + l = Q_snprintf( p, l2, "%-2d - %s^7\n", i, cl->pers.netname ); + p += l; + l2 -= l; + } + + found++; + m = i; + } + } + + if( found == 1 ) + return m; + + if( found == 0 && err ) + Q_strncpyz( err, "no connected player by that name or slot #\n", len ); + + return -1; +} + +/* +================== +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. +================== +*/ +int G_ClientNumbersFromString( char *s, int *plist, int max ) +{ + gclient_t *p; + int i, found = 0; + char *endptr; + char n2[ MAX_NAME_LENGTH ] = {""}; + char s2[ MAX_NAME_LENGTH ] = {""}; + + if( max == 0 ) + return 0; + + if( !s[ 0 ] ) + return 0; + + // if a number is provided, it is a clientnum + i = strtol( s, &endptr, 10 ); + if( *endptr == '\0' ) + { + 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 + return 0; + } + + // now look for name matches + G_SanitiseString( s, s2, sizeof( s2 ) ); + if( !s2[ 0 ] ) + 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++; + } + } + 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 + ping = cl->ps.ping < 999 ? cl->ps.ping : 999; + + if( cl->sess.spectatorState == SPECTATOR_NOT && + ( ent->client->pers.teamSelection == TEAM_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_MK1, cl->ps.stats ) ) + upgrade = UP_HELMET_MK1; + else if( BG_InventoryContainsUpgrade( UP_HELMET_MK2, cl->ps.stats ) ) + upgrade = UP_HELMET_MK2; + 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->ps.persistant[ 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%s", + 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; +} + +/* +================== +ConcatArgsPrintable +Duplicate of concatargs but enquotes things that need to be +Used to log command arguments in a way that preserves user intended tokenizing +================== +*/ +char *ConcatArgsPrintable( int start ) +{ + int i, c, tlen; + static char line[ MAX_STRING_CHARS ]; + int len; + char arg[ MAX_STRING_CHARS + 2 ]; + char *printArg; + + len = 0; + c = trap_Argc( ); + + for( i = start; i < c; i++ ) + { + printArg = arg; + trap_Argv( i, arg, sizeof( arg ) ); + if( strchr( arg, ' ' ) ) + printArg = va( "\"%s\"", arg ); + tlen = strlen( printArg ); + + if( len + tlen >= MAX_STRING_CHARS - 1 ) + break; + + memcpy( line + len, printArg, tlen ); + len += tlen; + + if( len == MAX_STRING_CHARS - 1 ) + break; + + if( i != c - 1 ) + { + line[ len ] = ' '; + len++; + } + } + + line[ len ] = 0; + + return line; +} + + +/* +================== +Cmd_Give_f + +Give items to a client +================== +*/ +void Cmd_Give_f( gentity_t *ent ) +{ + char *name; + qboolean give_all = qfalse; + + if( trap_Argc( ) < 2 ) + { + ADMP( "usage: give [what]\n" ); + ADMP( "usage: valid choices are: all, health, funds [amount], stamina, " + "poison, gas, ammo\n" ); + return; + } + + name = ConcatArgs( 1 ); + if( Q_stricmp( name, "all" ) == 0 ) + give_all = qtrue; + + if( give_all || Q_stricmp( name, "health" ) == 0 ) + { + 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 ) + { + float credits; + + if( give_all || trap_Argc( ) < 3 ) + credits = 30000.0f; + else + { + credits = atof( name + 6 ) * + ( ent->client->pers.teamSelection == + TEAM_ALIENS ? ALIEN_CREDITS_PER_KILL : 1.0f ); + + // clamp credits manually, as G_AddCreditToClient() expects a short int + if( credits > SHRT_MAX ) + credits = 30000.0f; + else if( credits < SHRT_MIN ) + credits = -30000.0f; + } + + G_AddCreditToClient( ent->client, (short)credits, qtrue ); + } + + if( give_all || Q_stricmp( name, "stamina" ) == 0 ) + ent->client->ps.stats[ STAT_STAMINA ] = STAMINA_MAX; + + if( Q_stricmp( name, "poison" ) == 0 ) + { + if( ent->client->pers.teamSelection == TEAM_HUMANS ) + { + ent->client->ps.stats[ STAT_STATE ] |= SS_POISONED; + ent->client->lastPoisonTime = level.time; + ent->client->lastPoisonClient = ent; + } + else + { + ent->client->ps.stats[ STAT_STATE ] |= SS_BOOSTED; + ent->client->boostedTime = level.time; + } + } + + if( Q_stricmp( name, "gas" ) == 0 ) + { + ent->client->ps.eFlags |= EF_POISONCLOUDED; + ent->client->lastPoisonCloudedTime = level.time; + trap_SendServerCommand( ent->client->ps.clientNum, + "poisoncloud" ); + } + + if( give_all || Q_stricmp( name, "ammo" ) == 0 ) + { + gclient_t *client = ent->client; + + if( client->ps.weapon != WP_ALEVEL3_UPG && + BG_Weapon( client->ps.weapon )->infiniteAmmo ) + return; + + client->ps.ammo = BG_Weapon( client->ps.weapon )->maxAmmo; + client->ps.clips = BG_Weapon( client->ps.weapon )->maxClips; + + if( BG_Weapon( client->ps.weapon )->usesEnergy && + BG_InventoryContainsUpgrade( UP_BATTPACK, client->ps.stats ) ) + client->ps.ammo = (int)( (float)client->ps.ammo * BATTPACK_MODIFIER ); + } + + if( give_all || Q_stricmp( name, "fuel" ) == 0 ) + { + if( BG_InventoryContainsUpgrade( UP_JETPACK, ent->client->ps.stats ) ) + ent->client->ps.stats[ STAT_FUEL ] = JETPACK_FUEL_FULL; + } +} + + +/* +================== +Cmd_God_f + +Sets client to godmode + +argv(0) god +================== +*/ +void Cmd_God_f( gentity_t *ent ) +{ + char *msg; + + ent->flags ^= FL_GODMODE; + + if( !( ent->flags & FL_GODMODE ) ) + msg = "godmode OFF\n"; + else + msg = "godmode ON\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; + + ent->flags ^= FL_NOTARGET; + + if( !( ent->flags & FL_NOTARGET ) ) + msg = "notarget OFF\n"; + else + msg = "notarget ON\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( ent->client->noclip ) + msg = "noclip OFF\n"; + else + msg = "noclip ON\n"; + + ent->client->noclip = !ent->client->noclip; + + 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( 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 cancelled\n\"" ); + ent->suicideTime = 0; + } + } +} + +/* +================= +Cmd_Team_f +================= +*/ +void Cmd_Team_f( gentity_t *ent ) +{ + team_t team; + team_t oldteam = ent->client->pers.teamSelection; + char s[ MAX_TOKEN_CHARS ]; + qboolean force = G_admin_permission( ent, ADMF_FORCETEAMCHANGE ); + int aliens = level.numAlienClients; + int humans = level.numHumanClients; + + if( oldteam == TEAM_ALIENS ) + aliens--; + else if( oldteam == TEAM_HUMANS ) + humans--; + + // stop team join spam + if( ent->client->pers.teamChangeTime && + level.time - ent->client->pers.teamChangeTime < 1000 ) + return; + + // stop switching teams for gameplay exploit reasons by enforcing a long + // wait before they can come back + if( !force && !g_cheats.integer && ent->client->pers.aliveSeconds && + level.time - ent->client->pers.teamChangeTime < 30000 ) + { + trap_SendServerCommand( ent-g_entities, + va( "print \"You must wait another %d seconds before changing teams again\n\"", + (int) ( ( 30000 - ( level.time - ent->client->pers.teamChangeTime ) ) / 1000.f ) ) ); + return; + } + + // disallow joining teams during warmup + if( g_doWarmup.integer && ( ( level.warmupTime - level.time ) / 1000 ) > 0 ) + { + G_TriggerMenu( ent - g_entities, MN_WARMUP ); + return; + } + + trap_Argv( 1, s, sizeof( s ) ); + + if( !s[ 0 ] ) + { + trap_SendServerCommand( ent-g_entities, va( "print \"team: %s\n\"", + BG_TeamName( oldteam ) ) ); + return; + } + + if( !Q_stricmp( s, "auto" ) ) + { + if( level.humanTeamLocked && level.alienTeamLocked ) + team = TEAM_NONE; + else if( level.humanTeamLocked || humans > aliens ) + team = TEAM_ALIENS; + + else if( level.alienTeamLocked || aliens > humans ) + team = TEAM_HUMANS; + else + team = TEAM_ALIENS + rand( ) / ( RAND_MAX / 2 + 1 ); + } + else switch( G_TeamFromString( s ) ) + { + case TEAM_NONE: + team = TEAM_NONE; + break; + + case TEAM_ALIENS: + if( level.alienTeamLocked ) + { + G_TriggerMenu( ent - g_entities, MN_A_TEAMLOCKED ); + return; + } + else if( level.humanTeamLocked ) + force = qtrue; + + if( !force && g_teamForceBalance.integer && aliens > humans ) + { + G_TriggerMenu( ent - g_entities, MN_A_TEAMFULL ); + return; + } + + team = TEAM_ALIENS; + break; + + case TEAM_HUMANS: + if( level.humanTeamLocked ) + { + G_TriggerMenu( ent - g_entities, MN_H_TEAMLOCKED ); + return; + } + else if( level.alienTeamLocked ) + force = qtrue; + + if( !force && g_teamForceBalance.integer && humans > aliens ) + { + G_TriggerMenu( ent - g_entities, MN_H_TEAMFULL ); + return; + } + + team = TEAM_HUMANS; + break; + + default: + trap_SendServerCommand( ent-g_entities, + va( "print \"Unknown team: %s\n\"", s ) ); + return; + } + + // stop team join spam + if( oldteam == team ) + return; + + if( team != TEAM_NONE && g_maxGameClients.integer && + level.numPlayingClients >= g_maxGameClients.integer ) + { + G_TriggerMenu( ent - g_entities, MN_PLAYERLIMIT ); + return; + } + + // Apply the change + G_ChangeTeam( ent, team ); +} + +/* +================== +G_CensorString +================== +*/ +static char censors[ 20000 ]; +static int numcensors; + +void G_LoadCensors( void ) +{ + char *text_p, *token; + char text[ 20000 ]; + char *term; + int len; + fileHandle_t f; + + numcensors = 0; + + if( !g_censorship.string[ 0 ] ) + return; + + len = trap_FS_FOpenFile( g_censorship.string, &f, FS_READ ); + if( len < 0 ) + { + Com_Printf( S_COLOR_RED "ERROR: Censors file %s doesn't exist\n", + g_censorship.string ); + return; + } + if( len == 0 || len >= sizeof( text ) - 1 ) + { + trap_FS_FCloseFile( f ); + Com_Printf( S_COLOR_RED "ERROR: Censors file %s is %s\n", + g_censorship.string, len == 0 ? "empty" : "too long" ); + return; + } + trap_FS_Read( text, len, f ); + trap_FS_FCloseFile( f ); + text[ len ] = 0; + + term = censors; + + text_p = text; + while( 1 ) + { + token = COM_Parse( &text_p ); + if( !*token || sizeof( censors ) - ( term - censors ) < 4 ) + break; + Q_strncpyz( term, token, sizeof( censors ) - ( term - censors ) ); + Q_strlwr( term ); + term += strlen( term ) + 1; + if( sizeof( censors ) - ( term - censors ) == 0 ) + break; + token = COM_ParseExt( &text_p, qfalse ); + Q_strncpyz( term, token, sizeof( censors ) - ( term - censors ) ); + term += strlen( term ) + 1; + numcensors++; + } + G_Printf( "Parsed %d string replacements\n", numcensors ); +} + +void G_CensorString( char *out, const char *in, int len, gentity_t *ent ) +{ + const char *s, *m; + int i; + + if( !numcensors || G_admin_permission( ent, ADMF_NOCENSORFLOOD) ) + { + Q_strncpyz( out, in, len ); + return; + } + + len--; + while( *in ) + { + if( Q_IsColorString( in ) ) + { + if( len < 2 ) + break; + *out++ = *in++; + *out++ = *in++; + len -= 2; + continue; + } + if( !isalnum( *in ) ) + { + if( len < 1 ) + break; + *out++ = *in++; + len--; + continue; + } + m = censors; + for( i = 0; i < numcensors; i++, m++ ) + { + s = in; + while( *s && *m ) + { + if( Q_IsColorString( s ) ) + { + s += 2; + continue; + } + if( !isalnum( *s ) ) + { + s++; + continue; + } + if( tolower( *s ) != *m ) + break; + s++; + m++; + } + // match + if( !*m ) + { + in = s; + m++; + while( *m ) + { + if( len < 1 ) + break; + *out++ = *m++; + len--; + } + break; + } + else + { + while( *m ) + m++; + m++; + while( *m ) + m++; + } + } + if( len < 1 ) + break; + // no match + if( i == numcensors ) + { + *out++ = *in++; + len--; + } + } + *out = 0; +} + +/* +================== +G_Say +================== +*/ +static qboolean G_SayTo( gentity_t *ent, gentity_t *other, saymode_t mode, const char *message ) +{ + if( !other ) + return qfalse; + + if( !other->inuse ) + return qfalse; + + if( !other->client ) + return qfalse; + + if( other->client->pers.connected != CON_CONNECTED ) + return qfalse; + + if( ( ent && !OnSameTeam( ent, other ) ) && + ( mode == SAY_TEAM || mode == SAY_AREA || mode == SAY_TPRIVMSG ) ) + { + if( other->client->pers.teamSelection != TEAM_NONE ) + return qfalse; + + // specs with ADMF_SPEC_ALLCHAT flag can see team chat + if( !G_admin_permission( other, ADMF_SPEC_ALLCHAT ) && mode != SAY_TPRIVMSG ) + return qfalse; + } + + trap_SendServerCommand( other-g_entities, va( "chat %d %d \"%s\"", + ent ? ent-g_entities : -1, + mode, + message ) ); + + return qtrue; +} + +void G_Say( gentity_t *ent, saymode_t mode, const char *chatText ) +{ + int j; + gentity_t *other; + // don't let text be too long for malicious reasons + char text[ MAX_SAY_TEXT ]; + + // check if blocked by g_specChat 0 + if( ( !g_specChat.integer ) && ( mode != SAY_TEAM ) && + ( ent ) && ( ent->client->pers.teamSelection == TEAM_NONE ) && + ( !G_admin_permission( ent, ADMF_NOCENSORFLOOD ) ) ) + { + trap_SendServerCommand( ent-g_entities, "print \"say: Global chatting for " + "spectators has been disabled. You may only use team chat.\n\"" ); + mode = SAY_TEAM; + } + + switch( mode ) + { + case SAY_ALL: + G_LogPrintf( "Say: %d \"%s" S_COLOR_WHITE "\": " S_COLOR_GREEN "%s\n", + ( ent ) ? ent - g_entities : -1, + ( ent ) ? ent->client->pers.netname : "console", chatText ); + break; + case SAY_TEAM: + // console say_team is handled in g_svscmds, not here + if( !ent || !ent->client ) + Com_Error( ERR_FATAL, "SAY_TEAM by non-client entity\n" ); + G_LogPrintf( "SayTeam: %d \"%s" S_COLOR_WHITE "\": " S_COLOR_CYAN "%s\n", + ent - g_entities, ent->client->pers.netname, chatText ); + break; + case SAY_RAW: + if( ent ) + Com_Error( ERR_FATAL, "SAY_RAW by client entity\n" ); + G_LogPrintf( "Chat: -1 \"console\": %s\n", chatText ); + default: + break; + } + + G_CensorString( text, chatText, sizeof( text ), ent ); + + // send it to all the apropriate clients + for( j = 0; j < level.maxclients; j++ ) + { + other = &g_entities[ j ]; + G_SayTo( ent, other, mode, text ); + } +} + +/* +================== +Cmd_SayArea_f +================== +*/ +static void Cmd_SayArea_f( gentity_t *ent ) +{ + int entityList[ MAX_GENTITIES ]; + int num, i; + vec3_t range = { 1000.0f, 1000.0f, 1000.0f }; + vec3_t mins, maxs; + char *msg; + + if( trap_Argc( ) < 2 ) + { + ADMP( "usage: say_area [message]\n" ); + return; + } + + msg = ConcatArgs( 1 ); + + for(i = 0; i < 3; i++ ) + range[ i ] = g_sayAreaRange.value; + + G_LogPrintf( "SayArea: %d \"%s" S_COLOR_WHITE "\": " S_COLOR_BLUE "%s\n", + ent - g_entities, ent->client->pers.netname, msg ); + + 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_AREA, msg ); + + //Send to ADMF_SPEC_ALLCHAT candidates + for( i = 0; i < level.maxclients; i++ ) + { + if( g_entities[ i ].client->pers.teamSelection == TEAM_NONE && + G_admin_permission( &g_entities[ i ], ADMF_SPEC_ALLCHAT ) ) + { + G_SayTo( ent, &g_entities[ i ], SAY_AREA, msg ); + } + } +} + + +/* +================== +Cmd_Say_f +================== +*/ +static void Cmd_Say_f( gentity_t *ent ) +{ + char *p; + char cmd[ MAX_TOKEN_CHARS ]; + saymode_t mode = SAY_ALL; + + if( trap_Argc( ) < 2 ) + return; + + trap_Argv( 0, cmd, sizeof( cmd ) ); + if( Q_stricmp( cmd, "say_team" ) == 0 ) + mode = SAY_TEAM; + + p = ConcatArgs( 1 ); + + G_Say( ent, mode, p ); +} + +/* +================== +Cmd_VSay_f +================== +*/ +void Cmd_VSay_f( gentity_t *ent ) +{ + char arg[MAX_TOKEN_CHARS]; + char text[ MAX_TOKEN_CHARS ]; + voiceChannel_t vchan; + voice_t *voice; + voiceCmd_t *cmd; + voiceTrack_t *track; + int cmdNum = 0; + int trackNum = 0; + char voiceName[ MAX_VOICE_NAME_LEN ] = {"default"}; + char voiceCmd[ MAX_VOICE_CMD_LEN ] = {""}; + char vsay[ 12 ] = {""}; + weapon_t weapon; + + if( !ent || !ent->client ) + Com_Error( ERR_FATAL, "Cmd_VSay_f() called by non-client entity\n" ); + + trap_Argv( 0, arg, sizeof( arg ) ); + if( trap_Argc( ) < 2 ) + { + trap_SendServerCommand( ent-g_entities, va( + "print \"usage: %s command [text] \n\"", arg ) ); + return; + } + if( !level.voices ) + { + trap_SendServerCommand( ent-g_entities, va( + "print \"%s: voice system is not installed on this server\n\"", arg ) ); + return; + } + if( !g_voiceChats.integer ) + { + trap_SendServerCommand( ent-g_entities, va( + "print \"%s: voice system administratively disabled on this server\n\"", + arg ) ); + return; + } + if( !Q_stricmp( arg, "vsay" ) ) + vchan = VOICE_CHAN_ALL; + else if( !Q_stricmp( arg, "vsay_team" ) ) + vchan = VOICE_CHAN_TEAM; + else if( !Q_stricmp( arg, "vsay_local" ) ) + vchan = VOICE_CHAN_LOCAL; + else + return; + Q_strncpyz( vsay, arg, sizeof( vsay ) ); + + if( ent->client->pers.voice[ 0 ] ) + Q_strncpyz( voiceName, ent->client->pers.voice, sizeof( voiceName ) ); + voice = BG_VoiceByName( level.voices, voiceName ); + if( !voice ) + { + trap_SendServerCommand( ent-g_entities, va( + "print \"%s: voice '%s' not found\n\"", vsay, voiceName ) ); + return; + } + + trap_Argv( 1, voiceCmd, sizeof( voiceCmd ) ) ; + cmd = BG_VoiceCmdFind( voice->cmds, voiceCmd, &cmdNum ); + if( !cmd ) + { + trap_SendServerCommand( ent-g_entities, va( + "print \"%s: command '%s' not found in voice '%s'\n\"", + vsay, voiceCmd, voiceName ) ); + return; + } + + // filter non-spec humans by their primary weapon as well + weapon = WP_NONE; + if( ent->client->sess.spectatorState == SPECTATOR_NOT ) + { + weapon = BG_PrimaryWeapon( ent->client->ps.stats ); + } + + track = BG_VoiceTrackFind( cmd->tracks, ent->client->pers.teamSelection, + ent->client->pers.classSelection, weapon, (int)ent->client->voiceEnthusiasm, + &trackNum ); + if( !track ) + { + trap_SendServerCommand( ent-g_entities, va( + "print \"%s: no available track for command '%s', team %d, " + "class %d, weapon %d, and enthusiasm %d in voice '%s'\n\"", + vsay, voiceCmd, ent->client->pers.teamSelection, + ent->client->pers.classSelection, weapon, + (int)ent->client->voiceEnthusiasm, voiceName ) ); + return; + } + + if( !Q_stricmp( ent->client->lastVoiceCmd, cmd->cmd ) ) + ent->client->voiceEnthusiasm++; + + Q_strncpyz( ent->client->lastVoiceCmd, cmd->cmd, + sizeof( ent->client->lastVoiceCmd ) ); + + // optional user supplied text + trap_Argv( 2, arg, sizeof( arg ) ); + G_CensorString( text, arg, sizeof( text ), ent ); + + switch( vchan ) + { + case VOICE_CHAN_ALL: + case VOICE_CHAN_LOCAL: + trap_SendServerCommand( -1, va( + "voice %d %d %d %d \"%s\"\n", + ent-g_entities, vchan, cmdNum, trackNum, text ) ); + break; + case VOICE_CHAN_TEAM: + G_TeamCommand( ent->client->pers.teamSelection, va( + "voice %d %d %d %d \"%s\"\n", + ent-g_entities, vchan, cmdNum, trackNum, text ) ); + break; + default: + break; + } +} + +/* +================== +Cmd_Where_f +================== +*/ +void Cmd_Where_f( gentity_t *ent ) +{ + if( !ent->client ) + return; + trap_SendServerCommand( ent - g_entities, + va( "print \"origin: %f %f %f\n\"", + ent->s.origin[ 0 ], ent->s.origin[ 1 ], + ent->s.origin[ 2 ] ) ); +} + +/* +================== +Cmd_CallVote_f +================== +*/ +void Cmd_CallVote_f( gentity_t *ent ) +{ + char cmd[ MAX_TOKEN_CHARS ], + vote[ MAX_TOKEN_CHARS ], + arg[ MAX_TOKEN_CHARS ]; + char name[ MAX_NAME_LENGTH ] = ""; + char caller[ MAX_NAME_LENGTH ] = ""; + char reason[ MAX_TOKEN_CHARS ]; + char *creason; + int clientNum = -1; + int id = -1; + team_t team; + + trap_Argv( 0, cmd, sizeof( cmd ) ); + trap_Argv( 1, vote, sizeof( vote ) ); + trap_Argv( 2, arg, sizeof( arg ) ); + creason = ConcatArgs( 3 ); + G_DecolorString( creason, reason, sizeof( reason ) ); + + if( !Q_stricmp( cmd, "callteamvote" ) ) + team = ent->client->pers.teamSelection; + else + team = TEAM_NONE; + + if( !g_allowVote.integer ) + { + trap_SendServerCommand( ent-g_entities, + va( "print \"%s: voting not allowed here\n\"", cmd ) ); + return; + } + + if( level.voteTime[ team ] ) + { + trap_SendServerCommand( ent-g_entities, + va( "print \"%s: a vote is already in progress\n\"", cmd ) ); + return; + } + + if( level.voteExecuteTime[ team ] ) + G_ExecuteVote( team ); + + level.voteDelay[ team ] = 0; + level.voteThreshold[ team ] = 50; + + if( g_voteLimit.integer > 0 && + ent->client->pers.namelog->voteCount >= g_voteLimit.integer && + !G_admin_permission( ent, ADMF_NO_VOTE_LIMIT ) ) + { + trap_SendServerCommand( ent-g_entities, va( + "print \"%s: you have already called the maximum number of votes (%d)\n\"", + cmd, g_voteLimit.integer ) ); + return; + } + + // kick, mute, unmute, denybuild, allowbuild + if( !Q_stricmp( vote, "kick" ) || + !Q_stricmp( vote, "mute" ) || !Q_stricmp( vote, "unmute" ) || + !Q_stricmp( vote, "denybuild" ) || !Q_stricmp( vote, "allowbuild" ) ) + { + char err[ MAX_STRING_CHARS ]; + + if( !arg[ 0 ] ) + { + trap_SendServerCommand( ent-g_entities, + va( "print \"%s: no target\n\"", cmd ) ); + return; + } + + // with a little extra work only players from the right team are considered + clientNum = G_ClientNumberFromString( arg, err, sizeof( err ) ); + + if( clientNum == -1 ) + { + ADMP( va( "%s: %s", cmd, err ) ); + return; + } + + G_DecolorString( level.clients[ clientNum ].pers.netname, name, sizeof( name ) ); + id = level.clients[ clientNum ].pers.namelog->id; + + if( !Q_stricmp( vote, "kick" ) || !Q_stricmp( vote, "mute" ) || + !Q_stricmp( vote, "denybuild" ) ) + { + if( G_admin_permission( g_entities + clientNum, ADMF_IMMUNITY ) ) + { + trap_SendServerCommand( ent-g_entities, + va( "print \"%s: admin is immune\n\"", cmd ) ); + + G_AdminMessage( NULL, va( S_COLOR_WHITE "%s" S_COLOR_YELLOW " attempted %s %s" + " on immune admin " S_COLOR_WHITE "%s" S_COLOR_YELLOW + " for: %s", + ent->client->pers.netname, cmd, vote, + g_entities[ clientNum ].client->pers.netname, + reason[ 0 ] ? reason : "no reason" ) ); + return; + } + + if( team != TEAM_NONE && + ( ent->client->pers.teamSelection != + level.clients[ clientNum ].pers.teamSelection ) ) + { + trap_SendServerCommand( ent-g_entities, + va( "print \"%s: player is not on your team\n\"", cmd ) ); + return; + } + + if( !reason[ 0 ] && !G_admin_permission( ent, ADMF_UNACCOUNTABLE ) ) + { + trap_SendServerCommand( ent-g_entities, + va( "print \"%s: You must provide a reason\n\"", cmd ) ); + return; + } + } + } + + if( !Q_stricmp( vote, "kick" ) ) + { + if( level.clients[ clientNum ].pers.localClient ) + { + trap_SendServerCommand( ent-g_entities, + va( "print \"%s: admin is immune\n\"", cmd ) ); + return; + } + + Com_sprintf( level.voteString[ team ], sizeof( level.voteString[ team ] ), + "ban %s \"1s%s\" vote kick (%s)", level.clients[ clientNum ].pers.ip.str, + g_adminTempBan.string, reason ); + Com_sprintf( level.voteDisplayString[ team ], + sizeof( level.voteDisplayString[ team ] ), "Kick player '%s'", name ); + if( reason[ 0 ] ) + { + Q_strcat( level.voteDisplayString[ team ], + sizeof( level.voteDisplayString[ team ] ), va( " for '%s'", reason ) ); + } + } + else if( team == TEAM_NONE ) + { + if( !Q_stricmp( vote, "mute" ) ) + { + if( level.clients[ clientNum ].pers.namelog->muted ) + { + trap_SendServerCommand( ent-g_entities, + va( "print \"%s: player is already muted\n\"", cmd ) ); + return; + } + + Com_sprintf( level.voteString[ team ], sizeof( level.voteString[ team ] ), + "mute %d", id ); + Com_sprintf( level.voteDisplayString[ team ], + sizeof( level.voteDisplayString[ team ] ), + "Mute player '%s'", name ); + if( reason[ 0 ] ) + { + Q_strcat( level.voteDisplayString[ team ], + sizeof( level.voteDisplayString[ team ] ), va( " for '%s'", reason ) ); + } + } + else if( !Q_stricmp( vote, "unmute" ) ) + { + if( !level.clients[ clientNum ].pers.namelog->muted ) + { + trap_SendServerCommand( ent-g_entities, + va( "print \"%s: player is not currently muted\n\"", cmd ) ); + return; + } + + Com_sprintf( level.voteString[ team ], sizeof( level.voteString[ team ] ), + "unmute %d", id ); + Com_sprintf( level.voteDisplayString[ team ], + sizeof( level.voteDisplayString[ team ] ), + "Unmute player '%s'", name ); + } + else if( !Q_stricmp( vote, "map_restart" ) ) + { + strcpy( level.voteString[ team ], vote ); + strcpy( level.voteDisplayString[ team ], "Restart current map" ); + // map_restart comes with a default delay + } + else if( !Q_stricmp( vote, "map" ) ) + { + if( !G_MapExists( arg ) ) + { + trap_SendServerCommand( ent-g_entities, + va( "print \"%s: 'maps/%s.bsp' could not be found on the server\n\"", + cmd, arg ) ); + return; + } + + Com_sprintf( level.voteString[ team ], sizeof( level.voteString ), + "%s \"%s\"", vote, arg ); + Com_sprintf( level.voteDisplayString[ team ], + sizeof( level.voteDisplayString[ team ] ), + "Change to map '%s'", arg ); + level.voteDelay[ team ] = 3000; + } + else if( !Q_stricmp( vote, "nextmap" ) ) + { + if( G_MapExists( g_nextMap.string ) ) + { + trap_SendServerCommand( ent-g_entities, + va( "print \"%s: the next map is already set to '%s'\n\"", + cmd, g_nextMap.string ) ); + return; + } + + if( !G_MapExists( arg ) ) + { + trap_SendServerCommand( ent-g_entities, + va( "print \"%s: 'maps/%s.bsp' could not be found on the server\n\"", + cmd, arg ) ); + return; + } + + Com_sprintf( level.voteString[ team ], sizeof( level.voteString[ team ] ), + "set g_nextMap \"%s\"", arg ); + Com_sprintf( level.voteDisplayString[ team ], + sizeof( level.voteDisplayString[ team ] ), + "Set the next map to '%s'", arg ); + } + else if( !Q_stricmp( vote, "draw" ) ) + { + strcpy( level.voteString[ team ], "evacuation" ); + strcpy( level.voteDisplayString[ team ], "End match in a draw" ); + level.voteDelay[ team ] = 3000; + } + else if( !Q_stricmp( vote, "sudden_death" ) ) + { + if(!g_suddenDeathVotePercent.integer) + { + trap_SendServerCommand( ent-g_entities, + "print \"Sudden Death votes have been disabled\n\"" ); + return; + } + if( G_TimeTilSuddenDeath( ) <= 0 ) + { + trap_SendServerCommand( ent - g_entities, + va( "print \"callvote: Sudden Death has already begun\n\"") ); + return; + } + if( level.suddenDeathBeginTime > 0 && + G_TimeTilSuddenDeath() <= g_suddenDeathVoteDelay.integer * 1000 ) + { + trap_SendServerCommand( ent - g_entities, + va( "print \"callvote: Sudden Death is imminent\n\"") ); + return; + } + level.voteThreshold[ team ] = g_suddenDeathVotePercent.integer; + Com_sprintf( level.voteString[ team ], sizeof( level.voteString[ team ] ), + "suddendeath %d", g_suddenDeathVoteDelay.integer ); + Com_sprintf( level.voteDisplayString[ team ], + sizeof( level.voteDisplayString[ team ] ), + "Begin sudden death in %d seconds", + g_suddenDeathVoteDelay.integer ); + } + else + { + trap_SendServerCommand( ent-g_entities, "print \"Invalid vote string\n\"" ); + trap_SendServerCommand( ent-g_entities, "print \"Valid vote commands are: " + "map, nextmap, map_restart, draw, sudden_death, kick, mute and unmute\n" ); + return; + } + } + else if( !Q_stricmp( vote, "denybuild" ) ) + { + if( level.clients[ clientNum ].pers.namelog->denyBuild ) + { + trap_SendServerCommand( ent-g_entities, + va( "print \"%s: player already lost building rights\n\"", cmd ) ); + return; + } + + Com_sprintf( level.voteString[ team ], sizeof( level.voteString[ team ] ), + "denybuild %d", id ); + Com_sprintf( level.voteDisplayString[ team ], + sizeof( level.voteDisplayString[ team ] ), + "Take away building rights from '%s'", name ); + if( reason[ 0 ] ) + { + Q_strcat( level.voteDisplayString[ team ], + sizeof( level.voteDisplayString[ team ] ), va( " for '%s'", reason ) ); + } + } + else if( !Q_stricmp( vote, "allowbuild" ) ) + { + if( !level.clients[ clientNum ].pers.namelog->denyBuild ) + { + trap_SendServerCommand( ent-g_entities, + va( "print \"%s: player already has building rights\n\"", cmd ) ); + return; + } + + Com_sprintf( level.voteString[ team ], sizeof( level.voteString[ team ] ), + "allowbuild %d", id ); + Com_sprintf( level.voteDisplayString[ team ], + sizeof( level.voteDisplayString[ team ] ), + "Allow '%s' to build", name ); + } + else if( !Q_stricmp( vote, "admitdefeat" ) ) + { + Com_sprintf( level.voteString[ team ], sizeof( level.voteString[ team ] ), + "admitdefeat %d", team ); + strcpy( level.voteDisplayString[ team ], "Admit Defeat" ); + level.voteDelay[ team ] = 3000; + } + 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 and admitdefeat\n\"" ); + return; + } + + G_LogPrintf( "%s: %d \"%s" S_COLOR_WHITE "\": %s\n", + team == TEAM_NONE ? "CallVote" : "CallTeamVote", + ent - g_entities, ent->client->pers.netname, level.voteString[ team ] ); + + if( team == TEAM_NONE ) + { + trap_SendServerCommand( -1, va( "print \"%s" S_COLOR_WHITE + " called a vote: %s\n\"", ent->client->pers.netname, + level.voteDisplayString[ team ] ) ); + } + else + { + 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 == TEAM_NONE && + G_admin_permission( &g_entities[ i ], ADMF_SPEC_ALLCHAT ) ) ) + { + trap_SendServerCommand( i, va( "print \"%s" S_COLOR_WHITE + " called a team vote: %s\n\"", ent->client->pers.netname, + level.voteDisplayString[ team ] ) ); + } + else if( G_admin_permission( &g_entities[ i ], ADMF_ADMINCHAT ) ) + { + trap_SendServerCommand( i, va( "chat -1 %d \"" S_COLOR_YELLOW "%s" + S_COLOR_YELLOW " called a team vote (%ss): %s\"", SAY_ADMINS, + ent->client->pers.netname, BG_TeamName( team ), + level.voteDisplayString[ team ] ) ); + } + } + } + } + + G_DecolorString( ent->client->pers.netname, caller, sizeof( caller ) ); + + level.voteTime[ team ] = level.time; + trap_SetConfigstring( CS_VOTE_TIME + team, + va( "%d", level.voteTime[ team ] ) ); + trap_SetConfigstring( CS_VOTE_STRING + team, + level.voteDisplayString[ team ] ); + trap_SetConfigstring( CS_VOTE_CALLER + team, + caller ); + + ent->client->pers.namelog->voteCount++; + ent->client->pers.vote |= 1 << team; + G_Vote( ent, team, qtrue ); +} + +/* +================== +Cmd_Vote_f +================== +*/ +void Cmd_Vote_f( gentity_t *ent ) +{ + char cmd[ MAX_TOKEN_CHARS ], vote[ MAX_TOKEN_CHARS ]; + team_t team = ent->client->pers.teamSelection; + + trap_Argv( 0, cmd, sizeof( cmd ) ); + if( Q_stricmp( cmd, "teamvote" ) ) + team = TEAM_NONE; + + if( !level.voteTime[ team ] ) + { + trap_SendServerCommand( ent-g_entities, + va( "print \"%s: no vote in progress\n\"", cmd ) ); + return; + } + + if( ent->client->pers.voted & ( 1 << team ) ) + { + trap_SendServerCommand( ent-g_entities, + va( "print \"%s: vote already cast\n\"", cmd ) ); + return; + } + + trap_SendServerCommand( ent-g_entities, + va( "print \"%s: vote cast\n\"", cmd ) ); + + trap_Argv( 1, vote, sizeof( vote ) ); + if( vote[ 0 ] == 'y' ) + ent->client->pers.vote |= 1 << team; + else + ent->client->pers.vote &= ~( 1 << team ); + G_Vote( ent, team, qtrue ); +} + + +/* +================= +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, "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) + +static qboolean G_RoomForClassChange( gentity_t *ent, class_t class, + vec3_t newOrigin ) +{ + vec3_t fromMins, fromMaxs; + vec3_t toMins, toMaxs; + vec3_t temp; + trace_t tr; + float nudgeHeight; + float maxHorizGrowth; + class_t oldClass = ent->client->ps.stats[ STAT_CLASS ]; + + BG_ClassBoundingBox( oldClass, fromMins, fromMaxs, NULL, NULL, NULL ); + BG_ClassBoundingBox( 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 ] -= toMins[ 2 ] - 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 + return ( !tr.startsolid && tr.fraction == 1.0f ); +} + +/* +================= +Cmd_Class_f +================= +*/ +void Cmd_Class_f( gentity_t *ent ) +{ + char s[ MAX_TOKEN_CHARS ]; + int clientNum; + int i; + vec3_t infestOrigin; + class_t currentClass = ent->client->pers.classSelection; + class_t newClass; + 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; + int oldBoostTime = -1; + vec3_t oldVel; + + clientNum = ent->client - level.clients; + trap_Argv( 1, s, sizeof( s ) ); + newClass = BG_ClassByName( s )->number; + + if( ent->client->sess.spectatorState != SPECTATOR_NOT ) + { + if( ent->client->sess.spectatorState == SPECTATOR_FOLLOW ) + G_StopFollowing( ent ); + if( ent->client->pers.teamSelection == TEAM_ALIENS ) + { + if( newClass != PCL_ALIEN_BUILDER0 && + newClass != PCL_ALIEN_BUILDER0_UPG && + newClass != PCL_ALIEN_LEVEL0 ) + { + G_TriggerMenuArgs( ent->client->ps.clientNum, MN_A_CLASSNOTSPAWN, newClass ); + return; + } + + if( !BG_ClassIsAllowed( newClass ) ) + { + G_TriggerMenuArgs( ent->client->ps.clientNum, MN_A_CLASSNOTALLOWED, newClass ); + return; + } + + if( !BG_ClassAllowedInStage( newClass, g_alienStage.integer ) ) + { + G_TriggerMenuArgs( ent->client->ps.clientNum, MN_A_CLASSNOTATSTAGE, newClass ); + return; + } + + // spawn from an egg + if( G_PushSpawnQueue( &level.alienSpawnQueue, clientNum ) ) + { + ent->client->pers.classSelection = newClass; + ent->client->ps.stats[ STAT_CLASS ] = newClass; + } + } + else if( ent->client->pers.teamSelection == TEAM_HUMANS ) + { + //set the item to spawn with + if( !Q_stricmp( s, BG_Weapon( WP_MACHINEGUN )->name ) && + BG_WeaponIsAllowed( WP_MACHINEGUN ) ) + { + ent->client->pers.humanItemSelection = WP_MACHINEGUN; + } + else if( !Q_stricmp( s, BG_Weapon( WP_HBUILD )->name ) && + BG_WeaponIsAllowed( WP_HBUILD ) ) + { + ent->client->pers.humanItemSelection = WP_HBUILD; + } + else + { + G_TriggerMenu( ent->client->ps.clientNum, MN_H_UNKNOWNSPAWNITEM ); + return; + } + // spawn from a telenode + if( G_PushSpawnQueue( &level.humanSpawnQueue, clientNum ) ) + { + ent->client->pers.classSelection = PCL_HUMAN; + ent->client->ps.stats[ STAT_CLASS ] = PCL_HUMAN; + } + } + return; + } + + if( ent->health <= 0 ) + return; + + if( ent->client->pers.teamSelection == TEAM_ALIENS ) + { + if( newClass == PCL_NONE ) + { + G_TriggerMenu( ent->client->ps.clientNum, MN_A_UNKNOWNCLASS ); + return; + } + + //if we are not currently spectating, we are attempting evolution + if( ent->client->pers.classSelection != PCL_NONE ) + { + int cost; + + //check that we have an overmind + if( !G_Overmind( ) ) + { + G_TriggerMenu( clientNum, MN_A_NOOVMND_EVOLVE ); + 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_TEAM ] == TEAM_HUMANS ) || + ( other->s.eType == ET_BUILDABLE && other->buildableTeam == TEAM_HUMANS && + other->powered ) ) + { + trace_t tr; + + trap_Trace( &tr, ent->client->ps.origin, NULL, NULL, other->s.origin, ent->client->ps.clientNum, MASK_SOLID ); + + if( tr.fraction > 0.99f ) + { + G_TriggerMenu( clientNum, MN_A_TOOCLOSE ); + return; + } + } + } + + //check that we are not wallwalking + if( ent->client->ps.eFlags & EF_WALLCLIMB ) + { + G_TriggerMenu( clientNum, MN_A_EVOLVEWALLWALK ); + return; + } + + //guard against selling the HBUILD weapons exploit + if( ent->client->sess.spectatorState == SPECTATOR_NOT && + ( currentClass == PCL_ALIEN_BUILDER0 || + currentClass == PCL_ALIEN_BUILDER0_UPG ) && + ent->client->buildTimer ) + { + G_TriggerMenu( ent->client->ps.clientNum, MN_A_EVOLVEBUILDTIMER ); + return; + } + + cost = BG_ClassCanEvolveFromTo( currentClass, newClass, + ent->client->pers.credit, + g_alienStage.integer, 0 ); + + if( G_RoomForClassChange( ent, newClass, infestOrigin ) ) + { + if( cost >= 0 ) + { + + ent->client->pers.evolveHealthFraction = (float)ent->client->ps.stats[ STAT_HEALTH ] / + (float)BG_Class( currentClass )->health; + + 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, -cost, qtrue ); + ent->client->pers.classSelection = newClass; + ClientUserinfoChanged( clientNum, qfalse ); + VectorCopy( infestOrigin, ent->s.pos.trBase ); + VectorCopy( ent->client->ps.velocity, oldVel ); + + if( ent->client->ps.stats[ STAT_STATE ] & SS_BOOSTED ) + oldBoostTime = ent->client->boostedTime; + + ClientSpawn( ent, ent, ent->s.pos.trBase, ent->s.apos.trBase ); + + VectorCopy( oldVel, ent->client->ps.velocity ); + if( oldBoostTime > 0 ) + { + ent->client->boostedTime = oldBoostTime; + ent->client->ps.stats[ STAT_STATE ] |= SS_BOOSTED; + } + } + else + G_TriggerMenuArgs( clientNum, MN_A_CANTEVOLVE, newClass ); + } + else + G_TriggerMenu( clientNum, MN_A_NOEROOM ); + } + } + else if( ent->client->pers.teamSelection == TEAM_HUMANS ) + G_TriggerMenu( clientNum, MN_H_DEADTOCLASS ); +} + + +/* +================= +Cmd_Destroy_f +================= +*/ +void Cmd_Destroy_f( gentity_t *ent ) +{ + vec3_t viewOrigin, forward, end; + trace_t tr; + gentity_t *traceEnt; + char cmd[ 12 ]; + qboolean deconstruct = qtrue; + qboolean lastSpawn = qfalse; + + if( ent->client->pers.namelog->denyBuild ) + { + G_TriggerMenu( ent->client->ps.clientNum, MN_B_REVOKED ); + return; + } + + trap_Argv( 0, cmd, sizeof( cmd ) ); + if( Q_stricmp( cmd, "destroy" ) == 0 ) + deconstruct = qfalse; + + BG_GetClientViewOrigin( &ent->client->ps, viewOrigin ); + AngleVectors( ent->client->ps.viewangles, forward, NULL, NULL ); + VectorMA( viewOrigin, 100, forward, end ); + + trap_Trace( &tr, viewOrigin, NULL, NULL, end, ent->s.number, MASK_PLAYERSOLID ); + traceEnt = &g_entities[ tr.entityNum ]; + + if( tr.fraction < 1.0f && + ( traceEnt->s.eType == ET_BUILDABLE ) && + ( traceEnt->buildableTeam == ent->client->pers.teamSelection ) && + ( ( ent->client->ps.weapon >= WP_ABUILD ) && + ( ent->client->ps.weapon <= WP_HBUILD ) ) ) + { + // Always let the builder prevent the explosion + if( traceEnt->health <= 0 ) + { + G_QueueBuildPoints( traceEnt ); + G_RewardAttackers( traceEnt ); + G_FreeEntity( traceEnt ); + return; + } + + // Cancel deconstruction (unmark) + if( deconstruct && g_markDeconstruct.integer && traceEnt->deconstruct ) + { + traceEnt->deconstruct = qfalse; + return; + } + + // Prevent destruction of the last spawn + if( ent->client->pers.teamSelection == TEAM_ALIENS && + traceEnt->s.modelindex == BA_A_SPAWN ) + { + if( level.numAlienSpawns <= 1 ) + lastSpawn = qtrue; + } + else if( ent->client->pers.teamSelection == TEAM_HUMANS && + traceEnt->s.modelindex == BA_H_SPAWN ) + { + if( level.numHumanSpawns <= 1 ) + lastSpawn = qtrue; + } + + if( lastSpawn && !g_cheats.integer && + !g_markDeconstruct.integer ) + { + G_TriggerMenu( ent->client->ps.clientNum, MN_B_LASTSPAWN ); + return; + } + + // Don't allow destruction of buildables that cannot be rebuilt + if( G_TimeTilSuddenDeath( ) <= 0 ) + { + G_TriggerMenu( ent->client->ps.clientNum, MN_B_SUDDENDEATH ); + return; + } + + if( !g_markDeconstruct.integer || + ( ent->client->pers.teamSelection == TEAM_HUMANS && + !G_FindPower( traceEnt, qtrue ) ) ) + { + if( ent->client->buildTimer ) + { + G_AddEvent( ent, EV_BUILD_DELAY, ent->client->ps.clientNum ); + return; + } + } + + if( traceEnt->health > 0 ) + { + if( !deconstruct ) + { + G_Damage( traceEnt, ent, ent, forward, tr.endpos, + traceEnt->health, 0, MOD_SUICIDE ); + } + else if( g_markDeconstruct.integer && + ( ent->client->pers.teamSelection != TEAM_HUMANS || + G_FindPower( traceEnt , qtrue ) || lastSpawn ) ) + { + traceEnt->deconstruct = qtrue; // Mark buildable for deconstruction + traceEnt->deconstructTime = level.time; + } + else + { + if( !g_cheats.integer && !g_instantBuild.integer ) // add a bit to the build timer + { + ent->client->buildTimer += + BG_Buildable( traceEnt->s.modelindex, traceEnt->cuboidSize )->buildTime / 4; + G_RecalcBuildTimer(ent->client); + } + G_Damage( traceEnt, ent, ent, forward, tr.endpos, + traceEnt->health, 0, MOD_DECONSTRUCT ); + G_FreeEntity( traceEnt ); + } + } + } +} + +/* +================= +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 ) ); + + // "weapon" aliased to whatever weapon you have + if( !Q_stricmp( "weapon", s ) ) + { + if( ent->client->ps.weapon == WP_BLASTER && + BG_PlayerCanChangeWeapon( &ent->client->ps ) ) + G_ForceWeaponChange( ent, WP_NONE ); + return; + } + + upgrade = BG_UpgradeByName( s )->number; + weapon = BG_WeaponByName( s )->number; + + 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 ) ) + { + if( ent->client->ps.weapon != weapon && + BG_PlayerCanChangeWeapon( &ent->client->ps ) ) + 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 ]; + upgrade_t upgrade; + + trap_Argv( 1, s, sizeof( s ) ); + upgrade = BG_UpgradeByName( s )->number; + + if( BG_InventoryContainsUpgrade( upgrade, ent->client->ps.stats ) ) + { + BG_DeactivateUpgrade( upgrade, ent->client->ps.stats ); + if( upgrade == UP_JETPACK ) + BG_AddPredictableEventToPlayerstate( EV_JETPACK_DEACTIVATE, 0, &ent->client->ps ); + } + 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 ]; + weapon_t weapon; + upgrade_t upgrade; + + trap_Argv( 1, s, sizeof( s ) ); + upgrade = BG_UpgradeByName( s )->number; + weapon = BG_WeaponByName( s )->number; + + if( weapon != WP_NONE ) + { + if( !BG_PlayerCanChangeWeapon( &ent->client->ps ) ) + return; + + //special case to allow switching between + //the blaster and the primary weapon + if( ent->client->ps.weapon != WP_BLASTER ) + weapon = WP_BLASTER; + else + weapon = WP_NONE; + + 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 ); + if( upgrade == UP_JETPACK ) + BG_AddPredictableEventToPlayerstate( EV_JETPACK_DEACTIVATE, 0, &ent->client->ps ); + } + 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 ]; + weapon_t weapon; + upgrade_t upgrade; + qboolean energyOnly, sellHelmet = qfalse; + + trap_Argv( 1, s, sizeof( s ) ); + + weapon = BG_WeaponByName( s )->number; + upgrade = BG_UpgradeByName( s )->number; + + if( upgrade == UP_NONE && !Q_stricmp(s, "helmet") ) + { + if( g_humanStage.integer < S2 ) + upgrade = UP_HELMET_MK1; + else + upgrade = UP_HELMET_MK2; + sellHelmet = qtrue; + } + + // Only give energy from reactors or repeaters + if( G_BuildableRange( ent->client->ps.origin, 100, BA_H_ARMOURY ) ) + energyOnly = qfalse; + else if( upgrade == UP_AMMO && + BG_Weapon( ent->client->ps.stats[ STAT_WEAPON ] )->usesEnergy && + ( G_BuildableRange( ent->client->ps.origin, 100, BA_H_REACTOR ) || + G_BuildableRange( ent->client->ps.origin, 100, BA_H_REPEATER ) ) ) + energyOnly = qtrue; + else + { + if( upgrade == UP_AMMO && + BG_Weapon( ent->client->ps.weapon )->usesEnergy ) + G_TriggerMenu( ent->client->ps.clientNum, MN_H_NOENERGYAMMOHERE ); + else + G_TriggerMenu( ent->client->ps.clientNum, MN_H_NOARMOURYHERE ); + 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; + } + + // Only humans can buy stuff + if( BG_Weapon( weapon )->team != TEAM_HUMANS ) + { + trap_SendServerCommand( ent-g_entities, "print \"You can't buy alien items\n\"" ); + return; + } + + //are we /allowed/ to buy this? + if( !BG_Weapon( weapon )->purchasable ) + { + trap_SendServerCommand( ent-g_entities, "print \"You can't buy this item\n\"" ); + return; + } + + //are we /allowed/ to buy this? + if( !BG_WeaponAllowedInStage( weapon, g_humanStage.integer ) || !BG_WeaponIsAllowed( weapon ) ) + { + trap_SendServerCommand( ent-g_entities, "print \"You can't buy this item\n\"" ); + return; + } + + //can afford this? + if( BG_Weapon( weapon )->price > (short)ent->client->pers.credit ) + { + G_TriggerMenu( ent->client->ps.clientNum, MN_H_NOFUNDS ); + return; + } + + //have space to carry this? + if( BG_Weapon( weapon )->slots & BG_SlotsForInventory( ent->client->ps.stats ) ) + { + G_TriggerMenu( ent->client->ps.clientNum, MN_H_NOSLOTS ); + return; + } + + // In some instances, weapons can't be changed + if( !BG_PlayerCanChangeWeapon( &ent->client->ps ) ) + return; + + ent->client->ps.stats[ STAT_WEAPON ] = weapon; + ent->client->ps.ammo = BG_Weapon( weapon )->maxAmmo; + ent->client->ps.clips = BG_Weapon( weapon )->maxClips; + + if( BG_Weapon( weapon )->usesEnergy && + BG_InventoryContainsUpgrade( UP_BATTPACK, ent->client->ps.stats ) ) + ent->client->ps.ammo *= BATTPACK_MODIFIER; + + G_ForceWeaponChange( ent, weapon ); + + //set build delay/pounce etc to 0 + ent->client->ps.stats[ STAT_MISC ] = 0; + ent->client->buildTimer = 0; + + //subtract from funds + G_AddCreditToClient( ent->client, -(short)BG_Weapon( weapon )->price, qfalse ); + } + else if( upgrade != UP_NONE ) + { + //already got this? + if( !sellHelmet && BG_InventoryContainsUpgrade( upgrade, ent->client->ps.stats ) ) + { + G_TriggerMenu( ent->client->ps.clientNum, MN_H_ITEMHELD ); + return; + } + + //can afford this? + if( BG_Upgrade( upgrade )->price > (short)ent->client->pers.credit ) + { + G_TriggerMenu( ent->client->ps.clientNum, MN_H_NOFUNDS ); + return; + } + + //have space to carry this? + if( !sellHelmet && BG_Upgrade( upgrade )->slots & BG_SlotsForInventory( ent->client->ps.stats ) ) + { + G_TriggerMenu( ent->client->ps.clientNum, MN_H_NOSLOTS ); + return; + } + + // Only humans can buy stuff + if( BG_Upgrade( upgrade )->team != TEAM_HUMANS ) + { + trap_SendServerCommand( ent-g_entities, "print \"You can't buy alien items\n\"" ); + return; + } + + //are we /allowed/ to buy this? + if( !BG_Upgrade( upgrade )->purchasable ) + { + trap_SendServerCommand( ent-g_entities, "print \"You can't buy this item\n\"" ); + return; + } + + //are we /allowed/ to buy this? + if( !BG_UpgradeAllowedInStage( upgrade, g_humanStage.integer ) || !BG_UpgradeIsAllowed( upgrade ) ) + { + trap_SendServerCommand( ent-g_entities, "print \"You can't buy this item\n\"" ); + return; + } + + if( upgrade == UP_AMMO ) + { + G_GiveClientMaxAmmo( ent, energyOnly ); + if( !energyOnly && BG_InventoryContainsUpgrade( UP_JETPACK, ent->client->ps.stats ) && + ent->client->ps.stats[ STAT_FUEL ] < JETPACK_FUEL_FULL ) + { + G_AddEvent( ent, EV_JETPACK_REFUEL, 0) ; + ent->client->ps.stats[ STAT_FUEL ] = JETPACK_FUEL_FULL; + } + } + else + { + if( upgrade == UP_BATTLESUIT ) + { + vec3_t newOrigin; + + if( !G_RoomForClassChange( ent, PCL_HUMAN_BSUIT, newOrigin ) ) + { + G_TriggerMenu( ent->client->ps.clientNum, MN_H_NOROOMBSUITON ); + return; + } + VectorCopy( newOrigin, ent->client->ps.origin ); + ent->client->ps.stats[ STAT_CLASS ] = PCL_HUMAN_BSUIT; + ent->client->pers.classSelection = PCL_HUMAN_BSUIT; + ent->client->ps.eFlags ^= EF_TELEPORT_BIT; + } + else if( upgrade == UP_JETPACK ) + ent->client->ps.stats[ STAT_FUEL ] = JETPACK_FUEL_FULL; + + if( sellHelmet ) + { + BG_RemoveUpgradeFromInventory( UP_HELMET_MK1, ent->client->ps.stats ); + BG_RemoveUpgradeFromInventory( UP_HELMET_MK2, ent->client->ps.stats ); + } + + //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_Upgrade( upgrade )->price, qfalse ); + } + else + G_TriggerMenu( ent->client->ps.clientNum, MN_H_UNKNOWNITEM ); + + //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; + weapon_t weapon; + upgrade_t upgrade; + + trap_Argv( 1, s, sizeof( s ) ); + + //no armoury nearby + if( !G_BuildableRange( ent->client->ps.origin, 100, BA_H_ARMOURY ) ) + { + G_TriggerMenu( ent->client->ps.clientNum, MN_H_NOARMOURYHERE ); + return; + } + + if( !Q_stricmpn( s, "weapon", 6 ) ) + weapon = ent->client->ps.stats[ STAT_WEAPON ]; + else + weapon = BG_WeaponByName( s )->number; + + upgrade = BG_UpgradeByName( s )->number; + + if( !upgrade && !Q_stricmp( s, "helmet" ) ) + { + if( BG_InventoryContainsUpgrade( UP_HELMET_MK1, ent->client->ps.stats ) ) + upgrade = UP_HELMET_MK1; + else if( BG_InventoryContainsUpgrade( UP_HELMET_MK2, ent->client->ps.stats ) ) + upgrade = UP_HELMET_MK2; + } + + if( weapon != WP_NONE ) + { + weapon_t selected = BG_GetPlayerWeapon( &ent->client->ps ); + + if( !BG_PlayerCanChangeWeapon( &ent->client->ps ) ) + return; + + //are we /allowed/ to sell this? + if( !BG_Weapon( weapon )->purchasable ) + { + trap_SendServerCommand( ent-g_entities, "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 && ent->client->buildTimer ) + { + G_TriggerMenu( ent->client->ps.clientNum, MN_H_ARMOURYBUILDTIMER ); + return; + } + + ent->client->ps.stats[ STAT_WEAPON ] = WP_NONE; + // Cancel ghost buildables + ent->client->ps.stats[ STAT_BUILDABLE ] = BA_NONE; + + //add to funds + G_AddCreditToClient( ent->client, (short)BG_Weapon( weapon )->price, qfalse ); + } + + //if we have this weapon selected, force a new selection + if( weapon == selected ) + G_ForceWeaponChange( ent, WP_NONE ); + } + else if( upgrade != UP_NONE ) + { + //are we /allowed/ to sell this? + if( !BG_Upgrade( upgrade )->purchasable ) + { + trap_SendServerCommand( ent-g_entities, "print \"You can't sell this item\n\"" ); + return; + } + //remove upgrade if carried + if( BG_InventoryContainsUpgrade( upgrade, ent->client->ps.stats ) ) + { + // shouldn't really need to test for this, but just to be safe + if( upgrade == UP_BATTLESUIT ) + { + vec3_t newOrigin; + + if( !G_RoomForClassChange( ent, PCL_HUMAN, newOrigin ) ) + { + G_TriggerMenu( ent->client->ps.clientNum, MN_H_NOROOMBSUITOFF ); + return; + } + VectorCopy( newOrigin, ent->client->ps.origin ); + ent->client->ps.stats[ STAT_CLASS ] = PCL_HUMAN; + ent->client->pers.classSelection = PCL_HUMAN; + ent->client->ps.eFlags ^= EF_TELEPORT_BIT; + } + + //add to inventory + BG_RemoveUpgradeFromInventory( upgrade, ent->client->ps.stats ); + + if( upgrade == UP_BATTPACK ) + G_GiveClientMaxAmmo( ent, qtrue ); + + //add to funds + G_AddCreditToClient( ent->client, (short)BG_Upgrade( upgrade )->price, qfalse ); + } + } + else if( !Q_stricmp( s, "weapons" ) ) + { + weapon_t selected = BG_GetPlayerWeapon( &ent->client->ps ); + + if( !BG_PlayerCanChangeWeapon( &ent->client->ps ) ) + return; + + for( i = WP_NONE + 1; i < WP_NUM_WEAPONS; i++ ) + { + //guard against selling the HBUILD weapons exploit + if( i == WP_HBUILD && ent->client->buildTimer ) + { + G_TriggerMenu( ent->client->ps.clientNum, MN_H_ARMOURYBUILDTIMER ); + continue; + } + + if( BG_InventoryContainsWeapon( i, ent->client->ps.stats ) && + BG_Weapon( i )->purchasable ) + { + ent->client->ps.stats[ STAT_WEAPON ] = WP_NONE; + + //add to funds + G_AddCreditToClient( ent->client, (short)BG_Weapon( i )->price, qfalse ); + } + + //if we have this weapon selected, force a new selection + if( i == selected ) + 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_Upgrade( i )->purchasable ) + { + + // shouldn't really need to test for this, but just to be safe + if( i == UP_BATTLESUIT ) + { + vec3_t newOrigin; + + if( !G_RoomForClassChange( ent, PCL_HUMAN, newOrigin ) ) + { + G_TriggerMenu( ent->client->ps.clientNum, MN_H_NOROOMBSUITOFF ); + continue; + } + VectorCopy( newOrigin, ent->client->ps.origin ); + ent->client->ps.stats[ STAT_CLASS ] = PCL_HUMAN; + ent->client->pers.classSelection = PCL_HUMAN; + ent->client->ps.eFlags ^= EF_TELEPORT_BIT; + } + + BG_RemoveUpgradeFromInventory( i, ent->client->ps.stats ); + + if( i == UP_BATTPACK ) + G_GiveClientMaxAmmo( ent, qtrue ); + + //add to funds + G_AddCreditToClient( ent->client, (short)BG_Upgrade( i )->price, qfalse ); + } + } + } + else + G_TriggerMenu( ent->client->ps.clientNum, MN_H_UNKNOWNITEM ); + + //update ClientInfo + ClientUserinfoChanged( ent->client->ps.clientNum, qfalse ); +} + +/* +================= +Cmd_CheckCuboidSize + +Check if the specified dimensions are valid. +================= +*/ +qboolean Cmd_CheckCuboidSize(vec3_t dims) +{ + if(g_cuboidSizeLimit.integer) + if(dims[0]>g_cuboidSizeLimit.integer||dims[1]>g_cuboidSizeLimit.integer||dims[2]>g_cuboidSizeLimit.integer) + return qfalse; + if(dims[0]*dims[1]*dims[2]<CUBOID_MINVOLUME) + return qfalse; + if(dims[0]<1||dims[1]<1||dims[2]<1) + return qfalse; + return qtrue; +} + +/* +================= +Cmd_Cb_f + +Sent by cgame in background (not directly by a person). +Update player's cuboid selection (after validation) with data sent over network. +================= +*/ +void Cmd_Cb_f(gentity_t *ent) +{ + char s[MAX_TOKEN_CHARS]; + int echo; + vec3_t dims; + + if(trap_Argc()!=5) + return; + trap_Argv(1,s,sizeof(s)); + echo=atoi(s); + trap_Argv(2,s,sizeof(s)); + dims[0]=atof(s); + trap_Argv(3,s,sizeof(s)); + dims[1]=atof(s); + trap_Argv(4,s,sizeof(s)); + dims[2]=atof(s); + if(Cmd_CheckCuboidSize(dims)) + { + VectorCopy(dims,ent->client->cuboidSelection); + trap_SendServerCommand(ent->client-level.clients,va("cb3 %i\n",echo)); + G_RelayCuboidToSpectators(ent->client); + } + else + { + if(Cmd_CheckCuboidSize(ent->client->cuboidSelection)) + trap_SendServerCommand(ent->client-level.clients,va("cb3 %i %f %f %f\n",echo,ent->client->cuboidSelection[0],ent->client->cuboidSelection[1],ent->client->cuboidSelection[2])); + else + trap_SendServerCommand(ent->client-level.clients,va("cb3 %i %f %f %f\n",echo,g_cuboidSizeLimit.integer,g_cuboidSizeLimit.integer,g_cuboidSizeLimit.integer)); + } +} + + +/* +================= +Cmd_Build_f +================= +*/ +void Cmd_Build_f( gentity_t *ent ) +{ + char s[ MAX_TOKEN_CHARS ]; + buildable_t buildable; + float dist; + vec3_t origin, normal; + team_t team; + char buf[128]; + vec3_t dims; + + if( ent->client->pers.namelog->denyBuild ) + { + G_TriggerMenu( ent->client->ps.clientNum, MN_B_REVOKED ); + return; + } + + if( ent->client->pers.teamSelection == level.surrenderTeam ) + { + G_TriggerMenu( ent->client->ps.clientNum, MN_B_SURRENDER ); + return; + } + + trap_Argv( 1, s, sizeof( s ) ); + + buildable = BG_BuildableByName( s )->number; + + /* To allow players build cuboids with completely arbitrary + * dimensions (the current resizing method doesn't provide + * much precision) the build command was extended with the + * following syntax: + * build [building] [X] [Y] [Z] + * where X, Y and Z are respectively the cuboid's length + * on the X, Y and Z axis. + */ + if( BG_Buildable(buildable,NULL)->cuboid ) + { + if( trap_Argc() >= 5 ) + { + trap_Argv(2,s,sizeof(s)); + dims[0]=MAX(1,atof(s)); + trap_Argv(3,s,sizeof(s)); + dims[1]=MAX(1,atof(s)); + trap_Argv(4,s,sizeof(s)); + dims[2]=MAX(1,atof(s)); + if(!Cmd_CheckCuboidSize(dims)) + { + Com_sprintf(buf,sizeof(buf),"print \"^1error: invalid cuboid size (min volume: %i, max size: %s)\n\"", + CUBOID_MINVOLUME,(g_cuboidSizeLimit.integer?va("%ix%ix%i",g_cuboidSizeLimit.integer,g_cuboidSizeLimit.integer, g_cuboidSizeLimit.integer):"no limit")); + trap_SendServerCommand(ent->client-level.clients,buf); + return; + } + VectorCopy(dims,ent->client->cuboidSelection); + } + // client is building a cuboid for the first time so reset the selection to default + if(!Cmd_CheckCuboidSize(ent->client->cuboidSelection)) + { + ent->client->cuboidSelection[0]=32; + ent->client->cuboidSelection[1]=32; + ent->client->cuboidSelection[2]=32; + trap_SendServerCommand(ent->client-level.clients,"cb2 32 32 32"); + G_RelayCuboidToSpectators(ent->client); + } + + if(!BG_CuboidAllowed((team==TEAM_ALIENS?g_alienStage.integer:g_humanStage.integer))) + { + if(BG_CuboidMode()==1) + G_TriggerMenu(ent->client->ps.clientNum,MN_B_CUBOID_MODE1); + else + G_TriggerMenu(ent->client->ps.clientNum,MN_B_CUBOID_MODE2); + return; + } + } + + if( G_TimeTilSuddenDeath( ) <= 0 ) + { + G_TriggerMenu( ent->client->ps.clientNum, MN_B_SUDDENDEATH ); + return; + } + + team = ent->client->ps.stats[ STAT_TEAM ]; + + if( buildable != BA_NONE && + ( ( 1 << ent->client->ps.weapon ) & BG_Buildable( buildable, NULL )->buildWeapon ) && + BG_BuildableIsAllowed( buildable ) && + ( ( team == TEAM_ALIENS && BG_BuildableAllowedInStage( buildable, g_alienStage.integer ) ) || + ( team == TEAM_HUMANS && BG_BuildableAllowedInStage( buildable, g_humanStage.integer ) ) ) ) + { + dynMenu_t err; + dist = BG_Class( ent->client->ps.stats[ STAT_CLASS ] )->buildDist; + + ent->client->ps.stats[ STAT_BUILDABLE ] = BA_NONE; + + //these are the errors displayed when the builder first selects something to use + switch( G_CanBuild( ent, buildable, dist, origin, normal, ent->client->cuboidSelection ) ) + { + // can place right away, set the blueprint and the valid togglebit + case IBE_NONE: + case IBE_TNODEWARN: + case IBE_RPTNOREAC: + case IBE_RPTPOWERHERE: + case IBE_SPWNWARN: + err = MN_NONE; + // we OR-in the selected builable later + ent->client->ps.stats[ STAT_BUILDABLE ] = SB_VALID_TOGGLEBIT; + break; + + // can't place yet but maybe soon: start with valid togglebit off + case IBE_NORMAL: + case IBE_NOCREEP: + case IBE_NOROOM: + case IBE_NOOVERMIND: + case IBE_NOPOWERHERE: + case IBE_NOSURF: + err = MN_NONE; + break; + + // more serious errors just pop a menu + case IBE_NOALIENBP: + err = MN_A_NOBP; + break; + + case IBE_ONEOVERMIND: + err = MN_A_ONEOVERMIND; + break; + + case IBE_ONEREACTOR: + err = MN_H_ONEREACTOR; + break; + + case IBE_NOHUMANBP: + err = MN_H_NOBP; + break; + + case IBE_NODCC: + err = MN_H_NODCC; + break; + + case IBE_PERMISSION: + err = MN_B_CANNOT; + break; + + case IBE_LASTSPAWN: + err = MN_B_LASTSPAWN; + break; + + case IBE_TOODENSE: + err = MN_B_TOODENSE; + break; + + default: + err = -1; // stop uninitialised warning + break; + } + + if( ( err == MN_A_NOBP || err == MN_H_NOBP ) && BG_Buildable(buildable,NULL)->cuboid ) + { + err = MN_NONE; + ent->client->ps.stats[ STAT_BUILDABLE ] = SB_VALID_TOGGLEBIT; + } + + if( err == MN_NONE || ent->client->pers.disableBlueprintErrors ) + { + trap_SendServerCommand(ent->client-level.clients,va("cb2 %f %f %f\n", + ent->client->cuboidSelection[0], + ent->client->cuboidSelection[1], + ent->client->cuboidSelection[2])); + G_RelayCuboidToSpectators(ent->client); + ent->client->ps.stats[ STAT_BUILDABLE ] |= buildable; + } + else + G_TriggerMenu( ent->client->ps.clientNum, err ); + } + else + G_TriggerMenu( ent->client->ps.clientNum, MN_B_CANNOT ); +} + +/* +================= +Cmd_Reload_f +================= +*/ +void Cmd_Reload_f( gentity_t *ent ) +{ + playerState_t *ps = &ent->client->ps; + int ammo; + + // weapon doesn't ever need reloading + if( BG_Weapon( ps->weapon )->infiniteAmmo ) + return; + + if( ps->clips <= 0 ) + return; + + if( BG_Weapon( ps->weapon )->usesEnergy && + BG_InventoryContainsUpgrade( UP_BATTPACK, ps->stats ) ) + ammo = BG_Weapon( ps->weapon )->maxAmmo * BATTPACK_MODIFIER; + else + ammo = BG_Weapon( ps->weapon )->maxAmmo; + + // don't reload when full + if( ps->ammo >= ammo ) + return; + + // the animation, ammo refilling etc. is handled by PM_Weapon + if( ent->client->ps.weaponstate != WEAPON_RELOADING ) + ent->client->ps.pm_flags |= PMF_WEAPON_RELOAD; +} + +/* +================= +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->client->ps.clientNum ) + { + 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.stats[ STAT_TEAM ] = ent->client->pers.teamSelection; + + if( ent->client->pers.teamSelection == TEAM_NONE ) + { + ent->client->sess.spectatorState = + ent->client->ps.persistant[ PERS_SPECSTATE ] = SPECTATOR_FREE; + } + else + { + vec3_t spawn_origin, spawn_angles; + + ent->client->sess.spectatorState = + ent->client->ps.persistant[ PERS_SPECSTATE ] = SPECTATOR_LOCKED; + + if( ent->client->pers.teamSelection == TEAM_ALIENS ) + G_SelectAlienLockSpawnPoint( spawn_origin, spawn_angles ); + else if( ent->client->pers.teamSelection == TEAM_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; + ent->client->ps.groundEntityNum = ENTITYNUM_NONE; + ent->client->ps.stats[ STAT_STATE ] = 0; + ent->client->ps.stats[ STAT_VIEWLOCK ] = 0; + ent->client->ps.eFlags &= ~( EF_WALLCLIMB | EF_WALLCLIMBCEILING ); + ent->client->ps.viewangles[ PITCH ] = 0.0f; + ent->client->ps.clientNum = ent - g_entities; + ent->client->ps.persistant[ PERS_CREDIT ] = ent->client->pers.credit; + + CalculateRanks( ); +} + +/* +================= +G_FollowLockView + +Client is still following a player, but that player has gone to spectator +mode and cannot be followed for the moment +================= +*/ +void G_FollowLockView( gentity_t *ent ) +{ + vec3_t spawn_origin, spawn_angles; + int clientNum; + + clientNum = ent->client->sess.spectatorClient; + ent->client->sess.spectatorState = + ent->client->ps.persistant[ PERS_SPECSTATE ] = SPECTATOR_FOLLOW; + ent->client->ps.clientNum = clientNum; + ent->client->ps.pm_flags &= ~PMF_FOLLOW; + ent->client->ps.stats[ STAT_TEAM ] = ent->client->pers.teamSelection; + ent->client->ps.stats[ STAT_STATE ] &= ~SS_WALLCLIMBING; + ent->client->ps.stats[ STAT_VIEWLOCK ] = 0; + ent->client->ps.eFlags &= ~( EF_WALLCLIMB | EF_WALLCLIMBCEILING ); + ent->client->ps.eFlags ^= EF_TELEPORT_BIT; + ent->client->ps.viewangles[ PITCH ] = 0.0f; + + // Put the view at the team spectator lock position + if( level.clients[ clientNum ].pers.teamSelection == TEAM_ALIENS ) + G_SelectAlienLockSpawnPoint( spawn_origin, spawn_angles ); + else if( level.clients[ clientNum ].pers.teamSelection == TEAM_HUMANS ) + G_SelectHumanLockSpawnPoint( spawn_origin, spawn_angles ); + + G_SetOrigin( ent, spawn_origin ); + VectorCopy( spawn_origin, ent->client->ps.origin ); + G_SetClientViewAngle( ent, spawn_angles ); +} + +/* +================= +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.spectatorState == SPECTATOR_NOT ) + 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; + + // can't follow self + if( &g_entities[ clientnum ] == ent ) + continue; + + // avoid selecting existing follow target + if( clientnum == original && !selectAny ) + continue; //effectively break; + + // can only follow connected clients + if( level.clients[ clientnum ].pers.connected != CON_CONNECTED ) + continue; + + // can't follow a spectator + if( level.clients[ clientnum ].pers.teamSelection == TEAM_NONE ) + continue; + + // if stickyspec is disabled, can't follow someone in queue either + if( !ent->client->pers.stickySpec && + level.clients[ clientnum ].sess.spectatorState != SPECTATOR_NOT ) + continue; + + // can only follow teammates when dead and on a team + if( ent->client->pers.teamSelection != TEAM_NONE && + ( level.clients[ clientnum ].pers.teamSelection != + ent->client->pers.teamSelection ) ) + continue; + + // this is good, we can use it + ent->client->sess.spectatorClient = clientnum; + ent->client->sess.spectatorState = SPECTATOR_FOLLOW; + + // if this client is in the spawn queue, we need to do something special + if( level.clients[ clientnum ].sess.spectatorState != SPECTATOR_NOT ) + G_FollowLockView( ent ); + + return qtrue; + + } while( clientnum != original ); + + return qfalse; +} + +/* +================= +G_ToggleFollow +================= +*/ +void G_ToggleFollow( gentity_t *ent ) +{ + 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; + char arg[ MAX_NAME_LENGTH ]; + + // won't work unless spectating + if( ent->client->sess.spectatorState == SPECTATOR_NOT ) + return; + + if( trap_Argc( ) != 2 ) + { + G_ToggleFollow( ent ); + } + else + { + char err[ MAX_STRING_CHARS ]; + trap_Argv( 1, arg, sizeof( arg ) ); + + i = G_ClientNumberFromString( arg, err, sizeof( err ) ); + + if( i == -1 ) + { + trap_SendServerCommand( ent - g_entities, + va( "print \"follow: %s\"", err ) ); + return; + } + + // can't follow self + if( &level.clients[ i ] == ent->client ) + return; + + // can't follow another spectator if sticky spec is off + if( !ent->client->pers.stickySpec && + level.clients[ i ].sess.spectatorState != SPECTATOR_NOT ) + return; + + // if not on team spectator, you can only follow teammates + if( ent->client->pers.teamSelection != TEAM_NONE && + ( level.clients[ i ].pers.teamSelection != + ent->client->pers.teamSelection ) ) + 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.spectatorState == SPECTATOR_NOT ) + return; + + G_FollowNewClient( ent, dir ); +} + +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]" + "usage: %s [clientNum | partial name match]\n\"", cmd ) ); + return; + } + + Q_strncpyz( name, ConcatArgs( 1 ), sizeof( name ) ); + matches = G_ClientNumbersFromString( name, pids, MAX_CLIENTS ); + 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( !Com_ClientListContains( &ent->client->sess.ignoreList, pids[ i ] ) ) + { + Com_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( Com_ClientListContains( &ent->client->sess.ignoreList, pids[ i ] ) ) + { + Com_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_ListMaps_f + +List all maps on the server +================= +*/ + +static int SortMaps( const void *a, const void *b ) +{ + return strcmp( *(char **)a, *(char **)b ); +} + +#define MAX_MAPLIST_MAPS 256 +#define MAX_MAPLIST_ROWS 9 +void Cmd_ListMaps_f( gentity_t *ent ) +{ + char search[ 16 ] = {""}; + char fileList[ 4096 ] = {""}; + char *fileSort[ MAX_MAPLIST_MAPS ]; + char *filePtr, *p; + int numFiles; + int fileLen = 0; + int shown = 0; + int count = 0; + int page = 0; + int pages; + int row, rows; + int start, i, j; + + if( trap_Argc( ) > 1 ) + { + trap_Argv( 1, search, sizeof( search ) ); + for( p = search; ( *p ) && isdigit( *p ); p++ ); + if( !( *p ) ) + { + page = atoi( search ); + search[ 0 ] = '\0'; + } + else if( trap_Argc( ) > 2 ) + { + char lp[ 8 ]; + trap_Argv( 2, lp, sizeof( lp ) ); + page = atoi( lp ); + } + + if( page > 0 ) + page--; + else if( page < 0 ) + page = 0; + } + + numFiles = trap_FS_GetFileList( "maps/", ".bsp", + fileList, sizeof( fileList ) ); + filePtr = fileList; + for( i = 0; i < numFiles && count < MAX_MAPLIST_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 + 2 ) / 3; + pages = MAX( 1, ( rows + MAX_MAPLIST_ROWS - 1 ) / MAX_MAPLIST_ROWS ); + if( page >= pages ) + page = pages - 1; + + start = page * MAX_MAPLIST_ROWS * 3; + if( count < start + ( 3 * MAX_MAPLIST_ROWS ) ) + rows = ( count - start + 2 ) / 3; + else + rows = MAX_MAPLIST_ROWS; + + ADMBP_begin( ); + for( row = 0; row < rows; row++ ) + { + for( i = start + row, j = 0; i < count && j < 3; i += rows, j++ ) + { + ADMBP( va( "^7 %-20s", fileSort[ i ] ) ); + shown++; + } + ADMBP( "\n" ); + } + if ( search[ 0 ] ) + ADMBP( va( "^3listmaps: ^7found %d maps matching '%s^7'", count, search ) ); + else + ADMBP( va( "^3listmaps: ^7listing %d of %d maps", shown, count ) ); + if( pages > 1 ) + ADMBP( va( ", page %d of %d", page + 1, pages ) ); + if( page + 1 < pages ) + ADMBP( va( ", use 'listmaps %s%s%d' to see more", + search, ( search[ 0 ] ) ? " ": "", page + 2 ) ); + ADMBP( ".\n" ); + ADMBP_end( ); +} + +/* +================= +Cmd_Test_f +================= +*/ +void Cmd_Test_f( gentity_t *humanPlayer ) +{ +} + +/* +================= +Cmd_Damage_f + +Deals damage to you (for testing), arguments: [damage] [dx] [dy] [dz] +The dx/dy arguments describe the damage point's offset from the entity origin +================= +*/ +void Cmd_Damage_f( gentity_t *ent ) +{ + vec3_t point; + char arg[ 16 ]; + float dx = 0.0f, dy = 0.0f, dz = 100.0f; + int damage = 100; + qboolean nonloc = qtrue; + + if( trap_Argc() > 1 ) + { + trap_Argv( 1, arg, sizeof( arg ) ); + damage = atoi( arg ); + } + if( trap_Argc() > 4 ) + { + trap_Argv( 2, arg, sizeof( arg ) ); + dx = atof( arg ); + trap_Argv( 3, arg, sizeof( arg ) ); + dy = atof( arg ); + trap_Argv( 4, arg, sizeof( arg ) ); + dz = atof( arg ); + nonloc = qfalse; + } + VectorCopy( ent->s.origin, point ); + point[ 0 ] += dx; + point[ 1 ] += dy; + point[ 2 ] += dz; + G_Damage( ent, NULL, NULL, NULL, point, damage, + ( nonloc ? DAMAGE_NO_LOCDAMAGE : 0 ), MOD_TARGET_LASER ); +} + +/* +================== +G_FloodLimited + +Determine whether a user is flood limited, and adjust their flood demerits +Print them a warning message if they are over the limit +Return is time in msec until the user can speak again +================== +*/ +int G_FloodLimited( gentity_t *ent ) +{ + int deltatime, ms; + + if( g_floodMinTime.integer <= 0 ) + return 0; + + // handles !ent + if( G_admin_permission( ent, ADMF_NOCENSORFLOOD ) ) + return 0; + + deltatime = level.time - ent->client->pers.floodTime; + + ent->client->pers.floodDemerits += g_floodMinTime.integer - deltatime; + if( ent->client->pers.floodDemerits < 0 ) + ent->client->pers.floodDemerits = 0; + ent->client->pers.floodTime = level.time; + + ms = ent->client->pers.floodDemerits - g_floodMaxDemerits.integer; + if( ms <= 0 ) + return 0; + trap_SendServerCommand( ent - g_entities, va( "print \"You are flooding: " + "please wait %d second%s before trying again\n", + ( ms + 999 ) / 1000, ( ms > 1000 ) ? "s" : "" ) ); + return ms; +} + +commands_t cmds[ ] = { + { "a", CMD_MESSAGE|CMD_INTERMISSION, Cmd_AdminMessage_f }, + { "build", CMD_TEAM|CMD_LIVING, Cmd_Build_f }, + { "buy", CMD_HUMAN|CMD_LIVING, Cmd_Buy_f }, + { "callteamvote", CMD_MESSAGE|CMD_TEAM, Cmd_CallVote_f }, + { "callvote", CMD_MESSAGE, Cmd_CallVote_f }, + { "cb", 0, Cmd_Cb_f }, //NOTE: it's a command used only by cgame + { "class", CMD_TEAM, Cmd_Class_f }, + { "damage", CMD_CHEAT|CMD_LIVING, Cmd_Damage_f }, + { "deconstruct", CMD_TEAM|CMD_LIVING, Cmd_Destroy_f }, + { "destroy", CMD_CHEAT|CMD_TEAM|CMD_LIVING, Cmd_Destroy_f }, + { "follow", CMD_SPEC, Cmd_Follow_f }, + { "follownext", CMD_SPEC, Cmd_FollowCycle_f }, + { "followprev", CMD_SPEC, Cmd_FollowCycle_f }, + { "give", CMD_CHEAT|CMD_TEAM|CMD_LIVING, Cmd_Give_f }, + { "god", CMD_CHEAT|CMD_TEAM|CMD_LIVING, Cmd_God_f }, + { "ignore", 0, Cmd_Ignore_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 }, + { "kill", CMD_TEAM|CMD_LIVING, Cmd_Kill_f }, + { "levelshot", CMD_CHEAT, Cmd_LevelShot_f }, + { "listmaps", CMD_MESSAGE|CMD_INTERMISSION, Cmd_ListMaps_f }, + { "m", CMD_MESSAGE|CMD_INTERMISSION, Cmd_PrivateMessage_f }, + { "mt", CMD_MESSAGE|CMD_INTERMISSION, Cmd_PrivateMessage_f }, + { "noclip", CMD_CHEAT_TEAM, Cmd_Noclip_f }, + { "notarget", CMD_CHEAT|CMD_TEAM|CMD_LIVING, Cmd_Notarget_f }, + { "reload", CMD_HUMAN|CMD_LIVING, Cmd_Reload_f }, + { "say", CMD_MESSAGE|CMD_INTERMISSION, Cmd_Say_f }, + { "say_area", CMD_MESSAGE|CMD_TEAM|CMD_LIVING, Cmd_SayArea_f }, + { "say_team", CMD_MESSAGE|CMD_INTERMISSION, Cmd_Say_f }, + { "score", CMD_INTERMISSION, ScoreboardMessage }, + { "sell", CMD_HUMAN|CMD_LIVING, Cmd_Sell_f }, + { "setviewpos", CMD_CHEAT_TEAM, Cmd_SetViewpos_f }, + { "team", 0, Cmd_Team_f }, + { "teamvote", CMD_TEAM, Cmd_Vote_f }, + { "test", CMD_CHEAT, Cmd_Test_f }, + { "unignore", 0, Cmd_Ignore_f }, + { "vote", 0, Cmd_Vote_f }, + { "vsay", CMD_MESSAGE|CMD_INTERMISSION, Cmd_VSay_f }, + { "vsay_local", CMD_MESSAGE|CMD_INTERMISSION, Cmd_VSay_f }, + { "vsay_team", CMD_MESSAGE|CMD_INTERMISSION, Cmd_VSay_f }, + { "where", 0, Cmd_Where_f } +}; +static size_t numCmds = sizeof( cmds ) / sizeof( cmds[ 0 ] ); + +/* +================= +ClientCommand +================= +*/ +void ClientCommand( int clientNum ) +{ + gentity_t *ent; + char cmd[ MAX_TOKEN_CHARS ]; + commands_t *command; + + ent = g_entities + clientNum; + if( !ent->client || ent->client->pers.connected != CON_CONNECTED ) + return; // not fully in game yet + + trap_Argv( 0, cmd, sizeof( cmd ) ); + + command = bsearch( cmd, cmds, numCmds, sizeof( cmds[ 0 ] ), cmdcmp ); + + if( !command ) + { + if( !G_admin_cmd_check( ent ) ) + trap_SendServerCommand( clientNum, + va( "print \"Unknown command %s\n\"", cmd ) ); + return; + } + + // do tests here to reduce the amount of repeated code + + if( !( command->cmdFlags & CMD_INTERMISSION ) && + ( level.intermissiontime || level.pausedTime ) ) + return; + + if( command->cmdFlags & CMD_CHEAT && !g_cheats.integer ) + { + G_TriggerMenu( clientNum, MN_CMD_CHEAT ); + return; + } + + if( command->cmdFlags & CMD_MESSAGE && ( ent->client->pers.namelog->muted || + G_FloodLimited( ent ) ) ) + return; + + if( command->cmdFlags & CMD_TEAM && + ent->client->pers.teamSelection == TEAM_NONE ) + { + G_TriggerMenu( clientNum, MN_CMD_TEAM ); + return; + } + + if( command->cmdFlags & CMD_CHEAT_TEAM && !g_cheats.integer && + ent->client->pers.teamSelection != TEAM_NONE ) + { + G_TriggerMenu( clientNum, MN_CMD_CHEAT_TEAM ); + return; + } + + if( command->cmdFlags & CMD_SPEC && + ent->client->sess.spectatorState == SPECTATOR_NOT ) + { + G_TriggerMenu( clientNum, MN_CMD_SPEC ); + return; + } + + if( command->cmdFlags & CMD_ALIEN && + ent->client->pers.teamSelection != TEAM_ALIENS ) + { + G_TriggerMenu( clientNum, MN_CMD_ALIEN ); + return; + } + + if( command->cmdFlags & CMD_HUMAN && + ent->client->pers.teamSelection != TEAM_HUMANS ) + { + G_TriggerMenu( clientNum, MN_CMD_HUMAN ); + return; + } + + if( command->cmdFlags & CMD_LIVING && + ( ent->client->ps.stats[ STAT_HEALTH ] <= 0 || + ent->client->sess.spectatorState != SPECTATOR_NOT ) ) + { + G_TriggerMenu( clientNum, MN_CMD_LIVING ); + return; + } + + command->cmdHandler( ent ); +} + +void G_ListCommands( gentity_t *ent ) +{ + int i; + char out[ MAX_STRING_CHARS ] = ""; + int len, outlen; + + outlen = 0; + + for( i = 0; i < numCmds; i++ ) + { + len = strlen( cmds[ i ].cmdName ) + 1; + if( len + outlen >= sizeof( out ) - 1 ) + { + trap_SendServerCommand( ent - g_entities, va( "cmds%s\n", out ) ); + outlen = 0; + } + + strcpy( out + outlen, va( " %s", cmds[ i ].cmdName ) ); + outlen += len; + } + + trap_SendServerCommand( ent - g_entities, va( "cmds%s\n", out ) ); + G_admin_cmdlist( ent ); +} + +void G_DecolorString( char *in, char *out, int len ) +{ + qboolean decolor = qtrue; + + len--; + + while( *in && len > 0 ) { + if( *in == DECOLOR_OFF || *in == DECOLOR_ON ) + { + decolor = ( *in == DECOLOR_ON ); + in++; + continue; + } + if( Q_IsColorString( in ) && decolor ) { + in += 2; + continue; + } + *out++ = *in++; + len--; + } + *out = '\0'; +} + +void G_UnEscapeString( char *in, char *out, int len ) +{ + len--; + + while( *in && len > 0 ) + { + if( *in >= ' ' || *in == '\n' ) + { + *out++ = *in; + len--; + } + in++; + } + *out = '\0'; +} + +void Cmd_PrivateMessage_f( gentity_t *ent ) +{ + int pids[ MAX_CLIENTS ]; + char name[ MAX_NAME_LENGTH ]; + char cmd[ 12 ]; + char text[ MAX_STRING_CHARS ]; + char *msg; + char color; + int i, pcount; + int count = 0; + qboolean teamonly = qfalse; + char recipients[ MAX_STRING_CHARS ] = ""; + + if( !g_privateMessages.integer && ent ) + { + ADMP( "Sorry, but private messages have been disabled\n" ); + return; + } + + trap_Argv( 0, cmd, sizeof( cmd ) ); + if( trap_Argc( ) < 3 ) + { + ADMP( va( "usage: %s [name|slot#] [message]\n", cmd ) ); + return; + } + + if( !Q_stricmp( cmd, "mt" ) ) + teamonly = qtrue; + + trap_Argv( 1, name, sizeof( name ) ); + msg = ConcatArgs( 2 ); + pcount = G_ClientNumbersFromString( name, pids, MAX_CLIENTS ); + + G_CensorString( text, msg, sizeof( text ), ent ); + + // send the message + for( i = 0; i < pcount; i++ ) + { + if( G_SayTo( ent, &g_entities[ pids[ i ] ], + teamonly ? SAY_TPRIVMSG : SAY_PRIVMSG, text ) ) + { + count++; + Q_strcat( recipients, sizeof( recipients ), va( "%s" S_COLOR_WHITE ", ", + level.clients[ pids[ i ] ].pers.netname ) ); + } + } + + // report the results + color = teamonly ? COLOR_CYAN : COLOR_YELLOW; + + if( !count ) + ADMP( va( "^3No player matching ^7\'%s^7\' ^3to send message to.\n", + name ) ); + else + { + ADMP( va( "^%cPrivate message: ^7%s\n", color, text ) ); + // remove trailing ", " + recipients[ strlen( recipients ) - 2 ] = '\0'; + ADMP( va( "^%csent to %i player%s: " S_COLOR_WHITE "%s\n", color, count, + count == 1 ? "" : "s", recipients ) ); + + G_LogPrintf( "%s: %d \"%s" S_COLOR_WHITE "\" \"%s\": ^%c%s\n", + ( teamonly ) ? "TPrivMsg" : "PrivMsg", + ( ent ) ? ent - g_entities : -1, + ( ent ) ? ent->client->pers.netname : "console", + name, color, msg ); + } +} + +/* +================= +Cmd_AdminMessage_f + +Send a message to all active admins +================= +*/ +void Cmd_AdminMessage_f( gentity_t *ent ) +{ + // Check permissions and add the appropriate user [prefix] + if( !G_admin_permission( ent, ADMF_ADMINCHAT ) ) + { + if( !g_publicAdminMessages.integer ) + { + ADMP( "Sorry, but use of /a by non-admins has been disabled.\n" ); + return; + } + else + { + ADMP( "Your message has been sent to any available admins " + "and to the server logs.\n" ); + } + } + + if( trap_Argc( ) < 2 ) + { + ADMP( "usage: a [message]\n" ); + return; + } + + G_AdminMessage( ent, ConcatArgs( 1 ) ); +} + diff --git a/src/game/g_combat.c b/src/game/g_combat.c new file mode 100644 index 0000000..c381f91 --- /dev/null +++ b/src/game/g_combat.c @@ -0,0 +1,1470 @@ +/* +=========================================================================== +Copyright (C) 1999-2005 Id Software, Inc. +Copyright (C) 2000-2009 Darklegion Development + +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_DAMAGE_REGIONS ]; +int g_numDamageRegions[ PCL_NUM_CLASSES ]; + +damageRegion_t g_armourRegions[ UP_NUM_UPGRADES ][ MAX_DAMAGE_REGIONS ]; +int g_numArmourRegions[ UP_NUM_UPGRADES ]; + +/* +============ +AddScore + +Adds score to the client +============ +*/ +void AddScore( gentity_t *ent, int score ) +{ + if( !ent->client ) + return; + + // make alien and human scores equivalent + if ( ent->client->pers.teamSelection == TEAM_ALIENS ) + { + score = rint( ((float)score) / 2.0f ); + } + + // scale values down to fit the scoreboard better + score = rint( ((float)score) / 50.0f ); + + ent->client->ps.persistant[ PERS_SCORE ] += score; + CalculateRanks( ); +} + +/* +================== +LookAtKiller +================== +*/ +void LookAtKiller( gentity_t *self, gentity_t *inflictor, gentity_t *attacker ) +{ + + if ( attacker && attacker != self ) + self->client->ps.stats[ STAT_VIEWLOCK ] = attacker - g_entities; + else if( inflictor && inflictor != self ) + self->client->ps.stats[ STAT_VIEWLOCK ] = inflictor - g_entities; + else + self->client->ps.stats[ STAT_VIEWLOCK ] = self - g_entities; +} + +// 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_TRAMPLE", + "MOD_LEVEL4_CRUSH", + + "MOD_SLOWBLOB", + "MOD_POISON", + "MOD_SWARM", + + "MOD_HSPAWN", + "MOD_TESLAGEN", + "MOD_MGTURRET", + "MOD_REACTOR", + + "MOD_ASPAWN", + "MOD_ATUBE", + "MOD_OVERMIND", + "MOD_DECONSTRUCT", + "MOD_REPLACE", + "MOD_NOCREEP" +}; + +/* +================== +G_RewardAttackers + +Function to distribute rewards to entities that killed this one. +Returns the total damage dealt. +================== +*/ +float G_RewardAttackers( gentity_t *self ) +{ + float value, totalDamage = 0; + int team, i, maxHealth = 0; + int alienCredits = 0, humanCredits = 0; + gentity_t *player; + + // Total up all the damage done by non-teammates + for( i = 0; i < level.maxclients; i++ ) + { + player = g_entities + i; + + if( !OnSameTeam( self, player ) || + self->buildableTeam != player->client->ps.stats[ STAT_TEAM ] ) + totalDamage += (float)self->credits[ i ]; + } + + if( totalDamage <= 0.0f ) + return 0.0f; + + // Only give credits for killing players and buildables + if( self->client ) + { + value = BG_GetValueOfPlayer( &self->client->ps ); + team = self->client->pers.teamSelection; + maxHealth = self->client->ps.stats[ STAT_MAX_HEALTH ]; + } + else if( self->s.eType == ET_BUILDABLE ) + { + value = BG_Buildable( self->s.modelindex, self->cuboidSize )->value; + + // only give partial credits for a buildable not yet completed + if( !self->spawned ) + { + value *= (float)( level.time - self->buildTime ) / + BG_Buildable( self->s.modelindex, self->cuboidSize )->buildTime; + } + + team = self->buildableTeam; + maxHealth = BG_Buildable( self->s.modelindex, self->cuboidSize )->health; + } + else + return totalDamage; + + // Give credits and empty the array + for( i = 0; i < level.maxclients; i++ ) + { + int stageValue = value * self->credits[ i ] / totalDamage; + player = g_entities + i; + + if( player->client->pers.teamSelection != team ) + { + if( totalDamage < maxHealth ) + stageValue *= totalDamage / maxHealth; + + if( !self->credits[ i ] || player->client->ps.stats[ STAT_TEAM ] == team ) + continue; + + AddScore( player, stageValue ); + + // killing buildables earns score, but not credits + if( self->s.eType != ET_BUILDABLE ) + { + G_AddCreditToClient( player->client, stageValue, qtrue ); + + // add to stage counters + if( player->client->ps.stats[ STAT_TEAM ] == TEAM_ALIENS ) + alienCredits += stageValue; + else if( player->client->ps.stats[ STAT_TEAM ] == TEAM_HUMANS ) + humanCredits += stageValue; + } + } + self->credits[ i ] = 0; + } + + if( alienCredits ) + { + trap_Cvar_Set( "g_alienCredits", + va( "%d", g_alienCredits.integer + alienCredits ) ); + trap_Cvar_Update( &g_alienCredits ); + } + if( humanCredits ) + { + trap_Cvar_Set( "g_humanCredits", + va( "%d", g_humanCredits.integer + humanCredits ) ); + trap_Cvar_Update( &g_humanCredits ); + } + + return totalDamage; +} + +/* +================== +player_die +================== +*/ +void player_die( gentity_t *self, gentity_t *inflictor, gentity_t *attacker, int damage, int meansOfDeath ) +{ + gentity_t *ent, *ent2; + int anim; + int killer; + int i; + char *killerName, *obit; + vec3_t dir; + + 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; + else + killerName = "<world>"; + } + else + { + killer = ENTITYNUM_WORLD; + killerName = "<world>"; + } + + if( meansOfDeath < 0 || meansOfDeath >= sizeof( modNames ) / sizeof( modNames[0] ) ) + // fall back on the number + obit = va( "%d", meansOfDeath ); + else + obit = modNames[ meansOfDeath ]; + + G_LogPrintf( "Die: %d %d %s: %s" S_COLOR_WHITE " killed %s\n", + killer, + self - g_entities, + obit, + killerName, + self->client->pers.netname ); + + // deactivate all upgrades + for( i = UP_NONE + 1; i < UP_NUM_UPGRADES; i++ ) + BG_DeactivateUpgrade( i, self->client->ps.stats ); + + // kill all player's buildables if they havent spawned yet + // this should eliminate build timer hacks for ever + dir[0] = dir[1] = 0.0f; + dir[2] = 1.0f; + + for( i = MAX_CLIENTS, ent = g_entities + i; i < level.num_entities; i++, ent++ ) + { + if( ent->s.eType != ET_BUILDABLE ) + continue; + + if( ent == self ) + continue; + + if( ent->spawned ) + continue; + + if( ent->builtBy != self->client->ps.clientNum ) + continue; + + G_Damage( ent, self, attacker, dir, dir, ent->health, 0, MOD_DECONSTRUCT ); + } + + // broadcast the death event to everyone + 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 + + self->enemy = attacker; + self->client->ps.persistant[ PERS_KILLED ]++; + + if( attacker && attacker->client ) + { + attacker->client->lastkilled_client = self->s.number; + + if( ( attacker == self || OnSameTeam( self, attacker ) ) && meansOfDeath != MOD_HSPAWN ) + { + //punish team kills and suicides + if( attacker->client->ps.stats[ STAT_TEAM ] == TEAM_ALIENS ) + { + G_AddCreditToClient( attacker->client, -ALIEN_TK_SUICIDE_PENALTY, qtrue ); + AddScore( attacker, -ALIEN_TK_SUICIDE_PENALTY ); + } + else if( attacker->client->ps.stats[ STAT_TEAM ] == TEAM_HUMANS ) + { + G_AddCreditToClient( attacker->client, -HUMAN_TK_SUICIDE_PENALTY, qtrue ); + AddScore( attacker, -HUMAN_TK_SUICIDE_PENALTY ); + } + } + } + else if( attacker->s.eType != ET_BUILDABLE ) + { + if( self->client->ps.stats[ STAT_TEAM ] == TEAM_ALIENS ) + AddScore( self, -ALIEN_TK_SUICIDE_PENALTY ); + else if( self->client->ps.stats[ STAT_TEAM ] == TEAM_HUMANS ) + AddScore( self, -HUMAN_TK_SUICIDE_PENALTY ); + } + + // give credits for killing this player + G_RewardAttackers( self ); + + 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.spectatorState == SPECTATOR_NOT ) + continue; + + if( client->sess.spectatorClient == self->s.number ) + ScoreboardMessage( g_entities + i ); + } + + 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 ]; + + if( meansOfDeath != MOD_ALIEN_HATCH ) //don't look at the alien that jumped out of your chest, it screws up the camera + 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; + + // clear misc + memset( self->client->ps.misc, 0, sizeof( self->client->ps.misc ) ); + + { + // 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 ); +} + +/* +=============== +G_ParseDmgScript +=============== +*/ +static int G_ParseDmgScript( damageRegion_t *regions, char *buf ) +{ + char *token; + float angleSpan, heightSpan; + int count; + + for( count = 0; ; count++ ) + { + token = COM_Parse( &buf ); + if( !token[ 0 ] ) + break; + + if( strcmp( token, "{" ) ) + { + COM_ParseError( "Missing {" ); + break; + } + + if( count >= MAX_DAMAGE_REGIONS ) + { + COM_ParseError( "Max damage regions exceeded" ); + break; + } + + // defaults + regions[ count ].name[ 0 ] = '\0'; + regions[ count ].minHeight = 0.0f; + regions[ count ].maxHeight = 1.0f; + regions[ count ].minAngle = 0.0f; + regions[ count ].maxAngle = 360.0f; + regions[ count ].modifier = 1.0f; + regions[ count ].crouch = qfalse; + + while( 1 ) + { + token = COM_ParseExt( &buf, qtrue ); + + if( !token[ 0 ] ) + { + COM_ParseError( "Unexpected end of file" ); + break; + } + + if( !Q_stricmp( token, "}" ) ) + { + break; + } + else if( !strcmp( token, "name" ) ) + { + token = COM_ParseExt( &buf, qfalse ); + if( token[ 0 ] ) + Q_strncpyz( regions[ count ].name, token, + sizeof( regions[ count ].name ) ); + } + else if( !strcmp( token, "minHeight" ) ) + { + token = COM_ParseExt( &buf, qfalse ); + if( !token[ 0 ] ) + strcpy( token, "0" ); + regions[ count ].minHeight = atof( token ); + } + else if( !strcmp( token, "maxHeight" ) ) + { + token = COM_ParseExt( &buf, qfalse ); + if( !token[ 0 ] ) + strcpy( token, "100" ); + regions[ count ].maxHeight = atof( token ); + } + else if( !strcmp( token, "minAngle" ) ) + { + token = COM_ParseExt( &buf, qfalse ); + if( !token[ 0 ] ) + strcpy( token, "0" ); + regions[ count ].minAngle = atoi( token ); + } + else if( !strcmp( token, "maxAngle" ) ) + { + token = COM_ParseExt( &buf, qfalse ); + if( !token[ 0 ] ) + strcpy( token, "360" ); + regions[ count ].maxAngle = atoi( token ); + } + else if( !strcmp( token, "modifier" ) ) + { + token = COM_ParseExt( &buf, qfalse ); + if( !token[ 0 ] ) + strcpy( token, "1.0" ); + regions[ count ].modifier = atof( token ); + } + else if( !strcmp( token, "crouch" ) ) + { + regions[ count ].crouch = qtrue; + } + else + { + COM_ParseWarning("Unknown token \"%s\"", token); + } + } + + // Angle portion covered + angleSpan = regions[ count ].maxAngle - regions[ count ].minAngle; + if( angleSpan < 0.0f ) + angleSpan += 360.0f; + angleSpan /= 360.0f; + + // Height portion covered + heightSpan = regions[ count ].maxHeight - regions[ count ].minHeight; + if( heightSpan < 0.0f ) + heightSpan = -heightSpan; + if( heightSpan > 1.0f ) + heightSpan = 1.0f; + + regions[ count ].area = angleSpan * heightSpan; + if( !regions[ count ].area ) + regions[ count ].area = 0.00001f; + } + + return count; +} + +/* +============ +GetRegionDamageModifier +============ +*/ +static float GetRegionDamageModifier( gentity_t *targ, int class, int piece ) +{ + damageRegion_t *regions, *overlap; + float modifier = 0.0f, areaSum = 0.0f; + int j, i; + qboolean crouch; + + crouch = targ->client->ps.pm_flags & PMF_DUCKED; + overlap = &g_damageRegions[ class ][ piece ]; + + if( g_debugDamage.integer > 2 ) + G_Printf( "GetRegionDamageModifier():\n" + ". bodyRegion = [%d %d %f %f] (%s)\n" + ". modifier = %f\n", + overlap->minAngle, overlap->maxAngle, + overlap->minHeight, overlap->maxHeight, + overlap->name, overlap->modifier ); + + // Find the armour layer modifier, assuming that none of the armour regions + // overlap and that any areas that are not covered have a modifier of 1.0 + for( j = UP_NONE + 1; j < UP_NUM_UPGRADES; j++ ) + { + if( !BG_InventoryContainsUpgrade( j, targ->client->ps.stats ) || + !g_numArmourRegions[ j ] ) + continue; + regions = g_armourRegions[ j ]; + + for( i = 0; i < g_numArmourRegions[ j ]; i++ ) + { + float overlapMaxA, regionMinA, regionMaxA, angleSpan, heightSpan, area; + + if( regions[ i ].crouch != crouch ) + continue; + + // Convert overlap angle to 0 to max + overlapMaxA = overlap->maxAngle - overlap->minAngle; + if( overlapMaxA < 0.0f ) + overlapMaxA += 360.0f; + + // Convert region angles to match overlap + regionMinA = regions[ i ].minAngle - overlap->minAngle; + if( regionMinA < 0.0f ) + regionMinA += 360.0f; + regionMaxA = regions[ i ].maxAngle - overlap->minAngle; + if( regionMaxA < 0.0f ) + regionMaxA += 360.0f; + + // Overlapping Angle portion + if( regionMinA <= regionMaxA ) + { + angleSpan = 0.0f; + if( regionMinA < overlapMaxA ) + { + if( regionMaxA > overlapMaxA ) + regionMaxA = overlapMaxA; + angleSpan = regionMaxA - regionMinA; + } + } + else + { + if( regionMaxA > overlapMaxA ) + regionMaxA = overlapMaxA; + angleSpan = regionMaxA; + if( regionMinA < overlapMaxA ) + angleSpan += overlapMaxA - regionMinA; + } + angleSpan /= 360.0f; + + // Overlapping height portion + heightSpan = MIN( overlap->maxHeight, regions[ i ].maxHeight ) - + MAX( overlap->minHeight, regions[ i ].minHeight ); + if( heightSpan < 0.0f ) + heightSpan = 0.0f; + if( heightSpan > 1.0f ) + heightSpan = 1.0f; + + if( g_debugDamage.integer > 2 ) + G_Printf( ". armourRegion = [%d %d %f %f] (%s)\n" + ". . modifier = %f\n" + ". . angleSpan = %f\n" + ". . heightSpan = %f\n", + regions[ i ].minAngle, regions[ i ].maxAngle, + regions[ i ].minHeight, regions[ i ].maxHeight, + regions[ i ].name, regions[ i ].modifier, + angleSpan, heightSpan ); + + areaSum += area = angleSpan * heightSpan; + modifier += regions[ i ].modifier * area; + } + } + + if( g_debugDamage.integer > 2 ) + G_Printf( ". areaSum = %f\n" + ". armourModifier = %f\n", areaSum, modifier ); + + return overlap->modifier * ( overlap->area + modifier - areaSum ); +} + +/* +============ +GetNonLocDamageModifier +============ +*/ +static float GetNonLocDamageModifier( gentity_t *targ, int class ) +{ + float modifier = 0.0f, area = 0.0f, scale = 0.0f; + int i; + qboolean crouch; + + // For every body region, use stretch-armor formula to apply armour modifier + // for any overlapping area that armour shares with the body region + crouch = targ->client->ps.pm_flags & PMF_DUCKED; + for( i = 0; i < g_numDamageRegions[ class ]; i++ ) + { + damageRegion_t *region; + + region = &g_damageRegions[ class ][ i ]; + + if( region->crouch != crouch ) + continue; + + modifier += GetRegionDamageModifier( targ, class, i ); + + scale += region->modifier * region->area; + area += region->area; + + } + + modifier = !scale ? 1.0f : 1.0f + ( modifier / scale - 1.0f ) * area; + + if( g_debugDamage.integer > 1 ) + G_Printf( "GetNonLocDamageModifier() modifier:%f, area:%f, scale:%f\n", + modifier, area, scale ); + + return modifier; +} + +/* +============ +GetPointDamageModifier + +Returns the damage region given an angle and a height proportion +============ +*/ +static float GetPointDamageModifier( gentity_t *targ, damageRegion_t *regions, + int len, float angle, float height ) +{ + float modifier = 1.0f; + int i; + + for( i = 0; i < len; i++ ) + { + if( regions[ i ].crouch != ( targ->client->ps.pm_flags & PMF_DUCKED ) ) + continue; + + // Angle must be within range + if( ( regions[ i ].minAngle <= regions[ i ].maxAngle && + ( angle < regions[ i ].minAngle || + angle > regions[ i ].maxAngle ) ) || + ( regions[ i ].minAngle > regions[ i ].maxAngle && + angle > regions[ i ].maxAngle && angle < regions[ i ].minAngle ) ) + continue; + + // Height must be within range + if( height < regions[ i ].minHeight || height > regions[ i ].maxHeight ) + continue; + + modifier *= regions[ i ].modifier; + } + + if( g_debugDamage.integer ) + G_Printf( "GetDamageRegionModifier(angle = %f, height = %f): %f\n", + angle, height, modifier ); + + return modifier; +} + +/* +============ +G_CalcDamageModifier +============ +*/ +static float G_CalcDamageModifier( vec3_t point, gentity_t *targ, gentity_t *attacker, int class, int dflags ) +{ + vec3_t targOrigin, bulletPath, bulletAngle, pMINUSfloor, floor, normal; + float clientHeight, hitRelative, hitRatio, modifier; + int hitRotation, i; + + if( point == NULL ) + return 1.0f; + + // Don't need to calculate angles and height for non-locational damage + if( dflags & DAMAGE_NO_LOCDAMAGE ) + return GetNonLocDamageModifier( targ, class ); + + // Get the point location relative to the floor under the target + if( g_unlagged.integer && targ->client && targ->client->unlaggedCalc.used ) + VectorCopy( targ->client->unlaggedCalc.origin, targOrigin ); + else + VectorCopy( targ->r.currentOrigin, targOrigin ); + + BG_GetClientNormal( &targ->client->ps, normal ); + VectorMA( targOrigin, targ->r.mins[ 2 ], normal, floor ); + VectorSubtract( point, floor, pMINUSfloor ); + + // Get the proportion of the target height where the hit landed + clientHeight = targ->r.maxs[ 2 ] - targ->r.mins[ 2 ]; + + if( !clientHeight ) + clientHeight = 1.0f; + + hitRelative = DotProduct( normal, pMINUSfloor ) / VectorLength( normal ); + + if( hitRelative < 0.0f ) + hitRelative = 0.0f; + + if( hitRelative > clientHeight ) + hitRelative = clientHeight; + + hitRatio = hitRelative / clientHeight; + + // Get the yaw of the attack relative to the target's view yaw + VectorSubtract( point, targOrigin, bulletPath ); + vectoangles( bulletPath, bulletAngle ); + + hitRotation = AngleNormalize360( targ->client->ps.viewangles[ YAW ] - + bulletAngle[ YAW ] ); + + // Get modifiers from the target's damage regions + modifier = GetPointDamageModifier( targ, g_damageRegions[ class ], + g_numDamageRegions[ class ], + hitRotation, hitRatio ); + + for( i = UP_NONE + 1; i < UP_NUM_UPGRADES; i++ ) + { + if( BG_InventoryContainsUpgrade( i, targ->client->ps.stats ) ) + { + modifier *= GetPointDamageModifier( targ, g_armourRegions[ i ], + g_numArmourRegions[ i ], + hitRotation, hitRatio ); + } + } + + return modifier; +} + + +/* +============ +G_InitDamageLocations +============ +*/ +void G_InitDamageLocations( void ) +{ + char *modelName; + char filename[ MAX_QPATH ]; + int i; + int len; + fileHandle_t fileHandle; + char buffer[ MAX_DAMAGE_REGION_TEXT ]; + + for( i = PCL_NONE + 1; i < PCL_NUM_CLASSES; i++ ) + { + modelName = BG_ClassConfig( i )->modelName; + Com_sprintf( filename, sizeof( filename ), + "models/players/%s/locdamage.cfg", modelName ); + + len = trap_FS_FOpenFile( filename, &fileHandle, FS_READ ); + if ( !fileHandle ) + { + G_Printf( S_COLOR_RED "file not found: %s\n", filename ); + continue; + } + + if( len >= MAX_DAMAGE_REGION_TEXT ) + { + G_Printf( S_COLOR_RED "file too large: %s is %i, max allowed is %i", + filename, len, MAX_DAMAGE_REGION_TEXT ); + trap_FS_FCloseFile( fileHandle ); + continue; + } + + COM_BeginParseSession( filename ); + + trap_FS_Read( buffer, len, fileHandle ); + buffer[len] = 0; + trap_FS_FCloseFile( fileHandle ); + + g_numDamageRegions[ i ] = G_ParseDmgScript( g_damageRegions[ i ], buffer ); + } + + for( i = UP_NONE + 1; i < UP_NUM_UPGRADES; i++ ) + { + modelName = BG_Upgrade( i )->name; + 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_DAMAGE_REGION_TEXT ) + { + G_Printf( S_COLOR_RED "file too large: %s is %i, max allowed is %i", + filename, len, MAX_DAMAGE_REGION_TEXT ); + trap_FS_FCloseFile( fileHandle ); + continue; + } + + COM_BeginParseSession( filename ); + + trap_FS_Read( buffer, len, fileHandle ); + buffer[len] = 0; + trap_FS_FCloseFile( fileHandle ); + + g_numArmourRegions[ i ] = G_ParseDmgScript( g_armourRegions[ i ], buffer ); + } +} + + +/* +============ +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 +============ +*/ + +// 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_TEAM ] ) ) + 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 asave = 0; + int knockback; + + // Can't deal damage sometimes + if( !targ->takedamage || targ->health <= 0 || level.intermissionQueued ) + return; + + if( !inflictor ) + inflictor = &g_entities[ ENTITYNUM_WORLD ]; + + if( !attacker ) + attacker = &g_entities[ ENTITYNUM_WORLD ]; + + // 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 && client->noclip ) + return; + + if( !dir ) + dflags |= DAMAGE_NO_KNOCKBACK; + else + VectorNormalize( dir ); + + knockback = damage; + + if( inflictor->s.weapon != WP_NONE ) + { + knockback = (int)( (float)knockback * + BG_Weapon( inflictor->s.weapon )->knockbackScale ); + } + + if( targ->client ) + { + knockback = (int)( (float)knockback * + BG_Class( targ->client->ps.stats[ STAT_CLASS ] )->knockbackScale ); + } + + // Too much knockback from falling really far makes you "bounce" and + // looks silly. However, none at all also looks bad. Cap it. + if( mod == MOD_FALLING && knockback > 50 ) + knockback = 50; + + 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; + } + } + + // don't do friendly fire on movement attacks + if( ( mod == MOD_LEVEL4_TRAMPLE || mod == MOD_LEVEL3_POUNCE || + mod == MOD_LEVEL4_CRUSH ) && + targ->s.eType == ET_BUILDABLE && targ->buildableTeam == TEAM_ALIENS ) + { + return; + } + + // 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 ) ) + { + // don't do friendly fire on movement attacks + if( mod == MOD_LEVEL4_TRAMPLE || mod == MOD_LEVEL3_POUNCE || + mod == MOD_LEVEL4_CRUSH ) + return; + + // if dretchpunt is enabled and this is a dretch, do dretchpunt instead of damage + if( g_dretchPunt.integer && + targ->client->ps.stats[ STAT_CLASS ] == 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; + } + + // check if friendly fire has been disabled + if( !g_friendlyFire.integer ) + { + return; + } + } + + if( targ->s.eType == ET_BUILDABLE && attacker->client && + mod != MOD_DECONSTRUCT && mod != MOD_SUICIDE && + mod != MOD_REPLACE && mod != MOD_NOCREEP ) + { + if( targ->buildableTeam == attacker->client->pers.teamSelection && + !g_friendlyBuildableFire.integer ) + { + return; + } + + // base is under attack warning if DCC'd + if( targ->buildableTeam == TEAM_HUMANS && G_FindDCC( targ ) && + level.time > level.humanBaseAttackTimer ) + { + level.humanBaseAttackTimer = level.time + DC_ATTACK_PERIOD; + G_BroadcastEvent( EV_DCC_ATTACK, 0 ); + } + } + + // check for godmode + if ( targ->flags & FL_GODMODE ) + 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; + + // 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; + take = (int)( take * G_CalcDamageModifier( point, targ, attacker, + client->ps.stats[ STAT_CLASS ], + dflags ) + 0.5f ); + + //if boosted poison every attack + if( attacker->client && attacker->client->ps.stats[ STAT_STATE ] & SS_BOOSTED ) + { + if( (targ->client->ps.stats[ STAT_TEAM ] == TEAM_HUMANS ) && + mod != MOD_LEVEL2_ZAP && mod != MOD_POISON && + mod != MOD_LEVEL1_PCLOUD && mod != MOD_HSPAWN && + mod != MOD_ASPAWN && targ->client->poisonImmunityTime < level.time ) + { + targ->client->ps.stats[ STAT_STATE ] |= SS_POISONED; + targ->client->lastPoisonTime = level.time; + targ->client->lastPoisonClient = attacker; + } + } + } + + 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 ); + } + + // do the damage + if( take ) + { + targ->health = targ->health - take; + + if( targ->client ) + targ->client->ps.stats[ STAT_HEALTH ] = targ->health; + + targ->lastDamageTime = level.time; + targ->nextRegenTime = level.time + ALIEN_REGEN_DAMAGE_TIME; + + // add to the attackers "account" on the target + if( attacker->client && attacker != targ ) + targ->credits[ attacker->client->ps.clientNum ] += take; + + 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 ) + { + //if(targ->s.eType==ET_BUILDABLE&&BG_Buildable(targ->s.modelindex,NULL)->cuboid) + // VectorCopy(point,targ->s.angles2); + 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; +} + +/* +============ +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; + + if( ent->flags & FL_NOTARGET ) + 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 ) && ent->client && + ent->client->ps.stats[ STAT_TEAM ] != team ) + { + 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; + hitClient = qtrue; + G_Damage( ent, NULL, attacker, dir, origin, + (int)points, DAMAGE_RADIUS|DAMAGE_NO_LOCDAMAGE, mod ); + } + } + + 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; + hitClient = qtrue; + G_Damage( ent, NULL, attacker, dir, origin, + (int)points, DAMAGE_RADIUS|DAMAGE_NO_LOCDAMAGE, mod ); + } + } + + return hitClient; +} + +/* +================ +G_LogDestruction + +Log deconstruct/destroy events +================ +*/ +void G_LogDestruction( gentity_t *self, gentity_t *actor, int mod ) +{ + buildFate_t fate; + + switch( mod ) + { + case MOD_DECONSTRUCT: + fate = BF_DECONSTRUCT; + break; + case MOD_REPLACE: + fate = BF_REPLACE; + break; + case MOD_NOCREEP: + fate = ( actor->client ) ? BF_UNPOWER : BF_AUTO; + break; + default: + if( actor->client ) + { + if( actor->client->pers.teamSelection == + BG_Buildable( self->s.modelindex, NULL )->team ) + { + fate = BF_TEAMKILL; + } + else + fate = BF_DESTROY; + } + else + fate = BF_AUTO; + break; + } + G_BuildLogAuto( actor, self, fate ); + + // don't log when marked structures are removed + if( mod == MOD_REPLACE ) + return; + + G_LogPrintf( S_COLOR_YELLOW "Deconstruct: %d %d %s %s: %s %s by %s\n", + actor - g_entities, + self - g_entities, + BG_Buildable( self->s.modelindex, NULL )->name, + modNames[ mod ], + G_CuboidName(self->s.modelindex,self->cuboidSize,qtrue), + mod == MOD_DECONSTRUCT ? "deconstructed" : "destroyed", + actor->client ? actor->client->pers.netname : "<world>" ); + + // No-power deaths for humans come after some minutes and it's confusing + // when the messages appear attributed to the deconner. Just don't print them. + if( mod == MOD_NOCREEP && actor->client && + actor->client->pers.teamSelection == TEAM_HUMANS ) + return; + + if( actor->client && actor->client->pers.teamSelection == + BG_Buildable( self->s.modelindex, NULL )->team ) + { + G_TeamCommand( actor->client->ps.stats[ STAT_TEAM ], + va( "print \"%s ^3%s^7 by %s\n\"", + G_CuboidName(self->s.modelindex,self->cuboidSize,qfalse), + mod == MOD_DECONSTRUCT ? "DECONSTRUCTED" : "DESTROYED", + actor->client->pers.netname ) ); + } + +} diff --git a/src/game/g_combat.c.orig b/src/game/g_combat.c.orig new file mode 100644 index 0000000..f4e89ee --- /dev/null +++ b/src/game/g_combat.c.orig @@ -0,0 +1,1468 @@ +/* +=========================================================================== +Copyright (C) 1999-2005 Id Software, Inc. +Copyright (C) 2000-2009 Darklegion Development + +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_DAMAGE_REGIONS ]; +int g_numDamageRegions[ PCL_NUM_CLASSES ]; + +damageRegion_t g_armourRegions[ UP_NUM_UPGRADES ][ MAX_DAMAGE_REGIONS ]; +int g_numArmourRegions[ UP_NUM_UPGRADES ]; + +/* +============ +AddScore + +Adds score to the client +============ +*/ +void AddScore( gentity_t *ent, int score ) +{ + if( !ent->client ) + return; + + // make alien and human scores equivalent + if ( ent->client->pers.teamSelection == TEAM_ALIENS ) + { + score = rint( ((float)score) / 2.0f ); + } + + // scale values down to fit the scoreboard better + score = rint( ((float)score) / 50.0f ); + + ent->client->ps.persistant[ PERS_SCORE ] += score; + CalculateRanks( ); +} + +/* +================== +LookAtKiller +================== +*/ +void LookAtKiller( gentity_t *self, gentity_t *inflictor, gentity_t *attacker ) +{ + + if ( attacker && attacker != self ) + self->client->ps.stats[ STAT_VIEWLOCK ] = attacker - g_entities; + else if( inflictor && inflictor != self ) + self->client->ps.stats[ STAT_VIEWLOCK ] = inflictor - g_entities; + else + self->client->ps.stats[ STAT_VIEWLOCK ] = self - g_entities; +} + +// 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_TRAMPLE", + "MOD_LEVEL4_CRUSH", + + "MOD_SLOWBLOB", + "MOD_POISON", + "MOD_SWARM", + + "MOD_HSPAWN", + "MOD_TESLAGEN", + "MOD_MGTURRET", + "MOD_REACTOR", + + "MOD_ASPAWN", + "MOD_ATUBE", + "MOD_OVERMIND", + "MOD_DECONSTRUCT", + "MOD_REPLACE", + "MOD_NOCREEP" +}; + +/* +================== +G_RewardAttackers + +Function to distribute rewards to entities that killed this one. +Returns the total damage dealt. +================== +*/ +float G_RewardAttackers( gentity_t *self ) +{ + float value, totalDamage = 0; + int team, i, maxHealth = 0; + int alienCredits = 0, humanCredits = 0; + gentity_t *player; + + // Total up all the damage done by non-teammates + for( i = 0; i < level.maxclients; i++ ) + { + player = g_entities + i; + + if( !OnSameTeam( self, player ) || + self->buildableTeam != player->client->ps.stats[ STAT_TEAM ] ) + totalDamage += (float)self->credits[ i ]; + } + + if( totalDamage <= 0.0f ) + return 0.0f; + + // Only give credits for killing players and buildables + if( self->client ) + { + value = BG_GetValueOfPlayer( &self->client->ps ); + team = self->client->pers.teamSelection; + maxHealth = self->client->ps.stats[ STAT_MAX_HEALTH ]; + } + else if( self->s.eType == ET_BUILDABLE ) + { + value = BG_Buildable( self->s.modelindex, self->cuboidSize )->value; + + // only give partial credits for a buildable not yet completed + if( !self->spawned ) + { + value *= (float)( level.time - self->buildTime ) / + BG_Buildable( self->s.modelindex, self->cuboidSize )->buildTime; + } + + team = self->buildableTeam; + maxHealth = BG_Buildable( self->s.modelindex, self->cuboidSize )->health; + } + else + return totalDamage; + + // Give credits and empty the array + for( i = 0; i < level.maxclients; i++ ) + { + int stageValue = value * self->credits[ i ] / totalDamage; + player = g_entities + i; + + if( player->client->pers.teamSelection != team ) + { + if( totalDamage < maxHealth ) + stageValue *= totalDamage / maxHealth; + + if( !self->credits[ i ] || player->client->ps.stats[ STAT_TEAM ] == team ) + continue; + + AddScore( player, stageValue ); + + // killing buildables earns score, but not credits + if( self->s.eType != ET_BUILDABLE ) + { + G_AddCreditToClient( player->client, stageValue, qtrue ); + + // add to stage counters + if( player->client->ps.stats[ STAT_TEAM ] == TEAM_ALIENS ) + alienCredits += stageValue; + else if( player->client->ps.stats[ STAT_TEAM ] == TEAM_HUMANS ) + humanCredits += stageValue; + } + } + self->credits[ i ] = 0; + } + + if( alienCredits ) + { + trap_Cvar_Set( "g_alienCredits", + va( "%d", g_alienCredits.integer + alienCredits ) ); + trap_Cvar_Update( &g_alienCredits ); + } + if( humanCredits ) + { + trap_Cvar_Set( "g_humanCredits", + va( "%d", g_humanCredits.integer + humanCredits ) ); + trap_Cvar_Update( &g_humanCredits ); + } + + return totalDamage; +} + +/* +================== +player_die +================== +*/ +void player_die( gentity_t *self, gentity_t *inflictor, gentity_t *attacker, int damage, int meansOfDeath ) +{ + gentity_t *ent, *ent2; + int anim; + int killer; + int i; + char *killerName, *obit; + vec3_t dir; + + 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; + else + killerName = "<world>"; + } + else + { + killer = ENTITYNUM_WORLD; + killerName = "<world>"; + } + + if( meansOfDeath < 0 || meansOfDeath >= sizeof( modNames ) / sizeof( modNames[0] ) ) + // fall back on the number + obit = va( "%d", meansOfDeath ); + else + obit = modNames[ meansOfDeath ]; + + G_LogPrintf( "Die: %d %d %s: %s" S_COLOR_WHITE " killed %s\n", + killer, + self - g_entities, + obit, + killerName, + self->client->pers.netname ); + + // deactivate all upgrades + for( i = UP_NONE + 1; i < UP_NUM_UPGRADES; i++ ) + BG_DeactivateUpgrade( i, self->client->ps.stats ); + + // kill all player's buildables if they havent spawned yet + // this should eliminate build timer hacks for ever + dir[0] = dir[1] = 0.0f; + dir[2] = 1.0f; + + for( i = MAX_CLIENTS, ent = g_entities + i; i < level.num_entities; i++, ent++ ) + { + if( ent->s.eType != ET_BUILDABLE ) + continue; + + if( ent == self ) + continue; + + if( ent->spawned ) + continue; + + if( ent->builtBy != self->client->ps.clientNum ) + continue; + + G_Damage( ent, self, attacker, dir, dir, ent->health, 0, MOD_DECONSTRUCT ); + } + + // broadcast the death event to everyone + 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 + + self->enemy = attacker; + self->client->ps.persistant[ PERS_KILLED ]++; + + if( attacker && attacker->client ) + { + attacker->client->lastkilled_client = self->s.number; + + if( ( attacker == self || OnSameTeam( self, attacker ) ) && meansOfDeath != MOD_HSPAWN ) + { + //punish team kills and suicides + if( attacker->client->ps.stats[ STAT_TEAM ] == TEAM_ALIENS ) + { + G_AddCreditToClient( attacker->client, -ALIEN_TK_SUICIDE_PENALTY, qtrue ); + AddScore( attacker, -ALIEN_TK_SUICIDE_PENALTY ); + } + else if( attacker->client->ps.stats[ STAT_TEAM ] == TEAM_HUMANS ) + { + G_AddCreditToClient( attacker->client, -HUMAN_TK_SUICIDE_PENALTY, qtrue ); + AddScore( attacker, -HUMAN_TK_SUICIDE_PENALTY ); + } + } + } + else if( attacker->s.eType != ET_BUILDABLE ) + { + if( self->client->ps.stats[ STAT_TEAM ] == TEAM_ALIENS ) + AddScore( self, -ALIEN_TK_SUICIDE_PENALTY ); + else if( self->client->ps.stats[ STAT_TEAM ] == TEAM_HUMANS ) + AddScore( self, -HUMAN_TK_SUICIDE_PENALTY ); + } + + // give credits for killing this player + G_RewardAttackers( self ); + + 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.spectatorState == SPECTATOR_NOT ) + continue; + + if( client->sess.spectatorClient == self->s.number ) + ScoreboardMessage( g_entities + i ); + } + + 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; + + // clear misc + memset( self->client->ps.misc, 0, sizeof( self->client->ps.misc ) ); + + { + // 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 ); +} + +/* +=============== +G_ParseDmgScript +=============== +*/ +static int G_ParseDmgScript( damageRegion_t *regions, char *buf ) +{ + char *token; + float angleSpan, heightSpan; + int count; + + for( count = 0; ; count++ ) + { + token = COM_Parse( &buf ); + if( !token[ 0 ] ) + break; + + if( strcmp( token, "{" ) ) + { + COM_ParseError( "Missing {" ); + break; + } + + if( count >= MAX_DAMAGE_REGIONS ) + { + COM_ParseError( "Max damage regions exceeded" ); + break; + } + + // defaults + regions[ count ].name[ 0 ] = '\0'; + regions[ count ].minHeight = 0.0f; + regions[ count ].maxHeight = 1.0f; + regions[ count ].minAngle = 0.0f; + regions[ count ].maxAngle = 360.0f; + regions[ count ].modifier = 1.0f; + regions[ count ].crouch = qfalse; + + while( 1 ) + { + token = COM_ParseExt( &buf, qtrue ); + + if( !token[ 0 ] ) + { + COM_ParseError( "Unexpected end of file" ); + break; + } + + if( !Q_stricmp( token, "}" ) ) + { + break; + } + else if( !strcmp( token, "name" ) ) + { + token = COM_ParseExt( &buf, qfalse ); + if( token[ 0 ] ) + Q_strncpyz( regions[ count ].name, token, + sizeof( regions[ count ].name ) ); + } + else if( !strcmp( token, "minHeight" ) ) + { + token = COM_ParseExt( &buf, qfalse ); + if( !token[ 0 ] ) + strcpy( token, "0" ); + regions[ count ].minHeight = atof( token ); + } + else if( !strcmp( token, "maxHeight" ) ) + { + token = COM_ParseExt( &buf, qfalse ); + if( !token[ 0 ] ) + strcpy( token, "100" ); + regions[ count ].maxHeight = atof( token ); + } + else if( !strcmp( token, "minAngle" ) ) + { + token = COM_ParseExt( &buf, qfalse ); + if( !token[ 0 ] ) + strcpy( token, "0" ); + regions[ count ].minAngle = atoi( token ); + } + else if( !strcmp( token, "maxAngle" ) ) + { + token = COM_ParseExt( &buf, qfalse ); + if( !token[ 0 ] ) + strcpy( token, "360" ); + regions[ count ].maxAngle = atoi( token ); + } + else if( !strcmp( token, "modifier" ) ) + { + token = COM_ParseExt( &buf, qfalse ); + if( !token[ 0 ] ) + strcpy( token, "1.0" ); + regions[ count ].modifier = atof( token ); + } + else if( !strcmp( token, "crouch" ) ) + { + regions[ count ].crouch = qtrue; + } + else + { + COM_ParseWarning("Unknown token \"%s\"", token); + } + } + + // Angle portion covered + angleSpan = regions[ count ].maxAngle - regions[ count ].minAngle; + if( angleSpan < 0.0f ) + angleSpan += 360.0f; + angleSpan /= 360.0f; + + // Height portion covered + heightSpan = regions[ count ].maxHeight - regions[ count ].minHeight; + if( heightSpan < 0.0f ) + heightSpan = -heightSpan; + if( heightSpan > 1.0f ) + heightSpan = 1.0f; + + regions[ count ].area = angleSpan * heightSpan; + if( !regions[ count ].area ) + regions[ count ].area = 0.00001f; + } + + return count; +} + +/* +============ +GetRegionDamageModifier +============ +*/ +static float GetRegionDamageModifier( gentity_t *targ, int class, int piece ) +{ + damageRegion_t *regions, *overlap; + float modifier = 0.0f, areaSum = 0.0f; + int j, i; + qboolean crouch; + + crouch = targ->client->ps.pm_flags & PMF_DUCKED; + overlap = &g_damageRegions[ class ][ piece ]; + + if( g_debugDamage.integer > 2 ) + G_Printf( "GetRegionDamageModifier():\n" + ". bodyRegion = [%d %d %f %f] (%s)\n" + ". modifier = %f\n", + overlap->minAngle, overlap->maxAngle, + overlap->minHeight, overlap->maxHeight, + overlap->name, overlap->modifier ); + + // Find the armour layer modifier, assuming that none of the armour regions + // overlap and that any areas that are not covered have a modifier of 1.0 + for( j = UP_NONE + 1; j < UP_NUM_UPGRADES; j++ ) + { + if( !BG_InventoryContainsUpgrade( j, targ->client->ps.stats ) || + !g_numArmourRegions[ j ] ) + continue; + regions = g_armourRegions[ j ]; + + for( i = 0; i < g_numArmourRegions[ j ]; i++ ) + { + float overlapMaxA, regionMinA, regionMaxA, angleSpan, heightSpan, area; + + if( regions[ i ].crouch != crouch ) + continue; + + // Convert overlap angle to 0 to max + overlapMaxA = overlap->maxAngle - overlap->minAngle; + if( overlapMaxA < 0.0f ) + overlapMaxA += 360.0f; + + // Convert region angles to match overlap + regionMinA = regions[ i ].minAngle - overlap->minAngle; + if( regionMinA < 0.0f ) + regionMinA += 360.0f; + regionMaxA = regions[ i ].maxAngle - overlap->minAngle; + if( regionMaxA < 0.0f ) + regionMaxA += 360.0f; + + // Overlapping Angle portion + if( regionMinA <= regionMaxA ) + { + angleSpan = 0.0f; + if( regionMinA < overlapMaxA ) + { + if( regionMaxA > overlapMaxA ) + regionMaxA = overlapMaxA; + angleSpan = regionMaxA - regionMinA; + } + } + else + { + if( regionMaxA > overlapMaxA ) + regionMaxA = overlapMaxA; + angleSpan = regionMaxA; + if( regionMinA < overlapMaxA ) + angleSpan += overlapMaxA - regionMinA; + } + angleSpan /= 360.0f; + + // Overlapping height portion + heightSpan = MIN( overlap->maxHeight, regions[ i ].maxHeight ) - + MAX( overlap->minHeight, regions[ i ].minHeight ); + if( heightSpan < 0.0f ) + heightSpan = 0.0f; + if( heightSpan > 1.0f ) + heightSpan = 1.0f; + + if( g_debugDamage.integer > 2 ) + G_Printf( ". armourRegion = [%d %d %f %f] (%s)\n" + ". . modifier = %f\n" + ". . angleSpan = %f\n" + ". . heightSpan = %f\n", + regions[ i ].minAngle, regions[ i ].maxAngle, + regions[ i ].minHeight, regions[ i ].maxHeight, + regions[ i ].name, regions[ i ].modifier, + angleSpan, heightSpan ); + + areaSum += area = angleSpan * heightSpan; + modifier += regions[ i ].modifier * area; + } + } + + if( g_debugDamage.integer > 2 ) + G_Printf( ". areaSum = %f\n" + ". armourModifier = %f\n", areaSum, modifier ); + + return overlap->modifier * ( overlap->area + modifier - areaSum ); +} + +/* +============ +GetNonLocDamageModifier +============ +*/ +static float GetNonLocDamageModifier( gentity_t *targ, int class ) +{ + float modifier = 0.0f, area = 0.0f, scale = 0.0f; + int i; + qboolean crouch; + + // For every body region, use stretch-armor formula to apply armour modifier + // for any overlapping area that armour shares with the body region + crouch = targ->client->ps.pm_flags & PMF_DUCKED; + for( i = 0; i < g_numDamageRegions[ class ]; i++ ) + { + damageRegion_t *region; + + region = &g_damageRegions[ class ][ i ]; + + if( region->crouch != crouch ) + continue; + + modifier += GetRegionDamageModifier( targ, class, i ); + + scale += region->modifier * region->area; + area += region->area; + + } + + modifier = !scale ? 1.0f : 1.0f + ( modifier / scale - 1.0f ) * area; + + if( g_debugDamage.integer > 1 ) + G_Printf( "GetNonLocDamageModifier() modifier:%f, area:%f, scale:%f\n", + modifier, area, scale ); + + return modifier; +} + +/* +============ +GetPointDamageModifier + +Returns the damage region given an angle and a height proportion +============ +*/ +static float GetPointDamageModifier( gentity_t *targ, damageRegion_t *regions, + int len, float angle, float height ) +{ + float modifier = 1.0f; + int i; + + for( i = 0; i < len; i++ ) + { + if( regions[ i ].crouch != ( targ->client->ps.pm_flags & PMF_DUCKED ) ) + continue; + + // Angle must be within range + if( ( regions[ i ].minAngle <= regions[ i ].maxAngle && + ( angle < regions[ i ].minAngle || + angle > regions[ i ].maxAngle ) ) || + ( regions[ i ].minAngle > regions[ i ].maxAngle && + angle > regions[ i ].maxAngle && angle < regions[ i ].minAngle ) ) + continue; + + // Height must be within range + if( height < regions[ i ].minHeight || height > regions[ i ].maxHeight ) + continue; + + modifier *= regions[ i ].modifier; + } + + if( g_debugDamage.integer ) + G_Printf( "GetDamageRegionModifier(angle = %f, height = %f): %f\n", + angle, height, modifier ); + + return modifier; +} + +/* +============ +G_CalcDamageModifier +============ +*/ +static float G_CalcDamageModifier( vec3_t point, gentity_t *targ, gentity_t *attacker, int class, int dflags ) +{ + vec3_t targOrigin, bulletPath, bulletAngle, pMINUSfloor, floor, normal; + float clientHeight, hitRelative, hitRatio, modifier; + int hitRotation, i; + + if( point == NULL ) + return 1.0f; + + // Don't need to calculate angles and height for non-locational damage + if( dflags & DAMAGE_NO_LOCDAMAGE ) + return GetNonLocDamageModifier( targ, class ); + + // Get the point location relative to the floor under the target + if( g_unlagged.integer && targ->client && targ->client->unlaggedCalc.used ) + VectorCopy( targ->client->unlaggedCalc.origin, targOrigin ); + else + VectorCopy( targ->r.currentOrigin, targOrigin ); + + BG_GetClientNormal( &targ->client->ps, normal ); + VectorMA( targOrigin, targ->r.mins[ 2 ], normal, floor ); + VectorSubtract( point, floor, pMINUSfloor ); + + // Get the proportion of the target height where the hit landed + clientHeight = targ->r.maxs[ 2 ] - targ->r.mins[ 2 ]; + + if( !clientHeight ) + clientHeight = 1.0f; + + hitRelative = DotProduct( normal, pMINUSfloor ) / VectorLength( normal ); + + if( hitRelative < 0.0f ) + hitRelative = 0.0f; + + if( hitRelative > clientHeight ) + hitRelative = clientHeight; + + hitRatio = hitRelative / clientHeight; + + // Get the yaw of the attack relative to the target's view yaw + VectorSubtract( point, targOrigin, bulletPath ); + vectoangles( bulletPath, bulletAngle ); + + hitRotation = AngleNormalize360( targ->client->ps.viewangles[ YAW ] - + bulletAngle[ YAW ] ); + + // Get modifiers from the target's damage regions + modifier = GetPointDamageModifier( targ, g_damageRegions[ class ], + g_numDamageRegions[ class ], + hitRotation, hitRatio ); + + for( i = UP_NONE + 1; i < UP_NUM_UPGRADES; i++ ) + { + if( BG_InventoryContainsUpgrade( i, targ->client->ps.stats ) ) + { + modifier *= GetPointDamageModifier( targ, g_armourRegions[ i ], + g_numArmourRegions[ i ], + hitRotation, hitRatio ); + } + } + + return modifier; +} + + +/* +============ +G_InitDamageLocations +============ +*/ +void G_InitDamageLocations( void ) +{ + char *modelName; + char filename[ MAX_QPATH ]; + int i; + int len; + fileHandle_t fileHandle; + char buffer[ MAX_DAMAGE_REGION_TEXT ]; + + for( i = PCL_NONE + 1; i < PCL_NUM_CLASSES; i++ ) + { + modelName = BG_ClassConfig( i )->modelName; + Com_sprintf( filename, sizeof( filename ), + "models/players/%s/locdamage.cfg", modelName ); + + len = trap_FS_FOpenFile( filename, &fileHandle, FS_READ ); + if ( !fileHandle ) + { + G_Printf( S_COLOR_RED "file not found: %s\n", filename ); + continue; + } + + if( len >= MAX_DAMAGE_REGION_TEXT ) + { + G_Printf( S_COLOR_RED "file too large: %s is %i, max allowed is %i", + filename, len, MAX_DAMAGE_REGION_TEXT ); + trap_FS_FCloseFile( fileHandle ); + continue; + } + + COM_BeginParseSession( filename ); + + trap_FS_Read( buffer, len, fileHandle ); + buffer[len] = 0; + trap_FS_FCloseFile( fileHandle ); + + g_numDamageRegions[ i ] = G_ParseDmgScript( g_damageRegions[ i ], buffer ); + } + + for( i = UP_NONE + 1; i < UP_NUM_UPGRADES; i++ ) + { + modelName = BG_Upgrade( i )->name; + 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_DAMAGE_REGION_TEXT ) + { + G_Printf( S_COLOR_RED "file too large: %s is %i, max allowed is %i", + filename, len, MAX_DAMAGE_REGION_TEXT ); + trap_FS_FCloseFile( fileHandle ); + continue; + } + + COM_BeginParseSession( filename ); + + trap_FS_Read( buffer, len, fileHandle ); + buffer[len] = 0; + trap_FS_FCloseFile( fileHandle ); + + g_numArmourRegions[ i ] = G_ParseDmgScript( g_armourRegions[ i ], buffer ); + } +} + + +/* +============ +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 +============ +*/ + +// 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_TEAM ] ) ) + 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 asave = 0; + int knockback; + + // Can't deal damage sometimes + if( !targ->takedamage || targ->health <= 0 || level.intermissionQueued ) + return; + + if( !inflictor ) + inflictor = &g_entities[ ENTITYNUM_WORLD ]; + + if( !attacker ) + attacker = &g_entities[ ENTITYNUM_WORLD ]; + + // 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 && client->noclip ) + return; + + if( !dir ) + dflags |= DAMAGE_NO_KNOCKBACK; + else + VectorNormalize( dir ); + + knockback = damage; + + if( inflictor->s.weapon != WP_NONE ) + { + knockback = (int)( (float)knockback * + BG_Weapon( inflictor->s.weapon )->knockbackScale ); + } + + if( targ->client ) + { + knockback = (int)( (float)knockback * + BG_Class( targ->client->ps.stats[ STAT_CLASS ] )->knockbackScale ); + } + + // Too much knockback from falling really far makes you "bounce" and + // looks silly. However, none at all also looks bad. Cap it. + if( mod == MOD_FALLING && knockback > 50 ) + knockback = 50; + + 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; + } + } + + // don't do friendly fire on movement attacks + if( ( mod == MOD_LEVEL4_TRAMPLE || mod == MOD_LEVEL3_POUNCE || + mod == MOD_LEVEL4_CRUSH ) && + targ->s.eType == ET_BUILDABLE && targ->buildableTeam == TEAM_ALIENS ) + { + return; + } + + // 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 ) ) + { + // don't do friendly fire on movement attacks + if( mod == MOD_LEVEL4_TRAMPLE || mod == MOD_LEVEL3_POUNCE || + mod == MOD_LEVEL4_CRUSH ) + return; + + // if dretchpunt is enabled and this is a dretch, do dretchpunt instead of damage + if( g_dretchPunt.integer && + targ->client->ps.stats[ STAT_CLASS ] == 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; + } + + // check if friendly fire has been disabled + if( !g_friendlyFire.integer ) + { + return; + } + } + + if( targ->s.eType == ET_BUILDABLE && attacker->client && + mod != MOD_DECONSTRUCT && mod != MOD_SUICIDE && + mod != MOD_REPLACE && mod != MOD_NOCREEP ) + { + if( targ->buildableTeam == attacker->client->pers.teamSelection && + !g_friendlyBuildableFire.integer ) + { + return; + } + + // base is under attack warning if DCC'd + if( targ->buildableTeam == TEAM_HUMANS && G_FindDCC( targ ) && + level.time > level.humanBaseAttackTimer ) + { + level.humanBaseAttackTimer = level.time + DC_ATTACK_PERIOD; + G_BroadcastEvent( EV_DCC_ATTACK, 0 ); + } + } + + // check for godmode + if ( targ->flags & FL_GODMODE ) + 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; + + // 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; + take = (int)( take * G_CalcDamageModifier( point, targ, attacker, + client->ps.stats[ STAT_CLASS ], + dflags ) + 0.5f ); + + //if boosted poison every attack + if( attacker->client && attacker->client->ps.stats[ STAT_STATE ] & SS_BOOSTED ) + { + if( (targ->client->ps.stats[ STAT_TEAM ] == TEAM_HUMANS ) && + mod != MOD_LEVEL2_ZAP && mod != MOD_POISON && + mod != MOD_LEVEL1_PCLOUD && mod != MOD_HSPAWN && + mod != MOD_ASPAWN && targ->client->poisonImmunityTime < level.time ) + { + targ->client->ps.stats[ STAT_STATE ] |= SS_POISONED; + targ->client->lastPoisonTime = level.time; + targ->client->lastPoisonClient = attacker; + } + } + } + + 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 ); + } + + // do the damage + if( take ) + { + targ->health = targ->health - take; + + if( targ->client ) + targ->client->ps.stats[ STAT_HEALTH ] = targ->health; + + targ->lastDamageTime = level.time; + targ->nextRegenTime = level.time + ALIEN_REGEN_DAMAGE_TIME; + + // add to the attackers "account" on the target + if( attacker->client && attacker != targ ) + targ->credits[ attacker->client->ps.clientNum ] += take; + + 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 ) + { + //if(targ->s.eType==ET_BUILDABLE&&BG_Buildable(targ->s.modelindex,NULL)->cuboid) + // VectorCopy(point,targ->s.angles2); + 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; +} + +/* +============ +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; + + if( ent->flags & FL_NOTARGET ) + 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 ) && ent->client && + ent->client->ps.stats[ STAT_TEAM ] != team ) + { + 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; + hitClient = qtrue; + G_Damage( ent, NULL, attacker, dir, origin, + (int)points, DAMAGE_RADIUS|DAMAGE_NO_LOCDAMAGE, mod ); + } + } + + 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; + hitClient = qtrue; + G_Damage( ent, NULL, attacker, dir, origin, + (int)points, DAMAGE_RADIUS|DAMAGE_NO_LOCDAMAGE, mod ); + } + } + + return hitClient; +} + +/* +================ +G_LogDestruction + +Log deconstruct/destroy events +================ +*/ +void G_LogDestruction( gentity_t *self, gentity_t *actor, int mod ) +{ + buildFate_t fate; + + switch( mod ) + { + case MOD_DECONSTRUCT: + fate = BF_DECONSTRUCT; + break; + case MOD_REPLACE: + fate = BF_REPLACE; + break; + case MOD_NOCREEP: + fate = ( actor->client ) ? BF_UNPOWER : BF_AUTO; + break; + default: + if( actor->client ) + { + if( actor->client->pers.teamSelection == + BG_Buildable( self->s.modelindex, NULL )->team ) + { + fate = BF_TEAMKILL; + } + else + fate = BF_DESTROY; + } + else + fate = BF_AUTO; + break; + } + G_BuildLogAuto( actor, self, fate ); + + // don't log when marked structures are removed + if( mod == MOD_REPLACE ) + return; + + G_LogPrintf( S_COLOR_YELLOW "Deconstruct: %d %d %s %s: %s %s by %s\n", + actor - g_entities, + self - g_entities, + BG_Buildable( self->s.modelindex, NULL )->name, + modNames[ mod ], + G_CuboidName(self->s.modelindex,self->cuboidSize,qtrue), + mod == MOD_DECONSTRUCT ? "deconstructed" : "destroyed", + actor->client ? actor->client->pers.netname : "<world>" ); + + // No-power deaths for humans come after some minutes and it's confusing + // when the messages appear attributed to the deconner. Just don't print them. + if( mod == MOD_NOCREEP && actor->client && + actor->client->pers.teamSelection == TEAM_HUMANS ) + return; + + if( actor->client && actor->client->pers.teamSelection == + BG_Buildable( self->s.modelindex, NULL )->team ) + { + G_TeamCommand( actor->client->ps.stats[ STAT_TEAM ], + va( "print \"%s ^3%s^7 by %s\n\"", + G_CuboidName(self->s.modelindex,self->cuboidSize,qfalse), + mod == MOD_DECONSTRUCT ? "DECONSTRUCTED" : "DESTROYED", + actor->client->pers.netname ) ); + } + +} diff --git a/src/game/g_local.h b/src/game/g_local.h new file mode 100644 index 0000000..a8c78b0 --- /dev/null +++ b/src/game/g_local.h @@ -0,0 +1,1270 @@ +/* +=========================================================================== +Copyright (C) 1999-2005 Id Software, Inc. +Copyright (C) 2000-2009 Darklegion Development + +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 + +// 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 healthLeft; // (used for buildings being built) health remaining for the construction to end + 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 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; + + team_t stageTeam; + stage_t stageStage; + + team_t buildableTeam; // buildable item team + gentity_t *parentNode; // for creep and defence/spawn dependencies + qboolean active; // for power repeater, but could be useful elsewhere + qboolean locked; // used for turret tracking + qboolean powered; // for human buildables + int builtBy; // clientNum of person that built this + int dcc; // number of controlling dccs + qboolean spawned; // whether or not this buildable has finished spawning + int shrunkTime; // time when a barricade shrunk or zero + 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 + + int credits[ MAX_CLIENTS ]; // human credits for each client + 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 + vec3_t turretAimRate; // track turn speed for norfenturrets + int turretSpinupTime; // spinup delay for norfenturrets + + vec4_t animation; // animated map objects + + qboolean nonSegModel; // this entity uses a nonsegmented player model + + buildable_t bTriggers[ BA_NUM_BUILDABLES ]; // which buildables are triggers + class_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 nextRegenTime; + + qboolean ownerClear; // used for missle tracking + + qboolean pointAgainstWorld; // don't use the bbox for map collisions + + int buildPointZone; // index for zone + int usesBuildPointZone; // does it use a zone? + + vec3_t cuboidSize; +}; + +typedef enum +{ + CON_DISCONNECTED, + CON_CONNECTING, + CON_CONNECTED +} clientConnected_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 +{ + int spectatorTime; // for determining next-in-line to play + spectatorState_t spectatorState; + int spectatorClient; // for chasecam and follow mode + team_t restartTeam; //for !restart keepteams and !restart switchteams + clientList_t ignoreList; +} clientSession_t; + +// namelog +#define MAX_NAMELOG_NAMES 5 +#define MAX_NAMELOG_ADDRS 5 +typedef struct namelog_s +{ + struct namelog_s *next; + char name[ MAX_NAMELOG_NAMES ][ MAX_NAME_LENGTH ]; + addr_t ip[ MAX_NAMELOG_ADDRS ]; + char guid[ 33 ]; + int slot; + qboolean banned; + + int nameOffset; + int nameChangeTime; + int nameChanges; + int voteCount; + + qboolean muted; + qboolean denyBuild; + + int score; + int credits; + team_t team; + + int id; +} namelog_t; + +// 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 stickySpec; // don't stop spectating a player after they get killed + qboolean pmoveFixed; // + char netname[ MAX_NAME_LENGTH ]; + int enterTime; // level.time the client entered the game + int location; // player locations + qboolean teamInfo; // send team overlay updates? + float flySpeed; // for spectator/noclip moves + qboolean disableBlueprintErrors; // should the buildable blueprint never be hidden from the players? + + class_t classSelection; // player class (copied to ent->client->ps.stats[ STAT_CLASS ] once spawned) + float evolveHealthFraction; + weapon_t humanItemSelection; // humans have a starting item + team_t teamSelection; // player team (copied to ps.stats[ STAT_TEAM ]) + + int teamChangeTime; // level.time of last team change + namelog_t *namelog; + g_admin_admin_t *admin; + + int aliveSeconds; // time player has been alive in seconds + qboolean hasHealed; // has healed a player (basi regen aura) in the last 10sec (for score use) + + // used to save persistant[] values while in SPECTATOR_FOLLOW mode + int credit; + + int voted; + int vote; + + // flood protection + int floodDemerits; + int floodTime; + + vec3_t lastDeathLocation; + char guid[ 33 ]; + addr_t ip; + char voice[ MAX_VOICE_NAME_LEN ]; + qboolean useUnlagged; + // keep track of other players' info for tinfo + char cinfo[ MAX_CLIENTS ][ 16 ]; +} clientPersistant_t; + +#define MAX_UNLAGGED_MARKERS 10 +typedef struct unlagged_s { + vec3_t origin; + vec3_t mins; + vec3_t maxs; + qboolean used; +} unlagged_t; + +#define MAX_TRAMPLE_BUILDABLES_TRACKED 20 +// 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 boostedTime; // last time we touched a booster + + int airOutTime; + + 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 + + int time100; // timer for 100ms interval events + int time1000; // timer for one second interval events + int time10000; // timer for ten second interval events + + char *areabits; + + int lastPoisonTime; + int poisonImmunityTime; + gentity_t *lastPoisonClient; + int lastPoisonCloudedTime; + int grabExpiryTime; + int lastLockTime; + int lastSlowTime; + int lastMedKitTime; + int medKitHealthToRestore; + int medKitIncrementTime; + int lastCreepSlowTime; // time until creep can be removed + + int bioresHealTimer; + + qboolean charging; + + int lastFlameBall; // s.number of the last flame ball fired + + unlagged_t unlaggedHist[ MAX_UNLAGGED_MARKERS ]; + unlagged_t unlaggedBackup; + unlagged_t unlaggedCalc; + int unlaggedTime; + + float voiceEnthusiasm; + char lastVoiceCmd[ MAX_VOICE_CMD_LEN ]; + + int lcannonStartTime; + int trampleBuildablesHitPos; + int trampleBuildablesHit[ MAX_TRAMPLE_BUILDABLES_TRACKED ]; + + int lastCrushTime; // Tyrant crush + int lastRantBombTime; // last time client got a tyrant bomb + + vec3_t cuboidSelection; + int buildTimer; + + qboolean isImpregnated; // has a spawn implanted + int impregnationTime; + int impregnatedBy; + qboolean isImplantMature; // ready to spawn enemy troops +}; + + +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 ); +void G_PrintSpawnQueue( spawnQueue_t *sq ); + + +#define MAX_DAMAGE_REGION_TEXT 8192 +#define MAX_DAMAGE_REGIONS 16 + +// build point zone +typedef struct +{ + int active; + + int totalBuildPoints; + int queuedBuildPoints; + int nextQueueTime; +} buildPointZone_t; + +// store locational damage regions +typedef struct damageRegion_s +{ + char name[ 32 ]; + float area, modifier, minHeight, maxHeight; + int minAngle, maxAngle; + qboolean crouch; +} damageRegion_t; + +//status of the warning of certain events +typedef enum +{ + TW_NOT = 0, + TW_IMMINENT, + TW_PASSED +} timeWarning_t; + +// fate of a buildable +typedef enum +{ + BF_CONSTRUCT, + BF_DECONSTRUCT, + BF_REPLACE, + BF_DESTROY, + BF_TEAMKILL, + BF_UNPOWER, + BF_AUTO +} buildFate_t; + +// data needed to revert a change in layout +typedef struct +{ + int time; + buildFate_t fate; + namelog_t *actor; + buildable_t modelindex; + qboolean deconstruct; + int deconstructTime; + vec3_t origin; + vec3_t angles; + vec3_t origin2; + vec3_t angles2; + buildable_t powerSource; + int powerValue; +} buildLog_t; + +// +// this structure is cleared as each map is entered +// +#define MAX_SPAWN_VARS 64 +#define MAX_SPAWN_VARS_CHARS 4096 +#define MAX_BUILDLOG 128 + +typedef struct +{ + struct gclient_s *clients; // [maxclients] + + struct gentity_s *gentities; + int gentitySize; + int num_entities; // current number, <= MAX_GENTITIES + + int warmupTime; // restart match at this time + + 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[ 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 snd_fry; // sound index for standing in lava + + int warmupModificationCount; // for detecting if g_warmup is changed + + // voting state + int voteThreshold[ NUM_TEAMS ]; // need at least this percent to pass + char voteString[ NUM_TEAMS ][ MAX_STRING_CHARS ]; + char voteDisplayString[ NUM_TEAMS ][ MAX_STRING_CHARS ]; + int voteTime[ NUM_TEAMS ]; // level.time vote was called + int voteExecuteTime[ NUM_TEAMS ]; // time the vote is executed + int voteDelay[ NUM_TEAMS ]; // it doesn't make sense to always delay vote execution + int voteYes[ NUM_TEAMS ]; + int voteNo[ NUM_TEAMS ]; + int numVotingClients[ NUM_TEAMS ];// 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; + + gentity_t *locationHead; // head of the location list + + int numAlienSpawns; + int numHumanSpawns; + + int numAlienImplantedSpawns; // number of impregnated humans + int numHumanImplantedSpawns; // number of impregnated aliens (NOTE: no microtelenodes yet, spawning works though) + + int numAlienClients; + int numHumanClients; + + float averageNumAlienClients; + int numAlienSamples; + float averageNumHumanClients; + int numHumanSamples; + + int numLiveAlienClients; + int numLiveHumanClients; + + int alienBuildPoints; + int alienBuildPointQueue; + int alienNextQueueTime; + int humanBuildPoints; + int humanBuildPointQueue; + int humanNextQueueTime; + + buildPointZone_t *buildPointZones; + + gentity_t *markedBuildables[ MAX_GENTITIES ]; + int numBuildablesForRemoval; + + int alienKills; + int humanKills; + + qboolean overmindMuted; + + int humanBaseAttackTimer; + + team_t lastWin; + + int suddenDeathBeginTime; + timeWarning_t suddenDeathWarning; + timeWarning_t timelimitWarning; + + spawnQueue_t alienSpawnQueue; + spawnQueue_t humanSpawnQueue; + + int alienStage2Time; + int alienStage3Time; + int humanStage2Time; + int humanStage3Time; + + qboolean uncondAlienWin; + qboolean uncondHumanWin; + qboolean alienTeamLocked; + qboolean humanTeamLocked; + int pausedTime; + + int unlaggedIndex; + int unlaggedTimes[ MAX_UNLAGGED_MARKERS ]; + + char layout[ MAX_QPATH ]; + + team_t surrenderTeam; + int lastTeamImbalancedTime; + int numTeamImbalanceWarnings; + + voice_t *voices; + + emoticon_t emoticons[ MAX_EMOTICONS ]; + int emoticonCount; + + namelog_t *namelogs; + + buildLog_t buildLog[ MAX_BUILDLOG ]; + int buildId; + int numBuildLogs; +} level_locals_t; + +#define CMD_CHEAT 0x0001 +#define CMD_CHEAT_TEAM 0x0002 // is a cheat when used on a team +#define CMD_MESSAGE 0x0004 // sends message to others (skip when muted) +#define CMD_TEAM 0x0008 // must be on a team +#define CMD_SPEC 0x0010 // must be a spectator +#define CMD_ALIEN 0x0020 +#define CMD_HUMAN 0x0040 +#define CMD_LIVING 0x0080 +#define CMD_INTERMISSION 0x0100 // 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 +// + +#define DECOLOR_OFF '\16' +#define DECOLOR_ON '\17' + +void G_StopFollowing( gentity_t *ent ); +void G_StopFromFollowing( gentity_t *ent ); +void G_FollowLockView( gentity_t *ent ); +qboolean G_FollowNewClient( gentity_t *ent, int dir ); +void G_ToggleFollow( gentity_t *ent ); +int G_ClientNumberFromString( char *s, char *err, int len ); +int G_ClientNumbersFromString( char *s, int *plist, int max ); +char *ConcatArgs( int start ); +char *ConcatArgsPrintable( int start ); +void G_Say( gentity_t *ent, saymode_t mode, const char *chatText ); +void G_DecolorString( char *in, char *out, int len ); +void G_UnEscapeString( char *in, char *out, int len ); +void G_SanitiseString( char *in, char *out, int len ); +void Cmd_PrivateMessage_f( gentity_t *ent ); +void Cmd_ListMaps_f( gentity_t *ent ); +void Cmd_Test_f( gentity_t *ent ); +void Cmd_AdminMessage_f( gentity_t *ent ); +int G_FloodLimited( gentity_t *ent ); +void G_ListCommands( gentity_t *ent ); +void G_LoadCensors( void ); +void G_CensorString( char *out, const char *in, int len, 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_ONEOVERMIND, + IBE_NOALIENBP, + IBE_SPWNWARN, // not currently used + IBE_NOCREEP, + + IBE_ONEREACTOR, + IBE_NOPOWERHERE, + IBE_TNODEWARN, // not currently used + IBE_RPTNOREAC, + IBE_RPTPOWERHERE, + IBE_NOHUMANBP, + IBE_NODCC, + + IBE_NORMAL, // too steep + IBE_NOROOM, + IBE_PERMISSION, + IBE_LASTSPAWN, + IBE_NOSURF, + IBE_TOODENSE, + + IBE_MAXERRORS +} itemBuildError_t; + +gentity_t *G_CheckSpawnPoint( int spawnNum, const vec3_t origin, + const vec3_t normal, buildable_t spawn, + vec3_t spawnOrigin ); + +buildable_t G_IsPowered( vec3_t origin ); +qboolean G_IsDCCBuilt( void ); +int G_FindDCC( gentity_t *self ); +gentity_t *G_Reactor( void ); +gentity_t *G_Overmind( void ); +qboolean G_FindCreep( gentity_t *self ); + +void G_BuildableThink( gentity_t *ent, int msec ); +qboolean G_BuildableRange( vec3_t origin, float r, buildable_t buildable ); +void G_ClearDeconMarks( void ); +itemBuildError_t G_CanBuild( gentity_t *ent, buildable_t buildable, int distance, vec3_t origin, vec3_t normal, vec3_t cuboidSize ); +qboolean G_BuildIfValid( gentity_t *ent, buildable_t buildable, vec3_t cuboidSize ); +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_LayoutSave( char *name ); +int G_LayoutList( const char *map, char *list, int len ); +void G_LayoutSelect( void ); +void G_LayoutLoad( void ); +void G_BaseSelfDestruct( team_t team ); +int G_NextQueueTime( int queuedBP, int totalBP, int queueBaseRate ); +void G_QueueBuildPoints( gentity_t *self ); +int G_GetBuildPoints( const vec3_t pos, team_t team ); +int G_GetMarkedBuildPoints( const vec3_t pos, team_t team ); +qboolean G_FindPower( gentity_t *self, qboolean searchUnspawned ); +gentity_t *G_PowerEntityForPoint( const vec3_t origin ); +gentity_t *G_PowerEntityForEntity( gentity_t *ent ); +gentity_t *G_RepeaterEntityForPoint( vec3_t origin ); +gentity_t *G_InPowerZone( gentity_t *self ); +buildLog_t *G_BuildLogNew( gentity_t *actor, buildFate_t fate ); +void G_BuildLogSet( buildLog_t *log, gentity_t *ent ); +void G_BuildLogAuto( gentity_t *actor, gentity_t *buildable, buildFate_t fate ); +void G_BuildLogRevert( int id ); +const char *G_CuboidName(buildable_t buildable, const vec3_t cuboidSize, qboolean verbose); +void G_LayoutBuildItem( buildable_t buildable, vec3_t origin, vec3_t angles, vec3_t origin2, vec3_t angles2 ); + +// +// g_utils.c +// +//addr_t in g_admin.h for g_admin_ban_t +qboolean G_AddressParse( const char *str, addr_t *addr ); +qboolean G_AddressCompare( const addr_t *a, const addr_t *b ); + +int G_ParticleSystemIndex( char *name ); +int G_ShaderIndex( char *name ); +int G_ModelIndex( char *name ); +int G_SoundIndex( char *name ); +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_TriggerMenuArgs( int clientNum, dynMenu_t menu, int arg ); +void G_CloseMenus( int clientNum ); + +qboolean G_Visible( gentity_t *ent1, gentity_t *ent2, int contents ); +gentity_t *G_ClosestEnt( vec3_t origin, gentity_t **entities, int numEntities ); + +void G_RecalcBuildTimer( gclient_t *client ); + +// 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 ); +float G_RewardAttackers( gentity_t *self ); +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_LogDestruction( gentity_t *self, gentity_t *actor, int mod ); + +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, int speed ); +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_rantBomb( 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( team_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 +// + +typedef struct zap_s +{ + qboolean used; + + gentity_t *creator; + gentity_t *targets[ LEVEL2_AREAZAP_MAX_TARGETS ]; + int numTargets; + float distances[ LEVEL2_AREAZAP_MAX_TARGETS ]; + + int timeToLive; + + 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 CheckCkitRepair( gentity_t *ent ); +void G_ChargeAttack( gentity_t *ent, gentity_t *victim ); +void G_CrushAttack( gentity_t *ent, gentity_t *victim ); +void G_UpdateZaps( int msec ); +void G_ClearPlayerZapEffects( gentity_t *player ); + + +// +// 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( team_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 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 ); +void G_ReplaceBuildablesBuilder(int from, int to); +void G_RestoreClientBuildables( gentity_t *ent ); +void G_SaveClientBuildables( gentity_t *ent ); +// +// g_svcmds.c +// +qboolean ConsoleCommand( void ); +void G_RegisterCommands( void ); +void G_UnregisterCommands( void ); + +// +// g_weapon.c +// +void FireWeapon( gentity_t *ent ); +void FireWeapon2( gentity_t *ent ); +void FireWeapon3( gentity_t *ent ); + +// +// 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 G_AdminMessage( gentity_t *ent, const char *string ); +void QDECL G_LogPrintf( const char *fmt, ... ); +void SendScoreboardMessageToAllClients( void ); +void QDECL G_Printf( const char *fmt, ... ); +void QDECL G_Error( const char *fmt, ... ); +void G_Vote( gentity_t *ent, team_t team, qboolean voting ); +void G_ExecuteVote( team_t team ); +void G_CheckVote( team_t team ); +void LogExit( const char *string ); +int G_TimeTilSuddenDeath( void ); + +// +// g_client.c +// +char *ClientConnect( int clientNum, qboolean firstTime ); +char *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 +// +team_t G_TeamFromString( char *str ); +void G_TeamCommand( team_t team, char *cmd ); +qboolean OnSameTeam( gentity_t *ent1, gentity_t *ent2 ); +void G_LeaveTeam( gentity_t *self ); +void G_ChangeTeam( gentity_t *ent, team_t newTeam ); +gentity_t *Team_GetLocation( gentity_t *ent ); +void TeamplayInfoMessage( gentity_t *ent ); +void CheckTeamStatus( void ); +void G_UpdateTeamConfigStrings( 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 +// +void G_PrintRotations( void ); +void G_AdvanceMapRotation( int depth ); +qboolean G_StartMapRotation( char *name, qboolean advance, + qboolean putOnStack, qboolean reset_index, int depth ); +void G_StopMapRotation( void ); +qboolean G_MapRotationActive( void ); +void G_InitMapRotations( void ); +void G_ShutdownMapRotations( void ); +qboolean G_MapExists( char *name ); +void G_ClearRotationStack( void ); + +// +// g_namelog.c +// + +void G_namelog_connect( gclient_t *client ); +void G_namelog_disconnect( gclient_t *client ); +void G_namelog_restore( gclient_t *client ); +void G_namelog_update_score( gclient_t *client ); +void G_namelog_update_name( gclient_t *client ); +void G_namelog_cleanup( void ); + +// +// g_items.c +// + +gentity_t *G_ItemSpawn(const vec3_t origin, int type, int subtype, int mag); +gentity_t *G_ItemEject(const vec3_t origin_center, float origin_dispersal, float speed, int type, int subtype, int mag); + +//some maxs +#define MAX_FILEPATH 144 + +extern level_locals_t level; +extern gentity_t g_entities[ MAX_GENTITIES ]; + +#define FOFS(x) ((size_t)&(((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_minNameChangePeriod; +extern vmCvar_t g_maxNameChanges; + +extern vmCvar_t g_timelimit; +extern vmCvar_t g_suddenDeathTime; +extern vmCvar_t g_friendlyFire; +extern vmCvar_t g_friendlyBuildableFire; +extern vmCvar_t g_dretchPunt; +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_inactivity; +extern vmCvar_t g_debugMove; +extern vmCvar_t g_debugDamage; +extern vmCvar_t g_synchronousClients; +extern vmCvar_t g_motd; +extern vmCvar_t g_warmup; +extern vmCvar_t g_doWarmup; +extern vmCvar_t g_allowVote; +extern vmCvar_t g_voteLimit; +extern vmCvar_t g_suddenDeathVotePercent; +extern vmCvar_t g_suddenDeathVoteDelay; +extern vmCvar_t g_teamForceBalance; +extern vmCvar_t g_smoothClients; +extern vmCvar_t pmove_fixed; +extern vmCvar_t pmove_msec; + +extern vmCvar_t g_alienBuildPoints; +extern vmCvar_t g_alienBuildQueueTime; +extern vmCvar_t g_humanBuildPoints; +extern vmCvar_t g_humanBuildQueueTime; +extern vmCvar_t g_humanRepeaterBuildPoints; +extern vmCvar_t g_humanRepeaterBuildQueueTime; +extern vmCvar_t g_humanRepeaterMaxZones; +extern vmCvar_t g_humanStage; +extern vmCvar_t g_humanCredits; +extern vmCvar_t g_humanMaxStage; +extern vmCvar_t g_humanStage2Threshold; +extern vmCvar_t g_humanStage3Threshold; +extern vmCvar_t g_alienStage; +extern vmCvar_t g_alienCredits; +extern vmCvar_t g_alienMaxStage; +extern vmCvar_t g_alienStage2Threshold; +extern vmCvar_t g_alienStage3Threshold; +extern vmCvar_t g_teamImbalanceWarnings; +extern vmCvar_t g_freeFundPeriod; + +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_debugMapRotation; +extern vmCvar_t g_currentMapRotation; +extern vmCvar_t g_mapRotationNodes; +extern vmCvar_t g_mapRotationStack; +extern vmCvar_t g_nextMap; +extern vmCvar_t g_initialMapRotation; +extern vmCvar_t g_sayAreaRange; + +extern vmCvar_t g_debugVoices; +extern vmCvar_t g_voiceChats; + +extern vmCvar_t g_floodMaxDemerits; +extern vmCvar_t g_floodMinTime; + +extern vmCvar_t g_shove; + +extern vmCvar_t g_mapConfigs; + +extern vmCvar_t g_layouts; +extern vmCvar_t g_layoutAuto; + +extern vmCvar_t g_emoticonsAllowedInNames; + +extern vmCvar_t g_admin; +extern vmCvar_t g_adminTempBan; +extern vmCvar_t g_adminMaxBan; + +extern vmCvar_t g_privateMessages; +extern vmCvar_t g_specChat; +extern vmCvar_t g_publicAdminMessages; +extern vmCvar_t g_allowTeamOverlay; + +extern vmCvar_t g_censorship; + +extern vmCvar_t g_unlimited; +extern vmCvar_t g_instantBuild; +extern vmCvar_t g_cuboidSizeLimit; + +extern vmCvar_t g_buildableDensityLimit; +extern vmCvar_t g_buildableDensityLimitRange; + +void trap_Print( 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 ); +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_SetConfigstringRestrictions( int num, const clientList_t *clientList ); +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 ); + +void trap_AddCommand( const char *cmdName ); +void trap_RemoveCommand( const char *cmdName ); diff --git a/src/game/g_main.c b/src/game/g_main.c new file mode 100644 index 0000000..4bc03b5 --- /dev/null +++ b/src/game/g_main.c @@ -0,0 +1,2565 @@ +/* +=========================================================================== +Copyright (C) 1999-2005 Id Software, Inc. +Copyright (C) 2000-2009 Darklegion Development + +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" + +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 + /* certain cvars can be set in worldspawn, but we don't want those values to + persist, so keep track of non-worldspawn changes and restore that on map + end. unfortunately, if the server crashes, the value set in worldspawn may + persist */ + char *explicit; +} cvarTable_t; + +gentity_t g_entities[ MAX_GENTITIES ]; +gclient_t g_clients[ MAX_CLIENTS ]; + +vmCvar_t g_timelimit; +vmCvar_t g_suddenDeathTime; +vmCvar_t g_friendlyFire; +vmCvar_t g_friendlyBuildableFire; +vmCvar_t g_dretchPunt; +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_inactivity; +vmCvar_t g_debugMove; +vmCvar_t g_debugDamage; +vmCvar_t g_motd; +vmCvar_t g_synchronousClients; +vmCvar_t g_warmup; +vmCvar_t g_doWarmup; +vmCvar_t g_restarted; +vmCvar_t g_lockTeamsAtStart; +vmCvar_t g_logFile; +vmCvar_t g_logFileSync; +vmCvar_t g_allowVote; +vmCvar_t g_voteLimit; +vmCvar_t g_suddenDeathVotePercent; +vmCvar_t g_suddenDeathVoteDelay; +vmCvar_t g_teamForceBalance; +vmCvar_t g_smoothClients; +vmCvar_t pmove_fixed; +vmCvar_t pmove_msec; +vmCvar_t g_minNameChangePeriod; +vmCvar_t g_maxNameChanges; + +vmCvar_t g_alienBuildPoints; +vmCvar_t g_alienBuildQueueTime; +vmCvar_t g_humanBuildPoints; +vmCvar_t g_humanBuildQueueTime; +vmCvar_t g_humanRepeaterBuildPoints; +vmCvar_t g_humanRepeaterBuildQueueTime; +vmCvar_t g_humanRepeaterMaxZones; +vmCvar_t g_humanStage; +vmCvar_t g_humanCredits; +vmCvar_t g_humanMaxStage; +vmCvar_t g_humanStage2Threshold; +vmCvar_t g_humanStage3Threshold; +vmCvar_t g_alienStage; +vmCvar_t g_alienCredits; +vmCvar_t g_alienMaxStage; +vmCvar_t g_alienStage2Threshold; +vmCvar_t g_alienStage3Threshold; +vmCvar_t g_teamImbalanceWarnings; +vmCvar_t g_freeFundPeriod; + +vmCvar_t g_unlagged; + +vmCvar_t g_disabledEquipment; +vmCvar_t g_disabledClasses; +vmCvar_t g_disabledBuildables; + +vmCvar_t g_markDeconstruct; + +vmCvar_t g_debugMapRotation; +vmCvar_t g_currentMapRotation; +vmCvar_t g_mapRotationNodes; +vmCvar_t g_mapRotationStack; +vmCvar_t g_nextMap; +vmCvar_t g_initialMapRotation; + +vmCvar_t g_debugVoices; +vmCvar_t g_voiceChats; + +vmCvar_t g_shove; + +vmCvar_t g_mapConfigs; +vmCvar_t g_sayAreaRange; + +vmCvar_t g_floodMaxDemerits; +vmCvar_t g_floodMinTime; + +vmCvar_t g_layouts; +vmCvar_t g_layoutAuto; + +vmCvar_t g_emoticonsAllowedInNames; + +vmCvar_t g_admin; +vmCvar_t g_adminTempBan; +vmCvar_t g_adminMaxBan; + +vmCvar_t g_privateMessages; +vmCvar_t g_specChat; +vmCvar_t g_publicAdminMessages; +vmCvar_t g_allowTeamOverlay; + +vmCvar_t g_censorship; + +vmCvar_t g_tag; + +vmCvar_t g_unlimited; +vmCvar_t g_instantBuild; +vmCvar_t g_cuboidSizeLimit; +vmCvar_t g_cuboidMode; + +vmCvar_t g_buildableDensityLimit; +vmCvar_t g_buildableDensityLimitRange; + +// copy cvars that can be set in worldspawn so they can be restored later +static char cv_gravity[ MAX_CVAR_VALUE_STRING ]; +static char cv_humanMaxStage[ MAX_CVAR_VALUE_STRING ]; +static char cv_alienMaxStage[ MAX_CVAR_VALUE_STRING ]; + +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 }, + + // latched vars + + { &g_maxclients, "sv_maxclients", "8", CVAR_SERVERINFO | CVAR_LATCH | CVAR_ARCHIVE, 0, qfalse }, + + // change anytime vars + { &g_maxGameClients, "g_maxGameClients", "0", CVAR_SERVERINFO | CVAR_ARCHIVE, 0, qfalse }, + + { &g_timelimit, "timelimit", "0", CVAR_SERVERINFO | CVAR_ARCHIVE | CVAR_NORESTART, 0, qtrue }, + { &g_suddenDeathTime, "g_suddenDeathTime", "0", CVAR_SERVERINFO | CVAR_ARCHIVE | CVAR_NORESTART, 0, qtrue }, + + { &g_synchronousClients, "g_synchronousClients", "0", CVAR_SYSTEMINFO, 0, qfalse }, + + { &g_friendlyFire, "g_friendlyFire", "0", CVAR_SERVERINFO | CVAR_ARCHIVE, 0, qtrue }, + { &g_friendlyBuildableFire, "g_friendlyBuildableFire", "0", CVAR_SERVERINFO | CVAR_ARCHIVE, 0, qtrue }, + { &g_dretchPunt, "g_dretchPunt", "1", CVAR_ARCHIVE, 0, qtrue }, + + { &g_teamForceBalance, "g_teamForceBalance", "0", CVAR_ARCHIVE, 0, qtrue }, + + { &g_warmup, "g_warmup", "10", CVAR_ARCHIVE, 0, qtrue }, + { &g_doWarmup, "g_doWarmup", "0", 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_needpass, "g_needpass", "0", CVAR_SERVERINFO | CVAR_ROM, 0, qfalse }, + + { &g_dedicated, "dedicated", "0", 0, 0, qfalse }, + + { &g_speed, "g_speed", "320", 0, 0, qtrue }, + { &g_gravity, "g_gravity", "800", 0, 0, qtrue, cv_gravity }, + { &g_knockback, "g_knockback", "1000", 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_motd, "g_motd", "", 0, 0, qfalse }, + + { &g_allowVote, "g_allowVote", "1", CVAR_ARCHIVE, 0, qfalse }, + { &g_voteLimit, "g_voteLimit", "5", CVAR_ARCHIVE, 0, qfalse }, + { &g_suddenDeathVotePercent, "g_suddenDeathVotePercent", "74", CVAR_ARCHIVE, 0, qfalse }, + { &g_suddenDeathVoteDelay, "g_suddenDeathVoteDelay", "180", CVAR_ARCHIVE, 0, qfalse }, + { &g_minNameChangePeriod, "g_minNameChangePeriod", "5", 0, 0, qfalse}, + { &g_maxNameChanges, "g_maxNameChanges", "5", 0, 0, qfalse}, + + { &g_smoothClients, "g_smoothClients", "1", 0, 0, qfalse}, + { &pmove_fixed, "pmove_fixed", "0", CVAR_SYSTEMINFO, 0, qfalse}, + { &pmove_msec, "pmove_msec", "8", CVAR_SYSTEMINFO, 0, qfalse}, + + { &g_alienBuildPoints, "g_alienBuildPoints", DEFAULT_ALIEN_BUILDPOINTS, 0, 0, qfalse }, + { &g_alienBuildQueueTime, "g_alienBuildQueueTime", DEFAULT_ALIEN_QUEUE_TIME, CVAR_ARCHIVE, 0, qfalse }, + { &g_humanBuildPoints, "g_humanBuildPoints", DEFAULT_HUMAN_BUILDPOINTS, 0, 0, qfalse }, + { &g_humanBuildQueueTime, "g_humanBuildQueueTime", DEFAULT_HUMAN_QUEUE_TIME, CVAR_ARCHIVE, 0, qfalse }, + { &g_humanRepeaterBuildPoints, "g_humanRepeaterBuildPoints", DEFAULT_HUMAN_REPEATER_BUILDPOINTS, CVAR_ARCHIVE, 0, qfalse }, + { &g_humanRepeaterMaxZones, "g_humanRepeaterMaxZones", DEFAULT_HUMAN_REPEATER_MAX_ZONES, CVAR_ARCHIVE, 0, qfalse }, + { &g_humanRepeaterBuildQueueTime, "g_humanRepeaterBuildQueueTime", DEFAULT_HUMAN_REPEATER_QUEUE_TIME, CVAR_ARCHIVE, 0, qfalse }, + { &g_humanStage, "g_humanStage", "0", 0, 0, qfalse }, + { &g_humanCredits, "g_humanCredits", "0", 0, 0, qfalse }, + { &g_humanMaxStage, "g_humanMaxStage", DEFAULT_HUMAN_MAX_STAGE, 0, 0, qfalse, cv_humanMaxStage }, + { &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_alienCredits, "g_alienCredits", "0", 0, 0, qfalse }, + { &g_alienMaxStage, "g_alienMaxStage", DEFAULT_ALIEN_MAX_STAGE, 0, 0, qfalse, cv_alienMaxStage }, + { &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_freeFundPeriod, "g_freeFundPeriod", DEFAULT_FREEKILL_PERIOD, CVAR_ARCHIVE, 0, qtrue }, + + { &g_unlagged, "g_unlagged", "1", CVAR_SERVERINFO | CVAR_ARCHIVE, 0, qtrue }, + + { &g_disabledEquipment, "g_disabledEquipment", "", CVAR_ROM | CVAR_SYSTEMINFO, 0, qfalse }, + { &g_disabledClasses, "g_disabledClasses", "", CVAR_ROM | CVAR_SYSTEMINFO, 0, qfalse }, + { &g_disabledBuildables, "g_disabledBuildables", "", CVAR_ROM | CVAR_SYSTEMINFO, 0, qfalse }, + + { &g_sayAreaRange, "g_sayAreaRange", "1000", CVAR_ARCHIVE, 0, qtrue }, + + { &g_floodMaxDemerits, "g_floodMaxDemerits", "5000", CVAR_ARCHIVE, 0, qfalse }, + { &g_floodMinTime, "g_floodMinTime", "2000", CVAR_ARCHIVE, 0, qfalse }, + + { &g_markDeconstruct, "g_markDeconstruct", "3", CVAR_SERVERINFO | CVAR_ARCHIVE, 0, qtrue }, + + { &g_debugMapRotation, "g_debugMapRotation", "0", 0, 0, qfalse }, + { &g_currentMapRotation, "g_currentMapRotation", "-1", 0, 0, qfalse }, // -1 = NOT_ROTATING + { &g_mapRotationNodes, "g_mapRotationNodes", "", CVAR_ROM, 0, qfalse }, + { &g_mapRotationStack, "g_mapRotationStack", "", CVAR_ROM, 0, qfalse }, + { &g_nextMap, "g_nextMap", "", 0 , 0, qtrue }, + { &g_initialMapRotation, "g_initialMapRotation", "", CVAR_ARCHIVE, 0, qfalse }, + { &g_debugVoices, "g_debugVoices", "0", 0, 0, qfalse }, + { &g_voiceChats, "g_voiceChats", "1", CVAR_ARCHIVE, 0, qfalse }, + { &g_shove, "g_shove", "0.0", 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_emoticonsAllowedInNames, "g_emoticonsAllowedInNames", "1", CVAR_LATCH|CVAR_ARCHIVE, 0, qfalse }, + + { &g_admin, "g_admin", "admin.dat", CVAR_ARCHIVE, 0, qfalse }, + { &g_adminTempBan, "g_adminTempBan", "2m", CVAR_ARCHIVE, 0, qfalse }, + { &g_adminMaxBan, "g_adminMaxBan", "2w", CVAR_ARCHIVE, 0, qfalse }, + + { &g_privateMessages, "g_privateMessages", "1", CVAR_ARCHIVE, 0, qfalse }, + { &g_specChat, "g_specChat", "1", CVAR_ARCHIVE, 0, qfalse }, + { &g_publicAdminMessages, "g_publicAdminMessages", "1", CVAR_ARCHIVE, 0, qfalse }, + { &g_allowTeamOverlay, "g_allowTeamOverlay", "1", CVAR_ARCHIVE, 0, qtrue }, + + { &g_censorship, "g_censorship", "", CVAR_ARCHIVE, 0, qfalse }, + + { &g_tag, "g_tag", "gpp", CVAR_INIT, 0, qfalse }, + + { &g_unlimited, "g_unlimited", "0", CVAR_ARCHIVE | CVAR_SERVERINFO, 0, qfalse }, + { &g_instantBuild, "g_instantBuild", "0", CVAR_ARCHIVE | CVAR_SERVERINFO, 0, qfalse }, + { &g_cuboidSizeLimit, "g_cuboidSizeLimit", "0", CVAR_ARCHIVE, 0, qfalse }, + { &g_cuboidMode, "g_cuboidMode", "0", CVAR_ARCHIVE, 0, qfalse }, + + { &g_buildableDensityLimit, "g_buildableDensityLimit", "0", CVAR_ARCHIVE, 0, qfalse }, + { &g_buildableDensityLimitRange, "g_buildableDensityLimitRange", "0", 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 +================ +*/ +Q_EXPORT 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 ); + Q_vsnprintf( text, sizeof( text ), fmt, argptr ); + va_end( argptr ); + + trap_Print( text ); +} + +void QDECL G_Error( const char *fmt, ... ) +{ + va_list argptr; + char text[ 1024 ]; + + va_start( argptr, fmt ); + Q_vsnprintf( text, sizeof( 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 ); +} + + +/* +================= +G_RegisterCvars +================= +*/ +void G_RegisterCvars( void ) +{ + int i; + cvarTable_t *cv; + + 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->explicit ) + strcpy( cv->explicit, cv->vmCvar->string ); + } +} + +/* +================= +G_UpdateCvars +================= +*/ +void G_UpdateCvars( void ) +{ + int i; + cvarTable_t *cv; + + 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 ) ); + + if( !level.spawning && cv->explicit ) + strcpy( cv->explicit, cv->vmCvar->string ); + } + } + } +} + +/* +================= +G_RestoreCvars +================= +*/ +void G_RestoreCvars( void ) +{ + int i; + cvarTable_t *cv; + + for( i = 0, cv = gameCvarTable; i < gameCvarTableSize; i++, cv++ ) + { + if( cv->vmCvar && cv->explicit ) + trap_Cvar_Set( cv->cvarName, cv->explicit ); + } +} + +/* +================= +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_CheckCuboidConfig + +even tokens are map names +odd tokens are cuboid modes +================ +*/ +void G_CheckCuboidConfig(char* mapname) +{ + char* token; + char config[MAX_CVAR_VALUE_STRING]; //should be enough for few maps + qboolean type=qfalse; + qboolean found=qfalse; + int mode; + + mode=trap_Cvar_VariableIntegerValue("g_cuboidDefaultMode"); + trap_Cvar_VariableStringBuffer("g_cuboidConfig",config,sizeof(config)); + + while(1) + { + COM_Parse(&token); + + if(!token) + break; + + if(!type) + if(!Q_stricmp(token,mapname)) + found=qtrue; + else + if(found) + { + mode=atoi(token); + break; + } + } + Com_Printf("cuboids are %s%s^7 on %s\n",(mode==0?"^2ENABLED":(mode==1?"^1DISABLED ON S1":"^1DISABLED")),(found?"":" BY DEFAULT"),mapname); +} + +/* +============ +G_InitGame + +============ +*/ +void G_InitGame( int levelTime, int randomSeed, int restart ) +{ + int i; + char map[ MAX_CVAR_VALUE_STRING ] = {""}; + + srand( randomSeed ); + + G_RegisterCvars( ); + + G_Printf( "------- Game Initialization -------\n" ); + G_Printf( "gamename: %s\n", GAME_VERSION ); + G_Printf( "gamedate: %s\n", __DATE__ ); + + BG_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 + + 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" ); + + 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" ); + + G_RegisterCommands( ); + G_admin_readconfig( NULL ); + G_LoadCensors( ); + + // 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 ] ) ); + + level.emoticonCount = BG_LoadEmoticons( level.emoticons, MAX_EMOTICONS ); + + trap_SetConfigstring( CS_INTERMISSION, "0" ); + + // test to see if a custom buildable layout will be loaded + G_LayoutSelect( ); + + // this has to be flipped after the first UpdateCvars + level.spawning = qtrue; + // parse the key/value pairs and spawn gentities + G_SpawnEntitiesFromString( ); + + // load up a custom building layout if there is one + G_LayoutLoad( ); + + // find out g_cuboidMode value + G_CheckCuboidConfig( map ); + + // the map might disable some things + BG_InitAllowedGameElements( ); + + // general initialization + G_FindTeams( ); + + BG_InitClassConfigs( ); + BG_InitBuildableConfigs( ); + G_InitDamageLocations( ); + G_InitMapRotations( ); + G_InitSpawnQueue( &level.alienSpawnQueue ); + G_InitSpawnQueue( &level.humanSpawnQueue ); + + if( g_debugMapRotation.integer ) + G_PrintRotations( ); + + level.voices = BG_VoiceInit( ); + BG_PrintVoices( level.voices, g_debugVoices.integer ); + + //reset stages + trap_Cvar_Set( "g_alienStage", va( "%d", S1 ) ); + trap_Cvar_Set( "g_humanStage", va( "%d", S1 ) ); + trap_Cvar_Set( "g_alienCredits", 0 ); + trap_Cvar_Set( "g_humanCredits", 0 ); + level.suddenDeathBeginTime = g_suddenDeathTime.integer * 60000; + + G_Printf( "-----------------------------------\n" ); + + // So the server counts the spawns without a client attached + G_CountSpawns( ); + + G_UpdateTeamConfigStrings( ); + + 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 ) +{ + int i; + memset( level.voteTime, 0, sizeof( level.voteTime ) ); + for( i = 0; i < NUM_TEAMS; i++ ) + { + trap_SetConfigstring( CS_VOTE_TIME + i, "" ); + trap_SetConfigstring( CS_VOTE_STRING + i, "" ); + } +} + +/* +================= +G_ShutdownGame +================= +*/ +void G_ShutdownGame( int restart ) +{ + // in case of a map_restart + G_ClearVotes( ); + + G_RestoreCvars( ); + + G_Printf( "==== ShutdownGame ====\n" ); + + if( level.logFile ) + { + G_LogPrintf( "ShutdownGame:\n" ); + G_LogPrintf( "------------------------------------------------------------\n" ); + trap_FS_FCloseFile( level.logFile ); + level.logFile = 0; + } + + // write all the client session data so we can get it back + G_WriteSessionData( ); + + G_admin_cleanup( ); + G_namelog_cleanup( ); + G_UnregisterCommands( ); + + G_ShutdownMapRotations( ); + + level.restarted = qfalse; + level.surrenderTeam = TEAM_NONE; + trap_SetConfigstring( CS_WINNER, "" ); +} + + + +//=================================================================== + +void QDECL Com_Error( int level, const char *error, ... ) +{ + va_list argptr; + char text[ 1024 ]; + + va_start( argptr, error ); + Q_vsnprintf( text, sizeof( 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 ); + Q_vsnprintf( text, sizeof( 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->ps.persistant[ PERS_SCORE ] > cb->ps.persistant[ PERS_SCORE ] ) + return -1; + if( ca->ps.persistant[ PERS_SCORE ] < cb->ps.persistant[ 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 the 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_StopFollowing( g_entities + clientNum ); + 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( team_t team ) +{ + int clientNum; + gentity_t *ent, *spawn; + vec3_t spawn_origin, spawn_angles; + spawnQueue_t *sq = NULL; + int numSpawns = 0; + + if( team == TEAM_ALIENS ) + { + sq = &level.alienSpawnQueue; + numSpawns = level.numAlienSpawns + level.numAlienImplantedSpawns; + } + else if( team == TEAM_HUMANS ) + { + sq = &level.humanSpawnQueue; + numSpawns = level.numHumanSpawns + level.numHumanImplantedSpawns; + } + + 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.spectatorState = SPECTATOR_NOT; + 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.numAlienImplantedSpawns = 0; + level.numHumanSpawns = 0; + level.numHumanImplantedSpawns = 0; + for( i = 0, ent = g_entities + i ; i < level.num_entities ; i++, ent++ ) + { + if( !ent->inuse ) + continue; + + if( ent->s.eType == ET_BUILDABLE && ent->health > 0 ) + { + if( ent->s.modelindex == BA_A_SPAWN ) + level.numAlienSpawns++; + + else if( ent->s.modelindex == BA_H_SPAWN ) + level.numHumanSpawns++; + } + else if( ent->client && ent->client->sess.spectatorState == SPECTATOR_NOT && + ent->client->ps.stats[ STAT_HEALTH ] > 0 && + ent->client->isImpregnated && ent->client->isImplantMature ) + { + if( ent->client->pers.teamSelection == TEAM_HUMANS ) + level.numAlienImplantedSpawns++; + + else if( ent->client->pers.teamSelection == TEAM_ALIENS ) + level.numHumanImplantedSpawns++; + } + } +} + + +/* +============ +G_TimeTilSuddenDeath +============ +*/ +#define SUDDENDEATHWARNING 60000 +int G_TimeTilSuddenDeath( void ) +{ + if( ( !g_suddenDeathTime.integer && level.suddenDeathBeginTime == 0 ) || + ( level.suddenDeathBeginTime < 0 ) ) + return SUDDENDEATHWARNING + 1; // 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; + buildPointZone_t *zone; + + // BP queue updates + while( level.alienBuildPointQueue > 0 && + level.alienNextQueueTime < level.time ) + { + level.alienBuildPointQueue--; + level.alienNextQueueTime += G_NextQueueTime( level.alienBuildPointQueue, + g_alienBuildPoints.integer, + g_alienBuildQueueTime.integer ); + } + + while( level.humanBuildPointQueue > 0 && + level.humanNextQueueTime < level.time ) + { + level.humanBuildPointQueue--; + level.humanNextQueueTime += G_NextQueueTime( level.humanBuildPointQueue, + g_humanBuildPoints.integer, + g_humanBuildQueueTime.integer ); + } + + // Sudden Death checks + if( G_TimeTilSuddenDeath( ) <= 0 && level.suddenDeathWarning < TW_PASSED ) + { + G_LogPrintf( "Beginning Sudden Death\n" ); + trap_SendServerCommand( -1, "cp \"Sudden Death!\"" ); + trap_SendServerCommand( -1, "print \"Beginning Sudden Death.\n\"" ); + level.suddenDeathWarning = TW_PASSED; + G_ClearDeconMarks( ); + + // Clear blueprints, or else structs that cost 0 BP can still be built after SD + for( i = 0; i < level.maxclients; i++ ) + { + if( g_entities[ i ].client->ps.stats[ STAT_BUILDABLE ] != BA_NONE ) + g_entities[ i ].client->ps.stats[ STAT_BUILDABLE ] = BA_NONE; + } + } + else if( G_TimeTilSuddenDeath( ) <= SUDDENDEATHWARNING && + level.suddenDeathWarning < TW_IMMINENT ) + { + trap_SendServerCommand( -1, va( "cp \"Sudden Death in %d seconds!\"", + (int)( G_TimeTilSuddenDeath( ) / 1000 ) ) ); + trap_SendServerCommand( -1, va( "print \"Sudden Death will begin in %d seconds.\n\"", + (int)( G_TimeTilSuddenDeath( ) / 1000 ) ) ); + level.suddenDeathWarning = TW_IMMINENT; + } + + level.humanBuildPoints = g_humanBuildPoints.integer - level.humanBuildPointQueue; + level.alienBuildPoints = g_alienBuildPoints.integer - level.alienBuildPointQueue; + + // Reset buildPointZones + for( i = 0; i < g_humanRepeaterMaxZones.integer; i++ ) + { + buildPointZone_t *zone = &level.buildPointZones[ i ]; + + zone->active = qfalse; + zone->totalBuildPoints = g_humanRepeaterBuildPoints.integer; + } + + // Iterate through entities + for( i = MAX_CLIENTS; i < level.num_entities; i++ ) + { + gentity_t *ent = &g_entities[ i ]; + buildPointZone_t *zone; + buildable_t buildable; + int cost; + + if( ent->s.eType != ET_BUILDABLE || ent->s.eFlags & EF_DEAD ) + continue; + + // mark a zone as active + if( ent->usesBuildPointZone ) + { + assert( ent->buildPointZone >= 0 && ent->buildPointZone < g_humanRepeaterMaxZones.integer ); + + zone = &level.buildPointZones[ ent->buildPointZone ]; + zone->active = qtrue; + } + + // Subtract the BP from the appropriate pool + buildable = ent->s.modelindex; + cost = BG_Buildable( buildable, ent->cuboidSize )->buildPoints; + + if( ent->buildableTeam == TEAM_ALIENS ) + level.alienBuildPoints -= cost; + if( buildable == BA_H_REPEATER ) + level.humanBuildPoints -= cost; + else if( buildable != BA_H_REACTOR ) + { + gentity_t *power = G_PowerEntityForEntity( ent ); + + if( power ) + { + if( power->s.modelindex == BA_H_REACTOR ) + level.humanBuildPoints -= cost; + else if( power->s.modelindex == BA_H_REPEATER && power->usesBuildPointZone ) + level.buildPointZones[ power->buildPointZone ].totalBuildPoints -= cost; + } + } + } + + // Finally, update repeater zones and their queues + // note that this has to be done after the used BP is calculated + for( i = MAX_CLIENTS; i < level.num_entities; i++ ) + { + gentity_t *ent = &g_entities[ i ]; + + if( ent->s.eType != ET_BUILDABLE || ent->s.eFlags & EF_DEAD || + ent->buildableTeam != TEAM_HUMANS ) + continue; + + buildable = ent->s.modelindex; + + if( buildable != BA_H_REPEATER ) + continue; + + if( ent->usesBuildPointZone && level.buildPointZones[ ent->buildPointZone ].active ) + { + zone = &level.buildPointZones[ ent->buildPointZone ]; + + if( G_TimeTilSuddenDeath( ) > 0 ) + { + // BP queue updates + while( zone->queuedBuildPoints > 0 && + zone->nextQueueTime < level.time ) + { + zone->queuedBuildPoints--; + zone->nextQueueTime += G_NextQueueTime( zone->queuedBuildPoints, + zone->totalBuildPoints, + g_humanRepeaterBuildQueueTime.integer ); + } + } + else + { + zone->totalBuildPoints = zone->queuedBuildPoints = 0; + } + } + } + + if( level.humanBuildPoints < 0 ) + level.humanBuildPoints = 0; + + if( level.alienBuildPoints < 0 ) + level.alienBuildPoints = 0; +} + +/* +============ +G_CalculateStages +============ +*/ +void G_CalculateStages( void ) +{ + float alienPlayerCountMod = level.averageNumAlienClients / PLAYER_COUNT_MOD; + float humanPlayerCountMod = level.averageNumHumanClients / PLAYER_COUNT_MOD; + int alienNextStageThreshold, humanNextStageThreshold; + static int lastAlienStageModCount = 1; + static int lastHumanStageModCount = 1; + + if( alienPlayerCountMod < 0.1f ) + alienPlayerCountMod = 0.1f; + + if( humanPlayerCountMod < 0.1f ) + humanPlayerCountMod = 0.1f; + + if( g_alienCredits.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_alienCredits.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_humanCredits.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_humanCredits.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; + lastHumanStageModCount = g_humanStage.modificationCount; + G_LogPrintf("Stage: H 3: Humans reached Stage 3\n"); + } + + if( g_alienStage.modificationCount > lastAlienStageModCount ) + { + G_Checktrigger_stages( TEAM_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( TEAM_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; + } + + 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; + + // save a lot of bandwidth by rounding thresholds up to the nearest 100 + if( alienNextStageThreshold > 0 ) + alienNextStageThreshold = ceil( (float)alienNextStageThreshold / 100 ) * 100; + + if( humanNextStageThreshold > 0 ) + humanNextStageThreshold = ceil( (float)humanNextStageThreshold / 100 ) * 100; + + trap_SetConfigstring( CS_ALIEN_STAGES, va( "%d %d %d", + g_alienStage.integer, g_alienCredits.integer, + alienNextStageThreshold ) ); + + trap_SetConfigstring( CS_HUMAN_STAGES, va( "%d %d %d", + g_humanStage.integer, g_humanCredits.integer, + humanNextStageThreshold ) ); +} + +/* +============ +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_alienCredits", "0" ); + } + + if( !level.numHumanClients ) + { + level.numHumanSamples = 0; + trap_Cvar_Set( "g_humanCredits", "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 ] = {""}; + + level.numConnectedClients = 0; + level.numPlayingClients = 0; + memset( level.numVotingClients, 0, sizeof( level.numVotingClients ) ); + 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; + + level.numVotingClients[ TEAM_NONE ]++; + + if( level.clients[ i ].pers.connected != CON_CONNECTED ) + continue; + + if( level.clients[ i ].pers.teamSelection != TEAM_NONE ) + { + level.numPlayingClients++; + if( level.clients[ i ].pers.teamSelection == TEAM_ALIENS ) + { + level.numAlienClients++; + if( level.clients[ i ].sess.spectatorState == SPECTATOR_NOT ) + level.numLiveAlienClients++; + } + else if( level.clients[ i ].pers.teamSelection == TEAM_HUMANS ) + { + level.numHumanClients++; + if( level.clients[ i ].sess.spectatorState == SPECTATOR_NOT ) + level.numLiveHumanClients++; + } + } + } + } + level.numNonSpectatorClients = level.numLiveAlienClients + + level.numLiveHumanClients; + level.numVotingClients[ TEAM_ALIENS ] = level.numAlienClients; + level.numVotingClients[ TEAM_HUMANS ] = level.numHumanClients; + P[ i ] = '\0'; + trap_Cvar_Set( "P", P ); + + 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 ) + 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.misc, 0, sizeof( ent->client->ps.misc ) ); + + 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.intermissiontime = level.time; + + G_ClearVotes( ); + + G_UpdateTeamConfigStrings( ); + + 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( ); +} + + +/* +============= +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; + + if ( G_MapExists( g_nextMap.string ) ) + trap_SendConsoleCommand( EXEC_APPEND, va("map \"%s\"\n", g_nextMap.string ) ); + else if( G_MapRotationActive( ) ) + G_AdvanceMapRotation( 0 ); + 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_AdminMessage + +Print to all active server admins, and to the logfile, and to the server console +================= +*/ +void G_AdminMessage( gentity_t *ent, const char *msg ) +{ + char string[ 1024 ]; + int i; + + Com_sprintf( string, sizeof( string ), "chat %d %d \"%s\"", + ent ? ent - g_entities : -1, + G_admin_permission( ent, ADMF_ADMINCHAT ) ? SAY_ADMINS : SAY_ADMINS_PUBLIC, + msg ); + + // Send to all appropriate clients + for( i = 0; i < level.maxclients; i++ ) + if( G_admin_permission( &g_entities[ i ], ADMF_ADMINCHAT ) ) + trap_SendServerCommand( i, string ); + + // Send to the logfile and server console + G_LogPrintf( "%s: %d \"%s" S_COLOR_WHITE "\": " S_COLOR_MAGENTA "%s\n", + G_admin_permission( ent, ADMF_ADMINCHAT ) ? "AdminMsg" : "AdminMsgPublic", + ent ? ent - g_entities : -1, ent ? ent->client->pers.netname : "console", + msg ); +} + + +/* +================= +G_LogPrintf + +Print to the logfile with a time stamp if it is open, and to the server console +================= +*/ +void QDECL G_LogPrintf( const char *fmt, ... ) +{ + va_list argptr; + char string[ 1024 ], decolored[ 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 ); + Q_vsnprintf( string + 7, sizeof( string ) - 7, fmt, argptr ); + va_end( argptr ); + + if( g_dedicated.integer ) + { + G_UnEscapeString( string, decolored, sizeof( decolored ) ); + G_Printf( "%s", decolored + 7 ); + } + + if( !level.logFile ) + return; + + G_DecolorString( string, decolored, sizeof( decolored ) ); + trap_FS_Write( decolored, strlen( decolored ), level.logFile ); +} + +/* +================= +G_SendGameStat +================= +*/ +void G_SendGameStat( team_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; + + // games with cheats enabled are not very good for balance statistics + if( g_cheats.integer ) + return; + + trap_Cvar_VariableStringBuffer( "mapname", map, sizeof( map ) ); + + switch( team ) + { + case TEAM_ALIENS: teamChar = 'A'; break; + case TEAM_HUMANS: teamChar = 'H'; break; + case TEAM_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 ] ]; + + if( cl->pers.connected == CON_CONNECTING ) + ping = -1; + else + ping = cl->ps.ping < 999 ? cl->ps.ping : 999; + + switch( cl->ps.stats[ STAT_TEAM ] ) + { + case TEAM_ALIENS: teamChar = 'A'; break; + case TEAM_HUMANS: teamChar = 'H'; break; + case TEAM_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_TEAM ] == TEAM_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; + int i; + gclient_t *cl; + clientList_t readyMasks; + + //if no clients are connected, just exit + if( level.numConnectedClients == 0 ) + { + ExitLevel( ); + return; + } + + // see which players are ready + ready = 0; + notReady = 0; + Com_Memset( &readyMasks, 0, sizeof( readyMasks ) ); + for( i = 0; i < g_maxclients.integer; i++ ) + { + cl = level.clients + i; + + if( cl->pers.connected != CON_CONNECTED ) + continue; + + if( cl->ps.stats[ STAT_TEAM ] == TEAM_NONE ) + continue; + + if( cl->readyToExit ) + { + ready++; + + Com_ClientListAdd( &readyMasks, i ); + } + else + notReady++; + } + + trap_SetConfigstring( CS_CLIENTS_READY, Com_ClientListString( &readyMasks ) ); + + // 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 == 0 && notReady > 0 ) + { + level.readyToExit = qfalse; + return; + } + + // if everyone wants to go, go now + if( notReady == 0 ) + { + 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 = TEAM_NONE; + trap_SendServerCommand( -1, "print \"Timelimit hit\n\"" ); + trap_SetConfigstring( CS_WINNER, "Stalemate" ); + LogExit( "Timelimit hit." ); + 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.uncondAlienWin && + ( level.time > level.startTime + 1000 ) && + ( level.numAlienSpawns == 0 ) && + ( level.numAlienImplantedSpawns == 0 ) && + ( level.numLiveAlienClients == 0 ) ) ) + { + //humans win + level.lastWin = TEAM_HUMANS; + trap_SendServerCommand( -1, "print \"Humans win\n\""); + trap_SetConfigstring( CS_WINNER, "Humans Win" ); + LogExit( "Humans win." ); + } + else if( level.uncondAlienWin || + ( ( level.time > level.startTime + 1000 ) && + ( level.numHumanSpawns == 0 ) && + ( level.numHumanImplantedSpawns == 0 ) && + ( level.numLiveHumanClients == 0 ) ) ) + { + //aliens win + level.lastWin = TEAM_ALIENS; + trap_SendServerCommand( -1, "print \"Aliens win\n\""); + trap_SetConfigstring( CS_WINNER, "Aliens Win" ); + LogExit( "Aliens win." ); + } +} + +/* +================== +G_Vote +================== +*/ +void G_Vote( gentity_t *ent, team_t team, qboolean voting ) +{ + if( !level.voteTime[ team ] ) + return; + + if( voting && ent->client->pers.voted & ( 1 << team ) ) + return; + + if( !voting && !( ent->client->pers.voted & ( 1 << team ) ) ) + return; + + ent->client->pers.voted |= 1 << team; + + if( ent->client->pers.vote & ( 1 << team ) ) + { + if( voting ) + level.voteYes[ team ]++; + else + level.voteYes[ team ]--; + + trap_SetConfigstring( CS_VOTE_YES + team, + va( "%d", level.voteYes[ team ] ) ); + } + else + { + if( voting ) + level.voteNo[ team ]++; + else + level.voteNo[ team ]--; + + trap_SetConfigstring( CS_VOTE_NO + team, + va( "%d", level.voteNo[ team ] ) ); + } +} + + +/* +======================================================================== + +FUNCTIONS CALLED EVERY FRAME + +======================================================================== +*/ + + +void G_ExecuteVote( team_t team ) +{ + level.voteExecuteTime[ team ] = 0; + + trap_SendConsoleCommand( EXEC_APPEND, va( "%s\n", + level.voteString[ team ] ) ); + + if( !Q_stricmpn( level.voteString[ team ], "map", 3 ) ) + level.restarted = qtrue; +} + +/* +================== +G_CheckVote +================== +*/ +void G_CheckVote( team_t team ) +{ + float votePassThreshold = (float)level.voteThreshold[ team ] / 100.0f; + qboolean pass = qfalse; + char *msg; + int i; + + if( level.voteExecuteTime[ team ] && + level.voteExecuteTime[ team ] < level.time ) + { + G_ExecuteVote( team ); + } + + if( !level.voteTime[ team ] ) + return; + + if( ( level.time - level.voteTime[ team ] >= VOTE_TIME ) || + ( level.voteYes[ team ] + level.voteNo[ team ] == level.numVotingClients[ team ] ) ) + { + pass = ( level.voteYes[ team ] && + (float)level.voteYes[ team ] / ( (float)level.voteYes[ team ] + (float)level.voteNo[ team ] ) > votePassThreshold ); + } + else + { + if( (float)level.voteYes[ team ] > + (float)level.numVotingClients[ team ] * votePassThreshold ) + { + pass = qtrue; + } + else if( (float)level.voteNo[ team ] <= + (float)level.numVotingClients[ team ] * ( 1.0f - votePassThreshold ) ) + { + return; + } + } + + if( pass ) + level.voteExecuteTime[ team ] = level.time + level.voteDelay[ team ]; + + G_LogPrintf( "EndVote: %s %s %d %d %d\n", + team == TEAM_NONE ? "global" : BG_TeamName( team ), + pass ? "pass" : "fail", + level.voteYes[ team ], level.voteNo[ team ], level.numVotingClients[ team ] ); + + msg = va( "print \"%sote %sed (%d - %d)\n\"", + team == TEAM_NONE ? "V" : "Team v", pass ? "pass" : "fail", + level.voteYes[ team ], level.voteNo[ team ] ); + + if( team == TEAM_NONE ) + trap_SendServerCommand( -1, msg ); + else + G_TeamCommand( team, msg ); + + level.voteTime[ team ] = 0; + level.voteYes[ team ] = 0; + level.voteNo[ team ] = 0; + + for( i = 0; i < level.maxclients; i++ ) + level.clients[ i ].pers.voted &= ~( 1 << team ); + + trap_SetConfigstring( CS_VOTE_TIME + team, "" ); + trap_SetConfigstring( CS_VOTE_STRING + team, "" ); + trap_SetConfigstring( CS_VOTE_YES + team, "0" ); + trap_SetConfigstring( CS_VOTE_NO + team, "0" ); +} + + +/* +================== +CheckCvars +================== +*/ +void CheckCvars( void ) +{ + static int lastPasswordModCount = -1; + static int lastMarkDeconModCount = -1; + static int lastSDTimeModCount = -1; + static int lastNumZones = 0; + + 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 ) + { + lastMarkDeconModCount = g_markDeconstruct.modificationCount; + G_ClearDeconMarks( ); + } + + // If we change g_suddenDeathTime during a map, we need to update + // when sd will begin + if( g_suddenDeathTime.modificationCount != lastSDTimeModCount ) + { + lastSDTimeModCount = g_suddenDeathTime.modificationCount; + level.suddenDeathBeginTime = g_suddenDeathTime.integer * 60000; + } + + // If the number of zones changes, we need a new array + if( g_humanRepeaterMaxZones.integer != lastNumZones ) + { + buildPointZone_t *newZones; + size_t newsize = g_humanRepeaterMaxZones.integer * sizeof( buildPointZone_t ); + size_t oldsize = lastNumZones * sizeof( buildPointZone_t ); + + newZones = BG_Alloc( newsize ); + if( level.buildPointZones ) + { + Com_Memcpy( newZones, level.buildPointZones, MIN( oldsize, newsize ) ); + BG_Free( level.buildPointZones ); + } + + level.buildPointZones = newZones; + lastNumZones = g_humanRepeaterMaxZones.integer; + } + + 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; + static int ptime3000 = 0; + + // if we are waiting for the level to restart, do nothing + if( level.restarted ) + return; + + if( level.pausedTime ) + { + msec = levelTime - level.time - level.pausedTime; + level.pausedTime = levelTime - level.time; + + ptime3000 += msec; + while( ptime3000 > 3000 ) + { + ptime3000 -= 3000; + trap_SendServerCommand( -1, "cp \"The game has been paused. Please wait.\"" ); + + if( level.pausedTime >= 110000 && level.pausedTime <= 119000 ) + trap_SendServerCommand( -1, va( "print \"Server: Game will auto-unpause in %d seconds\n\"", + (int) ( (float) ( 120000 - level.pausedTime ) / 1000.0f ) ) ); + } + + // Prevents clients from getting lagged-out messages + for( i = 0; i < level.maxclients; i++ ) + { + if( level.clients[ i ].pers.connected == CON_CONNECTED ) + level.clients[ i ].ps.commandTime = levelTime; + } + + if( level.pausedTime > 120000 ) + { + trap_SendServerCommand( -1, "print \"Server: The game has been unpaused automatically (2 minute max)\n\"" ); + trap_SendServerCommand( -1, "cp \"The game has been unpaused!\"" ); + level.pausedTime = 0; + } + + return; + } + + level.framenum++; + level.previousTime = level.time; + level.time = levelTime; + msec = level.time - level.previousTime; + + // get any cvar changes + G_UpdateCvars( ); + CheckCvars( ); + // now we are done spawning + level.spawning = qfalse; + + // + // go through all allocated objects + // + 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; + + // 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 ); + } + + // 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( ); + + G_CountSpawns( ); + G_CalculateBuildPoints( ); + G_CalculateStages( ); + + G_SpawnClients( TEAM_ALIENS ); + G_SpawnClients( TEAM_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 + for( i = 0; i < NUM_TEAMS; i++ ) + G_CheckVote( i ); + + level.frameMsec = trap_Milliseconds(); +} + diff --git a/src/game/g_main.c.orig b/src/game/g_main.c.orig new file mode 100644 index 0000000..33784c9 --- /dev/null +++ b/src/game/g_main.c.orig @@ -0,0 +1,2548 @@ +/* +=========================================================================== +Copyright (C) 1999-2005 Id Software, Inc. +Copyright (C) 2000-2009 Darklegion Development + +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" + +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 + /* certain cvars can be set in worldspawn, but we don't want those values to + persist, so keep track of non-worldspawn changes and restore that on map + end. unfortunately, if the server crashes, the value set in worldspawn may + persist */ + char *explicit; +} cvarTable_t; + +gentity_t g_entities[ MAX_GENTITIES ]; +gclient_t g_clients[ MAX_CLIENTS ]; + +vmCvar_t g_timelimit; +vmCvar_t g_suddenDeathTime; +vmCvar_t g_friendlyFire; +vmCvar_t g_friendlyBuildableFire; +vmCvar_t g_dretchPunt; +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_inactivity; +vmCvar_t g_debugMove; +vmCvar_t g_debugDamage; +vmCvar_t g_motd; +vmCvar_t g_synchronousClients; +vmCvar_t g_warmup; +vmCvar_t g_doWarmup; +vmCvar_t g_restarted; +vmCvar_t g_lockTeamsAtStart; +vmCvar_t g_logFile; +vmCvar_t g_logFileSync; +vmCvar_t g_allowVote; +vmCvar_t g_voteLimit; +vmCvar_t g_suddenDeathVotePercent; +vmCvar_t g_suddenDeathVoteDelay; +vmCvar_t g_teamForceBalance; +vmCvar_t g_smoothClients; +vmCvar_t pmove_fixed; +vmCvar_t pmove_msec; +vmCvar_t g_minNameChangePeriod; +vmCvar_t g_maxNameChanges; + +vmCvar_t g_alienBuildPoints; +vmCvar_t g_alienBuildQueueTime; +vmCvar_t g_humanBuildPoints; +vmCvar_t g_humanBuildQueueTime; +vmCvar_t g_humanRepeaterBuildPoints; +vmCvar_t g_humanRepeaterBuildQueueTime; +vmCvar_t g_humanRepeaterMaxZones; +vmCvar_t g_humanStage; +vmCvar_t g_humanCredits; +vmCvar_t g_humanMaxStage; +vmCvar_t g_humanStage2Threshold; +vmCvar_t g_humanStage3Threshold; +vmCvar_t g_alienStage; +vmCvar_t g_alienCredits; +vmCvar_t g_alienMaxStage; +vmCvar_t g_alienStage2Threshold; +vmCvar_t g_alienStage3Threshold; +vmCvar_t g_teamImbalanceWarnings; +vmCvar_t g_freeFundPeriod; + +vmCvar_t g_unlagged; + +vmCvar_t g_disabledEquipment; +vmCvar_t g_disabledClasses; +vmCvar_t g_disabledBuildables; + +vmCvar_t g_markDeconstruct; + +vmCvar_t g_debugMapRotation; +vmCvar_t g_currentMapRotation; +vmCvar_t g_mapRotationNodes; +vmCvar_t g_mapRotationStack; +vmCvar_t g_nextMap; +vmCvar_t g_initialMapRotation; + +vmCvar_t g_debugVoices; +vmCvar_t g_voiceChats; + +vmCvar_t g_shove; + +vmCvar_t g_mapConfigs; +vmCvar_t g_sayAreaRange; + +vmCvar_t g_floodMaxDemerits; +vmCvar_t g_floodMinTime; + +vmCvar_t g_layouts; +vmCvar_t g_layoutAuto; + +vmCvar_t g_emoticonsAllowedInNames; + +vmCvar_t g_admin; +vmCvar_t g_adminTempBan; +vmCvar_t g_adminMaxBan; + +vmCvar_t g_privateMessages; +vmCvar_t g_specChat; +vmCvar_t g_publicAdminMessages; +vmCvar_t g_allowTeamOverlay; + +vmCvar_t g_censorship; + +vmCvar_t g_tag; + +vmCvar_t g_unlimited; +vmCvar_t g_instantBuild; +vmCvar_t g_cuboidSizeLimit; +vmCvar_t g_cuboidMode; + +vmCvar_t g_buildableDensityLimit; +vmCvar_t g_buildableDensityLimitRange; + +// copy cvars that can be set in worldspawn so they can be restored later +static char cv_gravity[ MAX_CVAR_VALUE_STRING ]; +static char cv_humanMaxStage[ MAX_CVAR_VALUE_STRING ]; +static char cv_alienMaxStage[ MAX_CVAR_VALUE_STRING ]; + +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 }, + + // latched vars + + { &g_maxclients, "sv_maxclients", "8", CVAR_SERVERINFO | CVAR_LATCH | CVAR_ARCHIVE, 0, qfalse }, + + // change anytime vars + { &g_maxGameClients, "g_maxGameClients", "0", CVAR_SERVERINFO | CVAR_ARCHIVE, 0, qfalse }, + + { &g_timelimit, "timelimit", "0", CVAR_SERVERINFO | CVAR_ARCHIVE | CVAR_NORESTART, 0, qtrue }, + { &g_suddenDeathTime, "g_suddenDeathTime", "0", CVAR_SERVERINFO | CVAR_ARCHIVE | CVAR_NORESTART, 0, qtrue }, + + { &g_synchronousClients, "g_synchronousClients", "0", CVAR_SYSTEMINFO, 0, qfalse }, + + { &g_friendlyFire, "g_friendlyFire", "0", CVAR_SERVERINFO | CVAR_ARCHIVE, 0, qtrue }, + { &g_friendlyBuildableFire, "g_friendlyBuildableFire", "0", CVAR_SERVERINFO | CVAR_ARCHIVE, 0, qtrue }, + { &g_dretchPunt, "g_dretchPunt", "1", CVAR_ARCHIVE, 0, qtrue }, + + { &g_teamForceBalance, "g_teamForceBalance", "0", CVAR_ARCHIVE, 0, qtrue }, + + { &g_warmup, "g_warmup", "10", CVAR_ARCHIVE, 0, qtrue }, + { &g_doWarmup, "g_doWarmup", "0", 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_needpass, "g_needpass", "0", CVAR_SERVERINFO | CVAR_ROM, 0, qfalse }, + + { &g_dedicated, "dedicated", "0", 0, 0, qfalse }, + + { &g_speed, "g_speed", "320", 0, 0, qtrue }, + { &g_gravity, "g_gravity", "800", 0, 0, qtrue, cv_gravity }, + { &g_knockback, "g_knockback", "1000", 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_motd, "g_motd", "", 0, 0, qfalse }, + + { &g_allowVote, "g_allowVote", "1", CVAR_ARCHIVE, 0, qfalse }, + { &g_voteLimit, "g_voteLimit", "5", CVAR_ARCHIVE, 0, qfalse }, + { &g_suddenDeathVotePercent, "g_suddenDeathVotePercent", "74", CVAR_ARCHIVE, 0, qfalse }, + { &g_suddenDeathVoteDelay, "g_suddenDeathVoteDelay", "180", CVAR_ARCHIVE, 0, qfalse }, + { &g_minNameChangePeriod, "g_minNameChangePeriod", "5", 0, 0, qfalse}, + { &g_maxNameChanges, "g_maxNameChanges", "5", 0, 0, qfalse}, + + { &g_smoothClients, "g_smoothClients", "1", 0, 0, qfalse}, + { &pmove_fixed, "pmove_fixed", "0", CVAR_SYSTEMINFO, 0, qfalse}, + { &pmove_msec, "pmove_msec", "8", CVAR_SYSTEMINFO, 0, qfalse}, + + { &g_alienBuildPoints, "g_alienBuildPoints", DEFAULT_ALIEN_BUILDPOINTS, 0, 0, qfalse }, + { &g_alienBuildQueueTime, "g_alienBuildQueueTime", DEFAULT_ALIEN_QUEUE_TIME, CVAR_ARCHIVE, 0, qfalse }, + { &g_humanBuildPoints, "g_humanBuildPoints", DEFAULT_HUMAN_BUILDPOINTS, 0, 0, qfalse }, + { &g_humanBuildQueueTime, "g_humanBuildQueueTime", DEFAULT_HUMAN_QUEUE_TIME, CVAR_ARCHIVE, 0, qfalse }, + { &g_humanRepeaterBuildPoints, "g_humanRepeaterBuildPoints", DEFAULT_HUMAN_REPEATER_BUILDPOINTS, CVAR_ARCHIVE, 0, qfalse }, + { &g_humanRepeaterMaxZones, "g_humanRepeaterMaxZones", DEFAULT_HUMAN_REPEATER_MAX_ZONES, CVAR_ARCHIVE, 0, qfalse }, + { &g_humanRepeaterBuildQueueTime, "g_humanRepeaterBuildQueueTime", DEFAULT_HUMAN_REPEATER_QUEUE_TIME, CVAR_ARCHIVE, 0, qfalse }, + { &g_humanStage, "g_humanStage", "0", 0, 0, qfalse }, + { &g_humanCredits, "g_humanCredits", "0", 0, 0, qfalse }, + { &g_humanMaxStage, "g_humanMaxStage", DEFAULT_HUMAN_MAX_STAGE, 0, 0, qfalse, cv_humanMaxStage }, + { &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_alienCredits, "g_alienCredits", "0", 0, 0, qfalse }, + { &g_alienMaxStage, "g_alienMaxStage", DEFAULT_ALIEN_MAX_STAGE, 0, 0, qfalse, cv_alienMaxStage }, + { &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_freeFundPeriod, "g_freeFundPeriod", DEFAULT_FREEKILL_PERIOD, CVAR_ARCHIVE, 0, qtrue }, + + { &g_unlagged, "g_unlagged", "1", CVAR_SERVERINFO | CVAR_ARCHIVE, 0, qtrue }, + + { &g_disabledEquipment, "g_disabledEquipment", "", CVAR_ROM | CVAR_SYSTEMINFO, 0, qfalse }, + { &g_disabledClasses, "g_disabledClasses", "", CVAR_ROM | CVAR_SYSTEMINFO, 0, qfalse }, + { &g_disabledBuildables, "g_disabledBuildables", "", CVAR_ROM | CVAR_SYSTEMINFO, 0, qfalse }, + + { &g_sayAreaRange, "g_sayAreaRange", "1000", CVAR_ARCHIVE, 0, qtrue }, + + { &g_floodMaxDemerits, "g_floodMaxDemerits", "5000", CVAR_ARCHIVE, 0, qfalse }, + { &g_floodMinTime, "g_floodMinTime", "2000", CVAR_ARCHIVE, 0, qfalse }, + + { &g_markDeconstruct, "g_markDeconstruct", "3", CVAR_SERVERINFO | CVAR_ARCHIVE, 0, qtrue }, + + { &g_debugMapRotation, "g_debugMapRotation", "0", 0, 0, qfalse }, + { &g_currentMapRotation, "g_currentMapRotation", "-1", 0, 0, qfalse }, // -1 = NOT_ROTATING + { &g_mapRotationNodes, "g_mapRotationNodes", "", CVAR_ROM, 0, qfalse }, + { &g_mapRotationStack, "g_mapRotationStack", "", CVAR_ROM, 0, qfalse }, + { &g_nextMap, "g_nextMap", "", 0 , 0, qtrue }, + { &g_initialMapRotation, "g_initialMapRotation", "", CVAR_ARCHIVE, 0, qfalse }, + { &g_debugVoices, "g_debugVoices", "0", 0, 0, qfalse }, + { &g_voiceChats, "g_voiceChats", "1", CVAR_ARCHIVE, 0, qfalse }, + { &g_shove, "g_shove", "0.0", 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_emoticonsAllowedInNames, "g_emoticonsAllowedInNames", "1", CVAR_LATCH|CVAR_ARCHIVE, 0, qfalse }, + + { &g_admin, "g_admin", "admin.dat", CVAR_ARCHIVE, 0, qfalse }, + { &g_adminTempBan, "g_adminTempBan", "2m", CVAR_ARCHIVE, 0, qfalse }, + { &g_adminMaxBan, "g_adminMaxBan", "2w", CVAR_ARCHIVE, 0, qfalse }, + + { &g_privateMessages, "g_privateMessages", "1", CVAR_ARCHIVE, 0, qfalse }, + { &g_specChat, "g_specChat", "1", CVAR_ARCHIVE, 0, qfalse }, + { &g_publicAdminMessages, "g_publicAdminMessages", "1", CVAR_ARCHIVE, 0, qfalse }, + { &g_allowTeamOverlay, "g_allowTeamOverlay", "1", CVAR_ARCHIVE, 0, qtrue }, + + { &g_censorship, "g_censorship", "", CVAR_ARCHIVE, 0, qfalse }, + + { &g_tag, "g_tag", "gpp", CVAR_INIT, 0, qfalse }, + + { &g_unlimited, "g_unlimited", "0", CVAR_ARCHIVE | CVAR_SERVERINFO, 0, qfalse }, + { &g_instantBuild, "g_instantBuild", "0", CVAR_ARCHIVE | CVAR_SERVERINFO, 0, qfalse }, + { &g_cuboidSizeLimit, "g_cuboidSizeLimit", "0", CVAR_ARCHIVE, 0, qfalse }, + { &g_cuboidMode, "g_cuboidMode", "0", CVAR_ARCHIVE, 0, qfalse }, + + { &g_buildableDensityLimit, "g_buildableDensityLimit", "0", CVAR_ARCHIVE, 0, qfalse }, + { &g_buildableDensityLimitRange, "g_buildableDensityLimitRange", "0", 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 +================ +*/ +Q_EXPORT 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 ); + Q_vsnprintf( text, sizeof( text ), fmt, argptr ); + va_end( argptr ); + + trap_Print( text ); +} + +void QDECL G_Error( const char *fmt, ... ) +{ + va_list argptr; + char text[ 1024 ]; + + va_start( argptr, fmt ); + Q_vsnprintf( text, sizeof( 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 ); +} + + +/* +================= +G_RegisterCvars +================= +*/ +void G_RegisterCvars( void ) +{ + int i; + cvarTable_t *cv; + + 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->explicit ) + strcpy( cv->explicit, cv->vmCvar->string ); + } +} + +/* +================= +G_UpdateCvars +================= +*/ +void G_UpdateCvars( void ) +{ + int i; + cvarTable_t *cv; + + 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 ) ); + + if( !level.spawning && cv->explicit ) + strcpy( cv->explicit, cv->vmCvar->string ); + } + } + } +} + +/* +================= +G_RestoreCvars +================= +*/ +void G_RestoreCvars( void ) +{ + int i; + cvarTable_t *cv; + + for( i = 0, cv = gameCvarTable; i < gameCvarTableSize; i++, cv++ ) + { + if( cv->vmCvar && cv->explicit ) + trap_Cvar_Set( cv->cvarName, cv->explicit ); + } +} + +/* +================= +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_CheckCuboidConfig + +even tokens are map names +odd tokens are cuboid modes +================ +*/ +void G_CheckCuboidConfig(char* mapname) +{ + char* token; + char config[MAX_CVAR_VALUE_STRING]; //should be enough for few maps + qboolean type=qfalse; + qboolean found=qfalse; + int mode; + + mode=trap_Cvar_VariableIntegerValue("g_cuboidDefaultMode"); + trap_Cvar_VariableStringBuffer("g_cuboidConfig",config,sizeof(config)); + + while(1) + { + COM_Parse(&token); + + if(!token) + break; + + if(!type) + if(!Q_stricmp(token,mapname)) + found=qtrue; + else + if(found) + { + mode=atoi(token); + break; + } + } + Com_Printf("cuboids are %s%s^7 on %s\n",(mode==0?"^2ENABLED":(mode==1?"^1DISABLED ON S1":"^1DISABLED")),(found?"":" BY DEFAULT"),mapname); +} + +/* +============ +G_InitGame + +============ +*/ +void G_InitGame( int levelTime, int randomSeed, int restart ) +{ + int i; + char map[ MAX_CVAR_VALUE_STRING ] = {""}; + + srand( randomSeed ); + + G_RegisterCvars( ); + + G_Printf( "------- Game Initialization -------\n" ); + G_Printf( "gamename: %s\n", GAME_VERSION ); + G_Printf( "gamedate: %s\n", __DATE__ ); + + BG_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 + + 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" ); + + 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" ); + + G_RegisterCommands( ); + G_admin_readconfig( NULL ); + G_LoadCensors( ); + + // 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 ] ) ); + + level.emoticonCount = BG_LoadEmoticons( level.emoticons, MAX_EMOTICONS ); + + trap_SetConfigstring( CS_INTERMISSION, "0" ); + + // test to see if a custom buildable layout will be loaded + G_LayoutSelect( ); + + // this has to be flipped after the first UpdateCvars + level.spawning = qtrue; + // parse the key/value pairs and spawn gentities + G_SpawnEntitiesFromString( ); + + // load up a custom building layout if there is one + G_LayoutLoad( ); + + // find out g_cuboidMode value + G_CheckCuboidConfig( map ); + + // the map might disable some things + BG_InitAllowedGameElements( ); + + // general initialization + G_FindTeams( ); + + BG_InitClassConfigs( ); + BG_InitBuildableConfigs( ); + G_InitDamageLocations( ); + G_InitMapRotations( ); + G_InitSpawnQueue( &level.alienSpawnQueue ); + G_InitSpawnQueue( &level.humanSpawnQueue ); + + if( g_debugMapRotation.integer ) + G_PrintRotations( ); + + level.voices = BG_VoiceInit( ); + BG_PrintVoices( level.voices, g_debugVoices.integer ); + + //reset stages + trap_Cvar_Set( "g_alienStage", va( "%d", S1 ) ); + trap_Cvar_Set( "g_humanStage", va( "%d", S1 ) ); + trap_Cvar_Set( "g_alienCredits", 0 ); + trap_Cvar_Set( "g_humanCredits", 0 ); + level.suddenDeathBeginTime = g_suddenDeathTime.integer * 60000; + + G_Printf( "-----------------------------------\n" ); + + // So the server counts the spawns without a client attached + G_CountSpawns( ); + + G_UpdateTeamConfigStrings( ); + + 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 ) +{ + int i; + memset( level.voteTime, 0, sizeof( level.voteTime ) ); + for( i = 0; i < NUM_TEAMS; i++ ) + { + trap_SetConfigstring( CS_VOTE_TIME + i, "" ); + trap_SetConfigstring( CS_VOTE_STRING + i, "" ); + } +} + +/* +================= +G_ShutdownGame +================= +*/ +void G_ShutdownGame( int restart ) +{ + // in case of a map_restart + G_ClearVotes( ); + + G_RestoreCvars( ); + + G_Printf( "==== ShutdownGame ====\n" ); + + if( level.logFile ) + { + G_LogPrintf( "ShutdownGame:\n" ); + G_LogPrintf( "------------------------------------------------------------\n" ); + trap_FS_FCloseFile( level.logFile ); + level.logFile = 0; + } + + // write all the client session data so we can get it back + G_WriteSessionData( ); + + G_admin_cleanup( ); + G_namelog_cleanup( ); + G_UnregisterCommands( ); + + G_ShutdownMapRotations( ); + + level.restarted = qfalse; + level.surrenderTeam = TEAM_NONE; + trap_SetConfigstring( CS_WINNER, "" ); +} + + + +//=================================================================== + +void QDECL Com_Error( int level, const char *error, ... ) +{ + va_list argptr; + char text[ 1024 ]; + + va_start( argptr, error ); + Q_vsnprintf( text, sizeof( 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 ); + Q_vsnprintf( text, sizeof( 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->ps.persistant[ PERS_SCORE ] > cb->ps.persistant[ PERS_SCORE ] ) + return -1; + if( ca->ps.persistant[ PERS_SCORE ] < cb->ps.persistant[ 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 the 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_StopFollowing( g_entities + clientNum ); + 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( team_t team ) +{ + int clientNum; + gentity_t *ent, *spawn; + vec3_t spawn_origin, spawn_angles; + spawnQueue_t *sq = NULL; + int numSpawns = 0; + + if( team == TEAM_ALIENS ) + { + sq = &level.alienSpawnQueue; + numSpawns = level.numAlienSpawns; + } + else if( team == TEAM_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.spectatorState = SPECTATOR_NOT; + 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 || ent->s.eType != ET_BUILDABLE || ent->health <= 0 ) + continue; + + if( ent->s.modelindex == BA_A_SPAWN ) + level.numAlienSpawns++; + + if( ent->s.modelindex == BA_H_SPAWN ) + level.numHumanSpawns++; + } +} + + +/* +============ +G_TimeTilSuddenDeath +============ +*/ +#define SUDDENDEATHWARNING 60000 +int G_TimeTilSuddenDeath( void ) +{ + if( ( !g_suddenDeathTime.integer && level.suddenDeathBeginTime == 0 ) || + ( level.suddenDeathBeginTime < 0 ) ) + return SUDDENDEATHWARNING + 1; // 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; + buildPointZone_t *zone; + + // BP queue updates + while( level.alienBuildPointQueue > 0 && + level.alienNextQueueTime < level.time ) + { + level.alienBuildPointQueue--; + level.alienNextQueueTime += G_NextQueueTime( level.alienBuildPointQueue, + g_alienBuildPoints.integer, + g_alienBuildQueueTime.integer ); + } + + while( level.humanBuildPointQueue > 0 && + level.humanNextQueueTime < level.time ) + { + level.humanBuildPointQueue--; + level.humanNextQueueTime += G_NextQueueTime( level.humanBuildPointQueue, + g_humanBuildPoints.integer, + g_humanBuildQueueTime.integer ); + } + + // Sudden Death checks + if( G_TimeTilSuddenDeath( ) <= 0 && level.suddenDeathWarning < TW_PASSED ) + { + G_LogPrintf( "Beginning Sudden Death\n" ); + trap_SendServerCommand( -1, "cp \"Sudden Death!\"" ); + trap_SendServerCommand( -1, "print \"Beginning Sudden Death.\n\"" ); + level.suddenDeathWarning = TW_PASSED; + G_ClearDeconMarks( ); + + // Clear blueprints, or else structs that cost 0 BP can still be built after SD + for( i = 0; i < level.maxclients; i++ ) + { + if( g_entities[ i ].client->ps.stats[ STAT_BUILDABLE ] != BA_NONE ) + g_entities[ i ].client->ps.stats[ STAT_BUILDABLE ] = BA_NONE; + } + } + else if( G_TimeTilSuddenDeath( ) <= SUDDENDEATHWARNING && + level.suddenDeathWarning < TW_IMMINENT ) + { + trap_SendServerCommand( -1, va( "cp \"Sudden Death in %d seconds!\"", + (int)( G_TimeTilSuddenDeath( ) / 1000 ) ) ); + trap_SendServerCommand( -1, va( "print \"Sudden Death will begin in %d seconds.\n\"", + (int)( G_TimeTilSuddenDeath( ) / 1000 ) ) ); + level.suddenDeathWarning = TW_IMMINENT; + } + + level.humanBuildPoints = g_humanBuildPoints.integer - level.humanBuildPointQueue; + level.alienBuildPoints = g_alienBuildPoints.integer - level.alienBuildPointQueue; + + // Reset buildPointZones + for( i = 0; i < g_humanRepeaterMaxZones.integer; i++ ) + { + buildPointZone_t *zone = &level.buildPointZones[ i ]; + + zone->active = qfalse; + zone->totalBuildPoints = g_humanRepeaterBuildPoints.integer; + } + + // Iterate through entities + for( i = MAX_CLIENTS; i < level.num_entities; i++ ) + { + gentity_t *ent = &g_entities[ i ]; + buildPointZone_t *zone; + buildable_t buildable; + int cost; + + if( ent->s.eType != ET_BUILDABLE || ent->s.eFlags & EF_DEAD ) + continue; + + // mark a zone as active + if( ent->usesBuildPointZone ) + { + assert( ent->buildPointZone >= 0 && ent->buildPointZone < g_humanRepeaterMaxZones.integer ); + + zone = &level.buildPointZones[ ent->buildPointZone ]; + zone->active = qtrue; + } + + // Subtract the BP from the appropriate pool + buildable = ent->s.modelindex; + cost = BG_Buildable( buildable, ent->cuboidSize )->buildPoints; + + if( ent->buildableTeam == TEAM_ALIENS ) + level.alienBuildPoints -= cost; + if( buildable == BA_H_REPEATER ) + level.humanBuildPoints -= cost; + else if( buildable != BA_H_REACTOR ) + { + gentity_t *power = G_PowerEntityForEntity( ent ); + + if( power ) + { + if( power->s.modelindex == BA_H_REACTOR ) + level.humanBuildPoints -= cost; + else if( power->s.modelindex == BA_H_REPEATER && power->usesBuildPointZone ) + level.buildPointZones[ power->buildPointZone ].totalBuildPoints -= cost; + } + } + } + + // Finally, update repeater zones and their queues + // note that this has to be done after the used BP is calculated + for( i = MAX_CLIENTS; i < level.num_entities; i++ ) + { + gentity_t *ent = &g_entities[ i ]; + + if( ent->s.eType != ET_BUILDABLE || ent->s.eFlags & EF_DEAD || + ent->buildableTeam != TEAM_HUMANS ) + continue; + + buildable = ent->s.modelindex; + + if( buildable != BA_H_REPEATER ) + continue; + + if( ent->usesBuildPointZone && level.buildPointZones[ ent->buildPointZone ].active ) + { + zone = &level.buildPointZones[ ent->buildPointZone ]; + + if( G_TimeTilSuddenDeath( ) > 0 ) + { + // BP queue updates + while( zone->queuedBuildPoints > 0 && + zone->nextQueueTime < level.time ) + { + zone->queuedBuildPoints--; + zone->nextQueueTime += G_NextQueueTime( zone->queuedBuildPoints, + zone->totalBuildPoints, + g_humanRepeaterBuildQueueTime.integer ); + } + } + else + { + zone->totalBuildPoints = zone->queuedBuildPoints = 0; + } + } + } + + if( level.humanBuildPoints < 0 ) + level.humanBuildPoints = 0; + + if( level.alienBuildPoints < 0 ) + level.alienBuildPoints = 0; +} + +/* +============ +G_CalculateStages +============ +*/ +void G_CalculateStages( void ) +{ + float alienPlayerCountMod = level.averageNumAlienClients / PLAYER_COUNT_MOD; + float humanPlayerCountMod = level.averageNumHumanClients / PLAYER_COUNT_MOD; + int alienNextStageThreshold, humanNextStageThreshold; + static int lastAlienStageModCount = 1; + static int lastHumanStageModCount = 1; + + if( alienPlayerCountMod < 0.1f ) + alienPlayerCountMod = 0.1f; + + if( humanPlayerCountMod < 0.1f ) + humanPlayerCountMod = 0.1f; + + if( g_alienCredits.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_alienCredits.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_humanCredits.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_humanCredits.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; + lastHumanStageModCount = g_humanStage.modificationCount; + G_LogPrintf("Stage: H 3: Humans reached Stage 3\n"); + } + + if( g_alienStage.modificationCount > lastAlienStageModCount ) + { + G_Checktrigger_stages( TEAM_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( TEAM_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; + } + + 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; + + // save a lot of bandwidth by rounding thresholds up to the nearest 100 + if( alienNextStageThreshold > 0 ) + alienNextStageThreshold = ceil( (float)alienNextStageThreshold / 100 ) * 100; + + if( humanNextStageThreshold > 0 ) + humanNextStageThreshold = ceil( (float)humanNextStageThreshold / 100 ) * 100; + + trap_SetConfigstring( CS_ALIEN_STAGES, va( "%d %d %d", + g_alienStage.integer, g_alienCredits.integer, + alienNextStageThreshold ) ); + + trap_SetConfigstring( CS_HUMAN_STAGES, va( "%d %d %d", + g_humanStage.integer, g_humanCredits.integer, + humanNextStageThreshold ) ); +} + +/* +============ +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_alienCredits", "0" ); + } + + if( !level.numHumanClients ) + { + level.numHumanSamples = 0; + trap_Cvar_Set( "g_humanCredits", "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 ] = {""}; + + level.numConnectedClients = 0; + level.numPlayingClients = 0; + memset( level.numVotingClients, 0, sizeof( level.numVotingClients ) ); + 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; + + level.numVotingClients[ TEAM_NONE ]++; + + if( level.clients[ i ].pers.connected != CON_CONNECTED ) + continue; + + if( level.clients[ i ].pers.teamSelection != TEAM_NONE ) + { + level.numPlayingClients++; + if( level.clients[ i ].pers.teamSelection == TEAM_ALIENS ) + { + level.numAlienClients++; + if( level.clients[ i ].sess.spectatorState == SPECTATOR_NOT ) + level.numLiveAlienClients++; + } + else if( level.clients[ i ].pers.teamSelection == TEAM_HUMANS ) + { + level.numHumanClients++; + if( level.clients[ i ].sess.spectatorState == SPECTATOR_NOT ) + level.numLiveHumanClients++; + } + } + } + } + level.numNonSpectatorClients = level.numLiveAlienClients + + level.numLiveHumanClients; + level.numVotingClients[ TEAM_ALIENS ] = level.numAlienClients; + level.numVotingClients[ TEAM_HUMANS ] = level.numHumanClients; + P[ i ] = '\0'; + trap_Cvar_Set( "P", P ); + + 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 ) + 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.misc, 0, sizeof( ent->client->ps.misc ) ); + + 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.intermissiontime = level.time; + + G_ClearVotes( ); + + G_UpdateTeamConfigStrings( ); + + 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( ); +} + + +/* +============= +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; + + if ( G_MapExists( g_nextMap.string ) ) + trap_SendConsoleCommand( EXEC_APPEND, va("map \"%s\"\n", g_nextMap.string ) ); + else if( G_MapRotationActive( ) ) + G_AdvanceMapRotation( 0 ); + 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_AdminMessage + +Print to all active server admins, and to the logfile, and to the server console +================= +*/ +void G_AdminMessage( gentity_t *ent, const char *msg ) +{ + char string[ 1024 ]; + int i; + + Com_sprintf( string, sizeof( string ), "chat %d %d \"%s\"", + ent ? ent - g_entities : -1, + G_admin_permission( ent, ADMF_ADMINCHAT ) ? SAY_ADMINS : SAY_ADMINS_PUBLIC, + msg ); + + // Send to all appropriate clients + for( i = 0; i < level.maxclients; i++ ) + if( G_admin_permission( &g_entities[ i ], ADMF_ADMINCHAT ) ) + trap_SendServerCommand( i, string ); + + // Send to the logfile and server console + G_LogPrintf( "%s: %d \"%s" S_COLOR_WHITE "\": " S_COLOR_MAGENTA "%s\n", + G_admin_permission( ent, ADMF_ADMINCHAT ) ? "AdminMsg" : "AdminMsgPublic", + ent ? ent - g_entities : -1, ent ? ent->client->pers.netname : "console", + msg ); +} + + +/* +================= +G_LogPrintf + +Print to the logfile with a time stamp if it is open, and to the server console +================= +*/ +void QDECL G_LogPrintf( const char *fmt, ... ) +{ + va_list argptr; + char string[ 1024 ], decolored[ 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 ); + Q_vsnprintf( string + 7, sizeof( string ) - 7, fmt, argptr ); + va_end( argptr ); + + if( g_dedicated.integer ) + { + G_UnEscapeString( string, decolored, sizeof( decolored ) ); + G_Printf( "%s", decolored + 7 ); + } + + if( !level.logFile ) + return; + + G_DecolorString( string, decolored, sizeof( decolored ) ); + trap_FS_Write( decolored, strlen( decolored ), level.logFile ); +} + +/* +================= +G_SendGameStat +================= +*/ +void G_SendGameStat( team_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; + + // games with cheats enabled are not very good for balance statistics + if( g_cheats.integer ) + return; + + trap_Cvar_VariableStringBuffer( "mapname", map, sizeof( map ) ); + + switch( team ) + { + case TEAM_ALIENS: teamChar = 'A'; break; + case TEAM_HUMANS: teamChar = 'H'; break; + case TEAM_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 ] ]; + + if( cl->pers.connected == CON_CONNECTING ) + ping = -1; + else + ping = cl->ps.ping < 999 ? cl->ps.ping : 999; + + switch( cl->ps.stats[ STAT_TEAM ] ) + { + case TEAM_ALIENS: teamChar = 'A'; break; + case TEAM_HUMANS: teamChar = 'H'; break; + case TEAM_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_TEAM ] == TEAM_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; + int i; + gclient_t *cl; + clientList_t readyMasks; + + //if no clients are connected, just exit + if( level.numConnectedClients == 0 ) + { + ExitLevel( ); + return; + } + + // see which players are ready + ready = 0; + notReady = 0; + Com_Memset( &readyMasks, 0, sizeof( readyMasks ) ); + for( i = 0; i < g_maxclients.integer; i++ ) + { + cl = level.clients + i; + + if( cl->pers.connected != CON_CONNECTED ) + continue; + + if( cl->ps.stats[ STAT_TEAM ] == TEAM_NONE ) + continue; + + if( cl->readyToExit ) + { + ready++; + + Com_ClientListAdd( &readyMasks, i ); + } + else + notReady++; + } + + trap_SetConfigstring( CS_CLIENTS_READY, Com_ClientListString( &readyMasks ) ); + + // 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 == 0 && notReady > 0 ) + { + level.readyToExit = qfalse; + return; + } + + // if everyone wants to go, go now + if( notReady == 0 ) + { + 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 = TEAM_NONE; + trap_SendServerCommand( -1, "print \"Timelimit hit\n\"" ); + trap_SetConfigstring( CS_WINNER, "Stalemate" ); + LogExit( "Timelimit hit." ); + 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.uncondAlienWin && + ( level.time > level.startTime + 1000 ) && + ( level.numAlienSpawns == 0 ) && + ( level.numLiveAlienClients == 0 ) ) ) + { + //humans win + level.lastWin = TEAM_HUMANS; + trap_SendServerCommand( -1, "print \"Humans win\n\""); + trap_SetConfigstring( CS_WINNER, "Humans Win" ); + LogExit( "Humans win." ); + } + else if( level.uncondAlienWin || + ( ( level.time > level.startTime + 1000 ) && + ( level.numHumanSpawns == 0 ) && + ( level.numLiveHumanClients == 0 ) ) ) + { + //aliens win + level.lastWin = TEAM_ALIENS; + trap_SendServerCommand( -1, "print \"Aliens win\n\""); + trap_SetConfigstring( CS_WINNER, "Aliens Win" ); + LogExit( "Aliens win." ); + } +} + +/* +================== +G_Vote +================== +*/ +void G_Vote( gentity_t *ent, team_t team, qboolean voting ) +{ + if( !level.voteTime[ team ] ) + return; + + if( voting && ent->client->pers.voted & ( 1 << team ) ) + return; + + if( !voting && !( ent->client->pers.voted & ( 1 << team ) ) ) + return; + + ent->client->pers.voted |= 1 << team; + + if( ent->client->pers.vote & ( 1 << team ) ) + { + if( voting ) + level.voteYes[ team ]++; + else + level.voteYes[ team ]--; + + trap_SetConfigstring( CS_VOTE_YES + team, + va( "%d", level.voteYes[ team ] ) ); + } + else + { + if( voting ) + level.voteNo[ team ]++; + else + level.voteNo[ team ]--; + + trap_SetConfigstring( CS_VOTE_NO + team, + va( "%d", level.voteNo[ team ] ) ); + } +} + + +/* +======================================================================== + +FUNCTIONS CALLED EVERY FRAME + +======================================================================== +*/ + + +void G_ExecuteVote( team_t team ) +{ + level.voteExecuteTime[ team ] = 0; + + trap_SendConsoleCommand( EXEC_APPEND, va( "%s\n", + level.voteString[ team ] ) ); + + if( !Q_stricmpn( level.voteString[ team ], "map", 3 ) ) + level.restarted = qtrue; +} + +/* +================== +G_CheckVote +================== +*/ +void G_CheckVote( team_t team ) +{ + float votePassThreshold = (float)level.voteThreshold[ team ] / 100.0f; + qboolean pass = qfalse; + char *msg; + int i; + + if( level.voteExecuteTime[ team ] && + level.voteExecuteTime[ team ] < level.time ) + { + G_ExecuteVote( team ); + } + + if( !level.voteTime[ team ] ) + return; + + if( ( level.time - level.voteTime[ team ] >= VOTE_TIME ) || + ( level.voteYes[ team ] + level.voteNo[ team ] == level.numVotingClients[ team ] ) ) + { + pass = ( level.voteYes[ team ] && + (float)level.voteYes[ team ] / ( (float)level.voteYes[ team ] + (float)level.voteNo[ team ] ) > votePassThreshold ); + } + else + { + if( (float)level.voteYes[ team ] > + (float)level.numVotingClients[ team ] * votePassThreshold ) + { + pass = qtrue; + } + else if( (float)level.voteNo[ team ] <= + (float)level.numVotingClients[ team ] * ( 1.0f - votePassThreshold ) ) + { + return; + } + } + + if( pass ) + level.voteExecuteTime[ team ] = level.time + level.voteDelay[ team ]; + + G_LogPrintf( "EndVote: %s %s %d %d %d\n", + team == TEAM_NONE ? "global" : BG_TeamName( team ), + pass ? "pass" : "fail", + level.voteYes[ team ], level.voteNo[ team ], level.numVotingClients[ team ] ); + + msg = va( "print \"%sote %sed (%d - %d)\n\"", + team == TEAM_NONE ? "V" : "Team v", pass ? "pass" : "fail", + level.voteYes[ team ], level.voteNo[ team ] ); + + if( team == TEAM_NONE ) + trap_SendServerCommand( -1, msg ); + else + G_TeamCommand( team, msg ); + + level.voteTime[ team ] = 0; + level.voteYes[ team ] = 0; + level.voteNo[ team ] = 0; + + for( i = 0; i < level.maxclients; i++ ) + level.clients[ i ].pers.voted &= ~( 1 << team ); + + trap_SetConfigstring( CS_VOTE_TIME + team, "" ); + trap_SetConfigstring( CS_VOTE_STRING + team, "" ); + trap_SetConfigstring( CS_VOTE_YES + team, "0" ); + trap_SetConfigstring( CS_VOTE_NO + team, "0" ); +} + + +/* +================== +CheckCvars +================== +*/ +void CheckCvars( void ) +{ + static int lastPasswordModCount = -1; + static int lastMarkDeconModCount = -1; + static int lastSDTimeModCount = -1; + static int lastNumZones = 0; + + 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 ) + { + lastMarkDeconModCount = g_markDeconstruct.modificationCount; + G_ClearDeconMarks( ); + } + + // If we change g_suddenDeathTime during a map, we need to update + // when sd will begin + if( g_suddenDeathTime.modificationCount != lastSDTimeModCount ) + { + lastSDTimeModCount = g_suddenDeathTime.modificationCount; + level.suddenDeathBeginTime = g_suddenDeathTime.integer * 60000; + } + + // If the number of zones changes, we need a new array + if( g_humanRepeaterMaxZones.integer != lastNumZones ) + { + buildPointZone_t *newZones; + size_t newsize = g_humanRepeaterMaxZones.integer * sizeof( buildPointZone_t ); + size_t oldsize = lastNumZones * sizeof( buildPointZone_t ); + + newZones = BG_Alloc( newsize ); + if( level.buildPointZones ) + { + Com_Memcpy( newZones, level.buildPointZones, MIN( oldsize, newsize ) ); + BG_Free( level.buildPointZones ); + } + + level.buildPointZones = newZones; + lastNumZones = g_humanRepeaterMaxZones.integer; + } + + 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; + static int ptime3000 = 0; + + // if we are waiting for the level to restart, do nothing + if( level.restarted ) + return; + + if( level.pausedTime ) + { + msec = levelTime - level.time - level.pausedTime; + level.pausedTime = levelTime - level.time; + + ptime3000 += msec; + while( ptime3000 > 3000 ) + { + ptime3000 -= 3000; + trap_SendServerCommand( -1, "cp \"The game has been paused. Please wait.\"" ); + + if( level.pausedTime >= 110000 && level.pausedTime <= 119000 ) + trap_SendServerCommand( -1, va( "print \"Server: Game will auto-unpause in %d seconds\n\"", + (int) ( (float) ( 120000 - level.pausedTime ) / 1000.0f ) ) ); + } + + // Prevents clients from getting lagged-out messages + for( i = 0; i < level.maxclients; i++ ) + { + if( level.clients[ i ].pers.connected == CON_CONNECTED ) + level.clients[ i ].ps.commandTime = levelTime; + } + + if( level.pausedTime > 120000 ) + { + trap_SendServerCommand( -1, "print \"Server: The game has been unpaused automatically (2 minute max)\n\"" ); + trap_SendServerCommand( -1, "cp \"The game has been unpaused!\"" ); + level.pausedTime = 0; + } + + return; + } + + level.framenum++; + level.previousTime = level.time; + level.time = levelTime; + msec = level.time - level.previousTime; + + // get any cvar changes + G_UpdateCvars( ); + CheckCvars( ); + // now we are done spawning + level.spawning = qfalse; + + // + // go through all allocated objects + // + 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; + + // 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 ); + } + + // 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( ); + + G_CountSpawns( ); + G_CalculateBuildPoints( ); + G_CalculateStages( ); + + G_SpawnClients( TEAM_ALIENS ); + G_SpawnClients( TEAM_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 + for( i = 0; i < NUM_TEAMS; i++ ) + G_CheckVote( i ); + + level.frameMsec = trap_Milliseconds(); +} + diff --git a/src/game/g_maprotation.c b/src/game/g_maprotation.c new file mode 100644 index 0000000..a8a9ea2 --- /dev/null +++ b/src/game/g_maprotation.c @@ -0,0 +1,1264 @@ +/* +=========================================================================== +Copyright (C) 1999-2005 Id Software, Inc. +Copyright (C) 2000-2009 Darklegion Development + +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" + +#define MAX_MAP_ROTATIONS 64 +#define MAX_MAP_ROTATION_MAPS 256 + +#define NOT_ROTATING -1 + +typedef enum +{ + CV_ERR, + CV_RANDOM, + CV_NUMCLIENTS, + CV_LASTWIN +} conditionVariable_t; + +typedef enum +{ + CO_LT, + CO_EQ, + CO_GT +} conditionOp_t; + +typedef struct condition_s +{ + struct node_s *target; + + conditionVariable_t lhs; + conditionOp_t op; + + int numClients; + team_t lastWin; +} condition_t; + +typedef struct map_s +{ + char name[ MAX_QPATH ]; + + char postCommand[ MAX_STRING_CHARS ]; + char layouts[ MAX_CVAR_VALUE_STRING ]; +} map_t; + +typedef struct label_s +{ + char name[ MAX_QPATH ]; +} label_t; + +typedef enum +{ + NT_MAP, + NT_CONDITION, + NT_GOTO, + NT_RESUME, + NT_LABEL, + NT_RETURN +} nodeType_t; + +typedef struct node_s +{ + nodeType_t type; + + union + { + map_t map; + condition_t condition; + label_t label; + } u; + +} node_t; + +typedef struct mapRotation_s +{ + char name[ MAX_QPATH ]; + + node_t *nodes[ MAX_MAP_ROTATION_MAPS ]; + int numNodes; + int currentNode; +} mapRotation_t; + +typedef struct mapRotations_s +{ + mapRotation_t rotations[ MAX_MAP_ROTATIONS ]; + int numRotations; +} mapRotations_t; + +static mapRotations_t mapRotations; + +static int G_NodeIndexAfter( int currentNode, int rotation ); + +/* +=============== +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_LabelExists + +Check if a label exists in a rotation +=============== +*/ +static qboolean G_LabelExists( int rotation, char *name ) +{ + mapRotation_t *mr = &mapRotations.rotations[ rotation ]; + int i; + + for( i = 0; i < mr->numNodes; i++ ) + { + node_t *node = mr->nodes[ i ]; + + if( node->type == NT_LABEL && + !Q_stricmp( name, node->u.label.name ) ) + return qtrue; + + if( node->type == NT_MAP && + !Q_stricmp( name, node->u.map.name ) ) + return qtrue; + } + + return qfalse; +} + +/* +=============== +G_AllocateNode + +Allocate memory for a node_t +=============== +*/ +static node_t *G_AllocateNode( void ) +{ + node_t *node = BG_Alloc( sizeof( node_t ) ); + + return node; +} + +/* +=============== +G_ParseMapCommandSection + +Parse a map rotation command section +=============== +*/ +static qboolean G_ParseMapCommandSection( node_t *node, char **text_p ) +{ + char *token; + map_t *map = &node->u.map; + int commandLength = 0; + + // read optional parameters + while( 1 ) + { + token = COM_Parse( text_p ); + + if( !*token ) + break; + + if( !Q_stricmp( token, "" ) ) + return qfalse; + + if( !Q_stricmp( token, "}" ) ) + { + if( commandLength > 0 ) + { + // Replace last ; with \n + map->postCommand[ commandLength - 1 ] = '\n'; + } + + return qtrue; //reached the end of this command section + } + + if( !Q_stricmp( token, "layouts" ) ) + { + token = COM_ParseExt( text_p, qfalse ); + map->layouts[ 0 ] = '\0'; + + while( token[ 0 ] != 0 ) + { + Q_strcat( map->layouts, sizeof( map->layouts ), token ); + Q_strcat( map->layouts, sizeof( map->layouts ), " " ); + token = COM_ParseExt( text_p, qfalse ); + } + + continue; + } + + // Parse the rest of the line into map->postCommand + Q_strcat( map->postCommand, sizeof( map->postCommand ), token ); + Q_strcat( map->postCommand, sizeof( map->postCommand ), " " ); + + token = COM_ParseExt( text_p, qfalse ); + + while( token[ 0 ] != 0 ) + { + Q_strcat( map->postCommand, sizeof( map->postCommand ), token ); + Q_strcat( map->postCommand, sizeof( map->postCommand ), " " ); + token = COM_ParseExt( text_p, qfalse ); + } + + commandLength = strlen( map->postCommand ); + map->postCommand[ commandLength - 1 ] = ';'; + } + + return qfalse; +} + +/* +=============== +G_ParseNode + +Parse a node +=============== +*/ +static qboolean G_ParseNode( node_t **node, char *token, char **text_p, qboolean conditional ) +{ + if( !Q_stricmp( token, "if" ) ) + { + condition_t *condition; + + (*node)->type = NT_CONDITION; + condition = &(*node)->u.condition; + + token = COM_Parse( text_p ); + + if( !*token ) + return qfalse; + + if( !Q_stricmp( token, "numClients" ) ) + { + condition->lhs = CV_NUMCLIENTS; + + token = COM_Parse( text_p ); + + if( !*token ) + return qfalse; + + if( !Q_stricmp( token, "<" ) ) + condition->op = CO_LT; + else if( !Q_stricmp( token, ">" ) ) + condition->op = CO_GT; + else if( !Q_stricmp( token, "=" ) ) + condition->op = CO_EQ; + else + { + G_Printf( S_COLOR_RED "ERROR: invalid operator in expression: %s\n", token ); + return qfalse; + } + + token = COM_Parse( text_p ); + + if( !*token ) + return qfalse; + + condition->numClients = atoi( token ); + } + else if( !Q_stricmp( token, "lastWin" ) ) + { + condition->lhs = CV_LASTWIN; + + token = COM_Parse( text_p ); + + if( !*token ) + return qfalse; + + if( !Q_stricmp( token, "aliens" ) ) + condition->lastWin = TEAM_ALIENS; + else if( !Q_stricmp( token, "humans" ) ) + condition->lastWin = TEAM_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" ) ) + condition->lhs = CV_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 ) + return qfalse; + + condition->target = G_AllocateNode( ); + *node = condition->target; + + return G_ParseNode( node, token, text_p, qtrue ); + } + else if( !Q_stricmp( token, "return" ) ) + { + (*node)->type = NT_RETURN; + } + else if( !Q_stricmp( token, "goto" ) || + !Q_stricmp( token, "resume" ) ) + { + label_t *label; + + if( !Q_stricmp( token, "goto" ) ) + (*node)->type = NT_GOTO; + else + (*node)->type = NT_RESUME; + label = &(*node)->u.label; + + token = COM_Parse( text_p ); + + if( !*token ) + { + G_Printf( S_COLOR_RED "ERROR: goto or resume without label\n" ); + return qfalse; + } + + Q_strncpyz( label->name, token, sizeof( label->name ) ); + } + else if( *token == '#' || conditional ) + { + label_t *label; + + (*node)->type = ( conditional ) ? NT_GOTO : NT_LABEL; + label = &(*node)->u.label; + + Q_strncpyz( label->name, token, sizeof( label->name ) ); + } + else + { + map_t *map; + + (*node)->type = NT_MAP; + map = &(*node)->u.map; + + Q_strncpyz( map->name, token, sizeof( map->name ) ); + map->postCommand[ 0 ] = '\0'; + } + + return qtrue; +} + +/* +=============== +G_ParseMapRotation + +Parse a map rotation section +=============== +*/ +static qboolean G_ParseMapRotation( mapRotation_t *mr, char **text_p ) +{ + char *token; + node_t *node = NULL; + + // read optional parameters + while( 1 ) + { + token = COM_Parse( text_p ); + + if( !*token ) + break; + + if( !Q_stricmp( token, "" ) ) + return qfalse; + + if( !Q_stricmp( token, "{" ) ) + { + if( node == NULL ) + { + G_Printf( S_COLOR_RED "ERROR: map command section with no associated map\n" ); + return qfalse; + } + + if( !G_ParseMapCommandSection( node, text_p ) ) + { + G_Printf( S_COLOR_RED "ERROR: failed to parse map command section\n" ); + return qfalse; + } + + continue; + } + else if( !Q_stricmp( token, "}" ) ) + { + // Reached the end of this map rotation + return qtrue; + } + + if( mr->numNodes == 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; + } + + node = G_AllocateNode( ); + mr->nodes[ mr->numNodes++ ] = node; + + if( !G_ParseNode( &node, token, text_p, qfalse ) ) + return qfalse; + } + + 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; + 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 + if( G_RotationExists( mrName ) ) + { + G_Printf( S_COLOR_RED "ERROR: a map rotation is already named %s\n", mrName ); + return 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; + } + + 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; + } + + mapRotations.numRotations++; + + //start parsing map rotations again + mrNameSet = qfalse; + + continue; + } + else + { + G_Printf( S_COLOR_RED "ERROR: unnamed 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++ ) + { + mapRotation_t *mr = &mapRotations.rotations[ i ]; + int mapCount = 0; + + for( j = 0; j < mr->numNodes; j++ ) + { + node_t *node = mr->nodes[ j ]; + + if( node->type == NT_MAP ) + { + mapCount++; + if( !G_MapExists( node->u.map.name ) ) + { + G_Printf( S_COLOR_RED "ERROR: rotation map \"%s\" doesn't exist\n", + node->u.map.name ); + return qfalse; + } + continue; + } + else if( node->type == NT_RETURN ) + continue; + else if( node->type == NT_LABEL ) + continue; + else while( node->type == NT_CONDITION ) + node = node->u.condition.target; + + if( ( node->type == NT_GOTO || node->type == NT_RESUME ) && + !G_LabelExists( i, node->u.label.name ) && + !G_RotationExists( node->u.label.name ) ) + { + G_Printf( S_COLOR_RED "ERROR: goto destination named \"%s\" doesn't exist\n", + node->u.label.name ); + return qfalse; + } + } + + if( mapCount == 0 ) + { + G_Printf( S_COLOR_RED "ERROR: rotation \"%s\" needs at least one map entry\n", + mr->name ); + return qfalse; + } + } + + return qtrue; +} + +/* +=============== +G_PrintSpaces +=============== +*/ +static void G_PrintSpaces( int spaces ) +{ + int i; + + for( i = 0; i < spaces; i++ ) + G_Printf( " " ); +} + +/* +=============== +G_PrintRotations + +Print the parsed map rotations +=============== +*/ +void G_PrintRotations( void ) +{ + int i, j; + int size = sizeof( mapRotations ); + + G_Printf( "Map rotations as parsed:\n\n" ); + + for( i = 0; i < mapRotations.numRotations; i++ ) + { + mapRotation_t *mr = &mapRotations.rotations[ i ]; + + G_Printf( "rotation: %s\n{\n", mr->name ); + + size += mr->numNodes * sizeof( node_t ); + + for( j = 0; j < mr->numNodes; j++ ) + { + node_t *node = mr->nodes[ j ]; + int indentation = 0; + + while( node->type == NT_CONDITION ) + { + G_PrintSpaces( indentation ); + G_Printf( " condition\n" ); + node = node->u.condition.target; + + size += sizeof( node_t ); + + indentation += 2; + } + + G_PrintSpaces( indentation ); + + switch( node->type ) + { + case NT_MAP: + G_Printf( " %s\n", node->u.map.name ); + + if( strlen( node->u.map.postCommand ) > 0 ) + G_Printf( " command: %s", node->u.map.postCommand ); + + break; + + case NT_RETURN: + G_Printf( " return\n" ); + break; + + case NT_LABEL: + G_Printf( " label: %s\n", node->u.label.name ); + break; + + case NT_GOTO: + G_Printf( " goto: %s\n", node->u.label.name ); + break; + + case NT_RESUME: + G_Printf( " resume: %s\n", node->u.label.name ); + break; + + default: + break; + } + } + + G_Printf( "}\n" ); + } + + G_Printf( "Total memory used: %d bytes\n", size ); +} + +/* +=============== +G_ClearRotationStack + +Clear the rotation stack +=============== +*/ +void G_ClearRotationStack( void ) +{ + trap_Cvar_Set( "g_mapRotationStack", "" ); + trap_Cvar_Update( &g_mapRotationStack ); +} + +/* +=============== +G_PushRotationStack + +Push the rotation stack +=============== +*/ +static void G_PushRotationStack( int rotation ) +{ + char text[ MAX_CVAR_VALUE_STRING ]; + + Com_sprintf( text, sizeof( text ), "%d %s", + rotation, g_mapRotationStack.string ); + trap_Cvar_Set( "g_mapRotationStack", text ); + trap_Cvar_Update( &g_mapRotationStack ); +} + +/* +=============== +G_PopRotationStack + +Pop the rotation stack +=============== +*/ +static int G_PopRotationStack( void ) +{ + int value = -1; + char text[ MAX_CVAR_VALUE_STRING ]; + char *text_p, *token; + + Q_strncpyz( text, g_mapRotationStack.string, sizeof( text ) ); + + text_p = text; + token = COM_Parse( &text_p ); + + if( *token ) + value = atoi( token ); + + if( text_p ) + { + while ( *text_p == ' ' ) + text_p++; + trap_Cvar_Set( "g_mapRotationStack", text_p ); + trap_Cvar_Update( &g_mapRotationStack ); + } + else + G_ClearRotationStack( ); + + return value; +} + +/* +=============== +G_RotationNameByIndex + +Returns the name of a rotation by its index +=============== +*/ +static char *G_RotationNameByIndex( int index ) +{ + if( index >= 0 && index < mapRotations.numRotations ) + return mapRotations.rotations[ index ].name; + return NULL; +} + +/* +=============== +G_CurrentNodeIndexArray + +Fill a static array with the current node of each rotation +=============== +*/ +static int *G_CurrentNodeIndexArray( void ) +{ + static int currentNode[ MAX_MAP_ROTATIONS ]; + int i = 0; + char text[ MAX_MAP_ROTATIONS * 2 ]; + char *text_p, *token; + + Q_strncpyz( text, g_mapRotationNodes.string, sizeof( text ) ); + + text_p = text; + + while( 1 ) + { + token = COM_Parse( &text_p ); + + if( !*token ) + break; + + currentNode[ i++ ] = atoi( token ); + } + + return currentNode; +} + +/* +=============== +G_SetCurrentNodeByIndex + +Set the current map in some rotation +=============== +*/ +static void G_SetCurrentNodeByIndex( int currentNode, int rotation ) +{ + char text[ MAX_MAP_ROTATIONS * 4 ] = { 0 }; + int *p = G_CurrentNodeIndexArray( ); + int i; + + p[ rotation ] = currentNode; + + for( i = 0; i < mapRotations.numRotations; i++ ) + Q_strcat( text, sizeof( text ), va( "%d ", p[ i ] ) ); + + trap_Cvar_Set( "g_mapRotationNodes", text ); + trap_Cvar_Update( &g_mapRotationNodes ); +} + +/* +=============== +G_CurrentNodeIndex + +Return the current node index in some rotation +=============== +*/ +static int G_CurrentNodeIndex( int rotation ) +{ + int *p = G_CurrentNodeIndexArray( ); + + return p[ rotation ]; +} + +/* +=============== +G_NodeByIndex + +Return a node in a rotation by its index +=============== +*/ +static node_t *G_NodeByIndex( int index, int rotation ) +{ + if( rotation >= 0 && rotation < mapRotations.numRotations && + index >= 0 && index < mapRotations.rotations[ rotation ].numNodes ) + return mapRotations.rotations[ rotation ].nodes[ index ]; + + return NULL; +} + +/* +=============== +G_IssueMapChange + +Send commands to the server to actually change the map +=============== +*/ +static void G_IssueMapChange( int index, int rotation ) +{ + node_t *node = mapRotations.rotations[ rotation ].nodes[ index ]; + map_t *map = &node->u.map; + + // allow a manually defined g_layouts setting to override the maprotation + if( !g_layouts.string[ 0 ] && map->layouts[ 0 ] ) + { + trap_Cvar_Set( "g_layouts", map->layouts ); + } + + trap_SendConsoleCommand( EXEC_APPEND, va( "map %s\n", map->name ) ); + + // Load up map defaults if g_mapConfigs is set + G_MapConfigs( map->name ); + + if( strlen( map->postCommand ) > 0 ) + trap_SendConsoleCommand( EXEC_APPEND, map->postCommand ); +} + +/* +=============== +G_GotoLabel + +Resolve the label of some condition +=============== +*/ +static qboolean G_GotoLabel( int rotation, int nodeIndex, char *name, + qboolean reset_index, int depth ) +{ + node_t *node; + int i; + + // Search the rotation names... + if( G_StartMapRotation( name, qtrue, qtrue, reset_index, depth ) ) + return qtrue; + + // ...then try labels in the rotation + for( i = 0; i < mapRotations.rotations[ rotation ].numNodes; i++ ) + { + node = mapRotations.rotations[ rotation ].nodes[ i ]; + + if( node->type == NT_LABEL && !Q_stricmp( node->u.label.name, name ) ) + { + G_SetCurrentNodeByIndex( G_NodeIndexAfter( i, rotation ), rotation ); + G_AdvanceMapRotation( depth ); + return qtrue; + } + } + + // finally check for a map by name + for( i = 0; i < mapRotations.rotations[ rotation ].numNodes; i++ ) + { + nodeIndex = G_NodeIndexAfter( nodeIndex, rotation ); + node = mapRotations.rotations[ rotation ].nodes[ nodeIndex ]; + + if( node->type == NT_MAP && !Q_stricmp( node->u.map.name, name ) ) + { + G_SetCurrentNodeByIndex( nodeIndex, rotation ); + G_AdvanceMapRotation( depth ); + return qtrue; + } + } + + return qfalse; +} + +/* +=============== +G_EvaluateMapCondition + +Evaluate a map condition +=============== +*/ +static qboolean G_EvaluateMapCondition( condition_t **condition ) +{ + qboolean result = qfalse; + condition_t *localCondition = *condition; + + switch( localCondition->lhs ) + { + case CV_RANDOM: + result = rand( ) / ( RAND_MAX / 2 + 1 ); + break; + + case CV_NUMCLIENTS: + switch( localCondition->op ) + { + case CO_LT: + result = level.numConnectedClients < localCondition->numClients; + break; + + case CO_GT: + result = level.numConnectedClients > localCondition->numClients; + break; + + case CO_EQ: + result = level.numConnectedClients == localCondition->numClients; + break; + } + break; + + case CV_LASTWIN: + result = level.lastWin == localCondition->lastWin; + break; + + default: + case CV_ERR: + G_Printf( S_COLOR_RED "ERROR: malformed map switch localCondition\n" ); + break; + } + + if( localCondition->target->type == NT_CONDITION ) + { + *condition = &localCondition->target->u.condition; + + return result && G_EvaluateMapCondition( condition ); + } + + return result; +} + +/* +=============== +G_NodeIndexAfter +=============== +*/ +static int G_NodeIndexAfter( int currentNode, int rotation ) +{ + mapRotation_t *mr = &mapRotations.rotations[ rotation ]; + + return ( currentNode + 1 ) % mr->numNodes; +} + +/* +=============== +G_StepMapRotation + +Run one node of a map rotation +=============== +*/ +qboolean G_StepMapRotation( int rotation, int nodeIndex, int depth ) +{ + node_t *node; + condition_t *condition; + int returnRotation; + qboolean step = qtrue; + + node = G_NodeByIndex( nodeIndex, rotation ); + depth++; + + // guard against inifinite loop in conditional code + if( depth > 32 && node->type != NT_MAP ) + { + if( depth > 64 ) + { + G_Printf( S_COLOR_RED "ERROR: infinite loop protection stopped at map rotation %s\n", + G_RotationNameByIndex( rotation ) ); + return qfalse; + } + + G_Printf( S_COLOR_YELLOW "WARNING: possible infinite loop in map rotation %s\n", + G_RotationNameByIndex( rotation ) ); + return qtrue; + } + + while( step ) + { + step = qfalse; + switch( node->type ) + { + case NT_CONDITION: + condition = &node->u.condition; + + if( G_EvaluateMapCondition( &condition ) ) + { + node = condition->target; + step = qtrue; + continue; + } + break; + + case NT_RETURN: + returnRotation = G_PopRotationStack( ); + if( returnRotation >= 0 ) + { + G_SetCurrentNodeByIndex( + G_NodeIndexAfter( nodeIndex, rotation ), rotation ); + if( G_StartMapRotation( G_RotationNameByIndex( returnRotation ), + qtrue, qfalse, qfalse, depth ) ) + { + return qfalse; + } + } + break; + + case NT_MAP: + if( G_MapExists( node->u.map.name ) ) + { + G_SetCurrentNodeByIndex( + G_NodeIndexAfter( nodeIndex, rotation ), rotation ); + G_IssueMapChange( nodeIndex, rotation ); + return qfalse; + } + + G_Printf( S_COLOR_YELLOW "WARNING: skipped missing map %s in rotation %s\n", + node->u.map.name, G_RotationNameByIndex( rotation ) ); + break; + + case NT_LABEL: + break; + + case NT_GOTO: + case NT_RESUME: + G_SetCurrentNodeByIndex( + G_NodeIndexAfter( nodeIndex, rotation ), rotation ); + if ( G_GotoLabel( rotation, nodeIndex, node->u.label.name, + ( node->type == NT_GOTO ), depth ) ) + return qfalse; + + G_Printf( S_COLOR_YELLOW "WARNING: label, map, or rotation %s not found in %s\n", + node->u.label.name, G_RotationNameByIndex( rotation ) ); + break; + } + } + + return qtrue; +} + +/* +=============== +G_AdvanceMapRotation + +Increment the current map rotation +=============== +*/ +void G_AdvanceMapRotation( int depth ) +{ + node_t *node; + int rotation; + int nodeIndex; + + rotation = g_currentMapRotation.integer; + if( rotation < 0 || rotation >= MAX_MAP_ROTATIONS ) + return; + + nodeIndex = G_CurrentNodeIndex( rotation ); + node = G_NodeByIndex( nodeIndex, rotation ); + if( !node ) + { + G_Printf( S_COLOR_YELLOW "WARNING: index incorrect for map rotation %s, trying 0\n", + G_RotationNameByIndex( rotation) ); + nodeIndex = 0; + node = G_NodeByIndex( nodeIndex, rotation ); + } + + while( node && G_StepMapRotation( rotation, nodeIndex, depth ) ) + { + nodeIndex = G_NodeIndexAfter( nodeIndex, rotation ); + node = G_NodeByIndex( nodeIndex, rotation ); + depth++; + } + + if( !node ) + G_Printf( S_COLOR_RED "ERROR: unexpected end of maprotation '%s'\n", + G_RotationNameByIndex( rotation) ); +} + +/* +=============== +G_StartMapRotation + +Switch to a new map rotation +=============== +*/ +qboolean G_StartMapRotation( char *name, qboolean advance, + qboolean putOnStack, qboolean reset_index, int depth ) +{ + int i; + int currentRotation = g_currentMapRotation.integer; + + for( i = 0; i < mapRotations.numRotations; i++ ) + { + if( !Q_stricmp( mapRotations.rotations[ i ].name, name ) ) + { + if( putOnStack && currentRotation >= 0 ) + G_PushRotationStack( currentRotation ); + + trap_Cvar_Set( "g_currentMapRotation", va( "%d", i ) ); + trap_Cvar_Update( &g_currentMapRotation ); + + if( advance ) + { + if( reset_index ) + G_SetCurrentNodeByIndex( 0, i ); + + G_AdvanceMapRotation( depth ); + } + + 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 initialise 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, qtrue, qfalse, 0 ); + + trap_Cvar_Set( "g_initialMapRotation", "" ); + trap_Cvar_Update( &g_initialMapRotation ); + } + } +} + +/* +=============== +G_FreeNode + +Free up memory used by a node +=============== +*/ +void G_FreeNode( node_t *node ) +{ + if( node->type == NT_CONDITION ) + G_FreeNode( node->u.condition.target ); + + BG_Free( node ); +} + +/* +=============== +G_ShutdownMapRotations + +Free up memory used by map rotations +=============== +*/ +void G_ShutdownMapRotations( void ) +{ + int i, j; + + for( i = 0; i < mapRotations.numRotations; i++ ) + { + mapRotation_t *mr = &mapRotations.rotations[ i ]; + + for( j = 0; j < mr->numNodes; j++ ) + { + node_t *node = mr->nodes[ j ]; + + G_FreeNode( node ); + } + } +} diff --git a/src/game/g_misc.c b/src/game/g_misc.c new file mode 100644 index 0000000..861fc64 --- /dev/null +++ b/src/game/g_misc.c @@ -0,0 +1,440 @@ +/* +=========================================================================== +Copyright (C) 1999-2005 Id Software, Inc. +Copyright (C) 2000-2009 Darklegion Development + +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 ); + + // cut all relevant zap beams + G_ClearPlayerZapEffects( player ); + + // set angles + G_SetClientViewAngle( player, angles ); + + // 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.spectatorState == SPECTATOR_NOT ) + { + // kill anything at the destination + G_KillBox( player ); + + 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.misc = 0; + } + else + ent->s.misc = 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.misc = (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..e409596 --- /dev/null +++ b/src/game/g_missile.c @@ -0,0 +1,913 @@ +/* +=========================================================================== +Copyright (C) 1999-2005 Id Software, Inc. +Copyright (C) 2000-2009 Darklegion Development + +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; + + 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_TEAM ] == TEAM_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_TEAM ] == TEAM_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 ); + + if( !other->client->isImpregnated ) + { + float chance; + + if( !BG_InventoryContainsUpgrade( UP_BATTLESUIT, other->client->ps.stats ) ) //never impregnate suits + { + if( BG_InventoryContainsUpgrade( UP_LIGHTARMOUR, other->client->ps.stats ) ) + chance = ALIEN_IMPREGNATION_CHANCE_ARMORED; + else + chance = ALIEN_IMPREGNATION_CHANCE_NAKED; + + if( random() < chance ) + { + other->client->isImpregnated = qtrue; + other->client->isImplantMature = qfalse; + other->client->impregnationTime = level.time; + other->client->impregnatedBy = attacker->s.number; + + trap_SendServerCommand( attacker-g_entities, "cp \"[egg] An egg was implanted! [egg]\"" ); + trap_SendServerCommand( attacker-g_entities, "print \"An egg was implanted!\n\"" ); + + G_LogPrintf( "Impregnation (alien): %d %d: %s impregnated %s\n", + attacker->s.number, + other->s.number, + attacker->client->pers.netname, + other->client->pers.netname ); + } + } + } + } + } + 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 = G_ExplodeMissile; + ent->nextthink = level.time + FRAMETIME; + + //only damage humans + if( other->client && other->client->ps.stats[ STAT_TEAM ] == TEAM_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, + DAMAGE_NO_LOCDAMAGE, 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->s.eType == ET_PLAYER || other->s.eType == ET_BUILDABLE ) ) + { + 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; + qboolean impact = qfalse; + + // get current position + BG_EvaluateTrajectory( &ent->s.pos, level.time, origin ); + + // ignore interactions with the missile owner + passent = ent->r.ownerNum; + + // general trace to see if we hit anything at all + trap_Trace( &tr, ent->r.currentOrigin, ent->r.mins, ent->r.maxs, + origin, passent, ent->clipmask ); + + if( tr.startsolid || tr.allsolid ) + { + tr.fraction = 0.0f; + VectorCopy( ent->r.currentOrigin, tr.endpos ); + } + + if( tr.fraction < 1.0f ) + { + if( !ent->pointAgainstWorld || tr.contents & CONTENTS_BODY ) + { + // We hit an entity or we don't care + impact = qtrue; + } + else + { + trap_Trace( &tr, ent->r.currentOrigin, NULL, NULL, origin, + passent, ent->clipmask ); + + if( tr.fraction < 1.0f ) + { + // Hit the world with point trace + impact = qtrue; + } + else + { + if( tr.contents & CONTENTS_BODY ) + { + // Hit an entity + impact = qtrue; + } + else + { + trap_Trace( &tr, ent->r.currentOrigin, ent->r.mins, ent->r.maxs, + origin, passent, CONTENTS_BODY ); + + if( tr.fraction < 1.0f ) + impact = qtrue; + } + } + } + } + + VectorCopy( tr.endpos, ent->r.currentOrigin ); + + if( impact ) + { + if( tr.surfaceFlags & SURF_NOIMPACT ) + { + // Never explode or bounce on sky + G_FreeEntity( ent ); + return; + } + + G_MissileImpact( ent, &tr ); + + if( ent->s.eType != ET_MISSILE ) + return; // exploded + } + + ent->r.contents = CONTENTS_SOLID; //trick trap_LinkEntity into... + trap_LinkEntity( ent ); + ent->r.contents = 0; //...encoding bbox information + + // 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->pointAgainstWorld = qfalse; + 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_SPLASHDAMAGE; + 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 ] = -FLAMER_SIZE; + bolt->r.maxs[ 0 ] = bolt->r.maxs[ 1 ] = bolt->r.maxs[ 2 ] = FLAMER_SIZE; + + 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->pointAgainstWorld = qtrue; + 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->r.mins[ 0 ] = bolt->r.mins[ 1 ] = bolt->r.mins[ 2 ] = -BLASTER_SIZE; + bolt->r.maxs[ 0 ] = bolt->r.maxs[ 1 ] = bolt->r.maxs[ 2 ] = BLASTER_SIZE; + + 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->pointAgainstWorld = qtrue; + 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->r.mins[ 0 ] = bolt->r.mins[ 1 ] = bolt->r.mins[ 2 ] = -PRIFLE_SIZE; + bolt->r.maxs[ 0 ] = bolt->r.maxs[ 1 ] = bolt->r.maxs[ 2 ] = PRIFLE_SIZE; + + 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, int speed ) +{ + gentity_t *bolt; + float charge; + + VectorNormalize( dir ); + + bolt = G_Spawn( ); + bolt->classname = "lcannon"; + bolt->pointAgainstWorld = qtrue; + + if( damage == LCANNON_DAMAGE ) + 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 = damage; + bolt->splashDamage = damage / 2; + bolt->splashRadius = radius; + bolt->methodOfDeath = MOD_LCANNON; + bolt->splashMethodOfDeath = MOD_LCANNON_SPLASH; + bolt->clipmask = MASK_SHOT; + bolt->target_ent = NULL; + + // Give the missile a small bounding box + bolt->r.mins[ 0 ] = bolt->r.mins[ 1 ] = bolt->r.mins[ 2 ] = + -LCANNON_SIZE; + bolt->r.maxs[ 0 ] = bolt->r.maxs[ 1 ] = bolt->r.maxs[ 2 ] = + -bolt->r.mins[ 0 ]; + + // Pass the missile charge through + charge = (float)( damage - LCANNON_SECONDARY_DAMAGE ) / LCANNON_DAMAGE; + bolt->s.torsoAnim = charge * 255; + if( bolt->s.torsoAnim < 0 ) + bolt->s.torsoAnim = 0; + + 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, 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->pointAgainstWorld = qfalse; + 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_SearchAndDestroy + +Adjust the trajectory to point towards the target +================ +*/ +void AHive_SearchAndDestroy( gentity_t *self ) +{ + vec3_t dir; + trace_t tr; + gentity_t *ent; + int i; + float d, nearest; + + if( level.time > self->timestamp ) + { + 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 + 50; + self->parent->active = qfalse; //allow the parent to start again + return; + } + + nearest = DistanceSquared( self->r.currentOrigin, self->target_ent->r.currentOrigin ); + //find the closest human + for( i = 0; i < MAX_CLIENTS; i++ ) + { + ent = &g_entities[ i ]; + + if( ent->flags & FL_NOTARGET ) + continue; + + if( ent->client && + ent->health > 0 && + ent->client->ps.stats[ STAT_TEAM ] == TEAM_HUMANS && + nearest > (d = DistanceSquared( ent->r.currentOrigin, self->r.currentOrigin ) ) ) + { + trap_Trace( &tr, self->r.currentOrigin, self->r.mins, self->r.maxs, + ent->r.currentOrigin, self->r.ownerNum, self->clipmask ); + if( tr.entityNum != ENTITYNUM_WORLD ) + { + nearest = d; + self->target_ent = ent; + } + } + } + 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->pointAgainstWorld = qfalse; + 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->timestamp = level.time + HIVE_LIFETIME; + + 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->pointAgainstWorld = qtrue; + 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->pointAgainstWorld = qtrue; + 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->pointAgainstWorld = qtrue; + 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->pointAgainstWorld = qtrue; + 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 = LEVEL3_BOUNCEBALL_DMG; + bolt->splashRadius = LEVEL3_BOUNCEBALL_RADIUS; + 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 ); + + return bolt; +} + +/* +================= +fire_rantBomb +================= +*/ +gentity_t *fire_rantBomb( gentity_t *self, vec3_t start, vec3_t dir ) +{ + gentity_t *bolt; + + VectorNormalize ( dir ); + + bolt = G_Spawn( ); + bolt->classname = "rantbomb"; + bolt->pointAgainstWorld = qtrue; + 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_ALEVEL4; + bolt->s.generic1 = self->s.generic1; //weaponMode + bolt->r.ownerNum = self->s.number; + bolt->parent = self; + bolt->damage = LEVEL4_BOMB_DMG; + bolt->splashDamage = LEVEL4_BOMB_DMG; + bolt->splashRadius = LEVEL4_BOMB_RADIUS; + bolt->methodOfDeath = MOD_LEVEL4_BOMB; + bolt->splashMethodOfDeath = MOD_LEVEL4_BOMB; + 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, LEVEL4_BOMB_SPEED, bolt->s.pos.trDelta ); + SnapVector( bolt->s.pos.trDelta ); // save net bandwidth + VectorCopy( start, bolt->r.currentOrigin ); + + return bolt; +} diff --git a/src/game/g_mover.c b/src/game/g_mover.c new file mode 100644 index 0000000..6c92302 --- /dev/null +++ b/src/game/g_mover.c @@ -0,0 +1,2485 @@ +/* +=========================================================================== +Copyright (C) 1999-2005 Id Software, Inc. +Copyright (C) 2000-2009 Darklegion Development + +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_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_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.spectatorState != SPECTATOR_NOT ) + { + // 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; + + // 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.misc = (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; + + // Be sure to send to clients after any fast move case + ent->r.svFlags &= ~SVF_NOCLIENT; + + // Fast move case + if( ent->s.pos.trDuration < 1 ) + { + // As trDuration is used later in a division, we need to avoid that case now + ent->s.pos.trDuration = 1; + + // Don't send entity to clients so it becomes really invisible + ent->r.svFlags |= SVF_NOCLIENT; + } + + // 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->buildableTeam == TEAM_ALIENS ) + { + VectorCopy( other->s.origin2, dir ); + tent = G_TempEntity( other->s.origin, EV_ALIEN_BUILDABLE_EXPLOSION ); + tent->s.eventParm = DirToByte( dir ); + } + else if( other->buildableTeam == TEAM_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_namelog.c b/src/game/g_namelog.c new file mode 100644 index 0000000..6613684 --- /dev/null +++ b/src/game/g_namelog.c @@ -0,0 +1,127 @@ +/* +=========================================================================== +Copyright (C) 2010 Darklegion Development + +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 G_namelog_cleanup( void ) +{ + namelog_t *namelog, *n; + + for( namelog = level.namelogs; namelog; namelog = n ) + { + n = namelog->next; + BG_Free( namelog ); + } +} + +void G_namelog_connect( gclient_t *client ) +{ + namelog_t *n, *p = NULL; + int i; + char *newname; + + for( n = level.namelogs; n; p = n, n = n->next ) + { + if( n->slot != -1 ) + continue; + if( !Q_stricmp( client->pers.guid, n->guid ) ) + break; + } + if( !n ) + { + n = BG_Alloc( sizeof( namelog_t ) ); + strcpy( n->guid, client->pers.guid ); + if( p ) + { + p->next = n; + n->id = p->id + 1; + } + else + { + level.namelogs = n; + n->id = MAX_CLIENTS; + } + } + client->pers.namelog = n; + n->slot = client - level.clients; + n->banned = qfalse; + + newname = n->name[ n->nameOffset ]; + // If they're muted, copy in their last known name - this will stop people + // reconnecting to get around the name change protection. + if( n->muted && G_admin_name_check( &g_entities[ n->slot ], + newname, NULL, 0 ) ) + Q_strncpyz( client->pers.netname, newname, MAX_NAME_LENGTH ); + + for( i = 0; i < MAX_NAMELOG_ADDRS && n->ip[ i ].str[ 0 ]; i++ ) + if( !strcmp( n->ip[ i ].str, client->pers.ip.str ) ) + return; + if( i == MAX_NAMELOG_ADDRS ) + i--; + memcpy( &n->ip[ i ], &client->pers.ip, sizeof( n->ip[ i ] ) ); +} + +void G_namelog_disconnect( gclient_t *client ) +{ + if( client->pers.namelog == NULL ) + return; + client->pers.namelog->slot = -1; + client->pers.namelog = NULL; +} + +void G_namelog_update_score( gclient_t *client ) +{ + namelog_t *n = client->pers.namelog; + if( n == NULL ) + return; + + n->team = client->pers.teamSelection; + n->score = client->ps.persistant[ PERS_SCORE ]; + n->credits = client->pers.credit; +} + +void G_namelog_update_name( gclient_t *client ) +{ + char n1[ MAX_NAME_LENGTH ], n2[ MAX_NAME_LENGTH ]; + namelog_t *n = client->pers.namelog; + + if( n->name[ n->nameOffset ][ 0 ] ) + { + G_SanitiseString( client->pers.netname, n1, sizeof( n1 ) ); + G_SanitiseString( n->name[ n->nameOffset ], + n2, sizeof( n2 ) ); + if( strcmp( n1, n2 ) != 0 ) + n->nameOffset = ( n->nameOffset + 1 ) % MAX_NAMELOG_NAMES; + } + strcpy( n->name[ n->nameOffset ], client->pers.netname ); +} + +void G_namelog_restore( gclient_t *client ) +{ + namelog_t *n = client->pers.namelog; + + G_ChangeTeam( g_entities + n->slot, n->team ); + client->ps.persistant[ PERS_SCORE ] = n->score; + client->ps.persistant[ PERS_CREDIT ] = 0; + G_AddCreditToClient( client, n->credits, qfalse ); +} + diff --git a/src/game/g_physics.c b/src/game/g_physics.c new file mode 100644 index 0000000..840f5c4 --- /dev/null +++ b/src/game/g_physics.c @@ -0,0 +1,169 @@ +/* +=========================================================================== +Copyright (C) 1999-2005 Id Software, Inc. +Copyright (C) 2000-2009 Darklegion Development + +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_Buildable( ent->s.modelindex, NULL )->minNormal; + invert = BG_Buildable( ent->s.modelindex, NULL )->invertNormal; + } + else + minNormal = 0.707f; + + // cut the velocity to keep from bouncing forever + if( ent->s.eType == ET_BUILDABLE ) // buildable should never bounce + VectorScale( ent->s.pos.trDelta, 0.0f, ent->s.pos.trDelta ); + else 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 50 + +/* +================ +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_Buildable( ent->s.modelindex, NULL )->traj ) + { + ent->s.pos.trType = BG_Buildable( ent->s.modelindex, NULL )->traj; + 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; + + 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_public.h b/src/game/g_public.h new file mode 100644 index 0000000..312a5e5 --- /dev/null +++ b/src/game/g_public.h @@ -0,0 +1,265 @@ +/* +=========================================================================== +Copyright (C) 1999-2005 Id Software, Inc. +Copyright (C) 2000-2009 Darklegion Development + +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 9 + +// 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 + +#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_SET_CONFIGSTRING_RESTRICTIONS, // ( int num, const clientList* clientList ); + + 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, + + G_ADDCOMMAND, + G_REMOVECOMMAND +} 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..181c739 --- /dev/null +++ b/src/game/g_session.c @@ -0,0 +1,152 @@ +/* +=========================================================================== +Copyright (C) 1999-2005 Id Software, Inc. +Copyright (C) 2000-2009 Darklegion Development + +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 %s", + client->sess.spectatorTime, + client->sess.spectatorState, + client->sess.spectatorClient, + client->sess.restartTeam, + Com_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; + int spectatorState; + int restartTeam; + char ignorelist[ 17 ]; + + var = va( "session%i", client - level.clients ); + trap_Cvar_VariableStringBuffer( var, s, sizeof(s) ); + + sscanf( s, "%i %i %i %i %16s", + &client->sess.spectatorTime, + &spectatorState, + &client->sess.spectatorClient, + &restartTeam, + ignorelist + ); + + client->sess.spectatorState = (spectatorState_t)spectatorState; + client->sess.restartTeam = (team_t)restartTeam; + Com_ClientListParse( &client->sess.ignoreList, ignorelist ); +} + + +/* +================ +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->spectatorState = SPECTATOR_FREE; + } + else + { + if( g_maxGameClients.integer > 0 && + level.numNonSpectatorClients >= g_maxGameClients.integer ) + sess->spectatorState = SPECTATOR_FREE; + else + sess->spectatorState = SPECTATOR_NOT; + } + + sess->restartTeam = TEAM_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; + + //FIXME: What's this for? + 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..1ebc04d --- /dev/null +++ b/src/game/g_spawn.c @@ -0,0 +1,665 @@ +/* +=========================================================================== +Copyright (C) 1999-2005 Id Software, Inc. +Copyright (C) 2000-2009 Darklegion Development + +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" ); + return qfalse; + } + + 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, + 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[ ] = +{ + {"acceleration", FOFS(acceleration), F_VECTOR}, + {"alpha", FOFS(pos1), F_VECTOR}, + {"angle", FOFS(s.angles), F_ANGLEHACK}, + {"angles", FOFS(s.angles), F_VECTOR}, + {"animation", FOFS(animation), F_VECTOR4}, + {"bounce", FOFS(physicsBounce), F_FLOAT}, + {"classname", FOFS(classname), F_LSTRING}, + {"count", FOFS(count), F_INT}, + {"dmg", FOFS(damage), F_INT}, + {"health", FOFS(health), F_INT}, + {"light", 0, F_IGNORE}, + {"message", FOFS(message), F_LSTRING}, + {"model", FOFS(model), F_LSTRING}, + {"model2", FOFS(model2), F_LSTRING}, + {"origin", FOFS(s.origin), F_VECTOR}, + {"radius", FOFS(pos2), F_VECTOR}, + {"random", FOFS(random), F_FLOAT}, + {"rotatorAngle", FOFS(rotatorAngle), F_FLOAT}, + {"spawnflags", FOFS(spawnflags), F_INT}, + {"speed", FOFS(speed), F_FLOAT}, + {"target", FOFS(target), F_LSTRING}, + {"targetname", FOFS(targetname), F_LSTRING}, + {"targetShaderName", FOFS(targetShaderName), F_LSTRING}, + {"targetShaderNewName", FOFS(targetShaderNewName), F_LSTRING}, + {"team", FOFS(team), F_LSTRING}, + {"wait", FOFS(wait), F_FLOAT} +}; + + +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[ ] = +{ + { "func_bobbing", SP_func_bobbing }, + { "func_button", SP_func_button }, + { "func_door", SP_func_door }, + { "func_door_model", SP_func_door_model }, + { "func_door_rotating", SP_func_door_rotating }, + { "func_group", SP_info_null }, + { "func_pendulum", SP_func_pendulum }, + { "func_plat", SP_func_plat }, + { "func_rotating", SP_func_rotating }, + { "func_static", SP_func_static }, + { "func_timer", SP_func_timer }, // rename trigger_timer? + { "func_train", SP_func_train }, + + // info entities don't do anything at all, but provide positional + // information for things controlled by other processes + { "info_alien_intermission", SP_info_alien_intermission }, + { "info_human_intermission", SP_info_human_intermission }, + { "info_notnull", SP_info_notnull }, // use target_position instead + { "info_null", SP_info_null }, + { "info_player_deathmatch", SP_info_player_deathmatch }, + { "info_player_intermission", SP_info_player_intermission }, + { "info_player_start", SP_info_player_start }, + { "light", SP_light }, + { "misc_anim_model", SP_misc_anim_model }, + { "misc_light_flare", SP_misc_light_flare }, + { "misc_model", SP_misc_model }, + { "misc_particle_system", SP_misc_particle_system }, + { "misc_portal_camera", SP_misc_portal_camera }, + { "misc_portal_surface", SP_misc_portal_surface }, + { "misc_teleporter_dest", SP_misc_teleporter_dest }, + { "path_corner", SP_path_corner }, + + // targets perform no action by themselves, but must be triggered + // by another entity + { "target_alien_win", SP_target_alien_win }, + { "target_delay", SP_target_delay }, + { "target_human_win", SP_target_human_win }, + { "target_hurt", SP_target_hurt }, + { "target_kill", SP_target_kill }, + { "target_location", SP_target_location }, + { "target_position", SP_target_position }, + { "target_print", SP_target_print }, + { "target_push", SP_target_push }, + { "target_relay", SP_target_relay }, + { "target_rumble", SP_target_rumble }, + { "target_score", SP_target_score }, + { "target_speaker", SP_target_speaker }, + { "target_teleporter", SP_target_teleporter }, + + // 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_ammo", SP_trigger_ammo }, + { "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_hurt", SP_trigger_hurt }, + { "trigger_multiple", SP_trigger_multiple }, + { "trigger_push", SP_trigger_push }, + { "trigger_stage", SP_trigger_stage }, + { "trigger_teleport", SP_trigger_teleport }, + { "trigger_win", SP_trigger_win } +}; + +/* +=============== +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 + buildable = BG_BuildableByEntityName( ent->classname )->number; + if( buildable != 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 qfalse; + + 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 + s = bsearch( ent->classname, spawns, sizeof( spawns ) / sizeof( spawn_t ), + sizeof( spawn_t ), cmdcmp ); + if( s ) + { + // 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 = BG_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; + + f = bsearch( key, fields, sizeof( fields ) / sizeof( field_t ), + sizeof( field_t ), cmdcmp ); + if( !f ) + return; + 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; + } +} + + + + +/* +=================== +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 + + if( G_SpawnString( "gravity", "", &s ) ) + trap_Cvar_Set( "g_gravity", s ); + + if( G_SpawnString( "humanMaxStage", "", &s ) ) + trap_Cvar_Set( "g_humanMaxStage", s ); + + if( G_SpawnString( "alienMaxStage", "", &s ) ) + trap_Cvar_Set( "g_alienMaxStage", 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" ); + + // see if we want a warmup time + trap_SetConfigstring( CS_WARMUP, "-1" ); + if( g_doWarmup.integer ) + { + level.warmupTime = level.time - level.startTime + ( g_warmup.integer * 1000 ); + trap_SetConfigstring( CS_WARMUP, va( "%i", level.warmupTime ) ); + G_LogPrintf( "Warmup: %i\n", g_warmup.integer ); + } + +} + + +/* +============== +G_SpawnEntitiesFromString + +Parses textual entity definitions out of an entstring and spawns gentities. +============== +*/ +void G_SpawnEntitiesFromString( void ) +{ + 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( ); +} + diff --git a/src/game/g_svcmds.c b/src/game/g_svcmds.c new file mode 100644 index 0000000..16cb6b8 --- /dev/null +++ b/src/game/g_svcmds.c @@ -0,0 +1,639 @@ +/* +=========================================================================== +Copyright (C) 1999-2005 Id Software, Inc. +Copyright (C) 2000-2009 Darklegion Development + +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" + +/* +=================== +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_LOCATION: + G_Printf( "ET_LOCATION " ); + 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; + case ET_CORPSE: + G_Printf( "ET_CORPSE " ); + break; + case ET_PARTICLE_SYSTEM: + G_Printf( "ET_PARTICLE_SYSTEM " ); + break; + case ET_ANIMMAPOBJ: + G_Printf( "ET_ANIMMAPOBJ " ); + break; + case ET_MODELDOOR: + G_Printf( "ET_MODELDOOR " ); + break; + case ET_LIGHTFLARE: + G_Printf( "ET_LIGHTFLARE " ); + break; + case ET_LEV2_ZAP_CHAIN: + G_Printf( "ET_LEV2_ZAP_CHAIN " ); + break; + default: + G_Printf( "%-3i ", check->s.eType ); + break; + } + + if( check->classname ) + G_Printf( "%s", check->classname ); + + G_Printf( "\n" ); + } +} + +static gclient_t *ClientForString( char *s ) +{ + int idnum; + char err[ MAX_STRING_CHARS ]; + + idnum = G_ClientNumberFromString( s, err, sizeof( err ) ); + if( idnum == -1 ) + { + G_Printf( "%s", err ); + return NULL; + } + + return &level.clients[ idnum ]; +} + +static void Svcmd_Status_f( void ) +{ + int i; + gclient_t *cl; + char userinfo[ MAX_INFO_STRING ]; + + G_Printf( "slot score ping address rate name\n" ); + G_Printf( "---- ----- ---- ------- ---- ----\n" ); + for( i = 0, cl = level.clients; i < level.maxclients; i++, cl++ ) + { + if( cl->pers.connected == CON_DISCONNECTED ) + continue; + + G_Printf( "%-4d ", i ); + G_Printf( "%-5d ", cl->ps.persistant[ PERS_SCORE ] ); + + if( cl->pers.connected == CON_CONNECTING ) + G_Printf( "CNCT " ); + else + G_Printf( "%-4d ", cl->ps.ping ); + + trap_GetUserinfo( i, userinfo, sizeof( userinfo ) ); + G_Printf( "%-21s ", Info_ValueForKey( userinfo, "ip" ) ); + G_Printf( "%-8d ", Info_ValueForKey( userinfo, "rate" ) ); + G_Printf( "%s\n", cl->pers.netname ); // Info_ValueForKey( userinfo, "name" ) + } +} + +/* +=================== +Svcmd_ForceTeam_f + +forceteam <player> <team> +=================== +*/ +static void Svcmd_ForceTeam_f( void ) +{ + gclient_t *cl; + char str[ MAX_TOKEN_CHARS ]; + team_t team; + + if( trap_Argc( ) != 3 ) + { + G_Printf( "usage: forceteam <player> <team>\n" ); + return; + } + + trap_Argv( 1, str, sizeof( str ) ); + cl = ClientForString( str ); + + if( !cl ) + return; + + trap_Argv( 2, str, sizeof( str ) ); + team = G_TeamFromString( str ); + if( team == NUM_TEAMS ) + { + G_Printf( "forceteam: invalid team \"%s\"\n", str ); + return; + } + G_ChangeTeam( &g_entities[ cl - level.clients ], team ); +} + +/* +=================== +Svcmd_LayoutSave_f + +layoutsave <name> +=================== +*/ +static 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 <name>\n" ); + return; + } + trap_Argv( 1, str, sizeof( str ) ); + + // sanitize name + s = &str[ 0 ]; + while( *s && i < sizeof( str2 ) - 1 ) + { + if( isalnum( *s ) || *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 +=================== +*/ +static void Svcmd_LayoutLoad_f( void ) +{ + char layouts[ MAX_CVAR_VALUE_STRING ]; + char *s; + + if( trap_Argc( ) < 2 ) + { + G_Printf( "usage: layoutload <name> ...\n" ); + return; + } + + 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 = G_TeamFromString( teamNum ); + if( team == TEAM_ALIENS ) + { + G_TeamCommand( TEAM_ALIENS, "cp \"Hivemind Link Broken\" 1"); + trap_SendServerCommand( -1, "print \"Alien team has admitted defeat\n\"" ); + } + else if( team == TEAM_HUMANS ) + { + G_TeamCommand( TEAM_HUMANS, "cp \"Life Support Terminated\" 1"); + trap_SendServerCommand( -1, "print \"Human team has admitted defeat\n\"" ); + } + else + { + G_Printf("admitdefeat: invalid team\n"); + return; + } + level.surrenderTeam = team; + G_BaseSelfDestruct( team ); +} + +static void Svcmd_TeamWin_f( void ) +{ + // this is largely made redundant by admitdefeat <team> + char cmd[ 6 ]; + trap_Argv( 0, cmd, sizeof( cmd ) ); + + switch( G_TeamFromString( cmd ) ) + { + case TEAM_ALIENS: + G_BaseSelfDestruct( TEAM_HUMANS ); + break; + + case TEAM_HUMANS: + G_BaseSelfDestruct( TEAM_ALIENS ); + break; + + default: + return; + } +} + +static void Svcmd_Evacuation_f( void ) +{ + trap_SendServerCommand( -1, "print \"Evacuation ordered\n\"" ); + level.lastWin = TEAM_NONE; + trap_SetConfigstring( CS_WINNER, "Evacuation" ); + LogExit( "Evacuation." ); +} + +static void Svcmd_MapRotation_f( void ) +{ + char rotationName[ MAX_QPATH ]; + + if( trap_Argc( ) != 2 ) + { + G_Printf( "usage: maprotation <name>\n" ); + return; + } + + G_ClearRotationStack( ); + + trap_Argv( 1, rotationName, sizeof( rotationName ) ); + if( !G_StartMapRotation( rotationName, qfalse, qtrue, qfalse, 0 ) ) + G_Printf( "maprotation: invalid map rotation \"%s\"\n", rotationName ); +} + +static void Svcmd_TeamMessage_f( void ) +{ + char teamNum[ 2 ]; + team_t team; + + if( trap_Argc( ) < 3 ) + { + G_Printf( "usage: say_team <team> <message>\n" ); + return; + } + + trap_Argv( 1, teamNum, sizeof( teamNum ) ); + team = G_TeamFromString( teamNum ); + + if( team == NUM_TEAMS ) + { + G_Printf( "say_team: invalid team \"%s\"\n", teamNum ); + return; + } + + G_TeamCommand( team, va( "chat -1 %d \"%s\"", SAY_TEAM, ConcatArgs( 2 ) ) ); + G_LogPrintf( "SayTeam: -1 \"console\": %s\n", ConcatArgs( 2 ) ); +} + +static void Svcmd_CenterPrint_f( void ) +{ + if( trap_Argc( ) < 2 ) + { + G_Printf( "usage: cp <message>\n" ); + return; + } + + trap_SendServerCommand( -1, va( "cp \"%s\"", ConcatArgs( 1 ) ) ); +} + +static void Svcmd_EjectClient_f( void ) +{ + char *reason, name[ MAX_STRING_CHARS ]; + + if( trap_Argc( ) < 2 ) + { + G_Printf( "usage: eject <player|-1> <reason>\n" ); + return; + } + + trap_Argv( 1, name, sizeof( name ) ); + reason = ConcatArgs( 2 ); + + if( atoi( name ) == -1 ) + { + int i; + for( i = 0; i < level.maxclients; i++ ) + { + if( level.clients[ i ].pers.connected == CON_DISCONNECTED ) + continue; + if( level.clients[ i ].pers.localClient ) + continue; + trap_DropClient( i, reason ); + } + } + else + { + gclient_t *cl = ClientForString( name ); + if( !cl ) + return; + if( cl->pers.localClient ) + { + G_Printf( "eject: cannot eject local clients\n" ); + return; + } + trap_DropClient( cl-level.clients, reason ); + } +} + +static void Svcmd_DumpUser_f( void ) +{ + char name[ MAX_STRING_CHARS ], userinfo[ MAX_INFO_STRING ]; + char key[ BIG_INFO_KEY ], value[ BIG_INFO_VALUE ]; + const char *info; + gclient_t *cl; + + if( trap_Argc( ) != 2 ) + { + G_Printf( "usage: dumpuser <player>\n" ); + return; + } + + trap_Argv( 1, name, sizeof( name ) ); + cl = ClientForString( name ); + if( !cl ) + return; + + trap_GetUserinfo( cl-level.clients, userinfo, sizeof( userinfo ) ); + info = &userinfo[ 0 ]; + G_Printf( "userinfo\n--------\n" ); + //Info_Print( userinfo ); + while( 1 ) + { + Info_NextPair( &info, key, value ); + if( !*info ) + return; + + G_Printf( "%-20s%s\n", key, value ); + } +} + +static void Svcmd_Pr_f( void ) +{ + char targ[ 4 ]; + int cl; + + if( trap_Argc( ) < 3 ) + { + G_Printf( "usage: <clientnum|-1> <message>\n" ); + return; + } + + trap_Argv( 1, targ, sizeof( targ ) ); + cl = atoi( targ ); + + if( cl >= MAX_CLIENTS || cl < -1 ) + { + G_Printf( "invalid clientnum %d\n", cl ); + return; + } + + trap_SendServerCommand( cl, va( "print \"%s\n\"", ConcatArgs( 2 ) ) ); +} + +static void Svcmd_PrintQueue_f( void ) +{ + char team[ MAX_STRING_CHARS ]; + + if( trap_Argc() != 2 ) + { + G_Printf( "usage: printqueue <team>\n" ); + return; + } + + trap_Argv( 1, team, sizeof( team ) ); + + switch( G_TeamFromString( team ) ) + { + case TEAM_ALIENS: + G_PrintSpawnQueue( &level.alienSpawnQueue ); + break; + + case TEAM_HUMANS: + G_PrintSpawnQueue( &level.humanSpawnQueue ); + break; + + default: + G_Printf( "unknown team\n" ); + } +} + +// dumb wrapper for "a", "m", "chat", and "say" +static void Svcmd_MessageWrapper( void ) +{ + char cmd[ 5 ]; + trap_Argv( 0, cmd, sizeof( cmd ) ); + + if( !Q_stricmp( cmd, "a" ) ) + Cmd_AdminMessage_f( NULL ); + else if( !Q_stricmp( cmd, "m" ) ) + Cmd_PrivateMessage_f( NULL ); + else if( !Q_stricmp( cmd, "say" ) ) + G_Say( NULL, SAY_ALL, ConcatArgs( 1 ) ); + else if( !Q_stricmp( cmd, "chat" ) ) + G_Say( NULL, SAY_RAW, ConcatArgs( 1 ) ); +} + +static void Svcmd_ListMapsWrapper( void ) +{ + Cmd_ListMaps_f( NULL ); +} + +static void Svcmd_SuddenDeath_f( void ) +{ + char secs[ 5 ]; + int offset; + trap_Argv( 1, secs, sizeof( secs ) ); + offset = atoi( secs ); + + level.suddenDeathBeginTime = level.time - level.startTime + offset * 1000; + trap_SendServerCommand( -1, + va( "cp \"Sudden Death will begin in %d second%s\"", + offset, offset == 1 ? "" : "s" ) ); +} + +static void Svcmd_G_AdvanceMapRotation_f( void ) +{ + G_AdvanceMapRotation( 0 ); +} + +struct svcmd +{ + char *cmd; + qboolean dedicated; + void ( *function )( void ); +} svcmds[ ] = { + { "a", qtrue, Svcmd_MessageWrapper }, + { "admitDefeat", qfalse, Svcmd_AdmitDefeat_f }, + { "advanceMapRotation", qfalse, Svcmd_G_AdvanceMapRotation_f }, + { "alienWin", qfalse, Svcmd_TeamWin_f }, + { "chat", qtrue, Svcmd_MessageWrapper }, + { "cp", qtrue, Svcmd_CenterPrint_f }, + { "dumpuser", qfalse, Svcmd_DumpUser_f }, + { "eject", qfalse, Svcmd_EjectClient_f }, + { "entityList", qfalse, Svcmd_EntityList_f }, + { "evacuation", qfalse, Svcmd_Evacuation_f }, + { "forceTeam", qfalse, Svcmd_ForceTeam_f }, + { "game_memory", qfalse, BG_MemoryInfo }, + { "humanWin", qfalse, Svcmd_TeamWin_f }, + { "layoutLoad", qfalse, Svcmd_LayoutLoad_f }, + { "layoutSave", qfalse, Svcmd_LayoutSave_f }, + { "listmaps", qtrue, Svcmd_ListMapsWrapper }, + { "loadcensors", qfalse, G_LoadCensors }, + { "m", qtrue, Svcmd_MessageWrapper }, + { "mapRotation", qfalse, Svcmd_MapRotation_f }, + { "pr", qfalse, Svcmd_Pr_f }, + { "printqueue", qfalse, Svcmd_PrintQueue_f }, + { "say", qtrue, Svcmd_MessageWrapper }, + { "say_team", qtrue, Svcmd_TeamMessage_f }, + { "status", qfalse, Svcmd_Status_f }, + { "stopMapRotation", qfalse, G_StopMapRotation }, + { "suddendeath", qfalse, Svcmd_SuddenDeath_f } +}; + +/* +================= +ConsoleCommand + +================= +*/ +qboolean ConsoleCommand( void ) +{ + char cmd[ MAX_TOKEN_CHARS ]; + struct svcmd *command; + + trap_Argv( 0, cmd, sizeof( cmd ) ); + + command = bsearch( cmd, svcmds, sizeof( svcmds ) / sizeof( struct svcmd ), + sizeof( struct svcmd ), cmdcmp ); + + if( !command ) + { + // see if this is an admin command + if( G_admin_cmd_check( NULL ) ) + return qtrue; + + if( g_dedicated.integer ) + G_Printf( "unknown command: %s\n", cmd ); + + return qfalse; + } + + if( command->dedicated && !g_dedicated.integer ) + return qfalse; + + command->function( ); + return qtrue; +} + +void G_RegisterCommands( void ) +{ + int i; + + for( i = 0; i < sizeof( svcmds ) / sizeof( svcmds[ 0 ] ); i++ ) + { + if( svcmds[ i ].dedicated && !g_dedicated.integer ) + continue; + trap_AddCommand( svcmds[ i ].cmd ); + } + + G_admin_register_cmds( ); +} + +void G_UnregisterCommands( void ) +{ + int i; + + for( i = 0; i < sizeof( svcmds ) / sizeof( svcmds[ 0 ] ); i++ ) + { + if( svcmds[ i ].dedicated && !g_dedicated.integer ) + continue; + trap_RemoveCommand( svcmds[ i ].cmd ); + } + + G_admin_unregister_cmds( ); +} diff --git a/src/game/g_syscalls.asm b/src/game/g_syscalls.asm new file mode 100644 index 0000000..f39b138 --- /dev/null +++ b/src/game/g_syscalls.asm @@ -0,0 +1,68 @@ +code + +equ trap_Print -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_SetConfigstringRestrictions -21 +equ trap_GetUserinfo -22 +equ trap_SetUserinfo -23 +equ trap_GetServerinfo -24 +equ trap_SetBrushModel -25 +equ trap_Trace -26 +equ trap_PointContents -27 +equ trap_InPVS -28 +equ trap_InPVSIgnorePortals -29 +equ trap_AdjustAreaPortalState -30 +equ trap_AreasConnected -31 +equ trap_LinkEntity -32 +equ trap_UnlinkEntity -33 +equ trap_EntitiesInBox -34 +equ trap_EntityContact -35 +equ trap_GetUsercmd -36 +equ trap_GetEntityToken -37 +equ trap_FS_GetFileList -38 +equ trap_RealTime -39 +equ trap_SnapVector -40 +equ trap_TraceCapsule -41 +equ trap_EntityContactCapsule -42 +equ trap_FS_Seek -43 + +equ trap_Parse_AddGlobalDefine -44 +equ trap_Parse_LoadSource -45 +equ trap_Parse_FreeSource -46 +equ trap_Parse_ReadToken -47 +equ trap_Parse_SourceFileAndLine -48 + +equ trap_SendGameStat -49 + +equ trap_AddCommand -50 +equ trap_RemoveCommand -51 + +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..38e5203 --- /dev/null +++ b/src/game/g_syscalls.c @@ -0,0 +1,299 @@ +/* +=========================================================================== +Copyright (C) 1999-2005 Id Software, Inc. +Copyright (C) 2000-2009 Darklegion Development + +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; + + +Q_EXPORT void dllEntry( intptr_t (QDECL *syscallptr)( intptr_t arg,... ) ) +{ + syscall = syscallptr; +} + +int PASSFLOAT( float x ) +{ + float floatTemp; + floatTemp = x; + return *(int *)&floatTemp; +} + +void trap_Print( 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_SetConfigstringRestrictions( int num, const clientList_t *clientList ) +{ + syscall( G_SET_CONFIGSTRING_RESTRICTIONS, num, clientList ); +} + +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 ); +} + +void trap_AddCommand( const char *cmdName ) +{ + syscall( G_ADDCOMMAND, cmdName ); +} + +void trap_RemoveCommand( const char *cmdName ) +{ + syscall( G_REMOVECOMMAND, cmdName ); +} + diff --git a/src/game/g_target.c b/src/game/g_target.c new file mode 100644 index 0000000..3157c8f --- /dev/null +++ b/src/game/g_target.c @@ -0,0 +1,478 @@ +/* +=========================================================================== +Copyright (C) 1999-2005 Id Software, Inc. +Copyright (C) 2000-2009 Darklegion Development + +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( ent->spawnflags & 4 ) + { + if( activator && activator->client ) + trap_SendServerCommand( activator-g_entities, va( "cp \"%s\"", ent->message ) ); + return; + } + + if( ent->spawnflags & 3 ) + { + if( ent->spawnflags & 1 ) + G_TeamCommand( TEAM_HUMANS, va( "cp \"%s\"", ent->message ) ); + if( ent->spawnflags & 2 ) + G_TeamCommand( TEAM_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_TEAM ] != TEAM_HUMANS ) + return; + + if( ( self->spawnflags & 2 ) && activator && activator->client && + activator->client->ps.stats[ STAT_TEAM ] != TEAM_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 ); +} + +/*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 ) +{ + static int n = 1; + char *message; + self->s.eType = ET_LOCATION; + self->r.svFlags = SVF_BROADCAST; + trap_LinkEntity( self ); // make the server send them to the clients + if( n == MAX_LOCATIONS ) + { + G_Printf( S_COLOR_YELLOW "too many target_locations\n" ); + return; + } + if( self->count ) + { + if( self->count < 0 ) + self->count = 0; + + if( self->count > 7 ) + self->count = 7; + + message = va( "%c%c%s" S_COLOR_WHITE, Q_COLOR_ESCAPE, self->count + '0', + self->message); + } + else + message = self->message; + trap_SetConfigstring( CS_LOCATIONS + n, message ); + self->nextTrain = level.locationHead; + self->s.generic1 = n; // use for location marking + level.locationHead = self; + n++; + + 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 ) +{ + if( !level.uncondHumanWin ) + 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 ) +{ + if( !level.uncondAlienWin ) + 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..fd15aa7 --- /dev/null +++ b/src/game/g_team.c @@ -0,0 +1,473 @@ +/* +=========================================================================== +Copyright (C) 1999-2005 Id Software, Inc. +Copyright (C) 2000-2009 Darklegion Development + +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( Q_vsnprintf( msg, sizeof( 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 ) ); +} + +/* +================ +G_TeamFromString + +Return the team referenced by a string +================ +*/ +team_t G_TeamFromString( char *str ) +{ + switch( tolower( *str ) ) + { + case '0': case 's': return TEAM_NONE; + case '1': case 'a': return TEAM_ALIENS; + case '2': case 'h': return TEAM_HUMANS; + default: return NUM_TEAMS; + } +} + +/* +================ +G_TeamCommand + +Broadcasts a command to only a specific team +================ +*/ +void G_TeamCommand( team_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 == TEAM_NONE && + G_admin_permission( &g_entities[ i ], ADMF_SPEC_ALLCHAT ) ) ) + trap_SendServerCommand( i, cmd ); + } + } +} + +/* +============== +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; +} + +/* +================== +G_ClientListForTeam +================== +*/ +static clientList_t G_ClientListForTeam( team_t team ) +{ + int i; + clientList_t clientList; + + Com_Memset( &clientList, 0, sizeof( clientList_t ) ); + + for( i = 0; i < g_maxclients.integer; i++ ) + { + gentity_t *ent = g_entities + i; + if( ent->client->pers.connected != CON_CONNECTED ) + continue; + + if( ent->inuse && ( ent->client->ps.stats[ STAT_TEAM ] == team ) ) + Com_ClientListAdd( &clientList, ent->client->ps.clientNum ); + } + + return clientList; +} + +/* +================== +G_UpdateTeamConfigStrings +================== +*/ +void G_UpdateTeamConfigStrings( void ) +{ + clientList_t alienTeam = G_ClientListForTeam( TEAM_ALIENS ); + clientList_t humanTeam = G_ClientListForTeam( TEAM_HUMANS ); + + if( level.intermissiontime ) + { + // No restrictions once the game has ended + Com_Memset( &alienTeam, 0, sizeof( clientList_t ) ); + Com_Memset( &humanTeam, 0, sizeof( clientList_t ) ); + } + + trap_SetConfigstringRestrictions( CS_VOTE_TIME + TEAM_ALIENS, &humanTeam ); + trap_SetConfigstringRestrictions( CS_VOTE_STRING + TEAM_ALIENS, &humanTeam ); + trap_SetConfigstringRestrictions( CS_VOTE_YES + TEAM_ALIENS, &humanTeam ); + trap_SetConfigstringRestrictions( CS_VOTE_NO + TEAM_ALIENS, &humanTeam ); + + trap_SetConfigstringRestrictions( CS_VOTE_TIME + TEAM_HUMANS, &alienTeam ); + trap_SetConfigstringRestrictions( CS_VOTE_STRING + TEAM_HUMANS, &alienTeam ); + trap_SetConfigstringRestrictions( CS_VOTE_YES + TEAM_HUMANS, &alienTeam ); + trap_SetConfigstringRestrictions( CS_VOTE_NO + TEAM_HUMANS, &alienTeam ); + + trap_SetConfigstringRestrictions( CS_ALIEN_STAGES, &humanTeam ); + trap_SetConfigstringRestrictions( CS_HUMAN_STAGES, &alienTeam ); +} + +/* +================== +G_LeaveTeam +================== +*/ +void G_LeaveTeam( gentity_t *self ) +{ + team_t team = self->client->pers.teamSelection; + gentity_t *ent; + int i; + + if( team == TEAM_ALIENS ) + G_RemoveFromSpawnQueue( &level.alienSpawnQueue, self->client->ps.clientNum ); + else if( team == TEAM_HUMANS ) + G_RemoveFromSpawnQueue( &level.humanSpawnQueue, self->client->ps.clientNum ); + else + { + if( self->client->sess.spectatorState == SPECTATOR_FOLLOW ) + G_StopFollowing( self ); + return; + } + + // stop any following clients + G_StopFromFollowing( self ); + + G_Vote( self, team, qfalse ); + self->suicideTime = 0; + + for( i = 0; i < level.num_entities; i++ ) + { + ent = &g_entities[ i ]; + if( !ent->inuse ) + continue; + + if( ent->client && ent->client->pers.connected == CON_CONNECTED ) + { + // cure poison + if( ent->client->ps.stats[ STAT_STATE ] & SS_POISONED && + ent->client->lastPoisonClient == self ) + ent->client->ps.stats[ STAT_STATE ] &= ~SS_POISONED; + + // clear impregnatedBy for everyone impregnated by this player + if( ent->client->isImpregnated && ent->client->impregnatedBy == self->s.number ) + ent->client->impregnatedBy = -2; + } + else if( ent->s.eType == ET_MISSILE && ent->r.ownerNum == self->s.number ) + G_FreeEntity( ent ); + } + + // cut all relevant zap beams + G_ClearPlayerZapEffects( self ); + + G_namelog_update_score( self->client ); +} + +/* +================= +G_ChangeTeam +================= +*/ +void G_ChangeTeam( gentity_t *ent, team_t newTeam ) +{ + team_t oldTeam = ent->client->pers.teamSelection; + + if( oldTeam == newTeam ) + return; + + G_LeaveTeam( ent ); + ent->client->pers.teamChangeTime = level.time; + ent->client->pers.teamSelection = newTeam; + ent->client->pers.classSelection = PCL_NONE; + ClientSpawn( ent, NULL, NULL, NULL ); + + if( oldTeam == TEAM_HUMANS && newTeam == TEAM_ALIENS ) + { + // Convert from human to alien credits + ent->client->pers.credit = + (int)( ent->client->pers.credit * + ALIEN_MAX_CREDITS / HUMAN_MAX_CREDITS + 0.5f ); + } + else if( oldTeam == TEAM_ALIENS && newTeam == TEAM_HUMANS ) + { + // Convert from alien to human credits + ent->client->pers.credit = + (int)( ent->client->pers.credit * + HUMAN_MAX_CREDITS / ALIEN_MAX_CREDITS + 0.5f ); + } + + // Copy credits to ps for the client + ent->client->ps.persistant[ PERS_CREDIT ] = ent->client->pers.credit; + + ClientUserinfoChanged( ent->client->ps.clientNum, qfalse ); + + G_UpdateTeamConfigStrings( ); + + G_LogPrintf( "ChangeTeam: %d %s: %s" S_COLOR_WHITE " switched teams\n", + ent - g_entities, BG_TeamName( newTeam ), ent->client->pers.netname ); + + G_namelog_update_score( ent->client ); + TeamplayInfoMessage( ent ); +} + +/* +=========== +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; + + best = NULL; + bestlen = 3.0f * 8192.0f * 8192.0f; + + for( eloc = level.locationHead; eloc; eloc = eloc->nextTrain ) + { + len = DistanceSquared( ent->r.currentOrigin, eloc->r.currentOrigin ); + + if( len > bestlen ) + continue; + + if( !trap_InPVS( ent->r.currentOrigin, eloc->r.currentOrigin ) ) + continue; + + bestlen = len; + best = eloc; + } + + return best; +} + + +/*---------------------------------------------------------------------------*/ + +/* +================== +TeamplayInfoMessage + +Format: + clientNum location health weapon upgrade + +================== +*/ +void TeamplayInfoMessage( gentity_t *ent ) +{ + char entry[ 19 ], string[ 1143 ]; + int i, j; + int team, stringlength; + int sent = 0; + gentity_t *player; + gclient_t *cl; + upgrade_t upgrade = UP_NONE; + int curWeaponClass = WP_NONE ; // sends weapon for humans, class for aliens + char *tmp; + + if( !g_allowTeamOverlay.integer ) + return; + + if( !ent->client->pers.teamInfo ) + return; + + if( ent->client->pers.teamSelection == TEAM_NONE ) + { + if( ent->client->sess.spectatorState == SPECTATOR_FREE || + ent->client->sess.spectatorClient < 0 ) + return; + team = g_entities[ ent->client->sess.spectatorClient ].client-> + pers.teamSelection; + } + else + team = ent->client->pers.teamSelection; + + string[ 0 ] = '\0'; + stringlength = 0; + + for( i = 0; i < MAX_CLIENTS; i++) + { + player = g_entities + i ; + cl = player->client; + + if( ent == player || !cl || team != cl->pers.teamSelection || + !player->inuse ) + continue; + + if( cl->sess.spectatorState != SPECTATOR_NOT ) + { + curWeaponClass = WP_NONE; + upgrade = UP_NONE; + } + else if ( cl->pers.teamSelection == TEAM_HUMANS ) + { + curWeaponClass = 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_MK1, cl->ps.stats ) ) + upgrade = UP_HELMET_MK1; + else if( BG_InventoryContainsUpgrade( UP_HELMET_MK2, cl->ps.stats ) ) + upgrade = UP_HELMET_MK2; + else if( BG_InventoryContainsUpgrade( UP_LIGHTARMOUR, cl->ps.stats ) ) + upgrade = UP_LIGHTARMOUR; + else + upgrade = UP_NONE; + } + else if( cl->pers.teamSelection == TEAM_ALIENS ) + { + curWeaponClass = cl->ps.stats[ STAT_CLASS ]; + upgrade = UP_NONE; + } + + tmp = va( "%i %i %i %i", + player->client->pers.location, + player->client->ps.stats[ STAT_HEALTH ] < 1 ? 0 : + player->client->ps.stats[ STAT_HEALTH ], + curWeaponClass, + upgrade ); + + if( !strcmp( ent->client->pers.cinfo[ i ], tmp ) ) + continue; + + Q_strncpyz( ent->client->pers.cinfo[ i ], tmp, + sizeof( ent->client->pers.cinfo[ i ] ) ); + + Com_sprintf( entry, sizeof( entry ), " %i %s", i, tmp ); + + j = strlen( entry ); + + if( stringlength + j > sizeof( string ) ) + break; + + strcpy( string + stringlength, entry ); + stringlength += j; + sent++; + } + + if( !sent ) + return; + + trap_SendServerCommand( ent - g_entities, va( "tinfo%s", 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_TEAM ] == TEAM_HUMANS || + ent->client->ps.stats[ STAT_TEAM ] == TEAM_ALIENS ) ) + { + + loc = Team_GetLocation( ent ); + + if( loc ) + ent->client->pers.location = loc->s.generic1; + else + ent->client->pers.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 ) + TeamplayInfoMessage( ent ); + } + } + + // Warn on imbalanced teams + if( g_teamImbalanceWarnings.integer && !level.intermissiontime && + ( level.time - level.lastTeamImbalancedTime > + ( g_teamImbalanceWarnings.integer * 1000 ) ) && + level.numTeamImbalanceWarnings < 3 && !level.restarted ) + { + level.lastTeamImbalancedTime = level.time; + if( level.numAlienSpawns > 0 && + level.numHumanClients - level.numAlienClients > 2 ) + { + trap_SendServerCommand( -1, "print \"Teams are imbalanced. " + "Humans have more players.\n\""); + level.numTeamImbalanceWarnings++; + } + else if( level.numHumanSpawns > 0 && + level.numAlienClients - level.numHumanClients > 2 ) + { + trap_SendServerCommand ( -1, "print \"Teams are imbalanced. " + "Aliens have more players.\n\""); + level.numTeamImbalanceWarnings++; + } + else + { + level.numTeamImbalanceWarnings = 0; + } + } +} diff --git a/src/game/g_trigger.c b/src/game/g_trigger.c new file mode 100644 index 0000000..65bf1bd --- /dev/null +++ b/src/game/g_trigger.c @@ -0,0 +1,1148 @@ +/* +=========================================================================== +Copyright (C) 1999-2005 Id Software, Inc. +Copyright (C) 2000-2009 Darklegion Development + +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 && activator->client ) + { + if( ( ent->spawnflags & 1 ) && + activator->client->ps.stats[ STAT_TEAM ] != TEAM_HUMANS ) + return; + + if( ( ent->spawnflags & 2 ) && + activator->client->ps.stats[ STAT_TEAM ] != TEAM_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 || !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.spectatorState == SPECTATOR_NOT ) + 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( team_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( !activator ) + return qfalse; + + //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 || 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( !activator ) + return qfalse; + + //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_CLASS ] == self->cTriggers[ i ] ) + return qtrue; + } + } + + return qfalse; +} + +/* +=============== +trigger_class_trigger +=============== +*/ +void trigger_class_trigger( gentity_t *self, gentity_t *activator ) +{ + //sanity check + if( !activator || !activator->client ) + return; + + if( activator->client->ps.stats[ STAT_TEAM ] != TEAM_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( !activator ) + return qfalse; + + //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 || !activator->client ) + return; + + if( activator->client->ps.stats[ STAT_TEAM ] != TEAM_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 maxClips, maxAmmo; + weapon_t weapon; + + if( !other->client ) + return; + + if( other->client->ps.stats[ STAT_TEAM ] != TEAM_HUMANS ) + return; + + if( self->timestamp > level.time ) + return; + + if( other->client->ps.weaponstate != WEAPON_READY ) + return; + + weapon = BG_PrimaryWeapon( other->client->ps.stats ); + if( BG_Weapon( weapon )->usesEnergy && self->spawnflags & 2 ) + return; + + if( !BG_Weapon( weapon )->usesEnergy && self->spawnflags & 4 ) + return; + + if( self->spawnflags & 1 ) + self->timestamp = level.time + 1000; + else + self->timestamp = level.time + FRAMETIME; + + maxAmmo = BG_Weapon( weapon )->maxAmmo; + maxClips = BG_Weapon( weapon )->maxClips; + + if( ( other->client->ps.ammo + self->damage ) > maxAmmo ) + { + if( other->client->ps.clips < maxClips ) + { + other->client->ps.clips++; + other->client->ps.ammo = 1; + } + else + other->client->ps.ammo = maxAmmo; + } + else + other->client->ps.ammo += self->damage; +} + +/* +=============== +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..c0b6b0e --- /dev/null +++ b/src/game/g_utils.c @@ -0,0 +1,1032 @@ +/* +=========================================================================== +Copyright (C) 1999-2005 Id Software, Inc. +Copyright (C) 2000-2009 Darklegion Development + +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; +} + +int G_ParticleSystemIndex( char *name ) +{ + return G_FindConfigstringIndex( name, CS_PARTICLE_SYSTEMS, MAX_GAME_PARTICLE_SYSTEMS, qtrue ); +} + +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_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( ) / ( RAND_MAX / num_choices + 1 ) ]; +} + + +/* +============================== +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->r.currentOrigin, ent->r.mins, mins ); + VectorAdd( ent->r.currentOrigin, 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; + + // 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( %s ) has eventParm %d, " + "which will overflow\n", BG_EventName( 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 ); +} + +// 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, int contents ) +{ + trace_t trace; + + trap_Trace( &trace, ent1->s.pos.trBase, NULL, NULL, ent2->s.pos.trBase, + ent1->s.number, contents ); + + return trace.fraction >= 1.0f || trace.entityNum == ent2 - g_entities; +} + +/* +=============== +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; + gentity_t *closestEnt; + + if( numEntities <= 0 ) + return NULL; + + closestEnt = entities[ 0 ]; + d = DistanceSquared( origin, closestEnt->s.origin ); + + for( i = 1; i < numEntities; i++ ) + { + gentity_t *ent = entities[ i ]; + + nd = DistanceSquared( origin, ent->s.origin ); + if( 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, sizeof( buffer ), "servermenu %d", menu ); + trap_SendServerCommand( clientNum, buffer ); +} + +/* +=============== +G_TriggerMenuArgs + +Trigger a menu on some client and passes an argument +=============== +*/ +void G_TriggerMenuArgs( int clientNum, dynMenu_t menu, int arg ) +{ + char buffer[ 64 ]; + + Com_sprintf( buffer, sizeof( buffer ), "servermenu %d %d", menu, arg ); + 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 ); +} + + +/* +=============== +G_AddressParse + +Make an IP address more usable +=============== +*/ +static const char *addr4parse( const char *str, addr_t *addr ) +{ + int i; + int octet = 0; + int num = 0; + memset( addr, 0, sizeof( addr_t ) ); + addr->type = IPv4; + for( i = 0; octet < 4; i++ ) + { + if( isdigit( str[ i ] ) ) + num = num * 10 + str[ i ] - '0'; + else + { + if( num < 0 || num > 255 ) + return NULL; + addr->addr[ octet ] = (byte)num; + octet++; + if( str[ i ] != '.' || str[ i + 1 ] == '.' ) + break; + num = 0; + } + } + if( octet < 1 ) + return NULL; + return str + i; +} + +static const char *addr6parse( const char *str, addr_t *addr ) +{ + int i; + qboolean seen = qfalse; + /* keep track of the parts before and after the :: + it's either this or even uglier hacks */ + byte a[ ADDRLEN ], b[ ADDRLEN ]; + size_t before = 0, after = 0; + int num = 0; + /* 8 hexadectets unless :: is present */ + for( i = 0; before + after <= 8; i++ ) + { + //num = num << 4 | str[ i ] - '0'; + if( isdigit( str[ i ] ) ) + num = num * 16 + str[ i ] - '0'; + else if( str[ i ] >= 'A' && str[ i ] <= 'F' ) + num = num * 16 + 10 + str[ i ] - 'A'; + else if( str[ i ] >= 'a' && str[ i ] <= 'f' ) + num = num * 16 + 10 + str[ i ] - 'a'; + else + { + if( num < 0 || num > 65535 ) + return NULL; + if( i == 0 ) + { + // + } + else if( seen ) // :: has been seen already + { + b[ after * 2 ] = num >> 8; + b[ after * 2 + 1 ] = num & 0xff; + after++; + } + else + { + a[ before * 2 ] = num >> 8; + a[ before * 2 + 1 ] = num & 0xff; + before++; + } + if( !str[ i ] ) + break; + if( str[ i ] != ':' || before + after == 8 ) + break; + if( str[ i + 1 ] == ':' ) + { + // ::: or multiple :: + if( seen || str[ i + 2 ] == ':' ) + break; + seen = qtrue; + i++; + } + else if( i == 0 ) // starts with : but not :: + return NULL; + num = 0; + } + } + if( seen ) + { + // there have to be fewer than 8 hexadectets when :: is present + if( before + after == 8 ) + return NULL; + } + else if( before + after < 8 ) // require exactly 8 hexadectets + return NULL; + memset( addr, 0, sizeof( addr_t ) ); + addr->type = IPv6; + if( before ) + memcpy( addr->addr, a, before * 2 ); + if( after ) + memcpy( addr->addr + ADDRLEN - 2 * after, b, after * 2 ); + return str + i; +} + +qboolean G_AddressParse( const char *str, addr_t *addr ) +{ + const char *p; + int max; + if( strchr( str, ':' ) ) + { + p = addr6parse( str, addr ); + max = 128; + } + else if( strchr( str, '.' ) ) + { + p = addr4parse( str, addr ); + max = 32; + } + else + return qfalse; + Q_strncpyz( addr->str, str, sizeof( addr->str ) ); + if( !p ) + return qfalse; + if( *p == '/' ) + { + addr->mask = atoi( p + 1 ); + if( addr->mask < 1 || addr->mask > max ) + addr->mask = max; + } + else + { + if( *p ) + return qfalse; + addr->mask = max; + } + return qtrue; +} + +/* +=============== +G_AddressCompare + +Based largely on NET_CompareBaseAdrMask from ioq3 revision 1557 +=============== +*/ +qboolean G_AddressCompare( const addr_t *a, const addr_t *b ) +{ + int i, netmask; + if( a->type != b->type ) + return qfalse; + netmask = a->mask; + if( a->type == IPv4 ) + { + if( netmask < 1 || netmask > 32 ) + netmask = 32; + } + else if( a->type == IPv6 ) + { + if( netmask < 1 || netmask > 128 ) + netmask = 128; + } + for( i = 0; netmask > 7; i++, netmask -= 8 ) + if( a->addr[ i ] != b->addr[ i ] ) + return qfalse; + if( netmask ) + { + netmask = ( ( 1 << netmask ) - 1 ) << ( 8 - netmask ); + return ( a->addr[ i ] & netmask ) == ( b->addr[ i ] & netmask ); + } + return qtrue; +} + +/* +=============== +G_RecalcBuildTimer + +recalc client's STAT_MISC +=============== +*/ + +void G_RecalcBuildTimer( gclient_t *client ) +{ + if(BG_InventoryContainsWeapon(WP_ABUILD,client->ps.stats)|| + BG_InventoryContainsWeapon(WP_ABUILD2,client->ps.stats)|| + BG_InventoryContainsWeapon(WP_HBUILD,client->ps.stats)) + client->ps.stats[STAT_MISC]=MIN(client->buildTimer,MAXIMUM_BUILD_TIME); +} diff --git a/src/game/g_weapon.c b/src/game/g_weapon.c new file mode 100644 index 0000000..63e80be --- /dev/null +++ b/src/game/g_weapon.c @@ -0,0 +1,1647 @@ +/* +=========================================================================== +Copyright (C) 1999-2005 Id Software, Inc. +Copyright (C) 2000-2009 Darklegion Development + +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 ) +{ + playerState_t *ps = &ent->client->ps; + + // stop a reload in progress + if( ps->weaponstate == WEAPON_RELOADING ) + { + ps->torsoAnim = ( ( ps->torsoAnim & ANIM_TOGGLEBIT ) ^ ANIM_TOGGLEBIT ) | TORSO_RAISE; + ps->weaponTime = 250; + ps->weaponstate = WEAPON_READY; + } + + if( weapon == WP_NONE || + !BG_InventoryContainsWeapon( weapon, ps->stats ) ) + { + // switch to the first non blaster weapon + ps->persistant[ PERS_NEWWEAPON ] = + BG_PrimaryWeapon( ent->client->ps.stats ); + } + else + ps->persistant[ PERS_NEWWEAPON ] = weapon; + + // force this here to prevent flamer effect from continuing + ps->generic1 = WPM_NOTFIRING; + + // The PMove will do an animated drop, raise, and set the new weapon + ps->pm_flags |= PMF_WEAPON_SWITCH; +} + +/* +================= +G_GiveClientMaxAmmo +================= +*/ +void G_GiveClientMaxAmmo( gentity_t *ent, qboolean buyingEnergyAmmo ) +{ + int i, maxAmmo, maxClips; + qboolean restoredAmmo = qfalse, restoredEnergy = qfalse; + + for( i = WP_NONE + 1; i < WP_NUM_WEAPONS; i++ ) + { + qboolean energyWeapon; + + energyWeapon = BG_Weapon( i )->usesEnergy; + if( !BG_InventoryContainsWeapon( i, ent->client->ps.stats ) || + BG_Weapon( i )->infiniteAmmo || + BG_WeaponIsFull( i, ent->client->ps.stats, + ent->client->ps.ammo, ent->client->ps.clips ) || + ( buyingEnergyAmmo && !energyWeapon ) ) + continue; + + maxAmmo = BG_Weapon( i )->maxAmmo; + maxClips = BG_Weapon( i )->maxClips; + + // Apply battery pack modifier + if( energyWeapon && + BG_InventoryContainsUpgrade( UP_BATTPACK, ent->client->ps.stats ) ) + { + maxAmmo *= BATTPACK_MODIFIER; + restoredEnergy = qtrue; + } + + ent->client->ps.ammo = maxAmmo; + ent->client->ps.clips = maxClips; + + restoredAmmo = qtrue; + } + + if( restoredAmmo ) + G_ForceWeaponChange( ent, ent->client->ps.weapon ); + + if( restoredEnergy ) + G_AddEvent( ent, EV_RPTUSE_SOUND, 0 ); +} + +/* +================ +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, float height, gentity_t **target ) +{ + vec3_t mins, maxs; + vec3_t end; + + VectorSet( mins, -width, -width, -height ); + VectorSet( maxs, width, width, width ); + + *target = NULL; + + if( !ent->client ) + return; + + G_UnlaggedOn( ent, muzzle, range + width ); + + VectorMA( muzzle, range, forward, end ); + + // 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, ent->s.number, CONTENTS_SOLID ); + if( tr->entityNum != ENTITYNUM_NONE ) + *target = &g_entities[ tr->entityNum ]; + + G_UnlaggedOff( ); +} + +/* +====================== +SnapVectorTowards +SnapVectorNormal + +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( v[ i ] >= 0 ) + v[ i ] = (int)( v[ i ] + ( to[ i ] <= v[ i ] ? 0 : 1 ) ); + else + v[ i ] = (int)( v[ i ] + ( to[ i ] <= v[ i ] ? -1 : 0 ) ); + } +} + +void SnapVectorNormal( vec3_t v, vec3_t normal ) +{ + int i; + + for( i = 0 ; i < 3 ; i++ ) + { + if( v[ i ] >= 0 ) + v[ i ] = (int)( v[ i ] + ( normal[ i ] <= 0 ? 0 : 1 ) ); + else + v[ i ] = (int)( v[ i ] + ( normal[ i ] <= 0 ? -1 : 0 ) ); + } +} + +/* +=============== +BloodSpurt + +Generates a blood spurt event for traces with accurate end points +=============== +*/ +static void BloodSpurt( gentity_t *attacker, gentity_t *victim, trace_t *tr ) +{ + gentity_t *tent; + + if( !attacker->client ) + return; + + if( victim->health <= 0 ) + return; + + tent = G_TempEntity( tr->endpos, EV_MISSILE_HIT ); + tent->s.otherEntityNum = victim->s.number; + tent->s.eventParm = DirToByte( tr->plane.normal ); + tent->s.weapon = attacker->s.weapon; + tent->s.generic1 = attacker->s.generic1; // weaponMode +} + +/* +=============== +WideBloodSpurt + +Calculates the position of a blood spurt for wide traces and generates an event +=============== +*/ +static void WideBloodSpurt( gentity_t *attacker, gentity_t *victim, trace_t *tr ) +{ + gentity_t *tent; + vec3_t normal, origin; + float mag, radius; + + if( !attacker->client ) + return; + + if( victim->health <= 0 ) + return; + + if( tr ) + VectorSubtract( tr->endpos, victim->s.origin, normal ); + else + VectorSubtract( attacker->client->ps.origin, + victim->s.origin, normal ); + + // Normalize the horizontal components of the vector difference to the + // "radius" of the bounding box + mag = sqrt( normal[ 0 ] * normal[ 0 ] + normal[ 1 ] * normal[ 1 ] ); + radius = victim->r.maxs[ 0 ] * 1.21f; + if( mag > radius ) + { + normal[ 0 ] = normal[ 0 ] / mag * radius; + normal[ 1 ] = normal[ 1 ] / mag * radius; + } + + // Clamp origin to be within bounding box vertically + if( normal[ 2 ] > victim->r.maxs[ 2 ] ) + normal[ 2 ] = victim->r.maxs[ 2 ]; + if( normal[ 2 ] < victim->r.mins[ 2 ] ) + normal[ 2 ] = victim->r.mins[ 2 ]; + + VectorAdd( victim->s.origin, normal, origin ); + VectorNegate( normal, normal ); + VectorNormalize( normal ); + + // Create the blood spurt effect entity + tent = G_TempEntity( origin, EV_MISSILE_HIT ); + tent->s.eventParm = DirToByte( normal ); + tent->s.otherEntityNum = victim->s.number; + tent->s.weapon = attacker->s.weapon; + tent->s.generic1 = attacker->s.generic1; // weaponMode +} + +/* +=============== +meleeAttack +=============== +*/ +void meleeAttack( gentity_t *ent, float range, float width, float height, + int damage, meansOfDeath_t mod ) +{ + trace_t tr; + gentity_t *traceEnt; + + G_WideTrace( &tr, ent, range, width, height, &traceEnt ); + if( traceEnt == NULL || !traceEnt->takedamage ) + return; + + WideBloodSpurt( ent, traceEnt, &tr ); + 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->s.eType == ET_PLAYER || + traceEnt->s.eType == ET_BUILDABLE ) ) + { + 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, SHOTGUN_RANGE, 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() / ( RAND_MAX / 0x100 + 1 ); // seed for spread pattern + tent->s.otherEntityNum = ent->s.number; + G_UnlaggedOn( ent, muzzle, SHOTGUN_RANGE ); + 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.0f * 16.0f, forward, end ); + + G_UnlaggedOn( ent, muzzle, 8192.0f * 16.0f ); + 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->s.eType == ET_BUILDABLE || + traceEnt->s.eType == ET_PLAYER ) ) + { + BloodSpurt( ent, traceEnt, &tr ); + } + 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 ) +{ + fire_lockblob( ent, muzzle, forward ); +} + +/* +====================================================================== + +HIVE + +====================================================================== +*/ + +void hiveFire( gentity_t *ent ) +{ + vec3_t origin; + + // Fire from the hive tip, not the center + VectorMA( muzzle, ent->r.maxs[ 2 ], ent->s.origin2, origin ); + + fire_hive( ent, origin, forward ); +} + +/* +====================================================================== + +BLASTER PISTOL + +====================================================================== +*/ + +void blasterFire( gentity_t *ent ) +{ + fire_blaster( ent, muzzle, forward ); +} + +/* +====================================================================== + +PULSE RIFLE + +====================================================================== +*/ + +void pulseRifleFire( gentity_t *ent ) +{ + fire_pulseRifle( ent, muzzle, forward ); +} + +/* +====================================================================== + +FLAME THROWER + +====================================================================== +*/ + +void flamerFire( gentity_t *ent ) +{ + vec3_t origin; + + // Correct muzzle so that the missile does not start in the ceiling + VectorMA( muzzle, -7.0f, up, origin ); + + // Correct muzzle so that the missile fires from the player's hand + VectorMA( origin, 4.5f, right, origin ); + + fire_flamer( ent, origin, forward ); +} + +/* +====================================================================== + +GRENADE + +====================================================================== +*/ + +void throwGrenade( gentity_t *ent ) +{ + 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->s.eType == ET_BUILDABLE || + traceEnt->s.eType == ET_PLAYER ) ) + { + BloodSpurt( ent, traceEnt, &tr ); + } + 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 temp; + gentity_t *tent, *traceEnt; + + G_WideTrace( &tr, ent, PAINSAW_RANGE, PAINSAW_WIDTH, PAINSAW_HEIGHT, + &traceEnt ); + if( !traceEnt || !traceEnt->takedamage ) + return; + + // hack to line up particle system with weapon model + tr.endpos[ 2 ] -= 5.0f; + + // send blood impact + if( traceEnt->s.eType == ET_PLAYER || traceEnt->s.eType == ET_BUILDABLE ) + { + BloodSpurt( ent, traceEnt, &tr ); + } + else + { + VectorCopy( tr.endpos, temp ); + 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 + } + + 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 ) +{ + if( secondary && ent->client->ps.stats[ STAT_MISC ] <= 0 ) + fire_luciferCannon( ent, muzzle, forward, LCANNON_SECONDARY_DAMAGE, + LCANNON_SECONDARY_RADIUS, LCANNON_SECONDARY_SPEED ); + else + fire_luciferCannon( ent, muzzle, forward, + ent->client->ps.stats[ STAT_MISC ] * + LCANNON_DAMAGE / LCANNON_CHARGE_TIME_MAX, + LCANNON_RADIUS, LCANNON_SPEED ); + + ent->client->ps.stats[ STAT_MISC ] = 0; +} + +/* +====================================================================== + +TESLA GENERATOR + +====================================================================== +*/ + + +void teslaFire( gentity_t *self ) +{ + trace_t tr; + vec3_t origin, target; + gentity_t *tent; + + if( !self->enemy ) + return; + + // Move the muzzle from the entity origin up a bit to fire over turrets + VectorMA( muzzle, self->r.maxs[ 2 ], self->s.origin2, origin ); + + // Don't aim for the center, aim at the top of the bounding box + VectorCopy( self->enemy->s.origin, target ); + target[ 2 ] += self->enemy->r.maxs[ 2 ]; + + // Trace to the target entity + trap_Trace( &tr, origin, NULL, NULL, target, self->s.number, MASK_SHOT ); + if( tr.entityNum != self->enemy->s.number ) + return; + + // Client side firing effect + self->s.eFlags |= EF_FIRING; + + // Deal damage + if( self->enemy->takedamage ) + { + vec3_t dir; + + VectorSubtract( target, origin, dir ); + G_Damage( self->enemy, self, self, dir, tr.endpos, + TESLAGEN_DMG, 0, MOD_TESLAGEN ); + } + + // Send tesla zap trail + tent = G_TempEntity( tr.endpos, EV_TESLATRAIL ); + tent->s.generic1 = self->s.number; // src + tent->s.clientNum = self->enemy->s.number; // dest +} + + +/* +====================================================================== + +BUILD GUN + +====================================================================== +*/ +void CheckCkitRepair( gentity_t *ent ) +{ + vec3_t viewOrigin, forward, end; + trace_t tr; + gentity_t *traceEnt; + int bHealth; + + if( ent->client->ps.weaponTime > 0 || + ent->client->buildTimer > 0 ) + return; + + BG_GetClientViewOrigin( &ent->client->ps, viewOrigin ); + AngleVectors( ent->client->ps.viewangles, forward, NULL, NULL ); + VectorMA( viewOrigin, 100, forward, end ); + + trap_Trace( &tr, viewOrigin, NULL, NULL, end, ent->s.number, MASK_PLAYERSOLID ); + traceEnt = &g_entities[ tr.entityNum ]; + + if( tr.fraction < 1.0f && traceEnt->spawned && traceEnt->health > 0 && + traceEnt->s.eType == ET_BUILDABLE && traceEnt->buildableTeam == TEAM_HUMANS ) + { + if(BG_Buildable(traceEnt->s.modelindex,NULL)->cuboid) + if(!BG_CuboidAttributes(traceEnt->s.modelindex)->repairable) + return; + + bHealth = BG_Buildable( traceEnt->s.modelindex, traceEnt->cuboidSize )->health; + + if( traceEnt->health < bHealth ) + { + traceEnt->health += HBUILD_HEALRATE; + if( traceEnt->health >= bHealth ) + { + traceEnt->health = bHealth; + G_AddEvent( ent, EV_BUILD_REPAIRED, 0 ); + } + else + G_AddEvent( ent, EV_BUILD_REPAIR, 0 ); + + ent->client->ps.weaponTime += BG_Weapon( ent->client->ps.weapon )->repeatRate1; + } + } +} + +/* +=============== +cancelBuildFire +=============== +*/ +void cancelBuildFire( gentity_t *ent ) +{ + // Cancel ghost buildable + if( ent->client->ps.stats[ STAT_BUILDABLE ] != BA_NONE ) + { + ent->client->ps.stats[ STAT_BUILDABLE ] = BA_NONE; + return; + } + + if( ent->client->ps.weapon == WP_ABUILD || + ent->client->ps.weapon == WP_ABUILD2 ) + meleeAttack( ent, ABUILDER_CLAW_RANGE, ABUILDER_CLAW_WIDTH, + ABUILDER_CLAW_WIDTH, ABUILDER_CLAW_DMG, MOD_ABUILDER_CLAW ); +} + +/* +=============== +buildFire +=============== +*/ +void buildFire( gentity_t *ent, dynMenu_t menu ) +{ + buildable_t buildable = ( ent->client->ps.stats[ STAT_BUILDABLE ] + & ~SB_VALID_TOGGLEBIT ); + + if( buildable > BA_NONE ) + { + if( ent->client->buildTimer > 0 ) + { + G_AddEvent( ent, EV_BUILD_DELAY, ent->client->ps.clientNum ); + return; + } + + if( G_BuildIfValid( ent, buildable, ent->client->cuboidSelection ) ) + { + if( !g_cheats.integer && !g_instantBuild.integer ) + { + ent->client->buildTimer += BG_Buildable( buildable, ent->client->cuboidSelection )->buildTime; + G_RecalcBuildTimer(ent->client); + } + + ent->client->ps.stats[ STAT_BUILDABLE ] = BA_NONE; + } + + return; + } + + G_TriggerMenu( ent->client->ps.clientNum, menu ); +} + +void slowBlobFire( gentity_t *ent ) +{ + fire_slowBlob( ent, muzzle, forward ); +} + + +/* +====================================================================== + +LEVEL0 + +====================================================================== +*/ + +/* +=============== +CheckVenomAttack +=============== +*/ +qboolean CheckVenomAttack( gentity_t *ent ) +{ + trace_t tr; + gentity_t *traceEnt; + int damage = LEVEL0_BITE_DMG; + + if( ent->client->ps.weaponTime ) + return qfalse; + + // Calculate muzzle point + AngleVectors( ent->client->ps.viewangles, forward, right, up ); + CalcMuzzlePoint( ent, forward, right, up, muzzle ); + + G_WideTrace( &tr, ent, LEVEL0_BITE_RANGE, LEVEL0_BITE_WIDTH, + LEVEL0_BITE_WIDTH, &traceEnt ); + + if( traceEnt == NULL ) + return qfalse; + + if( !traceEnt->takedamage ) + return qfalse; + + if( traceEnt->health <= 0 ) + return qfalse; + + // only allow bites to work against buildings as they are constructing + if( traceEnt->s.eType == ET_BUILDABLE ) + { + if( traceEnt->spawned ) + return qfalse; + + if( traceEnt->buildableTeam == TEAM_ALIENS ) + return qfalse; + } + + if( traceEnt->client ) + { + if( traceEnt->client->ps.stats[ STAT_TEAM ] == TEAM_ALIENS ) + return qfalse; + if( traceEnt->client->ps.stats[ STAT_HEALTH ] <= 0 ) + return qfalse; + } + + // send blood impact + WideBloodSpurt( ent, traceEnt, &tr ); + + G_Damage( traceEnt, ent, ent, forward, tr.endpos, damage, DAMAGE_NO_KNOCKBACK, MOD_LEVEL0_BITE ); + ent->client->ps.weaponTime += LEVEL0_BITE_REPEAT; + 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 ); + + if( ent->client->ps.weapon == WP_ALEVEL1 ) + VectorMA( muzzle, LEVEL1_GRAB_RANGE, forward, end ); + else if( ent->client->ps.weapon == WP_ALEVEL1_UPG ) + VectorMA( muzzle, LEVEL1_GRAB_U_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_TEAM ] == TEAM_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; + } +} + +/* +=============== +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->pers.teamSelection == TEAM_HUMANS ) + { + trap_Trace( &tr, muzzle, NULL, NULL, humanPlayer->s.origin, + humanPlayer->s.number, CONTENTS_SOLID ); + + //can't see target from here + if( tr.entityNum == ENTITYNUM_WORLD ) + continue; + + humanPlayer->client->ps.eFlags |= EF_POISONCLOUDED; + humanPlayer->client->lastPoisonCloudedTime = level.time; + + trap_SendServerCommand( humanPlayer->client->ps.clientNum, + "poisoncloud" ); + } + } + G_UnlaggedOff( ); +} + + +/* +====================================================================== + +LEVEL2 + +====================================================================== +*/ +#define MAX_ZAPS MAX_CLIENTS + +static zap_t zaps[ MAX_ZAPS ]; + +/* +=============== +G_FindZapChainTargets +=============== +*/ +static void G_FindZapChainTargets( zap_t *zap ) +{ + gentity_t *ent = zap->targets[ 0 ]; // the source + int entityList[ MAX_GENTITIES ]; + vec3_t range = { LEVEL2_AREAZAP_CHAIN_RANGE, + LEVEL2_AREAZAP_CHAIN_RANGE, + LEVEL2_AREAZAP_CHAIN_RANGE }; + vec3_t mins, maxs; + int i, num; + gentity_t *enemy; + trace_t tr; + float distance; + + 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 ] ]; + // don't chain to self; noclippers can be listed, don't chain to them either + if( enemy == ent || ( enemy->client && enemy->client->noclip ) ) + continue; + + distance = Distance( ent->s.origin, enemy->s.origin ); + + if((enemy->client&&enemy->client->ps.stats[STAT_TEAM]==TEAM_HUMANS) || + (enemy->s.eType==ET_BUILDABLE&&BG_Buildable(enemy->s.modelindex,NULL)->team==TEAM_HUMANS && + (!BG_Buildable(enemy->s.modelindex,NULL)->cuboid||BG_CuboidAttributes(enemy->s.modelindex)->zappable)) && + enemy->health>0 && distance <= LEVEL2_AREAZAP_CHAIN_RANGE) + { + + // world-LOS check: trace against the world, ignoring other BODY entities + trap_Trace( &tr, ent->s.origin, NULL, NULL, + enemy->s.origin, ent->s.number, CONTENTS_SOLID ); + + if( tr.entityNum == ENTITYNUM_NONE ) + { + zap->targets[ zap->numTargets ] = enemy; + zap->distances[ zap->numTargets ] = distance; + if( ++zap->numTargets >= LEVEL2_AREAZAP_MAX_TARGETS ) + return; + } + } + } +} + +/* +=============== +G_UpdateZapEffect +=============== +*/ +static void G_UpdateZapEffect( zap_t *zap ) +{ + int i; + int entityNums[ LEVEL2_AREAZAP_MAX_TARGETS + 1 ]; + + entityNums[ 0 ] = zap->creator->s.number; + + for( i = 0; i < zap->numTargets; i++ ) + entityNums[ i + 1 ] = zap->targets[ i ]->s.number; + + BG_PackEntityNumbers( &zap->effectChannel->s, + entityNums, zap->numTargets + 1 ); + + VectorCopy( zap->creator->s.origin, zap->effectChannel->r.currentOrigin ); + trap_LinkEntity( zap->effectChannel ); +} + +/* +=============== +G_CreateNewZap +=============== +*/ +static void G_CreateNewZap( gentity_t *creator, gentity_t *target ) +{ + int i; + zap_t *zap; + + for( i = 0; i < MAX_ZAPS; i++ ) + { + zap = &zaps[ i ]; + if( zap->used ) + continue; + + zap->used = qtrue; + zap->timeToLive = LEVEL2_AREAZAP_TIME; + + zap->creator = creator; + zap->targets[ 0 ] = target; + zap->numTargets = 1; + + // the zap chains only through living entities + if( target->health > 0 ) + { + G_Damage( target, creator, creator, forward, + target->s.origin, LEVEL2_AREAZAP_DMG, + DAMAGE_NO_KNOCKBACK | DAMAGE_NO_LOCDAMAGE, + MOD_LEVEL2_ZAP ); + + G_FindZapChainTargets( zap ); + + for( i = 1; i < zap->numTargets; i++ ) + { + G_Damage( zap->targets[ i ], target, zap->creator, forward, target->s.origin, + LEVEL2_AREAZAP_DMG * ( 1 - pow( (zap->distances[ i ] / + LEVEL2_AREAZAP_CHAIN_RANGE ), LEVEL2_AREAZAP_CHAIN_FALLOFF ) ) + 1, + DAMAGE_NO_KNOCKBACK | DAMAGE_NO_LOCDAMAGE, + MOD_LEVEL2_ZAP ); + } + } + + zap->effectChannel = G_Spawn( ); + zap->effectChannel->s.eType = ET_LEV2_ZAP_CHAIN; + zap->effectChannel->classname = "lev2zapchain"; + G_UpdateZapEffect( zap ); + + return; + } +} + + +/* +=============== +G_UpdateZaps +=============== +*/ +void G_UpdateZaps( int msec ) +{ + int i, j; + zap_t *zap; + + for( i = 0; i < MAX_ZAPS; i++ ) + { + zap = &zaps[ i ]; + if( !zap->used ) + continue; + + zap->timeToLive -= msec; + + // first, the disappearance of players is handled immediately in G_ClearPlayerZapEffects() + + // the deconstruction or gibbing of a directly targeted buildable destroys the whole zap effect + if( zap->timeToLive <= 0 || !zap->targets[ 0 ]->inuse ) + { + G_FreeEntity( zap->effectChannel ); + zap->used = qfalse; + continue; + } + + // the deconstruction or gibbing of chained buildables destroy the appropriate beams + for( j = 1; j < zap->numTargets; j++ ) + { + if( !zap->targets[ j ]->inuse ) + zap->targets[ j-- ] = zap->targets[ --zap->numTargets ]; + } + + G_UpdateZapEffect( zap ); + } +} + +/* +=============== +G_ClearPlayerZapEffects + +called from G_LeaveTeam() and TeleportPlayer() +=============== +*/ +void G_ClearPlayerZapEffects( gentity_t *player ) +{ + int i, j; + zap_t *zap; + + for( i = 0; i < MAX_ZAPS; i++ ) + { + zap = &zaps[ i ]; + if( !zap->used ) + continue; + + // the disappearance of the creator or the first target destroys the whole zap effect + if( zap->creator == player || zap->targets[ 0 ] == player ) + { + G_FreeEntity( zap->effectChannel ); + zap->used = qfalse; + continue; + } + + // the disappearance of chained players destroy the appropriate beams + for( j = 1; j < zap->numTargets; j++ ) + { + if( zap->targets[ j ] == player ) + zap->targets[ j-- ] = zap->targets[ --zap->numTargets ]; + } + } +} + +/* +=============== +areaZapFire +=============== +*/ +void areaZapFire( gentity_t *ent ) +{ + trace_t tr; + gentity_t *traceEnt; + + G_WideTrace( &tr, ent, LEVEL2_AREAZAP_RANGE, LEVEL2_AREAZAP_WIDTH, LEVEL2_AREAZAP_WIDTH, &traceEnt ); + + if( traceEnt == NULL ) + return; + + if( ( traceEnt->client && traceEnt->client->ps.stats[ STAT_TEAM ] == TEAM_HUMANS ) || + ( traceEnt->s.eType == ET_BUILDABLE && + BG_Buildable( traceEnt->s.modelindex, NULL )->team == TEAM_HUMANS ) && + (!BG_Buildable(traceEnt->s.modelindex,NULL)->cuboid||BG_CuboidAttributes(traceEnt->s.modelindex)->zappable)) + { + G_CreateNewZap( ent, traceEnt ); + } +} + + +/* +====================================================================== + +LEVEL3 + +====================================================================== +*/ + +/* +=============== +CheckPounceAttack +=============== +*/ +qboolean CheckPounceAttack( gentity_t *ent ) +{ + trace_t tr; + gentity_t *traceEnt; + int damage, timeMax, pounceRange, payload; + + if( ent->client->pmext.pouncePayload <= 0 ) + return qfalse; + + // In case the goon lands on his target, he get's one shot after landing + payload = ent->client->pmext.pouncePayload; + if( !( ent->client->ps.pm_flags & PMF_CHARGE ) ) + ent->client->pmext.pouncePayload = 0; + + // Calculate muzzle point + AngleVectors( ent->client->ps.viewangles, forward, right, up ); + CalcMuzzlePoint( ent, forward, right, up, muzzle ); + + // Trace from muzzle to see what we hit + pounceRange = ent->client->ps.weapon == WP_ALEVEL3 ? LEVEL3_POUNCE_RANGE : + LEVEL3_POUNCE_UPG_RANGE; + G_WideTrace( &tr, ent, pounceRange, LEVEL3_POUNCE_WIDTH, + LEVEL3_POUNCE_WIDTH, &traceEnt ); + if( traceEnt == NULL ) + return qfalse; + + // Send blood impact + if( traceEnt->takedamage ) + WideBloodSpurt( ent, traceEnt, &tr ); + + if( !traceEnt->takedamage ) + return qfalse; + + // Deal damage + timeMax = ent->client->ps.weapon == WP_ALEVEL3 ? LEVEL3_POUNCE_TIME : + LEVEL3_POUNCE_TIME_UPG; + damage = payload * LEVEL3_POUNCE_DMG / timeMax; + ent->client->pmext.pouncePayload = 0; + G_Damage( traceEnt, ent, ent, forward, tr.endpos, damage, + DAMAGE_NO_LOCDAMAGE, MOD_LEVEL3_POUNCE ); + + return qtrue; +} + +void bounceBallFire( gentity_t *ent ) +{ + fire_bounceBall( ent, muzzle, forward ); +} + + +/* +====================================================================== + +LEVEL4 + +====================================================================== +*/ + +/* +=============== +G_ChargeAttack +=============== +*/ +void G_ChargeAttack( gentity_t *ent, gentity_t *victim ) +{ + int damage; + int i; + vec3_t forward; + + if( ent->client->ps.stats[ STAT_MISC ] <= 0 || + !( ent->client->ps.stats[ STAT_STATE ] & SS_CHARGING ) || + ent->client->ps.weaponTime ) + return; + + VectorSubtract( victim->s.origin, ent->s.origin, forward ); + VectorNormalize( forward ); + + if( !victim->takedamage ) + return; + + // For buildables, track the last MAX_TRAMPLE_BUILDABLES_TRACKED buildables + // hit, and do not do damage if the current buildable is in that list + // in order to prevent dancing over stuff to kill it very quickly + if( !victim->client ) + { + for( i = 0; i < MAX_TRAMPLE_BUILDABLES_TRACKED; i++ ) + { + if( ent->client->trampleBuildablesHit[ i ] == victim - g_entities ) + return; + } + + ent->client->trampleBuildablesHit[ + ent->client->trampleBuildablesHitPos++ % MAX_TRAMPLE_BUILDABLES_TRACKED ] = + victim - g_entities; + } + + WideBloodSpurt( ent, victim, NULL ); + + damage = LEVEL4_TRAMPLE_DMG * ent->client->ps.stats[ STAT_MISC ] / + LEVEL4_TRAMPLE_DURATION; + + G_Damage( victim, ent, ent, forward, victim->s.origin, damage, + DAMAGE_NO_LOCDAMAGE, MOD_LEVEL4_TRAMPLE ); + + ent->client->ps.weaponTime += LEVEL4_TRAMPLE_REPEAT; +} + +/* +=============== +G_CrushAttack + +Should only be called if there was an impact between a tyrant and another player +=============== +*/ +void G_CrushAttack( gentity_t *ent, gentity_t *victim ) +{ + vec3_t dir; + float jump; + int damage; + + if( !victim->takedamage || + ent->client->ps.origin[ 2 ] + ent->r.mins[ 2 ] < + victim->s.origin[ 2 ] + victim->r.maxs[ 2 ] || + ( victim->client && + victim->client->ps.groundEntityNum == ENTITYNUM_NONE ) ) + return; + + // Deal velocity based damage to target + jump = BG_Class( ent->client->ps.stats[ STAT_CLASS ] )->jumpMagnitude; + damage = ( ent->client->pmext.fallVelocity + jump ) * + -LEVEL4_CRUSH_DAMAGE_PER_V; + + if( damage < 0 ) + damage = 0; + + // Players also get damaged periodically + if( victim->client && + ent->client->lastCrushTime + LEVEL4_CRUSH_REPEAT < level.time ) + { + ent->client->lastCrushTime = level.time; + damage += LEVEL4_CRUSH_DAMAGE; + } + + if( damage < 1 ) + return; + + // Crush the victim over a period of time + VectorSubtract( victim->s.origin, ent->client->ps.origin, dir ); + G_Damage( victim, ent, ent, dir, victim->s.origin, damage, + DAMAGE_NO_LOCDAMAGE, MOD_LEVEL4_CRUSH ); +} + +void rantBombFire( gentity_t *ent ) +{ + fire_rantBomb( ent, muzzle, forward ); +} + +//====================================================================== + +/* +=============== +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 ) +{ + vec3_t normal; + + VectorCopy( ent->client->ps.origin, muzzlePoint ); + BG_GetClientNormal( &ent->client->ps, normal ); + VectorMA( muzzlePoint, ent->client->ps.viewheight, normal, muzzlePoint ); + VectorMA( muzzlePoint, 1, forward, 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_ALEVEL4: + rantBombFire( 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_LUCIFER_CANNON: + LCChargeFire( ent, qtrue ); + break; + + case WP_ALEVEL2_UPG: + areaZapFire( ent ); + break; + + case WP_ABUILD: + case WP_ABUILD2: + case WP_HBUILD: + cancelBuildFire( ent ); + break; + default: + break; + } +} + +/* +=============== +FireWeapon +=============== +*/ +void FireWeapon( 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->turretAim, forward, right, up ); + VectorCopy( ent->s.pos.trBase, muzzle ); + } + + // fire the specific weapon + switch( ent->s.weapon ) + { + case WP_ALEVEL1: + meleeAttack( ent, LEVEL1_CLAW_RANGE, LEVEL1_CLAW_WIDTH, LEVEL1_CLAW_WIDTH, + LEVEL1_CLAW_DMG, MOD_LEVEL1_CLAW ); + break; + case WP_ALEVEL1_UPG: + meleeAttack( ent, LEVEL1_CLAW_U_RANGE, LEVEL1_CLAW_WIDTH, LEVEL1_CLAW_WIDTH, + LEVEL1_CLAW_DMG, MOD_LEVEL1_CLAW ); + break; + case WP_ALEVEL3: + meleeAttack( ent, LEVEL3_CLAW_RANGE, LEVEL3_CLAW_WIDTH, LEVEL3_CLAW_WIDTH, + LEVEL3_CLAW_DMG, MOD_LEVEL3_CLAW ); + break; + case WP_ALEVEL3_UPG: + meleeAttack( ent, LEVEL3_CLAW_UPG_RANGE, LEVEL3_CLAW_WIDTH, + LEVEL3_CLAW_WIDTH, LEVEL3_CLAW_DMG, MOD_LEVEL3_CLAW ); + break; + case WP_ALEVEL2: + meleeAttack( ent, LEVEL2_CLAW_RANGE, LEVEL2_CLAW_WIDTH, LEVEL2_CLAW_WIDTH, + LEVEL2_CLAW_DMG, MOD_LEVEL2_CLAW ); + break; + case WP_ALEVEL2_UPG: + meleeAttack( ent, LEVEL2_CLAW_U_RANGE, LEVEL2_CLAW_WIDTH, LEVEL2_CLAW_WIDTH, + LEVEL2_CLAW_DMG, MOD_LEVEL2_CLAW ); + break; + case WP_ALEVEL4: + meleeAttack( ent, LEVEL4_CLAW_RANGE, LEVEL4_CLAW_WIDTH, + LEVEL4_CLAW_HEIGHT, 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: + 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..effa064 --- /dev/null +++ b/src/game/tremulous.h @@ -0,0 +1,718 @@ +/* +=========================================================================== +Copyright (C) 1999-2005 Id Software, Inc. +Copyright (C) 2000-2009 Darklegion Development + +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_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 2000 + +#define LEVEL0_BITE_DMG ADM(36) +#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 64.0f +#define LEVEL1_CLAW_U_RANGE LEVEL1_CLAW_RANGE + 3.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 96.0f +#define LEVEL1_GRAB_U_RANGE LEVEL1_GRAB_RANGE + 3.0f +#define LEVEL1_GRAB_TIME 300 +#define LEVEL1_GRAB_U_TIME 300 +#define LEVEL1_PCLOUD_DMG ADM(4) +#define LEVEL1_PCLOUD_RANGE 120.0f +#define LEVEL1_PCLOUD_REPEAT 2000 +#define LEVEL1_PCLOUD_TIME 10000 +#define LEVEL1_REGEN_MOD 2.0f +#define LEVEL1_UPG_REGEN_MOD 3.0f +#define LEVEL1_REGEN_SCOREINC AVM(100) // score added for healing per 10s +#define LEVEL1_UPG_REGEN_SCOREINC AVM(200) + +#define LEVEL2_CLAW_DMG ADM(40) +#define LEVEL2_CLAW_RANGE 80.0f +#define LEVEL2_CLAW_U_RANGE LEVEL2_CLAW_RANGE + 2.0f +#define LEVEL2_CLAW_WIDTH 14.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 6 //ADM(60) +#define LEVEL2_AREAZAP_RANGE 200.0f +#define LEVEL2_AREAZAP_CHAIN_RANGE 150.0f +#define LEVEL2_AREAZAP_CHAIN_FALLOFF 8.0f +#define LEVEL2_AREAZAP_WIDTH 15.0f +#define LEVEL2_AREAZAP_REPEAT 100 //1500 +#define LEVEL2_AREAZAP_TIME 100 //1000 +#define LEVEL2_AREAZAP_MAX_TARGETS 5 +#define LEVEL2_WALLJUMP_MAXSPEED 1000.0f + +#define LEVEL3_CLAW_DMG ADM(80) +#define LEVEL3_CLAW_RANGE 80.0f +#define LEVEL3_CLAW_UPG_RANGE LEVEL3_CLAW_RANGE + 3.0f +#define LEVEL3_CLAW_WIDTH 12.0f +#define LEVEL3_CLAW_REPEAT 900 +#define LEVEL3_CLAW_K_SCALE 1.0f +#define LEVEL3_CLAW_U_REPEAT 800 +#define LEVEL3_CLAW_U_K_SCALE 1.0f +#define LEVEL3_POUNCE_DMG ADM(75) //ADM(100) +#define LEVEL3_POUNCE_RANGE 48.0f +#define LEVEL3_POUNCE_UPG_RANGE LEVEL3_POUNCE_RANGE + 3.0f +#define LEVEL3_POUNCE_WIDTH 14.0f +#define LEVEL3_POUNCE_TIME 800 // msec for full Dragoon pounce +#define LEVEL3_POUNCE_TIME_UPG 800 // msec for full Adv. Dragoon pounce +#define LEVEL3_POUNCE_TIME_MIN 200 // msec before which pounce cancels +#define LEVEL3_POUNCE_REPEAT 400 // msec before a new pounce starts +#define LEVEL3_POUNCE_SPEED_MOD 0.75f // walking speed modifier for pounce charging +#define LEVEL3_POUNCE_JUMP_MAG 700 // Dragoon pounce jump power +#define LEVEL3_POUNCE_JUMP_MAG_UPG 800 // Adv. Dragoon pounce jump power +#define LEVEL3_BOUNCEBALL_DMG ADM(110) +#define LEVEL3_BOUNCEBALL_REPEAT 1200 +#define LEVEL3_BOUNCEBALL_SPEED 1000.0f +#define LEVEL3_BOUNCEBALL_RADIUS 75 +#define LEVEL3_BOUNCEBALL_REGEN 15000 // msec until new barb + +#define LEVEL4_CLAW_DMG ADM(100) +#define LEVEL4_CLAW_RANGE 100.0f +#define LEVEL4_CLAW_WIDTH 14.0f +#define LEVEL4_CLAW_HEIGHT 20.0f +#define LEVEL4_CLAW_REPEAT 800 +#define LEVEL4_CLAW_K_SCALE 1.0f + +#define LEVEL4_TRAMPLE_DMG ADM(111) +#define LEVEL4_TRAMPLE_SPEED 2.0f +#define LEVEL4_TRAMPLE_CHARGE_MIN 375 // minimum msec to start a charge +#define LEVEL4_TRAMPLE_CHARGE_MAX 1000 // msec to maximum charge stored +#define LEVEL4_TRAMPLE_CHARGE_TRIGGER 3000 // msec charge starts on its own +#define LEVEL4_TRAMPLE_DURATION 3000 // msec trample lasts on full charge +#define LEVEL4_TRAMPLE_STOP_PENALTY 1 // charge lost per msec when stopped +#define LEVEL4_TRAMPLE_REPEAT 100 // msec before a trample will rehit a player + +#define LEVEL4_CRUSH_DAMAGE_PER_V 0.5f // damage per falling velocity +#define LEVEL4_CRUSH_DAMAGE 120 // to players only +#define LEVEL4_CRUSH_REPEAT 500 // player damage repeat + +#define LEVEL4_BOMB_DMG 150 +#define LEVEL4_BOMB_RADIUS 400 +#define LEVEL4_BOMB_SPEED 500.0f +#define LEVEL4_BOMB_REGEN 120000 // minimum time between getting a bomb + +/* + * 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.9f +#define ABUILDER_VALUE AVM(240) +#define ABUILDER_HEALTH AHM(50) +#define ABUILDER_REGEN (0.04f * ABUILDER_HEALTH) +#define ABUILDER_COST 0 + +#define ABUILDER_UPG_SPEED 0.9f +#define ABUILDER_UPG_VALUE AVM(300) +#define ABUILDER_UPG_HEALTH AHM(75) +#define ABUILDER_UPG_REGEN (0.04f * ABUILDER_UPG_HEALTH) +#define ABUILDER_UPG_COST 0 + +#define LEVEL0_SPEED 1.4f +#define LEVEL0_VALUE AVM(180) +#define LEVEL0_HEALTH AHM(25) +#define LEVEL0_REGEN (0.05f * LEVEL0_HEALTH) +#define LEVEL0_COST 0 + +#define LEVEL1_SPEED 1.25f +#define LEVEL1_VALUE AVM(270) +#define LEVEL1_HEALTH AHM(60) +#define LEVEL1_REGEN (0.03f * LEVEL1_HEALTH) +#define LEVEL1_COST 1 + +#define LEVEL1_UPG_SPEED 1.25f +#define LEVEL1_UPG_VALUE AVM(330) +#define LEVEL1_UPG_HEALTH AHM(80) +#define LEVEL1_UPG_REGEN (0.03f * LEVEL1_UPG_HEALTH) +#define LEVEL1_UPG_COST 1 + +#define LEVEL2_SPEED 1.2f +#define LEVEL2_VALUE AVM(420) +#define LEVEL2_HEALTH AHM(150) +#define LEVEL2_REGEN (0.03f * LEVEL2_HEALTH) +#define LEVEL2_COST 1 + +#define LEVEL2_UPG_SPEED 1.2f +#define LEVEL2_UPG_VALUE AVM(540) +#define LEVEL2_UPG_HEALTH AHM(175) +#define LEVEL2_UPG_REGEN (0.03f * LEVEL2_UPG_HEALTH) +#define LEVEL2_UPG_COST 1 + +#define LEVEL3_SPEED 1.1f +#define LEVEL3_VALUE AVM(600) +#define LEVEL3_HEALTH AHM(200) +#define LEVEL3_REGEN (0.03f * LEVEL3_HEALTH) +#define LEVEL3_COST 1 + +#define LEVEL3_UPG_SPEED 1.1f +#define LEVEL3_UPG_VALUE AVM(720) +#define LEVEL3_UPG_HEALTH AHM(250) +#define LEVEL3_UPG_REGEN (0.03f * LEVEL3_UPG_HEALTH) +#define LEVEL3_UPG_COST 1 + +#define LEVEL4_SPEED 1.2f +#define LEVEL4_VALUE AVM(960) +#define LEVEL4_HEALTH AHM(350) +#define LEVEL4_REGEN (0.025f * LEVEL4_HEALTH) +#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 ALIEN_BVALUE_MODIFIER 90.0f +#define ABVM(h) ((int)((float)h*ALIEN_BVALUE_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 PCLOUD_MODIFIER 0.5f +#define PCLOUD_ARMOUR_MODIFIER 0.75f + +#define ASPAWN_BP 10 +#define ASPAWN_BT 10000 +#define ASPAWN_HEALTH ABHM(250) +#define ASPAWN_REGEN 8 +#define ASPAWN_SPLASHDAMAGE 50 +#define ASPAWN_SPLASHRADIUS 100 +#define ASPAWN_CREEPSIZE 120 +#define ASPAWN_VALUE ABVM(ASPAWN_BP) + +#define BARRICADE_BP 8 +#define BARRICADE_BT 15000 +#define BARRICADE_HEALTH ABHM(300) +#define BARRICADE_REGEN 14 +#define BARRICADE_SPLASHDAMAGE 50 +#define BARRICADE_SPLASHRADIUS 100 +#define BARRICADE_CREEPSIZE 120 +#define BARRICADE_SHRINKPROP 0.25f +#define BARRICADE_SHRINKTIMEOUT 500 +#define BARRICADE_VALUE ABVM(BARRICADE_BP) + +#define BOOSTER_BP 12 +#define BOOSTER_BT 10000 +#define BOOSTER_HEALTH ABHM(150) +#define BOOSTER_REGEN 8 +#define BOOSTER_SPLASHDAMAGE 50 +#define BOOSTER_SPLASHRADIUS 100 +#define BOOSTER_CREEPSIZE 120 +#define BOOSTER_REGEN_MOD 3.0f +#define BOOSTER_VALUE ABVM(BOOSTER_BP) +#define BOOST_TIME 20000 +#define BOOST_WARN_TIME 15000 + +#define ACIDTUBE_BP 8 +#define ACIDTUBE_BT 10000 +#define ACIDTUBE_HEALTH ABHM(125) +#define ACIDTUBE_REGEN 10 +#define ACIDTUBE_SPLASHDAMAGE 50 +#define ACIDTUBE_SPLASHRADIUS 100 +#define ACIDTUBE_CREEPSIZE 120 +#define ACIDTUBE_DAMAGE 8 +#define ACIDTUBE_RANGE 300.0f +#define ACIDTUBE_REPEAT 300 +#define ACIDTUBE_REPEAT_ANIM 2000 +#define ACIDTUBE_VALUE ABVM(ACIDTUBE_BP) + +#define HIVE_BP 12 +#define HIVE_BT 15000 +#define HIVE_HEALTH ABHM(125) +#define HIVE_REGEN 10 +#define HIVE_SPLASHDAMAGE 30 +#define HIVE_SPLASHRADIUS 200 +#define HIVE_CREEPSIZE 120 +#define HIVE_SENSE_RANGE 500.0f +#define HIVE_LIFETIME 3000 +#define HIVE_REPEAT 3000 +#define HIVE_K_SCALE 0.0f +#define HIVE_DMG 80 +#define HIVE_SPEED 320.0f +#define HIVE_DIR_CHANGE_PERIOD 500 +#define HIVE_VALUE ABVM(HIVE_BP) + +#define TRAPPER_BP 8 +#define TRAPPER_BT 7500 +#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_VALUE ABVM(TRAPPER_BP) +#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 20000 +#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 ABVM(30) + +/* + * ALIEN misc + * + * ALIENSENSE_RANGE - the distance alien sense is useful for + * + */ + +#define ALIENSENSE_RANGE 1000.0f +#define REGEN_BOOST_RANGE 200.0f + +#define ALIEN_POISON_TIME 10000 +#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 +#define ALIEN_REGEN_NOCREEP_MOD (1.0f/3.0f) //regen off creep + +#define ALIEN_MAX_FRAGS 9 +#define ALIEN_MAX_CREDITS (ALIEN_MAX_FRAGS*ALIEN_CREDITS_PER_KILL) +#define ALIEN_CREDITS_PER_KILL 400 +#define ALIEN_TK_SUICIDE_PENALTY 350 + +/* + * 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(10) +#define BLASTER_SIZE 5 + +#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(11) +#define PAINSAW_RANGE 64.0f +#define PAINSAW_WIDTH 0.0f +#define PAINSAW_HEIGHT 8.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 11 //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 700 +#define SHOTGUN_DMG HDM(5) +#define SHOTGUN_RANGE (8192 * 12) + +#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(40) +#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 900 +#define CHAINGUN_DMG HDM(6) + +#define FLAMER_PRICE 400 +#define FLAMER_GAS 200 +#define FLAMER_REPEAT 200 +#define FLAMER_K_SCALE 0.0f +#define FLAMER_DMG HDM(20) +#define FLAMER_SPLASHDAMAGE HDM(10) +#define FLAMER_RADIUS 50 // splash radius +#define FLAMER_SIZE 15 // missile bounding box +#define FLAMER_LIFETIME 700.0f +#define FLAMER_SPEED 500.0f +#define FLAMER_LAG 0.65f // the amount of player velocity that is added to the fireball + +#define PRIFLE_PRICE 450 +#define PRIFLE_CLIPS 40 +#define PRIFLE_MAXCLIPS 5 +#define PRIFLE_REPEAT 100 +#define PRIFLE_K_SCALE 1.0f +#define PRIFLE_RELOAD 2000 +#define PRIFLE_DMG HDM(9) +#define PRIFLE_SPEED 1200 +#define PRIFLE_SIZE 5 + +#define LCANNON_PRICE 600 +#define LCANNON_AMMO 90 +#define LCANNON_K_SCALE 1.0f +#define LCANNON_REPEAT 500 +#define LCANNON_RELOAD 0 +#define LCANNON_DAMAGE HDM(265) +#define LCANNON_RADIUS 150 +#define LCANNON_SIZE 5 // missile bounding box radius +#define LCANNON_SECONDARY_DAMAGE HDM(27) +#define LCANNON_SECONDARY_RADIUS 75 +#define LCANNON_SECONDARY_SPEED 350 +#define LCANNON_SECONDARY_RELOAD 2000 +#define LCANNON_SECONDARY_REPEAT 666 +#define LCANNON_SPEED 350 +#define LCANNON_TOTAL_CHARGE 255 +#define LCANNON_CHARGE_TIME_MIN 50 +#define LCANNON_CHARGE_TIME_MAX 2000 +#define LCANNON_CHARGE_TIME_WARN 1500 +#define LCANNON_CHARGE_AMMO 10 + +#define HBUILD_PRICE 0 +#define HBUILD_REPEAT 1000 +#define HBUILD_HEALRATE 18 + +/* + * HUMAN upgrades + */ + +#define LIGHTARMOUR_PRICE 70 +#define LIGHTARMOUR_POISON_PROTECTION 1 +#define LIGHTARMOUR_PCLOUD_PROTECTION 1000 + +#define HELMET_RANGE 1000.0f +#define HELMET_MK1_PRICE 90 +#define HELMET_MK1_POISON_PROTECTION 0 +#define HELMET_MK1_PCLOUD_PROTECTION 500 +#define HELMET_MK2_PRICE HELMET_MK1_PRICE +#define HELMET_MK2_POISON_PROTECTION 1 +#define HELMET_MK2_PCLOUD_PROTECTION 1000 + +#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 JETPACK_FUEL_FULL 6000 //can't exceed 32767 +#define JETPACK_FUEL_LOW 1500 +#define JETPACK_FUEL_USAGE 10 //every 100ms +#define JETPACK_FUEL_JUMP 85 + +#define BSUIT_PRICE 400 +#define BSUIT_POISON_PROTECTION 3 +#define BSUIT_PCLOUD_PROTECTION 3000 + +#define MGCLIP_PRICE 0 + +#define CGAMMO_PRICE 0 + +#define GAS_PRICE 0 + +#define MEDKIT_POISON_IMMUNITY_TIME 15000 +#define MEDKIT_STARTUP_TIME 4000 +#define MEDKIT_STARTUP_SPEED 5 + +#define BIORES_PRICE 300 +#define BIORES_EQUATION (1-hp)*sqrt(hp)*5+0.2 //health regenerated per second, note: hp is a floating-point value 0 <= hp <= 1 +#define BIORES_POISON_MODIFIER 0.3f + +/* + * 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 HUMAN_BVALUE_MODIFIER 240.0f +#define HBVM(h) ((int)((float)h*(float)HUMAN_BVALUE_MODIFIER)) // remember these are measured in credits not frags (c.f. ALIEN_CREDITS_PER_KILL) + +#define REACTOR_BASESIZE 1000 +#define REPEATER_BASESIZE 500 +#define HUMAN_DETONATION_DELAY 5000 + +#define HSPAWN_BP 10 +#define HSPAWN_BT 15000 +#define HSPAWN_HEALTH HBHM(310) +#define HSPAWN_SPLASHDAMAGE 50 +#define HSPAWN_SPLASHRADIUS 100 +#define HSPAWN_VALUE HBVM(HSPAWN_BP) + +#define MEDISTAT_BP 8 +#define MEDISTAT_BT 15000 +#define MEDISTAT_HEALTH HBHM(190) +#define MEDISTAT_SPLASHDAMAGE 50 +#define MEDISTAT_SPLASHRADIUS 100 +#define MEDISTAT_VALUE HBVM(MEDISTAT_BP) + +#define MGTURRET_BP 8 +#define MGTURRET_BT 15000 +#define MGTURRET_HEALTH HBHM(190) +#define MGTURRET_SPLASHDAMAGE 100 +#define MGTURRET_SPLASHRADIUS 100 +#define MGTURRET_ANGULARSPEED 12 +#define MGTURRET_ACCURACY_TO_FIRE 0 +#define MGTURRET_VERTICALCAP 30 // +/- maximum pitch +#define MGTURRET_REPEAT 150 +#define MGTURRET_K_SCALE 1.0f +#define MGTURRET_RANGE 400.0f +#define MGTURRET_SPREAD 200 +#define MGTURRET_DMG HDM(8) +#define MGTURRET_SPINUP_TIME 750 // time between target sighted and fire +#define MGTURRET_VALUE HBVM(MGTURRET_BP) + +#define TESLAGEN_BP 10 +#define TESLAGEN_BT 20000 +#define TESLAGEN_HEALTH HBHM(220) +#define TESLAGEN_SPLASHDAMAGE 50 +#define TESLAGEN_SPLASHRADIUS 100 +#define TESLAGEN_REPEAT 250 +#define TESLAGEN_K_SCALE 1.0f //4.0f +#define TESLAGEN_RANGE 200 +#define TESLAGEN_DMG HDM(10) +#define TESLAGEN_VALUE HBVM(TESLAGEN_BP) + +#define DC_BP 8 +#define DC_BT 15000 +#define DC_HEALTH HBHM(190) +#define DC_SPLASHDAMAGE 50 +#define DC_SPLASHRADIUS 100 +#define DC_ATTACK_PERIOD 10000 // how often to spam "under attack" +#define DC_HEALRATE 4 +#define DC_RANGE 1000 +#define DC_VALUE HBVM(DC_BP) + +#define ARMOURY_BP 10 +#define ARMOURY_BT 15000 +#define ARMOURY_HEALTH HBHM(420) +#define ARMOURY_SPLASHDAMAGE 50 +#define ARMOURY_SPLASHRADIUS 100 +#define ARMOURY_VALUE HBVM(ARMOURY_BP) + +#define REACTOR_BP 0 +#define REACTOR_BT 30000 +#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_ATTACK_DCC_REPEAT 1000 +#define REACTOR_ATTACK_DCC_RANGE 150.0f +#define REACTOR_ATTACK_DCC_DAMAGE 40 +#define REACTOR_VALUE HBVM(30) + +#define REPEATER_BP 4 +#define REPEATER_BT 15000 +#define REPEATER_HEALTH HBHM(250) +#define REPEATER_SPLASHDAMAGE 50 +#define REPEATER_SPLASHRADIUS 100 +#define REPEATER_VALUE HBVM(REPEATER_BP) + +/* + * 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 HUMAN_LAND_FRICTION 3.0f + +#define STAMINA_STOP_RESTORE 30 +#define STAMINA_WALK_RESTORE 15 +#define STAMINA_MEDISTAT_RESTORE 30 // stacked on STOP or WALK +#define STAMINA_SPRINT_TAKE 6 +#define STAMINA_JUMP_TAKE 250 +#define STAMINA_DODGE_TAKE 250 +#define STAMINA_MAX 1000 +#define STAMINA_BREATHING_LEVEL 0 +#define STAMINA_SLOW_LEVEL -500 +#define STAMINA_BLACKOUT_LEVEL -800 + +#define HUMAN_SPAWN_REPEAT_TIME 10000 +#define HUMAN_REGEN_DAMAGE_TIME 2000 //msec since damage before dcc repairs + +#define HUMAN_MAX_CREDITS 2000 +#define HUMAN_TK_SUICIDE_PENALTY 150 + +#define HUMAN_BUILDER_SCOREINC 50 // builders receive this many points every 10 seconds +#define ALIEN_BUILDER_SCOREINC AVM(100) // builders receive this many points every 10 seconds + +#define HUMAN_BUILDABLE_INACTIVE_TIME 90000 + +/* + * 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 DEFAULT_FREEKILL_PERIOD "120" //seconds +#define FREEKILL_ALIEN ALIEN_CREDITS_PER_KILL +#define FREEKILL_HUMAN LEVEL0_VALUE + +#define DEFAULT_ALIEN_BUILDPOINTS "150" +#define DEFAULT_ALIEN_QUEUE_TIME "12000" +#define DEFAULT_ALIEN_STAGE2_THRESH "12000" +#define DEFAULT_ALIEN_STAGE3_THRESH "24000" +#define DEFAULT_ALIEN_MAX_STAGE "2" +#define DEFAULT_HUMAN_BUILDPOINTS "100" +#define DEFAULT_HUMAN_QUEUE_TIME "8000" +#define DEFAULT_HUMAN_REPEATER_BUILDPOINTS "20" +#define DEFAULT_HUMAN_REPEATER_QUEUE_TIME "2000" +#define DEFAULT_HUMAN_REPEATER_MAX_ZONES "500" +#define DEFAULT_HUMAN_STAGE2_THRESH "6000" +#define DEFAULT_HUMAN_STAGE3_THRESH "12000" +#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 + +#define MAXIMUM_BUILD_TIME 20000 // used for pie timer + +/* + * Cuboids + */ + +#define CUBOID_MINVOLUME 150 +//everything else is on the bottom of bg_misc.c + +/* + * Impregnation + */ + +#define ALIEN_IMPREGNATION_CHANCE_NAKED 0.60f +#define ALIEN_IMPREGNATION_CHANCE_ARMORED 0.15f +#define ALIEN_IMPREGNATION_REWARD ALIEN_CREDITS_PER_KILL //reward for the granger than impregnated (awarded when someone kills the human by spawning) +#define ALIEN_IMPREGNATION_REWARD_SCORE 1500 //15 points + +#define ALIEN_IMPLANT_MATURING_TIME_MIN 8000 //time before maturing can occur +#define ALIEN_IMPLANT_MATURING_CHANCE 0.05f //chance of maturing (every second) + +#define ALIEN_HATCHING_VELOCITY 250.0f +#define ALIEN_HATCHING_MAX_BATTLESUIT_HEALTH 80 // alien dies if tried to spawn from a battlesuit at least this healthy +#define ALIEN_FAILED_HATCH_DAMAGE 65 // shouldn't be higher than the value above |