#!/usr/bin/tcl
#
# halfd
#
# Half-Life dedicated server (hlds_run) administration/monitoring daemon
# Copyright (C) 2000-2003 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 forums at http://www.halfd.org
#
# Rob Abbott    <maelstrom@halfd.org>               rabbott 
# Frank Odignal <odignal@stud.fbi.fh-darmstadt.de>  odignal
# Alec Shaner   <ashaner@chumpland.org>             ashaner
#
#   rabbott 2003-12-22   - Add versioning to patch system
#                        - Add "amx_chat" to chatmode
#                        - 2.20b15
#
#   rabbott 2003-09-17   - Remove requirement to have 'hlds' executable with -d
#
#   rabbott 2003-09-10   - Add 'saymax'
#
#   rabbott 2003-08-27   - Add patch functionality
#                        - Add average ping to STATS
#
#   rabbott 2003-08-26   - Fix problem with AMD cpu-type-determination
#                          (this is getting old!)
#                        - 2.20b14
#
#   rabbott 2003-08-24   - Fix problem with cpu-type-determination
#                        - Fix "UTF character longer than a int" crash
#                          on higher versions of Tcl.
#                        - Add 'support information' to logfile
#                        - 2.20b13
#
#   rabbott 2003-08-13   - AMX Mod support
#                        - Force 'getstats' to 0 for 3.1.1.0
#                        - If optimized binary is missing, use 'hlds'
#                        - Robustify determineHLDS - thanks BAS
#                        - 2.20b12
#
#   rabbott 2003-08-07   - Initialize stats related to 'getstats'
#
#   rabbott 2003-06-09   - 2.20b10
#
#   rabbott 2003-05-29   - Add 'timelimit1' option
#
#   rabbott 2003-05-21   - Fix logfile-deletion bug (due to 3.1.1.1?)
#                        - Add SETCONFIG command
#
#   rabbott 2003-05-14   - 2.20b9
#                        - Parse 'stats' output 
#                        - Added SERVERSTATUS command
#                        - Determine hlds binary to use based on CPU
#
#   rabbott 2003-05-05   - Remove trailling slash from hldir
#                        - Make 'halfbot open error' message more concise
#
#   rabbott 2003-04-23   - halfbotauth
#
#   rabbott 2003-04-07   - Check for unique passwords
#
#   rabbott 2003-03-09   - 2.20b7 release
#
#   rabbott 2003-02-18   - Make 'checkForHungServer' more forgiving
#
#   rabbott 2003-01-22   - Move everything to addons/halfd/
#                        - newapi MAPLIST now terminates with blank line
#
#   rabbott 2003-01-06   - Fix problem with HalfBot "P:" directive
#
#   rabbott 2002-10-06   - Added "getteamfromuser" option
#                        - More "forgive tk" keys (tkforgive, !forgivetk, etc)
#                        - Update GPL output; add 'GPL' command
#
#   rabbott 2002-10-04   - Added "-k" option
#
#   rabbott 2002-10-03   - Updated GETCONFIG command
#                        - Add CHAT2 command
#
#   rabbott 2002-09-25   - Add GETCONFIG command
#
#   rabbott 2002-09-03   - Yet another hostage-check fix.  Stupid hostages.
#
#   rabbott 2002-09-03   - Fix MORE hostage-check bugs :(
#
#   rabbott 2002-08-29   - Fix hostage-check bugs
#
#   rabbott 2002-08-14   - If map is out-of-cycle, don't announce nextmap
#
#   rabbott 2002-08-08   - Add 'voteignore2'
#                        - RELEASE 2.19
#
#   rabbott 2002-08-07   - Fix bug in 'ignoretkta'
#                        - Add ABORTVOTE
#                        - Add 'votewinmsg'
#
#   rabbott 2002-07-25   - Count Adminmod voting in stats
#
#   rabbott 2002-07-19   - Fix TK and TA decay issue (forgives broke it!)
#                        - Fix %L in halfbot responses
#                        - Add 'ignoretkta'
#
#   rabbott 2002-07-02   - Add 'newapiusrauth' logic
#
#   rabbott 2002-06-21   - Add 'hostageroundlimit' logic
#
#   rabbott 2002-06-07   - Fix logfile deletion bug
#
#   rabbott 2002-04-27   - Accomodate HLTV weirdness in 1109
#
#   rabbott 2002-04-26   - Don't print warning messages if authid is blank
#
#   rabbott 2002-04-24   - Accomodate 4109 ("hlds" instead of "hlds_run")
#
#   rabbott 2002-04-09   - if nextmap is 0, disable automated nextmap messages
#
#   rabbott 2002-04-06   - Fix problem with concurrent newapi clients
#
#   rabbott 2002-04-04   - Add 'sayquotes' option
#
#   rabbott 2002-03-18   - Add ARGS command
#
#   rabbott 2002-03-18   - Allow %K in weapon-taunt for killer
#
#   rabbott 2002-03-09   - Handle HLBP auto-restart of server
#
#   rabbott 2002-03-07   - Add hostage-kill penalties
#
#   rabbott 2002-02-28   - If halfd.gdb_commands doesn't exist, create it
#
#   rabbott 2002-02-26   - Fix stats logfile function
#                        - Add "W:" (wait) command to HalfBot
#
#   rabbott 2002-02-11   - Work around DoD 2.0 ugliness
#
#   rabbott 2002-01-25   - Don't count HLTV in votes
#
#   rabbott 2002-01-15   - Reset user information immediately on map change
#
#   rabbott 2002-01-09   - Trap SIGTERM
#
#   rabbott 2002-01-08   - MAPLIST on newapi uses separator between maps
#
#   rabbott 2002-01-06   - New commands: HBON, HBOFF, TAUNTSON, TAUNTSOFF
#                        - newapi: MAPLIST and MODLIST now proper format
#
#   rabbott 2001-12-23   - newapi: blank line between packets
#
#   rabbott 2001-12-20   - Trap SIGQUIT
#                        - newapi: optionally wrap names or text in quotes
#
#   rabbott 2001-12-18   - Add 'fortuneargs' option
#                        - Don't duplicate penalty messages with psay
#                        - Added 'newapiauth', 'newapisep'
#
#   rabbott 2001-12-17   - 'mypenalty' uses admin_psay if adminmod installed
#
#   rabbott 2001-12-13   - Change unix logfile event handler to be 
#                          more responsive
#                        - Added RECONNECT command (win32 only)
#                        - Added hlbpreset option (win32 only)
#
#   rabbott 2001-12-12   - Print correct message for TA forgives
#                        - Stop console output if we get i/o errors
#                        - Add support for console INPUT - allows screen!
#                        - Add CONSOLEOFF command
#
#   rabbott 2001-12-09   - Fix crash if 'stats' was enabled
#                        - Print correct message for ATTACKON and ATTACKOFF
#                        - Fix crash when starting server from idle halfd
#
#   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 'getmodels'
#
#   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
#                        - 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 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 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 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
#                          (getmodels=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 {
		if [cequal $fd stdout] { 
			if [cequal $type TEXT] {
				puts $fd $data
			} else {
				puts $fd $kl
			}

			return
		}
		set fds $fd
	}

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

		foreach var "auth text api access" {
			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 #1
				if ![regexp $exprs(chatmode) $data] { continue }
			} elseif [cequal $text 3] {
				# Chat mode #2
				if ![regexp $exprs(chatmode2) $data] { continue }
				set fc [csubstr $data 0 1]
				if ![cequal $fc L] continue
			}
		}

		# Change keyed-list to new API if so specified
		set mykl $kl
		if $api { newAPI mykl }

		writeSock $fd $mykl
	}
}

