--- deliantra/Deliantra-Client/DC/Main.pm 2011/12/27 09:17:27 1.2 +++ deliantra/Deliantra-Client/DC/Main.pm 2012/04/05 04:38:43 1.20 @@ -19,6 +19,8 @@ use common::sense; use Carp 'verbose'; +use Cwd (); +use Digest::MD5 (); use EV; BEGIN { *time = \&EV::time } @@ -33,6 +35,7 @@ use Compress::LZF; use JSON::XS; +use Urlader; use DC; @@ -42,13 +45,22 @@ BEGIN { $SIG{__DIE__} = sub { - return if $^S; + return if $^S; # quick reject + + # return if there are any eval contexts in the call stack + for my $i (0..999) { + my ($sub, $is_require) = (caller $i)[3, 7] + or last; + return if $sub eq "(eval)" && !$is_require; + } + crash "CRASH/DIE: $_[0]" => 1; DC::fatal Carp::longmess "$_[0]"; } } use DC::OpenGL (); +use DC::Audio (); use DC::Protocol; use DC::DB; use DC::UI; @@ -135,6 +147,7 @@ our $MUSIC_PLAYING_WIDGET; our $LICENSE_WIDGET; +our $DOWNLOADS_WIDGET; our $PICKUP_PAGE; our $INVENTORY_PAGE; @@ -187,7 +200,7 @@ $msg =~ s/\s+$//; # backtrace as second step, in case it crashes, too - crash Carp::longmess "$msg\nbacktrace, for client version $DC::VERSION, generated" + crash Carp::longmess "$msg\nbacktrace, for client version $DC::VERSION$Urlader::EXE_VER, generated" if $backtrace; }; @@ -199,7 +212,7 @@ return unless $CONN; $CONN->send_exti_msg (clientlog => $msg); - $CONN->send_exti_msg (clientlog => Carp::longmess "$msg\nbacktrace, for client version $DC::VERSION, generated") if $backtrace; + $CONN->send_exti_msg (clientlog => Carp::longmess "$msg\nbacktrace, for client version $DC::VERSION$Urlader::EXE_VER, generated") if $backtrace; } ############################################################################# @@ -464,25 +477,9 @@ sub audio_init { if ($CFG->{audio_enable}) { - if (length $CFG->{audio_driver}) { - local $ENV{SDL_AUDIODRIVER} = $CFG->{audio_driver}; - DC::SDL_Init DC::SDL_INIT_AUDIO - and die "SDL::Init failed!\n"; - } else { - DC::SDL_Init DC::SDL_INIT_AUDIO - and die "SDL::Init failed!\n"; - } - - $ENV{MIX_EFFECTSMAXSPEED} = 1; - $SDL_MIXER = !DC::Mix_OpenAudio - $CFG->{audio_hw_frequency}, - DC::MIX_DEFAULT_FORMAT, - $CFG->{audio_hw_channels}, - $CFG->{audio_hw_chunksize}; + DC::Audio::init $CFG->{audio_driver}; if ($SDL_MIXER) { - DC::Mix_AllocateChannels $CFG->{audio_mix_channels}; - audio_music_finished; } else { status "Unable to open sound device: there will be no sound"; @@ -517,6 +514,196 @@ } ############################################################################# +# Over-the-air updates + +sub ota_update { + my ($face, $size, $md5) = @_; + + my $coro = Coro::async_pool { + my $override = "$Urlader::EXE_DIR/override"; + + $MESSAGE_DIST->add_channel ({ + id => "ota_update", + title => "Update", + tooltip => "Software Update Log", + }); + + $MESSAGE_DIST->message ({ type => "ota_update", markup => "preparing override..." }); + + my $fh = Coro::AIO::aio_open "$override.tmp", IO::AIO::O_WRONLY | IO::AIO::O_CREAT | IO::AIO::O_TRUNC, 0777; + + unless ($fh) { + $MESSAGE_DIST->message ({ type => "ota_update", markup => (DC::asxml "unable to write software update:\n$Urlader::EXE_DIR/override.tmp:\n$!") }); + return; + } + + $MESSAGE_DIST->message ({ type => "ota_update", markup => "downloading $size bytes..." }); + + my $cv = AE::cv; + my $error; + + $cv->begin (Coro::rouse_cb); + $CONN->ask_face ( + $face, + -1000, + sub { + $STATUSBOX->add ( + (sprintf "update download: %d/%d", $size - $_[1], $size), + pri => -9, group => "ota_update", timeout => 60, fg => [1, 1, 0, 1] + ); + + $cv->begin; + my $len = length $_[2]; + IO::AIO::aio_write $fh, $_[1], $len, $_[2], undef, sub { + $error ||= $_[0] != $len; + $cv->end; + }; + }, + sub { + $cv->end; + }, + ); + + Coro::rouse_wait; + + $STATUSBOX->clr_group ("ota_update"); + + $error ||= Coro::AIO::aio_fsync $fh; + $error ||= Coro::AIO::aio_close $fh; + + if ($error) { + $MESSAGE_DIST->message ({ type => "ota_update", markup => "file write error, update aborted." }); + Coro::AIO::aio_unlink "$override.tmp"; + return; + } + + { + $MESSAGE_DIST->message ({ type => "ota_update", markup => "verifying update file..." }); + + my $fh = Coro::AIO::aio_open "$override.tmp", IO::AIO::O_RDONLY, 0; + + if ($fh) { + $error ||= Coro::AIO::aio_stat "$override.tmp"; + $error ||= -s _ != $size; + Coro::AIO::aio_readahead $fh, 0, $size; + + my $f_md5 = new Digest::MD5; + binmode $fh; # ugh :( + $f_md5->addfile ($fh); + $f_md5 = $f_md5->hexdigest; + $error ||= $md5 ne $f_md5; + } + } + + if ($error) { + $MESSAGE_DIST->message ({ type => "ota_update", markup => "verification failed, update aborted." }); + Coro::AIO::aio_unlink "$override.tmp"; + return; + } + + $MESSAGE_DIST->message ({ type => "ota_update", markup => "replacing override file..." }); + + if (Coro::AIO::aio_rename "$override.tmp", $override) { + $MESSAGE_DIST->message ({ type => "ota_update", markup => "unable to replace override file, update aborted." }); + Coro::AIO::aio_unlink "$override.tmp"; + } + + $MESSAGE_DIST->message ({ type => "ota_update", markup => "success - update becomes active after restarting." }); + }; + + $CONN->{ota_update} = Guard::guard { + $coro->cancel; + }; +} + +sub ota_update_ask { + my ($ok, $face, $ver, $size, $md5, $changes) = @_; + + $CONN->{w}{ota_dialog} = my $dialog = new DC::UI::Toplevel + x => "center", + y => "center", + z => 55, + force_w => $::WIDTH * 0.7, + force_h => $::HEIGHT * 0.7, + title => "Software update available", + child => my $vbox = new DC::UI::VBox, + ; + + $vbox->add (new DC::UI::Label + ellipsise => 0, + text => "The server offers a software update, " + . "do you want to start downloading this update in the background?", + ); + + $vbox->add (new DC::UI::FancyFrame + expand => 1, + label => "Details", + child => (new DC::UI::TextScroller + expand => 1, fontsize => 0.8, padding_x => 4, padding_y => 4, + par => [{ + markup => "Old revision: $Urlader::EXE_VER\n" + . "New revision: $ver\n" + . "Download size: $size bytes\n\n" + . "Changes:\n\n" + . DC::asxml $changes + }], + ), + ); + + $vbox->add (my $hbox = new DC::UI::HBox); + + $hbox->add (new DC::UI::Button + expand => 1, + text => "Not now", + on_activate => sub { + $dialog->destroy; + 0 + } + ); + $hbox->add (new DC::UI::Button + expand => 1, + text => "Yes, start downloading", + on_activate => sub { + $dialog->destroy; + ota_update $face, $size, $md5; + 0 + }, + ); + + $dialog->show; +} + +sub ota_update_check { + return unless defined $Urlader::EXE_ID; + + ::message { markup => "Checking for software update..." }; + + $CONN->send_exti_req (ota_update => $Urlader::URLADER_VERSION, $Urlader::EXE_ID, $Urlader::EXE_VER, sub { + my ($ok, $face, $ver, $size, $md5, $changes) = @_; + + if ($ok) { + if (defined $ver) { + ::message { markup => "Server offers version $ver (we are version $Urlader::EXE_VER)." }; + &ota_update_ask; + } else { + ::message { markup => "Server has no newer version." }; + } + } else { + ::message { markup => "Server does not support software update." }; + } + +# $self->register_face_handler ($exp_table, sub { +# my ($face) = @_; + +# $self->{exp_table} = $self->{json_coder}->decode (delete $face->{data}); +# $_->() for values %{ $self->{on_exp_update} || {} }; +# }); + + () + }); +} + +############################################################################# sub destroy_query_dialog { (delete $_[0]{query_dialog})->destroy @@ -766,7 +953,7 @@ c_version => { client => "deliantra", - clientver => $DC::VERSION, + clientver => "$DC::VERSION$Urlader::EXE_VER", gl_vendor => DC::OpenGL::gl_vendor, gl_version => DC::OpenGL::gl_version, }, @@ -785,6 +972,8 @@ if ($_[0]) { DC::lowdelay fileno $CONN->{fh}; + ota_update_check; + status "successfully connected to the server"; } else { undef $CONN; @@ -1133,7 +1322,7 @@ tooltip => "You can override the audio driver to use here. Leaving it empty will result " . "in Deliantra picking one automatically. GNU/Linux users often prefer specific " . "drivers though, and can experiment with alsa, dsp, esd, pulse, arts, nas " - . "or other system-specific drivers. Selecting the wrong driver here will simply result" + . "or other system-specific drivers. Selecting the wrong driver here will simply result " . "in no sound.", on_changed => sub { my ($self, $value) = @_; $CFG->{audio_driver} = $value; 1 } ); @@ -1828,7 +2017,7 @@ tooltip => "Use this to manually save configuration and UI layout when " . "autosave is disabled.", on_activate => sub { - DC::write_cfg; + DC::save_cfg; 0 } ); @@ -1843,12 +2032,12 @@ $table->add_at (1, $row++, new DC::UI::Label align => 0, text => $Deliantra::VARDIR, tooltip => ""); $table->add_at (0, $row , new DC::UI::Label align => 1, text => "Database Directory"); $table->add_at (1, $row++, new DC::UI::Label align => 0, text => $DC::DB::DBDIR, tooltip => ""); + $table->add_at (0, $row , new DC::UI::Label align => 1, text => "Urlader (Prebuilt)"); + $table->add_at (1, $row++, new DC::UI::Label align => 0, text => $ENV{URLADER_VERSION}, tooltip => ""); $table->add_at (0, $row , new DC::UI::Label align => 1, text => "Branch (Prebuilt)"); - $table->add_at (1, $row++, new DC::UI::Label align => 0, text => $::EXE_ID, tooltip => ""); - $table->add_at (0, $row , new DC::UI::Label align => 1, text => "Version (Prebuilt)"); - $table->add_at (1, $row++, new DC::UI::Label align => 0, text => $::EXE_VER, tooltip => ""); - $table->add_at (0, $row , new DC::UI::Label align => 1, text => "Update (Prebuilt)"); - $table->add_at (1, $row++, new DC::UI::Label align => 0, text => $::UPDPAR, tooltip => ""); + $table->add_at (1, $row++, new DC::UI::Label align => 0, text => $ENV{URLADER_EXE_ID}, tooltip => ""); + $table->add_at (0, $row , new DC::UI::Label align => 1, text => "Revision (Prebuilt)"); + $table->add_at (1, $row++, new DC::UI::Label align => 0, text => $ENV{URLADER_EXE_VER}, tooltip => ""); } $vbox @@ -2032,16 +2221,51 @@ my $vb = new DC::UI::VBox; $vb->add (new DC::UI::FancyFrame - label => "Currently playing music", + label => "Current background music", child => new DC::UI::ScrolledWindow scroll_x => 1, scroll_y => 0, child => ($MUSIC_PLAYING_WIDGET = new DC::UI::Label ellipsise => 0, fontsize => 0.8), ); $vb->add (new DC::UI::FancyFrame + label => "Current downloads", + child => ($DOWNLOADS_WIDGET = new DC::UI::Table + expand => 1, fontsize => 0.8, padding_x => 4, padding_y => 4), + ); + + $DOWNLOADS_WIDGET->connect (visibility_change => sub { + my ($self) = @_; + + delete $self->{updater}; + return unless $_[1]; + + $self->{updater} = AE::timer 0, 0.7, sub { + $self->clear; + + return unless $CONN; + + my @nums = sort { $b <=> $a } keys %{ $CONN->{ix_recv_buf} }; + return unless @nums; + + $self->add_at (0, 0, new DC::UI::Label align => 1, text => "Face"); + $self->add_at (1, 0, new DC::UI::Label align => 0, text => "Octets/Total"); + + for my $row (0 .. $#nums) { + my $num = $nums[$row]; + + my $total = length $CONN->{ix_recv_buf}{$num}; + my $got = $total - $CONN->{ix_recv_ofs}{$num}; + + $self->add_at (0, $row + 1, new DC::UI::Label align => 1, text => $num, tooltip => ""); + $self->add_at (1, $row + 1, new DC::UI::Label align => 0, text => "$got/$total", tooltip => ""); + } + }; + }); + + $vb->add (new DC::UI::FancyFrame label => "Other media used in this session", expand => 1, child => ($LICENSE_WIDGET = new DC::UI::TextScroller - expand => 1, fontsize => 0.8, padding_x => 4, padding_y => 4), + expand => 1, fontsize => 0.8, padding_x => 4, padding_y => 4), ); $vb @@ -2236,7 +2460,7 @@ $QUIT_DIALOG = new DC::UI::Toplevel x => "center", y => "center", - z => 50, + z => 60, title => "Really Quit?", on_key_down => sub { my ($dialog, $ev) = @_; @@ -2463,6 +2687,8 @@ $FULLSCREEN = $CFG->{fullscreen}; $FAST = $CFG->{fast}; + DC::SDL_WM_SetCaption "Deliantra MORPG Client $DC::VERSION$Urlader::EXE_VER", "Deliantra"; # must be after SDL_Init + # due to mac os x braindamage, we simply retry with !fullscreen in case of an error DC::SDL_SetVideoMode $WIDTH, $HEIGHT, $rgb, $alpha, $FULLSCREEN or DC::SDL_SetVideoMode $WIDTH, $HEIGHT, $rgb, $alpha, !$FULLSCREEN @@ -2729,142 +2955,97 @@ }; # due to mac os x + sdl combined braindamage, we need this contortion -sub DC::Main::main { - { - DC::Pod::load_docwiki DC::find_rcfile "docwiki.pst"; +sub DC::Main::run { + DC::SDL_main_hack { + { + DC::Pod::load_docwiki DC::find_rcfile "docwiki.pst"; - if (-e "$Deliantra::VARDIR/client.cf") { - DC::read_cfg "$Deliantra::VARDIR/client.cf"; - } else { - #TODO: compatibility cruft - DC::read_cfg "$Deliantra::OLDDIR/cfplusrc"; - print STDERR "INFO: used old configuration file\n"; - } - - DC::DB::Server::run; - - if ($CFG->{db_schema} < 1) { - warn "INFO: upgrading database schema from 0 to 1, mapcache and tilecache will be lost\n"; - DC::DB::nuke_db; - $CFG->{db_schema} = 1; - DC::write_cfg; - } - - DC::DB::open_db; - - DC::UI::set_layout ($::CFG->{layout}); - - my %DEF_CFG = ( - config_autosave => 1, - sdl_mode => undef, - fullscreen => 1, - fast => 0, - force_opengl11 => undef, - disable_alpha => 0, - smooth_movement => 1, - smooth_transitions => 1, - texture_compression => 1, - map_scale => 1, - fow_enable => 1, - fow_intensity => 0, - fow_texture => 0, - map_smoothing => 1, - gui_fontsize => 1, - log_fontsize => 0.7, - gauge_fontsize => 1, - gauge_size => 0.35, - stat_fontsize => 0.7, - mapsize => 100, - audio_enable => 1, - audio_hw_channels => 0, - audio_hw_frequency => 0, - audio_hw_chunksize => 0, - audio_mix_channels => 8, - effects_enable => 1, - effects_volume => 1, - bgm_enable => 1, - bgm_volume => 0.5, - output_rate => "", - pickup => PICKUP_SPELLBOOK | PICKUP_SKILLSCROLL | PICKUP_VALUABLES, - inv_sort => "mtime", - default => "profile", # default profile - show_tips => 1, - logview_max_par => 1000, - shift_fire_stop => 0, - uitheme => "wood", - map_shift_x => -24, # arbitrary - map_shift_y => +24, # arbitrary - ); - - while (my ($k, $v) = each %DEF_CFG) { - $CFG->{$k} = $v unless exists $CFG->{$k}; - } - - my @args = @ARGV; - - # OS X passes some process serial number of other shit. they - # could have used an env var or any other sane mechanism. but - # would it be os x then? no... - shift @args if $args[0] =~ /^-psn_/; - - my $profile = 'default'; - - for (my $i = 0; $i < @args; $i++) { - if ($args[$i] =~ /^--?profile$/) { - $profile = $args[$i + 1]; - splice @args, $i, 2, (); - $i = 0; - } elsif ($args[$i] =~ /^--?h/) { - print STDERR "Usage: $0 [--profile name] [host [user [password]]]\n"; - exit 0; - } - } - - $CFG->{profile}{$profile} ||= {}; - $PROFILE = $CFG->{profile}{$profile}; - $PROFILE->{host} ||= "gameserver.deliantra.net"; - - $PROFILE->{host} = $args[0] if @args > 0; - $PROFILE->{user} = $args[1] if @args > 1; - $PROFILE->{password} = $args[2] if @args > 2; - - # convert old bindings (only default profile matters) - if (my $bindings = delete $PROFILE->{bindings}) { - while (my ($mod, $syms) = each %$bindings) { - while (my ($sym, $cmds) = each %$syms) { - push @{ $PROFILE->{macro} }, { - accelkey => [$mod*1, $sym*1], - action => $cmds, - }; + DC::load_cfg; + DC::upgrade_cfg; + + DC::Audio::probe; + + DC::DB::Server::run; + + if ($CFG->{db_schema} < 1) { + warn "INFO: upgrading database schema from 0 to 1, mapcache and tilecache will be lost\n"; + DC::DB::nuke_db; + $CFG->{db_schema} = 1; + DC::save_cfg; + } + + DC::DB::open_db; + + DC::UI::set_layout ($::CFG->{layout}); + + my @args = @ARGV; + + # OS X passes some process serial number of other shit. they + # could have used an env var or any other sane mechanism. but + # would it be os x then? no... + shift @args if $args[0] =~ /^-psn_/; + + my $profile = 'default'; + + for (my $i = 0; $i < @args; $i++) { + if ($args[$i] =~ /^--?profile$/) { + $profile = $args[$i + 1]; + splice @args, $i, 2, (); + $i = 0; + } elsif ($args[$i] =~ /^--?h/) { + print STDERR "Usage: $0 [--profile name] [host [user [password]]]\n"; + exit 0; } } - } - $ENV{FONTCONFIG_FILE} = DC::find_rcfile "fonts/fonts.conf"; - $ENV{FONTCONFIG_DIR} = DC::find_rcfile "fonts"; + $CFG->{profile}{$profile} ||= {}; + $PROFILE = $CFG->{profile}{$profile}; + $PROFILE->{host} ||= "gameserver.deliantra.net"; + + $PROFILE->{host} = $args[0] if @args > 0; + $PROFILE->{user} = $args[1] if @args > 1; + $PROFILE->{password} = $args[2] if @args > 2; + + # convert old bindings (only default profile matters) + if (my $bindings = delete $PROFILE->{bindings}) { + while (my ($mod, $syms) = each %$bindings) { + while (my ($sym, $cmds) = each %$syms) { + push @{ $PROFILE->{macro} }, { + accelkey => [$mod*1, $sym*1], + action => $cmds, + }; + } + } + } - { - my @fonts = map DC::find_rcfile "fonts/$_", qw( - DejaVuSans.ttf - DejaVuSansMono.ttf - DejaVuSans-Bold.ttf - DejaVuSansMono-Bold.ttf - DejaVuSans-Oblique.ttf - DejaVuSansMono-Oblique.ttf - DejaVuSans-BoldOblique.ttf - DejaVuSansMono-BoldOblique.ttf - mona.ttf - ); + # fontconfig doesn't support relative paths anymore, so use abs_path and keep fingers crossed + # these are ignored under windows, for some reason, and thus set in the loader + $ENV{FONTCONFIG_FILE} = "fonts.conf"; + $ENV{FONTCONFIG_PATH} = Cwd::abs_path DC::find_rcfile "fonts"; + $ENV{FONTCONFIG_DIR} = $ENV{FONTCONFIG_PATH}; # helps with older versions + + { + my @fonts = map DC::find_rcfile "fonts/$_", qw( + DejaVuSans.ttf + DejaVuSansMono.ttf + DejaVuSans-Bold.ttf + DejaVuSansMono-Bold.ttf + DejaVuSans-Oblique.ttf + DejaVuSansMono-Oblique.ttf + DejaVuSans-BoldOblique.ttf + DejaVuSansMono-BoldOblique.ttf + mona.ttf + ); - DC::add_font $_ for @fonts; + DC::add_font $_ for @fonts; - $FONT_PROP = new_from_file DC::Font $fonts[0]; - $FONT_FIXED = new_from_file DC::Font $fonts[1]; + $FONT_PROP = new_from_file DC::Font $fonts[0]; + $FONT_FIXED = new_from_file DC::Font $fonts[1]; - $FONT_PROP->make_default; + $FONT_PROP->make_default; - DC::pango_init; - } + DC::pango_init; + } # compare mono (ft) vs. rgba (cairo) # ft - 1.8s, cairo 3s, even in alpha-only mode @@ -2879,33 +3060,34 @@ # warn $t2-$t1; # } + } + + DC::SDL_Init 0; DC::IMG_Init; video_init; DC::Mix_Init; audio_init; - } - show_tip_of_the_day if $CFG->{show_tips}; + show_tip_of_the_day if $CFG->{show_tips}; - my $STARTUP_CANCEL; $STARTUP_CANCEL = EV::idle sub { - undef $STARTUP_CANCEL; - (pop @::STARTUP_DONE)->() - while @::STARTUP_DONE; - }; + my $STARTUP_CANCEL; $STARTUP_CANCEL = EV::idle sub { + undef $STARTUP_CANCEL; + (pop @::STARTUP_DONE)->() + while @::STARTUP_DONE; + }; - debug_toggle 0; + debug_toggle 0; - delete $SIG{__DIE__}; - EV::loop; + delete $SIG{__DIE__}; + EV::loop; - DC::write_cfg if $CFG->{config_autosave}; + DC::save_cfg if $CFG->{config_autosave}; - #video_shutdown; - #audio_shutdown; + #video_shutdown; + #audio_shutdown; - DC::OpenGL::quit; - DC::SDL_Quit; - DC::DB::Server::stop; + DC::OpenGL::quit; + DC::SDL_Quit; + DC::DB::Server::stop; + }; } -*DC::Main::run = \&DC::SDL_braino; # see sub above - 1