#!/usr/bin/perl

our $VERSION = '0.4.2 svn $Revision$';

# Copyright (c) 2008 Rudolf "divVerent" Polzer
# 
# Permission is hereby granted, free of charge, to any person
# obtaining a copy of this software and associated documentation
# files (the "Software"), to deal in the Software without
# restriction, including without limitation the rights to use,
# copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the
# Software is furnished to do so, subject to the following
# conditions:
# 
# The above copyright notice and this permission notice shall be
# included in all copies or substantial portions of the Software.
# 
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
# OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
# HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
# WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
# OTHER DEALINGS IN THE SOFTWARE.

# Interfaces:
#   Connection:
#     $conn->sockname() returns a connection type specific representation
#       string of the local address, or undef if not applicable.
#     $conn->send("string") sends something over the connection.
#     $conn->recv() receives a string from the connection, or returns "" if no
#       data is available.
#     $conn->fds() returns all file descriptors used by the connection, so one
#       can use select() on them.
#   Channel:
#     Usually wraps around a connection and implements a command based
#     structure over it. It usually is constructed using new
#     ChannelType($connection, someparameters...)
#     @cmds = $chan->join_commands(@cmds) joins multiple commands to a single
#       command string if the protocol supports it, or does nothing and leaves
#       @cmds unchanged if the protocol does not support that usage (this is
#       meant to save send() invocations).
#     $chan->send($command, $nothrottle) sends a command over the channel. If
#       $nothrottle is sent, the command must not be left out even if the channel
#       is saturated (for example, because of IRC's flood control mechanism).
#     $chan->quote($str) returns a string in a quoted form so it can safely be
#       inserted as a substring into a command, or returns $str as is if not
#       applicable. It is assumed that the result of the quote method is used
#       as part of a quoted string, if the protocol supports that.
#     $chan->recv() returns a list of received commands from the channel, or
#       the empty list if none are available.
#     $conn->fds() returns all file descriptors used by the channel's
#       connections, so one can use select() on them.







# Socket connection.
# Represents a connection over a socket.
# Mainly used to wrap a channel around it for, in this case, line based or rcon-like operation.
package Connection::Socket;
use strict;
use warnings;
use IO::Socket::INET;
use IO::Handle;

# Constructor:
#   my $conn = new Connection::Socket(tcp => "localaddress" => "remoteaddress" => 6667);
# If the remote address does not contain a port number, the numeric port is
# used (it serves as a default port).
sub new($$)
{
	my ($class, $proto, $local, $remote, $defaultport) = @_;
	my $sock = IO::Socket::INET->new(
		Proto => $proto,
		(length($local) ? (LocalAddr => $local) : ()),
		PeerAddr => $remote,
		PeerPort => $defaultport
	) or die "socket $proto/$local/$remote: $!";
	$sock->blocking(0);
	my $you = {
		# Mortal fool! Release me from this wretched tomb! I must be set free
		# or I will haunt you forever! I will hide your keys beneath the
		# cushions of your upholstered furniture... and NEVERMORE will you be
		# able to find socks that match!
		sock => $sock,
		# My demonic powers have made me OMNIPOTENT! Bwahahahahahahaha!
	};
	return
		bless $you, 'Connection::Socket';
}

# $sock->sockname() returns the local address of the socket.
sub sockname($)
{
	my ($self) = @_;
	my ($port, $addr) = sockaddr_in $self->{sock}->sockname();
	return "@{[inet_ntoa $addr]}:$port";
}

# $sock->send($data) sends some data over the socket; on success, 1 is returned.
sub send($$)
{
	my ($self, $data) = @_;
	return 1
		if not length $data;
	if(not eval { $self->{sock}->send($data); })
	{
		warn "$@";
		return 0;
	}
	return 1;
}

# $sock->recv() receives as much as possible from the socket (or at most 32k). Returns "" if no data is available.
sub recv($)
{
	my ($self) = @_;
	my $data = "";
	if(defined $self->{sock}->recv($data, 32768, 0))
	{
		return $data;
	}
	elsif($!{EAGAIN})
	{
		return "";
	}
	else
	{
		return undef;
	}
}

# $sock->fds() returns the socket file descriptor.
sub fds($)
{
	my ($self) = @_;
	return fileno $self->{sock};
}







# Line-based buffered connectionless FIFO channel.
# Whatever is sent to it using send() is echoed back when using recv().
package Channel::FIFO;
use strict;
use warnings;

# Constructor:
#   my $chan = new Channel::FIFO();
sub new($)
{
	my ($class) = @_;
	my $you = {
		buffer => []
	};
	return
		bless $you, 'Channel::FIFO';
}

sub join_commands($@)
{
	my ($self, @data) = @_;
	return @data;
}

sub send($$$)
{
	my ($self, $line, $nothrottle) = @_;
	push @{$self->{buffer}}, $line;
}

sub quote($$)
{
	my ($self, $data) = @_;
	return $data;
}

sub recv($)
{
	my ($self) = @_;
	my $r = $self->{buffer};
	$self->{buffer} = [];
	return @$r;
}

sub fds($)
{
	my ($self) = @_;
	return ();
}







# QW rcon protocol channel.
# Wraps around a UDP based Connection and sends commands as rcon commands as
# well as receives rcon replies. The quote and join_commands methods are using
# DarkPlaces engine specific rcon protocol extensions.
package Channel::QW;
use strict;
use warnings;

# Constructor:
#   my $chan = new Channel::QW($connection, "password");
sub new($$)
{
	my ($class, $conn, $password) = @_;
	my $you = {
		connector => $conn,
		password => $password,
		recvbuf => "",
	};
	return
		bless $you, 'Channel::QW';
}

