File Coverage

blib/lib/Games/Roguelike/World/Daemon.pm
Criterion Covered Total %
statement 111 204 54.4
branch 21 60 35.0
condition 4 18 22.2
subroutine 15 29 51.7
pod 11 18 61.1
total 162 329 49.2


line stmt bran cond sub pod time code
1             package Games::Roguelike::World::Daemon;
2              
3 1     1   64348 use strict;
  1         3  
  1         69  
4              
5 1     1   2139 use Games::Roguelike::Utils qw(:all);
  1         3  
  1         309  
6 1     1   8 use Games::Roguelike::Console::ANSI;
  1         2  
  1         47  
7 1     1   7 use Games::Roguelike::Mob;
  1         1  
  1         21  
8 1     1   5 use POSIX;
  1         2  
  1         11  
9              
10 1     1   4511 use IO::Socket;
  1         37565  
  1         8  
11 1     1   13027 use IO::Select;
  1         2466  
  1         69  
12 1     1   9 use IO::File qw(); # this prevents warnings on win32
  1         3  
  1         47  
13              
14             our $VERSION = '0.4.' . [qw$Revision: 253 $]->[1];
15              
16 1     1   1305 use Time::HiRes qw(time);
  1         2459  
  1         8  
17              
18 1     1   237 use base 'Games::Roguelike::World';
  1         3  
  1         1023  
