Skip to content

Instantly share code, notes, and snippets.

@EdGruberman
Created December 10, 2020 16:01
Show Gist options
  • Select an option

  • Save EdGruberman/b92d15cee0e6845546a4f5850a82e7a8 to your computer and use it in GitHub Desktop.

Select an option

Save EdGruberman/b92d15cee0e6845546a4f5850a82e7a8 to your computer and use it in GitHub Desktop.

Revisions

  1. EdGruberman created this gist Dec 10, 2020.
    1,104 changes: 1,104 additions & 0 deletions sentinel.mrc
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,1104 @@
    on *:START:{ sentinel.load }
    on *:EXIT:{ sentinel.unload }
    on *:CONNECT:{ if ( $hget(sentinel.general,autoactivate) == 1 && $hget(sentinel.general.status,activated) != 1 ) sentinel.activate }
    alias sentinel.load {
    ;--Load settings from ini file into hash tables
    hmake sentinel.irc 5
    hload -i sentinel.irc sentinel.ini irc
    if ( $hget(sentinel.irc,admin) == $null ) hadd sentinel.irc admin #clan.private password
    if ( $hget(sentinel.irc,tv) == $null ) hadd sentinel.irc tv #clan.tv
    if ( $hget(sentinel.irc,monitor) == $null ) hadd sentinel.irc monitor #clan

    hmake sentinel.general 5
    hload -i sentinel.general sentinel.ini general
    hadd sentinel.general version v0.5.0b - 2008/06/11 by EdGruberman
    hadd sentinel.general url http://sentinel.rjump.com
    if ( $hget(sentinel.general,port.type) == $null ) hadd sentinel.general port.type dynamic
    if ( $hget(sentinel.general,port.log) == $null ) hadd sentinel.general port.log 49889
    if ( $hget(sentinel.general,show.connects) == $null ) hadd sentinel.general show.connects 1
    if ( $hget(sentinel.general,show.sayteam) == $null ) hadd sentinel.general show.sayteam 1
    .fullname Sentinel Bot $hget(sentinel.general,version)

    hmake sentinel.server 5
    hload -i sentinel.server sentinel.ini server
    if ( $hget(sentinel.server,address) == $null ) hadd sentinel.server address 127.0.0.1
    if ( $hget(sentinel.server,rcon_password) == $null ) hadd sentinel.server rcon_password password

    hmake sentinel.log 5
    hload -i sentinel.log sentinel.ini log

    hmake sentinel.teams 20
    hload -i sentinel.teams sentinel.ini teams
    if ( $hget(sentinel.teams,0).item == 0 ) {
    hadd sentinel.teams Blue 12
    hadd sentinel.teams Red 04
    hadd sentinel.teams Green 03
    hadd sentinel.teams Yellow 08
    hadd sentinel.teams Spectator 14
    hadd sentinel.teams Console 07
    hadd sentinel.teams Unassigned 15
    }

    hmake sentinel.rules.default 5
    hload -i sentinel.rules.default sentinel.ini rules.default
    if ( $hget(sentinel.rules.default,players) == $null ) hadd sentinel.rules.default players $ctime Default 8
    if ( $hget(sentinel.rules.default,max) == $null ) hadd sentinel.rules.default max $ctime Default 900
    if ( $hget(sentinel.rules.default,min) == $null ) hadd sentinel.rules.default min $ctime Default 300
    if ( $hget(sentinel.rules.default,type) == $null ) hadd sentinel.rules.default type $ctime Default ignore
    if ( $hget(sentinel.rules.default,maps) == $null ) hadd sentinel.rules.default maps $ctime Default cp_badlands cp_dustbowl cp_granary cp_gravelpit cp_well ctf_2fort ctf_well pl_goldrush tc_hydro

    hmake sentinel.invited 5
    hload -i sentinel.invited sentinel.ini invited

    ;--Load the rules for any invited channels
    var %i = 1
    while ( %i <= $hget(sentinel.invited,0).item ) {
    hmake [ sentinel.rules. $+ [ $hget(sentinel.invited,%i).item ] ] 5
    hload -i [ sentinel.rules. $+ [ $hget(sentinel.invited,%i).item ] ] sentinel.ini [ rules. $+ [ $hget(sentinel.invited,%i).item ] ]
    inc %i
    }
    }
    alias sentinel.unload { sentinel.deactivate | hfree -w sentinel.* }
    alias sentinel.activate {
    if ( $hget(sentinel.general.status,activated) == 1 ) return
    hadd -m sentinel.general.status activated 1

    ;--Join monitor and admin irc channels
    join $hget(sentinel.irc,monitor)
    if ( ($gettok($hget(sentinel.irc,monitor),2,32) != $null) ) .timersentinel.monitor.pw 1 2 mode #$gettok($hget(sentinel.irc,monitor),1,32) +k $gettok($hget(sentinel.irc,monitor),2,32)
    join $hget(sentinel.irc,admin)
    if ( ($gettok($hget(sentinel.irc,admin),2,32) != $null) ) .timersentinel.admin.pw 1 2 mode #$gettok($hget(sentinel.irc,admin),1,32) +k $gettok($hget(sentinel.irc,admin),2,32)

    ;--Join invited irc channels
    var %i = 1
    while ( %i <= $hget(sentinel.invited,0).item ) {
    join $hget(sentinel.invited,%i).item $gettok($hget(sentinel.invited,%i).data,3,32)
    inc %i
    }

    sentinel.server.open
    }
    alias sentinel.deactivate {
    if ( $hget(sentinel.general.status,activated) != 1) return

    hadd sentinel.general.status activated 0
    sentinel.server.close

    ;--Depart invited channels
    var %i = 1
    while ( %i <= $hget(sentinel.invited,0).item ) {
    part $gettok($hget(sentinel.invited,%i).item,1,32) 5Bot Deactivated
    inc %i
    }

    ;--Depart irc channels
    part $gettok($hget(sentinel.irc,admin),1,32) 5Bot Deactivated
    part $gettok($hget(sentinel.irc,monitor),1,32) 5Bot Deactivated

    ;--Clean-up yer mess!
    hfree -w sentinel.server.*
    hfree -w sentinel.rcon.*
    timers off
    }
    alias sentinel.server.open {
    ;--Join the tv channel and then open the log socket.
    hmake sentinel.server.status 5
    join $hget(sentinel.irc,tv)
    if ( ($gettok($hget(sentinel.irc,tv),2,32) != $null) ) .timersentinel.tv.pw 1 2 mode #$gettok($hget(sentinel.irc,tv),1,32) +k $gettok($hget(sentinel.irc,tv),2,32)
    sentinel.rcon status
    .timersentinel.rcon 1 3 sentinel.rcon sv_visiblemaxplayers
    .timersentinel.log.open 1 3 sentinel.log.open
    }
    alias sentinel.server.close {
    ;--Close the log socket and leave the tv channel.
    hfree sentinel.server.status
    sentinel.log.close
    if ( ($gettok($hget(sentinel.irc,tv),1,32) != $gettok($hget(sentinel.irc,admin),1,32)) $&
    && ($gettok($hget(sentinel.irc,tv),1,32) != $gettok($hget(sentinel.irc,monitor),1,32)) ) $&
    part $gettok($hget(sentinel.irc,tv),1,32) 5Server Closed
    else msg $gettok($hget(sentinel.irc,tv),1,32) 5Server Closed
    }
    alias sentinel.server.address {
    var %address = $hget(sentinel.server,address)
    if ( $pos(%address,:,0) == 0 ) var %address = %address $+ :27015
    return $replace(%address,:,$chr(32))
    }
    alias sentinel.port {
    var %port = $hget(sentinel.general,$1)
    if ( $hget(sentinel.general,port.type) == dynamic ) return $null
    else return %port
    }
    alias sentinel.log.open {
    hadd sentinel.general.status log.status Open
    sockudp -k sentinel.socket.log $sentinel.port(port.log) $sentinel.server.address
    sentinel.rcon logaddress_add $ip $+ : $+ $sock(sentinel.socket.log).port
    }
    alias sentinel.log.close {
    hadd sentinel.general.status log.status Closed
    sentinel.rcon logaddress_del $ip $+ : $+ $sock(sentinel.socket.log).port
    sockclose sentinel.socket.log
    }
    alias sentinel.log.check {
    ;--If log has been explicitly closed, don't re-open it!
    if ( $hget(sentinel.general.status,log.status) == Closed ) return

    ;--If no log entry for over 5 mins, check status which will generate a log entry for the rcon.
    if ( $sock(sentinel.socket.log).lr > $calc(5 * 60) ) sentinel.rcon status

    ;--If no log entry still in over 15 mins, attempt to reopen the log.
    if ( $sock(sentinel.socket.log).lr > $calc(15 * 60) ) sentinel.log.open
    }
    alias sentinel.log.address {
    var %address = $hget(sentinel.log,address)
    if ( $pos(%address,:,0) == 0 ) var %address = %address $+ :28888
    return $replace(%address,:,$chr(32))
    }
    alias sentinel.rcon {
    var %cmd = $1-
    if ( $1 == =rcon ) var %cmd = $2-

    ;--Save command in the rcon queue.
    var %request_id = $sentinel.rcon.queue(%cmd)

    ;--If this is an =rcon request, associate the request_id with the response target.
    if ( $1 == =rcon ) set -u10 %sentinel.rcon.response %sentinel.rcon.response %request_id

    ;--If the socket is already attempting to authenticate, just let it do it's thing.
    if ( %sentinel.socket.rcon.status == authenticating ) return

    if ( $sock(sentinel.socket.rcon).to == $null ) {
    set -u15 %sentinel.socket.rcon.status authenticating
    sockclose sentinel.socket.rcon
    sockopen sentinel.socket.rcon $sentinel.server.address
    }
    else { sentinel.rcon.write %request_id }

    return %request_id
    }
    alias sentinel.rcon.queue {
    var %id = $hget(sentinel.rcon.queue,$hget(sentinel.rcon.queue,0).item).item + 1
    hadd -m sentinel.rcon.queue %id $1-
    return %id
    }
    alias sentinel.rcon.queue.check {
    if ( $hget(sentinel.rcon.queue,$1) != $null ) sentinel.rcon.write $1
    }
    on *:SOCKOPEN:sentinel.socket.rcon:{
    ;--Set 15 second connection timeout.
    .timersentinel.socket.rcon.timeout 1 15 sockclose sentinel.socket.rcon

    ;--Send the initial authentication request.
    sentinel.rcon.write 0
    unset %sentinel.rcon.request_id
    }
    alias sentinel.rcon.write {
    ;--Retrieve command in rcon queue.
    var %request_id = $1
    var %cmd = $hget(sentinel.rcon.queue,%request_id)
    var %type = 2

    ;--If request_id is 0, this needs to be an authentication request.
    if ( %request_id == 0 ) {
    var %type = 3
    var %cmd = $hget(sentinel.server,rcon_password)
    }

    var %packet_size = $calc(10 + $len(%cmd))

    ;--Compile packet data to be sent.
    bset &packet 1 %packet_size
    bset &packet 5 %request_id
    bset &packet 9 %type
    bset -t &packet 13 %cmd
    bset &packet $calc(13 + $len(%cmd)) 0 0

    ;--Debug output.
    if ( %sentinel.rcon.debug == 2 ) debug.binvar &packet RCON TCP OUT $chr($asc(|)) FROM: $sock(sentinel.socket.rcon).bindip $+ : $+ $sock(sentinel.socket.rcon).bindport $chr($asc(|)) TO: $sock(sentinel.socket.rcon).ip $+ : $+ $sock(sentinel.socket.rcon).port
    if ( %sentinel.rcon.debug >= 1 ) {
    echo -s $timestamp RCON TCP OUT $chr($asc(|)) FROM: $sock(sentinel.socket.rcon).bindip $+ : $+ $sock(sentinel.socket.rcon).bindport $chr($asc(|)) TO: $sock(sentinel.socket.rcon).ip $+ : $+ $sock(sentinel.socket.rcon).port
    echo -si11 ---------- Packet Size: %packet_size -- Request ID: %request_id -- Request Type: %type
    echo -si11 ---------- %cmd
    }

    ;--Actual socket write.
    sockwrite sentinel.socket.rcon &packet
    [ .timersentinel.rcon.queue $+ [ %request_id ] ] 1 10 sentinel.rcon.queue.check %request_id
    }
    on *:SOCKREAD:sentinel.socket.rcon:{
    ;--Reset 15 second connection timeout.
    .timersentinel.socket.rcon.timeout 1 15 sockclose sentinel.socket.rcon

    sockread $sock(sentinel.socket.rcon).rq &data
    if ( %sentinel.rcon.debug == 2 ) debug.binvar &data RCON TCP IN $chr($asc(|)) FROM: $sock(sentinel.socket.rcon).ip $+ : $+ $sock(sentinel.socket.rcon).port $chr($asc(|)) TO: $sock(sentinel.socket.rcon).bindip $+ : $+ $sock(sentinel.socket.rcon).bindport

    ;--Might be more than one packet of data. Seperate each packet by finding the double null at the end of the packet.
    var %i = 1
    while (%i <= $bvar(&data,0)) {
    var %eop = $bfind(&data,$calc(%i + 12),0) + 1
    if ( %eop == 1 ) var %eop = $bvar(&data,0)
    bcopy -c &packet 1 &data %i $calc(%eop - %i + 1)
    sentinel.rcon.parsePacket &packet
    var %i = %eop + 1
    }
    }
    alias sentinel.rcon.parsePacket {
    var %packet_size = $calc($bvar($1,1) + $bvar($1, 2) * 256 + $bvar($1, 3) * (256 ^ 2) + $bvar($1, 4) * (256 ^ 3))
    var %request_id = $calc($bvar($1,5) + $bvar($1, 6) * 256 + $bvar($1, 7) * (256 ^ 2) + $bvar($1, 8) * (256 ^ 3))
    var %type = $calc($bvar($1,9) + $bvar($1,10) * 256 + $bvar($1,11) * (256 ^ 2) + $bvar($1,12) * (256 ^ 3))

    ;--Parse strings seperated by linefeeds into a hash table.
    var %i = 13
    var %j = 1
    while (%i <= $calc($bvar($1,0) - 2)) {
    var %len = $calc($bfind($1,%i,10) - %i)
    if ( %len < 0 ) var %len = $calc($bvar($1,0) - %i - 2)
    if ( %len >= 0 ) {
    hadd -m sentinel.rcon.string1 %j $bvar($1,%i,%len).text
    inc %j
    }
    var %i = $calc(%i + %len + 1)
    }

    ;--Debug output
    if ( %sentinel.rcon.debug >= 1 ) {
    echo -s $timestamp RCON TCP IN $chr($asc(|)) FROM: $sock(sentinel.socket.rcon).ip $+ : $+ $sock(sentinel.socket.rcon).port $chr($asc(|)) TO: $sock(sentinel.socket.rcon).bindip $+ : $+ $sock(sentinel.socket.rcon).bindport
    echo -si11 ---------- Packet Size: %packet_size -- Request ID: %request_id -- Response Type: %type
    var %i = 1
    while ( %i <= $hget(sentinel.rcon.string1,0).item ) {
    echo -si11 ---------- String 1: $hget(sentinel.rcon.string1,%i)
    inc %i
    }
    }

    if ( %type == 2 ) {
    ;--SERVERDATA_AUTH_RESPONSE
    ;--If a request_id is 0xFFFFFFFF(4294967295) or not matching any request_id we've sent, then it's an error.
    ;--The exception is that when we first open or close and reopen the rcon socket, we send the authentication as request_id = 0.
    if ( (%request_id != 4294967295) && (($hget(sentinel.rcon.queue,%request_id) != $null) || (%request_id == 0)) ) {
    unset %sentinel.socket.rcon.status

    ;--Send every command that built up in the rcon queue while authenticating now.
    ;--As the queue may start emptying before we finish stepping through it, save out a list of request ids first.
    var %i = 1
    while ( %i <= $hget(sentinel.rcon.queue,0).item ) {
    var %id_list = %id_list $hget(sentinel.rcon.queue,%i).item
    inc %i
    }
    var %i = 1
    while ( %i <= $numtok(%id_list,32) ) {
    sentinel.rcon.write $gettok(%id_list,%i,32)
    inc %i
    }
    }
    else { echo 4 -s RCON Authentication Error: %request_id }
    }
    elseif ( %type == 0 ) {
    ;--SERVERDATA_RESPONSE_VALUE
    if ( %request_id != 0 ) {
    ;--Not an authentication request response. So, remove request from the rcon queue and parse the data.
    hdel sentinel.rcon.queue %request_id
    if ( $hget(sentinel.rcon.string1,0).item > 0 ) sentinel.rcon.parse %request_id
    }
    }
    else echo 4 -s Unrecognized RCON Response Type: %type

    hfree -w sentinel.rcon.string1
    }
    alias sentinel.rcon.parse {
    ;--Here is where we do meaningful manipulation of the data in the packets we got.
    var %lines = $hget(sentinel.rcon.string1,0).item

    ;--If this is the response to an =rcon command, then show the results back
    if ( $1 == $gettok(%sentinel.rcon.response,2,32) ) {
    var %i = 1
    while ( %i <= %lines ) {
    ;--Frequently rcon responses end with two linefeeds resulting in an empty line following the response.
    ;--No reason to relay an empty line at the end in IRC.
    if ( (%i == $hget(sentinel.rcon.string1,0).item) && ($hget(sentinel.rcon.string1,%i) == $null) ) { }
    else msg $gettok(%sentinel.rcon.response,1,32) RCON: $hget(sentinel.rcon.string1,%i)
    inc %i
    }
    unset %sentinel.rcon.response
    }

    ;--"status" response; Example:
    ;hostname: EdGruberman's Test Server
    ;version : 1.0.3.1/14 3504 secure
    ;udp/ip : 192.168.1.103:27015
    ;map : cp_roswell at: 0 x, 0 y, 0 z
    ;players : 2 (24 max)
    ;
    ;# userid name uniqueid connected ping loss state adr
    ;# 1 "EdGruberman" STEAM_0:0:204432 54:06 69 0 active 192.168.1.11:27005
    ;# 2 "Player" BOT active
    if ( $gettok($hget(sentinel.rcon.string1,1),1,32) == hostname: ) {
    ;hfree -w sentinel.server.status
    hadd -m sentinel.server.status hostname $gettok($hget(sentinel.rcon.string1,1),2-,32)
    hadd -m sentinel.server.status map $gettok($hget(sentinel.rcon.string1,4),3,32)
    hadd -m sentinel.server.status player.max $mid($gettok($hget(sentinel.rcon.string1,5),4,32),2)

    ;--Verify TV channel topic
    var %tv = $gettok($hget(sentinel.irc,tv),1,32)
    if ( $chan(%tv).topic != MAP:  $+ $hget(sentinel.server.status,map) $+  on $hget(sentinel.server.status,hostname) at $hget(sentinel.server,address) ) $&
    topic %tv MAP:  $+ $hget(sentinel.server.status,map) $+  on $hget(sentinel.server.status,hostname) at $hget(sentinel.server,address)

    ;--Clean out/reset old player information
    hfree -w sentinel.rcon.status
    hadd -m sentinel.server.status player.count 0
    hadd -m sentinel.server.status bot.count 0

    ;--Loop through player list
    var %i = 8
    while ( %i <= %lines ) {
    var %line = $hget(sentinel.rcon.string1,%i)

    ;--Example: # 1 "EdGruberman" STEAM:0:1:204432 25:09 19 0 active 192.168.1.11:27005
    ;--Example: #68 "Player" BOT active
    var %p_name = $gettok(%line,2,$asc("))
    var %p_stats = $gettok(%line,3,$asc("))
    var %p_uniqueid = $gettok(%p_stats,1,32)
    var %p_userid = $mid($gettok(%line,1,$asc(")),2)
    if ( %p_uniqueid == BOT ) hinc -m sentinel.server.status bot.count
    elseif ( $left(%p_uniqueid,4) == hltv ) hinc -m sentinel.server.status hltv.count
    else {
    ;--.rcon.status = ID1 time2 ping3 loss4 adr5 state6 name7-
    hinc -m sentinel.server.status player.count
    hadd -m sentinel.rcon.status %p_userid %p_uniqueid $gettok(%p_stats,2-4,32) $gettok(%p_stats,6,32) $gettok(%p_stats,5,32) %p_name
    }
    inc %i
    }

    ;--Since we've just processed the current status, might as well check to see if anyone wants to be updated
    sentinel.notify
    }

    ;--Server variable response; Example: "maxplayers" is "12"
    elseif ( $left($hget(sentinel.rcon.string1,1),1) == " ) {
    tokenize $asc(") $hget(sentinel.rcon.string1,1)
    hadd -m sentinel.server.status $1 $3
    }

    ;--server_game_time response; Example: Server game time: 499.919983
    elseif ( $gettok($hget(sentinel.rcon.string1,1),1-3,32) == Server game time: ) {
    tokenize 32 $hget(sentinel.rcon.string1,1)
    hadd -m sentinel.server.status server_game_time $gettok($4,1,$asc(.))
    }

    ;--logaddress_add response; Example: logaddress_add: 1.2.3.4:49889
    elseif ( $gettok($hget(sentinel.rcon.string1,1),1,32) == logaddress_add: ) {
    if ( $gettok($hget(sentinel.rcon.string1,1),2,32) == $ip $+ : $+ $sock(sentinel.socket.log).port ) $&
    msg $gettok($hget(sentinel.irc,tv),1,32) 5Server Opened
    }

    else { }

    }
    on *:UDPREAD:sentinel.socket.log:{
    ;--Only accept data from our designated server.
    if ( $sock(sentinel.socket.log).saddr != $gettok($sentinel.server.address,1,32) ) return

    sockread $sock(sentinel.socket.log).rq &packet
    if ($sockbr == 0) return
    if (%sentinel.log.debug == 2) debug.binvar &packet LOG UDP IN $chr($asc(|)) FROM: $sock(sentinel.socket.log).saddr $+ : $+ $sock(sentinel.socket.log).sport $chr($asc(|)) TO: $sock(sentinel.socket.log).ip $+ : $+ $sock(sentinel.socket.log).port
    sentinel.log.parsePacket &packet
    }
    alias sentinel.log.parsePacket {
    var %line = $bvar($1,1,$bvar($1,0)).text

    if ( %sentinel.log.debug >= 1 ) {
    echo -s $timestamp LOG UDP IN $chr($asc(|)) FROM: $sock(sentinel.socket.log).saddr $+ : $+ $sock(sentinel.socket.log).sport $chr($asc(|)) TO: $sock(sentinel.socket.log).ip $+ : $+ $sock(sentinel.socket.log).port
    echo -si11 ---------- %line
    }

    ;--Strip the header and the last character which is always a linefeed.
    if ( $left(%line,6) == ÿÿÿÿRL ) sentinel.log.parse $left($mid(%line,6),$calc($len($mid(%line,6)) - 1))
    }
    alias sentinel.log.parse {
    var %tv = $gettok($hget(sentinel.irc,tv),1,32)
    var %monitor = $gettok($hget(sentinel.irc,monitor),1,32)
    var %admin = $gettok($hget(sentinel.irc,admin),1,32)

    ;--Reset 1min timer to check if still open
    .timersentinel.log.check -o 0 60 sentinel.log.check

    ;--Record to log file if desired
    if ( $hget(sentinel.log,file) != $null ) { write $shortfn($hget(sentinel.log,file)) $+ $replace($hget(sentinel.server,address),:,.) $+ .log $1- }

    ;--Example: L 07/09/2003 - 23:34:02: Kick: "EdGruberman<5><204432><>" was kicked by "Console" (message "spawn camping")
    ;--Example: L 07/09/2003 - 23:34:02: Kick: "EdGruberman<6><204432><>" was kicked by "Console"
    if ( $5 == Kick: ) {
    tokenize $asc(") $1-
    msg %tv * $sentinel.format.player($2) was kicked. $iif($len($6) > 0,Reason: 07 $+ $6)
    }

    ;--Example: L 07/09/2003 - 22:49:21: Started map "avanti" (CRC "-2138426516")
    elseif ( $5-6 == Started map ) {
    set -u120 %sentinel.status mapchange
    sentinel.rcon status

    ;--We know all players start out as Unassigned at a map change.
    var %i = 1
    while ( %i <= $hget(sentinel.rcon.status,0).item ) {
    hadd -m sentinel.players Unassigned $gettok($hget(sentinel.players,%i).data,2,32)
    inc %i
    }

    tokenize $asc(") $1-
    msg %tv ¯¯¯¯¯¯¯¯¯¯ Map changed to " $+ $2 $+ " ¯¯¯¯¯¯¯¯¯¯
    topic %tv MAP:  $+ $2 $+  on $hget(sentinel.server.status,hostname) at $hget(sentinel.server,address)
    }

    ;--Example: L 07/09/2003 - 23:21:40: Team "Blue" scored "0" with "1" player (kills "0") (kills_unaccounted "0")
    elseif ( ($5 = Team) && ($7 == scored) ) {
    tokenize $asc(") $1-
    msg %tv ===== $sentinel.format.team($2) scored $4 $+ pts with $6 players on $hget(sentinel.server.status,map) $+ . =====
    }

    ;--Example: L 06/15/2008 - 21:01:22: Team "Blue" triggered "pointcaptured" (cp "0") (cpname "Courtyard") (numcappers "4") (player1 "^N^BlueInGreen<53><STEAM_0:0:17012120><Blue>") (position1 "921 2765 -152") (player2 "BladeGamer[cnlm]<62><STEAM_0:1:17491723><Blue>") (position2 "886 2866 -152") (player3 "Swashbuckler<94><STEAM_0:1:5199409><Blue>") (position3 "892 2745 -152") (player4 "ScorpioZ<108><STEAM_0:1:7492850><Blue>") (position4 "866 2812 -152")
    elseif ( ($5 = Team) && ($7 == triggered) ) {
    tokenize $asc(") $1-
    if ( $4 == pointcaptured ) msg %tv ===== $sentinel.format.team($2) captured $8 $+  with $10 cappers on $hget(sentinel.server.status,map) $+ . =====
    }

    ;--Example: L 06/18/2008 - 10:59:42: server_message: "quit"
    elseif ( $5-6 == server_message: "quit" ) {
    msg %tv Server shutdown.
    }

    ;--Player event... parse type of event
    elseif ( $left($5,1) == " ) {
    var %playerinfo = $mid($1-,27,$calc($pos($1-,",2) - 27))
    var %team = $replace($mid($gettok(%playerinfo,-1,$asc(>)),2),$chr(32),_)
    var %player = $left(%playerinfo,$calc($pos(%playerinfo,<,$calc($pos(%playerinfo,<,0) - 2)) - 1))
    var %userid_tok = $gettok(%playerinfo,$calc($numtok(%playerinfo,$asc(<)) - 2),$asc(<))
    var %userid = $left(%userid_tok,$calc($len(%userid_tok) - 1))

    ;--Ensure players hash is up to date on team status for player.
    hadd -m sentinel.players %userid %team $gettok($hget(sentinel.players,%userid),2,32)

    var %event_start = $calc($pos($1-,",2) + 2)
    var %event_end = $calc($pos($1-,",3) - 1)
    if ( %event_end == -1 ) { var %event_end = $calc($len($1-) + 1) }
    var %event = $mid($1-,%event_start,$calc(%event_end - %event_start))

    ;--Example: L 06/14/2008 - 16:14:04: "CYBER<69><STEAM_0:1:1485814><Red>" say_team "sry"
    ;--Example: L 06/14/2008 - 00:28:01: "Console<0><Console><Console>" say "#sentinel.tv | EdGruberman : hi"
    if ( %event == say || ( (%event == say_team) && ($hget(sentinel.general,show.sayteam) == 1) ) ) {
    var %text_start = $pos($1-,",3) + 1
    var %text_end = $len($1-)
    var %text = $mid($1-,%text_start,$calc(%text_end - %text_start))
    var %admin = $mid(%text,$pos(%text,admin,1),5)
    if ( %event == say_team ) {
    if ( %team != Spectator ) var %playerinfo = $chr(40) $+ TEAM $+ $chr(41) %playerinfo
    else var %playerinfo = $chr(40) $+ Spectator $+ $chr(41) %playerinfo
    }
    if ( (%event == say) && (%team == Spectator) ) { var %playerinfo = *SPEC* %playerinfo }
    if ( (%team == Unassigned) && (%event == say) ) var %playerinfo = *DEAD* %playerinfo
    if ( (%team == Unassigned) && (%event == say_team) ) var %playerinfo = *DEAD* $+ %playerinfo
    var %message = $sentinel.format.player(%playerinfo) : $replace(%text,admin, $+ %admin $+ )
    msg %tv %message
    if ( admin isin %text ) { msg %monitor 07 $+ %tv $chr($asc(|)) %message }
    }

    ;--Example: L 07/09/2003 - 10:17:54: "EdGruberman<1><204432><>" connected, address "1.2.3.4:27005"
    elseif ( %event == connected, address ) {
    if ( $hget(sentinel.general,show.connects) == 1 ) {
    tokenize $asc(") $1-
    msg %tv * $sentinel.format.player(%playerinfo) $+ < $+ $mid($gettok(%playerinfo,-2,$asc(>)),2) $+ > is trying to connect $&
    $iif($hget(sentinel.general,show.ip) == 1,from $4,)
    }
    }

    ;--Example: L 07/09/2003 - 23:07:42: "EdGruberman<2><204432><>" entered the game
    elseif ( %event == entered the game ) {
    if ( ($hget(sentinel.general,show.connects) == 1) && (%sentinel.status != mapchange) ) {
    msg %tv * $sentinel.format.player(%playerinfo) $+ < $+ $mid($gettok(%playerinfo,-2,$asc(>)),2) $+ > has entered the game.
    sentinel.rcon status
    }
    else {
    .timersentinel.enteredthegame 1 15 sentinel.rcon status
    .timersentinel.players 1 30 sentinel.players %tv
    }
    }

    ;--Example: L 06/15/2008 - 22:01:51: "EdGruberman<111><STEAM_0:0:204432><Blue>" disconnected (reason "Disconnect by user.")
    elseif ( %event == disconnected $chr(40) $+ reason ) {
    if ( $hget(sentinel.general,show.connects) == 1 ) {
    tokenize $asc(") $1-
    var %reason = $4
    if ($right(%reason,1) = $chr(10)) var %reason = $mid(%reason,1,$calc($len(%reason) - 1))
    msg %tv * $sentinel.format.player(%playerinfo) $+ < $+ $mid($gettok(%playerinfo,-2,$asc(>)),2) $+ > disconnected. $chr(40) $+ %reason $+ $chr(41)
    }
    sentinel.rcon status
    }

    ;--Example: L 07/09/2003 - 23:21:54: "EdGruberman<3><204432><Blue>" changed name to "Luka"
    elseif ( %event == changed name to ) {
    tokenize $asc(") $1-
    msg %tv * $sentinel.format.player(%playerinfo) changed name to " $+ $4 $+ ".
    }

    ;--Example: L 07/09/2003 - 23:35:39: "EdGruberman<6><204432><SPECTATOR>" joined team "Blue"
    elseif ( %event == joined team ) {
    tokenize $asc(") $1-
    hadd -m sentinel.players %userid $4 $gettok($hget(sentinel.players,%userid),2,32)
    }

    else { }
    }

    else { }
    }
    alias sentinel.format.weapon {
    ;--Returns an irc formatted, ascii art representation of a weapon
    if ( $1 == axe ) { return 14.04/14` }
    elseif ( $1 == caltrop ) { return 15¸ }
    elseif ( $1 == medikit ) { return 15,00[04,00+15,00] }
    elseif ( $1 == spanner ) { return 14©==C }
    else { return $null }
    }
    alias sentinel.format.player {
    ;--Returns the player name passed to it colored according to it's team as per the ini
    ;--Color is left open, must terminate in calling function if so desired
    ;--Example: EdGruberman<4><204432><#Dustbowl_team1>
    ;--Example: Console<0><Console><Console>
    var %team = $replace($mid($gettok($1,-1,$asc(>)),2),$chr(32),_)
    var %color = $gettok($hget(sentinel.teams,%team),1,32)
    var %player = $left($1-,$calc($pos($1-,<,$calc($pos($1-,<,0) - 2)) - 1))
    if ( %color == $null && %team != $null ) { var %color = 01 $+ < $+ %team $+ > }
    if ( %color != $null ) { var %color =  $+ %color }
    return %color $+ %player
    }
    alias sentinel.format.team {
    ;--Returns the team name passed to it colored as per the ini
    ;--Color is left open, must terminate in calling function if so desired
    var %color = $gettok($hget(sentinel.teams,$replace($1,$chr(32),_)),1,32)
    var %team = $gettok($hget(sentinel.teams,$replace($1,$chr(32),_)),2,32)
    if ( %team == $null ) { var %team = $1 }
    if ( %color == $null ) { return " $+ %team $+ " }
    else { return  $+ %color $+ %team }
    }
    on *:INVITE:#:{
    join $chan
    .timersentinel.invite.check 1 5 sentinel.invite.check $nick $chan
    }
    alias sentinel.invite.check {
    ;--If invited to the admin channel, just say there so the =activate command can be used correctly.
    if ( $2 == $gettok($hget(sentinel.irc,admin),1,32) ) return

    ;--Force users to use /invite so the bot can record who exactly invited it
    if ( $1 = ChanServ ) { msg $2 rawr! you need to use the following command to invite me: /invite $me $2 | part $2 | return }

    ;--If ChanServ is not a part of channel, don't stick around
    if ( ChanServ !isop $2 ) { msg $2 i will only stay in a registered channel, sorry... :~( | part $2 | return }

    ;--Notify the monitor channel then join and introduce yerself!
    msg $gettok($hget(sentinel.irc,monitor),1,32) rawr! i just got invited to $2 by $1 $+ ! i feel teh 4<3 ;)~
    msg $2 rawr! i'm a Sentinel bot!
    msg $1 i've just added $2 to my list of auto-joins because you invited me. all ops in $2 can use =rules to change my channel notification settings. use =rules help to get started!

    ;--Save invited information to ini
    hadd sentinel.invited $2 $ctime $1
    hsave -i sentinel.invited sentinel.ini invited
    }
    on *:KICK:#:{
    ;--For the GameSurge IRC network, after netsplits ChanServ will kick you with the reason of "Net Rider" for channel management purposes.
    ;--Simply rejoining the channel is acceptable. The trick is make sure we have the password or not.
    if ( ( $knick == $me ) && ( $1- == Net Rider ) ) {
    if ( $chan == $gettok($hget(sentinel.irc,monitor),1,32) ) { var %rejoin = $hget(sentinel.irc,monitor) }
    elseif ( $chan == $gettok($hget(sentinel.irc,admin),1,32) ) { var %rejoin = $hget(sentinel.irc,admin) }
    elseif ( $chan == $gettok($hget(sentinel.irc,tv),1,32) ) { var %rejoin = $hget(sentinel.irc,tv) }
    else { var %rejoin = $chan }
    join %rejoin
    return
    }

    ;--If this is the admin channel, just leave as expected to.
    if ( $2 == $gettok($hget(sentinel.irc,admin),1,32) ) return

    ;--Else it looks like this really is a goodbye. :~(
    if ( $knick == $me ) {
    part $chan 5Sentinel Bot: Removed from service.
    notice $nick thx for having me! i won't come back to $chan unless someone invites me again...
    msg $gettok($hget(sentinel.irc,monitor),1,32)  $+ $nick just kicked me outta $chan ... :~( i fear they might not wub me anymore *sniff*

    ;--Remove this channel from the invited list
    hdel sentinel.invited $chan
    hsave -i sentinel.invited sentinel.ini invited

    ;--Remove all the rules for this channel
    hfree [ sentinel.rules. $+ [ $chan ] ]
    remini sentinel.ini [ rules. $+ [ $chan ] ]
    }
    }
    on *:TEXT:=rules*:#:{
    if ( $nick !isop $chan ) { [ .timersentinel.denied 1 1 notice $nick Denied: You lack operator status in $chan $+ . }
    else {
    if ( $2 == $null ) {
    ;--Get channel rules
    var %players = $hget([ sentinel.rules. $+ [ $chan ] ],players)
    var %maps = $hget([ sentinel.rules. $+ [ $chan ] ],maps)
    var %type = $hget([ sentinel.rules. $+ [ $chan ] ],type)
    var %min = $hget([ sentinel.rules. $+ [ $chan ] ],min)
    var %max = $hget([ sentinel.rules. $+ [ $chan ] ],max)

    ;--If a rule is not defined for a channel, use the default
    if ( %players == $null ) { var %players = $hget(sentinel.rules.default,players) }
    if ( %maps == $null ) { var %maps = $hget(sentinel.rules.default,maps) }
    if ( %type == $null ) { var %type = $hget(sentinel.rules.default,type) }
    if ( %min == $null ) { var %min = $hget(sentinel.rules.default,min) }
    if ( %max == $null ) { var %max = $hget(sentinel.rules.default,max) }

    ;--Give the user the info
    notice $nick $chan $+ : Players = $gettok(%players,3,32)  $+ $chr(40) $+ Default: $gettok($hget(sentinel.rules.default,players),3,32) $+ $chr(41) $chr($asc(|)) Min number of players, bots are not counted.
    notice $nick $chan $+ : Min = $gettok(%min,3,32)  $+ $chr(40) $+ Default: $gettok($hget(sentinel.rules.default,min),3,32) $+ $chr(41) $chr($asc(|)) Shortest time in seconds before bot can notify again.
    notice $nick $chan $+ : Max = $gettok(%max,3,32)  $+ $chr(40) $+ Default: $gettok($hget(sentinel.rules.default,max),3,32) $+ $chr(41) $chr($asc(|)) Longest time in seconds bot will wait before notifying again.
    notice $nick $chan $+ : Type = $gettok(%type,3,32)  $+ $chr(40) $+ Default: $gettok($hget(sentinel.rules.default,type),3,32) $+ $chr(41) $chr($asc(|)) If notify, Maps contains maps to notify if played. If ignore, Maps contains maps that no notifications will occur on.
    notice $nick $chan $+ : Maps = $gettok(%maps,3-,32)
    }
    else {
    ;--Verify the parameter is one this function accepts
    var %params = players min max forced type maps help
    if ( $findtok(%params,$lower($2),0,32) == 0 ) { [ .timersentinel. $+ [ $nick ] ] 1 1 notice $nick Error: I don't understand the $2 parameter. See =rules help for more info. }
    elseif ( $lower($2) == help ) {
    [ .timersentinel. $+ [ $nick ] $+ 1 ] 1 1 notice $nick =rules ( View/change/reset channel notification rules for all servers with this command. )
    [ .timersentinel. $+ [ $nick ] $+ 2 ] 1 1 notice $nick Syntax: =rules $chr($asc([)) $+ <rule_name> $+ $chr($asc([)) <rule_value> $+ $chr($asc(])) $+ $chr($asc(]))
    [ .timersentinel. $+ [ $nick ] $+ 3 ] 1 1 notice $nick To view the channel's current rules, do not include any parameters. Example: =rules
    [ .timersentinel. $+ [ $nick ] $+ 4 ] 1 1 notice $nick To edit a rule, include the <rule_name> and the <rule_value> parameters. Example: =rules min 120
    [ .timersentinel. $+ [ $nick ] $+ 5 ] 1 1 notice $nick To reset a rule to default, only put the <rule_name> parameter. Example: =rules min
    }
    else {
    ;--Edit hash entry if param $3- isn't null, else del the hash entry
    var %old = $gettok($hget([ sentinel.rules. $+ [ $chan ] ],$2),3-,32)
    if ( %old == $null ) { var %old = $gettok($hget(sentinel.rules.default,$2),3-,32) }
    if ( $3- != $null ) { hadd -m [ sentinel.rules. $+ [ $chan ] ] $2 $ctime $nick $3- }
    else { hdel [ sentinel.rules. $+ [ $chan ] ] $2 }
    hsave -i [ sentinel.rules. $+ [ $chan ] ] sentinel.ini [ rules. $+ [ $chan ] ]
    if ( $3- != $null ) { [ .timersentinel. $+ [ $nick ] ] 1 1 notice $nick  $+ $2 rule changed for $chan $+  from " $+ %old $+ " to " $+ $3- $+ " }
    else { [ .timersentinel. $+ [ $nick ] ] 1 1 notice $nick  $+ $2 rule reset to default for $chan $+  from " $+ %old $+ " to " $+ $gettok($hget(sentinel.rules.default,$2),3-,32) $+ " }
    }
    }
    }
    }
    on *:TEXT:=help:*:{
    var %adminop = $iif($nick isop $gettok($hget(sentinel.irc,admin),1,32),$true,$false)
    var %monitorop = $iif($nick isop $gettok($hget(sentinel.irc,monitor),1,32) || $nick isop $gettok($hget(sentinel.irc,tv),1,32),$true,$false)
    var %status = You $iif(%adminop,are,are not) an op in the admin channel ( $+ $gettok($hget(sentinel.irc,admin),1,32) $+ ) and you $iif(%monitorop,are,are not) an op in the monitor ( $+ $gettok($hget(sentinel.irc,monitor),1,32) $+ ) or tv ( $+ $gettok($hget(sentinel.irc,tv),1,32) $+ ) channel.
    [ .timersentinel. $+ [ $nick ] $+ 1 ] 1 1 notice $nick %status $iif(%adminop || %monitorop,With your current status you can use the following commands:,)
    if ( ( $nick isop $chan ) && ( $chan != $gettok($hget(sentinel.irc,tv),1,32) ) ) { [ .timersentinel. $+ [ $nick ] $+ 2 ] 1 1 notice $nick =rules help :: Adjusts channel notification criteria. :: Usable in any channel except $gettok($hget(sentinel.irc,tv),1,32) by Ops in calling channel }
    if ( $nick isop $gettok($hget(sentinel.irc,monitor),1,32) || $nick isop $gettok($hget(sentinel.irc,tv),1,32)$nick isop $gettok($hget(sentinel.irc,admin),1,32) ) {
    [ .timersentinel. $+ [ $nick ] $+ 3 ] 1 1 notice $nick =chat <message> :: Sends <message> into game public chat. :: Usable only in $gettok($hget(sentinel.irc,tv),1,32) by Ops in $gettok($hget(sentinel.irc,monitor),1,32) or $gettok($hget(sentinel.irc,tv),1,32)
    [ .timersentinel. $+ [ $nick ] $+ 4 ] 1 1 notice $nick =status :: Displays current game status on server. :: Usable in any channel by Ops in $gettok($hget(sentinel.irc,monitor),1,32) or $gettok($hget(sentinel.irc,tv),1,32)
    [ .timersentinel. $+ [ $nick ] $+ 5 ] 1 1 notice $nick =players[ <player_name>] :: Lists current player names or detailed player info if <player_name> matches a current player. :: Usable in any channel by Ops in $gettok($hget(sentinel.irc,monitor),1,32) or $gettok($hget(sentinel.irc,tv),1,32)
    [ .timersentinel. $+ [ $nick ] $+ 6 ] 1 1 notice $nick =invited :: Lists channels bot has been invited into. :: Usable in any channel by Ops in $gettok($hget(sentinel.irc,monitor),1,32) or $gettok($hget(sentinel.irc,tv),1,32)
    }
    if ( $nick isop $gettok($hget(sentinel.irc,admin),1,32) ) {
    [ .timersentinel. $+ [ $nick ] $+ 7 ] 1 1 notice $nick =close :: Closes server log/rcon interaction with bot but keeps bot available in irc to open. :: Usable in any channel by Ops in $gettok($hget(sentinel.irc,admin),1,32)
    [ .timersentinel. $+ [ $nick ] $+ 8 ] 1 1 notice $nick =open :: Opens server log/rcon interaction with bot. :: Usable in any channel by Ops in $gettok($hget(sentinel.irc,admin),1,32)
    if ( $hget(sentinel.general,allowrcon) == 1 ) { [ .timersentinel. $+ [ $nick ] $+ 9 ] 1 1 notice $nick =rcon <cmd> :: Issues RCON command <cmd> to server. :: Usable in any channel by Ops in $gettok($hget(sentinel.irc,admin),1,32) }
    }
    [ .timersentinel. $+ [ $nick ] $+ 9 ] 1 1 notice $nick Sentinel Bot $hget(sentinel.general,version) :: $hget(sentinel.general,url)
    }
    on *:TEXT:=rcon *:*:{
    if ( $nick !isop $gettok($hget(sentinel.irc,admin),1,32) ) { .timersentinel.denied 1 1 notice $nick Denied: You lack operator status in $gettok($hget(sentinel.irc,admin),1,32) $+ . | return }
    if ( $hget(sentinel.general,allowrcon) != 1 ) { .timersentinel.disabled 1 1 notice $nick Feature Disabled: RCON access not allowed. | return }
    if ( %sentinel.rcon.response != $null ) { [ .timersentinel. $+ [ $nick ] ] 1 1 notice $nick RCON Temporarily Unavailable. Currently in use $iif($left(%sentinel.rcon.response,1) == $chr($asc(#)),in,by) %sentinel.rcon.response $+ . Please try again in a few seconds. }
    set -u10 %sentinel.rcon.response $iif($chan != $null,$chan,$nick)
    sentinel.rcon =rcon $2-
    }
    on *:TEXT:=chat *:#:{
    if ( $chan != $gettok($hget(sentinel.irc,tv),1,32) ) return
    if ( $nick !isop $gettok($hget(sentinel.irc,monitor),1,32) && $nick !isop $gettok($hget(sentinel.irc,tv),1,32) && $nick !isop $gettok($hget(sentinel.irc,admin),1,32) ) return
    var %message = $strip($2-)
    if ( %message == $null ) return
    var %message = $chan $chr($asc(|)) $nick : %message
    sentinel.rcon say %message
    }
    on *:TEXT:=close:*:{ if ( $nick isop $gettok($hget(sentinel.irc,admin),1,32) ) sentinel.server.close }
    on *:TEXT:=open:*:{ if ( $nick isop $gettok($hget(sentinel.irc,admin),1,32) ) sentinel.server.open }
    on *:TEXT:=activate:*:{ if ( $nick isop $gettok($hget(sentinel.irc,admin),1,32) ) sentinel.activate }
    on *:TEXT:=deactivate:*:{ if ( $nick isop $gettok($hget(sentinel.irc,admin),1,32) ) sentinel.deactivate }
    alias sentinel.servers { msg $1  $+ %i  $+ $chr(40) $+  $+ %closed $+  $+ $chr(41) $chr($asc(|)) $+ 1,15 $hget([ server $+ [ %i ] $+ .status ],hostName) $+ $chr(40) $+ $hget([ server $+ [ %i ] ],ip) $+ : $+ $hget([ server $+ [ %i ] ],port) $+ $chr(41)  $+ $chr($asc(|)) $hget([ server $+ [ %i ] ],tv)  $+ $chr($asc(|)) $+ 1,15 $hget([ server $+ [ %i ] $+ .status ],map) $+ $chr(40) $+ $hget([ server $+ [ %i ] $+ .status ],playerCount) $+ $chr($asc(/)) $+ $hget([ server $+ [ %i ] $+ .status ],playerMax) $+ $chr(41) }
    on *:TEXT:=invited:*:{
    echo -s chan: $chan nick: $nick
    if ( $nick !isop $gettok($hget(sentinel.irc,monitor),1,32) && $nick !isop $gettok($hget(sentinel.irc,tv),1,32) && $nick !isop $gettok($hget(sentinel.irc,admin),1,32) ) { return }
    [ .timersentinel.invited. $+ [ $chan ] ] 1 2 sentinel.invited $iif($chan != $null,$chan,$nick)
    }
    alias sentinel.invited {
    if ($hget(sentinel.invited,0).item == 0) {
    msg $1 Nobody has invited me. :~(
    return
    }
    var %i = 1
    while ( %i <= $hget(sentinel.invited,0).item ) {
    var %invited = $hget(sentinel.invited,%i).item $hget(sentinel.invited,%i).data
    msg $1 $gettok(%invited,1,32) $chr($asc(|)) $+ 1,15 $gettok(%invited,3,32)  $+ $chr($asc(|)) $asctime($gettok(%invited,2,32),m/d/yy @ h:mmtt) 
    inc %i
    }
    }
    on *:TEXT:=status:*:{
    if ( $nick !isop $gettok($hget(sentinel.irc,monitor),1,32) && $nick !isop $gettok($hget(sentinel.irc,tv),1,32) && $nick !isop $gettok($hget(sentinel.irc,admin),1,32) ) { return }
    if ( $timer(sentinel.status) == $null ) {
    sentinel.rcon server_game_time
    .timersentinel.rcon.mp_timelimit 1 1 sentinel.rcon mp_timelimit
    .timersentinel.rcon.sm_nextmap 1 1 sentinel.rcon sm_nextmap
    }
    .timersentinel.status 1 3 sentinel.status $iif($chan != $null,$chan,$nick)
    }
    alias sentinel.status {
    if ($hget(sentinel.server.status,sv_visiblemaxplayers) == -1) var %max = $hget(sentinel.server.status,player.max)
    else var %max = $hget(sentinel.server.status,sv_visiblemaxplayers)
    hadd -m sentinel.server.status playersNbots $hget(sentinel.server.status,player.count) $+ / $+ %max players
    if ( $hget(sentinel.server.status,bot.count) > 0 ) { hadd -m sentinel.server.status playersNbots $hget(sentinel.server.status,playersNbots) $chr($asc(|)) + $+ $hget(sentinel.server.status,bot.count) bots }
    if ( $hget(sentinel.server.status,mp_timelimit) = 0 ) var %timeleft = No Time Limit
    else {
    var %timeleft = $calc($hget(sentinel.server.status,mp_timelimit) * 60 - $hget(sentinel.server.status,server_game_time))
    if (%timeleft < 0) var %timeleft = 0
    var %timeleft = $duration(%timeleft) left
    }
    var %status = 14 $+ $hget(sentinel.server.status,hostname) $+  $chr($asc(|)) 5Status: $hget(sentinel.server.status,map)  ::  $hget(sentinel.server.status,playersNbots)  ::  %timeleft
    if ($hget(sentinel.server.status,sm_nextmap) != $null) var %status = %status  :: Next map is " $+ $hget(sentinel.server.status,sm_nextmap) $+ "
    var %status = %status  :: steam://connect/ $+ $hget(sentinel.server,address)
    msg $1 %status
    }
    on *:TEXT:=players*:*:{
    if ( $nick !isop $gettok($hget(sentinel.irc,monitor),1,32) && $nick !isop $gettok($hget(sentinel.irc,tv),1,32) && $nick !isop $gettok($hget(sentinel.irc,admin),1,32) ) { return }
    .timersentinel.players 1 3 sentinel.players $iif($chan != $null,$chan,$nick) $2-
    }
    alias sentinel.players {
    ;--Remove the mapchange status at this point
    unset %sentinel.status

    if ( $hget(sentinel.rcon.status,0).item == 0 ) { msg $1 5Players: None :~( | return }
    if ( $2 != $null ) {
    var %i = 1
    while ( %i <= $hget(sentinel.rcon.status,0).item ) {
    if ( $gettok($hget(sentinel.rcon.status,%i).data,7-,32) == $2- ) {
    tokenize 32 $1 $hget(sentinel.rcon.status,%i).data
    ;--sentinel.rcon.status = ID2 time3 ping4 loss5 adr6 state7 name8-
    var %team = $hget(sentinel.players,$hget(sentinel.rcon.status,%i).item)
    var %pinfo = $8- $+ $chr($asc(<)) $+ 0 $+ $chr($asc(>)) $+ $chr($asc(<)) $+ 0 $+ $chr($asc(>)) $+ $chr($asc(<)) $+ %team $+ $chr($asc(>))
    msg $1 5Player Info: $sentinel.format.player(%pinfo) $+  $chr($asc(|)) $2 $chr($asc(|)) $4 $+ ms $+ $chr(40) $+ loss= $+ $5 $+ $chr(41) $chr($asc(|)) $3 $iif($7 != active,$chr($asc(|)) $7,)
    }
    inc %i
    }
    }
    else {
    var %i = 1
    :nextplayer
    tokenize 32 $1 $hget(sentinel.rcon.status,%i).data
    ;--sentinel.rcon.status = ID2 time3 ping4 loss5 adr6 state7 name8-
    var %team = $hget(sentinel.players,$hget(sentinel.rcon.status,%i).item)
    var %pinfo = $8- $+ $chr($asc(<)) $+ 0 $+ $chr($asc(>)) $+ $chr($asc(<)) $+ 0 $+ $chr($asc(>)) $+ $chr($asc(<)) $+ %team $+ $chr($asc(>))
    var %names = %names " $+ $sentinel.format.player(%pinfo) $+  $+ "
    if ( $gettok($calc(%i / 5),2,$asc(.)) == $null ) {
    msg $1 5Players: %names
    var %names = $null
    }
    inc %i
    if ( $hget(sentinel.rcon.status,%i).data != $null ) {
    if ( %names != $null ) var %names = %names $+ ,
    goto nextplayer
    }
    else {
    if ( %names != $null ) msg $1 5Players: %names
    var %names = $null
    }
    }
    }
    alias sentinel.notify {
    ;--Check individual channel notification rules
    var %i = -1
    while ( %i <= $hget(sentinel.invited,0).item ) {
    if ( %i == -1 ) { var %chan = $gettok($hget(sentinel.irc,monitor),1,32) }
    elseif ( %i == 0 ) { var %chan = $gettok($hget(sentinel.irc,admin),1,32) }
    else { var %chan = $hget(sentinel.invited,%i).item }

    ;--Setup status and channel rule variables
    var %status.map = $hget(sentinel.server.status,map)
    var %status.player.count = $hget(sentinel.server.status,player.count)
    var %status.hostname = $hget(sentinel.server.status,hostname)
    if ($hget(sentinel.server.status,sv_visiblemaxplayers) == -1) var %max = $hget(sentinel.server.status,player.max)
    else var %max = $hget(sentinel.server.status,sv_visiblemaxplayers)
    var %status.player.max = %max

    var %rules.players = $gettok($hget([ sentinel.rules. $+ [ %chan ] ],players),3,32)
    var %rules.maps = $gettok($hget([ sentinel.rules. $+ [ %chan ] ],maps),3-,32)
    var %rules.type = $gettok($hget([ sentinel.rules. $+ [ %chan ] ],type),3,32)
    var %rules.max = $gettok($hget([ sentinel.rules. $+ [ %chan ] ],max),3,32)
    var %rules.min = $gettok($hget([ sentinel.rules. $+ [ %chan ] ],min),3,32)

    var %rules.default.players = $gettok($hget(sentinel.rules.default,players),3,32)
    var %rules.default.maps = $gettok($hget(sentinel.rules.default,maps),3-,32)
    var %rules.default.type = $gettok($hget(sentinel.rules.default,type),3,32)
    var %rules.default.max = $gettok($hget(sentinel.rules.default,max),3,32)
    var %rules.default.min = $gettok($hget(sentinel.rules.default,min),3,32)

    if ( %rules.players == $null ) { var %rules.players = %rules.default.players }
    if ( %rules.maps == $null ) { var %rules.maps = %rules.default.maps }
    if ( %rules.type == $null ) { var %rules.type = %rules.default.type }
    if ( %rules.max == $null ) { var %rules.max = %rules.default.max }
    if ( %rules.min == $null ) { var %rules.min = %rules.default.min }

    ;--Compare status to rules for channel, if a rule is broken, the loop continues on the next channel
    if ( %status.player.count < %rules.players ) { inc %i | continue }
    if ( %rules.type != notify ) { if ( $findtok($lower(%rules.maps),$lower(%status.map),1,32) != $null ) { inc %i | continue } }
    else { if ( $findtok($lower(%rules.maps),$lower(%status.map),1,32) == $null ) { inc %i | continue } }
    var %last.ctime = $hget([ sentinel.notify. $+ [ %chan ] ],last.ctime)
    var %last.player.count = $hget([ sentinel.notify. $+ [ %chan ] ],last.player.count)
    if ( $calc($ctime - %last.ctime) < %rules.min ) { inc %i | continue }
    if ( (%status.players <= %last.player.count) && ($calc($ctime - %last.ctime) < %rules.max) ) { inc %i | continue }

    ;--All rules met with current status, send out the alert
    var %info = 5 $+ %status.map is being played on 12 $+ %status.hostname $+  $chr(40) $hget(sentinel.server,address) $chr(41) 5with %status.player.count $+ $chr($asc(/)) $+ %status.player.max players! Join in on the carnage!!
    msg %chan %info
    hadd -m [ sentinel.notify. $+ [ %chan ] ] last.ctime $ctime
    hadd -m [ sentinel.notify. $+ [ %chan ] ] last.player.count %status.player.count

    inc %i
    }
    }
    dialog sentinel.options {
    title "Sentinel Bot Options"
    size -1 -1 186 168
    option dbu
    tab "General", 29, 1 0 182 149
    check "Automatically activate Sentinel Bot on IRC connection", 9, 6 17 144 10, tab 29
    edit "", 14, 29 37 50 10, tab 29 autohs
    edit "", 18, 128 37 50 10, tab 29 autohs
    edit "", 15, 29 48 50 10, tab 29 autohs
    edit "", 21, 128 48 50 10, tab 29 autohs
    edit "", 16, 29 59 50 10, tab 29 autohs
    edit "", 22, 128 59 50 10, tab 29 autohs
    edit "", 4, 50 85 67 10, tab 29 autohs
    edit "", 5, 50 96 67 10, tab 29 autohs
    edit "", 26, 30 121 138 10, tab 29 autohs
    edit "", 27, 30 132 67 10, tab 29 autohs
    box "IRC Channels", 10, 4 29 178 45, tab 29
    text "Admin:", 11, 7 39 22 8, tab 29 right
    text "Admin Password:", 17, 81 39 47 8, tab 29 right
    text "TV Password:", 19, 81 50 47 8, tab 29 right
    text "TV:", 12, 7 50 22 8, tab 29 right
    text "Monitor:", 13, 7 61 22 8, tab 29 right
    text "Monitor Password:", 20, 81 61 47 8, tab 29 right
    box "Server", 1, 4 77 178 34, tab 29
    text "Address:", 2, 7 87 43 8, tab 29 right
    text "RCON Password:", 3, 7 98 43 8, tab 29 right
    text "Example: 1.2.3.4:27015", 8, 118 87 60 8, disable tab 29
    text "File:", 24, 7 123 23 8, tab 29 right
    text "Address:", 25, 7 134 23 8, tab 29 right
    box "Logging", 23, 4 114 178 33, tab 29
    text "Example: 1.2.3.4:28888", 28, 98 134 60 8, disable tab 29
    button "...", 53, 169 122 9 8, tab 29
    tab "Default Rules", 30
    edit "", 43, 31 18 21 10, tab 30 autohs
    edit "", 42, 31 29 21 10, tab 30 autohs
    edit "", 41, 31 40 21 10, tab 30 autohs
    edit "", 37, 9 63 71 10, tab 30 autohs right
    list 36, 9 73 71 63, tab 30 sort size extsel
    button "Add", 38, 81 63 30 10, tab 30
    button "Remove", 39, 81 74 30 10, tab 30
    radio "Ignore when Maps are played", 40, 89 105 82 10, tab 30
    radio "Notify when Maps are played", 47, 89 116 82 10, tab 30
    text "Minimum number of players, bots are not counted.", 44, 53 20 124 8, tab 30
    text "Shortest time in seconds before bot notifies again.", 45, 53 31 124 8, tab 30
    text "Longest time in seconds before bot notifies again.", 46, 53 42 124 8, tab 30
    box "Maps", 48, 4 53 178 94, tab 30
    text "Players:", 31, 6 20 25 8, tab 30 right
    text "Type:", 33, 88 96 27 8, tab 30
    text "Min:", 34, 6 31 25 8, tab 30 right
    text "Max:", 35, 6 42 25 8, tab 30 right
    text ":Selected", 32, 19 136 23 8, tab 30
    text "0", 50, 7 136 12 8, tab 30 right
    text ":Total", 51, 58 136 15 8, tab 30
    text "0", 52, 45 136 13 8, tab 30 right
    tab "Features", 300
    box "TV Channel Display", 58, 4 17 178 59, tab 300
    check "Player connects and disconnects", 305, 9 26 92 10, tab 300
    check "Player IP and ID information upon connection", 310, 18 38 122 10, tab 300
    check "Team chat (say_team / mm2)", 315, 9 50 158 10, tab 300
    check "Allow RCON access to Ops in Admin channel", 325, 9 81 118 10, tab 300
    tab "Advanced", 59
    radio "Dynamic", 61, 9 26 33 10, tab 59
    radio "Static", 62, 9 38 32 10, tab 59
    box "Port Options", 68, 4 17 178 60, tab 59
    text "Preferrably in the Private Port range of 49152 through 65535", 60, 72 53 78 14, disable tab 59
    text "Log:", 66, 24 61 18 8, disable tab 59 right
    edit "", 64, 42 60 23 10, disable tab 59 limit 5
    link "v0.0.0b - YYYY/MM/DD by EdGruberman", 49, 3 156 98 8
    button "OK", 6, 105 154 37 12, ok
    button "Cancel", 7, 146 154 37 12, default cancel
    }

    on *:DIALOG:sentinel.options:sclick:53:{
    var %folder = $sdir($iif($did(26) != $null,$did(26),.),Select Log File Location)
    did -ra sentinel.options 26 $iif(%folder != $null,%folder,$did(26))
    }
    on *:DIALOG:sentinel.options:sclick:49:{ url -an $hget(sentinel.general,url) }
    on *:DIALOG:sentinel.options:sclick:36:{ sentinel.options.update }
    alias sentinel.options.update {
    did -ra sentinel.options 50 $did(sentinel.options,36,0).sel
    did -ra sentinel.options 52 $did(sentinel.options,36).lines
    }
    on *:DIALOG:sentinel.options:sclick:38:{
    ;--Add entered map to list
    var %map = $replace($did(37),$chr(32),$null)
    if ( %map == $null ) { return }
    did -i sentinel.options 36 1 %map
    did -r sentinel.options 37
    sentinel.options.update
    }
    on *:DIALOG:sentinel.options:sclick:39:{
    ;--Remove selected maps from list
    var %i = $did(36,0).sel
    while ( %i > 0 ) {
    var %sel = $did(36,%i).sel
    did -ra sentinel.options 37 $did(36,%sel)
    did -d sentinel.options 36 %sel
    dec %i
    }
    sentinel.options.update
    }
    on *:DIALOG:sentinel.options:sclick:61:{
    ;--Dynamic ports selected
    did -b sentinel.options 60,64,66
    }
    on *:DIALOG:sentinel.options:sclick:62:{
    ;--Static ports selected
    did -e sentinel.options 60,64,66
    }
    on *:DIALOG:sentinel.options:sclick:305:{
    ;--Connects/Disconnects display toggled
    if ( $did(305).state == 1 ) { did -e sentinel.options 310 }
    else { did -b sentinel.options 310 }
    }
    on *:DIALOG:sentinel.options:init:0:{
    ;--General Tab
    if ( $hget(sentinel.general,autoactivate) == 1 ) { did -c sentinel.options 9 }
    did -ra sentinel.options 14 $gettok($hget(sentinel.irc,admin),1,32)
    did -ra sentinel.options 18 $gettok($hget(sentinel.irc,admin),2,32)
    did -ra sentinel.options 15 $gettok($hget(sentinel.irc,tv),1,32)
    did -ra sentinel.options 21 $gettok($hget(sentinel.irc,tv),2,32)
    did -ra sentinel.options 16 $gettok($hget(sentinel.irc,monitor),1,32)
    did -ra sentinel.options 22 $gettok($hget(sentinel.irc,monitor),2,32)
    did -ra sentinel.options 4 $hget(sentinel.server,address)
    did -ra sentinel.options 5 $hget(sentinel.server,rcon_password)
    did -ra sentinel.options 26 $hget(sentinel.log,file)
    did -ra sentinel.options 27 $hget(sentinel.log,address)

    ;--Default Rules Tab
    didtok sentinel.options 36 32 $gettok($hget(sentinel.rules.default,maps),3-,32)
    sentinel.options.update
    did -ra sentinel.options 43 $gettok($hget(sentinel.rules.default,players),3,32)
    did -ra sentinel.options 42 $gettok($hget(sentinel.rules.default,min),3,32)
    did -ra sentinel.options 41 $gettok($hget(sentinel.rules.default,max),3,32)
    if ( $gettok($hget(sentinel.rules.default,type),3,32) == ignore ) { did -c sentinel.options 40 }
    else { did -c sentinel.options 47 }

    ;--Features Tab
    if ( $hget(sentinel.general,show.connects) == 1 ) { did -c sentinel.options 305 }
    else { did -b sentinel.options 310 }
    if ( $hget(sentinel.general,show.ip) == 1 ) { did -c sentinel.options 310 }
    if ( $hget(sentinel.general,show.sayteam) == 1 ) { did -c sentinel.options 315 }
    if ( $hget(sentinel.general,allowrcon) == 1 ) { did -c sentinel.options 325 }

    ;--Advanced Tab
    if ( $hget(sentinel.general,port.type) == dynamic ) {
    did -c sentinel.options 61
    did -b sentinel.options 60,64,66
    }
    else {
    did -c sentinel.options 62
    did -e sentinel.options 60,64,66
    }
    did -ra sentinel.options 64 $hget(sentinel.general,port.log)

    ;--Link
    did -ra sentinel.options 49 $hget(sentinel.general,version)

    ;--If bot is activated, disable disruptive settings
    if ( $hget(sentinel.general.status,activated) == 1 ) { did -b sentinel.options 14,15,16,4,61,62,64 }
    }
    on *:DIALOG:sentinel.options:sclick:6:{
    ;--OK clicked, update all hash tables and save to ini file
    hadd sentinel.general autoactivate $did(9).state
    hadd sentinel.general show.connects $did(305).state
    hadd sentinel.general show.ip $did(310).state
    hadd sentinel.general show.sayteam $did(315).state
    hadd sentinel.general allowrcon $did(325).state
    hadd sentinel.general port.type $iif($did(62).state == 1,static,dynamic)
    hadd sentinel.general port.log $did(64)
    hsave -i sentinel.general sentinel.ini general

    hadd sentinel.irc admin $did(14) $did(18)
    hadd sentinel.irc tv $did(15) $did(21)
    hadd sentinel.irc monitor $did(16) $did(22)
    hsave -i sentinel.irc sentinel.ini irc

    hadd sentinel.server address $did(4)
    hadd sentinel.server rcon_password $did(5)
    hsave -i sentinel.server sentinel.ini server

    hadd sentinel.log file $did(26)
    hadd sentinel.log address $did(27)
    hsave -i sentinel.log sentinel.ini log

    hadd sentinel.rules.default players $ctime Default $did(43)
    hadd sentinel.rules.default min $ctime Default $did(42)
    hadd sentinel.rules.default max $ctime Default $did(41)
    hadd sentinel.rules.default type $ctime Default $iif($did(40).state == 1,ignore,notify)
    var %i = 1
    hadd sentinel.rules.default maps $ctime Default
    while ( %i <= $did(36).lines ) {
    hadd sentinel.rules.default maps $ctime Default $gettok($hget(sentinel.rules.default,maps),3-,32) $did(36,%i)
    inc %i
    }
    hsave -i sentinel.rules.default sentinel.ini rules.default
    }
    menu status,channel,menubar {
    Sentinel Bot
    .$iif($status != connected || $hget(sentinel.general.status,activated) == 1,$style(2)) Activate:sentinel.activate
    .$iif($hget(sentinel.general.status,activated) != 1,$style(2)) Deactivate:sentinel.deactivate
    .-
    .Options...:{ if ( $dialog(sentinel.options).hwnd == $null ) { dialog -am sentinel.options sentinel.options } | dialog -v sentinel.options }
    }
    alias debug.binvar {
    ;--This is a generic routine that will output the contents of a binary variable to the status window
    ;--much like a network packet analyzer commonly displays such binary information.

    var %header = _OFFSET__00 01 02 03 04 05 06 07___08 09 0A 0B 0C 0D 0E 0F___01234567 89ABCDEF
    echo -s $str(_,$len(%header))
    echo -s $timestamp $2-
    echo -s %header

    var %offset = 0
    var %i = 1
    while (%i <= $bvar($1,0)) {
    var %line = %line $+ $chr(32) $+ $base($bvar($1,%i),10,16,2)

    var %char = $bvar($1,%i).text
    if (($bvar($1,%i) < 33) || ($bvar($1,%i) > 126)) var %char = .
    if ($calc((%i - 9) % 8) == 0) {
    var %text = %text $+ $chr(32) $+ %char
    }
    else { var %text = %text $+ %char }

    if (($calc(%i % 8) == 0) && ($calc(%i % 16) != 0)) { var %line = %line $+ $chr(32) $+ _ }

    if (($calc(%i % 16) == 0) || (%i == $bvar($1,0))) {
    if ((%i = $bvar($1,0)) && ($calc($bvar($1,0) % 16) > 0)) {
    var %line = %line $+ $str($chr(32) $+ __,$calc(16 - ($bvar($1,0) % 16) - 8))
    if ($calc(16 - ($bvar($1,0) % 16) - 8) > 0) var %line = %line _
    var %ext = $calc(16 - ($bvar($1,0) % 16))
    if ( %ext > 8) var %ext = 8
    var %line = %line $str($chr(32) $+ __,%ext)
    }
    echo -s $base(%offset,10,16,8) %line _ %text
    var %line, %text
    var %offset = %offset + 16
    }
    inc %i
    }

    echo -s $str(¯,$len(%header))
    }