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