# Change the keyed-list format to something third-parties can understand
proc newAPI { klvar } {
	global config text

	upvar $klvar kl

	set sep $config(newapisep)

	set data ""
	set type [keylget kl type]

	foreach key [keylkeys kl] {
		if [cequal $key type] continue

		set val [keylget kl $key]
		if [cequal $key data] {
			switch $type {
				TEXT -
				ERR {
					if $config(newapiquotestext) {
						append data "$key$sep\"$val\"\n"
					} else {
						append data "$key$sep$val\n"
					}
				}

				MAPLIST {
					set myval [join $val $sep]
					append data "$key$sep$myval\n"
				}

				default {
					if [cequal $type CONFIG] {
						set getconfig 1
					} else {
						set getconfig 0
					}

					append data "data\n"
					if [catch { unkeyList $val $getconfig } unkey] {
						myEcho ERR "Error on unkeyList: $unkey"
						myEcho ERR "TYPE='$type', KEY='$key', VAL='$val', KL='$kl'"
					} else {
						append data $unkey
					}
				}
			}
		} else {
			append data "$key$sep$val\n"
		}
	}

	set data "type${sep}$type\n$data"

	set lines [regsub -all -- "\n" $data "\n" foo]

	# keep blank line between packets
	#set data [crange $data 0 end-1]

	# add 1 to 'lines' to accomodate blank line
	incr lines

	set data "lines${sep}$lines\n$data"

	set kl $data
}

proc unkeyList { kl getconfig } {
	global config

	set sep $config(newapisep)

	set data ""
	foreach key [keylkeys kl] {
		set sub ""
		if { ![cequal $key text] && !$getconfig } {
			foreach el [keylget kl $key] {
				if { [cequal $key users] && $config(newapiquotesname) } {
					append sub "\"$el\"$sep"
				} else {
					append sub "$el$sep"
				}
			}
		} else {
			set sub "\"[keylget kl $key]\"x"
		}

		append data "$key$sep[crange $sub 0 end-1]\n"
	}

	return $data
}

######################################################################
# 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
	global forked

	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 api  0
	keylset kl access  0
	keylset kl text 0

	fcntl $fd NOBUF

	if $config(fork) {
		if [catch {fork} pid] {
			myEcho ERR "Can't fork: $pid"
			close $fd
			return
		}

		if [catch getListenPort listenport] {
			myEcho ERR "Can't get listener: $listenport"
			close $fd
			return
		}

		if $pid {
			# Parent
			keylset kl pid $pid
			close $fd
			mySleep 0.5

			# connect to listener
			if [catch {connectToForked $listenport} fd] {
				myEcho DBUG "Setting fileevent on $fd"
				fileevent $fd readable readSock
			}
		} else {
			# Child
			foreach id [after info] { after cancel $id }
		}
	} else {
		if [cequal $plat unix] { fcntl $fd CLOEXEC }

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

	set conns($fd) $kl
}

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
######################################################################

######################################################################
# net code for forked process
proc getListenPort {} {
	global config forked

	set i $config(forkedport)

	while 1 {
		set rc [catch {socket -server acceptConnForked -myaddr localhost $i} fd]

		if $rc {
			set err $errorCode

			if [regexp EADDRINUSE $err] {
				myEcho DBUG "Port $i in use, trying next..."
				incr i
			} else {
				myEcho DBUG "Error '$fd' on port $i in use, trying next..."
				incr i
			}

			if { $i > 32767 } { set i 2048 }
		} else {
			break
		}
	}

	set forked(serverfd) $fd

	return $i
}


######################################################################
# 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 serverUp

	# 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

				# HLBP doesn't send 112 if it auto-restarts server...
				if !$serverUp {
					if [regexp -nocase -- "host_init" $msg] hlbpServerUp
				}
				
				# 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 0 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
}