19              
20             # purpose of module:
21             #
22             # multi-user telnet daemon
23             # finite-state processor, allows for single-thread engine
24              
25             =head1 NAME
26              
27             Games::Roguelike::World::Daemon - roguelike game telnet daemon
28              
29             =head1 SYNOPSIS
30              
31             # for an extended example with move overrides, see the scripts/netgame included
32              
33             use strict;
34              
35             package myWorld; # always override
36             use base 'Games::Roguelike::World::Daemon';
37              
38             my $r = myWorld->new(w=>80,h=>50,dispw=>40,disph=>18); # create a networked world
39             $r->area(new Games::Roguelike::Area(name=>'1')); # create a new area in this world called "1"
40             $r->area->generate('cavelike'); # make a cavelike maze
41              
42             while (1) {
43             $r->proc();
44             }
45              
46             sub readinput { # called when input is available
47             my $self = shift;
48             if (my $c = $self->getch()) { # returns undef on failure
49             if ($self->{vp}->kbdmove($c, 1)) { # '1' in second param means "test only"
50             $r->queuemove($self->{vp}, $c); # if the move is good, queue it
51             }
52             }
53             }
54              
55             sub newconn { # called when someone connects
56             my $self = shift;
57             my $char = mychar->new($self->area(1), # create a new character
58             sym=>'@',
59             color=>'green',
60             pov=>7
61             );
62             $self->{vp} = $char; # viewpoint is a connection state obect
63             $self->{state} = 'MOVE'; # set state (another state object)
64             }
65              
66             package mychar;
67             use base 'Games::Roguelike::Mob';
68              
69             =head1 DESCRIPTION
70              
71             This module uses the Games::Roguelike::World object as the basis for a finite-state based
72             network game engine.
73              
74             * uses Games::Roguelike::Console::ANSI library to draw the current area
75             * currently assumes Games::Roguelike::Mob's as characters in the game
76             * currently assumes Games::Roguelike::Item's as items in the game
77              
78             The module provides th eservice of accepting connections, maintainting he association between
79             the connection and a "state" and "viewpoint" for each connection, managing "tick" times,
80             and rendering maps for each connection.
81              
82             =head2 METHODS
83              
84             =over
85              
86             =cut
87              
88             my $WIN32 = ($^O=~/win32/i);
89             my @SOCKS;
90              
91             =item new ()
92              
93             Similar to ::World new, but with arguments: host, port, and addr
94              
95             This begins listening for connections, and sets up some signal handlers for
96             graceful death.
97              
98             =cut
99              
100             sub new {
101 1     1 1 1504 my $pkg = shift;
102 1         12 my $r = $pkg->SUPER::new(@_, noconsole=>1);
103 1         3 bless $r, $pkg;
104              
105 1 50       5 $r->{tick} = 0.5 if !$r->{tick};
106              
107 1         35 local $! = 0;
108 1         2 my %addrs;
109              
110 1 50       4 $addrs{LocalAddr} = $r->{addr} if $r->{addr};
111 1 50       3 $addrs{LocalHost} = $r->{host} if $r->{host};
112 1 50       4 $addrs{LocalPort} = $r->{port} if $r->{port};
113              
114 1         11 $r->{main_sock} = new IO::Socket::INET(
115             %addrs,
116             Listen => 1,
117             ReuseAddr => 1);
118              
119 1 50       330 die $! unless $r->{main_sock};
120              
121 1 50       11 $r->{stdout} = *STDOUT unless $r->{stdout};
122              
123 1         9 $r->{read_set} = new IO::Select();
124 1         15 $r->{read_set}->add($r->{main_sock});
125 1         40 $r->{write_set} = new IO::Select();
126              
127 1         9 push @SOCKS, $r->{main_sock};
128            
129 1         4 $SIG{__DIE__} = \&sig_die_handler;
130 1         11 $SIG{INT} = \&sig_int_handler;
131              
132 1         5 return $r;
133             }
134              
135             sub sig_int_handler {
136 0     0 0 0 sig_die_handler();
137 0         0 exit(0);
138             }
139              
140             sub sig_die_handler {
141 0     0 0 0 for (@SOCKS) {
142 0         0 close($_);
143             }
144 0         0 undef @SOCKS;
145 0         0 1;
146             }
147              
148             sub DESTROY {
149 1     1   58 my $r = shift;
150 1 50       5 if ($r->{main_sock}) {
151 1         5 $r->{main_sock}->close();
152             }
153 1         25 $r->SUPER::DESTROY();
154             }
155              
156             =item proc ()
157              
158             Look for waiting input and calls:
159              
160             newconn() - for new conneciton
161             readinput() - when input is available
162             tick() - to process per-turn moves
163             drawallmaps() - to render all the maps
164              
165             When those functions are called the class {vp} and {state} variables are
166             set to the connection's "viewpoint" (character) and "state".
167              
168             Also, the special scalar state 'QUIT' gracefully removes a connection.
169              
170             (It might be interesting to use code refs as states)
171              
172             =cut
173              
174             sub proc {
175 2     2 1 1295 my $self = shift;
176              
177             # $self->log("proc " . $self->{read_set}->count());
178              
179 2         8 my $now = time();
180 2 100       10 $self->{ts} = $now unless $self->{ts};
181 2         14 my $rem = max(0.1, $self->{tick} - ($now - $self->{ts}));
182              
183             # $self->log("rem", $rem);
184              
185 2         18 my ($new_readable, $new_writable, $new_error) = IO::Select->select($self->{read_set}, $self->{write_set}, $self->{read_set}, $rem + .01);
186              
187 2         123 foreach my $sock (@$new_readable) {
188 2 100       8 if ($sock == $self->{main_sock}) {
189 1         9 my $new_sock = $sock->accept();
190 1         117 $self->log("incoming connection from: " , $new_sock->peerhost());
191             # new socket may not be readable yet.
192 1 50       15 if ($new_sock) {
193 1         3 push @SOCKS, $new_sock;
194 1         3 ++$self->{req_count};
195 1 50       4 if ($WIN32) {
196 0         0 ioctl($new_sock, 0x8004667e, pack("I", 1));
197             } else {
198 1         7 fcntl($new_sock, F_SETFL(), O_NONBLOCK());
199             }
200 1         5 $new_sock->autoflush(1);
201 1         32 my @opts;
202             # pass through some options to console object on new connections
203 1         4 for (qw(usereadkey noinit)) {
204 2 100       11 push @opts, $_=>$self->{$_} if defined $self->{$_};
205             }
206 1         5 $self->{read_set}->add($new_sock);
207 1         49 *$new_sock{HASH}->{con} = new Games::Roguelike::Console::ANSI (in=>$new_sock, out=>$new_sock, @opts);
208 1         5 *$new_sock{HASH}->{time} = time();
209 1         5 *$new_sock{HASH}->{errc} = 0;
210 1         4 $self->{con} = *$new_sock{HASH}->{con};
211 1         10 $self->echo_off();
212 1         3 $self->{state} = '';
213 1         2 $self->{vp} = '';
214 1         5 $self->newconn($new_sock);
215 1         6 *$new_sock{HASH}->{state} = $self->{state};
216 1         4 *$new_sock{HASH}->{char} = $self->{vp};
217 1 50 33     14 $self->{vp}->{con} = $self->{con} if $self->{vp} && !$self->{vp}->{con};
218 1         15 $self->log("state is: " , $self->{state});
219             }
220             } else {
221 1 50 33     10 if ($sock->eof() || !$sock->connected() || (*$sock{HASH}->{errc} > 5)) {
      33        
222 0         0 $self->{state} = 'QUIT';
223             } else {
224 1         61 $self->log("reading from: " , $sock->peerhost());
225 1         6 $self->log("state was: " , $self->{state});
226 1         4 $self->{con} = *$sock{HASH}->{con};
227 1         3 $self->{state} = *$sock{HASH}->{state};
228 1         3 $self->{vp} = *$sock{HASH}->{char};
229 1         4 $self->readinput($sock);
230 1         6 *$sock{HASH}->{state} = $self->{state};
231 1         2 *$sock{HASH}->{char} = $self->{vp};
232 1 50 33     9 $self->{vp}->{con} = $self->{con} if $self->{vp} && !$self->{vp}->{con};
233 1         8 $self->log("state is: " , $self->{state});
234             }
235              
236 1 50       6 if ($self->{state} eq 'QUIT') {
237 1         3 eval {
238 1 50       10 *$sock{HASH}->{char}->{area}->delmob(*$sock{HASH}->{char}) if *$sock{HASH}->{char};
239             };
240 1         6 $self->{read_set}->remove($sock);
241 1         40 $sock->close();
242             }
243             }
244             }
245 2         63 foreach my $sock (@$new_error) {
246 0         0 *$sock{HASH}->{char}->{area}->delmob(*$sock{HASH}->{char});
247 0         0 $self->{read_set}->remove($sock);
248 0         0 close($sock);
249             }
250             {
251 2         4 my $now = time();
  2         7  
252 2         5 my $rem = $now - $self->{ts};
253              
254 2 50       13 if ($rem >= $self->{tick}) {
255             #$self->log("tick");
256 0         0 $self->tick();
257 0         0 $self->drawallmaps();
258 0         0 $self->{ts} = $now;
259             }
260             }
261             }
262              
263             sub drawallmaps {
264 0     0 0 0 my $self = shift;
265 0         0 foreach my $sock ($self->{read_set}->handles()) {
266 0 0       0 if (*$sock{HASH}->{char}) {
267 0         0 $self->{vp} = *$sock{HASH}->{char};
268 0         0 $self->{con} = *$sock{HASH}->{con};
269 0         0 $self->{area} = $self->{vp}->{area};
270 0         0 my $color = $self->{vp}->{color};
271 0         0 my $sym = $self->{vp}->{sym};
272 0         0 $self->setfocuscolor();
273 0         0 $self->drawmap();
274 0         0 $sock->flush();
275 0         0 $self->{vp}->{color} = $color;
276 0         0 $self->{vp}->{sym} = $sym;
277             }
278             }
279             }
280              
281             sub echo_off {
282 1     1 0 1 my $self = shift;
283 1         3 my $sock = $self->{con}->{out};
284             # i will echo if needed, you don't echo, i will suppress go ahead, you do suppress goahead
285 1         46 print $sock "\xff\xfb\x01\xff\xfb\x03\xff\xfd\x03";
286             }
287              
288             sub echo_on {
289 0     0 0 0 my $self = shift;
290 0         0 my $sock = $self->{con}->{out};
291             # i wont echo, you do echo
292 0         0 print $sock "\xff\xfc\x01\xff\xfd\x01";
293             }
294              
295             =item getstr ()
296              
297             Reads a string from the active connection.
298              
299             Returns undef if the string is not ready.
300              
301             =cut
302              
303             sub hexify {
304 0     0 0 0 my ($s) = @_;
305 0         0 my $ret = '';
306 0         0 for (split(//,$s)) {
307 0         0 $ret .= sprintf("x%x", ord($_));
308 0 0       0 $ret .= "($_)" if $_ =~ /\w/;
309             }
310 0         0 return $ret;
311             }
312              
313             sub getstr {
314 0     0 1 0 my $self = shift;
315 0         0 my $sock = $self->{con}->{in};
316 0         0 my $first = 1;
317              
318 0         0 while (1) {
319 0         0 my $b = $self->getch();
320 0 0 0     0 if (!defined($b)) {
    0          
    0          
321 0 0       0 ++(*$sock{HASH}->{errc}) if $first;
322 0         0 return undef;
323             } elsif($b eq 'BACKSPACE') {
324 0         0 $self->log("getstr read $b");
325 0 0       0 if (length(*$sock{HASH}->{sbuf}) > 0) {
326 0         0 syswrite($sock, chr(8), 1);
327 0         0 syswrite($sock, ' ', 1);
328 0         0 syswrite($sock, chr(8), 1);
329 0         0 substr(*$sock{HASH}->{sbuf},-1,1) = '';
330             }
331             } elsif(length($b) > 1 || $b eq '') {
332 0         0 next;
333             } else {
334 0         0 $self->log("getstr read " . ord($b));
335 0         0 syswrite($sock,$b,1); # echo on getstr
336 0 0       0 $first = 0 if $first;
337 0         0 *$sock{HASH}->{errc} = 0;
338 0         0 *$sock{HASH}->{sbuf} .= $b;
339             }
340 0 0 0     0 if ($b eq "\n" || $b eq "\r") {
341 0         0 my $temp = *$sock{HASH}->{sbuf};
342 0         0 *$sock{HASH}->{sbuf} = '';
343 0         0 return $temp;
344             }
345             }
346             }
347              
348             =item getch ()
349              
350             Reads a character from the active connection.
351              
352             Returns undef if no input is ready.
353              
354             =cut
355              
356             sub getch {
357 0     0 1 0 my $self = shift;
358 0         0 my $c = $self->{con}->nbgetch();
359 0 0       0 if (! defined $c) {
360 0         0 my $sock = $self->{con}->{in};
361 0         0 ++(*$sock{HASH}->{errc})
362             }
363 0         0 return $c;
364             }
365              
366             =item charmsg ($char)
367              
368             Calls showmsg on the console contained in $char;
369              
370             =cut
371              
372             sub charmsg {
373 0     0 1 0 my $self = shift;
374 0         0 my ($char, $msg, $attr) = @_;
375 0         0 my $con = $self->{con};
376 0         0 $self->{con} = $char->{con};
377 0         0 $self->showmsg($msg,$attr);
378 0         0 $self->{con} = $con;
379             }
380              
381             # log and debug print are essentially the same thing
382              
383             sub log {
384 5     5 0 88 my $self = shift;
385 5         11 my $out = $self->{stdout};
386 5         277 print $out scalar(localtime()) . "\t" . join("\t", @_) . "\n";
387             }
388              
389             sub dprint {
390 0     0 1   my $self = shift;
391 0           my $out = $self->{stdout};
392 0           print $out scalar(localtime()) . "\t" . join("\t", @_) . "\n";
393             }
394              
395             # override this for your game
396              
397             # for now, the way we report back state changes is to modify
398             #
399             # $self->{state}
400             # $self->{vp} # for creating/loading/switching to a character's viewpoint
401             #
402             # these are then linked to the socket
403             #
404             # actual action/movement by a charcter should be queued here, then processed according to a random sort and/or a sort based
405             # on the speed of the character at tick() time
406             #
407             # ie: if an ogre and a sprite move during the same tick, the sprite always goes first, even if the
408             # ogre's player has a faster internet connection
409             #
410             # use getch for a no-echo read of a character
411             # use getstr for an echoed read of a carraige return delimited string
412             #
413             # both will return undef if there's no input yet
414             # don't "wait" for anything in your functons, game is single threaded!
415             #
416              
417             =item readinput ()
418              
419             Must override and call getch() or getstr().
420              
421             The {vp}, {state}, and {con} vars are set on this call, can be
422             changed, and will be preserved.
423              
424             Actual action/movement by a charcter should be queued here, then processed according to
425             a random sort and/or a sort based on the speed of the character.
426              
427             For example: If a tank and a motorcycle move during the same tick, the motorcycle would always go first, even if the tank's player has a faster internet connection. Queueing the moves allows you to do this.
428              
429             Remember never to do something that blocks or waits for input, game is single-threaded.
430              
431             =cut
432              
433             sub readinput {
434 0     0 1   die "need to overide this, see netgame example";
435             }
436              
437             # override this for intro screen, please enter yor name, etc.
438             # use $self->{con} for the the Games::Roguelike::Console object (remember, chars are not actually written until flushed, which you can do here if you want)
439              
440             =item newconn ()
441              
442             Must override and either create a character or show an intro screen, or something.
443              
444             The {vp}, {state}, and {con} vars are set on this call, can be changed, and will be preserved.
445              
446             =cut
447              
448             sub newconn {
449 0     0 1   die "need to overide this, see netgame example";
450             }
451              
452             =item setfocuscolor ()
453              
454             Change the display color/symbol of the {vp} character here in order to distinguish it from other (enemy?) characters.
455              
456             =cut
457              
458             # change the symbol/color of the character when it's "in focus"
459             sub setfocuscolor {
460 0     0 1   my $self = shift;
461 0           $self->{vp}->{color} = 'bold yellow';
462             }
463              
464             =item queuemove ($char, $move[, $msg])
465              
466             Pushes a "move" for char $char showing message $msg. By default will not queu if a move has been set. The "move" variabe is set in the "char" object to record whether a move has occured.
467              
468             =cut
469              
470             # queue a move until tick time
471             sub queuemove {
472 0     0 1   my $self = shift;
473 0           my ($char, $move, $msg) = @_;
474 0 0         if ($char->{move}) {
475             # already moving, so do nothing
476             # might what to show a message here
477             } else {
478 0 0         $self->showmsg($msg) if $msg;
479 0           $self->{con}->refresh();
480 0           $char->{move} = $move;
481 0           push @{$self->{qmove}}, $char;
  0            
482             }
483             }
484              
485             # override this to sort the queue by character speed, display hit points, turn-counts or other status info, etc.
486             # override to process character and mob actions/movement map is auto-redrawn for all connections after the tick (if changed)
487             # don't try to draw here... since no character has the focus...it will fail
488              
489             =item tick ()
490              
491             Override for per-turn move processing. This is called for each game turn, which defaults to a half-second.
492             Default behavior is to sort all the queued moves and execute them.
493              
494             A good way to handle this might be to make the "moves" be code references, which get passed "char" as the argument.
495              
496             =cut
497              
498             sub tick {
499 0     0 1   my $self = shift;
500 0           my @auto;
501 0           foreach my $char (randsort(@{$self->{qmove}})) {
  0            
502 0           $char->kbdmove($char->{move});
503 0           $char->{move} = '';
504             }
505             }
506              
507             =back
508              
509             =head1 BUGS
510              
511             Currently this fails on Win32
512              
513             =head1 SEE ALSO
514              
515             L
516              
517             =head1 AUTHOR
518              
519             Erik Aronesty C
520              
521             =head1 LICENSE
522              
523             This program is free software; you can redistribute it and/or
524             modify it under the same terms as Perl itself.
525              
526             See L or the included LICENSE file.
527              
528             =cut
529              
530             1;