From 68da0ee4bead3a36b5057471a30414f11cc01577 Mon Sep 17 00:00:00 2001 From: Tim Angus Date: Mon, 31 Jul 2006 21:50:12 +0000 Subject: * tjw's spiffy admin system (tjw, obviously) --- Makefile | 1 + misc/manual.lyx | 1049 ++++++++++++++++++++ src/game/g_admin.c | 2666 +++++++++++++++++++++++++++++++++++++++++++++++++++ src/game/g_admin.h | 174 ++++ src/game/g_client.c | 108 ++- src/game/g_cmds.c | 422 ++++++-- src/game/g_local.h | 32 +- src/game/g_main.c | 21 + src/game/g_mem.c | 2 +- src/game/g_svcmds.c | 4 + 10 files changed, 4404 insertions(+), 75 deletions(-) create mode 100644 src/game/g_admin.c create mode 100644 src/game/g_admin.h diff --git a/Makefile b/Makefile index 193eb0a9..7a8ed11d 100644 --- a/Makefile +++ b/Makefile @@ -1298,6 +1298,7 @@ GOBJ_ = \ $(B)/base/game/g_maprotation.o \ $(B)/base/game/g_ptr.o \ $(B)/base/game/g_weapon.o \ + $(B)/base/game/g_admin.o \ \ $(B)/base/qcommon/q_math.o \ $(B)/base/qcommon/q_shared.o diff --git a/misc/manual.lyx b/misc/manual.lyx index 857b36dd..e0bc0e7d 100644 --- a/misc/manual.lyx +++ b/misc/manual.lyx @@ -7109,6 +7109,1055 @@ random \series default condition simply chooses whether or not to execute the change randomly, with each outcome equally likely. +\layout Subsection + +Server Administration System (g_admin) +\layout Standard + +The Tremulous game code has a built-in administration system which can work + outside of traditional server console/rcon admin commands. + Instead of passwords, administration rights are granted on a unique player + identifier called cl_guid. + Because of this, day to day administration tasks (like !kick and !mute) + can easily be shared among a server's regular players without the risk + of giving those players too much power or having to share passwords. +\layout Standard + +Although specific admin rights can be granted to an individual, rights are + primarily handed through a level system. + By default there are 6 levels defined (0-5). + Players with out any admin status are treated as level 0 with various additiona +l rights added to each following level with level 5 having full rights. + You can change what rights each level has by editing the configuration + file (see below). + Levels are referenced by number, but they can also be given names. + There can up to 32 levels defined. + The number used to define the level has special significance since rights + are handled very heirarchically (e.g. + a level 4 admin can not !mute a level 5 admin since his victim has a higher + level). +\layout Standard + +Administrator rights can granted with !setlevel command so a server operator + need not leave the game, edit files, restart, or even type a password to + adjust another player's admin status. + However, the configuration for this system is contained in an easy to edit + text file that allows a great deal of flexibility in configuring fine-grained + access rights for each user and/or access level. +\layout Subsubsection + +Quick Start +\layout Standard + +To get started, you simply need to ensure that the g_admin cvar is set to + the name of a writable data file (default is +\begin_inset Quotes eld +\end_inset + +admin.dat +\begin_inset Quotes erd +\end_inset + +). + Then connect to the server with your Tremulous client, then run the following + command in your client console: +\layout LyX-Code + +/rcon YOUR_RCON_PASSORD !setlevel YOUR_NAME 5 +\layout Standard + +By default, the level 5 user is a super-user and has access to all '!' commands. + From that point you can use the /!help command in your client to familiarize + yourself with all the commands. +\layout Subsubsection + +Related Cvars +\layout Standard + + +\begin_inset Tabular + + + + + + +\begin_inset Text + +\layout Standard + +g_admin +\end_inset + + +\begin_inset Text + +\layout Standard + +Set to the name of the file in the fs_game directory that should + contain all admin data such as admin definitions and bans. +\layout Standard + +If set to a blank string +\begin_inset Quotes eld +\end_inset + + +\begin_inset Quotes erd +\end_inset + + admin commands will not be available. +\layout Standard + +Example: +\layout Standard + +set g_admin +\begin_inset Quotes eld +\end_inset + +admin.dat +\begin_inset Quotes erd +\end_inset + + +\layout Standard + +Defaults to +\begin_inset Quotes eld +\end_inset + +admin.dat +\begin_inset Quotes erd +\end_inset + + (off) +\end_inset + + + + +\begin_inset Text + +\layout Standard + +g_adminLog +\end_inset + + +\begin_inset Text + +\layout Standard + +Set to the name of the file in the fs_game directory that will + log all '!' commands. +\layout Standard + +Defaults to +\begin_inset Quotes eld +\end_inset + +admin.log +\begin_inset Quotes erd +\end_inset + + +\end_inset + + + + +\begin_inset Text + +\layout Standard + +g_adminParseSay +\end_inset + + +\begin_inset Text + +\layout Standard + +Set this to non-zero if you want the admin system to accept commands in + player chat messages. +\layout Standard + +Default is 1 (on) +\end_inset + + + + +\begin_inset Text + +\layout Standard + +g_adminNameProtect +\end_inset + + +\begin_inset Text + +\layout Standard + +Set this to non-zero if you want the admin system to lock each admin's name + to his cl_guid to prevent imporsonation. +\layout Standard + +Default is 1 (on) +\end_inset + + + + +\begin_inset Text + +\layout Standard + +g_adminTempBan +\end_inset + + +\begin_inset Text + +\layout Standard + +Set this to the number of seconds a player should be automatically banned + for when he/she is vote kicked or kicked with the !kick command. +\layout Standard + +Default is 120 (two minutes) +\end_inset + + + + +\end_inset + + +\layout Subsubsection + +Data File Format +\layout Standard + +All admin authorization, configuration, and ban information is storedin + the file identified with the g_admin cvar. + This file is plain text and each of the data elements are seperated by + blank lines. + The supported data elements are [level], [admin], [ban], and [command]. + +\layout Standard + +The [level] block is used to define which admin rights a user of a particular + level has. + For example: +\layout LyX-Code + +[level] +\layout LyX-Code + +level = 3 +\layout LyX-Code + +name = Level 3 Admin +\layout LyX-Code + +flags = i1ahCpPkmy +\layout Standard + +This definition grants all level 3 admins all the commands identified by + the characters in the flags string (see flags table below). +\layout Standard + +The [admin] block is used to define all players with administrative rights + as identified by their cl_guid. + These blocks are created/updated/deleted automatically when the !setlevel + command is used. + Additionally, these blocks can be used to grant special rights to specific + users above or below the rights given to that user's [level] definition. + For example: +\layout LyX-Code + +[admin] +\layout LyX-Code + +name = bill +\layout LyX-Code + +guid = 1ABABAA74D54C3D25722E5E21121334 +\layout LyX-Code + +level = 3 +\layout LyX-Code + +flags = B-ym +\layout Standard + +This grants the user bill all the rights of a level 3 user, plus the 'B' + flag which grants access to the !showbans command. + It also takes away from bill the !allready (y) command and the !mute and + !unmute (m) commands. +\layout Standard + +The [ban] block is created with the !ban command, and removed with the !unban + command (or when it expires). + Both the guid and the ip parameters are used for ban enforement. + The ip parameter can also be used to crudely widen the scope of the IP + ban. + For example: +\layout LyX-Code + +[ban] +\layout LyX-Code + +name = all !nexterholland@ +\layout LyX-Code + +guid = ABCABCABCABCABCABCABCABCABCABCAB +\layout LyX-Code + +ip = 206.248.131. +\layout LyX-Code + +reason = banned by admin +\layout LyX-Code + +made = 04/18/06 19:15:35 +\layout LyX-Code + +expires = 0 +\layout LyX-Code + +banner = Fry +\layout Standard + +This would prevent anyone with an IP address inside of 206.248.131.0/24 or + with the cl_guid ABCABCABCABCABCABCABCABCABCABCAB from connecting to the + server. + The expires field is the UNIX timestamp when the ban is no longer in effect, + the special case is 0 which means it never expires. +\layout Standard + +The [command] block can be used to create simple ! commands. + The most practial use is to create certain .cfg files which change game + settings and allow high ranking admins to load up those settings through + a ! command. + For example: +\layout LyX-Code + +[command] +\layout LyX-Code + +command = havefun +\layout LyX-Code + +exec = exec fun.cfg +\layout LyX-Code + +desc = Load up some crazy settings/commands levels = 4 5 +\layout Standard + +This would allow all level 4 and 5 admins to run the command !havefun which + would be similar to running the command +\begin_inset Quotes eld +\end_inset + +exec fun.cfg +\begin_inset Quotes erd +\end_inset + + on the server console. +\layout Subsubsection + +Admin Flags +\layout Standard + +Both the [level] and [admin] blocks have the flags parameter which is a + string of characters that grant access rights. + The following table shows the flags for built-in COMMANDS: +\layout Standard + + +\begin_inset Tabular + + + + + + +\begin_inset Text + +\layout Standard + +FLAG +\end_inset + + +\begin_inset Text + +\layout Standard + +COMMAND +\end_inset + + + + +\begin_inset Text + +\layout Standard + +a +\end_inset + + +\begin_inset Text + +\layout Standard + +!admintest +\end_inset + + + + +\begin_inset Text + +\layout Standard + +y +\end_inset + + +\begin_inset Text + +\layout Standard + +!allready +\end_inset + + + + +\begin_inset Text + +\layout Standard + +b +\end_inset + + +\begin_inset Text + +\layout Standard + +!ban/!unban +\end_inset + + + + +\begin_inset Text + +\layout Standard + +c +\end_inset + + +\begin_inset Text + +\layout Standard + +!cancelvote/!passvote +\end_inset + + + + +\begin_inset Text + +\layout Standard + +h +\end_inset + + +\begin_inset Text + +\layout Standard + +!help +\end_inset + + + + +\begin_inset Text + +\layout Standard + +k +\end_inset + + +\begin_inset Text + +\layout Standard + +!kick +\end_inset + + + + +\begin_inset Text + +\layout Standard + +D +\end_inset + + +\begin_inset Text + +\layout Standard + +!listadmins +\end_inset + + + + +\begin_inset Text + +\layout Standard + +i +\end_inset + + +\begin_inset Text + +\layout Standard + +!listplayers +\end_inset + + + + +\begin_inset Text + +\layout Standard + +m +\end_inset + + +\begin_inset Text + +\layout Standard + +!mute/!unmute +\end_inset + + + + +\begin_inset Text + +\layout Standard + +e +\end_inset + + +\begin_inset Text + +\layout Standard + +!namelog +\end_inset + + + + +\begin_inset Text + +\layout Standard + +n +\end_inset + + +\begin_inset Text + +\layout Standard + +!nextmap +\end_inset + + + + +\begin_inset Text + +\layout Standard + +p +\end_inset + + +\begin_inset Text + +\layout Standard + +!putteam +\end_inset + + + + +\begin_inset Text + +\layout Standard + +G +\end_inset + + +\begin_inset Text + +\layout Standard + +!readconfig +\end_inset + + + + +\begin_inset Text + +\layout Standard + +N +\end_inset + + +\begin_inset Text + +\layout Standard + +!rename +\end_inset + + + + +\begin_inset Text + +\layout Standard + +r +\end_inset + + +\begin_inset Text + +\layout Standard + +!restart +\end_inset + + + + +\begin_inset Text + +\layout Standard + +s +\end_inset + + +\begin_inset Text + +\layout Standard + +!setlevel +\end_inset + + + + +\begin_inset Text + +\layout Standard + +B +\end_inset + + +\begin_inset Text + +\layout Standard + +!showbans +\end_inset + + + + +\begin_inset Text + +\layout Standard + +P +\end_inset + + +\begin_inset Text + +\layout Standard + +!spec999 +\end_inset + + + + +\begin_inset Text + +\layout Standard + +C +\end_inset + + +\begin_inset Text + +\layout Standard + +!time +\end_inset + + + + +\end_inset + + +\layout Standard + +The following table shows the flags for RIGHTS: +\layout Standard + + +\begin_inset Tabular + + + + + + +\begin_inset Text + +\layout Standard + +FLAG +\end_inset + + +\begin_inset Text + +\layout Standard + +RIGHT +\end_inset + + + + +\begin_inset Text + +\layout Standard + +1 +\end_inset + + +\begin_inset Text + +\layout Standard + +cannot be vote kicked +\end_inset + + + + +\begin_inset Text + +\layout Standard + +4 +\end_inset + + +\begin_inset Text + +\layout Standard + +can see team chat as a spectator +\end_inset + + + + +\begin_inset Text + +\layout Standard + +5 +\end_inset + + +\begin_inset Text + +\layout Standard + +can switch teams regardless of balance settings +\end_inset + + + + +\begin_inset Text + +\layout Standard + +6 +\end_inset + + +\begin_inset Text + +\layout Standard + +does not need to specify a reason for kick/ban +\end_inset + + + + +\begin_inset Text + +\layout Standard + +7 +\end_inset + + +\begin_inset Text + +\layout Standard + +can call a vote at any time regardless of g_voteLimit +\end_inset + + + + +\begin_inset Text + +\layout Standard + +8 +\end_inset + + +\begin_inset Text + +\layout Standard + +does not need to specify a duration for a ban +\end_inset + + + + +\begin_inset Text + +\layout Standard + +9 +\end_inset + + +\begin_inset Text + +\layout Standard + +can run commands in team chat +\end_inset + + + + +\begin_inset Text + +\layout Standard + +0 +\end_inset + + +\begin_inset Text + +\layout Standard + +inactivity settings do not apply +\end_inset + + + + +\begin_inset Text + +\layout Standard + +! +\end_inset + + +\begin_inset Text + +\layout Standard + +no ! commands can be used on a player with this flag +\end_inset + + + + +\begin_inset Text + +\layout Standard + +@ +\end_inset + + +\begin_inset Text + +\layout Standard + +does not show up as an admin in the output of listplayers +\end_inset + + + + +\end_inset + + +\layout Standard + +In addition, there are 3 special case characters in the flags string: +\layout Standard + + +\begin_inset Tabular + + + + + + +\begin_inset Text + +\layout Standard + +FLAG +\end_inset + + +\begin_inset Text + +\layout Standard + +MEANING +\end_inset + + + + +\begin_inset Text + +\layout Standard + +* +\end_inset + + +\begin_inset Text + +\layout Standard + +signifies ALL commands and rights any flags following this character are + negated. + The only exceptions are the ! and @ flags which must be given to individual + admins explicitly. +\end_inset + + + + +\begin_inset Text + +\layout Standard + ++ +\end_inset + + +\begin_inset Text + +\layout Standard + +any flags following this flag will be ADDED. + this is implied at the beginning of any flags string so it's pretty much + worthless. +\end_inset + + + + +\begin_inset Text + +\layout Standard + +- +\end_inset + + +\begin_inset Text + +\layout Standard + +any flags following this flag will be REMOVED. + this is particularly useful if you wish to remove a right from an admin + that has been given through that admin's [level] definition. +\end_inset + + + + +\end_inset + + \layout Section \pagebreak_top Credits diff --git a/src/game/g_admin.c b/src/game/g_admin.c new file mode 100644 index 00000000..0fb2975b --- /dev/null +++ b/src/game/g_admin.c @@ -0,0 +1,2666 @@ +/* +=========================================================================== +Copyright (C) 2004-2006 Tony J. White + +This file is part of Tremulous. + +This shrubbot implementation is the original work of Tony J. White. + +Contains contributions from Wesley van Beelen, Chris Bajumpaa, Josh Menke, +and Travis Maurer. + +The functionality of this code mimics the behaviour of the currently +inactive project shrubet (http://www.etstats.com/shrubet/index.php?ver=2) +by Ryan Mannion. However, shrubet was a closed-source project and +none of it's code has been copied, only it's functionality. + +Tremulous is free software; you can redistribute it +and/or modify it under the terms of the GNU General Public License as +published by the Free Software Foundation; either version 2 of the License, +or (at your option) any later version. + +Tremulous is distributed in the hope that it will be +useful, but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with Tremulous; if not, write to the Free Software +Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA +=========================================================================== +*/ + +#include "g_local.h" + +// big ugly global buffer for use with buffered printing of long outputs +static char g_bfb[ 32000 ]; + +// note: list ordered alphabetically +g_admin_cmd_t g_admin_cmds[ ] = + { + {"admintest", G_admin_admintest, "a", + "display your current admin level", + "" + }, + + {"allready", G_admin_allready, "y", + "makes everyone ready in intermission", + "" + }, + + {"ban", G_admin_ban, "b", + "ban a player by IP and GUID with an optional expiration time and reason." + "time is seconds or suffix with 'w' - weeks, 'd' - days, 'h' - hours, or " + "'m' - minutes", + "[^3name|slot#|IP^7] (^5time^7) (^5reason^7)" + }, + + {"cancelvote", G_admin_cancelvote, "c", + "cancel a vote taking place", + "" + }, + + {"help", G_admin_help, "h", + "display commands available to you or help on a specific command", + "(^5command^7)" + }, + + {"kick", G_admin_kick, "k", + "kick a player with an optional reason", + "(^5reason^7)" + }, + + {"listadmins", G_admin_listadmins, "D", + "display a list of all server admins and their levels", + "(^5name|start admin#^7)" + }, + + {"listplayers", G_admin_listplayers, "i", + "display a list of players, their client numbers and their levels", + "" + }, + + {"mute", G_admin_mute, "m", + "mute a player", + "[^3name|slot#^7]" + }, + + {"namelog", G_admin_namelog, "e", + "display a list of names used by recently connected players", + "(^5name^7)" + }, + + {"nextmap", G_admin_nextmap, "n", + "go to the next map in the cycle", + "" + }, + + {"passvote", G_admin_passvote, "V", + "pass a vote currently taking place", + "" + }, + + {"putteam", G_admin_putteam, "p", + "move a player to a specified team", + "[^3name|slot#^7] [^3h|a|s^7]" + }, + + {"readconfig", G_admin_readconfig, "G", + "reloads the admin config file and refreshes permission flags", + "" + }, + + {"rename", G_admin_rename, "N", + "rename a player", + "[^3name|slot#^7] [^3new name^7]" + }, + + {"restart", G_admin_restart, "r", + "restart the current map", + "" + }, + + {"setlevel", G_admin_setlevel, "s", + "sets the admin level of a player", + "[^3name|slot#|admin#^7] [^3level^7]" + }, + + {"showbans", G_admin_showbans, "B", + "display a (partial) list of active bans", + "(^5start at ban#^7)" + }, + + {"spec999", G_admin_spec999, "P", + "move 999 pingers to the spectator team", + ""}, + + {"time", G_admin_time, "C", + "show the current local server time", + ""}, + + {"unban", G_admin_unban, "b", + "unbans a player specified by the slot as seen in showbans", + "[^3ban slot#^7]" + }, + + {"unmute", G_admin_mute, "m", + "unmute a muted player", + "[^3name|slot#^7]" + } + }; + +static int adminNumCmds = sizeof( g_admin_cmds ) / sizeof( g_admin_cmds[ 0 ] ); + +static int admin_level_maxname = 0; +g_admin_level_t *g_admin_levels[ MAX_ADMIN_LEVELS ]; +g_admin_admin_t *g_admin_admins[ MAX_ADMIN_ADMINS ]; +g_admin_ban_t *g_admin_bans[ MAX_ADMIN_BANS ]; +g_admin_command_t *g_admin_commands[ MAX_ADMIN_COMMANDS ]; +g_admin_namelog_t *g_admin_namelog[ MAX_ADMIN_NAMELOGS ]; + +qboolean G_admin_permission( gentity_t *ent, char flag ) +{ + int i; + int l = 0; + char *flags; + + // console always wins + if( !ent ) + return qtrue; + + for( i = 0; i < MAX_ADMIN_ADMINS && g_admin_admins[ i ]; i++ ) + { + if( !Q_stricmp( ent->client->pers.guid, g_admin_admins[ i ]->guid ) ) + { + flags = g_admin_admins[ i ]->flags; + while( *flags ) + { + if( *flags == flag ) + return qtrue; + else if( *flags == '-' ) + { + while( *flags++ ) + { + if( *flags == flag ) + return qfalse; + else if( *flags == '+' ) + break; + } + } + else if( *flags == '*' ) + { + while( *flags++ ) + { + if( *flags == flag ) + return qfalse; + } + // flags with significance only for individuals ( + // like ADMF_INCOGNITO and ADMF_IMMUTABLE are NOT covered + // by the '*' wildcard. They must be specified manually. + switch( flag ) + { + case ADMF_INCOGNITO: + case ADMF_IMMUTABLE: + return qfalse; + default: + return qtrue; + } + } + flags++; + } + l = g_admin_admins[ i ]->level; + } + } + for( i = 0; i < MAX_ADMIN_LEVELS && g_admin_levels[ i ]; i++ ) + { + if( g_admin_levels[ i ]->level == l ) + { + flags = g_admin_levels[ i ]->flags; + while( *flags ) + { + if( *flags == flag ) + return qtrue; + if( *flags == '*' ) + { + while( *flags++ ) + { + if( *flags == flag ) + return qfalse; + } + // flags with significance only for individuals ( + // like ADMF_INCOGNITO and ADMF_IMMUTABLE are NOT covered + // by the '*' wildcard. They must be specified manually. + switch( flag ) + { + case ADMF_INCOGNITO: + case ADMF_IMMUTABLE: + return qfalse; + default: + return qtrue; + } + } + flags++; + } + } + } + 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_SanitiseName( name, name2 ); + + if( !Q_stricmp( name2, "UnnamedPlayer" ) ) + return qtrue; + + for( i = 0; i < level.maxclients; i++ ) + { + client = &level.clients[ i ]; + if( client->pers.connected != CON_CONNECTING + && client->pers.connected != CON_CONNECTED ) + { + continue; + } + + // can rename ones self to the same name using different colors + if( i == ( ent - g_entities ) ) + continue; + + G_SanitiseName( client->pers.netname, testName ); + if( !Q_stricmp( name, testName ) ) + { + Q_strncpyz( err, va( "The name '%s^7' is already in use", name ), + len ); + return qfalse; + } + } + + if( !g_admin.string[ 0 ] || !g_adminNameProtect.string[ 0 ] ) + return qtrue; + + for( i = 0; i < MAX_ADMIN_ADMINS && g_admin_admins[ i ]; i++ ) + { + G_SanitiseName( g_admin_admins[ i ]->name, testName ); + if( !Q_stricmp( name2, testName ) && + Q_stricmp( ent->client->pers.guid, g_admin_admins[ i ]->guid ) ) + { + Q_strncpyz( err, va( "The name '%s^7' belongs to an admin, " + "please use another name", name ), len ); + return qfalse; + } + } + return qtrue; +} + +static qboolean admin_higher_guid( char *admin_guid, char *victim_guid ) +{ + int i; + int alevel = 0; + + for( i = 0; i < MAX_ADMIN_ADMINS && g_admin_admins[ i ]; i++ ) + { + if( !Q_stricmp( admin_guid, g_admin_admins[ i ]->guid ) ) + { + alevel = g_admin_admins[ i ]->level; + break; + } + } + for( i = 0; i < MAX_ADMIN_ADMINS && g_admin_admins[ i ]; i++ ) + { + if( !Q_stricmp( victim_guid, g_admin_admins[ i ]->guid ) ) + { + if( alevel < g_admin_admins[ i ]->level ) + return qfalse; + if( strstr( g_admin_admins[ i ]->flags, va( "%c", ADMF_IMMUTABLE ) ) ) + return qfalse; + } + } + return qtrue; +} + +static qboolean admin_higher( gentity_t *admin, gentity_t *victim ) +{ + + // console always wins + if( !admin ) + return qtrue; + // just in case + if( !victim ) + return qtrue; + + return admin_higher_guid( admin->client->pers.guid, + victim->client->pers.guid ); +} + +static void admin_writeconfig_string( char *s, fileHandle_t f ) +{ + char buf[ MAX_STRING_CHARS ]; + + buf[ 0 ] = '\0'; + if( s[ 0 ] ) + { + //Q_strcat(buf, sizeof(buf), s); + Q_strncpyz( buf, s, sizeof( buf ) ); + trap_FS_Write( buf, strlen( buf ), f ); + } + trap_FS_Write( "\n", 1, f ); +} + +static void admin_writeconfig_int( int v, fileHandle_t f ) +{ + char buf[ 32 ]; + + Com_sprintf( buf, sizeof(buf), "%d", v ); + if( buf[ 0 ] ) + trap_FS_Write( buf, strlen( buf ), f ); + trap_FS_Write( "\n", 1, f ); +} + +static void admin_writeconfig( void ) +{ + fileHandle_t f; + int len, i, j; + qtime_t qt; + int t; + char levels[ MAX_STRING_CHARS ] = {""}; + + if( !g_admin.string[ 0 ] ) + return ; + t = trap_RealTime( &qt ); + len = trap_FS_FOpenFile( g_admin.string, &f, FS_WRITE ); + if( len < 0 ) + { + G_Printf( "admin_writeconfig: could not open %s\n", + g_admin.string ); + } + for( i = 0; i < MAX_ADMIN_LEVELS && g_admin_levels[ i ]; i++ ) + { + trap_FS_Write( "[level]\n", 8, f ); + trap_FS_Write( "level = ", 10, f ); + admin_writeconfig_int( g_admin_levels[ i ]->level, f ); + trap_FS_Write( "name = ", 10, f ); + admin_writeconfig_string( g_admin_levels[ i ]->name, f ); + trap_FS_Write( "flags = ", 10, f ); + admin_writeconfig_string( g_admin_levels[ i ]->flags, f ); + trap_FS_Write( "\n", 1, f ); + } + for( i = 0; i < MAX_ADMIN_ADMINS && g_admin_admins[ i ]; i++ ) + { + // don't write level 0 users + if( g_admin_admins[ i ]->level < 1 ) + continue; + + trap_FS_Write( "[admin]\n", 8, f ); + trap_FS_Write( "name = ", 10, f ); + admin_writeconfig_string( g_admin_admins[ i ]->name, f ); + trap_FS_Write( "guid = ", 10, f ); + admin_writeconfig_string( g_admin_admins[ i ]->guid, f ); + trap_FS_Write( "level = ", 10, f ); + admin_writeconfig_int( g_admin_admins[ i ]->level, f ); + trap_FS_Write( "flags = ", 10, f ); + admin_writeconfig_string( g_admin_admins[ i ]->flags, f ); + trap_FS_Write( "\n", 1, f ); + } + for( i = 0; i < MAX_ADMIN_BANS && g_admin_bans[ i ]; i++ ) + { + // don't write expired bans + // if expires is 0, then it's a perm ban + if( g_admin_bans[ i ]->expires != 0 && + ( g_admin_bans[ i ]->expires - t ) < 1 ) + continue; + + trap_FS_Write( "[ban]\n", 6, f ); + trap_FS_Write( "name = ", 10, f ); + admin_writeconfig_string( g_admin_bans[ i ]->name, f ); + trap_FS_Write( "guid = ", 10, f ); + admin_writeconfig_string( g_admin_bans[ i ]->guid, f ); + trap_FS_Write( "ip = ", 10, f ); + admin_writeconfig_string( g_admin_bans[ i ]->ip, f ); + trap_FS_Write( "reason = ", 10, f ); + admin_writeconfig_string( g_admin_bans[ i ]->reason, f ); + trap_FS_Write( "made = ", 10, f ); + admin_writeconfig_string( g_admin_bans[ i ]->made, f ); + trap_FS_Write( "expires = ", 10, f ); + admin_writeconfig_int( g_admin_bans[ i ]->expires, f ); + trap_FS_Write( "banner = ", 10, f ); + admin_writeconfig_string( g_admin_bans[ i ]->banner, f ); + trap_FS_Write( "\n", 1, f ); + } + for( i = 0; i < MAX_ADMIN_COMMANDS && g_admin_commands[ i ]; i++ ) + { + levels[ 0 ] = '\0'; + trap_FS_Write( "[command]\n", 10, f ); + trap_FS_Write( "command = ", 10, f ); + admin_writeconfig_string( g_admin_commands[ i ]->command, f ); + trap_FS_Write( "exec = ", 10, f ); + admin_writeconfig_string( g_admin_commands[ i ]->exec, f ); + trap_FS_Write( "desc = ", 10, f ); + admin_writeconfig_string( g_admin_commands[ i ]->desc, f ); + trap_FS_Write( "levels = ", 10, f ); + for( j = 0; g_admin_commands[ i ]->levels[ j ] != -1; j++ ) + { + Q_strcat( levels, sizeof( levels ), + va( "%i ", g_admin_commands[ i ]->levels[ j ] ) ); + } + admin_writeconfig_string( levels, f ); + trap_FS_Write( "\n", 1, f ); + } + trap_FS_FCloseFile( f ); +} + +static void admin_readconfig_string( char **cnf, char *s, int size ) +{ + char * t; + + //COM_MatchToken(cnf, "="); + t = COM_ParseExt( cnf, qfalse ); + if( !strcmp( t, "=" ) ) + { + t = COM_ParseExt( cnf, qfalse ); + } + else + { + G_Printf( "readconfig: warning missing = before " + "\"%s\" on line %d\n", + t, + COM_GetCurrentParseLine() ); + } + s[ 0 ] = '\0'; + while( t[ 0 ] ) + { + if( ( s[ 0 ] == '\0' && strlen( t ) <= size ) + || ( strlen( t ) + strlen( s ) < size ) ) + { + + Q_strcat( s, size, t ); + Q_strcat( s, size, " " ); + } + t = COM_ParseExt( cnf, qfalse ); + } + // trim the trailing space + if( strlen( s ) > 0 && s[ strlen( s ) - 1 ] == ' ' ) + s[ strlen( s ) - 1 ] = '\0'; +} + +static void admin_readconfig_int( char **cnf, int *v ) +{ + char * t; + + //COM_MatchToken(cnf, "="); + t = COM_ParseExt( cnf, qfalse ); + if( !strcmp( t, "=" ) ) + { + t = COM_ParseExt( cnf, qfalse ); + } + else + { + G_Printf( "readconfig: warning missing = before " + "\"%s\" on line %d\n", + t, + COM_GetCurrentParseLine() ); + } + *v = atoi( t ); +} + +// if we can't parse any levels from readconfig, set up default +// ones to make new installs easier for admins +static void admin_default_levels( void ) +{ + g_admin_level_t * l; + int i; + + for( i = 0; i < MAX_ADMIN_LEVELS && g_admin_levels[ i ]; i++ ) + { + G_Free( g_admin_levels[ i ] ); + g_admin_levels[ i ] = NULL; + } + for( i = 0; i <= 5; i++ ) + { + l = G_Alloc( sizeof( g_admin_level_t ) ); + l->level = i; + *l->name = '\0'; + *l->flags = '\0'; + g_admin_levels[ i ] = l; + } + Q_strncpyz( g_admin_levels[ 0 ]->name, "^4Unknown Player", + sizeof( l->name ) ); + Q_strncpyz( g_admin_levels[ 0 ]->flags, "iahC", sizeof( l->flags ) ); + + Q_strncpyz( g_admin_levels[ 1 ]->name, "^5Server Regular", + sizeof( l->name ) ); + Q_strncpyz( g_admin_levels[ 1 ]->flags, "iahC", sizeof( l->flags ) ); + + Q_strncpyz( g_admin_levels[ 2 ]->name, "^6Team Manager", + sizeof( l->name ) ); + Q_strncpyz( g_admin_levels[ 2 ]->flags, "iahCpP", sizeof( l->flags ) ); + + Q_strncpyz( g_admin_levels[ 3 ]->name, "^2Junior Admin", + sizeof( l->name ) ); + Q_strncpyz( g_admin_levels[ 3 ]->flags, "iahCpPkm", sizeof( l->flags ) ); + + Q_strncpyz( g_admin_levels[ 4 ]->name, "^3Senior Admin", + sizeof( l->name ) ); + Q_strncpyz( g_admin_levels[ 4 ]->flags, "iahCpPkmBbe", sizeof( l->flags ) ); + + Q_strncpyz( g_admin_levels[ 5 ]->name, "^1Server Operator", + sizeof( l->name ) ); + Q_strncpyz( g_admin_levels[ 5 ]->flags, "*", sizeof( l->flags ) ); +} + +// return a level for a player entity. +int G_admin_level( gentity_t *ent ) +{ + int i; + qboolean found = qfalse; + + if( !ent ) + { + return MAX_ADMIN_LEVELS; + } + + for( i = 0; i < MAX_ADMIN_ADMINS && g_admin_admins[ i ]; i++ ) + { + if( !Q_stricmp( g_admin_admins[ i ]->guid, ent->client->pers.guid ) ) + { + + found = qtrue; + break; + } + } + + if( found ) + { + return g_admin_admins[ i ]->level; + } + + return 0; +} + +static qboolean admin_command_permission( gentity_t *ent, char *command ) +{ + int i, j; + int level; + + if( !ent ) + return qtrue; + level = ent->client->pers.adminLevel; + for( i = 0; i < MAX_ADMIN_COMMANDS && g_admin_commands[ i ]; i++ ) + { + if( !Q_stricmp( command, g_admin_commands[ i ]->command ) ) + { + for( j = 0; g_admin_commands[ i ]->levels[ j ] != -1; j++ ) + { + if( g_admin_commands[ i ]->levels[ j ] == level ) + { + return qtrue; + } + } + } + } + return qfalse; +} + +static void admin_log( gentity_t *admin, char *cmd, int skiparg ) +{ + fileHandle_t f; + int len, i, j; + char string[ MAX_STRING_CHARS ]; + int min, tens, sec; + g_admin_admin_t *a; + g_admin_level_t *l; + char flags[ MAX_ADMIN_FLAGS * 2 ]; + gentity_t *victim = NULL; + int pids[ MAX_CLIENTS ]; + char name[ MAX_NAME_LENGTH ]; + + if( !g_adminLog.string[ 0 ] ) + return ; + + + len = trap_FS_FOpenFile( g_adminLog.string, &f, FS_APPEND ); + if( len < 0 ) + { + G_Printf( "admin_log: error could not open %s\n", g_adminLog.string ); + return ; + } + + sec = level.time / 1000; + min = sec / 60; + sec -= min * 60; + tens = sec / 10; + sec -= tens * 10; + + *flags = '\0'; + if( admin ) + { + for( i = 0; i < MAX_ADMIN_ADMINS && g_admin_admins[ i ]; i++ ) + { + if( !Q_stricmp( g_admin_admins[ i ]->guid , admin->client->pers.guid ) ) + { + + a = g_admin_admins[ i ]; + Q_strncpyz( flags, a->flags, sizeof( flags ) ); + for( j = 0; j < MAX_ADMIN_LEVELS && g_admin_levels[ j ]; j++ ) + { + if( g_admin_levels[ j ]->level == a->level ) + { + l = g_admin_levels[ j ]; + Q_strcat( flags, sizeof( flags ), l->flags ); + break; + } + } + break; + } + } + } + + if( G_SayArgc() > 1 + skiparg ) + { + G_SayArgv( 1 + skiparg, name, sizeof( name ) ); + if( G_ClientNumbersFromString( name, pids ) == 1 ) + { + victim = &g_entities[ pids[ 0 ] ]; + } + } + + if( victim && Q_stricmp( cmd, "attempted" ) ) + { + Com_sprintf( string, sizeof( string ), + "%3i:%i%i: %i: %s: %s: %s: %s: %s: %s: \"%s\"\n", + min, + tens, + sec, + ( admin ) ? admin->s.clientNum : -1, + ( admin ) ? admin->client->pers.guid + : "XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX", + ( admin ) ? admin->client->pers.netname : "console", + flags, + cmd, + victim->client->pers.guid, + victim->client->pers.netname, + G_SayConcatArgs( 2 + skiparg ) ); + } + else + { + Com_sprintf( string, sizeof( string ), + "%3i:%i%i: %i: %s: %s: %s: %s: \"%s\"\n", + min, + tens, + sec, + ( admin ) ? admin->s.clientNum : -1, + ( admin ) ? admin->client->pers.guid + : "XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX", + ( admin ) ? admin->client->pers.netname : "console", + flags, + cmd, + G_SayConcatArgs( 1 + skiparg ) ); + } + trap_FS_Write( string, strlen( string ), f ); + trap_FS_FCloseFile( f ); +} + +static int admin_listadmins( gentity_t *ent, int start, char *search ) +{ + int drawn = 0; + char guid_stub[9]; + char name[ MAX_NAME_LENGTH ] = {""}; + char name2[ MAX_NAME_LENGTH ] = {""}; + char lname[ MAX_NAME_LENGTH ] = {""}; + char lname_fmt[ 5 ]; + int i,j; + gentity_t *vic; + int l = 0; + qboolean dup = qfalse; + + ADMBP_begin(); + + // print out all connected players regardless of level if name searching + for( i = 0; i < level.maxclients && search[ 0 ]; i++ ) + { + vic = &g_entities[ i ]; + + if( vic->client && vic->client->pers.connected != CON_CONNECTED ) + continue; + + l = vic->client->pers.adminLevel; + + G_SanitiseName( vic->client->pers.netname, name ); + if( !strstr( name, search ) ) + continue; + + for( j = 0; j <= 8; j++ ) + guid_stub[ j ] = vic->client->pers.guid[ j + 24 ]; + guid_stub[ j ] = '\0'; + + lname[ 0 ] = '\0'; + Q_strncpyz( lname_fmt, "%s", sizeof( lname_fmt ) ); + for( j = 0; j < MAX_ADMIN_LEVELS && g_admin_levels[ j ]; j++ ) + { + if( g_admin_levels[ j ]->level == l ) + { + G_DecolorString( g_admin_levels[ j ]->name, lname ); + Com_sprintf( lname_fmt, sizeof( lname_fmt ), "%%%is", + ( admin_level_maxname + strlen( g_admin_levels[ j ]->name ) + - strlen( lname ) ) ); + Com_sprintf( lname, sizeof( lname ), lname_fmt, + g_admin_levels[ j ]->name ); + break; + } + } + ADMBP( va( "%4i %4i %s^7 (*%s) %s^7\n", + i, + l, + lname, + guid_stub, + vic->client->pers.netname ) ); + drawn++; + } + + for( i = start; i < MAX_ADMIN_ADMINS && g_admin_admins[ i ] + && drawn < MAX_ADMIN_LISTITEMS; i++ ) + { + if( search[ 0 ] ) + { + G_SanitiseName( g_admin_admins[ i ]->name, name ); + if( !strstr( name, search ) ) + continue; + + // verify we don't have the same guid/name pair in connected players + // since we don't want to draw the same player twice + dup = qfalse; + for( j = 0; j < level.maxclients; j++ ) + { + vic = &g_entities[ j ]; + if( !vic->client || vic->client->pers.connected != CON_CONNECTED ) + continue; + G_SanitiseName( vic->client->pers.netname, name2 ); + if( !Q_stricmp( vic->client->pers.guid, g_admin_admins[ i ]->guid ) + && strstr( name2, search ) ) + { + dup = qtrue; + break; + } + } + if( dup ) + continue; + } + for( j = 0; j <= 8; j++ ) + guid_stub[ j ] = g_admin_admins[ i ]->guid[ j + 24 ]; + guid_stub[ j ] = '\0'; + + lname[ 0 ] = '\0'; + Q_strncpyz( lname_fmt, "%s", sizeof( lname_fmt ) ); + for( j = 0; j < MAX_ADMIN_LEVELS && g_admin_levels[ j ]; j++ ) + { + if( g_admin_levels[ j ]->level == g_admin_admins[ i ]->level ) + { + G_DecolorString( g_admin_levels[ j ]->name, lname ); + Com_sprintf( lname_fmt, sizeof( lname_fmt ), "%%%is", + ( admin_level_maxname + strlen( g_admin_levels[ j ]->name ) + - strlen( lname ) ) ); + Com_sprintf( lname, sizeof( lname ), lname_fmt, + g_admin_levels[ j ]->name ); + break; + } + } + ADMBP( va( "%4i %4i %s^7 (*%s) %s^7\n", + ( i + MAX_CLIENTS ), + g_admin_admins[ i ]->level, + lname, + guid_stub, + g_admin_admins[ i ]->name ) ); + drawn++; + } + ADMBP_end(); + return drawn; +} + +void G_admin_duration( int secs, char *duration, int dursize ) +{ + + if( secs > ( 60 * 60 * 24 * 365 * 50 ) || secs < 0 ) + Q_strncpyz( duration, "PERMANENT", dursize ); + else if( secs >= ( 60 * 60 * 24 * 365 ) ) + Com_sprintf( duration, dursize, "%1.1f years", + ( secs / ( 60 * 60 * 24 * 365.0f ) ) ); + else if( secs >= ( 60 * 60 * 24 * 90 ) ) + Com_sprintf( duration, dursize, "%1.1f weeks", + ( secs / ( 60 * 60 * 24 * 7.0f ) ) ); + else if( secs >= ( 60 * 60 * 24 ) ) + Com_sprintf( duration, dursize, "%1.1f days", + ( secs / ( 60 * 60 * 24.0f ) ) ); + else if( secs >= ( 60 * 60 ) ) + Com_sprintf( duration, dursize, "%1.1f hours", + ( secs / ( 60 * 60.0f ) ) ); + else if( secs >= 60 ) + Com_sprintf( duration, dursize, "%1.1f minutes", + ( secs / 60.0f ) ); + else + Com_sprintf( duration, dursize, "%i seconds", secs ); +} + +qboolean G_admin_ban_check( char *userinfo, char *reason, int rlen ) +{ + char *guid, *ip; + int i; + qtime_t qt; + int t; + + *reason = '\0'; + t = trap_RealTime( &qt ); + if( !*userinfo ) + return qfalse; + ip = Info_ValueForKey( userinfo, "ip" ); + if( !*ip ) + return qfalse; + guid = Info_ValueForKey( userinfo, "cl_guid" ); + for( i = 0; i < MAX_ADMIN_BANS && g_admin_bans[ i ]; i++ ) + { + // 0 is for perm ban + if( g_admin_bans[ i ]->expires != 0 && + ( g_admin_bans[ i ]->expires - t ) < 1 ) + continue; + if( strstr( ip, g_admin_bans[ i ]->ip ) ) + { + char duration[ 32 ]; + G_admin_duration( ( g_admin_bans[ i ]->expires - t ), + duration, sizeof( duration ) ); + Com_sprintf( + reason, + rlen, + "You have been banned by %s^7 reason: %s^7 expires: %s", + g_admin_bans[ i ]->banner, + g_admin_bans[ i ]->reason, + duration + ); + G_Printf("Banned player tried to connect from IP %s\n", ip); + return qtrue; + } + if( *guid && !Q_stricmp( g_admin_bans[ i ]->guid, guid ) ) + { + char duration[ 32 ]; + G_admin_duration( ( g_admin_bans[ i ]->expires - t ), + duration, sizeof( duration ) ); + Com_sprintf( + reason, + rlen, + "You have been banned by %s^7 reason: %s^7 expires: %s", + g_admin_bans[ i ]->banner, + g_admin_bans[ i ]->reason, + duration + ); + G_Printf("Banned player tried to connect with GUID %s\n", guid); + return qtrue; + } + } + return qfalse; +} + +qboolean G_admin_cmd_check( gentity_t *ent, qboolean say ) +{ + int i; + char command[ MAX_ADMIN_CMD_LEN ]; + char *cmd; + int skip = 0; + + if( g_admin.string[ 0 ] == '\0' ) + return qfalse; + + command[ 0 ] = '\0'; + G_SayArgv( 0, command, sizeof( command ) ); + if( !Q_stricmp( command, "say" ) || + ( G_admin_permission( ent, ADMF_TEAMFTCMD ) && + ( !Q_stricmp( command, "say_team" ) || + !Q_stricmp( command, "say_buddy" ) ) ) ) + { + skip = 1; + G_SayArgv( 1, command, sizeof( command ) ); + } + if( !command[ 0 ] ) + return qfalse; + + if( command[ 0 ] == '!' ) + { + cmd = &command[ 1 ]; + } + else + { + return qfalse; + } + + for( i = 0; i < MAX_ADMIN_COMMANDS && g_admin_commands[ i ]; i++ ) + { + if( Q_stricmp( cmd, g_admin_commands[ i ]->command ) ) + continue; + + if( admin_command_permission( ent, cmd ) ) + { + trap_SendConsoleCommand( EXEC_APPEND, g_admin_commands[ i ]->exec ); + admin_log( ent, cmd, skip ); + return qtrue; + } + else + { + ADMP( va( "^3!%s: ^7permission denied\n", g_admin_commands[ i ]->command ) ); + admin_log( ent, "attempted", skip - 1 ); + return qfalse; + } + } + + for( i = 0; i < adminNumCmds; i++ ) + { + if( Q_stricmp( cmd, g_admin_cmds[ i ].keyword ) ) + continue; + if( G_admin_permission( ent, g_admin_cmds[ i ].flag[ 0 ] ) ) + { + g_admin_cmds[ i ].handler( ent, skip ); + admin_log( ent, cmd, skip ); + return qtrue; + } + else + { + ADMP( va( "^3!%s: ^7permission denied\n", g_admin_cmds[ i ].keyword ) ); + admin_log( ent, "attempted", skip - 1 ); + } + } + return qfalse; +} + +void G_admin_namelog_cleanup( ) +{ + int i; + + for( i = 0; i < MAX_ADMIN_NAMELOGS && g_admin_namelog[ i ]; i++ ) + { + G_Free( g_admin_namelog[ i ] ); + g_admin_namelog[ i ] = NULL; + } +} + +void G_admin_namelog_update( gclient_t *client, int clientNum ) +{ + int i, j; + g_admin_namelog_t *namelog; + char n1[ MAX_NAME_LENGTH ]; + char n2[ MAX_NAME_LENGTH ]; + + if( !g_admin.string[0] ) + return; + G_SanitiseName( client->pers.netname, n1 ); + for( i = 0; i < MAX_ADMIN_NAMELOGS && g_admin_namelog[ i ]; i++ ) + { + if( !Q_stricmp( client->pers.ip, g_admin_namelog[ i ]->ip ) + && !Q_stricmp( client->pers.guid, g_admin_namelog[ i ]->guid ) ) + { + for( j = 0; j < MAX_ADMIN_NAMELOG_NAMES + && g_admin_namelog[ i ]->name[ j ][ 0 ]; j++ ) + { + G_SanitiseName( g_admin_namelog[ i ]->name[ j ], n2 ); + if( !Q_stricmp( n1, n2 ) ) + break; + } + if( j == MAX_ADMIN_NAMELOG_NAMES ) + j = MAX_ADMIN_NAMELOG_NAMES - 1; + Q_strncpyz( g_admin_namelog[ i ]->name[ j ], client->pers.netname, + sizeof( g_admin_namelog[ i ]->name[ j ] ) ); + g_admin_namelog[ i ]->slot = clientNum; + return; + } + } + if( i >= MAX_ADMIN_NAMELOGS ) + { + G_Printf( "G_admin_namelog_update: warning, g_admin_namelogs overflow\n" ); + return; + } + namelog = G_Alloc( sizeof( g_admin_namelog_t ) ); + memset( namelog, 0, sizeof( namelog ) ); + for( j = 0; j < MAX_ADMIN_NAMELOG_NAMES ; j++ ) + namelog->name[ j ][ 0 ] = '\0'; + Q_strncpyz( namelog->ip, client->pers.ip, sizeof( namelog->ip ) ); + Q_strncpyz( namelog->guid, client->pers.guid, sizeof( namelog->guid ) ); + Q_strncpyz( namelog->name[ 0 ], client->pers.netname, + sizeof( namelog->name[ 0 ] ) ); + namelog->slot = clientNum; + g_admin_namelog[ i ] = namelog; +} + +qboolean G_admin_readconfig( gentity_t *ent, int skiparg ) +{ + g_admin_level_t * l = NULL; + g_admin_admin_t *a = NULL; + g_admin_ban_t *b = NULL; + g_admin_command_t *c = NULL; + int lc = 0, ac = 0, bc = 0, cc = 0; + fileHandle_t f; + int len; + char *cnf, *cnf2; + char *t; + qboolean level_open, admin_open, ban_open, command_open; + char levels[ MAX_STRING_CHARS ] = {""}; + + if( !g_admin.string[ 0 ] ) + return qfalse; + len = trap_FS_FOpenFile( g_admin.string, &f, FS_READ ) ; + if( len < 0 ) + { + ADMP( va( "^3!readconfig: ^7could not open admin config file %s\n", + g_admin.string ) ); + admin_default_levels(); + return qfalse; + } + cnf = G_Alloc( len + 1 ); + cnf2 = cnf; + trap_FS_Read( cnf, len, f ); + *( cnf + len ) = '\0'; + trap_FS_FCloseFile( f ); + + G_admin_cleanup(); + + t = COM_Parse( &cnf ); + level_open = admin_open = ban_open = command_open = qfalse; + while( *t ) + { + if( !Q_stricmp( t, "[level]" ) || + !Q_stricmp( t, "[admin]" ) || + !Q_stricmp( t, "[ban]" ) || + !Q_stricmp( t, "[command]" ) ) + { + + if( level_open ) + g_admin_levels[ lc++ ] = l; + else if( admin_open ) + g_admin_admins[ ac++ ] = a; + else if( ban_open ) + g_admin_bans[ bc++ ] = b; + else if( command_open ) + g_admin_commands[ cc++ ] = c; + level_open = admin_open = + ban_open = command_open = qfalse; + } + + if( level_open ) + { + if( !Q_stricmp( t, "level" ) ) + { + admin_readconfig_int( &cnf, &l->level ); + } + else if( !Q_stricmp( t, "name" ) ) + { + admin_readconfig_string( &cnf, l->name, sizeof( l->name ) ); + } + else if( !Q_stricmp( t, "flags" ) ) + { + admin_readconfig_string( &cnf, l->flags, sizeof( l->flags ) ); + } + else + { + ADMP( va( "^3!readconfig: ^7[level] parse error near %s on line %d\n", + t, + COM_GetCurrentParseLine() ) ); + } + } + else if( admin_open ) + { + if( !Q_stricmp( t, "name" ) ) + { + admin_readconfig_string( &cnf, a->name, sizeof( a->name ) ); + } + else if( !Q_stricmp( t, "guid" ) ) + { + admin_readconfig_string( &cnf, a->guid, sizeof( a->guid ) ); + } + else if( !Q_stricmp( t, "level" ) ) + { + admin_readconfig_int( &cnf, &a->level ); + } + else if( !Q_stricmp( t, "flags" ) ) + { + admin_readconfig_string( &cnf, a->flags, sizeof( a->flags ) ); + } + else + { + ADMP( va( "^3!readconfig: ^7[admin] parse error near %s on line %d\n", + t, + COM_GetCurrentParseLine() ) ); + } + + } + else if( ban_open ) + { + if( !Q_stricmp( t, "name" ) ) + { + admin_readconfig_string( &cnf, b->name, sizeof( b->name ) ); + } + else if( !Q_stricmp( t, "guid" ) ) + { + admin_readconfig_string( &cnf, b->guid, sizeof( b->guid ) ); + } + else if( !Q_stricmp( t, "ip" ) ) + { + admin_readconfig_string( &cnf, b->ip, sizeof( b->ip ) ); + } + else if( !Q_stricmp( t, "reason" ) ) + { + admin_readconfig_string( &cnf, b->reason, sizeof( b->reason ) ); + } + else if( !Q_stricmp( t, "made" ) ) + { + admin_readconfig_string( &cnf, b->made, sizeof( b->made ) ); + } + else if( !Q_stricmp( t, "expires" ) ) + { + admin_readconfig_int( &cnf, &b->expires ); + } + else if( !Q_stricmp( t, "banner" ) ) + { + admin_readconfig_string( &cnf, b->banner, sizeof( b->banner ) ); + } + else + { + ADMP( va( "^3!readconfig: ^7[ban] parse error near %s on line %d\n", + t, + COM_GetCurrentParseLine() ) ); + } + } + else if( command_open ) + { + if( !Q_stricmp( t, "command" ) ) + { + admin_readconfig_string( &cnf, c->command, sizeof( c->command ) ); + } + else if( !Q_stricmp( t, "exec" ) ) + { + admin_readconfig_string( &cnf, c->exec, sizeof( c->exec ) ); + } + else if( !Q_stricmp( t, "desc" ) ) + { + admin_readconfig_string( &cnf, c->desc, sizeof( c->desc ) ); + } + else if( !Q_stricmp( t, "levels" ) ) + { + char level[ 4 ] = {""}; + char *lp = levels; + int cmdlevel = 0; + + admin_readconfig_string( &cnf, levels, sizeof( levels ) ); + while( *lp ) + { + if( *lp == ' ' ) + { + c->levels[ cmdlevel++ ] = atoi( level ); + level[ 0 ] = '\0'; + lp++; + continue; + } + Q_strcat( level, sizeof( level ), va( "%c", *lp ) ); + lp++; + } + if( level[ 0 ] ) + c->levels[ cmdlevel++ ] = atoi( level ); + // ensure the list is -1 terminated + c->levels[ MAX_ADMIN_LEVELS ] = -1; + } + else + { + ADMP( va( "^3!readconfig: ^7[command] parse error near %s on line %d\n", + t, + COM_GetCurrentParseLine() ) ); + } + } + + if( !Q_stricmp( t, "[level]" ) ) + { + if( lc >= MAX_ADMIN_LEVELS ) + return qfalse; + l = G_Alloc( sizeof( g_admin_level_t ) ); + l->level = 0; + *l->name = '\0'; + *l->flags = '\0'; + level_open = qtrue; + } + else if( !Q_stricmp( t, "[admin]" ) ) + { + if( ac >= MAX_ADMIN_ADMINS ) + return qfalse; + a = G_Alloc( sizeof( g_admin_admin_t ) ); + *a->name = '\0'; + *a->guid = '\0'; + a->level = 0; + *a->flags = '\0'; + admin_open = qtrue; + } + else if( !Q_stricmp( t, "[ban]" ) ) + { + if( bc >= MAX_ADMIN_BANS ) + return qfalse; + b = G_Alloc( sizeof( g_admin_ban_t ) ); + *b->name = '\0'; + *b->guid = '\0'; + *b->ip = '\0'; + *b->made = '\0'; + b->expires = 0; + *b->reason = '\0'; + ban_open = qtrue; + } + else if( !Q_stricmp( t, "[command]" ) ) + { + if( bc >= MAX_ADMIN_COMMANDS ) + return qfalse; + c = G_Alloc( sizeof( g_admin_command_t ) ); + *c->command = '\0'; + *c->exec = '\0'; + *c->desc = '\0'; + memset( c->levels, -1, sizeof( c->levels ) ); + command_open = qtrue; + } + t = COM_Parse( &cnf ); + } + if( level_open ) + { + + g_admin_levels[ lc++ ] = l; + } + if( admin_open ) + g_admin_admins[ ac++ ] = a; + if( ban_open ) + g_admin_bans[ bc++ ] = b; + if( command_open ) + g_admin_commands[ cc++ ] = c; + G_Free( cnf2 ); + ADMP( va( "^3!readconfig: ^7loaded %d levels, %d admins, %d bans, %d commands\n", + lc, ac, bc, cc ) ); + if( lc == 0 ) + admin_default_levels(); + else + { + char n[ MAX_NAME_LENGTH ] = {""}; + int i = 0; + + // max printable name length for formatting + for( i = 0; i < MAX_ADMIN_LEVELS && g_admin_levels[ i ]; i++ ) + { + G_DecolorString( l->name, n ); + if( strlen( n ) > admin_level_maxname ) + admin_level_maxname = strlen( n ); + } + } + return qtrue; +} + +qboolean G_admin_time( gentity_t *ent, int skiparg ) +{ + qtime_t qt; + int t; + + t = trap_RealTime( &qt ); + AP( va( "print \"^3!time: ^7local time is %02i:%02i:%02i\n\"", + qt.tm_hour, qt.tm_min, qt.tm_sec ) ); + return qtrue; +} + +qboolean G_admin_setlevel( gentity_t *ent, int skiparg ) +{ + char name[ MAX_NAME_LENGTH ] = {""}; + char lstr[ 11 ]; // 10 is max strlen() for 32-bit int + char adminname[ MAX_NAME_LENGTH ] = {""}; + char testname[ MAX_NAME_LENGTH ] = {""}; + char testname2[ MAX_NAME_LENGTH ] = {""}; + char guid[ 33 ]; + int l, i, j; + gentity_t *vic = NULL; + qboolean updated = qfalse; + g_admin_admin_t *a; + qboolean found = qfalse; + qboolean numeric = qtrue; + int matches = 0; + int id = -1; + + + if( G_SayArgc() < 3 + skiparg ) + { + ADMP( "^3!setlevel: ^7usage: setlevel [name|slot#] [level]\n" ); + return qfalse; + } + G_SayArgv( 1 + skiparg, testname, sizeof( testname ) ); + G_SayArgv( 2 + skiparg, lstr, sizeof( lstr ) ); + l = atoi( lstr ); + G_SanitiseName( testname, name ); + for( i = 0; i < sizeof( name ) && name[ i ] ; i++ ) + { + if( name[ i ] < '0' || name[ i ] > '9' ) + { + numeric = qfalse; + break; + } + } + if( numeric ) + id = atoi( name ); + + if( ent && l > ent->client->pers.adminLevel ) + { + ADMP( "^3!setlevel: ^7you may not use !setlevel to set a level higher " + "than your current level\n" ); + return qfalse; + } + + // if admin is activated for the first time on a running server, we need + // to ensure at least the default levels get created + if( !ent && !g_admin_levels[ 0 ] ) + G_admin_readconfig(NULL, 0); + + for( i = 0; i < MAX_ADMIN_LEVELS && g_admin_levels[ i ]; i++ ) + { + if( g_admin_levels[ i ]->level == l ) + { + found = qtrue; + break; + } + } + if( !found ) + { + ADMP( "^3!setlevel: ^7level is not defined\n" ); + return qfalse; + } + + if( numeric && id >= 0 && id < level.maxclients ) + vic = &g_entities[ id ]; + + if( vic && vic->client && vic->client->pers.connected == CON_CONNECTED ) + { + vic = &g_entities[ id ]; + Q_strncpyz( adminname, vic->client->pers.netname, sizeof( adminname ) ); + Q_strncpyz( guid, vic->client->pers.guid, sizeof( guid ) ); + matches = 1; + } + else if( numeric && id >= MAX_CLIENTS && id < MAX_CLIENTS + MAX_ADMIN_ADMINS + && g_admin_admins[ id - MAX_CLIENTS ] ) + { + Q_strncpyz( adminname, g_admin_admins[ id - MAX_CLIENTS ]->name, + sizeof( adminname ) ); + Q_strncpyz( guid, g_admin_admins[ id - MAX_CLIENTS ]->guid, + sizeof( guid ) ); + matches = 1; + } + else + { + for( i = 0; i < level.maxclients && matches < 2; i++ ) + { + vic = &g_entities[ i ]; + if( !vic->client || vic->client->pers.connected != CON_CONNECTED ) + continue; + G_SanitiseName( vic->client->pers.netname, testname ); + if( strstr( testname, name ) ) + { + matches++; + Q_strncpyz( adminname, vic->client->pers.netname, sizeof( adminname ) ); + Q_strncpyz( guid, vic->client->pers.guid, sizeof( guid ) ); + } + } + for( i = 0; i < MAX_ADMIN_ADMINS && g_admin_admins[ i ] && matches < 2; i++) + { + G_SanitiseName( g_admin_admins[ i ]->name, testname ); + if( strstr( testname, name ) ) + { + qboolean dup = qfalse; + + // verify we don't have the same guid/name pair in connected players + for( j = 0; j < level.maxclients; j++ ) + { + vic = &g_entities[ j ]; + if( !vic->client || vic->client->pers.connected != CON_CONNECTED ) + continue; + G_SanitiseName( vic->client->pers.netname, testname2 ); + if( !Q_stricmp( vic->client->pers.guid, g_admin_admins[ i ]->guid ) + && strstr( testname2, name ) ) + { + dup = qtrue; + break; + } + } + if( dup ) + continue; + Q_strncpyz( adminname, g_admin_admins[ i ]->name, sizeof( adminname ) ); + Q_strncpyz( guid, g_admin_admins[ i ]->guid, sizeof( guid ) ); + matches++; + } + } + } + + if( matches == 0 ) + { + ADMP( "^3!setlevel:^7 no match. use !listplayers or !listadmins to " + "find an appropriate number to use instead of name.\n" ); + return qfalse; + } + else if( matches > 1 ) + { + ADMP( "^3!setlevel:^7 more than one match. Use the admin number " + "instead:\n" ); + admin_listadmins( ent, 0, name ); + return qfalse; + } + + if( !Q_stricmp( guid, "XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX" ) ) + { + ADMP( va( "^3!setlevel: ^7%s does not have a valid GUID\n", adminname ) ); + return qfalse; + } + if( ent && !admin_higher_guid( ent->client->pers.guid, guid ) ) + { + ADMP( "^3!setlevel: ^7sorry, but your intended victim has a higher" + " admin level than you\n" ); + return qfalse; + } + + for( i = 0; i < MAX_ADMIN_ADMINS && g_admin_admins[ i ];i++ ) + { + if( !Q_stricmp( g_admin_admins[ i ]->guid, guid ) ) + { + g_admin_admins[ i ]->level = l; + Q_strncpyz( g_admin_admins[ i ]->name, adminname, + sizeof( g_admin_admins[ i ]->name ) ); + updated = qtrue; + } + } + if( !updated ) + { + if( i == MAX_ADMIN_ADMINS ) + { + ADMP( "^3!setlevel: ^7too many admins\n" ); + return qfalse; + } + a = G_Alloc( sizeof( g_admin_admin_t ) ); + a->level = l; + Q_strncpyz( a->name, adminname, sizeof( a->name ) ); + Q_strncpyz( a->guid, guid, sizeof( a->guid ) ); + *a->flags = '\0'; + g_admin_admins[ i ] = a; + } + + AP( va( + "print \"^3!setlevel: ^7%s^7 was given level %d admin rights by %s\n\"", + adminname, l, ( ent ) ? ent->client->pers.netname : "console" ) ); + if( vic ) + vic->client->pers.adminLevel = l; + admin_writeconfig(); + return qtrue; +} + +static qboolean admin_create_ban( gentity_t *ent, + char *netname, + char *guid, + char *ip, + int seconds, + char *reason ) +{ + g_admin_ban_t *b = NULL; + qtime_t qt; + int t; + int i; + + t = trap_RealTime( &qt ); + b = G_Alloc( sizeof( g_admin_ban_t ) ); + + if( !b ) + return qfalse; + + Q_strncpyz( b->name, netname, sizeof( b->name ) ); + Q_strncpyz( b->guid, guid, sizeof( b->guid ) ); + Q_strncpyz( b->ip, ip, sizeof( b->ip ) ); + + //strftime( b->made, sizeof( b->made ), "%m/%d/%y %H:%M:%S", lt ); + Q_strncpyz( b->made, va( "%02i/%02i/%02i %02i:%02i:%02i", + (qt.tm_mon + 1), qt.tm_mday, (qt.tm_year - 100), + qt.tm_hour, qt.tm_min, qt.tm_sec ), + sizeof( b->made ) ); + + if( ent ) + Q_strncpyz( b->banner, ent->client->pers.netname, sizeof( b->banner ) ); + else + Q_strncpyz( b->banner, "console", 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 ) ); + for( i = 0; i < MAX_ADMIN_BANS && g_admin_bans[ i ]; i++ ) + ; + if( i == MAX_ADMIN_BANS ) + { + ADMP( "^3!ban: ^7too many bans\n" ); + G_Free( b ); + return qfalse; + } + g_admin_bans[ i ] = b; + admin_writeconfig(); + return qtrue; +} + + +qboolean G_admin_kick( gentity_t *ent, int skiparg ) +{ + int pids[ MAX_CLIENTS ]; + char name[ MAX_NAME_LENGTH ], *reason, err[ MAX_STRING_CHARS ]; + int minargc; + gentity_t *vic; + + minargc = 3 + skiparg; + if( G_admin_permission( ent, ADMF_UNACCOUNTABLE ) ) + minargc = 2 + skiparg; + + if( G_SayArgc() < minargc ) + { + ADMP( "^3!kick: ^7usage: kick [name] [reason]\n" ); + return qfalse; + } + G_SayArgv( 1 + skiparg, name, sizeof( name ) ); + reason = G_SayConcatArgs( 2 + skiparg ); + if( G_ClientNumbersFromString( name, pids ) != 1 ) + { + G_MatchOnePlayer( pids, err, sizeof( err ) ); + ADMP( va( "^3!kick: ^7%s\n", err ) ); + return qfalse; + } + if( !admin_higher( ent, &g_entities[ pids[ 0 ] ] ) ) + { + ADMP( "^3!kick: ^7sorry, but your intended victim has a higher admin" + " level than you\n" ); + return qfalse; + } + vic = &g_entities[ pids[ 0 ] ]; + if( g_adminTempBan.integer > 0 ) + { + admin_create_ban( ent, + vic->client->pers.netname, + vic->client->pers.guid, + vic->client->pers.ip, g_adminTempBan.integer, + "automatic temp ban created by kick" ); + } + + trap_SendServerCommand( pids[ 0 ], + va( "disconnect \"You have been kicked.\n%s^7\nreason:\n%s\"", + ( ent ) ? va( "admin:\n%s", ent->client->pers.netname ) : "", + ( *reason ) ? reason : "kicked by admin" ) ); + + trap_DropClient( pids[ 0 ], va( "has been kicked%s^7. reason: %s", + ( ent ) ? va( " by %s", ent->client->pers.netname ) : "", + ( *reason ) ? reason : "kicked by admin" ) ); + + return qtrue; +} + +qboolean G_admin_ban( gentity_t *ent, int skiparg ) +{ + int seconds; + char search[ MAX_NAME_LENGTH ]; + char secs[ 7 ]; + char *reason; + int minargc; + char duration[ 32 ]; + int modifier = 1; + int logmatch = -1, logmatches = 0; + int i, j; + qboolean exactmatch = qfalse; + char n2[ MAX_NAME_LENGTH ]; + char s2[ MAX_NAME_LENGTH ]; + char guid_stub[ 9 ]; + + if( G_admin_permission( ent, ADMF_CAN_PERM_BAN ) && + G_admin_permission( ent, ADMF_UNACCOUNTABLE ) ) + { + minargc = 2 + skiparg; + } + else if( G_admin_permission( ent, ADMF_CAN_PERM_BAN ) || + G_admin_permission( ent, ADMF_UNACCOUNTABLE ) ) + { + minargc = 3 + skiparg; + } + else + { + minargc = 4 + skiparg; + } + if( G_SayArgc() < minargc ) + { + ADMP( "^3!ban: ^7usage: ban [name|slot|ip] [seconds] [reason]\n" ); + return qfalse; + } + G_SayArgv( 1 + skiparg, search, sizeof( search ) ); + G_SanitiseName( search, s2 ); + G_SayArgv( 2 + skiparg, secs, sizeof( secs ) ); + + // support "w" (weeks), "d" (days), "h" (hours), and "m" (minutes) modifiers + if( secs[ 0 ] ) + { + int lastchar = strlen( secs ) - 1; + if( secs[ lastchar ] == 'w' ) + modifier = 60 * 60 * 24 * 7; + else if( secs[ lastchar ] == 'd' ) + modifier = 60 * 60 * 24; + else if( secs[ lastchar ] == 'h' ) + modifier = 60 * 60; + else if( secs[ lastchar ] == 'm' ) + modifier = 60; + secs[ lastchar ] = '\0'; + } + seconds = atoi( secs ); + if( seconds > 0 ) + seconds *= modifier; + + if( seconds <= 0 ) + { + if( G_admin_permission( ent, ADMF_CAN_PERM_BAN ) ) + { + seconds = 0; + } + else + { + ADMP( "^3!ban: ^7ban time must be positive\n" ); + return qfalse; + } + reason = G_SayConcatArgs( 2 + skiparg ); + } + else + { + reason = G_SayConcatArgs( 3 + skiparg ); + } + + for( i = 0; i < MAX_ADMIN_NAMELOGS && g_admin_namelog[ i ]; i++ ) + { + if( !Q_stricmp( g_admin_namelog[ i ]->ip, s2 ) + || !Q_stricmp( va( "%d", g_admin_namelog[ i ]->slot ), s2 ) ) + { + logmatches = 1; + logmatch = i; + exactmatch = qtrue; + break; + } + for( j = 0; j < MAX_ADMIN_NAMELOG_NAMES + && g_admin_namelog[ i ]->name[ j ][ 0 ]; j++ ) + { + G_SanitiseName(g_admin_namelog[ i ]->name[ j ], n2); + if( strstr( n2, s2 ) ) + { + if( logmatch != i ) + logmatches++; + logmatch = i; + } + } + } + + if( !logmatches ) + { + ADMP( "^3!ban: ^7no player found by that name, IP, or slot number\n" ); + return qfalse; + } + else if( logmatches > 1 ) + { + ADMBP_begin(); + ADMBP( "^3!ban: ^7multiple recent clients match name, use IP or slot#:\n" ); + for( i = 0; i < MAX_ADMIN_NAMELOGS && g_admin_namelog[ i ]; i++ ) + { + for( j = 0; j <= 8; j++ ) + guid_stub[ j ] = g_admin_namelog[ i ]->guid[ j + 24 ]; + guid_stub[ j ] = '\0'; + for( j = 0; j < MAX_ADMIN_NAMELOG_NAMES + && g_admin_namelog[ i ]->name[ j ][ 0 ]; j++ ) + { + G_SanitiseName(g_admin_namelog[ i ]->name[ j ], n2); + if( strstr( n2, s2 ) ) + { + if( g_admin_namelog[ i ]->slot > -1 ) + ADMBP( "^3" ); + ADMBP( va( "%-2s (*%s) %15s ^7'%s^7'\n", + (g_admin_namelog[ i ]->slot > -1) ? + va( "%d", g_admin_namelog[ i ]->slot ) : "-", + guid_stub, + g_admin_namelog[ i ]->ip, + g_admin_namelog[ i ]->name[ j ] ) ); + } + } + } + ADMBP_end(); + return qfalse; + } + + G_admin_duration( ( seconds ) ? seconds : -1, + duration, sizeof( duration ) ); + + if( ent && !admin_higher_guid( ent->client->pers.guid, + g_admin_namelog[ logmatch ]->guid ) ) + { + + ADMP( "^3!ban: ^7sorry, but your intended victim has a higher admin" + " level than you\n" ); + return qfalse; + } + + admin_create_ban( ent, + g_admin_namelog[ logmatch ]->name[ 0 ], + g_admin_namelog[ logmatch ]->guid, + g_admin_namelog[ logmatch ]->ip, + seconds, reason ); + + if(g_admin_namelog[ logmatch ]->slot == -1 ) + { + // client is already disconnected so stop here + AP( va( "print \"^3!ban:^7 %s^7 has been banned by %s^7 " + "duration: %s, reason: %s\n\"", + g_admin_namelog[ logmatch ]->name[ 0 ], + ( ent ) ? ent->client->pers.netname : "console", + duration, + ( *reason ) ? reason : "banned by admin" ) ); + return qtrue; + } + + trap_SendServerCommand( g_admin_namelog[ logmatch ]->slot, + va( "disconnect \"You have been banned.\n" + "admin:\n%s^7\nduration:\n%s\nreason:\n%s\"", + ( ent ) ? ent->client->pers.netname : "console", + duration, + ( *reason ) ? reason : "kicked by admin" ) ); + + trap_DropClient( g_admin_namelog[ logmatch ]->slot, + va( "has been banned by %s^7 duration: %s, reason: %s", + ( ent ) ? ent->client->pers.netname : "console", + duration, + ( *reason ) ? reason : "banned by admin" ) ); + return qtrue; +} + +qboolean G_admin_unban( gentity_t *ent, int skiparg ) +{ + int bnum; + char bs[ 4 ]; + qtime_t qt; + int t; + + t = trap_RealTime( &qt ); + if( G_SayArgc() < 2 + skiparg ) + { + ADMP( "^3!unban: ^7usage: unban [ban #]\n" ); + return qfalse; + } + G_SayArgv( 1 + skiparg, bs, sizeof( bs ) ); + bnum = atoi( bs ); + if( bnum < 1 ) + { + ADMP( "^3!unban: ^7invalid ban #\n" ); + return qfalse; + } + if( !g_admin_bans[ bnum - 1 ] ) + { + ADMP( "^3!unban: ^7invalid ban #\n" ); + return qfalse; + } + g_admin_bans[ bnum -1 ]->expires = t; + AP( va( "print \"^3!unban: ^7ban #%d for %s^7 has been removed by %s\n\"", + bnum, + g_admin_bans[ bnum - 1 ]->name, + ( ent ) ? ent->client->pers.netname : "console" ) ); + admin_writeconfig(); + return qtrue; +} + +qboolean G_admin_putteam( gentity_t *ent, int skiparg ) +{ + int pids[ MAX_CLIENTS ]; + char name[ MAX_NAME_LENGTH ], team[ 7 ], err[ MAX_STRING_CHARS ]; + gentity_t *vic; + pTeam_t teamnum = PTE_NONE; + char teamdesc[ 32 ] = {"spectators"}; + + G_SayArgv( 1 + skiparg, name, sizeof( name ) ); + G_SayArgv( 2 + skiparg, team, sizeof( team ) ); + if( G_SayArgc() < 3 + skiparg ) + { + ADMP( "^3!putteam: ^7usage: putteam [name] [h|a|s]\n" ); + return qfalse; + } + + if( G_ClientNumbersFromString( name, pids ) != 1 ) + { + G_MatchOnePlayer( pids, err, sizeof( err ) ); + ADMP( va( "^3!putteam: ^7%s\n", err ) ); + return qfalse; + } + if( !admin_higher( ent, &g_entities[ pids[ 0 ] ] ) ) + { + ADMP( "^3!putteam: ^7sorry, but your intended victim has a higher " + " admin level than you\n" ); + return qfalse; + } + vic = &g_entities[ pids[ 0 ] ]; + switch( team[ 0 ] ) + { + case 'a': + teamnum = PTE_ALIENS; + Q_strncpyz( teamdesc, "aliens", sizeof( teamdesc ) ); + break; + case 'h': + teamnum = PTE_HUMANS; + Q_strncpyz( teamdesc, "humans", sizeof( teamdesc ) ); + break; + case 's': + teamnum = PTE_NONE; + break; + default: + ADMP( va( "^3!putteam: ^7unknown team %c\n", team[ 0 ] ) ); + return qfalse; + } + G_ChangeTeam( vic, teamnum ); + + AP( va( "print \"^3!putteam: ^7%s^7 put %s^7 on to the %s team\n\"", + ( ent ) ? ent->client->pers.netname : "console", + vic->client->pers.netname, teamdesc ) ); + return qtrue; +} + +qboolean G_admin_mute( gentity_t *ent, int skiparg ) +{ + int pids[ MAX_CLIENTS ]; + char name[ MAX_NAME_LENGTH ], err[ MAX_STRING_CHARS ]; + char command[ MAX_ADMIN_CMD_LEN ], *cmd; + gentity_t *vic; + + if( G_SayArgc() < 2 + skiparg ) + { + ADMP( "^3!mute: ^7usage: mute [name|slot#]\n" ); + return qfalse; + } + G_SayArgv( skiparg, command, sizeof( command ) ); + cmd = command; + if( cmd && *cmd == '!' ) + cmd++; + G_SayArgv( 1 + skiparg, name, sizeof( name ) ); + if( G_ClientNumbersFromString( name, pids ) != 1 ) + { + G_MatchOnePlayer( pids, err, sizeof( err ) ); + ADMP( va( "^3!mute: ^7%s\n", err ) ); + return qfalse; + } + if( !admin_higher( ent, &g_entities[ pids[ 0 ] ] ) ) + { + ADMP( "^3!mute: ^7sorry, but your intended victim has a higher admin" + " level than you\n" ); + return qfalse; + } + vic = &g_entities[ pids[ 0 ] ]; + if( vic->client->pers.muted == qtrue ) + { + if( !Q_stricmp( cmd, "mute" ) ) + { + ADMP( "^3!mute: ^7player is already muted\n" ); + return qtrue; + } + vic->client->pers.muted = qfalse; + CPx( pids[ 0 ], "cp \"^1You have been unmuted\"" ); + AP( va( "print \"^3!unmute: ^7%s^7 has been unmuted by %s\n\"", + vic->client->pers.netname, + ( ent ) ? ent->client->pers.netname : "console" ) ); + } + else + { + if( !Q_stricmp( cmd, "unmute" ) ) + { + ADMP( "^3!unmute: ^7player is not currently muted\n" ); + return qtrue; + } + vic->client->pers.muted = qtrue; + CPx( pids[ 0 ], "cp \"^1You've been muted\"" ); + AP( va( "print \"^3!mute: ^7%s^7 has been muted by ^7%s\n\"", + vic->client->pers.netname, + ( ent ) ? ent->client->pers.netname : "console" ) ); + } + ClientUserinfoChanged( pids[ 0 ] ); + return qtrue; +} + +qboolean G_admin_listadmins( gentity_t *ent, int skiparg ) +{ + int i, found = 0; + qtime_t qt; + int t; + char search[ MAX_NAME_LENGTH ] = {""}; + char s[ MAX_NAME_LENGTH ] = {""}; + int start = 0; + qboolean numeric = qtrue; + int drawn = 0; + + t = trap_RealTime( &qt ); + + for( i = 0; i < MAX_ADMIN_ADMINS && g_admin_admins[ i ]; i++ ) + { + if( g_admin_admins[ i ]->level == 0 ) + continue; + found++; + } + + if( G_SayArgc() == 2 + skiparg ) + { + G_SayArgv( 1 + skiparg, s, sizeof( s ) ); + for( i = 0; i < sizeof( s ) && s[ i ]; i++ ) + { + if( s[ i ] >= '0' && s[ i ] <= '9' ) + continue; + numeric = qfalse; + } + if( numeric ) + { + start = atoi( s ); + if( start > 0 ) + start -= 1; + else if( start < 0 ) + start = found + start; + } + else + G_SanitiseName( s, search ); + } + + if( start >= found || start < 0 ) + start = 0; + + if( start >= found ) + { + ADMP( va( "^3!listadmins: ^7there are only %d admins\n", found ) ); + return qfalse; + } + + drawn = admin_listadmins( ent, start, search ); + + if( search[ 0 ] ) + { + ADMP( va( "^3!listadmins:^7 found %d admins matching '%s^7'\n", + drawn, search ) ); + } + else + { + ADMBP_begin(); + ADMBP( va( "^3!listadmins:^7 showing admin %d - %d of %d. ", + ( found ) ? ( start + 1 ) : 0, + ( ( start + MAX_ADMIN_LISTITEMS ) > found ) ? + found : ( start + MAX_ADMIN_LISTITEMS ), + found ) ); + if( ( start + MAX_ADMIN_LISTITEMS ) < found ) + { + ADMBP( va( "run '!listadmins %d' to see more", + ( start + MAX_ADMIN_LISTITEMS + 1 ) ) ); + } + ADMBP( "\n" ); + ADMBP_end(); + } + return qtrue; +} + +qboolean G_admin_listplayers( gentity_t *ent, int skiparg ) +{ + int i, j; + gclient_t *p; + char c[ 3 ], t[ 2 ]; // color and team letter + char n[ MAX_NAME_LENGTH ] = {""}; + char n2[ MAX_NAME_LENGTH ] = {""}; + char n3[ MAX_NAME_LENGTH ] = {""}; + char lname[ MAX_NAME_LENGTH ]; + char lname2[ MAX_NAME_LENGTH ]; + char guid_stub[ 9 ]; + char muted[ 2 ]; + int l; + char lname_fmt[ 5 ]; + + ADMBP_begin(); + ADMBP( va( "^3!listplayers^7: %d players connected:\n", + level.numConnectedClients ) ); + for( i = 0; i < level.maxclients; i++ ) + { + p = &level.clients[ i ]; + Q_strncpyz( t, "S", sizeof( t ) ); + Q_strncpyz( c, S_COLOR_YELLOW, sizeof( c ) ); + if( p->ps.stats[ STAT_PTEAM ] == PTE_HUMANS ) + { + Q_strncpyz( t, "H", sizeof( t ) ); + Q_strncpyz( c, S_COLOR_BLUE, sizeof( c ) ); + } + else if( p->ps.stats[ STAT_PTEAM ] == PTE_ALIENS ) + { + Q_strncpyz( t, "A", sizeof( t ) ); + Q_strncpyz( c, S_COLOR_RED, sizeof( c ) ); + } + + if( p->pers.connected == CON_CONNECTING ) + { + Q_strncpyz( t, "C", sizeof( t ) ); + Q_strncpyz( c, S_COLOR_CYAN, sizeof( c ) ); + } + else if( p->pers.connected != CON_CONNECTED ) + { + continue; + } + + for( j = 0; j <= 8; j++ ) + guid_stub[ j ] = p->pers.guid[ j + 24 ]; + guid_stub[ j ] = '\0'; + + muted[ 0 ] = '\0'; + if( p->pers.muted ) + { + Q_strncpyz( muted, "M", sizeof( muted ) ); + } + + l = 0; + G_SanitiseName( p->pers.netname, n2 ); + n[ 0 ] = '\0'; + for( j = 0; j < MAX_ADMIN_ADMINS && g_admin_admins[ j ]; j++ ) + { + if( !Q_stricmp( g_admin_admins[ j ]->guid, p->pers.guid ) ) + { + + // don't gather aka or level info if the admin is incognito + if( G_admin_permission( &g_entities[ i ], ADMF_INCOGNITO ) ) + { + break; + } + l = g_admin_admins[ j ]->level; + G_SanitiseName( g_admin_admins[ j ]->name, n3 ); + if( Q_stricmp( n2, n3 ) ) + { + Q_strncpyz( n, g_admin_admins[ j ]->name, sizeof( n ) ); + } + break; + } + } + lname[ 0 ] = '\0'; + Q_strncpyz( lname_fmt, "%s", sizeof( lname_fmt ) ); + for( j = 0; j < MAX_ADMIN_LEVELS && g_admin_levels[ j ]; j++ ) + { + if( g_admin_levels[ j ]->level == l ) + { + Q_strncpyz( lname, g_admin_levels[ j ]->name, sizeof( lname ) ); + if( *lname ) + { + G_DecolorString( lname, lname2 ); + Com_sprintf( lname_fmt, sizeof( lname_fmt ), "%%%is", + ( admin_level_maxname + strlen( lname ) - strlen( lname2 ) ) ); + Com_sprintf( lname2, sizeof( lname2 ), lname_fmt, lname ); + } + break; + } + + } + + ADMBP( va( "%2i %s%s^7 %-2i %s^7 (*%s) ^1%1s^7 %s^7 %s%s^7%s\n", + i, + c, + t, + l, + ( *lname ) ? lname2 : "", + guid_stub, + muted, + p->pers.netname, + ( *n ) ? "(a.k.a. " : "", + n, + ( *n ) ? ")" : "" + ) ); + } + ADMBP_end(); + return qtrue; +} + +qboolean G_admin_showbans( gentity_t *ent, int skiparg ) +{ + int i, found = 0; + qtime_t qt; + int t; + char duration[ 32 ]; + char name_fmt[ 32 ] = { "%s" }; + char banner_fmt[ 32 ] = { "%s" }; + int max_name = 1, max_banner = 1; + int secs; + int start = 0; + char skip[ 11 ]; + char date[ 11 ]; + char *made; + int j; + char n1[ MAX_NAME_LENGTH ] = {""}; + char n2[ MAX_NAME_LENGTH ] = {""}; + + t = trap_RealTime( &qt ); + + for( i = 0; i < MAX_ADMIN_BANS && g_admin_bans[ i ]; i++ ) + { + if( g_admin_bans[ i ]->expires != 0 + && ( g_admin_bans[ i ]->expires - t ) < 1 ) + { + continue; + } + found++; + } + + if( G_SayArgc() < 3 + skiparg ) + { + G_SayArgv( 1 + skiparg, skip, sizeof( skip ) ); + start = atoi( skip ); + // showbans 1 means start with ban 0 + if( start > 0 ) + start -= 1; + else if( start < 0 ) + start = found + start; + } + + if( start >= MAX_ADMIN_BANS || start < 0 ) + start = 0; + + for( i = start; i < MAX_ADMIN_BANS && g_admin_bans[ i ] + && ( i - start ) < MAX_ADMIN_SHOWBANS; i++ ) + { + G_DecolorString( g_admin_bans[ i ]->name, n1 ); + G_DecolorString( g_admin_bans[ i ]->banner, n2 ); + if( strlen( n1 ) > max_name ) + { + max_name = strlen( n1 ); + } + if( strlen( n2 ) > max_banner ) + max_banner = strlen( n2 ); + } + + if( start >= found ) + { + ADMP( va( "^3!showbans: ^7there are %d active bans\n", found ) ); + return qfalse; + } + ADMBP_begin(); + for( i = start; i < MAX_ADMIN_BANS && g_admin_bans[ i ] + && ( i - start ) < MAX_ADMIN_SHOWBANS; i++ ) + { + if( g_admin_bans[ i ]->expires != 0 + && ( g_admin_bans[ i ]->expires - t ) < 1 ) + continue; + + // only print out the the date part of made + date[ 0 ] = '\0'; + made = g_admin_bans[ i ]->made; + for( j = 0; made && *made; j++ ) + { + if( ( j + 1 ) >= sizeof( date ) ) + break; + if( *made == ' ' ) + break; + date[ j ] = *made; + date[ j + 1 ] = '\0'; + made++; + } + + secs = ( g_admin_bans[ i ]->expires - t ); + G_admin_duration( secs, duration, sizeof( duration ) ); + + G_DecolorString( g_admin_bans[ i ]->name, n1 ); + Com_sprintf( name_fmt, sizeof( name_fmt ), "%%%is", + ( max_name + strlen( g_admin_bans[ i ]->name ) - strlen( n1 ) ) ); + Com_sprintf( n1, sizeof( n1 ), name_fmt, g_admin_bans[ i ]->name ); + + G_DecolorString( g_admin_bans[ i ]->banner, n2 ); + Com_sprintf( banner_fmt, sizeof( banner_fmt ), "%%%is", + ( max_banner + strlen( g_admin_bans[ i ]->banner ) - strlen( n2 ) ) ); + Com_sprintf( n2, sizeof( n2 ), banner_fmt, g_admin_bans[ i ]->banner ); + + ADMBP( va( "%4i %s^7 %-15s %-8s %s^7 %-10s\n \\__ %s\n", + ( i + 1 ), + n1, + g_admin_bans[ i ]->ip, + date, + n2, + duration, + g_admin_bans[ i ]->reason ) ); + } + + ADMBP( va( "^3!showbans:^7 showing bans %d - %d of %d. ", + ( found ) ? ( start + 1 ) : 0, + ( ( start + MAX_ADMIN_SHOWBANS ) > found ) ? + found : ( start + MAX_ADMIN_SHOWBANS ), + found ) ); + if( ( start + MAX_ADMIN_SHOWBANS ) < found ) + { + ADMBP( va( "run !showbans %d to see more", + ( start + MAX_ADMIN_SHOWBANS + 1 ) ) ); + } + ADMBP( "\n" ); + ADMBP_end(); + return qtrue; +} + +qboolean G_admin_help( gentity_t *ent, int skiparg ) +{ + int i; + + if( G_SayArgc() < 2 + skiparg ) + { + int j = 0; + int count = 0; + + ADMBP_begin(); + for( i = 0; i < adminNumCmds; i++ ) + { + if( G_admin_permission( ent, g_admin_cmds[ i ].flag[ 0 ] ) ) + { + ADMBP( va( "^3!%-12s", g_admin_cmds[ i ].keyword ) ); + j++; + count++; + } + // show 6 commands per line + if( j == 6 ) + { + ADMBP( "\n" ); + j = 0; + } + } + for( i = 0; i < MAX_ADMIN_COMMANDS && g_admin_commands[ i ]; i++ ) + { + if( ! admin_command_permission( ent, g_admin_commands[ i ]->command ) ) + continue; + ADMBP( va( "^3!%-12s", g_admin_commands[ i ]->command ) ); + j++; + count++; + // show 6 commands per line + if( j == 6 ) + { + ADMBP( "\n" ); + j = 0; + } + } + if( count ) + ADMBP( "\n" ); + ADMBP( va( "^3!help: ^7%i available commands\n", count ) ); + ADMBP( "run !help [^3command^7] for help with a specific command.\n" ); + ADMBP_end(); + + return qtrue; + } + else + { + //!help param + char param[ MAX_ADMIN_CMD_LEN ]; + char *cmd; + + G_SayArgv( 1 + skiparg, param, sizeof( param ) ); + cmd = ( param[0] == '!' ) ? ¶m[1] : ¶m[0]; + ADMBP_begin(); + for( i = 0; i < adminNumCmds; i++ ) + { + if( !Q_stricmp( cmd, g_admin_cmds[ i ].keyword ) ) + { + if( !G_admin_permission( ent, g_admin_cmds[ i ].flag[ 0 ] ) ) + { + ADMBP( va( "^3!help: ^7you have no permission to use '%s'\n", + g_admin_cmds[ i ].keyword ) ); + return qfalse; + } + ADMBP( va( "^3!help: ^7help for '!%s':\n", + g_admin_cmds[ i ].keyword ) ); + ADMBP( va( " ^3Funtion: ^7%s\n", g_admin_cmds[ i ].function ) ); + ADMBP( va( " ^3Syntax: ^7%s %s\n", g_admin_cmds[ i ].keyword, + g_admin_cmds[ i ].syntax ) ); + ADMBP( va( " ^3Flag: ^7'%c'\n", g_admin_cmds[ i ].flag[ 0 ] ) ); + ADMBP_end(); + return qtrue; + } + } + for( i = 0; i < MAX_ADMIN_COMMANDS && g_admin_commands[ i ]; i++ ) + { + if( !Q_stricmp( cmd, g_admin_commands[ i ]->command ) ) + { + if( !admin_command_permission( ent, g_admin_commands[ i ]->command ) ) + { + ADMBP( va( "^3!help: ^7you have no permission to use '%s'\n", + g_admin_commands[ i ]->command ) ); + ADMBP_end(); + return qfalse; + } + ADMBP( va( "^3!help: ^7help for '%s':\n", + g_admin_commands[ i ]->command ) ); + ADMBP( va( " ^3Description: ^7%s\n", g_admin_commands[ i ]->desc ) ); + ADMBP( va( " ^3Syntax: ^7%s\n", g_admin_commands[ i ]->command ) ); + ADMBP_end(); + return qtrue; + } + } + ADMBP( va( "^3!help: ^7no help found for '%s'\n", cmd ) ); + ADMBP_end(); + return qfalse; + } +} + +qboolean G_admin_admintest( gentity_t *ent, int skiparg ) +{ + int i, l = 0; + qboolean found = qfalse; + qboolean lname = qfalse; + + if( !ent ) + { + ADMP( "^3!admintest: ^7you are on the console.\n" ); + return qtrue; + } + for( i = 0; i < MAX_ADMIN_ADMINS && g_admin_admins[ i ]; i++ ) + { + if( !Q_stricmp( g_admin_admins[ i ]->guid, ent->client->pers.guid ) ) + { + found = qtrue; + break; + } + } + + if( found ) + { + l = g_admin_admins[ i ]->level; + for( i = 0; i < MAX_ADMIN_LEVELS && g_admin_levels[ i ]; i++ ) + { + if( g_admin_levels[ i ]->level != l ) + continue; + if( *g_admin_levels[ i ]->name ) + { + lname = qtrue; + break; + } + } + } + AP( va( "print \"^3!admintest: ^7%s^7 is a level %d admin %s%s^7%s\n\"", + ent->client->pers.netname, + l, + ( lname ) ? "(" : "", + ( lname ) ? g_admin_levels[ i ]->name : "", + ( lname ) ? ")" : "" ) ); + return qtrue; +} + +qboolean G_admin_allready( gentity_t *ent, int skiparg ) +{ + int i = 0; + gclient_t *cl; + + if( !level.intermissiontime ) + { + ADMP( "^3!allready: ^7this command is only valid during intermission\n" ); + return qfalse; + } + + for( i = 0; i < g_maxclients.integer; i++ ) + { + cl = level.clients + i; + if( cl->pers.connected != CON_CONNECTED ) + continue; + + if( cl->ps.stats[ STAT_PTEAM ] == PTE_NONE ) + continue; + + cl->readyToExit = 1; + } + AP( va( "print \"^3!allready:^7 %s^7 says everyone is READY now\n\"", + ( ent ) ? ent->client->pers.netname : "console" ) ); + return qtrue; +} + +qboolean G_admin_cancelvote( gentity_t *ent, int skiparg ) +{ + + if(!level.voteTime && !level.teamVoteTime[ 0 ] && !level.teamVoteTime[ 1 ] ) + { + ADMP( "^3!cancelvote^7: no vote in progress\n" ); + return qfalse; + } + level.voteNo = level.numConnectedClients; + level.voteYes = 0; + CheckVote( ); + level.teamVoteNo[ 0 ] = level.numConnectedClients; + level.teamVoteYes[ 0 ] = 0; + CheckTeamVote( 0 ); + level.teamVoteNo[ 1 ] = level.numConnectedClients; + level.teamVoteYes[ 1 ] = 0; + CheckTeamVote( 1 ); + AP( va( "print \"^3!cancelvote: ^7%s^7 decided that everyone voted No\n\"", + ( ent ) ? ent->client->pers.netname : "console" ) ); + return qtrue; +} + +qboolean G_admin_passvote( gentity_t *ent, int skiparg ) +{ + if(!level.voteTime && !level.teamVoteTime[ 0 ] && !level.teamVoteTime[ 1 ] ) + { + ADMP( "^3!passvote^7: no vote in progress\n" ); + return qfalse; + } + level.voteYes = level.numConnectedClients; + level.voteNo = 0; + CheckVote( ); + level.teamVoteYes[ 0 ] = level.numConnectedClients; + level.teamVoteNo[ 0 ] = 0; + CheckTeamVote( 0 ); + level.teamVoteYes[ 1 ] = level.numConnectedClients; + level.teamVoteNo[ 1 ] = 0; + CheckTeamVote( 1 ); + AP( va( "print \"^3!passvote: ^7%s^7 decided that everyone voted Yes\n\"", + ( ent ) ? ent->client->pers.netname : "console" ) ); + return qtrue; +} + +qboolean G_admin_spec999( gentity_t *ent, int skiparg ) +{ + int i; + gentity_t *vic; + + for( i = 0; i < level.maxclients; i++ ) + { + vic = &g_entities[ i ]; + if( !vic->client ) + continue; + if( vic->client->pers.connected != CON_CONNECTED ) + continue; + if( vic->client->ps.ping == 999 ) + { + G_ChangeTeam( vic, PTE_NONE ); + AP( va( "print \"^3!spec999: ^7%s^7 moved ^7%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 skiparg ) +{ + int pids[ MAX_CLIENTS ]; + char name[ MAX_NAME_LENGTH ]; + char newname[ MAX_NAME_LENGTH ]; + char oldname[ MAX_NAME_LENGTH ]; + char err[ MAX_STRING_CHARS ]; + char userinfo[ MAX_INFO_STRING ]; + char *s; + gentity_t *victim = NULL; + + if( G_SayArgc() < 3 + skiparg ) + { + ADMP( "^3!rename: ^7usage: rename [name] [newname]\n" ); + return qfalse; + } + G_SayArgv( 1 + skiparg, name, sizeof( name ) ); + s = G_SayConcatArgs( 2 + skiparg ); + Q_strncpyz( newname, s, sizeof( newname ) ); + if( G_ClientNumbersFromString( name, pids ) != 1 ) + { + G_MatchOnePlayer( pids, err, sizeof( err ) ); + ADMP( va( "^3!rename: ^7%s\n", err ) ); + return qfalse; + } + victim = &g_entities[ pids[ 0 ] ] ; + if( !admin_higher( ent, victim ) ) + { + ADMP( "^3!rename: ^7sorry, but your intended victim has a higher admin" + " level than you\n" ); + return qfalse; + } + if( !G_admin_name_check( victim, newname, err, sizeof( err ) ) ) + { + ADMP( va( "^3!rename: ^7%s\n", err ) ); + return qfalse; + } + level.clients[ pids[ 0 ] ].pers.nameChanges--; + level.clients[ pids[ 0 ] ].pers.nameChangeTime = 0; + trap_GetUserinfo( pids[ 0 ], userinfo, sizeof( userinfo ) ); + s = Info_ValueForKey( userinfo, "name" ); + Q_strncpyz( oldname, s, sizeof( oldname ) ); + Info_SetValueForKey( userinfo, "name", newname ); + trap_SetUserinfo( pids[ 0 ], userinfo ); + ClientUserinfoChanged( pids[ 0 ] ); + AP( va( "print \"^3!rename: ^7%s^7 has been renamed to %s^7 by %s\n\"", + oldname, + newname, + ( ent ) ? ent->client->pers.netname : "console" ) ); + return qtrue; +} + +qboolean G_admin_restart( gentity_t *ent, int skiparg ) +{ + char command[ MAX_ADMIN_CMD_LEN ]; + + G_SayArgv( skiparg, command, sizeof( command ) ); + trap_SendConsoleCommand( EXEC_APPEND, "map_restart" ); + AP( va( "print \"^3!restart: ^7map restarted by %s\n\"", + ( ent ) ? ent->client->pers.netname : "console" ) ); + return qtrue; +} + +qboolean G_admin_nextmap( gentity_t *ent, int skiparg ) +{ + AP( va( "print \"^3!nextmap: ^7%s^7 decided to load the next map\n\"", + ( ent ) ? ent->client->pers.netname : "console" ) ); + level.lastWin = PTE_NONE; + LogExit( va( "nextmap was run by %s", + ( ent ) ? ent->client->pers.netname : "console" ) ); + return qtrue; +} + +qboolean G_admin_namelog( gentity_t *ent, int skiparg ) +{ + int i, j; + char search[ MAX_NAME_LENGTH ] = {""}; + char s2[ MAX_NAME_LENGTH ] = {""}; + char n2[ MAX_NAME_LENGTH ] = {""}; + char guid_stub[ 9 ]; + qboolean found = qfalse; + int printed = 0; + + if( G_SayArgc() > 1 + skiparg ) + { + G_SayArgv( 1 + skiparg, search, sizeof( search ) ); + G_SanitiseName( search, s2 ); + } + ADMBP_begin(); + for( i = 0; i < MAX_ADMIN_NAMELOGS && g_admin_namelog[ i ]; i++ ) + { + if( search[0] ) + { + found = qfalse; + for( j = 0; j < MAX_ADMIN_NAMELOG_NAMES && + g_admin_namelog[ i ]->name[ j ][ 0 ]; j++ ) + { + G_SanitiseName( g_admin_namelog[ i ]->name[ j ], n2 ); + if( strstr( n2, s2 ) ) + { + found = qtrue; + break; + } + } + if( !found ) + continue; + } + printed++; + for( j = 0; j <= 8; j++ ) + guid_stub[ j ] = g_admin_namelog[ i ]->guid[ j + 24 ]; + guid_stub[ j ] = '\0'; + if( g_admin_namelog[ i ]->slot > -1 ) + ADMBP( "^3" ); + ADMBP( va( "%-2s (*%s) %15s^7", + (g_admin_namelog[ i ]->slot > -1 ) ? + va( "%d", g_admin_namelog[ i ]->slot ) : "-", + guid_stub, g_admin_namelog[ i ]->ip ) ); + for( j = 0; j < MAX_ADMIN_NAMELOG_NAMES && + g_admin_namelog[ i ]->name[ j ][ 0 ]; j++ ) + { + ADMBP( va( " '%s^7'", g_admin_namelog[ i ]->name[ j ] ) ); + } + ADMBP( "\n" ); + } + ADMBP( va( "^3!namelog:^7 %d recent clients found\n", printed ) ); + ADMBP_end(); + return qtrue; +} + +/* + * This function facilitates the TP 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 ]; + G_DecolorString( m, m2 ); + G_Printf( m2 ); + } +} + +void G_admin_buffer_begin() +{ + g_bfb[ 0 ] = '\0'; +} + +void G_admin_buffer_end( gentity_t *ent ) +{ + ADMP( g_bfb ); +} + +void G_admin_buffer_print( gentity_t *ent, char *m ) +{ + // 1022 - strlen("print 64 \"\"") - 1 + if( strlen( m ) + strlen( g_bfb ) >= 1009 ) + { + ADMP( g_bfb ); + g_bfb[ 0 ] = '\0'; + } + Q_strcat( g_bfb, sizeof( g_bfb ), m ); +} + + +void G_admin_cleanup() +{ + int i = 0; + + for( i = 0; i < MAX_ADMIN_LEVELS && g_admin_levels[ i ]; i++ ) + { + G_Free( g_admin_levels[ i ] ); + g_admin_levels[ i ] = NULL; + } + for( i = 0; i < MAX_ADMIN_ADMINS && g_admin_admins[ i ]; i++ ) + { + G_Free( g_admin_admins[ i ] ); + g_admin_admins[ i ] = NULL; + } + for( i = 0; i < MAX_ADMIN_BANS && g_admin_bans[ i ]; i++ ) + { + G_Free( g_admin_bans[ i ] ); + g_admin_bans[ i ] = NULL; + } + for( i = 0; i < MAX_ADMIN_COMMANDS && g_admin_commands[ i ]; i++ ) + { + G_Free( g_admin_commands[ i ] ); + g_admin_commands[ i ] = NULL; + } +} diff --git a/src/game/g_admin.h b/src/game/g_admin.h new file mode 100644 index 00000000..0b5c6907 --- /dev/null +++ b/src/game/g_admin.h @@ -0,0 +1,174 @@ +/* +=========================================================================== +Copyright (C) 2004-2006 Tony J. White + +This file is part of Tremulous. + +Tremulous is free software; you can redistribute it +and/or modify it under the terms of the GNU General Public License as +published by the Free Software Foundation; either version 2 of the License, +or (at your option) any later version. + +Tremulous is distributed in the hope that it will be +useful, but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with Tremulous; if not, write to the Free Software +Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA +=========================================================================== +*/ + +#ifndef _G_ADMIN_H +#define _G_ADMIN_H + +#define AP(x) trap_SendServerCommand(-1, x) +#define CP(x) trap_SendServerCommand(ent-g_entities, x) +#define CPx(x, y) trap_SendServerCommand(x, y) +#define ADMP(x) G_admin_print(ent, x) +#define ADMBP(x) G_admin_buffer_print(ent, x) +#define ADMBP_begin() G_admin_buffer_begin() +#define ADMBP_end() G_admin_buffer_end(ent) + +#define MAX_ADMIN_LEVELS 32 +#define MAX_ADMIN_ADMINS 1024 +#define MAX_ADMIN_BANS 1024 +#define MAX_ADMIN_NAMELOGS 128 +#define MAX_ADMIN_NAMELOG_NAMES 5 +#define MAX_ADMIN_FLAGS 64 +#define MAX_ADMIN_COMMANDS 64 +#define MAX_ADMIN_CMD_LEN 20 +#define MAX_ADMIN_BAN_REASON 50 + +/* + * 1 - cannot be vote kicked, vote muted + * 2 - cannot be censored or flood protected TODO + * 3 - UNUSED + * 4 - can see team chat as a spectator + * 5 - can switch teams any time, regardless of balance + * 6 - does not need to specify a reason for a kick/ban + * 7 - can call a vote at any time (regardless of a vote being disabled or + * voting limitations) + * 8 - does not need to specify a duration for a ban + * 9 - can run commands from team chat + * 0 - inactivity rules do not apply to them + * ! - admin commands cannot be used on them + * @ - does not show up as an admin in !listplayers + */ +#define ADMF_IMMUNITY '1' +#define ADMF_NOCENSORFLOOD '2' /* TODO */ + +#define ADMF_SPEC_ALLCHAT '4' +#define ADMF_FORCETEAMCHANGE '5' +#define ADMF_UNACCOUNTABLE '6' +#define ADMF_NO_VOTE_LIMIT '7' +#define ADMF_CAN_PERM_BAN '8' +#define ADMF_TEAMFTCMD '9' +#define ADMF_ACTIVITY '0' + +#define ADMF_IMMUTABLE '!' +#define ADMF_INCOGNITO '@' + +#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, int skiparg ); + char *flag; + char *function; // used for !help + char *syntax; // used for !help +} +g_admin_cmd_t; + +typedef struct g_admin_level +{ + int level; + char name[ MAX_NAME_LENGTH ]; + char flags[ MAX_ADMIN_FLAGS ]; +} +g_admin_level_t; + +typedef struct g_admin_admin +{ + char guid[ 33 ]; + char name[ MAX_NAME_LENGTH ]; + int level; + char flags[ MAX_ADMIN_FLAGS ]; +} +g_admin_admin_t; + +typedef struct g_admin_ban +{ + char name[ MAX_NAME_LENGTH ]; + char guid[ 33 ]; + char ip[ 18 ]; + char reason[ MAX_ADMIN_BAN_REASON ]; + char made[ 18 ]; // big enough for strftime() %c + int expires; + char banner[ MAX_NAME_LENGTH ]; +} +g_admin_ban_t; + +typedef struct g_admin_command +{ + char command[ MAX_ADMIN_CMD_LEN ]; + char exec[ MAX_QPATH ]; + char desc[ 50 ]; + int levels[ MAX_ADMIN_LEVELS + 1 ]; +} +g_admin_command_t; + +typedef struct g_admin_namelog +{ + char name[ MAX_ADMIN_NAMELOG_NAMES ][MAX_NAME_LENGTH ]; + char ip[ 16 ]; + char guid[ 33 ]; + int slot; +} +g_admin_namelog_t; + +qboolean G_admin_ban_check( char *userinfo, char *reason, int rlen ); +qboolean G_admin_cmd_check( gentity_t *ent, qboolean say ); +qboolean G_admin_readconfig( gentity_t *ent, int skiparg ); +qboolean G_admin_permission( gentity_t *ent, char flag ); +qboolean G_admin_name_check( gentity_t *ent, char *name, char *err, int len ); +void G_admin_namelog_update( gclient_t *ent, int clientNum ); +int G_admin_level( gentity_t *ent ); + +// ! command functions +qboolean G_admin_time( gentity_t *ent, int skiparg ); +qboolean G_admin_setlevel( gentity_t *ent, int skiparg ); +qboolean G_admin_kick( gentity_t *ent, int skiparg ); +qboolean G_admin_ban( gentity_t *ent, int skiparg ); +qboolean G_admin_unban( gentity_t *ent, int skiparg ); +qboolean G_admin_putteam( gentity_t *ent, int skiparg ); +qboolean G_admin_listadmins( gentity_t *ent, int skiparg ); +qboolean G_admin_listplayers( gentity_t *ent, int skiparg ); +qboolean G_admin_mute( gentity_t *ent, int skiparg ); +qboolean G_admin_showbans( gentity_t *ent, int skiparg ); +qboolean G_admin_help( gentity_t *ent, int skiparg ); +qboolean G_admin_admintest( gentity_t *ent, int skiparg ); +qboolean G_admin_allready( gentity_t *ent, int skiparg ); +qboolean G_admin_cancelvote( gentity_t *ent, int skiparg ); +qboolean G_admin_passvote( gentity_t *ent, int skiparg ); +qboolean G_admin_spec999( gentity_t *ent, int skiparg ); +qboolean G_admin_rename( gentity_t *ent, int skiparg ); +qboolean G_admin_restart( gentity_t *ent, int skiparg ); +qboolean G_admin_nextmap( gentity_t *ent, int skiparg ); +qboolean G_admin_namelog( gentity_t *ent, int skiparg ); + +void G_admin_print( gentity_t *ent, char *m ); +void G_admin_buffer_print( gentity_t *ent, char *m ); +void G_admin_buffer_begin( void ); +void G_admin_buffer_end( gentity_t *ent ); + +void G_admin_duration( int secs, char *duration, int dursize ); +void G_admin_cleanup( void ); +void G_admin_namelog_cleanup( void ); + +#endif /* ifndef _G_ADMIN_H */ diff --git a/src/game/g_client.c b/src/game/g_client.c index 284ac2f3..217b97ed 100644 --- a/src/game/g_client.c +++ b/src/game/g_client.c @@ -951,6 +951,8 @@ void ClientUserinfoChanged( int clientNum ) char filename[ MAX_QPATH ]; char oldname[ MAX_STRING_CHARS ]; char newname[ MAX_STRING_CHARS ]; + char err[ MAX_STRING_CHARS ]; + qboolean revertName = qfalse; gclient_t *client; char c1[ MAX_INFO_STRING ]; char c2[ MAX_INFO_STRING ]; @@ -987,21 +989,49 @@ void ClientUserinfoChanged( int clientNum ) if( strcmp( oldname, newname ) ) { - // If not connected or time since name change has passed threshold, allow the change - if( client->pers.connected != CON_CONNECTED || - ( level.time - client->pers.nameChangeTime ) > ( g_minNameChangePeriod.integer * 1000 ) ) + // in case we need to revert and there's no oldname + if( client->pers.connected != CON_CONNECTED ) + Q_strncpyz( oldname, "UnnamedPlayer", sizeof( oldname ) ); + + if( client->pers.nameChangeTime && + ( level.time - client->pers.nameChangeTime ) + <= ( g_minNameChangePeriod.value * 1000 ) ) + { + trap_SendServerCommand( ent - g_entities, va( + "print \"Name change spam protection (g_minNameChangePeriod = %d)\n\"", + g_minNameChangePeriod.integer ) ); + revertName = qtrue; + } + else if( g_maxNameChanges.integer > 0 + && client->pers.nameChanges >= g_maxNameChanges.integer ) { - Q_strncpyz( client->pers.netname, newname, sizeof( client->pers.netname ) ); - client->pers.nameChangeTime = level.time; + trap_SendServerCommand( ent - g_entities, va( + "print \"Maximum name changes reached (g_maxNameChanges = %d)\n\"", + g_maxNameChanges.integer ) ); + 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, + sizeof( client->pers.netname ) ); + Info_SetValueForKey( userinfo, "name", oldname ); + trap_SetUserinfo( clientNum, userinfo ); } else { - // Note this leaves the client in a strange state where it has changed its "name" cvar - // but the server has refused to honour the change. In this case the client's cvar does - // not match the actual client's name any longer. This isn't so bad since really the - // only case where the name would be changing so fast is when it was being abused, and - // we don't really care if that kind of player screws their client up. - // Nevertheless, maybe FIXME this later. + Q_strncpyz( client->pers.netname, newname, + sizeof( client->pers.netname ) ); + if( client->pers.connected == CON_CONNECTED ) + { + client->pers.nameChangeTime = level.time; + client->pers.nameChanges++; + } } } @@ -1015,8 +1045,11 @@ void ClientUserinfoChanged( int clientNum ) { if( strcmp( oldname, client->pers.netname ) ) { - trap_SendServerCommand( -1, va( "print \"%s" S_COLOR_WHITE " renamed to %s\n\"", oldname, - client->pers.netname ) ); + trap_SendServerCommand( -1, va( "print \"%s" S_COLOR_WHITE + " renamed to %s\n\"", oldname, client->pers.netname ) ); + G_LogPrintf( "ClientRename: %i [%s] (%s) \"%s\" -> \"%s\"\n", clientNum, + client->pers.ip, client->pers.guid, oldname, client->pers.netname ); + G_admin_namelog_update( client, clientNum ); } } @@ -1145,16 +1178,39 @@ char *ClientConnect( int clientNum, qboolean firstTime ) gclient_t *client; char userinfo[ MAX_INFO_STRING ]; gentity_t *ent; + char guid[ 33 ]; + char ip[ 16 ] = {""}; + char reason[ MAX_STRING_CHARS ] = {""}; + int i; ent = &g_entities[ clientNum ]; trap_GetUserinfo( clientNum, userinfo, sizeof( userinfo ) ); + value = Info_ValueForKey( userinfo, "cl_guid" ); + Q_strncpyz( guid, value, sizeof( guid ) ); + + // check for admin ban + if( G_admin_ban_check( userinfo, reason, sizeof( reason ) ) ) + { + return va( "%s", reason ); + } + + // IP filtering // https://zerowing.idsoftware.com/bugzilla/show_bug.cgi?id=500 // recommanding PB based IP / GUID banning, the builtin system is pretty limited // check to see if they are on the banned IP list value = Info_ValueForKey( userinfo, "ip" ); + i = 0; + while( *value && i < sizeof( ip ) - 2 ) + { + if( *value != '.' && ( *value < '0' || *value > '9' ) ) + break; + ip[ i++ ] = *value; + value++; + } + ip[ i ] = '\0'; if( G_FilterPacket( value ) ) return "You are banned from this server."; @@ -1171,6 +1227,19 @@ char *ClientConnect( int clientNum, qboolean firstTime ) memset( client, 0, sizeof(*client) ); + // add guid to session so we don't have to keep parsing userinfo everywhere + if( !guid[0] ) + { + Q_strncpyz( client->pers.guid, "XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX", + sizeof( client->pers.guid ) ); + } + else + { + Q_strncpyz( client->pers.guid, guid, sizeof( client->pers.guid ) ); + } + Q_strncpyz( client->pers.ip, ip, sizeof( client->pers.ip ) ); + client->pers.adminLevel = G_admin_level( ent ); + client->pers.connected = CON_CONNECTING; // read or initialize the session data @@ -1180,8 +1249,9 @@ char *ClientConnect( int clientNum, qboolean firstTime ) G_ReadSessionData( client ); // get and distribute relevent paramters - G_LogPrintf( "ClientConnect: %i\n", clientNum ); ClientUserinfoChanged( clientNum ); + G_LogPrintf( "ClientConnect: %i [%s] (%s) \"%s\"\n", clientNum, + client->pers.ip, client->pers.guid, client->pers.netname ); // don't do the "xxx connected" messages if they were caried over from previous level if( firstTime ) @@ -1189,7 +1259,7 @@ char *ClientConnect( int clientNum, qboolean firstTime ) // count current clients and rank for scoreboard CalculateRanks( ); - + G_admin_namelog_update( client, clientNum ); return NULL; } @@ -1240,6 +1310,9 @@ void ClientBegin( int clientNum ) trap_SendServerCommand( -1, va( "print \"%s" S_COLOR_WHITE " entered the game\n\"", client->pers.netname ) ); + // name can change between ClientConnect() and ClientBegin() + G_admin_namelog_update( client, clientNum ); + // request the clients PTR code trap_SendServerCommand( ent - g_entities, "ptrcrequest" ); @@ -1583,6 +1656,8 @@ void ClientDisconnect( int clientNum ) if( !ent->client ) return; + + G_admin_namelog_update( ent->client, -1 ); // stop any following clients for( i = 0; i < level.maxclients; i++ ) @@ -1604,7 +1679,8 @@ void ClientDisconnect( int clientNum ) tent->s.clientNum = ent->s.clientNum; } - G_LogPrintf( "ClientDisconnect: %i\n", clientNum ); + G_LogPrintf( "ClientDisconnect: %i [%s] (%s) \"%s\"\n", clientNum, + ent->client->pers.ip, ent->client->pers.guid, ent->client->pers.netname ); trap_UnlinkEntity( ent ); ent->s.modelindex = 0; diff --git a/src/game/g_cmds.c b/src/game/g_cmds.c index 7bef68c2..d3701bea 100644 --- a/src/game/g_cmds.c +++ b/src/game/g_cmds.c @@ -32,9 +32,28 @@ Remove case and control characters from a player name */ void G_SanitiseName( char *in, char *out ) { + qboolean skip = qtrue; + int spaces = 0; + while( *in ) { - if( *in == 27 ) + // strip leading white space + if( *in == ' ' ) + { + if( skip ) + { + in++; + continue; + } + spaces++; + } + else + { + spaces = 0; + skip = qfalse; + } + + if( *in == 27 || *in == '^' ) { in += 2; // skip color code continue; @@ -48,7 +67,7 @@ void G_SanitiseName( char *in, char *out ) *out++ = tolower( *in++ ); } - + out -= spaces; *out = 0; } @@ -107,6 +126,119 @@ int G_ClientNumberFromString( gentity_t *to, char *s ) return -1; } + +/* +================== +G_MatchOnePlayer + +This is a companion function to G_ClientNumbersFromString() + +returns qtrue if the int array plist only has one client id, false otherwise +In the case of false, err will be populated with an error message. +================== +*/ +qboolean G_MatchOnePlayer( int *plist, char *err, int len ) +{ + gclient_t *cl; + int *p; + char line[ MAX_NAME_LENGTH + 10 ] = {""}; + + err[ 0 ] = '\0'; + if( plist[ 0 ] == -1 ) + { + Q_strcat( err, len, "no connected player by that name or slot #" ); + return qfalse; + } + if( plist[ 1 ] != -1 ) + { + Q_strcat( err, len, "more than one player name matches. " + "be more specific or use the slot #:\n" ); + for( p = plist; *p != -1; p++ ) + { + cl = &level.clients[ *p ]; + if( cl->pers.connected == CON_CONNECTED ) + { + Com_sprintf( line, sizeof( line ), "%2i - %s^7\n", + *p, cl->pers.netname ); + if( strlen( err ) + strlen( line ) > len ) + break; + Q_strcat( err, len, line ); + } + } + return qfalse; + } + return qtrue; +} + +/* +================== +G_ClientNumbersFromString + +Sets plist to an array of integers that represent client numbers that have +names that are a partial match for s. List is terminated by a -1. + +Returns number of matching clientids. +================== +*/ +int G_ClientNumbersFromString( char *s, int *plist ) +{ + gclient_t *p; + int i, found = 0; + char n2[ MAX_NAME_LENGTH ] = {""}; + char s2[ MAX_NAME_LENGTH ] = {""}; + qboolean is_slot = qtrue; + + *plist = -1; + + // if a number is provided, it might be a slot # + for( i = 0; i < (int)strlen( s ); i++ ) + { + if( s[i] < '0' || s[i] > '9' ) + { + is_slot = qfalse; + break; + } + } + + if( is_slot ) { + i = atoi( s ); + if( i >= 0 && i < level.maxclients ) { + p = &level.clients[ i ]; + if( p->pers.connected == CON_CONNECTED || + p->pers.connected == CON_CONNECTING ) + { + *plist++ = i; + *plist = -1; + return 1; + } + } + // we must assume that if only a number is provided, it is a clientNum + return 0; + } + + // now look for name matches + G_SanitiseName( s, s2 ); + if( strlen( s2 ) < 1 ) + return 0; + for( i = 0; i < level.maxclients; i++ ) + { + p = &level.clients[ i ]; + if(p->pers.connected != CON_CONNECTED + && p->pers.connected != CON_CONNECTING) + { + continue; + } + G_SanitiseName( p->pers.netname, n2 ); + if( strstr( n2, s2 ) ) + { + *plist++ = i; + found++; + } + } + *plist = -1; + return found; +} + /* ================== ScoreboardMessage @@ -508,6 +640,7 @@ void Cmd_Team_f( gentity_t *ent ) { pTeam_t team; char s[ MAX_TOKEN_CHARS ]; + qboolean force = G_admin_permission(ent, ADMF_FORCETEAMCHANGE); trap_Argv( 1, s, sizeof( s ) ); @@ -521,7 +654,8 @@ void Cmd_Team_f( gentity_t *ent ) team = PTE_NONE; else if( !Q_stricmp( s, "aliens" ) ) { - if( g_teamForceBalance.integer && ( ( level.numAlienClients > level.numHumanClients ) || + if( !force && g_teamForceBalance.integer + && ( ( level.numAlienClients > level.numHumanClients ) || ( ent->client->ps.stats[ STAT_PTEAM ] == PTE_HUMANS && level.numAlienClients >= level.numHumanClients ) ) ) { @@ -533,7 +667,8 @@ void Cmd_Team_f( gentity_t *ent ) } else if( !Q_stricmp( s, "humans" ) ) { - if( g_teamForceBalance.integer && ( ( level.numHumanClients > level.numAlienClients ) || + if( !force && g_teamForceBalance.integer && + ( ( level.numHumanClients > level.numAlienClients ) || ( ent->client->ps.stats[ STAT_PTEAM ] == PTE_ALIENS && level.numHumanClients >= level.numAlienClients ) ) ) { @@ -587,7 +722,15 @@ static void G_SayTo( gentity_t *ent, gentity_t *other, int mode, int color, cons return; if( mode == SAY_TEAM && !OnSameTeam( ent, other ) ) - return; + { + if( other->client->ps.stats[ STAT_PTEAM ] != PTE_NONE ) + return; + + if( !G_admin_permission( other, ADMF_SPEC_ALLCHAT ) ) + return; + + // specs with ADMF_SPEC_ALLCHAT flag can see team chat + } trap_SendServerCommand( other-g_entities, va( "%s \"%s%c%c%s\"", mode == SAY_TEAM ? "tchat" : "chat", @@ -677,6 +820,11 @@ void G_Say( gentity_t *ent, gentity_t *target, int mode, const char *chatText ) other = &g_entities[ j ]; G_SayTo( ent, other, mode, color, name, text ); } + + if( g_adminParseSay.integer ) + { + G_admin_cmd_check ( ent, qtrue ); + } } @@ -689,6 +837,11 @@ static void Cmd_Say_f( gentity_t *ent, int mode, qboolean arg0 ) { char *p; + if( ent->client->pers.muted ) + { + return; + } + if( trap_Argc( ) < 2 && !arg0 ) return; @@ -769,7 +922,8 @@ void Cmd_CallVote_f( gentity_t *ent ) } if( g_voteLimit.integer > 0 - && ent->client->pers.voteCount >= g_voteLimit.integer ) + && ent->client->pers.voteCount >= g_voteLimit.integer + && !G_admin_permission( ent, ADMF_NO_VOTE_LIMIT ) ) { trap_SendServerCommand( ent-g_entities, va( "print \"You have already called the maxium number of votes (%d)\n\"", @@ -802,17 +956,30 @@ void Cmd_CallVote_f( gentity_t *ent ) if( !Q_stricmp( arg1, "kick" ) ) { - char kickee[ MAX_NETNAME ]; - - Q_strncpyz( kickee, arg2, sizeof( kickee ) ); - Q_CleanStr( kickee ); - - Com_sprintf( level.voteString, sizeof( level.voteString ), - "%s \"%s\"", arg1, kickee ); - Com_sprintf( level.voteDisplayString, sizeof( level.voteDisplayString ), - "Kick player \'%s\'", kickee ); + int clientNum; + int clientNums[ MAX_CLIENTS ] = { -1 }; + + if( G_ClientNumbersFromString( arg2, clientNums ) == 1 ) + { + // there was one partial name match name was clientNum + clientNum = clientNums[ 0 ]; + } + else + { + // look for an exact name match before bailing out + clientNum = G_ClientNumberFromString( ent, arg2 ); + if( clientNum == -1 ) + { + trap_SendServerCommand( ent-g_entities, + "print \"callvote: invalid player\n\"" ); + return; + } + } + Q_strncpyz( arg1, "clientkick", sizeof( arg1 ) ); + Q_strncpyz( arg2, va( "%d", clientNum ), sizeof( arg2 ) ); } - else if( !Q_stricmp( arg1, "clientkick" ) ) + + if( !Q_stricmp( arg1, "clientkick" ) ) { char kickee[ MAX_NETNAME ]; int clientNum = 0; @@ -827,19 +994,34 @@ void Cmd_CallVote_f( gentity_t *ent ) } } - if( clientNum >= 0 ) + if( clientNum >= 0 && clientNum < level.maxclients ) { clientNum = atoi( arg2 ); + if( G_admin_permission( &g_entities[ clientNum ], ADMF_IMMUNITY ) ) + { + trap_SendServerCommand( ent-g_entities, + "print \"callvote: admin is immune from vote kick\n\"" ); + return; + } if( level.clients[ clientNum ].pers.connected != CON_DISCONNECTED ) { - Q_strncpyz( kickee, level.clients[ clientNum ].pers.netname, sizeof( kickee ) ); + Q_strncpyz( kickee, level.clients[ clientNum ].pers.netname, + sizeof( kickee ) ); Q_CleanStr( kickee ); - - Com_sprintf( level.voteString, sizeof( level.voteString ), + if( g_admin.string[ 0 ] ) + { + // !kick will add a temp ban and a descriptive drop message + Com_sprintf( level.voteString, sizeof( level.voteString ), + "!kick %d vote kick", clientNum ); + } + else + { + Com_sprintf( level.voteString, sizeof( level.voteString ), "clientkick %d", clientNum ); + } Com_sprintf( level.voteDisplayString, sizeof( level.voteDisplayString ), - "Kick player \'%s\'", kickee ); + "Kick player \'%s\'", kickee ); } else return; @@ -975,7 +1157,8 @@ void Cmd_CallTeamVote_f( gentity_t *ent ) } if( g_voteLimit.integer > 0 - && ent->client->pers.voteCount >= g_voteLimit.integer ) + && ent->client->pers.voteCount >= g_voteLimit.integer + && !G_admin_permission( ent, ADMF_NO_VOTE_LIMIT ) ) { trap_SendServerCommand( ent-g_entities, va( "print \"You have already called the maxium number of votes (%d)\n\"", @@ -1001,39 +1184,30 @@ void Cmd_CallTeamVote_f( gentity_t *ent ) if( !Q_stricmp( arg1, "teamkick" ) ) { - char netname[ MAX_NETNAME ], kickee[ MAX_NETNAME ]; - - Q_strncpyz( kickee, arg2, sizeof( kickee ) ); - Q_CleanStr( kickee ); - - for( i = 0; i < level.maxclients; i++ ) + int clientNum; + int clientNums[ MAX_CLIENTS ] = { -1 }; + + if( G_ClientNumbersFromString( arg2, clientNums ) == 1 ) { - if( level.clients[ i ].pers.connected == CON_DISCONNECTED ) - continue; - - if( level.clients[ i ].ps.stats[ STAT_PTEAM ] != team ) - continue; - - Q_strncpyz( netname, level.clients[ i ].pers.netname, sizeof( netname ) ); - Q_CleanStr( netname ); - - if( !Q_stricmp( netname, kickee ) ) - break; + // there was one partial name match or name was clientNum + clientNum = clientNums[ 0 ]; } - - if( i >= level.maxclients ) + else { - trap_SendServerCommand( ent-g_entities, va( "print \"\'%s\' " - S_COLOR_WHITE "is not a valid player on your team\n\"", arg2 ) ); - return; + // look for an exact name match before bailing out + clientNum = G_ClientNumberFromString( ent, arg2 ); + if( clientNum == -1 ) + { + trap_SendServerCommand( ent-g_entities, + "print \"callvote: invalid player\n\"" ); + return; + } } - - Com_sprintf( level.teamVoteString[ cs_offset ], - sizeof( level.teamVoteString[ cs_offset ] ), "kick \"%s\"", kickee ); - Com_sprintf( level.teamVoteDisplayString[ cs_offset ], - sizeof( level.teamVoteDisplayString[ cs_offset ] ), "Kick player \'%s\'", kickee ); + Q_strncpyz( arg1, "teamclientkick", sizeof( arg1 ) ); + Q_strncpyz( arg2, va( "%d", clientNum ), sizeof( arg2 ) ); } - else if( !Q_stricmp( arg1, "teamclientkick" ) ) + + if( !Q_stricmp( arg1, "teamclientkick" ) ) { int clientNum = 0; char kickee[ MAX_NETNAME ]; @@ -1048,7 +1222,7 @@ void Cmd_CallTeamVote_f( gentity_t *ent ) } } - if( clientNum >= 0 ) + if( clientNum >= 0 && clientNum < level.maxclients ) { clientNum = atoi( arg2 ); @@ -1075,13 +1249,33 @@ void Cmd_CallTeamVote_f( gentity_t *ent ) return; } - Q_strncpyz( kickee, level.clients[ clientNum ].pers.netname, sizeof( kickee ) ); + if( G_admin_permission( &g_entities[ clientNum ], ADMF_IMMUNITY ) ) + { + trap_SendServerCommand( ent-g_entities, + "print \"callteamvote: admin is immune from vote kick\n\"" ); + return; + } + + Q_strncpyz( kickee, level.clients[ clientNum ].pers.netname, + sizeof( kickee ) ); Q_CleanStr( kickee ); - Com_sprintf( level.teamVoteString[ cs_offset ], - sizeof( level.teamVoteString[ cs_offset ] ), "clientkick %d", clientNum ); + if( g_admin.string[ 0 ] ) + { + // !kick will add a temp ban and a descriptive drop message + Com_sprintf( level.teamVoteString[ cs_offset ], + sizeof( level.teamVoteString[ cs_offset ] ), + "!kick %d team vote kick", clientNum ); + } + else + { + Com_sprintf( level.teamVoteString[ cs_offset ], + sizeof( level.teamVoteString[ cs_offset ] ), + "clientkick %d", clientNum ); + } Com_sprintf( level.teamVoteDisplayString[ cs_offset ], - sizeof( level.teamVoteDisplayString[ cs_offset ] ), "Kick player \'%s\'", kickee ); + sizeof( level.teamVoteDisplayString[ cs_offset ] ), + "Kick player \'%s\'", kickee ); } else { @@ -2376,6 +2570,9 @@ void ClientCommand( int clientNum ) return; } + if( G_admin_cmd_check( ent, qfalse ) ) + return; + // ignore all other commands when at intermission if( level.intermissiontime ) return; @@ -2443,3 +2640,118 @@ void ClientCommand( int clientNum ) else trap_SendServerCommand( clientNum, va( "print \"unknown cmd %s\n\"", cmd ) ); } + +int G_SayArgc() +{ + int c = 1; + char *s; + + s = ConcatArgs( 0 ); + if( !*s ) + return 0; + while( *s ) + { + if( *s == ' ' ) + { + s++; + if( *s != ' ' ) + { + c++; + continue; + } + while( *s && *s == ' ' ) + s++; + c++; + } + s++; + } + return c; +} + +qboolean G_SayArgv( int n, char *buffer, int bufferLength ) +{ + int bc = 1; + int c = 0; + char *s; + + if( bufferLength < 1 ) + return qfalse; + if(n < 0) + return qfalse; + *buffer = '\0'; + s = ConcatArgs( 0 ); + while( *s ) + { + if( c == n ) + { + while( *s && ( bc < bufferLength ) ) + { + if( *s == ' ' ) + { + *buffer = '\0'; + return qtrue; + } + *buffer = *s; + buffer++; + s++; + bc++; + } + *buffer = '\0'; + return qtrue; + } + if( *s == ' ' ) + { + s++; + if( *s != ' ' ) + { + c++; + continue; + } + while( *s && *s == ' ' ) + s++; + c++; + } + s++; + } + return qfalse; +} + +char *G_SayConcatArgs(int start) +{ + char *s; + int c = 0; + + s = ConcatArgs( 0 ); + while( *s ) { + if( c == start ) + return s; + if( *s == ' ' ) + { + s++; + if( *s != ' ' ) + { + c++; + continue; + } + while( *s && *s == ' ' ) + s++; + c++; + } + s++; + } + return s; +} + +void G_DecolorString( char *in, char *out ) +{ + while( *in ) { + if( *in == 27 || *in == '^' ) { + in++; + if( *in ) + in++; + continue; + } + *out++ = *in++; + } + *out = '\0'; +} diff --git a/src/game/g_local.h b/src/game/g_local.h index 07a32ab2..54276cea 100644 --- a/src/game/g_local.h +++ b/src/game/g_local.h @@ -27,6 +27,11 @@ Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA #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 @@ -71,9 +76,6 @@ typedef enum //============================================================================ -typedef struct gentity_s gentity_t; -typedef struct gclient_s gclient_t; - struct gentity_s { entityState_t s; // communicated by server to clients @@ -336,8 +338,13 @@ typedef struct connectionRecord_t *connection; int nameChangeTime; + int nameChanges; vec3_t lastDeathLocation; + char guid[ 33 ]; + char ip[ 16 ]; + qboolean muted; + int adminLevel; } clientPersistant_t; // this structure is cleared on each ClientSpawn(), @@ -635,6 +642,14 @@ void Cmd_Score_f( gentity_t *ent ); void G_StopFollowing( gentity_t *ent ); qboolean G_FollowNewClient( gentity_t *ent, int dir ); void Cmd_Follow_f( gentity_t *ent, qboolean toggle ); +qboolean G_MatchOnePlayer( int *plist, char *err, int len ); +int G_ClientNumbersFromString( char *s, int *plist ); +int G_SayArgc( void ); +qboolean G_SayArgv( int n, char *buffer, int bufferLength ); +char *G_SayConcatArgs( int start ); +void G_DecolorString( char *in, char *out ); +void G_ChangeTeam( gentity_t *ent, pTeam_t newTeam ); +void G_SanitiseName( char *in, char *out ); // // g_physics.c @@ -875,6 +890,9 @@ 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 CheckVote( void ); +void CheckTeamVote( int teamnum ); +void LogExit( const char *string ); // // g_client.c @@ -1021,6 +1039,7 @@ extern vmCvar_t g_maxGameClients; // allow this many active extern vmCvar_t g_restarted; extern vmCvar_t g_minCommandPeriod; extern vmCvar_t g_minNameChangePeriod; +extern vmCvar_t g_maxNameChanges; extern vmCvar_t g_timelimit; extern vmCvar_t g_suddenDeathTime; @@ -1085,9 +1104,16 @@ extern vmCvar_t g_chatTeamPrefix; extern vmCvar_t g_mapConfigs; +extern vmCvar_t g_admin; +extern vmCvar_t g_adminLog; +extern vmCvar_t g_adminParseSay; +extern vmCvar_t g_adminNameProtect; +extern vmCvar_t g_adminTempBan; + void trap_Printf( const char *fmt ); void trap_Error( const char *fmt ); int trap_Milliseconds( void ); +int trap_RealTime( qtime_t *qtime ); int trap_Argc( void ); void trap_Argv( int n, char *buffer, int bufferLength ); void trap_Args( char *buffer, int bufferLength ); diff --git a/src/game/g_main.c b/src/game/g_main.c index b0e1bdeb..2d2a1427 100644 --- a/src/game/g_main.c +++ b/src/game/g_main.c @@ -87,6 +87,7 @@ vmCvar_t g_rankings; vmCvar_t g_listEntity; vmCvar_t g_minCommandPeriod; vmCvar_t g_minNameChangePeriod; +vmCvar_t g_maxNameChanges; //TA vmCvar_t g_humanBuildPoints; @@ -114,6 +115,12 @@ vmCvar_t g_initialMapRotation; vmCvar_t g_mapConfigs; vmCvar_t g_chatTeamPrefix; +vmCvar_t g_admin; +vmCvar_t g_adminLog; +vmCvar_t g_adminParseSay; +vmCvar_t g_adminNameProtect; +vmCvar_t g_adminTempBan; + static cvarTable_t gameCvarTable[ ] = { // don't override the cheat state set by the system @@ -181,6 +188,7 @@ static cvarTable_t gameCvarTable[ ] = { &g_listEntity, "g_listEntity", "0", 0, 0, qfalse }, { &g_minCommandPeriod, "g_minCommandPeriod", "500", 0, 0, qfalse}, { &g_minNameChangePeriod, "g_minNameChangePeriod", "5", 0, 0, qfalse}, + { &g_maxNameChanges, "g_maxNameChanges", "5", 0, 0, qfalse}, { &g_smoothClients, "g_smoothClients", "1", 0, 0, qfalse}, { &pmove_fixed, "pmove_fixed", "0", CVAR_SYSTEMINFO, 0, qfalse}, @@ -212,6 +220,12 @@ static cvarTable_t gameCvarTable[ ] = { &g_mapConfigs, "g_mapConfigs", "", CVAR_ARCHIVE, 0, qfalse }, { NULL, "g_mapConfigsLoaded", "0", CVAR_ROM, 0, qfalse }, + { &g_admin, "g_admin", "admin.dat", CVAR_ARCHIVE, 0, qfalse }, + { &g_adminLog, "g_adminLog", "admin.log", CVAR_ARCHIVE, 0, qfalse }, + { &g_adminParseSay, "g_adminParseSay", "1", CVAR_ARCHIVE, 0, qfalse }, + { &g_adminNameProtect, "g_adminNameProtect", "1", CVAR_ARCHIVE, 0, qfalse }, + { &g_adminTempBan, "g_adminTempBan", "120", CVAR_ARCHIVE, 0, qfalse }, + { &g_rankings, "g_rankings", "0", 0, 0, qfalse} }; @@ -526,6 +540,10 @@ void G_InitGame( int levelTime, int randomSeed, int restart ) // we're done with g_mapConfigs, so reset this for the next map trap_Cvar_Set( "g_mapConfigsLoaded", "0" ); + if ( g_admin.string[ 0 ] ) { + G_admin_readconfig( NULL, 0 ); + } + // initialize all entities for this game memset( g_entities, 0, MAX_GENTITIES * sizeof( g_entities[ 0 ] ) ); level.gentities = g_entities; @@ -606,6 +624,9 @@ void G_ShutdownGame( int restart ) // write all the client session data so we can get it back G_WriteSessionData( ); + + G_admin_cleanup( ); + G_admin_namelog_cleanup( ); } diff --git a/src/game/g_mem.c b/src/game/g_mem.c index 3631d06b..69351940 100644 --- a/src/game/g_mem.c +++ b/src/game/g_mem.c @@ -23,7 +23,7 @@ Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA #include "g_local.h" -#define POOLSIZE (256 * 1024) +#define POOLSIZE ( 1024 * 1024 ) #define FREEMEMCOOKIE ((int)0xDEADBE3F) // Any unlikely to be used value #define ROUNDBITS 31 // Round to 32 bytes diff --git a/src/game/g_svcmds.c b/src/game/g_svcmds.c index 63234aba..df3824fb 100644 --- a/src/game/g_svcmds.c +++ b/src/game/g_svcmds.c @@ -576,6 +576,10 @@ qboolean ConsoleCommand( void ) return qtrue; } + + // see if this is a a admin command + if( G_admin_cmd_check( NULL, qfalse ) ) + return qtrue; if( g_dedicated.integer ) { -- cgit