proc hlbpResetConnection {} {
	global text after

	catch { after cancel $after(hlbpreset) }

	myEcho INFO $text(309)

	closeHlbp
	mySleep 1
	connectToHlbp
}
# 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
	keylget conns($fd) api  api 
	keylget conns($fd) access  access 

	if { !$auth || $init } {
		if !$init {
			# Switch doesn't evaluate patterns.  Bleh
			if { [cequal $data $config(usrauth)] || [cequal $data $config(newapiusrauth)] } {
				set auth 1

				if { [cequal $data $config(newapiusrauth)] && 
					![cequal $config(newapiusrauth) $config(usrauth)] } {
						set api 1
				}

				myEcho AUTH [format $text(18) $addr $port]
				incr stats(user)
			} elseif { [expr [cequal $data $config(admauth)] || [cequal $data $config(newapiauth)]] && ![cequal $data ""] } {
				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
					}
				}
				
				if [cequal $data $config(admauth)] {
					set auth 2
					myEcho AUTH [format $text(20) $addr $port]
				} else {
					set auth 2
					set api  1
					myEcho AUTH [format $text(310) $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
			keylset conns($fd) api  $api
		}

		# 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)

		if $api { newAPI kl }
		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 $api
	}
}

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

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

	switch -glob -- $data {

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

		CHAT2 {
			if [cequal $fd stdout] return
			if $fromhlbp return
			keylset conns($fd) text 3
			checkProtected $fd $auth
		}

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

		STATS { 
			dumpStats 1 $fd $auth $fromhlbp
		}

		NOTEXT { 
			if [cequal $fd stdout] return
			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 {
				if $api { newAPI kl }
				writeSock $fd $kl
			}
		}

		MAPLIST* {
			set pattern ""

			set mapList [getMaps]
			set idx [lsearch -exact $mapList restart]
			if ![cequal $idx -1] {
				set mapList [lreplace $mapList $idx $idx] 
			}

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

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

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

			if $fromhlbp {
				writeHlbp05 $maps MAPLIST $hlbptarget
			} else {
				if $api { newAPI kl }
				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 {
			if $vote(votedin) {
				set nextmap UNKNOWN
			} else {
				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 0 $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
			}
		}

		GPL {
			foreach el [GPL] {
				if $fromhlbp {
					writeHlbp05 $el GPL $hlbptarget
				} else {
					sendClient TEXT $el $fd
				}
			}
		}

		SERVERSTATUS {
			set kl ""
			keylset kl type SERVERSTATUS
			keylset kl up   $serverUp

			foreach myId "avgcpu avgfps avgplayers curcpu curfps curplayers curin curout curup curusers" {
				keylset kl [string toupper $myId] $stats($myId)
			}

			if $fromhlbp {
				writeHlbp05 $maps SERVERSTATUS $hlbptarget
			} else {
				if $api { newAPI kl }
				writeSock $fd $kl
			}
		}

		default {
			# Any other commands require authorization
			if { $auth < 2 } {
				myEcho WARN [format $text(23) $addr $port $data]
				if ![cequal $fd stdout] {
					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 plat
	global vote votes

	switch -glob -- $data {
		RECONNECT {
			if [cequal $plat windows] {
				hlbpResetConnection
			}
		}

		CONSOLEOFF {
			if [cequal $fd stdout] {
				myEcho INFO $text(305)
				doConsole text(305)
				doConsole text(306)
				set config(console) 0
			}
		}

		DUMPVARS { 
			set olddebug $config(debug)
			set config(debug) 1
			dumpVars
			set config(debug) $olddebug
		}

		DUMPSTATS {
			dumpStats
		}

		GETCONFIG* {
			set pattern ""
			if [catch { set pattern [lindex $data 1] } err] { return }

			if [cequal $pattern ""] {
				set names [array names config]
			} else {
				set names [array names config $pattern]
			}

			set kl ""
			foreach name [lsort $names] {
				keylset kl $name $config($name)
			}

			if [cequal $kl ""] {
				sendClient TEXT "No configuration parameters matched pattern '$pattern'." $fd
			} else {
				sendClient CONFIG $kl
			}
		}

		SETCONFIG* {
			set pattern ""
			if [catch { set pattern [lindex $data 1] } err] { return }

			if [cequal $pattern ""] {
				sendClient TEXT "Please specify a halfd.cfg variable to set!" $fd
				return
			} else {
				set names [array names config $pattern]
			}

			if [cequal $names ""] {
				sendClient TEXT "Unknown configuration variable: $pattern" $fd
				return
			}

			if { [llength $names] > 1 } {
				sendClient TEXT "Please specify only one variable! " $fd
				return
			}

			set mydata $data
			lvarpop mydata
			lvarpop mydata

			set config($names) $mydata

			myEcho INFO  "Set configuration $names=$mydata" $fd
			sendClient TEXT "Set configuration $names=$mydata" $fd

			if ![validateConfig] {
				sendClient TEXT "Error in configuration!  See halfd.log for details." $fd
			}
		}

		REFRESH { getServerStatus 1 1 }

		RELOAD { loadPlugins }

		PATCH { loadPatchFile }

		CYCLE { cycleLogFile }

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

		STOPVOTE  { endVoting }

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

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

		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(294)
			say $text(294)
			set config(attackcheck) 1
		}

		ATTACKOFF  { 
			myEcho INFO $text(295)
			say $text(295)
			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 ![isDigit $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 ![isDigit $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 }

		TAUNTSON  { set config(taunts) 1 }
		TAUNTSOFF { set config(taunts) 0 }

		HBON  { set config(halfbot) 1 }
		HBOFF { set config(halfbot) 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 [isDigit $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
		}

		ARGS*  {
			set args ""
			if [catch { set args [lrange $data 1 end] } err] { return }

			set msg $text(316)
			myEcho INFO $msg
			sendClient TEXT $msg $fd

			set msg [format $text(317) $config(args)]
			myEcho INFO $msg
			sendClient TEXT $msg $fd

			set config(args) $args

			set msg [format $text(318) $config(args)]
			myEcho INFO $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'"
					set vote(votewon) 1
					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 userid {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 {
				adminPsay $line $userid
			}
		}
	} else {
		if $fromhlbp {
			writeHlbp05 $id NOPENALTIES $hlbptarget
		} elseif ![cequal $fd ""] {
			sendClient TEXT [format $text(243) $id] $fd
		} else {
			adminPsay [format $text(243) $myId] $userid
		}
	}
}

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 }
		signal trap   SIGTERM { gotSignal SIGTERM }
		signal trap   SIGQUIT { gotSignal SIGQUIT }
	}
}

proc gotSignal { signal } {
	global text args

	catch { myEcho WARN [format $text(26) $signal] }
	catch {
		if $args(killhlds) {
			myEcho WARN [format $text(322) $signal]
			stopServer
		}
	}

	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 config

	myEcho DBUG "Changing to $map..."

	if $config(timelimit1) {
		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 config after

	if $config(hlbpreset) {
		# Set a timer to bounce HLBP connection
		set timer [expr $config(hlbpreset) * 1000]
		set after(hlbpreset) [after $timer hlbpResetConnection]
		myEcho INFO [format $text(308) $timer]
	}

	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 args env

	if [catch { open $myhldir/$gameId/addons/halfd/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
		}
	}

	defaultConfig

	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" }
	}

	# Arguments always override halfd.cfg
	if { $config(debug) < $args(debug) } {
		set config(debug) $args(debug)
	}

	if { $config(console) < $args(console) } {
		set config(console) $args(console)
	}

	# Fix newapisep - look for 0x*, this will be the actual character
	if [cequal [csubstr $config(newapisep) 0 2] 0x] {
		if [catch { format %c $config(newapisep) } sep] {
			myEcho ERR [format $text(311) $sep]
			myEcho ERR $text(312)
			set config(newapisep) [format %c 0x09]
		} else {
			set config(newapisep) $sep
		}
	}

	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

	# Process 'envvars'
	if ![cequal $config(envvars) ""] {
		foreach envvar $config(envvars) {
			lassign [split $envvar =] var value
			set env($var) $value
			myEcho INFO [format $text(345) $var $value]
		}
	}
}

proc defaultConfig {} {
	global config

	# Clear the array
	foreach id [array names config] { 
		if [cequal $id admininstalled] continue
		if [cequal $id amxinstalled] 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(statsinbg) 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(newapiauth)     ""	;# ADMINISTRATOR password for NEWAPI
	set config(newapiusrauth)  ""	;# USER  password for NEWAPI
	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(fortuneargs) "-s"

	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(maxbots) 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(sayquotes) 0
	set config(saysleep) 0
	set config(saymax)   63	;# Max amount of data 'saycmd' allows

	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(halfbotauth)		0

	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(broadcastpen) 	1

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

	set config(checkserver) 	30

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

	set config(rsattempts)		10

	set config(oldeventhandler)	0

	set config(hlbpreset)		0

	set config(initmap)			""

	set config(newapisep)      "\t"	;# Separator for new API
	set config(newapiquotesname)   0 ;# Wrap names with quotes
	set config(newapiquotestext)   0 ;# Wrap text with quotes?

	set config(taunts)   1 ;# Allows TAUNTSON and TAUNTSOFF commands

	set config(dodhack)  0 ;# Ignore second DoD chat message

	set config(hostagecheck)		0
	set config(hostagedecay)		600
	set config(hostagecounts)		"3 6"
	set config(hostageargs)			"0 10"
	set config(hostagepenalty)		"kick banid"
	set config(hostagewarnmsg)      "%P has %C hostage-kill-warnings left before %T."
	set config(hostagepenmsg)		"%P gets %T for killing hostages!"

	set config(hostageroundlimit)   0
	set config(hostageroundpenalty) 1

	set config(killszget)	0

	set config(ignoretkta) ""
	set config(votewinmsg) ""
	set config(voteignore2) 1

	set config(getteamfromuser) 0

	set config(fork) 0
	set config(forkport) 9000

	set config(envvars) ""

	set config(archivecore) 0

	set config(getstats) 1
	set config(maxsamples) 60

	set config(timelimit1) 1
}


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 amxinstalled
	lappend bools attackcheck
	lappend bools halfbot
	lappend bools plugins
	lappend bools allowsource
	lappend bools quiet
	lappend bools broadcastwarn
	lappend bools broadcastpen
	lappend bools getdeaths
	lappend bools senddeaths
	lappend bools getmodelsfromrole
	lappend bools hlds_boost
	lappend bools oldeventhandler
	lappend bools dodhack
	lappend bools statsinbg
	lappend bools hostagecheck
	lappend bools killszget
	lappend bools fork
	lappend bools archivecore
	lappend bools halfbotauth
	lappend bools getstats
	lappend bools timelimit1
	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
	lappend ints hlbpreset
	lappend ints maxsamples
	lappend ints saymax
	foreach int $ints {
		if ![isDigit $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
	lappend listints hostagecounts
	lappend listints hostageargs
	foreach listint $listints {
		foreach int $config($listint) {
			if ![isDigit $int] {
				# Handle negative integers
				if { ![cequal [csubstr $int 0 1] -] || ![isDigit [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 for hlds_boost has been deprecated!  Instead, set"
		myEcho ERR "'hlds_boost=1' in halfd.cfg"
		set hlds_run ""
		set hlds_boost 1
		set validated 0
	}

	# getteamfromuser
	if { $config(getteamfromuser) && [expr !$config(getmodels) || $config(getmodelsfromrole)] } {
		myEcho ERR [format $text(323) getteamfromuser "getmodels=1 getmodelsfromrole=0"]
		myEcho ERR [format $text(324) "getmodels=$config(getmodels) and getmodelsfromrole=$config(getmodelsfromrole)"]
		myEcho ERR [format $text(325) getteamfromuser]
		set config(getteamfromuser) 0
		set validated 0
	}

	# regexps
	if [catch { regexp $config(ignoretkta) "" } err] {
		myEcho ERR "Error in regular expression \"$config(ignoretkta)\": $err"
		myEcho ERR "Disabling ignoretkta..."
		set config(ignoretkta) ""
		set validated 0
	}

	# password uniqueness
	set auths "usrauth admauth newapiauth newapiusrauth" 
	foreach auth $auths {
		foreach auth2 $auths {
			if { ![cequal $config($auth) ""] && ![cequal $auth $auth2] } {
				if [cequal $config($auth) $config($auth2)] {
					myEcho ERR [format $text(348) $auth $auth2]
					myEcho ERR $text(349)
					set validated 0
				}
			}
		}
	}

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

	return $validated
}


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/addons/halfd/$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
	set hlds_run [determineHLDS]
	myEcho INFO $text(32)
	myEcho INFO "./$hlds_run -game $gameId -port $config(port) $myArgs"

	if [catch {exec \
		./$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/addons/halfd/hlds_l.log.rma w]
}

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

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

	# TEST
	if ![cequal $config(initmap) ""] {
		myEcho DBUG "Initializing map to $config(initmap)"
		set after(initmap) [after 5000 { execCmd "changelevel $config(initmap)" }]
	}

	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

	if { $config(admininstalled) || $config(amxinstalled) } {
		# Save off configured 'saycmd'
		set mySayCmd $config(saycmd)
		if $config(admininstalled) {
			set config(saycmd) "admin_command admin_psay"
		} else {
			set config(saycmd) "amx_psay"
		}

		# Say it using admin_psay
		say $saytext $userid

		# Restore configured 'saycmd'
		set config(saycmd) $mySayCmd
	} else {
		say $saytext $userid
	}

	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 than 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"] {
			if $config(sayquotes) {
				set cmd "$config(saycmd) $userid \"$saytext\""
			} else {
				set cmd "$config(saycmd) $userid $saytext"
			}
		} else {
			if $config(sayquotes) {
				set cmd "$config(saycmd) \"$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

	if $voteclosed { catch { after cancel $after(tally) }}

	# 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] || [cequal $wonid $config(hltvwonid)] } {
		#	if [cequal [lindex $userInfo(ips) $i] 127.0.0.1] {
				myEcho DBUG "Not counting wonid $wonid ([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] || [cequal $wonid $config(hltvwonid)] } {
			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
		}

		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]

			if ![cequal $config(votewinmsg) ""] {
				say $config(votewinmsg)
				myEcho DBUG $config(votewinmsg)
			}
			set vote(votewon) 1

			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 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) }

		# Check to see if they voted again for the same map
		# if so, don't announce anything (damn llamas)
		if { $config(voteignore2) && [info exists votes($uid)] } {
			if [cequal $votes($uid) $map] {
				myEcho DBUG "Llama '$name<$uid>' voted again for $map.  Ignoring vote."
				adminPsay $text(320) $uid
				return $gotVote
			}
		}


		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
			}
		}

		# 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 $svrInfo(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 && !$vote(votedin) } {
			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 {
		foreach el [trimMapList $maplist] {
			say $el
			mySleep 1
		}
	}
}

proc trimMapList { maplist } {
	global config

	set mymaplist ""

	# Trim maplist if we need to
	if { [clength $maplist] <= $config(saymax) } {
		# no trim required
		set mymaplist [list $maplist]
	} else {
		# need to trim
		if [catch {
		while 1 {
			set mylist ""
			while 1 {
				if { [clength $mylist] > $config(saymax) ||
					 [cequal $maplist ""] } { break }
				set mymap [lvarpop maplist]
				lappend mylist $mymap
			}

			if { [clength $mylist] > $config(saymax) } {
				# Delete last map and push it back
				set mylist [lrange $mylist 0 [expr [llength $mylist] - 2]]
				lvarpush maplist $mymap
			}

			lappend mymaplist $mylist

			if [cequal $maplist ""] break
		}
		} err] {
			myEcho WARN "Error processing maplist: $err"
		}
	}

	return $mymaplist
}

# 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 enterwait idlecache logline statslog
	global votewins

	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/addons/halfd/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) {
						if ![cequal $wonid $config(hltvwonid)] {
							myEcho DBUG "updated uid $uid idlecache (entered the game)"
							set idlecache($uid) [clock seconds]
							assignTeam $uid IDLE 1
						} else {
							catch { unset idlecache($uid) }
						}
					} 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)]
					if ![cequal $wonid $config(hltvwonid)] {
						myEcho DBUG "added uid $uid to idlecache: [clock seconds]"
						set idlecache($uid) [clock seconds]
					} else {
						catch { unset idlecache($uid) }
					}
				} 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 $config(killszget) {
		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)'"

		set myVer [lindex [split $svrInfo(version) /] 1]
		if [cequal $myVer "3.1.1.0"] { 
			if $config(getstats) {
				myEcho WARN "Found version $myVer -- forcing getstats=0"
				set $config(getstats) 0
			}
		}

		if ![cequal $svrInfo(logFile) ""] {
			set statslog $svrInfo(logFile)
		}
		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
			}
		}

		# check for winning adminmod voting session -- add it to stats
		if [regexp -- $exprs(adminvote) $data foo map] {
			myEcho DBUG "Counted adminmod vote win for '$map'"

			if ![cequal $config(votewinmsg) ""] {
				say $config(votewinmsg)
				myEcho DBUG $config(votewinmsg)
			}

			set vote(votewon) 1

			incr stats(votewins)

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

	# 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 [isGame tfc] {
				set fname2 $myhldir/foxbot/tfc/waypoints/$mapname.fwp
			} else {
				set fname2 /proc/fileshouldnotexist
			}

