#! 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; our $SCHEDULER = cf::coro { Coro::Timer::sleep 3600;#d#TODO#for debugging only while () { Coro::Timer::sleep $SCHEDULE_INTERVAL; for my $map (values %cf::MAP) { eval { next if $map->in_memory != cf::MAP_IN_MEMORY; next if $map->players; 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 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}) } 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 rpelace 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; $exit->slaying ("?random/" . cf::to_json $rmp); $exit->msg (undef); } # 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 { my ($path, $origin) = @_; warn "find_map<$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) { 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->{reset_time} = $cf::RUNTIME + $map->reset_timeout; $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->reset if $map->should_reset; $cf::MAP{$key} = $map } } sub cf::map::load { my ($self) = @_; 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; 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; unless ($self->{path}->is_style_map) { $self->fix_auto_apply; $self->decay_objects; $self->update_buttons; $self->set_darkness_map; $self->difficulty ($self->estimate_difficulty) unless $self->difficulty; $self->activate; } $self->in_memory (cf::MAP_IN_MEMORY); } sub cf::map::load_map_sync { my ($path, $origin) = @_; warn "load_map_sync<$path, $origin>\n";#d# cf::abort if $path =~ /CVS/;#d# cf::sync_job { my $map = cf::map::find_map $path, $origin or return; $map->load; $map } } sub cf::map::save { my ($self) = @_; my $save = $self->{path}->save_path; utf8::encode $save; my $uniq = $self->{path}->uniq_path; utf8::encode $uniq; $self->{last_save} = $cf::RUNTIME; return unless $self->dirty; $self->{load_path} = $save; return if $self->{path}->is_style_map; warn "saving map ", $self->path; 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 cf::map::swap_out { my ($self) = @_; return if $self->players; return if $self->in_memory != cf::MAP_IN_MEMORY; $self->save; $self->clear; $self->in_memory (cf::MAP_SWAPPED); } sub cf::map::should_reset { my ($map) = @_; return;#d# # TODO: safety, remove and allow resettable per-player maps return if $map->{path}{user_rel};#d# 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# return;#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) = @_; return unless $ob->type == cf::PLAYER; my ($oldmap, $oldx, $oldy) = ($ob->map, $ob->x, $ob->y); $ob->enter_map ($LINK_MAP, 20, 20); $ob->deactivate_recursive; (Coro::async { my ($map, $x, $y); unless (eval { prepare_random_map $exit if $exit->slaying eq "/!"; my $path = new cf::path $exit->slaying, $exit->map && $exit->map->path; $map = cf::map::find_map $path->as_string; $map = $map->customise_for ($ob) if $map; ($x, $y) = ($exit->stats->hp, $exit->stats->sp); unless ($map) { $ob->message ("The exit is closed", cf::NDI_UNIQUE | cf::NDI_RED); # restore original map position ($map, $x, $y) = ($oldmap, $oldx, $oldy); unless ($map) { $map = cf::map::find_map $emergency_position->[0] or die "FATAL: cannot load emergency map\n"; $x = $emergency_position->[1]; $y = $emergency_position->[2]; } } # use -1, -1 as default coordinates, not 0, 0 ($x, $y) = ($map->enter_x, $map->enter_y) if $x <=0 && $y <= 0; warn "entering ", $map->path, " at ($x, $y)\n";#d# $map->load; 1; }) { ($map, $x, $y) = ($oldmap, $oldx, $oldy); $ob->message ("Something went wrong within the 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: $@"; } $ob->activate_recursive; $ob->enter_map ($map, $x, $y); })->prio (1); } sub cf::map::customise_for { my ($map, $ob) = @_; if ($map->per_player) { return cf::map::find_map "~" . $ob->name . "/" . $map->{path}{path}; } $map } sub cf::map::emergency_save { local $cf::FREEZE = 1; 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"; }