--- deliantra/server/lib/cf.pm 2008/05/04 14:12:37 1.430 +++ deliantra/server/lib/cf.pm 2010/04/07 18:35:56 1.511 @@ -1,69 +1,83 @@ -# +# # This file is part of Deliantra, the Roguelike Realtime MMORPG. # -# Copyright (©) 2006,2007,2008 Marc Alexander Lehmann / Robin Redeker / the Deliantra team +# Copyright (©) 2006,2007,2008,2009,2010 Marc Alexander Lehmann / Robin Redeker / the Deliantra team # -# Deliantra is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. +# Deliantra is free software: you can redistribute it and/or modify it under +# the terms of the Affero GNU General Public License as published by the +# Free Software Foundation, either version 3 of the License, or (at your +# option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # -# You should have received a copy of the GNU General Public License -# along with this program. If not, see . +# You should have received a copy of the Affero GNU General Public License +# and the GNU General Public License along with this program. If not, see +# . # # The authors can be reached via e-mail to -# +# package cf; +use 5.10.0; use utf8; -use strict; +use strict qw(vars subs); use Symbol; use List::Util; use Socket; -use EV 3.2; +use EV; use Opcode; use Safe; use Safe::Hole; use Storable (); +use Carp (); -use Coro 4.50 (); +use Guard (); +use Coro (); use Coro::State; use Coro::Handle; use Coro::EV; +use Coro::AnyEvent; use Coro::Timer; use Coro::Signal; use Coro::Semaphore; +use Coro::SemaphoreSet; +use Coro::AnyEvent; use Coro::AIO; -use Coro::BDB; +use Coro::BDB 1.6; use Coro::Storable; use Coro::Util (); use JSON::XS 2.01 (); use BDB (); use Data::Dumper; -use Digest::MD5; use Fcntl; -use YAML (); -use IO::AIO 2.51 (); +use YAML::XS (); +use IO::AIO (); use Time::HiRes; use Compress::LZF; use Digest::MD5 (); +AnyEvent::detect; + # configure various modules to our taste # $Storable::canonical = 1; # reduce rsync transfers Coro::State::cctx_stacksize 256000; # 1-2MB stack, for deep recursions in maze generator -Compress::LZF::sfreeze_cr { }; # prime Compress::LZF so it does not use require later $Coro::main->prio (Coro::PRIO_MAX); # run main coroutine ("the server") with very high priority +# make sure c-lzf reinitialises itself +Compress::LZF::set_serializer "Storable", "Storable::net_mstore", "Storable::mretrieve"; +Compress::LZF::sfreeze_cr { }; # prime Compress::LZF so it does not use require later + +# strictly for debugging +$SIG{QUIT} = sub { Carp::cluck "SIGQUIT" }; + sub WF_AUTOCANCEL () { 1 } # automatically cancel this watcher on reload our %COMMAND = (); @@ -75,34 +89,39 @@ our %EXT_CORO = (); # coroutines bound to extensions our %EXT_MAP = (); # pluggable maps -our $RELOAD; # number of reloads so far +our $RELOAD; # number of reloads so far, non-zero while in reload our @EVENT; +our @REFLECT; # set by XS +our %REFLECT; # set by us + +our $CONFDIR = confdir; +our $DATADIR = datadir; +our $LIBDIR = "$DATADIR/ext"; +our $PODDIR = "$DATADIR/pod"; +our $MAPDIR = "$DATADIR/" . mapdir; +our $LOCALDIR = localdir; +our $TMPDIR = "$LOCALDIR/" . tmpdir; +our $UNIQUEDIR = "$LOCALDIR/" . uniquedir; +our $PLAYERDIR = "$LOCALDIR/" . playerdir; +our $RANDOMDIR = "$LOCALDIR/random"; +our $BDBDIR = "$LOCALDIR/db"; +our $PIDFILE = "$LOCALDIR/pid"; +our $RUNTIMEFILE = "$LOCALDIR/runtime"; -our $CONFDIR = confdir; -our $DATADIR = datadir; -our $LIBDIR = "$DATADIR/ext"; -our $PODDIR = "$DATADIR/pod"; -our $MAPDIR = "$DATADIR/" . mapdir; -our $LOCALDIR = localdir; -our $TMPDIR = "$LOCALDIR/" . tmpdir; -our $UNIQUEDIR = "$LOCALDIR/" . uniquedir; -our $PLAYERDIR = "$LOCALDIR/" . playerdir; -our $RANDOMDIR = "$LOCALDIR/random"; -our $BDBDIR = "$LOCALDIR/db"; our %RESOURCE; our $TICK = MAX_TIME * 1e-6; # this is a CONSTANT(!) -our $AIO_POLL_WATCHER; our $NEXT_RUNTIME_WRITE; # when should the runtime file be written our $NEXT_TICK; -our $USE_FSYNC = 1; # use fsync to write maps - default off +our $USE_FSYNC = 1; # use fsync to write maps - default on -our $BDB_POLL_WATCHER; our $BDB_DEADLOCK_WATCHER; our $BDB_CHECKPOINT_WATCHER; our $BDB_TRICKLE_WATCHER; our $DB_ENV; +our @EXTRA_MODULES = qw(pod match mapscript); + our %CFG; our $UPTIME; $UPTIME ||= time; @@ -121,16 +140,23 @@ our $JITTER; # average jitter our $TICK_START; # for load detecting purposes +our @POST_INIT; + +our $REATTACH_ON_RELOAD; # set to true to force object reattach on reload (slow) +our $REALLY_UNLOOP; # never set to true, please :) + binmode STDOUT; binmode STDERR; # read virtual server time, if available -unless ($RUNTIME || !-e "$LOCALDIR/runtime") { - open my $fh, "<", "$LOCALDIR/runtime" - or die "unable to read runtime file: $!"; +unless ($RUNTIME || !-e $RUNTIMEFILE) { + open my $fh, "<", $RUNTIMEFILE + or die "unable to read $RUNTIMEFILE file: $!"; $RUNTIME = <$fh> + 0.; } +eval "sub TICK() { $TICK } 1" or die; + mkdir $_ for $LOCALDIR, $TMPDIR, $UNIQUEDIR, $PLAYERDIR, $RANDOMDIR, $BDBDIR; @@ -140,6 +166,17 @@ ############################################################################# +%REFLECT = (); +for (@REFLECT) { + my $reflect = JSON::XS::decode_json $_; + $REFLECT{$reflect->{class}} = $reflect; +} + +# this is decidedly evil +$REFLECT{object}{flags} = { map +($_ => undef), grep $_, map /^FLAG_([A-Z0-9_]+)$/ && lc $1, keys %{"cf::"} }; + +############################################################################# + =head2 GLOBAL VARIABLES =over 4 @@ -193,42 +230,45 @@ =item @cf::INVOKE_RESULTS -This array contains the results of the last C call. When +This array contains the results of the last C call. When C is called C<@cf::INVOKE_RESULTS> is set to the parameters of that call. +=item %cf::REFLECT + +Contains, for each (C++) class name, a hash reference with information +about object members (methods, scalars, arrays and flags) and other +metadata, which is useful for introspection. + =back =cut -BEGIN { - *CORE::GLOBAL::warn = sub { - my $msg = join "", @_; +$Coro::State::WARNHOOK = sub { + my $msg = join "", @_; - $msg .= "\n" - unless $msg =~ /\n$/; + $msg .= "\n" + unless $msg =~ /\n$/; - $msg =~ s/([\x00-\x08\x0b-\x1f])/sprintf "\\x%02x", ord $1/ge; + $msg =~ s/([\x00-\x08\x0b-\x1f])/sprintf "\\x%02x", ord $1/ge; - LOG llevError, $msg; - }; -} + LOG llevError, $msg; +}; $Coro::State::DIEHOOK = sub { return unless $^S eq 0; # "eq", not "==" + warn Carp::longmess $_[0]; + if ($Coro::current == $Coro::main) {#d# warn "DIEHOOK called in main context, Coro bug?\n";#d# return;#d# }#d# # kill coroutine otherwise - warn Carp::longmess $_[0]; Coro::terminate }; -$SIG{__DIE__} = sub { }; #d#? - @safe::cf::global::ISA = @cf::global::ISA = 'cf::attachable'; @safe::cf::object::ISA = @cf::object::ISA = 'cf::attachable'; @safe::cf::player::ISA = @cf::player::ISA = 'cf::attachable'; @@ -244,14 +284,14 @@ cf::object cf::object::player cf::client cf::player cf::arch cf::living - cf::map cf::party cf::region + cf::map cf::mapspace + cf::party cf::region )) { - no strict 'refs'; @{"safe::$pkg\::wrap::ISA"} = @{"$pkg\::wrap::ISA"} = $pkg; } $EV::DIED = sub { - warn "error in event callback: @_"; + Carp::cluck "error in event callback: @_"; }; ############################################################################# @@ -298,6 +338,20 @@ sub encode_json($) { $json_coder->encode ($_[0]) } sub decode_json($) { $json_coder->decode ($_[0]) } +=item cf::post_init { BLOCK } + +Execute the given codeblock, I all extensions have been (re-)loaded, +but I the server starts ticking again. + +The cdoeblock will have a single boolean argument to indicate whether this +is a reload or not. + +=cut + +sub post_init(&) { + push @POST_INIT, shift; +} + =item cf::lock_wait $string Wait until the given lock is available. See cf::lock_acquire. @@ -305,7 +359,7 @@ =item my $lock = cf::lock_acquire $string Wait until the given lock is available and then acquires it and returns -a Coro::guard object. If the guard object gets destroyed (goes out of scope, +a L object. If the guard object gets destroyed (goes out of scope, for example when the coroutine gets canceled), the lock is automatically returned. @@ -321,50 +375,24 @@ =cut -our %LOCK; -our %LOCKER;#d# +our $LOCKS = new Coro::SemaphoreSet; sub lock_wait($) { - my ($key) = @_; - - if ($LOCKER{$key} == $Coro::current) {#d# - Carp::cluck "lock_wait($key) for already-acquired lock";#d# - return;#d# - }#d# - - # wait for lock, if any - while ($LOCK{$key}) { - push @{ $LOCK{$key} }, $Coro::current; - Coro::schedule; - } + $LOCKS->wait ($_[0]); } sub lock_acquire($) { - my ($key) = @_; - - # wait, to be sure we are not locked - lock_wait $key; - - $LOCK{$key} = []; - $LOCKER{$key} = $Coro::current;#d# - - Coro::guard { - delete $LOCKER{$key};#d# - # wake up all waiters, to be on the safe side - $_->ready for @{ delete $LOCK{$key} }; - } + $LOCKS->guard ($_[0]) } sub lock_active($) { - my ($key) = @_; - - ! ! $LOCK{$key} + $LOCKS->count ($_[0]) < 1 } sub freeze_mainloop { tick_inhibit_inc; - Coro::guard \&tick_inhibit_dec; + &Guard::guard (\&tick_inhibit_dec); } =item cf::periodic $interval, $cb @@ -397,24 +425,29 @@ our @SLOT_QUEUE; our $SLOT_QUEUE; +our $SLOT_DECAY = 0.9; $SLOT_QUEUE->cancel if $SLOT_QUEUE; $SLOT_QUEUE = Coro::async { $Coro::current->desc ("timeslot manager"); my $signal = new Coro::Signal; + my $busy; while () { next_job: + my $avail = cf::till_tick; - if ($avail > 0.01) { - for (0 .. $#SLOT_QUEUE) { - if ($SLOT_QUEUE[$_][0] < $avail) { - my $job = splice @SLOT_QUEUE, $_, 1, (); - $job->[2]->send; - Coro::cede; - goto next_job; - } + + for (0 .. $#SLOT_QUEUE) { + if ($SLOT_QUEUE[$_][0] <= $avail) { + $busy = 0; + my $job = splice @SLOT_QUEUE, $_, 1, (); + $job->[2]->send; + Coro::cede; + goto next_job; + } else { + $SLOT_QUEUE[$_][0] *= $SLOT_DECAY; } } @@ -423,6 +456,7 @@ push @cf::WAIT_FOR_TICK, $signal; $signal->wait; } else { + $busy = 0; Coro::schedule; } } @@ -433,7 +467,8 @@ my ($time, $pri, $name) = @_; - $time = $TICK * .6 if $time > $TICK * .6; + $time = clamp $time, 0.01, $TICK * .6; + my $sig = new Coro::Signal; push @SLOT_QUEUE, [$time, $pri, $sig, $name]; @@ -726,7 +761,7 @@ =head2 ATTACHABLE OBJECTS -Many objects in crossfire are so-called attachable objects. That means you can +Many objects in deliantra are so-called attachable objects. That means you can attach callbacks/event handlers (a collection of which is called an "attachment") to it. All such attachable objects support the following methods. @@ -786,7 +821,7 @@ Register an attachment by C<$name> through which attachable objects of the given CLASS can refer to this attachment. -Some classes such as crossfire maps and objects can specify attachments +Some classes such as deliantra maps and objects can specify attachments that are attached at load/instantiate time, thus the need for a name. These calls expect any number of the following handler/hook descriptions: @@ -1087,7 +1122,8 @@ # basically do the same as instantiate, without calling instantiate my ($obj) = @_; - bless $obj, ref $obj; # re-bless in case extensions have been reloaded + # no longer needed after getting rid of delete_package? + #bless $obj, ref $obj; # re-bless in case extensions have been reloaded my $registry = $obj->registry; @@ -1145,7 +1181,10 @@ if (my $fh = aio_open "$filename~", O_WRONLY | O_CREAT, 0600) { aio_chmod $fh, SAVE_MODE; aio_write $fh, 0, (length $$rdata), $$rdata, 0; - aio_fsync $fh if $cf::USE_FSYNC; + if ($cf::USE_FSYNC) { + aio_sync_file_range $fh, 0, 0, IO::AIO::SYNC_FILE_RANGE_WAIT_BEFORE | IO::AIO::SYNC_FILE_RANGE_WRITE | IO::AIO::SYNC_FILE_RANGE_WAIT_AFTER; + aio_fsync $fh; + } aio_close $fh; if (@$objs) { @@ -1153,7 +1192,10 @@ aio_chmod $fh, SAVE_MODE; my $data = Coro::Storable::nfreeze { version => 1, objs => $objs }; aio_write $fh, 0, (length $data), $data, 0; - aio_fsync $fh if $cf::USE_FSYNC; + if ($cf::USE_FSYNC) { + aio_sync_file_range $fh, 0, 0, IO::AIO::SYNC_FILE_RANGE_WAIT_BEFORE | IO::AIO::SYNC_FILE_RANGE_WRITE | IO::AIO::SYNC_FILE_RANGE_WAIT_AFTER; + aio_fsync $fh; + } aio_close $fh; aio_rename "$filename.pst~", "$filename.pst"; } @@ -1162,8 +1204,11 @@ } aio_rename "$filename~", $filename; + + $filename =~ s%/[^/]+$%%; + aio_pathsync $filename if $cf::USE_FSYNC; } else { - warn "FATAL: $filename~: $!\n"; + warn "unable to save objects: $filename~: $!\n"; } } else { aio_unlink $filename; @@ -1264,8 +1309,10 @@ $EXTICMD{$name} = $cb; } +use File::Glob (); + cf::player->attach ( - on_command => sub { + on_unknown_command => sub { my ($pl, $name, $params) = @_; my $cb = $COMMAND{$name} @@ -1305,6 +1352,19 @@ }, ); +# "readahead" all extensions +sub cache_extensions { + my $grp = IO::AIO::aio_group; + + add $grp IO::AIO::aio_readdirx $LIBDIR, IO::AIO::READDIR_STAT_ORDER, sub { + for (grep /\.ext$/, @{$_[0]}) { + add $grp IO::AIO::aio_load "$LIBDIR/$_", my $data; + } + }; + + $grp +} + sub load_extensions { cf::sync_job { my %todo; @@ -1333,7 +1393,7 @@ if $source =~ /\A#!.*?perl.*?#\s*(.*)$/m; $ext{source} = - "package $pkg; use strict; use utf8;\n" + "package $pkg; use 5.10.0; use strict 'vars', 'subs'; use utf8;\n" . "#line 1 \"$path\"\n{\n" . $source . "\n};\n1"; @@ -1341,38 +1401,47 @@ $todo{$base} = \%ext; } + my $pass = 0; my %done; while (%todo) { my $progress; + ++$pass; + + ext: while (my ($k, $v) = each %todo) { for (split /,\s*/, $v->{meta}{depends}) { - goto skip + next ext unless exists $done{$_}; } - warn "... loading '$k' into '$v->{pkg}'\n"; + warn "... pass $pass, loading '$k' into '$v->{pkg}'\n"; - unless (eval $v->{source}) { - my $msg = $@ ? "$v->{path}: $@\n" - : "$v->{base}: extension inactive.\n"; - - if (exists $v->{meta}{mandatory}) { - warn $msg; - cf::cleanup "mandatory extension failed to load, exiting."; - } - - warn $msg; - } + my $active = eval $v->{source}; - $done{$k} = delete $todo{$k}; - push @EXTS, $v->{pkg}; - $progress = 1; + if (length $@) { + warn "$v->{path}: $@\n"; + + cf::cleanup "mandatory extension '$k' failed to load, exiting." + if exists $v->{meta}{mandatory}; + } else { + $done{$k} = delete $todo{$k}; + push @EXTS, $v->{pkg}; + $progress = 1; + + warn "$v->{base}: extension inactive.\n" + unless $active; + } } - skip: - die "cannot load " . (join ", ", keys %todo) . ": unable to resolve dependencies\n" - unless $progress; + unless ($progress) { + warn "cannot load " . (join ", ", keys %todo) . ": unable to resolve dependencies\n"; + + while (my ($k, $v) = each %todo) { + cf::cleanup "mandatory extension '$k' has unresolved dependencies, exiting." + if exists $v->{meta}{mandatory}; + } + } } }; } @@ -1383,7 +1452,7 @@ =head2 CORE EXTENSIONS -Functions and methods that extend core crossfire objects. +Functions and methods that extend core deliantra objects. =cut @@ -1436,7 +1505,7 @@ my ($login) = @_; $cf::PLAYER{$login} - or cf::sync_job { !aio_stat path $login } + or !aio_stat path $login } sub find($) { @@ -1466,6 +1535,19 @@ } } +cf::player->attach ( + on_load => sub { + my ($pl, $path) = @_; + + # restore slots saved in save, below + my $slots = delete $pl->{_slots}; + + $pl->ob->current_weapon ($slots->[0]); + $pl->combat_ob ($slots->[1]); + $pl->ranged_ob ($slots->[2]); + }, +); + sub save($) { my ($pl) = @_; @@ -1481,6 +1563,9 @@ cf::get_slot 0.01; + # save slots, to be restored later + local $pl->{_slots} = [$pl->ob->current_weapon, $pl->combat_ob, $pl->ranged_ob]; + $pl->save_pl ($path); cf::cede_to_tick; } @@ -1521,10 +1606,12 @@ my $name = $pl->ob->name; $pl->{deny_save} = 1; - $pl->password ("*"); # this should lock out the player until we nuked the dir + $pl->password ("*"); # this should lock out the player until we have nuked the dir $pl->invoke (cf::EVENT_PLAYER_LOGOUT, 1) if $pl->active; $pl->deactivate; + my $killer = cf::arch::get "killer_quit"; $pl->killer ($killer); $killer->destroy; + $pl->ob->check_score; $pl->invoke (cf::EVENT_PLAYER_QUIT); $pl->ns->destroy if $pl->ns; @@ -1615,98 +1702,9 @@ \@paths } -=item $protocol_xml = $player->expand_cfpod ($crossfire_pod) - -Expand crossfire pod fragments into protocol xml. - -=cut - -use re 'eval'; - -my $group; -my $interior; $interior = qr{ - # match a pod interior sequence sans C<< >> - (?: - \ (.*?)\ (?{ $group = $^N }) - | < (??{$interior}) > - ) -}x; - -sub expand_cfpod { - my ($self, $pod) = @_; - - my $xml; - - while () { - if ($pod =~ /\G( (?: [^BCGHITU]+ | .(?!<) )+ )/xgcs) { - $group = $1; - - $group =~ s/&/&/g; - $group =~ s/]*) (?{ $group = $^N }) - | < $interior > - ) - > - %gcsx - ) { - my ($code, $data) = ($1, $group); - - if ($code eq "B") { - $xml .= "" . expand_cfpod ($self, $data) . ""; - } elsif ($code eq "I") { - $xml .= "" . expand_cfpod ($self, $data) . ""; - } elsif ($code eq "U") { - $xml .= "" . expand_cfpod ($self, $data) . ""; - } elsif ($code eq "C") { - $xml .= "" . expand_cfpod ($self, $data) . ""; - } elsif ($code eq "T") { - $xml .= "" . expand_cfpod ($self, $data) . ""; - } elsif ($code eq "G") { - my ($male, $female) = split /\|/, $data; - $data = $self->gender ? $female : $male; - $xml .= expand_cfpod ($self, $data); - } elsif ($code eq "H") { - $xml .= ("[" . expand_cfpod ($self, $data) . " (Use hintmode to suppress hints)]", - "[Hint suppressed, see hintmode]", - "") - [$self->{hintmode}]; - } else { - $xml .= "error processing '$code($data)' directive"; - } - } else { - if ($pod =~ /\G(.+)/) { - warn "parse error while expanding $pod (at $1)"; - } - last; - } - } - - for ($xml) { - # create single paragraphs (very hackish) - s/(?<=\S)\n(?=\w)/ /g; - - # compress some whitespace - s/\s+\n/\n/g; # ws line-ends - s/\n\n+/\n/g; # double lines - s/^\n+//; # beginning lines - s/\n+$//; # ending lines - } - - $xml -} - -no re 'eval'; +=item $protocol_xml = $player->expand_cfpod ($cfpod) -sub hintmode { - $_[0]{hintmode} = $_[1] if @_ > 1; - $_[0]{hintmode} -} +Expand deliantra pod fragments into protocol xml. =item $player->ext_reply ($msgid, @msg) @@ -1789,7 +1787,7 @@ # mit "rum" bekleckern, nicht $self->_create_random_map ( $rmp->{wallstyle}, $rmp->{wall_name}, $rmp->{floorstyle}, $rmp->{monsterstyle}, - $rmp->{treasurestyle}, $rmp->{layoutstyle}, $rmp->{doorstyle}, $rmp->{decorstyle}, + $rmp->{treasurestyle}, $rmp->{layoutstyle}, $rmp->{doorstyle}, $rmp->{decorstyle}, $rmp->{miningstyle}, $rmp->{origin_map}, $rmp->{final_map}, $rmp->{exitstyle}, $rmp->{this_map}, $rmp->{exit_on_final_map}, $rmp->{xsize}, $rmp->{ysize}, @@ -1929,15 +1927,6 @@ "$UNIQUEDIR/$path" } -# 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 decay_objects { my ($self) = @_; @@ -2029,13 +2018,10 @@ $path = normalise $path, $origin && $origin->path; - cf::lock_wait "map_data:$path";#d#remove - cf::lock_wait "map_find:$path"; + my $guard1 = cf::lock_acquire "map_data:$path";#d#remove + my $guard2 = cf::lock_acquire "map_find:$path"; $cf::MAP{$path} || do { - my $guard1 = cf::lock_acquire "map_data:$path"; # just for the fun of it - my $guard2 = cf::lock_acquire "map_find:$path"; - my $map = new_from_path cf::map $path or return; @@ -2057,8 +2043,8 @@ } } -sub pre_load { } -sub post_load { } +sub pre_load { } +#sub post_load { } # XS sub load { my ($self) = @_; @@ -2085,7 +2071,7 @@ $self->_load_objects ($f) or return; - $self->set_object_flag (cf::FLAG_OBJ_ORIGINAL, 1) + $self->post_load_original if delete $self->{load_original}; if (my $uniq = $self->uniq_path) { @@ -2113,8 +2099,6 @@ $self->fix_auto_apply; $self->update_buttons; cf::cede_to_tick; - $self->set_darkness_map; - cf::cede_to_tick; $self->activate; } @@ -2127,6 +2111,9 @@ $self->post_load; } +# customize the map for a given player, i.e. +# return the _real_ map. used by e.g. per-player +# maps to change the path to ~playername/mappath sub customise_for { my ($self, $ob) = @_; @@ -2290,7 +2277,7 @@ return if $self->players; - warn "resetting map ", $self->path; + warn "resetting map ", $self->path, "\n"; $self->in_memory (cf::MAP_SWAPPED); @@ -2391,6 +2378,38 @@ ] } +=item cf::map::static_maps + +Returns an arrayref if paths of all static maps (all preinstalled F<.map> +file in the shared directory excluding F and F). May +block. + +=cut + +sub static_maps() { + my @dirs = ""; + my @maps; + + while (@dirs) { + my $dir = shift @dirs; + + next if $dir eq "/styles" || $dir eq "/editor"; + + my ($dirs, $files) = Coro::AIO::aio_scandir "$MAPDIR$dir", 2 + or return; + + for (@$files) { + s/\.map$// or next; + utf8::decode $_; + push @maps, "$dir/$_"; + } + + push @dirs, map "$dir/$_", @$dirs; + } + + \@maps +} + =back =head3 cf::object @@ -2465,7 +2484,7 @@ id => "say", title => "Map", reply => "say ", - tooltip => "Things said to and replied from npcs near you and other players on the same map only.", + tooltip => "Things said to and replied from NPCs near you and other players on the same map only.", }; our $CHAT_CHANNEL = { @@ -2530,8 +2549,10 @@ Freezes the player and moves him/her to a special map (C<{link}>). -The player should be reasonably safe there for short amounts of time. You -I call C as soon as possible, though. +The player should be reasonably safe there for short amounts of time (e.g. +for loading a map). You I call C as soon as possible, +though, as the palyer cannot control the character while it is on the link +map. Will never block. @@ -2593,16 +2614,18 @@ # use -1 or undef as default coordinates, not 0, 0 ($x, $y) = ($map->enter_x, $map->enter_y) - if $x <=0 && $y <= 0; + if $x <= 0 && $y <= 0; $map->load; $map->load_neighbours; return unless $self->contr->active; - $self->activate_recursive; local $self->{_prev_pos} = $link_pos; # ugly hack for rent.ext $self->enter_map ($map, $x, $y); + + # only activate afterwards, to support waiting in hooks + $self->activate_recursive; } =item $player_object->goto ($path, $x, $y[, $check->($map)[, $done->()]]) @@ -2613,6 +2636,9 @@ the new (success) or old (failed) map position. In either case, $done will be called at the end of this process. +Note that $check will be called with a potentially non-loaded map, so if +it needs a loaded map it has to call C<< ->load >>. + =cut our $GOTOGEN; @@ -2768,7 +2794,7 @@ 1 }) { - $self->message ("Something went wrong deep within the crossfire server. " + $self->message ("Something went wrong deep within the deliantra 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); @@ -2797,6 +2823,31 @@ $self->send_packet (sprintf "drawinfo %d %s", $flags || cf::NDI_BLACK, $text); } +=item $client->send_big_packet ($pkt) + +Like C, but tries to compress large packets, and fragments +them as required. + +=cut + +our $MAXFRAGSIZE = cf::MAXSOCKBUF - 64; + +sub cf::client::send_big_packet { + my ($self, $pkt) = @_; + + # try lzf for large packets + $pkt = "lzf " . Compress::LZF::compress $pkt + if 1024 <= length $pkt and $self->{can_lzf}; + + # split very large packets + if ($MAXFRAGSIZE < length $pkt and $self->{can_lzf}) { + $self->send_packet ("frag $_") for unpack "(a$MAXFRAGSIZE)*", $pkt; + $pkt = "frag"; + } + + $self->send_packet ($pkt); +} + =item $client->send_msg ($channel, $msg, $color, [extra...]) Send a drawinfo or msg packet to the client, formatting the msg for the @@ -2808,6 +2859,12 @@ # non-persistent channels (usually the info channel) our %CHANNEL = ( + "c/motd" => { + id => "infobox", + title => "MOTD", + reply => undef, + tooltip => "The message of the day", + }, "c/identify" => { id => "infobox", title => "Identify", @@ -2820,6 +2877,12 @@ reply => undef, tooltip => "Signs and other items you examined", }, + "c/shopinfo" => { + id => "infobox", + title => "Shop Info", + reply => undef, + tooltip => "What your bargaining skill tells you about the shop", + }, "c/book" => { id => "infobox", title => "Book", @@ -2844,6 +2907,42 @@ reply => undef, tooltip => "Shows which body parts you posess and are available", }, + "c/statistics" => { + id => "infobox", + title => "Statistics", + reply => undef, + tooltip => "Shows your primary statistics", + }, + "c/skills" => { + id => "infobox", + title => "Skills", + reply => undef, + tooltip => "Shows your experience per skill and item power", + }, + "c/shopitems" => { + id => "infobox", + title => "Shop Items", + reply => undef, + tooltip => "Shows the items currently for sale in this shop", + }, + "c/resistances" => { + id => "infobox", + title => "Resistances", + reply => undef, + tooltip => "Shows your resistances", + }, + "c/pets" => { + id => "infobox", + title => "Pets", + reply => undef, + tooltip => "Shows information abotu your pets/a specific pet", + }, + "c/perceiveself" => { + id => "infobox", + title => "Perceive Self", + reply => undef, + tooltip => "You gained detailed knowledge about yourself", + }, "c/uptime" => { id => "infobox", title => "Uptime", @@ -2862,12 +2961,21 @@ reply => "gsay ", tooltip => "Messages and chat related to your party", }, + "c/death" => { + id => "death", + title => "Death", + reply => undef, + tooltip => "Reason for and more info about your most recent death", + }, + "c/say" => $SAY_CHANNEL, + "c/chat" => $CHAT_CHANNEL, ); sub cf::client::send_msg { my ($self, $channel, $msg, $color, @extra) = @_; - $msg = $self->pl->expand_cfpod ($msg); + $msg = $self->pl->expand_cfpod ($msg) + unless $color & cf::NDI_VERBATIM; $color &= cf::NDI_CLIENT_MASK; # just in case... @@ -2875,17 +2983,14 @@ if ($CHANNEL{$channel}) { $channel = $CHANNEL{$channel}; - $self->ext_msg (channel_info => $channel) - if $self->can_msg; - + $self->ext_msg (channel_info => $channel); $channel = $channel->{id}; } elsif (ref $channel) { # send meta info to client, if not yet sent unless (exists $self->{channel}{$channel->{id}}) { $self->{channel}{$channel->{id}} = $channel; - $self->ext_msg (channel_info => $channel) - if $self->can_msg; + $self->ext_msg (channel_info => $channel); } $channel = $channel->{id}; @@ -2893,38 +2998,16 @@ return unless @extra || length $msg; - if ($self->can_msg) { - # default colour, mask it out - $color &= ~(cf::NDI_COLOR_MASK | cf::NDI_DEF) - if $color & cf::NDI_DEF; + # default colour, mask it out + $color &= ~(cf::NDI_COLOR_MASK | cf::NDI_DEF) + if $color & cf::NDI_DEF; + + my $pkt = "msg " + . $self->{json_coder}->encode ( + [$color & cf::NDI_CLIENT_MASK, $channel, $msg, @extra] + ); - $self->send_packet ("msg " . $self->{json_coder}->encode ( - [$color & cf::NDI_CLIENT_MASK, $channel, $msg, @extra])); - } else { - if ($color >= 0) { - # replace some tags by gcfclient-compatible ones - for ($msg) { - 1 while - s/([^<]*)<\/b>/[b]${1}[\/b]/ - || s/([^<]*)<\/i>/[i]${1}[\/i]/ - || s/([^<]*)<\/u>/[ul]${1}[\/ul]/ - || s/([^<]*)<\/tt>/[fixed]${1}[\/fixed]/ - || s/([^<]*)<\/fg>/[color=$1]${2}[\/color]/; - } - - $color &= cf::NDI_COLOR_MASK; - - utf8::encode $msg; - - if (0 && $msg =~ /\[/) { - # COMMAND/INFO - $self->send_packet ("drawextinfo $color 10 8 $msg") - } else { - $msg =~ s/\[\/?(?:b|i|u|fixed|color)[^\]]*\]//g; - $self->send_packet ("drawinfo $color $msg") - } - } - } + $self->send_big_packet ($pkt); } =item $client->ext_msg ($type, @msg) @@ -2937,10 +3020,10 @@ my ($self, $type, @msg) = @_; if ($self->extcmd == 2) { - $self->send_packet ("ext " . $self->{json_coder}->encode ([$type, @msg])); + $self->send_big_packet ("ext " . $self->{json_coder}->encode ([$type, @msg])); } elsif ($self->extcmd == 1) { # TODO: remove push @msg, msgtype => "event_$type"; - $self->send_packet ("ext " . $self->{json_coder}->encode ({@msg})); + $self->send_big_packet ("ext " . $self->{json_coder}->encode ({@msg})); } } @@ -2954,11 +3037,11 @@ my ($self, $id, @msg) = @_; if ($self->extcmd == 2) { - $self->send_packet ("ext " . $self->{json_coder}->encode (["reply-$id", @msg])); + $self->send_big_packet ("ext " . $self->{json_coder}->encode (["reply-$id", @msg])); } elsif ($self->extcmd == 1) { #TODO: version 1, remove unshift @msg, msgtype => "reply", msgid => $id; - $self->send_packet ("ext " . $self->{json_coder}->encode ({@msg})); + $self->send_big_packet ("ext " . $self->{json_coder}->encode ({@msg})); } } @@ -3069,7 +3152,7 @@ } cf::client->attach ( - on_destroy => sub { + on_client_destroy => sub { my ($ns) = @_; $_->cancel for values %{ (delete $ns->{_coro}) || {} }; @@ -3095,7 +3178,7 @@ $SIG{FPE} = 'IGNORE'; $safe->permit_only (Opcode::opset qw( - :base_core :base_mem :base_orig :base_math + :base_core :base_mem :base_orig :base_math :base_loop grepstart grepwhile mapstart mapwhile sort time )); @@ -3109,6 +3192,7 @@ cf::object contr pay_amount pay_player map x y force_find force_add destroy insert remove name archname title slaying race decrease split + value cf::object::player player @@ -3123,10 +3207,10 @@ for ( ["cf::object" => qw(contr pay_amount pay_player map force_find force_add x y - insert remove inv name archname title slaying race - decrease split destroy)], + insert remove inv nrof name archname title slaying race + decrease split destroy change_exp value msg lore send_msg)], ["cf::object::player" => qw(player)], - ["cf::player" => qw(peaceful)], + ["cf::player" => qw(peaceful send_msg)], ["cf::map" => qw(trigger)], ) { no strict 'refs'; @@ -3154,8 +3238,10 @@ $qcode =~ s/"/‟/g; # not allowed in #line filenames $qcode =~ s/\n/\\n/g; + %vars = (_dummy => 0) unless %vars; + + my @res; local $_; - local @safe::cf::_safe_eval_args = values %vars; my $eval = "do {\n" @@ -3165,9 +3251,15 @@ . "\n}" ; - sub_generation_inc; - my @res = wantarray ? $safe->reval ($eval) : scalar $safe->reval ($eval); - sub_generation_inc; + if ($CFG{safe_eval}) { + sub_generation_inc; + local @safe::cf::_safe_eval_args = values %vars; + @res = wantarray ? $safe->reval ($eval) : scalar $safe->reval ($eval); + sub_generation_inc; + } else { + local @cf::_safe_eval_args = values %vars; + @res = wantarray ? eval eval : scalar eval $eval; + } if ($@) { warn "$@"; @@ -3196,8 +3288,8 @@ sub register_script_function { my ($fun, $cb) = @_; - no strict 'refs'; - *{"safe::$fun"} = $safe_hole->wrap ($cb); + $fun = "safe::$fun" if $CFG{safe_eval}; + *$fun = $safe_hole->wrap ($cb); } =back @@ -3228,9 +3320,11 @@ or cf::cleanup "$path: version mismatch, cannot proceed."; # patch in the exptable + my $exp_table = $enc->encode ([map cf::level_to_min_exp $_, 1 .. cf::settings->max_level]); $facedata->{resource}{"res/exp_table"} = { type => FT_RSRC, - data => $enc->encode ([map cf::level_to_min_exp $_, 1 .. cf::settings->max_level]), + data => $exp_table, + hash => (Digest::MD5::md5 $exp_table), }; cf::cede_to_tick; @@ -3242,8 +3336,8 @@ cf::face::set_visibility $idx, $info->{visibility}; cf::face::set_magicmap $idx, $info->{magicmap}; - cf::face::set_data $idx, 0, $info->{data32}, Digest::MD5::md5 $info->{data32}; - cf::face::set_data $idx, 1, $info->{data64}, Digest::MD5::md5 $info->{data64}; + cf::face::set_data $idx, 0, $info->{data32}, $info->{hash32}; + cf::face::set_data $idx, 1, $info->{data64}, $info->{hash64}; cf::cede_to_tick; } @@ -3277,29 +3371,13 @@ } { - # TODO: for gcfclient pleasure, we should give resources - # that gcfclient doesn't grok a >10000 face index. my $res = $facedata->{resource}; while (my ($name, $info) = each %$res) { if (defined $info->{type}) { my $idx = (cf::face::find $name) || cf::face::alloc $name; - my $data; - - if ($info->{type} & 1) { - # prepend meta info - - my $meta = $enc->encode ({ - name => $name, - %{ $info->{meta} || {} }, - }); - $data = pack "(w/a*)*", $meta, $info->{data}; - } else { - $data = $info->{data}; - } - - cf::face::set_data $idx, 0, $data, Digest::MD5::md5 $data; + cf::face::set_data $idx, 0, $info->{data}, $info->{hash}; cf::face::set_type $idx, $info->{type}; } else { $RESOURCE{$name} = $info; @@ -3392,18 +3470,14 @@ warn "finished reloading resource files\n"; } -sub init { - my $guard = freeze_mainloop; - - reload_resources; -} - sub reload_config { + warn "reloading config file...\n"; + open my $fh, "<:utf8", "$CONFDIR/config" or return; local $/; - *CFG = YAML::Load <$fh>; + *CFG = YAML::XS::Load scalar <$fh>; $EMERGENCY_POSITION = $CFG{emergency_position} || ["/world/world_105_115", 5, 37]; @@ -3417,9 +3491,53 @@ }; warn $@ if $@; } + + warn "finished reloading resource files\n"; +} + +sub pidfile() { + sysopen my $fh, $PIDFILE, O_RDWR | O_CREAT + or die "$PIDFILE: $!"; + flock $fh, &Fcntl::LOCK_EX + or die "$PIDFILE: flock: $!"; + $fh +} + +# make sure only one server instance is running at any one time +sub atomic { + my $fh = pidfile; + + my $pid = <$fh>; + kill 9, $pid if $pid > 0; + + seek $fh, 0, 0; + print $fh $$; +} + +sub main_loop { + warn "EV::loop starting\n"; + if (1) { + EV::loop; + } + warn "EV::loop returned\n"; + goto &main_loop unless $REALLY_UNLOOP; } sub main { + cf::init_globals; # initialise logging + + LOG llevInfo, "Welcome to Deliantra, v" . VERSION; + LOG llevInfo, "Copyright (C) 2005-2008 Marc Alexander Lehmann / Robin Redeker / the Deliantra team."; + LOG llevInfo, "Copyright (C) 1994 Mark Wedel."; + LOG llevInfo, "Copyright (C) 1992 Frank Tore Johansen."; + + cf::init_experience; + cf::init_anim; + cf::init_attackmess; + cf::init_dynamic; + + $Coro::current->prio (Coro::PRIO_MAX); # give the main loop max. priority + # we must not ever block the main coroutine local $Coro::idle = sub { Carp::cluck "FATAL: Coro::idle was called, major BUG, use cf::sync_job!\n";#d# @@ -3429,17 +3547,36 @@ })->prio (Coro::PRIO_MAX); }; - { - my $guard = freeze_mainloop; + evthread_start IO::AIO::poll_fileno; + + cf::sync_job { + cf::load_settings; + cf::load_materials; + + reload_resources; reload_config; db_init; + + cf::init_uuid; + cf::init_signals; + cf::init_skills; + + cf::init_beforeplay; + + atomic; + load_extensions; - $Coro::current->prio (Coro::PRIO_MAX); # give the main loop max. priority - evthread_start IO::AIO::poll_fileno; - } + utime time, time, $RUNTIMEFILE; - EV::loop; + # no (long-running) fork's whatsoever before this point(!) + use POSIX (); + POSIX::close delete $ENV{LOCKUTIL_LOCK_FD} if exists $ENV{LOCKUTIL_LOCK_FD}; + + (pop @POST_INIT)->(0) while @POST_INIT; + }; + + main_loop; } ############################################################################# @@ -3456,16 +3593,16 @@ } sub write_runtime_sync { - my $runtime = "$LOCALDIR/runtime"; + my $t0 = EV::time; # first touch the runtime file to show we are still running: # the fsync below can take a very very long time. - IO::AIO::aio_utime $runtime, undef, undef; + IO::AIO::aio_utime $RUNTIMEFILE, undef, undef; my $guard = cf::lock_acquire "write_runtime"; - my $fh = aio_open "$runtime~", O_WRONLY | O_CREAT, 0644 + my $fh = aio_open "$RUNTIMEFILE~", O_WRONLY | O_CREAT | O_TRUNC, 0644 or return; my $value = $cf::RUNTIME + 90 + 10; @@ -3485,10 +3622,10 @@ close $fh or return; - aio_rename "$runtime~", $runtime + aio_rename "$RUNTIMEFILE~", $RUNTIMEFILE and return; - warn "runtime file written.\n"; + warn sprintf "runtime file written (%gs).\n", EV::time - $t0; 1 } @@ -3507,7 +3644,14 @@ my $fh = aio_open "$uuid~", O_WRONLY | O_CREAT, 0644 or return; - my $value = uuid_str $uuid_skip + uuid_seq uuid_cur; + my $value = uuid_seq uuid_cur; + + unless ($value) { + warn "cowardly refusing to write zero uuid value!\n"; + return; + } + + my $value = uuid_str $value + $uuid_skip; $uuid_skip = 0; (aio_write $fh, 0, (length $value), $value, 0) <= 0 @@ -3539,39 +3683,49 @@ sub emergency_save() { my $freeze_guard = cf::freeze_mainloop; - warn "enter emergency perl save\n"; + warn "emergency_perl_save: enter\n"; cf::sync_job { + # this is a trade-off: we want to be very quick here, so + # save all maps without fsync, and later call a global sync + # (which in turn might be very very slow) + local $USE_FSYNC = 0; + # use a peculiar iteration method to avoid tripping on perl # refcount bugs in for. also avoids problems with players # and maps saved/destroyed asynchronously. - warn "begin emergency player save\n"; + warn "emergency_perl_save: begin player save\n"; for my $login (keys %cf::PLAYER) { my $pl = $cf::PLAYER{$login} or next; $pl->valid or next; delete $pl->{unclean_save}; # not strictly necessary, but cannot hurt $pl->save; } - warn "end emergency player save\n"; + warn "emergency_perl_save: end player save\n"; - warn "begin emergency map save\n"; + warn "emergency_perl_save: begin map save\n"; for my $path (keys %cf::MAP) { my $map = $cf::MAP{$path} or next; $map->valid or next; $map->save; } - warn "end emergency map save\n"; + warn "emergency_perl_save: end map save\n"; - warn "begin emergency database checkpoint\n"; + warn "emergency_perl_save: begin database checkpoint\n"; BDB::db_env_txn_checkpoint $DB_ENV; - warn "end emergency database checkpoint\n"; + warn "emergency_perl_save: end database checkpoint\n"; - warn "begin write uuid\n"; + warn "emergency_perl_save: begin write uuid\n"; write_uuid_sync 1; - warn "end write uuid\n"; + warn "emergency_perl_save: end write uuid\n"; }; - warn "leave emergency perl save\n"; + warn "emergency_perl_save: starting sync()\n"; + IO::AIO::aio_sync sub { + warn "emergency_perl_save: finished sync()\n"; + }; + + warn "emergency_perl_save: leave\n"; } sub post_cleanup { @@ -3579,6 +3733,35 @@ warn Carp::longmess "post_cleanup backtrace" if $make_core; + + my $fh = pidfile; + unlink $PIDFILE if <$fh> == $$; +} + +# a safer delete_package, copied from Symbol +sub clear_package($) { + my $pkg = shift; + + # expand to full symbol table name if needed + unless ($pkg =~ /^main::.*::$/) { + $pkg = "main$pkg" if $pkg =~ /^::/; + $pkg = "main::$pkg" unless $pkg =~ /^main::/; + $pkg .= '::' unless $pkg =~ /::$/; + } + + my($stem, $leaf) = $pkg =~ m/(.*::)(\w+::)$/; + my $stem_symtab = *{$stem}{HASH}; + + defined $stem_symtab and exists $stem_symtab->{$leaf} + or return; + + # clear all symbols + my $leaf_symtab = *{$stem_symtab->{$leaf}}{HASH}; + for my $name (keys %$leaf_symtab) { + _gv_clear *{"$pkg$name"}; +# use PApp::Util; PApp::Util::sv_dump *{"$pkg$name"}; + } + warn "cleared package $pkg\n";#d# } sub do_reload_perl() { @@ -3588,114 +3771,123 @@ return; } - warn "reloading..."; + return if $RELOAD++; - warn "entering sync_job"; + my $t1 = EV::time; - cf::sync_job { - cf::write_runtime_sync; # external watchdog should not bark - cf::emergency_save; - cf::write_runtime_sync; # external watchdog should not bark + while ($RELOAD) { + warn "reloading..."; - warn "syncing database to disk"; - BDB::db_env_txn_checkpoint $DB_ENV; + warn "entering sync_job"; + + cf::sync_job { + cf::write_runtime_sync; # external watchdog should not bark + cf::emergency_save; + cf::write_runtime_sync; # external watchdog should not bark - # if anything goes wrong in here, we should simply crash as we already saved + warn "syncing database to disk"; + BDB::db_env_txn_checkpoint $DB_ENV; - warn "flushing outstanding aio requests"; - for (;;) { - BDB::flush; - IO::AIO::flush; - Coro::cede_notself; - last unless IO::AIO::nreqs || BDB::nreqs; - warn "iterate..."; - } + # if anything goes wrong in here, we should simply crash as we already saved - ++$RELOAD; + warn "flushing outstanding aio requests"; + while (IO::AIO::nreqs || BDB::nreqs) { + Coro::EV::timer_once 0.01; # let the sync_job do it's thing + } - warn "cancelling all extension coros"; - $_->cancel for values %EXT_CORO; - %EXT_CORO = (); + warn "cancelling all extension coros"; + $_->cancel for values %EXT_CORO; + %EXT_CORO = (); - warn "removing commands"; - %COMMAND = (); + warn "removing commands"; + %COMMAND = (); - warn "removing ext/exti commands"; - %EXTCMD = (); - %EXTICMD = (); + warn "removing ext/exti commands"; + %EXTCMD = (); + %EXTICMD = (); - warn "unloading/nuking all extensions"; - for my $pkg (@EXTS) { - warn "... unloading $pkg"; + warn "unloading/nuking all extensions"; + for my $pkg (@EXTS) { + warn "... unloading $pkg"; - if (my $cb = $pkg->can ("unload")) { - eval { - $cb->($pkg); - 1 - } or warn "$pkg unloaded, but with errors: $@"; + if (my $cb = $pkg->can ("unload")) { + eval { + $cb->($pkg); + 1 + } or warn "$pkg unloaded, but with errors: $@"; + } + + warn "... clearing $pkg"; + clear_package $pkg; } - warn "... nuking $pkg"; - Symbol::delete_package $pkg; - } + warn "unloading all perl modules loaded from $LIBDIR"; + while (my ($k, $v) = each %INC) { + next unless $v =~ /^\Q$LIBDIR\E\/.*\.pm$/; - warn "unloading all perl modules loaded from $LIBDIR"; - while (my ($k, $v) = each %INC) { - next unless $v =~ /^\Q$LIBDIR\E\/.*\.pm$/; + warn "... unloading $k"; + delete $INC{$k}; - warn "... unloading $k"; - delete $INC{$k}; + $k =~ s/\.pm$//; + $k =~ s/\//::/g; - $k =~ s/\.pm$//; - $k =~ s/\//::/g; + if (my $cb = $k->can ("unload_module")) { + $cb->(); + } - if (my $cb = $k->can ("unload_module")) { - $cb->(); + clear_package $k; } - Symbol::delete_package $k; - } + warn "getting rid of safe::, as good as possible"; + clear_package "safe::$_" + for qw(cf::attachable cf::object cf::object::player cf::client cf::player cf::map cf::party cf::region); - warn "getting rid of safe::, as good as possible"; - Symbol::delete_package "safe::$_" - for qw(cf::attachable cf::object cf::object::player cf::client cf::player cf::map cf::party cf::region); + warn "unloading cf.pm \"a bit\""; + delete $INC{"cf.pm"}; + delete $INC{"cf/$_.pm"} for @EXTRA_MODULES; - warn "unloading cf.pm \"a bit\""; - delete $INC{"cf.pm"}; - delete $INC{"cf/pod.pm"}; + # don't, removes xs symbols, too, + # and global variables created in xs + #clear_package __PACKAGE__; - # don't, removes xs symbols, too, - # and global variables created in xs - #Symbol::delete_package __PACKAGE__; + warn "unload completed, starting to reload now"; - warn "unload completed, starting to reload now"; + warn "reloading cf.pm"; + require cf; + cf::_connect_to_perl_1; - warn "reloading cf.pm"; - require cf; - cf::_connect_to_perl; # nominally unnecessary, but cannot hurt + warn "loading config and database again"; + cf::reload_config; - warn "loading config and database again"; - cf::reload_config; + warn "loading extensions"; + cf::load_extensions; - warn "loading extensions"; - cf::load_extensions; + if ($REATTACH_ON_RELOAD) { + warn "reattaching attachments to objects/players"; + _global_reattach; # objects, sockets + warn "reattaching attachments to maps"; + reattach $_ for values %MAP; + warn "reattaching attachments to players"; + reattach $_ for values %PLAYER; + } - warn "reattaching attachments to objects/players"; - _global_reattach; # objects, sockets - warn "reattaching attachments to maps"; - reattach $_ for values %MAP; - warn "reattaching attachments to players"; - reattach $_ for values %PLAYER; + warn "running post_init jobs"; + (pop @POST_INIT)->(1) while @POST_INIT; - warn "leaving sync_job"; + warn "leaving sync_job"; - 1 - } or do { - warn $@; - cf::cleanup "error while reloading, exiting."; - }; + 1 + } or do { + warn $@; + cf::cleanup "error while reloading, exiting."; + }; - warn "reloaded"; + warn "reloaded"; + --$RELOAD; + } + + $t1 = EV::time - $t1; + warn "reload completed in ${t1}s\n"; }; our $RELOAD_WATCHER; # used only during reload @@ -3704,9 +3896,13 @@ # doing reload synchronously and two reloads happen back-to-back, # coro crashes during coro_state_free->destroy here. - $RELOAD_WATCHER ||= EV::timer 0, 0, sub { - do_reload_perl; - undef $RELOAD_WATCHER; + $RELOAD_WATCHER ||= cf::async { + Coro::AIO::aio_wait cache_extensions; + + $RELOAD_WATCHER = EV::timer $TICK * 1.5, 0, sub { + do_reload_perl; + undef $RELOAD_WATCHER; + }; }; } @@ -3730,7 +3926,7 @@ our @WAIT_FOR_TICK_BEGIN; sub wait_for_tick { - return if tick_inhibit || $Coro::current == $Coro::main; + return Coro::cede if tick_inhibit || $Coro::current == $Coro::main; my $signal = new Coro::Signal; push @WAIT_FOR_TICK, $signal; @@ -3738,7 +3934,7 @@ } sub wait_for_tick_begin { - return if tick_inhibit || $Coro::current == $Coro::main; + return Coro::cede if tick_inhibit || $Coro::current == $Coro::main; my $signal = new Coro::Signal; push @WAIT_FOR_TICK_BEGIN, $signal; @@ -3754,6 +3950,8 @@ cf::server_tick; # one server iteration + #for(1..3e6){} EV::now_update; $NOW=EV::now; # generate load #d# + if ($NOW >= $NEXT_RUNTIME_WRITE) { $NEXT_RUNTIME_WRITE = List::Util::max $NEXT_RUNTIME_WRITE + 10, $NOW + 5.; Coro::async_pool { @@ -3785,14 +3983,15 @@ { # configure BDB - BDB::min_parallel 8; + BDB::min_parallel 16; BDB::max_poll_reqs $TICK * 0.1; - $Coro::BDB::WATCHER->priority (1); + $AnyEvent::BDB::WATCHER->priority (1); unless ($DB_ENV) { $DB_ENV = BDB::db_env_create; - $DB_ENV->set_flags (BDB::AUTO_COMMIT | BDB::REGION_INIT | BDB::TXN_NOSYNC - | BDB::LOG_AUTOREMOVE, 1); + $DB_ENV->set_flags (BDB::AUTO_COMMIT | BDB::REGION_INIT); + $DB_ENV->set_flags (&BDB::LOG_AUTOREMOVE ) if BDB::VERSION v0, v4.7; + $DB_ENV->log_set_config (&BDB::LOG_AUTO_REMOVE) if BDB::VERSION v4.7; $DB_ENV->set_timeout (30, BDB::SET_TXN_TIMEOUT); $DB_ENV->set_timeout (30, BDB::SET_LOCK_TIMEOUT); @@ -3828,7 +4027,7 @@ IO::AIO::min_parallel 8; IO::AIO::max_poll_time $TICK * 0.1; - $Coro::AIO::WATCHER->priority (1); + undef $AnyEvent::AIO::WATCHER; } my $_log_backtrace; @@ -3841,6 +4040,7 @@ # limit the # of concurrent backtraces if ($_log_backtrace < 2) { ++$_log_backtrace; + my $perl_bt = Carp::longmess $msg; async { $Coro::current->{desc} = "abt $msg"; @@ -3861,7 +4061,8 @@ @funcs }; - LOG llevInfo, "[ABT] $msg\n"; + LOG llevInfo, "[ABT] $perl_bt\n"; + LOG llevInfo, "[ABT] --- C backtrace follows ---\n"; LOG llevInfo, "[ABT] $_\n" for @bt; --$_log_backtrace; }; @@ -3872,7 +4073,8 @@ } # load additional modules -use cf::pod; +require "cf/$_.pm" for @EXTRA_MODULES; +cf::_connect_to_perl_2; END { cf::emergency_save }