summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorTim Angus <tim@ngus.net>2006-07-31 21:50:12 +0000
committerTim Angus <tim@ngus.net>2006-07-31 21:50:12 +0000
commit68da0ee4bead3a36b5057471a30414f11cc01577 (patch)
treee2c647fc80d7559fd432ee4bf3edecb356c86ee1
parentae15fd3f111ba37c32a273e65b6a2ee35b579c86 (diff)
* tjw's spiffy admin system (tjw, obviously)
-rw-r--r--Makefile1
-rw-r--r--misc/manual.lyx1049
-rw-r--r--src/game/g_admin.c2666
-rw-r--r--src/game/g_admin.h174
-rw-r--r--src/game/g_client.c108
-rw-r--r--src/game/g_cmds.c422
-rw-r--r--src/game/g_local.h32
-rw-r--r--src/game/g_main.c21
-rw-r--r--src/game/g_mem.c2
-rw-r--r--src/game/g_svcmds.c4
10 files changed, 4404 insertions, 75 deletions
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
+<lyxtabular version="3" rows="5" columns="2">
+<features islongtable="true">
+<column alignment="left" valignment="top" width="0">
+<column alignment="block" valignment="top" leftline="true" width="5.5cm">
+<row>
+<cell multicolumn="1" alignment="left" valignment="top" usebox="none">
+\begin_inset Text
+
+\layout Standard
+
+g_admin <string>
+\end_inset
+</cell>
+<cell alignment="center" valignment="top" usebox="none">
+\begin_inset Text
+
+\layout Standard
+
+Set <string> 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
+</cell>
+</row>
+<row topline="true" bottomline="true">
+<cell alignment="center" valignment="top" topline="true" leftline="true" usebox="none">
+\begin_inset Text
+
+\layout Standard
+
+g_adminLog <string>
+\end_inset
+</cell>
+<cell alignment="center" valignment="top" topline="true" leftline="true" rightline="true" usebox="none">
+\begin_inset Text
+
+\layout Standard
+
+Set <string> 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
+</cell>
+</row>
+<row>
+<cell alignment="center" valignment="top" topline="true" leftline="true" usebox="none">
+\begin_inset Text
+
+\layout Standard
+
+g_adminParseSay <integer>
+\end_inset
+</cell>
+<cell alignment="center" valignment="top" topline="true" leftline="true" rightline="true" usebox="none">
+\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
+</cell>
+</row>
+<row>
+<cell alignment="center" valignment="top" topline="true" leftline="true" usebox="none">
+\begin_inset Text
+
+\layout Standard
+
+g_adminNameProtect <integer>
+\end_inset
+</cell>
+<cell alignment="center" valignment="top" topline="true" leftline="true" rightline="true" usebox="none">
+\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
+</cell>
+</row>
+<row>
+<cell alignment="center" valignment="top" topline="true" leftline="true" usebox="none">
+\begin_inset Text
+
+\layout Standard
+
+g_adminTempBan
+\end_inset
+</cell>
+<cell alignment="center" valignment="top" topline="true" leftline="true" rightline="true" usebox="none">
+\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
+</cell>
+</row>
+</lyxtabular>
+
+\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
+<lyxtabular version="3" rows="20" columns="2">
+<features islongtable="true">
+<column alignment="center" valignment="top" leftline="true" width="0">
+<column alignment="left" valignment="top" leftline="true" rightline="true" width="0">
+<row topline="true" bottomline="true">
+<cell alignment="center" valignment="top" topline="true" leftline="true" usebox="none">
+\begin_inset Text
+
+\layout Standard
+
+FLAG
+\end_inset
+</cell>
+<cell alignment="center" valignment="top" topline="true" leftline="true" rightline="true" usebox="none">
+\begin_inset Text
+
+\layout Standard
+
+COMMAND
+\end_inset
+</cell>
+</row>
+<row topline="true">
+<cell alignment="center" valignment="top" topline="true" leftline="true" usebox="none">
+\begin_inset Text
+
+\layout Standard
+
+a
+\end_inset
+</cell>
+<cell alignment="center" valignment="top" topline="true" leftline="true" rightline="true" usebox="none">
+\begin_inset Text
+
+\layout Standard
+
+!admintest
+\end_inset
+</cell>
+</row>
+<row topline="true">
+<cell alignment="center" valignment="top" topline="true" leftline="true" usebox="none">
+\begin_inset Text
+
+\layout Standard
+
+y
+\end_inset
+</cell>
+<cell alignment="center" valignment="top" topline="true" leftline="true" rightline="true" usebox="none">
+\begin_inset Text
+
+\layout Standard
+
+!allready
+\end_inset
+</cell>
+</row>
+<row topline="true">
+<cell alignment="center" valignment="top" topline="true" leftline="true" usebox="none">
+\begin_inset Text
+
+\layout Standard
+
+b
+\end_inset
+</cell>
+<cell alignment="center" valignment="top" topline="true" leftline="true" rightline="true" usebox="none">
+\begin_inset Text
+
+\layout Standard
+
+!ban/!unban
+\end_inset
+</cell>
+</row>
+<row topline="true">
+<cell alignment="center" valignment="top" topline="true" leftline="true" usebox="none">
+\begin_inset Text
+
+\layout Standard
+
+c
+\end_inset
+</cell>
+<cell alignment="center" valignment="top" topline="true" leftline="true" rightline="true" usebox="none">
+\begin_inset Text
+
+\layout Standard
+
+!cancelvote/!passvote
+\end_inset
+</cell>
+</row>
+<row topline="true">
+<cell alignment="center" valignment="top" topline="true" leftline="true" usebox="none">
+\begin_inset Text
+
+\layout Standard
+
+h
+\end_inset
+</cell>
+<cell alignment="center" valignment="top" topline="true" leftline="true" rightline="true" usebox="none">
+\begin_inset Text
+
+\layout Standard
+
+!help
+\end_inset
+</cell>
+</row>
+<row topline="true">
+<cell alignment="center" valignment="top" topline="true" leftline="true" usebox="none">
+\begin_inset Text
+
+\layout Standard
+
+k
+\end_inset
+</cell>
+<cell alignment="center" valignment="top" topline="true" leftline="true" rightline="true" usebox="none">
+\begin_inset Text
+
+\layout Standard
+
+!kick
+\end_inset
+</cell>
+</row>
+<row topline="true">
+<cell alignment="center" valignment="top" topline="true" leftline="true" usebox="none">
+\begin_inset Text
+
+\layout Standard
+
+D
+\end_inset
+</cell>
+<cell alignment="center" valignment="top" topline="true" leftline="true" rightline="true" usebox="none">
+\begin_inset Text
+
+\layout Standard
+
+!listadmins
+\end_inset
+</cell>
+</row>
+<row topline="true">
+<cell alignment="center" valignment="top" topline="true" leftline="true" usebox="none">
+\begin_inset Text
+
+\layout Standard
+
+i
+\end_inset
+</cell>
+<cell alignment="center" valignment="top" topline="true" leftline="true" rightline="true" usebox="none">
+\begin_inset Text
+
+\layout Standard
+
+!listplayers
+\end_inset
+</cell>
+</row>
+<row topline="true">
+<cell alignment="center" valignment="top" topline="true" leftline="true" usebox="none">
+\begin_inset Text
+
+\layout Standard
+
+m
+\end_inset
+</cell>
+<cell alignment="center" valignment="top" topline="true" leftline="true" rightline="true" usebox="none">
+\begin_inset Text
+
+\layout Standard
+
+!mute/!unmute
+\end_inset
+</cell>
+</row>
+<row topline="true">
+<cell alignment="center" valignment="top" topline="true" leftline="true" usebox="none">
+\begin_inset Text
+
+\layout Standard
+
+e
+\end_inset
+</cell>
+<cell alignment="center" valignment="top" topline="true" leftline="true" rightline="true" usebox="none">
+\begin_inset Text
+
+\layout Standard
+
+!namelog
+\end_inset
+</cell>
+</row>
+<row topline="true">
+<cell alignment="center" valignment="top" topline="true" leftline="true" usebox="none">
+\begin_inset Text
+
+\layout Standard
+
+n
+\end_inset
+</cell>
+<cell alignment="center" valignment="top" topline="true" leftline="true" rightline="true" usebox="none">
+\begin_inset Text
+
+\layout Standard
+
+!nextmap
+\end_inset
+</cell>
+</row>
+<row topline="true">
+<cell alignment="center" valignment="top" topline="true" leftline="true" usebox="none">
+\begin_inset Text
+
+\layout Standard
+
+p
+\end_inset
+</cell>
+<cell alignment="center" valignment="top" topline="true" leftline="true" rightline="true" usebox="none">
+\begin_inset Text
+
+\layout Standard
+
+!putteam
+\end_inset
+</cell>
+</row>
+<row topline="true">
+<cell alignment="center" valignment="top" topline="true" leftline="true" usebox="none">
+\begin_inset Text
+
+\layout Standard
+
+G
+\end_inset
+</cell>
+<cell alignment="center" valignment="top" topline="true" leftline="true" rightline="true" usebox="none">
+\begin_inset Text
+
+\layout Standard
+
+!readconfig
+\end_inset
+</cell>
+</row>
+<row topline="true">
+<cell alignment="center" valignment="top" topline="true" leftline="true" usebox="none">
+\begin_inset Text
+
+\layout Standard
+
+N
+\end_inset
+</cell>
+<cell alignment="center" valignment="top" topline="true" leftline="true" rightline="true" usebox="none">
+\begin_inset Text
+
+\layout Standard
+
+!rename
+\end_inset
+</cell>
+</row>
+<row topline="true">
+<cell alignment="center" valignment="top" topline="true" leftline="true" usebox="none">
+\begin_inset Text
+
+\layout Standard
+
+r
+\end_inset
+</cell>
+<cell alignment="center" valignment="top" topline="true" leftline="true" rightline="true" usebox="none">
+\begin_inset Text
+
+\layout Standard
+
+!restart
+\end_inset
+</cell>
+</row>
+<row topline="true">
+<cell alignment="center" valignment="top" topline="true" leftline="true" usebox="none">
+\begin_inset Text
+
+\layout Standard
+
+s
+\end_inset
+</cell>
+<cell alignment="center" valignment="top" topline="true" leftline="true" rightline="true" usebox="none">
+\begin_inset Text
+
+\layout Standard
+
+!setlevel
+\end_inset
+</cell>
+</row>
+<row topline="true">
+<cell alignment="center" valignment="top" topline="true" leftline="true" usebox="none">
+\begin_inset Text
+
+\layout Standard
+
+B
+\end_inset
+</cell>
+<cell alignment="center" valignment="top" topline="true" leftline="true" rightline="true" usebox="none">
+\begin_inset Text
+
+\layout Standard
+
+!showbans
+\end_inset
+</cell>
+</row>
+<row topline="true" bottomline="true">
+<cell alignment="center" valignment="top" topline="true" leftline="true" usebox="none">
+\begin_inset Text
+
+\layout Standard
+
+P
+\end_inset
+</cell>
+<cell alignment="center" valignment="top" topline="true" leftline="true" rightline="true" usebox="none">
+\begin_inset Text
+
+\layout Standard
+
+!spec999
+\end_inset
+</cell>
+</row>
+<row bottomline="true">
+<cell alignment="center" valignment="top" topline="true" leftline="true" usebox="none">
+\begin_inset Text
+
+\layout Standard
+
+C
+\end_inset
+</cell>
+<cell alignment="center" valignment="top" topline="true" leftline="true" rightline="true" usebox="none">
+\begin_inset Text
+
+\layout Standard
+
+!time
+\end_inset
+</cell>
+</row>
+</lyxtabular>
+
+\end_inset
+
+
+\layout Standard
+
+The following table shows the flags for RIGHTS:
+\layout Standard
+
+
+\begin_inset Tabular
+<lyxtabular version="3" rows="11" columns="2">
+<features islongtable="true">
+<column alignment="center" valignment="top" leftline="true" width="0">
+<column alignment="left" valignment="top" leftline="true" rightline="true" width="0">
+<row topline="true" bottomline="true">
+<cell alignment="center" valignment="top" topline="true" leftline="true" usebox="none">
+\begin_inset Text
+
+\layout Standard
+
+FLAG
+\end_inset
+</cell>
+<cell alignment="center" valignment="top" topline="true" leftline="true" rightline="true" usebox="none">
+\begin_inset Text
+
+\layout Standard
+
+RIGHT
+\end_inset
+</cell>
+</row>
+<row topline="true">
+<cell alignment="center" valignment="top" topline="true" leftline="true" usebox="none">
+\begin_inset Text
+
+\layout Standard
+
+1
+\end_inset
+</cell>
+<cell alignment="center" valignment="top" topline="true" leftline="true" rightline="true" usebox="none">
+\begin_inset Text
+
+\layout Standard
+
+cannot be vote kicked
+\end_inset
+</cell>
+</row>
+<row topline="true">
+<cell alignment="center" valignment="top" topline="true" leftline="true" usebox="none">
+\begin_inset Text
+
+\layout Standard
+
+4
+\end_inset
+</cell>
+<cell alignment="center" valignment="top" topline="true" leftline="true" rightline="true" usebox="none">
+\begin_inset Text
+
+\layout Standard
+
+can see team chat as a spectator
+\end_inset
+</cell>
+</row>
+<row topline="true">
+<cell alignment="center" valignment="top" topline="true" leftline="true" usebox="none">
+\begin_inset Text
+
+\layout Standard
+
+5
+\end_inset
+</cell>
+<cell alignment="center" valignment="top" topline="true" leftline="true" rightline="true" usebox="none">
+\begin_inset Text
+
+\layout Standard
+
+can switch teams regardless of balance settings
+\end_inset
+</cell>
+</row>
+<row topline="true">
+<cell alignment="center" valignment="top" topline="true" leftline="true" usebox="none">
+\begin_inset Text
+
+\layout Standard
+
+6
+\end_inset
+</cell>
+<cell alignment="center" valignment="top" topline="true" leftline="true" rightline="true" usebox="none">
+\begin_inset Text
+
+\layout Standard
+
+does not need to specify a reason for kick/ban
+\end_inset
+</cell>
+</row>
+<row topline="true">
+<cell alignment="center" valignment="top" topline="true" leftline="true" usebox="none">
+\begin_inset Text
+
+\layout Standard
+
+7
+\end_inset
+</cell>
+<cell alignment="center" valignment="top" topline="true" leftline="true" rightline="true" usebox="none">
+\begin_inset Text
+
+\layout Standard
+
+can call a vote at any time regardless of g_voteLimit
+\end_inset
+</cell>
+</row>
+<row topline="true">
+<cell alignment="center" valignment="top" topline="true" leftline="true" usebox="none">
+\begin_inset Text
+
+\layout Standard
+
+8
+\end_inset
+</cell>
+<cell alignment="center" valignment="top" topline="true" leftline="true" rightline="true" usebox="none">
+\begin_inset Text
+
+\layout Standard
+
+does not need to specify a duration for a ban
+\end_inset
+</cell>
+</row>
+<row topline="true">
+<cell alignment="center" valignment="top" topline="true" leftline="true" usebox="none">
+\begin_inset Text
+
+\layout Standard
+
+9
+\end_inset
+</cell>
+<cell alignment="center" valignment="top" topline="true" leftline="true" rightline="true" usebox="none">
+\begin_inset Text
+
+\layout Standard
+
+can run commands in team chat
+\end_inset
+</cell>
+</row>
+<row topline="true">
+<cell alignment="center" valignment="top" topline="true" leftline="true" usebox="none">
+\begin_inset Text
+
+\layout Standard
+
+0
+\end_inset
+</cell>
+<cell alignment="center" valignment="top" topline="true" leftline="true" rightline="true" usebox="none">
+\begin_inset Text
+
+\layout Standard
+
+inactivity settings do not apply
+\end_inset
+</cell>
+</row>
+<row topline="true">
+<cell alignment="center" valignment="top" topline="true" leftline="true" usebox="none">
+\begin_inset Text
+
+\layout Standard
+
+!
+\end_inset
+</cell>
+<cell alignment="center" valignment="top" topline="true" leftline="true" rightline="true" usebox="none">
+\begin_inset Text
+
+\layout Standard
+
+no ! commands can be used on a player with this flag
+\end_inset
+</cell>
+</row>
+<row topline="true" bottomline="true">
+<cell alignment="center" valignment="top" topline="true" leftline="true" usebox="none">
+\begin_inset Text
+
+\layout Standard
+
+@
+\end_inset
+</cell>
+<cell alignment="center" valignment="top" topline="true" leftline="true" rightline="true" usebox="none">
+\begin_inset Text
+
+\layout Standard
+
+does not show up as an admin in the output of listplayers
+\end_inset
+</cell>
+</row>
+</lyxtabular>
+
+\end_inset
+
+
+\layout Standard
+
+In addition, there are 3 special case characters in the flags string:
+\layout Standard
+
+
+\begin_inset Tabular
+<lyxtabular version="3" rows="4" columns="2">
+<features islongtable="true">
+<column alignment="center" valignment="top" leftline="true" width="0">
+<column alignment="block" valignment="top" leftline="true" rightline="true" width="5.5cm">
+<row topline="true" bottomline="true">
+<cell alignment="center" valignment="top" topline="true" leftline="true" usebox="none">
+\begin_inset Text
+
+\layout Standard
+
+FLAG
+\end_inset
+</cell>
+<cell alignment="center" valignment="top" topline="true" leftline="true" rightline="true" usebox="none">
+\begin_inset Text
+
+\layout Standard
+
+MEANING
+\end_inset
+</cell>
+</row>
+<row topline="true">
+<cell alignment="center" valignment="top" topline="true" leftline="true" usebox="none">
+\begin_inset Text
+
+\layout Standard
+
+*
+\end_inset
+</cell>
+<cell alignment="center" valignment="top" topline="true" leftline="true" rightline="true" usebox="none">
+\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
+</cell>
+</row>
+<row topline="true">
+<cell alignment="center" valignment="top" topline="true" leftline="true" usebox="none">
+\begin_inset Text
+
+\layout Standard
+
++
+\end_inset
+</cell>
+<cell alignment="center" valignment="top" topline="true" leftline="true" rightline="true" usebox="none">
+\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
+</cell>
+</row>
+<row topline="true" bottomline="true">
+<cell alignment="center" valignment="top" topline="true" leftline="true" usebox="none">
+\begin_inset Text
+
+\layout Standard
+
+-
+\end_inset
+</cell>
+<cell alignment="center" valignment="top" topline="true" leftline="true" rightline="true" usebox="none">
+\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
+</cell>
+</row>
+</lyxtabular>
+
+\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] == '!' ) ? &param[1] : &param[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 )
{