myEcho DBUG "checking for existence of $fname && $fname2"
myEcho DBUG "result: expr { ![file exists $fname] && ![file exists $fname2] }"

			if { ![file exists $fname] && ![file exists $fname2] } {
				myEcho DBUG "No wpt found for $mapname.  Setting max_bots to $config(maxbots)."
				set after(maxbots) [after 10000 { execCmd "bot \"max_bots $config(maxbots)\"" } ]
			} 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
	}

	# Check for hostage kills
	if $config(hostagecheck) {
		if [regexp -- $exprs(hostage) $data foo kilinfo] {
			lassign [parsePlayerInfo $kilinfo] killer kilid kilwonid kilteam
			hostageKilled $killer $kilid $kilwonid $kilteam
		}
	}

	return $needRefresh
}

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

	# 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) } }
	}

	# Blow away death counts
	foreach id [array names deaths] { unset deaths($id) }

	# Blow away roles
	foreach id [array names role] { unset role($id) }

	resetUserInfo

	sendClient UPDATE [buildClientUpdate]

	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 stats if there were kills during the match
		if { $svrInfo(kills) >= $myKills || $config(alwaysrunstats) } {
			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
	myEcho DBUG "Checking deleteLog on '$svrInfo(logFile)'"
	myEcho DBUG "# of kills is '$svrInfo(kills)'"
	if { $myKills && [expr $svrInfo(kills) < $myKills] && \
		![cequal $svrInfo(logFile) ""] } {

		if [catch { unlink $svrInfo(logFile) } err] {
			myEcho DBUG "Logfile delete failed on '$svrInfo(logFile)': $err"
			if [catch { unlink $gameId/$svrInfo(logFile) } err] {
				myEcho ERR "Logfile delete failed on '$gameId/$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 [format $text(360) Adminmod $value]
			set config(admininstalled) 1
		}
	}

	if [cequal $var amx_version] {
		myEcho INFO [format $text(360) "AMX mod" $value]
		set config(amxinstalled) 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

	set rc [regsub -all -- "%L" $config(statsargs) $statslog args]
	if { !$rc && [cequal $statslog INIT] } return

	set cmd "$config(statspath) $args"

	if $config(statsinbg) {
		catch {system $cmd >> $myhldir/$gameId/addons/halfd/stats.out 2>&1 &} err
	} else {
		catch {system $cmd >> $myhldir/$gameId/addons/halfd/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 badTeams

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

	if ![info exists badTeams] { set badTeams "" }

	set myteaml [string tolower $myteam]
	if ![catch { set teammap($myteaml) } err] {
		set myteam $teammap($myteaml)
	} else {
		if [cequal [lsearch -exact $badTeams $myteaml] -1] {
			myEcho WARN "Team '$myteaml' is not in teammap file"
			lappend badTeams $myteaml
		}
	}

	return $myteam
}

proc assignTeam { uid myteam { fromuser 0 } } {
	global team teammap config

	if { $config(getteamfromuser) && !$fromuser } return

	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
	}

	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
	if $config(taunts) {
		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) $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
		if $config(taunts) {
			foreach weaparr [array names config weap.*] {
				set weapname [lindex [split $weaparr .] 1]
				if [cequal $weapon $weapname] {
					regsub -all "\%P" $config($weaparr) $ename mymsg
					regsub -all "\%K" $mymsg $rname 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
	}

	if { [isGame dod] && $config(dodhack) } {
		# DoD does some sort of weird double-logging of chat messages.
		# This is an attempt to ignore the second chat...
		set first5 [csubstr $team 0 5]
		if { [cequal $first5 "axis-"] || [cequal $first5 "allie"] } {
			if { $config(debug) > 1 } {
				myEcho DBUG "Ignored 2nd DoD chat-msg: '$pinfo', '$chattype', '$said'"
			}
			return
		}
	}

	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)] } {
			if $vote(votedin) {
				say $text(319)
			} else {
				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"
		if $config(admininstalled) { set last(mypenalty) 0 }
		if $config(amxinstalled) { set last(mypenalty) 0 }
		# 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 $uid
			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 [regexp $exprs(forgivetk) $said] {
			foundTKForgive $name $uid
			return
		}
	}

	# TA-forgive
	if { $config(attackcheck) && $config(attackforgive) } {
		if [regexp $exprs(forgiveta) $said] {
			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 svrInfo

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

	myEcho ERR [format $text(313) $svrInfo(map)]

	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]
		if ![cequal $svrInfo(map) ""] {
			lappend emailtxt [format $text(313) $svrInfo(map)]
		}
		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 w} fd] {
				myEcho ERR "Couldn't open $gdb_fname: $fd"
			} else {
				puts $fd bt
				puts $fd "info locals"
				puts $fd "info sharedlibrary"
				puts $fd "info frame"
				puts $fd quit
				close $fd
			}
		#}

		if [catch {
			set hlds_run [determineHLDS]
			exec gdb $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
		if !$config(archivecore) {
			catch {unlink core}
		} else {
			catch {file copy -force core crashes/$now.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 {
		set timer 25000
		if $config(oldeventhandler) { set timer 2500 }
		if $reschedule { set after(pl) [after $timer {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 }

	# Set an event
	if !$config(oldeventhandler) {
		if [cequal $plat unix] logFileEventHandler
	}

	return $i 
}

# unix-only event handler for reading from hlds_l.log file
# (windows uses hlbp socket)
proc logFileEventHandler {} {
	global fds config after

	set fd $fds(logfd)

	if [catch { gets $fd } data] {
		myEcho ERR "Error reading from server: $data"
		return
	}

	if [catch { eof $fd } err] {
		myEcho ERR "Error checking EOF on server: $err"
		return
	}

	if $err {
		# We got EOF - disable handler and reschedule in .1 seconds...
		fileevent $fd readable ""
		set after(logevent) [after 100 [list fileevent $fd readable logFileEventHandler]]
	} else {

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

		if ![cequal $data ""] { processLogLine data }
		processLogFile 1
	}
}

# 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/addons/halfd/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/addons/halfd/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 hostageKilled { killer kilid kilwonid kilteam } {
	global text svrInfo config roundstart hostagecount

	myEcho HSTG [format $text(315) $killer $kilid $kilwonid $kilteam]

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

	processPenalty hostage $kilid hostageTable hostagecount $increment
}

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 [lindex $uinfo 0]
			say [format $text(297) $killee $name]
			myEcho DBUG "$killee forgave TA against $name<$uid>"
			set forgave 1
			break
		}
	}

	if !$forgave { 
		adminPsay $text(296) $uid
	}
}

# 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 [lindex $uinfo 0]
			say [format $text(156) $killee $name]
			myEcho DBUG "$killee forgave TK against $name<$uid>"
			set forgave 1
			break
		}
	}

	if !$forgave { 
		adminPsay $text(155) $uid
	}
}

proc foundAttack { ruid rname euid ename rteam } {
	global roundstart config attackcache
	if ![cequal $config(ignoretkta) ""] {
		set ignore 0
		if [regexp $config(ignoretkta) $ename] { set ignore 1 }
		if [regexp $config(ignoretkta) $rname] { set ignore 1 }
		if $ignore {
			myEcho DBUG "Ignoring TR on $ename by $rname..."
			return
		}
	}
	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)"

	if ![cequal $config(ignoretkta) ""] {
		set ignore 0
		if [regexp $config(ignoretkta) $ename] { set ignore 1 }
		if [regexp $config(ignoretkta) $rname] { set ignore 1 }
		if $ignore {
			myEcho DBUG "Ignoring TK on $ename by $rname..."
			return
		}
	}

	# 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) || $config(amxinstalled) } {
					if $config(broadcastwarn) {
						say $mymsg
					} else {
						adminPsay $mymsg $wonid
					}
				} 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

			if { $config(admininstalled) || $config(amxinstalled) } {
				if $config(broadcastpen) {
					say $mymsg
				} else {
					adminPsay $mymsg $wonid
				}
			} else {
				say $mymsg
			}

			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 }

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 [info exists exclude($wonid)] {
			# 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
				if $config(admininstalled) {
					set cmd "admin_command $myaction"
				} else {
					set cmd "$myaction"
				}
			} else { 
				# It doesn't exist, use userid as the argument
				if $config(admininstalled) {
					set cmd "admin_command $action $userid"
				} else {
					set cmd "$action $userid"
				}
			}
			execCmd $cmd
			myEcho DBUG "executed '$cmd'"
		}
	}
}

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"

	# 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 $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 ![isDigit $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 [isDigit $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 timelimit [getServerVariable mp_timelimit]
	if [cequal $timelimit ""] { set timelimit 0 }
	if [catch {expr int( $timelimit )} 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 mapCycleList

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

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

	if $vote(votewon) {
		set vote(votedin) 1
	} else {
		readMapCycle
		if [cequal [lsearch -exact $mapCycleList $svrInfo(map)] -1] {
			set vote(votedin) 1
		} else {
			set vote(votedin) 0
		}

	}
	set vote(votewon) 0


	set nullMapCnt 0

	# Should we run custom config file?
	if $config(cm) {
		# Run custom config file if it's there...
		set cfgfile $myhldir/$gameId/addons/halfd/$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/addons/halfd/$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)]
					if { $config(nextmap) > 0 && !$vote(votedin) } {
						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

	if ![cequal $config(serverMsg) ""] {
		say \"$config(serverMsg)\"
	}
}

