#!/usr/bin/tcl
#
# halfd
#
# Half-Life dedicated server (hlds_run) administration/monitoring daemon
# Copyright (C) 2000, 2001 Rob Abbott
#
#    This program 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.
#
#    This program 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 this program; if not, write to the Free Software
#    Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
#
# Comments, bug reports, etc to <maelstrom@halfd.org>
#
# Rob Abbott    <maelstrom@halfd.org>               rabbott 
# Frank Odignal <odignal@stud.fbi.fh-darmstadt.de>  odignal
# Alec Shaner   <ashaner@chumpland.org>             ashaner
#
#   rabbott 2001-12-03   - Add 'rsattempts' option
#
#   rabbott 2001-12-02   - Add 'hlds_boost' option
#
#   rabbott 2001-11-30   - Fix sayServerMsg (thanks Uli)
#
#   rabbott 2001-11-29   - Add 'getmodelsfromrole' option
#
#   rabbott 2001-11-26   - Add 'getdeaths' option
#                        - Performance enhancements for idle halfd processes
#
#   rabbott 2001-11-20   - Fix 'enforcepass'
#
#   rabbott 2001-11-14   - Fix attack decay.  Thanks to STEDevil for help!
#                        - Add "forgive ta"
#                        - Add DECAYALL and DECAYPLAYERALL commands
#                        - Add email-on-crash function
#                        - Add 'enforcepass' function
#                        - Code cleanup: create initializeMap function
#                        - Fix last N lines on crash
#
#   rabbott 2001-11-12   - Add PID command
#                        - Add 'quiet' function
#
#   rabbott 2001-11-09   - Add 'idle' and 'leech' functions (INCOMPLETE)
#
#   rabbott 2001-11-07   - Add command-line debugging
#
#   rabbott 2001-11-01   - Don't penalize attackcheck if player attacks self
#                        - Add per-map totals to voting stats
#
#   rabbott 2001-10-26   - If teamcheck > 1, penalize role changes too.
#
#   rabbott 2001-10-13   - Release 2.10
#
#   rabbott 2001-10-08   - Avoid spamming server with multiple penalty-warnings
#
#   rabbott 2001-10-05   - Only schedule decays if there's stuff to decay :)
#
#   rabbott 2001-09-24   - Add "teamcheck" logic
#
#   rabbott 2001-09-23   - Add "maplistdisplay"
#                        - Fix foulprefix
#                        - Add "vote restart" option
#                        - HLTV fix for 'getmodel'
#
#   rabbott 2001-09-21   - Recognize dead player chat for CS 1.3
#
#   rabbott 2001-09-14   - Change command-line arguments
#
#   rabbott 2001-09-05   - Begin adding 'plugin' capability.
#
#   rabbott 2001-08-31   - Allow multiple actions for single HalfBot trigger (%B)
#                        - Change HalfBot trigger sorting algorithm
#
#   rabbott 2001-08-30   - Fix some problems with pingcheck (thanks Husayn):
#                        - Lag spikes give correct message
#                        - Excluded players don't get violation message
#
#   rabbott 2001-08-29   - Added 'nullname' option
#
#   rabbott 2001-08-28   - %L in say = line break
#
#   rabbott 2001-08-24   - HalfBot can now exec commands or procs
#
#   rabbott 2001-08-22   - HalfBot code added
#
#   rabbott 2001-08-20   - Allow for multiple args to admin_command penalties
#                        - Internal code to make it easier to add penalties
#                        - Fixed 'last N lines on crash' - was broken for small files
#
#   rabbott 2001-08-19   - Fix suicide penalties
#                        - Add 'suicideteams' option
#
#   rabbott 2001-08-17   - Fix lan-specific 'DECAYPLAYER' bug
#                        - Fix lan-specific timing problem on penalties within
#                          first 15 seconds of map
#                        - Thanks to 'Quincy' for LAN bug-hunt and testing :-)
#
#   rabbott 2001-08-16   - Fix lan-specific 'mypenalty' bug
#
#   rabbott 2001-08-14   - Added dumpVars to help check for memory leaks
#                        - Added suicide penalties
#
#   rabbott 2001-08-13   - Return 0 for map remaining if mp_timelimit = 0
#
#   rabbott 2001-08-10   - Process "change role" event
#
#   rabbott 2001-07-31   - Fix intermittent win32 read-after-close error
#
#   rabbott 2001-07-30   - Accomodate DoD LAN - uses "-1" for WONid :-(
#
#   rabbott 2001-07-26   - Don't 'say' anything longer than 100 bytes
#
#   rabbott 2001-07-20   - Release 2.00
#                        - Don't 'exec listip.cfg' - this creates duplicate entries 
#
#   rabbott 2001-07-20   - Add vote totals to STATS
#                        - Interpret 'STOP' as 'quit' for hladmin.exe
#
#   rabbott 2001-07-17   - Add suicide taunts
#                        - Add admin-mod control
#                        - Add 'attacked' logic
#
#   rabbott 2001-07-13   - Ignore team-change for Firearms
#
#   rabbott 2001-07-12   - Merge in changes from Fritz Elfert <felfert@to.com>
#                        - Server hostname always updates correctly
#                        - If bindip=nic, use address also
#                        - Use absolute path for ifconfig (/sbin/ifconfig)
#
#   rabbott 2001-07-11   - Add 'banlog' option
#
#   rabbott 2001-07-08   - Win32 now properly deletes log files
#
#   rabbott 2001-07-05   - Global warfare returns 0.000 for mp_timelimit
#                        - Fixed player 'connect' logic old-logging mods
#
#   rabbott 2001-07-02   - Added 'autovotetime' option
#
#   rabbott 2001-07-02   - Release 2.00b3
#                        - Don't puke if 'mapcyclefile' doesn't exist
#
#   rabbott 2001-06-30   - Don't puke if 'pingcounts' isn't in halfd.cfg
#
#   rabbott 2001-06-27   - Added 'modlist' command
#                        - Don't check for hung server on Win32
#
#   rabbott 2001-06-19   - A null sv_password can be "" or "none"
#                        - mypenalty in-game displays name, PENALTY displays id
#                        - More debug code around voting messages
#                        - Fix win32-specific voting problem
#
#   rabbott 2001-06-17   - Release 2.00b1 BETA #1
#                        - Add '%T' to warnmsgs
#
#   rabbott 2001-06-16   - Add 'statswait' logic
#                        - Add 'PENALTY' server command
#                        - Add 'mypenalty' ingame command
#
#   rabbott 2001-06-13   - Add support for 05 packets from HLBP
#                        - Add DECAYPLAYER command
#                        - Add PING command
#                        - Allow in-game commands (e.g. nextmap) from clients
#
#   rabbott 2001-06-12   - Release 2.00a2 ALPHA
#                        - S&I teams are now determined
#                        - Thanks to HoundDawg for lots of win32 feedback:
#                          Map change should now always trigger properly on win32
#                          win32 events are processed in realtime
#                          win32 old-log files are now blah.yymmddhhmm.log
#                          Added 'oldlogsdir' config parameter
#                          Reduced number of times 'status' is executed
#
#   rabbott 2001-06-05   - Added 'saycmd' and 'saysleep' logic (thanks Uli for idea)
#                        - Bugfix in 'votestatus' code
#
#   rabbott 2001-06-04   - Release 2.00a1 ALPHA
#                        - Add HLBP logon code
#                        - Add 'votekwtime' option
#                        - Add 'votemaps' option (with "mapcycle" support)
#
#   rabbott 2001-06-03   - Add HLBP functionality for Win32 (www.unitedadmins.com/hlbp)
#                        - Add win32-specific code
#
#   rabbott 2001-05-28   - Force 'fullserverinfo'
#                        - Fix timer issue with 'votestatus'
#                        - Echo last N lines of hlds_l.log on a crash
#
#   rabbott 2001-05-24   - Release 1.72b1.  Experimental "config postfix" code.
#
#   rabbott 2001-05-23   - Backtrace on server crash wasn't working.  Is now.
#
#   rabbott 2001-05-19   - Release 1.70
#
#   rabbott 2001-05-18   - Added 'votestatus' command
#
#   rabbott 2001-05-15   - If all users are in violation of ping threshold, assume
#                          lag spike and ignore.
#                        - Add 'admin_bot_kick' support
#
#   rabbott 2001-05-11   - Add 'tkroundlimit' and 'tkroundpenalty' logic
#
#   rabbott 2001-05-10   - Rename to 'halfd'
#
#   rabbott 2001-05-08   - Deprecate 'voteignoretag' option
#
#   rabbott 2001-05-07   - Add 'autovote' function
#                        - Check names for foul-langauage
#
#   rabbott 2001-05-01   - Allow 'global' configuration file in $HLDIR
#                        - Genericize server input routine
#
#   rabbott 2001-04-29   - 'modmax' ignored if 'modswitch=0'
#                        - If current mod wins vote, correct text message is displayed
#
#   rabbott 2001-04-26   - Never ban WONid 0.  Kick by uid instead if this occurs.
#                        - Added 'voteprogress' logic
#
#   rabbott 2001-04-25   - Release 1.60b2
#                        - Fix problem with penalty messages
#                        - Include 'banip' support
#
#   rabbott 2001-04-25   - Release 1.60b1
#                        - Badword files ending in blank lines won't cause all
#                          words to be level 1
#                        - Cleanup on generic penalty system
#
#   rabbott 2001-04-23   - Lots of cleanup on 'oldhack' and 'fahack'
#
#   rabbott 2001-04-20   - Added 'oldhack' for OP4 (and others???)
#                        - Empty pid files no longer cause problems
#
#   rabbott 2001-04-17   - Make all penalties (weapon, tk, badwords, ping) use
#                          same generic algorithm
#                        - Allow admin-mod commands in penalties :)
#
#   rabbott 2001-04-13   - Backtraces generated on crash are now *complete* bt's :)
#
#   rabbott 2001-04-06   - Add "%P" support in "fun weapon" taunts
#                        - Clean up MOD-voting so it doesn't require a server crash
#                        - Add weapon and ping violations to exclude lists
#
#   rabbott 2001-04-05   - Beta release 1.50b5
#                          Fix problem with 'timeleft' (I hope :P)
#
#   rabbott 2001-03-28   - UPDATE messages no longer sent twice on server refresh
#                          Added 'pingdecay' support
#                          Added 'whattime' command
#
#   rabbott 2001-03-27   - Attempt to work-around Firearms nonstandard logging
#                          with "fahack"
#
#   rabbott 2001-03-27   - Beta release 1.50b4
#                          Count kills using simple expression for mods that 
#                          don't support new logging standard
#
#   rabbott 2001-03-26   - Clean up 'newMap' code
#                          Add support for 'pingcheck' (EXPERIMENTAL)
#                          Add support for 'console'
#                          Add support for new 'mapcyclefile' cvar
#
#   rabbott 2001-03-23   - Fix 'badwords' functionality
#
#   rabbott 2001-03-19   - Beta release 1.50b3
#                          Send version information to newly-connected clients
#                          Fix 'size' function
#                          Clean up vote and size regexps
#
#   rabbott 2001-03-15   - Added 'teammap' function
#                          Force "mp_logmessages 1" (for counter-strike)
#
#   rabbott 2001-03-15   - Beta release 1.50b1
#                          Chatmode now actually sends chat messages to clients :)
#                          SG-TK announces the fact that a SG-TK occurred
#                          Failure to open "hlds_ld.admin" file no longer results in
#                          a crash
#
#   rabbott 2001-03-15   - Alpha release 1.50a4
#                          Fix chatmode 
#                          Fix SG-TK
#                          Fix badwords "."
#
#   rabbott 2001-03-14   - Alpha release 1.50a3
#                          Fix problem with updateTeams
#
#   rabbott 2001-03-14   - Alpha release 1.50a2
#                          Fix 'sgtkcheck' logic
#
#   rabbott 2001-03-14   - Alpha release 1.50a
#                          Many changes to support new logging standard
#
#   rabbott 2001-03-12   - Added 'modmax', 'modprimary', 'modswitch' logic
#
#   rabbott 2001-03-06   - Players with WONid of 0 no longer show a local IP
#
#   rabbott 2001-03-04   - Allow badwords to end with a period ".", indicating
#                          that matching should not include non-whitespace
#
#   rabbott 2001-03-02   - Added 'bindip' support
#                          Added 'deletecust' support
#                          Fixed 'rotatetype=1' logic
#
#   rabbott 2001-02-25   - Release 1.40
#
#   rabbott 2001-02-20   - Added MOD-voting (experimental)
#
#   rabbott 2001-02-12   - Add weapon-limit support
#
#   rabbott 2001-02-11   - Fix problem with 'voteignoretag' if it's empty
#
#   rabbott 2001-02-09   - Attempt to work-around vehicle-tk bug in CS: 
#                          'vehiclehack' logic
#
#   rabbott 2001-02-08   - Fix 'forgive-tk' bug introduced in beta3
#                        - Add 'rotatetype' support
#
#   rabbott 2001-02-03   - Change maxextend logic to during vote rather than end
#
#   rabbott 2001-01-30   - Added 'novotemaps' support
#                        - Added 'maxextend' support
#
#   rabbott 2001-01-23   - Add 'voteignoretag' support
#
#   rabbott 2001-01-18   - Support for DoD team-determination (getmodel=1 required)
#
#   rabbott 2001-01-17   - Support for HPB bots:
#                        - Bots don't have an IP address
#                        - Bots don't show up in 'users'
#
#   rabbott 2001-01-11   - Add 'adminlog' code
#
#   rabbott 2001-01-09   - Use military time for logging rather than AM/PM
#
#   rabbott 2001-01-08   - More fixes to name-matching code
#
#   rabbott 2000-12-20   - Add 'foulprefix' code
#
#   rabbott 2000-12-18   - Fix name-matching code
#
#   rabbott 2000-12-14   - Add FOULON/FOULOFF commands
#
#   rabbott 2000-12-04   - Release 1.31
#                        - Plug hole that allowed execution of server commands
#                          by clients :-(
#                          Many thanks to Nour Sharabash and his buddy Paige for
#                          finding this!!!
#                        - Added 'fortunepath' option
#
#   rabbott 2000-11-28   - TKON/TKOFF now say the right thing (thanks Topper)
#
#   rabbott 2000-11-20   - FLF: Figure out team based on model :-(
#                        - More clean-up on model-determination code
#
#   rabbott 2000-11-15   - Add support for Firearms and it's funky logging ;-)
#
#   rabbott 2000-11-13   - Release 1.30
#
#   rabbott 2000-11-12   - If 'nextmap' is 0, don't ever try to figure
#                          out what the next map is.
#                        - Added 'alwaysrunstats' logic
#
#   rabbott 2000-11-07   - Ignore M$-style tags in hlds_ld.cfg
#
#   rabbott 2000-11-01   - Fix 'sgtkcheck' logic.
#                          Thanks to Colin Corbett for all the testing!
#
#   rabbott 2000-10-31   - Happy halloween.
#                        - Fix getMapTime crash
#                        - Set "mp_timelimit 1" before changing map on vote
#
#   rabbott 2000-10-30   - Added 'getteam' - team parsing and reporting
#                        - Added 'sgtkcheck' logic
#                        - Further cleanup of processLogLine
#                        - Fixed getServerVariable procedure
#
#   rabbott 2000-10-29   - Release 1.28b2
#                        - Added processLogLine; ALL lines should be worked now 
#                        - Added 'exclude' logic
#                        - Ignore comments and "minplayers" directives in 
#                          mapcycle.txt
#                        - Added 'getServerVariable' procedure
#
#   rabbott 2000-10-28   - Speed up model detection logic
#
#   rabbott 2000-10-26   - Addded 'checkpass' logic 
#
#   rabbott 2000-10-26   - Release 1.27b
#                        - Fix problem with 'maxconns'
#
#   rabbott 2000-10-25   - Argh.  No IP addresses returned from 'status'
#                          in 3.0.1.4
#
#   ashaner 2000-10-21   - Add player's model in the player status message.
#
#   rabbott 2000-10-17   - Don't open tcldebugfd twice :)
#
#   rabbott 2000-10-06   - Don't show active connections to USERs that ask
#                          for STATS
#
#   rabbott 2000-10-05   - Added 'maxconns' and 'localconns' logic
#                        - Added 'tcldebug' logic
#                        - Release 1.25b
#
#   rabbott 2000-09-18   - Determine IP address at server start-time
#                          if 'nic' option is enabled
#
#   rabbott 2000-09-06   - Don't puke if no logfile on getMapTime
#                        - Added TKON/TKOFF commands
#
#   rabbott 2000-09-05   - Debugging of 'exec' added
#                        - For TFC, figure out timeleft based on mp_timeleft 
#                          at startup
#
#   rabbott 2000-08-30   - Release 1.23
#
#   rabbott 2000-08-29   - Fixed TK-checking for TFC
#                        - TK and foul will still decay if server is stopped
#
#   rabbott 2000-08-29   - Release 1.22
#
#   rabbott 2000-08-27   - If no maps/ dir, don't generate an error
#                        - Fix error if TKs > greatest tkcount (Thanks TnT)
#
#   rabbott 2000-08-26   - Release 1.21
#                        - Fix error in forgive code (Thanks Cory V.A.!)
#
#   rabbott 2000-08-26   - Release 1.20
#
#   rabbott 2000-08-25   - Check for level-1 before level-2 swear words
#
#   rabbott 2000-08-14   - Don't broadcast maplist patterns (used to cheat!)
#
#   rabbott 2000-08-14   - Release 1.20b2
#                        - Fix 'warnleft not set' error in TK-checking
#                        - Index TKs by userid not name
#                        - Add 'fortune' function :-)
#
#   rabbott 2000-08-11   - Add 'votefreq' functionality
#
#   rabbott 2000-08-08   - Remove name tags before checking forgive
#
#   rabbott 2000-08-05   - Release 1.20b1
#                        - Fixed many TK kick/ban bugs 
#                        - Major thanks to hlmonitor for code/ideas!
#
#   rabbott 2000-08-02   - Added TK kick/ban functionality
#
#   rabbott 2000-07-31   - hostnames with ":" are now parsed correctly
#
#   rabbott 2000-07-29   - Filter sensitive ADMIN-mod messages to USERs
#                        - Correctly parse players with " : " in their names
#
#   rabbott 2000-07-26   - Moved hlds_ld.badwords to $HLDIR/<mod>
#                        - Fixed error message if foul-lang regexp fails
#
#   rabbott 2000-07-25   - Release 1.11b2
#                        - Fixed a problem with Level 1 kick/ban
#                        - Users with "<" in their name can now vote
#
#   rabbott 2000-07-24   - Release 1.11b1
#                        - Foul-language now based on "hlds_ld.badwords"
#                        - Level 1 and Level 2 foul-language offenses
#                        - Configurable amount of warnings for level 2
#                        - Added client 'size mapname' command
#
#   rabbott 2000-07-18   - Debugging option in .cfg file
#
#   rabbott 2000-07-16   - Added foul-language checking feature
#                        - Remove duplicates from maplist
#                        - More performance enhancements
#
#   rabbott 2000-07-10   - If mp_timelimit's value is a string, make it 0
#
#   rabbott 2000-06-29   - Release 1.10
#                        - Fix bug in 'users' parsing (Thanks Ken Kirchner)
#                        - Added 'autorotate' (Suggestion from Ken)
#                        - Don't allow votes to carry over from a previous
#                          match
#                        - Read env(HLDIR) once at startup (Thanks philstr)
#
#   rabbott 2000-06-28   - Works with 3.1.0.1 
#                        - Use editable configuration file
#
#   rabbott 2000-06-23   - Make maptime an integer - Firearms gives float...
#
#   rabbott 2000-06-23   - Release 1.09b4
#                        - Works with 3.1.0.0
#                        - No longer need to cache IP addy's
#                        - All 'say' regexps: look for ^0x02, not just 0x02
#   
#   rabbott 2000-06-09   - Major performance improvements in getServerStatus
#                          and processLogFile
#                        - Version-check on hlds_ld.txt
#
#   rabbott 2000-06-08   - Release 1.09b3
#                        - Fix bugs associated with multiple players and
#                          the 3.1.0.0 patch:
#                          * two-digit userids no longer throw off columns
#                          * concurrent disconnects no longer cause tcl error
#                        - Improve performance by only executing 'users' when
#                          necessary
#
#   rabbott 2000-06-08   - Release 1.09b2
#                          Jump through lots of hoops to get it to work with
#                          latest patch :)
#
#   rabbott 2000-06-03   - Merge in Frank's changes
#                        - Make 'maplist' do pattern-matching
#                        - Make download message part of 'server message',
#                          allow server message to be displayed at configurable
#                          intervals
#                        - Added CYCLE command
#                        - Compress logs that go in crashes/ directory
#
#   odignal 2000-05-12   - Logging of hlds_ld msgs to gui even in NOTEXT mode
#   odignal 2000-05-10   - Display 15 and 30 sec after mapchange a download message
#   odignal 2000-05-05   - Filtering of 0x02 in front of chat msgs
#   odignal 2000-04-20   - Added maplist support
#   odignal 2000-04-20   - Starting hlds_l with nice -15 (seems not to function properly ;()

#   rabbott 2000-01-10   - Port all server-side code from hlds_l_admin
#
#
#######################################################################
# tkerror - supress any gui error msgs; server *may* continue to run...?
#
#proc tkerror {errmsg} {
#     puts stderr "an error has occurred.  if you can reliably reproduce it,"
#     puts stderr "please contact the author!   see help->about."
#     puts stderr $errmsg
#}

# output 
#	global keys
#		up = is hlds_l up
#		time = time left in map - in seconds (if up)
#		data = if type has any sub-keys associated
#
#	type ERR = error occurred, data contains err msg
#
#	type STATUS = global keys only
#
#	type SB = statusbar, keys:
#		keys log = log to view area y/n
#		     sb = log to statusbar y/n
#		     text = text of the msg
#

proc sendClient { type { data "" } {fd ""} } {
	global exprs serverUp svrInfo conns protected

	calcMapTimeRem

	set kl ""
	keylset kl type $type
	keylset kl up   $serverUp
	keylset kl time $svrInfo(maprem)

	# Filter out 0x02 in data
	regsub -- "^[format %c 0x02]" $data "" filtereddata

	if ![cequal $type STATUS] {
		keylset kl data $filtereddata
	}

	# Figure out what clients get this
	if [cequal $fd ""] {
		set fds $conns(fds)
	} else {
		set fds $fd
	}

	foreach fd $fds {
		if [catch { set connAttr $conns($fd) } err] continue

		foreach var "auth text" {
			keylget connAttr $var $var
		}

		# Make sure they've authenticated
		if !$auth continue

		if [cequal $type TEXT] {
			# If they didn't ask for text, don't send any
			if !$text continue

			# If they're not admin, don't send team, rcon, etc
			if { $auth < 2 } {
				if [regexp $exprs(txtfilter) $data] { continue }

				# If they're not admin, and server is passworded, don't
				# send ANY text messages.
				if $protected continue
			}

			if [cequal $text 2] {
				# Chat mode
				if ![regexp $exprs(chatmode) $data] { continue }
			}
		}

		writeSock $fd $kl
	}
}

######################################################################
# start of net code - halfd server side (for talking to halfd clients)
proc doListen {} {
	global config fds conns text after plat
	global errorCode errorInfo

	set conns(fds) ""

	set rc 0

	if [cequal $config(bindip) $config(nic)] {
		set config(bindip) [determineIP]
	}

	if ![cequal $config(bindip) ""] {
		myEcho INFO [format $text(246) $config(bindip)]
		set rc [catch {socket -server acceptConn -myaddr $config(bindip) $config(dport)} fd]
	} else {
		myEcho DBUG "halfd not bound to a specific IP address"
		set rc [catch {socket -server acceptConn $config(dport)} fd]
	}

	if $rc {
		set err $errorCode

		if [regexp EADDRINUSE $err] {
			myEcho WARN [format $text(10) $err]
			set after(listen) [after 10000 doListen]
		} else {
			myEcho ERR  [format $text(11) $fd]
			myExit 0
		}
	} else {
		myEcho INFO [format $text(89) $config(dport)]
		set fds(listen) $fd
		if [cequal $plat unix] { fcntl $fd CLOEXEC }
	}
}

proc writeSock { fd data } {
	global stats conns text config

	if [catch {
		flush $fd
		puts $fd $data
		flush $fd
	} err] {
		keylget conns($fd) addr addr
		keylget conns($fd) port port

		if [regexp "broken pipe" $err] {
			if { $config(debug) > 1 } {
				myEcho DBUG [format $text(12) $addr $port $err]
			}
		} else {
			myEcho ERR [format $text(12) $addr $port $err]
		}
		closeSock $fd
	}

	incr stats(out) [expr [clength $data] + 1] ;# Add 1 for nl
}

proc acceptConn { fd addr port } {
	global stats conns config text plat

	myEcho INFO [format $text(13) $addr $port $fd]

	incr stats(conns)
	if [cequal [lsearch -exact $stats(ips) $addr] -1] {
		lappend stats(ips) $addr
	}

	if { $config(localmode) && ![cequal $addr "127.0.0.1"] } {
		myEcho WARN [format $text(14) $addr $port]
		close $fd
		return
	}

	# Make sure we haven't maxed-out the number of connections
	if $config(maxconns) {
		if { [llength $conns(fds)] >= $config(maxconns) } {
			if { $config(localconns) && ![cequal $addr "127.0.0.1"] } {
				myEcho WARN [format $text(133) $addr $port [llength $conns(fds)]]
				close $fd
				return
			}
		}
	}

	lappend conns(fds) $fd

	set kl ""
	keylset kl addr $addr
	keylset kl port $port

	keylset kl auth 0
	keylset kl text 0

	set conns($fd) $kl

	if [cequal $plat unix] { fcntl $fd CLOEXEC }
	fcntl $fd NOBUF

	myEcho DBUG "Setting fileevent on $fd"
	fileevent $fd readable readSock
}

proc closeSock { fd } {
	global conns text

	keylget conns($fd) addr addr
	keylget conns($fd) port port

	myEcho INFO [format $text(15) $addr $port]

	close $fd

	unset conns($fd)

	set newconns [lindex [intersect3 $conns(fds) $fd] 0]
	set conns(fds) $newconns
}

proc readSock {} {
	global stats conns text plat

	# Can't pass an arg for which fd is readable.  Bleh.  Figure it out.
	if [cequal $plat unix] {
		set fds [lindex [select $conns(fds) {} {} 0] 0]
	} else {
		# Gross.  What a bitch that select doesn't work on Windoze!!
		set fds $conns(fds)
	}

	foreach fd $fds {
		if [catch { set conns($fd) }] {
			# Tried to read on already-closed socket...?
			myEcho DBUG "Attempted to read on already-closed socket: $fd"
			myEcho DBUG "Removing $fd from list '$fds'"
			set newconns [lindex [intersect3 $conns(fds) $fd] 0]
			set conns(fds) $newconns
			return
		}

		if [catch {eof $fd} eof] {
			keylget conns($fd) addr addr
			keylget conns($fd) port port
			if [regexp "broken pipe" $eof] {
				myEcho DBUG [format $text(16) $addr $port $eof]
			} else {
				myEcho ERR [format $text(16) $addr $port $eof]
			}
			closeSock $fd
		}

		if $eof {
			closeSock $fd 
		} else {
			if [cequal $plat windows] { fconfigure $fd -blocking 0 }
			if [catch {gets $fd} data] {
				keylget conns($fd) addr addr
				keylget conns($fd) port port
				if [regexp "broken pipe" $data] {
					myEcho DBUG [format $text(17) $addr $port $data]
				} else {
					myEcho ERR [format $text(17) $addr $port $data]
				}
				closeSock $fd
			} else {
				incr stats(in) [expr [clength $data] + 1] ;# add 1 for NL
				processClientReq $data $fd
			}
		}
	}
}

# end of net code
######################################################################


######################################################################
# start of net code - halfd client side (for talking to HLBP)
proc connectToHlbp {} {
	global after hlbpFd gotInit connected config text plat

	set gotInit 0

	catch { after cancel $after(connect) }


	set needResched 0

	if !$connected {
		set addr $config(hlbpaddr)
		set port $config(hlbpport)

		myEcho INFO [format $text(213) $addr $port]

		if [cequal $plat unix] {
			# Use asynchronous connections.  Very cool.
			if [ catch {socket -async $addr $port} hlbpFd] {
				myEcho ERR [format $text(214) $addr $port $hlbpFd]
				set needResched 1
			} else {
				# We need to be non-blocking until we're connected.
				fconfigure $hlbpFd -blocking 0

				# Now wait for it to connect
				fileevent $hlbpFd writable hlbpConnected
			}
		} else {
			# We're on windoze.  Use a blocking connection.  Gross.
			if [ catch { socket $addr $port} hlbpFd] {
				myEcho ERR [format $text(214) $addr $port $hlbpFd]
				set needResched 1
			} else {
				# We're connected.
				set connected 1
				hlbpConnectionComplete
			}
		}
	}

	if $needResched {
		myEcho INFO [format $text(215) $config(hlbpwait)]
		set ms [expr $config(hlbpwait) * 1000]
		set after(connect) [after $ms connectToHlbp]
	}
}

proc hlbpConnected {} {
	global hlbpFd config connected text

	set addr $config(hlbpaddr)
	set port $config(hlbpport)

	# Delete the event
	fileevent $hlbpFd writable {}
	
	# Make sure we *really* connected
	if [catch { gets $hlbpFd } err] {
		myEcho ERR [format $text(214) $addr $port $err]
		myEcho INFO [format $text(215) $config(hlbpwait)]
		set ms [expr $config(hlbpwait) * 1000]
		set after(connect) [after $ms connectToHlbp]
	} else {
		# Now we want to block
		fconfigure $hlbpFd -blocking 1
		set connected 1
		hlbpConnectionComplete
	}
}

proc writeHlbp05 { data type dest } {
	global config

	set mySource "$config(hlbpserveraddr):$config(hlbpserverport)"

	set kl "{ADDON {halfd}} "
	if [cequal [csubstr $data 0 1] "\{"] {
		append kl "{RESPONSE {$type $data}}"
	} else {
		append kl "{RESPONSE {$type {$data}}}"
	}

	writeHlbpCommand 05 $kl $dest
}

proc writeHlbpCommand { type data {target ""}} {
	global connected config

	if [cequal $target ""] {
		# Default to single server as defined in config
		set target $config(hlbpserveraddr):$config(hlbpserverport)
	}

	set msg [format "%02s %- 22s %s" $type $target $data]

	writeHlbpRaw $msg
}


proc writeHlbpRaw { msg } {
	global hlbpFd connected text config
	if !$connected return

	if { $config(debug) > 1 } {
		myEcho DBUG "writing '$msg'"
	}

	if [catch {
		flush $hlbpFd
		puts $hlbpFd $msg
		flush $hlbpFd
	} err ] {
		myEcho ERR [format $text(216) $err]
		closeHlbp
	}
}

proc closeHlbp {} {
	global hlbpFd connected text

	set connected 0

	catch { close $hlbpFd }

	myEcho INFO $text(217) 
	hlbpDown 1
}

