ViewVC Help
View File | Revision Log | Show Annotations | Download File
/cvs/deliantra/Deliantra/Deliantra/Protocol.pm
Revision: 1.18
Committed: Wed Apr 12 21:43:22 2006 UTC (18 years, 1 month ago) by root
Branch: MAIN
Changes since 1.17: +0 -2 lines
Log Message:
queue askface commands instead of sending them in one go

File Contents

# Content
1 =head1 NAME
2
3 Crossfire::Protocol - client protocol module
4
5 =head1 SYNOPSIS
6
7 use base Crossfire::Protocol; # you have to subclass
8
9 =head1 DESCRIPTION
10
11 Base class to implement a corssfire client.
12
13 =over 4
14
15 =cut
16
17 package Crossfire::Protocol;
18
19 our $VERSION = '0.1';
20
21 use strict;
22
23 use AnyEvent;
24 use IO::Socket::INET;
25
26 =item new Crossfire::Protocol host => ..., port => ...
27
28 =cut
29
30 sub new {
31 my $class = shift;
32 my $self = bless {
33 mapw => 13,
34 maph => 13,
35 max_outstanding => 2,
36 @_
37 }, $class;
38
39 $self->{fh} = new IO::Socket::INET PeerHost => $self->{host}, PeerPort => $self->{port}
40 or die "$self->{host}:$self->{port}: $!";
41 $self->{fh}->blocking (0); # stupid nonblock default
42
43 my $buf;
44
45 $self->{w} = AnyEvent->io (fh => $self->{fh}, poll => 'r', cb => sub {
46 if (sysread $self->{fh}, $buf, 16384, length $buf) {
47 for (;;) {
48 last unless 2 <= length $buf;
49 my $len = unpack "n", $buf;
50 last unless $len + 2 <= length $buf;
51
52 substr $buf, 0, 2, "";
53 $self->feed (substr $buf, 0, $len, "");
54 }
55 } else {
56 delete $self->{w};
57 close $self->{fh};
58 }
59 });
60
61 $self->{setup_req} = {
62 sound => 1,
63 exp64 => 1,
64 map1acmd => 1,
65 itemcmd => 2,
66 darkness => 1,
67 facecache => 1,
68 newmapcmd => 1,
69 extendedTextInfos => 1,
70 };
71
72 $self->send ("version 1023 1027 perlclient");
73 $self->send_setup;
74
75 $self
76 }
77
78 sub feed {
79 my ($self, $data) = @_;
80
81 $data =~ s/^(\S+)(?:\s|$)//
82 or return;
83
84 my $command = "feed_$1";
85
86 $self->$command ($data);
87 }
88
89 sub feed_version {
90 my ($self, $version) = @_;
91 }
92
93 sub feed_setup {
94 my ($self, $data) = @_;
95
96 $data =~ s/^ +//;
97
98 $self->{setup} = { split / +/, $data };
99
100 my ($mapw, $maph) = split /x/, $self->{setup}{mapsize};
101
102 if ($mapw != $self->{mapw} || $maph != $self->{maph}) {
103 ($self->{mapw}, $self->{maph}) = ($mapw, $maph);
104 $self->send_setup;
105 } else {
106 $self->send ("addme");
107 }
108
109 $self->feed_newmap;
110 }
111
112 sub feed_addme_success {
113 my ($self, $data) = @_;
114 }
115
116 sub feed_addme_failure {
117 my ($self, $data) = @_;
118 # maybe should notify user
119 }
120
121 =back
122
123 =head2 METHODS THAT CAN/MUST BE OVERWRITTEN
124
125 =over 4
126
127 =cut
128
129 sub feed_face1 {
130 my ($self, $data) = @_;
131
132 my ($num, $chksum, $name) = unpack "nNa*", $data;
133
134 my $face = $self->{face}[$num] = { name => $name, chksum => $chksum };
135
136 if (my $data = $self->face_find ($face)) {
137 $face->{image} = $data;
138 $self->face_update ($face);
139 } else {
140 $self->send_queue ("askface $num");
141 }
142 }
143
144 =item $conn->anim_update ($num) [OVERWRITE]
145
146 =cut
147
148 sub anim_update { }
149
150 sub feed_anim {
151 my ($self, $data) = @_;
152
153 my ($num, @faces) = unpack "n*", $data;
154
155 $self->{anim}[$num] = \@faces;
156
157 $self->anim_update ($num);
158 }
159
160 =item $conn->play_sound ($x, $y, $soundnum, $type)
161
162 =cut
163
164 sub sound_play { }
165
166 sub feed_sound {
167 my ($self, $data) = @_;
168
169 $self->sound_play (unpack "CCnC", $data);
170 }
171
172 =item $conn->query ($flags, $prompt)
173
174 =cut
175
176 sub query { }
177
178 sub feed_query {
179 my ($self, $data) = @_;
180
181 my ($flags, $prompt) = split /\s+/, $data, 2;
182
183 if ($flags == 0 && $prompt =~ /^What is your name\?\s+:$/ && length $self->{user}) {
184 $self->send ("reply $self->{user}");
185 } elsif ($flags == 4 && $prompt =~ /^What is your password\?\s+:$/ && length $self->{pass}) {
186 $self->send ("reply $self->{pass}");
187 } elsif ($flags == 4 && $prompt =~ /^Please type your password again\.\s+:$/ && length $self->{pass}) {
188 $self->send ("reply $self->{pass}");
189 } else {
190 $self->query ($flags, $prompt);
191 }
192 }
193
194 =item $conn->drawinfo ($color, $text)
195
196 =cut
197
198 sub drawinfo { }
199
200 sub feed_drawinfo {
201 my ($self, $data) = @_;
202
203 $self->drawinfo (split / /, $data, 2);
204 }
205
206 =item $conn->player_update ($player)
207
208 tag, weight, face, name
209
210 =cut
211
212 sub player_update { }
213
214 sub feed_player {
215 my ($self, $data) = @_;
216
217 my ($tag, $weight, $face, $name) = unpack "NNN C/a", $data;
218
219 $self->player_update ($self->{player} = {
220 tag => $tag,
221 weight => $weight,
222 face => $face,
223 name => $name,
224 });
225 }
226
227 =item $conn->stats_update ($stats)
228
229 =cut
230
231 sub stats_update { }
232
233 sub feed_stats {
234 my ($self, $data) = @_;
235
236 while (length $data) {
237 my $stat = unpack "C", substr $data, 0, 1, "";
238 my $value;
239
240 if ($stat == 26 || $stat == 29 || $stat == 30 || $stat == 31 || $stat == 11) {
241 $value = unpack "N", substr $data, 0, 4, "";
242 } elsif ($stat == 17 || $stat == 19) {
243 $value = (1 / 100000) * unpack "N", substr $data, 0, 4, "";
244 } elsif ($stat == 20 || $stat == 21) {
245 my $len = unpack "C", substr $data, 0, 1, "";
246 $value = substr $data, 0, $len, "";
247 } elsif ($stat == 28) {
248 my ($lo, $hi) = unpack "NN", substr $data, 0, 8, "";
249 $value = $hi * 2**32 + $lo;
250 } elsif ($stat >= 118 && $stat <= 129) {
251 my ($level, $lo, $hi) = unpack "CNN", substr $data, 0, 9, "";
252 $value = [$level, $hi * 2**32 + $lo];
253 } else {
254 $value = unpack "n", substr $data, 0, 2, "";
255 }
256
257 $self->{stat}{$stat} = $value;
258 }
259
260 $self->stats_update ($self->{stat});
261 }
262
263 =item $conn->inventory_clear ($id)
264
265 =cut
266
267 sub inventory_clear { }
268
269 sub feed_delinv {
270 my ($self, $data) = @_;
271
272 $self->inventory_clear ($data);
273
274 delete $self->{inventory}[$data];
275 }
276
277 =item $conn->items_delete ($tag...)
278
279 =cut
280
281 sub items_delete { }
282
283 sub feed_delitem {
284 my ($self, $data) = @_;
285
286 $self->items_delete (unpack "n*", $data);
287 }
288
289 =item $conn->inventory_add ($id, [\%item...])
290
291 =cut
292
293 sub inventory_add {
294 }
295
296 sub feed_item2 {
297 my ($self, $data) = @_;
298
299 my ($location, @values) = unpack "N (NNNN C/a* nC Nn)*", $data;
300
301 my @items;
302
303 while (@values) {
304 my ($tag, $flags, $weight, $face, $names, $anim, $animspeed, $nrof, $type) =
305 splice @values, 0, 9, ();
306
307 my ($name, $name_pl) = split /\x000/, $names;
308
309 push @items, {
310 tag => $tag,
311 flags => $flags,
312 weight => $weight,
313 face => $face,
314 name => $name,
315 name_pl => $name_pl,
316 anim => $anim,
317 animspeed => $animspeed * 0.120, #???
318 nrof => $nrof,
319 type => $type,
320 };
321 }
322
323 $self->inventory_add ($location, \@items);
324 }
325
326 =item $conn->item_update ($tag)
327
328 =cut
329
330 sub item_update { }
331
332 sub feed_upditem {
333 #todo
334 }
335
336 sub feed_map1a {
337 my ($self, $data) = @_;
338
339 my $map = $self->{map} ||= [];
340
341 my ($dx, $dy) = delete @$self{qw(delayed_scroll_x delayed_scroll_y)};
342
343 if ($dx || $dy) {
344 my ($mx, $my, $mw, $mh) = @$self{qw(mapx mapy mapw maph)};
345
346 # TODO: optimise this a lot, or maybe just do it on c-level
347 # set flag to -1 for spaces we scroll out
348 {
349 my @darkness;
350
351 if ($dx > 0) {
352 push @darkness, [$mx, $my, $dx - 1, $mh];
353 } elsif ($dx < 0) {
354 push @darkness, [$mx + $mw + $dx + 1, $my, 1 - $dx, $mh];
355 }
356
357 if ($dy > 0) {
358 push @darkness, [$mx, $my, $mw, $dy - 1];
359 } elsif ($dy < 0) {
360 push @darkness, [$mx, $my + $mh + $dy + 1, $mw, 1 - $dy];
361 }
362
363 for (@darkness) {
364 my ($x0, $y0, $w, $h) = @$_;
365 for my $x ($x0 .. $x0 + $w) {
366 for my $y ($y0 .. $y0 + $h) {
367
368 my $cell = $map->[$x][$y]
369 or next;
370
371 $cell->[0] = -1;
372 }
373 }
374 }
375 }
376
377 # now scroll
378
379 $self->{mapx} += $dx;
380 $self->{mapy} += $dy;
381
382 # shift in new space if moving to "negative indices"
383 if ($self->{mapy} < 0) {
384 unshift @$_, (undef) x -$self->{mapy} for @$map;
385 $self->{mapy} = 0;
386 }
387
388 if ($self->{mapx} < 0) {
389 unshift @$map, (undef) x -$self->{mapx};
390 $self->{mapx} = 0;
391 }
392
393 $self->map_scroll ($dx, $dy);
394 }
395
396 my @dirty;
397 my ($coord, $x, $y, $darkness, $fa, $fb, $fc, $cell);
398
399 while (length $data) {
400 $coord = unpack "n", substr $data, 0, 2, "";
401
402 $x = (($coord >> 10) & 63) + $self->{mapx};
403 $y = (($coord >> 4) & 63) + $self->{mapy};
404
405 $cell = $map->[$x][$y] ||= [];
406
407 if ($coord & 15) {
408 @$cell = () if $cell->[0] < 0;
409
410 $cell->[0] = $coord & 8
411 ? unpack "C", substr $data, 0, 1, ""
412 : 255;
413
414 $cell->[1] = unpack "n", substr $data, 0, 2, ""
415 if $coord & 4;
416 $cell->[2] = unpack "n", substr $data, 0, 2, ""
417 if $coord & 2;
418 $cell->[3] = unpack "n", substr $data, 0, 2, ""
419 if $coord & 1;
420 } else {
421 $cell->[0] = -1;
422 }
423
424 push @dirty, [$x, $y];
425 }
426
427 $self->map_update (\@dirty);
428 }
429
430 sub feed_map_scroll {
431 my ($self, $data) = @_;
432
433 my ($dx, $dy) = split / /, $data;
434
435 $self->{delayed_scroll_x} += $dx;
436 $self->{delayed_scroll_y} += $dy;
437 }
438
439 sub feed_newmap {
440 my ($self) = @_;
441
442 $self->{map} = [];
443 $self->{mapx} = 0;
444 $self->{mapy} = 0;
445
446 delete $self->{delayed_scroll_x};
447 delete $self->{delayed_scroll_y};
448
449 $self->map_clear;
450 }
451
452 sub feed_image {
453 my ($self, $data) = @_;
454
455 $self->send_queue;
456
457 my ($num, $len, $data) = unpack "NNa*", $data;
458
459 $self->{face}[$num]{image} = $data;
460 $self->face_update ($self->{face}[$num]);
461
462 my @dirty;
463
464 for my $x (0..$self->{mapw} - 1) {
465 for my $y (0..$self->{maph} - 1) {
466 push @dirty, [$x, $y]
467 if grep $_ == $num, @{$self->{map}[$x][$y] || []};
468 }
469 }
470
471 $self->map_update (\@dirty);
472 }
473
474 =item $conn->map_clear [OVERWRITE]
475
476 Called whenever the map is to be erased completely.
477
478 =cut
479
480 sub map_clear { }
481
482 =item $conn->map_update ([ [x,y], [x,y], ...]) [OVERWRITE]
483
484 Called with a list of x|y coordinate pairs (as arrayrefs) for cells that
485 have been updated and need refreshing.
486
487 =cut
488
489 sub map_update { }
490
491 =item $conn->map_scroll ($dx, $dy) [OVERWRITE]
492
493 Called whenever the map has been scrolled.
494
495 =cut
496
497 sub map_scroll { }
498
499 =item $conn->face_update ($face) [OVERWRITE]
500
501 Called with the face number of face structure whenever a face image has
502 changed.
503
504 =cut
505
506 sub face_update { }
507
508 =item $conn->face_find ($face) [OVERWRITE]
509
510 Find and return the png image for the given face, or the empty list if no
511 face could be found, in which case it will be requested from the server.
512
513 =cut
514
515 sub face_find { }
516
517 =item $conn->send ($data)
518
519 Send a single packet/line to the server.
520
521 =cut
522
523 sub send {
524 my ($self, $data) = @_;
525
526 $data = pack "na*", length $data, $data;
527
528 syswrite $self->{fh}, $data;
529 }
530
531 sub send_queue {
532 my ($self, $cmd) = @_;
533
534 if (defined $cmd) {
535 push @{ $self->{send_queue} }, $cmd;
536 } else {
537 --$self->{outstanding};
538 }
539
540 if ($self->{outstanding} < $self->{max_outstanding}) {
541 ++$self->{outstanding};
542 $self->send (shift @{ $self->{send_queue} });
543 }
544 }
545
546 sub send_setup {
547 my ($self) = @_;
548
549 my $setup = join " ", setup => %{$self->{setup_req}},
550 mapsize => "$self->{mapw}x$self->{maph}";
551
552 $self->send ($setup);
553 }
554
555 =back
556
557 =head1 AUTHOR
558
559 Marc Lehmann <schmorp@schmorp.de>
560 http://home.schmorp.de/
561
562 Robin Redeker <elmex@ta-sa.org>
563 http://www.ta-sa.org/
564
565 =cut
566
567 1