--- deliantra/server/lib/cf.pm 2007/02/12 00:27:20 1.208 +++ deliantra/server/lib/cf.pm 2007/07/10 07:01:51 1.301 @@ -5,53 +5,76 @@ use Symbol; use List::Util; +use Socket; use Storable; +use Event; use Opcode; use Safe; use Safe::Hole; -use Coro 3.5 (); +use Coro 3.61 (); +use Coro::State; +use Coro::Handle; use Coro::Event; use Coro::Timer; use Coro::Signal; use Coro::Semaphore; use Coro::AIO; +use Coro::Storable; +use JSON::XS 1.4 (); use BDB (); use Data::Dumper; use Digest::MD5; use Fcntl; -use IO::AIO 2.32 (); use YAML::Syck (); +use IO::AIO 2.32 (); use Time::HiRes; use Compress::LZF; +# 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 -use Event; $Event::Eval = 1; # no idea why this is required, but it is - -sub WF_AUTOCANCEL () { 1 } # automatically cancel this watcher on reload +$Event::Eval = 1; # no idea why this is required, but it is # work around bug in YAML::Syck - bad news for perl6, will it be as broken wrt. unicode? $YAML::Syck::ImplicitUnicode = 1; $Coro::main->prio (Coro::PRIO_MAX); # run main coroutine ("the server") with very high priority +sub WF_AUTOCANCEL () { 1 } # automatically cancel this watcher on reload + our %COMMAND = (); our %COMMAND_TIME = (); our @EXTS = (); # list of extension package names our %EXTCMD = (); +our %EXTICMD = (); our %EXT_CORO = (); # coroutines bound to extensions our %EXT_MAP = (); # pluggable maps +our $RELOAD; # number of reloads so far our @EVENT; -our $LIBDIR = datadir . "/ext"; -our $TICK = MAX_TIME * 1e-6; +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 $TICK = MAX_TIME * 1e-6; # this is a CONSTANT(!) our $TICK_WATCHER; our $AIO_POLL_WATCHER; -our $WRITE_RUNTIME_WATCHER; +our $NEXT_RUNTIME_WRITE; # when should the runtime file be written our $NEXT_TICK; our $NOW; our $USE_FSYNC = 1; # use fsync to write maps - default off @@ -67,31 +90,26 @@ our %PLAYER; # all users our %MAP; # all maps our $LINK_MAP; # the special {link} map, which is always available -our $RANDOM_MAPS = cf::localdir . "/random"; -our $BDB_ENV_DIR = cf::localdir . "/db"; - -our $WAIT_FOR_TICK; $WAIT_FOR_TICK ||= new Coro::Signal; -our $WAIT_FOR_TICK_ONE; $WAIT_FOR_TICK_ONE ||= new Coro::Signal; # used to convert map paths into valid unix filenames by replacing / by ∕ our $PATH_SEP = "∕"; # U+2215, chosen purely for visual reasons +our $LOAD; # a number between 0 (idle) and 1 (too many objects) +our $LOADAVG; # same thing, but with alpha-smoothing +our $tick_start; # for load detecting purposes + binmode STDOUT; binmode STDERR; # read virtual server time, if available -unless ($RUNTIME || !-e cf::localdir . "/runtime") { - open my $fh, "<", cf::localdir . "/runtime" +unless ($RUNTIME || !-e "$LOCALDIR/runtime") { + open my $fh, "<", "$LOCALDIR/runtime" or die "unable to read runtime file: $!"; $RUNTIME = <$fh> + 0.; } -mkdir cf::localdir; -mkdir cf::localdir . "/" . cf::playerdir; -mkdir cf::localdir . "/" . cf::tmpdir; -mkdir cf::localdir . "/" . cf::uniquedir; -mkdir $RANDOM_MAPS; -mkdir $BDB_ENV_DIR; +mkdir $_ + for $LOCALDIR, $TMPDIR, $UNIQUEDIR, $PLAYERDIR, $RANDOMDIR, $BDBDIR; our $EMERGENCY_POSITION; @@ -112,10 +130,14 @@ The time this server has run, starts at 0 and is increased by $cf::TICK on every server tick. -=item $cf::LIBDIR - -The perl library directory, where extensions and cf-specific modules can -be found. It will be added to C<@INC> automatically. +=item $cf::CONFDIR $cf::DATADIR $cf::LIBDIR $cf::PODDIR +$cf::MAPDIR $cf::LOCALDIR $cf::TMPDIR $cf::UNIQUEDIR +$cf::PLAYERDIR $cf::RANDOMDIR $cf::BDBDIR + +Various directories - "/etc", read-only install directory, perl-library +directory, pod-directory, read-only maps directory, "/var", "/var/tmp", +unique-items directory, player file directory, random maps directory and +database environment. =item $cf::NOW @@ -125,17 +147,26 @@ The interval between server ticks, in seconds. +=item $cf::LOADAVG + +The current CPU load on the server (alpha-smoothed), as a value between 0 +(none) and 1 (overloaded), indicating how much time is spent on processing +objects per tick. Healthy values are < 0.5. + +=item $cf::LOAD + +The raw value load value from the last tick. + =item %cf::CFG Configuration for the server, loaded from C, or from wherever your confdir points to. -=item $cf::WAIT_FOR_TICK, $cf::WAIT_FOR_TICK_ONE +=item cf::wait_for_tick, cf::wait_for_tick_begin -These are Coro::Signal objects that are C<< ->broadcast >> (WAIT_FOR_TICK) -or C<< ->send >> (WAIT_FOR_TICK_ONE) on after normal server tick -processing has been done. Call C<< ->wait >> on them to maximise the -window of cpu time available, or simply to synchronise to the server tick. +These are functions that inhibit the current coroutine one tick. cf::wait_for_tick_begin only +returns directly I the tick processing (and consequently, can only wake one process +per tick), while cf::wait_for_tick wakes up all waiters after tick processing. =back @@ -144,11 +175,13 @@ BEGIN { *CORE::GLOBAL::warn = sub { my $msg = join "", @_; - utf8::encode $msg; $msg .= "\n" unless $msg =~ /\n$/; + $msg =~ s/([\x00-\x08\x0b-\x1f])/sprintf "\\x%02x", ord $1/ge; + + utf8::encode $msg; LOG llevError, $msg; }; } @@ -158,7 +191,8 @@ @safe::cf::player::ISA = @cf::player::ISA = 'cf::attachable'; @safe::cf::client::ISA = @cf::client::ISA = 'cf::attachable'; @safe::cf::map::ISA = @cf::map::ISA = 'cf::attachable'; -@safe::cf::object::player::ISA = @cf::object::player::ISA = 'cf::object'; +@safe::cf::arch::ISA = @cf::arch::ISA = 'cf::object'; +@safe::cf::object::player::ISA = @cf::object::player::ISA = 'cf::object'; # not really true (yet) # we bless all objects into (empty) derived classes to force a method lookup # within the Safe compartment. @@ -177,6 +211,8 @@ warn "error in event callback: @_"; }; +############################################################################# + =head2 UTILITY FUNCTIONS =over 4 @@ -204,29 +240,20 @@ } || "[unable to dump $_[0]: '$@']"; } -use JSON::Syck (); # TODO# replace by JSON::PC once working - =item $ref = cf::from_json $json Converts a JSON string into the corresponding perl data structure. -=cut - -sub from_json($) { - $JSON::Syck::ImplicitUnicode = 1; # work around JSON::Syck bugs - JSON::Syck::Load $_[0] -} - =item $json = cf::to_json $ref Converts a perl data structure into its JSON representation. =cut -sub to_json($) { - $JSON::Syck::ImplicitUnicode = 0; # work around JSON::Syck bugs - JSON::Syck::Dump $_[0] -} +our $json_coder = JSON::XS->new->utf8->max_size (1e6); # accept ~1mb max + +sub to_json ($) { $json_coder->encode ($_[0]) } +sub from_json ($) { $json_coder->decode ($_[0]) } =item cf::lock_wait $string @@ -242,6 +269,10 @@ Lock names should begin with a unique identifier (for example, cf::map::find uses map_find and cf::map::load uses map_load). +=item $locked = cf::lock_active $string + +Return true if the lock is currently active, i.e. somebody has locked it. + =cut our %LOCK; @@ -270,14 +301,18 @@ } } +sub lock_active($) { + my ($key) = @_; + + ! ! $LOCK{$key} +} + sub freeze_mainloop { return unless $TICK_WATCHER->is_active; my $guard = Coro::guard { $TICK_WATCHER->start; - $WRITE_RUNTIME_WATCHER->start; }; - $WRITE_RUNTIME_WATCHER->stop; $TICK_WATCHER->stop; $guard } @@ -294,9 +329,10 @@ =item cf::sync_job { BLOCK } -The design of crossfire+ requires that the main coro ($Coro::main) is -always able to handle events or runnable, as crossfire+ is only partly -reentrant. Thus "blocking" it by e.g. waiting for I/O is not acceptable. +The design of Crossfire TRT requires that the main coroutine ($Coro::main) +is always able to handle events or runnable, as Crossfire TRT is only +partly reentrant. Thus "blocking" it by e.g. waiting for I/O is not +acceptable. If it must be done, put the blocking parts into C. This will run the given BLOCK in another coroutine while waiting for the result. The @@ -309,6 +345,8 @@ my ($job) = @_; if ($Coro::current == $Coro::main) { + my $time = Event::time; + # this is the main coro, too bad, we have to block # till the operation succeeds, freezing the server :/ @@ -329,6 +367,13 @@ Coro::cede or Event::one_event; } + $time = Event::time - $time; + + LOG llevError | logBacktrace, Carp::longmess "long sync job" + if $time > $TICK * 0.5 && $TICK_WATCHER->is_active; + + $tick_start += $time; # do not account sync jobs to server load + wantarray ? @res : $res[0] } else { # we are in another coroutine, how wonderful, everything just works @@ -357,30 +402,193 @@ $coro } -sub write_runtime { - my $runtime = cf::localdir . "/runtime"; +=item fork_call { }, $args - my $fh = aio_open "$runtime~", O_WRONLY | O_CREAT, 0644 - or return; +Executes the given code block with the given arguments in a seperate +process, returning the results. Everything must be serialisable with +Coro::Storable. May, of course, block. Note that the executed sub may +never block itself or use any form of Event handling. - my $value = $cf::RUNTIME + 90 + 10; - # 10 is the runtime save interval, for a monotonic clock - # 60 allows for the watchdog to kill the server. +=cut - (aio_write $fh, 0, (length $value), $value, 0) <= 0 - and return; +sub _store_scalar { + open my $fh, ">", \my $buf + or die "fork_call: cannot open fh-to-buf in child : $!"; + Storable::store_fd $_[0], $fh; + close $fh; - # always fsync - this file is important - aio_fsync $fh - and return; + $buf +} - close $fh - or return; +sub fork_call(&@) { + my ($cb, @args) = @_; - aio_rename "$runtime~", $runtime - and return; +# socketpair my $fh1, my $fh2, Socket::AF_UNIX, Socket::SOCK_STREAM, Socket::PF_UNSPEC +# or die "socketpair: $!"; + pipe my $fh1, my $fh2 + or die "pipe: $!"; - 1 + if (my $pid = fork) { + close $fh2; + + my $res = (Coro::Handle::unblock $fh1)->readline (undef); + warn "pst<$res>" unless $res =~ /^pst/; + $res = Coro::Storable::thaw $res; + + waitpid $pid, 0; # should not block anymore, we expect the child to simply behave + + Carp::confess $$res unless "ARRAY" eq ref $res; + + return wantarray ? @$res : $res->[-1]; + } else { + reset_signals; + local $SIG{__WARN__}; + local $SIG{__DIE__}; + # just in case, this hack effectively disables event + # in the child. cleaner and slower would be canceling all watchers, + # but this works for the time being. + local $Coro::idle; + $Coro::current->prio (Coro::PRIO_MAX); + + eval { + close $fh1; + + my @res = eval { $cb->(@args) }; + + syswrite $fh2, _store_scalar $@ ? \"$@" : \@res; + close $fh2; + }; + + warn $@ if $@; + _exit 0; + } +} + +=item $value = cf::db_get $family => $key + +Returns a single value from the environment database. + +=item cf::db_put $family => $key => $value + +Stores the given C<$value> in the family. It can currently store binary +data only (use Compress::LZF::sfreeze_cr/sthaw to convert to/from binary). + +=cut + +our $DB; + +sub db_init { + unless ($DB) { + $DB = BDB::db_create $DB_ENV; + + cf::sync_job { + eval { + $DB->set_flags (BDB::CHKSUM); + + BDB::db_open $DB, undef, "db", undef, BDB::BTREE, + BDB::CREATE | BDB::AUTO_COMMIT, 0666; + cf::cleanup "db_open(db): $!" if $!; + }; + cf::cleanup "db_open(db): $@" if $@; + }; + } +} + +sub db_get($$) { + my $key = "$_[0]/$_[1]"; + + cf::sync_job { + BDB::db_get $DB, undef, $key, my $data; + + $! ? () + : $data + } +} + +sub db_put($$$) { + BDB::dbreq_pri 4; + BDB::db_put $DB, undef, "$_[0]/$_[1]", $_[2], 0, sub { }; +} + +=item cf::cache $id => [$paths...], $processversion => $process + +Generic caching function that returns the value of the resource $id, +caching and regenerating as required. + +This function can block. + +=cut + +sub cache { + my ($id, $src, $processversion, $process) = @_; + + my $meta = + join "\x00", + $processversion, + map { + aio_stat $_ + and Carp::croak "$_: $!"; + + ($_, (stat _)[7,9]) + } @$src; + + my $dbmeta = db_get cache => "$id/meta"; + if ($dbmeta ne $meta) { + # changed, we may need to process + + my @data; + my $md5; + + for (0 .. $#$src) { + 0 <= aio_load $src->[$_], $data[$_] + or Carp::croak "$src->[$_]: $!"; + } + + # if processing is expensive, check + # checksum first + if (1) { + $md5 = + join "\x00", + $processversion, + map { + Coro::cede; + ($src->[$_], Digest::MD5::md5_hex $data[$_]) + } 0.. $#$src; + + + my $dbmd5 = db_get cache => "$id/md5"; + if ($dbmd5 eq $md5) { + db_put cache => "$id/meta", $meta; + + return db_get cache => "$id/data"; + } + } + + my $t1 = Time::HiRes::time; + my $data = $process->(\@data); + my $t2 = Time::HiRes::time; + + warn "cache: '$id' processed in ", $t2 - $t1, "s\n"; + + db_put cache => "$id/data", $data; + db_put cache => "$id/md5" , $md5; + db_put cache => "$id/meta", $meta; + + return $data; + } + + db_get cache => "$id/data" +} + +=item cf::datalog type => key => value, ... + +Log a datalog packet of the given type with the given key-value pairs. + +=cut + +sub datalog($@) { + my ($type, %kv) = @_; + warn "DATALOG ", JSON::XS->new->ascii->encode ({ %kv, type => $type }); } =back @@ -397,7 +605,7 @@ In the following description, CLASS can be any of C, C C, C or C (i.e. the attachable objects in -crossfire+). +Crossfire TRT). =over 4 @@ -570,7 +778,7 @@ } elsif ($type eq "subtype") { defined $object_type or Carp::croak "subtype specified without type"; my $object_subtype = shift @arg; - $registry = $CB_TYPE[$object_type + $object_subtype * NUM_SUBTYPES] ||= []; + $registry = $CB_TYPE[$object_type + $object_subtype * NUM_TYPES] ||= []; } elsif ($type eq "package") { my $pkg = shift @arg; @@ -609,7 +817,7 @@ $obj->{$name} = \%arg; } else { - warn "object uses attachment '$name' that is not available, postponing.\n"; + warn "object uses attachment '$name' which is not available, postponing.\n"; } $obj->{_attachment}{$name} = undef; @@ -621,6 +829,7 @@ } else { _attach shift->_attach_registry, @_; } + _recalc_want; }; # all those should be optimised @@ -633,6 +842,7 @@ } else { Carp::croak "cannot, currently, detach class attachments"; } + _recalc_want; }; sub cf::attachable::attached { @@ -789,12 +999,6 @@ aio_unlink $filename; aio_unlink "$filename.pst"; } - - #d##TODO# nuke non .map-files if exist - if ($filename =~ s/\.map$//) { - aio_unlink $filename; - aio_unlink "$filename.pst"; - } } } @@ -811,9 +1015,6 @@ my ($data, $av); - #d#TODO remove .map if file does not exist - aio_stat $filename and $filename =~ s/\.map$//; - (aio_load $filename, $data) >= 0 or return; @@ -824,10 +1025,16 @@ } warn sprintf "loading %s (%d)\n", - $filename, length $data, scalar @{$av || []};#d# + $filename, length $data, scalar @{$av || []}; return ($data, $av); } +=head2 COMMAND CALLBACKS + +=over 4 + +=cut + ############################################################################# # command handling &c @@ -849,7 +1056,22 @@ =item cf::register_extcmd $name => \&callback($pl,$packet); -Register a callbackf ro execution when the client sends an extcmd packet. +Register a callback for execution when the client sends an (synchronous) +extcmd packet. Ext commands will be processed in the order they are +received by the server, like other user commands. The first argument is +the logged-in player. Ext commands can only be processed after a player +has logged in successfully. + +If the callback returns something, it is sent back as if reply was being +called. + +=item cf::register_exticmd $name => \&callback($ns,$packet); + +Register a callback for execution when the client sends an (asynchronous) +exticmd packet. Exti commands are processed by the server as soon as they +are received, i.e. out of order w.r.t. other commands. The first argument +is a client socket. Exti commands can be received anytime, even before +log-in. If the callback returns something, it is sent back as if reply was being called. @@ -862,6 +1084,12 @@ $EXTCMD{$name} = $cb; } +sub register_exticmd { + my ($name, $cb) = @_; + + $EXTICMD{$name} = $cb; +} + cf::player->attach ( on_command => sub { my ($pl, $name, $params) = @_; @@ -878,7 +1106,7 @@ on_extcmd => sub { my ($pl, $buf) = @_; - my $msg = eval { from_json $buf }; + my $msg = eval { $pl->ns->{json_coder}->decode ($buf) }; if (ref $msg) { if (my $cb = $EXTCMD{$msg->{msgtype}}) { @@ -894,52 +1122,83 @@ }, ); -sub load_extension { - my ($path) = @_; +sub load_extensions { + cf::sync_job { + my %todo; - $path =~ /([^\/\\]+)\.ext$/ or die "$path"; - my $base = $1; - my $pkg = $1; - $pkg =~ s/[^[:word:]]/_/g; - $pkg = "ext::$pkg"; + for my $path (<$LIBDIR/*.ext>) { + next unless -r $path; - warn "... loading '$path' into '$pkg'\n"; + $path =~ /([^\/\\]+)\.ext$/ or die "$path"; + my $base = $1; + my $pkg = $1; + $pkg =~ s/[^[:word:]]/_/g; + $pkg = "ext::$pkg"; - open my $fh, "<:utf8", $path - or die "$path: $!"; + open my $fh, "<:utf8", $path + or die "$path: $!"; - my $source = - "package $pkg; use strict; use utf8;\n" - . "#line 1 \"$path\"\n{\n" - . (do { local $/; <$fh> }) - . "\n};\n1"; - - unless (eval $source) { - my $msg = $@ ? "$path: $@\n" - : "extension disabled.\n"; - if ($source =~ /^#!.*perl.*#.*MANDATORY/m) { # ugly match - warn $@; - warn "mandatory extension failed to load, exiting.\n"; - exit 1; + my $source = do { local $/; <$fh> }; + + my %ext = ( + path => $path, + base => $base, + pkg => $pkg, + ); + + $ext{meta} = { map { (split /=/, $_, 2)[0, 1] } split /\s+/, $1 } + if $source =~ /\A#!.*?perl.*?#\s*(.*)$/m; + + $ext{source} = + "package $pkg; use strict; use utf8;\n" + . "#line 1 \"$path\"\n{\n" + . $source + . "\n};\n1"; + + $todo{$base} = \%ext; } - die $@; - } - push @EXTS, $pkg; -} + my %done; + while (%todo) { + my $progress; -sub load_extensions { - for my $ext (<$LIBDIR/*.ext>) { - next unless -r $ext; - eval { - load_extension $ext; - 1 - } or warn "$ext not loaded: $@"; - } + while (my ($k, $v) = each %todo) { + for (split /,\s*/, $v->{meta}{depends}) { + goto skip + unless exists $done{$_}; + } + + warn "... 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; + warn "mandatory extension failed to load, exiting.\n"; + exit 1; + } + + warn $msg; + } + + $done{$k} = delete $todo{$k}; + push @EXTS, $v->{pkg}; + $progress = 1; + } + + skip: + die "cannot load " . (join ", ", keys %todo) . ": unable to resolve dependencies\n" + unless $progress; + } + }; } ############################################################################# +=back + =head2 CORE EXTENSIONS Functions and methods that extend core crossfire objects. @@ -961,17 +1220,14 @@ =cut sub playerdir($) { - cf::localdir - . "/" - . cf::playerdir - . "/" + "$PLAYERDIR/" . (ref $_[0] ? $_[0]->ob->name : $_[0]) } sub path($) { my $login = ref $_[0] ? $_[0]->ob->name : $_[0]; - (playerdir $login) . "/$login.pl" + (playerdir $login) . "/playerdata" } sub find_active($) { @@ -994,6 +1250,13 @@ my $guard = cf::lock_acquire "user_find:$login"; $cf::PLAYER{$_[0]} || do { + # rename old playerfiles to new ones + #TODO: remove when no longer required + aio_link +(playerdir $login) . "/$login.pl.pst", (playerdir $login) . "/playerdata.pst"; + aio_link +(playerdir $login) . "/$login.pl" , (playerdir $login) . "/playerdata"; + aio_unlink +(playerdir $login) . "/$login.pl.pst"; + aio_unlink +(playerdir $login) . "/$login.pl"; + my $pl = load_pl path $login or return; $cf::PLAYER{$login} = $pl @@ -1041,6 +1304,8 @@ sub quit_character { my ($pl) = @_; + my $name = $pl->ob->name; + $pl->{deny_save} = 1; $pl->password ("*"); # this should lock out the player until we nuked the dir @@ -1054,9 +1319,29 @@ aio_rename $path, $temp; delete $cf::PLAYER{$pl->ob->name}; $pl->destroy; + + my $prefix = qr<^~\Q$name\E/>; + + # nuke player maps + $cf::MAP{$_}->nuke for grep /$prefix/, keys %cf::MAP; + IO::AIO::aio_rmtree $temp; } +=item $pl->kick + +Kicks a player out of the game. This destroys the connection. + +=cut + +sub kick { + my ($pl, $kicker) = @_; + + $pl->invoke (cf::EVENT_PLAYER_KICK, $kicker); + $pl->killer ("kicked"); + $pl->ns->destroy; +} + =item cf::player::list_logins Returns am arrayref of all valid playernames in the system, can take a @@ -1065,7 +1350,7 @@ =cut sub list_logins { - my $dirs = aio_readdir cf::localdir . "/" . cf::playerdir + my $dirs = aio_readdir $PLAYERDIR or return []; my @logins; @@ -1110,24 +1395,111 @@ \@paths } -=item $player->ext_reply ($msgid, $msgtype, %msg) +=item $protocol_xml = $player->expand_cfpod ($crossfire_pod) + +Expand crossfire pod fragments into protocol xml. + +=cut + +sub expand_cfpod { + ((my $self), (local $_)) = @_; + + # escape & and < + s/&/&/g; + s/(?, I<>, U<> etc. + s/B<([^\>]*)>/$1<\/b>/ + || s/I<([^\>]*)>/$1<\/i>/ + || s/U<([^\>]*)>/$1<\/u>/ + # replace G tags + || s{G<([^>|]*)\|([^>]*)>}{ + $self->gender ? $2 : $1 + }ge + # replace H + || s{H<([^\>]*)>} + { + ("[$1 (Use hintmode to suppress hints)]", + "[Hint suppressed, see hintmode]", + "") + [$self->{hintmode}] + }ge; + + # 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 + + $_ +} + +sub hintmode { + $_[0]{hintmode} = $_[1] if @_ > 1; + $_[0]{hintmode} +} + +=item $player->ext_reply ($msgid, %msg) Sends an ext reply to the player. =cut -sub ext_reply($$$%) { +sub ext_reply($$%) { my ($self, $id, %msg) = @_; $msg{msgid} = $id; + $self->send ("ext " . $self->ns->{json_coder}->encode (\%msg)); +} - $self->send ("ext " . cf::to_json \%msg); +=item $player->ext_event ($type, %msg) + +Sends an ext event to the client. + +=cut + +sub ext_event($$%) { + my ($self, $type, %msg) = @_; + + $self->ns->ext_event ($type, %msg); } -package cf; +=head3 cf::region -=back +=over 4 + +=cut + +package cf::region; + +=item cf::region::find_by_path $path + +Tries to decuce the likely region for a map knowing only its path. + +=cut + +sub find_by_path($) { + my ($path) = @_; + + my ($match, $specificity); + + for my $region (list) { + if ($region->match && $path =~ $region->match) { + ($match, $specificity) = ($region, $region->specificity) + if $region->specificity > $specificity; + } + } + + $match +} +=back =head3 cf::map @@ -1192,8 +1564,6 @@ # we have to keep some variables in memory intact local $self->{path}; local $self->{load_path}; - local $self->{deny_save}; - local $self->{deny_reset}; $self->SUPER::thawer_merge ($merge); } @@ -1275,7 +1645,7 @@ sub load_path { my ($self) = @_; - sprintf "%s/%s/%s.map", cf::datadir, cf::mapdir, $self->{path} + "$MAPDIR/$self->{path}.map" } # the temporary/swap location @@ -1283,7 +1653,7 @@ my ($self) = @_; (my $path = $_[0]{path}) =~ s/\//$PATH_SEP/g; - sprintf "%s/%s/%s.map", cf::localdir, cf::tmpdir, $path + "$TMPDIR/$path.map" } # the unique path, undef == no special unique path @@ -1291,7 +1661,7 @@ my ($self) = @_; (my $path = $_[0]{path}) =~ s/\//$PATH_SEP/g; - sprintf "%s/%s/%s", cf::localdir, cf::uniquedir, $path + "$UNIQUEDIR/$path" } # and all this just because we cannot iterate over @@ -1303,17 +1673,20 @@ for grep $_->outdoor, values %cf::MAP; } +sub decay_objects { + my ($self) = @_; + + return if $self->{deny_reset}; + + $self->do_decay_objects; +} + sub unlink_save { my ($self) = @_; utf8::encode (my $save = $self->save_path); IO::AIO::aioreq_pri 4; Coro::AIO::aio_unlink $save; IO::AIO::aioreq_pri 4; Coro::AIO::aio_unlink "$save.pst"; - - #d#TODO remove .map and also nuke - $save =~ s/\.map// or return;#d# - IO::AIO::aioreq_pri 4; Coro::AIO::aio_unlink $save;#d# - IO::AIO::aioreq_pri 4; Coro::AIO::aio_unlink "$save.pst";#d# } sub load_header_from($) { @@ -1373,6 +1746,12 @@ $self->prepare_orig; } + $self->{deny_reset} = 1 + if $self->no_reset; + + $self->default_region (cf::region::find_by_path $self->{path}) + unless $self->default_region; + 1 } @@ -1394,7 +1773,7 @@ $map->load_header or return; - if ($map->should_reset && 0) {#d#TODO# disabled, crashy (locking issue?) + if ($map->should_reset) {#d#TODO# disabled, crashy (locking issue?) # doing this can freeze the server in a sync job, obviously #$cf::WAIT_FOR_TICK->wait; $map->reset; @@ -1415,50 +1794,54 @@ local $self->{deny_reset} = 1; # loading can take a long time my $path = $self->{path}; - my $guard = cf::lock_acquire "map_load:$path"; - return if $self->in_memory != cf::MAP_SWAPPED; + { + my $guard = cf::lock_acquire "map_load:$path"; - $self->in_memory (cf::MAP_LOADING); + return if $self->in_memory != cf::MAP_SWAPPED; - $self->alloc; + $self->in_memory (cf::MAP_LOADING); - $self->pre_load; + $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}; + $self->pre_load; + Coro::cede; - if (my $uniq = $self->uniq_path) { - utf8::encode $uniq; - if (aio_open $uniq, O_RDONLY, 0) { - $self->clear_unique_items; - $self->_load_objects ($uniq, 0); - } - } + $self->_load_objects ($self->{load_path}, 1) + or return; - Coro::cede; + $self->set_object_flag (cf::FLAG_OBJ_ORIGINAL, 1) + if delete $self->{load_original}; - # now do the right thing for maps - $self->link_multipart_objects; + if (my $uniq = $self->uniq_path) { + utf8::encode $uniq; + if (aio_open $uniq, O_RDONLY, 0) { + $self->clear_unique_items; + $self->_load_objects ($uniq, 0); + } + } - unless ($self->{deny_activate}) { - $self->decay_objects; - $self->fix_auto_apply; - $self->update_buttons; Coro::cede; - $self->set_darkness_map; + # now do the right thing for maps + $self->link_multipart_objects; $self->difficulty ($self->estimate_difficulty) unless $self->difficulty; Coro::cede; - $self->activate; + + unless ($self->{deny_activate}) { + $self->decay_objects; + $self->fix_auto_apply; + $self->update_buttons; + Coro::cede; + $self->set_darkness_map; + Coro::cede; + $self->activate; + } + + $self->in_memory (cf::MAP_IN_MEMORY); } $self->post_load; - - $self->in_memory (cf::MAP_IN_MEMORY); } sub customise_for { @@ -1467,6 +1850,9 @@ return find "~" . $ob->name . "/" . $self->{path} if $self->per_player; +# return find "?party/" . $ob->name . "/" . $self->{path} +# if $self->per_party; + $self } @@ -1586,7 +1972,6 @@ my ($self) = @_; # TODO: safety, remove and allow resettable per-player maps - return 1e99 if $self->isa ("ext::map_per_player");#d# return 1e99 if $self->{deny_reset}; my $time = $self->fixed_resettime ? $self->{instantiate_time} : $self->last_access; @@ -1607,13 +1992,21 @@ my $lock = cf::lock_acquire "map_data:$self->{path}"; return if $self->players; - return if $self->isa ("ext::map_per_player");#d# - warn "resetting map ", $self->path;#d# + warn "resetting map ", $self->path; + + $self->in_memory (cf::MAP_SWAPPED); + + # need to save uniques path + unless ($self->{deny_save}) { + my $uniq = $self->uniq_path; utf8::encode $uniq; + + $self->_save_objects ($uniq, cf::IO_UNIQUES) + if $uniq; + } delete $cf::MAP{$self->path}; - $self->in_memory (cf::MAP_SWAPPED); $self->clear; $_->clear_links_to ($self) for values %cf::MAP; @@ -1642,28 +2035,58 @@ $self->reset; # polite request, might not happen } -=item cf::map::unique_maps +=item $maps = cf::map::tmp_maps -Returns an arrayref of paths of all shared maps that have -instantiated unique items. May block. +Returns an arrayref with all map paths of currently instantiated and saved +maps. May block. =cut -sub unique_maps() { - my $files = aio_readdir cf::localdir . "/" . cf::uniquedir - or return; +sub tmp_maps() { + [ + map { + utf8::decode $_; + /\.map$/ + ? normalise $_ + : () + } @{ aio_readdir $TMPDIR or [] } + ] +} - my @paths; +=item $maps = cf::map::random_maps - for (@$files) { - utf8::decode $_; - next if /\.pst$/; - next unless /^$PATH_SEP/o; +Returns an arrayref with all map paths of currently instantiated and saved +random maps. May block. - push @paths, cf::map::normalise $_; - } +=cut - \@paths +sub random_maps() { + [ + map { + utf8::decode $_; + /\.map$/ + ? normalise "?random/$_" + : () + } @{ aio_readdir $RANDOMDIR or [] } + ] +} + +=item cf::map::unique_maps + +Returns an arrayref of paths of all shared maps that have +instantiated unique items. May block. + +=cut + +sub unique_maps() { + [ + map { + utf8::decode $_; + /\.map$/ + ? normalise $_ + : () + } @{ aio_readdir $UNIQUEDIR or [] } + ] } package cf; @@ -1719,9 +2142,23 @@ if ($self->{record_replies}) { push @{ $self->{record_replies} }, [$npc, $msg, $flags]; + } else { - $msg = $npc->name . " says: $msg" if $npc; - $self->message ($msg, $flags); + my $pl = $self->contr; + + if ($pl->{npc_dialog} && $pl->{npc_dialog}->{id}) { + my $diag = $pl->{npc_dialog}; + $diag->{pl}->ext_reply ( + $diag->{id}, + msgtype => "reply", + msg => $diag->{pl}->expand_cfpod ($msg), + add_topics => [] + ); + + } else { + $msg = $npc->name . " says: $msg" if $npc; + $self->message ($msg, $flags); + } } } @@ -1779,7 +2216,7 @@ return if UNIVERSAL::isa $self->map, "ext::map_link"; $self->{_link_pos} ||= [$self->map->{path}, $self->x, $self->y] - if $self->map; + if $self->map && $self->map->{path} ne "{link}"; $self->enter_map ($LINK_MAP || link_map, 10, 10); } @@ -1787,6 +2224,8 @@ sub cf::object::player::leave_link { my ($self, $map, $x, $y) = @_; + return unless $self->contr->active; + my $link_pos = delete $self->{_link_pos}; unless ($map) { @@ -1813,59 +2252,53 @@ 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); } -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; - (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 - $pl->ob->reply (undef, - "There was an internal problem at your last logout, " - . "the server will try to bring you to your intended destination in a second.", - cf::NDI_RED); - Coro::Timer::sleep 1; - $pl->ob->leave_link; - })->prio (2); - } - }, -); +=item $player_object->goto ($path, $x, $y[, $check->($map)]) -=item $player_object->goto ($path, $x, $y) +Moves the player to the given map-path and coordinates by first freezing +her, loading and preparing them map, calling the provided $check callback +that has to return the map if sucecssful, and then unfreezes the player on +the new (success) or old (failed) map position. =cut +our $GOTOGEN; + sub cf::object::player::goto { - my ($self, $path, $x, $y) = @_; + my ($self, $path, $x, $y, $check) = @_; + + # do generation counting so two concurrent goto's will be executed in-order + my $gen = $self->{_goto_generation} = ++$GOTOGEN; $self->enter_link; (async { my $map = eval { my $map = cf::map::find $path; - $map = $map->customise_for ($self) if $map; + + if ($map) { + $map = $map->customise_for ($self); + $map = $check->($map) if $check && $map; + } else { + $self->message ("The exit to '$path' is closed", cf::NDI_UNIQUE | cf::NDI_RED); + } + $map - } or - $self->message ("The exit to '$path' is closed", cf::NDI_UNIQUE | cf::NDI_RED); + }; - $self->leave_link ($map, $x, $y); + if ($@) { + $self->message ("Something went wrong within the server, please report this incident!", cf::NDI_UNIQUE | cf::NDI_RED); + LOG llevError | logBacktrace, Carp::longmess $@; + } + + if ($gen == $self->{_goto_generation}) { + delete $self->{_goto_generation}; + $self->leave_link ($map, $x, $y); + } })->prio (1); } @@ -1914,7 +2347,7 @@ my $data = cf::to_json $rmp; my $md5 = Digest::MD5::md5_hex $data; - my $meta = "$cf::RANDOM_MAPS/$md5.meta"; + my $meta = "$RANDOMDIR/$md5.meta"; if (my $fh = aio_open "$meta~", O_WRONLY | O_CREAT, 0666) { aio_write $fh, 0, (length $data), $data, 0; @@ -1933,6 +2366,8 @@ if ($exit->slaying eq "/!") { #TODO: this should de-fi-ni-te-ly not be a sync-job + # the problem is that $exit might not survive long enough + # so it needs to be done right now, right here cf::sync_job { prepare_random_map $exit }; } @@ -1942,6 +2377,10 @@ $self->enter_link; + # if exit is damned, update players death & WoR home-position + $self->contr->savebed ($slaying, $hp, $sp) + if $exit->flag (FLAG_DAMNED); + (async { $self->deactivate_recursive; # just to be sure unless (eval { @@ -1950,9 +2389,9 @@ 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); + . "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; @@ -1975,9 +2414,61 @@ my ($self, $text, $flags) = @_; utf8::encode $text; - $self->send_packet (sprintf "drawinfo %d %s", $flags, $text); + $self->send_packet (sprintf "drawinfo %d %s", $flags || cf::NDI_BLACK, $text); } +=item $client->send_msg ($color, $type, $msg, [extra...]) + +Send a drawinfo or msg packet to the client, formatting the msg for the +client if neccessary. C<$type> should be a string identifying the type of +the message, with C being the default. If C<$color> is negative, suppress +the message unless the client supports the msg packet. + +=cut + +sub cf::client::send_msg { + my ($self, $color, $type, $msg, @extra) = @_; + + $msg = $self->pl->expand_cfpod ($msg); + + return unless @extra || length $msg; + + if ($self->can_msg) { + $self->send_packet ("msg " . $self->{json_coder}->encode ([$color, $type, $msg, @extra])); + } else { + # 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]/; + } + + if ($color >= 0) { + if (0 && $msg =~ /\[/) { + $self->send_packet ("drawextinfo $color 4 0 $msg") + } else { + $msg =~ s/\[\/?(?:b|i|u|fixed|color)[^\]]*\]//g; + $self->send_packet ("drawinfo $color $msg") + } + } + } +} + +=item $client->ext_event ($type, %msg) + +Sends an ext event to the client. + +=cut + +sub cf::client::ext_event($$%) { + my ($self, $type, %msg) = @_; + + $msg{msgtype} = "event_$type"; + $self->send_packet ("ext " . $self->{json_coder}->encode (\%msg)); +} =item $success = $client->query ($flags, "text", \&cb) @@ -1985,8 +2476,8 @@ the reply text on a reply. flags can be C, C or C or C<0>. -Queries can fail, so check the return code. Or don't, as queries will become -reliable at some point in the future. +Queries can fail, so check the return code. Or don't, as queries will +become reliable at some point in the future. =cut @@ -2004,9 +2495,16 @@ $self->send_packet ($self->{query_queue}[0][0]) if @{ $self->{query_queue} } == 1; + + 1 } cf::client->attach ( + on_connect => sub { + my ($ns) = @_; + + $ns->{json_coder} = JSON::XS->new->utf8->max_size (1e6)->convert_blessed; + }, on_reply => sub { my ($ns, $msg) = @_; @@ -2028,6 +2526,24 @@ } } }, + on_exticmd => sub { + my ($ns, $buf) = @_; + + my $msg = eval { $ns->{json_coder}->decode ($buf) }; + + if (ref $msg) { + if (my $cb = $EXTICMD{$msg->{msgtype}}) { + if (my %reply = $cb->($ns, $msg)) { + $reply{msgid} = $msg->{msgid}; + $ns->send ("ext " . $ns->{json_coder}->encode (\%reply)); + } + } + } else { + warn "client " . ($ns->pl ? $ns->pl->ob->name : $ns->host) . " sent unparseable exti message: <$buf>\n"; + } + + cf::override; + }, ); =item $client->async (\&cb) @@ -2084,17 +2600,26 @@ =pod -The following fucntions and emthods are available within a safe environment: +The following functions and methods are available within a safe environment: + + cf::object + contr pay_amount pay_player map x y force_find force_add + insert remove + + cf::object::player + player - cf::object contr pay_amount pay_player map - cf::object::player player - cf::player peaceful - cf::map trigger + cf::player + peaceful + + cf::map + trigger =cut for ( - ["cf::object" => qw(contr pay_amount pay_player map)], + ["cf::object" => qw(contr pay_amount pay_player map force_find force_add x y + insert remove)], ["cf::object::player" => qw(player)], ["cf::player" => qw(peaceful)], ["cf::map" => qw(trigger)], @@ -2175,87 +2700,103 @@ =cut ############################################################################# +# the server's init and main functions -=head2 EXTENSION DATABASE SUPPORT +sub load_facedata($) { + my ($path) = @_; -Crossfire maintains a very simple database for extension use. It can -currently store anything that can be serialised using Storable, which -excludes objects. + warn "loading facedata from $path\n"; -The parameter C<$family> should best start with the name of the extension -using it, it should be unique. + my $facedata; + 0 < aio_load $path, $facedata + or die "$path: $!"; -=over 4 + $facedata = Coro::Storable::thaw $facedata; -=item $value = cf::db_get $family => $key + $facedata->{version} == 2 + or cf::cleanup "$path: version mismatch, cannot proceed."; -Returns a single value from the database. + { + my $faces = $facedata->{faceinfo}; -=item cf::db_put $family => $key => $value + while (my ($face, $info) = each %$faces) { + my $idx = (cf::face::find $face) || cf::face::alloc $face; + cf::face::set $idx, $info->{visibility}, $info->{magicmap}; + cf::face::set_data $idx, 0, $info->{data32}, $info->{chksum32}; + cf::face::set_data $idx, 1, $info->{data64}, $info->{chksum64}; + Coro::cede; + } -Stores the given C<$value> in the family. + while (my ($face, $info) = each %$faces) { + next unless $info->{smooth}; + my $idx = cf::face::find $face + or next; + if (my $smooth = cf::face::find $info->{smooth}) { + cf::face::set_smooth $idx, $smooth, $info->{smoothlevel}; + } else { + warn "smooth face '$info->{smooth}' not found for face '$face'"; + } + Coro::cede; + } + } -=cut + { + my $anims = $facedata->{animinfo}; -our $DB; + while (my ($anim, $info) = each %$anims) { + cf::anim::set $anim, $info->{frames}, $info->{facings}; + Coro::cede; + } -sub db_load() { - unless ($DB) { - $DB = BDB::db_create $DB_ENV; + cf::anim::invalidate_all; # d'oh + } - cf::sync_job { - eval { - $DB->set_flags (BDB::CHKSUM); + 1 +} - BDB::db_open $DB, undef, "db", undef, BDB::BTREE, - BDB::CREATE | BDB::AUTO_COMMIT, 0666; - cf::cleanup "db_open(db): $!" if $!; - }; - cf::cleanup "db_open(db): $@" if $@; - }; +sub reload_regions { + load_resource_file "$MAPDIR/regions" + or die "unable to load regions file\n"; +} - my $path = cf::localdir . "/database.pst"; - if (stat $path) { - cf::sync_job { - my $pst = Storable::retrieve $path; - - cf::db_put (board => data => $pst->{board}); - cf::db_put (guildrules => data => $pst->{guildrules}); - cf::db_put (rent => balance => $pst->{rent}{balance}); - BDB::db_env_txn_checkpoint $DB_ENV; +sub reload_facedata { + load_facedata "$DATADIR/facedata" + or die "unable to load facedata\n"; +} - unlink $path; - }; - } - } +sub reload_archetypes { + load_resource_file "$DATADIR/archetypes" + or die "unable to load archetypes\n"; + #d# NEED to laod twice to resolve forward references + # this really needs to be done in an extra post-pass + # (which needs to be synchronous, so solve it differently) + load_resource_file "$DATADIR/archetypes" + or die "unable to load archetypes\n"; } -sub db_get($$) { - my $key = "$_[0]/$_[1]"; +sub reload_treasures { + load_resource_file "$DATADIR/treasures" + or die "unable to load treasurelists\n"; +} - cf::sync_job { - BDB::db_get $DB, undef, $key, my $data; +sub reload_resources { + warn "reloading resource files...\n"; - $! ? () - : Compress::LZF::sthaw $data - } -} + reload_regions; + reload_facedata; + #reload_archetypes;#d# + reload_archetypes; + reload_treasures; -sub db_put($$$) { - BDB::dbreq_pri 4; - BDB::db_put $DB, undef, "$_[0]/$_[1]", Compress::LZF::sfreeze_cr $_[2], 0, sub { }; + warn "finished reloading resource files\n"; } -############################################################################# -# the server's init and main functions - -sub load_resources { - load_regions sprintf "%s/%s/regions", cf::datadir, cf::mapdir - or die "unable to load regions file\n";#d# +sub init { + reload_resources; } sub cfg_load { - open my $fh, "<:utf8", cf::confdir . "/config" + open my $fh, "<:utf8", "$CONFDIR/config" or return; local $/; @@ -2275,10 +2816,6 @@ } } -sub init { - load_resources; -} - sub main { # we must not ever block the main coroutine local $Coro::idle = sub { @@ -2289,7 +2826,7 @@ }; cfg_load; - db_load; + db_init; load_extensions; $TICK_WATCHER->start; @@ -2314,6 +2851,44 @@ } } +sub write_runtime { + my $runtime = "$LOCALDIR/runtime"; + + # 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; + + my $guard = cf::lock_acquire "write_runtime"; + + my $fh = aio_open "$runtime~", O_WRONLY | O_CREAT, 0644 + or return; + + my $value = $cf::RUNTIME + 90 + 10; + # 10 is the runtime save interval, for a monotonic clock + # 60 allows for the watchdog to kill the server. + + (aio_write $fh, 0, (length $value), $value, 0) <= 0 + and return; + + # always fsync - this file is important + aio_fsync $fh + and return; + + # touch it again to show we are up-to-date + aio_utime $fh, undef, undef; + + close $fh + or return; + + aio_rename "$runtime~", $runtime + and return; + + warn "runtime file written.\n"; + + 1 +} + sub emergency_save() { my $freeze_guard = cf::freeze_mainloop; @@ -2347,7 +2922,14 @@ warn "leave emergency perl save\n"; } -sub reload() { +sub post_cleanup { + my ($make_core) = @_; + + warn Carp::longmess "post_cleanup backtrace" + if $make_core; +} + +sub do_reload_perl() { # can/must only be called in main if ($Coro::current != $Coro::main) { warn "can only reload from main coroutine"; @@ -2356,12 +2938,16 @@ warn "reloading..."; - warn "cancelling server ticker"; - $TICK_WATCHER->cancel; + warn "entering sync_job"; + + cf::sync_job { + cf::write_runtime; # external watchdog should not bark + cf::emergency_save; + cf::write_runtime; # external watchdog should not bark - cf::emergency_save; + warn "syncing database to disk"; + BDB::db_env_txn_checkpoint $DB_ENV; - eval { # if anything goes wrong in here, we should simply crash as we already saved warn "cancelling all WF_AUTOCANCEL watchers"; @@ -2369,9 +2955,6 @@ $_->cancel if $_->data & WF_AUTOCANCEL; } - warn "syncing database to disk"; - BDB::db_env_txn_checkpoint $DB_ENV, 0, 0, 0, sub { }; - warn "flushing outstanding aio requests"; for (;;) { BDB::flush; @@ -2381,6 +2964,8 @@ warn "iterate..."; } + ++$RELOAD; + warn "cancelling all extension coros"; $_->cancel for values %EXT_CORO; %EXT_CORO = (); @@ -2388,8 +2973,9 @@ warn "removing commands"; %COMMAND = (); - warn "removing ext commands"; - %EXTCMD = (); + warn "removing ext/exti commands"; + %EXTCMD = (); + %EXTICMD = (); warn "unloading/nuking all extensions"; for my $pkg (@EXTS) { @@ -2429,6 +3015,7 @@ 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 @@ -2442,53 +3029,52 @@ warn "loading config and database again"; cf::cfg_load; - cf::db_load; warn "loading extensions"; cf::load_extensions; warn "reattaching attachments to objects/players"; - _global_reattach; + _global_reattach; # objects, sockets warn "reattaching attachments to maps"; reattach $_ for values %MAP; + warn "reattaching attachments to players"; + reattach $_ for values %PLAYER; - warn "loading reloadable resources"; - load_resources; - - warn "restarting server ticker"; + warn "leaving sync_job"; - $TICK_WATCHER->start; - }; - - if ($@) { + 1 + } or do { warn $@; warn "error while reloading, exiting."; exit 1; - } + }; warn "reloaded"; }; our $RELOAD_WATCHER; # used only during reload +sub reload_perl() { + # doing reload synchronously and two reloads happen back-to-back, + # coro crashes during coro_state_free->destroy here. + + $RELOAD_WATCHER ||= Event->timer ( + reentrant => 0, + after => 0, + data => WF_AUTOCANCEL, + cb => sub { + do_reload_perl; + undef $RELOAD_WATCHER; + }, + ); +} + register_command "reload" => sub { my ($who, $arg) = @_; if ($who->flag (FLAG_WIZ)) { $who->message ("reloading server."); - - # doing reload synchronously and two reloads happen back-to-back, - # coro crashes during coro_state_free->destroy here. - - $RELOAD_WATCHER ||= Event->timer ( - reentrant => 0, - after => 0, - data => WF_AUTOCANCEL, - cb => sub { - reload; - undef $RELOAD_WATCHER; - }, - ); + async { reload_perl }; } }; @@ -2496,6 +3082,29 @@ my $bug_warning = 0; +our @WAIT_FOR_TICK; +our @WAIT_FOR_TICK_BEGIN; + +sub wait_for_tick { + return unless $TICK_WATCHER->is_active; + return if $Coro::current == $Coro::main; + + my $signal = new Coro::Signal; + push @WAIT_FOR_TICK, $signal; + $signal->wait; +} + +sub wait_for_tick_begin { + return unless $TICK_WATCHER->is_active; + return if $Coro::current == $Coro::main; + + my $signal = new Coro::Signal; + push @WAIT_FOR_TICK_BEGIN, $signal; + $signal->wait; +} + + my $min = 1e6;#d# + my $avg = 10; $TICK_WATCHER = Event->timer ( reentrant => 0, parked => 1, @@ -2509,26 +3118,68 @@ return; } - $NOW = Event::time; + $NOW = $tick_start = Event::time; cf::server_tick; # one server iteration + + 0 && sync_job {#d# + for(1..10) { + my $t = Event::time; + my $map = my $map = new_from_path cf::map "/tmp/x.map" + or die; + + $map->width (50); + $map->height (50); + $map->alloc; + $map->_load_objects ("/tmp/x.map", 1); + my $t = Event::time - $t; + + #next unless $t < 0.0013;#d# + if ($t < $min) { + $min = $t; + } + $avg = $avg * 0.99 + $t * 0.01; + } + warn "XXXXXXXXXXXXXXXXXX min $min avg $avg\n";#d# + exit 0; + # 2007-05-22 02:33:04.569 min 0.00112509727478027 avg 0.0012259249572477 + }; + $RUNTIME += $TICK; $NEXT_TICK += $TICK; - $WAIT_FOR_TICK->broadcast; - $WAIT_FOR_TICK_ONE->send if $WAIT_FOR_TICK_ONE->awaited; + if ($NOW >= $NEXT_RUNTIME_WRITE) { + $NEXT_RUNTIME_WRITE = $NOW + 10; + Coro::async_pool { + write_runtime + or warn "ERROR: unable to write runtime file: $!"; + }; + } - Event::sweep; - Coro::cede_notself; - # my $AFTER = Event::time; # warn $AFTER - $NOW;#d# + if (my $sig = shift @WAIT_FOR_TICK_BEGIN) { + $sig->send; + } + while (my $sig = shift @WAIT_FOR_TICK) { + $sig->send; + } + + $NOW = Event::time; + # if we are delayed by four ticks or more, skip them all - $NEXT_TICK = Event::time if Event::time >= $NEXT_TICK + $TICK * 4; + $NEXT_TICK = $NOW if $NOW >= $NEXT_TICK + $TICK * 4; $TICK_WATCHER->at ($NEXT_TICK); $TICK_WATCHER->start; + + $LOAD = ($NOW - $tick_start) / $TICK; + $LOADAVG = $LOADAVG * 0.75 + $LOAD * 0.25; + + _post_tick; + + }, ); @@ -2566,12 +3217,12 @@ eval { BDB::db_env_open $DB_ENV, - $BDB_ENV_DIR, + $BDBDIR, BDB::INIT_LOCK | BDB::INIT_LOG | BDB::INIT_MPOOL | BDB::INIT_TXN | BDB::RECOVER | BDB::REGISTER | BDB::USE_ENVIRON | BDB::CREATE, 0666; - cf::cleanup "db_env_open($BDB_ENV_DIR): $!" if $!; + cf::cleanup "db_env_open($BDBDIR): $!" if $!; $DB_ENV->set_flags (BDB::AUTO_COMMIT | BDB::REGION_INIT | BDB::TXN_NOSYNC, 1); $DB_ENV->set_lk_detect; @@ -2589,25 +3240,54 @@ IO::AIO::max_poll_time $TICK * 0.1; $AIO_POLL_WATCHER = Event->io ( reentrant => 0, + data => WF_AUTOCANCEL, fd => IO::AIO::poll_fileno, poll => 'r', prio => 6, - data => WF_AUTOCANCEL, cb => \&IO::AIO::poll_cb, ); } -$WRITE_RUNTIME_WATCHER = Event->timer ( - reentrant => 0, - data => WF_AUTOCANCEL, - after => 1, - interval => 10, - prio => 6, # keep it lowest so it acts like a watchdog - cb => Coro::unblock_sub { - write_runtime - or warn "ERROR: unable to write runtime file: $!"; - }, -); +my $_log_backtrace; + +sub _log_backtrace { + my ($msg, @addr) = @_; + + $msg =~ s/\n//; + + # limit the # of concurrent backtraces + if ($_log_backtrace < 2) { + ++$_log_backtrace; + async { + my @bt = fork_call { + @addr = map { sprintf "%x", $_ } @addr; + my $self = (-f "/proc/$$/exe") ? "/proc/$$/exe" : $^X; + open my $fh, "exec addr2line -C -f -i -e \Q$self\E @addr 2>&1 |" + or die "addr2line: $!"; + + my @funcs; + my @res = <$fh>; + chomp for @res; + while (@res) { + my ($func, $line) = splice @res, 0, 2, (); + push @funcs, "[$func] $line"; + } + + @funcs + }; + + LOG llevInfo, "[ABT] $msg\n"; + LOG llevInfo, "[ABT] $_\n" for @bt; + --$_log_backtrace; + }; + } else { + LOG llevInfo, "[ABT] $msg\n"; + LOG llevInfo, "[ABT] [suppressed]\n"; + } +} + +# load additional modules +use cf::pod; END { cf::emergency_save }