proc readHlbp {} {
	global hlbpFd text config

	if { $config(debug) > 2 } {
		myEcho DBUG "readHlbp [clock seconds]"
	}

	if [catch { eof $hlbpFd } eof] {
		myEcho ERR [format $text(218) $eof]
		closeHlbp
		return
	}

	if $eof {
		myEcho ERR $text(219)
		closeHlbp
	} else {
		if [catch {gets $hlbpFd} data] {
			myEcho ERR [format $text(218) $data]
			closeHlbp
		} else {
			# statusBar "Received [clength $data] bytes" 0
			if { $config(debug) > 1 } {
				myEcho DBUG "HLBP sent ([clength $data]) '$data'"
			}
			cacheHlbpData $data
		}
	}
}

proc cacheHlbpData { data } {
	global hlbpCache config text lastcmd dontprocess

	# Rip apart the header.
	set type [csubstr $data 0 2]
	set targ [string trim [csubstr $data 3 22]]
	set msg  [csubstr $data 26 end]

	set myTarget "$config(hlbpserveraddr):$config(hlbpserverport)"

	if { $config(debug) > 2 } {
		myEcho DBUG "HLBP type ([clength $type]): '$type'"
		myEcho DBUG "HLBP targ ([clength $targ]): '$targ'"
		myEcho DBUG "HLBP msg  ([clength $msg]): '$msg'"
	}

	if [cequal $targ ""] {
		myEcho DBUG "Received null target.  Forcing '*'"
		set targ *
	}

	# Process HLBP messages here
	switch -exact -- $type {
		01 {
			set first3 [csubstr $msg 0 3]
			processHlbpEvent $msg
		}

		03 {
			if ![cequal [string trim $msg] $lastcmd] {
				lappend hlbpCache $msg
				
				# We can process immediately...
				if !$dontprocess { processLogFile 1 }
			}
		}

		05 {
			# Process this as if it came from a client GUI
			if $config(hlbpcommands) {
				# Parse it.
				if [catch {
					set addon  [keylget msg ADDON]
				} err] {
					set foo [format $text(238) $err]
					myEcho WARN $foo
					writeHlbpCommand 02 $foo
				} else {
					# Make sure this is destined for us
					if { [cequal $myTarget $targ] || [cequal $targ *] } {
						if [keylget msg COMMAND command] {
							# Process the beast
							processClientCommand HLBP 0 $command "" 2 1 $targ
						} elseif [keylget msg RESPONSE response] {
							# A response...ignore it?
							myEcho INFO [format $text(239) $response]
						} else {
							myEcho WARN [format $text(240) $msg]
							writeHlbpCommand 02 [format $text(240) $msg]
						}
					} else {
						# Not for us
						myEcho DBUG "Ignoring 05 for another server: '$msg'"
					}
				}
			} else {
				myEcho WARN $text(235)
				myEcho WARN [format $text(236) $msg]

				# Send error message
				writeHlbpCommand 02 $text(237)
			}
		}

		default {
			myEcho INFO "*** Not processing HLBP message type $type"
			myEcho INFO "*** data = '$msg'"
		}
	}
}

proc hlbpConnectionComplete {} {
	global config text hlbpauthed hlbpFd

	set hlbpauthed 0

	myEcho INFO [format $text(212) $config(hlbpaddr) $config(hlbpport)]

	myEcho DBUG "Setting file event for HLBP"
	fileevent $hlbpFd readable { readHlbp }

	sendHlbpLogon
}


# end of HLBP net code
######################################################################

######################################################################
# HLBP-specific functions
proc sendHlbpLogon {} {
	global config text gameId vnum

	# Can't use keylset here - HLBP requires curlies around the values
	set    kl "{USERNAME {$config(hlbpuser)}}"
	append kl " {PASSWORD {$config(hlbppass)}}"
	append kl " {NAME {halfd-$vnum-$gameId}}"

	# Send a "290 - Logon" message to HLBP
	writeHlbpCommand 01 "290 $kl"
}

proc processHlbpEvent { msg } {
	global hlbpauthed text config serverUp

	# A HLBP-specific '01' event arrived, process it

	set first3 [csubstr $msg 0 3]

	myEcho INFO "Processing event '$first3' from HLBP (detail=$msg)"

	switch -exact -- $first3 {
		100 {
			# HLBP shutdown
			statusBar $text(222)
			hlbpDown
		}

		101 {
			# Mode changing
			set mode [csubstr 4 1]
			myEcho INFO [format $text(227) $mode]
			myEcho INFO "Mode change not implemented yet :-)"
		}

		112 {
			# HLDS Started
			hlbpServerUp
		}

		113 {
			# HLDS Shutdown
			serverDown
		}

		114 {
			# Connect HLDS response
			set target "$config(hlbpserveraddr):$config(hlbpserverport)"
			set rc [csubstr $msg 4 1]
			if $rc {
				# Connected - server up
				myEcho INFO [format $text(225) $target]
				hlbpServerUp
			} else {
				# Connected - but server is down
				myEcho INFO [format $text(226) $target]
				serverDown

				# If configured to autostart, start the server
				if $config(autostart) {
					# Send 212 - Start HLDS
					writeHlbpCommand 01 212

					# Will receive a "112 - HLDS Started"
				}
			}
		}

		115 {
			# HLDS Status Response
			set rc [csubstr $msg 4 1]
			if $rc {
				# Server is UP
				if !$serverUp {
					serverUp
					getServerStatus 1 1
				}
			} else {
				# Server is DOWN
				if $serverUp serverDown
			}
		}

		119 {
			# HLDS crashed
			myEcho WARN $text(228)
			serverDown
		}

		190 {
			# Logon response
			set rc [csubstr $msg 4 1]
			if $rc {
				# Logon successful
				myEcho INFO $text(223)
				set hlbpauthed 1
				hlbpUp

				# Now we tell HLBP what server we want to manage
				# 214 - Connect HLDS
				writeHlbpCommand 01 214

				# We expect a 114 response to this
			} else {
				# Logon failed
				myEcho INFO $text(224)
				hlbpDown
			}
		}

		191 {
			# Ping reply
			myEcho DBUG $text(229)
		}

		199 {
			# HLBP closing connection
			myEcho INFO $text(230)
		}

		default {
			myEcho WARN "Unknown message type '$first3' received from HLBP."
			myEcho WARN "Detail: '$msg'"
		}
	}
}

proc hlbpServerUp {} {
	initModMax
	serverUp
	getServerStatus 1 1
	getMapTimeRem
}
# end HLBP-specific functions
######################################################################

proc processClientReq { data fd {init 0}} {
	global stats conns config svrInfo serverUp text fds wlimit vnum plat

	keylget conns($fd) addr addr
	keylget conns($fd) port port
	# keylget conns($fd) text text
	keylget conns($fd) auth auth

	if { !$auth || $init } {
		if !$init {
			# Switch doesn't evaluate patterns.  Bleh
			if [cequal $data $config(usrauth)] {
				set auth 1
				myEcho AUTH [format $text(18) $addr $port]
				incr stats(user)
			} elseif [cequal $data $config(admauth)] {
				if $config(localadmin) {
					if ![cequal $addr "127.0.0.1"] {
					myEcho AUTH [format $text(19) $addr $port]
					myEcho WARN [format $text(19) $addr $port]
					closeSock $fd
					return
					}
				}
				set auth 2
				myEcho AUTH [format $text(20) $addr $port]
				incr stats(admin)
			} else {
				# See Ya
				myEcho AUTH [format $text(21) $addr $port]
				myEcho WARN [format $text(21) $addr $port]
				closeSock $fd
				return
			}

			keylset conns($fd) auth $auth
		}

		# Auth OK, send initialization msg
		set kl ""
		keylset kl type INIT
		keylset kl up $serverUp
		keylset kl auth $auth

		myEcho DBUG "Sending INIT message to $fd"
		if { $serverUp && [info exists svrInfo(ip)] } {
			foreach id "name ip max" {
				keylset kl $id $svrInfo($id)
			}
		}

		keylset kl port $config(port)

		writeSock $fd $kl

		# getServerStatus 1 1
		sendClient UPDATE [buildClientUpdate] $fd
		statusBar [format $text(188) $vnum] 1 $fd
		statusBar [format $text(189) $svrInfo(version)] 1 $fd
	} else {
		if !$serverUp { checkForServer 1 }

		# Client is authorized, process their request
		# The first set of commands are allowed by anyone
		if [cequal $data ""] return

		# First check to see if we're connected to HLBP
		if [cequal $plat windows] {
			if !$svrInfo(hlbp) {
				sendClient ERR $text(220) $fd
				sendClient TEXT $text(221)
			}
		}

		processClientCommand $addr $port $data $fd $auth
	}
}

proc processClientCommand { addr port data fd auth {fromhlbp 0} {hlbptarget "*"}} {
	global fds text config conns mapCycleList svrInfo
	global myhldir gameId serverUp votestart vote

	myEcho INFO [format $text(22) $addr $port $data]

	switch -glob -- $data {

		CHAT { 
			if $fromhlbp return
			keylset conns($fd) text 2
			checkProtected $fd $auth
		}

		TEXT { 
			if $fromhlbp return
			keylset conns($fd) text 1
			checkProtected $fd $auth
		}

		STATS { 
			dumpStats 1 $fd $auth $fromhlbp
		}

		NOTEXT { 
			if $fromhlbp return
			keylset conns($fd) text 0
		}

		BYE { 
			if $fromhlbp {
				closeHlbp
			} else {
				closeSock $fd
			}
		}

		MODLIST {
			set kl ""
			keylset kl type MAPLIST
			keylset kl up   $serverUp
			keylset kl data $config(votemodnames)

			if $fromhlbp {
				writeHlbp05 $maps MAPLIST $hlbptarget
			} else {
				writeSock $fd $kl
			}
		}

		MAPLIST* {
			set pattern ""
			if [catch { set pattern [lindex $data 1] } err] { 
				myEcho DBUG "MAPLIST lindex err: $err"
				return
			}

			if ![cequal $pattern ""] {
				if [catch {lmatch [getMaps] $pattern} maps] { 
					myEcho DBUG "MAPLIST lmatch err: $maps"
					return
				}
			} else {
				set maps [getMaps]
			}

			set kl ""
			keylset kl type MAPLIST
			keylset kl up   $serverUp
			keylset kl data $maps

			if $fromhlbp {
				writeHlbp05 $maps MAPLIST $hlbptarget
			} else {
				writeSock $fd $kl
			}
		}

		PING {
			if $fromhlbp {
				writeHlbp05 PONG PING $hlbptarget
			} else {
				sendClient TEXT PONG $fd
			}
		}

		VOTESTATUS {
			if $vote(insession) {
				tallyVotes 0
				set timeSoFar [expr [clock seconds] - $votestart]
				set timeRem [expr $config(votetime) - $timeSoFar]
				say [format $text(207) $timeRem]
			}
		}

		NEXTMAP {
			set nextmap [getNextMap $svrInfo(map)]
			if $fromhlbp {
				writeHlbp05 $nextmap NEXTMAP $hlbptarget
			} else {
				sendClient TEXT $nextmap $fd
			}
		}

		TIMELEFT {
			set timeleft [fmtTimer]
			if $fromhlbp {
				writeHlbp05 $timeleft TIMELEFT $hlbptarget
			} else {
				sendClient TEXT $timeleft $fd
			}
		}

		MAPCYCLE {
			readMapCycle
			if $fromhlbp {
				writeHlbp05 $mapCycleList MAPCYCLE $hlbptarget
			} else {
				sendClient TEXT $mapCycleList $fd
			}
		}

		PENALTY* {
			set player ""
			if [catch { set player [lindex $data 1] } err] { return }

			if [cequal $player ""] return

			set player [string trim $player]
			printPenalties $player $fd $fromhlbp $hlbptarget

		}

		SIZE* {
			set map ""
			if [catch { set map [lindex $data 1] } err] { return }

			if [cequal $map ""] return

			set map [string trim $map]
			regsub -nocase "\.bsp" $map "" szmap
			set fname $myhldir/$gameId/maps/${szmap}.bsp
			if [catch {file size $fname} size] {
				set size 0
			}

			if $fromhlbp {
				writeHlbp05 $size SIZE $hlbptarget
			} else {
				sendClient TEXT $size $fd
			}
		}

		WHATTIME {
			set now [now]
			if $fromhlbp {
				writeHlbp05 $now WHATTIME $hlbptarget
			} else {
				sendClient TEXT $now $fd
			}
		}
			
		default {
			# Any other commands require authorization
			if { $auth < 2 } {
				myEcho WARN [format $text(23) $addr $port $data]
				closeSock $fd
				return
			}

			# Log the admin action if we've been told to do so.
			if $config(adminlog) {
				while 1 {
					set first3 [string tolower [csubstr $data 0 3]]
					if [cequal $first3 say] {
						if [cequal $config(adminlog) 1] {
							myEcho DBUG $text(154)
							break
						}
					}

					if [catch { open $fds(adminlog) a+ } adminfd] {
						myEcho ERR [format $text(30) $adminfd]
					} else {
						puts $adminfd "\[[now]\] [format $text(22) $addr $port $data]"
						close $adminfd 
					}
					break
				}
			}

			# Handle any 'special' commands (like REFRESH, CYCLE, etc)
			specialClientCommands $data $fd $fromhlbp $hlbptarget
		}
	}
}

proc specialClientCommands { data {fd ""} {fromhlbp 0} {hlbptarget ""}} {
	global config svrInfo text serverUp conns wlimit penaltyFuncList
	global fds

	switch -glob -- $data {
		DUMPVARS { 
			set olddebug $config(debug)
			set config(debug) 1
			dumpVars
			set config(debug) $olddebug
		}

		DUMPSTATS {
			dumpStats
		}

		REFRESH { getServerStatus 1 1 }

		RELOAD { loadPlugins }

		CYCLE { cycleLogFile }

		STARTVOTE { 
			if $config(vote) {
				beginVoting 1
			} else {
				if { ![cequal $fd ""] } {
					sendClient TEXT "STARTVOTE aborted - Voting is currently disabled." $fd
				}
			}
		}

		STOPVOTE  { endVoting }

		VOTEON  { 
			myEcho INFO $text(24)
			set config(vote) 1
			if { ![cequal $fd ""] } {
				sendClient UPDATE [buildClientUpdate] $fd
			}
		}

		VOTEOFF {
			myEcho INFO $text(25)
			set config(vote) 0
			if { ![cequal $fd ""] } {
				sendClient UPDATE [buildClientUpdate] $fd
			}
		}

		FOULON  { 
			myEcho INFO $text(150)
			say $text(150)
			set config(foulcheck) 1
		}

		FOULOFF { 
			myEcho INFO $text(151)
			say $text(151)
			set config(foulcheck) 0
		}

		PINGON  { 
			myEcho INFO $text(233)
			say $text(233)
			set config(pingcheck) 1
		}

		PINGOFF { 
			myEcho INFO $text(234)
			say $text(234)
			set config(pingcheck) 0
		}

		TKON  { 
			myEcho INFO $text(128)
			say $text(128)
			set config(tkcheck) 1
		}

		TKOFF  { 
			myEcho INFO $text(129)
			say $text(129)
			set config(tkcheck) 0
		}

		ATTACKON  { 
			myEcho INFO $text(128)
			say $text(128)
			set config(attackcheck) 1
		}

		ATTACKOFF  { 
			myEcho INFO $text(129)
			say $text(129)
			set config(attackcheck) 0
		}

		WEAPON  { 
			myEcho INFO $text(166)
			say $text(166)
			set config(weaponlimit) 1
		}

		WEAPOFF  { 
			myEcho INFO $text(167)
			say $text(167)
			set config(weaponlimit) 0
		}

		IDLEON*  { 
			set secs ""
			if [catch { set secs [lindex $data 1] } err] { return }

			if ![ctype digit $secs] return

			set msg  [format $text(263) $secs]
			myEcho INFO $msg
			sendClient TEXT $msg $fd
			set config(idle) $secs
		}

		IDLEOFF  { 
			myEcho INFO $text(264)
			sendClient TEXT $text(264) $fd
			set config(idle) 0
		}

		LEECHON*  { 
			set secs ""
			if [catch { set secs [lindex $data 1] } err] { return }

			if ![ctype digit $secs] return

			set msg  [format $text(265) $secs]
			myEcho INFO $msg
			sendClient TEXT $msg $fd
			set config(leech) $secs
		}

		LEECHOFF  { 
			myEcho INFO $text(266)
			sendClient TEXT $text(266) $fd
			set config(leech) 0
		}

		MAXOFF {
			myEcho INFO [format $text(186) modmax]
			set config(modmax) 0
		}

		MODELON  { set config(getmodels) 1 }
		MODELOFF { set config(getmodels) 0 }

		start -
		START {
			if !$serverUp { launchServer }
		}

		STOPALL {
			stopServer
			myExit 0
		}

		STOPD { myExit 0 }

		DECAY {
			foreach tOp $penaltyFuncList {
				$tOp decay 
			}
		}

		DECAYALL {
			foreach tOp $penaltyFuncList {
				$tOp decay 0 1
			}
		}

		DECAYPLAYER* {
			set id ""
			if [catch { set id [lindex $data 1] } err] { return }

			if [cequal $id ""] return

			foreach tOp $penaltyFuncList {
				$tOp decay $id
			}
		}

		DECAYPLAYERALL* {
			set id ""
			if [catch { set id [lindex $data 1] } err] { return }

			if [cequal $id ""] return

			foreach tOp $penaltyFuncList {
				$tOp decay $id 1
			}
		}

		FORTUNE { sayFortune }

		SOURCE* {
			if !$config(allowsource) return

			set file ""
			if [catch { set file [lindex $data 1] } err] { return }

			if [cequal $file ""] return

			if [catch { source $file } err] {
				set msg "Source error: $err"
				myEcho ERR $msg
				sendClient TEXT $msg $fd
			} else {
				set msg "Sourced $file"
				myEcho INFO $msg
				sendClient TEXT $msg $fd
			}
		}

		TESTEMAIL {
			if $config(emailtest) {
				set subject "Test email from halfd"
				set txt [list "This is a test email" ""]
				lappend txt "Sent: [clock format [clock seconds]]"
				sendEmail $subject $txt 1
			}
		}

		DEBUG* {
			set dbg ""
			if [catch { set dbg [lindex $data 1] } err] { return }

			if [ctype digit $dbg] { set config(debug) $dbg }
			sendClient TEXT "Debug level now $dbg" $fd
		}

		PID {
			if ![info exists fds(pidnum)] {
				set fds(pidnum) UNKNOWN
			}
			set msg [format $text(268) [pid] $fds(pidnum)]
			myEcho DBUG $msg
			sendClient TEXT $msg $fd
		}

		default {
			if [cequal [csubstr $data 0 3] ZAP] {
				# Handle 'ZAP'
				if ![catch {lindex $data 1} zapme] {
					if [cequal [string tolower $zapme] all] {
						foreach id $conns(fds) { closeSock $id }
					} else {
						# check for sock
						if ![cequal [lsearch -exact $conns(fds) $zapme] -1] {
							closeSock $zapme
					} else {
							if { ![cequal $fd ""] } {
								sendClient TEXT "Unknown connection: $zapme" $fd
							} elseif $fromhlbp {
								writeHlbpCommand 03 "Unknown connection: $zapme"
							}
						}
					}
				}
			} elseif [cequal [csubstr [string tolower $data] 0 [clength changelevel]] changelevel] {
				# Handle 'changelevel'
				set data [string trim $data]
				if [catch {lindex $data 1} map] {
					myEcho DBUG "Err on 'changelevel' is '$map'"
					execCmd $data
				} else {
					myEcho DBUG "Found 'changelevel', changing to '$map'"
					changeLevel $map
				}
			} elseif [cequal [csubstr $data 0 4] WEAP] {
				# Handle WEAP, WEAPDEF
				set idx 4
				if [cequal [csubstr $data 0 7] WEAPDEF] {
					set idx 7
					set config(weapondefault) [string trim [csubstr $data $idx end]]
				}

				# Change wlimit to whatever they sent
				set wlimit [string trim [csubstr $data $idx end]]

				set msg "Banned weapons set to:"
				say $msg
				say '$wlimit'
				myEcho INFO "$msg '$wlimit'"
			} elseif [cequal [csubstr $data 0 5] FORCE] {
				if ![catch {lindex $data 1} map] {
					if ![cequal [string trim $map] ""] {
						set map [string trim [string tolower $map]]
						endVoting $map
					}
				} else {
					myEcho ERR "Error parsing FORCE arg ('$data'): $map"
				}
			} else {
				# Anything else goes directly to the server
				execCmd $data
			}
		}
	}

	return
}

proc printPenalties { id {fd ""} {fromhlbp 0} {hlbptarget ""}} {
	global text config enterwait penaltyArrays

	foreach var $penaltyArrays { global $var }

	if $enterwait { getServerStatus 1 1 }

	set types "TK WEAPON FOUL PING ATTACK"

	# Should we return an ID or Name?
	set myId $id
	if { [cequal $fd ""] && !$fromhlbp } { 
		# Do we lookup name on IP or WONid?
		if $config(banip) {
			lassign [getUserInfo ip $id] name foo wonid ip
		} else {
			lassign [getUserInfo wonid $id] name foo wonid ip
		}

		if ![cequal $name -1] { set myId $name }
	}

	set i 0
	set foundone 0
	set displaytext ""
	foreach indexArray $penaltyArrays {
		set type [lindex $types $i]
		incr i

myEcho DBUG "printPenalties: checking for $type on ${indexArray}($id)"
		if [info exists ${indexArray}($id)] {
			incr foundone

			set value [set ${indexArray}($id)]

			if $fromhlbp {
				writeHlbp05 [list $id $value $type $hlbptarget]
			} else {
				lappend displaytext [format $text(242) $myId $value $type]
			}
		}
	}

	if $foundone {
		foreach line $displaytext {
			myEcho DBUG $line
			if ![cequal $fd ""] {
				sendClient TEXT $line $fd
			} else {
				say $line
			}
		}
	} else {
		if $fromhlbp {
			writeHlbp05 $id NOPENALTIES $hlbptarget
		} elseif ![cequal $fd ""] {
			sendClient TEXT [format $text(243) $id] $fd
		} else {
			say [format $text(243) $myId]
		}
	}
}

proc checkProtected { fd auth } {
	global config protected text

	if !$config(checkpass) return

	if { $protected && [expr $auth < 2] } {
		sendClient ERR $text(139) $fd
	}
}

proc signalHandler {} {
	global plat

	if [cequal $plat unix] { 
		signal ignore SIGHUP
		signal trap   SIGINT { gotSignal SIGINT }
	}
}

proc gotSignal { signal } {
	global text

	catch { myEcho WARN [format $text(26) $signal] }
	myExit 1
}

proc myExit { {status 0} } {
	global fds text plat svrInfo config

	if [info exists svrInfo(hlbp)] {
		if $svrInfo(hlbp) {
			# Tell HLBP we're going away
			writeHlbpCommand 01 299
		}
	}

	if { ![cequal $status 2] && [info exists config(debug)] } {
			if { $config(debug) > 1 } dumpVars
	}
	if ![cequal $status 2] dumpStats

	catch { myEcho INFO $text(27) }

	if [cequal $plat unix] {
		if ![cequal $status 2] {
			catch { unlink $fds(myPid) }
		}
	}

	if [cequal $plat unix] {
		exit $status
	} else {
		# Sigh. Windoze crashes with non-zero exit status
		if { $status > 0 } {
			# something bad, let them hit <cr>"
			puts -nonewline  "Hit <return> to exit:"
			flush stdout
			gets stdin
		}
		exit 0
	}
}

proc bgerror { error } {
	global errorCode errorInfo text fds

	# Get the calling procedure
	set lvl  [expr [info level] - 1]
	set proc [lindex [info level $lvl] 0]

	myEcho ERR $text(28)
	myEcho ERR $text(29)
	myEcho ERR [format $text(232) $proc]
	myEcho ERR "($errorCode) : $errorInfo"

	catch { flush $fds(mylogfd) }
}

proc statusBar { text {log 1} {fd ""} } {
	set kl ""

	keylset kl sb 0
	if { $log < 2 } {
		keylset kl sb 1
	}

	if !$log {
		# probably don't want to waste bandwidth...
		myEcho INFO $text
		return
		keylset kl LOG 1
	}

	keylset kl text $text

	sendClient SB $kl $fd

}

proc changeLevel { map } {
	global fds text ignoretime

	myEcho DBUG "Changing to $map..."

	myEcho DBUG "executing 'mp_timelimit 1'"
	execCmd "mp_timelimit 1"

	mySleep 2

	myEcho DBUG "executing 'changelevel $map'"
	execCmd "changelevel $map"

	set ignoretime 1

	getServerStatus

	statusBar [format $text(90) $map]
}


proc serverUp {} {
	global serverUp conns text
	set serverUp 1

	# Send an INIT & UPDATE msg to all
	myEcho DBUG "Sending INIT message to ALL clients"
	foreach fd $conns(fds) {
		processClientReq "" $fd 1
	}

	sendClient UPDATE [buildClientUpdate]
}

proc hlbpDown { {serverDown 1} } {
	global svrInfo hlbpauthed

	set svrInfo(hlbp) 0
	set hlbpauthed 0

	if $serverDown serverDown
}

proc hlbpUp {} {
	global svrInfo

	set svrInfo(hlbp) 1
}

proc serverDown { {update 1} } {
	global fds serverUp after svrInfo initializing text uidip plat config

	cancelEvents
	set after(serverChk) [after [expr $config(checkserver) * 1000] checkForServer]

	set serverUp 0

	set uidip(refresh) 0
	cleanWonip

	set vote(insession) 0

	set svrInfo(maprem) 0
	set svrInfo(map)    ""
	set svrInfo(player) 0
	set initializing 1

	if [cequal $plat unix] {
		catch { unset fds(pidnum) }
		catch { unlink $fds(pid)  }
		catch { close $fds(cmdfd) }
		catch { close $fds(logfd) }
	}

	if $update {
		clearUserInfo
		sendClient UPDATE [buildClientUpdate]
	}
}

