--- deliantra/server/lib/cf.pm 2006/12/31 22:23:12 1.109 +++ deliantra/server/lib/cf.pm 2007/01/04 17:28:49 1.134 @@ -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; @@ -51,7 +51,6 @@ our %MAP; # all maps our $LINK_MAP; # the special {link} map -our $FREEZE; our $RANDOM_MAPS = cf::localdir . "/random"; our %EXT_CORO; @@ -73,6 +72,7 @@ # a special map that is always available our $LINK_MAP; +our $EMERGENCY_POSITION; ############################################################################# @@ -181,6 +181,94 @@ 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, cf::map::find +uses map_find and cf::map::load 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 $@; + } +} + +sub freeze_mainloop { + return unless $TICK_WATCHER->is_active; + + my $guard = guard { $TICK_WATCHER->start }; + $TICK_WATCHER->stop; + $guard +} + =item cf::sync_job { BLOCK } The design of crossfire+ requires that the main coro ($Coro::main) is @@ -197,29 +285,34 @@ sub sync_job(&) { my ($job) = @_; - my $busy = 1; - my @res; - - # TODO: use suspend/resume instead - local $FREEZE = 1; - - my $coro = Coro::async { - @res = eval { $job->() }; - warn $@ if $@; - undef $busy; - }; - if ($Coro::current == $Coro::main) { - $coro->prio (Coro::PRIO_MAX); + # this is the main coro, too bad, we have to block + # till the operation succeeds, freezing the server :/ + + # TODO: use suspend/resume instead + # (but this is cancel-safe) + my $freeze_guard = freeze_mainloop; + + my $busy = 1; + my @res; + + (Coro::async { + @res = eval { $job->() }; + warn $@ if $@; + undef $busy; + })->prio (Coro::PRIO_MAX); + while ($busy) { Coro::cede_notself; Event::one_event unless Coro::nready; } + + wantarray ? @res : $res[0] } else { - $coro->join; + # we are in another coroutine, how wonderful, everything just works + + $job->() } - - wantarray ? @res : $res[0] } =item $coro = cf::coro { BLOCK } @@ -232,12 +325,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}; @@ -253,7 +341,7 @@ my $fh = aio_open "$runtime~", O_WRONLY | O_CREAT, 0644 or return; - my $value = $cf::RUNTIME; + my $value = $cf::RUNTIME + 1 + 10; # 10 is the runtime save interval, for a monotonic clock (aio_write $fh, 0, (length $value), $value, 0) <= 0 and return; @@ -280,9 +368,21 @@ sub new { my ($class, $path, $base) = @_; + $path = $path->as_string if ref $path; + my $self = bless { }, $class; - if ($path =~ s{^\?random/}{}) { + # {... are special paths that are not touched + # ?xxx/... are special absolute paths + # ?random/... random maps + # /! non-realised random map exit + # /... normal maps + # ~/... per-player maps without a specific player (DO NOT USE) + # ~user/... per-player map of a specific user + + if ($path =~ /^{/) { + # fine as it is + } elsif ($path =~ s{^\?random/}{}) { Coro::AIO::aio_load "$cf::RANDOM_MAPS/$path.meta", my $data; $self->{random} = cf::from_json $data; } else { @@ -797,9 +897,11 @@ unless (aio_stat "$filename.pst") { (aio_load "$filename.pst", $av) >= 0 or return; - $av = eval { (Storable::thaw <$av>)->{objs} }; + $av = eval { (Storable::thaw $av)->{objs} }; } + warn sprintf "loading %s (%d)\n", + $filename, length $data, scalar @{$av || []};#d# return ($data, $av); } @@ -1031,6 +1133,320 @@ =back + +=head3 cf::map + +=over 4 + +=cut + +package cf::map; + +use Fcntl; +use Coro::AIO; + +our $MAX_RESET = 3600; +our $DEFAULT_RESET = 3000; + +sub generate_random_map { + my ($path, $rmp) = @_; + + # mit "rum" bekleckern, nicht + cf::map::_create_random_map + $path, + $rmp->{wallstyle}, $rmp->{wall_name}, $rmp->{floorstyle}, $rmp->{monsterstyle}, + $rmp->{treasurestyle}, $rmp->{layoutstyle}, $rmp->{doorstyle}, $rmp->{decorstyle}, + $rmp->{origin_map}, $rmp->{final_map}, $rmp->{exitstyle}, $rmp->{this_map}, + $rmp->{exit_on_final_map}, + $rmp->{xsize}, $rmp->{ysize}, + $rmp->{expand2x}, $rmp->{layoutoptions1}, $rmp->{layoutoptions2}, $rmp->{layoutoptions3}, + $rmp->{symmetry}, $rmp->{difficulty}, $rmp->{difficulty_given}, $rmp->{difficulty_increase}, + $rmp->{dungeon_level}, $rmp->{dungeon_depth}, $rmp->{decoroptions}, $rmp->{orientation}, + $rmp->{origin_y}, $rmp->{origin_x}, $rmp->{random_seed}, $rmp->{total_map_hp}, + $rmp->{map_layout_style}, $rmp->{treasureoptions}, $rmp->{symmetry_used}, + (cf::region::find $rmp->{region}) +} + +# and all this just because we cannot iterate over +# all maps in C++... +sub change_all_map_light { + my ($change) = @_; + + $_->change_map_light ($change) + for grep $_->outdoor, values %cf::MAP; +} + +sub try_load_header($) { + my ($path) = @_; + + utf8::encode $path; + aio_open $path, O_RDONLY, 0 + or return; + + my $map = cf::map::new + or return; + + $map->load_header ($path) + or return; + + $map->{load_path} = $path; + + $map +} + +sub find; +sub find { + my ($path, $origin) = @_; + + #warn "find<$path,$origin>\n";#d# + + $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; + + Coro::cede; + + if ($map) { + $map->last_access ((delete $map->{last_access}) + || $cf::RUNTIME); #d# + # safety + $map->{instantiate_time} = $cf::RUNTIME + if $map->{instantiate_time} > $cf::RUNTIME; + } else { + if (my $rmp = $path->random_map_params) { + $map = generate_random_map $key, $rmp; + } else { + $map = try_load_header $path->load_path; + } + + $map or return; + + $map->{load_original} = 1; + $map->{instantiate_time} = $cf::RUNTIME; + $map->last_access ($cf::RUNTIME); + $map->instantiate; + + # per-player maps become, after loading, normal maps + $map->per_player (0) if $path->{user_rel}; + } + + $map->path ($key); + $map->{path} = $path; + $map->{last_save} = $cf::RUNTIME; + + Coro::cede; + + if ($map->should_reset) { + $map->reset; + undef $guard; + $map = find $path + or return; + } + + $cf::MAP{$key} = $map + } +} + +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); + + $self->alloc; + $self->load_objects ($self->{load_path}, 1) + or return; + + $self->set_object_flag (cf::FLAG_OBJ_ORIGINAL, 1) + if delete $self->{load_original}; + + if (my $uniq = $path->uniq_path) { + utf8::encode $uniq; + if (aio_open $uniq, O_RDONLY, 0) { + $self->clear_unique_items; + $self->load_objects ($uniq, 0); + } + } + + Coro::cede; + + # now do the right thing for maps + $self->link_multipart_objects; + + if ($self->{path}->is_style_map) { + $self->{deny_save} = 1; + $self->{deny_reset} = 1; + } else { + $self->fix_auto_apply; + $self->decay_objects; + $self->update_buttons; + $self->set_darkness_map; + $self->difficulty ($self->estimate_difficulty) + unless $self->difficulty; + $self->activate; + } + + Coro::cede; + + $self->in_memory (cf::MAP_IN_MEMORY); +} + +sub find_sync { + my ($path, $origin) = @_; + + cf::sync_job { cf::map::find $path, $origin } +} + +sub do_load_sync { + my ($map) = @_; + + cf::sync_job { $map->load }; +} + +sub save { + my ($self) = @_; + + $self->{last_save} = $cf::RUNTIME; + + return unless $self->dirty; + + my $save = $self->{path}->save_path; utf8::encode $save; + my $uniq = $self->{path}->uniq_path; utf8::encode $uniq; + + $self->{load_path} = $save; + + return if $self->{deny_save}; + + local $self->{last_access} = $self->last_access;#d# + + if ($uniq) { + $self->save_objects ($save, cf::IO_HEADER | cf::IO_OBJECTS); + $self->save_objects ($uniq, cf::IO_UNIQUES); + } else { + $self->save_objects ($save, cf::IO_HEADER | cf::IO_OBJECTS | cf::IO_UNIQUES); + } +} + +sub swap_out { + my ($self) = @_; + + # save first because save cedes + $self->save; + + return if $self->players; + return if $self->in_memory != cf::MAP_IN_MEMORY; + return if $self->{deny_save}; + + $self->clear; + $self->in_memory (cf::MAP_SWAPPED); +} + +sub reset_at { + my ($self) = @_; + + # TODO: safety, remove and allow resettable per-player maps + return 1e99 if $self->{path}{user_rel}; + return 1e99 if $self->{deny_reset}; + + my $time = $self->fixed_resettime ? $self->{instantiate_time} : $self->last_access; + my $to = List::Util::min $MAX_RESET, $self->reset_timeout || $DEFAULT_RESET; + + $time + $to +} + +sub should_reset { + my ($self) = @_; + + $self->reset_at <= $cf::RUNTIME +} + +sub unlink_save { + my ($self) = @_; + + utf8::encode (my $save = $self->{path}->save_path); + aioreq_pri 3; IO::AIO::aio_unlink $save; + aioreq_pri 3; IO::AIO::aio_unlink "$save.pst"; +} + +sub rename { + my ($self, $new_path) = @_; + + $self->unlink_save; + + delete $cf::MAP{$self->path}; + $self->{path} = new cf::path $new_path; + $self->path ($self->{path}->as_string); + $cf::MAP{$self->path} = $self; + + $self->save; +} + +sub reset { + my ($self) = @_; + + return if $self->players; + return if $self->{path}{user_rel};#d# + + warn "resetting map ", $self->path;#d# + + delete $cf::MAP{$self->path}; + + $_->clear_links_to ($self) for values %cf::MAP; + + $self->unlink_save; + $self->destroy; +} + +my $nuke_counter = "aaaa"; + +sub nuke { + my ($self) = @_; + + $self->{deny_save} = 1; + $self->reset_timeout (1); + $self->rename ("{nuke}/" . ($nuke_counter++)); + $self->reset; # polite request, might not happen +} + +sub customise_for { + my ($map, $ob) = @_; + + if ($map->per_player) { + return cf::map::find "~" . $ob->name . "/" . $map->{path}{path}; + } + + $map +} + +sub emergency_save { + my $freeze_guard = cf::freeze_mainloop; + + warn "enter emergency map save\n"; + + cf::sync_job { + warn "begin emergency map save\n"; + $_->save for values %cf::MAP; + }; + + warn "end emergency map save\n"; +} + +package cf; + +=back + + =head3 cf::object::player =over 4 @@ -1075,6 +1491,198 @@ : $cf::CFG{"may_$access"}) } +=item $player_object->enter_link + +Freezes the player and moves him/her to a special map (C<{link}>). + +The player should be reaosnably safe there for short amounts of time. You +I call C as soon as possible, though. + +=item $player_object->leave_link ($map, $x, $y) + +Moves the player out of the specila link map onto the given map. If the +map is not valid (or omitted), the player will be moved back to the +location he/she was before the call to C, or, if that fails, +to the emergency map position. + +Might block. + +=cut + +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] + if $self->map; + + $self->enter_map ($LINK_MAP, 20, 20); +} + +sub cf::object::player::leave_link { + my ($self, $map, $x, $y) = @_; + + my $link_pos = delete $self->{_link_pos}; + + unless ($map) { + # restore original map position + ($map, $x, $y) = @{ $link_pos || [] }; + $map = cf::map::find $map; + + unless ($map) { + ($map, $x, $y) = @$EMERGENCY_POSITION; + $map = cf::map::find $map + or die "FATAL: cannot load emergency map\n"; + } + } + + ($x, $y) = (-1, -1) + unless (defined $x) && (defined $y); + + # use -1 or undef as default coordinates, not 0, 0 + ($x, $y) = ($map->enter_x, $map->enter_y) + if $x <=0 && $y <= 0; + + $map->load; + + $self->activate_recursive; + $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 + +sub cf::object::player::goto_map { + my ($self, $path, $x, $y) = @_; + + $self->enter_link; + + (cf::async { + $path = new cf::path $path; + + my $map = cf::map::find $path->as_string; + $map = $map->customise_for ($self) if $map; + +# warn "entering ", $map->path, " at ($x, $y)\n" +# if $map; + + $map or $self->message ("The exit is closed", cf::NDI_UNIQUE | cf::NDI_RED); + + $self->leave_link ($map, $x, $y); + })->prio (1); +} + +=item $player_object->enter_exit ($exit_object) + +=cut + +sub parse_random_map_params { + my ($spec) = @_; + + my $rmp = { # defaults + xsize => 10, + ysize => 10, + }; + + for (split /\n/, $spec) { + my ($k, $v) = split /\s+/, $_, 2; + + $rmp->{lc $k} = $v if (length $k) && (length $v); + } + + $rmp +} + +sub prepare_random_map { + my ($exit) = @_; + + # all this does is basically replace the /! path by + # a new random map path (?random/...) with a seed + # that depends on the exit object + + my $rmp = parse_random_map_params $exit->msg; + + if ($exit->map) { + $rmp->{region} = $exit->map->region_name; + $rmp->{origin_map} = $exit->map->path; + $rmp->{origin_x} = $exit->x; + $rmp->{origin_y} = $exit->y; + } + + $rmp->{random_seed} ||= $exit->random_seed; + + my $data = cf::to_json $rmp; + my $md5 = Digest::MD5::md5_hex $data; + + if (my $fh = aio_open "$cf::RANDOM_MAPS/$md5.meta", O_WRONLY | O_CREAT, 0666) { + aio_write $fh, 0, (length $data), $data, 0; + + $exit->slaying ("?random/$md5"); + $exit->msg (undef); + } +} + +sub cf::object::player::enter_exit { + my ($self, $exit) = @_; + + return unless $self->type == cf::PLAYER; + + $self->enter_link; + + (cf::async { + $self->deactivate_recursive; # just to be sure + unless (eval { + prepare_random_map $exit + if $exit->slaying eq "/!"; + + my $path = new cf::path $exit->slaying, $exit->map && $exit->map->path; + $self->goto_map ($path, $exit->stats->hp, $exit->stats->sp); + + 1; + }) { + $self->message ("Something went wrong deep within the crossfire server. " + . "I'll try to bring you back to the map you were before. " + . "Please report this to the dungeon master", + cf::NDI_UNIQUE | cf::NDI_RED); + + warn "ERROR in enter_exit: $@"; + $self->leave_link; + } + })->prio (1); +} + =head3 cf::client =over 4 @@ -1127,7 +1735,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 can happen, but we saw a crash here (shift @$queue)->[1]->($msg); @@ -1154,12 +1763,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}; @@ -1341,14 +1945,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; @@ -1406,12 +2008,22 @@ local $/; *CFG = YAML::Syck::Load <$fh>; + + $EMERGENCY_POSITION = $CFG{emergency_position} || ["/world/world_105_115", 5, 37]; + + if (exists $CFG{mlockall}) { + eval { + $CFG{mlockall} ? &mlockall : &munlockall + and die "WARNING: m(un)lockall failed: $!\n"; + }; + warn $@ if $@; + } } sub main { # we must not ever block the main coroutine local $Coro::idle = sub { - Carp::cluck "FATAL: Coro::idle was called, major BUG\n";#d# + Carp::cluck "FATAL: Coro::idle was called, major BUG, use cf::sync_job!\n";#d# (Coro::unblock_sub { Event::one_event; })->(); @@ -1426,7 +2038,7 @@ ############################################################################# # initialisation -sub perl_reload() { +sub reload() { # can/must only be called in main if ($Coro::current != $Coro::main) { warn "can only reload from main coroutine\n"; @@ -1435,7 +2047,7 @@ warn "reloading..."; - local $FREEZE = 1; + my $guard = freeze_mainloop; cf::emergency_save; eval { @@ -1529,16 +2141,46 @@ $LINK_MAP->path ("{link}"); $LINK_MAP->{path} = bless { path => "{link}" }, "cf::path"; $LINK_MAP->in_memory (MAP_IN_MEMORY); + + # dirty hack because... archetypes are not yet loaded + Event->timer ( + after => 2, + cb => sub { + $_[0]->w->cancel; + + # provide some exits "home" + my $exit = cf::object::new "exit"; + + $exit->slaying ($EMERGENCY_POSITION->[0]); + $exit->stats->hp ($EMERGENCY_POSITION->[1]); + $exit->stats->sp ($EMERGENCY_POSITION->[2]); + + $LINK_MAP->insert ($exit->clone, 19, 19); + $LINK_MAP->insert ($exit->clone, 19, 20); + $LINK_MAP->insert ($exit->clone, 19, 21); + $LINK_MAP->insert ($exit->clone, 20, 19); + $LINK_MAP->insert ($exit->clone, 20, 21); + $LINK_MAP->insert ($exit->clone, 21, 19); + $LINK_MAP->insert ($exit->clone, 21, 20); + $LINK_MAP->insert ($exit->clone, 21, 21); + + $exit->destroy; + }); + + $LINK_MAP->{deny_save} = 1; + $LINK_MAP->{deny_reset} = 1; + + $cf::MAP{$LINK_MAP->path} = $LINK_MAP; } register "", __PACKAGE__; -register_command "perl-reload" => sub { +register_command "reload" => sub { my ($who, $arg) = @_; if ($who->flag (FLAG_WIZ)) { $who->message ("start of reload."); - perl_reload; + reload; $who->message ("end of reload."); } }; @@ -1551,11 +2193,8 @@ at => $NEXT_TICK || $TICK, data => WF_AUTOCANCEL, cb => sub { - unless ($FREEZE) { - cf::server_tick; # one server iteration - $RUNTIME += $TICK; - } - + cf::server_tick; # one server iteration + $RUNTIME += $TICK; $NEXT_TICK += $TICK; # if we are delayed by four ticks or more, skip them all @@ -1588,5 +2227,7 @@ }, ); +END { cf::emergency_save } + 1