]> icculus.org git repositories - divverent/nexuiz.git/blob - Docs/server/rcon2irc/rcon2irc.pl
fix typo in unban command
[divverent/nexuiz.git] / Docs / server / rcon2irc / rcon2irc.pl
1 #!/usr/bin/perl
2
3 our $VERSION = '0.4.2 svn $Revision$';
4
5 # Copyright (c) 2008 Rudolf "divVerent" Polzer
6
7 # Permission is hereby granted, free of charge, to any person
8 # obtaining a copy of this software and associated documentation
9 # files (the "Software"), to deal in the Software without
10 # restriction, including without limitation the rights to use,
11 # copy, modify, merge, publish, distribute, sublicense, and/or sell
12 # copies of the Software, and to permit persons to whom the
13 # Software is furnished to do so, subject to the following
14 # conditions:
15
16 # The above copyright notice and this permission notice shall be
17 # included in all copies or substantial portions of the Software.
18
19 # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
20 # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
21 # OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
22 # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
23 # HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
24 # WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
25 # FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
26 # OTHER DEALINGS IN THE SOFTWARE.
27
28 # Interfaces:
29 #   Connection:
30 #     $conn->sockname() returns a connection type specific representation
31 #       string of the local address, or undef if not applicable.
32 #     $conn->send("string") sends something over the connection.
33 #     $conn->recv() receives a string from the connection, or returns "" if no
34 #       data is available.
35 #     $conn->fds() returns all file descriptors used by the connection, so one
36 #       can use select() on them.
37 #   Channel:
38 #     Usually wraps around a connection and implements a command based
39 #     structure over it. It usually is constructed using new
40 #     ChannelType($connection, someparameters...)
41 #     @cmds = $chan->join_commands(@cmds) joins multiple commands to a single
42 #       command string if the protocol supports it, or does nothing and leaves
43 #       @cmds unchanged if the protocol does not support that usage (this is
44 #       meant to save send() invocations).
45 #     $chan->send($command, $nothrottle) sends a command over the channel. If
46 #       $nothrottle is sent, the command must not be left out even if the channel
47 #       is saturated (for example, because of IRC's flood control mechanism).
48 #     $chan->quote($str) returns a string in a quoted form so it can safely be
49 #       inserted as a substring into a command, or returns $str as is if not
50 #       applicable. It is assumed that the result of the quote method is used
51 #       as part of a quoted string, if the protocol supports that.
52 #     $chan->recv() returns a list of received commands from the channel, or
53 #       the empty list if none are available.
54 #     $conn->fds() returns all file descriptors used by the channel's
55 #       connections, so one can use select() on them.
56
57
58
59
60
61
62
63 # Socket connection.
64 # Represents a connection over a socket.
65 # Mainly used to wrap a channel around it for, in this case, line based or rcon-like operation.
66 package Connection::Socket;
67 use strict;
68 use warnings;
69 use IO::Socket::INET;
70 use IO::Handle;
71
72 # Constructor:
73 #   my $conn = new Connection::Socket(tcp => "localaddress" => "remoteaddress" => 6667);
74 # If the remote address does not contain a port number, the numeric port is
75 # used (it serves as a default port).
76 sub new($$)
77 {
78         my ($class, $proto, $local, $remote, $defaultport) = @_;
79         my $sock = IO::Socket::INET->new(
80                 Proto => $proto,
81                 (length($local) ? (LocalAddr => $local) : ()),
82                 PeerAddr => $remote,
83                 PeerPort => $defaultport
84         ) or die "socket $proto/$local/$remote: $!";
85         $sock->blocking(0);
86         my $you = {
87                 # Mortal fool! Release me from this wretched tomb! I must be set free
88                 # or I will haunt you forever! I will hide your keys beneath the
89                 # cushions of your upholstered furniture... and NEVERMORE will you be
90                 # able to find socks that match!
91                 sock => $sock,
92                 # My demonic powers have made me OMNIPOTENT! Bwahahahahahahaha!
93         };
94         return
95                 bless $you, 'Connection::Socket';
96 }
97
98 # $sock->sockname() returns the local address of the socket.
99 sub sockname($)
100 {
101         my ($self) = @_;
102         my ($port, $addr) = sockaddr_in $self->{sock}->sockname();
103         return "@{[inet_ntoa $addr]}:$port";
104 }
105
106 # $sock->send($data) sends some data over the socket; on success, 1 is returned.
107 sub send($$)
108 {
109         my ($self, $data) = @_;
110         return 1
111                 if not length $data;
112         if(not eval { $self->{sock}->send($data); })
113         {
114                 warn "$@";
115                 return 0;
116         }
117         return 1;
118 }
119
120 # $sock->recv() receives as much as possible from the socket (or at most 32k). Returns "" if no data is available.
121 sub recv($)
122 {
123         my ($self) = @_;
124         my $data = "";
125         if(defined $self->{sock}->recv($data, 32768, 0))
126         {
127                 return $data;
128         }
129         elsif($!{EAGAIN})
130         {
131                 return "";
132         }
133         else
134         {
135                 return undef;
136         }
137 }
138
139 # $sock->fds() returns the socket file descriptor.
140 sub fds($)
141 {
142         my ($self) = @_;
143         return fileno $self->{sock};
144 }
145
146
147
148
149
150
151
152 # Line-based buffered connectionless FIFO channel.
153 # Whatever is sent to it using send() is echoed back when using recv().
154 package Channel::FIFO;
155 use strict;
156 use warnings;
157
158 # Constructor:
159 #   my $chan = new Channel::FIFO();
160 sub new($)
161 {
162         my ($class) = @_;
163         my $you = {
164                 buffer => []
165         };
166         return
167                 bless $you, 'Channel::FIFO';
168 }
169
170 sub join_commands($@)
171 {
172         my ($self, @data) = @_;
173         return @data;
174 }
175
176 sub send($$$)
177 {
178         my ($self, $line, $nothrottle) = @_;
179         push @{$self->{buffer}}, $line;
180 }
181
182 sub quote($$)
183 {
184         my ($self, $data) = @_;
185         return $data;
186 }
187
188 sub recv($)
189 {
190         my ($self) = @_;
191         my $r = $self->{buffer};
192         $self->{buffer} = [];
193         return @$r;
194 }
195
196 sub fds($)
197 {
198         my ($self) = @_;
199         return ();
200 }
201
202
203
204
205
206
207
208 # QW rcon protocol channel.
209 # Wraps around a UDP based Connection and sends commands as rcon commands as
210 # well as receives rcon replies. The quote and join_commands methods are using
211 # DarkPlaces engine specific rcon protocol extensions.
212 package Channel::QW;
213 use strict;
214 use warnings;
215
216 # Constructor:
217 #   my $chan = new Channel::QW($connection, "password");
218 sub new($$)
219 {
220         my ($class, $conn, $password) = @_;
221         my $you = {
222                 connector => $conn,
223                 password => $password,
224                 recvbuf => "",
225         };
226         return
227                 bless $you, 'Channel::QW';
228 }
229
230 # Note: multiple commands in one rcon packet is a DarkPlaces extension.
231 sub join_commands($@)
232 {
233         my ($self, @data) = @_;
234         return join "\0", @data;
235 }
236
237 sub send($$$)
238 {
239         my ($self, $line, $nothrottle) = @_;
240         return $self->{connector}->send("\377\377\377\377rcon $self->{password} $line");
241 }
242
243 # Note: backslash and quotation mark escaping is a DarkPlaces extension.
244 sub quote($$)
245 {
246         my ($self, $data) = @_;
247         $data =~ s/[\000-\037]//g;
248         $data =~ s/([\\"])/\\$1/g;
249         $data =~ s/\$/\$\$/g;
250         return $data;
251 }
252
253 sub recv($)
254 {
255         my ($self) = @_;
256         for(;;)
257         {
258                 my $s = $self->{connector}->recv();
259                 die "read error\n"
260                         if not defined $s;
261                 length $s
262                         or last;
263                 next
264                         if $s !~ /^\377\377\377\377n(.*)$/s;
265                 $self->{recvbuf} .= $1;
266         }
267         my @out = ();
268         while($self->{recvbuf} =~ s/^(.*?)(?:\r\n?|\n)//)
269         {
270                 push @out, $1;
271         }
272         return @out;
273 }
274
275 sub fds($)
276 {
277         my ($self) = @_;
278         return $self->{connector}->fds();
279 }
280
281
282
283
284
285
286
287 # Line based protocol channel.
288 # Wraps around a TCP based Connection and sends commands as text lines
289 # (separated by CRLF). When reading responses from the Connection, any type of
290 # line ending is accepted.
291 # A flood control mechanism is implemented.
292 package Channel::Line;
293 use strict;
294 use warnings;
295 use Time::HiRes qw/time/;
296
297 # Constructor:
298 #   my $chan = new Channel::Line($connection);
299 sub new($$)
300 {
301         my ($class, $conn) = @_;
302         my $you = {
303                 connector => $conn,
304                 recvbuf => "",
305                 capacity => undef,
306                 linepersec => undef,
307                 maxlines => undef,
308                 lastsend => time()
309         };
310         return 
311                 bless $you, 'Channel::Line';
312 }
313
314 sub join_commands($@)
315 {
316         my ($self, @data) = @_;
317         return @data;
318 }
319
320 # Sets new flood control parameters:
321 #   $chan->throttle(maximum lines per second, maximum burst length allowed to
322 #     exceed the lines per second limit);
323 #   RFC 1459 describes these parameters to be 0.5 and 5 for the IRC protocol.
324 #   If the $nothrottle flag is set while sending, the line is sent anyway even
325 #   if flooding would take place.
326 sub throttle($$$)
327 {
328         my ($self, $linepersec, $maxlines) = @_;
329         $self->{linepersec} = $linepersec;
330         $self->{maxlines} = $maxlines;
331         $self->{capacity} = $maxlines;
332 }
333
334 sub send($$$)
335 {
336         my ($self, $line, $nothrottle) = @_;
337         my $t = time();
338         if(defined $self->{capacity})
339         {
340                 $self->{capacity} += ($t - $self->{lastsend}) * $self->{linepersec};
341                 $self->{lastsend} = $t;
342                 $self->{capacity} = $self->{maxlines}
343                         if $self->{capacity} > $self->{maxlines};
344                 if(!$nothrottle)
345                 {
346                         return -1
347                                 if $self->{capacity} < 0;
348                 }
349                 $self->{capacity} -= 1;
350         }
351         $line =~ s/\r|\n//g;
352         return $self->{connector}->send("$line\r\n");
353 }
354
355 sub quote($$)
356 {
357         my ($self, $data) = @_;
358         $data =~ s/\r\n?/\n/g;
359         $data =~ s/\n/*/g;
360         return $data;
361 }
362
363 sub recv($)
364 {
365         my ($self) = @_;
366         for(;;)
367         {
368                 my $s = $self->{connector}->recv();
369                 die "read error\n"
370                         if not defined $s;
371                 length $s
372                         or last;
373                 $self->{recvbuf} .= $s;
374         }
375         my @out = ();
376         while($self->{recvbuf} =~ s/^(.*?)(?:\r\n?|\n)//)
377         {
378                 push @out, $1;
379         }
380         return @out;
381 }
382
383 sub fds($)
384 {
385         my ($self) = @_;
386         return $self->{connector}->fds();
387 }
388
389
390
391
392
393
394 # main program... a gateway between IRC and DarkPlaces servers
395 package main;
396
397 use strict;
398 use warnings;
399 use IO::Select;
400 use Digest::MD5;
401 use Time::HiRes qw/time/;
402
403 our @handlers = (); # list of [channel, expression, sub to handle result]
404 our @tasks = (); # list of [time, sub]
405 our %channels = ();
406 our %store = (
407         irc_nick => "",
408         playernick_0 => "(console)",
409 );
410 our %config = (
411         irc_server => undef,
412         irc_nick => undef,
413         irc_user => undef,
414         irc_channel => undef,
415         irc_ping_delay => 120,
416         irc_trigger => "",
417
418         irc_nickserv_password => "",
419         irc_nickserv_identify => 'PRIVMSG NickServ :IDENTIFY %2$s',
420         irc_nickserv_ghost => 'PRIVMSG NickServ :GHOST %1$s %2$s',
421         irc_nickserv_ghost_attempts => 3,
422
423         irc_quakenet_authname => "",
424         irc_quakenet_password => "",
425         irc_quakenet_getchallenge => 'PRIVMSG Q@CServe.quakenet.org :CHALLENGE',
426         irc_quakenet_challengeauth => 'PRIVMSG Q@CServe.quakenet.org :CHALLENGEAUTH',
427         irc_quakenet_challengeprefix => ':Q!TheQBot@CServe.quakenet.org NOTICE [^:]+ :CHALLENGE',
428
429         dp_server => undef,
430         dp_listen => "", 
431         dp_password => undef,
432         dp_status_delay => 30,
433         dp_server_from_wan => "",
434         irc_local => "",
435
436         irc_admin_password => "",
437         irc_admin_timeout => 3600,
438
439         plugins => "",
440 );
441
442
443
444 # MISC STRING UTILITY ROUTINES to convert between DarkPlaces and IRC conventions
445
446 # convert mIRC color codes to DP color codes
447 our @color_irc2dp_table = (7, 0, 4, 2, 1, 1, 6, 1, 3, 2, 5, 5, 4, 6, 7, 7);
448 our @color_dp2irc_table = (14, 4, 9, 8, 12, 11, 13, 14, 15, 15); # not accurate, but legible
449 our @color_dp2ansi_table = ("m", "1;31m", "1;32m", "1;33m", "1;34m", "1;36m", "1;35m", "m", "1m", "1m"); # not accurate, but legible
450 our %color_team2dp_table = (5 => 1, 14 => 4, 13 => 3, 10 => 6);
451 our %color_team2irc_table = (5 => 4, 14 => 12, 13 => 8, 10 => 13);
452 sub color_irc2dp($)
453 {
454         my ($message) = @_;
455         $message =~ s/\^/^^/g;
456         my $color = 7;
457         $message =~ s{\003(\d\d?)(?:,(\d?\d?))?|(\017)}{
458                 # $1 is FG, $2 is BG, but let's ignore BG
459                 my $oldcolor = $color;
460                 if($3)
461                 {
462                         $color = 7;
463                 }
464                 else
465                 {
466                         $color = $color_irc2dp_table[$1];
467                         $color = $oldcolor if not defined $color;
468                 }
469                 ($color == $oldcolor) ? '' : '^' . $color;
470         }esg;
471         $message =~ s{[\000-\037]}{}gs; # kill bold etc. for now
472         return $message;
473 }
474
475 our @text_qfont_table = ( # ripped from DP console.c qfont_table
476     "\0", '#',  '#',  '#',  '#',  '.',  '#',  '#',
477     '#',  9,    10,   '#',  ' ',  13,   '.',  '.',
478     '[',  ']',  '0',  '1',  '2',  '3',  '4',  '5',
479     '6',  '7',  '8',  '9',  '.',  '<',  '=',  '>',
480     ' ',  '!',  '"',  '#',  '$',  '%',  '&',  '\'',
481     '(',  ')',  '*',  '+',  ',',  '-',  '.',  '/',
482     '0',  '1',  '2',  '3',  '4',  '5',  '6',  '7',
483     '8',  '9',  ':',  ';',  '<',  '=',  '>',  '?',
484     '@',  'A',  'B',  'C',  'D',  'E',  'F',  'G',
485     'H',  'I',  'J',  'K',  'L',  'M',  'N',  'O',
486     'P',  'Q',  'R',  'S',  'T',  'U',  'V',  'W',
487     'X',  'Y',  'Z',  '[',  '\\', ']',  '^',  '_',
488     '`',  'a',  'b',  'c',  'd',  'e',  'f',  'g',
489     'h',  'i',  'j',  'k',  'l',  'm',  'n',  'o',
490     'p',  'q',  'r',  's',  't',  'u',  'v',  'w',
491     'x',  'y',  'z',  '{',  '|',  '}',  '~',  '<',
492     '<',  '=',  '>',  '#',  '#',  '.',  '#',  '#',
493     '#',  '#',  ' ',  '#',  ' ',  '>',  '.',  '.',
494     '[',  ']',  '0',  '1',  '2',  '3',  '4',  '5',
495     '6',  '7',  '8',  '9',  '.',  '<',  '=',  '>',
496     ' ',  '!',  '"',  '#',  '$',  '%',  '&',  '\'',
497     '(',  ')',  '*',  '+',  ',',  '-',  '.',  '/',
498     '0',  '1',  '2',  '3',  '4',  '5',  '6',  '7',
499     '8',  '9',  ':',  ';',  '<',  '=',  '>',  '?',
500     '@',  'A',  'B',  'C',  'D',  'E',  'F',  'G',
501     'H',  'I',  'J',  'K',  'L',  'M',  'N',  'O',
502     'P',  'Q',  'R',  'S',  'T',  'U',  'V',  'W',
503     'X',  'Y',  'Z',  '[',  '\\', ']',  '^',  '_',
504     '`',  'a',  'b',  'c',  'd',  'e',  'f',  'g',
505     'h',  'i',  'j',  'k',  'l',  'm',  'n',  'o',
506     'p',  'q',  'r',  's',  't',  'u',  'v',  'w',
507     'x',  'y',  'z',  '{',  '|',  '}',  '~',  '<'
508 );
509 sub text_dp2ascii($)
510 {
511         my ($message) = @_;
512         $message = join '', map { $text_qfont_table[ord $_] } split //, $message;
513 }
514
515 sub color_dp2none($)
516 {
517         my ($message) = @_;
518         my $color = -1;
519         $message =~ s{\^(.)(?=([0-9,]?))}{
520                 my $c = $1;
521                 $c eq '^' ? '^' :
522                 $c =~ /^[0-9]$/ ? '' : "^$c";
523         }esg;
524         return text_dp2ascii $message;
525 }
526
527 sub color_dp2irc($)
528 {
529         my ($message) = @_;
530         my $color = -1;
531         $message =~ s{\^(.)(?=([0-9,]?))}{
532                 my $c = $1;
533                 my $f = $2;
534                 $c eq '^' ? '^' :
535                 $c =~ /^[0-9]$/ ? do {
536                         my $oldcolor = $color;
537                         $c = 0 if $c >= 7; # map 0, 7, 8, 9 to default (no bright white or such stuff)
538                         $color = $color_dp2irc_table[$c];
539                         ($color == $oldcolor) ? '' :
540                         $c == 0 ? "\0001" :
541                         $f eq ',' ? "\0003$color\0002\0002" :
542                         $f ne ''  ? sprintf "\0003%02d", $color : "\0003$color";
543                 } : "^$c";
544         }esg;
545         $message = text_dp2ascii $message;
546         $message =~ s/\0001/\017/g;
547         $message =~ s/\0002/\002/g;
548         $message =~ s/\0003/\003/g;
549         return $message;
550 }
551
552 sub color_dp2ansi($)
553 {
554         my ($message) = @_;
555         my $color = -1;
556         $message =~ s{\^(.)}{
557                 my $c = $1;
558                 $c eq '^' ? '^' :
559                 $c =~ /^[0-9]$/ ? do {
560                         my $oldcolor = $color;
561                         $color = $color_dp2ansi_table[$c];
562                         ($color eq $oldcolor) ? '' :
563                         "\000[${color}" # "
564                 } : "^$c";
565         }esg;
566         $message = text_dp2ascii $message;
567         $message =~ s/\000/\033/g;
568         return $message;
569 }
570
571 sub color_dpfix($)
572 {
573         my ($message) = @_;
574         # if the message ends with an odd number of ^, kill one
575         chop $message if $message =~ /(?:^|[^\^])\^(\^\^)*$/;
576         return $message;
577 }
578
579
580
581 # Nexuiz specific parsing of some server messages
582
583 sub nex_is_teamplay($)
584 {
585         my ($map) = @_;
586         return $map =~ /^(?:kh|ctf|tdm|dom)_/;
587 }
588
589 sub nex_slotsstring()
590 {
591         my $slotsstr = "";
592         if(defined $store{slots_max})
593         {
594                 my $slots = $store{slots_max} - $store{slots_active};
595                 my $slots_s = ($slots == 1) ? '' : 's';
596                 $slotsstr = " ($slots free slot$slots_s)";
597                 my $s = $config{dp_server_from_wan} || $config{dp_server};
598                 $slotsstr .= "; join now: \002nexuiz +connect $s"
599                         if $slots >= 1 and not $store{lms_blocked};
600         }
601         return $slotsstr;
602 }
603
604
605
606 # Do we have a config file? If yes, read and parse it (syntax: key = value
607 # pairs, separated by newlines), if not, complain.
608 die "Usage: $0 configfile\n"
609         unless @ARGV == 1;
610
611 open my $fh, "<", $ARGV[0]
612         or die "open $ARGV[0]: $!";
613 while(<$fh>)
614 {
615         chomp;
616         /^#/ and next;
617         /^(.*?)\s+=(?:\s+(.*))?$/ or next;
618         warn "Undefined config item: $1"
619                 unless exists $config{$1};
620         $config{$1} = defined $2 ? $2 : "";
621 }
622 close $fh;
623 my @missing = grep { !defined $config{$_} } keys %config;
624 die "The following config items are missing: @missing"
625         if @missing;
626
627
628
629 # Create a channel for error messages and other internal status messages...
630
631 $channels{system} = new Channel::FIFO();
632
633 # for example, quit messages caused by signals (if SIGTERM or SIGINT is first
634 # received, try to shut down cleanly, and if such a signal is received a second
635 # time, just exit)
636 my $quitting = 0;
637 $SIG{INT} = sub {
638         exit 1 if $quitting++;
639         $channels{system}->send("quit SIGINT");
640 };
641 $SIG{TERM} = sub {
642         exit 1 if $quitting++;
643         $channels{system}->send("quit SIGTERM");
644 };
645
646
647
648 # Create the two channels to gateway between...
649
650 $channels{irc} = new Channel::Line(new Connection::Socket(tcp => $config{irc_local} => $config{irc_server} => 6667));
651 $channels{dp} = new Channel::QW(my $dpsock = new Connection::Socket(udp => $config{dp_listen} => $config{dp_server} => 26000), $config{dp_password});
652 $config{dp_listen} = $dpsock->sockname();
653 print "Listening on $config{dp_listen}\n";
654
655 $channels{irc}->throttle(0.5, 5);
656
657
658 # Utility routine to write to a channel by name, also outputting what's been written and some status
659 sub out($$@)
660 {
661         my $chanstr = shift;
662         my $nothrottle = shift;
663         my $chan = $channels{$chanstr};
664         if(!$chan)
665         {
666                 print "UNDEFINED: $chanstr, ignoring message\n";
667                 return;
668         }
669         @_ = $chan->join_commands(@_);
670         for(@_)
671         {
672                 my $result = $chan->send($_, $nothrottle);
673                 if($result > 0)
674                 {
675                         print "           $chanstr << $_\n";
676                 }
677                 elsif($result < 0)
678                 {
679                         print "FLOOD:     $chanstr << $_\n";
680                 }
681                 else
682                 {
683                         print "ERROR:     $chanstr << $_\n";
684                         $channels{system}->send("error $chanstr", 0);
685                 }
686         }
687 }
688
689
690
691 # Schedule a task for later execution by the main loop; usage: schedule sub {
692 # task... }, $time; When a scheduled task is run, a reference to the task's own
693 # sub is passed as first argument; that way, the task is able to re-schedule
694 # itself so it gets periodically executed.
695 sub schedule($$)
696 {
697         my ($sub, $time) = @_;
698         push @tasks, [time() + $time, $sub];
699 }
700
701 # On IRC error, delete some data store variables of the connection, and
702 # reconnect to the IRC server soon (but only if someone is actually playing)
703 sub irc_error()
704 {
705         # prevent multiple instances of this timer
706         return if $store{irc_error_active};
707         $store{irc_error_active} = 1;
708
709         delete $channels{irc};
710         schedule sub {
711                 my ($timer) = @_;
712                 if(!defined $store{slots_full})
713                 {
714                         # DP is not running, then delay IRC reconnecting
715                         #use Data::Dumper; print Dumper \$timer;
716                         schedule $timer => 1;;
717                         return;
718                         # this will keep irc_error_active
719                 }
720                 $channels{irc} = new Channel::Line(new Connection::Socket(tcp => "" => $config{irc_server}));
721                 delete $store{$_} for grep { /^irc_/ } keys %store;
722                 $store{irc_nick} = "";
723                 schedule sub {
724                         my ($timer) = @_;
725                         out dp => 0, 'sv_cmd bans', 'status 1', 'log_dest_udp';
726                         $store{status_waiting} = -1;
727                 } => 1;
728                 # this will clear irc_error_active
729         } => 30;
730         return 0;
731 }
732
733 # IRC joining (if this is called as response to a nick name collision, $is433 is set);
734 # among other stuff, it performs NickServ or Quakenet authentication. This is to be called
735 # until the channel has been joined for every message that may be "interesting" (basically,
736 # IRC 001 hello messages, 443 nick collision messages and some notices by services).
737 sub irc_joinstage($)
738 {
739         my($is433) = @_;
740
741         return 0
742                 if $store{irc_joined_channel};
743         
744                 #use Data::Dumper; print Dumper \%store;
745
746         if($is433)
747         {
748                 if(length $store{irc_nick})
749                 {
750                         # we already have another nick, but couldn't change to the new one
751                         # try ghosting and then get the nick again
752                         if(length $config{irc_nickserv_password})
753                         {
754                                 if(++$store{irc_nickserv_ghost_attempts} <= $config{irc_nickserv_ghost_attempts})
755                                 {
756                                         $store{irc_nick_requested} = $config{irc_nick};
757                                         out irc => 1, sprintf($config{irc_nickserv_ghost}, $config{irc_nick}, $config{irc_nickserv_password});
758                                         schedule sub {
759                                                 out irc => 1, "NICK $config{irc_nick}";
760                                         } => 1;
761                                         return; # we'll get here again for the NICK success message, or for a 433 failure
762                                 }
763                                 # otherwise, we failed to ghost and will continue with the wrong
764                                 # nick... also, no need to try to identify here
765                         }
766                         # otherwise, we can't handle this and will continue with our wrong nick
767                 }
768                 else
769                 {
770                         # we failed to get an initial nickname
771                         # change ours a bit and try again
772                         if(length $store{irc_nick_requested} < 9)
773                         {
774                                 $store{irc_nick_requested} .= '_';
775                         }
776                         else
777                         {
778                                 substr $store{irc_nick_requested}, int(rand length $store{irc_nick_requested}), 1, chr(97 + int rand 26);
779                         }
780                         out irc => 1, "NICK $store{irc_nick_requested}";
781                         return; # when it fails, we'll get here again, and when it succeeds, we will continue
782                 }
783         }
784
785         # we got a 001 or a NICK message, so $store{irc_nick} has been updated
786         if(length $config{irc_nickserv_password})
787         {
788                 if($store{irc_nick} eq $config{irc_nick})
789                 {
790                         # identify
791                         out irc => 1, sprintf($config{irc_nickserv_identify}, $config{irc_nick}, $config{irc_nickserv_password});
792                 }
793                 else
794                 {
795                         # ghost
796                         if(++$store{irc_nickserv_ghost_attempts} <= $config{irc_nickserv_ghost_attempts})
797                         {
798                                 $store{irc_nick_requested} = $config{irc_nick};
799                                 out irc => 1, sprintf($config{irc_nickserv_ghost}, $config{irc_nick}, $config{irc_nickserv_password});
800                                 schedule sub {
801                                         out irc => 1, "NICK $config{irc_nick}";
802                                 } => 1;
803                                 return; # we'll get here again for the NICK success message, or for a 433 failure
804                         }
805                         # otherwise, we failed to ghost and will continue with the wrong
806                         # nick... also, no need to try to identify here
807                 }
808         }
809
810         # we are on Quakenet. Try to authenticate.
811         if(length $config{irc_quakenet_password} and length $config{irc_quakenet_authname})
812         {
813                 if(defined $store{irc_quakenet_challenge})
814                 {
815                         if($store{irc_quakenet_challenge} =~ /^MD5 (.*)/)
816                         {
817                                 out irc => 1, "$config{irc_quakenet_challengeauth} $config{irc_quakenet_authname} " . Digest::MD5::md5_hex("$config{irc_quakenet_password} $1");
818                         }
819                 }
820                 else
821                 {
822                         out irc => 1, $config{irc_quakenet_getchallenge};
823                         return;
824                         # we get here again when Q asks us
825                 }
826         }
827         
828         # if we get here, we are on IRC
829         $store{irc_joined_channel} = 1;
830         schedule sub {
831                 out irc => 1, "JOIN $config{irc_channel}";
832         } => 1;
833         return 0;
834 }
835
836 my $RE_FAIL = qr/$ $/;
837 my $RE_SUCCEED = qr//;
838 sub cond($)
839 {
840         return $_[0] ? $RE_FAIL : $RE_SUCCEED;
841 }
842
843
844 # List of all handlers on the various sockets. Additional handlers can be added by a plugin.
845 @handlers = (
846         # detect a server restart and set it up again
847         [ dp => q{ *(?:Warning: Could not expand \$|Unknown command ")(?:rcon2irc_[a-z0-9_]*)[" ]*} => sub {
848                 out dp => 0,
849                         'alias rcon2irc_eval "$*"',
850                         'log_dest_udp',
851                         'sv_logscores_console 0',
852                         'sv_logscores_bots 1',
853                         'sv_eventlog 1',
854                         'sv_eventlog_console 1',
855                         'alias rcon2irc_say_as "set say_as_restorenick \"$sv_adminnick\"; sv_adminnick \"$1^3\"; say \"^7$2\"; rcon2irc_say_as_restore"',
856                         'alias rcon2irc_say_as_restore "set sv_adminnick \"$say_as_restorenick\""',
857                         'alias rcon2irc_quit "echo \"quitting rcon2irc $1: log_dest_udp is $log_dest_udp\""'; # note: \\\\\\" ->perl \\\" ->console \"
858                 return 0;
859         } ],
860
861         # detect missing entry in log_dest_udp and fix it
862         [ dp => q{"log_dest_udp" is "([^"]*)" \["[^"]*"\]} => sub {
863                 my ($dest) = @_;
864                 my @dests = split ' ', $dest;
865                 return 0 if grep { $_ eq $config{dp_listen} } @dests;
866                 out dp => 0, 'log_dest_udp "' . join(" ", @dests, $config{dp_listen}) . '"';
867                 return 0;
868         } ],
869
870         # retrieve list of banned hosts
871         [ dp => q{#(\d+): (\S+) is still banned for (\S+) seconds} => sub {
872                 return 0 unless $store{status_waiting} < 0;
873                 my ($id, $ip, $time) = @_;
874                 $store{bans_new} = [] if $id == 0;
875                 $store{bans_new}[$id] = { ip => $ip, 'time' => $time };
876                 return 0;
877         } ],
878
879         # retrieve hostname from status replies
880         [ dp => q{host:     (.*)} => sub {
881                 return 0 unless $store{status_waiting} < 0;
882                 my ($name) = @_;
883                 $store{dp_hostname} = $name;
884                 $store{bans} = $store{bans_new};
885                 return 0;
886         } ],
887
888         # retrieve version from status replies
889         [ dp => q{version:  (.*)} => sub {
890                 return 0 unless $store{status_waiting} < 0;
891                 my ($version) = @_;
892                 $store{dp_version} = $version;
893                 return 0;
894         } ],
895
896         # retrieve player names
897         [ dp => q{players:  (\d+) active \((\d+) max\)} => sub {
898                 return 0 unless $store{status_waiting} < 0;
899                 my ($active, $max) = @_;
900                 my $full = ($active >= $max);
901                 $store{slots_max} = $max;
902                 $store{slots_active} = $active;
903                 $store{status_waiting} = $active;
904                 $store{playerslots_active_new} = [];
905                 if($full != ($store{slots_full} || 0))
906                 {
907                         $store{slots_full} = $full;
908                         return 0
909                                 if $store{lms_blocked};
910                         if($full)
911                         {
912                                 out irc => 0, "PRIVMSG $config{irc_channel} :\001ACTION is full!\001";
913                         }
914                         else
915                         {
916                                 my $slotsstr = nex_slotsstring();
917                                 out irc => 0, "PRIVMSG $config{irc_channel} :\001ACTION can be joined again$slotsstr!\001";
918                         }
919                 }
920                 return 0;
921         } ],
922
923         # retrieve player names
924         [ dp => q{\^\d(\S+)\s+(\d+)\s+(\d+)\s+(\S+)\s+(-?\d+)\s+\#(\d+)\s+\^\d(.*)} => sub {
925                 return 0 unless $store{status_waiting} > 0;
926                 my ($ip, $pl, $ping, $time, $frags, $no, $name) = ($1, $2, $3, $4, $5, $6, $7);
927                 $store{"playerslot_$no"} = { ip => $ip, pl => $pl, ping => $ping, 'time' => $time, frags => $frags, no => $no, name => $name };
928                 push @{$store{playerslots_active_new}}, $no;
929                 if(--$store{status_waiting} == 0)
930                 {
931                         $store{playerslots_active} = $store{playerslots_active_new};
932                 }
933                 return 0;
934         } ],
935
936         # IRC admin commands
937         [ irc => q{:(([^! ]*)![^ ]*) (?i:PRIVMSG) [^&#%]\S* :(.*)} => sub {
938                 return 0 unless $config{irc_admin_password} ne '';
939
940                 my ($hostmask, $nick, $command) = @_;
941                 my $dpnick = color_dpfix $nick;
942
943                 if($command eq "login $config{irc_admin_password}")
944                 {
945                         $store{logins}{$hostmask} = time() + $config{irc_admin_timeout};
946                         out irc => 0, "PRIVMSG $nick :my wish is your command";
947                         return -1;
948                 }
949
950                 if($command =~ /^login /)
951                 {
952                         out irc => 0, "PRIVMSG $nick :invalid password";
953                         return -1;
954                 }
955
956                 if(($store{logins}{$hostmask} || 0) < time())
957                 {
958                         out irc => 0, "PRIVMSG $nick :authentication required";
959                         return -1;
960                 }
961
962                 if($command =~ /^status(?: (.*))?$/)
963                 {
964                         my ($match) = $1;
965                         my $found = 0;
966                         my $foundany = 0;
967                         for my $slot(@{$store{playerslots_active}})
968                         {
969                                 my $s = $store{"playerslot_$slot"};
970                                 if(not defined $match or index(color_dp2none($s->{name}), $match) >= 0)
971                                 {
972                                         out irc => 0, sprintf 'PRIVMSG %s :%-21s %2i %4i %8s %4i #%-3u %s', $nick, $s->{ip}, $s->{pl}, $s->{ping}, $s->{time}, $s->{frags}, $slot, color_dp2irc $s->{name};
973                                         ++$found;
974                                 }
975                                 ++$foundany;
976                         }
977                         if(!$found)
978                         {
979                                 if(!$foundany)
980                                 {
981                                         out irc => 0, "PRIVMSG $nick :the server is empty";
982                                 }
983                                 else
984                                 {
985                                         out irc => 0, "PRIVMSG $nick :no nicknames match";
986                                 }
987                         }
988                         return 0;
989                 }
990
991                 if($command =~ /^kick # (\d+) (.*)$/)
992                 {
993                         my ($id, $reason) = ($1, $2);
994                         my $dpreason = color_irc2dp $reason;
995                         $dpreason =~ s/^(~?)(.*)/$1irc $dpnick: $2/g;
996                         $dpreason =~ s/(["\\])/\\$1/g;
997                         out dp => 0, "kick # $id $dpreason";
998                         my $slotnik = "playerslot_$id";
999                         out irc => 0, "PRIVMSG $nick :kicked $id (@{[color_dp2irc $store{$slotnik}{name}]} @ $store{$slotnik}{ip}) ($reason)";
1000                         return 0;
1001                 }
1002
1003                 if($command =~ /^kickban # (\d+) (\d+) (\d+) (.*)$/)
1004                 {
1005                         my ($id, $bantime, $mask, $reason) = ($1, $2, $3, $4);
1006                         my $dpreason = color_irc2dp $reason;
1007                         $dpreason =~ s/^(~?)(.*)/$1irc $dpnick: $2/g;
1008                         $dpreason =~ s/(["\\])/\\$1/g;
1009                         out dp => 0, "kickban # $id $bantime $mask $dpreason";
1010                         my $slotnik = "playerslot_$id";
1011                         out irc => 0, "PRIVMSG $nick :kickbanned $id (@{[color_dp2irc $store{$slotnik}{name}]} @ $store{$slotnik}{ip}), netmask $mask, for $bantime seconds ($reason)";
1012                         return 0;
1013                 }
1014
1015                 if($command eq "bans")
1016                 {
1017                         my $banlist =
1018                                 join ", ",
1019                                 map { "$_ ($store{bans}[$_]{ip}, $store{bans}[$_]{time}s)" }
1020                                 0..@{$store{bans} || []}-1;
1021                         $banlist = "no bans"
1022                                 if $banlist eq "";
1023                         out irc => 0, "PRIVMSG $nick :$banlist";
1024                         return 0;
1025                 }
1026
1027                 if($command =~ /^unban (\d+)$/)
1028                 {
1029                         my ($id) = ($1);
1030                         out dp => 0, "unban $id";
1031                         out irc => 0, "PRIVMSG $nick :removed ban #$id ($store{bans}[$id])";
1032                         return 0;
1033                 }
1034
1035                 out irc => 0, "PRIVMSG $nick :unknown command (supported: status [substring], kick # id, kickban # id bantime mask reason, bans, unban banid)";
1036
1037                 return -1;
1038         } ],
1039
1040         # LMS: detect "no more lives" message
1041         [ dp => q{\^4.*\^4 has no more lives left} => sub {
1042                 if(!$store{lms_blocked})
1043                 {
1044                         $store{lms_blocked} = 1;
1045                         if(!$store{slots_full})
1046                         {
1047                                 schedule sub {
1048                                         if($store{lms_blocked})
1049                                         {
1050                                                 out irc => 0, "PRIVMSG $config{irc_channel} :\001ACTION can't be joined until next round (a player has no more lives left)\001";
1051                                         }
1052                                 } => 1;
1053                         }
1054                 }
1055         } ],
1056
1057         # detect IRC errors and reconnect
1058         [ irc => q{ERROR .*} => \&irc_error ],
1059         [ system => q{error irc} => \&irc_error ],
1060
1061         # IRC nick in use
1062         [ irc => q{:[^ ]* 433 .*} => sub {
1063                 return irc_joinstage(433);
1064         } ],
1065
1066         # IRC welcome
1067         [ irc => q{:[^ ]* 001 .*} => sub {
1068                 $store{irc_seen_welcome} = 1;
1069                 $store{irc_nick} = $store{irc_nick_requested};
1070                 return irc_joinstage(0);
1071         } ],
1072
1073         # IRC my nickname changed
1074         [ irc => q{:(?i:(??{$store{irc_nick}}))![^ ]* (?i:NICK) :(.*)} => sub {
1075                 my ($n) = @_;
1076                 $store{irc_nick} = $n;
1077                 return irc_joinstage(0);
1078         } ],
1079
1080         # Quakenet: challenge from Q
1081         [ irc => q{(??{$config{irc_quakenet_challengeprefix}}) (.*)} => sub {
1082                 $store{irc_quakenet_challenge} = $1;
1083                 return irc_joinstage(0);
1084         } ],
1085
1086         # shut down everything on SIGINT
1087         [ system => q{quit (.*)} => sub {
1088                 my ($cause) = @_;
1089                 out irc => 1, "QUIT :$cause";
1090                 $store{quitcookie} = int rand 1000000000;
1091                 out dp => 0, "rcon2irc_quit $store{quitcookie}";
1092         } ],
1093
1094         # remove myself from the log destinations and exit everything
1095         [ dp => q{quitting rcon2irc (??{$store{quitcookie}}): log_dest_udp is (.*) *} => sub {
1096                 my ($dest) = @_;
1097                 my @dests = grep { $_ ne $config{dp_listen} } split ' ', $dest;
1098                 out dp => 0, 'log_dest_udp "' . join(" ", @dests) . '"';
1099                 exit 0;
1100                 return 0;
1101         } ],
1102
1103         # IRC PING
1104         [ irc => q{PING (.*)} => sub {
1105                 my ($data) = @_;
1106                 out irc => 1, "PONG $data";
1107                 return 1;
1108         } ],
1109
1110         # IRC PONG
1111         [ irc => q{:[^ ]* PONG .* :(.*)} => sub {
1112                 my ($data) = @_;
1113                 return 0
1114                         if not defined $store{irc_pingtime};
1115                 return 0
1116                         if $data ne $store{irc_pingtime};
1117                 print "* measured IRC line delay: @{[time() - $store{irc_pingtime}]}\n";
1118                 undef $store{irc_pingtime};
1119                 return 0;
1120         } ],
1121
1122         # detect channel join message and note hostname length to get the maximum allowed line length
1123         [ irc => q{(:(?i:(??{$store{irc_nick}}))![^ ]* )(?i:JOIN) :(?i:(??{$config{irc_channel}}))} => sub {
1124                 $store{irc_maxlen} = 510 - length($1);
1125                 $store{irc_joined_channel} = 1;
1126                 print "* detected maximum line length for channel messages: $store{irc_maxlen}\n";
1127                 return 0;
1128         } ],
1129
1130         # chat: Nexuiz server -> IRC channel
1131         [ dp => q{\001(.*?)\^7: (.*)} => sub {
1132                 my ($nick, $message) = map { color_dp2irc $_ } @_;
1133                 out irc => 0, "PRIVMSG $config{irc_channel} :<$nick\017> $message";
1134                 return 0;
1135         } ],
1136
1137         # chat: Nexuiz server -> IRC channel, nick set
1138         [ dp => q{:join:(\d+):(\d+):([^:]*):(.*)} => sub {
1139                 my ($id, $slot, $ip, $nick) = @_;
1140                 $nick = color_dp2irc $nick;
1141                 $store{"playernick_$id"} = $nick;
1142                 $store{"playerslot_$id"} = $slot;
1143                 $store{"playerip_$id"} = $ip;
1144                 return 0;
1145         } ],
1146
1147         # chat: Nexuiz server -> IRC channel, nick change/set
1148         [ dp => q{:name:(\d+):(.*)} => sub {
1149                 my ($id, $nick) = @_;
1150                 $nick = color_dp2irc $nick;
1151                 my $oldnick = $store{"playernick_$id"};
1152                 out irc => 0, "PRIVMSG $config{irc_channel} :* $oldnick\017 is now known as $nick";
1153                 $store{"playernick_$id"} = $nick;
1154                 return 0;
1155         } ],
1156
1157         # chat: Nexuiz server -> IRC channel, vote call
1158         [ dp => q{:vote:vcall:(\d+):(.*)} => sub {
1159                 my ($id, $command) = @_;
1160                 $command = color_dp2irc $command;
1161                 my $oldnick = $id ? $store{"playernick_$id"} : "(console)";
1162                 out irc => 0, "PRIVMSG $config{irc_channel} :* $oldnick\017 calls a vote for \"$command\017\"";
1163                 return 0;
1164         } ],
1165
1166         # chat: Nexuiz server -> IRC channel, vote stop
1167         [ dp => q{:vote:vstop:(\d+)} => sub {
1168                 my ($id) = @_;
1169                 my $oldnick = $id ? $store{"playernick_$id"} : "(console)";
1170                 out irc => 0, "PRIVMSG $config{irc_channel} :* $oldnick\017 stopped the vote";
1171                 return 0;
1172         } ],
1173
1174         # chat: Nexuiz server -> IRC channel, master login
1175         [ dp => q{:vote:vlogin:(\d+)} => sub {
1176                 my ($id) = @_;
1177                 my $oldnick = $id ? $store{"playernick_$id"} : "(console)";
1178                 out irc => 0, "PRIVMSG $config{irc_channel} :* $oldnick\017 logged in as master";
1179                 return 0;
1180         } ],
1181
1182         # chat: Nexuiz server -> IRC channel, master do
1183         [ dp => q{:vote:vdo:(\d+):(.*)} => sub {
1184                 my ($id, $command) = @_;
1185                 $command = color_dp2irc $command;
1186                 my $oldnick = $id ? $store{"playernick_$id"} : "(console)";
1187                 out irc => 0, "PRIVMSG $config{irc_channel} :* $oldnick\017 used his master status to do \"$command\017\"";
1188                 return 0;
1189         } ],
1190
1191         # chat: Nexuiz server -> IRC channel, result
1192         [ dp => q{:vote:v(yes|no|timeout):(\d+):(\d+):(\d+):(\d+):(-?\d+)} => sub {
1193                 my ($result, $yes, $no, $abstain, $not, $min) = @_;
1194                 my $spam = "$yes:$no" . (($min >= 0) ? " ($min needed)" : "") . ", $abstain didn't care, $not didn't vote";
1195                 out irc => 0, "PRIVMSG $config{irc_channel} :* the vote ended with $result: $spam";
1196                 return 0;
1197         } ],
1198
1199         # chat: IRC channel -> Nexuiz server
1200         [ irc => q{:([^! ]*)![^ ]* (?i:PRIVMSG) (?i:(??{$config{irc_channel}})) :(?i:(??{$store{irc_nick}}))(?: |: ?|, ?)(.*)} => sub {
1201                 my ($nick, $message) = @_;
1202                 $nick = color_dpfix $nick;
1203                         # allow the nickname to contain colors in DP format! Therefore, NO color_irc2dp on the nickname!
1204                 $message = color_irc2dp $message;
1205                 $message =~ s/(["\\])/\\$1/g;
1206                 out dp => 0, "rcon2irc_say_as \"$nick on IRC\" \"$message\"";
1207                 return 0;
1208         } ],
1209
1210         (
1211                 length $config{irc_trigger}
1212                         ?
1213                                 [ irc => q{:([^! ]*)![^ ]* (?i:PRIVMSG) (?i:(??{$config{irc_channel}})) :(?i:(??{$config{irc_trigger}}))(?: |: ?|, ?)(.*)} => sub {
1214                                         my ($nick, $message) = @_;
1215                                         $nick = color_dpfix $nick;
1216                                                 # allow the nickname to contain colors in DP format! Therefore, NO color_irc2dp on the nickname!
1217                                         $message = color_irc2dp $message;
1218                                         $message =~ s/(["\\])/\\$1/g;
1219                                         out dp => 0, "rcon2irc_say_as \"$nick on IRC\" \"$message\"";
1220                                         return 0;
1221                                 } ]
1222                         :
1223                                 ()
1224         ),
1225
1226         # irc: CTCP VERSION reply
1227         [ irc => q{:([^! ]*)![^ ]* (?i:PRIVMSG) (?i:(??{$store{irc_nick}})) :\001VERSION( .*)?\001} => sub {
1228                 my ($nick) = @_;
1229                 my $ver = $store{dp_version} or return 0;
1230                 $ver .= ", rcon2irc $VERSION";
1231                 out irc => 0, "NOTICE $nick :\001VERSION $ver\001";
1232         } ],
1233
1234         # on game start, notify the channel
1235         [ dp => q{:gamestart:(.*):[0-9.]*} => sub {
1236                 my ($map) = @_;
1237                 $store{playing} = 1;
1238                 $store{map} = $map;
1239                 $store{map_starttime} = time();
1240                 my $slotsstr = nex_slotsstring();
1241                 out irc => 0, "PRIVMSG $config{irc_channel} :\00304" . $map . "\017 has begun$slotsstr";
1242                 delete $store{lms_blocked};
1243                 return 0;
1244         } ],
1245
1246         # on game over, clear the current map
1247         [ dp => q{:gameover} => sub {
1248                 $store{playing} = 0;
1249                 return 0;
1250         } ],
1251
1252         # scores: Nexuiz server -> IRC channel (start)
1253         [ dp => q{:scores:(.*):(\d+)} => sub {
1254                 my ($map, $time) = @_;
1255                 $store{scores} = {};
1256                 $store{scores}{map} = $map;
1257                 $store{scores}{time} = $time;
1258                 $store{scores}{players} = [];
1259                 delete $store{lms_blocked};
1260                 return 0;
1261         } ],
1262
1263         # scores: Nexuiz server -> IRC channel, legacy format
1264         [ dp => q{:player:(-?\d+):(\d+):(\d+):(\d+):(\d+):(.*)} => sub {
1265                 my ($frags, $deaths, $time, $team, $id, $name) = @_;
1266                 return if not exists $store{scores};
1267                 push @{$store{scores}{players}}, [$frags, $team, $name]
1268                         unless $frags <= -666; # no spectators
1269                 return 0;
1270         } ],
1271
1272         # scores: Nexuiz server -> IRC channel (CTF), legacy format
1273         [ dp => q{:teamscores:(\d+:-?\d*(?::\d+:-?\d*)*)} => sub {
1274                 my ($teams) = @_;
1275                 return if not exists $store{scores};
1276                 $store{scores}{teams} = {split /:/, $teams};
1277                 return 0;
1278         } ],
1279
1280         # scores: Nexuiz server -> IRC channel, new format
1281         [ dp => q{:player:see-labels:(\d+)[-0-9,]*:(\d+):(\d+):(\d+):(.*)} => sub {
1282                 my ($frags, $time, $team, $id, $name) = @_;
1283                 return if not exists $store{scores};
1284                 push @{$store{scores}{players}}, [$frags, $team, $name];
1285                 return 0;
1286         } ],
1287
1288         # scores: Nexuiz server -> IRC channel (CTF), new format
1289         [ dp => q{:teamscores:see-labels:(\d+)[-0-9,]*:(\d+)} => sub {
1290                 my ($frags, $team) = @_;
1291                 return if not exists $store{scores};
1292                 $store{scores}{teams}{$team} = $frags;
1293                 return 0;
1294         } ],
1295
1296         # scores: Nexuiz server -> IRC channel
1297         [ dp => q{:end} => sub {
1298                 return if not exists $store{scores};
1299                 my $s = $store{scores};
1300                 delete $store{scores};
1301                 my $teams_matter = nex_is_teamplay($s->{map});
1302
1303                 my @t = ();
1304                 my @p = ();
1305
1306                 if($teams_matter)
1307                 {
1308                         # put players into teams
1309                         my %t = ();
1310                         for(@{$s->{players}})
1311                         {
1312                                 my $thisteam = ($t{$_->[1]} ||= {score => 0, team => $_->[1], players => []});
1313                                 push @{$thisteam->{players}}, [$_->[0], $_->[1], $_->[2]];
1314                                 if($s->{teams})
1315                                 {
1316                                         $thisteam->{score} = $s->{teams}{$_->[1]};
1317                                 }
1318                                 else
1319                                 {
1320                                         $thisteam->{score} += $_->[0];
1321                                 }
1322                         }
1323
1324                         # sort by team score
1325                         @t = sort { $b->{score} <=> $a->{score} } values %t;
1326
1327                         # sort by player score
1328                         @p = ();
1329                         for(@t)
1330                         {
1331                                 @{$_->{players}} = sort { $b->[0] <=> $a->[0] } @{$_->{players}};
1332                                 push @p, @{$_->{players}};
1333                         }
1334                 }
1335                 else
1336                 {
1337                         @p = sort { $b->[0] <=> $a->[0] } @{$s->{players}};
1338                 }
1339
1340                 # no display for empty server
1341                 return 0
1342                         if !@p;
1343
1344                 # make message fit somehow
1345                 for my $maxnamelen(reverse 3..64)
1346                 {
1347                         my $scores_string = "PRIVMSG $config{irc_channel} :\00304" . $s->{map} . "\017 ended:";
1348                         if($teams_matter)
1349                         {
1350                                 my $sep = ' ';
1351                                 for(@t)
1352                                 {
1353                                         $scores_string .= $sep . sprintf "\003%02d\%d\017", $color_team2irc_table{$_->{team}}, $_->{score};
1354                                         $sep = ':';
1355                                 }
1356                         }
1357                         my $sep = '';
1358                         for(@p)
1359                         {
1360                                 my ($frags, $team, $name) = @$_;
1361                                 $name = color_dpfix substr($name, 0, $maxnamelen);
1362                                 if($teams_matter)
1363                                 {
1364                                         $name = "\003" . $color_team2irc_table{$team} . " " . color_dp2none $name;
1365                                 }
1366                                 else
1367                                 {
1368                                         $name = " " . color_dp2irc $name;
1369                                 }
1370                                 $scores_string .= "$sep$name\017 $frags";
1371                                 $sep = ',';
1372                         }
1373                         if(length($scores_string) <= ($store{irc_maxlen} || 256))
1374                         {
1375                                 out irc => 0, $scores_string;
1376                                 return 0;
1377                         }
1378                 }
1379                 out irc => 0, "PRIVMSG $config{irc_channel} :\001ACTION would have LIKED to put the scores here, but they wouldn't fit :(\001";
1380                 return 0;
1381         } ],
1382
1383         # complain when system load gets too high
1384         [ dp => q{timing:   (([0-9.]*)% CPU, ([0-9.]*)% lost, offset avg ([0-9.]*)ms, max ([0-9.]*)ms, sdev ([0-9.]*)ms)} => sub {
1385                 my ($all, $cpu, $lost, $avg, $max, $sdev) = @_;
1386                 return 0 # don't complain when just on the voting screen
1387                         if !$store{playing};
1388                 return 0 # don't complain if it was less than 0.5%
1389                         if $lost < 0.5;
1390                 return 0 # don't complain if nobody is looking
1391                         if $store{slots_active} == 0;
1392                 return 0 # don't complain in the first two minutes
1393                         if time() - $store{map_starttime} < 120;
1394                 return 0 # don't complain if it was already at least half as bad in this round
1395                         if $store{map_starttime} == $store{timingerror_map_starttime} and $lost <= 2 * $store{timingerror_lost};
1396                 $store{timingerror_map_starttime} = $store{map_starttime};
1397                 $store{timingerror_lost} = $lost;
1398                 out dp => 0, 'rcon2irc_say_as server "There are currently some severe system load problems. The admins have been notified."';
1399                 out irc => 1, "PRIVMSG $config{irc_channel} :\001ACTION has big trouble on $store{map} after @{[int(time() - $store{map_starttime})]}s: $all\001";
1400                 #out irc => 1, "PRIVMSG OpBaI :\001ACTION has big trouble on $store{map} after @{[int(time() - $store{map_starttime})]}s: $all\001";
1401                 return 0;
1402         } ],
1403 );
1404
1405
1406
1407 # Load plugins and add them to the handler list in the front.
1408 for my $p(split ' ', $config{plugins})
1409 {
1410         my @h = eval { do $p; }
1411                 or die "Invalid plugin $p: $@";
1412         for(reverse @h)
1413         {
1414                 ref $_ eq 'ARRAY' or die "Invalid plugin $p: did not return a list of arrays";
1415                 @$_ == 3 or die "Invalid plugin $p: did not return a list of three-element arrays";
1416                 !ref $_->[0] && !ref $_->[1] && ref $_->[2] eq 'CODE' or die "Invalid plugin $p: did not return a list of string-string-sub arrays";
1417                 unshift @handlers, $_;
1418         }
1419 }
1420
1421
1422
1423 # verify that the server is up by letting it echo back a string that causes
1424 # re-initialization of the required aliases
1425 out dp => 0, 'echo "Unknown command \"rcon2irc_eval\""'; # assume the server has been restarted
1426
1427
1428
1429 # regularily, query the server status and if it still is connected to us using
1430 # the log_dest_udp feature. If not, we will detect the response to this rcon
1431 # command and re-initialize the server's connection to us (either by log_dest_udp
1432 # not containing our own IP:port, or by rcon2irc_eval not being a defined command).
1433 schedule sub {
1434         my ($timer) = @_;
1435         out dp => 0, 'sv_cmd bans', 'status 1', 'log_dest_udp', 'rcon2irc_eval set dummy 1';
1436         $store{status_waiting} = -1;
1437         schedule $timer => (exists $store{dp_hostname} ? $config{dp_status_delay} : 1);;
1438 } => 1;
1439
1440
1441
1442 # Continue with connecting to IRC as soon as we get our first status reply from
1443 # the DP server (which contains the server's hostname that we'll use as
1444 # realname for IRC).
1445 schedule sub {
1446         my ($timer) = @_;
1447
1448         # log on to IRC when needed
1449         if(exists $store{dp_hostname} && !exists $store{irc_logged_in})
1450         {
1451                 $store{irc_nick_requested} = $config{irc_nick};
1452                 out irc => 1, "NICK $config{irc_nick}", "USER $config{irc_user} localhost localhost :$store{dp_hostname}";
1453                 $store{irc_logged_in} = 1;
1454                 undef $store{irc_maxlen};
1455                 undef $store{irc_pingtime};
1456         }
1457
1458         schedule $timer => 1;;
1459 } => 1;
1460
1461
1462
1463 # Regularily ping the IRC server to detect if the connection is down. If it is,
1464 # schedule an IRC error that will cause reconnection later.
1465 schedule sub {
1466         my ($timer) = @_;
1467
1468         if($store{irc_logged_in})
1469         {
1470                 if(defined $store{irc_pingtime})
1471                 {
1472                         # IRC connection apparently broke
1473                         # so... KILL IT WITH FIRE
1474                         $channels{system}->send("error irc", 0);
1475                 }
1476                 else
1477                 {
1478                         # everything is fine, send a new ping
1479                         $store{irc_pingtime} = time();
1480                         out irc => 1, "PING $store{irc_pingtime}";
1481                 }
1482         }
1483
1484         schedule $timer => $config{irc_ping_delay};;
1485 } => 1;
1486
1487
1488
1489 # Main loop.
1490 for(;;)
1491 {
1492         # Build up an IO::Select object for all our channels.
1493         my $s = IO::Select->new();
1494         for my $chan(values %channels)
1495         {
1496                 $s->add($_) for $chan->fds();
1497         }
1498
1499         # wait for something to happen on our sockets, or wait 2 seconds without anything happening there
1500         $s->can_read(2);
1501         my @errors = $s->has_exception(0);
1502
1503         # on every channel, look for incoming messages
1504         CHANNEL:
1505         for my $chanstr(keys %channels)
1506         {
1507                 my $chan = $channels{$chanstr};
1508                 my @chanfds = $chan->fds();
1509
1510                 for my $chanfd(@chanfds)
1511                 {
1512                         if(grep { $_ == $chanfd } @errors)
1513                         {
1514                                 # STOP! This channel errored!
1515                                 $channels{system}->send("error $chanstr", 0);
1516                                 next CHANNEL;
1517                         }
1518                 }
1519
1520                 eval
1521                 {
1522                         for my $line($chan->recv())
1523                         {
1524                                 # found one! Check if it matches the regular expression of one of
1525                                 # our handlers...
1526                                 my $handled = 0;
1527                                 my $private = 0;
1528                                 for my $h(@handlers)
1529                                 {
1530                                         my ($chanstr_wanted, $re, $sub) = @$h;
1531                                         next
1532                                                 if $chanstr_wanted ne $chanstr;
1533                                         use re 'eval';
1534                                         my @matches = ($line =~ /^$re$/s);
1535                                         no re 'eval';
1536                                         next
1537                                                 unless @matches;
1538                                         # and if it is a match, handle it.
1539                                         ++$handled;
1540                                         my $result = $sub->(@matches);
1541                                         $private = 1
1542                                                 if $result < 0;
1543                                         last
1544                                                 if $result;
1545                                 }
1546                                 # print the message, together with info on whether it has been handled or not
1547                                 if($private)
1548                                 {
1549                                         print "           $chanstr >> (private)\n";
1550                                 }
1551                                 elsif($handled)
1552                                 {
1553                                         print "           $chanstr >> $line\n";
1554                                 }
1555                                 else
1556                                 {
1557                                         print "unhandled: $chanstr >> $line\n";
1558                                 }
1559                         }
1560                         1;
1561                 } or do {
1562                         if($@ eq "read error\n")
1563                         {
1564                                 $channels{system}->send("error $chanstr", 0);
1565                                 next CHANNEL;
1566                         }
1567                         else
1568                         {
1569                                 # re-throw
1570                                 die $@;
1571                         }
1572                 };
1573         }
1574
1575         # handle scheduled tasks...
1576         my @t = @tasks;
1577         my $t = time();
1578         # by emptying the list of tasks...
1579         @tasks = ();
1580         for(@t)
1581         {
1582                 my ($time, $sub) = @$_;
1583                 if($t >= $time)
1584                 {
1585                         # calling them if they are schedled for the "past"...
1586                         $sub->($sub);
1587                 }
1588                 else
1589                 {
1590                         # or re-adding them to the task list if they still are scheduled for the "future"
1591                         push @tasks, [$time, $sub];
1592                 }
1593         }
1594 }