proc readConfig { {init 0} } {
	global config text gameId myhldir fds oldfoulprefix cfgPostfix plat
	global svrInfo

	if [catch { open $myhldir/$gameId/halfd.cfg$cfgPostfix r } fd] {
		if [info exists fds(logfd)] {
			myEcho ERR [format $text(30) $fd]
			myExit 1
		} else {
			puts stdout [format $text(81) $fd]
			exit 1
		}
	}

	# Clear the array
	foreach id [array names config] { 
		if [cequal $id admininstalled] continue
		unset config($id)
	}

	# Set up the defaults
	set config(rs)         1	;# Restart y/n
	set config(rsDelay)    10	;# Restart Delay
	set config(cm)         1	;# Execute custom map.cfg y/n
	set config(refresh)    60	;# Refresh timeout

	set config(vote)       1	;# Voting enabled y/n
	set config(autovote)   1	;# Voting sessions start automatically?
	set config(autovotetime)   60	;# Auto-vote starts (votetime+autovotetime) secs
	set config(votetime)   180	;# Number of seconds in a voting session
	set config(minvotes)   4	;# Min # of votes to win (unless majority)
	set config(votekw)     ""	;# Voting keyword
	set config(votekwtime) 0 	;# # of seconds after map starts when votekw is allowed
	set config(votefreq)   90	;# Frequency of voting keyword
	set config(novotemaps) ""	;# Maps that can't be voted on
	set config(votemaps)   ""	;# Maps that can only be voted on
	set config(maxextend)  0	;# Maximum amount of time a map can be extended
	set config(voteprogress) 1	;# Display progress as players vote?

	set config(votetext)   {EXAMPLE: "vote nml" would vote for No Man's Land}

	set config(votemodnames) ""
	set config(votemoddirs)  ""
	set config(modmax)       0
	set config(modswitch)    1
	set config(modprimary)   ""

	set config(autostart)  1	;# Autostart hlds_run y/n
	set config(localmode)  0	;# Only accept connections from localhost
	set config(localadmin) 0	;# Only accept ADMINISTRATORs from localhost

	set config(port) 27015		;# Port hlds_run will listen on
	set config(dport) 3000		;# Port halfd will listen on
	set config(bindip) ""		;# IP address halfd will bind to

	set config(stats)     0	;# Execute stats after each match?
	set config(statspath) "./tfc/tfstats/tfstats_l";# Path to stats pgm
	set config(statsargs) "%L outputDir=/home/httpd/html/tfstats";# Stats args
	set config(statswait) 0

	set config(serverMsg)      ""	;# Server Message
	set config(serverMsgTimes) ""	;# Times to display server msg
	set config(usrauth)        ""	;# USER password
	set config(admauth)        ""	;# ADMINISTRATOR password
	set config(minhack)        0	;# Map time hack in in minutes
	set config(maplist)        30	;# Prevent 'maplist' spamming (seconds)
	set config(maplistdisplay) 20	;# Max amount of maps maplist will display
	set config(modlist)        30	;# Prevent 'modlist' spamming (seconds)
	set config(mapcycle)       30	;# Prevent 'mapcycle' spamming (seconds)
	set config(timeleft)       30	;# Prevent 'timeleft' spamming (seconds)
	set config(nextmap)        30	;# Prevent 'nextmap' spamming (seconds)
	set config(size)           30	;# Prevent 'size' spamming (seconds)
	set config(whattime)       30	;# Prevent 'whattime' spamming (seconds)
	set config(votestatus)     30	;# Prevent 'votestatus' spamming (seconds)
	set config(mypenalty)      30	;# Prevent 'mypenalty' spamming (seconds)

	set config(deleteLogs)     1	;# Delete empty log files (# kills)
	set config(deletecust)     0	;# Delete custom.hpk before launching server?

	set config(args) "+exec autoexec.cfg"	;# Args to pass to hlds_run
	set config(maps) ""						;# Builtin maps

	set config(autorotate) 0		;# Auto-rotate halfd.log?
	set config(rotatetype) 0		;# Rotate to .old or to .yymmddhhmm?

	set config(tkcheck)   0
	set config(sgtkcheck) 0
	set config(tkdecay)   3600
	set config(tkcounts)  "3 5 10"
	set config(tkargs) "-1 30 0"
	set config(tkpenalty) "kick banid banid"
	set config(tkwarnmsg) "%P has %C TK-warnings left before action is taken."
	set config(tkpenmsg) "%P gets %T for %C TKs!"
	set config(tkforgive) 1
	set config(tkroundlimit) 0
	set config(tkroundpenalty) 1

	set config(foulcheck)   0
	set config(foulname)    0
	set config(foulcounts)  3
	set config(fouldecay)   3600
	set config(foulargs)    0
	set config(foulpenalty) kick
	set config(foulwarnmsg) "%P has %C FOUL warnings left before %T."
	set config(foulpenmsg)	"%P gets %T for profanity. (%C)"
	set config(foulargs1)    30
	set config(foulpenalty1) banid
	set config(foulmsg1)    "%P is being kicked for using inappropriate language."
	set config(foulprefix) "\'\"\_"


	set config(debug) 0
	set config(tcldebug) 0

	set config(banlog) 0

	set config(fortune) 0
	set config(fortunepath) "/usr/games/fortune"

	set config(nic) ""

	set config(maxconns)   0
	set config(localconns) 1

	set config(getmodels) 0
	set config(getmodelsfromrole) 0

	set config(modesleep) 1

	set config(checkpass)  1	;# should we check to see if the server is pwd'd

	set config(exclude)    0	;# should we look for halfd.exclude?

	set config(getteam)    0	;# should we get and report on teams?

	set config(alwaysrunstats) 0;# Should stats always run even if no kills on a map?

	set config(adminlog) 0		;# Should we log administrator activity?

	# Status hacks :(
	set config(ipfromstats)  1	;# should we determine IP from 'status'?
	set config(statusfields) 9	;# how many fields does 'status' return
	set config(statuscmd)    "status";# what's the 'status' command to run?
	set config(vehiclehack)  0	;# Ignore vehicle-tk's in cs?

	# Weapon limits
	set config(weaponlimit) 0	;# Should we limit usage of weapons?
	set config(weapondefault) "";# What are default weapons if map-specific not there?
	set config(weaponmsg) "WEAPONS BANNED on %M are:"

	set config(weapondecay)   3600
	set config(weaponcounts)  1
	set config(weaponargs)    0
	set config(weaponpenalty) kick
	set config(weaponwarnmsg) "%P has %C weapon-warnings left before action is taken."
	set config(weaponpenmsg)  "%P gets %T for %C weapon violations!"

	set config(banip) 0

	set config(console) 0

	set config(pingcheck)   0
	set config(pingmin)     0
	set config(pingmax)     0
	set config(pingwarn)    3
	set config(pingact)     -1
	set config(pingmaxmsg)  "Pings above %G are not allowed.  %P leave or be kicked."
	set config(pingminmsg)  "Pings below %G are not allowed.  %P leave or be kicked."
	set config(pingactmsg)  "%P is being kicked for ping violations."
	set config(pingwarnmsg) "%P has %C more ping warnings before penalty."
	set config(pingpenmsg)  "%P gets %T for %C ping violations."
	set config(pingdecay)   0
	set config(pingcounts)  "3 5"
	set config(pingargs)  "0 0"
	set config(pingpenalty) "kick kick"

	set config(fahack) 0
	set config(oldhack) 0
	set config(oldprefix)  ""
	set config(oldpostfix) ""

	set config(checkwpt) 0
	set config(nullmap) 15

	set config(idle) 0
	set config(idlemsg) "%P has been idle too long."
	set config(idleaction) 0

	set config(leech) 0
	set config(leechmsg) "Please download maps from http://www.clanakb.org"
	set config(leechaction) 0

	set config(lowstatus) 1

	set config(hlbpaddr) localhost
	set config(hlbpport) 2000
	set config(hlbpuser) ""
	set config(hlbppass) ""
	set config(hlbpserveraddr) 192.168.1.4
	set config(hlbpserverport) 27015
	set config(hlbpwait) 30
	set config(hlbpoutput) 0
	set config(hlbpcommands) 1

	set config(saycmd)   say
	set config(saysleep) 0

	set config(lastNlines) 5

	set config(testplat) ""

	set config(oldlogsdir) oldlogs

	set config(adminmod) 0

	set config(attackcheck)        0
	set config(attackdecay)        600
	set config(attackcounts)       "10 20 30 40"
	set config(attackargs)         "0 0 0 10"
	set config(attackpenalty)      "kick kick kick banid"
	set config(attackwarnmsg)      "%P has %C attack-warnings left before action is taken."
	set config(attackpenmsg)       "%P gets %T for %C attacks!"
	set config(attackforgive)      1
	set config(attackroundlimit)   0
	set config(attackroundpenalty) 1

	set config(suicidecheck)        0
	set config(suicidedecay)        60
	set config(suicidecounts)       "10 20 30 40"
	set config(suicideargs)         "0 0 0 10"
	set config(suicidepenalty)      "kick kick kick banid"
	set config(suicidewarnmsg)      "%P has %C suicide-warnings left before action is taken."
	set config(suicidepenmsg)       "%P gets %T for %C suicides!"
	set config(suicideteams)        ""

	set config(halfbot)			0
	set config(halfbotmax)		20
	set config(halfbotprefix)	"\'\"\_"

	set config(nullname)		0
	set config(nullpenalty)		banid
	set config(nullargs)		30

	set config(plugins)			0
	set config(pluginsource) 	halfd_plugin_example.tcl

	set config(teamcheck)		0
	set config(teamdecay)		10
	set config(teamcounts)		3
	set config(teamargs)		10
	set config(teampenalty)		banid
	set config(teamwarnmsg)		""
	set config(teampenmsg)		"%P gets %T for trying to crash the server!"

	set config(hltvwonid)		1448

	set config(allowsource) 	0

	set config(quiet) 			0

	set config(email) 			""
	set config(emailcmd) 		/bin/mail
	set config(emailtest) 		0

	set config(enforcepass) 	""

	set config(broadcastwarn) 	0

	set config(getdeaths) 		0
	set config(senddeaths) 		1

	set config(checkserver) 	30

	set config(hlds_run) 		hlds_run
	set config(hlds_boost) 		0

	set config(rsattempts)		10

	readConfigFile $fd
	close $fd

	if ![catch { open $myhldir/halfd.cfg r } fd] {
		if [info exists fds(mylogfd)] { myEcho INFO $text(197) }
		readConfigFile $fd
		close $fd
	} else {
		if [info exists fds(mylogfd)] { myEcho DBUG "Global config file not found: $fd" }
	}


	if [info exists fds(mylogfd)] { validateConfig }

	# Fix foul-prefix
	if ![cequal $config(foulprefix) ""] {
		if [cequal [string first " " $config(foulprefix)] -1] {
			set pfx " $config(foulprefix)"
		} else {
			set pfx $config(foulprefix)
		}
		regsub -all ""  $pfx "\\" pfx
		regsub -all " " $pfx " \*" pfx
		set config(foulprefix) "\[$pfx\]"
	}

	# Fix halfbot-prefix
	if ![cequal $config(halfbotprefix) ""] {
		set pfx " $config(halfbotprefix)"
		regsub -all ""  $pfx "\\" pfx
		regsub -all " " $pfx " \*" pfx
		set config(halfbotprefix) "\[$pfx\]"
	}

	if { ![cequal $oldfoulprefix $config(foulprefix)] && !$init } {
		set oldfoulprefix $config(foulprefix)
		readBadwords
	} else {
		set oldfoulprefix $config(foulprefix)
	}

	if ![cequal $config(testplat) ""] {
		set plat $config(testplat)
		myEcho INFO "Forcing platform to '$plat'.  I hope you know what you're doing!"
	}

	if { !$init && $config(plugins) } registerPlugins
}

proc readConfigFile { fd } {
	global config

	# Read the file..
	while 1 {
		#set line [string trim [gets $fd]]
		set line [gets $fd]
		if [eof $fd] break
		if [cequal $line EOF] break

		if [cequal [csubstr $line 0 1] "#"] continue
		if [cequal [csubstr $line 0 1] "\["] continue
		if [cequal $line ""] continue

		lassign [parseConfigFile $line] key val
		set config($key) $val
	}
}

proc parseConfigFile { data } {
	global text config

	# Look for "key=value" pairs
	set list [split $data =]
	set len [llength $list]
	if { $len < 2 } {
		myEcho WARN $text(114)
		myEcho WARN '$data'
		return
	} elseif { $len > 2 } {
		# We have an "=" in the value...
		set key [lvarpop list]
		set val [join $list =]
	} else {
		lassign $list key val
	}

	set key [string trim $key]

	if ![cequal $key foulprefix] {
		set val [string trim $val]
	}

	return [list $key $val]
}


proc validateConfig {} {
	global config text penaltyList

	proc tf { num } {
		if { [cequal $num 1] || [cequal $num 0] } {
			return 1
		} else {
			return 0
		}
	}

	set validated 1

	# Set up a list of booleans
	lappend bools autostart
	lappend bools rs
	lappend bools localmode
	lappend bools localadmin
	lappend bools cm
	lappend bools deleteLogs
	lappend bools tkcheck
	lappend bools tkforgive
	lappend bools foulcheck
	lappend bools foulname
	lappend bools vote
	lappend bools autovote
	lappend bools voteprogress
	lappend bools stats
	lappend bools localconns
	lappend bools ipfromstats
	lappend bools checkpass
	lappend bools exclude
	lappend bools sgtkcheck
	lappend bools alwaysrunstats
	lappend bools autorotate
	lappend bools rotatetype
	lappend bools weaponlimit
	lappend bools deletecust
	lappend bools modswitch
	lappend bools pingcheck
	lappend bools suicidecheck
	lappend bools fahack
	lappend bools oldhack
	lappend bools checkwpt
	lappend bools hlbpoutput
	lappend bools adminmod
	lappend bools admininstalled
	lappend bools attackcheck
	lappend bools halfbot
	lappend bools plugins
	lappend bools allowsource
	lappend bools quiet
	lappend bools broadcastwarn
	lappend bools getdeaths
	lappend bools senddeaths
	lappend bools getmodelsfromrole
	lappend bools hlds_boost
	foreach bool $bools {
		if ![tf $config($bool)] {
			myEcho ERR [format $text(126) $bool $config($bool)]
			set config($bool) 0
			set validated 0
		}
	}

	# Set up a list of digits
	lappend ints dport
	lappend ints port
	lappend ints rsDelay
	lappend ints refresh
	lappend ints tkdecay
	lappend ints fouldecay
	lappend ints votetime
	lappend ints maplist
	lappend ints maplistdisplay
	lappend ints modlist
	lappend ints mapcycle
	lappend ints timeleft
	lappend ints nextmap
	lappend ints minhack
	lappend ints votefreq
	lappend ints maxconns
	lappend ints statusfields
	lappend ints tcldebug
	lappend ints adminlog
	lappend ints maxextend
	lappend ints debug
	lappend ints modmax
	lappend ints pingmin
	lappend ints pingwarn
	lappend ints pingdecay
	lappend ints whattime
	lappend ints votestatus
	lappend ints tkroundpenalty
	lappend ints tkroundlimit
	lappend ints nullmap
	lappend ints votekwtime
	lappend ints hlbpport
	lappend ints hlbpserverport
	lappend ints hlbpwait
	lappend ints lastNlines
	lappend ints saysleep
	lappend ints statswait
	lappend ints banlog
	lappend ints attackdecay
	lappend ints attackroundpenalty
	lappend ints attackroundlimit
	lappend ints suicidedecay
	lappend ints halfbotmax
	lappend ints nullname
	lappend ints teamdecay
	lappend ints teamcheck
	lappend ints checkserver
	lappend ints rsattempts
	foreach int $ints {
		if ![ctype digit $config($int)] {
			myEcho ERR [format $text(127) $int $config($int)]
			set config($int) 0
			set validated 0
		}
	}

	# Set up a list-of-list-of-digits :)
	lappend listints serverMsgTimes
	lappend listints tkcounts
	lappend listints tkargs
	lappend listints weaponcounts
	lappend listints weaponargs
	lappend listints foulcounts
	lappend listints foulargs
	lappend listints pingcounts
	lappend listints pingargs
	lappend listints attackcounts
	lappend listints attackargs
	lappend listints suicidecounts
	lappend listints suicideargs
	lappend listints teamcounts
	lappend listints teamargs
	foreach listint $listints {
		foreach int $config($listint) {
			if ![ctype digit $int] {
				# Handle negative integers
				if { ![cequal [csubstr $int 0 1] -] || ![ctype digit [csubstr $int 1 end]] } {
					myEcho ERR [format $text(146) $listint $config($listint)]
					myEcho ERR $text(147)
					set config($listint) ""
					set validated 0
				}
			}
		}
	}

	set lists ""

	foreach penalty $penaltyList {
		set cnt [llength $config(${penalty}counts)]
		set pen [llength $config(${penalty}penalty)]
		set arg [llength $config(${penalty}args)]

		lappend lists ${penalty}penalty

		if { ![cequal $cnt $pen] || ![cequal $cnt $arg] } {
			myEcho ERR [format $text(202) $penalty]
			myEcho ERR [format $text(203) $penalty $cnt $pen $arg]
			set validated 0
		}
	}

	# Set up a list-of-lists
	lappend lists maps
	lappend lists votemaps
	lappend lists novotemaps
	lappend lists votemodnames
	lappend lists votemoddirs
	lappend lists oldprefix
	lappend lists oldpostfix
	foreach el $lists {
		if [catch { lindex $config($el) 0 } err] {
			myEcho ERR [format $text(283) $el]
			myEcho ERR "(Error was: $err)"
			set validated 0
		}
	}


	if ![cequal $config(votemodnames) ""] {
		if [catch {
			if ![cequal [llength $config(votemodnames)] [llength $config(votemoddirs)]] {
				myEcho ERR [format $text(169) votemodnames votemoddirs]
				set validated 0
				set config(votemodnames) ""
			}
		} err] {
			myEcho ERR "Error validating votemod: $err"
			set validated 0
			set config(votemodnames) ""
		}
	}

	if $config(modmax) {
		if [cequal $config(modprimary) ""] {
			myEcho ERR $text(185)
			set validated 0
		}

		set idx [lsearch -exact $config(votemodnames) $config(modprimary)]
		if [cequal $idx -1] {
			myEcho ERR [format $text(184) modprimary votemodnames]
			set config(modmax) 0
			set validated 0
		}
	}

	set config(suicideteams) [string tolower $config(suicideteams)]

	if [cequal $config(hlds_run) hlds_boost] {
		myEcho ERR "hlds_run option has been deprecated!  Instead, set"
		myEcho ERR "'hlds_boost=1' in halfd.cfg"
		set hlds_run hlds_run
		set hlds_boost 1
		set validated 0
	}

	if !$validated { 
		myEcho ERR $text(148)
	} else {
		myEcho INFO $text(149)
	}
}


proc checkForServer { {update 0} } {
	global plat serverUp after config

	catch { after cancel $after(serverChk) }


	if [cequal $plat unix] {
		checkForServerUnix $update
	} else {
		checkForServerWin32 $update
	}

	if !$serverUp {
		set after(serverChk) [after [expr $config(checkserver) * 1000] checkForServer]
	}
}

proc checkForServerUnix { update } {
	global gameId fds svrInfo text config after

	if { $config(debug) > 1 } { myEcho DBUG "checkForServerUnix executing..." }

	catch { after cancel $after(serverChk) }

	if [file exists $fds(pid)] {
		set fd [open $fds(pid) r]
		set pid [gets $fd]
		close $fd

		# Somebody ended up with an empty pid file...ow
		if [cequal $pid ""] {
			serverDown
			return
		}

		catch {wait -nohang}

		if ![catch {kill 0 $pid} err] {
			# Somebody's running on that process!
			if { ![file exists $fds(cmd)] || ![file exists $fds(log)] } {
				puts stdout [format $text(82) $pid]
				puts stdout $text(83)
				catch {unlink $fds(pid)}
				exit 1
			}
		} else {
			catch {unlink $fds(pid)}
			serverDown
			return
		}

		# Found a running server!
		myEcho INFO [format $text(31) $pid]

		initModMax

		set fds(pidnum) $pid
		set fds(cmdfd)   [open $fds(cmd) w]
		set fds(logfd) [open $fds(log) {RDONLY NONBLOCK}]
		seek $fds(logfd) 0 end
		fcntl $fds(cmdfd) CLOEXEC
		fcntl $fds(cmdfd) NOBUF
		fcntl $fds(logfd) CLOEXEC

		serverUp
		getServerStatus 1 1
	} else {
	#	serverDown $update
	}
}

proc checkForServerWin32 { update } {
	global gameId fds svrInfo text

	catch { after cancel $after(serverChk) }

	# If we're not connected to HLBP, do that

	# Otherwise, we'll get a message from HLBP when the server
	# starts.
	if !$svrInfo(hlbp) {
		connectToHlbp
	} else {
	#	serverDown $update
	}
}

proc determineIP {} {
	global text config

	if [cequal $config(nic) ""] {
		myEcho INFO $text(130)
		return ""
	}

	# Thanks to Eric Hameleers for this code / idea!
	if [catch {exec /sbin/ifconfig $config(nic) | grep inet} ipdata] {
		myEcho INFO [format $text(131) $config(nic) $ipdata]
		return ""
	}

	set tagaddr [lindex $ipdata 1]
	set addr    [lindex [split $tagaddr :] 1]

	myEcho INFO [format $text(132) $config(nic) $addr]

	return $addr
}

proc cancelEvents {} {
	global after penaltyList serverUp config

	# 'nocancel' is a list of events to *never* cancel...
	set nocancel $penaltyList
	lappend nocancel listen

	set cancelList ""
	foreach id $nocancel {
		set x 0
		catch { set x $after($id) }
		if ![cequal $x 0] { lappend cancelList $x }
	}

	foreach id [after info] {
		if [cequal [lsearch -exact $cancelList $id] -1] {
			if $config(debug) {
				set didit 0
				foreach var [array names after] {
					if [cequal $after($var) $id] {
						myEcho DBUG "Cancelling event: $var ($id)"
						set didit 1
						break
					}
				}
				if !$didit { myEcho DBUG "Cancelling event: UNKNOWN ($id): [after info $id]" }
			}
			catch { after cancel $id }
		}
	}
}

proc launchServer {} {
	global gameId fds config vote after text myhldir gameId plat
	global env

	if ![cequal $plat unix]  {
		myEcho DBUG "Can't launch server from halfd.  Use HLBP."
		return
	}

	cancelEvents

	set vote(insession) 0

	if ![file exists $fds(cmd)] {
	if [catch {exec mkfifo $fds(cmd)} err] {
		set msg "FATAL Error creating command pipe: '$err'"
		myEcho ERR $msg
		sendClient ERR  $msg
		myExit
	}
	catch { chmod 0664 $fds(cmd) }
	}

	# Copy existing log file to .old
	if $config(rotatetype) {
		set yymmddhhmm [clock format [clock seconds] -format %Y%m%d%H%M]
		if [cequal $plat windows] { append yymmddhhmm .log }
		set mylogtail [file tail $fds(log)]
		if [catch {file rename -force $fds(log) $myhldir/$gameId/$config(oldlogsdir)/$mylogtail.$yymmddhhmm} err] {
			myEcho WARN "Error renaming log file: $err"
		}
	} else {
		catch {file rename -force $fds(log) $fds(log).old}
	}

	set fds(cmdfd) [open $fds(cmd) {RDWR NONBLOCK}]
	fcntl $fds(cmdfd) CLOEXEC
	fcntl $fds(cmdfd) NOBUF

	# Delete custom.hpk if so configured
	if $config(deletecust) {
		if ![catch { file size $gameId/custom.hpk } err] {
			myEcho INFO [format $text(178) $err]
			if [catch { unlink $gameId/custom.hpk } err] {
				myEcho WARN [format $text(179) $err]
			}
		} else {
			myEcho INFO "Couldn't read custom.hpk: $err"
		}
	}
		

	set myArgs $config(args)
	set ip [determineIP]
	if ![cequal $ip ""] { append myArgs " +ip $ip" }

	if $config(hlds_boost) {
		if [file exists booster.so] {
			set env(LD_PRELOAD) "$myhldir/booster.so"
			set env(HLDS_FPS)   200
			append myArgs " +sys_ticrate 10000 +sv_maxupdaterate 100"
			myEcho INFO $text(289)
		} else {
			myEcho WARN $text(288)
		}
	}

	# START THE SERVER
	myEcho INFO $text(32)
	myEcho INFO "./$config(hlds_run) -game $gameId -port $config(port) $myArgs"

	if [catch {exec \
		./$config(hlds_run) -game $gameId -port $config(port) \
		$myArgs >& $fds(log) < $fds(cmd) &} \
	    pid] {

		catch {
			unset env(LD_PRELOAD)
			unset env(HLDS_FPS)
		}

		sendClient ERR [format $text(33) $pid]
		myEcho     ERR [format $text(33) $pid]
		serverDown

		if $config(rs) {
			set msg  [format $text(34) $config(rsDelay)]
			statusBar $msg
			myEcho INFO $msg
			set after(launch) [after [expr $config(rsDelay) * 1000] launchServer]
		}

		return
	}

	# Get rid of booster envvars if they're set.  LD_PRELOAD will hose
	# any other executables (e.g. ifconfig, mail)
	catch {
		unset env(LD_PRELOAD)
		unset env(HLDS_FPS)
	}

	set fd [open $fds(pid) w]
	puts $fd $pid
	close $fd

	mySleep 5
	statusBar "The server is starting.  Waiting for it to come all the way up..."
	mySleep 10

	myEcho INFO [format $text(35) $pid $gameId]

	initModMax
	
	set fds(pidnum) $pid
	set fds(logfd) [open $fds(log) {RDONLY NONBLOCK}]

if 0 {
	if [info exists fds(rma)] {
		catch [close $fds(rma)]
	}
	set fds(rma) [open $myhldir/$gameId/hlds_l.log.rma w]
}

	fcntl $fds(logfd) CLOEXEC
	fcntl $fds(logfd) NOBUF

	myEcho DBUG [format $text(268) [pid] $fds(pidnum)]

	serverUp
	processLogFile 1 1
	getServerStatus 1
}

proc initModMax {} {
	global svrInfo after text config

	set svrInfo(starttime) [clock seconds]
	set svrInfo(expired) 0

	if !$config(modswitch) return

	if $config(modmax) {
		set msg [format $text(182) $config(modprimary) $config(modmax)]
		myEcho INFO $msg
		sendClient TEXT $msg
		
		# In 10 minutes, tell the users about the impending switch
		set after(modmax) [after [expr 10 * 60 * 1000] { say [format $text(182) $config(modprimary) $config(modmax)] } ]
	}
}

proc adminPsay { saytext userid } {
	global config

	# Save off configured 'saycmd'
	set mySayCmd $config(saycmd)
	set config(saycmd) "admin_command admin_psay"

	# Say it using admin_psay
	say $saytext $userid

	# Restore configured 'saycmd'
	set config(saycmd) $mySayCmd

	return
}

proc say { { saytext "" } { userid "" } } {
	global fds config text

	if ![cequal [string first "%L" $saytext] -1] {
		# Replace %L with newlines
		myEcho DBUG "Found %L in say. Splitting line."

		regsub -all %L $saytext \n foo
		set list [split $foo \n]

		foreach el $list {
			say $el $userid
		}

		return
	}

	# Configurable "say" thanks to Uli Stark.
	if ![cequal $saytext ""] { 
		# If we try to say more that 122 characters, hlds_run will hang.  Bleh.
		if { [clength $saytext] > 100 } {
			# Make it 100 to be safe.  It won't display that many anyway.
			myEcho WARN [format $text(249) [clength $saytext]] 
			myEcho WARN $saytext
			set saytext [csubstr 0 100 $saytext]
		}

		if [cequal $config(saycmd) "admin_command admin_psay"] {
			set cmd "$config(saycmd) $userid $saytext"
		} else {
			set cmd "$config(saycmd) $saytext"
		}
		execCmd $cmd

		if { $config(saysleep) > 0 } {
			mySleep $config(saysleep)
		}
	}
}

proc displayVoteProgress {} {
	global votes text

	foreach uid [array names votes] {
		set map $votes($uid)
		if ![info exists mapcnt($map)] {
			set mapcnt($map) 1
		} else {
			incr mapcnt($map)
		}
	}

	set winmap   ""
	set winvotes 0
	foreach map [array names mapcnt] {
		if { $mapcnt($map) > $winvotes } {
			set winmap $map
			set winvotes $mapcnt($map)
		}
	}

	if { $winvotes > 0 } {
		set msg [format $text(196) $winmap $winvotes]
		myEcho DBUG $msg
		say $msg
	}
}

proc tallyVotes { {voteclosed 1} } {
	global vote votes userInfo config svrInfo after text last gameId

	# Get the latest list of users; anyone that voted and is not
	# logged on will be ignored.
	getServerStatus 1 1

	set map ""
	set cnt 0

	set numBots 0
	set i 0
	foreach wonid $userInfo(wonid) {
		if [cequal $wonid 0] {
			if [cequal [lindex $userInfo(ips) $i] 127.0.0.1] {
				myEcho DBUG "Not counting wonid 0 ([lindex $userInfo(users) $i]) in votes..."
				incr numBots
			}
		}
		incr i
	}

	set oneVote 0
	foreach uid [array names votes] {
		if [catch {lsearch -exact $userInfo(userid) $uid} idx] {
			set idx -1
		}

		if [cequal $idx -1] {
			myEcho DBUG "User # '$idx' voted but left the game."
			myEcho DBUG "Discarding vote: $votes($uid)"
			catch {unset votes($uid)}
			continue
		}

		lassign [getUserInfo userid $uid] name foo wonid ip
		if [cequal $wonid 0] {
			incr numBots -1
			myEcho DBUG "A bot ($name) voted(!)...decrementing numBots"
		}

		set map $votes($uid)
		if ![info exists goodVotes($map)] {
			set oneVote 1
			set goodVotes($map) 1
		} else {
			incr goodVotes($map)
		}

		# Clear out the vote, it's been counted or discarded
		if $voteclosed {
			catch {unset votes($uid)}
		}
	}

	# Now find the map with the highest # of votes
	if $oneVote {
		set highVoteCnt 0
		set highVoteMap ""

		foreach map [array names goodVotes] {
			myEcho DBUG [format $text(52) $map $goodVotes($map)]
			say [format $text(52) $map $goodVotes($map)]
			if { $goodVotes($map) > $highVoteCnt } {
				set highVoteCnt $goodVotes($map)
				set highVoteMap $map
			}
		}

		set map $highVoteMap
		set cnt $highVoteCnt

		# Check to see if we have a majority
		# Don't count clients that are connecting
		set numConn  [llength [lmatch $userInfo(ips) CONNECTING]]
		set numUsers [expr [llength $userInfo(users)] - $numConn]

		# Don't count bots
		set numUsers [expr $numUsers - $numBots]

		myEcho DBUG "Total: [llength $userInfo(users)], Conn: $numConn, Bots: $numBots, Real: $numUsers"

		set majority [expr [expr $numUsers / 2] + 1]

		if { $majority <= $config(minvotes) } {
			set max $majority
		} else {
			set max $config(minvotes)
		}

		if { $cnt >= $max } {
			if !$voteclosed {
				say [format $text(206) $map $cnt]
				myEcho DBUG [format $text(206) $map $cnt]
			}
		} else {
			# Not enough votes for a winner
			say [format $text(56) $max $numUsers]
			myEcho DBUG [format $text(56) $max $numUsers]
			set map ""
		}
	} else {
		# No votes
myEcho DBUG "Checking vote-message suppress: $svrInfo(player) users connected"
		if $voteclosed {
			if $svrInfo(player) { say $text(57) }
			myEcho DBUG $text(57)
		} else {
			if $svrInfo(player) { say $text(205) }
			myEcho DBUG $text(205)
		}
	}

	return [list $map $cnt]
}

proc endVoting {{winner ""}} {
	global vote votes userInfo config svrInfo after text last gameId stats
	global votewins

	set last(vote) [clock seconds]

	if [cequal $winner ""] {
		if !$config(vote) {
			# Voting was canceled
			return
		}

		if !$vote(insession) {
			myEcho WARN $text(36)
			return
		}

myEcho DBUG "Checking vote-message suppress: $svrInfo(player) users connected"
		if $svrInfo(player) { 
			myEcho DBUG $text(50)
			say $text(50)
		}

		lassign [tallyVotes] map cnt
	} else {
		myEcho INFO "Forcing $winner to win the vote..."
		if [cequal $winner extend] { set winner $svrInfo(map) }
		set map $winner
		set max 0
		set cnt [llength $userInfo(users)]

		# Discard any active votes
		foreach id [array names votes] {
			catch { unset votes($id) }
		}
	}

	set vote(insession) 0
	catch { after cancel $after(vote) }

	if [cequal $map ""] return

	# If we got here, we got enough votes!
	incr stats(votewins)

	if ![info exists votewins($map)] {
		lappend stats(votelist) $map
		set votewins($map) 1
	} else {
		incr votewins($map)
	}

	# Check to see if a MOD won...
	if ![cequal $config(votemodnames) ""] {
		set idx [lsearch -exact $config(votemodnames) $map]
		if ![cequal $idx -1] {
			set dir [lindex $config(votemoddirs) $idx]
			if [cequal $dir $gameId] {
				# They voted for the current mod... do nothing
				say [format $text(176) $gameId]
				myEcho DBUG [format $text(176) $gameId]
				return
			}

			say [format $text(172) $map $cnt]
			myEcho INFO [format $text(172) $map $cnt]
			say [format $text(173) $map]
			myEcho DBUG [format $text(173) $map]

			# Test to see if the MOD is already running, or doesn't
			# exist

			if [catch { exec hlcmd -g $dir port } err] {
				# We got an error.  The mod is either not running, or
				# doesn't exist.
				#
				# hlcmd reports a 2-digit error code if it finds
				# an error...
				#
				set errcd [csubstr $err 0 2]
				if [cequal $errcd 01] { 
					# The mod exists and is not running
					set err ""
				}
			} else {
				set err "MOD '$map' is already running in '$dir'!"
			}

			if ![cequal $err ""] {
				say $text(175)
				myEcho ERR "Error changing mod: $err"
				myEcho ERR "Command was: 'hlcmd -g $dir start'"
				return
			}

			# We're probably OK.
			say $text(174)
			myEcho DBUG $text(174)
			say [format $text(177) "$svrInfo(ip):$config(port)"]
			myEcho DBUG [format $text(177) "$svrInfo(ip):$config(port)"]
			say $text(194)
			myEcho DBUG $text(194)

			mySleep 3
			stopServer

			# Wait a few seconds.  Sometimes the new server has trouble starting.
			# Weird...
			mySleep 5

			if [catch { exec hlcmd -g $dir start } err] {
				# Yikes.  Problem starting the new MOD.  Re-start this one!
				myEcho ERR "Error changing mod: $err"
				myEcho ERR "Command was: 'hlcmd -g $dir start'"
				launchServer
				return
			} else {
				myEcho DBUG "Success, hlcmd returned: '$err'"
				return
			}
		}
	}

	say [format $text(53) $map $cnt]
	myEcho DBUG [format $text(53) $map $cnt]

	# Check to see if map exists and changelevel to it
	if { [lsearch -exact [getMaps] $map] != -1 } {
		if ![cequal $map $svrInfo(map)] {
			# Check for 'restart'
			if [cequal $map restart] { set map $svrInfo(map) }

			say [format $text(54) $map]
			myEcho DBUG [format $text(54) $map]
			mySleep 10
			changeLevel $map
		} else {
			# The current map won
			set svrInfo(maptime) [expr $svrInfo(orglimit) + $svrInfo(maptime)]
			say [format $text(55) $map $svrInfo(orglimit) $svrInfo(maptime)]
			myEcho DBUG [format $text(55) $map $svrInfo(orglimit) $svrInfo(maptime)]

			execCmd "mp_timelimit $svrInfo(maptime)"
		
			# Reschedule voting
			set sched [scheduleVoting] 
			if ![cequal $sched ""] {
				set after(vote) $sched
			}
		}

	}
}


proc preprocessName { name } {
	regsub -all ";" $name "," myName
	#set myName [lindex [split $name ";"] 0]
	return $myName
}
	

proc checkForVote { name uid datavar } {
	global votes svrInfo userInfo exprs config text

	upvar $datavar data

	set gotVote 0

	# This will match "vote <mapname>"
	set rc [regexp -nocase -- $exprs(vote) $data foo foo map]

	myEcho DBUG "vote rc = $rc on '$data'"

	if $rc {
		myEcho DBUG "*** name/id/map = '$name'/'$uid'/'$map'"

		set map [string tolower $map]

		if [cequal [crange $map end-3 end] .bsp] {
			set map [crange $map 0 end-4]
		}

		# Check for valid uid
		if { [lsearch -exact $userInfo(userid) $uid] == -1 } {
			myEcho DBUG "*** didn't find '$uid' in '$userInfo(userid)'"
			return $gotVote
		}

		# Check this map against the "novotemaps" list
		if ![cequal [lsearch -exact $config(novotemaps) $map] -1] {
			say [format $text(158) $map]
			myEcho DBUG "$name voted for banned map $map, discarding vote..."
			return $gotVote
		}

		# If they said 'extend', they really meant the current map
		if [cequal $map extend] { set map $svrInfo(map) }

		if [cequal $svrInfo(map) $map] {
			# They voted for the current map
			# Check to see if extension limit has been reached
			if { $svrInfo(maptime) >= $config(maxextend) && \
				![cequal $config(maxextend) 0] } {
myEcho DBUG "Refusing to extend $map, maptime=$svrInfo(maptime), max=$config(maxextend)"
				say [format $text(159) $config(maxextend)]
				return $gotVote
			}
		}

		#set name [preprocessName $name]
		# Check for valid map
		if { [lsearch -exact $config(votemodnames) $map] != -1 } {
			say [format $text(171) $name $map]
			myEcho DBUG [format $text(171) $name $map]
			set votes($uid) $map
			set gotVote 1
		} elseif ![lempty $config(votemaps)] {
			if { [lsearch -exact $config(votemaps) $map] != -1 } {
				say [format $text(58) $name $map]
				myEcho DBUG [format $text(58) $name $map]
				set votes($uid) $map
				set gotVote 1
			} else {
				say [format $text(158) $map]
			}
		} elseif { [lsearch -exact [getMaps] $map] != -1 } {
			if [cequal $svrInfo(map) $map] {
				say [format $text(190) $name $map]
				myEcho DBUG [format $text(190) $name $map]
			} elseif [cequal $map restart] {
				say [format $text(262) $name $map]
				myEcho DBUG [format $text(262) $name $map]
			} else {
				say [format $text(58) $name $map]
				myEcho DBUG [format $text(58) $name $map]
			}
			set votes($uid) $map
			set gotVote 1
		} else {
			say [format $text(59) $name]
			myEcho DBUG [format $text(59) $name]
		}
	}

	return $gotVote
}

proc scheduleVoting {} {
	global svrInfo config vote after text
	if { !$config(vote) || !$config(autovote) } return

	catch { after cancel $after(vote) }
	set vote(insession) 0

	set mapsecs [expr $svrInfo(orglimit) * 60]

	# Voting should start 'votetime' + 'autovotetime' seconds before
	# the map ends
	set pollstart [expr [clock seconds] + $mapsecs -\
		$config(votetime) - $config(autovotetime)]
	set pollstart [expr $pollstart - [clock seconds]]
	statusBar [format $text(91) $pollstart]
	if { $pollstart > 0 } {
		return [after [expr $pollstart * 1000] { beginVoting }]
	}

	return ""
}

proc readMapCycle {} {
	global cycletime mapCycleList text gameId myhldir svrInfo config

	if ![info exists cycletime] { set cycletime 0 }

	if [cequal $svrInfo(mapcyclefile) ""] {
		myEcho DBUG "No mapcyclefile, skipping readMapCycle..."
		set mapCycleList ""
		return ""
	}

	set cycleFile $myhldir/$gameId/$svrInfo(mapcyclefile)
	if ![file exists $cycleFile] {
		myEcho ERR [format $text(193) $cycleFile]
		set mapCycleList ""
		return ""
	}

	set mtime [file mtime $cycleFile]

	if ![cequal $mtime $cycletime] {
		if [catch {open $cycleFile r} fd] {
			myEcho ERR [format $text(37) $fd]
			return ""
		}

		set mapCycleList ""

		while 1 {
			set data [string trim [gets $fd]]
			if [eof $fd] break

			# Ignore comments and blank lines
			if [cequal $data ""] continue
			if [cequal [csubstr $data 0 2] "//"] continue

			# Strip off the maxplayers/n/minplayers/n crap
			if [catch { lindex $data 0 } map] { 
				set map [lindex [split $data " "] 0]
			}

			lappend mapCycleList $map
		}

		close $fd
	}

	if [cequal $config(votemaps) mapcycle] {
		set config(votemaps) $mapCycleList
		myEcho INFO [format $text(231) $mapCycleList]
	}

	myEcho DBUG "mapCycleList = '$mapCycleList'"
}

proc getNextMap { curMap } {
	global gameId myhldir cycletime mapCycleList text

	readMapCycle

	set idx [lsearch -exact $mapCycleList $curMap]
	incr idx
	if [cequal $idx [llength $mapCycleList]] { set idx 0 }
	set nextMap [lindex $mapCycleList $idx]

	return $nextMap
}

proc beginVoting {{forced 0}} {
	global vote svrInfo config after vnum text votestart

	# Check to see if we're in a voting session already
	if $vote(insession) {
		statusBar $text(92)
		return
	}

	set votestart [clock seconds]

	# Open the polls
	statusBar [format $text(93) [now]]

myEcho DBUG "Checking vote-message suppress: $svrInfo(player) users connected"
	if $svrInfo(player) {
	say [format $text(60) $vnum]
	say $text(61)
	mySleep 1
	say $config(votetext)
	mySleep 1
	if !$forced {
		say [format $text(63) $svrInfo(map) [expr $config(votetime) + $config(autovotetime)]]
		mySleep 1
		if { $config(nextmap) > 0 } {
			say [format $text(64) [getNextMap $svrInfo(map)]]
		}
	}
	say [format $text(65) $config(votetime)]
	}

	set after(tally) [after [expr $config(votetime) * 1000] endVoting]

	set vote(insession) 1
}

proc printMapList { pattern { cycle 0 } } {
	global text last mapCycleList config

	# print out a map list, 5 maps per line
	if [cequal $cycle 1] {
		say $text(138)
		readMapCycle; # Populate mapCycleList
		set maplist $mapCycleList
	} elseif { [cequal $cycle 2] && ![lempty $config(votemodnames)] } {
		say $text(244)
		set maplist $config(votemodnames)
	} elseif ![cequal $pattern ""] {
		say $text(104)
		if [catch {lmatch [getMaps] $pattern} maplist] {
			say $text(105)
			set maplist ""
		}
	} else {
		say $text(101)
		set maplist [getMaps]
	}

	mySleep 1

	if [cequal $maplist ""] {
		say $text(106)
	} elseif { [llength $maplist] > $config(maplistdisplay) } {
		say [format $text(107) [llength $maplist]]
	} else {
		set q 0

		while { [llength $maplist] > $q } {
			say [format $text(102) [lrange $maplist $q [expr $q+4]]]
			mySleep 1
			incr q 5
		}
		# say $text(103)
		set last(maplist) [clock seconds]
	}
}

# This function should be called *before* any execCmd if you're going
# to eat data afterward.
#
proc slurpLogFile {} {
	global fds dontprocess

	set needRefresh 0

	while 1 {
		set dontprocess 1
		set rc [getServerData data]
		if [cequal $rc -1] return
		set data [string trim $data]
		if { [cequal $data ""] && $rc } return
		if [cequal $data ""] continue
		doConsole data

		set dontprocess 0
		processLogLine data 1
	}
}

proc processLogLine { datavar { fromstatus 0 } { sendupdate 1 } } {
	global myhldir fds svrInfo config notIdle vote after exprs text
	global prevMap gameId stats last uidip 
	global szcnt team ignoretime vehiclekill wlimit initializing
	global statslog enterwait idlecache 
	global logline

	set needRefresh 0

	upvar $datavar data

	if [cequal $data ""] { return $needRefresh }

	set logline $data

if 0 {
	if ![info exists fds(rma)] {
		set fds(rma) [open $myhldir/$gameId/hlds_l.log.rma w]
	}
	puts $fds(rma) $data
	flush $fds(rma)
}

	# FILTERS
	if [regexp -- $exprs(usercnt)  $data] { return $needRefresh }
	if !$config(fahack) {
		if [regexp -- $exprs(ignore) $data] { return $needRefresh }
	}

	# Log to the gui any other (significant) messages
	if { [clength $data] > 5 } { 
		if $sendupdate { 
			sendClient TEXT $data
		} else {
			if [regexp -- $exprs(sendupdate) $data] {
				# Send it anyway
				sendClient TEXT $data
			}
		}
	}


	# Chat messages (new)
	if [regexp -- $exprs(chat) $data foo playerinfo chattype said] {
		processChatMessage $playerinfo $chattype $said
		return $needRefresh
	}

	# Chat messages (old)
	if $config(oldhack) {
		if [regexp -- $exprs(oldchat) $data foo name said] {
			set chattype say
			if [cequal [csubstr $name 0 6] "(TEAM)"] {
				set chattype say_team
				set name [csubstr $name 6 end]
			}

			set name [fixOldNames $name]

			lassign [getUserInfo name $name] name uid wonid ip

			if ![cequal $name -1] {
				set pinfo "$name<$uid>"
				processChatMessage $pinfo $chattype $said
			}
			return $needRefresh
		}
	}

	# If we're checking names, look for connected and entered
	if { $config(foulname) && $config(foulcheck) } {
		set rc [regexp -- $exprs(connected) $data foo pinfo ip] 
		if !$rc { set rc [regexp -- $exprs(entered) $data foo pinfo] }

		if $rc {
			lassign [parsePlayerInfo $pinfo] name uid wonid foo
			checkFoulName $name $uid $wonid
		}
	}

	# Look for 'nullname'
	if $config(nullname) {
		if [regexp -- $exprs(name) $data oldinfo newname] {
			if [cequal $newname ""] {
				lassign [parsePlayerInfo $oldinfo] name uid wonid foo
				myEcho NULLNAME "$oldinfo gets $nullpenalty $nullargs for null name."
				penalize $nullpenalty $nullargs $wonid $userid $name 
				return $needRefresh
			}
		}
	}

	# Look for connect, disconnect, and Dropped
	# messages.  We should refresh immediately upon these
	# conditions...
	if [regexp -- $exprs(refresh) $data] {

		if !$config(ipfromstats) {
			# Look for "connected" explicitly, so we can store their
			# IP addy
			if [regexp -- $exprs(connected) $data foo pinfo ip] {
				lassign [parsePlayerInfo $pinfo] name uid wonid foo
				set uidip($uid) $ip
				myEcho DBUG "Cached IP: uidip($uid) = $ip"
				set uidip(refresh) 1
			}

			# Look for "Dropped", we need to remove their IP if they
			# are gone
			if [regexp -- $exprs(dropped) $data] { 
				if ![info exists after(cleanuidip)] {
					# Housekeeping 30 minutes after drop
					set after(cleanuidip) [after 300000 cleanWonip]
				}
			}
		}

		if $config(lowstatus) {
			if [regexp -- $exprs(dropped) $data foo dname] {
				playerDropped $dname
			} elseif [regexp -- $exprs(connected) $data foo cinfo ip] {
				playerConnected $cinfo $ip
			} elseif [regexp -- $exprs(entered) $data foo cinfo] {
				set elapsed [expr [clock seconds] - $svrInfo(mapstart)]

				# They aren't leeching anymore...
				if $config(leech) {
					lassign [parsePlayerInfo $cinfo] name uid wonid foo
					if $config(idle) {
						myEcho DBUG "updated uid $uid idlecache (entered the game)"
						set idlecache($uid) [clock seconds]
						assignTeam $uid IDLE
					} else {
						myEcho DBUG "removed uid $uid idlecache (entered the game)"
						catch { unset idlecache($uid) }
					}
				}

				if $config(banip) { set enterwait 1 }

				if { $elapsed < 15 } {
					myEcho DBUG "Skipping refresh on 'entered the game', < 15 secs since map start"
					set needRefresh 0
				} else {
					if { $config(debug) > 2 } { myEcho DBUG "entered refresh: [clock seconds]" }
					set needRefresh 0
					catch { after cancel $after(gs) }
					set after(gs) [after 3000 {getServerStatus 1 1}]
				}
			} else {
				set needRefresh 1
			}
		} else {
			set needRefresh 1
		}

		if !$config(fahack) { return $needRefresh }
	}

	# Count kills - not all mods support new logging standard.
	if [regexp -- $exprs(killcount) $data] {
		incr svrInfo(kills)
		if { $config(debug) > 1 } {
			myEcho DBUG "Counted a kill.  Total for map=$svrInfo(kills)"
		}
	}

	# Deal with kills (only works with new logging)
	set exp $exprs(killed)
	if $config(fahack) { 
		set exp $exprs(fakilled)
	} elseif $config(oldhack) {
		set exp $exprs(oldkilled)
	}

	if { $config(debug) > 2 } {
		myEcho DBUG "Checking for kill detail ($sendupdate): '$data'"
	}

	if [regexp -- $exp $data foo attacker attackee weapon properties] {
		processKill $attacker $attackee $weapon $properties

		# FA misses a linefeed on kills.  Really ugly :-(
		if $config(fahack) {
			set newdata [csubstr $properties 1 end]
#myEcho DBUG "fahack: fixing newline -- old = '$data'"
			if ![cequal $newdata ""] {
				myEcho DBUG "fahack: fixing newline -- new = '$newdata'"
				processLogLine newdata 1
			}
		}

		return $needRefresh
	}

	if { $config(tkcheck) && $config(sgtkcheck) } {
		if [regexp -- $exprs(sgkill) $data foo attacker attackee] {
			myEcho DBUG "Found sentry-gun kill: '$data' ($attacker => $attackee)"
			processKill $attacker $attackee SGKILL ""
			return $needRefresh
		}
	}

	if { $config(attackcheck) } {
		if [regexp -- $exprs(attacked) $data foo attacker attackee weapon properties] {
			processKill $attacker $attackee $weapon $properties 1
			return $needRefresh
		}
	}

	# Team join or change
	# Firearms blows the logging on this one, so screw 'em.
	if !$config(fahack) {
		set exp $exprs(team)
		if $config(oldhack) { 
			if [isGame action] { set exp $exprs(actionteam) }
			if [isGame si]     { set exp $exprs(siteam) }
		}

		if { [regexp -- $exp $data foo pinfo myteam] } {
			lassign [parsePlayerInfo $pinfo] name uid wonid foo
			assignTeam $uid $myteam
			myEcho DBUG "$name<$uid> is on team $myteam"
			updateTeams

			# Put teamswitch penalty call here
			if $config(teamcheck) {
				processPenalty team $uid teamTable teamcount
			}

			# Update idlecache
			if { $config(idle) || $config(leech) } {
				if { [cequal $myteam SPECTATOR] && $config(idle) } {
					# FIXME: this check may need to stay in...?
					#if [info exists idlecache($uid)]
						myEcho DBUG "added uid $uid to idlecache: [clock seconds]"
						set idlecache($uid) [clock seconds]
				} elseif { [info exists idlecache($uid)] && ![cequal $myteam SPECTATOR] } {
					myEcho DBUG "removed uid $uid from idlecache (team = '$myteam')"
					catch { unset idlecache($uid) }
				}
			}

			return $needRefresh
		}
	}

	if $config(oldhack) {
		# Look for a team change
		if { [regexp -nocase -- $exprs(oldchange) $data foo pinfo myteam] } {
			lassign [parsePlayerInfo $pinfo] name uid wonid foo
			assignTeam $uid $myteam
			myEcho DBUG "$name<$uid> is on team $myteam"
			updateTeams
			return $needRefresh
		}
	}

	# suicide
	if [regexp $exprs(suicide) $data foo player weapon properties] {
		processSuicide $player $weapon $properties
		return $needRefresh
	}


	# world events
	if [regexp $exprs(waction) $data foo event properties] {
		processWorldEvent $event $properties
		return $needRefresh
	}

	# role change
	if [regexp $exprs(role) $data foo pinfo rolename] {
		processRoleChange $pinfo $rolename
		return $needRefresh

		if { $config(teamcheck) > 1 } {
			processPenalty team $uid teamTable teamcount
		}
	}

	# mp_timelimit variable change
	if [regexp $exprs(timechange) $data] {
		if { $initializing || $ignoretime } {
			myEcho DBUG "timechange, init=$initializing, ignore=$ignoretime"
			# noop
		} else {
			# Somebody mucked with the timelimit.  Get the new one.
			#myEcho DBUG "Found time change, getting new timelimit..."
			getMapTime
			set ignoretime 0
		}
		return $needRefresh
	}

	# Look for 20 or more SZ_GetSpace: errors. in a row, fast.
	# This is usually fatal, and requires us to kill the
	# server process. Ouch.
	if [regexp -- $exprs(szget) $data] { incr szcnt }
	if { $szcnt > 20 } {
		myEcho ERR [format $text(41) [now]]
		sendClient ERR [format $text(41) [now]]

		stopServer 1 2
		if $config(rs) {
			statusBar [format $text(34) $config(rsDelay)]
			myEcho INFO  [format $text(34) $config(rsDelay)]
			mySleep $config(rsDelay)
			launchServer
		}
		return $needRefresh
	}

	# Look for logfile name -- this tells us we're on a new map
	if [regexp -- $exprs(log) $data foo logFile svrInfo(version)] {
		myEcho DBUG "Found start-of-map line: '$data'"
		myEcho DBUG "LOG:'$logFile'"
		myEcho DBUG "VER:'$svrInfo(version)'"

		initializeMap $logFile

		return $needRefresh
	}

	# Admin-mod control
	if $config(adminmod) {
		if [regexp -- $exprs(admincmd) $data foo command] {
			if ![regexp -- say $data] {
				specialClientCommands $command
				return $needRefresh
			}
		}
	}

	# bot-kick
	if [regexp -- $exprs(botkick) $data foo botid] {
		# We shouldn't get here with any "say" messages, but better safe than
		# sorry...
		if ![regexp -- say $data] {
			kickBot $botid
			return $needRefresh
		}
	}

	if $config(checkwpt) {
		if [regexp -- $exprs(mapload) $data foo mapname] {
			# If no .wpt file, turn off bots (HPB_Bot only)
			set fname $myhldir/$gameId/maps/$mapname.wpt
			if ![file exists $fname] {
				myEcho DBUG "No wpt found for $mapname.  Setting max_bots to 0."
				execCmd "bot \"max_bots 0\""
			} else {
				myEcho DBUG "Found wpt file for $mapname."
			}
		}
	}

	if [regexp -- $exprs(mapstart) $data foo mapname] {
		set svrInfo(map) $mapname
		set prevMap $svrInfo(map)
		set initializing 0
		set ignoretime   0
		after 1000 newMap
		if !$fromstatus { getServerStatus 1 1 }
	}

	if [regexp -- $exprs(cvar) $data foo var value] {
		cvarChanged $var $value
	}

	return $needRefresh
}

proc initializeMap { logFile } {
	global config svrInfo tkcache attackcache team votes after text plat
	global deaths role

	# Do all this stuff here instead of newMap, bots connect too damn fast :)
	set ignoretime 1

	# Check to see if 'modmax' expired...
	if $config(modmax) {
		if $svrInfo(expired) {
			myEcho DBUG "On a new map and modmax has expired.  Switching server."
			endVoting $config(modprimary)
			return
		}
	}

	# We're on a new map now, so blow away caches
	# Clear the TK-and-TA-forgive caches
	catch {
		foreach id [array names tkcache] { catch { unset tkcache($id) } }
	}

	catch {
		foreach id [array names attackcache] { catch { unset attackcache($id) } }
	}

	# Clear the team array
	catch {
		foreach id [array names team] { catch { unset team($id) } }
	}

	# Blow away any leftover votes
	catch {
		foreach id [array names votes] { catch { unset votes($id) } }
	}

	foreach id [array names deaths] { unset deaths($id) }

	foreach id [array names role] { unset role($id) }

	set svrInfo(map) ""
	
	#if !$fromstatus { getServerStatus 1 1 }

	set myKills $config(deleteLogs)

	# Run stats if so specified
	if { $config(stats) && ![cequal $svrInfo(logFile) ""]} {
		# Only run tfstats if there were kills during the match
		if { $svrInfo(kills) >= $myKills || $config(alwaysrunstats) } {
			set statslog $svrInfo(logFile)
			if $config(statswait) {
				set after(stats) [after [expr $config(statswait) * 1000] runStats] 
				myEcho INFO [format $text(241) $config(statswait)]
			} else {
				runStats
			}
		} else {
			statusBar [format $text(94) $svrInfo(logFile)]
		}
	}

	# Delete log file if so specified
	if { $myKills && [expr $svrInfo(kills) < $myKills] && \
		![cequal $svrInfo(logFile) ""] } {

		if [catch { unlink $svrInfo(logFile) } err] {
			myEcho ERR "Logfile delete failed on '$svrInfo(logFile)': $err"
		}
		statusBar [format $text(95) $myKills $svrInfo(logFile)]
		myEcho INFO [format $text(95) $myKills $svrInfo(logFile)]
	}

	if [cequal $plat windows] {
		# Sigh.  Change backslashes to forward.
		set myLogFile $logFile
		regsub -all "\\\\" $myLogFile / logFile
	}

	set svrInfo(kills) 0
	set svrInfo(logFile) $logFile

}

proc cvarChanged { var value } {
	global config text

	if { $config(debug) > 1 } { myEcho DBUG "cvar '$var' = '$value'" }

	if ![isGame cstrike] {
		if { $config(idle) && [cequal $var allow_spectators] } {
			if [cequal $value 1] {
				myEcho WARN $text(267)
				set config(idle) 0
			}
		}
	}

	if !$config(admininstalled) {
		if [cequal $var admin_mod_version] {
			myEcho INFO "Adminmod is installed - version $value"
			set config(admininstalled) 1
		}
	}

	if ![cequal $config(enforcepass) ""] {
		if [cequal $var sv_password] {
			myEcho DBUG "Checking enforcepass on password change..."
			if [cequal $value "***PROTECTED***"] {
				set value [getServerVariable sv_password]
			}
			if { [cequal $value none] || [cequal $value ""] } {
				if [cequal $config(enforcepass) RANDOM] {
					set myRandom [format %06s [random 999999]]
					set pass "halfd-$myRandom"
				} else {
					set pass $config(enforcepass)
				}

				execCmd "sv_password $pass"
				myEcho WARN [format $text(281) $pass]
			}
		}
	}
}

proc runStats {} {
	global config statslog myhldir gameId text

	regsub -all -- "%L" $config(statsargs) $statslog args
	set cmd "$config(statspath) $args"
	catch {system $cmd >> $myhldir/$gameId/stats.out 2>&1} err
	myEcho INFO [format $text(42) $cmd $err]
	statusBar [format $text(43) $statslog]
	myEcho INFO [format $text(43) $statslog]

	# TFstats has a tendency to barf occasionally...
	# catch { unlink core }
}

proc teamLookup { myteam } {
	global teammap

	if [cequal $myteam -]    { return $myteam }
	if [cequal $myteam IDLE] { return $myteam }

	set myteaml [string tolower $myteam]
	if ![catch { set teammap($myteaml) } err] {
		set myteam $teammap($myteaml)
	} else {
		myEcho DBUG "Team '$myteaml' is not in teammap file"
	}

	return $myteam
}

proc assignTeam { uid myteam } {
	global team teammap config

	if { $config(debug) > 1 } {
		myEcho DBUG "assignTeam <$uid> = <$myteam>"
	}

	set team($uid) $myteam

	updateTeams 0
}

proc kickBot { uid } {
	global text

	lassign [getUserInfo userid $uid] name foo wonid ip
	if [cequal $name -1] {
		myEcho WARN "Bogus userid in kickBot: '$uid'"
		return
	}

	#set name [preprocessName $name]
	if ![cequal $wonid 0] {
		myEcho INFO "Refusing to bot-kick $name/$uid/$wonid - WONid is != 0"
		say "$name<$uid> isn't a bot!"
	} else {
		execCmd "kick # $uid"
		set msg "Kicked BOT: $name<$uid>" 
		myEcho INFO $msg
	}
}

proc processRoleChange { player rolename } {
	global config text role

	lassign [parsePlayerInfo $player] name uid wonid team
	if ![cequal $team ""] { assignTeam $uid $team }

	if $config(getmodelsfromrole) {
		set role($uid) $rolename
		myEcho DBUG "Cached role '$rolename' for $name<$uid>"
		sendClient UPDATE [buildClientUpdate]
	}
}

proc processSuicide { player weapon properties } {
	global config text

	lassign [parsePlayerInfo $player] name uid wonid team
	if ![cequal $team ""] { assignTeam $uid $team }

	incrDeaths $uid

	# Suicide penalties
	if $config(suicidecheck) {
		myEcho DBUG "$name<$uid><$wonid><$team> committed suicide ($weapon) ($properties)"
		set myteam [string tolower $team]
		set teams $config(suicideteams)
		if { ![cequal [lsearch $teams $myteam] -1] || [cequal $teams all] } {
			processPenalty suicide $uid suicideTable suicidecount
		} else {
			myEcho DBUG "$name wasn't on a suicide team ('$config(suicideteams)')"
		}
	}

	# Special messages
	set suicide_list [array names config suicide.*]
	if ![lempty $suicide_list] {
		foreach weaparr $suicide_list {
			set weapname [lindex [split $weaparr .] 1]
			if [cequal $weapon $weapname] {
				#regsub -all "\%P" $config($weaparr) [preprocessName $name] mymsg
				regsub -all "\%P" $config($weaparr) $name mymsg
				say $mymsg
			}
		}
	}
}

proc processKill { attacker attackee weapon properties { attack 0 } } {
	global config exprs wlimit text pluginReg

	lassign [parsePlayerInfo $attacker] rname ruid rwonid rteam
	lassign [parsePlayerInfo $attackee] ename euid ewonid eteam

	if { $config(debug) > 1 } {
		myEcho DBUG "KILL by $rname<$ruid><$rwonid><$rteam> on $ename<$euid><$ewonid><$eteam> (weap: '$weapon') (prop: '$properties')"
	}

	if ![cequal $rteam ""] { assignTeam $ruid $rteam }
	if ![cequal $eteam ""] { assignTeam $euid $eteam }

	checkFoulName $rname $ruid $rwonid
	checkFoulName $ename $euid $ewonid

	# Increment the death-count for the attackee
	if { !$attack && ![cequal $weapon SGKILL] } { incrDeaths $euid }

	if !$attack {
		# Plugins
		if [info exists pluginReg(kill)] {
			foreach proc $pluginReg(kill) {
				set args [list $rname $ruid $rwonid $rteam $ename $euid $ewonid $eteam $weapon $properties ""]
				runPlugin kill $proc $args
			}
		}

		# Special messages
		foreach weaparr [array names config weap.*] {
			set weapname [lindex [split $weaparr .] 1]
			if [cequal $weapon $weapname] {
				#regsub -all "\%P" $config($weaparr) [preprocessName $ename] mymsg
				regsub -all "\%P" $config($weaparr) $ename mymsg
				say $mymsg
			}
		}

		# Sentry-gun TK
		if [cequal $weapon SGKILL] {
			if [cequal $rteam $eteam] {
				# Found a SG-TK, enforce it
				myEcho DBUG "Found SG-TK against '$attacker'"
				say $text(145)

				foundTK $ruid $rname $euid $ename $rteam
				return
			}
		}

		# WEAPON-LIMIT
		if { $config(weaponlimit) && ![cequal $wlimit ""] } {
			if ![catch { lsearch -exact $wlimit $weapon } rc] {
				if ![cequal $rc -1] {
					foundWeapon $rname $ruid $weapon
				}
			} else {
				myEcho ERR "Error on lsearch for weapon: $rc"
				myEcho ERR "weapon='$weapon', wlimit='$wlimit'"
			}
		}

		# TK
		if { $config(tkcheck) } {
			if [cequal $rteam $eteam] {
				if ![cequal $rteam ""] {
					foundTK $ruid $rname $euid $ename $rteam
				} else {
					myEcho DBUG "Found a kill with blank teams - skipping TK"
				}
			}
		}
	} else {
		if [info exists pluginReg(attack)] {
			foreach proc $pluginReg(attack) {
				set args [list $rname $ruid $rwonid $rteam $ename $euid $ewonid $eteam $weapon $properties ""]
				runPlugin attack $proc $args
			}
		}

		# Attack
		if $config(attackcheck) {
			if [cequal $rteam $eteam] {

				# Don't penalize if they attacked themselves...
				if ![cequal $ruid $euid] {
					foundAttack $ruid $rname $euid $ename $rteam
				}

			}
		}
	}
}


proc backtrace {} {
	set rlist ""

	set level [info level]
	incr level -1

	loop i $level 0 -1 {
		lappend rlist  [info level $i]
	}

	return $rlist
}

proc parsePlayerInfo { pinfo {newlogging 0}} {
	global config exprs team text logline

	if [cequal [string trim $pinfo] ""] {
		myEcho DBUG "Got empty pinfo to parse: '$pinfo'"
		myEcho DBUG "data = '$logline'"
		myEcho DBUG "Call stack: [backtrace]"
		return [list "" "" "" ""]
	}

	set exp $exprs(player)

	# This exists to support the connect message, which ALWAYS
	# uses the new logging standard
	if !$newlogging {
		if $config(fahack) { 
			set exp $exprs(faplayer)
		} elseif $config(oldhack) {
			set exp $exprs(oldplayer)
		}
	}

	if ![regexp -- $exp $pinfo foo name uid wonid myteam] {
		if ![cequal [csubstr $pinfo 0 4] "<-1>"] {
			myEcho WARN [format $text(198) $pinfo]
			myEcho WARN $text(199)
			myEcho WARN "data = '$logline'"
			myEcho WARN "Call stack: [backtrace]"
		} else {
			myEcho DBUG [format $text(198) $pinfo]
			myEcho DBUG "data = '$logline'"
			myEcho DBUG "Call stack: [backtrace]"
		}
		return [list "" "" "" ""]
	}

	if !$newlogging {
		if $config(fahack) {
			set myteam $wonid
			set wonid [lindex [getUserInfo userid $uid] 2]
		} elseif $config(oldhack) {
			lassign [getUserInfo userid $uid] name uid wonid ip
			if [catch {set $team($uid)} myteam] {
				set myteam ""
			}
		}
	}

	if { $config(nullname) > 1 } {
		if [cequal $name ""] {
			lassign [parsePlayerInfo $oldinfo] name uid wonid foo
			myEcho NULLNAME "$oldinfo gets $nullpenalty $nullargs for null name."
			penalize $nullpenalty $nullargs $wonid $userid $name 
			return $needRefresh
		}
	}

	return [list $name $uid $wonid $myteam]
}

proc fixOldNames { name } {
	global config

	# Fix all the weird shit that mods do to names on a chat.
	foreach prefix $config(oldprefix) {
		set len [clength $prefix]
		if [cequal [csubstr $name 0 $len] $prefix] {
			set name [csubstr $name $len end]
		}
	}

	foreach postfix $config(oldpostfix) {
		set len [expr [clength $postfix] -1]
		if [cequal [csubstr $name end-$len end] $postfix] {
			set name [csubstr $name 0 end-$len]
		}
	}

	return $name
}

proc processChatMessage { pinfo chattype said } {
	global config text last svrInfo vote exprs myhldir gameId votestart
	global enterwait

	# Get the player's details
	lassign [parsePlayerInfo $pinfo] name uid wonid team
	if ![cequal $team ""] { assignTeam $uid $team }

	if { $config(debug) > 1 } {
		myEcho DBUG "CHAT by $name<$uid><$wonid><$team> $chattype '$said'"
	}

	if [isGame firearms] {
		# Firearms doesn't log the ^b chat messages...
		set chatteam ""
		if [cequal $chattype say_team] { set chatteam "(TEAM) " }
		set chatmsg "[format %c 0x02]$chatteam$name: $said"
		sendClient TEXT $chatmsg
	}

	set said [string tolower [string trim $said]]

	if [cequal $uid ""] return

	checkFoulName $name $uid $wonid

	# Look for HalfBot triggers
	if $config(halfbot) {
		halfBot $name $uid $wonid $team said
	}

	# Look for 'timeleft'
	if { $config(timeleft) > 0 } {
	if [cequal $said timeleft] {
		# Only allow this command once a minute, max
		if { [clock seconds] >= [expr $last(timeleft) + $config(timeleft)] } {
			say [format $text(66) $svrInfo(map) [fmtTimer]]
			set last(timeleft) [clock seconds]
		}
		return
	}
	}

	# Look for 'nextmap'
	if { $config(nextmap) > 0 } {
	if [cequal $said nextmap] {
		# Only allow this command once a minute, max
		if { [clock seconds] >= [expr $last(nextmap) + $config(nextmap)] } {
			say [format $text(100) [getNextMap $svrInfo(map)]]
			set last(nextmap) [clock seconds]
		}
		return
	}
	}

	# Look for 'maplist' if so configured
	if { $config(maplist) > 0 } {
	if [regexp -- $exprs(maplist) $said foo foo pattern] {
		if { [clock seconds] >= [expr $last(maplist) + $config(maplist)] } {
			set pattern [string trim $pattern]
			printMapList $pattern
		}
		return
	}
	}

	# Look for 'mapcycle' if so configured
	if { $config(mapcycle) > 0 } {
	if [cequal $said mapcycle] {
		if { [clock seconds] >= [expr $last(mapcycle) + $config(mapcycle)] } {
			printMapList "" 1
		}
		return
	}
	}

	if { $config(modlist) > 0 } {
	if [cequal $said modlist] {
		if { [clock seconds] >= [expr $last(modlist) + $config(modlist)] } {
			printMapList "" 2
		}
		return
	}
	}

	# Look for 'size'
	if { $config(size) > 0 } {
	if [regexp -- $exprs(size) $said foo foo map] {
		if { [clock seconds] >= [expr $last(size) + $config(size)] } {
			set map [string trim $map]
			regsub -nocase "\.bsp" $map "" szmap
			set fname $myhldir/$gameId/maps/${szmap}.bsp
			if [catch {file size $fname} size] {
				myEcho DBUG "size failed on $szmap: $size"
				say $text(121)
			} else {
				say [format $text(122) $szmap $size]
				set last(size) [clock seconds]
			}
		}
		return
	}
	}

	# Look for 'whattime'
	if { $config(whattime) > 0 } {
	if [cequal $said whattime] {
		# Only allow this command once a minute, max
		if { [clock seconds] >= [expr $last(whattime) + $config(whattime)] } {
			say [clock format [clock seconds]]
			set last(whattime) [clock seconds]
		}
		return
	}
	}

	# Look for 'votestatus'
	if { $config(votestatus) > 0 } {
	if [cequal $said votestatus] {
		# Only allow this command once a minute, max
		if { [clock seconds] >= [expr $last(votestatus) + $config(votestatus)] } {
			if $vote(insession) {
				tallyVotes 0
				set timeSoFar [expr [clock seconds] - $votestart]
				set timeRem [expr $config(votetime) - $timeSoFar]
				say [format $text(207) $timeRem]
				myEcho DBUG "votestatus timer: now=[clock seconds] votestart=$votestart used=$timeSoFar left=$timeRem"
			} else {
				say $text(208)
			}
			set last(votestatus) [clock seconds]
		}
		return
	}
	}

	# Look for 'mypenalty'
	if { $config(mypenalty) > 0 } {
	if [cequal $said mypenalty] {
myEcho DBUG "mypenalty triggered"
		# Only allow this command once a minute, max
		if { [clock seconds] >= [expr $last(mypenalty) + $config(mypenalty)] } {
			if $config(banip) {
				if $enterwait { getServerStatus 1 1 }
				# We need to get their IP address
				set uinfo [getUserInfo userid $uid]
				if [cequal $uinfo -1] return
				lassign $uinfo name foo wonid ip
				set id $ip
			} else {
				# Use their WONid
				set id $wonid
			}

			printPenalties $id
			set last(mypenalty) [clock seconds]
		}
		return
	}
	}


	# Look for switch to turn on voting
	if $config(vote) {
	if !$vote(insession) {
		if ![cequal $config(votekw) ""] {
			if { [clock seconds] >= [expr $last(vote) + $config(votefreq)] } {
			if [cequal $said $config(votekw)] {
				# check to see if enough time has elapsed since start-of-map
				if $config(votekwtime) {
					set elapsed [expr [clock seconds] - $svrInfo(mapstart)]
					if { $elapsed <= $config(votekwtime) } {
						myEcho DBUG "$config(votekw) found before enough time elapsed (have=$elapsed, need=$config(votekwtime)"
						return 
					}
				}
				# Somebody said the keyword
				beginVoting 1
				return
			}
			}
		}
	} else {
		if [checkForVote $name $uid said] { 
			if $config(voteprogress) { displayVoteProgress }
			return
		}
	}
	}

	# Foul language
	if $config(foulcheck) {
		if [catch {regexp -nocase $exprs(foul) " $said"} rc] {
			myEcho ERR [format $text(116) $rc]
		} else {
			if $rc { 
				foundFoulLang $name $uid $wonid said
				return
			}
		}
	}

	# TK-forgive
	if { $config(tkcheck) && $config(tkforgive) } {
		if [cequal $said "forgive tk"] {
			foundTKForgive $name $uid
			return
		}
	}

	# TA-forgive
	if { $config(attackcheck) && $config(attackforgive) } {
		if [cequal $said "forgive ta"] {
			foundTAForgive $name $uid
			return
		}
	}

	return
}


proc processWorldEvent { event properties } {
	global roundstart

	# Right now, we just look for "Round_Start" (CS only)
	if [cequal $event "Round_Start"] {
		myEcho DBUG "Found round start event"
		set roundstart [clock seconds]
	}
}

proc serverCrashed {} {
	global text stats fds config myhldir gameId svrInfo
	global iCrashLevel

	myEcho ERR [format $text(38) [now]]
	sendClient ERR  [format $text(38) [now]]
	incr stats(crash)

	set now [clock seconds]

	set emailtxt ""
	
	catch {
		if [cequal $svrInfo(name) ""] { set svrInfo(name) $text(293)    }
		if [cequal $svrInfo(ip) ""]   { set svrInfo(ip)   [determineIP] }
		if [cequal $svrInfo(port) ""] { set svrInfo(port) $config(port) }

		lappend emailtxt [format $text(285) $svrInfo(name)]
		lappend emailtxt [format $text(286) $svrInfo(ip) $config(port)]
		lappend emailtxt [format $text(287) $gameId]
		lappend emailtxt ""
	}

	# Create crashes/ dir if it isn't there
	if ![file exists crashes] { 
		catch {file mkdir crashes}
		chmod 0777 crashes
	}

	# Echo the last 5 lines in the log file
	catch { close $fds(logfd) }

	if [catch {open $fds(log) r} myfd] {
		myEcho WARN "Couldn't open $fds(log): $myfd"
	} else {
		set mySeek 1
		set offset [expr ( $config(lastNlines) * -80 ) - 1024]
		set numBytes [expr $offset * -1]

		if { $offset > 0 } { set offset INVALID }

		if { $config(debug) > 1 } {
			myEcho DBUG "lastN offset: $offset"
		}

		if [catch { seek $myfd $offset end } err] {
			myEcho DBUG "lastN seek returned: $err"
			# Read from the start
			set numBytes [file size $fds(log)] 
			if { $numBytes > 65535 } {
				set mySeek 0
				myEcho ERR "Can't get lastNlines...seek=$err, file size=$numBytes"
			}
		}

		if $mySeek {
			if [catch {
				set data [read $myfd $numBytes]

				if { $config(debug) > 1 } {
					myEcho DBUG "lastN data: '$data'"
				}

				# Remove blank lines
				set mydata ""
				foreach el [split $data \n] {
					if ![cequal [string trim $el] ""] {
						lappend mydata $el
					}
				}

				set msg  [format $text(211) $config(lastNlines)]
				myEcho ERR $msg

				lappend emailtxt $msg
				lappend emailtxt [replicate - 70]
				loop i $config(lastNlines) 0 -1 {
					set msg [lindex $mydata [expr [llength $mydata] -$i]]
					myEcho ERR $msg
					lappend emailtxt $msg
				}
				lappend emailtxt [replicate - 70]

			} err] {
				set msg "Error getting last N lines from hlds_l.log: $err"
				myEcho WARN $msg
				lappend emailtxt $msg
			}
		}

		catch { close $myfd }
	}

	# Put information at bottom of hlds_l.log letting them know it
	# crashed
	if [catch {open $fds(log) a} myfd] {
		myEcho WARN "Couldn't open $fds(log): $myfd"
	} else {
		puts $myfd "\n$text(269)"
		puts $myfd $text(270)
		if ![file exists core] {
			puts $myfd $text(271)
		} else {
			puts $myfd [format $text(272) $now]
		}

		foreach file "qconsole.log overflow.dat" {
			if [file exists $file] {
				puts $myfd [format $text(273) $file $now $file]
			}
		}

		puts $myfd $text(274)

		close $myfd
	}

	# Move the current log file to the crashes/ directory
	# and try to compress it
	catch {file copy -force $fds(log) crashes/$now.log}
	catch {exec gzip -v crashes/$now.log} err
	myEcho DBUG "gzip crash log returned: '$err'"

	set msg [format $text(250) "$myhldir/crashes/$now.log.gz"]
	myEcho INFO $msg
	myEcho INFO $text(251) 

	lappend emailtxt ""
	lappend emailtxt $msg
	lappend emailtxt $text(251)

	# If they exist, move qconsole.log and overflow.dat to crashes
	# and compress them
	if [file exists qconsole.log] {
		catch { file copy -force qconsole.log crashes/$now.qconsole.log }
		catch { exec gzip -v crashes/$now.qconsole.log }
	}

	if [file exists $gameId/overflow.dat] {
		catch { file copy -force $gameId/overflow.dat crashes/$now.overflow.dat }
		catch { exec gzip -v crashes/$now.overflow.dat }
	}


	# 
	if [file exists core] {
		myEcho ERR $text(39)
		lappend emailtxt ""
		lappend emailtxt $text(277)

		set gdb_fname halfd.gdb_commands

		if ![file exists $gdb_fname] {
			if [catch {open $gdb_fname} fd] {
				myEcho ERR "Couldn't open $gdb_fname: $fd"
			} else {
				puts $gdb_fname bt
				puts $gdb_fname quit
				close $gdb_fname
			}
		}

		if [catch {
			exec gdb $config(hlds_run) core > crashes/$now.backtrace < $gdb_fname
		} err] {
			set msg "Error processing core file: $err"
			myEcho WARN $msg
			lappend emailtxt $msg
		} else {
			myEcho DBUG "gdb returned '$err'"
			lappend emailtxt [format $text(278) $now.backtrace]
		}
		catch {file copy -force $fds(log) crashes/$now.log}

		mySleep 5
		catch {unlink core}

		myEcho ERR $text(40)
	}

	serverDown

	set subject [format $text(280) $gameId $myhldir]
	sendEmail $subject $emailtxt

	if ![info exists iCrashLevel] { set iCrashLevel [info level] }

	set crashLevel [expr [info level] - $iCrashLevel]
	set restartAttempts [expr $crashLevel / 3]

	myEcho DBUG "### Crash recovery complete" 
	myEcho DBUG "### Level      = [info level]"
	myEcho DBUG "### initLevel  = $iCrashLevel"
	myEcho DBUG "### crashLevel = $crashLevel"
	myEcho DBUG "### rsAttempts = $restartAttempts"

	set msg  [format $text(292) $restartAttempts $config(rsattempts)]
	myEcho INFO $msg

	if { $restartAttempts > 0 } {
		sendClient TEXT $msg
	}

	if { $restartAttempts >= $config(rsattempts) } {
		myEcho ERR $text(290)
		myEcho ERR $text(291)

		sendClient ERR $text(290)
		sendClient TEXT $text(291)

		set subject $text(291)
		set emailtxt [linsert $emailtxt 0 ""]
		set emailtxt [linsert $emailtxt 0 $text(291)]
		set emailtxt [linsert $emailtxt 0 $text(290)]
		set emailtxt [linsert $emailtxt 0 $msg]
		sendEmail $subject $emailtxt

		return
	}

	if $config(rs) {
		set msg [format $text(34) $config(rsDelay)]
		statusBar $msg
		myEcho INFO $msg
		mySleep $config(rsDelay)
		launchServer
	}
}

proc sendEmail { subj emailtxt {test 0}} {
	global text config gameId myhldir

	if [cequal $config(email) ""] {
		if $test { 
			myEcho INFO $text(282)
		} else {
			myEcho DBUG $text(282)
		}
		return
	}

	set myfile /tmp/halfd.email.[pid]
	if [catch { open $myfile w } fd] {
		myEcho WARN [format $text(279) $fd]
		myEcho INFO "Subject: $subj"
		return
	}

	foreach line $emailtxt {
		puts $fd $line
	}
	close $fd

	if [catch { exec $config(emailcmd) -s $subj $config(email) < $myfile } err] {
		myEcho WARN [format $text(279) $err]
	} else {
		myEcho INFO "Email sent to $config(email)"
	}

	catch { unlink $myfile }
}

proc processLogFile { {reschedule 0} {startup 0}} {
	global myhldir fds svrInfo config notIdle vote after exprs text
	global prevMap gameId stats last uidip 
	global szcnt serverUp plat dontprocess

	catch { after cancel $after(pl) }

	if $dontprocess {
		if $reschedule { set after(pl) [after 1000 {processLogFile 1}] }
		return
	}

	if ![info exists notIdle] {set notIdle 0}

	# Make sure it's still running...
	if !$serverUp return

	if [cequal $plat unix] {
		catch {wait -nohang}

		if [catch {kill 0 $fds(pidnum)} err] {
			serverCrashed
			return
		}
	} else {
		if !$svrInfo(hlbp) return
	}

	set i 0
	set szcnt 0
	set needRefresh 0

	if { ![cequal $svrInfo(map) $prevMap] && [cequal $prevMap ""] } {
		set prevMap $svrInfo(map)
		newMap
	}

	#if [cequal [csubstr $svrInfo(maprem) 0 1] "+"] {
	#	set mysecs [csubstr $svrInfo(maprem) 1 end]
	#}

	while 1 {
		if !$serverUp return
		set rc [getServerData data]
		if [cequal $rc -1] return
		set data [string trim $data]

		if { [cequal $data ""] && $rc } break
		if [cequal $data ""] continue
		doConsole data
		incr i

		set nr [processLogLine data 0]
		if $nr { set needRefresh 1 }
	}


	if [cequal $plat windows] {
		if $reschedule { set after(pl) [after 25000 {processLogFile 1}] }
	} else {
		if $reschedule { set after(pl) [after 2500 {processLogFile 1}] }
	}

	#if { [cequal $svrInfo(name) ""] || [cequal $svrInfo(map) ""] } {
	#	mySleep 1
	#	set needRefresh 1
	#}

	if { $needRefresh && !$startup } {getServerStatus 1 1}

	if !$notIdle { set notIdle $i }

	return $i 
}

# Returns 0 if more data
# Returns 1 if EOF
# Returns -1 if error
# 'datavar' will be updated with any data read.
proc getServerData { datavar } {
	global fds config hlbpCache plat hlbpFd dontprocess

	upvar $datavar data
	set data "X"

	set rc 0
	if [cequal $plat unix] {
		if [catch {gets $fds(logfd)} data] {
			myEcho ERR "Error reading from server: $data"
			set data ""
			set rc -1
		} else {
			if [cequal $data ""] {
				if [catch {eof $fds(logfd)} err] {
					myEcho ERR "Error checking EOF on server: $err"
					set rc -1
				} else {
					set rc $err
				}
			}
		}
		set dontprocess 0
	} else {
		# Make sure the cache gets filled if there's any data out there
		# (the readHlbp callback will get called here)
		set dontprocess 1
		update
		set dontprocess 0

		# Now just pull stuff from the cache
		#myEcho DBUG "fileevent on hlbpFd: [fileevent $hlbpFd readable]"
		set rc [lempty $hlbpCache]
		set data [string trim [lvarpop hlbpCache]]

	}

	if { $config(debug) > 2 } {
		myEcho DBUG "getServerData ($rc): '$data'"
	}

	return $rc
}

proc isGame { game } {
	global gameId

	return [cequal [csubstr $gameId 0 [clength $game]] $game]
}

proc readExclude {} {
	global myhldir gameId config exprs text exclude

	if !$config(exclude) return

	set fname $myhldir/$gameId/halfd.exclude

	foreach id [array names exclude] { unset exclude($id) }

	if [catch { open $fname r} fd] {
		myEcho ERR [format $text(30) $fd]
		set config(exclude) 0
		return
	}

	set exCnt 0
	while 1 {
		set data [string trim [gets $fd]]
		if [eof $fd] break

		if [cequal $data ""] continue
		if [cequal [csubstr $data 0 1] "#"] continue

		set exclude($data) 1
		incr exCnt

		myEcho DBUG [format $text(142) $data]
	}

	myEcho INFO [format $text(157) $exCnt]
}

proc readBadwords {} {
	global myhldir gameId config exprs text

	set fname $myhldir/$gameId/halfd.badwords

	set exprs(foul)  ""
	set exprs(foul1) ""
	set exprs(foul2) ""

	if [catch { open $fname r } fd] {
		if $config(foulcheck) {
			myEcho ERR [format $text(30) $fd]
		} else {
			myEcho DBUG [format $text(30) $fd]
		}
		set config(foulcheck) 0
		return
	}

	set i  0
	set l1 0
	set l2 0
	set lvl 1
	myEcho DBUG [replicate - 45]
	while 1 {
		set word [string trim [gets $fd]]
		if [eof $fd] break

		if { [cequal $word ""] && [cequal $lvl 1] } {
			set lvl 2
			set exprs(foul1) $exprs(foul)
			continue
		}

		if [cequal $word ""] continue

		if [cequal [csubstr $word end end] .] {
			# Found a period at the end.  Strip off the period and Just match 'word'
			set word [crange $word 0 end-1]
			myEcho DBUG "Only matching the exact word '$word'"
			set myword "$word "
		} else {
			# Match all text after "word" up to whitespace or EOL
			set myword "$word.*"
		}

		myEcho DBUG "Read level $lvl word: '$word'"

		if $i {
			append exprs(foul) "|$config(foulprefix)$myword"
		} else  {
			set exprs(foul) "$config(foulprefix)$myword"
		}

		incr i
		incr l$lvl
	}

	close $fd

	if $l2 {
		set idx [expr [clength $exprs(foul1)] + 1]
		if [cequal $idx 1] { set idx 0 }
		if { $idx < [clength $exprs(foul)] } {
			set exprs(foul2) [csubstr $exprs(foul) $idx end]
		}
	} else {
		set exprs(foul1) $exprs(foul)
	}

	myEcho INFO [format $text(117) $i halfd.badwords]
	myEcho INFO [format $text(118) $l1 $l2]
	myEcho DBUG "expr0 is '$exprs(foul)'"
	myEcho DBUG "expr1 is '$exprs(foul1)'"
	myEcho DBUG "expr2 is '$exprs(foul2)'"
	myEcho DBUG [replicate - 45]

	# Check to make sure regexps "compile"
	if [catch {regexp -nocase $exprs(foul) ""} rc] {
		myEcho ERR $text(140)
		myEcho ERR $rc
		set config(foulcheck) 0
	}

	return
}

proc foundWeapon { killer kilid weapon } {
	global text svrInfo

	say [format $text(163) $weapon $svrInfo(map)]
	myEcho DBUG  [format $text(163) $weapon $svrInfo(map)]

	myEcho WEAP "$killer<$kilid> $weapon $svrInfo(map)"
	processPenalty weapon $kilid weaponTable weaponcount
}

proc foundTAForgive { killee uid } {
	global attackcache attackcount text config
				
	set forgave 0

	foreach id [array names attackcache] {
#myEcho DBUG "checking $id against $uid"
		if [cequal $id $uid] {
			set kilid $attackcache($uid)
			set uinfo  [getUserInfo userid $kilid]
			if [cequal $uinfo -1] return

			if $config(banip) {
				set wonid  [lindex $uinfo 3]
			} else {
				set wonid  [lindex $uinfo 2]
			}

			attackTable decay $wonid

			catch { unset attackcache($uid) }

			set uinfo [getUserInfo userid $kilid]
			#set name [preprocessName [lindex $uinfo 0]]
			set name [lindex $uinfo 0]
			set mykillee [preprocessName $killee]
			say [format $text(156) $mykillee $name]
			myEcho DBUG "$mykillee forgave TA against $name<$uid>"
			set forgave 1
			break
		}
	}

	if !$forgave { 
		if $config(admininstalled) {
			adminPsay $text(275) $uid
		} else {
			say $text(275)
		}
	}
}

# tkcache(killeeid) = killerid
proc foundTKForgive { killee uid } {
	global tkcache tkcount text config
				
	set forgave 0

	foreach id [array names tkcache] {
#myEcho DBUG "checking $id against $uid"
		if [cequal $id $uid] {
			set kilid $tkcache($uid)
			set uinfo  [getUserInfo userid $kilid]
			if [cequal $uinfo -1] return

			if $config(banip) {
				set wonid  [lindex $uinfo 3]
			} else {
				set wonid  [lindex $uinfo 2]
			}

			tkTable decay $wonid

			catch { unset tkcache($uid) }

			set uinfo [getUserInfo userid $kilid]
			#set name [preprocessName [lindex $uinfo 0]]
			set name [lindex $uinfo 0]
			set mykillee [preprocessName $killee]
			say [format $text(156) $mykillee $name]
			myEcho DBUG "$mykillee forgave TK against $name<$uid>"
			set forgave 1
			break
		}
	}

	if !$forgave { 
		if $config(admininstalled) {
			adminPsay $text(155) $uid
		} else {
			say $text(155)
		}
	}
}

proc foundAttack { ruid rname euid ename rteam } {
	global roundstart config attackcache
	myEcho DBUG "Found Attack on $ruid (attacked # $euid)"

	myEcho DBUG "Assigned $ename ($euid) ATTACKED BY $rname ($ruid) to attack-forgive-cache (team=$rteam)"
	set attackcache($euid) $ruid

	# attackroundlimit logic
	set increment 1
	if $config(attackroundlimit) {
		set roundlimit [expr $roundstart + $config(attackroundlimit)]
		if { $roundlimit >= [clock seconds] } {
			myEcho DBUG "Attack by $ruid within $config(attackroundlimit) seconds!"
			myEcho DBUG "roundstart = [clock format $roundstart]"
			myEcho DBUG "now        = [clock format [clock seconds]]"
			myEcho DBUG "Setting Attack-penalty to $config(attackroundpenalty)"
			set increment $config(attackroundpenalty)
		} else {
			myEcho DBUG "Attack by $ruid not within $config(attackroundlimit) seconds."
			myEcho DBUG "roundstart = [clock format $roundstart]"
			myEcho DBUG "now        = [clock format [clock seconds]]"
		}
	}

	processPenalty attack $ruid attackTable attackcount $increment
}

proc foundTK { ruid rname euid ename rteam } {
	global roundstart config tkcache
	myEcho DBUG "Found TK on $ruid (attacked # $euid)"

	# Teams are equal, found a TK
	myEcho DBUG "Assigned $ename ($euid) KILLED BY $rname ($ruid) to tk-forgive-cache (team=$rteam)"
	set tkcache($euid) $ruid

	# tkroundlimit logic
	set increment 1
	if $config(tkroundlimit) {
		set roundlimit [expr $roundstart + $config(tkroundlimit)]
		if { $roundlimit >= [clock seconds] } {
			myEcho DBUG "TK by $ruid within $config(tkroundlimit) seconds!"
			myEcho DBUG "roundstart = [clock format $roundstart]"
			myEcho DBUG "now        = [clock format [clock seconds]]"
			myEcho DBUG "Setting TK-penalty to $config(tkroundpenalty)"
			set increment $config(tkroundpenalty)
		} else {
			myEcho DBUG "TK by $ruid not within $config(tkroundlimit) seconds."
			myEcho DBUG "roundstart = [clock format $roundstart]"
			myEcho DBUG "now        = [clock format [clock seconds]]"
		}
	}

	processPenalty tk $ruid tkTable tkcount $increment
}

proc processPenalty { type userid tableFunc indexArray { increment 1 } } {
	global config stats gameId exclude text enterwait kicked
	global $indexArray

	set warns ${type}warn
	set acts  ${type}acts

	proc msgUser { type name wonid warnleft action args counter } {
		global config

		set warnmsg ${type}warnmsg
		set penmsg  ${type}penmsg

		if [info exists kicked($wonid)] {
			myEcho DBUG "WONid $wonid has already been kicked, skipping warning to avoid spam..."
			return
		}

		if $warnleft {
			if ![cequal $config($warnmsg) ""] {
				regsub -all "%U" $action "" myaction

				regsub -all "\%P" $config($warnmsg) $name mymsg
				regsub -all "\%C" $mymsg $warnleft mymsg
				regsub -all "\%T" $mymsg $myaction mymsg
				myEcho DBUG "on warning, next action = '$action'"

				if $config(admininstalled) {
					adminPsay $mymsg $wonid
					if $config(broadcastwarn) {
						say $mymsg
					}
				} else {
					say $mymsg
				}

				myEcho DBUG $mymsg
				myEcho [string toupper $type] "\[WONID: $wonid\] $mymsg"
			}
		} else {
			set pentype $action
			if [cequal $action banid] {
				set pentype "banid $args"
			}

			regsub -all "%U" $pentype "" mypentype

			regsub -all "\%P" $config($penmsg) $name mymsg
			regsub -all "\%C" $mymsg $counter mymsg
			regsub -all "\%T" $mymsg $mypentype mymsg

			say $mymsg

			if $config(admininstalled) {
				adminPsay $mymsg $wonid
			}

			myEcho DBUG $mymsg
			myEcho [string toupper $type] "\[WONID: $wonid\] $mymsg"
		}
	}


	if $enterwait { getServerStatus 1 1 }

	set uinfo [getUserInfo userid $userid]
	if [cequal $uinfo -1] return
	lassign $uinfo name foo wonid ip
	if $config(banip) { set wonid $ip }
	#set name [preprocessName $name]

if ![cequal $type team] {
	myEcho DBUG "$type userid: '$userid'"
	myEcho DBUG "$type name  : '$name'"
	myEcho DBUG "$type wonid : '$wonid'"
	myEcho DBUG "$type incr  : '$increment'"
}

	if [cequal $increment 0] {
		myEcho DBUG "increment is zero -- bailing!"
		return
	}

	# Check to see if we should exclude them
	if $config(exclude) {
		if ![catch { set exclude($wonid) } err] {
			# They're excluded
			myEcho INFO [format $text(195) $type "$name:$wonid"]
			return
		}
	}

	# Add them to the array or increment their counter
	if [catch { set ${indexArray}($wonid) } err] {
		#myEcho DBUG "err = $err"
		set ${indexArray}($wonid) $increment
	} else {
		incr ${indexArray}($wonid) $increment
	}

	set counter [set ${indexArray}($wonid)]
if ![cequal $type team] { myEcho DBUG "counter=$counter" }

	$tableFunc write

	# Find out if they hit an actionable penalty or if they should
	# just be warned
	set idx 0
	set warnleft 0
	set action ""
	set warnaction ""
	set counts  ${type}counts
	set penalty ${type}penalty
	set args    ${type}args
	foreach cnt $config($counts) {
		if { $counter <= $cnt } {
			set warnaction [lindex $config($penalty) $idx]
			if [cequal $counter $cnt] {
				set action [lindex $config($penalty) $idx]
				set args   [lindex $config($args) $idx]
				if [cequal $action ""] { set action -1 }
				if [cequal $args   ""] { set args   -1 }
				set warnleft 0
			} else {
				set warnleft [expr $cnt - $counter]
				set action -1
			}
			break
		}

		incr idx
	}

	if [cequal $action ""] {
		# Somehow they are > than the last action...so use the last action
		set action \
			[lindex $config($penalty) [expr [llength $config($penalty)] -1]]
		set args \
			[lindex $config($args) [expr [llength $config($args)] -1]]
		set warnleft 0
	}

	if $warnleft {
		# Warn them
		msgUser $type $name $wonid $warnleft $warnaction $args $counter
		incr stats($warns)
	} else {
		# Broadcast kick/ban msg, then kick/ban them
		incr stats($acts)
		msgUser $type $name $wonid $warnleft $action $args $counter

		penalize $action $args $wonid $userid $name
	}
}

proc penalize { action args wonid userid name { reason "" } } {
	global text config kicked

	if { $config(debug) && ![cequal $action team] } {
		myEcho DBUG "Penalize called with: "
		myEcho DBUG "   action = $action"
		myEcho DBUG "   args   = $args"
		myEcho DBUG "   userid = $userid"
		myEcho DBUG "   wonid  = $wonid"
		myEcho DBUG "   reason = $reason"
	}


	switch -exact -- $action {
		banid {
			set kicked($wonid) 1
			# Let victim see their penalize message
			mySleep 1
			set bancmd ""
			if $config(banip) {
				set bancmd "addip $args $wonid"
				if [cequal $args 0] {

					# banip allows duplicate entries :(
					# Commented out - thanks to Vitaliy N. Soldatov
					#execCmd "exec listip.cfg"
					#myEcho DBUG "executed 'exec listip.cfg'"

					execCmd "addip 0 $wonid"
					myEcho DBUG "executed 'addip 0 $wonid'"
					execCmd "kick # $userid"
					myEcho DBUG "executed 'kick # $userid'"
					execCmd writeip
					myEcho DBUG "executed 'writeip'"
				} else {
					execCmd "addip $args $wonid"
					myEcho DBUG "executed 'addip $args $wonid'"
					execCmd "kick # $userid"
					myEcho DBUG "executed 'kick # $userid'"
				}
			} else {
				if [cequal $wonid 0] {
					myEcho WARN "Refusing to ban WONid 0 on '$name'!"
					myEcho WARN "Kicking with userid instead."
					execCmd "kick # $userid"
					myEcho DBUG "executed 'kick # $userid'"
				} else {
					set bancmd "banid $args $wonid"
					if [cequal $args 0] {
						execCmd "exec banned.cfg"
						myEcho DBUG "executed 'exec banned.cfg'"
						execCmd "banid 0 $wonid kick"
						myEcho DBUG "executed 'banid 0 $wonid kick'"
						execCmd writeid
						myEcho DBUG "executed 'writeid'"
					} else {
						execCmd "banid $args $wonid kick"
						myEcho DBUG "executed 'banid $args $wonid kick'"
					}
				}
			}

			if $config(banlog) {
				set logit 0
				if { [cequal $args 0] || [cequal $config(banlog) 2] } {
					if [catch {open halfd.banned a} fd] {
						myEcho ERR [format $text(245) halfd.banned $fd]
					} else {
						puts $fd $bancmd
						catch { close $fd }
					}
				}
			}
		}

		kick {
			set kicked($wonid) 1

			# Let victim see their penalize message
			mySleep 1

			execCmd "kick # $userid $reason"
			myEcho DBUG "executed 'kick # $userid $reason'"
	   }

	   default {
	   		# Assume admin-mod command

			# Look for %U
			set rc [regsub -all -- "%U" $action $userid myaction]
			if $rc {
				# it exists, use the new action
				execCmd "admin_command $myaction"
				myEcho DBUG "executed 'admin_command $myaction'"
			} else { 
				# It doesn't exist, use userid as the argument
				execCmd "admin_command $action $userid"
				myEcho DBUG "executed 'admin_command $action $userid'"
			}

		}
	}
}

proc checkFoulName { name uid wonid } {
	global config text exprs

	if !$config(foulcheck) return
	if !$config(foulname) return

	if [catch {regexp -nocase $exprs(foul) " $name"} rc] {
		myEcho ERR [format $text(116) $rc]
	} else {
		if $rc { 
			set said $name
			foundFoulLang $name $uid $wonid said
		}
	}
}

proc foundFoulLang { name uid wonid datavar } {
	global config exprs foulcount stats exclude text enterwait
	upvar $datavar data

	if [cequal $exprs(foul) ""] return

	set myData " $data"
	#set name [preprocessName $name]

	# Get the IP
	if $config(banip) {
		if $enterwait { getServerStatus 1 1 }
		lassign [getUserInfo userid $uid] x x x ip
		if [cequal $ip ""] {
			myEcho WARN "Couldn't process foul-language on '$name', no IP!"
			return
		}
		set wonid $ip
	}

	# Check to see if we should exclude them
	if $config(exclude) {
		if ![catch { set exclude($wonid) } err] {
			# They're excluded
			myEcho DBUG [format $text(144) "$name:$wonid"]
			return
		}
		myEcho DBUG "err on foul-lang exclude: $err"
	}

	myEcho FOUL "$name<$uid><$wonid>: $data"
	# Check to see what 'level' this is -- need to check Level 1 first!
	if { ![cequal $exprs(foul1) ""] && [regexp -nocase -- $exprs(foul1) $myData] } {
		# It's a level 1. handle it here.
		incr stats(foulacts)
		#set myname [preprocessName $name]
		set myname $name
		regsub -all -- "%P" $config(foulmsg1) $name mymsg
		myEcho FOUL $mymsg
		say $mymsg
		penalize $config(foulpenalty1) $config(foulargs1) $wonid $uid $name
	} elseif { ![cequal $exprs(foul2) ""] } {
		if [regexp -nocase -- $exprs(foul2) $myData] {
			# It's a Level 2.  Let processPenalty handle it.
			processPenalty foul $uid foulTable foulcount
		}
	}
}



proc cleanWonip {} {
	global uidip userInfo after config

	catch { after cancel $after(cleanuidip) }
	catch { unset after(cleanuidip) }

	if $uidip(refresh) { 
		myEcho DBUG "### skipping clean, we need a refresh..."
		set after(cleanuidip) [after 10000 cleanWonip]
		return
	}

	if !$config(ipfromstats) {
		set uidips [array names uidip]
		set uids   $userInfo(userid)

		set gone [lindex [intersect3 $uidips $uids] 0]

		foreach id $gone {
			if [cequal $id refresh] continue
			myEcho DBUG "### cleaning id: $id"
			unset uidip($id)
		}
	}
}

proc calcMapTimeRem {} {
	global svrInfo config text

	set maprem 0

	# If mp_timelimt = 0, set map rem to 0
	if [cequal $svrInfo(maptime) 0] {
		set svrInfo(maprem) 0
		return
	}

	if $svrInfo(mapstart) {
		set maprem ""

		set secs [expr [expr $svrInfo(maptime) + $config(minhack)] * 60]

		set mapsec [expr [expr $svrInfo(mapstart) + $secs] - [clock seconds]]

		set mapsec [expr int($mapsec)]

		incr mapsec -5

		if { $mapsec > 0 } {
			set maprem $mapsec
		} else {
			set maprem "+[expr abs($mapsec)]"
		}
	}

	set svrInfo(maprem) $maprem
}

proc fmtTimer {} {
	global svrInfo text
	if [cequal $svrInfo(maprem) 0] {
		set maprem ??:??
	} elseif ![ctype digit $svrInfo(maprem)] {
		set maprem $svrInfo(maprem)
	} else {
		set maprem ""
		set mapsec $svrInfo(maprem)
		set maphr  [expr $mapsec / 3600]
		if $maphr { set maprem "$maphr:" }

		set mapmin [expr [expr $mapsec / 60] % 60]
		set mapsec [format %02d [expr $mapsec % 60]]

		append maprem $mapmin:$mapsec
	}

	return $maprem
}

proc getMapTimeRem {} {
	global fds svrInfo exprs text gameId config

	# Only works for TFC, FLF and DoD
	# Make sure timelimit != 0
	if [cequal $svrInfo(maptime) 0] return

	set timeleft [getServerVariable mp_timeleft]

	set badval 0
	if [cequal $timeleft ""] { set timeleft 0 }
	if [cequal $timeleft 0] { set badval 1 }
	if [catch { expr int($timeleft) } err] { set badval 1 }
	if [cequal $err 0] { set badval 1 }

	if $badval {
		myEcho DBUG "This mod doesn't appear to support mp_timeleft (value is '$timeleft')"
		return
	}

	# Convert to integer so things don't blow up.
	# If this fails, assume time is unlimited.
	if [catch {expr int($timeleft)} time] {
		myEcho ERR "Couldn't convert mp_timeleft '$timeleft' to integer: $time"
	} else {

		set start [expr [clock seconds] - (($svrInfo(maptime) * 60) - $timeleft)]
		set svrInfo(mapstart) $start
		calcMapTimeRem
		if { $config(debug) > 1 } {
			myEcho DBUG "timeleft calc: "
			myEcho DBUG "   now     = [clock seconds]"
			myEcho DBUG "   maptime = $svrInfo(maptime)"
			myEcho DBUG "   mapsecs = [expr $svrInfo(maptime) * 60]"
			myEcho DBUG "   timelef = $timeleft"
			myEcho DBUG "   start   = $start"
		}

		#if [ctype digit $svrInfo(maprem)] { scheduleVoting }
	}
}

# Get the value of a server variable (e.g. mp_timelimit)
#
proc getServerVariable { var } {
	global fds exprs config plat

	# Set up the command and expression to parse the return
	set cmd $var
	regsub -all -- "%V" $exprs(varquery) $var expr

	if { $config(debug) > 2 } {
		myEcho DBUG "---> getServerVariable $var"
		myEcho DBUG "varquery expr is '$expr'"
	}

	# Sometimes we miss it...
	loop j 0 3 {
		slurpLogFile
		execCmd $cmd

		mySleep 0.5

		set rc NULL
		set cnt 0
		while 1 {
			set eof [getServerData data]
			if [cequal $eof -1] return

			set data [string trim $data]

			set myrc [regexp $expr $data foo rc]

			if { $config(debug) > 2 } {
				myEcho DBUG "varquery data is '$data'"
				myEcho DBUG "varquery rc   is '$myrc'"
				myEcho DBUG "varquery cnt  is '$cnt'"
			}

			if $myrc break

			processLogLine data 1
			doConsole data
			if { $cnt > 10 } { break }
			mySleep 1
			incr cnt
		}

		if ![cequal $rc NULL] {
			set len [clength $rc]
			set rc [csubstr $rc 0 [expr $len-1]]
			break
		}
	}

	if [cequal $rc NULL] { set rc "" }

	myEcho DBUG "Server variable '$var' is '$rc'"

	if { $config(debug) > 2 } {
		myEcho DBUG "<--- getServerVariable $var = $rc"
	}

	return $rc
}

proc checkForPassword {} {
	global config protected exprs

	if !$config(checkpass) return

	set svrPass [getServerVariable sv_password]
	if [cequal $svrPass "\""] { set svrPass "" }

	if { [cequal $svrPass ""] || [cequal $svrPass "none"] } {
		myEcho DBUG "Server is NOT password protected."
		set protected 0
	} else {
		myEcho DBUG "Server IS password protected."
		set protected 1
	}
}
		

proc getMapTime {} {
	global fds svrInfo exprs text

	set svrInfo(maptime) [getServerVariable mp_timelimit]
	if [cequal $svrInfo(maptime) ""] { set svrInfo(maptime) 0 }
	set svrInfo(maptime) [expr int($svrInfo(maptime))]

	# Convert to integer so things don't blow up.
	# If this fails, assume time is unlimited.
	if [catch {expr int($svrInfo(maptime))} time] {
		myEcho ERR "Couldn't convert mp_timelimit '$svrInfo(maptime)' to integer: $time"
		set time 0
	}

	set svrInfo(maptime) $time

	calcMapTimeRem
}

# this gets executed when the map has changed
proc newMap {} {
	global svrInfo config myhldir fds gameId vote after initializing stats
	global text votes tkcache team wlimit weaponcount nullMapCnt userInfo 
	global idlecache idlewarns

	myEcho DBUG "newMap executing on '$svrInfo(map)'"

	if [cequal $svrInfo(map) ""] {
		after 1000 newMap
		return
	}

	set nullMapCnt 0

	# Should we run custom config file?
	if $config(cm) {
		# Run custom config file if it's there...
		set cfgfile $myhldir/$gameId/$svrInfo(map).cfg
		if [file exists $cfgfile] {
			execCmd "exec $svrInfo(map).cfg"
			myEcho INFO [format $text(44) $svrInfo(map)]
		}
	}

	getMapTime
	checkForPassword

	set svrInfo(mapcyclefile) [getServerVariable mapcyclefile]
	readMapCycle

	set svrInfo(orglimit) $svrInfo(maptime)

	if !$initializing {
		set svrInfo(mapstart) [clock seconds]

		# Cache all currently-connected players
		if { $config(idle) || $config(leech) } {
			foreach uid [array names idlecache] {
				catch { unset idlecache($uid) }
			}
			
			foreach uid [array names idlewarns] {
				catch { unset idlewarns($uid) }
			}

			foreach uid $userInfo(userid) {
				myEcho DBUG "added uid $uid to idlecache: $svrInfo(mapstart)"
				set idlecache($uid) $svrInfo(mapstart)
			}
		}
	}

	# Set up our weapon-limits if so configured
	if $config(weaponlimit) {
		# Zero out anything left-over from last map
		set wlimit ""

		# Look for map-specific weapon.cfg
		set fname "$myhldir/$gameId/maps/$svrInfo(map)_weapon.cfg"
		if [catch { open $fname r } weaponfd] {
			myEcho DBUG "$weaponfd"
			myEcho DBUG "Default weapons will be used."
			set wlimit $config(weapondefault)
		} else {
			myEcho INFO "Limiting weapons as specified in $fname"
			while 1 {
				set weapon [string trim [gets $weaponfd]]
				if [eof $weaponfd] break
				if ![cequal $weapon ""] {
					lappend wlimit $weapon
				}
			}
		}

		myEcho DBUG "Weapon limits for $svrInfo(map) are '$wlimit'"

		# Schedule our broadcast message
		catch { after cancel $after(weaponmsg) }
		set after(weaponmsg) [after 60000 {
			# Only broadcast if ppl are connected and there is a limit on weapons
			if { [llength $userInfo(users)] && ![cequal $wlimit ""] } {
				regsub -all -- "%M" $config(weaponmsg) $svrInfo(map) myMsg
				say $myMsg
				say $wlimit
			}
		}]
	}


	if !$initializing {
		if !$config(quiet) {
			# Send messages to all customers after 3 minutes
			catch { after cancel $after(map) }
			set after(map) [after 180000 { 
				# Only broadcast msgs if ppl are conneted
				if [llength $userInfo(users)] {
					say [format $text(67) $svrInfo(map) $svrInfo(maptime)]
					say [format $text(68) [getNextMap $svrInfo(map)]]
					if ![cequal $config(serverMsg) ""] {
						if $config(fortune) {
							sayFortune
						} else {
							sayServerMsg
						}
					}
				}
			}]
		}

		# Process serverMsgTimes directive
		set i 0
		foreach time $config(serverMsgTimes) {
			# after uses milliseconds
			set time [expr $time * 1000]
			set after(svrtime$i) [after $time { sayServerMsg }] 
		}
	}

	# Reset any active voting, map has changed
	catch { after cancel $after(tally) }
	catch { after cancel $after(vote)  }
	set vote(insession) 0

	statusBar "<[replicate - 20] $svrInfo(map) [replicate - 20]>"

	# Schedule a new voting session
	set after(vote) [scheduleVoting] 

	# This is required for Cstrike, maybe others...won't hurt I suppose
	myEcho DBUG "Setting mp_logmessages 1..."
	execCmd "mp_logmessages 1"

	# Required for all mods
	execCmd "fullserverinfo"

	dumpVars 1
	myEcho DBUG [format $text(252) $stats(globalvars)]

	# Read halfbot_map.cfg
	if $config(halfbot) {
		readHalfBot $svrInfo(map)
	}

	set initializing 0
}


proc sayServerMsg {} {
	global config

	say \"$config(serverMsg)\"
}

proc sayFortune {} {
	global svrInfo config

	if [catch { exec $config(fortunepath) -s } fortune] {
		myEcho ERR "Error getting fortune: $fortune"
		sayServerMsg
	} else {
		set svrLen [clength $svrInfo(name)]
		set max 85 ;# Max # of chars that HL will display to client

		foreach line [string trim [split $fortune "\n"]] {
			if { [expr [clength $line] + $svrLen] > $max } {
				# bust the last three words off
				set words [split $line " "]
				set last [expr [llength $words] - 3]
				set line2 [join [lrange $words $last end]]
				incr last -1
				set line1 [join [lrange $words 0 $last]]
				say $line1
				say $line2
			} else {
				say $line
			}
		}
	}
}

proc getLog { id { update 1 } { model 0 } } {
	global fds text exprs

	while 1 {
		set rc [getServerData data]
		if ![cequal $rc 0] return

		set data [string trim $data]
		if { [cequal $data ""] && $rc } { return "" }
		doConsole data

		if $model {
			# We're looking for a model or team.  Bots don't return this info.
			if [regexp $exprs(bot) $data] { return "-" }
		}

		if [cequal [csubstr $data 0 [clength $id]] $id] { return $data }
		processLogLine data 1 $update
	}
}

proc execCmd {{cmd ""}} {
	global fds serverUp text config plat lastcmd

	if !$serverUp return

	if [cequal $cmd ""] return

	# Make sure no l33t h4x0rz snuck a semicolon in their name
	regsub -all ";" $cmd "," cmd

	if { $config(debug) > 1 } {
		myEcho DBUG "Executing '$cmd'"
	}

	set mycmd [string tolower $cmd]
	if {[cequal $mycmd quit] || [cequal $mycmd exit] || [cequal $mycmd stop]} {
		stopServer
	} else {
		if [cequal $plat unix] {
			puts $fds(cmdfd) $cmd
			catch { flush $fds(cmdfd) }
		} else {
			writeHlbpCommand 03 $cmd
		}
	}

	set lastcmd $cmd
}

proc stopServer { {running 1} {wait 29} } {
	global fds vote text svrInfo plat

	myEcho INFO $text(45)

	set svrInfo(starttime) 0
	set svrInfo(expired) 0

	if [cequal $plat unix] {
		catch {wait -nohang}
        if $running {
			catch {
				myEcho DBUG "STOPPING THE SERVER WITH 'quit' COMMAND"
				puts $fds(cmdfd) quit
				catch { flush $fds(cmdfd) }
			}
        }

        # Wait for it to die
        set i 0
        while { $i < $wait } {
                catch {wait -nohang}
                if [catch {kill 0 $fds(pidnum)} err] break
                incr i
                mySleep 1
        }

        if { $i >= $wait } {
                # Kill it
                catch {kill 2 $fds(pidnum)}
                mySleep 1
                catch {kill 9 $fds(pidnum)}
                catch {wait -nohang}
				statusBar [format $text(99) [now]]
        }

        serverDown

	} else {
		# Tell HLBP to stop the server
		writeHlbpCommand 01 213
	}

	statusBar [format $text(97) [now]]
}

proc now { { clock "" } } {
	set fmt "%a %b %d - %T"

	if [cequal $clock ""] {
		set time [clock format [clock seconds] -format $fmt]
	} else {
		set time [clock format $clock -format $fmt]
	}

	return $time
}

proc checkForHungServer {} {
	global fds config text plat

	if [cequal $plat windows] { return 0 }

	execCmd hostname

	mySleep 5

	set rc [getServerData data]
	myEcho DBUG "checkHung: rc='$rc', data='$data'"
	if [cequal $rc -1] { 
		set data ""
	} else {
		set data [string trim $data]
		doConsole data
	}

	# If some other log entry snuck in then process it...
	if ![cequal [csubstr $data 1 4] host] {
		processLogLine data 1
	}
	
	if [cequal $data ""] {
		# Ouch.  No data there!
		sendClient ERR [format $text(46) [now]]
		myEcho     ERR [format $text(46) [now]]

		stopServer 1 2
		if $config(rs)  {
			mySleep $config(rsDelay)
			launchServer
		}

		return 1
	}

	return 0
}

# Check to see if we need to reload anything
proc checkReload {} {
	global modified myhldir gameId text cfgPostfix

	set files [list cfg txt badwords exclude teammap halfbot] 

	if ![info exists modified(cfg)] {
		# First time here, initialize the array
		foreach type $files {
			if [cequal $type txt] {
				set fname halfd.$type
			} elseif [cequal $type cfg] {
				set fname $myhldir/$gameId/halfd.cfg$cfgPostfix
			} elseif [cequal $type halfbot] {
				set fname $myhldir/$gameId/halfbot.cfg
			} else {
				set fname $myhldir/$gameId/halfd.$type
			}

			if [catch {file mtime $fname} modified($type)] {
				set modified($type) 0
			}
		}
	} else {
		foreach type $files {
			if [cequal $type txt] {
				set fname halfd.$type
			} elseif [cequal $type cfg] {
				set fname $myhldir/$gameId/halfd.cfg$cfgPostfix
			} elseif [cequal $type halfbot] {
				set fname $myhldir/$gameId/halfbot.cfg
			} else {
				set fname $myhldir/$gameId/halfd.$type
			}

			if [catch {file mtime $fname} mtime] {
				set modified($type) 0
			} else {
				if { $mtime != $modified($type) } {
					myEcho INFO [format $text(48) $type]
					switch -exact -- $type {
						cfg      { readConfig   }
						txt      { readText     }
						badwords { readBadwords }
						exclude  { readExclude  }
						teammap  { readTeammap  }
						halfbot  { readHalfBot  }
					}
					set modified($type) $mtime
				}
			}
		}
	}
}

proc tcldebugOn {} {
	global fds text gameId cfgPostfix

	if ![info exists fds(tcldebug)] {
		# Set everything up if this is our first time here
		set fds(tcldebug) "$gameId/halfd.debug$cfgPostfix"
		myEcho INFO [format $text(134) $fds(tcldebug)]
		if [catch { open $fds(tcldebug) w } fds(tcldebugfd)]  {
			myEcho ERR [format $text(135) $fds(tcldebug)] 
			myEcho ERR $fds(tcldebugfd)
			set config(tcldebug) 0
		} else {
			puts $fds(tcldebugfd) [format $text(136) [now]]
			cmdtrace on $fds(tcldebugfd)
		}
	} else {
		catch { close $fds(tcldebugfd) }
		catch { file rename -force $fds(tcldebug) $fds(tcldebug).old }

		if [catch { open $fds(tcldebug) w } fds(tcldebugfd)]  {
			myEcho ERR [format $text(135) $fds(tcldebug)] 
			myEcho ERR $fds(tcldebugfd)
			set config(tcldebug) 0
		} else {
			puts $fds(tcldebugfd) [format $text(136) [now]]
			cmdtrace on $fds(tcldebugfd)
		}
	}
}

proc tcldebugOff {} {
	global fds text

	# Disable tracing
	cmdtrace off

	catch { puts  $fds(tcldebugfd) [format $text(137) [now]] }
	catch { close $fds(tcldebugfd) }
}
	

proc getServerStatus {{reschedule 1} {force 0}} {
	global fds svrInfo userInfo config serverUp notIdle exprs text
	global gameId myhldir vote after uidip lastrotate
	global nullMapCnt plat dontprocess enterwait kicked

	set enterwait 0

	if { $config(debug) > 2 } { myEcho DBUG "getServerStatus: [clock seconds]" }

	if $dontprocess {
		if $reschedule {
			set after(gs) [after [expr $config(refresh) * 1000] getServerStatus]
		}
		return
	}

	# Clear our 'kicked' array
	foreach id [array names kicked] { unset kicked($id) }

	checkReload

	# Check to see if we should rotate the log file
	if $config(autorotate) {
		set today [clock format [clock seconds] -format %a]
		if ![cequal $today $lastrotate] {
			cycleLogFile
			set lastrotate $today
		}
	}

	# If tcl-debugging is on, cycle the log file
	if $config(tcldebug) {
		# Cycle log file if we're at level 1
		if [cequal $config(tcldebug) 1] {
			tcldebugOff
			tcldebugOn
		}
	} else {
		tcldebugOff
	}


	if !$serverUp { return }

	# Check to see if 'modmax' is expired
	if { $config(modmax) && $svrInfo(starttime) && !$svrInfo(expired) } {
		set runtime [expr [clock seconds] - $svrInfo(starttime)]
		set maxsecs [expr $config(modmax) * 60]
		myEcho DBUG "Server has been running for $runtime seconds. Max=$maxsecs"
		if { $runtime > $maxsecs } {
			set msg [format $text(180) $gameId $config(modmax)]
			myEcho INFO $msg
			say $msg

			if $config(modswitch) {
				set msg [format $text(181) $config(modprimary)]
				myEcho INFO $msg
				say $msg
				set svrInfo(expired) 1
			} else {
				set msg [format $text(183) $config(modprimary)]
				myEcho INFO $msg
				say $msg
				endVoting $config(modprimary)
				return
			}
		}
	}

	catch { after cancel $after(gs) }
	catch { after cancel $after(pl) }

	set uidip(refresh) 0

	# Slurp any data we might've missed
	slurpLogFile

	if { !$force && !$notIdle } {
		if [checkForHungServer] {
			set nullMapCnt 0
			return
		}

		#statusBar [format $text(98) [now]]
		sendClient STATUS
		if $reschedule {
			set after(gs) [after [expr $config(refresh) * 1000] getServerStatus]
		}

		set dontprocess 0
		processLogFile 1
		return
	}

	set notIdle 0

	set oldSvrName ""
	if [info exists svrInfo(name)] {
		set oldSvrName $svrInfo(name)
	}

	set svrInfo(name) ""
	set svrInfo(ip) ""
	set svrInfo(map) ""
	set svrInfo(player) 0
	set svrInfo(max) 0

	slurpLogFile
	execCmd $config(statuscmd)

	if [cequal $plat unix] {
		mySleep 0.5
	} else {
		set dontprocess 1
		update
		mySleep 1
	}

	# Process output from 'status' command
	# Get hostname
	set data [getLog hostname]
	set svrInfo(name) [string trim [csubstr $data 10 end]]

	#if { ![cequal $svrInfo(name) ""] && !$serverUp } { serverUp }
	if ![cequal $svrInfo(name) $oldSvrName] { serverUp }

	set data [getLog tcp/ip 0]
	getDataSep $data tcp/ip : svrInfo(ip)

	set data [getLog map 0]
	getDataSep $data map : map

	set data [getLog players 0]
	getDataSep $data players : players

	if [info exists map] {
		set svrInfo(map) [lindex $map 0]
	} else {
		set map ""
	}

	if [info exists players] {
		set svrInfo(player) [lindex $players 0]
		regsub -all -- "\\(" [lindex $players 2] \
			"" svrInfo(max)
	}


	clearUserInfo

	if ![cequal $map ""] {

	# For each player, get their data
	set data [getLog "#"]
	set myPlayerCnt 0
	while 1 {
#      name id wonid adr frag time ping drop address
		set rc [getServerData data]
		if ![cequal $rc 0] break

		doConsole data
		if ![regexp "^#" $data] { break }

		lassign [parseStatusOutput $data] name userid wonid frags time ping ip

		incr myPlayerCnt

		# Bots have wonid=0 and don't have IP addressess...
		# ...except on windoze, their IP address is 00000000...
		if [cequal $ip ""] { set ip "127.0.0.1" }
		if [cequal $ip 00000000] { set ip "127.0.0.1" }

		lappend userInfo(users)  $name
		lappend userInfo(userid) $userid
		lappend userInfo(wonid)  $wonid
		lappend userInfo(frags)  $frags
		lappend userInfo(times)  $time
		lappend userInfo(pings)  $ping
		lappend userInfo(ips)    $ip
		lappend userInfo(team)   [teamLookup [getPlayerTeam $userid]]
	}

	# Process the line that we broke out of the loop on...
	if ![cequal $data ""] {
		processLogLine data 1
	}

	# How annoying.  'status' doesn't show players that are connecting.
	# parse 'user' as well.  Sigh.
	if ![cequal $myPlayerCnt $svrInfo(player)] {
		slurpLogFile
		execCmd users

		mySleep 0.5

		getLog userid
		getServerData foo

		loop i 0 $svrInfo(player) {
			set rc [getServerData data]
			doConsole data
			set data [string trim $data]

			if [regexp $exprs(endusers) $data] {
				# Reached end of user list.  Probably have bots - bail. 
				break
			}

			if [regexp $exprs(users) $data foo myUid myWonid myName] {

				# USERS: Account for users with " : " in their names (sigh)
				set l [split $data :]
				if { [llength $l] > 3 } {
					set myUid   [string trim [lvarpop l]]
					set myWonid [string trim [lvarpop l]]
					set myName  [string trim [join $l :]]
				}

				# Only append if they already haven't been...
				if [cequal [lsearch -exact $userInfo(userid) $myUid] -1] {
					# This user is connecting
					lappend userInfo(users)  $myName
					lappend userInfo(userid) $myUid
					lappend userInfo(wonid)  $myWonid
					lappend userInfo(frags)  0
					lappend userInfo(times)  00:00
					lappend userInfo(pings)  0
					lappend userInfo(ips)    CONNECTING
					lappend userInfo(team)   "-"
				}
			} else {
				set dontprocess 0
				processLogLine data 1
			}
		}

		# Pop off the "N users" line
		set rc [getServerData data]
		doConsole data
		processLogLine data 1 0
	}

	getPlayerModels
	}

	updateTeams 0

	if $reschedule {
		set after(gs) [after [expr $config(refresh) * 1000] getServerStatus]
	}

	# Sometimes we get here without a map name, usually on a map change...
	if ![cequal $svrInfo(map) ""] {
		sendClient UPDATE [buildClientUpdate]
		set nullMapCnt 0
	} else {
myEcho DBUG "MAP IS NULL (count=$nullMapCnt / $config(nullmap))"
		mySleep 5

		# Only check for hung servers on unix
		if [cequal $plat unix] {
			if ![info exists nullMapCnt] { set nullMapCnt 0 }
			incr nullMapCnt
			if { $nullMapCnt > $config(nullmap) } {
				if [checkForHungServer] {
					set nullMapCnt 0
					return
				}
			}
		}

		catch { after cancel $after(gs) }
		set after(gs) [after 2000 getServerStatus 1 1]
	}

	if $config(pingcheck) checkPings
	if { $config(idle) || $config(leech) } checkIdle

	set dontprocess 0
	processLogFile 1
}

proc parseStatusOutput { data } {
	global config exprs dbgnewstatus

	if [regexp -- $exprs(status) $data foo name details] {
		# Goody.  New status-output format being used!
		# Two bloody lines of code!
		lassign $details userid wonid frags time ping foo ip

		if $config(debug) {
			if { ![info exists dbgnewstatus] && ![cequal $wonid 0] } {
				myEcho DBUG [replicate - 40]
				myEcho DBUG "NEW STATUS FORMAT - PLAYER NAMES HAVE QUOTES"
				foreach val "name userid wonid frags time ping foo ip" {
					myEcho DBUG "$val = [set $val]"
				}
				myEcho DBUG [replicate - 40]
				set dbgnewstatus 1
			}
		}
	} else {
		# Yuck.  Do it the old way.  Many lines of code :-(

		set data [string trim [csubstr $data 3 end]]

		set playerList ""
		set list [split $data " "]
		foreach el $list {
			if ![cequal $el ""] { lappend playerList $el }
		}

		# playerList should be 10 entries UNLESS	
		#	- they have spaces in their name
		set len [llength $playerList]
		set idx 1
		set myLen [expr $config(statusfields) - 2]

		if { $config(debug) > 2 } {
			myEcho DBUG "### data  = $data"
			myEcho DBUG "### len   = $len"
			myEcho DBUG "### mYlen = $myLen"
		}

		if { $len > $myLen } {
			# skip idx ahead to userid
			set idx [expr $len - [expr $myLen - 1]]
			incr idx -1

			# Get the name
			set name [join [lrange $playerList 0 $idx] " "]

			incr idx
		} else {
			set name [lindex $playerList 0]
		}

		set userid [lindex $playerList $idx]
		set wonid  [lindex $playerList [incr idx]]
		# TODO: what the hell is 'adr'?
		set frags  [lindex $playerList [incr idx]]
		set time   [lindex $playerList [incr idx]]
		set ping   [lindex $playerList [incr idx]]

		# skip 'drop'
		incr idx

		# Get IP and remove port #
		set ip [lindex $playerList [incr idx]]
	}

	set ip [lindex [split $ip :] 0]

	if [ctype digit $ping] {
		if { $ping > 9999 } { set ping 9999 }
	} else {
		myEcho WARN "Found non-numeric ping: $ping"
		myEcho WARN "Data: '$data'"
		set ping 0
	}

	if { $config(debug) > 3 } {
		foreach x "name userid wonid ip frags time ping" {
			myEcho DBUG "$x: [set $x]"
		}
		myEcho DBUG "--------------------------------"
	}

	if !$config(ipfromstats) {
		set ip ""
		# Figure out their IP from the array we built from 'connected' msgs
		if [catch { set ip $uidip($userid) } err] { 
			global debugip
			if ![info exists debugip($userid)] {
				myEcho DBUG "$name: ip unknown:"
				myEcho DBUG "ip determine err is $err"
				set debugip($userid) 1
			}
			if ![cequal $name ""] { set ip unknown }
		}
	}

	return [list $name $userid $wonid $frags $time $ping $ip]
}

proc playerConnected { pinfo ip } {
	global userInfo config svrInfo idlecache

	# Force parsePlayerInfo to use new-logging, since connect message
	# always uses new-logging.
	lassign [parsePlayerInfo $pinfo 1] name uid wonid team

	lappend userInfo(users)  $name
	lappend userInfo(userid) $uid
	lappend userInfo(wonid)  $wonid
	lappend userInfo(frags)  0
	lappend userInfo(times)  00:00
	lappend userInfo(pings)  0
	lappend userInfo(ips)    CONNECTING
	lappend userInfo(model)  "-"
	lappend userInfo(team)   "-"

	incr svrInfo(player)

	myEcho DBUG "playerConnected: $name $uid $wonid $ip"

	if $config(leech) { 
		set idlecache($uid) [clock seconds]
		myEcho DBUG "added uid $uid to idlecache: $svrInfo(mapstart)"
	}

	sendClient UPDATE [buildClientUpdate]
}

proc playerDropped { name } {
	global userInfo svrInfo idlecache

	lassign [getUserInfo name $name] name uid wonid ip

	if ![cequal $name -1] {
		# Find the player in our array
		set idx [lsearch -exact $userInfo(userid) $uid]

		# Remove this guy from userInfo
		if ![cequal $idx -1] {
			foreach arr "users userid wonid frags times pings ips model team" {
				catch {
					set userInfo($arr) [lreplace $userInfo($arr) $idx $idx]
				}
			}
			sendClient UPDATE [buildClientUpdate]
		}

		catch { unset idlecache($uid) }
	}

	incr svrInfo(player) -1

	myEcho DBUG "playerDropped: $name $uid $wonid"
}

proc checkPings {} {
	global config last userInfo text

	set now [clock seconds]

	# Make sure it's been at least 30 seconds since we last checked
	if { [expr $last(ping) + 30] >= $now } {
		myEcho DBUG "Less than 30 seconds since last ping check, skipping..."
		return
	}
	set last(ping) [clock seconds]

	# Check the pings
	set i 0
	set uids  ""
	set pings ""
	set types ""
	set pingCnt 0
	foreach ping $userInfo(pings) {
		if { [cequal $ping 0] || [cequal $ping 9999] } {
			# Skip check on 0 and 9999
		} else {
			incr pingCnt
			if { $ping > $config(pingmax) && $config(pingmax) != 0 } {
				lappend uids  [lindex $userInfo(userid) $i]
				lappend pings $ping
				lappend types max
			} elseif { $ping < $config(pingmin) && $config(pingmin) != 0 } {
				lappend uids  [lindex $userInfo(userid) $i]
				lappend pings $ping
				lappend types min
			}
		}
		incr i
	}

	if { [cequal [llength $pings] $pingCnt] } {
		# All players are violating threshold, must be some sort of lag spike.
		# Skip it.
		if ![lempty $pings] {
			myEcho WARN $text(204)
			myEcho DBUG "uids  = '$uids'"
			myEcho WARN "pings = '$pings'"
			myEcho DBUG "types = '$types'"
		}
	} else {
		# Process every ping we found in violation
		set i 0
		foreach uid $uids {
			pingViolation $uid [lindex $pings $i] [lindex $types $i]
			incr i
		}
	}
}

proc checkIdle {} {
	global idlecache svrInfo config userInfo

	if [lempty [array names idlecache]] return

	set now [clock seconds]

	foreach uid [array names idlecache] {
		# They may have dropped since being cached...
		if [cequal [lsearch -exact $userInfo(userid) $uid] -1] {
			catch { unset idlecache($uid) }
			continue
		}

		set team [getPlayerTeam $uid]
		switch -exact -- $team {
			- {
				if $config(leech) {
					if [expr [expr $idlecache($uid) + $config(leech)] < $now] {
						myEcho DBUG "LEECHING details: '[getUserInfo userid $uid]' team=$team, map=$svrInfo(map)"
						penalizeIdle $uid $config(leechmsg) leech
					} else {
						myEcho DBUG "Too early to check for leech on $uid"
					}
				}
			}

			IDLE - 
			SPECTATOR {
				if $config(idle) {
					if [expr [expr $idlecache($uid) + $config(idle)] < $now] {
						myEcho DBUG "IDLE details: '[getUserInfo userid $uid]' - team=$team"
						penalizeIdle $uid $config(idlemsg) idle
					} else {
						myEcho DBUG "Too early to check for idle on $uid"
					}
				}
			}

			default {	
				myEcho DBUG "uid $uid is not idle or leeching (team = '$team')"
				catch { unset idlecache($uid) }
			}
		}
	}
}

proc penalizeIdle { userid reason type } {
	global config idlecache idlewarns svrInfo

	set uinfo [getUserInfo userid $userid]
	if [cequal $uinfo -1] {
		myEcho DBUG "Couldn't determine user for '$reason' - abort."
		return
	}
	lassign $uinfo name foo wonid ip

	regsub -all "\%P" $reason $name myreason
	regsub -all "\%IP" $myreason $svrInfo(ip) myreason

	if [info exists idlewarns($userid)] {
		incr idlewarns($userid)
	} else {
		set idlewarns($userid) 1
	}

	myEcho DBUG "$name<$userid> warned with '$myreason' (warns=$idlewarns($userid))"
	if { [info exists idlecache($userid)] && $config(debug) > 1 } {
		myEcho DBUG "     Cached: $idlecache($userid)"
		myEcho DBUG "     Now   : [clock seconds]"
		myEcho DBUG "     Diff  : [expr [clock seconds] - $idlecache($userid)]"
		myEcho DBUG "     Warns : $idlewarns($userid)"
	}

	if $config(admininstalled) {
		adminPsay $myreason $userid
	} else {
		# FIXME
		# broadcast (?)
		#say $myreason
	}

	if { [cequal $type idle] && $config(idleaction) } {
		if { $idlewarns($userid) >= $config(idleaction) } {
			penalize kick "" $wonid $userid $name $myreason
		}
	} elseif { [cequal $type leech] && $config(leechaction) } {
		if { $idlewarns($userid) >= $config(leechaction) } {
			penalize kick "" $wonid $userid $name $myreason
		}
	}
}

################################################################################
#
# Table operations.
#
# Each penalty type requires it's own table op proc; this is to accomodate the
# way Tcl's 'after' function works.  We use this for 'decay' and 'sched'.
#
# They call the generic tableOp procedure for all other functions (read and write).
#
proc tableOp { op type indexArray } {
	global text config after gameId myhldir cfgPostfix
	global $indexArray

	set fname $myhldir/$gameId/halfd.$type$cfgPostfix

	switch -exact $op {
		write {
			if [catch {open $fname w} fd] return 

			foreach id [array names $indexArray] { 
				set value [set ${indexArray}($id)]
				puts $fd "$id $value"
			}

			close $fd
		}

		read {
			if [catch {open $fname r} fd] { myEcho DBUG $fd ; return }

			while 1 {
				set data [string trim [gets $fd]]
				if [eof $fd] break

				lassign $data id cnt
				set ${indexArray}($id) $cnt
myEcho DBUG "assigned ${indexArray}($id) to $cnt"
			}

			close $fd
		}
	}
}

proc decay { indexArray {id 0} {all 0}} {
	global $indexArray config

	set i 0
	if ![cequal $id 0] {
		set ids $id
	} else {
		set ids [array names $indexArray]
	}

	if { $config(debug) > 1 } {
		myEcho DBUG "Decaying $indexArray...[llength $ids] entries"
	}


	foreach id $ids {
		if ![info exists ${indexArray}($id)] continue

		if $all {
			set ${indexArray}($id) 0
			set val 0
			myEcho DBUG "decayed $indexArray for $id to ZERO"
		} else {
			if [catch { incr ${indexArray}($id) -1 } val] {
				myEcho WARN "Internal decay error: $val"
				myEcho WARN "var = '${indexArray}($id)' id = '$id'"
				set val 0
			}

			if ![cequal $indexArray teamcount] {
				myEcho DBUG "decayed $indexArray for $id (now $val)"
			}
		}

		if { $val < 1 } { unset ${indexArray}($id) }
		incr i
	}

	return $i
}

proc tkTable { op {id 0} {decayall 0}} {
	global tkcount config myhldir gameId after cfgPostfix

	switch -exact $op  {
		sched {
			# Only schedule if we have a decay configured
			if [cequal $config(tkdecay) 0] return
			catch { after cancel $after(tk) }
			if [catch {
					after [expr $config(tkdecay) * 1000] { tkTable decay }
				} rc] {
					myEcho ERR [format $text(119) $rc]
			} else {
				set after(tk) $rc
			}
		}

		decay {
			if [decay tkcount $id $decayall] { 
				tkTable write
				catch { after cancel $after(tk) }
				tkTable sched
			} else {
				set after(tk) 0
			}
		}

		default {
			if { [cequal $op write] && [cequal $after(tk) 0] } {
				tkTable sched
			}
			tableOp $op tk tkcount
		}
	}

	return
}

proc attackTable { op {id 0} {decayall 0}} {
	global attackcount config myhldir gameId after cfgPostfix

	switch -exact $op  {
		sched {
			# Only schedule if we have a decay configured
			if [cequal $config(attackdecay) 0] return
			catch { after cancel $after(attack) }
			if [catch {
					after [expr $config(attackdecay) * 1000] { attackTable decay }
				} rc] {
					myEcho ERR [format $text(119) $rc]
			} else {
				set after(attack) $rc
			}
		}

		decay {
			if [decay attackcount $id $decayall] { 
				attackTable write
				catch { after cancel $after(attack) }
				attackTable sched
			} else {
				set after(attack) 0
			}
		}

		default {
			if { [cequal $op write] && [cequal $after(attack) 0] } {
				attackTable sched
			}
			tableOp $op attack attackcount
		}
	}

	return
}

proc foulTable { op {id 0} {decayall 0}} {
	global foulcount config myhldir gameId after

	switch -exact $op  {
		sched {
			# Only schedule if we have a decay configured
			if [cequal $config(fouldecay) 0] return
			catch { after cancel $after(foul) }
			if [catch {
					after [expr $config(fouldecay) * 1000] foulTable decay
				} rc] {
					myEcho ERR [format $text(119) $rc]
			} else {
				set after(foul) $rc
			}
		}

		decay {
			if [decay foulcount $id $decayall] { 
				foulTable write
				catch { after cancel $after(foul) }
				foulTable sched
			} else {
				set after(foul) 0
			}
		}

		default {
			if { [cequal $op write] && [cequal $after(foul) 0] } {
				foulTable sched
			}
			tableOp $op foul foulcount
		}
	}
}

proc weaponTable { op {id 0} {decayall 0}} {
	global weaponcount text config after

	if { $config(debug) > 1 } {myEcho DBUG "weaponTable $op" }

	switch -exact $op  {
		sched {
			# Only schedule if we have a decay configured
			if [cequal $config(weapondecay) 0] return
			catch { after cancel $after(weapon) }
			if [catch {
					after [expr $config(weapondecay) * 1000] weaponTable decay
				} rc] {
					myEcho ERR [format $text(119) $rc]
			} else {
				set after(weapon) $rc
			}
		}

		decay {
			if [decay weaponcount $id $decayall] { 
				weaponTable write
				catch { after cancel $after(weapon) }
				weaponTable sched
			} else {
				set after(weapon) 0
			}
		}

		default {
			if { [cequal $op write] && [cequal $after(weapon) 0] } {
				weaponTable sched
			}
			tableOp $op weapon weaponcount
		}
	}
}

proc suicideTable { op {id 0} {decayall 0}} {
	global suicidecount text config after

	switch -exact $op  {
		sched {
			# Only schedule if we have a decay configured
			if [cequal $config(suicidedecay) 0] return
			catch { after cancel $after(suicide) }
			if [catch {
					after [expr $config(suicidedecay) * 1000] suicideTable decay
				} rc] {
					myEcho ERR [format $text(119) $rc]
			} else {
				set after(suicide) $rc
			}
		}

		decay {
			if [decay suicidecount $id $decayall] { 
				suicideTable write
				catch { after cancel $after(suicide) }
				suicideTable sched
			} else {
				set after(suicide) 0
			}
		}

		default {
			if { [cequal $op write] && [cequal $after(suicide) 0] } {
				suicideTable sched
			}
			tableOp $op suicide suicidecount
		}
	}
}

proc pingTable { op {id 0} {decayall 0}} {
	global pingcount text config after

	switch -exact $op  {
		sched {
			# Only schedule if we have a decay configured
			if [cequal $config(pingdecay) 0] return
			catch { after cancel $after(ping) }
			if [catch {
					after [expr $config(pingdecay) * 1000] pingTable decay
				} rc] {
					myEcho ERR [format $text(119) $rc]
			} else {
				set after(ping) $rc
			}
		}

		decay {
			if [decay pingcount $id $decayall] { 
				pingTable write
				catch { after cancel $after(ping) }
				pingTable sched
			} else {
				set after(ping) 0
			}
		}

		default {
			if { [cequal $op write] && [cequal $after(ping) 0] } {
				pingTable sched
			}
			tableOp $op ping pingcount
		}
	}
}

proc teamTable { op {id 0} {decayall 0}} {
	global teamcount text config after

	switch -exact $op  {
		sched {
			# Only schedule if we have a decay configured
			if [cequal $config(teamdecay) 0] return
			catch { after cancel $after(team) }
			if [catch {
					after [expr $config(teamdecay) * 1000] teamTable decay
				} rc] {
					myEcho ERR [format $text(119) $rc]
			} else {
				set after(team) $rc
			}
		}

		decay {
			set cnt [decay teamcount $id $decayall]
			if $cnt {
				teamTable write
				catch { after cancel $after(team) }
				teamTable sched
			} else {
				set after(team) 0
			}
		}

		default {
			if { [cequal $op write] && [cequal $after(team) 0] } {
				teamTable sched
			}
			# Don't do read/write for teams - this event happens way too often
			# tableOp $op team teamcount
		}
	}
}
#
# End table operations
#
################################################################################


proc pingViolation { userid ping type } {
	global config pingcount text exclude enterwait

	if $enterwait { getServerStatus 1 1 }

	set uinfo [getUserInfo userid $userid]
	if [cequal $uinfo -1] return
	lassign $uinfo name foo wonid ip

	if $config(exclude) {
		if ![catch { set exclude($wonid) } err] {
			# They're excluded
			myEcho INFO [format $text(195) $type "$name:$wonid"]
			return
		}
	}

	if [cequal $wonid 0] {
		if [cequal $ip 127.0.0.1] {
		if { $config(debug) > 1 } {
			myEcho DBUG "Found a ping violation on a BOT: $name<$wonid><$userid>.  Ping = $ping"
			myEcho DBUG "Skipping the BOT."
		}
		return
		}
	}

	if $config(debug) {
		if $config(banip) { set wonid $ip }
		set myInfo "$name<$wonid><$userid>"
		myEcho DBUG "Found a '$type' ping violation on $myInfo.  Ping = $ping"
	}

	# Warn 'em - pings need to be warned about min/max limits
	if [cequal $type max] {
		regsub -all "\%P" $config(pingmaxmsg) $name mymsg
		regsub -all "\%G" $mymsg $config(pingmax) mymsg
	} else {
		regsub -all "\%P" $config(pingminmsg) $name mymsg
		regsub -all "\%G" $mymsg $config(pingmin) mymsg
	}
	myEcho DBUG $mymsg
	say $mymsg

	processPenalty ping $userid pingTable pingcount
}

proc updateTeams {{update 1}} {
	global userInfo config

	set userInfo(team) ""
	foreach id $userInfo(userid) {
		if $config(getteam) {
			lappend userInfo(team) [teamLookup [getPlayerTeam $id]]
		} else {
			lappend userInfo(team) "-"
		}
	}

	if { $update && $config(getteam) } { 
		sendClient UPDATE [buildClientUpdate]
	}
}

proc getPlayerTeam { userid } {
	global team

	if [info exists team($userid)] {
		set myTeam $team($userid)
	} else {
		set myTeam "-"
	}

	return $myTeam
}

proc getPlayerModels {} {
	global userInfo svrInfo config exprs plat role

	# If they don't want us looking for models, just initialize to "-"
	loop i 0 $svrInfo(player) { lappend userInfo(model) "-" }

	if !$config(getmodels) { return }

	if $config(getmodelsfromrole) {
		set userInfo(model) ""
		foreach uid $userInfo(userid) {
			if [info exists role($uid)] {
				lappend userInfo(model) $role($uid)
			} else {
				lappend userInfo(model) -
			}
		}

		return
	}

	# Get the model if possible.  We have to run the User command for each user.
	slurpLogFile
	set myPlayerCnt 0
	loop i 0 $svrInfo(player) {
		set myUserid [lindex $userInfo(userid) $i]
		set myAddr   [lindex $userInfo(ips) $i]
		set myWonid  [lindex $userInfo(wonid) $i]

		# HLTV seems to hose things
		if [cequal $myWonid $config(hltvwonid)] continue

		# Bots don't report any user information
		if [cequal $myWonid 0] continue

		# We only want models for players that are connected :)
		if [cequal $myAddr CONNECTING] continue

		execCmd "user $myUserid"

		set idx($myPlayerCnt) $i
		incr myPlayerCnt
	}

	mySleep 1

	set i 0
	set j 0
	while { $i < $myPlayerCnt } {
		# Just in case...
		if { $j > 2 } break

		# Valve team halflife will return a team tag from the user command.
		# TFC, and CS do not, so just use the model tag.
		# I don't know if model and team are always the same...
		set data [getLog model 0 1]
		if [cequal $data ""] {
			# If we got no data then the server is still spewing output,
			# so wait for it to finish...
			myEcho DBUG "Waiting for server to finish output on getmodel, i = $i, j = $j"
			mySleep 1
			incr j
			continue
		} else {
			set j 0
		}

		if [regexp $exprs(parsemodel) $data foo myModel] {
			set myModel [string trim $myModel]
		} else {
			set myModel "-"
		}

		# If we should ever use the team tag set the "if 0" to "if 1"
		if 0 {
			set data [getLog team 0 1]
			if [cequal $data ""] {
				# If we got no data then the server is still spewing output,
				# so wait for it to finish...
				myEcho DBUG "Waiting for server to finish output on getmodel, i = $i, j = $j"
				mySleep 1
				incr j
				continue
			} else {
				set j 0
			}

			if [regexp $exprs(parseteam) $data foo myTeam] {
				set myModel [string trim $myTeam]
			} else {
				set myModel "-"
			}
		}

		# Replace the "-" for this player with the real model
		catch {
		set userInfo(model) \
			[lreplace $userInfo(model) $idx($i) $idx($i) $myModel]
		}

if 1 {
		if [isGame frontline] {
			# Set the team for this player based on the model
			set first4 [csubstr $myModel 0 4] 
			if [cequal $first4 nato] {
				set myTeam Rebel
			} elseif [cequal $first4 axis] {
				set myTeam Cmndo
			} else {
				set myTeam ""
			}
			set uid [lindex $userInfo(userid) $idx($i)]
			assignTeam $uid $myTeam
		}

		if [isGame dod] {
			# Set the team for this player based on the model
			set first4 [csubstr $myModel 0 4] 
			if [cequal $first4 alli] {
				set myTeam Allies
			} elseif [cequal $first4 axis] {
				set myTeam Axis
			} else {
				set myTeam -
			}
			set uid [lindex $userInfo(userid) $idx($i)]
			assignTeam $uid $myTeam
		}

		if [isGame gearbox] {
			# Set the team for this player based on the model
			set uid [lindex $userInfo(userid) $idx($i)]
			assignTeam $uid $myModel
		}

		if [isGame globalwarfare] {
			# Set the team for this player based on the model
			set first4 [csubstr $myModel 2 4] 
			if [cequal $first4 army] {
				set myTeam U.N.
			} elseif [cequal $first4 rebe] {
				set myTeam ALF
			} else {
				set myTeam -
			}
			set uid [lindex $userInfo(userid) $idx($i)]
			assignTeam $uid $myTeam
		}
}

		incr i
		set j 0
	}

	# Clean out extra lines so they don't clutter tool output.
	# Last tag in valve is team.
	# This will get EOF in TFC, no problem :-)
	getLog team 0
}

proc clearUserInfo {} {
	global userInfo

	set userInfo(users)  ""
	set userInfo(pings)  ""
	set userInfo(frags)  ""
	set userInfo(times)  ""
	set userInfo(ips)    ""
	set userInfo(wonid)  ""
	set userInfo(userid) ""
	set userInfo(model)  ""
	set userInfo(team)   ""
}

proc getUserInfo { intype inval } {
	global userInfo

	set idx -1

	# Should fall out of this switch with idx set
	switch -exact -- $intype {
		name {
			set name $inval
			set idx 0
			foreach myname $userInfo(users) {
				if [cequal [string trim $myname] [string trim $name]] break
				incr idx
			}
			if [cequal $idx [llength $userInfo(users)]] { set idx -1 }

			if [cequal $idx -1] {
				# Do some funky things to really find the name
				set myname $name
				regsub -all ""  $name "\\" name
				regsub -all " " $name " \*" name
				if [catch {lsearch -regexp $userInfo(users) $name} idx] {
					myEcho DBUG "Error searching users: '$userInfo(users)'"
					myEcho DBUG "Name search was: '$name'"
					set idx -1
				}

				if ![cequal $idx -1] {
					myEcho DBUG "Found index for '$myname'/'$name' using regexp"
					myEcho DBUG "Names  : '$userInfo(users)'"
					myEcho DBUG "USERids: '$userInfo(userid)'"
				} else {
					myEcho DBUG "Unable to get index for '$myname'/'$name'"
					myEcho DBUG "Names  : '$userInfo(users)'"
					myEcho DBUG "USERids: '$userInfo(userid)'"
					return -1
				}
			}
		}

		userid {
			set idx [lsearch -exact $userInfo(userid) $inval]
		}

		wonid {
			set idx [lsearch -exact $userInfo(wonid) $inval]
		}

		ip {
			set idx [lsearch -exact $userInfo(ips) $inval]
		}

		default {
			myEcho ERR "Unsupported type in getUserInfo: $intype"
			return -1
		}
	}

	if [cequal $idx -1] {
		myEcho DBUG "Unable to get index for '$intype'/'$inval'"
		myEcho DBUG "$intype  : '$userInfo($intype)'"
		return -1
	}

	set outval ""
	if [catch {
		lappend outval [lindex $userInfo(users) $idx]
		lappend outval [lindex $userInfo(userid) $idx]
		lappend outval [lindex $userInfo(wonid) $idx]
		lappend outval [lindex $userInfo(ips) $idx]
		} ] {
			myEcho DBUG "Error getting user info based on $intype: $outval"
			myEcho DBUG "Input value was $inval"
			myEcho DBUG "Input : $userInfo($intype)"
			return -1
		}

	# This function returns a 4-element list:
	# 0 = name
	# 1 = userid
	# 2 = wonid
	# 3 = ip

	return $outval
}

proc buildClientUpdate {} {
	global svrInfo userInfo config vote text

	set kl ""

	if { $config(getmodels) && $config(getmodelsfromrole) } { getPlayerModels }

	set ids "users pings frags times ips userid wonid model team"

	foreach id $ids {
		keylset kl $id $userInfo($id)
	}

	# Send deaths if so specified
	if $config(senddeaths) {
		keylset kl deaths [getDeaths]
	}

	keylset kl map  $svrInfo(map)
	keylset kl cnt  $svrInfo(player)

	if $vote(insession) {
		keylset kl vote 2
	} else {
		keylset kl vote $config(vote)
	}

	return $kl
}

proc incrDeaths { uid } {
	global deaths stats

	if ![info exists deaths($uid)] {
		set deaths($uid) 1
	} else {
		incr deaths($uid)
	}

	incr stats(deaths)
}

proc getDeaths {} {
	global userInfo config deaths

	set deathList ""
	foreach id $userInfo(userid) {
		if [info exists deaths($id)] {
			lappend deathList $deaths($id)
		} else {
			lappend deathList 0
		}
	}

	return $deathList
}

proc getDataSep { data key sep var } {
	upvar $var myvar

	set foo [split $data $sep]
	set theirkey [string trim [lindex $foo 0]]
	set theirdat [string trim [lindex $foo 1]]

	if [cequal $theirkey $key] { 
		set myvar $theirdat
	}

}
	
proc getMaps {} {
	global myhldir gameId config mapList mapMtime text
	set builtins $config(maps)

	if ![info exists mapMtime] {
		set mapMtime 0
	}


	set mapdir $myhldir/$gameId/maps

	# If no map-dir, then just return builtins
	if ![file exists $mapdir] {
		return $builtins
	}

	set mtime [file mtime $mapdir]

	# If mapdir has changed, re-read into cache
	if ![cequal $mtime $mapMtime] {
		if [catch {glob $mapdir/*.{bsp,BSP}} mapList] { set mapList "" }

		foreach map $mapList {
			set map [crange $map 0 end-4]; # Strip off '.bsp'
			lappend builtins [file tail $map]
		}

		lappend builtins restart

		# Update cache
		set mapList [lsort -decreasing -ascii [lrmdups $builtins]]
		set mapMtime $mtime
	}

	return $mapList
}

proc startupMessages { vnum gameId } {
	global config fds text tcl_version myhldir tcl_platform env tcl_patchLevel

	if ![cequal $text(0) $vnum] {
		myEcho ERR $text(1)
		myEcho ERR [format "halfd = '%s', halfd.txt = '%s'" $vnum $text(0)]
	}

	myEcho INFO [format $text(2) $vnum [pid]]
	myEcho INFO $text(3)
	myEcho INFO $text(4)
	myEcho INFO $text(5)

	if ![info exists tcl_patchLevel] {
		set tcl_patchLevel ??
	}

	myEcho INFO [format $text(255) $tcl_version $tcl_patchLevel]
	myEcho INFO [format $text(153) $myhldir]

	myEcho INFO $text(209)
	foreach key [array names tcl_platform] {
		myEcho INFO [format "% 14s = '%s'"  $key $tcl_platform($key)]
	}

	myEcho DBUG "PATH = '$env(PATH)'"

	myEcho INFO $text(108)
	myEcho INFO $text(141)
	myEcho INFO $text(168)
	myEcho INFO [format $text(6) $gameId $myhldir/$gameId]

	myEcho INFO [replicate - 45]
	myEcho INFO $text(113)
	foreach key [lsort [array names config]] {
		set cfg $config($key)
		if [cequal $key usrauth]  { set cfg "***PROTECTED***" }
		if [cequal $key admauth]  { set cfg "***PROTECTED***" }
		if [cequal $key hlbpuser] { set cfg "***PROTECTED***" }
		if [cequal $key hlbppass] { set cfg "***PROTECTED***" }
		myEcho INFO [format "% 14s = '%s'" $key $cfg]
	}
	validateConfig
	myEcho INFO [replicate - 45]

	if [cequal $config(admauth) ""] {
		myEcho INFO $text(111)
		myEcho INFO $text(112)
	} else {
		if $config(localmode) {
			myEcho INFO $text(7)
		} elseif $config(localadmin) {
			myEcho INFO $text(8)
		} elseif ![cequal $config(admauth) ""] {
			myEcho INFO $text(9)
		}
	}
}

proc usage {} {
	global vnum defaultgame argv0

	set hinfo "halfd version $vnum, installed as $argv0"

	if [cequal [csubstr $argv0 0 1] .] {
		append hinfo " (in [pwd])"
	}

	puts stdout "
$hinfo

Usage: halfd \[-g <game>\] \[-d <hldir>\] \[-e <ext>\] 
             \[-debug <n>\] \[-h\] \[-v\] \[-V\] \[-cmddebug\]

Where: -g <game>   = a MOD name.  (e.g. tfc or cstrike)
                     Default = \"$defaultgame\"

       -d <hldir>  = The directory where hlds_run is installed.
                     This overrides the environment variable HLDIR.

       -e <ext>    = a string specifying which halfd.cfg to read.
                     This allows you to run multiple copies of the same game 
                     from one \$HLDIR/mod directory.  See FAQ for more info.

       -debug <n>  = Overrides \"debug\" option in halfd.cfg

       -h          = Print this usage information and exit.

       -v          = Print version information and exit.

       -V          = Print extended version information and exit.

       -cmddebug   = Command-line debugging (prints debug info and exits)


Examples:

halfd -g tfc
	Start halfd managing a TFC server in \$HLDIR/tfc.  
	Will read \"halfd.cfg\"

halfd -g tfc -e 27016 -d /games/hlds_l 
	Start halfd managing TFC server in /games/hlds_l/tfc.  
	Will read \"halfd.cfg.27016\"
"
	myExit 1
}

proc readText {} {
	global text myhldir vnum config

	set file $myhldir/halfd.txt

	if [catch {open $file r} fd] {
		puts stdout "FATAL: can't open 'halfd.txt': $fd"
		myExit 1
	}

	# Initialize the array; just in case someone doesn't copy the
	# file to the right place
	loop i 0 1000 { set text($i) "" }

	set i 0
	while 1 {
		if [eof $fd] break
		set text($i) [gets $fd]

		if [info exists config(debug)] {
			if { $config(debug) > 2 } {
				set j [format %03d $i]
				myEcho DBUG "text($j) = $text($i)"
			}
		}

		incr i
	}

	close $fd
}

proc readTeammap {} {
	global text myhldir vnum fds teammap

	if [catch {open $fds(teammap) r} fd] {
		myEcho INFO "Couldn't open teammap file: '$fd'"
		return
	}

	myEcho DBUG "Successfully opened teammap file."
	while 1 {
		if [eof $fd] break
		set data [string trim [gets $fd]]
		if [cequal $data ""] continue

		set list [split $data "="]
		if { [llength $list] != 2 } continue

		lassign $list left right
		set left [string tolower $left]

		set teammap($left) $right
		myEcho DBUG "Will display team '$left' as '$right'"
	}

	close $fd
}

proc daemonize {} {
	if [catch {fork} pid] {
		puts stdout [format $text(84) $pid]
		exit 1
	}

	if $pid {
		# Parent
		echo [format $text(85) $pid]
		exit 0
	}

	return
}

proc doConsole { datavar } {
	global config
	upvar $datavar data

	if $config(console) {
		if ![cequal [string trim $data] ""] { puts stdout $data }
	}
}

#######################################################################
# MAIN
#
proc main { argc argv } {
   global lbxs vnum config gameId myhldir fds serverUp text env cfgPostfix
   global plat penaltyFuncList args

	parseArgs $argc $argv

	if [cequal $plat unix] {
		set myhldir $args(hldir)
	} else {
		if [cequal $args(hldir) ""] {
			set myhldir [pwd]
		} else {
			set myhldir $args(hldir)
		}
	}

	# If they used -d, set HLDIR so mod-voting will do the right thing
	set $env(HLDIR) $myhldir

	if ![file isdir $myhldir] {
		puts stdout "Couldn't find HLDIR directory '$myhldir'"
		usage
	}

	cd $myhldir

	# Stop maplist code from blowing up
	if ![file exists maps] { 
		catch {file mkdir maps}
		chmod 0777 maps
	}

	initStats
	readText

	# Get arguments
	if ![cequal $args(extension) ""] {
		set cfgPostfix ".$args(extension)"
	} else {
		set cfgPostfix ""
	}

	set gameId $args(gameid)

	if ![file isdir $myhldir/$gameId] {
		puts stdout [format $text(86) $gameId $myhldir]
		usage
	}

	if ![cequal $cfgPostfix ""] {
		myEcho INFO [format $text(210) $cfgPostfix]
	}

	readConfig 1
	if { $config(debug) < $args(debug) } {
		set config(debug) $args(debug)
	}

	if ![file exists $myhldir/$gameId/$config(oldlogsdir)] {
		catch {file mkdir $myhldir/$gameId/$config(oldlogsdir)}
		chmod 0777 $config(oldlogsdir)
	}

	if [cequal $plat unix] {
		# Check to see if halfd is already out there
		set fds(myPid) $myhldir/$gameId/halfd.pid$cfgPostfix

		if [file exists $fds(myPid)] {
			set pid [read_file $fds(myPid)]
			if ![catch {kill 0 $pid} err] {
				# Probably already running!
				puts stdout [format $text(87) $gameId $pid]
				puts stdout [format $text(88) $fds(myPid)]
				myExit 2
			}
		}
	}

	if $config(tcldebug) tcldebugOn

	#daemonize

	if [cequal $plat unix] {
		if [catch {write_file $fds(myPid) [pid]} err] {
			myEcho ERR "FATAL: Unable to create PID file: $err"
			myEcho ERR "       Make sure you have write permission to your mod directory!"
			myExit 1
		}

		set fds(pid)   $myhldir/$gameId/hlds_l.pid$cfgPostfix
		set fds(cmd)   $myhldir/$gameId/hlds_l.cmd$cfgPostfix
		set fds(log)   $myhldir/$gameId/hlds_l.log$cfgPostfix
	}

	set fds(mylog) $myhldir/$gameId/halfd.log$cfgPostfix
	set fds(adminlog) $myhldir/$gameId/halfd.admin
	set fds(teammap)  $myhldir/$gameId/halfd.teammap

	# Open the logfile for the daemon
	if $config(rotatetype) {
		set yymmddhhmm [clock format [clock seconds] -format %Y%m%d%H%M]
		if [cequal $plat windows] { append yymmddhhmm .log }
		set mylogtail [file tail $fds(mylog)]
		if [catch {file rename -force $fds(mylog) $myhldir/$gameId/$config(oldlogsdir)/$mylogtail.$yymmddhhmm} err] {
			myEcho WARN "Error renaming log file: $err"
		}
	} else {
		catch {file rename -force $fds(mylog) $fds(mylog).old}
	}
	set fds(mylogfd) [open $fds(mylog) w]

	if ![cequal $cfgPostfix ""] {
		myEcho INFO [format $text(210) $cfgPostfix]
	}

	if ![cequal $args(loadobj) ""] {
		myEcho INFO "Loading Tcl file $args(loadobj)..."
		if [catch { source $args(loadobj) } err] {
			myEcho ERR "Error: $err"
			myExit 1
		}
	}

	readConfig
	if { $config(debug) < $args(debug) } {
		set config(debug) $args(debug)
	}

	setExprsNew
	startupMessages $vnum $gameId

	clearUserInfo
	readBadwords
	readExclude
	readTeammap

	foreach tOp $penaltyFuncList {
		$tOp read
		$tOp sched
	}

	if $config(halfbot) { readHalfBot }

	if $config(plugins) { loadPlugins 1 }

	checkForServer
	if !$serverUp { serverDown }
	doListen

	signalHandler
	if { !$serverUp && $config(autostart) } launchServer

	if $serverUp { 
		getMapTime
		getMapTimeRem
	}

	if $config(console) {
		puts stdout "halfd console output is ENABLED"
	}

	mainloop
	myEcho DBUG "SHOULDNT BE HERE"
}

################################# HalfBot ###############################
proc halfBot { name uid wonid team theysaid } {
	global config hbGlobal hbMap hbTriggers text stats last

	upvar $theysaid said

	if ![info exists last(trigger)] {
		set last(trigger) 0
	}

	if { [clock seconds] < [expr $last(trigger) + $config(halfbotmax)] } {
		if { $config(debug) > 1 } {
			myEcho DBUG "Less than $config(halfbotmax) seconds since last trigger.  Not checking '$said'"
		}
		return
	}

	if [catch {regexp -nocase $hbTriggers " $said"} rc] {
		myEcho ERR [format $text(253) $rc]
		return
	}

	if $rc {
		# Find the trigger that fired
		# NOTE: map triggers will override global triggers!
		set response ""
		foreach array "hbMap hbGlobal" {
			foreach trigger [array names $array] {
				if [catch {regexp -nocase $trigger " $said"} rc] {
					myEcho ERR [format $text(253) $rc]
					return
				}

				if $rc {
					set response [set ${array}($trigger)]
					break
				}
			}

			if ![cequal $response ""] break
		}

		if ![cequal $response ""] {
			set last(trigger) [clock seconds]
			myEcho DBUG "HalfBot triggered: '$trigger'"
			regsub -all "%N" $response $name  response
			regsub -all "%U" $response $uid   response
			regsub -all "%W" $response $wonid response
			regsub -all "%T" $response $team  response

			# %B = break.
			if ![cequal [string first "%B" $response] -1] {
				myEcho DBUG "Found %B in response. Splitting line."

				regsub -all %B $response \n foo
				set list [split $foo \n]

				foreach el $list {
					execHalfBot $el
				}
			} else {
				execHalfBot $response
			}

			incr stats(halfbot)
		}
	}
}

proc execHalfBot { response } {
	myEcho DBUG "HalfBot response : '$response'"

	set first2 [csubstr $response 0 2]
	switch -exact -- $first2 {
		"C:" {
			# execute a command on the server
			execCmd [csubstr $response 2 end]
		}

		"P:" {
			# execute a halfd procedure
			if [catch { [csubstr $response 2 end] } err]
				myEcho WARN "HalfBot: [csubstr $response 2 end] error: '$err'"
			}
		
		default {
			say $response
		}
	}
}

proc readHalfBot { {map ""} } {
	global config hbGlobal hbTriggers hbMap myhldir gameId text

	if [cequal $map ""] {
		# Read the 'global' config
		foreach id [array names hbGlobal] { unset hbGlobal($id) }
		set fname $myhldir/$gameId/halfbot.cfg
		set array hbGlobal
		myEcho DBUG "Reading global HalfBot config."
	} else {
		# Read map-specific config
		foreach id [array names hbMap] { unset hbMap($id) }
		set fname $myhldir/$gameId/halfbot_$map.cfg
		set array hbMap
		if ![file exists $fname] {
			myEcho DBUG "No HalfBot file for $map."
			return
		}
		myEcho DBUG "Reading HalfBot config for $map."
	}

	readHalfBotDetail $fname $array

	# Populate hbTriggers
	set hbTriggers ""
	set first 1
	set hbGlobalList [lsort -decreasing [array names hbGlobal]]
	set hbMapList    [lsort -decreasing [array names hbMap]]
myEcho DBUG "hbGlobal   = '$hbGlobalList'"
myEcho DBUG "hbMap      = '$hbMapList'"
	foreach trigger $hbMapList {
		if $first {
			set hbTriggers "$config(halfbotprefix)$trigger"
			set first 0
		} else {
			append hbTriggers "|$config(halfbotprefix)$trigger"
		}
	}

	foreach trigger $hbGlobalList {
		if $first {
			set hbTriggers "$config(halfbotprefix)$trigger"
			set first 0
		} else {
			append hbTriggers "|$config(halfbotprefix)$trigger"
		}
	}

	myEcho DBUG "hbTriggers = \"$hbTriggers\""
}

proc readHalfBotDetail { fname array } {
	global $array text config

	if [catch { open $fname r } fd] {
		myEcho ERR "Error opening $fname: $fd"
		return
	}

	myEcho DBUG "Reading HalfBot configuration file: $fname"

	while 1 {
		if [eof $fd] break

		if [catch { gets $fd } data] {
			myEcho ERR "Error reading from $fname: $data"
			catch { close $fd }
			return
		}

		set data [string trim $data]
		if [cequal $data ""] continue
		if [cequal [csubstr $data 0 1] #] continue

		lassign [parseConfigFile $data] key val
		set ${array}($key) $val

		if { $config(debug) > 1 } {
			myEcho DBUG "HalfBot ($key) = ($val)"
		}
	}

	catch { close $fd }
}
############################### end HalfBot #############################

################################# Plugins ###############################
proc loadPlugins { { init 0 } } {
	global config text pluginList

	if $init { set pluginList "" }

	set sourceme ""
	foreach file $config(pluginsource) {
		if [catch {read_file $file} code] {
			myEcho ERR [format $text(256) $file $code]
			continue
		} 

		myEcho DBUG "Validating code in $file..."
		set good 1
		foreach line [split $code \n] {
			if [cequal [csubstr $line 0 5] "proc "] {
				set rest [string trim [csubstr $line 5 end]]
				set pname [lindex [split $rest " "] 0]

				if ![lempty [info commands $pname]] {
					if $init {
						myEcho WARN [format $text(257) $pname $file]
						set good 0
						break
					} else {
						if [cequal [lsearch -exact $pluginList $pname] -1] {
							myEcho WARN [format $text(257) $pname $file]
							set good 0
							break
						} else {
							myEcho INFO "Will reload procedure '$pname'..."
						}
					}
				} else {
					myEcho DBUG "Found new procedure '$pname'"
				}

				if $init { lappend pluginList $pname }
			}
		}

		if $good { lappend sourceme $file }
	}

	foreach file $sourceme {
		myEcho DBUG "Sourcing $file..."
		if [catch { source $file } err] {
			myEcho ERR [format $text(256) $file $err]
		}
	}

	registerPlugins
}

proc registerPlugins {} {
	global config pluginReg text

	foreach name [array names pluginReg] { 
		myEcho DBUG "Unregistering $name plugins: '$pluginReg($name)'"
		unset pluginReg($name)
	}

	if !$config(plugins) return

	foreach pname [array names config plugin.*] {
		if !$config($pname) continue

		lassign [split $pname .] foo event proc

		if [lempty [info commands $proc]] {
			myEcho WARN [format $text(258) $proc]
			continue
		}

		myEcho DBUG "Registering plugin '$proc' for event '$event'"

		if [info exists pluginReg($event)] {
			lappend pluginReg($event) $proc
		} else {
			set pluginReg($event) $proc
		}
	}
}

proc runPlugin { type proc args } {
	global config text
	
	if !$config(plugins) return

	myEcho DBUG "Calling plugin $proc with $type '$args'"
	if [catch {$proc $type $args} err] {
		myEcho WARN [format $text(259) $proc $type]
		myEcho WARN [format $text(260) $args]
		myEcho WARN [format $text(261) $err]
	}
}
############################### end Plugins #############################
	

proc myEcho { type text } {
	global fds config plat

	if [cequal $text ""] {
		set text "You probably don't have the most recent halfd.txt!"
		set type WARN
	}

	if [info exists config(debug)] {
		if { !$config(debug) && [cequal $type DBUG] } return
	}

	if ![info exists fds(mylogfd)] {
		echo $text
		return
	}

	set msg  "\[[now]\] ([format "%-4s" $type]) $text"

	puts $fds(mylogfd) $msg

	if [cequal $plat unix] {
		# Bah.  Windoze file i/o seems to choke on heavy output.
		catch { flush $fds(mylogfd) }
	}

	if $config(hlbpoutput) { puts $msg }
}

proc cycleLogFile {} {
	global fds text vnum gameId stats config myhldir plat

	if ![info exists fds(mylogfd)] return

	dumpStats
	myEcho INFO $text(109)

	close $fds(mylogfd)
	unset fds(mylogfd)

	if $config(rotatetype)  {
		set yymmddhhmm [clock format [clock seconds] -format %Y%m%d%H%M]
		if [cequal $plat windows] { append yymmddhhmm .log }
		set mylogtail [file tail $fds(mylog)]
		if [catch { file rename -force $fds(mylog) $myhldir/$gameId/$config(oldlogsdir)/$mylogtail.$yymmddhhmm } err] {
			myEcho WARN "Error renaming log file: $err"
		}
	} else {
		catch { file rename -force $fds(mylog) $fds(mylog).old }
	}

	set fds(mylogfd) [open $fds(mylog) w]

	myEcho INFO $text(110)
	myEcho INFO [format $text(115) $stats(start)]

	startupMessages $vnum $gameId
}

proc dumpStats { {bcast 0 } { fd "" } { auth 2 } { hlbp 0 } } {
	global fds stats conns text config penaltyList votewins

	if ![info exists text(1)] return

	# Populate stats(globalvars)
	dumpVars 1

	set txt [list [format $text(69) [now]]]
	lappend txt [format $text(70) $stats(start)]
	lappend txt [format $text(71) $stats(out)]
	lappend txt [format $text(72) $stats(in)]
	lappend txt [format $text(73) [expr $stats(in) + $stats(out)]]

	# Connection stats
	lappend txt [format $text(74) $stats(conns)]
	lappend txt [format $text(75) $stats(user)]
	lappend txt [format $text(76) $stats(admin)]

	set failed [expr $stats(conns) - [expr $stats(user) + $stats(admin)]]

	lappend txt [format $text(77) $failed]
	lappend txt [format $text(78) $stats(ips)]

	if $config(getdeaths) {
		lappend txt [format $text(284) $stats(deaths)]
	}

	# Voting stats
	if { $stats(votewins) > 0 } {
		lappend txt [format $text(248) ""]

		set max 0
		foreach map $stats(votelist) {
			if { [clength $map] > $max } { set max [clength $map] }
		}

		foreach map [lsort $stats(votelist)] {
			set fmtmap [format "% ${max}s" $map]
			set fmtwin [format "% 3d" $votewins($map)]
			lappend txt "   $fmtmap - $fmtwin"
		}

		lappend txt [format $text(247) $stats(votewins)]
	}

	# Penalty stats
	foreach penalty $penaltyList {
		set warn $stats(${penalty}warn)
		set acts $stats(${penalty}acts)

		if { $config(debug) > 1 } {
			myEcho DBUG "Checking for warn on $penalty: var=stats(${penalty}warn) val=$warn"
			myEcho DBUG "Checking for acts on $penalty: var=stats(${penalty}acts) val=$acts"
		}

		if $warn {
			lappend txt [format $text(200) $penalty $stats(${penalty}warn)]
		}

		if $acts {
			lappend txt [format $text(201) $penalty $stats(${penalty}acts)]
		}
	}

	# Crashes
	lappend txt [format $text(79) $stats(crash)]

	# Memory usage
	lappend txt [format $text(252) $stats(globalvars)]

	# Halfbot
	if { $stats(halfbot) > 0 } {
		lappend txt [format $text(254) $stats(halfbot)]
	}

	# Active connections
	# Non-admins shouldn't see active connections
	if { $auth > 1 } {
		lappend txt $text(80)

		set i 0
		foreach sock [array names conns] {
			if [cequal $sock fds] continue

			keylget conns($sock) addr addr
			keylget conns($sock) port port
			keylget conns($sock) auth auth

			lappend txt "### \[$sock\] - $addr:$port (auth=$auth)"
		}
	}

	foreach line $txt {
		if !$bcast { 
			myEcho INFO $line
		} else {
			if $hlbp {
				writeHlbpCommand 04 $line
			} else {
				sendClient TEXT "[format %c 0x02]$line" $fd
			}
		}
	}
}

proc initStats {} {
	global stats text penaltyList

	set stats(start) [now] ;# When did daemon start

	set stats(out)   0     ;# How many bytes out
	set stats(in)    0     ;# How many bytes in

	set stats(conns) 0     ;# How many total connections (from halfd clients)
	set stats(user)  0     ;# How many auth'ed as USER
	set stats(admin) 0     ;# How many auth'ed as ADMINISTRATOR
	set stats(ips)   ""    ;# What IPs did halfd clients come from

	set stats(crash) 0     ;# How many times we crashed

	foreach penalty $penaltyList {
		set stats(${penalty}warn) 0
		set stats(${penalty}acts) 0
	}

	set stats(votewins) 0
	set stats(votelist) ""

	set stats(globalvars) 0
	set stats(halfbot) 0

	set stats(deaths) 0
}

proc checkForExtendedTcl {} {
	if { [string length [info commands cequal]] > 0 } { return }

	puts stdout "The Tcl interpeter you are running is NOT an Extended Tcl"
	puts stdout "(TclX) interpreter."
	puts stdout "halfd requires TclX.  Please install TclX and try again."
	puts stdout "BTW - The 'X' is for eXtended.  It has nothing to do with X-Windows."
	puts stdout "See FAQ or visit the forums for more information."

	exit 1
}

proc dumpVars { {fromstats 0} } {
	global stats text config
	set stats(globalvars) 0

	if ![info exists config(debug)] return

	if !$fromstats {
		myEcho DBUG "################ DUMPING ALL GLOBAL VARIABLES"
	}

	foreach var [lsort [info globals]] {
		global $var

		# Check to see if it's an array
		if [array exists $var] {
			foreach name [lsort [array names $var]] {
				set myvar ${var}($name)
				set val   [set $myvar]
				if !$fromstats { dumpOneVar $myvar val }
				incr stats(globalvars) [expr [clength $val] + [clength $var]]
			}
		} else {
			set val [set $var]
			if !$fromstats { dumpOneVar $var val }
			incr stats(globalvars) [expr [clength $val] + [clength $var]]
		}
	}

	if !$fromstats {
		myEcho DBUG "################ DUMP COMPLETE"
		myEcho INFO [format $text(252) $stats(globalvars)]
	}
}

proc dumpOneVar { var myval } {
	global config

	upvar $myval val

	if $config(debug) {
		if { [clength $val] >  0 } {
			myEcho DBUG [format "%05s % 15s %s" [clength $val] $var $val]
		}
	}
}

proc initVars {} {
	global vote serverUp svrInfo conns lastrotate prevMap protected
	global last szcnt initializing notIdle ignoretime wlimit svrInfo
	global oldfoulprefix roundstart votestart tcl_platform plat connected
	global hlbpCache dontprocess lastcmd enterwait penaltyList penaltyArrays
	global penaltyFuncList after config

	random seed [clock seconds]

	clearUserInfo

	set vote(insession) 0

	set serverUp 0

	foreach id "name ip port map logFile" { set svrInfo($id) "" }
	foreach id "mapstart kills player"  { set svrInfo($id) 0 }

	set conns(fds) ""

	set lastrotate [clock format [clock seconds] -format %a]

	set svrInfo(starttime) 0
	set svrInfo(expired) 0
	set svrInfo(mapcyclefile) ""
	set svrInfo(hlbp) 0

	set prevMap ""
	set protected 0
	set roundstart 0
	set votestart 0
	set last(timeleft) 0
	set last(nextmap) 0
	set last(maplist) 0
	set last(size) 0
	set last(vote) 0
	set last(ping) 0
	set last(whattime) 0
	set last(votestatus) 0
	set last(mypenalty) 0
	set last(modlist) 0
	set last(mapcycle) 0
	set szcnt 0

	set initializing 1

	set notIdle 1

	set ignoretime 0
	set wlimit ""

	set svrInfo(version) "<unknown>"
	set svrInfo(maptime) 0

	set oldfoulprefix ""

	if ![info exists tcl_platform(platform)] {
		puts stdout "ERROR: Can't determine platform!  Assuming Windows."
		set plat windows
	} else {
		set plat $tcl_platform(platform)
	}
	#set plat windows

	set connected 0
	set hlbpCache ""
	set dontprocess 0
	set lastcmd ""

	set enterwait 0

	set config(admininstalled) 0

	# PENALTIES
	# Add penalties here.  You also need to create a "<penalty>Table" function.
	set penaltyList "tk weapon foul ping attack suicide team"

	# Don't mess with this :-)
	set penaltyFuncList ""
	set penaltyArrays ""
	foreach pen $penaltyList {
		lappend penaltyFuncList "${pen}Table"
		lappend penaltyArrays   "${pen}count"
		set after($pen) 0
	}
}

# Can you BELIEVE that sleep() doesn't work on windoze!?!! !AGH!!
proc mySleep { seconds } {
	global plat dontprocess sleepwait

	if [cequal $plat unix] {
		# Ah. Sanity.
		if ![ctype digit $seconds] {
			# Assume floating-point
			select {} {} {} $seconds
		} else {
			sleep $seconds
		}
	} else {
		# %&*&@#$!
		set sleepwait 0
		set sleeptime [expr int([expr $seconds * 1000])]
		after $sleeptime { set sleepwait 1 }
		set dontprocess 1
		vwait sleepwait
		set dontprocess 0
	}
}

proc parseArgs { argc argv } {
	global args env plat defaultgame vnum 

	set args(debug) 	0 
	set args(gameid)	$defaultgame 
	set args(extension)	"" 
	set args(loadobj)	"" 
	set skip 0

	if [info exists env(HLDIR)] {
		set args(hldir)	$env(HLDIR)
	} else {
		set args(hldir)	""
	}

	set i 0
	while 1 {
		set arg [lindex $argv $i]

		switch -exact -- $arg {
			-debug {
				# Debug
				incr i
				set arg [lindex $argv $i]

				if ![ctype digit $arg] {
					puts stdout "-debug requires an argument; must be an integer!"
					usage
				}

				set args(debug) $arg
			}

			-h usage

			-s { set skip 1 }

			-d {
				# HLDIR
				incr i
				set arg [lindex $argv $i]

				if [cequal $arg ""] {
					puts stdout "-d requires an argument; must be a directory"
					usage
				}

				if !$skip {
					if ![file exists "$arg/hlds_run"] {
						puts stdout "-d argument must be a directory containing hlds_run"
						usage
					}
				}

				set args(hldir) $arg
			}

			-g {
				# Game
				incr i
				set arg [lindex $argv $i]

				if [cequal $arg ""] {
					puts stdout "-g requires an argument; must be a valid MOD."
					usage
				}

				if [cequal [csubstr $arg 0 1] -] {
					puts stdout "-g requires an argument; must be a valid MOD."
					usage
				}

				set args(gameid) $arg
			}

			-e {
				# Extension
				incr i
				set arg [lindex $argv $i]

				if [cequal $arg ""] {
					puts stdout "-e requires an argument; must be a file extension."
					usage
				}

				if [cequal [csubstr $arg 0 1] -] {
					puts stdout "-e requires an argument; must be a file extension."
					usage
				}

				set args(extension) $arg
			}

			-l {
				# Load new procedures
				incr i
				set arg [lindex $argv $i]

				if [cequal $arg ""] {
					puts stdout "-l requires an argument; must be a Tcl source file"
					usage
				}

				if [cequal [csubstr $arg 0 1] -] {
					puts stdout "-l requires an argument; must be a Tcl source file"
					usage
				}

				set args(loadobj) $arg
			}

			-v {
				# Version information
				echo "halfd version $vnum"

				myExit 0
			}

			-V {
				printExtendedVersionInfo

				myExit 0
			}

			default {
				if [cequal $arg ""] break
				puts stdout "Unknown argument: $arg"
				usage
			}
		}

		incr i
		if { $i > $argc } break
	}

	if { [cequal $args(hldir) ""] && [cequal $plat unix] } {
		puts stdout "Must either use -d argument or HLDIR environment variable."
		usage
	}
}

proc commandLineDebug {} {
	global argv0 argv 

	echo "argv0 = '$argv0'"
	echo "argv  = '$argv' (llength = '[llength $argv]')\n"

	set i 0
	foreach el $argv {
		echo "$i = $el"
		incr i
	}

	echo ""
		
	if [catch {printExtendedVersionInfo} err] {
		echo "Error printing extended information: $err"
	}

	exit 0
}

proc printExtendedVersionInfo {} {
	global argv0 vnum tcl_version tcl_patchLevel tcl_platform

	# Extended version information
	set hinfo $argv0
	if [cequal [csubstr $argv0 0 1] .] {
		append hinfo " (in [pwd])"
	}

	echo "halfd version  : $vnum"
	echo "\nExecuting from : $hinfo"
	echo "\nTcl version    : $tcl_version"
	echo "\nTcl patchlevel : $tcl_patchLevel"
	echo "\nTcl platform information :"
	foreach key [array names tcl_platform] {
		myEcho INFO [format "% 14s = '%s'"  $key $tcl_platform($key)]
	}
}

# Set up all regular expressions in one place so that I can keep myself
# sane ;)
proc setExprsNew {} {
	global exprs

	########################################
	# (1) Connection
	# returns playerinfo and ip address (port is trimmed off)
	set exprs(connected) "^L.*\"(.+)\" *connected, address *\"(.+):.+\""

	# (2) Enter game
	set exprs(entered) "^L.*\"(.+)\" .*entered the game"

	# (3) Disconnection
	set exprs(disconnected) "^L.*\"(.+)\" *disconnected"

	# (4) Suicide
	set exprs(suicide) "^L.*\"(\[^\"\]+)\" *committed suicide with *\"(\[^\"\]+)\"(.*)$"

	# (5) Team selection
	#
	# returns the following:
	#	1 = player info
	#	2 = team name
	set exprs(team)        "^L.*\"(.+)\" *joined team *\"(.+)\""
	set exprs(actionteam)  "^L.*\"(.+)\" *joined *(.+)"
	set exprs(siteam)      "^L.*\"(.+)\" *is switching to *(.+)"
	set exprs(oldchange)   "^L.*\"(.+)\" *changed to team *(.+)"

	# (6) Role selection
	#
	# returns the following:
	#	1 = player info
	#	2 = role
	set exprs(role) "^L.*\"(.+)\" *changed role to *\"(.+)\""

	# (7) Change Name
	# returns the following:
	#	1 = player info (old name?)
	#	2 = new name
	set exprs(name) "^L.*\"(\[^\"\]+)\" *changed name to *\"(\[^\"\]+)\"$"

	# Events (8) killed, (9) attacked
	#
	# returns the following:
	#	1 = attacker info
	#	2 = attackee info
	#	3 = weapon name
	#	4 = properties (headshot, damage)
	set exprs(killed)   "^L.*\"(\[^\"\]+)\" *killed *\"(\[^\"\]+)\" *with *\"(\[^\"\]+)\"(.*)$"
	set exprs(attacked) "^L.*\"(.+)\" *attacked *\"(.+)\" *with *\"(\[^\"\]+)\"(.*)$"

    set exprs(fakilled) "^L.*\"(\[^\"\]+)\" *killed *\"(\[^\"\]+)\".*weapon *\"(\[^\"\]+)\"(.*)$"
	set exprs(oldkilled) "^L.*\"(\[^\"\]+)\" *killed *\"(\[^\"\]+)\" *with *(.+)$"

	# (10) Player-to-Player action triggers
	#
	# returns the following:
	#	1 = trigger-er info
	#	2 = action
	#	3 = trigger-ee info
	#	4 = properties
	set exprs(ptopaction) "^L.*\"(.+)\" *triggered *\"(.+)\" *against *\"(.+)\"(.*)"
	set exprs(sgkill) "^L.*\"(.+)\" *triggered *\"Sentry_Destroyed\" *against *\"(\[^\"\]+)\"(.*)"

	# (11) Player-independent actions
	#
	# returns the following:
	#	1 = trigger-er info
	#	2 = action
	#	3 = properties
	set exprs(paction) "^L.*\"(.+)\" *triggered *\"(.+)\"(.*)"

	# (12) Team actions
	# returns the following:
	#	1 = team name
	#	2 = action
	#	3 = properties
	set exprs(taction) "^L.*Team \"(.+)\" *triggered *\"(.+)\"(.*)"

	# (13) World actions
	# returns the following:
	#	1 = action
	#	2 = properties
	set exprs(waction) "^L.*World *triggered *\"(.+)\"(.*)"

	# (14) Chat
	# returns the following:
	#	1 = player info
	#	2 = "say", "say_team"
	#	3 = what they said
	set exprs(chat) "^L.*\"(\[^\"\]+)\" *(say|say_team) *\"(\[^\"\]+)\"$"
	set exprs(chat) "^L.*\"(\[^\"\]+)\" *(say|say_team) *\"(\[^\"\]+)\""
	set exprs(oldchat) "^[format %c 0x02](\[^:\]+): *(.*)"

	# (15) Team alliances

	# (16) Round-end team score

	# (17) Cvars
	# returns variable name and value
	set exprs(cvar) "^L.*Server cvar *\"(.+)\" *= *\"(.+)\""

	# (18) Start-of-map
	# Returns log file name and server version
	set exprs(log) "^L.*Log file started.*file *\"(\[^\"\]+)\".*version *\"(\[^\"\]+)\".*"

	# (19) Map change
	# Returns map name.
	set exprs(mapload)  "^L.*Loading *map *\"(\[^\"\]+)\""
	set exprs(mapstart) "^L.*Started *map *\"(\[^\"\]+)\""

	# (20) Rcon

	# (21) Server Name
	# Returns host name.
	set exprs(svrname) "^L.*Server name is *\"(.+)\""

	########################################
	# MISCELLANEOUS

	# Player Info - use this on expressions above that return player information
	#
	# returns the following:
	#	1 = player name
	#	2 = uid
	#	3 = wonid
	#	4 = team
	# DoD uses "-1" for WONids on LAN.  Sigh...
	set exprs(player) "(.+)<(\[0-9\]+)><(\[0-9\]+)><(.*)>"
	set exprs(player) "(.+)<(\[0-9\]+)><(.\[^>\]*)><(.*)>"

	# Firearms
	set exprs(faplayer) "(.+)<(\[0-9\]+)><(.*)>"

	# Old-logging mods
	set exprs(oldplayer) "(.+)<(\[0-9\]+)>"


	# Filters
	set exprs(ignore) "^PackFile|^NET_"
	set exprs(usercnt) "^\[0-9\]* users"

	# Refresh conditions
	set exprs(refresh) "entered the game$|onnected,|^Dropped|changed name|^Creating bot"

	# "low status" feature
	set exprs(dropped) "^Dropped (.*) from server$"

	# Look for SZ_GetSpace, if it repeats then kill the server
	set exprs(szget) "^SZ_GetSpace"

	# Server messages
	set exprs(srvmsg) "^\<"

	set exprs(parsemodel) "^model (.+)"
	set exprs(parseteam)  "^team (.+)"
	set exprs(bot) "User not in server"

	set exprs(maplist) "^(maplist|listmaps|listmap) *(.*)"
	set exprs(size)    "^(size|mapsize|sizemap) +(\[^ \]+)$"
	set exprs(vote)    "^(vote|mapvote|votemap) +(\[^ \]+)$"

	set exprs(foul) "";# This is built by readBadwords
	#set exprs(dropped) "^Dropped"
	set exprs(timechange) "^L.*\"mp_timelimit\" = .*|Rcon.*mp_timelimit.*"
	set exprs(varquery)  "\"%V\" is \"(.+)"

	set exprs(users) "(.+) : (.+) : (.+)"
	set exprs(endusers) "\[0-9\]* users"

	# Filter out team-based and rcon messages for non-admins that are
	# receiving text
	set exprs(txtfilter) "TEAM|^\[Rr\]con|Rcon:| say_team |sv_password|\\\[ADMIN\\\] User|\\\[ADMIN\\\] DEBUG|tfc_adminpwd|password_field"

	# Server messages
	set exprs(srvmsg) "^\<"
	set exprs(chatmode) "^[format %c 0x02]|^\<"

	# 'status' output
	#	returns: name userid wonid frags time ping loss ip
	#		ip is blank for bots
	#	
	set exprs(status) "^#.*\"(\[^\"\]+)\" *(.+) *(.+) *(.+) *(.+) *(.+) *(.+) *(.+)"
	set exprs(status) "^#.*\"(\[^\"\]+)\" *(.+)"
	set exprs(killcount)   " killed "
	
	# Send update anyway
	set exprs(sendupdate) "^L|^\<|^[format %c 0x02]|^Reason|^Drop"

	# admin-mod
	#
	# admin_bot_kick
	set exprs(botkick)  "^L.*: .*ADMIN.*\\\] BOT-KICK (.*)"

	# admin_halfd
	set exprs(admincmd) "^L.*: .*ADMIN.*\\\] halfd: (.*)"
}

#######################################################################
# set some variables for use in the procs and call MAIN
#
checkForExtendedTcl
initVars

#set fd [open dbg w]
#cmdtrace on $fd 

set vnum 2.14
set defaultgame valve

if ![cequal [string first cmddebug $argv] -1] commandLineDebug

main [llength $argv] $argv
