ViewVC Help
View File | Revision Log | Show Annotations | Download File
/cvs/deliantra/Deliantra-Client/bin/cfplus
Revision: 1.223
Committed: Tue Aug 28 01:23:47 2007 UTC (16 years, 8 months ago) by root
Branch: MAIN
Changes since 1.222: +22 -2 lines
Log Message:
- generalise keyboard handling a bit into a default
  keymapping + macro functions (such as "!completer shout").
- default keymappings ahve a lower priority, so its possible to
  (in theory) override them. a UI for this has not yet been created.
- re-enable NPOT texture usage, if available.

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