proc sayFortune {} {
	global svrInfo config

	set args $config(fortuneargs)

	if [catch { exec $config(fortunepath) $args } 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 [regexp $id $data] { 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 stats

	if [cequal $plat windows] { return 0 }

	# Try three times.  If we don't get a response then assume the
	# server is hung.
	#
	# NB: don't increase the number of attempts, too many writes
	# to the named pipe with no reader will cause halfd to crash.
	loop i 1 4 {
		execCmd hostname

		mySleep [expr 10 * $i]

		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 we got a response bail out of the loop early
		if ![cequal $data ""] break
	}

	# 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]]

		incr stats(hung)

		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/addons/halfd/halfd.cfg$cfgPostfix
			} elseif [cequal $type halfbot] {
				set fname $myhldir/$gameId/addons/halfd/halfbot.cfg
			} else {
				set fname $myhldir/$gameId/addons/halfd/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/addons/halfd/halfd.cfg$cfgPostfix
			} elseif [cequal $type halfbot] {
				set fname $myhldir/$gameId/addons/halfd/halfbot.cfg
			} else {
				set fname $myhldir/$gameId/addons/halfd/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/addons/halfd/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
		if $config(getstats) getServerStats
	} else {
		mySleep 5

		# Only check for hung servers on unix
		if [cequal $plat unix] {
			if ![info exists nullMapCnt] { set nullMapCnt 0 }
myEcho DBUG "MAP IS NULL (count=$nullMapCnt / $config(nullmap))"
			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 getAveragePing {} {
	global userInfo

	set i 0
	set tot 0
	set cnt 0
	foreach ping $userInfo(pings) {
		if { ![cequal [lindex $userInfo(wonid) $i] 0] && [expr $ping > 0] } {
			incr tot $ping
			incr cnt
		}
		incr i
	}

	if { $cnt > 0 } {
		set cnt [format %.2f $cnt]
		set avgping [format %.2f [expr $tot / $cnt]]
	} else {
		set avgping 0
	}
myEcho DBUG "avgping = $avgping, cnt = $cnt"

	return $avgping
}

proc getServerStats {} {
	global stats config text

	execCmd stats
	mySleep 0.1

	# Jump to the header line - "CPU In Out Uptime Users FPS Players"
	getLog ^CPU

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

	lassign $data cpu in out uptime users fps players

	set stats(curcpu) $cpu
	set stats(curin) $in
	set stats(curout) $in
	set stats(curup) $uptime
	set stats(curusers) $users
	set stats(curfps) $fps
	set stats(curplayers) $players

	lappend stats(sampcpu) $cpu
	lappend stats(sampfps) $fps
	lappend stats(sampplayers) $players

	if ![info exists stats(sampping)] { set stats(sampping) "" }
	set avgping [getAveragePing]
	if ![cequal $avgping 0] { lappend stats(sampping) $avgping }

	set avgs "avgcpu avgfps avgplayers avgping"
	set i 0
	foreach id "sampcpu sampfps sampplayers sampping" {
		if { [llength $stats($id)] > $config(maxsamples) } {
			lvarpop stats($id)
		}

		set tot 0
		set cnt 0
		foreach val $stats($id) {
			set tot [expr $tot + $val]
			incr cnt
		}

		if { $cnt > 0 } {
			set cnt [format %.2f $cnt]
			set stats([lindex $avgs $i]) [format %.2f [expr $tot / $cnt]]
		} else {
			set stats([lindex $avgs $i]) 0
		}

		incr i
	}
}
	

proc parseStatusOutput { data } {
	global config exprs dbgnewstatus dbghltv

	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

		# Deal with HLTV weirdness, hltv in 1109 looks like this:
		# 3    "Name Of Proxy" 87 1448 hltv:1/128 delay:30 02:03 1.2.3.4:27021
		#
		if [cequal [string tolower [csubstr $frags 0 4]] hltv] {
			set frags 0
			set time $ping
			set ping 0
			set ip $foo
			if $config(debug) {
				if ![info exists dbghltv] {
					myEcho DBUG [replicate - 40]
					myEcho DBUG "Found HLTV status entry: '$data'"
					myEcho DBUG "Name   : $name"
					myEcho DBUG "Userid : $userid"
					myEcho DBUG "WONid  : $wonid"
					myEcho DBUG "Frags  : $frags"
					myEcho DBUG "Time   : $time"
					myEcho DBUG "Ping   : $ping"
					myEcho DBUG "IP     : $ip"
					set dbghltv 1
					myEcho DBUG [replicate - 40]
				}
			}
		}

		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 [isDigit $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 stats

	# 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)
	incr stats(playerconns)

	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) && ![cequal $config(pingmax) 0] } {
				lappend uids  [lindex $userInfo(userid) $i]
				lappend pings $ping
				lappend types max
			} elseif { $ping < $config(pingmin) && ![cequal $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 WARN "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

	if [cequal $wonid $config(hltvwonid)] {
		catch { unset idlecache($userid) }
		return
	}

	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) || $config(amxinstalled) } {
		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/addons/halfd/tmp/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} {forgive 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
				# If id != 0, this is a forgive, so don't reset scheduler
				if { [cequal $id 0] && ![cequal $after(tk) 0] } {
					catch { after cancel $after(tk) }
					tkTable sched
				} else {
					catch { after info $after(tk) } info
					myEcho DBUG "tkForgive - not resetting scheduler ($after(tk) / $info )"
				}
			} 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
				# If id != 0, this is a forgive, so don't reset scheduler
				if { [cequal $id 0] && ![cequal $after(attack) 0] } {
					catch { after cancel $after(attack) }
					attackTable sched
				} else {
					catch { after info $after(attack) } info
					myEcho DBUG "attackForgive - not resetting scheduler ($after(attack) / $info )"
				}
			} 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
		}
	}
}

