#! perl # why this is an extension is a good question. next question. use Fcntl; use Coro; use Coro::AIO; our $emergency_position = $cf::CFG{emergency_position} || ["/world/world_105_115", 5, 37]; our $DEACTIVATE_TIMEOUT = 60; # number of seconds after which maps get deactivated to save cpu our $SWAP_TIMEOUT = 600; # number of seconds after which maps inactive get swapped out our $SCHEDULE_INTERVAL = 8; # time the map scheduler sleeps between runs our $SAVE_TIMEOUT = 60; # save maps every n seconds our $SAVE_INTERVAL = 0.4; # save at max. one map every $SAVE_HOLD our $MAX_RESET = 7200; $DEACTIVATE_TIMEOUT = 3;#d# $SWAP_TIMEOUT = 5;#d# $SCHEDULE_INTERVAL = 1; $cf::LINK_MAP ||= do { my $map = cf::map::new; $map->width (1); $map->height (1); $map->alloc; $map->path ("{link}"); $map->{path} = bless { path => "{link}" }, "cf::path"; $map->in_memory (cf::MAP_IN_MEMORY); $map }; { package cf::path; sub new { my ($class, $path, $base) = @_; my %res; if ($path =~ s{^~([^/]+)?}{}) { $res{user_rel} = 1; if (defined $1) { $res{user} = $1; } elsif ($base =~ m{^~([^/]+)/}) { $res{user} = $1; } else { warn "cannot resolve user-relative path without user <$path,$base>\n"; } } elsif ($path =~ /^\//) { # already absolute } else { $base =~ s{[^/]+/?$}{}; return $class->new ("$base/$path"); } for ($path) { redo if s{/\.?/}{/}; redo if s{/[^/]+/\.\./}{/}; } $res{path} = $path; bless \%res, $class } # the name / primary key / in-game path sub as_string { my ($self) = @_; $self->{user_rel} ? "~$self->{user}$self->{path}" : $self->{path} } # escape the /'s in the path sub escaped_path { # ∕ is U+2215 (my $path = $_[0]{path}) =~ s/\//∕/g; $path } # the original (read-only) location sub load_path { my ($self) = @_; sprintf "%s/%s/%s", cf::datadir, cf::mapdir, $self->{path} } # the temporary/swap location sub save_path { my ($self) = @_; $self->{user_rel} ? sprintf "%s/%s/%s/%s", cf::localdir, cf::playerdir, $self->{user}, $self->escaped_path : sprintf "%s/%s/%s", cf::localdir, cf::tmpdir, $self->escaped_path } # the unique path, might be eq to save_path sub uniq_path { my ($self) = @_; $self->{user_rel} ? undef : sprintf "%s/%s/%s", cf::localdir, cf::uniquedir, $self->escaped_path } } sub write_runtime { my $runtime = cf::localdir . "/runtime"; my $fh = aio_open "$runtime~", O_WRONLY | O_CREAT, 0644 or return; my $value = $cf::RUNTIME; (aio_write $fh, 0, (length $value), $value, 0) <= 0 and return; aio_fsync $fh and return; close $fh or return; aio_rename "$runtime~", $runtime and return; 1 } (Coro::async { unless (write_runtime) { warn "unable to write runtime file: $!"; exit 1; } })->prio (Coro::PRIO_MAX); our $SCHEDULER = cf::coro { while () { Coro::Timer::sleep $SCHEDULE_INTERVAL; write_runtime or warn "unable to write runtime file: $!"; for my $map (values %cf::MAP) { eval { next if $map->in_memory != cf::MAP_IN_MEMORY; my $last_access = $map->last_access; # not yet, because maps might become visible to players nearby # we need a tiled meta map for this to work # if ($last_access + $DEACTIVATE_TIMEOUT <= $cf::RUNTIME) { # $map->deactivate; # delete $map->{active}; # } if ($map->should_reset) { $map->reset; } elsif ($last_access + $SWAP_TIMEOUT <= $cf::RUNTIME) { $map->swap_out; Coro::Timer::sleep $SAVE_INTERVAL; } elsif ($map->{last_save} + $SAVE_TIMEOUT <= $cf::RUNTIME) { $map->save; Coro::Timer::sleep $SAVE_INTERVAL; } }; warn $@ if $@; cede; } } }; $SCHEDULER->prio (-2); sub sync_job(&) { my ($job) = @_; my $done; my @res; Carp::confess "nested sync_job" if $cf::FREEZE; local $cf::FREEZE = 1; (Coro::async { @res = eval { $job->() }; warn $@ if $@; $done = 1; })->prio (Coro::PRIO_MAX); while (!$done) { Coro::cede_notself; Event::one_event unless Coro::nready; } wantarray ? @res : $res[0] } # and all this just because we cannot iterate over # all maps in C++... sub cf::map::change_all_map_light { my ($change) = @_; $_->change_map_light ($change) for 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->reset_time (0) if $map->reset_time > $cf::RUNTIME; $map->reset_timeout (10);#d# $map->{load_path} = $path; $map } sub cf::map::find_map_nb { my ($path, $origin) = @_; warn "find_map_nb<$path,$origin>\n";#d# $path = ref $path ? $path : new cf::path $path, $origin && $origin->path; my $key = $path->as_string; $cf::MAP{$key} || do { # do it the slow way my $map = try_load_header $path->save_path; if (!$map) { $map = try_load_header $path->load_path or return; } $map or return; $map->instantiate; $map->path ($key); $map->{path} = $path; $map->per_player (0) if $path->{user_rel}; $map->reset if $map->should_reset; $cf::MAP{$key} = $map } } sub cf::map::find_map { my ($path, $origin) = @_; $path = new cf::path $path, $origin && $origin->path; my $key = $path->as_string; warn "find_map<$path,$origin>\n";#d# $cf::MAP{$key} || sync_job { cf::map::find_map_nb $path; } } sub cf::map::do_load_nb { my ($self) = @_; return 0 unless $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; 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); } } # now do the right thing for maps $self->link_multipart_objects; $self->fix_auto_apply; $self->decay_objects; $self->update_buttons; $self->set_darkness_map; $self->difficulty ($self->estimate_difficulty) unless $self->difficulty; $self->in_memory (cf::MAP_IN_MEMORY); } sub cf::map::do_load { my ($self) = @_; warn "do_load<$self>\n";#d# sync_job { cf::map::do_load_nb $self; }; } sub cf::map::save { my ($self) = @_; warn "saving map ", $self->path; my $save = $self->{path}->save_path; utf8::encode $save; my $uniq = $self->{path}->uniq_path; utf8::encode $uniq; 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); } $self->{load_path} = $save; $self->{last_save} = $cf::RUNTIME; } sub cf::map::swap_out { my ($self) = @_; $self->save if $self->in_memory == cf::MAP_IN_MEMORY; $self->clear; $self->in_memory (cf::MAP_SWAPPED); } sub cf::map::should_reset { my ($map) = @_; # TODO: safety, remove and allow resettable per-player maps return if $map->{path}{user_rel};#d# return if $map->per_player; return unless $map->reset_timeout; my $time = $map->fixed_resettime ? $map->reset_time : $map->last_access; $time + $map->reset_timeout < $cf::RUNTIME } sub cf::map::reset { my ($self) = @_; return if $self->players; return if $self->{path}{user_rel};#d# warn "resetting map ", $self->path;#d# 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"; $self->clear; $self->in_memory (cf::MAP_SWAPPED); utf8::encode ($self->{load_path} = $self->{path}->load_path); } sub cf::object::player::enter_exit { my ($ob, $exit) = @_; # if at login, move to interim map immediately unless ($exit) { # used on login only $ob->enter_map ($LINK_MAP, 0, 0); } #TODO: do this in the background, freeze the player if required sync_job { my ($map, $x, $y); unless ($exit) { # used on login only(?) $map = cf::map::find_map_nb $ob->contr->maplevel; ($x, $y) = ($ob->x, $ob->y); } else { my $path = new cf::path $exit->slaying, $exit->map && $exit->map->path; $map = cf::map::find_map_nb $path->as_string; $map = $map->customise_for ($ob) if $map; ($x, $y) = ($exit->stats->hp, $exit->stats->sp); } unless ($map) { $map = cf::map::find_map_nb $emergency_position->[0] or die "FATAL: cannot load emergency map\n"; $x = $emergency_position->[1]; $y = $emergency_position->[2]; } if ($map) { warn "entering ", $map->path, " at ($x, $y)\n";#d# $map->do_load_nb; $ob->enter_map ($map, $x, $y); } else { $ob->message ("The exit is closed", cf::NDI_UNIQUE | cf::NDI_RED); } } } sub cf::map::customise_for { my ($map, $ob) = @_; if ($map->per_player) { return cf::map::find_map_nb "~" . $ob->name . "/" . $map->{path}{path}; } $map } sub cf::map::emergency_save { local $cf::FREEZE = 1; warn "enter emergency map save\n"; my $saver = async { warn "begin emergency map save\n"; $_->save for values %cf::MAP; }; $saver->prio (Coro::PRIO_MAX); $saver->join; warn "emergency map save drain\n"; Event::one_event while IO::AIO::nreqs; warn "end emergency map save\n"; }