ViewVC Help
View File | Revision Log | Show Annotations | Download File
/cvs/deliantra/Deliantra-Client/DC/Main.pm
Revision: 1.7
Committed: Sat Dec 31 06:51:29 2011 UTC (12 years, 5 months ago) by root
Branch: MAIN
Changes since 1.6: +2 -1 lines
Log Message:
*** empty log message ***

File Contents

# User Rev Content
1 root 1.1 package DC::Main;
2    
3     # "dummy" package, which contains the real "main" program
4     # bin/deliantra is just a wrapper, to reduce the amount
5     # of code that needs to be in BEGIN clauses, and
6     # to make upgrades easier.
7    
8     package main;
9    
10     if ($ENV{DELIANTRA_CORO_DEBUG}) {
11     eval '
12     use Coro;
13     use Coro::EV;
14     use Coro::Debug;
15     our $debug = new_unix_server Coro::Debug "/tmp/dc";
16     ';
17     die if $@;
18     }
19    
20     use common::sense;
21     use Carp 'verbose';
22 root 1.3 use Cwd ();
23 root 1.1 use EV;
24     BEGIN { *time = \&EV::time }
25    
26     use List::Util qw(max min);
27    
28     use Deliantra;
29     use Deliantra::Protocol::Constants;
30    
31     use AnyEvent::Util ();
32     use AnyEvent::Socket ();
33     use AnyEvent::DNS ();
34    
35     use Compress::LZF;
36     use JSON::XS;
37    
38     use DC;
39    
40     sub crash($;$) {
41     # nop at compiletime
42     }
43    
44     BEGIN {
45     $SIG{__DIE__} = sub {
46 root 1.4 return if $^S; # quick reject
47    
48     # return if there are any eval contexts in the csall stack
49     for my $i (0..999) {
50     my ($sub, $is_require) = (caller $i)[3, 7]
51     or last;
52     return if $sub eq "(eval)" && !$is_require;
53     }
54    
55 root 1.1 crash "CRASH/DIE: $_[0]" => 1;
56     DC::fatal Carp::longmess "$_[0]";
57     }
58     }
59    
60     use DC::OpenGL ();
61     use DC::Protocol;
62     use DC::DB;
63     use DC::UI;
64     use DC::UI::Canvas;
65     use DC::UI::Inventory;
66     use DC::UI::SpellList;
67     use DC::UI::Dockable;
68     use DC::UI::Dockbar;
69     use DC::UI::ChatView;
70     use DC::MessageDistributor;
71     use DC::Pod;
72     use DC::MapWidget;
73     use DC::Macro;
74    
75     $SIG{QUIT} = sub { Carp::cluck "QUIT" };
76     $SIG{PIPE} = 'IGNORE';
77    
78     $EV::DIED = sub {
79     crash "CRASH/EV::DIED: $@" => 0;
80     DC::fatal Carp::longmess $@;
81     };
82    
83     # initialise the resolver before going fullscreen,
84     # as vista forces us back to the desktop
85     # when doing this later.
86     use AnyEvent::DNS;
87     AnyEvent::DNS::resolver ();
88    
89     our $MAX_FPS = 60;
90    
91     our $DEFAULT_SERVER = "gameserver.deliantra.net";
92    
93     our $META_SERVER = "http://metaserver.schmorp.de/current.json";
94    
95     our $LAST_REFRESH;
96     our $NOW;
97    
98     our $CFG;
99     our $PROFILE; # current profile
100     our $FAST; # fast, low-quality mode, possibly useful for software-rendering
101     our $DELIANTRA_DEBUG = $ENV{DELIANTRA_DEBUG} * 1;
102    
103     our $WANT_REFRESH;
104    
105     our $MODE_SLIDER;
106     our $CAVEAT_LABEL;
107    
108     our @SDL_MODES;
109     our $SDL_REINIT = 1;
110     our $WIDTH;
111     our $HEIGHT;
112     our $FULLSCREEN;
113     our $FONTSIZE;
114    
115     our $FONT_PROP;
116     our $FONT_FIXED;
117    
118     our $CONN;
119    
120     our $MAP;
121     our $MAPMAP;
122     our $MAPWIDGET;
123     our $COMPLETER;
124     our $MENUFRAME; # the rectangle at the top
125     our $MENUBAR; # the hbox at the top
126     our $MENUPOPUP;
127     our $BUTTONBAR; # the menu buttons
128     our $METASERVER;
129     our $LOGIN_BUTTON;
130     our $QUIT_DIALOG;
131     our $HOST_ENTRY;
132     our $FULLSCREEN_ENABLE;
133     our $PICKUP_ENABLE;
134     our $SERVER_INFO;
135    
136     our $SETUP_DIALOG;
137     our $SETUP_NOTEBOOK;
138     our $SETUP_SERVER;
139     our $SETUP_LOGIN;
140     our $SETUP_KEYBOARD;
141    
142     our $PL_NOTEBOOK;
143     our $PL_WINDOW;
144    
145     our $MUSIC_PLAYING_WIDGET;
146     our $LICENSE_WIDGET;
147 root 1.6 our $DOWNLOADS_WIDGET;
148 root 1.1
149     our $PICKUP_PAGE;
150     our $INVENTORY_PAGE;
151     our $STATS_PAGE;
152     our $SKILL_PAGE;
153     our $SPELL_PAGE;
154     our $SPELL_LIST;
155    
156     our $HELP_WINDOW;
157     our $MESSAGE_WINDOW;
158     our $MESSAGE_DIST;
159     our $FLOORBOX;
160     our $GAUGES;
161     our $STATWIDS;
162    
163     our $SDL_ACTIVE;
164     our @SDL_CB;
165    
166     our $ALT_ENTER_MESSAGE;
167     our $STATUSBOX;
168     our $MODBOX;
169     our $DEBUG_STATUS;
170    
171     our $INV;
172     our $INVR;
173     our $INVR_HB;
174    
175     #############################################################################
176    
177     # write a crash message blockingly to the socket, if possible
178     # this is a bit too complicated for my tastes, but it was easy.
179     *crash = sub($;$) {
180     my ($msg, $backtrace) = @_;
181    
182     warn $msg;
183    
184     return unless $CONN;
185    
186     my $fh = $CONN->{fh}
187     or return;
188    
189     my $buf = delete $CONN->{wbuf};
190    
191     $buf .= pack "n/a*", "exti " . JSON::XS::encode_json [clientlog => undef, substr $msg, 0, 8000];
192    
193     AnyEvent::Util::fh_nonblocking $fh, 0;
194     syswrite $fh, $buf;
195     AnyEvent::Util::fh_nonblocking $fh, 1;
196    
197     $msg =~ s/\s+$//;
198    
199     # backtrace as second step, in case it crashes, too
200     crash Carp::longmess "$msg\nbacktrace, for client version $DC::VERSION, generated"
201     if $backtrace;
202     };
203    
204     sub clienterror($;$) {
205     my ($msg, $backtrace) = @_;
206    
207     warn $msg;
208    
209     return unless $CONN;
210    
211     $CONN->send_exti_msg (clientlog => $msg);
212     $CONN->send_exti_msg (clientlog => Carp::longmess "$msg\nbacktrace, for client version $DC::VERSION, generated") if $backtrace;
213     }
214    
215     #############################################################################
216    
217     sub status {
218     $STATUSBOX->add (DC::asxml $_[0], pri => -10, group => "status", timeout => 10, fg => [1, 1, 0, 1]);
219     }
220    
221     sub debug {
222     $DEBUG_STATUS->set_text ($_[0]);
223     }
224    
225     sub message {
226     $MESSAGE_DIST->message (@_);
227     }
228    
229     sub update_modbox {
230     my $mod = DC::SDL_GetModState;
231    
232     my $markup;
233    
234     $markup .= $mod & DC::KMOD_CTRL
235     ? ($MAPWIDGET->{ctrl} ? "[REPEAT]" : "[<span foreground='#888'>REPEAT</span>]")
236     : "[<span foreground='#888'> once </span>]";
237    
238     $markup .= $mod & DC::KMOD_SHIFT
239     ? ($MAPWIDGET->{shft} ? "[FIRE]" : "[<span foreground='#888'>FIRE</span>]")
240     : "[<span foreground='#888'>move</span>]";
241    
242     $markup .= $mod & (DC::KMOD_ALT | DC::KMOD_META)
243     ? "[ALT]"
244     : "[<span foreground='#888'>alt</span>]";
245    
246     $markup .= $mod & DC::KMOD_NUM
247     ? "[NUM]"
248     : "[<span foreground='#888'>num</span>]";
249    
250     # <tt> around next statement works around some bug that keeps the
251     # "font =>" from being used on windows
252     $MODBOX->set_markup ("<tt>$markup</tt>");
253     }
254    
255     #############################################################################
256     #TODO: maybe move into own audio module...
257    
258     our $SDL_MIXER;
259    
260     our $MUSIC_DEFAULT = "in_a_heartbeat.ogg";
261     our $MUSIC_WANT; # arryref of ambient music we want to play
262     our @MUSIC_HAVE; # ambient music we have on disk
263     our $MUSIC_START;
264     our @MUSIC_JINGLE; # which jingles to play next
265     our $MUSIC_PLAYING_DATA;
266     our $MUSIC_PLAYING_META;
267     our $MUSIC_PLAYER;
268     our $MUSIC_RESUME = 30; # resume music when played less than these many seconds before
269    
270     our %AUDIO_CHUNK; # audio "files"
271     our %AUDIO_PLAY; # which audio faces should be played
272    
273     sub audio_channel_finished {
274     my ($channel) = @_;
275    
276     # warn "channel $channel finished\n";#d#
277     }
278    
279     sub audio_sound_push($) {
280     my ($face) = @_;
281    
282     $CFG->{effects_enable}
283     or return;
284    
285     $AUDIO_PLAY{$face}
286     or return;
287    
288     if (my $chunk = $AUDIO_CHUNK{$face}) {
289     for (grep $_->[0] >= EV::now, @{(delete $AUDIO_PLAY{$face}) || []}) {
290     my (undef, $dx, $dy, $vol) = @$_;
291    
292     my $channel = DC::Channel::find;
293     $channel->volume ($vol * $CFG->{effects_volume} * 128 / 255);
294     $channel->set_position_r ($dx, $dy, 20);
295     $chunk->play ($channel);
296     }
297     } else {
298     # sound_meta not set means data is in flight either way
299     my $meta = $CONN->{face}[$face]
300     or return;
301    
302     $meta->{data}
303     or return;
304    
305     # if it's a jingle, play it as ambient music
306     if ($meta->{data}{jingle}) {
307     if (delete $AUDIO_PLAY{$face}) { # take the jingle out of the sound queue
308     push @MUSIC_JINGLE, $meta; # push it unto the music/jingle queue
309     &audio_music_push ($face);
310     }
311     } else {
312     # fetch from database
313     DC::DB::get res_data => $meta->{name}, sub {
314     my $rwops = new DC::RW $_[0];
315     my $chunk = new DC::MixChunk $rwops
316     or Carp::confess "sound face " . (JSON::XS::encode_json $meta) . " (" . (unpack "H64", $_[0]) . ") unloadable: " . DC::Mix_GetError;
317     $chunk->volume (($meta->{data}{volume} || 1) * 128);
318     $AUDIO_CHUNK{$face} = $chunk;
319    
320     audio_sound_push ($face);
321     };
322     }
323     }
324     }
325    
326     sub audio_sound_play {
327     my ($face, $dx, $dy, $vol) = @_;
328    
329     $SDL_MIXER
330     or return;
331     $CFG->{effects_enable}
332     or return;
333    
334     my $queue = $AUDIO_PLAY{$face} ||= [];
335     push @$queue, [EV::now + 0.6, $dx, $dy, $vol]; # do not play sound for outdated events
336     audio_sound_push $face
337     unless @$queue > 1;
338     }
339    
340     sub audio_music_set_meta {
341     my ($meta) = @_;
342    
343     $MUSIC_PLAYING_META = $meta;
344     $MUSIC_PLAYING_WIDGET->set_markup (
345     "<b>Name</b>: " . (DC::asxml $meta->{data}{name}) . "\n"
346     . "<b>Author</b>: " . (DC::asxml $meta->{data}{author}) . "\n"
347     . "<b>Source</b>: " . (DC::asxml $meta->{data}{source}) . "\n"
348     . "<b>License</b>: " . (DC::asxml $meta->{data}{license})
349     );
350     }
351    
352     sub audio_music_update_volume {
353     return unless $MUSIC_PLAYING_META;
354     my $volume = $MUSIC_PLAYING_META->{data}{volume} || 1;
355     my $base = $MUSIC_PLAYING_META->{data}{jingle} ? 1 : $CFG->{bgm_volume};
356     DC::MixMusic::volume $base * $volume * 128;
357     }
358    
359     sub audio_music_start {
360     my $meta = $MUSIC_PLAYING_META;
361    
362     DC::DB::get res_data => $meta->{name}, sub {
363     return unless $SDL_MIXER;
364    
365     # music might have changed...
366     $meta eq $MUSIC_PLAYING_META
367     or return &audio_music_start ();
368    
369     audio_music_update_volume;
370    
371     $MUSIC_PLAYING_DATA = \$_[0];
372    
373     $meta->{path} or length $_[0]
374     or return clienterror "empty music face from res_data ($meta->{face})";#d#
375    
376     my $rwops = $meta->{path}
377     ? (new_from_file DC::RW $meta->{path} or return clienterror "unable to load music face $meta->{path}: $!")#d#clienterror
378     : new DC::RW $$MUSIC_PLAYING_DATA;
379    
380     $MUSIC_PLAYER = new DC::MixMusic $rwops
381     or return clienterror "music face $meta->{face} unloadable: " . DC::Mix_GetError => 1;
382    
383     my $NOW = time;
384    
385     if ($MUSIC_PLAYING_META->{stop_time} > $NOW - $MUSIC_RESUME) {
386     my $pos = $MUSIC_PLAYING_META->{stop_pos};
387     $MUSIC_PLAYER->fade_in_pos (0, 700, $pos);
388     $MUSIC_START = time - $pos;
389     } else {
390     $MUSIC_PLAYER->play (0);
391     $MUSIC_START = time;
392     }
393    
394     delete $meta->{stop_time};
395     delete $meta->{stop_pos};
396     }
397     }
398    
399     sub audio_music_push {
400     return unless $SDL_MIXER;
401    
402     my $fade_out;
403    
404     if (@MUSIC_JINGLE) {
405     $fade_out = 333;
406     @MUSIC_HAVE = $MUSIC_JINGLE[0];
407    
408     } else {
409     return unless $CFG->{bgm_enable};
410    
411     $fade_out = 700;
412    
413     @MUSIC_HAVE =
414     grep $_ && $_->{data},
415     map $CONN->{face}[$_],
416     @$MUSIC_WANT;
417    
418     # randomize music a bit so that the order is not always the same
419     $_->{stop_time} ||= rand for @MUSIC_HAVE;
420    
421     # default MUSIC_HAVE == MUSIC_DEFAULT
422     @MUSIC_HAVE = { path => DC::find_rcfile "music/$MUSIC_DEFAULT" }
423     unless @MUSIC_HAVE;
424     }
425    
426     # if the currently playing song is acceptable, let it continue
427     return if grep $MUSIC_PLAYING_META == $_, @MUSIC_HAVE;
428    
429     my $NOW = time;
430    
431     if ($MUSIC_PLAYING_META) {
432     $MUSIC_PLAYING_META->{stop_time} = $NOW;
433     $MUSIC_PLAYING_META->{stop_pos} = $NOW - $MUSIC_START;
434     DC::MixMusic::fade_out $fade_out;
435     } else {
436     # sort by stop time, oldest first
437     @MUSIC_HAVE = sort { $a->{stop_time} <=> $b->{stop_time} } @MUSIC_HAVE;
438    
439     # if the most recently-played piece played very recently,
440     # resume it, else choose the oldest piece for rotation.
441     audio_music_set_meta
442     $MUSIC_HAVE[-1]{stop_pos} && $MUSIC_HAVE[-1]{stop_time} > $NOW - $MUSIC_RESUME
443     ? $MUSIC_HAVE[-1]
444     : $MUSIC_HAVE[0];
445    
446     audio_music_start;
447     }
448     }
449    
450     sub audio_music_set_ambient {
451     my ($songs) = @_;
452    
453     $MUSIC_WANT = $songs;
454     audio_music_push;
455     }
456    
457     sub audio_music_finished {
458     if ($MUSIC_PLAYING_META) {
459     $MUSIC_PLAYING_META->{stop_time} = time;
460     }
461    
462     # we compress multiple jingles of the same type
463     shift @MUSIC_JINGLE
464     while @MUSIC_JINGLE && $MUSIC_PLAYING_META == $MUSIC_JINGLE[0];
465    
466     $MUSIC_PLAYING_WIDGET->clear;
467    
468     undef $MUSIC_PLAYER;
469     undef $MUSIC_PLAYING_META;
470     undef $MUSIC_PLAYING_DATA;
471    
472     audio_music_push;
473     }
474    
475     sub audio_init {
476     if ($CFG->{audio_enable}) {
477     if (length $CFG->{audio_driver}) {
478     local $ENV{SDL_AUDIODRIVER} = $CFG->{audio_driver};
479     DC::SDL_Init DC::SDL_INIT_AUDIO
480     and die "SDL::Init failed!\n";
481     } else {
482     DC::SDL_Init DC::SDL_INIT_AUDIO
483     and die "SDL::Init failed!\n";
484     }
485    
486     $ENV{MIX_EFFECTSMAXSPEED} = 1;
487     $SDL_MIXER = !DC::Mix_OpenAudio
488     $CFG->{audio_hw_frequency},
489     DC::MIX_DEFAULT_FORMAT,
490     $CFG->{audio_hw_channels},
491     $CFG->{audio_hw_chunksize};
492    
493     if ($SDL_MIXER) {
494     DC::Mix_AllocateChannels $CFG->{audio_mix_channels};
495    
496     audio_music_finished;
497     } else {
498     status "Unable to open sound device: there will be no sound";
499     }
500     } else {
501     undef $SDL_MIXER;
502     }
503    
504     sub audio_tab_update;
505     audio_tab_update;
506     }
507    
508     sub audio_shutdown {
509     if ($SDL_MIXER) {
510     DC::MixMusic::halt;
511     DC::Mix_AllocateChannels 0;
512     }
513    
514     undef $MUSIC_PLAYER;
515     undef $MUSIC_PLAYING_META;
516     undef $MUSIC_PLAYING_DATA;
517    
518     $MUSIC_WANT = [];
519     @MUSIC_JINGLE = ();
520     %AUDIO_PLAY = ();
521     %AUDIO_CHUNK = ();
522    
523     DC::Mix_CloseAudio if $SDL_MIXER;
524     undef $SDL_MIXER;
525    
526     DC::SDL_QuitSubSystem DC::SDL_INIT_AUDIO;
527     }
528    
529     #############################################################################
530    
531     sub destroy_query_dialog {
532     (delete $_[0]{query_dialog})->destroy
533     if $_[0]{query_dialog};
534     }
535    
536     # FIXME: a very ugly hack to wait for stat update look below! #d#
537     our $QUERY_TIMER; #d#
538    
539     # server query dialog
540     sub server_query {
541     my ($conn, $flags, $prompt) = @_;
542    
543     # FIXME: a very ugly hack to wait for stat update #d#
544     if ($prompt =~ /roll new stats/ and not $conn->{stat_change_with}) {
545     unless ($QUERY_TIMER) {
546     $QUERY_TIMER = EV::timer 1, 0, sub {
547     server_query ($conn, $flags, $prompt, 1);
548     $QUERY_TIMER = undef
549     };
550    
551     return;
552     }
553     }
554    
555     $conn->{query_dialog} = my $dialog = new DC::UI::Toplevel
556     x => "center",
557     y => "center",
558     title => "Server Query",
559     child => my $vbox = new DC::UI::VBox,
560     ;
561    
562     my @dialog = my $label = new DC::UI::Label
563     max_w => $::WIDTH * 0.8,
564     ellipsise => 0,
565     text => $prompt;
566    
567     if ($flags & CS_QUERY_YESNO) {
568     push @dialog, my $hbox = new DC::UI::HBox;
569    
570     $hbox->add (new DC::UI::Button
571     text => "No",
572     on_activate => sub {
573     $conn->send ("reply n");
574     $dialog->destroy;
575     0
576     }
577     );
578     $hbox->add (new DC::UI::Button
579     text => "Yes",
580     on_activate => sub {
581     $conn->send ("reply y");
582     destroy_query_dialog $conn;
583     0
584     },
585     );
586    
587     $dialog->grab_focus;
588    
589     } elsif ($flags & CS_QUERY_SINGLECHAR) {
590     if ($prompt =~ /Now choose a character|Press any key for the next race/i) {
591     $dialog->{tooltip} = "#charcreation_focus";
592    
593     unshift @dialog, new DC::UI::Label
594     max_w => $::WIDTH * 0.8,
595     ellipsise => 0,
596     markup => "\nOr use your keyboard and the text entry below:\n";
597    
598     unshift @dialog, my $table = new DC::UI::Table;
599    
600     $table->add_at (0, 0, new DC::UI::Button
601     text => "Next Race",
602     on_activate => sub {
603     $conn->send ("reply n");
604     destroy_query_dialog $conn;
605     0
606     },
607     );
608     $table->add_at (2, 0, new DC::UI::Button
609     text => "Accept",
610     on_activate => sub {
611     $conn->send ("reply d");
612     destroy_query_dialog $conn;
613     0
614     },
615     );
616    
617     if ($conn->{chargen_race_description}) {
618     unshift @dialog, new DC::UI::Label
619     max_w => $::WIDTH * 0.8,
620     ellipsise => 0,
621     markup => "<span foreground='#ccccff'>$conn->{chargen_race_description}</span>",
622     ;
623     }
624    
625     unshift @dialog, new DC::UI::Face
626     face => $conn->{player}{face},
627     bg => [.2, .2, .2, 1],
628     min_w => 64,
629     min_h => 64,
630     ;
631    
632     if ($conn->{chargen_race_title}) {
633     unshift @dialog, new DC::UI::Label
634     allign => 1,
635     ellipsise => 0,
636     markup => "<span foreground='#ccccff' size='large'>Race: $conn->{chargen_race_title}</span>",
637     ;
638     }
639    
640     unshift @dialog, new DC::UI::Label
641     max_w => $::WIDTH * 0.4,
642     ellipsise => 0,
643     markup => (DC::Pod::section_label ui => "chargen_race"),
644     ;
645    
646     } elsif ($prompt =~ /roll new stats/) {
647     if (my $stat = delete $conn->{stat_change_with}) {
648     $conn->send ("reply $stat");
649     destroy_query_dialog $conn;
650     return;
651     }
652    
653     unshift @dialog, new DC::UI::Label
654     max_w => $::WIDTH * 0.4,
655     ellipsise => 0,
656     markup => "\nOr use your keyboard and the text entry below:\n";
657    
658     unshift @dialog, my $table = new DC::UI::Table;
659    
660     # left: re-roll
661     $table->add_at (0, 0, new DC::UI::Button
662     text => "Roll Again",
663     on_activate => sub {
664     $conn->send ("reply y");
665     destroy_query_dialog $conn;
666     0
667     },
668     );
669    
670     # center: swap stats
671     my ($sw1, $sw2) = map +(new DC::UI::Selector
672     expand => 1,
673     value => $_,
674     options => [
675     [1 => "Str", "Strength ($conn->{stat}{+CS_STAT_STR})"],
676     [2 => "Dex", "Dexterity ($conn->{stat}{+CS_STAT_DEX})"],
677     [3 => "Con", "Constitution ($conn->{stat}{+CS_STAT_CON})"],
678     [4 => "Int", "Intelligence ($conn->{stat}{+CS_STAT_INT})"],
679     [5 => "Wis", "Wisdom ($conn->{stat}{+CS_STAT_WIS})"],
680     [6 => "Pow", "Power ($conn->{stat}{+CS_STAT_POW})"],
681     [7 => "Cha", "Charisma ($conn->{stat}{+CS_STAT_CHA})"],
682     ],
683     ), 1 .. 2;
684    
685     $table->add_at (2, 0, new DC::UI::Button
686     text => "Swap Stats",
687     on_activate => sub {
688     $conn->{stat_change_with} = $sw2->{value};
689     $conn->send ("reply $sw1->{value}");
690     destroy_query_dialog $conn;
691     0
692     },
693     );
694     $table->add_at (2, 1, new DC::UI::HBox children => [$sw1, $sw2]);
695    
696     # right: accept
697     $table->add_at (4, 0, new DC::UI::Button
698     text => "Accept",
699     on_activate => sub {
700     $conn->send ("reply n");
701     destroy_query_dialog $conn;
702     0
703     },
704     );
705    
706     unshift @dialog, my $hbox = new DC::UI::HBox;
707     for (
708     [Str => CS_STAT_STR],
709     [Dex => CS_STAT_DEX],
710     [Con => CS_STAT_CON],
711     [Int => CS_STAT_INT],
712     [Wis => CS_STAT_WIS],
713     [Pow => CS_STAT_POW],
714     [Cha => CS_STAT_CHA],
715     ) {
716     my ($name, $id) = @$_;
717     $hbox->add (new DC::UI::Label
718     markup => "$conn->{stat}{$id} <span foreground='yellow'>$name</span>",
719     expand => 1,
720     can_events => 1,
721     can_hover => 1,
722     tooltip => "#stat_$name",
723     );
724     }
725    
726     unshift @dialog, new DC::UI::Label
727     max_w => $::WIDTH * 0.4,
728     ellipsise => 0,
729     markup => (DC::Pod::section_label ui => "chargen_stats"),
730     ;
731     }
732    
733     push @dialog, my $entry = new DC::UI::Entry
734     on_changed => sub {
735     $conn->send ("reply $_[1]");
736     destroy_query_dialog $conn;
737     0
738     },
739     ;
740    
741     $entry->grab_focus;
742    
743     } else {
744     $dialog->{tooltip} = "Enter the reply and press return (click on the entry to make sure it has keyboard focus)";
745    
746     push @dialog, my $entry = new DC::UI::Entry
747     $flags & CS_QUERY_HIDEINPUT ? (hidden => "*") : (),
748     on_activate => sub {
749     $conn->send ("reply $_[1]");
750     destroy_query_dialog $conn;
751     0
752     },
753     ;
754    
755     $entry->grab_focus;
756     }
757    
758     $vbox->add (@dialog);
759     $dialog->show;
760     }
761    
762     sub dc_connect {
763     my ($host, $port) = @_;
764    
765     my $mapw = List::Util::min 48, List::Util::max 11, int 1.5 + $WIDTH * $CFG->{mapsize} * 0.01 / 32;
766     my $maph = List::Util::min 48, List::Util::max 11, int 1.5 + $HEIGHT * $CFG->{mapsize} * 0.01 / 32;
767    
768     $CONN =
769     new DC::Protocol
770     host => $host,
771     port => $port,
772     user => $PROFILE->{user},
773     pass => $PROFILE->{password},
774     mapw => $mapw,
775     maph => $maph,
776    
777     c_version => {
778     client => "deliantra",
779     clientver => $DC::VERSION,
780     gl_vendor => DC::OpenGL::gl_vendor,
781     gl_version => DC::OpenGL::gl_version,
782     },
783    
784     map_widget => $MAPWIDGET,
785     statusbox => $STATUSBOX,
786     map => $MAP,
787     mapmap => $MAPMAP,
788     query => \&server_query,
789    
790     setup_req => {
791     smoothing => $CFG->{map_smoothing}*1,
792     },
793    
794     on_connect => sub {
795     if ($_[0]) {
796     DC::lowdelay fileno $CONN->{fh};
797    
798     status "successfully connected to the server";
799     } else {
800     undef $CONN;
801     status "unable to connect: $!";
802     stop_game();
803     }
804     },
805     ;
806     }
807    
808     sub start_game {
809     status "logging in...";
810    
811     my $server = $PROFILE->{host} || $DEFAULT_SERVER;
812     my ($host, $port) = AnyEvent::Socket::parse_hostport $server, "deliantra=13327"
813     or return status "$server: unable to parse server address, try an empty field.";
814    
815     $LOGIN_BUTTON->set_text ("Logout");
816     $SETUP_DIALOG->hide;
817    
818     $MAP = new DC::Map;
819    
820     # hack to make SURE we find the IP address all right
821     # can be removed once AnyEvent::DNS is proven stable.
822     if ($host eq "gameserver.deliantra.net") {
823     AnyEvent::DNS::a "dnstest.deliantra.net", sub {
824     if ($_[0] ne "80.101.114.108") { # P-e-r-l
825     status "dns failure, trying differently";
826     $host = eval { Socket::inet_ntoa Socket::inet_aton "gameserver.deliantra.net" };
827     unless (defined $host) {
828     status "dns failure, using hardcoded address";
829     $host = "194.126.175.154";
830     }
831     }
832    
833     dc_connect $host, $port;
834     };
835     } else {
836     dc_connect $host, $port;
837     }
838     }
839    
840     sub stop_game {
841     crash "stop_game";
842    
843     $LOGIN_BUTTON->set_text ("Login / Register");
844     $SETUP_NOTEBOOK->set_current_page ($SETUP_LOGIN);
845     $SETUP_DIALOG->show;
846     $PL_WINDOW->hide;
847     $SPELL_LIST->clear_spells;
848     $DC::UI::ROOT->emit (stop_game => ! ! $CONN);
849    
850     &audio_music_set_ambient ([]);
851    
852     return unless $CONN;
853    
854     status "connection closed";
855    
856     destroy_query_dialog $CONN;
857     $CONN->destroy;
858     $CONN = 0; # false, does not autovivify
859    
860     undef $MAP;
861     }
862    
863     sub graphics_setup {
864     my $vbox = new DC::UI::VBox;
865    
866     {
867     $vbox->add (my $frame = new DC::UI::FancyFrame expand => 1, label => "Video Mode");
868    
869     $frame->add (my $table = new DC::UI::Table expand => 1, col_expand => [0, 1]);
870    
871     my $row = 0;
872    
873     $table->add_at (0, $row, new DC::UI::Label align => 1, text => "OpenGL Info");
874     $table->add_at (1, $row++, new DC::UI::Label fontsize => 0.8, text => DC::OpenGL::gl_vendor . ", " . DC::OpenGL::gl_version,
875     can_events => 1,
876     tooltip => "<tt><span size='8192'>" . (DC::OpenGL::gl_extensions) . "</span></tt>");
877    
878     $table->add_at (0, $row, new DC::UI::Label align => 1, text => "Caveats");
879     $table->add_at (1, $row++, $CAVEAT_LABEL = new DC::UI::Label fontsize => 0.8,
880     can_events => 1,
881     tooltip => "This field shows any known issues with your config or driver, such as "
882     . "a non-accelerated display format. You can try to work around these issues "
883     . "by selecting a different video mode, changing the settings below or "
884     . "by installing the right driver for your graphics card.");
885    
886     $table->add_at (0, $row, new DC::UI::Label align => 1, text => "UI Theme");
887     $table->add_at (1, $row++, $FULLSCREEN_ENABLE = new DC::UI::Selector
888     value => $CFG->{uitheme},
889     options => [
890     [wood => "Wood (the default)"],
891     [plain => "Plain (very)"],
892     [blue => "Blue (dark)"],
893     [metal => "Metal (light)"],
894     ],
895     tooltip => "Choose the User Interface theme that you like most :)",
896     on_changed => sub { my ($self, $value) = @_; $CFG->{uitheme} = $value; 0 }
897     );
898    
899     my $vidmode_tooltip =
900     "<b>Video Mode.</b> The video mode to use for fullscreen (and the window size for windowed operation). "
901     . "The format is <i>width</i> x <i>height</i> \@ <i>depth-per-channel</i> + <i>alpha-channel</i>.";
902    
903     $table->add_at (0, $row, new DC::UI::Label align => 1, text => "Video Mode");
904     $table->add_at (1, $row++, my $hbox = new DC::UI::HBox);
905    
906     $hbox->add ($MODE_SLIDER = new DC::UI::Slider
907     c_rescale => 1,
908     force_w => $WIDTH * 0.1, expand => 1,
909     range => [ ($CFG->{sdl_mode}) x 3 ],
910     tooltip => $vidmode_tooltip);
911     $hbox->add (my $mode_label = new DC::UI::Label
912     height => 0.8, template => "9999x9999@9+9",
913     can_events => 1, tooltip => $vidmode_tooltip);
914    
915     $MODE_SLIDER->connect (changed => sub {
916     my ($self, $value) = @_;
917    
918     $CFG->{sdl_mode} = $self->{range}[0] = $value = int $value;
919     $mode_label->set_text (sprintf '%dx%d@%d+%d', @{$SDL_MODES[$value]});
920     });
921     $MODE_SLIDER->emit (changed => $MODE_SLIDER->{range}[0]);
922    
923     $table->add_at (0, $row, new DC::UI::Label align => 1, text => "Fullscreen");
924     $table->add_at (1, $row++, $FULLSCREEN_ENABLE = new DC::UI::CheckBox
925     state => $CFG->{fullscreen},
926     tooltip => "Bring the client into fullscreen mode.",
927     on_changed => sub { my ($self, $value) = @_; $CFG->{fullscreen} = $value; 0 }
928     );
929    
930     $table->add_at (0, $row, new DC::UI::Label align => 1, text => "Force OpenGL 1.1");
931     $table->add_at (1, $row++, new DC::UI::CheckBox
932     state => $CFG->{force_opengl11},
933     tooltip => "Limit Deliantra to use OpenGL 1.1 features only. This will normally result in "
934     . "higher memory usage and slower performance. It will, however, help tremendously on "
935     . "cards that claim to support a feature but fall back to software rendering. "
936     . "Nvidia Geforce FX cards are known to claim features the hardware doesn't support, "
937     . "but cards and drivers from other vendors (ATI) are often just as bad. "
938     . "<b>If you experience extremely low framerates and your card should do better, try this option.</b>",
939     on_changed => sub { my ($self, $value) = @_; $CFG->{force_opengl11} = $value; 0 }
940     );
941    
942     $table->add_at (0, $row, new DC::UI::Label align => 1, text => "Forbid Alpha");
943     $table->add_at (1, $row++, new DC::UI::CheckBox
944     state => $CFG->{disable_alpha},
945     tooltip => "Forbid the use of the alpha channel. This makes Deliantra look a lot worse "
946     . "by disabling a number of textures and transparency effects. Normally, these "
947     . "effects do not cost a lot of resources, but some graphics cards might fall "
948     . "back to extremely slow rendering if this is enabled. If disabling this option "
949     . "noticably improves the framerate of the client please report this! "
950     . "<b>If you experience extremely low framerates and your card should do better, try this option.</b>",
951     on_changed => sub {
952     my ($self, $value) = @_;
953     $CFG->{disable_alpha} = $value;
954     $SDL_REINIT = 1; # SDL_SetVideoMode ignores GL attr changes
955     0
956     }
957     );
958    
959     $table->add_at (0, $row, new DC::UI::Label align => 1, text => "Compress Textures");
960     $table->add_at (1, $row++, new DC::UI::CheckBox
961     state => $CFG->{texture_compression},
962     tooltip => "Use texture compression. Normally this will not reduce visual quality noticable but "
963     . "will save a lot of memory and increase performance (and also fall prey to the ever-buggy Mac OS X software renderer). "
964     . "The compression algorithm can differ form card to card, so your mileage may vary. This setting is ignored in "
965     . "forced OpenGL 1.1 mode and when using the Apple renderer.",
966     on_changed => sub { my ($self, $value) = @_; $CFG->{texture_compression} = $value; 0 }
967     );
968    
969     $table->add_at (0, $row, new DC::UI::Label align => 1, text => "Fast & Ugly");
970     $table->add_at (1, $row++, new DC::UI::CheckBox
971     state => $CFG->{fast},
972     tooltip => "Lower the visual quality considerably to speed up rendering.",
973     on_changed => sub { my ($self, $value) = @_; $CFG->{fast} = $value; 0 }
974     );
975    
976     $table->add_at (0, $row, new DC::UI::Label align => 1, text => "GUI Fontsize");
977     $table->add_at (1, $row++, new DC::UI::Slider
978     range => [$CFG->{gui_fontsize}, 0.5, 2, 0, 0.1],
979     tooltip => "The base font size used by most GUI elements that do not have their own setting.",
980     on_changed => sub { $CFG->{gui_fontsize} = $_[1]; 0 },
981     );
982    
983     $table->add_at (1, $row++, new DC::UI::Button
984     expand => 1, text => "Apply",
985     tooltip => "Apply the video settings above.",
986     on_activate => sub {
987     video_shutdown ();
988     video_init ();
989     0
990     }
991     );
992     }
993    
994     {
995     $vbox->add (my $frame = new DC::UI::FancyFrame expand => 1, label => "Other Settings");
996    
997     $frame->add (my $table = new DC::UI::Table expand => 1, col_expand => [0, 1]);
998    
999     my $row = 0;
1000     $table->add_at (0, $row, new DC::UI::Label align => 1, text => "Smooth Movement");
1001     $table->add_at (1, $row++, new DC::UI::CheckBox
1002     state => $CFG->{smooth_movement},
1003     tooltip => "<b>Smooth Movement</b> tries to make movement, well, smoother, but also increases the framerate. "
1004     . "If you have a very slow system, non-accelerated drivers or plain dislike smooth scrolling, "
1005     . "then disable this option. Changes take effect immdiately.",
1006     on_changed => sub { my ($self, $value) = @_; $CFG->{smooth_movement} = $value; 0 }
1007     );
1008    
1009     $table->add_at (0, $row, new DC::UI::Label align => 1, text => "Smooth Transitions");
1010     $table->add_at (1, $row++, new DC::UI::CheckBox
1011     state => $CFG->{smooth_transitions},
1012     tooltip => "<b>Smooth Transitions</b> tries to blend the fog of war and lighting smoothly between updates. "
1013     . "If you have a very slow system, non-accelerated drivers or plain dislike smooth scrolling, "
1014     . "then disable this option. Requires Smooth Movement and OpenGL Multitexturing. Changes take effect immdiately.",
1015     on_changed => sub { my ($self, $value) = @_; $CFG->{smooth_transitions} = $value; 0 }
1016     );
1017    
1018    
1019     $table->add_at (0, $row, new DC::UI::Label align => 1, text => "Map Scale");
1020     $table->add_at (1, $row++, new DC::UI::Slider
1021     range => [(log $CFG->{map_scale}) / (log 2), -3, 1, 0, 1],
1022     tooltip => "Enlarge or shrink the displayed map. Changes are instant.",
1023     on_changed => sub { my ($self, $value) = @_; $CFG->{map_scale} = 2 ** $value; 0 }
1024     );
1025    
1026     $table->add_at (0, $row, new DC::UI::Label align => 1, text => "Map Smoothing");
1027     $table->add_at (1, $row++, new DC::UI::CheckBox
1028     state => $CFG->{map_smoothing},
1029     tooltip => "<b>Map Smoothing</b> tries to make tile borders less square. "
1030     . "This increases load on the graphics subsystem and works only with TRT servers. "
1031     . "Changes take effect at next login only.",
1032     on_changed => sub { my ($self, $value) = @_; $CFG->{map_smoothing} = $value; 0 }
1033     );
1034    
1035     $table->add_at (0, $row, new DC::UI::Label align => 1, text => "Fog of War");
1036     $table->add_at (1, $row++, new DC::UI::CheckBox
1037     state => $CFG->{fow_enable},
1038     tooltip => "<b>Fog-of-War</b> marks areas that cannot be seen by the player. Changes are instant.",
1039     on_changed => sub { my ($self, $value) = @_; $CFG->{fow_enable} = $value; 0 }
1040     );
1041    
1042     $table->add_at (0, $row, new DC::UI::Label align => 1, text => "FoW Pattern");
1043     $table->add_at (1, $row++, new DC::UI::ImageButton
1044     tex => $DC::MapWidget::TEX_HIDDEN[$CFG->{fow_texture}],
1045     bg => [0.3, 0.3, 0.2],
1046     force_w => 64,
1047     force_h => 64,
1048     tooltip => "<b>Fog of War Pattern.</b> The pattern that is overlaid over areas hidden from view. Click to cycle through various alternatives. Changes are instant.",
1049     on_activate => sub {
1050     my ($self) = @_;
1051     $CFG->{fow_texture} = ($CFG->{fow_texture} + 1) % @DC::MapWidget::TEX_HIDDEN;
1052     $self->set_texture ($DC::MapWidget::TEX_HIDDEN[$CFG->{fow_texture}]);
1053     $MAPWIDGET->update;
1054     }
1055     );
1056    
1057     $table->add_at (0, $row, new DC::UI::Label align => 1, text => "FoW Intensity");
1058     $table->add_at (1, $row++, new DC::UI::Slider
1059     range => [$CFG->{fow_intensity}, 0, 1, 0, 1 / 256],
1060     tooltip => "<b>Fog of War Lightness.</b> The higher the intensity, the lighter the Fog-of-War color. Changes are instant.",
1061     on_changed => sub { my ($self, $value) = @_; $CFG->{fow_intensity} = $value; 0 }
1062     );
1063    
1064     $table->add_at (0, $row, new DC::UI::Label align => 1, text => "Message Fontsize");
1065     $table->add_at (1, $row++, new DC::UI::Slider
1066     range => [$CFG->{log_fontsize}, 0.5, 2, 0, 0.1],
1067     tooltip => "The font size used by the <b>message/server log</b> window only. Changes are instant, "
1068     . "but you still need to press apply to correctly re-layout the widget.",
1069     on_changed => sub { $MESSAGE_DIST->set_fontsize ($CFG->{log_fontsize} = $_[1]); 0 },
1070     );
1071    
1072     $table->add_at (0, $row, new DC::UI::Label align => 1, text => "Gauge fontsize");
1073     $table->add_at (1, $row++, new DC::UI::Slider
1074     range => [$CFG->{gauge_fontsize}, 0.5, 2, 0, 0.1],
1075     tooltip => "Adjusts the fontsize of the gauges at the bottom right. Changes are instant.",
1076     on_changed => sub {
1077     $CFG->{gauge_fontsize} = $_[1];
1078     &set_gauge_window_fontsize;
1079     0
1080     }
1081     );
1082    
1083     $table->add_at (0, $row, new DC::UI::Label align => 1, text => "Gauge size");
1084     $table->add_at (1, $row++, new DC::UI::Slider
1085     range => [$CFG->{gauge_size}, 0.2, 0.8],
1086     tooltip => "Adjust the size of the stats gauges at the bottom right. Changes are instant.",
1087     on_changed => sub {
1088     $CFG->{gauge_size} = $_[1];
1089     $GAUGES->{win}->set_size ($WIDTH, int $HEIGHT * $CFG->{gauge_size});
1090     0
1091     }
1092     );
1093     }
1094    
1095     $vbox
1096     }
1097    
1098     our $AUDIO_HW_CHUNKSIZE;
1099     our $AUDIO_INFO;
1100    
1101     sub audio_tab_update {
1102     my ($freq, $format, $chans) = DC::Mix_QuerySpec;
1103    
1104     $AUDIO_HW_CHUNKSIZE->set_options ([
1105     [0, "default", "Use System Default"],
1106     map {
1107     my $ms = sprintf "%dms", 1000 * $_ / ($CFG->{audio_hw_frequency} || 22050);
1108     [$_, $ms, "$ms ($_ samples)"],
1109     } 256, 512, 1024, 2048, 4096, 8192, 16384, 32768
1110     ]);
1111    
1112     my $text = !$freq
1113     ? "audio is off"
1114     : "audio is enabled\n"
1115     . "driver: " . DC::SDL_AudioDriverName . "\n"
1116     . "frequency (Hz): $freq\n"
1117     . "channels: $chans\n"
1118     . "chunk decoders available: " . (join ", ", DC::MixChunk::decoders) . "\n"
1119     . "music decoders available: " . (join ", ", DC::MixMusic::decoders);
1120    
1121     $AUDIO_INFO->set_text ($text);
1122     }
1123    
1124     sub audio_setup {
1125     my $vbox = new DC::UI::VBox;
1126    
1127     $vbox->add (my $table = new DC::UI::Table expand => 1, col_expand => [0, 0, 1]);
1128    
1129     my $row = 0;
1130    
1131     $table->add_at (0, $row, new DC::UI::Label align => 1, text => "Audio Enable");
1132     $table->add_at (1, $row, new DC::UI::CheckBox
1133     state => $CFG->{audio_enable},
1134     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.",
1135     on_changed => sub { $CFG->{audio_enable} = $_[1]; 1 }
1136     );
1137     $table->add_at (2, $row++, my $driver = new DC::UI::HBox expand => 1);
1138    
1139     $driver->add (new DC::UI::Label align => 1, text => " Audio driver override");
1140     $driver->add (new DC::UI::Entry
1141     text => $CFG->{audio_driver},
1142     template => "dsound1234",
1143     tooltip => "You can override the audio driver to use here. Leaving it empty will result "
1144     . "in Deliantra picking one automatically. GNU/Linux users often prefer specific "
1145     . "drivers though, and can experiment with <b>alsa</b>, <b>dsp</b>, <b>esd</b>, <b>pulse</b>, <b>arts</b>, <b>nas</b> "
1146 root 1.3 . "or other system-specific drivers. Selecting the wrong driver here will simply result "
1147 root 1.1 . "in no sound.",
1148     on_changed => sub { my ($self, $value) = @_; $CFG->{audio_driver} = $value; 1 }
1149     );
1150    
1151     $table->add_at (0, $row, new DC::UI::Label align => 1, text => "Sound Effects");
1152     $table->add_at (1, $row, new DC::UI::CheckBox
1153     expand => 1, state => $CFG->{effects_enable},
1154     tooltip => "If enabled, sound effects are enabled. If disabled, no sound effects will be played.",
1155     on_changed => sub {
1156     $CFG->{effects_enable} = $_[1];
1157     $CONN->update_fx_want if $CONN;
1158     1
1159     }
1160     );
1161     $table->add_at (2, $row++, new DC::UI::Slider
1162     expand => 1, range => [$CFG->{effects_volume}, 0, 1, 0, 1/128],
1163     tooltip => "The relative volume of sound effects. Best audio quality is achieved if this "
1164     . "is set highest (rightmost) and you use your operating system volume setting. Changes are instant.",
1165     on_changed => sub { $CFG->{effects_volume} = $_[1]; 1 }
1166     );
1167    
1168     $table->add_at (0, $row, new DC::UI::Label align => 1, text => "Background Music");
1169     $table->add_at (1, $row, new DC::UI::CheckBox
1170     expand => 1, state => $CFG->{bgm_enable},
1171     tooltip => "If enabled, playing of background music is enabled. If disabled, no background music will be played. Needs server reconnect to take effect.",
1172     on_changed => sub {
1173     $CFG->{bgm_enable} = $_[1];
1174     $CONN->update_fx_want if $CONN;
1175     audio_music_push;
1176     1
1177     }
1178     );
1179     $table->add_at (2, $row++, new DC::UI::Slider
1180     expand => 1, range => [$CFG->{bgm_volume}, 0, 1, 0, 1/128],
1181     tooltip => "The volume of the background music. Changes are instant.",
1182     on_changed => sub { $CFG->{bgm_volume} = $_[1]; audio_music_update_volume; 0 }
1183     );
1184    
1185     $table->add_at (0, $row, new DC::UI::Label align => 1, text => "Frequency");
1186     $table->add_at (1, $row++, new DC::UI::Selector
1187     c_colspan => 2, expand => 1,
1188     value => $CFG->{audio_hw_frequency},
1189     options => [
1190     [ 0, "default" , "Use System Default"],
1191     [11025, "11 kHz" , "11kHz (low quality)"],
1192     [22050, "22 kHz" , "22kHz (reduced quality, recommended)"],
1193     [44100, "44.1 kHz", "44.1kHz (cd quality)"],
1194     [48000, "48 kHz" , "48kHz (studio quality, not recommended)"],
1195     ],
1196     tooltip => "The sampling frequency to use. Higher sounds better, but also more cpu-intensive and might cause stuttering.",
1197     on_changed => sub {
1198     $CFG->{audio_hw_frequency} = $_[1];
1199     audio_tab_update;
1200     1
1201     }
1202     );
1203    
1204     $table->add_at (0, $row, new DC::UI::Label align => 1, text => "Channels");
1205     $table->add_at (1, $row++, new DC::UI::Selector
1206     c_colspan => 2, expand => 1,
1207     value => $CFG->{audio_hw_channels},
1208     options => [
1209     [0, "default" , "Use System Default"],
1210     [1, "Mono" , "Mono (single channel, low quality)"],
1211     [2, "Stereo" , "Stereo (dual channel, standard quality)"],
1212     [4, "4 Ch Surround", "4 Channel Surround Sound (3d sound, high quality)"],
1213     [6, "6 Ch Surround", "6 Channel Surround Sound (3d sound + center + lfe)"],
1214     ],
1215     tooltip => "The number of independent sound channels to use. Higher sounds better, but also more cpu-intensive and might cause stuttering.",
1216     on_changed => sub {
1217     $CFG->{audio_hw_channels} = $_[1];
1218     audio_tab_update;
1219     1
1220     }
1221     );
1222    
1223     $table->add_at (0, $row, new DC::UI::Label align => 1, text => "Latency");
1224     $table->add_at (1, $row++, $AUDIO_HW_CHUNKSIZE = new DC::UI::Selector
1225     c_colspan => 2, expand => 1,
1226     value => $CFG->{audio_hw_chunksize},
1227     tooltip => "The guarenteed latency. Lower is better, but also more cpu-intensive and might cause stuttering. If music playback "
1228     . "is stuttering, increase this value. Values of 50-150ms are optimal.",
1229     on_changed => sub {
1230     $CFG->{audio_hw_chunksize} = $_[1];
1231     audio_tab_update;
1232     1
1233     }
1234     );
1235    
1236     # should really be a slider
1237     $table->add_at (0, $row, new DC::UI::Label align => 1, text => "Mixer Voices");
1238     $table->add_at (1, $row++, new DC::UI::ValSlider
1239     c_colspan => 2, expand => 1,
1240     tooltip => "The number of simultaneous sound effects possible. Higher is better, but also more cpu-intensive and might cause stuttering.",
1241     range => [$::CFG->{audio_mix_channels}, 4, 32, 0, 1],
1242     template => ">= 99",
1243     on_changed => sub {
1244     my ($slider, $value) = @_;
1245    
1246     $CFG->{audio_mix_channels} = $value
1247     if $value;
1248     1;
1249     }
1250     );
1251    
1252     $table->add_at (1, $row++, new DC::UI::Button
1253     c_colspan => 2, expand => 1, text => "Apply",
1254     tooltip => "Apply the audio settings",
1255     on_activate => sub {
1256     audio_shutdown ();
1257     audio_init ();
1258     0
1259     }
1260     );
1261    
1262     $vbox->add (new DC::UI::FancyFrame
1263     expand => 1,
1264     label => "Audio Info",
1265     child => ($AUDIO_INFO = new DC::UI::Label ellipsise => 0),
1266     );
1267    
1268     audio_tab_update;
1269    
1270     $vbox
1271     }
1272    
1273     sub set_gauge_window_fontsize {
1274     for (map { $GAUGES->{$_} } grep { $_ ne 'win' } keys %{$GAUGES}) {
1275     $_->set_fontsize ($::CFG->{gauge_fontsize});
1276     }
1277     }
1278    
1279     sub make_gauge_window {
1280     my $gh = int $HEIGHT * $CFG->{gauge_size};
1281    
1282     $GAUGES->{win} = my $win = new DC::UI::Frame (
1283     force_x => 0,
1284     force_y => "max",
1285     force_w => $WIDTH,
1286     force_h => $gh,
1287     );
1288    
1289     $win->add (my $hbox = new DC::UI::HBox
1290     children => [
1291     (new DC::UI::HBox expand => 1),
1292     (new DC::UI::VBox children => [
1293     (new DC::UI::Empty expand => 1),
1294     (new DC::UI::Frame bg => [0, 0, 0, 0.4], child => ($FLOORBOX = new DC::UI::Table)),
1295     ]),
1296     (my $vbox = new DC::UI::VBox),
1297     ],
1298     );
1299    
1300     $vbox->add (new DC::UI::HBox
1301     expand => 1,
1302     children => [
1303     (new DC::UI::Empty expand => 1),
1304     (my $hb = new DC::UI::HBox),
1305     ],
1306     );
1307    
1308     $hb->add ($GAUGES->{hp} = new DC::UI::Gauge type => 'hp', tooltip => "#stat_health");
1309     $hb->add ($GAUGES->{mana} = new DC::UI::Gauge type => 'mana', tooltip => "#stat_mana");
1310     $hb->add ($GAUGES->{grace} = new DC::UI::Gauge type => 'grace', tooltip => "#stat_grace");
1311     $hb->add ($GAUGES->{food} = new DC::UI::Gauge type => 'food', tooltip => "#stat_food");
1312    
1313     &set_gauge_window_fontsize;
1314    
1315     $win
1316     }
1317    
1318     our $BW_WATCHER;
1319    
1320     sub debug_toggle($) {
1321     $DELIANTRA_DEBUG ^= $_[0];
1322    
1323     if ($DELIANTRA_DEBUG & 16) {
1324     $BW_WATCHER = EV::periodic 0, 1, 0, sub {
1325     return unless $CONN;
1326     debug sprintf "%8.2gKB/s", $CONN->{octets_in} / 1e3;
1327     $CONN->{octets_in} = 0;
1328     };
1329     } else {
1330     undef $BW_WATCHER;
1331     }
1332    
1333     }
1334    
1335     sub debug_setup {
1336     my $table = new DC::UI::Table;
1337    
1338     $table->add_at (0, 0, new DC::UI::Label text => "Widget Borders");
1339     $table->add_at (1, 0, new DC::UI::CheckBox on_changed => sub { debug_toggle 1; 0 });
1340     $table->add_at (0, 1, new DC::UI::Label text => "Tooltip Widget Info");
1341     $table->add_at (1, 1, new DC::UI::CheckBox on_changed => sub { debug_toggle 2; 0 });
1342     $table->add_at (0, 2, new DC::UI::Label text => "Show FPS");
1343     $table->add_at (1, 2, new DC::UI::CheckBox on_changed => sub { debug_toggle 4; 0 });
1344     $table->add_at (0, 3, new DC::UI::Label text => "Suppress Tooltips");
1345     $table->add_at (1, 3, new DC::UI::CheckBox on_changed => sub { debug_toggle 8; 0 });
1346     $table->add_at (0, 4, new DC::UI::Label text => "Show Bandwidth");
1347     $table->add_at (1, 4, new DC::UI::CheckBox on_changed => sub { debug_toggle 16; 0 });
1348    
1349     $table->add_at (0, 6, new DC::UI::Button text => "die on click(tm)", on_activate => sub { &DC::debug() } );
1350     $table->add_at (0, 7, new DC::UI::TextEdit text => "line1\0152\0153\nµikachu\nづx゙つ゛");#d#
1351    
1352     $table->add_at (7,7, my $t = new DC::UI::Table expand => 0);
1353     $t->add_at (0,0, new DC::UI::Label text => "a a", c_rowspan => 1, c_colspan => 2);
1354     $t->add_at (2,0, new DC::UI::Label text => "b\nb", c_rowspan => 2, c_colspan => 1, ellipsise => 0 );
1355     $t->add_at (1,2, new DC::UI::Label text => "c c", c_rowspan => 1, c_colspan => 2);
1356     $t->add_at (0,1, new DC::UI::Label text => "d\nd", c_rowspan => 2, c_colspan => 1, ellipsise => 0 );
1357     $t->add_at (1,1, new DC::UI::Label text => "e");
1358    
1359     $table->add_at (7, 6, my $c = new DC::UI::Canvas);
1360    
1361     $c->add_items ({
1362     type => "line_loop",
1363     color => [0, 1, 0],
1364     width => 9,
1365     coord_mode => "abs",
1366     coord => [[10, 5], [5, 50], [20, 5], [5, 60]],
1367     });
1368    
1369     $c->add_items ({
1370     type => "lines",
1371     color => [1, 1, 0],
1372     width => 2,
1373     coord_mode => "rel",
1374     coord => [[0,0], [1,1], [1,0], [0,1]],
1375     });
1376    
1377     $c->add_items ({
1378     type => "polygon",
1379     color => [0, 0.43, 0],
1380     width => 2,
1381     coord_mode => "rel",
1382     coord => [[0,0.2], [1,.4], [1,.6], [0,.8]],
1383     });
1384    
1385     $table
1386     }
1387    
1388     sub stats_window {
1389     my $r = new DC::UI::ScrolledWindow (
1390     expand => 1,
1391     scroll_y => 1
1392     );
1393     $r->add (my $vb = new DC::UI::VBox);
1394    
1395     $vb->add (new DC::UI::FancyFrame
1396     label => "Player",
1397     child => (my $pi = new DC::UI::VBox),
1398     );
1399    
1400     $pi->add ($STATWIDS->{title} = new DC::UI::Label text => "Title:", expand => 1, align => 0,
1401     can_hover => 1, can_events => 1,
1402     tooltip => "Your name and title. You can change your title by using the <b>title</b> command, if supported by the server.");
1403     $pi->add ($STATWIDS->{map} = new DC::UI::Label align => 0, text => "Map:", expand => 1,
1404     can_hover => 1, can_events => 1,
1405     tooltip => "The map you are currently on (if supported by the server).");
1406    
1407     $pi->add (my $hb0 = new DC::UI::HBox);
1408     $hb0->add ($STATWIDS->{weight} = new DC::UI::Label text => "Weight:", expand => 1, align => 0,
1409     can_hover => 1, can_events => 1,
1410     tooltip => "The weight of the player including all inventory items.");
1411     $hb0->add ($STATWIDS->{m_weight} = new DC::UI::Label align => 0, text => "Max weight:", expand => 1,
1412     can_hover => 1, can_events => 1,
1413     tooltip => "The weight limit: you cannot carry more than this.");
1414    
1415     $vb->add (new DC::UI::FancyFrame
1416     label => "Primary/Secondary Statistics",
1417     child => (my $hb = new DC::UI::HBox expand => 1),
1418     );
1419     $hb->add (my $tbl = new DC::UI::Table expand => 1);
1420    
1421     my $color2 = [1, 1, 0];
1422    
1423     for (
1424     [0, 0, st_str => "Str", 30],
1425     [0, 1, st_dex => "Dex", 30],
1426     [0, 2, st_con => "Con", 30],
1427     [0, 3, st_int => "Int", 30],
1428     [0, 4, st_wis => "Wis", 30],
1429     [0, 5, st_pow => "Pow", 30],
1430     [0, 6, st_cha => "Cha", 30],
1431    
1432     [2, 0, st_wc => "Wc", -120],
1433     [2, 1, st_ac => "Ac", -120],
1434     [2, 2, st_dam => "Dam", 120],
1435     [2, 3, st_arm => "Arm", 120],
1436     [2, 4, st_spd => "Spd", 10.54],
1437     [2, 5, st_wspd => "WSp", 10.54],
1438     ) {
1439     my ($col, $row, $id, $label, $template) = @$_;
1440    
1441     $tbl->add_at ($col , $row, $STATWIDS->{$id} = new DC::UI::Label
1442     font => $FONT_FIXED, can_hover => 1, can_events => 1,
1443     align => 1, template => $template, tooltip => "#stat_$label");
1444     $tbl->add_at ($col + 1, $row, $STATWIDS->{"$id\_lbl"} = new DC::UI::Label
1445     font => $FONT_FIXED, can_hover => 1, can_events => 1, fg => $color2,
1446     align => 0, text => $label, tooltip => "#stat_$label");
1447     }
1448    
1449     $vb->add (new DC::UI::FancyFrame
1450     label => "Resistancies",
1451     child => (my $tbl2 = new DC::UI::Table expand => 1, col_expand => [1, 1, 0, 1, 1, 1, 0, 1, 1, 1, 0]),
1452     );
1453    
1454     my $row = 0;
1455     my $col = 0;
1456    
1457     my %resist_names = (
1458     slow => ["Slow",
1459     "<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.)"],
1460     holyw => ["Holy Word",
1461     "<b>Holy Word</b> (resistance you against getting the fear when someone whose god doesn't like you spells the holy word on you.)"],
1462     conf => ["Confusion",
1463     "<b>Confusion</b> (If you are hit by confusion you will move into random directions, and likely into monsters.)"],
1464     fire => ["Fire",
1465     "<b>Fire</b> (just your resistance to fire spells like burning hands, dragonbreath, meteor swarm fire, ...)"],
1466     depl => ["Depletion",
1467     "<b>Depletion</b> (some monsters and other effects can cause stats depletion)"],
1468     magic => ["Magic",
1469     "<b>Magic</b> (resistance to magic spells like magic missile or similar)"],
1470     drain => ["Draining",
1471     "<b>Draining</b> (some monsters (e.g. vampires) and other effects can steal experience)"],
1472     acid => ["Acid",
1473     "<b>Acid</b> (resistance to acid, acid hurts pretty much and also corrodes your weapons)"],
1474     pois => ["Poison",
1475     "<b>Poison</b> (resistance to getting poisoned)"],
1476     para => ["Paralysation",
1477     "<b>Paralysation</b> (this resistance affects the chance you get paralysed)"],
1478     deat => ["Death",
1479     "<b>Death</b> (resistance against death spells)"],
1480     phys => ["Physical",
1481     "<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 as the 'Arm' secondary stat.)"],
1482     blind => ["Blind",
1483     "<b>Blind</b> (blind resistance affects the chance of a successful blinding attack)"],
1484     fear => ["Fear",
1485     "<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)"],
1486     tund => ["Turn undead",
1487     "<b>Turn undead</b> (affects your resistancy to various forms of 'turn undead' spells. Only relevant when you are, in fact, undead..."],
1488     elec => ["Electricity",
1489     "<b>Electricity</b> (resistance against electricity, spells like large lightning, small lightning, ...)"],
1490     cold => ["Cold",
1491     "<b>Cold</b> (this is your resistance against cold spells like icestorm, snowstorm, ...)"],
1492     ghit => ["Ghost hit",
1493     "<b>Ghost hit</b> (special attack used by ghosts and ghost-like beings)"],
1494     );
1495    
1496     for (qw/slow holyw conf fire depl magic
1497     drain acid pois para deat phys
1498     blind fear tund elec cold ghit/)
1499     {
1500     $tbl2->add_at ($col + 2, $row,
1501     $STATWIDS->{"res_$_"} =
1502     new DC::UI::Label
1503     font => $FONT_FIXED,
1504     template => "-100%",
1505     align => 1,
1506     can_events => 1,
1507     can_hover => 1,
1508     tooltip => $resist_names{$_}->[1],
1509     );
1510     $tbl2->add_at ($col + 1, $row, new DC::UI::Image
1511     font => $FONT_FIXED,
1512     can_hover => 1,
1513     can_events => 1,
1514     path => "ui/resist/resist_$_.png",
1515     tooltip => $resist_names{$_}->[1],
1516     );
1517     $tbl2->add_at ($col + 0, $row, new DC::UI::Label
1518     text => $resist_names{$_}->[0],
1519     font => $FONT_FIXED,
1520     align => 1,
1521     can_hover => 1,
1522     can_events => 1,
1523     tooltip => $resist_names{$_}->[1],
1524     );
1525    
1526     $row++;
1527     if ($row % 6 == 0) {
1528     $col += 4;
1529     $row = 0;
1530     }
1531     }
1532    
1533     #update_stats_window ({});
1534    
1535     $r
1536     }
1537    
1538     sub skill_window {
1539     my $sw = new DC::UI::ScrolledWindow (expand => 1);
1540    
1541     $sw->add ($STATWIDS->{skill_tbl} = new DC::UI::Table expand => 1, col_expand => [0, 0, 1, .1, 0, 0, 1, .1]);
1542    
1543     $sw
1544     }
1545    
1546     sub formsep($) {
1547     scalar reverse join ",", unpack "(A3)*", reverse $_[0] * 1
1548     }
1549    
1550     my $METASERVER_ATIME;
1551    
1552     sub update_metaserver {
1553     my ($metaserver_dialog) = @_;
1554    
1555     $METASERVER = $metaserver_dialog
1556     if defined $metaserver_dialog;
1557    
1558     return if $METASERVER_ATIME > time;
1559     $METASERVER_ATIME = time + 60;
1560    
1561     my $table = $METASERVER->{table};
1562     $table->clear;
1563     $table->add_at (0, 0, my $label = new DC::UI::Label max_w => $WIDTH * 0.8, text => "fetching server list...");
1564    
1565     my $ok = 0;
1566    
1567     DC::background {
1568     my $ua = DC::lwp_useragent;
1569    
1570     DC::background_msg DC::decode_json +(DC::lwp_check $ua->get ($META_SERVER))->decoded_content;
1571     } sub {
1572     my ($msg) = @_;
1573     if ($msg) {
1574     $table->clear;
1575    
1576     my @tip = (
1577     "The current number of users logged in on the server.",
1578     "The hostname of the server.",
1579     "The time this server has been running without being restarted.",
1580     "Short information about this server provided by its admins.",
1581     );
1582     my @col = qw(#Users Host Uptime Version Description);
1583     $table->add_at ($_, 0, new DC::UI::Label
1584     can_hover => 1, can_events => 1, fg => [1, 1, 0],
1585     text => $col[$_], tooltip => $tip[$_])
1586     for 0 .. $#col;
1587    
1588     my @align = qw(1 0.5 1 1 0);
1589    
1590     my $y = 0;
1591     for my $m (@{ $msg->{servers} }) {
1592     my ($ip, $last, $host, $users, $version, $desc, $ibytes, $obytes, $uptime, $highlight) =
1593     @$m{qw(ip age hostname users version description ibytes obytes uptime highlight)};
1594    
1595     for ($desc) {
1596     s/<br>/\n/gi;
1597     s/<li>/\n· /gi;
1598     s/<.*?>//sgi;
1599     s/&amp;/&/g;
1600     s/&lt;/</g;
1601     s/&gt;/>/g;
1602     }
1603    
1604     $uptime = sprintf "%dd %02d:%02d:%02d",
1605     (int $uptime / 86400),
1606     (int $uptime / 3600) % 24,
1607     (int $uptime / 60) % 60,
1608     $uptime % 60;
1609    
1610     $m = [$users, $host, $uptime, $version, $desc];
1611    
1612     $y++;
1613    
1614     $table->add_at (scalar @$m, $y, new DC::UI::VBox children => [
1615     (new DC::UI::Button
1616     text => "Use",
1617     tooltip => "Put this server into the <b>Host:Port</b> field",
1618     on_activate => sub {
1619     $HOST_ENTRY->set_text ($CFG->{profile}{default}{host} = $host);
1620     $METASERVER->hide;
1621     0
1622     },
1623     ),
1624     (new DC::UI::Empty expand => 1),
1625     ]);
1626    
1627     $table->add_at ($_, $y, new DC::UI::Label
1628     max_w => $::WIDTH * 0.4,
1629     ellipsise => 0,
1630     align => $align[$_],
1631     text => $m->[$_],
1632     tooltip => $tip[$_],
1633     fg => ($highlight ? [1, 1, 1] : [.7, .7, .7]),
1634     can_hover => 1,
1635     can_events => 1,
1636     fontsize => 0.8)
1637     for 0 .. $#$m;
1638     }
1639     } else {
1640     $ok or $label->set_text ("error while contacting metaserver");
1641     }
1642     };
1643    
1644     }
1645    
1646     sub metaserver_dialog {
1647     my $vbox = new DC::UI::VBox;
1648     my $table = new DC::UI::Table;
1649     $vbox->add (new DC::UI::ScrolledWindow expand => 1, child => $table);
1650    
1651     my $dialog = new DC::UI::Toplevel
1652     title => "Server List",
1653     name => 'metaserver_dialog',
1654     x => 'center',
1655     y => 'center',
1656     z => 3,
1657     force_w => $::WIDTH * 0.9,
1658     force_h => $::HEIGHT * 0.7,
1659     child => $vbox,
1660     has_close_button => 1,
1661     table => $table,
1662     on_visibility_change => sub {
1663     update_metaserver ($_[0]) if $_[1];
1664     0
1665     },
1666     ;
1667    
1668     $dialog
1669     }
1670    
1671     sub login_setup {
1672     my $vbox = new DC::UI::VBox;
1673    
1674     $vbox->add (new DC::UI::FancyFrame
1675     label => "Login Settings",
1676     child => (my $table = new DC::UI::Table expand => 1, col_expand => [0, 1]),
1677     );
1678    
1679     $table->add_at (0, 4, new DC::UI::Label align => 1, text => "Username");
1680     $table->add_at (1, 4, new DC::UI::Entry
1681     text => $PROFILE->{user},
1682     tooltip => "The name of your character on the server. The name is case-sensitive!",
1683     on_changed => sub { my ($self, $value) = @_; $PROFILE->{user} = $value; 1 }
1684     );
1685    
1686     $table->add_at (0, 5, new DC::UI::Label align => 1, text => "Password");
1687     $table->add_at (1, 5, new DC::UI::Entry
1688     text => $PROFILE->{password},
1689     hidden => 1,
1690     tooltip => "The password for your character.",
1691     on_changed => sub { my ($self, $value) = @_; $PROFILE->{password} = $value; 1 }
1692     );
1693    
1694     $table->add_at (1, 11, $LOGIN_BUTTON = new DC::UI::Button
1695     expand => 1,
1696     text => "Login / Register",
1697     tooltip => "This button will either login to the account configured above or register a new account.",
1698     on_activate => sub {
1699     $CONN ? stop_game
1700     : start_game;
1701     1
1702     },
1703     );
1704    
1705     $vbox->add (new DC::UI::FancyFrame
1706     label => "How to Play",
1707     min_h => 240,
1708     child => (new DC::UI::Label valign => 0, ellipsise => 0,
1709     markup =>
1710     "First select a suitable video resolution in the <b>Graphics</b> tab, above.\n\n"
1711     . "Then register a new account (or use an existing one if you have one). "
1712     . "To register an account, choose a username that hasn't been taken yet (just guess) and "
1713     . "try to log-in. Follow the instructions in the Log tab in the message window.",
1714     ),
1715     );
1716    
1717     $vbox
1718     }
1719    
1720     sub server_setup {
1721     my $vbox = new DC::UI::VBox;
1722    
1723     $vbox->add (new DC::UI::FancyFrame
1724     label => "Connection Settings",
1725     child => (my $table = new DC::UI::Table expand => 1, col_expand => [0, 1]),
1726     );
1727    
1728     my $row = 0;
1729    
1730     $table->add_at (0, ++$row, new DC::UI::Label align => 1, text => "Host:Port");
1731     {
1732     $table->add_at (1, $row, my $vbox = new DC::UI::VBox);
1733    
1734     $vbox->add (
1735     $HOST_ENTRY = new DC::UI::Entry
1736     expand => 1,
1737     text => $PROFILE->{host},
1738     tooltip => "The hostname or ip address of the Deliantra server to connect to (e.g. <b>gameserver.deliantra.net</b>)",
1739     on_changed => sub {
1740     my ($self, $value) = @_;
1741     $PROFILE->{host} = $value;
1742     1
1743     }
1744     );
1745    
1746     if (0) { #d# disabled
1747     $vbox->add (new DC::UI::Button
1748     expand => 1,
1749     text => "Server List",
1750     other => $METASERVER,
1751     tooltip => "Show a list of available Deliantra servers",
1752     on_activate => sub { $METASERVER->toggle_visibility; 0 },
1753     on_visibility_change => sub { $METASERVER->hide unless $_[1]; 1 },
1754     );
1755     }#d#
1756     }
1757    
1758     $table->add_at (0, ++$row, new DC::UI::Label align => 1, text => "Map Size");
1759     $table->add_at (1, $row, new DC::UI::Slider
1760     force_w => 100,
1761     range => [$CFG->{mapsize}, 10, 100, 0, 1],
1762     tooltip => "This is the size of the portion of the map update the server sends you. "
1763     . "If you set this to a high value you will be able to see further, "
1764     . "but you also increase bandwidth requirements and latency. "
1765     . "This option is only used once at log-in.",
1766     on_changed => sub { my ($self, $value) = @_; $CFG->{mapsize} = $self->{range}[0] = $value = int $value; 1 },
1767     );
1768    
1769     $table->add_at (0, ++$row, new DC::UI::Label align => 1, text => "Output-Rate");
1770     $table->add_at (1, $row, new DC::UI::Entry
1771     text => $CFG->{output_rate},
1772     tooltip => "The maximum bandwidth in bytes per second that the server should not exceed "
1773     . "when sending data. When 0 or unset, the server "
1774     . "default will be used, which is usually around 100kb/s. Most servers will "
1775     . "dynamically find an optimal rate, so adjust this only when necessary.",
1776     on_changed => sub { $CFG->{output_rate} = $_[1]; 1 },
1777     );
1778    
1779     $vbox->add (new DC::UI::FancyFrame
1780     label => "Server Info",
1781     child => ($SERVER_INFO = new DC::UI::Label ellipsise => 0),
1782     );
1783    
1784     $vbox
1785     }
1786    
1787     sub client_setup {
1788 root 1.2 my $vbox = new DC::UI::VBox;
1789 root 1.1
1790 root 1.2 $vbox->add (my $top = new DC::UI::FancyFrame expand => 1, label => "Client Settings");
1791     $vbox->add (my $bot = new DC::UI::FancyFrame expand => 1, label => "Client Info");
1792 root 1.1
1793 root 1.2 {
1794     $top->add (my $table = new DC::UI::Table expand => 1, col_expand => [0, 1]);
1795    
1796     my $row = 0;
1797    
1798     $table->add_at (0, $row, new DC::UI::Label align => 1, text => "Tip of the day");
1799     $table->add_at (1, $row++, new DC::UI::CheckBox
1800     c_colspan => 2,
1801     state => $CFG->{show_tips},
1802     tooltip => "Show the <b>Tip of the day</b> window at startup?",
1803     on_changed => sub {
1804     my ($self, $value) = @_;
1805     $CFG->{show_tips} = $value;
1806     0
1807     }
1808     );
1809    
1810     $table->add_at (0, $row, new DC::UI::Label align => 1, text => "Message Window Size");
1811     $table->add_at (1, $row++, my $saycmd = new DC::UI::Entry
1812     c_colspan => 2,
1813     text => $CFG->{logview_max_par},
1814     tooltip => "This is maximum number of messages remembered in the <b>Message</b> window. If the server "
1815     . "sends more messages than this number, older messages get removed to save memory and "
1816     . "computing time. A value of <b>0</b> disables this feature, but that is not recommended.",
1817     on_changed => sub {
1818     my ($self, $value) = @_;
1819     $MESSAGE_DIST->set_max_par ($CFG->{logview_max_par} = $value*1);
1820     0
1821     },
1822     );
1823    
1824     $table->add_at (0, $row, new DC::UI::Label align => 1, text => "Config Autosave");
1825     $table->add_at (1, $row, new DC::UI::CheckBox
1826     state => $CFG->{config_autosave},
1827     tooltip => "Normally, configuration settings and the user interface layout "
1828     . "are saved on client exit. You can disable this behaviour by "
1829     . "unchecking this checkbox.",
1830     on_changed => sub {
1831     my ($self, $value) = @_;
1832     $CFG->{config_autosave} = $value;
1833     0
1834     }
1835     );
1836     $table->add_at (2, $row++, new DC::UI::Button
1837     text => "Save Now",
1838     tooltip => "Use this to manually save configuration and UI layout when "
1839     . "autosave is disabled.",
1840     on_activate => sub {
1841     DC::write_cfg;
1842     0
1843     }
1844     );
1845     }
1846    
1847     {
1848     $bot->add (my $table = new DC::UI::Table expand => 1, col_expand => [0, 1]);
1849 root 1.1
1850 root 1.2 my $row = 0;
1851 root 1.1
1852 root 1.2 $table->add_at (0, $row , new DC::UI::Label align => 1, text => "Data Directory");
1853     $table->add_at (1, $row++, new DC::UI::Label align => 0, text => $Deliantra::VARDIR, tooltip => "");
1854     $table->add_at (0, $row , new DC::UI::Label align => 1, text => "Database Directory");
1855     $table->add_at (1, $row++, new DC::UI::Label align => 0, text => $DC::DB::DBDIR, tooltip => "");
1856 root 1.5 $table->add_at (0, $row , new DC::UI::Label align => 1, text => "Urlader (Prebuilt)");
1857     $table->add_at (1, $row++, new DC::UI::Label align => 0, text => $ENV{URLADER_VERSION}, tooltip => "");
1858 root 1.2 $table->add_at (0, $row , new DC::UI::Label align => 1, text => "Branch (Prebuilt)");
1859 root 1.5 $table->add_at (1, $row++, new DC::UI::Label align => 0, text => $ENV{URLADER_EXE_ID}, tooltip => "");
1860 root 1.2 $table->add_at (0, $row , new DC::UI::Label align => 1, text => "Version (Prebuilt)");
1861 root 1.5 $table->add_at (1, $row++, new DC::UI::Label align => 0, text => $ENV{URLADER_EXE_VER}, tooltip => "");
1862 root 1.2 }
1863 root 1.1
1864 root 1.2 $vbox
1865 root 1.1 }
1866    
1867     sub autopickup_setup {
1868     my $r = new DC::UI::ScrolledWindow (
1869     expand => 1,
1870     scroll_y => 1
1871     );
1872     $r->add (my $table = new DC::UI::Table
1873     row_expand => [0],
1874     col_expand => [0, 1, 0, 1],
1875     );
1876    
1877     for (
1878     ["General", 0, 0,
1879     # ["Inhibit autopickup" => PICKUP_INHIBIT],
1880     ["Stop before pickup" => PICKUP_STOP],
1881     ["Debug autopickup" => PICKUP_DEBUG],
1882     ],
1883     ["Weapons", 0, 6,
1884     ["All weapons" => PICKUP_ALLWEAPON],
1885     ["Missile weapons" => PICKUP_MISSILEWEAPON],
1886     ["Bows" => PICKUP_BOW],
1887     ["Arrows" => PICKUP_ARROW],
1888     ],
1889     ["Armour", 0, 12,
1890     ["Helmets" => PICKUP_HELMET],
1891     ["Shields" => PICKUP_SHIELD],
1892     ["Body Armour" => PICKUP_ARMOUR],
1893     ["Boots" => PICKUP_BOOTS],
1894     ["Gloves" => PICKUP_GLOVES],
1895     ["Cloaks" => PICKUP_CLOAK],
1896     ],
1897    
1898     ["Readables", 2, 0,
1899     ["Spellbooks" => PICKUP_SPELLBOOK],
1900     ["Skillscrolls" => PICKUP_SKILLSCROLL],
1901     ["Normal Books/Scrolls" => PICKUP_READABLES],
1902     ],
1903     ["Misc", 2, 5,
1904     ["Food" => PICKUP_FOOD],
1905     ["Drinks" => PICKUP_DRINK],
1906     ["Valuables (Money, Gems)" => PICKUP_VALUABLES],
1907     ["Keys" => PICKUP_KEY],
1908     ["Magical Items" => PICKUP_MAGICAL],
1909     ["Potions" => PICKUP_POTION],
1910     ["Magic Devices" => PICKUP_MAGIC_DEVICE],
1911     ["Ignore cursed" => PICKUP_NOT_CURSED],
1912     ["Jewelery" => PICKUP_JEWELS],
1913     ["Flesh" => PICKUP_FLESH],
1914     ],
1915     ["Value/Weight ratio", 2, 17]
1916     )
1917     {
1918     my ($title, $x, $y, @bits) = @$_;
1919     $table->add_at ($x, $y, new DC::UI::Label text => $title, align => 1, fg => [1, 1, 0]);
1920    
1921     for (@bits) {
1922     ++$y;
1923    
1924     my $mask = $_->[1];
1925     $table->add_at ($x , $y, new DC::UI::Label text => $_->[0], align => 1, expand => 1);
1926     $table->add_at ($x+1, $y, my $checkbox = new DC::UI::CheckBox
1927     state => $::CFG->{pickup} & $mask,
1928     on_changed => sub {
1929     my ($box, $value) = @_;
1930    
1931     if ($value) {
1932     $::CFG->{pickup} |= $mask;
1933     } else {
1934     $::CFG->{pickup} &= ~$mask;
1935     }
1936    
1937     $::CONN->send_pickup ($::CFG->{pickup})
1938     if defined $::CONN;
1939    
1940     0
1941     });
1942    
1943     ${$_->[2]} = $checkbox if $_->[2];
1944     }
1945     }
1946    
1947     $table->add_at (2, 18, new DC::UI::ValSlider
1948     range => [$::CFG->{pickup} & 0xF, 0, 16, 1, 1],
1949     template => ">= 99",
1950     tooltip => "Pick up items whose value/weight (silver/kg) ratio is equal or higher than this setting (which is specified in gold coins).",
1951     to_value => sub { ">= " . 5 * $_[0] },
1952     on_changed => sub {
1953     my ($slider, $value) = @_;
1954    
1955     $::CFG->{pickup} &= ~0xF;
1956     $::CFG->{pickup} |= int $value
1957     if $value;
1958     1;
1959     });
1960    
1961     $table->add_at (3, 18, new DC::UI::Button
1962     text => "set",
1963     on_activate => sub {
1964     $::CONN->send_pickup ($::CFG->{pickup})
1965     if defined $::CONN;
1966     0
1967     });
1968    
1969     $r
1970     }
1971    
1972     my %SORT_ORDER = (
1973     type => sub {
1974     use sort 'stable';
1975     sort { $a->{type} <=> $b->{type} or $a->{name} cmp $b->{name} } @_
1976     },
1977     mtime => sub {
1978     use sort 'stable';
1979     my $NOW = time;
1980     sort {
1981     my $atime = $a->{mtime} - $NOW; $atime = $atime < 5 * 60 ? int $atime / 60 : 6;
1982     my $btime = $b->{mtime} - $NOW; $btime = $btime < 5 * 60 ? int $btime / 60 : 6;
1983    
1984     ($a->{flags} & F_LOCKED) <=> ($b->{flags} & F_LOCKED)
1985     or $btime <=> $atime
1986     or $a->{type} <=> $b->{type}
1987     } @_
1988     },
1989     weight => sub {
1990     use sort 'stable';
1991     sort {
1992     $a->{weight} * ($a->{nrof} || 1) <=> $b->{weight} * ($b->{nrof} || 1)
1993     or $a->{type} <=> $b->{type}
1994     } @_
1995     },
1996     );
1997    
1998     sub inventory_widget {
1999     my $hb = new DC::UI::HBox homogeneous => 1;
2000    
2001     $hb->add (my $vb1 = new DC::UI::VBox);
2002     $vb1->add (new DC::UI::Label text => "Player");
2003    
2004     $vb1->add (my $hb1 = new DC::UI::HBox);
2005    
2006     use sort 'stable';
2007    
2008     $hb1->add (new DC::UI::Selector
2009     value => $::CFG->{inv_sort},
2010     options => [
2011     [type => "Type/Name"],
2012     [mtime => "Recent/Normal/Locked"],
2013     [weight => "Weight/Type"],
2014     ],
2015     on_changed => sub {
2016     $::CFG->{inv_sort} = $_[1];
2017     $INV->set_sort_order ($SORT_ORDER{$_[1]});
2018     },
2019     );
2020     $hb1->add (new DC::UI::Label text => "Weight: ", align => 1, expand => 1);
2021     #TODO# update to weight/maxweight
2022     $hb1->add ($STATWIDS->{i_weight} = new DC::UI::Label align => 0);
2023    
2024     $vb1->add (my $sw1 = new DC::UI::ScrolledWindow expand => 1, scroll_y => 1);
2025     $sw1->add ($INV = new DC::UI::Inventory);
2026     $INV->set_sort_order ($SORT_ORDER{$::CFG->{inv_sort}});
2027    
2028     $hb->add (my $vb2 = new DC::UI::VBox);
2029    
2030     $vb2->add ($INVR_HB = new DC::UI::HBox);
2031    
2032     $vb2->add (my $sw2 = new DC::UI::ScrolledWindow expand => 1, scroll_y => 1);
2033     $sw2->add ($INVR = new DC::UI::Inventory);
2034    
2035     # XXX: Call after $INVR = ... because set_opencont sets the items
2036     DC::Protocol::set_opencont ($::CONN, 0, "Floor");
2037    
2038     $hb
2039     }
2040    
2041     sub media_window {
2042     my $vb = new DC::UI::VBox;
2043    
2044     $vb->add (new DC::UI::FancyFrame
2045 root 1.6 label => "Current background music",
2046 root 1.1 child => new DC::UI::ScrolledWindow scroll_x => 1, scroll_y => 0,
2047     child => ($MUSIC_PLAYING_WIDGET = new DC::UI::Label ellipsise => 0, fontsize => 0.8),
2048     );
2049    
2050     $vb->add (new DC::UI::FancyFrame
2051 root 1.6 label => "Current downloads",
2052     child => ($DOWNLOADS_WIDGET = new DC::UI::Table
2053     expand => 1, fontsize => 0.8, padding_x => 4, padding_y => 4),
2054     );
2055    
2056     $DOWNLOADS_WIDGET->connect (visibility_change => sub {
2057     my ($self) = @_;
2058    
2059     delete $self->{updater};
2060     return unless $_[1];
2061    
2062 root 1.7 $self->{updater} = AE::timer 0, 0.7, sub {
2063 root 1.6 $self->clear;
2064    
2065     return unless $CONN;
2066    
2067     my @nums = sort { $b <=> $a } keys %{ $CONN->{ix_recv_buf} };
2068 root 1.7 return unless @nums;
2069 root 1.6
2070     $self->add_at (0, 0, new DC::UI::Label align => 1, text => "Face");
2071     $self->add_at (1, 0, new DC::UI::Label align => 0, text => "Octets/Total");
2072    
2073     for my $row (0 .. $#nums) {
2074     my $num = $nums[$row];
2075    
2076     my $total = length $CONN->{ix_recv_buf}{$num};
2077     my $got = $total - $CONN->{ix_recv_ofs}{$num};
2078    
2079     $self->add_at (0, $row + 1, new DC::UI::Label align => 1, text => $num, tooltip => "");
2080     $self->add_at (1, $row + 1, new DC::UI::Label align => 0, text => "$got/$total", tooltip => "");
2081     }
2082     };
2083     });
2084    
2085     $vb->add (new DC::UI::FancyFrame
2086 root 1.1 label => "Other media used in this session",
2087     expand => 1,
2088     child => ($LICENSE_WIDGET = new DC::UI::TextScroller
2089 root 1.6 expand => 1, fontsize => 0.8, padding_x => 4, padding_y => 4),
2090 root 1.1 );
2091    
2092     $vb
2093     }
2094    
2095     sub add_license {
2096     my ($meta) = @_;
2097    
2098     $meta = $meta->{data}
2099     or return;
2100    
2101     $meta->{license} || $meta->{author} || $meta->{source}
2102     or return;
2103    
2104     $LICENSE_WIDGET->add_paragraph ({
2105     fg => [1, 1, 1, 1],
2106     markup => "<small>"
2107     . "<b>Name:</b> " . (DC::asxml $meta->{name}) . "\n"
2108     . "<b>Author:</b> " . (DC::asxml $meta->{author}) . "\n"
2109     . "<b>Source:</b> " . (DC::asxml $meta->{source}) . "\n"
2110     . "<b>License:</b> " . (DC::asxml $meta->{license}) . "\n"
2111     . "</small>",
2112     });
2113     $LICENSE_WIDGET->scroll_to_bottom;
2114     }
2115    
2116     sub toggle_player_page {
2117     my ($widget) = @_;
2118    
2119     if ($PL_WINDOW->{visible} && $PL_NOTEBOOK->get_current_page == $widget) {
2120     $PL_WINDOW->hide;
2121     } else {
2122     $PL_NOTEBOOK->set_current_page ($widget);
2123     $PL_WINDOW->show;
2124     }
2125     }
2126    
2127     sub make_playerbook {
2128     my $plwin = $PL_WINDOW = new DC::UI::Toplevel
2129     x => "center",
2130     y => "center",
2131     force_w => $WIDTH * 9/10,
2132     force_h => $HEIGHT * 9/10,
2133     title => "Player",
2134     name => "playerbook",
2135     has_close_button => 1
2136     ;
2137    
2138     my $ntb =
2139     $PL_NOTEBOOK =
2140     new DC::UI::Notebook expand => 1;
2141    
2142     $ntb->add_tab (
2143     "Statistics (F2)" => $STATS_PAGE = stats_window,
2144     "Shows statistics, where all your Stats and Resistances are shown."
2145     );
2146     $ntb->add_tab (
2147     "Skills (F3)" => $SKILL_PAGE = skill_window,
2148     "Shows all your Skills."
2149     );
2150    
2151     my $spellsw = $SPELL_PAGE = new DC::UI::ScrolledWindow (expand => 1, scroll_y => 1);
2152     $spellsw->add ($SPELL_LIST = new DC::UI::SpellList);
2153     $ntb->add_tab (
2154     "Spellbook (F4)" => $spellsw,
2155     "Displays all spells you have and lets you edit keyboard shortcuts for them."
2156     );
2157     $ntb->add_tab (
2158     "Inventory (F5)" => $INVENTORY_PAGE = inventory_widget,
2159     "Toggles the inventory window, where you can manage your loot (or treasures :). "
2160     . "You can also hit the <b>Tab</b>-key to show/hide the Inventory."
2161     );
2162     $ntb->add_tab (Pickup => $PICKUP_PAGE = autopickup_setup,
2163     "Configure autopickup settings, i.e. which items you will pick up automatically when walking (or running) over them.");
2164    
2165     $ntb->add_tab (Media => media_window,
2166     "License, Author and Source info for media sent by the server.");
2167    
2168     $ntb->set_current_page ($INVENTORY_PAGE);
2169    
2170     $plwin->add ($ntb);
2171     }
2172    
2173     sub keyboard_setup {
2174     DC::Macro::keyboard_setup
2175     }
2176    
2177     sub make_help_window {
2178     my $win = new DC::UI::Toplevel
2179     x => 'center',
2180     y => 'center',
2181     z => 4,
2182     name => 'doc_browser',
2183     force_w => int $WIDTH * 7/8,
2184     force_h => int $HEIGHT * 7/8,
2185     title => "Help Browser",
2186     has_close_button => 1;
2187    
2188     $win->add (my $vbox = new DC::UI::VBox);
2189    
2190     $vbox->add (new DC::UI::FancyFrame
2191     label => "Navigation",
2192     child => (my $buttons = new DC::UI::HBox),
2193     );
2194     $vbox->add (my $viewer = new DC::UI::TextScroller
2195     expand => 1, fontsize => 0.8, padding_x => 4, padding_y => 4);
2196    
2197     my @history;
2198     my @future;
2199     my $curnode;
2200    
2201     my $load_node; $load_node = sub {
2202     my ($node, $para) = @_;
2203    
2204     $buttons->clear;
2205    
2206     $buttons->add (new DC::UI::Button
2207     text => "⇤",
2208     tooltip => "back to the starting page",
2209     on_activate => sub {
2210     unshift @future, [$curnode, $viewer->current_paragraph] if $curnode;
2211     unshift @future, @history;
2212     @history = ();
2213     $load_node->(@{shift @future});
2214     },
2215     );
2216    
2217     if (@history) {
2218     $buttons->add (new DC::UI::Button
2219     text => "⋘",
2220     tooltip => "back to <i>" . (DC::asxml DC::Pod::full_path $history[-1][0]) . "</i>",
2221     on_activate => sub {
2222     unshift @future, [$curnode, $viewer->current_paragraph] if $curnode;
2223     $load_node->(@{pop @history});
2224     },
2225     );
2226     }
2227    
2228     if (@future) {
2229     $buttons->add (new DC::UI::Button
2230     text => "⋙",
2231     tooltip => "forward to <i>" . (DC::asxml DC::Pod::full_path $future[0][0]) . "</i>",
2232     on_activate => sub {
2233     push @history, [$curnode, $viewer->current_paragraph];
2234     $load_node->(@{shift @future});
2235     },
2236     );
2237     }
2238    
2239     $buttons->add (new DC::UI::Label text => " ");
2240    
2241     my @path = DC::Pod::full_path_of $node;
2242     pop @path; # drop current node
2243    
2244     for my $node (@path) {
2245     $buttons->add (new DC::UI::Button
2246     text => $node->[DC::Pod::N_KW][0],
2247     tooltip => "go to <i>" . (DC::asxml DC::Pod::full_path $node) . "</i>",
2248     on_activate => sub {
2249     push @history, [$curnode, $viewer->current_paragraph] if $curnode; @future = ();
2250     $load_node->($node);
2251     },
2252     );
2253     $buttons->add (new DC::UI::Label text => "/");
2254     }
2255    
2256     $buttons->add (new DC::UI::Label text => $node->[DC::Pod::N_KW][0], padding_x => 4, padding_y => 4);
2257    
2258     $curnode = $node;
2259    
2260     $viewer->clear;
2261     $viewer->add_paragraph (DC::Pod::as_paragraphs DC::Pod::section_of $curnode);
2262     $viewer->scroll_to ($para);
2263     };
2264    
2265     $load_node->(DC::Pod::find pod => "mainpage");
2266    
2267     $DC::Pod::goto_document = sub {
2268     my (@path) = @_;
2269    
2270     push @history, [$curnode, $viewer->current_paragraph] if $curnode; @future = ();
2271    
2272     $load_node->((DC::Pod::find @path)[0]);
2273     $win->show;
2274     };
2275    
2276     $HELP_WINDOW = $win;
2277     }
2278    
2279     sub open_quit_dialog {
2280     unless ($QUIT_DIALOG) {
2281     $QUIT_DIALOG = new DC::UI::Toplevel
2282     x => "center",
2283     y => "center",
2284     z => 50,
2285     title => "Really Quit?",
2286     on_key_down => sub {
2287     my ($dialog, $ev) = @_;
2288     $ev->{sym} == 27 and $dialog->hide;
2289     }
2290     ;
2291    
2292     $QUIT_DIALOG->add (my $vb = new DC::UI::VBox expand => 1);
2293    
2294     $vb->add (new DC::UI::Label
2295     text => "You should find a savebed and apply it first!",
2296     max_w => $WIDTH * 0.25,
2297     ellipsize => 0,
2298     );
2299     $vb->add (my $hb = new DC::UI::HBox expand => 1);
2300     $hb->add (new DC::UI::Button
2301     text => "Ok",
2302     expand => 1,
2303     on_activate => sub { $QUIT_DIALOG->hide; 0 },
2304     );
2305     $hb->add (new DC::UI::Button
2306     text => "Quit anyway",
2307     expand => 1,
2308     on_activate => sub {
2309     crash "Quit anyway";
2310     EV::break EV::BREAK_ALL;
2311     },
2312     );
2313     }
2314    
2315     $QUIT_DIALOG->show;
2316     $QUIT_DIALOG->grab_focus;
2317     }
2318    
2319     sub make_menubar {
2320     $MENUFRAME = new DC::UI::Toplevel
2321     border => 0,
2322     force_x => 0,
2323     force_y => 0,
2324     force_w => $::WIDTH,
2325     child => ($MENUBAR = new DC::UI::HBox),
2326     ;
2327    
2328     $MENUBAR->add ($BUTTONBAR = new DC::UI::Buttonbar);
2329    
2330     # XXX: this has to be done before make_stats_window as make_stats_window calls update_stats_window which updated the gauges also X-D
2331     make_gauge_window->show;
2332    
2333     # $BUTTONBAR->add (new DC::UI::Flopper text => "Message Window", other => $MESSAGE_WINDOW,
2334     # tooltip => "Toggles the server message log, where the client collects <i>all</i> messages from the server.");
2335    
2336     make_playerbook;
2337    
2338     $MENUPOPUP = DC::UI::Menu->new (items => [
2339     ["Setup…\tF9" , sub { $SETUP_DIALOG->toggle_visibility }],
2340     ["Playerbook…\tTab" , sub { $PL_WINDOW ->toggle_visibility }],
2341     ["…Statistics\tF2" , sub { toggle_player_page ($::STATS_PAGE) }],
2342     ["…Skills\tF3" , sub { toggle_player_page ($::SKILL_PAGE) }],
2343     ["…Spells\tF4" , sub { toggle_player_page ($::SPELL_PAGE) }],
2344     ["…Inventory\tF5" , sub { toggle_player_page ($::INVENTORY_PAGE) }],
2345     ["Help Browser…\tF1" , sub { $HELP_WINDOW ->toggle_visibility }],
2346     ["Quit…" , sub {
2347     if ($CONN) {
2348     open_quit_dialog;
2349     } else {
2350     EV::unloop EV::UNLOOP_ALL;
2351     }
2352     }],
2353     ]);
2354    
2355     $BUTTONBAR->add (new DC::UI::Button text => "Menu…",
2356     tooltip => "Shows the main menu",
2357     on_button_down => sub {
2358     my ($self, $ev) = @_;
2359     local $ev->{x} = 0;
2360     local $ev->{y} = 0;
2361     $MENUPOPUP->popup ($ev);
2362     },
2363     );
2364    
2365     $MENUBAR->add ($GAUGES->{exp} = new DC::UI::ExperienceProgress
2366     padding_x => 6,
2367     padding_y => 3,
2368     tooltip => "This progress bar shows your overall experience and your progress towards the next character level.",
2369     template => " Exp: 888,888,888,888 (lvl 188) ",
2370     );
2371    
2372     $MENUBAR->add ($PICKUP_ENABLE = new DC::UI::CheckBox # checkbox bad, button better?
2373     tooltip => "Automatic Pickup Enable - when this checkbox is enabled, then your character "
2374     . "will automatically pick up items as defined by your item pickup settings "
2375     . "in the playerbook. Often (e.g. in apartments) you want to temporarily "
2376     . "disable autopickup by disabling this checkbox.",
2377     state => $CFG->{pickup} & PICKUP_INHIBIT ? 0 : 1,
2378     on_changed => sub {
2379     my ($self, $value) = @_;
2380     $CFG->{pickup} &= ~PICKUP_INHIBIT;
2381     $CFG->{pickup} |= PICKUP_INHIBIT unless $_[1];
2382     $CONN->send_pickup ($CFG->{pickup})
2383     if $CONN;
2384     },
2385     );
2386    
2387     $MENUBAR->add ($GAUGES->{skillexp} = new DC::UI::ExperienceProgress
2388     c_rescale => 1,
2389     padding_x => 6,
2390     padding_y => 3,
2391     force_w => $::WIDTH * 0.2,
2392     tooltip => "This progress bar shows the currently used skill and your progress towards the next skill level of that skill.",
2393     template => "two handed weapons 99%",
2394     );
2395    
2396     $MENUBAR->add ($GAUGES->{range} = new DC::UI::Label
2397     expand => 1,
2398     align => 1, can_hover => 1, can_events => 1,
2399     text => "Range and Combat Slots",
2400     tooltip => "#stat_ranged",
2401     );
2402    
2403     $MENUFRAME->show;
2404     }
2405    
2406     sub open_string_query {
2407     my ($title, $cb, $txt, $tooltip) = @_;
2408     my $dialog = new DC::UI::Toplevel
2409     x => "center",
2410     y => "center",
2411     z => 50,
2412     force_w => $WIDTH * 4/5,
2413     title => $title;
2414    
2415     $dialog->add (
2416     my $e = new DC::UI::Entry
2417     on_activate => sub { $cb->(@_); $dialog->hide; 0 },
2418     on_key_down => sub { $_[1]->{sym} == 27 and $dialog->hide; 0 },
2419     tooltip => $tooltip
2420     );
2421    
2422     $e->grab_focus;
2423     $e->set_text ($txt) if $txt;
2424     $dialog->show;
2425     }
2426    
2427     sub show_tip_of_the_day {
2428     # find all tips
2429     my @tod = DC::Pod::find tip_of_the_day => "*";
2430    
2431     DC::DB::get state => "tip_of_the_day", sub {
2432     my ($todindex) = @_;
2433     $todindex = 0 if $todindex >= @tod;
2434     DC::DB::put state => tip_of_the_day => $todindex + 1, sub { };
2435    
2436     # create dialog
2437     my $dialog;
2438    
2439     my $close = sub {
2440     $dialog->destroy;
2441     };
2442    
2443     $dialog = new DC::UI::Toplevel
2444     x => "center",
2445     y => "center",
2446     z => 3,
2447     name => 'tip_of_the_day',
2448     force_w => int $WIDTH * 4/9,
2449     force_h => int $WIDTH * 2/9,
2450     title => "Tip of the day #" . (1 + $todindex),
2451     child => my $vbox = new DC::UI::VBox,
2452     has_close_button => 1,
2453     on_delete => $close,
2454     ;
2455    
2456     $vbox->add (my $viewer = new DC::UI::TextScroller
2457     expand => 1, fontsize => 0.8, padding_x => 4, padding_y => 4);
2458     $viewer->add_paragraph (DC::Pod::as_paragraphs DC::Pod::section_of $tod[$todindex]);
2459    
2460     $vbox->add (my $table = new DC::UI::Table col_expand => [0, 1]);
2461    
2462     $table->add_at (0, 0, new DC::UI::Button
2463     text => "Close",
2464     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>.",
2465     on_activate => $close,
2466     );
2467    
2468     $table->add_at (2, 0, new DC::UI::Button
2469     text => "Next",
2470     tooltip => "Show the next <b>Tip of the day</b>.",
2471     on_activate => sub {
2472     $close->();
2473     &show_tip_of_the_day;
2474     },
2475     );
2476    
2477     $dialog->show;
2478     };
2479     }
2480    
2481     sub video_init {
2482     DC::set_theme $CFG->{uitheme};
2483    
2484     DC::SDL_InitSubSystem DC::SDL_INIT_VIDEO if $SDL_REINIT;
2485     $SDL_REINIT = 0;
2486    
2487     @SDL_MODES = DC::SDL_ListModes 8, $CFG->{disable_alpha} ? 0 : 8;
2488     @SDL_MODES = DC::SDL_ListModes 8, 8 unless @SDL_MODES;
2489     @SDL_MODES = DC::SDL_ListModes 5, 0 unless @SDL_MODES;
2490     @SDL_MODES or DC::fatal "Unable to find a usable video mode\n(hardware accelerated opengl fullscreen)";
2491    
2492     @SDL_MODES = sort { $a->[0] * $a->[1] <=> $b->[0] * $b->[1] } @SDL_MODES;
2493    
2494     if (!defined $CFG->{sdl_mode} or $CFG->{sdl_mode} > $#SDL_MODES) {
2495     $CFG->{sdl_mode} = 0; # lowest resolution by default
2496    
2497     # now choose biggest mode <= 1024x768
2498     for (0 .. $#SDL_MODES) {
2499     if ($SDL_MODES[$_][0] * $SDL_MODES[$_][1] <= 1024 * 768) {
2500     $CFG->{sdl_mode} = $_;
2501     }
2502     }
2503     }
2504    
2505     my ($old_w, $old_h) = ($WIDTH, $HEIGHT);
2506    
2507     ($WIDTH, $HEIGHT, my ($rgb, $alpha)) = @{ $SDL_MODES[$CFG->{sdl_mode}] };
2508     $FULLSCREEN = $CFG->{fullscreen};
2509     $FAST = $CFG->{fast};
2510    
2511     # due to mac os x braindamage, we simply retry with !fullscreen in case of an error
2512     DC::SDL_SetVideoMode $WIDTH, $HEIGHT, $rgb, $alpha, $FULLSCREEN
2513     or DC::SDL_SetVideoMode $WIDTH, $HEIGHT, $rgb, $alpha, !$FULLSCREEN
2514     or die "SDL_SetVideoMode failed: " . (DC::SDL_GetError) . "\n";
2515    
2516     $SDL_ACTIVE = 1;
2517     $LAST_REFRESH = time - 0.01;
2518    
2519     DC::OpenGL::init;
2520     DC::Macro::init;
2521    
2522     $FONTSIZE = int $HEIGHT / 40 * $CFG->{gui_fontsize};
2523    
2524     $DC::UI::ROOT->configure (0, 0, $WIDTH, $HEIGHT);#d#
2525    
2526     #############################################################################
2527    
2528     if ($DEBUG_STATUS) {
2529     DC::UI::rescale_widgets $WIDTH / $old_w, $HEIGHT / $old_h;
2530     } else {
2531     # create/configure the widgets
2532    
2533     $DC::UI::ROOT->connect (key_down => sub {
2534     my (undef, $ev) = @_;
2535    
2536     if (my @macros = DC::Macro::find $ev) {
2537     DC::Macro::execute $_ for @macros;
2538    
2539     return 1;
2540     }
2541    
2542     0
2543     });
2544    
2545     $DEBUG_STATUS = new DC::UI::Label
2546     padding => 0,
2547     z => 100,
2548     force_x => "max",
2549     force_y => 20;
2550     $DEBUG_STATUS->show;
2551    
2552     $STATUSBOX = new DC::UI::Statusbox;
2553    
2554     $MODBOX = new DC::UI::Label
2555     can_events => 1,
2556     can_hover => 1,
2557     markup => "",
2558     align => 0,
2559     font => $FONT_FIXED,
2560     tooltip => "#modifier_box",
2561     tooltip_width => 0.67,
2562     ;
2563    
2564     update_modbox;
2565    
2566     (new DC::UI::Frame
2567     bg => [0, 0, 0, 0.4],
2568     force_x => 0,
2569     force_y => "max",
2570     child => (my $LL = new DC::UI::VBox),
2571     )->show;
2572    
2573     $LL->add ($STATUSBOX);
2574     $LL->add ($MODBOX);
2575     $LL->add (new DC::UI::Label
2576     align => 0,
2577     markup => "Use <b>Alt-Enter</b> to toggle fullscreen mode",
2578     fontsize => 0.5,
2579     fg => [1, 1, 0, 0.7],
2580     );
2581    
2582     DC::UI::Toplevel->new (
2583     title => "Minimap",
2584     name => "mapmap",
2585     x => 0,
2586     y => $::FONTSIZE + 8,#d# hack to move messages window below the menubar
2587     border_bg => [1, 1, 1, 192/255],
2588     bg => [1, 1, 1, 0],
2589     child => ($MAPMAP = new DC::MapWidget::MapMap
2590     tooltip => "<b>Minimap</b>. This will display an overview of the surrounding areas.",
2591     ),
2592     )->show;
2593    
2594     $MAPWIDGET = new DC::MapWidget;
2595     $MAPWIDGET->connect (activate_console => sub {
2596     my ($mapwidget, $preset) = @_;
2597    
2598     $MESSAGE_DIST->activate_console ($preset)
2599     if $MESSAGE_DIST;
2600     });
2601     $MAPWIDGET->show;
2602     $MAPWIDGET->grab_focus;
2603    
2604     $COMPLETER = new DC::MapWidget::Command::
2605     command => { },
2606     tooltip => "#completer_help",
2607     ;
2608    
2609     $SETUP_DIALOG = new DC::UI::Toplevel
2610     title => "Setup",
2611     name => "setup_dialog",
2612     x => 'center',
2613     y => 'center',
2614     z => 2,
2615     force_w => $::WIDTH * 0.6,
2616     force_h => $::HEIGHT * 0.6,
2617     has_close_button => 1,
2618     ;
2619    
2620     $METASERVER = metaserver_dialog;
2621     # the name is changed to not conflict with the older name as users could have hidden it
2622     $MESSAGE_WINDOW = new DC::UI::Dockbar
2623     name => "message_window2",
2624     title => 'Messages',
2625     y => $::FONTSIZE + 8,#d# hack to move messages window below the menubar
2626     force_w => $::WIDTH * 0.6,
2627     force_h => $::HEIGHT * 0.25,
2628     ;
2629    
2630     $MESSAGE_DIST = new DC::MessageDistributor dockbar => $MESSAGE_WINDOW;
2631    
2632     $SETUP_DIALOG->add ($SETUP_NOTEBOOK = new DC::UI::Notebook expand => 1,
2633     filter => new DC::UI::ScrolledWindow expand => 1, scroll_y => 1);
2634    
2635     $SETUP_NOTEBOOK->add_tab (Login => $SETUP_LOGIN = login_setup,
2636     "Configure the server to play on, your username and password.");
2637     $SETUP_NOTEBOOK->add_tab (Server => $SETUP_SERVER = server_setup,
2638     "Configure other server related options.");
2639     $SETUP_NOTEBOOK->add_tab (Client => client_setup,
2640     "Configure various client-specific settings.");
2641     $SETUP_NOTEBOOK->add_tab (Graphics => graphics_setup,
2642     "Configure the video mode, performance, fonts and other graphical aspects of the game.");
2643     $SETUP_NOTEBOOK->add_tab (Audio => audio_setup,
2644     "Configure the use of audio, sound effects and background music.");
2645     $SETUP_NOTEBOOK->add_tab (Keyboard => $SETUP_KEYBOARD = keyboard_setup,
2646     "Lets you define, edit and delete key bindings."
2647     . "There is a shortcut for making bindings: <b>Control-Insert</b> opens the binding editor "
2648     . "with nothing set and the recording started. After doing the actions you "
2649     . "want to record press <b>Insert</b> and you will be asked to press a key-combo. "
2650     . "After pressing the combo the binding will be saved automatically and the "
2651     . "binding editor closes");
2652     $SETUP_NOTEBOOK->add_tab (Debug => debug_setup,
2653     "Some debuggin' options. Do not ask.");
2654    
2655     make_help_window;
2656     make_menubar;
2657    
2658     $SETUP_DIALOG->show;
2659     $MESSAGE_WINDOW->show;
2660     }
2661    
2662     $MODE_SLIDER->set_range ([$CFG->{sdl_mode}, 0, scalar @SDL_MODES, 1, 1]);
2663     $MODE_SLIDER->emit (changed => $CFG->{sdl_mode});
2664    
2665     $CAVEAT_LABEL->set_text ("None :)");
2666     $CAVEAT_LABEL->set_text ("Apple/NVIDIA Texture bug (slow)")
2667     if $DC::OpenGL::APPLE_NVIDIA_BUG;
2668     $CAVEAT_LABEL->set_text ("Software Rendering (very slow)")
2669     unless DC::SDL_GL_GetAttribute DC::SDL_GL_ACCELERATED_VISUAL;
2670    
2671     $STATUSBOX->add ("Set video mode $WIDTH×$HEIGHT", timeout => 10, fg => [1, 1, 1, 0.5]);
2672     }
2673    
2674     sub video_shutdown {
2675     DC::OpenGL::shutdown;
2676     DC::SDL_QuitSubSystem DC::SDL_INIT_VIDEO if $SDL_REINIT;
2677    
2678     undef $SDL_ACTIVE;
2679     }
2680    
2681     my %animate_object;
2682     my $animate_timer;
2683    
2684     my $fps = 9;
2685    
2686     sub force_refresh {
2687     if ($DELIANTRA_DEBUG & 4) {
2688     $fps = $fps * 0.98 + 1 / (($NOW - $LAST_REFRESH) || 0.1) * 0.02;
2689     debug sprintf "%3.2f", $fps;
2690     }
2691    
2692     undef $WANT_REFRESH;
2693     $_[0]->stop;
2694    
2695     $DC::UI::ROOT->draw;
2696     DC::SDL_GL_SwapBuffers;
2697     $LAST_REFRESH = $NOW;
2698     }
2699    
2700     my $want_refresh = EV::prepare_ns \&force_refresh;
2701    
2702     our $INPUT_WATCHER = EV::periodic 0, 1 / $MAX_FPS, undef, sub {
2703     $NOW = EV::now;
2704    
2705     ($SDL_CB[$_->{type}] || sub { warn "unhandled event $_->{type}" })->($_)
2706 root 1.2 for DC::peep_events;
2707 root 1.1
2708     if (%animate_object) {
2709     $_->animate ($LAST_REFRESH - $NOW) for values %animate_object;
2710     $WANT_REFRESH = 1;
2711     }
2712    
2713     $want_refresh->start
2714     if $WANT_REFRESH;
2715     };
2716    
2717     sub animation_start {
2718     my ($widget) = @_;
2719     $animate_object{$widget} = $widget;
2720     }
2721    
2722     sub animation_stop {
2723     my ($widget) = @_;
2724     delete $animate_object{$widget};
2725     }
2726    
2727     $SDL_CB[DC::SDL_QUIT] = sub {
2728     crash "SDL_QUIT";
2729     EV::unloop EV::UNLOOP_ALL;
2730     };
2731     $SDL_CB[DC::SDL_VIDEORESIZE] = sub { };
2732     $SDL_CB[DC::SDL_VIDEOEXPOSE] = sub {
2733     DC::UI::full_refresh;
2734     };
2735     $SDL_CB[DC::SDL_ACTIVEEVENT] = sub {
2736     # not useful, as APPACTIVE includes only iconified state, not unmapped
2737     # printf "active %x %x %x\n", $_[0]{gain}, $_[0]{state}, DC::SDL_GetAppState;#d#
2738     # printf "a %x\n", DC::SDL_GetAppState & DC::SDL_APPACTIVE;#d#
2739     # printf "A\n" if $_[0]{state} & DC::SDL_APPACTIVE;
2740     # printf "K\n" if $_[0]{state} & DC::SDL_APPINPUTFOCUS;
2741     # printf "M\n" if $_[0]{state} & DC::SDL_APPMOUSEFOCUS;
2742     };
2743     $SDL_CB[DC::SDL_KEYDOWN] = sub {
2744     if ($_[0]{mod} & DC::KMOD_ALT && $_[0]{sym} == 13) {
2745     # alt-enter
2746     video_shutdown;
2747     $FULLSCREEN_ENABLE->toggle;
2748     video_init;
2749     } else {
2750     &DC::UI::feed_sdl_key_down_event;
2751     }
2752     update_modbox;
2753     };
2754     $SDL_CB[DC::SDL_KEYUP] = sub {
2755     &DC::UI::feed_sdl_key_up_event;
2756     update_modbox;
2757     };
2758     $SDL_CB[DC::SDL_MOUSEMOTION] = \&DC::UI::feed_sdl_motion_event,
2759     $SDL_CB[DC::SDL_MOUSEBUTTONDOWN] = \&DC::UI::feed_sdl_button_down_event,
2760     $SDL_CB[DC::SDL_MOUSEBUTTONUP] = \&DC::UI::feed_sdl_button_up_event,
2761     $SDL_CB[DC::SDL_USEREVENT] = sub {
2762     if ($_[0]{code} == 1) {
2763     audio_channel_finished $_[0]{data1};
2764     } elsif ($_[0]{code} == 0) {
2765     audio_music_finished;
2766     }
2767     };
2768    
2769     #############################################################################
2770    
2771     $SIG{INT} = $SIG{TERM} = sub {
2772     EV::unloop;
2773     #d# TODO calling exit here hangs the process in some futex
2774     };
2775    
2776     # due to mac os x + sdl combined braindamage, we need this contortion
2777     sub DC::Main::main {
2778     {
2779     DC::Pod::load_docwiki DC::find_rcfile "docwiki.pst";
2780    
2781     if (-e "$Deliantra::VARDIR/client.cf") {
2782     DC::read_cfg "$Deliantra::VARDIR/client.cf";
2783     } else {
2784     #TODO: compatibility cruft
2785     DC::read_cfg "$Deliantra::OLDDIR/cfplusrc";
2786     print STDERR "INFO: used old configuration file\n";
2787     }
2788    
2789     DC::DB::Server::run;
2790    
2791     if ($CFG->{db_schema} < 1) {
2792     warn "INFO: upgrading database schema from 0 to 1, mapcache and tilecache will be lost\n";
2793     DC::DB::nuke_db;
2794     $CFG->{db_schema} = 1;
2795     DC::write_cfg;
2796     }
2797    
2798     DC::DB::open_db;
2799    
2800     DC::UI::set_layout ($::CFG->{layout});
2801    
2802     my %DEF_CFG = (
2803     config_autosave => 1,
2804     sdl_mode => undef,
2805     fullscreen => 1,
2806     fast => 0,
2807     force_opengl11 => undef,
2808     disable_alpha => 0,
2809     smooth_movement => 1,
2810     smooth_transitions => 1,
2811     texture_compression => 1,
2812     map_scale => 1,
2813     fow_enable => 1,
2814     fow_intensity => 0,
2815     fow_texture => 0,
2816     map_smoothing => 1,
2817     gui_fontsize => 1,
2818     log_fontsize => 0.7,
2819     gauge_fontsize => 1,
2820     gauge_size => 0.35,
2821     stat_fontsize => 0.7,
2822     mapsize => 100,
2823     audio_enable => 1,
2824     audio_hw_channels => 0,
2825     audio_hw_frequency => 0,
2826     audio_hw_chunksize => 0,
2827     audio_mix_channels => 8,
2828     effects_enable => 1,
2829     effects_volume => 1,
2830     bgm_enable => 1,
2831     bgm_volume => 0.5,
2832     output_rate => "",
2833     pickup => PICKUP_SPELLBOOK | PICKUP_SKILLSCROLL | PICKUP_VALUABLES,
2834     inv_sort => "mtime",
2835     default => "profile", # default profile
2836     show_tips => 1,
2837     logview_max_par => 1000,
2838     shift_fire_stop => 0,
2839     uitheme => "wood",
2840     map_shift_x => -24, # arbitrary
2841     map_shift_y => +24, # arbitrary
2842     );
2843    
2844     while (my ($k, $v) = each %DEF_CFG) {
2845     $CFG->{$k} = $v unless exists $CFG->{$k};
2846     }
2847    
2848     my @args = @ARGV;
2849    
2850     # OS X passes some process serial number of other shit. they
2851     # could have used an env var or any other sane mechanism. but
2852     # would it be os x then? no...
2853     shift @args if $args[0] =~ /^-psn_/;
2854    
2855     my $profile = 'default';
2856    
2857     for (my $i = 0; $i < @args; $i++) {
2858     if ($args[$i] =~ /^--?profile$/) {
2859     $profile = $args[$i + 1];
2860     splice @args, $i, 2, ();
2861     $i = 0;
2862     } elsif ($args[$i] =~ /^--?h/) {
2863     print STDERR "Usage: $0 [--profile name] [host [user [password]]]\n";
2864     exit 0;
2865     }
2866     }
2867    
2868     $CFG->{profile}{$profile} ||= {};
2869     $PROFILE = $CFG->{profile}{$profile};
2870     $PROFILE->{host} ||= "gameserver.deliantra.net";
2871    
2872     $PROFILE->{host} = $args[0] if @args > 0;
2873     $PROFILE->{user} = $args[1] if @args > 1;
2874     $PROFILE->{password} = $args[2] if @args > 2;
2875    
2876     # convert old bindings (only default profile matters)
2877     if (my $bindings = delete $PROFILE->{bindings}) {
2878     while (my ($mod, $syms) = each %$bindings) {
2879     while (my ($sym, $cmds) = each %$syms) {
2880     push @{ $PROFILE->{macro} }, {
2881     accelkey => [$mod*1, $sym*1],
2882     action => $cmds,
2883     };
2884     }
2885     }
2886     }
2887    
2888 root 1.3 # fontconfig doesn't support relative paths anymore, so use abs_path and keep fingers crossed
2889 root 1.4 # these are ignored under windows, for some reason, and thus set in the loader
2890 root 1.3 $ENV{FONTCONFIG_FILE} = "fonts.conf";
2891     $ENV{FONTCONFIG_PATH} = Cwd::abs_path DC::find_rcfile "fonts";
2892 root 1.4 $ENV{FONTCONFIG_DIR} = $ENV{FONTCONFIG_PATH}; # helps with older versions
2893 root 1.1
2894     {
2895     my @fonts = map DC::find_rcfile "fonts/$_", qw(
2896     DejaVuSans.ttf
2897     DejaVuSansMono.ttf
2898     DejaVuSans-Bold.ttf
2899     DejaVuSansMono-Bold.ttf
2900     DejaVuSans-Oblique.ttf
2901     DejaVuSansMono-Oblique.ttf
2902     DejaVuSans-BoldOblique.ttf
2903     DejaVuSansMono-BoldOblique.ttf
2904     mona.ttf
2905     );
2906    
2907     DC::add_font $_ for @fonts;
2908    
2909     $FONT_PROP = new_from_file DC::Font $fonts[0];
2910     $FONT_FIXED = new_from_file DC::Font $fonts[1];
2911    
2912     $FONT_PROP->make_default;
2913    
2914     DC::pango_init;
2915     }
2916    
2917     # compare mono (ft) vs. rgba (cairo)
2918     # ft - 1.8s, cairo 3s, even in alpha-only mode
2919     # for my $rgba (0..1) {
2920     # my $t1 = Time::HiRes::time;
2921     # for (1..1000) {
2922     # my $layout = DC::Layout->new ($rgba);
2923     # $layout->set_text ("hallo" x 100);
2924     # $layout->render;
2925     # }
2926     # my $t2 = Time::HiRes::time;
2927     # warn $t2-$t1;
2928     # }
2929    
2930     DC::IMG_Init; video_init;
2931     DC::Mix_Init; audio_init;
2932     }
2933    
2934     show_tip_of_the_day if $CFG->{show_tips};
2935    
2936     my $STARTUP_CANCEL; $STARTUP_CANCEL = EV::idle sub {
2937     undef $STARTUP_CANCEL;
2938     (pop @::STARTUP_DONE)->()
2939     while @::STARTUP_DONE;
2940     };
2941    
2942     debug_toggle 0;
2943    
2944     delete $SIG{__DIE__};
2945     EV::loop;
2946    
2947     DC::write_cfg if $CFG->{config_autosave};
2948    
2949     #video_shutdown;
2950     #audio_shutdown;
2951    
2952     DC::OpenGL::quit;
2953     DC::SDL_Quit;
2954     DC::DB::Server::stop;
2955     }
2956    
2957     *DC::Main::run = \&DC::SDL_braino; # see sub above
2958    
2959     1