# Note: multiple commands in one rcon packet is a DarkPlaces extension.
sub join_commands($@)
{
	my ($self, @data) = @_;
	return join "\0", @data;
}

sub send($$$)
{
	my ($self, $line, $nothrottle) = @_;
	return $self->{connector}->send("\377\377\377\377rcon $self->{password} $line");
}

# Note: backslash and quotation mark escaping is a DarkPlaces extension.
sub quote($$)
{
	my ($self, $data) = @_;
	$data =~ s/[\000-\037]//g;
	$data =~ s/([\\"])/\\$1/g;
	$data =~ s/\$/\$\$/g;
	return $data;
}

sub recv($)
{
	my ($self) = @_;
	for(;;)
	{
		my $s = $self->{connector}->recv();
		die "read error\n"
			if not defined $s;
		length $s
			or last;
		next
			if $s !~ /^\377\377\377\377n(.*)$/s;
		$self->{recvbuf} .= $1;
	}
	my @out = ();
	while($self->{recvbuf} =~ s/^(.*?)(?:\r\n?|\n)//)
	{
		push @out, $1;
	}
	return @out;
}

sub fds($)
{
	my ($self) = @_;
	return $self->{connector}->fds();
}







# Line based protocol channel.
# Wraps around a TCP based Connection and sends commands as text lines
# (separated by CRLF). When reading responses from the Connection, any type of
# line ending is accepted.
# A flood control mechanism is implemented.
package Channel::Line;
use strict;
use warnings;
use Time::HiRes qw/time/;

# Constructor:
#   my $chan = new Channel::Line($connection);
sub new($$)
{
	my ($class, $conn) = @_;
	my $you = {
		connector => $conn,
		recvbuf => "",
		capacity => undef,
		linepersec => undef,
		maxlines => undef,
		lastsend => time()
	};
	return 
		bless $you, 'Channel::Line';
}

sub join_commands($@)
{
	my ($self, @data) = @_;
	return @data;
}

# Sets new flood control parameters:
#   $chan->throttle(maximum lines per second, maximum burst length allowed to
#     exceed the lines per second limit);
#   RFC 1459 describes these parameters to be 0.5 and 5 for the IRC protocol.
#   If the $nothrottle flag is set while sending, the line is sent anyway even
#   if flooding would take place.
sub throttle($$$)
{
	my ($self, $linepersec, $maxlines) = @_;
	$self->{linepersec} = $linepersec;
	$self->{maxlines} = $maxlines;
	$self->{capacity} = $maxlines;
}

sub send($$$)
{
	my ($self, $line, $nothrottle) = @_;
	my $t = time();
	if(defined $self->{capacity})
	{
		$self->{capacity} += ($t - $self->{lastsend}) * $self->{linepersec};
		$self->{lastsend} = $t;
		$self->{capacity} = $self->{maxlines}
			if $self->{capacity} > $self->{maxlines};
		if(!$nothrottle)
		{
			return -1
				if $self->{capacity} < 0;
		}
		$self->{capacity} -= 1;
	}
	$line =~ s/\r|\n//g;
	return $self->{connector}->send("$line\r\n");
}

sub quote($$)
{
	my ($self, $data) = @_;
	$data =~ s/\r\n?/\n/g;
	$data =~ s/\n/*/g;
	return $data;
}

sub recv($)
{
	my ($self) = @_;
	for(;;)
	{
		my $s = $self->{connector}->recv();
		die "read error\n"
			if not defined $s;
		length $s
			or last;
		$self->{recvbuf} .= $s;
	}
	my @out = ();
	while($self->{recvbuf} =~ s/^(.*?)(?:\r\n?|\n)//)
	{
		push @out, $1;
	}
	return @out;
}

sub fds($)
{
	my ($self) = @_;
	return $self->{connector}->fds();
}






# main program... a gateway between IRC and DarkPlaces servers
package main;

use strict;
use warnings;
use IO::Select;
use Digest::MD5;
use Time::HiRes qw/time/;

our @handlers = (); # list of [channel, expression, sub to handle result]
our @tasks = (); # list of [time, sub]
our %channels = ();
our %store = (
	irc_nick => "",
	playernick_0 => "(console)",
);
our %config = (
	irc_server => undef,
	irc_nick => undef,
	irc_user => undef,
	irc_channel => undef,
	irc_ping_delay => 120,

	irc_nickserv_password => "",
	irc_nickserv_identify => 'PRIVMSG NickServ :IDENTIFY %2$s',
	irc_nickserv_ghost => 'PRIVMSG NickServ :GHOST %1$s %2$s',
	irc_nickserv_ghost_attempts => 3,

	irc_quakenet_authname => "",
	irc_quakenet_password => "",
	irc_quakenet_getchallenge => 'PRIVMSG Q@CServe.quakenet.org :CHALLENGE',
	irc_quakenet_challengeauth => 'PRIVMSG Q@CServe.quakenet.org :CHALLENGEAUTH',
	irc_quakenet_challengeprefix => ':Q!TheQBot@CServe.quakenet.org NOTICE [^:]+ :CHALLENGE',

	dp_server => undef,
	dp_listen => "", 
	dp_password => undef,
	dp_status_delay => 30,
	dp_server_from_wan => "",

	plugins => "",
);



# MISC STRING UTILITY ROUTINES to convert between DarkPlaces and IRC conventions

# convert mIRC color codes to DP color codes
our @color_irc2dp_table = (7, 0, 4, 2, 1, 1, 6, 1, 3, 2, 5, 5, 4, 6, 7, 7);
our @color_dp2irc_table = (14, 4, 9, 8, 12, 11, 13, 14, 15, 15); # not accurate, but legible
our @color_dp2ansi_table = ("m", "1;31m", "1;32m", "1;33m", "1;34m", "1;36m", "1;35m", "m", "1m", "1m"); # not accurate, but legible
our %color_team2dp_table = (5 => 1, 14 => 4, 13 => 3, 10 => 6);
our %color_team2irc_table = (5 => 4, 14 => 12, 13 => 8, 10 => 13);
sub color_irc2dp($)
{
	my ($message) = @_;
	$message =~ s/\^/^^/g;
	my $color = 7;
	$message =~ s{\003(\d\d?)(?:,(\d?\d?))?|(\017)}{
		# $1 is FG, $2 is BG, but let's ignore BG
		my $oldcolor = $color;
		if($3)
		{
			$color = 7;
		}
		else
		{
			$color = $color_irc2dp_table[$1];
			$color = $oldcolor if not defined $color;
		}
		($color == $oldcolor) ? '' : '^' . $color;
	}esg;
	$message =~ s{[\000-\037]}{}gs; # kill bold etc. for now
	return $message;
}

our @text_qfont_table = ( # ripped from DP console.c qfont_table
    "\0", '#',  '#',  '#',  '#',  '.',  '#',  '#',
    '#',  9,    10,   '#',  ' ',  13,   '.',  '.',
    '[',  ']',  '0',  '1',  '2',  '3',  '4',  '5',
    '6',  '7',  '8',  '9',  '.',  '<',  '=',  '>',
    ' ',  '!',  '"',  '#',  '$',  '%',  '&',  '\'',
    '(',  ')',  '*',  '+',  ',',  '-',  '.',  '/',
    '0',  '1',  '2',  '3',  '4',  '5',  '6',  '7',
    '8',  '9',  ':',  ';',  '<',  '=',  '>',  '?',
    '@',  'A',  'B',  'C',  'D',  'E',  'F',  'G',
    'H',  'I',  'J',  'K',  'L',  'M',  'N',  'O',
    'P',  'Q',  'R',  'S',  'T',  'U',  'V',  'W',
    'X',  'Y',  'Z',  '[',  '\\', ']',  '^',  '_',
    '`',  'a',  'b',  'c',  'd',  'e',  'f',  'g',
    'h',  'i',  'j',  'k',  'l',  'm',  'n',  'o',
    'p',  'q',  'r',  's',  't',  'u',  'v',  'w',
    'x',  'y',  'z',  '{',  '|',  '}',  '~',  '<',
    '<',  '=',  '>',  '#',  '#',  '.',  '#',  '#',
    '#',  '#',  ' ',  '#',  ' ',  '>',  '.',  '.',
    '[',  ']',  '0',  '1',  '2',  '3',  '4',  '5',
    '6',  '7',  '8',  '9',  '.',  '<',  '=',  '>',
    ' ',  '!',  '"',  '#',  '$',  '%',  '&',  '\'',
    '(',  ')',  '*',  '+',  ',',  '-',  '.',  '/',
    '0',  '1',  '2',  '3',  '4',  '5',  '6',  '7',
    '8',  '9',  ':',  ';',  '<',  '=',  '>',  '?',
    '@',  'A',  'B',  'C',  'D',  'E',  'F',  'G',
    'H',  'I',  'J',  'K',  'L',  'M',  'N',  'O',
    'P',  'Q',  'R',  'S',  'T',  'U',  'V',  'W',
    'X',  'Y',  'Z',  '[',  '\\', ']',  '^',  '_',
    '`',  'a',  'b',  'c',  'd',  'e',  'f',  'g',
    'h',  'i',  'j',  'k',  'l',  'm',  'n',  'o',
    'p',  'q',  'r',  's',  't',  'u',  'v',  'w',
    'x',  'y',  'z',  '{',  '|',  '}',  '~',  '<'
);
sub text_dp2ascii($)
{
	my ($message) = @_;
	$message = join '', map { $text_qfont_table[ord $_] } split //, $message;
}

sub color_dp2none($)
{
	my ($message) = @_;
	my $color = -1;
	$message =~ s{\^(.)(?=([0-9,]?))}{
		my $c = $1;
		$c eq '^' ? '^' :
		$c =~ /^[0-9]$/ ? '' : "^$c";
	}esg;
	return text_dp2ascii $message;
}

sub color_dp2irc($)
{
	my ($message) = @_;
	my $color = -1;
	$message =~ s{\^(.)(?=([0-9,]?))}{
		my $c = $1;
		my $f = $2;
		$c eq '^' ? '^' :
		$c =~ /^[0-9]$/ ? do {
			my $oldcolor = $color;
			$c = 0 if $c >= 7; # map 0, 7, 8, 9 to default (no bright white or such stuff)
			$color = $color_dp2irc_table[$c];
			($color == $oldcolor) ? '' :
			$c == 0 ? "\0001" :
			$f eq ',' ? "\0003$color\0002\0002" :
			$f ne ''  ? sprintf "\0003%02d", $color : "\0003$color";
		} : "^$c";
	}esg;
	$message = text_dp2ascii $message;
	$message =~ s/\0001/\017/g;
	$message =~ s/\0002/\002/g;
	$message =~ s/\0003/\003/g;
	return $message;
}

sub color_dp2ansi($)
{
	my ($message) = @_;
	my $color = -1;
	$message =~ s{\^(.)}{
		my $c = $1;
		$c eq '^' ? '^' :
		$c =~ /^[0-9]$/ ? do {
			my $oldcolor = $color;
			$color = $color_dp2ansi_table[$c];
			($color eq $oldcolor) ? '' :
			"\000[${color}" # "
		} : "^$c";
	}esg;
	$message = text_dp2ascii $message;
	$message =~ s/\000/\033/g;
	return $message;
}

sub color_dpfix($)
{
	my ($message) = @_;
	# if the message ends with an odd number of ^, kill one
	chop $message if $message =~ /(?:^|[^\^])\^(\^\^)*$/;
	return $message;
}



# Nexuiz specific parsing of some server messages

sub nex_is_teamplay($)
{
	my ($map) = @_;
	return $map =~ /^(?:kh|ctf|tdm|dom)_/;
}

sub nex_slotsstring()
{
	my $slotsstr = "";
	if(defined $store{slots_max})
	{
		my $slots = $store{slots_max} - $store{slots_active};
		my $slots_s = ($slots == 1) ? '' : 's';
		$slotsstr = " ($slots free slot$slots_s)";
		my $s = $config{dp_server_from_wan} || $config{dp_server};
		$slotsstr .= "; join now: \002nexuiz +connect $s"
			if $slots >= 1 and not $store{lms_blocked};
	}
	return $slotsstr;
}



# Do we have a config file? If yes, read and parse it (syntax: key = value
# pairs, separated by newlines), if not, complain.
die "Usage: $0 configfile\n"
	unless @ARGV == 1;

open my $fh, "<", $ARGV[0]
	or die "open $ARGV[0]: $!";
while(<$fh>)
{
	chomp;
	/^#/ and next;
	/^(.*?)\s+=(?:\s+(.*))?$/ or next;
	warn "Undefined config item: $1"
		unless exists $config{$1};
	$config{$1} = defined $2 ? $2 : "";
}
close $fh;
my @missing = grep { !defined $config{$_} } keys %config;
die "The following config items are missing: @missing"
	if @missing;



# Create a channel for error messages and other internal status messages...

$channels{system} = new Channel::FIFO();

# for example, quit messages caused by signals (if SIGTERM or SIGINT is first
# received, try to shut down cleanly, and if such a signal is received a second
# time, just exit)
my $quitting = 0;
$SIG{INT} = sub {
	exit 1 if $quitting++;
	$channels{system}->send("quit SIGINT");
};
$SIG{TERM} = sub {
	exit 1 if $quitting++;
	$channels{system}->send("quit SIGTERM");
};



# Create the two channels to gateway between...

$channels{irc} = new Channel::Line(new Connection::Socket(tcp => "" => $config{irc_server} => 6667));
$channels{dp} = new Channel::QW(my $dpsock = new Connection::Socket(udp => $config{dp_listen} => $config{dp_server} => 26000), $config{dp_password});
$config{dp_listen} = $dpsock->sockname();
print "Listening on $config{dp_listen}\n";

$channels{irc}->throttle(0.5, 5);


# Utility routine to write to a channel by name, also outputting what's been written and some status
sub out($$@)
{
	my $chanstr = shift;
	my $nothrottle = shift;
	my $chan = $channels{$chanstr};
	if(!$chan)
	{
		print "UNDEFINED: $chanstr, ignoring message\n";
		return;
	}
	@_ = $chan->join_commands(@_);
	for(@_)
	{
		my $result = $chan->send($_, $nothrottle);
		if($result > 0)
		{
			print "           $chanstr << $_\n";
		}
		elsif($result < 0)
		{
			print "FLOOD:     $chanstr << $_\n";
		}
		else
		{
			print "ERROR:     $chanstr << $_\n";
			$channels{system}->send("error $chanstr", 0);
		}
	}
}



# Schedule a task for later execution by the main loop; usage: schedule sub {
# task... }, $time; When a scheduled task is run, a reference to the task's own
# sub is passed as first argument; that way, the task is able to re-schedule
# itself so it gets periodically executed.
sub schedule($$)
{
	my ($sub, $time) = @_;
	push @tasks, [time() + $time, $sub];
}

# On IRC error, delete some data store variables of the connection, and
# reconnect to the IRC server soon (but only if someone is actually playing)
sub irc_error()
{
	# prevent multiple instances of this timer
	return if $store{irc_error_active};
	$store{irc_error_active} = 1;

	delete $channels{irc};
	schedule sub {
		my ($timer) = @_;
		if(!defined $store{slots_full})
		{
			# DP is not running, then delay IRC reconnecting
			#use Data::Dumper; print Dumper \$timer;
			schedule $timer => 1;;
			return;
			# this will keep irc_error_active
		}
		$channels{irc} = new Channel::Line(new Connection::Socket(tcp => "" => $config{irc_server}));
		delete $store{$_} for grep { /^irc_/ } keys %store;
		$store{irc_nick} = "";
		schedule sub {
			my ($timer) = @_;
			out dp => 0, 'status', 'log_dest_udp';
		} => 1;
		# this will clear irc_error_active
	} => 30;
	return 0;
}

# IRC joining (if this is called as response to a nick name collision, $is433 is set);
# among other stuff, it performs NickServ or Quakenet authentication. This is to be called
# until the channel has been joined for every message that may be "interesting" (basically,
# IRC 001 hello messages, 443 nick collision messages and some notices by services).
sub irc_joinstage($)
{
	my($is433) = @_;

	return 0
		if $store{irc_joined_channel};
	
		#use Data::Dumper; print Dumper \%store;

	if($is433)
	{
		if(length $store{irc_nick})
		{
			# we already have another nick, but couldn't change to the new one
			# try ghosting and then get the nick again
			if(length $config{irc_nickserv_password})
			{
				if(++$store{irc_nickserv_ghost_attempts} <= $config{irc_nickserv_ghost_attempts})
				{
					$store{irc_nick_requested} = $config{irc_nick};
					out irc => 1, sprintf($config{irc_nickserv_ghost}, $config{irc_nick}, $config{irc_nickserv_password});
					schedule sub {
						out irc => 1, "NICK $config{irc_nick}";
					} => 1;
					return; # we'll get here again for the NICK success message, or for a 433 failure
				}
				# otherwise, we failed to ghost and will continue with the wrong
				# nick... also, no need to try to identify here
			}
			# otherwise, we can't handle this and will continue with our wrong nick
		}
		else
		{
			# we failed to get an initial nickname
			# change ours a bit and try again
			if(length $store{irc_nick_requested} < 9)
			{
				$store{irc_nick_requested} .= '_';
			}
			else
			{
				substr $store{irc_nick_requested}, int(rand length $store{irc_nick_requested}), 1, chr(97 + int rand 26);
			}
			out irc => 1, "NICK $store{irc_nick_requested}";
			return; # when it fails, we'll get here again, and when it succeeds, we will continue
		}
	}

	# we got a 001 or a NICK message, so $store{irc_nick} has been updated
	if(length $config{irc_nickserv_password})
	{
		if($store{irc_nick} eq $config{irc_nick})
		{
			# identify
			out irc => 1, sprintf($config{irc_nickserv_identify}, $config{irc_nick}, $config{irc_nickserv_password});
		}
		else
		{
			# ghost
			if(++$store{irc_nickserv_ghost_attempts} <= $config{irc_nickserv_ghost_attempts})
			{
				$store{irc_nick_requested} = $config{irc_nick};
				out irc => 1, sprintf($config{irc_nickserv_ghost}, $config{irc_nick}, $config{irc_nickserv_password});
				schedule sub {
					out irc => 1, "NICK $config{irc_nick}";
				} => 1;
				return; # we'll get here again for the NICK success message, or for a 433 failure
			}
			# otherwise, we failed to ghost and will continue with the wrong
			# nick... also, no need to try to identify here
		}
	}

	# we are on Quakenet. Try to authenticate.
	if(length $config{irc_quakenet_password} and length $config{irc_quakenet_authname})
	{
		if(defined $store{irc_quakenet_challenge})
		{
			if($store{irc_quakenet_challenge} =~ /^MD5 (.*)/)
			{
				out irc => 1, "$config{irc_quakenet_challengeauth} $config{irc_quakenet_authname} " . Digest::MD5::md5_hex("$config{irc_quakenet_password} $1");
			}
		}
		else
		{
			out irc => 1, $config{irc_quakenet_getchallenge};
			return;
			# we get here again when Q asks us
		}
	}
	
	# if we get here, we are on IRC
	$store{irc_joined_channel} = 1;
	schedule sub {
		out irc => 1, "JOIN $config{irc_channel}";
	} => 1;
	return 0;
}



# List of all handlers on the various sockets. Additional handlers can be added by a plugin.
@handlers = (
	# detect a server restart and set it up again
	[ dp => q{ *(?:Warning: Could not expand \$|Unknown command ")(?:rcon2irc_[a-z0-9_]*)[" ]*} => sub {
		out dp => 0,
			'alias rcon2irc_eval "$*"',
			'log_dest_udp',
			'sv_logscores_console 0',
			'sv_logscores_bots 1',
			'sv_eventlog 1',
			'sv_eventlog_console 1',
			'alias rcon2irc_say_as "set say_as_restorenick \"$sv_adminnick\"; sv_adminnick \"$1^3\"; say \"^7$2\"; rcon2irc_say_as_restore"',
			'alias rcon2irc_say_as_restore "set sv_adminnick \"$say_as_restorenick\""',
			'alias rcon2irc_quit "echo \"quitting rcon2irc $1: log_dest_udp is $log_dest_udp\""'; # note: \\\\\\" ->perl \\\" ->console \"
		return 0;
	} ],

	# detect missing entry in log_dest_udp and fix it
	[ dp => q{"log_dest_udp" is "([^"]*)" \["[^"]*"\]} => sub {
		my ($dest) = @_;
		my @dests = split ' ', $dest;
		return 0 if grep { $_ eq $config{dp_listen} } @dests;
		out dp => 0, 'log_dest_udp "' . join(" ", @dests, $config{dp_listen}) . '"';
		return 0;
	} ],

	# retrieve hostname from status replies
	[ dp => q{host:     (.*)} => sub {
		my ($name) = @_;
		$store{dp_hostname} = $name;
		return 0;
	} ],

	# retrieve version from status replies
	[ dp => q{version:  (.*)} => sub {
		my ($version) = @_;
		$store{dp_version} = $version;
		return 0;
	} ],

	# retrieve number of open player slots
	[ dp => q{players:  (\d+) active \((\d+) max\)} => sub {
		my ($active, $max) = @_;
		my $full = ($active >= $max);
		$store{slots_max} = $max;
		$store{slots_active} = $active;
		if($full != ($store{slots_full} || 0))
		{
			$store{slots_full} = $full;
			return 0
				if $store{lms_blocked};
			if($full)
			{
				out irc => 0, "PRIVMSG $config{irc_channel} :\001ACTION is full!\001";
			}
			else
			{
				my $slotsstr = nex_slotsstring();
				out irc => 0, "PRIVMSG $config{irc_channel} :\001ACTION can be joined again$slotsstr!\001";
			}
		}
		return 0;
	} ],

	# LMS: detect "no more lives" message
	[ dp => q{\^4.*\^4 has no more lives left} => sub {
		if(!$store{lms_blocked})
		{
			$store{lms_blocked} = 1;
			if(!$store{slots_full})
			{
				schedule sub {
					if($store{lms_blocked})
					{
						out irc => 0, "PRIVMSG $config{irc_channel} :\001ACTION can't be joined until next round (a player has no more lives left)\001";
					}
				} => 1;
			}
		}
	} ],

	# detect IRC errors and reconnect
	[ irc => q{ERROR .*} => \&irc_error ],
	[ system => q{error irc} => \&irc_error ],

	# IRC nick in use
	[ irc => q{:[^ ]* 433 .*} => sub {
		return irc_joinstage(433);
	} ],

	# IRC welcome
	[ irc => q{:[^ ]* 001 .*} => sub {
		$store{irc_seen_welcome} = 1;
		$store{irc_nick} = $store{irc_nick_requested};
		return irc_joinstage(0);
	} ],

	# IRC my nickname changed
	[ irc => q{:(?i:(??{$store{irc_nick}}))![^ ]* (?i:NICK) :(.*)} => sub {
		my ($n) = @_;
		$store{irc_nick} = $n;
		return irc_joinstage(0);
	} ],

	# Quakenet: challenge from Q
	[ irc => q{(??{$config{irc_quakenet_challengeprefix}}) (.*)} => sub {
		$store{irc_quakenet_challenge} = $1;
		return irc_joinstage(0);
	} ],

	# shut down everything on SIGINT
	[ system => q{quit (.*)} => sub {
		my ($cause) = @_;
		out irc => 1, "QUIT :$cause";
		$store{quitcookie} = int rand 1000000000;
		out dp => 0, "rcon2irc_quit $store{quitcookie}";
	} ],

	# remove myself from the log destinations and exit everything
	[ dp => q{quitting rcon2irc (??{$store{quitcookie}}): log_dest_udp is (.*) *} => sub {
		my ($dest) = @_;
		my @dests = grep { $_ ne $config{dp_listen} } split ' ', $dest;
		out dp => 0, 'log_dest_udp "' . join(" ", @dests) . '"';
		exit 0;
		return 0;
	} ],

	# IRC PING
	[ irc => q{PING (.*)} => sub {
		my ($data) = @_;
		out irc => 1, "PONG $data";
		return 1;
	} ],

	# IRC PONG
	[ irc => q{:[^ ]* PONG .* :(.*)} => sub {
		my ($data) = @_;
		return 0
			if not defined $store{irc_pingtime};
		return 0
			if $data ne $store{irc_pingtime};
		print "* measured IRC line delay: @{[time() - $store{irc_pingtime}]}\n";
		undef $store{irc_pingtime};
		return 0;
	} ],

	# detect channel join message and note hostname length to get the maximum allowed line length
	[ irc => q{(:(?i:(??{$store{irc_nick}}))![^ ]* )(?i:JOIN) :(?i:(??{$config{irc_channel}}))} => sub {
		$store{irc_maxlen} = 510 - length($1);
		$store{irc_joined_channel} = 1;
		print "* detected maximum line length for channel messages: $store{irc_maxlen}\n";
		return 0;
	} ],

	# chat: Nexuiz server -> IRC channel
	[ dp => q{\001(.*?)\^7: (.*)} => sub {
		my ($nick, $message) = map { color_dp2irc $_ } @_;
		out irc => 0, "PRIVMSG $config{irc_channel} :<$nick\017> $message";
		return 0;
	} ],

	# chat: Nexuiz server -> IRC channel, nick set
	[ dp => q{:join:(\d+):(?:player|bot):(.*)} => sub {
		my ($id, $nick) = @_;
		$nick = color_dp2irc $nick;
		$store{"playernick_$id"} = $nick;
		return 0;
	} ],

	# chat: Nexuiz server -> IRC channel, nick change/set
	[ dp => q{:name:(\d+):(.*)} => sub {
		my ($id, $nick) = @_;
		$nick = color_dp2irc $nick;
		my $oldnick = $store{"playernick_$id"};
		out irc => 0, "PRIVMSG $config{irc_channel} :* $oldnick\017 is now known as $nick";
		$store{"playernick_$id"} = $nick;
		return 0;
	} ],

	# chat: Nexuiz server -> IRC channel, vote call
	[ dp => q{:vote:vcall:(\d+):(.*)} => sub {
		my ($id, $command) = @_;
		$command = color_dp2irc $command;
		my $oldnick = $store{"playernick_$id"};
		out irc => 0, "PRIVMSG $config{irc_channel} :* $oldnick\017 calls a vote for \"$command\017\"";
		return 0;
	} ],

	# chat: Nexuiz server -> IRC channel, vote stop
	[ dp => q{:vote:vstop:(\d+)} => sub {
		my ($id) = @_;
		my $oldnick = $store{"playernick_$id"};
		out irc => 0, "PRIVMSG $config{irc_channel} :* $oldnick\017 stopped the vote";
		return 0;
	} ],

	# chat: Nexuiz server -> IRC channel, master login
	[ dp => q{:vote:vlogin:(\d+)} => sub {
		my ($id) = @_;
		my $oldnick = $store{"playernick_$id"};
		out irc => 0, "PRIVMSG $config{irc_channel} :* $oldnick\017 logged in as master";
		return 0;
	} ],

	# chat: Nexuiz server -> IRC channel, master do
	[ dp => q{:vote:vdo:(\d+):(.*)} => sub {
		my ($id, $command) = @_;
		my $oldnick = $store{"playernick_$id"};
		out irc => 0, "PRIVMSG $config{irc_channel} :* $oldnick\017 used his master status to do \"$command\017\"";
		return 0;
	} ],

	# chat: Nexuiz server -> IRC channel, result
	[ dp => q{:vote:v(yes|no|timeout):(\d+):(\d+):(\d+):(\d+):(-?\d+)} => sub {
		my ($result, $yes, $no, $abstain, $not, $min) = @_;
		my $spam = "$yes:$no" . (($min >= 0) ? " ($min needed)" : "") . ", $abstain didn't care, $not didn't vote";
		out irc => 0, "PRIVMSG $config{irc_channel} :* the vote ended with $result: $spam";
		return 0;
	} ],

	# chat: IRC channel -> Nexuiz server
	[ irc => q{:([^! ]*)![^ ]* (?i:PRIVMSG) (?i:(??{$config{irc_channel}})) :(?i:(??{$store{irc_nick}}))(?: |: ?)(.*)} => sub {
		my ($nick, $message) = @_;
		$nick = color_dpfix $nick;
			# allow the nickname to contain colors in DP format! Therefore, NO color_irc2dp on the nickname!
		$message = color_irc2dp $message;
		$message =~ s/(["\\])/\\$1/g;
		out dp => 0, "rcon2irc_say_as \"$nick on IRC\" \"$message\"";
		return 0;
	} ],

	# irc: CTCP VERSION reply
	[ irc => q{:([^! ]*)![^ ]* (?i:PRIVMSG) (?i:(??{$store{irc_nick}})) :\001VERSION( .*)?\001} => sub {
		my ($nick) = @_;
		my $ver = $store{dp_version} or return 0;
		$ver .= ", rcon2irc $VERSION";
		out irc => 0, "NOTICE $nick :\001VERSION $ver\001";
	} ],

	# on game start, notify the channel
	[ dp => q{:gamestart:(.*):[0-9.]*} => sub {
		my ($map) = @_;
		$store{playing} = 1;
		$store{map} = $map;
		$store{map_starttime} = time();
		my $slotsstr = nex_slotsstring();
		out irc => 0, "PRIVMSG $config{irc_channel} :\00304" . $map . "\017 has begun$slotsstr";
		delete $store{lms_blocked};
		return 0;
	} ],

	# on game over, clear the current map
	[ dp => q{:gameover} => sub {
		$store{playing} = 0;
		return 0;
	} ],

	# scores: Nexuiz server -> IRC channel (start)
	[ dp => q{:scores:(.*):(\d+)} => sub {
		my ($map, $time) = @_;
		$store{scores} = {};
		$store{scores}{map} = $map;
		$store{scores}{time} = $time;
		$store{scores}{players} = [];
		delete $store{lms_blocked};
		return 0;
	} ],

	# scores: Nexuiz server -> IRC channel
	[ dp => q{:player:(-?\d+):(\d+):(\d+):(\d+):(\d+):(.*)} => sub {
		my ($frags, $deaths, $time, $team, $id, $name) = @_;
		return if not exists $store{scores};
		push @{$store{scores}{players}}, [$frags, $team, $name]
			unless $frags <= -666; # no spectators
		return 0;
	} ],

	# scores: Nexuiz server -> IRC channel (CTF)
	[ dp => q{:teamscores:(\d+:-?\d*(?::\d+:-?\d*)*)} => sub {
		my ($teams) = @_;
		return if not exists $store{scores};
		$store{scores}{teams} = {split /:/, $teams};
		return 0;
	} ],

	# scores: Nexuiz server -> IRC channel
	[ dp => q{:end} => sub {
		return if not exists $store{scores};
		my $s = $store{scores};
		delete $store{scores};
		my $teams_matter = nex_is_teamplay($s->{map});

		my @t = ();
		my @p = ();

		if($teams_matter)
		{
			# put players into teams
			my %t = ();
			for(@{$s->{players}})
			{
				my $thisteam = ($t{$_->[1]} ||= {score => 0, team => $_->[1], players => []});
				push @{$thisteam->{players}}, [$_->[0], $_->[1], $_->[2]];
				if($s->{teams})
				{
					$thisteam->{score} = $s->{teams}{$_->[1]};
				}
				else
				{
					$thisteam->{score} += $_->[0];
				}
			}

			# sort by team score
			@t = sort { $b->{score} <=> $a->{score} } values %t;

			# sort by player score
			@p = ();
			for(@t)
			{
				@{$_->{players}} = sort { $b->[0] <=> $a->[0] } @{$_->{players}};
				push @p, @{$_->{players}};
			}
		}
		else
		{
			@p = sort { $b->[0] <=> $a->[0] } @{$s->{players}};
		}

		# no display for empty server
		return 0
			if !@p;

		# make message fit somehow
		for my $maxnamelen(reverse 3..64)
		{
			my $scores_string = "PRIVMSG $config{irc_channel} :\00304" . $s->{map} . "\017 ended:";
			if($teams_matter)
			{
				my $sep = ' ';
				for(@t)
				{
					$scores_string .= $sep . sprintf "\003%02d\%d\017", $color_team2irc_table{$_->{team}}, $_->{score};
					$sep = ':';
				}
			}
			my $sep = '';
			for(@p)
			{
				my ($frags, $team, $name) = @$_;
				$name = color_dpfix substr($name, 0, $maxnamelen);
				if($teams_matter)
				{
					$name = "\003" . $color_team2irc_table{$team} . " " . color_dp2none $name;
				}
				else
				{
					$name = " " . color_dp2irc $name;
				}
				$scores_string .= "$sep$name\017 $frags";
				$sep = ',';
			}
			if(length($scores_string) <= ($store{irc_maxlen} || 256))
			{
				out irc => 0, $scores_string;
				return 0;
			}
		}
		out irc => 0, "PRIVMSG $config{irc_channel} :\001ACTION would have LIKED to put the scores here, but they wouldn't fit :(\001";
		return 0;
	} ],

	# complain when system load gets too high
	[ dp => q{timing:   (([0-9.]*)% CPU, ([0-9.]*)% lost, offset avg ([0-9.]*)ms, max ([0-9.]*)ms, sdev ([0-9.]*)ms)} => sub {
		my ($all, $cpu, $lost, $avg, $max, $sdev) = @_;
		return 0 # don't complain when just on the voting screen
			if !$store{playing};
		return 0 # don't complain if it was less than 0.5%
			if $lost < 0.5;
		return 0 # don't complain if nobody is looking
			if $store{slots_active} == 0;
		return 0 # don't complain in the first two minutes
			if time() - $store{map_starttime} < 120;
		return 0 # don't complain if it was already at least half as bad in this round
			if $store{map_starttime} == $store{timingerror_map_starttime} and $lost <= 2 * $store{timingerror_lost};
		$store{timingerror_map_starttime} = $store{map_starttime};
		$store{timingerror_lost} = $lost;
		out dp => 0, 'rcon2irc_say_as server "There are currently some severe system load problems. The admins have been notified."';
		out irc => 1, "PRIVMSG $config{irc_channel} :\001ACTION has big trouble on $store{map} after @{[int(time() - $store{map_starttime})]}s: $all\001";
		#out irc => 1, "PRIVMSG OpBaI :\001ACTION has big trouble on $store{map} after @{[int(time() - $store{map_starttime})]}s: $all\001";
		return 0;
	} ],
);



# Load plugins and add them to the handler list in the front.
for my $p(split ' ', $config{plugins})
{
	my @h = eval { do $p; }
		or die "Invalid plugin $p: $@";
	for(reverse @h)
	{
		ref $_ eq 'ARRAY' or die "Invalid plugin $p: did not return a list of arrays";
		@$_ == 3 or die "Invalid plugin $p: did not return a list of three-element arrays";
		!ref $_->[0] && !ref $_->[1] && ref $_->[2] eq 'CODE' or die "Invalid plugin $p: did not return a list of string-string-sub arrays";
		unshift @handlers, $_;
	}
}



# verify that the server is up by letting it echo back a string that causes
# re-initialization of the required aliases
out dp => 0, 'echo "Unknown command \"rcon2irc_eval\""'; # assume the server has been restarted



# regularily, query the server status and if it still is connected to us using
# the log_dest_udp feature. If not, we will detect the response to this rcon
# command and re-initialize the server's connection to us (either by log_dest_udp
# not containing our own IP:port, or by rcon2irc_eval not being a defined command).
schedule sub {
	my ($timer) = @_;
	out dp => 0, 'status', 'log_dest_udp', 'rcon2irc_eval set dummy 1';
	schedule $timer => (exists $store{dp_hostname} ? $config{dp_status_delay} : 1);;
} => 1;



# Continue with connecting to IRC as soon as we get our first status reply from
# the DP server (which contains the server's hostname that we'll use as
# realname for IRC).
schedule sub {
	my ($timer) = @_;

	# log on to IRC when needed
	if(exists $store{dp_hostname} && !exists $store{irc_logged_in})
	{
		$store{irc_nick_requested} = $config{irc_nick};
		out irc => 1, "NICK $config{irc_nick}", "USER $config{irc_user} localhost localhost :$store{dp_hostname}";
		$store{irc_logged_in} = 1;
		undef $store{irc_maxlen};
		undef $store{irc_pingtime};
	}

	schedule $timer => 1;;
} => 1;



# Regularily ping the IRC server to detect if the connection is down. If it is,
# schedule an IRC error that will cause reconnection later.
schedule sub {
	my ($timer) = @_;

	if($store{irc_logged_in})
	{
		if(defined $store{irc_pingtime})
		{
			# IRC connection apparently broke
			# so... KILL IT WITH FIRE
			$channels{system}->send("error irc", 0);
		}
		else
		{
			# everything is fine, send a new ping
			$store{irc_pingtime} = time();
			out irc => 1, "PING $store{irc_pingtime}";
		}
	}

	schedule $timer => $config{irc_ping_delay};;
} => 1;



# Main loop.
for(;;)
{
	# Build up an IO::Select object for all our channels.
	my $s = IO::Select->new();
	for my $chan(values %channels)
	{
		$s->add($_) for $chan->fds();
	}

	# wait for something to happen on our sockets, or wait 2 seconds without anything happening there
	$s->can_read(2);
	my @errors = $s->has_exception(0);

	# on every channel, look for incoming messages
	CHANNEL:
	for my $chanstr(keys %channels)
	{
		my $chan = $channels{$chanstr};
		my @chanfds = $chan->fds();

		for my $chanfd(@chanfds)
		{
			if(grep { $_ == $chanfd } @errors)
			{
				# STOP! This channel errored!
				$channels{system}->send("error $chanstr", 0);
				next CHANNEL;
			}
		}

		eval
		{
			for my $line($chan->recv())
			{
				# found one! Check if it matches the regular expression of one of
				# our handlers...
				my $handled = 0;
				for my $h(@handlers)
				{
					my ($chanstr_wanted, $re, $sub) = @$h;
					next
						if $chanstr_wanted ne $chanstr;
					use re 'eval';
					my @matches = ($line =~ /^$re$/s);
					no re 'eval';
					next
						unless @matches;
					# and if it is a match, handle it.
					++$handled;
					my $result = $sub->(@matches);
					last
						if $result;
				}
				# print the message, together with info on whether it has been handled or not
				if($handled)
				{
					print "           $chanstr >> $line\n";
				}
				else
				{
					print "unhandled: $chanstr >> $line\n";
				}
			}
			1;
		} or do {
			if($@ eq "read error\n")
			{
				$channels{system}->send("error $chanstr", 0);
				next CHANNEL;
			}
			else
			{
				# re-throw
				die $@;
			}
		};
	}

	# handle scheduled tasks...
	my @t = @tasks;
	my $t = time();
	# by emptying the list of tasks...
	@tasks = ();
	for(@t)
	{
		my ($time, $sub) = @$_;
		if($t >= $time)
		{
			# calling them if they are schedled for the "past"...
			$sub->($sub);
		}
		else
		{
			# or re-adding them to the task list if they still are scheduled for the "future"
			push @tasks, [$time, $sub];
		}
	}
}
