ViewVC Help
View File | Revision Log | Show Annotations | Download File
/cvs/deliantra/gde/GCE/MapEditor.pm
Revision: 1.47
Committed: Sun Dec 17 20:24:46 2006 UTC (17 years, 6 months ago) by elmex
Branch: MAIN
Changes since 1.46: +15 -8 lines
Log Message:
added picker group finding from object samples

File Contents

# Content
1 package GCE::MapEditor;
2
3 =head1 NAME
4
5 GCE::MapEditor - the map editing widget
6
7 =cut
8
9 use Gtk2;
10 use Gtk2::Gdk::Keysyms;
11 use Gtk2::SimpleMenu;
12
13 use Crossfire;
14 use Crossfire::Map;
15 use Crossfire::MapWidget;
16
17 use GCE::AttrEdit;
18 use GCE::Util;
19 use GCE::HashDialog;
20
21 use Glib::Object::Subclass
22 Gtk2::Window;
23
24 use Storable qw/dclone/;
25
26 use strict;
27
28 #################################################################
29 ###### WINDOW MANAGEMENT ########################################
30 #################################################################
31
32 sub save_layout {
33 my ($self) = @_;
34
35 $self->{attach_editor}->save_layout if $self->{attach_editor};
36 $self->{map_properties}->save_layout if $self->{map_properties};
37 $self->{meta_info_win}->save_layout if $self->{meta_info_win};
38 }
39
40 sub close_windows {
41 my ($self) = @_;
42
43 $self->{attach_editor}->destroy if $self->{attach_editor};
44 $self->{map_properties}->destroy if $self->{map_properties};
45 $self->{meta_info_win}->destroy if $self->{meta_info_win};
46 }
47
48 #################################################################
49 ###### MENU MANAGEMENT ##########################################
50 #################################################################
51
52 sub do_context_menu {
53 my ($self, $map, $event) = @_;
54
55 my ($x, $y) = $map->coord ($event->x, $event->y);
56
57 my $menu = Gtk2::Menu->new;
58 foreach my $cm (
59 [
60 Follow => sub {
61 $::MAINWIN->{edit_collection}{followexit}->edit ($map, $x, $y, $self)
62 },
63 ]
64 ) {
65 my $item = Gtk2::MenuItem->new ($cm->[0]);
66 $menu->append ($item);
67 $item->show;
68 $item->signal_connect (activate => $cm->[1]);
69 }
70
71 $menu->append (my $sep = new Gtk2::SeparatorMenuItem);
72 $sep->show;
73
74 for my $sr (reverse $self->get_stack_refs ($map, $x, $y)) {
75 my $item = Gtk2::MenuItem->new ($sr->longname);
76 $menu->append ($item);
77 $item->set_submenu (my $smenu = new Gtk2::Menu);
78
79 for my $act (
80 [ 'Add inventory' => sub { $_[0]->add_inv ($::MAINWIN->get_pick) } ],
81 [ 'Find in picker' => sub { $::MAINWIN->open_pick_window ({ selection => $sr->picker_folder }) } ],
82 ) {
83 my $sitem = Gtk2::MenuItem->new ($act->[0]);
84 $smenu->append ($sitem);
85 $sitem->signal_connect (activate => sub { $act->[1]->($sr) });
86 $sitem->show;
87 }
88
89 $item->show;
90 }
91
92 $menu->popup (undef, undef, undef, undef, $event->button, $event->time);
93 }
94
95 sub build_menu {
96 my ($self) = @_;
97
98 my $menu_tree = [
99 _File => {
100 item_type => '<Branch>',
101 children => [
102 "_Save" => {
103 callback => sub { $self->save_map },
104 accelerator => '<ctrl>S'
105 },
106 "Save As" => {
107 callback => sub { $self->save_map_as },
108 },
109 "Map _Properties" => {
110 callback => sub { $self->open_map_prop },
111 accelerator => "<ctrl>P"
112 },
113 "Map _Attachments" => {
114 callback => sub { $self->open_attach_edit },
115 accelerator => "<ctrl>A"
116 },
117 "Map Meta _Info" => {
118 callback => sub { $self->open_meta_info },
119 },
120 Upload => {
121 item_type => '<Branch>',
122 children => [
123 "Upload for testing" => {
124 callback => sub { $self->upload_map_test },
125 },
126 "Upload for inclusion" => {
127 callback => sub { $self->upload_map_incl },
128 },
129 ]
130 },
131 "_Map Resize" => {
132 callback => sub { $self->open_resize_map },
133 },
134 "Close" => {
135 callback => sub { $self->destroy },
136 },
137 ]
138 },
139 _Edit => {
140 item_type => '<Branch>',
141 children => [
142 "_Undo" => {
143 callback => sub { $self->undo },
144 accelerator => "<ctrl>Z"
145 },
146 "_Redo" => {
147 callback => sub { $self->redo },
148 accelerator => "<ctrl>Y"
149 },
150 ]
151 },
152 _Go => {
153 item_type => '<Branch>',
154 children => [
155 "_Up" => {
156 callback => sub { $self->follow ('u') },
157 accelerator => "<ctrl>Up"
158 },
159 "_Down" => {
160 callback => sub { $self->follow ('d') },
161 accelerator => "<ctrl>Down"
162 },
163 "_Right" => {
164 callback => sub { $self->follow ('r') },
165 accelerator => "<ctrl>Right"
166 },
167 "_Left" => {
168 callback => sub { $self->follow ('l') },
169 accelerator => "<ctrl>Left"
170 },
171 ]
172 },
173 _Help => {
174 item_type => '<Branch>',
175 children => [
176 _Manual => {
177 callback => sub { $::MAINWIN->show_help_window },
178 accelerator => "<ctrl>H"
179 },
180 ]
181 },
182 ];
183
184 my $men =
185 Gtk2::SimpleMenu->new (
186 menu_tree => $menu_tree,
187 default_callback => \&default_cb,
188 );
189
190 for (
191 [i => 'pick'],
192 [p => 'place'],
193 [e => 'erase'],
194 [s => 'select'],
195 [l => 'eval'],
196 [t => 'connect'],
197 [f => 'followexit']
198 )
199 {
200 my $tool = $_->[1];
201 $men->{accel_group}->connect ($Gtk2::Gdk::Keysyms{$_->[0]}, [], 'visible',
202 sub { $::MAINWIN->set_edit_tool ($tool) });
203 }
204
205 $men->{accel_group}->connect ($Gtk2::Gdk::Keysyms{'r'}, ['control-mask'], 'visible',
206 sub { $self->redo });
207
208 $self->add_accel_group ($men->{accel_group});
209
210 return $men->{widget};
211 }
212
213 #################################################################
214 ###### EDIT TOOL STUFF ##########################################
215 #################################################################
216
217 sub set_edit_tool {
218 my ($self, $tool) = @_;
219
220 $self->{etool} = $tool;
221
222 if ($self->ea->special_arrow) {
223 $self->{map}{window}->set_cursor (Gtk2::Gdk::Cursor->new ($self->ea->special_arrow));
224 } else {
225 # FIXME: Get the original cursor and insert it here
226 $self->{map}{window}->set_cursor (Gtk2::Gdk::Cursor->new ('GDK_LEFT_PTR'));
227 }
228 }
229
230 sub ea {
231 my ($self) = @_;
232 $self->{ea_alt} || $self->{etool};
233 }
234
235 sub start_drawmode {
236 my ($self, $map) = @_;
237
238 $self->{draw_mode} and return;
239
240 # XXX: is this okay? my ($x, $y) = $map->coord ($event->x, $event->y);
241 my ($x, $y) = $map->coord ($map->get_pointer);
242
243 my $ea = $self->ea;
244
245 $ea->begin ($map, $x, $y, $self);
246
247 $ea->edit ($map, $x, $y, $self)
248 if $x >= 0 and $y >= 0 and $x < $map->{map}{width} and $y < $map->{map}{height};
249
250 $self->{draw_mode} = [$x, $y];
251 }
252
253 sub stop_drawmode {
254 my ($self, $map) = @_;
255
256 $self->{draw_mode} or return;
257
258 my ($x, $y) = $map->coord ($map->get_pointer);
259
260 my $ea = $self->ea;
261 $ea->end ($map, $x, $y, $self);
262
263 delete $self->{draw_mode};
264 }
265
266 #################################################################
267 ###### UTILITY FUNCTIONS ########################################
268 #################################################################
269
270 sub follow {
271 my ($self, $dir) = @_;
272
273 my %dir_to_path = (
274 u => 'tile_path_1',
275 d => 'tile_path_3',
276 r => 'tile_path_2',
277 l => 'tile_path_4',
278 );
279
280 defined $dir_to_path{$dir}
281 or return;
282 my $map = $self->{map}{map}{info}{$dir_to_path{$dir}}
283 or return;
284
285 $map = map2abs ($map, $self);
286 $::MAINWIN->open_map_editor ($map);
287 }
288
289 # FIXME: Fix the automatic update of the attribute editor! and also the stack view!
290 sub undo {
291 my ($self) = @_;
292
293 my $map = $self->{map}; # the Crossfire::MapWidget
294
295 $map->{undo_stack_pos}
296 or return;
297
298 $map->change_swap ($map->{undo_stack}[--$map->{undo_stack_pos}]);
299 }
300
301 sub get_stack_refs {
302 my ($self, $map, $x, $y) = @_;
303
304 my $cstack = $map->get ($x, $y);
305
306 return [] unless @$cstack;
307
308 my @refs;
309
310 for my $arch (@$cstack) {
311 my ($ox, $oy) = ($x, $y);
312 if ($arch->{_virtual}) {
313 $ox = $arch->{virtual_x};
314 $oy = $arch->{virtual_y};
315 $arch = $arch->{_virtual};
316 $cstack = $map->get ($ox, $oy);
317 # XXX: This heavily blows up if $arch isn't on $cstack now.. and it actually really does :(
318 }
319
320 push @refs,
321 GCE::ArchRef->new (
322 arch => $arch,
323 cb => sub {
324 $map->change_begin ('attredit');
325 $map->change_stack ($ox, $oy, $cstack);
326
327 if (my $changeset = $map->change_end) {
328 splice @{ $map->{undo_stack} ||= [] },
329 $map->{undo_stack_pos}++, 1e6,
330 $changeset;
331 }
332 }
333 );
334 }
335
336 return @refs;
337 }
338
339 sub redo {
340 my ($self) = @_;
341
342 my $map = $self->{map}; # the Crossfire::MapWidget
343
344 $map->{undo_stack}
345 and $map->{undo_stack_pos} < @{$map->{undo_stack}}
346 or return;
347
348 $map->change_swap ($map->{undo_stack}[$map->{undo_stack_pos}++]);
349 }
350
351 sub load_meta_info {
352 my ($mapfile) = @_;
353 if (-e "$mapfile.meta") {
354 open my $metafh, "<", "$mapfile.meta"
355 or warn "Couldn't open meta file $mapfile.meta: $!";
356 my $metadata = do { local $/; <$metafh> };
357 return Crossfire::from_json ($metadata);
358 }
359 }
360
361 sub save_meta_info {
362 my ($mapfile, $metainfo) = @_;
363 open my $metafh, ">", "$mapfile.meta"
364 or warn "Couldn't write meta file $mapfile.meta: $!";
365 print $metafh Crossfire::to_json ($metainfo);
366 }
367
368 sub open_map {
369 my ($self, $path, $key) = @_;
370
371 $self->{mapkey} = $key;
372
373
374 if (ref $path) {
375 $self->{map}->set_map ($path);
376 delete $self->{meta_info};
377 $self->set_title ('<ram>');
378
379 } else {
380 $self->{path} = $path;
381 $self->{map}->set_map (my $m = new_from_file Crossfire::Map $path);
382 $self->{meta_info} = load_meta_info ($path);
383 $self->set_title ("gce - map editor - $self->{path}");
384 }
385 $self->close_windows;
386 }
387
388 sub save_map {
389 my ($self) = @_;
390
391 if ($self->{path}) {
392 $self->{map}{map}->write_file ($self->{path});
393 if ($self->{meta_info}) {
394 save_meta_info ($self->{path}, $self->{meta_info});
395 }
396 quick_msg ($self, "saved to $self->{path}");
397 $self->set_title ("gce - map editor - $self->{path}");
398 } else {
399 $self->save_map_as;
400 }
401 }
402
403 sub save_map_as {
404 my ($self) = @_;
405
406 my $fc = $::MAINWIN->new_filechooser ('gce - save map', 1, $self->{path});
407
408 if ('ok' eq $fc->run) {
409
410 $::MAINWIN->{fc_last_folder} = $fc->get_current_folder;
411 $::MAINWIN->{fc_last_folders}->{$self->{fc_last_folder}}++;
412
413 $self->{map}{map}->write_file ($self->{path} = $fc->get_filename);
414 if ($self->{meta_info}) {
415 save_meta_info ($self->{path}, $self->{meta_info});
416 }
417 quick_msg ($self, "saved to $self->{path}");
418 $self->set_title ("gce - map editor - $self->{path}");
419 }
420
421 $fc->destroy;
422 }
423
424 #################################################################
425 ###### DIALOGOUES ###############################################
426 #################################################################
427
428 sub open_resize_map {
429 my ($self) = @_;
430
431 return if $self->{meta_info_win};
432
433 my $w = $self->{meta_info_win} = GCE::HashDialogue->new ();
434
435 $w->init (
436 dialog_default_size => [500, 200, 220, 20],
437 layout_name => 'resize_win',
438 title => 'resize map',
439 ref_hash => $self->{map}{map}{info},
440 dialog => [
441 [width => 'Width' => 'string'],
442 [height => 'Height' => 'string'],
443 ],
444 save_cb => sub {
445 my ($info) = @_;
446 $self->{map}{map}->resize ($info->{width}, $info->{height});
447 $self->{map}->invalidate_all;
448 $w->destroy;
449 }
450 );
451
452 $w->signal_connect (destroy => sub { delete $self->{meta_info_win} });
453
454 $w->show_all;
455 }
456
457 sub open_attach_edit {
458 my ($self) = @_;
459
460 my $w = GCE::AttachEditor->new;
461 $w->set_attachment (
462 $self->{map}{map}{info}{attach},
463 sub {
464 if (@{$_[0]}) {
465 $self->{map}{map}{info}{attach} = $_[0]
466 } else {
467 delete $self->{map}{map}{info}{attach};
468 }
469 }
470 );
471 $self->{attach_editor} = $w;
472 $w->signal_connect (destroy => sub { delete $self->{attach_editor} });
473 $w->show_all;
474 }
475
476 sub upload_map_incl {
477 my ($self) = @_;
478
479 my $meta = dclone $self->{meta_info};
480
481 my $w = $self->{meta_info_win} = GCE::HashDialogue->new ();
482
483 $w->init (
484 dialog_default_size => [500, 300, 220, 20],
485 layout_name => 'map_upload_incl',
486 title => 'gce - map inclusion upload',
487 ref_hash => $meta,
488 text_entry => { key => 'changes', label => 'Changes (required for inclusion):' },
489 dialog => [
490 [gameserver => 'Game server' => 'label'],
491 [testserver => 'Test server' => 'label'],
492 [undef => x => 'sep' ],
493 [cf_login => 'Server login name' => 'string'],
494 [cf_password=> 'Password' => 'password'],
495 [path => 'Map path' => 'string'],
496 ],
497 save_cb => sub {
498 my ($meta) = @_;
499 warn "UPLOAD[".Crossfire::to_json ($meta)."]\n";
500 }
501 );
502
503 $w->signal_connect (destroy => sub { delete $self->{meta_info_win} });
504
505 $w->show_all;
506 }
507
508 sub upload_map_test {
509 my ($self) = @_;
510
511 my $meta = dclone $self->{meta_info};
512
513 my $w = $self->{meta_info_win} = GCE::HashDialogue->new ();
514
515 $w->init (
516 dialog_default_size => [500, 300, 220, 20],
517 layout_name => 'map_upload_test',
518 title => 'gce - map test upload',
519 ref_hash => $meta,
520 dialog => [
521 [gameserver => 'Game server' => 'string'],
522 [testserver => 'Test server' => 'string'],
523 [undef => x => 'sep' ],
524 [cf_login => 'Server login name' => 'string'],
525 [cf_password=> 'Password' => 'password'],
526 [path => 'Map path' => 'string'],
527 ],
528 save_cb => sub {
529 my ($meta) = @_;
530 warn "UPLOAD[".Crossfire::to_json ($meta)."]\n";
531 }
532 );
533
534 $w->signal_connect (destroy => sub { delete $self->{meta_info_win} });
535
536 $w->show_all;
537
538
539 }
540
541 sub open_meta_info {
542 my ($self) = @_;
543
544 return if $self->{meta_info_win};
545
546 my $w = $self->{meta_info_win} = GCE::HashDialogue->new ();
547
548 $w->init (
549 dialog_default_size => [500, 300, 220, 20],
550 layout_name => 'meta_info_win',
551 title => 'meta info',
552 ref_hash => $self->{meta_info},
553 dialog => [
554 [path => 'Map path' => 'string'],
555 [cf_login => 'Login name' => 'string'],
556 [revision => 'CVS Revision' => 'label'],
557 [cvs_root => 'CVS Root' => 'label'],
558 [lib_root => 'LIB Root' => 'label'],
559 [testserver => 'Test server' => 'label'],
560 [gameserver => 'Game server' => 'label'],
561 ],
562 );
563
564 $w->signal_connect (destroy => sub { delete $self->{meta_info_win} });
565
566 $w->show_all;
567 }
568
569 sub open_map_prop {
570 my ($self) = @_;
571
572 return if $self->{map_properties};
573
574 my $w = $self->{map_properties} = GCE::HashDialogue->new ();
575
576 $w->init (
577 dialog_default_size => [500, 500, 220, 20],
578 layout_name => 'map_prop_win',
579 title => 'map properties',
580 ref_hash => $self->{map}{map}{info},
581 dialog => [
582 [qw/name Name string/],
583 [qw/region Region string/],
584 [qw/enter_x Enter-x string/],
585 [qw/enter_y Enter-y string/],
586 [qw/reset_timeout Reset-timeout string/],
587 [qw/swap_time Swap-timeout string/],
588 [undef, qw/x sep/],
589 [qw/difficulty Difficulty string/],
590 [qw/windspeed Windspeed string/],
591 [qw/pressure Pressure string/],
592 [qw/humid Humid string/],
593 [qw/temp Temp string/],
594 [qw/darkness Darkness string/],
595 [qw/sky Sky string/],
596 [qw/winddir Winddir string/],
597 [undef, qw/x sep/],
598 [qw/width Width label/], # sub { $self->{map}{map}->resize ($_[0], $self->{map}{map}{height}) }],
599 [qw/height Height label/], # sub { $self->{map}{map}->resize ($self->{map}{map}{width}, $_[0]) }],
600 [undef, qw/x sep/],
601 # [qw/msg Text text/],
602 # [qw/maplore Maplore text/],
603 [qw/outdoor Outdoor check/],
604 [qw/unique Unique check/],
605 [qw/fixed_resettime Fixed-resettime check/],
606 [undef, qw/x sep/],
607 [qw/tile_path_1 Northpath string/],
608 [qw/tile_path_2 Eastpath string/],
609 [qw/tile_path_3 Southpath string/],
610 [qw/tile_path_4 Westpath string/],
611 [qw/tile_path_5 Toppath string/],
612 [qw/tile_path_6 Bottompath string/],
613 [undef, qw/x sep/],
614 [undef, 'For shop description look in the manual',
615 'button', sub { $::MAINWIN->show_help_window }],
616 [qw/shopmin Shopmin string/],
617 [qw/shopmax Shopmax string/],
618 [qw/shoprace Shoprace string/],
619 [qw/shopgreed Shopgreed string/],
620 [qw/shopitems Shopitems string/],
621 ]
622 );
623
624 $w->signal_connect (destroy => sub { delete $self->{map_properties} });
625 $w->show_all;
626 }
627
628 #################################################################
629 ###### MAP EDITOR INIT ##########################################
630 #################################################################
631
632 sub INIT_INSTANCE {
633 my ($self) = @_;
634
635 $self->set_title ('gce - map editor');
636 $self->add (my $vb = Gtk2::VBox->new);
637
638 $vb->pack_start (my $menu = $self->build_menu, 0, 1, 0);
639
640 $vb->pack_start (my $map = $self->{map} = Crossfire::MapWidget->new, 1, 1, 0);
641
642 $map->signal_connect_after (key_press_event => sub {
643 my ($map, $event) = @_;
644
645 my $kv = $event->keyval;
646
647 my $ret = 0;
648
649 my ($x, $y) = $map->coord ($map->get_pointer);
650 for ([Control_L => sub { $self->{ea_alt} = $::MAINWIN->{edit_collection}{erase} }],
651 [Alt_L => sub { $self->{ea_alt} = $::MAINWIN->{edit_collection}{pick} }],
652 [c => sub { $::MAINWIN->{edit_collection}{select}->copy }],
653 [v => sub { $::MAINWIN->{edit_collection}{select}->paste ($map, $x, $y) }],
654 [n => sub { $::MAINWIN->{edit_collection}{select}->invoke }],
655 )
656 {
657 my $ed = $_;
658
659 if ($kv == $Gtk2::Gdk::Keysyms{$ed->[0]}) {
660 my $was_in_draw = defined $self->{draw_mode};
661
662 $self->stop_drawmode ($map)
663 if $was_in_draw && grep { $ed->[0] eq $_ } qw/Control_L Alt_L/;
664
665 $ed->[1]->();
666 $ret = 1;
667
668 $self->start_drawmode ($map)
669 if $was_in_draw && grep { $ed->[0] eq $_ } qw/Control_L Alt_L/;
670 }
671 }
672
673 if ($self->ea->special_arrow) {
674 $map->{window}->set_cursor (Gtk2::Gdk::Cursor->new ($self->ea->special_arrow));
675 } else {
676 # FIXME: Get the original cursor and insert it here
677 $map->{window}->set_cursor (Gtk2::Gdk::Cursor->new ('GDK_LEFT_PTR'));
678 }
679
680 $ret
681 });
682
683 $map->signal_connect_after (key_release_event => sub {
684 my ($map, $event) = @_;
685
686 my $ret = 0;
687
688 if ($event->keyval == $Gtk2::Gdk::Keysyms{Control_L}
689 or $event->keyval == $Gtk2::Gdk::Keysyms{Alt_L})
690 {
691 my $was_in_draw = defined $self->{draw_mode};
692
693 $self->stop_drawmode ($map)
694 if $was_in_draw;
695
696 delete $self->{ea_alt};
697 $ret = 1;
698
699 $self->start_drawmode ($map)
700 if $was_in_draw;
701 }
702
703 if ($self->ea->special_arrow) {
704 $map->{window}->set_cursor (Gtk2::Gdk::Cursor->new ($self->ea->special_arrow));
705 } else {
706 # FIXME: Get the original cursor and insert it here
707 $map->{window}->set_cursor (Gtk2::Gdk::Cursor->new ('GDK_LEFT_PTR'));
708 }
709
710 $ret
711 });
712 $map->signal_connect_after (button_press_event => sub {
713 my ($map, $event) = @_;
714
715 if ((not $self->{draw_mode}) and $event->button == 1) {
716 my $ea = $self->ea;
717
718 $self->start_drawmode ($map);
719
720 $ea->want_cursor
721 or $map->disable_tooltip;
722
723 return 1;
724 } elsif ($event->button == 3) {
725 $self->do_context_menu ($map, $event);
726 return 1;
727 }
728
729 0
730 });
731
732 $map->signal_connect_after (motion_notify_event => sub {
733 my ($map, $event) = @_;
734
735 $self->{draw_mode}
736 or return;
737
738 my $ea = $self->ea;
739
740 my ($X, $Y) = @{$self->{draw_mode}}[0,1];
741 my ($x, $y) = $map->coord ($map->get_pointer);
742
743 while ($x != $X || $y != $Y) {
744
745 $X++ if $X < $x;
746 $X-- if $X > $x;
747 $Y++ if $Y < $y;
748 $Y-- if $Y > $y;
749
750 unless ($ea->only_on_click) {
751 $ea->edit ($map, $X, $Y, $self)
752 if $X >= 0 and $Y >= 0 and $X < $map->{map}{width} and $Y < $map->{map}{height};
753 }
754 }
755
756 @{$self->{draw_mode}}[0,1] = ($X, $Y);
757
758 1
759 });
760
761 $map->signal_connect_after (button_release_event => sub {
762 my ($map, $event) = @_;
763
764 if ($self->{draw_mode} and $event->button == 1) {
765 my $ea = $self->ea;
766
767 $self->stop_drawmode ($map);
768
769 $ea->want_cursor
770 or $map->enable_tooltip;
771
772 return 1;
773 }
774
775 0
776 });
777
778 ::set_pos_and_size ($self, $main::CFG->{map_window}, 500, 500, 200, 0);
779 }
780
781 =head1 AUTHOR
782
783 Marc Lehmann <schmorp@schmorp.de>
784 http://home.schmorp.de/
785
786 Robin Redeker <elmex@ta-sa.org>
787 http://www.ta-sa.org/
788
789 =cut
790
791 1
792