proc hostageTable { op {id 0} {decayall 0}} {
	global hostagecount text config after

	switch -exact $op  {
		sched {
			# Only schedule if we have a decay configured
			if [cequal $config(hostagedecay) 0] return
			catch { after cancel $after(hostage) }
			if [catch {
					after [expr $config(hostagedecay) * 1000] hostageTable decay
				} rc] {
					myEcho ERR [format $text(119) $rc]
			} else {
				set after(hostage) $rc
			}
		}

		decay {
			if [decay hostagecount $id $decayall] { 
				suicideTable write
				catch { after cancel $after(hostage) }
				hostageTable sched
			} else {
				set after(hostage) 0
			}
		}

		default {
			if { [cequal $op write] && [cequal $after(hostage) 0] } {
				hostageTable sched
			}
			tableOp $op hostage hostagecount
		}
	}
}
#
# 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
	set mt 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.
		if $config(getteamfromuser) {
			set data [getLog "^model|^team" 0 1]
		} else {
			set data [getLog "^model" 0 1]
		}

		if { $config(debug) > 1 } {
			myEcho DBUG "getmodel data = '$data'"
		}

		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]
			# Replace the "-" for this player with the real model
			catch {
				set userInfo(model) \
				[lreplace $userInfo(model) $idx($i) $idx($i) $myModel]
			}
		} else {
			set myModel "-"
		}


		set uid [lindex $userInfo(userid) $idx($i)]

		if $config(getteamfromuser) {
			if [regexp $exprs(parseteam) $data foo myTeam] {
				set myTeam [string trim $myTeam]
				assignTeam $uid $myTeam 1
			} else {
				set myTeam "-"
			}
		}

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 ""
			}
			assignTeam $uid $myTeam
		}

		if [isGame gearbox] {
			# Set the team for this player based on the model
			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 -
			}
			assignTeam $uid $myTeam
		}
}

		if $config(getteamfromuser) {
			incr mt
			if { $mt > 1 } { 
				incr i 
				set mt 0	
			}
		} else {
			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)   ""
}

