ViewVC Help
View File | Revision Log | Show Annotations | Download File
/cvs/deliantra/Deliantra-Client/bin/cfplus
Revision: 1.224
Committed: Thu Aug 30 07:57:59 2007 UTC (16 years, 8 months ago) by root
Branch: MAIN
Changes since 1.223: +10 -3 lines
Log Message:
oh my, cfplus always played the same song when given the choice of many

File Contents

# Content
1 #!/opt/bin/perl
2
3 my $startup_done = sub { };
4 our $PANGO = "1.5.0";
5
6 # do splash-screen thingy on win32
7 BEGIN {
8 if (%PAR::LibCache && $^O eq "MSWin32") {
9 while (my ($filename, $zip) = each %PAR::LibCache) {
10 $zip->extractMember ("SPLASH.bmp", "$ENV{PAR_TEMP}/SPLASH.bmp");
11 }
12
13 require Win32::GUI::SplashScreen;
14
15 Win32::GUI::SplashScreen::Show (
16 -file => "$ENV{PAR_TEMP}/SPLASH.bmp",
17 );
18
19 $startup_done = sub {
20 Win32::GUI::SplashScreen::Done (1);
21 };
22 }
23 }
24
25 use strict;
26 use utf8;
27
28 use Carp 'verbose';
29
30 # do things only needed for single-binary version (par)
31 BEGIN {
32 if (%PAR::LibCache) {
33 @INC = grep ref, @INC; # weed out all paths except pars loader refs
34
35 my $tmp = $ENV{PAR_TEMP};
36
37 while (my ($filename, $zip) = each %PAR::LibCache) {
38 for ($zip->memberNames) {
39 next unless /^root\/(.*)/;
40 $zip->extractMember ($_, "$tmp/$1")
41 unless -e "$tmp/$1";
42 }
43 }
44
45 if ($^O eq "MSWin32") {
46 # relocatable
47 } else {
48 # unix, need to patch pango rc file
49 open my $fh, "<:perlio", "$tmp/usr/lib/pango/$PANGO/module-files.d/libpango1.0-0.modules"
50 or die "$tmp/usr/lib/$PANGO/module-files.d/libpango1.0-0.modules: $!";
51 local $/;
52 my $rc = <$fh>;
53 $rc =~ s/^\//$tmp\//gm; # replace abs paths by relative ones
54
55 mkdir "$tmp/pango-modules";
56 open my $fh, ">:perlio", "$tmp/pango-modules/pango.modules"
57 or die "$tmp/pango-modules/pango.modules: $!";
58 print $fh $rc;
59
60 $ENV{PANGO_RC_FILE} = "$tmp/pango.rc";
61 open my $fh, ">:perlio", $ENV{PANGO_RC_FILE}
62 or die "$ENV{PANGO_RC_FILE}: $!";
63 print $fh "[Pango]\nModuleFiles = $tmp/pango-modules\n";
64 }
65
66 unshift @INC, $tmp;
67 }
68 }
69
70 # need to do it again because that pile of garbage called PAR nukes it before main
71 unshift @INC, $ENV{PAR_TEMP}
72 if %PAR::LibCache;
73
74 use Time::HiRes 'time';
75 use Event;
76 use List::Util qw(max min);
77
78 use Crossfire;
79 use Crossfire::Protocol::Constants;
80
81 use Compress::LZF;
82
83 use CFPlus;
84 use CFPlus::OpenGL ();
85 use CFPlus::Protocol;
86 use CFPlus::DB;
87 use CFPlus::UI;
88 use CFPlus::UI::Canvas;
89 use CFPlus::UI::Inventory;
90 use CFPlus::UI::SpellList;
91 use CFPlus::UI::MessageWindow;
92 use CFPlus::Pod;
93 use CFPlus::MapWidget;
94 use CFPlus::Macro;
95
96 $SIG{QUIT} = sub { Carp::cluck "QUIT" };
97 $SIG{PIPE} = 'IGNORE';
98
99 $Event::Eval = 1;
100 $Event::DIED = sub {
101 CFPlus::fatal Carp::longmess $_[1]
102 };
103
104 my $MAX_FPS = 60;
105 my $MIN_FPS = 5; # unused as of yet
106
107 our $META_SERVER = "http://metaserver.schmorp.de/current.json";
108
109 our $LAST_REFRESH;
110 our $NOW;
111
112 our $CFG;
113 our $CONN;
114 our $PROFILE; # current profile
115 our $FAST; # fast, low-quality mode, possibly useful for software-rendering
116
117 our $WANT_REFRESH;
118
119 our @SDL_MODES;
120 our $WIDTH;
121 our $HEIGHT;
122 our $FULLSCREEN;
123 our $FONTSIZE;
124
125 our $FONT_PROP;
126 our $FONT_FIXED;
127
128 our $MAP;
129 our $MAPMAP;
130 our $MAPWIDGET;
131 our $COMPLETER;
132 our $BUTTONBAR;
133 our $METASERVER;
134 our $LOGIN_BUTTON;
135 our $QUIT_DIALOG;
136 our $HOST_ENTRY;
137 our $FULLSCREEN_ENABLE;
138 our $PICKUP_ENABLE;
139 our $SERVER_INFO;
140
141 our $SETUP_DIALOG;
142 our $SETUP_NOTEBOOK;
143 our $SETUP_SERVER;
144 our $SETUP_KEYBOARD;
145
146 our $PL_NOTEBOOK;
147 our $PL_WINDOW;
148
149 our $MUSIC_PLAYING_WIDGET;
150 our $LICENSE_WIDGET;
151
152 our $PICKUP_PAGE;
153 our $INVENTORY_PAGE;
154 our $STATS_PAGE;
155 our $SKILL_PAGE;
156 our $SPELL_PAGE;
157 our $SPELL_LIST;
158
159 our $HELP_WINDOW;
160 our $MESSAGE_WINDOW;
161 our $FLOORBOX;
162 our $GAUGES;
163 our $STATWIDS;
164
165 our $SDL_ACTIVE;
166 our %SDL_CB;
167
168 our $ALT_ENTER_MESSAGE;
169 our $STATUSBOX;
170 our $DEBUG_STATUS;
171
172 our $INV;
173 our $INVR;
174 our $INVR_HB;
175
176 #############################################################################
177
178 sub status {
179 $STATUSBOX->add (CFPlus::asxml $_[0], pri => -10, group => "status", timeout => 10, fg => [1, 1, 0, 1]);
180 }
181
182 sub debug {
183 $DEBUG_STATUS->set_text ($_[0]);
184 }
185
186 sub message {
187 my ($para) = @_;
188 $MESSAGE_WINDOW->message ($para);
189 }
190
191 #############################################################################
192 #TODO: maybe move into own audio module...
193
194 our $SDL_MIXER;
195
196 our $MUSIC_DEFAULT = "in_a_heartbeat.ogg";
197 our $MUSIC_WANT; # arryref of ambient music we want to play
198 our @MUSIC_HAVE; # ambient music we have on disk
199 our $MUSIC_START;
200 our @MUSIC_JINGLE; # which jingles to play next
201 our $MUSIC_PLAYING_DATA;
202 our $MUSIC_PLAYING_META;
203 our $MUSIC_PLAYER;
204 our $MUSIC_RESUME = 30; # resume music when played less than these many seconds before
205
206 our %AUDIO_CHUNK; # audio "files"
207 our %AUDIO_PLAY; # which audio faces should be played
208
209 sub audio_channel_finished {
210 my ($channel) = @_;
211
212 # warn "channel $channel finished\n";#d#
213 }
214
215 sub audio_sound_push($) {
216 my ($face) = @_;
217
218 $CFG->{effects_enable}
219 or return;
220
221 $AUDIO_PLAY{$face}
222 or return;
223
224 if (my $chunk = $AUDIO_CHUNK{$face}) {
225 for (grep $_->[0] >= Event::time, @{(delete $AUDIO_PLAY{$face}) || []}) {
226 my (undef, $dx, $dy, $vol) = @$_;
227
228 my $channel = CFPlus::Channel::find;
229 $channel->volume ($vol * $CFG->{effects_volume} * 128 / 255);
230 $channel->set_position_r ($dx, $dy, 20);
231 $chunk->play ($channel);
232 }
233 } else {
234 # sound_meta not set means data is in flight either way
235 my $meta = $CONN->{face}[$face]
236 or return;
237
238 $meta->{data}
239 or return;
240
241 # if its a jingle, play it as ambient music
242 if ($meta->{data}{jingle}) {
243 if (delete $AUDIO_PLAY{$face}) { # take the jingle out of the sound queue
244 push @MUSIC_JINGLE, $meta; # push it oto the music/jingle queue
245 &audio_music_push ($face);
246 }
247 } else {
248 # fetch from database
249 CFPlus::DB::get res_data => $meta->{name}, sub {
250 my $rwops = new CFPlus::RW $_[0];
251 my $chunk = new CFPlus::MixChunk $rwops
252 or Carp::confess "sound face " . (JSON::XS::to_json $meta) . " unloadable: " . CFPlus::Mix_GetError;
253 $chunk->volume (($meta->{data}{volume} || 1) * 128);
254 $AUDIO_CHUNK{$face} = $chunk;
255
256 audio_sound_push ($face);
257 };
258 }
259 }
260 }
261
262 sub audio_sound_play {
263 my ($face, $dx, $dy, $vol) = @_;
264
265 $SDL_MIXER
266 or return;
267 $CFG->{effects_enable}
268 or return;
269
270 my $queue = $AUDIO_PLAY{$face} ||= [];
271 push @$queue, [Event::time + 0.6, $dx, $dy, $vol]; # do not play sound for outdated events
272 audio_sound_push $face
273 unless @$queue > 1;
274 }
275
276 sub audio_music_set_meta {
277 my ($meta) = @_;
278
279 $MUSIC_PLAYING_META = $meta;
280 $MUSIC_PLAYING_WIDGET->set_markup (
281 "<b>Name</b>: " . (CFPlus::asxml $meta->{data}{name}) . "\n"
282 . "<b>Author</b>: " . (CFPlus::asxml $meta->{data}{author}) . "\n"
283 . "<b>Source</b>: " . (CFPlus::asxml $meta->{data}{source}) . "\n"
284 . "<b>License</b>: " . (CFPlus::asxml $meta->{data}{license})
285 );
286 }
287
288 sub audio_music_update_volume {
289 return unless $MUSIC_PLAYING_META;
290 my $volume = $MUSIC_PLAYING_META->{data}{volume} || 1;
291 my $base = $MUSIC_PLAYING_META->{data}{jingle} ? 1 : $CFG->{bgm_volume};
292 CFPlus::MixMusic::volume $base * $volume * 128;
293 }
294
295 sub audio_music_start {
296 my $meta = $MUSIC_PLAYING_META;
297
298 CFPlus::DB::get res_data => $meta->{name}, sub {
299 return unless $SDL_MIXER;
300
301 # music might have changed...
302 $meta eq $MUSIC_PLAYING_META
303 or return &audio_music_start ();
304
305 audio_music_update_volume;
306
307 $MUSIC_PLAYING_DATA = \$_[0];
308
309 my $rwops = $meta->{path}
310 ? new_from_file CFPlus::RW $meta->{path}
311 : new CFPlus::RW $$MUSIC_PLAYING_DATA;
312
313 $MUSIC_PLAYER = new CFPlus::MixMusic $rwops
314 or Carp::confess "music face $meta->{face} unloadable: " . CFPlus::Mix_GetError;
315
316 my $NOW = time;
317
318 if ($MUSIC_PLAYING_META->{stop_time} > $NOW - $MUSIC_RESUME) {
319 my $pos = $MUSIC_PLAYING_META->{stop_pos};
320 $MUSIC_PLAYER->fade_in_pos (0, 700, $pos);
321 $MUSIC_START = time - $pos;
322 } else {
323 $MUSIC_PLAYER->play (0);
324 $MUSIC_START = time;
325 }
326
327 delete $meta->{stop_time};
328 delete $meta->{stop_pos};
329 }
330 }
331
332 sub audio_music_push {
333 return unless $SDL_MIXER;
334
335 my $fade_out;
336
337 if (@MUSIC_JINGLE) {
338 @MUSIC_HAVE = $MUSIC_JINGLE[0];
339 $fade_out = 333;
340 } else {
341 return unless $CFG->{bgm_enable};
342
343 my @have =
344 grep $_ && $_->{data},
345 map $CONN->{face}[$_],
346 @$MUSIC_WANT;
347
348 # randomize music a bit so that the order is not always the same
349 $_->{stop_time} ||= rand for @have;
350
351 @MUSIC_HAVE = @have
352 if @have;
353
354 # default MUSIC_HAVE == MUSIC_DEFAULT
355 @MUSIC_HAVE = { path => CFPlus::find_rcfile "music/$MUSIC_DEFAULT" } unless @MUSIC_HAVE;
356 $fade_out = 700;
357 }
358
359 # if the currently playing song is acceptable, let it continue
360 return if grep $MUSIC_PLAYING_META == $_, @MUSIC_HAVE;
361
362 my $NOW = time;
363
364 if ($MUSIC_PLAYING_META) {
365 $MUSIC_PLAYING_META->{stop_time} = $NOW;
366 $MUSIC_PLAYING_META->{stop_pos} = $NOW - $MUSIC_START;
367 CFPlus::MixMusic::fade_out $fade_out;
368 } else {
369 # sort by stop time, oldest first
370 @MUSIC_HAVE = sort { $a->{stop_time} <=> $b->{stop_time} } @MUSIC_HAVE;
371
372 # if the most recently-played piece played very recently,
373 # resume it, else choose the oldest piece for rotation.
374 audio_music_set_meta
375 $MUSIC_HAVE[-1]{stop_pos} && $MUSIC_HAVE[-1]{stop_time} > $NOW - $MUSIC_RESUME
376 ? $MUSIC_HAVE[-1]
377 : $MUSIC_HAVE[0];
378
379 audio_music_start;
380 }
381 }
382
383 sub audio_music_set_ambient {
384 my ($songs) = @_;
385
386 $MUSIC_WANT = $songs;
387 audio_music_push;
388 }
389
390 sub audio_music_finished {
391 if ($MUSIC_PLAYING_META) {
392 $MUSIC_PLAYING_META->{stop_time} = time;
393 }
394
395 # we compress multiple jingles of the same type
396 shift @MUSIC_JINGLE
397 while @MUSIC_JINGLE && $MUSIC_PLAYING_META == $MUSIC_JINGLE[0];
398
399 $MUSIC_PLAYING_WIDGET->clear;
400
401 undef $MUSIC_PLAYER;
402 undef $MUSIC_PLAYING_META;
403 undef $MUSIC_PLAYING_DATA;
404
405 audio_music_push;
406 }
407
408 sub audio_init {
409 if ($CFG->{audio_enable}) {
410 $ENV{MIX_EFFECTSMAXSPEED} = 1;
411 $SDL_MIXER = !CFPlus::Mix_OpenAudio;
412
413 unless ($SDL_MIXER) {
414 status "Unable to open sound device: there will be no sound";
415 return;
416 }
417
418 CFPlus::Mix_AllocateChannels 16;
419
420 audio_music_finished;
421 } else {
422 undef $SDL_MIXER;
423 }
424 }
425
426 sub audio_shutdown {
427 undef $MUSIC_PLAYER;
428 undef $MUSIC_PLAYING_META;
429 undef $MUSIC_PLAYING_DATA;
430
431 $MUSIC_WANT = [];
432 @MUSIC_JINGLE = ();
433 %AUDIO_PLAY = ();
434 %AUDIO_CHUNK = ();
435
436 CFPlus::Mix_CloseAudio if $SDL_MIXER;
437 undef $SDL_MIXER;
438 }
439
440 #############################################################################
441
442 sub destroy_query_dialog {
443 (delete $_[0]{query_dialog})->destroy
444 if $_[0]{query_dialog};
445 }
446
447 # FIXME: a very ugly hack to wait for stat update look below! #d#
448 our $QUERY_TIMER; #d#
449
450 # server query dialog
451 sub server_query {
452 my ($conn, $flags, $prompt) = @_;
453
454 # FIXME: a very ugly hack to wait for stat update #d#
455 if ($prompt =~ /roll new stats/ and not $conn->{stat_change_with}) {
456 unless ($QUERY_TIMER) {
457 $QUERY_TIMER =
458 Event->timer (
459 after => 1,
460 cb => sub {
461 server_query ($conn, $flags, $prompt, 1);
462 $QUERY_TIMER = undef
463 }
464 );
465 return;
466 }
467 }
468
469 $conn->{query_dialog} = my $dialog = new CFPlus::UI::Toplevel
470 x => "center",
471 y => "center",
472 title => "Server Query",
473 child => my $vbox = new CFPlus::UI::VBox,
474 ;
475
476 my @dialog = my $label = new CFPlus::UI::Label
477 max_w => $::WIDTH * 0.8,
478 ellipsise => 0,
479 text => $prompt;
480
481 if ($flags & CS_QUERY_YESNO) {
482 push @dialog, my $hbox = new CFPlus::UI::HBox;
483
484 $hbox->add (new CFPlus::UI::Button
485 text => "No",
486 on_activate => sub {
487 $conn->send ("reply n");
488 $dialog->destroy;
489 0
490 }
491 );
492 $hbox->add (new CFPlus::UI::Button
493 text => "Yes",
494 on_activate => sub {
495 $conn->send ("reply y");
496 destroy_query_dialog $conn;
497 0
498 },
499 );
500
501 $dialog->grab_focus;
502
503 } elsif ($flags & CS_QUERY_SINGLECHAR) {
504 if ($prompt =~ /Now choose a character|Press any key for the next race/i) {
505 $dialog->{tooltip} = "#charcreation_focus";
506
507 unshift @dialog, new CFPlus::UI::Label
508 max_w => $::WIDTH * 0.8,
509 ellipsise => 0,
510 markup => "\nOr use your keyboard and the text entry below:\n";
511
512 unshift @dialog, my $table = new CFPlus::UI::Table;
513
514 $table->add_at (0, 0, new CFPlus::UI::Button
515 text => "Next Race",
516 on_activate => sub {
517 $conn->send ("reply n");
518 destroy_query_dialog $conn;
519 0
520 },
521 );
522 $table->add_at (2, 0, new CFPlus::UI::Button
523 text => "Accept",
524 on_activate => sub {
525 $conn->send ("reply d");
526 destroy_query_dialog $conn;
527 0
528 },
529 );
530
531 if ($conn->{chargen_race_description}) {
532 unshift @dialog, new CFPlus::UI::Label
533 max_w => $::WIDTH * 0.8,
534 ellipsise => 0,
535 markup => "<span foreground='#ccccff'>$conn->{chargen_race_description}</span>",
536 ;
537 }
538
539 unshift @dialog, new CFPlus::UI::Face
540 face => $conn->{player}{face},
541 bg => [.2, .2, .2, 1],
542 min_w => 64,
543 min_h => 64,
544 ;
545
546 if ($conn->{chargen_race_title}) {
547 unshift @dialog, new CFPlus::UI::Label
548 allign => 1,
549 ellipsise => 0,
550 markup => "<span foreground='#ccccff' size='large'>Race: $conn->{chargen_race_title}</span>",
551 ;
552 }
553
554 unshift @dialog, new CFPlus::UI::Label
555 max_w => $::WIDTH * 0.4,
556 ellipsise => 0,
557 markup => (CFPlus::Pod::section_label ui => "chargen_race"),
558 ;
559
560 } elsif ($prompt =~ /roll new stats/) {
561 if (my $stat = delete $conn->{stat_change_with}) {
562 $conn->send ("reply $stat");
563 destroy_query_dialog $conn;
564 return;
565 }
566
567 unshift @dialog, new CFPlus::UI::Label
568 max_w => $::WIDTH * 0.4,
569 ellipsise => 0,
570 markup => "\nOr use your keyboard and the text entry below:\n";
571
572 unshift @dialog, my $table = new CFPlus::UI::Table;
573
574 # left: re-roll
575 $table->add_at (0, 0, new CFPlus::UI::Button
576 text => "Roll Again",
577 on_activate => sub {
578 $conn->send ("reply y");
579 destroy_query_dialog $conn;
580 0
581 },
582 );
583
584 # center: swap stats
585 my ($sw1, $sw2) = map +(new CFPlus::UI::Selector
586 expand => 1,
587 value => $_,
588 options => [
589 [1 => "Str", "Strength ($conn->{stat}{+CS_STAT_STR})"],
590 [2 => "Dex", "Dexterity ($conn->{stat}{+CS_STAT_DEX})"],
591 [3 => "Con", "Constitution ($conn->{stat}{+CS_STAT_CON})"],
592 [4 => "Int", "Intelligence ($conn->{stat}{+CS_STAT_INT})"],
593 [5 => "Wis", "Wisdom ($conn->{stat}{+CS_STAT_WIS})"],
594 [6 => "Pow", "Power ($conn->{stat}{+CS_STAT_POW})"],
595 [7 => "Cha", "Charisma ($conn->{stat}{+CS_STAT_CHA})"],
596 ],
597 ), 1 .. 2;
598
599 $table->add_at (2, 0, new CFPlus::UI::Button
600 text => "Swap Stats",
601 on_activate => sub {
602 $conn->{stat_change_with} = $sw2->{value};
603 $conn->send ("reply $sw1->{value}");
604 destroy_query_dialog $conn;
605 0
606 },
607 );
608 $table->add_at (2, 1, new CFPlus::UI::HBox children => [$sw1, $sw2]);
609
610 # right: accept
611 $table->add_at (4, 0, new CFPlus::UI::Button
612 text => "Accept",
613 on_activate => sub {
614 $conn->send ("reply n");
615 $STATS_PAGE->hide;
616 destroy_query_dialog $conn;
617 0
618 },
619 );
620
621 unshift @dialog, my $hbox = new CFPlus::UI::HBox;
622 for (
623 [Str => CS_STAT_STR],
624 [Dex => CS_STAT_DEX],
625 [Con => CS_STAT_CON],
626 [Int => CS_STAT_INT],
627 [Wis => CS_STAT_WIS],
628 [Pow => CS_STAT_POW],
629 [Cha => CS_STAT_CHA],
630 ) {
631 my ($name, $id) = @$_;
632 $hbox->add (new CFPlus::UI::Label
633 markup => "$conn->{stat}{$id} <span foreground='yellow'>$name</span>",
634 align => 0,
635 expand => 1,
636 can_events => 1,
637 can_hover => 1,
638 tooltip => "#stat_$name",
639 );
640 }
641
642 unshift @dialog, new CFPlus::UI::Label
643 max_w => $::WIDTH * 0.4,
644 ellipsise => 0,
645 markup => (CFPlus::Pod::section_label ui => "chargen_stats"),
646 ;
647 }
648
649 push @dialog, my $entry = new CFPlus::UI::Entry
650 on_changed => sub {
651 $conn->send ("reply $_[1]");
652 destroy_query_dialog $conn;
653 0
654 },
655 ;
656
657 $entry->grab_focus;
658
659 } else {
660 $dialog->{tooltip} = "Enter the reply and press return (click on the entry to make sure it has keyboard focus)";
661
662 push @dialog, my $entry = new CFPlus::UI::Entry
663 $flags & CS_QUERY_HIDEINPUT ? (hidden => "*") : (),
664 on_activate => sub {
665 $conn->send ("reply $_[1]");
666 destroy_query_dialog $conn;
667 0
668 },
669 ;
670
671 $entry->grab_focus;
672 }
673
674 $vbox->add (@dialog);
675 $dialog->show;
676 }
677
678 sub start_game {
679 status "logging in...";
680
681 $LOGIN_BUTTON->set_text ("Logout");
682 $SETUP_DIALOG->hide;
683
684 my $mapsize = List::Util::min 32, List::Util::max 11, int $WIDTH * $CFG->{mapsize} * 0.01 / 32;
685
686 my ($host, $port) = split /:/, $PROFILE->{host};
687
688 $MAP = new CFPlus::Map;
689
690 $CONN = eval {
691 new CFPlus::Protocol
692 host => $host,
693 port => $port || 13327,
694 user => $PROFILE->{user},
695 pass => $PROFILE->{password},
696 mapw => $mapsize,
697 maph => $mapsize,
698
699 client => "cfplus $CFPlus::VERSION $] $^O",
700
701 map_widget => $MAPWIDGET,
702 statusbox => $STATUSBOX,
703 map => $MAP,
704 mapmap => $MAPMAP,
705 query => \&server_query,
706
707 setup_req => {
708 smoothing => $CFG->{map_smoothing}*1,
709 },
710 };
711
712 if ($CONN) {
713 CFPlus::lowdelay fileno $CONN->{fh};
714
715 status "login successful";
716 } else {
717 status "unable to connect";
718 stop_game();
719 }
720 }
721
722 sub stop_game {
723 $LOGIN_BUTTON->set_text ("Login");
724 $SETUP_NOTEBOOK->set_current_page ($SETUP_SERVER);
725 $SETUP_DIALOG->show;
726 $PL_WINDOW->hide;
727 $SPELL_LIST->clear_spells;
728 $CFPlus::UI::ROOT->emit (stop_game => ! ! $CONN);
729
730 &audio_music_set_ambient ([]);
731
732 return unless $CONN;
733
734 status "connection closed";
735
736 destroy_query_dialog $CONN;
737 $CONN->destroy;
738 $CONN = 0; # false, does not autovivify
739
740 undef $MAP;
741 }
742
743 sub graphics_setup {
744 my $vbox = new CFPlus::UI::VBox;
745
746 $vbox->add (my $table = new CFPlus::UI::Table expand => 1, col_expand => [0, 1]);
747
748 my $row = 0;
749
750 $table->add_at (0, $row, new CFPlus::UI::Label valign => 0, align => 1, text => "OpenGL Info");
751 $table->add_at (1, $row++, new CFPlus::UI::Label valign => 0, fontsize => 0.8, text => CFPlus::OpenGL::gl_vendor . ", " . CFPlus::OpenGL::gl_version,
752 can_events => 1,
753 tooltip => "<tt><span size='8192'>" . (CFPlus::OpenGL::gl_extensions) . "</span></tt>");
754
755 my $vidmode_tooltip =
756 "<b>Video Mode.</b> The video mode to use for fullscreen (and the window size for windowed operation). "
757 . "The format is <i>width</i> x <i>height</i> \@ <i>depth-per-channel</i> + <i>alpha-channel</i>.";
758
759 $table->add_at (0, $row, new CFPlus::UI::Label valign => 0, align => 1, text => "Video Mode");
760 $table->add_at (1, $row++, my $hbox = new CFPlus::UI::HBox);
761
762 $hbox->add (my $mode_slider = new CFPlus::UI::Slider
763 force_w => $WIDTH * 0.1, expand => 1, range => [$CFG->{sdl_mode}, 0, $#SDL_MODES, 0, 1],
764 tooltip => $vidmode_tooltip);
765 $hbox->add (my $mode_label = new CFPlus::UI::Label
766 align => 0, valign => 0, height => 0.8, template => "9999x9999@9+9",
767 can_events => 1, tooltip => $vidmode_tooltip);
768
769 $mode_slider->connect (changed => sub {
770 my ($self, $value) = @_;
771
772 $CFG->{sdl_mode} = $self->{range}[0] = $value = int $value;
773 $mode_label->set_text (sprintf '%dx%d@%d+%d', @{$SDL_MODES[$value]});
774 });
775 $mode_slider->emit (changed => $mode_slider->{range}[0]);
776
777 $table->add_at (0, $row, new CFPlus::UI::Label valign => 0, align => 1, text => "Fullscreen");
778 $table->add_at (1, $row++, $FULLSCREEN_ENABLE = new CFPlus::UI::CheckBox
779 state => $CFG->{fullscreen},
780 tooltip => "Bring the client into fullscreen mode.",
781 on_changed => sub { my ($self, $value) = @_; $CFG->{fullscreen} = $value; 0 }
782 );
783
784 $table->add_at (0, $row, new CFPlus::UI::Label valign => 0, align => 1, text => "Force OpenGL 1.1");
785 $table->add_at (1, $row++, new CFPlus::UI::CheckBox
786 state => $CFG->{force_opengl11},
787 tooltip => "Limit CFPlus to use OpenGL 1.1 features only. This will normally result in "
788 . "higher memory usage and slower performance. It will, however, help tremendously on "
789 . "cards that claim to support a feature but fall back to software rendering. "
790 . "Nvidia Geforce FX cards are known to claim features the hardware doesn't support, "
791 . "but cards and drivers from other vendors (ATI) are often just as bad. <b>If you "
792 . "experience extremely low framerates and your card should do better, try this option.</b>",
793 on_changed => sub { my ($self, $value) = @_; $CFG->{force_opengl11} = $value; 0 }
794 );
795
796 $table->add_at (0, $row, new CFPlus::UI::Label valign => 0, align => 1, text => "Compress Textures");
797 $table->add_at (1, $row++, new CFPlus::UI::CheckBox
798 state => $CFG->{texture_compression},
799 tooltip => "Use texture compression. Normally this will not reduce visual quality noticable but "
800 . "will save a lot of memory and increase performance. The compression algorithm "
801 . "can differ form card to card, so your mileage may vary. This setting is ignored in "
802 . "forced OpenGL 1.1 mode.",
803 on_changed => sub { my ($self, $value) = @_; $CFG->{texture_compression} = $value; 0 }
804 );
805
806 $table->add_at (0, $row, new CFPlus::UI::Label valign => 0, align => 1, text => "Fast & Ugly");
807 $table->add_at (1, $row++, new CFPlus::UI::CheckBox
808 state => $CFG->{fast},
809 tooltip => "Lower the visual quality considerably to speed up rendering.",
810 on_changed => sub { my ($self, $value) = @_; $CFG->{fast} = $value; 0 }
811 );
812
813 $table->add_at (0, $row, new CFPlus::UI::Label valign => 0, align => 1, text => "GUI Fontsize");
814 $table->add_at (1, $row++, new CFPlus::UI::Slider
815 range => [$CFG->{gui_fontsize}, 0.5, 2, 0, 0.1],
816 tooltip => "The base font size used by most GUI elements that do not have their own setting.",
817 on_changed => sub { $CFG->{gui_fontsize} = $_[1]; 0 },
818 );
819
820 $table->add_at (1, $row++, new CFPlus::UI::Button
821 expand => 1, align => 0, text => "Apply",
822 tooltip => "Apply the video settings above.",
823 on_activate => sub {
824 video_shutdown ();
825 video_init ();
826 0
827 }
828 );
829
830 $table->add_at (0, $row, new CFPlus::UI::Label valign => 0, align => 1, text => "Map Scale");
831 $table->add_at (1, $row++, new CFPlus::UI::Slider
832 range => [(log $CFG->{map_scale}) / (log 2), -3, 1, 0, 1],
833 tooltip => "Enlarge or shrink the displayed map. Changes are instant.",
834 on_changed => sub { my ($self, $value) = @_; $CFG->{map_scale} = 2 ** $value; 0 }
835 );
836
837 $table->add_at (0, $row, new CFPlus::UI::Label valign => 0, align => 1, text => "Map Smoothing");
838 $table->add_at (1, $row++, new CFPlus::UI::CheckBox
839 state => $CFG->{map_smoothing},
840 tooltip => "<b>Map Smoothing</b> tries to make tile borders less square. "
841 . "This increases load on the graphics subsystem and works only with TRT servers. "
842 . "Changes take effect at next login only.",
843 on_changed => sub { my ($self, $value) = @_; $CFG->{map_smoothing} = $value; 0 }
844 );
845
846 $table->add_at (0, $row, new CFPlus::UI::Label valign => 0, align => 1, text => "Fog of War");
847 $table->add_at (1, $row++, new CFPlus::UI::CheckBox
848 state => $CFG->{fow_enable},
849 tooltip => "<b>Fog-of-War</b> marks areas that cannot be seen by the player. Changes are instant.",
850 on_changed => sub { my ($self, $value) = @_; $CFG->{fow_enable} = $value; 0 }
851 );
852
853 $table->add_at (0, $row, new CFPlus::UI::Label valign => 0, align => 1, text => "FoW Intensity");
854 $table->add_at (1, $row++, new CFPlus::UI::Slider
855 range => [$CFG->{fow_intensity}, 0, 1, 0, 1 / 256],
856 tooltip => "<b>Fog of War Lightness.</b> The higher the intensity, the lighter the Fog-of-War color. Changes are instant.",
857 on_changed => sub { my ($self, $value) = @_; $CFG->{fow_intensity} = $value; 0 }
858 );
859
860 $table->add_at (0, $row, new CFPlus::UI::Label valign => 0, align => 1, text => "Message Fontsize");
861 $table->add_at (1, $row++, new CFPlus::UI::Slider
862 range => [$CFG->{log_fontsize}, 0.5, 2, 0, 0.1],
863 tooltip => "The font size used by the <b>message/server log</b> window only. Changes are instant, "
864 . "but you still need to press apply to correctly re-layout the widget.",
865 on_changed => sub { $MESSAGE_WINDOW->set_fontsize ($CFG->{log_fontsize} = $_[1]); 0 },
866 );
867
868 $table->add_at (0, $row, new CFPlus::UI::Label valign => 0, align => 1, text => "Gauge fontsize");
869 $table->add_at (1, $row++, new CFPlus::UI::Slider
870 range => [$CFG->{gauge_fontsize}, 0.5, 2, 0, 0.1],
871 tooltip => "Adjusts the fontsize of the gauges at the bottom right. Changes are instant.",
872 on_changed => sub {
873 $CFG->{gauge_fontsize} = $_[1];
874 &set_gauge_window_fontsize;
875 0
876 }
877 );
878
879 $table->add_at (0, $row, new CFPlus::UI::Label valign => 0, align => 1, text => "Gauge size");
880 $table->add_at (1, $row++, new CFPlus::UI::Slider
881 range => [$CFG->{gauge_size}, 0.2, 0.8],
882 tooltip => "Adjust the size of the stats gauges at the bottom right. Changes are instant.",
883 on_changed => sub {
884 $CFG->{gauge_size} = $_[1];
885 $GAUGES->{win}->set_size ($WIDTH, int $HEIGHT * $CFG->{gauge_size});
886 0
887 }
888 );
889
890 $vbox
891 }
892
893 sub audio_setup {
894 my $vbox = new CFPlus::UI::VBox;
895
896 $vbox->add (my $table = new CFPlus::UI::Table expand => 1, col_expand => [0, 0, 1]);
897
898 my $row = 0;
899
900 $table->add_at (0, $row, new CFPlus::UI::Label valign => 0, align => 1, text => "Audio Enable");
901 $table->add_at (1, $row++, new CFPlus::UI::CheckBox
902 state => $CFG->{audio_enable},
903 tooltip => "<b>Master Audio Enable.</b> If enabled, sound effects and music will be played. If disabled, no audio will be used and the soundcard will not be opened.",
904 on_changed => sub { $CFG->{audio_enable} = $_[1]; 0 }
905 );
906 # $table->add_at (0, 9, new CFPlus::UI::Label valign => 0, align => 1, text => "Effects Volume");
907 # $table->add_at (1, 8, new CFPlus::UI::Slider range => [$CFG->{effects_volume}, 0, 128, 1], on_changed => sub {
908 # $CFG->{effects_volume} = $_[1];
909 # });
910
911 $table->add_at (0, $row, new CFPlus::UI::Label valign => 0, align => 1, text => "Sound Effects");
912 $table->add_at (1, $row, new CFPlus::UI::CheckBox
913 expand => 1, state => $CFG->{effects_enable},
914 tooltip => "If enabled, sound effects are enabled. If disabled, no sound effects will be played.",
915 on_changed => sub {
916 $CFG->{effects_enable} = $_[1];
917 $CONN->update_fx_want if $CONN;
918 0
919 }
920 );
921 $table->add_at (2, $row++, new CFPlus::UI::Slider
922 expand => 1, range => [$CFG->{effects_volume}, 0, 1, 0, 1/128],
923 tooltip => "The relative volume of sound effects. Best audio quality is achieved if this "
924 . "is set highest (rightmost) and you use your operating system volume setting. Changes are instant.",
925 on_changed => sub { $CFG->{effects_volume} = $_[1]; 0 }
926 );
927
928 $table->add_at (0, $row, new CFPlus::UI::Label valign => 0, align => 1, text => "Background Music");
929 $table->add_at (1, $row, new CFPlus::UI::CheckBox
930 expand => 1, state => $CFG->{bgm_enable},
931 tooltip => "If enabled, playing of background music is enabled. If disabled, no background music will be played.",
932 on_changed => sub {
933 $CFG->{bgm_enable} = $_[1];
934 $CONN->update_fx_want if $CONN;
935 audio_music_push;
936 0
937 }
938 );
939 $table->add_at (2, $row++, new CFPlus::UI::Slider
940 expand => 1, range => [$CFG->{bgm_volume}, 0, 1, 0, 1/128],
941 tooltip => "The volume of the background music. Changes are instant.",
942 on_changed => sub { $CFG->{bgm_volume} = $_[1]; audio_music_update_volume; 0 }
943 );
944
945 $table->add_at (1, $row++, new CFPlus::UI::Button
946 c_colspan => 2, expand => 1, align => 0, text => "Apply",
947 tooltip => "Apply the audio settings",
948 on_activate => sub {
949 audio_shutdown ();
950 audio_init ();
951 0
952 }
953 );
954
955 $vbox
956 }
957
958 sub set_gauge_window_fontsize {
959 for (map { $GAUGES->{$_} } grep { $_ ne 'win' } keys %{$GAUGES}) {
960 $_->set_fontsize ($::CFG->{gauge_fontsize});
961 }
962 }
963
964 sub make_gauge_window {
965 my $gh = int $HEIGHT * $CFG->{gauge_size};
966
967 my $win = new CFPlus::UI::Frame (
968 force_x => 0,
969 force_y => "max",
970 force_w => $WIDTH,
971 force_h => $gh,
972 );
973
974 $win->add (my $hbox = new CFPlus::UI::HBox
975 children => [
976 (new CFPlus::UI::HBox expand => 1),
977 (new CFPlus::UI::VBox children => [
978 (new CFPlus::UI::Empty expand => 1),
979 (new CFPlus::UI::Frame bg => [0, 0, 0, 0.4], child => ($FLOORBOX = new CFPlus::UI::Table)),
980 ]),
981 (my $vbox = new CFPlus::UI::VBox),
982 ],
983 );
984
985 $vbox->add (new CFPlus::UI::HBox
986 expand => 1,
987 children => [
988 (new CFPlus::UI::Empty expand => 1),
989 (my $hb = new CFPlus::UI::HBox),
990 ],
991 );
992
993 $hb->add (my $hg = new CFPlus::UI::Gauge type => 'hp', tooltip => "#stat_health");
994 $hb->add (my $mg = new CFPlus::UI::Gauge type => 'mana', tooltip => "#stat_mana");
995 $hb->add (my $gg = new CFPlus::UI::Gauge type => 'grace', tooltip => "#stat_grace");
996 $hb->add (my $fg = new CFPlus::UI::Gauge type => 'food', tooltip => "#stat_food");
997
998 $vbox->add (my $exp = new CFPlus::UI::Label valign => 0, align => 1, can_hover => 1, can_events => 1, tooltip => "#stat_exp");
999 $vbox->add (my $prg = new CFPlus::UI::ExperienceProgress);
1000 $vbox->add (my $sklprg = new CFPlus::UI::ExperienceProgress);
1001 $vbox->add (my $rng = new CFPlus::UI::Label valign => 0, align => 1, can_hover => 1, can_events => 1, tooltip => "#stat_ranged");
1002
1003 $GAUGES = {
1004 exp => $exp, prg => $prg, sklprg => $sklprg,
1005 win => $win, range => $rng,
1006 hp => $hg, mana => $mg, grace => $gg, food => $fg,
1007 };
1008
1009 &set_gauge_window_fontsize;
1010
1011 $win
1012 }
1013
1014 sub debug_setup {
1015 my $table = new CFPlus::UI::Table;
1016
1017 $table->add_at (0, 0, new CFPlus::UI::Label text => "Widget Borders");
1018 $table->add_at (1, 0, new CFPlus::UI::CheckBox on_changed => sub { $ENV{CFPLUS_DEBUG} ^= 1; 0 });
1019 $table->add_at (0, 1, new CFPlus::UI::Label text => "Tooltip Widget Info");
1020 $table->add_at (1, 1, new CFPlus::UI::CheckBox on_changed => sub { $ENV{CFPLUS_DEBUG} ^= 2; 0 });
1021 $table->add_at (0, 2, new CFPlus::UI::Label text => "Show FPS");
1022 $table->add_at (1, 2, new CFPlus::UI::CheckBox on_changed => sub { $ENV{CFPLUS_DEBUG} ^= 4; 0 });
1023 $table->add_at (0, 3, new CFPlus::UI::Label text => "Suppress Tooltips");
1024 $table->add_at (1, 3, new CFPlus::UI::CheckBox on_changed => sub { $ENV{CFPLUS_DEBUG} ^= 8; 0 });
1025 $table->add_at (0, 4, new CFPlus::UI::Button text => "die on click(tm)", on_activate => sub { &CFPlus::debug() } );
1026
1027 $table->add_at (0, 5, new CFPlus::UI::TextEdit text => "line1\0152\0153");#d#
1028
1029 $table->add_at (7,7, my $t = new CFPlus::UI::Table expand => 0);
1030 $t->add_at (0,0, new CFPlus::UI::Label text => "a a a a", c_rowspan => 1, c_colspan => 2);
1031 $t->add_at (2,0, new CFPlus::UI::Label text => "b\nb", c_rowspan => 2, c_colspan => 1);
1032 $t->add_at (1,2, new CFPlus::UI::Label text => "c c c c", c_rowspan => 1, c_colspan => 2);
1033 $t->add_at (0,1, new CFPlus::UI::Label text => "d\nd", c_rowspan => 2, c_colspan => 1);
1034 $t->add_at (1,1, new CFPlus::UI::Label text => "e");
1035
1036 $table->add_at (7, 6, my $c = new CFPlus::UI::Canvas);
1037
1038 $c->add_items ({
1039 type => "line_loop",
1040 color => [0, 1, 0],
1041 width => 9,
1042 coord_mode => "abs",
1043 coord => [[10, 5], [5, 50], [20, 5], [5, 60]],
1044 });
1045
1046 $c->add_items ({
1047 type => "lines",
1048 color => [1, 1, 0],
1049 width => 2,
1050 coord_mode => "rel",
1051 coord => [[0,0], [1,1], [1,0], [0,1]],
1052 });
1053
1054 $c->add_items ({
1055 type => "polygon",
1056 color => [0, 0.43, 0],
1057 width => 2,
1058 coord_mode => "rel",
1059 coord => [[0,0.2], [1,.4], [1,.6], [0,.8]],
1060 });
1061
1062 $table
1063 }
1064
1065 sub stats_window {
1066 my $r = new CFPlus::UI::ScrolledWindow (
1067 expand => 1,
1068 scroll_y => 1
1069 );
1070 $r->add (my $vb = new CFPlus::UI::VBox);
1071
1072 $vb->add (new CFPlus::UI::FancyFrame
1073 label => "Player",
1074 child => (my $pi = new CFPlus::UI::VBox),
1075 );
1076
1077 $pi->add ($STATWIDS->{title} = new CFPlus::UI::Label valign => 0, align => -1, text => "Title:", expand => 1,
1078 can_hover => 1, can_events => 1,
1079 tooltip => "Your name and title. You can change your title by using the <b>title</b> command, if supported by the server.");
1080 $pi->add ($STATWIDS->{map} = new CFPlus::UI::Label valign => 0, align => -1, text => "Map:", expand => 1,
1081 can_hover => 1, can_events => 1,
1082 tooltip => "The map you are currently on (if supported by the server).");
1083
1084 $pi->add (my $hb0 = new CFPlus::UI::HBox);
1085 $hb0->add ($STATWIDS->{weight} = new CFPlus::UI::Label valign => 0, align => -1, text => "Weight:", expand => 1,
1086 can_hover => 1, can_events => 1,
1087 tooltip => "The weight of the player including all inventory items.");
1088 $hb0->add ($STATWIDS->{m_weight} = new CFPlus::UI::Label valign => 0, align => -1, text => "Max weight:", expand => 1,
1089 can_hover => 1, can_events => 1,
1090 tooltip => "The weight limit: you cannot carry more than this.");
1091
1092 $vb->add (new CFPlus::UI::FancyFrame
1093 label => "Primary/Secondary Statistics",
1094 child => (my $hb = new CFPlus::UI::HBox expand => 1),
1095 );
1096 $hb->add (my $tbl = new CFPlus::UI::Table expand => 1);
1097
1098 my $color2 = [1, 1, 0];
1099
1100 for (
1101 [0, 0, st_str => "Str", 30],
1102 [0, 1, st_dex => "Dex", 30],
1103 [0, 2, st_con => "Con", 30],
1104 [0, 3, st_int => "Int", 30],
1105 [0, 4, st_wis => "Wis", 30],
1106 [0, 5, st_pow => "Pow", 30],
1107 [0, 6, st_cha => "Cha", 30],
1108
1109 [2, 0, st_wc => "Wc", -120],
1110 [2, 1, st_ac => "Ac", -120],
1111 [2, 2, st_dam => "Dam", 120],
1112 [2, 3, st_arm => "Arm", 120],
1113 [2, 4, st_spd => "Spd", 10.54],
1114 [2, 5, st_wspd => "WSp", 10.54],
1115 ) {
1116 my ($col, $row, $id, $label, $template) = @$_;
1117
1118 $tbl->add_at ($col , $row, $STATWIDS->{$id} = new CFPlus::UI::Label
1119 font => $FONT_FIXED, can_hover => 1, can_events => 1, valign => 0,
1120 align => +1, template => $template, tooltip => "#stat_$label");
1121 $tbl->add_at ($col + 1, $row, $STATWIDS->{"$id\_lbl"} = new CFPlus::UI::Label
1122 font => $FONT_FIXED, can_hover => 1, can_events => 1, fg => $color2, valign => 0,
1123 align => -1, text => $label, tooltip => "#stat_$label");
1124 }
1125
1126 $vb->add (new CFPlus::UI::FancyFrame
1127 label => "Resistancies",
1128 child => (my $tbl2 = new CFPlus::UI::Table expand => 1),
1129 );
1130
1131 my $row = 0;
1132 my $col = 0;
1133
1134 my %resist_names = (
1135 slow => ["Slow",
1136 "<b>Slow</b> (slows you down when you are hit by the spell. Monsters will have an opportunity to come near you faster and hit you more often.)"],
1137 holyw => ["Holy Word",
1138 "<b>Holy Word</b> (resistance you against getting the fear when someone whose god doesn't like you spells the holy word on you.)"],
1139 conf => ["Confusion",
1140 "<b>Confusion</b> (If you are hit by confusion you will move into random directions, and likely into monsters.)"],
1141 fire => ["Fire",
1142 "<b>Fire</b> (just your resistance to fire spells like burning hands, dragonbreath, meteor swarm fire, ...)"],
1143 depl => ["Depletion",
1144 "<b>Depletion</b> (some monsters and other effects can cause stats depletion)"],
1145 magic => ["Magic",
1146 "<b>Magic</b> (resistance to magic spells like magic missile or similar)"],
1147 drain => ["Draining",
1148 "<b>Draining</b> (some monsters (e.g. vampires) and other effects can steal experience)"],
1149 acid => ["Acid",
1150 "<b>Acid</b> (resistance to acid, acid hurts pretty much and also corrodes your weapons)"],
1151 pois => ["Poison",
1152 "<b>Poison</b> (resistance to getting poisoned)"],
1153 para => ["Paralysation",
1154 "<b>Paralysation</b> (this resistance affects the chance you get paralysed)"],
1155 deat => ["Death",
1156 "<b>Death</b> (resistance against death spells)"],
1157 phys => ["Physical",
1158 "<b>Physical</b> (this is the resistance against physical attacks, like when a monster hit you in melee combat. The value displayed here is also displayed in the 'Arm' field on the left.)"],
1159 blind => ["Blind",
1160 "<b>Blind</b> (blind resistance affects the chance of a successful blinding attack)"],
1161 fear => ["Fear",
1162 "<b>Fear</b> (this attack will drive you away from monsters who cast this and hit you successfully, being resistant to this helps a lot when fighting those monsters)"],
1163 tund => ["Turn undead",
1164 "<b>Turn undead</b> (affects your resistancy to various forms of 'turn undead' spells. Only relevant when you are, in fact, undead..."],
1165 elec => ["Electricity",
1166 "<b>Electricity</b> (resistance against electricity, spells like large lightning, small lightning, ...)"],
1167 cold => ["Cold",
1168 "<b>Cold</b> (this is your resistance against cold spells like icestorm, snowstorm, ...)"],
1169 ghit => ["Ghost hit",
1170 "<b>Ghost hit</b> (special attack used by ghosts and ghost-like beings)"],
1171 );
1172
1173 for (qw/slow holyw conf fire depl magic
1174 drain acid pois para deat phys
1175 blind fear tund elec cold ghit/)
1176 {
1177 $tbl2->add_at ($col, $row,
1178 $STATWIDS->{"res_$_"} =
1179 new CFPlus::UI::Label
1180 font => $FONT_FIXED,
1181 template => "-100%",
1182 align => +1,
1183 valign => 0,
1184 can_events => 1,
1185 can_hover => 1,
1186 tooltip => $resist_names{$_}->[1],
1187 );
1188 $tbl2->add_at ($col + 1, $row, new CFPlus::UI::Image
1189 font => $FONT_FIXED,
1190 can_hover => 1,
1191 can_events => 1,
1192 path => "ui/resist/resist_$_.png",
1193 tooltip => $resist_names{$_}->[1],
1194 );
1195 $tbl2->add_at ($col + 2, $row, new CFPlus::UI::Label
1196 text => $resist_names{$_}->[0],
1197 font => $FONT_FIXED,
1198 can_hover => 1,
1199 can_events => 1,
1200 tooltip => $resist_names{$_}->[1],
1201 );
1202
1203 $row++;
1204 if ($row % 6 == 0) {
1205 $col += 3;
1206 $row = 0;
1207 }
1208 }
1209
1210 #update_stats_window ({});
1211
1212 $r
1213 }
1214
1215 sub skill_window {
1216 my $sw = new CFPlus::UI::ScrolledWindow (expand => 1);
1217
1218 $sw->add ($STATWIDS->{skill_tbl} = new CFPlus::UI::Table expand => 1, col_expand => [0, 0, 1, .1, 0, 0, 1, .1]);
1219
1220 $sw
1221 }
1222
1223 sub formsep($) {
1224 scalar reverse join ",", unpack "(A3)*", reverse $_[0] * 1
1225 }
1226
1227 my $METASERVER_ATIME;
1228
1229 sub update_metaserver {
1230 my ($metaserver_dialog) = @_;
1231
1232 $METASERVER = $metaserver_dialog
1233 if defined $metaserver_dialog;
1234
1235 return if $METASERVER_ATIME > time;
1236 $METASERVER_ATIME = time + 60;
1237
1238 my $table = $METASERVER->{table};
1239 $table->clear;
1240 $table->add_at (0, 0, my $label = new CFPlus::UI::Label max_w => $WIDTH * 0.8, text => "fetching server list...");
1241
1242 my $ok = 0;
1243
1244 CFPlus::background {
1245 my $ua = CFPlus::lwp_useragent;
1246
1247 CFPlus::background_msg CFPlus::from_json +(CFPlus::lwp_check $ua->get ($META_SERVER))->decoded_content;
1248 } sub {
1249 my ($msg) = @_;
1250 if ($msg) {
1251 $table->clear;
1252
1253 my @tip = (
1254 "The current number of users logged in on the server.",
1255 "The hostname of the server.",
1256 "The time this server has been running without being restarted.",
1257 "The server software version - a '+' indicates a Crossfire+ server.",
1258 "Short information about this server provided by its admins.",
1259 );
1260 my @col = qw(#Users Host Uptime Version Description);
1261 $table->add_at ($_, 0, new CFPlus::UI::Label
1262 can_hover => 1, can_events => 1,
1263 align => 0, fg => [1, 1, 0],
1264 text => $col[$_], tooltip => $tip[$_])
1265 for 0 .. $#col;
1266
1267 my @align = qw(1 0 1 1 -1);
1268
1269 my $y = 0;
1270 for my $m (@{ $msg->{servers} }) {
1271 my ($ip, $last, $host, $users, $version, $desc, $ibytes, $obytes, $uptime, $highlight) =
1272 @$m{qw(ip age hostname users version description ibytes obytes uptime highlight)};
1273
1274 for ($desc) {
1275 s/<br>/\n/gi;
1276 s/<li>/\n· /gi;
1277 s/<.*?>//sgi;
1278 s/&amp;/&/g;
1279 s/&lt;/</g;
1280 s/&gt;/>/g;
1281 }
1282
1283 $uptime = sprintf "%dd %02d:%02d:%02d",
1284 (int $uptime / 86400),
1285 (int $uptime / 3600) % 24,
1286 (int $uptime / 60) % 60,
1287 $uptime % 60;
1288
1289 $m = [$users, $host, $uptime, $version, $desc];
1290
1291 $y++;
1292
1293 $table->add_at (scalar @$m, $y, new CFPlus::UI::VBox children => [
1294 (new CFPlus::UI::Button
1295 text => "Use",
1296 tooltip => "Put this server into the <b>Host:Port</b> field",
1297 on_activate => sub {
1298 $HOST_ENTRY->set_text ($CFG->{profile}{default}{host} = $host);
1299 $METASERVER->hide;
1300 0
1301 },
1302 ),
1303 (new CFPlus::UI::Empty expand => 1),
1304 ]);
1305
1306 $table->add_at ($_, $y, new CFPlus::UI::Label
1307 max_w => $::WIDTH * 0.4,
1308 ellipsise => 0,
1309 align => $align[$_],
1310 text => $m->[$_],
1311 tooltip => $tip[$_],
1312 fg => ($highlight ? [1, 1, 1] : [.7, .7, .7]),
1313 can_hover => 1,
1314 can_events => 1,
1315 fontsize => 0.8)
1316 for 0 .. $#$m;
1317 }
1318 } else {
1319 $ok or $label->set_text ("error while contacting metaserver");
1320 }
1321 };
1322
1323 }
1324
1325 sub metaserver_dialog {
1326 my $vbox = new CFPlus::UI::VBox;
1327 my $table = new CFPlus::UI::Table;
1328 $vbox->add (new CFPlus::UI::ScrolledWindow expand => 1, child => $table);
1329
1330 my $dialog = new CFPlus::UI::Toplevel
1331 title => "Server List",
1332 name => 'metaserver_dialog',
1333 x => 'center',
1334 y => 'center',
1335 z => 3,
1336 force_w => $::WIDTH * 0.9,
1337 force_h => $::HEIGHT * 0.7,
1338 child => $vbox,
1339 has_close_button => 1,
1340 table => $table,
1341 on_visibility_change => sub {
1342 update_metaserver ($_[0]) if $_[1];
1343 0
1344 },
1345 ;
1346
1347 $dialog
1348 }
1349
1350 sub server_setup {
1351 my $vbox = new CFPlus::UI::VBox;
1352
1353 $vbox->add (new CFPlus::UI::FancyFrame
1354 label => "Connection Settings",
1355 child => (my $table = new CFPlus::UI::Table expand => 1, col_expand => [0, 1]),
1356 );
1357 $table->add_at (0, 2, new CFPlus::UI::Label valign => 0, align => 1, text => "Host:Port");
1358
1359 {
1360 $table->add_at (1, 2, my $vbox = new CFPlus::UI::VBox);
1361
1362 $vbox->add (
1363 $HOST_ENTRY = new CFPlus::UI::Entry
1364 expand => 1,
1365 text => $CFG->{profile}{default}{host},
1366 tooltip => "The hostname or ip address of the Crossfire(+) server to connect to",
1367 on_changed => sub {
1368 my ($self, $value) = @_;
1369 $CFG->{profile}{default}{host} = $value;
1370 1
1371 }
1372 );
1373
1374 $vbox->add (new CFPlus::UI::Button
1375 expand => 1,
1376 text => "Server List",
1377 other => $METASERVER,
1378 tooltip => "Show a list of available crossfire servers",
1379 on_activate => sub { $METASERVER->toggle_visibility; 0 },
1380 on_visibility_change => sub { $METASERVER->hide unless $_[1]; 1 },
1381 );
1382 }
1383
1384 $table->add_at (0, 4, new CFPlus::UI::Label valign => 0, align => 1, text => "Username");
1385 $table->add_at (1, 4, new CFPlus::UI::Entry
1386 text => $CFG->{profile}{default}{user},
1387 tooltip => "The name of your character on the server",
1388 on_changed => sub { my ($self, $value) = @_; $CFG->{profile}{default}{user} = $value; 1 }
1389 );
1390
1391 $table->add_at (0, 5, new CFPlus::UI::Label valign => 0, align => 1, text => "Password");
1392 $table->add_at (1, 5, new CFPlus::UI::Entry
1393 text => $CFG->{profile}{default}{password},
1394 hidden => 1,
1395 tooltip => "The password for your character",
1396 on_changed => sub { my ($self, $value) = @_; $CFG->{profile}{default}{password} = $value; 1 }
1397 );
1398
1399 $table->add_at (0, 7, new CFPlus::UI::Label valign => 0, align => 1, text => "Map Size");
1400 $table->add_at (1, 7, new CFPlus::UI::Slider
1401 force_w => 100,
1402 range => [$CFG->{mapsize}, 10, 100, 0, 1],
1403 tooltip => "This is the size of the portion of the map update the server sends you. "
1404 . "If you set this to a high value you will be able to see further, "
1405 . "but you also increase bandwidth requirements and latency. "
1406 . "This option is only used once at log-in.",
1407 on_changed => sub { my ($self, $value) = @_; $CFG->{mapsize} = $self->{range}[0] = $value = int $value; 1 },
1408 );
1409
1410 $table->add_at (0, 8, new CFPlus::UI::Label valign => 0, align => 1, text => "Output-Rate");
1411 $table->add_at (1, 8, new CFPlus::UI::Entry
1412 text => $CFG->{output_rate},
1413 tooltip => "The maximum bandwidth in bytes per second that the server should not exceed "
1414 . "when sending data. When 0 or unset, the server "
1415 . "default will be used, which is usually around 100kb/s. Most servers will "
1416 . "dynamically find an optimal rate, so adjust this only when necessary.",
1417 on_changed => sub { $CFG->{output_rate} = $_[1]; 1 },
1418 );
1419
1420 $table->add_at (0, 9, new CFPlus::UI::Label valign => 0, align => 1, text => "Output-Count");
1421 $table->add_at (1, 9, new CFPlus::UI::Entry
1422 text => $CFG->{output_count},
1423 tooltip => "Should be set to 1 unless you know what you are doing. This option is only used once at log-in.",
1424 on_changed => sub { $CFG->{output_count} = $_[1]; 1 },
1425 );
1426
1427 $table->add_at (0, 10, new CFPlus::UI::Label valign => 0, align => 1, text => "Output-Sync");
1428 $table->add_at (1, 10, new CFPlus::UI::Entry
1429 text => $CFG->{output_sync},
1430 tooltip => "Should be set to 1 unless you know what you are doing. This option is only used once at log-in.",
1431 on_changed => sub { $CFG->{output_sync} = $_[1]; 1 },
1432 );
1433
1434 $table->add_at (1, 11, $LOGIN_BUTTON = new CFPlus::UI::Button
1435 expand => 1,
1436 align => 0,
1437 text => "Login",
1438 on_activate => sub {
1439 $CONN ? stop_game
1440 : start_game;
1441 1
1442 },
1443 );
1444
1445 $vbox->add (new CFPlus::UI::FancyFrame
1446 label => "Server Info",
1447 child => ($SERVER_INFO = new CFPlus::UI::Label ellipsise => 0),
1448 );
1449
1450 $vbox
1451 }
1452
1453 sub client_setup {
1454 my $table = new CFPlus::UI::Table expand => 1, col_expand => [0, 1];
1455
1456 my $row = 0;
1457
1458 $table->add_at (0, $row, new CFPlus::UI::Label valign => 0, align => 1, text => "Tip of the day");
1459 $table->add_at (1, $row++, new CFPlus::UI::CheckBox
1460 state => $CFG->{show_tips},
1461 tooltip => "Show the <b>Tip of the day</b> window at startup?",
1462 on_changed => sub {
1463 my ($self, $value) = @_;
1464 $CFG->{show_tips} = $value;
1465 0
1466 }
1467 );
1468
1469 $table->add_at (0, $row, new CFPlus::UI::Label valign => 0, align => 1, text => "Messages Window Size");
1470 $table->add_at (1, $row++, my $saycmd = new CFPlus::UI::Entry
1471 text => $CFG->{logview_max_par},
1472 tooltip => "This is maximum number of messages remembered in the <b>Messages</b> window. If the server "
1473 . "sends more messages than this number, older messages get removed to save memory and "
1474 . "computing time. A value of <b>0</b> disables this feature, but that is not recommended.",
1475 on_changed => sub {
1476 my ($self, $value) = @_;
1477 $MESSAGE_WINDOW->set_max_para ($CFG->{logview_max_par} = $value*1);
1478 0
1479 },
1480 );
1481
1482 $table
1483 }
1484
1485 sub autopickup_setup {
1486 my $r = new CFPlus::UI::ScrolledWindow (
1487 expand => 1,
1488 scroll_y => 1
1489 );
1490 $r->add (my $table = new CFPlus::UI::Table
1491 row_expand => [0],
1492 col_expand => [0, 1, 0, 1],
1493 );
1494
1495 for (
1496 ["General", 0, 0,
1497 ["Enable autopickup" => PICKUP_NEWMODE, \$PICKUP_ENABLE],
1498 ["Inhibit autopickup" => PICKUP_INHIBIT],
1499 ["Stop before pickup" => PICKUP_STOP],
1500 ["Debug autopickup" => PICKUP_DEBUG],
1501 ],
1502 ["Weapons", 0, 6,
1503 ["All weapons" => PICKUP_ALLWEAPON],
1504 ["Missile weapons" => PICKUP_MISSILEWEAPON],
1505 ["Bows" => PICKUP_BOW],
1506 ["Arrows" => PICKUP_ARROW],
1507 ],
1508 ["Armour", 0, 12,
1509 ["Helmets" => PICKUP_HELMET],
1510 ["Shields" => PICKUP_SHIELD],
1511 ["Body Armour" => PICKUP_ARMOUR],
1512 ["Boots" => PICKUP_BOOTS],
1513 ["Gloves" => PICKUP_GLOVES],
1514 ["Cloaks" => PICKUP_CLOAK],
1515 ],
1516
1517 ["Readables", 2, 0,
1518 ["Spellbooks" => PICKUP_SPELLBOOK],
1519 ["Skillscrolls" => PICKUP_SKILLSCROLL],
1520 ["Normal Books/Scrolls" => PICKUP_READABLES],
1521 ],
1522 ["Misc", 2, 5,
1523 ["Food" => PICKUP_FOOD],
1524 ["Drinks" => PICKUP_DRINK],
1525 ["Valuables (Money, Gems)" => PICKUP_VALUABLES],
1526 ["Keys" => PICKUP_KEY],
1527 ["Magical Items" => PICKUP_MAGICAL],
1528 ["Potions" => PICKUP_POTION],
1529 ["Magic Devices" => PICKUP_MAGIC_DEVICE],
1530 ["Ignore cursed" => PICKUP_NOT_CURSED],
1531 ["Jewelery" => PICKUP_JEWELS],
1532 ["Flesh" => PICKUP_FLESH],
1533 ],
1534 ["Weight/Value ratio", 2, 17]
1535 )
1536 {
1537 my ($title, $x, $y, @bits) = @$_;
1538 $table->add_at ($x, $y, new CFPlus::UI::Label text => $title, align => 1, fg => [1, 1, 0]);
1539
1540 for (@bits) {
1541 ++$y;
1542
1543 my $mask = $_->[1];
1544 $table->add_at ($x , $y, new CFPlus::UI::Label text => $_->[0], align => 1, expand => 1);
1545 $table->add_at ($x+1, $y, my $checkbox = new CFPlus::UI::CheckBox
1546 state => $::CFG->{pickup} & $mask,
1547 on_changed => sub {
1548 my ($box, $value) = @_;
1549
1550 if ($value) {
1551 $::CFG->{pickup} |= $mask;
1552 } else {
1553 $::CFG->{pickup} &= ~$mask;
1554 }
1555
1556 $::CONN->send_command ("pickup $::CFG->{pickup}")
1557 if defined $::CONN;
1558
1559 0
1560 });
1561
1562 ${$_->[2]} = $checkbox if $_->[2];
1563 }
1564 }
1565
1566 $table->add_at (2, 18, new CFPlus::UI::ValSlider
1567 range => [$::CFG->{pickup} & 0xF, 0, 16, 1, 1],
1568 template => ">= 99",
1569 to_value => sub { ">= " . 5 * $_[0] },
1570 on_changed => sub {
1571 my ($slider, $value) = @_;
1572
1573 $::CFG->{pickup} &= ~0xF;
1574 $::CFG->{pickup} |= int $value
1575 if $value;
1576 1;
1577 });
1578
1579 $table->add_at (3, 18, new CFPlus::UI::Button
1580 text => "set",
1581 on_activate => sub {
1582 $::CONN->send_command ("pickup $::CFG->{pickup}")
1583 if defined $::CONN;
1584 0
1585 });
1586
1587 $r
1588 }
1589
1590 my %SORT_ORDER = (
1591 type => undef,
1592 mtime => sub {
1593 my $NOW = time;
1594 sort {
1595 my $atime = $a->{mtime} - $NOW; $atime = $atime < 5 * 60 ? int $atime / 60 : 6;
1596 my $btime = $b->{mtime} - $NOW; $btime = $btime < 5 * 60 ? int $btime / 60 : 6;
1597
1598 ($a->{flags} & F_LOCKED) <=> ($b->{flags} & F_LOCKED)
1599 or $btime <=> $atime
1600 or $a->{type} <=> $b->{type}
1601 } @_
1602 },
1603 weight => sub { sort {
1604 $a->{weight} * ($a->{nrof} || 1) <=> $b->{weight} * ($b->{nrof} || 1)
1605 or $a->{type} <=> $b->{type}
1606 } @_ },
1607 );
1608
1609 sub inventory_widget {
1610 my $hb = new CFPlus::UI::HBox homogeneous => 1;
1611
1612 $hb->add (my $vb1 = new CFPlus::UI::VBox);
1613 $vb1->add (new CFPlus::UI::Label align => 0, text => "Player");
1614
1615 $vb1->add (my $hb1 = new CFPlus::UI::HBox);
1616
1617 use sort 'stable';
1618
1619 $hb1->add (new CFPlus::UI::Selector
1620 value => $::CFG->{inv_sort},
1621 options => [
1622 [type => "Type/Name"],
1623 [mtime => "Recent/Normal/Locked"],
1624 [weight => "Weight/Type"],
1625 ],
1626 on_changed => sub {
1627 $::CFG->{inv_sort} = $_[1];
1628 $INV->set_sort_order ($SORT_ORDER{$_[1]});
1629 },
1630 );
1631 $hb1->add (new CFPlus::UI::Label text => "Weight: ", align => 1, expand => 1);
1632 #TODO# update to weigh/maxweight
1633 $hb1->add ($STATWIDS->{i_weight} = new CFPlus::UI::Label align => -1);
1634
1635 $vb1->add (my $sw1 = new CFPlus::UI::ScrolledWindow expand => 1, scroll_y => 1);
1636 $sw1->add ($INV = new CFPlus::UI::Inventory);
1637 $INV->set_sort_order ($SORT_ORDER{$::CFG->{inv_sort}});
1638
1639 $hb->add (my $vb2 = new CFPlus::UI::VBox);
1640
1641 $vb2->add ($INVR_HB = new CFPlus::UI::HBox);
1642
1643 $vb2->add (my $sw2 = new CFPlus::UI::ScrolledWindow expand => 1, scroll_y => 1);
1644 $sw2->add ($INVR = new CFPlus::UI::Inventory);
1645
1646 # XXX: Call after $INVR = ... because set_opencont sets the items
1647 CFPlus::Protocol::set_opencont ($::CONN, 0, "Floor");
1648
1649 $hb
1650 }
1651
1652 sub media_window {
1653 my $vb = new CFPlus::UI::VBox;
1654
1655 $vb->add (new CFPlus::UI::FancyFrame
1656 label => "Currently playing music",
1657 child => new CFPlus::UI::ScrolledWindow scroll_x => 1, scroll_y => 0,
1658 child => ($MUSIC_PLAYING_WIDGET = new CFPlus::UI::Label ellipsise => 0, fontsize => 0.8),
1659 );
1660
1661 $vb->add (new CFPlus::UI::FancyFrame
1662 label => "Other media used in this session",
1663 expand => 1,
1664 child => ($LICENSE_WIDGET = new CFPlus::UI::TextScroller
1665 expand => 1, fontsize => 0.8, padding_x => 4, padding_y => 4),
1666 );
1667
1668 $vb
1669 }
1670
1671 sub add_license {
1672 my ($meta) = @_;
1673
1674 $meta = $meta->{data}
1675 or return;
1676
1677 $meta->{license} || $meta->{author} || $meta->{source}
1678 or return;
1679
1680 $LICENSE_WIDGET->add_paragraph ({
1681 fg => [1, 1, 1, 1],
1682 markup => "<small>"
1683 . "<b>Name:</b> " . (CFPlus::asxml $meta->{name}) . "\n"
1684 . "<b>Author:</b> " . (CFPlus::asxml $meta->{author}) . "\n"
1685 . "<b>Source:</b> " . (CFPlus::asxml $meta->{source}) . "\n"
1686 . "<b>License:</b> " . (CFPlus::asxml $meta->{license}) . "\n"
1687 . "</small>",
1688 });
1689 $LICENSE_WIDGET->scroll_to_bottom;
1690 }
1691
1692 sub toggle_player_page {
1693 my ($widget) = @_;
1694
1695 if ($PL_WINDOW->{visible} && $PL_NOTEBOOK->get_current_page == $widget) {
1696 $PL_WINDOW->hide;
1697 } else {
1698 $PL_NOTEBOOK->set_current_page ($widget);
1699 $PL_WINDOW->show;
1700 }
1701 }
1702
1703 sub player_window {
1704 my $plwin = $PL_WINDOW = new CFPlus::UI::Toplevel
1705 x => "center",
1706 y => "center",
1707 force_w => $WIDTH * 9/10,
1708 force_h => $HEIGHT * 9/10,
1709 title => "Player",
1710 name => "playerbook",
1711 has_close_button => 1
1712 ;
1713
1714 my $ntb =
1715 $PL_NOTEBOOK =
1716 new CFPlus::UI::Notebook expand => 1;
1717
1718 $ntb->add_tab (
1719 "Statistics (F2)" => $STATS_PAGE = stats_window,
1720 "Shows statistics, where all your Stats and Resistances are shown."
1721 );
1722 $ntb->add_tab (
1723 "Skills (F3)" => $SKILL_PAGE = skill_window,
1724 "Shows all your Skills."
1725 );
1726
1727 my $spellsw = $SPELL_PAGE = new CFPlus::UI::ScrolledWindow (expand => 1, scroll_y => 1);
1728 $spellsw->add ($SPELL_LIST = new CFPlus::UI::SpellList);
1729 $ntb->add_tab (
1730 "Spellbook (F4)" => $spellsw,
1731 "Displays all spells you have and lets you edit keyboard shortcuts for them."
1732 );
1733 $ntb->add_tab (
1734 "Inventory (F5)" => $INVENTORY_PAGE = inventory_widget,
1735 "Toggles the inventory window, where you can manage your loot (or treasures :). "
1736 . "You can also hit the <b>Tab</b>-key to show/hide the Inventory."
1737 );
1738 $ntb->add_tab (Pickup => $PICKUP_PAGE = autopickup_setup,
1739 "Configure autopickup settings, i.e. which items you will pick up automatically when walking (or running) over them.");
1740
1741 $ntb->add_tab (Media => media_window,
1742 "License, Author and Source info for media sent by the server.");
1743
1744 $ntb->set_current_page ($INVENTORY_PAGE);
1745
1746 $plwin->add ($ntb);
1747 $plwin
1748 }
1749
1750 sub keyboard_setup {
1751 CFPlus::Macro::keyboard_setup
1752 }
1753
1754 sub help_window {
1755 my $win = new CFPlus::UI::Toplevel
1756 x => 'center',
1757 y => 'center',
1758 z => 4,
1759 name => 'doc_browser',
1760 force_w => int $WIDTH * 7/8,
1761 force_h => int $HEIGHT * 7/8,
1762 title => "Help Browser",
1763 has_close_button => 1;
1764
1765 $win->add (my $vbox = new CFPlus::UI::VBox);
1766
1767 $vbox->add (new CFPlus::UI::FancyFrame
1768 label => "Navigation",
1769 child => (my $buttons = new CFPlus::UI::HBox),
1770 );
1771 $vbox->add (my $viewer = new CFPlus::UI::TextScroller
1772 expand => 1, fontsize => 0.8, padding_x => 4, padding_y => 4);
1773
1774 my @history;
1775 my @future;
1776 my $curnode;
1777
1778 my $load_node; $load_node = sub {
1779 my ($node, $para) = @_;
1780
1781 $buttons->clear;
1782
1783 $buttons->add (new CFPlus::UI::Button
1784 text => "⇤",
1785 tooltip => "back to the starting page",
1786 on_activate => sub {
1787 unshift @future, [$curnode, $viewer->current_paragraph] if $curnode;
1788 unshift @future, @history;
1789 @history = ();
1790 $load_node->(@{shift @future});
1791 },
1792 );
1793
1794 if (@history) {
1795 $buttons->add (new CFPlus::UI::Button
1796 text => "⋘",
1797 tooltip => "back to <i>" . (CFPlus::asxml CFPlus::Pod::full_path $history[-1][0]) . "</i>",
1798 on_activate => sub {
1799 unshift @future, [$curnode, $viewer->current_paragraph] if $curnode;
1800 $load_node->(@{pop @history});
1801 },
1802 );
1803 }
1804
1805 if (@future) {
1806 $buttons->add (new CFPlus::UI::Button
1807 text => "⋙",
1808 tooltip => "forward to <i>" . (CFPlus::asxml CFPlus::Pod::full_path $future[0][0]) . "</i>",
1809 on_activate => sub {
1810 push @history, [$curnode, $viewer->current_paragraph];
1811 $load_node->(@{shift @future});
1812 },
1813 );
1814 }
1815
1816 $buttons->add (new CFPlus::UI::Label text => " ");
1817
1818 my @path = CFPlus::Pod::full_path_of $node;
1819 pop @path; # drop current node
1820
1821 for my $node (@path) {
1822 $buttons->add (new CFPlus::UI::Button
1823 text => $node->{kw}[0],
1824 tooltip => "go to <i>" . (CFPlus::asxml CFPlus::Pod::full_path $node) . "</i>",
1825 on_activate => sub {
1826 push @history, [$curnode, $viewer->current_paragraph] if $curnode; @future = ();
1827 $load_node->($node);
1828 },
1829 );
1830 $buttons->add (new CFPlus::UI::Label text => "/");
1831 }
1832
1833 $buttons->add (new CFPlus::UI::Label text => $node->{kw}[0], padding_x => 4, padding_y => 4);
1834
1835 $curnode = $node;
1836
1837 $viewer->clear;
1838 $viewer->add_paragraph (CFPlus::Pod::as_paragraphs CFPlus::Pod::section_of $curnode);
1839 $viewer->scroll_to ($para);
1840 };
1841
1842 $load_node->(CFPlus::Pod::find pod => "mainpage");
1843
1844 $CFPlus::Pod::goto_document = sub {
1845 my (@path) = @_;
1846
1847 push @history, [$curnode, $viewer->current_paragraph] if $curnode; @future = ();
1848
1849 $load_node->((CFPlus::Pod::find @path)[0]);
1850 $win->show;
1851 };
1852
1853 $win
1854 }
1855
1856 sub open_string_query {
1857 my ($title, $cb, $txt, $tooltip) = @_;
1858 my $dialog = new CFPlus::UI::Toplevel
1859 x => "center",
1860 y => "center",
1861 z => 50,
1862 force_w => $WIDTH * 4/5,
1863 title => $title;
1864
1865 $dialog->add (
1866 my $e = new CFPlus::UI::Entry
1867 on_activate => sub { $cb->(@_); $dialog->hide; 0 },
1868 on_key_down => sub { $_[1]->{sym} == 27 and $dialog->hide; 0 },
1869 tooltip => $tooltip
1870 );
1871
1872 $e->grab_focus;
1873 $e->set_text ($txt) if $txt;
1874 $dialog->show;
1875 }
1876
1877 sub open_quit_dialog {
1878 unless ($QUIT_DIALOG) {
1879 $QUIT_DIALOG = new CFPlus::UI::Toplevel
1880 x => "center",
1881 y => "center",
1882 z => 50,
1883 title => "Really Quit?",
1884 on_key_down => sub {
1885 my ($dialog, $ev) = @_;
1886 $ev->{sym} == 27 and $dialog->hide;
1887 }
1888 ;
1889
1890 $QUIT_DIALOG->add (my $vb = new CFPlus::UI::VBox expand => 1);
1891
1892 $vb->add (new CFPlus::UI::Label
1893 text => "You should find a savebed and apply it first!",
1894 max_w => $WIDTH * 0.25,
1895 ellipsize => 0,
1896 );
1897 $vb->add (my $hb = new CFPlus::UI::HBox expand => 1);
1898 $hb->add (new CFPlus::UI::Button
1899 text => "Ok",
1900 expand => 1,
1901 on_activate => sub { $QUIT_DIALOG->hide; 0 },
1902 );
1903 $hb->add (new CFPlus::UI::Button
1904 text => "Quit anyway",
1905 expand => 1,
1906 on_activate => sub { Event::unloop_all },
1907 );
1908 }
1909
1910 $QUIT_DIALOG->show;
1911 $QUIT_DIALOG->grab_focus;
1912 }
1913
1914 sub show_tip_of_the_day {
1915 # find all tips
1916 my @tod = CFPlus::Pod::find tip_of_the_day => "*";
1917
1918 CFPlus::DB::get state => "tip_of_the_day", sub {
1919 my ($todindex) = @_;
1920 $todindex = 0 if $todindex >= @tod;
1921 CFPlus::DB::put state => tip_of_the_day => $todindex + 1, sub { };
1922
1923 # create dialog
1924 my $dialog;
1925
1926 my $close = sub {
1927 $dialog->destroy;
1928 };
1929
1930 $dialog = new CFPlus::UI::Toplevel
1931 x => "center",
1932 y => "center",
1933 z => 3,
1934 name => 'tip_of_the_day',
1935 force_w => int $WIDTH * 4/9,
1936 force_h => int $WIDTH * 2/9,
1937 title => "Tip of the day #" . (1 + $todindex),
1938 child => my $vbox = new CFPlus::UI::VBox,
1939 has_close_button => 1,
1940 on_delete => $close,
1941 ;
1942
1943 $vbox->add (my $viewer = new CFPlus::UI::TextScroller
1944 expand => 1, fontsize => 0.8, padding_x => 4, padding_y => 4);
1945 $viewer->add_paragraph (CFPlus::Pod::as_paragraphs CFPlus::Pod::section_of $tod[$todindex]);
1946
1947 $vbox->add (my $table = new CFPlus::UI::Table col_expand => [0, 1]);
1948
1949 $table->add_at (0, 0, new CFPlus::UI::Button
1950 text => "Close",
1951 tooltip => "Close the tip of the day window. To never see it again, disable the tip of the day in the <b>Server Setup</b>.",
1952 on_activate => $close,
1953 );
1954
1955 $table->add_at (2, 0, new CFPlus::UI::Button
1956 text => "Next",
1957 tooltip => "Show the next <b>Tip of the day</b>.",
1958 on_activate => sub {
1959 $close->();
1960 &show_tip_of_the_day;
1961 },
1962 );
1963
1964 $dialog->show;
1965 };
1966 }
1967
1968 sub sdl_init {
1969 CFPlus::SDL_Init
1970 and die "SDL::Init failed!\n";
1971 }
1972
1973 sub video_init {
1974 $CFG->{sdl_mode} = 0 if $CFG->{sdl_mode} >= @SDL_MODES;
1975
1976 my ($old_w, $old_h) = ($WIDTH, $HEIGHT);
1977
1978 ($WIDTH, $HEIGHT, my ($rgb, $alpha)) = @{ $SDL_MODES[$CFG->{sdl_mode}] };
1979 $FULLSCREEN = $CFG->{fullscreen};
1980 $FAST = $CFG->{fast};
1981
1982 CFPlus::SDL_SetVideoMode $WIDTH, $HEIGHT, $rgb, $alpha, $FULLSCREEN
1983 or die "SDL_SetVideoMode failed: " . (CFPlus::SDL_GetError) . "\n";
1984
1985 $SDL_ACTIVE = 1;
1986 $LAST_REFRESH = time - 0.01;
1987
1988 CFPlus::OpenGL::init;
1989 CFPlus::Macro::init;
1990
1991 $FONTSIZE = int $HEIGHT / 40 * $CFG->{gui_fontsize};
1992
1993 $CFPlus::UI::ROOT->configure (0, 0, $WIDTH, $HEIGHT);#d#
1994
1995 #############################################################################
1996
1997 if ($DEBUG_STATUS) {
1998 CFPlus::UI::rescale_widgets $WIDTH / $old_w, $HEIGHT / $old_h;
1999 } else {
2000 # create/configure the widgets
2001
2002 $CFPlus::UI::ROOT->connect (key_down => sub {
2003 my (undef, $ev) = @_;
2004
2005 if (my @macros = CFPlus::Macro::find $ev) {
2006 CFPlus::Macro::execute $_ for @macros;
2007
2008 return 1;
2009 }
2010
2011 0
2012 });
2013
2014 $DEBUG_STATUS = new CFPlus::UI::Label
2015 padding => 0,
2016 z => 100,
2017 force_x => "max",
2018 force_y => 0;
2019 $DEBUG_STATUS->show;
2020
2021 $STATUSBOX = new CFPlus::UI::Statusbox;
2022 $STATUSBOX->add ("Use <b>Alt-Enter</b> to toggle fullscreen mode", timeout => 864000, pri => -100, color => [1, 1, 1, 0.8]);
2023
2024 (new CFPlus::UI::Frame
2025 bg => [0, 0, 0, 0.4],
2026 force_x => 0,
2027 force_y => "max",
2028 child => $STATUSBOX,
2029 )->show;
2030
2031 CFPlus::UI::Toplevel->new (
2032 title => "Map",
2033 name => "mapmap",
2034 x => 0,
2035 y => $FONTSIZE + 8,
2036 border_bg => [1, 1, 1, 192/255],
2037 bg => [1, 1, 1, 0],
2038 child => ($MAPMAP = new CFPlus::MapWidget::MapMap
2039 tooltip => "<b>Map</b>. On servers that support this feature, this will display an overview of the surrounding areas.",
2040 ),
2041 )->show;
2042
2043 $MAPWIDGET = new CFPlus::MapWidget;
2044 $MAPWIDGET->connect (activate_console => sub {
2045 my ($mapwidget, $preset) = @_;
2046
2047 $MESSAGE_WINDOW->activate_console ($preset)
2048 if $MESSAGE_WINDOW;
2049 });
2050 $MAPWIDGET->show;
2051 $MAPWIDGET->grab_focus;
2052
2053 $COMPLETER = new CFPlus::MapWidget::Command::
2054 command => { },
2055 tooltip => "#completer_help",
2056 ;
2057
2058 $SETUP_DIALOG = new CFPlus::UI::Toplevel
2059 title => "Setup",
2060 name => "setup_dialog",
2061 x => 'center',
2062 y => 'center',
2063 z => 2,
2064 force_w => $::WIDTH * 0.6,
2065 force_h => $::HEIGHT * 0.6,
2066 has_close_button => 1,
2067 ;
2068
2069 $METASERVER = metaserver_dialog;
2070 $MESSAGE_WINDOW = new CFPlus::UI::MessageWindow;
2071
2072 $SETUP_DIALOG->add ($SETUP_NOTEBOOK = new CFPlus::UI::Notebook expand => 1, debug => 1,
2073 filter => new CFPlus::UI::ScrolledWindow expand => 1, scroll_y => 1);
2074
2075 $SETUP_NOTEBOOK->add_tab (Server => $SETUP_SERVER = server_setup,
2076 "Configure the server to play on, your username, password and other server-related options.");
2077 $SETUP_NOTEBOOK->add_tab (Client => client_setup,
2078 "Configure various client-specific settings.");
2079 $SETUP_NOTEBOOK->add_tab (Graphics => graphics_setup,
2080 "Configure the video mode, performance, fonts and other graphical aspects of the game.");
2081 $SETUP_NOTEBOOK->add_tab (Audio => audio_setup,
2082 "Configure the use of audio, sound effects and background music.");
2083 $SETUP_NOTEBOOK->add_tab (Keyboard => $SETUP_KEYBOARD = keyboard_setup,
2084 "Lets you define, edit and delete key bindings."
2085 . "There is a shortcut for making bindings: <b>Control-Insert</b> opens the binding editor "
2086 . "with nothing set and the recording started. After doing the actions you "
2087 . "want to record press <b>Insert</b> and you will be asked to press a key-combo. "
2088 . "After pressing the combo the binding will be saved automatically and the "
2089 . "binding editor closes");
2090 $SETUP_NOTEBOOK->add_tab (Debug => debug_setup,
2091 "Some debuggin' options. Do not ask.");
2092
2093 $BUTTONBAR = new CFPlus::UI::Buttonbar x => 0, y => 0, z => 200; # put on top
2094
2095 $BUTTONBAR->add (new CFPlus::UI::Flopper text => "Setup", other => $SETUP_DIALOG,
2096 tooltip => "Toggles a dialog where you can configure all aspects of this client.");
2097
2098 $BUTTONBAR->add (new CFPlus::UI::Flopper text => "Message Window", other => $MESSAGE_WINDOW,
2099 tooltip => "Toggles the server message log, where the client collects <i>all</i> messages from the server.");
2100
2101 make_gauge_window->show; # XXX: this has to be set before make_stats_window as make_stats_window calls update_stats_window which updated the gauges also X-D
2102
2103 $BUTTONBAR->add (new CFPlus::UI::Flopper text => "Playerbook", other => player_window,
2104 tooltip => "Toggles the player view, where you can manage Inventory, Spells, Skills and see your Stats.");
2105
2106 $BUTTONBAR->add (new CFPlus::UI::Button
2107 text => "Save Config",
2108 tooltip => "Saves the options chosen in the client setting, server settings and the window layout to be restored on later runs.",
2109 on_activate => sub {
2110 $::CFG->{layout} = CFPlus::UI::get_layout;
2111 CFPlus::write_cfg "$Crossfire::VARDIR/cfplusrc";
2112 status "Configuration Saved";
2113 0
2114 },
2115 );
2116
2117 $BUTTONBAR->add (new CFPlus::UI::Flopper text => "Help!", other => $HELP_WINDOW = help_window,
2118 tooltip => "View Documentation");
2119
2120
2121 $BUTTONBAR->add (new CFPlus::UI::Button
2122 text => "Quit",
2123 tooltip => "Terminates the program",
2124 on_activate => sub {
2125 if ($CONN) {
2126 open_quit_dialog;
2127 } else {
2128 Event::unloop_all;
2129 }
2130 0
2131 },
2132 );
2133
2134 $BUTTONBAR->show;
2135 $SETUP_DIALOG->show;
2136 $MESSAGE_WINDOW->show;
2137 }
2138
2139 $STATUSBOX->add ("Set video mode $WIDTH×$HEIGHT", timeout => 10, fg => [1, 1, 1, 0.5]);
2140 }
2141
2142 sub video_shutdown {
2143 CFPlus::OpenGL::shutdown;
2144
2145 undef $SDL_ACTIVE;
2146 }
2147
2148 my %animate_object;
2149 my $animate_timer;
2150
2151 my $fps = 9;
2152
2153 sub force_refresh {
2154 if ($ENV{CFPLUS_DEBUG} & 4) {
2155 $fps = $fps * 0.98 + 1 / (($NOW - $LAST_REFRESH) || 0.1) * 0.02;
2156 debug sprintf "%3.2f", $fps;
2157 }
2158
2159 $CFPlus::UI::ROOT->draw;
2160 CFPlus::SDL_GL_SwapBuffers;
2161 $LAST_REFRESH = $NOW;
2162 $WANT_REFRESH->stop;
2163 }
2164
2165 $WANT_REFRESH = Event->idle (min => 0.001, max => 0.06, parked => 1, cb => \&force_refresh);
2166
2167 my $input = Event->timer (after => 0, hard => 0, interval => 1 / 50, cb => sub {
2168 $NOW = time;
2169
2170 ($SDL_CB{$_->{type}} || sub { warn "unhandled event $_->{type}" })->($_)
2171 for CFPlus::poll_events;
2172
2173 if (%animate_object) {
2174 $_->animate ($LAST_REFRESH - $NOW) for values %animate_object;
2175 $WANT_REFRESH->start;
2176 }
2177 });
2178
2179 sub animation_start {
2180 my ($widget) = @_;
2181 $animate_object{$widget} = $widget;
2182 }
2183
2184 sub animation_stop {
2185 my ($widget) = @_;
2186 delete $animate_object{$widget};
2187 }
2188
2189 %SDL_CB = (
2190 CFPlus::SDL_QUIT => sub {
2191 Event::unloop_all;
2192 },
2193 CFPlus::SDL_VIDEORESIZE => sub {
2194 },
2195 CFPlus::SDL_VIDEOEXPOSE => sub {
2196 CFPlus::UI::full_refresh;
2197 },
2198 CFPlus::SDL_ACTIVEEVENT => sub {
2199 # not useful, as APPACTIVE include sonly iconified state, not unmapped
2200 # printf "active %x %x %x\n", $_[0]{gain}, $_[0]{state}, CFPlus::SDL_GetAppState;#d#
2201 # printf "a %x\n", CFPlus::SDL_GetAppState & CFPlus::SDL_APPACTIVE;#d#
2202 # printf "A\n" if $_[0]{state} & CFPlus::SDL_APPACTIVE;
2203 # printf "K\n" if $_[0]{state} & CFPlus::SDL_APPINPUTFOCUS;
2204 # printf "M\n" if $_[0]{state} & CFPlus::SDL_APPMOUSEFOCUS;
2205 },
2206 CFPlus::SDL_KEYDOWN => sub {
2207 if ($_[0]{mod} & CFPlus::KMOD_ALT && $_[0]{sym} == 13) {
2208 # alt-enter
2209 $FULLSCREEN_ENABLE->toggle;
2210 video_shutdown;
2211 video_init;
2212 } else {
2213 CFPlus::UI::feed_sdl_key_down_event ($_[0]);
2214 }
2215 },
2216 CFPlus::SDL_KEYUP => \&CFPlus::UI::feed_sdl_key_up_event,
2217 CFPlus::SDL_MOUSEMOTION => \&CFPlus::UI::feed_sdl_motion_event,
2218 CFPlus::SDL_MOUSEBUTTONDOWN => \&CFPlus::UI::feed_sdl_button_down_event,
2219 CFPlus::SDL_MOUSEBUTTONUP => \&CFPlus::UI::feed_sdl_button_up_event,
2220 CFPlus::SDL_USEREVENT => sub {
2221 if ($_[0]{code} == 1) {
2222 audio_channel_finished $_[0]{data1};
2223 } elsif ($_[0]{code} == 0) {
2224 audio_music_finished;
2225 }
2226 },
2227 );
2228
2229 #############################################################################
2230
2231 $SIG{INT} = $SIG{TERM} = sub { exit };
2232
2233 {
2234 CFPlus::read_cfg "$Crossfire::VARDIR/cfplusrc";
2235 CFPlus::DB::Server::run;
2236
2237 CFPlus::UI::set_layout ($::CFG->{layout});
2238
2239 my %DEF_CFG = (
2240 sdl_mode => 0,
2241 fullscreen => 0,
2242 fast => 0,
2243 force_opengl11 => undef,
2244 texture_compression => 1,
2245 map_scale => 1,
2246 fow_enable => 1,
2247 fow_intensity => 0,
2248 map_smoothing => 1,
2249 gui_fontsize => 1,
2250 log_fontsize => 0.7,
2251 gauge_fontsize => 1,
2252 gauge_size => 0.35,
2253 stat_fontsize => 0.7,
2254 mapsize => 100,
2255 audio_enable => 1,
2256 effects_enable => 1,
2257 effects_volume => 1,
2258 bgm_enable => 1,
2259 bgm_volume => 0.5,
2260 output_sync => 1,
2261 output_count => 1,
2262 output_rate => "",
2263 pickup => 0,
2264 inv_sort => "mtime",
2265 default => "profile", # default profile
2266 show_tips => 1,
2267 logview_max_par => 1000,
2268 );
2269
2270 while (my ($k, $v) = each %DEF_CFG) {
2271 $CFG->{$k} = $v unless exists $CFG->{$k};
2272 }
2273
2274 $CFG->{profile}{default}{host} ||= "crossfire.schmorp.de";
2275 $PROFILE = $CFG->{profile}{default};
2276
2277 # convert old bindings (only default profile matters)
2278 if (my $bindings = delete $PROFILE->{bindings}) {
2279 while (my ($mod, $syms) = each %$bindings) {
2280 while (my ($sym, $cmds) = each %$syms) {
2281 push @{ $PROFILE->{macro} }, {
2282 accelkey => [$mod*1, $sym*1],
2283 action => $cmds,
2284 };
2285 }
2286 }
2287 }
2288
2289 sdl_init;
2290
2291 @SDL_MODES = CFPlus::SDL_ListModes 8, 8;
2292 @SDL_MODES = CFPlus::SDL_ListModes 5, 0 unless @SDL_MODES;
2293 @SDL_MODES or CFPlus::fatal "Unable to find a usable video mode\n(hardware accelerated opengl fullscreen)";
2294
2295 @SDL_MODES = sort { $a->[0] * $a->[1] <=> $b->[0] * $b->[1] } @SDL_MODES;
2296
2297 $CFG->{sdl_mode} = 0 if $CFG->{sdl_mode} > @SDL_MODES;
2298
2299 {
2300 my @fonts = map CFPlus::find_rcfile "fonts/$_", qw(
2301 DejaVuSans.ttf
2302 DejaVuSansMono.ttf
2303 DejaVuSans-Bold.ttf
2304 DejaVuSansMono-Bold.ttf
2305 DejaVuSans-Oblique.ttf
2306 DejaVuSansMono-Oblique.ttf
2307 DejaVuSans-BoldOblique.ttf
2308 DejaVuSansMono-BoldOblique.ttf
2309 );
2310
2311 CFPlus::add_font $_ for @fonts;
2312
2313 CFPlus::pango_init;
2314
2315 $FONT_PROP = new_from_file CFPlus::Font $fonts[0];
2316 $FONT_FIXED = new_from_file CFPlus::Font $fonts[1];
2317
2318 $FONT_PROP->make_default;
2319 }
2320
2321 # compare mono (ft) vs. rgba (cairo)
2322 # ft - 1.8s, cairo 3s, even in alpha-only mode
2323 # for my $rgba (0..1) {
2324 # my $t1 = Time::HiRes::time;
2325 # for (1..1000) {
2326 # my $layout = CFPlus::Layout->new ($rgba);
2327 # $layout->set_text ("hallo" x 100);
2328 # $layout->render;
2329 # }
2330 # my $t2 = Time::HiRes::time;
2331 # warn $t2-$t1;
2332 # }
2333
2334 video_init;
2335 audio_init;
2336 }
2337
2338 show_tip_of_the_day if $CFG->{show_tips};
2339
2340 Event->idle (cb => sub {
2341 $_[0]->w->cancel;
2342 $startup_done->();
2343 });
2344
2345 Event::loop;
2346
2347 #video_shutdown;
2348 #audio_shutdown;
2349 CFPlus::SDL_Quit;
2350 CFPlus::DB::Server::stop;
2351
2352 =head1 NAME
2353
2354 cfplus - A Crossfire TRT and Crossfire game client
2355
2356 =head1 SYNOPSIS
2357
2358 Just run it - no commandline arguments are supported.
2359
2360 =head1 USAGE
2361
2362 cfplus utilises OpenGL for all UI elements and the game. It is supposed to
2363 be used in fullscreen mode and interactively.
2364
2365 =head1 DEBUGGING
2366
2367
2368 CFPLUS_DEBUG - environment variable
2369
2370 1 draw borders around widgets
2371 2 add low-level widget info to tooltips
2372 4 show fps
2373 8 suppress tooltips
2374
2375 =head1 AUTHOR
2376
2377 Marc Lehmann <crossfire@schmorp.de>, Robin Redeker <elmex@ta-sa.org>
2378
2379
2380