--- deliantra/server/lib/cf.pm 2007/01/02 08:26:42 1.119 +++ deliantra/server/lib/cf.pm 2007/01/03 00:28:47 1.128 @@ -10,7 +10,7 @@ use Safe; use Safe::Hole; -use Coro 3.3; +use Coro 3.3 (); use Coro::Event; use Coro::Timer; use Coro::Signal; @@ -183,6 +183,86 @@ JSON::Syck::Dump $_[0] } +=item my $guard = cf::guard { BLOCK } + +Run the given callback when the guard object gets destroyed (useful for +coroutine cancellations). + +You can call C<< ->cancel >> on the guard object to stop the block from +being executed. + +=cut + +sub guard(&) { + bless \(my $cb = $_[0]), cf::guard::; +} + +sub cf::guard::cancel { + ${$_[0]} = sub { }; +} + +sub cf::guard::DESTROY { + ${$_[0]}->(); +} + +=item cf::lock_wait $string + +Wait until the given lock is available. See cf::lock_acquire. + +=item my $lock = cf::lock_acquire $string + +Wait until the given lock is available and then acquires it and returns +a guard object. If the guard object gets destroyed (goes out of scope, +for example when the coroutine gets canceled), the lock is automatically +returned. + +Lock names should begin with a unique identifier (for example, find_map +uses map_find and load_map uses map_load). + +=cut + +our %LOCK; + +sub lock_wait($) { + my ($key) = @_; + + # wait for lock, if any + while ($LOCK{$key}) { + push @{ $LOCK{$key} }, $Coro::current; + Coro::schedule; + } +} + +sub lock_acquire($) { + my ($key) = @_; + + # wait, to be sure we are not locked + lock_wait $key; + + $LOCK{$key} = []; + + cf::guard { + # wake up all waiters, to be on the safe side + $_->ready for @{ delete $LOCK{$key} }; + } +} + +=item cf::async { BLOCK } + +Like C, but runs the given BLOCK in an eval and only logs the +error instead of exiting the server in case of a problem. + +=cut + +sub async(&) { + my ($cb) = @_; + + Coro::async { + eval { $cb->() }; + warn $@ if $@; + } +} + =item cf::sync_job { BLOCK } The design of crossfire+ requires that the main coro ($Coro::main) is @@ -239,12 +319,7 @@ sub coro(&) { my $cb = shift; - my $coro; $coro = async { - eval { - $cb->(); - }; - warn $@ if $@; - }; + my $coro = &cf::async ($cb); $coro->on_destroy (sub { delete $EXT_CORO{$coro+0}; @@ -1091,7 +1166,8 @@ sub change_all_map_light { my ($change) = @_; - $_->change_map_light ($change) for values %cf::MAP; + $_->change_map_light ($change) + for grep $_->outdoor, values %cf::MAP; } sub try_load_header($) { @@ -1112,6 +1188,7 @@ $map } +sub find_map; sub find_map { my ($path, $origin) = @_; @@ -1120,7 +1197,11 @@ $path = new cf::path $path, $origin && $origin->path; my $key = $path->as_string; + cf::lock_wait "map_find:$key"; + $cf::MAP{$key} || do { + my $guard = cf::lock_acquire "map_find:$key"; + # do it the slow way my $map = try_load_header $path->save_path; @@ -1144,7 +1225,6 @@ # per-player maps become, after loading, normal maps $map->per_player (0) if $path->{user_rel}; } - #Coro::Timer::sleep 1;#d# $map->path ($key); $map->{path} = $path; @@ -1153,7 +1233,9 @@ if ($map->should_reset) { $map->reset; - $map = find_map $path; + undef $guard; + $map = find_map $path + or return; } $cf::MAP{$key} = $map @@ -1163,12 +1245,13 @@ sub load { my ($self) = @_; + my $path = $self->{path}; + my $guard = cf::lock_acquire "map_load:" . $path->as_string; + return if $self->in_memory != cf::MAP_SWAPPED; $self->in_memory (cf::MAP_LOADING); - my $path = $self->{path}; - $self->alloc; $self->load_objects ($self->{load_path}, 1) or return; @@ -1410,13 +1493,14 @@ sub cf::object::player::enter_link { my ($self) = @_; + $self->deactivate_recursive; + return if $self->map == $LINK_MAP; - $self->{_link_pos} = [$self->map->{path}, $self->x, $self->y] + $self->{_link_pos} ||= [$self->map->{path}, $self->x, $self->y] if $self->map; $self->enter_map ($LINK_MAP, 20, 20); - $self->deactivate_recursive; } sub cf::object::player::leave_link { @@ -1449,6 +1533,34 @@ $self->enter_map ($map, $x, $y); } +cf::player->attach ( + on_logout => sub { + my ($pl) = @_; + + # abort map switching before logout + if ($pl->ob->{_link_pos}) { + cf::sync_job { + $pl->ob->leave_link + }; + } + }, + on_login => sub { + my ($pl) = @_; + + # try to abort aborted map switching on player login :) + # should happen only on crashes + if ($pl->ob->{_link_pos}) { + $pl->ob->enter_link; + cf::async { + # we need this sleep as the login has a concurrent enter_exit running + # and this sleep increases chances of the player not ending up in scorn + Coro::Timer::sleep 1; + $pl->ob->leave_link; + }; + } + }, +); + =item $player_object->goto_map ($path, $x, $y) =cut @@ -1458,7 +1570,7 @@ $self->enter_link; - (Coro::async { + (cf::async { $path = new cf::path $path; my $map = cf::map::find_map $path->as_string; @@ -1530,9 +1642,8 @@ $self->enter_link; - (Coro::async { + (cf::async { unless (eval { - prepare_random_map $exit if $exit->slaying eq "/!"; @@ -1604,7 +1715,8 @@ # this weird shuffling is so that direct followup queries # get handled first - my $queue = delete $ns->{query_queue}; + my $queue = delete $ns->{query_queue} + or return; # be conservative, not sure how that cna happen, but we saw a crash here (shift @$queue)->[1]->($msg); @@ -1631,12 +1743,7 @@ sub cf::client::coro { my ($self, $cb) = @_; - my $coro; $coro = async { - eval { - $cb->(); - }; - warn $@ if $@; - }; + my $coro = &cf::async ($cb); $coro->on_destroy (sub { delete $self->{_coro}{$coro+0}; @@ -1818,14 +1925,12 @@ my $path = cf::localdir . "/database.pst"; sub db_load() { - warn "loading database $path\n";#d# remove later $DB = stat $path ? Storable::retrieve $path : { }; } my $pid; sub db_save() { - warn "saving database $path\n";#d# remove later waitpid $pid, 0 if $pid; if (0 == ($pid = fork)) { $DB->{_meta}{version} = 1; @@ -2095,5 +2200,7 @@ }, ); +END { cf::emergency_save } + 1