# Reset novolatile user info on a map change
proc resetUserInfo {} {
	global userInfo config

	myEcho DBUG "resetUserInfo is executing"

	set userInfo(pings) ""
	set userInfo(frags) ""
	set userInfo(times) ""
	set userInfo(model) ""
	set userInfo(team)  ""

	if $config(ipfromstats) { set userInfo(ips) "" }

	foreach x $userInfo(users) {
		foreach id "pings frags" {
			lappend userInfo($id) 0
		}

		foreach id "model team" {
			lappend userInfo($id) -
		}

		lappend userInfo(times) 00:00
		if $config(ipfromstats) { lappend userInfo(ips) CONNECTING }
	}
}

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 { $config(getdeaths) && [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
	global tclx_library halfdPatchLevel

	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]]

	# Support information
	if ![info exists tcl_patchLevel] {
		set tcl_patchLevel unknown
	}

	myEcho INFO $text(361)
	if [catch {
		myEcho INFO [format $text(362) $vnum]
		myEcho INFO [format $text(370) $halfdPatchLevel]
		myEcho INFO [format $text(363) $gameId]
		myEcho INFO [format $text(364) $tcl_version $tcl_patchLevel]
		myEcho INFO [format $text(365) $tclx_library]
		myEcho INFO [format $text(373) [determineCPU]]
		myEcho INFO $text(209)
		foreach key [array names tcl_platform] {
			myEcho INFO [format "% 14s = '%s'"  $key $tcl_platform($key)]
		}
	} err] {
		myEcho ERR [format $text(367) $err]
	}
	# TODO: my CPU determination
	myEcho INFO $text(366)

	# GPL
	myEcho INFO [format $text(326) $vnum]
	myEcho INFO $text(327)
	myEcho INFO $text(328)
	myEcho INFO $text(329)

	# myEcho INFO $text(3)
	# myEcho INFO $text(4)
	myEcho INFO $text(5)

	myEcho INFO [format $text(255) $tcl_version $tcl_patchLevel]
	myEcho INFO [format $text(153) $myhldir]

	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 newapiauth] { set cfg "***PROTECTED***" }
		if [cequal $key newapiusrauth] { 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\]
             \[-console \[<n>\]\] \[-k\]

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)

       -console    = Start halfd in console mode (at level <n>)

       -k          = Shutdown HLDS if halfd receives INT, TERM, or QUIT signal


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 gotConsoleInput {} {
	global config text serverUp gameId after

	myEcho DBUG "Console Input triggered"

	if [catch {gets stdin} data] {
		myEcho ERR [format $text(300) $data]
		myEcho ERR $text(301)

		# Delete the event - this will turn off input
		fileevent stdin readable ""

		return
	}

	if { [cequal $data ""] && $serverUp } return

	if !$serverUp {
		switch $data {
			stopd - 
			STOPD - 
			start - 
			START { 
				set msg [format $text(307) $gameId]
				doConsole msg
				processClientCommand console 0 [string toupper $data] stdout 2 0
			}

			default {
				set cmds "STOPD, START"
				set msg [format $text(302) $cmds]
				doConsole msg
			}
		}
	} else {
		processClientCommand console 0 $data stdout 2 0
	}

	if $config(console) {
		# Reset our event
		fileevent stdin readable gotConsoleInput

		catch [after cancel $after(pl)]
	} else {
		# Delete our event
		fileevent stdin readable ""
	}
}

proc doConsole { datavar } {
	global config text
	upvar $datavar data

	if $config(console) {
		if ![cequal [string trim $data] ""] { 
			if [catch {puts stdout $data} err] {
				myEcho ERR [format $text(298) $err]
				myEcho ERR $text(299)
				set config(console) 0
			}
		}
	}
}

proc loadPatchFile {} {
	global fds text halfdPatchLevel halfdSourceLevel
	global patchComments

	set patchFile halfd-patch.tcl

	if [file exists $patchFile] {
		# Make sure this is not an old patch
		set fd [open $patchFile r]
		set level [gets $fd]
		close $fd

		if [catch { eval $level } err] {
			myEcho ERR [format $text(374) $err]
			myEcho ERR $text(375)
			return
		}

		if { $halfdPatchLevel < $halfdSourceLevel } {
			myEcho WARN [format $text(374) [pwd]/halfd-]
			myEcho WARN $text(375)
			set halfdPatchLevel $halfdSourceLevel
			return
		}

		# Load the patch file
		set patchComments ""
		if [catch { source $patchFile } err] {
			myEcho ERR [format $text(368) [pwd]/$patchFile $err]
		} else {
			myEcho INFO [format $text(369) $patchFile $halfdPatchLevel]
			foreach el $patchComments {
				myEcho INFO $el
			}
		}
	} else {
		if [info exists fds(mylogfd)] {
			myEcho DBUG $text(371)
		}
	}
}

#######################################################################
# 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)
		}
	}

	# Remove slash on end of HLDIR if necessary
	if [cequal [csubstr $myhldir end end] /] {
		myEcho DBUG "Removing trailing slash from HLDIR"
		set myhldir [csubstr $myhldir 0 end]
	}

	# 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

	# Look for patch
	loadPatchFile

	# 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 ![file isdir $myhldir/$gameId/addons/halfd/tmp] {
		mkdir $myhldir/$gameId/addons/halfd/tmp
		myEcho DBUG [format $text(344) $myhldir/gameId/addons/halfd/tmp/]
	}


	if ![file exists $myhldir/$gameId/addons/halfd/$config(oldlogsdir)] {
		catch {file mkdir $myhldir/$gameId/addons/halfd/$config(oldlogsdir)}
		chmod 0777 $config(oldlogsdir)
	}

	if [cequal $plat unix] {
		# Check to see if halfd is already out there
		set fds(myPid) $myhldir/$gameId/addons/halfd/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/addons/halfd/hlds_l.pid$cfgPostfix
		set fds(cmd)   $myhldir/$gameId/addons/halfd/hlds_l.cmd$cfgPostfix
		set fds(log)   $myhldir/$gameId/addons/halfd/hlds_l.log$cfgPostfix
	}

	set fds(mylog) $myhldir/$gameId/addons/halfd/halfd.log$cfgPostfix
	set fds(adminlog) $myhldir/$gameId/addons/halfd/halfd.admin
	set fds(teammap)  $myhldir/$gameId/addons/halfd/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/addons/halfd/$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

	setExprs
	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) {
		myEcho INFO $text(303)
		puts stdout "\n\n"
		puts stdout $text(303)
		puts stdout $text(304)
		fileevent stdin readable gotConsoleInput
	}

	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 ""
		set auth ""
		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)]
					if $config(halfbotauth) {
						set autharray ${array}Auth
						set auth [set ${autharray}($trigger)]
					}

					break
				}
			}

			if ![cequal $response ""] break
		}

		if ![cequal $response ""] {
			if $config(halfbotauth) {
				if { ![cequal $auth ""] && [cequal [lsearch --exact $auth $wonid] -1] } {
					# Not authorized
					myEcho WARN "Ignoring: $name<$uid><$wonid><$team> not authorized for HalfBot '$trigger'"
				}
			}

			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

			# 'say' will catch %L case
			#regsub -all "%L" $response $said  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 $uid said
				}
			} else {
				execHalfBot $response $uid said
			}

			incr stats(halfbot)
		}
	}
}

proc execHalfBot { response uid theysaid } {

	upvar $theysaid said

	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'"
			}
		}

		"W:" { mySleep 0.5 }

		default { say $response }
	}
}

proc readHalfBot { {map ""} } {
	global config hbGlobal hbTriggers hbMap myhldir gameId text
	global hbGlobalAuth hbMapAuth

	if [cequal $map ""] {
		# Read the 'global' config
		foreach id [array names hbGlobal] { unset hbGlobal($id) }
		set fname $myhldir/$gameId/addons/halfd/halfbot.cfg
		set array hbGlobal
		set auth  hbGlobalAuth
		myEcho DBUG "Reading global HalfBot config."
	} else {
		# Read map-specific config
		foreach id [array names hbMap] { unset hbMap($id) }
		set fname $myhldir/$gameId/addons/halfd/halfbot_$map.cfg
		set array hbMap
		set auth  hbMapAuth
		if ![file exists $fname] {
			myEcho DBUG "No HalfBot file for $map."
			return
		}
		myEcho DBUG "Reading HalfBot config for $map."
	}

	readHalfBotDetail $fname $array $auth

	# 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 auth } {
	global $array $auth text config

	if [catch { open $fname r } fd] {
		myEcho ERR "Error: $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

		if $config(halfbotauth) {
			set leftover [lassign [split $key :] myKey myAuth]
			set ${array}($myKey) $val
			set ${auth}($myKey) $myAuth
		} else {
			set ${array}($key) $val
		}

		if { $config(debug) > 1 } {
			if $config(halfbotauth) {
				myEcho DBUG "HalfBot ($key) = ($val) -- auth = '$myAuth'"
			} else {
				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 { toconsole 1 } } {
	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 }

	if { $config(console) > 1 && $toconsole } {
		if [cequal $config(console) 1] {
			if [regexp $type "INFO|ERR|WARN"] {
				doConsole msg
			}
		} else {
			# console > 1, do everything
			doConsole 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/addons/halfd/$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 config(getdeaths)] { return }

	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)]

	lappend txt [format $text(314) $stats(playerconns)]


	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)]
	lappend txt [format $text(346) $stats(hung)]

	# Memory usage
	lappend txt [format $text(252) $stats(globalvars)]

	# Halfbot
	if { $stats(halfbot) > 0 } {
		lappend txt [format $text(254) $stats(halfbot)]
	}

	# Server Stats
	if $config(getstats) {
		lappend txt [format $text(349) $stats(curcpu)]
		lappend txt [format $text(350) $stats(curin)]
		lappend txt [format $text(351) $stats(curout)]
		lappend txt [format $text(352) $stats(curup)]
		lappend txt [format $text(353) $stats(curusers)]
		lappend txt [format $text(354) $stats(curfps)]
		lappend txt [format $text(355) $stats(curplayers)]

		lappend txt [format $text(356) $stats(avgcpu)]
		lappend txt [format $text(357) $stats(avgfps)]
		lappend txt [format $text(358) $stats(avgplayers)]
		lappend txt [format $text(372) $stats(avgping)]
	}

	# 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
			keylget conns($sock) api  api 

			lappend txt "### \[$sock\] - $addr:$port (auth=$auth) (api=$api)"
		}
	}

	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
	set stats(hung)  0     ;# How many times we hung

	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
	set stats(playerconns) 0

	foreach myId "avgcpu avgfps avgplayers curcpu curfps curplayers curin curout curup curusers avgping" {
		set stats($myId) 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 statslog env 
	global halfdPatchLevel halfdSourceLevel

	set halfdPatchLevel 0.0
	set halfdSourceLevel 0.0

	if [catch {random seed [clock seconds]} err] {
		random seed [pid]
	}

	clearUserInfo

	set vote(insession) 0
	set vote(votewon) 0
	set vote(votedin) 0

	set serverUp 0

	foreach id "name ip port map logFile" { set svrInfo($id) "" }
	foreach id "mapstart kills player max"  { 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
	set config(amxinstalled) 0

	# PENALTIES
	# Add penalties here.  You also need to create a "<penalty>Table" function.
	set penaltyList "tk weapon foul ping attack suicide team hostage"

	# Don't mess with this :-)
	set penaltyFuncList ""
	set penaltyArrays ""
	foreach pen $penaltyList {
		lappend penaltyFuncList "${pen}Table"
		lappend penaltyArrays   "${pen}count"
		set after($pen) 0
	}

	set statslog "INIT"

	if [cequal $plat unix] {
		if [info exists env(LD_LIBRARY_PATH)] {
			set env(LD_LIBRARY_PATH) ".:$env(LD_LIBRARY_PATH)"
		} else {
			set env(LD_LIBRARY_PATH) "."
		}
	}
}

# 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 ![isDigit $seconds] {
			# Assume floating-point
			set dontprocess 1
			select {} {} {} $seconds
			set dontprocess 0
		} 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 text

	set args(debug) 	0 
	set args(gameid)	$defaultgame 
	set args(extension)	"" 
	set args(loadobj)	"" 
	set args(console) 	0 
	set args(killhlds) 	0 
	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 ![isDigit $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
				}

				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
			}

			-console {
				# Console mode
				set arg [lindex $argv [expr $i + 1]]

				if [isDigit $arg] {
					incr i
				} else {
					set arg 1
				}

				set args(console) $arg
			}

			-k { 
				myEcho INFO $text(321)
				set args(killhlds) 1 
			}


			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 determineCPU {} {
	global text

	set cpu i486

	if [file readable /proc/cpuinfo] {
		if [catch { exec grep "cpu family" /proc/cpuinfo } cpufamily] {
			myEcho WARN "Error determining CPU: $cpufamily"
		} elseif [catch { exec grep "model name" /proc/cpuinfo} modelname] {
			myEcho WARN "Error determining CPU: $modelname"
		} else {
			set famnum [string trim [lindex [split $cpufamily :] 1]]
			if ![isDigit $famnum] {
				myEcho WARN "Non-numeric cpu family: $cpufamily"
			} else {
				if { [regexp -nocase "AMD" $modelname] && [expr $famnum > 5 ] } {
					set cpu AMD
				} elseif { $famnum < 6 } {
					set cpu i486
				} else {
					set cpu i686
				}
			}
		}
	} else {
		if [catch { exec uname } uname] {
			myEcho WARN "Error executing uname: $uname"
		} else {
			if [regexp -nocase freebsd $uname] {
				if [catch { exec grep "CPU:" /var/run/dmesg.boot } bsdcpu] {
					myEcho WARN "Error determining CPU: $bsdcpu"
				} elseif [regexp AMD $bsdcpu] {
					set cpu AMD
				} elseif [regexp 686 $bsdcpu] {
					set cpu i686
				} 
			}
		}
	}

	return $cpu
}

proc determineHLDS {} {
	global text config

	if ![cequal $config(hlds_run) ""] { return $config(hlds_run) }

	set cpu [determineCPU]

	if [cequal $cpu i686] {
		set hlds hlds_i686
	} elseif [cequal $cpu AMD] {
		set hlds hlds_amd
	} else {
		set hlds hlds_i486
	}

	if ![file executable $hlds] {
		myEcho WARN "Couldn't find executable $hlds ... using 'hlds' binary instead"
		set hlds hlds
	}

	myEcho INFO [format $text(359) $hlds]

	return $hlds
}

proc GPL {} {
	global text

	loop i 330 344 {
		lappend gpl_list $text($i)
	}

	return $gpl_list
}

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)]
	}
}

# 'ctype' sometimes blows up on newer versions of Tcl.  How nice.
# I only use it for 'digit', so re-implementing in pure tcl should
# solve the problem.
proc isDigit { data } {
	set digits "0 1 2 3 4 5 6 7 8 9"
	foreach el [split $data ""] {
		if { [lsearch -exact $digits $el] == -1 } { return 0 }
	}

	return 1
}

# Set up all regular expressions in one place so that I can keep myself
# sane ;)
proc setExprs {} {
	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](\[^:\]+): *(.*)"

	set exprs(adminvote) "^L.*Changing map to (.+) due to vote\.$"

	# (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 (may be null)
	#	4 = team
	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]|^\<|admin_chat|\[ADMIN\] \(Admin\)|admin_?say|amx_chat"
	set exprs(chatmode2) " say | say_team "

	# '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: (.*)"

	# hostage kill in CS
	set exprs(hostage) "^L.*\"(.+)\" *triggered *\"Killed_A_Hostage\""

	# Forgive-ness
	set exprs(forgivetk) "^forgive tk|^!forgivetk|^forgivetk|^tkforgive|!^!tkforgive"
	set exprs(forgiveta) "^forgive ta|^!forgiveta|^forgiveta|^taforgive|!^!taforgive"
}

#######################################################################
# set some variables for use in the procs and call MAIN
#
checkForExtendedTcl
initVars

#set fd [open dbg w]
#cmdtrace on $fd 

set vnum 2.20b15
set defaultgame valve

if ![cequal [string first cmddebug $argv] -1] commandLineDebug

main [llength $argv] $argv
